📄 src/downloader/test/state.test.ts
D-OPEN SOVEREIGN
1import { describe, it, expect, vi } from 'vitest';
2import {
3 downloadState,
4 _setFileState, _setRenderPage, _setBookData, _setPruning,
5 _hydrateFiles, _hydrateRenders,
6} from '../state';
7
8describe('downloadState', () => {
9 it('get() returns "unknown" phase for an untracked txId', () => {
10 expect(downloadState.get('never-seen')).toEqual({ phase: 'unknown' });
11 });
12
13 it('_setFileState merges into existing state and notifies subscribers', () => {
14 const seen: string[] = [];
15 const unsubscribe = downloadState.subscribe(key => seen.push(key));
16
17 _setFileState('tx1', { phase: 'downloading', loaded: 10, total: 100 });
18 expect(downloadState.get('tx1')).toEqual({ phase: 'downloading', loaded: 10, total: 100 });
19
20 _setFileState('tx1', { phase: 'done' });
21 // loaded/total from the prior patch are preserved (merge, not replace).
22 expect(downloadState.get('tx1')).toEqual({ phase: 'done', loaded: 10, total: 100 });
23
24 expect(seen).toEqual(['file:tx1', 'file:tx1']);
25 unsubscribe();
26 });
27
28 it('subscribe() returns an unsubscribe function that stops further notifications', () => {
29 const cb = vi.fn();
30 const unsubscribe = downloadState.subscribe(cb);
31 unsubscribe();
32 _setFileState('tx2', { phase: 'queued' });
33 expect(cb).not.toHaveBeenCalled();
34 });
35
36 it('getRender() returns an empty pages map for an untracked txId', () => {
37 expect(downloadState.getRender('no-render')).toEqual({ pages: new Map() });
38 });
39
40 it('_setRenderPage creates render state on first write and updates on subsequent writes', () => {
41 _setRenderPage('book1', 0, 'downloading');
42 expect(downloadState.getRender('book1').pages.get(0)).toBe('downloading');
43 _setRenderPage('book1', 0, 'done');
44 expect(downloadState.getRender('book1').pages.get(0)).toBe('done');
45 });
46
47 it('_setBookData attaches bookData without clobbering existing page state', () => {
48 _setRenderPage('book2', 0, 'done');
49 _setBookData('book2', { txId: 'book2', dimensions: [{ width: 1, height: 2 }], parsed: true });
50 const rs = downloadState.getRender('book2');
51 expect(rs.bookData?.txId).toBe('book2');
52 expect(rs.pages.get(0)).toBe('done');
53 });
54
55 it('_setPruning merges partial patches into the pruning state', () => {
56 _setPruning({ active: true });
57 expect(downloadState.pruning.active).toBe(true);
58 _setPruning({ active: false, freedBytes: 500, removedCount: 3 });
59 expect(downloadState.pruning).toEqual({ active: false, freedBytes: 500, removedCount: 3 });
60 });
61
62 it('_hydrateFiles bulk-sets file state entries', () => {
63 _hydrateFiles([['h1', { phase: 'done' }], ['h2', { phase: 'error', error: 'x' }]]);
64 expect(downloadState.get('h1')).toEqual({ phase: 'done' });
65 expect(downloadState.get('h2')).toEqual({ phase: 'error', error: 'x' });
66 });
67
68 it('_hydrateRenders bulk-sets page phases per txId, preserving existing bookData', () => {
69 _setBookData('h3', { txId: 'h3', dimensions: [], parsed: true });
70 _hydrateRenders([['h3', [[0, 'done'], [1, 'done']]]]);
71 const rs = downloadState.getRender('h3');
72 expect(rs.pages.get(0)).toBe('done');
73 expect(rs.pages.get(1)).toBe('done');
74 expect(rs.bookData?.txId).toBe('h3');
75 });
76
77 it('summary() aggregates active/queued counts and loaded/total bytes', () => {
78 _setFileState('s1', { phase: 'downloading', loaded: 10, total: 100 });
79 _setFileState('s2', { phase: 'downloading', loaded: 20, total: 200 });
80 _setFileState('s3', { phase: 'queued' });
81 _setFileState('s4', { phase: 'done' });
82
83 const summary = downloadState.summary();
84 expect(summary.active).toBeGreaterThanOrEqual(2);
85 expect(summary.queued).toBeGreaterThanOrEqual(1);
86 expect(summary.loaded).toBeGreaterThanOrEqual(30);
87 expect(summary.total).toBeGreaterThanOrEqual(300);
88 });
89
90 it('files/renders/pruning getters expose live, read-only views', () => {
91 _setFileState('view-check', { phase: 'done' });
92 expect(downloadState.files.get('view-check')).toEqual({ phase: 'done' });
93 expect(downloadState.renders).toBeInstanceOf(Map);
94 expect(typeof downloadState.pruning.active).toBe('boolean');
95 });
96});
97