Ctrl K
ComunidadGuías…

e2e/exact-catalog-project.spec.ts

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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<void> {
  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<Response> {
  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<string, unknown>; };
  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<string, unknown> };
  }, 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<string, unknown>;
  expect(beginUploadBody).toMatchObject({
    projectId: createdProjectId,
    projectRevisionId: snapshot?.projectRevisionId,
    filename: "e2e-bound-setup.step",
    buildConfigurationSnapshotId: snapshotId,
  });
  const finalizedUploadBody = (await finalizeUpload.json()) as { data?: Record<string, unknown>; };
  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);
});