test(core): add more tests for the standalone injector (#45687)

This commit reorganizes the tests around the EnvironmentInjector and its use
for standalone injectors, and adds a number of new test cases.

PR Close #45687
This commit is contained in:
Pawel Kozlowski
2022-04-14 10:19:54 -07:00
committed by Andrew Scott
parent 3616d7eb59
commit e162fa3735
3 changed files with 169 additions and 70 deletions
@@ -0,0 +1,73 @@
/**
* @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 {Component, createEnvironmentInjector, importProvidersFrom, NgModule} from '@angular/core';
describe('environement injector and standalone components', () => {
it('should see providers from modules imported by standalone components', () => {
class ModuleService {}
@NgModule({providers: [ModuleService]})
class Module {
}
@Component({standalone: true, imports: [Module]})
class StandaloneComponent {
}
const envInjector = createEnvironmentInjector(importProvidersFrom(StandaloneComponent));
expect(envInjector.get(ModuleService)).toBeInstanceOf(ModuleService);
});
it('should see providers when exporting a standalone components', () => {
class ModuleService {}
@NgModule({providers: [ModuleService]})
class Module {
}
@Component({standalone: true, imports: [Module]})
class StandaloneComponent {
}
@NgModule({imports: [StandaloneComponent], exports: [StandaloneComponent]})
class AppModule {
}
const envInjector = createEnvironmentInjector(importProvidersFrom(AppModule));
expect(envInjector.get(ModuleService)).toBeInstanceOf(ModuleService);
});
it('should not collect duplicate providers', () => {
class ModuleService {}
@NgModule({providers: [{provide: ModuleService, useClass: ModuleService, multi: true}]})
class Module {
}
@Component({standalone: true, imports: [Module]})
class StandaloneComponent1 {
}
@Component({standalone: true, imports: [Module]})
class StandaloneComponent2 {
}
@NgModule({
imports: [StandaloneComponent1, StandaloneComponent2],
exports: [StandaloneComponent1, StandaloneComponent2]
})
class AppModule {
}
const envInjector = createEnvironmentInjector(importProvidersFrom(AppModule));
const services = envInjector.get(ModuleService) as ModuleService[];
expect(services.length).toBe(1);
});
});
@@ -0,0 +1,96 @@
/**
* @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 {Component, ComponentFactoryResolver, createEnvironmentInjector, EnvironmentInjector, Injector, NgModule, ViewChild, ViewContainerRef} from '@angular/core';
import {TestBed} from '@angular/core/testing';
describe('standalone injector', () => {
it('should create one standalone injector for each parent EnvInjector', () => {
let counter = 0;
class Service {
value = counter++;
}
@NgModule({providers: [Service]})
class ModuleWithAService {
}
@Component({
selector: 'standalone',
standalone: true,
imports: [ModuleWithAService],
template: `({{service.value}})`
})
class TestComponent {
constructor(readonly service: Service) {}
}
@Component({selector: 'app', template: `<ng-template #insert></ng-template>`})
class AppComponent {
@ViewChild('insert', {static: true, read: ViewContainerRef}) vcRef!: ViewContainerRef;
createComponent(envInjector?: EnvironmentInjector): void {
this.vcRef.createComponent(TestComponent, {environmentInjector: envInjector});
}
}
const fixture = TestBed.createComponent(AppComponent);
const currEnvInjector = TestBed.inject(EnvironmentInjector);
fixture.componentInstance.createComponent(currEnvInjector);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('(0)');
// inserting the same standalone component second time and asserting that no new injector /
// service instance gets created
fixture.componentInstance.createComponent(currEnvInjector);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('(0)(0)');
// inserting with a different EnvInjector as a parent should trigger a new service instance
// creation
fixture.componentInstance.createComponent(createEnvironmentInjector([], currEnvInjector));
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('(0)(0)(1)');
});
it('should create a standalone Injector for ComponentRefs that are not inserted anywhere', () => {
class Service {
value = 'Service value';
}
@NgModule({providers: [Service]})
class ModuleWithAService {
}
@Component({
selector: 'standalone',
standalone: true,
imports: [ModuleWithAService],
template: `{{service.value}}`
})
class DynamicComponent {
constructor(readonly service: Service) {}
}
@Component({})
class AppComponent {
}
const fixture = TestBed.createComponent(AppComponent);
const cfr = TestBed.inject(ComponentFactoryResolver);
const cf = cfr.resolveComponentFactory(DynamicComponent);
const componentRef = cf.create(Injector.NULL);
componentRef.changeDetectorRef.detectChanges();
expect(componentRef.location.nativeElement.textContent).toBe('Service value');
});
});
@@ -1,70 +0,0 @@
/**
* @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 {InjectionToken, Injector, ɵRenderFlags, ɵɵdefineComponent, ɵɵdefineInjector, ɵɵelement, ɵɵStandaloneFeature} from '../../src/core';
import {getComponentDef} from '../../src/render3/definition';
describe('standalone components, directives and pipes', () => {
it('should filter out imported NgModules from a list of directives to match', () => {
const token = new InjectionToken<String>('TestToken');
let count = 0;
class TestModule {
static ɵinj = ɵɵdefineInjector({
providers: [{
provide: token,
useFactory:
() => {
return `From module: ${count++}`;
}
}]
});
}
class TestComponent {
static ɵfac = () => new TestComponent();
static ɵcmp = ɵɵdefineComponent({
type: TestComponent,
selectors: [['test-cmp']],
decls: 1,
vars: 0,
template:
(rf: ɵRenderFlags, ctx: TestComponent) => {
if (rf & ɵRenderFlags.Create) {
ɵɵelement(0, 'div');
}
},
dependencies: [TestModule],
features: [ɵɵStandaloneFeature],
standalone: true
});
}
// simulate logic executed by ComponentFactory::create
const def = getComponentDef(TestComponent);
if (def !== null && def.getStandaloneInjector !== null) {
const parentInjector = Injector.create({providers: []});
expect(def.getStandaloneInjector(parentInjector)!.get(token)).toEqual('From module: 0');
// subsequent calls with the same same parent injector should not create new provider
// instances
expect(def.getStandaloneInjector(parentInjector)!.get(token)).toEqual('From module: 0');
// but specifying a different injector _should_ create a new provider instance
const otherParentInjector = Injector.create({providers: []});
expect(def.getStandaloneInjector(otherParentInjector)!.get(token)).toEqual('From module: 1');
// again, same calls should not create new instances
expect(def.getStandaloneInjector(parentInjector)!.get(token)).toEqual('From module: 0');
expect(def.getStandaloneInjector(otherParentInjector)!.get(token)).toEqual('From module: 1');
} else {
fail('Failed to get component def or standalone feature function');
}
});
});