/**
* @license
* Copyright Google LLC 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 {animate, AnimationEvent, state, style, transition, trigger} from '@angular/animations';
import {AnimationDriver} from '@angular/animations/browser';
import {MockAnimationDriver, MockAnimationPlayer} from '@angular/animations/browser/testing';
import {CommonModule} from '@angular/common';
import {Component, ContentChild, Directive, ElementRef, EventEmitter, HostBinding, HostListener, Input, NgModule, OnInit, Output, Pipe, QueryList, TemplateRef, ViewChild, ViewChildren, ViewContainerRef} from '@angular/core';
import {Inject} from '@angular/core/src/di';
import {TVIEW} from '@angular/core/src/render3/interfaces/view';
import {getLView} from '@angular/core/src/render3/state';
import {ngDevModeResetPerfCounters} from '@angular/core/src/util/ng_dev_mode';
import {fakeAsync, flushMicrotasks, TestBed} from '@angular/core/testing';
import {By} from '@angular/platform-browser';
import {expect} from '@angular/platform-browser/testing/src/matchers';
import {ivyEnabled, 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: 3, // Host 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: 3,
});
});
});
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, {static: true}) 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, {static: true}) 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('');
});
it('should be able to render the result of a function called $any by using this', () => {
@Component({template: '{{this.$any(1, 2)}}'})
class App {
$any(value: number, multiplier: number) {
return value * multiplier;
}
}
TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toBe('2');
});
});
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')).toBeFalsy();
});
@Directive({selector: '[DirWithClass]'})
class DirWithClassDirective {
public classesVal: string = '';
@Input('class')
set klass(value: string) {
this.classesVal = value;
}
}
@Directive({selector: '[DirWithStyle]'})
class DirWithStyleDirective {
public stylesVal: any = '';
@Input()
set style(value: any) {
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();
// the initial values always get sorted in non VE code
// but there is no sorting guarantee within VE code
expect(fixture.componentInstance.mockClassDirective.classesVal.split(/\s+/).sort())
.toEqual(['apple', 'banana', 'orange']);
});
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: string|null|undefined = undefined;
height: string|null|undefined = undefined;
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('');
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('100px');
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 = undefined;
dirInstance.height = undefined;
fixture.detectChanges();
expect(target.style.getPropertyValue('width')).toEqual('100px');
expect(target.style.getPropertyValue('height')).toEqual('');
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|undefined = undefined;
}
@Directive({
selector: '[Dir2WithStyle]',
host: {'style': 'width: 111px', '[style.width]': 'width'}
})
class Dir2WithStyle {
width: null|string|undefined = undefined;
}
@Component(
{template: '
'})
class App {
@ViewChild(Dir1WithStyle) dir1Instance!: Dir1WithStyle;
@ViewChild(Dir2WithStyle) dir2Instance!: Dir2WithStyle;
width: string|null|undefined = undefined;
}
TestBed.configureTestingModule({declarations: [App, Dir2WithStyle, Dir1WithStyle]});
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 = undefined;
fixture.detectChanges();
expect(target.style.getPropertyValue('width')).toEqual('222px');
dir1Instance.width = undefined;
fixture.detectChanges();
expect(target.style.getPropertyValue('width')).toEqual('333px');
dir2Instance.width = undefined;
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, Dir2WithStyling, Dir1WithStyling]});
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: undefined};
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();
});
});
describe('NgModule assertions', () => {
it('should throw with descriptive error message when a module imports itself', () => {
@Component({template: ''})
class FixtureComponent {
}
@NgModule({imports: [SomeModule], declarations: [FixtureComponent]})
class SomeModule {
}
expect(() => {
TestBed.configureTestingModule({imports: [SomeModule]}).createComponent(FixtureComponent);
}).toThrowError(`'SomeModule' module can't import itself`);
});
it('should throw with descriptive error message when a directive is passed to imports', () => {
@Component({template: ''})
class SomeComponent {
}
@NgModule({imports: [SomeComponent]})
class ModuleWithImportedComponent {
}
expect(() => {
TestBed.configureTestingModule({imports: [ModuleWithImportedComponent]})
.createComponent(SomeComponent);
})
.toThrowError(
// The ViewEngine error has a typo, whereas the Ivy one fixes it.
/^Unexpected directive 'SomeComponent' imported by the module 'ModuleWithImportedComponent'\. Please add (a|an) @NgModule annotation\.$/);
});
it('should throw with descriptive error message when a pipe is passed to imports', () => {
@Component({template: ''})
class FixtureComponent {
}
@Pipe({name: 'somePipe'})
class SomePipe {
}
@NgModule({imports: [SomePipe], declarations: [FixtureComponent]})
class ModuleWithImportedPipe {
}
expect(() => {
TestBed.configureTestingModule({imports: [ModuleWithImportedPipe]})
.createComponent(FixtureComponent);
})
.toThrowError(
// The ViewEngine error has a typo, whereas the Ivy one fixes it.
/^Unexpected pipe 'SomePipe' imported by the module 'ModuleWithImportedPipe'\. Please add (a|an) @NgModule annotation\.$/);
});
it('should throw with descriptive error message when a module is passed to declarations', () => {
@Component({template: ''})
class FixtureComponent {
}
@NgModule({})
class SomeModule {
}
@NgModule({declarations: [SomeModule, FixtureComponent]})
class ModuleWithDeclaredModule {
}
// The error is almost the same in Ivy and ViewEngine, however since Ivy's
// message is more correct it doesn't make sense to align it ViewEngine.
const expectedErrorMessage = ivyEnabled ?
`Unexpected value 'SomeModule' declared by the module 'ModuleWithDeclaredModule'. Please add a @Pipe/@Directive/@Component annotation.` :
`Unexpected module 'SomeModule' declared by the module 'ModuleWithDeclaredModule'. Please add a @Pipe/@Directive/@Component annotation.`;
expect(() => {
TestBed.configureTestingModule({imports: [ModuleWithDeclaredModule]})
.createComponent(FixtureComponent);
}).toThrowError(expectedErrorMessage);
});
it('should throw with descriptive error message when a declaration is missing annotation', () => {
@Component({template: ''})
class FixtureComponent {
}
class SomeClass {}
@NgModule({declarations: [SomeClass, FixtureComponent]})
class SomeModule {
}
expect(() => {
TestBed.configureTestingModule({imports: [SomeModule]}).createComponent(FixtureComponent);
})
.toThrowError(
`Unexpected value 'SomeClass' declared by the module 'SomeModule'. Please add a @Pipe/@Directive/@Component annotation.`);
});
it('should throw with descriptive error message when an imported module is missing annotation',
() => {
@Component({template: ''})
class FixtureComponent {
}
class SomeModule {}
@NgModule({imports: [SomeModule], declarations: [FixtureComponent]})
class ModuleWithImportedModule {
}
expect(() => {
TestBed.configureTestingModule({imports: [ModuleWithImportedModule]})
.createComponent(FixtureComponent);
})
.toThrowError(
// The ViewEngine error has a typo, whereas the Ivy one fixes it.
/^Unexpected value 'SomeModule' imported by the module 'ModuleWithImportedModule'\. Please add (a|an) @NgModule annotation\.$/);
});
});
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, {static: true}) 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: 'Click me '})
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: 'Click me '})
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: 'Click me '})
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: 'Click me '})
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: 'Click me '})
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: 'Click me '})
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);
});
it('should not mask errors thrown during lifecycle hooks', () => {
@Directive({
selector: '[dir]',
inputs: ['dir'],
})
class Dir {
get dir(): any {
return null;
}
set dir(value: any) {
throw new Error('this error is expected');
}
}
@Component({
template: '
',
})
class Cmp {
ngAfterViewInit(): void {
// This lifecycle hook should never run, since attempting to bind to Dir's input will throw
// an error. If the runtime continues to run lifecycle hooks after that error, then it will
// execute this hook and throw this error, which will mask the real problem. This test
// verifies this don't happen.
throw new Error('this error is unexpected');
}
}
TestBed.configureTestingModule({
declarations: [Cmp, Dir],
});
const fixture = TestBed.createComponent(Cmp);
expect(() => fixture.detectChanges()).toThrowError('this error is expected');
});
it('should handle nullish coalescing inside templates', () => {
@Component({
template: `
Hello, {{ firstName ?? 'Frodo' }}!
You are a Balrog: {{ falsyValue ?? true }}
`
})
class App {
firstName: string|null = null;
lastName: string|null = null;
lastNameFallback = 'Baggins';
falsyValue = false;
}
TestBed.configureTestingModule({declarations: [App]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const content = fixture.nativeElement.innerHTML;
expect(content).toContain('Hello, Frodo!');
expect(content).toContain('You are a Balrog: false');
expect(content).toContain(``);
});
it('should handle nullish coalescing inside host bindings', () => {
const logs: string[] = [];
@Directive({
selector: '[some-dir]',
host: {
'[attr.first-name]': `'Hello, ' + (firstName ?? 'Frodo') + '!'`,
'(click)': `logLastName(lastName ?? lastNameFallback ?? 'unknown')`
}
})
class Dir {
firstName: string|null = null;
lastName: string|null = null;
lastNameFallback = 'Baggins';
logLastName(name: string) {
logs.push(name);
}
}
@Component({template: `Click me `})
class App {
}
TestBed.configureTestingModule({declarations: [App, Dir]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const button = fixture.nativeElement.querySelector('button');
button.click();
fixture.detectChanges();
expect(button.getAttribute('first-name')).toBe('Hello, Frodo!');
expect(logs).toEqual(['Baggins']);
});
it('should render SVG nodes placed inside ng-template', () => {
@Component({
template: `
Hello
`,
})
class MyComp {
condition = true;
}
TestBed.configureTestingModule({declarations: [MyComp], imports: [CommonModule]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
expect(fixture.nativeElement.innerHTML).toContain('Hello ');
});
describe('tView.firstUpdatePass', () => {
function isFirstUpdatePass() {
const lView = getLView();
const tView = lView[TVIEW];
return tView.firstUpdatePass;
}
function assertAttrValues(element: Element, value: string) {
expect(element.getAttribute('data-comp')).toEqual(value);
expect(element.getAttribute('data-dir')).toEqual(value);
}
onlyInIvy('tView instances are ivy-specific')
.it('should be marked with `firstUpdatePass` up until the template and host bindings are evaluated',
() => {
@Directive({
selector: '[dir]',
})
class Dir {
@HostBinding('attr.data-dir')
get text() {
return isFirstUpdatePass() ? 'first-update-pass' : 'post-update-pass';
}
}
@Component({
template: '
',
})
class Cmp {
get text() {
return isFirstUpdatePass() ? 'first-update-pass' : 'post-update-pass';
}
}
TestBed.configureTestingModule({
declarations: [Cmp, Dir],
});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges(false);
const element = fixture.nativeElement.querySelector('div')!;
assertAttrValues(element, 'first-update-pass');
fixture.detectChanges(false);
assertAttrValues(element, 'post-update-pass');
});
onlyInIvy('tView instances are ivy-specific')
.it('tView.firstUpdatePass should be applied immediately after the first embedded view is processed',
() => {
@Directive({
selector: '[dir]',
})
class Dir {
@HostBinding('attr.data-dir')
get text() {
return isFirstUpdatePass() ? 'first-update-pass' : 'post-update-pass';
}
}
@Component({
template: `
...
`
})
class Cmp {
items = [1, 2, 3];
get text() {
return isFirstUpdatePass() ? 'first-update-pass' : 'post-update-pass';
}
}
TestBed.configureTestingModule({
declarations: [Cmp, Dir],
});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges(false);
const elements = fixture.nativeElement.querySelectorAll('div');
assertAttrValues(elements[0], 'first-update-pass');
assertAttrValues(elements[1], 'post-update-pass');
assertAttrValues(elements[2], 'post-update-pass');
fixture.detectChanges(false);
assertAttrValues(elements[0], 'post-update-pass');
assertAttrValues(elements[1], 'post-update-pass');
assertAttrValues(elements[2], 'post-update-pass');
});
});
describe('animations', () => {
it('should apply triggers for a list of items when they are sorted and reSorted',
fakeAsync(() => {
interface Item {
value: any;
id: number;
}
@Component({
template: `
Nooo!
{{ item.value }}
`
})
class Cmp {
showWarningMessage = false;
items: Item[] = [
{value: 1, id: 1},
{value: 2, id: 2},
{value: 3, id: 3},
{value: 4, id: 4},
{value: 5, id: 5},
];
itemTrackFn(value: Item) {
return value.id;
}
}
@Component({
selector: 'animation-comp',
animations: [
trigger(
'host',
[
state('void', style({height: '0px'})),
transition(
'* => *',
[
animate('1s'),
]),
]),
],
template: `
`
})
class AnimationComp {
@HostBinding('@host') public hostState = '';
@HostListener('@host.start', ['$event'])
onLeaveStart(event: AnimationEvent) {
// we just want to register the listener
}
}
TestBed.configureTestingModule({
declarations: [Cmp, AnimationComp],
providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
let elements = queryAll(fixture.nativeElement, 'animation-comp');
expect(elements.length).toEqual(5);
expect(elements.map(e => e.textContent?.trim())).toEqual(['1', '2', '3', '4', '5']);
const items = fixture.componentInstance.items;
arraySwap(items, 2, 0); // 3 2 1 4 5
arraySwap(items, 2, 1); // 3 1 2 4 5
const first = items.shift()!;
items.push(first); // 1 2 4 5 3
fixture.detectChanges();
elements = queryAll(fixture.nativeElement, 'animation-comp');
expect(elements.length).toEqual(5);
expect(elements.map(e => e.textContent?.trim())).toEqual(['1', '2', '4', '5', '3']);
completeAnimations();
fixture.componentInstance.showWarningMessage = true;
fixture.detectChanges();
completeAnimations();
elements = queryAll(fixture.nativeElement, 'animation-comp');
expect(elements.length).toEqual(0);
expect(fixture.nativeElement.textContent.trim()).toEqual('Nooo!');
fixture.componentInstance.showWarningMessage = false;
fixture.detectChanges();
elements = queryAll(fixture.nativeElement, 'animation-comp');
expect(elements.length).toEqual(5);
}));
it('should insert and remove views in the correct order when animations are present',
fakeAsync(() => {
@Component({
animations: [
trigger('root', [transition('* => *', [])]),
trigger('outer', [transition('* => *', [])]),
trigger('inner', [transition('* => *', [])]),
],
template: `
`
})
class Cmp {
showRoot = true;
showIfContents = true;
items = [1];
log: string[] = [];
track(name: string, event: AnimationEvent) {
this.log.push(name);
}
}
@Component({
selector: 'inner-comp',
animations: [
trigger('host', [transition('* => *', [])]),
],
template: `
`
})
class InnerComp {
@HostBinding('@host') public hostState = '';
constructor(@Inject(Cmp) private parent: Cmp) {}
@HostListener('@host.start', ['$event'])
onLeaveStart(event: AnimationEvent) {
this.parent.log.push('host');
}
}
TestBed.configureTestingModule({
declarations: [Cmp, InnerComp],
providers: [{provide: AnimationDriver, useClass: MockAnimationDriver}],
});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
completeAnimations();
const comp = fixture.componentInstance;
expect(comp.log).toEqual([
'root', // insertion of the inner-comp content
'outer', // insertion of the default ngIf
]);
comp.log = [];
comp.showIfContents = false;
fixture.detectChanges();
completeAnimations();
expect(comp.log).toEqual([
'host', // insertion of the inner-comp content
'outer', // insertion of the template into the ngIf
'inner' // insertion of the inner comp element
]);
comp.log = [];
comp.showRoot = false;
fixture.detectChanges();
completeAnimations();
expect(comp.log).toEqual([
'root', // removal the root div container
'host', // removal of the inner-comp content
'inner' // removal of the inner comp element
]);
}));
});
});
function completeAnimations() {
flushMicrotasks();
const log = MockAnimationDriver.log as MockAnimationPlayer[];
log.forEach(player => player.finish());
flushMicrotasks();
}
function arraySwap(arr: any[], indexA: number, indexB: number): void {
const item = arr[indexA];
arr[indexA] = arr[indexB];
arr[indexB] = item;
}
/**
* Queries the provided `root` element for sub elements by the selector and casts the result as an
* array of elements
*/
function queryAll(root: HTMLElement, selector: string): HTMLElement[] {
return Array.from(root.querySelectorAll(selector));
}