angular/packages/platform-browser/test/browser/bootstrap_standalone_spec.ts
Andrew Kushnir fa755b2a54 fix(core): prevent BrowserModule providers from being loaded twice (#45826)
This commit updates the logic of the `BrowserModule` to detect a situation when it's used in the `bootstrapApplication` case, which already includes `BrowserModule` providers.

PR Close #45826
2022-05-03 16:08:04 -07:00

167 lines
5.3 KiB
TypeScript

/**
* @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.io/license
*/
import {DOCUMENT, ɵgetDOM as getDOM} from '@angular/common';
import {Component, destroyPlatform, Inject, Injectable, InjectionToken, NgModule} from '@angular/core';
import {inject} from '@angular/core/testing';
import {bootstrapApplication, BrowserModule} from '../../src/browser';
describe('bootstrapApplication for standalone components', () => {
let rootEl: HTMLUnknownElement;
beforeEach(inject([DOCUMENT], (doc: any) => {
rootEl = getDOM().createElement('test-app', doc);
getDOM().getDefaultDocument().body.appendChild(rootEl);
}));
afterEach(() => {
destroyPlatform();
rootEl?.remove();
});
it('should create injector where ambient providers shadow explicit providers', async () => {
const testToken = new InjectionToken('test token');
@NgModule({
providers: [
{provide: testToken, useValue: 'Ambient'},
]
})
class AmbientModule {
}
@Component({
selector: 'test-app',
standalone: true,
template: `({{testToken}})`,
imports: [AmbientModule]
})
class StandaloneCmp {
constructor(@Inject(testToken) readonly testToken: String) {}
}
const appRef = await bootstrapApplication(StandaloneCmp, {
providers: [
{provide: testToken, useValue: 'Bootstrap'},
]
});
appRef.tick();
// make sure that ambient providers "shadow" ones explicitly provided during bootstrap
expect(rootEl.textContent).toBe('(Ambient)');
});
/*
This test verifies that ambient providers for the standalone component being bootstrapped
(providers collected from the import graph of a standalone component) are instantiated in a
dedicated standalone injector. As the result we are ending up with the following injectors
hierarchy:
- platform injector (platform specific providers go here);
- application injector (providers specified in the bootstrap options go here);
- standalone injector (ambient providers go here);
*/
it('should create a standalone injector for standalone components with ambient providers',
async () => {
const ambientToken = new InjectionToken('ambient token');
@NgModule({
providers: [
{provide: ambientToken, useValue: 'Only in AmbientNgModule'},
]
})
class AmbientModule {
}
@Injectable()
class NeedsAmbientProvider {
constructor(@Inject(ambientToken) readonly ambientToken: String) {}
}
@Component({
selector: 'test-app',
template: `({{service.ambientToken}})`,
standalone: true,
imports: [AmbientModule]
})
class StandaloneCmp {
constructor(readonly service: NeedsAmbientProvider) {}
}
try {
await bootstrapApplication(
StandaloneCmp,
{providers: [NeedsAmbientProvider]},
);
// we expect the bootstrap process to fail since the "NeedsAmbientProvider" service
// (located in the application injector) can't "see" ambient providers (located in a
// standalone injector that is a child of the application injector).
fail('Expected to throw');
} catch (e: unknown) {
expect(e).toBeInstanceOf(Error);
expect((e as Error).message).toContain('No provider for InjectionToken ambient token!');
}
});
it('should throw if `BrowserModule` is imported in the standalone bootstrap scenario',
async () => {
@Component({
selector: 'test-app',
template: '...',
standalone: true,
imports: [BrowserModule],
})
class StandaloneCmp {
}
try {
await bootstrapApplication(StandaloneCmp);
// The `bootstrapApplication` already includes the set of providers from the
// `BrowserModule`, so including the `BrowserModule` again will bring duplicate providers
// and we want to avoid it.
fail('Expected to throw');
} catch (e: unknown) {
expect(e).toBeInstanceOf(Error);
expect((e as Error).message)
.toContain('Providers from the `BrowserModule` have already been loaded.');
}
});
it('should throw if `BrowserModule` is imported indirectly in the standalone bootstrap scenario',
async () => {
@NgModule({
imports: [BrowserModule],
})
class SomeDependencyModule {
}
@Component({
selector: 'test-app',
template: '...',
standalone: true,
imports: [SomeDependencyModule],
})
class StandaloneCmp {
}
try {
await bootstrapApplication(StandaloneCmp);
// The `bootstrapApplication` already includes the set of providers from the
// `BrowserModule`, so including the `BrowserModule` again will bring duplicate providers
// and we want to avoid it.
fail('Expected to throw');
} catch (e: unknown) {
expect(e).toBeInstanceOf(Error);
expect((e as Error).message)
.toContain('Providers from the `BrowserModule` have already been loaded.');
}
});
});