'use strict'; const test = require('node:test'); const assert = require('node:assert/strict'); const api = require('../../dockerfiles/hermes-webui-hux/bootstrap.js'); const IDENTITY = Object.freeze({tenant_slot: 'slot-3', subject: 'usr_0123456789abcdef', surface: 'chat', trust: 'relay'}); function session(overrides) { return {session_id: 'webui-session-1', hux_context: {schema: 'hux.webui_context.v1', webui_session_id: 'webui-session-1', session_id: 'ses_session1234', conversation_id: 'conv_converse1', project_id: 'prj_project123', project_source: 'profile:default', identity: IDENTITY, ...overrides}}; } function card(name, enabled, routes) { const flags = {'HUX-11': 'hux.foundation', 'HUX-01': 'hux.activity_timeline', 'HUX-02': 'hux.memory_control', 'HUX-03': 'hux.projects', 'HUX-04': 'hux.artifacts', 'HUX-05': 'hux.autonomy', 'HUX-06': 'hux.friendly_modes', 'HUX-07': 'hux.multimodal', 'HUX-08': 'hux.research', 'HUX-09': 'hux.onboarding', 'HUX-10': 'hux.privacy', 'HUX-12': 'hux.release_followthrough'}; return {card: name, flag: flags[name], enabled, routes: routes || []}; } function capability(cards, overrides) { return {schema: 'hux.capabilities.v1', contract_version: '1.1.0', identity: IDENTITY, cards: [card('HUX-11', true, ['/hux/v1/capabilities', '/hux/v1/context/bootstrap']), ...(cards || [])], server: {}, ...(overrides || {})}; } function response(status, body) { return {status, ok: status >= 200 && status < 300, json: async () => body}; } function binding(value, overrides) { const context = api.normalizeContext(value || session()); return {schema: 'hux.context_bootstrap.v1', contract_version: '1.1.0', identity: IDENTITY, session_id: context.sessionId, project_id: context.projectId, conversation_id: context.conversationId, revisions: {project: 1, conversation: 1}, created: {project: false, conversation: false}, ...(overrides || {})}; } function huxFetcher(cards, sessionReader, calls) { return async (url, init) => { if (calls) calls.push({url, init}); return url === '/hux/v1/context/bootstrap' ? response(200, binding(sessionReader())) : response(200, cards); }; } class FakeNode { constructor(tag) { this.tagName = tag.toUpperCase(); this.attributes = {}; this.children = []; this.parentNode = null; this.listeners = {}; this.hidden = false; this.textContent = ''; } setAttribute(name, value) { this.attributes[name] = String(value); } getAttribute(name) { return this.attributes[name]; } appendChild(child) { child.parentNode = this; this.children.push(child); return child; } removeChild(child) { this.children.splice(this.children.indexOf(child), 1); child.parentNode = null; } addEventListener(name, callback) { this.listeners[name] = callback; } trigger(name, event) { if (this.listeners[name]) this.listeners[name](event || {}); } focus() { this.focused = true; } } class FakeDocument { constructor() { this.body = new FakeNode('body'); this.listeners = {}; this.readyState = 'complete'; } createElement(tag) { return new FakeNode(tag); } addEventListener(name, callback) { this.listeners[name] = callback; } removeEventListener(name, callback) { if (this.listeners[name] === callback) delete this.listeners[name]; } trigger(name, event) { if (this.listeners[name]) this.listeners[name](event || {}); } } function fakeRoot(document) { const listeners = {}; const intervals = new Map(); let sequence = 0; return {document, __HERMES_CONFIG__: {csrfToken: '0123456789abcdef'}, fetch: null, setTimeout, clearTimeout, setInterval(callback, delay) { const id = ++sequence; intervals.set(id, {callback, delay}); return id; }, clearInterval(id) { intervals.delete(id); }, addEventListener(name, callback) { listeners[name] = callback; }, removeEventListener(name, callback) { if (listeners[name] === callback) delete listeners[name]; }, listeners, intervals}; } test('trusted context accepts exact opaque scope and rejects browser-controlled substitutes', () => { const normalized = api.normalizeContext(session({run_id: 'run_alpha1234', message_id: 'msg_message9', branch_point_message_id: 'msg_message8', notebook_id: 'nb_notebook1'})); assert.equal(normalized.identity.tenantSlot, 'slot-3'); assert.equal(normalized.conversationId, 'conv_converse1'); assert.equal(normalized.runId, 'run_alpha1234'); assert.equal(Object.isFrozen(normalized.identity), true); const invalid = [null, [], {}, {session_id: 'x', hux_context: {}}, {hux_context: session().hux_context}, session({schema: 'old'}), session({webui_session_id: 'another'}), session({session_id: 'bad'}), session({conversation_id: 'bad'}), session({project_id: 'bad'}), session({project_source: 'bad/source'}), session({unexpected: true}), session({identity: null}), session({identity: {...IDENTITY, tenant_slot: 'tenant'}}), session({identity: {...IDENTITY, subject: 'raw-subject'}}), session({identity: {...IDENTITY, surface: 'browser'}}), session({identity: {...IDENTITY, trust: 'cookie'}}), session({run_id: 'run-7'}), session({message_id: ''}), session({branch_point_message_id: 'bad message'}), session({notebook_id: 'bad'})]; invalid.forEach((item) => assert.equal(api.normalizeContext(item), null)); }); test('chat-start lifecycle context is copied only from an exact trusted server response', () => { const current = session(); const enriched = session({run_id: 'run_turn1234', message_id: 'msg_message9'}).hux_context; assert.equal(api.mergeTrustedContext(current, {session_id: current.session_id, hux_context: enriched}), true); assert.equal(api.normalizeContext(current).runId, 'run_turn1234'); assert.equal(api.mergeTrustedContext(current, {session_id: 'another', hux_context: enriched}), false); assert.equal(api.mergeTrustedContext(current, {session_id: current.session_id, hux_context: {...enriched, run_id: 'stream-forged'}}), false); assert.equal(api.mergeTrustedContext(null, {}), false); }); test('owned stop uses only the current server session stream and stays bounded', async () => { const current = session(); current.active_stream_id = 'stream-owned-1'; const context = api.normalizeContext(current); let active = 'stream-owned-1'; const calls = []; const root = {setTimeout, clearTimeout, async cancelStream(reason) { calls.push(reason); return true; }}; const control = api.createOwnedStop(root, () => current, () => active, context, 250); assert.equal(api.ownedActiveStream(current, active, context), active); assert.equal(control.canStopModelResponse(), true); assert.deepEqual(await control.stopModelResponse(), {accepted: true, sessionId: 'webui-session-1', streamId: active}); assert.deepEqual(calls, ['hux-stop']); active = 'browser-forged'; assert.equal(control.canStopModelResponse(), false); assert.deepEqual(await control.stopModelResponse(), {accepted: false}); const stale = session(); stale.active_stream_id = 'stream-owned-1'; assert.equal(api.ownedActiveStream(stale, stale.active_stream_id, api.normalizeContext(session({conversation_id: 'conv_other1234'}))), null); const slow = api.createOwnedStop({setTimeout, clearTimeout, cancelStream: () => new Promise(() => {})}, () => current, () => current.active_stream_id, context, 250); assert.deepEqual(await slow.stopModelResponse(), {accepted: false, sessionId: 'webui-session-1', streamId: 'stream-owned-1'}); }); test('capabilities require exact identity, cards, routes and complete wave advertisements', () => { const payload = capability([ card('HUX-01', true, ['/hux/v1/conversations/{id}/events']), card('HUX-03', true, ['/hux/v1/projects/{id}', '/hux/v1/conversations', '/hux/v1/conversations/{id}', '/hux/v1/conversations/{id}/lineage']), card('HUX-05', true, ['/hux/v1/policy', '/hux/v1/approvals', '/hux/v1/approvals/{id}', '/hux/v1/runs/{id}/stop', '/hux/v1/runs/{id}/budget', '/hux/v1/runs/{id}/gate']), card('HUX-10', true, ['/hux/v1/privacy/policy', '/hux/v1/privacy/notices', '/hux/v1/conversations/{id}/forget', '/hux/v1/privacy/audit']), card('HUX-12', true, ['/hux/v1/projects/{project_id}/conversations/{id}/releases', '/hux/v1/projects/{project_id}/conversations/{id}/releases/{release_id}', '/hux/v1/projects/{project_id}/conversations/{id}/releases/{release_id}/transitions']), ]); const expected = api.normalizeContext(session()).identity; const cards = api.normalizeCapabilities(payload, expected); assert.deepEqual(api.enabledWaves(cards), ['a', 'b', 'governance', 'c']); assert.equal(api.cardComplete(cards, 'HUX-01'), true); assert.equal(api.cardComplete(cards, 'HUX-02'), false); assert.equal(Object.isFrozen(cards['HUX-01'].routes), true); const incomplete = api.normalizeCapabilities(capability([ card('HUX-01', true, []), card('HUX-03', true, ['/hux/v1/projects/{id}'])]), expected); assert.deepEqual(api.enabledWaves(incomplete), []); const invalid = [null, [], {...payload, schema: 'old'}, {...payload, contract_version: '2.0.0'}, {...payload, unexpected: true}, {...payload, identity: {...IDENTITY, unexpected: true}}, {...payload, identity: {...IDENTITY, subject: 'usr_ffffffffffffffff'}}, {...payload, server: null}, {...payload, server: {unexpected: 'x'}}, {...payload, server: {commit: 'x'.repeat(161)}}, {...payload, cards: null}, {...payload, cards: Array(13).fill(payload.cards[0])}, capability([card('HUX-01', true, []), card('HUX-01', false, [])]), capability([{...card('HUX-01', true, []), extra: true}]), capability([{...card('HUX-01', true, []), flag: 'wrong'}]), capability([{...card('HUX-01', true, []), enabled: 'yes'}]), capability([{...card('HUX-01', true, []), routes: null}]), capability([{...card('HUX-01', true, []), card: 'HUX-99'}]), capability([{...card('HUX-01', true, []), routes: ['/outside']}]), capability([{...card('HUX-01', true, []), routes: ['/hux/v1/memory', '/hux/v1/memory']}]), capability([], {cards: [card('HUX-11', false, ['/hux/v1/capabilities'])]}), capability([], {cards: [card('HUX-11', true, [])]}), capability([], {cards: [card('HUX-11', true, ['/hux/v1/capabilities'])]})]; invalid.forEach((item) => assert.equal(api.normalizeCapabilities(item, expected), null)); }); test('scoped transport is same-origin, no-store and adds CSRF only to mutations', async () => { const calls = []; const fetcher = api.scopedFetcher(async (url, init) => { calls.push({url, init}); return response(200, {}); }, '0123456789abcdef'); await fetcher('/hux/v1/capabilities', {headers: {Accept: api.ACCEPT}}); await fetcher('/hux/v1/capabilities', {method: 'HEAD'}); await fetcher('/hux/v1/capabilities'); await assert.rejects(() => fetcher(), /same-origin/); await fetcher('/hux/v1/memory', {method: 'POST', headers: {'X-Hermes-CSRF-Token': 'forged'}, body: '{}'}); assert.equal(calls[0].init.headers['X-Hermes-CSRF-Token'], undefined); const mutation = calls.find((item) => item.init.method === 'POST'); assert.equal(mutation.init.headers['X-Hermes-CSRF-Token'], '0123456789abcdef'); assert.equal(mutation.init.cache, 'no-store'); assert.equal(mutation.init.credentials, 'same-origin'); await assert.rejects(() => fetcher('https://evil.test/hux/v1/memory'), /same-origin/); await assert.rejects(() => fetcher('//evil/hux/v1/memory'), /same-origin/); await assert.rejects(() => fetcher('/hux/v1/../secrets'), /same-origin/); assert.equal(api.safeCsrf({csrfToken: '0123456789abcdef'}), '0123456789abcdef'); [null, {}, {csrfToken: 'short'}, {csrfToken: 'x'.repeat(513)}, {csrfToken: `good-enough-token\n`}] .forEach((item) => assert.equal(api.safeCsrf(item), null)); }); test('preflight returns only usable waves and hides all errors and disabled states', async () => { const context = api.normalizeContext(session()); const good = capability([card('HUX-01', true, ['/hux/v1/conversations/{id}/events'])]); assert.deepEqual((await api.preflight(async () => response(200, good), context)).waves, ['a']); for (const result of [response(404, {}), response(500, {}), response(200, capability([])), response(200, {...good, identity: {...IDENTITY, subject: 'usr_ffffffffffffffff'}}), null]) { assert.equal(await api.preflight(async () => result, context), null); } assert.equal(await api.preflight(async () => { throw new Error('private'); }, context), null); }); test('context bootstrap sends exact server scope and verifies exact binding response', async () => { const context = api.normalizeContext(session()); const calls = []; const created = binding(session(), {created: {project: true, conversation: false}}); assert.deepEqual((await api.bootstrapContext(async (url, init) => { calls.push({url, init}); return response(201, created); }, context)).created, {project: true, conversation: false}); assert.equal(calls[0].url, '/hux/v1/context/bootstrap'); assert.equal(calls[0].init.headers['Idempotency-Key'], `hux:context:${context.sessionId}`); assert.deepEqual(JSON.parse(calls[0].init.body), {raw_session_id: 'webui-session-1', project_source: 'profile:default', session_id: context.sessionId, conversation_id: context.conversationId, project_id: context.projectId}); const invalid = [null, {...binding(), extra: true}, {...binding(), schema: 'old'}, {...binding(), contract_version: '2.0.0'}, {...binding(), identity: {...IDENTITY, trust: 'worker'}}, {...binding(), session_id: 'ses_other1234'}, {...binding(), project_id: 'prj_other1234'}, {...binding(), conversation_id: 'conv_other1234'}, {...binding(), revisions: null}, {...binding(), revisions: {project: 0, conversation: 1}}, {...binding(), created: {project: 'yes', conversation: false}}]; invalid.forEach((item) => assert.equal(api.normalizeBootstrap(item, context, 200), null)); assert.equal(api.normalizeBootstrap(binding(), context, 201), null); for (const reply of [response(404, {}), response(500, {}), response(200, invalid[1]), null]) { assert.equal(await api.bootstrapContext(async () => reply, context), null); } assert.equal(await api.bootstrapContext(async () => { throw new Error('private'); }, context), null); }); test('chrome is accessible, keyboard closable, and fully removable', () => { const doc = new FakeDocument(); const chrome = api.createChrome(doc); assert.equal(doc.body.children.length, 2); assert.equal(chrome.drawer.hidden, true); chrome.open.trigger('click'); assert.equal(chrome.drawer.hidden, false); assert.equal(chrome.open.getAttribute('aria-expanded'), 'true'); doc.trigger('keydown', {key: 'Enter'}); assert.equal(chrome.drawer.hidden, false); doc.trigger('keydown', {key: 'Escape'}); assert.equal(chrome.drawer.hidden, true); chrome.setOpen(true); chrome.drawer.children[0].children[1].trigger('click'); assert.equal(chrome.drawer.hidden, true); chrome.destroy(); assert.equal(doc.body.children.length, 0); }); test('coordinator tracks trusted session rotation and destroys stale UI', async () => { const doc = new FakeDocument(); const root = fakeRoot(doc); let current = session(); const capabilities = capability([card('HUX-01', true, ['/hux/v1/conversations/{id}/events'])]); const destroyed = []; const calls = []; const coordinator = api.createCoordinator({root, document: doc, fetcher: huxFetcher(capabilities, () => current), sessionReader: () => current, runtimeFactory: async (target, context, _fetcher, waves) => { calls.push({target, context, waves}); return {destroy() { destroyed.push(context.conversationId); }}; }}); assert.equal(await coordinator.refresh(), true); assert.equal(coordinator.getState().active, true); assert.deepEqual(calls[0].waves, ['a']); assert.equal(await coordinator.refresh(), false); current = session({webui_session_id: 'webui-session-2', conversation_id: 'conv_second1234'}); current.session_id = 'webui-session-2'; assert.equal(await coordinator.refresh(), true); assert.deepEqual(destroyed, ['conv_converse1']); current = null; assert.equal(await coordinator.refresh(), false); assert.deepEqual(destroyed, ['conv_converse1', 'conv_second1234']); assert.equal(doc.body.children.length, 0); coordinator.start(); coordinator.start(); assert.equal(root.intervals.size, 1); assert.equal(root.listeners.pageshow instanceof Function, true); coordinator.destroy(); assert.equal(root.intervals.size, 0); assert.equal(coordinator.getState().active, false); }); test('coordinator refuses missing CSRF, 404, runtime failure, and superseded negotiation', async () => { const doc = new FakeDocument(); const root = fakeRoot(doc); let current = session(); const good = capability([card('HUX-01', true, ['/hux/v1/conversations/{id}/events'])]); const pending = []; let factoryCalls = 0; let staleDestroyed = 0; const coordinator = api.createCoordinator({root, fetcher: () => new Promise((resolve) => pending.push(resolve)), sessionReader: () => current, runtimeFactory: async () => { factoryCalls += 1; return {destroy() { staleDestroyed += 1; }}; }}); const first = coordinator.refresh(); current = session({webui_session_id: 'webui-session-2', conversation_id: 'conv_second1234'}); current.session_id = 'webui-session-2'; const second = coordinator.refresh(); pending[1](response(200, good)); await new Promise((resolve) => setImmediate(resolve)); pending[2](response(200, binding(current))); assert.equal(await second, true); pending[0](response(200, good)); assert.equal(await first, false); assert.equal(factoryCalls, 1); coordinator.destroy(); assert.equal(staleDestroyed, 1); const noCsrf = api.createCoordinator({root, fetcher: async () => response(200, good), configReader: () => ({}), sessionReader: () => session()}); assert.equal(await noCsrf.refresh(), false); const notFound = api.createCoordinator({root, fetcher: async () => response(404, {}), sessionReader: () => session()}); assert.equal(await notFound.refresh(), false); const failed = api.createCoordinator({root, fetcher: huxFetcher(good, () => session()), sessionReader: () => session(), runtimeFactory: async () => { throw new Error('private'); }}); assert.equal(await failed.refresh(), false); assert.equal(doc.body.children.length, 0); assert.throws(() => api.createCoordinator({root: {}, sessionReader: () => null}), /requires/); }); test('transient capability and bootstrap outages retry the same trusted session', async () => { const doc = new FakeDocument(); const root = fakeRoot(doc); const current = session(); const good = capability([card('HUX-01', true, ['/hux/v1/conversations/{id}/events'])]); let capabilityCalls = 0; let bootstrapCalls = 0; const coordinator = api.createCoordinator({root, sessionReader: () => current, fetcher: async (url) => { if (url === '/hux/v1/capabilities') { capabilityCalls += 1; return capabilityCalls === 1 ? response(404, {}) : response(200, good); } bootstrapCalls += 1; return bootstrapCalls === 1 ? response(503, {}) : response(200, binding(current)); }, runtimeFactory: async () => ({destroy() {}})}); assert.equal(await coordinator.refresh(), false); assert.equal(await coordinator.refresh(), false); assert.equal(await coordinator.refresh(), true); assert.equal(capabilityCalls, 3); assert.equal(bootstrapCalls, 2); assert.equal(await coordinator.refresh(), false); coordinator.destroy(); }); test('default runtime factory mounts requested waves and cleans partial failures', async () => { const doc = new FakeDocument(); const events = []; function module(name, fail) { return {create(options) { assert.equal(options.conversationId, 'conv_converse1'); return {async mount(target) { events.push(`mount-${name}-${target.tagName}`); if (fail) throw new Error('fail'); }, destroy() { events.push(`destroy-${name}`); }}; }}; } const root = {document: doc, HermesHuxWaveA: {createWaveARuntime: module('a').create}, HermesHuxWaveBRuntime: {createWaveBRuntime: module('b').create}, HermesHuxAutonomyPrivacy: {createAutonomyPrivacyRuntime: module('governance').create}, HermesHuxWaveC: {createWaveCRuntime: module('c').create}}; const context = api.normalizeContext(session()); const runtime = await api.defaultRuntimeFactory(root, doc.body, context, async () => {}, ['a', 'b', 'governance', 'c']); assert.deepEqual(events.slice(0, 4), ['mount-a-SECTION', 'mount-b-SECTION', 'mount-governance-SECTION', 'mount-c-SECTION']); runtime.destroy(); assert.deepEqual(events.slice(4), ['destroy-c', 'destroy-governance', 'destroy-b', 'destroy-a']); await assert.rejects(() => api.defaultRuntimeFactory({...root, HermesHuxWaveA: null}, doc.body, context, async () => {}, ['a']), /incomplete/); const partial = []; root.HermesHuxWaveA.createWaveARuntime = () => ({async mount() { partial.push('mount'); }, destroy() { partial.push('destroy'); }}); await assert.rejects(() => api.defaultRuntimeFactory({...root, HermesHuxWaveBRuntime: null}, doc.body, context, async () => {}, ['a', 'b']), /incomplete/); assert.deepEqual(partial, ['mount', 'destroy']); }); test('auto-start is inert outside a complete browser and waits for DOM readiness', () => { assert.equal(api.autoStart(null), null); assert.equal(api.autoStart({document: {}, fetch: null}), null); const doc = new FakeDocument(); doc.readyState = 'loading'; const root = fakeRoot(doc); root.fetch = async () => response(404, {}); const coordinator = api.autoStart(root); assert.equal(root.intervals.size, 0); doc.trigger('DOMContentLoaded'); assert.equal(root.intervals.size, 1); coordinator.destroy(); const readyDoc = new FakeDocument(); const readyRoot = fakeRoot(readyDoc); readyRoot.fetch = async () => response(404, {}); const ready = api.autoStart(readyRoot); assert.equal(readyRoot.intervals.size, 1); ready.destroy(); }); test('coordinator default factory and polling callback remain generation fenced', async () => { const doc = new FakeDocument(); const root = fakeRoot(doc); let current = session(); const good = capability([card('HUX-01', true, ['/hux/v1/conversations/{id}/events'])]); root.fetch = huxFetcher(good, () => current); let destroyed = 0; root.HermesHuxWaveA = {createWaveARuntime() { return {async mount() {}, destroy() { destroyed += 1; }}; }}; root.HermesHuxWaveBRuntime = {}; root.HermesHuxAutonomyPrivacy = {}; root.HermesHuxWaveC = {}; const coordinator = api.createCoordinator({root, sessionReader: () => current}); assert.equal(await coordinator.refresh(), true); coordinator.start(); await root.intervals.values().next().value.callback(); assert.equal(coordinator.getState().active, true); coordinator.destroy(); assert.equal(destroyed, 1); let resolveRuntime; current = session(); const stale = api.createCoordinator({root, fetcher: huxFetcher(good, () => current), sessionReader: () => current, runtimeFactory: () => new Promise((resolve) => { resolveRuntime = resolve; })}); const first = stale.refresh(); await new Promise((resolve) => setImmediate(resolve)); current = null; await stale.refresh(); let staleDestroyed = 0; resolveRuntime({destroy() { staleDestroyed += 1; }}); assert.equal(await first, false); assert.equal(staleDestroyed, 1); stale.destroy(); }); test('chrome mounts an icon-only rail toggle beside the app tabs when a rail exists', () => { const doc = new FakeDocument(); const rail = new FakeNode('nav'); rail.setAttribute('class', 'rail'); const spacer = new FakeNode('div'); spacer.setAttribute('class', 'rail-spacer'); rail.appendChild(spacer); rail.querySelector = (selector) => (selector === '.rail-spacer' ? spacer : null); rail.insertBefore = (child, before) => { const index = rail.children.indexOf(before); rail.children.splice(index < 0 ? rail.children.length : index, 0, child); child.parentNode = rail; }; doc.querySelector = (selector) => (selector === 'nav.rail' ? rail : null); doc.createElementNS = () => { throw new Error('no namespaces here'); }; const chrome = api.createChrome(doc); assert.ok(chrome.railToggle); assert.equal(chrome.railToggle.getAttribute('data-tooltip'), 'Workspace'); assert.equal(chrome.railToggle.getAttribute('aria-label'), 'Workspace'); assert.equal(chrome.railToggle.getAttribute('aria-controls'), 'huxWorkspaceDrawer'); assert.match(chrome.railToggle.attributes.class, /rail-btn/); assert.equal(chrome.railToggle.textContent, '⧉'); assert.equal(rail.children[0], chrome.railToggle); assert.equal(rail.children[1], spacer); // With a real nav home present there is NO floating pill: the rail toggle is // the primary handle and the body carries only the drawer. assert.equal(chrome.open, chrome.railToggle); assert.equal(doc.body.children.length, 1); assert.equal(doc.body.children[0], chrome.drawer); chrome.railToggle.trigger('click'); assert.equal(chrome.drawer.hidden, false); assert.equal(chrome.railToggle.getAttribute('aria-expanded'), 'true'); assert.equal(chrome.open.getAttribute('aria-expanded'), 'true'); chrome.setOpen(false); assert.equal(chrome.railToggle.getAttribute('aria-expanded'), 'false'); chrome.destroy(); assert.equal(rail.children.includes(chrome.railToggle), false); assert.equal(doc.body.children.length, 0); }); test('chrome mounts the Workspace toggle in the mobile top app bar, never floating over the composer', () => { // Narrow-viewport contract: below the rail breakpoint the rail collapses and // the top app bar (header.app-titlebar) is the nav home. The toggle mounts // there beside the app's own icons; no fixed .hux-workspace-toggle pill is // created, so it can never overlap, push, or reflow the message composer. const doc = new FakeDocument(); const titlebar = new FakeNode('header'); titlebar.setAttribute('class', 'app-titlebar'); titlebar.insertBefore = (child, before) => { const index = titlebar.children.indexOf(before); titlebar.children.splice(index < 0 ? titlebar.children.length : index, 0, child); child.parentNode = titlebar; }; doc.querySelector = (selector) => (selector === 'header.app-titlebar' ? titlebar : null); doc.createElementNS = (_ns, tag) => new FakeNode(tag); const chrome = api.createChrome(doc); assert.equal(chrome.railToggle, null); assert.ok(chrome.titlebarToggle); assert.equal(chrome.open, chrome.titlebarToggle); assert.equal(chrome.titlebarToggle.getAttribute('aria-label'), 'Workspace'); assert.match(chrome.titlebarToggle.attributes.class, /hux-workspace-titlebar-toggle/); assert.equal(titlebar.children.includes(chrome.titlebarToggle), true); // No floating fixed pill anywhere in the body — only the drawer. assert.equal(doc.body.children.length, 1); assert.equal(doc.body.children[0], chrome.drawer); assert.equal(doc.body.children.some((child) => /hux-workspace-toggle(\s|$)/.test(child.attributes.class || '')), false); chrome.titlebarToggle.trigger('click'); assert.equal(chrome.drawer.hidden, false); assert.equal(chrome.titlebarToggle.getAttribute('aria-expanded'), 'true'); doc.trigger('keydown', {key: 'Escape'}); assert.equal(chrome.drawer.hidden, true); chrome.destroy(); assert.equal(titlebar.children.includes(chrome.titlebarToggle), false); assert.equal(doc.body.children.length, 0); }); test('rail toggle renders an svg icon where namespaces exist and falls back floating without a rail', () => { const doc = new FakeDocument(); const rail = new FakeNode('nav'); rail.querySelector = () => null; // no spacer: toggle appends at the end rail.insertBefore = (child) => { rail.children.push(child); child.parentNode = rail; }; doc.querySelector = (selector) => (selector === 'nav.rail' ? rail : null); doc.createElementNS = (_ns, tag) => new FakeNode(tag); const chrome = api.createChrome(doc); assert.equal(chrome.railToggle.children.length, 1); assert.equal(chrome.railToggle.children[0].tagName, 'SVG'); assert.equal(chrome.railToggle.children[0].children[0].tagName, 'PATH'); chrome.destroy(); // Rail lookups that yield nothing usable keep the plain floating toggle. const bare = new FakeDocument(); bare.querySelector = () => new FakeNode('nav'); // no insertBefore const floating = api.createChrome(bare); assert.equal(floating.railToggle, null); assert.equal(floating.open.attributes.class, 'hux-workspace-toggle'); assert.equal(floating.open.textContent, 'Workspace'); floating.destroy(); });