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", "toast-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, }), }); }); } test("login failure shows inline error and error toast", async ({ page }) => { await mockConfig(page); await page.route("**/auth/login", async (route) => { await route.fulfill({ status: 401, contentType: "application/json", body: JSON.stringify({ message: "Invalid credentials" }), }); }); await page.goto("/login"); await page.getByPlaceholder("Username").fill("bad-user"); await page.getByPlaceholder("Password").fill("bad-password"); await page.getByRole("button", { name: "Login" }).click(); await expect(page.getByText("Invalid credentials")).toBeVisible(); await expect(page.locator(".action-toast.action-toast-error")).toContainText("Login failed"); await expect(page.locator(".action-toast.action-toast-error")).toContainText("Invalid credentials"); }); test("manage stores add success shows success toast", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); let linkedStoreIds = [10]; const allStores = [ { id: 10, name: "Costco North", location: "North", is_default: true }, { id: 11, name: "Costco South", location: "South", is_default: false }, ]; await page.route("**/households", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 1, name: "Toast Home", role: "admin", invite_code: "ABCD1234" }]), }); }); await page.route("**/stores/household/1", async (route) => { const request = route.request(); if (request.method() === "GET") { const payload = linkedStoreIds.map((id, index) => { const store = allStores.find((candidate) => candidate.id === id); return { ...store, is_default: index === 0, }; }); await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(payload), }); return; } if (request.method() === "POST") { const body = request.postDataJSON() as { storeId?: number }; if (body.storeId && !linkedStoreIds.includes(body.storeId)) { linkedStoreIds = [...linkedStoreIds, body.storeId]; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ ok: true }), }); return; } await route.fallback(); }); await page.route("**/stores", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(allStores), }); }); await page.goto("/manage?tab=stores"); await page.getByRole("button", { name: "+ Add Store" }).click(); await page.locator(".available-store-card").filter({ hasText: "Costco South" }).getByRole("button", { name: "Add" }).click(); await expect(page.locator(".action-toast.action-toast-success")).toContainText("Added store"); await expect(page.locator(".action-toast.action-toast-success")).toContainText("Costco South"); }); test("manage stores add failure shows normalized error toast", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); const allStores = [ { id: 10, name: "Costco North", location: "North", is_default: true }, { id: 11, name: "Costco South", location: "South", is_default: false }, ]; await page.route("**/households", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 1, name: "Toast Home", role: "admin", invite_code: "ABCD1234" }]), }); }); await page.route("**/stores/household/1", async (route) => { const request = route.request(); if (request.method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 10, name: "Costco North", location: "North", is_default: true }]), }); return; } if (request.method() === "POST") { await route.fulfill({ status: 400, contentType: "application/json", body: JSON.stringify({ error: { message: "Store already linked to household" }, }), }); return; } await route.fallback(); }); await page.route("**/stores", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify(allStores), }); }); await page.goto("/manage?tab=stores"); await page.getByRole("button", { name: "+ Add Store" }).click(); await page.locator(".available-store-card").filter({ hasText: "Costco South" }).getByRole("button", { name: "Add" }).click(); await expect(page.locator(".action-toast.action-toast-error")).toContainText("Add store failed"); await expect(page.locator(".action-toast.action-toast-error")).toContainText("Store already linked to household"); }); test("invite accept JOINED shows success toast", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); await page.route("**/api/invite-links/toast-token", async (route) => { const request = route.request(); if (request.method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ link: { token: "toast-token", status: "ACTIVE", viewerStatus: null, active_policy: "AUTO_ACCEPT", group_name: "Toast Group", }, }), }); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ result: { status: "JOINED", group: { name: "Toast Group" }, }, }), }); }); await page.route("**/households", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 1, name: "Toast Home", role: "member", invite_code: "ABCD1234" }]), }); }); await page.goto("/invite/toast-token"); await page.getByRole("button", { name: "Join Group" }).click(); await expect(page.locator(".action-toast.action-toast-success")).toContainText("Joined group"); await expect(page.locator(".action-toast.action-toast-success")).toContainText("Toast Group"); }); test("invite accept PENDING shows info toast", async ({ page }) => { await seedAuthStorage(page); await mockConfig(page); await page.route("**/api/invite-links/pending-token", async (route) => { const request = route.request(); if (request.method() === "GET") { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ link: { token: "pending-token", status: "ACTIVE", viewerStatus: null, active_policy: "APPROVAL_REQUIRED", group_name: "Pending Group", }, }), }); return; } await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ result: { status: "PENDING", group: { name: "Pending Group" }, }, }), }); }); await page.route("**/households", async (route) => { await route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify([{ id: 1, name: "Toast Home", role: "member", invite_code: "ABCD1234" }]), }); }); await page.goto("/invite/pending-token"); await page.getByRole("button", { name: "Join Group" }).click(); await expect(page.locator(".action-toast.action-toast-info")).toContainText("Join request sent"); await expect(page.locator(".action-toast.action-toast-info")).toContainText("Pending Group"); });