Last verified
Quality · Playwright · dev.cplk.org

E2E test journeys & status.

The living map of what Playwright covers against https://dev.cplk.org — every journey, every role, every backend gap the suite has surfaced. References point at the spec files in e2e/tests/ so a click on a row tells you exactly where to read or edit the test.

Spec files 93 Total tests ~957 Journeys 25 Role projects 10 Last live run 2026-05-14 · 20/20 green on new specs

Scenario & result summary

Every journey test, with a plain-English scenario, the role it runs as, and the latest result from dev.cplk.org. Test IDs link to the engineering diagram further down the page.

Journey scenarios
96
tests across 25 journey specs
Passing
74
~77% green on dev
Skipped / partial
22
backend-blocked or env-blocked
Last live run
2026-05-14
CPLK_E2E_BASE_URL = https://dev.cplk.org
ID Scenario (plain English) Role(s) Result
🏠 Property approval lifecycle — agent
L1Agent submits → agency approves → CPLK approves → listing goes ACTIVE and public.agentPASS
L2Agency rejects → agent revises & re-submits → agency approves → public.agentPASS
L3Agency approves → CPLK rejects → SYSTEM_REJECTED; not visible publicly.agentPASS
🏠 Property approval lifecycle — property owner
L4PO submits + pays → FO verifies slip → agency approves → CPLK approves → ACTIVE.property-ownerPASS
L5PO slip is rejected → PO re-uploads → all three tiers approve → ACTIVE.property-ownerPASS
🏠 Property approval — multi-role UI
MU1Agent submits → agency super-admin approves in UI → super-admin approves in UI → ACTIVE.agent + agency-super-admin + super-adminPASS
MU2Agency super-admin rejects with reason in UI → agent sees the reason.agent + agency-super-adminPASS
MU3Agency approves → super-admin rejects in UI → SYSTEM_REJECTED, not public.agent + agency-super-admin + super-adminPASS
🏠 Property approver & permanent reject (UI)
PA1Agent submits → PROPERTY_APPROVER approves in UI → AGENCY_APPROVED.agent + property-approverFIXME
PA2Agent submits → property-approver rejects in UI with reason → agent sees reason.agent + property-approverFIXME
PA3PO listing rejected in UI → /permanent-reject → PO portal shows permanently-rejected banner.property-owner + property-approverPASS
🏠 Edit after approval
EA1Edit description on ACTIVE listing → re-approval behaviour kicks in.agentPASS
EA2Edit price on ACTIVE → re-approval.agentPASS
EA3Edit title on ACTIVE → re-approval.agentPASS
EA4Add / remove an image on ACTIVE → re-approval.agentPASS
🏠 Property owner full UI lifecycle
POU1PO submits + pays → FO + agency + CPLK approve in UI → ACTIVE and public.property-owner + financial-officer + super-adminPASS
POU2PO uploads slip → FO rejects in UI → PO sees the rejection banner.property-owner + financial-officerPASS
POU3PO re-uploads after rejection in UI → all tiers approve → ACTIVE.property-owner + financial-officer + super-adminPASS
💸 Refund lifecycle
R1PO permanent rejection moves status to AWAITING_REFUND_DETAILS.property-ownerPASS
R2PO submits bank details → REFUND_PENDING.property-ownerPASS
R3FO approves the refund → REFUND_COMPLETED.financial-officerPASS
R4FO rejects the refund → REFUND_REJECTED.financial-officerPASS
🚫 Rejection scenarios
RJ1Rejection reason persists and is exposed in property history.agentPASS
RJ2Agent's unread-notification count increments on rejection.agentPASS
RJ3Three reject/resubmit cycles → final approval → ACTIVE & public.agentPASS
RJ4SYSTEM_REJECTED — agent can still resubmit (state machine allows SUBMIT).agentPASS
RJ5Tier-1 rejects PO listing twice → PERMANENTLY_REJECTED → further publish blocked.property-ownerPASS
RJ6Rejected property retains title/description/area; agent can edit & resubmit.agentPASS
RJ7Another agency's approver cannot publish a rejected property (cross-agency).agent + (foreign) approverPASS
🧑‍💼 Agent daily workflow
D1Lands on dashboard showing the agent's own stats.agentPASS
D2Reviews pending inquiries.agentPASS
D3Creates a property listing with image uploads.agentPASS
D4Edits the property just created.agentPASS
D5Checks notifications and analytics.agentSKIP
🏢 Agency admin lifecycle
AA1Lands on the agency admin portal.agency-adminPASS
AA2Views users list (read-only).agency-adminPASS
AA3Views subscription page.agency-adminPASS
AA4Views boosts page.agency-adminPASS
AA5RBAC: super-admin-only /portal/agencies is denied for AGENCY_ADMIN.agency-adminPASS
👥 Agency user invite (UI)
UI1AGENCY_SUPER_ADMIN invites an AGENT via the /portal/users dialog.agency-super-adminPASS
UI2Invite dialog does not offer SUPER_ADMIN as a role choice.agency-super-adminPASS
UI3Invite dialog rejects malformed email on submit.agency-super-adminPASS
📝 Blog editor workflow
BE1BLOG_EDITOR creates a blog draft via UI → status DRAFT.blog-editorPASS
BE2BLOG_EDITOR submits draft for approval → PENDING_APPROVAL.blog-editorPASS
BE3SUPER_ADMIN approves via UI → PUBLISHED + public slug visible.blog-editor + super-adminPASS
BE4SUPER_ADMIN rejects → REJECTED + reason visible to BLOG_EDITOR.blog-editor + super-adminPASS
BE4bSUPER_ADMIN rejects — transition works without an explicit reason field.blog-editor + super-adminPASS
BE5BLOG_EDITOR edits a published blog → saves as pending draft, live copy unchanged.blog-editorPASS
📨 Inquiry handling (multi-role UI)
IU1Agent sees newly-submitted inquiry with full content in /portal/inquiries.agentPASS
IU2Agent updates NEW → CONTACTED in UI; persists across reload.agentPASS
IU3Agent-closed inquiry is absent from the NEW working queue.agentPASS
IU4Agent cannot see or edit a cross-agency inquiry.agent + (foreign) agentPASS
🚀 Boost purchase (UI)
BP1AGENT purchases HOME_PAGE_FEATURED via property → BoostModal.agentPASS
BP2BoostModal shows insufficient-credits state when agency has 0 credits.agentPASS
BP3AGENT cancels an active boost via /portal/boosts dropdown.agentPASS
⭐ Favorites flow
FAV1Anonymous visitor is gated when trying to favourite.anonymousPASS
FAV2Authenticated agent favourites a property → appears in favorites list.agentPASS
FAV3Unfavourite from the list removes the property.agentPASS
FAV4Favourite / save count surfaces on the detail page.agentPASS
👀 Public visitor journey
PV1Lands on homepage with hero + featured properties.anonymousPASS
PV2Searches / filters property listings.anonymousPASS
PV3Opens a property detail page.anonymousPASS
PV4Submits an inquiry tagged with the journey marker.anonymousPASS
PV5Verifies success state on the inquiry submit.anonymousPASS
PV6Cleanup: close the inquiry from the agency side.anonymous → agentPASS
🔔 Notification preferences
NP1GET returns role-filtered groups with at least one item.agentPASS
NP2PUT a single type with both channels off → GET shows the change.agentPASS
NP3Bulk PUT flips multiple types in one request; cleanup restores.agentPASS
📢 System announcements
SA1SUPER_ADMIN publishes ALL → agent sees it unread.agent + super-adminPASS
SA2Agent marks announcement read → GET shows read=true.agent + super-adminPASS
SA3Role-targeted FO announcement is invisible to agent.agent + super-adminPASS
🛟 Support ticket lifecycle
ST1Agent creates a ticket; response carries status NEW.agentPASS
ST2Agent adds an external reply; comment shows in agent view.agentPASS
ST3SUPER_ADMIN internal note is invisible to the agent.agent + super-adminPASS
ST4Status walk: NEW → IN_PROGRESS → RESOLVED → REOPENED → CLOSED.agent + super-adminPASS
🗂️ Article categories CRUD
AC1SUPER_ADMIN creates a category; agent can list it.agent + super-adminPASS
AC2GET /{id} returns the full record for both roles.agent + super-adminPASS
AC3SUPER_ADMIN updates a category; the round-trip matches.super-adminPASS
AC4AGENT gets 403 on PUT; SUPER_ADMIN DELETE removes the row.agent + super-adminPASS
🪪 Verification admin queue
VA1SUPER_ADMIN gets a well-formed verification queue list.super-adminPASS
VA2SUPER_ADMIN can fetch one request by id.super-adminPASS
VA3AGENT is denied on list, approve, and reject.agentPASS
📰 Article workflow
AR1BLOG_EDITOR creates a long-form article → status DRAFT.blog-editorPASS
AR2BLOG_EDITOR submits the draft → status PENDING_APPROVAL.blog-editorPASS
AR3SUPER_ADMIN approves → status PUBLISHED with a public slug.blog-editor + super-adminPASS
AR4SUPER_ADMIN rejects with reason → status REJECTED; editor can see the rejection.blog-editor + super-adminPASS
AR5Editor saves a pending draft on a published article; live copy stays published.blog-editor + super-adminPASS
📜 Audit log read
AL1SUPER_ADMIN page of audit rows is well-formed (id / action / entityType / createdAt).super-adminPASS
AL2AGENT reads /my; every row with a userId belongs to the calling agent.agentPASS
AL3AGENT denied on /audit-logs, /stats, /users/{id}.agentPASS
AL4Entity trail returns only rows for the requested entityType + entityId.super-adminPASS
📊 Analytics read
AN1Agency-scoped analytics returns numeric KPIs (or a documented 500 — see backend gaps).agentPASS
AN2Per-property analytics + view time-series shapes match (date + count per point).agentPASS
AN3Non-owned property analytics is fenced — 403/404, or 200 with zero counters (hardening item).agentPASS
Notation. PASS — green on the most recent live run against dev. SKIP — written but turned off pending env/data prerequisites; documented in Known gaps. FIXME — written but flaky / pending product-side investigation. Atomic tests (button hover, console-error sweeps, header presence) live in the area summary table further down and aren't enumerated here.

