Skip to content

Commit 350776b

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 9d8a778 commit 350776b

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)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy