5112 lines
178 KiB
TypeScript
5112 lines
178 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 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");
|
|
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");
|
|
|
|
await page.getByRole("button", { name: "Decrease quantity" }).click();
|
|
await page.getByRole("button", { name: "Decrease quantity" }).click();
|
|
await expect(page.getByRole("spinbutton", { name: "Quantity to mark bought" })).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 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);
|
|
});
|