95 lines
4.5 KiB
TypeScript
95 lines
4.5 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>\s*<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/);
|
|
});
|
|
});
|