8 new spec files covering sections previously only smoke-tested:
- ai-endpoints-contract.spec.js — hits 8 real AI endpoints via request
context and fails if the response leaks TypeError / ReferenceError /
'Cannot read properties of undefined' / 'is not defined' / 'is not a
function'. This is the class of bug that shipped the PE-narrative
regression to prod because every page-level mock prevented the real
handler from running.
- encounter-workflow.spec.js — generate HPI, refine, clear transcript.
- encounter-save-load.spec.js — save draft, load popover, repopulate.
- wellvisit-workflow.spec.js — byvisit, milestones, SSHADESS (12+
reveal), visit note.
- vaxschedule-content.spec.js — schedule + catch-up panels populate
beyond "Loading".
- chart-review-workflow.spec.js — generate + load popover.
- learning-tab.spec.js — search filter, category pills, feed.
- settings-faq-dictation.spec.js — voice/password/2FA/Nextcloud
sections, FAQ expand/collapse, dictation generate flow.
Baseline fixes:
- Added CORS_ORIGINS + API_RATE_LIMIT_MAX env overrides so the e2e
container accepts the browser's Origin header and can absorb the
full suite's API traffic without tripping the 200/min guard.
- Server's /api/ rate limit is now configurable via
API_RATE_LIMIT_MAX (default stays 200).
- extensions-crud: replaced native page.on('dialog') listeners with
#confirm-modal-ok clicks (we moved off native confirm()).
- pe-guide-smoke + extensions-crud: mobile viewport opens the hamburger
before clicking sidebar tabs.
- fixtures.js: /api/refine mock uses 'refined' (real API shape), not
'content'. /api/chart-review replaced with /api/generate-chart-review.
Suite: 230 passed / 0 failed in 4m36s.
134 lines
6.3 KiB
JavaScript
134 lines
6.3 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/generate-sick-visit', { timeout: 15000 }).catch(() => null),
|
|
// Note: the well-visit generate button hits either /generate-sick-visit or a
|
|
// dedicated endpoint depending on the code — just ensure it doesn't crash.
|
|
page.click('#btn-wv-generate'),
|
|
]);
|
|
// The output container should become visible with SOME text (mocked)
|
|
await expect(page.locator('#wv-note-output')).toBeVisible({ timeout: 15000 });
|
|
});
|
|
});
|