costco-grocery-list/frontend/tests/toast-notifications.spec.ts
Nico 77ae5be445
All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m10s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 3s
Build & Deploy Costco Grocery List / deploy (push) Successful in 11s
Build & Deploy Costco Grocery List / notify (push) Successful in 1s
refactor
2026-02-22 01:27:03 -08:00

273 lines
8.3 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", "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");
});