Ctrl K
ComunidadGuías…

e2e/inventory-workspace.spec.ts

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

Archivo original ↗Preguntar a KiBot
import { navigateWorkspace } from "./workspace-controls";
import { expect, test, type Page } from "@playwright/test";
import AxeBuilder from "@axe-core/playwright";

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();
}
const records = Array.from({ length: 32 }, (_, i) => ({ id: `inspector-part-${i}`, name: `Inspector part ${String(i).padStart(2,"0")}`, kind: "electronic", quantity: 10, availableQuantity: i < 28 ? 0 : 8, allocatedQuantity: i < 28 ? 0 : 2, unit: "each", location: i % 2 === 0 ? "Drawer A" : "Drawer B", manufacturer: "Example Components", model: `EX-${i}`, tags: ["test fixture"], links: [], evidence: { state: i < 28 ? "delivered_uncounted" : "physically_counted", source: "synthetic fixture", observedAt: "2026-09-19T08:00:00Z" }, createdAt: "2026-09-19T08:00:00Z", updatedAt: "2026-09-19T08:00:00Z", version: 1 }));
async function fixtureInventory(page: Page) {
  await page.route("**/api/v1/inventory?**", async (route) => {
    const url = new URL(route.request().url());
    const query = url.searchParams;
    let rows = records.filter((row) => (!query.get("q") || row.name.toLowerCase().includes(query.get("q")!.toLowerCase())) && (!query.get("location") || row.location === query.get("location")) && (query.get("stockView") === "available" || query.get("stockView") === "reserved" ? row.availableQuantity > 0 : query.get("stockView") === "check" ? row.availableQuantity === 0 : query.get("stockView") === "depleted" ? false : true));
    rows = rows.sort((a,b) => (query.get("sort") === "name_desc" ? -1 : 1) * a.name.localeCompare(b.name));
    const offset = Number(query.get("cursor") ?? "0"), limit = Number(query.get("limit") ?? "25");
    const data = rows.slice(offset, offset + limit);
    await route.fulfill({ json: { data, limit, total: rows.length, ...(offset + data.length < rows.length ? { nextCursor: String(offset + data.length) } : {}) } });
  });
}

test("inventory inspector supports keyboard review, explicit editing and clipboard fallback", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 1000 });
  await page.addInitScript(() => Object.defineProperty(navigator, "clipboard", { configurable: true, value: undefined }));
  await login(page); await fixtureInventory(page);
  await navigateWorkspace(page, "Inventory");
  const inspector = page.getByRole("complementary", { name: "Inventory inspector" });
  await expect(inspector).toHaveCount(0);
  await page.locator(".inventory-table .table-item").filter({ hasText: "Inspector part 00" }).click();
  await expect(inspector.getByRole("heading", { name: "Inspector part 00", exact: true })).toBeVisible();
  await expect(inspector).toContainText("not been physically confirmed");
  const first = page.locator(".inventory-table tbody tr").first();
  await first.focus(); await page.keyboard.press("ArrowDown");
  await expect(inspector.getByRole("heading", { name: "Inspector part 01", exact: true })).toBeVisible();
  await expect(page.getByRole("dialog")).toHaveCount(0);
  await page.keyboard.press("Enter");
  await expect(page.getByRole("dialog")).toBeVisible();
  await page.keyboard.press("Escape");
  await inspector.getByRole("button", { name: "Use with an AI assistant" }).click();
  await inspector.getByRole("button", { name: "Copy for AI", exact: true }).click();
  const brief = JSON.parse(await inspector.getByRole("textbox", { name: "Inventory brief" }).inputValue());
  expect(brief.items).toHaveLength(1); expect(brief.items[0].id).toBe("inspector-part-1");
  expect(brief.items[0].availableQuantity).toBe(0); expect(brief.scope).toContain("not a complete inventory");
  await inspector.getByRole("button", { name: "Hide item inspector" }).click();
  await expect(inspector).toHaveCount(0);
  await page.locator(".inventory-table .table-item").filter({ hasText: "Inspector part 02" }).click();
  await expect(inspector.getByRole("heading", { name: "Inspector part 02", exact: true })).toBeVisible();
});

