Files
crewli/apps/portal/tests/unit/formatFieldValue.spec.ts
bert.hausmans e95f9a75f6 fix(portal): review display, hover overlay, and drag ghost for complex field types
- Extract formatFieldValue helper for shared use between review step
  and confirmation page — one source of truth for TAG_PICKER,
  AVAILABILITY_PICKER, and SECTION_PRIORITY display, so the raw-ID
  and [object Object] leaks from two parallel stringifiers can't
  regress on either side.
- TAG_PICKER: lookup via field.available_tags (server-inlined).
- AVAILABILITY_PICKER: lookup via usePublicFormTimeSlots, strip
  seconds. "Laden…" while the cache warms.
- SECTION_PRIORITY: defensive shape-guard prevents [object Object]
  leaks, sorted priority-prefixed rendering ("1. Bar, 2. Hospitality").
- Subtle primary-tinted hover (4% primary, primary border) replacing
  the near-black Vuetify default overlay on unranked section cards.
- Explicit ghost-class / drag-class / chosen-class on vuedraggable
  with solid drag-clone + elevation shadow and a 30%-opacity silhouette
  at the origin, so mid-drag text no longer overlaps.
- 17 new formatFieldValue unit assertions + 2 new FieldSectionPriority
  assertions locking in the draggable classes and the disabled-card
  toggle at max.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-23 22:12:47 +02:00

189 lines
6.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { describe, expect, it } from 'vitest'
import { formatFieldValue } from '@/composables/formatFieldValue'
import { FormFieldType } from '@/types/formBuilder'
import type {
AvailableTag,
PublicFormField,
PublicFormSectionOption,
PublicFormTimeSlot,
} from '@/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 (startend)" 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:0016: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:0023: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')
})
})
})