mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-05 17:08:26 +00:00
414 lines
15 KiB
TypeScript
414 lines
15 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
import { useDownloadStore } from './useDownloadStore';
|
|
import { useSettingsStore } from './useSettingsStore';
|
|
import * as ipc from '../ipc';
|
|
|
|
vi.mock('../ipc', () => ({
|
|
invokeCommand: vi.fn(),
|
|
}));
|
|
|
|
// Mock window.__TAURI_INTERNALS__ and log to prevent errors
|
|
vi.mock('../utils/logger', () => ({
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
}));
|
|
|
|
vi.mock('@tauri-apps/api/path', () => ({
|
|
homeDir: vi.fn().mockResolvedValue('/Users/test'),
|
|
join: vi.fn(async (...parts: string[]) =>
|
|
parts
|
|
.map((part, index) => index === 0 ? part.replace(/[\\/]+$/, '') : part.replace(/^[\\/]+|[\\/]+$/g, ''))
|
|
.join('/')
|
|
),
|
|
}));
|
|
|
|
vi.mock('./useSettingsStore', () => ({
|
|
useSettingsStore: {
|
|
getState: vi.fn(() => ({
|
|
proxyMode: 'none',
|
|
siteLogins: [],
|
|
globalSpeedLimit: '',
|
|
perServerConnections: 16,
|
|
customUserAgent: '',
|
|
maxAutomaticRetries: 3,
|
|
mediaCookieSource: 'none',
|
|
baseDownloadFolder: '~/Downloads',
|
|
categorySubfolders: {
|
|
Musics: 'Musics',
|
|
Movies: 'Movies',
|
|
Compressed: 'Compressed',
|
|
Documents: 'Documents',
|
|
Pictures: 'Pictures',
|
|
Applications: 'Applications',
|
|
Other: 'Other',
|
|
},
|
|
categoryDirectoryOverrides: {},
|
|
})),
|
|
}
|
|
}));
|
|
|
|
describe('useDownloadStore', () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
useDownloadStore.setState({
|
|
downloads: [],
|
|
backendRegisteredIds: new Set(),
|
|
pendingOrder: [],
|
|
isAddModalOpen: false,
|
|
pendingAddUrls: '',
|
|
pendingAddReferer: '',
|
|
pendingAddFilename: '',
|
|
pendingAddHeaders: '',
|
|
pendingAddCookies: '',
|
|
});
|
|
});
|
|
|
|
it('Start Queue dispatches exactly once for mixed dispatched/undispatched items', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: [
|
|
{ id: '1', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
|
|
{ id: '2', url: 'http://test2', fileName: 'f2', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
|
|
] as any[],
|
|
backendRegisteredIds: new Set(['1']), // 1 is already registered, so it skips dispatch
|
|
});
|
|
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
|
if (cmd === 'get_pending_order') return ['1', '2'];
|
|
return undefined;
|
|
});
|
|
|
|
const dispatched = await useDownloadStore.getState().startQueue('MAIN');
|
|
expect(dispatched).toEqual(['1', '2']);
|
|
|
|
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
|
|
const enqueues = calls.filter(c => c[0] === 'enqueue_download');
|
|
expect(enqueues.length).toBe(1);
|
|
expect((enqueues[0] as any)[1].item.id).toBe('2');
|
|
});
|
|
|
|
it('does not overwrite a downloading event received while starting a queue', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: [
|
|
{ id: '1', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
|
|
] as any[],
|
|
});
|
|
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
|
if (cmd === 'enqueue_download') {
|
|
useDownloadStore.getState().updateDownload('1', {
|
|
status: 'downloading',
|
|
speed: '1 MB/s',
|
|
eta: '10s'
|
|
});
|
|
}
|
|
if (cmd === 'get_pending_order') return ['1'];
|
|
return undefined;
|
|
});
|
|
|
|
expect(await useDownloadStore.getState().startQueue('MAIN')).toEqual(['1']);
|
|
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
|
|
status: 'downloading',
|
|
speed: '1 MB/s',
|
|
eta: '10s',
|
|
hasBeenDispatched: true
|
|
});
|
|
});
|
|
|
|
it('resumeDownload unregisters ID and re-dispatches if un-resumable', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: [
|
|
{ id: '1', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
|
|
] as any[],
|
|
backendRegisteredIds: new Set(['1']),
|
|
});
|
|
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
|
if (cmd === 'resume_download') return false; // Not resumable
|
|
if (cmd === 'get_pending_order') return ['1'];
|
|
return undefined;
|
|
});
|
|
|
|
await useDownloadStore.getState().resumeDownload('1');
|
|
|
|
// It should have called resume_download, then unregistered, then enqueue_download
|
|
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
|
|
expect(calls.some(c => c[0] === 'resume_download')).toBe(true);
|
|
expect(calls.some(c => c[0] === 'enqueue_download')).toBe(true);
|
|
expect(useDownloadStore.getState().backendRegisteredIds.has('1')).toBe(true); // Re-registered by dispatchItem
|
|
});
|
|
|
|
|
|
it('adds to the selected queue without dispatching', async () => {
|
|
await useDownloadStore.getState().addDownload({
|
|
id: 'queue-1',
|
|
url: 'https://example.com/queue.bin',
|
|
fileName: 'queue.bin',
|
|
category: 'Other',
|
|
dateAdded: ''
|
|
}, { type: 'add-to-queue', queueId: 'queue-b' });
|
|
|
|
const item = useDownloadStore.getState().downloads[0];
|
|
expect(item.status).toBe('staged');
|
|
expect(item.queueId).toBe('queue-b');
|
|
expect(item.queuePosition).toBe(0);
|
|
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything());
|
|
});
|
|
|
|
it('starts immediately in the main queue', async () => {
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
|
if (cmd === 'get_pending_order') return ['start-1'];
|
|
return undefined;
|
|
});
|
|
|
|
await useDownloadStore.getState().addDownload({
|
|
id: 'start-1',
|
|
url: 'https://example.com/start.bin',
|
|
fileName: 'start.bin',
|
|
category: 'Other',
|
|
dateAdded: ''
|
|
}, { type: 'start-now' });
|
|
|
|
const item = useDownloadStore.getState().downloads[0];
|
|
expect(item.queueId).toBe('00000000-0000-0000-0000-000000000001');
|
|
expect(item.hasBeenDispatched).toBe(true);
|
|
expect(ipc.invokeCommand).toHaveBeenCalledWith(
|
|
'enqueue_download',
|
|
expect.objectContaining({
|
|
item: expect.objectContaining({ id: 'start-1' })
|
|
})
|
|
);
|
|
});
|
|
|
|
it('reports a rejected immediate start instead of claiming success', async () => {
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string) => {
|
|
if (command === 'enqueue_download') {
|
|
throw new Error('backend unavailable');
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
const added = await useDownloadStore.getState().addDownload({
|
|
id: 'rejected-start',
|
|
url: 'https://example.com/rejected.bin',
|
|
fileName: 'rejected.bin',
|
|
category: 'Other',
|
|
dateAdded: ''
|
|
}, { type: 'start-now' });
|
|
|
|
expect(added).toBe(false);
|
|
expect(useDownloadStore.getState().downloads[0].status).toBe('failed');
|
|
});
|
|
|
|
it('redownloads fallback media without requiring a format selector', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: [{
|
|
id: 'media-fallback',
|
|
url: 'https://youtube.com/watch?v=test',
|
|
fileName: 'watch',
|
|
destination: '/tmp',
|
|
status: 'completed',
|
|
category: 'Other',
|
|
dateAdded: '',
|
|
isMedia: true
|
|
}] as any[]
|
|
});
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
|
if (cmd === 'get_pending_order') return [];
|
|
return undefined;
|
|
});
|
|
|
|
await useDownloadStore.getState().redownload('media-fallback');
|
|
|
|
expect(ipc.invokeCommand).toHaveBeenCalledWith(
|
|
'enqueue_download',
|
|
expect.objectContaining({
|
|
item: expect.objectContaining({
|
|
is_media: true,
|
|
format_selector: null
|
|
})
|
|
})
|
|
);
|
|
});
|
|
|
|
it('starts and pauses all items regardless of legacy missing queue ids', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: [
|
|
{ id: 'ready', url: 'http://ready', fileName: 'ready', status: 'ready', category: 'Other', dateAdded: '' },
|
|
{ id: 'active', url: 'http://active', fileName: 'active', status: 'processing', category: 'Other', dateAdded: '' },
|
|
] as any[],
|
|
});
|
|
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
|
if (cmd === 'get_pending_order') return ['ready'];
|
|
return undefined;
|
|
});
|
|
|
|
expect(await useDownloadStore.getState().startAll()).toBe(1);
|
|
expect(await useDownloadStore.getState().pauseAll()).toBe(2);
|
|
|
|
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
|
|
expect(calls.some(call => call[0] === 'enqueue_download')).toBe(true);
|
|
expect(calls.some(call => call[0] === 'pause_download' && (call[1] as any).id === 'active')).toBe(true);
|
|
});
|
|
|
|
it('migrates legacy downloads without queue ids into the main queue', async () => {
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
|
if (cmd === 'db_get_all_queues') return [];
|
|
if (cmd === 'db_get_all_downloads') {
|
|
return [JSON.stringify({
|
|
id: 'legacy',
|
|
url: 'https://example.com/legacy.bin',
|
|
fileName: 'legacy.bin',
|
|
status: 'ready',
|
|
category: 'Other',
|
|
dateAdded: ''
|
|
})];
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
await useDownloadStore.getState().initDB();
|
|
|
|
expect(useDownloadStore.getState().downloads[0].queueId)
|
|
.toBe('00000000-0000-0000-0000-000000000001');
|
|
});
|
|
|
|
it('pauses queued, downloading, processing, and retrying queue items', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: ['queued', 'downloading', 'processing', 'retrying'].map((status, index) => ({
|
|
id: `${index}`,
|
|
url: `https://example.com/${index}`,
|
|
fileName: `${index}.bin`,
|
|
status,
|
|
category: 'Other',
|
|
dateAdded: '',
|
|
queueId: 'queue-a'
|
|
})) as any[]
|
|
});
|
|
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
|
|
|
|
expect(await useDownloadStore.getState().pauseQueue('queue-a')).toBe(4);
|
|
expect(
|
|
vi.mocked(ipc.invokeCommand).mock.calls.filter(call => call[0] === 'pause_download')
|
|
).toHaveLength(4);
|
|
});
|
|
|
|
it('assigns selected unfinished downloads to a queue without moving completed items', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: [
|
|
{ id: 'ready', status: 'ready', queueId: 'old' },
|
|
{ id: 'done', status: 'completed', queueId: 'old' }
|
|
] as any[]
|
|
});
|
|
|
|
await useDownloadStore.getState().assignToQueue(['ready', 'done'], 'new');
|
|
|
|
expect(useDownloadStore.getState().downloads.find(item => item.id === 'ready')?.queueId).toBe('new');
|
|
expect(useDownloadStore.getState().downloads.find(item => item.id === 'done')?.queueId).toBe('old');
|
|
});
|
|
|
|
it('disables scheduler when its last selected queue is deleted', async () => {
|
|
const originalSettings = useSettingsStore.getState();
|
|
const setScheduler = vi.fn();
|
|
vi.mocked(useSettingsStore.getState).mockReturnValue({
|
|
scheduler: {
|
|
enabled: true,
|
|
selectedQueueIds: ['queue-a']
|
|
},
|
|
setScheduler
|
|
} as any);
|
|
useDownloadStore.setState({
|
|
queues: [
|
|
{ id: '00000000-0000-0000-0000-000000000001', name: 'Main Queue', isMain: true },
|
|
{ id: 'queue-a', name: 'Scheduled', isMain: false }
|
|
],
|
|
downloads: []
|
|
});
|
|
|
|
await useDownloadStore.getState().removeQueue('queue-a');
|
|
|
|
expect(setScheduler).toHaveBeenCalledWith(expect.objectContaining({
|
|
enabled: false,
|
|
selectedQueueIds: []
|
|
}));
|
|
vi.mocked(useSettingsStore.getState).mockReturnValue(originalSettings);
|
|
});
|
|
|
|
it('retains the UI item when backend removal fails', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: [
|
|
{ id: 'active', url: 'https://example.com/file', fileName: 'file', status: 'downloading', category: 'Other', dateAdded: '', queueId: 'main' }
|
|
] as any[]
|
|
});
|
|
vi.mocked(ipc.invokeCommand).mockRejectedValueOnce(new Error('writer did not stop'));
|
|
|
|
await expect(useDownloadStore.getState().removeDownload('active', true))
|
|
.rejects.toThrow('writer did not stop');
|
|
expect(useDownloadStore.getState().downloads.map(download => download.id))
|
|
.toEqual(['active']);
|
|
});
|
|
|
|
it('starts staged queue items in their persisted queue order', async () => {
|
|
useDownloadStore.setState({
|
|
downloads: [
|
|
{ id: 'later', url: 'https://example.com/later', fileName: 'later', status: 'staged', category: 'Other', dateAdded: '', queueId: 'queue-a', queuePosition: 1 },
|
|
{ id: 'first', url: 'https://example.com/first', fileName: 'first', status: 'staged', category: 'Other', dateAdded: '', queueId: 'queue-a', queuePosition: 0 }
|
|
] as any[]
|
|
});
|
|
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string, args?: unknown) => {
|
|
if (command === 'get_pending_order') {
|
|
return [(args as { queueId: string }).queueId === 'queue-a' ? 'first' : 'later'];
|
|
}
|
|
return undefined;
|
|
});
|
|
|
|
expect(await useDownloadStore.getState().startQueue('queue-a')).toEqual(['first', 'later']);
|
|
const enqueuedIds = vi.mocked(ipc.invokeCommand).mock.calls
|
|
.filter(call => call[0] === 'enqueue_download')
|
|
.map(call => (call[1] as any).item.id);
|
|
expect(enqueuedIds).toEqual(['first', 'later']);
|
|
expect((vi.mocked(ipc.invokeCommand).mock.calls.find(call =>
|
|
call[0] === 'enqueue_download'
|
|
)?.[1] as any).item.queue_id).toBe('queue-a');
|
|
});
|
|
|
|
it('preserves extension request headers and cookies for the Add modal', async () => {
|
|
await useDownloadStore.getState().handleExtensionDownload({
|
|
urls: ['https://example.com/file.bin'],
|
|
referer: 'https://example.com/page',
|
|
silent: false,
|
|
filename: 'file.bin',
|
|
headers: 'X-Test: value',
|
|
cookies: 'session=secret'
|
|
});
|
|
|
|
const state = useDownloadStore.getState();
|
|
expect(state.isAddModalOpen).toBe(true);
|
|
expect(state.pendingAddUrls).toBe('https://example.com/file.bin');
|
|
expect(state.pendingAddReferer).toBe('https://example.com/page');
|
|
expect(state.pendingAddFilename).toBe('file.bin');
|
|
expect(state.pendingAddHeaders).toBe('X-Test: value');
|
|
expect(state.pendingAddCookies).toBe('session=secret');
|
|
});
|
|
|
|
it('routes silent extension captures to the Add Modal instead of queuing immediately', async () => {
|
|
await useDownloadStore.getState().handleExtensionDownload({
|
|
urls: ['https://example.com/downloads/report.pdf'],
|
|
referer: 'https://example.com/page',
|
|
silent: true,
|
|
filename: 'report.pdf',
|
|
headers: 'User-Agent: Test',
|
|
cookies: 'session=secret'
|
|
});
|
|
|
|
const state = useDownloadStore.getState();
|
|
expect(state.isAddModalOpen).toBe(true);
|
|
expect(state.pendingAddUrls).toBe('https://example.com/downloads/report.pdf');
|
|
expect(state.pendingAddReferer).toBe('https://example.com/page');
|
|
expect(state.pendingAddFilename).toBe('report.pdf');
|
|
expect(state.pendingAddHeaders).toBe('User-Agent: Test');
|
|
expect(state.pendingAddCookies).toBe('session=secret');
|
|
});
|
|
});
|