/
githubmirror
/
angular
Обзор
Документация
Войти
/
githubmirror
/
angular
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
main
packages/core/test/test_bed_spec.ts
2 966 строк
93 KB
Matthieu Riegler
refactor(core): Don't throw when there are not async metadata
28 апр 2026, 03:04
28 апр 2026, 03:04
4ad3a1f
Код
Авторство
О чём код?
/** * @license * Copyright Google LLC All Rights Reserved. * * Use of this source code is governed by an MIT-style license that can be * found in the LICENSE file at https://angular.dev/license */ import {ɵPLATFORM_BROWSER_ID} from '@angular/common'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/private/testing/matchers'; import { APP_INITIALIZER, ChangeDetectionStrategy, ChangeDetectorRef, Compiler, Component, ɵɵdefineComponent as defineComponent, ɵɵdefineInjector as defineInjector, ɵɵdefineNgModule as defineNgModule, Directive, DOCUMENT, ɵɵelementEnd as elementEnd, ElementRef, ɵɵelementStart as elementStart, ErrorHandler, EventEmitter, getNgModuleById, inject, Inject, Injectable, InjectionToken, InjectOptions, Injector, Input, inputBinding, LOCALE_ID, ModuleWithProviders, NgModule, Optional, Output, outputBinding, Pipe, PLATFORM_ID, provideZoneChangeDetection, provideZonelessChangeDetection, ɵsetClassMetadata as setClassMetadata, ɵɵsetNgModuleScope as setNgModuleScope, signal, ɵɵtext as text, twoWayBinding, Type, ViewChild, } from '../src/core'; import {DeferBlockBehavior} from '../testing'; import {TestBed, TestBedImpl} from '../testing/src/test_bed'; import {NgModuleType} from '../src/render3'; import {depsTracker} from '../src/render3/deps_tracker/deps_tracker'; import {setClassMetadataAsync} from '../src/render3/metadata'; import { ComponentFixtureAutoDetect, TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, THROW_ON_UNKNOWN_ELEMENTS_DEFAULT, THROW_ON_UNKNOWN_PROPERTIES_DEFAULT, } from '../testing/src/test_bed_common'; const NAME = new InjectionToken<string>('name'); @Injectable({providedIn: 'root'}) class SimpleService { static ngOnDestroyCalls: number = 0; id: number = 1; ngOnDestroy() { SimpleService.ngOnDestroyCalls++; } } // -- module: HWModule @Component({ selector: 'hello-world', template: '<greeting-cmp></greeting-cmp>', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) export class HelloWorld {} // -- module: Greeting @Component({ selector: 'greeting-cmp', template: 'Hello {{ name }}', standalone: false, changeDetection: ChangeDetectionStrategy.Eager, }) export class GreetingCmp { name: string; constructor( @Inject(NAME) @Optional() name: string, @Inject(SimpleService) @Optional() service: SimpleService, ) { this.name = name || 'nobody!'; } } @Component({ selector: 'cmp-with-providers', template: '<hello-world></hello-world>', providers: [ SimpleService, // {provide: NAME, useValue: `from Component`}, ], standalone: false, }) class CmpWithProviders {} @NgModule({ declarations: [GreetingCmp], exports: [GreetingCmp], }) export class GreetingModule {} @Component({ selector: 'simple-cmp', template: '<b>simple</b>', standalone: false, }) export class SimpleCmp {} @Component({ selector: 'with-refs-cmp', template: '<div #firstDiv></div>', standalone: false, }) export class WithRefsCmp {} @Component({ selector: 'inherited-cmp', template: 'inherited', standalone: false, }) export class InheritedCmp extends SimpleCmp {} @Directive({ selector: '[hostBindingDir]', host: {'[id]': 'id'}, standalone: false, }) export class HostBindingDir { id = 'one'; } @Component({ selector: 'component-with-prop-bindings', template: ` <div hostBindingDir [title]="title" [attr.aria-label]="label"></div> <p title="( {{ label }} - {{ title }} )" [attr.aria-label]="label" id="[ {{ label }} ] [ {{ title }} ]" ></p> `, standalone: false, }) export class ComponentWithPropBindings { title = 'some title'; label = 'some label'; } @Component({ selector: 'simple-app', template: ` <simple-cmp></simple-cmp> - <inherited-cmp></inherited-cmp> `, standalone: false, }) export class SimpleApp {} @Component({ selector: 'inline-template', template: '<p>Hello</p>', standalone: false, }) export class ComponentWithInlineTemplate {} @NgModule({ declarations: [ HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, ComponentWithPropBindings, HostBindingDir, CmpWithProviders, ], imports: [GreetingModule], providers: [{provide: NAME, useValue: 'World!'}], }) export class HelloWorldModule {} describe('TestBed (isolated)', () => { // This test is extracted to an individual `describe` block to avoid any extra TestBed // initialization logic that happens in the `beforeEach` functions in other `describe` sections. it('should apply scopes correctly for components in the lazy-loaded module', () => { // Reset TestBed to the initial state, emulating an invocation of a first test. // Check `TestBed.checkGlobalCompilationFinished` for additional info. TestBedImpl.INSTANCE.globalCompilationChecked = false; @Component({ selector: 'root', template: '<div dirA></div>', standalone: false, }) class Root {} @Directive({ selector: '[dirA]', host: {'title': 'Test title'}, standalone: false, }) class DirA {} // Note: this module is not directly reference in the test intentionally. // Its presence triggers a side-effect of patching correct scopes on the declared components. @NgModule({ declarations: [Root, DirA], }) class Module {} TestBed.configureTestingModule({}); // Trigger TestBed initialization. TestBed.inject(Injector); // Emulate the end of a test (trigger module scoping queue flush). TestBed.resetTestingModule(); // Emulate a lazy-loading scenario by creating a component // without importing a module into a TestBed testing module. TestBed.configureTestingModule({}); const fixture = TestBed.createComponent(Root); fixture.detectChanges(); expect(fixture.nativeElement.firstChild.getAttribute('title')).toEqual('Test title'); }); }); describe('TestBed with Standalone types', () => { beforeEach(() => { TestBed.resetTestingModule(); }); it('should override dependencies of standalone components', () => { @Component({ selector: 'dep', template: 'main dep', }) class MainDep {} @Component({ selector: 'dep', template: 'mock dep', }) class MockDep {} @Component({ selector: 'app-root', imports: [MainDep], template: '<dep />', }) class AppComponent {} TestBed.configureTestingModule({imports: [AppComponent]}); let fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); // No overrides defined, expecting main dependency to be used. expect(fixture.nativeElement.innerHTML).toBe('<dep>main dep</dep>'); // Emulate an end of a test. TestBed.resetTestingModule(); // Emulate the start of a next test, make sure previous overrides // are not persisted across tests. TestBed.configureTestingModule({imports: [AppComponent]}); TestBed.overrideComponent(AppComponent, {set: {imports: [MockDep]}}); fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); // Main dependency was overridden, expect to see a mock. expect(fixture.nativeElement.innerHTML).toBe('<dep>mock dep</dep>'); }); it('should override providers on standalone component itself', () => { const A = new InjectionToken('A'); @Component({ template: '{{ a }}', providers: [{provide: A, useValue: 'A'}], }) class MyStandaloneComp { constructor(@Inject(A) public a: string) {} } // NOTE: the `TestBed.configureTestingModule` is load-bearing here: it instructs // TestBed to examine and override providers in dependencies. TestBed.configureTestingModule({imports: [MyStandaloneComp]}); TestBed.overrideProvider(A, {useValue: 'Overridden A'}); const fixture = TestBed.createComponent(MyStandaloneComp); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toBe('Overridden A'); }); it('should override providers on components used as standalone component dependency', () => { @Injectable() class Service { id = 'Service(original)'; } @Injectable() class MockService { id = 'Service(mock)'; } @Component({ selector: 'dep', template: '{{ service.id }}', providers: [Service], }) class Dep { service = inject(Service); } @Component({ template: '<dep />', imports: [Dep], }) class MyStandaloneComp {} TestBed.configureTestingModule({imports: [MyStandaloneComp]}); TestBed.overrideProvider(Service, {useFactory: () => new MockService()}); let fixture = TestBed.createComponent(MyStandaloneComp); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toBe('<dep>Service(mock)</dep>'); // Emulate an end of a test. TestBed.resetTestingModule(); // Emulate the start of a next test, make sure previous overrides // are not persisted across tests. TestBed.configureTestingModule({imports: [MyStandaloneComp]}); fixture = TestBed.createComponent(MyStandaloneComp); fixture.detectChanges(); // No provider overrides, expect original provider value to be used. expect(fixture.nativeElement.innerHTML).toBe('<dep>Service(original)</dep>'); }); it('should override providers in standalone component dependencies via overrideProvider', () => { const A = new InjectionToken('A'); @NgModule({ providers: [{provide: A, useValue: 'A'}], }) class ComponentDependenciesModule {} @Component({ template: '{{ a }}', imports: [ComponentDependenciesModule], }) class MyStandaloneComp { constructor(@Inject(A) public a: string) {} } // NOTE: the `TestBed.configureTestingModule` is load-bearing here: it instructs // TestBed to examine and override providers in dependencies. TestBed.configureTestingModule({imports: [MyStandaloneComp]}); TestBed.overrideProvider(A, {useValue: 'Overridden A'}); const fixture = TestBed.createComponent(MyStandaloneComp); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toBe('Overridden A'); }); it('should override providers in standalone component dependencies via overrideModule', () => { const A = new InjectionToken('A'); @NgModule({ providers: [{provide: A, useValue: 'A'}], }) class ComponentDependenciesModule {} @Component({ template: '{{ a }}', imports: [ComponentDependenciesModule], }) class MyStandaloneComp { constructor(@Inject(A) public a: string) {} } // NOTE: the `TestBed.configureTestingModule` is *not* needed here, since the TestBed // knows which NgModule was overridden and needs re-compilation. TestBed.overrideModule(ComponentDependenciesModule, { set: {providers: [{provide: A, useValue: 'Overridden A'}]}, }); const fixture = TestBed.createComponent(MyStandaloneComp); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toBe('Overridden A'); }); it('should allow overriding a template of a standalone component', () => { @Component({ template: 'Original', }) class MyStandaloneComp {} // NOTE: the `TestBed.configureTestingModule` call is *not* required here, since TestBed already // knows that the `MyStandaloneComp` should be overridden/recompiled. TestBed.overrideComponent(MyStandaloneComp, {set: {template: 'Overridden'}}); const fixture = TestBed.createComponent(MyStandaloneComp); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toBe('Overridden'); }); it('should allow overriding the set of directives and pipes used in a standalone component', () => { @Directive({ selector: '[dir]', host: {'[id]': 'id'}, }) class MyStandaloneDirectiveA { id = 'A'; } @Directive({ selector: '[dir]', host: {'[id]': 'id'}, }) class MyStandaloneDirectiveB { id = 'B'; } @Pipe({name: 'pipe'}) class MyStandalonePipeA { transform(value: string): string { return `transformed ${value} (A)`; } } @Pipe({name: 'pipe'}) class MyStandalonePipeB { transform(value: string): string { return `transformed ${value} (B)`; } } @Component({ template: '<div dir>{{ name | pipe }}</div>', imports: [MyStandalonePipeA, MyStandaloneDirectiveA], }) class MyStandaloneComp { name = 'MyStandaloneComp'; } // NOTE: the `TestBed.configureTestingModule` call is *not* required here, since TestBed // already knows that the `MyStandaloneComp` should be overridden/recompiled. TestBed.overrideComponent(MyStandaloneComp, { set: {imports: [MyStandalonePipeB, MyStandaloneDirectiveB]}, }); const fixture = TestBed.createComponent(MyStandaloneComp); fixture.detectChanges(); const rootElement = fixture.nativeElement.firstChild; expect(rootElement.id).toBe('B'); expect(rootElement.innerHTML).toBe('transformed MyStandaloneComp (B)'); }); it('should reflect overrides on imported standalone directive', () => { @Directive({ selector: '[dir]', host: {'[id]': 'id'}, }) class DepStandaloneDirective { id = 'A'; } @Component({ selector: 'standalone-cmp', template: 'Original MyStandaloneComponent', }) class DepStandaloneComponent { id = 'A'; } @Component({ template: '<standalone-cmp dir>Hello world!</standalone-cmp>', imports: [DepStandaloneDirective, DepStandaloneComponent], }) class RootStandaloneComp {} // NOTE: the `TestBed.configureTestingModule` call is *not* required here, since TestBed // already knows which Components/Directives are overridden and should be recompiled. TestBed.overrideComponent(DepStandaloneComponent, { set: {template: 'Overridden MyStandaloneComponent'}, }); TestBed.overrideDirective(DepStandaloneDirective, {set: {host: {'[id]': "'Overridden'"}}}); const fixture = TestBed.createComponent(RootStandaloneComp); fixture.detectChanges(); const rootElement = fixture.nativeElement.firstChild; expect(rootElement.id).toBe('Overridden'); expect(rootElement.innerHTML).toBe('Overridden MyStandaloneComponent'); }); it('should make overridden providers available in pipes', () => { const TOKEN_A = new InjectionToken('TOKEN_A'); @Pipe({ name: 'testPipe', }) class TestPipe { constructor(@Inject(TOKEN_A) private token: string) {} transform(value: string): string { return `transformed ${value} using ${this.token} token`; } } @NgModule({ imports: [TestPipe], exports: [TestPipe], providers: [{provide: TOKEN_A, useValue: 'A'}], }) class TestNgModule {} @Component({ selector: 'test-component', imports: [TestNgModule], template: `{{ 'original value' | testPipe }}`, }) class TestComponent {} TestBed.configureTestingModule({ imports: [TestComponent], }); TestBed.overrideProvider(TOKEN_A, {useValue: 'Overridden A'}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); const hostElement = fixture.nativeElement.firstChild; expect(hostElement.textContent).toBe('transformed original value using Overridden A token'); }); describe('NgModules as dependencies', () => { @Component({ selector: 'test-cmp', template: '...', standalone: false, }) class TestComponent { testField = 'default'; } @Component({ selector: 'test-cmp', template: '...', standalone: false, }) class MockTestComponent { testField = 'overridden'; } @NgModule({ declarations: [TestComponent], exports: [TestComponent], }) class TestModule {} @Component({ selector: 'app-root', template: `<test-cmp #testCmpCtrl></test-cmp>`, imports: [TestModule], }) class AppComponent { @ViewChild('testCmpCtrl', {static: true}) testCmpCtrl!: TestComponent; } it('should allow declarations and exports overrides on an imported NgModule', () => { // replace TestComponent with MockTestComponent TestBed.overrideModule(TestModule, { remove: {declarations: [TestComponent], exports: [TestComponent]}, add: {declarations: [MockTestComponent], exports: [MockTestComponent]}, }); const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); const app = fixture.componentInstance; expect(app.testCmpCtrl.testField).toBe('overridden'); }); it('should allow removing an import via `overrideModule`', () => { const fooToken = new InjectionToken<string>('foo'); @NgModule({ providers: [{provide: fooToken, useValue: 'FOO'}], }) class ImportedModule {} @NgModule({ imports: [ImportedModule], }) class ImportingModule {} TestBed.configureTestingModule({ imports: [ImportingModule], }); TestBed.overrideModule(ImportingModule, { remove: { imports: [ImportedModule], }, }); expect(TestBed.inject(fooToken, 'BAR')).toBe('BAR'); // Emulate an end of a test. TestBed.resetTestingModule(); // Emulate the start of a next test, make sure previous overrides // are not persisted across tests. TestBed.configureTestingModule({ imports: [ImportingModule], }); expect(TestBed.inject(fooToken, 'BAR')).toBe('FOO'); }); }); }); describe('TestBed', () => { beforeEach(() => { TestBed.resetTestingModule(); TestBed.configureTestingModule({imports: [HelloWorldModule]}); }); it('should compile and render a component', () => { const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.nativeElement).toHaveText('Hello World!'); }); it('should not allow overrides of the `standalone` field', () => { @Component({ selector: 'standalone-comp', template: '...', }) class StandaloneComponent {} @Component({ selector: 'non-standalone-comp', template: '...', standalone: false, }) class NonStandaloneComponent {} @Directive() class StandaloneDirective {} @Directive({ standalone: false, }) class NonStandaloneDirective {} @Pipe({name: 'test'}) class StandalonePipe {} @Pipe({ name: 'test', standalone: false, }) class NonStandalonePipe {} const getExpectedError = (typeName: string) => `An override for the ${typeName} class has the \`standalone\` flag. ` + `Changing the \`standalone\` flag via TestBed overrides is not supported.`; const overrides = [ {set: {standalone: false}}, {add: {standalone: false}}, {remove: {standalone: true}}, ]; const scenarios = [ [TestBed.overrideComponent, StandaloneComponent, NonStandaloneComponent], [TestBed.overrideDirective, StandaloneDirective, NonStandaloneDirective], [TestBed.overridePipe, StandalonePipe, NonStandalonePipe], ]; overrides.forEach((override) => { scenarios.forEach(([fn, standaloneType, nonStandaloneType]) => { expect(() => (fn as Function)(standaloneType, override)).toThrowError( getExpectedError(standaloneType.name), ); expect(() => (fn as Function)(nonStandaloneType, override)).toThrowError( getExpectedError(nonStandaloneType.name), ); }); }); }); it('should give access to the component instance', () => { const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.componentInstance).toBeInstanceOf(HelloWorld); }); it('should give the ability to query by css', () => { const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); const greetingByCss = hello.debugElement.query(By.css('greeting-cmp')); expect(greetingByCss.nativeElement).toHaveText('Hello World!'); expect(greetingByCss.componentInstance).toBeInstanceOf(GreetingCmp); }); it('should give the ability to trigger the change detection', () => { TestBed.configureTestingModule({providers: [provideZoneChangeDetection()]}); const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); const greetingByCss = hello.debugElement.query(By.css('greeting-cmp')); expect(greetingByCss.nativeElement).toHaveText('Hello World!'); greetingByCss.componentInstance.name = 'TestBed!'; hello.detectChanges(); expect(greetingByCss.nativeElement).toHaveText('Hello TestBed!'); }); it('should give the ability to access property bindings on a node', () => { const fixture = TestBed.createComponent(ComponentWithPropBindings); fixture.detectChanges(); const divElement = fixture.debugElement.query(By.css('div')); expect(divElement.properties['id']).toEqual('one'); expect(divElement.properties['title']).toEqual('some title'); }); it('should give the ability to access interpolated properties on a node', () => { const fixture = TestBed.createComponent(ComponentWithPropBindings); fixture.detectChanges(); const paragraphEl = fixture.debugElement.query(By.css('p')); expect(paragraphEl.properties['title']).toEqual('( some label - some title )'); expect(paragraphEl.properties['id']).toEqual('[ some label ] [ some title ]'); }); it('should give access to the node injector', () => { const fixture = TestBed.createComponent(HelloWorld); fixture.detectChanges(); const injector = fixture.debugElement.query(By.css('greeting-cmp')).injector; // from the node injector const greetingCmp = injector.get(GreetingCmp); expect(greetingCmp.constructor).toBe(GreetingCmp); // from the node injector (inherited from a parent node) const helloWorldCmp = injector.get(HelloWorld); expect(fixture.componentInstance).toBe(helloWorldCmp); const nameInjected = injector.get(NAME); expect(nameInjected).toEqual('World!'); }); it('should give access to the node injector for root node', () => { const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); const injector = hello.debugElement.injector; // from the node injector const helloInjected = injector.get(HelloWorld); expect(helloInjected).toBe(hello.componentInstance); // from the module injector const nameInjected = injector.get(NAME); expect(nameInjected).toEqual('World!'); }); it('should give access to local refs on a node', () => { const withRefsCmp = TestBed.createComponent(WithRefsCmp); const firstDivDebugEl = withRefsCmp.debugElement.query(By.css('div')); // assert that a native element is referenced by a local ref expect(firstDivDebugEl.references['firstDiv'].tagName.toLowerCase()).toBe('div'); }); it('should give the ability to query by directive', () => { const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); const greetingByDirective = hello.debugElement.query(By.directive(GreetingCmp)); expect(greetingByDirective.componentInstance).toBeInstanceOf(GreetingCmp); }); it('should allow duplicate NgModule registrations with the same id', () => { const id = 'registered'; @NgModule({id}) class ModuleA {} expect(getNgModuleById(id)).toBe(ModuleA); // This would ordinarily error, if not in a test scenario. @NgModule({id}) class ModuleB {} expect(getNgModuleById(id)).toBe(ModuleB); }); it('allow to override a template', () => { // use original template when there is no override let hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.nativeElement).toHaveText('Hello World!'); // override the template TestBed.resetTestingModule(); TestBed.configureTestingModule({imports: [HelloWorldModule]}); TestBed.overrideComponent(GreetingCmp, {set: {template: `Bonjour {{ name }}`}}); hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.nativeElement).toHaveText('Bonjour World!'); // restore the original template by calling `.resetTestingModule()` TestBed.resetTestingModule(); TestBed.configureTestingModule({imports: [HelloWorldModule]}); hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.nativeElement).toHaveText('Hello World!'); }); // https://github.com/angular/angular/issues/42734 it('should override a component which is declared in an NgModule which is imported as a `ModuleWithProviders`', () => { // This test verifies that an overridden component that is declared in an NgModule that has // been imported as a `ModuleWithProviders` continues to have access to the declaration scope // of the NgModule. TestBed.resetTestingModule(); const moduleWithProviders: ModuleWithProviders<HelloWorldModule> = {ngModule: HelloWorldModule}; TestBed.configureTestingModule({imports: [moduleWithProviders]}); TestBed.overrideComponent(HelloWorld, { set: {template: 'Overridden <greeting-cmp></greeting-cmp>'}, }); const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.nativeElement).toHaveText('Overridden Hello World!'); }); it('should run `APP_INITIALIZER` before accessing `LOCALE_ID` provider', () => { let locale: string = ''; @NgModule({ providers: [ {provide: APP_INITIALIZER, useValue: () => (locale = 'fr-FR'), multi: true}, {provide: LOCALE_ID, useFactory: () => locale}, ], }) class TestModule {} TestBed.configureTestingModule({imports: [TestModule]}); expect(TestBed.inject(LOCALE_ID)).toBe('fr-FR'); }); it('allow to override a provider', () => { TestBed.overrideProvider(NAME, {useValue: 'injected World!'}); const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.nativeElement).toHaveText('Hello injected World!'); }); it('uses the most recent provider override', () => { TestBed.overrideProvider(NAME, {useValue: 'injected World!'}); TestBed.overrideProvider(NAME, {useValue: 'injected World a second time!'}); const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.nativeElement).toHaveText('Hello injected World a second time!'); }); it('overrides a providers in an array', () => { TestBed.configureTestingModule({ imports: [HelloWorldModule], providers: [[{provide: NAME, useValue: 'injected World!'}]], }); TestBed.overrideProvider(NAME, {useValue: 'injected World a second time!'}); const hello = TestBed.createComponent(HelloWorld); hello.detectChanges(); expect(hello.nativeElement).toHaveText('Hello injected World a second time!'); }); it('should not call ngOnDestroy for a service that was overridden', () => { SimpleService.ngOnDestroyCalls = 0; TestBed.overrideProvider(SimpleService, {useValue: {id: 2, ngOnDestroy: () => {}}}); const fixture = TestBed.createComponent(CmpWithProviders); fixture.detectChanges(); const service = TestBed.inject(SimpleService); expect(service.id).toBe(2); fixture.destroy(); // verify that original `ngOnDestroy` was not called expect(SimpleService.ngOnDestroyCalls).toBe(0); }); it('should be able to create a fixture if a test module is reset mid-compilation', async () => { const token = new InjectionToken<number>('value'); @Component({ template: 'hello {{_token}}', standalone: false, }) class TestComponent { constructor(@Inject(token) public _token: number) {} } TestBed.resetTestingModule(); // Reset the state from `beforeEach`. function compile(tokenValue: number) { return TestBed.configureTestingModule({ declarations: [TestComponent], providers: [{provide: token, useValue: tokenValue}], teardown: {destroyAfterEach: true}, }).compileComponents(); } const initialCompilation = compile(1); TestBed.resetTestingModule(); await initialCompilation; await compile(2); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); expect(fixture.nativeElement).toHaveText('hello 2'); }); describe('module overrides using TestBed.overrideModule', () => { @Component({ selector: 'test-cmp', template: '...', standalone: false, }) class TestComponent { testField = 'default'; } @NgModule({ declarations: [TestComponent], exports: [TestComponent], }) class TestModule {} @Component({ selector: 'app-root', template: `<test-cmp #testCmpCtrl></test-cmp>`, standalone: false, }) class AppComponent { @ViewChild('testCmpCtrl', {static: true}) testCmpCtrl!: TestComponent; } @NgModule({ declarations: [AppComponent], imports: [TestModule], }) class AppModule {} @Component({ selector: 'test-cmp', template: '...', standalone: false, }) class MockTestComponent { testField = 'overwritten'; } it('should allow declarations override', () => { TestBed.configureTestingModule({ imports: [AppModule], }); // replace TestComponent with MockTestComponent TestBed.overrideModule(TestModule, { remove: {declarations: [TestComponent], exports: [TestComponent]}, add: {declarations: [MockTestComponent], exports: [MockTestComponent]}, }); const fixture = TestBed.createComponent(AppComponent); const app = fixture.componentInstance; expect(app.testCmpCtrl.testField).toBe('overwritten'); }); }); describe('nested module overrides using TestBed.overrideModule', () => { // Set up an NgModule hierarchy with two modules, A and B, each with their own component. // Module B additionally re-exports module A. Also declare two mock components which can be // used in tests to verify that overrides within this hierarchy are working correctly. // ModuleA content: @Component({ selector: 'comp-a', template: 'comp-a content', standalone: false, }) class CompA {} @Component({ selector: 'comp-a', template: 'comp-a mock content', standalone: false, }) class MockCompA {} @NgModule({ declarations: [CompA], exports: [CompA], }) class ModuleA {} // ModuleB content: @Component({ selector: 'comp-b', template: 'comp-b content', standalone: false, }) class CompB {} @Component({ selector: 'comp-b', template: 'comp-b mock content', standalone: false, }) class MockCompB {} @NgModule({ imports: [ModuleA], declarations: [CompB], exports: [CompB, ModuleA], }) class ModuleB {} // AppModule content: @Component({ selector: 'app', template: ` <comp-a></comp-a> <comp-b></comp-b> `, standalone: false, }) class App {} @NgModule({ imports: [ModuleB], exports: [ModuleB], }) class AppModule {} it('should detect nested module override', () => { TestBed.configureTestingModule({ declarations: [App], // AppModule -> ModuleB -> ModuleA (to be overridden) imports: [AppModule], }).overrideModule(ModuleA, { remove: {declarations: [CompA], exports: [CompA]}, add: {declarations: [MockCompA], exports: [MockCompA]}, }); const fixture = TestBed.createComponent(App); fixture.detectChanges(); // CompA is overridden, expect mock content. expect(fixture.nativeElement.textContent).toContain('comp-a mock content'); // CompB is not overridden, expect original content. expect(fixture.nativeElement.textContent).toContain('comp-b content'); }); it('should detect chained modules override', () => { TestBed.configureTestingModule({ declarations: [App], // AppModule -> ModuleB (to be overridden) -> ModuleA (to be overridden) imports: [AppModule], }) .overrideModule(ModuleA, { remove: {declarations: [CompA], exports: [CompA]}, add: {declarations: [MockCompA], exports: [MockCompA]}, }) .overrideModule(ModuleB, { remove: {declarations: [CompB], exports: [CompB]}, add: {declarations: [MockCompB], exports: [MockCompB]}, }); const fixture = TestBed.createComponent(App); fixture.detectChanges(); // Both CompA and CompB are overridden, expect mock content for both. expect(fixture.nativeElement.textContent).toContain('comp-a mock content'); expect(fixture.nativeElement.textContent).toContain('comp-b mock content'); }); }); describe('multi providers', () => { const multiToken = new InjectionToken<string[]>('multiToken'); const singleToken = new InjectionToken<string>('singleToken'); const multiTokenToOverrideAtModuleLevel = new InjectionToken<string[]>( 'moduleLevelMultiOverride', ); @NgModule({providers: [{provide: multiToken, useValue: 'valueFromModule', multi: true}]}) class MyModule {} @NgModule({ providers: [ {provide: singleToken, useValue: 't1'}, { provide: multiTokenToOverrideAtModuleLevel, useValue: 'multiTokenToOverrideAtModuleLevelOriginal', multi: true, }, {provide: multiToken, useValue: 'valueFromModule2', multi: true}, {provide: multiToken, useValue: 'secondValueFromModule2', multi: true}, ], }) class MyModule2 {} beforeEach(() => { TestBed.configureTestingModule({ imports: [ MyModule, { ngModule: MyModule2, providers: [ {provide: multiTokenToOverrideAtModuleLevel, useValue: 'override', multi: true}, ], }, ], }); }); it('is preserved when other provider is overridden', () => { TestBed.overrideProvider(singleToken, {useValue: ''}); expect(TestBed.inject(multiToken).length).toEqual(3); expect(TestBed.inject(multiTokenToOverrideAtModuleLevel).length).toEqual(2); expect(TestBed.inject(multiTokenToOverrideAtModuleLevel)).toEqual([ 'multiTokenToOverrideAtModuleLevelOriginal', 'override', ]); }); it('overridden with an array', () => { const overrideValue = ['override']; TestBed.overrideProvider(multiToken, {useValue: overrideValue, multi: true}); const value = TestBed.inject(multiToken); expect(value.length).toEqual(overrideValue.length); expect(value).toEqual(overrideValue); }); it('overridden with a non-array', () => { // This is actually invalid because multi providers return arrays. We have this here so we can // ensure Ivy behaves the same as VE does currently. const overrideValue = 'override'; TestBed.overrideProvider(multiToken, {useValue: overrideValue, multi: true}); const value = TestBed.inject(multiToken); expect(value.length).toEqual(overrideValue.length); expect(value).toEqual(overrideValue as {} as string[]); }); }); describe('overrides providers in ModuleWithProviders', () => { const TOKEN = new InjectionToken<string[]>('token'); @NgModule() class MyMod { static multi = false; static forRoot() { return { ngModule: MyMod, providers: [{provide: TOKEN, multi: MyMod.multi, useValue: 'forRootValue'}], }; } } beforeEach(() => (MyMod.multi = true)); it('when provider is a "regular" provider', () => { MyMod.multi = false; @NgModule({imports: [MyMod.forRoot()]}) class MyMod2 {} TestBed.configureTestingModule({imports: [MyMod2]}); TestBed.overrideProvider(TOKEN, {useValue: ['override']}); expect(TestBed.inject(TOKEN)).toEqual(['override']); }); it('when provider is multi', () => { @NgModule({imports: [MyMod.forRoot()]}) class MyMod2 {} TestBed.configureTestingModule({imports: [MyMod2]}); TestBed.overrideProvider(TOKEN, {useValue: ['override']}); expect(TestBed.inject(TOKEN)).toEqual(['override']); }); it('restores the original value', () => { @NgModule({imports: [MyMod.forRoot()]}) class MyMod2 {} TestBed.configureTestingModule({imports: [MyMod2]}); TestBed.overrideProvider(TOKEN, {useValue: ['override']}); expect(TestBed.inject(TOKEN)).toEqual(['override']); TestBed.resetTestingModule(); TestBed.configureTestingModule({imports: [MyMod2]}); expect(TestBed.inject(TOKEN)).toEqual(['forRootValue']); }); }); describe('bindings', () => { it('should be able to bind to inputs', () => { @Component({template: ''}) class TestComp { @Input() value = 0; } const value = signal(1); const fixture = TestBed.createComponent(TestComp, { bindings: [inputBinding('value', value)], }); fixture.detectChanges(); expect(fixture.componentInstance.value).toBe(1); value.set(2); fixture.detectChanges(); expect(fixture.componentInstance.value).toBe(2); }); it('should be able to bind to outputs', () => { let count = 0; @Component({template: '<button (click)="event.emit()">Click me</button>'}) class TestComp { @Output() event = new EventEmitter<void>(); } const fixture = TestBed.createComponent(TestComp, { bindings: [outputBinding('event', () => count++)], }); fixture.detectChanges(); const button = fixture.nativeElement.querySelector('button'); expect(count).toBe(0); button.click(); fixture.detectChanges(); expect(count).toBe(1); }); it('should be able to bind two-way bindings', () => { @Component({template: 'Value: {{value}}'}) class TestComp { @Input() value = ''; @Output() valueChange = new EventEmitter<string>(); } const value = signal('initial'); const fixture = TestBed.createComponent(TestComp, { bindings: [twoWayBinding('value', value)], }); fixture.detectChanges(); expect(value()).toBe('initial'); expect(fixture.nativeElement.textContent).toBe('Value: initial'); value.set('1'); fixture.detectChanges(); expect(value()).toBe('1'); expect(fixture.nativeElement.textContent).toBe('Value: 1'); fixture.componentInstance.value = '2'; fixture.componentInstance.valueChange.emit('2'); fixture.detectChanges(); expect(value()).toBe('2'); expect(fixture.nativeElement.textContent).toBe('Value: 2'); }); }); it('should allow overriding a provider defined via ModuleWithProviders (using TestBed.overrideProvider)', () => { const serviceOverride = { get() { return 'override'; }, }; @Injectable({providedIn: 'root'}) class MyService { get() { return 'original'; } } @NgModule({}) class MyModule { static forRoot(): ModuleWithProviders<MyModule> { return { ngModule: MyModule, providers: [MyService], }; } } TestBed.overrideProvider(MyService, {useValue: serviceOverride}); TestBed.configureTestingModule({ imports: [MyModule.forRoot()], }); const service = TestBed.inject(MyService); expect(service.get()).toEqual('override'); }); it('should handle overrides for a provider that has `ChangeDetectorRef` as a dependency', () => { @Injectable({providedIn: 'root'}) class MyService { token = 'original'; constructor(public cdr: ChangeDetectorRef) {} } TestBed.configureTestingModule({}); TestBed.overrideProvider(MyService, {useValue: {token: 'override'}}); const service = TestBed.inject(MyService); expect(service.token).toBe('override'); }); it('should allow overriding a provider defined via ModuleWithProviders (using TestBed.configureTestingModule)', () => { const serviceOverride = { get() { return 'override'; }, }; @Injectable({providedIn: 'root'}) class MyService { get() { return 'original'; } } @NgModule({}) class MyModule { static forRoot(): ModuleWithProviders<MyModule> { return { ngModule: MyModule, providers: [MyService], }; } } TestBed.configureTestingModule({ imports: [MyModule.forRoot()], providers: [{provide: MyService, useValue: serviceOverride}], }); const service = TestBed.inject(MyService); expect(service.get()).toEqual('override'); }); it('should allowing overriding a module with a cyclic structure in its metadata', () => { class Cyclic { cycle = this; constructor(public name: string) {} } const CYCLES = new InjectionToken<Cyclic[]>('cycles', {factory: () => []}); @NgModule({ providers: [ {provide: CYCLES, useValue: new Cyclic('a'), multi: true}, {provide: CYCLES, useValue: new Cyclic('b'), multi: true}, ], }) class TestModule {} TestBed.configureTestingModule({ imports: [TestModule], }).overrideModule(TestModule, { remove: { providers: [ // Removing the cycle named "a" should result in removing the provider for "a". // Note: although this removes a different instance than the one provided, metadata // overrides compare objects by value, not by reference. {provide: CYCLES, useValue: new Cyclic('a'), multi: true}, // Also attempt to remove a cycle named "B" (which does not exist) to verify that // objects are correctly compared by value. {provide: CYCLES, useValue: new Cyclic('B'), multi: true}, ], }, add: { providers: [{provide: CYCLES, useValue: new Cyclic('c'), multi: true}], }, }); const values = TestBed.inject(CYCLES); expect(values.map((v) => v.name)).toEqual(['b', 'c']); }); it('overrides injectable that is using providedIn: AModule', () => { @NgModule() class ServiceModule {} @Injectable({providedIn: ServiceModule}) class Service {} const fake = 'fake'; TestBed.overrideProvider(Service, {useValue: fake}); // Create an injector whose source is the ServiceModule, not DynamicTestModule. const ngModuleFactory = TestBed.inject(Compiler).compileModuleSync(ServiceModule); const injector = ngModuleFactory.create(TestBed.inject(Injector)).injector; const service = injector.get(Service); expect(service).toBe(fake); }); it('allow to override multi provider', () => { const MY_TOKEN = new InjectionToken('MyProvider'); class MyProvider {} @Component({ selector: 'my-comp', template: ``, standalone: false, }) class MyComp { constructor(@Inject(MY_TOKEN) public myProviders: MyProvider[]) {} } TestBed.configureTestingModule({ declarations: [MyComp], providers: [{provide: MY_TOKEN, useValue: {value: 'old provider'}, multi: true}], }); const multiOverride = {useValue: [{value: 'new provider'}], multi: true}; TestBed.overrideProvider(MY_TOKEN, multiOverride); const fixture = TestBed.createComponent(MyComp); expect(fixture.componentInstance.myProviders).toEqual([{value: 'new provider'}]); }); it('should resolve components that are extended by other components', () => { // SimpleApp uses SimpleCmp in its template, which is extended by InheritedCmp const simpleApp = TestBed.createComponent(SimpleApp); simpleApp.detectChanges(); expect(simpleApp.nativeElement).toHaveText('simple - inherited'); }); it('should not trigger change detection for ComponentA while calling TestBed.createComponent for ComponentB', () => { const log: string[] = []; @Component({ selector: 'comp-a', template: '...', standalone: false, }) class CompA { @Input() inputA: string = ''; ngOnInit() { log.push('CompA:ngOnInit', this.inputA); } } @Component({ selector: 'comp-b', template: '...', standalone: false, }) class CompB { @Input() inputB: string = ''; ngOnInit() { log.push('CompB:ngOnInit', this.inputB); } } TestBed.configureTestingModule({declarations: [CompA, CompB]}); log.length = 0; const appA = TestBed.createComponent(CompA); appA.componentInstance.inputA = 'a'; appA.autoDetectChanges(); expect(log).toEqual(['CompA:ngOnInit', 'a']); log.length = 0; const appB = TestBed.createComponent(CompB); appB.componentInstance.inputB = 'b'; appB.autoDetectChanges(); expect(log).toEqual(['CompB:ngOnInit', 'b']); }); it('should resolve components without async resources synchronously', (done) => { TestBed.configureTestingModule({ declarations: [ComponentWithInlineTemplate], }) .compileComponents() .then(done) .catch((error) => { // This should not throw any errors. If an error is thrown, the test will fail. // Specifically use `catch` here to mark the test as done and *then* throw the error // so that the test isn't treated as a timeout. done(); throw error; }); // Intentionally call `createComponent` before `compileComponents` is resolved. We want this to // work for components that don't have any async resources (templateUrl, styleUrls). TestBed.createComponent(ComponentWithInlineTemplate); }); it('should be able to override the ErrorHandler via an import', () => { class CustomErrorHandler {} @NgModule({providers: [{provide: ErrorHandler, useClass: CustomErrorHandler}]}) class ProvidesErrorHandler {} TestBed.resetTestingModule(); TestBed.configureTestingModule({imports: [ProvidesErrorHandler, HelloWorldModule]}); expect(TestBed.inject(ErrorHandler)).toEqual(jasmine.any(CustomErrorHandler)); }); it('should throw errors in CD', () => { @Component({ selector: 'my-comp', template: '', standalone: false, }) class MyComp { name!: {hello: string}; ngOnInit() { // this should throw because this.name is undefined this.name.hello = 'hello'; } } TestBed.configureTestingModule({declarations: [MyComp]}); expect(() => { const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); }).toThrowError(); }); // TODO(FW-1245): properly fix issue where errors in listeners aren't thrown and don't cause // tests to fail. This is an issue in both View Engine and Ivy, and may require a breaking // change to completely fix (since simple re-throwing breaks handlers in ngrx, etc). xit('should throw errors in listeners', () => { @Component({ selector: 'my-comp', template: '<button (click)="onClick()">Click me</button>', standalone: false, }) class MyComp { name!: {hello: string}; onClick() { // this should throw because this.name is undefined this.name.hello = 'hello'; } } TestBed.configureTestingModule({declarations: [MyComp]}); const fixture = TestBed.createComponent(MyComp); fixture.detectChanges(); expect(() => { const button = fixture.nativeElement.querySelector('button'); button.click(); }).toThrowError(); }); it('should allow both the declaration and import of a component into the testing module', () => { // This test validates that a component (Outer) which is both declared and imported // (via its module) in the testing module behaves correctly. That is: // // 1) the component should be compiled in the scope of its original module. // // This condition is tested by having the component (Outer) use another component // (Inner) within its template. Thus, if it's compiled in the correct scope then the // text 'Inner' from the template of (Inner) should appear in the result. // // 2) the component should be available in the TestingModule scope. // // This condition is tested by attempting to use the component (Outer) inside a test // fixture component (Fixture) which is declared in the testing module only. @Component({ selector: 'inner', template: 'Inner', standalone: false, }) class Inner {} @Component({ selector: 'outer', template: '<inner></inner>', standalone: false, }) class Outer {} @NgModule({ declarations: [Inner, Outer], }) class Module {} @Component({ template: '<outer></outer>', selector: 'fixture', standalone: false, }) class Fixture {} TestBed.configureTestingModule({ declarations: [Outer, Fixture], imports: [Module], }); const fixture = TestBed.createComponent(Fixture); // The Outer component should have its template stamped out, and that template should // include a correct instance of the Inner component with the 'Inner' text from its // template. expect(fixture.nativeElement.innerHTML).toEqual('<outer><inner>Inner</inner></outer>'); }); describe('checking types before compiling them', () => { @Directive({ selector: 'my-dir', standalone: false, }) class MyDir {} @NgModule() class MyModule {} // [decorator, type, overrideFn] const cases: [string, Type<any>, string][] = [ ['Component', MyDir, 'overrideComponent'], ['NgModule', MyDir, 'overrideModule'], ['Pipe', MyModule, 'overridePipe'], ['Directive', MyModule, 'overrideDirective'], ]; cases.forEach(([decorator, type, overrideFn]) => { it(`should throw an error in case invalid type is used in ${overrideFn} function`, () => { TestBed.configureTestingModule({declarations: [MyDir]}); expect(() => { (TestBed as any)[overrideFn](type, {}); TestBed.createComponent(type); }).toThrowError(new RegExp(`class doesn't have @${decorator} decorator`, 'g')); }); }); }); describe('defer blocks', () => { /** * Function returns a class that represents AOT-compiled version of the following Component: * * @Component({ * * imports: [...], * selector: '...', * template: '...', * }) * class ComponentClass {} * * This is needed to closer match the behavior of AOT pre-compiled components (compiled * outside of TestBed) for cases when defer blocks are used. */ const getAOTCompiledComponent = ( selector: string, dependencies: Array<Type<unknown>> = [], deferrableDependencies: Array<Type<unknown>> = [], ) => { class ComponentClass { static ɵfac = () => new ComponentClass(); static ɵcmp = defineComponent({ type: ComponentClass, selectors: [[selector]], decls: 2, vars: 0, dependencies, consts: [['dir']], template: (rf: any, ctx: any) => { if (rf & 1) { elementStart(0, 'div', 0); text(1, `${selector} cmp!`); elementEnd(); } }, }); } if (dependencies.length || deferrableDependencies.length) { setClassMetadataAsync( ComponentClass, function () { const promises: Array<Promise<Type<unknown>>> = deferrableDependencies.map( // Emulates a dynamic import, e.g. `import('./cmp-a').then(m => m.CmpA)` (dep) => new Promise((resolve) => setTimeout(() => resolve(dep))), ); return promises; }, function (...deferrableSymbols) { setClassMetadata( ComponentClass, [ { type: Component, args: [ { selector, imports: [...dependencies, ...deferrableSymbols], template: `<div>root cmp!</div>`, }, ], }, ], null, null, ); }, ); } return ComponentClass; }; it('should not require compilerComponents if the component with a defer-block is not overridden', async () => { const DeferredComponent = getAOTCompiledComponent('deferred'); const RootAotComponent = getAOTCompiledComponent('root', [], [DeferredComponent]); TestBed.configureTestingModule({imports: [RootAotComponent]}); // If we had overriden the component, we would need to compile it // but since we didn't, we can create the component synchronously const fixture = TestBed.createComponent(RootAotComponent); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toBe('root cmp!'); // Here we're just confirming that the same component would throw on override. TestBed.resetTestingModule() .configureTestingModule({imports: [RootAotComponent]}) .overrideComponent(RootAotComponent, { set: {template: `Override of a root template!`}, }); expect(() => TestBed.createComponent(RootAotComponent)).toThrow(); }); it('should not throw an error in AOT component is overriden but has no async metadata', () => { const RootAotComponent = getAOTCompiledComponent('root', [], []); TestBed.configureTestingModule({imports: [RootAotComponent]}); TestBed.overrideComponent(RootAotComponent, { set: {template: `Override of a root template! <nested-cmp />`}, }); expect(() => TestBed.createComponent(RootAotComponent)).not.toThrowError(); }); it('should throw an error if a deferred component is not compiled', () => { @Component({ selector: 'cmp-a', template: 'CmpA!', }) class CmpA {} const NestedAotComponent = getAOTCompiledComponent('nested-cmp', [], [CmpA]); const RootAotComponent = getAOTCompiledComponent('root', [], [NestedAotComponent]); TestBed.configureTestingModule({imports: [RootAotComponent]}); TestBed.overrideComponent(RootAotComponent, { set: {template: `Override of a root template! <nested-cmp />`}, }); TestBed.overrideComponent(NestedAotComponent, { set: {template: `Override of a nested template! <cmp-a />`}, }); // We did override but not compile, therefore we expect to throw on createComponent expect(() => TestBed.createComponent(RootAotComponent)).toThrowError( `Component 'ComponentClass' has unresolved metadata. Please call \`await TestBed.compileComponents()\` before running this test.`, ); }); it('should not throw if component is created after override+reset', async () => { @Component({ selector: 'cmp-a', template: 'CmpA!', }) class CmpA {} const NestedAotComponent = getAOTCompiledComponent('nested-cmp', [], [CmpA]); const RootAotComponent = getAOTCompiledComponent('root', [], [NestedAotComponent]); TestBed.configureTestingModule({imports: [RootAotComponent]}); TestBed.overrideComponent(RootAotComponent, { set: {template: `Override of a root template! <nested-cmp />`}, }); TestBed.overrideComponent(NestedAotComponent, { set: {template: `Override of a nested template! <cmp-a />`}, }); // Not compiled yet, so we expect to throw expect(() => TestBed.createComponent(RootAotComponent)).toThrowError(); await TestBed.compileComponents(); // We're compiled now, so we can create the component const fixture = TestBed.createComponent(RootAotComponent); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toBe( 'Override of a root template! Override of a nested template! CmpA!', ); // We reset the override TestBed.resetTestingModule(); TestBed.configureTestingModule({imports: [RootAotComponent]}); // We're back to the nominal behavior const fixture2 = TestBed.createComponent(RootAotComponent); fixture2.detectChanges(); expect(fixture2.nativeElement.textContent).toBe('root cmp!'); }); it('should handle async metadata on root and nested components', async () => { @Component({ selector: 'cmp-a', template: 'CmpA!', }) class CmpA {} const NestedAotComponent = getAOTCompiledComponent('nested-cmp', [], [CmpA]); const RootAotComponent = getAOTCompiledComponent('root', [], [NestedAotComponent]); TestBed.configureTestingModule({imports: [RootAotComponent]}); TestBed.overrideComponent(RootAotComponent, { set: {template: `Override of a root template! <nested-cmp />`}, }); TestBed.overrideComponent(NestedAotComponent, { set: {template: `Override of a nested template! <cmp-a />`}, }); // We need to compile the component because it has async metadata + overrides await TestBed.compileComponents(); const fixture = TestBed.createComponent(RootAotComponent); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toBe( 'Override of a root template! Override of a nested template! CmpA!', ); }); it('should override providers on dependencies of dynamically loaded components', async () => { function timer(delay: number): Promise<void> { return new Promise<void>((resolve) => { setTimeout(() => resolve(), delay); }); } @Injectable({providedIn: 'root'}) class ImportantService { value = 'original'; } @NgModule({ providers: [ImportantService], }) class ThisModuleProvidesService {} @Component({ selector: 'child', imports: [ThisModuleProvidesService], template: '<h1>{{value}}</h1>', }) class ChildCmp { service = inject(ImportantService); value = this.service.value; } @Component({ selector: 'parent', imports: [ChildCmp], template: ` @defer (when true) { <child /> } `, }) class ParentCmp {} const deferrableDependencies = [ChildCmp]; setClassMetadataAsync( ParentCmp, function () { const promises: Array<Promise<Type<unknown>>> = deferrableDependencies.map( // Emulates a dynamic import, e.g. `import('./cmp-a').then(m => m.CmpA)` (dep) => new Promise((resolve) => setTimeout(() => resolve(dep))), ); return promises; }, function (...deferrableSymbols) { setClassMetadata( ParentCmp, [ { type: Component, args: [ { selector: 'parent', imports: [...deferrableSymbols], template: `<div>root cmp!</div>`, }, ], }, ], null, null, ); }, ); // Set `PLATFORM_ID` to a browser platform value to trigger defer loading // while running tests in Node. const COMMON_PROVIDERS = [{provide: PLATFORM_ID, useValue: ɵPLATFORM_BROWSER_ID}]; TestBed.configureTestingModule({imports: [ParentCmp], providers: [COMMON_PROVIDERS]}); TestBed.overrideProvider(ImportantService, {useValue: {value: 'overridden'}}); // We need to compile the component because it has async metadata + overrides await TestBed.compileComponents(); const fixture = TestBed.createComponent(ParentCmp); fixture.detectChanges(); await timer(10); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toContain('overridden'); }); it('should allow import overrides on components with async metadata', async () => { const DeferredComponent = getAOTCompiledComponent('deferred', [], []); const NestedAotComponent = getAOTCompiledComponent('nested-cmp', [], []); const RootAotComponent = getAOTCompiledComponent('root', [], [DeferredComponent]); TestBed.configureTestingModule({imports: [RootAotComponent]}); TestBed.overrideComponent(RootAotComponent, { set: { // Adding an import that was not present originally imports: [NestedAotComponent], template: `Override of a root template! <nested-cmp />`, }, }); // We need to compile the component because it has async metadata + overrides await TestBed.compileComponents(); const fixture = TestBed.createComponent(RootAotComponent); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toBe( 'Override of a root template! nested-cmp cmp!', ); }); it('should throw when overriding template', async () => { const DeferredComponent = getAOTCompiledComponent('deferred', [], []); const RootAotComponent = getAOTCompiledComponent('root', [], [DeferredComponent]); TestBed.overrideTemplate(RootAotComponent, 'foobar'); expect(() => TestBed.createComponent(RootAotComponent)).toThrowError( /has unresolved metadata/, ); }); it('should throw if overriding template', async () => { const DeferredComponent = getAOTCompiledComponent('deferred', [], []); const RootAotComponent = getAOTCompiledComponent('root', [], [DeferredComponent]); class Foo {} TestBed.overrideComponent(RootAotComponent, {set: {providers: [Foo]}}); expect(() => TestBed.createComponent(RootAotComponent)).toThrowError( /has unresolved metadata/, ); }); describe('ensure AsyncMetadata loading is side-effect free', () => { let Component: any; let run = 1; beforeEach(() => { const DeferredComponent = getAOTCompiledComponent('deferred', [], []); Component = getAOTCompiledComponent('root', [], [DeferredComponent]); TestBed.overrideComponent(Component, {set: {template: 'bar'}}); }); it('should throw if creating an overridden component', async () => { if (run === 1) { await TestBed.compileComponents(); const fixture = TestBed.createComponent(Component); expect(fixture.nativeElement.textContent).toBe('bar'); } else { // Component was compiled in the previous test // but we still require compileComponents because of the override expect(() => TestBed.createComponent(Component)).toThrowError(); } run++; }); it('should throw if creating an overridden component (run 2)', async () => { if (run === 1) { await TestBed.compileComponents(); const fixture = TestBed.createComponent(Component); expect(fixture.nativeElement.textContent).toBe('bar'); } else { // Component was compiled in the previous test // but we still require compileComponents because of the override expect(() => TestBed.createComponent(Component)).toThrowError(); } run++; }); }); }); describe('AOT pre-compiled components', () => { /** * Function returns a class that represents AOT-compiled version of the following Component: * * @Component({ * standalone: true|false, * imports: [...], // for standalone only * selector: 'comp', * templateUrl: './template.ng.html', * styleUrls: ['./style.css'] * }) * class ComponentClass {} * * This is needed to closer match the behavior of AOT pre-compiled components (compiled * outside of TestBed) without changing TestBed state and/or Component metadata to compile * them via TestBed with external resources. */ const getAOTCompiledComponent = (standalone: boolean = false, dependencies: any[] = []) => { class ComponentClass { static ɵfac = () => new ComponentClass(); static ɵcmp = defineComponent({ standalone, type: ComponentClass, selectors: [['comp']], decls: 2, vars: 0, dependencies, consts: [['dir']], template: (rf: any, ctx: any) => { if (rf & 1) { elementStart(0, 'div', 0); text(1, 'Some template'); elementEnd(); } }, styles: ['body { margin: 0; }'], }); } setClassMetadata( ComponentClass, [ { type: Component, args: [ { standalone, imports: dependencies, selector: 'comp', templateUrl: './template.ng.html', styleUrls: ['./style.css'], }, ], }, ], null, null, ); return ComponentClass; }; it('should allow to override a provider used in a dependency of a standalone component', () => { const A = new InjectionToken('A'); @Directive({ selector: '[dir]', providers: [{provide: A, useValue: 'A'}], standalone: false, }) class SomeDir { constructor( @Inject(A) private tokenA: string, private elementRef: ElementRef, ) {} ngAfterViewInit() { this.elementRef.nativeElement.innerHTML = this.tokenA; } } const SomeComponent = getAOTCompiledComponent(true, [SomeDir]); TestBed.configureTestingModule({imports: [SomeComponent]}); TestBed.overrideProvider(A, {useValue: 'Overridden A'}); const fixture = TestBed.createComponent(SomeComponent); fixture.detectChanges(); expect(fixture.nativeElement.textContent).toBe('Overridden A'); }); it('should have an ability to override template', () => { const SomeComponent = getAOTCompiledComponent(); TestBed.configureTestingModule({declarations: [SomeComponent]}); TestBed.overrideTemplateUsingTestingModule(SomeComponent, 'Template override'); const fixture = TestBed.createComponent(SomeComponent); expect(fixture.nativeElement.innerHTML).toBe('Template override'); }); it('should have an ability to override template with empty string', () => { const SomeComponent = getAOTCompiledComponent(); TestBed.configureTestingModule({declarations: [SomeComponent]}); TestBed.overrideTemplateUsingTestingModule(SomeComponent, ''); const fixture = TestBed.createComponent(SomeComponent); expect(fixture.nativeElement.innerHTML).toBe(''); }); it('should allow component in both in declarations and imports', () => { const SomeComponent = getAOTCompiledComponent(); // This is an AOT compiled module which declares (but does not export) SomeComponent. class ModuleClass { static ɵmod = defineNgModule({ type: ModuleClass, declarations: [SomeComponent], }); } @Component({ template: '<comp></comp>', selector: 'fixture', standalone: false, }) class TestFixture {} TestBed.configureTestingModule({ // Here, SomeComponent is both declared, and then the module which declares it is // also imported. This used to be a duplicate declaration error, but is now interpreted // to mean: // 1) Compile (or reuse) SomeComponent in the context of its original NgModule // 2) Make SomeComponent available in the scope of the testing module, even if it wasn't // originally exported from its NgModule. // // This allows TestFixture to use SomeComponent, which is asserted below. declarations: [SomeComponent, TestFixture], imports: [ModuleClass], }); const fixture = TestBed.createComponent(TestFixture); // The regex avoids any issues with styling attributes. expect(fixture.nativeElement.innerHTML).toMatch( /<comp[^>]*><div[^>]*>Some template<\/div><\/comp>/, ); }); }); describe('resetting ng defs', () => { it('should restore ng defs to their initial states', () => { @Pipe({ name: 'somePipe', pure: true, standalone: false, }) class SomePipe { transform(value: string): string { return `transformed ${value}`; } } @Directive({ selector: 'someDirective', standalone: false, }) class SomeDirective { someProp = 'hello'; } @Component({ selector: 'comp', template: 'someText', standalone: false, }) class SomeComponent {} @NgModule({declarations: [SomeComponent]}) class SomeModule {} TestBed.configureTestingModule({imports: [SomeModule]}); // adding Pipe and Directive via metadata override TestBed.overrideModule(SomeModule, { set: {declarations: [SomeComponent, SomePipe, SomeDirective]}, }); TestBed.overrideComponent(SomeComponent, { set: {template: `<span someDirective>{{'hello' | somePipe}}</span>`}, }); TestBed.createComponent(SomeComponent); const cmpDefBeforeReset = (SomeComponent as any).ɵcmp; expect(cmpDefBeforeReset.pipeDefs().length).toEqual(1); expect(cmpDefBeforeReset.directiveDefs().length).toEqual(2); // directive + component const scopeBeforeReset = depsTracker.getNgModuleScope(SomeModule as NgModuleType); expect(scopeBeforeReset.compilation.pipes.size).toEqual(1); expect(scopeBeforeReset.compilation.directives.size).toEqual(2); TestBed.resetTestingModule(); const cmpDefAfterReset = (SomeComponent as any).ɵcmp; expect(cmpDefAfterReset.pipeDefs).toBe(null); expect(cmpDefAfterReset.directiveDefs).toBe(null); const scopeAfterReset = depsTracker.getNgModuleScope(SomeModule as NgModuleType); expect(scopeAfterReset).toEqual({ compilation: { pipes: new Set(), directives: new Set([SomeComponent]), }, exported: { pipes: new Set(), directives: new Set(), }, }); }); it('should cleanup ng defs for classes with no ng annotations (in case of inheritance)', () => { @Component({ selector: 'someDirective', template: '...', standalone: false, }) class SomeComponent {} class ComponentWithNoAnnotations extends SomeComponent {} @Directive({ selector: 'some-directive', standalone: false, }) class SomeDirective {} class DirectiveWithNoAnnotations extends SomeDirective {} @Pipe({ name: 'some-pipe', standalone: false, }) class SomePipe {} class PipeWithNoAnnotations extends SomePipe {} TestBed.configureTestingModule({ declarations: [ ComponentWithNoAnnotations, DirectiveWithNoAnnotations, PipeWithNoAnnotations, ], }); TestBed.createComponent(ComponentWithNoAnnotations); expect(ComponentWithNoAnnotations.hasOwnProperty('ɵcmp')).toBeTruthy(); expect(SomeComponent.hasOwnProperty('ɵcmp')).toBeTruthy(); expect(DirectiveWithNoAnnotations.hasOwnProperty('ɵdir')).toBeTruthy(); expect(SomeDirective.hasOwnProperty('ɵdir')).toBeTruthy(); expect(PipeWithNoAnnotations.hasOwnProperty('ɵpipe')).toBeTruthy(); expect(SomePipe.hasOwnProperty('ɵpipe')).toBeTruthy(); TestBed.resetTestingModule(); // ng defs should be removed from classes with no annotations expect(ComponentWithNoAnnotations.hasOwnProperty('ɵcmp')).toBeFalsy(); expect(DirectiveWithNoAnnotations.hasOwnProperty('ɵdir')).toBeFalsy(); expect(PipeWithNoAnnotations.hasOwnProperty('ɵpipe')).toBeFalsy(); // ng defs should be preserved on super types expect(SomeComponent.hasOwnProperty('ɵcmp')).toBeTruthy(); expect(SomeDirective.hasOwnProperty('ɵdir')).toBeTruthy(); expect(SomePipe.hasOwnProperty('ɵpipe')).toBeTruthy(); }); it('should cleanup scopes (configured via `TestBed.configureTestingModule`) between tests', () => { @Component({ selector: 'child', template: 'Child comp', standalone: false, }) class ChildCmp {} @Component({ selector: 'root', template: '<child></child>', standalone: false, }) class RootCmp {} // Case #1: `RootCmp` and `ChildCmp` are both included in the `declarations` field of // the testing module, so `ChildCmp` is in the scope of `RootCmp`. TestBed.configureTestingModule({ declarations: [RootCmp, ChildCmp], }); let fixture = TestBed.createComponent(RootCmp); fixture.detectChanges(); let childCmpInstance = fixture.debugElement.query(By.directive(ChildCmp)); expect(childCmpInstance.componentInstance).toBeInstanceOf(ChildCmp); expect(fixture.nativeElement.textContent).toBe('Child comp'); TestBed.resetTestingModule(); const spy = spyOn(console, 'error'); // Case #2: the `TestBed.configureTestingModule` was not invoked, thus the `ChildCmp` // should not be available in the `RootCmp` scope and no child content should be // rendered. fixture = TestBed.createComponent(RootCmp); // also an error should be logged to the user informing them that // the child component is not part of the module expect(spy).toHaveBeenCalledTimes(1); fixture.detectChanges(); childCmpInstance = fixture.debugElement.query(By.directive(ChildCmp)); expect(childCmpInstance).toBeNull(); expect(fixture.nativeElement.textContent).toBe(''); TestBed.resetTestingModule(); // Case #3: `ChildCmp` is included in the `declarations` field, but `RootCmp` is not, // so `ChildCmp` is NOT in the scope of `RootCmp` component. TestBed.configureTestingModule({ declarations: [ChildCmp], }); fixture = TestBed.createComponent(RootCmp); fixture.detectChanges(); childCmpInstance = fixture.debugElement.query(By.directive(ChildCmp)); expect(childCmpInstance).toBeNull(); expect(fixture.nativeElement.textContent).toBe(''); }); it('should clean up overridden providers for modules that are imported more than once', () => { @Injectable() class Token { name: string = 'real'; } @NgModule({ providers: [Token], }) class Module {} TestBed.configureTestingModule({imports: [Module, Module]}); TestBed.overrideProvider(Token, {useValue: {name: 'fake'}}); expect(TestBed.inject(Token).name).toEqual('fake'); TestBed.resetTestingModule(); // The providers for the module should have been restored to the original array, with // no trace of the overridden providers. expect((Module as any).ɵinj.providers).toEqual([Token]); }); it('should clean up overridden providers on components whose modules are compiled more than once', async () => { @Injectable() class SomeInjectable { id: string | undefined; } @Component({ providers: [SomeInjectable], standalone: false, }) class ComponentWithProvider { constructor(readonly injectable: SomeInjectable) {} } @NgModule({declarations: [ComponentWithProvider]}) class MyModule {} TestBed.configureTestingModule({imports: [MyModule]}); const originalResolver = (ComponentWithProvider as any).ɵcmp.providersResolver; TestBed.overrideProvider(SomeInjectable, {useValue: {id: 'fake'}}); const compiler = TestBed.inject(Compiler); await compiler.compileModuleAsync(MyModule); compiler.compileModuleSync(MyModule); TestBed.resetTestingModule(); expect((ComponentWithProvider as any).ɵcmp.providersResolver).toEqual(originalResolver); }); }); describe('overrides provider', () => { it('with empty provider object', () => { @Injectable() class Service {} TestBed.overrideProvider(Service, {}); // Should be able to get a Service instance because it has no dependencies that can't be // resolved expect(TestBed.inject(Service)).toBeDefined(); }); }); it('should handle provider overrides when module imports are provided as a function', () => { class InjectedString { value?: string; } @Component({ template: '{{injectedString.value}}', standalone: false, }) class AppComponent { constructor(public injectedString: InjectedString) {} } @NgModule({}) class DependencyModule {} // We need to write the compiler output manually here, // because it depends on code generated by ngcc. class TestingModule { static ɵmod = defineNgModule({type: TestingModule}); static ɵinj = defineInjector({imports: [DependencyModule]}); } setNgModuleScope(TestingModule, {imports: () => [DependencyModule]}); TestBed.configureTestingModule({ imports: [TestingModule], declarations: [AppComponent], providers: [{provide: InjectedString, useValue: {value: 'initial'}}], }); TestBed.overrideProvider(InjectedString, {useValue: {value: 'changed'}}); const fixture = TestBed.createComponent(AppComponent); fixture.detectChanges(); expect(fixture!.nativeElement.textContent).toContain('changed'); }); describe('TestBed.inject', () => { describe('injection flags', () => { it('should be able to optionally inject a token', () => { const TOKEN = new InjectionToken<string>('TOKEN'); expect(TestBed.inject(TOKEN, undefined, {optional: true})).toBeNull(); expect(TestBed.inject(TOKEN, undefined, {optional: true})).toBeNull(); }); it('should include `null` into the result type when the optional flag is used', () => { const TOKEN = new InjectionToken<string>('TOKEN'); const flags: InjectOptions = {optional: true}; let result = TestBed.inject(TOKEN, undefined, flags); expect(result).toBe(null); // Verify that `null` can be a valid value (from typing standpoint), // the line below would fail a type check in case the result doesn't // have `null` in the type. result = null; }); it('should be able to use skipSelf injection', () => { const TOKEN = new InjectionToken<string>('TOKEN'); TestBed.configureTestingModule({ providers: [{provide: TOKEN, useValue: 'from TestBed'}], }); expect(TestBed.inject(TOKEN)).toBe('from TestBed'); expect(TestBed.inject(TOKEN, undefined, {skipSelf: true, optional: true})).toBeNull(); }); }); }); it('should be able to call Testbed.runInInjectionContext in tests', () => { const expectedValue = 'testValue'; @Injectable({providedIn: 'root'}) class SomeInjectable { readonly instanceValue = expectedValue; } function functionThatUsesInject(): string { return inject(SomeInjectable).instanceValue; } expect(TestBed.runInInjectionContext(functionThatUsesInject)).toEqual(expectedValue); }); describe('TestBed.tick', () => { @Component({ template: '{{state()}}', }) class Thing1 { state = signal(1); } describe('with zone change detection', () => { it('should update fixtures with autoDetect', () => { TestBed.configureTestingModule({ providers: [ {provide: ComponentFixtureAutoDetect, useValue: true}, provideZoneChangeDetection(), ], }); const {nativeElement, componentInstance} = TestBed.createComponent(Thing1); expect(nativeElement.textContent).toBe('1'); componentInstance.state.set(2); TestBed.tick(); expect(nativeElement.textContent).toBe('2'); }); it('should update fixtures without autoDetect', () => { const {nativeElement, componentInstance} = TestBed.createComponent(Thing1); expect(nativeElement.textContent).toBe(''); // change detection didn't run yet componentInstance.state.set(2); TestBed.tick(); expect(nativeElement.textContent).toBe('2'); }); }); describe('with zoneless change detection', () => { beforeEach(() => { TestBed.configureTestingModule({ providers: [provideZonelessChangeDetection()], }); }); it('should update fixtures with zoneless', async () => { const fixture = TestBed.createComponent(Thing1); await fixture.whenStable(); const {nativeElement, componentInstance} = fixture; expect(nativeElement.textContent).toBe('1'); componentInstance.state.set(2); TestBed.tick(); expect(nativeElement.textContent).toBe('2'); }); }); }); describe('inferTagName', () => { it('should not infer the tag name of the root component by default', () => { @Component({selector: 'my-test-comp[foo]', template: ''}) class TestComp {} const fixture = TestBed.createComponent(TestComp); expect(fixture.nativeElement.tagName).toBe('DIV'); }); it('should be able to opt into inferring the tag of the root component from the selector', () => { @Component({selector: 'my-test-comp[foo]', template: ''}) class TestComp {} const fixture = TestBed.createComponent(TestComp, {inferTagName: true}); expect(fixture.nativeElement.tagName).toBe('MY-TEST-COMP'); }); it('should fall back to `div` if the test component does not have a tag selector', () => { @Component({selector: '[foo]', template: ''}) class TestComp {} const fixture = TestBed.createComponent(TestComp, {inferTagName: true}); expect(fixture.nativeElement.tagName).toBe('DIV'); }); it('should fall back to `ng-component` if the test component does not have any selector', () => { @Component({template: ''}) class TestComp {} const fixture = TestBed.createComponent(TestComp, {inferTagName: true}); expect(fixture.nativeElement.tagName).toBe('NG-COMPONENT'); }); it('should be able to opt into inferring the tag name through configureTestingModule', () => { @Component({selector: 'my-test-comp', template: ''}) class TestComp {} TestBed.configureTestingModule({inferTagName: true}); const fixture = TestBed.createComponent(TestComp); expect(fixture.nativeElement.tagName).toBe('MY-TEST-COMP'); }); it('should give precedence to inferTagName from createComponent over configureTestingModule', () => { @Component({selector: 'my-test-comp', template: ''}) class TestComp {} TestBed.configureTestingModule({inferTagName: false}); const fixture = TestBed.createComponent(TestComp); expect(fixture.nativeElement.tagName).toBe('DIV'); }); }); }); describe('TestBed defer block behavior', () => { beforeEach(() => { TestBed.resetTestingModule(); }); it('should default defer block behavior to playthrough', () => { expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Playthrough); }); it('should be able to configure defer block behavior', () => { TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Manual}); expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Manual); }); it('should reset the defer block behavior back to the default when TestBed is reset', () => { TestBed.configureTestingModule({deferBlockBehavior: DeferBlockBehavior.Manual}); expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Manual); TestBed.resetTestingModule(); expect(TestBedImpl.INSTANCE.getDeferBlockBehavior()).toBe(DeferBlockBehavior.Playthrough); }); }); describe('TestBed animations behavior', () => { beforeEach(() => { TestBed.resetTestingModule(); }); it('should default animations behavior to disabled', () => { expect(TestBedImpl.INSTANCE.getAnimationsEnabled()).toBe(false); }); it('should be able to configure animations behavior', () => { TestBed.configureTestingModule({animationsEnabled: true}); expect(TestBedImpl.INSTANCE.getAnimationsEnabled()).toBe(true); }); it('should reset the animations behavior back to the default when TestBed is reset', () => { TestBed.configureTestingModule({animationsEnabled: true}); expect(TestBedImpl.INSTANCE.getAnimationsEnabled()).toBe(true); TestBed.resetTestingModule(); expect(TestBedImpl.INSTANCE.getAnimationsEnabled()).toBe(false); }); }); describe('TestBed module teardown', () => { beforeEach(() => { TestBed.resetTestingModule(); }); it('should tear down the test module by default', () => { expect(TestBedImpl.INSTANCE.shouldTearDownTestingModule()).toBe(true); }); it('should be able to configure the teardown behavior', () => { TestBed.configureTestingModule({teardown: {destroyAfterEach: false}}); expect(TestBedImpl.INSTANCE.shouldTearDownTestingModule()).toBe(false); }); it('should reset the teardown behavior back to the default when TestBed is reset', () => { TestBed.configureTestingModule({teardown: {destroyAfterEach: false}}); expect(TestBedImpl.INSTANCE.shouldTearDownTestingModule()).toBe(false); TestBed.resetTestingModule(); expect(TestBedImpl.INSTANCE.shouldTearDownTestingModule()).toBe(true); }); it('should destroy test module providers when test module teardown is enabled', () => { SimpleService.ngOnDestroyCalls = 0; TestBed.configureTestingModule({ providers: [SimpleService], declarations: [GreetingCmp], teardown: {destroyAfterEach: true}, }); TestBed.createComponent(GreetingCmp); expect(SimpleService.ngOnDestroyCalls).toBe(0); TestBed.resetTestingModule(); expect(SimpleService.ngOnDestroyCalls).toBe(1); }); it('should not error on mocked and partially-implemented `DOCUMENT`', () => { SimpleService.ngOnDestroyCalls = 0; TestBed.configureTestingModule({ providers: [SimpleService, {provide: DOCUMENT, useValue: {}}], teardown: {destroyAfterEach: true}, }); TestBed.inject(SimpleService); expect(SimpleService.ngOnDestroyCalls).toBe(0); TestBed.resetTestingModule(); expect(SimpleService.ngOnDestroyCalls).toBe(1); }); it('should remove the fixture root element from the DOM when module teardown is enabled', () => { TestBed.configureTestingModule({ declarations: [SimpleCmp], teardown: {destroyAfterEach: true}, }); const fixture = TestBed.createComponent(SimpleCmp); const fixtureDocument = fixture.nativeElement.ownerDocument; expect(fixtureDocument.body.contains(fixture.nativeElement)).toBe(true); TestBed.resetTestingModule(); expect(fixtureDocument.body.contains(fixture.nativeElement)).toBe(false); }); it('should re-throw errors that were thrown during fixture cleanup', () => { @Component({ template: '', standalone: false, }) class ThrowsOnDestroy { ngOnDestroy() { throw Error('oh no'); } } TestBed.configureTestingModule({ declarations: [ThrowsOnDestroy], teardown: {destroyAfterEach: true}, }); TestBed.createComponent(ThrowsOnDestroy); const spy = spyOn(console, 'error'); expect(() => TestBed.resetTestingModule()).toThrowError( '1 component threw errors during cleanup', ); expect(spy).toHaveBeenCalledTimes(1); }); it('should not interrupt fixture destruction if an error is thrown', () => { @Component({ template: '', standalone: false, }) class ThrowsOnDestroy { ngOnDestroy() { throw Error('oh no'); } } TestBed.configureTestingModule({ declarations: [ThrowsOnDestroy], teardown: {destroyAfterEach: true}, }); for (let i = 0; i < 3; i++) { TestBed.createComponent(ThrowsOnDestroy); } const spy = spyOn(console, 'error'); expect(() => TestBed.resetTestingModule()).toThrowError( '3 components threw errors during cleanup', ); expect(spy).toHaveBeenCalledTimes(3); }); it('should re-throw errors that were thrown during module teardown by default', () => { @Injectable() class ThrowsOnDestroy { ngOnDestroy() { throw Error('oh no'); } } @Component({ template: '', standalone: false, }) class App { constructor(_service: ThrowsOnDestroy) {} } TestBed.configureTestingModule({ providers: [ThrowsOnDestroy], declarations: [App], teardown: {destroyAfterEach: true}, }); TestBed.createComponent(App); expect(() => TestBed.resetTestingModule()).toThrowError('oh no'); }); it('should be able to opt out of rethrowing of errors coming from module teardown', () => { @Injectable() class ThrowsOnDestroy { ngOnDestroy() { throw Error('oh no'); } } @Component({ template: '', standalone: false, }) class App { constructor(_service: ThrowsOnDestroy) {} } TestBed.configureTestingModule({ providers: [ThrowsOnDestroy], declarations: [App], teardown: {destroyAfterEach: true, rethrowErrors: false}, }); TestBed.createComponent(App); const spy = spyOn(console, 'error'); expect(() => TestBed.resetTestingModule()).not.toThrow(); expect(spy).toHaveBeenCalledTimes(1); }); it('should remove the styles associated with a test component when the test module is torn down', () => { @Component({ template: '<span>Hello</span>', styles: [ ` span { color: hotpink; } `, ], standalone: false, }) class StyledComp1 {} @Component({ template: '<div>Hello</div>', styles: [ ` div { color: red; } `, ], standalone: false, }) class StyledComp2 {} TestBed.configureTestingModule({ declarations: [StyledComp1, StyledComp2], teardown: {destroyAfterEach: true}, }); const fixtures = [TestBed.createComponent(StyledComp1), TestBed.createComponent(StyledComp2)]; const fixtureDocument = fixtures[0].nativeElement.ownerDocument; const styleCountBefore = fixtureDocument.querySelectorAll('style').length; // Note that we can only assert that the behavior works as expected by checking that the // number of stylesheets has decreased. We can't expect that they'll be zero, because there // may by stylesheets leaking in from other tests that don't use the module teardown // behavior. expect(styleCountBefore).toBeGreaterThan(0); TestBed.resetTestingModule(); expect(fixtureDocument.querySelectorAll('style').length).toBeLessThan(styleCountBefore); }); it('should rethrow errors based on the default teardown behavior', () => { expect(TestBedImpl.INSTANCE.shouldRethrowTeardownErrors()).toBe( TEARDOWN_TESTING_MODULE_ON_DESTROY_DEFAULT, ); }); it('should rethrow errors if the option is omitted and test teardown is enabled', () => { TestBed.configureTestingModule({teardown: {destroyAfterEach: true}}); expect(TestBedImpl.INSTANCE.shouldRethrowTeardownErrors()).toBe(true); }); it('should not rethrow errors if the option is omitted and test teardown is disabled', () => { TestBed.configureTestingModule({teardown: {destroyAfterEach: false}}); expect(TestBedImpl.INSTANCE.shouldRethrowTeardownErrors()).toBe(false); }); it('should rethrow errors if the option is enabled, but teardown is disabled', () => { TestBed.configureTestingModule({teardown: {destroyAfterEach: false, rethrowErrors: true}}); expect(TestBedImpl.INSTANCE.shouldRethrowTeardownErrors()).toBe(true); }); it('should not rethrow errors if the option is disabled, but teardown is enabled', () => { TestBed.configureTestingModule({teardown: {destroyAfterEach: true, rethrowErrors: false}}); expect(TestBedImpl.INSTANCE.shouldRethrowTeardownErrors()).toBe(false); }); }); describe('TestBed module `errorOnUnknownElements`', () => { beforeEach(() => { TestBed.resetTestingModule(); }); it('should not throw based on the default behavior', () => { expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownElements()).toBe( THROW_ON_UNKNOWN_ELEMENTS_DEFAULT, ); }); it('should not throw if the option is omitted', () => { TestBed.configureTestingModule({}); expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownElements()).toBe(false); }); it('should be able to configure the option', () => { TestBed.configureTestingModule({errorOnUnknownElements: true}); expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownElements()).toBe(true); }); it('should reset the option back to the default when TestBed is reset', () => { TestBed.configureTestingModule({errorOnUnknownElements: true}); expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownElements()).toBe(true); TestBed.resetTestingModule(); expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownElements()).toBe(false); }); }); describe('TestBed module `errorOnUnknownProperties`', () => { beforeEach(() => { TestBed.resetTestingModule(); }); it('should not throw based on the default behavior', () => { expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownProperties()).toBe( THROW_ON_UNKNOWN_PROPERTIES_DEFAULT, ); }); it('should not throw if the option is omitted', () => { TestBed.configureTestingModule({}); expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownProperties()).toBe(false); }); it('should be able to configure the option', () => { TestBed.configureTestingModule({errorOnUnknownProperties: true}); expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownProperties()).toBe(true); }); it('should reset the option back to the default when TestBed is reset', () => { TestBed.configureTestingModule({errorOnUnknownProperties: true}); expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownProperties()).toBe(true); TestBed.resetTestingModule(); expect(TestBedImpl.INSTANCE.shouldThrowErrorOnUnknownProperties()).toBe(false); }); });