Files
lanspread/crates/lanspread-tauri-deno-ts/tests/gameState.test.ts
T
ddidderr f62515451b feat(ui): label streamed installs as not shareable
NEXT_STEPS item 7 needed the installed-but-not-downloaded state to be
clear to users. Keep streamed installs in the installed visual state so
sorting, filters, and the primary Play action stay unchanged, but make the
sharing limitation visible in the UI.

Cards now label that state as `Not shareable`, while the detail modal
status says `Installed, not shareable`. Downloaded-and-installed games
keep the normal `Installed` wording.

Test Plan:
- just frontend-test
- just build
- git diff --check
- git diff --cached --check

Refs: NEXT_STEPS.md item 7
2026-06-07 22:29:26 +02:00

287 lines
8.4 KiB
TypeScript

import {
actionLabel,
activeStatusById,
applyFilterAndSort,
canStreamInstall,
countByFilter,
deriveState,
downloadProgressPercent,
formatDownloadBytes,
formatBytesPerSecond,
formatDownloadEta,
formatDownloadSpeed,
formatDownloadSpeedShort,
gameStatusLabel,
mergeGameUpdate,
stateChipLabel,
} from '../src/lib/gameState.ts';
import {
ActiveOperationKind,
GameAvailability,
InstallStatus,
type Game,
} from '../src/lib/types.ts';
const assertEquals = <T>(actual: T, expected: T, message: string) => {
if (actual !== expected) {
throw new Error(`${message}: expected ${expected}, got ${actual}`);
}
};
const game = (overrides: Partial<Game> = {}): 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');
});
Deno.test('stream install is available only for idle remote games', () => {
assertEquals(
canStreamInstall(game({ downloaded: false, installed: false, peer_count: 1 })),
true,
'remote-only idle games should allow streamed install',
);
assertEquals(
canStreamInstall(game({ downloaded: true, installed: false, peer_count: 1 })),
false,
'downloaded games should install from local archives',
);
assertEquals(
canStreamInstall(game({ downloaded: false, installed: true, peer_count: 1 })),
false,
'installed games should not expose streamed install',
);
assertEquals(
canStreamInstall(game({ downloaded: false, installed: false, peer_count: 0 })),
false,
'games without peers should not expose streamed install',
);
assertEquals(
canStreamInstall(game({
downloaded: false,
installed: false,
peer_count: 1,
install_status: InstallStatus.CheckingPeers,
})),
false,
'busy games should not expose streamed install',
);
});
Deno.test('streamed local installs are labeled installed but not shareable', () => {
const streamed = game({
downloaded: false,
installed: true,
install_status: InstallStatus.Installed,
});
const downloadedInstall = game({
downloaded: true,
installed: true,
install_status: InstallStatus.Installed,
});
assertEquals(
deriveState(streamed),
'installed',
'streamed local installs should keep installed visual state',
);
assertEquals(
stateChipLabel(streamed),
'Not shareable',
'card chip should make the non-shareable state visible',
);
assertEquals(
gameStatusLabel(streamed),
'Installed, not shareable',
'detail status should spell out installed plus non-shareable',
);
assertEquals(
stateChipLabel(downloadedInstall),
'Installed',
'normal downloaded installs should keep the installed chip label',
);
assertEquals(
gameStatusLabel(downloadedInstall),
'Installed',
'normal downloaded installs should keep the installed detail label',
);
});