Replace the remaining ~155 untyped invoke() calls across stores, services, components, and routes with the generated commands.* wrappers from $lib/api/bindings, so every IPC call is compile-time-checked against the command signatures. - Register repository_get_subtitle_url and repository_get_video_download_url in specta_builder() and the invoke_handler; regenerate bindings.ts. - Source duplicated wire types (AutoplaySettings, CacheConfig, Session, ConnectivityStatus, audio/video settings, etc.) from bindings. - Fix two bugs surfaced by the typed wrappers: - VideoDownloadButton passed an un-awaited Promise as the stream URL. - setAutoplaySettings omitted the required userId argument. - Update unit tests asserting the old invoke(name, args) shape. - Remove the five param-naming guard tests; the compiler and codegen now enforce what they checked. svelte-check: 0 errors. vitest: green. cargo test --lib: green.
914 lines
26 KiB
TypeScript
914 lines
26 KiB
TypeScript
// Tests for downloads store
|
|
// TRACES: UR-011, UR-013, UR-018 | DR-015, DR-017 | UT-010, UT-024
|
|
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
import { get } from "svelte/store";
|
|
|
|
// Mock Tauri APIs
|
|
const mockInvoke = vi.fn();
|
|
const mockListen = vi.fn();
|
|
|
|
vi.mock("@tauri-apps/api/core", () => ({
|
|
invoke: mockInvoke,
|
|
}));
|
|
|
|
vi.mock("@tauri-apps/api/event", () => ({
|
|
listen: mockListen,
|
|
}));
|
|
|
|
describe("downloads store", () => {
|
|
let eventHandler: ((event: { payload: unknown }) => void) | null = null;
|
|
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
// Reset the invoke mock to clear any remaining queued return values
|
|
mockInvoke.mockReset();
|
|
|
|
// Capture the event handler when listen is called
|
|
mockListen.mockImplementation((_event: string, handler: (event: { payload: unknown }) => void) => {
|
|
eventHandler = handler;
|
|
return Promise.resolve(() => {});
|
|
});
|
|
});
|
|
|
|
afterEach(async () => {
|
|
// Clean up event listeners
|
|
const { cleanupDownloadEvents } = await import("./downloads");
|
|
cleanupDownloadEvents();
|
|
eventHandler = null;
|
|
|
|
// Clear all mocks
|
|
vi.clearAllMocks();
|
|
mockInvoke.mockReset();
|
|
mockListen.mockReset();
|
|
});
|
|
|
|
describe("initial state", () => {
|
|
it("should have empty downloads initially", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
const state = get(downloads);
|
|
|
|
expect(state.downloads).toEqual({});
|
|
expect(state.stats.activeCount).toBe(0);
|
|
expect(state.stats.queuedCount).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe("downloadItem", () => {
|
|
it("should call invoke with correct parameters", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
|
|
mockInvoke
|
|
.mockResolvedValueOnce(123) // download_item returns ID
|
|
.mockResolvedValueOnce({
|
|
downloads: [],
|
|
stats: {
|
|
total: 0,
|
|
activeCount: 0,
|
|
queuedCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
}); // get_downloads returns empty
|
|
|
|
const downloadId = await downloads.downloadItem(
|
|
"item-1",
|
|
"user-1",
|
|
"/path/to/file.mp3",
|
|
"audio/mpeg",
|
|
10
|
|
);
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("download_item", {
|
|
request: {
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/to/file.mp3",
|
|
mimeType: "audio/mpeg",
|
|
priority: 10,
|
|
itemName: null,
|
|
artistName: null,
|
|
albumName: null,
|
|
expectedSize: null,
|
|
},
|
|
});
|
|
expect(downloadId).toBe(123);
|
|
});
|
|
|
|
it("should refresh downloads after queuing", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
|
|
mockInvoke
|
|
.mockResolvedValueOnce(123)
|
|
.mockResolvedValueOnce({
|
|
downloads: [
|
|
{
|
|
id: 123,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/to/file.mp3",
|
|
status: "pending",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 10,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 1,
|
|
activeCount: 0,
|
|
queuedCount: 1,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
await downloads.downloadItem("item-1", "user-1", "/path/to/file.mp3");
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("get_downloads", {
|
|
userId: "user-1",
|
|
statusFilter: null,
|
|
});
|
|
|
|
const state = get(downloads);
|
|
expect(state.downloads[123]).toBeDefined();
|
|
expect(state.stats.queuedCount).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("downloadAlbum", () => {
|
|
it("should call invoke with correct parameters", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
|
|
mockInvoke
|
|
.mockResolvedValueOnce([1, 2, 3]) // download_album returns IDs
|
|
.mockResolvedValueOnce({
|
|
downloads: [],
|
|
stats: {
|
|
total: 0,
|
|
activeCount: 0,
|
|
queuedCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
}); // get_downloads
|
|
|
|
const ids = await downloads.downloadAlbum("album-1", "user-1", "/base/path");
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("download_album", {
|
|
albumId: "album-1",
|
|
userId: "user-1",
|
|
basePath: "/base/path",
|
|
});
|
|
expect(ids).toEqual([1, 2, 3]);
|
|
});
|
|
});
|
|
|
|
describe("pause/resume/cancel", () => {
|
|
it("should call pause_download with correct ID", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
mockInvoke.mockResolvedValueOnce(undefined);
|
|
|
|
await downloads.pause(123);
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("pause_download", { downloadId: 123 });
|
|
});
|
|
|
|
it("should call resume_download with correct ID", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
mockInvoke.mockResolvedValueOnce(undefined);
|
|
|
|
await downloads.resume(123);
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("resume_download", { downloadId: 123 });
|
|
});
|
|
|
|
it("should call cancel_download with correct ID", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
mockInvoke.mockResolvedValueOnce(undefined);
|
|
|
|
await downloads.cancel(123);
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("cancel_download", { downloadId: 123 });
|
|
});
|
|
});
|
|
|
|
describe("delete", () => {
|
|
it("should call delete_download and remove from store", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
|
|
// First add a download via refresh
|
|
mockInvoke.mockResolvedValueOnce({
|
|
downloads: [
|
|
{
|
|
id: 123,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/to/file.mp3",
|
|
status: "completed",
|
|
progress: 1.0,
|
|
bytesDownloaded: 1000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 1,
|
|
activeCount: 0,
|
|
queuedCount: 0,
|
|
completedCount: 1,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
expect(get(downloads).downloads[123]).toBeDefined();
|
|
|
|
// Now delete
|
|
mockInvoke.mockResolvedValueOnce(undefined);
|
|
await downloads.delete(123);
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("delete_download", { downloadId: 123 });
|
|
expect(get(downloads).downloads[123]).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("refresh", () => {
|
|
it("should update store with downloads from backend", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
|
|
mockInvoke.mockResolvedValueOnce({
|
|
downloads: [
|
|
{
|
|
id: 1,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/1.mp3",
|
|
status: "downloading",
|
|
progress: 0.5,
|
|
bytesDownloaded: 500,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 2,
|
|
itemId: "item-2",
|
|
userId: "user-1",
|
|
filePath: "/path/2.mp3",
|
|
status: "pending",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "2024-01-01T00:00:01Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 3,
|
|
itemId: "item-3",
|
|
userId: "user-1",
|
|
filePath: "/path/3.mp3",
|
|
status: "completed",
|
|
progress: 1.0,
|
|
bytesDownloaded: 1000,
|
|
queuedAt: "2024-01-01T00:00:02Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 3,
|
|
activeCount: 1,
|
|
queuedCount: 1,
|
|
completedCount: 1,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
const state = get(downloads);
|
|
expect(Object.keys(state.downloads).length).toBe(3);
|
|
expect(state.stats.activeCount).toBe(1); // 1 downloading
|
|
expect(state.stats.queuedCount).toBe(1); // 1 pending
|
|
});
|
|
|
|
it("should support status filter", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
|
|
mockInvoke.mockResolvedValueOnce({
|
|
downloads: [],
|
|
stats: {
|
|
total: 0,
|
|
activeCount: 0,
|
|
queuedCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
await downloads.refresh("user-1", ["pending", "downloading"]);
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("get_downloads", {
|
|
userId: "user-1",
|
|
statusFilter: ["pending", "downloading"],
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("event handling", () => {
|
|
it("should initialize event listener via initDownloadEvents", async () => {
|
|
const { initDownloadEvents } = await import("./downloads");
|
|
|
|
await initDownloadEvents();
|
|
|
|
expect(mockListen).toHaveBeenCalledWith("download-event", expect.any(Function));
|
|
expect(eventHandler).not.toBeNull();
|
|
});
|
|
|
|
it("should handle started event and refresh", async () => {
|
|
const { downloads, initDownloadEvents } = await import("./downloads");
|
|
|
|
// First add a pending download
|
|
mockInvoke.mockResolvedValueOnce({
|
|
downloads: [
|
|
{
|
|
id: 123,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/to/file.mp3",
|
|
status: "pending",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 1,
|
|
activeCount: 0,
|
|
queuedCount: 1,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
// Initialize event listener
|
|
await initDownloadEvents();
|
|
expect(eventHandler).not.toBeNull();
|
|
|
|
// Mock refresh call that will happen when event is handled
|
|
mockInvoke.mockResolvedValueOnce({
|
|
downloads: [
|
|
{
|
|
id: 123,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/to/file.mp3",
|
|
status: "downloading",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 1,
|
|
activeCount: 1,
|
|
queuedCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
// Simulate started event
|
|
eventHandler!({
|
|
payload: {
|
|
type: "started",
|
|
downloadId: 123,
|
|
itemId: "item-1",
|
|
},
|
|
});
|
|
|
|
// Wait for async operations to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const state = get(downloads);
|
|
expect(state.downloads[123].status).toBe("downloading");
|
|
});
|
|
|
|
it("should handle completed event and refresh", async () => {
|
|
const { downloads, initDownloadEvents } = await import("./downloads");
|
|
|
|
mockInvoke.mockResolvedValueOnce({
|
|
downloads: [
|
|
{
|
|
id: 123,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/to/file.mp3",
|
|
status: "downloading",
|
|
progress: 0.99,
|
|
bytesDownloaded: 990000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 1,
|
|
activeCount: 1,
|
|
queuedCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
// Initialize event listener
|
|
await initDownloadEvents();
|
|
|
|
// Mock the mark_download_completed invoke call
|
|
mockInvoke.mockResolvedValueOnce(undefined);
|
|
|
|
// Simulate completed event
|
|
eventHandler!({
|
|
payload: {
|
|
type: "completed",
|
|
downloadId: 123,
|
|
itemId: "item-1",
|
|
filePath: "/path/to/file.mp3",
|
|
totalBytes: 1000000,
|
|
},
|
|
});
|
|
|
|
// Wait for async operations to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const state = get(downloads);
|
|
expect(state.downloads[123].status).toBe("completed");
|
|
});
|
|
|
|
it("should handle failed event and refresh", async () => {
|
|
const { downloads, initDownloadEvents } = await import("./downloads");
|
|
|
|
// Setup mock to return downloading download when refresh is called
|
|
mockInvoke.mockImplementation((command: string) => {
|
|
if (command === "get_downloads") {
|
|
return Promise.resolve({
|
|
downloads: [
|
|
{
|
|
id: 123,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/to/file.mp3",
|
|
status: "downloading",
|
|
progress: 0.5,
|
|
bytesDownloaded: 500000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 1,
|
|
activeCount: 1,
|
|
queuedCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
} else if (command === "mark_download_failed") {
|
|
return Promise.resolve(undefined);
|
|
}
|
|
return Promise.reject(new Error(`Unexpected command: ${command}`));
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
// Initialize event listener
|
|
await initDownloadEvents();
|
|
|
|
// Simulate failed event
|
|
eventHandler!({
|
|
payload: {
|
|
type: "failed",
|
|
downloadId: 123,
|
|
itemId: "item-1",
|
|
error: "Network timeout",
|
|
},
|
|
});
|
|
|
|
// Wait for async operations to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const state = get(downloads);
|
|
expect(state.downloads[123].status).toBe("failed");
|
|
expect(state.downloads[123].errorMessage).toBe("Network timeout");
|
|
});
|
|
|
|
it("should handle cancelled event and refresh", async () => {
|
|
const { downloads, initDownloadEvents } = await import("./downloads");
|
|
|
|
mockInvoke.mockResolvedValueOnce({
|
|
downloads: [
|
|
{
|
|
id: 123,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/to/file.mp3",
|
|
status: "downloading",
|
|
progress: 0.5,
|
|
bytesDownloaded: 500000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 1,
|
|
activeCount: 1,
|
|
queuedCount: 0,
|
|
completedCount: 0,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
// Initialize event listener
|
|
await initDownloadEvents();
|
|
|
|
// Simulate cancelled event
|
|
// Note: The cancelled event handler does NOT call refresh, it only removes from store
|
|
eventHandler!({
|
|
payload: {
|
|
type: "cancelled",
|
|
downloadId: 123,
|
|
itemId: "item-1",
|
|
},
|
|
});
|
|
|
|
// Wait for async operations to complete
|
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
|
|
const state = get(downloads);
|
|
expect(state.downloads[123]).toBeUndefined();
|
|
});
|
|
});
|
|
|
|
describe("derived stores", () => {
|
|
it("activeDownloads should filter downloading status", async () => {
|
|
const { downloads, activeDownloads } = await import("./downloads");
|
|
|
|
mockInvoke.mockImplementation((command: string) => {
|
|
if (command === "get_downloads") {
|
|
return Promise.resolve({
|
|
downloads: [
|
|
{
|
|
id: 1,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/1.mp3",
|
|
status: "downloading",
|
|
progress: 0.5,
|
|
bytesDownloaded: 500,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 2,
|
|
itemId: "item-2",
|
|
userId: "user-1",
|
|
filePath: "/path/2.mp3",
|
|
status: "completed",
|
|
progress: 1.0,
|
|
bytesDownloaded: 1000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 3,
|
|
itemId: "item-3",
|
|
userId: "user-1",
|
|
filePath: "/path/3.mp3",
|
|
status: "pending",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 3,
|
|
activeCount: 1,
|
|
queuedCount: 1,
|
|
completedCount: 1,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
}
|
|
return Promise.reject(new Error(`Unexpected command: ${command}`));
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
const active = get(activeDownloads);
|
|
expect(active.length).toBe(1);
|
|
expect(active[0].id).toBe(1);
|
|
expect(active[0].status).toBe("downloading");
|
|
});
|
|
|
|
it("completedDownloads should filter completed status", async () => {
|
|
const { downloads, completedDownloads } = await import("./downloads");
|
|
|
|
mockInvoke.mockImplementation((command: string) => {
|
|
if (command === "get_downloads") {
|
|
return Promise.resolve({
|
|
downloads: [
|
|
{
|
|
id: 1,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/1.mp3",
|
|
status: "downloading",
|
|
progress: 0.5,
|
|
bytesDownloaded: 500,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 2,
|
|
itemId: "item-2",
|
|
userId: "user-1",
|
|
filePath: "/path/2.mp3",
|
|
status: "completed",
|
|
progress: 1.0,
|
|
bytesDownloaded: 1000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 3,
|
|
itemId: "item-3",
|
|
userId: "user-1",
|
|
filePath: "/path/3.mp3",
|
|
status: "completed",
|
|
progress: 1.0,
|
|
bytesDownloaded: 1000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 3,
|
|
activeCount: 1,
|
|
queuedCount: 0,
|
|
completedCount: 2,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
}
|
|
return Promise.reject(new Error(`Unexpected command: ${command}`));
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
const completed = get(completedDownloads);
|
|
expect(completed.length).toBe(2);
|
|
expect(completed.every((d) => d.status === "completed")).toBe(true);
|
|
});
|
|
|
|
it("pendingDownloads should filter pending status", async () => {
|
|
const { downloads, pendingDownloads } = await import("./downloads");
|
|
|
|
mockInvoke.mockImplementation((command: string) => {
|
|
if (command === "get_downloads") {
|
|
return Promise.resolve({
|
|
downloads: [
|
|
{
|
|
id: 1,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/1.mp3",
|
|
status: "pending",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 10,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 2,
|
|
itemId: "item-2",
|
|
userId: "user-1",
|
|
filePath: "/path/2.mp3",
|
|
status: "completed",
|
|
progress: 1.0,
|
|
bytesDownloaded: 1000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 3,
|
|
itemId: "item-3",
|
|
userId: "user-1",
|
|
filePath: "/path/3.mp3",
|
|
status: "pending",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 5,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 3,
|
|
activeCount: 0,
|
|
queuedCount: 2,
|
|
completedCount: 1,
|
|
failedCount: 0,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
}
|
|
return Promise.reject(new Error(`Unexpected command: ${command}`));
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
const pending = get(pendingDownloads);
|
|
expect(pending.length).toBe(2);
|
|
expect(pending.every((d) => d.status === "pending")).toBe(true);
|
|
});
|
|
|
|
it("failedDownloads should filter failed status", async () => {
|
|
const { downloads, failedDownloads } = await import("./downloads");
|
|
|
|
mockInvoke.mockImplementation((command: string) => {
|
|
if (command === "get_downloads") {
|
|
return Promise.resolve({
|
|
downloads: [
|
|
{
|
|
id: 1,
|
|
itemId: "item-1",
|
|
userId: "user-1",
|
|
filePath: "/path/1.mp3",
|
|
status: "failed",
|
|
progress: 0.5,
|
|
bytesDownloaded: 500,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 3,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
errorMessage: "Network error",
|
|
},
|
|
{
|
|
id: 2,
|
|
itemId: "item-2",
|
|
userId: "user-1",
|
|
filePath: "/path/2.mp3",
|
|
status: "completed",
|
|
progress: 1.0,
|
|
bytesDownloaded: 1000,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 3,
|
|
itemId: "item-3",
|
|
userId: "user-1",
|
|
filePath: "/path/3.mp3",
|
|
status: "failed",
|
|
progress: 0.3,
|
|
bytesDownloaded: 300,
|
|
queuedAt: "2024-01-01T00:00:00Z",
|
|
retryCount: 2,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
errorMessage: "Timeout",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 3,
|
|
activeCount: 0,
|
|
queuedCount: 0,
|
|
completedCount: 1,
|
|
failedCount: 2,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
}
|
|
return Promise.reject(new Error(`Unexpected command: ${command}`));
|
|
});
|
|
|
|
await downloads.refresh("user-1");
|
|
|
|
const failed = get(failedDownloads);
|
|
expect(failed.length).toBe(2);
|
|
expect(failed.every((d) => d.status === "failed")).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe("error handling", () => {
|
|
it("should throw error when download_item fails", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
|
|
mockInvoke.mockImplementation((command: string) => {
|
|
if (command === "download_item") {
|
|
return Promise.reject(new Error("Backend error: failed to queue download"));
|
|
}
|
|
return Promise.reject(new Error(`Unexpected command: ${command}`));
|
|
});
|
|
|
|
await expect(
|
|
downloads.downloadItem("item-1", "user-1", "/path/to/file.mp3")
|
|
).rejects.toThrow("Backend error: failed to queue download");
|
|
});
|
|
|
|
it("should throw error when refresh fails", async () => {
|
|
const { downloads } = await import("./downloads");
|
|
|
|
mockInvoke.mockImplementation((command: string) => {
|
|
if (command === "get_downloads") {
|
|
return Promise.reject(new Error("Backend error: failed to fetch downloads"));
|
|
}
|
|
return Promise.reject(new Error(`Unexpected command: ${command}`));
|
|
});
|
|
|
|
await expect(
|
|
downloads.refresh("user-1")
|
|
).rejects.toThrow("Backend error: failed to fetch downloads");
|
|
});
|
|
});
|
|
});
|