/** @fileoverview COD-358 resizable vertical session rail policy and wiring. */ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import vm from 'node:vm'; import { afterEach, describe, expect, it, vi } from 'vitest'; const publicDir = resolve(import.meta.dirname, '../src/web/public'); const readPublic = (name: string) => readFileSync(resolve(publicDir, name), 'utf8'); const indexHtml = readPublic('index.html'); const stylesCss = readPublic('styles.css'); const settingsJs = readPublic('settings-ui.js'); const appJs = readPublic('app.js'); const terminalUiJs = readPublic('terminal-ui.js'); const buildJs = readFileSync(resolve(import.meta.dirname, '../scripts/build.mjs'), 'utf8'); type RailPolicy = { DEFAULT_WIDTH: number; MIN_WIDTH: number; MAX_WIDTH: number; resolveWidth: (input?: Record) => number; resolveKeyboardWidth: (input: Record) => number | null; }; function loadRailPolicy(): RailPolicy { const context = vm.createContext({ window: {}, globalThis: {} }); vm.runInContext(readPublic('constants.js'), context, { filename: 'constants.js' }); return (context.window as { CodemanTabRail: RailPolicy }).CodemanTabRail; } afterEach(() => vi.useRealTimers()); describe('tab rail width policy', () => { it('uses fixed storage bounds and separate viewport/terminal clamps', () => { const policy = loadRailPolicy(); expect(policy.DEFAULT_WIDTH).toBe(256); expect(policy.MIN_WIDTH).toBe(208); expect(policy.MAX_WIDTH).toBe(360); expect(policy.resolveWidth()).toBe(256); expect(policy.resolveWidth({ width: 120 })).toBe(208); expect(policy.resolveWidth({ width: 900 })).toBe(360); expect(policy.resolveWidth({ width: 360, viewportWidth: 700 })).toBe(280); expect(policy.resolveWidth({ width: 360 })).toBe(360); expect(policy.resolveWidth({ width: 360, mainWidth: 700, minTerminalWidth: 400 })).toBe(300); }); it('supports accessible keyboard steps and reset keys', () => { const policy = loadRailPolicy(); const base = { currentWidth: 256, viewportWidth: 1600, mainWidth: 1200, minTerminalWidth: 400 }; expect(policy.resolveKeyboardWidth({ ...base, key: 'ArrowLeft' })).toBe(248); expect(policy.resolveKeyboardWidth({ ...base, key: 'ArrowRight' })).toBe(264); expect(policy.resolveKeyboardWidth({ ...base, key: 'ArrowRight', shiftKey: true })).toBe(288); expect(policy.resolveKeyboardWidth({ ...base, key: 'Home' })).toBe(208); expect(policy.resolveKeyboardWidth({ ...base, key: 'End' })).toBe(360); expect(policy.resolveKeyboardWidth({ ...base, key: 'Enter' })).toBe(256); // Enter resets to the CALLER's effective default: a rich rail passes 320 // (its unsized rendering width), so the reset cannot land it below the // 288px tight threshold the way a hardcoded 256 did. expect(policy.resolveKeyboardWidth({ ...base, key: 'Enter', defaultWidth: 320 })).toBe(320); expect(policy.resolveKeyboardWidth({ ...base, key: 'Escape' })).toBeNull(); }); }); describe('tab rail resize wiring', () => { it('ships an accessible handle and width presets', () => { expect(indexHtml).toContain('id="tabRailResizeHandle"'); expect(indexHtml).toContain('role="separator"'); expect(indexHtml).toContain('aria-orientation="vertical"'); expect(indexHtml).toContain('aria-valuemin="208"'); expect(indexHtml).toContain('aria-valuemax="360"'); expect(indexHtml).toContain('id="appSettingsTabRailWidth"'); expect(indexHtml).toContain('