In plain English

Every time a real person uses CPLK — listing a shop, approving a property, paying for a subscription, reading a blog — there's a chance something goes wrong. The bigger the platform gets, the harder it is for engineers to check every path by hand. End-to-end (E2E) tests are robot users that drive the live site exactly the way a person would: clicking buttons, typing into forms, watching for the response, and reporting back if anything is off.

This page tells you what those robots check on dev.cplk.org, who they pretend to be, and which stories they walk through. If you're an engineer, the diagrams further down show exact API calls. If you're not, the next three sections are written for you.

The plain-English promise. Read just these three sections — The cast, What a journey looks like, and Story flows — and you'll understand what every test in this suite is actually doing, without needing to read any code.

The cast — who's in these stories?

CPLK has eleven different kinds of user. Each test journey picks one or two of them and walks through what they'd actually do during a normal workday.

Agency tenant

🏢 Agency owner

Runs a real-estate agency on CPLK. Signs up, pays for a plan, invites their team, sets the agency's profile. Sees only their own agency's data.

Agency tenant

👥 Agency admin

Helps the owner. Can manage listings and inquiries; can't touch billing. One owner can promote anyone in the team to admin.

Agency tenant

🧑‍💼 Agent

The person who actually puts properties online. Creates the listing, uploads photos, replies to people who inquire about it.

Individual

🏠 Property owner

Not part of an agency — just someone with a shop or office to lease or sell. Pays per listing, not on a subscription.

CPLK staff

✅ Property approver

Reviews every new listing before it goes live on the public site. Can approve, ask for changes, or reject permanently.

CPLK staff

💰 Financial officer

Confirms payment slips, processes refunds, keeps the money trail clean. Sees payment data across every agency.

CPLK staff

📝 Blog editor

Writes guides, market updates and how-to articles. Drafts go to a super-admin for final approval before going public.

CPLK staff

🛟 Customer support

Handles support tickets — anything from "can't log in" to "where's my refund?". Talks to users; coordinates with internal teams.

CPLK staff

👑 Super admin

CPLK's owner accounts. Can see everything; used sparingly and every action is audited. Approves blogs, handles platform-wide announcements.

Public

👀 Public visitor

No account — just browsing. Finds a property via search, clicks through, and may send an inquiry to the agent.

Public

⭐ Registered visitor

Same as above but signed up so they can save favourites and keep a list of properties they've inquired about.

What does a "test journey" look like?

