From c69739c61ae3c9dae885739503a0f023cfac41c7 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Wed, 17 Dec 2025 13:39:39 -0500 Subject: [PATCH 1/2] test: introduce Puppeteer infrastructure for E2E tests Add a new `executeBrowserTest` utility in `tests/e2e/utils/puppeteer.ts` to manage the Puppeteer browser lifecycle. This utility handles `ng serve` output parsing for both Webpack and Vite, captures browser console errors, and provides default assertions for page content to reduce test boilerplate. Migrate the `jit-prod` E2E test to use this new utility as an initial validation of the new infrastructure. --- tests/BUILD.bazel | 1 + tests/e2e/tests/BUILD.bazel | 1 + tests/e2e/tests/build/jit-prod.ts | 4 +- tests/e2e/utils/BUILD.bazel | 1 + tests/e2e/utils/puppeteer.ts | 86 +++++++++++++++++++++++++++++++ tests/rollup.config.mjs | 2 +- 6 files changed, 92 insertions(+), 3 deletions(-) create mode 100644 tests/e2e/utils/puppeteer.ts diff --git a/tests/BUILD.bazel b/tests/BUILD.bazel index c93ffba8b5da..318bf3b965e4 100644 --- a/tests/BUILD.bazel +++ b/tests/BUILD.bazel @@ -73,6 +73,7 @@ e2e_suites( # TODO: Clean this up. "//:node_modules/express", "//:node_modules/undici", + "//:node_modules/puppeteer", ], runner = ":runner_entrypoint", ) diff --git a/tests/e2e/tests/BUILD.bazel b/tests/e2e/tests/BUILD.bazel index 33ced88bca3a..891814cb24eb 100644 --- a/tests/e2e/tests/BUILD.bazel +++ b/tests/e2e/tests/BUILD.bazel @@ -12,6 +12,7 @@ ts_project( "//:node_modules/@types/semver", "//:node_modules/express", "//:node_modules/fast-glob", + "//:node_modules/puppeteer", "//:node_modules/semver", "//:node_modules/undici", "//tests/e2e/utils", diff --git a/tests/e2e/tests/build/jit-prod.ts b/tests/e2e/tests/build/jit-prod.ts index 2042b0a8c93d..b2dc9d0bdddc 100644 --- a/tests/e2e/tests/build/jit-prod.ts +++ b/tests/e2e/tests/build/jit-prod.ts @@ -1,6 +1,6 @@ import { getGlobalVariable } from '../../utils/env'; -import { ng } from '../../utils/process'; import { updateJsonFile } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; export default async function () { // Make prod use JIT. @@ -18,5 +18,5 @@ export default async function () { }); // Test it works - await ng('e2e', '--configuration=production'); + await executeBrowserTest({ configuration: 'production' }); } diff --git a/tests/e2e/utils/BUILD.bazel b/tests/e2e/utils/BUILD.bazel index b3de5c29f9b5..8082ab9d97c4 100644 --- a/tests/e2e/utils/BUILD.bazel +++ b/tests/e2e/utils/BUILD.bazel @@ -15,6 +15,7 @@ ts_project( "//:node_modules/@types/semver", "//:node_modules/fast-glob", "//:node_modules/protractor", + "//:node_modules/puppeteer", "//:node_modules/semver", "//:node_modules/verdaccio", "//:node_modules/verdaccio-auth-memory", diff --git a/tests/e2e/utils/puppeteer.ts b/tests/e2e/utils/puppeteer.ts new file mode 100644 index 000000000000..af6b18278c3c --- /dev/null +++ b/tests/e2e/utils/puppeteer.ts @@ -0,0 +1,86 @@ +import { type Page, launch } from 'puppeteer'; +import { execAndWaitForOutputToMatch, killAllProcesses } from './process'; + +export interface BrowserTestOptions { + project?: string; + configuration?: string; + baseUrl?: string; + checkFn?: (page: Page) => Promise; + expectedTitleText?: string; +} + +export async function executeBrowserTest(options: BrowserTestOptions = {}) { + let url = options.baseUrl; + let hasStartedServer = false; + + try { + if (!url) { + // Start serving and find address (1 - Webpack; 2 - Vite) + const match = /(?:open your browser on|Local:)\s+(http:\/\/localhost:\d+\/)/; + const serveArgs = ['serve', '--port=0']; + if (options.project) { + serveArgs.push(options.project); + } + if (options.configuration) { + serveArgs.push(`--configuration=${options.configuration}`); + } + + const { stdout } = await execAndWaitForOutputToMatch('ng', serveArgs, match); + url = stdout.match(match)?.[1]; + if (!url) { + throw new Error('Could not find serving URL'); + } + hasStartedServer = true; + } + + const browser = await launch({ + executablePath: process.env['CHROME_BIN'], + headless: true, + args: ['--no-sandbox'], + }); + try { + const page = await browser.newPage(); + + // Capture errors + const errors: string[] = []; + page.on('console', (msg) => { + if (msg.type() === 'error') { + errors.push(msg.text()); + } + }); + page.on('pageerror', (err) => { + errors.push(err.toString()); + }); + + await page.goto(url); + + if (options.checkFn) { + await options.checkFn(page); + } else { + // Default check: verify h1 content and no browser errors + const expectedText = options.expectedTitleText || 'Hello, test-project'; + + // Wait for the h1 element to appear and contain the expected text + await page.waitForFunction( + (selector: string, text: string) => { + const doc = (globalThis as any).document; + return doc.querySelector(selector)?.textContent?.includes(text); + }, + { timeout: 10000 }, // Max 10 seconds wait time + 'h1', + expectedText, + ); + } + + if (errors.length > 0) { + throw new Error(`Browser console errors detected:\n${errors.join('\n')}`); + } + } finally { + await browser.close(); + } + } finally { + if (hasStartedServer) { + await killAllProcesses(); + } + } +} diff --git a/tests/rollup.config.mjs b/tests/rollup.config.mjs index 208e4cc78c42..c48299094fef 100644 --- a/tests/rollup.config.mjs +++ b/tests/rollup.config.mjs @@ -25,7 +25,7 @@ for (const file of testFiles) { export default { input: chunks, - external: ['undici'], // This cannot be bundled as `node:sqlite` is experimental in node.js 22. Remove once this feature is no longer behind a flag + external: ['undici', 'puppeteer'], // This cannot be bundled as `node:sqlite` is experimental in node.js 22. Remove once this feature is no longer behind a flag plugins: [ nodeResolve({ preferBuiltins: true, From 020148e5aec3561a5ab4d265c019ea759a457c29 Mon Sep 17 00:00:00 2001 From: Charles Lyding <19598772+clydin@users.noreply.github.com> Date: Thu, 18 Dec 2025 10:16:56 -0500 Subject: [PATCH 2/2] test: verify Puppeteer E2E infrastructure on Windows CI Integrates `executeBrowserTest` into the `basic/serve` E2E test to verify browser connectivity against a running HMR server. Additionally, enables the `basic/serve` test in the Windows CI workflow to ensure Puppeteer infrastructure stability on Windows. --- .github/workflows/pr.yml | 2 +- tests/e2e/tests/basic/serve.ts | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 5adafe0f79d9..fee961b29bc8 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -170,7 +170,7 @@ jobs: test_target_name: e2e.esbuild_node22 env: E2E_SHARD_TOTAL: 1 - TESTBRIDGE_TEST_ONLY: tests/basic/{build,rebuild}.ts + TESTBRIDGE_TEST_ONLY: tests/basic/{build,rebuild,serve}.ts e2e-package-managers: needs: build diff --git a/tests/e2e/tests/basic/serve.ts b/tests/e2e/tests/basic/serve.ts index 7623cc3a6afc..eac4823a3126 100644 --- a/tests/e2e/tests/basic/serve.ts +++ b/tests/e2e/tests/basic/serve.ts @@ -1,6 +1,7 @@ import assert from 'node:assert/strict'; import { killAllProcesses } from '../../utils/process'; import { ngServe } from '../../utils/project'; +import { executeBrowserTest } from '../../utils/puppeteer'; export default async function () { // Serve works without HMR @@ -11,6 +12,8 @@ export default async function () { // Serve works with HMR const hmrPort = await ngServe('--hmr'); await verifyResponse(hmrPort); + + await executeBrowserTest({ baseUrl: `http://localhost:${hmrPort}/` }); } async function verifyResponse(port: number): Promise {