All checks were successful
Build & Deploy Costco Grocery List / build (push) Successful in 1m36s
Build & Deploy Costco Grocery List / verify-images (push) Successful in 2s
Build & Deploy Costco Grocery List / deploy (push) Successful in 8s
Build & Deploy Costco Grocery List / notify (push) Successful in 0s
294 lines
8.9 KiB
TypeScript
294 lines
8.9 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
import {
|
|
collectFailedApiRequests,
|
|
confirmSlide,
|
|
expectNoFailedApiRequests,
|
|
mockConfig,
|
|
seedAuthStorage,
|
|
} from "./helpers/e2e";
|
|
|
|
test("join household modal accepts invite links but rejects legacy invite codes", async ({ page }) => {
|
|
await seedAuthStorage(page, { username: "manager-user" });
|
|
await mockConfig(page);
|
|
|
|
await page.route("**/households", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([]),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/invite-links/approvaltoken", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
link: {
|
|
token: "approvaltoken",
|
|
status: "ACTIVE",
|
|
viewerStatus: null,
|
|
active_policy: "APPROVAL_REQUIRED",
|
|
group_name: "Approval Home",
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/manage");
|
|
await page.getByRole("button", { name: "Join Household", exact: true }).click();
|
|
await page.getByLabel("Invite Link").fill("HABC123");
|
|
await page.getByRole("button", { name: "Open Invite" }).click();
|
|
|
|
await expect(page.locator(".create-join-modal .error-message")).toHaveText(
|
|
"Use a household invite link like /invite/abcd1234."
|
|
);
|
|
|
|
await page.getByLabel("Invite Link").fill("/invite/approvaltoken");
|
|
await page.getByRole("button", { name: "Open Invite" }).click();
|
|
|
|
await expect(page).toHaveURL(/\/invite\/approvaltoken$/);
|
|
await expect(page.getByRole("heading", { name: "Join Approval Home" })).toBeVisible();
|
|
});
|
|
|
|
test("household management shows pending invite approvals and can approve them", async ({ page }) => {
|
|
await seedAuthStorage(page, { username: "manager-user" });
|
|
await mockConfig(page);
|
|
|
|
let members = [
|
|
{ id: 1, username: "manager-user", role: "owner" },
|
|
];
|
|
let pendingRequests = [
|
|
{
|
|
id: 41,
|
|
user_id: 7,
|
|
username: "pending-pal",
|
|
name: "Pending Pal",
|
|
display_name: "",
|
|
created_at: "2026-03-31T12:00:00.000Z",
|
|
status: "PENDING",
|
|
},
|
|
];
|
|
|
|
await page.route("**/households", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify([{ id: 1, name: "Approval Home", role: "owner", invite_code: "ABCD1234" }]),
|
|
});
|
|
});
|
|
|
|
await page.route("**/households/1/members", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(members),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/groups/join-policy", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() === "GET") {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ joinPolicy: "APPROVAL_REQUIRED" }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ ok: true }),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/groups/invites", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() === "GET") {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
links: [
|
|
{
|
|
id: 9,
|
|
token: "invite-token-1234",
|
|
policy: "APPROVAL_REQUIRED",
|
|
single_use: false,
|
|
expires_at: "2030-01-01T00:00:00.000Z",
|
|
used_at: null,
|
|
revoked_at: null,
|
|
},
|
|
],
|
|
}),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 201,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ link: { id: 10, token: "new-token" } }),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/groups/join-requests", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ requests: pendingRequests }),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/groups/join-requests/decision", async (route) => {
|
|
const body = route.request().postDataJSON() as { requestId?: number; decision?: string };
|
|
if (body.requestId === 41 && body.decision === "APPROVE") {
|
|
pendingRequests = [];
|
|
members = [
|
|
...members,
|
|
{ id: 7, username: "pending-pal", role: "member" },
|
|
];
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
request: {
|
|
id: 41,
|
|
user_id: 7,
|
|
status: body.decision === "APPROVE" ? "APPROVED" : "DENIED",
|
|
},
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.goto("/manage?tab=household");
|
|
|
|
await expect(page.getByRole("heading", { name: "Invite Links" })).toBeVisible();
|
|
await expect(page.getByText("Pending Pal")).toBeVisible();
|
|
await expect(page.getByText("Invite Code")).toHaveCount(0);
|
|
|
|
await page.getByRole("button", { name: "Approve" }).click();
|
|
|
|
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Approved join request");
|
|
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Pending Pal");
|
|
await expect(page.getByText("No pending join requests right now.")).toBeVisible();
|
|
await expect(page.getByText("Members (2)")).toBeVisible();
|
|
});
|
|
|
|
test("household owner can transfer ownership from household settings", async ({ page }) => {
|
|
const failedApiRequests = collectFailedApiRequests(page);
|
|
await seedAuthStorage(page, { role: "owner", username: "manager-user" });
|
|
await mockConfig(page);
|
|
|
|
let households = [{ id: 1, name: "Approval Home", role: "owner", invite_code: "ABCD1234" }];
|
|
let members = [
|
|
{ id: 1, username: "manager-user", role: "owner" },
|
|
{ id: 2, username: "nico-admin", role: "admin" },
|
|
];
|
|
|
|
await page.route("**/households", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify(households),
|
|
});
|
|
});
|
|
|
|
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/members/*/role", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() !== "PATCH") {
|
|
await route.fulfill({ status: 405 });
|
|
return;
|
|
}
|
|
|
|
const body = request.postDataJSON() as { role?: string };
|
|
if (body.role === "owner") {
|
|
households = [{ id: 1, name: "Approval Home", role: "admin", invite_code: "ABCD1234" }];
|
|
members = [
|
|
{ id: 1, username: "manager-user", role: "admin" },
|
|
{ id: 2, username: "nico-admin", role: "owner" },
|
|
];
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({
|
|
message: "Household ownership transferred successfully",
|
|
member: { user_id: 2, role: body.role || "member" },
|
|
}),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/groups/join-policy", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() === "GET") {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ joinPolicy: "APPROVAL_REQUIRED" }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ ok: true }),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/groups/invites", async (route) => {
|
|
const request = route.request();
|
|
if (request.method() === "GET") {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ links: [] }),
|
|
});
|
|
return;
|
|
}
|
|
|
|
await route.fulfill({
|
|
status: 201,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ link: { id: 10, token: "new-token" } }),
|
|
});
|
|
});
|
|
|
|
await page.route("**/api/groups/join-requests", async (route) => {
|
|
await route.fulfill({
|
|
status: 200,
|
|
contentType: "application/json",
|
|
body: JSON.stringify({ requests: [] }),
|
|
});
|
|
});
|
|
|
|
await page.goto("/manage?tab=household");
|
|
|
|
await expect(page.getByRole("button", { name: "Make Owner" })).toBeVisible();
|
|
|
|
await page.getByRole("button", { name: "Make Owner" }).click();
|
|
await expect(page.getByText("Transfer ownership to nico-admin?")).toBeVisible();
|
|
await confirmSlide(page);
|
|
|
|
await expect(page.locator(".action-toast.action-toast-success")).toContainText("Transferred household ownership");
|
|
await expect(page.locator(".action-toast.action-toast-success")).toContainText("nico-admin");
|
|
await expect(page.getByText("👑 Owner")).toContainText("Owner");
|
|
await expect(page.getByText("🛠️ Admin")).toContainText("Admin");
|
|
await expect(page.getByRole("button", { name: "Make Owner" })).toHaveCount(0);
|
|
expectNoFailedApiRequests(failedApiRequests);
|
|
});
|