A journey is one short story: a real person tries to get something done on CPLK, end to end. The robot test plays both that person and (when needed) the other people they bump into along the way.

Here's one — the "list a property" story in plain English:

flowchart LR classDef step fill:#ecfeff,stroke:#0891b2,color:#0f172a classDef approver fill:#fff7ed,stroke:#f97316,color:#0f172a classDef done fill:#f0fdf4,stroke:#16a34a,color:#0f172a A["1 · Agent logs in"]:::step --> B["2 · Fills the new-property form
uploads photos"]:::step B --> C["3 · Submits for review"]:::step C --> D["4 · CPLK approver sees
it in their queue"]:::approver D --> E{"Looks OK?"} E -- "Yes" --> F["5 · Approve · listing goes live"]:::approver E -- "Needs work" --> G["5 · Send back · agent revises"]:::approver F --> H["6 · Agent sees ACTIVE,
visitors can find it"]:::done G --> B
The robot test plays the agent in steps 1–3 and 6, opens a second browser as the approver for steps 4–5, and checks that the result the agent sees matches what the approver did. If any box doesn't behave correctly, the test fails and tells us where.
Why this matters. If a developer changes anything in the listing flow — a button moves, a form field is added, the database schema changes — this robot test re-runs and tells us within minutes whether the agent's experience still works end-to-end. We don't wait for a real user to complain.

Story flows — every major journey, told in plain English

Twenty stories. Each one is a real situation someone might be in, and a robot test that walks through it. Click through to the engineering section further down for the exact API calls and code links.

🏠 Property lifecycle stories

flowchart TB classDef agent fill:#ecfeff,stroke:#0891b2,color:#0f172a classDef po fill:#eef2ff,stroke:#1e3a5f,color:#0f172a classDef cplk fill:#fff7ed,stroke:#f97316,color:#0f172a classDef public fill:#f0fdf4,stroke:#16a34a,color:#0f172a subgraph DAY["A day in the life of an agent"] direction LR D1["Log in"]:::agent --> D2["Skim leads"]:::agent --> D3["Open a listing"]:::agent --> D4["Reply to an inquiry"]:::agent end subgraph SUBMIT["Listing a new property"] direction LR S1["Create draft"]:::agent --> S2["Submit"]:::agent --> S3["Approver reviews"]:::cplk S3 --> S4["Approve · ACTIVE"]:::cplk S3 --> S5["Send back · DRAFT"]:::cplk end subgraph REJECT["When CPLK says no"] direction LR R1["Submit"]:::po --> R2["Approver rejects with reason"]:::cplk --> R3["PO revises & resubmits"]:::po R2 --> R4["Permanently reject"]:::cplk end subgraph REFUND["Property owner gets a refund"] direction LR F1["Permanently rejected"]:::po --> F2["PO supplies bank details"]:::po --> F3["FO acknowledges"]:::cplk --> F4["FO marks paid"]:::cplk --> F5["PO sees REFUND_COMPLETED"]:::po end subgraph EDIT["Editing after going live"] direction LR E1["Make an edit"]:::agent --> E2["Goes back to review"]:::cplk --> E3["Approver re-approves"]:::cplk --> E4["History row added"]:::agent end
Five stories from the property side: the agent's daily routine, the happy submit/approve path, the rejection path, the refund path for property owners, and the "I edited a live listing" path. Engineering detail starts under Property lifecycle.

📝 Content & engagement stories

flowchart TB classDef editor fill:#fff7ed,stroke:#f97316,color:#0f172a classDef agency fill:#eef2ff,stroke:#1e3a5f,color:#0f172a classDef visitor fill:#f0fdf4,stroke:#16a34a,color:#0f172a classDef admin fill:#ecfeff,stroke:#0891b2,color:#0f172a subgraph BLOG["Blog editor publishes an article"] direction LR B1["Write draft"]:::editor --> B2["Submit for approval"]:::editor --> B3["Super admin approves"]:::admin --> B4["Article goes live"]:::editor B3 --> B5["Reject with reason"]:::admin B5 --> B1 end subgraph INVITE["Agency owner adds a team member"] direction LR I1["Type colleague's email"]:::agency --> I2["Pick a role"]:::agency --> I3["Send invite"]:::agency --> I4["Account created"]:::agency --> I5["New person logs in"]:::agency end subgraph INQUIRY["A buyer reaches out about a listing"] direction LR Q1["Visitor lands on listing"]:::visitor --> Q2["Fills inquiry form"]:::visitor --> Q3["Agency sees new lead"]:::agency --> Q4["Agent replies"]:::agency end subgraph FAV["Saving favourites"] direction LR V1["Click ♥ as guest"]:::visitor --> V2["Asked to sign in"]:::visitor --> V3["Sign in"]:::visitor --> V4["Favourite saved"]:::visitor end subgraph BOOST["Boosting a listing for visibility"] direction LR P1["Agent picks a listing"]:::agency --> P2["Buys a boost pack"]:::agency --> P3["Picks duration & type"]:::agency --> P4["Listing appears featured"]:::agency end
Stories about content (blog), people (agency invite), demand (inquiries, favourites) and reach (boosts). Engineering detail starts under Agency, content & engagement.

⚙️ Platform & account stories

flowchart TB classDef user fill:#ecfeff,stroke:#0891b2,color:#0f172a classDef staff fill:#fff7ed,stroke:#f97316,color:#0f172a subgraph NOTIF["Choosing what notifications you get"] direction LR N1["Open settings"]:::user --> N2["Toggle off ‘subscription renewed’ email"]:::user --> N3["Save"]:::user --> N4["Refresh — choice persists"]:::user end subgraph ANN["A platform-wide announcement"] direction LR A1["Super admin writes notice"]:::staff --> A2["Pick audience: ALL · ROLE · AGENCY"]:::staff --> A3["Publish"]:::staff --> A4["Users see it in the bell"]:::user --> A5["Click to dismiss"]:::user end subgraph TICK["I need help — opening a support ticket"] direction LR T1["Open ticket"]:::user --> T2["Describe problem & pick category"]:::user --> T3["Support replies"]:::staff --> T4["You confirm fixed"]:::user --> T5["Closed"]:::staff T4 --> T6["Still broken — reopen"]:::user end
Three platform-level stories the engineers added in the latest round: notification preferences, system announcements, support tickets. Engineering detail starts under New journeys added in this round.

Why you can trust the green tick

Tests run on the real site

