grocery-app/frontend/tests/location-map-manager.spec.ts
2026-06-16 22:30:55 -07:00

5241 lines
182 KiB
TypeScript

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 secondStoreLocation = {
...storeLocation,
id: 11,
location_name: "Ontario",
display_name: "Costco - Ontario",
};
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<Record<string, unknown>>, 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<Record<string, unknown>>, 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<Record<string, unknown>>,
publishedObjects: Array<Record<string, unknown>>,
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: typeof adminHousehold | Array<typeof adminHousehold> = adminHousehold,
locations: Array<typeof storeLocation> = [storeLocation]
) {
const households = Array.isArray(household) ? household : [household];
const primaryHousehold = households[0] || adminHousehold;
await seedAuthStorage(page, { username: "map-user", role: primaryHousehold.role });
await mockConfig(page);
await page.route("**/households", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(households),
});
});
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/locations/10/zones", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ zones }),
});
});
}
async function expectMapFitsCanvas(page: Page, maxZoomPercent: number, context: string) {
const scroll = page.locator(".location-map-scroll");
const zoomText = page.locator(".location-map-zoom-controls span");
await expect.poll(async () => {
const value = await zoomText.textContent();
return Number(value?.replace("%", "") || 0);
}).toBeLessThan(maxZoomPercent);
const scrollBox = await scroll.boundingBox();
const svgBox = await page.locator(".location-map-svg").boundingBox();
expect(scrollBox).not.toBeNull();
expect(svgBox).not.toBeNull();
if (!scrollBox || !svgBox) throw new Error(`${context} fitted map was not measurable`);
expect(svgBox.width).toBeLessThanOrEqual(scrollBox.width + 1);
expect(svgBox.height).toBeLessThanOrEqual(scrollBox.height + 1);
return { scroll };
}
async function readMapZoomPercent(page: Page) {
const value = await page.locator(".location-map-zoom-controls span").textContent();
return Number(value?.replace("%", "") || 0);
}
async function expectResizeHandleWithinMapScroll(page: Page) {
const scroll = page.locator(".location-map-scroll");
const resizeHandle = page.locator(".location-map-resize-handle");
await expect(resizeHandle).toBeVisible();
const scrollBox = await scroll.boundingBox();
const handleBox = await resizeHandle.boundingBox();
expect(scrollBox).not.toBeNull();
expect(handleBox).not.toBeNull();
if (!scrollBox || !handleBox) {
throw new Error("Selected resize handle was not measurable");
}
expect(handleBox.x).toBeGreaterThanOrEqual(scrollBox.x);
expect(handleBox.y).toBeGreaterThanOrEqual(scrollBox.y);
expect(handleBox.x + handleBox.width).toBeLessThanOrEqual(scrollBox.x + scrollBox.width + 1);
expect(handleBox.y + handleBox.height).toBeLessThanOrEqual(scrollBox.y + scrollBox.height + 1);
}
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<string, unknown> | 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<Record<string, unknown>>).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: "Move Map" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Edit Areas" })).toHaveCount(0);
await expect(page.locator(".location-map-tool-buttons")).toHaveCount(0);
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 expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery");
await expect(page.locator(".location-map-object").last()).toHaveAttribute("data-object-key", "1001");
await expect(page.getByLabel("Object type")).toHaveCount(0);
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 area Bread Wall" }).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 area Temporary Freezer" }).click();
await confirmSlide(page);
await expect(page.locator(".location-map-object", { hasText: "Temporary Freezer" })).toHaveCount(0);
await page.getByRole("button", { name: "Back to grocery list" }).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$/);
const mapStatus = page.locator(".location-map-status");
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await expect(mapStatus).toHaveAccessibleName("Map status: Unsaved Draft");
await page.getByRole("button", { name: "View", exact: true }).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.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");
await expect(mapStatus).toHaveAccessibleName("Map status: Draft");
expect(savedPayload).not.toBeNull();
const savedObjects = savedPayload?.objects as Array<Record<string, unknown>>;
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 expect(page.getByRole("button", { name: "View Draft" })).toBeVisible();
await expect(page.getByRole("button", { name: "Publish", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Preview Map" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Publish Map" })).toHaveCount(0);
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(mapStatus).toHaveAccessibleName("Map status: Published");
await expect(page.getByRole("button", { name: "Edit Draft" })).toBeVisible();
await expect(page.getByRole("button", { name: "Edit Map" })).toHaveCount(0);
await expect(page.getByText("loose batteries")).toHaveCount(0);
await page.locator(".location-map-object", { hasText: "Bread Wall" }).locator("rect").first().click();
await expect(page.locator(".location-map-sheet-count")).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.getByRole("button", { name: "Unmapped" }).click();
await expect(page.getByText("loose batteries")).toHaveCount(0);
await page.locator(".location-map-background").click({ position: { x: 8, y: 8 } });
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Map Details");
await expect(page.getByText("loose batteries")).toBeVisible();
});
test("admin setup uses compact status rows 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();
const setupDetails = page.getByRole("group", { name: "Map setup details" });
await expect(setupDetails.locator(".location-map-setup-row")).toHaveCount(1);
await expect(setupDetails.locator(".location-map-setup-row", { hasText: "Zones" })).toContainText("0");
await expect(setupDetails.getByText("Status")).toHaveCount(0);
await expect(page.getByText("Not started")).toHaveCount(0);
await expect(page.getByText("No zones are available yet.")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Create From Zones" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Create Blank Map" })).toBeEnabled();
const blankButtonBox = await page.getByRole("button", { name: "Create Blank Map" }).boundingBox();
expect(blankButtonBox).not.toBeNull();
if (!blankButtonBox) throw new Error("No-zone setup action was not measurable");
expect(blankButtonBox.height).toBeGreaterThanOrEqual(40);
await expect(page.getByLabel("Map controls")).toHaveCount(0);
});
test("map message prompts for a household with compact direct text", async ({ page }) => {
await seedAuthStorage(page, { username: "map-user", role: "member" });
await mockConfig(page);
await page.route("**/households", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
await page.goto("/stores/100/locations/10/map");
await expect(page.getByRole("heading", { name: "Select Household" })).toBeVisible();
const messageCard = page.locator(".location-map-message-card");
await expect(messageCard.getByText("Select a household to manage maps.")).toBeVisible();
await expect(messageCard.locator(".location-map-state-message")).toHaveText("Select a household to manage maps.");
await expect(messageCard.getByRole("group", { name: "Map message details" })).toHaveCount(0);
await expect(messageCard.locator(".location-map-setup-row", { hasText: "Message" })).toHaveCount(0);
await expect(messageCard.locator(".location-map-setup-row", { hasText: "Status" })).toHaveCount(0);
await expect(page.getByLabel("Map controls")).toHaveCount(0);
});
test("load failure shows retryable error instead of no-map setup", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 391,
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,
},
], true);
let attempts = 0;
await page.route("**/households/1/locations/10/map", async (route) => {
attempts += 1;
if (attempts === 1) {
await route.fulfill({
status: 503,
contentType: "application/json",
body: JSON.stringify({ error: { message: "Map service unavailable" } }),
});
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mapState),
});
});
await page.goto("/stores/100/locations/10/map");
const errorCard = page.locator(".location-map-load-error");
await expect(errorCard.getByRole("heading", { name: "Map Unavailable" })).toBeVisible();
await expect(errorCard.locator(".location-map-state-message")).toHaveText("Map service unavailable");
await expect(errorCard.getByRole("group", { name: "Map load details" })).toHaveCount(0);
await expect(errorCard.locator(".location-map-setup-row", { hasText: "Error" })).toHaveCount(0);
await expect(errorCard.locator(".location-map-setup-row", { hasText: "Status" })).toHaveCount(0);
await expect(page.getByText("Load Error")).toBeVisible();
await expect(page.getByRole("heading", { name: "No Map" })).toHaveCount(0);
await expect(page.getByRole("button", { name: /Create From/ })).toHaveCount(0);
await page.getByRole("button", { name: "Retry" }).click();
await expect(page.getByRole("heading", { name: "Map Unavailable" })).toHaveCount(0);
await expect(page.getByText("Published")).toBeVisible();
await expect(page.getByRole("button", { name: "Map area Bakery" })).toBeVisible();
expect(attempts).toBe(2);
});
test("loading map keeps location context visible", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 461,
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,
},
], true);
let resolveMapRequestStarted: () => void = () => {};
let releaseMapResponse: () => void = () => {};
let resolveMapResponseSent: () => void = () => {};
const mapRequestStarted = new Promise<void>((resolve) => {
resolveMapRequestStarted = resolve;
});
const mapResponseGate = new Promise<void>((resolve) => {
releaseMapResponse = resolve;
});
const mapResponseSent = new Promise<void>((resolve) => {
resolveMapResponseSent = resolve;
});
await page.route("**/households/1/locations/10/map", async (route) => {
resolveMapRequestStarted();
await mapResponseGate;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mapState),
});
resolveMapResponseSent();
});
const navigation = page.goto("/stores/100/locations/10/map");
await mapRequestStarted;
await expect(page.getByRole("heading", { name: "Costco" })).toBeVisible();
await expect(page.getByText("Eastvale")).toBeVisible();
await expect(page.getByRole("button", { name: "Back to grocery list" })).toBeVisible();
await expect(page.locator(".location-map-status")).toHaveText("Loading");
await expect(page.getByRole("status")).toHaveAccessibleName("Map status: Loading");
const messageCard = page.locator(".location-map-message-card");
await expect(messageCard.locator(".location-map-state-message")).toHaveText("Loading map...");
await expect(messageCard.getByRole("group", { name: "Map message details" })).toHaveCount(0);
await expect(messageCard.locator(".location-map-setup-row", { hasText: "Message" })).toHaveCount(0);
await expect(messageCard.locator(".location-map-setup-row", { hasText: "Status" })).toHaveCount(0);
releaseMapResponse();
await mapResponseSent;
await navigation;
await expect(page.locator(".location-map-status")).toHaveText("Published");
await expect(page.getByRole("button", { name: "Map area Bakery" })).toBeVisible();
});
test("ignores stale map responses after switching locations", async ({ page }) => {
await mockMapShell(page, adminHousehold, [storeLocation, secondStoreLocation]);
const oldLocationState = publishedMapState([
{
id: 471,
location_map_id: 901,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Old Bakery",
x: 40,
y: 40,
width: 260,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextLocationBaseState = publishedMapState([
{
id: 472,
location_map_id: 902,
zone_id: 502,
zone_name: "Produce",
type: "zone",
label: "Ontario Produce",
x: 80,
y: 80,
width: 280,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextPublishedMap = {
...nextLocationBaseState.published_map,
id: 902,
store_location_id: secondStoreLocation.id,
};
const nextLocationState = {
...nextLocationBaseState,
location: secondStoreLocation,
map: nextPublishedMap,
published_map: nextPublishedMap,
};
let resolveFirstRequestStarted: () => void = () => {};
let releaseFirstResponse: () => void = () => {};
let resolveFirstResponseSent: () => void = () => {};
const firstRequestStarted = new Promise<void>((resolve) => {
resolveFirstRequestStarted = resolve;
});
const firstResponseGate = new Promise<void>((resolve) => {
releaseFirstResponse = resolve;
});
const firstResponseSent = new Promise<void>((resolve) => {
resolveFirstResponseSent = resolve;
});
await page.route("**/households/1/locations/10/map", async (route) => {
resolveFirstRequestStarted();
await firstResponseGate;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(oldLocationState),
});
resolveFirstResponseSent();
});
await page.route("**/households/1/locations/11/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(nextLocationState),
});
});
await page.goto("/stores/100/locations/10/map");
await firstRequestStarted;
await page.evaluate(() => {
window.history.pushState({}, "", "/stores/100/locations/11/map");
window.dispatchEvent(new PopStateEvent("popstate"));
});
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
releaseFirstResponse();
await firstResponseSent;
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
await expect(page.getByRole("button", { name: "Map area Old Bakery" })).toHaveCount(0);
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
});
test("ignores stale map save responses after switching locations", async ({ page }) => {
await mockMapShell(page, adminHousehold, [storeLocation, secondStoreLocation]);
const firstLocationState = draftMapState([
{
id: 483,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Eastvale Bakery",
x: 40,
y: 40,
width: 260,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextLocationBaseState = publishedMapState([
{
id: 484,
location_map_id: 902,
zone_id: 502,
zone_name: "Produce",
type: "zone",
label: "Ontario Produce",
x: 80,
y: 80,
width: 280,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextPublishedMap = {
...nextLocationBaseState.published_map,
id: 902,
store_location_id: secondStoreLocation.id,
};
const nextLocationState = {
...nextLocationBaseState,
location: secondStoreLocation,
map: nextPublishedMap,
published_map: nextPublishedMap,
};
let releaseSaveResponse: () => void = () => {};
let resolveSaveStarted: () => void = () => {};
let resolveSaveResponded: () => void = () => {};
const saveStarted = new Promise<void>((resolve) => {
resolveSaveStarted = resolve;
});
const saveGate = new Promise<void>((resolve) => {
releaseSaveResponse = resolve;
});
const saveResponded = new Promise<void>((resolve) => {
resolveSaveResponded = resolve;
});
await page.route("**/households/1/locations/10/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(firstLocationState),
});
});
await page.route("**/households/1/locations/10/map/draft", async (route) => {
resolveSaveStarted();
await saveGate;
const payload = await route.request().postDataJSON();
const savedObjects = (payload.objects as Array<Record<string, unknown>>).map((object, index) => ({
id: 1480 + index,
location_map_id: 900,
zone_name: object.zone_id === 501 ? "Bakery" : null,
...object,
label: "Saved Eastvale Bakery",
}));
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(draftMapState(savedObjects, true)),
});
resolveSaveResponded();
});
await page.route("**/households/1/locations/11/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(nextLocationState),
});
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Continue Editing" }).click();
await page.getByRole("button", { name: "Map area Eastvale Bakery" }).click();
await page.getByRole("textbox", { name: "Label" }).fill("Saving Eastvale Bakery");
await page.getByRole("button", { name: "Save Draft" }).click();
await saveStarted;
await page.evaluate(() => {
window.history.pushState({}, "", "/stores/100/locations/11/map");
window.dispatchEvent(new PopStateEvent("popstate"));
});
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
await expect(page.getByRole("button", { name: "Edit Draft" })).toBeEnabled();
releaseSaveResponse();
await saveResponded;
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
await expect(page.getByRole("button", { name: "Edit Draft" })).toBeEnabled();
await expect(page.getByRole("button", { name: "Map area Saved Eastvale Bakery" })).toHaveCount(0);
await expect(page.getByText("Saved draft")).toHaveCount(0);
});
test("resets map layer filters after switching locations", async ({ page }) => {
await mockMapShell(page, adminHousehold, [storeLocation, secondStoreLocation]);
const firstLocationState = publishedMapState([
{
id: 481,
location_map_id: 901,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Eastvale Bakery",
x: 40,
y: 40,
width: 260,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextLocationBaseState = publishedMapState([
{
id: 482,
location_map_id: 902,
zone_id: 502,
zone_name: "Produce",
type: "zone",
label: "Ontario Produce",
x: 80,
y: 80,
width: 280,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextPublishedMap = {
...nextLocationBaseState.published_map,
id: 902,
store_location_id: secondStoreLocation.id,
};
const nextLocationState = {
...nextLocationBaseState,
location: secondStoreLocation,
map: nextPublishedMap,
published_map: nextPublishedMap,
};
await page.route("**/households/1/locations/10/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(firstLocationState),
});
});
await page.route("**/households/1/locations/11/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(nextLocationState),
});
});
await page.goto("/stores/100/locations/10/map");
await expect(page.getByRole("button", { name: "Map area Eastvale Bakery" })).toBeVisible();
await page.getByRole("button", { name: "Layers" }).click();
await page.getByRole("button", { name: "Zones" }).click();
await expect(page.getByText("Zones hidden in Layers")).toBeVisible();
await expect(page.getByRole("button", { name: "Map area Eastvale Bakery" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toBeVisible();
await page.goto("/stores/100/locations/11/map");
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
await expect(page.getByText("Zones hidden in Layers")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Layers" })).toBeVisible();
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toHaveCount(0);
});
test("closes stale map buy modal after switching locations", async ({ page }) => {
await mockMapShell(page, adminHousehold, [storeLocation, secondStoreLocation]);
const firstLocationState = publishedMapState([
{
id: 491,
location_map_id: 901,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Eastvale Bakery",
x: 40,
y: 40,
width: 260,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextLocationBaseState = publishedMapState([
{
id: 492,
location_map_id: 902,
zone_id: 502,
zone_name: "Produce",
type: "zone",
label: "Ontario Produce",
x: 80,
y: 80,
width: 280,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextPublishedMap = {
...nextLocationBaseState.published_map,
id: 902,
store_location_id: secondStoreLocation.id,
};
const nextLocationState = {
...nextLocationBaseState,
location: secondStoreLocation,
map: nextPublishedMap,
published_map: nextPublishedMap,
items: [
{
id: 1,
item_name: "ontario oranges",
quantity: 1,
bought: false,
zone_id: 502,
zone: "Produce",
added_by_users: ["map-user"],
},
],
};
let resolveBuyRequestStarted: () => void = () => {};
let releaseBuyResponse: () => void = () => {};
let resolveBuyResponseSent: () => void = () => {};
const buyRequestStarted = new Promise<void>((resolve) => {
resolveBuyRequestStarted = resolve;
});
const buyResponseGate = new Promise<void>((resolve) => {
releaseBuyResponse = resolve;
});
const buyResponseSent = new Promise<void>((resolve) => {
resolveBuyResponseSent = resolve;
});
await page.route("**/households/1/locations/10/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(firstLocationState),
});
});
await page.route("**/households/1/locations/11/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(nextLocationState),
});
});
await page.route("**/households/1/locations/10/list/item", async (route) => {
if (route.request().method() !== "PATCH") {
await route.fulfill({ status: 404, contentType: "application/json", body: "{}" });
return;
}
resolveBuyRequestStarted();
await buyResponseGate;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ success: true }),
});
resolveBuyResponseSent();
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Map area Eastvale Bakery" }).click();
await page.getByRole("button", { name: "Mark french bread bought" }).click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("french bread");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await buyRequestStarted;
await page.evaluate(() => {
window.history.pushState({}, "", "/stores/100/locations/11/map");
window.dispatchEvent(new PopStateEvent("popstate"));
});
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
await expect(page.getByText("french bread")).toHaveCount(0);
await page.getByRole("button", { name: "Map area Ontario Produce" }).click();
await expect(page.getByRole("button", { name: "Mark ontario oranges bought" })).toBeVisible();
releaseBuyResponse();
await buyResponseSent;
await expect(page.getByRole("button", { name: "Mark ontario oranges bought" })).toBeVisible();
await expect(page.getByText("Marked item bought")).toHaveCount(0);
});
test("closes stale map delete confirmation after switching locations", async ({ page }) => {
await mockMapShell(page, adminHousehold, [storeLocation, secondStoreLocation]);
const firstLocationState = draftMapState([
{
id: 493,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Eastvale Bakery",
x: 40,
y: 40,
width: 260,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextLocationBaseState = publishedMapState([
{
id: 494,
location_map_id: 902,
zone_id: 502,
zone_name: "Produce",
type: "zone",
label: "Ontario Produce",
x: 80,
y: 80,
width: 280,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], true);
const nextPublishedMap = {
...nextLocationBaseState.published_map,
id: 902,
store_location_id: secondStoreLocation.id,
};
const nextLocationState = {
...nextLocationBaseState,
location: secondStoreLocation,
map: nextPublishedMap,
published_map: nextPublishedMap,
};
await page.route("**/households/1/locations/10/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(firstLocationState),
});
});
await page.route("**/households/1/locations/11/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(nextLocationState),
});
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Continue Editing" }).click();
await page.getByRole("button", { name: "Map area Eastvale Bakery" }).click();
await page.getByRole("button", { name: "Delete area Eastvale Bakery" }).click();
await expect(page.getByRole("heading", { name: "Delete Eastvale Bakery?" })).toBeVisible();
await page.evaluate(() => {
window.history.pushState({}, "", "/stores/100/locations/11/map");
window.dispatchEvent(new PopStateEvent("popstate"));
});
await expect(page.getByRole("button", { name: "Map area Ontario Produce" })).toBeVisible();
await expect(page.locator(".location-map-title span")).toHaveText("Ontario");
await expect(page.getByRole("heading", { name: "Delete Eastvale Bakery?" })).toHaveCount(0);
await expect(page.locator(".confirm-slide-modal")).toHaveCount(0);
});
test("admin can add the first area from a blank map canvas", async ({ page }) => {
await mockMapShell(page);
let mapState = {
...noMapState(true),
zones: [],
items: [],
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.route("**/households/1/locations/10/map/blank", async (route) => {
mapState = {
...draftMapState([], true),
zones: [],
items: [],
unmapped_count: 0,
};
await route.fulfill({
status: 201,
contentType: "application/json",
body: JSON.stringify(mapState),
});
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Create Blank Map" }).click();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByText("No map areas yet")).toBeVisible();
await expect(
page.locator(".location-map-empty-canvas").getByRole("button", { name: "Add Area" })
).toBeVisible();
await page.locator(".location-map-empty-canvas").getByRole("button", { name: "Add Area" }).click();
await expect(page.getByRole("button", { name: "Map area New Area" })).toBeVisible();
await expect(page.locator(".location-map-empty-canvas")).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("New Area");
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("New Area");
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled();
});
test("admin added map areas stay inside the canvas", async ({ page }) => {
await mockMapShell(page);
const existingObjects = Array.from({ length: 31 }, (_, index) => ({
id: 5000 + index,
location_map_id: 900,
zone_id: null,
zone_name: null,
type: "zone",
label: `Existing Area ${index + 1}`,
x: 24 + (index % 5) * 128,
y: 24 + Math.floor(index / 5) * 88,
width: 120,
height: 72,
rotation: 0,
locked: false,
visible: true,
sort_order: index + 1,
}));
const mapState = {
...draftMapState(existingObjects, true),
zones: [],
};
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: "Add Area" }).click();
const newArea = page.getByRole("button", { name: "Map area New Area" });
await expect(newArea).toHaveAttribute("aria-pressed", "true");
await expect(newArea).toHaveAttribute("x", "780");
await expect(newArea).toHaveAttribute("y", "570");
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
});
test("admin view mode previews the edited draft 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: "View", exact: true }).click();
await expect(page.locator(".location-map-status")).toHaveText("Viewing Draft");
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("Viewing Draft");
await expect(page.locator(".location-map-object", { hasText: "Draft Bakery" })).toBeVisible();
});
test("admin keeps the selected draft area when switching to view mode", async ({ page }) => {
await mockMapShell(page);
const publishedObjects = [
{
id: 1211,
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: 1311,
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();
const draftBakeryArea = page.getByRole("button", { name: "Map area Draft Bakery" });
await draftBakeryArea.click();
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Draft Bakery");
await expect(page.locator(".location-map-object.is-selected")).toHaveAttribute("data-object-key", "1311");
await page.getByRole("button", { name: "View", exact: true }).click();
await expect(page.locator(".location-map-status")).toHaveText("Viewing Draft");
await expect(page.locator(".location-map-object.is-selected")).toHaveAttribute("data-object-key", "1311");
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Draft Bakery");
await expect(page.getByRole("button", { name: "Close selected map area" })).toBeVisible();
});
test("admin edit mode hides unavailable draft actions until changes exist", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
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,
},
], 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: "Edit Draft" }).click();
const secondaryActions = page.locator(".location-map-secondary-actions");
await expect(page.locator(".location-map-primary-actions")).toHaveCount(0);
await expect(secondaryActions.getByRole("button", { name: "Add Area" })).toBeVisible();
await expect(secondaryActions.getByRole("button", { name: "View Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Publish", exact: true })).toHaveCount(0);
await secondaryActions.getByRole("button", { name: "Add Area" }).click();
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: "Publish", exact: true })).toBeEnabled();
});
test("mode switches close open map layers without resetting filters", async ({ page }) => {
await mockMapShell(page);
const publishedObjects = [
{
id: 1241,
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: 1242,
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: "Layers" }).click();
await page.getByRole("button", { name: "Pins" }).click();
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toHaveAttribute("aria-expanded", "true");
await expect(page.getByRole("group", { name: "Layer filters" })).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toHaveAttribute("aria-expanded", "false");
await expect(page.getByRole("group", { name: "Layer filters" })).toHaveCount(0);
await page.getByRole("button", { name: "Layers, 1 changed" }).click();
await expect(page.getByRole("group", { name: "Layer filters" })).toBeVisible();
await page.getByRole("button", { name: "Edit Draft" }).click();
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toHaveAttribute("aria-expanded", "false");
await expect(page.getByRole("group", { name: "Layer filters" })).toHaveCount(0);
await page.getByRole("button", { name: "Layers, 1 changed" }).click();
await expect(page.getByRole("group", { name: "Layer filters" })).toBeVisible();
await page.getByRole("button", { name: "View", exact: true }).click();
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toHaveAttribute("aria-expanded", "false");
await expect(page.getByRole("group", { name: "Layer filters" })).toHaveCount(0);
await page.getByRole("button", { name: "Edit Draft" }).click();
await page.getByRole("button", { name: "Layers, 1 changed" }).click();
await expect(page.getByRole("group", { name: "Layer filters" })).toBeVisible();
await page.getByRole("button", { name: "View", exact: true }).click();
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toHaveAttribute("aria-expanded", "false");
await expect(page.getByRole("group", { name: "Layer filters" })).toHaveCount(0);
});
test("mobile keeps viewing draft status compact in the topbar", async ({ page }) => {
await page.setViewportSize({ width: 409, height: 838 });
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: "View", exact: true }).click();
const topbar = page.locator(".location-map-topbar");
const backButton = page.getByRole("button", { name: "Back to grocery list" });
const status = page.locator(".location-map-status");
const toolbar = page.locator(".location-map-toolbar");
const modeGroup = page.locator(".location-map-mode-group");
const modeButtons = page.locator(".location-map-mode-buttons");
const historyActions = page.locator(".location-map-history-buttons");
const zoomControls = page.locator(".location-map-zoom-controls");
await expect(status).toHaveText("Viewing Draft");
const topbarBox = await topbar.boundingBox();
const backButtonBox = await backButton.boundingBox();
const titleBox = await page.locator(".location-map-title").boundingBox();
const statusBox = await status.boundingBox();
const toolbarBox = await toolbar.boundingBox();
const modeGroupBox = await modeGroup.boundingBox();
const modeBox = await modeButtons.boundingBox();
const zoomBox = await zoomControls.boundingBox();
expect(topbarBox).not.toBeNull();
expect(backButtonBox).not.toBeNull();
expect(titleBox).not.toBeNull();
expect(statusBox).not.toBeNull();
expect(toolbarBox).not.toBeNull();
expect(modeGroupBox).not.toBeNull();
expect(modeBox).not.toBeNull();
expect(zoomBox).not.toBeNull();
if (
!topbarBox ||
!backButtonBox ||
!titleBox ||
!statusBox ||
!toolbarBox ||
!modeGroupBox ||
!modeBox ||
!zoomBox
) {
throw new Error("Mobile map controls layout was not measurable");
}
expect(topbarBox.height).toBeLessThanOrEqual(56);
expect(backButtonBox.width).toBeLessThanOrEqual(42);
expect(titleBox.height).toBeLessThanOrEqual(24);
expect(statusBox.height).toBeLessThanOrEqual(24);
expect(statusBox.y).toBeLessThan(titleBox.y + titleBox.height);
expect(statusBox.x).toBeGreaterThan(titleBox.x);
expect(toolbarBox.height).toBeLessThanOrEqual(64);
expect(modeBox.width).toBeGreaterThanOrEqual(130);
expect(modeBox.width).toBeLessThanOrEqual(136);
await expect(historyActions).toHaveCount(0);
expect(zoomBox.y).toBeLessThan(modeBox.y + modeBox.height);
expect(zoomBox.x).toBeGreaterThan(modeBox.x + modeBox.width - 1);
expect(zoomBox.x + zoomBox.width).toBeLessThanOrEqual(toolbarBox.x + toolbarBox.width + 1);
await page.getByRole("button", { name: "Edit Draft" }).click();
await expect(historyActions).toHaveCount(1);
await expect(historyActions.getByRole("button", { name: "Undo" })).toBeVisible();
await expect(historyActions.getByRole("button", { name: "Redo" })).toBeVisible();
const editToolbarBox = await toolbar.boundingBox();
const editModeGroupBox = await modeGroup.boundingBox();
const editModeBox = await modeButtons.boundingBox();
const editHistoryBox = await historyActions.boundingBox();
const editZoomBox = await zoomControls.boundingBox();
expect(editToolbarBox).not.toBeNull();
expect(editModeGroupBox).not.toBeNull();
expect(editModeBox).not.toBeNull();
expect(editHistoryBox).not.toBeNull();
expect(editZoomBox).not.toBeNull();
if (!editToolbarBox || !editModeGroupBox || !editModeBox || !editHistoryBox || !editZoomBox) {
throw new Error("Mobile edit controls layout was not measurable");
}
expect(Math.abs(editModeBox.x - modeBox.x)).toBeLessThanOrEqual(1);
expect(Math.abs(editModeGroupBox.width - modeGroupBox.width)).toBeLessThanOrEqual(1);
expect(Math.abs(editModeBox.width - modeBox.width)).toBeLessThanOrEqual(1);
expect(editToolbarBox.height).toBeLessThanOrEqual(64);
expect(editModeBox.width).toBeGreaterThanOrEqual(130);
expect(editModeBox.width).toBeLessThanOrEqual(136);
expect(editHistoryBox.width).toBeGreaterThanOrEqual(82);
expect(editHistoryBox.x).toBeGreaterThan(editModeBox.x + editModeBox.width);
expect(editHistoryBox.x + editHistoryBox.width).toBeLessThanOrEqual(editToolbarBox.x + editToolbarBox.width + 1);
expect(editZoomBox.y).toBeLessThan(editModeBox.y + editModeBox.height);
expect(editZoomBox.x).toBeGreaterThan(editHistoryBox.x + editHistoryBox.width - 1);
expect(editZoomBox.x + editZoomBox.width).toBeLessThanOrEqual(editToolbarBox.x + editToolbarBox.width + 1);
});
test("mobile map refits after compact viewport resize until manually zoomed", async ({ page }) => {
await page.setViewportSize({ width: 700, height: 844 });
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1258,
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,
},
], 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.poll(async () => readMapZoomPercent(page)).toBeGreaterThan(0);
const initialZoom = await readMapZoomPercent(page);
await page.setViewportSize({ width: 390, height: 844 });
await expect.poll(async () => readMapZoomPercent(page)).toBeLessThan(initialZoom - 8);
const refitZoom = await readMapZoomPercent(page);
await page.getByRole("button", { name: "Zoom in" }).click();
await page.getByRole("button", { name: "Zoom in" }).click();
const manualZoom = await readMapZoomPercent(page);
expect(manualZoom).toBeGreaterThan(refitZoom);
await page.setViewportSize({ width: 700, height: 844 });
await page.waitForTimeout(150);
await expect.poll(async () => readMapZoomPercent(page)).toBe(manualZoom);
});
test("narrow mobile toolbar keeps map controls tappable without overflow", async ({ page }) => {
await page.setViewportSize({ width: 360, height: 844 });
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1261,
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();
const toolbar = page.locator(".location-map-toolbar");
const modeButtons = page.locator(".location-map-mode-buttons");
const historyActions = page.locator(".location-map-history-buttons");
const zoomControls = page.locator(".location-map-zoom-controls");
const toolbarBox = await toolbar.boundingBox();
const modeBox = await modeButtons.boundingBox();
const historyBox = await historyActions.boundingBox();
const zoomBox = await zoomControls.boundingBox();
expect(toolbarBox).not.toBeNull();
expect(modeBox).not.toBeNull();
expect(historyBox).not.toBeNull();
expect(zoomBox).not.toBeNull();
if (!toolbarBox || !modeBox || !historyBox || !zoomBox) {
throw new Error("Narrow mobile toolbar layout was not measurable");
}
expect(toolbarBox.x).toBeGreaterThanOrEqual(0);
expect(toolbarBox.x + toolbarBox.width).toBeLessThanOrEqual(361);
expect(modeBox.width).toBeGreaterThanOrEqual(130);
expect(modeBox.width).toBeLessThanOrEqual(136);
expect(modeBox.x + modeBox.width).toBeLessThanOrEqual(historyBox.x);
expect(zoomBox.y).toBeGreaterThan(modeBox.y + modeBox.height - 1);
expect(zoomBox.x).toBeGreaterThanOrEqual(toolbarBox.x);
expect(zoomBox.x + zoomBox.width).toBeLessThanOrEqual(toolbarBox.x + toolbarBox.width + 1);
const toolbarButtons = toolbar.getByRole("button");
const buttonCount = await toolbarButtons.count();
expect(buttonCount).toBe(7);
await expect(toolbar.getByRole("button", { name: "Undo" })).toBeVisible();
await expect(toolbar.getByRole("button", { name: "Redo" })).toBeVisible();
await expect(toolbar.getByRole("button", { name: "Zoom out" })).toBeVisible();
await expect(toolbar.getByRole("button", { name: "Zoom in" })).toBeVisible();
await expect(toolbar.getByRole("button", { name: "Fit" })).toBeVisible();
for (let index = 0; index < buttonCount; index += 1) {
const buttonBox = await toolbarButtons.nth(index).boundingBox();
expect(buttonBox).not.toBeNull();
if (!buttonBox) throw new Error("Toolbar button was not measurable");
expect(buttonBox.width).toBeGreaterThanOrEqual(40);
expect(buttonBox.height).toBeGreaterThanOrEqual(40);
}
const iconButtons = toolbar.locator(".location-map-icon-button");
await expect(iconButtons).toHaveCount(5);
for (let index = 0; index < await iconButtons.count(); index += 1) {
const iconButtonBox = await iconButtons.nth(index).boundingBox();
expect(iconButtonBox).not.toBeNull();
if (!iconButtonBox) throw new Error("Toolbar icon button was not measurable");
expect(iconButtonBox.width).toBeLessThanOrEqual(44);
expect(iconButtonBox.height).toBeLessThanOrEqual(44);
await expect(iconButtons.nth(index).locator(".location-map-toolbar-icon")).toHaveCount(1);
}
});
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();
const summary = page.getByRole("group", { name: "Draft map summary" });
await expect(summary.locator(".location-map-overview-row")).toHaveCount(1);
await expect(summary.locator(".location-map-overview-row", { hasText: "Areas" }).locator("strong")).toHaveText("1");
await expect(summary.locator(".location-map-overview-row", { hasText: "Hidden" })).toHaveCount(0);
await expect(summary.locator(".location-map-overview-row", { hasText: "Unlinked" })).toHaveCount(0);
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" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Undo" })).toBeDisabled();
await page.getByRole("textbox", { name: "Label" }).fill("Bakery");
await page.getByLabel("Linked zone").selectOption("501");
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
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 undoing the only draft change clears unsaved state", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1356,
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: "Map area Bakery" }).click();
await page.getByLabel("Linked zone").selectOption("");
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();
await page.getByRole("button", { name: "Undo" }).click();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByLabel("Linked zone")).toHaveValue("501");
await expect(page.getByRole("button", { name: "Redo" })).toBeEnabled();
await page.getByRole("button", { name: "Redo" }).click();
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled();
await expect(page.getByLabel("Linked zone")).toHaveValue("");
await page.getByRole("button", { name: "Undo" }).focus();
await page.keyboard.down("Control");
await page.keyboard.press("KeyZ");
await page.keyboard.up("Control");
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByLabel("Linked zone")).toHaveValue("501");
await expect(page.getByRole("button", { name: "Redo" })).toBeEnabled();
await page.getByRole("button", { name: "Redo" }).focus();
await page.keyboard.down("Control");
await page.keyboard.press("KeyY");
await page.keyboard.up("Control");
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toBeEnabled();
await expect(page.getByLabel("Linked zone")).toHaveValue("");
});
test("admin can recover hidden map areas from the empty canvas", async ({ page }) => {
await mockMapShell(page);
let savedPayload: Record<string, unknown> | null = null;
let mapState = draftMapState([
{
id: 1354,
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.route("**/households/1/locations/10/map/draft", async (route) => {
savedPayload = await route.request().postDataJSON();
const savedObjects = (savedPayload.objects as Array<Record<string, unknown>>).map((object, index) => ({
id: 1370 + index,
location_map_id: 900,
zone_name: object.zone_id === 501 ? "Bakery" : null,
...object,
}));
mapState = draftMapState(savedObjects, 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: "Continue Editing" }).click();
await page.getByRole("button", { name: "Map area Bakery" }).click();
const visibleToggle = page.getByRole("button", { name: "Visible" });
const lockedToggle = page.getByRole("button", { name: "Locked" });
await expect(visibleToggle).toHaveAttribute("aria-pressed", "true");
await expect(lockedToggle).toHaveAttribute("aria-pressed", "false");
await visibleToggle.click();
await expect(page.getByRole("button", { name: "Map area Bakery" })).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Edit Areas");
await expect(page.getByText("No visible map areas")).toBeVisible();
await expect(page.locator(".location-map-empty-canvas").getByRole("button", { name: "Show Hidden" })).toBeVisible();
await expect(page.getByRole("button", { name: "Show Hidden Areas (1)" })).toHaveCount(0);
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await page.locator(".location-map-empty-canvas").getByRole("button", { name: "Show Hidden" }).click();
await expect(page.getByRole("button", { name: "Map area Bakery" })).toBeVisible();
await expect(page.locator(".location-map-empty-canvas").getByRole("button", { name: "Show Hidden" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Show Hidden Areas (1)" })).toHaveCount(0);
await page.getByRole("button", { name: "Save Draft" }).click();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
expect(savedPayload).not.toBeNull();
expect((savedPayload?.objects as Array<Record<string, unknown>>)[0]).toEqual(
expect.objectContaining({ visible: true, zone_id: 501 })
);
});
test("admin edit mode summarizes draft areas before selection", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1381,
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,
},
{
id: 1382,
location_map_id: 900,
zone_id: null,
zone_name: null,
type: "zone",
label: "Seasonal Endcap",
x: 330,
y: 40,
width: 220,
height: 130,
rotation: 0,
locked: false,
visible: false,
sort_order: 2,
},
], 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();
const summary = page.getByRole("group", { name: "Draft map summary" });
const areasRow = summary.locator(".location-map-overview-row", { hasText: "Areas" });
const hiddenRow = summary.locator(".location-map-overview-row", { hasText: "Hidden" });
const unlinkedRow = summary.locator(".location-map-overview-row", { hasText: "Unlinked" });
await expect(summary).toBeVisible();
await expect(areasRow.locator("strong")).toHaveText("2");
await expect(hiddenRow.locator("strong")).toHaveText("1");
await expect(unlinkedRow.locator("strong")).toHaveText("1");
await expect(page.getByRole("button", { name: "Show Hidden Areas (1)" })).toBeVisible();
await page.getByRole("button", { name: "Map area Bakery" }).click();
await expect(page.getByRole("group", { name: "Draft map summary" })).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery");
});
test("admin locked map areas hide resize affordances", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1355,
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();
const bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
const bakeryObject = page.locator(".location-map-object", { hasText: "Bakery" });
await bakeryArea.click();
await expect(page.locator(".location-map-resize-handle")).toBeVisible();
await expect(page.locator(".location-map-lock-badge")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Map area Bakery, locked", exact: true })).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "X" })).toHaveCount(0);
const lockedToggle = page.getByRole("button", { name: "Locked", exact: true });
const visibleToggle = page.getByRole("button", { name: "Visible", exact: true });
await expect(lockedToggle).toHaveAttribute("aria-pressed", "false");
await lockedToggle.click();
await expect(lockedToggle).toHaveAttribute("aria-pressed", "true");
await expect(page.getByRole("textbox", { name: "Label" })).toBeEnabled();
await expect(visibleToggle).toBeEnabled();
await expect(page.getByRole("spinbutton", { name: "X" })).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "Y" })).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "W" })).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "H" })).toHaveCount(0);
await expect(page.locator(".location-map-resize-handle")).toHaveCount(0);
await expect(page.locator(".location-map-lock-badge")).toBeVisible();
await expect(bakeryObject).toHaveClass(/is-locked/);
await expect(page.getByRole("button", { name: "Map area Bakery, locked", exact: true })).toBeVisible();
await expect(page.getByText("Unlock this area before moving or resizing it.")).toHaveCount(0);
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await bakeryArea.focus();
await page.keyboard.press("ArrowRight");
await expect(bakeryArea).toHaveAttribute("x", "40");
await lockedToggle.click();
await expect(lockedToggle).toHaveAttribute("aria-pressed", "false");
await expect(page.locator(".location-map-resize-handle")).toBeVisible();
await expect(page.locator(".location-map-lock-badge")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Map area Bakery, locked", exact: true })).toHaveCount(0);
});
test("admin selected overlapping map area renders above other areas", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1311,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Bakery",
x: 40,
y: 40,
width: 260,
height: 180,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
{
id: 1312,
location_map_id: 900,
zone_id: 502,
zone_name: "Frozen Foods",
type: "zone",
label: "Frozen Foods",
x: 120,
y: 90,
width: 260,
height: 180,
rotation: 0,
locked: false,
visible: true,
sort_order: 2,
},
], 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();
const bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
await bakeryArea.focus();
await page.keyboard.press("Enter");
await expect(bakeryArea).toHaveAttribute("aria-pressed", "true");
await expect.poll(async () =>
page.locator(".location-map-object").evaluateAll((nodes) =>
nodes.map((node) => node.getAttribute("data-object-key"))
)
).toEqual(["1312", "1311"]);
await expect(page.locator('[data-object-key="1311"] .location-map-resize-handle')).toBeVisible();
});
test("admin cleared map area labels keep linked zone action names", async ({ page }) => {
await mockMapShell(page);
let savedPayload: Record<string, unknown> | null = null;
let mapState = draftMapState([
{
id: 1353,
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.route("**/households/1/locations/10/map/draft", async (route) => {
savedPayload = await route.request().postDataJSON();
const savedObjects = (savedPayload.objects as Array<Record<string, unknown>>).map((object, index) => ({
id: 1360 + index,
location_map_id: 900,
zone_name: object.zone_id === 501 ? "Bakery" : null,
...object,
}));
mapState = draftMapState(savedObjects, 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: "Continue Editing" }).click();
await page.getByRole("button", { name: "Map area Bakery" }).click();
await page.getByRole("textbox", { name: "Label" }).fill("");
await expect(page.getByRole("button", { name: "Map area Bakery", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Map area 1", exact: true })).toHaveCount(0);
await page.getByRole("button", { name: "Save Draft" }).click();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
expect(savedPayload).not.toBeNull();
expect((savedPayload?.objects as Array<Record<string, unknown>>)[0]).toEqual(
expect.objectContaining({ label: "", zone_id: 501 })
);
await expect(page.getByRole("button", { name: "Map area Bakery", exact: true })).toBeVisible();
await page.getByRole("button", { name: "Map area Bakery", exact: true }).click();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("");
await page.getByRole("button", { name: "Delete area Bakery" }).click();
await expect(page.getByRole("heading", { name: "Delete Bakery?" })).toBeVisible();
await page.getByRole("button", { name: "Cancel" }).click();
await page.getByRole("button", { name: "Duplicate area Bakery" }).click();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Bakery Copy");
await expect(page.getByRole("button", { name: "Map area Bakery Copy" })).toBeVisible();
await expect(page.getByRole("button", { name: "Map area Area Copy" })).toHaveCount(0);
});
test("admin restores selected map area through duplicate undo and redo", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1355,
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: "Map area Bakery" }).click();
await expect(page.locator(".location-map-object.is-selected")).toHaveAttribute("data-object-key", "1355");
await page.getByRole("button", { name: "Duplicate area Bakery" }).click();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Bakery Copy");
await expect(page.locator(".location-map-object.is-selected")).toContainText("Bakery Copy");
await page.getByRole("button", { name: "Undo" }).click();
await expect(page.getByRole("button", { name: "Map area Bakery Copy" })).toHaveCount(0);
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Bakery");
await expect(page.locator(".location-map-object.is-selected")).toHaveAttribute("data-object-key", "1355");
await page.getByRole("button", { name: "Redo" }).click();
await expect(page.getByRole("button", { name: "Map area Bakery Copy" })).toBeVisible();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Bakery Copy");
await expect(page.locator(".location-map-object.is-selected")).toContainText("Bakery Copy");
});
test("admin nudges selected map areas with arrow keys", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1352,
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();
const bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
await bakeryArea.focus();
await expect(bakeryArea).toBeFocused();
await page.keyboard.press("Enter");
await expect(page.getByText("Move, resize, link, rename, or nudge with arrow keys.")).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "X" })).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "Y" })).toHaveCount(0);
await expect(bakeryArea).toHaveAttribute("x", "40");
await expect(bakeryArea).toHaveAttribute("y", "40");
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await page.keyboard.press("ArrowRight");
await expect(bakeryArea).toHaveAttribute("x", "50");
await expect(bakeryArea).toHaveAttribute("y", "40");
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();
await page.keyboard.press("Shift+ArrowDown");
await expect(bakeryArea).toHaveAttribute("x", "50");
await expect(bakeryArea).toHaveAttribute("y", "65");
});
test("admin cannot nudge map areas outside the canvas", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1353,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Bakery",
x: 740,
y: 540,
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();
const bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
await bakeryArea.focus();
await page.keyboard.press("Enter");
await expect(bakeryArea).toHaveAttribute("x", "740");
await expect(bakeryArea).toHaveAttribute("y", "540");
await page.keyboard.press("Shift+ArrowRight");
await page.keyboard.press("Shift+ArrowDown");
await expect(bakeryArea).toHaveAttribute("x", "740");
await expect(bakeryArea).toHaveAttribute("y", "540");
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Undo" })).toBeDisabled();
});
test("admin cannot resize map areas beyond the canvas", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1354,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Bakery",
x: 820,
y: 580,
width: 180,
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");
await page.getByRole("button", { name: "Continue Editing" }).click();
const bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
await bakeryArea.click();
await expect(bakeryArea).toHaveAttribute("x", "820");
await expect(bakeryArea).toHaveAttribute("y", "580");
await expect(bakeryArea).toHaveAttribute("width", "180");
await expect(bakeryArea).toHaveAttribute("height", "120");
const resizeHandle = page.locator(".location-map-resize-handle");
await expect(resizeHandle).toBeVisible();
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 + 160, resizeBox.y + resizeBox.height / 2 + 160, {
steps: 8,
});
await page.mouse.up();
await expect(bakeryArea).toHaveAttribute("x", "820");
await expect(bakeryArea).toHaveAttribute("y", "580");
await expect(bakeryArea).toHaveAttribute("width", "180");
await expect(bakeryArea).toHaveAttribute("height", "120");
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Undo" })).toBeDisabled();
});
test("admin selected map area uses compact editable fields", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1352,
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.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
await expect(page.locator(".location-map-field-row")).toHaveCount(2);
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Bakery");
await expect(page.getByLabel("Linked zone")).toHaveValue("501");
await expect(page.getByLabel("Object type")).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "X" })).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "Y" })).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "W" })).toHaveCount(0);
await expect(page.getByRole("spinbutton", { name: "H" })).toHaveCount(0);
await page.getByRole("textbox", { name: "Label" }).focus();
await page.keyboard.press("Delete");
await expect(page.getByRole("heading", { name: "Delete Bakery?" })).toHaveCount(0);
await expect(page.locator(".location-map-object.is-selected")).toHaveCount(1);
await page.getByRole("button", { name: "Visible" }).focus();
await page.keyboard.press("Delete");
await expect(page.getByRole("heading", { name: "Delete Bakery?" })).toBeVisible();
await page.getByRole("button", { name: "Cancel" }).click();
await expect(page.getByRole("heading", { name: "Delete Bakery?" })).toHaveCount(0);
await expect(page.locator(".location-map-object.is-selected")).toHaveCount(1);
await page.getByRole("textbox", { name: "Label" }).focus();
await page.keyboard.press("Escape");
await expect(page.getByRole("textbox", { name: "Label" })).toBeVisible();
await expect(page.locator(".location-map-object.is-selected")).toHaveCount(1);
await page.getByRole("button", { name: "Visible" }).focus();
await page.keyboard.press("Escape");
await expect(page.getByRole("textbox", { name: "Label" })).toHaveCount(0);
await expect(page.locator(".location-map-object.is-selected")).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Edit Areas");
});
test("admin save shortcut locks draft controls", async ({ page }) => {
await mockMapShell(page);
let mapState = draftMapState([
{
id: 1361,
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);
let releaseSave: () => void = () => {};
let saveStarted = false;
const saveGate = new Promise<void>((resolve) => {
releaseSave = resolve;
});
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) => {
saveStarted = true;
const payload = await route.request().postDataJSON();
await saveGate;
const savedObjects = (payload.objects as Array<Record<string, unknown>>).map((object, index) => ({
...(mapState.draft_objects[index] || {}),
...object,
}));
mapState = draftMapState(savedObjects, 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: "Continue Editing" }).click();
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
const labelInput = page.getByRole("textbox", { name: "Label" });
await labelInput.fill("Saving Bakery");
await labelInput.focus();
await page.keyboard.down("Control");
await page.keyboard.press("KeyS");
await page.keyboard.up("Control");
await expect.poll(() => saveStarted).toBe(true);
await expect(page.getByRole("button", { name: "Saving..." })).toBeDisabled();
await expect(page.getByRole("button", { name: "Publish" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Add Area" })).toBeDisabled();
await expect(page.getByRole("button", { name: "View Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Undo" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Duplicate area Saving Bakery" })).toBeDisabled();
await expect(page.getByRole("button", { name: "Delete area Saving Bakery" })).toBeDisabled();
await expect(labelInput).toBeDisabled();
releaseSave();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(labelInput).toBeEnabled();
await expect(labelInput).toHaveValue("Saving Bakery");
await expect(page.getByRole("button", { name: "Map area Saving Bakery" })).toHaveAttribute("aria-pressed", "true");
});
test("admin save progress locks empty-canvas recovery actions", async ({ page }) => {
await mockMapShell(page);
let mapState = draftMapState([
{
id: 1362,
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);
let releaseSave: () => void = () => {};
let saveStarted = false;
const saveGate = new Promise<void>((resolve) => {
releaseSave = resolve;
});
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) => {
saveStarted = true;
const payload = await route.request().postDataJSON();
await saveGate;
const savedObjects = (payload.objects as Array<Record<string, unknown>>).map((object, index) => ({
...(mapState.draft_objects[index] || {}),
...object,
}));
mapState = draftMapState(savedObjects, 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: "Continue Editing" }).click();
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
await page.getByRole("button", { name: "Visible" }).click();
await expect(page.getByText("No visible map areas")).toBeVisible();
await expect(
page.locator(".location-map-empty-canvas").getByRole("button", { name: "Show Hidden" })
).toBeEnabled();
await expect(page.getByRole("button", { name: "Show Hidden Areas (1)" })).toHaveCount(0);
await page.getByRole("button", { name: "Save Draft" }).click();
await expect.poll(() => saveStarted).toBe(true);
await expect(
page.locator(".location-map-empty-canvas").getByRole("button", { name: "Show Hidden" })
).toBeDisabled();
await expect(page.getByRole("button", { name: "Show Hidden Areas (1)" })).toHaveCount(0);
releaseSave();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(
page.locator(".location-map-empty-canvas").getByRole("button", { name: "Show Hidden" })
).toBeEnabled();
});
test("admin hard navigation warns when draft edits are unsaved", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1371,
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");
const cleanBeforeUnload = await page.evaluate(() => {
const event = new Event("beforeunload", { cancelable: true });
const dispatchResult = window.dispatchEvent(event);
return {
defaultPrevented: event.defaultPrevented,
dispatchResult,
};
});
expect(cleanBeforeUnload).toEqual({
defaultPrevented: false,
dispatchResult: true,
});
await page.getByRole("button", { name: "Continue Editing" }).click();
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
await page.getByRole("textbox", { name: "Label" }).fill("Hard Navigation Bakery");
const dirtyBeforeUnload = await page.evaluate(() => {
const event = new Event("beforeunload", { cancelable: true });
const dispatchResult = window.dispatchEvent(event);
return {
defaultPrevented: event.defaultPrevented,
dispatchResult,
};
});
expect(dirtyBeforeUnload).toEqual({
defaultPrevented: true,
dispatchResult: false,
});
});
test("admin app navigation warns when draft edits are unsaved", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1372,
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.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
await page.getByRole("textbox", { name: "Label" }).fill("Unsaved Navbar Bakery");
await page.getByRole("link", { name: "Home" }).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.getByRole("textbox", { name: "Label" })).toHaveValue("Unsaved Navbar Bakery");
await page.getByRole("link", { name: "Home" }).click();
await confirmSlide(page);
await expect(page).toHaveURL(/\/$/);
});
test("admin browser back warns when draft edits are unsaved", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1375,
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.evaluate(() => {
window.history.replaceState(window.history.state, "", "/");
window.history.pushState({}, "", "/stores/100/locations/10/map");
});
await expect(page).toHaveURL(/\/stores\/100\/locations\/10\/map$/);
await page.getByRole("button", { name: "Continue Editing" }).click();
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
await page.getByRole("textbox", { name: "Label" }).fill("Unsaved Back Bakery");
await page.evaluate(() => window.history.back());
await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toBeVisible();
await expect(page).toHaveURL(/\/stores\/100\/locations\/10\/map$/);
await page.getByRole("button", { name: "Cancel" }).click();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Unsaved Back Bakery");
await page.evaluate(() => window.history.back());
await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toBeVisible();
await confirmSlide(page);
await expect(page).toHaveURL(/\/$/);
});
test("admin browser back leaves after draft edits are undone clean", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1376,
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.evaluate(() => {
window.history.replaceState(window.history.state, "", "/");
window.history.pushState({}, "", "/stores/100/locations/10/map");
});
await expect(page).toHaveURL(/\/stores\/100\/locations\/10\/map$/);
await page.getByRole("button", { name: "Continue Editing" }).click();
await page.getByRole("button", { name: "Map area Bakery" }).click();
await page.getByLabel("Linked zone").selectOption("");
await expect(page.locator(".location-map-status")).toHaveText("Unsaved Draft");
await page.getByRole("button", { name: "Undo" }).click();
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toHaveCount(0);
await page.evaluate(() => window.history.back());
await expect(page).toHaveURL(/\/$/);
await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toHaveCount(0);
});
test("admin logout warns when draft edits are unsaved", async ({ page }) => {
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1373,
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);
let logoutCalls = 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.route("**/auth/logout", async (route) => {
logoutCalls += 1;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ message: "Logged out" }),
});
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Continue Editing" }).click();
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
await page.getByRole("textbox", { name: "Label" }).fill("Unsaved Logout Bakery");
await page.getByRole("button", { name: "User menu" }).click();
await page.getByRole("button", { name: "Logout", exact: true }).click();
await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toBeVisible();
expect(logoutCalls).toBe(0);
await page.getByRole("button", { name: "Cancel" }).click();
await expect(page).toHaveURL(/\/stores\/100\/locations\/10\/map$/);
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Unsaved Logout Bakery");
expect(logoutCalls).toBe(0);
await page.getByRole("button", { name: "Logout", exact: true }).click();
await confirmSlide(page);
await expect(page).toHaveURL(/\/login$/);
expect(logoutCalls).toBe(1);
});
test("admin household switch warns when draft edits are unsaved", async ({ page }) => {
const secondHousehold = {
id: 2,
name: "Second House",
role: "admin",
invite_code: "EFGH5678",
};
await mockMapShell(page, [adminHousehold, secondHousehold]);
const mapState = draftMapState([
{
id: 1374,
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);
const secondHouseholdMapState = {
...noMapState(true),
location: {
...storeLocation,
household_id: 2,
name: "Second Costco",
display_name: "Second Costco - Eastvale",
},
};
let secondHouseholdMapRequests = 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.route("**/households/2/stores", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ ...storeLocation, household_id: 2 }]),
});
});
await page.route("**/households/2/locations/10/zones", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ zones }),
});
});
await page.route("**/households/2/locations/10/map", async (route) => {
secondHouseholdMapRequests += 1;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(secondHouseholdMapState),
});
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Continue Editing" }).click();
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
await page.getByRole("textbox", { name: "Label" }).fill("Unsaved Household Bakery");
await page.locator(".household-switcher-toggle").click();
await page.getByRole("button", { name: "Second House", exact: true }).click();
await expect(page.getByRole("heading", { name: "Leave with unsaved changes?" })).toBeVisible();
expect(secondHouseholdMapRequests).toBe(0);
await page.getByRole("button", { name: "Cancel" }).click();
await expect(page.locator(".household-switcher-toggle .household-name")).toHaveText("Map House");
await expect(page.getByRole("textbox", { name: "Label" })).toHaveValue("Unsaved Household Bakery");
expect(secondHouseholdMapRequests).toBe(0);
await page.getByRole("button", { name: "Second House", exact: true }).click();
await confirmSlide(page);
await expect(page.locator(".household-switcher-toggle .household-name")).toHaveText("Second House");
await expect(page.getByRole("heading", { name: "No Map" })).toBeVisible();
expect(secondHouseholdMapRequests).toBe(1);
});
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<string, unknown> | 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<Record<string, unknown>>).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 Draft" }).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);
await expect(page.locator(".location-map-object.is-selected")).toHaveAttribute("data-object-key", "1450");
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Quick Publish Bakery");
await expect(page.getByRole("button", { name: "Close selected map area" })).toBeVisible();
});
test("admin publish failure keeps successfully saved pending edits", async ({ page }) => {
await mockMapShell(page);
let mapState = publishedMapState([
{
id: 1461,
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<string, unknown> | 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<Record<string, unknown>>).map((object, index) => ({
id: 1465 + 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;
await route.fulfill({
status: 503,
contentType: "application/json",
body: JSON.stringify({ error: { message: "Publish service unavailable" } }),
});
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Edit Draft" }).click();
await page.locator(".location-map-object", { hasText: "Live Bakery" }).locator("rect").first().click();
await page.getByRole("textbox", { name: "Label" }).fill("Saved Draft 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: "Saved Draft Bakery",
zone_id: 501,
}),
])
);
await expect(page.locator(".location-map-status")).toHaveText("Draft");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Map area Saved Draft 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();
const layerFilters = page.getByRole("group", { name: "Layer filters" });
const labelsToggle = layerFilters.getByRole("button", { name: "Labels" });
await expect(layerFilters.locator(".location-map-layer-state")).toHaveCount(8);
await expect(labelsToggle).toHaveAttribute("aria-pressed", "true");
await expect(labelsToggle.locator(".location-map-layer-state.is-on")).toHaveCount(1);
await expect(page.getByRole("button", { name: "Bought" })).toBeVisible();
await expect(page.getByRole("button", { name: "Done" })).toHaveCount(0);
await labelsToggle.click();
await expect(labelsToggle).toHaveAttribute("aria-pressed", "false");
await expect(labelsToggle.locator(".location-map-layer-state.is-on")).toHaveCount(0);
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");
await page.getByRole("button", { name: "Layers, 1 changed" }).click();
await expect(page.getByRole("button", { name: "Reset layer filters" })).toBeVisible();
await page.getByRole("button", { name: "Reset layer filters" }).click();
await expect(page.getByRole("button", { name: "Layers" })).toBeVisible();
await expect(page.getByRole("button", { name: "Reset layer filters" })).toHaveCount(0);
await expect(produceArea.locator(".location-map-label tspan")).toHaveCount(2);
await expect(produceArea.locator(".location-map-count")).toHaveAttribute("y", "116");
});
test("viewer explains when the zones layer hides the map", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1521,
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,
},
], 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("button", { name: "Map area Bakery" })).toBeVisible();
await page.getByRole("button", { name: "Map area Bakery" }).click();
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery");
await expect(page.locator(".location-map-sheet-count")).toHaveText("2 items");
await page.getByRole("button", { name: "Layers" }).click();
await page.getByRole("button", { name: "Zones" }).click();
await expect(page.getByRole("button", { name: "Map area Bakery" })).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Map Details");
await expect(page.locator(".location-map-sheet-count")).toHaveCount(0);
await expect(page.getByText("Zones hidden in Layers")).toBeVisible();
await expect(page.getByRole("button", { name: "Show Zones" })).toBeVisible();
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toBeVisible();
await page.getByRole("button", { name: "Show Zones" }).click();
await expect(page.getByRole("button", { name: "Map area Bakery" })).toBeVisible();
await expect(page.getByText("Zones hidden in Layers")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Layers" })).toBeVisible();
});
test("viewer shows a compact map overview before selecting an area", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1531,
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,
},
], 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 overview = page.getByRole("group", { name: "Map summary" });
const areasRow = overview.locator(".location-map-overview-row", { hasText: "Areas" });
const mappedRow = overview.locator(".location-map-overview-row", { hasText: "Mapped items" });
const unmappedRow = overview.locator(".location-map-overview-row", { hasText: "Unmapped" });
await expect(overview).toBeVisible();
await expect(areasRow.locator("strong")).toHaveText("1 shown");
await expect(mappedRow.locator("strong")).toHaveText("2 shown");
await expect(unmappedRow.locator("strong")).toHaveText("1 hidden");
await page.getByRole("button", { name: "Layers" }).click();
await page.getByRole("button", { name: "Unmapped" }).click();
await expect(unmappedRow.locator("strong")).toHaveText("1");
await page.getByRole("button", { name: "Map area Bakery" }).click();
await expect(page.getByRole("group", { name: "Map summary" })).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery");
});
test("viewer omits zero item rows from the map overview", async ({ page }) => {
await mockMapShell(page);
const mapState = {
...publishedMapState([
{
id: 1531,
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,
},
], true),
items: [],
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");
const overview = page.getByRole("group", { name: "Map summary" });
await expect(overview.locator(".location-map-overview-row")).toHaveCount(1);
await expect(overview.locator(".location-map-overview-row", { hasText: "Areas" }).locator("strong")).toHaveText("1 shown");
await expect(overview.locator(".location-map-overview-row", { hasText: "Mapped items" })).toHaveCount(0);
await expect(overview.locator(".location-map-overview-row", { hasText: "Unmapped" })).toHaveCount(0);
});
test("viewer shows a compact overflow cue for long unmapped lists", async ({ page }) => {
await mockMapShell(page);
const unmappedItems = Array.from({ length: 11 }, (_, index) => ({
id: 4000 + index,
item_name: `unmapped item ${index + 1}`,
quantity: 1,
bought: false,
zone_id: null,
zone: null,
added_by_users: ["map-user"],
}));
const mapState = {
...publishedMapState([], true),
items: unmappedItems,
unmapped_count: unmappedItems.length,
};
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: "Layers" }).click();
const unmappedToggle = page.getByRole("group", { name: "Layer filters" }).getByRole("button", { name: "Unmapped" });
await expect(unmappedToggle).toHaveAttribute("aria-pressed", "false");
await unmappedToggle.click();
await expect(unmappedToggle).toHaveAttribute("aria-pressed", "true");
await expect(page.getByText("unmapped item 1")).toBeVisible();
await expect(page.getByText("unmapped item 8")).toBeVisible();
await expect(page.getByText("unmapped item 9")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Show 3 more unmapped items" })).toHaveText("+3 more");
const firstUnmappedButtonBox = await page.getByRole("button", { name: "Mark unmapped item 1 bought" }).boundingBox();
const moreUnmappedButtonBox = await page.getByRole("button", { name: "Show 3 more unmapped items" }).boundingBox();
expect(firstUnmappedButtonBox).not.toBeNull();
expect(moreUnmappedButtonBox).not.toBeNull();
if (!firstUnmappedButtonBox || !moreUnmappedButtonBox) {
throw new Error("Unmapped item controls were not measurable");
}
expect(firstUnmappedButtonBox.height).toBeGreaterThanOrEqual(40);
expect(moreUnmappedButtonBox.height).toBeGreaterThanOrEqual(40);
await page.getByRole("button", { name: "Show 3 more unmapped items" }).click();
await expect(page.getByText("unmapped item 9")).toBeVisible();
await expect(page.getByText("unmapped item 11")).toBeVisible();
await expect(page.getByRole("button", { name: "Show fewer unmapped items" })).toHaveText("Show fewer");
await page.getByRole("button", { name: "Show fewer unmapped items" }).click();
await expect(page.getByText("unmapped item 9")).toHaveCount(0);
});
test("viewer can buy unmapped map items in place", async ({ page }) => {
await mockMapShell(page);
const unmappedMapItems = [
{
id: 4201,
item_name: "loose batteries",
quantity: 1,
bought: false,
zone_id: null,
zone: null,
added_by_users: ["map-user"],
},
{
id: 4202,
item_name: "paper plates",
quantity: 2,
bought: false,
zone_id: null,
zone: null,
added_by_users: ["map-user"],
},
];
const mapState = {
...publishedMapState([], true),
items: unmappedMapItems,
unmapped_count: unmappedMapItems.length,
};
const boughtPayloads: Array<Record<string, unknown>> = [];
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/list/item", async (route) => {
boughtPayloads.push(await route.request().postDataJSON());
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ success: true }),
});
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Layers" }).click();
await page.getByRole("button", { name: "Unmapped" }).click();
await page.getByRole("button", { name: "Mark loose batteries bought" }).click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("loose batteries");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await expect.poll(() => boughtPayloads.length).toBe(1);
expect(boughtPayloads[0]).toMatchObject({
item_name: "loose batteries",
bought: true,
quantity_bought: 1,
});
await expect(page.locator(".confirm-buy-item-name")).toHaveText("paper plates");
await expect(page.getByText("loose batteries")).toHaveCount(0);
await expect(page.getByRole("group", { name: "Map summary" }).locator(".location-map-overview-row", { hasText: "Unmapped" })).toContainText("1 shown");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await expect.poll(() => boughtPayloads.length).toBe(2);
expect(boughtPayloads[1]).toMatchObject({
item_name: "paper plates",
bought: true,
quantity_bought: 2,
});
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
await expect(page.getByText("paper plates")).toHaveCount(0);
await expect(page.getByRole("group", { name: "Map summary" }).locator(".location-map-overview-row", { hasText: "Unmapped" })).toContainText("0 shown");
await page.getByRole("button", { name: "Bought" }).click();
await expect(page.getByRole("group", { name: "Map summary" }).locator(".location-map-overview-row", { hasText: "Unmapped" })).toContainText("2");
const looseBatteriesRow = page.getByRole("button", { name: "loose batteries bought" });
const paperPlatesRow = page.getByRole("button", { name: "paper plates bought" });
await expect(looseBatteriesRow).toBeVisible();
await expect(looseBatteriesRow).toBeDisabled();
await expect(paperPlatesRow).toBeVisible();
await expect(paperPlatesRow).toBeDisabled();
});
test("viewer hides unrelated unmapped items while a zone is selected", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1531,
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,
},
], 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: "Layers" }).click();
await page.getByRole("button", { name: "Unmapped" }).click();
await expect(page.getByRole("group", { name: "Layer filters" })).toBeVisible();
await expect(page.getByRole("group", { name: "Unmapped items" })).toBeVisible();
await expect(page.getByText("Unmapped Items")).toHaveCount(0);
await expect(page.getByText("loose batteries")).toBeVisible();
await page.locator('[data-object-key="1531"]').locator("rect").first().click();
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery");
await expect(page.getByRole("button", { name: "Layers, 1 changed" })).toHaveAttribute("aria-expanded", "false");
await expect(page.getByRole("group", { name: "Layer filters" })).toHaveCount(0);
await expect(page.getByText("french bread")).toBeVisible();
await expect(page.getByRole("group", { name: "Unmapped items" })).toHaveCount(0);
await expect(page.getByText("loose batteries")).toHaveCount(0);
await page.locator(".location-map-background").click({ position: { x: 8, y: 8 } });
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Map Details");
await expect(page.getByRole("group", { name: "Unmapped items" })).toBeVisible();
await expect(page.getByText("Unmapped Items")).toHaveCount(0);
await expect(page.getByText("loose batteries")).toBeVisible();
});
test("viewer can buy selected zone items from the map", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1532,
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,
},
], true);
const boughtPayloads: Array<Record<string, unknown>> = [];
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/list/item", async (route) => {
if (route.request().method() !== "PATCH") {
await route.fulfill({ status: 404, contentType: "application/json", body: "{}" });
return;
}
boughtPayloads.push(await route.request().postDataJSON());
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ success: true }),
});
});
await page.goto("/stores/100/locations/10/map");
const bakeryArea = page.locator('[data-object-key="1532"]');
await bakeryArea.locator("rect").first().click();
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery");
await expect(page.locator(".location-map-sheet-count")).toHaveText("2 items");
await expect(bakeryArea.locator(".location-map-count")).toHaveText("2 items");
await page.getByRole("button", { name: "Mark french bread bought" }).click();
await expect(page.locator(".confirm-buy-modal")).toBeVisible();
await expect(page.getByRole("dialog", { name: "french bread" })).toBeVisible();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("french bread");
await expect(page.getByRole("img", { name: "No item photo" })).toHaveText("\uD83D\uDCE6");
const nextItemButton = page.getByRole("button", { name: "Next item" });
const nextItemButtonBox = await nextItemButton.boundingBox();
expect(nextItemButtonBox).not.toBeNull();
if (!nextItemButtonBox) throw new Error("Map buy next item button was not measurable");
expect(nextItemButtonBox.width).toBeGreaterThanOrEqual(40);
expect(nextItemButtonBox.height).toBeGreaterThanOrEqual(40);
await nextItemButton.click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("sourdough");
await page.keyboard.press("ArrowLeft");
await expect(page.locator(".confirm-buy-item-name")).toHaveText("french bread");
await page.keyboard.press("ArrowRight");
await expect(page.locator(".confirm-buy-item-name")).toHaveText("sourdough");
const previousItemButton = page.getByRole("button", { name: "Previous item" });
const previousItemButtonBox = await previousItemButton.boundingBox();
expect(previousItemButtonBox).not.toBeNull();
if (!previousItemButtonBox) throw new Error("Map buy previous item button was not measurable");
expect(previousItemButtonBox.width).toBeGreaterThanOrEqual(40);
expect(previousItemButtonBox.height).toBeGreaterThanOrEqual(40);
await previousItemButton.click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("french bread");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await expect.poll(() => boughtPayloads.length).toBe(1);
expect(boughtPayloads[0]).toMatchObject({
item_name: "french bread",
bought: true,
quantity_bought: 1,
});
await expect(page.locator(".confirm-buy-item-name")).toHaveText("sourdough");
await expect(page.locator(".location-map-sheet-count")).toHaveText("1 shown");
await expect(bakeryArea.locator(".location-map-count")).toHaveText("1 shown");
await expect(page.getByRole("group", { name: "Zone items" })).not.toContainText("french bread");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await expect.poll(() => boughtPayloads.length).toBe(2);
expect(boughtPayloads[1]).toMatchObject({
item_name: "sourdough",
bought: true,
quantity_bought: 1,
});
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
await expect(page.locator(".location-map-sheet-count")).toHaveText("0 shown");
await expect(bakeryArea.locator(".location-map-count")).toHaveText("0 shown");
const hiddenZoneState = page.getByRole("group", { name: "Zone items" }).locator(".location-map-inline-state", { hasText: "Hidden" });
await expect(hiddenZoneState).toContainText("2");
await expect(hiddenZoneState.getByRole("button", { name: "Show Zone Items" })).toHaveText("Show");
await expect(page.getByText("Items are assigned here, but hidden by layer filters.")).toHaveCount(0);
await page.getByRole("button", { name: "Layers" }).click();
await page.getByRole("button", { name: "Bought" }).click();
await expect(page.locator(".location-map-sheet-count")).toHaveText("2 items");
await expect(page.getByRole("button", { name: "french bread bought" })).toBeDisabled();
await expect(page.getByRole("button", { name: "sourdough bought" })).toBeDisabled();
});
test("viewer can close map buy modal with Escape", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1533,
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,
},
], true);
const boughtPayloads: Array<Record<string, unknown>> = [];
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/list/item", async (route) => {
if (route.request().method() === "PATCH") {
boughtPayloads.push(await route.request().postDataJSON());
}
await route.fulfill({ status: 404, contentType: "application/json", body: "{}" });
});
await page.goto("/stores/100/locations/10/map");
await page.locator('[data-object-key="1533"]').locator("rect").first().click();
await page.getByRole("button", { name: "Mark french bread bought" }).click();
await expect(page.locator(".confirm-buy-modal")).toBeVisible();
await page.keyboard.press("Escape");
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
expect(boughtPayloads).toEqual([]);
await expect(page.getByRole("button", { name: "Mark french bread bought" })).toBeVisible();
});
test("viewer advances map buy modal after partial quantity buys", async ({ page }) => {
await mockMapShell(page);
const partialItems = [
{
id: 4301,
item_name: "flour",
quantity: 3,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
},
{
id: 4302,
item_name: "yeast",
quantity: 1,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
},
];
const updatedFlour = { ...partialItems[0], quantity: 2 };
const mapState = {
...publishedMapState([
{
id: 1534,
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,
},
], true),
items: partialItems,
};
const boughtPayloads: Array<Record<string, unknown>> = [];
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/list/item**", async (route) => {
if (route.request().method() === "GET") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(updatedFlour),
});
return;
}
boughtPayloads.push(await route.request().postDataJSON());
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ success: true }),
});
});
await page.goto("/stores/100/locations/10/map");
await page.locator('[data-object-key="1534"]').locator("rect").first().click();
await page.getByRole("button", { name: "Mark flour bought" }).click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("flour");
const quantityInput = page.getByRole("spinbutton", { name: "Quantity to mark bought" });
await page.keyboard.press("ArrowDown");
await page.keyboard.press("ArrowDown");
await expect(quantityInput).toHaveValue("1");
await page.keyboard.press("ArrowUp");
await expect(quantityInput).toHaveValue("2");
await page.keyboard.press("ArrowDown");
await expect(quantityInput).toHaveValue("1");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await expect.poll(() => boughtPayloads.length).toBe(1);
expect(boughtPayloads[0]).toMatchObject({
item_name: "flour",
bought: true,
quantity_bought: 1,
});
await expect(page.locator(".confirm-buy-item-name")).toHaveText("yeast");
await expect(page.getByRole("button", { name: "Mark flour bought" }).locator("small")).toHaveText("x2");
});
test("viewer keeps map buy flow moving when quantity refresh fails", async ({ page }) => {
await mockMapShell(page);
const partialItems = [
{
id: 4311,
item_name: "flour",
quantity: 3,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
},
{
id: 4312,
item_name: "yeast",
quantity: 1,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
},
];
const mapState = {
...publishedMapState([
{
id: 1535,
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,
},
], true),
items: partialItems,
};
const boughtPayloads: Array<Record<string, unknown>> = [];
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/list/item**", async (route) => {
if (route.request().method() === "GET") {
await route.fulfill({
status: 503,
contentType: "application/json",
body: JSON.stringify({ error: "refresh unavailable" }),
});
return;
}
boughtPayloads.push(await route.request().postDataJSON());
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ success: true }),
});
});
await page.goto("/stores/100/locations/10/map");
await page.locator('[data-object-key="1535"]').locator("rect").first().click();
await page.getByRole("button", { name: "Mark flour bought" }).click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("flour");
await page.locator(".confirm-buy-counter-btn").first().click();
await page.locator(".confirm-buy-counter-btn").first().click();
await expect(page.locator(".confirm-buy-counter-display")).toHaveValue("1");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await expect.poll(() => boughtPayloads.length).toBe(1);
expect(boughtPayloads[0]).toMatchObject({
item_name: "flour",
bought: true,
quantity_bought: 1,
});
await expect(page.getByText("Updated item locally")).toBeVisible();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("yeast");
await expect(page.getByRole("button", { name: "Mark flour bought" }).locator("small")).toHaveText("x2");
await expect(page.getByText("Mark item bought failed")).toHaveCount(0);
});
test("viewer shows completed zone items without rebuy actions", async ({ page }) => {
await mockMapShell(page);
const mapState = {
...publishedMapState([
{
id: 1533,
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,
},
], true),
items: [
{
...items[0],
bought: true,
},
items[1],
],
};
const boughtPayloads: Array<Record<string, unknown>> = [];
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/list/item", async (route) => {
if (route.request().method() !== "PATCH") {
await route.fulfill({ status: 404, contentType: "application/json", body: "{}" });
return;
}
boughtPayloads.push(await route.request().postDataJSON());
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ success: true }),
});
});
await page.goto("/stores/100/locations/10/map");
await page.getByRole("button", { name: "Layers" }).click();
await page.getByRole("button", { name: "Bought" }).click();
await page.locator('[data-object-key="1533"]').locator("rect").first().click();
const boughtRow = page.getByRole("button", { name: "french bread bought" });
await expect(boughtRow).toBeVisible();
await expect(boughtRow).toBeDisabled();
await expect(boughtRow.locator("small")).toHaveText("Bought");
await boughtRow.click({ force: true });
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
expect(boughtPayloads).toHaveLength(0);
await page.getByRole("button", { name: "Mark sourdough bought" }).click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("sourdough");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await expect.poll(() => boughtPayloads.length).toBe(1);
expect(boughtPayloads[0]).toMatchObject({
item_name: "sourdough",
bought: true,
quantity_bought: 1,
});
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
const newlyBoughtRow = page.getByRole("button", { name: "sourdough bought" });
await expect(newlyBoughtRow).toBeVisible();
await expect(newlyBoughtRow).toBeDisabled();
await expect(newlyBoughtRow.locator("small")).toHaveText("Bought");
});
test("viewer shows compact state when unmapped items are hidden by filters", async ({ page }) => {
await mockMapShell(page);
const mapState = {
...publishedMapState([], true),
items: [
{
id: 4100,
item_name: "other batteries",
quantity: 1,
bought: false,
zone_id: null,
zone: null,
added_by_users: ["other-user"],
},
],
unmapped_count: 1,
};
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: "Layers" }).click();
await page.getByRole("button", { name: "Unmapped" }).click();
await expect(page.getByText("other batteries")).toHaveCount(0);
const hiddenUnmappedState = page.getByRole("group", { name: "Unmapped items" }).locator(".location-map-inline-state", { hasText: "Hidden" });
await expect(hiddenUnmappedState).toContainText("1");
await expect(hiddenUnmappedState.getByRole("button", { name: "Show Unmapped Items" })).toHaveText("Show");
await expect(page.getByText("Unmapped items are hidden by layer filters.")).toHaveCount(0);
await page.getByRole("button", { name: "Show Unmapped Items" }).click();
await expect(page.getByText("other batteries")).toBeVisible();
await expect(hiddenUnmappedState).toHaveCount(0);
await expect(page.getByRole("button", { name: "Layers, 3 changed" })).toBeVisible();
});
test("viewer marks partial unmapped counts when filters hide some items", async ({ page }) => {
await mockMapShell(page);
const mapState = {
...publishedMapState([], true),
items: [
{
id: 4110,
item_name: "visible batteries",
quantity: 1,
bought: false,
zone_id: null,
zone: null,
added_by_users: ["map-user"],
},
{
id: 4111,
item_name: "hidden batteries",
quantity: 1,
bought: false,
zone_id: null,
zone: null,
added_by_users: ["other-user"],
},
],
unmapped_count: 2,
};
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: "Layers" }).click();
await page.getByRole("button", { name: "Unmapped" }).click();
await expect(page.getByText("visible batteries")).toBeVisible();
await expect(page.getByText("hidden batteries")).toHaveCount(0);
const hiddenUnmappedState = page.getByRole("group", { name: "Unmapped items" }).locator(".location-map-inline-state", { hasText: "Hidden" });
await expect(hiddenUnmappedState).toContainText("1");
await expect(hiddenUnmappedState.getByRole("button", { name: "Show All Unmapped Items" })).toHaveText("Show All");
await expect(page.getByText("1 more hidden by layer filters.")).toHaveCount(0);
await page.getByRole("button", { name: "Show All Unmapped Items" }).click();
await expect(page.getByText("hidden batteries")).toBeVisible();
await expect(hiddenUnmappedState).toHaveCount(0);
});
test("viewer shows compact state 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 shown");
await expect(page.locator(".location-map-pin")).toHaveCount(0);
await frozenArea.locator("rect").first().click();
await expect(page.locator(".location-map-sheet-count")).toHaveText("0 shown");
const hiddenZoneState = page.getByRole("group", { name: "Zone items" }).locator(".location-map-inline-state", { hasText: "Hidden" });
await expect(hiddenZoneState).toContainText("1");
await expect(hiddenZoneState.getByRole("button", { name: "Show Zone Items" })).toHaveText("Show");
await expect(page.getByText("Items are assigned here, but hidden by layer filters.")).toHaveCount(0);
await expect(page.getByText("frozen salmon")).toHaveCount(0);
await page.getByRole("button", { name: "Show Zone Items" }).click();
await expect(page.locator(".location-map-sheet-count")).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 marks partial zone counts when filters hide some items", async ({ page }) => {
await mockMapShell(page);
const mapState = {
...publishedMapState([
{
id: 1552,
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,
},
], true),
items: [
{
id: 5001,
item_name: "visible bread",
quantity: 1,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
},
{
id: 5002,
item_name: "hidden baguette",
quantity: 1,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["other-user"],
},
],
};
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 bakeryArea = page.locator('[data-object-key="1552"]');
await expect(bakeryArea.locator(".location-map-count")).toHaveText("1 shown");
await bakeryArea.locator("rect").first().click();
await expect(page.locator(".location-map-sheet-count")).toHaveText("1 shown");
await expect(page.getByText("visible bread")).toBeVisible();
await expect(page.getByText("hidden baguette")).toHaveCount(0);
const hiddenZoneState = page.getByRole("group", { name: "Zone items" }).locator(".location-map-inline-state", { hasText: "Hidden" });
await expect(hiddenZoneState).toContainText("1");
await expect(hiddenZoneState.getByRole("button", { name: "Show All Zone Items" })).toHaveText("Show All");
await expect(page.getByText("1 more hidden by layer filters.")).toHaveCount(0);
await page.getByRole("button", { name: "Show All Zone Items" }).click();
await expect(page.locator(".location-map-sheet-count")).toHaveText("2 items");
await expect(page.getByText("hidden baguette")).toBeVisible();
await expect(bakeryArea.locator(".location-map-count")).toHaveText("2 items");
});
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-sheet-count")).toHaveText("0 items");
await expect(page.getByRole("group", { name: "Zone items" })).toHaveCount(0);
await expect(page.locator(".location-map-inline-state", { hasText: "Items" })).toHaveCount(0);
await expect(page.getByText("No items assigned to this zone yet.")).toHaveCount(0);
await page.locator('[data-object-key="1561"]').locator("rect").first().click();
await expect(page.locator(".location-map-sheet-count")).toHaveText("14 items");
const zoneItems = page.getByRole("group", { name: "Zone items" });
await expect(zoneItems.locator(".location-map-item-row")).toHaveCount(8);
await expect(zoneItems.locator(".location-map-item-row").first()).toContainText("bulk item 1");
await expect(page.getByText("bulk item 9")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Show 6 more zone items" })).toHaveText("+6 more");
const firstZoneItemButtonBox = await page.getByRole("button", { name: "Mark bulk item 1 bought" }).boundingBox();
const moreZoneItemsButtonBox = await page.getByRole("button", { name: "Show 6 more zone items" }).boundingBox();
expect(firstZoneItemButtonBox).not.toBeNull();
expect(moreZoneItemsButtonBox).not.toBeNull();
if (!firstZoneItemButtonBox || !moreZoneItemsButtonBox) {
throw new Error("Zone item controls were not measurable");
}
expect(firstZoneItemButtonBox.height).toBeGreaterThanOrEqual(40);
expect(moreZoneItemsButtonBox.height).toBeGreaterThanOrEqual(40);
await page.getByRole("button", { name: "Show 6 more zone items" }).click();
await expect(zoneItems.locator(".location-map-item-row")).toHaveCount(14);
await expect(zoneItems.locator(".location-map-item-row").last()).toContainText("bulk item 14");
await expect(page.getByRole("button", { name: "Show fewer zone items" })).toHaveText("Show fewer");
await expect(page.locator(".location-map-pin")).toHaveCount(0);
});
test("viewer collapses expanded zone items after selecting another zone", async ({ page }) => {
await mockMapShell(page);
const bakeryItems = Array.from({ length: 11 }, (_, index) => ({
id: 5100 + index,
item_name: `bakery item ${index + 1}`,
quantity: 1,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
}));
const frozenItems = Array.from({ length: 10 }, (_, index) => ({
id: 5200 + index,
item_name: `frozen item ${index + 1}`,
quantity: 1,
bought: false,
zone_id: 502,
zone: "Frozen Foods",
added_by_users: ["map-user"],
}));
const mapState = {
...publishedMapState([
{
id: 1563,
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: 1564,
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: [...bakeryItems, ...frozenItems],
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 page.locator('[data-object-key="1563"]').locator("rect").first().click();
const zoneItems = page.getByRole("group", { name: "Zone items" });
await expect(zoneItems.locator(".location-map-item-row")).toHaveCount(8);
await page.getByRole("button", { name: "Show 3 more zone items" }).click();
await expect(zoneItems.locator(".location-map-item-row")).toHaveCount(11);
await expect(page.getByText("bakery item 11")).toBeVisible();
await page.locator('[data-object-key="1564"]').locator("rect").first().click();
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Frozen Foods");
await expect(zoneItems.locator(".location-map-item-row")).toHaveCount(8);
await expect(page.getByText("frozen item 8")).toBeVisible();
await expect(page.getByText("frozen item 9")).toHaveCount(0);
await expect(page.getByText("bakery item 11")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Show 2 more zone items" })).toHaveText("+2 more");
await expect(page.getByRole("button", { name: "Show fewer zone items" })).toHaveCount(0);
});
test("viewer map areas are keyboard selectable", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1571,
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: 1572,
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);
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 bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
const frozenArea = page.getByRole("button", { name: "Map area Frozen Foods" });
const bakeryRect = page.locator('[data-object-key="1571"]').locator("rect").first();
await expect(page.getByRole("button", { name: "Map area Bakery, 2 items", exact: true })).toBeVisible();
await expect(page.getByRole("button", { name: "Map area Frozen Foods, 0 shown", exact: true })).toBeVisible();
await expect(bakeryArea).toHaveAttribute("aria-pressed", "false");
const initialFill = await bakeryRect.evaluate((element) => getComputedStyle(element).fill);
await bakeryRect.hover();
await expect.poll(async () => (
bakeryRect.evaluate((element) => getComputedStyle(element).fill)
)).not.toBe(initialFill);
await bakeryArea.focus();
await expect(bakeryArea).toBeFocused();
await page.keyboard.press("Enter");
await expect(bakeryArea).toHaveAttribute("aria-pressed", "true");
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery");
await expect(page.locator(".location-map-sheet-count")).toHaveText("2 items");
await expect(page.getByText("french bread")).toBeVisible();
await frozenArea.focus();
await expect(frozenArea).toBeFocused();
await page.keyboard.press("Space");
await expect(frozenArea).toHaveAttribute("aria-pressed", "true");
await expect(bakeryArea).toHaveAttribute("aria-pressed", "false");
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Frozen Foods");
await expect(page.locator(".location-map-sheet-count")).toHaveText("0 shown");
await page.keyboard.press("Escape");
await expect(frozenArea).toBeFocused();
await expect(frozenArea).toHaveAttribute("aria-pressed", "false");
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Map Details");
await expect(page.getByRole("group", { name: "Map summary" })).toBeVisible();
});
test("mobile starts fitted and fit returns panned maps into the canvas", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1581,
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,
},
], 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 { scroll } = await expectMapFitsCanvas(page, 45, "Initial mobile");
const zoomIn = page.getByRole("button", { name: "Zoom in" });
await zoomIn.click();
await zoomIn.click();
await zoomIn.click();
await scroll.evaluate((element) => {
element.scrollLeft = 320;
element.scrollTop = 240;
});
const pannedScroll = await scroll.evaluate((element) => ({
left: element.scrollLeft,
top: element.scrollTop,
}));
expect(pannedScroll.left).toBeGreaterThan(0);
expect(pannedScroll.top).toBeGreaterThan(0);
await page.getByRole("button", { name: "Fit" }).click();
await expectMapFitsCanvas(page, 45, "Mobile");
await expect.poll(async () => scroll.evaluate((element) => ({
left: element.scrollLeft,
top: element.scrollTop,
}))).toEqual({ left: 0, top: 0 });
});
test("compact tablet map starts fitted on first load", async ({ page }) => {
await page.setViewportSize({ width: 768, height: 720 });
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1586,
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,
},
], 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 toolbar = page.locator(".location-map-toolbar");
const modeButtons = page.locator(".location-map-mode-buttons");
const historyActions = page.locator(".location-map-history-buttons");
const zoomControls = page.locator(".location-map-zoom-controls");
const viewModeBox = await modeButtons.boundingBox();
const viewZoomBox = await zoomControls.boundingBox();
expect(viewModeBox).not.toBeNull();
expect(viewZoomBox).not.toBeNull();
if (!viewModeBox || !viewZoomBox) {
throw new Error("Compact tablet toolbar layout was not measurable");
}
expect(viewModeBox.width).toBeGreaterThanOrEqual(180);
expect(viewModeBox.width).toBeLessThanOrEqual(190);
await expect(historyActions).toHaveCount(0);
expect(viewZoomBox.x).toBeGreaterThan(viewModeBox.x + viewModeBox.width - 1);
await page.getByRole("button", { name: "Edit Draft" }).click();
await expect(historyActions).toHaveCount(1);
const toolbarBox = await toolbar.boundingBox();
const editModeBox = await modeButtons.boundingBox();
const editHistoryBox = await historyActions.boundingBox();
const editZoomBox = await zoomControls.boundingBox();
expect(toolbarBox).not.toBeNull();
expect(editModeBox).not.toBeNull();
expect(editHistoryBox).not.toBeNull();
expect(editZoomBox).not.toBeNull();
if (!toolbarBox || !editModeBox || !editHistoryBox || !editZoomBox) {
throw new Error("Compact tablet edit toolbar layout was not measurable");
}
expect(Math.abs(editModeBox.x - viewModeBox.x)).toBeLessThanOrEqual(1);
expect(Math.abs(editModeBox.width - viewModeBox.width)).toBeLessThanOrEqual(1);
expect(editHistoryBox.x).toBeGreaterThan(editModeBox.x + editModeBox.width - 1);
expect(editZoomBox.x).toBeGreaterThan(editHistoryBox.x + editHistoryBox.width - 1);
expect(editZoomBox.x + editZoomBox.width).toBeLessThanOrEqual(toolbarBox.x + toolbarBox.width + 1);
await expectMapFitsCanvas(page, 75, "Compact tablet");
});
test("zoom controls disable at map zoom limits", async ({ page }) => {
await mockMapShell(page);
const mapState = publishedMapState([
{
id: 1591,
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,
},
], 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 zoomText = page.locator(".location-map-zoom-controls span");
const zoomIn = page.getByRole("button", { name: "Zoom in" });
const zoomOut = page.getByRole("button", { name: "Zoom out" });
await expect(zoomIn).toBeEnabled();
await expect(zoomOut).toBeEnabled();
for (let index = 0; index < 6; index += 1) {
await zoomIn.click();
}
await expect(zoomText).toHaveText("160%");
await expect(zoomIn).toBeDisabled();
await expect(zoomOut).toBeEnabled();
for (let index = 0; index < 9; index += 1) {
await zoomOut.click();
}
await expect(zoomText).toHaveText("30%");
await expect(zoomOut).toBeDisabled();
await expect(zoomIn).toBeEnabled();
});
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")).toHaveCount(0);
await expect(page.locator(".location-map-mobile-tool-switch")).toHaveCount(0);
const historyActions = page.locator(".location-map-history-buttons");
await expect(historyActions.getByRole("button", { name: "Undo" })).toBeVisible();
await expect(historyActions.getByRole("button", { name: "Redo" })).toBeVisible();
await expect(historyActions.getByRole("button", { name: "Undo" })).toBeDisabled();
await expect(historyActions.getByRole("button", { name: "Redo" })).toBeDisabled();
const primaryActions = page.locator(".location-map-primary-actions");
const secondaryActions = page.locator(".location-map-secondary-actions");
await expect(primaryActions.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(primaryActions.getByRole("button", { name: "Publish" })).toBeVisible();
await expect(secondaryActions.getByRole("button", { name: "Add Area" })).toBeVisible();
await expect(secondaryActions.getByRole("button", { name: "View Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "View", exact: true })).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);
const secondaryActionButtons = secondaryActions.getByRole("button");
const secondaryActionButtonCount = await secondaryActionButtons.count();
for (let index = 0; index < secondaryActionButtonCount; index += 1) {
const actionButtonBox = await secondaryActionButtons.nth(index).boundingBox();
expect(actionButtonBox).not.toBeNull();
if (!actionButtonBox) throw new Error("Mobile secondary action was not measurable");
expect(actionButtonBox.height).toBeGreaterThanOrEqual(40);
}
await page.getByRole("button", { name: "Layers" }).click();
await expect(page.getByRole("group", { name: "Layer filters" })).toBeVisible();
await page.getByRole("button", { name: "Pins" }).click();
const resetLayersButtonBox = await page.getByRole("button", { name: "Reset layer filters" }).boundingBox();
expect(resetLayersButtonBox).not.toBeNull();
if (!resetLayersButtonBox) throw new Error("Mobile layer reset button was not measurable");
expect(resetLayersButtonBox.height).toBeGreaterThanOrEqual(40);
expect(resetLayersButtonBox.width).toBeLessThanOrEqual(44);
await expect(page.locator(".location-map-layer-reset-icon")).toHaveCount(1);
await page.getByRole("button", { name: "Reset layer filters" }).click();
await page.getByRole("button", { name: "Layers" }).click();
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();
const sheetHandleBox = await page.locator(".location-map-bottom-sheet-handle").boundingBox();
expect(canvasBox).not.toBeNull();
expect(sheetBox).not.toBeNull();
expect(sheetHandleBox).not.toBeNull();
if (!canvasBox || !sheetBox || !sheetHandleBox) throw new Error("Mobile map layout was not measurable");
expect(canvasBox.height).toBeGreaterThan(260);
expect(sheetBox.height).toBeLessThanOrEqual(360);
expect(sheetHandleBox.width).toBeGreaterThanOrEqual(36);
expect(sheetHandleBox.height).toBeLessThanOrEqual(6);
await page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first().click();
const labelInput = page.getByRole("textbox", { name: "Label" });
await expect(labelInput).toBeVisible();
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Bakery");
await expect(page.locator(".location-map-object").last()).toHaveAttribute("data-object-key", "1601");
await expect(page.getByLabel("Object type")).toHaveCount(0);
await expect(page.locator(".location-map-geometry-controls")).toHaveCount(0);
await expect(page.getByLabel("X position")).toHaveCount(0);
await expect(page.getByLabel("Y position")).toHaveCount(0);
await expect(page.getByLabel("Width")).toHaveCount(0);
await expect(page.getByLabel("Height")).toHaveCount(0);
const fieldRows = page.locator(".location-map-field-row");
await expect(fieldRows).toHaveCount(2);
const objectFlagButtons = page.locator(".location-map-object-flags").getByRole("button");
await expect(objectFlagButtons).toHaveCount(2);
await expect(page.locator(".location-map-object-flags input[type='checkbox']")).toHaveCount(0);
const visibleFlag = page.getByRole("button", { name: "Visible" });
const lockedFlag = page.getByRole("button", { name: "Locked" });
await expect(page.locator(".location-map-object-flag-state")).toHaveCount(2);
await expect(visibleFlag).toHaveAttribute("aria-pressed", "true");
await expect(visibleFlag.locator(".location-map-object-flag-state.is-on")).toHaveCount(1);
await expect(lockedFlag).toHaveAttribute("aria-pressed", "false");
await expect(lockedFlag.locator(".location-map-object-flag-state.is-on")).toHaveCount(0);
const selectedPrimaryActions = page.locator(".location-map-selected-primary-actions");
const selectedSecondaryActions = page.locator(".location-map-selected-secondary-actions");
await expect(selectedPrimaryActions.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(selectedPrimaryActions.getByRole("button", { name: "Publish" })).toBeVisible();
await expect(selectedSecondaryActions.getByRole("button", { name: "Add Area" })).toBeVisible();
await expect(selectedSecondaryActions.getByRole("button", { name: "View Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Bought" })).toHaveCount(0);
const selectedPrimaryBox = await selectedPrimaryActions.boundingBox();
const selectedSecondaryButtonBox = await selectedSecondaryActions.getByRole("button", { name: "Add Area" }).boundingBox();
const labelBox = await labelInput.boundingBox();
const firstFieldRowBox = await fieldRows.first().boundingBox();
const firstFlagButtonBox = await objectFlagButtons.first().boundingBox();
expect(selectedPrimaryBox).not.toBeNull();
expect(selectedSecondaryButtonBox).not.toBeNull();
expect(labelBox).not.toBeNull();
expect(firstFieldRowBox).not.toBeNull();
expect(firstFlagButtonBox).not.toBeNull();
if (!selectedPrimaryBox || !selectedSecondaryButtonBox || !labelBox || !firstFieldRowBox || !firstFlagButtonBox) {
throw new Error("Selected object draft actions were not measurable");
}
expect(selectedPrimaryBox.y).toBeLessThan(labelBox.y);
expect(selectedSecondaryButtonBox.height).toBeGreaterThanOrEqual(40);
expect(firstFieldRowBox.height).toBeLessThanOrEqual(54);
expect(firstFlagButtonBox.height).toBeGreaterThanOrEqual(40);
const closeSelectedButton = page.getByRole("button", { name: "Close selected map area" });
await expect(closeSelectedButton).toBeVisible();
const closeSelectedButtonBox = await closeSelectedButton.boundingBox();
expect(closeSelectedButtonBox).not.toBeNull();
if (!closeSelectedButtonBox) throw new Error("Selected area close button was not measurable");
expect(closeSelectedButtonBox.width).toBeGreaterThanOrEqual(40);
expect(closeSelectedButtonBox.height).toBeGreaterThanOrEqual(40);
await closeSelectedButton.click();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveCount(0);
await expect(page.locator(".location-map-sheet-header strong")).toHaveText("Edit Areas");
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await page.getByRole("button", { name: "View", exact: true }).click();
await expect(page.getByRole("textbox", { name: "Label" })).toHaveCount(0);
});
test("mobile edit selection keeps resize handle in reach", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1606,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Bakery",
x: 720,
y: 480,
width: 240,
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();
const scroll = page.locator(".location-map-scroll");
const bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
const zoomIn = page.getByRole("button", { name: "Zoom in" });
await zoomIn.click();
await zoomIn.click();
await zoomIn.click();
await bakeryArea.focus();
await scroll.evaluate((element) => {
element.scrollLeft = 0;
element.scrollTop = 0;
});
await expect.poll(async () => scroll.evaluate((element) => element.scrollLeft)).toBe(0);
await expect.poll(async () => scroll.evaluate((element) => element.scrollTop)).toBe(0);
await page.keyboard.press("Enter");
await expect(bakeryArea).toHaveAttribute("aria-pressed", "true");
await expect.poll(async () => scroll.evaluate((element) => element.scrollLeft)).toBeGreaterThan(200);
await expect.poll(async () => scroll.evaluate((element) => element.scrollTop)).toBeGreaterThan(80);
await expectResizeHandleWithinMapScroll(page);
});
test("mobile edit zoom keeps selected resize handle in reach", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1608,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Bakery",
x: 720,
y: 480,
width: 240,
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();
const scroll = page.locator(".location-map-scroll");
const bakeryArea = page.getByRole("button", { name: "Map area Bakery" });
await bakeryArea.focus();
await page.keyboard.press("Enter");
await expect(bakeryArea).toHaveAttribute("aria-pressed", "true");
await expectResizeHandleWithinMapScroll(page);
await scroll.evaluate((element) => {
element.scrollLeft = 0;
element.scrollTop = 0;
});
await expect.poll(async () => scroll.evaluate((element) => element.scrollLeft)).toBe(0);
await expect.poll(async () => scroll.evaluate((element) => element.scrollTop)).toBe(0);
const zoomIn = page.getByRole("button", { name: "Zoom in" });
await zoomIn.click();
await zoomIn.click();
await zoomIn.click();
await expect.poll(async () => scroll.evaluate((element) => element.scrollLeft)).toBeGreaterThan(200);
await expect.poll(async () => scroll.evaluate((element) => element.scrollTop)).toBeGreaterThan(80);
await expectResizeHandleWithinMapScroll(page);
});
test("mobile editor pans from empty map space without moving areas", async ({ page }) => {
await page.setViewportSize({ width: 390, height: 844 });
await mockMapShell(page);
const mapState = draftMapState([
{
id: 1611,
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();
const scroll = page.locator(".location-map-scroll");
const scrollBox = await scroll.boundingBox();
expect(scrollBox).not.toBeNull();
if (!scrollBox) throw new Error("Mobile edit map scroll area was not measurable");
const bakeryArea = page.locator('[data-object-key="1611"]').locator("rect").first();
await expect(bakeryArea).toHaveAttribute("x", "40");
await expect(bakeryArea).toHaveAttribute("y", "40");
const zoomIn = page.getByRole("button", { name: "Zoom in" });
await zoomIn.click();
await zoomIn.click();
await zoomIn.click();
const beforePan = await scroll.evaluate((element) => ({
left: element.scrollLeft,
top: element.scrollTop,
}));
expect(beforePan).toEqual({ left: 0, top: 0 });
const startX = scrollBox.x + scrollBox.width - 48;
const startY = scrollBox.y + scrollBox.height - 48;
await page.mouse.move(startX, startY);
await page.mouse.down();
await page.mouse.move(startX - 140, startY - 100, { steps: 8 });
await page.mouse.up();
await expect.poll(async () =>
scroll.evaluate((element) => element.scrollLeft)
).toBeGreaterThan(80);
await expect.poll(async () =>
scroll.evaluate((element) => element.scrollTop)
).toBeGreaterThan(50);
await expect(bakeryArea).toHaveAttribute("x", "40");
await expect(bakeryArea).toHaveAttribute("y", "40");
await expect(page.locator(".location-map-status")).toHaveText("Draft");
});
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 can buy items from a published map", async ({ page }) => {
await mockMapShell(page, memberHousehold);
const memberItems = [
{
id: 1721,
item_name: "bagels",
quantity: 1,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
},
];
const mapState = {
...publishedMapState([
{
id: 1720,
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),
items: memberItems,
};
const boughtPayloads: Array<Record<string, unknown>> = [];
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/list/item**", async (route) => {
boughtPayloads.push(await route.request().postDataJSON());
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ success: true }),
});
});
await page.goto("/stores/100/locations/10/map");
await expect(page.getByRole("button", { name: "Edit Draft" })).toHaveCount(0);
await page.getByRole("button", { name: "Map area Bakery, 1 item" }).click();
await page.getByRole("button", { name: "Mark bagels bought" }).click();
await expect(page.locator(".confirm-buy-item-name")).toHaveText("bagels");
await page.getByRole("button", { name: "Mark as Bought" }).click();
await expect.poll(() => boughtPayloads.length).toBe(1);
expect(boughtPayloads[0]).toMatchObject({
item_name: "bagels",
bought: true,
quantity_bought: 1,
});
await expect(page.locator(".confirm-buy-modal")).toHaveCount(0);
const hiddenZoneState = page.getByRole("group", { name: "Zone items" }).locator(".location-map-inline-state", { hasText: "Hidden" });
await expect(hiddenZoneState).toContainText("1");
await expect(page.getByRole("button", { name: "bagels bought" })).toHaveCount(0);
await hiddenZoneState.getByRole("button", { name: "Show Zone Items" }).click();
await expect(page.getByRole("button", { name: "bagels bought" })).toBeDisabled();
});
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 Published Map" })).toBeVisible();
await expect(page.getByRole("heading", { name: "No Map" })).toHaveCount(0);
await expect(page.getByRole("group", { name: "Map setup details" })).toHaveCount(0);
await expect(page.getByText("Not published")).toHaveCount(0);
await expect(page.getByText("A household admin has not published a map yet.")).toHaveCount(0);
await expect(page.getByText("Not started")).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);
});
test("members do not see unpublished draft maps", async ({ page }) => {
await mockMapShell(page, memberHousehold);
const mapState = draftMapState([
{
id: 1701,
location_map_id: 900,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Draft 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.getByRole("heading", { name: "No Published Map" })).toBeVisible();
await expect(page.getByRole("heading", { name: "No Map" })).toHaveCount(0);
await expect(page.getByRole("group", { name: "Map setup details" })).toHaveCount(0);
await expect(page.getByText("Not published")).toHaveCount(0);
await expect(page.getByText("A household admin has not published a map yet.")).toHaveCount(0);
await expect(page.getByRole("heading", { name: "Draft Map" })).toHaveCount(0);
await expect(page.getByText("A map draft exists for this location")).toHaveCount(0);
await expect(page.getByText("Draft Bakery")).toHaveCount(0);
await expect(page.getByRole("button", { name: "Continue Editing" })).toHaveCount(0);
await expect(page.getByLabel("Map controls")).toHaveCount(0);
});