import { describe, it, expect, vi, beforeEach } from 'vitest'
import { mountSuspended, mockNuxtImport } from '@nuxt/test-utils/runtime'
import { flushPromises } from '@vue/test-utils'
import { ref } from 'vue'
import CollectionsPage from '../../app/pages/collections.vue'
const { api, mutate, toast } = vi.hoisted(() => ({ api: vi.fn(), mutate: vi.fn(), toast: vi.fn() }))
mockNuxtImport('useApi', () => api)
mockNuxtImport('useApiFetch', () => () => mutate)
mockNuxtImport('useToast', () => () => ({ add: toast }))
const modal = { props: ['open'], template: '
' }
const mountPage = () => mountSuspended(CollectionsPage, { global: { stubs: { UModal: modal } } })
describe('Collections management', () => {
const refresh = vi.fn()
beforeEach(() => {
vi.clearAllMocks()
api.mockReturnValue({ data: ref([{ id: 'games', name: 'PS5 games', description: 'Our favorites', icon: 'i-lucide-gamepad-2', asset_count: 3 }]), status: ref('success'), error: ref(null), refresh })
mutate.mockResolvedValue({})
})
it('links to filtered inventory and saves edited metadata', async () => {
const wrapper = await mountPage()
expect(wrapper.find('a[href="/assets?collection_id=games"]').text()).toContain('View assets')
expect(wrapper.get('article').text()).toContain('3 assets')
await wrapper.get('button[aria-label="Edit PS5 games"]').trigger('click')
await wrapper.get('form input').setValue('PlayStation 5')
await wrapper.get('form').trigger('submit')
await flushPromises()
expect(mutate).toHaveBeenCalledWith('/api/collections/games', {
method: 'PUT', body: JSON.stringify({ name: 'PlayStation 5', description: 'Our favorites', icon: 'i-lucide-gamepad-2' })
})
expect(refresh).toHaveBeenCalledOnce()
wrapper.unmount()
})
it('preserves input and displays duplicate-name errors', async () => {
mutate.mockRejectedValue({ data: { error: 'a collection with this name already exists' } })
const wrapper = await mountPage()
await wrapper.findAll('button').find(button => button.text() === 'New collection')!.trigger('click')
await wrapper.get('form input').setValue('PS5 games')
await wrapper.get('form').trigger('submit')
await flushPromises()
expect(wrapper.get('[role="alert"]').text()).toContain('already exists')
expect((wrapper.get('form input').element as HTMLInputElement).value).toBe('PS5 games')
expect(mutate.mock.calls[0]?.[1].method).toBe('POST')
wrapper.unmount()
})
it('requires confirmation before deleting a collection', async () => {
const wrapper = await mountPage()
await wrapper.get('button[aria-label="Actions for PS5 games"]').trigger('keydown', { key: 'Enter' })
await flushPromises()
const deleteItem = document.querySelector('[role="menuitem"]')!
expect(deleteItem.textContent).toContain('Delete')
deleteItem.click()
await flushPromises()
expect(mutate).not.toHaveBeenCalled()
await wrapper.findAll('button').find(button => button.text() === 'Delete collection')!.trigger('click')
await flushPromises()
expect(mutate).toHaveBeenCalledWith('/api/collections/games', { method: 'DELETE' })
expect(refresh).toHaveBeenCalledOnce()
wrapper.unmount()
})
it('searches names and descriptions without changing summary totals', async () => {
api.mockReturnValue({ data: ref([
{ id: 'games', name: 'PS5 games', description: 'Our favorites', icon: 'i-lucide-gamepad-2', asset_count: 3 },
{ id: 'books', name: 'Books', description: 'Weekend reading', icon: 'i-lucide-book-open', asset_count: 0 }
]), status: ref('success'), error: ref(null), refresh })
const wrapper = await mountPage()
expect(wrapper.get('dl').text()).toContain('Collections2')
expect(wrapper.get('dl').text()).toContain('With assets1')
expect(wrapper.get('dl').text()).toContain('Empty1')
await wrapper.get('input[aria-label="Search collections"]').setValue(' WEEKEND ')
expect(wrapper.findAll('article')).toHaveLength(1)
expect(wrapper.get('article').text()).toContain('Books')
expect(wrapper.get('[role="status"]').text()).toBe('Showing 1 of 2')
expect(wrapper.get('dl').text()).toContain('Collections2')
await wrapper.get('input[aria-label="Search collections"]').setValue('unknown')
expect(wrapper.text()).toContain('No matching collections')
await wrapper.findAll('button').find(button => button.text() === 'Clear search')!.trigger('click')
expect(wrapper.findAll('article')).toHaveLength(2)
wrapper.unmount()
})
})