import { test, expect, type Page } from "@playwright/test"; // Override with BASE_URL (e.g. when the dev server runs on another port). const BASE = process.env.BASE_URL ?? "http://localhost:3000"; // Collects browser console errors and page errors for the duration of a test. function trackErrors(page: Page): string[] { const errors: string[] = []; page.on("console", (msg) => { if (msg.type() === "error") { errors.push(msg.text()); } }); page.on("pageerror", (err) => { errors.push(String(err)); }); return errors; } // Autosave is debounced at 500 ms; give it time before reloading. const DEBOUNCE_MS = 700; test("undo and redo a campaign name edit", async ({ page }) => { const errors = trackErrors(page); await page.goto(`${BASE}/`); const original = await page.locator("#campaign-name").inputValue(); const undo = page.getByRole("button", { name: "Отменить" }); const redo = page.getByRole("button", { name: "Вернуть" }); // Fresh history: both buttons are disabled. await expect(undo).toBeDisabled(); await expect(redo).toBeDisabled(); // One edit burst; the undo button wakes up immediately (the burst does // not need to settle first). const edited = "Имя для отмены"; await page.locator("#campaign-name").fill(edited); await expect(undo).toBeEnabled(); await undo.click(); await expect(page.locator("#campaign-name")).toHaveValue(original); await redo.click(); await expect(page.locator("#campaign-name")).toHaveValue(edited); // Keyboard: Ctrl+Z outside a text field undoes the redo. await page.locator(".version").click(); await page.keyboard.press("Control+z"); await expect(page.locator("#campaign-name")).toHaveValue(original); expect(errors).toEqual([]); }); test("Ctrl+Z inside a text field keeps the browser's native undo", async ({ page, }) => { const errors = trackErrors(page); await page.goto(`${BASE}/`); const input = page.locator("#campaign-name"); await input.fill("Текст набран в поле"); await input.press("Control+z"); // The app-wide handler must not fire while the field has focus: the // undo button stays enabled (the pending burst is still undoable). // The field's own value change is the browser's native undo — it // owns Ctrl+Z inside text fields by design. await expect(page.getByRole("button", { name: "Отменить" })).toBeEnabled(); expect(errors).toEqual([]); }); test("undone state survives a reload", async ({ page }) => { const errors = trackErrors(page); await page.goto(`${BASE}/`); const original = await page.locator("#campaign-name").inputValue(); const edited = "Будет отменено до перезагрузки"; await page.locator("#campaign-name").fill(edited); await page.getByRole("button", { name: "Отменить" }).click(); await expect(page.locator("#campaign-name")).toHaveValue(original); // The undo went through the autosave path; wait for the debounce. await page.waitForTimeout(DEBOUNCE_MS); await page.reload(); await expect(page.locator("#campaign-name")).toHaveValue(original); expect(errors).toEqual([]); }); test("clear is undoable", async ({ page }) => { const errors = trackErrors(page); await page.goto(`${BASE}/`); const original = await page.locator("#campaign-name").inputValue(); await page.getByRole("button", { name: "Очистить" }).click(); await page.locator(".modal").getByRole("button", { name: "Очистить" }).click(); await expect(page.locator("#campaign-name")).toHaveValue(""); // One undo step restores everything the clear replaced. await page.getByRole("button", { name: "Отменить" }).click(); await expect(page.locator("#campaign-name")).toHaveValue(original); expect(errors).toEqual([]); });