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
|
|
|
|
*/
|
|
|
|
|
2019-11-08 13:16:40 -05:00
|
|
|
<<<<<<< HEAD
|
2019-11-05 17:33:46 -05:00
|
|
|
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';
|
2019-10-01 18:02:59 -04:00
|
|
|
import {registerModuleFactory} from '@angular/core/src/linker/ng_module_factory_registration';
|
|
|
|
import {NgModuleFactory} from '@angular/core/src/render3';
|
2019-11-08 13:16:40 -05:00
|
|
|
=======
|
|
|
|
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';
|
|
|
|
>>>>>>> parent of 63256b511a... fix(ivy): Only restore registered modules if user compiles modules with TestBed (#32944)
|
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');
|
|
|
|
|
2019-11-08 19:27:53 -05:00
|
|
|
@Injectable({providedIn: 'root'})
|
|
|
|
class SimpleService {
|
|
|
|
static ngOnDestroyCalls: number = 0;
|
|
|
|
id: number = 1;
|
|
|
|
ngOnDestroy() { SimpleService.ngOnDestroyCalls++; }
|
|
|
|
}
|
|
|
|
|
2018-08-06 17:09:38 -04:00
|
|
|
// -- 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;
|
|
|
|
|
2019-11-08 19:27:53 -05:00
|
|
|
constructor(
|
|
|
|
@Inject(NAME) @Optional() name: string,
|
|
|
|
@Inject(SimpleService) @Optional() service: SimpleService) {
|
|
|
|
this.name = name || 'nobody!';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'cmp-with-providers',
|
|
|
|
template: '<hello-world></hello-world>',
|
|
|
|
providers: [
|
|
|
|
SimpleService, //
|
|
|
|
{provide: NAME, useValue: `from Component`}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
class CmpWithProviders {
|
2018-08-06 17:09:38 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
@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,
|
2019-11-08 19:27:53 -05:00
|
|
|
HostBindingDir, CmpWithProviders
|
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', () => {
|
2019-10-11 12:43:37 -04:00
|
|
|
TestBed.overrideProvider(NAME, {useValue: 'injected World!'});
|
2018-08-06 17:09:38 -04:00
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
2019-10-11 12:43:37 -04:00
|
|
|
expect(hello.nativeElement).toHaveText('Hello injected World!');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('uses the most recent provider override', () => {
|
|
|
|
TestBed.overrideProvider(NAME, {useValue: 'injected World!'});
|
|
|
|
TestBed.overrideProvider(NAME, {useValue: 'injected World a second time!'});
|
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
expect(hello.nativeElement).toHaveText('Hello injected World a second time!');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('overrides a providers in an array', () => {
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
imports: [HelloWorldModule],
|
|
|
|
providers: [
|
|
|
|
[{provide: NAME, useValue: 'injected World!'}],
|
|
|
|
]
|
|
|
|
});
|
|
|
|
TestBed.overrideProvider(NAME, {useValue: 'injected World a second time!'});
|
|
|
|
const hello = TestBed.createComponent(HelloWorld);
|
|
|
|
hello.detectChanges();
|
|
|
|
expect(hello.nativeElement).toHaveText('Hello injected World a second time!');
|
|
|
|
});
|
|
|
|
|
2019-11-08 19:27:53 -05:00
|
|
|
it('should not call ngOnDestroy for a service that was overridden', () => {
|
|
|
|
SimpleService.ngOnDestroyCalls = 0;
|
|
|
|
|
|
|
|
TestBed.overrideProvider(SimpleService, {useValue: {id: 2, ngOnDestroy: () => {}}});
|
|
|
|
const fixture = TestBed.createComponent(CmpWithProviders);
|
|
|
|
fixture.detectChanges();
|
|
|
|
|
|
|
|
const service = TestBed.inject(SimpleService);
|
|
|
|
expect(service.id).toBe(2);
|
|
|
|
|
|
|
|
fixture.destroy();
|
|
|
|
|
|
|
|
// verify that original `ngOnDestroy` was not called
|
|
|
|
expect(SimpleService.ngOnDestroyCalls).toBe(0);
|
|
|
|
});
|
|
|
|
|
2019-10-11 12:43:37 -04:00
|
|
|
describe('multi providers', () => {
|
|
|
|
const multiToken = new InjectionToken<string[]>('multiToken');
|
|
|
|
const singleToken = new InjectionToken<string>('singleToken');
|
|
|
|
@NgModule({providers: [{provide: multiToken, useValue: 'valueFromModule', multi: true}]})
|
|
|
|
class MyModule {
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
providers: [
|
|
|
|
{provide: singleToken, useValue: 't1'},
|
|
|
|
{provide: multiToken, useValue: 'valueFromModule2', multi: true},
|
|
|
|
{provide: multiToken, useValue: 'secondValueFromModule2', multi: true}
|
|
|
|
]
|
|
|
|
})
|
|
|
|
class MyModule2 {
|
|
|
|
}
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
imports: [MyModule, MyModule2],
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
it('is preserved when other provider is overridden', () => {
|
|
|
|
TestBed.overrideProvider(singleToken, {useValue: ''});
|
|
|
|
const value = TestBed.inject(multiToken);
|
|
|
|
expect(value.length).toEqual(3);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('overridden with an array', () => {
|
|
|
|
const overrideValue = ['override'];
|
|
|
|
TestBed.overrideProvider(multiToken, { useValue: overrideValue, multi: true } as any);
|
|
|
|
|
|
|
|
const value = TestBed.inject(multiToken);
|
|
|
|
expect(value.length).toEqual(overrideValue.length);
|
|
|
|
expect(value).toEqual(overrideValue);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('overridden with a non-array', () => {
|
|
|
|
// This is actually invalid because multi providers return arrays. We have this here so we can
|
|
|
|
// ensure Ivy behaves the same as VE does currently.
|
|
|
|
const overrideValue = 'override';
|
|
|
|
TestBed.overrideProvider(multiToken, { useValue: overrideValue, multi: true } as any);
|
|
|
|
|
|
|
|
const value = TestBed.inject(multiToken);
|
|
|
|
expect(value.length).toEqual(overrideValue.length);
|
|
|
|
expect(value).toEqual(overrideValue as {} as string[]);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('overrides providers in ModuleWithProviders', () => {
|
|
|
|
const TOKEN = new InjectionToken<string[]>('token');
|
|
|
|
@NgModule()
|
|
|
|
class MyMod {
|
|
|
|
static multi = false;
|
|
|
|
|
|
|
|
static forRoot() {
|
|
|
|
return {
|
|
|
|
ngModule: MyMod,
|
|
|
|
providers: [{provide: TOKEN, multi: MyMod.multi, useValue: 'forRootValue'}]
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
beforeEach(() => MyMod.multi = true);
|
|
|
|
|
|
|
|
it('when provider is a "regular" provider', () => {
|
|
|
|
MyMod.multi = false;
|
|
|
|
@NgModule({imports: [MyMod.forRoot()]})
|
|
|
|
class MyMod2 {
|
|
|
|
}
|
|
|
|
TestBed.configureTestingModule({imports: [MyMod2]});
|
|
|
|
TestBed.overrideProvider(TOKEN, {useValue: ['override']});
|
|
|
|
expect(TestBed.inject(TOKEN)).toEqual(['override']);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('when provider is multi', () => {
|
|
|
|
@NgModule({imports: [MyMod.forRoot()]})
|
|
|
|
class MyMod2 {
|
|
|
|
}
|
|
|
|
TestBed.configureTestingModule({imports: [MyMod2]});
|
|
|
|
TestBed.overrideProvider(TOKEN, {useValue: ['override']});
|
|
|
|
expect(TestBed.inject(TOKEN)).toEqual(['override']);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('restores the original value', () => {
|
|
|
|
@NgModule({imports: [MyMod.forRoot()]})
|
|
|
|
class MyMod2 {
|
|
|
|
}
|
|
|
|
TestBed.configureTestingModule({imports: [MyMod2]});
|
|
|
|
TestBed.overrideProvider(TOKEN, {useValue: ['override']});
|
|
|
|
expect(TestBed.inject(TOKEN)).toEqual(['override']);
|
|
|
|
|
|
|
|
TestBed.resetTestingModule();
|
|
|
|
TestBed.configureTestingModule({imports: [MyMod2]});
|
|
|
|
expect(TestBed.inject(TOKEN)).toEqual(['forRootValue']);
|
|
|
|
});
|
2018-08-06 17:09:38 -04:00
|
|
|
});
|
2018-12-20 20:45:53 -05:00
|
|
|
|
2019-07-03 19:38:45 -04:00
|
|
|
it('should allow overriding a provider defined via ModuleWithProviders (using TestBed.overrideProvider)',
|
|
|
|
() => {
|
|
|
|
const serviceOverride = {
|
|
|
|
get() { return 'override'; },
|
|
|
|
};
|
|
|
|
|
|
|
|
@Injectable({providedIn: 'root'})
|
|
|
|
class MyService {
|
|
|
|
get() { return 'original'; }
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({})
|
|
|
|
class MyModule {
|
|
|
|
static forRoot(): ModuleWithProviders<MyModule> {
|
|
|
|
return {
|
|
|
|
ngModule: MyModule,
|
|
|
|
providers: [MyService],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
TestBed.overrideProvider(MyService, {useValue: serviceOverride});
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
imports: [MyModule.forRoot()],
|
|
|
|
});
|
|
|
|
|
2019-08-28 19:22:36 -04:00
|
|
|
const service = TestBed.inject(MyService);
|
2019-07-03 19:38:45 -04:00
|
|
|
expect(service.get()).toEqual('override');
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should allow overriding a provider defined via ModuleWithProviders (using TestBed.configureTestingModule)',
|
|
|
|
() => {
|
|
|
|
const serviceOverride = {
|
|
|
|
get() { return 'override'; },
|
|
|
|
};
|
|
|
|
|
|
|
|
@Injectable({providedIn: 'root'})
|
|
|
|
class MyService {
|
|
|
|
get() { return 'original'; }
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({})
|
|
|
|
class MyModule {
|
|
|
|
static forRoot(): ModuleWithProviders<MyModule> {
|
|
|
|
return {
|
|
|
|
ngModule: MyModule,
|
|
|
|
providers: [MyService],
|
|
|
|
};
|
|
|
|
}
|
|
|
|
}
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
imports: [MyModule.forRoot()],
|
|
|
|
providers: [{provide: MyService, useValue: serviceOverride}],
|
|
|
|
});
|
|
|
|
|
2019-08-28 19:22:36 -04:00
|
|
|
const service = TestBed.inject(MyService);
|
2019-07-03 19:38:45 -04:00
|
|
|
expect(service.get()).toEqual('override');
|
|
|
|
});
|
|
|
|
|
2019-11-05 17:33:46 -05:00
|
|
|
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);
|
|
|
|
});
|
|
|
|
|
2019-04-15 20:15:40 -04:00
|
|
|
it('allow to override multi provider', () => {
|
|
|
|
const MY_TOKEN = new InjectionToken('MyProvider');
|
|
|
|
class MyProvider {}
|
|
|
|
|
|
|
|
@Component({selector: 'my-comp', template: ``})
|
|
|
|
class MyComp {
|
|
|
|
constructor(@Inject(MY_TOKEN) public myProviders: MyProvider[]) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
declarations: [MyComp],
|
|
|
|
providers: [{provide: MY_TOKEN, useValue: {value: 'old provider'}, multi: true}]
|
|
|
|
});
|
|
|
|
|
|
|
|
const multiOverride = {useValue: [{value: 'new provider'}], multi: true};
|
|
|
|
TestBed.overrideProvider(MY_TOKEN, multiOverride as any);
|
|
|
|
|
|
|
|
const fixture = TestBed.createComponent(MyComp);
|
|
|
|
expect(fixture.componentInstance.myProviders).toEqual([{value: 'new provider'}]);
|
|
|
|
});
|
|
|
|
|
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-04-18 20:52:04 -04:00
|
|
|
it('should not trigger change detection for ComponentA while calling TestBed.createComponent for ComponentB',
|
|
|
|
() => {
|
|
|
|
const log: string[] = [];
|
|
|
|
@Component({
|
|
|
|
selector: 'comp-a',
|
|
|
|
template: '...',
|
|
|
|
})
|
|
|
|
class CompA {
|
|
|
|
@Input() inputA: string = '';
|
|
|
|
ngOnInit() { log.push('CompA:ngOnInit', this.inputA); }
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'comp-b',
|
|
|
|
template: '...',
|
|
|
|
})
|
|
|
|
class CompB {
|
|
|
|
@Input() inputB: string = '';
|
|
|
|
ngOnInit() { log.push('CompB:ngOnInit', this.inputB); }
|
|
|
|
}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({declarations: [CompA, CompB]});
|
|
|
|
|
|
|
|
log.length = 0;
|
|
|
|
const appA = TestBed.createComponent(CompA);
|
|
|
|
appA.componentInstance.inputA = 'a';
|
|
|
|
appA.autoDetectChanges();
|
|
|
|
expect(log).toEqual(['CompA:ngOnInit', 'a']);
|
|
|
|
|
|
|
|
log.length = 0;
|
|
|
|
const appB = TestBed.createComponent(CompB);
|
|
|
|
appB.componentInstance.inputB = 'b';
|
|
|
|
appB.autoDetectChanges();
|
|
|
|
expect(log).toEqual(['CompB:ngOnInit', 'b']);
|
|
|
|
});
|
|
|
|
|
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]});
|
|
|
|
|
2019-08-28 19:22:36 -04:00
|
|
|
expect(TestBed.inject(ErrorHandler)).toEqual(jasmine.any(CustomErrorHandler));
|
2019-04-11 19:11:45 -04:00
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should throw errors in CD', () => {
|
|
|
|
@Component({selector: 'my-comp', template: ''})
|
|
|
|
class MyComp {
|
|
|
|
name !: {hello: string};
|
|
|
|
|
|
|
|
ngOnInit() {
|
|
|
|
// this should throw because this.name is undefined
|
|
|
|
this.name.hello = 'hello';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({declarations: [MyComp]});
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
const fixture = TestBed.createComponent(MyComp);
|
|
|
|
fixture.detectChanges();
|
|
|
|
}).toThrowError();
|
|
|
|
});
|
|
|
|
|
|
|
|
// TODO(FW-1245): properly fix issue where errors in listeners aren't thrown and don't cause
|
|
|
|
// tests to fail. This is an issue in both View Engine and Ivy, and may require a breaking
|
|
|
|
// change to completely fix (since simple re-throwing breaks handlers in ngrx, etc).
|
|
|
|
xit('should throw errors in listeners', () => {
|
|
|
|
|
|
|
|
@Component({selector: 'my-comp', template: '<button (click)="onClick()">Click me</button>'})
|
|
|
|
class MyComp {
|
|
|
|
name !: {hello: string};
|
|
|
|
|
|
|
|
onClick() {
|
|
|
|
// this should throw because this.name is undefined
|
|
|
|
this.name.hello = 'hello';
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({declarations: [MyComp]});
|
|
|
|
const fixture = TestBed.createComponent(MyComp);
|
|
|
|
fixture.detectChanges();
|
|
|
|
|
|
|
|
expect(() => {
|
|
|
|
const button = fixture.nativeElement.querySelector('button');
|
|
|
|
button.click();
|
|
|
|
}).toThrowError();
|
2019-03-22 17:18:09 -04:00
|
|
|
});
|
|
|
|
|
feat(ivy): TestBed support for reusing non-exported components (#30578)
This is a new feature of the Ivy TestBed.
A common user pattern is to test one component with another. This is
commonly done by creating a `TestFixture` component which exercises the
main component under test.
This pattern is more difficult if the component under test is declared in an
NgModule but not exported. In this case, overriding the module is necessary.
In g3 (and View Engine), it's possible to use an NgSummary to override the
recompilation of a component, and depend on its AOT compiled factory. The
way this is implemented, however, specifying a summary for a module
effectively overrides much of the TestBed's other behavior. For example, the
following is legal:
```typescript
TestBed.configureTestingModule({
declarations: [FooCmp, TestFixture],
imports: [FooModule],
aotSummaries: [FooModuleNgSummary],
});
```
Here, `FooCmp` is declared in both the testing module and in the imported
`FooModule`. However, because the summary is provided, `FooCmp` is not
compiled within the context of the testing module, but _is_ made available
for `TestFixture` to use, even if it wasn't originally exported from
`FooModule`.
This pattern breaks in Ivy - because summaries are a no-op, this amounts
to a true double declaration of `FooCmp` which raises an error.
Fixing this in user code is possible, but is difficult to do in an
automated or backwards compatible way. An alternative solution is
implemented in this PR.
This PR attempts to capture the user intent of the following previously
unsupported configuration:
```typescript
TestBed.configureTestingModule({
declarations: [FooCmp, TestFixture],
imports: [FooModule],
});
```
Note that this is the same as the configuration above in Ivy, as the
`aotSummaries` value provided has no effect.
The user intent here is interpreted as follows:
1) `FooCmp` is a pre-existing component that's being used in the test
(via import of `FooModule`). It may or may not be exported by this
module.
2) `FooCmp` should be part of the testing module's scope. That is, it
should be visible to `TestFixture`. This is because it's listed in
`declarations`.
This feature effectively makes the `TestBed` testing module special. It's
able to declare components without compiling them, if they're already
compiled (or configured to be compiled) in the imports. And crucially, the
behavior matches the first example with the summary, making Ivy backwards
compatible with View Engine for tests that use summaries.
PR Close #30578
2019-05-20 19:49:20 -04:00
|
|
|
onlyInIvy('TestBed new feature to allow declaration and import of component')
|
|
|
|
.it('should allow both the declaration and import of a component into the testing module',
|
|
|
|
() => {
|
|
|
|
// This test validates that a component (Outer) which is both declared and imported
|
|
|
|
// (via its module) in the testing module behaves correctly. That is:
|
|
|
|
//
|
|
|
|
// 1) the component should be compiled in the scope of its original module.
|
|
|
|
//
|
|
|
|
// This condition is tested by having the component (Outer) use another component
|
|
|
|
// (Inner) within its template. Thus, if it's compiled in the correct scope then the
|
|
|
|
// text 'Inner' from the template of (Inner) should appear in the result.
|
|
|
|
//
|
|
|
|
// 2) the component should be available in the TestingModule scope.
|
|
|
|
//
|
|
|
|
// This condition is tested by attempting to use the component (Outer) inside a test
|
|
|
|
// fixture component (Fixture) which is declared in the testing module only.
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'inner',
|
|
|
|
template: 'Inner',
|
|
|
|
})
|
|
|
|
class Inner {
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
selector: 'outer',
|
|
|
|
template: '<inner></inner>',
|
|
|
|
})
|
|
|
|
class Outer {
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({
|
|
|
|
declarations: [Inner, Outer],
|
|
|
|
})
|
|
|
|
class Module {
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<outer></outer>',
|
|
|
|
selector: 'fixture',
|
|
|
|
})
|
|
|
|
class Fixture {
|
|
|
|
}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
declarations: [Outer, Fixture],
|
|
|
|
imports: [Module],
|
|
|
|
});
|
|
|
|
|
|
|
|
const fixture = TestBed.createComponent(Fixture);
|
|
|
|
// The Outer component should have its template stamped out, and that template should
|
|
|
|
// include a correct instance of the Inner component with the 'Inner' text from its
|
|
|
|
// template.
|
|
|
|
expect(fixture.nativeElement.innerHTML).toEqual('<outer><inner>Inner</inner></outer>');
|
|
|
|
});
|
|
|
|
|
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 {
|
2019-10-11 17:18:45 -04:00
|
|
|
static ɵfac = () => new ComponentClass();
|
2019-10-10 17:57:15 -04:00
|
|
|
static ɵcmp = defineComponent({
|
2019-03-27 17:56:08 -04:00
|
|
|
type: ComponentClass,
|
|
|
|
selectors: [['comp']],
|
2019-09-23 14:08:51 -04:00
|
|
|
decls: 1,
|
2019-03-27 17:56:08 -04:00
|
|
|
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');
|
|
|
|
});
|
2019-05-21 19:54:10 -04:00
|
|
|
|
|
|
|
it('should have an ability to override template with empty string', () => {
|
|
|
|
const SomeComponent = getAOTCompiledComponent();
|
|
|
|
TestBed.configureTestingModule({declarations: [SomeComponent]});
|
|
|
|
TestBed.overrideTemplateUsingTestingModule(SomeComponent, '');
|
|
|
|
const fixture = TestBed.createComponent(SomeComponent);
|
|
|
|
expect(fixture.nativeElement.innerHTML).toBe('');
|
|
|
|
});
|
feat(ivy): TestBed support for reusing non-exported components (#30578)
This is a new feature of the Ivy TestBed.
A common user pattern is to test one component with another. This is
commonly done by creating a `TestFixture` component which exercises the
main component under test.
This pattern is more difficult if the component under test is declared in an
NgModule but not exported. In this case, overriding the module is necessary.
In g3 (and View Engine), it's possible to use an NgSummary to override the
recompilation of a component, and depend on its AOT compiled factory. The
way this is implemented, however, specifying a summary for a module
effectively overrides much of the TestBed's other behavior. For example, the
following is legal:
```typescript
TestBed.configureTestingModule({
declarations: [FooCmp, TestFixture],
imports: [FooModule],
aotSummaries: [FooModuleNgSummary],
});
```
Here, `FooCmp` is declared in both the testing module and in the imported
`FooModule`. However, because the summary is provided, `FooCmp` is not
compiled within the context of the testing module, but _is_ made available
for `TestFixture` to use, even if it wasn't originally exported from
`FooModule`.
This pattern breaks in Ivy - because summaries are a no-op, this amounts
to a true double declaration of `FooCmp` which raises an error.
Fixing this in user code is possible, but is difficult to do in an
automated or backwards compatible way. An alternative solution is
implemented in this PR.
This PR attempts to capture the user intent of the following previously
unsupported configuration:
```typescript
TestBed.configureTestingModule({
declarations: [FooCmp, TestFixture],
imports: [FooModule],
});
```
Note that this is the same as the configuration above in Ivy, as the
`aotSummaries` value provided has no effect.
The user intent here is interpreted as follows:
1) `FooCmp` is a pre-existing component that's being used in the test
(via import of `FooModule`). It may or may not be exported by this
module.
2) `FooCmp` should be part of the testing module's scope. That is, it
should be visible to `TestFixture`. This is because it's listed in
`declarations`.
This feature effectively makes the `TestBed` testing module special. It's
able to declare components without compiling them, if they're already
compiled (or configured to be compiled) in the imports. And crucially, the
behavior matches the first example with the summary, making Ivy backwards
compatible with View Engine for tests that use summaries.
PR Close #30578
2019-05-20 19:49:20 -04:00
|
|
|
|
|
|
|
it('should allow component in both in declarations and imports', () => {
|
|
|
|
const SomeComponent = getAOTCompiledComponent();
|
|
|
|
|
|
|
|
// This is an AOT compiled module which declares (but does not export) SomeComponent.
|
|
|
|
class ModuleClass {
|
2019-10-14 10:20:26 -04:00
|
|
|
static ɵmod = defineNgModule({
|
feat(ivy): TestBed support for reusing non-exported components (#30578)
This is a new feature of the Ivy TestBed.
A common user pattern is to test one component with another. This is
commonly done by creating a `TestFixture` component which exercises the
main component under test.
This pattern is more difficult if the component under test is declared in an
NgModule but not exported. In this case, overriding the module is necessary.
In g3 (and View Engine), it's possible to use an NgSummary to override the
recompilation of a component, and depend on its AOT compiled factory. The
way this is implemented, however, specifying a summary for a module
effectively overrides much of the TestBed's other behavior. For example, the
following is legal:
```typescript
TestBed.configureTestingModule({
declarations: [FooCmp, TestFixture],
imports: [FooModule],
aotSummaries: [FooModuleNgSummary],
});
```
Here, `FooCmp` is declared in both the testing module and in the imported
`FooModule`. However, because the summary is provided, `FooCmp` is not
compiled within the context of the testing module, but _is_ made available
for `TestFixture` to use, even if it wasn't originally exported from
`FooModule`.
This pattern breaks in Ivy - because summaries are a no-op, this amounts
to a true double declaration of `FooCmp` which raises an error.
Fixing this in user code is possible, but is difficult to do in an
automated or backwards compatible way. An alternative solution is
implemented in this PR.
This PR attempts to capture the user intent of the following previously
unsupported configuration:
```typescript
TestBed.configureTestingModule({
declarations: [FooCmp, TestFixture],
imports: [FooModule],
});
```
Note that this is the same as the configuration above in Ivy, as the
`aotSummaries` value provided has no effect.
The user intent here is interpreted as follows:
1) `FooCmp` is a pre-existing component that's being used in the test
(via import of `FooModule`). It may or may not be exported by this
module.
2) `FooCmp` should be part of the testing module's scope. That is, it
should be visible to `TestFixture`. This is because it's listed in
`declarations`.
This feature effectively makes the `TestBed` testing module special. It's
able to declare components without compiling them, if they're already
compiled (or configured to be compiled) in the imports. And crucially, the
behavior matches the first example with the summary, making Ivy backwards
compatible with View Engine for tests that use summaries.
PR Close #30578
2019-05-20 19:49:20 -04:00
|
|
|
type: ModuleClass,
|
|
|
|
declarations: [SomeComponent],
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({
|
|
|
|
template: '<comp></comp>',
|
|
|
|
|
|
|
|
selector: 'fixture',
|
|
|
|
})
|
|
|
|
class TestFixture {
|
|
|
|
}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({
|
|
|
|
// Here, SomeComponent is both declared, and then the module which declares it is
|
|
|
|
// also imported. This used to be a duplicate declaration error, but is now interpreted
|
|
|
|
// to mean:
|
|
|
|
// 1) Compile (or reuse) SomeComponent in the context of its original NgModule
|
|
|
|
// 2) Make SomeComponent available in the scope of the testing module, even if it wasn't
|
|
|
|
// originally exported from its NgModule.
|
|
|
|
//
|
|
|
|
// This allows TestFixture to use SomeComponent, which is asserted below.
|
|
|
|
declarations: [SomeComponent, TestFixture],
|
|
|
|
imports: [ModuleClass],
|
|
|
|
});
|
|
|
|
const fixture = TestBed.createComponent(TestFixture);
|
|
|
|
// The regex avoids any issues with styling attributes.
|
|
|
|
expect(fixture.nativeElement.innerHTML).toMatch(/<comp[^>]*>Some template<\/comp>/);
|
|
|
|
});
|
2019-03-27 17:56:08 -04:00
|
|
|
});
|
|
|
|
|
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);
|
|
|
|
|
2019-10-10 17:57:15 -04:00
|
|
|
const defBeforeReset = (SomeComponent as any).ɵcmp;
|
2019-01-29 20:13:02 -05:00
|
|
|
expect(defBeforeReset.pipeDefs().length).toEqual(1);
|
|
|
|
expect(defBeforeReset.directiveDefs().length).toEqual(2); // directive + component
|
|
|
|
|
|
|
|
TestBed.resetTestingModule();
|
|
|
|
|
2019-10-10 17:57:15 -04:00
|
|
|
const defAfterReset = (SomeComponent as any).ɵcmp;
|
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);
|
|
|
|
|
2019-10-10 17:57:15 -04:00
|
|
|
expect(ComponentWithNoAnnotations.hasOwnProperty('ɵcmp')).toBeTruthy();
|
|
|
|
expect(SomeComponent.hasOwnProperty('ɵcmp')).toBeTruthy();
|
2019-01-29 20:13:02 -05:00
|
|
|
|
2019-10-11 15:28:12 -04:00
|
|
|
expect(DirectiveWithNoAnnotations.hasOwnProperty('ɵdir')).toBeTruthy();
|
|
|
|
expect(SomeDirective.hasOwnProperty('ɵdir')).toBeTruthy();
|
2019-03-20 20:58:20 -04:00
|
|
|
|
2019-10-11 22:19:59 -04:00
|
|
|
expect(PipeWithNoAnnotations.hasOwnProperty('ɵpipe')).toBeTruthy();
|
|
|
|
expect(SomePipe.hasOwnProperty('ɵpipe')).toBeTruthy();
|
2019-03-20 20:58:20 -04:00
|
|
|
|
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-10-10 17:57:15 -04:00
|
|
|
expect(ComponentWithNoAnnotations.hasOwnProperty('ɵcmp')).toBeFalsy();
|
2019-10-11 15:28:12 -04:00
|
|
|
expect(DirectiveWithNoAnnotations.hasOwnProperty('ɵdir')).toBeFalsy();
|
2019-10-11 22:19:59 -04:00
|
|
|
expect(PipeWithNoAnnotations.hasOwnProperty('ɵpipe')).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-10-10 17:57:15 -04:00
|
|
|
expect(SomeComponent.hasOwnProperty('ɵcmp')).toBeTruthy();
|
2019-10-11 15:28:12 -04:00
|
|
|
expect(SomeDirective.hasOwnProperty('ɵdir')).toBeTruthy();
|
2019-10-11 22:19:59 -04:00
|
|
|
expect(SomePipe.hasOwnProperty('ɵpipe')).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'}});
|
|
|
|
|
2019-08-28 19:22:36 -04:00
|
|
|
expect(TestBed.inject(Token).name).toEqual('fake');
|
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
|
|
|
|
|
|
|
TestBed.resetTestingModule();
|
|
|
|
|
|
|
|
// The providers for the module should have been restored to the original array, with
|
|
|
|
// no trace of the overridden providers.
|
2019-10-14 18:28:01 -04:00
|
|
|
expect((Module as any).ɵinj.providers).toEqual([Token]);
|
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
|
|
|
});
|
2019-09-23 19:15:11 -04:00
|
|
|
|
|
|
|
it('should clean up overridden providers on components whose modules are compiled more than once',
|
|
|
|
async() => {
|
|
|
|
@Injectable()
|
|
|
|
class SomeInjectable {
|
|
|
|
id: string|undefined;
|
|
|
|
}
|
|
|
|
|
|
|
|
@Component({providers: [SomeInjectable]})
|
|
|
|
class ComponentWithProvider {
|
|
|
|
constructor(readonly injectable: SomeInjectable) {}
|
|
|
|
}
|
|
|
|
|
|
|
|
@NgModule({declarations: [ComponentWithProvider]})
|
|
|
|
class MyModule {
|
|
|
|
}
|
|
|
|
|
|
|
|
TestBed.configureTestingModule({imports: [MyModule]});
|
2019-10-10 17:57:15 -04:00
|
|
|
const originalResolver = (ComponentWithProvider as any).ɵcmp.providersResolver;
|
2019-09-23 19:15:11 -04:00
|
|
|
TestBed.overrideProvider(SomeInjectable, {useValue: {id: 'fake'}});
|
|
|
|
|
|
|
|
const compiler = TestBed.inject(Compiler);
|
|
|
|
await compiler.compileModuleAsync(MyModule);
|
|
|
|
compiler.compileModuleSync(MyModule);
|
|
|
|
|
|
|
|
TestBed.resetTestingModule();
|
2019-10-10 17:57:15 -04:00
|
|
|
expect((ComponentWithProvider as any).ɵcmp.providersResolver)
|
2019-09-23 19:15:11 -04:00
|
|
|
.toEqual(originalResolver);
|
|
|
|
});
|
2018-12-20 20:45:53 -05:00
|
|
|
});
|
2019-09-26 20:03:29 -04:00
|
|
|
|
|
|
|
onlyInIvy('Ivy module registration happens when NgModuleFactory is created')
|
2019-11-08 13:16:40 -05:00
|
|
|
.it('cleans up registered modules', async() => {
|
|
|
|
@NgModule({id: 'my_module'})
|
|
|
|
class MyModule {
|
|
|
|
}
|
|
|
|
|
|
|
|
expect(() => getModuleFactory('my_module')).toThrowError();
|
|
|
|
await TestBed.inject(Compiler).compileModuleAsync(MyModule);
|
|
|
|
expect(() => getModuleFactory('my_module')).not.toThrowError();
|
|
|
|
TestBed.resetTestingModule();
|
|
|
|
expect(() => getModuleFactory('my_module')).toThrowError();
|
2019-09-26 20:03:29 -04:00
|
|
|
});
|
2018-08-06 17:09:38 -04:00
|
|
|
});
|