📄 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