diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index deb1bfa7..d82ac54f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -30,13 +30,25 @@ jobs: node-version: '22' cache: 'npm' - run: npm ci - - run: npm run test:unit - - run: npm run test:integration - - name: Upload coverage + # Coverage runs enforce the thresholds in vitest.config.ts / vitest.integration.config.ts. + # The thresholds are a ratchet: set at the measured level, raise them as tests are added. + - run: npm run test:coverage:unit + - name: Upload unit coverage uses: codecov/codecov-action@v4 if: always() with: files: ./coverage/lcov.info + flags: unit + - run: npm run test:coverage:integration + - name: Upload integration coverage + uses: codecov/codecov-action@v4 + if: always() + with: + files: ./coverage/lcov.info + flags: integration + # Structural gap check: fails if the number of source files without a test grows. + - name: Test-gap ratchet + run: npm run test:gaps -- --max-gaps 25 build: name: Build & Bundle Size Check @@ -57,53 +69,60 @@ jobs: name: dist path: dist/ - # e2e: - # name: E2E Tests - # runs-on: ubuntu-latest - # needs: build - # steps: - # - uses: actions/checkout@v4 - # - uses: actions/setup-node@v4 - # with: - # node-version: '22' - # cache: 'npm' - # - run: npm ci - # - name: Download build artifacts - # uses: actions/download-artifact@v4 - # with: - # name: dist - # path: dist/ - # - name: Install Playwright browsers - # run: npx playwright install chromium --with-deps - # - run: npm run test:e2e - # - name: Upload test results - # uses: actions/upload-artifact@v4 - # if: always() - # with: - # name: playwright-report - # path: playwright-report/ - # E2E tests temporarily disabled - will be re-enabled in future PR - - chromatic: - name: Visual Regression Tests + e2e: + name: E2E Tests (${{ matrix.mode }}) runs-on: ubuntu-latest - # Only run on push events to main branch - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + strategy: + fail-fast: false + matrix: + mode: [default, encrypted] + env: + # Playwright's webServer starts Storybook; CI=true stops it opening a browser. + CI: 'true' + PLAYWRIGHT_BROWSERS_PATH: ~/.cache/ms-playwright steps: - uses: actions/checkout@v4 - with: - fetch-depth: 0 - uses: actions/setup-node@v4 with: node-version: '22' cache: 'npm' - run: npm ci - - name: Run Chromatic - uses: chromaui/action@latest - # Don't fail if CHROMATIC_PROJECT_TOKEN is not configured - continue-on-error: true + - name: Install Playwright Chromium + run: npx playwright install chromium --with-deps + # The E2E pages under dita-demo/ load the bundle from the copied resources, so build + # and copy the bundle for the mode under test (the copy is not committed back). + - name: Build bundle (default) + if: matrix.mode == 'default' + run: npm run build:dita + - name: Build bundle (encrypted) + if: matrix.mode == 'encrypted' + run: npm run build:dita:encrypted + - name: Run Playwright (default) + if: matrix.mode == 'default' + run: npm run test:e2e + - name: Run Playwright (encrypted) + if: matrix.mode == 'encrypted' + run: npm run test:e2e:encrypted + - name: Upload Playwright report + uses: actions/upload-artifact@v4 + if: always() with: - projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} - exitZeroOnChanges: true - # Don't fail the build on Chromatic errors during Phase 0 - onlyChanged: true + name: playwright-report-${{ matrix.mode }} + path: playwright-report/ + retention-days: 7 + + storybook: + name: Storybook Build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + - run: npm ci + # Every story must compile. A story importing a deleted module used to break + # this silently (it was hidden behind continue-on-error on the old Chromatic + # job), which also took down the Storybook half of PR previews. Running it on + # pull requests catches that before merge. + - run: npm run build-storybook diff --git a/.storybook/main.js b/.storybook/main.js index 1e3d3185..50c5e1b6 100644 --- a/.storybook/main.js +++ b/.storybook/main.js @@ -5,7 +5,6 @@ const config = { '@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions', - '@chromatic-com/storybook' ], framework: { name: '@storybook/web-components-vite', diff --git a/ARCHITECTURE_FLOWS.md b/ARCHITECTURE_FLOWS.md index c53be3ea..7f93057a 100644 --- a/ARCHITECTURE_FLOWS.md +++ b/ARCHITECTURE_FLOWS.md @@ -62,13 +62,19 @@ graph LR | Event | Producer | Consumers | Payload | Purpose | |-------|----------|-----------|---------|---------| -| `qd:login` | qd-login | index.ts, home-badges, storage-monitor | SessionData | User authentication | -| `qd:logout` | qd-status | index.ts, home-badges, storage-monitor | {serviceId} | Session termination | -| `qd:answer-saved` | quiz-table enhancer | index.ts, storage-monitor | {pageId, answer} | Quiz progress | -| `qd:state-changed` | index.ts | home-badges, storage-monitor | {pageId, state} | Page completion state | -| `qd:instructor-unlock` | qd-instructor | storage-monitor, quiz tables | {timestamp} | Reveal answers | -| `qd:instructor-login` | qd-login, qd-status | index.ts | {password} | Instructor auth | -| `qd:data-cleared` | qd-instructor | storage-monitor | {timestamp} | Data erasure | +| `qd:login` | qd-login, qd-instructor-login, SessionService | event-coordinator, bootstrap, qd-status, qd-instructor, quiz-table | {serviceId, name, release, role} | Student or instructor authentication (instructor login carries `role: 'instructor'`) | +| `qd:logout` | qd-status, qd-instructor, SessionService | event-coordinator, qd-status, qd-login, qd-instructor, home-badges | {serviceId} | Session termination | +| `qd:answer-saved` | quiz-answer-persistence | event-coordinator (→ `qd:cache-update`) | {pageId, answer} | Quiz progress | +| `qd:state-changed` | quiz-answer-persistence | event-coordinator (→ `qd:badge-update`), qd-status, home-badges | {pageId, state} | Page completion state | +| `qd:analysis-saved` | analysis-persistence | (none) | {pageId, tableId} | Analysis cell saved | +| `qd:instructor-unlock` / `-lock` | qd-instructor-unlock, SessionService | event-coordinator (log only) | {} | Toolbar unlock / lock | +| `qd:instructor-show-answers` / `-hide-answers` | qd-instructor | quiz-table, analysis-table, instructor-answer-reveal | {} | Per-student overlay toggle | +| `qd:data-cleared` | qd-instructor-manage | event-coordinator (→ `qd:cache-clear`) | {timestamp} | Data erasure | +| `qd:cache-rebuild` / `-update` / `-clear`, `qd:badge-update` | event-coordinator | qd-status, home-badges | varies | Cache and badge coordination | + +All of these are dispatched on `document` (not `window`); see `src/init/event-coordinator.ts` and +`Technical_Design.md` §19 for the full list including component‑local events. There is no +`qd:instructor-login` event: instructor login emits `qd:login` with `role: 'instructor'`. --- @@ -113,12 +119,13 @@ sequenceDiagram SessionService->>Storage: Save to sessionStorage index.ts->>DOM: Rebuild cache from IndexedDB else Path 2: Instructor Login - User->>qd-status: Click instructor button - qd-status->>EventBus: Dispatch qd:instructor-login - EventBus->>index.ts: Show instructor modal - User->>qd-instructor: Enter password - qd-instructor->>EventBus: Dispatch qd:instructor-unlock - EventBus->>DOM: Reveal correct answers + User->>qd-login: Click "Instructor" button + qd-login->>qd-instructor-login: Open password modal + User->>qd-instructor-login: Enter password (rate limited) + qd-instructor-login->>instructor-auth: verifyInstructorPassword (#qd-instructor-hash) + qd-instructor-login->>SessionService: createSession('INSTRUCTOR') + qd/instructor flag + qd-instructor-login->>EventBus: Dispatch qd:login (role: instructor) + EventBus->>bootstrap: revealInstructorAnswers for each quiz table else Path 3: Session Resume User->>DOM: Return to page DOM->>SessionService: Check sessionStorage diff --git a/CLAUDE.md b/CLAUDE.md index 12fa7be3..b717a08e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -6,7 +6,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co 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) +**Current Phase**: All eight delivery phases and specs 001–012 are complete (June 2026). The project was dormant until the September 2026 consolidation; see `docs/PROJECT_STATE.md` for the audit and `docs/MAINTAINERS.md` for the maintainer guide. ## Core Architecture @@ -27,7 +27,7 @@ User Input → DOM Handler → Service Layer → Storage Adapter **Storage Strategy**: - **IndexedDB**: Primary persistence with composite keys `qd/{release}/u{serviceId}` - Database name: `BrowserTest` (defined in `src/services/storage/indexeddb.ts`) - - Object stores: `students`, `backups` + - Object stores: `students`, `backups`, `auditLog` (DB version 3, see `src/services/storage/idb-connection.ts`) - **sessionStorage**: Active session + R/A/G cache (expires 30 min) - **No network**: All data remains local, no telemetry/CDN/remote config @@ -76,7 +76,7 @@ Consolidated control center with: - For analysis tables: Shows student entries in comparison format - **Button**: "View All Scores" → Opens `` - **Button**: "Export to CSV" → Downloads detailed answer data -- **Button**: "Erase All Data" → Clears all storage (with confirmation dialog) +- **Button**: "Erase All Data" → Clears IndexedDB (students, backups, audit log) and all `qd/*` sessionStorage keys (typed confirmation required) - **Button**: "Logout" #### **Scores Modal** (``) @@ -116,7 +116,6 @@ 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 @@ -204,7 +203,7 @@ python3 -m http.server 8000 # Visit: http://localhost:8000/demo/quiz-index.html ``` -All demo files load the built bundle from `dist/sonar-quiz.iife.js` with configuration provided via hidden `` elements. Debug mode is controlled by `DEBUG_MODE` constant in `src/index.ts`. See **demo/README.md** for: +All demo files load the built bundle from `dist/sonar-quiz.iife.js` with configuration provided via hidden `` elements. Debug logging is off by default and can be switched on at runtime with `setDebugMode(true)` from `src/utils/logger.ts` (exported on `window.SonarQuiz`). 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 @@ -289,7 +288,7 @@ npm run build - `#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_MODE` constant in `src/index.ts` (single toggle point) +- Debug logging controlled by `setDebugMode()` in `src/utils/logger.ts` (off by default; no `data-qd-debug` attribute is read) - No setup, no manual config, no network dependencies **Example HTML Structure** (injected by DITA/Oxygen transform): @@ -435,8 +434,8 @@ Before committing any code changes, ALL of the following MUST pass: ### Phase Exit Gates - **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 1**: Storybook interactions pass, parsing unit tests +- **Phase 2**: Storybook renders all components, 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 @@ -493,7 +492,9 @@ Enable via `data-qd-debug` attribute on quiz/analysis tables: - **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 +- **docs/MAINTAINERS.md**: Maintainer guide — setup, repo map, where the truth lives, known issues +- **docs/SECURITY.md**: What the project actually does for security, and its limits +- **docs/history/**: Archived design reviews and proposals (historical, not current) - **demo/README.md**: Demo HTML files, testing workflows, troubleshooting guide - **specs/001-sonar-quiz-system/**: Feature spec, plan, data model, contracts diff --git a/README.md b/README.md index 1d3c7184..3377e22c 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ A browser-based quiz tool, capable of storing scores across a range of players a BrowserTest is an interactive quiz and analysis platform designed for offline use. It progressively enhances HTML documents with quiz functionality, student progress tracking, and instructor review capabilities—all without requiring a network connection or server backend. -**Status:** In active development +**Status:** Feature-complete for the original eight-phase plan (June 2026); consolidated for restart in September 2026. See [docs/PROJECT_STATE.md](./docs/PROJECT_STATE.md) for the audit and [docs/MAINTAINERS.md](./docs/MAINTAINERS.md) for the maintainer guide. ## Key Features @@ -20,7 +20,7 @@ BrowserTest is an interactive quiz and analysis platform designed for offline us - **Framework:** Lit 3 (Web Components) - **Build:** Vite + TypeScript - **Storage:** IndexedDB (primary), sessionStorage (session cache) -- **Testing:** Vitest (unit), Playwright (E2E), Storybook + Chromatic (visual regression) +- **Testing:** Vitest (unit + integration), Playwright (E2E), Storybook (component development) - **Target:** Chromium ≥96, Firefox ≥102, offline-capable ## Prerequisites @@ -101,10 +101,10 @@ npm run test:e2e:debug # Debug mode with Playwright Inspector **No manual Storybook management required** - just run `npm run test:e2e`. -### Visual Regression Tests +### Storybook ```bash npm run storybook # Start Storybook for local development -npm run chromatic # Run visual regression tests (requires API token) +npm run build-storybook # Verify every story compiles (also a CI gate) ``` ### All Tests @@ -143,14 +143,16 @@ See [docs/RELEASE.md](./docs/RELEASE.md) for complete release instructions. | [Technical_Design.md](./Technical_Design.md) | Architecture, technology decisions, packaging, integration, and acceptance criteria | | [ARCHITECTURE_FLOWS.md](./ARCHITECTURE_FLOWS.md) | Event flows, login processes, DOM patterns, and service interactions | | [Contracts.md](./Contracts.md) | Core types, interfaces, events, constants, and validation rules (frozen API) | -| [Delivery_Plan.md](./Delivery_Plan.md) | Phase-by-phase development plan with exit gates and testing strategy | -| [Storybook_Plan.md](./Storybook_Plan.md) | Component stories, controls, interaction tests, and visual regression policy | +| [docs/MAINTAINERS.md](./docs/MAINTAINERS.md) | Maintainer guide: setup, repo map, where the truth lives, known issues | +| [docs/PROJECT_STATE.md](./docs/PROJECT_STATE.md) | September 2026 consolidation audit and restart backlog | +| [docs/SECURITY.md](./docs/SECURITY.md) | What the project does for security, and its limits | +| [docs/README.md](./docs/README.md) | Index of all documentation, including archived material | ## Development Phases The project follows an 8-phase delivery plan: -- **Phase 0:** Bootstrap + Contracts (toolchain, frozen interfaces) ← *Current phase* +- **Phase 0:** Bootstrap + Contracts (toolchain, frozen interfaces) - **Phase 1:** Quiz Core (interactive quizzes, no login/IDB) - **Phase 2:** Analysis Workbook (editable tables) - **Phase 3:** Instructor Unlock (supervisor mode) @@ -160,11 +162,11 @@ The project follows an 8-phase delivery plan: - **Phase 7:** Beta Deployment & Feedback - **Phase 8:** Security/Enhancements (optional) -See [Delivery_Plan.md](./Delivery_Plan.md) for detailed phase descriptions and exit criteria. +All eight phases are complete. Later work (specs 001–012 under `specs/`) added security hardening, PIN authentication, help popups, storage obfuscation, release automation and a hot-spot refactor. ## Architecture -- **Runtime:** Single JavaScript bundle (IIFE ≤25 KB gzipped) that progressively enhances DITA-published HTML +- **Runtime:** Single JavaScript bundle (IIFE ≤40 KB gzipped, enforced in CI) that progressively enhances DITA-published HTML - **Pattern:** DOM upgrades for tables + Lit 3 custom elements for UI overlays - **Data flow:** User actions → sessionStorage cache → IndexedDB persistence - **Isolation:** Shadow DOM for components, scoped CSS for upgraded tables diff --git a/Technical_Design.md b/Technical_Design.md index c776a5a2..f341d915 100644 --- a/Technical_Design.md +++ b/Technical_Design.md @@ -1,132 +1,145 @@ # Technical Design Document (TDD) — Sonar Training Interactive Component +> **Re-baselined 2026-09-05** against the code as built. The November 2025 version of this +> document recorded design intent; 14 of its 20 checkable claims no longer matched `src/` +> (see `docs/PROJECT_STATE.md` §4). Section numbering is unchanged so existing references still +> resolve. Where something was designed but not built, this document says so. + ## 1. Purpose and Scope - Defines how the interactive quiz and analysis features are built, packaged, and integrated into DITA‑published HTML. -- Targets offline, file:// deployments on constrained machines. +- Targets offline, `file://` deployments on constrained, air‑gapped machines. - Out of scope: procurement, authoring workflows, detailed UX copy, server integration. ## 2. Architectural Overview -- **Runtime:** Single drop‑in JavaScript bundle that progressively enhances DITA HTML pages. -- **Pattern:** DOM upgrades for quiz and analysis tables; small custom elements for UI overlays. -- **Data:** IndexedDB for persistence; sessionStorage for session + per‑session cache. -- **Isolation:** Shadow DOM for components; injected scoped CSS for upgraded tables. -- **No network:** No telemetry, no remote config, no CDNs. +- **Runtime:** Single drop‑in JavaScript bundle (`sonar-quiz.iife.js`) that progressively enhances DITA HTML pages on `DOMContentLoaded`. +- **Pattern:** In‑place DOM upgrade of `table.qd-quiz` and `table.qd-analysis`; Lit 3 custom elements for every piece of UI chrome (login, status, instructor tools, dialogs). +- **Layers** (`src/`): `init/` (bootstrap, component injection, event and session coordination) → `enhancers/` (table upgrades, overlays, badges) → `services/` (parsing, state, auth, storage) → `services/storage/` (IndexedDB adapter, codec, migrations). `components/` sit alongside and talk to services directly. +- **Data:** IndexedDB for persistence; `sessionStorage` for the active session, instructor flag and R/A/G cache. +- **Isolation:** Shadow DOM for components; one injected global stylesheet (`#qd-global-styles`) for upgraded tables and the `.qd-hidden` utility. +- **No network:** No telemetry, no remote config, no CDNs, no dynamic imports. ## 3. UI Technology Decisions -- **Framework:** Lit 3 custom elements for UI islands (e.g., status, login). -- **Why Lit:** Small runtime, clean reactivity, strong encapsulation, good maintainability. -- **Components:** ``, ``; rest by DOM upgrade. -- **Styling:** CSS variables `--qd-*` for theming; avoid global CSS collisions. -- **Author-visible contracts:** Zero or one `table.qd-quiz.qd-page` and zero or one `table.qd-analysis` per page; `#qd-status` placeholder panel. +- **Framework:** Lit 3 custom elements, all prefixed `qd-`. 22 elements are registered (`src/components/`): `qd-login`, `qd-status`, `qd-instructor` and its four sub‑panels (`-unlock`, `-scores`, `-export`, `-manage`), `qd-instructor-login`, `qd-modal`, `qd-password-modal`, `qd-confirm-dialog`, `qd-scores-modal`, `qd-student-table`, `qd-student-answers`, `qd-student-entries`, `qd-pin-reset-dialog`, `qd-lockout-banner`, `qd-migration-dialog`, `qd-help-trigger`, `qd-help-popup`, `qd-build-info`, `qd-spinner`. +- **Why Lit:** Small runtime, clean reactivity, strong encapsulation. +- **Injection:** `qd-login`, `qd-status` and `qd-instructor` are appended as children of the container matched by the `#qd-status-container` selector (default `.wh_top_menu_and_indexterms_link`). There is no `#qd-status` placeholder; if the selector matches nothing, nothing is injected and a warning is logged. +- **Styling:** Component styles live in Shadow DOM; a shared `sharedStyles` module covers the instructor sub‑panels. CSS variables are not used for theming. +- **Author‑visible contracts:** At most one `table.qd-quiz` and one `table.qd-analysis` per page. All matching tables are enhanced; the "one per page" rule is a documented authoring constraint, not enforced at runtime. ## 4. Packaging and Distribution -- **Bundles:** - - IIFE: `sonar-quiz.iife.js` (global `window.SonarQuiz`, auto‑init on DOMContentLoaded). - - ESM: `sonar-quiz.esm.js` for integrators. -- **Byte budget:** IIFE ≤ 40 KB min+gzip. -- **Assets:** No external CSS or fonts. Inline minimal styles where required. -- **Release artifacts:** GitHub Release ZIP including `/dist`, `/demo/index.html`, quick start, checksums. -- **SemVer:** Tag each release; maintain CHANGELOG. +- **Bundles:** IIFE `dist/sonar-quiz.iife.js` (global `window.SonarQuiz`, auto‑init) and ESM `dist/sonar-quiz.esm.js` with `index.d.ts`. +- **Byte budget:** IIFE ≤ 40 KB min+gzip, enforced by `npm run size-check` in CI. Measured 37.6 KB at re‑baseline, so about 2.4 KB of headroom. The ESM build (~53 KB gzip) is **not budgeted**; it is for integrators who tree‑shake. +- **Assets:** No external CSS or fonts. +- **Release artifacts:** `release.yml` runs on a `v*` tag and attaches the built bundle to a GitHub Release (see `docs/RELEASE.md`). There is no CHANGELOG file; release notes are generated from commits. +- **Version stamp:** `VERSION` is injected from `package.json` at build time (`__APP_VERSION__`), alongside `BUILD_DATE`, and shown by ``. ## 5. Build Strategy -- **Tooling:** Vite (library mode) + Rollup outputs; tree‑shaken Lit imports. -- **Outputs:** IIFE + ESM + source maps; TypeScript definitions for ESM consumers. -- **CI:** Lint, unit tests, e2e tests, bundle size check, release packaging. +- **Tooling:** Vite (library mode) + Rollup; TypeScript 5; `vite-plugin-dts` for typings. +- **Build‑time defines:** `__BUILD_DATE__`, `__APP_VERSION__`, `__ENCRYPT_STORAGE__` (from the `ENCRYPT_STORAGE` env var). +- **CI (`.github/workflows/ci.yml`):** lint + format; unit and integration tests with coverage thresholds (a ratchet, raised as tests are added); test‑gap ratchet (`scripts/check-test-gaps.js --max-gaps N`); build + bundle size; Playwright E2E in default and encrypted modes; Storybook build. ## 6. Integration with DITA / Oxygen -- **Include:** Single `