import { expect, test, type Page } from "@playwright/test"; import { mockConfig, mockHouseholdAndStoreShell, seedAuthStorage, } from "./helpers/e2e"; type MockItem = { id: number; item_id: number; item_name: string; quantity: number; bought: boolean; item_image: string | null; image_mime_type: string | null; added_by_users: string[]; last_added_on: string; item_type: string | null; item_group: string | null; zone: string | null; }; function makeItem(id: number, itemName: string, quantity = 1): MockItem { return { id, item_id: id + 500, item_name: itemName, quantity, bought: false, item_image: null, image_mime_type: null, added_by_users: ["Owner User"], last_added_on: "2026-03-28T12:00:00.000Z", item_type: null, item_group: null, zone: "Produce", }; } async function setupGroceryRoutes( page: Page, options: { householdRole?: string; items?: MockItem[]; } = {} ) { const items = options.items || [ makeItem(1, "milk", 2), makeItem(2, "bread", 1), ]; await mockConfig(page); await mockHouseholdAndStoreShell(page, { household: { name: "Grocery Actions House", role: options.householdRole || "admin", }, stores: [ { id: 10, name: "Costco", location: "Warehouse", is_default: true, }, ], }); await page.route("**/households/1/members", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { id: 1, username: "owner", name: "Owner User", display_name: "Owner User", role: "owner" }, ]), }); }); await page.route("**/households/1/locations/10/zones", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ zones: [{ id: 1, name: "Produce" }] }), }); }); await page.route("**/households/1/locations/10/list/recent", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]), }); }); await page.route("**/households/1/locations/10/list/suggestions**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]), }); }); await page.route("**/households/1/locations/10/list/item**", async (route) => { const url = new URL(route.request().url()); const itemName = (url.searchParams.get("item_name") || "").toLowerCase(); const item = items.find((candidate) => candidate.item_name === itemName); await route.fulfill({ status: item ? 200 : 404, contentType: "application/json", body: JSON.stringify(item || { message: "Item not found" }), }); }); await page.route("**/households/1/locations/10/list", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items }), }); }); } test("item actions expose bought flow and local skipped restore", async ({ page }) => { await seedAuthStorage(page, { username: "grocery-actions-user" }); await setupGroceryRoutes(page); await page.goto("/"); const milkRow = page.locator(".glist-classification-group .glist-li").filter({ hasText: "milk" }); await expect(milkRow).toBeVisible(); await milkRow.getByRole("button", { name: "Actions for milk" }).click(); await milkRow.getByRole("button", { name: "Bought" }).click(); await expect(page.locator(".confirm-buy-modal")).toBeVisible(); await page.getByRole("button", { name: "Cancel" }).click(); await milkRow.getByRole("button", { name: "Actions for milk" }).click(); await milkRow.getByRole("button", { name: "Skip" }).click(); await expect(milkRow).toHaveCount(0); await expect(page.locator(".glist-skipped-section")).toContainText("milk"); await page.reload(); await expect(page.locator(".glist-skipped-section")).toContainText("milk"); await page.locator(".glist-skipped-section").getByRole("button", { name: "Restore" }).click(); await expect(page.locator(".glist-classification-group .glist-li").filter({ hasText: "milk" })).toBeVisible(); }); test("viewer role cannot see mutation controls", async ({ page }) => { await seedAuthStorage(page, { username: "grocery-viewer-user" }); await setupGroceryRoutes(page, { householdRole: "viewer" }); await page.goto("/"); await expect(page.getByPlaceholder("Enter item name")).toHaveCount(0); await expect(page.getByRole("button", { name: "Actions for milk" })).toHaveCount(0); await expect(page.getByLabel("Search list")).toBeVisible(); }); test("offline banner blocks server-backed add flow with a clear toast", async ({ page }) => { await seedAuthStorage(page, { username: "grocery-offline-user" }); await setupGroceryRoutes(page); await page.goto("/"); await page.evaluate(() => window.dispatchEvent(new Event("offline"))); await expect(page.locator(".glist-offline-banner")).toContainText("Offline"); await page.getByPlaceholder("Enter item name").fill("bananas"); await page.getByRole("button", { name: "Create + Add" }).click(); await expect(page.locator(".add-item-details-modal")).toHaveCount(0); await expect(page.locator(".action-toast.action-toast-error")).toContainText("Adding items needs a connection"); });