import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, flushPromises } from '@vue/test-utils' import ListView from '../../src/views/ListView.vue' import { toasts } from '../../src/state/useToasts' import { clone, listFixture, listSchemaFixture } from '../fixtures/typed' import { API, mountApp, queryOf, requestsTo, resetState, type Route } from '../helpers' const BASE = `${API}/acme/demo/widgets` const serverError = { status: 500, body: { error: { code: 'server_error', message: 'Database down', details: {} } } } function routes(overrides: Record = {}): Record { return { [`GET ${BASE}/schema/list`]: { body: listSchemaFixture }, [`GET ${BASE}`]: { body: listFixture }, ...overrides, } } function schemaWith(change: (schema: typeof listSchemaFixture.data) => void) { const envelope = clone(listSchemaFixture) change(envelope.data) return { body: envelope } } async function selectAllAndDelete(wrapper: Awaited>['wrapper']) { await wrapper.find('th [role="checkbox"]').trigger('click') await wrapper.find('[data-action="delete"]').trigger('click') await flushPromises() ;(document.body.querySelector('[data-confirm-dialog] [data-action="confirm"]') as HTMLElement).click() await flushPromises() } beforeEach(() => { resetState() }) afterEach(() => { document.body.innerHTML = '' }) enableAutoUnmount(afterEach) describe('list view', () => { it('is the component of the list route', async () => { const { wrapper } = await mountApp('/acme/demo/widgets', routes()) expect(wrapper.findComponent(ListView).exists()).toBe(true) expect(wrapper.find('h1').text()).toBe('Widgets') expect(wrapper.find('[data-record-count]').text()).toBe('2 widgets') }) it('sends the URL query to the list endpoint', async () => { const { calls } = await mountApp('/acme/demo/widgets?search=blue&sort=code&dir=desc&page=2&filter[active]=true', routes()) const [request] = requestsTo(calls, 'GET', BASE) const query = queryOf(request!) expect(query.get('search')).toBe('blue') expect(query.get('sort')).toBe('code') expect(query.get('dir')).toBe('desc') expect(query.get('page')).toBe('2') expect(query.get('filter[active]')).toBe('true') }) it('announces a failed list load as an alert and keeps the toolbar', async () => { const { wrapper } = await mountApp('/acme/demo/widgets', routes({ [`GET ${BASE}`]: serverError })) expect(wrapper.find('[role="alert"]').text()).toBe('Nie udało się wczytać listy.') expect(wrapper.find('table').exists()).toBe(false) expect(wrapper.find('[data-list-search]').exists()).toBe(true) }) it('announces a failed schema load and falls back to the navigation label', async () => { const { wrapper } = await mountApp('/acme/demo/gadgets', { [`GET ${API}/acme/demo/gadgets/schema/list`]: serverError, [`GET ${API}/acme/demo/gadgets`]: { body: listFixture }, }) expect(wrapper.find('h1').text()).toBe('Gadgets') expect(wrapper.find('[role="alert"]').exists()).toBe(true) expect(wrapper.find('[data-list-search]').exists()).toBe(false) }) it('shows the create button only when the toolbar declares it', async () => { const withCreate = await mountApp('/acme/demo/widgets', routes()) expect(withCreate.wrapper.find('[data-action="create"]').attributes('href')).toBe('/acme/demo/widgets/create') withCreate.wrapper.unmount() const { wrapper } = await mountApp( '/acme/demo/widgets', routes({ [`GET ${BASE}/schema/list`]: schemaWith((schema) => (schema.toolbarButtons = ['delete'])) }), ) expect(wrapper.find('[data-action="create"]').exists()).toBe(false) expect(wrapper.find('[data-action="delete"]').exists()).toBe(true) }) it('links no rows without a recordUrl and shows no checkboxes or sorting when turned off', async () => { const { wrapper } = await mountApp( '/acme/demo/widgets', routes({ [`GET ${BASE}/schema/list`]: schemaWith((schema) => { delete schema.recordUrl schema.showCheckboxes = false schema.showSorting = false }), }), ) expect(wrapper.find('tbody a').exists()).toBe(false) expect(wrapper.find('[role="checkbox"]').exists()).toBe(false) expect(wrapper.find('[data-sort]').exists()).toBe(false) }) it('toasts the server message when a bulk delete is refused', async () => { const { wrapper } = await mountApp( '/acme/demo/widgets', routes({ [`POST ${BASE}/bulk-delete`]: { status: 403, body: { error: { code: 'forbidden', message: 'Not allowed', details: {} } } } }), { attach: true }, ) await selectAllAndDelete(wrapper) expect(toasts.value[0]).toMatchObject({ text: 'Not allowed', tone: 'danger' }) }) it('toasts the generic failure when the bulk delete cannot reach the server', async () => { const { wrapper } = await mountApp( '/acme/demo/widgets', routes({ [`POST ${BASE}/bulk-delete`]: () => { throw new TypeError('offline') }, }), { attach: true }, ) await selectAllAndDelete(wrapper) expect(toasts.value[0]).toMatchObject({ text: 'Nie udało się usunąć zaznaczonych rekordów.', tone: 'danger' }) expect(wrapper.find('[data-action="delete"]').attributes('disabled')).toBeUndefined() }) })