- 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>
73 lines
3.8 KiB
TypeScript
73 lines
3.8 KiB
TypeScript
import { test, expect } from "@playwright/test";
|
|
import { readFileSync, writeFileSync, mkdirSync } from "node:fs";
|
|
|
|
const xml = readFileSync("fixtures/sample.sqlplan", "utf8");
|
|
test.use({ storageState: ".auth/user.json", permissions: ["clipboard-read", "clipboard-write"] });
|
|
|
|
test("full run: paste → stages → streamed report → copy → print", async ({ page }) => {
|
|
await page.goto("/app");
|
|
await page.fill("#xml", xml);
|
|
await page.fill("#note", "slow since the migration");
|
|
await page.getByRole("button", { name: "Analyse" }).click();
|
|
|
|
// Stages are named one at a time; the whole run takes tens of seconds with a real model.
|
|
await expect(page.getByText(/1 of 4 · Reading the plan/)).toBeVisible({ timeout: 20_000 });
|
|
await expect(page.getByText(/4 of 4 · Writing the report/)).toBeVisible({ timeout: 180_000 });
|
|
|
|
// The report paints while oul is still arriving, so the headline shows up first.
|
|
await expect(page.locator("#report h2").first()).toBeVisible({ timeout: 180_000 });
|
|
// The Analyse button only unlocks on the done frame: by then the whole report has arrived and parsed.
|
|
await expect(page.getByRole("button", { name: "Analyse" })).toBeEnabled({ timeout: 180_000 });
|
|
expect(await page.locator("#report .card").count()).toBeGreaterThan(2);
|
|
// No raw OpenUI Lang leaked into the page as text.
|
|
expect(await page.locator("main").innerText()).not.toContain("root = Report(");
|
|
|
|
await page.getByRole("button", { name: "Copy", exact: true }).click();
|
|
const clip = await page.evaluate(() => navigator.clipboard.readText());
|
|
expect(clip).toContain("slow since the migration");
|
|
|
|
await page.getByRole("button", { name: "Export to PDF" }).click(); // window.print()
|
|
await page.emulateMedia({ media: "print" });
|
|
await expect(page.locator(".rail")).toBeHidden();
|
|
await expect(page.locator(".toolbar")).toBeHidden();
|
|
});
|
|
|
|
test("a dropped .sqlplan file decodes and is accepted", async ({ page }) => {
|
|
await page.goto("/app");
|
|
await page.setInputFiles('input[type="file"]', "fixtures/sample.sqlplan");
|
|
const value = await page.locator("#xml").inputValue();
|
|
expect(value).toContain("ShowPlanXML");
|
|
expect(value).not.toContain("\u0000");
|
|
expect(value).not.toContain("\ufeff");
|
|
});
|
|
|
|
test("a UTF-16LE .sqlplan, which is what SSMS writes, decodes without NULs", async ({ page }) => {
|
|
// fixtures/sample.sqlplan is UTF-8+BOM, so the test above never exercised the SSMS encoding.
|
|
mkdirSync(".playwright-data", { recursive: true });
|
|
const path = ".playwright-data/upload-utf16le.sqlplan";
|
|
const clean = readFileSync("fixtures/sample.sqlplan", "utf8").replace(/^/, "");
|
|
writeFileSync(path, Buffer.from("\uFEFF" + clean, "utf16le"));
|
|
await page.goto("/app");
|
|
await page.setInputFiles('input[type="file"]', path);
|
|
const value = await page.locator("#xml").inputValue();
|
|
expect(value.startsWith("<?xml")).toBe(true);
|
|
expect(value).not.toContain("\u0000");
|
|
});
|
|
|
|
test("a truncated plan names the parse failure instead of blaming the export", async ({ page }) => {
|
|
await page.goto("/app");
|
|
await page.fill("#xml", readFileSync("fixtures/sample.sqlplan", "utf8").slice(0, 900));
|
|
await page.getByRole("button", { name: "Analyse" }).click();
|
|
const alert = page.getByRole("alert").filter({ hasText: "digesting" });
|
|
await expect(alert).toBeVisible({ timeout: 30_000 });
|
|
expect(await alert.innerText()).toMatch(/stopped parsing|Unexpected|unclosed|tag name/i);
|
|
});
|
|
|
|
test("a pasted SQL script is refused with a named reason", async ({ page }) => {
|
|
await page.goto("/app");
|
|
await page.fill("#xml", "SELECT * FROM dbo.Orders WHERE CustomerId = 42;");
|
|
await page.getByRole("button", { name: "Analyse" }).click();
|
|
await expect(page.getByRole("alert").filter({ hasText: "digesting" })).toBeVisible({ timeout: 30_000 });
|
|
await expect(page.locator("#report")).toHaveCount(0);
|
|
});
|