
If a component has its definition set by defineComponent (as opposed to JIT getter), then it will generate a factory that uses directiveInject() to retrieve its dependencies. This can be problematic in test code because tests could use the injection utility before bootstrapping the component, and directiveInject() relies on the view having been created. This commit tweaks directiveInject() to fall back to inject() if the view has not been created. This will allow injection to work in tests even if it is called before the component is bootstrapped. PR Close #29948
287 lines
8.3 KiB
TypeScript
287 lines
8.3 KiB
TypeScript
/**
|
|
* @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
|
|
*/
|
|
|
|
import {Component, Directive, Inject, Injectable, InjectionToken} from '@angular/core';
|
|
import {TestBed, async, inject} from '@angular/core/testing';
|
|
import {By} from '@angular/platform-browser';
|
|
import {onlyInIvy} from '@angular/private/testing';
|
|
|
|
describe('providers', () => {
|
|
|
|
describe('inheritance', () => {
|
|
|
|
it('should NOT inherit providers', () => {
|
|
const SOME_DIRS = new InjectionToken('someDirs');
|
|
|
|
@Directive({
|
|
selector: '[super-dir]',
|
|
providers: [{provide: SOME_DIRS, useClass: SuperDirective, multi: true}]
|
|
})
|
|
class SuperDirective {
|
|
}
|
|
|
|
@Directive({
|
|
selector: '[sub-dir]',
|
|
providers: [{provide: SOME_DIRS, useClass: SubDirective, multi: true}]
|
|
})
|
|
class SubDirective extends SuperDirective {
|
|
}
|
|
|
|
@Directive({selector: '[other-dir]'})
|
|
class OtherDirective {
|
|
constructor(@Inject(SOME_DIRS) public dirs: any) {}
|
|
}
|
|
|
|
@Component({selector: 'app-comp', template: `<div other-dir sub-dir></div>`})
|
|
class App {
|
|
}
|
|
|
|
TestBed.configureTestingModule(
|
|
{declarations: [SuperDirective, SubDirective, OtherDirective, App]});
|
|
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.detectChanges();
|
|
|
|
const otherDir = fixture.debugElement.query(By.css('div')).injector.get(OtherDirective);
|
|
expect(otherDir.dirs.length).toEqual(1);
|
|
expect(otherDir.dirs[0] instanceof SubDirective).toBe(true);
|
|
});
|
|
|
|
});
|
|
|
|
describe('lifecycles', () => {
|
|
it('should inherit ngOnDestroy hooks on providers', () => {
|
|
const logs: string[] = [];
|
|
|
|
@Injectable()
|
|
class SuperInjectableWithDestroyHook {
|
|
ngOnDestroy() { logs.push('OnDestroy'); }
|
|
}
|
|
|
|
@Injectable()
|
|
class SubInjectableWithDestroyHook extends SuperInjectableWithDestroyHook {
|
|
}
|
|
|
|
@Component({template: '', providers: [SubInjectableWithDestroyHook]})
|
|
class App {
|
|
constructor(foo: SubInjectableWithDestroyHook) {}
|
|
}
|
|
|
|
TestBed.configureTestingModule({declarations: [App]});
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.detectChanges();
|
|
fixture.destroy();
|
|
|
|
expect(logs).toEqual(['OnDestroy']);
|
|
});
|
|
|
|
it('should not call ngOnDestroy for providers that have not been requested', () => {
|
|
const logs: string[] = [];
|
|
|
|
@Injectable()
|
|
class InjectableWithDestroyHook {
|
|
ngOnDestroy() { logs.push('OnDestroy'); }
|
|
}
|
|
|
|
@Component({template: '', providers: [InjectableWithDestroyHook]})
|
|
class App {
|
|
}
|
|
|
|
TestBed.configureTestingModule({declarations: [App]});
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.detectChanges();
|
|
fixture.destroy();
|
|
|
|
expect(logs).toEqual([]);
|
|
});
|
|
|
|
it('should only call ngOnDestroy once for multiple instances', () => {
|
|
const logs: string[] = [];
|
|
|
|
@Injectable()
|
|
class InjectableWithDestroyHook {
|
|
ngOnDestroy() { logs.push('OnDestroy'); }
|
|
}
|
|
|
|
@Component({selector: 'my-cmp', template: ''})
|
|
class MyComponent {
|
|
constructor(foo: InjectableWithDestroyHook) {}
|
|
}
|
|
|
|
@Component({
|
|
template: `
|
|
<my-cmp></my-cmp>
|
|
<my-cmp></my-cmp>
|
|
`,
|
|
providers: [InjectableWithDestroyHook]
|
|
})
|
|
class App {
|
|
}
|
|
|
|
TestBed.configureTestingModule({declarations: [App, MyComponent]});
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.detectChanges();
|
|
fixture.destroy();
|
|
|
|
expect(logs).toEqual(['OnDestroy']);
|
|
});
|
|
|
|
it('should call ngOnDestroy when providing same token via useClass', () => {
|
|
const logs: string[] = [];
|
|
|
|
@Injectable()
|
|
class InjectableWithDestroyHook {
|
|
ngOnDestroy() { logs.push('OnDestroy'); }
|
|
}
|
|
|
|
@Component({
|
|
template: '',
|
|
providers: [{provide: InjectableWithDestroyHook, useClass: InjectableWithDestroyHook}]
|
|
})
|
|
class App {
|
|
constructor(foo: InjectableWithDestroyHook) {}
|
|
}
|
|
|
|
TestBed.configureTestingModule({declarations: [App]});
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.detectChanges();
|
|
fixture.destroy();
|
|
|
|
expect(logs).toEqual(['OnDestroy']);
|
|
});
|
|
|
|
onlyInIvy('Destroy hook of useClass provider is invoked correctly')
|
|
.it('should only call ngOnDestroy of value when providing via useClass', () => {
|
|
const logs: string[] = [];
|
|
|
|
@Injectable()
|
|
class InjectableWithDestroyHookToken {
|
|
ngOnDestroy() { logs.push('OnDestroy Token'); }
|
|
}
|
|
|
|
@Injectable()
|
|
class InjectableWithDestroyHookValue {
|
|
ngOnDestroy() { logs.push('OnDestroy Value'); }
|
|
}
|
|
|
|
@Component({
|
|
template: '',
|
|
providers: [
|
|
{provide: InjectableWithDestroyHookToken, useClass: InjectableWithDestroyHookValue}
|
|
]
|
|
})
|
|
class App {
|
|
constructor(foo: InjectableWithDestroyHookToken) {}
|
|
}
|
|
|
|
TestBed.configureTestingModule({declarations: [App]});
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.detectChanges();
|
|
fixture.destroy();
|
|
|
|
expect(logs).toEqual(['OnDestroy Value']);
|
|
});
|
|
|
|
it('should only call ngOnDestroy of value when providing via useExisting', () => {
|
|
const logs: string[] = [];
|
|
|
|
@Injectable()
|
|
class InjectableWithDestroyHookToken {
|
|
ngOnDestroy() { logs.push('OnDestroy Token'); }
|
|
}
|
|
|
|
@Injectable()
|
|
class InjectableWithDestroyHookExisting {
|
|
ngOnDestroy() { logs.push('OnDestroy Existing'); }
|
|
}
|
|
|
|
@Component({
|
|
template: '',
|
|
providers: [
|
|
InjectableWithDestroyHookExisting, {
|
|
provide: InjectableWithDestroyHookToken,
|
|
useExisting: InjectableWithDestroyHookExisting
|
|
}
|
|
]
|
|
})
|
|
class App {
|
|
constructor(foo1: InjectableWithDestroyHookExisting, foo2: InjectableWithDestroyHookToken) {
|
|
}
|
|
}
|
|
|
|
TestBed.configureTestingModule({declarations: [App]});
|
|
const fixture = TestBed.createComponent(App);
|
|
fixture.detectChanges();
|
|
fixture.destroy();
|
|
|
|
expect(logs).toEqual(['OnDestroy Existing']);
|
|
});
|
|
|
|
});
|
|
|
|
describe('components and directives', () => {
|
|
|
|
class MyService {
|
|
value = 'some value';
|
|
}
|
|
|
|
@Component({selector: 'my-comp', template: ``})
|
|
class MyComp {
|
|
constructor(public svc: MyService) {}
|
|
}
|
|
|
|
@Directive({selector: '[some-dir]'})
|
|
class MyDir {
|
|
constructor(public svc: MyService) {}
|
|
}
|
|
|
|
it('should support providing components in tests without @Injectable', () => {
|
|
@Component({selector: 'test-comp', template: '<my-comp></my-comp>'})
|
|
class TestComp {
|
|
}
|
|
|
|
TestBed.configureTestingModule({
|
|
declarations: [TestComp, MyComp],
|
|
// providing MyComp is unnecessary but it shouldn't throw
|
|
providers: [MyComp, MyService],
|
|
});
|
|
|
|
const fixture = TestBed.createComponent(TestComp);
|
|
const myCompInstance = fixture.debugElement.query(By.css('my-comp')).injector.get(MyComp);
|
|
expect(myCompInstance.svc.value).toEqual('some value');
|
|
});
|
|
|
|
it('should support providing directives in tests without @Injectable', () => {
|
|
@Component({selector: 'test-comp', template: '<div some-dir></div>'})
|
|
class TestComp {
|
|
}
|
|
|
|
TestBed.configureTestingModule({
|
|
declarations: [TestComp, MyDir],
|
|
// providing MyDir is unnecessary but it shouldn't throw
|
|
providers: [MyDir, MyService],
|
|
});
|
|
|
|
const fixture = TestBed.createComponent(TestComp);
|
|
const myCompInstance = fixture.debugElement.query(By.css('div')).injector.get(MyDir);
|
|
expect(myCompInstance.svc.value).toEqual('some value');
|
|
});
|
|
|
|
describe('injection without bootstrapping', () => {
|
|
beforeEach(() => {
|
|
TestBed.configureTestingModule({declarations: [MyComp], providers: [MyComp, MyService]});
|
|
});
|
|
|
|
it('should support injecting without bootstrapping',
|
|
async(inject([MyComp, MyService], (comp: MyComp, service: MyService) => {
|
|
expect(comp.svc.value).toEqual('some value');
|
|
})));
|
|
});
|
|
});
|
|
});
|