Phone - The desktop collapse rules (52px icon rail) were unscoped, and menu-hidden is remembered across viewports, so after collapsing on a desktop the phone sheet opened with its links, the Assistant/Workspace switch and the account name all display:none. Scoped them to min-width:769px. - Workspace pill: the sheet now closes on tap, and the launcher is no longer display:none below 640px, so the pill opens the workspace instead of toggling a view that could never appear. - The menu button has its own 48px row instead of floating over the page; layouts subtract --mobile-bar (0px on desktop, so desktop is unchanged). - Sources open as their own scrollable band under that row, and the toggle sits in the row, instead of a sheet/FAB covering the composer. - The sheet uses dvh, so the account card is not pushed below the visible screen by the browser toolbar. Admin - Image Generation card beside TTS and STT: search the gateway's image models and test one. No Set button: an image model is chosen per workflow. It searches only on demand, so opening Admin still makes one discovery call. - Account card sits at the same height in every rail state; the model availability list points to where new models are added. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
118 lines
6.9 KiB
JavaScript
118 lines
6.9 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 \{ display:flex; height:min\(46vh,340px\); \}/,
|
|
'they open into a band of their own rather than covering the composer');
|
|
assert.doesNotMatch(mobile, /\.assistant-side \{ position:fixed;/,
|
|
'a fixed overlay sat on top of the question box');
|
|
const sources = mobile.slice(mobile.indexOf('.assistant-sources {'));
|
|
assert.match(sources.slice(0, 200), /overflow-y:auto/, 'the list inside the band scrolls');
|
|
|
|
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');
|
|
});
|
|
|
|
test('the account menu escapes the collapsed rail instead of being clipped', () => {
|
|
const css = read('public/css/styles.css');
|
|
// .sidebar has overflow:hidden and the collapsed rail is 52px, so a menu laid
|
|
// out inside it was cut to a sliver — hiding Settings, FAQ and Log out behind
|
|
// the avatar exactly when the rail is narrow.
|
|
const sidebar = css.split('\n').find(l => l.startsWith('.sidebar{'));
|
|
assert.match(sidebar, /overflow:hidden/, 'the rail does clip, so the menu must escape it');
|
|
assert.match(css, /body\.menu-hidden \.account-menu \{ position:fixed;/,
|
|
'the collapsed menu is positioned against the viewport, not the rail');
|
|
assert.match(css, /body\.menu-hidden \.account-menu \{[^}]*width:210px/, 'and keeps a readable width');
|
|
});
|
|
|
|
test('the account card sits at the same height in both rails', () => {
|
|
const css = read('public/css/styles.css');
|
|
// The app column pads its bottom; the assistant rail did not, so its card
|
|
// started lower than the app's.
|
|
const app = css.split('\n').find(l => l.startsWith('.account-card {'));
|
|
const assistant = css.split('\n').find(l => l.startsWith('.assistant-history .account-card {'));
|
|
assert.match(app, /padding:8px;/);
|
|
assert.match(assistant, /padding:8px 0;/, 'same vertical padding, so the cards align');
|
|
assert.match(css, /\.account-card-btn \{ min-height:44px; \}/,
|
|
'and the same row height in both');
|
|
});
|
|
|
|
test('a desktop collapse does not follow the user onto a phone', () => {
|
|
const css = read('public/css/styles.css');
|
|
// menu-hidden is remembered in localStorage across viewports. Unscoped, its
|
|
// 52px-rail rules hid .sidebar-tabs, the Assistant/Workspace switch and the
|
|
// account name INSIDE the phone sheet — the menu opened onto a strip of icons
|
|
// and every link looked broken.
|
|
const start = css.indexOf('@media (min-width:769px) {\n /* Collapsing is a desktop idea');
|
|
assert.ok(start !== -1, 'the collapse rules are wrapped in a desktop-only query');
|
|
const block = css.slice(start, css.indexOf('\n}\n', start));
|
|
assert.match(block, /body\.menu-hidden \.sidebar-tabs/, 'including the one that hid the links');
|
|
assert.match(block, /body\.menu-hidden \.assistant-mode-switch/, 'and the one that hid Workspace');
|
|
assert.match(block, /body\.menu-hidden \.account-menu \{ position:fixed;/);
|
|
// Nothing outside that query may still collapse the phone sheet.
|
|
const outside = css.slice(0, start) + css.slice(css.indexOf('\n}\n', start));
|
|
assert.doesNotMatch(outside, /body\.menu-hidden \.sidebar \{ width:/);
|
|
});
|
|
|
|
test('the Workspace pill works on a phone', () => {
|
|
const app = read('public/js/app.js');
|
|
// The switch lives inside the sheet, so the view it changes is behind the
|
|
// sheet until the sheet closes.
|
|
assert.match(app, /\[data-assistant-mode\]'\)\)\) return;\s*\n\s*if \(window\.innerWidth <= 768 && sidebar\) sidebar\.classList\.remove\('open'\);/);
|
|
|
|
const css = read('public/css/assistant.css');
|
|
// The launcher used to be display:none below 640px, so the pill toggled a
|
|
// view that could never appear.
|
|
assert.doesNotMatch(css, /body\.assistant-mode-workspace \.assistant-workspace-view \{ display:none; \}/);
|
|
assert.match(css, /\.assistant-workspace-cards \{ grid-template-columns:1fr; \}/,
|
|
'and its cards stack into one column');
|
|
});
|