Moves formBuilder types, formValidation, useConditionalLogic, useFormSteps, and formatFieldValue from apps/portal/src to packages/form-schema/src. Adds @form-schema path alias to both apps/portal and apps/app. Vue field components remain per-app to allow independent visual evolution. Behavior-neutral: all 35 Vitest tests green. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
189 lines
6.6 KiB
TypeScript
189 lines
6.6 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
||
import { formatFieldValue } from '@form-schema/composables/formatFieldValue'
|
||
import { FormFieldType } from '@form-schema/types/formBuilder'
|
||
import type {
|
||
AvailableTag,
|
||
PublicFormField,
|
||
PublicFormSectionOption,
|
||
PublicFormTimeSlot,
|
||
} from '@form-schema/types/formBuilder'
|
||
|
||
function field(partial: Partial<PublicFormField> = {}): PublicFormField {
|
||
return {
|
||
id: partial.id ?? 'f',
|
||
slug: partial.slug ?? 'slug',
|
||
field_type: partial.field_type ?? FormFieldType.TEXT,
|
||
label: partial.label ?? 'Label',
|
||
help_text: partial.help_text ?? null,
|
||
options: partial.options ?? null,
|
||
available_tags: partial.available_tags ?? null,
|
||
validation_rules: partial.validation_rules ?? null,
|
||
is_required: partial.is_required ?? false,
|
||
display_width: partial.display_width ?? 'full',
|
||
conditional_logic: partial.conditional_logic ?? null,
|
||
sort_order: partial.sort_order ?? 0,
|
||
form_schema_section_id: partial.form_schema_section_id ?? null,
|
||
}
|
||
}
|
||
|
||
function tag(partial: Partial<AvailableTag>): AvailableTag {
|
||
return { id: partial.id ?? 't_1', name: partial.name ?? 'Tag', category: partial.category ?? '' }
|
||
}
|
||
|
||
function timeSlot(partial: Partial<PublicFormTimeSlot>): PublicFormTimeSlot {
|
||
return {
|
||
id: partial.id ?? 'ts_1',
|
||
name: partial.name ?? 'Vrijdag avond',
|
||
date: partial.date ?? '2026-07-10',
|
||
start_time: partial.start_time ?? '18:00:00',
|
||
end_time: partial.end_time ?? '23:00:00',
|
||
duration_hours: partial.duration_hours ?? 5,
|
||
event_id: partial.event_id ?? 'evt_1',
|
||
event_name: partial.event_name ?? 'Event',
|
||
}
|
||
}
|
||
|
||
function section(partial: Partial<PublicFormSectionOption>): PublicFormSectionOption {
|
||
return {
|
||
id: partial.id ?? 's_1',
|
||
name: partial.name ?? 'Section',
|
||
category: partial.category ?? null,
|
||
icon: partial.icon ?? null,
|
||
registration_description: partial.registration_description ?? null,
|
||
}
|
||
}
|
||
|
||
describe('formatFieldValue', () => {
|
||
describe('empty values', () => {
|
||
it.each<[unknown]>([
|
||
[null],
|
||
[undefined],
|
||
[''],
|
||
[[]],
|
||
])('returns "—" for empty value %s on any field type', v => {
|
||
expect(formatFieldValue(field({ field_type: FormFieldType.TEXT }), v, [], [])).toBe('—')
|
||
})
|
||
})
|
||
|
||
describe('TAG_PICKER', () => {
|
||
it('maps IDs to tag names using field.available_tags', () => {
|
||
const f = field({
|
||
field_type: FormFieldType.TAG_PICKER,
|
||
available_tags: [tag({ id: 'a', name: 'Tapper' }), tag({ id: 'b', name: 'Barista' })],
|
||
})
|
||
|
||
expect(formatFieldValue(f, ['a', 'b'], undefined, undefined)).toBe('Tapper, Barista')
|
||
})
|
||
|
||
it('labels unknown tag IDs as "(onbekende tag)"', () => {
|
||
const f = field({
|
||
field_type: FormFieldType.TAG_PICKER,
|
||
available_tags: [tag({ id: 'a', name: 'Tapper' })],
|
||
})
|
||
|
||
expect(formatFieldValue(f, ['a', 'zzz'], undefined, undefined)).toBe('Tapper, (onbekende tag)')
|
||
})
|
||
})
|
||
|
||
describe('AVAILABILITY_PICKER', () => {
|
||
it('maps IDs to "name (start–end)" with seconds stripped', () => {
|
||
const f = field({ field_type: FormFieldType.AVAILABILITY_PICKER })
|
||
const slots = [timeSlot({ id: 'x', name: 'Vrijwilligers afbraak zondag', start_time: '10:00:00', end_time: '16:00:00' })]
|
||
|
||
expect(formatFieldValue(f, ['x'], slots, undefined))
|
||
.toBe('Vrijwilligers afbraak zondag (10:00–16:00)')
|
||
})
|
||
|
||
it('labels unknown time_slot IDs as "(onbekend tijdslot)"', () => {
|
||
const f = field({ field_type: FormFieldType.AVAILABILITY_PICKER })
|
||
const slots = [timeSlot({ id: 'x' })]
|
||
|
||
expect(formatFieldValue(f, ['x', 'y'], slots, undefined))
|
||
.toBe('Vrijdag avond (18:00–23:00), (onbekend tijdslot)')
|
||
})
|
||
|
||
it('returns "Laden…" while the time-slots query is still fetching', () => {
|
||
const f = field({ field_type: FormFieldType.AVAILABILITY_PICKER })
|
||
|
||
expect(formatFieldValue(f, ['x'], undefined, undefined)).toBe('Laden…')
|
||
})
|
||
})
|
||
|
||
describe('SECTION_PRIORITY', () => {
|
||
it('renders "N. Name, …" sorted by priority', () => {
|
||
const f = field({ field_type: FormFieldType.SECTION_PRIORITY })
|
||
const sections = [
|
||
section({ id: 's_1', name: 'Hoofdpodium Bar' }),
|
||
section({ id: 's_2', name: 'Theatertent Bar' }),
|
||
]
|
||
|
||
expect(formatFieldValue(
|
||
f,
|
||
[{ section_id: 's_1', priority: 1 }, { section_id: 's_2', priority: 2 }],
|
||
undefined,
|
||
sections,
|
||
)).toBe('1. Hoofdpodium Bar, 2. Theatertent Bar')
|
||
})
|
||
|
||
it('re-sorts unordered input by priority before rendering', () => {
|
||
const f = field({ field_type: FormFieldType.SECTION_PRIORITY })
|
||
const sections = [section({ id: 's_1', name: 'A' }), section({ id: 's_2', name: 'B' })]
|
||
|
||
expect(formatFieldValue(
|
||
f,
|
||
[{ section_id: 's_2', priority: 2 }, { section_id: 's_1', priority: 1 }],
|
||
undefined,
|
||
sections,
|
||
)).toBe('1. A, 2. B')
|
||
})
|
||
|
||
it('labels unknown section IDs as "(onbekende sectie)"', () => {
|
||
const f = field({ field_type: FormFieldType.SECTION_PRIORITY })
|
||
const sections = [section({ id: 's_1', name: 'Known' })]
|
||
|
||
expect(formatFieldValue(
|
||
f,
|
||
[{ section_id: 's_1', priority: 1 }, { section_id: 'missing', priority: 2 }],
|
||
undefined,
|
||
sections,
|
||
)).toBe('1. Known, 2. (onbekende sectie)')
|
||
})
|
||
|
||
it('returns "—" when the value shape is malformed (defensive guard)', () => {
|
||
const f = field({ field_type: FormFieldType.SECTION_PRIORITY })
|
||
|
||
// Flat string[] — wrong shape, must not leak [object Object].
|
||
expect(formatFieldValue(f, ['s_1', 's_2'], undefined, [section({ id: 's_1', name: 'Known' })])).toBe('—')
|
||
})
|
||
|
||
it('returns "Laden…" while the sections query is still fetching', () => {
|
||
const f = field({ field_type: FormFieldType.SECTION_PRIORITY })
|
||
|
||
expect(formatFieldValue(
|
||
f,
|
||
[{ section_id: 's_1', priority: 1 }],
|
||
undefined,
|
||
undefined,
|
||
)).toBe('Laden…')
|
||
})
|
||
})
|
||
|
||
describe('scalars', () => {
|
||
it('formats BOOLEAN true as "Ja", false as "Nee"', () => {
|
||
const f = field({ field_type: FormFieldType.BOOLEAN })
|
||
expect(formatFieldValue(f, true, undefined, undefined)).toBe('Ja')
|
||
expect(formatFieldValue(f, false, undefined, undefined)).toBe('Nee')
|
||
})
|
||
|
||
it('stringifies unknown-type array values via join', () => {
|
||
const f = field({ field_type: FormFieldType.MULTISELECT })
|
||
expect(formatFieldValue(f, ['A', 'B'], undefined, undefined)).toBe('A, B')
|
||
})
|
||
|
||
it('stringifies scalars for text-like types', () => {
|
||
const f = field({ field_type: FormFieldType.TEXT })
|
||
expect(formatFieldValue(f, 'hello', undefined, undefined)).toBe('hello')
|
||
})
|
||
})
|
||
})
|