From a72e980558299452acfac6edaa1a0b76e96cc833 Mon Sep 17 00:00:00 2001 From: Hardik Date: Wed, 24 Jun 2026 11:49:48 +0530 Subject: [PATCH] test(staging): feature-level verification of closed issues + seeded test users MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a Playwright suite (App/tests/staging/) that logs into the running staging instance (ppms-staging, :3200) and verifies each closed portal issue is actually fixed — feature level, driving the real UI, one spec per issue. To make credential login possible against the prod-mirror pelagia_test (which only holds real, mostly SSO-only users), prisma/seed-test-users.ts idempotently seeds one known-password @pelagia.local user per role, and automation/refresh-test-db.sh runs it after every daily refresh so the logins persist on staging. Result against staging: 41 passed, 1 skipped (#10 — no attachment data on staging). Two closed issues were found NOT fixed and are recorded as documented test.fail(): - #13 Accounts "payments completed this month" card is absent. - #24/#40 logout tooltip still reads "Sign out" (pipeline test issues). Docs/TESTING.md documents the suite, the seeded users, how to run it against staging, and the full issue -> script mapping. Co-Authored-By: Claude Opus 4.8 --- App/.gitignore | 1 + App/playwright.staging.config.ts | 43 ++++++ App/prisma/seed-test-users.ts | 89 ++++++++++++ App/tests/staging/00-smoke.spec.ts | 22 +++ App/tests/staging/crewing-epics.spec.ts | 34 +++++ App/tests/staging/fixtures.ts | 127 +++++++++++++++++ App/tests/staging/helpers.ts | 39 ++++++ .../staging/issue-04-po-date-field.spec.ts | 22 +++ .../issue-05-approved-date-as-po-date.spec.ts | 24 ++++ .../issue-06-closed-list-filters.spec.ts | 35 +++++ ...sue-08-export-includes-description.spec.ts | 22 +++ .../issue-10-attachments-grouped.spec.ts | 25 ++++ .../issue-104-history-pagination.spec.ts | 18 +++ .../issue-109-new-po-vendor-search.spec.ts | 27 ++++ .../staging/issue-11-terms-catalogue.spec.ts | 22 +++ .../issue-12-approved-this-month-card.spec.ts | 22 +++ .../issue-13-payments-this-month-card.spec.ts | 20 +++ .../staging/issue-14-email-to-vendor.spec.ts | 21 +++ ...ssue-19-place-of-delivery-dropdown.spec.ts | 21 +++ .../issue-24-40-logout-tooltip.spec.ts | 18 +++ .../staging/issue-26-41-total-po-card.spec.ts | 27 ++++ .../issue-31-history-multi-status.spec.ts | 21 +++ ...sue-32-approved-month-clickthrough.spec.ts | 21 +++ .../staging/issue-44-line-item-units.spec.ts | 15 ++ .../issue-50-rupee-compact-format.spec.ts | 18 +++ .../staging/issue-53-cancel-po-modal.spec.ts | 38 +++++ .../issue-57-vendor-search-catalogue.spec.ts | 22 +++ .../issue-96-sidebar-collapsible.spec.ts | 31 ++++ Docs/TESTING.md | 132 ++++++++++++++++++ automation/refresh-test-db.sh | 18 +++ 30 files changed, 995 insertions(+) create mode 100644 App/playwright.staging.config.ts create mode 100644 App/prisma/seed-test-users.ts create mode 100644 App/tests/staging/00-smoke.spec.ts create mode 100644 App/tests/staging/crewing-epics.spec.ts create mode 100644 App/tests/staging/fixtures.ts create mode 100644 App/tests/staging/helpers.ts create mode 100644 App/tests/staging/issue-04-po-date-field.spec.ts create mode 100644 App/tests/staging/issue-05-approved-date-as-po-date.spec.ts create mode 100644 App/tests/staging/issue-06-closed-list-filters.spec.ts create mode 100644 App/tests/staging/issue-08-export-includes-description.spec.ts create mode 100644 App/tests/staging/issue-10-attachments-grouped.spec.ts create mode 100644 App/tests/staging/issue-104-history-pagination.spec.ts create mode 100644 App/tests/staging/issue-109-new-po-vendor-search.spec.ts create mode 100644 App/tests/staging/issue-11-terms-catalogue.spec.ts create mode 100644 App/tests/staging/issue-12-approved-this-month-card.spec.ts create mode 100644 App/tests/staging/issue-13-payments-this-month-card.spec.ts create mode 100644 App/tests/staging/issue-14-email-to-vendor.spec.ts create mode 100644 App/tests/staging/issue-19-place-of-delivery-dropdown.spec.ts create mode 100644 App/tests/staging/issue-24-40-logout-tooltip.spec.ts create mode 100644 App/tests/staging/issue-26-41-total-po-card.spec.ts create mode 100644 App/tests/staging/issue-31-history-multi-status.spec.ts create mode 100644 App/tests/staging/issue-32-approved-month-clickthrough.spec.ts create mode 100644 App/tests/staging/issue-44-line-item-units.spec.ts create mode 100644 App/tests/staging/issue-50-rupee-compact-format.spec.ts create mode 100644 App/tests/staging/issue-53-cancel-po-modal.spec.ts create mode 100644 App/tests/staging/issue-57-vendor-search-catalogue.spec.ts create mode 100644 App/tests/staging/issue-96-sidebar-collapsible.spec.ts create mode 100644 Docs/TESTING.md diff --git a/App/.gitignore b/App/.gitignore index c8417a6..994e4ca 100644 --- a/App/.gitignore +++ b/App/.gitignore @@ -13,6 +13,7 @@ # Testing /coverage /playwright-report +/playwright-report-staging /test-results /blob-report diff --git a/App/playwright.staging.config.ts b/App/playwright.staging.config.ts new file mode 100644 index 0000000..2c27857 --- /dev/null +++ b/App/playwright.staging.config.ts @@ -0,0 +1,43 @@ +import { defineConfig, devices } from "@playwright/test"; + +/** + * Playwright config for verifying closed issues against a RUNNING staging instance + * (pm2 `ppms-staging`, port 3200 on pms1), reached over an SSH tunnel: + * + * ssh -N -L 3200:localhost:3200 shad0w@ + * PLAYWRIGHT_BASE_URL=http://localhost:3200 \ + * pnpm exec playwright test --config playwright.staging.config.ts + * + * Unlike playwright.config.ts this does NOT start a local dev server — it drives the + * already-deployed staging build. Login uses the seeded `@pelagia.local` test users + * (prisma/seed-test-users.ts), so no production credentials are required. + * + * Staging runs `next dev`, so the first hit on a route compiles on demand and can be + * slow — timeouts are deliberately generous and workers default to 1 to keep the + * shared staging DB state predictable across specs. + */ +export default defineConfig({ + testDir: "./tests/staging", + fullyParallel: false, + forbidOnly: !!process.env.CI, + retries: 1, + workers: 1, + reporter: [["list"], ["html", { open: "never", outputFolder: "playwright-report-staging" }]], + timeout: 90_000, + expect: { timeout: 20_000 }, + use: { + baseURL: process.env.PLAYWRIGHT_BASE_URL ?? "http://localhost:3200", + trace: "retain-on-failure", + navigationTimeout: 45_000, + actionTimeout: 20_000, + }, + projects: [ + { + name: "chromium", + // Use a system browser channel (Google Chrome) so the suite does not depend on + // the bundled chrome-headless-shell download. Override with PW_CHANNEL=msedge + // if Chrome is unavailable. Both ship on a standard Windows install. + use: { ...devices["Desktop Chrome"], channel: process.env.PW_CHANNEL ?? "chrome" }, + }, + ], +}); diff --git a/App/prisma/seed-test-users.ts b/App/prisma/seed-test-users.ts new file mode 100644 index 0000000..59eda29 --- /dev/null +++ b/App/prisma/seed-test-users.ts @@ -0,0 +1,89 @@ +/** + * Seed deterministic, credential-capable TEST USERS into a database. + * + * Why this exists + * --------------- + * `pelagia_test` (the staging / autofix DB) is a daily mirror of production, so it + * only contains real `@pelagiamarine.com` users — most are SSO-only (no password) + * and none have a password we know. That makes it impossible to log into the + * staging instance (port 3200) with the credentials provider to run end-to-end + * feature tests. + * + * This script upserts one **known-password** user per `Role` (using the throwaway + * `@pelagia.local` domain, which never exists in prod, so there is zero collision + * with real accounts). Credentials intentionally mirror + * `tests/e2e/helpers/login.ts` so the same Playwright specs run locally and against + * staging unchanged. + * + * Safety + * ------ + * - Idempotent: upsert keyed on the (unique) email; re-running only refreshes the + * password hash / role / isActive. + * - `employeeId` uses a `TEST-*` prefix so it can never clash with a real + * production employee id carried over by the mirror. + * - Only ever creates the `@pelagia.local` users below — it touches no prod rows. + * + * Usage + * ----- + * DATABASE_URL="postgresql://.../pelagia_test" pnpm tsx prisma/seed-test-users.ts + * + * It is wired into `automation/refresh-test-db.sh` so these accounts are recreated + * automatically after every daily refresh of `pelagia_test`. + */ +import { PrismaClient, Role } from "@prisma/client"; +import bcrypt from "bcryptjs"; + +const prisma = new PrismaClient(); + +/** + * One login per role/flow that the closed-issue feature tests exercise. + * Passwords match tests/e2e/helpers/login.ts (do not change one without the other). + */ +const TEST_USERS: Array<{ + employeeId: string; + email: string; + name: string; + password: string; + role: Role; +}> = [ + { employeeId: "TEST-TECH", email: "tech@pelagia.local", name: "Test Technical", password: "tech1234", role: Role.TECHNICAL }, + { employeeId: "TEST-MANNING",email: "manning@pelagia.local", name: "Test Manning", password: "manning1234", role: Role.MANNING }, + { employeeId: "TEST-ACCT", email: "accounts@pelagia.local", name: "Test Accounts", password: "accounts1234", role: Role.ACCOUNTS }, + { employeeId: "TEST-MGR", email: "manager@pelagia.local", name: "Test Manager", password: "manager1234", role: Role.MANAGER }, + { employeeId: "TEST-SUPER", email: "superuser@pelagia.local", name: "Test Superuser", password: "super1234", role: Role.SUPERUSER }, + { employeeId: "TEST-AUDIT", email: "auditor@pelagia.local", name: "Test Auditor", password: "audit1234", role: Role.AUDITOR }, + { employeeId: "TEST-ADMIN", email: "admin@pelagia.local", name: "Test Admin", password: "admin1234", role: Role.ADMIN }, + { employeeId: "TEST-SITE", email: "site@pelagia.local", name: "Test Site Staff", password: "site1234", role: Role.SITE_STAFF}, +]; + +async function main() { + console.log(`Seeding ${TEST_USERS.length} test users...`); + for (const u of TEST_USERS) { + const passwordHash = await bcrypt.hash(u.password, 12); + await prisma.user.upsert({ + where: { email: u.email }, + // Keep an existing test account in sync (refresh the hash / role / active flag) + // but never overwrite its employeeId once created. + update: { name: u.name, passwordHash, role: u.role, isActive: true }, + create: { + employeeId: u.employeeId, + email: u.email, + name: u.name, + passwordHash, + role: u.role, + isActive: true, + }, + }); + console.log(` ✓ ${u.email.padEnd(28)} ${u.role}`); + } + console.log("Test users ready."); +} + +main() + .catch((e) => { + console.error("seed-test-users failed:", e); + process.exit(1); + }) + .finally(async () => { + await prisma.$disconnect(); + }); diff --git a/App/tests/staging/00-smoke.spec.ts b/App/tests/staging/00-smoke.spec.ts new file mode 100644 index 0000000..d1579c1 --- /dev/null +++ b/App/tests/staging/00-smoke.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Foundation check: the staging instance is reachable and every seeded test user + * can authenticate with the credentials provider. If this fails, none of the + * per-issue specs can run — fix the seed (prisma/seed-test-users.ts) or the tunnel. + */ +test.describe("staging smoke", () => { + test("login page renders the staging build", async ({ page }) => { + await page.goto("/login"); + await expect(page.getByLabel(/email address/i)).toBeVisible(); + await expect(page.getByRole("button", { name: "Sign in", exact: true })).toBeVisible(); + }); + + for (const [name, creds] of Object.entries(USERS)) { + test(`seeded user ${name} can log in`, async ({ page }) => { + await login(page, creds); + await expect(page).not.toHaveURL(/\/login/); + }); + } +}); diff --git a/App/tests/staging/crewing-epics.spec.ts b/App/tests/staging/crewing-epics.spec.ts new file mode 100644 index 0000000..402ba9b --- /dev/null +++ b/App/tests/staging/crewing-epics.spec.ts @@ -0,0 +1,34 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Crewing epics (#75 Requisitions, #76 Candidates, #79 Crew records, #81 Leave & + * Attendance, #83 Office verification, #86 Reference data/admin) — feature-flagged + * behind NEXT_PUBLIC_CREWING_ENABLED, which is "true" on staging. + * + * These are smoke checks that each epic's primary surface renders for an authorised + * role (the deep state-machine flows — pipeline #77, onboarding #78, PPE #80, + * appraisal #82, sign-off #85 — are covered by the existing integration suites noted + * in Docs/TESTING.md). Render-without-redirect is the proof the shipped feature is + * live on staging. + */ + +const PAGES: Array<{ issue: string; name: string; path: string; user: keyof typeof USERS; heading: RegExp }> = [ + { issue: "#75", name: "Requisitions", path: "/crewing/requisitions", user: "MANAGER", heading: /requisition/i }, + { issue: "#76", name: "Candidates", path: "/crewing/candidates", user: "MANAGER", heading: /candidate/i }, + { issue: "#79", name: "Crew records", path: "/crewing/crew", user: "MANAGER", heading: /crew/i }, + { issue: "#81", name: "Leave", path: "/crewing/leave", user: "MANAGER", heading: /leave/i }, + { issue: "#81", name: "Attendance", path: "/crewing/attendance", user: "MANAGER", heading: /attendance/i }, + { issue: "#83", name: "Verification", path: "/crewing/verification", user: "MANNING", heading: /verif/i }, + { issue: "#86", name: "Ranks", path: "/admin/ranks", user: "MANAGER", heading: /rank/i }, + { issue: "#86", name: "Crew admin", path: "/admin/crew", user: "MANAGER", heading: /crew/i }, +]; + +for (const p of PAGES) { + test(`${p.issue} ${p.name} surface renders on staging (${p.path})`, async ({ page }) => { + await login(page, USERS[p.user]); + await page.goto(p.path); + await expect(page, `should not redirect away from ${p.path}`).toHaveURL(new RegExp(p.path.replace(/\//g, "\\/"))); + await expect(page.getByRole("heading", { name: p.heading }).first()).toBeVisible(); + }); +} diff --git a/App/tests/staging/fixtures.ts b/App/tests/staging/fixtures.ts new file mode 100644 index 0000000..391381d --- /dev/null +++ b/App/tests/staging/fixtures.ts @@ -0,0 +1,127 @@ +/** + * Runtime fixture lookups for the staging verification suite. + * + * PO ids in `pelagia_test` change on every daily refresh, so specs must not hard-code + * them. These helpers anchor each spec on a real row that currently exists in the + * staging DB (read-only), keeping the suite stable across refreshes. They use the + * SAME `DATABASE_URL` the Playwright run is given (the SSH tunnel to pelagia_test). + * + * This is test *setup* only — every assertion still runs against the live UI. + */ +import { PrismaClient } from "@prisma/client"; + +let _db: PrismaClient | null = null; +export function db(): PrismaClient { + if (!_db) _db = new PrismaClient(); + return _db; +} +export async function closeDb() { + if (_db) await _db.$disconnect(); + _db = null; +} + +// Mirrors lib/utils.ts POST_APPROVAL_STATUSES — the statuses that count as "approved +// and beyond" for spend/approval trackers (issues #12, #32, #50). +export const POST_APPROVAL_STATUSES = [ + "MGR_APPROVED", + "SENT_FOR_PAYMENT", + "PARTIALLY_PAID", + "PAID_DELIVERED", + "PARTIALLY_CLOSED", + "CLOSED", +] as const; + +/** First day of the current month in local time (matches the dashboard query). */ +export function startOfMonth(now = new Date()): Date { + return new Date(now.getFullYear(), now.getMonth(), 1); +} + +/** Mirror of lib/utils.ts formatDate → e.g. "Jun 11, 2026". */ +export function formatDate(date: Date | string): string { + return new Intl.DateTimeFormat("en-US", { year: "numeric", month: "short", day: "numeric" }).format( + new Date(date), + ); +} + +export async function approvedPo() { + return db().purchaseOrder.findFirst({ + where: { status: { in: [...POST_APPROVAL_STATUSES] }, approvedAt: { not: null } }, + orderBy: { approvedAt: "desc" }, + select: { id: true, poNumber: true, status: true, approvedAt: true, poDate: true }, + }); +} + +/** + * An approved-or-later PO with NO explicit poDate, so its detail "PO Date" must fall + * back to the approval date — the exact case issue #5 is about. + */ +export async function approvedPoNoPoDate() { + return db().purchaseOrder.findFirst({ + where: { status: { in: [...POST_APPROVAL_STATUSES] }, approvedAt: { not: null }, poDate: null }, + orderBy: { approvedAt: "desc" }, + select: { id: true, poNumber: true, status: true, approvedAt: true }, + }); +} + +export async function closedPo() { + return db().purchaseOrder.findFirst({ + where: { status: "CLOSED" }, + select: { id: true, poNumber: true }, + }); +} + +/** An approved-or-later PO whose vendor has a contact email (issue #14). */ +export async function poWithVendorEmail() { + return db().purchaseOrder.findFirst({ + where: { + status: { in: [...POST_APPROVAL_STATUSES] }, + vendor: { contacts: { some: { email: { not: null } } } }, + }, + select: { id: true, poNumber: true, status: true }, + }); +} + +/** A PO that has at least one line item with a non-empty description (issue #8). */ +export async function poWithLineItemDescription() { + const li = await db().pOLineItem.findFirst({ + where: { description: { not: null }, AND: [{ description: { not: "" } }] }, + select: { description: true, po: { select: { id: true, poNumber: true, status: true } } }, + }); + return li ? { ...li.po, description: li.description as string } : null; +} + +/** All CLOSED PO numbers, to assert the manager sees the full closed set (issue #6). */ +export async function closedPoNumbers() { + const rows = await db().purchaseOrder.findMany({ + where: { status: "CLOSED" }, + select: { poNumber: true }, + }); + return rows.map((r) => r.poNumber); +} + +/** A PO that has at least one uploaded document, to exercise attachment grouping (#10). */ +export async function poWithDocuments() { + const doc = await db().pODocument.findFirst({ + select: { po: { select: { id: true, poNumber: true } } }, + }); + return doc?.po ?? null; +} + +export async function totalPoCount() { + return db().purchaseOrder.count(); +} + +/** Count of POs approved in the current month, regardless of current status (issues #12/#32). */ +export async function approvedThisMonthCount() { + return db().purchaseOrder.count({ + where: { status: { in: [...POST_APPROVAL_STATUSES] }, approvedAt: { gte: startOfMonth() } }, + }); +} + +/** A vendor that has a non-null vendorId code, for search-by-code specs (#57/#109). */ +export async function vendorWithCode() { + return db().vendor.findFirst({ + where: { vendorId: { not: null } }, + select: { name: true, vendorId: true }, + }); +} diff --git a/App/tests/staging/helpers.ts b/App/tests/staging/helpers.ts new file mode 100644 index 0000000..f3a7886 --- /dev/null +++ b/App/tests/staging/helpers.ts @@ -0,0 +1,39 @@ +/** + * Shared helpers for the staging closed-issue verification suite. + * + * Credentials mirror prisma/seed-test-users.ts (seeded into pelagia_test) and the + * dev-seed users in tests/e2e/helpers/login.ts. + */ +import { type Page, expect } from "@playwright/test"; + +export interface Credentials { + email: string; + password: string; +} + +export const USERS = { + TECH: { email: "tech@pelagia.local", password: "tech1234" }, + MANNING: { email: "manning@pelagia.local", password: "manning1234" }, + ACCOUNTS: { email: "accounts@pelagia.local", password: "accounts1234" }, + MANAGER: { email: "manager@pelagia.local", password: "manager1234" }, + SUPERUSER: { email: "superuser@pelagia.local", password: "super1234" }, + AUDITOR: { email: "auditor@pelagia.local", password: "audit1234" }, + ADMIN: { email: "admin@pelagia.local", password: "admin1234" }, + SITE: { email: "site@pelagia.local", password: "site1234" }, +} satisfies Record; + +/** Log in via the credentials provider and wait until off the /login page. */ +export async function login(page: Page, creds: Credentials): Promise { + await page.goto("/login"); + await page.getByLabel(/email address/i).fill(creds.email); + await page.getByLabel(/password/i).fill(creds.password); + // The staging login page has two buttons: "Sign in with Microsoft" (SSO) and the + // credentials "Sign in" submit. Target the exact credentials submit. + await page.getByRole("button", { name: "Sign in", exact: true }).click(); + await expect(page).not.toHaveURL(/\/login/, { timeout: 30_000 }); +} + +/** Log out via the header control (best-effort; ignores if already logged out). */ +export async function logout(page: Page): Promise { + await page.context().clearCookies(); +} diff --git a/App/tests/staging/issue-04-po-date-field.spec.ts b/App/tests/staging/issue-04-po-date-field.spec.ts new file mode 100644 index 0000000..f6882ae --- /dev/null +++ b/App/tests/staging/issue-04-po-date-field.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #4 — Submitter can set an optional PO date (back/forward-datable). + * Fix: a `poDate` date input on the PO create/edit forms. + */ +test("#4 PO create form exposes an optional, free-to-set PO Date field", async ({ page }) => { + await login(page, USERS.TECH); + await page.goto("/po/new"); + + const poDate = page.locator('input[name="poDate"]'); + await expect(poDate).toBeVisible(); + await expect(poDate).toHaveAttribute("type", "date"); + await expect(poDate).not.toHaveAttribute("required", ""); + + // It accepts a back-dated value and a forward-dated value. + await poDate.fill("2024-01-15"); + await expect(poDate).toHaveValue("2024-01-15"); + await poDate.fill("2030-12-31"); + await expect(poDate).toHaveValue("2030-12-31"); +}); diff --git a/App/tests/staging/issue-05-approved-date-as-po-date.spec.ts b/App/tests/staging/issue-05-approved-date-as-po-date.spec.ts new file mode 100644 index 0000000..fa588a3 --- /dev/null +++ b/App/tests/staging/issue-05-approved-date-as-po-date.spec.ts @@ -0,0 +1,24 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; +import { approvedPoNoPoDate, formatDate, closeDb } from "./fixtures"; + +/** + * Issue #5 — Once a PO is approved, the PO date shown is the approval date + * (when the submitter did not set an explicit poDate). Display rule: + * poDisplayDate = po.poDate ?? po.approvedAt ?? po.createdAt. + */ +test.afterAll(closeDb); + +test("#5 approved PO detail shows the approval date as the PO Date", async ({ page }) => { + const po = await approvedPoNoPoDate(); + test.skip(!po, "no approved PO without an explicit poDate in staging data"); + + await login(page, USERS.MANAGER); + await page.goto(`/po/${po!.id}`); + + await expect(page.getByText(po!.poNumber)).toBeVisible(); + // The "PO Date" detail row should render the approval date. + const expected = formatDate(po!.approvedAt!); + const row = page.getByText("PO Date", { exact: true }).locator("xpath=ancestor::*[1]"); + await expect(row).toContainText(expected); +}); diff --git a/App/tests/staging/issue-06-closed-list-filters.spec.ts b/App/tests/staging/issue-06-closed-list-filters.spec.ts new file mode 100644 index 0000000..4c782e0 --- /dev/null +++ b/App/tests/staging/issue-06-closed-list-filters.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; +import { closedPoNumbers, closeDb } from "./fixtures"; + +/** + * Issue #6 — Closed PO list filters. + * - MANAGER: the "Closed Purchase Orders" view shows ALL closed POs. + * - Submitter: the view shows ONLY CLOSED (no APPROVED leaking in). + * Route: /my-orders. + */ +test.afterAll(closeDb); + +test("#6 manager sees ALL closed POs on /my-orders", async ({ page }) => { + const closed = await closedPoNumbers(); + test.skip(closed.length === 0, "no closed POs in staging data"); + + await login(page, USERS.MANAGER); + await page.goto("/my-orders"); + await expect(page.getByRole("heading", { name: "Closed Purchase Orders" })).toBeVisible(); + + // The manager sees the FULL closed set (not just their own): every closed PO number + // is present, and no APPROVED status leaks into this view. + for (const poNumber of closed) { + await expect(page.getByText(poNumber).first()).toBeVisible(); + } + await expect(page.getByText("Approved", { exact: true })).toHaveCount(0); +}); + +test("#6 submitter's closed view excludes APPROVED POs", async ({ page }) => { + await login(page, USERS.TECH); + await page.goto("/my-orders"); + await expect(page.getByRole("heading", { name: "Closed Purchase Orders" })).toBeVisible(); + // The bug was APPROVED POs showing here; assert none do. + await expect(page.getByText("Approved", { exact: true })).toHaveCount(0); +}); diff --git a/App/tests/staging/issue-08-export-includes-description.spec.ts b/App/tests/staging/issue-08-export-includes-description.spec.ts new file mode 100644 index 0000000..fc353ae --- /dev/null +++ b/App/tests/staging/issue-08-export-includes-description.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; +import { poWithLineItemDescription, closeDb } from "./fixtures"; + +/** + * Issue #8 — The exported PO must include the line item's optional description. + * The export route renders the printable PO at /api/po/[id]/export?format=pdf; + * we fetch it with the logged-in session and assert the description text is present. + */ +test.afterAll(closeDb); + +test("#8 exported PO contains the line-item description", async ({ page }) => { + const po = await poWithLineItemDescription(); + test.skip(!po, "no PO with a line-item description in staging data"); + + await login(page, USERS.MANAGER); + // page.request shares the authenticated browser cookies. + const res = await page.request.get(`/api/po/${po!.id}/export?format=pdf`); + expect(res.ok()).toBeTruthy(); + const body = await res.text(); + expect(body).toContain(po!.description); +}); diff --git a/App/tests/staging/issue-10-attachments-grouped.spec.ts b/App/tests/staging/issue-10-attachments-grouped.spec.ts new file mode 100644 index 0000000..94950f7 --- /dev/null +++ b/App/tests/staging/issue-10-attachments-grouped.spec.ts @@ -0,0 +1,25 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; +import { poWithDocuments, closeDb } from "./fixtures"; + +/** + * Issue #10 — PO detail shows ALL attachments, grouped by type + * (Submission / Payment / Delivery). Requires a PO that actually has documents; + * if the staging mirror has none, the spec skips (documented data limitation). + */ +test.afterAll(closeDb); + +test("#10 PO detail groups attachments by type", async ({ page }) => { + const po = await poWithDocuments(); + test.skip(!po, "no PO with uploaded documents in staging data"); + + await login(page, USERS.MANAGER); + await page.goto(`/po/${po!.id}`); + await expect(page.getByText(po!.poNumber)).toBeVisible(); + + // The attachments region renders at least one of the typed group headings. + const groups = page.getByText( + /SUBMISSION DOCUMENTS|PAYMENT DOCUMENTS|DELIVERY RECEIPTS|OTHER ATTACHMENTS/i, + ); + await expect(groups.first()).toBeVisible(); +}); diff --git a/App/tests/staging/issue-104-history-pagination.spec.ts b/App/tests/staging/issue-104-history-pagination.spec.ts new file mode 100644 index 0000000..9283d18 --- /dev/null +++ b/App/tests/staging/issue-104-history-pagination.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #104 — /history is paginated with an items-per-page dropdown. + */ +test("#104 history has an items-per-page dropdown that drives a perPage query param", async ({ page }) => { + await login(page, USERS.MANAGER); + await page.goto("/history"); + + const perPage = page.locator("#perPage"); + await expect(perPage).toBeVisible(); + // Options include the configured page sizes. + await expect(perPage.locator('option[value="50"]')).toBeAttached(); + + await perPage.selectOption("50"); + await expect(page).toHaveURL(/perPage=50/); +}); diff --git a/App/tests/staging/issue-109-new-po-vendor-search.spec.ts b/App/tests/staging/issue-109-new-po-vendor-search.spec.ts new file mode 100644 index 0000000..785429c --- /dev/null +++ b/App/tests/staging/issue-109-new-po-vendor-search.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; +import { vendorWithCode, closeDb } from "./fixtures"; + +/** + * Issue #109 — the New PO screen vendor field is a searchable combobox that matches + * by vendor name AND code (mirrors the items search). + */ +test.afterAll(closeDb); + +test("#109 new PO vendor field is a searchable combobox (name + code)", async ({ page }) => { + const vendor = await vendorWithCode(); + test.skip(!vendor, "no vendor with a code in staging data"); + + await login(page, USERS.MANAGER); + await page.goto("/po/new"); + + // Open the vendor combobox (trigger shows the "No vendor selected" placeholder). + await page.getByText("No vendor selected").click(); + + const search = page.getByPlaceholder(/Search by name or code/i); + await expect(search).toBeVisible(); + + // Searching by the vendor's CODE surfaces the vendor by name — proving code search. + await search.fill(vendor!.vendorId!); + await expect(page.getByText(vendor!.name, { exact: false }).first()).toBeVisible(); +}); diff --git a/App/tests/staging/issue-11-terms-catalogue.spec.ts b/App/tests/staging/issue-11-terms-catalogue.spec.ts new file mode 100644 index 0000000..1600903 --- /dev/null +++ b/App/tests/staging/issue-11-terms-catalogue.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #11 — Admin-managed Terms & Conditions catalogue feeding a dynamic PO editor. + * - Admin surface at /admin/terms. + * - The PO create form renders a Terms & Conditions editor. + */ +test("#11 admin Terms & Conditions page renders for a manager", async ({ page }) => { + await login(page, USERS.MANAGER); + await page.goto("/admin/terms"); + await expect(page).not.toHaveURL(/\/login/); + await expect(page.getByRole("heading", { name: /terms.*conditions/i }).first()).toBeVisible(); +}); + +test("#11 new PO form includes a Terms & Conditions editor", async ({ page }) => { + await login(page, USERS.MANAGER); + await page.goto("/po/new"); + await expect(page.getByText(/terms.*conditions/i).first()).toBeVisible(); + // The dynamic editor offers an "Add term" affordance. + await expect(page.getByRole("button", { name: /add term/i })).toBeVisible(); +}); diff --git a/App/tests/staging/issue-12-approved-this-month-card.spec.ts b/App/tests/staging/issue-12-approved-this-month-card.spec.ts new file mode 100644 index 0000000..423a826 --- /dev/null +++ b/App/tests/staging/issue-12-approved-this-month-card.spec.ts @@ -0,0 +1,22 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; +import { approvedThisMonthCount, closeDb } from "./fixtures"; + +/** + * Issue #12 — Manager dashboard 'Total approved this month' was stuck at 0 / not + * updating. Fix: the card counts every PO approved this month (any post-approval + * status). We assert the rendered value matches the DB-computed count, proving it + * reflects real data rather than 0. + */ +test.afterAll(closeDb); + +test("#12 'Approved This Month' card shows the correct, live count", async ({ page }) => { + const expected = await approvedThisMonthCount(); + + await login(page, USERS.MANAGER); + await page.goto("/dashboard"); + + const card = page.locator("a,div", { has: page.getByText("Approved This Month", { exact: true }) }).first(); + await expect(card).toBeVisible(); + await expect(card).toContainText(String(expected)); +}); diff --git a/App/tests/staging/issue-13-payments-this-month-card.spec.ts b/App/tests/staging/issue-13-payments-this-month-card.spec.ts new file mode 100644 index 0000000..1ec1fc0 --- /dev/null +++ b/App/tests/staging/issue-13-payments-this-month-card.spec.ts @@ -0,0 +1,20 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #13 — Accounts dashboard should have a 'Payments completed this month' card + * (analogous to the manager's monthly approval card). + * + * VERIFICATION RESULT: NOT FIXED on staging. The Accounts dashboard currently shows + * only "Ready for Payment" and "Payment Queue Value" — there is no + * payments-completed-this-month card. This is marked test.fail() so the suite stays + * green while clearly recording the gap; if the card is later added, this test will + * start passing and flag that the annotation should be removed. + */ +test.fail(true, "Issue #13 not implemented: no 'payments completed this month' card on the Accounts dashboard"); + +test("#13 Accounts dashboard shows a 'Payments completed this month' card", async ({ page }) => { + await login(page, USERS.ACCOUNTS); + await page.goto("/dashboard"); + await expect(page.getByText(/payments? completed this month/i)).toBeVisible(); +}); diff --git a/App/tests/staging/issue-14-email-to-vendor.spec.ts b/App/tests/staging/issue-14-email-to-vendor.spec.ts new file mode 100644 index 0000000..9bf8cf4 --- /dev/null +++ b/App/tests/staging/issue-14-email-to-vendor.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; +import { poWithVendorEmail, closeDb } from "./fixtures"; + +/** + * Issue #14 — 'Email to vendor' option becomes available once a PO is approved (and + * after payment), when the vendor has a contact email. We assert the button is + * present on such a PO. (The underlying PDF/Outlook pipeline depends on PdfService + * env config; this verifies the user-facing affordance exists.) + */ +test.afterAll(closeDb); + +test("#14 approved PO with a vendor email shows the 'Email to vendor' button", async ({ page }) => { + const po = await poWithVendorEmail(); + test.skip(!po, "no approved PO with a vendor contact email in staging data"); + + await login(page, USERS.MANAGER); + await page.goto(`/po/${po!.id}`); + await expect(page.getByText(po!.poNumber)).toBeVisible(); + await expect(page.getByRole("button", { name: /email to vendor/i })).toBeVisible(); +}); diff --git a/App/tests/staging/issue-19-place-of-delivery-dropdown.spec.ts b/App/tests/staging/issue-19-place-of-delivery-dropdown.spec.ts new file mode 100644 index 0000000..9356040 --- /dev/null +++ b/App/tests/staging/issue-19-place-of-delivery-dropdown.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #19 — Place of Delivery is a dropdown (admin-managed delivery locations), + * not free text. The PO forms render dropdown", async ({ page }) => { + await login(page, USERS.MANAGER); + await page.goto("/po/new"); + const select = page.locator('select[name="placeOfDelivery"]'); + await expect(select).toBeVisible(); +}); + +test("#19 admin delivery-locations page renders for a manager", async ({ page }) => { + await login(page, USERS.MANAGER); + await page.goto("/admin/delivery-locations"); + await expect(page).not.toHaveURL(/\/login/); + await expect(page.getByRole("heading", { name: /delivery location/i }).first()).toBeVisible(); +}); diff --git a/App/tests/staging/issue-24-40-logout-tooltip.spec.ts b/App/tests/staging/issue-24-40-logout-tooltip.spec.ts new file mode 100644 index 0000000..abda311 --- /dev/null +++ b/App/tests/staging/issue-24-40-logout-tooltip.spec.ts @@ -0,0 +1,18 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issues #24 / #40 — change the sign-out control's tooltip from 'Sign out' to + * 'Log out'. Both were pipeline / button-simulation TEST issues. + * + * VERIFICATION RESULT: NOT FIXED on staging. The header logout control still uses + * title="Sign out". Marked test.fail() to record the gap without failing the suite; + * if the copy is changed to 'Log out' this test will pass and flag the annotation. + */ +test.fail(true, "Issues #24/#40 not implemented: logout tooltip is still 'Sign out'"); + +test("#24/#40 logout control tooltip reads 'Log out'", async ({ page }) => { + await login(page, USERS.MANAGER); + await page.goto("/dashboard"); + await expect(page.locator('[title="Log out"]')).toBeVisible(); +}); diff --git a/App/tests/staging/issue-26-41-total-po-card.spec.ts b/App/tests/staging/issue-26-41-total-po-card.spec.ts new file mode 100644 index 0000000..b6c933a --- /dev/null +++ b/App/tests/staging/issue-26-41-total-po-card.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; +import { totalPoCount, closeDb } from "./fixtures"; + +/** + * Issue #41 — the dashboard 'Total Purchase Orders' card shows the correct count. + * Issue #26 — that stat card is clickable and links to the history page. + * Both are verified on the generic dashboard (AUDITOR role). + */ +test.afterAll(closeDb); + +test("#41 'Total Purchase Orders' card shows the correct unfiltered count", async ({ page }) => { + const expected = await totalPoCount(); + await login(page, USERS.AUDITOR); + await page.goto("/dashboard"); + + const card = page.getByRole("link").filter({ hasText: "Total Purchase Orders" }); + await expect(card).toBeVisible(); + await expect(card).toContainText(String(expected)); +}); + +test("#26 'Total Purchase Orders' card links to the history page", async ({ page }) => { + await login(page, USERS.AUDITOR); + await page.goto("/dashboard"); + await page.getByRole("link").filter({ hasText: "Total Purchase Orders" }).click(); + await expect(page).toHaveURL(/\/history/); +}); diff --git a/App/tests/staging/issue-31-history-multi-status.spec.ts b/App/tests/staging/issue-31-history-multi-status.spec.ts new file mode 100644 index 0000000..915e9fb --- /dev/null +++ b/App/tests/staging/issue-31-history-multi-status.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #31 — PO history allows selecting MULTIPLE statuses (OR-ed). The status + * filter is a checkbox dropdown; applying two statuses yields two `status` query + * params. + */ +test("#31 history status filter supports multiple OR-ed statuses", async ({ page }) => { + await login(page, USERS.MANAGER); + await page.goto("/history"); + + // Open the status dropdown (button label is "All statuses" / "N statuses"). + await page.getByRole("button", { name: /statuses/i }).click(); + await page.getByRole("checkbox", { name: "Closed" }).check(); + await page.getByRole("checkbox", { name: "Approved" }).check(); + await page.getByRole("button", { name: "Apply" }).click(); + + await expect(page).toHaveURL(/status=CLOSED/); + await expect(page).toHaveURL(/status=MGR_APPROVED/); +}); diff --git a/App/tests/staging/issue-32-approved-month-clickthrough.spec.ts b/App/tests/staging/issue-32-approved-month-clickthrough.spec.ts new file mode 100644 index 0000000..72ef376 --- /dev/null +++ b/App/tests/staging/issue-32-approved-month-clickthrough.spec.ts @@ -0,0 +1,21 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #32 — the manager 'Approved This Month' card counts POs approved this month + * regardless of their current status, and clicking it opens history pre-filtered by + * approval date (?approvedFrom=YYYY-MM-01). + */ +test("#32 'Approved This Month' card links to history filtered by approval date", async ({ page }) => { + const now = new Date(); + const expectedParam = `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}-01`; + + await login(page, USERS.MANAGER); + await page.goto("/dashboard"); + + const card = page.getByRole("link").filter({ hasText: "Approved This Month" }); + await expect(card).toBeVisible(); + await card.click(); + + await expect(page).toHaveURL(new RegExp(`/history\\?approvedFrom=${expectedParam}`)); +}); diff --git a/App/tests/staging/issue-44-line-item-units.spec.ts b/App/tests/staging/issue-44-line-item-units.spec.ts new file mode 100644 index 0000000..2f98514 --- /dev/null +++ b/App/tests/staging/issue-44-line-item-units.spec.ts @@ -0,0 +1,15 @@ +import { test, expect } from "@playwright/test"; +import { USERS, login } from "./helpers"; + +/** + * Issue #44 — the PO line-item unit dropdown must offer months and year(s) + * (previously only days/short units). + */ +test("#44 line-item unit dropdown includes month and year options", async ({ page }) => { + await login(page, USERS.TECH); + await page.goto("/po/new"); + + // The line-items editor renders at least one unit