// 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; 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} x` }) const text = wrapper.find('[data-forbidden-banner] p') expect(text.classes()).toContain('[overflow-wrap:anywhere]') expect(text.text()).toContain('x') 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) }) })