test("stock views sort before paging, clear bulk selections, and persist saved filters through reload", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 1000 });
  await login(page); await fixtureInventory(page);
  await navigateWorkspace(page, "Inventory");
  await expect(page.locator(".inventory-page-status")).toHaveText("Showing 25 of 32 items");
  await page.getByRole("button", { name: "Load more", exact: true }).click();
  await expect(page.locator(".inventory-page-status")).toHaveText("Showing 32 of 32 items");
  await page.getByRole("checkbox", { name: /^Select Inspector part 00/u }).check();
  await page.getByRole("navigation", { name: "Inventory stock views", exact: true }).getByRole("button", { name: "Available", exact: true }).click();
  await expect(page.locator(".inventory-page-status")).toHaveText("Showing 4 of 4 items");
  await expect(page.getByRole("button", { name: "Bulk edit", exact: true })).toHaveCount(0);
  await expect(page.getByText("Selection cleared because the search or filters changed.")).toBeVisible();
  await page.getByRole("button", { name: "View options", exact: true }).click();
  await page.getByLabel("Sort inventory", { exact: true }).selectOption("name_desc");
  await expect(page.locator(".inventory-table tbody tr").first()).toContainText("Inspector part 31");
  await page.keyboard.press("Escape");
  await page.getByRole("button", { name: "Filters", exact: true }).click();
  await page.getByLabel("Filter inventory by exact location").fill("Drawer B");
  await expect(page.locator(".inventory-page-status")).toHaveText("Showing 2 of 2 items");
  await page.getByRole("button", { name: "View options", exact: true }).click();
  await page.getByRole("button", { name: "Save view…", exact: true }).click();
  await page.getByLabel("Inventory view name").fill("Available boards");
  await page.getByRole("button", { name: "Save view", exact: true }).click();
  await expect(page.getByLabel("Saved inventory view")).toHaveValue("Available boards");
  await page.getByRole("button", { name: "Reset filters", exact: true }).click();
  await expect(page.locator(".inventory-page-status")).toHaveText("Showing 25 of 32 items");
  await page.getByLabel("Saved inventory view").selectOption("Available boards");
  await expect(page.locator(".inventory-page-status")).toHaveText("Showing 2 of 2 items");
  await page.reload();
  await page.getByRole("button", { name: "View options", exact: true }).click();
  await expect(page.getByLabel("Saved inventory view")).toHaveValue("Available boards");
  await expect(page.locator(".inventory-table tbody tr").first()).toContainText("Inspector part 31");
  await expect(page.getByLabel("Sort inventory", { exact: true })).toHaveValue("name_desc");
  await page.getByRole("button", { name: "Remove saved view", exact: true }).click();
  await expect(page.getByLabel("Saved inventory view")).toHaveValue("");
});

test("inventory register and inspector remain accessible in light, dark and narrow layouts", async ({ page }) => {
  await page.emulateMedia({ reducedMotion: "reduce" });
  await page.setViewportSize({ width: 1440, height: 1000 });
  await login(page); await fixtureInventory(page);
  await navigateWorkspace(page, "Inventory");
  await expect(page.locator(".inventory-page-status")).toHaveText("Showing 25 of 32 items");
  // These evidence columns remain available on demand in the compact register.
  // Enable them explicitly so the narrow-layout checks cover every stock field.
  await page.getByRole("button", { name: "View options", exact: true }).click();
  const columns = page.getByRole("group", { name: "Additional columns" });
  await columns.getByRole("checkbox", { name: "Recorded", exact: true }).check();
  await columns.getByRole("checkbox", { name: "Status", exact: true }).check();
  await page.keyboard.press("Escape");
  await page.locator(".inventory-table .table-item").filter({ hasText: "Inspector part 00" }).click();
  await expect(page.getByRole("complementary", { name: "Inventory inspector" })).toBeVisible();
  for (const theme of ["light", "dark"]) {
    await page.evaluate((value) => document.documentElement.dataset.theme = value, theme);
    const results = await new AxeBuilder({ page }).include(".inventory-workstation").withTags(["wcag2a", "wcag2aa", "wcag21aa"]).analyze();
    expect(results.violations).toEqual([]);
  }
  await page.getByRole("button", { name: "Hide item inspector", exact: true }).click();
  for (const width of [768, 390, 320]) {
    await page.setViewportSize({ width, height: 900 });
    expect(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth)).toBe(true);
    const firstRow = page.locator(".inventory-table tbody tr").first();
    for (const cell of [".inventory-name-cell", ".inventory-recorded-cell", ".inventory-available-cell", ".inventory-status-cell"]) {
      // Responsive panel changes can remount the table; retry the full
      // measurement while keeping every stock/evidence cell within the viewport.
      await expect(async () => {
        await firstRow.locator(cell).scrollIntoViewIfNeeded();
        const bounds = await firstRow.locator(cell).boundingBox();
        expect(bounds).not.toBeNull();
        expect(bounds!.x).toBeGreaterThanOrEqual(0);
        expect(bounds!.x + bounds!.width).toBeLessThanOrEqual(width);
      }).toPass({ timeout: 5_000 });
    }
    await page.getByRole("columnheader", { name: "Item", exact: true }).getByRole("button").focus();
    await page.keyboard.press("Tab");
    const locationHeader = page.getByRole("columnheader", { name: "Location", exact: true });
    await expect(locationHeader.getByRole("button")).toBeFocused();
    expect(await locationHeader.evaluate((element) => getComputedStyle(element).clipPath)).toBe("none");
    expect((await locationHeader.boundingBox())!.width).toBeGreaterThan(20);
    const inspect = page.locator(".inventory-table .table-item").filter({ hasText: "Inspector part 01" });
    await inspect.click();
    await expect(page.getByRole("dialog").getByRole("heading", { name: "Inspector part 01", exact: true })).toBeVisible();
    await expect(page.getByLabel("Counted quantity")).toBeVisible();
    await page.getByRole("button", { name: "Close item details" }).click();
  }
});

