import { describe, it, expect } from 'vitest'; import { agentAvatar, designTokensCss, escapeHtml, liveTimerJs, statusPill, appHeader, appHeaderCss, appHeaderOnlyCss } from '../src/server/ui-shared.js'; describe('ui-shared helpers', () => { it('designTokensCss returns the dark palette variables', () => { const css = designTokensCss(); expect(css).toContain('--bg: #0F172A'); expect(css).toContain('--status-in_progress: #58A6FF'); expect(css).toContain('prefers-reduced-motion'); }); it('agentAvatar renders initials without emojis', () => { const html = agentAvatar('kimi'); expect(html).toContain('K'); expect(html).toContain('#7C3AED'); expect(html).not.toContain('emoji'); }); it('agentAvatar renders deterministic fallback for unknown agents', () => { const html = agentAvatar('robo-agent-42'); expect(html).toContain('R'); expect(html).toContain('agent-avatar'); }); it('agentAvatar adds architect ring when requested', () => { const html = agentAvatar('claude', { architectRing: true }); expect(html).toContain('box-shadow'); }); it('statusPill renders each known status', () => { for (const status of ['open', 'in_progress', 'review', 'done', 'cancelled'] as const) { const html = statusPill(status); expect(html).toContain(`data-status="${status}"`); expect(html).toContain('status-pill'); } }); it('escapeHtml escapes dangerous characters', () => { expect(escapeHtml('')).toBe( '<script>alert("x")</script>', ); }); it('liveTimerJs contains the setInterval updater', () => { const js = liveTimerJs(); expect(js).toContain('setInterval'); expect(js).toContain('data-live-timer'); expect(js).toContain('claimed'); }); }); describe('appHeader (v2 shared header)', () => { it('renders logo, project cell, nav links and status mounts', () => { const h = appHeader('demo', 'team'); expect(h).toContain('/logo.svg'); expect(h).toContain('id="projectName"'); expect(h).toContain('b2-proj'); expect(h).toContain('id="newTaskBtn"'); expect(h).toContain('id="sseStatus"'); for (const path of ['/board', '/team', '/activity', '/messages', '/decisions']) { expect(h).toContain(`href="${path}"`); } }); it('marks the current page active with aria-current', () => { const team = appHeader('demo', 'team'); expect(team).toContain('active" href="/team" aria-current="page"'); const board = appHeader('demo', 'board'); expect(board).toContain('active" href="/board" aria-current="page"'); expect(board).not.toContain('active" href="/team"'); }); it('escapes the project name', () => { expect(appHeader('x', 'board')).not.toContain('x'); }); it('appHeaderOnlyCss is a prefix of appHeaderCss', () => { expect(appHeaderCss().startsWith(appHeaderOnlyCss())).toBe(true); expect(appHeaderOnlyCss()).toContain('.app-header'); expect(appHeaderCss()).toContain('.modal-backdrop'); }); it('makes the mobile nav horizontally scrollable with 44px tap targets', () => { const css = appHeaderOnlyCss(); expect(css).toMatch(/@media \(max-width: 680px\)[\s\S]*\.app-header \.b2-nav \{[^}]*overflow-x: auto/); expect(css).toMatch(/@media \(max-width: 680px\)[\s\S]*\.app-header \.b2-nav a \{[^}]*min-height: 44px/); expect(css).toContain('mask-image: linear-gradient'); }); });