import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { useGalleryStore } from '../src/stores/gallery.js' function stubFetch(handler) { globalThis.fetch = vi.fn(async (url, init) => { const { status, body } = handler(url, init) return { ok: status >= 200 && status < 300, status, statusText: String(status), text: async () => (body == null ? '' : JSON.stringify(body)), } }) } const EMPTY = { images: [], date_groups: [], next_cursor: null } describe('gallery store: composable filter', () => { beforeEach(() => setActivePinia(createPinia())) afterEach(() => vi.restoreAllMocks()) it('applyFilterFromQuery parses the query and loadMore sends composable params', async () => { const s = useGalleryStore() const urls = [] stubFetch((url) => { urls.push(url) if (url.includes('/api/tags/')) { return { status: 200, body: { id: 1, name: 'X', kind: 'general' } } } return { status: 200, body: EMPTY } }) await s.applyFilterFromQuery({ tag_id: '1,2', artist_id: '5', media: 'video', sort: 'oldest' }) expect(s.filter.tag_ids).toEqual([1, 2]) expect(s.filter.artist_id).toBe(5) expect(s.filter.media_type).toBe('video') expect(s.filter.sort).toBe('oldest') const scroll = decodeURIComponent(urls.find((u) => u.includes('/api/gallery/scroll'))) expect(scroll).toContain('tag_id=1,2') expect(scroll).toContain('artist_id=5') expect(scroll).toContain('media=video') expect(scroll).toContain('sort=oldest') }) it('omits sort=newest and sends no filter params when empty', async () => { const s = useGalleryStore() const urls = [] stubFetch((url) => { urls.push(url); return { status: 200, body: EMPTY } }) await s.applyFilterFromQuery({}) const scroll = urls.find((u) => u.includes('/api/gallery/scroll')) expect(scroll).not.toContain('sort=') expect(scroll).not.toContain('tag_id=') expect(scroll).not.toContain('media=') }) it('treats post_id as the exclusive filter param', async () => { const s = useGalleryStore() const urls = [] stubFetch((url) => { urls.push(url); return { status: 200, body: EMPTY } }) await s.applyFilterFromQuery({ post_id: '7', tag_id: '1' }) expect(s.filter.post_id).toBe(7) const scroll = urls.find((u) => u.includes('/api/gallery/scroll')) expect(scroll).toContain('post_id=7') expect(scroll).not.toContain('tag_id=') }) it('noteTagLabel and noteArtistLabel pre-seed chip labels', () => { const s = useGalleryStore() s.noteTagLabel(9, 'Rukia') expect(s.tagLabels[9]).toBe('Rukia') s.noteArtistLabel('Kubo') expect(s.artistLabel).toBe('Kubo') }) it('loadInitial issues exactly one scroll request at the initial limit', async () => { const s = useGalleryStore() const urls = [] // Non-null cursor: the old 10Ă—serial-batch loop would have fired ten // requests here. The single-fetch path must fire exactly one. stubFetch((url) => { urls.push(url) return { status: 200, body: { images: [], date_groups: [], next_cursor: 'c1' } } }) await s.loadInitial() const scrollCalls = urls.filter((u) => u.includes('/api/gallery/scroll')) expect(scrollCalls).toHaveLength(1) expect(scrollCalls[0]).toContain('limit=50') }) })