Files
summercms/admin/tests/form/trigger.test.ts
Jakub Zych 4c61a180d0 test(19.6.1-03): SPA trigger, record lock and footer wrap tests
- 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
2026-10-22 12:18:34 +02:00

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'])
})
})