refactor(common): modernize directive tests to rely on whenStable

Replace synchronous detectChanges calls with zoneless-compatible scheduling and stability waits in NgClass, NgStyle, and NgOptimizedImage tests.

(cherry picked from commit 2f0be5bef8)
This commit is contained in:
SkyZeroZx
2026-08-03 14:42:45 -05:00
committed by Jessica Janiuk
parent 32af9b525e
commit f99e916ba7
3 changed files with 499 additions and 593 deletions
+136 -143
View File
@@ -9,7 +9,7 @@
import {ChangeDetectionStrategy} from '@angular/compiler';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {NgClass} from '../../index';
import {NgClass, NgFor} from '../../index';
describe('binding to CSS class list', () => {
let fixture: ComponentFixture<any> | null;
@@ -18,9 +18,9 @@ describe('binding to CSS class list', () => {
return classes.trim().split(' ').sort().join(' ');
}
function detectChangesAndExpectClassName(classes: string): void {
async function waitForStableAndExpectClassName(classes: string): Promise<void> {
fixture?.changeDetectorRef.markForCheck();
fixture!.detectChanges();
await fixture!.whenStable();
let nonNormalizedClassName = fixture!.debugElement.children[0].nativeElement.className;
expect(normalizeClassNames(nonNormalizedClassName)).toEqual(normalizeClassNames(classes));
}
@@ -33,433 +33,426 @@ describe('binding to CSS class list', () => {
fixture = null;
});
beforeEach(() => {
TestBed.configureTestingModule({
declarations: [TestComponent],
});
});
it('should clean up when the directive is destroyed', () => {
it('should clean up when the directive is destroyed', async () => {
fixture = createTestComponent('<div *ngFor="let item of items" [ngClass]="item"></div>');
getComponent().items = [['0']];
fixture.detectChanges();
await fixture.whenStable();
getComponent().items = [['1']];
detectChangesAndExpectClassName('1');
await waitForStableAndExpectClassName('1');
});
describe('expressions evaluating to objects', () => {
it('should add classes specified in an object literal', () => {
it('should add classes specified in an object literal', async () => {
fixture = createTestComponent('<div [ngClass]="{foo: true, bar: false}"></div>');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
});
it('should add classes specified in an object literal without change in class names', () => {
it('should add classes specified in an object literal without change in class names', async () => {
fixture = createTestComponent(`<div [ngClass]="{'foo-bar': true, 'fooBar': true}"></div>`);
detectChangesAndExpectClassName('foo-bar fooBar');
await waitForStableAndExpectClassName('foo-bar fooBar');
});
it('should add and remove classes based on changes in object literal values', () => {
it('should add and remove classes based on changes in object literal values', async () => {
fixture = createTestComponent('<div [ngClass]="{foo: condition, bar: !condition}"></div>');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().condition = false;
detectChangesAndExpectClassName('bar');
await waitForStableAndExpectClassName('bar');
});
it('should add and remove classes based on changes to the expression object', () => {
it('should add and remove classes based on changes to the expression object', async () => {
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
const objExpr = getComponent().objExpr;
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
objExpr!['bar'] = true;
detectChangesAndExpectClassName('foo bar');
await waitForStableAndExpectClassName('foo bar');
objExpr!['baz'] = true;
detectChangesAndExpectClassName('foo bar baz');
await waitForStableAndExpectClassName('foo bar baz');
delete objExpr!['bar'];
detectChangesAndExpectClassName('foo baz');
await waitForStableAndExpectClassName('foo baz');
});
it('should add and remove classes based on reference changes to the expression object', () => {
it('should add and remove classes based on reference changes to the expression object', async () => {
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().objExpr = {foo: true, bar: true};
detectChangesAndExpectClassName('foo bar');
await waitForStableAndExpectClassName('foo bar');
getComponent().objExpr = {baz: true};
detectChangesAndExpectClassName('baz');
await waitForStableAndExpectClassName('baz');
});
it('should remove active classes when expression evaluates to null', () => {
it('should remove active classes when expression evaluates to null', async () => {
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().objExpr = null;
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
getComponent().objExpr = {'foo': false, 'bar': true};
detectChangesAndExpectClassName('bar');
await waitForStableAndExpectClassName('bar');
});
it('should remove active classes when expression evaluates to undefined', () => {
it('should remove active classes when expression evaluates to undefined', async () => {
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().objExpr = undefined;
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
getComponent().objExpr = {'foo': false, 'bar': true};
detectChangesAndExpectClassName('bar');
await waitForStableAndExpectClassName('bar');
});
it('should allow multiple classes per expression', () => {
it('should allow multiple classes per expression', async () => {
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
getComponent().objExpr = {'bar baz': true, 'bar1 baz1': true};
detectChangesAndExpectClassName('bar baz bar1 baz1');
await waitForStableAndExpectClassName('bar baz bar1 baz1');
getComponent().objExpr = {'bar baz': false, 'bar1 baz1': true};
detectChangesAndExpectClassName('bar1 baz1');
await waitForStableAndExpectClassName('bar1 baz1');
});
it('should split by one or more spaces between classes', () => {
it('should split by one or more spaces between classes', async () => {
fixture = createTestComponent('<div [ngClass]="objExpr"></div>');
getComponent().objExpr = {'foo bar baz': true};
detectChangesAndExpectClassName('foo bar baz');
await waitForStableAndExpectClassName('foo bar baz');
});
});
describe('expressions evaluating to lists', () => {
it('should add classes specified in a list literal', () => {
it('should add classes specified in a list literal', async () => {
fixture = createTestComponent(`<div [ngClass]="['foo', 'bar', 'foo-bar', 'fooBar']"></div>`);
detectChangesAndExpectClassName('foo bar foo-bar fooBar');
await waitForStableAndExpectClassName('foo bar foo-bar fooBar');
});
it('should add and remove classes based on changes to the expression', () => {
it('should add and remove classes based on changes to the expression', async () => {
fixture = createTestComponent('<div [ngClass]="arrExpr"></div>');
const arrExpr = getComponent().arrExpr;
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
arrExpr.push('bar');
detectChangesAndExpectClassName('foo bar');
await waitForStableAndExpectClassName('foo bar');
arrExpr[1] = 'baz';
detectChangesAndExpectClassName('foo baz');
await waitForStableAndExpectClassName('foo baz');
getComponent().arrExpr = arrExpr.filter((v: string) => v !== 'baz');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
});
it('should add and remove classes when a reference changes', () => {
it('should add and remove classes when a reference changes', async () => {
fixture = createTestComponent('<div [ngClass]="arrExpr"></div>');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().arrExpr = ['bar'];
detectChangesAndExpectClassName('bar');
await waitForStableAndExpectClassName('bar');
});
it('should take initial classes into account when a reference changes', () => {
it('should take initial classes into account when a reference changes', async () => {
fixture = createTestComponent('<div class="foo" [ngClass]="arrExpr"></div>');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().arrExpr = ['bar'];
detectChangesAndExpectClassName('foo bar');
await waitForStableAndExpectClassName('foo bar');
});
it('should ignore empty or blank class names', () => {
it('should ignore empty or blank class names', async () => {
fixture = createTestComponent('<div class="foo" [ngClass]="arrExpr"></div>');
getComponent().arrExpr = ['', ' '];
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
});
it('should trim blanks from class names', () => {
it('should trim blanks from class names', async () => {
fixture = createTestComponent('<div class="foo" [ngClass]="arrExpr"></div>');
getComponent().arrExpr = [' bar '];
detectChangesAndExpectClassName('foo bar');
await waitForStableAndExpectClassName('foo bar');
});
it('should allow multiple classes per item in arrays', () => {
it('should allow multiple classes per item in arrays', async () => {
fixture = createTestComponent('<div [ngClass]="arrExpr"></div>');
getComponent().arrExpr = ['foo bar baz', 'foo1 bar1 baz1'];
detectChangesAndExpectClassName('foo bar baz foo1 bar1 baz1');
await waitForStableAndExpectClassName('foo bar baz foo1 bar1 baz1');
getComponent().arrExpr = ['foo bar baz foobar'];
detectChangesAndExpectClassName('foo bar baz foobar');
await waitForStableAndExpectClassName('foo bar baz foobar');
});
it('should throw with descriptive error message when CSS class is not a string', () => {
it('should throw with descriptive error message when CSS class is not a string', async () => {
fixture = createTestComponent(`<div [ngClass]="['foo', {}]"></div>`);
expect(() => fixture!.detectChanges()).toThrowError(
await expectAsync(fixture.whenStable()).toBeRejectedWithError(
/NgClass can only toggle CSS classes expressed as strings, got \[object Object\]/,
);
});
});
describe('expressions evaluating to sets', () => {
it('should add and remove classes if the set instance changed', () => {
it('should add and remove classes if the set instance changed', async () => {
fixture = createTestComponent('<div [ngClass]="setExpr"></div>');
let setExpr = new Set<string>();
setExpr.add('bar');
getComponent().setExpr = setExpr;
detectChangesAndExpectClassName('bar');
await waitForStableAndExpectClassName('bar');
setExpr = new Set<string>();
setExpr.add('baz');
getComponent().setExpr = setExpr;
detectChangesAndExpectClassName('baz');
await waitForStableAndExpectClassName('baz');
});
});
describe('expressions evaluating to string', () => {
it('should add classes specified in a string literal', () => {
it('should add classes specified in a string literal', async () => {
fixture = createTestComponent(`<div [ngClass]="'foo bar foo-bar fooBar'"></div>`);
detectChangesAndExpectClassName('foo bar foo-bar fooBar');
await waitForStableAndExpectClassName('foo bar foo-bar fooBar');
});
it('should add and remove classes based on changes to the expression', () => {
it('should add and remove classes based on changes to the expression', async () => {
fixture = createTestComponent('<div [ngClass]="strExpr"></div>');
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().strExpr = 'foo bar';
detectChangesAndExpectClassName('foo bar');
await waitForStableAndExpectClassName('foo bar');
getComponent().strExpr = 'baz';
detectChangesAndExpectClassName('baz');
await waitForStableAndExpectClassName('baz');
});
it('should remove active classes when switching from string to null', () => {
it('should remove active classes when switching from string to null', async () => {
fixture = createTestComponent(`<div [ngClass]="strExpr"></div>`);
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().strExpr = null;
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
});
it('should remove active classes when switching from string to undefined', () => {
it('should remove active classes when switching from string to undefined', async () => {
fixture = createTestComponent(`<div [ngClass]="strExpr"></div>`);
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().strExpr = undefined;
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
});
it('should take initial classes into account when switching from string to null', () => {
it('should take initial classes into account when switching from string to null', async () => {
fixture = createTestComponent(`<div class="foo" [ngClass]="strExpr"></div>`);
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().strExpr = null;
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
});
it('should take initial classes into account when switching from string to undefined', () => {
it('should take initial classes into account when switching from string to undefined', async () => {
fixture = createTestComponent(`<div class="foo" [ngClass]="strExpr"></div>`);
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
getComponent().strExpr = undefined;
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
});
it('should ignore empty and blank strings', () => {
it('should ignore empty and blank strings', async () => {
fixture = createTestComponent(`<div class="foo" [ngClass]="strExpr"></div>`);
getComponent().strExpr = '';
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
});
});
describe('cooperation with other class-changing constructs', () => {
it('should co-operate with the class attribute', () => {
it('should co-operate with the class attribute', async () => {
fixture = createTestComponent('<div [ngClass]="objExpr" class="init foo"></div>');
const objExpr = getComponent().objExpr;
objExpr!['bar'] = true;
detectChangesAndExpectClassName('init foo bar');
await waitForStableAndExpectClassName('init foo bar');
objExpr!['foo'] = false;
detectChangesAndExpectClassName('init bar');
await waitForStableAndExpectClassName('init bar');
getComponent().objExpr = null;
detectChangesAndExpectClassName('init foo');
await waitForStableAndExpectClassName('init foo');
getComponent().objExpr = undefined;
detectChangesAndExpectClassName('init foo');
await waitForStableAndExpectClassName('init foo');
});
it('should co-operate with the interpolated class attribute', () => {
it('should co-operate with the interpolated class attribute', async () => {
fixture = createTestComponent(`<div [ngClass]="objExpr" class="{{'init foo'}}"></div>`);
const objExpr = getComponent().objExpr;
objExpr!['bar'] = true;
detectChangesAndExpectClassName(`init foo bar`);
await waitForStableAndExpectClassName(`init foo bar`);
objExpr!['foo'] = false;
detectChangesAndExpectClassName(`init bar`);
await waitForStableAndExpectClassName(`init bar`);
getComponent().objExpr = null;
detectChangesAndExpectClassName(`init foo`);
await waitForStableAndExpectClassName(`init foo`);
getComponent().objExpr = undefined;
detectChangesAndExpectClassName(`init foo`);
await waitForStableAndExpectClassName(`init foo`);
});
it('should co-operate with the interpolated class attribute when interpolation changes', () => {
it('should co-operate with the interpolated class attribute when interpolation changes', async () => {
fixture = createTestComponent(
`<div [ngClass]="{large: false, small: true}" class="{{strExpr}}"></div>`,
);
detectChangesAndExpectClassName(`foo small`);
await waitForStableAndExpectClassName(`foo small`);
getComponent().strExpr = 'bar';
detectChangesAndExpectClassName(`bar small`);
await waitForStableAndExpectClassName(`bar small`);
getComponent().strExpr = undefined;
detectChangesAndExpectClassName(`small`);
await waitForStableAndExpectClassName(`small`);
});
it('should co-operate with the class attribute and binding to it', () => {
it('should co-operate with the class attribute and binding to it', async () => {
fixture = createTestComponent(`<div [ngClass]="objExpr" class="init" [class]="'foo'"></div>`);
const objExpr = getComponent().objExpr;
objExpr!['bar'] = true;
detectChangesAndExpectClassName(`init foo bar`);
await waitForStableAndExpectClassName(`init foo bar`);
objExpr!['foo'] = false;
detectChangesAndExpectClassName(`init bar`);
await waitForStableAndExpectClassName(`init bar`);
getComponent().objExpr = null;
detectChangesAndExpectClassName(`init foo`);
await waitForStableAndExpectClassName(`init foo`);
getComponent().objExpr = undefined;
detectChangesAndExpectClassName(`init foo`);
await waitForStableAndExpectClassName(`init foo`);
});
it('should co-operate with the class attribute and class.name binding', () => {
it('should co-operate with the class attribute and class.name binding', async () => {
const template = '<div class="init foo" [ngClass]="objExpr" [class.baz]="condition"></div>';
fixture = createTestComponent(template);
const objExpr = getComponent().objExpr;
detectChangesAndExpectClassName('init foo baz');
await waitForStableAndExpectClassName('init foo baz');
objExpr!['bar'] = true;
detectChangesAndExpectClassName('init foo baz bar');
await waitForStableAndExpectClassName('init foo baz bar');
objExpr!['foo'] = false;
detectChangesAndExpectClassName('init baz bar');
await waitForStableAndExpectClassName('init baz bar');
getComponent().condition = false;
detectChangesAndExpectClassName('init bar');
await waitForStableAndExpectClassName('init bar');
});
it('should co-operate with initial class and class attribute binding when binding changes', () => {
it('should co-operate with initial class and class attribute binding when binding changes', async () => {
const template = '<div class="init" [ngClass]="objExpr" [class]="strExpr"></div>';
fixture = createTestComponent(template);
const cmp = getComponent();
detectChangesAndExpectClassName('init foo');
await waitForStableAndExpectClassName('init foo');
cmp.objExpr!['bar'] = true;
detectChangesAndExpectClassName('init foo bar');
await waitForStableAndExpectClassName('init foo bar');
cmp.strExpr = 'baz';
detectChangesAndExpectClassName('init bar baz foo');
await waitForStableAndExpectClassName('init bar baz foo');
cmp.objExpr = null;
detectChangesAndExpectClassName('init baz');
await waitForStableAndExpectClassName('init baz');
cmp.objExpr = undefined;
detectChangesAndExpectClassName('init baz');
await waitForStableAndExpectClassName('init baz');
});
});
describe('prevent regressions', () => {
// https://github.com/angular/angular/issues/34336
it('should not write to the native node unless the bound expression has changed', () => {
it('should not write to the native node unless the bound expression has changed', async () => {
fixture = createTestComponent(`<div [ngClass]="{'color-red': condition}"></div>`);
detectChangesAndExpectClassName('color-red');
await waitForStableAndExpectClassName('color-red');
// Overwrite CSS classes so that we can check if ngClass performed DOM manipulation to
// update it
fixture.debugElement.children[0].nativeElement.className = '';
// Assert that the DOM node still has the same value after change detection
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
fixture.componentInstance.condition = false;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
fixture.componentInstance.condition = true;
fixture.changeDetectorRef.markForCheck();
detectChangesAndExpectClassName('color-red');
await waitForStableAndExpectClassName('color-red');
});
it('should not write to the native node when values are the same (obj reference change)', () => {
it('should not write to the native node when values are the same (obj reference change)', async () => {
fixture = createTestComponent(`<div [ngClass]="objExpr"></div>`);
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
// Overwrite CSS classes so that we can check if ngClass performed DOM manipulation to
// update it
fixture.debugElement.children[0].nativeElement.className = '';
// Assert that the DOM node still has the same value after change detection
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
// change the object reference (without changing values)
fixture.componentInstance.objExp = {...fixture.componentInstance.objExp};
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
});
it('should not write to the native node when values are the same (array reference change)', () => {
it('should not write to the native node when values are the same (array reference change)', async () => {
fixture = createTestComponent(`<div [ngClass]="arrExpr"></div>`);
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
// Overwrite CSS classes so that we can check if ngClass performed DOM manipulation to
// update it
fixture.debugElement.children[0].nativeElement.className = '';
// Assert that the DOM node still has the same value after change detection
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
// change the object reference (without changing values)
fixture.componentInstance.arrExpr = [...fixture.componentInstance.arrExpr];
detectChangesAndExpectClassName('');
await waitForStableAndExpectClassName('');
});
it('should not add css class when bound initial class is removed by ngClass binding', () => {
it('should not add css class when bound initial class is removed by ngClass binding', async () => {
fixture = createTestComponent(`<div [class]="'bar'" [ngClass]="objExpr"></div>`);
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
});
it('should not add css class when static initial class is removed by ngClass binding', () => {
it('should not add css class when static initial class is removed by ngClass binding', async () => {
fixture = createTestComponent(`<div class="bar" [ngClass]="objExpr"></div>`);
detectChangesAndExpectClassName('foo');
await waitForStableAndExpectClassName('foo');
});
it('should allow classes with trailing and leading spaces in [ngClass]', () => {
it('should allow classes with trailing and leading spaces in [ngClass]', async () => {
@Component({
template: `
<div leading-space [ngClass]="{' foo': applyClasses}"></div>
<div trailing-space [ngClass]="{'foo ': applyClasses}"></div>
`,
standalone: false,
imports: [NgClass],
changeDetection: ChangeDetectionStrategy.Eager,
})
class Cmp {
applyClasses = true;
}
TestBed.configureTestingModule({declarations: [Cmp]});
const fixture = TestBed.createComponent(Cmp);
fixture.detectChanges();
await fixture.whenStable();
const leading = fixture.nativeElement.querySelector('[leading-space]');
const trailing = fixture.nativeElement.querySelector('[trailing-space]');
@@ -467,7 +460,7 @@ describe('binding to CSS class list', () => {
expect(trailing.className).toBe('foo');
});
it('should mix class and ngClass bindings with the same value', () => {
it('should mix class and ngClass bindings with the same value', async () => {
@Component({
selector: 'test-component',
imports: [NgClass],
@@ -479,17 +472,17 @@ describe('binding to CSS class list', () => {
}
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.firstChild.className).toBe('option-1');
fixture.componentInstance.level = 5;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.firstChild.className).toBe('option-5');
});
it('should be available as a standalone directive', () => {
it('should be available as a standalone directive', async () => {
@Component({
selector: 'test-component',
imports: [NgClass],
@@ -500,7 +493,7 @@ describe('binding to CSS class list', () => {
}
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
await fixture.whenStable();
expect(fixture.nativeElement.firstChild.className).toBe('foo');
});
@@ -510,7 +503,7 @@ describe('binding to CSS class list', () => {
@Component({
selector: 'test-cmp',
template: '',
standalone: false,
imports: [NgClass, NgFor],
changeDetection: ChangeDetectionStrategy.Eager,
})
class TestComponent {
File diff suppressed because it is too large Load Diff
@@ -9,7 +9,7 @@
import {ChangeDetectionStrategy} from '@angular/compiler';
import {Component} from '@angular/core';
import {ComponentFixture, TestBed} from '@angular/core/testing';
import {CommonModule, NgStyle} from '../../index';
import {NgStyle} from '../../index';
describe('NgStyle', () => {
let fixture: ComponentFixture<TestComponent>;
@@ -32,112 +32,102 @@ describe('NgStyle', () => {
fixture = null!;
});
beforeEach(() => {
TestBed.configureTestingModule({declarations: [TestComponent], imports: [CommonModule]});
});
it('should add styles specified in an object literal', () => {
it('should add styles specified in an object literal', async () => {
const template = `<div [ngStyle]="{'max-width': '40px'}"></div>`;
fixture = createTestComponent(template);
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
});
it('should add and change styles specified in an object expression', () => {
it('should add and change styles specified in an object expression', async () => {
const template = `<div [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);
getComponent().expr = {'max-width': '40px'};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
let expr = getComponent().expr;
expr['max-width'] = '30%';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '30%'});
});
it('should remove styles with a null expression', () => {
it('should remove styles with a null expression', async () => {
const template = `<div [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);
getComponent().expr = {'max-width': '40px'};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
getComponent().expr = null;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
});
it('should remove styles with an undefined expression', () => {
it('should remove styles with an undefined expression', async () => {
const template = `<div [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);
getComponent().expr = {'max-width': '40px'};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
getComponent().expr = undefined;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
});
it('should add and remove styles specified using style.unit notation', () => {
it('should add and remove styles specified using style.unit notation', async () => {
const template = `<div [ngStyle]="{'max-width.px': expr}"></div>`;
fixture = createTestComponent(template);
getComponent().expr = '40';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
getComponent().expr = null;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
});
// https://github.com/angular/angular/issues/21064
it('should add and remove styles which names are not dash-cased', () => {
it('should add and remove styles which names are not dash-cased', async () => {
fixture = createTestComponent(`<div [ngStyle]="{'color': expr}"></div>`);
getComponent().expr = 'green';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'color': 'green'});
getComponent().expr = null;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).not.toHaveCssStyle('color');
});
it('should update styles using style.unit notation when unit changes', () => {
it('should update styles using style.unit notation when unit changes', async () => {
const template = `<div [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);
getComponent().expr = {'max-width.px': '40'};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
getComponent().expr = {'max-width.em': '40'};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40em'});
});
// keyValueDiffer is sensitive to key order #9115
it('should change styles specified in an object expression', () => {
it('should change styles specified in an object expression', async () => {
const template = `<div [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);
@@ -147,9 +137,7 @@ describe('NgStyle', () => {
height: '10px',
width: '10px',
};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'height': '10px', 'width': '10px'});
getComponent().expr = {
@@ -159,96 +147,89 @@ describe('NgStyle', () => {
};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'height': '5px', 'width': '5px'});
});
it('should remove styles when deleting a key in an object expression', () => {
it('should remove styles when deleting a key in an object expression', async () => {
const template = `<div [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);
getComponent().expr = {'max-width': '40px'};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px'});
delete getComponent().expr['max-width'];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
});
it('should co-operate with the style attribute', () => {
it('should co-operate with the style attribute', async () => {
const template = `<div style="font-size: 12px" [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);
getComponent().expr = {'max-width': '40px'};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px', 'font-size': '12px'});
delete getComponent().expr['max-width'];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
expectNativeEl(fixture).toHaveCssStyle({'font-size': '12px'});
});
it('should co-operate with the style.[styleName]="expr" special-case in the compiler', () => {
it('should co-operate with the style.[styleName]="expr" special-case in the compiler', async () => {
const template = `<div [style.font-size.px]="12" [ngStyle]="expr"></div>`;
fixture = createTestComponent(template);
getComponent().expr = {'max-width': '40px'};
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'max-width': '40px', 'font-size': '12px'});
delete getComponent().expr['max-width'];
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).not.toHaveCssStyle('max-width');
expectNativeEl(fixture).toHaveCssStyle({'font-size': '12px'});
});
it('should not write to the native node unless the bound expression has changed', () => {
it('should not write to the native node unless the bound expression has changed', async () => {
const template = `<div [ngStyle]="{'color': expr}"></div>`;
fixture = createTestComponent(template);
fixture.componentInstance.expr = 'red';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'color': 'red'});
// Overwrite native styles so that we can check if ngStyle has performed DOM manupulation to
// update it.
fixture.debugElement.children[0].nativeElement.style.color = 'blue';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
// Assert that the style hasn't been updated
expectNativeEl(fixture).toHaveCssStyle({'color': 'blue'});
fixture.componentInstance.expr = 'yellow';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
// Assert that the style has changed now that the model has changed
expectNativeEl(fixture).toHaveCssStyle({'color': 'yellow'});
});
it('should correctly update style with units (.px) when the model is set to number', () => {
it('should correctly update style with units (.px) when the model is set to number', async () => {
const template = `<div [ngStyle]="{'width.px': expr}"></div>`;
fixture = createTestComponent(template);
fixture.componentInstance.expr = 400;
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'width': '400px'});
});
it('should handle CSS variables', () => {
it('should handle CSS variables', async () => {
if (!supportsCssVariables) {
return;
}
@@ -256,14 +237,13 @@ describe('NgStyle', () => {
const template = `<div style="width: var(--width)" [ngStyle]="{'--width': expr}"></div>`;
fixture = createTestComponent(template);
fixture.componentInstance.expr = '100px';
fixture.changeDetectorRef.markForCheck();
fixture.detectChanges();
await fixture.whenStable();
const target: HTMLElement = fixture.nativeElement.querySelector('div');
expect(getComputedStyle(target).getPropertyValue('width')).toEqual('100px');
});
it('should be available as a standalone directive', () => {
it('should be available as a standalone directive', async () => {
@Component({
selector: 'test-component',
imports: [NgStyle],
@@ -275,7 +255,7 @@ describe('NgStyle', () => {
}
const fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
await fixture.whenStable();
expectNativeEl(fixture).toHaveCssStyle({'width': '400px'});
});
@@ -284,7 +264,7 @@ describe('NgStyle', () => {
@Component({
selector: 'test-cmp',
template: '',
standalone: false,
imports: [NgStyle],
changeDetection: ChangeDetectionStrategy.Eager,
})
class TestComponent {