import { ApiError } from '$lib/api/client'; import type { EventDTO, TournamentsResultDTO } from '$lib/api/schema-helpers'; import { beforeEach, describe, expect, it, vi } from 'vitest'; /* * `loadEventResults` exists to contain the blast radius of one bad event, so what * matters here is the failure behaviour: a broken event must not take the others * with it, while an abort or a 401 must stop everything. */ // Typed so the assertions below read the call arguments without falling back to `any`. const getResults = vi.hoisted(() => vi.fn<(ids: number[], options?: { signal?: AbortSignal }) => Promise>() ); vi.mock('$lib/api/tournaments', () => ({ getResults })); const { loadEventResults } = await import('./load'); function events(count: number): EventDTO[] { return Array.from({ length: count }, (_, i) => ({ id: i + 1, name: `Event ${i + 1}`, date: undefined })); } beforeEach(() => { getResults.mockReset(); }); describe('loadEventResults', () => { it('requests one event at a time and returns them all', async () => { getResults.mockImplementation((ids: number[]) => Promise.resolve({ results: [{ player: `P${ids[0]}` }] }) ); const outcome = await loadEventResults(events(3)); expect(outcome.loaded).toHaveLength(3); expect(outcome.failed).toEqual([]); // One id per call — batching is what this module exists to avoid. expect(getResults.mock.calls.map((c) => c[0])).toEqual([[1], [2], [3]]); }); it('skips events with no id rather than requesting undefined', async () => { getResults.mockResolvedValue({ results: [] }); await loadEventResults([{ id: 1, name: 'A' }, { name: 'B' }]); expect(getResults).toHaveBeenCalledTimes(1); }); it('isolates a failing event and reports it, keeping the rest', async () => { getResults.mockImplementation((ids: number[]) => ids[0] === 2 ? Promise.reject(new ApiError(500, 'Bracket has no rank-1 row')) : Promise.resolve({ results: [] }) ); const outcome = await loadEventResults(events(3)); expect(outcome.loaded).toHaveLength(2); expect(outcome.failed).toHaveLength(1); expect(outcome.failed[0].event.id).toBe(2); expect(outcome.failed[0].message).toBe('Bracket has no rank-1 row'); }); it('stops everything on a 401 — retrying N times would just burn requests', async () => { getResults.mockRejectedValue(new ApiError(401, 'Unauthorized')); await expect(loadEventResults(events(3))).rejects.toThrow(ApiError); }); it('propagates an abort rather than recording it as a failed event', async () => { getResults.mockRejectedValue(new DOMException('Aborted', 'AbortError')); await expect(loadEventResults(events(2))).rejects.toThrow(DOMException); }); it('makes no request at all when the signal is already aborted', async () => { const controller = new AbortController(); controller.abort(); const outcome = await loadEventResults(events(3), { signal: controller.signal }); expect(getResults).not.toHaveBeenCalled(); expect(outcome.loaded).toEqual([]); }); it('reports progress once per settled event, failures included', async () => { getResults.mockImplementation((ids: number[]) => ids[0] === 2 ? Promise.reject(new ApiError(500, 'boom')) : Promise.resolve({ results: [] }) ); const seen: number[] = []; await loadEventResults(events(3), { onProgress: (done, total) => { expect(total).toBe(3); seen.push(done); } }); expect(seen).toEqual([1, 2, 3]); }); it('never runs more requests at once than the concurrency allows', async () => { let inFlight = 0; let peak = 0; getResults.mockImplementation(async () => { inFlight++; peak = Math.max(peak, inFlight); await Promise.resolve(); inFlight--; return { results: [] }; }); await loadEventResults(events(10), { concurrency: 3 }); expect(peak).toBeLessThanOrEqual(3); expect(getResults).toHaveBeenCalledTimes(10); }); it('handles an empty scope without spawning workers', async () => { const outcome = await loadEventResults([]); expect(outcome).toEqual({ loaded: [], failed: [] }); expect(getResults).not.toHaveBeenCalled(); }); });