pediatric-ai-scribe-v3/e2e/tests/wellvisit-workflow.spec.js
Daniel 3e3b4866be test(e2e): +44 tests — sick visit, hospital course, auth screen,
session persistence, per-tab model selector

Brings detailed coverage to sections that were previously only smoke-
tested:

- sickvisit-workflow.spec.js: generate → mocked note, refine bar
  round-trip, load popover, New button clears demographics + transcript.
- hospitalcourse-workflow.spec.js: fill H&P → generate → mocked
  narrative renders via /api/generate-hospital-course, refine updates
  text, load popover open/close.
- auth-screen.spec.js: unauthenticated landing visible, login form
  structure, forgot-password swap + return, register link is
  intentionally display:none on this instance (pinned), register form
  DOM still wired correctly if the link is manually unhidden, reg-
  password has minlength=8 + type=password.
- session-persistence.spec.js: clear cookie simulates logout (UI login
  is gated by Turnstile which can't be completed in the e2e container);
  re-login via loginAs restores the last tab + sub-pill via localStorage.
- model-selector.spec.js: tab-model-select dropdowns render with >0
  options across 7 tabs.

Fixture corrections:
- /api/sick-visit/note and /api/well-visit/note were not being mocked
  at all — the wrong /api/generate-sick-visit pattern was intercepting
  nothing, so tests fell through to the real AI backend. Both now have
  correct patterns and response shapes (sick-visit returns `note`,
  well-visit note returns `note`).
- /api/generate-hospital-course response key updated from `narrative`
  to `hospitalCourse` to match what the frontend actually reads.

wellvisit-workflow: the Visit Note test now asserts a concrete
waitForResponse on /api/well-visit/note + text render, instead of the
previous "hit either endpoint" fallback.

Suite: 294 passed / 0 failed in 5m30s.
2026-04-23 18:58:38 +02:00

133 lines
6.1 KiB
JavaScript

// ============================================================
// WELL VISIT — detailed workflow across the 4 sub-tabs:
// 1. By Visit Age (reference display)
// 2. Milestones — generate narrative from toggles
// 3. SSHADESS — generate psychosocial assessment
// 4. Visit Note — end-to-end generate a well-child note
// All AI calls are mocked.
// ============================================================
const { test, expect, E2E_BASE, mockAI } = require('../fixtures');
async function openTab(page) {
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(() => {});
}
await page.click('button.tab-btn[data-tab="wellvisit"]');
await page.waitForFunction(() => {
const el = document.getElementById('wellvisit-tab');
return el && el.classList.contains('active') && el.innerHTML.trim().length > 100;
}, { timeout: 15000 });
}
async function openSubtab(page, key) {
await page.click(`button.wv-subtab-btn[data-subtab="${key}"]`);
await page.waitForFunction(
(k) => !document.getElementById('wv-panel-' + k).classList.contains('hidden'),
key,
{ timeout: 5000 }
);
}
test.describe('Well Visit — detailed workflows', () => {
test('By Visit Age — selecting a visit age renders content detail', async ({ authedPage: _, page }) => {
await openTab(page);
await openSubtab(page, 'byvisit');
// Dropdown must exist and be non-empty
const options = await page.locator('#wv-visit-select option').count();
expect(options).toBeGreaterThan(1);
// Pick the 2nd option (skip placeholder). Detail container should populate.
await page.selectOption('#wv-visit-select', { index: 1 });
await expect.poll(async () =>
(await page.locator('#wv-visit-detail').innerText()).trim().length,
{ timeout: 5000 }).toBeGreaterThan(0);
});
test('Milestones — toggle "All yes" then generate → narrative renders', async ({ authedPage: _, page }) => {
await mockAI(page);
await openTab(page);
await openSubtab(page, 'milestones');
await page.fill('#ms-age', '12 months');
await page.selectOption('#ms-gender', 'Male');
// Age group picker — pick the first real option if there is a placeholder
const optCount = await page.locator('#ms-age-group option').count();
if (optCount > 1) await page.selectOption('#ms-age-group', { index: 1 });
// Mark everything achieved (the "All yes" action), wait for checklist to have at least one item
await page.waitForFunction(() => document.querySelectorAll('#milestone-checklist .milestone-row').length > 0, null, { timeout: 5000 }).catch(() => {});
await page.click('#ms-all-yes').catch(() => {});
const [resp] = await Promise.all([
page.waitForResponse('**/api/generate-milestone-narrative'),
page.click('#ms-generate-btn'),
]);
expect(resp.status()).toBe(200);
await expect(page.locator('#ms-narrative-text')).toContainText('MOCK developmental narrative', { timeout: 10000 });
});
test('SSHADESS — domain list renders + generate fires /api/well-visit/shadess', async ({ authedPage: _, page }) => {
await mockAI(page);
await openTab(page);
// The SSHADESS subtab button is display:none until the user picks a 12+
// visit in byvisit. Iterate the visit dropdown options and pick one whose
// value looks like a 12+ year visit.
await openSubtab(page, 'byvisit');
await page.waitForFunction(() => document.querySelectorAll('#wv-visit-select option').length > 2, null, { timeout: 5000 });
const adolescentOpt = await page.locator('#wv-visit-select option').evaluateAll((opts) => {
const match = opts.find(o => /1[2-8].*(year|yr)/i.test(o.textContent || '') || /1[2-8].*year/i.test(o.value || ''));
return match ? match.value : null;
});
if (!adolescentOpt) test.skip(true, 'No 12+ visit option in dropdown — cannot expose SSHADESS subtab');
await page.selectOption('#wv-visit-select', adolescentOpt);
// Now the shadess subtab button becomes visible
await expect(page.locator('button.wv-subtab-btn[data-subtab="shadess"]')).toBeVisible({ timeout: 5000 });
await openSubtab(page, 'shadess');
// Age 14 — should surface SSHADESS (12+ only)
await page.fill('#shadess-age', '14 years');
await page.selectOption('#shadess-gender', 'Female');
// Wait for domains to render — JS populates after age/gender are set
await expect.poll(async () =>
(await page.locator('#shadess-domains').innerText()).trim().length,
{ timeout: 5000 }).toBeGreaterThan(0);
// Generate refuses with a toast if no domain has data. Fill the first
// domain's free-text comment so hasData is true.
await page.locator('.shadess-comment').first().fill('Lives at home with parents, supportive environment.');
const [resp] = await Promise.all([
page.waitForResponse('**/api/well-visit/shadess', { timeout: 10000 }),
page.click('#btn-shadess-generate'),
]);
expect(resp.status()).toBe(200);
await expect(page.locator('#shadess-result-text')).toContainText('MOCK SSHADESS', { timeout: 10000 });
});
test('Visit Note — minimal generate → mocked well-visit note in output', async ({ authedPage: _, page }) => {
await mockAI(page);
await openTab(page);
await openSubtab(page, 'note');
await page.fill('#wv-note-age', '5 years');
await page.selectOption('#wv-note-gender', 'Male');
// Provide a vitals string so the request body has something
await page.fill('#wv-vitals', 'T 37.0, HR 95, RR 22, BP 95/60, SpO2 99% RA');
// Use the transcript area for content
await page.locator('#wv-transcript').click();
await page.keyboard.type('Parent reports child is doing well; no concerns.');
const [resp] = await Promise.all([
page.waitForResponse('**/api/well-visit/note', { timeout: 15000 }),
page.click('#btn-wv-generate'),
]);
expect(resp.status()).toBe(200);
await expect(page.locator('#wv-note-output')).toBeVisible({ timeout: 10000 });
await expect(page.locator('#wv-note-text')).toContainText('MOCK well visit note', { timeout: 10000 });
});
});