166 lines
5.6 KiB
TypeScript
166 lines
5.6 KiB
TypeScript
/**
|
|
* Form Data Loss Prevention E2E Tests
|
|
*
|
|
* Verifies fixes from PR #133 — background refetches, false save states,
|
|
* and stale taxonomy selections no longer cause silent data loss.
|
|
*
|
|
* Seed data:
|
|
* - posts: "First Post" (published), with categories taxonomy
|
|
* - categories taxonomy: "News", "Tutorials", "Opinion"
|
|
* - sections: "Hero Section" (slug: hero)
|
|
*/
|
|
|
|
import { test, expect } from "../fixtures";
|
|
|
|
test.describe("Form Data Loss Prevention", () => {
|
|
test.beforeEach(async ({ admin }) => {
|
|
await admin.devBypassAuth();
|
|
});
|
|
|
|
test("settings edits survive window blur/focus", async ({ admin, page }) => {
|
|
await admin.goto("/settings/general");
|
|
await admin.waitForShell();
|
|
await admin.waitForLoading();
|
|
|
|
// Edit the tagline field
|
|
const taglineInput = page.getByLabel("Tagline");
|
|
await taglineInput.fill("My edited tagline");
|
|
|
|
// Simulate window blur + focus (triggers React Query refetches for stale queries)
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event("blur"));
|
|
window.dispatchEvent(new Event("focus"));
|
|
});
|
|
|
|
// Wait for any potential refetch to complete
|
|
await page.waitForTimeout(1000);
|
|
|
|
// The edit should persist (staleTime: Infinity prevents refetch from overwriting)
|
|
await expect(taglineInput).toHaveValue("My edited tagline");
|
|
});
|
|
|
|
test("section editor edits survive window blur/focus", async ({ admin, page }) => {
|
|
await admin.goto("/sections/hero");
|
|
await admin.waitForShell();
|
|
await admin.waitForLoading();
|
|
|
|
// Edit the title field
|
|
const titleInput = page.getByLabel("Title");
|
|
const originalTitle = await titleInput.inputValue();
|
|
const editedTitle = `Edited ${Date.now()}`;
|
|
await titleInput.fill(editedTitle);
|
|
|
|
// Simulate window blur + focus (triggers React Query refetches without staleTime)
|
|
await page.evaluate(() => {
|
|
window.dispatchEvent(new Event("blur"));
|
|
window.dispatchEvent(new Event("focus"));
|
|
});
|
|
|
|
// Small wait for any potential refetch to complete
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Edits should still be there (staleTime: Infinity prevents overwrite)
|
|
await expect(titleInput).toHaveValue(editedTitle);
|
|
|
|
// Save button should show "Save" (dirty), not "Saved" (clean)
|
|
await expect(page.getByRole("button", { name: "Save" })).toBeEnabled();
|
|
|
|
// Restore original value to avoid side effects on other tests
|
|
await titleInput.fill(originalTitle);
|
|
});
|
|
|
|
test("section editor save failure keeps form dirty", async ({ admin, page }) => {
|
|
await admin.goto("/sections/hero");
|
|
await admin.waitForShell();
|
|
await admin.waitForLoading();
|
|
|
|
// Edit the title field so the form is dirty
|
|
const titleInput = page.getByLabel("Title");
|
|
const originalTitle = await titleInput.inputValue();
|
|
await titleInput.fill(`Error test ${Date.now()}`);
|
|
|
|
// Intercept the section update API call and force a failure
|
|
await page.route("**/api/sections/hero", (route) => {
|
|
if (route.request().method() === "PUT" || route.request().method() === "PATCH") {
|
|
return route.fulfill({
|
|
status: 500,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ error: { code: "SERVER_ERROR", message: "Simulated failure" } }),
|
|
});
|
|
}
|
|
return route.continue();
|
|
});
|
|
|
|
// Click save
|
|
const saveButton = page.getByRole("button", { name: "Save" });
|
|
await saveButton.click();
|
|
|
|
// Wait for the error to be processed
|
|
await page.waitForTimeout(1000);
|
|
|
|
// The Save button should still be enabled (form is still dirty)
|
|
// It should NOT show "Saved" — the mutation failed
|
|
await expect(page.getByRole("button", { name: "Save" })).toBeEnabled();
|
|
|
|
// Remove the route intercept and restore title
|
|
await page.unroute("**/api/sections/hero");
|
|
await titleInput.fill(originalTitle);
|
|
});
|
|
|
|
test("taxonomy checkboxes clear when all terms are removed", async ({
|
|
admin,
|
|
page,
|
|
serverInfo,
|
|
}) => {
|
|
// Navigate to a published post that we can assign terms to
|
|
const postId = serverInfo.contentIds["posts"]?.[0];
|
|
if (!postId) {
|
|
test.skip();
|
|
return;
|
|
}
|
|
|
|
await admin.goToEditContent("posts", postId);
|
|
await admin.waitForLoading();
|
|
|
|
// Wait for the taxonomy sidebar to load
|
|
const taxonomyHeading = page.locator("h3", { hasText: "Taxonomies" });
|
|
await expect(taxonomyHeading).toBeVisible({ timeout: 10000 });
|
|
|
|
// Find the category checkboxes
|
|
const newsCheckbox = page.getByRole("checkbox", { name: "News" });
|
|
const tutorialsCheckbox = page.getByRole("checkbox", { name: "Tutorials" });
|
|
|
|
// Check two categories
|
|
await newsCheckbox.check();
|
|
await page.waitForTimeout(500); // Wait for auto-save
|
|
await tutorialsCheckbox.check();
|
|
await page.waitForTimeout(500); // Wait for auto-save
|
|
|
|
// Verify both are checked
|
|
await expect(newsCheckbox).toBeChecked();
|
|
await expect(tutorialsCheckbox).toBeChecked();
|
|
|
|
// Now uncheck both — this is the bug scenario from PR #133
|
|
await newsCheckbox.uncheck();
|
|
await page.waitForTimeout(500);
|
|
await tutorialsCheckbox.uncheck();
|
|
await page.waitForTimeout(500);
|
|
|
|
// All checkboxes should be unchecked (the old bug would leave stale checks)
|
|
await expect(newsCheckbox).not.toBeChecked();
|
|
await expect(tutorialsCheckbox).not.toBeChecked();
|
|
await expect(page.getByRole("checkbox", { name: "Opinion" })).not.toBeChecked();
|
|
|
|
// Reload to verify server state matches
|
|
await page.reload();
|
|
await admin.waitForShell();
|
|
await admin.waitForLoading();
|
|
|
|
// After reload, all should still be unchecked
|
|
await expect(taxonomyHeading).toBeVisible({ timeout: 10000 });
|
|
await expect(page.getByRole("checkbox", { name: "News" })).not.toBeChecked();
|
|
await expect(page.getByRole("checkbox", { name: "Tutorials" })).not.toBeChecked();
|
|
await expect(page.getByRole("checkbox", { name: "Opinion" })).not.toBeChecked();
|
|
});
|
|
});
|