import { expect, test } from "@playwright/test"; import { confirmSlide, mockConfig, mockHouseholdAndStoreShell, seedAuthStorage, } from "./helpers/e2e"; test("manage stores opens a modal to edit and delete household store items", async ({ page }) => { await seedAuthStorage(page, { username: "catalog-user" }); await mockConfig(page); await mockHouseholdAndStoreShell(page, { household: { name: "Catalog House" }, }); let availableItems = [ { item_id: 501, item_name: "milk", item_image: null, image_mime_type: null, item_type: "dairy", item_group: "Milk", zone: "Dairy & Refrigerated", has_managed_settings: true, }, { item_id: 777, item_name: "apples", item_image: null, image_mime_type: null, item_type: null, item_group: null, zone: null, has_managed_settings: false, }, ]; await page.route("**/households/1/stores", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 10, household_store_id: 100, name: "Costco", is_default: true }]), }); }); await page.route("**/households/1/locations/10/available-items*", async (route) => { const request = route.request(); const url = new URL(request.url()); const query = (url.searchParams.get("query") || "").toLowerCase(); if (request.method() === "GET") { const filteredItems = availableItems.filter((item) => item.item_name.includes(query)); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ items: filteredItems, catalog_ready: true }), }); return; } await route.fulfill({ status: 500 }); }); await page.route("**/households/1/locations/10/available-items/777", async (route) => { if (route.request().method() === "PATCH") { availableItems = availableItems.map((item) => item.item_id === 777 ? { ...item, item_type: "produce", item_group: "Fruits", zone: "Produce & Fresh Vegetables", has_managed_settings: true, } : item ); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Available item updated", item: availableItems.find((item) => item.item_id === 777), }), }); return; } await route.fulfill({ status: 500 }); }); await page.route("**/households/1/locations/10/available-items/501", async (route) => { if (route.request().method() === "DELETE") { availableItems = availableItems.filter((item) => item.item_id !== 501); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Store item deleted" }), }); return; } await route.fulfill({ status: 500 }); }); await page.goto("/manage?tab=stores"); const storeCard = page.locator(".store-card").filter({ hasText: "Costco" }); await expect(storeCard).toBeVisible(); await expect(storeCard.getByText("Costco", { exact: true })).toHaveCount(1); await expect(storeCard.getByText("Default location")).toBeVisible(); await expect(storeCard.getByText("Default shopping location")).toHaveCount(0); await expect(storeCard.getByRole("button", { name: "Manage Items" })).toBeVisible(); await storeCard.getByRole("button", { name: "Manage Items" }).click(); const managerModal = page.locator(".store-items-modal"); await expect(managerModal).toBeVisible(); await expect(managerModal.getByText("milk", { exact: true })).toBeVisible(); await expect(managerModal.getByText("apples", { exact: true })).toBeVisible(); await expect(managerModal.locator(".store-available-items-thumb-placeholder").first()).toHaveText("\uD83D\uDCE6"); await expect(managerModal.getByText("Store Defaults")).toHaveCount(0); await expect(managerModal.getByText("No store defaults set")).toHaveCount(0); await expect(managerModal.getByText("Edit Settings", { exact: true })).toHaveCount(0); await expect(managerModal.getByText("Delete Item", { exact: true })).toHaveCount(0); await expect(managerModal.locator(".store-available-items-action")).toHaveCount(0); const searchBox = await managerModal.getByPlaceholder("Search household/store items").boundingBox(); const addButtonBox = await managerModal.getByRole("button", { name: "Add Item" }).boundingBox(); expect(searchBox).not.toBeNull(); expect(addButtonBox).not.toBeNull(); expect( Math.abs( ((searchBox?.y ?? 0) + (searchBox?.height ?? 0) / 2) - ((addButtonBox?.y ?? 0) + (addButtonBox?.height ?? 0) / 2) ) ).toBeLessThan(2); const appleRow = managerModal.getByRole("button", { name: "Edit settings for apples" }); const milkRow = managerModal.locator(".store-items-table-row").filter({ hasText: "milk" }); await appleRow.click(); const editorModal = page.locator(".available-item-editor-modal"); await expect(editorModal).toBeVisible(); await expect(editorModal.getByLabel("Item Name")).toBeDisabled(); await editorModal.locator(".available-item-editor-select").nth(0).selectOption("produce"); await editorModal.locator(".available-item-editor-select").nth(1).selectOption("Fruits"); await editorModal.locator(".available-item-editor-select").nth(2).selectOption("Produce & Fresh Vegetables"); await editorModal.getByRole("button", { name: "Save Changes" }).click(); await expect(page.locator(".action-toast.action-toast-success")).toContainText("Updated store item"); await expect(managerModal.getByText("produce | Fruits | Produce & Fresh Vegetables")).toHaveCount(0); await managerModal.getByRole("button", { name: "Delete Items" }).click(); await expect(managerModal.getByRole("button", { name: "Confirm Delete (0)" })).toBeDisabled(); await expect(managerModal.getByRole("button", { name: "Cancel" })).toBeVisible(); await milkRow.click(); await expect(managerModal.getByRole("button", { name: "Confirm Delete (1)" })).toBeEnabled(); await expect(milkRow).toHaveClass(/is-selected/); await milkRow.click(); await expect(managerModal.getByRole("button", { name: "Confirm Delete (0)" })).toBeDisabled(); await expect(milkRow).not.toHaveClass(/is-selected/); await milkRow.click(); await managerModal.getByRole("button", { name: "Confirm Delete (1)" }).click(); const confirmModal = page.locator(".confirm-slide-modal"); await expect(confirmModal).toBeVisible(); await expect(confirmModal.getByText("Delete milk?")).toBeVisible(); await confirmSlide(page); await expect( page.locator(".action-toast.action-toast-success").filter({ hasText: "Deleted store item" }) ).toContainText("Deleted store item"); await expect(managerModal.locator(".store-items-table-row").filter({ hasText: "milk" })).toHaveCount(0); }); test("manage stores uses modal flows for locations and zones", async ({ page }) => { await seedAuthStorage(page, { username: "store-manager", role: "owner" }); await mockConfig(page); await mockHouseholdAndStoreShell(page, { household: { name: "Modal House", role: "owner" }, }); let locations = [ { id: 10, household_store_id: 100, name: "Costco", location_name: "Default Location", display_name: "Costco", address: "", is_default: true, }, { id: 11, household_store_id: 100, name: "Costco", location_name: "Fontana", display_name: "Costco - Fontana", address: "Sierra Ave", is_default: false, }, ]; let zones = [ { id: 901, name: "Bakery", sort_order: 10 }, { id: 902, name: "Frozen Foods", sort_order: 20 }, ]; await page.route("**/households/1/stores", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(locations), }); }); await page.route("**/households/1/stores/100/locations", async (route) => { const request = route.request(); if (request.method() === "POST") { const body = request.postDataJSON() as { name?: string; address?: string }; const locationName = body.name || "New Location"; locations = [ ...locations, { id: 12, household_store_id: 100, name: "Costco", location_name: locationName, display_name: `Costco - ${locationName}`, address: body.address || "", is_default: false, }, ]; await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify({ store: locations[locations.length - 1] }), }); return; } await route.fulfill({ status: 405 }); }); await page.route("**/households/1/locations/11", async (route) => { const request = route.request(); if (request.method() === "PATCH") { const body = request.postDataJSON() as { name?: string; address?: string }; locations = locations.map((location) => location.id === 11 ? { ...location, location_name: body.name || location.location_name, display_name: `Costco - ${body.name || location.location_name}`, address: body.address || "", } : location ); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ store: locations.find((location) => location.id === 11) }), }); return; } if (request.method() === "DELETE") { locations = locations.filter((location) => location.id !== 11); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Location removed successfully" }), }); return; } await route.fulfill({ status: 405 }); }); await page.route("**/households/1/locations/11/default", async (route) => { if (route.request().method() === "PATCH") { locations = locations.map((location) => ({ ...location, is_default: location.id === 11, })); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Default location updated successfully" }), }); return; } await route.fulfill({ status: 405 }); }); await page.route("**/households/1/locations/10/zones", async (route) => { const request = route.request(); if (request.method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ zones }), }); return; } if (request.method() === "POST") { const body = request.postDataJSON() as { name?: string; sort_order?: number }; zones = [ ...zones, { id: 903, name: body.name || "New Zone", sort_order: body.sort_order || 30 }, ]; await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify({ zone: zones[zones.length - 1] }), }); return; } await route.fulfill({ status: 405 }); }); await page.route("**/households/1/locations/10/zones/901", async (route) => { const request = route.request(); if (request.method() === "PATCH") { const body = request.postDataJSON() as { name?: string; sort_order?: number }; zones = zones.map((zone) => zone.id === 901 ? { ...zone, name: body.name || zone.name, sort_order: body.sort_order ?? zone.sort_order } : zone ); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ zone: zones.find((zone) => zone.id === 901) }), }); return; } if (request.method() === "DELETE") { zones = zones.filter((zone) => zone.id !== 901); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ message: "Zone removed successfully" }), }); return; } await route.fulfill({ status: 405 }); }); await page.goto("/manage?tab=stores"); const storeCard = page.locator(".store-card").filter({ hasText: "Costco" }); await expect(storeCard).toBeVisible(); await expect(storeCard.getByRole("button", { name: "Set Default" })).toHaveCount(0); await expect(storeCard.getByRole("button", { name: "Remove" })).toHaveCount(0); await expect(storeCard.getByPlaceholder("Location name")).toHaveCount(0); await storeCard.getByRole("button", { name: "Manage Locations" }).click(); const locationsModal = page.locator(".store-items-modal").filter({ has: page.getByRole("heading", { name: "Costco Locations" }), }); await expect(locationsModal).toBeVisible(); await expect(locationsModal.getByPlaceholder("Location name")).toBeVisible(); await expect(locationsModal.getByRole("button", { name: "Delete Locations" })).toBeVisible(); await locationsModal.getByRole("button", { name: "Edit location Costco - Fontana" }).click(); const locationSettings = page.locator(".store-items-modal").filter({ has: page.getByRole("heading", { name: "Costco - Fontana Settings" }), }); await expect(locationSettings).toBeVisible(); await locationSettings.getByLabel("Location name").fill("Upland"); await locationSettings.getByLabel("Address or notes").fill("Mountain Ave"); await locationSettings.getByRole("button", { name: "Save Changes" }).click(); await expect( page.locator(".action-toast.action-toast-success").filter({ hasText: "Updated location" }) ).toContainText("Updated location"); await locationsModal.getByRole("button", { name: "Delete Locations" }).click(); await locationsModal.getByRole("button", { name: "Select Costco - Upland for deletion" }).click(); await expect(locationsModal.getByRole("button", { name: "Confirm Delete (1)" })).toBeEnabled(); await locationsModal.getByRole("button", { name: "Confirm Delete (1)" }).click(); await expect(page.getByRole("heading", { name: "Delete Costco - Upland?" })).toBeVisible(); await confirmSlide(page); await expect( page.locator(".action-toast.action-toast-success").filter({ hasText: "Removed location" }) ).toContainText("Removed location"); await page.getByLabel("Close manage locations modal").click(); await storeCard.getByRole("button", { name: "Manage Zones" }).click(); const zonesModal = page.locator(".store-items-modal").filter({ has: page.getByRole("heading", { name: "Costco Zones" }), }); await expect(zonesModal).toBeVisible(); await expect(zonesModal.getByPlaceholder("New zone name")).toBeVisible(); await expect(zonesModal.getByRole("button", { name: "Up" })).toHaveCount(0); await expect(zonesModal.getByRole("button", { name: "Down" })).toHaveCount(0); await expect(zonesModal.getByRole("button", { name: "Remove" })).toHaveCount(0); await zonesModal.getByRole("button", { name: "Edit zone Bakery" }).click(); const zoneSettings = page.locator(".store-items-modal").filter({ has: page.getByRole("heading", { name: "Bakery Settings" }), }); await expect(zoneSettings).toBeVisible(); await zoneSettings.getByLabel("Zone name").fill("Bread"); await zoneSettings.getByRole("button", { name: "Save Changes" }).click(); await expect( page.locator(".action-toast.action-toast-success").filter({ hasText: "Updated zone" }) ).toContainText("Updated zone"); await zonesModal.getByRole("button", { name: "Delete Zones" }).click(); await zonesModal.getByRole("button", { name: "Select Bread for deletion" }).click(); await zonesModal.getByRole("button", { name: "Confirm Delete (1)" }).click(); await expect(page.getByRole("heading", { name: "Delete Bread?" })).toBeVisible(); await confirmSlide(page); await expect( page.locator(".action-toast.action-toast-success").filter({ hasText: "Removed zone" }) ).toContainText("Removed zone"); }); test("grocery page remains unchanged and does not show a store items picker", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); await mockHouseholdAndStoreShell(page); 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/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) => { await route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ 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: [] }), }); }); await page.goto("/"); await expect(page.getByRole("button", { name: "Store Items" })).toHaveCount(0); await expect(page.locator(".available-items-picker-modal")).toHaveCount(0); });