import { expect, test, type Page } from "@playwright/test"; import { 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, }; } 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 Map From Existing Zones" })).toBeVisible(); await page.getByRole("button", { name: "Create Map From Existing Zones" }).click(); await expect(page.getByText("Bakery")).toBeVisible(); await expect(page.getByText("Frozen Foods")).toBeVisible(); await expect(page.locator(".location-map-pin")).toHaveCount(0); const bakeryObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first(); await bakeryObject.click(); await page.getByRole("textbox", { name: "Label" }).fill("Bread Wall"); await page.getByRole("button", { name: "Save Draft" }).click(); expect(savedPayload).not.toBeNull(); expect(savedPayload?.objects).toEqual( expect.arrayContaining([ expect.objectContaining({ label: "Bread Wall", zone_id: 501 }), ]) ); await page.getByRole("button", { name: "Publish" }).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(); await expect(page.getByText("french bread")).toBeVisible(); await expect(page.getByText("sourdough")).toBeVisible(); await expect(page.getByText("frozen salmon")).toHaveCount(0); await page.getByLabel("Unmapped").check(); await expect(page.getByText("loose batteries")).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); });