diff --git a/packages/core/test/acceptance/directive_spec.ts b/packages/core/test/acceptance/directive_spec.ts index 6764573e7b..0fc8027586 100644 --- a/packages/core/test/acceptance/directive_spec.ts +++ b/packages/core/test/acceptance/directive_spec.ts @@ -6,7 +6,9 @@ * found in the LICENSE file at https://angular.io/license */ -import {Component, Directive} from '@angular/core'; +import {CommonModule} from '@angular/common'; +import {Component, Directive, EventEmitter, Output, TemplateRef, ViewChild, ViewContainerRef} from '@angular/core'; +import {Input} from '@angular/core/src/metadata'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; @@ -16,6 +18,7 @@ describe('directives', () => { @Directive({selector: 'ng-template[test]'}) class TestDirective { + constructor(public templateRef: TemplateRef) {} } @Directive({selector: '[title]'}) @@ -26,6 +29,77 @@ describe('directives', () => { class TestComponent { } + it('should match directives with attribute selectors on bindings', () => { + @Directive({selector: '[test]'}) + class TestDir { + testValue: boolean|undefined; + + /** Setter to assert that a binding is not invoked with stringified attribute value */ + @Input() + set test(value: any) { + // Assert that the binding is processed correctly. The property should be set + // to a "false" boolean and never to the "false" string literal. + this.testValue = value; + if (value !== false) { + fail('Should only be called with a false Boolean value, got a non-falsy value'); + } + } + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate(TestComponent, ``); + + const fixture = TestBed.createComponent(TestComponent); + const testDir = fixture.debugElement.query(By.directive(TestDir)).injector.get(TestDir); + const spanEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + // the "test" attribute should not be reflected in the DOM as it is here only + // for directive matching purposes + expect(spanEl.hasAttribute('test')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('fade'); + expect(testDir.testValue).toBe(false); + }); + + it('should not accidentally set inputs from attributes extracted from bindings / outputs', + () => { + @Directive({selector: '[test]'}) + class TestDir { + @Input() prop1: boolean|undefined; + @Input() prop2: boolean|undefined; + testValue: boolean|undefined; + + /** Setter to assert that a binding is not invoked with stringified attribute value */ + @Input() + set test(value: any) { + // Assert that the binding is processed correctly. The property should be set + // to a "false" boolean and never to the "false" string literal. + this.testValue = value; + if (value !== false) { + fail('Should only be called with a false Boolean value, got a non-falsy value'); + } + } + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate( + TestComponent, + ``); + + const fixture = TestBed.createComponent(TestComponent); + const testDir = fixture.debugElement.query(By.directive(TestDir)).injector.get(TestDir); + const spanEl = fixture.nativeElement.children[0]; + fixture.detectChanges(); + + // the "test" attribute should not be reflected in the DOM as it is here only + // for directive matching purposes + expect(spanEl.hasAttribute('test')).toBe(false); + expect(spanEl.hasAttribute('prop1')).toBe(false); + expect(spanEl.hasAttribute('prop2')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('fade'); + expect(testDir.testValue).toBe(false); + }); + it('should match directives on ng-template', () => { TestBed.configureTestingModule({declarations: [TestComponent, TestDirective]}); TestBed.overrideTemplate(TestComponent, ``); @@ -34,6 +108,8 @@ describe('directives', () => { const nodesWithDirective = fixture.debugElement.queryAllNodes(By.directive(TestDirective)); expect(nodesWithDirective.length).toBe(1); + expect(nodesWithDirective[0].injector.get(TestDirective).templateRef instanceof TemplateRef) + .toBe(true); }); it('should match directives on ng-template created by * syntax', () => { @@ -46,6 +122,32 @@ describe('directives', () => { expect(nodesWithDirective.length).toBe(1); }); + it('should match directives on ', () => { + @Directive({selector: 'ng-container[directiveA]'}) + class DirectiveA { + constructor(public viewContainerRef: ViewContainerRef) {} + } + + @Component({ + selector: 'my-component', + template: ` + + Some content + ` + }) + class MyComponent { + visible = true; + } + + TestBed.configureTestingModule( + {declarations: [MyComponent, DirectiveA], imports: [CommonModule]}); + const fixture = TestBed.createComponent(MyComponent); + fixture.detectChanges(); + const directiveA = fixture.debugElement.query(By.css('span')).injector.get(DirectiveA); + + expect(directiveA.viewContainerRef).toBeTruthy(); + }); + it('should match directives on i18n-annotated attributes', () => { TestBed.configureTestingModule({declarations: [TestComponent, TitleDirective]}); TestBed.overrideTemplate(TestComponent, ` @@ -82,6 +184,73 @@ describe('directives', () => { expect(nodesWithDirective.length).toBe(0); }); + it('should match directives with attribute selectors on outputs', () => { + @Directive({selector: '[out]'}) + class TestDir { + @Output() out = new EventEmitter(); + } + + TestBed.configureTestingModule({declarations: [TestComponent, TestDir]}); + TestBed.overrideTemplate(TestComponent, ``); + + const fixture = TestBed.createComponent(TestComponent); + const spanEl = fixture.nativeElement.children[0]; + + // "out" should not be part of reflected attributes + expect(spanEl.hasAttribute('out')).toBe(false); + expect(spanEl.getAttribute('class')).toBe('span'); + expect(fixture.debugElement.query(By.directive(TestDir))).toBeTruthy(); + }); + + }); + + describe('outputs', () => { + @Directive({selector: '[out]'}) + class TestDir { + @Output() out = new EventEmitter(); + } + + it('should allow outputs of directive on ng-template', () => { + @Component({template: ``}) + class TestComp { + @ViewChild(TestDir) testDir: TestDir|undefined; + value = false; + } + + TestBed.configureTestingModule({declarations: [TestComp, TestDir]}); + const fixture = TestBed.createComponent(TestComp); + fixture.detectChanges(); + + expect(fixture.componentInstance.testDir).toBeTruthy(); + expect(fixture.componentInstance.value).toBe(false); + + fixture.componentInstance.testDir !.out.emit(); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBe(true); + }); + + it('should allow outputs of directive on ng-container', () => { + @Component({ + template: ` + + Hello + ` + }) + class TestComp { + value = false; + } + + TestBed.configureTestingModule({declarations: [TestComp, TestDir]}); + const fixture = TestBed.createComponent(TestComp); + const testDir = fixture.debugElement.query(By.css('span')).injector.get(TestDir); + + expect(fixture.componentInstance.value).toBe(false); + + testDir.out.emit(); + fixture.detectChanges(); + expect(fixture.componentInstance.value).toBeTruthy(); + }); + }); }); diff --git a/packages/core/test/render3/directive_spec.ts b/packages/core/test/render3/directive_spec.ts deleted file mode 100644 index b5bd88b269..0000000000 --- a/packages/core/test/render3/directive_spec.ts +++ /dev/null @@ -1,316 +0,0 @@ -/** - * @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 {EventEmitter, TemplateRef, ViewContainerRef} from '@angular/core'; - -import {AttributeMarker, RenderFlags, ɵɵdefineComponent, ɵɵdefineDirective, ɵɵdirectiveInject} from '../../src/render3/index'; -import {ɵɵbind, ɵɵelement, ɵɵelementContainerEnd, ɵɵelementContainerStart, ɵɵelementEnd, ɵɵelementProperty, ɵɵelementStart, ɵɵlistener, ɵɵtemplate, ɵɵtext} from '../../src/render3/instructions/all'; - -import {NgIf} from './common_with_def'; -import {ComponentFixture, TemplateFixture, createComponent} from './render_util'; - -describe('directive', () => { - - describe('selectors', () => { - - it('should match directives with attribute selectors on bindings', () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'test', '']], - factory: () => directiveInstance = new Directive, - inputs: {test: 'test', other: 'other'} - }); - - // TODO(issue/24571): remove '!'. - testValue !: boolean; - // TODO(issue/24571): remove '!'. - other !: boolean; - - /** - * A setter to assert that a binding is not invoked with stringified attribute value - */ - set test(value: any) { - // if a binding is processed correctly we should only be invoked with a false Boolean - // and never with the "false" string literal - this.testValue = value; - if (value !== false) { - fail('Should only be called with a false Boolean value, got a non-falsy value'); - } - } - } - - /** - * - */ - function createTemplate() { - // using 2 bindings to show example shape of attributes array - ɵɵelement(0, 'span', ['class', 'fade', AttributeMarker.Bindings, 'test', 'other']); - } - - function updateTemplate() { ɵɵelementProperty(0, 'test', ɵɵbind(false)); } - - const fixture = new TemplateFixture(createTemplate, updateTemplate, 1, 1, [Directive]); - - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html).toEqual(''); - expect(directiveInstance !.testValue).toBe(false); - }); - - it('should not accidentally set inputs from attributes extracted from bindings / outputs', - () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'test', '']], - factory: () => directiveInstance = new Directive, - inputs: {test: 'test', prop1: 'prop1', prop2: 'prop2'} - }); - - // TODO(issue/24571): remove '!'. - prop1 !: boolean; - // TODO(issue/24571): remove '!'. - prop2 !: boolean; - // TODO(issue/24571): remove '!'. - testValue !: boolean; - - - /** - * A setter to assert that a binding is not invoked with stringified attribute value - */ - set test(value: any) { - // if a binding is processed correctly we should only be invoked with a false Boolean - // and never with the "false" string literal - this.testValue = value; - if (value !== false) { - fail('Should only be called with a false Boolean value, got a non-falsy value'); - } - } - } - - /** - * - */ - function createTemplate() { - // putting name (test) in the "usual" value position - ɵɵelement( - 0, 'span', ['class', 'fade', AttributeMarker.Bindings, 'prop1', 'test', 'prop2']); - } - - function updateTemplate() { - ɵɵelementProperty(0, 'prop1', ɵɵbind(true)); - ɵɵelementProperty(0, 'test', ɵɵbind(false)); - ɵɵelementProperty(0, 'prop2', ɵɵbind(true)); - } - - const fixture = new TemplateFixture(createTemplate, updateTemplate, 1, 3, [Directive]); - - // the "test" attribute should not be reflected in the DOM as it is here only for directive - // matching purposes - expect(fixture.html).toEqual(''); - expect(directiveInstance !.testValue).toBe(false); - }); - - it('should match directives on ', () => { - /** - * @Directive({ - * selector: 'ng-template[directiveA]' - * }) - * export class DirectiveA { - * constructor(public templateRef: TemplateRef) {} - * } - */ - let tmplRef: any; - class DirectiveA { - constructor(public templateRef: any) { tmplRef = templateRef; } - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveA, - selectors: [['ng-template', 'directiveA', '']], - factory: () => new DirectiveA(ɵɵdirectiveInject(TemplateRef as any)) - }); - } - - function MyComponent_ng_template_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtext(0, 'Some content'); - } - } - class MyComponent { - static ngComponentDef = ɵɵdefineComponent({ - type: MyComponent, - selectors: [['my-component']], - factory: () => new MyComponent(), - consts: 1, - vars: 0, - // Some content - template: function MyComponent_Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, MyComponent_ng_template_Template_0, 1, 0, 'ng-template', ['directiveA', '']); - } - }, - directives: [DirectiveA] - }); - } - - new ComponentFixture(MyComponent); - expect(tmplRef instanceof TemplateRef).toBeTruthy(); - }); - - it('should match directives on ', () => { - /** - * @Directive({ - * selector: 'ng-container[directiveA]' - * }) - * export class DirectiveA { - * constructor(public vcRef: ViewContainerRef) {} - * } - */ - let vcRef: any; - class DirectiveA { - constructor(public viewContainerRef: any) { vcRef = viewContainerRef; } - static ngDirectiveDef = ɵɵdefineDirective({ - type: DirectiveA, - selectors: [['ng-container', 'directiveA', '']], - factory: () => new DirectiveA(ɵɵdirectiveInject(ViewContainerRef as any)) - }); - } - - function MyComponent_ng_container_Template_0(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0, ['directiveA', '']); - ɵɵtext(1, 'Some content'); - ɵɵelementContainerEnd(); - } - } - class MyComponent { - visible = true; - - static ngComponentDef = ɵɵdefineComponent({ - type: MyComponent, - selectors: [['my-component']], - factory: () => new MyComponent(), - consts: 1, - vars: 1, - // Some content - template: function MyComponent_Template(rf: RenderFlags, ctx: any) { - if (rf & RenderFlags.Create) { - ɵɵtemplate( - 0, MyComponent_ng_container_Template_0, 2, 0, 'ng-container', - ['directiveA', '', AttributeMarker.Template, 'ngIf']); - } - if (rf & RenderFlags.Update) { - ɵɵelementProperty(0, 'ngIf', ɵɵbind(ctx.visible)); - } - }, - directives: [DirectiveA, NgIf] - }); - } - - new ComponentFixture(MyComponent); - expect(vcRef instanceof ViewContainerRef).toBeTruthy(); - }); - - it('should match directives with attribute selectors on outputs', () => { - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'out', '']], - factory: () => directiveInstance = new Directive, - outputs: {out: 'out'} - }); - - out = new EventEmitter(); - } - - /** - * - */ - function createTemplate() { - ɵɵelementStart(0, 'span', [AttributeMarker.Bindings, 'out']); - { ɵɵlistener('out', () => {}); } - ɵɵelementEnd(); - } - - const fixture = new TemplateFixture(createTemplate, () => {}, 1, 0, [Directive]); - - // "out" should not be part of reflected attributes - expect(fixture.html).toEqual(''); - expect(directiveInstance !).not.toBeUndefined(); - }); - }); - - describe('outputs', () => { - - let directiveInstance: Directive; - - class Directive { - static ngDirectiveDef = ɵɵdefineDirective({ - type: Directive, - selectors: [['', 'out', '']], - factory: () => directiveInstance = new Directive, - outputs: {out: 'out'} - }); - - out = new EventEmitter(); - } - - it('should allow outputs of directive on ng-template', () => { - /** - * - */ - const Cmpt = createComponent('Cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵtemplate(0, null, 0, 0, 'ng-template', [AttributeMarker.Bindings, 'out']); - ɵɵlistener('out', () => { ctx.value = true; }); - } - }, 1, 0, [Directive]); - - const fixture = new ComponentFixture(Cmpt); - - expect(directiveInstance !).not.toBeUndefined(); - expect(fixture.component.value).toBeFalsy(); - - directiveInstance !.out.emit(); - fixture.update(); - expect(fixture.component.value).toBeTruthy(); - }); - - it('should allow outputs of directive on ng-container', () => { - /** - * - */ - const Cmpt = createComponent('Cmpt', function(rf: RenderFlags, ctx: {value: any}) { - if (rf & RenderFlags.Create) { - ɵɵelementContainerStart(0, [AttributeMarker.Bindings, 'out']); - { - ɵɵlistener('out', () => { ctx.value = true; }); - } - ɵɵelementContainerEnd(); - } - }, 1, 0, [Directive]); - - const fixture = new ComponentFixture(Cmpt); - - expect(directiveInstance !).not.toBeUndefined(); - expect(fixture.component.value).toBeFalsy(); - - directiveInstance !.out.emit(); - fixture.update(); - expect(fixture.component.value).toBeTruthy(); - }); - - }); -});