80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
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('<script>alert("x")</script>')).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('<b>x</b>', 'board')).not.toContain('<b>x</b>');
|
|
});
|
|
it('appHeaderOnlyCss is a prefix of appHeaderCss', () => {
|
|
expect(appHeaderCss().startsWith(appHeaderOnlyCss())).toBe(true);
|
|
expect(appHeaderOnlyCss()).toContain('.app-header');
|
|
expect(appHeaderCss()).toContain('.modal-backdrop');
|
|
});
|
|
});
|