- trigger.test.ts: triggerState and applyTriggers - FormView: disable, absent-controller and hide-tab backstops, the live announcement - PermissionEditorField and formState: record-locked rows, notes and the payload omission, record lock over request lock, read-only screens - PreviewView: hide removes, disable shows no note - browser record-actions-wrap at 1024px; vitest.browser.config.ts gains the Tailwind plugin so the footer is laid out with the admin's CSS
123 lines
5.4 KiB
TypeScript
123 lines
5.4 KiB
TypeScript
// Field triggers (fields.yaml `trigger`, D-21, UI-SPEC C6): the pure
|
|
// helpers FormView and PreviewView call with the whole form's values.
|
|
// Fixtures are neutral acme.* data; no application names appear in
|
|
// framework tests.
|
|
import { beforeEach, describe, expect, it } from 'vitest'
|
|
import type { AdminRecord, FormField } from '../../src/api/types'
|
|
import { setBundle } from '../../src/app/i18n'
|
|
import { applyTriggers, triggerState } from '../../src/components/form/trigger'
|
|
|
|
const strings = {
|
|
'backend::lang.form.trigger_disabled_on': { other: 'Not editable while “:field” is on.' },
|
|
'backend::lang.form.trigger_disabled_off': { other: 'Not editable while “:field” is off.' },
|
|
}
|
|
|
|
const control: FormField = { name: 'is_admin', type: 'switch', label: 'Administrator' }
|
|
|
|
function triggered(action: 'disable' | 'hide', condition: 'checked' | 'unchecked', name = 'notes'): FormField {
|
|
return { name, type: 'textarea', label: 'Notes', trigger: { action, field: 'is_admin', condition } }
|
|
}
|
|
|
|
const labelOf = (name: string) => (name === 'is_admin' ? 'Administrator' : name)
|
|
|
|
beforeEach(() => {
|
|
setBundle(strings, 'en')
|
|
})
|
|
|
|
describe('triggerState', () => {
|
|
const present = new Set(['is_admin', 'notes'])
|
|
const on: unknown[] = [true, 1, '1', 'true', 'TRUE', 'yes']
|
|
const off: unknown[] = [false, 0, '0', '', 'false', null, undefined]
|
|
|
|
it('a checked trigger is active while the controlling value is on', () => {
|
|
for (const value of on) {
|
|
expect(triggerState(triggered('disable', 'checked'), { is_admin: value }, present), String(value)).toBe('active')
|
|
}
|
|
for (const value of off) {
|
|
expect(triggerState(triggered('disable', 'checked'), { is_admin: value }, present), String(value)).toBe('inactive')
|
|
}
|
|
})
|
|
|
|
it('an unchecked trigger is active while the controlling value is off', () => {
|
|
for (const value of on) {
|
|
expect(triggerState(triggered('hide', 'unchecked'), { is_admin: value }, present), String(value)).toBe('inactive')
|
|
}
|
|
for (const value of off) {
|
|
expect(triggerState(triggered('hide', 'unchecked'), { is_admin: value }, present), String(value)).toBe('active')
|
|
}
|
|
})
|
|
|
|
it('a missing value counts as off', () => {
|
|
expect(triggerState(triggered('disable', 'checked'), {}, present)).toBe('inactive')
|
|
expect(triggerState(triggered('disable', 'unchecked'), {}, present)).toBe('active')
|
|
})
|
|
|
|
it('a controlling field absent from the schema counts as off, whatever the values hold', () => {
|
|
const absent = new Set(['notes'])
|
|
expect(triggerState(triggered('disable', 'checked'), { is_admin: true }, absent)).toBe('inactive')
|
|
expect(triggerState(triggered('hide', 'unchecked'), { is_admin: true }, absent)).toBe('active')
|
|
})
|
|
|
|
it('a field without a trigger is always inactive', () => {
|
|
expect(triggerState({ name: 'title', type: 'text' }, { is_admin: true }, present)).toBe('inactive')
|
|
})
|
|
})
|
|
|
|
describe('applyTriggers', () => {
|
|
it('disable makes an active field a read-only copy with the note naming the controlling field', () => {
|
|
const notes = triggered('disable', 'checked')
|
|
const fields = [control, notes]
|
|
const values: AdminRecord = { is_admin: true }
|
|
const result = applyTriggers(fields, values, labelOf)
|
|
expect(result.fields.map((field) => field.name)).toEqual(['is_admin', 'notes'])
|
|
expect(result.fields[1]!.readOnly).toBe(true)
|
|
expect(result.fields[1]).not.toBe(notes)
|
|
expect(result.notes).toEqual({ notes: 'Not editable while “Administrator” is on.' })
|
|
// The inputs are not changed.
|
|
expect(notes.readOnly).toBeUndefined()
|
|
expect(fields).toHaveLength(2)
|
|
expect(values).toEqual({ is_admin: true })
|
|
})
|
|
|
|
it('an unchecked disable trigger uses the off note', () => {
|
|
const result = applyTriggers([control, triggered('disable', 'unchecked')], { is_admin: false }, labelOf)
|
|
expect(result.notes).toEqual({ notes: 'Not editable while “Administrator” is off.' })
|
|
})
|
|
|
|
it('an inactive trigger leaves the field as it is, without a note', () => {
|
|
const notes = triggered('disable', 'checked')
|
|
const result = applyTriggers([control, notes], { is_admin: false }, labelOf)
|
|
expect(result.fields[1]).toBe(notes)
|
|
expect(result.notes).toEqual({})
|
|
})
|
|
|
|
it('hide removes an active field and keeps the order of the rest', () => {
|
|
const title: FormField = { name: 'title', type: 'text' }
|
|
const result = applyTriggers([title, control, triggered('hide', 'checked'), { name: 'body', type: 'text' }], { is_admin: '1' }, labelOf)
|
|
expect(result.fields.map((field) => field.name)).toEqual(['title', 'is_admin', 'body'])
|
|
expect(result.notes).toEqual({})
|
|
})
|
|
|
|
it('a controlling field missing from the fields leaves a checked trigger inactive', () => {
|
|
const notes = triggered('disable', 'checked')
|
|
const result = applyTriggers([notes], { is_admin: true }, labelOf)
|
|
expect(result.fields).toEqual([notes])
|
|
expect(result.fields[0]!.readOnly).toBeUndefined()
|
|
expect(result.notes).toEqual({})
|
|
})
|
|
|
|
it('several triggers on one switch apply together', () => {
|
|
const result = applyTriggers(
|
|
[control, triggered('disable', 'checked', 'a'), triggered('hide', 'checked', 'b'), triggered('hide', 'unchecked', 'c')],
|
|
{ is_admin: 1 },
|
|
labelOf,
|
|
)
|
|
expect(result.fields.map((field) => [field.name, !!field.readOnly])).toEqual([
|
|
['is_admin', false],
|
|
['a', true],
|
|
['c', false],
|
|
])
|
|
expect(Object.keys(result.notes)).toEqual(['a'])
|
|
})
|
|
})
|