From a7742a5f08ef57fdf1de1457d09e894dcb746038 Mon Sep 17 00:00:00 2001 From: Abdelrahman Awad Date: Mon, 12 Aug 2024 03:12:39 +0300 Subject: [PATCH] chore: added nested path tests --- packages/playground/src/App.vue | 30 ++++++++------ packages/schema-yup/src/index.spec.ts | 52 ++++++++++++++++++++++++- packages/schema-yup/src/index.ts | 6 +-- packages/schema-zod/src/index.spec.ts | 56 ++++++++++++++++++++++++++- packages/test-utils/src/flush.ts | 2 +- 5 files changed, 129 insertions(+), 17 deletions(-) diff --git a/packages/playground/src/App.vue b/packages/playground/src/App.vue index f2e31281..16c30fee 100644 --- a/packages/playground/src/App.vue +++ b/packages/playground/src/App.vue @@ -1,26 +1,34 @@ diff --git a/packages/schema-yup/src/index.spec.ts b/packages/schema-yup/src/index.spec.ts index 7bdb09b7..15167f65 100644 --- a/packages/schema-yup/src/index.spec.ts +++ b/packages/schema-yup/src/index.spec.ts @@ -3,7 +3,7 @@ import { fireEvent, render, screen } from '@testing-library/vue'; import { useForm, useTextField } from '@formwerk/core'; import { defineSchema } from '.'; import * as y from 'yup'; -import flush from 'flush-promises'; +import { flush } from '@test-utils/index'; const requiredMessage = (field: string) => `${field} is a required field`; @@ -54,6 +54,56 @@ describe('schema-yup', () => { expect(screen.getByTestId('form-err').textContent).toBe(requiredMessage('test')); }); + test('validates nested paths', async () => { + await render({ + components: { Child: createInputComponent() }, + setup() { + const { getError, isValid } = useForm({ + schema: defineSchema( + y.object({ + some: y.object({ + deep: y.object({ + path: y.string().required(), + }), + array: y.array().of( + y.object({ + path: y.string().required(), + }), + ), + }), + }), + ), + }); + + return { getError, isValid }; + }, + template: ` +
+ + + + {{ isValid }} + {{ getError('some.deep.path') }} + {{ getError('some.array.0.path') }} + + `, + }); + + await flush(); + expect(screen.getByTestId('is-valid').textContent).toBe('false'); + expect(screen.getByTestId('e1').textContent).toBe(requiredMessage('some.deep.path')); + expect(screen.getByTestId('e2').textContent).toBe(requiredMessage('some.array[0].path')); + + await fireEvent.update(screen.getByTestId('some.deep.path'), 'test'); + await fireEvent.update(screen.getByTestId('some.array.0.path'), 'test'); + await fireEvent.blur(screen.getByTestId('some.deep.path')); + await fireEvent.blur(screen.getByTestId('some.array.0.path')); + await flush(); + expect(screen.getByTestId('is-valid').textContent).toBe('true'); + expect(screen.getByTestId('e1').textContent).toBe(''); + expect(screen.getByTestId('e2').textContent).toBe(''); + }); + test('prevents submission if the form is not valid', async () => { const handler = vi.fn(); diff --git a/packages/schema-yup/src/index.ts b/packages/schema-yup/src/index.ts index a20618ca..2652958b 100644 --- a/packages/schema-yup/src/index.ts +++ b/packages/schema-yup/src/index.ts @@ -1,6 +1,6 @@ import { InferType, Schema, ValidateOptions, ValidationError } from 'yup'; import type { PartialDeep } from 'type-fest'; -import { TypedSchema, TypedSchemaError } from '@formwerk/core'; +import { TypedSchema, TypedSchemaError, normalizePath } from '@formwerk/core'; import { isObject, merge } from '../../shared/src'; export function defineSchema, TInput = PartialDeep>( @@ -26,12 +26,12 @@ export function defineSchema = error.inner.reduce( (acc, curr) => { - const path = curr.path || ''; + const path = normalizePath(curr.path || ''); if (!acc[path]) { acc[path] = { messages: [], path }; } diff --git a/packages/schema-zod/src/index.spec.ts b/packages/schema-zod/src/index.spec.ts index 6c2b2b92..c67cdf88 100644 --- a/packages/schema-zod/src/index.spec.ts +++ b/packages/schema-zod/src/index.spec.ts @@ -5,7 +5,7 @@ import { defineSchema } from '.'; import { z } from 'zod'; import flush from 'flush-promises'; -describe('schema-yup', () => { +describe('schema-zod', () => { function createInputComponent(): Component { return { inheritAttrs: false, @@ -153,4 +153,58 @@ describe('schema-yup', () => { await expect(screen.getByDisplayValue('default-test')).toBeDefined(); await expect(screen.getByDisplayValue('22')).toBeDefined(); }); + + test('validates nested paths', async () => { + await render({ + components: { Child: createInputComponent() }, + setup() { + const { getError, isValid, values } = useForm({ + schema: defineSchema( + z.object({ + some: z.object({ + deep: z.object({ + path: z.string().min(1, 'Required'), + }), + array: z.array( + z.object({ + path: z.string().min(1, 'Required'), + }), + ), + }), + }), + ), + }); + + return { getError, isValid, values }; + }, + template: ` +
+ + + {{values}} + + {{ isValid }} + {{ getError('some.deep.path') }} + {{ getError('some.array.0.path') }} + + `, + }); + + await flush(); + vi.advanceTimersByTime(1000); + await flush(); + expect(screen.getByTestId('is-valid').textContent).toBe('false'); + expect(screen.getByTestId('e1').textContent).toBe('Required'); + expect(screen.getByTestId('e2').textContent).toBe('Required'); + + await fireEvent.update(screen.getByTestId('some.deep.path'), 'test'); + await fireEvent.update(screen.getByTestId('some.array.0.path'), 'test'); + await fireEvent.blur(screen.getByTestId('some.deep.path')); + await fireEvent.blur(screen.getByTestId('some.array.0.path')); + vi.advanceTimersByTime(1000); + await flush(); + expect(screen.getByTestId('is-valid').textContent).toBe('true'); + expect(screen.getByTestId('e1').textContent).toBe(''); + expect(screen.getByTestId('e2').textContent).toBe(''); + }); }); diff --git a/packages/test-utils/src/flush.ts b/packages/test-utils/src/flush.ts index 6c854e2c..7ac35367 100644 --- a/packages/test-utils/src/flush.ts +++ b/packages/test-utils/src/flush.ts @@ -4,6 +4,6 @@ import { vi } from 'vitest'; export async function flush() { await flushPromises(); - vi.advanceTimersByTime(SCHEMA_BATCH_MS); + vi.advanceTimersByTime(SCHEMA_BATCH_MS + 10); await flushPromises(); }