feat: consolidate shared UI and harden browser authority for release

This commit is contained in:
2026-09-08 01:35:05 +02:00
parent ac40774785
commit b75ca34295
143 changed files with 8664 additions and 752 deletions
@@ -0,0 +1,145 @@
import { expect, test, type Page } from "@playwright/test";
async function install(page: Page, options: { rejectSave?: boolean; rejectReload?: boolean; conflict?: boolean; hold?: Promise<void>; holdFirstRead?: Promise<void> } = {}) {
page.on("pageerror", (error) => { console.error("Campaign saving fixture:", error.message); });
let revision = 1;
let subject = "Original subject";
let rejected = false;
const writes: Record<string, unknown>[] = [];
let reads = 0;
const version = () => ({ id: "version-a", campaign_id: "campaign-a", version_number: 1,
edit_revision: revision, strong_etag: `"version-a:${revision}"`, editor_state: {},
current_flow: "manual", current_step: "template", workflow_state: "editing", is_complete: false,
updated_at: "2026-09-07T10:00:00Z", raw_json: { campaign: { name: "Original" }, template: { subject, text: "" }, server: {} } });
await page.route((url) => url.pathname.startsWith("/api/"), async (route) => {
if (route.request().method() === "POST" && route.request().url().endsWith("/autosave")) {
const body = route.request().postDataJSON();
writes.push(body);
if (options.hold) await options.hold;
if (options.rejectSave && !rejected) {
rejected = true;
return route.fulfill({ status: 422, json: { detail: "Fixture policy denied this save" } });
}
if ((options.conflict && !rejected) || body.base_revision !== revision) {
rejected = true;
if (options.conflict) { revision = 2; subject = "Other editor's subject"; }
return route.fulfill({ status: 409, json: { detail: { code: "revision_conflict", resource: { type: "campaign_version", id: "version-a" }, current_revision: revision, submitted_base_revision: body.base_revision, retryable: true } } });
}
subject = body.campaign_json.template.subject;
revision += 1;
return route.fulfill({ json: version() });
}
if (route.request().method() === "GET" && route.request().url().includes("/versions/version-a")) {
reads++;
const snapshot = version();
if (reads === 1 && options.holdFirstRead) await options.holdFirstRead;
return options.rejectReload ? route.fulfill({ status: 503, json: { detail: "Fixture refresh unavailable" } }) : route.fulfill({ json: snapshot });
}
return route.abort();
});
await page.goto("/?campaign-saving&language=en");
await expect(page.getByRole("textbox", { name: "Subject", exact: true })).toHaveValue("Original subject");
return { writes, reads: () => reads, subject: () => subject };
}
test("a rejected campaign save retains its draft and retries only on explicit request", async ({ page }) => {
const fixture = await install(page, { rejectSave: true });
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("Unsaved work");
await page.getByRole("button", { name: "Save draft", exact: true }).click();
await expect(page.getByTestId("save-error")).toContainText("Fixture policy denied");
await expect(page.getByTestId("save-dirty")).toHaveText("true");
await expect(page.getByRole("textbox", { name: "Subject", exact: true })).toHaveValue("Unsaved work");
expect(fixture.writes).toHaveLength(1);
await page.getByRole("button", { name: "Save draft", exact: true }).click();
await expect(page.getByTestId("save-result")).toHaveText("true");
await expect(page.getByTestId("save-dirty")).toHaveText("false");
expect(fixture.writes).toHaveLength(2);
expect(fixture.subject()).toBe("Unsaved work");
});
test("duplicate save requests coalesce and newer typing survives the older acknowledgement", async ({ page }) => {
let release!: () => void;
const fixture = await install(page, { hold: new Promise<void>((resolve) => { release = resolve; }) });
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("Submitted work");
await page.getByRole("button", { name: "Request save twice", exact: true }).click();
await expect.poll(() => fixture.writes.length).toBe(1);
await expect(page.getByRole("button", { name: "Save draft", exact: true })).toBeDisabled();
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("Newer unsaved work");
release();
await expect(page.getByTestId("save-busy")).toHaveText("false");
await expect(page.getByTestId("save-dirty")).toHaveText("true");
await expect(page.getByTestId("save-status")).toContainText("newer changes are still unsaved");
await expect(page.getByRole("textbox", { name: "Subject", exact: true })).toHaveValue("Newer unsaved work");
expect(fixture.subject()).toBe("Submitted work");
expect(fixture.writes).toHaveLength(1);
await page.getByRole("button", { name: "Save draft", exact: true }).click();
await expect(page.getByRole("alertdialog", { name: "Concurrent changes" })).toBeVisible();
await page.getByRole("tab", { name: "Use my change", exact: true }).click();
await page.getByRole("button", { name: "Apply resolution", exact: true }).click();
await expect(page.getByTestId("save-dirty")).toHaveText("false");
expect(fixture.subject()).toBe("Newer unsaved work");
});
test("failed follow-up refresh never turns an acknowledged save into save failed", async ({ page }) => {
const fixture = await install(page, { rejectReload: true });
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("Committed work");
await page.getByRole("button", { name: "Save draft", exact: true }).click();
await expect(page.getByTestId("save-result")).toHaveText("true");
await expect(page.getByTestId("save-status")).toContainText("Saved");
await expect(page.getByTestId("save-error")).toContainText("was saved");
await expect(page.getByTestId("save-dirty")).toHaveText("false");
expect(fixture.writes).toHaveLength(1);
});
test("cancelling a revision conflict ends saving and keeps the local draft", async ({ page }) => {
const fixture = await install(page, { conflict: true });
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("My conflicting work");
await page.getByRole("button", { name: "Save draft", exact: true }).click();
const dialog = page.getByRole("alertdialog", { name: "Concurrent changes" });
await expect(dialog).toBeVisible();
await dialog.getByRole("button", { name: "Cancel", exact: true }).click();
await expect(page.getByTestId("save-busy")).toHaveText("false");
await expect(page.getByTestId("save-status")).toContainText("Save cancelled");
await expect(page.getByTestId("save-dirty")).toHaveText("true");
await expect(page.getByRole("textbox", { name: "Subject", exact: true })).toHaveValue("My conflicting work");
expect(fixture.writes).toHaveLength(1);
});
test("background version refresh and failed discard do not silently clear unsaved work", async ({ page }) => {
await install(page, { rejectReload: true });
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("Keep this draft");
await page.getByRole("button", { name: "Background refresh", exact: true }).click();
await expect(page.getByRole("textbox", { name: "Subject", exact: true })).toHaveValue("Keep this draft");
await page.getByRole("button", { name: "Discard draft", exact: true }).click();
await expect(page.getByTestId("save-error")).toContainText("Fixture refresh unavailable");
await expect(page.getByTestId("save-dirty")).toHaveText("true");
await expect(page.getByRole("textbox", { name: "Subject", exact: true })).toHaveValue("Keep this draft");
});
test("new typing during a discard refresh is retained instead of being replaced by the late response", async ({ page }) => {
let release!: () => void;
const fixture = await install(page, { holdFirstRead: new Promise<void>(resolve => { release = resolve; }) });
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("Earlier draft");
await page.getByRole("button", { name: "Discard draft", exact: true }).click();
await expect.poll(fixture.reads).toBe(1);
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("Newer draft");
release();
await expect(page.getByTestId("save-error")).toContainText("Discard cancelled");
await expect(page.getByTestId("save-dirty")).toHaveText("true");
await expect(page.getByRole("textbox", { name: "Subject", exact: true })).toHaveValue("Newer draft");
expect(fixture.writes).toHaveLength(0);
});
test("a late discard response cannot overwrite an acknowledged save", async ({ page }) => {
let release!: () => void;
const fixture = await install(page, { holdFirstRead: new Promise<void>(resolve => { release = resolve; }) });
await page.getByRole("textbox", { name: "Subject", exact: true }).fill("Saved work");
await page.getByRole("button", { name: "Discard draft", exact: true }).click();
await expect.poll(fixture.reads).toBe(1);
await page.getByRole("button", { name: "Save draft", exact: true }).click();
await expect(page.getByTestId("save-result")).toHaveText("true");
release();
await expect(page.getByRole("textbox", { name: "Subject", exact: true })).toHaveValue("Saved work");
await expect(page.getByTestId("save-dirty")).toHaveText("false");
expect(fixture.subject()).toBe("Saved work");
});