Add Prettier and ESLint tooling (Refs #77)
This commit is contained in:
@@ -10,55 +10,121 @@ import { metricPage } from '../../src/lib/ui/metrics.js';
|
||||
import { page } from '$app/state';
|
||||
|
||||
vi.mock('$app/state', () => ({ page: { url: new URL('http://localhost/metrics'), state: {} } }));
|
||||
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn(), afterNavigate: vi.fn(), goto: vi.fn(), replaceState: vi.fn() }));
|
||||
const map = (i: number, changes: Partial<MapMetrics> = {}): MapMetrics => ({ game: 't5', map: `Map ${String(i).padStart(2, '0')}`, observed: 10, active: 5, idle: 5, unknown: 0, playerHours: i, full: 1, capacityObserved: 4, ...changes });
|
||||
const server = (i: number): ServerMetrics => ({ server: `t5_zombie_theater_${i}`, currentRegistered: i !== 1, observed: 10, active: i / 10, idle: 10 - i / 10, unknown: 0, playerHours: i });
|
||||
vi.mock('$app/navigation', () => ({
|
||||
beforeNavigate: vi.fn(),
|
||||
afterNavigate: vi.fn(),
|
||||
goto: vi.fn(),
|
||||
replaceState: vi.fn()
|
||||
}));
|
||||
const map = (i: number, changes: Partial<MapMetrics> = {}): MapMetrics => ({
|
||||
game: 't5',
|
||||
map: `Map ${String(i).padStart(2, '0')}`,
|
||||
observed: 10,
|
||||
active: 5,
|
||||
idle: 5,
|
||||
unknown: 0,
|
||||
playerHours: i,
|
||||
full: 1,
|
||||
capacityObserved: 4,
|
||||
...changes
|
||||
});
|
||||
const server = (i: number): ServerMetrics => ({
|
||||
server: `t5_zombie_theater_${i}`,
|
||||
currentRegistered: i !== 1,
|
||||
observed: 10,
|
||||
active: i / 10,
|
||||
idle: 10 - i / 10,
|
||||
unknown: 0,
|
||||
playerHours: i
|
||||
});
|
||||
function report(): MetricsData {
|
||||
const now = Date.parse('2026-10-06T12:00:00Z');
|
||||
const summary = { observed: 10, active: 5, idle: 5, unknown: 2, playerHours: 20 };
|
||||
return { days: 7, since: now - 7 * 86400000, now, firstRetainedSampleAt: now - 14 * 86400000, latestSampleAt: now,
|
||||
registeredServerCount: 24, scope: { game: null, map: null, timeZone: 'America/New_York' }, scopeOptions: { games: ['t5'], maps: [] },
|
||||
scopeFirstSampleAt: now - 14 * 86400000, scopeLatestSampleAt: now, summary,
|
||||
maps: Array.from({ length: 24 }, (_, i) => map(i + 1)), servers: Array.from({ length: 24 }, (_, i) => server(i + 1)),
|
||||
points: [{ ...summary, at: now - 86400000, startAt: now - 86400000, endAt: now, partial: true }],
|
||||
previous: { since: now - 14 * 86400000, until: now - 7 * 86400000, summary: { ...summary, active: 4, idle: 6, playerHours: 10 }, firstSampleAt: now - 14 * 86400000, lastSampleAt: now - 7 * 86400000, partialHistory: true },
|
||||
pooledPressure: { value: 25, fullMapHours: 1, knownMapHours: 4, coveredMaps: 24, totalMaps: 24 }, heatmap: { timeZone: 'America/New_York', cells: [] }
|
||||
return {
|
||||
days: 7,
|
||||
since: now - 7 * 86400000,
|
||||
now,
|
||||
firstRetainedSampleAt: now - 14 * 86400000,
|
||||
latestSampleAt: now,
|
||||
registeredServerCount: 24,
|
||||
scope: { game: null, map: null, timeZone: 'America/New_York' },
|
||||
scopeOptions: { games: ['t5'], maps: [] },
|
||||
scopeFirstSampleAt: now - 14 * 86400000,
|
||||
scopeLatestSampleAt: now,
|
||||
summary,
|
||||
maps: Array.from({ length: 24 }, (_, i) => map(i + 1)),
|
||||
servers: Array.from({ length: 24 }, (_, i) => server(i + 1)),
|
||||
points: [
|
||||
{ ...summary, at: now - 86400000, startAt: now - 86400000, endAt: now, partial: true }
|
||||
],
|
||||
previous: {
|
||||
since: now - 14 * 86400000,
|
||||
until: now - 7 * 86400000,
|
||||
summary: { ...summary, active: 4, idle: 6, playerHours: 10 },
|
||||
firstSampleAt: now - 14 * 86400000,
|
||||
lastSampleAt: now - 7 * 86400000,
|
||||
partialHistory: true
|
||||
},
|
||||
pooledPressure: {
|
||||
value: 25,
|
||||
fullMapHours: 1,
|
||||
knownMapHours: 4,
|
||||
coveredMaps: 24,
|
||||
totalMaps: 24
|
||||
},
|
||||
heatmap: { timeZone: 'America/New_York', cells: [] }
|
||||
};
|
||||
}
|
||||
function scope(search: string, stateSearch?: string) {
|
||||
Object.assign(page, { url: new URL(`http://localhost/metrics${search}`), state: stateSearch ? { metricsTableSearch: stateSearch } : {} });
|
||||
Object.assign(page, {
|
||||
url: new URL(`http://localhost/metrics${search}`),
|
||||
state: stateSearch ? { metricsTableSearch: stateSearch } : {}
|
||||
});
|
||||
}
|
||||
beforeEach(() => scope(''));
|
||||
|
||||
it('shows a summary and one paginated breakdown, preserving scope on range links', () => {
|
||||
scope('?game=t5&days=7&view=maps&mapPage=2');
|
||||
const body = render(MetricsPage, { props: { data: { ...report(), user: null, setupRequired: false } } }).body;
|
||||
const body = render(MetricsPage, {
|
||||
props: { data: { ...report(), user: null, setupRequired: false } }
|
||||
}).body;
|
||||
expect(body).toContain('aria-label="Metrics breakdown"');
|
||||
expect(body).toContain('aria-label="Map metrics table"');
|
||||
expect(body).not.toContain('aria-label="Instance metrics table"');
|
||||
expect(body).toContain('16–24 of 24 maps');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain('16–24 of 24 maps');
|
||||
expect(body).toContain('game=t5&days=30&view=maps');
|
||||
expect(body).toContain('Partial previous history');
|
||||
expect(body).toContain('About this report');
|
||||
expect(body).not.toContain('Explore this period');
|
||||
expect(body).toContain('Updates every minute');
|
||||
expect(body).not.toContain('>Refresh<');
|
||||
const heatmap = body.slice(body.indexOf('class="heatmap'), body.indexOf('aria-label="Metrics breakdown"'));
|
||||
expect(heatmap).toContain('<h2'); expect(heatmap).toContain('Activity by weekday and hour');
|
||||
const heatmap = body.slice(
|
||||
body.indexOf('class="heatmap'),
|
||||
body.indexOf('aria-label="Metrics breakdown"')
|
||||
);
|
||||
expect(heatmap).toContain('<h2');
|
||||
expect(heatmap).toContain('Activity by weekday and hour');
|
||||
expect(body).not.toContain('class="weekly');
|
||||
});
|
||||
|
||||
it('keeps percentage points, relative changes and unavailable capacity distinct', () => {
|
||||
const data = report();
|
||||
let body = render(MetricsSummary, { props: { report: data } }).body;
|
||||
expect(body).toContain('+10 percentage points'); expect(body).toContain('+100%');
|
||||
data.pooledPressure = { value: null, fullMapHours: 0, knownMapHours: 0, coveredMaps: 0, totalMaps: 24 };
|
||||
expect(body).toContain('+10 percentage points');
|
||||
expect(body).toContain('+100%');
|
||||
data.pooledPressure = {
|
||||
value: null,
|
||||
fullMapHours: 0,
|
||||
knownMapHours: 0,
|
||||
coveredMaps: 0,
|
||||
totalMaps: 24
|
||||
};
|
||||
data.maps = [map(1, { capacityObserved: 0 })];
|
||||
data.previous.summary.playerHours = 0;
|
||||
body = render(MetricsSummary, { props: { report: data } }).body;
|
||||
expect(body).toContain('Capacity data unavailable');
|
||||
expect(body).toContain('New · no previous player-hours');
|
||||
expect(body).toContain('1 of 1 maps lack capacity data');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain('1 of 1 maps lack capacity data');
|
||||
expect(body).not.toContain('Infinity');
|
||||
data.summary.observed = 0;
|
||||
body = render(MetricsSummary, { props: { report: data } }).body;
|
||||
@@ -69,10 +135,12 @@ it('searches and sorts the complete map set before pagination and clamps invalid
|
||||
const data = report();
|
||||
scope('?mapPage=2');
|
||||
let body = render(MetricsTables, { props: { maps: data.maps, servers: data.servers } }).body;
|
||||
expect(body).toContain('Map 09'); expect(body).not.toContain('Map 24');
|
||||
expect(body).toContain('Map 09');
|
||||
expect(body).not.toContain('Map 24');
|
||||
scope('?mapPage=2', '?mapQ=Map+24&mapPage=100&mapSort=playerHours');
|
||||
body = render(MetricsTables, { props: { maps: data.maps, servers: data.servers } }).body;
|
||||
expect(body).toContain('Map 24'); expect(body).toContain('1–1 of 1 maps');
|
||||
expect(body).toContain('Map 24');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain('1–1 of 1 maps');
|
||||
expect(body).not.toContain('>Next<');
|
||||
expect(metricPage(data.maps, '-1').page).toBe(1);
|
||||
expect(metricPage(data.maps, 'Infinity').page).toBe(1);
|
||||
@@ -85,12 +153,15 @@ it('keeps missing capacity last when sorting and never presents it as zero full
|
||||
const body = render(MetricsTables, { props: { maps, servers: [] } }).body;
|
||||
expect(body.indexOf('Map 02')).toBeLessThan(body.indexOf('Map 03'));
|
||||
expect(body.indexOf('Map 03')).toBeLessThan(body.indexOf('Map 01'));
|
||||
expect(body).toContain('0.0%'); expect(body).toContain('Capacity data unavailable');
|
||||
expect(body).toContain('0.0%');
|
||||
expect(body).toContain('Capacity data unavailable');
|
||||
});
|
||||
|
||||
it('preserves instance return context and distinguishes historic names without current links', () => {
|
||||
scope('?view=instances&serverQ=theater&days=30');
|
||||
const body = render(MetricsTables, { props: { maps: [], servers: [server(1), server(2)], view: 'instances' } }).body;
|
||||
const body = render(MetricsTables, {
|
||||
props: { maps: [], servers: [server(1), server(2)], view: 'instances' }
|
||||
}).body;
|
||||
expect(body).toContain('Historical');
|
||||
expect(body).not.toContain('href="/servers/t5_zombie_theater_1');
|
||||
expect(body).toContain('href="/servers/t5_zombie_theater_2?returnTo=');
|
||||
@@ -100,7 +171,8 @@ it('preserves instance return context and distinguishes historic names without c
|
||||
});
|
||||
|
||||
it('highlights at most three supported trials and collapses tentative findings', () => {
|
||||
const data = report(); data.maps.push(map(25, { observed: 0.1, active: 0.1, idle: 0, capacityObserved: 0 }));
|
||||
const data = report();
|
||||
data.maps.push(map(25, { observed: 0.1, active: 0.1, idle: 0, capacityObserved: 0 }));
|
||||
const body = render(MetricsExplore, { props: { report: data } }).body;
|
||||
const fold = body.indexOf('Other findings');
|
||||
expect(body.slice(0, fold).match(/class="recommendation /g)).toHaveLength(3);
|
||||
@@ -108,13 +180,27 @@ it('highlights at most three supported trials and collapses tentative findings',
|
||||
expect(body).toContain('Limited evidence');
|
||||
expect(body).not.toMatch(/<details[^>]*open/);
|
||||
data.maps = [map(1, { capacityObserved: 0 })];
|
||||
expect(render(MetricsExplore, { props: { report: data } }).body).toContain('No hosting trials meet the evidence threshold');
|
||||
expect(render(MetricsExplore, { props: { report: data } }).body).toContain(
|
||||
'No hosting trials meet the evidence threshold'
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps accessible chart selection, partial periods and gaps while collapsing inspection controls', () => {
|
||||
const data = report();
|
||||
data.points.push({ at: data.now, startAt: data.now, endAt: data.now + 1000, partial: false, observed: 0, active: 0, idle: 0, unknown: 0, playerHours: 0 });
|
||||
const body = render(MetricsChart, { props: { points: data.points, days: data.days, timeZone: data.scope.timeZone } }).body;
|
||||
data.points.push({
|
||||
at: data.now,
|
||||
startAt: data.now,
|
||||
endAt: data.now + 1000,
|
||||
partial: false,
|
||||
observed: 0,
|
||||
active: 0,
|
||||
idle: 0,
|
||||
unknown: 0,
|
||||
playerHours: 0
|
||||
});
|
||||
const body = render(MetricsChart, {
|
||||
props: { points: data.points, days: data.days, timeZone: data.scope.timeZone }
|
||||
}).body;
|
||||
expect(body).toContain('aria-label="Selected period summary"');
|
||||
expect(body).toContain('No captured time · not evidence of idle servers');
|
||||
expect(body).toContain('tabindex="0"');
|
||||
@@ -128,12 +214,16 @@ it('keeps accessible chart selection, partial periods and gaps while collapsing
|
||||
|
||||
it('uses shallow URL state for the selected breakdown while retaining the common summary', () => {
|
||||
scope('?view=maps', '?view=instances&serverQ=theater&serverPage=2');
|
||||
let body = render(MetricsPage, { props: { data: { ...report(), user: null, setupRequired: false } } }).body;
|
||||
let body = render(MetricsPage, {
|
||||
props: { data: { ...report(), user: null, setupRequired: false } }
|
||||
}).body;
|
||||
expect(body).toContain('aria-label="Instance metrics table"');
|
||||
expect(body).not.toContain('aria-label="Map metrics table"');
|
||||
expect(body).toContain('16–24 of 24 instances');
|
||||
expect(body.replace(/\s+/g, ' ')).toContain('16–24 of 24 instances');
|
||||
scope('?view=maps', '?view=insights');
|
||||
body = render(MetricsPage, { props: { data: { ...report(), user: null, setupRequired: false } } }).body;
|
||||
body = render(MetricsPage, {
|
||||
props: { data: { ...report(), user: null, setupRequired: false } }
|
||||
}).body;
|
||||
expect(body).toContain('id="metrics-explore-title"');
|
||||
expect(body).toContain('Time all lobbies were full');
|
||||
expect(body).not.toContain('aria-label="Map metrics table"');
|
||||
|
||||
Reference in New Issue
Block a user