diff --git a/goldens/public-api/core/testing/index.md b/goldens/public-api/core/testing/index.md index 3b81a9a8dc8..914ac63e97d 100644 --- a/goldens/public-api/core/testing/index.md +++ b/goldens/public-api/core/testing/index.md @@ -31,9 +31,9 @@ export const __core_private_testing_placeholder__ = ""; export function async(fn: Function): (done: any) => any; // @public -export class ComponentFixture { +export abstract class ComponentFixture { constructor(componentRef: ComponentRef); - autoDetectChanges(autoDetect?: boolean): void; + abstract autoDetectChanges(autoDetect?: boolean): void; changeDetectorRef: ChangeDetectorRef; checkNoChanges(): void; componentInstance: T; @@ -41,15 +41,15 @@ export class ComponentFixture { componentRef: ComponentRef; debugElement: DebugElement; destroy(): void; - detectChanges(checkNoChanges?: boolean): void; + abstract detectChanges(checkNoChanges?: boolean): void; elementRef: ElementRef; getDeferBlocks(): Promise; - isStable(): boolean; + abstract isStable(): boolean; nativeElement: any; // (undocumented) ngZone: NgZone | null; whenRenderingDone(): Promise; - whenStable(): Promise; + abstract whenStable(): Promise; } // @public (undocumented) diff --git a/packages/core/test/change_detection_scheduler_spec.ts b/packages/core/test/change_detection_scheduler_spec.ts index 940b2f0f592..2f12fbab87e 100644 --- a/packages/core/test/change_detection_scheduler_spec.ts +++ b/packages/core/test/change_detection_scheduler_spec.ts @@ -8,30 +8,19 @@ import {AsyncPipe} from '@angular/common'; import {PLATFORM_BROWSER_ID} from '@angular/common/src/platform_id'; -import {ApplicationRef, ChangeDetectorRef, Component, ComponentRef, createComponent, DebugElement, destroyPlatform, ElementRef, EnvironmentInjector, ErrorHandler, getDebugNode, inject, Injectable, Input, NgZone, PLATFORM_ID, signal, TemplateRef, Type, ViewChild, ViewContainerRef, ɵprovideZonelessChangeDetection as provideZonelessChangeDetection} from '@angular/core'; +import {ApplicationRef, ChangeDetectorRef, Component, createComponent, destroyPlatform, ElementRef, EnvironmentInjector, ErrorHandler, inject, Input, PLATFORM_ID, signal, TemplateRef, Type, ViewChild, ViewContainerRef, ɵprovideZonelessChangeDetection as provideZonelessChangeDetection} from '@angular/core'; import {toSignal} from '@angular/core/rxjs-interop'; -import {TestBed} from '@angular/core/testing'; +import {ComponentFixture, TestBed} from '@angular/core/testing'; import {bootstrapApplication} from '@angular/platform-browser'; import {withBody} from '@angular/private/testing'; import {BehaviorSubject, firstValueFrom} from 'rxjs'; import {filter, take, tap} from 'rxjs/operators'; describe('Angular with NoopNgZone', () => { - function whenStable(applicationRef = TestBed.inject(ApplicationRef)): Promise { - return firstValueFrom(applicationRef.isStable.pipe(filter(stable => stable))); - } - - function isStable(injector = TestBed.inject(EnvironmentInjector)): boolean { - return toSignal(injector.get(ApplicationRef).isStable, {requireSync: true, injector})(); - } - - async function createAndAttachComponent(type: Type): Promise> { - const environmentInjector = TestBed.inject(EnvironmentInjector); - const component = createComponent(type, {environmentInjector}); - environmentInjector.get(ApplicationRef).attachView(component.hostView); - expect(isStable()).toBeFalse(); - await whenStable(); - return component; + async function createFixture(type: Type): Promise> { + const fixture = TestBed.createComponent(type); + await fixture.whenStable(); + return fixture; } describe('notifies scheduler', () => { @@ -45,21 +34,16 @@ describe('Angular with NoopNgZone', () => { class TestComponent { val = val; } - const environmentInjector = TestBed.inject(EnvironmentInjector); - const component = createComponent(TestComponent, {environmentInjector}); - const appRef = environmentInjector.get(ApplicationRef); - - appRef.attachView(component.hostView); - expect(isStable()).toBeFalse(); + const fixture = await createFixture(TestComponent); // Cause another pending CD immediately after render and verify app has not stabilized - await whenStable().then(() => { + await fixture.whenStable().then(() => { val.set('new'); }); - expect(isStable()).toBeFalse(); + expect(fixture.isStable()).toBeFalse(); - await whenStable(); - expect(isStable()).toBeTrue(); + await fixture.whenStable(); + expect(fixture.isStable()).toBeTrue(); }); it('when signal updates', async () => { @@ -69,13 +53,13 @@ describe('Angular with NoopNgZone', () => { val = val; } - const component = await createAndAttachComponent(TestComponent); - expect(component.location.nativeElement.innerText).toEqual('initial'); + const fixture = await createFixture(TestComponent); + expect(fixture.nativeElement.innerText).toEqual('initial'); val.set('new'); - expect(isStable()).toBeFalse(); - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('new'); + expect(fixture.isStable()).toBeFalse(); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('new'); }); it('when using markForCheck()', async () => { @@ -89,13 +73,13 @@ describe('Angular with NoopNgZone', () => { } } - const component = await createAndAttachComponent(TestComponent); - expect(component.location.nativeElement.innerText).toEqual('initial'); + const fixture = await createFixture(TestComponent); + expect(fixture.nativeElement.innerText).toEqual('initial'); - component.instance.setVal('new'); - expect(isStable()).toBe(false); - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('new'); + fixture.componentInstance.setVal('new'); + expect(fixture.isStable()).toBe(false); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('new'); }); it('on input binding', async () => { @@ -104,13 +88,13 @@ describe('Angular with NoopNgZone', () => { @Input() val = 'initial'; } - const component = await createAndAttachComponent(TestComponent); - expect(component.location.nativeElement.innerText).toEqual('initial'); + const fixture = await createFixture(TestComponent); + expect(fixture.nativeElement.innerText).toEqual('initial'); - component.setInput('val', 'new'); - expect(isStable()).toBe(false); - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('new'); + fixture.componentRef.setInput('val', 'new'); + expect(fixture.isStable()).toBe(false); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('new'); }); it('on event listener bound in template', async () => { @@ -123,15 +107,14 @@ describe('Angular with NoopNgZone', () => { } } - const component = await createAndAttachComponent(TestComponent); - expect(component.location.nativeElement.innerText).toEqual('initial'); + const fixture = await createFixture(TestComponent); + expect(fixture.nativeElement.innerText).toEqual('initial'); - getDebugElement(component) - .query(p => p.nativeElement.tagName === 'DIV') + fixture.debugElement.query(p => p.nativeElement.tagName === 'DIV') .triggerEventHandler('click'); - expect(isStable()).toBe(false); - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('new'); + expect(fixture.isStable()).toBe(false); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('new'); }); it('on event listener bound in host', async () => { @@ -144,13 +127,13 @@ describe('Angular with NoopNgZone', () => { } } - const component = await createAndAttachComponent(TestComponent); - expect(component.location.nativeElement.innerText).toEqual('initial'); + const fixture = await createFixture(TestComponent); + expect(fixture.nativeElement.innerText).toEqual('initial'); - getDebugElement(component).triggerEventHandler('click'); - expect(isStable()).toBe(false); - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('new'); + fixture.debugElement.triggerEventHandler('click'); + expect(fixture.isStable()).toBe(false); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('new'); }); it('with async pipe', async () => { @@ -159,13 +142,13 @@ describe('Angular with NoopNgZone', () => { val = new BehaviorSubject('initial'); } - const component = await createAndAttachComponent(TestComponent); - expect(component.location.nativeElement.innerText).toEqual('initial'); + const fixture = await createFixture(TestComponent); + expect(fixture.nativeElement.innerText).toEqual('initial'); - component.instance.val.next('new'); - expect(isStable()).toBe(false); - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('new'); + fixture.componentInstance.val.next('new'); + expect(fixture.isStable()).toBe(false); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('new'); }); it('when creating a view', async () => { @@ -182,12 +165,12 @@ describe('Angular with NoopNgZone', () => { } } - const component = await createAndAttachComponent(TestComponent); + const fixture = await createFixture(TestComponent); - component.instance.createView(); - expect(isStable()).toBe(false); - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('binding'); + fixture.componentInstance.createView(); + expect(fixture.isStable()).toBe(false); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('binding'); }); it('when inserting a view', async () => { @@ -206,14 +189,14 @@ describe('Angular with NoopNgZone', () => { @ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef; } - const componentRef = await createAndAttachComponent(TestComponent); + const fixture = await createFixture(TestComponent); const otherComponent = createComponent(DynamicCmp, {environmentInjector: TestBed.inject(EnvironmentInjector)}); - componentRef.instance.viewContainer.insert(otherComponent.hostView); - expect(isStable()).toBe(false); - await whenStable(); - expect(componentRef.location.nativeElement.innerText).toEqual('binding'); + fixture.componentInstance.viewContainer.insert(otherComponent.hostView); + expect(fixture.isStable()).toBe(false); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('binding'); }); it('when destroying a view (with animations)', async () => { @@ -232,28 +215,35 @@ describe('Angular with NoopNgZone', () => { @ViewChild('ref', {read: ViewContainerRef}) viewContainer!: ViewContainerRef; } - const fixture = await createAndAttachComponent(TestComponent); + const fixture = await createFixture(TestComponent); const component = createComponent(DynamicCmp, {environmentInjector: TestBed.inject(EnvironmentInjector)}); - fixture.instance.viewContainer.insert(component.hostView); - await whenStable(); - expect(fixture.location.nativeElement.innerText).toEqual('binding'); - fixture.instance.viewContainer.remove(); - await whenStable(); - expect(fixture.location.nativeElement.innerText).toEqual(''); + fixture.componentInstance.viewContainer.insert(component.hostView); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('binding'); + fixture.componentInstance.viewContainer.remove(); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual(''); const component2 = createComponent(DynamicCmp, {environmentInjector: TestBed.inject(EnvironmentInjector)}); - fixture.instance.viewContainer.insert(component2.hostView); - await whenStable(); - expect(fixture.location.nativeElement.innerText).toEqual('binding'); + fixture.componentInstance.viewContainer.insert(component2.hostView); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('binding'); component2.destroy(); - expect(isStable()).toBe(false); - await whenStable(); - expect(fixture.location.nativeElement.innerText).toEqual(''); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual(''); }); + function whenStable(applicationRef = TestBed.inject(ApplicationRef)): Promise { + return firstValueFrom(applicationRef.isStable.pipe(filter(stable => stable))); + } + + function isStable(injector = TestBed.inject(EnvironmentInjector)): boolean { + return toSignal(injector.get(ApplicationRef).isStable, {requireSync: true, injector})(); + } + it('when destroying a view (*no* animations)', withBody('', async () => { destroyPlatform(); @Component({ @@ -335,8 +325,8 @@ describe('Angular with NoopNgZone', () => { val = val; } - const component = await createAndAttachComponent(TestComponent); - expect(component.location.nativeElement.innerText).toEqual('initial'); + const fixture = await createFixture(TestComponent); + expect(fixture.nativeElement.innerText).toEqual('initial'); val.set('new'); await TestBed.inject(ApplicationRef) @@ -347,8 +337,8 @@ describe('Angular with NoopNgZone', () => { tap(() => val.set('newer')), ) .toPromise(); - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('newer'); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('newer'); }); }); @@ -379,23 +369,18 @@ describe('Angular with NoopNgZone', () => { ] }); - const component = await createAndAttachComponent(TestComponent); - expect(component.location.nativeElement.innerText).toEqual('initial'); + const fixture = await createFixture(TestComponent); + expect(fixture.nativeElement.innerText).toEqual('initial'); val.set('new'); throwError = true; // error is thrown in a timeout and can't really be "caught". // Still need to wrap in expect so it happens in the expect context and doesn't fail the test. - expect(async () => await whenStable()).not.toThrow(); - expect(component.location.nativeElement.innerText).toEqual('initial'); + expect(async () => await fixture.whenStable()).not.toThrow(); + expect(fixture.nativeElement.innerText).toEqual('initial'); throwError = false; - await whenStable(); - expect(component.location.nativeElement.innerText).toEqual('new'); + await fixture.whenStable(); + expect(fixture.nativeElement.innerText).toEqual('new'); }); }); - - -function getDebugElement(component: ComponentRef) { - return getDebugNode(component.location.nativeElement) as DebugElement; -} diff --git a/packages/core/testing/src/component_fixture.ts b/packages/core/testing/src/component_fixture.ts index c271f40fd6e..be60ae90f1f 100644 --- a/packages/core/testing/src/component_fixture.ts +++ b/packages/core/testing/src/component_fixture.ts @@ -6,11 +6,12 @@ * found in the LICENSE file at https://angular.io/license */ -import {ApplicationRef, ChangeDetectorRef, ComponentRef, DebugElement, ElementRef, getDebugNode, inject, NgZone, RendererFactory2, ɵDeferBlockDetails as DeferBlockDetails, ɵEffectScheduler as EffectScheduler, ɵgetDeferBlocks as getDeferBlocks, ɵNoopNgZone as NoopNgZone} from '@angular/core'; -import {Subscription} from 'rxjs'; +import {ApplicationRef, ChangeDetectorRef, ComponentRef, DebugElement, ElementRef, getDebugNode, inject, NgZone, RendererFactory2, ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵDeferBlockDetails as DeferBlockDetails, ɵEffectScheduler as EffectScheduler, ɵgetDeferBlocks as getDeferBlocks, ɵNoopNgZone as NoopNgZone, ɵPendingTasks as PendingTasks} from '@angular/core'; +import {firstValueFrom, Subscription} from 'rxjs'; +import {filter, take} from 'rxjs/operators'; import {DeferBlockFixture} from './defer'; -import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone} from './test_bed_common'; +import {AllowDetectChangesAndAcknowledgeItCanHideApplicationBugs, ComponentFixtureAutoDetect, ComponentFixtureNoNgZone} from './test_bed_common'; /** @@ -18,7 +19,7 @@ import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone} from './test_bed_c * * @publicApi */ -export class ComponentFixture { +export abstract class ComponentFixture { /** * The DebugElement associated with the root element of this component. */ @@ -45,25 +46,24 @@ export class ComponentFixture { changeDetectorRef: ChangeDetectorRef; private _renderer: RendererFactory2|null|undefined; - private _isStable: boolean = true; private _isDestroyed: boolean = false; - private _resolve: ((result: boolean) => void)|null = null; - private _promise: Promise|null = null; - private readonly noZoneOptionIsSet = inject(ComponentFixtureNoNgZone, {optional: true}); - private _ngZone: NgZone = this.noZoneOptionIsSet ? new NoopNgZone() : inject(NgZone); - private _autoDetect = inject(ComponentFixtureAutoDetect, {optional: true}) ?? false; - private effectRunner = inject(EffectScheduler); - private _subscriptions = new Subscription(); + /** @internal */ + protected readonly _noZoneOptionIsSet = inject(ComponentFixtureNoNgZone, {optional: true}); + /** @internal */ + protected _ngZone: NgZone = this._noZoneOptionIsSet ? new NoopNgZone() : inject(NgZone); + /** @internal */ + protected _effectRunner = inject(EffectScheduler); // Inject ApplicationRef to ensure NgZone stableness causes after render hooks to run // This will likely happen as a result of fixture.detectChanges because it calls ngZone.run // This is a crazy way of doing things but hey, it's the world we live in. // The zoneless scheduler should instead do this more imperatively by attaching // the `ComponentRef` to `ApplicationRef` and calling `appRef.tick` as the `detectChanges` // behavior. - private appRef = inject(ApplicationRef); + /** @internal */ + protected readonly _appRef = inject(ApplicationRef); // TODO(atscott): Remove this from public API - public ngZone = this.noZoneOptionIsSet ? null : this._ngZone; + ngZone = this._noZoneOptionIsSet ? null : this._ngZone; /** @nodoc */ constructor(public componentRef: ComponentRef) { @@ -73,10 +73,146 @@ export class ComponentFixture { this.componentInstance = componentRef.instance; this.nativeElement = this.elementRef.nativeElement; this.componentRef = componentRef; - this.setupNgZone(); } - private setupNgZone() { + /** + * Trigger a change detection cycle for the component. + */ + abstract detectChanges(checkNoChanges?: boolean): void; + + /** + * Do a change detection run to make sure there were no changes. + */ + checkNoChanges(): void { + this.changeDetectorRef.checkNoChanges(); + } + + /** + * Set whether the fixture should autodetect changes. + * + * Also runs detectChanges once so that any existing change is detected. + */ + abstract autoDetectChanges(autoDetect?: boolean): void; + + /** + * Return whether the fixture is currently stable or has async tasks that have not been completed + * yet. + */ + abstract isStable(): boolean; + + /** + * Get a promise that resolves when the fixture is stable. + * + * This can be used to resume testing after events have triggered asynchronous activity or + * asynchronous change detection. + */ + abstract whenStable(): Promise; + + /** + * Retrieves all defer block fixtures in the component fixture. + * + * @developerPreview + */ + getDeferBlocks(): Promise { + const deferBlocks: DeferBlockDetails[] = []; + const lView = (this.componentRef.hostView as any)['_lView']; + getDeferBlocks(lView, deferBlocks); + + const deferBlockFixtures = []; + for (const block of deferBlocks) { + deferBlockFixtures.push(new DeferBlockFixture(block, this)); + } + + return Promise.resolve(deferBlockFixtures); + } + + + private _getRenderer() { + if (this._renderer === undefined) { + this._renderer = this.componentRef.injector.get(RendererFactory2, null); + } + return this._renderer as RendererFactory2 | null; + } + + /** + * Get a promise that resolves when the ui state is stable following animations. + */ + whenRenderingDone(): Promise { + const renderer = this._getRenderer(); + if (renderer && renderer.whenRenderingDone) { + return renderer.whenRenderingDone(); + } + return this.whenStable(); + } + + /** + * Trigger component destruction. + */ + destroy(): void { + if (!this._isDestroyed) { + this.componentRef.destroy(); + this._isDestroyed = true; + } + } +} + +/** + * ComponentFixture behavior that actually attaches the component to the application to ensure + * behaviors between fixture and application do not diverge. `detectChanges` is disabled by default + * (instead, tests should wait for the scheduler to detect changes), `whenStable` is directly the + * `ApplicationRef.isStable`, and `autoDetectChanges` cannot be disabled. + */ +export class ScheduledComponentFixture extends ComponentFixture { + private readonly disableDetectChangesError = + inject(AllowDetectChangesAndAcknowledgeItCanHideApplicationBugs, {optional: true}) ?? false; + private readonly pendingTasks = inject(PendingTasks); + + initialize(): void { + this._appRef.attachView(this.componentRef.hostView); + } + + override detectChanges(checkNoChanges: boolean = true): void { + if (!this.disableDetectChangesError) { + throw new Error( + 'Do not use `detectChanges` directly when using zoneless change detection.' + + ' Instead, wait for the next render or `fixture.whenStable`.'); + } else if (!checkNoChanges) { + throw new Error( + 'Cannot disable `checkNoChanges` in this configuration. ' + + 'Use `fixture.componentRef.hostView.changeDetectorRef.detectChanges()` instead.'); + } + this._effectRunner.flush(); + this._appRef.tick(); + this._effectRunner.flush(); + } + + override isStable(): boolean { + return !this.pendingTasks.hasPendingTasks.value; + } + + override whenStable(): Promise { + if (this.isStable()) { + return Promise.resolve(false); + } + return firstValueFrom(this._appRef.isStable.pipe(filter(stable => stable))); + } + + override autoDetectChanges(autoDetect?: boolean|undefined): void { + throw new Error('Cannot call autoDetectChanges when using change detection scheduling.'); + } +} + +/** + * ComponentFixture behavior that attempts to act as a "mini application". + */ +export class PseudoApplicationComponentFixture extends ComponentFixture { + private _subscriptions = new Subscription(); + private _autoDetect = inject(ComponentFixtureAutoDetect, {optional: true}) ?? false; + private _isStable: boolean = true; + private _promise: Promise|null = null; + private _resolve: ((result: boolean) => void)|null = null; + + initialize(): void { // Create subscriptions outside the NgZone so that the callbacks run outside // of NgZone. this._ngZone.runOutsideAngular(() => { @@ -123,63 +259,26 @@ export class ComponentFixture { }); } - private _tick(checkNoChanges: boolean) { - this.changeDetectorRef.detectChanges(); - if (checkNoChanges) { - this.checkNoChanges(); - } - } - - /** - * Trigger a change detection cycle for the component. - */ - detectChanges(checkNoChanges: boolean = true): void { - this.effectRunner.flush(); + override detectChanges(checkNoChanges = true): void { + this._effectRunner.flush(); // Run the change detection inside the NgZone so that any async tasks as part of the change // detection are captured by the zone and can be waited for in isStable. this._ngZone.run(() => { - this._tick(checkNoChanges); + this.changeDetectorRef.detectChanges(); + if (checkNoChanges) { + this.checkNoChanges(); + } }); // Run any effects that were created/dirtied during change detection. Such effects might become // dirty in response to input signals changing. - this.effectRunner.flush(); + this._effectRunner.flush(); } - /** - * Do a change detection run to make sure there were no changes. - */ - checkNoChanges(): void { - this.changeDetectorRef.checkNoChanges(); - } - - /** - * Set whether the fixture should autodetect changes. - * - * Also runs detectChanges once so that any existing change is detected. - */ - autoDetectChanges(autoDetect: boolean = true) { - if (this.noZoneOptionIsSet) { - throw new Error('Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set'); - } - this._autoDetect = autoDetect; - this.detectChanges(); - } - - /** - * Return whether the fixture is currently stable or has async tasks that have not been completed - * yet. - */ - isStable(): boolean { + override isStable(): boolean { return this._isStable && !this._ngZone.hasPendingMacrotasks; } - /** - * Get a promise that resolves when the fixture is stable. - * - * This can be used to resume testing after events have triggered asynchronous activity or - * asynchronous change detection. - */ - whenStable(): Promise { + override whenStable(): Promise { if (this.isStable()) { return Promise.resolve(false); } else if (this._promise !== null) { @@ -192,51 +291,16 @@ export class ComponentFixture { } } - /** - * Retrieves all defer block fixtures in the component fixture. - * - * @developerPreview - */ - getDeferBlocks(): Promise { - const deferBlocks: DeferBlockDetails[] = []; - const lView = (this.componentRef.hostView as any)['_lView']; - getDeferBlocks(lView, deferBlocks); - - const deferBlockFixtures = []; - for (const block of deferBlocks) { - deferBlockFixtures.push(new DeferBlockFixture(block, this)); + override autoDetectChanges(autoDetect = true): void { + if (this._noZoneOptionIsSet) { + throw new Error('Cannot call autoDetectChanges when ComponentFixtureNoNgZone is set.'); } - - return Promise.resolve(deferBlockFixtures); + this._autoDetect = autoDetect; + this.detectChanges(); } - - private _getRenderer() { - if (this._renderer === undefined) { - this._renderer = this.componentRef.injector.get(RendererFactory2, null); - } - return this._renderer as RendererFactory2 | null; - } - - /** - * Get a promise that resolves when the ui state is stable following animations. - */ - whenRenderingDone(): Promise { - const renderer = this._getRenderer(); - if (renderer && renderer.whenRenderingDone) { - return renderer.whenRenderingDone(); - } - return this.whenStable(); - } - - /** - * Trigger component destruction. - */ - destroy(): void { - if (!this._isDestroyed) { - this.componentRef.destroy(); - this._subscriptions.unsubscribe(); - this._isDestroyed = true; - } + override destroy(): void { + this._subscriptions.unsubscribe(); + super.destroy(); } } diff --git a/packages/core/testing/src/test_bed.ts b/packages/core/testing/src/test_bed.ts index 616cca628fd..723e529080b 100644 --- a/packages/core/testing/src/test_bed.ts +++ b/packages/core/testing/src/test_bed.ts @@ -13,6 +13,7 @@ /* clang-format off */ import { Component, + ComponentRef, Directive, EnvironmentInjector, InjectFlags, @@ -25,6 +26,7 @@ import { ProviderToken, runInInjectionContext, Type, + ɵChangeDetectionScheduler as ChangeDetectionScheduler, ɵconvertToBitFlags as convertToBitFlags, ɵDeferBlockBehavior as DeferBlockBehavior, ɵEffectScheduler as EffectScheduler, @@ -45,7 +47,7 @@ import { -import {ComponentFixture} from './component_fixture'; +import {ComponentFixture, PseudoApplicationComponentFixture, ScheduledComponentFixture} from './component_fixture'; import {MetadataOverride} from './metadata_override'; import {ComponentFixtureNoNgZone, DEFER_BLOCK_DEFAULT_BEHAVIOR, ModuleTeardownOptions, TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, TestComponentRenderer, TestEnvironmentOptions, TestModuleMetadata, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT, THROW_ON_UNKNOWN_PROPERTIES_DEFAULT} from './test_bed_common'; import {TestBedCompiler} from './test_bed_compiler'; @@ -632,8 +634,15 @@ export class TestBedImpl implements TestBed { const componentFactory = new ComponentFactory(componentDef); const initComponent = () => { const componentRef = - componentFactory.create(Injector.NULL, [], `#${rootElId}`, this.testModuleRef); - return this.runInInjectionContext(() => new ComponentFixture(componentRef)); + componentFactory.create(Injector.NULL, [], `#${rootElId}`, this.testModuleRef) as + ComponentRef; + return this.runInInjectionContext(() => { + const hasScheduler = this.inject(ChangeDetectionScheduler, null) !== null; + const fixture = hasScheduler ? new ScheduledComponentFixture(componentRef) : + new PseudoApplicationComponentFixture(componentRef); + fixture.initialize(); + return fixture; + }); }; const noNgZone = this.inject(ComponentFixtureNoNgZone, false); const ngZone = noNgZone ? null : this.inject(NgZone, null); diff --git a/packages/core/testing/src/test_bed_common.ts b/packages/core/testing/src/test_bed_common.ts index ead472e7e56..9f15ab0b5c8 100644 --- a/packages/core/testing/src/test_bed_common.ts +++ b/packages/core/testing/src/test_bed_common.ts @@ -36,6 +36,13 @@ export class TestComponentRenderer { */ export const ComponentFixtureAutoDetect = new InjectionToken('ComponentFixtureAutoDetect'); +/** + * TODO(atscott): Make public API once we have decided if we want this error and how we want devs to + * disable it. + */ +export const AllowDetectChangesAndAcknowledgeItCanHideApplicationBugs = + new InjectionToken('AllowDetectChangesAndAcknowledgeItCanHideApplicationBugs'); + /** * @publicApi */ diff --git a/packages/core/testing/src/testing.ts b/packages/core/testing/src/testing.ts index 6de700dc073..16c0be4c0ef 100644 --- a/packages/core/testing/src/testing.ts +++ b/packages/core/testing/src/testing.ts @@ -13,7 +13,7 @@ */ export * from './async'; -export * from './component_fixture'; +export {ComponentFixture} from './component_fixture'; export * from './fake_async'; export {TestBed, getTestBed, TestBedStatic, inject, InjectSetupWrapper, withModule} from './test_bed'; export {TestComponentRenderer, ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, TestModuleMetadata, TestEnvironmentOptions, ModuleTeardownOptions} from './test_bed_common';