fix(ivy): restore NG defs after running tests in TestBed (#27786)

`R3TestBed` allows consumers to configure a "testing module", declare components, override various metadata, etc. To do this, it implements its own JIT compilation, where components/directives/modules have Ivy definition fields generated based on testing metadata. It results in tests interfering with each other. One test might override something in a component that another test tries to use normally, causing failures.

In order to resolve this problem, we store current components/directives/modules defs before applying overrides and re-compiling. Once the test is complete, we restore initial defs, so the next tests interact with "clean" components.

PR Close #27786
This commit is contained in:
Andrew Kushnir 2018-12-20 17:45:53 -08:00 committed by Kara Erickson
parent e775313188
commit a75c734471
3 changed files with 77 additions and 2 deletions

View File

@ -162,6 +162,17 @@ export {
getLContext as ɵgetLContext
} from './render3/context_discovery';
export {
NG_ELEMENT_ID as ɵNG_ELEMENT_ID,
NG_COMPONENT_DEF as ɵNG_COMPONENT_DEF,
NG_DIRECTIVE_DEF as ɵNG_DIRECTIVE_DEF,
NG_INJECTABLE_DEF as ɵNG_INJECTABLE_DEF,
NG_INJECTOR_DEF as ɵNG_INJECTOR_DEF,
NG_PIPE_DEF as ɵNG_PIPE_DEF,
NG_MODULE_DEF as ɵNG_MODULE_DEF,
NG_BASE_DEF as ɵNG_BASE_DEF
} from './render3/fields';
export {
Player as ɵPlayer,
PlayerFactory as ɵPlayerFactory,

View File

@ -6,10 +6,11 @@
* found in the LICENSE file at https://angular.io/license
*/
import {Component, Inject, InjectionToken, NgModule, Optional} from '@angular/core';
import {Component, Directive, Inject, InjectionToken, NgModule, Optional, Pipe} from '@angular/core';
import {TestBed, getTestBed} from '@angular/core/testing/src/test_bed';
import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {onlyInIvy} from '@angular/private/testing';
const NAME = new InjectionToken<string>('name');
@ -172,4 +173,44 @@ describe('TestBed', () => {
hello.detectChanges();
expect(hello.nativeElement).toHaveText('Hello injected World !');
});
onlyInIvy('patched ng defs should be removed after resetting TestingModule')
.it('make sure we restore ng defs to their initial states', () => {
@Pipe({name: 'somePipe', pure: true})
class SomePipe {
transform(value: string): string { return `transformed ${value}`; }
}
@Directive({selector: 'someDirective'})
class SomeDirective {
someProp = 'hello';
}
@Component({selector: 'comp', template: 'someText'})
class SomeComponent {
}
@NgModule({declarations: [SomeComponent]})
class SomeModule {
}
TestBed.configureTestingModule({imports: [SomeModule]});
// adding Pipe and Directive via metadata override
TestBed.overrideModule(
SomeModule, {set: {declarations: [SomeComponent, SomePipe, SomeDirective]}});
TestBed.overrideComponent(
SomeComponent, {set: {template: `<span someDirective>{{'hello' | somePipe}}</span>`}});
TestBed.createComponent(SomeComponent);
const defBeforeReset = (SomeComponent as any).ngComponentDef;
expect(defBeforeReset.pipeDefs().length).toEqual(1);
expect(defBeforeReset.directiveDefs().length).toEqual(2); // directive + component
TestBed.resetTestingModule();
const defAfterReset = (SomeComponent as any).ngComponentDef;
expect(defAfterReset.pipeDefs().length).toEqual(0);
expect(defAfterReset.directiveDefs().length).toEqual(1); // component
});
});

View File

