This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
BrowserTest (internally "Sonar Quiz System") is an offline-first interactive quiz and analysis platform that progressively enhances DITA-published HTML training materials. The system operates entirely from file:// URLs with no network dependencies, targeting air-gapped training environments.
Current Phase: Phase 1 Complete (Foundation Layer) → Starting Phase 2 (Components & Integration)
The system enhances existing DITA HTML without modification through DOM upgrades:
- Detect tables with specific classes (
qd-quiz,qd-analysis) - Upgrade in-place with interactive controls
- Inject Lit 3 custom elements for UI overlays (
<qd-login>,<qd-status>,<qd-instructor>) - Graceful degradation if JavaScript disabled
User Input → DOM Handler → Service Layer → Storage Adapter
↓ ↓
Visual Update IndexedDB/SessionStorage
Storage Strategy:
- IndexedDB: Primary persistence with composite keys
qd/{release}/u{serviceId}- Database name:
BrowserTest(defined insrc/services/storage/indexeddb.ts) - Object stores:
students,backups
- Database name:
- sessionStorage: Active session + R/A/G cache (expires 30 min)
- No network: All data remains local, no telemetry/CDN/remote config
- DOM Enhancement (
src/enhancers/): Detect and upgrade DITA tables - Service Layer (
src/services/): Business logic, state management, event coordination - Component Layer (
src/components/): Lit 3 Web Components with Shadow DOM - Storage Layer (
src/services/storage/): IndexedDB adapter with atomic transactions
Compact login interface with modal for instructor access:
- Default View: Student login form (serviceId + name fields, Login + Instructor buttons)
- Instructor Modal: Password entry popup (opens on "Instructor" button click)
- Release ID Extraction: CRITICAL - Release ID comes from
.wh_publication_title .titleelement in the document DOM, NOT from any form input- DOM selector:
document.querySelector('.wh_publication_title .title') - Example HTML structure required:
<div class="wh_publication_title"> <span class="title">TRV Connectors Autumn 2025</span> </div>
- If missing, login will fail with error: "Release not found (missing .wh_publication_title .title element)"
- DOM selector:
- Behavior:
- Student login: Validates, creates session, shows student status panel
- Instructor login: Opens modal, validates password (SHA-256 + rate limiting), shows instructor status panel
- Integrates with SessionService and instructor password verification
- Emits
qd:loginevent on successful authentication
After successful login, ONE of these panels is displayed:
Student Status Panel (<qd-student-status>):
- Quiz progress display (R/A/G badges, answered/correct counts, percentage)
- Logout button
- Updates in real-time via session cache
Instructor Status Panel (<qd-instructor-status>):
Consolidated control center with:
- Toggle: Show/hide student answers on current page
- For quiz tables: Inserts
<div>after each question's input control - Shows list of students (name + last 4 digits of serviceId)
- Each student entry shows: answer + shortened timestamp
- Red/green color coding for incorrect/correct answers
- For analysis tables: Shows student entries in comparison format
- For quiz tables: Inserts
- Button: "View All Scores" → Opens
<qd-scores-modal> - Button: "Export to CSV" → Downloads detailed answer data
- Button: "Erase All Data" → Clears all storage (with confirmation dialog)
- Button: "Logout"
Modal overlay (Esc to close) showing:
- Student list with totals (attempted, correct, percentage)
- Per-student expandable breakdown showing per-page answers
- Close button
Quiz Tables (qd-quiz class):
- SECURITY: Immediately extract and remove answer column from DOM on load
- Correct answers stored in memory only (prevents view-source inspection)
- Render MCQ radio buttons or numeric input fields
- Submit answers, validate against in-memory answers
- Save to IndexedDB via storage adapter
- If instructor mode active: Show student answers after each question
Analysis Tables (qd-analysis class):
- Make cells with class="interactive" editable (contenteditable)
- Auto-save to IndexedDB with debouncing
- If instructor mode active: Show student entries
- R/A/G badges injected into navigation links (class
quizPageBtn) - Real-time updates from session cache
- Listen to
qd:answer-savedevents for immediate refresh
# Development
npm run dev # Start dev server with HMR
npm run storybook # Component development in isolation
# Testing (TDD mandatory)
npm test # Run all tests
npm run test:unit # Vitest unit tests
npm run test:integration # DOM upgrade integration tests
npm run test:e2e # Playwright E2E tests (auto-starts/stops Storybook)
npm run chromatic # Visual regression tests
# Coverage & Gap Analysis
npm run test:coverage # Unit tests with v8 coverage report
npm run test:coverage:all # Unit + integration coverage (separate reports)
npm run test:gaps # Structural gap analysis (files without tests)
npm run analyze:e2e-gaps # E2E feature coverage report (docs/test-coverage-report.md)
# Building
npm run build # Production build (IIFE + ESM)
npm run build:dita # Build + copy bundle to DITA directories
npm run update-dita-demo # Sync dita/out/oxygen → dita-demo/ (for PR previews)
npm run lint # TypeScript + ESLint checks
npm run format:check # Prettier formatting verification
# Size verification
npm run size-check # Verify bundle <40KB min+gzipBranch-based: Both workflows push to gh-pages branch, GitHub Pages serves from there.
Live at: https://DeepBlueCLtd.github.io/BrowserTest/main/
- Deploys demo files to
main/folder ingh-pagesbranch - Auto-deploys on every push to
main - Workflow:
.github/workflows/pages.yml - Content: demo HTML files + dist/ bundle
Each PR automatically gets a preview deployment:
- DITA WebHelp:
https://DeepBlueCLtd.github.io/BrowserTest/pr-{number}/ - Storybook:
https://DeepBlueCLtd.github.io/BrowserTest/pr-{number}/storybook/ - Auto-posted: PR comment includes live preview links
- Auto-cleanup: Preview deleted when PR closes/merges
- Build policy: Requires successful compilation, ignores test failures
- Workflow:
.github/workflows/pr-preview.yml - Content: DITA output + Storybook static build
Prerequisites:
- GitHub Pages enabled with "Deploy from a branch" →
gh-pagesbranch (Settings → Pages) - Workflow permissions: "Read and write" (Settings → Actions → General → Workflow permissions)
dita-demo/directory populated (runnpm run update-dita-demoafter DITA builds)
DITA Demo Sync:
dita-demo/is version-controlled and deployed by CI- After building DITA output:
npm run update-dita-demoto syncdita/out/oxygen/→dita-demo/ - Commit updated
dita-demo/before pushing PR for preview deployment
Path structure on gh-pages branch:
gh-pages/
├── main/ # Production demo (main branch)
│ ├── quiz-index.html
│ ├── dist/
│ └── ...
├── pr-123/ # PR #123 preview
│ ├── oxygen-webhelp/ # DITA output (from dita-demo/)
│ └── storybook/ # Storybook static build
├── pr-124/ # PR #124 preview
│ ├── oxygen-webhelp/
│ └── storybook/
...
The demo/ directory contains standalone HTML files for manual testing and demonstration:
- demo/quiz-index.html: Index page with login UI, status panel, and navigation with R/A/G badges
- demo/quiz-examples.html: Interactive quiz tables (MCQ and numeric questions)
- demo/analysis-examples.html: Editable analysis tables for free-form student work
# 1. Build the bundle
npm run build
# 2. Test via file:// protocol (recommended for offline testing)
open demo/quiz-index.html
# 3. Or serve via HTTP for full IndexedDB support
python3 -m http.server 8000
# Visit: http://localhost:8000/demo/quiz-index.htmlAll demo files load the built bundle from dist/sonar-quiz.iife.js with configuration provided via hidden <span> elements. Debug mode is controlled by DEBUG_MODE constant in src/index.ts. See demo/README.md for:
- Detailed test scenarios (login, quiz interaction, analysis tables, session management)
- Browser DevTools inspection tips (IndexedDB, sessionStorage, custom events)
- Configuration examples using hidden span elements
- Troubleshooting common issues
- E2E testing integration guidance
CRITICAL: Before marking ANY task as complete, ALL of the following must pass with ZERO errors:
# 1. TypeScript type checking (MUST pass)
npm run typecheck
# 2. Linter (zero errors required, warnings acceptable with justification)
npm run lint
# 3. Unit tests (all must pass, zero failures)
npm run test:unit
# 4. Integration tests (if applicable to the task)
npm run test:integration
# 5. Format check (code must be properly formatted)
npm run format:check
# 6. Build (final verification before commit)
npm run buildNo Exceptions:
- If ANY check fails, the task is NOT complete
- Fix all errors before committing
- Do not skip tests to achieve project goals (Constitution III)
- All code must be properly typed (no
anywithout eslint-disable comment) - All event handlers must use arrow functions or explicit
.bind()to avoid unbound method errors
Post-Implementation Checklist:
- All tests passing (green)
- Linter clean (zero errors)
- Build successful
- Bundle size within limits (<40KB gzipped)
- Code committed with descriptive message
- MUST work completely offline from
file://URLs - No network dependencies, telemetry, CDN, or remote config
- All features functional on air-gapped systems
- MUST enhance existing DITA HTML without breaking original functionality
- Zero impact to author workflow
- Graceful degradation without JavaScript
- TDD is MANDATORY: Tests → Review → Red → Green → Refactor
- Exit gates require: unit tests, integration tests, visual regression, E2E
- No skipping tests to achieve project goals
- 8-phase delivery with explicit exit criteria
- No phase starts until previous gate satisfied
- Each phase delivers independently testable value
- Bundle: ≤40KB min+gzip IIFE
- Operations: <200ms save, <2s page load (50 questions)
- Shadow DOM for isolation, no global CSS pollution
- Composite keys:
qd/{release}/u{serviceId} - 30-minute session timeout
- Complete data erasure capability for cohort reset
- Single
<script defer src="sonar-quiz.iife.js"></script>tag in DITA template (no attributes required) - Auto-init on DOMContentLoaded (always runs when script loads)
- Configuration via hidden
<span>elements injected by DITA/Oxygen transform:#qd-status-container: CSS selector for status panel (default:.wh_top_menu_and_indexterms_link)#qd-title-selector: CSS selector for publication title/Release ID (default:.wh_publication_title .title)#qd-db-name: IndexedDB database name (REQUIRED - no default, throws error if missing/empty)#qd-instructor-hash: SHA-256 hash of instructor password (optional)
- Debug mode controlled by
DEBUG_MODEconstant insrc/index.ts(single toggle point) - No setup, no manual config, no network dependencies
Example HTML Structure (injected by DITA/Oxygen transform):
<body>
<!-- Configuration spans (hidden, injected by Oxygen XSL transform) -->
<span id="qd-status-container" style="display:none;">.wh_top_menu_and_indexterms_link</span>
<span id="qd-title-selector" style="display:none;">.wh_publication_title .title</span>
<span id="qd-db-name" style="display:none;">BrowserTestDB</span>
<span id="qd-instructor-hash" style="display:none;">5e884898da28...</span>
<!-- Page content -->
<div id="page-content">
<nav class="wh_top_menu_and_indexterms_link">
<div class="wh_publication_title">
<span class="title">TRV Connectors Autumn 2025</span>
</div>
</nav>
<!-- ... rest of content ... -->
</div>
<!-- Script tag (no attributes required) -->
<script defer src="sonar-quiz.iife.js"></script>
</body>- CRITICAL: Quiz answers MUST be removed from DOM immediately on component load
- Correct answers extracted, stored in memory (JavaScript closure), never exposed in DOM
- This prevents students from viewing page source or using DevTools to see answers
- Implementation order: Parse → Store in memory → Remove from DOM → Render controls
- Validation performed against in-memory answers only
DO NOT MODIFY without version bump and migration strategy.
// Identity
type ReleaseId = string; // "MM-YYYY"
type ServiceId = string; // e.g. "RN2344"
type PageId = string; // e.g. "gram-1"
type TableId = string; // 16-char hash from structure
type CellKey = string; // "R{row}C{col}#f:{hash}" (8-char content hash)
// States
type CompletionState = 'unstarted' | 'incomplete' | 'complete';
type QuestionKind = 'mcq' | 'numeric';
// Core entities
interface AnswerRecord {
answer: string; // User's answer
success: boolean; // Correctness
timestamp: string; // ISO 8601 (MANDATORY)
}
interface StudentRecord {
schema: number;
serviceId: ServiceId;
name: string;
release: ReleaseId;
attempted: number;
correct: number;
pages: Record<PageId, PageData>;
}Content authors must follow these rules (runtime validation enforces):
- Exactly 3 columns: Question | Answer | Detail
- Class:
qd-quiz - MCQ: Use
<ol>lists (1-indexed, first option = 1) - Numeric: Tolerance in third column
- Maximum ONE quiz table per page
- Class:
qd-analysis - Cells WITH class="interactive" = editable (in interactive mode)
- Cells WITHOUT 'interactive' class = read-only (always)
- Maximum ONE analysis table per page
- Status panel: Auto-injected as last child of configured navbar container
- Default:
.wh_top_menu_and_indexterms_link(Oxygen WebHelp) - Configure via hidden
<span id="qd-status-container">selector</span>element - Not logged in: Shows login form within status panel
- Logged in: Shows quiz progress (R/A/G state, counts, percentage)
- Logout: Button at bottom-right clears sessionStorage and shows login form
- Default:
- Navigation links: Add class
quizPageBtnfor R/A/G badges - Publication title: Must have element matching selector in
<span id="qd-title-selector">(default:.wh_publication_title .title) for Release ID extraction
unstarted → No answers provided
incomplete → Some answered OR any incorrect
complete → All answered AND all correct
- SessionData stored in sessionStorage with serviceId/release duplicated for quick access
- SessionCache rebuilt from IndexedDB on login
- Auto-logout after 30 minutes inactivity
Format: 16-character hash derived from table structure
- Based on:
{rows}x{cols}:{className} - Example: "8e2b4a1c9f3d7b6e"
- Used to uniquely identify analysis tables
Format: R{row}C{col}#f:{hash}
- Hash: 8-character hash from normalized content (whitespace collapsed)
- Unique identifier for analysis table cells
- Example: "R2C4#f:abc123de"
- Write test(s) covering new functionality
- Review tests for correctness
- Confirm tests FAIL (red)
- Implement minimal code to pass (green)
- Refactor while keeping tests green
- Before committing: Run
npm run format:checkandnpm run lintto verify code quality
Before committing any code changes, ALL of the following MUST pass:
- ✅ Type checking passes:
npm run typecheck(fast TypeScript type checking) - ✅ Tests pass:
npm run test:unit(andnpm run test:integrationif applicable) - ✅ Linting passes:
npm run lint(fix withnpm run lint:fixif needed) - ✅ Formatting passes:
npm run format:check(fix withnpm run formatif needed) - ✅ Build succeeds:
npm run build(if modifying source files) - ✅ Bundle size: Under 40KB min+gzip (verify with
npm run size-checkif needed)
Rationale: CI will fail if any of these checks fail. Running them locally before committing prevents failed CI builds and reduces feedback cycles.
- Phase 0: Contracts published, Storybook renders, CI green
- Phase 1: Chromatic interactions pass, parsing unit tests
- Phase 2: Visual baselines stable, cell mapping tests
- Phase 3: A11y checks pass, event emission verified
- Phase 4: Session switch tests, expiry unit tests
- Phase 5: E2E file:// saves/reloads, CSV validation
- Phase 6: Perf/a11y green, <40KB budget met
E2E tests run via Playwright against Storybook stories at http://localhost:6006.
Key Configuration (playwright.config.ts):
- Auto-start/stop: Playwright automatically starts Storybook before tests and kills it on completion
- Storybook startup timeout: 12 seconds (Storybook reliably starts within 10s)
- Test timeout: 2 seconds max per test (local SPA, no network delays)
- Action/navigation timeout: 2 seconds max (clicks, fills, page.goto)
- Expect timeout: 2 seconds max for assertions
- Reuse existing server: If Storybook already running locally, tests will use it instead of starting new instance
Usage:
npm run test:e2e # Just run tests - Storybook auto-managed
npm run test:e2e:headed # Run with visible browser
npm run test:e2e:debug # Debug mode with Playwright InspectorNo manual Storybook management required - the test runner handles lifecycle automatically.
Custom events use qd: namespace:
qd:login- User logged inqd:logout- User logged outqd:answer-saved- Answer persistedqd:state-changed- Page completion state updatedqd:instructor-unlock- Instructor mode activatedqd:data-cleared- All data erased
Target: Chrome/Edge ≥96, Firefox ≥102
File Protocol: Must work from file:// URLs (no dynamic imports in IIFE)
CSP: Avoid eval, work with default Oxygen output
Accessibility: WCAG 2.1 Level AA required
Enable via data-qd-debug attribute on quiz/analysis tables:
- Console logs for state transitions
- On-page diagnostics (keys, page states)
- Production mode: silent, only validation banners
- System_Requirements.md: Functional requirements, data model, authoring rules
- Technical_Design.md: Architecture, packaging, integration patterns
- ARCHITECTURE_FLOWS.md: Event flows, login processes, DOM patterns, service interactions
- Contracts.md: Frozen types and interfaces
- Delivery_Plan.md: 8-phase plan with exit gates
- demo/README.md: Demo HTML files, testing workflows, troubleshooting guide
- specs/001-sonar-quiz-system/: Feature spec, plan, data model, contracts
CRITICAL: The ReleaseId is ALWAYS extracted from the DOM, never from user input.
// CORRECT: Extract from DOM
const titleElement = document.querySelector('.wh_publication_title .title');
const release = titleElement?.textContent?.trim() || '';
// WRONG: There is NO release input field in forms
// const release = form.elements.release.value; // ❌ DOES NOT EXISTRequired DOM Structure (added by DITA publishing):
<div class="wh_publication_title">
<span class="title">TRV Connectors Autumn 2025</span>
</div>Testing/Storybook: All stories MUST inject this element into the DOM before rendering login components.
function getStorageKey(release: ReleaseId, serviceId: ServiceId): string {
return `qd/${release}/u${serviceId}`;
}- MCQ: Single letter a-z
- Numeric: Valid number string within tolerance
- Both: Non-empty answer required
30 minutes from lastActivity timestamp, checked on every user interaction
- IIFE:
dist/sonar-quiz.iife.js(globalwindow.SonarQuiz, auto-init) - ESM:
dist/sonar-quiz.esm.js(for integrators) - Size limit: ≤40KB min+gzip for IIFE
- Source maps: Generated for debugging
- TypeScript definitions: For ESM consumers
- TypeScript 5.x / JavaScript ES2020+ + Lit 3.0 (Web Components), Vite 5.x (build), Vitest (testing) (001-security-refactor)
- IndexedDB (primary), sessionStorage (active session) (001-security-refactor)
- IndexedDB (primary), sessionStorage (rate limiting state) (004-student-pin-auth)
- N/A (no data model changes) (005-code-reduction)
- TypeScript 5.x / Node.js 18+ + @vitest/coverage-v8 (to install), Vitest 2.x, Playwright 1.x (existing) (006-test-coverage-gaps)
- N/A (development tooling - outputs to coverage/ directory and markdown reports) (006-test-coverage-gaps)
- TypeScript 5.x / ES2020+ + Lit 3.x (existing), Vitest 2.x (existing) (007-lit-component-refactor)
- N/A (no data model changes—internal refactor only) (007-lit-component-refactor)
- TypeScript 5.x / ES2020+ with Lit 3.x + Lit 3.0 (Web Components), existing qd-modal base componen (008-user-guidance-popups)
- N/A (no data persistence - content from DITA config only) (008-user-guidance-popups)
- TypeScript 5.x / ES2020+ with Lit 3.0 (Web Components) + Existing IndexedDBStorageAdapter (
src/services/storage/indexeddb.ts) (009-encrypt-stored-data) - IndexedDB (primary) - obfuscation at adapter layer; sessionStorage unchanged (009-encrypt-stored-data)
- CSS3 + TypeScript 5.x (for JS integration) + Existing DITA template CSS (
f13ldman.css), Lit 3.x components (010-css-answer-hiding) - N/A (CSS-only feature) (010-css-answer-hiding)
- YAML (GitHub Actions workflows), Bash (scripts) + GitHub Actions (actions/checkout, actions/setup-node, softprops/action-gh-release) (011-release-automation)
- N/A (CI/CD infrastructure only) (011-release-automation)
- 001-security-refactor: Added TypeScript 5.x / JavaScript ES2020+ + Lit 3.0 (Web Components), Vite 5.x (build), Vitest (testing)