From f99e916ba7fc0f6f80fcf579f0a9a47b91241325 Mon Sep 17 00:00:00 2001 From: SkyZeroZx <73321943+SkyZeroZx@users.noreply.github.com> Date: Mon, 3 Aug 2026 14:42:45 -0500 Subject: [PATCH] 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 2f0be5bef83ce15668fd401a470b4a5abe9c7f88) --- .../common/test/directives/ng_class_spec.ts | 279 ++++--- .../directives/ng_optimized_image_spec.ts | 705 ++++++++---------- .../common/test/directives/ng_style_spec.ts | 108 ++- 3 files changed, 499 insertions(+), 593 deletions(-) diff --git a/packages/common/test/directives/ng_class_spec.ts b/packages/common/test/directives/ng_class_spec.ts index 09a31f899b4..3cdf8945dea 100644 --- a/packages/common/test/directives/ng_class_spec.ts +++ b/packages/common/test/directives/ng_class_spec.ts @@ -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 | 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 { 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('
'); 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('
'); - 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(`
`); - 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('
'); - 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('
'); 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('
'); - 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('
'); - 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('
'); - 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('
'); 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('
'); 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(`
`); - 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('
'); 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('
'); - 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('
'); - 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('
'); getComponent().arrExpr = ['', ' ']; - detectChangesAndExpectClassName('foo'); + await waitForStableAndExpectClassName('foo'); }); - it('should trim blanks from class names', () => { + it('should trim blanks from class names', async () => { fixture = createTestComponent('
'); 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('
'); 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(`
`); - 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('
'); let setExpr = new Set(); setExpr.add('bar'); getComponent().setExpr = setExpr; - detectChangesAndExpectClassName('bar'); + await waitForStableAndExpectClassName('bar'); setExpr = new Set(); 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(`
`); - 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('
'); - 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(`
`); - 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(`
`); - 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(`
`); - 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(`
`); - 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(`
`); 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('
'); 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(`
`); 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( `
`, ); - 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(`
`); 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 = '
'; 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 = '
'; 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(`
`); - 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(`
`); - 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(`
`); - 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(`
`); - 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(`
`); - 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: `
`, - 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 { diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts index 19524b751c3..9ce03c14890 100644 --- a/packages/common/test/directives/ng_optimized_image_spec.ts +++ b/packages/common/test/directives/ng_optimized_image_spec.ts @@ -11,7 +11,7 @@ import {ComponentFixture, TestBed} from '@angular/core/testing'; import {DomSanitizer, SafeResourceUrl} from '@angular/platform-browser'; import {isBrowser, isNode, withHead} from '@angular/private/testing'; import {expect} from '@angular/private/testing/matchers'; -import {CommonModule, DOCUMENT, IMAGE_CONFIG, ImageConfig} from '../../index'; +import {DOCUMENT, IMAGE_CONFIG, ImageConfig} from '../../index'; import {RuntimeErrorCode} from '../../src/errors'; import {PLATFORM_SERVER_ID} from '../../src/platform_id'; @@ -47,7 +47,7 @@ describe('Image directive', () => { globalThis['ngServerMode'] = undefined; }); - it('should create `` element when the image priority attr is true', () => { + it('should create `` element when the image priority attr is true', async () => { // Only run this test in a browser since the Node-based DOM mocks don't // allow to override `HTMLImageElement.prototype.setAttribute` easily. if (!isBrowser) return; @@ -78,7 +78,7 @@ describe('Image directive', () => { ).and.callThrough(); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); const head = _document.head; @@ -103,7 +103,7 @@ describe('Image directive', () => { preloadLink!.remove(); }); - it('should not create a preload `` element when src is already preloaded.', () => { + it('should not create a preload `` element when src is already preloaded.', async () => { // Only run this test in a browser since the Node-based DOM mocks don't // allow to override `HTMLImageElement.prototype.setAttribute` easily. if (!isBrowser) return; @@ -128,7 +128,7 @@ describe('Image directive', () => { const _document = TestBed.inject(DOCUMENT); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); const head = _document.head; @@ -177,7 +177,7 @@ describe('Image directive', () => { preloadLinks.forEach((link) => link.remove()); }); - it('should warn when the number of preloaded images is larger than the limit', () => { + it('should warn when the number of preloaded images is larger than the limit', async () => { // Only run this test in a browser since the Node-based DOM mocks don't // allow to override `HTMLImageElement.prototype.setAttribute` easily. if (!isBrowser) return; @@ -206,7 +206,7 @@ describe('Image directive', () => { const consoleWarnSpy = spyOn(console, 'warn'); const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch( /NG02961: The `NgOptimizedImage` directive has detected that more than 5 images were marked as priority/, @@ -214,7 +214,7 @@ describe('Image directive', () => { }); }); - it('should not hit max preload limit when not on the server', () => { + it('should not hit max preload limit when not on the server', async () => { // Only run this test in a browser since the Node-based DOM mocks don't // allow to override `HTMLImageElement.prototype.setAttribute` easily. if (!isBrowser) return; @@ -245,7 +245,7 @@ describe('Image directive', () => { const _document = TestBed.inject(DOCUMENT); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); const head = _document.head; @@ -258,7 +258,7 @@ describe('Image directive', () => { }); }); - it('should set `loading` and `fetchpriority` attributes before `src`', () => { + it('should set `loading` and `fetchpriority` attributes before `src`', async () => { // Only run this test in a browser since the Node-based DOM mocks don't // allow to override `HTMLImageElement.prototype.setAttribute` easily. if (!isBrowser) return; @@ -276,7 +276,7 @@ describe('Image directive', () => { ).and.callThrough(); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; @@ -310,12 +310,12 @@ describe('Image directive', () => { expect(_fetchpriorityAttrId).toBeLessThan(_srcAttrId); // was set after `src` }); - it('should always reflect the width/height attributes if bound', () => { + it('should always reflect the width/height attributes if bound', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -324,14 +324,12 @@ describe('Image directive', () => { }); describe('setup error handling', () => { - it('should throw if both `src` and `ngSrc` are present', () => { + it('should throw if both `src` and `ngSrc` are present', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02950: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="path/img.png"`) has detected that both ' + '`src` and `ngSrc` have been set. Supplying both of these attributes ' + @@ -341,15 +339,13 @@ describe('Image directive', () => { ); }); - it('should throw if both `ngSrcet` and `srcset` is present', () => { + it('should throw if both `ngSrcet` and `srcset` is present', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02951: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="img-100.png"`) has detected that both ' + '`srcset` and `ngSrcset` have been set. Supplying both of these ' + @@ -360,14 +356,12 @@ describe('Image directive', () => { ); }); - it('should throw if `ngSrc` contains a Base64-encoded image (that starts with `data:`)', () => { + it('should throw if `ngSrc` contains a Base64-encoded image (that starts with `data:`)', async () => { setupTestingModule(); - expect(() => { - const template = ''; - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const template = ''; + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive has detected that `ngSrc` ' + 'is a Base64-encoded string (data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDov...). ' + 'NgOptimizedImage does not support Base64-encoded strings. ' + @@ -385,7 +379,7 @@ describe('Image directive', () => { } const canvas = document.createElement('canvas'); - canvas.toBlob(function (blob) { + canvas.toBlob(async function (blob) { const blobURL = URL.createObjectURL(blob!); setupTestingModule(); @@ -394,23 +388,19 @@ describe('Image directive', () => { // is created dynamically, so it might be different for each invocation. const errorMessageRegExp = /NG02952: The NgOptimizedImage directive (.*?) has detected that `ngSrc` was set to a blob URL \(blob:/; - expect(() => { - const template = ''; - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError(errorMessageRegExp); + const template = ''; + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError(errorMessageRegExp); done(); }); }); - it('should throw if `width` and `height` are not set', () => { + it('should throw if `width` and `height` are not set', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02954: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="img.png"`) has detected that these ' + 'required attributes are missing: "width", "height". Including "width" and ' + @@ -420,14 +410,12 @@ describe('Image directive', () => { ); }); - it('should throw if `width` is not set', () => { + it('should throw if `width` is not set', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02954: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="img.png"`) has detected that these ' + 'required attributes are missing: "width". Including "width" and ' + @@ -437,14 +425,12 @@ describe('Image directive', () => { ); }); - it('should throw if `width` is 0', () => { + it('should throw if `width` is 0', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="img.png"`) has detected that `width` ' + 'has an invalid value. To fix this, provide `width` as ' + @@ -452,14 +438,12 @@ describe('Image directive', () => { ); }); - it('should throw if `width` has an invalid value', () => { + it('should throw if `width` has an invalid value', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="img.png"`) has detected that `width` ' + 'has an invalid value. To fix this, provide `width` ' + @@ -467,14 +451,12 @@ describe('Image directive', () => { ); }); - it('should throw if `height` is not set', () => { + it('should throw if `height` is not set', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02954: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="img.png"`) has detected that these required ' + 'attributes are missing: "height". Including "width" and "height" ' + @@ -484,14 +466,12 @@ describe('Image directive', () => { ); }); - it('should throw if `height` is 0', () => { + it('should throw if `height` is 0', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="img.png"`) has detected that `height` ' + 'has an invalid value. To fix this, provide `height` as a number ' + @@ -499,28 +479,24 @@ describe('Image directive', () => { ); }); - it('should throw if `height` has an invalid value', () => { + it('should throw if `height` has an invalid value', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an element ' + 'with the `ngSrc="img.png"`) has detected that `height` has an invalid ' + 'value. To fix this, provide `height` as a number greater than 0.', ); }); - it('should throw if `ngSrc` value is not provided', () => { + it('should throw if `ngSrc` value is not provided', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc=""`) has detected that `ngSrc` has an ' + 'invalid value (``). ' + @@ -528,14 +504,12 @@ describe('Image directive', () => { ); }); - it('should throw if `ngSrc` value is set to an empty string', () => { + it('should throw if `ngSrc` value is set to an empty string', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an element ' + 'with the `ngSrc=" "`) has detected that `ngSrc` has an invalid value ' + '(` `). To fix this, change the value to a non-empty string.', @@ -545,7 +519,7 @@ describe('Image directive', () => { describe('invalid `ngSrcset` values', () => { const mockDirectiveInstance = {ngSrc: 'img.png'} as NgOptimizedImage; - it('should throw for empty ngSrcSet', () => { + it('should throw for empty ngSrcSet', async () => { const imageLoader = (config: ImageLoaderConfig) => { const width = config.width ? `-${config.width}` : ``; return window.location.origin + `/path/${config.src}${width}.png`; @@ -555,10 +529,8 @@ describe('Image directive', () => { const template = ` `; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an ' + 'element with the `ngSrc="img"`) has detected that `ngSrcset` ' + 'has an invalid value (``). ' + @@ -566,7 +538,7 @@ describe('Image directive', () => { ); }); - it('should throw for invalid ngSrcSet', () => { + it('should throw for invalid ngSrcSet', async () => { const imageLoader = (config: ImageLoaderConfig) => { const width = config.width ? `-${config.width}` : ``; return window.location.origin + `/path/${config.src}${width}.png`; @@ -576,10 +548,8 @@ describe('Image directive', () => { const template = ` `; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an element ' + 'with the `ngSrc="img"`) has detected that `ngSrcset` has an invalid value ' + '(`100q, 200q`). To fix this, supply `ngSrcset` using a comma-separated list ' + @@ -588,7 +558,7 @@ describe('Image directive', () => { ); }); - it('should throw if ngSrcset exceeds the density cap', () => { + it('should throw if ngSrcset exceeds the density cap', async () => { const imageLoader = (config: ImageLoaderConfig) => { const width = config.width ? `-${config.width}` : ``; return window.location.origin + `/path/${config.src}${width}.png`; @@ -598,10 +568,8 @@ describe('Image directive', () => { const template = ` `; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( `NG0${RuntimeErrorCode.INVALID_INPUT}: The NgOptimizedImage directive (activated on an element with the \`ngSrc="img"\`) ` + `has detected that the \`ngSrcset\` contains an unsupported image density:` + `\`1x, 2x, 3x, 4x, 5x\`. NgOptimizedImage generally recommends a max image density of ` + @@ -613,7 +581,7 @@ describe('Image directive', () => { ); }); - it('should throw if ngSrcset exceeds the density cap with multiple digits', () => { + it('should throw if ngSrcset exceeds the density cap with multiple digits', async () => { const imageLoader = (config: ImageLoaderConfig) => { const width = config.width ? `-${config.width}` : ``; return window.location.origin + `/path/${config.src}${width}.png`; @@ -623,10 +591,8 @@ describe('Image directive', () => { const template = ` `; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( `NG0${RuntimeErrorCode.INVALID_INPUT}: The NgOptimizedImage directive (activated on an element with the \`ngSrc="img"\`) ` + `has detected that the \`ngSrcset\` contains an unsupported image density:` + `\`1x, 200x\`. NgOptimizedImage generally recommends a max image density of ` + @@ -716,7 +682,7 @@ describe('Image directive', () => { ['loaderParams', '{foo: "test1"}'], ]; inputs.forEach(([inputName, value]) => { - it(`should throw if the \`${inputName}\` input changed after directive initialized the input`, () => { + it(`should throw if the \`${inputName}\` input changed after directive initialized the input`, async () => { @Component({ selector: 'test-cmp', template: ` { [disableOptimizedSrcset]="disableOptimizedSrcset" [loaderParams]="loaderParams" />`, - standalone: false, + imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent { @@ -749,21 +715,21 @@ describe('Image directive', () => { // Initial render const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); const expectedErrorMessage = // `NG02953: The NgOptimizedImage directive (.*)? ` + `has detected that \`${inputName}\` was updated after initialization`; - expect(() => { - // Update input (expect to throw) - (fixture.componentInstance as unknown as {[key: string]: unknown})[inputName as string] = - value; - fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); - }).toThrowError(new RegExp(expectedErrorMessage)); + // Update input (expect to throw) + (fixture.componentInstance as unknown as {[key: string]: unknown})[inputName as string] = + value; + fixture.changeDetectorRef.markForCheck(); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( + new RegExp(expectedErrorMessage), + ); }); }); - it(`should not throw if ngSrc changed after directive is initialized`, () => { + it(`should not throw if ngSrc changed after directive is initialized`, async () => { @Component({ selector: 'test-cmp', template: ` { [loading]="loading" [sizes]="sizes" />`, - standalone: false, + imports: [NgOptimizedImage], }) class TestComponent { width = 100; @@ -787,17 +753,16 @@ describe('Image directive', () => { // Initial render const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); - expect(() => { - fixture.componentInstance.ngSrc = 'newImg.png'; - fixture.detectChanges(); - }).not.toThrowError(new RegExp('was updated after initialization')); + await fixture.whenStable(); + fixture.componentInstance.ngSrc = 'newImg.png'; + fixture.changeDetectorRef.markForCheck(); + await expectAsync(fixture.whenStable()).toBeResolved(); }); - it('should accept a safeUrl ngSrc value', () => { + it('should accept a safeUrl ngSrc value', async () => { @Component({ selector: 'test-cmp', template: ``, - standalone: false, + imports: [NgOptimizedImage], }) class TestComponent { rawImage = `javascript:alert("Hi there")`; @@ -808,7 +773,7 @@ describe('Image directive', () => { } setupTestingModule({component: TestComponent}); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); let nativeElement = fixture.nativeElement as HTMLElement; let img = nativeElement.querySelector('img')!; @@ -817,24 +782,24 @@ describe('Image directive', () => { }); describe('lazy loading', () => { - it('should eagerly load priority images', () => { + it('should eagerly load priority images', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('loading')).toBe('eager'); }); - it('should lazily load non-priority images', () => { + it('should lazily load non-priority images', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -843,15 +808,13 @@ describe('Image directive', () => { }); describe('decoding attribute', () => { - it('should throw for invalid loading inputs', () => { + it('should throw for invalid loading inputs', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive ' + '(activated on an element with the `ngSrc="path/img.png"`) has detected ' + 'that the `decoding` attribute has an invalid value (`unknown_value`). ' + @@ -859,36 +822,36 @@ describe('Image directive', () => { ); }); - it('should set the decoding to "auto" by default', () => { + it('should set the decoding to "auto" by default', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('decoding')).toEqual('auto'); }); - it('should set the decoding to sync for priority images', () => { + it('should set the decoding to sync for priority images', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('decoding')).toEqual('sync'); }); - it('should override the default decoding behavior', () => { + it('should override the default decoding behavior', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -897,27 +860,25 @@ describe('Image directive', () => { }); describe('loading attribute', () => { - it('should override the default loading behavior for non-priority images', () => { + it('should override the default loading behavior for non-priority images', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('loading')).toBe('eager'); }); - it('should throw if used with priority images', () => { + it('should throw if used with priority images', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an element ' + 'with the `ngSrc="path/img.png"`) has detected that the `loading` attribute ' + 'was used on an image that was marked "priority". Setting `loading` on priority ' + @@ -926,26 +887,24 @@ describe('Image directive', () => { ); }); - it('should support setting loading priority to "auto"', () => { + it('should support setting loading priority to "auto"', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('loading')).toBe('auto'); }); - it('should throw for invalid loading inputs', () => { + it('should throw for invalid loading inputs', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an element ' + 'with the `ngSrc="path/img.png"`) has detected that the `loading` attribute ' + 'has an invalid value (`fast`). To fix this, provide a valid value ("lazy", ' + @@ -955,24 +914,24 @@ describe('Image directive', () => { }); describe('fetch priority', () => { - it('should be "high" for priority images', () => { + it('should be "high" for priority images', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('fetchpriority')).toBe('high'); }); - it('should be "auto" for non-priority images', () => { + it('should be "auto" for non-priority images', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -1013,8 +972,6 @@ describe('Image directive', () => { const consoleWarnSpy = spyOn(console, 'warn'); const fixture = createTestComponent(template); - fixture.detectChanges(); - await fixture.whenStable(); // trick to wait for the whenStable() to fire in the directive @@ -1036,22 +993,22 @@ describe('Image directive', () => { }); describe('meta data', () => { - it('should add a data attribute to the element for identification', () => { + it('should add a data attribute to the element for identification', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('ng-img')).not.toBeNull(); }); - it('should add a data attribute to the element for identification, when ngSrc bound', () => { + it('should add a data attribute to the element for identification, when ngSrc bound', async () => { setupTestingModule(); const template = ``; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('ng-img')).not.toBeNull(); @@ -1059,134 +1016,128 @@ describe('Image directive', () => { }); describe('fill mode', () => { - it('should allow unsized images in fill mode', () => { + it('should allow unsized images in fill mode', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).not.toThrow(); + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeResolved(); }); - it('should throw if width is provided for fill mode image', () => { + it('should throw if width is provided for fill mode image', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an element with the ' + '`ngSrc="path/img.png"`) has detected that the attributes `height` and/or `width` ' + 'are present along with the `fill` attribute. Because `fill` mode causes an image ' + 'to fill its containing element, the size attributes have no effect and should be removed.', ); }); - it('should throw if height is provided for fill mode image', () => { + it('should throw if height is provided for fill mode image', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive (activated on an element with the ' + '`ngSrc="path/img.png"`) has detected that the attributes `height` and/or `width` ' + 'are present along with the `fill` attribute. Because `fill` mode causes an image ' + 'to fill its containing element, the size attributes have no effect and should be removed.', ); }); - it('should apply appropriate styles in fill mode', () => { + it('should apply appropriate styles in fill mode', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('style')?.replace(/\s/g, '')).toMatch( 'position:absolute;width:100%;height:100%;inset:0', ); }); - it('should augment existing styles in fill mode', () => { + it('should augment existing styles in fill mode', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('style')?.replace(/\s/g, '')).toMatch( 'border-radius:5px;padding:10px;position:absolute;width:100%;height:100%;inset:0', ); }); - it('should not add fill styles if not in fill mode', () => { + it('should not add fill styles if not in fill mode', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('style')?.replace(/\s/g, '')).toBe( 'position:relative;border-radius:5px;', ); }); - it('should add default sizes value in fill mode', () => { + it('should add default sizes value in fill mode', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('sizes')).toBe('100vw'); }); - it('should add auto sizes to default in fill mode when lazy', () => { + it('should add auto sizes to default in fill mode when lazy', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('sizes')).toBe('auto, 100vw'); }); - it('should not overwrite sizes value in fill mode', () => { + it('should not overwrite sizes value in fill mode', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('sizes')).toBe('50vw'); }); - it('should prepend "auto" to sizes in fill mode when lazy', () => { + it('should prepend "auto" to sizes in fill mode when lazy', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('sizes')).toBe('auto, 50vw'); }); - it('should cause responsive srcset to be generated in fill mode', () => { + it('should cause responsive srcset to be generated in fill mode', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('srcset')).toBe( @@ -1207,13 +1158,13 @@ describe('Image directive', () => { return `${IMG_BASE_URL}/${config.src}?${queryString}`; }; - it('should add background size, position, and repeat styling if the placeholder attribute is present', () => { + it('should add background size, position, and repeat styling if the placeholder attribute is present', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; const styles = parseInlineStyles(img); @@ -1222,12 +1173,12 @@ describe('Image directive', () => { expect(styles.get('background-repeat')).toBe('no-repeat'); }); - it('should not add background styles if the placeholder attribute is not present', () => { + it('should not add background styles if the placeholder attribute is not present', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; const styles = parseInlineStyles(img); @@ -1237,12 +1188,12 @@ describe('Image directive', () => { expect(styles.get('background-image')).toBeUndefined(); }); - it('should add a background-image tag when placeholder is provided as a boolean', () => { + it('should add a background-image tag when placeholder is provided as a boolean', async () => { setupTestingModule({imageLoader}); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; const styles = parseInlineStyles(img); @@ -1252,7 +1203,7 @@ describe('Image directive', () => { ); }); - it('should escape loader output when placeholder is provided as a boolean (security regression)', () => { + it('should escape loader output when placeholder is provided as a boolean (security regression)', async () => { const maliciousLoader = (config: ImageLoaderConfig) => { if (config.isPlaceholder) { return 'https://mysite.com/img.png"); color: red;/*'; @@ -1263,7 +1214,7 @@ describe('Image directive', () => { const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const imageDirective = fixture.debugElement.children[0].injector.get(NgOptimizedImage); const placeholderValue = (imageDirective as any).generatePlaceholder(true) as string; @@ -1273,13 +1224,13 @@ describe('Image directive', () => { if (!isNode) { // DataURLs get stripped from background-image attribute in Node, but not browsers. - it('should add a background-image tag when placeholder is provided as a data URL', () => { + it('should add a background-image tag when placeholder is provided as a data URL', async () => { setupTestingModule({imageLoader}); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; // Double quotes removed to account for different browser behavior. @@ -1289,13 +1240,13 @@ describe('Image directive', () => { }); } - it('should add a background-image tag when placeholder is provided as a URL', () => { + it('should add a background-image tag when placeholder is provided as a URL', async () => { setupTestingModule({imageLoader}); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; // Double quotes removed to account for different browser behavior. @@ -1305,13 +1256,13 @@ describe('Image directive', () => { }); // DataURLs get stripped from background-image attribute in Node, but not browsers. - it('should add a background-image tag when placeholder is provided as relative URL', () => { + it('should add a background-image tag when placeholder is provided as relative URL', async () => { setupTestingModule({imageLoader}); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; // Double quotes removed to account for different browser behavior. @@ -1320,13 +1271,13 @@ describe('Image directive', () => { ); }); - it('should prevent CSS injection through placeholder URL values', () => { + it('should prevent CSS injection through placeholder URL values', async () => { setupTestingModule({imageLoader}); const maliciousPlaceholder = 'https://mysite.com/img.png"); color: red;'; const template = ``; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const imageDirective = fixture.debugElement.children[0].injector.get(NgOptimizedImage); const placeholderValue = (imageDirective as any).generatePlaceholder( @@ -1336,12 +1287,12 @@ describe('Image directive', () => { expect(placeholderValue).toBe('url("https://mysite.com/img.png\\"); color: red;")'); }); - it('should add a background-image tag when placeholder is provided without value', () => { + it('should add a background-image tag when placeholder is provided without value', async () => { setupTestingModule({imageLoader}); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; const styles = parseInlineStyles(img); @@ -1351,14 +1302,14 @@ describe('Image directive', () => { ); }); - it('should remove placeholder event listeners once view is removed', () => { + it('should remove placeholder event listeners once view is removed', async () => { const addEventListenerSpy = spyOn(HTMLImageElement.prototype, 'addEventListener'); const removeEventListenerSpy = spyOn(HTMLImageElement.prototype, 'removeEventListener'); setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); // The `load` event listener is being set up twice: once in // `assertNoImageDistortion` and once in `removePlaceholderOnLoad`. expect( @@ -1372,20 +1323,20 @@ describe('Image directive', () => { ).toEqual(2); }); - it('should replace the placeholder with the actual image on load', () => { + it('should replace the placeholder with the actual image on load', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template, ChangeDetectionStrategy.OnPush); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(parseInlineStyles(img).has('background-image')).toBe(true); img.dispatchEvent(new Event('load')); - fixture.detectChanges(); + await fixture.whenStable(); expect(parseInlineStyles(img).has('background-image')).toBe(false); }); - it('should use the placeholderResolution set in imageConfig', () => { + it('should use the placeholderResolution set in imageConfig', async () => { const imageConfig = { placeholderResolution: 30, }; @@ -1393,7 +1344,7 @@ describe('Image directive', () => { const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; const styles = parseInlineStyles(img); @@ -1403,7 +1354,7 @@ describe('Image directive', () => { ); }); - it('should pass calculated height to placeholder loader based on aspect ratio', () => { + it('should pass calculated height to placeholder loader based on aspect ratio', async () => { const placeholderLoaderWithHeight = (config: ImageLoaderConfig) => { const widthStr = config.width ? `w=${config.width}` : ''; const heightStr = config.height ? `h=${config.height}` : ''; @@ -1418,7 +1369,7 @@ describe('Image directive', () => { const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; const styles = parseInlineStyles(img); @@ -1430,90 +1381,82 @@ describe('Image directive', () => { ); }); - it('should apply a background blur to images with a placeholder', () => { + it('should apply a background blur to images with a placeholder', async () => { setupTestingModule({imageLoader}); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; const styles = parseInlineStyles(img); expect(styles.get('filter')).toBe(`blur(${PLACEHOLDER_BLUR_AMOUNT}px)`); }); - it('should not apply a background blur to placeholders with blur=false', () => { + it('should not apply a background blur to placeholders with blur=false', async () => { setupTestingModule({imageLoader}); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; const styles = parseInlineStyles(img); expect(styles.get('filter')).toBeUndefined(); }); - it('should throw if placeholderConfig is provided without placeholder', () => { + it('should throw if placeholderConfig is provided without placeholder', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( `NG02952: The NgOptimizedImage directive has detected that \`placeholderConfig\` options were ` + `provided for an image that does not use the \`placeholder\` attribute, and will have no effect.`, ); }); - it('should throw if placeholderConfig with blur=true is provided without placeholder', () => { + it('should throw if placeholderConfig with blur=true is provided without placeholder', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( `NG02952: The NgOptimizedImage directive has detected that \`placeholderConfig\` options were ` + `provided for an image that does not use the \`placeholder\` attribute, and will have no effect.`, ); }); - it('should throw if there is no image loader but `ngSrcset` is present', () => { + it('should throw if there is no image loader but `ngSrcset` is present', async () => { setUpModuleNoLoader(); const template = ``; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( `NG0${RuntimeErrorCode.MISSING_NECESSARY_LOADER}: The NgOptimizedImage directive (activated on an element with the \`ngSrc="img.png"\`) ` + `has detected that the \`placeholder\` attribute is set to true but no image loader is configured (i.e. the default one is being used), which ` + `would result in the same image being used for the primary image and its placeholder. To fix this, provide a loader or remove the \`placeholder\` attribute from the image.`, ); }); - it('should throw if a very large inline placeholder is supplied', () => { + it('should throw if a very large inline placeholder is supplied', async () => { setupTestingModule(); const template = ``; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( `NG0${RuntimeErrorCode.OVERSIZED_PLACEHOLDER}: The NgOptimizedImage directive (activated on an element with the \`ngSrc="path/img.png"\`) ` + `has detected that the \`placeholder\` attribute is set to a data URL which is longer than ${DATA_URL_ERROR_LIMIT} characters. This is strongly ` + `discouraged, as large inline placeholders directly increase the bundle size of Angular and hurt page load performance. To fix this, generate a smaller data URL placeholder.`, ); }); - it('should warn if a large inline placeholder is supplied', () => { + it('should warn if a large inline placeholder is supplied', async () => { setUpModuleNoLoader(); const template = ``; const fixture = createTestComponent(template); const consoleWarnSpy = spyOn(console, 'warn'); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toBe( @@ -1542,7 +1485,7 @@ describe('Image directive', () => { globalThis['ngServerMode'] = undefined; }); - it('should throw if the placeholder height exceeds the threshold', () => { + it('should throw if the placeholder height exceeds the threshold', async () => { setUpModuleNoLoader(); const template = `'; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toBe( @@ -1612,13 +1555,13 @@ describe('Image directive', () => { it( 'should not log a warning if there is no preconnect link, but the image is not set as a priority', - withHead('', () => { + withHead('', async () => { setupTestingModule({imageLoader}); const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); // Expect no warnings in the console. expect(consoleWarnSpy.calls.count()).toBe(0); @@ -1627,7 +1570,7 @@ describe('Image directive', () => { it( "should log a warning if there is a preconnect, but it doesn't match the priority image", - withHead('', () => { + withHead('', async () => { // The warning is only logged on the client if (!isBrowser) return; @@ -1636,7 +1579,7 @@ describe('Image directive', () => { const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toBe( @@ -1654,7 +1597,7 @@ describe('Image directive', () => { 'should log a warning if there is no matching preconnect link for a priority image, but there is a preload tag', withHead( '', - () => { + async () => { // The warning is only logged on the client if (!isBrowser) return; @@ -1663,7 +1606,7 @@ describe('Image directive', () => { const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toBe( @@ -1680,13 +1623,13 @@ describe('Image directive', () => { it( 'should not log a warning if there is a matching preconnect link for a priority image (with an extra `/` at the end)', - withHead('', () => { + withHead('', async () => { setupTestingModule({imageLoader}); const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); // Expect no warnings in the console. expect(consoleWarnSpy.calls.count()).toBe(0); @@ -1697,7 +1640,7 @@ describe('Image directive', () => { it( `should not log a warning if an origin domain is blocklisted ` + `(checking ${blocklistedHostname})`, - withHead('', () => { + withHead('', async () => { const imageLoader = () => { return `http://${blocklistedHostname}/a.png`; }; @@ -1706,7 +1649,7 @@ describe('Image directive', () => { const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); // Expect no warnings in the console. expect(consoleWarnSpy.calls.count()).toBe(0); @@ -1717,14 +1660,14 @@ describe('Image directive', () => { describe('PRECONNECT_CHECK_BLOCKLIST token', () => { it( `should allow passing host names`, - withHead('', () => { + withHead('', async () => { const providers = [{provide: PRECONNECT_CHECK_BLOCKLIST, useValue: 'angular.dev'}]; setupTestingModule({imageLoader, extraProviders: providers}); const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); // Expect no warnings in the console. expect(consoleWarnSpy.calls.count()).toBe(0); @@ -1733,7 +1676,7 @@ describe('Image directive', () => { it( `should allow passing origins`, - withHead('', () => { + withHead('', async () => { const providers = [ {provide: PRECONNECT_CHECK_BLOCKLIST, useValue: 'https://angular.dev'}, ]; @@ -1742,7 +1685,7 @@ describe('Image directive', () => { const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); // Expect no warnings in the console. expect(consoleWarnSpy.calls.count()).toBe(0); @@ -1751,7 +1694,7 @@ describe('Image directive', () => { it( `should allow passing arrays of host names`, - withHead('', () => { + withHead('', async () => { const providers = [ {provide: PRECONNECT_CHECK_BLOCKLIST, useValue: ['https://angular.dev']}, ]; @@ -1760,7 +1703,7 @@ describe('Image directive', () => { const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); // Expect no warnings in the console. expect(consoleWarnSpy.calls.count()).toBe(0); @@ -1769,7 +1712,7 @@ describe('Image directive', () => { it( `should allow passing nested arrays of host names`, - withHead('', () => { + withHead('', async () => { const providers = [ {provide: PRECONNECT_CHECK_BLOCKLIST, useValue: [['https://angular.dev']]}, ]; @@ -1778,7 +1721,7 @@ describe('Image directive', () => { const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); // Expect no warnings in the console. expect(consoleWarnSpy.calls.count()).toBe(0); @@ -1808,25 +1751,25 @@ describe('Image directive', () => { return `${config.src}/${config.loaderParams?.['transforms1'].example1}/${config.loaderParams?.['transforms2'].example2}`; }; - it('should set `src` to match `ngSrc` if image loader is not provided', () => { + it('should set `src` to match `ngSrc` if image loader is not provided', async () => { setupTestingModule(); const template = ``; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.src).toBe(`${IMG_BASE_URL}/img.png`); }); - it('should warn if there is no image loader but using Imgix URL', () => { + it('should warn if there is no image loader but using Imgix URL', async () => { setUpModuleNoLoader(); const template = ``; const fixture = createTestComponent(template); const consoleWarnSpy = spyOn(console, 'warn'); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch( @@ -1834,13 +1777,13 @@ describe('Image directive', () => { ); }); - it('should warn if there is no image loader but using ImageKit URL', () => { + it('should warn if there is no image loader but using ImageKit URL', async () => { setUpModuleNoLoader(); const template = ``; const fixture = createTestComponent(template); const consoleWarnSpy = spyOn(console, 'warn'); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch( @@ -1848,13 +1791,13 @@ describe('Image directive', () => { ); }); - it('should warn if there is no image loader but using Cloudinary URL', () => { + it('should warn if there is no image loader but using Cloudinary URL', async () => { setUpModuleNoLoader(); const template = ``; const fixture = createTestComponent(template); const consoleWarnSpy = spyOn(console, 'warn'); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch( @@ -1862,13 +1805,13 @@ describe('Image directive', () => { ); }); - it('should warn if there is no image loader but using Netlify URL', () => { + it('should warn if there is no image loader but using Netlify URL', async () => { setUpModuleNoLoader(); const template = ``; const fixture = createTestComponent(template); const consoleWarnSpy = spyOn(console, 'warn'); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toMatch( @@ -1876,24 +1819,24 @@ describe('Image directive', () => { ); }); - it('should NOT warn if there is a custom loader but using CDN URL', () => { + it('should NOT warn if there is a custom loader but using CDN URL', async () => { setupTestingModule(); const template = ``; const fixture = createTestComponent(template); const consoleWarnSpy = spyOn(console, 'warn'); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(0); }); - it('should warn if there is no image loader but `ngSrcset` is present', () => { + it('should warn if there is no image loader but `ngSrcset` is present', async () => { setUpModuleNoLoader(); const template = ``; const fixture = createTestComponent(template); const consoleWarnSpy = spyOn(console, 'warn'); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toBe( @@ -1905,13 +1848,13 @@ describe('Image directive', () => { ); }); - it('should warn if there is no image loader but `loaderParams` is present', () => { + it('should warn if there is no image loader but `loaderParams` is present', async () => { setUpModuleNoLoader(); const template = ``; const fixture = createTestComponent(template); const consoleWarnSpy = spyOn(console, 'warn'); - fixture.detectChanges(); + await fixture.whenStable(); expect(consoleWarnSpy.calls.count()).toBe(1); expect(consoleWarnSpy.calls.argsFor(0)[0]).toBe( @@ -1923,7 +1866,7 @@ describe('Image directive', () => { ); }); - it('should set `src` using the image loader provided via the `IMAGE_LOADER` token to compose src URL', () => { + it('should set `src` using the image loader provided via the `IMAGE_LOADER` token to compose src URL', async () => { const imageLoader = (config: ImageLoaderConfig) => `${IMG_BASE_URL}/${config.src}`; setupTestingModule({imageLoader}); @@ -1932,7 +1875,7 @@ describe('Image directive', () => { `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const imgs = nativeElement.querySelectorAll('img')!; @@ -1940,11 +1883,11 @@ describe('Image directive', () => { expect(imgs[1].src.trim()).toBe(`${IMG_BASE_URL}/img-2.png`); }); - it('should use the image loader to update `src` if `ngSrc` updated', () => { + it('should use the image loader to update `src` if `ngSrc` updated', async () => { @Component({ selector: 'test-cmp', template: ``, - standalone: false, + imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent { @@ -1953,7 +1896,7 @@ describe('Image directive', () => { const imageLoader = (config: ImageLoaderConfig) => `${IMG_BASE_URL}/${config.src}`; setupTestingModule({imageLoader, component: TestComponent}); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); let nativeElement = fixture.nativeElement as HTMLElement; let imgs = nativeElement.querySelectorAll('img')!; @@ -1961,15 +1904,15 @@ describe('Image directive', () => { fixture.componentInstance.ngSrc = 'updatedImg.png'; fixture.changeDetectorRef.markForCheck(); - fixture.detectChanges(); + await fixture.whenStable(); expect(imgs[0].src).toBe(`${IMG_BASE_URL}/updatedImg.png`); }); - it('should use the image loader to update `srcset` if `ngSrc` updated', () => { + it('should use the image loader to update `srcset` if `ngSrc` updated', async () => { @Component({ selector: 'test-cmp', template: ``, - standalone: false, + imports: [NgOptimizedImage], changeDetection: ChangeDetectionStrategy.Eager, }) class TestComponent { @@ -1981,7 +1924,7 @@ describe('Image directive', () => { }; setupTestingModule({imageLoader, component: TestComponent}); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); let nativeElement = fixture.nativeElement as HTMLElement; let imgs = nativeElement.querySelectorAll('img')!; @@ -1993,13 +1936,13 @@ describe('Image directive', () => { fixture.changeDetectorRef.markForCheck(); nativeElement = fixture.nativeElement as HTMLElement; imgs = nativeElement.querySelectorAll('img')!; - fixture.detectChanges(); + await fixture.whenStable(); expect(imgs[0].getAttribute('srcset')).toBe( `${IMG_BASE_URL}/updatedImg.png?w=640 640w, ${IMG_BASE_URL}/updatedImg.png?w=750 750w, ${IMG_BASE_URL}/updatedImg.png?w=828 828w, ${IMG_BASE_URL}/updatedImg.png?w=1080 1080w, ${IMG_BASE_URL}/updatedImg.png?w=1200 1200w, ${IMG_BASE_URL}/updatedImg.png?w=1920 1920w, ${IMG_BASE_URL}/updatedImg.png?w=2048 2048w, ${IMG_BASE_URL}/updatedImg.png?w=3840 3840w`, ); }); - it('should pass absolute URLs defined in the `ngSrc` to custom image loaders provided via the `IMAGE_LOADER` token', () => { + it('should pass absolute URLs defined in the `ngSrc` to custom image loaders provided via the `IMAGE_LOADER` token', async () => { const imageLoader = (config: ImageLoaderConfig) => `${config.src}?rewritten=true`; setupTestingModule({imageLoader}); @@ -2007,27 +1950,27 @@ describe('Image directive', () => { `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const imgs = nativeElement.querySelectorAll('img')!; expect(imgs[0].src.trim()).toBe(`${IMG_BASE_URL}/img.png?rewritten=true`); }); - it('should pass data payload from loaderParams to custom image loaders', () => { + it('should pass data payload from loaderParams to custom image loaders', async () => { setupTestingModule({imageLoader: imageLoaderWithData}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const imgs = nativeElement.querySelectorAll('img')!; expect(imgs[0].src).toBe(`${IMG_BASE_URL}/img.png?testProp1=testValue1&testProp2=testValue2`); }); - it('should pass nested data payloads from loaderParams to custom image loaders', () => { + it('should pass nested data payloads from loaderParams to custom image loaders', async () => { @Component({ selector: 'test-cmp', template: ` { [height]="height" [loaderParams]="params" />`, - standalone: false, + imports: [NgOptimizedImage], }) class TestComponent { ngSrc = `${IMG_BASE_URL}/img.png`; @@ -2046,20 +1989,20 @@ describe('Image directive', () => { } setupTestingModule({imageLoader: nestedImageLoader, component: TestComponent}); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const imgs = nativeElement.querySelectorAll('img')!; expect(imgs[0].src).toBe(`${IMG_BASE_URL}/img.png/foo/bar`); }); - it('should pass data payload from loaderParams to loader when generating srcsets', () => { + it('should pass data payload from loaderParams to loader when generating srcsets', async () => { setupTestingModule({imageLoader: imageLoaderWithData}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const imgs = nativeElement.querySelectorAll('img')!; expect(imgs[0].srcset).toBe( @@ -2067,14 +2010,14 @@ describe('Image directive', () => { ); }); - it('should pass data payload from loaderParams to loader when generating responsive srcsets', () => { + it('should pass data payload from loaderParams to loader when generating responsive srcsets', async () => { setupTestingModule({imageLoader: imageLoaderWithData}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const imgs = nativeElement.querySelectorAll('img')!; expect(imgs[0].srcset).toBe( @@ -2082,7 +2025,7 @@ describe('Image directive', () => { ); }); - it('should pass height to custom image loader based on aspect ratio', () => { + it('should pass height to custom image loader based on aspect ratio', async () => { const imageLoader = (config: ImageLoaderConfig) => { const widthStr = config.width ? `w=${config.width}` : ''; const heightStr = config.height ? `h=${config.height}` : ''; @@ -2093,7 +2036,7 @@ describe('Image directive', () => { const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2101,7 +2044,7 @@ describe('Image directive', () => { expect(img.src).toBe(`${IMG_BASE_URL}/img.png`); }); - it('should pass calculated height to custom image loader when generating srcsets', () => { + it('should pass calculated height to custom image loader when generating srcsets', async () => { const imageLoader = (config: ImageLoaderConfig) => { const widthStr = config.width ? `w=${config.width}` : ''; const heightStr = config.height ? `h=${config.height}` : ''; @@ -2112,7 +2055,7 @@ describe('Image directive', () => { const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2123,7 +2066,7 @@ describe('Image directive', () => { ); }); - it('should pass calculated height to custom image loader when generating responsive srcsets', () => { + it('should pass calculated height to custom image loader when generating responsive srcsets', async () => { const imageLoader = (config: ImageLoaderConfig) => { const widthStr = config.width ? `w=${config.width}` : ''; const heightStr = config.height ? `h=${config.height}` : ''; @@ -2134,7 +2077,7 @@ describe('Image directive', () => { const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2145,7 +2088,7 @@ describe('Image directive', () => { ); }); - it('should not pass height to custom image loader when height is not provided', () => { + it('should not pass height to custom image loader when height is not provided', async () => { const imageLoader = (config: ImageLoaderConfig) => { const widthStr = config.width ? `w=${config.width}` : ''; const heightStr = config.height ? `h=${config.height}` : ''; @@ -2156,7 +2099,7 @@ describe('Image directive', () => { const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2167,10 +2110,10 @@ describe('Image directive', () => { ); }); - it('should pass height to custom image loaders', () => { + it('should pass height to custom image loaders', async () => { @Component({ selector: 'test-cmp', - standalone: false, + imports: [NgOptimizedImage], template: ``, }) class TestComponent { @@ -2189,7 +2132,7 @@ describe('Image directive', () => { }; setupTestingModule({imageLoader, component: TestComponent}); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); let nativeElement = fixture.nativeElement as HTMLElement; let imgs = nativeElement.querySelectorAll('img')!; @@ -2198,7 +2141,7 @@ describe('Image directive', () => { ); }); - it('should set `src` to an image URL that does not include a default width parameter', () => { + it('should set `src` to an image URL that does not include a default width parameter', async () => { const imageLoader = (config: ImageLoaderConfig) => { const widthStr = config.width ? `?w=${config.width}` : ``; return `${IMG_BASE_URL}/${config.src}${widthStr}`; @@ -2207,7 +2150,7 @@ describe('Image directive', () => { const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2216,7 +2159,7 @@ describe('Image directive', () => { it( `should allow providing image loaders via Component providers`, - withHead('', () => { + withHead('', async () => { const createImgUrl = (path: string, config: ImageLoaderConfig) => `${path}/${config.src}`; const loaderWithPath = createImageLoader(createImgUrl); @@ -2224,7 +2167,7 @@ describe('Image directive', () => { selector: 'test-cmp', template: '', providers: [loaderWithPath('https://component.io')], - standalone: false, + imports: [NgOptimizedImage], }) class TestComponent {} @@ -2234,7 +2177,7 @@ describe('Image directive', () => { }); const fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + await fixture.whenStable(); const defaultLoader = TestBed.inject(IMAGE_LOADER); const nativeElement = fixture.nativeElement as HTMLElement; @@ -2255,14 +2198,14 @@ describe('Image directive', () => { }; }); - it('should set the `srcset` using the `ngSrcset` value with width descriptors', () => { + it('should set the `srcset` using the `ngSrcset` value with width descriptors', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2272,14 +2215,14 @@ describe('Image directive', () => { ); }); - it('should set the `srcset` using the `ngSrcset` value with density descriptors', () => { + it('should set the `srcset` using the `ngSrcset` value with density descriptors', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2289,14 +2232,14 @@ describe('Image directive', () => { ); }); - it('should set the `srcset` if `ngSrcset` has only one src defined', () => { + it('should set the `srcset` if `ngSrcset` has only one src defined', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2304,14 +2247,14 @@ describe('Image directive', () => { expect(img.srcset.trim()).toBe(`${IMG_BASE_URL}/img.png?w=100 100w`); }); - it('should set the `srcset` if `ngSrcSet` has extra spaces', () => { + it('should set the `srcset` if `ngSrcSet` has extra spaces', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2321,14 +2264,14 @@ describe('Image directive', () => { ); }); - it('should set the `srcset` if `ngSrcSet` has a trailing comma', () => { + it('should set the `srcset` if `ngSrcSet` has a trailing comma', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2338,14 +2281,14 @@ describe('Image directive', () => { ); }); - it('should set the `srcset` if `ngSrcSet` has 3+ srcs', () => { + it('should set the `srcset` if `ngSrcSet` has 3+ srcs', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2357,14 +2300,14 @@ describe('Image directive', () => { ); }); - it('should set the `srcset` if `ngSrcSet` has decimal density descriptors', () => { + it('should set the `srcset` if `ngSrcSet` has decimal density descriptors', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2375,28 +2318,28 @@ describe('Image directive', () => { `${IMG_BASE_URL}/img.png?w=300 3x`, ); }); - it('should automatically set a default sizes attribute when ngSrcset is used with a responsive srcset and is lazy', () => { + it('should automatically set a default sizes attribute when ngSrcset is used with a responsive srcset and is lazy', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.src).toBe(`${IMG_BASE_URL}/img.png`); expect(img.sizes).toBe(`auto, 100vw`); }); - it('should not automatically set a default sizes attribute when ngSrcset is used with a responsive srcset and is not lazy', () => { + it('should not automatically set a default sizes attribute when ngSrcset is used with a responsive srcset and is not lazy', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2406,14 +2349,14 @@ describe('Image directive', () => { }); describe('sizes attribute', () => { - it('should pass through the sizes attribute', () => { + it('should pass through the sizes attribute', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2423,14 +2366,14 @@ describe('Image directive', () => { ); }); - it('should prepend sizes="auto" to a lazy-loaded image', () => { + it('should prepend sizes="auto" to a lazy-loaded image', async () => { setupTestingModule(); const template = ''; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2440,43 +2383,37 @@ describe('Image directive', () => { ); }); - it('should throw if a complex `sizes` is used', () => { + it('should throw if a complex `sizes` is used', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive has detected that `sizes` was set to a string including pixel values. ' + 'For automatic `srcset` generation, `sizes` must only include responsive values, such as `sizes="50vw"` or ' + '`sizes="(min-width: 768px) 50vw, 100vw"`. To fix this, modify the `sizes` attribute, or provide your own `ngSrcset` value directly.', ); }); - it('should throw if a complex `sizes` is used with srcset', () => { + it('should throw if a complex `sizes` is used with srcset', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).toThrowError( + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeRejectedWithError( 'NG02952: The NgOptimizedImage directive has detected that `sizes` was set to a string including pixel values. ' + 'For automatic `srcset` generation, `sizes` must only include responsive values, such as `sizes="50vw"` or ' + '`sizes="(min-width: 768px) 50vw, 100vw"`. To fix this, modify the `sizes` attribute, or provide your own `ngSrcset` value directly.', ); }); - it('should not throw if a complex `sizes` is used with ngSrcset', () => { + it('should not throw if a complex `sizes` is used with ngSrcset', async () => { setupTestingModule(); const template = ''; - expect(() => { - const fixture = createTestComponent(template); - fixture.detectChanges(); - }).not.toThrow(); + const fixture = createTestComponent(template); + await expectAsync(fixture.whenStable()).toBeResolved(); }); }); @@ -2486,28 +2423,28 @@ describe('Image directive', () => { return `${IMG_BASE_URL}/${config.src}${width}`; }; - it('should not generate a srcset if the default noop loader is used', () => { + it('should not generate a srcset if the default noop loader is used', async () => { setupTestingModule({noLoader: true}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('srcset')).toBeNull(); }); - it('should add a responsive srcset to the img element if sizes attribute exists', () => { + it('should add a responsive srcset to the img element if sizes attribute exists', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2516,14 +2453,14 @@ describe('Image directive', () => { ); }); - it('should use the long responsive srcset if sizes attribute exists and is less than 100vw', () => { + it('should use the long responsive srcset if sizes attribute exists and is less than 100vw', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2532,14 +2469,14 @@ describe('Image directive', () => { ); }); - it('should add a fixed srcset to the img element if sizes attribute does not exist', () => { + it('should add a fixed srcset to the img element if sizes attribute does not exist', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2548,36 +2485,36 @@ describe('Image directive', () => { ); }); - it('should not add a fixed srcset to the img element if height is too large', () => { + it('should not add a fixed srcset to the img element if height is too large', async () => { setupTestingModule({imageLoader}); const template = ``; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('srcset')).toBeNull(); }); - it('should not add a fixed srcset to the img element if width is too large', () => { + it('should not add a fixed srcset to the img element if width is too large', async () => { setupTestingModule({imageLoader}); const template = ``; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('srcset')).toBeNull(); }); - it('should add a responsive srcset to the img element if height is too large', () => { + it('should add a responsive srcset to the img element if height is too large', async () => { setupTestingModule({imageLoader}); const template = ``; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2586,12 +2523,12 @@ describe('Image directive', () => { ); }); - it('should add a responsive srcset to the img element if width is too large', () => { + it('should add a responsive srcset to the img element if width is too large', async () => { setupTestingModule({imageLoader}); const template = ``; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; @@ -2600,7 +2537,7 @@ describe('Image directive', () => { ); }); - it('should use a custom breakpoint set if one is provided', () => { + it('should use a custom breakpoint set if one is provided', async () => { const imageConfig = { breakpoints: [16, 32, 48, 64, 96, 128, 256, 384, 640, 1280, 3840], }; @@ -2610,7 +2547,7 @@ describe('Image directive', () => { `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('srcset')).toBe( @@ -2618,7 +2555,7 @@ describe('Image directive', () => { ); }); - it('should sort custom breakpoint set', () => { + it('should sort custom breakpoint set', async () => { const imageConfig = { breakpoints: [48, 16, 3840, 640, 1280], }; @@ -2628,7 +2565,7 @@ describe('Image directive', () => { `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('srcset')).toBe( @@ -2636,14 +2573,14 @@ describe('Image directive', () => { ); }); - it('should disable automatic srcset generation if "disableOptimizedSrcset" attribute is set', () => { + it('should disable automatic srcset generation if "disableOptimizedSrcset" attribute is set', async () => { setupTestingModule({imageLoader}); const template = ` `; const fixture = createTestComponent(template); - fixture.detectChanges(); + await fixture.whenStable(); const nativeElement = fixture.nativeElement as HTMLElement; const img = nativeElement.querySelector('img')!; expect(img.getAttribute('srcset')).toBeNull(); @@ -2691,7 +2628,7 @@ const ANGULAR_LOGO_BASE64 = @Component({ selector: 'test-cmp', template: '', - standalone: false, + imports: [NgOptimizedImage], }) class TestComponent { width = 100; @@ -2723,10 +2660,7 @@ function setupTestingModule(config?: { } TestBed.configureTestingModule({ - declarations: [config?.component ?? TestComponent], - // Note: the `NgOptimizedImage` directive is experimental and is not a part of the - // `CommonModule` yet, so it's imported separately. - imports: [CommonModule, NgOptimizedImage], + imports: [config?.component ?? TestComponent], providers, }); } @@ -2735,8 +2669,7 @@ function setupTestingModule(config?: { // so the noopImageLoader should be used. function setUpModuleNoLoader() { TestBed.configureTestingModule({ - declarations: [TestComponent], - imports: [CommonModule, NgOptimizedImage], + imports: [TestComponent], providers: [{provide: DOCUMENT, useValue: window.document}], }); } diff --git a/packages/common/test/directives/ng_style_spec.ts b/packages/common/test/directives/ng_style_spec.ts index 20422e3c6d8..1b556438e76 100644 --- a/packages/common/test/directives/ng_style_spec.ts +++ b/packages/common/test/directives/ng_style_spec.ts @@ -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; @@ -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 = `
`; 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 = `
`; 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 = `
`; 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 = `
`; 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 = `
`; 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(`
`); 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 = `
`; 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 = `
`; 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 = `
`; 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 = `
`; 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 = `
`; 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 = `
`; 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 = `
`; 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 = `
`; 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 {