78 lines
4.3 KiB
TypeScript
78 lines
4.3 KiB
TypeScript
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(/<a\b[^>]*>/)?.[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(/<dt[^>]*>Round<\/dt><dd[^>]*>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(/<h2\b[^>]*>No campaigns<\/h2>/);
|
|
expect(body).not.toMatch(/role="status"|aria-live=/);
|
|
expect(render(EmptyState, { props: { ...props, headingLevel: 3 } }).body).toMatch(/<h3\b/);
|
|
});
|
|
});
|