e2e/zzzzzz-professional-smoke.spec.ts
Rama main · réplica de lectura del repositorio original.
import { openBuildTool, clickProjectAction, openProjectDetails, openProjectSection } from "./workspace-controls";
import { expect, test, type Page } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";
import { randomUUID } from "node:crypto";
async function login(page: Page) {
await page.goto("/"); await page.getByLabel("Workspace password").fill("demo-password-please-change"); await page.getByRole("button", { name: "Sign in", exact: true }).click(); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible();
}
async function seed(page: Page, name: string, count = 0) {
const id = `smoke-${randomUUID()}`, revision = `${id}-r1`;
const csrf = (await page.context().cookies()).find((cookie) => cookie.name === "forge_csrf")!.value;
const post = async (path: string, data: object) => { const response = await page.request.post(`/api/v1${path}`, { headers: { "x-csrf-token": csrf, "idempotency-key": randomUUID() }, data }); expect(response.status(), await response.text()).toBeLessThan(300); return response.json(); };
await post("/projects/with-initial-revision", { project: { id, name, status: "planned", description: "Synthetic smoke fixture." }, revision: { id: revision, name: "Initial", status: "concept", fabricationRoute: "printed" } });
for (let i = 0; i < count; i++) await post(`/project-revisions/${revision}/bom`, { name: i === count - 1 ? "Café rear-panel connector" : `Ordinary part ${i}`, requiredQuantity: 1, unit: "each", role: "consumed", optional: false, constraints: {}, alternatives: [] });
await page.goto(`/#/projects/${id}/plan`); await page.reload(); await expect(page.getByRole("heading", { name, exact: true })).toBeVisible(); return { id, revision };
}
const tab = openProjectSection;
test("an unavailable project link never opens an unrelated project", async ({ page }) => {
await login(page); await page.goto("/#/projects/missing-project/plan"); await expect(page.getByRole("heading", { name: "Project unavailable", exact: true })).toBeVisible(); expect(page.url()).toContain("missing-project");
await page.getByRole("button", { name: "Retry project lookup", exact: true }).click(); await expect(page.getByRole("heading", { name: "Project unavailable", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Open project register", exact: true }).click(); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible();
});
test("build draft navigation keeps edits until a deliberate discard", async ({ page }) => {
await login(page); await seed(page, "Smoke guarded build"); await tab(page, "Build steps"); await openBuildTool(page, "print"); await page.getByRole("button", { name: "Create build plan", exact: true }).click(); await page.getByLabel("Build plan name").fill("Keep this build draft");
await expect(page.getByRole("button", { name: "Refresh build plan", exact: true })).toBeDisabled(); await tab(page, "Files"); await expect(page.getByRole("alertdialog")).toBeVisible();
await page.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(page.getByLabel("Build plan name")).toHaveValue("Keep this build draft");
await tab(page, "Files"); await page.getByRole("button", { name: "Discard changes and leave", exact: true }).click(); await expect(page.getByRole("heading", { name: "Files", exact: true })).toBeVisible();
});
test("cancelling a build draft keeps meaningful edits until discard is confirmed", async ({ page }) => {
await login(page);
await seed(page, "Smoke deliberate build cancel");
let saves = 0;
page.on("request", (request) => {
if (request.method() === "PUT" && new URL(request.url()).pathname.endsWith("/build-plan")) saves += 1;
});
await tab(page, "Build steps");
await openBuildTool(page, "print"); await page.getByRole("button", { name: "Create build plan", exact: true }).click();
await page.getByLabel("Build plan name", { exact: true }).fill("Keep the bracket plan");
await page.getByRole("button", { name: "Add build part", exact: true }).click();
await page.getByLabel("Build part 1 name", { exact: true }).fill("Synthetic bracket");
await page.getByLabel("Build part 1 quantity", { exact: true }).fill("3");
await page.getByRole("button", { name: "Cancel build draft", exact: true }).click();
const confirmation = page.getByRole("alertdialog", { name: "Discard build draft?", exact: true });
await expect(confirmation).toBeVisible();
await confirmation.getByRole("button", { name: "Keep editing", exact: true }).click();
await expect(confirmation).toHaveCount(0);
await expect(page.getByLabel("Build plan name", { exact: true })).toHaveValue("Keep the bracket plan");
await expect(page.getByLabel("Build part 1 name", { exact: true })).toHaveValue("Synthetic bracket");
await expect(page.getByLabel("Build part 1 quantity", { exact: true })).toHaveValue("3");
await page.getByRole("button", { name: "Cancel build draft", exact: true }).click();
await confirmation.getByRole("button", { name: "Discard draft", exact: true }).click();
await expect(confirmation).toHaveCount(0);
await expect(page.getByRole("button", { name: "Create build plan", exact: true })).toBeFocused();
await expect(page.getByLabel("Build plan name", { exact: true })).toHaveCount(0);
expect(saves).toBe(0);
});
test("browser Back does not silently discard a workstream draft", async ({ page }) => {
await login(page); await seed(page, "Smoke browser history"); await tab(page, "Build steps"); await openBuildTool(page, "tasks"); await page.getByRole("button", { name: "Add task group", exact: true }).click(); await page.getByLabel("Task group type").selectOption("firmware"); await tab(page, "Files"); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(page.getByLabel("Task group type")).toHaveValue("firmware"); await page.getByLabel("Task group name").fill("Do not lose this task");
await page.goBack(); await expect(page.getByRole("alertdialog")).toBeVisible(); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(page.getByLabel("Task group name")).toHaveValue("Do not lose this task"); expect(page.url()).toMatch(/\/build$/u);
await tab(page, "Requirements"); await page.getByRole("button", { name: "Discard changes and leave", exact: true }).click(); await expect(page.locator(".bom-section")).toBeVisible();
});
test("supplier search finds requirements beyond the first page and protects quote entry", async ({ page }) => {
await login(page); await seed(page, "Smoke sourcing search", 25); await tab(page, "Shopping list");
await page.getByLabel("Search quote requirements").fill("connector cafe"); await expect(page.locator(".sourcing-requirement")).toHaveCount(1); await expect(page.locator(".sourcing-requirement")).toContainText("Café rear-panel connector");
await expect(page.locator(".sourcing-results-count")).toContainText("1 matching requirement"); await expect(page.locator(".sourcing-results-count")).toContainText("25 in the full revision");
await page.getByRole("button", { name: "Record quote for Café rear-panel connector", exact: true }).click(); await page.getByLabel("Supplier", { exact: true }).fill("Draft supplier");
await expect(page.getByRole("button", { name: "Refresh supplier quotes", exact: true })).toBeDisabled(); await tab(page, "Requirements"); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(page.getByLabel("Supplier", { exact: true })).toHaveValue("Draft supplier");
await page.getByRole("button", { name: "Cancel quote", exact: true }).click(); await tab(page, "Requirements"); await expect(page.getByRole("alertdialog")).toHaveCount(0);
});
test("new workstream appears in file scope without a browser reload", async ({ page }) => {
await login(page); await seed(page, "Smoke workstream context"); await tab(page, "Build steps"); await openBuildTool(page, "tasks"); await page.getByRole("button", { name: "Add task group", exact: true }).click(); await page.getByLabel("Task group name").fill("Fixture firmware"); await page.getByRole("button", { name: "Create task group", exact: true }).click();
await expect(page.getByText("Task group created.", { exact: true })).toBeVisible(); await expect(page.locator(".workstream-row")).toHaveCount(1);
await tab(page, "Files"); await expect(page.getByLabel("Choose file scope").locator("option").filter({ hasText: "Fixture firmware" })).toHaveCount(1);
});
test("staged files require a decision before switching revision scope", async ({ page }) => {
await login(page); await seed(page, "Smoke staged file"); await tab(page, "Files");
await page.getByLabel("Choose files to upload").setInputFiles({ name: "draft-file.md", mimeType: "text/markdown", buffer: Buffer.from("Synthetic staged file") });
await page.getByLabel("Choose file scope").selectOption("all"); await expect(page.getByRole("alertdialog")).toBeVisible(); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(page.getByRole("button", { name: "Add 1 file", exact: true })).toBeEnabled();
await tab(page, "Requirements"); await page.getByRole("button", { name: "Discard changes and leave", exact: true }).click(); await tab(page, "Files"); await expect(page.getByRole("button", { name: "Add 1 file", exact: true })).toHaveCount(0); await expect(page.getByRole("button", { name: "Add files", exact: true })).toBeEnabled();
});
test("mobile task area precedes setup context and remains within the viewport", async ({ page }) => {
await page.setViewportSize({ width: 320, height: 800 }); await login(page); await seed(page, "Smoke mobile hierarchy", 3);
await expect(page.getByRole("complementary", { name: "Project details" })).toHaveCount(0);
await openProjectDetails(page);
const order = await page.evaluate(() => { const tabs = document.querySelector(".project-navigation")!.getBoundingClientRect(), context = document.querySelector(".build-approach-card")!.getBoundingClientRect(); return { workspace: tabs.top, context: context.top, width: document.documentElement.scrollWidth }; });
expect(order.workspace).toBeLessThan(order.context); expect(order.width).toBeLessThanOrEqual(320);
await tab(page, "Shopping list"); await expect(page.locator(".sourcing-requirement")).toHaveCount(3);
});
test("build loading errors do not claim that no plan exists", async ({ page }) => {
await login(page); await seed(page, "Smoke failed plan read");
await page.route("**/build-plan", (route) => route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: { code: "unavailable", message: "Synthetic read failure" } }) }));
await tab(page, "Build steps"); await openBuildTool(page, "print"); await expect(page.locator(".build-planning").getByRole("alert")).toBeVisible(); await expect(page.getByText("No multi-plate plan recorded for this revision.", { exact: true })).toHaveCount(0); await expect(page.getByRole("button", { name: "Create build plan", exact: true })).toBeDisabled();
});
for (const dark of [false, true]) test(`rendered pages and dialogs pass accessibility checks in ${dark ? "dark" : "light"} mode`, async ({ page }) => {
await page.emulateMedia({ colorScheme: dark ? "dark" : "light", reducedMotion: "reduce" }); await login(page); const { id } = await seed(page, `Smoke accessibility ${dark ? "dark" : "light"}`, 3);
const audit = async () => { const result = await new AxeBuilder({ page }).withTags(["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"]).analyze(); expect(result.violations.map((issue) => ({ id: issue.id, nodes: issue.nodes.map((node) => node.target) }))).toEqual([]); };
for (const hash of ["", "inventory", `projects/${id}/plan`, `projects/${id}/files`, `projects/${id}/offers`, `projects/${id}/build`, "settings", "capabilities"]) { await page.goto(`/#/${hash}`); await expect(page.locator("main")).toBeVisible(); await audit(); }
await page.goto("/#/inventory"); await page.locator(".inventory-table .row-open").first().click(); await audit(); await page.keyboard.press("Escape");
await page.getByLabel("Open workspace commands").click(); await audit(); await page.keyboard.press("Escape");
await page.goto(`/#/projects/${id}/plan`); await clickProjectAction(page, "Edit project"); await audit(); await page.keyboard.press("Escape");
await page.setViewportSize({ width: 320, height: 800 }); await audit();
});
test("an unconfirmed plan save stays protected until an unchanged retry resolves it", async ({ page }) => {
await login(page); await seed(page, "Smoke unresolved plan"); await tab(page, "Build steps"); await openBuildTool(page, "print"); await page.getByRole("button", { name: "Create build plan", exact: true }).click(); await page.getByRole("button", { name: "Add build part", exact: true }).click(); await page.getByLabel("Build part 1 name", { exact: true }).fill("Bracket"); await page.getByRole("button", { name: "Review build plan", exact: true }).click();
let first = true; const keys: string[] = [];
await page.route("**/build-plan", async (route) => { if (route.request().method() !== "PUT") { await route.continue(); return; } keys.push(route.request().headers()["idempotency-key"]!); if (first) { first = false; await route.fetch(); await route.abort("failed"); } else await route.continue(); });
await page.getByRole("button", { name: "Save build plan", exact: true }).click(); await expect(page.getByRole("button", { name: "Retry unchanged plan", exact: true })).toBeVisible();
await tab(page, "Files"); await expect(page.getByRole("alertdialog", { name: "Finish the pending save" })).toBeVisible(); await expect(page.getByRole("button", { name: "Discard changes and leave" })).toHaveCount(0); await page.getByRole("button", { name: "Keep editing", exact: true }).click();
await page.getByRole("button", { name: "Retry unchanged plan", exact: true }).click(); await expect(page.getByRole("button", { name: "Revise build plan", exact: true })).toBeVisible(); expect(keys).toHaveLength(2); expect(keys[0]).toBe(keys[1]); await tab(page, "Files"); await expect(page.getByRole("alertdialog")).toHaveCount(0);
});
test("a committed workstream remains reported saved when its list refresh fails", async ({ page }) => {
await login(page); await seed(page, "Smoke saved workstream outage"); await tab(page, "Build steps"); await openBuildTool(page, "tasks"); await page.getByRole("button", { name: "Add task group", exact: true }).click(); await page.getByLabel("Task group name").fill("Saved task");
let created = false;
await page.route("**/workstreams*", async (route) => { if (route.request().method() === "POST") { const response = await route.fetch(); created = true; await route.fulfill({ response }); } else if (created) await route.fulfill({ status: 503, contentType: "application/json", body: JSON.stringify({ error: { code: "unavailable", message: "Synthetic read outage" } }) }); else await route.continue(); });
await page.getByRole("button", { name: "Create task group", exact: true }).click(); await expect(page.getByText("Task group created.", { exact: true })).toBeVisible(); await expect(page.locator(".workstream-planning").getByRole("alert")).toBeVisible(); await expect(page.getByText("No task groups recorded. Add a task group, such as firmware or assembly.", { exact: true })).toHaveCount(0);
await page.unroute("**/workstreams*"); await page.getByRole("button", { name: "Refresh task groups", exact: true }).click(); await expect(page.locator(".workstream-row")).toHaveCount(1); await expect(page.locator(".workstream-row")).toContainText("Saved task");
});
test("cancelled Back retains the destination for repeated Back and Forward", async ({ page }) => {
await login(page); const { id } = await seed(page, "Smoke retained history"); await tab(page, "Files"); await tab(page, "Build steps"); await openBuildTool(page, "tasks"); await page.getByRole("button", { name: "Add task group", exact: true }).click(); await page.getByLabel("Task group name").fill("Keep history intact");
for (let i = 0; i < 2; i++) { await page.goBack(); await expect(page.getByRole("alertdialog")).toBeVisible(); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); expect(page.url()).toContain(`/${id}/build`); }
await page.goBack(); await page.getByRole("button", { name: "Discard changes and leave", exact: true }).click(); await expect(page.getByRole("heading", { name: "Files", exact: true })).toBeVisible(); expect(page.url()).toContain(`/${id}/files`);
await page.goBack(); await expect(page.locator(".bom-section")).toBeVisible(); expect(page.url()).toContain(`/${id}/plan`); await page.goForward(); await expect(page.getByRole("heading", { name: "Files", exact: true })).toBeVisible();
await page.getByLabel("Choose files to upload").setInputFiles({ name: "forward-draft.txt", mimeType: "text/plain", buffer: Buffer.from("Synthetic forward test") });
await page.goForward(); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); expect(page.url()).toContain(`/${id}/files`);
await page.goForward(); await page.getByRole("button", { name: "Discard changes and leave", exact: true }).click(); await expect(page.getByRole("heading", { name: "Build plan", exact: true })).toBeVisible();
});
test("multi-entry traversal restores the original history position when cancelled", async ({ page }) => {
await login(page); const { id } = await seed(page, "Smoke multi-step history"); await tab(page, "Files"); await tab(page, "Build steps"); await openBuildTool(page, "tasks"); await page.getByRole("button", { name: "Add task group", exact: true }).click(); await page.getByLabel("Task group name").fill("Retain multi-step history");
await page.evaluate(() => window.history.go(-2)); await expect(page.getByRole("alertdialog")).toBeVisible(); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); expect(page.url()).toContain(`/${id}/build`);
await page.evaluate(() => window.history.go(-2)); await page.getByRole("button", { name: "Discard changes and leave", exact: true }).click(); await expect(page.locator(".bom-section")).toBeVisible(); expect(page.url()).toContain(`/${id}/plan`);
await page.goForward(); await expect(page.getByRole("heading", { name: "Files", exact: true })).toBeVisible();
});
test("changing only the observation date protects the quote provenance draft", async ({ page }) => {
await login(page); await seed(page, "Smoke observation-only draft", 1); await tab(page, "Shopping list"); await page.getByRole("button", { name: "Record quote for Café rear-panel connector", exact: true }).click(); await page.getByLabel("Observation date", { exact: true }).fill("2026-01-02");
await tab(page, "Requirements"); await expect(page.getByRole("alertdialog")).toBeVisible(); await page.getByRole("button", { name: "Keep editing", exact: true }).click(); await expect(page.getByLabel("Observation date", { exact: true })).toHaveValue("2026-01-02"); await page.getByRole("button", { name: "Cancel quote", exact: true }).click();
});