Files
hackerspace.sh/__tests__/forms.test.ts
T
Moheeb Zara 295d220849 feat(forms): onboarding form/waiver step with required enforcement (Phase 4)
- scripts/027 (idempotent CHECK replace) + schema.sql Section 14: allow
  space_onboarding_steps.step_type = 'form'; referenced form id lives in
  the step's existing config.form_id (no new column)
- finishOnboarding: a required form step is satisfied when a submission
  for config.form_id by the member exists (any version — re-sign is
  non-blocking / auto-satisfy); fails open if the form is missing or
  unpublished so a misconfig cannot trap a member. Probe uses the
  service client (submissions are forms.manage-only under RLS)
- onboarding page enriches form steps with the published form def +
  prior-submission flag; onboarding-flow renders FormRenderer + waiver
  legal text/consent and submits via submitForm then marks the step done
- customize: forms fetched and threaded to the onboarding panel; new
  "+ Form step" button + form picker writing config.form_id
- validations/types/casts include 'form'; forms.test.ts +2
- docs: DATABASE_SCHEMA/DB_SCHEMA_MAP/API_REFERENCE/ARCHITECTURE/HANDOFF

287/287 tests, build exit 0. No deploy.
2026-05-16 15:15:17 -07:00

143 lines
4.8 KiB
TypeScript

import { describe, it, expect } from 'vitest'
import {
createFormSchema,
updateFormSchema,
submitFormSchema,
formSchemaArray,
formFieldSchema,
onboardingStepTypeSchema,
createOnboardingStepSchema,
} from '@/lib/validations'
import { parseFormSchema, validateAnswers, type FormField } from '@/lib/forms-schema'
describe('form field schema', () => {
it('accepts a valid text field and defaults required to false', () => {
const r = formFieldSchema.safeParse({ key: 'full_name', type: 'short_text', label: 'Name' })
expect(r.success).toBe(true)
if (r.success) expect(r.data.required).toBe(false)
})
it('rejects a select field with no options', () => {
const r = formFieldSchema.safeParse({ key: 'tier', type: 'select', label: 'Tier' })
expect(r.success).toBe(false)
})
it('rejects an invalid field key', () => {
const r = formFieldSchema.safeParse({ key: 'Full Name', type: 'short_text', label: 'X' })
expect(r.success).toBe(false)
})
it('rejects duplicate field keys in the array', () => {
const r = formSchemaArray.safeParse([
{ key: 'a', type: 'short_text', label: 'A' },
{ key: 'a', type: 'short_text', label: 'A again' },
])
expect(r.success).toBe(false)
})
})
describe('createFormSchema', () => {
it('applies defaults', () => {
const r = createFormSchema.safeParse({ slug: 'liability-waiver', title: 'Waiver' })
expect(r.success).toBe(true)
if (r.success) {
expect(r.data.kind).toBe('form')
expect(r.data.visibility).toBe('members')
expect(r.data.schema).toEqual([])
}
})
it('rejects an uppercase or hyphen-edged slug', () => {
expect(createFormSchema.safeParse({ slug: 'Waiver', title: 'x' }).success).toBe(false)
expect(createFormSchema.safeParse({ slug: '-waiver', title: 'x' }).success).toBe(false)
expect(createFormSchema.safeParse({ slug: 'waiver-', title: 'x' }).success).toBe(false)
})
})
describe('submitFormSchema', () => {
it('requires formId or slug', () => {
expect(submitFormSchema.safeParse({ answers: {} }).success).toBe(false)
expect(submitFormSchema.safeParse({ slug: 'a', answers: {} }).success).toBe(true)
})
})
describe('updateFormSchema', () => {
it('does not accept a slug change', () => {
const r = updateFormSchema.safeParse({
formId: '11111111-1111-1111-1111-111111111111',
slug: 'new-slug',
})
// slug is simply stripped (not in the schema), so parse still succeeds
expect(r.success).toBe(true)
if (r.success) expect('slug' in r.data).toBe(false)
})
})
const fields: FormField[] = [
{ key: 'full_name', type: 'short_text', label: 'Full name', required: true },
{ key: 'agree', type: 'checkbox', label: 'I agree', required: true },
{ key: 'shirt', type: 'select', label: 'Shirt', required: false, options: ['S', 'M', 'L'] },
{ key: 'contact', type: 'email', label: 'Email', required: false },
]
describe('validateAnswers', () => {
it('errors when a required field is missing', () => {
const r = validateAnswers(fields, { agree: true })
expect(r.ok).toBe(false)
if (!r.ok) expect(r.error).toMatch(/Full name/)
})
it('errors when a required checkbox is not checked', () => {
const r = validateAnswers(fields, { full_name: 'Ada', agree: false })
expect(r.ok).toBe(false)
})
it('rejects a select value outside the options', () => {
const r = validateAnswers(fields, { full_name: 'Ada', agree: true, shirt: 'XL' })
expect(r.ok).toBe(false)
})
it('drops unknown keys and normalizes', () => {
const r = validateAnswers(fields, {
full_name: ' Ada ',
agree: 'on',
contact: 'Ada@Example.COM',
injected: 'evil',
})
expect(r.ok).toBe(true)
if (r.ok) {
expect(r.value).toEqual({ full_name: 'Ada', agree: true, contact: 'ada@example.com' })
expect('injected' in r.value).toBe(false)
}
})
})
describe('onboarding form step (Phase 4)', () => {
it('onboardingStepTypeSchema accepts "form" and rejects unknown', () => {
expect(onboardingStepTypeSchema.safeParse('form').success).toBe(true)
expect(onboardingStepTypeSchema.safeParse('content').success).toBe(true)
expect(onboardingStepTypeSchema.safeParse('quiz').success).toBe(false)
})
it('createOnboardingStepSchema accepts a form step with a form_id config', () => {
const r = createOnboardingStepSchema.safeParse({
step_type: 'form',
title: 'Sign the liability waiver',
config: { form_id: '11111111-1111-1111-1111-111111111111' },
})
expect(r.success).toBe(true)
})
})
describe('parseFormSchema', () => {
it('returns [] for malformed stored schema', () => {
expect(parseFormSchema('not an array')).toEqual([])
expect(parseFormSchema([{ key: 'a' }])).toEqual([])
})
it('round-trips a valid schema', () => {
const raw = [{ key: 'a', type: 'short_text', label: 'A' }]
expect(parseFormSchema(raw)).toHaveLength(1)
})
})