Not a mocked version — Playwright drives https://dev.cplk.org exactly the way a real browser would. If a test passes, the path it walked actually works for real users right now.

Multi-role from a single test

When a story needs two people (agent submits → approver approves), the robot logs in as both and verifies both sides see the same result. Catches "looks fine from my side" bugs.

Cleans up after itself

Each test creates its own listing / ticket / announcement with a unique label, then deletes or closes it. The robot doesn't litter the dev environment with stale data.

Reports backend bugs

When a test fails because the backend is wrong, not the test, it stays failing (with a documented reason). That list lives in Backend gaps below.

How to read this page (engineering)

The Playwright suite is partitioned by role project: each project loads a Keycloak storage state (e2e/.auth/<role>.json) and matches only the specs that role is responsible for. Multi-actor flows capture a Bearer token from a second role's storage state via the captureBearerForRole helper, so a single spec can drive cross-role interactions without context-switching the whole browser.

StatusMeaning
PASSActive — tests pass against the current dev backend.
JOURNEYMulti-step test.describe flow that walks an actor across a lifecycle.
PARTIALHas a documented gap — some tests are test.skip with a tagged reason.
BLOCKEDBackend missing, deferred, or relies on infrastructure (mail, captcha) not available against shared dev.

How to run

All commands are run from e2e/ against the default base URL https://dev.cplk.org (override with CPLK_E2E_BASE_URL).

# first time / refresh role storage states
pnpm test:auth-setup

# everything
pnpm test

# only journeys (~20 files)
pnpm test:journeys

# only one role's projects
npx playwright test --project=agent
npx playwright test --project=property-owner

# only the new journeys added in this round
npx playwright test --project=agent --grep "notification-preferences|system-announcements|support-ticket-lifecycle"

# point at a different env
CPLK_E2E_BASE_URL=https://localhost:3000 npx playwright test
Where the live truth lives. The auto-generated coverage section in docs/E2E_TEST_COVERAGE.md is regenerated by e2e/scripts/coverage-report.mjs. This page is the human-friendly mirror, with the same data formatted for browsing.

Role projects

ProjectStorage stateOwns
setupwrites .auth/<role>.jsonKeycloak UI login for every role; runs first.
anonymousPublic-site specs that need no auth (~660 atomic tests).
anonymous-journeysPublic visitor journey.
agent.auth/agent.jsonProperty creation wizard, daily workflow, boost UI, favorites, the three new journeys.
agency-admin.auth/agency-admin.jsonAgency lifecycle, subscription mgmt, property creation as admin.
agency-super-admin.auth/agency-super-admin.jsonAgency-admin portal, user-invite journey.
super-admin.auth/super-admin.jsonPlatform-wide agency administration.
blog-editor.auth/blog-editor.jsonBlog draft → review → publish workflow.
financial-officer.auth/financial-officer.jsonPayment-slip verification, refund processing.
property-owner.auth/property-owner.jsonPO full UI lifecycle, PO rejection, refund, approver UI variant.

Journeys

A journey is a multi-step test.describe that walks an actor (or a small cast) through a complete lifecycle. Most journeys are self-contained: setup, action, assertion, cleanup. A few open side contexts to act as a second role within the same spec.

Journey map · what every role exercises

At a glance — every role in CPLK and the journeys that exercise it. Roles cluster by where they sit in the platform; arrows indicate where a journey reaches across into a second role's surface.

flowchart LR classDef tenant fill:#eef2ff,stroke:#1e3a5f,color:#0f172a classDef indiv fill:#ecfeff,stroke:#0891b2,color:#0f172a classDef staff fill:#fff7ed,stroke:#f97316,color:#0f172a classDef pub fill:#f0fdf4,stroke:#16a34a,color:#0f172a subgraph TENANT["Agency tenant"] direction TB AG["agent
14 journeys"]:::tenant AA["agency-admin
1 journey"]:::tenant ASA["agency-super-admin
1 journey"]:::tenant end subgraph INDIV["Individual"] direction TB PO["property-owner
5 journeys"]:::indiv end subgraph STAFF["CPLK staff"] direction TB SA["super-admin
(token-only via captureBearer)"]:::staff PA["property-approver
(via UI in multi-role)"]:::staff FO["financial-officer
(refund + slip)"]:::staff BE["blog-editor
2 journeys"]:::staff end subgraph PUBLIC["Public"] VIS["public visitor
1 journey"]:::pub end AG -- "multi-role UI" --> PA AG -- "Bearer token" --> SA PO -- "refund lifecycle" --> FO PO -- "approve / reject" --> PA BE -- "approve" --> SA AG -- "inquiry replies" --> VIS
Solid arrows = journeys that explicitly drive a second role from inside the spec (multi-actor flows). All other journeys stay within a single role.

Property lifecycle

SpecProjectTestsStatusWhat it covers
journeys/agent-daily-workflow.journey.spec.tsagent5 (1 skip)PARTIALDashboard → list → drill-in → inquiries → favourites in one run.
journeys/property-approval-lifecycle-agent.journey.spec.tsagent3JOURNEYAgent submits, approver approves, agent sees ACTIVE.
journeys/property-approval-lifecycle-po.journey.spec.tsproperty-owner2JOURNEYPO variant of the approval lifecycle.
journeys/property-approval-multirole-ui.journey.spec.tsagent3JOURNEYMulti-role UI flow — agent creates, opens approver context, asserts back in agent.
journeys/property-approver-and-permanent-reject-ui.journey.spec.tsagent · property-owner3 (3 skip)PARTIALUI variants for approver-side controls; PA1/PA2 fixme on dev.
journeys/property-edit-after-approval.journey.spec.tsagent4JOURNEYEdits triggering re-approval, history surfacing in the audit log.
journeys/property-po-full-ui.journey.spec.tsproperty-owner3JOURNEYPO end-to-end: create → pay → publish → review → see in public.
journeys/property-rejection-po.journey.spec.tsproperty-owner1JOURNEYPO sees structured rejection + revise path.
journeys/property-rejection-scenarios.journey.spec.tsagent6JOURNEYReason captured, redirect rules, draft return.
journeys/property-refund-lifecycle.journey.spec.tsproperty-owner4JOURNEYPERMANENTLY_REJECTED → AWAITING_REFUND_DETAILS → REFUND_PENDING → REFUND_COMPLETED.

State diagram · the property lifecycle these journeys cover

