Files
summercms/admin/tests/form/registry.test.ts
Jakub Zych e3b3475b1c feat(14.2.1-03): document ML fields and regenerate admin artifacts
- Document markdown, mltext, and mlmarkdown plus TranslationWriter save semantics
- Extend the SPA registry tests and rebuild committed boardwalk dist

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-10-06 13:29:00 +02:00

214 lines
8.1 KiB
TypeScript

import { describe, expect, it } from 'vitest'
import CheckboxField from '../../src/components/form/fields/CheckboxField.vue'
import DropdownField from '../../src/components/form/fields/DropdownField.vue'
import NumberField from '../../src/components/form/fields/NumberField.vue'
import RelationField from '../../src/components/form/fields/RelationField.vue'
import SwitchField from '../../src/components/form/fields/SwitchField.vue'
import TextField from '../../src/components/form/fields/TextField.vue'
import TextareaField from '../../src/components/form/fields/TextareaField.vue'
import PartialField from '../../src/components/form/fields/PartialField.vue'
import MarkdownField from '../../src/components/form/fields/MarkdownField.vue'
import MLMarkdownField from '../../src/components/form/fields/MLMarkdownField.vue'
import MLTextField from '../../src/components/form/fields/MLTextField.vue'
import PasswordField from '../../src/components/form/fields/PasswordField.vue'
import PermissionEditorField from '../../src/components/form/fields/PermissionEditorField.vue'
import UnsupportedField from '../../src/components/form/fields/UnsupportedField.vue'
import WidgetField from '../../src/components/form/fields/WidgetField.vue'
import RelationManager from '../../src/components/relation/RelationManager.vue'
import {
controlAttributes,
controlClass,
groupLabelled,
isRegistered,
needsRecord,
ownsLabel,
RELATION_MANAGER,
rendererFor,
toggleOn,
toggleValue,
} from '../../src/components/form/registry'
import * as control from '../../src/components/form/control'
import { formField } from '../fixtures/typed'
describe('field renderer registry (D-05)', () => {
it.each([
['text', TextField],
['textarea', TextareaField],
['number', NumberField],
['dropdown', DropdownField],
['switch', SwitchField],
['checkbox', CheckboxField],
['relation', RelationField],
[RELATION_MANAGER, RelationManager],
])('renders %s with its control', (type, component) => {
expect(rendererFor(type)).toBe(component)
})
it.each(['colorpicker', 'codeeditor', '', 'TEXT', 'constructor'])('falls back to UnsupportedField for %j', (type) => {
expect(rendererFor(type)).toBe(UnsupportedField)
expect(isRegistered(type)).toBe(false)
})
it('edits values of every control type except the record-bound relation manager', () => {
expect(RELATION_MANAGER).toBe('relation-manager')
for (const type of ['text', 'textarea', 'number', 'dropdown', 'switch', 'checkbox', 'relation']) {
expect(isRegistered(type), type).toBe(true)
expect(needsRecord(type), type).toBe(false)
}
expect(isRegistered(RELATION_MANAGER)).toBe(false)
expect(needsRecord(RELATION_MANAGER)).toBe(true)
})
it('lets toggle cards and the relation manager render their own label', () => {
expect(ownsLabel('switch')).toBe(true)
expect(ownsLabel('checkbox')).toBe(true)
expect(ownsLabel(RELATION_MANAGER)).toBe(true)
expect(ownsLabel('text')).toBe(false)
expect(ownsLabel('relation')).toBe(false)
})
})
describe('plugin extension types (Phase 10.1, D-09)', () => {
it('registers widget and partial with their controls', () => {
expect(rendererFor('widget')).toBe(WidgetField)
expect(rendererFor('partial')).toBe(PartialField)
})
it.each(['widget', 'partial'])('treats %s as valueless, rendered on create and group-labelled', (type) => {
expect(isRegistered(type)).toBe(false)
expect(needsRecord(type)).toBe(false)
expect(groupLabelled(type)).toBe(true)
expect(ownsLabel(type)).toBe(false)
})
it('labels every other type as a control, not a group', () => {
for (const type of ['text', 'textarea', 'number', 'dropdown', 'switch', 'checkbox', 'relation', RELATION_MANAGER, 'colorpicker']) {
expect(groupLabelled(type), type).toBe(false)
}
})
})
describe('control attributes', () => {
it('passes only allow-listed Winter attributes, lower-cased, and drops null and false', () => {
const field = formField('name')
field.attributes = {
placeholder: 'Type…',
MaxLength: 20,
readonly: true,
spellcheck: false,
pattern: null,
onclick: 'alert(1)',
onfocus: 'x',
style: 'color:red',
class: 'evil',
id: 'hijack',
autofocus: true,
}
expect(controlAttributes(field)).toEqual({ placeholder: 'Type…', maxlength: 20, readonly: true })
})
it('answers no attributes without a map', () => {
expect(controlAttributes(formField('code'))).toEqual({})
})
it('switches the border to danger when invalid', () => {
expect(controlClass(true)).toContain('border-danger')
expect(controlClass(false)).toContain('border-border-strong')
expect(controlClass(undefined)).not.toContain('border-danger')
})
})
describe('toggle values', () => {
it.each([
[true, true],
[1, true],
['1', true],
['true', true],
['yes', true],
[false, false],
[0, false],
['0', false],
['', false],
['False', false],
[null, false],
[undefined, false],
])('%j is %s', (value, on) => {
expect(toggleOn(value)).toBe(on)
})
it('keeps a numeric stored value numeric and sends booleans otherwise', () => {
expect(toggleValue(0, true)).toBe(1)
expect(toggleValue(1, false)).toBe(0)
expect(toggleValue(false, true)).toBe(true)
expect(toggleValue('1', false)).toBe(false)
expect(toggleValue(undefined, true)).toBe(true)
})
})
describe('control module', () => {
it('is what the registry re-exports, so field controls never import the registry', () => {
expect(controlAttributes).toBe(control.controlAttributes)
expect(controlClass).toBe(control.controlClass)
expect(toggleOn).toBe(control.toggleOn)
expect(toggleValue).toBe(control.toggleValue)
})
})
describe('Phase 12.1 field types (UI-SPEC S5 and S7)', () => {
it('registers password and permissioneditor as editable types with their own controls', () => {
expect(rendererFor('password')).toBe(PasswordField)
expect(rendererFor('permissioneditor')).toBe(PermissionEditorField)
expect(isRegistered('password')).toBe(true)
expect(isRegistered('permissioneditor')).toBe(true)
// Both hold a form value: they are part of the save body.
expect(needsRecord('password')).toBe(false)
expect(needsRecord('permissioneditor')).toBe(false)
})
it('labels a password as an input and a permission editor as a group', () => {
expect(ownsLabel('password')).toBe(false)
expect(ownsLabel('permissioneditor')).toBe(false)
expect(groupLabelled('password')).toBe(false)
expect(groupLabelled('permissioneditor')).toBe(true)
})
it('still answers the unsupported control for a type it does not know', () => {
for (const type of ['passwordeditor', 'Password', 'permission', '']) {
expect(isRegistered(type), type).toBe(false)
expect(rendererFor(type), type).not.toBe(PasswordField)
expect(rendererFor(type), type).not.toBe(PermissionEditorField)
}
})
})
describe('markdown and multilingual field types (Phase 14.2.1)', () => {
it('registers markdown, mltext and mlmarkdown as editable types with their own controls', () => {
expect(rendererFor('markdown')).toBe(MarkdownField)
expect(rendererFor('mltext')).toBe(MLTextField)
expect(rendererFor('mlmarkdown')).toBe(MLMarkdownField)
expect(isRegistered('markdown')).toBe(true)
expect(isRegistered('mltext')).toBe(true)
expect(isRegistered('mlmarkdown')).toBe(true)
expect(needsRecord('markdown')).toBe(false)
expect(needsRecord('mltext')).toBe(false)
expect(needsRecord('mlmarkdown')).toBe(false)
})
it('labels markdown as an input and ML fields as groups', () => {
expect(ownsLabel('markdown')).toBe(false)
expect(ownsLabel('mltext')).toBe(false)
expect(ownsLabel('mlmarkdown')).toBe(false)
expect(groupLabelled('markdown')).toBe(false)
expect(groupLabelled('mltext')).toBe(true)
expect(groupLabelled('mlmarkdown')).toBe(true)
})
it.each(['mlunknown', 'MLTEXT', 'richtext', 'markdowneditor'])(
'falls back to UnsupportedField for unknown ML type %j',
(type) => {
expect(isRegistered(type)).toBe(false)
expect(rendererFor(type)).toBe(UnsupportedField)
},
)
})