import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { setActivePinia, createPinia } from 'pinia' // Mock the decode-gate so the cascade is paced purely by the store's cadence // in tests (no real load events in happy-dom). Individual tests can // override per-call to exercise the gating itself. vi.mock('../src/utils/preloadImage.js', () => ({ preloadImage: vi.fn(() => Promise.resolve()), })) import { preloadImage } from '../src/utils/preloadImage.js' import { useShowcaseStore } from '../src/stores/showcase.js' let nextId function stubShowcase({ empty = false } = {}) { globalThis.fetch = vi.fn(async () => { const images = empty ? [] : Array.from({ length: 3 }, () => ({ id: nextId++, sha256: 's', mime: 'image/jpeg', width: 1, height: 1, thumbnail_url: '/t', })) return { ok: true, status: 200, statusText: '200', text: async () => JSON.stringify({ images }), } }) } describe('showcase store: buffered cascade', () => { beforeEach(() => { setActivePinia(createPinia()) nextId = 1 preloadImage.mockReset() preloadImage.mockImplementation(() => Promise.resolve()) vi.useFakeTimers() }) afterEach(() => { vi.useRealTimers() vi.restoreAllMocks() }) it('drains buffered items in fire-order at a fixed cadence, no dupes', async () => { stubShowcase() const s = useShowcaseStore() const p = s.loadInitial() // Past INITIAL_COUNT (60) × CADENCE (160ms = 9.6s) plus fetch microtasks. await vi.advanceTimersByTimeAsync(12000) await p expect(s.images.length).toBe(60) const ids = s.images.map((i) => i.id) expect(new Set(ids).size).toBe(60) // dedup held expect(ids).toEqual([...ids].sort((a, b) => a - b)) // shown in fire order }) it('reveals at most one item per cadence tick (decoupled from fetch)', async () => { stubShowcase() const s = useShowcaseStore() const p = s.loadInitial() await vi.advanceTimersByTimeAsync(1000) const early = s.images.length // Even though fetches resolve instantly, the consumer is rate-limited by // the cadence — a 1s window must not dump the whole buffer at once. expect(early).toBeGreaterThan(0) expect(early).toBeLessThan(60) await vi.advanceTimersByTimeAsync(12000) await p expect(s.images.length).toBe(60) }) it('does not reveal a tile until its image has decoded', async () => { stubShowcase() let release // The first tile's preload hangs until we release it; all others resolve. preloadImage.mockImplementationOnce(() => new Promise((r) => { release = r })) const s = useShowcaseStore() const p = s.loadInitial() await vi.advanceTimersByTimeAsync(1000) expect(s.images.length).toBe(0) // gated on the un-decoded first image release() await vi.advanceTimersByTimeAsync(12000) await p expect(s.images.length).toBe(60) // cascade proceeds once it decodes }) it('flags an empty library without starting the cascade', async () => { stubShowcase({ empty: true }) const s = useShowcaseStore() const p = s.loadInitial() await vi.advanceTimersByTimeAsync(1000) await p expect(s.images.length).toBe(0) expect(s.isEmpty).toBe(true) }) })