import { expect, test, type Page } from "@playwright/test"; import { confirmSlide, mockConfig, seedAuthStorage } from "./helpers/e2e"; const adminHousehold = { id: 1, name: "Map House", role: "admin", invite_code: "ABCD1234" }; const memberHousehold = { ...adminHousehold, role: "member" }; const storeLocation = { id: 10, household_store_id: 100, household_id: 1, name: "Costco", location_name: "Eastvale", display_name: "Costco - Eastvale", is_default: true, }; const zones = [ { id: 501, name: "Bakery", sort_order: 10, item_count: 2 }, { id: 502, name: "Frozen Foods", sort_order: 20, item_count: 1 }, ]; const items = [ { id: 1, item_name: "french bread", quantity: 1, bought: false, zone_id: 501, zone: "Bakery", added_by_users: ["map-user"], }, { id: 2, item_name: "sourdough", quantity: 1, bought: false, zone_id: 501, zone: "Bakery", added_by_users: ["map-user"], }, { id: 3, item_name: "frozen salmon", quantity: 1, bought: false, zone_id: 502, zone: "Frozen Foods", added_by_users: ["other-user"], }, { id: 4, item_name: "loose batteries", quantity: 1, bought: false, zone_id: null, zone: null, added_by_users: ["map-user"], }, ]; function noMapState(canManage = true) { return { location: storeLocation, map: null, draft_map: null, published_map: null, objects: [], draft_objects: [], published_objects: [], zones, items, unmapped_count: 1, can_manage: canManage, }; } function draftMapState(objects: Array>, canManage = true) { const draft = { id: 900, household_id: 1, store_location_id: 10, name: "Store Map", width: 1000, height: 700, status: "draft", version: 1, }; return { ...noMapState(canManage), map: draft, draft_map: draft, objects, draft_objects: objects, }; } function publishedMapState(objects: Array>, canManage = true) { const published = { id: 901, household_id: 1, store_location_id: 10, name: "Store Map", width: 1000, height: 700, status: "published", version: 2, }; return { ...noMapState(canManage), map: published, published_map: published, objects, published_objects: objects, }; } function draftAndPublishedMapState( draftObjects: Array>, publishedObjects: Array>, canManage = true ) { const draft = { id: 900, household_id: 1, store_location_id: 10, name: "Store Map", width: 1000, height: 700, status: "draft", version: 3, }; const published = { ...draft, id: 901, status: "published", version: 2, }; return { ...noMapState(canManage), map: draft, draft_map: draft, published_map: published, objects: draftObjects, draft_objects: draftObjects, published_objects: publishedObjects, }; } async function mockMapShell(page: Page, household = adminHousehold) { await seedAuthStorage(page, { username: "map-user", role: household.role }); await mockConfig(page); await page.route("**/households", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([household]), }); }); await page.route("**/households/1/stores", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([storeLocation]), }); }); await page.route("**/households/1/locations/10/zones", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ zones }), }); }); } test("admin creates a map from zones, saves a draft, and publishes it", async ({ page }) => { await mockMapShell(page); let mapState = noMapState(true); let savedPayload: Record | null = null; await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.route("**/households/1/locations/10/map/from-zones", async (route) => { const objects = [ { id: 1001, location_map_id: 900, zone_id: 501, zone_name: "Bakery", type: "zone", label: "Bakery", x: 28, y: 28, width: 300, height: 180, rotation: 0, locked: false, visible: true, sort_order: 1, }, { id: 1002, location_map_id: 900, zone_id: 502, zone_name: "Frozen Foods", type: "zone", label: "Frozen Foods", x: 356, y: 28, width: 300, height: 180, rotation: 0, locked: false, visible: true, sort_order: 2, }, ]; mapState = draftMapState(objects, true); await route.fulfill({ status: 201, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.route("**/households/1/locations/10/map/draft", async (route) => { savedPayload = await route.request().postDataJSON(); const savedObjects = (savedPayload.objects as Array>).map((object, index) => ({ id: 1100 + index, location_map_id: 900, zone_name: object.zone_id === 501 ? "Bakery" : "Frozen Foods", ...object, })); mapState = draftMapState(savedObjects, true); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.route("**/households/1/locations/10/map/publish", async (route) => { mapState = publishedMapState(mapState.draft_objects, true); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); await expect(page.getByRole("heading", { name: "Costco" })).toBeVisible(); await expect(page.getByText("Eastvale")).toBeVisible(); await expect(page.getByRole("heading", { name: "No Map" })).toBeVisible(); await expect(page.getByRole("button", { name: "Create From 2 Zones" })).toBeVisible(); await expect(page.getByLabel("Map controls")).toHaveCount(0); await page.getByRole("button", { name: "Create From 2 Zones" }).click(); await expect(page.getByText("Bakery")).toBeVisible(); await expect(page.getByText("Frozen Foods")).toBeVisible(); await expect(page.getByRole("button", { name: "Pan Mode" })).toBeVisible(); await expect(page.getByRole("button", { name: "Edit Objects" })).toBeVisible(); await expect(page.locator(".location-map-pin")).toHaveCount(0); await page.getByRole("button", { name: "Pan Mode" }).click(); const bakeryPanObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first(); const panBox = await bakeryPanObject.boundingBox(); expect(panBox).not.toBeNull(); if (!panBox) throw new Error("Bakery map object was not measurable"); await page.mouse.move(panBox.x + panBox.width / 2, panBox.y + panBox.height / 2); await page.mouse.down(); await page.mouse.move(panBox.x + panBox.width / 2 - 80, panBox.y + panBox.height / 2 - 20, { steps: 8, }); await page.mouse.up(); await page.getByRole("button", { name: "Edit Objects" }).click(); const bakeryObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first(); await bakeryObject.click(); await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery"); const moveBox = await bakeryObject.boundingBox(); expect(moveBox).not.toBeNull(); if (!moveBox) throw new Error("Bakery map object could not be moved"); await page.mouse.move(moveBox.x + moveBox.width / 2, moveBox.y + moveBox.height / 2); await page.mouse.down(); await page.mouse.move(moveBox.x + moveBox.width / 2 + 60, moveBox.y + moveBox.height / 2 + 40, { steps: 8, }); await page.mouse.up(); const resizeHandle = page.locator(".location-map-resize-handle"); await expect(resizeHandle).toBeVisible(); await expect(resizeHandle).toHaveAttribute("width", "64"); await expect(resizeHandle).toHaveAttribute("height", "64"); const resizeBox = await resizeHandle.boundingBox(); expect(resizeBox).not.toBeNull(); if (!resizeBox) throw new Error("Resize handle was not measurable"); await page.mouse.move(resizeBox.x + resizeBox.width / 2, resizeBox.y + resizeBox.height / 2); await page.mouse.down(); await page.mouse.move(resizeBox.x + resizeBox.width / 2 + 45, resizeBox.y + resizeBox.height / 2 + 35, { steps: 8, }); await page.mouse.up(); await page.getByRole("textbox", { name: "Label" }).fill("Bread Wall"); await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bread Wall"); await page.getByRole("button", { name: "Duplicate" }).click(); await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Bread Wall Copy"); await page.getByLabel("Linked zone").selectOption("502"); await page.getByRole("textbox", { name: "Label" }).fill("Temporary Freezer"); await page.getByRole("button", { name: "Delete" }).click(); await confirmSlide(page); await expect(page.locator(".location-map-object", { hasText: "Temporary Freezer" })).toHaveCount(0); await page.getByRole("button", { name: "Back" }).click(); await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toBeVisible(); await page.getByRole("button", { name: "Cancel" }).click(); await expect(page).toHaveURL(/\/stores\/100\/locations\/10\/map$/); await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft"); await page.getByRole("button", { name: "Preview Draft" }).click(); await expect(page.locator(".location-map-object", { hasText: "Bread Wall" })).toBeVisible(); await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft"); await page.getByRole("button", { name: "Edit Draft" }).click(); await page.getByRole("button", { name: "Edit Objects" }).click(); await page.locator(".location-map-object", { hasText: "Bread Wall" }).locator("rect").first().click(); await page.getByRole("button", { name: "Save Draft" }).click(); await expect(page.locator(".location-map-status")).toHaveText("Draft"); expect(savedPayload).not.toBeNull(); const savedObjects = savedPayload?.objects as Array>; const savedBreadWall = savedObjects.find((object) => object.label === "Bread Wall"); expect(savedBreadWall).toEqual(expect.objectContaining({ zone_id: 501 })); expect(Number(savedBreadWall?.x)).toBeGreaterThan(28); expect(Number(savedBreadWall?.y)).toBeGreaterThan(28); expect(Number(savedBreadWall?.width)).toBeGreaterThan(300); expect(Number(savedBreadWall?.height)).toBeGreaterThan(180); expect(savedObjects.some((object) => object.label === "Temporary Freezer")).toBe(false); await page.goto("/login"); await expect(page.getByRole("heading", { name: "Login" })).toBeVisible(); await page.goto("/stores/100/locations/10/map"); await expect(page.locator(".location-map-status")).toHaveText("Draft"); await expect(page.getByRole("heading", { name: "Draft Map" })).toBeVisible(); await page.getByRole("button", { name: "Continue Editing" }).click(); await expect(page.locator(".location-map-object", { hasText: "Bread Wall" })).toBeVisible(); await expect(page.locator(".location-map-object", { hasText: "Temporary Freezer" })).toHaveCount(0); await page.getByRole("button", { name: "Publish", exact: true }).click(); await expect(page.locator(".location-map-status")).toHaveText("Published"); await expect(page.getByRole("button", { name: "Edit Map" })).toBeVisible(); await expect(page.getByText("loose batteries")).toHaveCount(0); await page.locator(".location-map-object", { hasText: "Bread Wall" }).locator("rect").first().click(); const zonePanel = page.locator(".location-map-zone-items"); await expect(zonePanel.locator(".location-map-zone-items-title span")).toHaveText("2 items"); await expect(page.getByText("french bread")).toBeVisible(); await expect(page.getByText("sourdough")).toBeVisible(); await expect(page.getByText("frozen salmon")).toHaveCount(0); await page.getByRole("button", { name: "Layers" }).click(); await page.getByLabel("Unmapped").check(); await expect(page.getByText("loose batteries")).toBeVisible(); }); test("admin setup explains when no zones exist yet", async ({ page }) => { await mockMapShell(page); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ...noMapState(true), zones: [], items: [], unmapped_count: 0, }), }); }); await page.goto("/stores/100/locations/10/map"); await expect(page.getByRole("heading", { name: "No Map" })).toBeVisible(); await expect(page.getByText("No zones are available yet.")).toBeVisible(); await expect(page.getByRole("button", { name: "Create From Zones" })).toBeDisabled(); await expect(page.getByRole("button", { name: "Create Blank Map" })).toBeEnabled(); await expect(page.getByLabel("Map controls")).toHaveCount(0); }); test("admin status follows the visible map when a saved draft exists", async ({ page }) => { await mockMapShell(page); const publishedObjects = [ { id: 1201, location_map_id: 901, zone_id: 501, zone_name: "Bakery", type: "zone", label: "Live Bakery", x: 40, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 1, }, ]; const draftObjects = [ { ...publishedObjects[0], id: 1301, location_map_id: 900, label: "Draft Bakery", x: 80, y: 80, }, ]; const mapState = draftAndPublishedMapState(draftObjects, publishedObjects, true); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); await expect(page.locator(".location-map-status")).toHaveText("Published"); await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible(); await expect(page.locator(".location-map-object", { hasText: "Draft Bakery" })).toHaveCount(0); await page.getByRole("button", { name: "Edit Draft" }).click(); await expect(page.locator(".location-map-status")).toHaveText("Draft"); await expect(page.locator(".location-map-object", { hasText: "Draft Bakery" })).toBeVisible(); await page.getByRole("button", { name: "Preview Draft" }).click(); await expect(page.locator(".location-map-status")).toHaveText("Draft Preview"); await expect(page.locator(".location-map-object", { hasText: "Draft Bakery" })).toBeVisible(); await page.getByRole("button", { name: "View" }).click(); await expect(page.locator(".location-map-status")).toHaveText("Published"); await expect(page.locator(".location-map-object", { hasText: "Live Bakery" })).toBeVisible(); }); test("mobile keeps draft preview status compact in the topbar", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await mockMapShell(page); const publishedObjects = [ { id: 1251, location_map_id: 901, zone_id: 501, zone_name: "Bakery", type: "zone", label: "Live Bakery", x: 40, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 1, }, ]; const draftObjects = [ { ...publishedObjects[0], id: 1252, location_map_id: 900, label: "Draft Bakery", x: 80, y: 80, }, ]; await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(draftAndPublishedMapState(draftObjects, publishedObjects, true)), }); }); await page.goto("/stores/100/locations/10/map"); await page.getByRole("button", { name: "Edit Draft" }).click(); await page.getByRole("button", { name: "Preview Draft" }).click(); const topbar = page.locator(".location-map-topbar"); const status = page.locator(".location-map-status"); const toolbar = page.locator(".location-map-toolbar"); const modeButtons = page.locator(".location-map-mode-buttons"); const zoomControls = page.locator(".location-map-zoom-controls"); await expect(status).toHaveText("Draft Preview"); const topbarBox = await topbar.boundingBox(); const titleBox = await page.locator(".location-map-title").boundingBox(); const statusBox = await status.boundingBox(); const toolbarBox = await toolbar.boundingBox(); const modeBox = await modeButtons.boundingBox(); const zoomBox = await zoomControls.boundingBox(); expect(topbarBox).not.toBeNull(); expect(titleBox).not.toBeNull(); expect(statusBox).not.toBeNull(); expect(toolbarBox).not.toBeNull(); expect(modeBox).not.toBeNull(); expect(zoomBox).not.toBeNull(); if (!topbarBox || !titleBox || !statusBox || !toolbarBox || !modeBox || !zoomBox) { throw new Error("Mobile map controls layout was not measurable"); } expect(topbarBox.height).toBeLessThanOrEqual(66); expect(statusBox.y).toBeLessThan(titleBox.y + titleBox.height); expect(statusBox.x).toBeGreaterThan(titleBox.x); expect(toolbarBox.height).toBeLessThanOrEqual(64); expect(zoomBox.y).toBeLessThan(modeBox.y + modeBox.height); }); test("admin selecting an object does not mark a draft dirty until it changes", async ({ page }) => { await mockMapShell(page); const mapState = draftMapState([ { id: 1351, location_map_id: 900, zone_id: 501, zone_name: "Bakery", type: "zone", label: "Bakery", x: 40, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 1, }, ], true); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); await page.getByRole("button", { name: "Continue Editing" }).click(); await page.getByRole("button", { name: "Edit Objects" }).click(); const bakeryObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first(); await bakeryObject.click(); await expect(page.getByRole("textbox", { name: "Label" })).toBeVisible(); await expect(page.locator(".location-map-status")).toHaveText("Draft"); await expect(page.getByRole("button", { name: "Save Draft" })).toBeDisabled(); await expect(page.getByRole("button", { name: "Undo" })).toBeDisabled(); const objectBox = await bakeryObject.boundingBox(); expect(objectBox).not.toBeNull(); if (!objectBox) throw new Error("Bakery map object was not measurable"); await page.mouse.move(objectBox.x + objectBox.width / 2, objectBox.y + objectBox.height / 2); await page.mouse.down(); await page.mouse.move(objectBox.x + objectBox.width / 2 + 50, objectBox.y + objectBox.height / 2 + 30, { steps: 8, }); await page.mouse.up(); await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft"); await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled(); await expect(page.getByRole("button", { name: "Undo" })).toBeEnabled(); }); test("admin publish saves pending map edits before publishing", async ({ page }) => { await mockMapShell(page); let mapState = publishedMapState([ { id: 1401, location_map_id: 901, zone_id: 501, zone_name: "Bakery", type: "zone", label: "Live Bakery", x: 40, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 1, }, ], true); let savedPayload: Record | null = null; let publishCalled = false; await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.route("**/households/1/locations/10/map/draft", async (route) => { savedPayload = await route.request().postDataJSON(); const savedObjects = (savedPayload.objects as Array>).map((object, index) => ({ id: 1450 + index, location_map_id: 900, zone_name: object.zone_id === 501 ? "Bakery" : null, ...object, })); mapState = draftAndPublishedMapState(savedObjects, mapState.published_objects, true); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.route("**/households/1/locations/10/map/publish", async (route) => { publishCalled = true; mapState = publishedMapState(mapState.draft_objects, true); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); await page.getByRole("button", { name: "Edit Map" }).click(); await page.getByRole("button", { name: "Edit Objects" }).click(); await page.locator(".location-map-object", { hasText: "Live Bakery" }).locator("rect").first().click(); await page.getByRole("textbox", { name: "Label" }).fill("Quick Publish Bakery"); await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft"); await page.getByRole("button", { name: "Publish", exact: true }).click(); await expect.poll(() => publishCalled).toBe(true); expect(savedPayload).not.toBeNull(); expect(savedPayload?.objects).toEqual( expect.arrayContaining([ expect.objectContaining({ label: "Quick Publish Bakery", zone_id: 501, }), ]) ); await expect(page.locator(".location-map-status")).toHaveText("Published"); await expect(page.getByRole("button", { name: "Map area Quick Publish Bakery" })).toBeVisible(); await expect(page.getByRole("button", { name: "Map area Live Bakery" })).toHaveCount(0); }); test("viewer wraps long zone labels and keeps item counts visible", async ({ page }) => { await mockMapShell(page); const mapState = publishedMapState([ { id: 1501, location_map_id: 901, zone_id: 501, zone_name: "Produce & Fresh Vegetables", type: "zone", label: "Produce & Fresh Vegetables", x: 40, y: 40, width: 260, height: 120, rotation: 0, locked: false, visible: true, sort_order: 1, }, ], true); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); const produceArea = page.locator('[data-object-key="1501"]'); await expect(produceArea.locator(".location-map-label tspan")).toHaveCount(2); await expect(produceArea.locator(".location-map-label tspan").nth(0)).toHaveText("Produce & Fresh"); await expect(produceArea.locator(".location-map-label tspan").nth(1)).toHaveText("Vegetables"); await expect(produceArea.locator(".location-map-count")).toHaveText("2 items"); await expect(produceArea.locator(".location-map-count")).toHaveAttribute("y", "116"); await expect(page.locator(".location-map-pin")).toHaveCount(0); await page.getByRole("button", { name: "Layers" }).click(); await page.getByLabel("Labels").uncheck(); await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toBeVisible(); await expect(produceArea.locator(".location-map-label")).toHaveCount(0); await expect(produceArea.locator(".location-map-count")).toHaveText("2 items"); await expect(produceArea.locator(".location-map-count")).toHaveAttribute("y", "92"); }); test("viewer explains when selected zone items are hidden by filters", async ({ page }) => { await mockMapShell(page); const mapState = publishedMapState([ { id: 1551, location_map_id: 901, zone_id: 502, zone_name: "Frozen Foods", type: "zone", label: "Frozen Foods", x: 40, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 1, }, ], true); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); const frozenArea = page.locator('[data-object-key="1551"]'); await expect(frozenArea.locator(".location-map-count")).toHaveText("0 items"); await expect(page.locator(".location-map-pin")).toHaveCount(0); await frozenArea.locator("rect").first().click(); await expect(page.locator(".location-map-zone-items-title span")).toHaveText("0 shown"); await expect(page.getByText("Items are assigned here, but hidden by layer filters.")).toBeVisible(); await expect(page.getByText("frozen salmon")).toHaveCount(0); await page.getByRole("button", { name: "Show Zone Items" }).click(); await expect(page.locator(".location-map-zone-items-title span")).toHaveText("1 item"); await expect(page.getByText("frozen salmon")).toBeVisible(); await expect(frozenArea.locator(".location-map-count")).toHaveText("1 item"); await expect(page.locator(".location-map-pin")).toHaveCount(0); }); test("viewer handles empty and many-item zone panels without default pins", async ({ page }) => { await mockMapShell(page); const manyItems = Array.from({ length: 14 }, (_, index) => ({ id: 3000 + index, item_name: `bulk item ${index + 1}`, quantity: index + 1, bought: false, zone_id: 501, zone: "Bakery", added_by_users: ["map-user"], })); const mapState = { ...publishedMapState([ { id: 1561, location_map_id: 901, zone_id: 501, zone_name: "Bakery", type: "zone", label: "Bakery", x: 40, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 1, }, { id: 1562, location_map_id: 901, zone_id: 502, zone_name: "Frozen Foods", type: "zone", label: "Frozen Foods", x: 340, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 2, }, ], true), items: manyItems, unmapped_count: 0, }; await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); await expect(page.locator(".location-map-pin")).toHaveCount(0); await page.locator('[data-object-key="1562"]').locator("rect").first().click(); await expect(page.locator(".location-map-zone-items-title span")).toHaveText("0 items"); await expect(page.getByText("No items assigned to this zone yet.")).toBeVisible(); await page.locator('[data-object-key="1561"]').locator("rect").first().click(); await expect(page.locator(".location-map-zone-items-title span")).toHaveText("14 items"); await expect(page.locator(".location-map-zone-items li")).toHaveCount(14); await expect(page.locator(".location-map-zone-items li").first()).toContainText("bulk item 1"); await expect(page.locator(".location-map-zone-items li").last()).toContainText("bulk item 14"); await expect(page.locator(".location-map-pin")).toHaveCount(0); }); test("mobile editor uses bottom sheet controls instead of desktop object toolbar", async ({ page }) => { await page.setViewportSize({ width: 390, height: 844 }); await mockMapShell(page); const mapState = draftMapState([ { id: 1601, location_map_id: 900, zone_id: 501, zone_name: "Bakery", type: "zone", label: "Bakery", x: 40, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 1, }, ], true); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); await expect(page.getByRole("heading", { name: "Costco" })).toBeVisible(); await expect(page.getByText("Eastvale")).toBeVisible(); await expect(page.getByRole("heading", { name: "Draft Map" })).toBeVisible(); await page.getByRole("button", { name: "Continue Editing" }).click(); await expect(page.getByLabel("Map controls")).toBeVisible(); await expect(page.locator(".location-map-tool-buttons")).toBeHidden(); await expect(page.locator(".location-map-mobile-tool-switch")).toBeVisible(); const primaryActions = page.locator(".location-map-primary-actions"); const secondaryActions = page.locator(".location-map-secondary-actions"); await expect(primaryActions.getByRole("button", { name: "Save Draft" })).toBeVisible(); await expect(primaryActions.getByRole("button", { name: "Publish" })).toBeVisible(); await expect(secondaryActions.getByRole("button", { name: "Add Area" })).toBeVisible(); await expect(secondaryActions.getByRole("button", { name: "Preview Draft" })).toBeVisible(); const primaryActionBox = await primaryActions.boundingBox(); const secondaryActionBox = await secondaryActions.boundingBox(); expect(primaryActionBox).not.toBeNull(); expect(secondaryActionBox).not.toBeNull(); if (!primaryActionBox || !secondaryActionBox) throw new Error("Mobile action groups were not measurable"); expect(primaryActionBox.y).toBeLessThan(secondaryActionBox.y); await expect(page.getByRole("button", { name: "Pan" })).toHaveClass(/active/); await expect(page.getByRole("button", { name: "Pan" })).toHaveClass(/is-pan/); await expect(page.getByRole("button", { name: "Objects" })).toHaveClass(/is-edit/); await expect(page.locator(".location-map-pin")).toHaveCount(0); const canvasBox = await page.locator(".location-map-canvas-shell").boundingBox(); const sheetBox = await page.locator(".location-map-bottom-sheet").boundingBox(); expect(canvasBox).not.toBeNull(); expect(sheetBox).not.toBeNull(); if (!canvasBox || !sheetBox) throw new Error("Mobile map layout was not measurable"); expect(canvasBox.height).toBeGreaterThan(260); expect(sheetBox.height).toBeLessThanOrEqual(360); await page.getByRole("button", { name: "Objects" }).click(); await expect(page.getByRole("button", { name: "Objects" })).toHaveClass(/active/); await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click(); await expect(page.getByRole("textbox", { name: "Label" })).toBeVisible(); }); test("members can view a published map but cannot edit it", async ({ page }) => { await mockMapShell(page, memberHousehold); const mapState = publishedMapState([ { id: 1201, location_map_id: 901, zone_id: 501, zone_name: "Bakery", type: "zone", label: "Bakery", x: 40, y: 40, width: 260, height: 160, rotation: 0, locked: false, visible: true, sort_order: 1, }, ], false); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(mapState), }); }); await page.goto("/stores/100/locations/10/map"); await expect(page.getByText("Published")).toBeVisible(); await expect(page.getByText("Bakery")).toBeVisible(); await expect(page.getByRole("button", { name: "Edit Map" })).toHaveCount(0); await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0); await expect(page.getByRole("button", { name: "Publish" })).toHaveCount(0); }); test("members see a clean empty state when no map is published", async ({ page }) => { await mockMapShell(page, memberHousehold); await page.route("**/households/1/locations/10/map", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(noMapState(false)), }); }); await page.goto("/stores/100/locations/10/map"); await expect(page.getByRole("heading", { name: "No Map" })).toBeVisible(); await expect(page.getByText("A household admin has not published a map yet.")).toBeVisible(); await expect(page.getByText("Create a rough map for this store location.")).toHaveCount(0); await expect(page.getByRole("button", { name: /Create From/ })).toHaveCount(0); await expect(page.getByRole("button", { name: "Create Blank Map" })).toHaveCount(0); await expect(page.getByLabel("Map controls")).toHaveCount(0); });