Knowledge > Products > WiseAI Realtor > QA Coverage Matrix
WiseAI Realtor — QA Coverage Matrix
Purpose. This is the founder's "assure every screen, tab, and section is present and working — not hope" artifact. It enumerates the full app surface, states the expected content + working behaviour for each, links the acceptance spec that governs it, and records the automated test that assures it today (COVERED / GAP / ADDED). The end of the doc defines the authenticated E2E plan that codifies presence + function + no-mis-route + saves-persist into a CI suite.
How to read a row. Expected state = what a human must see/be able to do for the screen to count as "working". Acceptance = the
knowledge/acceptance/realtor-*.mdspec (the prose contract). Assurance = the test(s) that lock it. A surface with no assurance is flagged.Verification ladder (CLAUDE.md). Unit/contract tests are rung 1 (necessary, never sufficient). Behavioural proof on the deployed URL is the bar for "done" (rung 2). This matrix's unit column is rung 1; the E2E plan is rung 2.
Source of truth for the surface inventory: the actual App-Router tree under churchwiseai-web/src/app/realtor/ + src/components/realtor/**, verified 2026-06-29. Nav model: src/components/realtor/shell/nav-model.ts.
0. Status legend
| Token | Meaning |
|---|---|
| LIVE | Fully built screen wired to real data/APIs. |
| STUB | Intentional RealtorStub placeholder (workstream deferred). Must render the stub copy, never a crash. |
| REDIRECT | Server redirect() to a real sub-screen (no standalone UI yet). |
| COVERED | An automated test asserts this surface's logic/shape today. |
| GAP | No automated test asserts this surface (presence or function). |
| ADDED | New test added in this pass (chore/re-qa-coverage). |
1. Surface inventory (master list)
1.1 Authentication surfaces (/realtor/*, outside the app shell)
| Route | Component | Status | Gate / behaviour |
|---|---|---|---|
/realtor/login | RealtorSigninForm | LIVE | Email → magic-link/OTP or password (re-signin-* testids); Google + Microsoft OAuth; redirects to /realtor/app on success. |
/realtor/mfa | RealtorMfaChallenge / RealtorMfaEnroll | LIVE | TOTP enroll (QR) + challenge; AAL2 step-up. |
/realtor/forgot-password | reset request | LIVE | Sends reset email. |
/realtor/update-password | reset completion | LIVE | Sets new password (post-reset / invited user). |
/realtor/accept-invite | invite acceptance | LIVE | Email-bound invite (R6) → creates membership within inviter ceiling. |
1.2 Command-center shell chrome (every /realtor/app/* screen)
| Element | Component | Status | Expected state |
|---|---|---|---|
| Left rail (nav) | RealtorRail + nav-model + nav-visibility | LIVE | Capability-gated destination set, resolved server-side; active-route highlight; account chip + signed-in user footer; collapses to 64px (desktop) / off-canvas drawer (mobile, Esc/backdrop/nav-click closes). |
| Top bar | RealtorTopBar | LIVE | Screen title (longest-prefix from nav-model) + date/greeting; Setup N% pill (links /setup, hidden when allDone); Aria launcher chip; notification bell (live WS6 feed); Search (⌘K) + New are intentionally disabled scaffolding. |
| Notification bell | NotificationBell | LIVE | Reads per-user feed for the active accountId; unread count; mark-read. |
| Ask-Aria panel | AskAriaPanel | LIVE | Mounted only with an active workspace; mobile FAB when closed; modal dialog (Esc/backdrop/focus-trap) when open; loads contextual opening (GET) then streams (SSE); quick replies; deep-links are Next-routed buttons. Read-only — Aria never writes settings. |
1.3 Backend screens (/realtor/app/*)
| Route | Component | Status | Nav capability gate |
|---|---|---|---|
/realtor/app | RealtorDashboardBody | LIVE | always (Home) |
/realtor/app/setup | SetupChecklist | LIVE | none (first-run hub) |
/realtor/app/leads | LeadsScreen | LIVE | leads:lead:read:own |
/realtor/app/inbox | InboxScreen | LIVE | inbox:conversation:read:own |
/realtor/app/contacts | ContactsScreen | LIVE | contacts:contact:read:own |
/realtor/app/contacts/[id] | ContactRecordScreen | LIVE | contacts:contact:read:own |
/realtor/app/pipeline | DealsBoard | LIVE | deals:deal:read:own |
/realtor/app/listings | ListingsManagerBody | LIVE | listings:manual:read:own |
/realtor/app/qr-campaigns | QrCampaignsBody | LIVE | analytics:report:view:own |
/realtor/app/website | WebsiteBuilderBody | LIVE | settings:branding:edit |
/realtor/app/studio | RealtorStub | STUB | content:item:create |
/realtor/app/calendar | CalendarTasksBody | LIVE | tasks:task:read:own |
/realtor/app/analytics | RealtorAnalyticsBody | LIVE | analytics:report:view:own |
/realtor/app/reviews | ReviewsManagerBody | LIVE | reviews:testimonial:read |
/realtor/app/settings/agents | AgentConfigClient | LIVE | ai:config:read |
/realtor/app/integrations | RealtorStub | STUB | integrations:crm:edit |
/realtor/app/settings | redirect → /settings/agents | REDIRECT | settings:profile:read |
/realtor/app/settings/notifications | NotificationPrefsScreen | LIVE | (settings child) |
1.4 Public site (/s/[slug] — the RealEstate Pro Website template)
Rendered by RealEstateSiteTemplate via src/app/s/[slug]/page.tsx + fixed sub-routes (do-not-reinvent §"Public site sub-pages"): /about, /reviews, /partners, /tools (mortgage/LTT/CMHC/property-tax/rent-vs-buy/house-hack), /guides (+/guides/[guide]), /blog (+/blog/[postSlug]), /featured/top-agent-magazine, /listing/[mls] (detail + maps), /scan/[mls] (QR → lead), /privacy, /terms, /[page] (generic). Plus /lr/[token] (public "leave a review" hub). These are the LIVE shipped surfaces (PR #1017 + follow-ons) — the backend's Website builder manages them; it does not re-implement them.
2. Per-screen expected-state specs
Each screen lists: tabs, sections (exact headings), the must-work actions (with the API/route each hits), the empty/loading/error states, the acceptance criteria, and the assurance (test files). Brand: navy chrome / teal action / gold accent (design-direction.md).
2.1 Dashboard — /realtor/app (bento command center)
Acceptance: realtor-dashboard-mvp.md, realtor-dashboard-shell-mvp.md.
| Section / tile | Expected state | Assurance |
|---|---|---|
| First-run entry banner | Rendered above the bento until setup complete (OnboardingEntryBanner). | onboarding-model.test.ts (logic) · GAP (component render) |
| Hero — "New leads today" | The one daily number from the REAL CRM book (deriveDashboardHero); supporting line always surfaces the open book so a 0-today day never reads "no leads"; links to leads. | dashboard-hero.test.ts (COVERED) |
| KPI strip (4) | AI conversations · Calls answered by AI · QR sign scans · Showings booked. Show — + "AI front office not active yet" until provisioned (never fabricated). | dashboard-hero.test.ts (aiActive) |
| Aria proof tile | "Aria — your AI front office" + Autopilot badge; honest empty ("not active yet") + "Set up your AI front office" CTA → /settings/agents. | route-integrity (ADDED, link) |
| Your access (live) | Role / scope / effectivePermissions count / MFA (aal) per membership; MFA CTA → /realtor/mfa when not aal2. | rbac.contract.test.ts (caps) |
| Finish setting up | Real onboarding %, each step + StatusPill, "Open the full setup guide" → /setup. Falls back to static steps if no onboarding prop. | onboarding-model.test.ts |
| Pipeline snapshot | 6 stages (colour+label, colourblind-safe), honest 0s, "Open board →" → /pipeline. | route-integrity (ADDED) |
| Needs a human | Honest empty queue, "Open the inbox →" → /inbox. | route-integrity (ADDED) |
| Fail-closed state | Signed in, no active membership → "Almost there" card naming the user email; zero capabilities. | rbac.contract.test.ts (fail-closed R1) |
Acceptance criteria: no fabricated metric ever (honesty contract); every tile CTA resolves to a real screen; fail-closed card shows for a membership-less user.
2.2 Setup hub — /realtor/app/setup (the onboarding CENTERPIECE)
Acceptance: realtor-onboarding-mvp.md.
- Welcome header: workspace name + "Welcome, {first}" (or "You're all set up" when
allDone); progress bar; "X of Y required · A of B recommended"; "Ready to launch" chip whenlaunchReady; Ask-Aria invite. - 9 step cards in 5 groups (catalogue is the single source of truth —
onboarding-model.ts): Foundations (brokerage_identity,profile_team), AI front office (ai_agent), Online presence (website,listings), Growth (notifications,reviews,qr_campaigns), Optional (vow_sold_data→VowOptInCard).*= required. - Each card: title + Required badge + "Start here" on the first unfinished required step;
WhyThisMatters(open by default); estimate + honest detail line; CTA deep-link (Confirm/Set up/Build…/Reviewwhen done). - Honesty: a step shows "Done" only when the backing data is real;
unknown("Can't check yet") when a table/column isn't provisioned — never a fabricated Done.
Must work: every step cta routes to its real screen; progress math (computeProgress) and startHere reflect live signals.
Assurance: onboarding-model.test.ts (catalogue invariants, per-step derivation, progress, startHere, groupSteps) — COVERED. Step CTA routes → route-integrity.contract.test.ts — ADDED. Server gather (computeOnboardingStatus) → aria-onboarding-context.int.test.ts (DB) partial; GAP on the gather's per-signal SQL.
2.3 Leads — /realtor/app/leads
Acceptance: realtor-leads-mvp.md. Data: GET /api/real-estate/crm/contacts?account_id&limit=200.
- Smart-list tabs (live counts): "All" + universal (Hot leads / New this week / Needs follow-up…) + per-language lists (roster ∩ rows, never hardcoded).
- Toolbar: search "Search leads…"; Status filter (new→lost); Source filter (ai_voice/ai_chat/web_form/qr_scan/referral/portal); Language filter; Density (Compact/Comfortable); View toggle List ⇄ Pipeline.
- List view (
ContactsTable): Name·Status·Source·Lang·Score·Last touch·Next action·(Agent if management); sortable Name + Last touch; persists sort per smart-list; mobile = card list. - Pipeline view (
KanbanBoard): 8 stages; drag →PATCH …/contacts/{id}{pipeline_status}; keyboard "Move to stage" alternative; per-column count + value. - Lead drawer: chips (Status/Source/Lang/Score); Call/Text/Email quick actions disabled when consent-suppressed (reason shown); Aria summary; key facts; activity timeline (loads
…/contacts/{id}/timeline); "Open record →". - States: loading skeleton(8); NotProvisioned ("Your leads" copy); error+retry; empty ("No leads yet"); no-match ("Nothing in '{list}' right now").
Assurance: presenters.test.ts (chips, language pill, smart-lists, filterByView, consentView suppression, nextActionFor, sort) — COVERED. Loader/intake → crm-pure + crm-spine.int.test.ts — COVERED (DB-gated). Component render + drag-persist → GAP (E2E).
2.4 Inbox — /realtor/app/inbox
Acceptance: realtor-inbox-mvp.md. Data: GET /api/real-estate/crm/conversations?account_id.
- 3-pane: (1) Views (needs_human / unassigned / ai_handled / assigned_to_me / all —
needs_humanurgent-red badge) + Channel + Language filters; (2) conversation list (avatar/name/time/snippet + chips: Channel/Lang/Escalated/AI/"You're in control"); (3) thread (message log, system escalation rows, contact/agent/AI bubbles). - Take-over / Resume:
POST …/contacts/{id}/handoff{action:'take_over'|'resume'}(gatedcanTakeOver). - Composer: present but Send is disabled with title "Replies are sent by you — message sending is founder-gated in this build" (CASL).
- Context rail (xl+): Aria summary + contact facts + "Open full record →".
- States: 3-pane skeleton; NotProvisioned ("Your inbox"); error+retry; empty ("No conversations yet"); "Select a conversation to read it."
Assurance: presenters.test.ts (rollupConversations, filterByView) + conversations.int.test.ts (inbox rollup, DB) — COVERED. Handoff POST + render → GAP (E2E). The founder-gated no-send is a hard invariant → casl-no-cron-send.contract.test.ts — COVERED.
2.5 Contacts — /realtor/app/contacts & Contact record — /realtor/app/contacts/[id]
Acceptance: realtor-contact-record-mvp.md.
- Contacts list: search "Search contacts…" + lifecycle filter (lead/active_client/under_contract/past_client/sphere) + count; row →
/contacts/{id}. States mirror Leads (NotProvisioned "Your contacts", empty "No contacts yet", no-match). - Contact record (3-col): Identity card (avatar/name/lifecycle/score/lang/phone/email/location) · Relationship memory (birthday/anniversary/family/mortgage/referral + tags) · Consent (per-channel allowed/blocked, red when suppressed) · Aria summary · Activity timeline (filter: All/Calls/Messages/Notes) · Note composer (
POST …/contacts/{id}/timeline, gatedcanUpdate) · Next best actions · deferred Tasks/Listings link cards · Deal card (commission row only ifcanSeeCommissions). - Merge: "Merge duplicate…" (gated
canMerge) →MergeDialogsearch (…/contacts?search=) → two-stage confirm →POST …/crm/merge(strictest consent, reversible tombstone). - States: 3 skeletons; NotProvisioned ("This contact"); forbidden ("No access"); error+retry.
Assurance: crm-pure.test.ts (identity, consent reconcile, merge-strictest) + crm-spine.int.test.ts (create/get/update/merge/scoping) + presenters.test.ts (chips, relativeTime, scoreTone) — COVERED. Note POST + merge UI + commission redaction render → GAP (E2E); commission gate logic → rbac.contract.test.ts (COVERED).
2.6 Pipeline / Deals — /realtor/app/pipeline
Acceptance: realtor-deals-mvp.md. Data: GET /api/real-estate/crm/deals?account_id.
- Header: "{active} active · {pipeline} in pipeline · {closed} closed"; Board ⇄ Table toggle; "+ New deal" (gated
canEdit) → dialog (Side Buy/Sell, Address, Price) →POST …/deals. - Board: 6 columns (New/In progress/Offer/Accepted/Close to follow/Closed); drag →
PATCH …/deals/{id}{status}; keyboard move alternative; cards show address/side/client/price/milestone/days-in-stage/lang. - Table: Address·Source·Status·Price·Beds·DOM·Leads·Actions, sortable.
- Deal drawer: Summary · Parties · Key dates & reminders (offer/conditions/waived/closing/possession) · conditions checklist · linked listing/contact · Compliance (FINTRAC status flag + "documents in your brokerage system" + brokerage link) · activity. No GCI/commission row. Actions (gated
canEdit): Log update (POST …/deals/{id}/activity), Advance → next stage, Add reminder (disabled → "set in Calendar & Tasks"). - States: column skeletons; error "Couldn't load your deals."; NotProvisioned banner; empty nudge ("Won leads become deals here…").
Assurance: deals.int.test.ts (pure derivations no commission A2, CRUD, status transition + stage_since, parties, ownership scoping, archive/tenant scoping) — COVERED. Drag-persist + drawer render → GAP (E2E).
2.7 Calendar & Tasks — /realtor/app/calendar
Acceptance: realtor-tasks-calendar-mvp.md.
- Calendar: week/day/list views; prev/today/next; sync badge (connected vs "request-capture still works"); legend (Showing/Open house/Listing pres./Call-back/Closing/Condition) + "Aria captures showing REQUESTS; you confirm. No AI auto-booking."
- Follow-Up Coach rail: speed-to-lead widget (median first response + "N unactioned > threshold"); task groups Overdue/Today/Upcoming; task card checkbox (
PATCH …/tasks/{id}{status:'completed'}); showing-request Confirm ({confirm_showing:true}) / Decline ({decline_showing:true}); Call/Text/Email quick actions (consent-gated); Action plans section (template badge, steps, channels, "SMS off by default", "no cron auto-sends" banner). - States: analytics skeletons; graceful empty workspace when tasks/deals tables unprovisioned; "No follow-ups yet"; "No events this week."
Assurance: ws3-pure.test.ts (day-bounds TZ, bucketing Overdue/Today/Upcoming, actionSuppressed, computeSpeedToLead, computeHotLead, routeLead, buildTasksFromSteps SMS-off/consent/idempotent) + ws3-spine.int.test.ts (CRUD, ownership, enroll spawns tasks, routing persists) — COVERED. Confirm/decline + complete render → GAP (E2E).
2.8 Listings — /realtor/app/listings
Acceptance: realtor-listings-mvp.md. Data: getUnifiedListings + DDF status from metadata.ddf.authorized.
- Header: "{active} active · {manual} manual · {ddf} from MLS"; FeedPill (CREA DDF live vs not connected); Gallery/Table/Map toggle; "+ Add listing" (gated
canEditManual). - Filter bar: search (location/address/MLS#) + min/max price + Beds + Baths + Type + status pills (all/active/pending/sold).
- Gallery card: hero+status+source badge; feature star (
PATCH …/crm/listings{action:'feature'}) + reorder ↑↓ ({action:'reorder'}) — works for feed AND manual (beats SellingToolz); compliance line ("MLS® … Courtesy of the listing brokerage via REALTOR.ca" for DDF / "Pre-market… not on REALTOR.ca" for manual); DOM·leads; Media modal; Edit (manual only) / "Read-only" (feed); "Kit ✦" →/studio?listing=; "QR sign" →/qr-campaigns?listing={id}(added 2026-07-01, PR #1091). - Agent tag (added 2026-07-01, PR #1095): a select per card/row (
{action:'assign_agent', listing_agent_id}) backed by thelocal_business_agentsroster, for the team's OWN internal viewing — manual only, because CREA's DDF feed does not expose per-listing agent identity (confirmed live:ListAgentFullNamereturns null on every synced row). Needed because an office-wide feed (e.g. Beckett's Sutton office,ListOfficeKeyscope) syncs every agent's listings at that office, not just the tenant's own — there is no way to auto-detect "which are mine." Writes directly to the existinglisting_agent_idcolumn, which the DDF sync already protects from being overwritten (seeddf/map.ts's feed-derived-columns list) — survives every resync exactly likefeatured/sort_orderdo via the overlay. - Listing form modal: pre-market banner; address(req)/city/neighbourhood/price/beds/baths/sqft/type/status/banner + "Show on public website";
POST/PATCH …/crm/listings(/{id}). - Media modal: photos + alt-text (AODA, required) (
{action:'alt_text'}works for feed too); reorder/add/remove (manual); video/3D/floorplan URLs. - States: EmptyState ("No listings yet" + Add / Connect DDF); NoMatches; map "no coordinates"; not-provisioned stub ("…WS4").
Assurance: listings-overlay-pure.test.ts (source/editable, DOM, overlay merge, featuring survives DDF sync, counts, alt-text) + listings-overlay-server.test.ts (feature INSERT/UPDATE fail-closed on foreign, reorder, alt-text on overlay, lead attribution, getUnified) + listing-attribution.test.ts (MLS®/courtesy/trademark hygiene) — COVERED, 21/21 still green after the agent-tag addition. Form/media modal save render + agent-tag select render → GAP (E2E).
2.9 QR Campaigns — /realtor/app/qr-campaigns
Acceptance: realtor-qr-campaigns-mvp.md. Data: getCampaignsAnalytics.
- Header: "{n} campaigns · {scans} scans · {leads} leads · {range}"; differentiator badge "Only on WiseAI: scan → live AI chat"; range All/7d/30d/90d; Export CSV; "+ Create QR campaign" (gated
canCreate). - KPIs (4): Total scans · Unique visitors · Leads captured · Scan→lead rate ($0 CPL).
- Charts: Scans over time (+ "view as table") · Scan funnel (Scans→Unique→Opened chat→Captured→Showing) · Top campaigns.
- Campaigns table: type chips + search; Campaign·Type·Linked to·Scans·Leads·Conv.·Status·Created; status badges Active/Paused/Draft/Archived.
- Create drawer (single-page form, not a stepper): type → connect content (listing/page) → landing template → capture fields (Name locked) → generate QR (style/logo/
QrCodePreviewPNG·SVG) → print assets (yard-sign/window card). "Save draft" (status:draft, generate_qr:false) / "Activate" (status:activethenPATCH generate_qr:true). The "connect content → Listing" dropdown sourcesgetUnifiedListings(the SAME real DB listings the Listings screen shows, manual+DDF) — fixed 2026-07-01 (PR #1091); it previously read a hardcodedsite-config.tsfixture with non-UUID ids, so picking a listing silently no-opped and the campaign never actually linked to it. A listing's realmls_numbernow flows through to the destination path (/s/{slug}/scan/{mls}), fulfilling the "pre-loaded with the listing" claim. A per-listing "QR sign" quick action on the Listings screen deep-links?listing={id}into this drawer, pre-selected. - A "QR sign" deep-link (
?listing=) reads the query param in auseEffect(client-only, post-mount), never at render time — deriving initial state fromuseSearchParams()is a hydration-mismatch risk (see thefmtDatefinding below). - States: ProvisioningEmpty; EmptyCampaigns; "No campaigns match this filter."
Assurance: qr-campaigns-pure.test.ts (slug, destination path, buildTrackedUrl, scan funnel, scan→lead, bucketByDay, range) + qr-campaigns-server.test.ts (not-provisioned, create, ownership filter, recordScan, analytics) — COVERED. Create drawer render + QR render → GAP (E2E). The QR-create-500 prod bug (task #67 / PR #1077) was fixed but never actually re-verified live until 2026-07-01, when a real create was triggered on prod and succeeded (201).
Hydration gotcha (found + fixed 2026-07-01, PR #1094): fmtDate() in QrCampaignsBody.tsx called toLocaleDateString() with no explicit timeZone — the Vercel server (UTC) and a visitor's browser (local zone) can format the same timestamp as a different calendar day, causing a real React #418 hydration error once a campaign row existed to render through it (0 campaigns = 0 errors, which is why it wasn't caught until this session's own test campaigns triggered it). Reproduces ONLY on real cross-machine infra — a local next build && next start shares one machine's timezone for both "server" and "client" and will never show it; verify any date-formatting fix against a live preview/prod deploy, never a local build. The same unpinned-timezone pattern was found and fixed in 5 more files: ContactRecordScreen.tsx, TeamManagerClient.tsx, NotificationBell.tsx, the shared presenters.ts, and reviews/manager-model.ts — all latent until real timestamped rows render on those screens. RealtorTopBar's date/greeting already guards this correctly via useSyncExternalStore (renders empty server-side, fills in client-side post-mount) — confirmed NOT a bug.
2.10 Website builder — /realtor/app/website (5 tabs)
Acceptance: realtor-website-builder-mvp.md. Loader: loadWebsiteBuilderState. Gate: settings:branding:edit (else stub); publish needs content:item:publish.
| Tab | Expected state | Assurance |
|---|---|---|
| Templates | 6 families (Local Authority/Luxury/Signature Agent/Modern Team/Multilingual Community/Brokerage Pro); "Current" badge; full editor opens from account editor. | GAP (static, presentational) |
| Pages | Nav pages (status + lang chips + SEO badge) + utility pages; nested area guides; reorder ↑↓ → PUT …/website/nav-order; "N pages need an SEO meta description" warning; Edit (manual) / View (feed). | pages-model.test.ts (buildPagesModel, status/SEO from real inputs, languages tenant-driven, applyNavOrder) — COVERED |
| Brand kit | Brand name + languages; colours/fonts/logo in full editor; AA contrast preserved. | GAP (static) |
| Domain | Hosting (Canada region · SSL); custom domain (verified/DNS pending); bring-your-own embed snippet. | brokerage-footer/status (partial) · GAP (embed render) |
| Tracking & Compliance (the publish GATE) | Readiness summary (province regulator + "X of Y required checks"); Brokerage identity form = the HARD gate (legal name, descriptor word, office address, registered agents — Publish blocked client+server until complete; live footer preview); PUT …/website/brokerage-identity; POST …/website/publish. Trademark & DDF · Analytics (Resend click-tracking OFF by design) · Cookie consent (PIPEDA/Law 25) · Accessibility (AODA) · Hosting. | brokerage-identity.test.ts (regulator resolution province-aware, the hard publish gate, footer line, defaults overlay) + brokerage-footer.test.ts + status.test.ts (requiresFrench, compliance status) — COVERED |
GAP: loadWebsiteBuilderState assembler (DB loader) has no direct test; constituent models (pages, identity, footer, compliance status) are covered.
2.11 Analytics — /realtor/app/analytics
Acceptance: realtor-analytics-mvp.md. Pure body from AnalyticsResult; controls are URL-param links (no client JS).
- Controls: date range 7d/30d/90d/MTD/YTD; first⇄last touch; compare prev period; Export CSV →
/api/real-estate/crm/analytics?format=csv. - KPI strip (5): Leads · Lead→client% · Showing requests · Deals firm/closed · Transaction volume (not GCI) [gold accent]. Each with delta vs prev.
- Reports: Lead source attribution & ROI (first/last touch, $0 owned, "—" until ad spend) · Leads by language · Conversion funnel (scan→…→close, biggest-drop flag) · Speed-to-lead (median AI vs human + unactioned>24h) · What's creating your leads (top listings / top QR signs scan→lead / top content) · Team performance (management-only; per-agent volume, never commission).
- Honesty: every number a real first-party count; GCI never appears (A2); honest "No data for this period" per report + a spine-empty card.
Assurance: analytics-compute.test.ts (helpers, KPI strip, source attribution & ROI, funnel, speed-to-lead, language, what's-creating-leads, team performance) + analytics.int.test.ts (team scope, own-scope isolation, empty honest empties — DB) — COVERED. Render + CSV export + URL-param controls → GAP (E2E).
2.12 Reviews — /realtor/app/reviews
Acceptance: realtor-reviews-manager-mvp.md. Loader: loadReviewsManagerState.
- Header + dry-run banner: "Owner-pressed · dry-run on · no cron auto-send"; "nothing is sent automatically."
- Review funnel: Requests sent → Clicked the link (real engagement) → ★ Public review / Private feedback; "Honest metrics — clicks, not opens"; same public ask to every eligible client (FTC 16 CFR 465 — no happy→public routing).
- Platform targets (Google/RankMyAgent…); Review requests table (Client·Consent(EBR)·Status·Last touch·Actions): Send (gated
canSend, consent re-checked), Reviewed, Opt-out, Read & reply (private feedback); batch send of approved. - CSV import (last 24mo only, transaction date required, staged as drafts); Testimonials gallery (republish only with consent + RECO ID + CREA disclosure).
- CASL footer: sender name/address/unsubscribe/EBR windows.
Assurance: manager-model.test.ts (funnel honest clicks-not-opens, rowEligibilityForSend mirrors engine gate, presentReviewRow owner actions + terminal state, consent/EBR labels, approved batch count, latest-import) + casl-no-cron-send.contract.test.ts (reviews send owner-pressed behind consent gate) — COVERED. Send/opt-out POST + render → GAP (E2E).
2.13 AI Agents — /realtor/app/settings/agents (Voice + Chat tabs)
Acceptance: realtor-agent-config-mvp.md. Data: GET /api/realtor/ai-config?account_id (account_id in query, not just header — middleware strips the header). Save: PATCH same. Test: POST /api/realtor/ai-config/test.
- Header: Aria live indicator; Discard/Save (gated
canEditChat); preview-only banner if config not persisted; view-only banner if!canEditChat; first-run "Chat" discoverability nudge (reduced-motion-safe). - Voice tab: Working mode (Co-pilot/Autopilot) · Guardrails (RE_GUARDRAIL_CHIPS shown LOCKED — never toggles) · Premium Voice Line (DID/voice/language/hours/greeting/capture/escalation/summary) · Community Language Lines (your differentiator; "Native review required" flag for non-EN/FR).
- Chat tab: Knowledge sources (brokerage policies Required/locked) + multilingual replies · Conversation rules (tone) · Website placement (3 placements + embed snippet) · Lead-capture fields (Name/contact Required) · Test Aria sandbox ("no lead is saved").
Assurance: ai-guardrails.test.ts (valuation/financing/fair-housing/confidentiality belts, safe deferral, bridge frame, guardrail block, church/funeral/vet unaffected, facts-used receipt, safety floor) — COVERED. agent-config-shared.test.ts — ADDED (catalogs' required-locked invariants, resolveAgentConfig defaulting + required-can't-toggle-off, mode sanitize, voice-line classifiers, languageLabel). /api/realtor/ai-config GET/PATCH/test route handlers → GAP (E2E — note the account_id-in-query 401 fix, task #65).
2.14 Notifications — /realtor/app/settings/notifications
Acceptance: realtor-notifications-mvp.md. Data: GET/PATCH /api/real-estate/crm/notifications/prefs.
- Event grid (Event·Push·Email·SMS·Timing); urgent events ("Always urgent" — hot lead/handoff/unactioned) always instant + never digested; transport-gate banner (push/email/SMS "ready, switching on gradually" — in-app feed always on); quiet hours; daily digest; Save (409 → "personal login required").
- Hard invariant: notifications address staff only, never a lead/prospect (CASL).
Assurance: notifications-pure.test.ts (staff-vs-lead boundary, taxonomy, in-app floor, urgent-never-digested, founder transport gate, quiet hours, deep-links, default copy) + notifications-server.test.ts (assertStaffRecipient, recipient fan-out, CRUD, visibility scope, prefs) — COVERED. Save round-trip render → GAP (E2E).
2.15 Stubs & redirects (must render, never crash)
| Screen | Expected | Assurance |
|---|---|---|
Content Studio /studio | RealtorStub "Content Studio" + 3-pane editor description (WS9 deferred). | route-integrity (ADDED — page exists) |
Integrations /integrations | RealtorStub "Integrations" + CRM/DDF/calendar/Zapier description (Connect deferred). | route-integrity (ADDED) |
Settings /settings | redirect() → /settings/agents. | nav-visibility (title) + route-integrity (target exists) |
2.16 Ask-Aria concierge panel (cross-cutting)
Acceptance: aria-onboarding-concierge-mvp.md. Opens from top-bar chip, setup/dashboard cards, step cards, mobile FAB.
- Contextual opening (GET) → streamed replies (SSE); quick replies; read-only ("she points you to the right screen, she doesn't change settings"); honest fallback when status unreadable; deep-links are real routes.
Assurance: aria-onboarding.test.ts (prompt grounding, honest metrics in SETUP STATE, deep-link accuracy never-invented-route, no-write floor, contextual opening) + aria-onboarding-context.int.test.ts (live DB) — COVERED. Panel component render/stream → GAP (E2E). Element-registry/highlight (Aria 1.5) → its own node --test coverage (tasks #68–71).
3. Test-coverage summary (per surface)
| Surface | Unit/Contract | Integration (DB) | Component render | E2E (authed) |
|---|---|---|---|---|
| Shell nav + title | ✅ nav-visibility | — | GAP | PLAN |
| Route wiring (nav+onboarding→screen) | ✅ route-integrity (ADDED) | — | n/a | PLAN |
| Dashboard | ✅ dashboard-hero | — | GAP | PLAN |
| Setup hub | ✅ onboarding-model | partial (context.int) | GAP | PLAN |
| Leads | ✅ presenters | ✅ crm-spine | GAP | PLAN |
| Inbox | ✅ presenters | ✅ conversations | GAP | PLAN |
| Contacts / record | ✅ crm-pure/presenters | ✅ crm-spine | GAP | PLAN |
| Pipeline/Deals | ✅ deals (pure) | ✅ deals.int | GAP | PLAN |
| Calendar/Tasks | ✅ ws3-pure | ✅ ws3-spine | GAP | PLAN |
| Listings | ✅ overlay-pure/attribution | ✅ overlay-server | GAP | PLAN |
| QR Campaigns | ✅ qr-pure | ✅ qr-server | GAP | PLAN |
| Website · Pages | ✅ pages-model | — | GAP | PLAN |
| Website · Compliance gate | ✅ brokerage-identity/footer/status | — | GAP | PLAN |
| Website · Templates/Brand/Domain | GAP (static) | GAP (loader) | GAP | PLAN |
| Analytics | ✅ analytics-compute | ✅ analytics.int | GAP | PLAN |
| Reviews | ✅ manager-model | — | GAP | PLAN |
| AI Agents config model | ✅ agent-config-shared (ADDED) + ai-guardrails | — | GAP | PLAN |
| Notifications | ✅ notifications-pure | ✅ notifications-server | GAP | PLAN |
| RBAC / capability gating | ✅ rbac.contract | — | n/a | PLAN |
| Compliance (CASL/VOW/attribution) | ✅ casl/vow-gate/listing-attr/status | — | n/a | PLAN |
| Auth bridge / invites | ✅ auth-bridge.contract | partial | GAP | PLAN |
| Ask-Aria concierge | ✅ aria-onboarding | ✅ context.int | GAP | PLAN |
| Stubs (Studio/Integrations) | ✅ route-integrity (ADDED) | — | n/a | smoke |
Top surfaces with the LEAST assurance (priority for E2E):
- Every LIVE screen's React render + interaction — no component-render tests exist; logic is well-covered but "does it paint + can the user act" is only assured by the authed walk. (Highest-value gap; the E2E plan §4 closes it.)
- API route handlers (
/api/real-estate/crm/*,/api/realtor/ai-config*,/api/real-estate/website/*) — covered indirectly via server-fn int tests, but the HTTP layer (auth, account_id resolution, status codes) has no route-handler test. Two prod 401/500 bugs (#65, #67) slipped exactly here. - Website Templates/Brand/Domain tabs +
loadWebsiteBuilderState— presentational/loader, no test. - Server gather
computeOnboardingStatus— the per-signal SQL that drives the whole setup %/Start-here.
4. Authenticated E2E plan (to codify into CI)
Why E2E is mandatory here. Unit tests prove the logic; they cannot prove a screen renders, a save persists, a route doesn't mis-navigate, or a capability actually hides a tab. CLAUDE.md's verification ladder requires deployed-URL behavioural proof for "done". The existing qa-teamtest walk (scripts/qa-walk.mjs, qa-final-walk.mjs, seeded by qa-teamtest-seed.mjs) is the seed of this — it logs in as the TeamTest fixture and visits every screen, asserting HTTP status, no page/console errors, no stub/error-boundary leak, and one expected-text hit. This plan deepens that from "loads without error" to "present + functional + persists".
4.1 Harness & credentials (secure-creds requirement)
- Auth: password sign-in as seeded TeamTest users (owner + a scoped agent) via the
re-signin-*testids (already inqa-walk.mjs). Credentials (WALK_EMAIL/WALK_PASSWORD) must come from CI secrets / env, never committed. The seed user is the canonicalTeamTest/teamtest-realtyfixture (andTeamMoelker). - Target: the deployed app (preview or prod host). Realtor backend is not host-aliased, so a
*.vercel.apppreview is acceptable for/realtor/app/*; the public/s/[slug]site must be checked on the real Pro Website host (middleware rewrites). Read the preview URL fresh per HEAD sha. - Realtime check: real desktop Chrome UA (the walk already sets one) to avoid bot-gating.
- Isolation: each run uses the seeded fixture account; writes go to demo/test rows only (never a customer account).
4.2 Per-screen E2E cases (presence + function + no-mis-route + saves-persist)
For every LIVE screen in §1.3, the suite asserts:
- Presence: the screen's H1/title matches
navTitleForPath; each documented tab label and section heading from §2 is in the DOM; no stub/error-boundary marker (STUB_MARKERSin qa-walk.mjs). - No mis-route: clicking each rail item lands on the matching route (URL + active highlight); each in-screen CTA (e.g. dashboard "Open board →", setup step CTAs, "Open record →") navigates to the documented target (this E2E mirrors what
route-integrityproves statically). - Function (sampled, behaviour not DOM-presence — verify-behavior rule):
- Leads: toggle List⇄Pipeline (assert view change at 2 timepoints); apply a status filter (row count changes); drag/keyboard-move a kanban card → reload → status persisted.
- Contacts record: add a note → it appears in the timeline → reload → still there.
- Pipeline: create a deal → appears in column; advance stage → persisted.
- Calendar: complete a task → it leaves the Today group; confirm a showing-request → becomes a showing.
- Listings: feature-star a feed listing → reload → still featured (overlay survives); save alt-text → persisted.
- QR: run the create drawer to "Save draft" → row appears as Draft (regression for the #67 500).
- Website: save brokerage identity incomplete → Publish disabled; complete it → Publish enabled; nav-order reorder persists.
- AI Agents: load (no 401 — #65 regression); toggle a non-required knowledge source + Save → reload → persisted; required source/field cannot be turned off; Test Aria returns a reply and writes no lead.
- Notifications: change a channel toggle + Save → reload → persisted (or the documented 409 for a shared-login session).
- Analytics: switch date range (URL param) → KPIs update; Export CSV returns a CSV content-type.
- States: for at least one screen, force the empty/not-provisioned path and assert the honest copy (not a blank or a crash).
- RBAC: as the scoped agent user, assert Website + Integrations are absent from the rail and the routes fail-closed (stub), while Leads/Contacts/etc. are present (mirrors nav-visibility.contract at runtime).
- Public site:
/s/teamtest-realtyrenders (title), chat CTA present, a listing detail loads, a/scan/[mls]capture works.
4.3 Suite shape & registry
- Author as Playwright specs under
churchwiseai-web/tests/e2e/realtor/(one spec per nav area + a shell/RBAC spec + a public-site spec), reusing theqa-walk.mjslogin + error-capture scaffolding. - Register in
knowledge/tests/registry.yamlas a realtor suite (today it has zero realtor entries — itself a gap); mark the AI-config / publish-gate / lead-persist casescritical_path: trueso the CI gate requires a saved Playwright artifact. - Cross-reference the
qa-teamtestlive coverage-walk scorecard (theqa/teamtest-e2eworktree) as the running baseline; this plan is its acceptance-criteria upgrade.
5. What changed in this pass (chore/re-qa-coverage)
- ADDED
src/components/realtor/shell/__tests__/route-integrity.contract.test.ts— every nav destination + every onboarding CTA resolves to a realpage.tsx; no orphan screens (any new screen must be wired to the rail or allowlisted). This is the "every screen is present" assurance, encoded. - ADDED
src/lib/real-estate/__tests__/agent-config-shared.test.ts— the AI Agents config model (previously zero coverage): catalog required-locked invariants,resolveAgentConfigdefaulting + the can't-toggle-off-a-required-field rule, mode sanitize, voice-line classifiers,languageLabel. - Both pass; full realtor pure+contract suite (with the
server-onlyshim) = 543/543 green.
Runner (no vitest):
node --require ./src/lib/real-estate/crm/__tests__/setup-server-only.cjs \
--import tsx --test <spec> # files that transitively import server-only
node --import tsx --test <spec> # pure files (route-integrity, agent-config-shared, …)