Independent education · Sources and limitations documented
Portfolio / Smart NOA simulation

Software simulation

Source snapshot · 5 November 2026

simulator/tests/ui.smoke.mjs

// Browser smoke test for the simulator UI (Playwright + Chromium).
// Not part of `npm test` (only *.test.mjs files are loaded there). Run it with `npm run test:ui`.
// It starts its own static file server (Node http, no dependencies) on a free port, serving the
// repository root, and opens /simulator/index.html?test=1 (the ?test=1 flag enables the
// window.__smartNoa test hook: snapshot(), lite(), testClock(on) and advance(sec)).
// DETERMINISTIC (2026-11 timing review): the suite switches the test clock on, so simulated time
// moves ONLY through advance(sec) (synchronous; the guided/automatic runner ticks every second) or
// the explicit +1 s / +10 s / +1 min step buttons. Every wait is a condition on simulated state or
// the DOM; no step depends on wall-clock speed. (One test turns the clock off to check that the
// extubation dialog pauses a playing clock, waiting on explicit DOM/state conditions.)
// Playwright is required: if it cannot be loaded the test FAILS, unless SMOKE_SKIP=1 is set.
// Env:  SIM_URL (use an already running server instead), SHOTS_DIR (optional screenshot folder),
//       SMOKE_SKIP=1 (skip when Playwright is unavailable), PLAYWRIGHT_BROWSERS_PATH / NODE_PATH as needed.
import test from 'node:test';
import assert from 'node:assert/strict';
import { createRequire } from 'node:module';
import { createServer } from 'node:http';
import { mkdirSync, readFile } from 'node:fs';
import { join, normalize, extname, sep } from 'node:path';
import { fileURLToPath } from 'node:url';

const ROOT = fileURLToPath(new URL('../../', import.meta.url));
const SHOTS = process.env.SHOTS_DIR || null;
if (SHOTS) mkdirSync(SHOTS, { recursive: true });

let chromium = null;
try { chromium = createRequire(import.meta.url)('playwright').chromium; } catch { try { chromium = (await import('playwright')).chromium; } catch { chromium = null; } }
const skip = !chromium && process.env.SMOKE_SKIP === '1' ? 'playwright not installed (SMOKE_SKIP=1)' : false;

const TYPES = { '.html': 'text/html; charset=utf-8', '.js': 'text/javascript; charset=utf-8', '.mjs': 'text/javascript; charset=utf-8', '.css': 'text/css; charset=utf-8',
  '.md': 'text/markdown; charset=utf-8', '.json': 'application/json', '.svg': 'image/svg+xml', '.png': 'image/png', '.jpg': 'image/jpeg', '.ico': 'image/x-icon' };
/** Minimal static server for the repository root (GET/HEAD only; no directory listing; no path traversal). */
function startServer() {
  const server = createServer((req, res) => {
    let path;
    try { path = decodeURIComponent(new URL(req.url, 'http://x').pathname); } catch { res.writeHead(400).end(); return; }
    const file = normalize(join(ROOT, path));
    if (!file.startsWith(ROOT.endsWith(sep) ? ROOT : ROOT + sep) || (req.method !== 'GET' && req.method !== 'HEAD')) { res.writeHead(403).end(); return; }
    readFile(file, (err, data) => {
      if (err) { res.writeHead(404).end(); return; }
      res.writeHead(200, { 'content-type': TYPES[extname(file)] || 'application/octet-stream', 'cache-control': 'no-store' });
      res.end(req.method === 'HEAD' ? undefined : data);
    });
  });
  return new Promise((resolve) => { server.listen(0, '127.0.0.1', () => resolve(server)); });
}
let URL_ = process.env.SIM_URL || null;
let server = null;

const shot = async (page, name, full = true) => { if (SHOTS) await page.screenshot({ path: join(SHOTS, name), fullPage: full }); };
const snap = (page) => page.evaluate(() => window.__smartNoa.snapshot());
const lite = (page) => page.evaluate(() => window.__smartNoa.lite());
const simState = async (page) => (await snap(page)).sim;
/** Advances simulated time synchronously (test hook); returns the lite status. */
const adv = (page, sec) => page.evaluate((n) => window.__smartNoa.advance(n), sec);
/** Advances in `chunk`-second steps until pred(lite) holds (simulated-time limit `maxSec`). */
async function advanceUntil(page, pred, { maxSec = 3600, chunk = 5, label = 'condition', resolvePauses = false } = {}) {
  let s = await lite(page); const t0 = s.t;
  for (;;) {
    if (pred(s)) return s;
    if (s.auto && ['stopped', 'manual'].includes(s.auto.state)) throw new Error(`${label}: runner ${s.auto.state} — ${s.auto.message}`);
    if (resolvePauses && s.auto && s.auto.state === 'paused') { await userResolvesPause(page); s = await lite(page); continue; }
    if (s.t - t0 > maxSec) throw new Error(`Timed out (simulated ${maxSec} s) waiting for ${label}; last ${JSON.stringify({ t: s.t, phase: s.phase, v: s.v, auto: s.auto && { state: s.auto.state, cur: s.auto.current && s.auto.current.id, msg: s.auto.message } })}`);
    s = await adv(page, chunk);
  }
}
async function noHScroll(page) {
  return page.evaluate(() => document.documentElement.scrollWidth <= document.documentElement.clientWidth + 1);
}
async function loadCase(page, id) {
  await page.click('#stepper-1');
  await page.waitForSelector('#pf-example');
  await page.selectOption('#pf-example', id);
  await page.click('#pf-load-example');
  await page.waitForSelector('#pf-continue');
}
async function toRun(page) {
  await page.click('#pf-continue'); await page.waitForSelector('#as-continue');
  await page.click('#as-continue'); await page.waitForSelector('#pl-continue');
  await page.click('#pl-continue'); await page.waitForSelector('#mon-play');
}


// ---- guided / automatic modes
const autoStatus = async (page) => { const x = await snap(page); return x.auto ? { ...x.auto, t: x.sim.t, phase: x.sim.phase, v: x.sim.v, ready: x.sim.ready } : null; };
const simLog = async (page) => (await snap(page)).sim.log;
/** Waits (wall clock, no simulated time) until the gate button is not in its post-click cooldown. */
const gateIdle = (page) => page.waitForFunction(() => { const b = document.getElementById('auto-gate'); return !b || !b.dataset.busy; });
/** Clicks a button if it is visible and enabled; returns whether it was clicked. */
async function clickIf(page, sel) { const l = page.locator(sel); if (await l.count() && await l.isVisible() && await l.isEnabled()) { await l.click(); return true; } return false; }
/** The clinician's checklist buttons (WHO time out, index below band, missing prophylaxis). */
async function clinicianItems(page) {
  let n = 0;
  for (const sel of ['#auto-timeout', '#auto-depth-ack', '#auto-abx-give']) if (await clickIf(page, sel)) n++;
  return n;
}
/** Acts as the user when the runner pauses: acknowledge HIGH alarms, release a latched hold, clinician items, press Resume. */
async function userResolvesPause(page) {
  for (const b of await page.locator('#mon-alarms button[data-ack]:not([disabled])').all()) await b.click();
  if (await page.isEnabled('#mon-resume')) await page.click('#mon-resume');
  await clinicianItems(page);
  if (await page.isEnabled('#auto-resume')) await page.click('#auto-resume');
}
async function waitAuto(page, pred, { maxSec = 14000, label = 'auto condition', resolvePauses = true } = {}) {
  return advanceUntil(page, (s) => !!s.auto && pred({ ...s.auto, t: s.t, phase: s.phase, v: s.v, ready: s.ready }), { maxSec, chunk: 10, label, resolvePauses });
}
/**
 * Guided gate: advance simulated time until the gate button shows `label` and is enabled (resolving
 * pauses as a user; with clinician:true also pressing the clinician checklist buttons), then click it.
 */
async function gate(page, label, { maxSec = 5000, clinician = false, chunk = 5 } = {}) {
  await gateIdle(page);
  let s = await lite(page); const t0 = s.t;
  for (;;) {
    const dom = await page.evaluate(() => { const b = document.getElementById('auto-gate'); return b ? { text: b.textContent.trim(), on: !b.disabled && !b.hidden } : null; });
    if (dom && dom.text === label && dom.on) { await page.click('#auto-gate'); await gateIdle(page); return s; }
    if (s.auto && ['stopped', 'manual', 'completed'].includes(s.auto.state)) throw new Error(`gate ${label}: runner ${s.auto.state} — ${s.auto.message}`);
    if (s.auto && s.auto.state === 'paused') { await userResolvesPause(page); s = await lite(page); continue; }
    if (clinician && dom && dom.text === label && await clinicianItems(page)) { s = await lite(page); continue; }
    if (s.t - t0 > maxSec) throw new Error(`Timed out (simulated ${maxSec} s) waiting for gate ${label}; DOM ${JSON.stringify(dom)}; ${JSON.stringify(s.auto && s.auto.current && s.auto.current.unmet)}`);
    s = await adv(page, chunk);
  }
}
const GUIDED_BANNER = 'Guided mode: each phase runs from your approved plan; you confirm every transition. Educational simulation — not a closed-loop controller or a clinical recommendation.';
const FULL_BANNER = 'Automatic demonstration: scripted sequence, not a closed-loop controller or a clinical recommendation';

