153 lines
5.4 KiB
TypeScript
153 lines
5.4 KiB
TypeScript
/**
|
||
* User stories covered: Feature 17 — Mobile Manager approval queue as cards
|
||
* - MANAGER at 375×812 sees /approvals as PO cards (not a table)
|
||
* - Tapping a card navigates to /approvals/[id]
|
||
* - ManagerEditPoForm is NOT visible at mobile viewport on approval detail page
|
||
* - Approve/Reject action buttons ARE visible at mobile
|
||
*
|
||
* Design: approvals/page.tsx renders:
|
||
* - <div class="hidden md:block"> for the desktop table
|
||
* - <div class="md:hidden space-y-3"> for mobile cards
|
||
* Each mobile card is wrapped in a <Link> pointing to /approvals/[id].
|
||
*
|
||
* approvals/[id]/page.tsx wraps ManagerEditPoForm in <div class="hidden md:block">
|
||
* so it is CSS-hidden at mobile, but ApprovalActions renders unconditionally.
|
||
*
|
||
* Created: 2026-05-17
|
||
*/
|
||
import { test, expect, type BrowserContext } from "@playwright/test";
|
||
import { login, USERS, submitPo } from "../helpers/login";
|
||
|
||
const MOBILE_VIEWPORT = { width: 375, height: 812 };
|
||
|
||
/** Ensure there is at least one PO in MGR_REVIEW status and return its approval URL. */
|
||
async function ensurePendingPo(
|
||
page: import("@playwright/test").Page,
|
||
context: BrowserContext,
|
||
title: string
|
||
): Promise<string> {
|
||
await login(page, USERS.TECH);
|
||
const poUrl = await submitPo(page, title);
|
||
const poId = poUrl.split("/po/")[1].replace(/\/$/, "");
|
||
|
||
await context.clearCookies();
|
||
await login(page, USERS.MANAGER);
|
||
|
||
return `/approvals/${poId}`;
|
||
}
|
||
|
||
test.describe("Feature 17 — Mobile Manager approval queue", () => {
|
||
test("US-17a: MANAGER at mobile viewport sees /approvals — mobile cards rendered", async ({
|
||
page,
|
||
context,
|
||
}: {
|
||
page: import("@playwright/test").Page;
|
||
context: BrowserContext;
|
||
}) => {
|
||
// Create a PO in MGR_REVIEW state first
|
||
await ensurePendingPo(page, context, `E2E_MOBILE_MGR_${Date.now()}`);
|
||
|
||
await page.setViewportSize(MOBILE_VIEWPORT);
|
||
await page.goto("/approvals");
|
||
await page.waitForLoadState("networkidle");
|
||
|
||
await expect(
|
||
page.getByRole("heading", { name: /approval/i })
|
||
).toBeVisible();
|
||
|
||
// Desktop table should be hidden (md:hidden class hides it on mobile)
|
||
// Mobile cards container should be visible
|
||
const mobileCards = page.locator(".md\\:hidden.space-y-3, [class*='md:hidden']").first();
|
||
// At minimum, verify at least one card link to /approvals/ exists
|
||
const cardLinks = page.locator("a[href*='/approvals/']");
|
||
const cardCount = await cardLinks.count();
|
||
expect(cardCount).toBeGreaterThan(0);
|
||
console.log(`✓ ${cardCount} approval card link(s) visible at mobile viewport`);
|
||
});
|
||
|
||
test("US-17b: tapping a card navigates to /approvals/[id]", async ({
|
||
page,
|
||
context,
|
||
}: {
|
||
page: import("@playwright/test").Page;
|
||
context: BrowserContext;
|
||
}) => {
|
||
const approvalDetailUrl = await ensurePendingPo(
|
||
page,
|
||
context,
|
||
`E2E_MOBILE_CARD_NAV_${Date.now()}`
|
||
);
|
||
|
||
await page.setViewportSize(MOBILE_VIEWPORT);
|
||
await page.goto("/approvals");
|
||
await page.waitForLoadState("networkidle");
|
||
|
||
// Click the first card link
|
||
const cardLink = page.locator("a[href*='/approvals/']").first();
|
||
await expect(cardLink).toBeVisible();
|
||
await cardLink.click();
|
||
|
||
await expect(page).toHaveURL(/\/approvals\/.+/, { timeout: 10_000 });
|
||
console.log(`✓ Tapping card navigated to ${page.url()}`);
|
||
});
|
||
|
||
test("US-17c: ManagerEditPoForm is NOT visible at mobile viewport on approval detail", async ({
|
||
page,
|
||
context,
|
||
}: {
|
||
page: import("@playwright/test").Page;
|
||
context: BrowserContext;
|
||
}) => {
|
||
const approvalDetailUrl = await ensurePendingPo(
|
||
page,
|
||
context,
|
||
`E2E_MOBILE_EDIT_FORM_${Date.now()}`
|
||
);
|
||
|
||
await page.setViewportSize(MOBILE_VIEWPORT);
|
||
await page.goto(approvalDetailUrl);
|
||
await page.waitForLoadState("networkidle");
|
||
|
||
// ManagerEditPoForm is inside <div class="hidden md:block"> — CSS hidden on mobile
|
||
// The form contains "Edit Line Items" or similar heading
|
||
// At 375px, Tailwind's md breakpoint (768px) is not active, so hidden md:block = invisible
|
||
const editForm = page
|
||
.getByRole("heading", { name: /edit line items|amend/i })
|
||
.first();
|
||
await expect(editForm).not.toBeVisible();
|
||
console.log("✓ Manager line-item edit form is not visible at mobile viewport");
|
||
});
|
||
|
||
test("US-17c: Approve/Reject action buttons ARE visible at mobile viewport", async ({
|
||
page,
|
||
context,
|
||
}: {
|
||
page: import("@playwright/test").Page;
|
||
context: BrowserContext;
|
||
}) => {
|
||
const approvalDetailUrl = await ensurePendingPo(
|
||
page,
|
||
context,
|
||
`E2E_MOBILE_ACTIONS_${Date.now()}`
|
||
);
|
||
|
||
await page.setViewportSize(MOBILE_VIEWPORT);
|
||
await page.goto(approvalDetailUrl);
|
||
await page.waitForLoadState("networkidle");
|
||
|
||
// ApprovalActions renders Approve and Reject buttons without any responsive hiding
|
||
// If manager has no signature, a warning is shown instead — check for either
|
||
const approveBtn = page.getByRole("button", { name: /^approve$/i });
|
||
const signatureWarning = page.getByText(/signature required/i);
|
||
|
||
const hasApproveBtn = await approveBtn.isVisible();
|
||
const hasSignatureWarning = await signatureWarning.isVisible();
|
||
|
||
expect(hasApproveBtn || hasSignatureWarning).toBeTruthy();
|
||
console.log(
|
||
hasApproveBtn
|
||
? "✓ Approve button visible at mobile viewport"
|
||
: "✓ Signature-required warning shown at mobile viewport (manager needs signature)"
|
||
);
|
||
});
|
||
});
|