mirror of
https://github.com/nexu-io/open-design.git
synced 2026-10-27 09:51:12 +01:00
* fix(web): keep the project split still across the Home hand-off The optimistic creation frame left `.split` on its stylesheet default (460px) while ProjectView resolved the saved width, or the equal split of the container, only after it mounted — and that first write ran through the 200ms width transition, so the chat column moved once the real view arrived (OPEND-3207). Move the split geometry into `project-split-layout.ts`, have both surfaces resolve their first-paint width through `resolveProjectSplitLayout`, and settle a surface's first measured write without the transition. Red spec: e2e/ui/home-send-split-width.test.ts (red on main, green here). * test(web): read the 4px handle constant from project-split-layout The material spec pinned `SPLIT_RESIZE_HANDLE_WIDTH = 4` in ProjectView's source; the constant now lives in project-split-layout.ts and ProjectView imports it, so assert on the module and on the import. * test(cms): wait for the Test modal impression before switching language The locale case only passed while the impression frame never fired: on a fast machine the next language switch unmounted the modal before its requestAnimationFrame recorded the impression. CI is slow enough for the frame to land, and the switch that follows then closes the modal for good. Waiting for the recorded impression makes that order deterministic. * fix(cms): keep the open Test modal across a same-deployment reload A language switch (or lease renewal) reloads the selected Test deployment. While the new decisions are in flight the runtime publishes a session without decisions, and the modal treated that gap as the presentation ending: it dropped its "open" marker, then the returning decision looked like a new offer of an activity this account had already seen and stayed closed. Key the open presentation by the selected deployment and snapshot. A reload of the same selection continues it; a redeploy, a different account, a dismissal, or the session ending still releases it. * test(e2e): enroll the Home hand-off specs in the ui_p0 entry-settings group home-amr-pending, home-send-single-loading and home-send-split-width were merged (or opened) without being enrolled in uiP0Groups, so no merge lane had ever executed them; only the prerelease full pool would have. Enroll them with the other Home-entry files.
195 lines
6.5 KiB
TypeScript
195 lines
6.5 KiB
TypeScript
export type UiPlaywrightGroup = {
|
|
files: readonly string[];
|
|
fullyParallel?: boolean;
|
|
grep: string;
|
|
workers?: number;
|
|
};
|
|
|
|
export type UiP0CiMatrixEntry = {
|
|
name: string;
|
|
shard: string;
|
|
};
|
|
|
|
export type VisualCiMatrixEntry = {
|
|
files: string;
|
|
name: string;
|
|
};
|
|
|
|
export const uiP0Groups = {
|
|
"critical-extras": {
|
|
grep: "@merge-extra",
|
|
workers: 1,
|
|
files: ["ui/app.test.ts"],
|
|
},
|
|
"workspace-restoration": {
|
|
fullyParallel: true,
|
|
grep: String.raw`\[P0\]`,
|
|
files: ["ui/app-restoration.test.ts", "ui/critical-smoke.test.ts"],
|
|
},
|
|
"entry-settings": {
|
|
grep: String.raw`\[P0\]`,
|
|
files: [
|
|
"ui/entry-chrome-flows.test.ts",
|
|
"ui/entry-configuration-flows.test.ts",
|
|
"ui/home-hero-rail.test.ts",
|
|
// Enrolled 2026-09-18: the Home → project hand-off specs (OPEND-2614 /
|
|
// 2170 / 3207) pin what only a running browser can see — the optimistic
|
|
// frame's timing, the balance dialog's placement and the split width.
|
|
// They were merged without enrollment, so no merge lane had executed them.
|
|
"ui/home-amr-pending.test.ts",
|
|
"ui/home-send-single-loading.test.ts",
|
|
"ui/home-send-split-width.test.ts",
|
|
"ui/amr-onboarding.test.ts",
|
|
"ui/api-empty-response.test.ts",
|
|
"ui/settings-api-protocol.test.ts",
|
|
"ui/settings-connectors-auth-happy-path.test.ts",
|
|
"ui/settings-connectors-auth-recovery.test.ts",
|
|
"ui/workspace-team-interactions.test.ts",
|
|
],
|
|
},
|
|
"project-workspace": {
|
|
grep: String.raw`\[P0\]`,
|
|
workers: 1,
|
|
files: [
|
|
"ui/app.test.ts",
|
|
"ui/fork-note-ellipsis.test.ts",
|
|
"ui/project-management-flows.test.ts",
|
|
"ui/workspace-keyboard-flows.test.ts",
|
|
],
|
|
},
|
|
// Keep editor-heavy files on a separate single-worker runtime. Running the
|
|
// whole workspace domain serially took 11.6 minutes on CI, while enabling a
|
|
// second worker in one job is unsafe because these flows share Workspace
|
|
// authority state outside the worker-local daemon. Two runner-isolated jobs
|
|
// preserve that boundary and balance the historical file timings.
|
|
"project-workspace-editor": {
|
|
grep: String.raw`\[P0\]`,
|
|
workers: 1,
|
|
files: [
|
|
"ui/app-design-files.test.ts",
|
|
"ui/app-manual-edit.test.ts",
|
|
"ui/workspace-team-design-system-picker.test.ts",
|
|
],
|
|
},
|
|
// Split out of "project-workspace" (2026-08-04): the two multi-client collab
|
|
// specs alone accounted for ~10 of that group's ~26min single-worker wall
|
|
// time (workspace-multi-client-collab.test.ts spins up two isolated
|
|
// client/daemon runtimes per case). Keep this shard limited to the cluster-
|
|
// owned spec so it does not also boot the default worker runtime needed by
|
|
// ordinary UI files.
|
|
"project-collab": {
|
|
grep: String.raw`\[P0\]`,
|
|
workers: 1,
|
|
files: ["ui/workspace-multi-client-collab.test.ts"],
|
|
},
|
|
"project-runtime": {
|
|
grep: String.raw`\[P0\]`,
|
|
workers: 1,
|
|
files: [
|
|
"ui/real-daemon-run.test.ts",
|
|
"ui/amr-run-failure-recovery.test.ts",
|
|
"ui/amr-logout-requires-relogin.test.ts",
|
|
"ui/settings-local-cli-codex-fallback.test.ts",
|
|
],
|
|
},
|
|
} as const satisfies Record<string, UiPlaywrightGroup>;
|
|
|
|
export type UiP0GroupName = keyof typeof uiP0Groups;
|
|
|
|
export const uiP0CiMatrix = [
|
|
{ name: "entry-settings", shard: "entry-settings" },
|
|
{ name: "project-workspace", shard: "project-workspace" },
|
|
{ name: "project-workspace-editor", shard: "project-workspace-editor" },
|
|
{ name: "project-collab", shard: "project-collab" },
|
|
{ name: "project-runtime", shard: "project-runtime" },
|
|
{ name: "workspace-restoration", shard: "workspace-restoration" },
|
|
] as const satisfies readonly UiP0CiMatrixEntry[];
|
|
|
|
export const visualCiMatrix = [
|
|
{ name: "entry-navigation", files: "ui/visual-entry.test.ts ui/visual-navigation.test.ts" },
|
|
{ name: "settings-workspace", files: "ui/visual-settings.test.ts ui/visual-workspace.test.ts" },
|
|
] as const satisfies readonly VisualCiMatrixEntry[];
|
|
|
|
const uiP0CoverageFiles = [
|
|
"ui/amr-logout-requires-relogin.test.ts",
|
|
"ui/amr-onboarding.test.ts",
|
|
"ui/amr-run-failure-recovery.test.ts",
|
|
"ui/api-empty-response.test.ts",
|
|
"ui/app-design-files.test.ts",
|
|
"ui/app-manual-edit.test.ts",
|
|
"ui/app-restoration.test.ts",
|
|
"ui/app.test.ts",
|
|
"ui/critical-smoke.test.ts",
|
|
"ui/entry-chrome-flows.test.ts",
|
|
"ui/entry-configuration-flows.test.ts",
|
|
"ui/fork-note-ellipsis.test.ts",
|
|
"ui/home-hero-rail.test.ts",
|
|
"ui/project-management-flows.test.ts",
|
|
"ui/real-daemon-run.test.ts",
|
|
"ui/settings-api-protocol.test.ts",
|
|
"ui/settings-connectors-auth-happy-path.test.ts",
|
|
"ui/settings-connectors-auth-recovery.test.ts",
|
|
"ui/settings-local-cli-codex-fallback.test.ts",
|
|
"ui/workspace-team-interactions.test.ts",
|
|
"ui/workspace-multi-client-collab.test.ts",
|
|
"ui/workspace-team-design-system-picker.test.ts",
|
|
"ui/workspace-keyboard-flows.test.ts",
|
|
] as const;
|
|
|
|
export function getUiP0Group(name: string): UiPlaywrightGroup | undefined {
|
|
return uiP0Groups[name as UiP0GroupName];
|
|
}
|
|
|
|
export function listUiP0GroupNames(): string[] {
|
|
return Object.keys(uiP0Groups).sort();
|
|
}
|
|
|
|
export function validatePlaywrightSuiteTopology(): string[] {
|
|
const errors: string[] = [];
|
|
const knownGroups = new Set(Object.keys(uiP0Groups));
|
|
const coverageFiles = sortedUnique(uiP0CoverageFiles);
|
|
const ciFileAssignments = uiP0CiMatrix.flatMap(
|
|
(entry) => uiP0Groups[entry.shard as UiP0GroupName]?.files ?? [],
|
|
);
|
|
const ciFiles = sortedUnique(ciFileAssignments);
|
|
|
|
for (const entry of uiP0CiMatrix) {
|
|
if (!knownGroups.has(entry.shard)) {
|
|
errors.push(`UI P0 CI matrix references unknown group ${entry.shard}`);
|
|
}
|
|
}
|
|
|
|
for (const file of difference(coverageFiles, ciFiles)) {
|
|
errors.push(`UI P0 CI matrix does not cover ${file}`);
|
|
}
|
|
|
|
for (const file of difference(ciFiles, coverageFiles)) {
|
|
errors.push(`UI P0 CI matrix unexpectedly covers ${file}`);
|
|
}
|
|
|
|
const seenFiles = new Set<string>();
|
|
for (const file of ciFileAssignments) {
|
|
if (seenFiles.has(file)) {
|
|
errors.push(`UI P0 CI matrix covers ${file} more than once`);
|
|
}
|
|
seenFiles.add(file);
|
|
}
|
|
|
|
for (const entry of visualCiMatrix) {
|
|
if (entry.files.trim().length === 0) {
|
|
errors.push(`Visual CI matrix entry ${entry.name} has no files`);
|
|
}
|
|
}
|
|
|
|
return errors;
|
|
}
|
|
|
|
function difference(left: readonly string[], right: readonly string[]): string[] {
|
|
const rightSet = new Set(right);
|
|
return left.filter((value) => !rightSet.has(value));
|
|
}
|
|
|
|
function sortedUnique(values: readonly string[]): string[] {
|
|
return [...new Set(values)].sort();
|
|
}
|