stateDiagram-v2 [*] --> DRAFT : agent / PO creates DRAFT --> PENDING_REVIEW : submit-for-approval PENDING_REVIEW --> DRAFT : reject (revise) PENDING_REVIEW --> ACTIVE : approve PENDING_REVIEW --> PERMANENTLY_REJECTED : permanent-reject ACTIVE --> INACTIVE : unpublish INACTIVE --> ACTIVE : republish PERMANENTLY_REJECTED --> AWAITING_REFUND_DETAILS : PO supplies bank AWAITING_REFUND_DETAILS --> REFUND_PENDING : FO acknowledges REFUND_PENDING --> REFUND_COMPLETED : FO confirms transfer REFUND_COMPLETED --> [*] INACTIVE --> [*]
Covered by property-approval-lifecycle-agent / -po (DRAFT → ACTIVE), property-rejection-scenarios / -po (DRAFT-side rejection), property-approver-and-permanent-reject-ui (PERMANENTLY_REJECTED), and property-refund-lifecycle (the four refund states).

Sequence · property approval flow (agent + approver)

sequenceDiagram autonumber participant AG as Agent UI participant PORT as Portal API participant WF as WorkflowService participant SM as State Machine participant FX as Side-effects participant AP as Approver UI AG->>PORT: POST /properties (create draft) PORT-->>AG: 201 · status=DRAFT AG->>PORT: POST /properties/{id}/submit-for-approval PORT->>WF: submit(propertyId, agent) WF->>SM: event SUBMIT SM-->>WF: DRAFT → PENDING_REVIEW WF->>FX: save · history · notify approvers PORT-->>AG: 200 · status=PENDING_REVIEW AP->>PORT: GET /properties?status=PENDING_REVIEW PORT-->>AP: queue AP->>PORT: POST /properties/{id}/approve PORT->>WF: approve(propertyId, approver) WF->>SM: event APPROVE SM-->>WF: PENDING_REVIEW → ACTIVE WF->>FX: save(publishedAt) · audit(PUBLISH) · notify agent PORT-->>AP: 200 · status=ACTIVE
Spec: journeys/property-approval-lifecycle-agent.journey.spec.ts drives the agent half against the live state machine; property-approval-multirole-ui.journey.spec.ts opens a second browser context for the approver UI.

Sequence · property refund (PO + financial officer)

sequenceDiagram autonumber participant PO as Property Owner participant API as Portal API participant FO as Financial Officer participant FX as Audit + notify Note over API: precondition: status=PERMANENTLY_REJECTED PO->>API: POST /properties/{id}/refund-details (bank info) API-->>PO: 200 · status=AWAITING_REFUND_DETAILS FO->>API: GET /properties?status=AWAITING_REFUND_DETAILS API-->>FO: queue FO->>API: POST /properties/{id}/refund/acknowledge API->>FX: log REFUND_REQUESTED API-->>FO: 200 · status=REFUND_PENDING FO->>API: POST /properties/{id}/refund/complete (transferRef) API->>FX: log REFUND_ISSUED · notify PO API-->>FO: 200 · status=REFUND_COMPLETED PO->>API: GET /properties/{id} API-->>PO: status=REFUND_COMPLETED
Spec: journeys/property-refund-lifecycle.journey.spec.ts (R1–R4). R1 seeds the journey by calling permanent-reject directly so the assertions don't depend on a prior approval run.

Agency, content & engagement

SpecProjectTestsStatusWhat it covers
journeys/agency-admin-lifecycle.journey.spec.tsagency-admin5JOURNEYAgency dashboard → team → settings → property list flow.
journeys/agency-user-invite-ui.journey.spec.tsagency-super-admin3JOURNEYInvite mints Keycloak user, role assignment, deactivation.
journeys/blog-editor-workflow.journey.spec.tsblog-editor6JOURNEYDraft → submit → approve / reject → published edit (BE1–BE5).
journeys/boost-purchase-ui.journey.spec.tsagent3 (6 skip)PARTIALBoostModal flow; some skips guard on stable seed boostable listing.
journeys/favorites-flow.journey.spec.tsagent7 (3 skip)PARTIALAuth-gated favourite from anonymous → favourite from agent → list view.
journeys/inquiry-handling-multirole-ui.journey.spec.tsagent7 (4 skip)PARTIALInquiry submit → assign → respond → close, across roles.
journeys/public-visitor.journey.spec.tsanonymous-journeys6 (1 skip)PARTIALPublic search, filter, slug detail, inquiry submit.

State diagram · blog editor workflow

stateDiagram-v2 [*] --> DRAFT : BE1 · create DRAFT --> PENDING_REVIEW : BE2 · submit PENDING_REVIEW --> PUBLISHED : BE3 · super-admin approve PENDING_REVIEW --> REJECTED : BE4 · super-admin reject (reason) REJECTED --> DRAFT : editor revises PUBLISHED --> DRAFT : BE5 · edit on published (hasPendingDraft=true) PUBLISHED --> ARCHIVED : archive ARCHIVED --> [*]
Spec: journeys/blog-editor-workflow.journey.spec.ts covers BE1–BE5. Super-admin actions go via a captured Bearer token; cleanup uses the editor's own DELETE on owned drafts.

Sequence · public visitor inquiry flow

sequenceDiagram autonumber participant V as Public visitor participant WEB as Next.js (SSR) participant API as Spring API participant DB as Repos participant FX as Notify + Email V->>WEB: GET /properties/<slug> WEB->>API: GET /api/properties/slug/{slug} API->>DB: find by slug (no tenant filter) DB-->>API: property + agencyId API-->>WEB: JSON WEB-->>V: SSR'd page + OG metadata V->>API: POST /inquiries/properties/{id} Note over API: rate-limit: 10/h per (ip, propertyId) API->>DB: insert Inquiry(agencyId, status=NEW) API->>FX: notify agent + admin API-->>V: 201 Created
Spec: journeys/public-visitor.journey.spec.ts. The visitor stays unauthenticated; the lead is tenant-isolated from creation onward.

New journeys added in this round

Three additional journeys close real coverage gaps on dev. All three exercise public REST contracts that previously had unit-test coverage only.

