diff --git a/examples/remix/app/routes/login-fetcher.tsx b/examples/remix/app/routes/login-fetcher.tsx index e1a5c34f..62c3faf3 100644 --- a/examples/remix/app/routes/login-fetcher.tsx +++ b/examples/remix/app/routes/login-fetcher.tsx @@ -12,22 +12,22 @@ interface SignupForm { function parseFormData(formData: FormData) { return parse(formData, { resolve({ email, password, confirmPassword }) { - const error: Record = {}; + const error: Record = {}; if (!email) { - error.email = 'Email is required'; + error.email = ['Email is required']; } else if (!email.includes('@')) { - error.email = 'Email is invalid'; + error.email = ['Email is invalid']; } if (!password) { - error.password = 'Password is required'; + error.password = ['Password is required']; } if (!confirmPassword) { - error.confirmPassword = 'Confirm password is required'; + error.confirmPassword = ['Confirm password is required']; } else if (confirmPassword !== password) { - error.confirmPassword = 'Password does not match'; + error.confirmPassword = ['Password does not match']; } if (error.email || error.password || error.confirmPassword) { diff --git a/packages/conform-dom/formdata.ts b/packages/conform-dom/formdata.ts index ee5417e2..12136f59 100644 --- a/packages/conform-dom/formdata.ts +++ b/packages/conform-dom/formdata.ts @@ -26,25 +26,24 @@ export function getFormData( * ``` */ export function getPaths(name: string): Array { - const pattern = /(\w*)\[(\d+)\]/; - if (!name) { return []; } - return name.split('.').flatMap((key) => { - const matches = pattern.exec(key); - - if (!matches) { - return key; - } - - if (matches[1] === '') { - return Number(matches[2]); - } - - return [matches[1], Number(matches[2])]; - }); + return name + .split(/\.|(\[\d*\])/) + .reduce>((result, segment) => { + if (typeof segment !== 'undefined' && segment !== '') { + if (segment.startsWith('[') && segment.endsWith(']')) { + const index = segment.slice(1, -1); + + result.push(Number(index)); + } else { + result.push(segment); + } + } + return result; + }, []); } /** @@ -72,22 +71,23 @@ export function formatPaths(paths: Array): string { * Assign a value to a target object by following the paths on the name */ export function setValue( - target: any, + target: Record, name: string, valueFn: (prev?: unknown) => any, ): void { - let paths = getPaths(name); - let length = paths.length; - let lastIndex = length - 1; + const paths = getPaths(name); + const length = paths.length; + const lastIndex = length - 1; + let index = -1; let pointer = target; while (pointer != null && ++index < length) { - let key = paths[index]; - let next = paths[index + 1]; - let newValue = + const key = paths[index] as string | number; + const nextKey = paths[index + 1]; + const newValue = index != lastIndex - ? pointer[key] ?? (typeof next === 'number' ? [] : {}) + ? pointer[key] ?? (typeof nextKey === 'number' ? [] : {}) : valueFn(pointer[key]); pointer[key] = newValue; @@ -144,17 +144,13 @@ export function resolve( /** * Format the error messages into a validation message */ -export function getValidationMessage(errors?: string | string[]): string { - return ([] as string[]).concat(errors ?? []).join(String.fromCharCode(31)); +export function getValidationMessage(errors?: string[]): string { + return errors?.join(String.fromCharCode(31)) ?? ''; } /** * Retrieve the error messages from the validation message */ export function getErrors(validationMessage: string | undefined): string[] { - if (!validationMessage) { - return []; - } - - return validationMessage.split(String.fromCharCode(31)); + return validationMessage?.split(String.fromCharCode(31)) ?? []; } diff --git a/packages/conform-dom/parse.ts b/packages/conform-dom/parse.ts index 8cadb81e..594198f7 100644 --- a/packages/conform-dom/parse.ts +++ b/packages/conform-dom/parse.ts @@ -4,7 +4,7 @@ import { INTENT, getIntent, parseIntent, updateList } from './intent.js'; export type Submission = { intent: string; payload: Record; - error: Record; + error: Record; value?: Schema | null; }; @@ -18,7 +18,7 @@ export function parse( resolve?: ( payload: Record, intent: string, - ) => { value?: Schema; error?: Record }; + ) => { value?: Schema; error?: Record }; stripEmptyValue?: boolean; }, ): Submission; @@ -28,7 +28,7 @@ export function parse( resolve?: ( payload: Record, intent: string, - ) => Promise<{ value?: Schema; error?: Record }>; + ) => Promise<{ value?: Schema; error?: Record }>; stripEmptyValue?: boolean; }, ): Promise>; @@ -39,8 +39,8 @@ export function parse( payload: Record, intent: string, ) => - | { value?: Schema; error?: Record } - | Promise<{ value?: Schema; error?: Record }>; + | { value?: Schema; error?: Record } + | Promise<{ value?: Schema; error?: Record }>; stripEmptyValue?: boolean; }, ): Submission | Promise>; @@ -51,8 +51,8 @@ export function parse( payload: Record, intent: string, ) => - | { value?: Schema; error?: Record } - | Promise<{ value?: Schema; error?: Record }>; + | { value?: Schema; error?: Record } + | Promise<{ value?: Schema; error?: Record }>; stripEmptyValue?: boolean; }, ): Submission | Promise> { @@ -83,7 +83,7 @@ export function parse( const result = options.resolve(submission.payload, submission.intent); const mergeResolveResult = (resolved: { - error?: Record; + error?: Record; value?: Schema; }) => { return { diff --git a/packages/conform-dom/tsconfig.json b/packages/conform-dom/tsconfig.json index 05ad47f2..f0160e86 100644 --- a/packages/conform-dom/tsconfig.json +++ b/packages/conform-dom/tsconfig.json @@ -4,6 +4,7 @@ "target": "ES2020", "moduleResolution": "node16", "allowSyntheticDefaultImports": false, + "noUncheckedIndexedAccess": true, "strict": true, "declaration": true, "emitDeclarationOnly": true, diff --git a/packages/conform-react/README.md b/packages/conform-react/README.md index f208b96b..136e5764 100644 --- a/packages/conform-react/README.md +++ b/packages/conform-react/README.md @@ -392,16 +392,16 @@ import { parse } from '@conform-to/react'; const formData = new FormData(); const submission = parse(formData, { resolve({ email, password }) { - const error: Record = {}; + const error: Record = {}; if (typeof email !== 'string') { - error.email = 'Email is required'; + error.email = ['Email is required']; } else if (!/^[^@]+@[^@]+$/.test(email)) { - error.email = 'Email is invalid'; + error.email = ['Email is invalid']; } if (typeof password !== 'string') { - error.password = 'Password is required'; + error.password = ['Password is required']; } if (error.email || error.password) { diff --git a/packages/conform-react/hooks.ts b/packages/conform-react/hooks.ts index de0f16e5..e013083c 100644 --- a/packages/conform-react/hooks.ts +++ b/packages/conform-react/hooks.ts @@ -43,7 +43,7 @@ export interface FieldConfig extends FieldConstraint { id?: string; name: string; defaultValue?: FieldValue; - initialError?: Record; + initialError?: Record; form?: string; descriptionId?: string; errorId?: string; @@ -179,24 +179,12 @@ interface FormProps { interface Form { id?: string; errorId?: string; - error: string; + error: string | undefined; errors: string[]; ref: RefObject; props: FormProps; } -/** - * Normalize error to an array of string. - */ -function normalizeError(error: string | string[] | undefined): string[] { - if (!error) { - // This treat both empty string and undefined as no error. - return []; - } - - return ([] as string[]).concat(error); -} - function useNoValidate( defaultNoValidate: boolean | undefined, validateBeforeHydrate: boolean | undefined, @@ -278,7 +266,7 @@ function useFormReporter( function useFormError( ref: RefObject, config: { - initialError: Record | undefined; + initialError: Record | undefined; name?: string; }, ) { @@ -290,10 +278,10 @@ function useFormError( const result: Record = {}; for (const [name, message] of Object.entries(config.initialError)) { - const paths = getPaths(name); + const [path, ...restPaths] = getPaths(name); - if (paths.length === 1) { - result[paths[0]] = normalizeError(message); + if (typeof path !== 'undefined' && restPaths.length === 0) { + result[path] = message; } } @@ -304,43 +292,32 @@ function useFormError( const handleInvalid = (event: Event) => { const form = getFormElement(ref.current); const element = event.target; + const prefix = config.name ?? ''; if ( !isFieldElement(element) || element.form !== form || + !element.name.startsWith(prefix) || !element.dataset.conformTouched ) { return; } - let key: string | number = element.name; - - if (config.name) { - const scopePaths = getPaths(config.name); - const fieldPaths = getPaths(element.name); + const name = element.name.slice(prefix.length); + const [path, ...restPaths] = getPaths(name); - for (let i = 0; i <= scopePaths.length; i++) { - const path = fieldPaths[i]; - - if (i < scopePaths.length) { - // Skip if the field is not in the scope - if (path !== scopePaths[i]) { - return; - } - } else { - key = path; - } - } + if (typeof path === 'undefined' || restPaths.length > 0) { + return; } setError((prev) => { - if (element.validationMessage === getValidationMessage(prev[key])) { + if (element.validationMessage === getValidationMessage(prev[path])) { return prev; } return { ...prev, - [key]: getErrors(element.validationMessage), + [path]: getErrors(element.validationMessage), }; }); @@ -380,8 +357,8 @@ export function useForm< const ref = useFormRef(config.ref); const noValidate = useNoValidate(config.noValidate, config.fallbackNative); const report = useFormReporter(ref, config.lastSubmission); - const [errors, setErrors] = useState(() => - normalizeError(config.lastSubmission?.error['']), + const [errors, setErrors] = useState( + () => config.lastSubmission?.error[''] ?? [], ); const initialError = useMemo(() => { const submission = config.lastSubmission; @@ -393,9 +370,11 @@ export function useForm< const intent = parseIntent(submission.intent); const scope = getScope(intent); - return scope === null - ? submission.error - : { [scope]: submission.error[scope] }; + if (typeof scope !== 'string') { + return submission.error; + } + + return { [scope]: submission.error[scope] ?? [] }; }, [config.lastSubmission]); // This payload from lastSubmission is only useful before hydration // After hydration, any new payload on lastSubmission will be ignored @@ -515,7 +494,7 @@ export function useForm< submission.error, ).reduce<{ errors: string[]; shouldServerValidate: boolean }>( (result, [, error]) => { - for (const message of normalizeError(error)) { + for (const message of error) { if (message === VALIDATION_UNDEFINED) { result.shouldServerValidate = true; } else if (message !== VALIDATION_SKIPPED) { @@ -598,7 +577,7 @@ export interface FieldsetConfig< /** * An object describing the initial error of each field */ - initialError?: Record; + initialError?: Record; /** * An object describing the constraint of each field @@ -659,7 +638,7 @@ export function useFieldset | undefined>( } return result; - }, {} as Record); + }, {} as Record); const field: FieldConfig = { ...constraint, name: fieldsetConfig.name ? `${fieldsetConfig.name}.${key}` : key, @@ -728,7 +707,7 @@ export function useFieldList | undefined>( } setEntries((entries) => { - let list = [...entries]; + const list = [...entries]; switch (intent.payload.operation) { case 'append': @@ -738,7 +717,7 @@ export function useFieldList | undefined>( ...intent.payload, defaultValue: [ // Generate a random key to avoid conflicts - crypto.getRandomValues(new Uint32Array(1))[0].toString(36), + getUniqueKey(), intent.payload.defaultValue, ], }); @@ -805,7 +784,7 @@ export function useFieldList | undefined>( return result; }, - {} as Record, + {} as Record, ); const fieldConfig: FieldConfig< Schema extends Array ? Item : never @@ -846,18 +825,6 @@ interface InputControl { blur: () => void; } -export function useEventListeners(type: string, ref: RefObject) { - useSafeLayoutEffect(() => { - const listener = (event: Event) => {}; - - document.addEventListener(type, listener); - - return () => { - document.removeEventListener(type, listener); - }; - }, [type, ref]); -} - /** * Returns a ref object and a set of helpers that dispatch corresponding dom event. * @@ -1092,8 +1059,8 @@ export function validateConstraint(options: { options?.formatMessages ?? (({ defaultErrors }) => defaultErrors); return parse(formData, { - resolve(payload, intent) { - const error: Record = {}; + resolve() { + const error: Record = {}; const constraintPattern = /^constraint[A-Z][^A-Z]*$/; for (const element of options.form.elements) { if (isFieldElement(element)) { @@ -1141,13 +1108,23 @@ export function validateConstraint(options: { }); } +export function getUniqueKey() { + const [value] = crypto.getRandomValues(new Uint32Array(1)); + + if (!value) { + throw new Error('Fail to generate an unique key'); + } + + return value.toString(36); +} + export function reportSubmission( form: HTMLFormElement, submission: SubmissionResult, ): void { for (const [name, message] of Object.entries(submission.error)) { // There is no need to create a placeholder button if all we want is to reset the error - if (message === '') { + if (message.length === 0) { continue; } @@ -1184,7 +1161,7 @@ export function reportSubmission( for (const element of getFormControls(form)) { const elementName = element.name !== FORM_ERROR_ELEMENT_NAME ? element.name : ''; - const messages = normalizeError(submission.error[elementName]); + const messages = submission.error[elementName] ?? []; if (scope === null || scope === elementName) { element.dataset.conformTouched = 'true'; diff --git a/packages/conform-react/tsconfig.json b/packages/conform-react/tsconfig.json index 05ad47f2..f0160e86 100644 --- a/packages/conform-react/tsconfig.json +++ b/packages/conform-react/tsconfig.json @@ -4,6 +4,7 @@ "target": "ES2020", "moduleResolution": "node16", "allowSyntheticDefaultImports": false, + "noUncheckedIndexedAccess": true, "strict": true, "declaration": true, "emitDeclarationOnly": true, diff --git a/packages/conform-yup/index.ts b/packages/conform-yup/index.ts index d4bf8777..aa863098 100644 --- a/packages/conform-yup/index.ts +++ b/packages/conform-yup/index.ts @@ -120,23 +120,13 @@ export function parse( const resolveError = (error: unknown) => { if (error instanceof yup.ValidationError) { return { - error: error.inner.reduce>( - (result, e) => { - const name = e.path ?? ''; + error: error.inner.reduce>((result, e) => { + const name = e.path ?? ''; - if (typeof result[name] === 'undefined') { - result[name] = e.message; - } else { - result[name] = ([] as string[]).concat( - result[name], - e.message, - ); - } + result[name] = [...(result[name] ?? []), e.message]; - return result; - }, - {}, - ), + return result; + }, {}), }; } diff --git a/packages/conform-yup/tsconfig.json b/packages/conform-yup/tsconfig.json index 05ad47f2..f0160e86 100644 --- a/packages/conform-yup/tsconfig.json +++ b/packages/conform-yup/tsconfig.json @@ -4,6 +4,7 @@ "target": "ES2020", "moduleResolution": "node16", "allowSyntheticDefaultImports": false, + "noUncheckedIndexedAccess": true, "strict": true, "declaration": true, "emitDeclarationOnly": true, diff --git a/packages/conform-zod/index.ts b/packages/conform-zod/index.ts index 8c881a2a..8ee246b5 100644 --- a/packages/conform-zod/index.ts +++ b/packages/conform-zod/index.ts @@ -148,6 +148,7 @@ export function getFieldsetConstraint( typeof nextConstraint[key] !== 'undefined' && prevConstraint[key] === nextConstraint[key] ) { + // @ts-expect-error result[name][key] = prevConstraint[key]; } } @@ -206,9 +207,7 @@ export function parse( : config.schema; const resolveResult = ( result: z.SafeParseReturnType, z.output>, - ): - | { value: z.output } - | { error: Record } => { + ): { value: z.output } | { error: Record } => { if (result.success) { return { value: result.data, @@ -216,15 +215,11 @@ export function parse( } return { - error: result.error.errors.reduce>( + error: result.error.errors.reduce>( (result, e) => { const name = getName(e.path); - if (typeof result[name] === 'undefined') { - result[name] = e.message; - } else { - result[name] = ([] as string[]).concat(result[name], e.message); - } + result[name] = [...(result[name] ?? []), e.message]; return result; }, diff --git a/packages/conform-zod/tsconfig.json b/packages/conform-zod/tsconfig.json index 05ad47f2..f0160e86 100644 --- a/packages/conform-zod/tsconfig.json +++ b/packages/conform-zod/tsconfig.json @@ -4,6 +4,7 @@ "target": "ES2020", "moduleResolution": "node16", "allowSyntheticDefaultImports": false, + "noUncheckedIndexedAccess": true, "strict": true, "declaration": true, "emitDeclarationOnly": true, diff --git a/playground/app/components.tsx b/playground/app/components.tsx index dbe1cdfb..eb3dd02e 100644 --- a/playground/app/components.tsx +++ b/playground/app/components.tsx @@ -4,7 +4,7 @@ import { useEffect, useState } from 'react'; interface PlaygroundProps< Submission extends { - error: Record | null; + error: Record | null; }, > { title: string; diff --git a/playground/app/routes/input-attributes.tsx b/playground/app/routes/input-attributes.tsx index 081ceb11..c2d37164 100644 --- a/playground/app/routes/input-attributes.tsx +++ b/playground/app/routes/input-attributes.tsx @@ -27,7 +27,7 @@ export async function action({ request }: ActionArgs) { return json({ ...submission, error: { - '': 'Submitted', + '': ['Submitted'], }, }); } diff --git a/playground/app/routes/login.tsx b/playground/app/routes/login.tsx index a8817ac6..0fd75a4b 100644 --- a/playground/app/routes/login.tsx +++ b/playground/app/routes/login.tsx @@ -13,14 +13,14 @@ interface Login { function parseLoginForm(formData: FormData) { return parse(formData, { resolve({ email, password }) { - const error: Partial> = {}; + const error: Partial> = {}; if (!email) { - error.email = 'Email is required'; + error.email = ['Email is required']; } if (!password) { - error.password = 'Password is required'; + error.password = ['Password is required']; } if (error.email || error.password) { @@ -50,7 +50,7 @@ export async function action({ request }: ActionArgs) { (submission.value.email !== 'me@edmund.dev' || submission.value.password !== '$eCreTP@ssWord') ) { - submission.error[''] = 'The provided email or password is not valid'; + submission.error[''] = ['The provided email or password is not valid']; } return json({ diff --git a/playground/app/routes/movie.tsx b/playground/app/routes/movie.tsx index 88203663..81d6e144 100644 --- a/playground/app/routes/movie.tsx +++ b/playground/app/routes/movie.tsx @@ -19,24 +19,24 @@ export async function action({ request }: ActionArgs) { const formData = await request.formData(); const submission = parse(formData, { resolve({ title, description, genre, rating }) { - const error: Record = {}; + const error: Record = {}; if (!title) { - error.title = 'Title is required'; + error.title = ['Title is required']; } else if (!title.match(/[0-9a-zA-Z ]{1,20}/)) { - error.title = 'Please enter a valid title'; + error.title = ['Please enter a valid title']; } if (description && description.length < 30) { - error.description = 'Please provides more details'; + error.description = ['Please provides more details']; } if (genre === '') { - error.genre = 'Genre is required'; + error.genre = ['Genre is required']; } if (rating && Number(rating) % 0.5 !== 0) { - error.rating = 'The provided rating is invalid'; + error.rating = ['The provided rating is invalid']; } if (error.title || error.description || error.genre || error.rating) { @@ -93,7 +93,7 @@ export default function MovieForm() { ? ({ form, formData }) => { const submission = parse(formData, { resolve({ title, description, genre, rating }) { - const error: Record = {}; + const error: Record = {}; for (const element of form.elements) { if (!isFieldElement(element)) { @@ -103,24 +103,24 @@ export default function MovieForm() { switch (element.name) { case 'title': if (element.validity.valueMissing) { - error[element.name] = 'Title is required'; + error[element.name] = ['Title is required']; } else if (element.validity.patternMismatch) { - error[element.name] = 'Please enter a valid title'; + error[element.name] = ['Please enter a valid title']; } break; case 'description': if (element.validity.tooShort) { - error[element.name] = 'Please provides more details'; + error[element.name] = ['Please provides more details']; } break; case 'genre': if (element.validity.valueMissing) { - error[element.name] = 'Genre is required'; + error[element.name] = ['Genre is required']; } break; case 'rating': if (element.validity.stepMismatch) { - error[element.name] = 'The provided rating is invalid'; + error[element.name] = ['The provided rating is invalid']; } break; } diff --git a/playground/app/routes/radio-buttons.tsx b/playground/app/routes/radio-buttons.tsx index 6a008992..425a0b61 100644 --- a/playground/app/routes/radio-buttons.tsx +++ b/playground/app/routes/radio-buttons.tsx @@ -10,10 +10,10 @@ interface Schema { function parseForm(formData: FormData) { return parse(formData, { resolve({ answer }) { - const error: Record = {}; + const error: Record = {}; if (!answer) { - error.answer = 'Required'; + error.answer = ['Required']; } if (error.answer) { diff --git a/playground/app/routes/signup.tsx b/playground/app/routes/signup.tsx index 9707ae34..51d02f5d 100644 --- a/playground/app/routes/signup.tsx +++ b/playground/app/routes/signup.tsx @@ -13,27 +13,27 @@ interface Signup { function parseSignupForm(formData: FormData) { return parse(formData, { resolve({ email, password, confirmPassword }) { - const error: Record = {}; + const error: Record = {}; if (!email) { - error.email = 'Email is required'; + error.email = ['Email is required']; } else if ( typeof email !== 'string' || !email.match(/^[^()@\s]+@[\w\d.]+$/) ) { - error.email = 'Email is invalid'; + error.email = ['Email is invalid']; } if (!password) { - error.password = 'Password is required'; + error.password = ['Password is required']; } else if (typeof password === 'string' && password.length < 8) { - error.password = 'Password is too short'; + error.password = ['Password is too short']; } if (!confirmPassword) { - error.confirmPassword = 'Confirm password is required'; + error.confirmPassword = ['Confirm password is required']; } else if (confirmPassword !== password) { - error.confirmPassword = 'The password provided does not match'; + error.confirmPassword = ['The password provided does not match']; } if (error.email || error.password || error.confirmPassword) { diff --git a/tests/conform-yup.spec.ts b/tests/conform-yup.spec.ts index 65f3330c..1a1d4248 100644 --- a/tests/conform-yup.spec.ts +++ b/tests/conform-yup.spec.ts @@ -64,16 +64,16 @@ test.describe('conform-yup', () => { const error = { text: ['min', 'regex'], tag: ['required', 'invalid'], - number: 'max', - timestamp: 'min', - 'options[1]': 'invalid', - options: 'min', - 'nested.key': 'required', - nested: 'error', - 'list[0].key': 'required', - 'list[0]': 'error', - list: 'max', - '': 'error', + number: ['max'], + timestamp: ['min'], + 'options[1]': ['invalid'], + options: ['min'], + 'nested.key': ['required'], + nested: ['error'], + 'list[0].key': ['required'], + 'list[0]': ['error'], + list: ['max'], + '': ['error'], }; test('getFieldsetConstraint', () => { diff --git a/tests/conform-zod.spec.ts b/tests/conform-zod.spec.ts index 55efb2cf..f3e0f454 100644 --- a/tests/conform-zod.spec.ts +++ b/tests/conform-zod.spec.ts @@ -213,16 +213,16 @@ test.describe('conform-zod', () => { }; const error = { text: ['min', 'regex', 'refine'], - number: 'step', - timestamp: 'min', - options: 'min', - 'options[0]': 'refine', - 'options[1]': 'refine', - 'nested.key': 'refine', - files: 'required', - nested: 'refine', - list: 'max', - 'list[0].key': 'required', + number: ['step'], + timestamp: ['min'], + options: ['min'], + 'options[0]': ['refine'], + 'options[1]': ['refine'], + 'nested.key': ['refine'], + files: ['required'], + nested: ['refine'], + list: ['max'], + 'list[0].key': ['required'], }; expect(parse(formData, { schema, stripEmptyValue: true })).toEqual({ @@ -258,17 +258,17 @@ test.describe('conform-zod', () => { }; const error = { text: ['min', 'regex', 'refine'], - number: 'step', - timestamp: 'min', - options: 'min', - 'options[0]': 'refine', - 'options[1]': 'refine', - 'nested.key': 'refine', - nested: 'refine', - list: 'max', - 'list[0].key': 'refine', - 'list[0]': 'refine', - '': 'refine', + number: ['step'], + timestamp: ['min'], + options: ['min'], + 'options[0]': ['refine'], + 'options[1]': ['refine'], + 'nested.key': ['refine'], + nested: ['refine'], + list: ['max'], + 'list[0].key': ['refine'], + 'list[0]': ['refine'], + '': ['refine'], }; expect(parse(formData, { schema, stripEmptyValue: false })).toEqual({ @@ -302,7 +302,7 @@ test.describe('conform-zod', () => { text: 'abc', }, error: { - text: 'The field is too short', + text: ['The field is too short'], }, }); }); @@ -335,13 +335,13 @@ test.describe('conform-zod', () => { expect(parse(formData, { schema: createSchema() })).toEqual({ ...submission, error: { - email: '__undefined__', + email: ['__undefined__'], }, }); expect(parse(formData, { schema: createSchema(() => false) })).toEqual({ ...submission, error: { - email: 'Email is invalid', + email: ['Email is invalid'], }, }); expect(parse(formData, { schema: createSchema(() => true) })).toEqual({ @@ -354,7 +354,7 @@ test.describe('conform-zod', () => { ).toEqual({ ...submission, error: { - email: '__skipped__', + email: ['__skipped__'], }, }); expect( @@ -362,7 +362,7 @@ test.describe('conform-zod', () => { ).toEqual({ ...submission, error: { - email: '__skipped__', + email: ['__skipped__'], }, }); expect( @@ -370,7 +370,7 @@ test.describe('conform-zod', () => { ).toEqual({ ...submission, error: { - email: 'Email is invalid', + email: ['Email is invalid'], }, }); expect(parse(formData, { schema: createSchema(() => true, true) })).toEqual( diff --git a/tests/react.spec.ts b/tests/react.spec.ts index c592bb82..44470137 100644 --- a/tests/react.spec.ts +++ b/tests/react.spec.ts @@ -329,8 +329,8 @@ test.describe('Client Validation', () => { email: '', }, error: { - email: 'Email is required', - password: 'Password is required', + email: ['Email is required'], + password: ['Password is required'], }, }); @@ -344,7 +344,7 @@ test.describe('Client Validation', () => { email: 'invalid email', }, error: { - password: 'Password is required', + password: ['Password is required'], }, }); });