Skip to content

Manual Test Plan — Viewing projections & subscriptions

Covers the read-only surface: projections list page, projection detail page, subscription detail page, per-store filtering and drill-in.

V-1 — Projections page renders one row per projection + one HWM row per store

FieldValue
Setupdotnet run --project src/BffHost (Full scenario, default). Wait for RepairShop, TripService, TripPublisher to appear in the Aspire dashboard as Running.
ActionNavigate to the Projections page (/projections) in the SPA.
Expected observationThe table renders one HWM row per (service, store) pair, then one row per projection beneath. For TripService you should see exactly one HWM row (single-store) and rows for TripProjection, DayProjection, DistanceProjection. For MultiStoreHost (PS#2 fix) you should see three HWM rows — one for each registered event store — with distinct sequence numbers.
How to verifyEach HWM row has name === 'HighWaterMark' and a non-empty store label in the Store column. For multi-store: assert at least 2 rows match el-table__row:has-text("HighWaterMark") and the row sequence numbers differ. Cross-check against GET /api/critterwatch/service-summaries/MultiStoreHostshardStatesByStore should have ≥ 2 outer keys (one per store URI). The Playwright spec at src/FrontEnd/e2e/projections/multi-store-projections.spec.ts automates this assertion.

V-2 — Projection detail page surfaces shard state + lag

FieldValue
SetupSame as V-1 — Full scenario, wait for traffic. Click into the TripProjection row from the Projections page.
ActionWait for the Projection Detail page to mount (URL pattern /projection/<serviceId>/<shardName>).
Expected observationHeader shows projection name, lifecycle (Async), shard name (Trip:All). Below: current sequence, owning store's HWM, gap (events behind), state badge (Updated), agent status pill, last-advanced and last-heartbeat timestamps. Sparkline / sequence history chart populates from the rolling shardHistory buffer (last ~100 snapshots).
How to verifyHeader text matches via .el-card__header:has-text("TripProjection"). The gap value should equal hwm - state.sequence (positive while TripPublisher is generating events; trends toward 0 when the publisher pauses). Cross-check GET /api/critterwatch/service-summaries/TripServiceshardStatesByStore[<storeUri>][<shardName>] matches what's rendered.

V-3 — Projection detail Dead Letter Events section is policy-aware (PS#3)

FieldValue
SetupSame as V-2, but click into an Incidents.Service projection (Trips publisher doesn't drive Incidents; you may need to drop a manual IncidentLogged via IncidentSamplePublisher first, or use the existing Incidents publisher which fires immediately on boot).
ActionOpen the Apply Errors card on the Projection Detail page.
Expected observationOne of three states based on the monitored service's Options.Projections.Errors.SkipApplyErrors policy:
1. skipApplyErrors === true (default — Trips, TeleHealth, MultiStoreHost-Incidents) — current section: failures route to Wolverine's DLQ, View Related Dead Letters button visible.
2. skipApplyErrors === false (Incidents.Service after #316 lands the stop-on-error config) — el-alert warning Shard halts on apply error, no DLQ button, restart/rebuild guidance pointer.
3. undefined (older monitored service that hasn't bumped to JasperFx.Events 2.9.2+ / Marten 9.7.1+ / Polecat 4.4.1+) — soft-warn copy that doesn't presume DLQ routing.
How to verifydata-testid selectors: apply-error-policy-skip, apply-error-policy-stop, or apply-error-policy-unknown. The descriptor on the wire is at service.eventStores[storeIndex].projectionErrors.skipApplyErrors. PS#3 frontend test in src/FrontEnd/src/stores/__tests__/projections-store.test.ts covers the three branches in isolation.

V-4 — Subscription detail page renders separately from projection detail

FieldValue
SetupFull scenario. Pick a service that registers an ISubscription (vs IProjection) — currently the Trip family doesn't register subscriptions explicitly; this scenario is partially gated by sample availability. If no subscription is registered, document that the Subscriptions tab on the service detail page is empty rather than missing.
ActionNavigate to a subscription's detail page once one exists.
Expected observationLayout mirrors Projection Detail but the action set is constrained — no Rebuild (subscriptions can't be rebuilt the same way, only rewound to a sequence/timestamp). Surface: current sequence, lag against HWM, state, Rewind Subscription action.
How to verifyURL pattern /subscription/<serviceId>/<shardName>. The Rewind action's modal is the same component used on the projection detail page for rewinds (see RewindSubscription message).
StatusGated on a sample that registers a subscription (vs a projection) — likely a future-task surface; flag as such.

V-5 — Per-store filtering / drill-in (PS#2 fix)

FieldValue
SetupRun BffHost with CRITTERWATCH_SCENARIO=multi-store (the dedicated MultiStoreHost profile — AncillaryOnly mode, no main store). Wait for MultiStoreHost and MultiStorePublisher to appear in the Aspire dashboard.
ActionOn the Projections page, open the Service filter dropdown and pick MultiStoreHost. Inspect the HWM rows that render.
Expected observationThree HWM rows — one per ancillary store (Trips, Incidents, Telehealth) — each with their own sequence number drawn from ShardStatesByStore[storeUri][HighWaterMark]. Below them, projection rows for Itinerary, IncidentsByCategory, TelehealthComposite. Each projection's Behind column is computed against its own store's HWM — the Itinerary projection (on the Trips store) doesn't appear thousands of events behind just because the TelehealthComposite store's HWM is way ahead.
How to verifyUI: three distinct el-table__row:has-text("HighWaterMark") rows when filtered to MultiStoreHost. Each projection row's gap number can be reproduced by hwm_for_its_store - projection_sequence (positive and small under steady-state publisher traffic). API: GET /api/critterwatch/service-summaries/MultiStoreHostshardStatesByStore keyed by 3 store URIs with HighWaterMark entries each at distinct sequence numbers.

Cross-reference

  • PS#2 closure: #329 (wire fix), #333 (frontend selectors), #334 (ProjectionsPage render + Playwright E2E).
  • PS#3 closure: #326 (UI), #327 (pin bumps).

Released under the MIT License.