e2e/approved-inventory-screens.spec.ts
Rama main · réplica de lectura del repositorio original.
import { expect, test, type Locator, type Page, type TestInfo } from "@playwright/test";
import { navigateWorkspace } from "./workspace-controls";
async function expectWithinViewport(page: Page, surface: Locator) {
await expect(surface).toBeVisible();
await expect(async () => {
const bounds = await surface.boundingBox();
const viewport = page.viewportSize()!;
expect(bounds).not.toBeNull();
expect(bounds!.x).toBeGreaterThanOrEqual(0);
expect(bounds!.y).toBeGreaterThanOrEqual(0);
expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(viewport.width + 1);
expect(bounds!.y + bounds!.height).toBeLessThanOrEqual(viewport.height + 1);
expect(await surface.evaluate(element => element.scrollWidth <= element.clientWidth + 1)).toBe(true);
}).toPass({ timeout: 5_000 });
}
async function expectReadableControl(page: Page, control: Locator) {
await expect(control).toBeVisible();
await control.scrollIntoViewIfNeeded();
await expect(control).toBeInViewport({ ratio: 1 });
const metrics = await control.evaluate(element => ({
fontSize: Number.parseFloat(getComputedStyle(element).fontSize),
height: element.getBoundingClientRect().height,
isField: element.matches("input, select, textarea"),
}));
const phone = page.viewportSize()!.width === 390;
expect(metrics.fontSize).toBeGreaterThanOrEqual(phone && metrics.isField ? 16 : 14);
expect(metrics.height).toBeGreaterThanOrEqual(phone ? 44 : 36);
}
async function capture(page: Page, info: TestInfo, name: string, surface?: Locator) {
await expect.poll(() => page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
if (surface) await expectWithinViewport(page, surface);
await page.screenshot({ path: info.outputPath(`${name}.png`), fullPage: false, animations: "disabled" });
}
for (const viewport of [{ width: 1536, height: 1024 }, { width: 390, height: 844 }]) {
test(`approved inventory detail and capture remain usable at ${viewport.width}px`, async ({ page }, info) => {
await page.setViewportSize(viewport);
await page.emulateMedia({ reducedMotion: "reduce" });
const pageErrors: string[] = [];
page.on("pageerror", error => pageErrors.push(error.message));
// Every stock response and count write belongs to this browser-only fixture.
// No physical-count observation is sent to the shared demo workspace.
let item = {
id: "synthetic-approved-inventory-item", name: "Synthetic M3 socket screws", kind: "fastener",
categoryNodeId: "category-fasteners", quantity: 12, availableQuantity: 10, allocatedQuantity: 2,
unit: "each", location: "Synthetic drawer A3", manufacturer: "Example Hardware", model: "M3 × 8 mm",
sku: "SYNTH-M3-8", condition: "good", description: "Check the thread and length against the project part.",
tags: ["synthetic fixture"], links: [],
evidence: { state: "physically_counted", source: "Synthetic fixture", observedAt: "2026-10-03T08:00:00Z" },
createdAt: "2026-10-03T08:00:00Z", updatedAt: "2026-10-03T08:00:00Z", version: 3,
};
const countWrites: Array<{ key: string | undefined; payload: unknown }> = [];
const unexpectedWrites: string[] = [];
await page.route(/\/api\/v1\/inventory(?:\?.*)?$/u, async route => {
if (route.request().method() !== "GET") {
unexpectedWrites.push(route.request().method());
return route.abort("blockedbyclient");
}
return route.fulfill({ json: { data: [item], total: 1, limit: 25 } });
});
await page.route(`**/api/v1/inventory/${item.id}`, route => route.fulfill({ json: item }));
await page.route(`**/api/v1/inventory/${item.id}/images`, route => route.fulfill({ json: { itemId: item.id, version: 0, images: [] } }));
await page.route(`**/api/v1/inventory/${item.id}/count`, async route => {
countWrites.push({ key: route.request().headers()["idempotency-key"], payload: route.request().postDataJSON() });
item = { ...item, quantity: 7, availableQuantity: 5, updatedAt: "2026-10-04T08:00:00Z", version: 4,
evidence: { ...item.evidence, observedAt: "2026-10-04T08:00:00Z" } };
return route.fulfill({ json: { data: { item, event: { id: "synthetic-approved-count-event" } } } });
});
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();
await navigateWorkspace(page, "Inventory");
const row = page.locator(".inventory-table tbody tr").filter({ hasText: item.name });
await expect(page.locator(".inventory-page-status")).toHaveText("Showing 1 of 1 items");
await expect(row).toBeVisible();
await expect(page.getByRole("textbox", { name: "Search inventory", exact: true })).toHaveCount(1);
await expect(row.locator(".inventory-available-cell")).toContainText("10 pieces");
await expect(row.locator(".inventory-available-cell")).toContainText("2 pieces reserved");
await capture(page, info, "inventory-loaded");
await row.locator(".table-item").click();
const stock = page.getByRole("dialog", { name: item.name, exact: true });
if (viewport.width === 1536) {
const inspector = page.getByRole("complementary", { name: "Inventory inspector", exact: true });
await expect(inspector.getByRole("heading", { name: item.name, exact: true })).toBeVisible();
await expect(inspector).toContainText("10 pieces available");
await expect(inspector).toContainText("12 pieces confirmed · 2 pieces reserved");
await expect(inspector).toContainText(item.location);
await expect(inspector).toContainText("2026-10-03");
await expect(inspector.getByRole("region", { name: "Needed by projects", exact: true })).toBeVisible();
await expect(page.getByRole("dialog")).toHaveCount(0);
await expectReadableControl(page, inspector.getByRole("button", { name: "Count stock", exact: true }));
await capture(page, info, "inventory-selected-detail", inspector);
await inspector.getByRole("button", { name: "Identity & compatibility", exact: true }).click();
await expect(inspector).toContainText(item.sku);
await expect(inspector).toContainText(item.id);
const openDetails = inspector.getByRole("button", { name: "Open item details", exact: true });
await expectReadableControl(page, openDetails);
await expect.poll(() => inspector.evaluate(element => element.scrollTop)).toBeGreaterThan(0);
await expect(inspector.getByRole("button", { name: "Hide item inspector", exact: true })).toBeInViewport({ ratio: 1 });
await capture(page, info, "inventory-detail-scrolled", inspector);
await inspector.getByRole("button", { name: "Identity & compatibility", exact: true }).click();
await inspector.getByRole("button", { name: "Count stock", exact: true }).click();
// Starting a new count must not silently reuse the previous observation.
await expect(stock.getByLabel("Counted quantity", { exact: true })).toHaveValue("");
} else {
await expect(stock).toBeVisible();
await expect(stock).toContainText(item.location);
const onHand = stock.getByRole("region", { name: "Stock on hand", exact: true });
await expect(onHand.getByText("12 pieces on hand.", { exact: true })).toBeInViewport({ ratio: 1 });
await expect(onHand.getByText("Physically counted", { exact: true })).toBeInViewport({ ratio: 1 });
await expect(stock).toContainText("2 pieces reserved; 10 pieces currently available for reuse");
await expect(stock.getByRole("region", { name: "Needed by projects", exact: true })).toBeVisible();
await expectReadableControl(page, stock.getByRole("button", { name: "Count stock", exact: true }));
await capture(page, info, "inventory-selected-detail", stock);
await stock.getByRole("button", { name: "Count stock", exact: true }).click();
}
const quantity = stock.getByLabel("Counted quantity", { exact: true });
await expectReadableControl(page, quantity);
await quantity.fill("7");
await expect(stock).toContainText("Recorded condition: good. A count changes quantity only.");
await expectReadableControl(page, stock.getByRole("button", { name: "Review physical count", exact: true }));
await capture(page, info, "inventory-count-form", stock);
await stock.getByRole("button", { name: "Review physical count", exact: true }).click();
const review = page.getByRole("alertdialog", { name: "Review physical count", exact: true });
await expect(review).toContainText(item.name);
await expect(review).toContainText(`${item.manufacturer} · ${item.model} · ${item.sku}`);
await expect(review).toContainText(item.location);
await expect(review).toContainText(`Record: ${item.id}`);
await expect(review.getByText("Before this count", { exact: true }).locator("..")).toContainText("12 pieces");
await expect(review.getByText("After this count", { exact: true }).locator("..")).toContainText("7 pieces");
await expect(review).toContainText("Reservations and recorded condition stay unchanged.");
await expect(review).toContainText("This count does not confirm that the item fits a project.");
expect(countWrites).toHaveLength(0);
await expectReadableControl(page, review.getByRole("button", { name: "Confirm physical count", exact: true }));
await capture(page, info, "inventory-count-review", review);
await review.getByRole("button", { name: "Back to item", exact: true }).click();
await expect(quantity).toHaveValue("7");
expect(countWrites).toHaveLength(0);
await stock.getByRole("button", { name: "Review physical count", exact: true }).click();
await review.getByRole("button", { name: "Confirm physical count", exact: true }).click();
await expect(review).toHaveCount(0);
const receipt = stock.getByRole("status");
await expect(receipt).toContainText("Physical count saved");
await expect(receipt).toContainText("Confirmed 7 pieces as the on-hand quantity.");
await expect(receipt).toContainText(item.location);
await expect(receipt).toContainText("Compatibility with a project is checked separately.");
await expect(quantity).toHaveCount(0);
await expect(stock.getByRole("button", { name: "Done", exact: true })).toHaveCount(1);
await expectReadableControl(page, stock.getByRole("button", { name: "Done", exact: true }));
await capture(page, info, "inventory-count-receipt", stock);
expect(countWrites).toHaveLength(1);
expect(countWrites[0]!.key).toBeTruthy();
expect(countWrites[0]!.payload).toEqual({ quantity: 7 });
await stock.getByRole("button", { name: "Done", exact: true }).click();
await expect(stock).toHaveCount(0);
await expect(row.locator(".inventory-available-cell")).toContainText("5 pieces");
await expect(row.locator(".inventory-available-cell")).toContainText("2 pieces reserved");
await page.getByRole("button", { name: "Add item", exact: true }).click();
const choose = page.getByRole("dialog", { name: "Add to inventory", exact: true });
await expectReadableControl(page, choose.getByRole("combobox", { name: "What are you adding?", exact: true }));
await choose.getByRole("combobox", { name: "What are you adding?", exact: true }).selectOption("electronic");
await expect(choose.getByRole("button", { name: "Continue", exact: true })).toBeEnabled();
await capture(page, info, "inventory-create-type", choose);
await choose.getByRole("button", { name: "Continue", exact: true }).click();
const create = page.getByRole("dialog", { name: "Add an inventory item", exact: true });
const name = create.getByLabel("Name", { exact: true });
const recorded = create.getByLabel("Recorded quantity", { exact: true });
const location = create.getByLabel("Location (optional)", { exact: true });
const identity = create.getByRole("button", { name: "Specifications & identity", exact: true });
await expect(identity).toHaveAttribute("aria-expanded", "false");
await expect(create.getByLabel("Product code (optional)", { exact: true })).toBeHidden();
await expect(create.getByRole("checkbox", { name: "I have counted these", exact: true })).not.toBeChecked();
await expect(create).toContainText("The quantity stays unconfirmed until you count it.");
for (const control of [name, recorded, create.getByRole("combobox", { name: "Unit", exact: true }), location]) await expectReadableControl(page, control);
await name.fill("Synthetic spare controller");
await recorded.fill("3");
await location.fill("Synthetic drawer B2");
await expectReadableControl(page, create.getByRole("button", { name: "Add item", exact: true }));
await name.scrollIntoViewIfNeeded();
await capture(page, info, "inventory-create-primary", create);
await identity.click();
await expect(identity).toHaveAttribute("aria-expanded", "true");
const maker = create.getByLabel("Brand or manufacturer (if known)", { exact: true });
const model = create.getByLabel("Model (if known)", { exact: true });
const code = create.getByLabel("Product code (optional)", { exact: true });
for (const control of [maker, model, code]) await expectReadableControl(page, control);
await maker.fill("Example Components");
await model.fill("SYNTH-3.3V");
await code.fill("SYNTH-CONTROL-01");
await capture(page, info, "inventory-create-identity", create);
await create.getByRole("button", { name: "Cancel", exact: true }).click();
const discard = page.getByRole("alertdialog", { name: "Discard this item draft?", exact: true });
await expect(discard).toContainText("Your item has not been added.");
await capture(page, info, "inventory-create-discard", discard);
await discard.getByRole("button", { name: "Keep editing", exact: true }).click();
await expect(name).toHaveValue("Synthetic spare controller");
await expect(recorded).toHaveValue("3");
await expect(location).toHaveValue("Synthetic drawer B2");
await expect(code).toHaveValue("SYNTH-CONTROL-01");
await create.getByRole("button", { name: "Cancel", exact: true }).click();
await discard.getByRole("button", { name: "Discard draft", exact: true }).click();
await expect(create).toHaveCount(0);
await expect(page.getByRole("alertdialog")).toHaveCount(0);
expect(countWrites).toHaveLength(1);
expect(unexpectedWrites).toEqual([]);
expect(pageErrors).toEqual([]);
await capture(page, info, "inventory-return");
});
}