// One field of the preview screen (UI-SPEC S3, D-11): every row of the // PreviewField table. Fixtures are neutral; no application names appear in // framework tests. import { afterEach, beforeEach, describe, expect, it } from 'vitest' import { enableAutoUnmount, mount } from '@vue/test-utils' import type { FormField } from '../../src/api/types' import { setBundle } from '../../src/app/i18n' import PreviewField from '../../src/components/form/PreviewField.vue' import { clone, langFixture, rosterFormSchemaFixture } from '../fixtures/typed' import { resetState } from '../helpers' const strings = { 'backend::lang.list.empty_value': { other: '—' }, 'backend::lang.list.column_switch_true': { other: 'Yes' }, 'backend::lang.list.column_switch_false': { other: 'No' }, 'backend::lang.permissioneditor.allow': { other: 'Allow' }, 'backend::lang.permissioneditor.inherit': { other: 'Inherit' }, 'backend::lang.permissioneditor.deny': { other: 'Deny' }, 'backend::lang.permissioneditor.locked': { other: 'You cannot change this permission.' }, 'backend::lang.permissioneditor.other': { other: 'Other' }, } function field(change: Partial): FormField { return { name: 'note', type: 'text', label: 'Note', ...change } as FormField } function preview(props: { field: FormField; value: unknown; labels?: { value: number; label: string }[] }) { return mount(PreviewField, { props, attachTo: document.body }) } beforeEach(() => { resetState() setBundle({ ...langFixture.data, ...strings }, 'en') }) afterEach(() => { document.body.innerHTML = '' }) enableAutoUnmount(afterEach) describe('preview field (UI-SPEC S3, D-11)', () => { it('is a dt/dd pair: the label names the value and no input is rendered', () => { const wrapper = preview({ field: field({}), value: 'Plain text' }) expect(wrapper.attributes('data-preview-field')).toBe('note') expect(wrapper.find('dt').text()).toBe('Note') expect(wrapper.find('dt').attributes('id')).toBe('preview-note-label') expect(wrapper.find('dd [data-preview-value]').text()).toBe('Plain text') expect(wrapper.find('input, textarea, select, button').exists()).toBe(false) // Values keep full contrast: nothing is styled as disabled. expect(wrapper.find('[data-preview-value] span').classes()).not.toContain('text-muted') }) it('falls back to the field name without a label', () => { expect(preview({ field: field({ label: '' }), value: 'x' }).find('dt').text()).toBe('note') }) it.each([[null], [undefined], ['']])('shows the muted dash for an empty value (%j)', (value) => { const wrapper = preview({ field: field({}), value }) const empty = wrapper.find('[data-preview-value] [data-empty]') expect(empty.text()).toBe('—') expect(empty.classes()).toContain('text-muted') }) it('renders 0 and false-like text as values, not as empty', () => { expect(preview({ field: field({ type: 'number' }), value: 0 }).find('[data-preview-value]').text()).toBe('0') expect(preview({ field: field({}), value: 'false' }).find('[data-preview-value]').text()).toBe('false') }) it('renders markup in a value as text', () => { const wrapper = preview({ field: field({}), value: '' }) expect(wrapper.find('[data-preview-value]').text()).toBe('') expect(wrapper.find('img').exists()).toBe(false) }) it('never prints an object or an array as text', () => { for (const value of [{ a: 1 }, [1, 2]]) { const wrapper = preview({ field: field({}), value }) expect(wrapper.find('[data-empty]').exists()).toBe(true) expect(wrapper.text()).not.toContain('[object') } }) it('keeps the line breaks of a textarea and lets long text wrap', () => { const wrapper = preview({ field: field({ type: 'textarea' }), value: 'one\ntwo' }) const box = wrapper.find('[data-preview-value]') expect(box.classes()).toEqual(expect.arrayContaining(['whitespace-pre-wrap', '[overflow-wrap:anywhere]'])) expect(box.element.textContent).toContain('one\ntwo') }) it('shows the label of a dropdown option, or the stored value when no option matches', () => { const dropdown = field({ type: 'dropdown', options: [{ value: 'a', label: 'Alpha' }, { value: '2', label: 'Two' }] } as Partial) expect(preview({ field: dropdown, value: 'a' }).find('[data-preview-value]').text()).toBe('Alpha') expect(preview({ field: dropdown, value: 2 }).find('[data-preview-value]').text()).toBe('Two') expect(preview({ field: dropdown, value: 'gone' }).find('[data-preview-value]').text()).toBe('gone') }) it.each([ ['switch', true, 'true', 'Yes'], ['switch', 0, 'false', 'No'], ['checkbox', '1', 'true', 'Yes'], ['checkbox', null, 'false', 'No'], ])('renders a %s holding %j as a text pill', (type, value, state, text) => { const wrapper = preview({ field: field({ type }), value }) const pill = wrapper.find('[data-switch]') expect(pill.attributes('data-switch')).toBe(state) expect(pill.text()).toBe(text) expect(wrapper.find('[role="switch"], [role="checkbox"], input').exists()).toBe(false) }) it('shows the label of a single relation from meta.labels, else the empty option or the dash', () => { const relation = field({ type: 'relation' }) expect(preview({ field: relation, value: 2, labels: [{ value: 2, label: 'Home' }] }).find('[data-preview-value]').text()).toBe('Home') // The id itself is never shown. expect(preview({ field: relation, value: 2 }).find('[data-preview-value]').text()).toBe('—') expect(preview({ field: field({ type: 'relation', emptyOption: 'No team' } as Partial), value: null }).find('[data-empty]').text()).toBe('No team') }) it('shows a multiple relation as chips with initials and no remove buttons', () => { const many = field({ type: 'relation', multiple: true } as Partial) const wrapper = preview({ field: many, value: [5, 6], labels: [ { value: 5, label: 'team leads' }, { value: 6, label: 'news' }, ], }) const chips = wrapper.findAll('[data-chip]') expect(chips.map((chip) => chip.find('.font-semibold').text())).toEqual(['team leads', 'news']) expect(chips.map((chip) => chip.find('[aria-hidden="true"]').text())).toEqual(['TL', 'N']) expect(wrapper.find('button').exists()).toBe(false) expect(preview({ field: many, value: [] }).find('[data-empty]').text()).toBe('—') // Labels without a list value are not shown. expect(preview({ field: many, value: null, labels: [{ value: 5, label: 'x' }] }).find('[data-chip]').exists()).toBe(false) }) it('renders a permission editor through its own control, read-only', () => { const permissions = clone(rosterFormSchemaFixture.data.fields.find((item) => item.name === 'permissions')!) const wrapper = preview({ field: permissions, value: { 'posts.edit': 1, 'reports.export': -1 } }) expect(wrapper.find('[data-permission-editor]').exists()).toBe(true) expect(wrapper.findAll('[role="radiogroup"]').every((group) => group.attributes('aria-disabled') === 'true')).toBe(true) // A read-only screen shows no lock and no "you cannot change" text. expect(wrapper.find('[data-permission-locked]').exists()).toBe(false) expect(wrapper.find('[data-locked]').exists()).toBe(false) // The field passed on is a copy: the schema's own field is not marked. expect(permissions.readOnly).toBeFalsy() }) it('uses the id prefix it is given', () => { const wrapper = mount(PreviewField, { props: { field: field({}), value: 'x', idPrefix: 'p2' } }) expect(wrapper.find('dt').attributes('id')).toBe('p2-note-label') }) })