- 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
155 lines
7.6 KiB
TypeScript
155 lines
7.6 KiB
TypeScript
// 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>): 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: '<img src=x onerror=alert(1)>' })
|
|
expect(wrapper.find('[data-preview-value]').text()).toBe('<img src=x onerror=alert(1)>')
|
|
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<FormField>)
|
|
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<FormField>), 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<FormField>)
|
|
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')
|
|
})
|
|
})
|