Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions goldens/public-api/core/testing/index.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -214,6 +214,7 @@ export interface TestModuleMetadata {
imports?: any[];
// (undocumented)
providers?: any[];
rethrowApplicationErrors?: boolean;
// (undocumented)
schemas?: Array<SchemaMetadata | any[]>;
// (undocumented)
Expand Down
17 changes: 5 additions & 12 deletions packages/core/test/component_fixture_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -367,32 +367,25 @@ describe('ComponentFixture', () => {
})
class Blank {}

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

TestBed.inject(ApplicationRef).attachView(throwingThing.hostView);
await expectAsync(fixture.whenStable()).toBeResolved();
await expectAsync(fixture.whenStable()).toBeRejected();
});

it('can opt-in to rethrowing application errors and rejecting whenStable promises', async () => {
TestBed.configureTestingModule({_rethrowApplicationTickErrors: true} as any);
it('can opt-out of rethrowing application errors and rejecting whenStable promises', async () => {
TestBed.configureTestingModule({rethrowApplicationErrors: false});
const fixture = TestBed.createComponent(Blank);
const throwingThing = createComponent(ThrowingThing, {
environmentInjector: TestBed.inject(EnvironmentInjector),
});

TestBed.inject(ApplicationRef).attachView(throwingThing.hostView);
await expectAsync(fixture.whenStable()).toBeRejected();
await expectAsync(fixture.whenStable()).toBeResolved();
});
});

Expand Down
4 changes: 2 additions & 2 deletions packages/core/testing/src/application_error_handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
* found in the LICENSE file at https://angular.io/license
*/

import {ErrorHandler, inject, NgZone, Injectable, InjectionToken} from '@angular/core';
import {ErrorHandler, inject, NgZone, Injectable} from '@angular/core';

export const RETHROW_APPLICATION_ERRORS = new InjectionToken<boolean>('rethrow application errors');
export const RETHROW_APPLICATION_ERRORS_DEFAULT = true;

@Injectable()
export class TestBedApplicationErrorHandler {
Expand Down
17 changes: 14 additions & 3 deletions packages/core/testing/src/test_bed_common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,14 +68,25 @@ export interface TestModuleMetadata {
*/
errorOnUnknownProperties?: boolean;

/**
* Whether errors that happen during application change detection should be rethrown.
*
* When `true`, errors that are caught during application change detection will
* be reported to the `ErrorHandler` and rethrown to prevent them from going
* unnoticed in tests.
*
* When `false`, errors are only forwarded to the `ErrorHandler`, which by default
* simply logs them to the console.
*
* Defaults to `true`.
*/
rethrowApplicationErrors?: boolean;

/**
* Whether defer blocks should behave with manual triggering or play through normally.
* Defaults to `manual`.
*/
deferBlockBehavior?: DeferBlockBehavior;

/** @internal */
_rethrowApplicationTickErrors?: boolean;
}

/**
Expand Down
42 changes: 20 additions & 22 deletions packages/core/testing/src/test_bed_compiler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import {
LOCALE_ID,
ModuleWithComponentFactories,
ModuleWithProviders,
ɵZONELESS_ENABLED as ZONELESS_ENABLED,
NgModule,
NgModuleFactory,
Pipe,
Expand Down Expand Up @@ -52,6 +51,7 @@ import {
ɵNG_INJ_DEF as NG_INJ_DEF,
ɵNG_MOD_DEF as NG_MOD_DEF,
ɵNG_PIPE_DEF as NG_PIPE_DEF,
ɵZONELESS_ENABLED as ZONELESS_ENABLED,
ɵNgModuleFactory as R3NgModuleFactory,
ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes,
ɵNgModuleType as NgModuleType,
Expand Down Expand Up @@ -80,7 +80,7 @@ import {
} from './resolvers';
import {DEFER_BLOCK_DEFAULT_BEHAVIOR, TestModuleMetadata} from './test_bed_common';
import {
RETHROW_APPLICATION_ERRORS,
RETHROW_APPLICATION_ERRORS_DEFAULT,
TestBedApplicationErrorHandler,
} from './application_error_handler';

Expand Down Expand Up @@ -190,6 +190,7 @@ export class TestBedCompiler {
private testModuleRef: NgModuleRef<any> | null = null;

private deferBlockBehavior = DEFER_BLOCK_DEFAULT_BEHAVIOR;
private rethrowApplicationTickErrors = RETHROW_APPLICATION_ERRORS_DEFAULT;

constructor(
private platform: PlatformRef,
Expand Down Expand Up @@ -226,16 +227,14 @@ export class TestBedCompiler {
if (moduleDef.providers !== undefined) {
this.providers.push(...moduleDef.providers);
}
this.providers.push({
provide: RETHROW_APPLICATION_ERRORS,
useValue: moduleDef._rethrowApplicationTickErrors ?? false,
});

if (moduleDef.schemas !== undefined) {
this.schemas.push(...moduleDef.schemas);
}

this.deferBlockBehavior = moduleDef.deferBlockBehavior ?? DEFER_BLOCK_DEFAULT_BEHAVIOR;
this.rethrowApplicationTickErrors =
moduleDef.rethrowApplicationErrors ?? RETHROW_APPLICATION_ERRORS_DEFAULT;
}

overrideModule(ngModule: Type<any>, override: MetadataOverride<NgModule>): void {
Expand Down Expand Up @@ -944,29 +943,28 @@ export class TestBedCompiler {
...this.rootProviderOverrides,
internalProvideZoneChangeDetection({}),
TestBedApplicationErrorHandler,
{
provide: INTERNAL_APPLICATION_ERROR_HANDLER,
useFactory: () => {
if (inject(ZONELESS_ENABLED) || inject(RETHROW_APPLICATION_ERRORS, {optional: true})) {
const handler = inject(TestBedApplicationErrorHandler);
return (e: unknown) => {
handler.handleError(e);
};
} else {
const userErrorHandler = inject(ErrorHandler);
const ngZone = inject(NgZone);
return (e: unknown) =>
ngZone.runOutsideAngular(() => userErrorHandler.handleError(e));
}
},
},
{provide: ChangeDetectionScheduler, useExisting: ChangeDetectionSchedulerImpl},
],
});

const providers = [
{provide: Compiler, useFactory: () => new R3TestCompiler(this)},
{provide: DEFER_BLOCK_CONFIG, useValue: {behavior: this.deferBlockBehavior}},
{
provide: INTERNAL_APPLICATION_ERROR_HANDLER,
useFactory: () => {
if (this.rethrowApplicationTickErrors) {
const handler = inject(TestBedApplicationErrorHandler);
return (e: unknown) => {
handler.handleError(e);
};
} else {
const userErrorHandler = inject(ErrorHandler);
const ngZone = inject(NgZone);
return (e: unknown) => ngZone.runOutsideAngular(() => userErrorHandler.handleError(e));
}
},
},
...this.providers,
...this.providerOverrides,
];
Expand Down