From cde881c889bf84f30a448f4afbe801d985fe9177 Mon Sep 17 00:00:00 2001 From: Jess Sullivan Date: Tue, 12 May 2026 23:12:49 -0400 Subject: [PATCH] Add Playwright Chromium RBE smoke target --- .bazelrc | 1 + BUILD.bazel | 30 +++++ MODULE.bazel | 3 + README.md | 5 +- scripts/run-playwright-chromium-bazel.mjs | 138 ++++++++++++++++++++++ 5 files changed, 175 insertions(+), 2 deletions(-) create mode 100644 scripts/run-playwright-chromium-bazel.mjs diff --git a/.bazelrc b/.bazelrc index 911ade8..667cf30 100644 --- a/.bazelrc +++ b/.bazelrc @@ -15,6 +15,7 @@ build --action_env=NODE_OPTIONS=--max-old-space-size=4096 test --test_output=errors test --test_timeout=120,600,1800,3600 +test --test_env=GF_RBE_CHROMIUM_EXECUTABLE=/bin/chromium build:local --disk_cache=~/.cache/bazel-jesssullivan-github-io diff --git a/BUILD.bazel b/BUILD.bazel index 525b2af..5636fa1 100644 --- a/BUILD.bazel +++ b/BUILD.bazel @@ -29,6 +29,14 @@ filegroup( ], ) +filegroup( + name = "browser_smoke_srcs", + srcs = [ + "package.json", + "scripts/run-playwright-chromium-bazel.mjs", + ], +) + js_binary( name = "vitest_bazel", data = [ @@ -57,11 +65,33 @@ js_test( visibility = ["//visibility:public"], ) +js_test( + name = "playwright_chromium_smoke", + copy_data_to_bin = False, + data = [ + ":browser_smoke_srcs", + ":node_modules", + ], + entry_point = "scripts/run-playwright-chromium-bazel.mjs", + patch_node_fs = False, + tags = [ + "chromium", + "gloriousflywheel-rbe-candidate", + "playwright", + "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-vitest-bazel.mjs", "vitest.bazel.config.ts", ], diff --git a/MODULE.bazel b/MODULE.bazel index 7c78814..55404a8 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -31,6 +31,9 @@ npm.npm_translate_lock( "//packages/pulse-core:package.json", ], lifecycle_hooks_envs = { + "playwright": [ + "PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1", + ], "puppeteer": [ "PUPPETEER_SKIP_DOWNLOAD=true", "PUPPETEER_SKIP_CHROMIUM_DOWNLOAD=true", diff --git a/README.md b/README.md index 173891c..e6daf04 100644 --- a/README.md +++ b/README.md @@ -84,11 +84,12 @@ flowchart LR This repo still uses the npm/SvelteKit workflow for normal local development and deployment. The Bazel files are a narrow GloriousFlywheel consumer proof surface, not a wholesale migration of the blog build. - `//: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. - `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 Puppeteer browser downloads; browser-backed Playwright/Puppeteer RBE needs a separate pinned browser/toolchain tranche. +- 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 one public SvelteKit/Vite/Vitest target class for remote execution evidence. It does not prove default repo-wide RBE, Playwright/Puppeteer browser execution, the full SvelteKit build, or deployment. +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. diff --git a/scripts/run-playwright-chromium-bazel.mjs b/scripts/run-playwright-chromium-bazel.mjs new file mode 100644 index 0000000..a6ee0bc --- /dev/null +++ b/scripts/run-playwright-chromium-bazel.mjs @@ -0,0 +1,138 @@ +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 { chromium } from '@playwright/test'; + +const runtimeDir = mkdtempSync(join(tmpdir(), 'ghio-playwright-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, PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH, or CHROME_BIN.', + ); + process.exit(1); +} + +const packageJson = JSON.parse(await readFile('package.json', 'utf8')); + +let browser; +let page; +try { + browser = await chromium.launch({ + executablePath: chromiumExecutable, + headless: true, + args: ['--disable-dev-shm-usage', '--no-sandbox'], + }); + page = await browser.newPage({ + deviceScaleFactor: 1, + viewport: { width: 1024, height: 640 }, + }); + await page.setContent(renderSmokePage(packageJson.name), { waitUntil: 'load' }); + + const title = await page.locator('main h1').textContent(); + const target = await page.locator('[data-rbe-target]').textContent(); + if (title !== 'GloriousFlywheel browser RBE smoke') { + throw new Error(`unexpected smoke title: ${title}`); + } + if (target !== packageJson.name) { + throw new Error(`unexpected package marker: ${target}`); + } + + console.log(`Playwright Chromium smoke passed with ${chromiumExecutable}`); +} catch (error) { + if (page) { + await printPageDiagnostics(page); + } + throw error; +} finally { + await browser?.close(); +} + +function renderSmokePage(packageName) { + return ` + + + + Browser RBE smoke + + +
+

GloriousFlywheel browser RBE smoke

+

${escapeHtml(packageName)}

+
+ +`; +} + +function findChromiumExecutable() { + const candidates = [ + process.env.GF_RBE_CHROMIUM_EXECUTABLE, + process.env.PLAYWRIGHT_CHROMIUM_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.viewportSize(), + mainText: await page + .locator('main') + .textContent({ timeout: 1000 }) + .then((text) => text?.slice(0, 240)) + .catch(() => null), + }; + console.error(`Playwright Chromium smoke diagnostics: ${JSON.stringify(diagnostics, null, 2)}`); +}