fix(ivy): Handle overrides for {providedIn: AModule} in R3TestBed (#33606)

This issue was found when debugging a test failure that was using lazy
loaded modules with the router. When doing this, the router calls
`NgModuleFactory.create` for the loaded module. This module gets a new
injector so the overrides provided in TestBed are not applied unless the
Injectable is in the providers list (which is not the case for
{providedIn...} Injectables).

PR Close #33606
This commit is contained in:
Andrew Scott 2019-11-05 14:33:46 -08:00
parent c25503b142
commit 1ebe172c2e
2 changed files with 40 additions and 8 deletions

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Compiler, Component, Directive, ErrorHandler, Inject, Injectable, InjectionToken, Input, ModuleWithProviders, NgModule, Optional, Pipe, getModuleFactory, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineNgModule as defineNgModule, ɵɵtext as text} from '@angular/core'; import {Compiler, Component, Directive, ErrorHandler, Inject, Injectable, InjectionToken, Injector, Input, ModuleWithProviders, NgModule, Optional, Pipe, getModuleFactory, ɵsetClassMetadata as setClassMetadata, ɵɵdefineComponent as defineComponent, ɵɵdefineNgModule as defineNgModule, ɵɵtext as text} from '@angular/core';
import {registerModuleFactory} from '@angular/core/src/linker/ng_module_factory_registration'; import {registerModuleFactory} from '@angular/core/src/linker/ng_module_factory_registration';
import {NgModuleFactory} from '@angular/core/src/render3'; import {NgModuleFactory} from '@angular/core/src/render3';
import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed'; import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed';
@ -409,6 +409,24 @@ describe('TestBed', () => {
expect(service.get()).toEqual('override'); expect(service.get()).toEqual('override');
}); });
it('overrides injectable that is using providedIn: AModule', () => {
@NgModule()
class ServiceModule {
}
@Injectable({providedIn: ServiceModule})
class Service {
}
const fake = 'fake';
TestBed.overrideProvider(Service, {useValue: fake});
// Create an injector whose source is the ServiceModule, not DynamicTestModule.
const ngModuleFactory = TestBed.inject(Compiler).compileModuleSync(ServiceModule);
const injector = ngModuleFactory.create(TestBed.inject(Injector)).injector;
const service = injector.get(Service);
expect(service).toBe(fake);
});
it('allow to override multi provider', () => { it('allow to override multi provider', () => {
const MY_TOKEN = new InjectionToken('MyProvider'); const MY_TOKEN = new InjectionToken('MyProvider');
class MyProvider {} class MyProvider {}

View File

@ -7,9 +7,9 @@
*/ */
import {ResourceLoader} from '@angular/compiler'; import {ResourceLoader} from '@angular/compiler';
import {ApplicationInitStatus, COMPILER_OPTIONS, Compiler, Component, Directive, Injector, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, Type, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵgetInjectableDef as getInjectableDef, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDef as InjectableDef} from '@angular/core'; import {ApplicationInitStatus, COMPILER_OPTIONS, Compiler, Component, Directive, Injector, InjectorType, LOCALE_ID, ModuleWithComponentFactories, ModuleWithProviders, NgModule, NgModuleFactory, NgZone, Pipe, PlatformRef, Provider, Type, ɵDEFAULT_LOCALE_ID as DEFAULT_LOCALE_ID, ɵDirectiveDef as DirectiveDef, ɵNG_COMP_DEF as NG_COMP_DEF, ɵNG_DIR_DEF as NG_DIR_DEF, ɵNG_INJ_DEF as NG_INJ_DEF, ɵNG_MOD_DEF as NG_MOD_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleFactory as R3NgModuleFactory, ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes, ɵNgModuleType as NgModuleType, ɵRender3ComponentFactory as ComponentFactory, ɵRender3NgModuleRef as NgModuleRef, ɵcompileComponent as compileComponent, ɵcompileDirective as compileDirective, ɵcompileNgModuleDefs as compileNgModuleDefs, ɵcompilePipe as compilePipe, ɵgetInjectableDef as getInjectableDef, ɵpatchComponentDefWithScope as patchComponentDefWithScope, ɵsetLocaleId as setLocaleId, ɵtransitiveScopesFor as transitiveScopesFor, ɵɵInjectableDef as InjectableDef} from '@angular/core';
import {ModuleRegistrationMap, getRegisteredModulesState, restoreRegisteredModulesState} from '../../src/linker/ng_module_factory_registration';
import {ModuleRegistrationMap, getRegisteredModulesState, restoreRegisteredModulesState} from '../../src/linker/ng_module_factory_registration';
import {clearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution, resolveComponentResources, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading'; import {clearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution, resolveComponentResources, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading';
import {MetadataOverride} from './metadata_override'; import {MetadataOverride} from './metadata_override';
@ -82,6 +82,9 @@ export class R3TestBedCompiler {
private providerOverrides: Provider[] = []; private providerOverrides: Provider[] = [];
private rootProviderOverrides: Provider[] = []; private rootProviderOverrides: Provider[] = [];
// Overrides for injectables with `{providedIn: SomeModule}` need to be tracked and added to that
// module's provider list.
private providerOverridesByModule = new Map<InjectorType<any>, Provider[]>();
private providerOverridesByToken = new Map<any, Provider>(); private providerOverridesByToken = new Map<any, Provider>();
private moduleProvidersOverridden = new Set<Type<any>>(); private moduleProvidersOverridden = new Set<Type<any>>();
@ -163,15 +166,23 @@ export class R3TestBedCompiler {
} : } :
{provide: token, useValue: provider.useValue, multi: provider.multi}; {provide: token, useValue: provider.useValue, multi: provider.multi};
let injectableDef: InjectableDef<any>|null; const injectableDef: InjectableDef<any>|null =
const isRoot = typeof token !== 'string' ? getInjectableDef(token) : null;
(typeof token !== 'string' && (injectableDef = getInjectableDef(token)) && const isRoot = injectableDef !== null && injectableDef.providedIn === 'root';
injectableDef.providedIn === 'root');
const overridesBucket = isRoot ? this.rootProviderOverrides : this.providerOverrides; const overridesBucket = isRoot ? this.rootProviderOverrides : this.providerOverrides;
overridesBucket.push(providerDef); overridesBucket.push(providerDef);
// Keep overrides grouped by token as well for fast lookups using token // Keep overrides grouped by token as well for fast lookups using token
this.providerOverridesByToken.set(token, providerDef); this.providerOverridesByToken.set(token, providerDef);
if (injectableDef !== null && injectableDef.providedIn !== null &&
typeof injectableDef.providedIn !== 'string') {
const existingOverrides = this.providerOverridesByModule.get(injectableDef.providedIn);
if (existingOverrides !== undefined) {
existingOverrides.push(providerDef);
} else {
this.providerOverridesByModule.set(injectableDef.providedIn, [providerDef]);
}
}
} }
overrideTemplateUsingTestingModule(type: Type<any>, template: string): void { overrideTemplateUsingTestingModule(type: Type<any>, template: string): void {
@ -371,7 +382,10 @@ export class R3TestBedCompiler {
const providersFromModules = flatten(flatten( const providersFromModules = flatten(flatten(
injectorDef.imports, (imported: NgModuleType<any>| ModuleWithProviders<any>) => injectorDef.imports, (imported: NgModuleType<any>| ModuleWithProviders<any>) =>
isModuleWithProviders(imported) ? imported.providers : [])); isModuleWithProviders(imported) ? imported.providers : []));
const providers = [...providersFromModules, ...injectorDef.providers]; const providers = [
...providersFromModules, ...injectorDef.providers,
...(this.providerOverridesByModule.get(moduleType as InjectorType<any>) || [])
];
if (this.hasProviderOverrides(providers)) { if (this.hasProviderOverrides(providers)) {
this.maybeStoreNgDef(NG_INJ_DEF, moduleType); this.maybeStoreNgDef(NG_INJ_DEF, moduleType);