mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-28 07:37:36 +09:00
## 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
95 lines
2.4 KiB
TypeScript
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;
|
|
};
|