- 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
112 lines
4.8 KiB
TypeScript
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()
|
|
})
|
|
})
|