diff --git a/BUILD.bazel b/BUILD.bazel index 5636fa1..637d0b2 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -34,6 +34,7 @@ filegroup( srcs = [ "package.json", "scripts/run-playwright-chromium-bazel.mjs", + "scripts/run-puppeteer-chromium-bazel.mjs", ], ) @@ -86,12 +87,34 @@ js_test( visibility = ["//visibility:public"], ) +js_test( + name = "puppeteer_chromium_smoke", + copy_data_to_bin = False, + data = [ + ":browser_smoke_srcs", + ":node_modules", + ], + entry_point = "scripts/run-puppeteer-chromium-bazel.mjs", + patch_node_fs = False, + tags = [ + "chromium", + "gloriousflywheel-rbe-candidate", + "puppeteer", + "sveltekit", + "vite", + "web-browser-runtime-authority", + ], + timeout = "moderate", + visibility = ["//visibility:public"], +) + exports_files( [ "package-lock.json", "package.json", "pnpm-lock.yaml", "scripts/run-playwright-chromium-bazel.mjs", + "scripts/run-puppeteer-chromium-bazel.mjs", "scripts/run-vitest-bazel.mjs", "vitest.bazel.config.ts", ], diff --git a/README.md b/README.md index e6daf04..e1db6e6 100644 --- a/README.md +++ b/README.md @@ -85,11 +85,12 @@ This repo still uses the npm/SvelteKit workflow for normal local development and - `//:types_unit_tests` wraps Vitest through `vitest.bazel.config.ts` and runs the existing `src/lib/types.test.ts` slice. - `//:playwright_chromium_smoke` launches Playwright against the pinned GloriousFlywheel Chromium runtime path. It is a browser-runtime smoke target, not the full hosted Playwright regression suite. +- `//:puppeteer_chromium_smoke` launches Puppeteer against the same pinned Chromium runtime path. It proves Puppeteer can consume browser runtime authority without lifecycle downloads. - `package-lock.json` remains the npm dependency authority for the app. `pnpm-lock.yaml` is the generated `rules_js` lock consumed by Bazel. - Bazel npm lifecycle hooks skip Playwright and Puppeteer browser downloads. Browser-backed RBE must use the pinned worker Chromium path rather than downloading browsers during proof actions. - GloriousFlywheel proof runs should use the external GF REAPI proof harness against this public repo checkout. -Current boundary: this proves narrow public SvelteKit/Vite/Vitest and Playwright/Chromium target classes for remote execution evidence. It does not prove default repo-wide RBE, the full hosted Playwright suite, Puppeteer browser execution, the full SvelteKit build, or deployment. +Current boundary: this proves narrow public SvelteKit/Vite/Vitest, Playwright/Chromium, and Puppeteer/Chromium target classes for remote execution evidence. It does not prove default repo-wide RBE, the full hosted Playwright suite, the full SvelteKit build, or deployment. diff --git a/scripts/run-puppeteer-chromium-bazel.mjs b/scripts/run-puppeteer-chromium-bazel.mjs new file mode 100644 index 0000000..03a6ccf --- /dev/null +++ b/scripts/run-puppeteer-chromium-bazel.mjs @@ -0,0 +1,132 @@ +import { accessSync, constants, existsSync, mkdirSync, mkdtempSync, statSync } from 'node:fs'; +import { readFile } from 'node:fs/promises'; +import { tmpdir } from 'node:os'; +import { join } from 'node:path'; +import puppeteer from 'puppeteer'; + +const runtimeDir = mkdtempSync(join(tmpdir(), 'ghio-puppeteer-chromium-')); +ensureWritableEnvDir('HOME', join(runtimeDir, 'home')); +ensureWritableEnvDir('XDG_CONFIG_HOME', join(runtimeDir, 'xdg-config')); +ensureWritableEnvDir('XDG_CACHE_HOME', join(runtimeDir, 'xdg-cache')); + +const chromiumExecutable = findChromiumExecutable(); +if (!chromiumExecutable) { + console.error( + 'No Chromium executable found. Set GF_RBE_CHROMIUM_EXECUTABLE, PUPPETEER_EXECUTABLE_PATH, or CHROME_BIN.', + ); + process.exit(1); +} + +const packageJson = JSON.parse(await readFile('package.json', 'utf8')); + +let browser; +let page; +try { + browser = await puppeteer.launch({ + executablePath: chromiumExecutable, + headless: 'new', + args: ['--disable-dev-shm-usage', '--no-sandbox'], + }); + page = await browser.newPage(); + await page.setViewport({ width: 1024, height: 640, deviceScaleFactor: 1 }); + await page.setContent(renderSmokePage(packageJson.name), { waitUntil: 'load' }); + + const title = await page.$eval('main h1', (element) => element.textContent); + const target = await page.$eval('[data-rbe-target]', (element) => element.textContent); + if (title !== 'GloriousFlywheel Puppeteer browser RBE smoke') { + throw new Error(`unexpected smoke title: ${title}`); + } + if (target !== packageJson.name) { + throw new Error(`unexpected package marker: ${target}`); + } + + console.log(`Puppeteer Chromium smoke passed with ${chromiumExecutable}`); +} catch (error) { + if (page) { + await printPageDiagnostics(page); + } + throw error; +} finally { + await browser?.close(); +} + +function renderSmokePage(packageName) { + return ` + + + + Puppeteer Browser RBE smoke + + +
+

GloriousFlywheel Puppeteer browser RBE smoke

+

${escapeHtml(packageName)}

+
+ +`; +} + +function findChromiumExecutable() { + const candidates = [ + process.env.GF_RBE_CHROMIUM_EXECUTABLE, + process.env.PUPPETEER_EXECUTABLE_PATH, + process.env.CHROME_BIN, + '/bin/chromium', + '/usr/bin/chromium', + '/usr/bin/chromium-browser', + '/usr/bin/google-chrome', + '/usr/bin/google-chrome-stable', + '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + '/Applications/Chromium.app/Contents/MacOS/Chromium', + ].filter(Boolean); + + for (const candidate of candidates) { + if (existsSync(candidate)) { + return candidate; + } + } + + return ''; +} + +function ensureWritableEnvDir(name, fallback) { + const current = process.env[name]; + if (current && isWritableDirectory(current)) { + return current; + } + + mkdirSync(fallback, { recursive: true }); + process.env[name] = fallback; + return fallback; +} + +function isWritableDirectory(path) { + try { + if (!existsSync(path) || !statSync(path).isDirectory()) { + return false; + } + accessSync(path, constants.W_OK); + return true; + } catch { + return false; + } +} + +function escapeHtml(value) { + return String(value) + .replaceAll('&', '&') + .replaceAll('<', '<') + .replaceAll('>', '>') + .replaceAll('"', '"') + .replaceAll("'", '''); +} + +async function printPageDiagnostics(page) { + const diagnostics = { + url: page.url(), + title: await page.title().catch(() => ''), + viewport: page.viewport(), + mainText: await page.$eval('main', (element) => element.textContent?.slice(0, 240)).catch(() => null), + }; + console.error(`Puppeteer Chromium smoke diagnostics: ${JSON.stringify(diagnostics, null, 2)}`); +}