Files
SnapOtter/tests/unit/e2e-harness-contract.test.ts
T
SnapOtterandGitHub d10d0f544f fix: release QA hardening across processing, media, security, and CI gates (#649)
A release-readiness QA pass over the whole product. The commits split into
defects a user would hit and gates that were reporting green while measuring
nothing.

## Fixes that change behaviour

Rate limiting was bypassable on every install: TRUST_PROXY defaulted to true, so
request.ip came from a client-set header and a forged X-Forwarded-For got past
the login limiter. The default is now a private-network trust list.

A transient Postgres outage stranded in-flight jobs, leaving finished output on
disk with no row pointing at it. A reconciler now resolves those rows and adopts
the bytes rather than dropping the work.

A Redis connection that moved to a new address wedged every read-blocked
consumer, so completions stopped signalling while health still answered 200.
Socket timeouts plus subscriber pings recover it.

Installing more than one AI bundle left the shared venv multi-versioned and
silently broke three tools. The installer now reconciles distributions to one
version each.

Converting an image to JXL at quality 1 through 4 returned a 500, because
libjxl 0.7 rejects the distance those values compute. The quality is floored at
what the encoder honours. A missing ffmpeg was also reported to the user as a
corrupt upload; it now says the engine is unavailable.

RAW uploads reached an unpatched LibRaw on arm64, so it is built from source at
0.22.2, and the release scan was split so it can fail on an unfixed critical
instead of hiding it behind ignore-unfixed.

## Gates that could not fail

Two mutation lanes ran zero mutants because Stryker crawled the gitignored docs
build; coverage discarded its whole report on any failing test; the lint gate
skipped root tests, scripts, and two workspaces; and several generated matrices
counted a host missing ffmpeg as a passing tool. Each now measures what it
claims.

Full evidence and the outstanding release items are tracked locally and are not
part of this branch.
2026-07-27 15:37:30 +08:00

783 lines
31 KiB
TypeScript

import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { afterEach, describe, expect, test, vi } from "vitest";
const root = process.cwd();
const createDbPath = path.join(root, "tests", "e2e-pg-create-db.cjs");
const e2eDir = path.join(root, "tests", "e2e");
const authSetupPath = path.join(e2eDir, "auth.setup.ts");
const helpersPath = path.join(root, "tests", "e2e", "helpers.ts");
const dockerPlaywrightConfigPath = path.join(root, "playwright.analytics.config.ts");
const analyticsLocalConfigPath = path.join(root, "playwright.analytics-local.config.ts");
const dockerAuthSetupPath = path.join(root, "tests", "e2e-docker", "auth.setup.ts");
const dockerAnalyticsApiPath = path.join(root, "tests", "e2e-docker", "analytics-api.spec.ts");
const analyticsLocalAuthSetupPath = path.join(root, "tests", "e2e-analytics", "auth.setup.ts");
const embeddedModePath = path.join(root, "tests", "e2e-docker", "embedded-mode.mjs");
const packagePath = path.join(root, "package.json");
const e2eRunnerPath = path.join(root, "scripts", "run-main-e2e.mjs");
const turboPath = path.join(root, "turbo.json");
const vitestConfigPath = path.join(root, "vitest.config.ts");
const isolationEnvKeys = [
"API_URL",
"BASE_URL",
"PLAYWRIGHT_API_PORT",
"PLAYWRIGHT_API_URL",
"PLAYWRIGHT_AUTH_FILE",
"PLAYWRIGHT_RUN_ID",
"PLAYWRIGHT_RUN_ROOT",
"PLAYWRIGHT_WEB_PORT",
"PLAYWRIGHT_WEB_URL",
] as const;
const originalEnv = new Map(isolationEnvKeys.map((key) => [key, process.env[key]]));
afterEach(() => {
vi.resetModules();
for (const key of isolationEnvKeys) {
const value = originalEnv.get(key);
if (value === undefined) delete process.env[key];
else process.env[key] = value;
}
});
async function loadConfig(overrides: Record<string, string>) {
for (const [key, value] of Object.entries(overrides)) process.env[key] = value;
vi.resetModules();
return (await import("../../playwright.config.js")).default;
}
async function loadDockerConfig(overrides: Record<string, string>) {
for (const key of isolationEnvKeys) delete process.env[key];
for (const [key, value] of Object.entries(overrides)) process.env[key] = value;
vi.resetModules();
return (await import("../../playwright.analytics.config.js")).default;
}
async function loadAnalyticsLocalConfig(overrides: Record<string, string>) {
for (const key of isolationEnvKeys) delete process.env[key];
for (const [key, value] of Object.entries(overrides)) process.env[key] = value;
vi.resetModules();
return (await import("../../playwright.analytics-local.config.js")).default;
}
function collectPlaywright(
configPath: string,
args: string[] = [],
env: Record<string, string> = {},
): string {
return execFileSync(
"pnpm",
["exec", "playwright", "test", "--config", configPath, ...args, "--list"],
{
cwd: root,
encoding: "utf8",
env: {
...process.env,
PLAYWRIGHT_RUN_ID: "collection_contract",
...env,
},
maxBuffer: 20 * 1024 * 1024,
stdio: "pipe",
},
);
}
function mutablePaths(config: Awaited<ReturnType<typeof loadConfig>>) {
const [apiServer] = config.webServer as Array<{ env: Record<string, string> }>;
const chromium = config.projects?.find((project) => project.name === "chromium");
const reporter = config.reporter as Array<[string, { outputFolder?: string }]>;
return {
authFile: chromium?.use?.storageState,
dataDir: apiServer.env.DATA_DIR,
filesStoragePath: apiServer.env.FILES_STORAGE_PATH,
outputDir: config.outputDir,
reportDir: reporter[0]?.[1]?.outputFolder,
runRoot: process.env.PLAYWRIGHT_RUN_ROOT,
workspacePath: apiServer.env.WORKSPACE_PATH,
};
}
describe("main Playwright harness isolation", () => {
test("binds exact backing state to the validated run and tears it down after API shutdown", async () => {
const config = await loadConfig({
PLAYWRIGHT_API_PORT: "18121",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18121",
PLAYWRIGHT_RUN_ID: "main_backing_contract",
PLAYWRIGHT_WEB_PORT: "28121",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28121",
});
const [apiServer] = config.webServer as Array<{
command: string;
env: Record<string, string>;
gracefulShutdown?: { signal: string; timeout: number };
}>;
const databaseName = new URL(apiServer.env.DATABASE_URL).pathname.slice(1);
expect(databaseName).toMatch(/^snapotter_e2e_main_[a-f0-9]{24}$/);
expect(apiServer.env.BULLMQ_PREFIX).toBe(databaseName);
expect(apiServer.command).toContain(
"node tests/playwright-api-lifecycle.mjs main_backing_contract main -- pnpm",
);
expect(apiServer.command).not.toContain("e2e-pg-create-db.cjs");
expect(apiServer.gracefulShutdown).toEqual({ signal: "SIGTERM", timeout: 30_000 });
});
test("propagates the exact isolated API and web endpoints through every process", async () => {
const apiUrl = "http://127.0.0.1:18123";
const webUrl = "http://127.0.0.1:28123";
const config = await loadConfig({
PLAYWRIGHT_API_PORT: "18123",
PLAYWRIGHT_API_URL: apiUrl,
PLAYWRIGHT_WEB_PORT: "28123",
PLAYWRIGHT_WEB_URL: webUrl,
});
expect(config.use?.baseURL).toBe(webUrl);
expect(process.env.API_URL).toBe(apiUrl);
const [apiServer, webServer] = config.webServer as Array<{
command: string;
env: Record<string, string>;
reuseExistingServer: boolean;
url?: string;
}>;
expect(apiServer.url).toBe(`${apiUrl}/api/v1/health`);
expect(apiServer.reuseExistingServer).toBe(false);
expect(apiServer.env.PORT).toBe("18123");
expect(apiServer.command).not.toContain("@snapotter/api dev");
expect(webServer.url).toBe(webUrl);
expect(webServer.reuseExistingServer).toBe(false);
expect(webServer.env.PORT).toBe("28123");
expect(webServer.env.VITE_API_URL).toBe(apiUrl);
expect(webServer.command).toContain("--port 28123");
expect(webServer.command).toContain("--strictPort");
});
test("scopes the web build output to the run so concurrent runs cannot empty it", async () => {
const config = await loadConfig({
PLAYWRIGHT_API_PORT: "18130",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18130",
PLAYWRIGHT_RUN_ID: "web_dist_contract",
PLAYWRIGHT_WEB_PORT: "28130",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28130",
});
const [, webServer] = config.webServer as Array<{ command: string }>;
const distDir = path.join(root, "test-results", "e2e-runs", "web_dist_contract", "web-dist");
// vite empties the output directory before writing it. Sharing the default
// apps/web/dist means a second run deletes the chunks the first run's
// preview server is still serving, and the first run then reports error
// boundaries that look like product defects.
expect(webServer.command).toContain(`vite build --outDir ${distDir} --emptyOutDir`);
expect(webServer.command).toContain(`vite preview --outDir ${distDir}`);
// pnpm does not forward extra args into a compound npm script, so routing
// the build through `pnpm --filter @snapotter/web build` would silently
// drop --outDir and write to the shared default again.
expect(webServer.command).not.toContain("@snapotter/web build");
});
test("generates distinct endpoint pairs when no endpoint override is supplied", async () => {
for (const key of isolationEnvKeys) delete process.env[key];
const first = await loadConfig({});
const firstApiUrl = process.env.API_URL;
const firstWebUrl = first.use?.baseURL;
for (const key of isolationEnvKeys) delete process.env[key];
const second = await loadConfig({});
expect(firstApiUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
expect(firstWebUrl).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
expect([process.env.API_URL, second.use?.baseURL]).not.toEqual([firstApiUrl, firstWebUrl]);
});
test("isolates every mutable path by a stable run identity", async () => {
const firstConfig = await loadConfig({
PLAYWRIGHT_API_PORT: "18125",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18125",
PLAYWRIGHT_RUN_ID: "run_a",
PLAYWRIGHT_WEB_PORT: "28125",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28125",
});
const first = mutablePaths(firstConfig);
const secondConfig = await loadConfig({
PLAYWRIGHT_API_PORT: "18126",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18126",
PLAYWRIGHT_RUN_ID: "run_b",
PLAYWRIGHT_WEB_PORT: "28126",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28126",
});
const second = mutablePaths(secondConfig);
for (const key of Object.keys(first) as Array<keyof typeof first>) {
expect(first[key], key).toContain("run_a");
expect(second[key], key).toContain("run_b");
expect(second[key], key).not.toBe(first[key]);
}
const reloaded = mutablePaths(
await loadConfig({
PLAYWRIGHT_API_PORT: "18126",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18126",
PLAYWRIGHT_RUN_ID: "run_b",
PLAYWRIGHT_WEB_PORT: "28126",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28126",
}),
);
expect(reloaded).toEqual(second);
});
test("rejects unsafe run identities", async () => {
await expect(
loadConfig({
PLAYWRIGHT_API_PORT: "18127",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18127",
PLAYWRIGHT_RUN_ID: "../shared",
PLAYWRIGHT_WEB_PORT: "28127",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28127",
}),
).rejects.toThrow(/PLAYWRIGHT_RUN_ID/);
});
test("rejects unsupported IPv6 loopback URLs", async () => {
await expect(
loadConfig({
PLAYWRIGHT_API_PORT: "18128",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18128",
PLAYWRIGHT_RUN_ID: "ipv6_test",
PLAYWRIGHT_WEB_PORT: "28128",
PLAYWRIGHT_WEB_URL: "http://[::1]:28128",
}),
).rejects.toThrow(/loopback host/);
});
test("matches only the canonical auth setup file", async () => {
const config = await loadConfig({
PLAYWRIGHT_API_PORT: "18124",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18124",
PLAYWRIGHT_WEB_PORT: "28124",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28124",
});
const setup = config.projects?.find((project) => project.name === "setup");
const match = setup?.testMatch as RegExp;
expect(match.test(path.join(root, "tests/e2e/auth.setup.ts"))).toBe(true);
expect(match.test(path.join(root, "tests/e2e/qa-auth.setup.ts"))).toBe(false);
expect(match.test(path.join(root, "tests/e2e/not-auth.setup.ts"))).toBe(false);
});
});
describe("E2E database bootstrap safety", () => {
test("rejects an unsafe target before attempting a database connection", () => {
expect(() =>
execFileSync(process.execPath, [createDbPath, "production"], {
cwd: root,
env: {
...process.env,
E2E_PG_BASE_URL: "postgres://snapotter:snapotter@127.0.0.1:1/postgres",
},
encoding: "utf8",
stdio: "pipe",
timeout: 10_000,
}),
).toThrow(/unsafe database name/i);
});
test("never enumerates or drops sibling E2E databases", () => {
const source = fs.readFileSync(createDbPath, "utf8");
expect(source).not.toContain("FROM pg_database");
expect(source).not.toContain("LIKE 'snapotter_e2e_%'");
expect(source).not.toContain("row.datname");
});
});
test("loggedInPage does not mutate global settings for every test", () => {
const source = fs.readFileSync(helpersPath, "utf8");
const fixture = source.slice(
source.indexOf("export const test = base.extend"),
source.indexOf("// isAiSidecarRunning"),
);
expect(fixture).toContain('await page.goto("/")');
expect(fixture).not.toContain("putSettings(");
expect(fixture).not.toContain("settings heal");
});
describe("E2E navigation targets", () => {
// Paths that are meant to land on the catch-all. Anything else that does not
// resolve is a spec asserting against a 404 page it never intended to load,
// which passes vacuously and reports coverage the suite does not have.
const INTENTIONAL_NOT_FOUND = new Set([
"/image/nonexistent-tool-abc123",
"/image/nonexistent-tool-xyz",
"/image/this-tool-does-not-exist",
"/some/deep/nested/invalid/path",
"/this-route-does-not-exist-404",
"/this-tool-does-not-exist-xyz",
"/tools/resize",
"/zzz-nonexistent-tool-xyz",
]);
function declaredStaticRoutes(): Set<string> {
const app = fs.readFileSync(path.join(root, "apps", "web", "src", "App.tsx"), "utf8");
const paths = [...app.matchAll(/\bpath="([^"]+)"/g)].map((match) => match[1]);
expect(paths, "App.tsx declared no routes; the parser is broken").not.toHaveLength(0);
return new Set(paths.filter((value) => value !== "*" && !value.includes(":")));
}
test("every literal e2e navigation resolves to a declared route", async () => {
const { TOOLS } = await import("@snapotter/shared");
const staticRoutes = declaredStaticRoutes();
const toolRoutes = new Set(TOOLS.map((tool) => tool.route));
const unresolved = new Map<string, string[]>();
for (const entry of fs.readdirSync(e2eDir)) {
if (!entry.endsWith(".ts")) continue;
const source = fs.readFileSync(path.join(e2eDir, entry), "utf8");
for (const match of source.matchAll(/\.goto\("(\/[^"]*)"/g)) {
const target = match[1].split("?")[0];
if (staticRoutes.has(target) || toolRoutes.has(target)) continue;
if (INTENTIONAL_NOT_FOUND.has(target)) continue;
// The a11y suite reaches the catch-all through a sentinel path.
if (target.startsWith("/__a11y-")) continue;
unresolved.set(target, [...(unresolved.get(target) ?? []), entry]);
}
}
expect(
Object.fromEntries(unresolved),
"these e2e navigations hit the catch-all route, so their assertions run against the 404 page",
).toEqual({});
});
test("catalog-generated specs navigate by section route, not by modality", () => {
const source = fs.readFileSync(path.join(e2eDir, "tools-all.spec.ts"), "utf8");
// A tool's modality is not its URL section: "document" splits into pdf and
// files, and "file" maps to files. Building a URL from modality silently
// sends every document and file tool to the catch-all.
expect(source).not.toMatch(/goto\(`\/\$\{tool\??\.?\.?modality/);
expect(source).not.toContain("tool?.modality");
expect(source).toContain("tool.route");
});
});
test("main E2E consumers use the resolved run endpoint and artifact root", () => {
const endpointOffenders: string[] = [];
const artifactOffenders: string[] = [];
for (const entry of fs.readdirSync(e2eDir)) {
if (!entry.endsWith(".ts")) continue;
const source = fs.readFileSync(path.join(e2eDir, entry), "utf8");
if (/=\s*["']http:\/\/(?:localhost|127\.0\.0\.1):13490["']/.test(source)) {
endpointOffenders.push(entry);
}
if (/path\.join\(\s*process\.cwd\(\),\s*["']test-results["']/.test(source)) {
artifactOffenders.push(entry);
}
}
expect.soft(endpointOffenders).toEqual([]);
expect.soft(artifactOffenders).toEqual([]);
const authSetup = fs.readFileSync(authSetupPath, "utf8");
expect.soft(authSetup).toContain("PLAYWRIGHT_AUTH_FILE");
expect.soft(authSetup).not.toContain(":13490");
expect.soft(authSetup).not.toContain(":2349");
});
test("no spec pins storageState to a path outside the run", () => {
// A literal auth path survives the run that wrote it. The token inside then
// belongs to a database that no longer exists, so the block runs signed out
// and drives the login page instead of the surface it claims to cover.
const offenders: string[] = [];
for (const entry of fs.readdirSync(e2eDir)) {
if (!entry.endsWith(".ts")) continue;
const source = fs.readFileSync(path.join(e2eDir, entry), "utf8");
for (const match of source.matchAll(/storageState:\s*(["'][^"']+["'])/g)) {
offenders.push(`${entry}: ${match[1]}`);
}
}
expect(
offenders,
"use the authFile export from playwright.config.ts, or an explicit empty state",
).toEqual([]);
});
test("auth setup proves the web endpoint proxies to the run-owned API", () => {
const authSetup = fs.readFileSync(authSetupPath, "utf8");
// A health probe only shows that some API answered. The session token minted
// through the web endpoint exists solely in the run-owned database, so
// replaying it against process.env.API_URL is the identity check.
expect(authSetup).toContain("/api/auth/session");
expect(authSetup).toMatch(/authorization: `Bearer \$\{proxiedToken\}`/);
expect(authSetup).toContain("Bearer not-a-real-session-token");
expect(authSetup).toMatch(/forged\.status\(\)\s*!==\s*401/);
// The preview server, not just the build, must take its proxy target from the
// resolved endpoint. Configuring only the build leaves preview.proxy on its
// hard-coded default, which is how an earlier sweep drove another instance.
const viteConfig = fs.readFileSync(path.join(root, "apps", "web", "vite.config.ts"), "utf8");
const preview = viteConfig.slice(
viteConfig.indexOf("preview: {"),
viteConfig.indexOf("build: {"),
);
expect(preview).toContain("process.env.VITE_API_URL");
});
test("canonical Docker commands collect the complete app and production release suites", () => {
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")) as {
scripts: Record<string, string>;
};
expect(packageJson.scripts["test:e2e:docker"]).toBe(
"playwright test --config playwright.analytics.config.ts",
);
expect(packageJson.scripts["test:e2e:production"]).toBe(
"playwright test --config tests/qa/playwright.qa.config.ts",
);
const dockerOutput = collectPlaywright(dockerPlaywrightConfigPath, [], {
BASE_URL: "http://127.0.0.1:1349",
CI: "1",
});
expect(dockerOutput).toContain("adjustment-tools.spec.ts");
expect(dockerOutput).toContain("analytics-no-data-leak.spec.ts");
expect(dockerOutput).toContain("pipeline-advanced.spec.ts");
expect(dockerOutput).toContain("Total: 1072 tests in 30 files");
const productionOutput = collectPlaywright(
path.join(root, "tests", "qa", "playwright.qa.config.ts"),
);
expect(productionOutput).toContain("crosscutting.qa.spec.ts");
expect(productionOutput).toContain("settings-extended.qa.spec.ts");
expect(productionOutput).toContain("Total: 397 tests in 9 files");
});
test("embedded lifecycle tests use exact labeled resources and dynamic loopback ports", () => {
const source = fs.readFileSync(embeddedModePath, "utf8");
expect(source).toContain("SNAPOTTER_TEST_RUN_ID");
expect(source).toContain("com.snapotter.e2e.run");
expect(source).toContain('"--label"');
expect(source).toContain('"127.0.0.1::1349"');
expect(source).toContain('["port", NAME, "1349/tcp"]');
expect(source).toContain("containerIsOwned");
expect(source).toContain("volumeIsOwned");
expect(source).not.toContain('const NAME = "so-embed-test"');
expect(source).not.toContain('const VOL = "so-embed-test-data"');
expect(source).not.toContain('|| "13492"');
const cleanup = source.slice(source.indexOf("const cleanup"), source.indexOf("async function"));
expect(cleanup).toContain("containerIsOwned");
expect(cleanup).toContain("volumeIsOwned");
expect(cleanup).toContain('["rm", "-f", NAME]');
expect(cleanup).toContain('["volume", "rm", VOL]');
expect(source).toContain('process.once("SIGINT"');
expect(source).toContain('process.once("SIGTERM"');
expect(source).toContain('process.once("exit", cleanup)');
expect(source).toContain(".finally(cleanup)");
expect(source).toContain("process.exitCode = exitCode");
expect(source).toContain("const restartEmbeddedContainer = () =>");
expect(source.match(/restartEmbeddedContainer\(\);/g)).toHaveLength(2);
});
describe("Docker analytics Playwright isolation", () => {
test("isolates auth state, output, and reports under the validated run identity", async () => {
const firstConfig = await loadDockerConfig({
BASE_URL: "http://127.0.0.1:18491",
PLAYWRIGHT_RUN_ID: "docker_run_a",
});
const firstChromium = firstConfig.projects?.find((project) => project.name === "chromium");
const firstReporter = firstConfig.reporter as Array<[string, { outputFolder?: string }]>;
const first = {
authFile: firstChromium?.use?.storageState,
envAuthFile: process.env.PLAYWRIGHT_AUTH_FILE,
outputDir: firstConfig.outputDir,
reportDir: firstReporter[0]?.[1]?.outputFolder,
runRoot: process.env.PLAYWRIGHT_RUN_ROOT,
};
const secondConfig = await loadDockerConfig({
BASE_URL: "http://127.0.0.1:18492",
PLAYWRIGHT_RUN_ID: "docker_run_b",
});
const secondChromium = secondConfig.projects?.find((project) => project.name === "chromium");
const secondReporter = secondConfig.reporter as Array<[string, { outputFolder?: string }]>;
const second = {
authFile: secondChromium?.use?.storageState,
envAuthFile: process.env.PLAYWRIGHT_AUTH_FILE,
outputDir: secondConfig.outputDir,
reportDir: secondReporter[0]?.[1]?.outputFolder,
runRoot: process.env.PLAYWRIGHT_RUN_ROOT,
};
for (const key of Object.keys(first) as Array<keyof typeof first>) {
expect(first[key], key).toContain("docker_run_a");
expect(second[key], key).toContain("docker_run_b");
expect(second[key], key).not.toBe(first[key]);
}
expect(first.authFile).toBe(first.envAuthFile);
expect(second.authFile).toBe(second.envAuthFile);
expect(firstConfig.use?.baseURL).toBe("http://127.0.0.1:18491");
expect(secondConfig.use?.baseURL).toBe("http://127.0.0.1:18492");
expect(process.env.API_URL).toBe("http://127.0.0.1:18492");
});
test("rejects unsafe run identities and keeps consumers on resolved paths", async () => {
await expect(
loadDockerConfig({
BASE_URL: "http://127.0.0.1:18493",
PLAYWRIGHT_RUN_ID: "../shared",
}),
).rejects.toThrow(/PLAYWRIGHT_RUN_ID/);
const authSetup = fs.readFileSync(dockerAuthSetupPath, "utf8");
const analyticsApi = fs.readFileSync(dockerAnalyticsApiPath, "utf8");
expect(authSetup).toContain("PLAYWRIGHT_AUTH_FILE");
expect(authSetup).not.toContain(".playwright");
expect(analyticsApi).toContain("process.env.API_URL");
expect(analyticsApi).not.toContain('const BASE_URL = "http://localhost:1349"');
});
});
describe("local analytics Playwright isolation", () => {
test("binds exact backing state to the validated run and tears it down after API shutdown", async () => {
const config = await loadAnalyticsLocalConfig({
PLAYWRIGHT_API_PORT: "18490",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18490",
PLAYWRIGHT_RUN_ID: "analytics_backing_contract",
PLAYWRIGHT_WEB_PORT: "28490",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28490",
});
const [apiServer] = config.webServer as Array<{
command: string;
env: Record<string, string>;
gracefulShutdown?: { signal: string; timeout: number };
}>;
const databaseName = new URL(apiServer.env.DATABASE_URL).pathname.slice(1);
expect(databaseName).toMatch(/^snapotter_e2e_analytics_local_[a-f0-9]{24}$/);
expect(apiServer.env.BULLMQ_PREFIX).toBe(databaseName);
expect(apiServer.command).toContain(
"node tests/playwright-api-lifecycle.mjs analytics_backing_contract analytics-local -- pnpm",
);
expect(apiServer.command).not.toContain("e2e-pg-create-db.cjs");
expect(apiServer.gracefulShutdown).toEqual({ signal: "SIGTERM", timeout: 30_000 });
});
test("uses exact run-scoped endpoints and artifacts without reusing servers", async () => {
const config = await loadAnalyticsLocalConfig({
PLAYWRIGHT_API_PORT: "18494",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18494",
PLAYWRIGHT_RUN_ID: "analytics_local_a",
PLAYWRIGHT_WEB_PORT: "28494",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28494",
});
const [apiServer, webServer] = config.webServer as Array<{
env: Record<string, string>;
reuseExistingServer: boolean;
url?: string;
}>;
const chromium = config.projects?.find((project) => project.name === "chromium");
const reporter = config.reporter as Array<[string, { outputFolder?: string }]>;
for (const value of [
chromium?.use?.storageState,
process.env.PLAYWRIGHT_AUTH_FILE,
process.env.PLAYWRIGHT_RUN_ROOT,
config.outputDir,
reporter[0]?.[1]?.outputFolder,
]) {
expect(value).toContain("analytics_local_a");
}
expect(chromium?.use?.storageState).toBe(process.env.PLAYWRIGHT_AUTH_FILE);
expect(config.use?.baseURL).toBe("http://127.0.0.1:28494");
expect(process.env.API_URL).toBe("http://127.0.0.1:18494");
expect(apiServer.url).toBe("http://127.0.0.1:18494/api/v1/health");
expect(apiServer.env.PORT).toBe("18494");
expect(apiServer.reuseExistingServer).toBe(false);
expect(webServer.url).toBe("http://127.0.0.1:28494");
expect(webServer.env.PORT).toBe("28494");
expect(webServer.env.VITE_API_URL).toBe("http://127.0.0.1:18494");
expect(webServer.reuseExistingServer).toBe(false);
});
test("generates unique endpoints, rejects unsafe IDs, and keeps auth setup run-scoped", async () => {
const first = await loadAnalyticsLocalConfig({ PLAYWRIGHT_RUN_ID: "analytics_auto_a" });
const firstEndpoints = [process.env.API_URL, first.use?.baseURL];
const second = await loadAnalyticsLocalConfig({ PLAYWRIGHT_RUN_ID: "analytics_auto_b" });
const secondEndpoints = [process.env.API_URL, second.use?.baseURL];
expect(firstEndpoints[0]).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
expect(firstEndpoints[1]).toMatch(/^http:\/\/127\.0\.0\.1:\d+$/);
expect(secondEndpoints).not.toEqual(firstEndpoints);
await expect(loadAnalyticsLocalConfig({ PLAYWRIGHT_RUN_ID: "../shared" })).rejects.toThrow(
/PLAYWRIGHT_RUN_ID/,
);
const configSource = fs.readFileSync(analyticsLocalConfigPath, "utf8");
const authSetup = fs.readFileSync(analyticsLocalAuthSetupPath, "utf8");
expect(configSource).not.toMatch(/(?:TEST_API_PORT\s*=\s*13491|TEST_WEB_PORT\s*=\s*2350)/);
expect(configSource).not.toContain("reuseExistingServer: !process.env.CI");
expect(authSetup).toContain("PLAYWRIGHT_AUTH_FILE");
expect(authSetup).not.toContain('"test-results", ".auth"');
});
});
test("the canonical E2E command exactly covers every release browser and device project", async () => {
const packageJson = JSON.parse(fs.readFileSync(packagePath, "utf8")) as {
scripts: Record<string, string>;
};
expect(packageJson.scripts["test:e2e"]).toBe("node scripts/run-main-e2e.mjs");
expect(packageJson.scripts["test:all"]).toContain("pnpm test:e2e");
const darwinPlan = JSON.parse(
execFileSync(process.execPath, [e2eRunnerPath, "--plan", "darwin"], { encoding: "utf8" }),
) as string[][];
const linuxPlan = JSON.parse(
execFileSync(process.execPath, [e2eRunnerPath, "--plan", "linux"], { encoding: "utf8" }),
) as string[][];
const selectedProjects = darwinPlan
.flat()
.flatMap((arg) => (arg.startsWith("--project=") ? [arg.slice("--project=".length)] : []));
const config = await loadConfig({
PLAYWRIGHT_API_PORT: "18129",
PLAYWRIGHT_API_URL: "http://127.0.0.1:18129",
PLAYWRIGHT_RUN_ID: "project_contract",
PLAYWRIGHT_WEB_PORT: "28129",
PLAYWRIGHT_WEB_URL: "http://127.0.0.1:28129",
});
const configuredProjects = config.projects
?.map((project) => project.name)
.filter((name) => name !== "setup");
expect(packageJson.scripts["test:e2e:core"]).toBeTruthy();
expect(packageJson.scripts["test:e2e:visual"]).toBe(
"playwright test --project=chromium-visual --project=chromium-legacy-visual",
);
expect(new Set(selectedProjects)).toEqual(new Set(configuredProjects));
expect(selectedProjects).toHaveLength(configuredProjects?.length ?? 0);
expect(linuxPlan.flat()).not.toContain("--project=chromium-visual");
expect(darwinPlan.flat()).toContain("--project=chromium-legacy-visual");
expect(linuxPlan[0]).toContain("--grep-invert=@visual");
expect(linuxPlan[1]).toEqual(["test", "--project=chromium-serial", "--workers=1"]);
});
test("Vitest excludes every Playwright spec directory", () => {
const config = fs.readFileSync(vitestConfigPath, "utf8");
const playwrightDirs = fs
.readdirSync(path.join(root, "tests"), { withFileTypes: true })
.filter((entry) => entry.isDirectory() && /^e2e(-|$)/.test(entry.name))
.map((entry) => entry.name);
// Reading the directory rather than listing names keeps this honest: adding
// a new browser suite fails here until vitest.config.ts learns to skip it.
// Without the exclusion a bare `vitest run` imports @playwright/test and the
// whole unit lane dies on a spec it was never meant to collect.
expect(playwrightDirs.length).toBeGreaterThan(0);
for (const dir of playwrightDirs) {
expect(config, `vitest.config.ts must exclude tests/${dir}`).toContain(`"tests/${dir}/**"`);
}
});
test("Firefox and WebKit collect the supported cross-browser core suite", () => {
for (const project of ["firefox", "webkit"]) {
const output = collectPlaywright(path.join(root, "playwright.config.ts"), [
`--project=${project}`,
]);
expect(output, project).toContain("gui-cross-browser.spec.ts");
expect(output, project).toContain("smoke.spec.ts");
expect(output, project).toContain("navigation.spec.ts");
expect(output, project).toContain("home-page.spec.ts");
expect(output, project).toContain("Total: 57 tests in 5 files");
}
});
test("the dedicated width project owns every required browser width", () => {
const output = collectPlaywright(path.join(root, "playwright.config.ts"), [
"--project=chromium-widths",
]);
for (const width of [320, 768, 1024, 1536, 2560]) {
expect(output).toContain(`${width}px viewport has no horizontal overflow`);
}
expect(output).toContain("Total: 6 tests in 2 files");
});
test("legacy visual coverage is runnable only through its dedicated collected project", () => {
const output = execFileSync(
"pnpm",
[
"exec",
"playwright",
"test",
"tests/e2e/visual-regression.spec.ts",
"--config",
path.join(root, "playwright.config.ts"),
"--project=chromium-legacy-visual",
"--list",
"--reporter=json",
],
{
cwd: root,
encoding: "utf8",
env: { ...process.env, PLAYWRIGHT_RUN_ID: "legacy_visual_contract" },
maxBuffer: 20 * 1024 * 1024,
stdio: "pipe",
},
);
const report = JSON.parse(output) as { suites: unknown[] };
const projectTests: Array<{ annotations?: Array<{ type?: string }>; projectName?: string }> = [];
const visit = (value: unknown): void => {
if (Array.isArray(value)) {
for (const entry of value) visit(entry);
return;
}
if (!value || typeof value !== "object") return;
const record = value as Record<string, unknown>;
if (record.projectName === "chromium-legacy-visual") {
projectTests.push(record as { annotations?: Array<{ type?: string }>; projectName?: string });
}
for (const entry of Object.values(record)) visit(entry);
};
visit(report.suites);
expect(projectTests).toHaveLength(14);
expect(
projectTests.flatMap((entry) => entry.annotations ?? []).some((entry) => entry.type === "skip"),
).toBe(false);
});
test("Turbo passes every main harness endpoint override through task boundaries", () => {
const turbo = JSON.parse(fs.readFileSync(turboPath, "utf8")) as {
globalPassThroughEnv: string[];
};
expect(turbo.globalPassThroughEnv).toEqual(
expect.arrayContaining([
"API_URL",
"E2E_PG_BASE_URL",
"PLAYWRIGHT_API_PORT",
"PLAYWRIGHT_API_URL",
"PLAYWRIGHT_AUTH_FILE",
"PLAYWRIGHT_RUN_ID",
"PLAYWRIGHT_RUN_ROOT",
"PLAYWRIGHT_WEB_PORT",
"PLAYWRIGHT_WEB_URL",
"PW_WORKERS",
"REDIS_URL",
]),
);
});