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