/** * @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 {CommonModule} from '@angular/common'; import {Component, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, OnInit, Output, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core'; import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode'; import {TestBed} from '@angular/core/testing'; import {By} from '@angular/platform-browser'; import {expect} from '@angular/platform-browser/testing/src/matchers'; import {onlyInIvy} from '@angular/private/testing'; describe('acceptance integration tests', () => { function stripHtmlComments(str: string) { return str.replace(//g, ''); } describe('render', () => { it('should render basic template', () => { @Component({template: 'Greetings'}) class App { } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); expect(fixture.nativeElement.innerHTML).toEqual('Greetings'); }); it('should render and update basic "Hello, World" template', () => { ngDevModeResetPerfCounters(); @Component({template: '

Hello, {{name}}!

'}) class App { name = ''; } onlyInIvy('perf counters').expectPerfCounters({ tView: 0, tNode: 0, }); TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.name = 'World'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('

Hello, World!

'); onlyInIvy('perf counters').expectPerfCounters({ tView: 2, // Host view + App tNode: 4, // Host Node + App Node + + #text }); fixture.componentInstance.name = 'New World'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('

Hello, New World!

'); // Assert that the tView/tNode count does not increase (they are correctly cached) onlyInIvy('perf counters').expectPerfCounters({ tView: 2, tNode: 4, }); }); }); describe('ng-container', () => { it('should insert as a child of a regular element', () => { @Component( {template: '
before|Greetings|after
'}) class App { } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); // Strip comments since VE and Ivy put them in different places. expect(stripHtmlComments(fixture.nativeElement.innerHTML)) .toBe('
before|Greetings|after
'); }); it('should add and remove DOM nodes when ng-container is a child of a regular element', () => { @Component({ template: '
content
' }) class App { render = false; } TestBed.configureTestingModule({declarations: [App], imports: [CommonModule]}); const fixture = TestBed.createComponent(App); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); fixture.componentInstance.render = true; fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
content
'); fixture.componentInstance.render = false; fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); }); it('should add and remove DOM nodes when ng-container is a child of an embedded view', () => { @Component({template: 'content'}) class App { render = false; } TestBed.configureTestingModule({declarations: [App], imports: [CommonModule]}); const fixture = TestBed.createComponent(App); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); fixture.componentInstance.render = true; fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('content'); fixture.componentInstance.render = false; fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); }); // https://stackblitz.com/edit/angular-tfhcz1?file=src%2Fapp%2Fapp.component.ts it('should add and remove DOM nodes when ng-container is a child of a delayed embedded view', () => { @Directive({selector: '[testDirective]'}) class TestDirective { constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } clear() { this._vcRef.clear(); } } @Component({ template: 'content' }) class App { @ViewChild(TestDirective) testDirective !: TestDirective; } TestBed.configureTestingModule({declarations: [App, TestDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); fixture.componentInstance.testDirective.createAndInsert(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('content'); fixture.componentInstance.testDirective.clear(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); }); it('should render at the component view root', () => { @Component( {selector: 'test-cmpt', template: 'component template'}) class TestCmpt { } @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [App, TestCmpt]}); const fixture = TestBed.createComponent(App); expect(stripHtmlComments(fixture.nativeElement.innerHTML)) .toBe('component template'); }); it('should render inside another ng-container', () => { @Component({ selector: 'test-cmpt', template: 'content' }) class TestCmpt { } @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [App, TestCmpt]}); const fixture = TestBed.createComponent(App); expect(stripHtmlComments(fixture.nativeElement.innerHTML)) .toBe('content'); }); it('should render inside another ng-container at the root of a delayed view', () => { @Directive({selector: '[testDirective]'}) class TestDirective { constructor(private _tplRef: TemplateRef, private _vcRef: ViewContainerRef) {} createAndInsert() { this._vcRef.insert(this._tplRef.createEmbeddedView({})); } clear() { this._vcRef.clear(); } } @Component({ template: 'content' }) class App { @ViewChild(TestDirective) testDirective !: TestDirective; } TestBed.configureTestingModule({declarations: [App, TestDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); fixture.componentInstance.testDirective.createAndInsert(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('content'); fixture.componentInstance.testDirective.createAndInsert(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe('contentcontent'); fixture.componentInstance.testDirective.clear(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toBe(''); }); it('should support directives and inject ElementRef', () => { @Directive({selector: '[dir]'}) class TestDirective { constructor(public elRef: ElementRef) {} } @Component({template: '
'}) class App { @ViewChild(TestDirective) testDirective !: TestDirective; } TestBed.configureTestingModule({declarations: [App, TestDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
'); expect(fixture.componentInstance.testDirective.elRef.nativeElement.nodeType) .toBe(Node.COMMENT_NODE); }); it('should support ViewContainerRef when ng-container is at the root of a view', () => { @Directive({selector: '[dir]'}) class TestDirective { @Input() contentTpl: TemplateRef<{}>|null = null; constructor(private _vcRef: ViewContainerRef) {} insertView() { this._vcRef.createEmbeddedView(this.contentTpl as TemplateRef<{}>); } clear() { this._vcRef.clear(); } } @Component({ template: 'Content' }) class App { @ViewChild(TestDirective) testDirective !: TestDirective; } TestBed.configureTestingModule({declarations: [App, TestDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); fixture.componentInstance.testDirective.insertView(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('Content'); fixture.componentInstance.testDirective.clear(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); }); it('should support ViewContainerRef on inside ', () => { @Directive({selector: '[dir]'}) class TestDirective { constructor(private _tplRef: TemplateRef<{}>, private _vcRef: ViewContainerRef) {} insertView() { this._vcRef.createEmbeddedView(this._tplRef); } clear() { this._vcRef.clear(); } } @Component({template: 'Content'}) class App { @ViewChild(TestDirective) testDirective !: TestDirective; } TestBed.configureTestingModule({declarations: [App, TestDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); fixture.componentInstance.testDirective.insertView(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('Content'); fixture.componentInstance.testDirective.clear(); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual(''); }); it('should not set any attributes', () => { @Component({template: '
'}) class App { } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripHtmlComments(fixture.nativeElement.innerHTML)).toEqual('
'); }); }); describe('text bindings', () => { it('should render "undefined" as ""', () => { @Component({template: '{{name}}'}) class App { name: string|undefined = 'benoit'; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('benoit'); fixture.componentInstance.name = undefined; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(''); }); it('should render "null" as ""', () => { @Component({template: '{{name}}'}) class App { name: string|null = 'benoit'; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('benoit'); fixture.componentInstance.name = null; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(''); }); }); describe('ngNonBindable handling', () => { function stripNgNonBindable(str: string) { return str.replace(/ ngnonbindable=""/i, ''); } it('should keep local ref for host element', () => { @Component({ template: ` Hello {{ name }}! {{ myRef.id }} ` }) class App { } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) .toEqual('Hello {{ name }}! my-id '); }); it('should invoke directives for host element', () => { let directiveInvoked: boolean = false; @Directive({selector: '[directive]'}) class TestDirective implements OnInit { ngOnInit() { directiveInvoked = true; } } @Component({ template: ` Hello {{ name }}! ` }) class App { name = 'World'; } TestBed.configureTestingModule({declarations: [App, TestDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) .toEqual('Hello {{ name }}!'); expect(directiveInvoked).toEqual(true); }); it('should not invoke directives for nested elements', () => { let directiveInvoked: boolean = false; @Directive({selector: '[directive]'}) class TestDirective implements OnInit { ngOnInit() { directiveInvoked = true; } } @Component({ template: ` Hello {{ name }}! ` }) class App { name = 'World'; } TestBed.configureTestingModule({declarations: [App, TestDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(stripNgNonBindable(fixture.nativeElement.innerHTML)) .toEqual('Hello {{ name }}!'); expect(directiveInvoked).toEqual(false); }); }); describe('Siblings update', () => { it('should handle a flat list of static/bound text nodes', () => { @Component({template: 'Hello {{name}}!'}) class App { name = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.name = 'world'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); fixture.componentInstance.name = 'monde'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('Hello monde!'); }); it('should handle a list of static/bound text nodes as element children', () => { @Component({template: 'Hello {{name}}!'}) class App { name = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.name = 'world'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); fixture.componentInstance.name = 'mundo'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('Hello mundo!'); }); it('should render/update text node as a child of a deep list of elements', () => { @Component({template: 'Hello {{name}}!'}) class App { name = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.name = 'world'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('Hello world!'); fixture.componentInstance.name = 'mundo'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('Hello mundo!'); }); it('should update 2 sibling elements', () => { @Component({template: ''}) class App { id = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.id = 'foo'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toEqual(''); fixture.componentInstance.id = 'bar'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toEqual(''); }); it('should handle sibling text node after element with child text node', () => { @Component({template: '

hello

{{name}}'}) class App { name = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.name = 'world'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('

hello

world'); fixture.componentInstance.name = 'mundo'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('

hello

mundo'); }); }); describe('basic components', () => { @Component({selector: 'todo', template: '

Todo{{value}}

'}) class TodoComponent { value = ' one'; } it('should support a basic component template', () => { @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [App, TodoComponent]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('

Todo one

'); }); it('should support a component template with sibling', () => { @Component({template: 'two'}) class App { } TestBed.configureTestingModule({declarations: [App, TodoComponent]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('

Todo one

two'); }); it('should support a component template with component sibling', () => { @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [App, TodoComponent]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toEqual('

Todo one

Todo one

'); }); it('should support a component with binding on host element', () => { @Component({selector: 'todo', template: '{{title}}'}) class TodoComponentHostBinding { @HostBinding() title = 'one'; } @Component({template: ''}) class App { @ViewChild(TodoComponentHostBinding) todoComponentHostBinding !: TodoComponentHostBinding; } TestBed.configureTestingModule({declarations: [App, TodoComponentHostBinding]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('one'); fixture.componentInstance.todoComponentHostBinding.title = 'two'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('two'); }); it('should support root component with host attribute', () => { @Component({selector: 'host-attr-comp', template: '', host: {'role': 'button'}}) class HostAttributeComp { } TestBed.configureTestingModule({declarations: [HostAttributeComp]}); const fixture = TestBed.createComponent(HostAttributeComp); fixture.detectChanges(); expect(fixture.nativeElement.getAttribute('role')).toEqual('button'); }); it('should support component with bindings in template', () => { @Component({selector: 'comp', template: '

{{ name }}

'}) class MyComp { name = 'Bess'; } @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [App, MyComp]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual('

Bess

'); }); it('should support a component with sub-views', () => { @Component({selector: 'comp', template: '
text
'}) class MyComp { @Input() condition !: boolean; } @Component({template: ''}) class App { condition = false; } TestBed.configureTestingModule({declarations: [App, MyComp], imports: [CommonModule]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const compElement = fixture.nativeElement.querySelector('comp'); fixture.componentInstance.condition = true; fixture.detectChanges(); expect(stripHtmlComments(compElement.innerHTML)).toEqual('
text
'); fixture.componentInstance.condition = false; fixture.detectChanges(); expect(stripHtmlComments(compElement.innerHTML)).toEqual(''); }); }); describe('element bindings', () => { describe('elementAttribute', () => { it('should support attribute bindings', () => { @Component({template: ''}) class App { title: string|null = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.title = 'Hello'; fixture.detectChanges(); // initial binding expect(fixture.nativeElement.innerHTML).toEqual(''); // update binding fixture.componentInstance.title = 'Hi!'; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(''); // remove attribute fixture.componentInstance.title = null; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(''); }); it('should stringify values used attribute bindings', () => { @Component({template: ''}) class App { title: any; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.title = NaN; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(''); fixture.componentInstance.title = {toString: () => 'Custom toString'}; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML) .toEqual(''); }); it('should update bindings', () => { @Component({ template: [ 'a:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[14]}}{{c[15]}}{{c[16]}}', 'a0:{{c[1]}}', 'a1:{{c[0]}}{{c[1]}}{{c[16]}}', 'a2:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[16]}}', 'a3:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[16]}}', 'a4:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[16]}}', 'a5:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[16]}}', 'a6:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[16]}}', 'a7:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[16]}}', 'a8:{{c[0]}}{{c[1]}}{{c[2]}}{{c[3]}}{{c[4]}}{{c[5]}}{{c[6]}}{{c[7]}}{{c[8]}}{{c[9]}}{{c[10]}}{{c[11]}}{{c[12]}}{{c[13]}}{{c[14]}}{{c[15]}}{{c[16]}}', ].join('\n') }) class App { c = ['(', 0, 'a', 1, 'b', 2, 'c', 3, 'd', 4, 'e', 5, 'f', 6, 'g', 7, ')']; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.nativeElement.textContent.trim()).toEqual([ 'a:(0a1b2c3d4e5f6g7)', 'a0:0', 'a1:(0)', 'a2:(0a1)', 'a3:(0a1b2)', 'a4:(0a1b2c3)', 'a5:(0a1b2c3d4)', 'a6:(0a1b2c3d4e5)', 'a7:(0a1b2c3d4e5f6)', 'a8:(0a1b2c3d4e5f6g7)', ].join('\n')); fixture.componentInstance.c.reverse(); fixture.detectChanges(); expect(fixture.nativeElement.textContent.trim()).toEqual([ 'a:)7g6f5e4d3c2b1a0(', 'a0:7', 'a1:)7(', 'a2:)7g6(', 'a3:)7g6f5(', 'a4:)7g6f5e4(', 'a5:)7g6f5e4d3(', 'a6:)7g6f5e4d3c2(', 'a7:)7g6f5e4d3c2b1(', 'a8:)7g6f5e4d3c2b1a0(', ].join('\n')); fixture.componentInstance.c.reverse(); fixture.detectChanges(); expect(fixture.nativeElement.textContent.trim()).toEqual([ 'a:(0a1b2c3d4e5f6g7)', 'a0:0', 'a1:(0)', 'a2:(0a1)', 'a3:(0a1b2)', 'a4:(0a1b2c3)', 'a5:(0a1b2c3d4)', 'a6:(0a1b2c3d4e5)', 'a7:(0a1b2c3d4e5f6)', 'a8:(0a1b2c3d4e5f6g7)', ].join('\n')); }); it('should not update DOM if context has not changed', () => { @Component({ template: ` ` }) class App { title: string|null = ''; shouldRender = true; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const span: HTMLSpanElement = fixture.nativeElement.querySelector('span'); const bold: HTMLElement = span.querySelector('b') !; fixture.componentInstance.title = 'Hello'; fixture.detectChanges(); // initial binding expect(span.getAttribute('title')).toBe('Hello'); expect(bold.getAttribute('title')).toBe('Hello'); // update DOM manually bold.setAttribute('title', 'Goodbye'); // refresh with same binding fixture.detectChanges(); expect(span.getAttribute('title')).toBe('Hello'); expect(bold.getAttribute('title')).toBe('Goodbye'); // refresh again with same binding fixture.detectChanges(); expect(span.getAttribute('title')).toBe('Hello'); expect(bold.getAttribute('title')).toBe('Goodbye'); }); it('should support host attribute bindings', () => { @Directive({selector: '[hostBindingDir]'}) class HostBindingDir { @HostBinding('attr.aria-label') label = 'some label'; } @Component({template: '
'}) class App { @ViewChild(HostBindingDir) hostBindingDir !: HostBindingDir; } TestBed.configureTestingModule({declarations: [App, HostBindingDir]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const hostBindingEl = fixture.nativeElement.querySelector('div'); // Needs `toLowerCase`, because different browsers produce // attributes either in camel case or lower case. expect(hostBindingEl.getAttribute('aria-label')).toBe('some label'); fixture.componentInstance.hostBindingDir.label = 'other label'; fixture.detectChanges(); expect(hostBindingEl.getAttribute('aria-label')).toBe('other label'); }); }); describe('elementStyle', () => { it('should support binding to styles', () => { @Component({template: ''}) class App { size: string|null = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.size = '10px'; fixture.detectChanges(); const span: HTMLElement = fixture.nativeElement.querySelector('span'); expect(span.style.fontSize).toBe('10px'); fixture.componentInstance.size = '16px'; fixture.detectChanges(); expect(span.style.fontSize).toBe('16px'); fixture.componentInstance.size = null; fixture.detectChanges(); expect(span.style.fontSize).toBeFalsy(); }); it('should support binding to styles with suffix', () => { @Component({template: ''}) class App { size: string|number|null = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.size = '100'; fixture.detectChanges(); const span: HTMLElement = fixture.nativeElement.querySelector('span'); expect(span.style.fontSize).toEqual('100px'); fixture.componentInstance.size = 200; fixture.detectChanges(); expect(span.style.fontSize).toEqual('200px'); fixture.componentInstance.size = 0; fixture.detectChanges(); expect(span.style.fontSize).toEqual('0px'); fixture.componentInstance.size = null; fixture.detectChanges(); expect(span.style.fontSize).toBeFalsy(); }); }); describe('class-based styling', () => { it('should support CSS class toggle', () => { @Component({template: ''}) class App { value: any; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.value = true; fixture.detectChanges(); const span = fixture.nativeElement.querySelector('span'); expect(span.getAttribute('class')).toEqual('active'); fixture.componentInstance.value = false; fixture.detectChanges(); expect(span.getAttribute('class')).toBeFalsy(); // truthy values fixture.componentInstance.value = 'a_string'; fixture.detectChanges(); expect(span.getAttribute('class')).toEqual('active'); fixture.componentInstance.value = 10; fixture.detectChanges(); expect(span.getAttribute('class')).toEqual('active'); // falsy values fixture.componentInstance.value = ''; fixture.detectChanges(); expect(span.getAttribute('class')).toBeFalsy(); fixture.componentInstance.value = 0; fixture.detectChanges(); expect(span.getAttribute('class')).toBeFalsy(); }); it('should work correctly with existing static classes', () => { @Component({template: ''}) class App { value: any; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.value = true; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(''); fixture.componentInstance.value = false; fixture.detectChanges(); expect(fixture.nativeElement.innerHTML).toEqual(''); }); it('should apply classes properly when nodes are components', () => { @Component({selector: 'my-comp', template: 'Comp Content'}) class MyComp { } @Component({template: ''}) class App { value: any; } TestBed.configureTestingModule({declarations: [App, MyComp]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.value = true; fixture.detectChanges(); const compElement = fixture.nativeElement.querySelector('my-comp'); expect(fixture.nativeElement.textContent).toContain('Comp Content'); expect(compElement.getAttribute('class')).toBe('active'); fixture.componentInstance.value = false; fixture.detectChanges(); expect(compElement.getAttribute('class')).toBeFalsy(); }); it('should apply classes properly when nodes have containers', () => { @Component({selector: 'structural-comp', template: 'Comp Content'}) class StructuralComp { @Input() tmp !: TemplateRef; constructor(public vcr: ViewContainerRef) {} create() { this.vcr.createEmbeddedView(this.tmp); } } @Component({ template: ` Temp Content ` }) class App { @ViewChild(StructuralComp) structuralComp !: StructuralComp; value: any; } TestBed.configureTestingModule({declarations: [App, StructuralComp]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.value = true; fixture.detectChanges(); const structuralCompEl = fixture.nativeElement.querySelector('structural-comp'); expect(structuralCompEl.getAttribute('class')).toEqual('active'); fixture.componentInstance.structuralComp.create(); fixture.detectChanges(); expect(structuralCompEl.getAttribute('class')).toEqual('active'); fixture.componentInstance.value = false; fixture.detectChanges(); expect(structuralCompEl.getAttribute('class')).toEqual(''); }); @Directive({selector: '[DirWithClass]'}) class DirWithClassDirective { public classesVal: string = ''; @Input('class') set klass(value: string) { this.classesVal = value; } } @Directive({selector: '[DirWithStyle]'}) class DirWithStyleDirective { public stylesVal: string = ''; @Input() set style(value: string) { this.stylesVal = value; } } it('should delegate initial classes to a [class] input binding if present on a directive on the same element', () => { @Component({template: '
'}) class App { @ViewChild(DirWithClassDirective) mockClassDirective !: DirWithClassDirective; } TestBed.configureTestingModule({declarations: [App, DirWithClassDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); expect(fixture.componentInstance.mockClassDirective.classesVal) .toEqual('apple orange banana'); }); it('should delegate initial styles to a [style] input binding if present on a directive on the same element', () => { @Component({template: '
'}) class App { @ViewChild(DirWithStyleDirective) mockStyleDirective !: DirWithStyleDirective; } TestBed.configureTestingModule({declarations: [App, DirWithStyleDirective]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const styles = fixture.componentInstance.mockStyleDirective.stylesVal; // Use `toContain` since Ivy and ViewEngine have some slight differences in formatting. expect(styles).toContain('width:100px'); expect(styles).toContain('height:200px'); }); it('should update `[class]` and bindings in the provided directive if the input is matched', () => { @Component({template: '
'}) class App { @ViewChild(DirWithClassDirective) mockClassDirective !: DirWithClassDirective; value = ''; } TestBed.configureTestingModule({declarations: [App, DirWithClassDirective]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.value = 'cucumber grape'; fixture.detectChanges(); expect(fixture.componentInstance.mockClassDirective.classesVal) .toEqual('cucumber grape'); }); onlyInIvy('Passing an object into [style] works differently') .it('should update `[style]` and bindings in the provided directive if the input is matched', () => { @Component({template: '
'}) class App { @ViewChild(DirWithStyleDirective) mockStyleDirective !: DirWithStyleDirective; value !: {[key: string]: string}; } TestBed.configureTestingModule({declarations: [App, DirWithStyleDirective]}); const fixture = TestBed.createComponent(App); fixture.componentInstance.value = {width: '200px', height: '500px'}; fixture.detectChanges(); expect(fixture.componentInstance.mockStyleDirective.stylesVal) .toEqual('width:200px;height:500px'); }); onlyInIvy('Style binding merging works differently in Ivy') .it('should apply initial styling to the element that contains the directive with host styling', () => { @Directive({ selector: '[DirWithInitialStyling]', host: { 'title': 'foo', 'class': 'heavy golden', 'style': 'color: purple', '[style.font-weight]': '"bold"' } }) class DirWithInitialStyling { } @Component({ template: `
` }) class App { } TestBed.configureTestingModule({declarations: [App, DirWithInitialStyling]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); const classes = target.getAttribute('class') !.split(/\s+/).sort(); expect(classes).toEqual(['big', 'golden', 'heavy']); expect(target.getAttribute('title')).toEqual('foo'); expect(target.style.getPropertyValue('color')).toEqual('black'); expect(target.style.getPropertyValue('font-size')).toEqual('200px'); expect(target.style.getPropertyValue('font-weight')).toEqual('bold'); }); onlyInIvy('Style binding merging works differently in Ivy') .it('should apply single styling bindings present within a directive onto the same element and defer the element\'s initial styling values when missing', () => { @Directive({ selector: '[DirWithSingleStylingBindings]', host: { 'class': 'def', '[class.xyz]': 'activateXYZClass', '[style.width]': 'width', '[style.height]': 'height' } }) class DirWithSingleStylingBindings { width: null|string = null; height: null|string = null; activateXYZClass: boolean = false; } @Component({ template: `
` }) class App { @ViewChild(DirWithSingleStylingBindings) dirInstance !: DirWithSingleStylingBindings; } TestBed.configureTestingModule({declarations: [App, DirWithSingleStylingBindings]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const dirInstance = fixture.componentInstance.dirInstance; const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); expect(target.style.getPropertyValue('width')).toEqual('100px'); expect(target.style.getPropertyValue('height')).toEqual('200px'); expect(target.classList.contains('abc')).toBeTruthy(); expect(target.classList.contains('def')).toBeTruthy(); expect(target.classList.contains('xyz')).toBeFalsy(); dirInstance.width = '444px'; dirInstance.height = '999px'; dirInstance.activateXYZClass = true; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('444px'); expect(target.style.getPropertyValue('height')).toEqual('999px'); expect(target.classList.contains('abc')).toBeTruthy(); expect(target.classList.contains('def')).toBeTruthy(); expect(target.classList.contains('xyz')).toBeTruthy(); dirInstance.width = null; dirInstance.height = null; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('100px'); expect(target.style.getPropertyValue('height')).toEqual('200px'); expect(target.classList.contains('abc')).toBeTruthy(); expect(target.classList.contains('def')).toBeTruthy(); expect(target.classList.contains('xyz')).toBeTruthy(); }); onlyInIvy('Style binding merging works differently in Ivy') .it('should properly prioritize single style binding collisions when they exist on multiple directives', () => { @Directive({selector: '[Dir1WithStyle]', host: {'[style.width]': 'width'}}) class Dir1WithStyle { width: null|string = null; } @Directive({ selector: '[Dir2WithStyle]', host: {'style': 'width: 111px', '[style.width]': 'width'} }) class Dir2WithStyle { width: null|string = null; } @Component( {template: '
'}) class App { @ViewChild(Dir1WithStyle) dir1Instance !: Dir1WithStyle; @ViewChild(Dir2WithStyle) dir2Instance !: Dir2WithStyle; width: string|null = null; } TestBed.configureTestingModule({declarations: [App, Dir1WithStyle, Dir2WithStyle]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const {dir1Instance, dir2Instance} = fixture.componentInstance; const target: HTMLDivElement = fixture.nativeElement.querySelector('div'); expect(target.style.getPropertyValue('width')).toEqual('111px'); fixture.componentInstance.width = '999px'; dir1Instance.width = '222px'; dir2Instance.width = '333px'; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('999px'); fixture.componentInstance.width = null; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('222px'); dir1Instance.width = null; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('333px'); dir2Instance.width = null; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('111px'); dir1Instance.width = '666px'; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('666px'); fixture.componentInstance.width = '777px'; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('777px'); }); onlyInIvy('Style binding merging works differently in Ivy') .it('should properly prioritize multi style binding collisions when they exist on multiple directives', () => { @Directive({ selector: '[Dir1WithStyling]', host: {'[style]': 'stylesExp', '[class]': 'classesExp'} }) class Dir1WithStyling { classesExp: any = {}; stylesExp: any = {}; } @Directive({ selector: '[Dir2WithStyling]', host: {'style': 'width: 111px', '[style]': 'stylesExp'} }) class Dir2WithStyling { stylesExp: any = {}; } @Component({ template: '
' }) class App { @ViewChild(Dir1WithStyling) dir1Instance !: Dir1WithStyling; @ViewChild(Dir2WithStyling) dir2Instance !: Dir2WithStyling; stylesExp: any = {}; classesExp: any = {}; } TestBed.configureTestingModule( {declarations: [App, Dir1WithStyling, Dir2WithStyling]}); const fixture = TestBed.createComponent(App); fixture.detectChanges(); const {dir1Instance, dir2Instance} = fixture.componentInstance; const target = fixture.nativeElement.querySelector('div') !; expect(target.style.getPropertyValue('width')).toEqual('111px'); const compInstance = fixture.componentInstance; compInstance.stylesExp = {width: '999px', height: null}; compInstance.classesExp = {one: true, two: false}; dir1Instance.stylesExp = {width: '222px'}; dir1Instance.classesExp = {two: true, three: false}; dir2Instance.stylesExp = {width: '333px', height: '100px'}; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('999px'); expect(target.style.getPropertyValue('height')).toEqual('100px'); expect(target.classList.contains('one')).toBeTruthy(); expect(target.classList.contains('two')).toBeFalsy(); expect(target.classList.contains('three')).toBeFalsy(); compInstance.stylesExp = {}; compInstance.classesExp = {}; dir1Instance.stylesExp = {width: '222px', height: '200px'}; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('222px'); expect(target.style.getPropertyValue('height')).toEqual('200px'); expect(target.classList.contains('one')).toBeFalsy(); expect(target.classList.contains('two')).toBeTruthy(); expect(target.classList.contains('three')).toBeFalsy(); dir1Instance.stylesExp = {}; dir1Instance.classesExp = {}; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('333px'); expect(target.style.getPropertyValue('height')).toEqual('100px'); expect(target.classList.contains('one')).toBeFalsy(); expect(target.classList.contains('two')).toBeFalsy(); expect(target.classList.contains('three')).toBeFalsy(); dir2Instance.stylesExp = {}; compInstance.stylesExp = {height: '900px'}; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('111px'); expect(target.style.getPropertyValue('height')).toEqual('900px'); dir1Instance.stylesExp = {width: '666px', height: '600px'}; dir1Instance.classesExp = {four: true, one: true}; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('666px'); expect(target.style.getPropertyValue('height')).toEqual('900px'); expect(target.classList.contains('one')).toBeTruthy(); expect(target.classList.contains('two')).toBeFalsy(); expect(target.classList.contains('three')).toBeFalsy(); expect(target.classList.contains('four')).toBeTruthy(); compInstance.stylesExp = {width: '777px'}; compInstance.classesExp = {four: false}; fixture.detectChanges(); expect(target.style.getPropertyValue('width')).toEqual('777px'); expect(target.style.getPropertyValue('height')).toEqual('600px'); expect(target.classList.contains('one')).toBeTruthy(); expect(target.classList.contains('two')).toBeFalsy(); expect(target.classList.contains('three')).toBeFalsy(); expect(target.classList.contains('four')).toBeFalsy(); }); }); it('should properly handle and render interpolation for class attribute bindings', () => { @Component({template: '
'}) class App { name = ''; age = ''; } TestBed.configureTestingModule({declarations: [App]}); const fixture = TestBed.createComponent(App); const target = fixture.nativeElement.querySelector('div') !; expect(target.classList.contains('-fred-36-')).toBeFalsy(); fixture.componentInstance.name = 'fred'; fixture.componentInstance.age = '36'; fixture.detectChanges(); expect(target.classList.contains('-fred-36-')).toBeTruthy(); }); }); it('should only call inherited host listeners once', () => { let clicks = 0; @Component({template: ''}) class ButtonSuperClass { @HostListener('click') clicked() { clicks++; } } @Component({selector: 'button[custom-button]', template: ''}) class ButtonSubClass extends ButtonSuperClass { } @Component({template: ''}) class MyApp { } TestBed.configureTestingModule({declarations: [MyApp, ButtonSuperClass, ButtonSubClass]}); const fixture = TestBed.createComponent(MyApp); const button = fixture.debugElement.query(By.directive(ButtonSubClass)); fixture.detectChanges(); button.nativeElement.click(); fixture.detectChanges(); expect(clicks).toBe(1); }); it('should support inherited view queries', () => { @Directive({selector: '[someDir]'}) class SomeDir { } @Component({template: '
'}) class SuperComp { @ViewChildren(SomeDir) dirs !: QueryList; } @Component({selector: 'button[custom-button]', template: '
'}) class SubComp extends SuperComp { } @Component({template: ''}) class MyApp { } TestBed.configureTestingModule({declarations: [MyApp, SuperComp, SubComp, SomeDir]}); const fixture = TestBed.createComponent(MyApp); const subInstance = fixture.debugElement.query(By.directive(SubComp)).componentInstance; fixture.detectChanges(); expect(subInstance.dirs.length).toBe(1); expect(subInstance.dirs.first).toBeAnInstanceOf(SomeDir); }); it('should not set inputs after destroy', () => { @Directive({ selector: '[no-assign-after-destroy]', }) class NoAssignAfterDestroy { private _isDestroyed = false; @Input() get value() { return this._value; } set value(newValue: any) { if (this._isDestroyed) { throw Error('Cannot assign to value after destroy.'); } this._value = newValue; } private _value: any; ngOnDestroy() { this._isDestroyed = true; } } @Component({template: '
'}) class App { directiveValue = 'initial-value'; } TestBed.configureTestingModule({declarations: [NoAssignAfterDestroy, App]}); let fixture = TestBed.createComponent(App); fixture.destroy(); expect(() => { fixture = TestBed.createComponent(App); fixture.detectChanges(); }).not.toThrow(); }); it('should support host attribute and @ContentChild on the same component', () => { @Component( {selector: 'test-component', template: `foo`, host: {'[attr.aria-disabled]': 'true'}}) class TestComponent { @ContentChild(TemplateRef) tpl !: TemplateRef; } TestBed.configureTestingModule({declarations: [TestComponent]}); const fixture = TestBed.createComponent(TestComponent); fixture.detectChanges(); expect(fixture.componentInstance.tpl).not.toBeNull(); expect(fixture.debugElement.nativeElement.getAttribute('aria-disabled')).toBe('true'); }); it('should inherit inputs from undecorated superclasses', () => { class ButtonSuperClass { @Input() isDisabled !: boolean; } @Component({selector: 'button[custom-button]', template: ''}) class ButtonSubClass extends ButtonSuperClass { } @Component({template: ''}) class MyApp { disableButton = false; } TestBed.configureTestingModule({declarations: [MyApp, ButtonSubClass]}); const fixture = TestBed.createComponent(MyApp); const button = fixture.debugElement.query(By.directive(ButtonSubClass)).componentInstance; fixture.detectChanges(); expect(button.isDisabled).toBe(false); fixture.componentInstance.disableButton = true; fixture.detectChanges(); expect(button.isDisabled).toBe(true); }); it('should inherit outputs from undecorated superclasses', () => { let clicks = 0; class ButtonSuperClass { @Output() clicked = new EventEmitter(); emitClick() { this.clicked.emit(); } } @Component({selector: 'button[custom-button]', template: ''}) class ButtonSubClass extends ButtonSuperClass { } @Component({template: ''}) class MyApp { handleClick() { clicks++; } } TestBed.configureTestingModule({declarations: [MyApp, ButtonSubClass]}); const fixture = TestBed.createComponent(MyApp); const button = fixture.debugElement.query(By.directive(ButtonSubClass)).componentInstance; button.emitClick(); fixture.detectChanges(); expect(clicks).toBe(1); }); it('should inherit host bindings from undecorated superclasses', () => { class BaseButton { @HostBinding('attr.tabindex') tabindex = -1; } @Component({selector: '[sub-button]', template: ''}) class SubButton extends BaseButton { } @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [SubButton, App]}); const fixture = TestBed.createComponent(App); const button = fixture.debugElement.query(By.directive(SubButton)); fixture.detectChanges(); expect(button.nativeElement.getAttribute('tabindex')).toBe('-1'); button.componentInstance.tabindex = 2; fixture.detectChanges(); expect(button.nativeElement.getAttribute('tabindex')).toBe('2'); }); it('should inherit host bindings from undecorated grand superclasses', () => { class SuperBaseButton { @HostBinding('attr.tabindex') tabindex = -1; } class BaseButton extends SuperBaseButton {} @Component({selector: '[sub-button]', template: ''}) class SubButton extends BaseButton { } @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [SubButton, App]}); const fixture = TestBed.createComponent(App); const button = fixture.debugElement.query(By.directive(SubButton)); fixture.detectChanges(); expect(button.nativeElement.getAttribute('tabindex')).toBe('-1'); button.componentInstance.tabindex = 2; fixture.detectChanges(); expect(button.nativeElement.getAttribute('tabindex')).toBe('2'); }); it('should inherit host listeners from undecorated superclasses', () => { let clicks = 0; class BaseButton { @HostListener('click') handleClick() { clicks++; } } @Component({selector: '[sub-button]', template: ''}) class SubButton extends BaseButton { } @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [SubButton, App]}); const fixture = TestBed.createComponent(App); const button = fixture.debugElement.query(By.directive(SubButton)).nativeElement; button.click(); fixture.detectChanges(); expect(clicks).toBe(1); }); it('should inherit host listeners from superclasses once', () => { let clicks = 0; @Directive({selector: '[baseButton]'}) class BaseButton { @HostListener('click') handleClick() { clicks++; } } @Component({selector: '[subButton]', template: ''}) class SubButton extends BaseButton { } @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [SubButton, BaseButton, App]}); const fixture = TestBed.createComponent(App); const button = fixture.debugElement.query(By.directive(SubButton)).nativeElement; button.click(); fixture.detectChanges(); expect(clicks).toBe(1); }); it('should inherit host listeners from grand superclasses once', () => { let clicks = 0; @Directive({selector: '[superBaseButton]'}) class SuperBaseButton { @HostListener('click') handleClick() { clicks++; } } @Directive({selector: '[baseButton]'}) class BaseButton extends SuperBaseButton { } @Component({selector: '[subButton]', template: ''}) class SubButton extends BaseButton { } @Component({template: ''}) class App { } TestBed.configureTestingModule({declarations: [SubButton, SuperBaseButton, BaseButton, App]}); const fixture = TestBed.createComponent(App); const button = fixture.debugElement.query(By.directive(SubButton)).nativeElement; button.click(); fixture.detectChanges(); expect(clicks).toBe(1); }); it('should inherit host listeners from grand grand superclasses once', () => { let clicks = 0; @Directive({selector: '[superSuperBaseButton]'}) class SuperSuperBaseButton { @HostListener('click') handleClick() { clicks++; } } @Directive({selector: '[superBaseButton]'}) class SuperBaseButton extends SuperSuperBaseButton { } @Directive({selector: '[baseButton]'}) class BaseButton extends SuperBaseButton { } @Component({selector: '[subButton]', template: ''}) class SubButton extends BaseButton { } @Component({template: ''}) class App { } TestBed.configureTestingModule( {declarations: [SubButton, SuperBaseButton, SuperSuperBaseButton, BaseButton, App]}); const fixture = TestBed.createComponent(App); const button = fixture.debugElement.query(By.directive(SubButton)).nativeElement; button.click(); fixture.detectChanges(); expect(clicks).toBe(1); }); });