test("inventory keeps categories in local filters and restores configurable desktop layout", async ({ page }) => {
  await page.setViewportSize({ width: 1440, height: 1000 });
  await login(page); await fixtureInventory(page);
  await navigateWorkspace(page, "Inventory");
  await expect(page.getByRole("region", { name: "Inventory navigator" })).toHaveCount(0);
  await expect(page.getByRole("region", { name: "Project navigator" })).toHaveCount(0);
  await expect(page.getByRole("textbox", { name: "Search inventory", exact: true })).toHaveCount(1);
  await expect(page.getByRole("navigation", { name: "Inventory stock views", exact: true }).getByRole("button")).toHaveText(["All", "Available", "Needs checking", "Reserved"]);
  await page.getByRole("button", { name: "Filters", exact: true }).click();
  await page.getByLabel("Filter inventory by category").selectOption("category-electronics");
  await expect(page.getByLabel("Filter inventory by category")).toHaveValue("category-electronics");
  await expect(page).toHaveURL(/categoryNodeId=category-electronics/);
  await expect(page.locator(".inventory-active-filters")).toContainText("Electronics");
  await page.getByLabel("Filter inventory by category").selectOption("");
  await expect(page.getByLabel("Filter inventory by category")).toHaveValue("");
  await expect(page).not.toHaveURL(/categoryNodeId=/);
  await page.getByRole("button", { name: "Filters", exact: true }).click();
  const item = page.locator(".inventory-table .table-item").filter({ hasText: "Inspector part 02" });
  await item.click();
  await expect(page.getByRole("dialog")).toHaveCount(0);
  await expect(page.getByRole("complementary", { name: "Inventory inspector" })).toContainText("Inspector part 02");
  await item.dblclick(); await expect(page.getByRole("dialog")).toBeVisible(); await page.keyboard.press("Escape");
  await item.focus(); await page.keyboard.press("F2"); await expect(page.getByRole("dialog")).toBeVisible(); await page.keyboard.press("Escape");
  await page.getByRole("columnheader", { name: "Item", exact: true }).getByRole("button").click();
  await expect(page.locator(".inventory-table tbody tr").first()).toContainText("Inspector part 31");
  await page.getByRole("button", { name: "View options", exact: true }).click();
  await expect(page.getByLabel("Sort inventory", { exact: true })).toHaveValue("name_desc");
  const columns = page.getByRole("group", { name: "Additional columns" });
  await columns.getByRole("checkbox", { name: "SKU / part number" }).check();
  await columns.getByRole("checkbox", { name: "Location", exact: true }).uncheck();
  await expect(page.getByRole("columnheader", { name: "SKU / part number" })).toBeVisible();
  await expect(page.getByRole("columnheader", { name: "Location", exact: true })).toHaveCount(0);
  const splitter = page.getByRole("separator", { name: "Resize item inspector" });
  await page.keyboard.press("Escape");
  await expect(page.getByRole("dialog", { name: "Inventory view options" })).toHaveCount(0);
  await expect(page.getByRole("button", { name: "View options", exact: true })).toBeFocused();
  const panel = page.locator('[data-slot="resizable-panel"]').last();
  const before = (await panel.boundingBox())!.width;
  await splitter.focus(); await page.keyboard.press("ArrowLeft");
  await expect.poll(async () => (await panel.boundingBox())!.width).toBeGreaterThan(before);
  const firstKeyboardWidth = (await panel.boundingBox())!.width;
  await expect.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem("benchledger.inventory.layout.v1.workspace") ?? "{}").inspectorWidth)).toBe(Math.round(firstKeyboardWidth));
  await page.keyboard.press("ArrowRight");
  await expect.poll(async () => (await panel.boundingBox())!.width).toBeLessThan(firstKeyboardWidth);
  const resized = (await panel.boundingBox())!.width;
  await expect.poll(() => page.evaluate(() => JSON.parse(localStorage.getItem("benchledger.inventory.layout.v1.workspace") ?? "{}").inspectorWidth)).toBe(Math.round(resized));
  const bounds = await splitter.boundingBox();
  await page.mouse.move(bounds!.x + bounds!.width / 2, bounds!.y + 60);
  await page.mouse.down(); await page.mouse.move(bounds!.x + bounds!.width / 2 - 30, bounds!.y + 60); await page.mouse.up();
  await expect.poll(async () => (await panel.boundingBox())!.width).toBeGreaterThan(resized);
  const savedWidth = (await panel.boundingBox())!.width;
  await page.reload();
  await expect.poll(async () => Math.abs((await panel.boundingBox())!.width - savedWidth)).toBeLessThan(2);
  await expect(page.getByRole("columnheader", { name: "SKU / part number" })).toBeVisible();
  await page.getByRole("button", { name: "Hide item inspector" }).click(); await page.reload();
  await expect(page.getByRole("complementary", { name: "Inventory inspector" })).toHaveCount(0);
  await page.getByRole("button", { name: "View options", exact: true }).click(); await page.getByRole("button", { name: "Reset layout" }).click();
  await page.keyboard.press("Escape");
  await expect(page.getByRole("complementary", { name: "Inventory inspector" })).toHaveCount(0);
  await page.getByRole("button", { name: "View options", exact: true }).click();
  await page.getByRole("button", { name: "Inspector", exact: true }).click();
  await page.keyboard.press("Escape");
  await expect.poll(async () => Math.abs((await panel.boundingBox())!.width - 420)).toBeLessThan(2);
  await expect(page.getByRole("columnheader", { name: "Location", exact: true })).toBeVisible();
  await navigateWorkspace(page, "Projects");
  await expect(page.getByRole("heading", { name: "Projects", exact: true })).toBeVisible();
  await expect(page.getByRole("navigation", { name: "Project views", exact: true })).toBeVisible();
});