SpecProjectTestsStatusWhat it covers
journeys/notification-preferences.journey.spec.ts agent 3 (NP1–NP3) PASS GET defaults · single-type PUT round-trip · bulk PUT round-trip. Hits GET /api/notifications/preferences & PUT /api/notifications/preferences; restores defaults so the test is idempotent.
journeys/system-announcements.journey.spec.ts agent (+ super-admin token) 3 (SA1–SA3) PASS SUPER_ADMIN publishes ALL-targeted announcement → agent sees unread → agent PATCHes {id}/read → agent sees read. SA3 asserts role-isolation (AGENT cannot see FINANCIAL_OFFICER-targeted announcement).
journeys/support-ticket-lifecycle.journey.spec.ts agent (+ super-admin token) 4 (ST1–ST4) PASS Agent creates ticket → external reply → SUPER_ADMIN internal note isolation → NEW → IN_PROGRESS → RESOLVED → reporter REOPENED → CLOSED transitions.
journeys/article-categories-crud.journey.spec.ts agent (+ super-admin token) 4 (AC1–AC4) PASS SUPER_ADMIN creates category → agent lists it → SUPER_ADMIN updates & round-trips → AGENT denied on PUT → SUPER_ADMIN deletes & GET returns 404.
journeys/verification-admin-queue.journey.spec.ts agent (+ super-admin token) 3 (VA1–VA3) PASS SUPER_ADMIN lists the KYC verification queue → fetches a single record by id → AGENT receives 403 on list, approve, and reject. No mutation against real submissions.
Run verification (2026-05-14). CPLK_E2E_BASE_URL=https://dev.cplk.org npx playwright test --grep "notification-preferences|system-announcements|support-ticket-lifecycle|article-categories-crud|verification-admin-queue|article-workflow|audit-log-read|analytics-read"20 passed (auth setup 8 + journey tests 12) in 19.1 s on the AR / AL / AN additions; the earlier NP / SA / ST / AC / VA sets continue to pass.

Sequence · notification preferences (NP1–NP3)

sequenceDiagram autonumber participant T as Test (agent) participant API as /api/notifications/preferences participant DB as preferences table rect rgb(236, 254, 255) Note over T,DB: NP1 · GET defaults T->>API: GET (Bearer agent) API->>DB: select role-filtered DB-->>API: groups + items API-->>T: 200 · groups[].items[] T->>T: assert role-filtered, every item has boolean flags end rect rgb(255, 247, 237) Note over T,DB: NP2 · PUT one type, GET round-trip T->>API: PUT [{type:X, emailEnabled:false, inAppEnabled:false}] API->>DB: upsert (one row) API-->>T: 200 T->>API: GET API-->>T: 200 · X has both flags=false, others unchanged end rect rgb(238, 242, 255) Note over T,DB: NP3 · bulk flip + restore T->>API: PUT [3 types, flags inverted] API-->>T: 200 T->>API: GET (verify all 3 flipped) API-->>T: 200 · all 3 flipped T->>API: PUT (restore originals) end
Spec: journeys/notification-preferences.journey.spec.ts. All three tests are independent — each restores its own mutation so re-running on a real account is idempotent.

Sequence · system announcements (SA1–SA3)

sequenceDiagram autonumber participant SA as super-admin (token) participant API as /api/system-announcements participant AG as agent (token) rect rgb(236, 254, 255) Note over SA,AG: SA1 · publish ALL → agent sees unread SA->>API: POST {title, message, targetType:ALL} API-->>SA: 200 · id, read=false AG->>API: GET ?page=0&size=50 API-->>AG: page · contains new id with read=false end rect rgb(255, 247, 237) Note over SA,AG: SA2 · agent marks read SA->>API: POST {targetType:ALL, ...} AG->>API: PATCH /{id}/read API-->>AG: 200 AG->>API: GET API-->>AG: same id now read=true end rect rgb(238, 242, 255) Note over SA,AG: SA3 · role isolation SA->>API: POST {targetType:ROLE, targetRole:AGENT} SA->>API: POST {targetType:ROLE, targetRole:FINANCIAL_OFFICER} AG->>API: GET API-->>AG: AGENT title in list · FO title NOT in list end
Spec: journeys/system-announcements.journey.spec.ts. Cleanup is implicit: marking the announcement read removes it from the agent's unread queue. There's no DELETE endpoint, by design.

Sequence · support ticket lifecycle (ST1–ST4)

sequenceDiagram autonumber participant AG as agent (token) participant API as /api/support/tickets participant SA as super-admin (token) rect rgb(236, 254, 255) Note over AG,SA: ST1 · create AG->>API: POST {title, description, category} API-->>AG: 201 · id, status=NEW end rect rgb(255, 247, 237) Note over AG,SA: ST2 · external reply AG->>API: POST /{id}/comments {content, internal:false} API-->>AG: 201 · comment id AG->>API: GET /{id} API-->>AG: ticket.comments contains reply end rect rgb(238, 242, 255) Note over AG,SA: ST3 · internal-note isolation SA->>API: POST /{id}/comments {content, internal:true} API-->>SA: 201 AG->>API: GET /{id} API-->>AG: ticket.comments does NOT contain internal note end rect rgb(240, 253, 244) Note over AG,SA: ST4 · status transitions SA->>API: PATCH /{id} {status:IN_PROGRESS} SA->>API: PATCH /{id} {status:RESOLVED, resolutionSummary} AG->>API: PATCH /{id} {status:REOPENED, statusChangeReason} SA->>API: PATCH /{id} {status:CLOSED} end
Spec: journeys/support-ticket-lifecycle.journey.spec.ts. ST3 includes a defensive branch: if the install denies internal notes to SUPER_ADMIN (CSA-only on some builds), the isolation assertion is skipped with a note.

State diagram · support ticket statuses

stateDiagram-v2 [*] --> NEW : ST1 NEW --> OPEN : staff opens NEW --> IN_PROGRESS : ST4 · staff OPEN --> IN_PROGRESS IN_PROGRESS --> WAITING_FOR_USER : staff IN_PROGRESS --> WAITING_FOR_INTERNAL_TEAM IN_PROGRESS --> ESCALATED IN_PROGRESS --> RESOLVED : ST4 · staff with resolutionSummary WAITING_FOR_USER --> IN_PROGRESS : reporter replies RESOLVED --> CLOSED : ST4 · staff RESOLVED --> REOPENED : ST4 · reporter (reason) REOPENED --> IN_PROGRESS CLOSED --> [*]
From TicketStatus enum. The journey exercises the highlighted ST4 path; the rest of the graph is reachable but not driven by an end-to-end test today.

