import { describe, expect, it } from 'vitest'; import { formatBytes, formatEta, formatSpeed, PopupStore, progressPercent } from '../../src/popup/store.js'; import type { TaskSummary } from '../../src/shared/protocol/index.js'; function summary(over: Partial = {}): TaskSummary { return { taskId: 't1', filename: 'file.zip', saveDir: '/home/u/Downloads', url: 'https://example.com/file.zip', downloadedBytes: 0, state: 'downloading', speedBps: 0, resumable: true, segments: 1, createdAt: '2026-01-01T00:00:00Z', ...over, }; } describe('PopupStore', () => { it('starts empty', () => { const s = new PopupStore(); expect(s.rows()).toEqual([]); expect(s.size).toBe(0); }); it('seeds from download.list', () => { const s = new PopupStore(); s.setInitial([summary({ taskId: 'a' }), summary({ taskId: 'b', filename: 'b.zip' })]); expect(s.size).toBe(2); }); it('adds a row on event.task.added', () => { const s = new PopupStore(); s.onAdded({ taskId: 't1', summary: summary() }); expect(s.rows()).toHaveLength(1); expect(s.rows()[0]!.filename).toBe('file.zip'); }); it('applies event.task.progress as a patch, not a rebuild', () => { const s = new PopupStore(); s.setInitial([summary({ taskId: 't1', sizeBytes: 1000 })]); s.onProgress({ at: '2026-01-01T00:00:00Z', tasks: [{ taskId: 't1', downloadedBytes: 500, speedBps: 2048, etaSeconds: 4 }] }); const row = s.rows()[0]!; expect(row.downloadedBytes).toBe(500); expect(row.speedBps).toBe(2048); expect(row.etaSeconds).toBe(4); expect(row.filename).toBe('file.zip'); // untouched by the patch }); it('ignores a progress tick for an unknown task rather than inserting a partial row', () => { const s = new PopupStore(); s.onProgress({ at: '2026-01-01T00:00:00Z', tasks: [{ taskId: 'ghost', downloadedBytes: 1, speedBps: 1 }] }); expect(s.size).toBe(0); }); it('replaces the row on event.task.state when a summary is attached', () => { const s = new PopupStore(); s.setInitial([summary({ taskId: 't1', state: 'downloading' })]); s.onState({ taskId: 't1', state: 'paused', summary: summary({ taskId: 't1', state: 'paused' }) }); expect(s.rows()[0]!.state).toBe('paused'); }); it('updates just the state when event.task.state has no summary', () => { const s = new PopupStore(); s.setInitial([summary({ taskId: 't1', state: 'downloading' })]); s.onState({ taskId: 't1', state: 'failed' }); expect(s.rows()[0]!.state).toBe('failed'); }); it('drops the row on event.task.removed', () => { const s = new PopupStore(); s.setInitial([summary({ taskId: 't1' })]); s.onRemoved({ taskId: 't1', deletedFile: false }); expect(s.size).toBe(0); }); it('sorts active tasks before paused/finished ones, then by filename', () => { const s = new PopupStore(); s.setInitial([ summary({ taskId: 'z', filename: 'z-paused.zip', state: 'paused' }), summary({ taskId: 'b', filename: 'b-active.zip', state: 'downloading' }), summary({ taskId: 'a', filename: 'a-active.zip', state: 'downloading' }), ]); expect(s.rows().map((r) => r.taskId)).toEqual(['a', 'b', 'z']); }); }); describe('format helpers', () => { it('formats bytes', () => { expect(formatBytes(500)).toBe('500 B'); expect(formatBytes(2048)).toBe('2.0 KB'); expect(formatBytes(5 * 1024 * 1024)).toBe('5.0 MB'); }); it('formats speed, blank when idle', () => { expect(formatSpeed(0)).toBe(''); expect(formatSpeed(1024)).toBe('1.0 KB/s'); }); it('formats eta, blank when unknown', () => { expect(formatEta(null)).toBe(''); expect(formatEta(-1)).toBe(''); expect(formatEta(45)).toBe('45s'); expect(formatEta(125)).toBe('2m 5s'); }); it('computes a progress percent, null when size is unknown', () => { expect(progressPercent({ taskId: 't', filename: 'f', state: 'downloading', downloadedBytes: 50, sizeBytes: 100, speedBps: 0, etaSeconds: null })).toBe(50); expect(progressPercent({ taskId: 't', filename: 'f', state: 'downloading', downloadedBytes: 50, sizeBytes: null, speedBps: 0, etaSeconds: null })).toBeNull(); }); });