336 lines
10 KiB
TypeScript
336 lines
10 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 picker uses available items and preserves quantity and assignee", async ({ page }) => {
|
|
await seedAuthStorage(page);
|
|
await mockConfig(page);
|
|
await mockHouseholdAndStoreShell(page);
|
|
|
|
const members = [
|
|
{ id: 1, username: "owner", name: "Owner User", display_name: "Owner User", role: "owner" },
|
|
{ id: 2, username: "casey", name: "Casey Client", display_name: "Casey Client", role: "member" },
|
|
];
|
|
|
|
let lastAddBody = "";
|
|
let currentItems: Array<{
|
|
id: number;
|
|
item_id: number;
|
|
item_name: string;
|
|
quantity: number;
|
|
bought: boolean;
|
|
item_image: string | null;
|
|
image_mime_type: string | null;
|
|
added_by_users: string[];
|
|
last_added_on: string;
|
|
item_type: string | null;
|
|
item_group: string | null;
|
|
zone: string | null;
|
|
}> = [];
|
|
|
|
await page.route("**/households/1/members", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(members),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/stores/10/available-items*", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
items: [
|
|
{
|
|
item_id: 600,
|
|
item_name: "bananas",
|
|
item_image: null,
|
|
image_mime_type: null,
|
|
item_type: "produce",
|
|
item_group: "Fresh Fruit",
|
|
zone: "Fresh Produce",
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
});
|
|
|
|
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([{ item_name: "bananas" }]),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/stores/10/list/item**", async (route) => {
|
|
const request = route.request();
|
|
const url = new URL(request.url());
|
|
const itemName = (url.searchParams.get("item_name") || "").toLowerCase();
|
|
const item = currentItems.find((candidate) => candidate.item_name === itemName);
|
|
|
|
if (request.method() === "GET") {
|
|
await route.fulfill({
|
|
status: item ? 200 : 404,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(item || { message: "Item not found" }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({ status: 500 });
|
|
});
|
|
|
|
await page.route("**/households/1/stores/10/list/add", async (route) => {
|
|
lastAddBody = route.request().postData() || "";
|
|
currentItems = [
|
|
{
|
|
id: 201,
|
|
item_id: 600,
|
|
item_name: "bananas",
|
|
quantity: 3,
|
|
bought: false,
|
|
item_image: null,
|
|
image_mime_type: null,
|
|
added_by_users: ["Casey Client"],
|
|
last_added_on: "2026-03-28T12:00:00.000Z",
|
|
item_type: "produce",
|
|
item_group: "Fresh Fruit",
|
|
zone: "Fresh Produce",
|
|
},
|
|
];
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
message: "Item added",
|
|
item: {
|
|
id: 201,
|
|
item_name: "bananas",
|
|
quantity: 3,
|
|
bought: false,
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/stores/10/list", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ items: currentItems }),
|
|
});
|
|
});
|
|
|
|
await page.goto("/");
|
|
|
|
await page.getByRole("button", { name: "Others" }).click();
|
|
const assignModal = page.locator(".assign-item-for-modal");
|
|
await assignModal.getByRole("button", { name: "Select member" }).click();
|
|
await page.locator("body > .assign-item-for-dropdown-menu").getByRole("option", { name: "Casey Client" }).click();
|
|
await assignModal.getByRole("button", { name: "Confirm" }).click();
|
|
|
|
await page.getByRole("button", { name: "+" }).click();
|
|
await page.getByRole("button", { name: "+" }).click();
|
|
await expect(page.locator(".add-item-form-quantity-input")).toHaveValue("3");
|
|
|
|
await page.getByRole("button", { name: "Store Items" }).click();
|
|
const pickerModal = page.locator(".available-items-picker-modal");
|
|
await expect(pickerModal).toBeVisible();
|
|
await pickerModal.getByRole("button", { name: /bananas/i }).click();
|
|
|
|
await page.getByRole("button", { name: "Skip All" }).click();
|
|
await expect(page.locator(".glist-li").filter({ hasText: "bananas" })).toContainText("Casey Client");
|
|
expect(lastAddBody).toContain('name="quantity"');
|
|
expect(lastAddBody).toContain("3");
|
|
expect(lastAddBody).toContain('name="added_for_user_id"');
|
|
expect(lastAddBody).toContain("2");
|
|
});
|