feat(ivy): implement `compileComponents` method for `TestBedRender3` (#27778)

The implementation of the `compileComponents` method for `TestBedRender3` was missing.
We now pass each component through `resolveComponentResources` when `TestBed.compileComponents` is called so that `templateUrl` and `styleUrls` can be resolved asynchronously and used once `TestBed.createComponent` is called.
The component's metadata are overriden in `TestBed` instead of mutating the original metadata like this is the case outside of TestBed. The reason for that is that we need to ensure that we didn't mutate anything so that the following tests can run with the same original metadata, otherwise we it could trigger or hide some errors.

FW-553 #resolve

PR Close #27778
This commit is contained in:
Olivier Combe 2019-01-14 14:46:21 +01:00 committed by Andrew Kushnir
parent 3a31a2795e
commit 29bff0f02e
6 changed files with 106 additions and 51 deletions

View File

@ -19,7 +19,7 @@ export {APP_ROOT as ɵAPP_ROOT} from './di/scope';
export {ivyEnabled as ɵivyEnabled} from './ivy_switch';
export {ComponentFactory as ɵComponentFactory} from './linker/component_factory';
export {CodegenComponentFactoryResolver as ɵCodegenComponentFactoryResolver} from './linker/component_factory_resolver';
export {resolveComponentResources as ɵresolveComponentResources} from './metadata/resource_loading';
export {clearResolutionOfComponentResourcesQueue as ɵclearResolutionOfComponentResourcesQueue, resolveComponentResources as ɵresolveComponentResources} from './metadata/resource_loading';
export {ReflectionCapabilities as ɵReflectionCapabilities} from './reflection/reflection_capabilities';
export {GetterFn as ɵGetterFn, MethodFn as ɵMethodFn, SetterFn as ɵSetterFn} from './reflection/types';
export {DirectRenderer as ɵDirectRenderer, RenderDebugInfo as ɵRenderDebugInfo} from './render/api';

View File

@ -79,7 +79,7 @@ export function resolveComponentResources(
});
});
});
componentResourceResolutionQueue.clear();
clearResolutionOfComponentResourcesQueue();
return Promise.all(urlFetches).then(() => null);
}

View File

