From 1ebe172c2ec53746b614f153fa6498be263aab55 Mon Sep 17 00:00:00 2001 From: Andrew Scott Date: Tue, 5 Nov 2019 14:33:46 -0800 Subject: [PATCH] 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 --- packages/core/test/test_bed_spec.ts | 20 ++++++++++++- .../core/testing/src/r3_test_bed_compiler.ts | 28 ++++++++++++++----- 2 files changed, 40 insertions(+), 8 deletions(-) diff --git a/packages/core/test/test_bed_spec.ts b/packages/core/test/test_bed_spec.ts index 4251d4c7c1..3254db34d6 100644 --- a/packages/core/test/test_bed_spec.ts +++ b/packages/core/test/test_bed_spec.ts @@ -6,7 +6,7 @@ * 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 {NgModuleFactory} from '@angular/core/src/render3'; import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed'; @@ -409,6 +409,24 @@ describe('TestBed', () => { 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', () => { const MY_TOKEN = new InjectionToken('MyProvider'); class MyProvider {} diff --git a/packages/core/testing/src/r3_test_bed_compiler.ts b/packages/core/testing/src/r3_test_bed_compiler.ts index d6f2da644e..551b038d7e 100644 --- a/packages/core/testing/src/r3_test_bed_compiler.ts +++ b/packages/core/testing/src/r3_test_bed_compiler.ts @@ -7,9 +7,9 @@ */ 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 {ModuleRegistrationMap, getRegisteredModulesState, restoreRegisteredModulesState} from '../../src/linker/ng_module_factory_registration'; +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 {clearResolutionOfComponentResourcesQueue, isComponentDefPendingResolution, resolveComponentResources, restoreComponentResolutionQueue} from '../../src/metadata/resource_loading'; import {MetadataOverride} from './metadata_override'; @@ -82,6 +82,9 @@ export class R3TestBedCompiler { private providerOverrides: 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, Provider[]>(); private providerOverridesByToken = new Map(); private moduleProvidersOverridden = new Set>(); @@ -163,15 +166,23 @@ export class R3TestBedCompiler { } : {provide: token, useValue: provider.useValue, multi: provider.multi}; - let injectableDef: InjectableDef|null; - const isRoot = - (typeof token !== 'string' && (injectableDef = getInjectableDef(token)) && - injectableDef.providedIn === 'root'); + const injectableDef: InjectableDef|null = + typeof token !== 'string' ? getInjectableDef(token) : null; + const isRoot = injectableDef !== null && injectableDef.providedIn === 'root'; const overridesBucket = isRoot ? this.rootProviderOverrides : this.providerOverrides; overridesBucket.push(providerDef); // Keep overrides grouped by token as well for fast lookups using token 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, template: string): void { @@ -371,7 +382,10 @@ export class R3TestBedCompiler { const providersFromModules = flatten(flatten( injectorDef.imports, (imported: NgModuleType| ModuleWithProviders) => isModuleWithProviders(imported) ? imported.providers : [])); - const providers = [...providersFromModules, ...injectorDef.providers]; + const providers = [ + ...providersFromModules, ...injectorDef.providers, + ...(this.providerOverridesByModule.get(moduleType as InjectorType) || []) + ]; if (this.hasProviderOverrides(providers)) { this.maybeStoreNgDef(NG_INJ_DEF, moduleType);