Sequence · article categories CRUD (AC1–AC4)

sequenceDiagram autonumber participant SA as super-admin (token) participant API as /api/article-categories participant AG as agent (token) rect rgb(236, 254, 255) Note over SA,AG: AC1 · create + list SA->>API: POST {title, description, sortOrder} API-->>SA: 201 · id, slug auto-generated AG->>API: GET API-->>AG: list contains new id end rect rgb(255, 247, 237) Note over SA,AG: AC2 · GET /{id} AG->>API: GET /{id} API-->>AG: 200 · full record, fields round-trip end rect rgb(238, 242, 255) Note over SA,AG: AC3 · update + round-trip SA->>API: PUT /{id} {title*, description*, featured:true} API-->>SA: 200 SA->>API: GET /{id} API-->>SA: updated fields persisted end rect rgb(254, 242, 242) Note over SA,AG: AC4 · RBAC + delete AG->>API: PUT /{id} (denied) API-->>AG: 401 / 403 SA->>API: DELETE /{id} API-->>SA: 200 SA->>API: GET /{id} API-->>SA: 404 / 410 end
Spec: journeys/article-categories-crud.journey.spec.ts. Each test creates its own category with an [E2E-AC] title prefix; afterAll cleans them up so the dev catalogue stays tidy.

Sequence · verification admin queue (VA1–VA3)

sequenceDiagram autonumber participant SA as super-admin (token) participant API as /api/admin/verification participant AG as agent (token) rect rgb(236, 254, 255) Note over SA,AG: VA1 · list (shape lock) SA->>API: GET /requests?page=0&size=20 API-->>SA: 200 · paged or array · items[].id/subjectType/status end rect rgb(255, 247, 237) Note over SA,AG: VA2 · single detail (when queue non-empty) SA->>API: GET /requests/{id} API-->>SA: 200 · full record · status field present end rect rgb(254, 242, 242) Note over SA,AG: VA3 · RBAC negative path AG->>API: GET /requests (denied) API-->>AG: 401 / 403 AG->>API: POST /requests/{dummy}/approve API-->>AG: 401 / 403 AG->>API: POST /requests/{dummy}/reject {rejectionReason} API-->>AG: 401 / 403 end
Spec: journeys/verification-admin-queue.journey.spec.ts. Real KYC submissions on dev are handled manually — this journey only exercises read paths under SUPER_ADMIN and the negative RBAC checks under AGENT.

Sequence · article workflow (AR1–AR5)

sequenceDiagram autonumber participant ED as blog-editor (token) participant API as /api/articles participant SA as super-admin (token) rect rgb(236, 254, 255) Note over ED,SA: AR1 · create draft ED->>API: POST {title, content} API-->>ED: 201 · id, status=DRAFT end rect rgb(255, 247, 237) Note over ED,SA: AR2 · submit ED->>API: POST /{id}/submit API-->>ED: 200 · status=PENDING_APPROVAL end rect rgb(238, 242, 255) Note over ED,SA: AR3 · approve + publish SA->>API: POST /{id}/approve API-->>SA: 200 · status=PUBLISHED, slug set end rect rgb(254, 242, 242) Note over ED,SA: AR4 · reject with reason SA->>API: POST /{id}/reject {reason} API-->>SA: 200 · status=REJECTED end rect rgb(240, 253, 244) Note over ED,SA: AR5 · draft on a published article ED->>API: PUT /{id}/draft {title*, content*} API-->>ED: 200 (draft staged) ED->>API: GET /{id} API-->>ED: status=PUBLISHED (live copy unchanged) end
Spec: journeys/article-workflow.journey.spec.ts. Tries the body field reason first, then falls back to rejectionReason — different builds expect different shapes. AR5 verifies the published copy is preserved while the draft is in flight; the draft surfaces in the editor UI via a separate endpoint.

Sequence · audit log read (AL1–AL4)

sequenceDiagram autonumber participant SA as super-admin (token) participant API as /api/audit-logs participant AG as agent (token) rect rgb(236, 254, 255) Note over SA,AG: AL1 · paged read (shape lock) SA->>API: GET ?page=0&size=20 API-->>SA: 200 · items[].{id, action, entityType, createdAt} end rect rgb(255, 247, 237) Note over SA,AG: AL2 · /my for current user AG->>API: GET /my API-->>AG: 200 · rows · every userId matches /auth/me id end rect rgb(254, 242, 242) Note over SA,AG: AL3 · RBAC negative path AG->>API: GET / (denied) AG->>API: GET /stats (denied) AG->>API: GET /users/{any} (denied) API-->>AG: 401 / 403 on all three end rect rgb(238, 242, 255) Note over SA,AG: AL4 · entity trail SA->>API: GET /entity/{entityType}/{entityId} API-->>SA: 200 · only rows for that entity end
Spec: journeys/audit-log-read.journey.spec.ts. AL4 doesn't require the sample row to appear in its own trail (paging may exclude it) — every returned row must just match the requested entityType + entityId.

Sequence · analytics read (AN1–AN3)

sequenceDiagram autonumber participant AG as agent (token) participant API as /api/analytics rect rgb(236, 254, 255) Note over AG,API: AN1 · agency-scoped KPIs AG->>API: GET /agency API-->>AG: 200 · numeric KPIs · (or 500 known-gap) AG->>API: GET /agency/referrers API-->>AG: 200 · referrer roll-up end rect rgb(255, 247, 237) Note over AG,API: AN2 · per-property + time-series AG->>API: GET /properties/{mine} API-->>AG: 200 · numeric per-property KPIs AG->>API: GET /properties/{mine}/views API-->>AG: 200 · array of {date, count} points end rect rgb(254, 242, 242) Note over AG,API: AN3 · isolation AG->>API: GET /properties/{foreign-id} alt fenced API-->>AG: 403 / 404 else hardening item API-->>AG: 200 · empty / zero counters end end
Spec: journeys/analytics-read.journey.spec.ts. AN1 records a backend-bug annotation when /agency returns 500 for AGENT (real gap surfaced by the test); AN3 records a hardening annotation when the foreign-property endpoint returns 200 instead of 403/404.

Atomic spec areas

Outside the journeys, ~660 atomic specs cover the rest of the surface. The full inventory is in docs/E2E_TEST_COVERAGE.md; below is the summary by area.

