2026-08-16 09:43:55 +00:00
|
|
|
|
import { test, expect, type Page } from "@playwright/test";
|
|
|
|
|
|
|
|
|
|
|
|
const BASE = process.env.BASE_URL ?? "http://localhost:3000";
|
|
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
test.describe("character page", () => {
|
|
|
|
|
|
test("add a spell, undo it, redo it", async ({ page }) => {
|
|
|
|
|
|
const errors = trackErrors(page);
|
|
|
|
|
|
await page.goto(`${BASE}/`);
|
|
|
|
|
|
|
|
|
|
|
|
const vacant = page.locator(".prepared-slot.vacant").first();
|
|
|
|
|
|
await vacant.locator("input").fill("Щит веры");
|
|
|
|
|
|
await vacant.locator("button[title='Записать заклинание']").click();
|
|
|
|
|
|
|
|
|
|
|
|
await expect(page.locator(".spell-name", { hasText: "Щит веры" })).toBeVisible();
|
|
|
|
|
|
|
|
|
|
|
|
// Ctrl+Z on the page (focus is on the button, not an input) undoes
|
|
|
|
|
|
// the add; Ctrl+Y redoes it.
|
|
|
|
|
|
await page.keyboard.press("Control+z");
|
|
|
|
|
|
await expect(page.locator(".spell-name", { hasText: "Щит веры" })).toHaveCount(0);
|
|
|
|
|
|
|
|
|
|
|
|
await page.keyboard.press("Control+y");
|
|
|
|
|
|
await expect(page.locator(".spell-name", { hasText: "Щит веры" })).toBeVisible();
|
|
|
|
|
|
|
|
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("remove a spell empties its slot", async ({ page }) => {
|
|
|
|
|
|
const errors = trackErrors(page);
|
|
|
|
|
|
await page.goto(`${BASE}/`);
|
|
|
|
|
|
|
|
|
|
|
|
const fireball = page.locator(".prepared-slot.occupied", { hasText: "Огненный шар" });
|
|
|
|
|
|
await fireball.locator("button[title='Убрать заклинание']").click();
|
|
|
|
|
|
|
|
|
|
|
|
await expect(page.locator(".spell-name", { hasText: "Огненный шар" })).toHaveCount(0);
|
|
|
|
|
|
await expect(page.locator(".prepared-slot.vacant")).toHaveCount(3);
|
|
|
|
|
|
|
|
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("a prepared spell survives a reload", async ({ page }) => {
|
|
|
|
|
|
const errors = trackErrors(page);
|
|
|
|
|
|
await page.goto(`${BASE}/`);
|
|
|
|
|
|
|
|
|
|
|
|
const vacant = page.locator(".prepared-slot.vacant").first();
|
|
|
|
|
|
await vacant.locator("input").fill("Щит веры");
|
|
|
|
|
|
await vacant.locator("button[title='Записать заклинание']").click();
|
|
|
|
|
|
await expect(page.locator(".spell-name", { hasText: "Щит веры" })).toBeVisible();
|
|
|
|
|
|
|
|
|
|
|
|
// Autosave is debounced by 500 ms; give it time before reloading.
|
|
|
|
|
|
await page.waitForTimeout(700);
|
|
|
|
|
|
await page.reload();
|
|
|
|
|
|
|
|
|
|
|
|
await expect(page.locator(".spell-name", { hasText: "Щит веры" })).toBeVisible();
|
|
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("attune an item, clear it, reload keeps the attunement", async ({ page }) => {
|
|
|
|
|
|
const errors = trackErrors(page);
|
|
|
|
|
|
await page.goto(`${BASE}/`);
|
|
|
|
|
|
|
|
|
|
|
|
const slot = page.locator(".attunement-slot").first();
|
|
|
|
|
|
await slot.locator("input").fill("Посох погоды");
|
|
|
|
|
|
await slot.locator("button[title='Привязать предмет']").click();
|
|
|
|
|
|
await expect(page.locator(".attuned-item")).toHaveText("Посох погоды");
|
|
|
|
|
|
|
|
|
|
|
|
// Reload: the attunement persists.
|
|
|
|
|
|
await page.waitForTimeout(700);
|
|
|
|
|
|
await page.reload();
|
|
|
|
|
|
await expect(page.locator(".attuned-item")).toHaveText("Посох погоды");
|
|
|
|
|
|
|
|
|
|
|
|
// Clear it.
|
|
|
|
|
|
await page.locator("button[title='Снять привязку']").click();
|
|
|
|
|
|
await expect(page.locator(".attuned-item")).toHaveCount(0);
|
|
|
|
|
|
|
|
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
|
|
});
|
2026-08-16 10:07:07 +00:00
|
|
|
|
|
|
|
|
|
|
test("more attunement slots can be added for custom campaigns", async ({ page }) => {
|
|
|
|
|
|
const errors = trackErrors(page);
|
|
|
|
|
|
await page.goto(`${BASE}/`);
|
|
|
|
|
|
|
|
|
|
|
|
await expect(page.locator(".attunement-slot")).toHaveCount(3);
|
|
|
|
|
|
await page.locator("button[title='Добавить слот привязки']").click();
|
|
|
|
|
|
await expect(page.locator(".attunement-slot")).toHaveCount(4);
|
|
|
|
|
|
|
|
|
|
|
|
const fourth = page.locator(".attunement-slot").nth(3);
|
|
|
|
|
|
await fourth.locator("input").fill("Волшебный рог");
|
|
|
|
|
|
await fourth.locator("button[title='Привязать предмет']").click();
|
|
|
|
|
|
await expect(page.locator(".attuned-item")).toHaveText("Волшебный рог");
|
|
|
|
|
|
|
|
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test("armor class is set manually and persists", async ({ page }) => {
|
|
|
|
|
|
const errors = trackErrors(page);
|
|
|
|
|
|
await page.goto(`${BASE}/`);
|
|
|
|
|
|
|
|
|
|
|
|
const ac = page.locator("#ac");
|
|
|
|
|
|
await expect(ac).toHaveValue("16");
|
|
|
|
|
|
await ac.fill("18");
|
|
|
|
|
|
|
|
|
|
|
|
// Autosave is debounced by 500 ms; give it time before reloading.
|
|
|
|
|
|
await page.waitForTimeout(700);
|
|
|
|
|
|
await page.reload();
|
|
|
|
|
|
await expect(page.locator("#ac")).toHaveValue("18");
|
|
|
|
|
|
|
|
|
|
|
|
expect(errors).toEqual([]);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|