build: switch core tests away from protractor

Reworks the tests under `core` not to depend on Protractor.

(cherry picked from commit 413c79c10b)
This commit is contained in:
Kristiyan Kostadinov
2026-08-14 09:13:11 +02:00
committed by Jessica Janiuk
parent 48fb5647b7
commit 003e35de81
14 changed files with 257 additions and 122 deletions
@@ -1,5 +1,5 @@
load("@aspect_rules_esbuild//esbuild:defs.bzl", "esbuild")
load("//tools:defaults.bzl", "http_server", "ng_project", "protractor_web_test_suite", "ts_project")
load("//tools:defaults.bzl", "http_server", "ng_project", "ts_project", "webdriver_test")
package(default_visibility = ["//visibility:public"])
@@ -66,16 +66,16 @@ ts_project(
]),
tsconfig = ":e2e/tsconfig-e2e.json",
deps = [
"//:node_modules/@types/jasminewd2",
"//:node_modules/@types/jasmine",
"//:node_modules/@types/node",
"//:node_modules/@types/selenium-webdriver",
"//:node_modules/protractor",
"//:node_modules/selenium-webdriver",
"//packages/private/testing",
],
)
protractor_web_test_suite(
name = "protractor_tests",
webdriver_test(
name = "webdriver_tests",
server = ":devserver",
deps = [
":img_dir_e2e_tests_lib",
@@ -6,8 +6,8 @@ This folder contains a simple application that can be used as a playground for t
pnpm 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:
There is also a set of e2e tests (powered by Selenium WebDriver), which can be invoked by running:
```
pnpm bazel test packages/core/test/bundling/image-directive:protractor_tests
pnpm bazel test packages/core/test/bundling/image-directive:webdriver_tests
```
@@ -6,21 +6,30 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {browser, by, element} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
import {collectBrowserLogs} from '../browser-logs-util';
describe('NgOptimizedImage directive (basic)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
describe('NgOptimizedImage directive', () => {
it('should render an image with an updated `src`', async () => {
await browser.get('/e2e/basic');
const imgs = element.all(by.css('img'));
const src = await imgs.get(0).getAttribute('src');
await driver.get(`${baseUrl}/e2e/basic`);
const imgs = await driver.findElements(webdriver.By.css('img'));
const src = await imgs[0].getAttribute('src');
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.
const logs = await collectBrowserLogs(logging.Level.WARNING);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(1);
// Verify that the error code and a raw image src are present.
@@ -7,15 +7,75 @@
*/
/* tslint:disable:no-console */
import {browser} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as fs from 'fs';
import * as path from 'path';
import * as webdriver from 'selenium-webdriver';
import * as chrome from 'selenium-webdriver/chrome.js';
declare const jasmine: any;
if (typeof jasmine !== 'undefined') {
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000;
}
function resolveRunfile(filePath: string): string {
if (fs.existsSync(filePath)) {
return filePath;
}
const runfilesDir = process.env['JS_BINARY__RUNFILES'] || process.env['RUNFILES_DIR'];
if (runfilesDir) {
const candidates = [
path.join(runfilesDir, filePath),
path.join(runfilesDir, '_main', filePath),
path.join(runfilesDir, filePath.replace(/^(\.\.\/)+/, '')),
path.join(runfilesDir, '_main', filePath.replace(/^(\.\.\/)+/, '')),
];
for (const c of candidates) {
if (fs.existsSync(c)) {
return c;
}
}
}
return filePath;
}
export interface E2eContext {
driver: webdriver.WebDriver;
baseUrl: string;
}
let serviceInitialized = false;
export async function createWebDriver(): Promise<E2eContext> {
const port = process.env['TEST_SERVER_PORT'] || '8080';
const baseUrl = `http://localhost:${port}`;
if (!serviceInitialized && process.env['CHROMEDRIVER']) {
const chromeDriverPath = resolveRunfile(process.env['CHROMEDRIVER']);
const service = new chrome.ServiceBuilder(chromeDriverPath).build();
chrome.setDefaultService(service);
serviceInitialized = true;
}
const options = new chrome.Options();
if (process.env['CHROME_HEADLESS_BIN']) {
options.setChromeBinaryPath(resolveRunfile(process.env['CHROME_HEADLESS_BIN']));
}
options.headless();
options.addArguments('--no-sandbox', '--disable-gpu', '--disable-dev-shm-usage');
const builder = new webdriver.Builder().forBrowser('chrome').setChromeOptions(options);
const driver = await builder.build();
return {driver, baseUrl};
}
export async function collectBrowserLogs(
loggingLevel: logging.Level,
driver: webdriver.WebDriver,
loggingLevel: webdriver.logging.Level,
collectMoreSevereErrors: boolean = false,
): Promise<logging.Entry[]> {
const browserLog = await browser.manage().logs().get('browser');
const collectedLogs: logging.Entry[] = [];
): Promise<webdriver.logging.Entry[]> {
const browserLog = await driver.manage().logs().get('browser');
const collectedLogs: webdriver.logging.Entry[] = [];
browserLog.forEach((logEntry) => {
const msg = logEntry.message;
@@ -32,9 +92,10 @@ export async function collectBrowserLogs(
return collectedLogs;
}
export async function verifyNoBrowserErrors() {
export async function verifyNoBrowserErrors(driver: webdriver.WebDriver) {
const logs = await collectBrowserLogs(
logging.Level.INFO,
driver,
webdriver.logging.Level.INFO,
true /* collect more severe errors too */,
);
expect(logs).toEqual([]);
@@ -6,26 +6,35 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {browser} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
import {collectBrowserLogs} from '../browser-logs-util';
describe('NgOptimizedImage directive (fill-mode)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
describe('NgOptimizedImage directive', () => {
it('should not warn when an image in the fill mode is rendered correctly', async () => {
await browser.get('/e2e/fill-mode-passing');
const logs = await collectBrowserLogs(logging.Level.WARNING);
await driver.get(`${baseUrl}/e2e/fill-mode-passing`);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(0);
});
it('should warn if an image in the fill mode has zero height after rendering', async () => {
await browser.get('/e2e/fill-mode-failing');
const logs = await collectBrowserLogs(logging.Level.WARNING);
await driver.get(`${baseUrl}/e2e/fill-mode-failing`);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(1);
// Image loading order is not guaranteed, so all logs, rather than single entry
// needs to be checked in order to test whether a given error message is present.
const expectErrorMessageInLogs = (logs: logging.Entry[], message: string) => {
const expectErrorMessageInLogs = (logs: webdriver.logging.Entry[], message: string) => {
expect(
logs.some((log) => {
return log.message.includes(message);
@@ -6,38 +6,44 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {browser} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
import {collectBrowserLogs} from '../browser-logs-util';
describe('NgOptimizedImage directive (image-distortion)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
describe('NgOptimizedImage directive', () => {
it('should not warn if there is no image distortion', async () => {
await browser.get('/e2e/image-distortion-passing');
const logs = await collectBrowserLogs(logging.Level.WARNING);
await driver.get(`${baseUrl}/e2e/image-distortion-passing`);
await new Promise((resolve) => setTimeout(resolve, 800));
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(0);
});
it('should warn if there is image distortion', async () => {
await browser.get('/e2e/image-distortion-failing');
await driver.get(`${baseUrl}/e2e/image-distortion-failing`);
const logs: webdriver.logging.Entry[] = [];
const startTime = Date.now();
while (Date.now() - startTime < 5000) {
const newLogs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
logs.push(...newLogs);
if (logs.length >= 8) break;
await new Promise((r) => setTimeout(r, 200));
}
// Use a deterministic wait to pool logs until all image distortion warnings are detected.
const logs: logging.Entry[] = [];
await browser.wait(
async () => {
const newLogs = await collectBrowserLogs(logging.Level.WARNING);
logs.push(
...newLogs.filter((l) => l.message.includes(`NG02952`)), // RuntimeErrorCode.UNEXPECTED_DEV_MODE_CHECK_IN_PROD_MODE
);
return logs.length >= 8;
},
5000,
'Expected 8 image distortion logs to be produced.',
);
expect(logs.length).toEqual(8);
// Image loading order is not guaranteed, so all logs, rather than single entry
// needs to be checked in order to test whether a given error message is present.
const expectErrorMessageInLogs = (logs: logging.Entry[], message: string) => {
const expectErrorMessageInLogs = (logs: webdriver.logging.Entry[], message: string) => {
expect(
logs.some((log) => {
return log.message.includes(message);
@@ -6,28 +6,37 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {browser, by, element} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
import {collectBrowserLogs} from '../browser-logs-util';
describe('Image performance warnings (lazy)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
describe('Image performance warnings', () => {
it('should log a warning when a LCP image is loaded lazily', async () => {
await browser.get('/e2e/image-perf-warnings-lazy');
await driver.get(`${baseUrl}/e2e/image-perf-warnings-lazy`);
// Wait for load event
await new Promise((resolve) => setTimeout(resolve, 600));
// Verify that both images were rendered.
const imgs = element.all(by.css('img'));
let srcA = await imgs.get(0).getAttribute('src');
const imgs = await driver.findElements(webdriver.By.css('img'));
let srcA = await imgs[0].getAttribute('src');
expect(srcA.endsWith('a.png')).toBe(true);
let srcB = await imgs.get(1).getAttribute('src');
let srcB = await imgs[1].getAttribute('src');
expect(srcB.endsWith('b.png')).toBe(true);
// Make sure that only one LCP performance warning is in the console for image `a.png`,
// since `b.png` should be below the fold and not treated as an LCP element.
// NOTE: We specifically filter on the IMAGE_PERFORMANCE_WARNING (913) code because the browser
// or framework may emit other unrelated warning logs here.
const logs = (await collectBrowserLogs(logging.Level.WARNING)).filter((l) =>
const logs = (await collectBrowserLogs(driver, webdriver.logging.Level.WARNING)).filter((l) =>
l.message.includes('NG0913'),
);
expect(logs.length).toEqual(1);
@@ -6,17 +6,26 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {browser} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
import {collectBrowserLogs} from '../browser-logs-util';
describe('Image performance warnings (oversized)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
describe('Image performance warnings', () => {
it('should warn if rendered image size is much smaller than intrinsic size', async () => {
await browser.get('/e2e/image-perf-warnings-oversized');
await driver.get(`${baseUrl}/e2e/image-perf-warnings-oversized`);
// Wait for load event
await new Promise((resolve) => setTimeout(resolve, 600));
const logs = await collectBrowserLogs(logging.Level.WARNING);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(1);
@@ -26,11 +35,11 @@ describe('Image performance warnings', () => {
// https://github.com/angular/angular/issues/57941
it('should NOT warn if rendered SVG image size is much smaller that intrinsic size', async () => {
await browser.get('/e2e/svg-no-perf-oversized-warnings');
await driver.get(`${baseUrl}/e2e/svg-no-perf-oversized-warnings`);
// Wait for load event
await new Promise((resolve) => setTimeout(resolve, 600));
const logs = await collectBrowserLogs(logging.Level.WARNING);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
// Please note that prior to shipping the fix, it was logging a warning
// for the SVG image (see the attached issue above).
expect(logs.length).toEqual(0);
@@ -7,29 +7,38 @@
*/
/* tslint:disable:no-console */
import {browser, by, element} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
import {collectBrowserLogs} from '../browser-logs-util';
describe('NgOptimizedImage directive (lcp-check-duplicate)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
describe('NgOptimizedImage directive', () => {
it('should log a warning when a `priority` is missing on an LCP image', async () => {
await browser.get('/e2e/lcp-check-duplicate');
await driver.get(`${baseUrl}/e2e/lcp-check-duplicate`);
// Verify that both images were rendered.
const imgs = element.all(by.css('img'));
let srcB = await imgs.get(0).getAttribute('src');
const imgs = await driver.findElements(webdriver.By.css('img'));
let srcB = await imgs[0].getAttribute('src');
expect(srcB.endsWith('b.png')).toBe(true);
let srcA = await imgs.get(1).getAttribute('src');
let srcA = await imgs[1].getAttribute('src');
expect(srcA.endsWith('a.png')).toBe(true);
// The `b.png` and `a.png` images are used twice in a template.
srcB = await imgs.get(2).getAttribute('src');
srcB = await imgs[2].getAttribute('src');
expect(srcB.endsWith('b.png')).toBe(true);
srcA = await imgs.get(3).getAttribute('src');
srcA = await imgs[3].getAttribute('src');
expect(srcA.endsWith('a.png')).toBe(true);
// Make sure that no warnings are in the console for image `a.png`,
// since the first instance has the `priority` attribute, and is the LCP element.
const logs = await collectBrowserLogs(logging.Level.SEVERE);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.SEVERE);
expect(logs.length).toEqual(0);
});
});
@@ -6,24 +6,33 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {browser, by, element} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
import {collectBrowserLogs} from '../browser-logs-util';
describe('NgOptimizedImage directive (lcp-check)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
describe('NgOptimizedImage directive', () => {
it('should log a warning when a `priority` is missing on an LCP image', async () => {
await browser.get('/e2e/lcp-check');
await driver.get(`${baseUrl}/e2e/lcp-check`);
// Wait for ngSrc to be modified
await new Promise((resolve) => setTimeout(resolve, 600));
// Verify that both images were rendered.
const imgs = element.all(by.css('img'));
let srcB = await imgs.get(0).getAttribute('src');
const imgs = await driver.findElements(webdriver.By.css('img'));
let srcB = await imgs[0].getAttribute('src');
expect(srcB.endsWith('b.png')).toBe(true);
const srcA = await imgs.get(1).getAttribute('src');
const srcA = await imgs[1].getAttribute('src');
expect(srcA.endsWith('logo-500w.jpg')).toBe(true);
// The `b.png` image is used twice in a template.
srcB = await imgs.get(2).getAttribute('src');
srcB = await imgs[2].getAttribute('src');
expect(srcB.endsWith('b.png')).toBe(true);
// Make sure that only one warning is in the console for image `a.png`,
@@ -31,7 +40,7 @@ describe('NgOptimizedImage directive', () => {
// We use >= 1 and check the last log because the browser may sometimes report `b.png`
// as an intermediate LCP element before `a.png` is painted, causing an extra log.
// NOTE: This highlights a potential bug where the directive warns on intermediate LCP elements.
const logs = (await collectBrowserLogs(logging.Level.SEVERE)).filter(
const logs = (await collectBrowserLogs(driver, webdriver.logging.Level.SEVERE)).filter(
(l) => l.message.includes(`NG02955`), // LCP_IMG_MISSING_PRIORITY
);
expect(logs.length).toBeGreaterThanOrEqual(1);
@@ -6,21 +6,30 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {browser} from 'protractor';
import {logging} from 'selenium-webdriver';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
import {collectBrowserLogs} from '../browser-logs-util';
describe('NgOptimizedImage directive (oversized-image)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
describe('NgOptimizedImage directive', () => {
it('should not warn if there is no oversized image', async () => {
await browser.get('/e2e/oversized-image-passing');
const logs = await collectBrowserLogs(logging.Level.WARNING);
await driver.get(`${baseUrl}/e2e/oversized-image-passing`);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(0);
});
it('should warn if rendered image size is much smaller than intrinsic size', async () => {
await browser.get('/e2e/oversized-image-failing');
const logs = await collectBrowserLogs(logging.Level.WARNING);
await driver.get(`${baseUrl}/e2e/oversized-image-failing`);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(1);
@@ -6,30 +6,39 @@
* found in the LICENSE file at https://angular.dev/license
*/
import {browser, by, element, ElementHelper} from 'protractor';
import {logging} from 'selenium-webdriver';
import {collectBrowserLogs} from '../browser-logs-util';
import * as webdriver from 'selenium-webdriver';
import {collectBrowserLogs, createWebDriver} from '../browser-logs-util';
// Verifies that both images used in a component were rendered.
async function verifyImagesPresent(element: ElementHelper) {
const imgs = element.all(by.css('img'));
const srcA = await imgs.get(0).getAttribute('src');
async function verifyImagesPresent(driver: webdriver.WebDriver) {
const imgs = await driver.findElements(webdriver.By.css('img'));
const srcA = await imgs[0].getAttribute('src');
expect(srcA.endsWith('a.png')).toBe(true);
const srcB = await imgs.get(1).getAttribute('src');
const srcB = await imgs[1].getAttribute('src');
expect(srcB.endsWith('b.png')).toBe(true);
}
describe('NgOptimizedImage directive', () => {
it('should log a warning when there is no preconnect for priority images', async () => {
await browser.get('/e2e/preconnect-check');
describe('NgOptimizedImage directive (preconnect-check)', () => {
let driver: webdriver.WebDriver;
let baseUrl: string;
await verifyImagesPresent(element);
beforeAll(async () => {
({driver, baseUrl} = await createWebDriver());
});
afterAll(async () => {
await driver.quit();
});
it('should log a warning when there is no preconnect for priority images', async () => {
await driver.get(`${baseUrl}/e2e/preconnect-check`);
await verifyImagesPresent(driver);
// Make sure that only one warning is in the console for both images,
// because they both have the same base URL (which is used to look for
// corresponding `<link rel="preconnect">` tags).
const logs = await collectBrowserLogs(logging.Level.WARNING);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(1);
// Verify that the error code and a raw image src are present in the
@@ -38,12 +47,12 @@ describe('NgOptimizedImage directive', () => {
});
it('should not produce any warnings in the console when a preconnect tag is present', async () => {
await browser.get('/e2e/preconnect-check?preconnect');
await driver.get(`${baseUrl}/e2e/preconnect-check?preconnect`);
await verifyImagesPresent(element);
await verifyImagesPresent(driver);
// Make sure there are no browser logs.
const logs = await collectBrowserLogs(logging.Level.WARNING);
const logs = await collectBrowserLogs(driver, webdriver.logging.Level.WARNING);
expect(logs.length).toEqual(0);
});
});
@@ -1,7 +1,7 @@
{
"compilerOptions": {
"lib": ["es2022"],
"types": ["node", "jasminewd2"],
"types": ["node", "jasmine", "selenium-webdriver"],
"declaration": true,
"sourceMap": true
}
@@ -7,7 +7,7 @@
*/
import '@angular/compiler';
import {bootstrapApplication, provideProtractorTestingSupport} from '@angular/platform-browser';
import {bootstrapApplication} from '@angular/platform-browser';
import {RouterModule} from '@angular/router';
import {
ChangeDetectionStrategy,
@@ -63,9 +63,5 @@ const ROUTES = [
];
bootstrapApplication(RootComponent, {
providers: [
provideZoneChangeDetection(),
provideProtractorTestingSupport(), //
importProvidersFrom(RouterModule.forRoot(ROUTES)),
],
providers: [provideZoneChangeDetection(), importProvidersFrom(RouterModule.forRoot(ROUTES))],
});