fix(ivy): adding TestBed.overrideProvider support (#27693)

Prior to this change, provider overrides defined via TestBed.overrideProvider were not applied to Components/Directives. Now providers are taken into account while compiling Components/Directives (metadata is updated accordingly before being passed to compilation).

PR Close #27693
This commit is contained in:
Andrew Kushnir 2018-12-15 14:46:47 -08:00 committed by Matias Niemelä
parent bba5e2632e
commit 4b67b0af3e
2 changed files with 313 additions and 300 deletions

View File

@ -16,6 +16,16 @@ import {ComponentFixtureAutoDetect, ComponentFixtureNoNgZone, TestBedStatic, Tes
let _nextRootElementId = 0;
const EMPTY_ARRAY: Type<any>[] = [];
// Resolvers for Angular decorators
type Resolvers = {
module: Resolver<NgModule>,
component: Resolver<Directive>,
directive: Resolver<Component>,
pipe: Resolver<Pipe>,
};
/**
* @description
* Configures and initializes environment for unit testing and provides methods for
@ -174,6 +184,7 @@ export class TestBedRender3 implements Injector, TestBed {
private _pipeOverrides: [Type<any>, MetadataOverride<Pipe>][] = [];
private _providerOverrides: Provider[] = [];
private _rootProviderOverrides: Provider[] = [];
private _providerOverridesByToken: Map<any, Provider[]> = new Map();
// test module configuration
private _providers: Provider[] = [];
@ -229,6 +240,7 @@ export class TestBedRender3 implements Injector, TestBed {
this._pipeOverrides = [];
this._providerOverrides = [];
this._rootProviderOverrides = [];
this._providerOverridesByToken.clear();
// reset test module config
this._providers = [];
@ -322,17 +334,21 @@ export class TestBedRender3 implements Injector, TestBed {
*/
overrideProvider(token: any, provider: {useFactory?: Function, useValue?: any, deps?: any[]}):
void {
const providerDef = provider.useFactory ?
{provide: token, useFactory: provider.useFactory, deps: provider.deps || []} :
{provide: token, useValue: provider.useValue};
let injectableDef: InjectableDef<any>|null;
const isRoot =
(typeof token !== 'string' && (injectableDef = getInjectableDef(token)) &&
injectableDef.providedIn === 'root');
const overrides = isRoot ? this._rootProviderOverrides : this._providerOverrides;
const overridesBucket = isRoot ? this._rootProviderOverrides : this._providerOverrides;
overridesBucket.push(providerDef);
if (provider.useFactory) {
overrides.push({provide: token, useFactory: provider.useFactory, deps: provider.deps || []});
} else {
overrides.push({provide: token, useValue: provider.useValue});
}
// keep all overrides grouped by token as well for fast lookups using token
const overridesForToken = this._providerOverridesByToken.get(token) || [];
overridesForToken.push(providerDef);
this._providerOverridesByToken.set(token, overridesForToken);
}
/**
@ -387,8 +403,7 @@ export class TestBedRender3 implements Injector, TestBed {
const resolvers = this._getResolvers();
const testModuleType = this._createTestModule();
compileNgModule(testModuleType, resolvers);
this._compileNgModule(testModuleType, resolvers);
const parentInjector = this.platform.injector;
this._moduleRef = new NgModuleRef(testModuleType, parentInjector);
@ -399,6 +414,14 @@ export class TestBedRender3 implements Injector, TestBed {
this._instantiated = true;
}
// get overrides for a specific provider (if any)
private _getProviderOverrides(provider: any) {
const token = typeof provider === 'object' && provider.hasOwnProperty('provide') ?
provider.provide :
provider;
return this._providerOverridesByToken.get(token) || [];
}
// creates resolvers taking overrides into account
private _getResolvers() {
const module = new NgModuleResolver();
@ -448,64 +471,45 @@ export class TestBedRender3 implements Injector, TestBed {
return DynamicTestModule as NgModuleType;
}
}
let testBed: TestBedRender3;
export function _getTestBedRender3(): TestBedRender3 {
return testBed = testBed || new TestBedRender3();
}
const OWNER_MODULE = '__NG_MODULE__';
/**
* This function clears the OWNER_MODULE property from the Types. This is set in r3/jit/modules.ts.
* It is common for the same Type to be compiled in different tests. If we don't clear this we will
* get errors which will complain that the same Component/Directive is in more than one NgModule.
*/
function clearNgModules(type: Type<any>) {
if (type.hasOwnProperty(OWNER_MODULE)) {
(type as any)[OWNER_MODULE] = undefined;
private _getMetaWithOverrides(meta: Component|Directive|NgModule) {
if (meta.providers && meta.providers.length) {
const overrides =
flatten(meta.providers, (provider: any) => this._getProviderOverrides(provider));
if (overrides.length) {
return {...meta, providers: [...meta.providers, ...overrides]};
}
}
return meta;
}
}
// Module compiler
const EMPTY_ARRAY: Type<any>[] = [];
// Resolvers for Angular decorators
type Resolvers = {
module: Resolver<NgModule>,
component: Resolver<Directive>,
directive: Resolver<Component>,
pipe: Resolver<Pipe>,
};
function compileNgModule(moduleType: NgModuleType, resolvers: Resolvers): void {
private _compileNgModule(moduleType: NgModuleType, resolvers: Resolvers): void {
const ngModule = resolvers.module.resolve(moduleType);
if (ngModule === null) {
throw new Error(`${stringify(moduleType)} has not @NgModule annotation`);
}
compileNgModuleDefs(moduleType, ngModule);
const metadata = this._getMetaWithOverrides(ngModule);
compileNgModuleDefs(moduleType, metadata);
const declarations: Type<any>[] = flatten(ngModule.declarations || EMPTY_ARRAY);
const compiledComponents: Type<any>[] = [];
// Compile the components, directives and pipes declared by this module
declarations.forEach(declaration => {
const component = resolvers.component.resolve(declaration);
if (component) {
compileComponent(declaration, component);
const metadata = this._getMetaWithOverrides(component);
compileComponent(declaration, metadata);
compiledComponents.push(declaration);
return;
}
const directive = resolvers.directive.resolve(declaration);
if (directive) {
compileDirective(declaration, directive);
const metadata = this._getMetaWithOverrides(directive);
compileDirective(declaration, metadata);
return;
}
@ -517,20 +521,21 @@ function compileNgModule(moduleType: NgModuleType, resolvers: Resolvers): void {
});
// Compile transitive modules, components, directives and pipes
const transitiveScope = transitiveScopesFor(moduleType, resolvers);
const transitiveScope = this._transitiveScopesFor(moduleType, resolvers);
compiledComponents.forEach(
cmp => patchComponentDefWithScope((cmp as any).ngComponentDef, transitiveScope));
}
}
/**
* Compute the pair of transitive scopes (compilation scope and exported scope) for a given module.
/**
* Compute the pair of transitive scopes (compilation scope and exported scope) for a given
* module.
*
* This operation is memoized and the result is cached on the module's definition. It can be called
* on modules with components that have not fully compiled yet, but the result should not be used
* until they have.
* This operation is memoized and the result is cached on the module's definition. It can be
* called on modules with components that have not fully compiled yet, but the result should not
* be used until they have.
*/
function transitiveScopesFor<T>(
moduleType: Type<T>, resolvers: Resolvers): NgModuleTransitiveScopes {
private _transitiveScopesFor<T>(moduleType: Type<T>, resolvers: Resolvers):
NgModuleTransitiveScopes {
if (!isNgModule(moduleType)) {
throw new Error(`${moduleType.name} does not have an ngModuleDef`);
}
@ -567,12 +572,12 @@ function transitiveScopesFor<T>(
if (ngModule === null) {
throw new Error(`Importing ${imported.name} which does not have an @ngModule`);
} else {
compileNgModule(imported, resolvers);
this._compileNgModule(imported, resolvers);
}
// When this module imports another, the imported module's exported directives and pipes are
// added to the compilation scope of this module.
const importedScope = transitiveScopesFor(imported, resolvers);
const importedScope = this._transitiveScopesFor(imported, resolvers);
importedScope.exported.directives.forEach(entry => scopes.compilation.directives.add(entry));
importedScope.exported.pipes.forEach(entry => scopes.compilation.pipes.add(entry));
});
@ -591,7 +596,7 @@ function transitiveScopesFor<T>(
if (isNgModule(exportedTyped)) {
// When this module exports another, the exported module's exported directives and pipes are
// added to both the compilation and exported scopes of this module.
const exportedScope = transitiveScopesFor(exportedTyped, resolvers);
const exportedScope = this._transitiveScopesFor(exportedTyped, resolvers);
exportedScope.exported.directives.forEach(entry => {
scopes.compilation.directives.add(entry);
scopes.exported.directives.add(entry);
@ -609,15 +614,35 @@ function transitiveScopesFor<T>(
def.transitiveCompileScopes = scopes;
return scopes;
}
}
function flatten<T>(values: any[]): T[] {
let testBed: TestBedRender3;
export function _getTestBedRender3(): TestBedRender3 {
return testBed = testBed || new TestBedRender3();
}
const OWNER_MODULE = '__NG_MODULE__';
/**
* This function clears the OWNER_MODULE property from the Types. This is set in
* r3/jit/modules.ts. It is common for the same Type to be compiled in different tests. If we don't
* clear this we will get errors which will complain that the same Component/Directive is in more
* than one NgModule.
*/
function clearNgModules(type: Type<any>) {
if (type.hasOwnProperty(OWNER_MODULE)) {
(type as any)[OWNER_MODULE] = undefined;
}
}
function flatten<T>(values: any[], mapFn?: (value: T) => any): T[] {
const out: T[] = [];
values.forEach(value => {
if (Array.isArray(value)) {
out.push(...flatten<T>(value));
out.push(...flatten<T>(value, mapFn));
} else {
out.push(value);
out.push(mapFn ? mapFn(value) : value);
}
});
return out;

View File

@ -458,7 +458,7 @@ class CompWithUrlTemplate {
expect(TestBed.get('a')).toBe('mockA: depValue');
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
fixmeIvy('FW-855: TestBed.get(Compiler) should return TestBed-specific Compiler instance')
.it('should support SkipSelf', () => {
@NgModule({
providers: [
@ -553,8 +553,7 @@ class CompWithUrlTemplate {
});
describe('in Components', () => {
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
.it('should support useValue', () => {
it('should support useValue', () => {
@Component({
template: '',
providers: [
@ -571,8 +570,7 @@ class CompWithUrlTemplate {
expect(ctx.debugElement.injector.get('a')).toBe('mockValue');
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
.it('should support useFactory', () => {
it('should support useFactory', () => {
@Component({
template: '',
providers: [
@ -585,14 +583,13 @@ class CompWithUrlTemplate {
TestBed.overrideProvider(
'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: ['dep']});
const ctx = TestBed.configureTestingModule({declarations: [MyComp]})
.createComponent(MyComp);
const ctx =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
expect(ctx.debugElement.injector.get('a')).toBe('mockA: depValue');
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
.it('should support @Optional without matches', () => {
it('should support @Optional without matches', () => {
@Component({
template: '',
providers: [
@ -603,16 +600,14 @@ class CompWithUrlTemplate {
}
TestBed.overrideProvider(
'a',
{useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]});
const ctx = TestBed.configureTestingModule({declarations: [MyComp]})
.createComponent(MyComp);
'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]});
const ctx =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
expect(ctx.debugElement.injector.get('a')).toBe('mockA: null');
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
.it('should support Optional with matches', () => {
it('should support Optional with matches', () => {
@Component({
template: '',
providers: [
@ -624,16 +619,14 @@ class CompWithUrlTemplate {
}
TestBed.overrideProvider(
'a',
{useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]});
const ctx = TestBed.configureTestingModule({declarations: [MyComp]})
.createComponent(MyComp);
'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new Optional(), 'dep']]});
const ctx =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
expect(ctx.debugElement.injector.get('a')).toBe('mockA: depValue');
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
.it('should support SkipSelf', () => {
it('should support SkipSelf', () => {
@Directive({
selector: '[myDir]',
providers: [
@ -654,16 +647,13 @@ class CompWithUrlTemplate {
}
TestBed.overrideProvider(
'a',
{useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new SkipSelf(), 'dep']]});
'a', {useFactory: (dep: any) => `mockA: ${dep}`, deps: [[new SkipSelf(), 'dep']]});
const ctx = TestBed.configureTestingModule({declarations: [MyComp, MyDir]})
.createComponent(MyComp);
expect(ctx.debugElement.children[0].injector.get('a'))
.toBe('mockA: parentDepValue');
expect(ctx.debugElement.children[0].injector.get('a')).toBe('mockA: parentDepValue');
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
.it('should support multiple providers in a template', () => {
it('should support multiple providers in a template', () => {
@Directive({
selector: '[myDir1]',
providers: [
@ -708,22 +698,20 @@ class CompWithUrlTemplate {
constructor(@Inject('a') a: any, @Inject('b') b: any) {}
}
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
.it('should inject providers that were declared before it', () => {
it('should inject providers that were declared before it', () => {
TestBed.overrideProvider(
'b', {useFactory: (a: string) => `mockB: ${a}`, deps: ['a']});
const ctx = TestBed.configureTestingModule({declarations: [MyComp]})
.createComponent(MyComp);
const ctx =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
expect(ctx.debugElement.injector.get('b')).toBe('mockB: aValue');
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
.it('should inject providers that were declared after it', () => {
it('should inject providers that were declared after it', () => {
TestBed.overrideProvider(
'a', {useFactory: (b: string) => `mockA: ${b}`, deps: ['b']});
const ctx = TestBed.configureTestingModule({declarations: [MyComp]})
.createComponent(MyComp);
const ctx =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
expect(ctx.debugElement.injector.get('a')).toBe('mockA: bValue');
});
@ -739,7 +727,7 @@ class CompWithUrlTemplate {
});
describe('overrideTemplateUsingTestingModule', () => {
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
fixmeIvy('FW-851: TestBed.overrideTemplateUsingTestingModule is not implemented')
.it('should compile the template in the context of the testing module', () => {
@Component({selector: 'comp', template: 'a'})
class MyComponent {
@ -768,7 +756,7 @@ class CompWithUrlTemplate {
expect(testDir !.test).toBe('some prop');
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
fixmeIvy('FW-851: TestBed.overrideTemplateUsingTestingModule is not implemented')
.it('should throw if the TestBed is already created', () => {
@Component({selector: 'comp', template: 'a'})
class MyComponent {
@ -781,7 +769,7 @@ class CompWithUrlTemplate {
/Cannot override template when the test module has already been instantiated/);
});
fixmeIvy('FW-788: Support metadata override in TestBed (for AOT-compiled components)')
fixmeIvy('FW-851: TestBed.overrideTemplateUsingTestingModule is not implemented')
.it('should reset overrides when the testing module is resetted', () => {
@Component({selector: 'comp', template: 'a'})
class MyComponent {