Formatting was configured but never enforced: `bun run format:check` reported 199 unformatted files and ran in no workflow and in no git hook, so .prettierrc (printWidth 100, trailing commas) described an intention rather than the tree. This is the one-time sweep that makes the check gateable. Whitespace and token-reflow only -- no behavioural change: `bun run check` reports 0 errors and all 1053 frontend tests pass before and after. Kept out of every other commit on purpose. A 199-file diff mixed with real changes is unreviewable, and the next commit turns format:check into a hard CI gate so this cannot silently accumulate again.
1002 lines
29 KiB
TypeScript
1002 lines
29 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("handle-1", "album-1", "user-1", "/base/path");
|
|
|
|
expect(mockInvoke).toHaveBeenCalledWith("download_album", {
|
|
handle: "handle-1",
|
|
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
|
|
});
|
|
|
|
// The Transfers view shows only in-flight rows; a completed transfer must
|
|
// NOT appear there (it lives in Downloaded). Mirrors the /downloads page's
|
|
// `transfers` derivation: active + pending + failed.
|
|
// TRACES: UR-055 | DR-084 | UT-052
|
|
it("transfers set excludes completed downloads", async () => {
|
|
const { downloads, activeDownloads, pendingDownloads, failedDownloads } =
|
|
await import("./downloads");
|
|
|
|
mockInvoke.mockResolvedValueOnce({
|
|
downloads: [
|
|
{
|
|
id: 1,
|
|
itemId: "a",
|
|
userId: "u",
|
|
filePath: "/a",
|
|
status: "downloading",
|
|
progress: 0.5,
|
|
bytesDownloaded: 5,
|
|
queuedAt: "t",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 2,
|
|
itemId: "b",
|
|
userId: "u",
|
|
filePath: "/b",
|
|
status: "pending",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "t",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 3,
|
|
itemId: "c",
|
|
userId: "u",
|
|
filePath: "/c",
|
|
status: "completed",
|
|
progress: 1,
|
|
bytesDownloaded: 9,
|
|
queuedAt: "t",
|
|
retryCount: 0,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
{
|
|
id: 4,
|
|
itemId: "d",
|
|
userId: "u",
|
|
filePath: "/d",
|
|
status: "failed",
|
|
progress: 0,
|
|
bytesDownloaded: 0,
|
|
queuedAt: "t",
|
|
retryCount: 1,
|
|
priority: 0,
|
|
mediaType: "audio",
|
|
downloadSource: "user",
|
|
},
|
|
],
|
|
stats: {
|
|
total: 4,
|
|
activeCount: 1,
|
|
queuedCount: 1,
|
|
completedCount: 1,
|
|
failedCount: 1,
|
|
pausedCount: 0,
|
|
},
|
|
});
|
|
|
|
await downloads.refresh("u");
|
|
|
|
const transfers = get(activeDownloads)
|
|
.concat(get(pendingDownloads))
|
|
.concat(get(failedDownloads));
|
|
const ids = transfers.map((d) => d.id).sort();
|
|
expect(ids).toEqual([1, 2, 4]);
|
|
expect(transfers.some((d) => d.status === "completed")).toBe(false);
|
|
});
|
|
|
|
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",
|
|
);
|
|
});
|
|
});
|
|
});
|