Files
electric-cherry/tests/unit/ui-game-file-transfer.test.ts

167 lines
5.6 KiB
TypeScript

import { describe, expect, it } from 'vitest';
import type { HostLibraryCache } from '../../src/lib/game-files.js';
import { gameFileTransferProgress } from '../../src/lib/ui/game-file-transfer.js';
const now = Date.parse('2026-10-04T16:00:00Z');
function cache(changes: Partial<HostLibraryCache> = {}): HostLibraryCache {
return {
hostId: 'host-1',
libraryVersionId: 't5-upload',
status: 'downloading',
bytesDownloaded: 200,
bytesVerified: 0,
bytesTotal: 1000,
manifestSha256: null,
operationId: 'operation-1',
error: null,
updatedAt: new Date(now).toISOString(),
transfer: {
phase: 'downloading',
startedAt: new Date(now - 10_000).toISOString(),
elapsedSeconds: 10,
phaseElapsedSeconds: 10,
bytesPerSecond: 20,
etaSeconds: 40
},
...changes
};
}
describe('game file transfer presentation', () => {
it('ticks elapsed time without inventing downloaded bytes or counting down an old estimate', () => {
expect(gameFileTransferProgress(cache(), true, now + 5000)).toMatchObject({
phase: 'downloading',
phaseBytes: 200,
downloaded: 200,
total: 1000,
elapsedSeconds: 15,
bytesPerSecond: 20,
etaSeconds: 40,
live: true
});
});
it('switches progress and the estimate to verification without resetting total elapsed', () => {
const verifying = cache({
status: 'verifying',
bytesDownloaded: 1000,
bytesVerified: 100,
transfer: {
phase: 'verifying',
startedAt: new Date(now - 60_000).toISOString(),
elapsedSeconds: 60,
phaseElapsedSeconds: 2,
bytesPerSecond: 50,
etaSeconds: 18
}
});
expect(gameFileTransferProgress(verifying, true, now + 1000)).toMatchObject({
phase: 'verifying',
phaseBytes: 100,
downloaded: 1000,
elapsedSeconds: 61,
bytesPerSecond: 50,
etaSeconds: 18
});
});
it('freezes at the reported duration and hides estimates while offline', () => {
expect(gameFileTransferProgress(cache(), false, now + 12_000)).toMatchObject({
elapsedSeconds: 10,
etaSeconds: null,
bytesPerSecond: null,
live: false
});
});
it('stops the clock and removes the estimate when reports become stale', () => {
expect(gameFileTransferProgress(cache(), true, now + 30_001)).toMatchObject({
elapsedSeconds: 40,
etaSeconds: null,
bytesPerSecond: null,
stale: true
});
expect(gameFileTransferProgress(cache(), true, now + 600_000).elapsedSeconds).toBe(40);
});
it.each(['ready', 'outdated', 'failed'] as const)(
'keeps a final duration for %s copies',
(status) => {
expect(gameFileTransferProgress(cache({ status }), true, now + 600_000)).toMatchObject({
elapsedSeconds: 10,
bytesPerSecond: null,
etaSeconds: null,
active: false,
stale: false
});
}
);
it('does not invent a timer for queued downloads or old agents', () => {
expect(
gameFileTransferProgress(cache({ transfer: undefined }), true, now + 60_000)
).toMatchObject({ elapsedSeconds: null, etaSeconds: null, phaseBytes: 200 });
expect(
gameFileTransferProgress(
cache({
status: 'verifying',
transfer: undefined,
bytesVerified: undefined,
bytesDownloaded: 1000
}),
true,
now
)
).toMatchObject({
elapsedSeconds: null,
etaSeconds: null,
phase: 'verifying',
phaseBytes: null
});
});
it.each([0, null, Number.NaN, Number.POSITIVE_INFINITY, -1])(
'hides estimates for an unavailable transfer rate (%s)',
(bytesPerSecond) => {
const current = cache();
current.transfer!.bytesPerSecond = bytesPerSecond;
expect(gameFileTransferProgress(current, true, now)).toMatchObject({
etaSeconds: null,
bytesPerSecond: null
});
}
);
it('does not show a misleading estimate before any bytes arrive or after all bytes arrive', () => {
for (const bytesDownloaded of [0, 1000]) {
expect(gameFileTransferProgress(cache({ bytesDownloaded }), true, now)).toMatchObject({
etaSeconds: null,
bytesPerSecond: null
});
}
});
it('does not reuse a download estimate after verification starts', () => {
expect(
gameFileTransferProgress(
cache({ status: 'verifying', bytesDownloaded: 1000, bytesVerified: 50 }),
true,
now + 5000
)
).toMatchObject({
phase: 'verifying',
phaseBytes: 50,
etaSeconds: null,
elapsedSeconds: 10
});
});
it('handles an invalid report timestamp without advancing the timer or displaying estimates', () => {
expect(gameFileTransferProgress(cache({ updatedAt: 'invalid' }), true, now)).toMatchObject({
elapsedSeconds: 10,
etaSeconds: null,
stale: true
});
});
});