All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 49s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 1s
Build & Deploy Costco Grocery List / deploy (push) Successful in 14s
Build & Deploy Costco Grocery List / notify (push) Successful in 0s
280 lines
7.8 KiB
TypeScript
280 lines
7.8 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
type MockItem = {
|
|
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;
|
|
};
|
|
|
|
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", "buy-modal-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,
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
|
|
function makeItem(
|
|
id: number,
|
|
itemName: string,
|
|
quantity: number,
|
|
overrides: Partial<MockItem> = {}
|
|
): MockItem {
|
|
return {
|
|
id,
|
|
item_id: id + 500,
|
|
item_name: itemName,
|
|
quantity,
|
|
bought: false,
|
|
item_image: null,
|
|
image_mime_type: null,
|
|
added_by_users: ["Owner User"],
|
|
last_added_on: "2026-03-28T12:00:00.000Z",
|
|
item_type: null,
|
|
item_group: null,
|
|
zone: "Produce",
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
async function setupBuyModalRoutes(
|
|
page: import("@playwright/test").Page,
|
|
initialItems: MockItem[]
|
|
) {
|
|
let activeItems = initialItems.map((item) => ({ ...item }));
|
|
let recentItems: MockItem[] = [];
|
|
|
|
await page.route("**/households", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([
|
|
{ id: 1, name: "Auto Advance 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 },
|
|
]),
|
|
});
|
|
});
|
|
|
|
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(recentItems),
|
|
});
|
|
});
|
|
|
|
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/classification**", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ classification: null }),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/stores/10/list/item", async (route) => {
|
|
const request = route.request();
|
|
|
|
if (request.method() === "PATCH") {
|
|
const body = request.postDataJSON() as {
|
|
item_name?: string;
|
|
quantity_bought?: number | null;
|
|
};
|
|
const itemName = String(body.item_name || "").toLowerCase();
|
|
const quantityBought = Number(body.quantity_bought ?? 0);
|
|
const currentItem = activeItems.find((item) => item.item_name === itemName);
|
|
|
|
if (!currentItem) {
|
|
await route.fulfill({
|
|
status: 404,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ error: { message: "Item not found" } }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const remainingQuantity = currentItem.quantity - quantityBought;
|
|
recentItems = [
|
|
{
|
|
...currentItem,
|
|
quantity: quantityBought,
|
|
bought: true,
|
|
},
|
|
...recentItems,
|
|
];
|
|
|
|
if (remainingQuantity <= 0) {
|
|
activeItems = activeItems.filter((item) => item.id !== currentItem.id);
|
|
} else {
|
|
activeItems = activeItems.map((item) =>
|
|
item.id === currentItem.id
|
|
? { ...item, quantity: remainingQuantity }
|
|
: item
|
|
);
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
message: "Item updated",
|
|
item: {
|
|
id: currentItem.id,
|
|
item_name: currentItem.item_name,
|
|
quantity: Math.max(remainingQuantity, 0),
|
|
bought: remainingQuantity <= 0,
|
|
},
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
|
|
const url = new URL(request.url());
|
|
const itemName = (url.searchParams.get("item_name") || "").toLowerCase();
|
|
const item = activeItems.find((entry) => entry.item_name === itemName);
|
|
|
|
await route.fulfill({
|
|
status: item ? 200 : 404,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(item || { 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: activeItems,
|
|
}),
|
|
});
|
|
});
|
|
}
|
|
|
|
async function openBuyModal(page: import("@playwright/test").Page, itemName: string) {
|
|
const row = page.locator(".glist-li").filter({ hasText: itemName });
|
|
await row.click();
|
|
await expect(page.locator(".confirm-buy-modal")).toBeVisible();
|
|
}
|
|
|
|
test("buying an item advances to the next one in the current sort order", async ({ page }) => {
|
|
await seedAuthStorage(page);
|
|
await mockConfig(page);
|
|
await setupBuyModalRoutes(page, [
|
|
makeItem(1, "milk", 2),
|
|
makeItem(2, "bread", 5),
|
|
makeItem(3, "apples", 3),
|
|
]);
|
|
|
|
await page.goto("/");
|
|
await page.locator(".glist-sort").selectOption("qty-high");
|
|
|
|
await openBuyModal(page, "bread");
|
|
await page.getByRole("button", { name: "Mark as Bought" }).click();
|
|
|
|
await expect(page.locator(".confirm-buy-item-name")).toHaveText("apples");
|
|
});
|
|
|
|
test("buying the last item in the current order wraps to the first remaining item", async ({ page }) => {
|
|
await seedAuthStorage(page);
|
|
await mockConfig(page);
|
|
await setupBuyModalRoutes(page, [
|
|
makeItem(1, "apples", 3),
|
|
makeItem(2, "bread", 5),
|
|
makeItem(3, "milk", 2),
|
|
]);
|
|
|
|
await page.goto("/");
|
|
await page.locator(".glist-sort").selectOption("az");
|
|
|
|
await openBuyModal(page, "milk");
|
|
await page.getByRole("button", { name: "Mark as Bought" }).click();
|
|
|
|
await expect(page.locator(".confirm-buy-item-name")).toHaveText("apples");
|
|
});
|
|
|
|
test("partial buy keeps the item on the list and advances past it", async ({ page }) => {
|
|
await seedAuthStorage(page);
|
|
await mockConfig(page);
|
|
await setupBuyModalRoutes(page, [
|
|
makeItem(1, "alpha", 1),
|
|
makeItem(2, "bravo", 3),
|
|
makeItem(3, "charlie", 5),
|
|
]);
|
|
|
|
await page.goto("/");
|
|
await page.locator(".glist-sort").selectOption("qty-low");
|
|
|
|
await openBuyModal(page, "bravo");
|
|
await page.locator(".confirm-buy-counter-btn").nth(0).click();
|
|
await page.getByRole("button", { name: "Mark as Bought" }).click();
|
|
|
|
await expect(page.locator(".confirm-buy-item-name")).toHaveText("charlie");
|
|
await expect(page.locator(".glist-li").filter({ hasText: "bravo" })).toContainText("x2");
|
|
});
|
|
|
|
test("buying the only remaining item closes the modal", async ({ page }) => {
|
|
await seedAuthStorage(page);
|
|
await mockConfig(page);
|
|
await setupBuyModalRoutes(page, [
|
|
makeItem(1, "solo", 1),
|
|
]);
|
|
|
|
await page.goto("/");
|
|
|
|
await openBuyModal(page, "solo");
|
|
await page.getByRole("button", { name: "Mark as Bought" }).click();
|
|
|
|
await expect(page.locator(".confirm-buy-modal")).toBeHidden();
|
|
});
|