pediatric-ai-scribe-v3/e2e/tests/settings-react-voice-content.spec.js
Daniel 719d0cb7f7 feat(client): port Settings — Voice + Content (8 sub-sections)
Third and final commit of the Settings port. Adds the remaining eight
sub-sections so the React page matches vanilla settings.html 1:1.
After this commit Settings is fully ported; Layout already flipped to
available in commit 1, and the page fills out cleanly for local-auth
and SSO users alike.

Voice Preferences (VoicePreferencesCard)
  GET /api/user/preferences + /api/user/preferences/options populate the
  STT model / TTS voice selectors. Save POSTs /api/user/preferences.
  Preview persists the current TTS selection, then fetches /api/text-to-
  speech (binary blob, bypasses the JSON api wrapper), wraps the blob in
  an Audio element and plays it. A one-shot hydrated flag drives the
  first selection sync; after that the fields are local state.

Browser Whisper (BrowserWhisperCard) — UI-only port
  Persists the enabled flag + model choice under the same localStorage
  keys the vanilla BrowserWhisper module reads, so behavior will light
  up automatically when the recording components port. The preload +
  WASM transcription flow stays in vanilla for this commit — noted in
  the page copy so users aren't surprised.

Web Speech Recognition (WebSpeechCard) — UI-only port
  Same localStorage approach. Enabling surfaces a styled ConfirmModal
  with the HIPAA privacy warning before persisting. Enabling Web Speech
  flips Browser Whisper off automatically (mirrors vanilla priority:
  Web Speech > Browser Whisper > server).

My Templates (TemplatesCard)
  Full Memories CRUD for non-correction entries: category select,
  name, content textarea, Add/Update toggle (in-place edit), per-row
  Delete confirm. Hits /api/memories {GET, POST, PUT, DELETE}.

AI Corrections (CorrectionsCard)
  Read-only list filtered to category starting with 'correction_'.
  Per-row expand reveals the parsed ORIGINAL / CORRECTED TO: split
  (same text delimiter the vanilla parseCorrection() uses). Delete is
  wired through /api/memories/:id.

Audio Backups (AudioBackupsCard)
  Lists /api/audio-backups (server-stored, 24h TTL). Play opens the
  decompressed audio stream in a new tab; Delete hits DELETE
  /api/audio-backups/:id. Retry flow stays in vanilla for this commit —
  it re-submits to /api/transcribe and that integration belongs with
  the recording components.

Saved Encounters (SavedEncountersCard)
  Lists /api/encounters/saved with label / type / expires / preview.
  Delete only — Resume requires the encounter pages to receive
  pre-filled state, which ports alongside those pages.

Compliance (ComplianceCard)
  Static info card — plain JSX, no API.

shared/types.ts + client/src/shared/types.ts — additive only:
  UserPreferencesOk, PreferencesOptionsOk, VoiceOption,
  SavedEncounterRow, SavedEncountersListOk, AudioBackupRow,
  AudioBackupsListOk, MemoryRow, MemoriesOk.

e2e/tests/settings-react-voice-content.spec.js — seven smoke tests
covering control presence, templates empty-save validation, and the
Web Speech privacy-confirm modal (with the no-native-dialog guard).

Client tsc -b, server tsc --noEmit, and vite build all pass locally.
Final bundle 425.42 kB / 121.55 kB gzipped (+21 kB over commit 2).
The e2e container still predates /app/*; running these specs against
it needs a rebuild.
2026-04-23 23:40:33 +02:00

85 lines
4 KiB
JavaScript

// ============================================================
// SETTINGS (React port) — Voice + Content sub-sections.
//
// Commit 3 of the Settings port. Covers the eight remaining
// sub-sections so the React Settings page matches settings.html
// feature-for-feature:
// • Voice Preferences (STT / TTS selectors + Save + Preview)
// • Browser Whisper (WASM stub — localStorage-only)
// • Web Speech Recognition (localStorage-only + privacy modal)
// • My Templates (Memories CRUD)
// • AI Corrections (read-only list)
// • Audio Backups (list + delete)
// • Saved Encounters (list + delete)
// • Compliance (static info card)
// ============================================================
const { test, expect, E2E_BASE } = require('../fixtures');
async function openReactSettings(page) {
await page.goto(E2E_BASE + '/app/settings');
await page.waitForSelector(
'[data-testid="voice-preferences-section"], [data-testid="templates-section"], [data-testid="compliance-section"]',
{ timeout: 15000 }
);
}
test.describe('React Settings — Voice + Content sections render', () => {
test('Voice Preferences: STT / TTS selectors + Save + Preview', async ({ authedPage: _, page }) => {
await openReactSettings(page);
await expect(page.locator('[data-testid="stt-model-select"]')).toBeVisible();
await expect(page.locator('[data-testid="tts-voice-select"]')).toBeVisible();
await expect(page.locator('[data-testid="btn-save-voice-prefs"]')).toBeVisible();
await expect(page.locator('[data-testid="btn-preview-voice"]')).toBeVisible();
});
test('Browser Whisper card: enable toggle + model select', async ({ authedPage: _, page }) => {
await openReactSettings(page);
await expect(page.locator('[data-testid="browser-whisper-enabled"]')).toBeVisible();
await expect(page.locator('[data-testid="browser-whisper-model"]')).toBeVisible();
await expect(page.locator('[data-testid="browser-whisper-status"]')).toBeVisible();
});
test('Web Speech: checkbox shows privacy confirm modal before enabling', async ({ authedPage: _, page }) => {
let nativeDialogFired = false;
page.on('dialog', async (d) => { nativeDialogFired = true; await d.dismiss(); });
await openReactSettings(page);
const checkbox = page.locator('[data-testid="web-speech-enabled"]');
await expect(checkbox).toBeVisible();
const isDisabled = await checkbox.isDisabled();
if (!isDisabled) {
await checkbox.check();
await expect(page.locator('[data-testid="confirm-modal-cancel"]')).toBeVisible();
await page.click('[data-testid="confirm-modal-cancel"]');
}
expect(nativeDialogFired).toBe(false);
});
test('Templates: inputs + save button present', async ({ authedPage: _, page }) => {
await openReactSettings(page);
await expect(page.locator('[data-testid="mem-category"]')).toBeVisible();
await expect(page.locator('[data-testid="mem-name"]')).toBeVisible();
await expect(page.locator('[data-testid="mem-content"]')).toBeVisible();
await expect(page.locator('[data-testid="btn-mem-save"]')).toBeVisible();
});
test('Templates: saving without name shows inline error', async ({ authedPage: _, page }) => {
await openReactSettings(page);
await page.click('[data-testid="btn-mem-save"]');
await expect(page.getByText('Enter a template name')).toBeVisible();
});
test('Corrections card renders (list may be empty)', async ({ authedPage: _, page }) => {
await openReactSettings(page);
await expect(page.locator('[data-testid="corrections-section"]')).toBeVisible();
});
test('Audio Backups + Saved Encounters + Compliance render', async ({ authedPage: _, page }) => {
await openReactSettings(page);
await expect(page.locator('[data-testid="audio-backups-section"]')).toBeVisible();
await expect(page.locator('[data-testid="saved-encounters-section"]')).toBeVisible();
await expect(page.locator('[data-testid="compliance-section"]')).toBeVisible();
});
});