Skip to content

Commit 971981e

Browse files
atscottthePunderWoman
authored andcommitted
fix(core): TestBed.tick should ensure test components are synchronized (#61382)
This ensures that `TestBed.tick` updates any components created with `TestBed.createComponent`, regardless of whether autoDetectChanges is on. PR Close #61382
1 parent 127bad7 commit 971981e

File tree

6 files changed

+99
-32
lines changed

6 files changed

+99
-32
lines changed

packages/core/src/application/application_ref.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -318,12 +318,17 @@ export class ApplicationRef {
318318

319319
// Needed for ComponentFixture temporarily during migration of autoDetect behavior
320320
// Eventually the hostView of the fixture should just attach to ApplicationRef.
321-
private externalTestViews: Set<InternalViewRef<unknown>> = new Set();
321+
private allTestViews: Set<InternalViewRef<unknown>> = new Set();
322+
private autoDetectTestViews: Set<InternalViewRef<unknown>> = new Set();
323+
private includeAllTestViews = false;
322324
/** @internal */
323325
afterTick = new Subject<void>();
324326
/** @internal */
325327
get allViews(): Array<InternalViewRef<unknown>> {
326-
return [...this.externalTestViews.keys(), ...this._views];
328+
return [
329+
...(this.includeAllTestViews ? this.allTestViews : this.autoDetectTestViews).keys(),
330+
...this._views,
331+
];
327332
}
328333

329334
/**

packages/core/src/change_detection/scheduling/ng_zone_scheduling.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import {Subscription} from 'rxjs';
1010

11-
import {ApplicationRef} from '../../application/application_ref';
11+
import {ApplicationRef, ApplicationRefDirtyFlags} from '../../application/application_ref';
1212
import {
1313
ENVIRONMENT_INITIALIZER,
1414
EnvironmentInjector,
@@ -58,7 +58,8 @@ export class NgZoneChangeDetectionScheduler {
5858
}
5959
this.zone.run(() => {
6060
try {
61-
this.applicationRef.tick();
61+
this.applicationRef.dirtyFlags |= ApplicationRefDirtyFlags.ViewTreeGlobal;
62+
this.applicationRef._tick();
6263
} catch (e) {
6364
this.applicationErrorHandler(e);
6465
}

packages/core/test/render3/reactivity_spec.ts

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -806,10 +806,8 @@ describe('reactivity', () => {
806806
}
807807
}
808808

809-
const fixture = TestBed.createComponent(TestCmp);
809+
TestBed.createComponent(TestCmp);
810810
TestBed.tick();
811-
expect(log).toEqual([]);
812-
fixture.detectChanges();
813811
expect(log).toEqual(['init', 'effect']);
814812
});
815813

@@ -879,17 +877,17 @@ describe('reactivity', () => {
879877
vcr = inject(ViewContainerRef);
880878
}
881879

882-
const fixture = TestBed.createComponent(DriverCmp);
883-
fixture.detectChanges();
880+
const componentRef = createComponent(DriverCmp, {
881+
environmentInjector: TestBed.inject(EnvironmentInjector),
882+
});
883+
componentRef.changeDetectorRef.detectChanges();
884884

885-
fixture.componentInstance.vcr.createComponent(TestCmp);
885+
componentRef.instance.vcr.createComponent(TestCmp);
886886

887887
// Verify that simply creating the component didn't schedule the effect.
888-
TestBed.tick();
888+
TestBed.inject(ApplicationRef).tick();
889889
expect(log).toEqual([]);
890-
891-
// Running change detection should schedule and run the effect.
892-
fixture.detectChanges();
890+
componentRef.changeDetectorRef.detectChanges();
893891
expect(log).toEqual(['init', 'effect']);
894892
});
895893

@@ -918,8 +916,6 @@ describe('reactivity', () => {
918916

919917
const fixture = TestBed.createComponent(TestCmp);
920918
TestBed.tick();
921-
expect(log).toEqual([]);
922-
fixture.detectChanges();
923919
expect(log).toEqual(['init', 'effect']);
924920
});
925921

packages/core/test/test_bed_spec.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ import {
4040
ɵɵsetNgModuleScope as setNgModuleScope,
4141
ɵɵtext as text,
4242
DOCUMENT,
43+
signal,
44+
provideZonelessChangeDetection,
4345
} from '../src/core';
4446
import {DeferBlockBehavior} from '../testing';
4547
import {TestBed, TestBedImpl} from '../testing/src/test_bed';
@@ -50,6 +52,7 @@ import {NgModuleType} from '../src/render3';
5052
import {depsTracker} from '../src/render3/deps_tracker/deps_tracker';
5153
import {setClassMetadataAsync} from '../src/render3/metadata';
5254
import {
55+
ComponentFixtureAutoDetect,
5356
TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT,
5457
THROW_ON_UNKNOWN_ELEMENTS_DEFAULT,
5558
THROW_ON_UNKNOWN_PROPERTIES_DEFAULT,
@@ -2273,6 +2276,58 @@ describe('TestBed', () => {
22732276

22742277
expect(TestBed.runInInjectionContext(functionThatUsesInject)).toEqual(expectedValue);
22752278
});
2279+
2280+
describe('TestBed.tick', () => {
2281+
@Component({
2282+
template: '{{state()}}',
2283+
})
2284+
class Thing1 {
2285+
state = signal(1);
2286+
}
2287+
2288+
describe('with zone change detection', () => {
2289+
it('should update fixtures with autoDetect', () => {
2290+
TestBed.configureTestingModule({
2291+
providers: [{provide: ComponentFixtureAutoDetect, useValue: true}],
2292+
});
2293+
const {nativeElement, componentInstance} = TestBed.createComponent(Thing1);
2294+
expect(nativeElement.textContent).toBe('1');
2295+
2296+
componentInstance.state.set(2);
2297+
TestBed.tick();
2298+
expect(nativeElement.textContent).toBe('2');
2299+
});
2300+
2301+
it('should update fixtures without autoDetect', () => {
2302+
const {nativeElement, componentInstance} = TestBed.createComponent(Thing1);
2303+
expect(nativeElement.textContent).toBe(''); // change detection didn't run yet
2304+
2305+
componentInstance.state.set(2);
2306+
TestBed.tick();
2307+
expect(nativeElement.textContent).toBe('2');
2308+
});
2309+
});
2310+
2311+
describe('with zoneless change detection', () => {
2312+
beforeEach(() => {
2313+
TestBed.configureTestingModule({
2314+
providers: [provideZonelessChangeDetection()],
2315+
});
2316+
});
2317+
2318+
it('should update fixtures with zoneless', async () => {
2319+
const fixture = TestBed.createComponent(Thing1);
2320+
await fixture.whenStable();
2321+
2322+
const {nativeElement, componentInstance} = fixture;
2323+
expect(nativeElement.textContent).toBe('1');
2324+
2325+
componentInstance.state.set(2);
2326+
TestBed.tick();
2327+
expect(nativeElement.textContent).toBe('2');
2328+
});
2329+
});
2330+
});
22762331
});
22772332

22782333
describe('TestBed defer block behavior', () => {

packages/core/testing/src/component_fixture.ts

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,9 @@ import {DeferBlockFixture} from './defer';
3333
import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone} from './test_bed_common';
3434

3535
interface TestAppRef {
36-
externalTestViews: Set<ViewRef>;
37-
skipCheckNoChangesForExternalTestViews: Set<ViewRef>;
36+
allTestViews: Set<ViewRef>;
37+
includeAllTestViews: boolean;
38+
autoDetectTestViews: Set<ViewRef>;
3839
}
3940

4041
/**
@@ -106,13 +107,15 @@ export class ComponentFixture<T> {
106107
this.nativeElement = this.elementRef.nativeElement;
107108
this.componentRef = componentRef;
108109

110+
this._testAppRef.allTestViews.add(this.componentRef.hostView);
109111
if (this.autoDetect) {
110-
this._testAppRef.externalTestViews.add(this.componentRef.hostView);
112+
this._testAppRef.autoDetectTestViews.add(this.componentRef.hostView);
111113
this.scheduler?.notify(ɵNotificationSource.ViewAttached);
112114
this.scheduler?.notify(ɵNotificationSource.MarkAncestorsForTraversal);
113115
}
114116
this.componentRef.hostView.onDestroy(() => {
115-
this._testAppRef.externalTestViews.delete(this.componentRef.hostView);
117+
this._testAppRef.allTestViews.delete(this.componentRef.hostView);
118+
this._testAppRef.autoDetectTestViews.delete(this.componentRef.hostView);
116119
});
117120
// Create subscriptions outside the NgZone so that the callbacks run outside
118121
// of NgZone.
@@ -150,12 +153,10 @@ export class ComponentFixture<T> {
150153

151154
if (this.zonelessEnabled) {
152155
try {
153-
this._testAppRef.externalTestViews.add(this.componentRef.hostView);
156+
this._testAppRef.includeAllTestViews = true;
154157
this._appRef.tick();
155158
} finally {
156-
if (!this.autoDetect) {
157-
this._testAppRef.externalTestViews.delete(this.componentRef.hostView);
158-
}
159+
this._testAppRef.includeAllTestViews = false;
159160
}
160161
} else {
161162
// Run the change detection inside the NgZone so that any async tasks as part of the change
@@ -203,12 +204,10 @@ export class ComponentFixture<T> {
203204
throw new Error('Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set.');
204205
}
205206

206-
if (autoDetect !== this.autoDetect) {
207-
if (autoDetect) {
208-
this._testAppRef.externalTestViews.add(this.componentRef.hostView);
209-
} else {
210-
this._testAppRef.externalTestViews.delete(this.componentRef.hostView);
211-
}
207+
if (autoDetect) {
208+
this._testAppRef.autoDetectTestViews.add(this.componentRef.hostView);
209+
} else {
210+
this._testAppRef.autoDetectTestViews.delete(this.componentRef.hostView);
212211
}
213212

214213
this.autoDetect = autoDetect;
@@ -282,7 +281,8 @@ export class ComponentFixture<T> {
282281
*/
283282
destroy(): void {
284283
this.subscriptions.unsubscribe();
285-
this._testAppRef.externalTestViews.delete(this.componentRef.hostView);
284+
this._testAppRef.autoDetectTestViews.delete(this.componentRef.hostView);
285+
this._testAppRef.allTestViews.delete(this.componentRef.hostView);
286286
if (!this._isDestroyed) {
287287
this.componentRef.destroy();
288288
this._isDestroyed = true;

packages/core/testing/src/test_bed.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -826,7 +826,17 @@ export class TestBedImpl implements TestBed {
826826
* @publicApi
827827
*/
828828
tick(): void {
829-
this.inject(ApplicationRef).tick();
829+
const appRef = this.inject(ApplicationRef);
830+
try {
831+
// TODO(atscott): ApplicationRef.tick should set includeAllTestViews to true itself rather than doing this here and in ComponentFixture
832+
// The behavior should be that TestBed.tick, ComponentFixture.detectChanges, and ApplicationRef.tick all result in the test fixtures
833+
// getting synchronized, regardless of whether they are autoDetect: true.
834+
// Automatic scheduling (zone or zoneless) will call _tick which will _not_ include fixtures with autoDetect: false
835+
(appRef as any).includeAllTestViews = true;
836+
appRef.tick();
837+
} finally {
838+
(appRef as any).includeAllTestViews = false;
839+
}
830840
}
831841
}
832842

0 commit comments

Comments
 (0)