Replace instance metrics with map lobby counts (Refs #139)
CI / checks (push) Successful in 4m55s

This commit is contained in:
2026-10-10 16:40:07 -04:00
parent 616b1937ed
commit aebfc7a783
10 changed files with 278 additions and 278 deletions
+61 -21
View File
@@ -30,6 +30,7 @@ const matches = (changes: Partial<MatchMetrics> = {}): MatchMetrics => ({
...changes
});
const map = (i: number, changes: Partial<MapMetrics> = {}): MapMetrics => ({
running: { average: 2.5, peak: 3, capturedHours: 4 },
game: 't5',
map: `Map ${String(i).padStart(2, '0')}`,
matches: matches({
@@ -189,11 +190,11 @@ it('shows concurrent lobbies and empty spare share with unknown lobby-hours in t
it('searches and sorts the complete map set before pagination and clamps invalid pages', () => {
const data = report();
scope('?mapPage=2');
let body = render(MetricsTables, { props: { maps: data.maps, servers: data.servers } }).body;
let body = render(MetricsTables, { props: { maps: data.maps } }).body;
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;
body = render(MetricsTables, { props: { maps: data.maps } }).body;
expect(body).toContain('Map 24');
expect(body.replace(/\s+/g, ' ')).toContain('1–1 of 1 maps');
expect(body).not.toContain('>Next<');
@@ -245,7 +246,7 @@ it('sorts map match counts numerically, including maps without occupancy or reco
map(4, { matches: { ...noRoster, started: 0 } })
];
scope('?mapSort=matches&mapDirection=asc');
let body = render(MetricsTables, { props: { maps, servers: [] } }).body;
let body = render(MetricsTables, { props: { maps } }).body;
expect(body.indexOf('Map 04')).toBeLessThan(body.indexOf('Map 02'));
expect(body.indexOf('Map 02')).toBeLessThan(body.indexOf('Map 03'));
expect(body.indexOf('Map 03')).toBeLessThan(body.indexOf('Map 01'));
@@ -255,7 +256,7 @@ it('sorts map match counts numerically, including maps without occupancy or reco
expect(body.replace(/\s+/g, ' ')).toContain('0 of 100');
scope('?mapSort=matches&mapDirection=desc');
body = render(MetricsTables, { props: { maps, servers: [] } }).body;
body = render(MetricsTables, { props: { maps } }).body;
expect(body.indexOf('Map 01')).toBeLessThan(body.indexOf('Map 03'));
expect(body.indexOf('Map 03')).toBeLessThan(body.indexOf('Map 02'));
expect(body.indexOf('Map 02')).toBeLessThan(body.indexOf('Map 04'));
@@ -274,6 +275,7 @@ it('keeps match summaries and map rows visible without any retained occupancy sa
data.maps = [
map(1, {
...data.summary,
running: data.running,
noEmpty: 0,
availabilityObserved: 0,
matches: data.matches
@@ -302,7 +304,7 @@ it('keeps unknown availability last when sorting and distinguishes it from known
map(2, { noEmpty: 0 }),
map(3)
];
const body = render(MetricsTables, { props: { maps, servers: [] } }).body;
const body = render(MetricsTables, { props: { maps } }).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%');
@@ -311,18 +313,56 @@ it('keeps unknown availability last when sorting and distinguishes it from known
expect(body).not.toContain('All lobbies full');
});
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;
expect(body).toContain('Historical');
expect(body).not.toContain('href="/servers/t5_zombie_theater_1');
expect(body).toContain('href="/servers/t5_zombie_theater_2?returnTo=');
expect(body).toContain('serverQ%3Dtheater');
expect(body).not.toContain('>Idle</button>');
expect(body).not.toContain('Open current server');
});
it.each(['', '?view=instances&serverQ=theater&days=30'])(
'shows map lobby metrics without an Instances tab for %s',
(search) => {
scope(search);
const body = render(MetricsPage, {
props: { data: { ...report(), user: null, setupRequired: false } }
}).body;
expect(body).toContain('aria-label="Map metrics table"');
expect(body).toContain('aria-labelledby="metrics-views-maps"');
expect(body).toContain('Lobbies (avg · peak)');
expect(body).not.toContain('Instances');
expect(body).not.toContain('Instance utilization');
expect(body).not.toContain('Historical');
expect(body).not.toContain('grouped by name');
}
);
it.each(['asc', 'desc'])(
'sorts average lobby counts numerically %s with missing evidence last and shows empty time',
(direction) => {
scope(`?mapSort=lobbies&mapDirection=${direction}`);
const maps = [
map(1, { running: { average: null, peak: 0, capturedHours: 0 }, observed: 0, idle: 0 }),
map(2, { running: { average: 2.5, peak: 4, capturedHours: 2 }, idle: 0 }),
map(3, { running: { average: 10, peak: 12, capturedHours: 1 }, idle: 1.2 }),
map(4, {
running: { average: 3, peak: 3, capturedHours: 1 },
observed: 0,
idle: 0,
unknown: 3
})
];
const body = render(MetricsTables, { props: { maps } }).body;
const ordered = direction === 'asc' ? [2, 4, 3, 1] : [3, 4, 2, 1];
for (let i = 1; i < ordered.length; i++) {
expect(body.indexOf(`Map 0${ordered[i - 1]}`)).toBeLessThan(
body.indexOf(`Map 0${ordered[i]}`)
);
}
expect(body).toContain(`aria-sort="${direction === 'asc' ? 'ascending' : 'descending'}"`);
expect(body).toContain('2.5 · 4');
expect(body).toContain('10 · 12');
expect(body).toContain('3 · 3');
expect(body).toContain('0 h empty');
expect(body).toContain('1.2 h empty');
expect(body).toContain('Empty time unavailable');
expect(body).toContain('Captured map time');
expect(body).toMatch(/class="lobbies [^"]*">—\s*<small/);
}
);
it('highlights at most three supported policy changes and collapses tentative findings', () => {
const data = report();
@@ -475,13 +515,13 @@ it('renders whole-lobby axis ticks without rounding fractional averages or other
});
it('uses shallow URL state for the selected breakdown while retaining the common summary', () => {
scope('?view=maps', '?view=instances&serverQ=theater&serverPage=2');
scope('?view=insights', '?view=maps&mapPage=2');
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.replace(/\s+/g, ' ')).toContain('16–24 of 24 instances');
expect(body).toContain('aria-label="Map metrics table"');
expect(body).not.toContain('id="metrics-explore-title"');
expect(body.replace(/\s+/g, ' ')).toContain('16–24 of 24 maps');
scope('?view=maps', '?view=insights');
body = render(MetricsPage, {
props: { data: { ...report(), user: null, setupRequired: false } }