fix(ivy): align NgModuleRef implementation between Ivy and ViewEngine (#27482)

Solves FW-765 and FW-767

PR Close #27482
This commit is contained in:
Marc Laval 2018-12-05 17:43:59 +01:00 committed by Igor Minar
parent 159ab1c257
commit 8e9858fadb
6 changed files with 353 additions and 254 deletions

View File

@ -34,10 +34,15 @@ import {createElementRef} from './view_engine_compatibility';
import {RootViewRef, ViewRef} from './view_ref'; import {RootViewRef, ViewRef} from './view_ref';
export class ComponentFactoryResolver extends viewEngine_ComponentFactoryResolver { export class ComponentFactoryResolver extends viewEngine_ComponentFactoryResolver {
/**
* @param ngModule The NgModuleRef to which all resolved factories are bound.
*/
constructor(private ngModule?: viewEngine_NgModuleRef<any>) { super(); }
resolveComponentFactory<T>(component: Type<T>): viewEngine_ComponentFactory<T> { resolveComponentFactory<T>(component: Type<T>): viewEngine_ComponentFactory<T> {
ngDevMode && assertComponentType(component); ngDevMode && assertComponentType(component);
const componentDef = getComponentDef(component) !; const componentDef = getComponentDef(component) !;
return new ComponentFactory(componentDef); return new ComponentFactory(componentDef, this.ngModule);
} }
} }
@ -75,10 +80,13 @@ function createChainedInjector(rootViewInjector: Injector, moduleInjector: Injec
get: <T>(token: Type<T>| InjectionToken<T>, notFoundValue?: T): T => { get: <T>(token: Type<T>| InjectionToken<T>, notFoundValue?: T): T => {
const value = rootViewInjector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR); const value = rootViewInjector.get(token, NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR);
if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) { if (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR ||
notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) {
// Return the value from the root element injector when // Return the value from the root element injector when
// - it provides it // - it provides it
// (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR) // (value !== NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
// - the module injector should not be checked
// (notFoundValue === NOT_FOUND_CHECK_ONLY_ELEMENT_INJECTOR)
return value; return value;
} }
@ -103,7 +111,12 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
return toRefArray(this.componentDef.outputs); return toRefArray(this.componentDef.outputs);
} }
constructor(private componentDef: ComponentDef<any>) { /**
* @param componentDef The component definition.
* @param ngModule The NgModuleRef to which the factory is bound.
*/
constructor(
private componentDef: ComponentDef<any>, private ngModule?: viewEngine_NgModuleRef<any>) {
super(); super();
this.componentType = componentDef.type; this.componentType = componentDef.type;
this.selector = componentDef.selectors[0][0] as string; this.selector = componentDef.selectors[0][0] as string;
@ -114,6 +127,7 @@ export class ComponentFactory<T> extends viewEngine_ComponentFactory<T> {
injector: Injector, projectableNodes?: any[][]|undefined, rootSelectorOrNode?: any, injector: Injector, projectableNodes?: any[][]|undefined, rootSelectorOrNode?: any,
ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<T> { ngModule?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<T> {
const isInternalRootView = rootSelectorOrNode === undefined; const isInternalRootView = rootSelectorOrNode === undefined;
ngModule = ngModule || this.ngModule;
const rootViewInjector = const rootViewInjector =
ngModule ? createChainedInjector(injector, ngModule.injector) : injector; ngModule ? createChainedInjector(injector, ngModule.injector) : injector;

View File

@ -6,7 +6,8 @@
* found in the LICENSE file at https://angular.io/license * found in the LICENSE file at https://angular.io/license
*/ */
import {Injector} from '../di/injector'; import {INJECTOR, Injector} from '../di/injector';
import {InjectFlags} from '../di/injector_compatibility';
import {StaticProvider} from '../di/provider'; import {StaticProvider} from '../di/provider';
import {createInjector} from '../di/r3_injector'; import {createInjector} from '../di/r3_injector';
import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver'; import {ComponentFactoryResolver as viewEngine_ComponentFactoryResolver} from '../linker/component_factory_resolver';
@ -14,27 +15,29 @@ import {InternalNgModuleRef, NgModuleFactory as viewEngine_NgModuleFactory, NgMo
import {NgModuleDef} from '../metadata/ng_module'; import {NgModuleDef} from '../metadata/ng_module';
import {Type} from '../type'; import {Type} from '../type';
import {stringify} from '../util'; import {stringify} from '../util';
import {assertDefined} from './assert'; import {assertDefined} from './assert';
import {ComponentFactoryResolver} from './component_ref'; import {ComponentFactoryResolver} from './component_ref';
import {getNgModuleDef} from './definition'; import {getNgModuleDef} from './definition';
export interface NgModuleType { ngModuleDef: NgModuleDef<any>; } export interface NgModuleType { ngModuleDef: NgModuleDef<any>; }
export const COMPONENT_FACTORY_RESOLVER: StaticProvider = { const COMPONENT_FACTORY_RESOLVER: StaticProvider = {
provide: viewEngine_ComponentFactoryResolver, provide: viewEngine_ComponentFactoryResolver,
useFactory: () => new ComponentFactoryResolver(), useClass: ComponentFactoryResolver,
deps: [], deps: [viewEngine_NgModuleRef],
}; };
export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements InternalNgModuleRef<T> { export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements InternalNgModuleRef<T> {
// tslint:disable-next-line:require-internal-with-underscore // tslint:disable-next-line:require-internal-with-underscore
_bootstrapComponents: Type<any>[] = []; _bootstrapComponents: Type<any>[] = [];
injector: Injector; // tslint:disable-next-line:require-internal-with-underscore
componentFactoryResolver: viewEngine_ComponentFactoryResolver; _r3Injector: Injector;
injector: Injector = this;
instance: T; instance: T;
destroyCbs: (() => void)[]|null = []; destroyCbs: (() => void)[]|null = [];
constructor(ngModuleType: Type<T>, parentInjector: Injector|null) { constructor(ngModuleType: Type<T>, public _parent: Injector|null) {
super(); super();
const ngModuleDef = getNgModuleDef(ngModuleType); const ngModuleDef = getNgModuleDef(ngModuleType);
ngDevMode && assertDefined( ngDevMode && assertDefined(
@ -43,14 +46,26 @@ export class NgModuleRef<T> extends viewEngine_NgModuleRef<T> implements Interna
this._bootstrapComponents = ngModuleDef !.bootstrap; this._bootstrapComponents = ngModuleDef !.bootstrap;
const additionalProviders: StaticProvider[] = [ const additionalProviders: StaticProvider[] = [
COMPONENT_FACTORY_RESOLVER, { {
provide: viewEngine_NgModuleRef, provide: viewEngine_NgModuleRef,
useValue: this, useValue: this,
} },
COMPONENT_FACTORY_RESOLVER
]; ];
this.injector = createInjector(ngModuleType, parentInjector, additionalProviders); this._r3Injector = createInjector(ngModuleType, _parent, additionalProviders);
this.instance = this.injector.get(ngModuleType); this.instance = this.get(ngModuleType);
this.componentFactoryResolver = new ComponentFactoryResolver(); }
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND,
injectFlags: InjectFlags = InjectFlags.Default): any {
if (token === Injector || token === viewEngine_NgModuleRef || token === INJECTOR) {
return this;
}
return this._r3Injector.get(token, notFoundValue, injectFlags);
}
get componentFactoryResolver(): viewEngine_ComponentFactoryResolver {
return this.get(viewEngine_ComponentFactoryResolver);
} }
destroy(): void { destroy(): void {

View File

@ -235,7 +235,7 @@ export function createContainerRef(
injector?: Injector|undefined, projectableNodes?: any[][]|undefined, injector?: Injector|undefined, projectableNodes?: any[][]|undefined,
ngModuleRef?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<C> { ngModuleRef?: viewEngine_NgModuleRef<any>|undefined): viewEngine_ComponentRef<C> {
const contextInjector = injector || this.parentInjector; const contextInjector = injector || this.parentInjector;
if (!ngModuleRef && contextInjector) { if (!ngModuleRef && (componentFactory as any).ngModule == null && contextInjector) {
ngModuleRef = contextInjector.get(viewEngine_NgModuleRef, null); ngModuleRef = contextInjector.get(viewEngine_NgModuleRef, null);
} }

View File

@ -187,5 +187,87 @@ describe('ComponentFactory', () => {
expect(mSanitizerFactorySpy).toHaveBeenCalled(); expect(mSanitizerFactorySpy).toHaveBeenCalled();
}); });
}); });
describe('(when the factory is bound to a `ngModuleRef`)', () => {
it('should retrieve `RendererFactory2` from the specified injector first', () => {
const injector = Injector.create([
{provide: RendererFactory2, useValue: {createRenderer: createRenderer2Spy}},
]);
(cf as any).ngModule = {
injector: Injector.create([
{provide: RendererFactory2, useValue: {createRenderer: createRenderer3Spy}},
])
};
cf.create(injector);
expect(createRenderer2Spy).toHaveBeenCalled();
expect(createRenderer3Spy).not.toHaveBeenCalled();
});
it('should retrieve `RendererFactory2` from the `ngModuleRef` if not provided by the injector',
() => {
const injector = Injector.create([]);
(cf as any).ngModule = {
injector: Injector.create([
{provide: RendererFactory2, useValue: {createRenderer: createRenderer2Spy}},
])
};
cf.create(injector);
expect(createRenderer2Spy).toHaveBeenCalled();
expect(createRenderer3Spy).not.toHaveBeenCalled();
});
it('should fall back to `domRendererFactory3` if `RendererFactory2` is not provided', () => {
const injector = Injector.create([]);
(cf as any).ngModule = {injector: Injector.create([])};
cf.create(injector);
expect(createRenderer2Spy).not.toHaveBeenCalled();
expect(createRenderer3Spy).toHaveBeenCalled();
});
it('should retrieve `Sanitizer` from the specified injector first', () => {
const iSanitizerFactorySpy =
jasmine.createSpy('Injector#sanitizerFactory').and.returnValue({});
const injector = Injector.create([
{provide: Sanitizer, useFactory: iSanitizerFactorySpy, deps: []},
]);
const mSanitizerFactorySpy =
jasmine.createSpy('NgModuleRef#sanitizerFactory').and.returnValue({});
(cf as any).ngModule = {
injector: Injector.create([
{provide: Sanitizer, useFactory: mSanitizerFactorySpy, deps: []},
])
};
cf.create(injector);
expect(iSanitizerFactorySpy).toHaveBeenCalled();
expect(mSanitizerFactorySpy).not.toHaveBeenCalled();
});
it('should retrieve `Sanitizer` from the `ngModuleRef` if not provided by the injector',
() => {
const injector = Injector.create([]);
const mSanitizerFactorySpy =
jasmine.createSpy('NgModuleRef#sanitizerFactory').and.returnValue({});
(cf as any).ngModule = {
injector: Injector.create([
{provide: Sanitizer, useFactory: mSanitizerFactorySpy, deps: []},
])
};
cf.create(injector);
expect(mSanitizerFactorySpy).toHaveBeenCalled();
});
});
}); });
}); });

