Files
summercms/admin/tests/form/Settings.test.ts
Jakub Zych 1c2a66df45 test(10-05): bring every SPA module, composable and component under Vitest
- 41 unit and component suites under admin/tests/{app,state,shell,list,form,relation,views,ui}
  covering states and a11y roles; every src module is imported by a test
- typed fixture helper assigns each JSON fixture to its generated OpenAPI type
- fix: iconFor ignores inherited object members such as "constructor"
- fix: field controls import ./control instead of the registry (import cycle
  left a renderer unregistered depending on module load order)
- fix: dropdown shows the placeholder for an unknown stored value next to an emptyOption
- fix: list announces a failed schema load even when the rows arrive after it
- tailwind no longer scans admin/tests; boardwalk/dist rebuilt
2026-09-27 17:53:57 +02:00

112 lines
4.8 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it } from 'vitest'
import { enableAutoUnmount, flushPromises } from '@vue/test-utils'
import SettingsFormView from '../../src/views/SettingsFormView.vue'
import SettingsIndexView from '../../src/views/SettingsIndexView.vue'
import { setSettings } from '../../src/state/useSettings'
import { toasts } from '../../src/state/useToasts'
import { settingsFixture } from '../fixtures/typed'
import { API, mountApp, requestsTo, resetState, type Route } from '../helpers'
const PAGE = `${API}/settings/search`
const failure = (status: number, message: string, details: Record<string, unknown> = {}) => ({
status,
body: { error: { code: 'x', message, details } },
})
function routes(overrides: Record<string, Route> = {}): Record<string, Route> {
return {
[`GET ${API}/settings`]: { body: settingsFixture.list },
[`GET ${PAGE}/schema`]: { body: settingsFixture.schema },
[`GET ${PAGE}`]: { body: settingsFixture.values },
...overrides,
}
}
async function openPage(overrides: Record<string, Route> = {}) {
setSettings(settingsFixture.list.data)
return mountApp('/settings/search', routes(overrides))
}
beforeEach(() => {
resetState()
})
afterEach(() => {
document.body.innerHTML = ''
})
enableAutoUnmount(afterEach)
describe('settings index (D-21)', () => {
it('says so when no settings page is permitted', async () => {
const { wrapper } = await mountApp('/settings', routes())
expect(wrapper.findComponent(SettingsIndexView).exists()).toBe(true)
expect(wrapper.find('h1').text()).toBe('Ustawienia')
expect(wrapper.find('section > p').text()).toBe('Brak dostępnych ustawień.')
})
it('links every page by encoded code with its label and description', async () => {
setSettings([{ ...settingsFixture.list.data[0]!, code: 'a b', category: '', description: '' }])
const { wrapper } = await mountApp('/settings', routes())
const link = wrapper.find('[data-settings="a b"]')
expect(link.attributes('href')).toBe('/settings/a%20b')
expect(link.text()).toBe('Search')
expect(wrapper.find('[data-settings-group] h2').exists()).toBe(false)
})
})
describe('settings form (D-21)', () => {
it('titles the page from the entry and links back to the index', async () => {
const { wrapper } = await openPage()
expect(wrapper.findComponent(SettingsFormView).exists()).toBe(true)
expect(wrapper.find('h1').text()).toBe('Search')
expect(wrapper.find('header p').text()).toBe('Search engine options')
expect(wrapper.find('[data-action="back"]').attributes('href')).toBe('/settings')
expect(wrapper.find('[data-action="back"]').attributes('aria-label')).toBe('Wróć do ustawień')
})
it('falls back to the schema name, then the code, without an entry', async () => {
const { wrapper } = await mountApp('/settings/search', routes())
expect(wrapper.find('h1').text()).toBe('Search')
expect(wrapper.find('header p').exists()).toBe(false)
})
it('announces a failed load and disables saving', async () => {
const { wrapper } = await openPage({ [`GET ${PAGE}`]: failure(500, 'Boom') })
expect(wrapper.find('[role="alert"]').text()).toBe('Nie udało się wczytać formularza.')
expect(wrapper.find('form').exists()).toBe(false)
})
it('toasts the server message for a refused save and the generic one for a network failure', async () => {
const { wrapper } = await openPage({ [`PUT ${PAGE}`]: failure(403, 'Not allowed') })
await wrapper.find('[data-action="save"]').trigger('click')
await flushPromises()
expect(toasts.value[0]).toMatchObject({ text: 'Not allowed', tone: 'danger' })
wrapper.unmount()
const offline = await openPage({
[`PUT ${PAGE}`]: () => {
throw new TypeError('offline')
},
})
await offline.wrapper.find('[data-action="save"]').trigger('click')
await flushPromises()
expect(toasts.value[0]).toMatchObject({ text: 'Nie udało się zapisać zmian. Spróbuj ponownie.', tone: 'danger' })
expect(offline.wrapper.find('[data-action="save"]').attributes('disabled')).toBeUndefined()
})
it('maps a 422 onto fields, lists unknown keys in the banner, focuses the field and clears it on change', async () => {
const { wrapper, calls } = await openPage({
[`PUT ${PAGE}`]: failure(422, 'Invalid', { engine_host: ['Unreachable'], other: ['Global problem'] }),
})
await wrapper.find('[data-action="save"]').trigger('click')
await flushPromises()
expect(requestsTo(calls, 'PUT', PAGE)).toHaveLength(1)
const input = wrapper.find('#setting-engine_host')
expect(input.attributes('aria-invalid')).toBe('true')
expect(wrapper.find('[data-error-banner]').text()).toContain('Global problem')
await input.setValue('ok.local')
expect(wrapper.find('#setting-engine_host').attributes('aria-invalid')).toBeUndefined()
})
})