A common pattern is to create Angular elements in the constructor or ngBootstrap
method of its associated module. This allows elements to use the module’s injector, inheriting it’s scope and lifecycle. A conditional check is required, because customElements.define
will throw if the registry already has an entry.
@NgModule()
export class AppModule {
constructor(injector: Injector) {
if (!customElement.get('custom-element')) {
const el = createCustomElement(CustomComponent, { injector: this.injector });
customElements.define('custom-element', el);
}
}
}
As of Angular 13, ModuleTeardownOptions.destroyAfterEach defaults to true. The test module passed to TestBed.initTestEnvironment
will be destroyed and the DOM cleared after every test. This should result in “faster, less memory-intensive, and less interdependent [tests].” For tests with an Angular element defined, however, its associated injector will be destroyed after the first test - resulting in the following error.
Error: NG0205: Injector has already been destroyed.
error properties: Object({ code: 205 })
Error: NG0205: Injector has already been destroyed.
at R3Injector.assertNotDestroyed (node_modules/@angular/core/fesm2020/core.mjs:6850:19)
at R3Injector.get (node_modules/@angular/core/fesm2020/core.mjs:6758:14)
at new ComponentNgElementStrategy (node_modules/@angular/elements/fesm2020/elements.mjs:219:37)
at ComponentNgElementStrategyFactory.create (node_modules/@angular/elements/fesm2020/elements.mjs:176:16)
at NgElementImpl.ngElementStrategy (node_modules/@angular/elements/fesm2020/elements.mjs:469:37)
at NgElementImpl.apply (node_modules/@angular/elements/fesm2020/elements.mjs:498:22)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:372:26)
at ProxyZoneSpec.onInvoke (node_modules/zone.js/fesm2015/zone-testing.js:287:39)
at _ZoneDelegate.invoke (node_modules/zone.js/fesm2015/zone.js:371:52)
at Zone.runGuarded (node_modules/zone.js/fesm2015/zone.js:144:47)
Debugging
To determine the problematic element, check the beginning of the above callstack. Run the tests in watch mode, and set a breakpoint where the exception is thrown (r3_injector.ts line 303 as of 14.2.0). Once hit, a few steps up the stack will be the ComponentNgElementStrategy
constructor (element.mjs line 219 corresponding to component-factory-strategy.ts line 88). Inpect the componentFactory
argument to reveal the component.
Resolution
Often the custom element isn’t necessary for the test. Write the test so that constructing it isn’t required. Alternatively, disable destroyAfterEach
for the affected set of tests.
beforeEach(() => {
TestBed.configureTestingModule({
teardown: { destroyAfterEach: true }
});
});
Developer Preview
As of Angular 14.2.0, standalone elements are available for preview (PR#46475). This offers an opportunity to create an independent application environment with createApplication which decouples elements from the test module.
beforeAll(async () => {
if (!customElement.get('custom-element')) {
const application = await createApplication({ providers: [] });
const el = createCustomElement(CustomComponent, { injector: appRef.injector });
customElements.define('custom-element', el);
}
});
Read more on the Angular blog.