Files
summercms/admin/tests/form/FormErrorBanner.test.ts
Jakub Zych 84efdc09e0 test(12.1-05): unit tests for the Phase 12.1 SPA components
- BulkActionsMenu, RowStateBadges, RecordActions, PreviewField, FormErrorBanner, PasswordField and PermissionEditorField, each mounted on its own
- backstop: a row state outside the fixed set renders no badge and no class
- backstop: the permission editor's emission rules per mode, the locked row and codes outside the options
2026-10-05 15:29:44 +02:00

90 lines
4.4 KiB
TypeScript

// The form's error banner: the 422 summary and the forbidden save banner
// (UI-SPEC S6, D-27). No application names appear in framework tests.
import { beforeEach, describe, expect, it } from 'vitest'
import { mount } from '@vue/test-utils'
import { setBundle } from '../../src/app/i18n'
import FormErrorBanner from '../../src/components/form/FormErrorBanner.vue'
import { langFixture } from '../fixtures/typed'
import { resetState } from '../helpers'
const strings = {
'backend::lang.form.error_title': { other: 'The record could not be saved.' },
'backend::lang.form.error_fields': { one: 'Correct :count field.', other: 'Correct :count fields.' },
}
const fieldNames = ['name', 'email']
function banner(props: { errors?: Record<string, string[]>; forbidden?: string | null }) {
return mount(FormErrorBanner, { props: { errors: props.errors ?? {}, fieldNames, forbidden: props.forbidden } })
}
beforeEach(() => {
resetState()
setBundle({ ...langFixture.data, ...strings }, 'en')
})
describe('form error banner', () => {
it('renders nothing without errors and without a refusal', () => {
const wrapper = banner({})
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
expect(wrapper.text()).toBe('')
})
it('counts the 422 fields and lists messages of keys that are not fields', () => {
const wrapper = banner({ errors: { name: ['Too short.'], record: ['The record is locked.', 'Try later.'] } })
const alert = wrapper.find('[data-error-banner]')
expect(alert.attributes('role')).toBe('alert')
expect(alert.text()).toContain('The record could not be saved.')
expect(alert.text()).toContain('Correct 2 fields.')
expect(alert.text()).toContain('The record is locked.')
expect(alert.text()).toContain('Try later.')
// A field's own message is shown on the field, not here.
expect(alert.text()).not.toContain('Too short.')
expect(banner({ errors: { name: ['x'] } }).text()).toContain('Correct 1 field.')
})
})
describe('forbidden save banner (UI-SPEC S6, D-27)', () => {
it('shows the refusal as a persistent alert in place of the 422 banner', () => {
const wrapper = banner({ forbidden: 'You may not change this group.', errors: { name: ['Reserved.'] } })
const alert = wrapper.find('[data-forbidden-banner]')
expect(alert.attributes('role')).toBe('alert')
expect(alert.find('p').text()).toBe('You may not change this group.')
expect(alert.find('svg').attributes('aria-hidden')).toBe('true')
expect(alert.classes()).toEqual(expect.arrayContaining(['bg-danger-soft', 'text-danger']))
expect(wrapper.find('[data-error-banner]').exists()).toBe(false)
// Field messages stay on their fields; no count line here.
expect(alert.text()).not.toContain('Reserved.')
expect(alert.text()).not.toContain('Correct')
// No close button: it stays until the next save attempt.
expect(alert.find('button').exists()).toBe(false)
})
it('lists details of keys that are not form fields inside the banner', () => {
const wrapper = banner({ forbidden: 'Refused.', errors: { groups: ['The admin group is locked.'], email: ['On the field.'] } })
const lines = wrapper.findAll('[data-forbidden-banner] p').map((line) => line.text())
expect(lines).toEqual(['Refused.', 'The admin group is locked.'])
})
it('wraps a long message and renders markup as text', () => {
const long = 'A very long refusal with a_long_unbroken_identifier_that_must_wrap '.repeat(6).trim()
const wrapper = banner({ forbidden: `${long} <b>x</b>` })
const text = wrapper.find('[data-forbidden-banner] p')
expect(text.classes()).toContain('[overflow-wrap:anywhere]')
expect(text.text()).toContain('<b>x</b>')
expect(wrapper.find('b').exists()).toBe(false)
expect(wrapper.html()).not.toContain('truncate')
})
it('falls back to the 422 banner when the refusal is cleared', async () => {
const wrapper = banner({ forbidden: 'Refused.', errors: { name: ['x'] } })
await wrapper.setProps({ forbidden: null })
expect(wrapper.find('[data-forbidden-banner]').exists()).toBe(false)
expect(wrapper.find('[data-error-banner]').exists()).toBe(true)
await wrapper.setProps({ errors: {} })
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
// An empty string is not a refusal.
await wrapper.setProps({ forbidden: '' })
expect(wrapper.find('[data-forbidden-banner]').exists()).toBe(false)
})
})