pediatric-ai-scribe-v3/e2e/tests/settings-faq-dictation.spec.js
Daniel 6b039c4d87 test: bring the e2e specs up to date with the app they test
Three specs were asserting screens that no longer exist. They passed for
as long as they did only because the e2e stack shared production's
database and its configuration; against a clean one they failed honestly.

Signing in is a stepped flow now — email, then a choice between a
password and an emailed code — so #login-password is in the DOM but
hidden until that choice is made. The spec asserted it visible on the
landing screen. Replaced with one test for the landing step and a new
one that walks the transition, which nothing covered before.

The register link is hidden only when registration is disabled. This
install has it enabled and invite-gated, so the link shows and the
invite field is required; the spec asserted display:none.

Connecting Nextcloud by signing in to Nextcloud is now the offered path,
with the username and app-password fields folded behind "Use an app
password instead". The spec asserted all three visible at once; it now
checks the primary path and then opens the fallback.

learning-tab.spec.js is deleted and `learning` is out of the smoke tab
list — that feature was removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Dv6sqaY6Vq3ChZHMem3cnU
2026-09-13 00:49:37 +02:00

135 lines
6.1 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: signing in is the offered path, app password the fallback', async ({ authedPage: _, page }) => {
// Connecting by signing in to Nextcloud itself is the ordinary way in, so
// the address and that button are what the card shows. The username and
// app-password fields still exist, folded away behind "Use an app password
// instead" — they were visible when this test was written.
await openTab(page, 'settings');
await expect(page.locator('#nc-url')).toBeVisible();
await expect(page.locator('#btn-nc-login-flow')).toBeVisible();
await expect(page.locator('#nc-user')).toBeHidden();
await page.locator('#nc-manual summary').click();
await expect(page.locator('#nc-user')).toBeVisible();
await expect(page.locator('#nc-pass')).toBeVisible();
await expect(page.locator('#btn-nc-connect')).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');
});
});