Files
dockhand-upstream/tests/windowed-list.test.ts
T

193 lines
7.4 KiB
TypeScript

import { describe, test, expect } from 'bun:test';
import { windowOffsetFor, WindowedListCore, type WindowState } from '../src/lib/utils/windowed-list.svelte';
describe('windowOffsetFor', () => {
test('centers the window on the target', () => {
expect(windowOffsetFor(500, 400)).toBe(300); // 500 - 200
expect(windowOffsetFor(1000, 100)).toBe(950); // 1000 - 50
});
test('clamps to 0 near the top', () => {
expect(windowOffsetFor(0, 400)).toBe(0);
expect(windowOffsetFor(50, 400)).toBe(0);
});
test('odd window sizes floor the half', () => {
expect(windowOffsetFor(500, 401)).toBe(300);
});
});
function deferred<T>() {
let resolve!: (v: T) => void;
const promise = new Promise<T>((r) => (resolve = r));
return { promise, resolve };
}
describe('WindowedListCore', () => {
test('reset() fetches offset 0 at the window size and emits loaded state', async () => {
const calls: { offset: number; limit: number }[] = [];
let last: WindowState<number> | null = null;
const core = new WindowedListCore<number>(
{ windowSize: 400, fetchPage: async (offset, limit) => { calls.push({ offset, limit }); return { items: [1, 2], total: 2 }; } },
(s) => (last = s)
);
await core.reset();
expect(calls).toEqual([{ offset: 0, limit: 400 }]);
expect(last!.items).toEqual([1, 2]);
expect(last!.total).toBe(2);
expect(last!.offset).toBe(0);
expect(last!.loaded).toBe(true);
expect(last!.loading).toBe(false);
});
test('shiftTo() centers the requested offset', async () => {
const calls: number[] = [];
const core = new WindowedListCore<number>(
{ windowSize: 400, fetchPage: async (offset) => { calls.push(offset); return { items: [], total: 1000 }; } }
);
await core.shiftTo(600);
expect(calls).toEqual([400]); // 600 - 200
});
test('sets loading=true while a fetch is in flight', async () => {
const d = deferred<{ items: number[]; total: number }>();
const states: boolean[] = [];
const core = new WindowedListCore<number>(
{ windowSize: 100, fetchPage: async () => d.promise },
(s) => states.push(s.loading)
);
const p = core.reset();
expect(states.at(-1)).toBe(true); // emitted loading=true synchronously
d.resolve({ items: [1], total: 1 });
await p;
expect(states.at(-1)).toBe(false);
});
test('a superseded fetch is aborted and its late result is discarded', async () => {
const first = deferred<{ items: number[]; total: number }>();
const second = deferred<{ items: number[]; total: number }>();
const signals: AbortSignal[] = [];
let n = 0;
let last: WindowState<number> | null = null;
const core = new WindowedListCore<number>(
{ windowSize: 100, fetchPage: async (_o, _l, signal) => { signals.push(signal); return n++ === 0 ? first.promise : second.promise; } },
(s) => (last = s)
);
const p1 = core.shiftTo(0); // fetch #1
const p2 = core.shiftTo(500); // supersedes #1
expect(signals[0].aborted).toBe(true); // #1 aborted by #2
expect(signals[1].aborted).toBe(false);
first.resolve({ items: [1, 2, 3], total: 3 }); // stale — must be ignored
second.resolve({ items: [9], total: 1000 });
await Promise.all([p1, p2]);
// Only the winning (second) result is applied.
expect(last!.items).toEqual([9]);
expect(last!.total).toBe(1000);
});
test('a repeat shiftTo to the same in-flight offset is deduped (fling guard)', async () => {
const d = deferred<{ items: number[]; total: number }>();
let fetchCount = 0;
const core = new WindowedListCore<number>(
{ windowSize: 400, fetchPage: async () => { fetchCount++; return d.promise; } }
);
const p1 = core.shiftTo(600); // desired offset 400
const p2 = core.shiftTo(600); // same offset while #1 is loading → skipped
expect(fetchCount).toBe(1);
d.resolve({ items: [1], total: 1000 });
await Promise.all([p1, p2]);
});
test('reset() forces a refetch even while an identical offset-0 fetch is in flight', async () => {
// Regression: a filter/sort change maps to offset 0 like the in-flight load,
// so the fling-dedupe used to drop it, leaving stale data. reset() must force.
const first = deferred<{ items: number[]; total: number }>();
const second = deferred<{ items: number[]; total: number }>();
const signals: AbortSignal[] = [];
let n = 0;
let last: WindowState<number> | null = null;
const core = new WindowedListCore<number>(
{ windowSize: 400, fetchPage: async (_o, _l, s) => { signals.push(s); return n++ === 0 ? first.promise : second.promise; } },
(s) => (last = s)
);
const p1 = core.reset(); // initial load, offset 0, in flight
const p2 = core.reset(); // filter changed → same offset 0, must NOT be deduped
expect(n).toBe(2); // second fetch actually started
expect(signals[0].aborted).toBe(true); // first was aborted
first.resolve({ items: [1, 2], total: 2 }); // stale — must be ignored
second.resolve({ items: [9], total: 1 }); // new query result wins
await Promise.all([p1, p2]);
expect(last!.items).toEqual([9]);
expect(last!.total).toBe(1);
});
test('a non-forced shiftTo to the same in-flight offset is still deduped (fling)', async () => {
const d = deferred<{ items: number[]; total: number }>();
let fetchCount = 0;
const core = new WindowedListCore<number>(
{ windowSize: 400, fetchPage: async () => { fetchCount++; return d.promise; } }
);
const p1 = core.shiftTo(600);
const p2 = core.shiftTo(600); // same offset, not forced → deduped
expect(fetchCount).toBe(1);
d.resolve({ items: [1], total: 1000 });
await Promise.all([p1, p2]);
});
test('shiftTo to a different offset still supersedes the in-flight fetch', async () => {
const first = deferred<{ items: number[]; total: number }>();
const second = deferred<{ items: number[]; total: number }>();
let n = 0;
const core = new WindowedListCore<number>(
{ windowSize: 400, fetchPage: async () => (n++ === 0 ? first.promise : second.promise) }
);
const p1 = core.shiftTo(600); // offset 400
const p2 = core.shiftTo(2000); // offset 1800 — different, must NOT be deduped
expect(n).toBe(2);
first.resolve({ items: [], total: 0 });
second.resolve({ items: [], total: 0 });
await Promise.all([p1, p2]);
});
test('fetchPage error is routed to onError, not thrown', async () => {
let captured: unknown = null;
const core = new WindowedListCore<number>(
{ windowSize: 100, onError: (e) => (captured = e), fetchPage: async () => { throw new Error('boom'); } }
);
await core.reset();
expect((captured as Error)?.message).toBe('boom');
});
test('AbortError is swallowed (not routed to onError)', async () => {
let captured: unknown = null;
const core = new WindowedListCore<number>(
{ windowSize: 100, onError: (e) => (captured = e), fetchPage: async () => { const err = new Error('x'); err.name = 'AbortError'; throw err; } }
);
await core.reset();
expect(captured).toBeNull();
});
test('clear() aborts in-flight and resets state', async () => {
const d = deferred<{ items: number[]; total: number }>();
const signals: AbortSignal[] = [];
let last: WindowState<number> | null = null;
const core = new WindowedListCore<number>(
{ windowSize: 100, fetchPage: async (_o, _l, s) => { signals.push(s); return d.promise; } },
(s) => (last = s)
);
const p = core.shiftTo(300);
core.clear();
expect(signals[0].aborted).toBe(true);
expect(last!.items).toEqual([]);
expect(last!.total).toBe(0);
expect(last!.loading).toBe(false);
d.resolve({ items: [1], total: 1 }); // late resolve must not reapply
await p;
expect(last!.items).toEqual([]);
});
});