mirror of
https://github.com/angular/angular.git
synced 2026-09-14 13:54:52 +08:00
test(common): add e2e tests for LCP check logic of the NgOptimizedImage directive (#47082)
This commit adds e2e tests for the LCP check logic. Those tests are needed to verify the behavior in a real browser (vs relying on a Node environment). PR Close #47082
This commit is contained in:
committed by
Pawel Kozlowski
parent
d3c3426aa4
commit
37e3a6050b
@@ -11,6 +11,8 @@ export const enum RuntimeErrorCode {
|
||||
// (undocumented)
|
||||
INVALID_PIPE_ARGUMENT = 2100,
|
||||
// (undocumented)
|
||||
LCP_IMG_MISSING_PRIORITY = 2954,
|
||||
// (undocumented)
|
||||
NG_FOR_MISSING_DIFFER = -2200,
|
||||
// (undocumented)
|
||||
PARENT_NG_SWITCH_NOT_FOUND = 2000,
|
||||
|
||||
@@ -86,9 +86,15 @@ class LCPImageObserver implements OnDestroy {
|
||||
// Based on https://web.dev/lcp/#measure-lcp-in-javascript
|
||||
private initPerformanceObserver(): PerformanceObserver {
|
||||
const observer = new PerformanceObserver((entryList) => {
|
||||
for (const entry of entryList.getEntries()) {
|
||||
const entries = entryList.getEntries();
|
||||
if (entries.length > 0) {
|
||||
// Note: we use the latest entry produced by the `PerformanceObserver` as the best
|
||||
// signal on which element is actually an LCP one. As an example, the first image to load on
|
||||
// a page, by virtue of being the only thing on the page so far, is often a LCP candidate
|
||||
// and gets reported by PerformanceObserver, but isn't necessarily the LCP element.
|
||||
const lcpElement = entries[entries.length - 1];
|
||||
// Cast to `any` due to missing `element` on observed type of entry.
|
||||
const imgSrc = (entry as any).element?.src ?? '';
|
||||
const imgSrc = (lcpElement as any).element?.src ?? '';
|
||||
const img = this.images.get(imgSrc);
|
||||
// Exclude `data:` and `blob:` URLs, since they are not supported by the directive.
|
||||
if (img && !img.priority && !imgSrc.startsWith('data:') && !imgSrc.startsWith('blob:')) {
|
||||
|
||||
@@ -8,6 +8,7 @@ ng_module(
|
||||
srcs = [
|
||||
"basic/basic.ts",
|
||||
"index.ts",
|
||||
"lcp-check/lcp-check.ts",
|
||||
],
|
||||
deps = [
|
||||
"//packages/common",
|
||||
@@ -71,18 +72,20 @@ http_server(
|
||||
ts_library(
|
||||
name = "img_dir_e2e_tests_lib",
|
||||
testonly = True,
|
||||
srcs = glob(["**/*.e2e-spec.ts"]),
|
||||
tsconfig = ":tsconfig-e2e.json",
|
||||
srcs = ["e2e-util/util.ts"] + glob([
|
||||
"**/*.e2e-spec.ts",
|
||||
]),
|
||||
tsconfig = ":e2e-util/tsconfig-e2e.json",
|
||||
deps = [
|
||||
"//packages/examples/test-utils",
|
||||
"//packages/private/testing",
|
||||
"@npm//@types/selenium-webdriver",
|
||||
"@npm//protractor",
|
||||
],
|
||||
)
|
||||
|
||||
protractor_web_test_suite(
|
||||
name = "protractor_tests",
|
||||
on_prepare = ":start-server.js",
|
||||
on_prepare = ":e2e-util/start-server.js",
|
||||
server = ":devserver",
|
||||
deps = [
|
||||
":img_dir_e2e_tests_lib",
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
* NgOptimizedImage directive testing
|
||||
|
||||
This folder contains a simple application that can be used as a playground for the `NgOptimizedImage` directive testing. You can run the following command to start the dev server:
|
||||
|
||||
```
|
||||
yarn ibazel run packages/core/test/bundling/image-directive:devserver
|
||||
```
|
||||
|
||||
There is also a set of e2e tests (powered by Protractor), which can be invoked by running:
|
||||
|
||||
```
|
||||
yarn bazel test packages/core/test/bundling/image-directive:protractor_tests
|
||||
```
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
import {browser, by, element} from 'protractor';
|
||||
|
||||
import {verifyNoBrowserErrors} from '../../../../../examples/test-utils';
|
||||
import {verifyNoBrowserErrors} from '../e2e-util/util';
|
||||
|
||||
describe('NgOptimizedImage directive', () => {
|
||||
afterEach(verifyNoBrowserErrors);
|
||||
|
||||
@@ -10,10 +10,10 @@ import {ɵIMAGE_LOADER as IMAGE_LOADER, ɵNgOptimizedImage as NgOptimizedImage}
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
selector: 'basic',
|
||||
standalone: true,
|
||||
imports: [NgOptimizedImage],
|
||||
template: `<img rawSrc="./a.png" width="150" height="150">`,
|
||||
template: `<img rawSrc="./a.png" width="150" height="150" priority>`,
|
||||
providers: [{provide: IMAGE_LOADER, useValue: () => 'b.png'}],
|
||||
})
|
||||
export class BasicComponent {
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/* tslint:disable:no-console */
|
||||
import {browser} from 'protractor';
|
||||
import {logging} from 'selenium-webdriver';
|
||||
|
||||
export async function collectBrowserLogs(minLoggingLevel: logging.Level): Promise<logging.Entry[]> {
|
||||
const browserLog = await browser.manage().logs().get('browser');
|
||||
const collectedLogs: logging.Entry[] = [];
|
||||
|
||||
browserLog.forEach(logEntry => {
|
||||
const msg = logEntry.message;
|
||||
|
||||
// Since we currently use the `ts_devserver` from the Bazel TypeScript rules, which does
|
||||
// fallback to the "index.html" file for HTML5 pushState routing but does always serve the
|
||||
// expected fallback with a 404 status code, the browser will print a message about the 404,
|
||||
// while the page loaded properly. Ideally the "ts_devserver" would allow us to opt-in for
|
||||
// just returning a 200 status code, but the devserver is intended to be kept manually, so
|
||||
// we manually filter this error before ensuring there are no console errors.
|
||||
// TODO: This is a current limitation of using the "ts_devserver" with Angular routing.
|
||||
// Tracked with: TOOL-629
|
||||
if (msg.includes(
|
||||
`Failed to load resource: the server responded with a status of 404 (Not Found)`)) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('>> ' + msg, logEntry);
|
||||
|
||||
if (logEntry.level.value >= minLoggingLevel.value) {
|
||||
collectedLogs.push(logEntry);
|
||||
}
|
||||
});
|
||||
return collectedLogs;
|
||||
}
|
||||
|
||||
export async function verifyNoBrowserErrors() {
|
||||
const logs = await collectBrowserLogs(logging.Level.INFO);
|
||||
expect(logs).toEqual([]);
|
||||
}
|
||||
@@ -7,10 +7,11 @@
|
||||
*/
|
||||
|
||||
import {Component, importProvidersFrom} from '@angular/core';
|
||||
import {bootstrapApplication} from '@angular/platform-browser';
|
||||
import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser';
|
||||
import {RouterModule} from '@angular/router';
|
||||
|
||||
import {BasicComponent} from './basic/basic';
|
||||
import {LcpCheckComponent} from './lcp-check/lcp-check';
|
||||
|
||||
@Component({
|
||||
selector: 'app-root',
|
||||
@@ -22,9 +23,13 @@ export class RootComponent {
|
||||
}
|
||||
|
||||
const ROUTES = [
|
||||
{path: '', component: BasicComponent} //
|
||||
{path: '', component: BasicComponent}, //
|
||||
{path: 'lcp-check', component: LcpCheckComponent}
|
||||
];
|
||||
|
||||
bootstrapApplication(RootComponent, {
|
||||
providers: [importProvidersFrom(RouterModule.forRoot(ROUTES))],
|
||||
providers: [
|
||||
provideProtractorTestingSupport(), //
|
||||
importProvidersFrom(RouterModule.forRoot(ROUTES))
|
||||
],
|
||||
});
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* @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
|
||||
*/
|
||||
|
||||
/* tslint:disable:no-console */
|
||||
import {browser, by, element} from 'protractor';
|
||||
import {logging} from 'selenium-webdriver';
|
||||
|
||||
import {collectBrowserLogs} from '../e2e-util/util';
|
||||
|
||||
describe('NgOptimizedImage directive', () => {
|
||||
it('should log a warning when a `priority` is missing on an LCP image', async () => {
|
||||
await browser.get('/lcp-check');
|
||||
|
||||
// Verify that both images were rendered.
|
||||
const imgs = element.all(by.css('img'));
|
||||
let srcB = await imgs.get(0).getAttribute('src');
|
||||
expect(srcB.endsWith('b.png')).toBe(true);
|
||||
const srcA = await imgs.get(1).getAttribute('src');
|
||||
expect(srcA.endsWith('a.png')).toBe(true);
|
||||
// The `b.png` image is used twice in a template.
|
||||
srcB = await imgs.get(2).getAttribute('src');
|
||||
expect(srcB.endsWith('b.png')).toBe(true);
|
||||
|
||||
// Make sure that only one warning is in the console for image `a.png`,
|
||||
// since the `b.png` should be below the fold and not treated as an LCP element.
|
||||
const logs = await collectBrowserLogs(logging.Level.WARNING);
|
||||
expect(logs.length).toEqual(1);
|
||||
// Verify that the error code and the image src are present in the error message.
|
||||
expect(logs[0].message).toMatch(/NG02954.*?a\.png/);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,38 @@
|
||||
/**
|
||||
* @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 {ɵNgOptimizedImage as NgOptimizedImage} from '@angular/common';
|
||||
import {Component} from '@angular/core';
|
||||
|
||||
@Component({
|
||||
selector: 'lcp-check',
|
||||
standalone: true,
|
||||
imports: [NgOptimizedImage],
|
||||
template: `
|
||||
<!--
|
||||
'b.png' should *not* be treated as an LCP element,
|
||||
since there is a bigger one right below it
|
||||
-->
|
||||
<img rawSrc="./b.png" width="5" height="5">
|
||||
|
||||
<br>
|
||||
|
||||
<!-- 'a.png' should be treated as an LCP element -->
|
||||
<img rawSrc="./a.png" width="1500" height="2500">
|
||||
|
||||
<br>
|
||||
|
||||
<!--
|
||||
'b.png' should *not* be treated as an LCP element here
|
||||
as well, since it's below the fold
|
||||
-->
|
||||
<img rawSrc="./b.png" width="10" height="10">
|
||||
`,
|
||||
})
|
||||
export class LcpCheckComponent {
|
||||
}
|
||||
Reference in New Issue
Block a user