Hardening the teaching assessment¶
Lock down the assessment experience so candidates can't navigate away, access the sidebar, or use browser back/forward during an active exam. Keep the TopRibbon but disable all navigation elements. Add a "close exam early" button that submits partial results.
Phase 1 — Exam layout lockdown (ribbon + nav suppression)¶
- Add
examModeprop toMainLayout— when true: hide burger button, hide SideNav (desktop), disable NavigationDrawer (mobile), hide search/patient info. Show only Quill branding in TopRibbon. - Add
examModeprop toTopRibbon— when true: hide burger, patient info, search. Show only QuillName logo. - Thread
examModethroughLayoutCtx—LayoutCtx(React Router outlet context) already provides shared state betweenRootLayoutand child pages (used forpatient/setPatient). AddexamMode/setExamModeto it.AssessmentAttemptcallssetExamMode(true)on mount (andfalseon unmount).RootLayoutreadsexamModeand passes it as a prop toMainLayout, which passes it toTopRibbon. Data flow:AssessmentAttempt→setExamMode(true)→RootLayoutre-renders →MainLayout examMode={true}→TopRibbon examMode={true}hides nav.
Files¶
frontend/src/components/layouts/MainLayout.tsx— addexamModepropfrontend/src/components/ribbon/TopRibbon.tsx— addexamModepropfrontend/src/RootLayout.tsx— threadexamModethroughLayoutCtx
Phase 2 — Block browser back/forward navigation¶
- Add
useBlockertoAssessmentAttempt— block all React Router navigation during active exam. Show a confirmation modal (reusing theDirtyFormNavigationpattern): "You have an active exam. Are you sure you want to leave?" Proceeding triggers early submission. - Add
beforeunloadhandler — prevents browser tab close/refresh. Browser shows native "Leave site?" dialog. Standardevent.preventDefault()pattern. - Add
popstateinterception — push a dummy history entry on exam start, intercept browser back viapopstatelistener. Re-pushes entry to stay on page. This covers the native browser back button whichuseBlockeralone doesn't fully handle.
Files¶
frontend/src/features/teaching/pages/AssessmentAttempt.tsx— adduseBlocker,beforeunload,popstatefrontend/src/components/warnings/DirtyFormNavigation.tsx— reuse pattern for exam navigation warning
Phase 3 — Close exam early component + flow¶
- Create
ExamCloseButtoncomponent — button + confirmation modal: "Are you sure you want to end this exam early? Unanswered questions will be marked as incorrect." Actions: "Continue exam" / "End exam". New component atcomponents/teaching/exam-close-button/. - Wire into QuestionView next to the timer — the timer is already fixed-position top-right. Place the "End exam" button alongside it in the same exam toolbar area. This keeps exam controls together and avoids threading callbacks through the layout chain.
QuestionViewalready receivesonExpire— add anonCloseExamprop.AssessmentAttemptprovides the handler that callsPOST /complete. - Backend: verify early completion — the existing
POST /completeshould already handle partial submissions (scores whatever is answered, unanswered = incorrect). Verify and fix if needed. Likely no changes.
Files¶
frontend/src/components/teaching/exam-close-button/— new component (button + confirmation modal)frontend/src/components/teaching/question-view/QuestionView.tsx— renderExamCloseButtonnext to timerfrontend/src/features/teaching/pages/AssessmentAttempt.tsx— provideonCloseExamhandlerbackend/app/features/teaching/router.py— verify early completion scoring
Phase 4 — Fullscreen (discussion)¶
Fullscreen is not reliably enforceable:
- Desktop:
requestFullscreen()works but user can exit with Esc/F11 at any time. Browsers explicitly prevent trapping users. - iOS Safari: No Fullscreen API support at all. Only PWA mode is fullscreen.
- Android Chrome: API works but same Esc-to-exit applies. Address bar reappears on scroll.
- We could detect fullscreen exit via
fullscreenchangeand show a "please return to fullscreen" prompt, but we cannot force it. This would require storing a "left fullscreen" event for audit purposes.
Recommendation: Don't use Fullscreen API. The focused layout from Phase 1 with nav disabled is the practical approach. If actual exam integrity enforcement is needed later (proctoring), that's a fundamentally different architecture (camera monitoring, screen recording, lockdown browser integration).
Verification¶
npx tsc --noEmit -p tsconfig.check.json— zero errors- Frontend tests:
docker exec quill_frontend sh -lc "yarn unit-test:run" - Backend tests:
docker exec quill_backend sh -lc "pytest -q -m 'not integration'" - Storybook build:
docker exec quill_frontend sh -lc "yarn storybook:build" - Manual: start exam → burger gone, sidebar hidden, browser back blocked, tab close shows warning, "End exam" submits partial results
- Manual mobile: only logo in ribbon, no drawer accessible, close button next to timer
Decisions¶
- Keep TopRibbon — disable nav elements within it rather than replacing layout
- Close early = submit partial — unanswered items scored as incorrect
- Fullscreen: recommend against, discuss further
Out of scope¶
- Proctoring (camera, screen recording)
- Exam PIN / password entry
- IP restrictions
- Multiple-tab detection
- Keyboard shortcut interception (browsers don't allow overriding Ctrl+W)