Measured in real Chromium (1440x900 and 390x844) rather than inferred. Desktop - The assistant rail sat inside the content area's 14px padding, on the tiled ground. So its background differed from the workspace sidebar's, its name card sat 6px lower and further in, and the page was 28px taller than the window. It is now the same flush white sidebar with the same insets; the chat and sources panels carry the 14px. Measured: name card 848-892 at x=8, brand, switch and 210px width identical in both views, expanded and collapsed, with no page scroll. Phone - The menu button opens the chat history in the assistant (its drawer) and the workspace list elsewhere. In workspace mode the drawer lists the workspace links. Choosing a page, switching mode or pressing « closes it. - The drawer no longer scrolls as a whole: only the chat list does, so the name card is pinned at the foot, the same place as in the workspace menu (790-836 at x=12 in both). - Both menus are one plain shape: same width, text rows, grey current row, no "Menu" header and one close control. - The assistant page no longer overflows the phone by 16px. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01W4rptBNvn6RYieQw54GXNS
142 lines
8.8 KiB
JavaScript
142 lines
8.8 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, /closeMobileMenus\(\)/, 'it closes whichever menu is open');
|
|
const close = app.slice(app.indexOf('function closeMobileMenus()'), app.indexOf('function closeMobileMenus()') + 400);
|
|
assert.match(close, /appMenu\.classList\.remove\('open'\)/, 'the workspace menu');
|
|
assert.match(close, /layout\.classList\.remove\('mobile-chats-open'\)/, 'and the chat history');
|
|
});
|
|
|
|
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\(85vw,320px\)/, 'a sheet the width of the chat drawer, not a narrow rail');
|
|
assert.match(mobile, /\.tab-btn\{ padding:11px 12px; font-size:14px/, 'rows are tappable');
|
|
// Plain, like the chat history: text rows and a grey current row.
|
|
assert.match(mobile, /\.tab-btn i\{ display:none; \}/);
|
|
assert.match(mobile, /\.tab-btn\.active\{ border-left:none; background:var\(--g100\)/);
|
|
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\) closeMobileMenus\(\);/);
|
|
|
|
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');
|
|
});
|
|
|
|
test('on a phone the menu follows the view: chat history in the assistant', () => {
|
|
const app = read('public/js/app.js');
|
|
// The assistant used to open the workspace list, so a saved chat was
|
|
// unreachable on a phone.
|
|
const handler = app.slice(app.indexOf("e.target.closest('#btn-menu-toggle')"), app.indexOf("e.target.closest('#btn-sidebar-close')"));
|
|
assert.match(handler, /window\.innerWidth <= 640 && document\.body\.classList\.contains\('assistant-workspace'\)/);
|
|
assert.match(handler, /railLayout\.classList\.toggle\('mobile-chats-open'\)/, 'the assistant opens its chat drawer');
|
|
assert.match(handler, /else if \(sidebar\) \{\s*sidebar\.classList\.toggle\('open'\)/, 'everywhere else, the workspace list');
|
|
assert.match(app, /if \(window\.innerWidth <= 768\) closeMobileMenus\(\);\s*\n\s*return true;/, 'choosing a page closes it');
|
|
|
|
const css = read('public/css/assistant.css');
|
|
const mobile = css.slice(css.indexOf('@media (max-width: 640px)'));
|
|
// The name card is pinned at the foot of both menus: only the list scrolls.
|
|
assert.match(mobile, /\.assistant-history \{ position:fixed;[^}]*padding:0; overflow:hidden;/, 'the drawer itself does not scroll');
|
|
assert.match(mobile, /#assistant-saved-chats \{ flex:1 1 auto; min-height:0; overflow-y:auto;/, 'the chat list does');
|
|
assert.match(mobile, /\.assistant-drawer-close \{ display:none !important; \}/, 'one close control, the « in the head row');
|
|
});
|