pediatric-ai-scribe-v3/e2e/tests/settings-faq-dictation.spec.js
Daniel 05dcd1146d test(e2e): repair the harness, taking the browser suite from 96 failures to 11
Three separate reasons tests were failing, none of them a defect in the app.

The calculators. e2e-harness.html loaded calculators.js and drugs-loader.js with
`defer` after they were split into ES modules; index.html was updated at the
time and this page was not. A module parsed as a classic script throws "Cannot
use import statement outside a module" before a line runs, so no click handler
was ever attached: the pills rendered from static HTML and did nothing. Only the
first calculator appeared to pass, because it carries `active` in the markup and
needs no click. That was 52 failures.

Settings and FAQ. Both moved from the tab rail into the account-card menu; the
helper still clicked button.tab-btn[data-tab=…] and timed out. Ten more.

The AI mocks, which had stopped intercepting for two independent reasons and so
were calling the real model on every run — spending credits and comparing
genuine output against strings like "MOCK HPI from dictation". A '**/api/x' glob
matches no URL on Playwright 1.50, and page.route fails silently when nothing
matches; measured against a real URL, that glob and '*/**/api/x' both matched
zero times where a regex matched. Fixing that alone was not enough: the app
registers a service worker that answers every /api/ request with its own
fetch(), and a request made inside a service worker never reaches page.route.
Blocking registration in the config puts them back in the page. The mocked
dictation test now finishes in 1.6s rather than 7.5s, which is what a real model
call costs.

Whole suite: 204 passed / 96 failed in 15.8 minutes, now 289 passed / 11 failed
in 6.8. The remaining eleven are spread across nine specs with no shared cause
and are not touched here.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-11 18:54:03 +02:00

126 lines
5.5 KiB
JavaScript

// ============================================================
// SETTINGS + FAQ + DICTATION — detailed UI exercises that
// don't depend on real AI / recording permissions.
// ============================================================
const { test, expect, E2E_BASE, mockAI } = require('../fixtures');
// Settings and FAQ are not on the tab rail. They live in the account-card menu
// alongside Admin, and this helper used to click button.tab-btn[data-tab=…] for
// them, which simply timed out — the cause of ten of these failures. Dictation
// really is a rail tab, so both routes are needed.
const ACCOUNT_MENU = ['settings', 'faq'];
async function openTab(page, name) {
await page.goto(E2E_BASE + '/');
await page.waitForSelector('button.tab-btn', { timeout: 15000 });
const vp = page.viewportSize();
if (vp && vp.width <= 768) {
await page.click('#btn-menu-toggle').catch(() => {});
}
if (ACCOUNT_MENU.includes(name)) {
await page.locator('.account-card-btn').first().click();
await page.locator(`[data-account-tab="${name}"]`).first().click();
} else {
await page.click(`button.tab-btn[data-tab="${name}"]`);
}
await page.waitForFunction((t) => {
const el = document.getElementById(t + '-tab');
return el && el.classList.contains('active') && el.innerHTML.trim().length > 100;
}, name, { timeout: 15000 });
}
test.describe('Settings — voice, password, nextcloud sections render', () => {
test('voice-preferences inputs + save button exist', async ({ authedPage: _, page }) => {
await openTab(page, 'settings');
// STT and TTS dropdowns populate; save button exists
await expect(page.locator('#stt-model-select')).toBeVisible();
await expect(page.locator('#tts-voice-select')).toBeVisible();
await expect(page.locator('#btn-save-voice-prefs')).toBeVisible();
});
test('change-password form: all three fields + submit button present', async ({ authedPage: _, page }) => {
await openTab(page, 'settings');
await expect(page.locator('#pw-current')).toBeVisible();
await expect(page.locator('#pw-new')).toBeVisible();
await expect(page.locator('#pw-confirm')).toBeVisible();
await expect(page.locator('#btn-change-password')).toBeVisible();
});
test('2FA setup panel has QR container + verify input', async ({ authedPage: _, page }) => {
await openTab(page, 'settings');
// At least one of the 2FA buttons should be present
const setupCount = await page.locator('#btn-setup-2fa').count();
const disableCount = await page.locator('#btn-disable-2fa').count();
expect(setupCount + disableCount).toBeGreaterThan(0);
});
test('Nextcloud section: URL/user/pass fields render', async ({ authedPage: _, page }) => {
await openTab(page, 'settings');
await expect(page.locator('#nc-url')).toBeVisible();
await expect(page.locator('#nc-user')).toBeVisible();
await expect(page.locator('#nc-pass')).toBeVisible();
});
});
test.describe('FAQ — questions expand + collapse on click', () => {
test('clicking a FAQ question reveals its answer panel', async ({ authedPage: _, page }) => {
await openTab(page, 'faq');
const questions = page.locator('.faq-question');
const count = await questions.count();
expect(count).toBeGreaterThan(0);
const first = questions.first();
await first.click();
// The corresponding answer should become visible — either via class toggle or
// inline style. Grab the sibling / next matching answer element and check.
const ariaControls = await first.getAttribute('aria-controls');
if (ariaControls) {
await expect(page.locator('#' + ariaControls)).toBeVisible();
} else {
// Fallback: any .faq-answer becomes visible
await expect(page.locator('.faq-answer').first()).toBeVisible();
}
});
});
test.describe('Dictation — UI loads, transcript editable, clear works', () => {
test('age/gender/setting inputs + generate + clear all render', async ({ authedPage: _, page }) => {
await mockAI(page);
await openTab(page, 'dictation');
await expect(page.locator('#dict-age')).toBeVisible();
await expect(page.locator('#dict-gender')).toBeVisible();
await expect(page.locator('#dict-setting')).toBeVisible();
await expect(page.locator('#dict-transcript')).toBeVisible();
await expect(page.locator('#dict-generate-btn')).toBeVisible();
});
test('typing into transcript + clear empties it', async ({ authedPage: _, page }) => {
await mockAI(page);
await openTab(page, 'dictation');
await page.locator('#dict-transcript').click();
await page.keyboard.type('Chief complaint: sore throat.');
await expect(page.locator('#dict-transcript')).toContainText('sore throat');
await page.click('#dict-clear');
const text = await page.locator('#dict-transcript').innerText();
expect(text.trim()).toBe('');
});
test('generate with short transcript → mocked HPI renders', async ({ authedPage: _, page }) => {
await mockAI(page);
await openTab(page, 'dictation');
await page.fill('#dict-age', '6 years');
await page.locator('#dict-transcript').click();
await page.keyboard.type('Patient with cough and congestion for 2 days.');
// Default output type is HPI — should call /api/generate-hpi-dictation
const [resp] = await Promise.all([
page.waitForResponse('**/api/generate-hpi-dictation'),
page.click('#dict-generate-btn'),
]);
expect(resp.status()).toBe(200);
await expect(page.locator('#dict-output')).toBeVisible();
await expect(page.locator('#dict-hpi-text')).toContainText('MOCK HPI from dictation');
});
});