@ -6,7 +6,7 @@
* found in the LICENSE file at https://angular.io/license
*/
import {ApplicationInitStatus, Component, Directive, Injector, NgModule, NgZone, Pipe, PlatformRef, Provider, SchemaMetadata, Type, resolveForwardRef, ɵInjectableDef as InjectableDef, ɵNgModuleDef as NgModuleDef, ɵ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, ɵresetCompiledComponents as resetCompiledComponents, ɵstringify as stringify} from '@angular/core';
import {ApplicationInitStatus, Component, Directive, Injector, NgModule, NgZone, Pipe, PlatformRef, Provider, SchemaMetadata, Type, resolveForwardRef, ɵInjectableDef as InjectableDef, ɵNG_COMPONENT_DEF as NG_COMPONENT_DEF, ɵNG_DIRECTIVE_DEF as NG_DIRECTIVE_DEF, ɵNG_INJECTOR_DEF as NG_INJECTOR_DEF, ɵNG_MODULE_DEF as NG_MODULE_DEF, ɵNG_PIPE_DEF as NG_PIPE_DEF, ɵNgModuleDef as NgModuleDef, ɵ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, ɵresetCompiledComponents as resetCompiledComponents, ɵstringify as stringify} from '@angular/core';
import {ComponentFixture} from './component_fixture';
import {MetadataOverride} from './metadata_override';
@ -204,6 +204,11 @@ export class TestBedRender3 implements Injector, TestBed {
private _instantiated: boolean = false;
// Map that keeps initial version of component/directive/pipe defs in case
// we compile a Type again, thus overriding respective static fields. This is
// required to make sure we restore defs to their initial states between test runs
private _initiaNgDefs: Map<Type<any>, [string, PropertyDescriptor|undefined]> = new Map();
/**
* Initialize the environment for testing with a compiler factory, a PlatformRef, and an
* angular module. These are common to every test in the suite.
@ -269,6 +274,12 @@ export class TestBedRender3 implements Injector, TestBed {
}
});
this._activeFixtures = [];
// restore initial component/directive/pipe defs
this._initiaNgDefs.forEach((value: [string, PropertyDescriptor], type: Type<any>) => {
Object.defineProperty(type, value[0], value[1]);
});
this._initiaNgDefs.clear();
}
configureCompiler(config: {providers?: any[]; useJit?: boolean;}): void {
@ -422,6 +433,13 @@ export class TestBedRender3 implements Injector, TestBed {
this._instantiated = true;
}
private _storeNgDef(prop: string, type: Type<any>) {
if (!this._initiaNgDefs.has(type)) {
const currentDef = Object.getOwnPropertyDescriptor(type, prop);
this._initiaNgDefs.set(type, [prop, currentDef]);
}
}
// get overrides for a specific provider (if any)
private _getProviderOverrides(provider: any) {
const token = typeof provider === 'object' && provider.hasOwnProperty('provide') ?
@ -503,6 +521,8 @@ export class TestBedRender3 implements Injector, TestBed {
throw new Error(`${stringify(moduleType)} has not @NgModule annotation`);
}
this._storeNgDef(NG_MODULE_DEF, moduleType);
this._storeNgDef(NG_INJECTOR_DEF, moduleType);
const metadata = this._getMetaWithOverrides(ngModule);
compileNgModuleDefs(moduleType, metadata);
@ -514,6 +534,7 @@ export class TestBedRender3 implements Injector, TestBed {
declarations.forEach(declaration => {
const component = resolvers.component.resolve(declaration);
if (component) {
this._storeNgDef(NG_COMPONENT_DEF, declaration);
const metadata = this._getMetaWithOverrides(component, declaration);
compileComponent(declaration, metadata);
compiledComponents.push(declaration);
@ -522,6 +543,7 @@ export class TestBedRender3 implements Injector, TestBed {
const directive = resolvers.directive.resolve(declaration);
if (directive) {
this._storeNgDef(NG_DIRECTIVE_DEF, declaration);
const metadata = this._getMetaWithOverrides(directive);
compileDirective(declaration, metadata);
return;
@ -529,6 +551,7 @@ export class TestBedRender3 implements Injector, TestBed {
const pipe = resolvers.pipe.resolve(declaration);
if (pipe) {
this._storeNgDef(NG_PIPE_DEF, declaration);
compilePipe(declaration, pipe);
return;
}