grocery-app/frontend/tests/location-map-manager.spec.ts
2026-06-02 01:59:20 -07:00

303 lines
8.1 KiB
TypeScript

import { expect, test, type Page } from "@playwright/test";
import { mockConfig, seedAuthStorage } from "./helpers/e2e";
const adminHousehold = { id: 1, name: "Map House", role: "admin", invite_code: "ABCD1234" };
const memberHousehold = { ...adminHousehold, role: "member" };
const storeLocation = {
id: 10,
household_store_id: 100,
household_id: 1,
name: "Costco",
location_name: "Eastvale",
display_name: "Costco - Eastvale",
is_default: true,
};
const zones = [
{ id: 501, name: "Bakery", sort_order: 10, item_count: 2 },
{ id: 502, name: "Frozen Foods", sort_order: 20, item_count: 1 },
];
const items = [
{
id: 1,
item_name: "french bread",
quantity: 1,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
},
{
id: 2,
item_name: "sourdough",
quantity: 1,
bought: false,
zone_id: 501,
zone: "Bakery",
added_by_users: ["map-user"],
},
{
id: 3,
item_name: "frozen salmon",
quantity: 1,
bought: false,
zone_id: 502,
zone: "Frozen Foods",
added_by_users: ["other-user"],
},
{
id: 4,
item_name: "loose batteries",
quantity: 1,
bought: false,
zone_id: null,
zone: null,
added_by_users: ["map-user"],
},
];
function noMapState(canManage = true) {
return {
location: storeLocation,
map: null,
draft_map: null,
published_map: null,
objects: [],
draft_objects: [],
published_objects: [],
zones,
items,
unmapped_count: 1,
can_manage: canManage,
};
}
function draftMapState(objects: Array<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,
};
}
async function mockMapShell(page: Page, household = adminHousehold) {
await seedAuthStorage(page, { username: "map-user", role: household.role });
await mockConfig(page);
await page.route("**/households", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([household]),
});
});
await page.route("**/households/1/stores", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([storeLocation]),
});
});
await page.route("**/households/1/locations/10/zones", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ zones }),
});
});
}
test("admin creates a map from zones, saves a draft, and publishes it", async ({ page }) => {
await mockMapShell(page);
let mapState = noMapState(true);
let savedPayload: Record<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 Map From Existing Zones" })).toBeVisible();
await page.getByRole("button", { name: "Create Map From Existing Zones" }).click();
await expect(page.getByText("Bakery")).toBeVisible();
await expect(page.getByText("Frozen Foods")).toBeVisible();
await expect(page.locator(".location-map-pin")).toHaveCount(0);
const bakeryObject = page.locator(".location-map-object", { hasText: "Bakery" }).locator("rect").first();
await bakeryObject.click();
await page.getByRole("textbox", { name: "Label" }).fill("Bread Wall");
await page.getByRole("button", { name: "Save Draft" }).click();
expect(savedPayload).not.toBeNull();
expect(savedPayload?.objects).toEqual(
expect.arrayContaining([
expect.objectContaining({ label: "Bread Wall", zone_id: 501 }),
])
);
await page.getByRole("button", { name: "Publish" }).click();
await expect(page.locator(".location-map-status")).toHaveText("Published");
await expect(page.getByRole("button", { name: "Edit Map" })).toBeVisible();
await expect(page.getByText("loose batteries")).toHaveCount(0);
await page.locator(".location-map-object", { hasText: "Bread Wall" }).locator("rect").first().click();
await expect(page.getByText("french bread")).toBeVisible();
await expect(page.getByText("sourdough")).toBeVisible();
await expect(page.getByText("frozen salmon")).toHaveCount(0);
await page.getByLabel("Unmapped").check();
await expect(page.getByText("loose batteries")).toBeVisible();
});
test("members can view a published map but cannot edit it", async ({ page }) => {
await mockMapShell(page, memberHousehold);
const mapState = publishedMapState([
{
id: 1201,
location_map_id: 901,
zone_id: 501,
zone_name: "Bakery",
type: "zone",
label: "Bakery",
x: 40,
y: 40,
width: 260,
height: 160,
rotation: 0,
locked: false,
visible: true,
sort_order: 1,
},
], false);
await page.route("**/households/1/locations/10/map", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(mapState),
});
});
await page.goto("/stores/100/locations/10/map");
await expect(page.getByText("Published")).toBeVisible();
await expect(page.getByText("Bakery")).toBeVisible();
await expect(page.getByRole("button", { name: "Edit Map" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Save Draft" })).toHaveCount(0);
await expect(page.getByRole("button", { name: "Publish" })).toHaveCount(0);
});