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.
114 lines
4.4 KiB
JavaScript
114 lines
4.4 KiB
JavaScript
// ============================================================
|
|
// AI ENDPOINT CONTRACT TESTS — hit the REAL server handlers
|
|
// ============================================================
|
|
// The UI smoke tests mock AI responses via page.route() so they never
|
|
// exercise the server-side handler. That meant a route whose require()
|
|
// statement was wrong (undefined PROMPTS → 500) shipped to prod without
|
|
// any test failing. This spec calls each AI generation endpoint
|
|
// through the Playwright request fixture (bypasses page.route) with a
|
|
// minimal valid payload and only checks the server didn't crash with a
|
|
// ReferenceError / TypeError. A mock model is installed on the server
|
|
// side via MOCK_AI=1 env var (if configured) so we don't spend API
|
|
// credits; otherwise the server still processes the request and returns
|
|
// a structured error (which is fine — we're guarding against 500s from
|
|
// bad imports, not end-to-end AI generation).
|
|
//
|
|
// A non-500 response (200 OK or 4xx with structured JSON) means the
|
|
// handler at least ran — that's the contract we're verifying.
|
|
// ============================================================
|
|
|
|
const { test, expect, E2E_BASE, getAuthToken } = require('../fixtures');
|
|
|
|
test.describe('AI endpoint contracts — handler loads + accepts POST', () => {
|
|
let token;
|
|
|
|
test.beforeAll(async ({ request }) => {
|
|
token = await getAuthToken(request);
|
|
});
|
|
|
|
// Each entry: path, minimal body that should make the handler run past
|
|
// its import statements. We don't need a valid AI key — a 500 from
|
|
// a require() bug will still fail, but a 4xx from "missing API key"
|
|
// is acceptable because it proves the route loaded.
|
|
const endpoints = [
|
|
{
|
|
path: '/api/generate-pe-narrative',
|
|
body: {
|
|
steps: [{ component: 'Inspection', label: 'General', method: 'Observed', status: 'normal' }],
|
|
ageGroup: 'School-Age (6-11 yr)',
|
|
system: 'neuro',
|
|
patientAge: '8 years',
|
|
patientGender: 'male',
|
|
format: 'narrative',
|
|
},
|
|
},
|
|
{
|
|
path: '/api/generate-milestone-narrative',
|
|
body: {
|
|
milestones: [{ domain: 'Motor', label: 'Walks', status: 'achieved' }],
|
|
ageGroup: '12 months',
|
|
patientAge: '12 months',
|
|
patientGender: 'male',
|
|
},
|
|
},
|
|
{
|
|
path: '/api/generate-hpi-encounter',
|
|
body: { transcript: 'Patient with cough for 3 days.', setting: 'outpatient' },
|
|
},
|
|
{
|
|
path: '/api/sick-visit/note',
|
|
body: { chiefComplaint: 'Cough', transcript: 'Cough x 3 days.' },
|
|
},
|
|
{
|
|
path: '/api/generate-soap',
|
|
body: { transcript: 'Patient with cough x 3 days.' },
|
|
},
|
|
{
|
|
path: '/api/generate-chart-review',
|
|
body: { pmh: 'None', outpatientVisits: [], edVisits: [], subspecialtyVisits: [], labs: [] },
|
|
},
|
|
{
|
|
path: '/api/refine',
|
|
body: { currentDocument: 'MOCK doc', instructions: 'Add severity.' },
|
|
},
|
|
{
|
|
path: '/api/well-visit/shadess',
|
|
body: { answers: { home: 'lives with parents' } },
|
|
},
|
|
];
|
|
|
|
// Signatures of runtime bugs that mean the handler crashed before it
|
|
// could reach its try/catch (i.e. the exact class of bug being guarded).
|
|
const CRASH_SIGNATURES = [
|
|
/Cannot read properties of undefined/i,
|
|
/is not a function/i,
|
|
/is not defined/i,
|
|
/ReferenceError/i,
|
|
/TypeError/i,
|
|
];
|
|
|
|
for (const { path, body } of endpoints) {
|
|
test(`${path} — handler loads, returns structured JSON, no import-bug crash`, async ({ request }) => {
|
|
const r = await request.post(E2E_BASE + path, {
|
|
headers: { Authorization: 'Bearer ' + token },
|
|
data: body,
|
|
});
|
|
// Must always be JSON — a 500 HTML page means the express error handler
|
|
// caught an unhandled exception (our bug class).
|
|
let json = null;
|
|
try { json = await r.json(); } catch (_) { /* remains null */ }
|
|
expect(json, `${path} did not return JSON (HTTP ${r.status()})`).toBeTruthy();
|
|
|
|
// If the response leaked a JS error message through to the client,
|
|
// that's the import/destructure-bug signature we want to catch.
|
|
const errText = (json && (json.error || json.message)) || '';
|
|
for (const sig of CRASH_SIGNATURES) {
|
|
expect(errText, `${path} leaked a runtime error: ${errText}`).not.toMatch(sig);
|
|
}
|
|
// Errors should be plain strings — never raw error objects.
|
|
if (json && json.success === false) {
|
|
expect(typeof json.error).toBe('string');
|
|
}
|
|
});
|
|
}
|
|
});
|