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)), } }) } const sugg = (over = {}) => ({ canonical_tag_id: 7, display_name: 'Ichigo', category: 'character', score: 0.9, source: 'head', creates_new_tag: false, rejected: false, ...over, }) // Seed the store through its own load/loadAll 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) await store.loadAll(1) } 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 raw suggestions by kind + name, case-insensitively', async () => { const s = useSuggestionsStore() await seed(s, { general: [sugg({ canonical_tag_id: null, display_name: 'Holding Sword', category: 'general', creates_new_tag: true, })], }) expect(s.findPending('general', 'holding sword')).toBeTruthy() // 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 with a known tag id', () => { beforeEach(() => setActivePinia(createPinia())) afterEach(() => vi.restoreAllMocks()) it('POSTs only the accept endpoint (no tag create) and drops the row', async () => { const s = useSuggestionsStore() await seed(s, { character: [sugg()] }) const calls = [] stubFetch((url, init) => { calls.push({ url, method: init?.method }) return { status: 200, body: { accepted: true, tag_id: 7 } } }) await s.accept(s.byCategory.character[0], { tagId: 7 }) expect(calls).toHaveLength(1) expect(calls[0].url).toContain('/api/images/1/suggestions/accept') expect(s.byCategory.character).toHaveLength(0) expect(s.allByCategory.character).toHaveLength(0) }) it('a RAW suggestion accepted with a known tagId skips create AND still drops its row', async () => { // TagPanel's manual-create path: the tag was just created by // host.createAndAdd, so accept must not create again — and the drop must // key off the original raw suggestion object, not the resolved id // (regression: spreading canonical_tag_id onto the suggestion changed its // identity key and left the panel row behind). const s = useSuggestionsStore() const raw = sugg({ canonical_tag_id: null, display_name: 'Holding Sword', category: 'general', creates_new_tag: true, }) await seed(s, { general: [raw] }) 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) expect(s.allByCategory.general).toHaveLength(0) }) })