Testing Summary
Rolling snapshot of every automated test running in this workspace. Rebuild this doc (or refresh the tables) whenever test counts shift meaningfully. For feature scope see features.md; for release-level history see changelog.md.
Last updated: 2026-04-22
Totals at a Glance
| Repo | Framework | Files | Tests | Passing | Skipped | Failing | Wall time (local) |
|---|---|---|---|---|---|---|---|
booking-api |
Jest (unit + integration) | 106 | 1145 | 1143 | 2 | 0 | ~5 s |
booking-api |
Jest (e2e, real DB + Redis) | — | 63 | 61 | 0 | 2 † | ~45 s |
booking-web |
Vitest (unit + component) | 14 | 112 | 112 | 0 | 0 | ~4 s |
booking-web |
Playwright (e2e, smoke + regression) | 4 | 25 | 24 | 0 | 1 fixme | ~34 s |
booking-web |
Playwright (@integration, live PSP) |
1 | 1 | — | 1 ‡ | — | — |
booking-mobile |
— | 0 | 0 | 0 | 0 | 0 | — |
| Total (repo-wide, non-integration) | 124 | 1338 | 1334 | 2 | 2 | — |
† customer-auth.e2e-spec.ts: 2 legacy scenarios around email-dedup flaking on a pre-existing seed interaction. Tracked separately, not caused by the current tranche.
‡ payment-settings-integration.spec.ts: hits live Bambora sandbox, gated by E2E_BAMBORA_* env vars, wrapped in test.skip() locally by default.
Commands
Run from each repo's own root (all tooling is yarn-only).
# booking-api — Jest
yarn test # unit + integration, ~5s
yarn test:e2e # e2e against docker-compose DB + Redis
yarn test:cov # coverage report
# booking-web — Vitest + Playwright
yarn test # vitest (unit/component), ~4s
yarn test:watch # vitest watch mode
yarn test:e2e # playwright, excludes @integration
yarn test:e2e:integration # playwright, only @integration (needs live PSP)
yarn test:e2e:ui # playwright UI mode for debugging
booking-api — Jest
Unit + integration (yarn test)
Coverage is broken down by bounded context / module. Counts below are grouped from suite manifests; see src/core/**/*.spec.ts for the live source.
| Area | Suites | Tests | Notes |
|---|---|---|---|
| Auth (login, refresh, guards, password reset) | 12 | ~38 | Token versioning, tenant isolation, rate-limit guard |
| Tenant + Onboarding | 8 | ~26 | Settings merge, slug uniqueness, industry resolution |
| Resource management | 9 | ~32 | Schedule, time-off, skill assignment, soft-delete |
| Service catalog | 7 | ~26 | Categories, VAT, active-flag, skill links |
| Booking engine (incl. multi-service) | 14 | ~50 | Conflict detection, cross-item availability, snapshot on create |
| Availability calculator | 5 | ~20 | Business hours, buffer, time-off overlap, weekday edge cases |
| Public booking API | 6 | ~25 | Slug resolution, guest path, resource filter, skill scoping |
| TenantCustomer (saved customer profile) | 4 | ~12 | Dedup by phone/email, per-tenant scope |
| Loyalty | 8 | ~28 | Discount compute, redemption ledger, idempotency |
| Loyalty Track L2–L3 (DDD cut) | 4 | 58 | Pure helper + DDD reservations + backfill guards |
| Payments — domain + app (Track A–C) | 18 | ~280 | State machine, intent creation, outbox, refund/capture/void |
| Payments — provider adapters | 12 | ~155 | Bambora Classic (adapter, MD5, mapper, http-client, retry), Worldline kept for future |
| Payments — expiry cron + metrics | 5 | ~25 | 15-min sweep job, findExpirable, Prometheus labels |
Shared primitives (src/shared/) |
6 | ~30 | Clock, UUIDv7, event-bus, outbox port + Prisma impl |
| Infrastructure (Prisma adapter, logger, etc.) | 4 | ~25 | PrismaService constructor DI, request-id interceptor |
| Total | ~106 | ~1145 | 2 skipped suites reserved for future PSP wiring |
Keep coverage ≥ 80 % on any module touched by a PR; use yarn test:cov locally to verify.
End-to-end (yarn test:e2e)
Boots the Nest app against a clean Postgres schema and Redis instance from docker-compose.yml, seeds fixtures per spec, asserts HTTP responses.
| Spec | Scenarios | Passing | Purpose |
|---|---|---|---|
booking.e2e-spec.ts |
12 | 12 | Admin booking CRUD + status transitions |
public-booking.e2e-spec.ts |
18 | 18 | Guest flow, deposit branch, skill enforcement, conflict |
customer-auth.e2e-spec.ts |
9 | 7 † | Google OAuth callback, token version, profile update |
payment.e2e-spec.ts |
14 | 14 | Initiate → capture / refund / void via mock provider |
outbox.e2e-spec.ts |
6 | 6 | Event publishing, retry, dead-letter after max attempts |
tenant.e2e-spec.ts |
4 | 4 | Settings PATCH, industry change guards |
| Total | 63 | 61 |
† The two currently failing scenarios are email-dedup edge cases exposed after the customer-auth cleanup commit — unrelated to current tranche, tracked for a follow-up fix.
booking-web — Vitest
Unit + component tests run with jsdom + @testing-library/react. Colocated under src/**/*.test.tsx.
| File | Tests | Concern |
|---|---|---|
lib/timezone.test.ts |
~16 | Salon-tz formatters, DST edges, month/day boundaries |
lib/booking-display.test.ts |
~12 | Booking detail formatting, status labels |
lib/formatters.test.ts |
(in domain files) | Duration + phone normalisation |
lib/payment/bambora-schema.test.ts |
17 | Zod validation for Bambora Classic credentials (T/P prefix → test/prod) |
lib/payment/deposit-calc.test.ts |
~8 | Percentage vs fixed, zero-price guard, rounding |
lib/payment/provider-metadata.test.ts |
4 | Provider label lookup |
lib/payment/public-payment-api.test.ts |
~6 | redirectToCheckout polling, timeout, failure mapping |
components/settings/payment/BamboraConfigForm.test.tsx |
5 | Merchant-mode badge, required fields |
components/settings/payment/ProviderCard.test.tsx |
7 | Active / inactive / Coming-soon states |
components/settings/payment/HealthCheckBadge.test.tsx |
3 | Healthy / degraded / unknown |
components/payments/refund-schema.test.ts |
8 | Refund amount bounds, partial vs full |
components/payments/capture-schema.test.ts |
5 | Capture ≤ authorized amount |
components/payments/collect-remaining-schema.test.ts |
7 | Remaining > 0, ≤ max, integer minor-units |
hooks/usePayments.test.tsx |
2 | Query terminal-status polling stop |
hooks/usePaymentConfigs.test.tsx |
8 | Active config resolution + invalidation |
| Total | 112 |
booking-web — Playwright
Headless Chromium by default (playwright.config.ts). Serial execution (workers: 1, fullyParallel: false) because every setting-mutation test PATCHes the shared studio-nordic tenant; restoring in finally keeps state clean.
ownerApi / staffApi fixtures are worker-scoped; loggedInPage / loggedInAsStaffPage load pre-saved storageState (login once per worker per role) so the auth endpoint is hit a handful of times per run — avoids tripping the 10-req/min login rate-limiter on long serial suites.
Smoke (admin)
| Spec | Scenarios | What it covers |
|---|---|---|
payment-settings.spec.ts |
3 | Provider list renders, Vipps disabled, Bambora drawer opens |
payments.spec.ts |
2 | Payments index renders + shows in sidebar |
staff-role.spec.ts |
7 | STAFF signin + sidebar filter (4 visible / 4 hidden) + OwnerOnlyGuard bounces (/admin/staff, /admin/settings, /admin/services) + /admin/bookings accessible + OWNER→STAFF role switch preserves tenantId |
Regression — public booking flow (public-booking.spec.ts)
13 tests, organised in 4 tranches.
Tranche 0 — DateStrip clamp by maxBookingDaysInAdvance
| # | Scenario |
|---|---|
| 0.1 | Cap ≥ 28 → strip saturates at exactly 28 date cells |
| 0.2 | Cap = 5 → strip renders exactly 6 cells (today through today+5) |
Tranche 1 — Service & staff selection
| # | Scenario |
|---|---|
| 1.1 | Happy path: 1 service → confirmed booking (deposit off) |
| 1.2 | Multi-service: 2 services → totals aggregate in summary |
| 1.3 | Unassigned booking (bookingMode=allow_unassigned) → succeeds without staff — test.fixme pending API fix: first slot at 09:00 lands on BOOKING_OUTSIDE_BUSINESS_HOURS on the unassigned path only (1.1 same slot + assigned staff passes) |
| 1.4 | Assigned-only: submit without staff → inline error on the staff field |
Tranche 2 — Settings enforcement
| # | Scenario |
|---|---|
| 2.3 | Closed day (Sunday in seed) renders as disabled cell in DateStrip |
| 2.4 | Time slots are bounded by business hours (no post-21:00 leakage) |
| 2.5 | depositEnabled=true → deposit notice visible + submit redirects to PSP (Bambora checkout URL) |
Tranche 3 — Validation & error states
| # | Scenario |
|---|---|
| 3.1 | Submit with blank customerName → inline error, no redirect |
| 3.2 | Staff dropdown count matches /resources?serviceId=… API (skill filter correctness) |
| 3.3 | Invalid serviceId → "no-service-selected" state, booking form does not mount |
Tranche 4 — Book again
| # | Scenario |
|---|---|
| 4.1 | ?from=<bookingId> pre-fills the service item + notes field from a seeded booking |
data-testid catalogue (added for the tranche above)
| TestID | File | Used by |
|---|---|---|
booking-form |
BookingPage.tsx |
Reach the form root; assert it didn't mount on invalid deep-link |
booking-submit |
BookingPage.tsx |
Click to submit |
booking-error |
BookingPage.tsx |
Top-level submit banner |
booking-summary |
BookingPage.tsx |
Side panel — tests assert totals appear here |
booking-success |
BookingPage.tsx |
Renders on requiresPayment=false; data-status attribute mirrors booking status |
deposit-notice |
BookingPage.tsx |
Amber banner; data-deposit-amount for numeric assertion |
no-service-selected |
BookingPage.tsx |
Empty-state when deep-link resolves to zero items |
service-item |
ServiceItem.tsx |
Per-service row (data-service-id on the same node) |
service-item-remove |
ServiceItem.tsx |
Remove button (only when canRemove=true) |
service-item-staff |
ServiceItem.tsx |
Staff SearchSelect wrapper (click to open, scope option queries inside) |
service-item-time |
ServiceItem.tsx |
Time-slot SearchSelect wrapper |
service-picker-add |
ServicePicker.tsx |
Add-service CTA |
service-picker-search |
ServicePicker.tsx |
Expanded search mode of the picker |
date-strip |
DateStrip.tsx |
Scrollable day container |
date-strip-day |
DateStrip.tsx |
Each day button; data-date=YYYY-MM-DD attribute |
booking-mobile
No automated tests yet. When testing starts, this section will mirror the web layout.
Conventions
- TDD is mandatory — write failing test first, then implement. See development-rules.md.
- E2E tests always restore settings in a
finallyblock — thestudio-nordictenant is shared between specs and serial execution depends on a known state. - Worker-scoped fixtures for auth — prevents rate-limiter false positives when running > 5 tests in one worker.
- Scope dropdown locators to the SearchSelect wrapper (via
data-testid) — the dropdown is rendered as a fixed-position<div>inside the component root, sopage.locator('li')without a scope will leak into unrelated lists on the page. - Playwright tests create real bookings on
studio-nordic— intentional so a developer can spot-check them in admin after a run. Leave a discoverable marker incustomerName/notes(e.g.E2E Happy Path,E2E rebook seed <ts>) so the run attribution is obvious.
Known Gaps (targets for the next tranche)
- Auth flows: no Playwright coverage of customer login / Google OAuth, admin login, or forgot-password round-trip.
- Admin calendar: drag-and-drop, conflict warning, booking drawer — still manual-test only.
- Loyalty: unit coverage is good, but no E2E of the redeem-on-checkout flow.
- Customer portal: booking list pagination + "my bookings" cancel flow have no tests.
- Settings: only the Payments tab is touched by E2E.
- Mobile app: zero automated tests (see above).
These are tracked in features.md under each corresponding module.