2022-04-26 12:39:30 +00:00
/ * *
* @license
* Copyright Google LLC All Rights Reserved .
*
* Use of this source code is governed by an MIT - style license that can be
2024-09-20 15:23:15 +00:00
* found in the LICENSE file at https : //angular.dev/license
2022-04-26 12:39:30 +00:00
* /
2023-04-03 09:21:05 +00:00
import {
Component ,
destroyPlatform ,
ErrorHandler ,
Inject ,
Injectable ,
InjectionToken ,
NgModule ,
NgZone ,
PlatformRef ,
2025-05-22 17:29:37 +00:00
ɵ R3Injector as R3Injector ,
ɵ NoopNgZone as NoopNgZone ,
2025-09-17 15:53:02 +00:00
APP_ID ,
2025-11-07 19:23:03 +00:00
signal ,
2023-04-03 09:21:05 +00:00
} from '@angular/core' ;
2025-11-07 19:23:03 +00:00
import { isNode , withBody } from '@angular/private/testing' ;
import { bootstrapApplication , BrowserModule , createApplication } from '../../src/browser' ;
2022-04-26 12:39:30 +00:00
describe ( 'bootstrapApplication for standalone components' , ( ) = > {
2023-04-03 09:21:05 +00:00
beforeEach ( destroyPlatform ) ;
afterEach ( destroyPlatform ) ;
2022-06-27 13:35:41 +00:00
class SilentErrorHandler extends ErrorHandler {
override handleError() {
// the error is already re-thrown by the application ref.
// we don't want to print it, but instead catch it in tests.
}
}
2022-06-25 01:13:16 +00:00
it (
'should create injector where ambient providers shadow explicit providers' ,
withBody ( '<test-app></test-app>' , async ( ) = > {
const testToken = new InjectionToken ( 'test token' ) ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
@NgModule ( {
providers : [ { provide : testToken , useValue : 'Ambient' } ] ,
} )
class AmbientModule { }
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
@Component ( {
selector : 'test-app' ,
template : ` ({{ testToken }}) ` ,
imports : [ AmbientModule ] ,
} )
class StandaloneCmp {
constructor ( @Inject ( testToken ) readonly testToken : String ) { }
}
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
const appRef = await bootstrapApplication ( StandaloneCmp , {
providers : [ { provide : testToken , useValue : 'Bootstrap' } ] ,
} ) ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
appRef . tick ( ) ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
// make sure that ambient providers "shadow" ones explicitly provided during bootstrap
expect ( document . body . textContent ) . toBe ( '(Ambient)' ) ;
} ) ,
) ;
2024-04-19 16:13:59 +00:00
2023-03-09 01:09:11 +00:00
it (
'should be able to provide a custom zone implementation in DI' ,
withBody ( '<test-app></test-app>' , async ( ) = > {
@Component ( {
selector : 'test-app' ,
template : ` ` ,
} )
class StandaloneCmp { }
2024-04-19 16:13:59 +00:00
2023-03-09 01:09:11 +00:00
class CustomZone extends NoopNgZone { }
const instance = new CustomZone ( ) ;
2024-04-19 16:13:59 +00:00
2023-03-09 01:09:11 +00:00
const appRef = await bootstrapApplication ( StandaloneCmp , {
providers : [ { provide : NgZone , useValue : instance } ] ,
} ) ;
2024-04-19 16:13:59 +00:00
2023-03-09 01:09:11 +00:00
appRef . tick ( ) ;
expect ( appRef . injector . get ( NgZone ) ) . toEqual ( instance ) ;
} ) ,
) ;
2022-04-26 12:39:30 +00:00
/ *
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' ,
2022-06-25 01:13:16 +00:00
withBody ( '<test-app></test-app>' , async ( ) = > {
2022-04-26 12:39:30 +00:00
const ambientToken = new InjectionToken ( 'ambient token' ) ;
2024-04-19 16:13:59 +00:00
2022-04-26 12:39:30 +00:00
@NgModule ( {
providers : [ { provide : ambientToken , useValue : 'Only in AmbientNgModule' } ] ,
} )
class AmbientModule { }
2024-04-19 16:13:59 +00:00
2022-04-26 12:39:30 +00:00
@Injectable ( )
class NeedsAmbientProvider {
constructor ( @Inject ( ambientToken ) readonly ambientToken : String ) { }
}
2024-04-19 16:13:59 +00:00
2022-04-26 12:39:30 +00:00
@Component ( {
selector : 'test-app' ,
template : ` ({{ service.ambientToken }}) ` ,
imports : [ AmbientModule ] ,
} )
class StandaloneCmp {
constructor ( readonly service : NeedsAmbientProvider ) { }
}
2024-04-19 16:13:59 +00:00
2022-04-26 12:39:30 +00:00
try {
await bootstrapApplication ( StandaloneCmp , {
2022-06-27 13:35:41 +00:00
providers : [ { provide : ErrorHandler , useClass : SilentErrorHandler } , NeedsAmbientProvider ] ,
2022-04-26 12:39:30 +00:00
} ) ;
2024-04-19 16:13:59 +00:00
2022-04-26 12:39:30 +00:00
// 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 ) ;
2026-01-07 21:21:25 +00:00
expect ( ( e as Error ) . message ) . toMatch (
/NG0201: No provider found for `InjectionToken ambient token`\. Source: Standalone\[StandaloneCmp\]\. Path: NeedsAmbientProvider -> InjectionToken ambient token\. Find more at https:\/\/(?:next\.)?angular\.dev\/errors\/NG0201/ ,
2022-06-28 18:11:25 +00:00
) ;
2022-04-26 12:39:30 +00:00
}
2022-06-25 01:13:16 +00:00
} ) ,
) ;
2024-04-19 16:13:59 +00:00
2022-04-30 02:06:36 +00:00
it (
'should throw if `BrowserModule` is imported in the standalone bootstrap scenario' ,
2022-06-25 01:13:16 +00:00
withBody ( '<test-app></test-app>' , async ( ) = > {
2022-04-30 02:06:36 +00:00
@Component ( {
selector : 'test-app' ,
template : '...' ,
imports : [ BrowserModule ] ,
} )
class StandaloneCmp { }
2024-04-19 16:13:59 +00:00
2022-04-30 02:06:36 +00:00
try {
2022-06-27 13:35:41 +00:00
await bootstrapApplication ( StandaloneCmp , {
providers : [ { provide : ErrorHandler , useClass : SilentErrorHandler } ] ,
} ) ;
2024-04-19 16:13:59 +00:00
2022-04-30 02:06:36 +00:00
// 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 (
2023-03-07 07:25:19 +00:00
'NG05100: Providers from the `BrowserModule` have already been loaded.' ,
) ;
2022-04-30 02:06:36 +00:00
}
2022-06-25 01:13:16 +00:00
} ) ,
) ;
2024-04-19 16:13:59 +00:00
2022-04-30 02:06:36 +00:00
it (
'should throw if `BrowserModule` is imported indirectly in the standalone bootstrap scenario' ,
2022-06-25 01:13:16 +00:00
withBody ( '<test-app></test-app>' , async ( ) = > {
2022-04-30 02:06:36 +00:00
@NgModule ( {
imports : [ BrowserModule ] ,
} )
class SomeDependencyModule { }
2024-04-19 16:13:59 +00:00
2022-04-30 02:06:36 +00:00
@Component ( {
selector : 'test-app' ,
template : '...' ,
imports : [ SomeDependencyModule ] ,
} )
class StandaloneCmp { }
2024-04-19 16:13:59 +00:00
2022-04-30 02:06:36 +00:00
try {
2022-06-27 13:35:41 +00:00
await bootstrapApplication ( StandaloneCmp , {
providers : [ { provide : ErrorHandler , useClass : SilentErrorHandler } ] ,
} ) ;
2024-04-19 16:13:59 +00:00
2022-04-30 02:06:36 +00:00
// 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 (
2023-03-07 07:25:19 +00:00
'NG05100: Providers from the `BrowserModule` have already been loaded.' ,
) ;
2022-04-30 02:06:36 +00:00
}
2022-06-25 01:13:16 +00:00
} ) ,
) ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
it (
'should trigger an app destroy when a platform is destroyed' ,
withBody ( '<test-app></test-app>' , async ( ) = > {
let compOnDestroyCalled = false ;
let serviceOnDestroyCalled = false ;
let injectorOnDestroyCalled = false ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
@Injectable ( { providedIn : 'root' } )
class ServiceWithOnDestroy {
ngOnDestroy() {
serviceOnDestroyCalled = true ;
2024-04-19 16:13:59 +00:00
}
2022-06-25 01:13:16 +00:00
}
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
@Component ( {
selector : 'test-app' ,
template : 'Hello' ,
} )
class ComponentWithOnDestroy {
constructor ( service : ServiceWithOnDestroy ) { }
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
ngOnDestroy() {
compOnDestroyCalled = true ;
2024-04-19 16:13:59 +00:00
}
2022-06-25 01:13:16 +00:00
}
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
const appRef = await bootstrapApplication ( ComponentWithOnDestroy ) ;
const injector = ( appRef as unknown as { injector : R3Injector } ) . injector ;
injector . onDestroy ( ( ) = > ( injectorOnDestroyCalled = true ) ) ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
expect ( document . body . textContent ) . toBe ( 'Hello' ) ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
const platformRef = injector . get ( PlatformRef ) ;
platformRef . destroy ( ) ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
// Verify the callbacks were invoked.
expect ( compOnDestroyCalled ) . toBe ( true ) ;
expect ( serviceOnDestroyCalled ) . toBe ( true ) ;
expect ( injectorOnDestroyCalled ) . toBe ( true ) ;
2024-04-19 16:13:59 +00:00
2022-06-25 01:13:16 +00:00
// Make sure the DOM has been cleaned up as well.
expect ( document . body . textContent ) . toBe ( '' ) ;
} ) ,
) ;
2025-09-17 15:53:02 +00:00
it ( 'should throw and error if the APP_ID is not valid' , async ( ) = > {
withBody ( '<test-app></test-app>' , async ( ) = > {
@Component ( {
selector : 'test-app' ,
template : ` ` ,
imports : [ ] ,
} )
class StandaloneCmp { }
try {
await bootstrapApplication ( StandaloneCmp , {
providers : [ { provide : APP_ID , useValue : 'foo:bar' } ] ,
} ) ;
// we expect the bootstrap process to fail because of the invalid APP_ID value
fail ( 'Expected to throw' ) ;
} catch ( e : unknown ) {
expect ( e ) . toBeInstanceOf ( Error ) ;
expect ( ( e as Error ) . message ) . toContain (
'APP_ID value "foo:bar" is not alphanumeric. The APP_ID must be a string of alphanumeric characters.' ,
) ;
}
} ) ;
} ) ;
2022-04-26 12:39:30 +00:00
} ) ;
2025-11-07 19:23:03 +00:00
describe ( 'createApplication' , ( ) = > {
beforeEach ( destroyPlatform ) ;
afterEach ( destroyPlatform ) ;
it (
'creates an `ApplicationRef` which can bootstrap a component' ,
withBody ( '<test-app></test-app>' , async ( ) = > {
@Component ( {
selector : 'test-app' ,
template : ` Hello, {{ name() }}! ` ,
} )
class TestComp {
protected readonly name = signal ( 'Dev' ) ;
}
const appRef = await createApplication ( ) ;
appRef . bootstrap ( TestComp ) ;
expect ( document . body . textContent . trim ( ) ) . toBe ( 'Hello, Dev!' ) ;
} ) ,
) ;
it (
'creates an `ApplicationRef` which can bootstrap a JIT component with an external resource' ,
withBody ( '<test-app></test-app>' , async ( ) = > {
// Resolving resources only makes sense in the browser.
if ( isNode ) {
expect ( ) . nothing ( ) ;
return ;
}
const templateUrl = URL . createObjectURL ( new Blob ( [ 'Hello, {{ name() }}!' ] ) ) ;
@Component ( {
selector : 'test-app' ,
templateUrl ,
} )
class TestComp {
protected readonly name = signal ( 'Dev' ) ;
}
const appRef = await createApplication ( ) ;
appRef . bootstrap ( TestComp ) ;
expect ( document . body . textContent . trim ( ) ) . toBe ( 'Hello, Dev!' ) ;
URL . revokeObjectURL ( templateUrl ) ;
} ) ,
) ;
} ) ;