Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/branch-review-ledger.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -278,3 +278,4 @@ Records before 2026-07-28 were written by hand and had drifted: 146 lines carrie
| 2026-07-30 | codex/coverage-scope-policy | 4da2a003bc2254507662d1b8b6e9768e94371abd | issue 139 coverage scope policy post-sync | approved: late main sync preserves deliberate workflow coverage and static-only skill policy | check:ci-scope; check:outstanding-issues; check:branch-review-ledger; diff check |
| 2026-07-30 | codex/archive-completed-ci-tasks | 5c902f422ceee78ef68132900fda734c1d5bc1f8 | archive issues 133 and 135 | approved: both rows were already resolved on current main and focused guards prove their contracts | check:ci-scope; check:outstanding-issues; check:branch-review-ledger; diff check |
| 2026-07-30 | codex/next-local-task | 3e6d6d69c15fc056773657e15879ba2283fa2899 | archive issues 129 and 132 | approved: documented constraints satisfy both explicit outcomes without overstating client-side enforcement | guard:push:self-test; focused vitest 24/24; check:github-actions; check:outstanding-issues; diff check |
| 2026-07-30 | codex/playwright-container-alignment | 5ce50f64993a43efb00c4f8cfa86c26c895b8532 | issue 121 container browser fallback | approved: managed browser remains preferred; immutable-container fallback is explicit, newest-compatible, logged, unit-pinned, and launch-proven | verify:cheap 443 files/4631 pass; focused vitest 37/37; fallback Chromium launch; focused Playwright 1/1; check:rag:fixtures; outstanding guard; diff check |
2 changes: 1 addition & 1 deletion docs/outstanding-issues.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/testing.md
Original file line numberDiff line numberDiff line change
Expand Up@@ -62,7 +62,7 @@ Reference examples: `tests/icon-button.dom.test.tsx` (accessible-name contract),

## Playwright ownership

The repository runner exclusively builds and serves each Playwright production app. It selects a safe port, verifies `/api/local-project-id`, uses an isolated `.next-playwright/<run-id>` build directory, replaces provider configuration with inert loopback values, and removes its server and output on success, failure, or signal. Playwright configuration never starts a server. The production boot guard permits this demo profile only when the output is isolated, provider mode is offline, credentials are absent, and the Supabase URL is the inert `127.0.0.1:1` target. Before acquiring the heavy lock or building, the runner preflights the Chromium (or requested Firefox/WebKit) executable — including the default `chrome-headless-shell` binary and any `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` override — and exits non-zero immediately when it is missing, so a launch-infra failure cannot be mistaken for product-test failures after a multi-minute build.
The repository runner exclusively builds and serves each Playwright production app. It selects a safe port, verifies `/api/local-project-id`, uses an isolated `.next-playwright/<run-id>` build directory, replaces provider configuration with inert loopback values, and removes its server and output on success, failure, or signal. Playwright configuration never starts a server. The production boot guard permits this demo profile only when the output is isolated, provider mode is offline, credentials are absent, and the Supabase URL is the inert `127.0.0.1:1` target. Before acquiring the heavy lock or building, the runner preflights the Chromium (or requested Firefox/WebKit) executable — including the default `chrome-headless-shell` binary and any `PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH` override — and exits non-zero immediately when it is missing, so a launch-infra failure cannot be mistaken for product-test failures after a multi-minute build. A download-disabled container that explicitly exposes `PLAYWRIGHT_BROWSERS_PATH` is the one exception: if the client-pinned shell is absent, the runner selects the newest preinstalled shell for the current platform and passes its exact path to Playwright. It logs that fallback before the build; ordinary developer caches still fail closed rather than silently selecting a stale browser.