test('simulator UI smoke', { skip, timeout: 900000 }, async (t) => {
  assert.ok(chromium, 'Playwright (chromium) could not be loaded: install it (npm i -D playwright; npx playwright install chromium) or set SMOKE_SKIP=1 to skip explicitly');
  if (!URL_) { server = await startServer(); URL_ = `http://127.0.0.1:${server.address().port}/simulator/index.html?test=1`; }
  const browser = await chromium.launch();
  const ctx = await browser.newContext({ viewport: { width: 1280, height: 900 }, acceptDownloads: true });
  const page = await ctx.newPage();
  const problems = [];
  page.on('console', (m) => { if (m.type() === 'error' || m.type() === 'warning' || /Content Security Policy|Refused to/i.test(m.text())) problems.push(`console ${m.type()}: ${m.text()}`); });
  page.on('pageerror', (e) => problems.push(`pageerror: ${e.message}`));
  page.on('requestfailed', (r) => problems.push(`requestfailed: ${r.url()}`));
  page.on('request', (r) => { if (!r.url().startsWith(new URL(URL_).origin) && !r.url().startsWith('blob:') && !r.url().startsWith('data:')) problems.push(`external request: ${r.url()}`); });

  try {
    await page.goto(URL_);
    await page.waitForSelector('#pf-continue');
    assert.equal(await page.evaluate(() => window.__smartNoa.testClock(true)), true, 'step-driven test clock on');
    assert.match(await page.textContent('.sim-banner'), /SIMULATION — fictional patient — educational use only\. Not a medical device\. Not medical advice\./);

    await t.test('validation shows inline messages', async () => {
      await page.fill('#pf-ageYears', '12');
      await page.click('#pf-continue');
      await page.waitForSelector('#pf-error-summary:not([hidden])');
      assert.match(await page.textContent('#pf-ageYears-err'), /between 18 and 90/);
      assert.equal(await page.getAttribute('#pf-ageYears', 'aria-invalid'), 'true');
      await page.fill('#pf-ageYears', '34');
    });

    await t.test('each example case loads and all five steps render', async () => {
      for (const id of ['healthy_adult', 'elderly_cad_ckd', 'obese_osa', 'mh_susceptible']) {
        await loadCase(page, id);
        await toRun(page);
        await page.click('[data-step="60"]');
        await page.click('#mon-debrief'); await page.waitForSelector('#db-download');
        assert.ok(await page.isVisible('#db-totals') || (await page.textContent('main')).includes('No drugs given'), `${id}: debrief`);
      }
      // MH-susceptible: volatile agents and succinylcholine are locked
      await loadCase(page, 'mh_susceptible'); await page.click('#pf-continue'); await page.waitForSelector('#as-continue');
      const as = await page.textContent('main');
      assert.match(as, /Sevoflurane[\s\S]*LOCKED/); assert.match(as, /Succinylcholine/);
    });

    await t.test('GA case: induction → airway → maintenance → emergence → sugammadex → extubation (step-driven)', async () => {
      await loadCase(page, 'healthy_adult');
      await shot(page, '1-patient.png');
      await page.click('#pf-continue'); await page.waitForSelector('#as-continue'); await shot(page, '2-assessment.png');
      await page.click('#as-continue'); await page.waitForSelector('#pl-continue');
      assert.ok((await page.locator('.src-tag').count()) > 5, 'source tags on plan');
      await shot(page, '3-plan.png');
      await page.click('#pl-continue'); await page.waitForSelector('#mon-play');
      await adv(page, 760); // pre-op holding: premedication, then transfer/setup/preoxygenation time (manual mode)
      await page.selectOption('#aw-fio2', '1'); await page.click('#aw-fio2-set');
      await page.click('#ph-induction');
      await page.click('#aw-mask');
      await advanceUntil(page, (s) => s.v.tofCount === 0 && s.v.depthIndex < 60, { maxSec: 900, label: 'paralysis + depth' });
      await shot(page, '4-run-induction.png');
      await page.click('#ph-secure');
      await page.click('#ph-maintenance');
      await page.click('#ph-surgery');
      let s = await lite(page);
      assert.ok(s.phase === 'maintenance' && s.airway.secured && s.airway.ventilated, 'maintenance');
      await page.selectOption('#aw-fio2', '0.5'); await page.click('#aw-fio2-set');
      await advanceUntil(page, (x) => x.t > s.t + 600, { maxSec: 700, chunk: 60, label: '10 min maintenance' });
      assert.notEqual((await page.textContent('#tile-mac .tile__value')).trim(), '—', 'total MAC shown');
      // processed-EEG index tile and chart show the GA band (sourced)
      assert.match(await page.textContent('#tile-depth'), /Processed-EEG depth index \(BIS-style, simulated\)/);
      assert.match(await page.textContent('#tile-depth .tile__sub'), /GA band 40–60: (in band|below band|above band)/);
      assert.match(await page.textContent('#mon-charts figcaption >> nth=3'), /GA band 40–60/);
      assert.match(await page.textContent('#mon-depth-band'), /not a BIS value \(BIS is a trademark of its owner\)/);
      assert.ok(await page.locator('#mon-depth-band .src-tag', { hasText: 'S52' }).count());
      await shot(page, '4-run-maintenance.png');
      await page.click('#ph-endsurgery');
      await page.click('#ph-emergence');
      await advanceUntil(page, (x) => x.v.tofRatio >= 0.9, { maxSec: 900, label: 'TOFR ≥ 0.9 after sugammadex' });
      const log = (await simLog(page)).map((e) => e.message).join('\n');
      assert.match(log, /Sugammadex .* bolus/);
      s = await advanceUntil(page, (x) => x.ready, { maxSec: 1800, chunk: 1, label: 'extubation readiness' });
      assert.ok((await simState(page)).ready, 'readiness met (clock does not move between steps)');
      await page.click('#ph-extubate');
      await page.waitForSelector('#ext-confirm');
      await page.click('#ext-confirm');
      assert.equal((await simState(page)).airway.secured, false);
      await page.click('#mon-debrief'); await page.waitForSelector('#db-download');
      assert.match(await page.textContent('main'), /Extubated at .* readiness met/);
      assert.ok(await page.isVisible('#db-timing'), 'debrief timing check');
      assert.match(await page.textContent('#db-timing'), /Opioid → hypnotic/);
      await shot(page, '5-debrief.png');
      const [dl] = await Promise.all([page.waitForEvent('download'), page.click('#db-download')]);
      assert.match(dl.suggestedFilename(), /\.json$/);
    });

    await t.test('extubating when not ready asks for confirmation and is logged', async () => {
      await page.click('#stepper-4'); await page.waitForSelector('#mon-play');
      await page.click('#mon-debrief'); await page.click('.actions .btn:has-text("Run again")');
      await page.waitForSelector('#mon-play');
      // induction drugs wait for the 2-min premedication; propofol +3:00 and rocuronium +4:00 after fentanyl → asleep after 7 min
      await page.click('#ph-induction'); for (let i = 0; i < 7; i++) await page.click('[data-step="60"]');
      await page.click('#ph-secure');
      await page.click('#ph-extubate');
      await page.waitForSelector('#ext-override');
      await page.click('#ext-override');
      const log = (await simLog(page)).map((e) => e.message).join('\n');
      assert.match(log, /User confirmed extubation despite unmet readiness/);
    });

    await t.test('MAC moderate sedation case runs', async () => {
      await loadCase(page, 'obese_osa');
      await page.click('#pf-continue'); await page.waitForSelector('#as-continue'); await page.click('#as-continue'); await page.waitForSelector('#pl-continue');
      assert.ok(await page.isChecked('#pl-mode-MAC')); assert.ok(await page.isChecked('#pl-target-moderate'));
      assert.match(await page.textContent('#pl-target'), /exhaled-CO2/);
      await page.click('#pl-continue'); await page.waitForSelector('#ph-sedation');
      await page.selectOption('#aw-fio2', '0.4'); await page.click('#aw-fio2-set');
      await page.click('#ph-sedation');
      const s = await advanceUntil(page, (x) => x.t > 480, { maxSec: 600, chunk: 30, label: 'MAC 8 min' });
      assert.equal(s.phase, 'sedation');
      assert.ok(['awake', 'minimal', 'moderate', 'deep', 'general'].includes(s.v.sedationLevel));
    });

    await t.test('sensor fault: tiles show "—" (accessible label), RED/HOLD, charts gap, restored when the fault clears', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      await page.click('#ph-induction'); await page.click('[data-step="10"]');
      await page.click('#sf-hr_sensor_fault');
      await page.click('[data-step="1"]'); await page.click('[data-step="1"]');
      assert.equal((await page.textContent('#tile-hr .tile__value')).trim(), '—');
      assert.equal(await page.getAttribute('#tile-hr .tile__value', 'aria-label'), 'no valid reading (sensor fault)');
      assert.match(await page.textContent('#tile-hr .tile__sub'), /SENSOR FAULT/);
      assert.match(await page.textContent('#mon-il-badge'), /RED|HOLD/);
      assert.match(await page.textContent('#mon-alarms'), /HR sensor fault/);
      assert.doesNotMatch(await page.textContent('#mon-log'), /NaN/);
      assert.match(await page.textContent('#mon-log'), /HR=unusable/);
      const gap = await snap(page);
      assert.equal(gap.hist.hr[gap.hist.hr.length - 1], null, 'the trend chart holds a gap (NaN sample), not the stale value');
      await shot(page, '4-run-alarm.png');
      // MAP and SpO2 faults: BP tile shows "—" (no stale systolic/diastolic), advisory reads "MAP reading unusable"
      await page.click('#sf-map_sensor_fault'); await page.click('#sf-spo2_probe_off');
      await page.click('[data-step="1"]'); await page.click('[data-step="1"]');
      for (const k of ['bp', 'spo2']) {
        assert.equal((await page.textContent(`#tile-${k} .tile__value`)).trim(), '—', k);
        assert.equal(await page.getAttribute(`#tile-${k} .tile__value`, 'aria-label'), 'no valid reading (sensor fault)', k);
      }
      assert.doesNotMatch(await page.textContent('#tile-bp'), /\d+\/\d+|MAP \d/);
      assert.match(await page.textContent('#adv-headline'), /MAP reading unusable/);
      await page.click('[data-step="60"]'); await page.click('[data-step="60"]');
      assert.match(await page.textContent('#mon-il-badge'), /HOLD/);
      for (const k of ['hr', 'bp', 'spo2']) {
        const v = (await page.textContent(`#tile-${k} .tile__value`)).trim();
        assert.match(v, /^\d+(\/\d+)?$/, `${k} restored after the fault clears: ${v}`);
        assert.equal(await page.getAttribute(`#tile-${k} .tile__value`, 'aria-label'), null, k);
      }
      await page.click('#mon-resume');
      assert.match(await page.textContent('#mon-log'), /Hold released by explicit user action/);
    });

    await t.test('MH patient + sevoflurane example plan: blocking "no maintenance hypnotic" warning; clinician adds propofol TIVA; then Run', async () => {
      await loadCase(page, 'mh_susceptible');
      await page.click('#pf-continue'); await page.waitForSelector('#as-continue'); await page.click('#as-continue'); await page.waitForSelector('#pl-continue');
      await page.selectOption('#pl-example', 'ga_healthy'); await page.click('text=Load example plan');
      await page.waitForSelector('#pl-hypnotic-gap');
      const msg = await page.textContent('#pl-hypnotic-gap');
      assert.match(msg, /No maintenance hypnotic after lockout: Sevoflurane is contraindicated \(.*malignant hyperthermia.*\)\. Add an alternative \(e\.g\. propofol infusion — TIVA\) before approving\./);
      assert.equal(await page.getAttribute('#pl-continue', 'aria-disabled'), 'true');
      const before = await page.locator('#pl-phases .step-card').count();
      await page.click('#pl-continue', { force: true }); // aria-disabled: the click is refused and focuses the warning
      assert.equal(await page.locator('#mon-play').count(), 0, 'Run is not reachable while blocked');
      assert.equal(await page.evaluate(() => document.activeElement && document.activeElement.id), 'pl-add-tiva');
      assert.equal(await page.locator('#pl-phases .step-card').count(), before, 'nothing added automatically');
      await page.click('#pl-add-tiva');
      assert.equal(await page.locator('#pl-hypnotic-gap').count(), 0);
      assert.equal(await page.locator('#pl-phases .step-card').count(), before + 2);
      assert.match(await page.textContent('#pl-notice'), /Clinician added propofol TIVA maintenance/);
      assert.match(await page.textContent('#pl-phase-maintenance'), /Propofol/);
      assert.match(await page.textContent('body'), /Propofol TIVA maintenance added by the clinician/); // "Why this time"
      assert.equal(await page.getAttribute('#pl-continue', 'aria-disabled'), 'false');
      await page.click('#pl-continue'); await page.waitForSelector('#mon-play');
      assert.match(await page.textContent('#mon-log'), /added by the clinician with "Add propofol TIVA maintenance"/);
    });

    await t.test('guided mode, GA (elderly example): gates in order, countdowns, pre-incision verification, intubate/extubate only when conditions met', async () => {
      await loadCase(page, 'elderly_cad_ckd'); await toRun(page);
      await page.click('#auto-guided');
      assert.equal((await page.textContent('#auto-banner')).trim(), GUIDED_BANNER);
      assert.equal((await page.textContent('#auto-gate')).trim(), 'Transfer to OR');
      let s = await adv(page, 30);
      assert.ok(s.t >= 30 && s.phase === 'premed', `nothing advances without the gate (t ${s.t}, ${s.phase})`);
      await gate(page, 'Transfer to OR');
      // OR setup + preoxygenation: the induction gate counts down and stays disabled with the reason shown
      s = await advanceUntil(page, (x) => x.auto.current && x.auto.current.id === 'start_induction', { maxSec: 300, label: 'induction gate' });
      await page.waitForFunction(() => document.getElementById('auto-gate').textContent.trim() === 'Approve & start induction');
      assert.ok(await page.isDisabled('#auto-gate'), 'induction disabled during preoxygenation');
      assert.ok(await page.isVisible('#auto-countdown')); assert.match(await page.textContent('#auto-countdown'), /available in \d+:\d\d/);
      assert.match(await page.textContent('#auto-unmet'), /Preoxygenation 3 min/);
      await gate(page, 'Approve & start induction');
      s = await waitAuto(page, (x) => x.current && x.current.id === 'secure_airway', { label: 'intubate gate shown' });
      await page.waitForFunction(() => document.getElementById('auto-gate').textContent.trim() === 'Intubate (secure airway)');
      assert.ok(await page.isDisabled('#auto-gate'), 'Intubate disabled before intubating conditions');
      await shot(page, 'auto-guided-induction.png');
      s = await gate(page, 'Intubate (secure airway)');
      assert.ok(s.v.tofCount <= 1 && s.v.depthIndex <= 60, `intubating conditions when enabled: ${JSON.stringify(s.v)}`);
      s = await lite(page);
      assert.equal(s.auto.current.id, 'start_maintenance');
      s = await adv(page, 60);
      assert.equal(s.phase, 'induction', 'maintenance waits for the clinician');
      await gate(page, 'Enter maintenance mode');
      // pre-incision verification: no time-based start; the time out is the clinician's
      s = await advanceUntil(page, (x) => x.auto.current && x.auto.current.id === 'start_surgery', { maxSec: 60, label: 'pre-incision gate' });
      await page.waitForSelector('#auto-checklist:not([hidden])');
      assert.match(await page.textContent('#auto-checklist'), /WHO Surgical Safety Checklist “Time Out” confirmed by the clinician/);
      assert.ok(await page.locator('#auto-checklist .src-tag', { hasText: 'S54' }).count());
      s = await adv(page, 300);
      assert.equal(s.auto.current.id, 'start_surgery', 'surgery never starts on time alone');
      assert.ok(await page.isDisabled('#auto-gate'));
      assert.ok(await page.isVisible('#auto-timing-box')); assert.match(await page.textContent('#auto-timing'), /Premedication|Opioid → hypnotic|Antibiotic/);
      await shot(page, 'auto-guided-preincision.png');
      await gate(page, 'Start surgery (incision)', { clinician: true });
      assert.ok((await simLog(page)).some((e) => e.kind === 'auto' && /^AUTO: step 6\/10 — Start surgery \(approved by the clinician\)/.test(e.message)), 'surgery started through the gate (logged)');
      assert.ok((await simLog(page)).some((e) => /time out confirmed by the clinician/.test(e.message)));
      await shot(page, 'auto-guided-maintenance.png');
      await gate(page, 'End surgery');
      await gate(page, 'Enter emergence / extubation mode');
      s = await waitAuto(page, (x) => x.current && x.current.id === 'extubate', { label: 'extubate gate' });
      if (!s.ready) assert.ok(await page.isDisabled('#auto-gate'), 'Extubate disabled until ready');
      await gate(page, 'Extubate', { chunk: 1 });
      s = await waitAuto(page, (x) => x.state === 'completed', { label: 'guided GA complete' });
      assert.equal((await simState(page)).airway.secured, false);
      await page.click('#mon-debrief'); await page.waitForSelector('#db-auto-note');
      assert.match(await page.textContent('#db-auto-note'), /ran in guided mode/);
      assert.match(await page.textContent('main'), /Extubated at .* readiness met/);
      assert.match(await page.textContent('#db-timing'), /Opioid → hypnotic/); // the elderly example plan has no premedication
      assert.equal(await page.getAttribute('#db-timing [data-check="opioid_hypnotic"]', 'data-status'), 'pass');
      await shot(page, 'auto-guided-debrief.png');
    });

    await t.test('guided mode, MAC (obese OSA example) at 60×', async () => {
      await loadCase(page, 'obese_osa'); await toRun(page);
      await page.click('#auto-guided');
      assert.equal((await page.textContent('#auto-banner')).trim(), GUIDED_BANNER);
      await gate(page, 'Transfer to OR');
      await gate(page, 'Approve & start sedation');
      await gate(page, 'Start procedure', { clinician: true });
      const s = await waitAuto(page, (x) => x.current && x.current.id === 'end_procedure', { label: 'procedure started' });
      assert.equal(s.phase, 'sedation');
      await gate(page, 'End procedure');
      await waitAuto(page, (x) => x.state === 'completed', { label: 'guided MAC complete' });
      await page.click('#mon-debrief'); await page.waitForSelector('#db-auto-note');
      assert.match(await page.textContent('#db-auto-note'), /ran in guided mode/);
    });

    for (const [id, label] of [['mh_susceptible', 'GA (MH-susceptible TIVA)'], ['obese_osa', 'MAC (obese OSA)']]) {
      await t.test(`run full case automatically, ${label} at 60×`, async () => {
        await loadCase(page, id); await toRun(page);
        await page.click('#auto-full');
        assert.equal((await page.textContent('#auto-banner')).trim(), FULL_BANNER);
        await shot(page, `auto-full-${id}-running.png`);
        await waitAuto(page, (x) => x.state === 'completed', { label: `full ${id} complete` });
        const log = await simLog(page);
        assert.ok(log.some((e) => /time out confirmed by the clinician/.test(e.message)), 'time out confirmed by the user, not the runner');
        assert.ok(!log.some((e) => e.kind === 'auto' && /^AUTO: step .*time out/i.test(e.message)));
        assert.ok(log.some((e) => e.kind === 'auto' && /^AUTO: step 1\//.test(e.message)));
        assert.ok(!log.some((e) => /despite unmet readiness/.test(e.message)), 'override never used');
        await page.click('#mon-debrief'); await page.waitForSelector('#db-auto-note');
        assert.match(await page.textContent('#db-auto-note'), /ran automatically/);
        await shot(page, `auto-full-${id}-debrief.png`);
        const [dl] = await Promise.all([page.waitForEvent('download'), page.click('#db-download')]);
        const json = JSON.parse(await (await import('node:fs/promises')).readFile(await dl.path(), 'utf8'));
        assert.equal(json.automaticMode.used, true); assert.equal(json.automaticMode.mode, 'full'); assert.equal(json.summary.automatic.outcome, 'completed');
      });
    }

    await t.test('a manual control ends automatic mode', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      await page.click('#auto-full');
      await page.waitForSelector('#auto-takeover:not([hidden])');
      await page.click('[data-step="10"]');
      const s = await autoStatus(page);
      assert.equal(s.state, 'manual');
      assert.match(await page.textContent('#auto-status'), /ended \(manual control\)/);
      assert.match(await page.textContent('#mon-log'), /AUTO: Automatic demonstration ended — manual control used/);
      assert.ok(await page.isVisible('#auto-full'), 'start buttons offered again');
    });

    await t.test('guided gate: a double-click approves only the gate that was shown', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      await page.click('#auto-guided');
      await gate(page, 'Transfer to OR'); await gate(page, 'Approve & start induction');
      await advanceUntil(page, (x) => x.auto.current && x.auto.current.id === 'secure_airway' && x.auto.current.enabled, { maxSec: 900, label: 'intubate gate enabled', resolvePauses: true });
      await page.waitForFunction(() => { const b = document.getElementById('auto-gate'); return b.textContent.trim() === 'Intubate (secure airway)' && !b.disabled; });
      await page.dblclick('#auto-gate');
      await gateIdle(page);
      const s = await autoStatus(page);
      const ids = s.history.map((x) => x.id);
      assert.ok(ids.includes('secure_airway'), 'the shown gate was approved');
      assert.ok(!ids.includes('start_maintenance'), `the next gate was NOT approved by the second click: ${ids.join(',')}`);
      assert.equal(s.current.id, 'start_maintenance'); assert.equal(s.phase, 'induction');
    });

    await t.test('extubation dialog pauses the clock and re-checks readiness at confirm', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      // induction drugs wait for the 2-min premedication; propofol +3:00 and rocuronium +4:00 after fentanyl → asleep after 7 min
      await page.click('#ph-induction'); for (let i = 0; i < 7; i++) await page.click('[data-step="60"]');
      await page.click('#ph-secure');
      // This test checks the real play loop: wall clock on, explicit state conditions only.
      await page.evaluate(() => window.__smartNoa.testClock(false));
      await page.click('[data-speed="60"]'); await page.click('#mon-play');
      const t0 = (await lite(page)).t;
      await page.waitForFunction((x) => window.__smartNoa.lite().t > x, t0);
      await page.click('#ph-extubate'); await page.waitForSelector('#extubate-dialog[open]');
      const a = await simState(page);
      assert.equal(a.playing, false, 'clock paused while the dialog is open');
      await page.waitForTimeout(1000); // paused: no simulated time may pass (any advance would be a failure)
      const b = await simState(page);
      assert.equal(b.t, a.t, `sim time did not advance (${a.t} → ${b.t})`);
      assert.ok(await page.isVisible('#ext-override'), 'not ready: override path offered');
      await page.click('#extubate-dialog .btn:has-text("Cancel")');
      await page.waitForFunction((x) => { const l = window.__smartNoa.lite(); return l.playing && l.t > x; }, b.t);
      const c = await simState(page);
      assert.equal(c.playing, true, 'clock resumes after the dialog closes'); assert.ok(c.t > b.t);
      assert.equal(c.airway.secured, true, 'cancel keeps the tube');
      await page.click('#mon-play'); // pause
      await page.waitForFunction(() => !window.__smartNoa.lite().playing);
      await page.evaluate(() => window.__smartNoa.testClock(true));
    });

    await t.test('reading a citation or pre-filling the form does not end automatic mode', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      await page.click('#auto-full');
      await waitAuto(page, (x) => x.state === 'running' && x.t > 5, { label: 'auto running' });
      await page.click('#mon-ready-h ~ p .src-tag');
      await page.waitForSelector('#sources-dialog[open]');
      await page.click('#sources-dialog [data-close-dialog]');
      let s = await autoStatus(page);
      assert.notEqual(s.state, 'manual', `source tag did not end automatic mode (${s.state}: ${s.message})`);
      await page.waitForSelector('#mon-drugs button:has-text("Bolus")');
      await page.click('#mon-drugs button:has-text("Bolus")');
      s = await autoStatus(page);
      assert.notEqual(s.state, 'manual', 'drug-row Bolus (pre-fill only) did not end automatic mode');
      await page.click('#ad-give');
      s = await autoStatus(page);
      assert.equal(s.state, 'manual', 'giving a drug is manual control and ends automatic mode');
    });

    // ---- 2026-11 additions: technique templates, fluids, antibiotic checklist
    const toPlan = async (id) => { await loadCase(page, id); await page.click('#pf-continue'); await page.waitForSelector('#as-continue'); await page.click('#as-continue'); await page.waitForSelector('#pl-template'); };
    const elShot = async (sel, name) => { if (SHOTS) await page.locator(sel).screenshot({ path: join(SHOTS, name) }); };
    const planSteps = async () => (await snap(page)).plan.steps;

    await t.test('technique templates: selector, opioid-free plan with POFA note, opioid-based pretreatment, dexmedetomidine MAC', async () => {
      await toPlan('healthy_adult');
      const labels = await page.$$eval('#pl-template option', (o) => o.map((x) => x.textContent));
      for (const l of ['GA — opioid-based (balanced)', 'GA — opioid-free', 'MAC — dexmedetomidine', 'MAC — propofol (existing technique)']) assert.ok(labels.includes(l), l);
      await page.selectOption('#pl-template', 'ga_opioid');
      assert.ok(await page.isVisible('#pl-tpl-opt-lidocainePretreatment'));
      await elShot('#pl-template-card', 'template-selector.png');
      await page.click('#pl-template-apply');
      let st = await planSteps();
      const pre = st.find((x) => x.drugId === 'lidocaine' && x.regimenKey === 'pretreatment'); const pro = st.find((x) => x.drugId === 'propofol' && x.regimenKey === 'induction');
      assert.ok(pre && pro && pre.amount === 10 && pre.atSec < pro.atSec, JSON.stringify(st));
      for (const d of ['cefazolin', 'lactated_ringers', 'fentanyl', 'sevoflurane', 'sugammadex']) assert.ok(st.some((x) => x.drugId === d), d);
      assert.ok(await page.isVisible('#pl-template-notes [data-note="propofolPain"]'));
      assert.match(await page.textContent('#pl-template-notes'), /pH of 7 to 8\.5/);
      await page.selectOption('#pl-template', 'ga_opioid_free'); await page.click('#pl-template-apply');
      st = await planSteps();
      assert.ok(!st.some((x) => ['fentanyl'].includes(x.drugId)) && !st.some((x) => x.regimenKey === 'pretreatment'));
      for (const d of ['dexmedetomidine', 'ketamine', 'lidocaine', 'propofol']) assert.ok(st.some((x) => x.drugId === d), d);
      const pofa = await page.textContent('#pl-template-notes [data-note="pofa"]');
      assert.match(pofa, /POFA/); assert.match(pofa, /severe bradycardia/); assert.match(pofa, /S49/);
      assert.match(await page.textContent('#pl-notice'), /Omitted: lidocaine pretreatment/);
      assert.match(await page.textContent('#pl-summary'), /no blocking issues/);
      await elShot('#pl-template-card', 'opioid-free-pofa-note.png');
      await shot(page, 'opioid-free-plan.png');
      await page.selectOption('#pl-template', 'mac_dex'); await page.click('#pl-template-apply');
      const sp = (await snap(page)).plan;
      assert.equal(sp.mode, 'MAC');
      assert.ok(sp.steps.some((x) => x.drugId === 'dexmedetomidine' && x.kind === 'infusion' && x.rate === 0.5), JSON.stringify(sp.steps));
      assert.ok(await page.isVisible('#pl-template-notes [data-note="dexMac"]')); assert.ok(await page.isVisible('#pl-template-notes [data-note="dexInterlock"]'));
      assert.match(await page.textContent('#pl-template-notes'), /use a GA template/);
      await page.check('#pl-target-deep'); assert.match(await page.textContent('#pl-summary'), /no blocking issues/);
      await shot(page, 'dex-mac-plan.png');
    });

    await t.test('antibiotic in guided mode: a REQUIRED pre-incision item; the gate waits, nothing is given automatically; the clinician gives it', async () => {
      await toPlan('healthy_adult');
      assert.equal(await page.isChecked('#pl-abx-indicated'), true);
      const card = page.locator('.step-card', { hasText: 'Cefazolin' });
      await card.locator('[data-del]').click();
      assert.ok(!(await planSteps()).some((x) => x.kind === 'antibiotic'));
      assert.match(await page.textContent('#pl-timing'), /Antibiotic before incision/);
      assert.equal(await page.getAttribute('#pl-timing [data-check="antibiotic_window"]', 'data-status'), 'warn');
      await page.click('#pl-continue'); await page.waitForSelector('#mon-play');
      await page.click('#auto-guided');
      await gate(page, 'Transfer to OR'); await gate(page, 'Approve & start induction'); await gate(page, 'Intubate (secure airway)'); await gate(page, 'Enter maintenance mode');
      await advanceUntil(page, (s) => s.auto.current && s.auto.current.id === 'start_surgery', { maxSec: 60, label: 'pre-incision gate' });
      await page.waitForSelector('#auto-checklist:not([hidden])'); await page.waitForSelector('#auto-abx-give:not([hidden])');
      assert.equal(await page.getAttribute('#auto-checklist-items [data-item="antibiotic"]', 'data-met'), 'false');
      await clickIf(page, '#auto-timeout'); await clickIf(page, '#auto-depth-ack');
      const s0 = await adv(page, 240);
      const s1 = await snap(page);
      assert.equal(s1.auto.current.id, 'start_surgery', 'surgery has not started');
      assert.equal(s1.sim.prophylaxis.doses.length, 0, 'never given automatically');
      assert.ok(await page.isDisabled('#auto-gate'), `gate closed while the antibiotic is missing (t ${s0.t})`);
      await elShot('#auto-panel', 'antibiotic-checklist-guided.png');
      await page.click('#auto-abx-give');
      await gate(page, 'Start surgery (incision)', { clinician: true });
      const s2 = await snap(page);
      assert.equal(s2.sim.prophylaxis.doses[0].source, 'clinician');
      assert.equal(s2.sim.prophylaxis.window.ok, true);
      assert.ok(!s2.sim.alarms.some((a) => a.id === 'abx_window'), 'no window alarm');
      await shot(page, 'antibiotic-guided.png');
      await page.click('#auto-takeover');
    });

    await t.test('timing: plan Timing check panel, per-step timing rationale, gate countdown in guided mode', async () => {
      await toPlan('healthy_adult');
      await page.selectOption('#pl-template', 'ga_opioid'); await page.click('#pl-template-apply');
      await page.waitForSelector('#pl-timing [data-check="opioid_hypnotic"]');
      assert.equal(await page.getAttribute('#pl-timing [data-check="opioid_hypnotic"]', 'data-status'), 'pass');
      assert.equal(await page.getAttribute('#pl-timing', 'data-warnings'), '0');
      const fent = page.locator('.step-card', { hasText: 'Fentanyl' }).first();
      assert.match(await fent.locator('.timing-why').textContent(), /Why this time: Opioid given ~3 min before the hypnotic.*simulator convention/);
      assert.match(await page.locator('.step-card', { hasText: 'Dexamethasone' }).locator('.timing-why').textContent(), /PONV prophylaxis \(consensus\)/);
      assert.match(await page.locator('.step-card', { hasText: 'Cefazolin' }).locator('.timing-why').textContent(), /within 60 min before incision \(Bratzler\)/);
      assert.ok(await page.locator('.tl-legend li[title*="Why this time"]').count() > 5, 'timeline tooltips');
      // make propofol too early → the Timing check warns
      const pro = page.locator('.step-card', { hasText: 'Propofol' }).filter({ hasText: 'Induction regimen' }).first();
      const tIn = pro.locator('input.input--time'); await tIn.fill('00:30'); await tIn.dispatchEvent('change');
      await page.waitForSelector('#pl-timing [data-check="opioid_hypnotic"][data-status="warn"]');
      await elShot('#pl-timing', 'plan-timing-check.png');
      await tIn.fill('03:00'); await tIn.dispatchEvent('change');
      await page.waitForSelector('#pl-timing [data-check="opioid_hypnotic"][data-status="pass"]');
      // MAC dexmedetomidine: loading 10 min, then maintenance at 10:00 (shown and checked)
      await page.selectOption('#pl-template', 'mac_dex'); await page.click('#pl-template-apply');
      assert.ok((await planSteps()).some((x) => x.drugId === 'dexmedetomidine' && x.kind === 'infusion' && x.atSec === 600));
      assert.equal(await page.getAttribute('#pl-timing [data-check="loading_maintenance"]', 'data-status'), 'pass');
      // guided countdown before Transfer to OR (opioid template has midazolam premedication)
      await page.selectOption('#pl-template', 'ga_opioid'); await page.click('#pl-template-apply');
      await page.click('#pl-continue'); await page.waitForSelector('#mon-play');
      await page.click('#auto-guided');
      await adv(page, 60);
      await page.waitForSelector('#auto-countdown:not([hidden])');
      assert.match(await page.textContent('#auto-countdown'), /Transfer to OR: available in \d+:\d\d/);
      assert.ok(await page.isDisabled('#auto-gate'));
      await elShot('#auto-panel', 'guided-countdown.png');
      await gate(page, 'Transfer to OR');
      const s = await lite(page);
      assert.ok(s.t >= 420, `transfer no earlier than 5 min after the 2-min premedication (t ${s.t})`);
      await page.click('#auto-takeover');
    });

    await t.test('fluids panel: type, boluses and maintenance; debrief totals by type and lidocaine pretreatment', async () => {
      await toPlan('healthy_adult');
      await page.selectOption('#pl-template', 'ga_opioid'); await page.click('#pl-template-apply');
      await page.selectOption('#pl-fluid-maintenance', 'sodium_chloride_0_9');
      await page.click('#pl-fluid-bolus-250-maintenance');
      assert.ok((await planSteps()).some((x) => x.kind === 'fluid_bolus' && x.drugId === 'sodium_chloride_0_9' && x.phase === 'maintenance'));
      await page.click('#pl-continue'); await page.waitForSelector('#mon-fluids-panel');
      await page.click('[data-step="60"]'); // the planned LR maintenance (premedication 00:00) starts first
      assert.equal((await simState(page)).fluids.maintenance.fluidId, 'lactated_ringers');
      await page.selectOption('#fl-type', 'sodium_chloride_0_9'); await page.click('#fl-500');
      await page.fill('#fl-rate', '150'); await page.click('#fl-maint-set');
      for (let i = 0; i < 2; i++) await page.click('[data-step="60"]');
      const F = (await simState(page)).fluids;
      assert.equal(F.byType.sodium_chloride_0_9.bolusMl, 500);
      assert.equal(F.maintenance.fluidId, 'sodium_chloride_0_9'); assert.equal(F.maintenance.rateMlH, 150);
      assert.ok(F.byType.lactated_ringers.maintenanceMl > 0 || F.byType.sodium_chloride_0_9.maintenanceMl > 0);
      assert.match(await page.textContent('#mon-fluid-totals'), /0\.9% sodium chloride/);
      assert.match(await page.textContent('#fl-maint-state'), /150 mL\/h/);
      await elShot('#mon-fluids-panel', 'fluids-panel.png');
      // run the whole template case as the automatic demonstration (restarts from 00:00 with the plan), then debrief
      await page.click('#auto-full');
      await waitAuto(page, (s) => s.state === 'completed', { label: 'full case with fluids' });
      const s = await simState(page);
      assert.equal(s.fluids.byType.sodium_chloride_0_9.bolusMl, 250, 'planned 0.9% NaCl bolus ran');
      assert.ok(s.fluids.byType.lactated_ringers.maintenanceMl > 0, 'LR maintenance from the template ran across phases');
      assert.equal(s.prophylaxis.doses[0].drugId, 'cefazolin');
      await page.click('#mon-debrief'); await page.waitForSelector('#db-fluids');
      assert.match(await page.textContent('#db-fluid-totals'), /Lactated Ringer's/);
      assert.match(await page.textContent('#db-abx'), /in the recommended window/);
      assert.match(await page.textContent('#db-lido'), /including pretreatment 10 mg/);
      await shot(page, 'debrief-totals.png');
    });

    await t.test('palliative modules: banner, ethics panel from every screen, pain cases, review gate, EOL sedation gate, phone width', async () => {
      const PAL_BANNER = 'Palliative / end-of-life simulation — fictional patient — educational only. Not medical advice. Palliative sedation is not euthanasia or medical aid in dying; laws differ by jurisdiction.';
      const palSnap = () => page.evaluate(() => window.__smartNoa.pal());
      const palAdv = (min) => page.evaluate((n) => window.__smartNoa.palAdvance(n), min);
      const ethicsOpens = async (where) => { await page.click('#open-ethics'); await page.waitForSelector('#ethics-dialog[open]'); assert.match(await page.textContent('#ethics-body'), /Proportionality/, where); assert.ok(await noHScroll(page), `ethics h-scroll ${where}`); await page.click('#ethics-dialog [data-close-dialog]'); };
      /** Decides every pending review item (approve; rotation declined), re-querying after each click. */
      const decideAll = async () => { for (let i = 0; i < 12; i++) { const b = page.locator('#pal-review [data-decide]').first(); if (!(await b.count())) break; const kind = await b.evaluate((el) => el.closest('[data-kind]').dataset.kind); const ack = page.locator('#pal-review input[id^="ack-"]').first(); if (await ack.count()) await ack.check(); await (kind === 'rotation' ? page.locator('#pal-review [data-decide="decline"]').first() : b).click(); } };
      await page.evaluate(() => window.__smartNoa.palTestClock(true));
      for (const mod of ['pain', 'eol']) {
        await page.click(`#mod-${mod}`); await page.waitForSelector('#pal-example');
        assert.equal((await page.textContent('#pal-banner')).trim(), PAL_BANNER); assert.ok(await page.isVisible('#pal-banner')); assert.ok(await page.isVisible('#open-ethics')); assert.ok(!(await page.isVisible('#stepper-list')));
        const ids = await page.$$eval('#pal-example option', (o) => o.map((x) => x.value));
        assert.equal(ids.length, mod === 'pain' ? 4 : 2);
        for (const id of ids) {
          await page.click('#pal-stepper-1'); await page.selectOption('#pal-example', id); await page.click('#pal-load'); await ethicsOpens(`${id} case`);
          await page.click('#pal-continue'); await page.waitForSelector('#pal-approve'); await ethicsOpens(`${id} plan`);
          if (await page.locator('#pal-red').count()) {
            await page.click('#pal-approve'); await page.waitForSelector('#pal-approve-err'); // no reduction entered: blocked
            await page.fill('#pal-red', '25'); await page.click('#pal-convert'); await page.waitForSelector('#pal-conv-result.notice:not(.notice--warn)');
          }
          if (await page.locator('#pal-warn-ack').count()) await page.check('#pal-warn-ack');
          await page.click('#pal-approve'); await page.waitForSelector('#pal-play'); await ethicsOpens(`${id} run`);
          let s = await palAdv(1440 + 30);
          assert.equal(s.sim.tMin, 1440, `${id}: paused at the daily review`); assert.equal(s.sim.paused.reason, 'review');
          const doseBefore = s.sim.regimen.dose;
          s = await palAdv(60); assert.equal(s.sim.tMin, 1440, `${id}: nothing advances while the review is open`); assert.equal(s.sim.regimen.dose, doseBefore);
          await decideAll(); s = await palSnap(); assert.equal(s.sim.pendingReview, null, `${id}: review decided`);
          if (s.sim.paused) { for (const b of await page.locator('#pal-alarms button[data-ack]:not([disabled])').all()) await b.click(); await page.click('#pal-resume'); }
          s = await palAdv(360); assert.ok(s.sim.tMin > 1440, `${id}: runs after the review`);
          await page.click('#pal-debrief'); await page.waitForSelector('#pal-download'); await ethicsOpens(`${id} debrief`);
          const [dl] = await Promise.all([page.waitForEvent('download'), page.click('#pal-download')]); assert.match(dl.suggestedFilename(), /palliative/);
        }
      }
      // EOL refractory-delirium case: documentation gate, approvals, opioid-not-for-sedation, free text as text.
      await page.click('#pal-stepper-1'); await page.selectOption('#pal-example', 'refractory_delirium'); await page.click('#pal-load');
      await page.click('#pal-continue'); await page.click('#pal-approve'); await page.waitForSelector('#pal-sed');
      assert.equal(await page.locator('#pal-gate-missing li').count(), 6); assert.ok(await page.isDisabled('#pal-sed-set')); assert.ok(await page.isDisabled('#pal-sed-apply'));
      await page.selectOption('#pd-ref-symptom', 'agitation'); await page.check('#pd-tried-0'); await page.fill('#pd-ref-why', 'Persisting despite reversible-cause review');
      await page.selectOption('#pd-goc-with', 'surrogate'); await page.fill('#pd-goc-summary', 'Comfort is the priority');
      await page.click('#pd-save'); await page.waitForSelector('#pal-gate-missing');
      assert.equal(await page.locator('#pal-gate-missing li').count(), 4); assert.ok(await page.isDisabled('#pal-sed-set'), 'still disabled with 4 missing');
      await page.selectOption('#pd-consent-type', 'surrogate_consent'); await page.check('#pd-consent-depth'); await page.check('#pd-idt-specialist'); await page.fill('#pd-idt-note', 'Palliative team');
      await page.check('#pd-prognosis'); await page.check('#pd-jurisdiction'); await page.click('#pd-save'); await page.waitForSelector('#pal-gate-ok');
      assert.ok(await page.isEnabled('#pal-sed-set'));
      await page.selectOption('#pal-sed-target', '-2'); await page.click('#pal-sed-set'); await page.click('#pal-sed-bolus'); await page.click('#pal-sed-apply');
      let e = await palSnap(); assert.equal(e.sim.sedation.titrations.length, 3); assert.equal(e.sim.sedation.infusionMgH, 1);
      await page.click('#pal-opioid-sedate'); assert.match(await page.textContent('#pal-feedback'), /not used to produce sedation/);
      await page.fill('#pal-family', '<b>bold?</b> Daughter informed');
      await palAdv(600); e = await palSnap(); assert.equal(e.sim.sedation.infusionMgH, 1, 'no automatic escalation'); assert.ok(e.sim.alarms.every((a) => !a.acknowledged || a.active === false), 'never auto-acknowledged');
      await page.click('#pal-debrief'); await page.waitForSelector('#pal-db-doc');
      assert.equal(await page.textContent('#pal-db-family'), '<b>bold?</b> Daughter informed'); assert.equal(await page.locator('#pal-db-family b').count(), 0, 'free text never rendered as HTML');
      assert.ok(await page.locator('#pal-db-titrations tbody tr').count() >= 3);
      // Phone width: every palliative screen without page-level horizontal scroll.
      await page.setViewportSize({ width: 390, height: 844 });
      for (const n of [1, 2, 3, 4]) { await page.click(`#pal-stepper-${n}`); await page.waitForSelector(`#pal-root[data-step="${n}"]`); assert.ok(await noHScroll(page), `pal step ${n} at 390 px`); await shot(page, `phone-pal-${n}.png`, false); }
      await ethicsOpens('phone');
      await page.setViewportSize({ width: 1280, height: 900 });
      await page.click('#mod-periop'); await page.waitForSelector('#stepper-1'); // perioperative case resumes on the step it was on
      assert.ok(!(await page.isVisible('#pal-banner'))); assert.ok(!(await page.isVisible('#open-ethics'))); assert.ok(await page.isVisible('#stepper-list'));
    });

    await t.test('advisory rescue: appears on induced hypotension, accept gives the bolus, dismiss logs, phone width, debrief', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      assert.ok(await page.locator('#adv').isHidden(), 'no advisory at normal MAP');
      await page.selectOption('#cx-select', 'hemorrhage'); await page.click('#cx-inject');
      let s = await lite(page);
      for (let i = 0; i < 200 && !(await page.isVisible('#adv-accept')); i++) s = await adv(page, 5);
      assert.ok(await page.isVisible('#adv-accept'), `advisory accept visible (MAP ${s.v.map})`);
      assert.ok(s.v.map < 65, `MAP ${s.v.map} < 65`);
      assert.equal(await page.getAttribute('#adv', 'role'), 'region');
      assert.equal(await page.getAttribute('#adv-headline', 'aria-live'), 'polite');
      assert.equal((await page.textContent('#adv-disclaimer')).trim(), 'Advisory only — the simulator never gives a vasopressor by itself.');
      assert.match(await page.textContent('#adv-suggestion'), /Suggested: (Phenylephrine 40 mcg|Ephedrine 5 mg) IV bolus/);
      assert.ok(await page.locator('#adv-suggestion .src-tag').count(), 'source tag on the suggestion');
      let log = (await simLog(page)).map((e) => e.message);
      assert.ok(log.some((m) => /^ADVISORY shown: ADV-1/.test(m)));
      assert.ok(!log.some((m) => /^(Phenylephrine|Ephedrine) .* bolus/.test(m)), 'nothing given before accept');
      await page.setViewportSize({ width: 390, height: 844 });
      assert.ok(await noHScroll(page), 'advisory card at 390 px'); await shot(page, 'phone-advisory.png', false);
      await page.setViewportSize({ width: 1280, height: 900 });
      await page.click('#adv-accept');
      log = (await simLog(page)).map((e) => e.message);
      assert.ok(log.some((m) => /^ADVISORY accepted by the clinician: ADV-1 — (Phenylephrine 40 mcg|Ephedrine 5 mg) IV bolus \(clinician-approved\)/.test(m)));
      assert.ok(log.some((m) => /^(Phenylephrine 40 mcg|Ephedrine 5 mg) bolus/.test(m)), 'bolus given through the engine');
      assert.match(await page.textContent('#adv'), /Reassess|recovering|MAP/);
      // wait for the next suggestion (after the repeat interval, haemorrhage continues) and dismiss it
      for (let i = 0; i < 300 && !(await page.isVisible('#adv-dismiss')); i++) await adv(page, 5);
      assert.ok(await page.isVisible('#adv-dismiss'), 'second advisory');
      await page.click('#adv-dismiss');
      log = (await simLog(page)).map((e) => e.message);
      assert.ok(log.some((m) => /^ADVISORY dismissed by the clinician: ADV-\d+/.test(m)));
      assert.ok(await page.isHidden('#adv-accept'));
      const rec = await page.evaluate(() => window.__smartNoa.advisories());
      assert.equal(rec.accepted, 1); assert.equal(rec.dismissed, 1);
      await page.click('#mon-debrief'); await page.waitForSelector('#db-advisories');
      assert.match(await page.textContent('#db-advisories'), /accepted 1 · dismissed 1/);
      assert.ok(await page.locator('#db-advisory-list li[data-status="accepted"]').count() === 1);
    });

    await t.test('depth-low advisory (text only): sustained index < 40 in a GA run shows the card (Run + panel), Dismiss logs, no alarm/pause, debrief lists it, phone width', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      assert.ok(await page.locator('#dadv').isHidden(), 'no depth advisory before the case');
      await page.click('#auto-full');
      let s = await lite(page); const t0 = s.t;
      while (!(await page.isVisible('#dadv'))) {
        if (s.auto && s.auto.state === 'paused') { await userResolvesPause(page); s = await lite(page); continue; }
        if (s.auto && ['stopped', 'manual', 'completed'].includes(s.auto.state)) throw new Error(`runner ${s.auto.state} before the depth advisory`);
        if (s.t - t0 > 6000) throw new Error('depth advisory not shown within 6000 simulated s');
        s = await adv(page, 10);
      }
      const x = await snap(page);
      assert.equal(x.sim.phase, 'maintenance'); assert.ok(x.sim.v.depthIndex < 42, `index ${x.sim.v.depthIndex}`);
      assert.ok(x.periop && x.periop.depthAck, 'after the pre-incision acknowledgement (gate unchanged)');
      assert.equal(await page.getAttribute('#dadv', 'role'), 'region');
      assert.equal(await page.getAttribute('#dadv-headline', 'aria-live'), 'polite');
      assert.equal(await page.getAttribute('#auto-dadv-headline', 'aria-live'), 'off', 'only one copy announces');
      assert.ok(await page.isVisible('#auto-dadv'), 'guided/automatic panel copy');
      assert.match(await page.textContent('#dadv-headline'), /^Depth index \d+ below the GA target band \(40–60\) for ≥ 300 s — the anaesthetic may be deeper than needed\.$/);
      assert.match(await page.textContent('#dadv-foot'), /Advisory only — the simulator never changes a dose by itself\. Simulated index \(illustrative\); the 40–60 band is the protocol target used in B-Unaware\/BAG-RECALL/);
      for (const id of ['S52', 'S53']) assert.ok(await page.locator('#dadv-foot .src-tag', { hasText: id }).count(), id);
      const card = await page.textContent('#dadv');
      assert.ok(!/\d\s*(mg|mcg|mL\/h|%)/.test(card), `no dose/rate/percentage in the card: ${card}`);
      assert.equal(await page.locator('#dadv button:not(.src-tag)').count(), 1, 'Dismiss is the only control');
      assert.ok(!x.sim.alarms.some((a) => /depth_low/.test(a.id)), 'no alarm added');
      assert.equal((await lite(page)).auto.state, 'running', 'not paused by the advisory');
      let log = (await simLog(page)).map((e) => e.message);
      assert.ok(log.some((m) => /^ADVISORY shown: DEPTH-1 — processed-EEG depth index \d+ below the GA target band/.test(m)));
      await page.setViewportSize({ width: 390, height: 844 });
      assert.ok(await noHScroll(page), 'depth advisory card at 390 px'); await shot(page, 'phone-depth-advisory.png', false);
      await page.setViewportSize({ width: 1280, height: 900 });
      await page.click('#dadv-dismiss');
      log = (await simLog(page)).map((e) => e.message);
      assert.ok(log.some((m) => /^ADVISORY dismissed by the clinician: DEPTH-1 \(depth index below the GA band; text only, no dose changed\)\./.test(m)));
      assert.ok(await page.isHidden('#dadv')); assert.ok(await page.isHidden('#auto-dadv'));
      assert.ok((await lite(page)).auto, 'Dismiss does not end automatic mode');
      const rec = await page.evaluate(() => window.__smartNoa.depthAdvisories());
      assert.equal(rec.dismissed, 1); assert.ok(rec.shown >= 1);
      await waitAuto(page, (y) => y.state === 'completed', { label: 'full healthy GA complete' });
      await page.click('#mon-debrief'); await page.waitForSelector('#db-depth-advisories');
      assert.match(await page.textContent('#db-depth-advisories'), /Shown \d+ · dismissed 1 · time below the band \(40\)/);
      assert.ok(await page.locator('#db-depth-advisory-list li[data-status="dismissed"]').count() === 1);
      assert.match(await page.textContent('#db-depth-below'), /^\d{2}:\d{2}/);
      await page.setViewportSize({ width: 390, height: 844 });
      assert.ok(await noHScroll(page), 'debrief at 390 px');
      await page.setViewportSize({ width: 1280, height: 900 });
    });

    await t.test('depth-low and hypotension advisories together: both cards shown, each names the other', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      await adv(page, 760);
      await page.selectOption('#aw-fio2', '1'); await page.click('#aw-fio2-set');
      await page.click('#ph-induction'); await page.click('#aw-mask');
      await advanceUntil(page, (x) => x.v.tofCount === 0 && x.v.depthIndex < 60, { maxSec: 900, label: 'paralysis + depth' });
      await page.click('#ph-secure'); await page.click('#ph-maintenance'); await page.click('#ph-surgery');
      for (let i = 0; i < 300 && !(await page.isVisible('#dadv')); i++) await adv(page, 5);
      assert.ok(await page.isVisible('#dadv'), 'depth advisory in manual GA after incision');
      await page.selectOption('#cx-select', 'hemorrhage'); await page.click('#cx-inject');
      for (let i = 0; i < 300 && !((await page.isVisible('#adv')) && (await page.isVisible('#dadv')) && /hypotension/.test(await page.getAttribute('#adv', 'data-kind') || '')); i++) await adv(page, 5);
      assert.ok(await page.isVisible('#adv') && await page.isVisible('#dadv'), 'both cards visible');
      assert.match(await page.textContent('#adv-cross'), /depth-index advisory is also shown/);
      assert.match(await page.textContent('#dadv-cross'), /haemodynamic rescue advisory is also shown/);
      await page.setViewportSize({ width: 390, height: 844 });
      assert.ok(await noHScroll(page), 'both cards at 390 px'); await shot(page, 'phone-both-advisories.png', false);
      await page.setViewportSize({ width: 1280, height: 900 });
    });

    await t.test('example plans show "Why this time" rationale', async () => {
      await loadCase(page, 'obese_osa');
      await page.click('#pf-continue'); await page.waitForSelector('#as-continue');
      await page.click('#as-continue'); await page.waitForSelector('#pl-continue');
      const main = await page.textContent('main');
      assert.match(main, /First, given over at least 2 min/);
      assert.match(main, /Started 4 min after the midazolam starts/);
    });

    await t.test('self-pause re-renders the guided/automatic panel at once (no other control clicked)', async () => {
      for (const mode of ['full', 'guided']) {
        await loadCase(page, 'healthy_adult'); await toRun(page);
        await page.click('[data-speed="5"]');
        await page.evaluate(() => window.__smartNoa.testClock(false));
        await page.click(mode === 'full' ? '#auto-full' : '#auto-guided');
        await page.waitForFunction(() => window.__smartNoa.lite().t > 3);
        await page.evaluate(() => window.__smartNoa.inject('spo2_probe_off'));
        await page.waitForFunction(() => { const a = window.__smartNoa.lite().auto; return a && a.state === 'paused'; }, null, { timeout: 30000 });
        // NO click here: the panel must already show the pause, its reason and the action, with Resume enabled.
        await page.waitForSelector('#auto-paused:not([hidden])', { timeout: 5000 });
        const head = await page.textContent('#auto-paused-head');
        assert.match(head, /^Paused — HIGH alarm: .*SpO2|^Paused — HIGH alarm: /, `${mode}: ${head}`);
        assert.match(head, /then press Resume/, `${mode}: ${head}`);
        assert.match(await page.textContent('#auto-status'), /PAUSED/, `${mode} status`);
        assert.ok(await page.isEnabled('#auto-resume'), `${mode}: Resume enabled`);
        assert.ok(await page.isDisabled('#auto-pause'), `${mode}: Pause disabled`);
        assert.equal(await page.evaluate(() => document.activeElement && document.activeElement.id), 'auto-resume', `${mode}: focus moved to Resume`);
        await page.waitForFunction(() => /Paused/.test(document.getElementById('live-assertive').textContent), null, { timeout: 5000 });
        assert.ok((await lite(page)).playing === false, `${mode}: clock stopped`);
        await page.evaluate(() => window.__smartNoa.testClock(true));
        await page.click('[data-speed="20"]');
      }
    });

    await t.test('interlock hold pause offers "Continue with hold latched (this hold only)"; explicit, logged, alarms not acknowledged', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      await page.click('#auto-full');
      await adv(page, 5);
      await page.evaluate(() => window.__smartNoa.inject('map_sensor_fault'));
      // first pause: the HIGH sensor-fault alarm (the interlock reads the unusable value on the next second)
      await advanceUntil(page, (x) => x.auto && x.auto.state === 'paused', { maxSec: 30, chunk: 1, label: 'alarm pause' });
      assert.ok(await page.isHidden('#auto-hold-continue'), 'not offered for an alarm-only pause');
      for (const b of await page.locator('#mon-alarms button[data-ack]:not([disabled])').all()) await b.click();
      await page.click('#auto-resume');
      await advanceUntil(page, (x) => x.auto && x.auto.state === 'paused', { maxSec: 30, chunk: 1, label: 'hold pause' });
      await page.waitForSelector('#auto-hold-continue:not([hidden])');
      assert.match(await page.textContent('#auto-paused-head'), /Interlock hold latched/);
      await page.click('#auto-hold-continue');
      const log = await simLog(page);
      assert.ok(log.some((l) => /Clinician chose to continue with hold latched/.test(l.message)));
      const st = await snap(page);
      assert.ok(st.sim.interlocks.holdLatched, 'hold not released');
      const iChoice = log.findIndex((l) => /Clinician chose to continue with hold latched/.test(l.message));
      assert.ok(!log.slice(iChoice).some((l) => /Alarm acknowledged/.test(l.message)), 'the choice acknowledges no alarm');
      if (st.auto.state === 'paused') { for (const b of await page.locator('#mon-alarms button[data-ack]:not([disabled])').all()) await b.click(); await page.click('#auto-resume'); }
      const after = await adv(page, 20);
      assert.equal(after.auto.state, 'running', 'the same hold does not pause again');
      await page.click('#auto-takeover');
    });

    await t.test('user pause banner reads cleanly (no double period)', async () => {
      await loadCase(page, 'healthy_adult'); await toRun(page);
      await page.click('#auto-guided'); await page.click('#auto-pause');
      await page.waitForSelector('#auto-paused:not([hidden])', { timeout: 5000 });
      const head = await page.textContent('#auto-paused-head');
      assert.match(head, /^Paused — Paused by the user\. Press Resume/, head);
      assert.ok(!/\.\./.test(head), `no double period: ${head}`);
    });

    await t.test('alarm list is reconciled by id (audit 05l): the Acknowledge button of a HIGH alarm survives renders while its text changes; focus kept; a click acknowledges at 20x', async () => {
      await loadCase(page, 'elderly_cad_ckd'); await toRun(page);
      await page.evaluate(() => { window.__smartNoa.inject('hemorrhage'); window.__smartNoa.inject('anaphylaxis_hypotension'); });
      const crit = (x) => x.alarms && x.alarms.some((a) => a.id === 'map_critical' && a.active && !a.acknowledged);
      let x = await simState(page);
      for (let i = 0; i < 400 && !crit(x); i++) { await adv(page, 5); x = await simState(page); }
      assert.ok(crit(x), 'HIGH map_critical alarm active and unacknowledged');
      const sel = '#mon-alarms button[data-ack="map_critical"]';
      const btn = await page.$(sel);
      assert.ok(btn && await btn.isEnabled());
      const same = () => btn.evaluate((el, q) => el.isConnected && document.querySelector(q) === el, sel);
      const msgOf = (q) => { const el = document.querySelector(q); return el ? el.closest('li').querySelector('.alarm__msg').firstChild.textContent : null; };
      const msg = () => page.evaluate(msgOf, sel);
      // Other live lists with controls are keyed too: a source-tag button in the extubation readiness list
      // (its values change every render) is the same element across renders.
      const rtag = await page.$('#mon-ready .src-tag');
      const readyText = () => page.textContent('#mon-ready');
      const r0 = await readyText();
      await btn.focus();
      // Step-driven: several renders, the message text changes, the element (and focus) stays.
      const texts = new Set([await msg()]);
      for (let i = 0; i < 60 && texts.size < 4; i++) { await adv(page, 1); assert.ok(await same(), `same element after render ${i + 1}`); texts.add(await msg()); }
      assert.ok(texts.size >= 3, `message text changed across renders (${[...texts].join(' | ')})`);
      assert.ok(rtag, 'readiness list has a source-tag button'); assert.ok(await rtag.evaluate((el) => el.isConnected), 'readiness source-tag button kept'); assert.notEqual(await readyText(), r0, 'readiness values changed meanwhile');
      assert.ok(await btn.evaluate((el) => document.activeElement === el), 'focus kept on the Acknowledge button');
      // Real time at 20x (150 ms renders): the handle survives while the reading changes, then a click acknowledges.
      await page.click('[data-speed="20"]');
      await page.evaluate(() => window.__smartNoa.testClock(false));
      if (!(await lite(page)).playing) await page.click('#mon-play');
      const live = new Set([await msg()]);
      for (let i = 0; i < 40 && live.size < 4; i++) {
        await page.waitForFunction(([q, prev]) => { const el = document.querySelector(q); return !el || el.closest('li').querySelector('.alarm__msg').firstChild.textContent !== prev; }, [sel, await msg()], { timeout: 10000 });
        assert.ok(await same(), `same element while playing at 20x (text ${live.size})`);
        live.add(await msg());
      }
      assert.ok(live.size >= 3, `text changed while playing (${[...live].join(' | ')})`);
      assert.ok((await lite(page)).playing, 'clock still running');
      await btn.click();
      assert.ok(await same(), 'still the same element after the click');
      await page.waitForFunction((q) => document.querySelector(q).disabled, sel, { timeout: 5000 });
      x = await simState(page);
      assert.ok(x.alarms.some((a) => a.id === 'map_critical' && a.acknowledged), 'map_critical acknowledged by the click');
      assert.equal(await btn.textContent(), 'Acknowledged');
      if ((await lite(page)).playing) await page.click('#mon-play');
      await page.evaluate(() => window.__smartNoa.testClock(true));
      const log = (await simLog(page)).map((e) => e.message);
      assert.equal(log.filter((m) => m === 'Alarm acknowledged: map_critical').length, 1, 'acknowledged once, by the click');
    });

    await t.test('palliative alarm list is reconciled by id (audit 05l): the Acknowledge button survives refreshes (step-driven and on the 250 ms timer); focus kept; a click acknowledges', async () => {
      const palSnap = () => page.evaluate(() => window.__smartNoa.pal());
      const palAdv = (min) => page.evaluate((n) => window.__smartNoa.palAdvance(n), min);
      await page.evaluate(() => window.__smartNoa.palTestClock(true));
      await page.click('#mod-pain'); await page.waitForSelector('#pal-example');
      await page.click('#pal-stepper-1'); await page.selectOption('#pal-example', 'naive_ckd_inpatient'); await page.click('#pal-load');
      await page.click('#pal-continue'); await page.waitForSelector('#pal-approve');
      if (await page.isChecked('#pal-bowel')) await page.uncheck('#pal-bowel'); // scheduled opioid without a bowel regimen → LOW alarm (no pause)
      if (await page.locator('#pal-red').count()) { await page.fill('#pal-red', '25'); await page.click('#pal-convert'); }
      if (await page.locator('#pal-warn-ack').count()) await page.check('#pal-warn-ack');
      await page.click('#pal-approve'); await page.waitForSelector('#pal-play');
      let s = await palAdv(5);
      for (let i = 0; i < 60 && !s.sim.alarms.some((a) => !a.acknowledged); i++) s = await palAdv(5);
      const al = s.sim.alarms.find((a) => !a.acknowledged);
      assert.ok(al, `an unacknowledged palliative alarm (${JSON.stringify(s.sim.alarms.map((a) => a.id))})`);
      assert.equal(s.sim.paused, null, 'not paused (non-HIGH alarm), so the timer keeps refreshing the list');
      const sel = `#pal-alarms button[data-ack="${al.id}"]`;
      const btn = await page.$(sel);
      const same = () => btn.evaluate((el, q) => el.isConnected && document.querySelector(q) === el, sel);
      await btn.focus();
      for (let i = 0; i < 5; i++) { await palAdv(1); assert.ok(await same(), `same element after refresh ${i + 1}`); }
      assert.ok(await btn.evaluate((el) => document.activeElement === el), 'focus kept on the Acknowledge button');
      // Real 250 ms timer: several refreshes (the clock text changes), the element and focus stay.
      await page.evaluate(() => window.__smartNoa.palTestClock(false));
      const t0 = (await palSnap()).sim.tMin;
      await page.click('#pal-play'); await btn.focus(); // the click moved focus to Play; put it back on the button
      const t1 = (await palSnap()).sim.tMin;
      await page.waitForFunction((m) => window.__smartNoa.pal().sim.tMin > m + 3, Math.max(t0, t1), { timeout: 10000 });
      assert.ok(await same(), 'same element after timer refreshes');
      assert.ok(await btn.evaluate((el) => document.activeElement === el), 'focus kept while playing');
      await btn.click();
      await page.waitForFunction((q) => { const el = document.querySelector(q); return !el || el.disabled; }, sel, { timeout: 5000 });
      s = await palSnap();
      assert.ok(!s.sim.alarms.some((a) => a.id === al.id && !a.acknowledged), `${al.id} acknowledged by the click`);
      if ((await page.textContent('#pal-play')).trim() === 'Pause') await page.click('#pal-play');
      await page.evaluate(() => window.__smartNoa.palTestClock(true));
      await page.click('#mod-periop'); await page.waitForSelector('#stepper-1');
    });

    await t.test('panels open', async () => {
      for (const id of ['open-sources', 'open-model', 'open-about']) {
        await page.click(`#${id}`);
        const dlg = id.replace('open-', '') + '-dialog';
        await page.waitForSelector(`#${dlg}[open]`);
        await page.click(`#${dlg} [data-close-dialog]`);
      }
    });

    await t.test('no page-level horizontal overflow at 390 px', async () => {
      await page.setViewportSize({ width: 390, height: 844 });
      await loadCase(page, 'healthy_adult');
      const names = ['phone-1-patient.png', 'phone-2-assessment.png', 'phone-3-plan.png', 'phone-4-run.png', 'phone-5-debrief.png'];
      assert.ok(await noHScroll(page), 'step 1'); await shot(page, names[0], false);
      await page.click('#pf-continue'); await page.waitForSelector('#as-continue'); assert.ok(await noHScroll(page), 'step 2'); await shot(page, names[1], false);
      await page.click('#as-continue'); await page.waitForSelector('#pl-continue'); assert.ok(await noHScroll(page), 'step 3'); await shot(page, names[2], false);
      await page.click('#pl-continue'); await page.waitForSelector('#mon-play');
      await page.click('#ph-induction'); await page.click('[data-step="60"]'); await page.click('[data-step="60"]');
      assert.ok(await noHScroll(page), 'step 4'); await shot(page, names[3], false);
      await page.click('#mon-debrief'); await page.waitForSelector('#db-download'); assert.ok(await noHScroll(page), 'step 5'); await shot(page, names[4], false);
      await page.click('#open-sources'); assert.ok(await noHScroll(page), 'sources'); await page.click('#sources-dialog [data-close-dialog]');
    });

    assert.deepEqual(problems, [], `console/CSP/network problems:\n${problems.join('\n')}`);
  } finally {
    await browser.close();
    if (server) await new Promise((r) => { server.close(r); });
  }
});
Source and scope

Published from Smart-NOA-Controller/simulator/tests/ui.smoke.mjs at revision 6e5b205e3843. Full source text is retained; HTML presentation does not establish independent clinical review. In-memory simulation with no patient, monitor or pump interface; not clinically validated.