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/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/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 { 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,