test("filters disclose secondary controls and keep active conditions clear when closed", async ({ page }) => {
  await login(page); await fixtureInventory(page);
  await navigateWorkspace(page, "Inventory");
  await expect(page.locator(".inventory-page-status")).toHaveText("Showing 25 of 32 items");
  const filters = page.getByRole("button", { name: "Filters", exact: true });
  for (const width of [1440, 390]) {
    await page.setViewportSize({ width, height: 900 });
    await expect(filters).toHaveAttribute("aria-expanded", "false");
    await expect(page.getByLabel("Filter inventory by category")).toBeHidden();
    await expect(page.getByLabel("Saved inventory view")).toBeHidden();
    await filters.click();
    await expect(page.getByLabel("Filter inventory by category")).toBeVisible();
    await expect(page.getByLabel("Saved inventory view")).toBeHidden();
    await page.getByRole("button", { name: "View options", exact: true }).click();
    await expect(page.getByLabel("Saved inventory view")).toBeVisible();
    await page.keyboard.press("Escape");
    await page.getByLabel("Filter inventory by availability").selectOption("available");
    await filters.click();
    await expect(page.getByLabel("Filter inventory by availability")).toBeHidden();
    await expect(page.locator(".inventory-active-filters")).toContainText("Available for reuse");
    await page.locator(".inventory-active-filters").getByRole("button", { name: "Clear filters" }).click();
    await expect(page.locator(".inventory-active-filters")).toHaveCount(0);
  }
});

test("empty inventory offers its first item while an empty search offers filter recovery", async ({ page }) => {
  await login(page);
  await page.route("**/api/v1/inventory?**", route => route.fulfill({ json: { data: [], total: 0, limit: 25 } }));
  await navigateWorkspace(page, "Inventory");
  await expect(page.getByRole("heading", { name: "Start with what you have", exact: true })).toBeVisible();
  await expect(page.getByLabel("Search inventory", { exact: true })).toHaveCount(0);
  await page.getByRole("button", { name: "Add first item", exact: true }).click();
  await expect(page.getByRole("dialog")).toBeVisible();
  await page.keyboard.press("Escape");
  await page.goto("/?q=missing+component#/inventory");
  await expect(page.getByLabel("Search inventory", { exact: true })).toHaveValue("missing component");
  await expect(page.getByRole("heading", { name: "No matching items" })).toBeVisible();
  await expect(page.getByRole("button", { name: "Add first item", exact: true })).toHaveCount(0);
  await page.locator(".inventory-active-filters").getByRole("button", { name: "Clear filters" }).click();
  await expect(page.getByRole("heading", { name: "Start with what you have", exact: true })).toBeVisible();
});