fix(hk): protect unsaved editors and await prefix save results
This commit is contained in:
1 parent
89af4eb1f6
commit
d5091d1a73
11 files changed
+602
-231
No files matched your search
@@ -197,6 +197,7 @@ const server = createServer(async (req, res) => {
|
||||
}
|
||||
if (
|
||||
[
|
||||
"/admin/unsaved-harness",
|
||||
"/admin/attachment-harness",
|
||||
"/admin/studio-harness",
|
||||
"/admin/jobs-harness",
|
||||
|
||||
@@ -16,6 +16,7 @@ import messages from "@/messages/en.json";
|
||||
import { AttachmentHarness } from "./attachment-harness";
|
||||
import { FurnitureJobsHarness } from "./furniture-jobs-harness";
|
||||
import { fixtureAction } from "./server-actions";
|
||||
import { UnsavedHarness } from "./unsaved-harness";
|
||||
|
||||
const theme = document.createElement("style");
|
||||
theme.textContent = themePaletteCss(
|
||||
@@ -105,7 +106,9 @@ createRoot(root).render(
|
||||
? "News editor"
|
||||
: "Furniture synchronization"}
|
||||
</h1>
|
||||
{route.includes("studio-harness") ? (
|
||||
{route.includes("unsaved-harness") ? (
|
||||
<UnsavedHarness />
|
||||
) : route.includes("studio-harness") ? (
|
||||
<StudioClient
|
||||
source={buildFurniImportSource("it")}
|
||||
initialTree={[]}
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import { useState } from "react";
|
||||
import { PrefixDialog } from "@/app/admin/prefixes/prefix-dialog";
|
||||
import {
|
||||
confirmUnsavedNavigation,
|
||||
useUnsavedChanges,
|
||||
} from "@/hooks/use-unsaved-changes";
|
||||
export function UnsavedHarness() {
|
||||
const [value, setValue] = useState("");
|
||||
const [open, setOpen] = useState(false);
|
||||
const [failSave, setFailSave] = useState(false);
|
||||
const [saved, setSaved] = useState(false);
|
||||
const [navigated, setNavigated] = useState(false);
|
||||
const guard = useUnsavedChanges(value !== "", "Discard unsaved changes?");
|
||||
return (
|
||||
<>
|
||||
<label>
|
||||
Working value
|
||||
<input
|
||||
aria-label="Working value"
|
||||
value={value}
|
||||
onChange={(event) => setValue(event.target.value)}
|
||||
/>
|
||||
</label>
|
||||
<a href="/admin/unsaved-destination">Leave editor</a>
|
||||
<a href="#same-page">Same page</a>
|
||||
<a href="mailto:[email protected]">Email</a>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
if (confirmUnsavedNavigation()) setNavigated(true);
|
||||
}}
|
||||
>
|
||||
Programmatic navigation
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => {
|
||||
guard.markSaved();
|
||||
setValue("");
|
||||
setSaved(true);
|
||||
}}
|
||||
>
|
||||
Save working value
|
||||
</button>
|
||||
{saved && <p>Working value saved</p>}
|
||||
{navigated && <p>Navigation allowed</p>}
|
||||
<button type="button" onClick={() => setOpen(true)}>
|
||||
Open prefix
|
||||
</button>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={failSave}
|
||||
onChange={(event) => setFailSave(event.target.checked)}
|
||||
/>
|
||||
Fail prefix save
|
||||
</label>
|
||||
<PrefixDialog
|
||||
isOpen={open}
|
||||
onClose={() => setOpen(false)}
|
||||
editPrefix={null}
|
||||
onSave={async () => {
|
||||
if (failSave) return false;
|
||||
setSaved(true);
|
||||
return true;
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test.beforeEach(async ({ page }) => {
|
||||
await page.goto("/admin/unsaved-harness");
|
||||
});
|
||||
test("cancelled programmatic navigation preserves changes and save clears protection", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.getByLabel("Working value").fill("Keep this");
|
||||
page.once("dialog", (dialog) => dialog.dismiss());
|
||||
await page.getByRole("button", { name: "Programmatic navigation" }).click();
|
||||
await expect(page.getByText("Navigation allowed")).toHaveCount(0);
|
||||
await expect(page.getByLabel("Working value")).toHaveValue("Keep this");
|
||||
await page.getByRole("button", { name: "Save working value" }).click();
|
||||
let prompts = 0;
|
||||
page.on("dialog", async (dialog) => {
|
||||
prompts++;
|
||||
await dialog.dismiss();
|
||||
});
|
||||
await page.getByRole("button", { name: "Programmatic navigation" }).click();
|
||||
await expect(page.getByText("Navigation allowed")).toBeVisible();
|
||||
expect(prompts).toBe(0);
|
||||
});
|
||||
test("cancelled link navigation and same-page anchors preserve the editor", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.getByLabel("Working value").fill("Keep this");
|
||||
let prompts = 0;
|
||||
page.on("dialog", async (dialog) => {
|
||||
prompts++;
|
||||
await dialog.dismiss();
|
||||
});
|
||||
await page.getByRole("link", { name: "Leave editor" }).click();
|
||||
await expect(page).toHaveURL(/unsaved-harness$/);
|
||||
await page.getByRole("link", { name: "Same page" }).click();
|
||||
await expect(page).toHaveURL(/#same-page$/);
|
||||
expect(prompts).toBe(1);
|
||||
});
|
||||
test("prefix cancel and Escape preserve edits; accepting discard resets reopening", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.getByRole("button", { name: "Open prefix" }).click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Prefix text", exact: true })
|
||||
.fill("VIP");
|
||||
page.once("dialog", (dialog) => dialog.dismiss());
|
||||
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await expect(
|
||||
page.getByRole("textbox", { name: "Prefix text", exact: true }),
|
||||
).toHaveValue("VIP");
|
||||
page.once("dialog", (dialog) => dialog.dismiss());
|
||||
await page.keyboard.press("Escape");
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
page.once("dialog", (dialog) => dialog.accept());
|
||||
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||
await page.getByRole("button", { name: "Open prefix" }).click();
|
||||
await expect(
|
||||
page.getByRole("textbox", { name: "Prefix text", exact: true }),
|
||||
).toHaveValue("");
|
||||
});
|
||||
test("successful prefix save closes without a discard prompt", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.getByRole("button", { name: "Open prefix" }).click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Username", exact: true })
|
||||
.fill("Alice");
|
||||
await page
|
||||
.getByRole("textbox", { name: "Prefix text", exact: true })
|
||||
.fill("VIP");
|
||||
let prompts = 0;
|
||||
page.on("dialog", async (dialog) => {
|
||||
prompts++;
|
||||
await dialog.dismiss();
|
||||
});
|
||||
await page.getByRole("button", { name: "Save", exact: true }).click();
|
||||
await expect(page.getByRole("dialog")).toHaveCount(0);
|
||||
expect(prompts).toBe(0);
|
||||
});
|
||||
test("supported browser back can be cancelled without changing the address or draft", async ({
|
||||
page,
|
||||
}) => {
|
||||
test.skip(
|
||||
!(await page.evaluate(() => "navigation" in window)),
|
||||
"Navigation API unavailable",
|
||||
);
|
||||
await page.evaluate(() =>
|
||||
history.pushState({}, "", "/admin/unsaved-harness?step=2"),
|
||||
);
|
||||
await page.getByLabel("Working value").fill("Keep this");
|
||||
const prompt = page.waitForEvent("dialog");
|
||||
await page.evaluate(() => history.back());
|
||||
await (await prompt).dismiss();
|
||||
await expect(page).toHaveURL(/step=2$/);
|
||||
await expect(page.getByLabel("Working value")).toHaveValue("Keep this");
|
||||
});
|
||||
|
||||
test("failed prefix save keeps the editor and dirty protection", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.getByLabel("Fail prefix save").check();
|
||||
await page.getByRole("button", { name: "Open prefix" }).click();
|
||||
await page
|
||||
.getByRole("textbox", { name: "Username", exact: true })
|
||||
.fill("Alice");
|
||||
await page
|
||||
.getByRole("textbox", { name: "Prefix text", exact: true })
|
||||
.fill("VIP");
|
||||
await page.getByRole("button", { name: "Save", exact: true }).click();
|
||||
await expect(page.getByRole("dialog")).toBeVisible();
|
||||
page.once("dialog", (dialog) => dialog.dismiss());
|
||||
await page.getByRole("button", { name: "Cancel", exact: true }).click();
|
||||
await expect(
|
||||
page.getByRole("textbox", { name: "Prefix text", exact: true }),
|
||||
).toHaveValue("VIP");
|
||||
});
|
||||
Reference in new issue
Block a user