pediatric-ai-scribe-v3/test/assistant-mobile.test.js
Daniel 2c9c2d12c0 fix: one mobile menu, and sources reachable on a phone
Three faults, all visible on a phone at once.

Two hamburgers, neither useful. The assistant added a drawer button of its own
AND the app's button was still there, stacked on top of it. Worse, the assistant
hid the app sidebar unconditionally, so the app's button opened something
invisible. A phone now has exactly one menu — the app sidebar — and the assistant
only replaces it with its own rail on desktop.

The collapse control had nothing to collapse on a phone, where the menu is a
sheet rather than a rail. It closes the sheet instead.

Sources were `display:none` below 640px with no alternative, so every SRC chip in
an answer pointed at nothing. They now slide up as a dismissible sheet, opened by
a button that appears only when an answer actually has citations, or by tapping a
citation itself. Tapping away or pressing Escape closes it — a sheet with no way
out is a trap.

The menu itself is sized for a thumb, borrowing the reference layout: a wide
sheet, close and search leading, tappable rows, and padding clear of the home
indicator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0161xNW1z4vPusrXKGWcQdQu
2026-09-10 10:19:20 +02:00

59 lines
3.3 KiB
JavaScript

const test = require('node:test');
const assert = require('node:assert/strict');
const fs = require('node:fs');
const path = require('node:path');
const read = f => fs.readFileSync(path.join(__dirname, '..', f), 'utf8');
// On a phone the assistant showed TWO hamburgers, stacked, and neither did
// anything useful: the app's opened a sidebar the assistant had hidden, and the
// assistant's opened a rail that duplicated it.
test('a phone has exactly one menu button', () => {
const assistant = read('public/components/assistant.html');
const index = read('public/index.html');
assert.doesNotMatch(assistant, /btn-assistant-mobile-menu/,
'the assistant no longer adds a drawer button of its own');
assert.equal((index.match(/id="btn-menu-toggle"/g) || []).length, 1,
'the app sidebar button is the only one');
});
test('the app sidebar stays reachable inside the assistant on a phone', () => {
const css = read('public/css/styles.css');
// Hiding it unconditionally is what made the app hamburger open nothing.
assert.match(css, /@media\(min-width:769px\)\{ body\.assistant-workspace \.sidebar\{ display:none; \} \}/,
'the sidebar is only replaced by the assistant rail on desktop');
const hideBlock = css.slice(css.indexOf('body.assistant-workspace .announcement-banner'), css.indexOf('body.assistant-workspace .app-body'));
assert.doesNotMatch(hideBlock, /\.sidebar/, 'nothing hides it unconditionally');
});
test('the collapse control closes the sheet on a phone rather than collapsing a rail', () => {
const app = read('public/js/app.js');
const handler = app.slice(app.indexOf("event.target.closest('[data-menu-toggle]')"), app.indexOf("localStorage.setItem('ped_sidebar_collapsed'"));
assert.match(handler, /if \(window\.innerWidth <= 768\)/);
assert.match(handler, /sidebar\.classList\.remove\('open'\)/, 'it closes the sheet');
});
test('sources are reachable on a phone instead of deleted', () => {
const css = read('public/css/assistant.css');
const mobile = css.slice(css.indexOf('@media (max-width: 640px)'));
// display:none left every citation pointing at nothing.
assert.doesNotMatch(mobile.slice(0, 2000), /\.assistant-side \{ display:none; \}/);
assert.match(mobile, /body\.assistant-sources-open \.assistant-side \{ transform:translateY\(0\); \}/,
'they slide up as a sheet');
const js = read('public/js/clinicalAssistant.js');
assert.match(js, /button\.hidden = !\(citationsOn && count > 0\);/,
'the opener appears only when an answer has citations');
assert.match(js, /event\.target\.closest\('\.assistant-cite'\)/, 'and tapping a citation opens it');
// A sheet with no way out is a trap.
assert.match(js, /if \(event\.key !== 'Escape'\) return;[\s\S]{0,160}assistant-sources-open/);
assert.match(js, /if \(!document\.body\.classList\.contains\('assistant-sources-open'\)\) return;/,
'tapping away closes it');
});
test('the mobile menu is sized for a thumb', () => {
const css = read('public/css/styles.css');
const mobile = css.slice(css.indexOf('/* ── Mobile menu ─'));
assert.match(mobile, /\.sidebar\{ width:min\(88vw,360px\)/, 'a sheet, not a narrow rail');
assert.match(mobile, /\.tab-btn\{ padding:12px 10px; font-size:14\.5px/, 'rows are tappable');
assert.match(mobile, /env\(safe-area-inset-bottom\)/, 'and clear of the home indicator');
});