533 lines
21 KiB
TypeScript
533 lines
21 KiB
TypeScript
import { beforeEach, expect, it, vi } from 'vitest';
|
||
import { render } from 'svelte/server';
|
||
import type {
|
||
MetricsData,
|
||
MapMetrics,
|
||
ServerMetrics,
|
||
MatchMetrics
|
||
} from '../../src/lib/metrics.js';
|
||
import MetricsPage from '../../src/routes/metrics/+page.svelte';
|
||
import MetricsSummary from '../../src/lib/ui/components/metrics/MetricsSummary.svelte';
|
||
import MetricsTables from '../../src/lib/ui/components/metrics/MetricsTables.svelte';
|
||
import MetricsExplore from '../../src/lib/ui/components/metrics/MetricsExplore.svelte';
|
||
import MetricsChart from '../../src/lib/ui/components/metrics/MetricsChart.svelte';
|
||
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 matches = (changes: Partial<MatchMetrics> = {}): MatchMetrics => ({
|
||
started: 271,
|
||
withRecordedPlayers: 271,
|
||
averageGroupSize: 2.4,
|
||
solo: 48,
|
||
soloShare: (100 * 48) / 271,
|
||
...changes
|
||
});
|
||
const map = (i: number, changes: Partial<MapMetrics> = {}): MapMetrics => ({
|
||
game: 't5',
|
||
map: `Map ${String(i).padStart(2, '0')}`,
|
||
matches: matches({
|
||
started: i,
|
||
withRecordedPlayers: i,
|
||
averageGroupSize: 2,
|
||
solo: 0,
|
||
soloShare: 0
|
||
}),
|
||
observed: 10,
|
||
active: 5,
|
||
idle: 5,
|
||
unknown: 0,
|
||
playerHours: i,
|
||
noEmpty: 1,
|
||
availabilityObserved: 4,
|
||
availabilityUnknown: 0,
|
||
noEmptyAtMaximum: 0,
|
||
noEmptyBelowMaximum: 0,
|
||
policy: null,
|
||
...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,
|
||
autoscalingPaused: false,
|
||
scope: { game: null, map: null, timeZone: 'America/New_York' },
|
||
scopeOptions: { games: ['t5'], maps: [] },
|
||
scopeFirstSampleAt: now - 14 * 86400000,
|
||
scopeLatestSampleAt: now,
|
||
matches: matches(),
|
||
summary,
|
||
running: { average: 12, peak: 24, capturedHours: 1 },
|
||
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,
|
||
capturedHours: 1
|
||
}
|
||
],
|
||
previous: {
|
||
since: now - 14 * 86400000,
|
||
until: now - 7 * 86400000,
|
||
summary: { ...summary, active: 4, idle: 6, playerHours: 10 },
|
||
running: { average: 12, peak: 24, capturedHours: 1 },
|
||
firstSampleAt: now - 14 * 86400000,
|
||
lastSampleAt: now - 7 * 86400000,
|
||
partialHistory: true
|
||
},
|
||
pooledAvailability: {
|
||
value: 25,
|
||
noEmptyMapHours: 1,
|
||
knownMapHours: 4,
|
||
unknownMapHours: 0,
|
||
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 } : {}
|
||
});
|
||
}
|
||
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;
|
||
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.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).toContain('Average lobbies running:');
|
||
expect(body.replace(/\s+/g, ' ')).toContain('Captured wall time:');
|
||
expect(body).not.toContain('Percentage points show');
|
||
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');
|
||
expect(body).not.toContain('class="weekly');
|
||
});
|
||
|
||
it('shows concurrent lobbies and empty spare share with unknown lobby-hours in the denominator', () => {
|
||
const data = report();
|
||
let body = render(MetricsSummary, { props: { report: data } }).body;
|
||
expect(body).toContain('12 avg');
|
||
expect(body).toContain('Peak 24 lobbies at once');
|
||
expect(body.replace(/\s+/g, ' ')).toContain(
|
||
'5 h of 12 h captured lobby-hours were empty spares (41.7%)'
|
||
);
|
||
expect(body).toContain('+100%');
|
||
expect(body).toContain('Time with no empty lobby');
|
||
expect(body).toContain('25.0%');
|
||
expect(body).not.toContain('capacity data');
|
||
data.pooledAvailability = {
|
||
value: null,
|
||
noEmptyMapHours: 0,
|
||
knownMapHours: 0,
|
||
unknownMapHours: 4,
|
||
coveredMaps: 0,
|
||
totalMaps: 24
|
||
};
|
||
data.maps = [map(1, { availabilityObserved: 0, availabilityUnknown: 4 })];
|
||
data.previous.summary.playerHours = 0;
|
||
body = render(MetricsSummary, { props: { report: data } }).body;
|
||
expect(body).toContain('Lobby availability unavailable');
|
||
expect(body).toContain('New · no previous player-hours');
|
||
expect(body).not.toContain('Infinity');
|
||
data.summary = { observed: 0, active: 0, idle: 0, playerHours: 0, unknown: 12 };
|
||
body = render(MetricsSummary, { props: { report: data } }).body;
|
||
expect(body).toContain('Comparison unavailable');
|
||
expect(body).toContain('12 avg');
|
||
expect(body.replace(/\s+/g, ' ')).toContain(
|
||
'0 h of 12 h captured lobby-hours were empty spares (0.0%)'
|
||
);
|
||
data.running = { average: null, peak: 0, capturedHours: 0 };
|
||
data.summary.unknown = 0;
|
||
body = render(MetricsSummary, { props: { report: data } }).body;
|
||
expect(body).toContain('Peak unavailable');
|
||
expect(body).not.toContain('Peak 0 lobbies');
|
||
});
|
||
|
||
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;
|
||
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.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);
|
||
expect(metricPage([], '20')).toMatchObject({ page: 1, start: 0, end: 0, total: 0 });
|
||
});
|
||
|
||
it('shows match counts, average group and solo share with explicit missing roster coverage', () => {
|
||
const data = report();
|
||
let body = render(MetricsSummary, { props: { report: data } }).body.replace(/\s+/g, ' ');
|
||
expect(body).toContain('Matches started');
|
||
expect(body).toContain('>271<');
|
||
expect(body).toContain('Average group 2.4 players');
|
||
expect(body).toContain('48 solo · 17.7% of matches with recorded players');
|
||
|
||
data.matches = matches({ started: 300 });
|
||
body = render(MetricsSummary, { props: { report: data } }).body.replace(/\s+/g, ' ');
|
||
expect(body).toContain('48 solo · 17.7% of matches with recorded players');
|
||
expect(body).toContain('Players recorded for 271 of 300 matches');
|
||
|
||
data.matches = matches({
|
||
started: 3,
|
||
withRecordedPlayers: 0,
|
||
averageGroupSize: null,
|
||
solo: 0,
|
||
soloShare: null
|
||
});
|
||
body = render(MetricsSummary, { props: { report: data } }).body.replace(/\s+/g, ' ');
|
||
expect(body).toContain('>3<');
|
||
expect(body).toContain('Average group unavailable');
|
||
expect(body).toContain('Solo share unavailable');
|
||
expect(body).toContain('Players recorded for 0 of 3 matches');
|
||
expect(body).not.toContain('0 solo');
|
||
});
|
||
|
||
it('sorts map match counts numerically, including maps without occupancy or recorded players', () => {
|
||
const noRoster = matches({
|
||
started: 100,
|
||
withRecordedPlayers: 0,
|
||
averageGroupSize: null,
|
||
solo: 0,
|
||
soloShare: null
|
||
});
|
||
const maps = [
|
||
map(1, { matches: noRoster, observed: 0, active: 0, idle: 0, playerHours: 0 }),
|
||
map(2, { matches: matches({ started: 2, withRecordedPlayers: 2 }) }),
|
||
map(3, { matches: matches({ started: 10, withRecordedPlayers: 10 }) }),
|
||
map(4, { matches: { ...noRoster, started: 0 } })
|
||
];
|
||
scope('?mapSort=matches&mapDirection=asc');
|
||
let body = render(MetricsTables, { props: { maps, servers: [] } }).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'));
|
||
expect(body).toContain('aria-sort="ascending"');
|
||
expect(body).toContain('avg group 2.4');
|
||
expect(body).toContain('Average group unavailable');
|
||
expect(body.replace(/\s+/g, ' ')).toContain('0 of 100');
|
||
|
||
scope('?mapSort=matches&mapDirection=desc');
|
||
body = render(MetricsTables, { props: { maps, servers: [] } }).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'));
|
||
expect(body).toContain('aria-sort="descending"');
|
||
});
|
||
|
||
it('keeps match summaries and map rows visible without any retained occupancy samples', () => {
|
||
const data = report();
|
||
data.summary = { observed: 0, active: 0, idle: 0, unknown: 0, playerHours: 0 };
|
||
data.running = { average: null, peak: 0, capturedHours: 0 };
|
||
data.latestSampleAt = null;
|
||
data.firstRetainedSampleAt = null;
|
||
data.scopeFirstSampleAt = null;
|
||
data.scopeLatestSampleAt = null;
|
||
data.servers = [];
|
||
data.maps = [
|
||
map(1, {
|
||
...data.summary,
|
||
noEmpty: 0,
|
||
availabilityObserved: 0,
|
||
matches: data.matches
|
||
})
|
||
];
|
||
const body = render(MetricsPage, {
|
||
props: { data: { ...data, user: null, setupRequired: false } }
|
||
}).body;
|
||
expect(body).toContain('Matches started');
|
||
expect(body).toContain('Average group 2.4 players');
|
||
expect(body).toContain('aria-label="Map metrics table"');
|
||
expect(body).toContain('Map 01');
|
||
expect(body).toContain('No retained observations yet');
|
||
expect(body).not.toContain('Lobbies running over time');
|
||
expect(body.replace(/\s+/g, ' ')).toContain('including both endpoints and all match statuses');
|
||
expect(body.replace(/\s+/g, ' ')).toContain(
|
||
'distinct recorded players across each match’s lifetime'
|
||
);
|
||
expect(body.replace(/\s+/g, ' ')).toContain('missing or purged records are not estimated');
|
||
});
|
||
|
||
it('keeps unknown availability last when sorting and distinguishes it from known empty lobbies', () => {
|
||
scope('?mapSort=noEmpty&mapDirection=asc');
|
||
const maps = [
|
||
map(1, { availabilityObserved: 0, availabilityUnknown: 4 }),
|
||
map(2, { noEmpty: 0 }),
|
||
map(3)
|
||
];
|
||
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('Lobby availability unavailable');
|
||
expect(body).toContain('No empty lobby');
|
||
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('highlights at most three supported policy changes and collapses tentative findings', () => {
|
||
const data = report();
|
||
data.maps.push(map(25, { observed: 0.1, active: 0.1, idle: 0, availabilityObserved: 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);
|
||
expect(body.indexOf('Map 25')).toBeGreaterThan(fold);
|
||
expect(body).toContain('Limited evidence');
|
||
expect(body).not.toMatch(/<details[^>]*open/);
|
||
data.maps = [map(1, { availabilityObserved: 0 })];
|
||
expect(render(MetricsExplore, { props: { report: data } }).body).toContain(
|
||
'No policy changes meet the evidence threshold'
|
||
);
|
||
});
|
||
|
||
it('shows a global pause once while keeping Kino’s shortage ahead of quiet maps', () => {
|
||
const data = report();
|
||
data.autoscalingPaused = true;
|
||
const policy = {
|
||
id: 't5-all',
|
||
mode: 'create-destroy' as const,
|
||
minimum: 1,
|
||
maximum: 3,
|
||
spares: 1,
|
||
pausedReason: null
|
||
};
|
||
data.maps = [
|
||
map(1, {
|
||
map: 'Kino Der Toten',
|
||
policy,
|
||
observed: 100,
|
||
active: 10,
|
||
idle: 90,
|
||
availabilityObserved: 100,
|
||
noEmpty: 4.1,
|
||
noEmptyAtMaximum: 4.1
|
||
}),
|
||
...Array.from({ length: 5 }, (_, i) => map(i + 2, { policy, noEmpty: 0 }))
|
||
];
|
||
let body = render(MetricsExplore, { props: { report: data } }).body;
|
||
expect(body.match(/Autoscaling is globally paused\./g)).toHaveLength(1);
|
||
expect(body).toContain('Consider raising the policy maximum');
|
||
expect(body).toContain('4.1%');
|
||
expect(body).toContain('href="/autoscaling#t5-all"');
|
||
expect(body).not.toContain('Review and resume the autoscaling policy');
|
||
const highlighted = body.slice(0, body.indexOf('Other findings'));
|
||
expect(highlighted.match(/class="recommendation /g)).toHaveLength(1);
|
||
expect(highlighted).toContain('Kino Der Toten');
|
||
data.autoscalingPaused = false;
|
||
body = render(MetricsExplore, { props: { report: data } }).body;
|
||
expect(body).not.toContain('Autoscaling is globally paused.');
|
||
expect(body).toContain('Consider raising the policy maximum');
|
||
});
|
||
|
||
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,
|
||
capturedHours: 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 empty lobbies');
|
||
expect(body).toContain('tabindex="0"');
|
||
expect(body).toContain('Inspect a period');
|
||
expect(body).toContain('Partial period');
|
||
expect(body).not.toMatch(/<details[^>]*open/);
|
||
expect(body).toContain('Weekdays show when each period starts in America/New York.');
|
||
expect(body).toContain('Daily periods are 24-hour windows.');
|
||
expect(body).toContain('Lobbies running over time');
|
||
expect(body).toContain('Average lobbies running');
|
||
expect(body).toContain('5 with players · 5 empty spares · 2 unknown on average');
|
||
});
|
||
|
||
it('keeps average lobby bar heights comparable across partial periods and collection gaps', () => {
|
||
const data = report();
|
||
const point = data.points[0];
|
||
data.points = [
|
||
{
|
||
...point,
|
||
observed: 48,
|
||
active: 12,
|
||
idle: 36,
|
||
unknown: 24,
|
||
capturedHours: 12,
|
||
partial: false
|
||
},
|
||
{
|
||
...point,
|
||
at: data.now,
|
||
startAt: data.now,
|
||
endAt: data.now + 2 * 3600000,
|
||
observed: 8,
|
||
active: 2,
|
||
idle: 6,
|
||
unknown: 4,
|
||
capturedHours: 2,
|
||
partial: true
|
||
}
|
||
];
|
||
const body = render(MetricsChart, {
|
||
props: { points: data.points, days: data.days, timeZone: data.scope.timeZone }
|
||
}).body;
|
||
const bars = body.match(/<button class="bar\b[\s\S]*?<\/button>/g) ?? [];
|
||
expect(bars).toHaveLength(2);
|
||
const heights = bars.map((bar) =>
|
||
[...bar.matchAll(/style="height:\s*([^"]+)"/g)].map((match) => match[1])
|
||
);
|
||
expect(heights[0]).toHaveLength(3);
|
||
expect(heights[0]).toEqual(heights[1]);
|
||
expect(heights[0]).toContain('50%;');
|
||
expect(
|
||
bars.every((bar) => bar.includes('1 with players · 3 empty spares · 2 unknown on average'))
|
||
).toBe(true);
|
||
});
|
||
|
||
it('renders whole-lobby axis ticks without rounding fractional averages or other measures', () => {
|
||
const data = report();
|
||
data.points[0] = { ...data.points[0], active: 1.5, idle: 1, observed: 2.5, unknown: 1.25 };
|
||
const renderChart = () =>
|
||
render(MetricsChart, {
|
||
props: { points: data.points, days: data.days, timeZone: data.scope.timeZone }
|
||
}).body;
|
||
const axisLabels = (body: string) => {
|
||
const axis = body.match(/<div class="y-axis[\s\S]*?<\/div>/)?.[0] ?? '';
|
||
return [...axis.matchAll(/<span[^>]*>([^<]+)<\/span>/g)].map((match) => match[1]);
|
||
};
|
||
let body = renderChart();
|
||
expect(axisLabels(body)).toEqual(['4', '3', '2', '1', '0']);
|
||
expect(body).toContain('1.5 with players');
|
||
scope('?chartMode=percent');
|
||
body = renderChart();
|
||
expect(axisLabels(body)).toEqual(['100%', '75%', '50%', '25%', '0%']);
|
||
scope('?chartMode=hours');
|
||
body = renderChart();
|
||
expect(axisLabels(body)).toEqual(['4', '3', '2', '1', '0']);
|
||
data.points[0] = { ...data.points[0], active: 0.5, idle: 0, observed: 0.5, unknown: 0 };
|
||
expect(axisLabels(renderChart())).toContain('0.375');
|
||
});
|
||
|
||
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;
|
||
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');
|
||
scope('?view=maps', '?view=insights');
|
||
body = render(MetricsPage, {
|
||
props: { data: { ...report(), user: null, setupRequired: false } }
|
||
}).body;
|
||
expect(body).toContain('id="metrics-explore-title"');
|
||
expect(body).toContain('Time with no empty lobby');
|
||
expect(body).toContain('Autoscaling signals');
|
||
expect(body).not.toContain('aria-label="Map metrics table"');
|
||
expect(body).not.toContain('aria-label="Instance metrics table"');
|
||
});
|
||
|
||
it('filters other signals without hiding the highlighted supported policy changes', () => {
|
||
scope('?view=insights&insightQ=Map+24&insightSort=map');
|
||
const body = render(MetricsExplore, { props: { report: report() } }).body;
|
||
expect(body).toContain('Map 01');
|
||
expect(body).toContain('Map 24');
|
||
expect(body).not.toContain('Map 23');
|
||
});
|
||
|
||
it('links autoscaling signals to current policies and explains their historical comparison', () => {
|
||
const data = report();
|
||
data.maps = [
|
||
map(1, {
|
||
noEmptyAtMaximum: 0.75,
|
||
noEmptyBelowMaximum: 0.25,
|
||
policy: {
|
||
id: 'policy-1',
|
||
mode: 'create-destroy',
|
||
minimum: 1,
|
||
maximum: 3,
|
||
spares: 1,
|
||
pausedReason: null
|
||
}
|
||
})
|
||
];
|
||
let body = render(MetricsExplore, { props: { report: data } }).body;
|
||
expect(body).toContain('href="/autoscaling#policy-1"');
|
||
expect(body).toContain('Review policy');
|
||
expect(body).toContain('Current policy: 1–3 lobbies, 1 spare');
|
||
expect(body.replace(/\s+/g, ' ')).toContain(
|
||
'Current settings are applied to historical lobby counts'
|
||
);
|
||
expect(body).not.toContain('player limits, and observation');
|
||
data.maps[0].policy = null;
|
||
body = render(MetricsExplore, { props: { report: data } }).body;
|
||
expect(body).toContain('href="/autoscaling"');
|
||
expect(body).toContain('Add policy');
|
||
});
|