- FormTabs groups fields by tab (untabbed fields in the default tab) as a
segmented tablist; a tab holding invalid fields after a 422 shows a
count badge and the form switches to the first invalid field
- switch, checkbox and relation join the renderer registry: toggle cards
(role=switch, 20px checkbox) keep a numeric value numeric; relation
fields are read-only labels from meta.labels, a searchable single
select over fields/{field}/options with emptyOption first, or ordered
removable chips with an appending search (300 ms debounce, 20 per
page, more on scroll or the more action)
- FormView gets the back button, record title and update subtitle, a
sticky footer (Usuń with deleteConfirm then DELETE, Anuluj, Zapisz i
zamknij to the mapped redirectClose, Zapisz), and asks before leaving a
dirty form on any route change plus a beforeunload guard
- Settings: the rail pins Ustawienia to the bottom when /settings is
non-empty; /settings lists pages by category; /settings/:code renders
the settings schema through FormGrid and the registry, PUTs the values
and maps a 422 like the record form
- New backend::lang keys (form.more_options, tab_default, discard,
settings.back); form and settings smoke tests; boardwalk/dist rebuilt
128 lines
5.0 KiB
TypeScript
128 lines
5.0 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { flushPromises } from '@vue/test-utils'
|
|
import { setBundle } from '../../src/app/i18n'
|
|
import { clearUser } from '../../src/state/useAuth'
|
|
import { setNavigation } from '../../src/state/useNavigation'
|
|
import { loadSettings, setSettings } from '../../src/state/useSettings'
|
|
import { clearToasts } from '../../src/state/useToasts'
|
|
import lang from '../fixtures/lang.json'
|
|
import listSchema from '../fixtures/widgets.list-schema.json'
|
|
import listRows from '../fixtures/widgets.list.json'
|
|
import settings from '../fixtures/settings.json'
|
|
import { API, mockApi, mountApp, requestsTo, type Route } from '../helpers'
|
|
|
|
const SETTING = `${API}/settings/search`
|
|
|
|
function routes(overrides: Record<string, Route> = {}): Record<string, Route> {
|
|
return {
|
|
[`GET ${API}/settings`]: { body: settings.list },
|
|
[`GET ${SETTING}/schema`]: { body: settings.schema },
|
|
[`GET ${SETTING}`]: { body: settings.values },
|
|
[`GET ${API}/acme/demo/widgets/schema/list`]: { body: listSchema },
|
|
[`GET ${API}/acme/demo/widgets`]: { body: listRows },
|
|
[`PUT ${SETTING}`]: async (request) => ({
|
|
body: { data: { data: { ...settings.values.data.data, ...((await request.json()) as object) }, exists: true }, meta: {} },
|
|
}),
|
|
...overrides,
|
|
}
|
|
}
|
|
|
|
beforeEach(() => {
|
|
clearUser()
|
|
setNavigation([])
|
|
setSettings([])
|
|
clearToasts()
|
|
setBundle(lang.data, 'pl')
|
|
})
|
|
|
|
afterEach(() => {
|
|
document.body.innerHTML = ''
|
|
})
|
|
|
|
describe('settings', () => {
|
|
it('shows the Ustawienia rail item only when settings pages are permitted', async () => {
|
|
const { wrapper } = await mountApp('/acme/demo/widgets', routes())
|
|
expect(wrapper.find('[data-rail-settings]').exists()).toBe(false)
|
|
|
|
mockApi(routes())
|
|
await loadSettings()
|
|
await flushPromises()
|
|
const item = wrapper.find('[data-rail-settings]')
|
|
expect(item.text()).toBe('Ustawienia')
|
|
expect(item.attributes('href')).toBe('/settings')
|
|
expect(item.attributes('aria-current')).toBeUndefined()
|
|
expect(item.find('svg').exists()).toBe(true)
|
|
})
|
|
|
|
it('lists every permitted settings page grouped by category', async () => {
|
|
mockApi(routes())
|
|
await loadSettings()
|
|
const { wrapper } = await mountApp('/settings', routes())
|
|
|
|
expect(wrapper.find('h1').text()).toBe('Ustawienia')
|
|
expect(wrapper.find('[data-rail-settings]').attributes('aria-current')).toBe('page')
|
|
const groups = wrapper.findAll('[data-settings-group]')
|
|
expect(groups.map((group) => group.find('h2').text())).toEqual(['Appearance', 'System'])
|
|
expect(groups[1]!.findAll('[data-settings]').map((link) => link.attributes('data-settings'))).toEqual([
|
|
'mail',
|
|
'search',
|
|
])
|
|
const search = wrapper.find('[data-settings="search"]')
|
|
expect(search.attributes('href')).toBe('/settings/search')
|
|
expect(search.text()).toContain('Search')
|
|
expect(search.text()).toContain('Search engine options')
|
|
expect(search.find('svg').exists()).toBe(true)
|
|
})
|
|
|
|
it('renders a settings page through the form registry and PUTs its values', async () => {
|
|
mockApi(routes())
|
|
await loadSettings()
|
|
const { wrapper, calls } = await mountApp('/settings/search', routes())
|
|
|
|
expect(wrapper.find('h1').text()).toBe('Search')
|
|
const toggle = wrapper.find('#setting-use_engine')
|
|
expect(toggle.attributes('role')).toBe('switch')
|
|
expect(toggle.attributes('aria-checked')).toBe('false')
|
|
expect((wrapper.find('#setting-engine_host').element as HTMLInputElement).value).toBe('search.local')
|
|
|
|
await toggle.trigger('click')
|
|
await wrapper.find('#setting-engine_host').setValue('engine.local')
|
|
await wrapper.find('[data-action="save"]').trigger('click')
|
|
await flushPromises()
|
|
|
|
const [put] = requestsTo(calls, 'PUT', SETTING)
|
|
expect(put!.headers.get('X-Requested-With')).toBe('XMLHttpRequest')
|
|
// A numeric toggle keeps its type: 0 becomes 1, not true.
|
|
expect(await put!.json()).toEqual({ use_engine: 1, engine_host: 'engine.local' })
|
|
expect(wrapper.find('[role="status"]').text()).toContain('Zapisano ustawienia')
|
|
expect(wrapper.find('#setting-use_engine').attributes('aria-checked')).toBe('true')
|
|
})
|
|
|
|
it('maps a settings 422 onto its fields', async () => {
|
|
const { wrapper } = await mountApp(
|
|
'/settings/search',
|
|
routes({
|
|
[`PUT ${SETTING}`]: {
|
|
status: 422,
|
|
body: {
|
|
error: {
|
|
code: 'validation_failed',
|
|
message: 'The given data was invalid.',
|
|
details: { engine_host: ['The engine host format is invalid.'] },
|
|
},
|
|
},
|
|
},
|
|
}),
|
|
{ attach: true },
|
|
)
|
|
|
|
await wrapper.find('[data-action="save"]').trigger('click')
|
|
await flushPromises()
|
|
|
|
expect(wrapper.find('[data-error-banner]').text()).toContain('Popraw 1 pole oznaczone poniżej.')
|
|
expect(wrapper.find('#setting-engine_host').attributes('aria-invalid')).toBe('true')
|
|
expect(wrapper.find('#setting-engine_host-error').text()).toBe('The engine host format is invalid.')
|
|
expect(document.activeElement?.id).toBe('setting-engine_host')
|
|
})
|
|
})
|