480 lines
17 KiB
TypeScript
480 lines
17 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
confirmSlide,
|
|
mockConfig,
|
|
mockHouseholdAndStoreShell,
|
|
seedAuthStorage,
|
|
} from "./helpers/e2e";
|
|
|
|
test("manage stores opens a modal to edit and delete household store items", async ({ page }) => {
|
|
await seedAuthStorage(page, { username: "catalog-user" });
|
|
await mockConfig(page);
|
|
await mockHouseholdAndStoreShell(page, {
|
|
household: { name: "Catalog House" },
|
|
});
|
|
|
|
let availableItems = [
|
|
{
|
|
item_id: 501,
|
|
item_name: "milk",
|
|
item_image: null,
|
|
image_mime_type: null,
|
|
item_type: "dairy",
|
|
item_group: "Milk",
|
|
zone: "Dairy & Refrigerated",
|
|
has_managed_settings: true,
|
|
},
|
|
{
|
|
item_id: 777,
|
|
item_name: "apples",
|
|
item_image: null,
|
|
image_mime_type: null,
|
|
item_type: null,
|
|
item_group: null,
|
|
zone: null,
|
|
has_managed_settings: false,
|
|
},
|
|
];
|
|
|
|
await page.route("**/households/1/stores", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([{ id: 10, household_store_id: 100, name: "Costco", is_default: true }]),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/available-items*", async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
const query = (url.searchParams.get("query") || "").toLowerCase();
|
|
|
|
if (request.method() === "GET") {
|
|
const filteredItems = availableItems.filter((item) => item.item_name.includes(query));
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ items: filteredItems, catalog_ready: true }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 500 });
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/available-items/777", async (route) => {
|
|
if (route.request().method() === "PATCH") {
|
|
availableItems = availableItems.map((item) =>
|
|
item.item_id === 777
|
|
? {
|
|
...item,
|
|
item_type: "produce",
|
|
item_group: "Fruits",
|
|
zone: "Produce & Fresh Vegetables",
|
|
has_managed_settings: true,
|
|
}
|
|
: item
|
|
);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
message: "Available item updated",
|
|
item: availableItems.find((item) => item.item_id === 777),
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 500 });
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/available-items/501", async (route) => {
|
|
if (route.request().method() === "DELETE") {
|
|
availableItems = availableItems.filter((item) => item.item_id !== 501);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ message: "Store item deleted" }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 500 });
|
|
});
|
|
|
|
await page.goto("/manage?tab=stores");
|
|
|
|
const storeCard = page.locator(".store-card").filter({ hasText: "Costco" });
|
|
await expect(storeCard).toBeVisible();
|
|
await expect(storeCard.getByText("Costco", { exact: true })).toHaveCount(1);
|
|
await expect(storeCard.getByText("Default location")).toBeVisible();
|
|
await expect(storeCard.getByText("Default shopping location")).toHaveCount(0);
|
|
await expect(storeCard.getByRole("button", { name: "Manage Items" })).toBeVisible();
|
|
|
|
await storeCard.getByRole("button", { name: "Manage Items" }).click();
|
|
|
|
const managerModal = page.locator(".store-items-modal");
|
|
await expect(managerModal).toBeVisible();
|
|
await expect(managerModal.getByText("milk", { exact: true })).toBeVisible();
|
|
await expect(managerModal.getByText("apples", { exact: true })).toBeVisible();
|
|
await expect(managerModal.locator(".store-available-items-thumb-placeholder").first()).toHaveText("\uD83D\uDCE6");
|
|
await expect(managerModal.getByText("Store Defaults")).toHaveCount(0);
|
|
await expect(managerModal.getByText("No store defaults set")).toHaveCount(0);
|
|
await expect(managerModal.getByText("Edit Settings", { exact: true })).toHaveCount(0);
|
|
await expect(managerModal.getByText("Delete Item", { exact: true })).toHaveCount(0);
|
|
await expect(managerModal.locator(".store-available-items-action")).toHaveCount(0);
|
|
|
|
const searchBox = await managerModal.getByPlaceholder("Search household/store items").boundingBox();
|
|
const addButtonBox = await managerModal.getByRole("button", { name: "Add Item" }).boundingBox();
|
|
expect(searchBox).not.toBeNull();
|
|
expect(addButtonBox).not.toBeNull();
|
|
expect(
|
|
Math.abs(
|
|
((searchBox?.y ?? 0) + (searchBox?.height ?? 0) / 2) -
|
|
((addButtonBox?.y ?? 0) + (addButtonBox?.height ?? 0) / 2)
|
|
)
|
|
).toBeLessThan(2);
|
|
|
|
const appleRow = managerModal.getByRole("button", { name: "Edit settings for apples" });
|
|
const milkRow = managerModal.locator(".store-items-table-row").filter({ hasText: "milk" });
|
|
|
|
await appleRow.click();
|
|
const editorModal = page.locator(".available-item-editor-modal");
|
|
await expect(editorModal).toBeVisible();
|
|
await expect(editorModal.getByLabel("Item Name")).toBeDisabled();
|
|
await editorModal.locator(".available-item-editor-select").nth(0).selectOption("produce");
|
|
await editorModal.locator(".available-item-editor-select").nth(1).selectOption("Fruits");
|
|
await editorModal.locator(".available-item-editor-select").nth(2).selectOption("Produce & Fresh Vegetables");
|
|
await editorModal.getByRole("button", { name: "Save Changes" }).click();
|
|
|
|
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Updated store item");
|
|
await expect(managerModal.getByText("produce | Fruits | Produce & Fresh Vegetables")).toHaveCount(0);
|
|
|
|
await managerModal.getByRole("button", { name: "Delete Items" }).click();
|
|
await expect(managerModal.getByRole("button", { name: "Confirm Delete (0)" })).toBeDisabled();
|
|
await expect(managerModal.getByRole("button", { name: "Cancel" })).toBeVisible();
|
|
|
|
await milkRow.click();
|
|
await expect(managerModal.getByRole("button", { name: "Confirm Delete (1)" })).toBeEnabled();
|
|
await expect(milkRow).toHaveClass(/is-selected/);
|
|
|
|
await milkRow.click();
|
|
await expect(managerModal.getByRole("button", { name: "Confirm Delete (0)" })).toBeDisabled();
|
|
await expect(milkRow).not.toHaveClass(/is-selected/);
|
|
|
|
await milkRow.click();
|
|
await managerModal.getByRole("button", { name: "Confirm Delete (1)" }).click();
|
|
const confirmModal = page.locator(".confirm-slide-modal");
|
|
await expect(confirmModal).toBeVisible();
|
|
await expect(confirmModal.getByText("Delete milk?")).toBeVisible();
|
|
|
|
await confirmSlide(page);
|
|
|
|
await expect(
|
|
page.locator(".action-toast.action-toast-success").filter({ hasText: "Deleted store item" })
|
|
).toContainText("Deleted store item");
|
|
await expect(managerModal.locator(".store-items-table-row").filter({ hasText: "milk" })).toHaveCount(0);
|
|
});
|
|
|
|
test("manage stores uses modal flows for locations and zones", async ({ page }) => {
|
|
await seedAuthStorage(page, { username: "store-manager", role: "owner" });
|
|
await mockConfig(page);
|
|
await mockHouseholdAndStoreShell(page, {
|
|
household: { name: "Modal House", role: "owner" },
|
|
});
|
|
|
|
let locations = [
|
|
{
|
|
id: 10,
|
|
household_store_id: 100,
|
|
name: "Costco",
|
|
location_name: "Default Location",
|
|
display_name: "Costco",
|
|
address: "",
|
|
is_default: true,
|
|
},
|
|
{
|
|
id: 11,
|
|
household_store_id: 100,
|
|
name: "Costco",
|
|
location_name: "Fontana",
|
|
display_name: "Costco - Fontana",
|
|
address: "Sierra Ave",
|
|
is_default: false,
|
|
},
|
|
];
|
|
let zones = [
|
|
{ id: 901, name: "Bakery", sort_order: 10 },
|
|
{ id: 902, name: "Frozen Foods", sort_order: 20 },
|
|
];
|
|
|
|
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/stores/100/locations", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() === "POST") {
|
|
const body = request.postDataJSON() as { name?: string; address?: string };
|
|
const locationName = body.name || "New Location";
|
|
locations = [
|
|
...locations,
|
|
{
|
|
id: 12,
|
|
household_store_id: 100,
|
|
name: "Costco",
|
|
location_name: locationName,
|
|
display_name: `Costco - ${locationName}`,
|
|
address: body.address || "",
|
|
is_default: false,
|
|
},
|
|
];
|
|
await route.fulfill({
|
|
status: 201,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ store: locations[locations.length - 1] }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 405 });
|
|
});
|
|
|
|
await page.route("**/households/1/locations/11", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() === "PATCH") {
|
|
const body = request.postDataJSON() as { name?: string; address?: string };
|
|
locations = locations.map((location) =>
|
|
location.id === 11
|
|
? {
|
|
...location,
|
|
location_name: body.name || location.location_name,
|
|
display_name: `Costco - ${body.name || location.location_name}`,
|
|
address: body.address || "",
|
|
}
|
|
: location
|
|
);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ store: locations.find((location) => location.id === 11) }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (request.method() === "DELETE") {
|
|
locations = locations.filter((location) => location.id !== 11);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ message: "Location removed successfully" }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 405 });
|
|
});
|
|
|
|
await page.route("**/households/1/locations/11/default", async (route) => {
|
|
if (route.request().method() === "PATCH") {
|
|
locations = locations.map((location) => ({
|
|
...location,
|
|
is_default: location.id === 11,
|
|
}));
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ message: "Default location updated successfully" }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 405 });
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/zones", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() === "GET") {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ zones }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (request.method() === "POST") {
|
|
const body = request.postDataJSON() as { name?: string; sort_order?: number };
|
|
zones = [
|
|
...zones,
|
|
{ id: 903, name: body.name || "New Zone", sort_order: body.sort_order || 30 },
|
|
];
|
|
await route.fulfill({
|
|
status: 201,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ zone: zones[zones.length - 1] }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 405 });
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/zones/901", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() === "PATCH") {
|
|
const body = request.postDataJSON() as { name?: string; sort_order?: number };
|
|
zones = zones.map((zone) =>
|
|
zone.id === 901
|
|
? { ...zone, name: body.name || zone.name, sort_order: body.sort_order ?? zone.sort_order }
|
|
: zone
|
|
);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ zone: zones.find((zone) => zone.id === 901) }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
if (request.method() === "DELETE") {
|
|
zones = zones.filter((zone) => zone.id !== 901);
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ message: "Zone removed successfully" }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 405 });
|
|
});
|
|
|
|
await page.goto("/manage?tab=stores");
|
|
|
|
const storeCard = page.locator(".store-card").filter({ hasText: "Costco" });
|
|
await expect(storeCard).toBeVisible();
|
|
await expect(storeCard.getByRole("button", { name: "Set Default" })).toHaveCount(0);
|
|
await expect(storeCard.getByRole("button", { name: "Remove" })).toHaveCount(0);
|
|
await expect(storeCard.getByPlaceholder("Location name")).toHaveCount(0);
|
|
|
|
await storeCard.getByRole("button", { name: "Manage Locations" }).click();
|
|
const locationsModal = page.locator(".store-items-modal").filter({
|
|
has: page.getByRole("heading", { name: "Costco Locations" }),
|
|
});
|
|
await expect(locationsModal).toBeVisible();
|
|
await expect(locationsModal.getByPlaceholder("Location name")).toBeVisible();
|
|
await expect(locationsModal.getByRole("button", { name: "Delete Locations" })).toBeVisible();
|
|
|
|
await locationsModal.getByRole("button", { name: "Edit location Costco - Fontana" }).click();
|
|
const locationSettings = page.locator(".store-items-modal").filter({
|
|
has: page.getByRole("heading", { name: "Costco - Fontana Settings" }),
|
|
});
|
|
await expect(locationSettings).toBeVisible();
|
|
await locationSettings.getByLabel("Location name").fill("Upland");
|
|
await locationSettings.getByLabel("Address or notes").fill("Mountain Ave");
|
|
await locationSettings.getByRole("button", { name: "Save Changes" }).click();
|
|
await expect(
|
|
page.locator(".action-toast.action-toast-success").filter({ hasText: "Updated location" })
|
|
).toContainText("Updated location");
|
|
|
|
await locationsModal.getByRole("button", { name: "Delete Locations" }).click();
|
|
await locationsModal.getByRole("button", { name: "Select Costco - Upland for deletion" }).click();
|
|
await expect(locationsModal.getByRole("button", { name: "Confirm Delete (1)" })).toBeEnabled();
|
|
await locationsModal.getByRole("button", { name: "Confirm Delete (1)" }).click();
|
|
await expect(page.getByRole("heading", { name: "Delete Costco - Upland?" })).toBeVisible();
|
|
await confirmSlide(page);
|
|
await expect(
|
|
page.locator(".action-toast.action-toast-success").filter({ hasText: "Removed location" })
|
|
).toContainText("Removed location");
|
|
|
|
await page.getByLabel("Close manage locations modal").click();
|
|
await storeCard.getByRole("button", { name: "Manage Zones" }).click();
|
|
const zonesModal = page.locator(".store-items-modal").filter({
|
|
has: page.getByRole("heading", { name: "Costco Zones" }),
|
|
});
|
|
await expect(zonesModal).toBeVisible();
|
|
await expect(zonesModal.getByPlaceholder("New zone name")).toBeVisible();
|
|
await expect(zonesModal.getByRole("button", { name: "Up" })).toHaveCount(0);
|
|
await expect(zonesModal.getByRole("button", { name: "Down" })).toHaveCount(0);
|
|
await expect(zonesModal.getByRole("button", { name: "Remove" })).toHaveCount(0);
|
|
|
|
await zonesModal.getByRole("button", { name: "Edit zone Bakery" }).click();
|
|
const zoneSettings = page.locator(".store-items-modal").filter({
|
|
has: page.getByRole("heading", { name: "Bakery Settings" }),
|
|
});
|
|
await expect(zoneSettings).toBeVisible();
|
|
await zoneSettings.getByLabel("Zone name").fill("Bread");
|
|
await zoneSettings.getByRole("button", { name: "Save Changes" }).click();
|
|
await expect(
|
|
page.locator(".action-toast.action-toast-success").filter({ hasText: "Updated zone" })
|
|
).toContainText("Updated zone");
|
|
|
|
await zonesModal.getByRole("button", { name: "Delete Zones" }).click();
|
|
await zonesModal.getByRole("button", { name: "Select Bread for deletion" }).click();
|
|
await zonesModal.getByRole("button", { name: "Confirm Delete (1)" }).click();
|
|
await expect(page.getByRole("heading", { name: "Delete Bread?" })).toBeVisible();
|
|
await confirmSlide(page);
|
|
await expect(
|
|
page.locator(".action-toast.action-toast-success").filter({ hasText: "Removed zone" })
|
|
).toContainText("Removed zone");
|
|
});
|
|
|
|
test("grocery page remains unchanged and does not show a store items picker", async ({ page }) => {
|
|
await seedAuthStorage(page);
|
|
await mockConfig(page);
|
|
await mockHouseholdAndStoreShell(page);
|
|
|
|
await page.route("**/households/1/members", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([
|
|
{ id: 1, username: "owner", name: "Owner User", display_name: "Owner User", role: "owner" },
|
|
]),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/list/recent", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([]),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/list/suggestions**", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([]),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/list/item**", async (route) => {
|
|
await route.fulfill({
|
|
status: 404,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ message: "Item not found" }),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/locations/10/list", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ items: [] }),
|
|
});
|
|
});
|
|
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByRole("button", { name: "Store Items" })).toHaveCount(0);
|
|
await expect(page.locator(".available-items-picker-modal")).toHaveCount(0);
|
|
});
|