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.
85 lines
4 KiB
JavaScript
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();
|
|
});
|
|
});
|