Files
EpicNext-Cms/e2e/ui/draft-recovery.spec.ts
T
openhands fb470123c3
CI / check (push) Successful in 30s
CI / tests-integration (push) Successful in 1m58s
CI / tests-imaging-browsers (push) Successful in 2m2s
CI / tests-unit (push) Successful in 2m9s
CI / tests-quality (push) Successful in 1m2s
CI / tests-ui (push) Successful in 4m50s
CI / preflight (push) Skipped
CI / deploy (push) Failing after 1m42s
test(ui): stabilize cross-browser suite
- Run visual snapshots on Chromium only; Firefox/WebKit rendering does
  not match Chromium pixel output. Functional and accessibility
  assertions still run on all four projects (baselines were renamed to
  the new project names in the previous commit).
- Poll axe audits until clean: WebKit can report stale color-contrast
  violations for a moment after interactions while styles settle.
- Poll the Tab-focus assertion: Firefox can swallow a Tab while the
  sandboxed srcdoc preview frame is still loading.
- Fix the recovery clock race: install the fake clock before navigation
  at a fixed time and pause at a fixed target instead of wall-clock
  values, which under load could already be in the fake clock's past
  ('Cannot fast-forward to the past').
2026-10-17 17:14:13 +02:00

175 lines
6.5 KiB
TypeScript

import { expect, test } from "@playwright/test";
const key = "cms:draft:v1:7:article:new";
test("browser draft survives session failure and reload, restores explicitly and clears only after save", async ({
page,
}) => {
await page.route("**/fixture/actions/load-recovery", (route) =>
route.fulfill({ status: 401, body: "expired" }),
);
await page.route("**/fixture/actions/article", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({ ok: false, error: "Session expired" }),
}),
);
await page.goto("/admin/articles/recovery");
const title = page.locator('input[name="title"]');
await title.fill("Keep this unfinished article");
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), key))
.toContain("Keep this unfinished article");
await page.getByRole("button", { name: "Save article", exact: true }).click();
await expect(page.getByRole("alert")).toContainText("Session expired");
expect(await page.evaluate((k) => localStorage.getItem(k), key)).toContain(
"Keep this unfinished article",
);
page.on("dialog", (dialog) => dialog.accept());
await page.reload();
await expect(title).toHaveValue("Community update");
await page
.getByRole("button", { name: "Restore browser copy", exact: true })
.click();
await expect(title).toHaveValue("Keep this unfinished article");
await page.unroute("**/fixture/actions/article");
await page.route("**/fixture/actions/article", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({ ok: true }),
}),
);
await page.getByRole("button", { name: "Save article", exact: true }).click();
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), key))
.toBeNull();
await page.reload();
await expect(
page.getByRole("button", { name: "Restore browser copy", exact: true }),
).toHaveCount(0);
});
test("pending recovery does not overwrite current edits and discard preserves them", async ({
page,
}) => {
await page.goto("/admin/articles/recovery");
const title = page.locator('input[name="title"]');
await title.fill("Older draft");
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), key))
.toContain("Older draft");
page.on("dialog", (dialog) => dialog.accept());
await page.reload();
await title.fill("New current edits");
await page
.getByRole("button", { name: "Discard browser copy", exact: true })
.click();
await expect(title).toHaveValue("New current edits");
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), key))
.toContain("New current edits");
});
test("unavailable browser storage leaves the form editable", async ({
page,
}) => {
await page.addInitScript(() => {
Storage.prototype.setItem = () => {
throw Error("quota");
};
});
await page.goto("/admin/articles/recovery");
await page.locator('input[name="title"]').fill("Still editable");
await expect(
page.getByText("Browser recovery is unavailable.", { exact: false }),
).toBeVisible();
await expect(page.locator('input[name="title"]')).toHaveValue(
"Still editable",
);
});
test("saving while recovery is pending enables recovery for subsequent edits", async ({
page,
}) => {
await page.route("**/fixture/actions/article", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({ ok: true }),
}),
);
await page.goto("/admin/articles/recovery");
const title = page.locator('input[name="title"]');
await title.fill("Previous browser draft");
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), key))
.toContain("Previous browser draft");
page.on("dialog", (dialog) => dialog.accept());
await page.reload();
await expect(
page.getByRole("button", { name: "Restore browser copy", exact: true }),
).toBeVisible();
await page.getByRole("button", { name: "Save article", exact: true }).click();
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), key))
.toBeNull();
await title.fill("Edits after successful save");
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), key))
.toContain("Edits after successful save");
});
test("unmount flush retains an edit before the autosave interval", async ({
page,
}) => {
// Install the fake clock before navigation so every page timer is faked,
// then jump forward to a fixed pause point. Using wall-clock `new Date()`
// for both calls raced under load: the fake clock ticks in real time
// between the calls and can pass the target ("Cannot fast-forward to the
// past"). The 60s jump only fires no-op persists on a clean form.
await page.clock.install({ time: new Date("2030-01-01T12:00:00Z") });
await page.goto("/admin/articles/recovery");
await page.clock.pauseAt(new Date("2030-01-01T12:01:00Z"));
await page.locator('input[name="title"]').fill("Immediate final edit");
await page
.getByRole("button", { name: "Close editor fixture", exact: true })
.click();
expect(await page.evaluate((k) => localStorage.getItem(k), key)).toContain(
"Immediate final edit",
);
});
test("event title and dates recover after reload and clear after successful update", async ({
page,
}) => {
const eventKey = "cms:draft:v1:7:event:12";
await page.route("**/fixture/actions/update-event", (route) =>
route.fulfill({
contentType: "application/json",
body: JSON.stringify({ ok: true, data: { id: 12 } }),
}),
);
await page.goto("/admin/events/recovery");
const title = page.locator("#title");
const dates = page.locator('input[type="datetime-local"]');
await title.fill("Recovered community event");
await dates.nth(0).fill("2030-02-03T14:30");
await dates.nth(1).fill("2030-02-03T16:00");
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), eventKey))
.toContain("2030-02-03T14:30:00.000Z");
page.on("dialog", (dialog) => dialog.accept());
await page.reload();
await expect(title).toHaveValue("Original event");
await page
.getByRole("button", { name: "Restore browser copy", exact: true })
.click();
await expect(title).toHaveValue("Recovered community event");
await expect(dates.nth(0)).toHaveValue("2030-02-03T14:30");
await expect(dates.nth(1)).toHaveValue("2030-02-03T16:00");
const save = page.waitForRequest("**/fixture/actions/update-event");
await page.locator('button[type="submit"]').click();
expect((await save).postDataJSON()).toMatchObject({
id: 12,
title: "Recovered community event",
startsAt: "2030-02-03T14:30:00.000Z",
endsAt: "2030-02-03T16:00:00.000Z",
});
await expect
.poll(() => page.evaluate((k) => localStorage.getItem(k), eventKey))
.toBeNull();
});