From abb67bd03afcf8c0363185bd3e581ef712717f7a Mon Sep 17 00:00:00 2001 From: Daniel Date: Wed, 22 Apr 2026 21:03:05 +0200 Subject: [PATCH] =?UTF-8?q?test(e2e):=20PE=20Guide=20smoke=20=E2=80=94=208?= =?UTF-8?q?=20tests=20covering=20all=20systems=20+=20age-group=20coverage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Uses the shared fixture so pageerror + console.error fail the test — would catch any regression of the bug-class that shipped the SSO ReferenceError. All tests log in as the seeded e2e user and drive the real UI. Coverage: - tab loads with empty-state before age selected - MSK (default) renders with scales + steps - switch to Neuro shows MRC scale + teaching pearl - Respiratory shows 8-sound library with play buttons + RR scale - Cardiovascular shows APTM image (naturalWidth > 0 — actual network fetch confirmed), all 5 landmark letters, innocent-murmur panel, 7 "S" criteria footer - parametrised: every age group × {resp, cv} must NOT show "no data", must have ≥ 1 step (catches the regression Daniel just flagged) - step toggle cycles Normal → Abnormal → Skip with visual state change and note-field show/hide - grading scales
is collapsible and expands on click --- e2e/tests/pe-guide-smoke.spec.js | 157 +++++++++++++++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 e2e/tests/pe-guide-smoke.spec.js diff --git a/e2e/tests/pe-guide-smoke.spec.js b/e2e/tests/pe-guide-smoke.spec.js new file mode 100644 index 0000000..ba1d996 --- /dev/null +++ b/e2e/tests/pe-guide-smoke.spec.js @@ -0,0 +1,157 @@ +// ============================================================ +// PE GUIDE — smoke tests for the Physical Exam Guide tab +// ============================================================ +// Exercises the full rendering path: tab load → age group selection → +// system switching (msk / neuro / resp / cv) → expected per-system +// cards (scales, sounds library for resp, APTM image + innocent +// murmur panel for cv) → step toggle interaction. +// +// Uses the shared fixture so any uncaught JS error or console.error +// fails the test automatically (catches a repeat of the SSO-bug +// class on this page). +// ============================================================ + +const { test, expect, E2E_BASE } = require('../fixtures'); + +async function openPEGuide(page) { + await page.goto(E2E_BASE + '/'); + await page.waitForSelector('button.tab-btn', { timeout: 15000 }); + await page.click('button.tab-btn[data-tab="peguide"]'); + await page.waitForFunction(() => { + const el = document.getElementById('peguide-tab'); + return el && el.classList.contains('active') && el.innerHTML.trim().length > 100; + }, { timeout: 15000 }); +} + +async function selectAge(page, value) { + await page.selectOption('#pe-age-group', value); + // Wait for components to render (at least one card with a step) + await page.waitForFunction(() => { + return document.querySelectorAll('#pe-content .pe-step').length > 0; + }, { timeout: 5000 }); +} + +async function switchSystem(page, sys) { + await page.click('button.wv-subtab-btn[data-pesystem="' + sys + '"]'); + // Either steps are there, or the resp/cv cards are (which have no .pe-step) + await page.waitForFunction((s) => { + const content = document.getElementById('pe-content'); + if (!content) return false; + if (s === 'resp') return content.innerHTML.includes('Respiratory sounds library'); + if (s === 'cv') return content.innerHTML.includes('Auscultation landmarks'); + return content.querySelectorAll('.pe-step').length > 0; + }, sys, { timeout: 5000 }); +} + +test.describe('PE Guide — smoke', () => { + + test('tab loads with empty-state message before age group selected', async ({ authedPage: _, page }) => { + await openPEGuide(page); + await expect(page.locator('#pe-content')).toContainText(/Select an age group/i); + }); + + test('MSK (default) renders with overview and grading scales for adolescent', async ({ authedPage: _, page }) => { + await openPEGuide(page); + await selectAge(page, 'adolescent'); + await expect(page.locator('#pe-content')).toContainText('Musculoskeletal'); + await expect(page.locator('#pe-content')).toContainText(/Scoliometer|Beighton/); + // At least one step card present + await expect(page.locator('.pe-step').first()).toBeVisible(); + }); + + test('switches to Neuro and shows MRC scale + teaching pearl', async ({ authedPage: _, page }) => { + await openPEGuide(page); + await selectAge(page, 'adolescent'); + await switchSystem(page, 'neuro'); + await expect(page.locator('#pe-content')).toContainText('Neurologic'); + await expect(page.locator('#pe-content')).toContainText(/MRC strength/i); + // Teaching pearl rendered (amber block) + await expect(page.locator('#pe-content')).toContainText(/Pronator drift/i); + }); + + test('Respiratory system shows the 8-sound library with play buttons', async ({ authedPage: _, page }) => { + await openPEGuide(page); + await selectAge(page, 'adolescent'); + await switchSystem(page, 'resp'); + await expect(page.locator('#pe-content')).toContainText('Respiratory sounds library'); + // All 8 sounds represented as play buttons + const playButtons = page.locator('.resp-sound-play'); + await expect(playButtons).toHaveCount(8); + // RR scale renders + await expect(page.locator('#pe-content')).toContainText(/Respiratory rate/i); + // Observation-first inspection card present + await expect(page.locator('#pe-content')).toContainText(/Inspection/i); + }); + + test('Cardiovascular system shows APTM image + all 5 landmarks + innocent murmur panel', async ({ authedPage: _, page }) => { + await openPEGuide(page); + await selectAge(page, 'adolescent'); + await switchSystem(page, 'cv'); + // APTM diagram image loaded + const aptmImg = page.locator('img[src*="aptm.png"]'); + await expect(aptmImg).toBeVisible(); + // Wait for it to actually have painted pixels (naturalWidth > 0 = fetched successfully) + await expect.poll(async () => { + return await aptmImg.evaluate(el => el.naturalWidth); + }, { timeout: 10000 }).toBeGreaterThan(0); + // All 5 landmark letters in the legend + for (const letter of ['A', 'P', 'E', 'T', 'M']) { + await expect(page.locator('#pe-content')).toContainText(letter); + } + // Innocent murmur panel + await expect(page.locator('#pe-content')).toContainText(/Innocent murmurs/i); + await expect(page.locator('#pe-content')).toContainText(/Still.s/i); + await expect(page.locator('#pe-content')).toContainText(/Venous hum/i); + // 7 "S" criteria footer + await expect(page.locator('#pe-content')).toContainText(/7.*S/); + }); + + test('each age group has resp + cv data (no "no data" empty state)', async ({ authedPage: _, page }) => { + await openPEGuide(page); + const ages = ['newborn', 'infant', 'toddler', 'preschool', 'school', 'adolescent']; + for (const age of ages) { + await selectAge(page, age); + for (const sys of ['resp', 'cv']) { + await switchSystem(page, sys); + // Must NOT show the "no data" placeholder + const content = await page.locator('#pe-content').innerText(); + expect(content, `${age}/${sys} should have data`).not.toMatch(/No data for this combination/i); + // Should have at least one component card with at least one step + const stepCount = await page.locator('.pe-step').count(); + expect(stepCount, `${age}/${sys} should have at least 1 step`).toBeGreaterThan(0); + } + } + }); + + test('step toggle cycles Normal → Abnormal → Skip and updates visual state', async ({ authedPage: _, page }) => { + await openPEGuide(page); + await selectAge(page, 'adolescent'); + await switchSystem(page, 'neuro'); + const firstStep = page.locator('.pe-step').first(); + // Click Normal (✓) button + await firstStep.locator('button[data-pe-status="normal"]').click(); + // Background should turn green (#ecfdf5) + await expect.poll(async () => { + return await firstStep.evaluate(el => el.style.background); + }).toMatch(/rgb\(236, 253, 245\)|#ecfdf5/); + // Click Abnormal (✗) — note field should appear + await firstStep.locator('button[data-pe-status="abnormal"]').click(); + await expect(firstStep.locator('.pe-note')).toBeVisible(); + // Click Skip (—) — note field hides + await firstStep.locator('button[data-pe-status="skip"]').click(); + await expect(firstStep.locator('.pe-note')).toBeHidden(); + }); + + test('grading scales card is collapsible and expands on click', async ({ authedPage: _, page }) => { + await openPEGuide(page); + await selectAge(page, 'adolescent'); + await switchSystem(page, 'neuro'); + //
element with "Grading scales" summary + const details = page.locator('details').filter({ hasText: /Grading scales/ }); + await expect(details).toBeVisible(); + // Open it + await details.locator('summary').click(); + // Should now see at least one scale title + await expect(details).toContainText(/MRC strength/); + }); +});