Files
FabledCurator/frontend/test/gallery.spec.js
T
bvandeusen 4f9464d215
CI / lint (push) Successful in 3s
CI / backend-lint-and-test (push) Successful in 13s
CI / frontend-build (push) Successful in 38s
CI / intimp (push) Successful in 3m52s
CI / intapi (push) Successful in 8m5s
CI / intcore (push) Successful in 8m36s
feat(gallery,tags): clear active filters
Two gaps where a filter couldn't be removed:

- Gallery: a tag_id filter (from clicking a tag) had no indicator or clear
  control — only post_id did (PostInfoHeader). Add an "Tag: <name> ✕" chip
  that clears the filter by dropping tag_id from the URL. New lightweight
  GET /api/tags/<id> resolves the name; the store fetches it on filter set.
- Tags view: the kind chip-group used mandatory="false" — a STRING ("false"
  is truthy in JS), which made the group mandatory so the active kind chip
  couldn't be deselected. Fixed to :mandatory="false" so the filter clears.

Tests: GET /tags/<id> shape + 404; gallery store resolves filterTagName.

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

81 lines
2.7 KiB
JavaScript
Raw 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'
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: tag/post filter exclusivity', () => {
beforeEach(() => setActivePinia(createPinia()))
afterEach(() => vi.restoreAllMocks())
it('setPostFilter sets post_id and clears tag_id', () => {
const s = useGalleryStore()
stubFetch(() => ({ status: 200, body: EMPTY }))
s.setTagFilter(3)
expect(s.filter.tag_id).toBe(3)
s.setPostFilter(7)
expect(s.filter.post_id).toBe(7)
expect(s.filter.tag_id).toBe(null)
})
it('setTagFilter clears post_id', () => {
const s = useGalleryStore()
stubFetch(() => ({ status: 200, body: EMPTY }))
s.setPostFilter(7)
s.setTagFilter(3)
expect(s.filter.tag_id).toBe(3)
expect(s.filter.post_id).toBe(null)
})
it('loadMore sends exactly the active filter param', async () => {
const s = useGalleryStore()
const urls = []
stubFetch((url) => { urls.push(url); return { status: 200, body: EMPTY } })
s.setPostFilter(7)
await s.loadMore()
const scrollCall = urls.filter(u => u.includes('/api/gallery/scroll')).pop()
expect(scrollCall).toContain('post_id=7')
expect(scrollCall).not.toContain('tag_id=')
})
it('setTagFilter resolves the tag name for the filter chip', async () => {
const s = useGalleryStore()
stubFetch((url) => {
if (url.includes('/api/tags/7')) {
return { status: 200, body: { id: 7, name: 'Asuka', kind: 'character' } }
}
return { status: 200, body: EMPTY } // scroll + timeline
})
s.setTagFilter(7)
expect(s.filter.tag_id).toBe(7)
await vi.waitFor(() => expect(s.filterTagName).toBe('Asuka'))
})
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')
})
})