Some checks failed
CI / Unit & Component Tests (pull_request) Failing after 4m5s
CI / OCR Service Tests (pull_request) Successful in 38s
CI / Backend Unit Tests (pull_request) Failing after 3m22s
CI / Unit & Component Tests (push) Failing after 3m57s
CI / OCR Service Tests (push) Successful in 37s
CI / Backend Unit Tests (push) Failing after 3m22s
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
156 lines
5.5 KiB
TypeScript
156 lines
5.5 KiB
TypeScript
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
import { flushSync } from 'svelte';
|
|
|
|
const { createTimelineDrag } = await import('./useTimelineDrag.svelte');
|
|
|
|
type EmitCall = { start: number; end: number; includeZoom: boolean };
|
|
|
|
function makeOpts(overrides: Partial<Parameters<typeof createTimelineDrag>[0]> = {}) {
|
|
const labels = ['1915-08', '1915-09', '1915-10', '1915']; // last entry is a year label
|
|
const calls: EmitCall[] = [];
|
|
const opts = {
|
|
indexFromClientX: vi.fn(() => 0),
|
|
labelAt: (i: number) => labels[i],
|
|
isYearLabel: (l: string) => l.length === 4,
|
|
emit: (start: number, end: number, includeZoom: boolean) => {
|
|
calls.push({ start, end, includeZoom });
|
|
},
|
|
...overrides
|
|
};
|
|
return { opts, calls };
|
|
}
|
|
|
|
function pointerDownEvent(button = 0): PointerEvent {
|
|
return { button } as unknown as PointerEvent;
|
|
}
|
|
|
|
describe('createTimelineDrag', () => {
|
|
beforeEach(() => {
|
|
// Reset listeners attached to the JSDOM document between tests
|
|
document.removeEventListener('pointermove', () => undefined);
|
|
});
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
});
|
|
|
|
it('starts with no drag in progress and null low/high indices', () => {
|
|
const { opts } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
expect(drag.isDragging).toBe(false);
|
|
expect(drag.lowIndex).toBeNull();
|
|
expect(drag.highIndex).toBeNull();
|
|
});
|
|
|
|
it('pointerDown on a primary button enters drag state at that index', () => {
|
|
const { opts } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(0), 1);
|
|
flushSync();
|
|
expect(drag.isDragging).toBe(true);
|
|
expect(drag.lowIndex).toBe(1);
|
|
expect(drag.highIndex).toBe(1);
|
|
});
|
|
|
|
it('pointerDown on a non-primary button is ignored', () => {
|
|
const { opts } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(2), 1);
|
|
flushSync();
|
|
expect(drag.isDragging).toBe(false);
|
|
});
|
|
|
|
it('pointerEnter during drag widens the range high boundary', () => {
|
|
const { opts } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(0), 0);
|
|
drag.pointerEnter(2);
|
|
flushSync();
|
|
expect(drag.lowIndex).toBe(0);
|
|
expect(drag.highIndex).toBe(2);
|
|
});
|
|
|
|
it('pointerEnter outside drag is a no-op', () => {
|
|
const { opts } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerEnter(2);
|
|
flushSync();
|
|
expect(drag.isDragging).toBe(false);
|
|
expect(drag.lowIndex).toBeNull();
|
|
});
|
|
|
|
it('click on a month bar emits filter only (no zoom)', () => {
|
|
const { opts, calls } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
drag.click(0);
|
|
expect(calls).toEqual([{ start: 0, end: 0, includeZoom: false }]);
|
|
});
|
|
|
|
it('click on a year bar emits filter + zoom atomically', () => {
|
|
const { opts, calls } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
drag.click(3); // labels[3] = '1915' (year label)
|
|
expect(calls).toEqual([{ start: 3, end: 3, includeZoom: true }]);
|
|
});
|
|
|
|
it('range drag commits emit with zoom in ascending order', async () => {
|
|
const { opts, calls } = makeOpts({ indexFromClientX: vi.fn(() => 2) });
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(0), 0);
|
|
document.dispatchEvent(new PointerEvent('pointermove', { clientX: 999 }));
|
|
document.dispatchEvent(new PointerEvent('pointerup'));
|
|
flushSync();
|
|
expect(calls).toEqual([{ start: 0, end: 2, includeZoom: true }]);
|
|
expect(drag.isDragging).toBe(false);
|
|
});
|
|
|
|
it('reverse-direction drag still emits ascending boundaries via emit ordering', async () => {
|
|
const { opts, calls } = makeOpts({ indexFromClientX: vi.fn(() => 0) });
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(0), 2);
|
|
document.dispatchEvent(new PointerEvent('pointermove', { clientX: 0 }));
|
|
document.dispatchEvent(new PointerEvent('pointerup'));
|
|
flushSync();
|
|
// emit receives raw start/end — orchestrator's emit() handles ordering
|
|
expect(calls).toEqual([{ start: 2, end: 0, includeZoom: true }]);
|
|
});
|
|
|
|
it('pointerup on the same bar (no-range) on a month label emits filter without zoom', () => {
|
|
const { opts, calls } = makeOpts({ indexFromClientX: vi.fn(() => 0) });
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(0), 0);
|
|
document.dispatchEvent(new PointerEvent('pointerup'));
|
|
flushSync();
|
|
expect(calls).toEqual([{ start: 0, end: 0, includeZoom: false }]);
|
|
});
|
|
|
|
it('pointercancel resets state without emitting', () => {
|
|
const { opts, calls } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(0), 1);
|
|
document.dispatchEvent(new PointerEvent('pointercancel'));
|
|
flushSync();
|
|
expect(calls).toEqual([]);
|
|
expect(drag.isDragging).toBe(false);
|
|
});
|
|
|
|
it('click after pointerup is suppressed (no double emit)', async () => {
|
|
const { opts, calls } = makeOpts({ indexFromClientX: vi.fn(() => 0) });
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(0), 0);
|
|
document.dispatchEvent(new PointerEvent('pointerup'));
|
|
flushSync();
|
|
drag.click(0); // synthesized click after pointerup
|
|
expect(calls.length).toBe(1); // only the pointerup-driven emit, not the click
|
|
});
|
|
|
|
it('cleanup removes document pointer listeners', () => {
|
|
const removeSpy = vi.spyOn(document, 'removeEventListener');
|
|
const { opts } = makeOpts();
|
|
const drag = createTimelineDrag(opts);
|
|
drag.pointerDown(pointerDownEvent(0), 0);
|
|
drag.cleanup();
|
|
const removed = removeSpy.mock.calls.map((c) => c[0]);
|
|
expect(removed).toEqual(expect.arrayContaining(['pointermove', 'pointerup', 'pointercancel']));
|
|
});
|
|
});
|