`
})
class Cmp {
w0: string|null|undefined = null;
w1: string|null|undefined = null;
w2: string|null|undefined = null;
}
TestBed.configureTestingModule(
{declarations: [Cmp, DirThatSetsWidthDirective, AnotherDirThatSetsWidthDirective]});
const fixture = TestBed.createComponent(Cmp);
fixture.componentInstance.w0 = '100px';
fixture.componentInstance.w1 = '200px';
fixture.componentInstance.w2 = '300px';
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
expect(element.style.width).toEqual('100px');
fixture.componentInstance.w0 = undefined;
fixture.detectChanges();
expect(element.style.width).toEqual('300px');
fixture.componentInstance.w2 = undefined;
fixture.detectChanges();
expect(element.style.width).toEqual('200px');
fixture.componentInstance.w1 = undefined;
fixture.detectChanges();
expect(element.style.width).toBeFalsy();
fixture.componentInstance.w2 = '400px';
fixture.detectChanges();
expect(element.style.width).toEqual('400px');
fixture.componentInstance.w1 = '500px';
fixture.componentInstance.w0 = '600px';
fixture.detectChanges();
expect(element.style.width).toEqual('600px');
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should only run stylingFlush once when there are no collisions between styling properties',
() => {
@Directive({selector: '[dir-with-styling]'})
class DirWithStyling {
@HostBinding('style.font-size') public fontSize = '100px';
}
@Component({selector: 'comp-with-styling'})
class CompWithStyling {
@HostBinding('style.width') public width = '900px';
@HostBinding('style.height') public height = '900px';
}
@Component({
template: `
...
`
})
class Cmp {
opacity: string|null = '0.5';
@ViewChild(CompWithStyling, {static: true})
compWithStyling: CompWithStyling|null = null;
@ViewChild(DirWithStyling, {static: true}) dirWithStyling: DirWithStyling|null = null;
}
TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const component = fixture.componentInstance;
const element = fixture.nativeElement.querySelector('comp-with-styling');
expect(element.style.opacity).toEqual('0.5');
expect(element.style.width).toEqual('900px');
expect(element.style.height).toEqual('900px');
expect(element.style.fontSize).toEqual('100px');
// once for the template flush and again for the host bindings
expect(ngDevMode!.rendererSetStyle).toEqual(4);
ngDevModeResetPerfCounters();
component.opacity = '0.6';
component.compWithStyling!.height = '100px';
component.compWithStyling!.width = '100px';
component.dirWithStyling!.fontSize = '50px';
fixture.detectChanges();
expect(element.style.opacity).toEqual('0.6');
expect(element.style.width).toEqual('100px');
expect(element.style.height).toEqual('100px');
expect(element.style.fontSize).toEqual('50px');
// once for the template flush and again for the host bindings
expect(ngDevMode!.rendererSetStyle).toEqual(4);
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should combine all styling across the template, directive and component host bindings',
() => {
@Directive({selector: '[dir-with-styling]'})
class DirWithStyling {
@HostBinding('style.color') public color = 'red';
@HostBinding('style.font-size') public fontSize = '100px';
@HostBinding('class.dir') public dirClass = true;
}
@Component({selector: 'comp-with-styling'})
class CompWithStyling {
@HostBinding('style.width') public width = '900px';
@HostBinding('style.height') public height = '900px';
@HostBinding('class.comp') public compClass = true;
}
@Component({
template: `
...
`
})
class Cmp {
opacity: string|null|undefined = '0.5';
width: string|null|undefined = 'auto';
tplClass = true;
}
TestBed.configureTestingModule({declarations: [Cmp, DirWithStyling, CompWithStyling]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('comp-with-styling');
expectStyle(element).toEqual({
'color': 'red',
'font-size': '100px',
'height': '900px',
'opacity': '0.5',
'width': 'auto',
});
expectClass(element).toEqual({
'dir': true,
'comp': true,
'tpl': true,
});
fixture.componentInstance.width = undefined;
fixture.componentInstance.opacity = undefined;
fixture.componentInstance.tplClass = false;
fixture.detectChanges();
expectStyle(element).toEqual(
{'color': 'red', 'width': '900px', 'height': '900px', 'font-size': '100px'});
expectClass(element).toEqual({
'dir': true,
'comp': true,
});
fixture.componentInstance.width = null;
fixture.componentInstance.opacity = null;
fixture.detectChanges();
expectStyle(element).toEqual({'color': 'red', 'height': '900px', 'font-size': '100px'});
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should properly apply styling across sub and super class directive host bindings',
() => {
@Directive({selector: '[super-class-dir]'})
class SuperClassDirective {
@HostBinding('style.width') public w1 = '100px';
}
@Component({selector: '[sub-class-dir]'})
class SubClassDirective extends SuperClassDirective {
@HostBinding('style.width') public w2 = '200px';
}
@Component({
template: `
`
})
class Cmp {
w3: string|null|undefined = '300px';
}
TestBed.configureTestingModule(
{declarations: [Cmp, SuperClassDirective, SubClassDirective]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
expectStyle(element).toEqual({
'width': '300px',
});
fixture.componentInstance.w3 = null;
fixture.detectChanges();
expectStyle(element).toEqual({});
fixture.componentInstance.w3 = undefined;
fixture.detectChanges();
expectStyle(element).toEqual({
'width': '200px',
});
});
onlyInIvy('only ivy has style debugging support')
.it('should apply map-based style and class entries', () => {
@Component({template: '
'})
class Cmp {
public c: {[key: string]: any}|null = null;
updateClasses(classes: string) {
const c = this.c || (this.c = {});
Object.keys(this.c).forEach(className => {
c[className] = false;
});
classes.split(/\s+/).forEach(className => {
c[className] = true;
});
}
public s: {[key: string]: any}|null = null;
updateStyles(prop: string, value: string|number|null) {
const s = this.s || (this.s = {});
Object.assign(s, {[prop]: value});
}
reset() {
this.s = null;
this.c = null;
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
comp.updateStyles('width', '100px');
comp.updateStyles('height', '200px');
comp.updateClasses('abc');
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
expectStyle(element).toEqual({width: '100px', height: '200px'});
expectClass(element).toEqual({abc: true});
comp.reset();
comp.updateStyles('width', '500px');
comp.updateStyles('height', null);
comp.updateClasses('def');
fixture.detectChanges();
expectStyle(element).toEqual({width: '500px'});
expectClass(element).toEqual({def: true});
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should resolve styling collisions across templates, directives and components for prop and map-based entries',
() => {
@Directive({selector: '[dir-that-sets-styling]'})
class DirThatSetsStyling {
@HostBinding('style') public map: any = {color: 'red', width: '777px'};
}
@Component({
template: `
`
})
class Cmp {
map: any = {width: '111px', opacity: '0.5'};
width: string|null|undefined = '555px';
@ViewChild('dir', {read: DirThatSetsStyling, static: true}) dir!: DirThatSetsStyling;
}
TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
expectStyle(element).toEqual({
'width': '555px',
'color': 'red',
'font-size': '99px',
'opacity': '0.5',
});
comp.width = undefined;
fixture.detectChanges();
expectStyle(element).toEqual({
'width': '111px',
'color': 'red',
'font-size': '99px',
'opacity': '0.5',
});
comp.map = null;
fixture.detectChanges();
expectStyle(element).toEqual({
'width': '200px',
'color': 'red',
'font-size': '99px',
});
comp.dir.map = null;
fixture.detectChanges();
expectStyle(element).toEqual({
'width': '200px',
'font-size': '99px',
});
});
onlyInIvy('ivy resolves styling across directives, components and templates in unison')
.it('should only apply each styling property once per CD across templates, components, directives',
() => {
@Directive(
{selector: '[dir-that-sets-styling]', host: {'style': 'width:0px; height:0px'}})
class DirThatSetsStyling {
@HostBinding('style') public map: any = {width: '999px', height: '999px'};
}
@Component({
template: `
`
})
class Cmp {
width: string|null|undefined = '111px';
height: string|null|undefined = '111px';
map: any = {width: '555px', height: '555px'};
@ViewChild('dir', {read: DirThatSetsStyling, static: true}) dir!: DirThatSetsStyling;
}
TestBed.configureTestingModule({declarations: [Cmp, DirThatSetsStyling]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
ngDevModeResetPerfCounters();
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
assertStyleCounters(4, 0);
assertStyle(element, 'width', '111px');
assertStyle(element, 'height', '111px');
comp.width = '222px';
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '222px');
assertStyle(element, 'height', '111px');
comp.height = '222px';
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '222px');
assertStyle(element, 'height', '222px');
comp.width = undefined;
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '555px');
assertStyle(element, 'height', '222px');
comp.width = '123px';
comp.height = '123px';
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyle(element, 'width', '123px');
assertStyle(element, 'height', '123px');
comp.map = {};
ngDevModeResetPerfCounters();
fixture.detectChanges();
// No change, hence no write
assertStyleCounters(0, 0);
assertStyle(element, 'width', '123px');
assertStyle(element, 'height', '123px');
comp.width = undefined;
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyleCounters(1, 0);
assertStyle(element, 'width', '999px');
assertStyle(element, 'height', '123px');
comp.dir.map = null;
ngDevModeResetPerfCounters();
fixture.detectChanges();
// the width is only applied once
assertStyleCounters(1, 0);
assertStyle(element, 'width', '0px');
assertStyle(element, 'height', '123px');
comp.dir.map = {width: '1000px', height: '1100px', color: 'red'};
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyleCounters(2, 0);
assertStyle(element, 'width', '1000px');
assertStyle(element, 'height', '123px');
assertStyle(element, 'color', 'red');
comp.height = undefined;
ngDevModeResetPerfCounters();
fixture.detectChanges();
// height gets applied twice and all other
// values get applied
assertStyleCounters(1, 0);
assertStyle(element, 'width', '1000px');
assertStyle(element, 'height', '1100px');
assertStyle(element, 'color', 'red');
comp.map = {color: 'blue', width: '2000px', opacity: '0.5'};
ngDevModeResetPerfCounters();
fixture.detectChanges();
assertStyleCounters(3, 0);
assertStyle(element, 'width', '2000px');
assertStyle(element, 'height', '1100px');
assertStyle(element, 'color', 'blue');
assertStyle(element, 'opacity', '0.5');
comp.map = {color: 'blue', width: '2000px'};
ngDevModeResetPerfCounters();
fixture.detectChanges();
// all four are applied because the map was altered
assertStyleCounters(0, 1);
assertStyle(element, 'width', '2000px');
assertStyle(element, 'height', '1100px');
assertStyle(element, 'color', 'blue');
assertStyle(element, 'opacity', '');
});
onlyInIvy('only ivy has [style.prop] support')
.it('should sanitize style values before writing them', () => {
@Component({
template: `
`
})
class Cmp {
widthExp = '';
bgImageExp = '';
styleMapExp: any = {};
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
comp.bgImageExp = 'url("javascript:img")';
fixture.detectChanges();
// for some reasons `background-image: unsafe` is suppressed
expect(getSortedStyle(div)).toEqual('');
fixture.detectChanges();
expect(getSortedStyle(div)).not.toContain('javascript');
// Prove that bindings work.
comp.widthExp = '789px';
comp.bgImageExp = bypassSanitizationTrustStyle(comp.bgImageExp) as string;
fixture.detectChanges();
expect(div.style.getPropertyValue('background-image')).toEqual('url("javascript:img")');
expect(div.style.getPropertyValue('width')).toEqual('789px');
});
onlyInIvy('only ivy has [style] support')
.it('should sanitize style values before writing them', () => {
@Component({
template: `
`
})
class Cmp {
widthExp = '';
styleMapExp: {[key: string]: any} = {};
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
comp.styleMapExp['background-image'] = 'url("javascript:img")';
fixture.detectChanges();
// for some reasons `background-image: unsafe` is suppressed
expect(getSortedStyle(div)).toEqual('');
// for some reasons `border-image: unsafe` is NOT suppressed
fixture.detectChanges();
expect(getSortedStyle(div)).not.toContain('javascript');
// Prove that bindings work.
comp.widthExp = '789px';
comp.styleMapExp = {
'background-image': bypassSanitizationTrustStyle(comp.styleMapExp['background-image'])
};
fixture.detectChanges();
expect(div.style.getPropertyValue('background-image')).toEqual('url("javascript:img")');
expect(div.style.getPropertyValue('width')).toEqual('789px');
});
it('should apply a unit to a style before writing it', () => {
@Component({
template: `
`
})
class Cmp {
widthExp: string|number|null = '';
heightExp: string|number|null = '';
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
comp.widthExp = '200';
comp.heightExp = 10;
fixture.detectChanges();
expect(getSortedStyle(div)).toEqual('height: 10em; width: 200px;');
comp.widthExp = 0;
comp.heightExp = null;
fixture.detectChanges();
expect(getSortedStyle(div)).toEqual('width: 0px;');
});
it('should be able to bind a SafeValue to clip-path', () => {
@Component({template: '
'})
class Cmp {
path!: SafeStyle;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const sanitizer: DomSanitizer = TestBed.inject(DomSanitizer);
fixture.componentInstance.path = sanitizer.bypassSecurityTrustStyle('url("#test")');
fixture.detectChanges();
const html = fixture.nativeElement.innerHTML;
// Note that check the raw HTML, because (at the time of writing) the Node-based renderer
// that we use to run tests doesn't support `clip-path` in `CSSStyleDeclaration`.
expect(html).toMatch(/style=["|']clip-path:\s*url\(.*#test.*\)/);
});
it('should handle values wrapped into SafeValue', () => {
@Component({
template: `
`,
})
class MyComp {
constructor(private sanitizer: DomSanitizer) {}
public width: string = 'calc(20%)';
public height: string = '10px';
public background: string = '1.png';
public color: string = 'red';
private getSafeStyle(value: string) {
return this.sanitizer.bypassSecurityTrustStyle(value);
}
getBackgroundSafe() {
return this.getSafeStyle(`url("/${this.background}")`);
}
getWidthSafe() {
return this.getSafeStyle(this.width);
}
getHeightSafe() {
return this.getSafeStyle(this.height);
}
getColorUnsafe() {
return this.color;
}
}
TestBed.configureTestingModule({
imports: [CommonModule],
declarations: [MyComp],
});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const comp = fixture.componentInstance;
const div = fixture.nativeElement.querySelector('div');
const p = fixture.nativeElement.querySelector('p');
const span = fixture.nativeElement.querySelector('span');
expect(div.style.background).toContain('url("/1.png")');
expect(p.style.width).toBe('calc(20%)');
expect(p.style.height).toBe('10px');
expect(span.style.color).toBe('red');
comp.background = '2.png';
comp.width = '5px';
comp.height = '100%';
comp.color = 'green';
fixture.detectChanges();
expect(div.style.background).toContain('url("/2.png")');
expect(p.style.width).toBe('5px');
expect(p.style.height).toBe('100%');
expect(span.style.color).toBe('green');
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should evaluate follow-up [style] maps even if a former map is null', () => {
@Directive({selector: '[dir-with-styling]'})
class DirWithStyleMap {
@HostBinding('style') public styleMap: any = {color: 'red'};
}
@Directive({selector: '[dir-with-styling-part2]'})
class DirWithStyleMapPart2 {
@HostBinding('style') public styleMap: any = {width: '200px'};
}
@Component({
template: `
`
})
class Cmp {
map: any = null;
@ViewChild('div', {read: DirWithStyleMap, static: true}) dir1!: DirWithStyleMap;
@ViewChild('div', {read: DirWithStyleMapPart2, static: true}) dir2!: DirWithStyleMapPart2;
}
TestBed.configureTestingModule(
{declarations: [Cmp, DirWithStyleMap, DirWithStyleMapPart2]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
expectStyle(element).toEqual({
color: 'red',
width: '200px',
});
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should evaluate initial style/class values on a list of elements that changes', () => {
@Component({
template: `
{{ item }}
`
})
class Cmp {
items = [1, 2, 3];
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
function getItemElements(): HTMLElement[] {
return [].slice.call(fixture.nativeElement.querySelectorAll('div'));
}
function getItemClasses(): string[] {
return getItemElements().map(e => e.className).sort().join(' ').split(' ');
}
expect(getItemElements().length).toEqual(3);
expect(getItemClasses()).toEqual([
'initial-class',
'item-1',
'initial-class',
'item-2',
'initial-class',
'item-3',
]);
comp.items = [2, 4, 6, 8];
fixture.detectChanges();
expect(getItemElements().length).toEqual(4);
expect(getItemClasses()).toEqual([
'initial-class',
'item-2',
'initial-class',
'item-4',
'initial-class',
'item-6',
'initial-class',
'item-8',
]);
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should create and update multiple class bindings across multiple elements in a template',
() => {
@Component({
template: `
{{ item }}
`
})
class Cmp {
items = [1, 2, 3];
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
function getItemElements(): HTMLElement[] {
return [].slice.call(fixture.nativeElement.querySelectorAll('div'));
}
function getItemClasses(): string[] {
return getItemElements().map(e => e.className).sort().join(' ').split(' ');
}
const header = fixture.nativeElement.querySelector('header');
expect(header.classList.contains('header'));
const footer = fixture.nativeElement.querySelector('footer');
expect(footer.classList.contains('footer'));
expect(getItemElements().length).toEqual(3);
expect(getItemClasses()).toEqual([
'item',
'item-1',
'item',
'item-2',
'item',
'item-3',
]);
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should understand multiple directives which contain initial classes', () => {
@Directive({selector: 'dir-one'})
class DirOne {
@HostBinding('class') public className = 'dir-one';
}
@Directive({selector: 'dir-two'})
class DirTwo {
@HostBinding('class') public className = 'dir-two';
}
@Component({
template: `
`
})
class Cmp {
}
TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const dirOne = fixture.nativeElement.querySelector('dir-one');
const div = fixture.nativeElement.querySelector('div');
const dirTwo = fixture.nativeElement.querySelector('dir-two');
expect(dirOne.classList.contains('dir-one')).toBeTruthy();
expect(dirTwo.classList.contains('dir-two')).toBeTruthy();
expect(div.classList.contains('initial')).toBeTruthy();
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should evaluate styling across the template directives when there are multiple elements/sources of styling',
() => {
@Directive({selector: '[one]'})
class DirOne {
@HostBinding('class') public className = 'dir-one';
}
@Directive({selector: '[two]'})
class DirTwo {
@HostBinding('class') public className = 'dir-two';
}
@Component({
template: `
`
})
class Cmp {
w = 100;
h = 200;
c = 'red';
}
TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const divA = fixture.nativeElement.querySelector('.a');
const divB = fixture.nativeElement.querySelector('.b');
const divC = fixture.nativeElement.querySelector('.c');
expect(divA.style.width).toEqual('100px');
expect(divB.style.height).toEqual('200px');
expect(divC.style.color).toEqual('red');
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should evaluate styling across the template and directives within embedded views',
() => {
@Directive({selector: '[some-dir-with-styling]'})
class SomeDirWithStyling {
@HostBinding('style')
public styles = {
width: '200px',
height: '500px',
};
}
@Component({
template: `
{{ item }}
`
})
class Cmp {
items: any[] = [];
c = 'red';
w = 100;
h = 100;
}
TestBed.configureTestingModule({declarations: [Cmp, SomeDirWithStyling]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
comp.items = [1, 2, 3, 4];
fixture.detectChanges();
const items = fixture.nativeElement.querySelectorAll('.item');
expect(items.length).toEqual(4);
const [a, b, c, d] = items;
expect(a.style.height).toEqual('0px');
expect(b.style.height).toEqual('100px');
expect(c.style.height).toEqual('200px');
expect(d.style.height).toEqual('300px');
const section = fixture.nativeElement.querySelector('section');
const p = fixture.nativeElement.querySelector('p');
expect(section.style['width']).toEqual('100px');
expect(p.style['height']).toEqual('100px');
});
onlyInIvy('only ivy has style/class bindings debugging support')
.it('should flush bindings even if any styling hasn\'t changed in a previous directive',
() => {
@Directive({selector: '[one]'})
class DirOne {
@HostBinding('style.width') w = '100px';
@HostBinding('style.opacity') o = '0.5';
}
@Directive({selector: '[two]'})
class DirTwo {
@HostBinding('style.height') h = '200px';
@HostBinding('style.color') c = 'red';
}
@Component({template: '
'})
class Cmp {
@ViewChild('target', {read: DirOne, static: true}) one!: DirOne;
@ViewChild('target', {read: DirTwo, static: true}) two!: DirTwo;
}
TestBed.configureTestingModule({declarations: [Cmp, DirOne, DirTwo]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.style.opacity).toEqual('0.5');
expect(div.style.color).toEqual('red');
expect(div.style.width).toEqual('100px');
expect(div.style.height).toEqual('200px');
comp.two.h = '300px';
fixture.detectChanges();
expect(div.style.opacity).toEqual('0.5');
expect(div.style.color).toEqual('red');
expect(div.style.width).toEqual('100px');
expect(div.style.height).toEqual('300px');
});
it('should work with NO_CHANGE values if they are applied to bindings ', () => {
@Component({
template: `
`
})
class Cmp {
w: any = null;
h: any = null;
o: any = null;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
comp.w = '100px';
comp.h = '200px';
comp.o = '0.5';
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.style.width).toEqual('100px');
expect(div.style.height).toEqual('200px');
expect(div.style.opacity).toEqual('0.5');
comp.w = '500px';
comp.o = '1';
fixture.detectChanges();
expect(div.style.width).toEqual('500px');
expect(div.style.height).toEqual('200px');
expect(div.style.opacity).toEqual('1');
});
it('should allow [ngStyle] and [ngClass] to be used together', () => {
@Component({
template: `
`
})
class Cmp {
c: any = 'foo bar';
s: any = {width: '200px'};
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.style.width).toEqual('200px');
expect(div.classList.contains('foo')).toBeTruthy();
expect(div.classList.contains('bar')).toBeTruthy();
});
it('should allow to reset style property value defined using ngStyle', () => {
@Component({
template: `
`
})
class Cmp {
s: any = {opacity: '1'};
clearStyle(): void {
this.s = null;
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
const comp = fixture.componentInstance;
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.style.opacity).toEqual('1');
comp.clearStyle();
fixture.detectChanges();
expect(div.style.opacity).toEqual('');
});
it('should allow detectChanges to be run in a property change that causes additional styling to be rendered',
() => {
@Component({
selector: 'child',
template: `
`,
})
class ChildCmp {
readyTpl = false;
@HostBinding('class.ready-host') readyHost = false;
}
@Component({
selector: 'parent',
template: `
`,
host: {
'[style.color]': 'color',
},
})
class ParentCmp {
private _prop = '';
@ViewChild('template', {read: ViewContainerRef}) vcr: ViewContainerRef = null!;
private child: ComponentRef
= null!;
@Input()
set prop(value: string) {
this._prop = value;
if (this.child && value === 'go') {
this.child.instance.readyHost = true;
this.child.instance.readyTpl = true;
this.child.changeDetectorRef.detectChanges();
}
}
get prop() {
return this._prop;
}
ngAfterViewInit() {
const factory = this.componentFactoryResolver.resolveComponentFactory(ChildCmp);
this.child = this.vcr.createComponent(factory);
}
constructor(private componentFactoryResolver: ComponentFactoryResolver) {}
}
@Component({
template: ``,
})
class App {
prop = 'a';
}
@NgModule({
entryComponents: [ChildCmp],
declarations: [ChildCmp],
})
class ChildCmpModule {
}
TestBed.configureTestingModule({declarations: [App, ParentCmp], imports: [ChildCmpModule]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges(false);
let readyHost = fixture.nativeElement.querySelector('.ready-host');
let readyChild = fixture.nativeElement.querySelector('.ready-child');
expect(readyHost).toBeFalsy();
expect(readyChild).toBeFalsy();
fixture.componentInstance.prop = 'go';
fixture.detectChanges(false);
readyHost = fixture.nativeElement.querySelector('.ready-host');
readyChild = fixture.nativeElement.querySelector('.ready-child');
expect(readyHost).toBeTruthy();
expect(readyChild).toBeTruthy();
});
it('should allow detectChanges to be run in a hook that causes additional styling to be rendered',
() => {
@Component({
selector: 'child',
template: `
`,
})
class ChildCmp {
readyTpl = false;
@HostBinding('class.ready-host') readyHost = false;
}
@Component({
selector: 'parent',
template: `
`,
})
class ParentCmp {
updateChild = false;
@ViewChild('template', {read: ViewContainerRef}) vcr: ViewContainerRef = null!;
private child: ComponentRef = null!;
ngDoCheck() {
if (this.updateChild) {
this.child.instance.readyHost = true;
this.child.instance.readyTpl = true;
this.child.changeDetectorRef.detectChanges();
}
}
ngAfterViewInit() {
const factory = this.componentFactoryResolver.resolveComponentFactory(ChildCmp);
this.child = this.vcr.createComponent(factory);
}
constructor(private componentFactoryResolver: ComponentFactoryResolver) {}
}
@Component({
template: ``,
})
class App {
@ViewChild('parent', {static: true}) public parent: ParentCmp|null = null;
}
@NgModule({
entryComponents: [ChildCmp],
declarations: [ChildCmp],
})
class ChildCmpModule {
}
TestBed.configureTestingModule({declarations: [App, ParentCmp], imports: [ChildCmpModule]});
const fixture = TestBed.createComponent(App);
fixture.detectChanges(false);
let readyHost = fixture.nativeElement.querySelector('.ready-host');
let readyChild = fixture.nativeElement.querySelector('.ready-child');
expect(readyHost).toBeFalsy();
expect(readyChild).toBeFalsy();
const parent = fixture.componentInstance.parent!;
parent.updateChild = true;
fixture.detectChanges(false);
readyHost = fixture.nativeElement.querySelector('.ready-host');
readyChild = fixture.nativeElement.querySelector('.ready-child');
expect(readyHost).toBeTruthy();
expect(readyChild).toBeTruthy();
});
onlyInIvy('only ivy allows for multiple styles/classes to be balanced across directives')
.it('should allow various duplicate properties to be defined in various styling maps within the template and directive styling bindings',
() => {
@Component({
template: `
`
})
class Cmp {
h = '100px';
w = '100px';
s1: any = {border: '10px solid black', width: '200px'};
s2: any = {border: '10px solid red', width: '300px'};
}
@Directive({selector: '[dir-with-styling]'})
class DirectiveExpectingStyling {
@Input('dir-with-styling') @HostBinding('style') public styles: any = null;
}
TestBed.configureTestingModule({declarations: [Cmp, DirectiveExpectingStyling]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
const element = fixture.nativeElement.querySelector('div');
expect(element.style.border).toEqual('10px solid black');
expect(element.style.width).toEqual('100px');
expect(element.style.height).toEqual('100px');
fixture.componentInstance.s1 = null;
fixture.detectChanges();
expect(element.style.border).toEqual('10px solid red');
expect(element.style.width).toEqual('100px');
expect(element.style.height).toEqual('100px');
});
it('should retrieve styles set via Renderer2', () => {
let dirInstance: any;
@Directive({
selector: '[dir]',
})
class Dir {
constructor(public elementRef: ElementRef, public renderer: Renderer2) {
dirInstance = this;
}
setStyles() {
const nativeEl = this.elementRef.nativeElement;
this.renderer.setStyle(nativeEl, 'transform', 'translate3d(0px, 0px, 0px)');
this.renderer.addClass(nativeEl, 'my-class');
}
}
@Component({template: `
`})
class App {
}
TestBed.configureTestingModule({
declarations: [App, Dir],
});
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
dirInstance.setStyles();
const div = fixture.debugElement.children[0];
expect(div.styles.transform).toMatch(/translate3d\(0px\s*,\s*0px\s*,\s*0px\)/);
expect(div.classes['my-class']).toBe(true);
});
it('should not set classes when falsy value is passed while a sanitizer is present', () => {
@Component({
// Note that we use `background` here because it needs to be sanitized.
template: `
`,
})
class AppComponent {
isDisabled = false;
background = 'orange';
}
TestBed.configureTestingModule({declarations: [AppComponent]});
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const span = fixture.nativeElement.querySelector('span');
expect(span.classList).not.toContain('disabled');
// The issue we're testing for happens after the second change detection.
fixture.detectChanges();
expect(span.classList).not.toContain('disabled');
});
it('should not set classes when falsy value is passed while a sanitizer from host bindings is present',
() => {
@Directive({selector: '[blockStyles]'})
class StylesDirective {
@HostBinding('style.border') border = '1px solid red';
@HostBinding('style.background') background = 'white';
}
@Component({
template: `
`,
})
class AppComponent {
isDisabled = false;
}
TestBed.configureTestingModule({declarations: [AppComponent, StylesDirective]});
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div');
expect(div.classList.contains('disabled')).toBe(false);
// The issue we're testing for happens after the second change detection.
fixture.detectChanges();
expect(div.classList.contains('disabled')).toBe(false);
});
it('should throw an error if a prop-based style/class binding value is changed during checkNoChanges',
() => {
@Component({
template: `
`
})
class Cmp {
color = 'red';
fooClass = true;
ngAfterViewInit() {
this.color = 'blue';
this.fooClass = false;
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
expect(() => {
fixture.detectChanges();
}).toThrowError(/ExpressionChangedAfterItHasBeenCheckedError/);
});
onlyInIvy('only ivy allows for map-based style AND class bindings')
.it('should throw an error if a map-based style/class binding value is changed during checkNoChanges',
() => {
@Component({
template: `
`
})
class Cmp {
style: any = 'width: 100px';
klass: any = 'foo';
ngAfterViewInit() {
this.style = 'height: 200px';
this.klass = 'bar';
}
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
expect(() => {
fixture.detectChanges();
}).toThrowError(/ExpressionChangedAfterItHasBeenCheckedError/);
});
it('should properly merge class interpolation with class-based directives', () => {
@Component(
{template: `
`})
class MyComp {
one = 'one';
}
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
fixture.detectChanges();
expect(fixture.debugElement.nativeElement.innerHTML).toContain('zero');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('one');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('two');
expect(fixture.debugElement.nativeElement.innerHTML).toContain('three');
});
it('should allow to reset style property value defined using [style.prop.px] binding', () => {
@Component({
template: '
',
})
class MyComp {
left = '';
}
TestBed.configureTestingModule({declarations: [MyComp]});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
const checks = [
['15', '15px'],
[undefined, ''],
[null, ''],
['', ''],
['0', '0px'],
];
const div = fixture.nativeElement.querySelector('div');
checks.forEach((check: any[]) => {
const [fieldValue, expectedValue] = check;
fixture.componentInstance.left = fieldValue;
fixture.detectChanges();
expect(div.style.left).toBe(expectedValue);
});
});
onlyInIvy('only ivy treats [class] in concert with other class bindings')
.it('should retain classes added externally', () => {
@Component({template: `
`})
class MyComp {
exp = '';
}
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
fixture.detectChanges();
const div = fixture.nativeElement.querySelector('div')!;
div.className += ' abc';
expect(splitSortJoin(div.className)).toEqual('abc');
fixture.componentInstance.exp = '1 2 3';
fixture.detectChanges();
expect(splitSortJoin(div.className)).toEqual('1 2 3 abc');
fixture.componentInstance.exp = '4 5 6 7';
fixture.detectChanges();
expect(splitSortJoin(div.className)).toEqual('4 5 6 7 abc');
function splitSortJoin(s: string) {
return s.split(/\s+/).sort().join(' ').trim();
}
});
describe('ExpressionChangedAfterItHasBeenCheckedError', () => {
it('should not throw when bound to SafeValue', () => {
@Component({template: `
`})
class MyComp {
icon = 'https://i.imgur.com/4AiXzf8.jpg';
get iconSafe() {
return this.sanitizer.bypassSecurityTrustStyle(`url("${this.icon}")`);
}
constructor(private sanitizer: DomSanitizer) {}
}
const fixture =
TestBed.configureTestingModule({declarations: [MyComp]}).createComponent(MyComp);
fixture.detectChanges(true /* Verify that check no changes does not cause an exception */);
const div: HTMLElement = fixture.nativeElement.querySelector('div');
expect(div.style.getPropertyValue('background-image'))
.toEqual('url("https://i.imgur.com/4AiXzf8.jpg")');
});
});
isBrowser && it('should process
`,
styles: [
'div { width: 100px; }',
]
})
class MyComp {
}
TestBed.configureTestingModule({
declarations: [MyComp],
});
const fixture = TestBed.createComponent(MyComp);
fixture.detectChanges();
// `styles` array values are applied first, styles from