2018-08-06 17:09:38 -04:00
|
|
|
/**
|
|
|
|
* @license
|
|
|
|
* Copyright Google Inc. All Rights Reserved.
|
|
|
|
*
|
|
|
|
* Use of this source code is governed by an MIT-style license that can be
|
|
|
|
* found in the LICENSE file at https://angular.io/license
|
|
|
|
*/
|
|
|
|
|
fix(ivy): fix proliferation of provider overrides for modules (#29571)
When an @NgModule is imported more than once in the testing module (for
example it appears in the imports of more than one module, or if it's
literally listed multiple times), then TestBed had a bug where the
providers for the module would be overridden many times.
This alone was problematic but would not break tests. However, the original
value of the providers field of the ngInjectorDef was saved each time, and
restored in the same order. Thus, if the provider array was [X], and
overrides were applied twice, then the override array would become
[X, X'] and then [X, X', X, X']. However, on the second override the state
[X, X'] would be stored as original. The array would then be restored to
[X] and then [X, X'].
Each test, therefore, would continue to double the size of the providers
array for the module, eventually exhausting the browser's memory.
This commit adds a Set to track when overrides have been applied to a module
and refrain from applying them more than once.
PR Close #29571
2019-03-28 13:48:29 -04:00
|
|
|
import {Component, Directive, ErrorHandler, Inject, Injectable, InjectionToken, NgModule, Optional, Pipe, ɵdefineComponent as defineComponent, ɵsetClassMetadata as setClassMetadata, ɵtext as text} from '@angular/core';
|
2018-08-06 17:09:38 -04:00
|
|
|
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';
|
2018-12-20 20:45:53 -05:00
|
|
|
import {onlyInIvy} from '@angular/private/testing';
|
2018-08-06 17:09:38 -04:00
|
|
|
|
|
|
|
const NAME = new InjectionToken<string>('name');
|
|
|
|
|
|
|
|
// -- module: HWModule
|
|
|
|
@Component({
|
|
|
|
selector: 'hello-world',
|
|
|
|
template: '<greeting-cmp></greeting-cmp>',
|
|
|
|
})
|
|
|
|
export class HelloWorld {
|
|
|
|
}
|
|
|
|
|
|
|
|
// -- module: Greeting
|
|
|
|
@Component({
|
|
|
|
selector: 'greeting-cmp',
|
|
|
|
template: 'Hello {{ name }}',
|
|
|
|
})
|
|
|
|
export class GreetingCmp {
|
|
|
|
name: string;
|
|
|
|
|
|
|
|
constructor(@Inject(NAME) @Optional() name: string) { this.name = name || 'nobody!'; }
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [GreetingCmp],
|
|
|
|
exports: [GreetingCmp],
|
|
|
|
})
|
|
|
|
export class GreetingModule {
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({selector: 'simple-cmp', template: '<b>simple</b>'})
|
|
|
|
export class SimpleCmp {
|
|
|
|
}
|
|
|
|
|
2018-09-24 04:30:29 -04:00
|
|
|
@Component({selector: 'with-refs-cmp', template: '<div #firstDiv></div>'})
|
|
|
|
export class WithRefsCmp {
|
|
|
|
}
|
|
|
|
|
2019-01-16 19:28:04 -05:00
|
|
|
@Component({selector: 'inherited-cmp', template: 'inherited'})
|
|
|
|
export class InheritedCmp extends SimpleCmp {
|
|
|
|
}
|
|
|
|
|
2019-01-24 11:53:00 -05:00
|
|
|
@Directive({selector: '[hostBindingDir]', host: {'[id]': 'id'}})
|
2019-01-22 18:02:52 -05:00
|
|
|
export class HostBindingDir {
|
|
|
|
id = 'one';
|
|
|
|
}
|
|
|
|
|
2019-01-24 11:53:00 -05:00
|
|
|
@Component({
|
|
|
|
selector: 'component-with-prop-bindings',
|
|
|
|
template: `
|
|
|
|
<div hostBindingDir [title]="title" [attr.aria-label]="label"></div>
|
|
|
|
<p title="( {{ label }} - {{ title }} )" [attr.aria-label]="label" id="[ {{ label }} ] [ {{ title }} ]">
|
|
|
|
</p>
|
|
|
|
`
|
|
|
|
})
|
|
|
|
export class ComponentWithPropBindings {
|
|
|
|
title = 'some title';
|
|
|
|
label = 'some label';
|
2019-01-22 18:02:52 -05:00
|
|
|
}
|
|
|
|
|
2019-01-16 19:28:04 -05:00
|
|
|
@Component({
|
|
|
|
selector: 'simple-app',
|
|
|
|
template: `
|
|
|
|
<simple-cmp></simple-cmp> - <inherited-cmp></inherited-cmp>
|
|
|
|
`
|
|
|
|
})
|
|
|
|
export class SimpleApp {
|
|
|
|
}
|
|
|
|
|
2019-01-23 18:09:27 -05:00
|
|
|
@Component({selector: 'inline-template', template: '<p>Hello</p>'})
|
|
|
|
export class ComponentWithInlineTemplate {
|
|
|
|
}
|
|
|
|
|
2018-08-06 17:09:38 -04:00
|
|
|
@NgModule({
|
2019-01-22 18:02:52 -05:00
|
|
|
declarations: [
|
2019-01-24 11:53:00 -05:00
|
|
|
HelloWorld, SimpleCmp, WithRefsCmp, InheritedCmp, SimpleApp, ComponentWithPropBindings,
|
|
|
|
HostBindingDir
|
2019-01-22 18:02:52 -05:00
|
|
|
],
|
2018-08-06 17:09:38 -04:00
|
|
|
imports: [GreetingModule],
|
|
|
|
providers: [
|
|
|
|
{provide: NAME, useValue: 'World!'},
|
|
|
|
]
|
|
|
|
})
|
|
|
|
export class HelloWorldModule {
|
|
|
|
}
|
|
|
|
|
|
|
|
describe('TestBed', () => {
|
|
|
|
beforeEach(() => {
|
|
|
|
getTestBed().resetTestingModule();
|
|
|
|
TestBed.configureTestingModule({imports: [HelloWorldModule]});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should compile and render a component', () => {
|
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
|
|
|
|
expect(hello.nativeElement).toHaveText('Hello World!');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should give access to the component instance', () => {
|
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
|
|
|
|
expect(hello.componentInstance).toBeAnInstanceOf(HelloWorld);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should give the ability to query by css', () => {
|
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
|
|
|
|
const greetingByCss = hello.debugElement.query(By.css('greeting-cmp'));
|
|
|
|
expect(greetingByCss.nativeElement).toHaveText('Hello World!');
|
|
|
|
expect(greetingByCss.componentInstance).toBeAnInstanceOf(GreetingCmp);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should give the ability to trigger the change detection', () => {
|
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
|
|
|
|
hello.detectChanges();
|
|
|
|
const greetingByCss = hello.debugElement.query(By.css('greeting-cmp'));
|
|
|
|
expect(greetingByCss.nativeElement).toHaveText('Hello World!');
|
|
|
|
|
|
|
|
greetingByCss.componentInstance.name = 'TestBed!';
|
|
|
|
hello.detectChanges();
|
|
|
|
expect(greetingByCss.nativeElement).toHaveText('Hello TestBed!');
|
|
|
|
});
|
|
|
|
|
2019-01-24 11:53:00 -05:00
|
|
|
it('should give the ability to access property bindings on a node', () => {
|
|
|
|
const fixture = TestBed.createComponent(ComponentWithPropBindings);
|
|
|
|
fixture.detectChanges();
|
|
|
|
|
|
|
|
const divElement = fixture.debugElement.query(By.css('div'));
|
|
|
|
expect(divElement.properties).toEqual({id: 'one', title: 'some title'});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should give the ability to access interpolated properties on a node', () => {
|
|
|
|
const fixture = TestBed.createComponent(ComponentWithPropBindings);
|
2019-01-22 18:02:52 -05:00
|
|
|
fixture.detectChanges();
|
|
|
|
|
2019-01-24 11:53:00 -05:00
|
|
|
const paragraphEl = fixture.debugElement.query(By.css('p'));
|
|
|
|
expect(paragraphEl.properties)
|
|
|
|
.toEqual({title: '( some label - some title )', id: '[ some label ] [ some title ]'});
|
2019-01-22 18:02:52 -05:00
|
|
|
});
|
|
|
|
|
2018-08-06 17:09:38 -04:00
|
|
|
it('should give access to the node injector', () => {
|
2018-09-24 04:30:29 -04:00
|
|
|
const fixture = TestBed.createComponent(HelloWorld);
|
|
|
|
fixture.detectChanges();
|
|
|
|
const injector = fixture.debugElement.query(By.css('greeting-cmp')).injector;
|
|
|
|
|
|
|
|
// from the node injector
|
|
|
|
const greetingCmp = injector.get(GreetingCmp);
|
|
|
|
expect(greetingCmp.constructor).toBe(GreetingCmp);
|
|
|
|
|
|
|
|
// from the node injector (inherited from a parent node)
|
|
|
|
const helloWorldCmp = injector.get(HelloWorld);
|
|
|
|
expect(fixture.componentInstance).toBe(helloWorldCmp);
|
|
|
|
|
|
|
|
const nameInjected = injector.get(NAME);
|
|
|
|
expect(nameInjected).toEqual('World!');
|
|
|
|
});
|
|
|
|
|
2018-11-30 11:14:53 -05:00
|
|
|
it('should give access to the node injector for root node', () => {
|
2018-08-06 17:09:38 -04:00
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
2018-09-24 04:30:29 -04:00
|
|
|
const injector = hello.debugElement.injector;
|
2018-08-06 17:09:38 -04:00
|
|
|
|
|
|
|
// from the node injector
|
|
|
|
const helloInjected = injector.get(HelloWorld);
|
|
|
|
expect(helloInjected).toBe(hello.componentInstance);
|
|
|
|
|
|
|
|
// from the module injector
|
|
|
|
const nameInjected = injector.get(NAME);
|
|
|
|
expect(nameInjected).toEqual('World!');
|
|
|
|
});
|
|
|
|
|
2018-09-24 04:30:29 -04:00
|
|
|
it('should give access to local refs on a node', () => {
|
|
|
|
const withRefsCmp = TestBed.createComponent(WithRefsCmp);
|
|
|
|
const firstDivDebugEl = withRefsCmp.debugElement.query(By.css('div'));
|
|
|
|
// assert that a native element is referenced by a local ref
|
|
|
|
expect(firstDivDebugEl.references.firstDiv.tagName.toLowerCase()).toBe('div');
|
|
|
|
});
|
|
|
|
|
2018-08-06 17:09:38 -04:00
|
|
|
it('should give the ability to query by directive', () => {
|
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
|
|
|
|
const greetingByDirective = hello.debugElement.query(By.directive(GreetingCmp));
|
|
|
|
expect(greetingByDirective.componentInstance).toBeAnInstanceOf(GreetingCmp);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('allow to override a template', () => {
|
|
|
|
// use original template when there is no override
|
|
|
|
let hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
expect(hello.nativeElement).toHaveText('Hello World!');
|
|
|
|
|
|
|
|
// override the template
|
|
|
|
getTestBed().resetTestingModule();
|
|
|
|
TestBed.configureTestingModule({imports: [HelloWorldModule]});
|
|
|
|
TestBed.overrideComponent(GreetingCmp, {set: {template: `Bonjour {{ name }}`}});
|
|
|
|
hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
expect(hello.nativeElement).toHaveText('Bonjour World!');
|
|
|
|
|
|
|
|
// restore the original template by calling `.resetTestingModule()`
|
|
|
|
getTestBed().resetTestingModule();
|
|
|
|
TestBed.configureTestingModule({imports: [HelloWorldModule]});
|
|
|
|
hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
expect(hello.nativeElement).toHaveText('Hello World!');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('allow to override a provider', () => {
|
|
|
|
TestBed.overrideProvider(NAME, {useValue: 'injected World !'});
|
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
expect(hello.nativeElement).toHaveText('Hello injected World !');
|
|
|
|
});
|
2018-12-20 20:45:53 -05:00
|
|
|
|
2019-01-16 19:28:04 -05:00
|
|
|
it('should resolve components that are extended by other components', () => {
|
|
|
|
// SimpleApp uses SimpleCmp in its template, which is extended by InheritedCmp
|
|
|
|
const simpleApp = TestBed.createComponent(SimpleApp);
|
|
|
|
simpleApp.detectChanges();
|
|
|
|
expect(simpleApp.nativeElement).toHaveText('simple - inherited');
|
|
|
|
});
|
|
|
|
|
2019-01-23 18:09:27 -05:00
|
|
|
it('should resolve components without async resources synchronously', (done) => {
|
|
|
|
TestBed
|
|
|
|
.configureTestingModule({
|
|
|
|
declarations: [ComponentWithInlineTemplate],
|
|
|
|
})
|
|
|
|
.compileComponents()
|
|
|
|
.then(done)
|
|
|
|
.catch(error => {
|
|
|
|
// This should not throw any errors. If an error is thrown, the test will fail.
|
|
|
|
// Specifically use `catch` here to mark the test as done and *then* throw the error
|
|
|
|
// so that the test isn't treated as a timeout.
|
|
|
|
done();
|
|
|
|
throw error;
|
|
|
|
});
|
|
|
|
|
|
|
|
// Intentionally call `createComponent` before `compileComponents` is resolved. We want this to
|
|
|
|
// work for components that don't have any async resources (templateUrl, styleUrls).
|
|
|
|
TestBed.createComponent(ComponentWithInlineTemplate);
|
|
|
|
});
|
|
|
|
|
2019-03-22 17:18:09 -04:00
|
|
|
it('should be able to override the ErrorHandler via an import', () => {
|
|
|
|
class CustomErrorHandler {}
|
|
|
|
|
|
|
|
@NgModule({providers: [{provide: ErrorHandler, useClass: CustomErrorHandler}]})
|
|
|
|
class ProvidesErrorHandler {
|
|
|
|
}
|
|
|
|
|
|
|
|
getTestBed().resetTestingModule();
|
|
|
|
TestBed.configureTestingModule({imports: [ProvidesErrorHandler, HelloWorldModule]});
|
|
|
|
|
|
|
|
expect(TestBed.get(ErrorHandler)).toEqual(jasmine.any(CustomErrorHandler));
|
|
|
|
});
|
|
|
|
|
2019-03-27 17:56:08 -04:00
|
|
|
onlyInIvy('TestBed should handle AOT pre-compiled Components')
|
|
|
|
.describe('AOT pre-compiled components', () => {
|
|
|
|
/**
|
|
|
|
* Function returns a class that represents AOT-compiled version of the following Component:
|
|
|
|
*
|
|
|
|
* @Component({
|
|
|
|
* selector: 'comp',
|
|
|
|
* templateUrl: './template.ng.html',
|
|
|
|
* styleUrls: ['./style.css']
|
|
|
|
* })
|
|
|
|
* class ComponentClass {}
|
|
|
|
*
|
|
|
|
* This is needed to closer match the behavior of AOT pre-compiled components (compiled
|
|
|
|
* outside of TestBed) without changing TestBed state and/or Component metadata to compile
|
|
|
|
* them via TestBed with external resources.
|
|
|
|
*/
|
|
|
|
const getAOTCompiledComponent = () => {
|
|
|
|
class ComponentClass {
|
|
|
|
static ngComponentDef = defineComponent({
|
|
|
|
type: ComponentClass,
|
|
|
|
selectors: [['comp']],
|
|
|
|
factory: () => new ComponentClass(),
|
|
|
|
consts: 1,
|
|
|
|
vars: 0,
|
|
|
|
template: (rf: any, ctx: any) => {
|
|
|
|
if (rf & 1) {
|
|
|
|
text(0, 'Some template');
|
|
|
|
}
|
|
|
|
},
|
|
|
|
styles: ['body { margin: 0; }']
|
|
|
|
});
|
|
|
|
}
|
|
|
|
setClassMetadata(
|
|
|
|
ComponentClass, [{
|
|
|
|
type: Component,
|
|
|
|
args: [{
|
|
|
|
selector: 'comp',
|
|
|
|
templateUrl: './template.ng.html',
|
|
|
|
styleUrls: ['./style.css'],
|
|
|
|
}]
|
|
|
|
}],
|
|
|
|
null, null);
|
|
|
|
return ComponentClass;
|
|
|
|
};
|
|
|
|
|
|
|
|
it('should have an ability to override template', () => {
|
|
|
|
const SomeComponent = getAOTCompiledComponent();
|
|
|
|
TestBed.configureTestingModule({declarations: [SomeComponent]});
|
|
|
|
TestBed.overrideTemplateUsingTestingModule(SomeComponent, 'Template override');
|
|
|
|
const fixture = TestBed.createComponent(SomeComponent);
|
|
|
|
expect(fixture.nativeElement.innerHTML).toBe('Template override');
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2018-12-20 20:45:53 -05:00
|
|
|
onlyInIvy('patched ng defs should be removed after resetting TestingModule')
|
2019-01-29 20:13:02 -05:00
|
|
|
.describe('resetting ng defs', () => {
|
|
|
|
it('should 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;
|
2019-03-11 13:35:25 -04:00
|
|
|
expect(defAfterReset.pipeDefs).toBe(null);
|
|
|
|
expect(defAfterReset.directiveDefs).toBe(null);
|
2019-01-29 20:13:02 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
it('should cleanup ng defs for classes with no ng annotations (in case of inheritance)',
|
|
|
|
() => {
|
|
|
|
@Component({selector: 'someDirective', template: '...'})
|
|
|
|
class SomeComponent {
|
|
|
|
}
|
|
|
|
|
|
|
|
class ComponentWithNoAnnotations extends SomeComponent {}
|
|
|
|
|
2019-03-20 20:58:20 -04:00
|
|
|
@Directive({selector: 'some-directive'})
|
|
|
|
class SomeDirective {
|
|
|
|
}
|
|
|
|
|
|
|
|
class DirectiveWithNoAnnotations extends SomeDirective {}
|
|
|
|
|
|
|
|
@Pipe({name: 'some-pipe'})
|
|
|
|
class SomePipe {
|
|
|
|
}
|
|
|
|
|
|
|
|
class PipeWithNoAnnotations extends SomePipe {}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
declarations: [
|
|
|
|
ComponentWithNoAnnotations, DirectiveWithNoAnnotations, PipeWithNoAnnotations
|
|
|
|
]
|
|
|
|
});
|
2019-01-29 20:13:02 -05:00
|
|
|
TestBed.createComponent(ComponentWithNoAnnotations);
|
|
|
|
|
|
|
|
expect(ComponentWithNoAnnotations.hasOwnProperty('ngComponentDef')).toBeTruthy();
|
|
|
|
expect(SomeComponent.hasOwnProperty('ngComponentDef')).toBeTruthy();
|
|
|
|
|
2019-03-20 20:58:20 -04:00
|
|
|
expect(DirectiveWithNoAnnotations.hasOwnProperty('ngDirectiveDef')).toBeTruthy();
|
|
|
|
expect(SomeDirective.hasOwnProperty('ngDirectiveDef')).toBeTruthy();
|
|
|
|
|
|
|
|
expect(PipeWithNoAnnotations.hasOwnProperty('ngPipeDef')).toBeTruthy();
|
|
|
|
expect(SomePipe.hasOwnProperty('ngPipeDef')).toBeTruthy();
|
|
|
|
|
2019-01-29 20:13:02 -05:00
|
|
|
TestBed.resetTestingModule();
|
|
|
|
|
2019-03-20 20:58:20 -04:00
|
|
|
// ng defs should be removed from classes with no annotations
|
2019-01-29 20:13:02 -05:00
|
|
|
expect(ComponentWithNoAnnotations.hasOwnProperty('ngComponentDef')).toBeFalsy();
|
2019-03-20 20:58:20 -04:00
|
|
|
expect(DirectiveWithNoAnnotations.hasOwnProperty('ngDirectiveDef')).toBeFalsy();
|
|
|
|
expect(PipeWithNoAnnotations.hasOwnProperty('ngPipeDef')).toBeFalsy();
|
2019-01-29 20:13:02 -05:00
|
|
|
|
2019-03-20 20:58:20 -04:00
|
|
|
// ng defs should be preserved on super types
|
2019-01-29 20:13:02 -05:00
|
|
|
expect(SomeComponent.hasOwnProperty('ngComponentDef')).toBeTruthy();
|
2019-03-20 20:58:20 -04:00
|
|
|
expect(SomeDirective.hasOwnProperty('ngDirectiveDef')).toBeTruthy();
|
|
|
|
expect(SomePipe.hasOwnProperty('ngPipeDef')).toBeTruthy();
|
2019-01-29 20:13:02 -05:00
|
|
|
});
|
fix(ivy): fix proliferation of provider overrides for modules (#29571)
When an @NgModule is imported more than once in the testing module (for
example it appears in the imports of more than one module, or if it's
literally listed multiple times), then TestBed had a bug where the
providers for the module would be overridden many times.
This alone was problematic but would not break tests. However, the original
value of the providers field of the ngInjectorDef was saved each time, and
restored in the same order. Thus, if the provider array was [X], and
overrides were applied twice, then the override array would become
[X, X'] and then [X, X', X, X']. However, on the second override the state
[X, X'] would be stored as original. The array would then be restored to
[X] and then [X, X'].
Each test, therefore, would continue to double the size of the providers
array for the module, eventually exhausting the browser's memory.
This commit adds a Set to track when overrides have been applied to a module
and refrain from applying them more than once.
PR Close #29571
2019-03-28 13:48:29 -04:00
|
|
|
|
|
|
|
it('should clean up overridden providers for modules that are imported more than once',
|
|
|
|
() => {
|
|
|
|
|
|
|
|
@Injectable()
|
|
|
|
class Token {
|
|
|
|
name: string = 'real';
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
providers: [Token],
|
|
|
|
})
|
|
|
|
class Module {
|
|
|
|
}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({imports: [Module, Module]});
|
|
|
|
TestBed.overrideProvider(Token, {useValue: {name: 'fake'}});
|
|
|
|
|
|
|
|
expect(TestBed.get(Token).name).toEqual('fake');
|
|
|
|
|
|
|
|
TestBed.resetTestingModule();
|
|
|
|
|
|
|
|
// The providers for the module should have been restored to the original array, with
|
|
|
|
// no trace of the overridden providers.
|
|
|
|
expect((Module as any).ngInjectorDef.providers).toEqual([Token]);
|
|
|
|
});
|
2018-12-20 20:45:53 -05:00
|
|
|
});
|
2018-08-06 17:09:38 -04:00
|
|
|
});
|