import { describe, expect, it, vi } from 'vitest'; import { render } from 'svelte/server'; import { get, writable } from 'svelte/store'; import ServerCard from '../../src/lib/ui/components/ServerCard.svelte'; import EmptyState from '../../src/lib/ui/components/primitives/EmptyState.svelte'; import StatusBadge from '../../src/lib/ui/components/primitives/StatusBadge.svelte'; import StatusPip from '../../src/lib/ui/components/primitives/StatusPip.svelte'; import { statusPresentation } from '../../src/lib/ui/status.js'; import { uiNow } from '../../src/lib/ui/stores/servers.js'; import { gameEvent } from '../helpers/telemetry.js'; vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/?filter=occupied') }) })); describe('status accessibility', () => { it.each([ ['online', 'Online', 'online', 'circle'], ['starting', 'Starting', 'info', 'ring'], ['provisioning', 'Provisioning', 'info', 'ring'], ['stale', 'Not responding', 'warning', 'triangle'], ['offline', 'Offline', 'danger', 'square'], ['queued', 'Queued', 'neutral', 'hollow'] ])('pairs %s with a consistent word and non-color cue without creating a live region', (state, label, tone, shape) => { expect(statusPresentation(state)).toEqual({ label, tone, shape }); const body = render(StatusBadge, { props: { state } }).body; expect(body).toContain(label); expect(body).toContain(`shape-${shape}`); expect(body).toContain(`v-${tone}`); expect(body).not.toMatch(/role="status"|aria-live=/); }); it('provides a static accessible name only when an isolated pip needs one', () => { const decorative = render(StatusPip, { props: { health: 'online' } }).body; expect(decorative).toContain('aria-hidden="true"'); expect(decorative).not.toContain('role='); const labelled = render(StatusPip, { props: { health: 'online', label: 'Online' } }).body; expect(labelled).toContain('role="img"'); expect(labelled).toContain('aria-label="Online"'); expect(labelled).not.toContain('aria-hidden='); }); it('names the card link from its title and keeps its clock and roster out of live regions', () => { const receivedAt = new Date(get(uiNow) - 10_000).toISOString(); const body = render(ServerCard, { props: { name: 't5_zombie_theater_1', health: 'online', players: 4, telemetry: { ...gameEvent('heartbeat', 30, 4), receivedAt }, activeSince: receivedAt, gameStartedAt: receivedAt, roster: [{ num: 0, name: 'Player One', score: 30000, ping: 40, guid: '1' }] } }).body; const card = body.match(/]*>/)?.[0] ?? ''; const titleId = card.match(/aria-labelledby="([^"]+)"/)?.[1]?.split(' ')[0]; expect(titleId).toBeTruthy(); expect(body).toContain(`id="${titleId}"`); expect(card).not.toContain('aria-label='); expect(body).toContain('Online'); expect(body).toMatch(/]*>Round<\/dt>]*>30<\/dd>/); expect(body).toContain('Player One'); expect(body).not.toMatch(/aria-live=|role="status"/); }); it('decodes persisted errors and keeps unknown technical errors in diagnostics', () => { const props = { name: 't5_zombie_theater_1', health: 'offline' as const }; const known = render(ServerCard, { props: { ...props, lastError: 'timeout:Agent received no reply.' } }).body; expect(known).toContain('Agent received no reply.'); expect(known).not.toContain('timeout:'); const unknown = render(ServerCard, { props: { ...props, lastError: 'unknown-error:raw diagnostic data' } }).body; expect(unknown).toContain('Open diagnostics for details.'); expect(unknown).not.toContain('raw diagnostic data'); }); it('uses a page-level heading by default and allows nested empty states without live announcements', () => { const props = { title: 'No campaigns', description: 'Campaigns apply file changes to your servers.' }; const body = render(EmptyState, { props }).body; expect(body.replaceAll(//g, '')).toMatch(/]*>No campaigns<\/h2>/); expect(body).not.toMatch(/role="status"|aria-live=/); expect(render(EmptyState, { props: { ...props, headingLevel: 3 } }).body).toMatch(/