Merge pull request 'test(staging): feature-level verification of closed issues + seeded test users' (#119) from feat/staging-issue-verification into master
All checks were successful
Refresh staging / refresh (push) Successful in 7s
All checks were successful
Refresh staging / refresh (push) Successful in 7s
Reviewed-on: #119
This commit is contained in:
commit
cf69292be3
30 changed files with 995 additions and 0 deletions
1
App/.gitignore
vendored
1
App/.gitignore
vendored
|
|
@ -13,6 +13,7 @@
|
|||
# Testing
|
||||
/coverage
|
||||
/playwright-report
|
||||
/playwright-report-staging
|
||||
/test-results
|
||||
/blob-report
|
||||
|
||||
|
|
|
|||
43
App/playwright.staging.config.ts
Normal file
43
App/playwright.staging.config.ts
Normal file
|
|
@ -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@<pms1>
|
||||
* 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" },
|
||||
},
|
||||
],
|
||||
});
|
||||
89
App/prisma/seed-test-users.ts
Normal file
89
App/prisma/seed-test-users.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
22
App/tests/staging/00-smoke.spec.ts
Normal file
22
App/tests/staging/00-smoke.spec.ts
Normal file
|
|
@ -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/);
|
||||
});
|
||||
}
|
||||
});
|
||||
34
App/tests/staging/crewing-epics.spec.ts
Normal file
34
App/tests/staging/crewing-epics.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
}
|
||||
127
App/tests/staging/fixtures.ts
Normal file
127
App/tests/staging/fixtures.ts
Normal file
|
|
@ -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 },
|
||||
});
|
||||
}
|
||||
39
App/tests/staging/helpers.ts
Normal file
39
App/tests/staging/helpers.ts
Normal file
|
|
@ -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<string, Credentials>;
|
||||
|
||||
/** Log in via the credentials provider and wait until off the /login page. */
|
||||
export async function login(page: Page, creds: Credentials): Promise<void> {
|
||||
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<void> {
|
||||
await page.context().clearCookies();
|
||||
}
|
||||
22
App/tests/staging/issue-04-po-date-field.spec.ts
Normal file
22
App/tests/staging/issue-04-po-date-field.spec.ts
Normal file
|
|
@ -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");
|
||||
});
|
||||
24
App/tests/staging/issue-05-approved-date-as-po-date.spec.ts
Normal file
24
App/tests/staging/issue-05-approved-date-as-po-date.spec.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
35
App/tests/staging/issue-06-closed-list-filters.spec.ts
Normal file
35
App/tests/staging/issue-06-closed-list-filters.spec.ts
Normal file
|
|
@ -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);
|
||||
});
|
||||
|
|
@ -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);
|
||||
});
|
||||
25
App/tests/staging/issue-10-attachments-grouped.spec.ts
Normal file
25
App/tests/staging/issue-10-attachments-grouped.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
18
App/tests/staging/issue-104-history-pagination.spec.ts
Normal file
18
App/tests/staging/issue-104-history-pagination.spec.ts
Normal file
|
|
@ -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/);
|
||||
});
|
||||
27
App/tests/staging/issue-109-new-po-vendor-search.spec.ts
Normal file
27
App/tests/staging/issue-109-new-po-vendor-search.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
22
App/tests/staging/issue-11-terms-catalogue.spec.ts
Normal file
22
App/tests/staging/issue-11-terms-catalogue.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
22
App/tests/staging/issue-12-approved-this-month-card.spec.ts
Normal file
22
App/tests/staging/issue-12-approved-this-month-card.spec.ts
Normal file
|
|
@ -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));
|
||||
});
|
||||
20
App/tests/staging/issue-13-payments-this-month-card.spec.ts
Normal file
20
App/tests/staging/issue-13-payments-this-month-card.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
21
App/tests/staging/issue-14-email-to-vendor.spec.ts
Normal file
21
App/tests/staging/issue-14-email-to-vendor.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
|
|
@ -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 <select name="placeOfDelivery">, and the admin
|
||||
* surface lives at /admin/delivery-locations.
|
||||
*/
|
||||
test("#19 PO form Place of Delivery is a <select> 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();
|
||||
});
|
||||
18
App/tests/staging/issue-24-40-logout-tooltip.spec.ts
Normal file
18
App/tests/staging/issue-24-40-logout-tooltip.spec.ts
Normal file
|
|
@ -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();
|
||||
});
|
||||
27
App/tests/staging/issue-26-41-total-po-card.spec.ts
Normal file
27
App/tests/staging/issue-26-41-total-po-card.spec.ts
Normal file
|
|
@ -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/);
|
||||
});
|
||||
21
App/tests/staging/issue-31-history-multi-status.spec.ts
Normal file
21
App/tests/staging/issue-31-history-multi-status.spec.ts
Normal file
|
|
@ -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/);
|
||||
});
|
||||
|
|
@ -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}`));
|
||||
});
|
||||
15
App/tests/staging/issue-44-line-item-units.spec.ts
Normal file
15
App/tests/staging/issue-44-line-item-units.spec.ts
Normal file
|
|
@ -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 <select>; assert the new options.
|
||||
await expect(page.locator('option[value="month"]').first()).toBeAttached();
|
||||
await expect(page.locator('option[value="year"]').first()).toBeAttached();
|
||||
});
|
||||
18
App/tests/staging/issue-50-rupee-compact-format.spec.ts
Normal file
18
App/tests/staging/issue-50-rupee-compact-format.spec.ts
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { USERS, login } from "./helpers";
|
||||
|
||||
/**
|
||||
* Issue #50 — the manager approved-spend card uses the rupee symbol and compact
|
||||
* Indian formatting (₹… with L / Cr / K), not a dollar sign.
|
||||
*/
|
||||
test("#50 'Total Approved Spend' card shows ₹ with compact L/Cr/K formatting", async ({ page }) => {
|
||||
await login(page, USERS.MANAGER);
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const card = page.locator("div").filter({ has: page.getByText("Total Approved Spend", { exact: true }) }).first();
|
||||
await expect(card).toBeVisible();
|
||||
// ₹ present, no dollar sign, value matches the compact format (₹2 Cr / ₹49 L / ₹75 K / ₹500).
|
||||
await expect(card).toContainText("₹");
|
||||
await expect(card).not.toContainText("$");
|
||||
await expect(card).toContainText(/₹\s?-?\d[\d.,]*\s?(Cr|L|K)?/);
|
||||
});
|
||||
38
App/tests/staging/issue-53-cancel-po-modal.spec.ts
Normal file
38
App/tests/staging/issue-53-cancel-po-modal.spec.ts
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { USERS, login } from "./helpers";
|
||||
import { approvedPo, closeDb } from "./fixtures";
|
||||
|
||||
/**
|
||||
* Issue #53 — Managers can cancel a PO via a confirmation modal that requires a
|
||||
* reason and typing the word "cancel". This spec verifies the affordance and the
|
||||
* type-to-confirm guard WITHOUT actually cancelling (non-destructive on staging
|
||||
* data): it confirms the submit button stays disabled until "cancel" is typed, then
|
||||
* closes the modal.
|
||||
*/
|
||||
test.afterAll(closeDb);
|
||||
|
||||
test("#53 Cancel PO modal enforces type-'cancel'-to-confirm", async ({ page }) => {
|
||||
const po = await approvedPo();
|
||||
test.skip(!po, "no approved PO in staging data to exercise the cancel control");
|
||||
|
||||
await login(page, USERS.MANAGER);
|
||||
await page.goto(`/po/${po!.id}`);
|
||||
|
||||
await page.getByRole("button", { name: "Cancel PO" }).click();
|
||||
|
||||
const submit = page.getByRole("button", { name: "Cancel this PO" });
|
||||
await expect(submit).toBeVisible();
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
// A reason alone is not enough.
|
||||
await page.getByPlaceholder(/Duplicate order/i).fill("Verification test — not a real cancellation");
|
||||
await expect(submit).toBeDisabled();
|
||||
|
||||
// Typing the confirmation word enables it.
|
||||
await page.getByPlaceholder("cancel").fill("cancel");
|
||||
await expect(submit).toBeEnabled();
|
||||
|
||||
// Back out without cancelling.
|
||||
await page.getByRole("button", { name: "Keep PO" }).click();
|
||||
await expect(submit).toBeHidden();
|
||||
});
|
||||
22
App/tests/staging/issue-57-vendor-search-catalogue.spec.ts
Normal file
22
App/tests/staging/issue-57-vendor-search-catalogue.spec.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { USERS, login } from "./helpers";
|
||||
import { vendorWithCode, closeDb } from "./fixtures";
|
||||
|
||||
/**
|
||||
* Issue #57 — /catalogue/vendors (formerly /inventory/vendors) is searchable by
|
||||
* vendor id/code, and the id is shown next to the name.
|
||||
*/
|
||||
test.afterAll(closeDb);
|
||||
|
||||
test("#57 vendors are searchable by vendor id, with the id shown next to the name", async ({ page }) => {
|
||||
const vendor = await vendorWithCode();
|
||||
test.skip(!vendor, "no vendor with a vendorId code in staging data");
|
||||
|
||||
await login(page, USERS.MANAGER);
|
||||
await page.goto("/catalogue/vendors");
|
||||
|
||||
await page.getByPlaceholder(/Search by name, ID, GSTIN/i).fill(vendor!.vendorId!);
|
||||
// The matching vendor row shows both the name and the id badge.
|
||||
await expect(page.getByText(vendor!.name).first()).toBeVisible();
|
||||
await expect(page.getByText(vendor!.vendorId!, { exact: false }).first()).toBeVisible();
|
||||
});
|
||||
31
App/tests/staging/issue-96-sidebar-collapsible.spec.ts
Normal file
31
App/tests/staging/issue-96-sidebar-collapsible.spec.ts
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
import { USERS, login } from "./helpers";
|
||||
|
||||
/**
|
||||
* Issue #96 — sidebar section headings (Purchasing / Crewing / Administration) are
|
||||
* collapsible, collapsed by default, and act as a single-open accordion (opening one
|
||||
* collapses the others).
|
||||
*/
|
||||
test("#96 sidebar sections are collapsible and single-open", async ({ page }) => {
|
||||
await login(page, USERS.MANAGER);
|
||||
// /dashboard is not inside any section, so all sections start collapsed.
|
||||
await page.goto("/dashboard");
|
||||
|
||||
const purchasing = page.getByRole("button", { name: "Purchasing" });
|
||||
const crewing = page.getByRole("button", { name: "Crewing" });
|
||||
await expect(purchasing).toBeVisible();
|
||||
await expect(crewing).toBeVisible();
|
||||
|
||||
// Collapsed by default.
|
||||
await expect(purchasing).toHaveAttribute("aria-expanded", "false");
|
||||
await expect(crewing).toHaveAttribute("aria-expanded", "false");
|
||||
|
||||
// Opening Purchasing expands it.
|
||||
await purchasing.click();
|
||||
await expect(purchasing).toHaveAttribute("aria-expanded", "true");
|
||||
|
||||
// Opening Crewing collapses Purchasing (single-open accordion).
|
||||
await crewing.click();
|
||||
await expect(crewing).toHaveAttribute("aria-expanded", "true");
|
||||
await expect(purchasing).toHaveAttribute("aria-expanded", "false");
|
||||
});
|
||||
132
Docs/TESTING.md
Normal file
132
Docs/TESTING.md
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
# Testing
|
||||
|
||||
This repo has three test tiers (see `App/CLAUDE.md` → Commands):
|
||||
|
||||
| Tier | Tool | Scope | Command |
|
||||
|---|---|---|---|
|
||||
| Unit | Vitest (jsdom) | pure functions / components | `pnpm test` |
|
||||
| Integration | Vitest (node + real DB) | server actions against a Postgres DB | `pnpm test:integration` |
|
||||
| E2E (local) | Playwright | full UI against a local `pnpm dev` | `pnpm test:e2e` |
|
||||
|
||||
This document covers a fourth, purpose-built tier:
|
||||
|
||||
## Staging closed-issue verification (`App/tests/staging/`)
|
||||
|
||||
A **feature-level** Playwright suite that drives the **running staging instance**
|
||||
(`pm2 ppms-staging`, port 3200 on pms1 — see [`../automation/README.md`](../automation/README.md) → *Staging*)
|
||||
to verify that every closed portal issue is actually fixed on the deployed build.
|
||||
Unlike the local E2E suite it does not start a dev server; it logs in and clicks
|
||||
through the real staging app, exactly as a user would.
|
||||
|
||||
### Why a dedicated tier
|
||||
|
||||
Staging runs against `pelagia_test`, a daily mirror of production. That mirror only
|
||||
contains real `@pelagiamarine.com` users — most are SSO-only and none have a password
|
||||
we know — so the credentials login can't be used for automated testing. To solve this
|
||||
without touching production, the refresh seeds **deterministic test users** (one per
|
||||
role) with known passwords.
|
||||
|
||||
### Test users (seeding)
|
||||
|
||||
[`App/prisma/seed-test-users.ts`](../App/prisma/seed-test-users.ts) idempotently
|
||||
upserts one credential-capable login per role on the throwaway `@pelagia.local`
|
||||
domain (no collision with real accounts):
|
||||
|
||||
| Email | Password | Role |
|
||||
|---|---|---|
|
||||
| `tech@pelagia.local` | `tech1234` | TECHNICAL |
|
||||
| `manning@pelagia.local` | `manning1234` | MANNING |
|
||||
| `accounts@pelagia.local` | `accounts1234` | ACCOUNTS |
|
||||
| `manager@pelagia.local` | `manager1234` | MANAGER |
|
||||
| `superuser@pelagia.local` | `super1234` | SUPERUSER |
|
||||
| `auditor@pelagia.local` | `audit1234` | AUDITOR |
|
||||
| `admin@pelagia.local` | `admin1234` | ADMIN |
|
||||
| `site@pelagia.local` | `site1234` | SITE_STAFF |
|
||||
|
||||
[`automation/refresh-test-db.sh`](../automation/refresh-test-db.sh) runs this seed
|
||||
automatically after every daily refresh of `pelagia_test`, so the logins always exist
|
||||
on staging. To seed manually (e.g. before a one-off run):
|
||||
|
||||
```bash
|
||||
DATABASE_URL="postgresql://…/pelagia_test" pnpm tsx prisma/seed-test-users.ts
|
||||
```
|
||||
|
||||
### Running the suite
|
||||
|
||||
From a machine that can reach pms1, open SSH tunnels to the staging app **and** the
|
||||
DB (the suite reads a few fixture ids straight from `pelagia_test` so it stays stable
|
||||
across the daily refresh):
|
||||
|
||||
```bash
|
||||
ssh -N -L 3200:localhost:3200 -L 15432:localhost:5432 shad0w@<pms1>
|
||||
```
|
||||
|
||||
Then, from `App/`:
|
||||
|
||||
```bash
|
||||
PLAYWRIGHT_BASE_URL=http://localhost:3200 \
|
||||
DATABASE_URL="postgresql://pelagia_user:…@localhost:15432/pelagia_test" \
|
||||
pnpm exec playwright test --config playwright.staging.config.ts
|
||||
```
|
||||
|
||||
- `PLAYWRIGHT_BASE_URL` — the staging app (default `http://localhost:3200`).
|
||||
- `DATABASE_URL` — the tunnelled staging DB, used only for read-only fixture lookups
|
||||
(which approved/closed PO to open, expected counts). Every assertion runs against
|
||||
the live UI.
|
||||
|
||||
### What each script verifies (issue → script map)
|
||||
|
||||
One spec file per issue; the filename is the mapping. `SKIP` means the staging data
|
||||
currently has no row to exercise the case (the spec self-skips with a message).
|
||||
|
||||
| Issue | Script | Verifies | Result |
|
||||
|---|---|---|---|
|
||||
| — | `00-smoke.spec.ts` | staging reachable + all seeded users can log in | PASS |
|
||||
| #4 | `issue-04-po-date-field.spec.ts` | optional, back/forward-datable PO Date field on the PO form | PASS |
|
||||
| #5 | `issue-05-approved-date-as-po-date.spec.ts` | approved PO detail shows the approval date as the PO Date | PASS |
|
||||
| #6 | `issue-06-closed-list-filters.spec.ts` | manager sees all CLOSED POs; submitter's Closed view excludes APPROVED | PASS |
|
||||
| #8 | `issue-08-export-includes-description.spec.ts` | exported PO includes the line-item optional description | PASS |
|
||||
| #10 | `issue-10-attachments-grouped.spec.ts` | PO detail groups attachments by type (Submission/Payment/Delivery) | SKIP (no attachment data on staging) |
|
||||
| #11 | `issue-11-terms-catalogue.spec.ts` | admin T&C catalogue page + dynamic PO terms editor | PASS |
|
||||
| #12 | `issue-12-approved-this-month-card.spec.ts` | manager 'Approved This Month' card shows the correct live count (was stuck at 0) | PASS |
|
||||
| #13 | `issue-13-payments-this-month-card.spec.ts` | accounts 'Payments completed this month' card | **KNOWN FAIL — not implemented on staging** |
|
||||
| #14 | `issue-14-email-to-vendor.spec.ts` | 'Email to vendor' button on an approved PO with a vendor email | PASS |
|
||||
| #19 | `issue-19-place-of-delivery-dropdown.spec.ts` | Place of Delivery is a dropdown + admin delivery-locations page | PASS |
|
||||
| #24/#40 | `issue-24-40-logout-tooltip.spec.ts` | logout tooltip reads 'Log out' | **KNOWN FAIL — still 'Sign out' (these were pipeline test issues)** |
|
||||
| #26/#41 | `issue-26-41-total-po-card.spec.ts` | 'Total Purchase Orders' card count correct (#41) + links to history (#26) | PASS |
|
||||
| #31 | `issue-31-history-multi-status.spec.ts` | PO history filter accepts multiple OR-ed statuses | PASS |
|
||||
| #32 | `issue-32-approved-month-clickthrough.spec.ts` | 'Approved This Month' card links to history filtered by approval date | PASS |
|
||||
| #44 | `issue-44-line-item-units.spec.ts` | line-item unit dropdown includes months and year(s) | PASS |
|
||||
| #50 | `issue-50-rupee-compact-format.spec.ts` | approved-spend card uses ₹ with compact L/Cr formatting | PASS |
|
||||
| #53 | `issue-53-cancel-po-modal.spec.ts` | manager Cancel-PO modal with type-'cancel'-to-confirm guard | PASS |
|
||||
| #57 | `issue-57-vendor-search-catalogue.spec.ts` | /catalogue/vendors searchable by vendor id, id shown next to name | PASS |
|
||||
| #96 | `issue-96-sidebar-collapsible.spec.ts` | sidebar sections collapsible, collapsed by default, single-open | PASS |
|
||||
| #104 | `issue-104-history-pagination.spec.ts` | /history items-per-page pagination | PASS |
|
||||
| #109 | `issue-109-new-po-vendor-search.spec.ts` | new-PO vendor field is a searchable combobox (name + code) | PASS |
|
||||
| #75/#76/#79/#81/#83/#86 | `crewing-epics.spec.ts` | each crewing epic's primary surface renders for an authorised role | PASS |
|
||||
|
||||
### Issues not covered by a staging spec (and why)
|
||||
|
||||
| Issue | Reason |
|
||||
|---|---|
|
||||
| #1 | "Add CHANGELOG.md" — pipeline bootstrap; verified by the file existing at the repo root, not a runtime feature. |
|
||||
| #3, #42 | Explicit pipeline / token test issues ("no action needed; close without a fix"). |
|
||||
| #7 | Inventory-on-approval — the inventory surface is gated by `NEXT_PUBLIC_INVENTORY_ENABLED`, which is **false** on staging, so it is not UI-verifiable there. Covered by the `tests/integration` inventory tests. |
|
||||
| #17 | GST CAPTCHA extraction — depends on the live external GST portal (GstService); not deterministically testable. |
|
||||
| #18 | "Prompt to save draft on navigate-away" — a client-side `beforeunload` guard; not reliably driveable in headless Playwright. |
|
||||
| Crewing deep flows (#77 pipeline, #78 onboarding, #80 PPE, #82 appraisal, #85 sign-off) | State-machine flows covered by the existing integration suites (`tests/integration/applications.test.ts`, `onboarding.test.ts`, `appraisal.test.ts`, `signoff.test.ts`, etc.). The staging suite smoke-checks the epic surfaces render. |
|
||||
|
||||
### Findings (verification result)
|
||||
|
||||
Running the suite against staging surfaced **two closed issues that are not actually
|
||||
fixed** on the current build:
|
||||
|
||||
- **#13** — the Accounts dashboard has no "Payments completed this month" card (only
|
||||
"Ready for Payment" and "Payment Queue Value").
|
||||
- **#24 / #40** — the logout control tooltip still reads "Sign out", not "Log out".
|
||||
(Both were pipeline / button-simulation test issues, likely closed without a code
|
||||
change.)
|
||||
|
||||
These are encoded as `test.fail()` specs so the suite stays green while the gaps are
|
||||
recorded; if either is fixed later, its spec flips to passing and flags the
|
||||
annotation for removal.
|
||||
|
|
@ -74,3 +74,21 @@ if [ -n "$MIG_DIR" ]; then
|
|||
else
|
||||
log "No master checkout with migrations found; skipping migrate (test DB has prod schema only)."
|
||||
fi
|
||||
|
||||
# Seed deterministic, credential-capable TEST USERS (one per role) so the staging
|
||||
# instance can be logged into for end-to-end feature verification. The prod mirror
|
||||
# only carries real @pelagiamarine.com users (mostly SSO-only, no known password),
|
||||
# which makes credential login — and therefore the Playwright closed-issue suite
|
||||
# (App/tests/staging/) — impossible. These @pelagia.local accounts never exist in
|
||||
# prod, so there is no collision; the seed is idempotent (upsert by email).
|
||||
# See App/tests/staging/ and Docs/TESTING.md.
|
||||
if [ -n "$MIG_DIR" ] && [ -f "$MIG_DIR/prisma/seed-test-users.ts" ]; then
|
||||
log "Seeding test users into $TEST_DB ..."
|
||||
if ( cd "$MIG_DIR" && DATABASE_URL="$TEST_URL" pnpm tsx prisma/seed-test-users.ts ) >/tmp/seed-test-users.log 2>&1; then
|
||||
log "Test users seeded."
|
||||
else
|
||||
log "WARNING: test-user seed failed; see /tmp/seed-test-users.log"; tail -5 /tmp/seed-test-users.log
|
||||
fi
|
||||
else
|
||||
log "Skipping test-user seed (no checkout with prisma/seed-test-users.ts)."
|
||||
fi
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue