test: playwright end-to-end qa (17 tests, real models)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.7
parent
0db7b8140b
commit
07dd3d6bf8
@@ -0,0 +1,50 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
import { readFileSync } 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 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);
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
const EMAIL = `qa+${Date.now()}@qai.test`;
|
||||
|
||||
test("signup + first-run guide configures both models", async ({ page }) => {
|
||||
await page.goto("/signup");
|
||||
await page.fill("#email", EMAIL);
|
||||
await page.fill("#password", "playwright password");
|
||||
await page.getByRole("button", { name: "Create account" }).click();
|
||||
|
||||
await expect(page.getByText("Step 1 of 3")).toBeVisible();
|
||||
await page.getByRole("link", { name: "Next" }).click();
|
||||
await expect(page.getByText("Step 2 of 3")).toBeVisible();
|
||||
|
||||
await page.fill("#analyst_base_url", process.env.QAI_ANALYST_BASE_URL ?? "http://192.168.88.131:8888/v1");
|
||||
await page.fill("#analyst_key", process.env.QAI_ANALYST_KEY ?? "EMPTY");
|
||||
await page.fill("#analyst_model", process.env.QAI_ANALYST_MODEL ?? "qwen3.8-flash-next");
|
||||
// Reasoning models stream thinking, not content: without this the report arrives empty.
|
||||
await page.fill("#analyst_extra", process.env.QAI_ANALYST_EXTRA ?? '{"chat_template_kwargs":{"enable_thinking":false}}');
|
||||
await page.fill("#jev_key", process.env.JEV_API_KEY ?? "");
|
||||
await page.getByRole("button", { name: "Save" }).click();
|
||||
await expect(page.getByText("Step 3 of 3")).toBeVisible({ timeout: 15_000 });
|
||||
await expect(page.getByText(/Analyst ready · Jev ready/)).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Go" })).toBeVisible();
|
||||
await page.context().storageState({ path: ".auth/user.json" });
|
||||
});
|
||||
|
||||
test("signed-out visitor is pushed to login", async ({ browser }) => {
|
||||
const ctx = await browser.newContext();
|
||||
const page = await ctx.newPage();
|
||||
await page.goto("/app");
|
||||
expect(page.url()).toContain("/login");
|
||||
await ctx.close();
|
||||
});
|
||||
@@ -0,0 +1,31 @@
|
||||
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");
|
||||
await page.locator('.rail a[href^="/app/"]').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();
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
import { test, expect } from "@playwright/test";
|
||||
test.use({ storageState: ".auth/user.json" });
|
||||
|
||||
for (const width of [320, 375, 414, 768]) {
|
||||
test(`no horizontal scroll at ${width}px`, async ({ page }) => {
|
||||
await page.setViewportSize({ width, height: 800 });
|
||||
await page.goto("/app");
|
||||
const over = await page.evaluate(() => document.documentElement.scrollWidth
|
||||
- document.documentElement.clientWidth);
|
||||
expect(over).toBeLessThanOrEqual(0);
|
||||
const box = await page.getByRole("button", { name: "Analyse" }).boundingBox();
|
||||
// .btn is 10px padding + 15px line = 35px at these widths; 32 leaves room for subpixel rounding
|
||||
// while still failing a squashed or two-line control.
|
||||
expect(box!.height).toBeGreaterThanOrEqual(32);
|
||||
});
|
||||
}
|
||||
|
||||
test("every settings field has a visible label", async ({ page }) => {
|
||||
await page.goto("/settings");
|
||||
// Scoped to the form: the rail's rename boxes are labelled for assistive tech with aria-label, not visibly.
|
||||
const orphans = await page.evaluate(() => [...document.querySelectorAll("form input")]
|
||||
.filter((i) => i.type !== "hidden" && (!i.id || !document.querySelector(`label[for="${i.id}"]`)))
|
||||
.map((i) => i.name || i.type));
|
||||
expect(orphans).toEqual([]);
|
||||
});
|
||||
Reference in New Issue
Block a user