diff --git a/packages/core/test/acceptance/di_spec.ts b/packages/core/test/acceptance/di_spec.ts index b7c2090d93..6df8bb54a7 100644 --- a/packages/core/test/acceptance/di_spec.ts +++ b/packages/core/test/acceptance/di_spec.ts @@ -7,13 +7,480 @@ */ import {CommonModule} from '@angular/common'; -import {Attribute, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, INJECTOR, Inject, Injector, Input, LOCALE_ID, Optional, Output, Pipe, PipeTransform, SkipSelf, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; +import {Attribute, ChangeDetectorRef, Component, Directive, ElementRef, EventEmitter, HostBinding, INJECTOR, Inject, Injectable, Injector, Input, LOCALE_ID, Optional, Output, Pipe, PipeTransform, SkipSelf, TemplateRef, ViewChild, ViewContainerRef, forwardRef} from '@angular/core'; import {ViewRef} from '@angular/core/src/render3/view_ref'; import {TestBed} from '@angular/core/testing'; import {onlyInIvy} from '@angular/private/testing'; - describe('di', () => { + describe('no dependencies', () => { + it('should create directive with no deps', () => { + @Directive({selector: '[dir]', exportAs: 'dir'}) + class MyDirective { + value = 'Created'; + } + @Component({template: '
{{ dir.value }}
'}) + class MyComp { + } + TestBed.configureTestingModule({declarations: [MyDirective, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.textContent).toContain('Created'); + }); + }); + + describe('directive injection', () => { + + let log: string[] = []; + + @Directive({selector: '[dirB]', exportAs: 'dirB'}) + class DirectiveB { + @Input() value = 'DirB'; + constructor() { log.push(this.value); } + } + + beforeEach(() => log = []); + + it('should create directive with intra view dependencies', () => { + @Directive({selector: '[dirA]', exportAs: 'dirA'}) + class DirectiveA { + value = 'DirA'; + } + @Directive({selector: '[dirC]', exportAs: 'dirC'}) + class DirectiveC { + value: string; + constructor(dirA: DirectiveA, dirB: DirectiveB) { this.value = dirA.value + dirB.value; } + } + @Component({ + template: ` +
+ {{ dir.value }} +
+ ` + }) + class MyComp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, DirectiveC, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('span'); + expect(divElement.textContent).toContain('DirADirB'); + }); + + it('should instantiate injected directives in dependency order', () => { + @Directive({selector: '[dirA]'}) + class DirectiveA { + value = 'dirA'; + constructor(dirB: DirectiveB) { log.push(`DirA (dep: ${dirB.value})`); } + } + @Component({template: '
'}) + class MyComp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(log).toEqual(['DirB', 'DirA (dep: DirB)']); + }); + + it('should fallback to the module injector', () => { + @Directive({selector: '[dirA]'}) + class DirectiveA { + value = 'dirA'; + constructor(dirB: DirectiveB) { log.push(`DirA (dep: ${dirB.value})`); } + } + // - dirB is know to the node injectors + // - then when dirA tries to inject dirB, it will check the node injector first tree + // - if not found, it will check the module injector tree + @Component({template: '
'}) + class MyComp { + } + TestBed.configureTestingModule({ + declarations: [DirectiveA, DirectiveB, MyComp], + providers: [{provide: DirectiveB, useValue: {value: 'module'}}] + }); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(log).toEqual(['DirB', 'DirA (dep: module)']); + }); + + it('should instantiate injected directives before components', () => { + @Component({selector: 'my-comp', template: ''}) + class MyComp { + constructor(dirB: DirectiveB) { log.push(`Comp (dep: ${dirB.value})`); } + } + @Component({template: ''}) + class MyApp { + } + TestBed.configureTestingModule({declarations: [DirectiveB, MyComp, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(log).toEqual(['DirB', 'Comp (dep: DirB)']); + }); + + it('should inject directives in the correct order in a for loop', () => { + @Directive({selector: '[dirA]'}) + class DirectiveA { + constructor(dir: DirectiveB) { log.push(`DirA (dep: ${dir.value})`); } + } + @Component({template: '
'}) + class MyComp { + array = [1, 2, 3]; + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(log).toEqual( + ['DirB', 'DirA (dep: DirB)', 'DirB', 'DirA (dep: DirB)', 'DirB', 'DirA (dep: DirB)']); + }); + + it('should instantiate directives with multiple out-of-order dependencies', () => { + @Directive({selector: '[dirA]'}) + class DirectiveA { + value = 'DirA'; + constructor() { log.push(this.value); } + } + @Directive({selector: '[dirC]'}) + class DirectiveC { + value = 'DirC'; + constructor() { log.push(this.value); } + } + @Directive({selector: '[dirB]'}) + class DirectiveB { + constructor(dirA: DirectiveA, dirC: DirectiveC) { + log.push(`DirB (deps: ${dirA.value} and ${dirC.value})`); + } + } + @Component({template: '
'}) + class MyComp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, DirectiveC, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + expect(log).toEqual(['DirA', 'DirC', 'DirB (deps: DirA and DirC)']); + }); + + it('should instantiate in the correct order for complex case', () => { + @Directive({selector: '[dirC]'}) + class DirectiveC { + value = 'DirC'; + constructor(dirB: DirectiveB) { log.push(`DirC (dep: ${dirB.value})`); } + } + @Directive({selector: '[dirA]'}) + class DirectiveA { + value = 'DirA'; + constructor(dirC: DirectiveC) { log.push(`DirA (dep: ${dirC.value})`); } + } + @Directive({selector: '[dirD]'}) + class DirectiveD { + value = 'DirD'; + constructor(dirA: DirectiveA) { log.push(`DirD (dep: ${dirA.value})`); } + } + @Component({selector: 'my-comp', template: ''}) + class MyComp { + constructor(dirD: DirectiveD) { log.push(`Comp (dep: ${dirD.value})`); } + } + @Component({template: ''}) + class MyApp { + } + TestBed.configureTestingModule( + {declarations: [DirectiveA, DirectiveB, DirectiveC, DirectiveD, MyComp, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(log).toEqual( + ['DirB', 'DirC (dep: DirB)', 'DirA (dep: DirC)', 'DirD (dep: DirA)', 'Comp (dep: DirD)']); + }); + + it('should instantiate in correct order with mixed parent and peer dependencies', () => { + @Component({template: '
'}) + class MyApp { + value = 'App'; + } + @Directive({selector: '[dirA]'}) + class DirectiveA { + constructor(dirB: DirectiveB, app: MyApp) { + log.push(`DirA (deps: ${dirB.value} and ${app.value})`); + } + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(log).toEqual(['DirB', 'DirA (deps: DirB and App)']); + }); + + it('should not use a parent when peer dep is available', () => { + let count = 1; + @Directive({selector: '[dirB]'}) + class DirectiveB { + count: number; + constructor() { + log.push(`DirB`); + this.count = count++; + } + } + @Directive({selector: '[dirA]'}) + class DirectiveA { + constructor(dirB: DirectiveB) { log.push(`DirA (dep: DirB - ${dirB.count})`); } + } + @Component({selector: 'my-comp', template: '
'}) + class MyComp { + } + @Component({template: ''}) + class MyApp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + expect(log).toEqual(['DirB', 'DirB', 'DirA (dep: DirB - 2)']); + }); + + describe('dependencies in parent views', () => { + + @Directive({selector: '[dirA]', exportAs: 'dirA'}) + class DirectiveA { + injector: Injector; + constructor(public dirB: DirectiveB, public vcr: ViewContainerRef) { + this.injector = vcr.injector; + } + } + + @Component( + {selector: 'my-comp', template: '
{{ dir.dirB.value }}
'}) + class MyComp { + } + + it('should find dependencies on component hosts', () => { + @Component({template: ''}) + class MyApp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.textContent).toEqual('DirB'); + }); + + it('should find dependencies for directives in embedded views', () => { + @Component({ + template: `
+
+
{{ dir.dirB.value }}
+
+
` + }) + class MyApp { + showing = false; + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.componentInstance.showing = true; + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.textContent).toEqual('DirB'); + }); + + it('should find dependencies of directives nested deeply in inline views', () => { + @Component({ + template: `
+ + +
{{ dir.dirB.value }}
+
+
+
` + }) + class MyApp { + skipContent = false; + skipContent2 = false; + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.textContent).toEqual('DirB'); + }); + + it('should find dependencies in declaration tree of ng-template (not insertion tree)', () => { + @Directive({selector: '[structuralDir]'}) + class StructuralDirective { + @Input() tmp !: TemplateRef; + constructor(public vcr: ViewContainerRef) {} + + create() { this.vcr.createEmbeddedView(this.tmp); } + } + @Component({ + template: `
+ +
{{ dir.dirB.value }}
+
+
+ +
+
+ +
` + }) + class MyComp { + @ViewChild(StructuralDirective) structuralDir !: StructuralDirective; + } + TestBed.configureTestingModule( + {declarations: [StructuralDirective, DirectiveA, DirectiveB, MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + fixture.componentInstance.structuralDir.create(); + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('div[value=insertion]'); + expect(divElement.textContent).toEqual('declaration'); + }); + + it('should create injectors on second template pass', () => { + @Component({ + template: `
+ + +
` + }) + class MyApp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.textContent).toEqual('DirBDirB'); + }); + + it('should create injectors and host bindings in same view', () => { + @Directive({selector: '[hostBindingDir]'}) + class HostBindingDirective { + @HostBinding('id') id = 'foo'; + } + + @Component({ + template: `
+

{{ dir.dirB.value }}

+
` + }) + class MyApp { + @ViewChild(HostBindingDirective) hostBindingDir !: HostBindingDirective; + @ViewChild(DirectiveA) dirA !: DirectiveA; + } + TestBed.configureTestingModule( + {declarations: [DirectiveA, DirectiveB, HostBindingDirective, MyApp]}); + const fixture = TestBed.createComponent(MyApp); + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.textContent).toEqual('DirB'); + expect(divElement.id).toEqual('foo'); + + const dirA = fixture.componentInstance.dirA; + expect(dirA.vcr.injector).toEqual(dirA.injector); + + const hostBindingDir = fixture.componentInstance.hostBindingDir; + hostBindingDir.id = 'bar'; + fixture.detectChanges(); + expect(divElement.id).toBe('bar'); + }); + }); + + it('should throw if directive is not found anywhere', () => { + @Directive({selector: '[dirB]'}) + class DirectiveB { + constructor() {} + } + @Directive({selector: '[dirA]'}) + class DirectiveA { + constructor(siblingDir: DirectiveB) {} + } + @Component({template: '
'}) + class MyComp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]}); + expect(() => TestBed.createComponent(MyComp)).toThrowError(/No provider for DirectiveB/); + }); + + it('should throw if directive is not found in ancestor tree', () => { + @Directive({selector: '[dirB]'}) + class DirectiveB { + constructor() {} + } + @Directive({selector: '[dirA]'}) + class DirectiveA { + constructor(siblingDir: DirectiveB) {} + } + @Component({template: '
'}) + class MyComp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]}); + expect(() => TestBed.createComponent(MyComp)).toThrowError(/No provider for DirectiveB/); + }); + + onlyInIvy('Ivy has different error message for circular dependency') + .it('should throw if directives try to inject each other', () => { + @Directive({selector: '[dirB]'}) + class DirectiveB { + constructor(@Inject(forwardRef(() => DirectiveA)) siblingDir: DirectiveA) {} + } + @Directive({selector: '[dirA]'}) + class DirectiveA { + constructor(siblingDir: DirectiveB) {} + } + @Component({template: '
'}) + class MyComp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]}); + expect(() => TestBed.createComponent(MyComp)).toThrowError(/Circular dep for/); + }); + + onlyInIvy('Ivy has different error message for circular dependency') + .it('should throw if directive tries to inject itself', () => { + @Directive({selector: '[dirA]'}) + class DirectiveA { + constructor(siblingDir: DirectiveA) {} + } + @Component({template: '
'}) + class MyComp { + } + TestBed.configureTestingModule({declarations: [DirectiveA, DirectiveB, MyComp]}); + expect(() => TestBed.createComponent(MyComp)).toThrowError(/Circular dep for/); + }); + }); + + describe('service injection', () => { + + it('should create instance even when no injector present', () => { + @Injectable({providedIn: 'root'}) + class MyService { + value = 'MyService'; + } + @Component({template: '
{{myService.value}}
'}) + class MyComp { + constructor(public myService: MyService) {} + } + TestBed.configureTestingModule({declarations: [MyComp]}); + const fixture = TestBed.createComponent(MyComp); + fixture.detectChanges(); + + const divElement = fixture.nativeElement.querySelector('div'); + expect(divElement.textContent).toEqual('MyService'); + }); + }); + describe('Special tokens', () => { describe('Injector', () => { diff --git a/packages/core/test/render3/di_spec.ts b/packages/core/test/render3/di_spec.ts index 0426e06777..3aa1d037df 100644 --- a/packages/core/test/render3/di_spec.ts +++ b/packages/core/test/render3/di_spec.ts @@ -6,14 +6,14 @@ * found in the LICENSE file at https://angular.io/license */ -import {ChangeDetectorRef, Host, Inject, InjectFlags, Injector, Optional, Renderer2, Self, SkipSelf, TemplateRef, ViewContainerRef, ɵɵdefineInjectable, ɵɵdefineInjector} from '@angular/core'; +import {ChangeDetectorRef, Host, Inject, InjectFlags, Injector, Optional, Renderer2, Self, SkipSelf, ViewContainerRef, ɵɵdefineInjector} from '@angular/core'; import {createLView, createNodeAtIndex, createTView} from '@angular/core/src/render3/instructions/shared'; import {ComponentType, RenderFlags} from '@angular/core/src/render3/interfaces/definition'; import {createInjector} from '../../src/di/r3_injector'; import {ɵɵdefineComponent} from '../../src/render3/definition'; import {bloomAdd, bloomHasToken, bloomHashBitOrFactory as bloomHash, getOrCreateNodeInjectorForNode} from '../../src/render3/di'; -import {ɵɵProvidersFeature, ɵɵallocHostVars, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation2, ɵɵprojection, ɵɵprojectionDef, ɵɵreference, ɵɵtemplate, ɵɵtemplateRefExtractor, ɵɵtext, ɵɵtextBinding} from '../../src/render3/index'; +import {ɵɵProvidersFeature, ɵɵbind, ɵɵcontainer, ɵɵcontainerRefreshEnd, ɵɵcontainerRefreshStart, ɵɵdefineDirective, ɵɵdirectiveInject, ɵɵelement, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵembeddedViewEnd, ɵɵembeddedViewStart, ɵɵinterpolation2, ɵɵprojection, ɵɵprojectionDef, ɵɵreference, ɵɵtemplate, ɵɵtext, ɵɵtextBinding} from '../../src/render3/index'; import {TNODE} from '../../src/render3/interfaces/injector'; import {AttributeMarker, TNodeType} from '../../src/render3/interfaces/node'; import {isProceduralRenderer} from '../../src/render3/interfaces/renderer'; @@ -26,36 +26,6 @@ import {getRendererFactory2} from './imported_renderer2'; import {ComponentFixture, createComponent, createDirective, getDirectiveOnNode, renderComponent, toHtml} from './render_util'; describe('di', () => { - describe('no dependencies', () => { - it('should create directive with no deps', () => { - class Directive { - value: string = 'Created'; - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'dir', '']], - factory: () => new Directive, - exportAs: ['dir'] - }); - } - - /**
{{ dir.value }}
*/ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dir', ''], ['dir', 'dir']); - { ɵɵtext(2); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const tmp = ɵɵreference(1) as any; - ɵɵtextBinding(2, ɵɵbind(tmp.value)); - } - }, 3, 1, [Directive]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('
Created
'); - }); - }); - describe('directive injection', () => { let log: string[] = []; @@ -73,129 +43,10 @@ describe('di', () => { beforeEach(() => log = []); - it('should create directive with intra view dependencies', () => { - class DirA { - value: string = 'DirA'; - static ngDirectiveDef = ɵɵdefineDirective( - {type: DirA, selectors: [['', 'dirA', '']], factory: () => new DirA()}); - } - - class DirC { - value: string; - constructor(a: DirA, b: DirB) { this.value = a.value + b.value; } - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirC, - selectors: [['', 'dirC', '']], - factory: () => new DirC(ɵɵdirectiveInject(DirA), ɵɵdirectiveInject(DirB)), - exportAs: ['dirC'] - }); - } - - /** - *
- * {{ dir.value }} - *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dirA', '']); - { - ɵɵelementStart(1, 'span', ['dirB', '', 'dirC', ''], ['dir', 'dirC']); - { ɵɵtext(3); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const tmp = ɵɵreference(2) as any; - ɵɵtextBinding(3, ɵɵbind(tmp.value)); - } - }, 4, 1, [DirA, DirB, DirC]); - - const fixture = new ComponentFixture(App); - expect(fixture.html).toEqual('
DirADirB
'); - }); - - it('should instantiate injected directives in dependency order', () => { - class DirA { - constructor(dir: DirB) { log.push(`DirA (dep: ${dir.value})`); } - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirA', '']], - type: DirA, - factory: () => new DirA(ɵɵdirectiveInject(DirB)), - }); - } - - /**
*/ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dirA', '', 'dirB', '']); - } - }, 1, 0, [DirA, DirB]); - - new ComponentFixture(App); - expect(log).toEqual(['DirB', 'DirA (dep: DirB)']); - }); - - it('should fallback to the module injector', () => { - class DirA { - constructor(dir: DirB) { log.push(`DirA (dep: ${dir.value})`); } - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirA', '']], - type: DirA, - factory: () => new DirA(ɵɵdirectiveInject(DirB)), - }); - } - - // `
` - // - dirB is know to the node injectors (it uses the diPublic feature) - // - then when dirA tries to inject dirB, it will check the node injector first tree - // - if not found, it will check the module injector tree - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dirB', '']); - ɵɵelement(1, 'div', ['dirA', '']); - } - }, 2, 0, [DirA, DirB]); - - const fakeModuleInjector: any = { - get: function(token: any) { - const value = token === DirB ? 'module' : 'fail'; - return {value: value}; - } - }; - - new ComponentFixture(App, {injector: fakeModuleInjector}); - expect(log).toEqual(['DirB', 'DirA (dep: module)']); - }); - - it('should instantiate injected directives before components', () => { - class Comp { - constructor(dir: DirB) { log.push(`Comp (dep: ${dir.value})`); } - - static ngComponentDef = ɵɵdefineComponent({ - selectors: [['comp']], - type: Comp, - consts: 0, - vars: 0, - factory: () => new Comp(ɵɵdirectiveInject(DirB)), - template: (rf: RenderFlags, ctx: Comp) => {} - }); - } - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dirB', '']); - } - }, 1, 0, [Comp, DirB]); - - new ComponentFixture(App); - expect(log).toEqual(['DirB', 'Comp (dep: DirB)']); - }); - + /** + * This test needs to be moved to acceptance/di_spec.ts + * when Ivy compiler supports inline views. + */ it('should inject directives in the correct order in a for loop', () => { class DirA { constructor(dir: DirB) { log.push(`DirA (dep: ${dir.value})`); } @@ -235,184 +86,6 @@ describe('di', () => { ['DirB', 'DirA (dep: DirB)', 'DirB', 'DirA (dep: DirB)', 'DirB', 'DirA (dep: DirB)']); }); - it('should instantiate directives with multiple out-of-order dependencies', () => { - class DirA { - value = 'DirA'; - constructor() { log.push(this.value); } - - static ngDirectiveDef = ɵɵdefineDirective( - {selectors: [['', 'dirA', '']], type: DirA, factory: () => new DirA()}); - } - - class DirB { - constructor(dirA: DirA, dirC: DirC) { - log.push(`DirB (deps: ${dirA.value} and ${dirC.value})`); - } - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirB', '']], - type: DirB, - factory: () => new DirB(ɵɵdirectiveInject(DirA), ɵɵdirectiveInject(DirC)) - }); - } - - class DirC { - value = 'DirC'; - constructor() { log.push(this.value); } - - static ngDirectiveDef = ɵɵdefineDirective( - {selectors: [['', 'dirC', '']], type: DirC, factory: () => new DirC()}); - } - - /**
*/ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dirA', '', 'dirB', '', 'dirC', '']); - } - }, 1, 0, [DirA, DirB, DirC]); - - new ComponentFixture(App); - expect(log).toEqual(['DirA', 'DirC', 'DirB (deps: DirA and DirC)']); - }); - - it('should instantiate in the correct order for complex case', () => { - class Comp { - constructor(dir: DirD) { log.push(`Comp (dep: ${dir.value})`); } - - static ngComponentDef = ɵɵdefineComponent({ - selectors: [['comp']], - type: Comp, - consts: 0, - vars: 0, - factory: () => new Comp(ɵɵdirectiveInject(DirD)), - template: (ctx: any, fm: boolean) => {} - }); - } - - class DirA { - value = 'DirA'; - constructor(dir: DirC) { log.push(`DirA (dep: ${dir.value})`); } - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirA', '']], - type: DirA, - factory: () => new DirA(ɵɵdirectiveInject(DirC)) - }); - } - - class DirC { - value = 'DirC'; - constructor(dir: DirB) { log.push(`DirC (dep: ${dir.value})`); } - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirC', '']], - type: DirC, - factory: () => new DirC(ɵɵdirectiveInject(DirB)) - }); - } - - class DirD { - value = 'DirD'; - constructor(dir: DirA) { log.push(`DirD (dep: ${dir.value})`); } - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirD', '']], - type: DirD, - factory: () => new DirD(ɵɵdirectiveInject(DirA)) - }); - } - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dirA', '', 'dirB', '', 'dirC', '', 'dirD', '']); - } - }, 1, 0, [Comp, DirA, DirB, DirC, DirD]); - - new ComponentFixture(App); - expect(log).toEqual( - ['DirB', 'DirC (dep: DirB)', 'DirA (dep: DirC)', 'DirD (dep: DirA)', 'Comp (dep: DirD)']); - }); - - it('should instantiate in correct order with mixed parent and peer dependencies', () => { - class DirA { - constructor(dirB: DirB, app: App) { - log.push(`DirA (deps: ${dirB.value} and ${app.value})`); - } - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirA', '']], - type: DirA, - factory: () => new DirA(ɵɵdirectiveInject(DirB), ɵɵdirectiveInject(App)), - }); - } - - class App { - value = 'App'; - - static ngComponentDef = ɵɵdefineComponent({ - selectors: [['app']], - type: App, - factory: () => new App(), - consts: 1, - vars: 0, - /**
*/ - template: (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dirA', '', 'dirB', '', 'dirC', 'dirC']); - } - }, - directives: [DirA, DirB] - }); - } - - new ComponentFixture(App); - expect(log).toEqual(['DirB', 'DirA (deps: DirB and App)']); - }); - - it('should not use a parent when peer dep is available', () => { - let count = 1; - - class DirA { - constructor(dirB: DirB) { log.push(`DirA (dep: DirB - ${dirB.count})`); } - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirA', '']], - type: DirA, - factory: () => new DirA(ɵɵdirectiveInject(DirB)), - }); - } - - class DirB { - count: number; - - constructor() { - log.push(`DirB`); - this.count = count++; - } - - static ngDirectiveDef = ɵɵdefineDirective( - {selectors: [['', 'dirB', '']], type: DirB, factory: () => new DirB()}); - } - - /**
*/ - const Parent = createComponent('parent', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dirA', '', 'dirB', '']); - } - }, 1, 0, [DirA, DirB]); - - /** */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'parent', ['dirB', '']); - } - }, 1, 0, [Parent, DirB]); - - new ComponentFixture(App); - expect(log).toEqual(['DirB', 'DirB', 'DirA (dep: DirB - 2)']); - }); - describe('dependencies in parent views', () => { class DirA { @@ -431,78 +104,9 @@ describe('di', () => { } /** - *
- * {{ dir.dirB.value }} - *
+ * This test needs to be moved to acceptance/di_spec.ts + * when Ivy compiler supports inline views. */ - const Comp = createComponent('comp', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dirA', ''], ['dir', 'dirA']); - { ɵɵtext(2); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const dir = ɵɵreference(1) as DirA; - ɵɵtextBinding(2, ɵɵbind(dir.dirB.value)); - } - }, 3, 1, [DirA]); - - it('should find dependencies on component hosts', () => { - /** /comp> */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dirB', '']); - } - }, 1, 0, [Comp, DirB]); - - const fixture = new ComponentFixture(App); - expect(fixture.hostElement.textContent).toEqual(`DirB`); - }); - - it('should find dependencies for directives in embedded views', () => { - - function IfTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div'); - { - ɵɵelementStart(1, 'div', ['dirA', ''], ['dir', 'dirA']); - { ɵɵtext(3); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - - if (rf & RenderFlags.Update) { - const dir = ɵɵreference(2) as DirA; - ɵɵtextBinding(3, ɵɵbind(dir.dirB.value)); - } - } - - /** - *
- *
- *
{{ dir.dirB.value }}
- *
- *
- */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dirB', '']); - { ɵɵtemplate(1, IfTemplate, 4, 1, 'div', [AttributeMarker.Template, 'ngIf']); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(1, 'ngIf', ɵɵbind(ctx.showing)); - } - }, 2, 1, [DirA, DirB, NgIf]); - - const fixture = new ComponentFixture(App); - fixture.component.showing = true; - fixture.update(); - - expect(fixture.hostElement.textContent).toEqual(`DirB`); - }); - it('should find dependencies of directives nested deeply in inline views', () => { /** *
@@ -560,293 +164,6 @@ describe('di', () => { const fixture = new ComponentFixture(App); expect(fixture.hostElement.textContent).toEqual(`DirB`); }); - - it('should find dependencies in declaration tree of ng-template (not insertion tree)', () => { - let structuralDir !: StructuralDir; - - class StructuralDir { - // @Input() - tmp !: TemplateRef; - - constructor(public vcr: ViewContainerRef) {} - - create() { this.vcr.createEmbeddedView(this.tmp); } - - static ngDirectiveDef = ɵɵdefineDirective({ - type: StructuralDir, - selectors: [['', 'structuralDir', '']], - factory: () => structuralDir = - new StructuralDir(ɵɵdirectiveInject(ViewContainerRef as any)), - inputs: {tmp: 'tmp'} - }); - } - - function FooTemplate(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dirA', ''], ['dir', 'dirA']); - { ɵɵtext(2); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const dir = ɵɵreference(1) as DirA; - ɵɵtextBinding(2, ɵɵbind(dir.dirB.value)); - } - } - - /** - *
- * - *
{{ dir.dirB.value }}
- *
- *
- * - *
- *
- * // insertion point - *
- */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', ['dirB', '', 'value', 'declaration']); - { - ɵɵtemplate( - 1, FooTemplate, 3, 1, 'ng-template', null, ['foo', ''], ɵɵtemplateRefExtractor); - } - ɵɵelementEnd(); - ɵɵelementStart(3, 'div', ['dirB', '', 'value', 'insertion']); - { ɵɵelement(4, 'div', ['structuralDir', '']); } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - const foo = ɵɵreference(2) as any; - ɵɵelementProperty(4, 'tmp', ɵɵbind(foo)); - } - }, 5, 1, [DirA, DirB, StructuralDir]); - - const fixture = new ComponentFixture(App); - structuralDir.create(); - fixture.update(); - expect(fixture.hostElement.textContent).toEqual(`declaration`); - }); - - it('should create injectors on second template pass', () => { - /** - * - * - */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'comp', ['dirB', '']); - ɵɵelement(1, 'comp', ['dirB', '']); - } - }, 2, 0, [Comp, DirB]); - - const fixture = new ComponentFixture(App); - expect(fixture.hostElement.textContent).toEqual(`DirBDirB`); - }); - - it('should create injectors and host bindings in same view', () => { - let hostBindingDir !: HostBindingDir; - - class HostBindingDir { - // @HostBinding('id') - id = 'foo'; - - static ngDirectiveDef = ɵɵdefineDirective({ - type: HostBindingDir, - selectors: [['', 'hostBindingDir', '']], - factory: () => hostBindingDir = new HostBindingDir(), - hostBindings: (rf: RenderFlags, ctx: any, elementIndex: number) => { - if (rf & RenderFlags.Create) { - ɵɵallocHostVars(1); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(elementIndex, 'id', ɵɵbind(ctx.id)); - } - } - }); - } - - let dir !: DirA; - /** - *
- *

- * {{ dir.dirB.value }} - *

- *
- */ - const App = createComponent('app', (rf: RenderFlags, ctx: any) => { - if (rf & RenderFlags.Create) { - ɵɵelementStart(0, 'div', [ - 'dirB', - '', - 'hostBindingDir', - '', - ]); - { - ɵɵelementStart(1, 'p', ['dirA', ''], ['dir', 'dirA']); - { ɵɵtext(3); } - ɵɵelementEnd(); - } - ɵɵelementEnd(); - } - if (rf & RenderFlags.Update) { - dir = ɵɵreference(2) as DirA; - ɵɵtextBinding(3, ɵɵbind(dir.dirB.value)); - } - }, 4, 1, [HostBindingDir, DirA, DirB]); - - const fixture = new ComponentFixture(App); - expect(fixture.hostElement.textContent).toEqual(`DirB`); - const hostDirEl = fixture.hostElement.querySelector('div') as HTMLElement; - expect(hostDirEl.id).toEqual('foo'); - // The injector should not be overwritten by host bindings - expect(dir.vcr.injector).toEqual(dir.injector); - - hostBindingDir.id = 'bar'; - fixture.update(); - expect(hostDirEl.id).toEqual('bar'); - }); - }); - - it('should create instance even when no injector present', () => { - class MyService { - value = 'MyService'; - static ngInjectableDef = - ɵɵdefineInjectable({providedIn: 'root', factory: () => new MyService()}); - } - - class MyComponent { - constructor(public myService: MyService) {} - static ngComponentDef = ɵɵdefineComponent({ - type: MyComponent, - selectors: [['my-component']], - consts: 1, - vars: 1, - factory: () => new MyComponent(ɵɵdirectiveInject(MyService)), - template: function(rf: RenderFlags, ctx: MyComponent) { - if (rf & RenderFlags.Create) { - ɵɵtext(0); - } - if (rf & RenderFlags.Update) { - ɵɵtextBinding(0, ɵɵbind(ctx.myService.value)); - } - } - }); - } - - const fixture = new ComponentFixture(MyComponent); - fixture.update(); - expect(fixture.html).toEqual('MyService'); - }); - - it('should throw if directive is not found anywhere', () => { - class Dir { - constructor(siblingDir: OtherDir) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dir', '']], - type: Dir, - factory: () => new Dir(ɵɵdirectiveInject(OtherDir)) - }); - } - - class OtherDir { - static ngDirectiveDef = ɵɵdefineDirective( - {selectors: [['', 'other', '']], type: OtherDir, factory: () => new OtherDir()}); - } - - /**
*/ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - }, 1, 0, [Dir, OtherDir]); - - expect(() => new ComponentFixture(App)).toThrowError(/Injector: NOT_FOUND \[OtherDir\]/); - }); - - it('should throw if directive is not found in ancestor tree', () => { - class Dir { - constructor(siblingDir: OtherDir) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dir', '']], - type: Dir, - factory: () => new Dir(ɵɵdirectiveInject(OtherDir)) - }); - } - - class OtherDir { - static ngDirectiveDef = ɵɵdefineDirective( - {selectors: [['', 'other', '']], type: OtherDir, factory: () => new OtherDir()}); - } - - /** - *
- *
- */ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['other', '']); - ɵɵelement(1, 'div', ['dir', '']); - } - }, 2, 0, [Dir, OtherDir]); - - expect(() => new ComponentFixture(App)).toThrowError(/Injector: NOT_FOUND \[OtherDir\]/); - }); - - - it('should throw if directives try to inject each other', () => { - class DirA { - constructor(dir: DirB) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirA', '']], - type: DirA, - factory: () => new DirA(ɵɵdirectiveInject(DirB)) - }); - } - - class DirB { - constructor(dir: DirA) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dirB', '']], - type: DirB, - factory: () => new DirB(ɵɵdirectiveInject(DirA)) - }); - } - - /**
*/ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dirA', '', 'dirB', '']); - } - }, 1, 0, [DirA, DirB]); - - expect(() => new ComponentFixture(App)).toThrowError(/Circular dep for/); - }); - - it('should throw if directive tries to inject itself', () => { - class Dir { - constructor(dir: Dir) {} - - static ngDirectiveDef = ɵɵdefineDirective({ - selectors: [['', 'dir', '']], - type: Dir, - factory: () => new Dir(ɵɵdirectiveInject(Dir)) - }); - } - - /**
*/ - const App = createComponent('app', function(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelement(0, 'div', ['dir', '']); - } - }, 1, 0, [Dir]); - - expect(() => new ComponentFixture(App)).toThrowError(/Circular dep for/); }); describe('flags', () => {