View File

@ -3561,94 +3561,93 @@ describe('Integration', () => {
expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]'); expect(fixture.nativeElement).toHaveText('lazy-loaded-parent [lazy-loaded-child]');
}))); })));
fixmeIvy('FW-646: Directive providers don\'t support primitive types as DI tokens') it('should have 2 injector trees: module and element',
.it('should have 2 injector trees: module and element', fakeAsync(inject(
fakeAsync(inject( [Router, Location, NgModuleFactoryLoader],
[Router, Location, NgModuleFactoryLoader], (router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => { @Component({
@Component({ selector: 'lazy',
selector: 'lazy', template: 'parent[<router-outlet></router-outlet>]',
template: 'parent[<router-outlet></router-outlet>]', viewProviders: [
viewProviders: [ {provide: 'shadow', useValue: 'from parent component'},
{provide: 'shadow', useValue: 'from parent component'}, ],
], })
}) class Parent {
class Parent { }
}
@Component({selector: 'lazy', template: 'child'}) @Component({selector: 'lazy', template: 'child'})
class Child { class Child {
} }
@NgModule({ @NgModule({
declarations: [Parent], declarations: [Parent],
imports: [RouterModule.forChild([{ imports: [RouterModule.forChild([{
path: 'parent', path: 'parent',
component: Parent, component: Parent,
children: [ children: [
{path: 'child', loadChildren: 'child'}, {path: 'child', loadChildren: 'child'},
] ]
}])], }])],
providers: [ providers: [
{provide: 'moduleName', useValue: 'parent'}, {provide: 'moduleName', useValue: 'parent'},
{provide: 'fromParent', useValue: 'from parent'}, {provide: 'fromParent', useValue: 'from parent'},
], ],
}) })
class ParentModule { class ParentModule {
} }
@NgModule({ @NgModule({
declarations: [Child], declarations: [Child],
imports: [RouterModule.forChild([{path: '', component: Child}])], imports: [RouterModule.forChild([{path: '', component: Child}])],
providers: [ providers: [
{provide: 'moduleName', useValue: 'child'}, {provide: 'moduleName', useValue: 'child'},
{provide: 'fromChild', useValue: 'from child'}, {provide: 'fromChild', useValue: 'from child'},
{provide: 'shadow', useValue: 'from child module'}, {provide: 'shadow', useValue: 'from child module'},
], ],
}) })
class ChildModule { class ChildModule {
} }
loader.stubbedModules = { loader.stubbedModules = {
parent: ParentModule, parent: ParentModule,
child: ChildModule, child: ChildModule,
}; };
const fixture = createRoot(router, RootCmp); const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'parent'}]); router.resetConfig([{path: 'lazy', loadChildren: 'parent'}]);
router.navigateByUrl('/lazy/parent/child'); router.navigateByUrl('/lazy/parent/child');
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/lazy/parent/child'); expect(location.path()).toEqual('/lazy/parent/child');
expect(fixture.nativeElement).toHaveText('parent[child]'); expect(fixture.nativeElement).toHaveText('parent[child]');
const pInj = fixture.debugElement.query(By.directive(Parent)).injector !; const pInj = fixture.debugElement.query(By.directive(Parent)).injector !;
const cInj = fixture.debugElement.query(By.directive(Child)).injector !; const cInj = fixture.debugElement.query(By.directive(Child)).injector !;
expect(pInj.get('moduleName')).toEqual('parent'); expect(pInj.get('moduleName')).toEqual('parent');
expect(pInj.get('fromParent')).toEqual('from parent'); expect(pInj.get('fromParent')).toEqual('from parent');
expect(pInj.get(Parent)).toBeAnInstanceOf(Parent); expect(pInj.get(Parent)).toBeAnInstanceOf(Parent);
expect(pInj.get('fromChild', null)).toEqual(null); expect(pInj.get('fromChild', null)).toEqual(null);
expect(pInj.get(Child, null)).toEqual(null); expect(pInj.get(Child, null)).toEqual(null);
expect(cInj.get('moduleName')).toEqual('child'); expect(cInj.get('moduleName')).toEqual('child');
expect(cInj.get('fromParent')).toEqual('from parent'); expect(cInj.get('fromParent')).toEqual('from parent');
expect(cInj.get('fromChild')).toEqual('from child'); expect(cInj.get('fromChild')).toEqual('from child');
expect(cInj.get(Parent)).toBeAnInstanceOf(Parent); expect(cInj.get(Parent)).toBeAnInstanceOf(Parent);
expect(cInj.get(Child)).toBeAnInstanceOf(Child); expect(cInj.get(Child)).toBeAnInstanceOf(Child);
// The child module can not shadow the parent component // The child module can not shadow the parent component
expect(cInj.get('shadow')).toEqual('from parent component'); expect(cInj.get('shadow')).toEqual('from parent component');
const pmInj = pInj.get(NgModuleRef).injector; const pmInj = pInj.get(NgModuleRef).injector;
const cmInj = cInj.get(NgModuleRef).injector; const cmInj = cInj.get(NgModuleRef).injector;
expect(pmInj.get('moduleName')).toEqual('parent'); expect(pmInj.get('moduleName')).toEqual('parent');
expect(cmInj.get('moduleName')).toEqual('child'); expect(cmInj.get('moduleName')).toEqual('child');
expect(pmInj.get(Parent, '-')).toEqual('-'); expect(pmInj.get(Parent, '-')).toEqual('-');
expect(cmInj.get(Parent, '-')).toEqual('-'); expect(cmInj.get(Parent, '-')).toEqual('-');
expect(pmInj.get(Child, '-')).toEqual('-'); expect(pmInj.get(Child, '-')).toEqual('-');
expect(cmInj.get(Child, '-')).toEqual('-'); expect(cmInj.get(Child, '-')).toEqual('-');
}))); })));
// https://github.com/angular/angular/issues/12889 // https://github.com/angular/angular/issues/12889
it('should create a single instance of lazy-loaded modules', it('should create a single instance of lazy-loaded modules',
@ -3689,57 +3688,55 @@ describe('Integration', () => {
}))); })));
// https://github.com/angular/angular/issues/13870 // https://github.com/angular/angular/issues/13870
fixmeIvy( it('should create a single instance of guards for lazy-loaded modules',
'FW-767: Lazy loaded modules are not used when resolving dependencies in one of their components') fakeAsync(inject(
.it('should create a single instance of guards for lazy-loaded modules', [Router, Location, NgModuleFactoryLoader],
fakeAsync(inject( (router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
[Router, Location, NgModuleFactoryLoader], @Injectable()
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => { class Service {
@Injectable() }
class Service {
}
@Injectable() @Injectable()
class Resolver implements Resolve<Service> { class Resolver implements Resolve<Service> {
constructor(public service: Service) {} constructor(public service: Service) {}
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) { resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
return this.service; return this.service;
} }
} }
@Component({selector: 'lazy', template: 'lazy'}) @Component({selector: 'lazy', template: 'lazy'})
class LazyLoadedComponent { class LazyLoadedComponent {
resolvedService: Service; resolvedService: Service;
constructor(public injectedService: Service, route: ActivatedRoute) { constructor(public injectedService: Service, route: ActivatedRoute) {
this.resolvedService = route.snapshot.data['service']; this.resolvedService = route.snapshot.data['service'];
} }
} }
@NgModule({ @NgModule({
declarations: [LazyLoadedComponent], declarations: [LazyLoadedComponent],
providers: [Service, Resolver], providers: [Service, Resolver],
imports: [ imports: [
RouterModule.forChild([{ RouterModule.forChild([{
path: 'loaded', path: 'loaded',
component: LazyLoadedComponent, component: LazyLoadedComponent,
resolve: {'service': Resolver}, resolve: {'service': Resolver},
}]), }]),
] ]
}) })
class LoadedModule { class LoadedModule {
} }
loader.stubbedModules = {expected: LoadedModule}; loader.stubbedModules = {expected: LoadedModule};
const fixture = createRoot(router, RootCmp); const fixture = createRoot(router, RootCmp);
router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]); router.resetConfig([{path: 'lazy', loadChildren: 'expected'}]);
router.navigateByUrl('/lazy/loaded'); router.navigateByUrl('/lazy/loaded');
advance(fixture); advance(fixture);
expect(fixture.nativeElement).toHaveText('lazy'); expect(fixture.nativeElement).toHaveText('lazy');
const lzc = fixture.debugElement.query(By.directive(LazyLoadedComponent)) const lzc =
.componentInstance; fixture.debugElement.query(By.directive(LazyLoadedComponent)).componentInstance;
expect(lzc.injectedService).toBe(lzc.resolvedService); expect(lzc.injectedService).toBe(lzc.resolvedService);
}))); })));
it('should emit RouteConfigLoadStart and RouteConfigLoadEnd event when route is lazy loaded', it('should emit RouteConfigLoadStart and RouteConfigLoadEnd event when route is lazy loaded',
@ -3990,28 +3987,26 @@ describe('Integration', () => {
}); });
}); });
fixmeIvy( it('should use the injector of the lazily-loaded configuration',
'FW-767: Lazy loaded modules are not used when resolving dependencies in one of their components') fakeAsync(inject(
.it('should use the injector of the lazily-loaded configuration', [Router, Location, NgModuleFactoryLoader],
fakeAsync(inject( (router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
[Router, Location, NgModuleFactoryLoader], loader.stubbedModules = {expected: LoadedModule};
(router: Router, location: Location, loader: SpyNgModuleFactoryLoader) => {
loader.stubbedModules = {expected: LoadedModule};
const fixture = createRoot(router, RootCmp); const fixture = createRoot(router, RootCmp);
router.resetConfig([{ router.resetConfig([{
path: 'eager-parent', path: 'eager-parent',
component: EagerParentComponent, component: EagerParentComponent,
children: [{path: 'lazy', loadChildren: 'expected'}] children: [{path: 'lazy', loadChildren: 'expected'}]
}]); }]);
router.navigateByUrl('/eager-parent/lazy/lazy-parent/lazy-child'); router.navigateByUrl('/eager-parent/lazy/lazy-parent/lazy-child');
advance(fixture); advance(fixture);
expect(location.path()).toEqual('/eager-parent/lazy/lazy-parent/lazy-child'); expect(location.path()).toEqual('/eager-parent/lazy/lazy-parent/lazy-child');
expect(fixture.nativeElement).toHaveText('eager-parent lazy-parent lazy-child'); expect(fixture.nativeElement).toHaveText('eager-parent lazy-parent lazy-child');
}))); })));
}); });
it('works when given a callback', it('works when given a callback',

View File

@ -8,7 +8,6 @@
import {Compiler, Component, NgModule, NgModuleFactoryLoader, NgModuleRef} from '@angular/core'; import {Compiler, Component, NgModule, NgModuleFactoryLoader, NgModuleRef} from '@angular/core';
import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing'; import {TestBed, fakeAsync, inject, tick} from '@angular/core/testing';
import {fixmeIvy} from '@angular/private/testing';
import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '@angular/router'; import {PreloadAllModules, PreloadingStrategy, RouterPreloader} from '@angular/router';
import {Route, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index'; import {Route, RouteConfigLoadEnd, RouteConfigLoadStart, Router, RouterModule} from '../index';
@ -60,66 +59,63 @@ describe('RouterPreloader', () => {
}); });
}); });
it('should work',
fakeAsync(inject(
[NgModuleFactoryLoader, RouterPreloader, Router, NgModuleRef],
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router,
testModule: NgModuleRef<any>) => {
const events: Array<RouteConfigLoadStart|RouteConfigLoadEnd> = [];
@NgModule({
declarations: [LazyLoadedCmp],
imports:
[RouterModule.forChild([{path: 'LoadedModule2', component: LazyLoadedCmp}])]
})
class LoadedModule2 {
}
fixmeIvy( @NgModule({
'FW-765: NgModuleRef hierarchy is differently constructed when the router preloads modules') imports:
.it('should work', [RouterModule.forChild([{path: 'LoadedModule1', loadChildren: 'expected2'}])]
fakeAsync(inject( })
[NgModuleFactoryLoader, RouterPreloader, Router, NgModuleRef], class LoadedModule1 {
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router, }
testModule: NgModuleRef<any>) => {
const events: Array<RouteConfigLoadStart|RouteConfigLoadEnd> = [];
@NgModule({
declarations: [LazyLoadedCmp],
imports: [RouterModule.forChild(
[{path: 'LoadedModule2', component: LazyLoadedCmp}])]
})
class LoadedModule2 {
}
@NgModule({ router.events.subscribe(e => {
imports: [RouterModule.forChild( if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) {
[{path: 'LoadedModule1', loadChildren: 'expected2'}])] events.push(e);
}) }
class LoadedModule1 { });
}
router.events.subscribe(e => { loader.stubbedModules = {
if (e instanceof RouteConfigLoadEnd || e instanceof RouteConfigLoadStart) { expected: LoadedModule1,
events.push(e); expected2: LoadedModule2,
} };
});
loader.stubbedModules = { preloader.preload().subscribe(() => {});
expected: LoadedModule1,
expected2: LoadedModule2,
};
preloader.preload().subscribe(() => {}); tick();
tick(); const c = router.config;
expect(c[0].loadChildren).toEqual('expected');
const c = router.config; const loadedConfig: LoadedRouterConfig = (c[0] as any)._loadedConfig !;
expect(c[0].loadChildren).toEqual('expected'); const module: any = loadedConfig.module;
expect(loadedConfig.routes[0].path).toEqual('LoadedModule1');
expect(module._parent).toBe(testModule);
const loadedConfig: LoadedRouterConfig = (c[0] as any)._loadedConfig !; const loadedConfig2: LoadedRouterConfig =
const module: any = loadedConfig.module; (loadedConfig.routes[0] as any)._loadedConfig !;
expect(loadedConfig.routes[0].path).toEqual('LoadedModule1'); const module2: any = loadedConfig2.module;
expect(module._parent).toBe(testModule); expect(loadedConfig2.routes[0].path).toEqual('LoadedModule2');
expect(module2._parent).toBe(module);
const loadedConfig2: LoadedRouterConfig = expect(events.map(e => e.toString())).toEqual([
(loadedConfig.routes[0] as any)._loadedConfig !; 'RouteConfigLoadStart(path: lazy)',
const module2: any = loadedConfig2.module; 'RouteConfigLoadEnd(path: lazy)',
expect(loadedConfig2.routes[0].path).toEqual('LoadedModule2'); 'RouteConfigLoadStart(path: LoadedModule1)',
expect(module2._parent).toBe(module); 'RouteConfigLoadEnd(path: LoadedModule1)',
]);
expect(events.map(e => e.toString())).toEqual([ })));
'RouteConfigLoadStart(path: lazy)',
'RouteConfigLoadEnd(path: lazy)',
'RouteConfigLoadStart(path: LoadedModule1)',
'RouteConfigLoadEnd(path: LoadedModule1)',
]);
})));
}); });
describe('should support modules that have already been loaded', () => { describe('should support modules that have already been loaded', () => {
@ -130,60 +126,57 @@ describe('RouterPreloader', () => {
}); });
}); });
fixmeIvy( it('should work', fakeAsync(inject(
'FW-765: NgModuleRef hierarchy is differently constructed when the router preloads modules') [NgModuleFactoryLoader, RouterPreloader, Router, NgModuleRef, Compiler],
.it('should work', (loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader,
fakeAsync(inject( router: Router, testModule: NgModuleRef<any>, compiler: Compiler) => {
[NgModuleFactoryLoader, RouterPreloader, Router, NgModuleRef, Compiler], @NgModule()
(loader: SpyNgModuleFactoryLoader, preloader: RouterPreloader, router: Router, class LoadedModule2 {
testModule: NgModuleRef<any>, compiler: Compiler) => { }
@NgModule()
class LoadedModule2 {
}
const module2 = compiler.compileModuleSync(LoadedModule2).create(null); const module2 = compiler.compileModuleSync(LoadedModule2).create(null);
@NgModule({ @NgModule({
imports: [RouterModule.forChild([ imports: [RouterModule.forChild([
<Route>{ <Route>{
path: 'LoadedModule2', path: 'LoadedModule2',
loadChildren: 'no', loadChildren: 'no',
_loadedConfig: { _loadedConfig: {
routes: [{path: 'LoadedModule3', loadChildren: 'expected3'}], routes: [{path: 'LoadedModule3', loadChildren: 'expected3'}],
module: module2, module: module2,
} }
}, },
])] ])]
}) })
class LoadedModule1 { class LoadedModule1 {
} }
@NgModule({imports: [RouterModule.forChild([])]}) @NgModule({imports: [RouterModule.forChild([])]})
class LoadedModule3 { class LoadedModule3 {
} }
loader.stubbedModules = { loader.stubbedModules = {
expected: LoadedModule1, expected: LoadedModule1,
expected3: LoadedModule3, expected3: LoadedModule3,
}; };
preloader.preload().subscribe(() => {}); preloader.preload().subscribe(() => {});
tick(); tick();
const c = router.config; const c = router.config;
const loadedConfig: LoadedRouterConfig = (c[0] as any)._loadedConfig !; const loadedConfig: LoadedRouterConfig = (c[0] as any)._loadedConfig !;
const module: any = loadedConfig.module; const module: any = loadedConfig.module;
expect(module._parent).toBe(testModule); expect(module._parent).toBe(testModule);
const loadedConfig2: LoadedRouterConfig = const loadedConfig2: LoadedRouterConfig =
(loadedConfig.routes[0] as any)._loadedConfig !; (loadedConfig.routes[0] as any)._loadedConfig !;
const loadedConfig3: LoadedRouterConfig = const loadedConfig3: LoadedRouterConfig =
(loadedConfig2.routes[0] as any)._loadedConfig !; (loadedConfig2.routes[0] as any)._loadedConfig !;
const module3: any = loadedConfig3.module; const module3: any = loadedConfig3.module;
expect(module3._parent).toBe(module2); expect(module3._parent).toBe(module2);
}))); })));
}); });
describe('should ignore errors', () => { describe('should ignore errors', () => {