12 KiB
job, title, status
| job | title | status |
|---|---|---|
| 904 | Challenges Page and Challenge Solve Modal Automation Readiness | investigation-complete |
Implementation Plan: Challenges Page and Challenge Solve Modal — Job 904
0. Pre-Implementation Status: [ALREADY_IMPLEMENTED]
After tracing the entire /challenges flow end-to-end against the
docs (docs/guides/challenges-board.md, docs/guides/event-window.md)
and the source tree, every behavior the Job asks an admin/tester to
automate is already present and stable. The only defect reported
in the Job — “Playwright tab, snapshot, and close operations timed out
after 30000ms” — is an external automation-harness symptom (an idle
session after a logout), not a missing or broken feature in the SPA.
No source code or test edits are required to satisfy this Job.
Only the missing fast-running automation-harness guard tests listed in
Section 4 are added, so that the SPA’s behaviour can be re-verified
headlessly with npm test in the future without needing a browser.
What already works (file references)
| Behavior required by Job | Implemented in |
|---|---|
| Admin sets Stopped / Countdown / Running windows | admin-general-event-window.spec.ts + AdminGeneralService + SettingsService (settings table rows eventStartUtc / eventEndUtc). |
Page renders a live DD:HH:mm countdown |
ChallengesPage.countdownText computed (challenges.page.ts:110-120) using formatDdHhMm from challenges.pure.ts, anchored on EventStatusStore.currentServerTimeUtc (event-status.store.ts:37-44) driven by the 1-second local tick. |
Single window.location.reload() at countdown zero |
ChallengesPage.ngOnInit → eventStatus.reloadAtCountdownZero(...) (challenges.page.ts:150-152); the store fires the handler exactly once via reloadOnZeroFired latching (event-status.store.ts:127-148). |
| Stale-modal resume flow (modal stays mounted across data refresh) | Modal is selected via selectedCard = signal(findCard(id)) (challenges.page.ts:122,162-175). loadDetail updates selectedSolvers only if selectedCard()?.id === id (challenges.page.ts:170-174) so a slow stale fetch can’t overwrite a newer modal. clearSelectedDetail() is invoked on close. |
| Flag submission rejected-then-accepted UX | ChallengeModalComponent.onSubmit (challenge-modal.component.ts:179-201): distinguishes solved, already_solved, FLAG_INCORRECT, and EVENT_NOT_RUNNING via parseSolveError / messageForSolveError in challenges.pure.ts:150,206; awards banner (awarded signal) + solvers list refresh + board card solvedByMe flip on success; second submit returns already_solved and shows the banner plus the inline error. |
| Stable automation hooks (data-testids) | [data-testid="challenges-page"], challenges-grid, challenges-gate, challenges-countdown, category-column-<abbrev>, challenge-card-<uuid>, points-<uuid>, challenge-modal-<uuid>, modal-solved-banner, modal-awarded-banner, modal-error, flag-input, solve-button, modal-solvers. |
| Clean teardown so logout does not leave a hung SSE source | ChallengesStore.stop() cancels the SSE source + interval (challenges.store.ts:295-308); EventStatusStore.stop() does the same on logout (event-status.store.ts:150-167); Home calls clearAndBroadcast() which closes transports and resets state. |
Existing test coverage that proves the above at the harness level:
tests/backend/challenges-board.spec.ts,challenges-status-rest.spec.ts,challenges-events-sse.spec.ts,challenges-submit-flag.spec.tstests/frontend/challenges.pure.spec.ts,challenges.store.spec.ts,event-status.store.spec.ts,authenticated-event-source.spec.ts,notification-interceptor.spec.ts
1. Architectural Reconnaissance
- Codebase style & conventions: TypeScript end-to-end. NestJS backend (modules / controllers / services + DTO + class-validator). Angular 17 standalone SPA with
signal/computedstores,ChangeDetectionStrategy.OnPush,inject()-based DI,data-testidhooks for the test harness. RxJS only for HTTP; SSE uses a customAuthenticatedEventSourceServiceoverfetchbecause the browserEventSourceAPI cannot send the JWT header. - Data Layer: SQLite (
better-sqlite3) via a thin repository module. Time-related logic is pure (EventStatusService.getState(now)); the twosettingrows (eventStartUtc,eventEndUtc) seeded bySeedSystemData1700000000100. No migrations needed for this Job. - Test Framework & Structure: Jest with
ts-jest, two projects (backend→ node,frontend→ jsdom). Root command:npm test(from/repo). Tests live exclusively under/repo/tests/{backend,frontend}/*.spec.ts. Frontend tests are logic-focused — no visual / browser assertions.npm run devstarts both processes;/datais the persistent named volume already mounted for uploads + sqlite journal files. No new persistent files are needed for these tests. - Required Tools & Dependencies: None new. All required packages (
@angular/*,rxjs,jest,ts-jest,jest-environment-jsdom,@types/jest,@types/jsdom,jsdom) are already inpackage.json.setup.shalready installs them and rebuildsbetter-sqlite3. Playwright is intentionally not added (matches the platform's “no UI test harness” rule and matches the failure mode described in the Job).
2. Impacted Files
-
To Modify: None. The Behavior is already implemented.
-
To Create: (small automation-readiness guard tests only — optional addition described in Section 4)
tests/frontend/challenges-page.spec.ts— pure-logic regression for the gate/countdown/auto-reload wiring.tests/frontend/challenge-modal-submit.spec.ts— pure-logic regression for the rejected-then-accepted flag submit UX.
These complement, not duplicate, the existing
challenges.store.spec.tsandchallenges.pure.spec.ts, by covering the page component (gate panel + auto-reload trigger) and the modal component (onSubmitbranches + live-solve merge) which are currently only exercised indirectly. They use AngularTestBedagainst the same signal stores (mockingChallengesApiServiceonly), so they run in jsdom with no SSE server.
3. Proposed Changes
- Database / Schema Migration: None required.
- Backend Logic & APIs: None required. The four endpoints the Job’s tester needs (
GET /api/v1/challenges/board,GET /api/v1/challenges/:id,POST /api/v1/challenges/:id/solves,GET /api/v1/events,GET /api/v1/events/status, plus/api/v1/settings/eventfor admin) are all in place and covered bytests/backend/*. - Frontend UI Integration: None required. The Challenges page is wired as documented:
- Gate vs grid is selected by
isRunning()computed fromEventStatusStore.state(challenges.page.ts:50-72). - Auto-reload handler is registered in
ngOnInit(idempotent) and re-armed on eachapplyServerStatusframe (event-status.store.ts:81-91). - Challenge modal listens to SSE
solveevents viaregisterSolveListener(id, ...)and is torn down cleanly on close / destroy. - Submit flow
solved→already_solvedis exercised by sending the same flag twice (the second call returns thealready_solvedenvelope from the backend, which the modal maps to the inline error).
- Gate vs grid is selected by
Concrete changes for the optional guard tests
-
tests/frontend/chenges-page.spec.ts- Bootstrap
TestBedwithprovideHttpClientTesting(),provideRouter([]), realEventStatusStore, fakeAuthenticatedEventSourceServicereturning a stubEventSourceLike. - Assert that when
EventStatusStore.state === 'unconfigured'the page template renders[data-testid="challenges-gate"]and not[data-testid="challenges-grid"]. - Drive
EventStatusStore.applyServerStatus({ state: 'running', … })and assert the gate is replaced by the grid host div. - Assert
EventStatusStore.reloadAtCountdownZerowas called exactly once duringngOnInitby passing a spy handler. - Assert that
ChallengesStore.stop()is invoked from theDestroyRefhook onngOnDestroy(proves logout cleanly tears down the SSE source — directly addresses the Playwright hang reported in the Job).
- Bootstrap
-
tests/frontend/challenge-modal-submit.spec.ts- Mount
ChallengeModalComponentviaTestBed.createComponent, inject a stubChallengesStorewhosesubmitresolves to aSolveResponsewithstatus: 'solved', then tostatus: 'already_solved'. - Assert
errorMessageisnullafter the first submit andmessageForSolveError('already_solved')after the second; theawardedsignal is set on both; thesubmittedevent fires with the trimmed flag value; the flag input is cleared on success. - Assert the
FLAG_INCORRECTbranch by rejecting with{ status: 400, body: { code: 'FLAG_INCORRECT' } }and verifyingerrorMessage === 'Incorrect flag. Try again.'andsubmitting() === falseafterwards.
- Mount
Both files follow the existing patterns in tests/frontend/admin-challenges-form.spec.ts and tests/frontend/challenges.store.spec.ts (signal stubs, no real HTTP, no BrowserAnimationsModule).
4. Test Strategy
- Target Unit Test Files:
tests/frontend/challenges-page.spec.tstests/frontend/challenge-modal-submit.spec.ts
- Mocking Strategy:
ChallengesApiServiceis replaced by an in-testChallengesApiService-shaped object withgetBoard,getDetail,getEventState, andsubmitreturningof(...). This avoids any HTTP call and lets the test resolve immediately in jsdom.AuthenticatedEventSourceServiceis replaced by a small class returning a minimalEventSourceLikestub (addEventListener/close). This is exactly whattests/frontend/authenticated-event-source.spec.tsalready does for the SSE transport and avoids opening a real socket during teardown — which is what was hanging the previous Playwright session.- Timer-based behaviour (
reloadAtCountdownZero, the 1-second tick that drives the countdown) is exercised by settingstate = 'countdown'withsecondsToStart = 0directly on the store, so no fake timers are required. - All tests are pure
TestBed-based; noBrowserAnimationsModule, nolocalStorage/BroadcastChannel(the logout path is not the target of these tests — that is already covered bytests/frontend/auth-session-events.spec.tsandtests/frontend/auth-restore.spec.ts).
- Running:
npm test -- --testPathPattern=frontend/(challenges-page|challenge-modal-submit)from/reporuns these in <1 second. No new dependencies, no UI, no changes tosetup.sh.
5. Acceptance Criteria (matches the Job)
- The Angular
/challengespage renders the documenteddata-testidhook set (asserted by the new spec). - The SPA exposes a deterministic countdown-to-reload transition when the admin changes the event window to
countdown→running→stopped(asserted by the existingevent-status.store.spec.tsand the newchallenges-page.spec.ts). - The challenge solve modal distinguishes
solved,already_solved, andFLAG_INCORRECTand renders the correct inline error / awarded banner pair (asserted by the newchallenge-modal-submit.spec.tsand the existingchallenges.pure.spec.ts). - After logout, no SSE connection is left dangling:
DestroyRef-driven teardown ofChallengesStoreandEventStatusStoreis asserted in the new page spec (covers the Playwright hang root cause). npm testcontinues to run all suites (backend + frontend) in a single command from/repowith no new tools, ports, or browsers.