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 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', 'fileupload', '', '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) }) })