import { afterEach, describe, expect, it, vi } from 'vitest'; import type { AssetRef } from '@tts/shared'; import { downloadAll, downloadAsset, guessMimeType } from './download.js'; const ref = (url: string): AssetRef => ({ kind: 'image', url, ownerGuid: 'g1', }); function blobResponse(): Response { return new Response(new Blob(['data']), { status: 200 }); } afterEach(() => { vi.unstubAllGlobals(); }); describe('guessMimeType', () => { it('maps common image extensions', () => { expect(guessMimeType('https://example.com/a.png')).toBe('image/png'); expect(guessMimeType('https://example.com/a.jpg')).toBe('image/jpeg'); expect(guessMimeType('https://example.com/a.jpeg')).toBe('image/jpeg'); expect(guessMimeType('https://example.com/a.gif')).toBe('image/gif'); expect(guessMimeType('https://example.com/a.webp')).toBe('image/webp'); expect(guessMimeType('https://example.com/a.pdf')).toBe('application/pdf'); }); it('ignores query strings when reading the extension', () => { expect(guessMimeType('https://example.com/a.png?v=2')).toBe('image/png'); }); it('falls back to octet-stream for unknown extensions', () => { expect(guessMimeType('https://example.com/a.xyz')).toBe( 'application/octet-stream', ); expect(guessMimeType('https://example.com/noext')).toBe( 'application/octet-stream', ); }); }); describe('downloadAsset', () => { it('returns a blob on success', async () => { vi.stubGlobal('fetch', vi.fn().mockResolvedValue(blobResponse())); const blob = await downloadAsset('https://example.com/a.png'); expect(blob).toBeInstanceOf(Blob); }); it('throws on a non-OK response', async () => { vi.stubGlobal( 'fetch', vi.fn().mockResolvedValue(new Response(null, { status: 404 })), ); await expect(downloadAsset('https://example.com/a.png')).rejects.toThrow( 'Failed to download asset (404)', ); }); }); describe('downloadAll', () => { it('downloads every ref and reports progress', async () => { vi.stubGlobal('fetch', vi.fn().mockImplementation(() => blobResponse())); const refs = [ref('https://example.com/1.png'), ref('https://example.com/2.png')]; const progress: number[] = []; const results = await downloadAll(refs, { concurrency: 2, onProgress: (p) => progress.push(p.done), }); expect(results).toHaveLength(2); expect(progress).toEqual([1, 2]); }); it('respects the concurrency limit', async () => { let inFlight = 0; let maxInFlight = 0; const fetchMock = vi.fn().mockImplementation(async () => { inFlight += 1; maxInFlight = Math.max(maxInFlight, inFlight); await new Promise((r) => setTimeout(r, 5)); inFlight -= 1; return blobResponse(); }); vi.stubGlobal('fetch', fetchMock); const refs = Array.from({ length: 6 }, (_, i) => ref(`https://example.com/${i}.png`), ); await downloadAll(refs, { concurrency: 2 }); expect(maxInFlight).toBeLessThanOrEqual(2); }); });