pediatric-ai-scribe-v3/shared/clinical/visit-status.test.ts
Daniel 8253b34dc5 feat(wellvisit): port the 4 sub-tabs vanilla had — By Visit / Milestones / SSHADESS / Note
Earlier WellVisit was a single-pane skeleton — the comment in that
file even admitted "Milestones and SSHADESS sub-tabs land in a
follow-up". This commit lands them, faithful to public/components/
wellvisit.html and the three modules behind it (wellVisit.js,
milestones.js, shadess.js, all @be14578).

WellVisit.tsx is now an 83-line shell that lazy-loads four panels:

  client/src/pages/wellvisit/
    ByVisitAge.tsx  — per-visit AAP Bright Futures recommendations
                       (billing codes, measurements, vaccines,
                       sensory/dev/proc/oral screens, growth +
                       feeding, expected reflexes, BMI table,
                       notes). Status buttons mirror vanilla
                       (Given/Refused/Deferred/Already-Done for
                       vaccines; Done/Refused/N-A for screens).
                       Statuses persist to localStorage under
                       ped_visit_statuses (same key as vanilla).
    Milestones.tsx  — checklist per age group, three-state toggle
                       (✓/✗/blank=not assessed), All-Yes/Clear,
                       Generate → /api/generate-milestone-narrative,
                       3-sentence summary → /api/generate-milestone-
                       summary, Copy-to-Note bridge.
    Shadess.tsx     — 8 SSHADESS domains verbatim from shadess.js
                       (Strengths, School, Home, Activities, Drugs,
                       Emotions/Eating, Sexuality, Safety) with
                       concern_if auto-flag, skip toggle, manual
                       concern toggle, optional Listen-In recorder,
                       Generate → /api/well-visit/shadess.
    VisitNote.tsx   — pre-existing note generator + carry-over
                       pickup from sessionStorage so the user can
                       flow Milestones → SSHADESS → Note without
                       retyping. Now also includes the recorder.

Server: GET /api/schedule-data now also returns wellVisitCodes,
growthReference, reflexesReference, and bmiClassification so the
React side has everything it needs without a second round trip.

Tests:
  shared/clinical/visit-status.ts (+ .test.ts) — pure helpers
  for the visitId → growth/reflex key mapping (the vanilla
  tables collapse 6y/7y/8y/9y/10y onto one window, etc.) and
  the reflex-status color rules. Lives in shared/ so the root
  vitest config picks it up; ByVisitAge.tsx imports from there.
2026-04-24 05:24:09 +02:00

105 lines
3.9 KiB
TypeScript

// Tests for shared/clinical/visit-status.ts. The mappings below mirror
// the dictionaries baked into public/js/wellVisit.js (@be14578) — if the
// vanilla schedule changes, both files update in the same commit.
import { describe, expect, it } from 'vitest';
import {
mapToGrowthKey,
mapToReflexKey,
reflexStatusColor,
getStatusValue,
isSshadessVisit,
SSHADESS_VISITS,
} from './visit-status';
const G_KEYS = {
'newborn': {}, '1mo': {}, '2mo': {}, '4mo': {}, '6mo': {}, '9mo': {},
'12mo': {}, '15mo': {}, '18mo': {}, '24mo': {}, '30mo': {},
'3y': {}, '4y': {}, '5y': {},
'6y_to_10y': {}, '11y_to_14y': {}, '15y_to_21y': {},
};
const R_KEYS = {
'newborn': {}, '1mo': {}, '2mo': {}, '4mo': {}, '6mo': {}, '9mo': {},
'12mo': {}, '15mo': {}, '18mo': {}, '24mo': {}, '30mo': {},
'3y_to_5y': {}, '6y_to_10y': {}, '11y_to_14y': {}, '15y_to_21y': {},
};
describe('mapToGrowthKey', () => {
it('returns the visitId itself when it is a direct key', () => {
expect(mapToGrowthKey('newborn', G_KEYS)).toBe('newborn');
expect(mapToGrowthKey('6mo', G_KEYS)).toBe('6mo');
expect(mapToGrowthKey('3y', G_KEYS)).toBe('3y');
});
it('aliases 3-5d to newborn (vanilla parity)', () => {
expect(mapToGrowthKey('3-5d', G_KEYS)).toBe('newborn');
});
it('collapses 6y..10y onto a single growth window', () => {
for (const v of ['6y','7y','8y','9y','10y']) {
expect(mapToGrowthKey(v, G_KEYS)).toBe('6y_to_10y');
}
});
it('collapses adolescence into 11y_to_14y / 15y_to_21y', () => {
for (const v of ['11y','12y','13y','14y']) {
expect(mapToGrowthKey(v, G_KEYS)).toBe('11y_to_14y');
}
for (const v of ['15y','16y','17y','18y','19y','20y','21y']) {
expect(mapToGrowthKey(v, G_KEYS)).toBe('15y_to_21y');
}
});
it('returns null when nothing matches', () => {
expect(mapToGrowthKey('totally-bogus', G_KEYS)).toBeNull();
});
});
describe('mapToReflexKey', () => {
it('groups 3y/4y/5y onto 3y_to_5y (different from growth)', () => {
for (const v of ['3y','4y','5y']) {
expect(mapToReflexKey(v, R_KEYS)).toBe('3y_to_5y');
}
});
it('keeps the same 11y_to_14y / 15y_to_21y windows as growth', () => {
expect(mapToReflexKey('13y', R_KEYS)).toBe('11y_to_14y');
expect(mapToReflexKey('18y', R_KEYS)).toBe('15y_to_21y');
});
});
describe('reflexStatusColor', () => {
it('greens out resolved / integrated reflexes', () => {
expect(reflexStatusColor('Absent / integrated')).toBe('#059669');
expect(reflexStatusColor('Present (lifelong)')).toBe('#059669');
expect(reflexStatusColor('Down-going')).toBe('#059669');
});
it('blues out present-and-expected', () => {
expect(reflexStatusColor('Present')).toBe('#2563eb');
expect(reflexStatusColor('Peak')).toBe('#2563eb');
expect(reflexStatusColor('2+ symmetric')).toBe('#2563eb');
});
it('purples emerging, ambers transitional', () => {
expect(reflexStatusColor('Emerging')).toBe('#7c3aed');
expect(reflexStatusColor('Fading')).toBe('#d97706');
expect(reflexStatusColor('In transition')).toBe('#d97706');
expect(reflexStatusColor('Up-going')).toBe('#d97706');
});
it('falls back to slate for unknowns', () => {
expect(reflexStatusColor('whatever')).toBe('#475569');
expect(reflexStatusColor('')).toBe('#475569');
});
});
describe('getStatusValue', () => {
it('handles undefined, bare strings, and object form', () => {
expect(getStatusValue(undefined)).toBe('');
expect(getStatusValue('Given')).toBe('Given');
expect(getStatusValue({ status: 'Refused', note: '' })).toBe('Refused');
expect(getStatusValue({ status: '', note: 'context' })).toBe('');
});
});
describe('isSshadessVisit', () => {
it('matches all 12+ visits exactly', () => {
for (const v of SSHADESS_VISITS) expect(isSshadessVisit(v)).toBe(true);
});
it('rejects under-12 visits', () => {
for (const v of ['newborn','6mo','11y','5y']) expect(isSshadessVisit(v)).toBe(false);
});
});