import { clickProjectAction, openProjectDetails, navigateWorkspace, openProject } from "./workspace-controls"; import { expect, test, type Page, type Response } from "@playwright/test"; const demoPassword = "demo-password-please-change"; const projectName = "E2E exact catalog build"; async function signIn(page: Page): Promise { await page.goto("/"); await page.getByLabel("Workspace password").fill(demoPassword); await page.getByRole("button", { name: "Sign in" }).click(); await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible(); } function catalogResponse(kind: "filament" | "printer", query: string) { return (response: Response): boolean => { const url = new URL(response.url()); return ( response.request().method() === "GET" && response.status() === 200 && url.pathname.endsWith("/api/v1/catalog/products") && url.searchParams.get("kind") === kind && url.searchParams.get("q") === query ); }; } function mutationResponse(path: string) { return (response: Response): boolean => response.request().method() === "POST" && response.status() === 201 && new URL(response.url()).pathname.endsWith(path); } async function addExactInventory( page: Page, category: "Printers" | "Filament", kind: "printer" | "filament", query: string, productName: RegExp, quantity: string, ): Promise { await page.getByRole("button", { name: "Add item", exact: true }).click(); const addDialog = page.getByRole("dialog", { name: "Add to inventory" }); await addDialog.getByRole("combobox", { name: "What are you adding?", exact: true }).selectOption(kind); await expect( addDialog.getByRole("combobox", { name: /Category/u }) ).toHaveCount(0); await addDialog.getByRole("button", { name: "Continue", exact: true }).click(); const productSearch = page.getByRole("combobox", { name: category === "Filament" ? "Exact filament product" : "Exact printer model", }); const searchResponse = page.waitForResponse(catalogResponse(kind, query)); await productSearch.fill(query); await searchResponse; const productOption = page.getByRole("option", { name: productName }); await expect(productOption).toBeVisible(); await productOption.click(); await page.getByLabel(category === "Filament" ? "Current mass (g)" : "Owned units").fill(quantity); await page.getByLabel("Product identity").selectOption("confirmed"); if (category === "Printers") { const details = page.getByRole("button", { name: "Printer details", exact: true }); await expect(details).toHaveAttribute("aria-expanded", "false"); await expect(page.getByLabel("Setup date")).toBeHidden(); await details.click(); await page.getByLabel("Setup date").fill("2026-09-04"); } const createResponse = page.waitForResponse(mutationResponse("/api/v1/inventory/with-product-profile")); await page.getByRole("button", { name: category === "Filament" ? "Add filament spool" : "Add printer", exact: true, }).click(); const response = await createResponse; await expect(page.getByRole("button", { name: "Close item details" })).toBeVisible(); await page.getByRole("button", { name: "Close item details" }).click(); await expect(page.getByRole("dialog")).toHaveCount(0); return response; } test("guides an exact catalog build from owned stock to an auditable setup snapshot", async ({ page }) => { test.info().annotations.push({ type: "known-product-boundary", description: "The artifact upload UI has no setup picker; it implicitly binds the current revision's saved setup, which this journey verifies from the upload request.", }); await signIn(page); await navigateWorkspace(page, "Inventory"); await expect(page.getByRole("heading", { name: "Inventory" })).toBeVisible(); const printerResponse = await addExactInventory( page, "Printers", "printer", "H2D", /Bambu Lab.*H2D/u, "2", ); const printerBody = (await printerResponse.json()) as { data?: { item?: unknown; profile?: unknown }; }; expect(printerBody.data).toMatchObject({ item: { kind: "printer", categoryNodeId: "category-printers", quantity: 2 }, profile: { catalogProductId: "catalog-printer-h2d", profileType: "printer_asset", linkState: "confirmed" }, }); expect(printerResponse.request().postDataJSON().item).toMatchObject({ kind: "printer", categoryNodeId: "category-printers" }); expect(printerResponse.request().postDataJSON().profile).not.toHaveProperty("itemId"); const filamentResponse = await addExactInventory( page, "Filament", "filament", "PETG HF", /Bambu Lab.*PETG.*Black/u, "777", ); const filamentBody = (await filamentResponse.json()) as { data?: { item?: unknown; profile?: unknown }; }; expect(filamentBody.data).toMatchObject({ item: { kind: "filament", categoryNodeId: "category-filament", quantity: 777, unit: "gram" }, profile: { catalogProductId: "catalog-filament-petg-hf", profileType: "filament_spool", linkState: "confirmed" }, }); expect(filamentResponse.request().postDataJSON().item).toMatchObject({ kind: "filament", categoryNodeId: "category-filament" }); expect(filamentResponse.request().postDataJSON().profile).not.toHaveProperty("itemId"); await navigateWorkspace(page, "Projects"); await clickProjectAction(page, "New project"); await page.getByLabel("Project name").fill(projectName); await page.getByLabel("Project goal").fill("A small exact-catalog test enclosure for the maker workflow."); const projectResponse = page.waitForResponse(mutationResponse("/api/v1/projects/with-initial-revision")); await page.getByRole("button", { name: "Create project", exact: true }).click(); const createdProjectResponse = await projectResponse; expect(createdProjectResponse.status()).toBe(201); const createdProjectBody = (await createdProjectResponse.json()) as { data?: { project?: { id?: string } }; }; const createdProjectId = createdProjectBody.data?.project?.id; expect(createdProjectId).toEqual(expect.any(String)); await expect(page.getByRole("heading", { name: projectName, exact: true })).toBeVisible(); await navigateWorkspace(page, "Settings"); await page .getByRole("switch", { name: "Technical details" }) .click(); await expect( page.getByRole("switch", { name: "Technical details" }) ).toBeVisible(); await navigateWorkspace(page, "Projects"); await openProject(page, projectName); await clickProjectAction(page, "New revision"); const revisionDialog = page.getByRole("dialog", { name: `New revision for ${projectName}` }); await expect(revisionDialog).toBeVisible(); await revisionDialog.getByLabel("Revision name").fill("Exact setup capture"); await expect(revisionDialog.getByText("How will you build it?", { exact: true })).toBeVisible(); await revisionDialog.getByRole("radio", { name: /^3D-print parts/u }).check(); await expect(revisionDialog.getByRole("combobox", { name: "Printer for this revision" })).toBeVisible(); await expect(revisionDialog.getByRole("combobox", { name: "Filament (optional technical setup)" })).toBeVisible(); await expect(revisionDialog).not.toContainText("immutable snapshot"); const printerPicker = revisionDialog.getByRole("combobox", { name: "Printer for this revision" }); await printerPicker.click(); const ownedPrinter = revisionDialog.getByRole("option").filter({ hasText: "H2D" }).filter({ hasText: "2 each" }).filter({ hasText: "Exact product confirmed" }); await expect(ownedPrinter).toHaveCount(1); await ownedPrinter.click(); const filamentPicker = revisionDialog.getByRole("combobox", { name: "Filament (optional technical setup)" }); await filamentPicker.click(); const ownedFilament = revisionDialog.getByRole("option").filter({ hasText: "PETG HF" }).filter({ hasText: "777 g" }).filter({ hasText: "Exact product confirmed" }); await expect(ownedFilament).toHaveCount(1); await ownedFilament.click(); await expect( revisionDialog.locator("[data-disclosure=trigger]").filter({ hasText: "Technical details" }) ).toBeVisible(); await revisionDialog.getByLabel("Hotend side").fill("single nozzle"); await revisionDialog.getByLabel("Nozzle diameter (mm)").fill("0.4"); await revisionDialog.getByLabel("Nozzle material").fill("hardened steel"); await revisionDialog.getByLabel("Build plate").fill("Textured PEI"); await revisionDialog.getByLabel("Accessories").fill("AMS 2 Pro"); await revisionDialog.getByLabel("Firmware").fill("01.08"); await revisionDialog.getByRole("textbox", { name: "Slicer", exact: true }).fill("Bambu Studio"); await revisionDialog.getByRole("textbox", { name: "Slicer version", exact: true }).fill("1.10.0"); await revisionDialog.getByLabel("Profile").fill("0.20 mm Standard"); await revisionDialog.getByLabel("Calibration state").fill("flow checked"); await revisionDialog.getByLabel("Explicit unknowns").fill("first-layer coupon"); const revisionResponse = page.waitForResponse((response) => { const url = new URL(response.url()); return ( response.request().method() === "POST" && response.status() === 201 && /\/api\/v1\/projects\/[^/]+\/revisions$/u.test(url.pathname) ); }); const snapshotResponse = page.waitForResponse((response) => { const url = new URL(response.url()); return ( response.request().method() === "POST" && response.status() === 201 && /\/api\/v1\/project-revisions\/[^/]+\/build-configurations$/u.test(url.pathname) ); }); await revisionDialog.getByRole("button", { name: "Create revision", exact: true }).click(); const [createdRevisionResponse, createdSnapshotResponse] = await Promise.all([revisionResponse, snapshotResponse]); const createdRevisionBody = (await createdRevisionResponse.json()) as { data?: { id?: string; projectId?: string }; }; const snapshotBody = (await createdSnapshotResponse.json()) as { data?: Record; }; const snapshot = snapshotBody.data; expect(createdRevisionBody.data?.id).toBeTruthy(); expect(snapshot).toMatchObject({ projectRevisionId: createdRevisionBody.data?.id, printerItemSnapshot: { catalogProductId: "catalog-printer-h2d", linkState: "confirmed" }, filamentSelections: [{ catalogProductId: "catalog-filament-petg-hf", linkState: "confirmed" }], activeHotend: "single nozzle", nozzle: { diameterMm: 0.4, material: "hardened steel" }, plate: "Textured PEI", slicer: { name: "Bambu Studio", version: "1.10.0" }, explicitUnknowns: ["first-layer coupon"], }); expect(snapshot?.id).toEqual(expect.any(String)); expect(snapshot?.contentSha256).toMatch(/^[a-f0-9]{64}$/u); expect(snapshot?.projectId).toBeUndefined(); await expect(page.getByRole("dialog")).toHaveCount(0); await page.reload(); await expect(page.getByRole("heading", { name: projectName, exact: true })).toBeVisible(); await openProject(page, projectName); await expect(page.getByRole("heading", { name: projectName, exact: true })).toBeVisible(); await openProjectDetails(page); const buildApproach = page.getByRole("region", { name: "Build approach" }); await expect(buildApproach).toContainText("Bambu Lab H2D"); await expect( page.getByRole("region", { name: "Build setup summary" }) ).toBeVisible(); await navigateWorkspace(page, "Settings"); await expect( page.getByRole("switch", { name: "Technical details" })).toBeVisible(); await navigateWorkspace(page, "Projects"); await openProject(page, projectName); await openProjectDetails(page); const expertSummary = page.getByRole("region", { name: "Build setup summary" }); await expect(expertSummary).toContainText( "Use Bambu Lab · H2D with Bambu Lab · PETG · PETG HF." ); await expect(expertSummary).toContainText( "Print setup: 0.4 mm nozzle · hardened steel · Textured PEI." ); await expect(expertSummary).toContainText( "Software: Bambu Studio 1.10.0 0.20 mm Standard." ); await expect(expertSummary).toContainText("Calibration: flow checked."); await expect( expertSummary.getByText("Show IDs, versions, evidence & unknowns", { exact: true })).toBeVisible(); await expertSummary .getByText("Show IDs, versions, evidence & unknowns", { exact: true }).click(); await expect(expertSummary).toContainText("Revision ID"); await expect(expertSummary).toContainText("Printer product"); await expect(expertSummary).toContainText("Filament product"); await expect(expertSummary).toContainText("confirmed · confirmed"); await expect(expertSummary).toContainText("first-layer coupon"); await expect( expertSummary .locator("code").filter({ hasText: /^[a-f0-9]{64}(?: · [a-f0-9]{64})?$/u })).toHaveCount(1); const snapshotId = typeof snapshot?.id === "string" ? snapshot.id : ""; expect(snapshotId).not.toBe(""); const readPersistedSnapshot = async () => page.evaluate(async (id) => { const response = await fetch(`/api/v1/build-configurations/${encodeURIComponent(id)}`); return { status: response.status, body: (await response.json()) as Record }; }, snapshotId); const persistedBeforeUpload = await readPersistedSnapshot(); expect(persistedBeforeUpload.status).toBe(200); expect(persistedBeforeUpload.body).toMatchObject({ id: snapshotId, contentSha256: snapshot?.contentSha256, projectRevisionId: snapshot?.projectRevisionId, printerItemSnapshot: snapshot?.printerItemSnapshot, filamentSelections: snapshot?.filamentSelections, }); await page.getByRole("tab", { name: "Files", exact: true }).click(); const fileChooser = page.waitForEvent("filechooser"); await page.getByRole("button", { name: "Add files", exact: true }).click(); await (await fileChooser).setFiles({ name: "e2e-bound-setup.step", mimeType: "model/step", buffer: Buffer.from("ISO-10303-21;\nHEADER;\nENDSEC;\nDATA;\nENDSEC;\nEND-ISO-10303-21;\n"), }); await expect(page.getByRole("button", { name: "Add 1 file", exact: true })).toBeEnabled(); const beginUploadResponse = page.waitForResponse((response) => { const url = new URL(response.url()); return ( response.request().method() === "POST" && response.status() === 201 && url.pathname.endsWith("/api/v1/artifacts/uploads") ); }); const finalizeUploadResponse = page.waitForResponse((response) => { const url = new URL(response.url()); return ( response.request().method() === "POST" && response.status() === 200 && /\/api\/v1\/artifacts\/uploads\/[^/]+\/finalize$/u.test(url.pathname) ); }); await page.getByRole("button", { name: "Add 1 file", exact: true }).click(); const [beginUpload, finalizeUpload] = await Promise.all([beginUploadResponse, finalizeUploadResponse]); const beginUploadBody = beginUpload.request().postDataJSON() as Record; expect(beginUploadBody).toMatchObject({ projectId: createdProjectId, projectRevisionId: snapshot?.projectRevisionId, filename: "e2e-bound-setup.step", buildConfigurationSnapshotId: snapshotId, }); const finalizedUploadBody = (await finalizeUpload.json()) as { data?: Record; }; expect(finalizedUploadBody.data).toMatchObject({ projectId: createdProjectId, revisionId: snapshot?.projectRevisionId, filename: "e2e-bound-setup.step", }); await expect(page.getByRole("status").filter({ hasText: "1 of 1 file uploaded" })).toBeVisible(); await expect(page.getByRole("tab", { name: "Files", exact: true })).toBeVisible(); await expect(page.locator(".project-file-name").filter({ hasText: /e2e-bound-setup\.step/u })).toBeVisible(); const persistedAfterUpload = await readPersistedSnapshot(); expect(persistedAfterUpload.status).toBe(200); expect(persistedAfterUpload.body).toMatchObject({ id: snapshotId, contentSha256: persistedBeforeUpload.body.contentSha256, projectRevisionId: persistedBeforeUpload.body.projectRevisionId, printerItemSnapshot: persistedBeforeUpload.body.printerItemSnapshot, filamentSelections: persistedBeforeUpload.body.filamentSelections, }); }); test("keeps the starter catalog facet path accessible and honest at 390px", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await signIn(page); await navigateWorkspace(page, "Inventory"); await page.getByRole("button", { name: "Add item", exact: true }).click(); const selectionDialog = page.getByRole("dialog", { name: "Add to inventory" }); const completeCatalog = page.waitForResponse((response) => { const url = new URL(response.url()); return ( response.request().method() === "GET" && response.status() === 200 && url.pathname.endsWith("/api/v1/catalog/products") && url.searchParams.get("kind") === "filament" && url.searchParams.get("limit") === "100" && !url.searchParams.has("q") ); }); await selectionDialog.getByRole("combobox", { name: "What are you adding?", exact: true }).selectOption("filament"); await expect( selectionDialog.getByRole("combobox", { name: /Category/u }) ).toHaveCount(0); await selectionDialog.getByRole("button", { name: "Continue", exact: true }).click(); await completeCatalog; const addDialog = page.getByRole("dialog", { name: "Add filament" }); await addDialog.getByRole("button", { name: "Find by product details" }).click(); await addDialog.getByLabel("Manufacturer / brand").selectOption({ label: "Bambu Lab" }); await addDialog.getByLabel("Product line / material family").selectOption({ label: "PETG" }); const materialSubtype = addDialog.getByLabel("Material subtype"); if (await materialSubtype.locator("option").filter({ hasText: "HF" }).count()) { await materialSubtype.selectOption({ label: "HF" }); } await addDialog.locator("#catalog-facet-filament-colour").selectOption({ label: "Black" }); await addDialog.locator("#catalog-facet-filament-diameterMm").selectOption({ label: "1.75 mm" }); await addDialog.locator("#catalog-facet-filament-netMassG").selectOption({ label: "1,000 g net" }); await expect(addDialog.getByRole("listbox", { name: "Exact filament products" })).toContainText("PETG HF"); await expect(addDialog).toContainText("Catalog entries describe products only"); await expect(addDialog).not.toContainText(/(?:In stock|Available now|Owned product)/u); const dialogBox = await addDialog.boundingBox(); const viewport = page.viewportSize()!; expect(dialogBox).not.toBeNull(); expect(dialogBox!.x).toBeGreaterThanOrEqual(0); expect(dialogBox!.y).toBeGreaterThanOrEqual(0); expect(dialogBox!.width).toBeCloseTo(viewport.width, 0); expect(dialogBox!.x + dialogBox!.width).toBeLessThanOrEqual(viewport.width + 1); expect(dialogBox!.y + dialogBox!.height).toBeLessThanOrEqual(viewport.height + 1); expect(await addDialog.evaluate(element => element.scrollWidth <= element.clientWidth + 1)).toBe(true); expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true); });