Make UI language, timestamps and live feedback consistent
This commit is contained in:
@@ -170,7 +170,7 @@ describe('browser folder uploads', () => {
|
||||
expect(next.version).not.toBe(first.version);
|
||||
expect(next.libraryId).toBe(first.libraryId);
|
||||
expect(otherGame.libraryId).not.toBe(first.libraryId);
|
||||
expect(listLibraries().map(library => library.name)).toEqual(['Black Ops I (T5)', 'Black Ops II (T6)']);
|
||||
expect(listLibraries().map(library => library.name)).toEqual(['Black Ops I', 'Black Ops II']);
|
||||
});
|
||||
|
||||
it('follows completion order and preserves the latest successful files during failed or unfinished uploads', async () => {
|
||||
|
||||
@@ -43,8 +43,8 @@ function legacyVersion(id: string, libraryId: string, createdAt: string, publish
|
||||
describe('fixed game file slots', () => {
|
||||
it('always lists exactly T5 and T6, including before any files are uploaded', () => {
|
||||
expect(listLibraries()).toEqual([
|
||||
{ id: 't5', game: 't5', name: 'Black Ops I (T5)', description: '', createdAt: '', latestVersionId: null, versions: [] },
|
||||
{ id: 't6', game: 't6', name: 'Black Ops II (T6)', description: '', createdAt: '', latestVersionId: null, versions: [] }
|
||||
{ id: 't5', game: 't5', name: 'Black Ops I', description: '', createdAt: '', latestVersionId: null, versions: [] },
|
||||
{ id: 't6', game: 't6', name: 'Black Ops II', description: '', createdAt: '', latestVersionId: null, versions: [] }
|
||||
]);
|
||||
expect(latestPublishedLibraries()).toEqual([]);
|
||||
expect(newestReadyLibrary('t5')).toBeNull();
|
||||
|
||||
@@ -137,7 +137,7 @@ describe('automatic host preparation', () => {
|
||||
hosts.updateHost(id, { mode: 'disabled' });
|
||||
hosts.updateHost(id, { mode: 'active' });
|
||||
const status = preparation.reconcileHostPreparation(id);
|
||||
expect(status.items).toContainEqual(expect.objectContaining({ label: 'Black Ops II (T6)', state: 'blocked' }));
|
||||
expect(status.items).toContainEqual(expect.objectContaining({ label: 'Black Ops II', state: 'blocked' }));
|
||||
expect(hosts.getHost(id)?.mode).toBe('disabled');
|
||||
});
|
||||
|
||||
@@ -510,8 +510,8 @@ describe('automatic host preparation', () => {
|
||||
hosts.updateHost(id, { minimumFreeBytes: 1_000 });
|
||||
const status = preparation.reconcileHostPreparation(id);
|
||||
expect(status.items).toEqual(expect.arrayContaining([
|
||||
expect.objectContaining({ label: 'Black Ops I (T5)', state: 'blocked' }),
|
||||
expect.objectContaining({ label: 'Black Ops II (T6)', state: 'preparing' })
|
||||
expect.objectContaining({ label: 'Black Ops I', state: 'blocked' }),
|
||||
expect.objectContaining({ label: 'Black Ops II', state: 'preparing' })
|
||||
]));
|
||||
expect(operations(id)).toHaveLength(1);
|
||||
expect(operations(id)[0].payload.libraryVersionId).toBe(small.id);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { get } from 'svelte/store';
|
||||
import { availableTimeZones, createTimeFormatter, DEFAULT_TIME_ZONE, validTimeZone } from '../../src/lib/time-zone.js';
|
||||
import { availableTimeZones, createTimeFormatter, DEFAULT_TIME_ZONE, validTimeZone, timestampISO } from '../../src/lib/time-zone.js';
|
||||
import { startTimeZonePreference, timeFormat, timeZone, timeZonePersistenceError, TIME_ZONE_STORAGE_KEY } from '../../src/lib/ui/stores/time-zone.js';
|
||||
|
||||
let stop: (() => void) | undefined;
|
||||
@@ -52,9 +52,29 @@ describe('timezone formatting', () => {
|
||||
it('renders missing or invalid timestamps without crashing the page', () => {
|
||||
for (const value of [null, undefined, '', 'bad timestamp', NaN, new Date(NaN)]) {
|
||||
expect(ny.dateTime(value)).toBe('—');
|
||||
expect(ny.relative(value)).toBe('—');
|
||||
expect(timestampISO(value)).toBeUndefined();
|
||||
}
|
||||
});
|
||||
|
||||
it('shares compact recent times, exact older dates, and UTC normalization', () => {
|
||||
const now = Date.parse('2026-10-07T18:00:00Z');
|
||||
expect(ny.relative(now - 2_000, now)).toBe('Just now');
|
||||
expect(ny.relative('2026-10-07 17:59:48', now)).toBe('12 s ago');
|
||||
expect(ny.relative(now - 120_000, now)).toBe('2 min ago');
|
||||
expect(ny.relative(now - 23 * 3_600_000, now)).toBe('23 h ago');
|
||||
expect(ny.relative('2026-10-06T17:00:00Z', now)).toBe('Yesterday, 1:00 PM');
|
||||
expect(ny.relative('2026-10-05T17:00:00Z', now)).toBe('Oct 5, 2026, 1:00 PM EDT');
|
||||
expect(ny.relative('2026-10-08T18:00:00Z', now)).toBe('Oct 8, 2026, 2:00 PM EDT');
|
||||
expect(timestampISO('2026-10-07 17:59:48')).toBe('2026-10-07T17:59:48.000Z');
|
||||
});
|
||||
|
||||
it('uses timezone calendar dates for yesterday across DST and year boundaries', () => {
|
||||
expect(ny.relative('2026-11-01T04:30:00Z', Date.parse('2026-11-02T05:15:00Z'))).toBe('Yesterday, 12:30 AM');
|
||||
expect(ny.relative('2025-12-31T05:00:00Z', Date.parse('2026-01-01T06:00:00Z'))).toBe('Yesterday, 12:00 AM');
|
||||
expect(createTimeFormatter('invalid').relative('2026-10-05T17:00:00Z', Date.parse('2026-10-07T18:00:00Z'))).toBe('Oct 5, 2026, 1:00 PM EDT');
|
||||
});
|
||||
|
||||
it('offers valid zones including New York and UTC, with an older-browser fallback', () => {
|
||||
expect(availableTimeZones()).toContain(DEFAULT_TIME_ZONE);
|
||||
expect(availableTimeZones()).toContain('UTC');
|
||||
|
||||
@@ -44,7 +44,7 @@ it('shows relative recent timestamps and preserves accurate older dates and inva
|
||||
expect(activityTimeLabel('2026-10-07T16:00:00Z', 'America/New_York', now)).toBe('2 h ago');
|
||||
expect(activityTimeLabel('2026-10-06T17:00:00Z', 'America/New_York', now)).toBe('Yesterday, 1:00 PM');
|
||||
expect(activityTimeLabel('not a date', 'UTC', now)).toBe('Time unavailable');
|
||||
expect(activityTimeLabel('2026-10-08T18:00:00Z', 'America/New_York', now)).toBe('Oct 8, 2:00 PM');
|
||||
expect(activityTimeLabel('2026-10-08T18:00:00Z', 'America/New_York', now)).toBe('Oct 8, 2026, 2:00 PM EDT');
|
||||
});
|
||||
|
||||
it('renders one page heading and keeps management tools available only to operators', () => {
|
||||
|
||||
@@ -36,7 +36,7 @@ it('keeps current estimates and host comparison before history with scoped histo
|
||||
expect(body.indexOf('aria-label="History range"')).toBeGreaterThan(body.indexOf('id="capacity-history-title"'));
|
||||
expect(body).toContain('range=7d&game=t5&host=ec-1');
|
||||
expect(body).toContain('3 of 3 available');
|
||||
expect(body).toContain('No queued instances');
|
||||
expect(body).toContain('No queued servers');
|
||||
expect(body).not.toContain('href="/provisioning"');
|
||||
expect(body).not.toContain('Manage hosts');
|
||||
expect(body).toContain('Updates every 30 seconds');
|
||||
@@ -48,13 +48,13 @@ it('keeps fleet-wide queue scope explicit and preserves unavailable estimates',
|
||||
const body = render(CapacityPage, { props: { data: report } }).body;
|
||||
expect(body).toContain('Capacity estimate unavailable');
|
||||
expect(body).toContain('href="/provisioning"');
|
||||
expect(body).toContain('2 instances queued');
|
||||
expect(body).toContain('2 servers queued');
|
||||
expect(body).toContain('Fleet-wide');
|
||||
});
|
||||
|
||||
it('keeps ineligible hosts and their reasons visible while collapsing detailed assumptions', () => {
|
||||
const body = render(CapacityHosts, { props: { returnTo: '/capacity', hosts: [host(), host({ id: 'missing-files', name: 'Missing files', eligible: false, estimatedServers: 0, limitingResource: null, reasons: ['Latest game files are not ready.'] }), host({ id: 'offline', name: 'Offline host', connected: false, eligible: false, estimatedServers: 0, cpuCoresAvailable: null, memoryBytesAvailable: null, limitingResource: null, reasons: ['Waiting for a fresh host report.'] })] } }).body;
|
||||
expect(body).toContain('Additional instances</th>');
|
||||
expect(body).toContain('Additional servers</th>');
|
||||
expect(body).toContain('Not eligible');
|
||||
expect(body).toContain('Latest game files are not ready.');
|
||||
expect(body).toContain('Offline');
|
||||
@@ -62,7 +62,7 @@ it('keeps ineligible hosts and their reasons visible while collapsing detailed a
|
||||
expect(body).not.toContain('>Ready<');
|
||||
expect(body).toMatch(/<tr hidden[^>]*id="capacity-host-ec-1"/);
|
||||
expect(body).toContain('aria-expanded="false"');
|
||||
expect(body).toContain('Running instance');
|
||||
expect(body).toContain('Running server');
|
||||
expect(body).toContain('aria-label="Host capacity comparison" tabindex="0"');
|
||||
});
|
||||
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
import { afterEach, describe, expect, it, vi } from 'vitest';
|
||||
import { render } from 'svelte/server';
|
||||
import Freshness from '../../src/lib/ui/components/Freshness.svelte';
|
||||
import { stripGameColorCodes } from '../../src/lib/ui/log-text.js';
|
||||
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
describe('quiet refresh feedback', () => {
|
||||
it('shows relative receipt time with an exact tooltip without announcing every tick', () => {
|
||||
vi.useFakeTimers(); vi.setSystemTime('2026-10-07T12:03:00Z');
|
||||
const body = render(Freshness, { props: { updatedAt: Date.parse('2026-10-07T12:00:00Z'), intervalLabel: 'Updates every 5 seconds' } }).body;
|
||||
expect(body).toContain('Live');
|
||||
expect(body).toContain('3 min ago');
|
||||
expect(body).toContain('datetime="2026-10-07T12:00:00.000Z"');
|
||||
expect(body).toMatch(/<time[^>]+title="[^"]+"/);
|
||||
expect(body).toContain('Updates every 5 seconds');
|
||||
expect(body).not.toMatch(/aria-live|role="status"|>Retry</);
|
||||
});
|
||||
|
||||
it('does not claim live updates or an automatic schedule while paused', () => {
|
||||
const body = render(Freshness, { props: { paused: true, updatedAt: Date.now(), intervalLabel: 'Updates every 5 seconds' } }).body;
|
||||
expect(body).toContain('Paused');
|
||||
expect(body).not.toMatch(/Live|Updates every/);
|
||||
});
|
||||
|
||||
it('replaces live claims with a retry on failures and treats invalid timestamps as unknown', () => {
|
||||
const body = render(Freshness, { props: { updatedAt: Date.now(), error: 'Log updates interrupted.', intervalLabel: 'Updates every 5 seconds', onRetry: () => {}, retryDisabled: true } }).body;
|
||||
expect(body).toContain('Log updates interrupted.');
|
||||
expect(body).toMatch(/<button[^>]+disabled[^>]*>Retry<\/button>/);
|
||||
expect(body).not.toMatch(/Live|Updates every/);
|
||||
const waiting = render(Freshness, { props: { updatedAt: Number.NaN } }).body;
|
||||
expect(waiting).toContain('Waiting for update');
|
||||
expect(waiting).not.toContain('<time');
|
||||
});
|
||||
});
|
||||
|
||||
describe('readable game logs', () => {
|
||||
it('strips digit colour directives while preserving log whitespace and other carets', () => {
|
||||
const raw = ' [12:30] ^1Red^7 player\t^2joined^0\r\npath ^name, exponent x^y, trailing ^';
|
||||
expect(stripGameColorCodes(raw)).toBe(' [12:30] Red player\tjoined\r\npath ^name, exponent x^y, trailing ^');
|
||||
expect(raw).toContain('^1Red^7');
|
||||
});
|
||||
|
||||
it('allows searches across embedded colour changes without mutating downloadable source lines', () => {
|
||||
const raw = ['^1Con^2nection^7 accepted', ' Player ^3Jamie^7 joined '];
|
||||
const displayed = raw.map(stripGameColorCodes);
|
||||
expect(displayed.filter(line => line.toLowerCase().includes('connection'))).toEqual(['Connection accepted']);
|
||||
expect(displayed.join('\n')).toBe('Connection accepted\n Player Jamie joined ');
|
||||
expect(raw.join('\n')).toBe('^1Con^2nection^7 accepted\n Player ^3Jamie^7 joined ');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,34 @@
|
||||
import { describe, expect, it, vi } from 'vitest';
|
||||
import { render } from 'svelte/server';
|
||||
import { writable } from 'svelte/store';
|
||||
import HostOperations from '../../src/routes/hosts/operations/+page.svelte';
|
||||
import type { HostOperationSummary } from '../../src/lib/hosts.js';
|
||||
|
||||
vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/hosts/operations') }), navigating: writable(null) }));
|
||||
vi.mock('$app/navigation', () => ({ beforeNavigate: vi.fn() }));
|
||||
|
||||
const operation: HostOperationSummary = {
|
||||
id: 'retained-op', hostId: 'host-1', hostName: 'Dallas 1', kind: 'instance.inspect',
|
||||
instanceId: 'container-1', generation: 1, state: 'completed', error: null,
|
||||
serverName: 't6_zombie_town_1', targetPath: null, source: 'maintenance', routine: true,
|
||||
createdAt: '2026-10-07 17:00:00', updatedAt: '2026-10-07 17:00:00'
|
||||
};
|
||||
|
||||
describe('host operation history presentation', () => {
|
||||
it('uses readable operation, server and state labels while keeping identifiers in technical detail', () => {
|
||||
const body = render(HostOperations, { props: { data: {
|
||||
history: { operations: [operation], page: 1, pageSize: 25, totalPages: 1, total: 1, filter: 'all' },
|
||||
scope: {}, hostName: null
|
||||
} as any } }).body;
|
||||
expect(body).toContain('Check server container');
|
||||
expect(body).toContain('Town Survival · Lobby 1');
|
||||
expect(body).toContain('Completed');
|
||||
expect(body).toContain('Technical details</summary>');
|
||||
expect(body.indexOf('instance.inspect')).toBeGreaterThan(body.indexOf('Technical details</summary>'));
|
||||
expect(body).toContain('retained-op');
|
||||
expect(body).toContain('datetime="2026-10-07T17:00:00.000Z"');
|
||||
expect(body).toContain('title="Oct 7, 2026, 1:00:00 PM EDT"');
|
||||
expect(body).toContain('Updates every 15 seconds');
|
||||
expect(body).not.toContain('>Refresh</button>');
|
||||
});
|
||||
});
|
||||
@@ -57,8 +57,8 @@ it('distinguishes real zero usage from missing or invalid metrics', () => {
|
||||
|
||||
it('renders compact host links and workload without duplicate identity, setup metadata or deletion actions', () => {
|
||||
const body = render(HostList, { props: { hosts: [{ ...host(), serverCount: 3, playerCount: 12, unreportedInstances: 0 }], now } }).body;
|
||||
expect(body).toContain('3 assigned instances on ec-1');
|
||||
expect(body).toContain('>Instances</th>');
|
||||
expect(body).toContain('3 assigned servers on ec-1');
|
||||
expect(body).toContain('>Servers</th>');
|
||||
expect(body).toContain('>Players</th>');
|
||||
expect(body).toContain('>12</td>');
|
||||
expect(body).toContain('0.0%');
|
||||
@@ -76,7 +76,7 @@ it('renders compact host links and workload without duplicate identity, setup me
|
||||
it('marks partial player totals and distinguishes unavailable counts from empty hosts', () => {
|
||||
const partial = render(HostList, { props: { hosts: [{ ...host(), serverCount: 3, playerCount: 4, unreportedInstances: 1 }], now } }).body;
|
||||
expect(partial).toContain('>4+</td>');
|
||||
expect(partial).toContain('Player counts unavailable for 1 of 3 instances');
|
||||
expect(partial).toContain('Player counts unavailable for 1 of 3 servers');
|
||||
const unknown = render(HostList, { props: { hosts: [{ ...host(), serverCount: 3, playerCount: 0, unreportedInstances: 3 }], now } }).body;
|
||||
expect(unknown).toContain('>—</td>');
|
||||
const empty = render(HostList, { props: { hosts: [{ ...host(), serverCount: 0, playerCount: 0, unreportedInstances: 0 }], now } }).body;
|
||||
|
||||
@@ -0,0 +1,72 @@
|
||||
import { expect, it, vi } from 'vitest';
|
||||
import { render } from 'svelte/server';
|
||||
import { writable } from 'svelte/store';
|
||||
import { plural, countLabel } from '../../src/lib/ui/plural.js';
|
||||
import { gameTitle } from '../../src/lib/ui/server-labels.js';
|
||||
import { playerCompletionLabel } from '../../src/lib/ui/player-match.js';
|
||||
import { hostCalibrationHint } from '../../src/lib/ui/host-overview.js';
|
||||
import { scopeLabel } from '../../src/lib/script-library.js';
|
||||
import Setup from '../../src/routes/setup/+page.svelte';
|
||||
import Hosts from '../../src/routes/hosts/+page.svelte';
|
||||
import PlayerMatchCards from '../../src/lib/ui/components/PlayerMatchCards.svelte';
|
||||
import GameFileHosts from '../../src/lib/ui/components/GameFileHosts.svelte';
|
||||
import type { Host } from '../../src/lib/hosts.js';
|
||||
import type { PlayerMatch } from '../../src/lib/leaderboard.js';
|
||||
|
||||
vi.mock('$app/stores', () => ({ page: writable({ url: new URL('http://localhost/hosts'), data: { user: { id: 'admin', role: 'admin' } } }) }));
|
||||
vi.mock('$app/navigation', () => ({ goto: vi.fn(), beforeNavigate: vi.fn(), afterNavigate: vi.fn(), invalidateAll: vi.fn() }));
|
||||
const now = Date.parse('2026-10-07T12:00:00Z');
|
||||
const host: Host = { id: 'dallas', name: 'Dallas 1', hostname: 'dallas.example', mode: 'active', portMin: 28960, portMax: 28980,
|
||||
bindAddress: '0.0.0.0', publicAddress: '', minimumFreeBytes: 100, createdAt: new Date(now).toISOString(),
|
||||
lastHeartbeat: new Date(now).toISOString(), connected: false, connectionState: 'offline', readiness: 'ready', readinessReasons: [],
|
||||
inventory: {}, enrolled: true, credentialActive: true, decommission: null };
|
||||
const match: PlayerMatch = { id: 17, game: 't5', server: 't5_zombie_theater_1', mapName: 'Kino Der Toten', startedAt: new Date(now).toISOString(), lastSeen: new Date(now).toISOString(), status: 'completed', completed: 1, highestRound: 30, roundsSurvived: 20, kills: 1200, headshots: 200, revives: 2, downs: 1, bestScore: 45000 };
|
||||
|
||||
it('pluralizes zero, one, multiple, fractional and irregular counts consistently', () => {
|
||||
expect(countLabel(0, 'server')).toBe('0 servers');
|
||||
expect(countLabel(1, 'player')).toBe('1 player');
|
||||
expect(countLabel(2000, 'host')).toBe('2,000 hosts');
|
||||
expect(countLabel(1, 'match', 'matches')).toBe('1 match');
|
||||
expect(countLabel(2, 'match', 'matches')).toBe('2 matches');
|
||||
expect(plural(1, 'needs', 'need')).toBe('needs');
|
||||
expect(plural(0, 'needs', 'need')).toBe('need');
|
||||
expect(plural(1.5, 'hour')).toBe('hours');
|
||||
});
|
||||
|
||||
it('recommends a sampling upgrade only for a known older version', () => {
|
||||
for (const version of ['1.0.0', '1.4.5', 'v1.4.0']) expect(hostCalibrationHint(version)).toContain('Update this host to agent 1.4.6');
|
||||
for (const version of ['1.4.6', '1.7.0', '1.10.0', '2.0.0', 'v2.0.0+build', null, undefined, 'unknown', '1.4']) {
|
||||
expect(hostCalibrationHint(version)).toContain('still calibrating');
|
||||
expect(hostCalibrationHint(version)).not.toContain('Update');
|
||||
}
|
||||
});
|
||||
|
||||
it('describes per-player completion as final-report evidence without inventing unfinished results', () => {
|
||||
expect(playerCompletionLabel(match)).toBe('Present in final report');
|
||||
expect(playerCompletionLabel({ ...match, completed: 0 })).toBe('Not present in final report');
|
||||
expect(playerCompletionLabel({ ...match, status: 'active', completed: 0 })).toBe('Match in progress');
|
||||
expect(playerCompletionLabel({ ...match, status: 'interrupted', completed: 0 })).toBe('Final report unavailable');
|
||||
const body = render(PlayerMatchCards, { props: { matches: [match, { ...match, id: 18, completed: 0 }] } }).body;
|
||||
expect(body).toContain('Present in final report'); expect(body).toContain('Not present in final report');
|
||||
expect(body).not.toContain('Completion credited'); expect(body).not.toContain('No completion credit');
|
||||
});
|
||||
|
||||
it('uses product game names while preserving unsupported game identifiers', () => {
|
||||
expect(gameTitle('t5')).toBe('Black Ops I'); expect(gameTitle('T6')).toBe('Black Ops II');
|
||||
expect(gameTitle('t5', true)).toBe('BO1'); expect(gameTitle('t6', true)).toBe('BO2');
|
||||
expect(gameTitle('custom')).toBe('CUSTOM'); expect(gameTitle(null)).toBe('Unknown game');
|
||||
expect(scopeLabel({ scope_type: 'game', scope_value: 't5' })).toBe('Black Ops I');
|
||||
});
|
||||
|
||||
it('does not offer an existing-account sign-in path on first administrator setup', () => {
|
||||
const body = render(Setup).body;
|
||||
expect(body).toContain('Create the first administrator'); expect(body).not.toContain('Already have an account');
|
||||
expect(body).not.toContain('href="/login"');
|
||||
});
|
||||
|
||||
it('renders singular host attention and explicit whitespace before an offline suffix', () => {
|
||||
const body = render(Hosts, { props: { data: { hosts: [{ ...host, serverCount: 0, playerCount: 0, unreportedInstances: 0 }], serverNow: now, user: null, setupRequired: false } } }).body;
|
||||
expect(body).toContain('1 host'); expect(body).toContain('1 needs attention'); expect(body).not.toContain('1 need attention');
|
||||
const files = render(GameFileHosts, { props: { hosts: [host], caches: [{ hostId: host.id, libraryVersionId: 'files', status: 'missing', bytesDownloaded: 0, bytesTotal: 0, manifestSha256: null, error: null, updatedAt: new Date(now).toISOString(), operationId: null }] } }).body;
|
||||
expect(files).toMatch(/Dallas 1<\/a>.*?> · Offline<\/span>/);
|
||||
});
|
||||
@@ -17,7 +17,7 @@ it('renders one page heading, six columns, a combined identity, and compact date
|
||||
const body = render(MatchHistoryPage, { props: { data: { history: report(), user: null, setupRequired: false } } }).body;
|
||||
expect(body.match(/<h1[ >]/g)).toHaveLength(1);
|
||||
expect(body.match(/scope="col"/g)).toHaveLength(6);
|
||||
expect(body).toContain('Today, 1:26 PM'); expect(body).toContain('Lobby 1 · Black Ops II');
|
||||
expect(body).toContain('2 h ago'); expect(body).toContain('Lobby 1 · Black Ops II');
|
||||
expect(body).not.toContain('>Server</th>'); expect(body).not.toContain('Show 25 more');
|
||||
expect(body).toContain('matchPage=2');
|
||||
});
|
||||
@@ -60,7 +60,7 @@ it('offers filter reset for an empty selection and retains the scoped instance v
|
||||
});
|
||||
it('keeps custom instance names and calculates yesterday across DST and year boundaries', () => {
|
||||
expect(matchInstanceLabel({ ...match, server: 'Weekend lobby' })).toBe('Weekend lobby · Black Ops II');
|
||||
expect(matchStartedLabel('2026-03-08T06:30:00Z', 'America/New_York', new Date('2026-03-09T04:15:00Z'))).toBe('Yesterday, 1:30 AM');
|
||||
expect(matchStartedLabel('2025-10-06T17:26:00Z', 'America/New_York', new Date('2026-10-06T20:00:00Z'))).toBe('Oct 6, 2025, 1:26 PM');
|
||||
expect(matchStartedLabel('2026-10-06 17:26:00', 'America/New_York', new Date('2026-10-06T20:00:00Z'))).toBe('Today, 1:26 PM');
|
||||
expect(matchStartedLabel('2026-03-08T06:30:00Z', 'America/New_York', new Date('2026-03-09T04:15:00Z'))).toBe('21 h ago');
|
||||
expect(matchStartedLabel('2025-10-06T17:26:00Z', 'America/New_York', new Date('2026-10-06T20:00:00Z'))).toBe('Oct 6, 2025, 1:26 PM EDT');
|
||||
expect(matchStartedLabel('2026-10-06 17:26:00', 'America/New_York', new Date('2026-10-06T20:00:00Z'))).toBe('2 h ago');
|
||||
});
|
||||
|
||||
@@ -116,7 +116,7 @@ it('keeps accessible chart selection, partial periods and gaps while collapsing
|
||||
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 instances');
|
||||
expect(body).toContain('No captured time · not evidence of idle servers');
|
||||
expect(body).toContain('tabindex="0"');
|
||||
expect(body).toContain('Inspect a period');
|
||||
expect(body).toContain('Partial period');
|
||||
|
||||
Reference in New Issue
Block a user