175 lines
5.3 KiB
TypeScript
175 lines
5.3 KiB
TypeScript
import { expect, test, type Page } from "@playwright/test";
|
|
import {
|
|
mockConfig,
|
|
mockHouseholdAndStoreShell,
|
|
seedAuthStorage,
|
|
} from "./helpers/e2e";
|
|
|
|
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 makeItem(id: number, itemName: string, quantity = 1): 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",
|
|
};
|
|
}
|
|
|
|
async function setupGroceryRoutes(
|
|
page: Page,
|
|
options: {
|
|
householdRole?: string;
|
|
items?: MockItem[];
|
|
} = {}
|
|
) {
|
|
const items = options.items || [
|
|
makeItem(1, "milk", 2),
|
|
makeItem(2, "bread", 1),
|
|
];
|
|
|
|
await mockConfig(page);
|
|
await mockHouseholdAndStoreShell(page, {
|
|
household: {
|
|
name: "Grocery Actions House",
|
|
role: options.householdRole || "admin",
|
|
},
|
|
stores: [
|
|
{
|
|
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/locations/10/zones", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ zones: [{ id: 1, name: "Produce" }] }),
|
|
});
|
|
});
|
|
|
|
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) => {
|
|
const url = new URL(route.request().url());
|
|
const itemName = (url.searchParams.get("item_name") || "").toLowerCase();
|
|
const item = items.find((candidate) => candidate.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/locations/10/list", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ items }),
|
|
});
|
|
});
|
|
}
|
|
|
|
test("item actions expose bought flow and local skipped restore", async ({ page }) => {
|
|
await seedAuthStorage(page, { username: "grocery-actions-user" });
|
|
await setupGroceryRoutes(page);
|
|
|
|
await page.goto("/");
|
|
|
|
const milkRow = page.locator(".glist-classification-group .glist-li").filter({ hasText: "milk" });
|
|
await expect(milkRow).toBeVisible();
|
|
|
|
await milkRow.getByRole("button", { name: "Actions for milk" }).click();
|
|
await milkRow.getByRole("button", { name: "Bought" }).click();
|
|
await expect(page.locator(".confirm-buy-modal")).toBeVisible();
|
|
await page.getByRole("button", { name: "Cancel" }).click();
|
|
|
|
await milkRow.getByRole("button", { name: "Actions for milk" }).click();
|
|
await milkRow.getByRole("button", { name: "Skip" }).click();
|
|
|
|
await expect(milkRow).toHaveCount(0);
|
|
await expect(page.locator(".glist-skipped-section")).toContainText("milk");
|
|
|
|
await page.reload();
|
|
await expect(page.locator(".glist-skipped-section")).toContainText("milk");
|
|
|
|
await page.locator(".glist-skipped-section").getByRole("button", { name: "Restore" }).click();
|
|
await expect(page.locator(".glist-classification-group .glist-li").filter({ hasText: "milk" })).toBeVisible();
|
|
});
|
|
|
|
test("viewer role cannot see mutation controls", async ({ page }) => {
|
|
await seedAuthStorage(page, { username: "grocery-viewer-user" });
|
|
await setupGroceryRoutes(page, { householdRole: "viewer" });
|
|
|
|
await page.goto("/");
|
|
|
|
await expect(page.getByPlaceholder("Enter item name")).toHaveCount(0);
|
|
await expect(page.getByRole("button", { name: "Actions for milk" })).toHaveCount(0);
|
|
await expect(page.getByLabel("Search list")).toBeVisible();
|
|
});
|
|
|
|
test("offline banner blocks server-backed add flow with a clear toast", async ({ page }) => {
|
|
await seedAuthStorage(page, { username: "grocery-offline-user" });
|
|
await setupGroceryRoutes(page);
|
|
|
|
await page.goto("/");
|
|
await page.evaluate(() => window.dispatchEvent(new Event("offline")));
|
|
|
|
await expect(page.locator(".glist-offline-banner")).toContainText("Offline");
|
|
await page.getByPlaceholder("Enter item name").fill("bananas");
|
|
await page.getByRole("button", { name: "Create + Add" }).click();
|
|
|
|
await expect(page.locator(".add-item-details-modal")).toHaveCount(0);
|
|
await expect(page.locator(".action-toast.action-toast-error")).toContainText("Adding items needs a connection");
|
|
});
|