107 lines
3.7 KiB
TypeScript
107 lines
3.7 KiB
TypeScript
|
|
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([]);
|
||
|
|
});
|