import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest' import { setActivePinia, createPinia } from 'pinia' import { useSuggestionsStore } from '../src/stores/suggestions.js' vi.mock('../src/utils/toast.js', () => ({ toast: vi.fn() })) 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)), } }) } // Every suggestion is a canonical DB tag now (tagging-v2): a real id, flagged // above/below its head's suggest threshold. No raw / creates-new / alias cases. const sugg = (over = {}) => ({ canonical_tag_id: 7, display_name: 'Ichigo', category: 'character', score: 0.9, source: 'head', above_threshold: true, rejected: false, ...over, }) // Seed the store through its single load() so currentImageId is set the way the // panel sets it (accept() derives the image from it). async function seed(store, byCategory) { stubFetch(() => ({ status: 200, body: { by_category: byCategory } })) await store.load(1) } describe('suggestions store: load derives aboveByCategory', () => { beforeEach(() => setActivePinia(createPinia())) afterEach(() => vi.restoreAllMocks()) it('one fetch backs both surfaces: byCategory is the full set, aboveByCategory the panel subset', async () => { const s = useSuggestionsStore() await seed(s, { general: [ sugg({ canonical_tag_id: 1, display_name: 'sword', category: 'general' }), sugg({ canonical_tag_id: 2, display_name: 'faint', category: 'general', above_threshold: false }), ], }) expect(s.byCategory.general).toHaveLength(2) // dropdown sees all expect(s.aboveByCategory.general).toHaveLength(1) // panel sees above-threshold only expect(s.aboveByCategory.general[0].display_name).toBe('sword') }) }) describe('suggestions store: findPending (manual add == accept, 2026-07-03)', () => { beforeEach(() => setActivePinia(createPinia())) afterEach(() => vi.restoreAllMocks()) it('matches by canonical tag id', async () => { const s = useSuggestionsStore() await seed(s, { character: [sugg()] }) expect(s.findPending('general', 'unrelated', 7)?.display_name).toBe('Ichigo') }) it('matches by kind + name, case-insensitively', async () => { const s = useSuggestionsStore() await seed(s, { general: [sugg({ canonical_tag_id: 12, display_name: 'Holding Sword', category: 'general' })], }) expect(s.findPending('general', 'holding sword')?.canonical_tag_id).toBe(12) // Kind must match: same name under another kind is a different tag. expect(s.findPending('character', 'holding sword')).toBeNull() }) it('skips rejected rows and returns null when nothing matches', async () => { const s = useSuggestionsStore() await seed(s, { character: [sugg({ rejected: true })] }) expect(s.findPending('character', 'Ichigo', 7)).toBeNull() expect(s.findPending('character', 'Rukia')).toBeNull() }) }) describe('suggestions store: accept', () => { beforeEach(() => setActivePinia(createPinia())) afterEach(() => vi.restoreAllMocks()) it('POSTs only the accept endpoint (canonical id) and drops the row', async () => { const s = useSuggestionsStore() await seed(s, { character: [sugg()] }) const calls = [] stubFetch((url, init) => { calls.push({ url, method: init?.method, body: init?.body }) return { status: 200, body: { accepted: true, tag_id: 7 } } }) await s.accept(s.byCategory.character[0]) expect(calls).toHaveLength(1) expect(calls[0].url).toContain('/api/images/1/suggestions/accept') expect(JSON.parse(calls[0].body)).toEqual({ tag_id: 7 }) expect(s.byCategory.character).toHaveLength(0) }) it('honors a known tagId and still drops the row by the suggestion identity', async () => { // TagPanel's manual-add-matches-suggestion path passes the resolved tag id; // accept sends exactly it and drops the original suggestion row (which keys // off the suggestion object, not the passed id). const s = useSuggestionsStore() await seed(s, { general: [sugg({ canonical_tag_id: 12, display_name: 'Holding Sword', category: 'general' })] }) const calls = [] stubFetch((url, init) => { calls.push({ url, body: init?.body }) return { status: 200, body: { accepted: true, tag_id: 42 } } }) await s.accept(s.byCategory.general[0], { tagId: 42 }) expect(calls).toHaveLength(1) expect(calls[0].url).toContain('/api/images/1/suggestions/accept') expect(JSON.parse(calls[0].body)).toEqual({ tag_id: 42 }) expect(s.byCategory.general).toHaveLength(0) }) })