import { actionLabel, activeStatusById, applyFilterAndSort, countByFilter, deriveState, downloadProgressPercent, formatDownloadBytes, formatBytesPerSecond, formatDownloadEta, formatDownloadSpeed, formatDownloadSpeedShort, mergeGameUpdate, } from '../src/lib/gameState.ts'; import { ActiveOperationKind, GameAvailability, InstallStatus, type Game, } from '../src/lib/types.ts'; const assertEquals = (actual: T, expected: T, message: string) => { if (actual !== expected) { throw new Error(`${message}: expected ${expected}, got ${actual}`); } }; const game = (overrides: Partial = {}): Game => ({ id: 'game', name: 'Game', description: '', size: 0, downloaded: false, installed: false, availability: GameAvailability.LocalOnly, install_status: InstallStatus.NotInstalled, peer_count: 1, ...overrides, }); Deno.test('snapshot keeps installing visible until installed state settles', () => { const fromDownloading = game({ install_status: InstallStatus.Downloading, }); const installing = mergeGameUpdate( game({ downloaded: true }), fromDownloading, InstallStatus.Installing, ); const installedWhileActive = mergeGameUpdate( game({ downloaded: true, installed: true }), installing, InstallStatus.Installing, ); const settled = mergeGameUpdate( game({ downloaded: true, installed: true }), installedWhileActive, ); assertEquals( installing.install_status, InstallStatus.Installing, 'active install snapshot should render Installing', ); assertEquals( installedWhileActive.install_status, InstallStatus.Installing, 'installed local state should not override an active install snapshot', ); assertEquals( settled.install_status, InstallStatus.Installed, 'cleared active snapshot with installed local state should render Installed', ); }); Deno.test('active operation snapshot is the source of busy status', () => { const statuses = activeStatusById([ { id: 'game', operation: ActiveOperationKind.Downloading }, { id: 'other', operation: ActiveOperationKind.Updating }, ]); assertEquals( statuses.get('game'), InstallStatus.Downloading, 'download operation should render Downloading', ); assertEquals( statuses.get('other'), InstallStatus.Installing, 'update operation should render Installing', ); }); Deno.test('download progress is preserved only while actively downloading', () => { const downloading = game({ install_status: InstallStatus.Downloading, download_progress: { downloaded_bytes: 50, total_bytes: 100, bytes_per_second: 12_500_000, active_peer_count: 2, }, }); const stillDownloading = mergeGameUpdate( game(), downloading, InstallStatus.Downloading, ); const settled = mergeGameUpdate(game({ downloaded: true }), stillDownloading); assertEquals( stillDownloading.download_progress?.downloaded_bytes, 50, 'active download snapshot should keep progress', ); assertEquals( stillDownloading.download_progress?.active_peer_count, 2, 'active download snapshot should keep live peer count', ); assertEquals( settled.download_progress, undefined, 'settled snapshot should clear progress', ); }); Deno.test('downloading action label includes current speed', () => { const downloading = game({ install_status: InstallStatus.Downloading, download_progress: { downloaded_bytes: 50, total_bytes: 100, bytes_per_second: 12_500_000, active_peer_count: 2, }, }); assertEquals( formatBytesPerSecond(12_500_000), '12.5 MB/s', 'speed formatter should use compact decimal units', ); assertEquals( actionLabel(downloading), 'Downloading… 12.5 MB/s', 'download label should include speed', ); }); Deno.test('downloading state is distinct and stays on the local filter', () => { const downloading = game({ id: 'downloading', name: 'Downloading', install_status: InstallStatus.Downloading, }); const local = game({ id: 'local', name: 'Local', downloaded: true, }); const remote = game({ id: 'remote', name: 'Remote', peer_count: 1, }); assertEquals( deriveState(downloading), 'downloading', 'download operation should render the dedicated downloading state', ); assertEquals( countByFilter([downloading, local, remote]).local, 2, 'local filter count should include in-flight downloads', ); assertEquals( applyFilterAndSort([downloading, local, remote], 'local', 'status', '').length, 2, 'local filter should include in-flight downloads', ); }); Deno.test('download progress formatting matches the progress-bar layouts', () => { const downloading = game({ install_status: InstallStatus.Downloading, download_progress: { downloaded_bytes: 12 * 1024 * 1024 * 1024, total_bytes: 35 * 1024 * 1024 * 1024, bytes_per_second: 49_400_000, active_peer_count: 3, }, }); assertEquals( Math.round(downloadProgressPercent(downloading)), 34, 'progress percent should come from backend byte counters', ); assertEquals(formatDownloadSpeed(49_400_000), '49.4 MB/s', 'large bar speed format'); assertEquals(formatDownloadSpeedShort(49_400_000), '49 MB/s', 'card speed format'); assertEquals( formatDownloadBytes(12 * 1024 * 1024 * 1024), '12 GB', 'downloaded byte format should avoid noisy trailing decimals', ); assertEquals(formatDownloadEta(485), '8 min', 'eta format should stay compact'); });