When capturing Playwright or `verify:phone-chrome` output through a shell pipe (`cmd 2>&1 | tee …`), enable `set -o pipefail` (or avoid the pipe). Without it, bash reports the pipeline exit from `tee` (`0`) while the log still ends in `N failed` — a measurement artifact that previously looked like a green-when-broken gate (outstanding-issues #120). The Node runners themselves already propagate Playwright’s exit status.

Expand Down
93 changes: 90 additions & 3 deletions scripts/playwright-browser-preflight.mjs
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
#!/usr/bin/env node
import { existsSync } from "node:fs";
import { existsSync, readdirSync } from "node:fs";
import path from "node:path";
import { chromium, firefox, webkit } from "playwright";

Expand DownExpand Up@@ -46,6 +46,61 @@ const CHROMIUM_HEADLESS_SHELL_LAYOUTS = Object.freeze({
"chrome-win64": ["chrome-headless-shell-win64", "chrome-headless-shell.exe"],
});

const PREINSTALLED_CHROMIUM_LAYOUTS = Object.freeze({
linux: [
["chrome-headless-shell-linux64", "chrome-headless-shell"],
["chrome-linux", "headless_shell"],
],
darwin: {
x64: [["chrome-headless-shell-mac-x64", "chrome-headless-shell"]],
arm64: [["chrome-headless-shell-mac-arm64", "chrome-headless-shell"]],
},
win32: [["chrome-headless-shell-win64", "chrome-headless-shell.exe"]],
});

function preinstalledChromiumLayouts(platform = process.platform, architecture = process.arch) {
if (platform === "darwin") return PREINSTALLED_CHROMIUM_LAYOUTS.darwin[architecture] ?? [];
return PREINSTALLED_CHROMIUM_LAYOUTS[platform] ?? [];
}

/**
* Find the newest headless shell supplied by a download-disabled container.
*
* This is intentionally narrower than scanning every Playwright cache: a stale
* developer cache must still fail closed. The fallback is available only when
* the caller explicitly exposes PLAYWRIGHT_BROWSERS_PATH and disables browser
* downloads, which is the immutable-container shape recorded in issue #121.
*/
export function newestPreinstalledChromiumHeadlessShell(
browsersRoot,
{
fileExists = existsSync,
readDirectory = readdirSync,
platform = process.platform,
architecture = process.arch,
} = {},
) {
if (!browsersRoot) return null;
let directories;
try {
directories = readDirectory(browsersRoot, { withFileTypes: true });
} catch {
return null;
}
const revisions = directories
.filter((entry) => entry.isDirectory() && /^chromium_headless_shell-\d+$/.test(entry.name))
.map((entry) => ({ name: entry.name, revision: Number(entry.name.slice("chromium_headless_shell-".length)) }))
.sort((left, right) => right.revision - left.revision);
const layouts = preinstalledChromiumLayouts(platform, architecture);
for (const directory of revisions) {
for (const layout of layouts) {
const executable = path.join(browsersRoot, directory.name, ...layout);
if (fileExists(executable)) return executable;
}
}
return null;
}

/**
* Derive the default headless-shell binary Playwright launches for Chromium
* tests. `chromium.executablePath()` points at full Chrome for Testing; the
Expand DownExpand Up@@ -96,7 +151,16 @@ function browserFamilyForProject(project) {
return PROJECT_BROWSER_FAMILIES[project] ?? null;
}

export function resolvePlaywrightBrowserExecutable(family, env = process.env) {
export function resolvePlaywrightBrowserExecutable(
family,
env = process.env,
{
managedChromiumPath = defaultChromiumHeadlessShellPath(),
fileExists = existsSync,
platform = process.platform,
architecture = process.arch,
} = {},
) {
if (family === "chromium") {
const override = env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH?.trim();
if (override) {
Expand All@@ -106,9 +170,32 @@ export function resolvePlaywrightBrowserExecutable(family, env = process.env) {
source: "PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH",
};
}
if (managedChromiumPath && fileExists(managedChromiumPath)) {
return {
family,
path: managedChromiumPath,
source: "playwright chromium-headless-shell",
};
}
const downloadsDisabled = /^(?:1|true)$/i.test(env.PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD?.trim() ?? "");
if (downloadsDisabled) {
const preinstalled = newestPreinstalledChromiumHeadlessShell(env.PLAYWRIGHT_BROWSERS_PATH?.trim(), {
Comment thread
BigSimmo marked this conversation as resolved.
fileExists,
platform,
architecture,
});
if (preinstalled) {
return {
family,
path: preinstalled,
source: "preinstalled container Chromium (PLAYWRIGHT_BROWSERS_PATH)",
managedPath: managedChromiumPath,
};
}
}
return {
family,
path: defaultChromiumHeadlessShellPath(),
path: managedChromiumPath,
source: "playwright chromium-headless-shell",
};
}
Expand Down
11 changes: 10 additions & 1 deletion scripts/run-playwright.mjs
Original file line numberDiff line numberDiff line change
Expand Up@@ -50,7 +50,16 @@ const mockupProjectRequested =
// Fail loud on missing browser binaries before the heavy lock or production build.
// Otherwise launch failures surface as "N failed" product tests and are easy to misread
// when a caller pipes output without `pipefail` (outstanding-issues #120).
assertPlaywrightBrowsersReady(playwrightArgs);
const browserPreflight = assertPlaywrightBrowsersReady(playwrightArgs);
const preinstalledChromium = browserPreflight.checked.find(
(entry) => entry.source === "preinstalled container Chromium (PLAYWRIGHT_BROWSERS_PATH)",
);
if (preinstalledChromium && !process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH) {
process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = preinstalledChromium.path;
console.error(
`[playwright] Managed Chromium is unavailable; using the preinstalled container browser at ${preinstalledChromium.path}.`,
);
}

const runId = `${process.pid}-${Date.now()}`;
const relativeRunRoot = `.next-playwright/${runId}`;
Expand Down
49 changes: 49 additions & 0 deletions tests/playwright-browser-preflight.test.ts
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, it } from "vitest";
import {
defaultChromiumHeadlessShellPath,
newestPreinstalledChromiumHeadlessShell,
playwrightBrowserPreflight,
playwrightProjectNames,
requestedPlaywrightBrowserProjects,
Expand DownExpand Up@@ -83,6 +87,51 @@ describe("playwright browser preflight", () => {
});
});

it("selects the newest container shell only when managed downloads are disabled", () => {
const root = mkdtempSync(join(tmpdir(), "pw-container-browsers-"));
const older = join(root, "chromium_headless_shell-1194", "chrome-linux", "headless_shell");
const newer = join(root, "chromium_headless_shell-1200", "chrome-linux", "headless_shell");
try {
mkdirSync(join(older, ".."), { recursive: true });
mkdirSync(join(newer, ".."), { recursive: true });
writeFileSync(older, "");
writeFileSync(newer, "");

expect(newestPreinstalledChromiumHeadlessShell(root, { platform: "linux", architecture: "x64" })).toBe(newer);

const managedPath = join(root, "chromium_headless_shell-1234", "missing");
expect(
resolvePlaywrightBrowserExecutable(
"chromium",
{
NODE_ENV: "test",
PLAYWRIGHT_BROWSERS_PATH: root,
PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: "1",
},
{ managedChromiumPath: managedPath, platform: "linux", architecture: "x64" },
),
).toMatchObject({
family: "chromium",
path: newer,
source: "preinstalled container Chromium (PLAYWRIGHT_BROWSERS_PATH)",
managedPath,
});
expect(
resolvePlaywrightBrowserExecutable(
"chromium",
{ NODE_ENV: "test", PLAYWRIGHT_BROWSERS_PATH: root },
{ managedChromiumPath: managedPath, platform: "linux", architecture: "x64" },
),
).toEqual({
family: "chromium",
path: managedPath,
source: "playwright chromium-headless-shell",
});
} finally {
rmSync(root, { recursive: true, force: true });
}
});

it("fails closed when the required Chromium binary is missing", () => {
const result = playwrightBrowserPreflight(["--project=chromium"], {
NODE_ENV: "test",
Expand Down
2 changes: 2 additions & 0 deletions tests/test-runner-safety.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -633,6 +633,8 @@ describe("provider-safe test environment", () => {
);
expect(preflight).toContain("chromium_headless_shell");
expect(preflight).toContain("PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH");
expect(preflight).toContain("PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD");
expect(runner).toContain("process.env.PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH = preinstalledChromium.path");
expect(packageJson.scripts["test:e2e:pr"]).toContain('--grep-invert "@quarantine|@mockup"');
expect(packageJson.scripts["test:e2e:regression"]).toContain('--grep-invert "@critical|@quarantine|@mockup"');
expect(baseUrl.indexOf("if (!allowEnsure)")).toBeLessThan(baseUrl.indexOf("findExistingLocalProjectUrl();"));
Expand Down
Loading