diff --git a/goldens/public-api/core/index.api.md b/goldens/public-api/core/index.api.md index aa9fd95d73f..d97a16a580d 100644 --- a/goldens/public-api/core/index.api.md +++ b/goldens/public-api/core/index.api.md @@ -137,6 +137,8 @@ export class ApplicationRef { tick(): void; get viewCount(): number; // (undocumented) + whenStable(): Promise; + // (undocumented) static ɵfac: i0.ɵɵFactoryDeclaration; // (undocumented) static ɵprov: i0.ɵɵInjectableDeclaration; diff --git a/packages/core/src/application/application_ref.ts b/packages/core/src/application/application_ref.ts index 8ca9e4cdb3f..af9be61f106 100644 --- a/packages/core/src/application/application_ref.ts +++ b/packages/core/src/application/application_ref.ts @@ -12,7 +12,7 @@ import { setActiveConsumer, setThrowInvalidWriteToSignalError, } from '@angular/core/primitives/signals'; -import {Observable, Subject} from 'rxjs'; +import {Observable, Subject, Subscription} from 'rxjs'; import {first, map} from 'rxjs/operators'; import {ZONELESS_ENABLED} from '../change_detection/scheduling/zoneless_scheduling'; @@ -347,6 +347,24 @@ export class ApplicationRef { map((pending) => !pending), ); + /** + * @returns A promise that resolves when the application becomes stable + */ + whenStable(): Promise { + let subscription: Subscription; + return new Promise((resolve) => { + subscription = this.isStable.subscribe({ + next: (stable) => { + if (stable) { + resolve(); + } + }, + }); + }).finally(() => { + subscription.unsubscribe(); + }); + } + private readonly _injector = inject(EnvironmentInjector); /** * The `EnvironmentInjector` used to create this application. diff --git a/packages/core/test/acceptance/after_render_hook_spec.ts b/packages/core/test/acceptance/after_render_hook_spec.ts index b1ceff3296d..63f3d96b342 100644 --- a/packages/core/test/acceptance/after_render_hook_spec.ts +++ b/packages/core/test/acceptance/after_render_hook_spec.ts @@ -1383,7 +1383,7 @@ describe('after render hooks', () => { const fixture = TestBed.createComponent(TestCmp); const appRef = TestBed.inject(ApplicationRef); appRef.attachView(fixture.componentRef.hostView); - await firstValueFrom(appRef.isStable.pipe(filter((stable) => stable))); + await appRef.whenStable(); expect(fixture.nativeElement.innerText).toBe('1'); }); diff --git a/packages/core/test/change_detection_scheduler_spec.ts b/packages/core/test/change_detection_scheduler_spec.ts index a43be4141f4..196cc7b60bb 100644 --- a/packages/core/test/change_detection_scheduler_spec.ts +++ b/packages/core/test/change_detection_scheduler_spec.ts @@ -277,8 +277,8 @@ describe('Angular with zoneless enabled', () => { expect(fixture.nativeElement.innerText).toEqual(''); }); - function whenStable(applicationRef = TestBed.inject(ApplicationRef)): Promise { - return firstValueFrom(applicationRef.isStable.pipe(filter((stable) => stable))); + function whenStable(): Promise { + return TestBed.inject(ApplicationRef).whenStable(); } it( @@ -316,14 +316,14 @@ describe('Angular with zoneless enabled', () => { ], }); const appViewRef = (applicationRef as any)._views[0] as {context: App; rootNodes: any[]}; - await whenStable(applicationRef); + await applicationRef.whenStable(); const component2 = createComponent(DynamicCmp, { environmentInjector: applicationRef.injector, }); appViewRef.context.viewContainer.insert(component2.hostView); expect(isStable(applicationRef.injector)).toBe(false); - await whenStable(applicationRef); + await applicationRef.whenStable(); component2.destroy(); // destroying the view synchronously removes element from DOM when not using animations @@ -333,7 +333,7 @@ describe('Angular with zoneless enabled', () => { let checkCountBeforeStable = doCheckCount; let renderCountBeforeStable = renderHookCalls; - await whenStable(applicationRef); + await applicationRef.whenStable(); // The view should not have refreshed expect(doCheckCount).toEqual(checkCountBeforeStable); // but render hooks should have run diff --git a/packages/core/test/event_emitter_spec.ts b/packages/core/test/event_emitter_spec.ts index 515e485c5f1..fa69100f463 100644 --- a/packages/core/test/event_emitter_spec.ts +++ b/packages/core/test/event_emitter_spec.ts @@ -203,7 +203,7 @@ describe('EventEmitter', () => { }, }); emitter.emit(1); - await firstValueFrom(TestBed.inject(ApplicationRef).isStable.pipe(filter((stable) => stable))); + await TestBed.inject(ApplicationRef).whenStable(); expect(emitValue!).toBeDefined(); expect(emitValue!).toEqual(1); }); diff --git a/packages/core/testing/src/component_fixture.ts b/packages/core/testing/src/component_fixture.ts index 186fb5ad807..e483e581a31 100644 --- a/packages/core/testing/src/component_fixture.ts +++ b/packages/core/testing/src/component_fixture.ts @@ -25,7 +25,6 @@ import { ɵPendingTasks as PendingTasks, } from '@angular/core'; import {Subject, Subscription} from 'rxjs'; -import {first} from 'rxjs/operators'; import {DeferBlockFixture} from './defer'; import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone} from './test_bed_common'; @@ -136,13 +135,10 @@ export abstract class ComponentFixture { return new Promise((resolve, reject) => { this.appErrorHandler.whenStableRejectFunctions.add(reject); - this._appRef.isStable - .pipe(first((stable) => stable)) - .toPromise() - .then((v) => { - this.appErrorHandler.whenStableRejectFunctions.delete(reject); - resolve(v); - }); + this._appRef.whenStable().then(() => { + this.appErrorHandler.whenStableRejectFunctions.delete(reject); + resolve(true); + }); }); } diff --git a/packages/service-worker/src/provider.ts b/packages/service-worker/src/provider.ts index 0ef2453c646..5a96c35d7f8 100644 --- a/packages/service-worker/src/provider.ts +++ b/packages/service-worker/src/provider.ts @@ -17,8 +17,8 @@ import { NgZone, PLATFORM_ID, } from '@angular/core'; -import {merge, Observable, of} from 'rxjs'; -import {delay, filter, take} from 'rxjs/operators'; +import {merge, from, Observable, of} from 'rxjs'; +import {delay, take} from 'rxjs/operators'; import {NgswCommChannel} from './low_level'; import {SwPush} from './push'; @@ -76,9 +76,10 @@ export function ngswAppInitializer( readyToRegister$ = delayWithTimeout(+args[0] || 0); break; case 'registerWhenStable': + const whenStable$ = from(injector.get(ApplicationRef).whenStable()); readyToRegister$ = !args[0] - ? whenStable(injector) - : merge(whenStable(injector), delayWithTimeout(+args[0])); + ? whenStable$ + : merge(whenStable$, delayWithTimeout(+args[0])); break; default: // Unknown strategy. @@ -108,11 +109,6 @@ function delayWithTimeout(timeout: number): Observable { return of(null).pipe(delay(timeout)); } -function whenStable(injector: Injector): Observable { - const appRef = injector.get(ApplicationRef); - return appRef.isStable.pipe(filter((stable) => stable)); -} - export function ngswCommChannelFactory( opts: SwRegistrationOptions, platformId: string, diff --git a/packages/service-worker/test/provider_spec.ts b/packages/service-worker/test/provider_spec.ts index 0b4367b221e..0d454d5de79 100644 --- a/packages/service-worker/test/provider_spec.ts +++ b/packages/service-worker/test/provider_spec.ts @@ -30,8 +30,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule'; let swRegisterSpy: jasmine.Spy; const untilStable = () => { - const appRef: ApplicationRef = TestBed.inject(ApplicationRef); - return appRef.isStable.pipe(filter(Boolean), take(1)).toPromise(); + return TestBed.inject(ApplicationRef).whenStable(); }; beforeEach( @@ -170,6 +169,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule'; provide: ApplicationRef, useValue: { isStable: isStableSub.asObservable(), + whenStable: () => isStableSub.pipe(filter(Boolean), take(1)), afterTick: new Subject(), onDestroy: () => {}, }, @@ -189,6 +189,7 @@ const serviceWorkerModuleApi = 'ServiceWorkerModule'; provide: ApplicationRef, useValue: { isStable: isStableSub.asObservable(), + whenStable: () => isStableSub.pipe(filter(Boolean), take(1)), afterTick: new Subject(), onDestroy: () => {}, },