Files
Oleg Isonen 8718feb465 test: simplify browser testing infrastructure (#6271)
## Summary

Unifies frontend testing around three intentional boundaries:

- Vitest Node projects for pure and server tests
- Vitest Browser Mode with Playwright/Chromium for DOM and component
tests
- Playwright Test for Builder end-to-end workflows

The Builder no longer uses jsdom. Ordinary E2E workflows use an
automatically managed isolated `page` fixture; explicit contexts remain
only for simultaneous identities and suite-scoped setup.

## Technical choices

- Split Builder tests into one Node project and independent browser
projects so each Chromium/Vite process has a bounded workload.
- Added shared Browser Mode configuration for the affected SDK React
packages.
- Preserve the Builder's production top-window bridge ownership; Browser
Mode uses explicit test-only installation seams.
- Centralized Playwright context options and kept action/navigation
timeouts bounded.
- Validate Playwright filters before Docker, migrations, browser
installation, or Builder builds start.
- Preserve the first failed E2E attempt's report, traces, screenshots,
and videos when CI retries a shard.
- Split static checks, typechecks, CLI tests, core package tests,
browser package tests, and Builder projects into parallel CI jobs with
five-minute hard ceilings.
- Distribute Builder E2E tests across ten isolated shards with two
workers; parallel suites create unique per-worker setup data.
- Reuse color contrast DOM/canvas samples so the exhaustive theme sweep
takes about four seconds instead of timing out at 30 seconds.
- The removed cost is jsdom behavior drift, oversized serial check jobs,
and unnecessary E2E startup for empty selections. Real browser startup
and E2E environment setup remain.

## Todo

- [x] Split Builder pure/server tests from browser/component tests.
- [x] Migrate Builder DOM tests from jsdom to Vitest Browser Mode.
- [x] Migrate affected SDK React DOM tests to the shared Browser Mode
setup.
- [x] Remove jsdom annotations, dependencies, and compatibility setup.
- [x] Replace ordinary manual E2E contexts with the isolated `page`
fixture.
- [x] Keep explicit contexts only for concurrent identities and
`beforeAll` setup.
- [x] Preserve setup authentication isolation from workflow pages.
- [x] Add bounded Playwright action and navigation timeouts.
- [x] Preserve first-attempt E2E failure evidence across CI retry.
- [x] Reject empty E2E selections before expensive environment setup.
- [x] Parallelize repository checks into bounded CI shards.
- [x] Isolate parallel E2E setup data across workers.
- [x] Remove the color contrast sweep bottleneck.
- [x] Update E2E contributor documentation.
- [x] Review product documentation impact; no user-facing product docs
are affected.
- [x] Run the full repository tests and typechecks.
- [x] Run formatting, lint, package-boundary, generated-file, and
fixture checks.
- [x] Complete final correctness and simplification review.

## Verification

- `pnpm --filter @webstudio-is/builder test`
- `pnpm -r typecheck`
- `pnpm lint`
- `pnpm check:package-boundaries`
- `pnpm check:generated-api`
- `pnpm check:generated-docs`
- `pnpm fixtures`
- Design-system: 222 tests pass locally.
- Builder: all seven Vitest projects pass locally.
- The formerly slow E2E shard passes locally in 1m55s.
- Full database-backed Builder E2E execution passes in CI.
- All CI jobs pass; the slowest completed in 4m58s.
- No fixture inputs/outputs or generated files changed.

Ref #6270
2026-08-31 17:17:28 +02:00

95 lines
2.4 KiB
TypeScript

import { parseBuilderUrl } from "@webstudio-is/protocol";
import {
test as base,
type Browser,
type BrowserContextOptions,
} from "@playwright/test";
import env from "../app/env/env.server";
const builderPort = process.env.PORT ?? "3000";
export const builderUrl =
process.env.E2E_BUILDER_URL ?? `https://127.0.0.1:${builderPort}`;
const builderUrlObject = new URL(builderUrl);
export const dashboardUrl = `${builderUrlObject.protocol}//wstd.dev:${builderUrlObject.port}`;
export const postgrestUrl = env.POSTGREST_URL;
export const browserContextOptions: BrowserContextOptions = {
ignoreHTTPSErrors: true,
permissions: ["clipboard-read", "clipboard-write"],
};
const isLoopbackHost = (hostname: string) =>
hostname === "127.0.0.1" || hostname === "localhost" || hostname === "::1";
export const getBrowserLaunchOptions = (url = builderUrl) => {
const hostname = new URL(url).hostname;
if (isLoopbackHost(hostname) === false) {
return {};
}
return {
args: [
"--host-resolver-rules=MAP wstd.dev 127.0.0.1,MAP *.wstd.dev 127.0.0.1",
],
};
};
export const newBrowserContext = (browser: Browser) =>
browser.newContext(browserContextOptions);
export const withBrowserContext = async <Result>(
browser: Browser,
run: (
context: Awaited<ReturnType<typeof newBrowserContext>>
) => Promise<Result> | Result
) => {
const context = await newBrowserContext(browser);
try {
return await run(context);
} finally {
await context.close();
}
};
export const test = base;
export const newIsolatedPage = async (browser: Browser) => {
const context = await newBrowserContext(browser);
return {
page: await context.newPage(),
close: () => context.close(),
};
};
export const getProjectBuilderUrl = ({
projectId,
authToken,
mode,
features,
}: {
projectId: string;
authToken?: string;
mode?: "content" | "preview";
features?: string[];
}) => {
const url = new URL(dashboardUrl);
url.hostname = `p-${projectId}.wstd.dev`;
if (authToken !== undefined) {
url.searchParams.set("authToken", authToken);
}
if (mode !== undefined) {
url.searchParams.set("mode", mode);
}
if (features !== undefined && features.length > 0) {
url.searchParams.set("features", features.join(","));
}
return url.href;
};
export const getProjectIdFromBuilderUrl = (url: string) => {
return parseBuilderUrl(url).projectId;
};