/** * @fileoverview The Tiles button's hover card (owner feedback 1: "give me the * hover info to right click over the tile button to adjust it"). * * - Installed once with the button's visibility, never in a solo window: a * hidden `role=tooltip` card in body, the button's `aria-describedby`, and * the button's native title removed (two tooltips never stack); the * aria-label stays. * - Shows 300 ms after a pointer that hovers rests on the button, or after a * keyboard focus (`:focus-visible`), under the button. Never for a touch * pointer, a device that cannot hover, a hidden button, or while the count * menu is open. * - Content: "Tiles · N" (the remembered count, live), what a click does * (open or close the grid), "Right-click: choose 2, 4 or 6 tiles", what * opens when the count does not fit the window, and Shift+F10 when shown * from the keyboard; hidden, it holds all of it (the button's description). * - Hides on pointer leave, blur, a click, a right-click or a press on the * button (and stays hidden while the pointer rests there), Escape, a scroll * and a resize; the count menu never opens beside it, and the Escape out of * the menu brings no card back. * - Only `.tile-hint` styles: the panel of the count menu, a fade on opacity * and transform only, nothing under reduced motion or without hover. * * Real code via the shared vm harness (test/mocks/tile-grid-vm.ts). Port: N/A. */ import { readFileSync } from 'node:fs'; import { resolve } from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { FakeEl, body, bySelector, localStore, makeGridApp, resetGridHarness, windowStub, type GridApp, } from './mocks/tile-grid-vm.js'; const IDS = ['s-a', 's-b', 's-c', 's-d', 's-e', 's-f']; const PUBLIC = resolve(import.meta.dirname, '../src/web/public'); const css = readFileSync(resolve(PUBLIC, 'styles.css'), 'utf8'); const html = readFileSync(resolve(PUBLIC, 'index.html'), 'utf8'); let btn: FakeEl; let wrapRect = { width: 2400, height: 1200 }; const card = () => body.children.find((c) => c.id === 'tileGridHint') ?? null; const lines = () => card()!.children.filter((c) => c.className.includes('tile-hint-line')); const textOf = (row: FakeEl) => row.children.at(-1)!.textContent; const visibleText = () => [ card()!.children[0].textContent, ...lines() .filter((l) => !l.hidden) .map(textOf), ]; const pointer = (type: string, pointerType = 'mouse') => btn.dispatch(type, { pointerType }); beforeEach(() => { resetGridHarness(); vi.useFakeTimers(); wrapRect = { width: 2400, height: 1200 }; const wrap = new FakeEl(); wrap.getBoundingClientRect = () => ({ ...wrapRect, top: 0, left: 0, right: wrapRect.width, bottom: wrapRect.height }); bySelector.set('.terminal-wrap', wrap); btn = new FakeEl(); btn.className = 'btn-icon-header btn-tile-grid btn-tile-grid--hidden'; btn.title = 'Tiles: show several sessions side by side (right-click for how many)'; btn.getBoundingClientRect = () => ({ width: 26, height: 26, top: 8, left: 2000, right: 2026, bottom: 34 }); bySelector.set('.btn-tile-grid', btn); }); afterEach(() => { vi.useRealTimers(); }); function hintApp(): GridApp { const app = makeGridApp(IDS); app.selectSession = vi.fn((id: string) => app._selectTiledSession(id, {})); app._applyTileGridButtonVisibility(true); return app; } describe('installed with the button', () => { it('a hidden tooltip card in body, the button described by it, its native title gone, its label kept', () => { const app = hintApp(); expect(card()).not.toBeNull(); expect(card()!.attrs.role).toBe('tooltip'); expect(card()!.hidden).toBe(true); expect(btn.getAttribute('aria-describedby')).toBe('tileGridHint'); expect(btn.title).toBe(''); expect(btn.getAttribute('title')).toBeNull(); app._updateTileGridButtonState(); expect(btn.getAttribute('aria-label')).toBe('Tiles: show several sessions side by side (right-click for how many)'); expect(btn.title).toBe(''); // Once: a second call adds no second card. app._applyTileGridButtonVisibility(true); expect(body.children.filter((c) => c.id === 'tileGridHint')).toHaveLength(1); // The markup: no title on the button, described by the card. const tag = html.match(/