Skip to content

Commit b3836c2

Browse files
atscottthePunderWoman
authored andcommitted
refactor(core): Private option to rethrow ApplicationRef.tick errors in tests (angular#57153)
This creates a private option that can be used internally while we migrate this to the default and only behavior. ~200 tests in TGP have errors that are being swallowed (console.log) and not causing the test to fail. We can first explicitly opt those out, flip the default internally, then "fix" them by adding expect...toThrow. PR Close angular#57153
1 parent 8098945 commit b3836c2

File tree

5 files changed

+63
-58
lines changed

5 files changed

+63
-58
lines changed

packages/core/test/component_fixture_spec.ts

Lines changed: 26 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import {
1313
ErrorHandler,
1414
Injectable,
1515
Input,
16-
NgZone,
1716
createComponent,
1817
provideExperimentalZonelessChangeDetection,
1918
signal,
@@ -352,14 +351,7 @@ describe('ComponentFixture', () => {
352351
expect(() => fixture.detectChanges()).toThrow();
353352
});
354353

355-
// note: this test only verifies existing behavior was not broken by a change to the zoneless fixture.
356-
// We probably do want the whenStable promise to be rejected. The current zone-based fixture is bad
357-
// and confusing for two reason:
358-
// 1. with autoDetect, errors in the fixture _cannot be handled_ with whenStable because
359-
// they're just thrown inside the rxjs subcription (and then goes to setTimeout(() => throw e))
360-
// 2. errors from other views attached to ApplicationRef just go to the ErrorHandler, which by default
361-
// only logs to console, allowing the test to pass
362-
it('resolves whenStable promise when errors happen during appRef.tick', async () => {
354+
describe('errors during ApplicationRef.tick', () => {
363355
@Component({
364356
template: '',
365357
standalone: true,
@@ -375,13 +367,33 @@ describe('ComponentFixture', () => {
375367
})
376368
class Blank {}
377369

378-
const fixture = TestBed.createComponent(Blank);
379-
const throwingThing = createComponent(ThrowingThing, {
380-
environmentInjector: TestBed.inject(EnvironmentInjector),
370+
// note: this test only verifies existing behavior was not broken by a change to the zoneless fixture.
371+
// We probably do want the whenStable promise to be rejected. The current zone-based fixture is bad
372+
// and confusing for two reason:
373+
// 1. with autoDetect, errors in the fixture _cannot be handled_ with whenStable because
374+
// they're just thrown inside the rxjs subcription (and then goes to setTimeout(() => throw e))
375+
// 2. errors from other views attached to ApplicationRef just go to the ErrorHandler, which by default
376+
// only logs to console, allowing the test to pass
377+
it('resolves whenStable promise when errors happen during appRef.tick', async () => {
378+
const fixture = TestBed.createComponent(Blank);
379+
const throwingThing = createComponent(ThrowingThing, {
380+
environmentInjector: TestBed.inject(EnvironmentInjector),
381+
});
382+
383+
TestBed.inject(ApplicationRef).attachView(throwingThing.hostView);
384+
await expectAsync(fixture.whenStable()).toBeResolved();
381385
});
382386

383-
TestBed.inject(ApplicationRef).attachView(throwingThing.hostView);
384-
await expectAsync(fixture.whenStable()).toBeResolved();
387+
it('can opt-in to rethrowing application errors and rejecting whenStable promises', async () => {
388+
TestBed.configureTestingModule({_rethrowApplicationTickErrors: true} as any);
389+
const fixture = TestBed.createComponent(Blank);
390+
const throwingThing = createComponent(ThrowingThing, {
391+
environmentInjector: TestBed.inject(EnvironmentInjector),
392+
});
393+
394+
TestBed.inject(ApplicationRef).attachView(throwingThing.hostView);
395+
await expectAsync(fixture.whenStable()).toBeRejected();
396+
});
385397
});
386398

387399
describe('defer', () => {

packages/core/testing/src/application_error_handler.ts

Lines changed: 3 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -6,31 +6,17 @@
66
* found in the LICENSE file at https://angular.io/license
77
*/
88

9-
import {
10-
ErrorHandler,
11-
inject,
12-
NgZone,
13-
Injectable,
14-
ɵZONELESS_ENABLED as ZONELESS_ENABLED,
15-
} from '@angular/core';
9+
import {ErrorHandler, inject, NgZone, Injectable, InjectionToken} from '@angular/core';
10+
11+
export const RETHROW_APPLICATION_ERRORS = new InjectionToken<boolean>('rethrow application errors');
1612

1713
@Injectable()
1814
export class TestBedApplicationErrorHandler {
1915
private readonly zone = inject(NgZone);
2016
private readonly userErrorHandler = inject(ErrorHandler);
21-
private readonly zoneless = inject(ZONELESS_ENABLED);
2217
readonly whenStableRejectFunctions: Set<(e: unknown) => void> = new Set();
2318

2419
handleError(e: unknown) {
25-
// TODO(atscott): Investigate if we can align the behaviors of zone and zoneless
26-
if (this.zoneless) {
27-
this.zonelessHandleError(e);
28-
} else {
29-
this.zone.runOutsideAngular(() => this.userErrorHandler.handleError(e));
30-
}
31-
}
32-
33-
private zonelessHandleError(e: unknown) {
3420
try {
3521
this.zone.runOutsideAngular(() => this.userErrorHandler.handleError(e));
3622
} catch (userError: unknown) {

packages/core/testing/src/component_fixture.ts

Lines changed: 11 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -81,10 +81,7 @@ export abstract class ComponentFixture<T> {
8181
/** @internal */
8282
protected readonly _testAppRef = this._appRef as unknown as TestAppRef;
8383
private readonly pendingTasks = inject(PendingTasks);
84-
/** @internal */
85-
protected readonly _appErrorHandler = inject(TestBedApplicationErrorHandler);
86-
/** @internal */
87-
protected _rejectWhenStablePromiseOnAppError = true;
84+
private readonly appErrorHandler = inject(TestBedApplicationErrorHandler);
8885

8986
// TODO(atscott): Remove this from public API
9087
ngZone = this._noZoneOptionIsSet ? null : this._ngZone;
@@ -137,7 +134,16 @@ export abstract class ComponentFixture<T> {
137134
return Promise.resolve(false);
138135
}
139136

140-
return this._appRef.isStable.pipe(first((stable) => stable)).toPromise();
137+
return new Promise((resolve, reject) => {
138+
this.appErrorHandler.whenStableRejectFunctions.add(reject);
139+
this._appRef.isStable
140+
.pipe(first((stable) => stable))
141+
.toPromise()
142+
.then((v) => {
143+
this.appErrorHandler.whenStableRejectFunctions.delete(reject);
144+
resolve(v);
145+
});
146+
});
141147
}
142148

143149
/**
@@ -200,16 +206,6 @@ export class ScheduledComponentFixture<T> extends ComponentFixture<T> {
200206
}
201207
}
202208

203-
override whenStable(): Promise<any> {
204-
return new Promise((resolve, reject) => {
205-
this._appErrorHandler.whenStableRejectFunctions.add(reject);
206-
super.whenStable().then((v) => {
207-
this._appErrorHandler.whenStableRejectFunctions.delete(reject);
208-
resolve(v);
209-
});
210-
});
211-
}
212-
213209
override detectChanges(checkNoChanges = true): void {
214210
if (!checkNoChanges) {
215211
throw new Error(
@@ -252,11 +248,6 @@ export class PseudoApplicationComponentFixture<T> extends ComponentFixture<T> {
252248
private beforeRenderSubscription: Subscription | undefined = undefined;
253249

254250
initialize(): void {
255-
// TODO(atscott): Determine whether we can align this behavior with the zoneless fixture.
256-
// This exists to keep the previous zone-based fixture behavior consistent with how it was before.
257-
// However, we currently feel that the zoneless fixture is doing the more correct thing.
258-
this._rejectWhenStablePromiseOnAppError = false;
259-
260251
if (this._autoDetect) {
261252
this.subscribeToAppRefEvents();
262253
}

packages/core/testing/src/test_bed_common.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ export interface TestModuleMetadata {
7373
* Defaults to `manual`.
7474
*/
7575
deferBlockBehavior?: DeferBlockBehavior;
76+
77+
/** @internal */
78+
_rethrowApplicationTickErrors?: boolean;
7679
}
7780

7881
/**

packages/core/testing/src/test_bed_compiler.ts

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import {
1515
Compiler,
1616
COMPILER_OPTIONS,
1717
Component,
18-
ErrorHandler,
1918
Directive,
2019
Injector,
2120
inject,
@@ -39,7 +38,6 @@ import {
3938
ɵcompilePipe as compilePipe,
4039
ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID,
4140
ɵDEFER_BLOCK_CONFIG as DEFER_BLOCK_CONFIG,
42-
ɵDeferBlockBehavior as DeferBlockBehavior,
4341
ɵdepsTracker as depsTracker,
4442
ɵDirectiveDef as DirectiveDef,
4543
ɵgenerateStandaloneInDeclarationsError,
@@ -67,6 +65,7 @@ import {
6765
ɵUSE_RUNTIME_DEPS_TRACKER_FOR_JIT as USE_RUNTIME_DEPS_TRACKER_FOR_JIT,
6866
ɵɵInjectableDeclaration as InjectableDeclaration,
6967
NgZone,
68+
ErrorHandler,
7069
} from '@angular/core';
7170

7271
import {ComponentDef, ComponentType} from '../../src/render3';
@@ -80,7 +79,10 @@ import {
8079
Resolver,
8180
} from './resolvers';
8281
import {DEFER_BLOCK_DEFAULT_BEHAVIOR, TestModuleMetadata} from './test_bed_common';
83-
import {TestBedApplicationErrorHandler} from './application_error_handler';
82+
import {
83+
RETHROW_APPLICATION_ERRORS,
84+
TestBedApplicationErrorHandler,
85+
} from './application_error_handler';
8486

8587
enum TestingModuleOverride {
8688
DECLARATION,
@@ -224,6 +226,10 @@ export class TestBedCompiler {
224226
if (moduleDef.providers !== undefined) {
225227
this.providers.push(...moduleDef.providers);
226228
}
229+
this.providers.push({
230+
provide: RETHROW_APPLICATION_ERRORS,
231+
useValue: moduleDef._rethrowApplicationTickErrors ?? false,
232+
});
227233

228234
if (moduleDef.schemas !== undefined) {
229235
this.schemas.push(...moduleDef.schemas);
@@ -941,10 +947,17 @@ export class TestBedCompiler {
941947
{
942948
provide: INTERNAL_APPLICATION_ERROR_HANDLER,
943949
useFactory: () => {
944-
const handler = inject(TestBedApplicationErrorHandler);
945-
return (e: unknown) => {
946-
handler.handleError(e);
947-
};
950+
if (inject(ZONELESS_ENABLED) || inject(RETHROW_APPLICATION_ERRORS, {optional: true})) {
951+
const handler = inject(TestBedApplicationErrorHandler);
952+
return (e: unknown) => {
953+
handler.handleError(e);
954+
};
955+
} else {
956+
const userErrorHandler = inject(ErrorHandler);
957+
const ngZone = inject(NgZone);
958+
return (e: unknown) =>
959+
ngZone.runOutsideAngular(() => userErrorHandler.handleError(e));
960+
}
948961
},
949962
},
950963
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},

0 commit comments

Comments
 (0)