// @vitest-environment jsdom /** * test/telemetry-gauge.test.ts — renderTelemetryGauge + renderStatusBadge + statusText('stuck') * * Covers T-preview-grid W1: telemetry gauge, status badge, and statusText extension. * Security: SEC-H5 (textContent only), SEC-L5 (PR URL https scheme guard). */ import { describe, it, expect, vi, beforeEach } from 'vitest' import type { StatusTelemetry, ClaudeStatus } from '../src/types.js' // Stub xterm so jsdom does not fail on canvas operations (same as preview-grid.test.ts) vi.mock('@xterm/xterm', () => ({ Terminal: class FakeTerm { cols = 80 rows = 24 open = vi.fn() reset = vi.fn() resize = vi.fn() dispose = vi.fn() write = vi.fn((_d: string, cb?: () => void) => cb?.()) }, })) const { renderTelemetryGauge, renderStatusBadge, statusText } = await import('../public/preview-grid.js') /** Helper: build a StatusTelemetry with only `at` (everything else optional). */ function makeTelemetry(over: Partial = {}): StatusTelemetry { return { at: Date.now(), ...over } } function makeContainer(): HTMLDivElement { return document.createElement('div') } beforeEach(() => { vi.restoreAllMocks() }) // ─────────────────────── statusText — stuck extension ──────────────────────── describe('statusText — stuck extension', () => { it('returns text containing ⚠ for stuck status', () => { const text = statusText('stuck' as ClaudeStatus) expect(text).toContain('⚠') expect(text).toContain('stuck') }) it('still maps existing statuses correctly (no regression)', () => { expect(statusText('working')).toContain('working') expect(statusText('waiting')).toContain('waiting') expect(statusText('idle')).toContain('idle') expect(statusText('unknown')).toBe('·') }) }) // ─────────────────────── renderTelemetryGauge ──────────────────────────────── describe('renderTelemetryGauge', () => { it('renders nothing for null telemetry', () => { const c = makeContainer() renderTelemetryGauge(c, null, 30_000) expect(c.children.length).toBe(0) }) it('is tolerant when telemetry has only the required at field (no optional fields)', () => { const c = makeContainer() expect(() => renderTelemetryGauge(c, makeTelemetry(), 30_000)).not.toThrow() // No optional fields → nothing rendered beyond the stale logic expect(c.children.length).toBe(0) }) it('clears existing children on each call', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry({ model: 'first' }), 30_000) renderTelemetryGauge(c, makeTelemetry({ model: 'second' }), 30_000) const chips = c.querySelectorAll('.tg-model') expect(chips.length).toBe(1) expect(chips[0]?.textContent).toBe('second') }) // ── context bar ── it('renders a context bar when contextUsedPct is present', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry({ contextUsedPct: 50 }), 30_000) expect(c.querySelector('.tg-ctx-bar')).not.toBeNull() expect(c.querySelector('.tg-ctx-fill')).not.toBeNull() }) it('adds warning class on context fill when contextUsedPct > 80', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry({ contextUsedPct: 85 }), 30_000) expect(c.querySelector('.tg-ctx-fill')?.classList.contains('tg-ctx-warn')).toBe(true) }) it('does NOT add warning class when contextUsedPct === 80', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry({ contextUsedPct: 80 }), 30_000) expect(c.querySelector('.tg-ctx-fill')?.classList.contains('tg-ctx-warn')).toBe(false) }) it('does NOT add warning class when contextUsedPct < 80', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry({ contextUsedPct: 60 }), 30_000) expect(c.querySelector('.tg-ctx-fill')?.classList.contains('tg-ctx-warn')).toBe(false) }) it('skips context bar when contextUsedPct is absent', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry({}), 30_000) expect(c.querySelector('.tg-ctx-bar')).toBeNull() }) // ── cost chip ── it('renders cost chip with $ prefix when costUsd is present', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry({ costUsd: 0.1234 }), 30_000) const cost = c.querySelector('.tg-cost') expect(cost).not.toBeNull() expect(cost?.textContent).toContain('$') expect(cost?.textContent).toContain('0.1234') }) it('skips cost chip when costUsd is absent', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry(), 30_000) expect(c.querySelector('.tg-cost')).toBeNull() }) // ── model chip ── it('renders model chip when model is present', () => { const c = makeContainer() renderTelemetryGauge(c, makeTelemetry({ model: 'claude-3-sonnet' }), 30_000) expect(c.querySelector('.tg-model')?.textContent).toBe('claude-3-sonnet') }) it('renders ' renderTelemetryGauge(c, makeTelemetry({ model: xss }), 30_000) const chip = c.querySelector('.tg-model') expect(chip?.textContent).toBe(xss) // No actual