Currently, we are not properly resolving forward refs when they appear in deps for providers created with the useFactory strategy. This commit wraps provider deps in the resolveForwardRef call so the tokens are passed into the inject function as expected. PR Close #30201
325 lines
9.2 KiB
TypeScript
325 lines
9.2 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, NgModule, forwardRef} 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');
|
|
})));
|
|
});
|
|
});
|
|
|
|
describe('forward refs', () => {
|
|
|
|
it('should support forward refs in provider deps', () => {
|
|
class MyService {
|
|
constructor(public dep: {value: string}) {}
|
|
}
|
|
|
|
class OtherService {
|
|
value = 'one';
|
|
}
|
|
|
|
@Component({selector: 'app-comp', template: ``})
|
|
class AppComp {
|
|
constructor(public myService: MyService) {}
|
|
}
|
|
|
|
@NgModule({
|
|
providers: [
|
|
OtherService, {
|
|
provide: MyService,
|
|
useFactory: (dep: {value: string}) => new MyService(dep),
|
|
deps: [forwardRef(() => OtherService)]
|
|
}
|
|
],
|
|
declarations: [AppComp]
|
|
})
|
|
class MyModule {
|
|
}
|
|
|
|
TestBed.configureTestingModule({imports: [MyModule]});
|
|
|
|
const fixture = TestBed.createComponent(AppComp);
|
|
expect(fixture.componentInstance.myService.dep.value).toBe('one');
|
|
});
|
|
|
|
});
|
|
|
|
});
|