From 586274fe65c5184b633e0e5ac12ca91979f138b2 Mon Sep 17 00:00:00 2001 From: Andrew Kushnir Date: Mon, 6 Jun 2022 17:19:47 -0700 Subject: [PATCH] feat(common): provide an ability to exclude origins from preconnect checks in NgOptimizedImage (#47082) This commit adds an extra logic to add an ability to exclude origins from preconnect checks in NgOptimizedImage by configuring the `PRECONNECT_CHECK_BLOCKLIST` multi-provider. PR Close #47082 --- goldens/public-api/common/errors.md | 4 + .../directives/ng_optimized_image/asserts.ts | 25 +++ .../directives/ng_optimized_image/index.ts | 1 + .../ng_optimized_image/ng_optimized_image.ts | 2 + .../preconnect_link_checker.ts | 52 +++++- .../src/directives/ng_optimized_image/util.ts | 24 ++- packages/common/src/errors.ts | 2 + packages/common/src/private_export.ts | 2 +- .../directives/ng_optimized_image_spec.ts | 163 +++++++++++++++--- .../e2e/basic/basic.e2e-spec.ts | 2 +- .../image-directive/e2e/basic/basic.ts | 5 +- .../preconnect-check.e2e-spec.ts | 2 +- .../e2e/preconnect-check/preconnect-check.ts | 8 +- .../test/bundling/image-directive/e2e/util.ts | 10 +- 14 files changed, 260 insertions(+), 42 deletions(-) create mode 100644 packages/common/src/directives/ng_optimized_image/asserts.ts diff --git a/goldens/public-api/common/errors.md b/goldens/public-api/common/errors.md index 695bfeb54f47c..1cac2f0e8916e 100644 --- a/goldens/public-api/common/errors.md +++ b/goldens/public-api/common/errors.md @@ -11,6 +11,8 @@ export const enum RuntimeErrorCode { // (undocumented) INVALID_PIPE_ARGUMENT = 2100, // (undocumented) + INVALID_PRECONNECT_CHECK_BLOCKLIST = 2957, + // (undocumented) LCP_IMG_MISSING_PRIORITY = 2955, // (undocumented) NG_FOR_MISSING_DIFFER = -2200, @@ -21,6 +23,8 @@ export const enum RuntimeErrorCode { // (undocumented) REQUIRED_INPUT_MISSING = 2954, // (undocumented) + UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958, + // (undocumented) UNEXPECTED_INPUT_CHANGE = 2953, // (undocumented) UNEXPECTED_SRC_ATTR = 2950, diff --git a/packages/common/src/directives/ng_optimized_image/asserts.ts b/packages/common/src/directives/ng_optimized_image/asserts.ts new file mode 100644 index 0000000000000..dfef38b052a0a --- /dev/null +++ b/packages/common/src/directives/ng_optimized_image/asserts.ts @@ -0,0 +1,25 @@ +/** + * @license + * Copyright Google LLC All Rights Reserved. + * + * Use of this source code is governed by an MIT-style license that can be + * found in the LICENSE file at https://angular.io/license + */ + +import {ɵRuntimeError as RuntimeError} from '@angular/core'; + +import {RuntimeErrorCode} from '../../errors'; + +/** + * Asserts whether an ngDevMode is enabled and throws an error if it's not the case. + * This assert can be used to make sure that there is no dev-mode code invoked in + * the prod mode accidentally. + */ +export function assertDevMode(check: string) { + if (!ngDevMode) { + throw new RuntimeError( + RuntimeErrorCode.UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE, + `Unexpected invocation of the ${check} in the prod mode. ` + + `Please make sure that the prod mode is enabled for production builds.`); + } +} diff --git a/packages/common/src/directives/ng_optimized_image/index.ts b/packages/common/src/directives/ng_optimized_image/index.ts index 78e213eb08c92..c80d4b5e4d4e1 100644 --- a/packages/common/src/directives/ng_optimized_image/index.ts +++ b/packages/common/src/directives/ng_optimized_image/index.ts @@ -8,3 +8,4 @@ export {IMAGE_LOADER, ImageLoader, ImageLoaderConfig} from './image_loaders/image_loader'; export {provideImgixLoader} from './image_loaders/imgix_loader'; export {NgOptimizedImage, NgOptimizedImageModule} from './ng_optimized_image'; +export {PRECONNECT_CHECK_BLOCKLIST} from './preconnect_link_checker'; diff --git a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts index 23ad3f4447cbb..b9ebcdb39f5ed 100644 --- a/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts +++ b/packages/common/src/directives/ng_optimized_image/ng_optimized_image.ts @@ -11,6 +11,7 @@ import {Directive, ElementRef, Inject, Injectable, Injector, Input, NgModule, Ng import {DOCUMENT} from '../../dom_tokens'; import {RuntimeErrorCode} from '../../errors'; +import {assertDevMode} from './asserts'; import {IMAGE_LOADER, ImageLoader} from './image_loaders/image_loader'; import {PreconnectLinkChecker} from './preconnect_link_checker'; import {getUrl, imgDirectiveDetails} from './util'; @@ -57,6 +58,7 @@ export class LCPImageObserver implements OnDestroy { private observer: PerformanceObserver|null = null; constructor(@Inject(DOCUMENT) doc: Document) { + assertDevMode('LCP checker'); const win = doc.defaultView; if (typeof win !== 'undefined' && typeof PerformanceObserver !== 'undefined') { this.window = win; diff --git a/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts b/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts index 9bc9bf9178abd..d6a8958a20bde 100644 --- a/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts +++ b/packages/common/src/directives/ng_optimized_image/preconnect_link_checker.ts @@ -6,12 +6,35 @@ * found in the LICENSE file at https://angular.io/license */ -import {Inject, Injectable, NgZone, ɵformatRuntimeError as formatRuntimeError} from '@angular/core'; +import {Inject, Injectable, InjectionToken, Optional, ɵformatRuntimeError as formatRuntimeError, ɵRuntimeError as RuntimeError} from '@angular/core'; import {DOCUMENT} from '../../dom_tokens'; import {RuntimeErrorCode} from '../../errors'; -import {getUrl, imgDirectiveDetails} from './util'; +import {assertDevMode} from './asserts'; +import {deepForEach, extractHostname, getUrl, imgDirectiveDetails} from './util'; + +// Set of origins that are always excluded from the preconnect checks. +const INTERNAL_PRECONNECT_CHECK_BLOCKLIST = new Set(['localhost', '127.0.0.1', '0.0.0.0']); + +/** + * Multi-provider injection token to configure which origins should be excluded + * from the preconnect checks. If can either be a single string or an array of strings + * to represent a group of origins, for example: + * + * ```typescript + * {provide: PRECONNECT_CHECK_BLOCKLIST, multi: true, useValue: 'https://your-domain.com'} + * ``` + * + * or: + * + * ```typescript + * {provide: PRECONNECT_CHECK_BLOCKLIST, multi: true, + * useValue: ['https://your-domain-1.com', 'https://your-domain-2.com']} + * ``` + */ +export const PRECONNECT_CHECK_BLOCKLIST = + new InjectionToken>('PRECONNECT_CHECK_BLOCKLIST'); /** * Contains the logic to detect whether an image, marked with the "priority" attribute @@ -31,18 +54,39 @@ export class PreconnectLinkChecker { private window: Window|null = null; - constructor(@Inject(DOCUMENT) private doc: Document, private ngZone: NgZone) { + private blocklist = new Set(INTERNAL_PRECONNECT_CHECK_BLOCKLIST); + + constructor( + @Inject(DOCUMENT) private doc: Document, + @Optional() @Inject(PRECONNECT_CHECK_BLOCKLIST) blocklist: Array|null) { + assertDevMode('preconnect link checker'); const win = doc.defaultView; if (typeof win !== 'undefined') { this.window = win; } + if (blocklist) { + this.pupulateBlocklist(blocklist); + } + } + + private pupulateBlocklist(origins: Array) { + if (Array.isArray(origins)) { + deepForEach(origins, origin => { + this.blocklist.add(extractHostname(origin)); + }); + } else { + throw new RuntimeError( + RuntimeErrorCode.INVALID_PRECONNECT_CHECK_BLOCKLIST, + `The blocklist for the preconnect check was not provided as an array. ` + + `Check that the \`PRECONNECT_CHECK_BLOCKLIST\` token is configured as a \`multi: true\` provider.`); + } } check(rewrittenSrc: string, rawSrc: string) { if (!this.window) return; const imgUrl = getUrl(rewrittenSrc, this.window); - if (this.alreadySeen.has(imgUrl.origin)) return; + if (this.blocklist.has(imgUrl.hostname) || this.alreadySeen.has(imgUrl.origin)) return; // Register this origin as seen, so we don't check it again later. this.alreadySeen.add(imgUrl.origin); diff --git a/packages/common/src/directives/ng_optimized_image/util.ts b/packages/common/src/directives/ng_optimized_image/util.ts index 556737f473fce..0b9af3e4d623e 100644 --- a/packages/common/src/directives/ng_optimized_image/util.ts +++ b/packages/common/src/directives/ng_optimized_image/util.ts @@ -8,9 +8,13 @@ // Converts a string that represents a URL into a URL class instance. export function getUrl(src: string, win: Window): URL { - const isAbsolute = /^https?:\/\//.test(src); // Don't use a base URL is the URL is absolute. - return isAbsolute ? new URL(src) : new URL(src, win.location.href); + return isAbsoluteURL(src) ? new URL(src) : new URL(src, win.location.href); +} + +// Checks whether a URL is absolute (i.e. starts with `http://` or `https://`). +export function isAbsoluteURL(src: string): boolean { + return /^https?:\/\//.test(src); } // Assembles directive details string, useful for error messages. @@ -18,3 +22,19 @@ export function imgDirectiveDetails(rawSrc: string) { return `The NgOptimizedImage directive (activated on an element ` + `with the \`rawSrc="${rawSrc}"\`)`; } + +// Invokes a callback for each element in the array. Also invokes a callback +// recursively for each nested array. +export function deepForEach(input: (T|any[])[], fn: (value: T) => void): void { + input.forEach(value => Array.isArray(value) ? deepForEach(value, fn) : fn(value)); +} + +// Given a URL, extract the hostname part. +// If a URL is a relative one - the URL is returned as is. +export function extractHostname(url: string): string { + if (isAbsoluteURL(url)) { + const instance = new URL(url); + return instance.hostname; + } + return url; +} diff --git a/packages/common/src/errors.ts b/packages/common/src/errors.ts index 27b8de1a3d765..b3d0073877f6f 100644 --- a/packages/common/src/errors.ts +++ b/packages/common/src/errors.ts @@ -28,4 +28,6 @@ export const enum RuntimeErrorCode { REQUIRED_INPUT_MISSING = 2954, LCP_IMG_MISSING_PRIORITY = 2955, PRIORITY_IMG_MISSING_PRECONNECT_TAG = 2956, + INVALID_PRECONNECT_CHECK_BLOCKLIST = 2957, + UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE = 2958, } diff --git a/packages/common/src/private_export.ts b/packages/common/src/private_export.ts index 8b7550e7700b3..a3340421bf547 100644 --- a/packages/common/src/private_export.ts +++ b/packages/common/src/private_export.ts @@ -6,6 +6,6 @@ * found in the LICENSE file at https://angular.io/license */ -export {IMAGE_LOADER as ɵIMAGE_LOADER, ImageLoader as ɵImageLoader, ImageLoaderConfig as ɵImageLoaderConfig, NgOptimizedImage as ɵNgOptimizedImage, NgOptimizedImageModule as ɵNgOptimizedImageModule, provideImgixLoader as ɵprovideImgixLoader} from './directives/ng_optimized_image'; +export {IMAGE_LOADER as ɵIMAGE_LOADER, ImageLoader as ɵImageLoader, ImageLoaderConfig as ɵImageLoaderConfig, NgOptimizedImage as ɵNgOptimizedImage, NgOptimizedImageModule as ɵNgOptimizedImageModule, PRECONNECT_CHECK_BLOCKLIST as ɵPRECONNECT_CHECK_BLOCKLIST, provideImgixLoader as ɵprovideImgixLoader} from './directives/ng_optimized_image/index'; export {DomAdapter as ɵDomAdapter, getDOM as ɵgetDOM, setRootDomAdapter as ɵsetRootDomAdapter} from './dom_adapter'; export {BrowserPlatformLocation as ɵBrowserPlatformLocation} from './location/platform_location'; diff --git a/packages/common/test/directives/ng_optimized_image_spec.ts b/packages/common/test/directives/ng_optimized_image_spec.ts index fd2d5d56393ad..521c2c4461ddf 100644 --- a/packages/common/test/directives/ng_optimized_image_spec.ts +++ b/packages/common/test/directives/ng_optimized_image_spec.ts @@ -15,6 +15,7 @@ import {withHead} from '@angular/private/testing'; import {IMAGE_LOADER, ImageLoader, ImageLoaderConfig} from '../../src/directives/ng_optimized_image/image_loaders/image_loader'; import {assertValidRawSrcset, NgOptimizedImageModule} from '../../src/directives/ng_optimized_image/ng_optimized_image'; +import {PRECONNECT_CHECK_BLOCKLIST} from '../../src/directives/ng_optimized_image/preconnect_link_checker'; describe('Image directive', () => { it('should set `loading` and `fetchpriority` attributes before `src`', () => { @@ -498,9 +499,14 @@ describe('Image directive', () => { }); describe('preconnect detector', () => { + const imageLoader = () => { + // We need something different from the `localhost` (as we don't want to produce + // a preconnect warning for local environments). + return 'https://angular.io/assets/images/logos/angular/angular.svg'; + }; it('should log a warning if there is no preconnect link for a priority image', withHead('', () => { - setupTestingModule(); + setupTestingModule({imageLoader}); const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; @@ -515,12 +521,12 @@ describe('Image directive', () => { 'contains the "priority" attribute, but doesn\'t have a corresponding ' + 'preconnect tag. Please add the following element into the of ' + 'the document to optimize loading of this image:' + - '\n '); + '\n '); })); it('should not log a warning if there is no preconnect link, but the image is not set as a priority', withHead('', () => { - setupTestingModule(); + setupTestingModule({imageLoader}); const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; @@ -532,8 +538,8 @@ describe('Image directive', () => { })); it('should log a warning if there is a preconnect, but it doesn\'t match the priority image', - withHead('', () => { - setupTestingModule(); + withHead('', () => { + setupTestingModule({imageLoader}); const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; @@ -548,32 +554,34 @@ describe('Image directive', () => { 'this image contains the "priority" attribute, but doesn\'t have ' + 'a corresponding preconnect tag. Please add the following element ' + 'into the of the document to optimize loading of this image:' + - '\n '); + '\n '); })); it('should log a warning if there is no matching preconnect link for a priority image, but there is a preload tag', - withHead('', () => { - setupTestingModule(); - - const consoleWarnSpy = spyOn(console, 'warn'); - const template = ''; - const fixture = createTestComponent(template); - fixture.detectChanges(); - - expect(consoleWarnSpy.calls.count()).toBe(1); - expect(consoleWarnSpy.calls.argsFor(0)[0]) - .toBe( - 'NG02956: The NgOptimizedImage directive (activated on an ' + - ' element with the `rawSrc="a.png"`) has detected that ' + - 'this image contains the "priority" attribute, but doesn\'t have ' + - 'a corresponding preconnect tag. Please add the following element ' + - 'into the of the document to optimize loading of this image:' + - '\n '); - })); + withHead( + '', + () => { + setupTestingModule({imageLoader}); + + const consoleWarnSpy = spyOn(console, 'warn'); + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + expect(consoleWarnSpy.calls.count()).toBe(1); + expect(consoleWarnSpy.calls.argsFor(0)[0]) + .toBe( + 'NG02956: The NgOptimizedImage directive (activated on an ' + + ' element with the `rawSrc="a.png"`) has detected that ' + + 'this image contains the "priority" attribute, but doesn\'t have ' + + 'a corresponding preconnect tag. Please add the following element ' + + 'into the of the document to optimize loading of this image:' + + '\n '); + })); it('should not log a warning if there is a matching preconnect link for a priority image (with an extra `/` at the end)', - withHead('', () => { - setupTestingModule(); + withHead('', () => { + setupTestingModule({imageLoader}); const consoleWarnSpy = spyOn(console, 'warn'); const template = ''; @@ -583,6 +591,105 @@ describe('Image directive', () => { // Expect no warnings in the console. expect(consoleWarnSpy.calls.count()).toBe(0); })); + + ['localhost', '127.0.0.1', '0.0.0.0'].forEach(blocklistedHostname => { + it(`should not log a warning if an origin domain is blocklisted ` + + `(checking ${blocklistedHostname})`, + withHead('', () => { + const imageLoader = () => { + return `http://${blocklistedHostname}/a.png`; + }; + setupTestingModule({imageLoader}); + + const consoleWarnSpy = spyOn(console, 'warn'); + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + // Expect no warnings in the console. + expect(consoleWarnSpy.calls.count()).toBe(0); + })); + }); + + describe('PRECONNECT_CHECK_BLOCKLIST token', () => { + it(`should allow passing host names`, withHead('', () => { + const providers = [ + {provide: PRECONNECT_CHECK_BLOCKLIST, useValue: 'angular.io', multi: true}, + ]; + setupTestingModule({imageLoader, extraProviders: providers}); + + const consoleWarnSpy = spyOn(console, 'warn'); + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + // Expect no warnings in the console. + expect(consoleWarnSpy.calls.count()).toBe(0); + })); + + it(`should allow passing origins`, withHead('', () => { + const providers = [ + {provide: PRECONNECT_CHECK_BLOCKLIST, useValue: 'https://angular.io', multi: true}, + ]; + setupTestingModule({imageLoader, extraProviders: providers}); + + const consoleWarnSpy = spyOn(console, 'warn'); + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + // Expect no warnings in the console. + expect(consoleWarnSpy.calls.count()).toBe(0); + })); + + it(`should allow passing arrays of host names`, withHead('', () => { + const providers = [ + {provide: PRECONNECT_CHECK_BLOCKLIST, useValue: ['https://angular.io'], multi: true}, + ]; + setupTestingModule({imageLoader, extraProviders: providers}); + + const consoleWarnSpy = spyOn(console, 'warn'); + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + // Expect no warnings in the console. + expect(consoleWarnSpy.calls.count()).toBe(0); + })); + + it(`should allow passing nested arrays of host names`, withHead('', () => { + const providers = [ + {provide: PRECONNECT_CHECK_BLOCKLIST, useValue: [['https://angular.io']], multi: true}, + ]; + setupTestingModule({imageLoader, extraProviders: providers}); + + const consoleWarnSpy = spyOn(console, 'warn'); + const template = ''; + const fixture = createTestComponent(template); + fixture.detectChanges(); + + // Expect no warnings in the console. + expect(consoleWarnSpy.calls.count()).toBe(0); + })); + + it(`should throw when PRECONNECT_CHECK_BLOCKLIST is not a multi provider`, + withHead('', () => { + const providers = [ + {provide: PRECONNECT_CHECK_BLOCKLIST, useValue: 'https://angular.io'}, + ]; + setupTestingModule({imageLoader, extraProviders: providers}); + + const template = ''; + expect(() => { + const fixture = createTestComponent(template); + fixture.detectChanges(); + }) + .toThrowError( + 'NG02957: The blocklist for the preconnect check was not ' + + 'provided as an array. Check that the `PRECONNECT_CHECK_BLOCKLIST` token ' + + 'is configured as a `multi: true` provider.'); + })); + }); }); describe('loaders', () => { @@ -794,15 +901,17 @@ class TestComponent { priority = false; } -function setupTestingModule(config?: {imageLoader: ImageLoader}) { +function setupTestingModule(config?: {imageLoader?: ImageLoader, extraProviders?: any[]}) { const defaultLoader = (config: ImageLoaderConfig) => { const isAbsolute = /^https?:\/\//.test(config.src); return isAbsolute ? config.src : window.location.origin + '/' + config.src; }; const loader = config?.imageLoader || defaultLoader; + const extraProviders = config?.extraProviders || []; const providers: Provider[] = [ {provide: DOCUMENT, useValue: window.document}, {provide: IMAGE_LOADER, useValue: loader}, + ...extraProviders, ]; TestBed.configureTestingModule({ diff --git a/packages/core/test/bundling/image-directive/e2e/basic/basic.e2e-spec.ts b/packages/core/test/bundling/image-directive/e2e/basic/basic.e2e-spec.ts index e40076d2a907f..ccfed9aa18d4a 100644 --- a/packages/core/test/bundling/image-directive/e2e/basic/basic.e2e-spec.ts +++ b/packages/core/test/bundling/image-directive/e2e/basic/basic.e2e-spec.ts @@ -16,7 +16,7 @@ describe('NgOptimizedImage directive', () => { await browser.get('/e2e/basic'); const imgs = element.all(by.css('img')); const src = await imgs.get(0).getAttribute('src'); - expect(/b\.png/.test(src)).toBe(true); + expect(/angular\.svg/.test(src)).toBe(true); // Since there are no preconnect tags on a page, // we expect a log in a console that mentions that. diff --git a/packages/core/test/bundling/image-directive/e2e/basic/basic.ts b/packages/core/test/bundling/image-directive/e2e/basic/basic.ts index 06b4701d81b2c..5fcf6f84dc9ef 100644 --- a/packages/core/test/bundling/image-directive/e2e/basic/basic.ts +++ b/packages/core/test/bundling/image-directive/e2e/basic/basic.ts @@ -14,7 +14,10 @@ import {Component} from '@angular/core'; standalone: true, imports: [NgOptimizedImageModule], template: ``, - providers: [{provide: IMAGE_LOADER, useValue: () => '/e2e/b.png'}], + providers: [{ + provide: IMAGE_LOADER, + useValue: () => 'https://angular.io/assets/images/logos/angular/angular.svg' + }], }) export class BasicComponent { } diff --git a/packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.e2e-spec.ts b/packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.e2e-spec.ts index 96d0ca781a809..db58c6ff5c1df 100644 --- a/packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.e2e-spec.ts +++ b/packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.e2e-spec.ts @@ -38,7 +38,7 @@ describe('NgOptimizedImage directive', () => { expect(logs[0].message).toMatch(/NG02956.*?a\.png/); }); - it('should not produce any logs when a preconect tag is present', async () => { + it('should not produce any warnings in the console when a preconect tag is present', async () => { await browser.get('/e2e/preconnect-check?preconnect'); await verifyImagesPresent(element); diff --git a/packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.ts b/packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.ts index dc5fb7ef8c935..51c51056b3b58 100644 --- a/packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.ts +++ b/packages/core/test/bundling/image-directive/e2e/preconnect-check/preconnect-check.ts @@ -6,7 +6,7 @@ * found in the LICENSE file at https://angular.io/license */ -import {DOCUMENT, ɵNgOptimizedImageModule as NgOptimizedImageModule} from '@angular/common'; +import {DOCUMENT, ɵIMAGE_LOADER as IMAGE_LOADER, ɵNgOptimizedImageModule as NgOptimizedImageModule} from '@angular/common'; import {Component, Inject} from '@angular/core'; @Component({ @@ -18,6 +18,10 @@ import {Component, Inject} from '@angular/core'; `, + providers: [{ + provide: IMAGE_LOADER, + useValue: (config: {src: string}) => `https://angular.io/assets/images/${config.src}` + }], }) export class PreconnectCheckComponent { constructor(@Inject(DOCUMENT) private doc: Document) { @@ -34,7 +38,7 @@ export class PreconnectCheckComponent { const url = new URL(win.location.href).searchParams; const preconnect = url.get('preconnect'); if (preconnect !== null) { - const link = this.createLinkElement('preconnect', win.location.origin); + const link = this.createLinkElement('preconnect', 'https://angular.io'); this.doc.head.appendChild(link); } } diff --git a/packages/core/test/bundling/image-directive/e2e/util.ts b/packages/core/test/bundling/image-directive/e2e/util.ts index ebc30a2c7ba2c..ec13bd282d539 100644 --- a/packages/core/test/bundling/image-directive/e2e/util.ts +++ b/packages/core/test/bundling/image-directive/e2e/util.ts @@ -10,7 +10,9 @@ import {browser} from 'protractor'; import {logging} from 'selenium-webdriver'; -export async function collectBrowserLogs(minLoggingLevel: logging.Level): Promise { +export async function collectBrowserLogs( + loggingLevel: logging.Level, + collectMoreSevereErrors: boolean = false): Promise { const browserLog = await browser.manage().logs().get('browser'); const collectedLogs: logging.Entry[] = []; @@ -32,7 +34,8 @@ export async function collectBrowserLogs(minLoggingLevel: logging.Level): Promis console.log('>> ' + msg, logEntry); - if (logEntry.level.value >= minLoggingLevel.value) { + if ((!collectMoreSevereErrors && logEntry.level.value === loggingLevel.value) || + (collectMoreSevereErrors && logEntry.level.value >= loggingLevel.value)) { collectedLogs.push(logEntry); } }); @@ -40,6 +43,7 @@ export async function collectBrowserLogs(minLoggingLevel: logging.Level): Promis } export async function verifyNoBrowserErrors() { - const logs = await collectBrowserLogs(logging.Level.INFO); + const logs = + await collectBrowserLogs(logging.Level.INFO, true /* collect more severe errors too */); expect(logs).toEqual([]); }