- lib/plan-file.ts: BOM-aware decode for UTF-8, UTF-16LE/BE (with and without BOM) and UTF-32, with a NUL-density fallback for BOM-less UTF-16. The browser had a two-byte FF FE sniff only, so a UTF-16BE or BOM-less plan arrived as NUL-filled junk and the digest blamed the export. - digestPlan now quotes fast-xml-parser's first line instead of a fixed sentence, so a truncated file, a stray entity and a wrong file stay distinguishable. - plan-file.ts holds the decode so the XML parser stays out of the client bundle. - tests: 6 encodings round-trip through decodePlan into a digest; a truncated plan's message is asserted to carry a reason; e2e drops a real UTF-16LE file. 36 unit, 23 e2e. - history.spec picked the rail's first row, which is now one of the new failing-input rows; it selects the newest finished run instead. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
34 lines
1.8 KiB
TypeScript
34 lines
1.8 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
test.use({ storageState: ".auth/user.json" });
|
|
|
|
test("rename sticks, delete asks first then removes", async ({ page }) => {
|
|
await page.goto("/app");
|
|
const row = page.locator(".rail li").first();
|
|
await row.locator('input[name="title"]').fill("Renamed by QA");
|
|
await row.getByRole("button", { name: "Save" }).click();
|
|
await expect(page.locator(".rail", { hasText: "Renamed by QA" })).toBeVisible({ timeout: 15_000 });
|
|
|
|
const before = await page.locator(".rail li").count();
|
|
let asked = false;
|
|
page.once("dialog", async (d) => { asked = true; await d.dismiss(); }); // refuse: nothing may vanish
|
|
await page.locator(".rail li").first().getByRole("button", { name: "Delete" }).click();
|
|
await page.waitForTimeout(1000);
|
|
expect(asked).toBe(true);
|
|
expect(await page.locator(".rail li").count()).toBe(before);
|
|
|
|
page.once("dialog", async (d) => { await d.accept(); });
|
|
await page.locator(".rail li").first().getByRole("button", { name: "Delete" }).click();
|
|
await expect.poll(async () => page.locator(".rail li").count(), { timeout: 20_000 }).toBe(before - 1);
|
|
});
|
|
|
|
test("reopening a stored run renders the report with no new model call", async ({ page }) => {
|
|
await page.goto("/app");
|
|
// The newest finished run, not the newest row: the encoding tests above leave `failed` rows on top,
|
|
// and only unfinished rows carry a status pill in the rail.
|
|
await page.locator('.rail a[href^="/app/"]:not(:has(.pill))').first().click();
|
|
await expect(page.locator("#report")).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.getByRole("button", { name: "Analyse" })).toHaveCount(0);
|
|
// The stored Jev verdict is server-rendered, so it is readable even if the report body had failed.
|
|
await expect(page.getByText(/Jev verdict ·/)).toBeVisible();
|
|
});
|