import { expect, test } from "@playwright/test"; function seedAuthStorage(page: import("@playwright/test").Page) { return page.addInitScript(() => { localStorage.setItem("token", "test-token"); localStorage.setItem("userId", "1"); localStorage.setItem("role", "admin"); localStorage.setItem("username", "classification-user"); }); } async function mockConfig(page: import("@playwright/test").Page) { await page.route("**/config", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ maxFileSizeMB: 20, maxImageDimension: 800, imageQuality: 85, }), }); }); } async function setupGroceryListRoutes(page: import("@playwright/test").Page) { let currentItem: { 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; } | null = null; let currentClassification: { item_type: string | null; item_group: string | null; zone: string | null; } | null = null; let classificationRequestMode: "success" | "error" = "success"; await page.route("**/households", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { id: 1, name: "Classification House", role: "admin", invite_code: "ABCD1234" }, ]), }); }); await page.route("**/stores/household/1", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([ { 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/stores/10/list/recent", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]), }); }); await page.route("**/households/1/stores/10/list/suggestions**", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([]), }); }); await page.route("**/households/1/stores/10/list/classification**", async (route) => { const request = route.request(); if (request.method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ classification: currentClassification }), }); return; } const body = request.postDataJSON() as { classification?: string | { item_type?: string | null; item_group?: string | null; zone?: string | null }; }; if (classificationRequestMode === "error") { await route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: { message: "Invalid zone" }, }), }); return; } const payload = typeof body.classification === "string" ? { item_type: body.classification, item_group: null, zone: null } : { item_type: body.classification?.item_type ?? null, item_group: body.classification?.item_group ?? null, zone: body.classification?.zone ?? null, }; currentClassification = payload; if (currentItem) { currentItem = { ...currentItem, item_type: payload.item_type, item_group: payload.item_group, zone: payload.zone, }; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Classification set", classification: payload, }), }); }); await page.route("**/households/1/stores/10/list/item**", async (route) => { const request = route.request(); if (request.method() === "PUT") { const body = request.postDataJSON() as { item_name?: string; quantity?: number }; if (currentItem) { currentItem = { ...currentItem, item_name: String(body.item_name || currentItem.item_name).toLowerCase(), quantity: Number(body.quantity || currentItem.quantity), }; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Item updated", item: { id: currentItem?.id || 201, item_name: currentItem?.item_name || "yogurt", quantity: currentItem?.quantity || 1, }, }), }); return; } const url = new URL(request.url()); const itemName = (url.searchParams.get("item_name") || "").toLowerCase(); const itemMatches = currentItem && currentItem.item_name === itemName; await route.fulfill({ status: itemMatches ? 200 : 404, contentType: "application/json", body: JSON.stringify(itemMatches ? currentItem : { message: "Item not found" }), }); }); await page.route("**/households/1/stores/10/list/add", async (route) => { currentItem = { id: 201, item_id: 501, item_name: "yogurt", quantity: 1, 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: currentClassification?.item_type ?? null, item_group: currentClassification?.item_group ?? null, zone: currentClassification?.zone ?? null, }; await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Item added", item: { id: 201, item_name: "yogurt", quantity: 1, bought: false, }, }), }); }); await page.route("**/households/1/stores/10/list", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: currentItem ? [currentItem] : [], }), }); }); return { setClassificationRequestMode(mode: "success" | "error") { classificationRequestMode = mode; }, }; } async function openEditModal(itemRow: ReturnType, page: import("@playwright/test").Page) { await itemRow.dispatchEvent("mousedown"); await page.waitForTimeout(650); await itemRow.dispatchEvent("mouseup"); await expect(page.locator(".edit-modal-content")).toBeVisible(); } test("add-details modal validates with toasts and persists classification details", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); await setupGroceryListRoutes(page); let dialogSeen = false; page.on("dialog", async (dialog) => { dialogSeen = true; await dialog.dismiss(); }); await page.goto("/"); await page.getByPlaceholder("Enter item name").fill("yogurt"); await page.getByRole("button", { name: "Create + Add" }).click(); const addDetailsModal = page.locator(".add-item-details-modal"); await expect(addDetailsModal).toBeVisible(); await addDetailsModal.locator(".add-item-details-select").nth(0).selectOption("dairy"); await addDetailsModal.getByRole("button", { name: "Add Item" }).click(); await expect(page.locator(".action-toast.action-toast-error")).toContainText("Select an item group"); expect(dialogSeen).toBe(false); await addDetailsModal.locator(".add-item-details-select").nth(1).selectOption("Milk"); await addDetailsModal.locator(".add-item-details-select").nth(2).selectOption("Dairy & Refrigerated"); await addDetailsModal.getByRole("button", { name: "Add Item" }).click(); const yogurtRow = page.locator(".glist-li").filter({ hasText: "yogurt" }); await expect(yogurtRow).toBeVisible(); await expect( page.locator(".action-toast.action-toast-success").filter({ hasText: "Added item" }) ).toContainText("Added item"); await openEditModal(yogurtRow, page); const editModal = page.locator(".edit-modal-content"); await expect(editModal.locator(".edit-modal-select").nth(0)).toHaveValue("dairy"); await expect(editModal.locator(".edit-modal-select").nth(1)).toHaveValue("Milk"); await expect(editModal.locator(".edit-modal-select").nth(2)).toHaveValue("Dairy & Refrigerated"); }); test("edit modal supports zone-only updates and shows API error toasts", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); const routes = await setupGroceryListRoutes(page); await page.goto("/"); await page.getByPlaceholder("Enter item name").fill("yogurt"); await page.getByRole("button", { name: "Create + Add" }).click(); await page.locator(".add-item-details-modal").getByRole("button", { name: "Skip All" }).click(); const yogurtRow = page.locator(".glist-li").filter({ hasText: "yogurt" }); await expect(yogurtRow).toBeVisible(); await openEditModal(yogurtRow, page); let editModal = page.locator(".edit-modal-content"); await editModal.locator(".edit-modal-select").nth(0).selectOption(""); await editModal.locator(".edit-modal-select").nth(1).selectOption("Checkout Area"); await editModal.getByRole("button", { name: "Save Changes" }).click(); await expect( page.locator(".action-toast.action-toast-success").filter({ hasText: "Updated item" }) ).toContainText("Updated item"); await expect(editModal).toBeHidden(); await openEditModal(yogurtRow, page); editModal = page.locator(".edit-modal-content"); await expect(editModal.locator(".edit-modal-select").nth(0)).toHaveValue(""); await expect(editModal.locator(".edit-modal-select").nth(1)).toHaveValue("Checkout Area"); routes.setClassificationRequestMode("error"); await editModal.locator(".edit-modal-select").nth(1).selectOption("Bakery"); await editModal.getByRole("button", { name: "Save Changes" }).click(); await expect(page.locator(".action-toast.action-toast-error")).toContainText("Invalid zone"); });