@ -12,6 +12,7 @@ ng_module(
module_name = "@angular/core/testing",
deps = [
"//packages:types",
"//packages/compiler",
"//packages/core",
"@ngdeps//@types/jasmine",
"@ngdeps//zone.js",

View File

@ -34,7 +34,6 @@ import {
ɵNG_PIPE_DEF as NG_PIPE_DEF,
ɵNgModuleDef as NgModuleDef,
ɵNgModuleFactory as R3NgModuleFactory,
ɵNgModuleTransitiveScopes as NgModuleTransitiveScopes,
ɵNgModuleType as NgModuleType,
ɵRender3ComponentFactory as ComponentFactory,
ɵRender3NgModuleRef as NgModuleRef,
@ -46,10 +45,16 @@ import {
ɵflushModuleScopingQueueAsMuchAsPossible as flushModuleScopingQueueAsMuchAsPossible,
ɵpatchComponentDefWithScope as patchComponentDefWithScope,
ɵresetCompiledComponents as resetCompiledComponents,
ɵstringify as stringify, ɵtransitiveScopesFor as transitiveScopesFor,
ɵstringify as stringify,
ɵtransitiveScopesFor as transitiveScopesFor,
CompilerOptions,
StaticProvider,
COMPILER_OPTIONS,
} from '@angular/core';
// clang-format on
import {ResourceLoader} from '@angular/compiler';
import {clearResolutionOfComponentResourcesQueue, resolveComponentResources} from '../../src/metadata/resource_loading';
import {ComponentFixture} from './component_fixture';
import {MetadataOverride} from './metadata_override';
import {ComponentResolver, DirectiveResolver, NgModuleResolver, PipeResolver, Resolver} from './resolvers';
@ -229,6 +234,7 @@ export class TestBedRender3 implements Injector, TestBed {
private _directiveOverrides: [Type<any>, MetadataOverride<Directive>][] = [];
private _pipeOverrides: [Type<any>, MetadataOverride<Pipe>][] = [];
private _providerOverrides: Provider[] = [];
private _compilerProviders: StaticProvider[] = [];
private _rootProviderOverrides: Provider[] = [];
private _providerOverridesByToken: Map<any, Provider[]> = new Map();
private _templateOverrides: Map<Type<any>, string> = new Map();
@ -236,12 +242,14 @@ export class TestBedRender3 implements Injector, TestBed {
// test module configuration
private _providers: Provider[] = [];
private _compilerOptions: CompilerOptions[] = [];
private _declarations: Array<Type<any>|any[]|any> = [];
private _imports: Array<Type<any>|any[]|any> = [];
private _schemas: Array<SchemaMetadata|any[]> = [];
private _activeFixtures: ComponentFixture<any>[] = [];
private _compilerInjector: Injector = null !;
private _moduleRef: NgModuleRef<any> = null !;
private _testModuleType: NgModuleType<any> = null !;
@ -302,12 +310,14 @@ export class TestBedRender3 implements Injector, TestBed {
// reset test module config
this._providers = [];
this._compilerOptions = [];
this._declarations = [];
this._imports = [];
this._schemas = [];
this._moduleRef = null !;
this._testModuleType = null !;
this._compilerInjector = null !;
this._instantiated = false;
this._activeFixtures.forEach((fixture) => {
try {
@ -326,6 +336,7 @@ export class TestBedRender3 implements Injector, TestBed {
Object.defineProperty(type, value[0], value[1]);
});
this._initiaNgDefs.clear();
clearResolutionOfComponentResourcesQueue();
}
configureCompiler(config: {providers?: any[]; useJit?: boolean;}): void {
@ -335,6 +346,7 @@ export class TestBedRender3 implements Injector, TestBed {
if (config.providers) {
this._providerOverrides.push(...config.providers);
this._compilerProviders.push(...config.providers);
}
}
@ -355,9 +367,37 @@ export class TestBedRender3 implements Injector, TestBed {
}
compileComponents(): Promise<any> {
// assume for now that components don't use templateUrl / stylesUrl to unblock further testing
// TODO(pk): plug into the ivy's resource fetching pipeline
return Promise.resolve();
const resolvers = this._getResolvers();
const declarations: Type<any>[] = flatten(this._declarations || EMPTY_ARRAY, resolveForwardRef);
const componentOverrides: [Type<any>, Component][] = [];
// Compile the components declared by this module
declarations.forEach(declaration => {
const component = resolvers.component.resolve(declaration);
if (component) {
// We make a copy of the metadata to ensure that we don't mutate the original metadata
const metadata = {...component};
compileComponent(declaration, metadata);
componentOverrides.push([declaration, metadata]);
}
});
let resourceLoader: ResourceLoader;
return resolveComponentResources(url => {
if (!resourceLoader) {
resourceLoader = this.compilerInjector.get(ResourceLoader);
}
return Promise.resolve(resourceLoader.get(url));
})
.then(() => {
componentOverrides.forEach((override: [Type<any>, Component]) => {
// Once resolved, we override the existing metadata, ensuring that the resolved
// resources
// are only available until the next TestBed reset (when `resetTestingModule` is called)
this.overrideComponent(override[0], {set: override[1]});
});
});
}
get(token: any, notFoundValue: any = Injector.THROW_IF_NOT_FOUND): any {
@ -549,6 +589,30 @@ export class TestBedRender3 implements Injector, TestBed {
return DynamicTestModule as NgModuleType;
}
get compilerInjector(): Injector {
if (this._compilerInjector !== undefined) {
this._compilerInjector;
}
const providers: StaticProvider[] = [];
const compilerOptions = this.platform.injector.get(COMPILER_OPTIONS);
compilerOptions.forEach(opts => {
if (opts.providers) {
providers.push(opts.providers);
}
});
providers.push(...this._compilerProviders);
// TODO(ocombe): make this work with an Injector directly instead of creating a module for it
@NgModule({providers})
class CompilerModule {
}
const CompilerModuleFactory = new R3NgModuleFactory(CompilerModule);
this._compilerInjector = CompilerModuleFactory.create(this.platform.injector).injector;
return this._compilerInjector;
}
private _getMetaWithOverrides(meta: Component|Directive|NgModule, type?: Type<any>) {
const overrides: {providers?: any[], template?: string} = {};
if (meta.providers && meta.providers.length) {
@ -659,19 +723,6 @@ 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 => {

View File

@ -161,7 +161,9 @@ function bootstrap(
afterEach(destroyPlatform);
// TODO(misko): can't use `fixmeIvy.it` because the `it` is somehow special here.
fixmeIvy('FW-553: TestBed is unaware of async compilation').isEnabled &&
fixmeIvy(
'FW-876: Bootstrap factory method should throw if bootstrapped Directive is not a Component')
.isEnabled &&
it('should throw if bootstrapped Directive is not a Component',
inject([AsyncTestCompleter], (done: AsyncTestCompleter) => {
const logger = new MockConsole();
@ -190,7 +192,8 @@ function bootstrap(
}));
// TODO(misko): can't use `fixmeIvy.it` because the `it` is somehow special here.
fixmeIvy('FW-553: TestBed is unaware of async compilation').isEnabled &&
fixmeIvy('FW-875: The source of the error is missing in the `StaticInjectorError` message')
.isEnabled &&
it('should throw if no provider',
inject([AsyncTestCompleter], (async: AsyncTestCompleter) => {
const logger = new MockConsole();

View File

@ -10,7 +10,7 @@ import {CompilerConfig, ResourceLoader} from '@angular/compiler';
import {CUSTOM_ELEMENTS_SCHEMA, Compiler, Component, Directive, Inject, Injectable, Injector, Input, NgModule, Optional, Pipe, SkipSelf, ɵstringify as stringify} from '@angular/core';
import {TestBed, async, fakeAsync, getTestBed, inject, tick, withModule} from '@angular/core/testing';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {fixmeIvy, obsoleteInIvy} from '@angular/private/testing';
import {fixmeIvy, ivyEnabled, obsoleteInIvy} from '@angular/private/testing';
// Services, and components for the tests.
@ -311,12 +311,11 @@ class CompWithUrlTemplate {
}));
isBrowser &&
fixmeIvy('FW-553: TestBed is unaware of async compilation')
.it('should allow to createSync components with templateUrl after explicit async compilation',
() => {
const fixture = TestBed.createComponent(CompWithUrlTemplate);
expect(fixture.nativeElement).toHaveText('from external template');
});
it('should allow to createSync components with templateUrl after explicit async compilation',
() => {
const fixture = TestBed.createComponent(CompWithUrlTemplate);
expect(fixture.nativeElement).toHaveText('from external template');
});
});
describe('overwriting metadata', () => {
@ -792,13 +791,12 @@ class CompWithUrlTemplate {
{providers: [{provide: ResourceLoader, useValue: {get: resourceLoaderGet}}]});
});
fixmeIvy('FW-553: TestBed is unaware of async compilation')
.it('should use set up providers', fakeAsync(() => {
TestBed.compileComponents();
tick();
const compFixture = TestBed.createComponent(CompWithUrlTemplate);
expect(compFixture.nativeElement).toHaveText('Hello world!');
}));
it('should use set up providers', fakeAsync(() => {
TestBed.compileComponents();
tick();
const compFixture = TestBed.createComponent(CompWithUrlTemplate);
expect(compFixture.nativeElement).toHaveText('Hello world!');
}));
});
describe('useJit true', () => {
@ -904,22 +902,25 @@ class CompWithUrlTemplate {
{providers: [{provide: ResourceLoader, useValue: {get: resourceLoaderGet}}]});
});
fixmeIvy('FW-553: TestBed is unaware of async compilation')
.it('should report an error for declared components with templateUrl which never call TestBed.compileComponents',
() => {
const itPromise = patchJasmineIt();
it('should report an error for declared components with templateUrl which never call TestBed.compileComponents',
() => {
const itPromise = patchJasmineIt();
expect(
() => it(
'should fail', withModule(
{declarations: [CompWithUrlTemplate]},
() => TestBed.createComponent(CompWithUrlTemplate))))
.toThrowError(
`This test module uses the component ${stringify(CompWithUrlTemplate)} which is using a "templateUrl" or "styleUrls", but they were never compiled. ` +
`Please call "TestBed.compileComponents" before your test.`);
expect(
() =>
it('should fail', withModule(
{declarations: [CompWithUrlTemplate]},
() => TestBed.createComponent(CompWithUrlTemplate))))
.toThrowError(
ivyEnabled ?
`Component 'CompWithUrlTemplate' is not resolved:
- templateUrl: /base/angular/packages/platform-browser/test/static_assets/test.html
Did you run and wait for 'resolveComponentResources()'?` :
`This test module uses the component ${stringify(CompWithUrlTemplate)} which is using a "templateUrl" or "styleUrls", but they were never compiled. ` +
`Please call "TestBed.compileComponents" before your test.`);
restoreJasmineIt();
});
restoreJasmineIt();
});
});
@ -1015,7 +1016,6 @@ class CompWithUrlTemplate {
});
it('should override component dependencies', async(() => {
const componentFixture = TestBed.createComponent(ParentComp);
componentFixture.detectChanges();
expect(componentFixture.nativeElement).toHaveText('Parent(Mock)');