mirror of
https://github.com/webstudio-is/webstudio.git
synced 2026-10-30 08:37:56 +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
63 lines
1.8 KiB
TypeScript
63 lines
1.8 KiB
TypeScript
const e2eFileSuffix = ".e2e.ts";
|
|
const shardTagPattern = /\[(shard-\d+)\]/g;
|
|
const shardNamePattern = /^shard-\d+$/;
|
|
|
|
export const getE2eFileShards = (fileName: string) => {
|
|
const shards = [...fileName.matchAll(shardTagPattern)].map(
|
|
(match) => match[1]
|
|
);
|
|
if (shards.length === 0) {
|
|
throw new Error(`Every e2e file must have a shard tag: ${fileName}`);
|
|
}
|
|
if (new Set(shards).size !== shards.length) {
|
|
throw new Error(
|
|
`Every shard tag in an e2e filename must be unique: ${fileName}`
|
|
);
|
|
}
|
|
return shards;
|
|
};
|
|
|
|
export const getE2eShardMatrix = (fileNames: readonly string[]) => {
|
|
const shardGroups = new Map<string, Set<string>>();
|
|
for (const fileName of fileNames) {
|
|
if (fileName.endsWith(e2eFileSuffix) === false) {
|
|
continue;
|
|
}
|
|
const shards = getE2eFileShards(fileName);
|
|
const group = shards.join(",");
|
|
for (const shard of shards) {
|
|
const groups = shardGroups.get(shard) ?? new Set<string>();
|
|
groups.add(group);
|
|
shardGroups.set(shard, groups);
|
|
}
|
|
}
|
|
|
|
return [...shardGroups]
|
|
.sort(([left], [right]) =>
|
|
left.localeCompare(right, undefined, { numeric: true })
|
|
)
|
|
.map(([shard, groups]) => {
|
|
if (groups.size !== 1) {
|
|
throw new Error(
|
|
`Every file selected by ${shard} must use the same shard tags`
|
|
);
|
|
}
|
|
const shards = [...groups][0].split(",");
|
|
const partition =
|
|
shards.length === 1
|
|
? ""
|
|
: `${shards.indexOf(shard) + 1}/${shards.length}`;
|
|
return { shard, partition };
|
|
});
|
|
};
|
|
|
|
export const getE2eTestMatch = (shard: string | undefined) => {
|
|
if (shard === undefined || shard === "") {
|
|
return /tests\/.*\.e2e\.ts/;
|
|
}
|
|
if (shardNamePattern.test(shard) === false) {
|
|
throw new Error(`Invalid e2e shard: ${shard}`);
|
|
}
|
|
return new RegExp(`tests/.*\\[${shard}\\].*\\.e2e\\.ts$`);
|
|
};
|