- Delete /inventory/items/[id] — items expand inline in the list - Move SiteSelect from deleted [id] folder to components/inventory/site-select - Fix admin product detail page import to use new shared path - Fix items-table: Fragment key prop, restore Link import, plain text item names - Fix vendor-items-table: remove broken link to deleted item detail page Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
13 KiB
Pelagia Portal — Test Plan
Version: 1.0
Date: 2026-05-09
Project: Pelagia Marine Services PO Portal
Scope: Unit, Integration, and E2E test coverage across all portal features
1. Overview
This document describes the testing strategy, scope, tooling, and coverage matrix for the Pelagia Portal. It is intended as the authoritative reference for what is tested, why, and how to run each layer.
The portal manages the full lifecycle of purchase orders: creation, submission, manager review, vendor assignment, payment, and receipt confirmation. Testing focuses on correctness of state transitions, permission enforcement, and data integrity.
2. Testing Stack
| Layer | Tool | Environment | Command |
|---|---|---|---|
| Unit | Vitest 2.x | jsdom | pnpm test |
| Integration | Vitest 2.x | Node (real DB) | pnpm test:integration |
| E2E | Playwright 1.49 | Chromium (dev server) | pnpm test:e2e |
Key libraries: @testing-library/react, @testing-library/jest-dom, @testing-library/user-event.
Unit tests live in tests/unit/. Integration tests live in tests/integration/. E2E specs live in tests/e2e/.
Integration tests run serially in a single fork (poolOptions.forks.singleFork = true) to avoid database conflicts. Each test suite cleans up its own data via afterEach using the deletePosByTitle(PREFIX) helper.
3. Test Data & Environment
3.1 Seeded Data (prisma/seed.ts)
| Entity | Records | Notes |
|---|---|---|
| Users | 5 | admin, manager, tech, accounts, manning |
| Vessels | 3 | MV Pelagia Star, MV Aegean Wind, MV Poseidon |
| Accounts | 3 | TECH-OPS, CREW-MGT, FUEL-BNK |
| Vendors | 12 | VND-0001 to VND-0012; VND-0003 and VND-0012 are unverified |
| Products | 25 | Spanning lubricants, filters, safety, rope, electrical, paint, navigation |
Re-run with npx tsx prisma/seed.ts before integration tests if the database is reset.
3.2 Authentication Mocking
Integration tests mock @/auth to inject a session without real credentials:
vi.mock("@/auth", () => ({ auth: vi.fn() }));
vi.mocked(auth).mockResolvedValue(makeSession(userId, "MANAGER"));
makeSession(userId, role) is defined in tests/integration/helpers.ts.
3.3 Side-Effect Mocking
All integration and unit tests mock:
@/lib/notifier— prevents email dispatchnext/cache(revalidatePath) — avoids Next.js cache calls outside a server context
4. Coverage Matrix
4.1 Unit Tests
| File | Test File | Cases Covered |
|---|---|---|
lib/permissions.ts |
tests/unit/permissions.test.ts |
All 7 roles × key permissions; requirePermission throws |
lib/po-state-machine.ts |
tests/unit/po-state-machine.test.ts |
canPerformAction, getTransition, requiresNote, getAvailableActions; MANAGER/ACCOUNTS expansions |
lib/po-import-parser.ts |
tests/unit/po-import-parser.test.ts |
cellStr, cellNum, parseSheet (real + synthetic), parseWorkbook |
lib/validations/po.ts |
tests/unit/validations.test.ts |
lineItemSchema, createPoSchema, TC defaults |
components/po/po-line-items-editor.tsx |
tests/unit/po-line-items-editor.test.tsx |
Edit mode, read-only mode, totals, add/remove |
components/po/po-status-badge.tsx |
tests/unit/po-status-badge.test.tsx |
All status labels |
lib/utils.ts |
tests/unit/utils.test.ts |
formatCurrency, formatDate, generatePoNumber, status maps |
4.2 Integration Tests
| Test File | Feature | Scenarios |
|---|---|---|
create-po.test.ts |
S-01, S-02, S-03 | Draft, submit, line items, totals, optional fields, notifications |
approval-actions.test.ts |
M-02, M-03, M-04, S-06, S-07 | Approve, reject, request edits, vendor ID flow, resubmit |
payment-actions.test.ts |
A-01, A-02 | Payment queue, mark paid |
discard-po.test.ts |
Discard draft | Owner, MANAGER, SUPERUSER can discard; ACCOUNTS and non-owners denied; status guard; cascade cleanup |
vendor-approval.test.ts |
Vendor gate + provide vendor ID | Approval blocked without vendor; ACCOUNTS can provide vendor ID; unverified vendor rejected; AUDITOR denied |
manager-po-creation.test.ts |
Manager creates POs | MANAGER can create, submit, discard; ACCOUNTS denied; role documented for self-approval |
products-search.test.ts |
Product search API | Auth, min-length validation, name/code/description search, case-insensitive, max 10, inactive excluded, Decimal serialised |
import-api.test.ts |
Excel import API | Auth (TECHNICAL/ACCOUNTS → 403), no file, invalid file, correct parse of Sample_PO.xlsx |
4.3 E2E Tests (Playwright)
| Spec File | Scenarios |
|---|---|
auth.spec.ts |
Login, redirect on bad creds, role badge, sign-out |
submitter-journey.spec.ts |
Create draft, add line items, submit, see status transitions |
manager-approvals.spec.ts |
Review PO, approve with/without note, reject, request edits |
accounts-payment.spec.ts |
Payment queue, process payment, confirm receipt |
po-export.spec.ts |
PDF and XLSX export buttons and content |
5. Permission Test Matrix
The table below documents every role's expected access to key operations. ✓ = allowed, ✗ = denied.
| Operation | TECHNICAL | MANNING | ACCOUNTS | MANAGER | SUPERUSER | AUDITOR | ADMIN |
|---|---|---|---|---|---|---|---|
| Create PO | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Submit PO | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Edit own draft | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Discard own draft | ✓ | ✓ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Discard any draft | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Approve PO | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Reject PO | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Request edits | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ |
| Provide vendor ID | Own PO only | Own PO only | ✓ | ✓ | ✓ | ✗ | ✗ |
| Process payment | ✗ | ✗ | ✓ | ✗ | ✓ | ✗ | ✗ |
| Confirm receipt | Own PO only | Own PO only | ✗ | ✗ | ✓ | ✗ | ✗ |
| Manage vendors | ✗ | ✗ | ✓ | ✓ | ✗ | ✗ | ✓ |
| Manage products | ✗ | ✗ | ✗ | ✗ | ✗ | ✗ | ✓ |
| Import PO | ✗ | ✗ | ✗ | ✓ | ✓ | ✗ | ✓ |
| View analytics | ✗ | ✗ | ✗ | ✓ | ✓ | ✓ | ✓ |
Business rules tested explicitly:
- A vendor must be assigned before a manager can approve a PO.
- Only verified vendors (those with a
vendorIdfield) may be assigned viaprovideVendorId. - Discarding is only possible on
DRAFTstatus POs.
6. Feature-Level Test Scenarios
F-01: PO Creation & Draft Management
| ID | Scenario | Type | File |
|---|---|---|---|
| S-01 | Create PO with multiple line items; verify totals | Integration | create-po.test.ts |
| S-02 | Save as draft; verify status = DRAFT | Integration | create-po.test.ts |
| S-02a | ACCOUNTS role denied creation | Integration | create-po.test.ts |
| S-02b | MANAGER can create and save a draft | Integration | manager-po-creation.test.ts |
| S-03 | Submit for approval; status = MGR_REVIEW | Integration | create-po.test.ts |
| S-04 | Discard draft by owner | Integration | discard-po.test.ts |
| S-04a | MANAGER discards any draft | Integration | discard-po.test.ts |
| S-04b | ACCOUNTS cannot discard | Integration | discard-po.test.ts |
| S-04c | Cannot discard a submitted PO | Integration | discard-po.test.ts |
F-02: Approval Workflow
| ID | Scenario | Type | File |
|---|---|---|---|
| M-01 | Manager sees pending POs | E2E | manager-approvals.spec.ts |
| M-02 | Approve PO → MGR_APPROVED | Integration / E2E | approval-actions.test.ts |
| M-02a | Approve with note stores managerNote | Integration | approval-actions.test.ts |
| M-02b | Approval blocked — no vendor assigned | Integration | vendor-approval.test.ts |
| M-03 | Reject PO with note | Integration / E2E | approval-actions.test.ts |
| M-04 | Request edits → EDITS_REQUESTED | Integration | approval-actions.test.ts |
| M-04a | Request vendor ID → VENDOR_ID_PENDING | Integration | approval-actions.test.ts |
| M-04b | TECHNICAL denied approval | Integration | approval-actions.test.ts |
F-03: Vendor ID Assignment
| ID | Scenario | Type | File |
|---|---|---|---|
| S-06 | TECHNICAL provides vendor ID on own PO | Integration | approval-actions.test.ts |
| S-06a | ACCOUNTS provides vendor ID | Integration | vendor-approval.test.ts |
| S-06b | Unverified vendor rejected | Integration | vendor-approval.test.ts |
| S-06c | AUDITOR cannot provide vendor ID | Integration | vendor-approval.test.ts |
| S-06d | Wrong status → error | Integration | vendor-approval.test.ts |
F-04: Payment & Receipt
| ID | Scenario | Type | File |
|---|---|---|---|
| A-01 | Accounts processes payment | Integration / E2E | payment-actions.test.ts |
| A-02 | Mark as paid with reference | Integration / E2E | payment-actions.test.ts |
F-05: Excel Import
| ID | Scenario | Type | File |
|---|---|---|---|
| I-01 | Parser extracts 1 line item from Sample_PO.xlsx | Unit | po-import-parser.test.ts |
| I-02 | T&C rows not included in line items | Unit | po-import-parser.test.ts |
| I-03 | Vendor name, PI quotation, place of delivery extracted | Unit | po-import-parser.test.ts |
| I-04 | GST rate > 1 normalised to fraction | Unit | po-import-parser.test.ts |
| I-05 | INSTRUCTIONS TO VENDORS row stops parsing | Unit | po-import-parser.test.ts |
| I-06 | TECHNICAL / ACCOUNTS denied (403) | Integration | import-api.test.ts |
| I-07 | Unauthenticated denied (401) | Integration | import-api.test.ts |
| I-08 | No file → 400 | Integration | import-api.test.ts |
| I-09 | Invalid binary → 400 | Integration | import-api.test.ts |
| I-10 | MANAGER receives parsed results (200) | Integration | import-api.test.ts |
| I-11 | Correct line item values in API response | Integration | import-api.test.ts |
F-06: Product Fuzzy Search
| ID | Scenario | Type | File |
|---|---|---|---|
| P-01 | Unauthenticated → 401 | Integration | products-search.test.ts |
| P-02 | Query < 2 chars → empty array | Integration | products-search.test.ts |
| P-03 | Search by name substring | Integration | products-search.test.ts |
| P-04 | Search by product code | Integration | products-search.test.ts |
| P-05 | Search by description text | Integration | products-search.test.ts |
| P-06 | Case-insensitive matching | Integration | products-search.test.ts |
| P-07 | Max 10 results returned | Integration | products-search.test.ts |
| P-08 | lastPrice serialised as number not Prisma Decimal |
Integration | products-search.test.ts |
| P-09 | Inactive products excluded | Integration | products-search.test.ts |
7. Known Gaps & Out-of-Scope Items
Currently untested (acceptable gaps)
| Area | Reason |
|---|---|
| File upload to S3 / storage | Requires live AWS credentials; tested manually in staging |
| Email notification content | notify() is mocked; email body format tested via review |
| PDF/XLSX export content | Snapshot-tested manually; E2E checks endpoint responds |
| Receipt confirmation workflow | Happy path covered in E2E; integration test pending |
| Admin CRUD (users, vessels, accounts, products) | Standard CRUD; covered by E2E smoke tests |
Out of scope
- Performance / load testing
- Accessibility (a11y) automated checks
- Cross-browser testing (Chromium only)
- Mobile viewport testing
8. Running the Tests
# All unit tests (fast, no DB needed)
pnpm test
# Unit tests in watch mode during development
pnpm test:watch
# Integration tests (requires seeded DB)
pnpm test:integration
# All unit + integration
pnpm test:all
# E2E tests (requires running dev server)
pnpm test:e2e
# E2E with interactive Playwright UI
pnpm test:e2e:ui
Pre-requisites for integration tests
- A PostgreSQL instance running and
.envpointing to it (DATABASE_URL). - Schema applied:
npx prisma migrate deploy(ornpx prisma db pushin dev). - Data seeded:
npx tsx prisma/seed.ts.
CI behaviour
Integration tests and E2E tests run on every PR. E2E tests retry twice on failure (playwright.config.ts). The test:all script is used for pre-merge validation.
9. Test Authorship Conventions
- Naming:
describeblocks map to feature scenarios (e.g.,"S-02 — save as draft").itblocks describe the outcome, not the action. - Prefix isolation: Every integration test uses a
PREFIXconstant (e.g.,"INTTEST_DISCARD_") and cleans up withafterEach(() => deletePosByTitle(PREFIX)). - No test interdependence: Each test creates its own data. Tests must pass in isolation and in any order.
- Negative tests first: Each describe block should include at least one negative (denial/error) case before or after the happy path.
- Avoid
any: Type assertions in tests should useas { id: string }or similar narrow casts, notas any.