Files
bvandeusen 0497394710
CI / lint (push) Successful in 2s
CI / backend-lint-and-test (push) Successful in 12s
CI / frontend-build (push) Successful in 17s
CI / integration (push) Successful in 2m56s
feat(ui): double showcase cadence + filter bar matches TopNav frost
- Showcase reveal cadence 80ms -> 160ms (slower, more deliberate one-at-a-time
  cascade per operator). Bump showcase.spec timer advances to cover 60x160ms.
- Gallery filter bar now uses the EXACT gradiated-obsidian frost + blur as
  TopNav (was a flat rgba(...,0.55) block), so the two read as one continuous
  piece of chrome with images visibly scrolling under both; the nav's
  transparent bottom edge against the bar's opaque top leaves a faint seam that
  separates them at the very top of scroll.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-04 09:02:21 -04:00

98 lines
3.2 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 <img> 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)
})
})