/** * @fileoverview Real-Chromium coverage for the L-shaped header strip (`_syncTabStripCorner()`, app.js). * * What DOM emulation cannot answer: whether the float the strip measures really keeps every tab out from under * the toolbar, whether the rows below it really run the header's full width, and when the strip falls back to * the plain wrap. The real `
` from index.html, the shipping styles.css, constants.js and app.js are * loaded into a page; `updateTabOverflowMode()` runs for real on a bare app instance. * * Port: none (page.route on a fake origin, no server). */ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { afterAll, beforeAll, describe, expect, it } from 'vitest'; import { chromium, type Browser, type Page } from 'playwright'; const dir = resolve(import.meta.dirname, '../src/web/public'); const read = (name: string) => readFileSync(resolve(dir, name), 'utf8'); const styles = read('styles.css'); const header = /
[\s\S]*?<\/header>/.exec(read('index.html'))![0]; const ORIGIN = 'http://app.test/'; type Rect = { left: number; right: number; top: number; bottom: number }; describe('L-shaped header tab strip (real Chromium)', () => { let browser: Browser; let p: Page; beforeAll(async () => { browser = await chromium.launch(); }); afterAll(async () => { await browser.close(); }); /** Open the real header with `tabs` classic tabs; `twoRows` is the manual two-rows setting. */ async function open(options: { tabs: number; width?: number; height?: number; twoRows?: boolean }) { p = await browser.newPage({ viewport: { width: options.width ?? 1400, height: options.height ?? 700 }, deviceScaleFactor: 1, }); await p.route(ORIGIN, (route) => route.fulfill({ contentType: 'text/html', body: '' + `${header}`, }) ); await p.goto(ORIGIN); await p.addScriptTag({ content: 'var MobileDetection = { isTouchDevice: () => false, getDeviceType: () => "desktop" }, KeyboardHandler = {}, ' + 'SwipeHandler = {}, VoiceInput = {}, DeepgramProvider = {}, NotificationManager = function(){};\n' + read('constants.js') + '\n' + read('app.js') + '\nwindow.CodemanApp = CodemanApp;', }); await p.evaluate( ({ tabs, twoRows }) => { const w = window as any; const app = Object.create(w.CodemanApp.prototype); app.$ = (id: string) => document.getElementById(id); app.sessions = new Map(); app.loadAppSettingsFromStorage = () => ({ tabTwoRows: twoRows }); app.getDefaultSettings = () => ({}); app.isSessionSidebarActive = () => false; app.updateConnectionLines = () => undefined; const strip = document.getElementById('sessionTabs')!; if (twoRows) strip.classList.add('tabs-two-rows'); for (let i = 1; i <= tabs; i++) { const id = `s${i}`; app.sessions.set(id, { id, name: `session-${i}` }); strip.insertAdjacentHTML( 'beforeend', `` ); } w.__app = app; }, { tabs: options.tabs, twoRows: !!options.twoRows } ); await relayout(); } const settled = () => p.evaluate(() => new Promise((r) => requestAnimationFrame(() => requestAnimationFrame(r)))); const relayout = async () => { await p.evaluate(() => (window as any).__app.updateTabOverflowMode()); await settled(); }; const cornered = () => p.evaluate(() => document.getElementById('sessionTabs')!.classList.contains('tabs-corner')); const toolbar = () => p.evaluate(() => document.getElementById('headerRight')!.getBoundingClientRect().toJSON()) as Promise; /** The visible part of each tab: the strip clips what it scrolls or overflows. */ const visibleTabs = () => p.evaluate(() => { const strip = document.getElementById('sessionTabs')!.getBoundingClientRect(); return [...document.querySelectorAll('#sessionTabs > .session-tab')] .map((el) => { const r = el.getBoundingClientRect(); return { left: Math.max(r.left, strip.left), right: Math.min(r.right, strip.right), top: Math.max(r.top, strip.top), bottom: Math.min(r.bottom, strip.bottom), }; }) .filter((r) => r.right > r.left && r.bottom > r.top); }) as Promise; const overlapping = async () => { const b = await toolbar(); return (await visibleTabs()).filter( (r) => r.left < b.right && r.right > b.left && r.top < b.bottom && r.bottom > b.top ); }; it('puts a wrapped classic desktop strip around the toolbar, with no tab under it', async () => { await open({ tabs: 40 }); expect(await p.evaluate(() => document.getElementById('sessionTabs')!.classList.contains('tabs-auto-wrap'))).toBe( true ); expect(await cornered()).toBe(true); expect(await overlapping()).toEqual([]); await p.close(); }); it('runs a row below the toolbar past its left edge', async () => { await open({ tabs: 40 }); const b = await toolbar(); const below = (await visibleTabs()).filter((r) => r.top >= b.bottom); expect(below.length).toBeGreaterThan(0); expect(Math.max(...below.map((r) => r.right))).toBeGreaterThan(b.left); await p.close(); }); it('keeps the plain wrap when the first tab is too wide for the room beside the toolbar', async () => { await open({ tabs: 40 }); await p.evaluate(() => { const head = document.querySelector('.header')!.getBoundingClientRect(); const right = document.getElementById('headerRight')!.getBoundingClientRect(); const first = document.querySelector('#sessionTabs > .session-tab')!; first.style.minWidth = first.style.width = `${Math.round(head.width - right.width / 2)}px`; }); await relayout(); expect(await cornered()).toBe(false); expect(await overlapping()).toEqual([]); await p.close(); }); it('leaves a strip that fits on one row unwrapped and plain', async () => { await open({ tabs: 3 }); expect(await p.evaluate(() => document.getElementById('sessionTabs')!.classList.contains('tabs-auto-wrap'))).toBe( false ); expect(await cornered()).toBe(false); await p.close(); }); for (const arrangement of ['tabs-triage', 'tabs-clusters', 'tabs-ledger']) { it(`never corners the ${arrangement} arrangement`, async () => { await open({ tabs: 40 }); await p.evaluate((cls) => document.getElementById('sessionTabs')!.classList.add(cls), arrangement); await relayout(); expect(await cornered()).toBe(false); await p.close(); }); } it('never corners a strip outside #sessionTabsHost', async () => { await open({ tabs: 40 }); await p.evaluate(() => { const elsewhere = document.createElement('div'); elsewhere.style.width = '600px'; document.body.appendChild(elsewhere); elsewhere.appendChild(document.getElementById('sessionTabs')!); document.getElementById('sessionTabs')!.classList.add('tabs-two-rows'); }); await relayout(); expect(await cornered()).toBe(false); await p.close(); }); it('keeps the plain wrap for a wrapped strip past its max-height, so scrolling never puts tabs under the toolbar', async () => { await open({ tabs: 50, width: 1100, height: 500 }); expect(await p.evaluate(() => document.getElementById('sessionTabs')!.classList.contains('tabs-auto-wrap'))).toBe( true ); expect(await cornered()).toBe(false); await p.evaluate(() => (document.getElementById('sessionTabs')!.scrollTop = 60)); await settled(); expect(await overlapping()).toEqual([]); await p.close(); }); it('keeps the plain wrap for a scrolled two-rows strip across a re-render', async () => { await open({ tabs: 50, width: 1100, height: 500, twoRows: true }); await p.evaluate(() => (document.getElementById('sessionTabs')!.scrollTop = 60)); await relayout(); expect(await cornered()).toBe(false); expect(await overlapping()).toEqual([]); await p.evaluate(() => (document.getElementById('sessionTabs')!.scrollTop = 0)); await settled(); expect(await overlapping()).toEqual([]); await p.close(); }); it('re-sizes the corner when the toolbar is narrowed with --header-right-width', async () => { await open({ tabs: 40 }); const width = () => p.evaluate(() => parseFloat(document.getElementById('sessionTabs')!.style.getPropertyValue('--tab-corner-width')) ); const before = await width(); await p.evaluate(() => { const root = document.documentElement; root.setAttribute('data-header-right-width', 'set'); root.style.setProperty('--header-right-width', '300px'); }); await relayout(); expect(await cornered()).toBe(true); const after = await width(); expect(after).toBeLessThan(before); expect(Math.abs(after - ((await toolbar()).width + 8))).toBeLessThan(16); expect(await overlapping()).toEqual([]); await p.close(); }); });