AreaSpecsTestsNotes
Auth498Login, register, password reset, email verification.
Properties13~130Wizard catalog by role, image upload, edit, detail, public detail, status transitions.
Inquiries345Public form, list, detail.
Favorites230Button + page.
Dashboard / Analytics368Dashboard widgets, property analytics, agency analytics.
Notifications336Bell, page, badge increments.
Portal (admin)764Agency admin / super-admin views, subscriptions, boost, financial officer.
Public site6116Homepage, agencies, properties, search, link audit.
SEO219Sitemap, robots, OG tags, structured data, canonicals.
Onboarding641Company agency, individual agent, PO, skip, redirect.
Security / RBAC693API auth errors, validation errors, RBAC denial, cross-agency, blog/article RBAC.
Errors425API 5xx fallback, network, permission, validation.
Accessibility3~18 (16 skip)A11y, form validation, modal focus.
Mobile / responsive212Mobile + tablet viewport runs.
Smoke / core / network59Console-error scan, network audit, smoke.

Known gaps & backend issues

The Playwright suite is also a forcing function for backend issues. Every gap below has a corresponding active or test.skip test, so the suite flips green automatically when the backend ships the fix.

Deferred — infrastructure / shared-env constraints

  • Email-channel notification delivery (requires SMTP / mailbox interception not wired against dev).
  • R2 upload failure retry behaviour (requires S3 5xx mocking).
  • Slow-network / offline behaviour (requires fault-injection harness).
  • Cross-browser (Firefox / WebKit / Edge) — deliberately Chrome-only since the journey redesign.
  • Two users editing the same property / shared ad-slot races (not deterministic on shared dev).

Backend gaps surfaced by the suite

SeverityIssueDiscovered by
CRITICALCross-agency property approve bypass — an agency-admin can approve a property in another agency.security/rbac-denial.spec.ts (S6)
CRITICALCross-agency DELETE — DELETE of another agency's resource succeeds where it should 403.security/api-auth-errors.spec.ts
HIGH500 instead of 400 on POST /api/properties bean validation.security/api-validation-errors.spec.ts (V2–V6)
HIGH500 instead of 400 on PATCH /api/inquiries/{id}/status invalid enum.security/api-validation-errors.spec.ts (V7)
HIGH500 instead of 400 on POST /api/boosts invalid enum.security/api-validation-errors.spec.ts (V12)
HIGHSOLD / LEASED property transition endpoints missing.properties/property-status-transitions.spec.ts (T3–T6 skip)
MEDIUMPOST /properties/{id}/reject does not require a rejection reason.security/api-validation-errors.spec.ts (V13)
MEDIUMValidation runs before role check on POST /api/agencies — leaks field names to non-admins.security/rbac-denial.spec.ts
MEDIUMAuto-renew toggle endpoint missing.portal/subscription.spec.ts (SUB6)
MEDIUMSubscription payment-slip upload flow has no backend.portal/subscription.spec.ts (SUB7)
LOWSitemap can lag behind property publish events.seo/sitemap-and-meta.spec.ts
LOWNo hard DELETE for agencies (suspension only).portal/agencies-admin.spec.ts (AG4)
HIGHGET /api/analytics/agency returns 500 for plain AGENT — should be 200 (with agency-scoped data) or 403.journeys/analytics-read.journey.spec.ts (AN1)
LOWGET /api/analytics/properties/{foreign-id} returns 200 with zero counters instead of 403/404 — minor isolation hardening item.journeys/analytics-read.journey.spec.ts (AN3)

Conventions in this suite

  • One file per journey. Each journey lives in e2e/tests/journeys/*.journey.spec.ts and is named after the lifecycle it covers.
  • Independent tests. Where possible, journey tests are not serial — each creates its own resource and cleans up in afterEach so a single flake doesn't cascade.
  • Cross-role via Bearer tokens. Multi-actor journeys use captureBearerForRole to pull a JWT from another role's storageState rather than spinning up a second browser session.
  • Unique title prefixes. Test artefacts use [E2E-XX] prefixes (e.g. [E2E-BE] for blog editor, [E2E-ST] for support tickets) so manual cleanup is straightforward if a spec crashes.
  • The coverage-report.mjs script walks tests/**.spec.ts and rewrites the auto-generated section of docs/E2E_TEST_COVERAGE.md between markers. This HTML page is the human-friendly mirror — keep both in sync.

Glossary — engineering words, plain meanings

TermWhat it means here
E2E test"End to end" — a test that drives the real website in a real browser, from the login screen to the final result, the way a person would.
JourneyOne short story end-to-end. "Agent lists a property and approver approves it" is one journey.
SpecA single file containing a group of related tests. A journey lives in one spec file.
Role / role projectWho the robot is pretending to be (agent, approver, financial officer, …). Each role has its own pre-saved login, like a stored cookie.
Storage stateThe "I'm already logged in" file that lets the robot skip the login screen between tests.
Bearer token / JWTA digital wristband the server gives you after you log in. The robot reuses this to act on behalf of a second role inside the same test.
Tenant / agencyOne real-estate agency on CPLK. Every agency's data is walled off — the tests check that wall stays up.
RBAC"Role-based access control." The rules about who can do what. RBAC tests make sure an agent can't, say, approve their own property.
Idempotency"Press the button twice, only one charge happens." Critical for payments — these tests fire the PayHere webhook twice on purpose and check we only update the database once.
State machineThe official list of statuses something can be in and the legal moves between them (DRAFT → PENDING_REVIEW → ACTIVE …). The tests walk through every legal move.
SUPER_ADMIN / CSA / FO / PACPLK staff role codes. Super admin = full access. CSA = customer support agent. FO = financial officer. PA = property approver.
MermaidThe tool that draws every diagram on this page from a few lines of text. If you can read flowcharts, you can read Mermaid.
PlaywrightThe robot. Microsoft's browser automation tool — drives Chrome the same way Selenium used to, but more reliably.
Skipped / partial / blockedSome tests are written but turned off because the backend isn't ready or because the dev environment can't reproduce the condition (e.g. real emails). They're documented in Known gaps.

References

  • Playwright confige2e/playwright.config.ts
  • Auth setupe2e/setup/auth.setup.ts
  • Credentials fixturee2e/tests/fixtures/credentials.ts
  • Live coverage runbookdocs/E2E_TEST_COVERAGE.md
  • Backend state machinesbackend/src/main/java/com/cplk/api/config/PropertyStateMachineConfig.java