grocery-app/frontend/tests/available-items-catalog.spec.ts

234 lines
7.2 KiB
TypeScript

import { expect, test } from "@playwright/test";
function seedAuthStorage(page: import("@playwright/test").Page) {
return page.addInitScript(() => {
localStorage.setItem("token", "test-token");
localStorage.setItem("userId", "1");
localStorage.setItem("role", "admin");
localStorage.setItem("username", "catalog-user");
});
}
async function mockConfig(page: import("@playwright/test").Page) {
await page.route("**/config", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
maxFileSizeMB: 20,
maxImageDimension: 800,
imageQuality: 85,
}),
});
});
}
async function mockHouseholdAndStoreShell(page: import("@playwright/test").Page) {
await page.route("**/households", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{ id: 1, name: "Catalog House", role: "admin", invite_code: "ABCD1234" },
]),
});
});
await page.route("**/stores/household/1", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([
{ id: 10, name: "Costco", location: "Warehouse", is_default: true },
]),
});
});
}
test("manage stores lets admins import and curate available items", async ({ page }) => {
await seedAuthStorage(page);
await mockConfig(page);
await mockHouseholdAndStoreShell(page);
let availableItems = [
{
item_id: 501,
item_name: "milk",
item_image: null,
image_mime_type: null,
item_type: "dairy",
item_group: "Milk",
zone: "Dairy & Refrigerated",
},
];
await page.route("**/stores", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([{ id: 10, name: "Costco" }]),
});
});
await page.route("**/households/1/stores/10/available-items/import-current", async (route) => {
availableItems = [
...availableItems,
{
item_id: 777,
item_name: "granola",
item_image: null,
image_mime_type: null,
item_type: null,
item_group: null,
zone: null,
},
];
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
message: "Imported current list items",
imported_count: 1,
}),
});
});
await page.route("**/households/1/stores/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 }),
});
return;
}
if (request.method() === "POST") {
availableItems = [
...availableItems,
{
item_id: 888,
item_name: "trail mix",
item_image: null,
image_mime_type: null,
item_type: "snack",
item_group: "Trail Mix",
zone: "Snacks & Candy",
},
];
await route.fulfill({
status: 201,
contentType: "application/json",
body: JSON.stringify({
message: "Available item added",
item: availableItems[availableItems.length - 1],
}),
});
return;
}
await route.fulfill({ status: 500 });
});
await page.route("**/households/1/stores/10/available-items/888", async (route) => {
if (route.request().method() === "DELETE") {
availableItems = availableItems.filter((item) => item.item_id !== 888);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ message: "Available item removed" }),
});
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 storeCard.getByRole("button", { name: "Manage" }).click();
await expect(storeCard.getByText("milk")).toBeVisible();
await storeCard.getByRole("button", { name: "Import Current List" }).click();
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Imported current list items");
await expect(storeCard.getByText("granola")).toBeVisible();
await storeCard.getByRole("button", { name: "Add Item" }).click();
const editorModal = page.locator(".available-item-editor-modal");
await expect(editorModal).toBeVisible();
await editorModal.getByLabel("Item Name").fill("trail mix");
await editorModal.locator(".available-item-editor-select").nth(0).selectOption("snack");
await editorModal.locator(".available-item-editor-select").nth(1).selectOption("Trail Mix");
await editorModal.locator(".available-item-editor-select").nth(2).selectOption("Snacks & Candy");
await editorModal.getByRole("button", { name: "Add Item" }).click();
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Added store item");
await expect(storeCard.getByText("trail mix")).toBeVisible();
page.once("dialog", (dialog) => dialog.accept());
await storeCard.locator(".store-available-items-card").filter({ hasText: "trail mix" }).getByRole("button", { name: "Remove" }).click();
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Removed store item");
});
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/stores/10/list/recent", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
await page.route("**/households/1/stores/10/list/suggestions**", async (route) => {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify([]),
});
});
await page.route("**/households/1/stores/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/stores/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);
});