@@ -0,0 +1,122 @@
|
||||
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<TournamentsResultDTO>>()
|
||||
);
|
||||
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();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user