diff --git a/examples/react/src/basic-ui/components/Containers/FormField.tsx b/examples/react/src/basic-ui/components/Containers/FormField.tsx index 24e5975..07f551b 100644 --- a/examples/react/src/basic-ui/components/Containers/FormField.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormField.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const FormField = ({ children, ...props }: any) => { - return
{children}
; + return
{children}
; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Containers/FormSectionContainer.tsx b/examples/react/src/basic-ui/components/Containers/FormSectionContainer.tsx index f5f125f..4da09ab 100644 --- a/examples/react/src/basic-ui/components/Containers/FormSectionContainer.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormSectionContainer.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const FormSectionContainer = ({ children, ...props }: any) => { - return
{children}
; + return
{children}
; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Containers/FormSectionGroup.tsx b/examples/react/src/basic-ui/components/Containers/FormSectionGroup.tsx index 59a9d65..e32fc5b 100644 --- a/examples/react/src/basic-ui/components/Containers/FormSectionGroup.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormSectionGroup.tsx @@ -2,5 +2,5 @@ import React from 'react'; export const FormSectionGroup = ({ children, columns, ...props }: any) => { const gridColumns = columns || 'repeat(auto-fit, minmax(200px, 1fr))'; - return
{children}
; + return
{children}
; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Containers/FormSectionGroupContainer.tsx b/examples/react/src/basic-ui/components/Containers/FormSectionGroupContainer.tsx index 3306a08..3d7140e 100644 --- a/examples/react/src/basic-ui/components/Containers/FormSectionGroupContainer.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormSectionGroupContainer.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const FormSectionGroupContainer = ({ children, ...props }: any) => { - return
{children}
; + return
{children}
; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/components/Containers/FormSectionTitle.tsx b/examples/react/src/basic-ui/components/Containers/FormSectionTitle.tsx index 4fed989..b0db5aa 100644 --- a/examples/react/src/basic-ui/components/Containers/FormSectionTitle.tsx +++ b/examples/react/src/basic-ui/components/Containers/FormSectionTitle.tsx @@ -1,5 +1,5 @@ import React from 'react'; export const FormSectionTitle = ({ 'x-content': content, children, ...props }: any) => { - return

{content || children}

; + return

{content || children}

; }; \ No newline at end of file diff --git a/examples/react/src/basic-ui/pages/BasicFormPage.tsx b/examples/react/src/basic-ui/pages/BasicFormPage.tsx index 9152a21..d3bd562 100644 --- a/examples/react/src/basic-ui/pages/BasicFormPage.tsx +++ b/examples/react/src/basic-ui/pages/BasicFormPage.tsx @@ -22,12 +22,12 @@ export function BasicFormPage() { <> - - - - - - + + + + + + diff --git a/examples/react/src/chakra-ui/components/Containers/FormField.tsx b/examples/react/src/chakra-ui/components/Containers/FormField.tsx index a4c7270..c38912e 100644 --- a/examples/react/src/chakra-ui/components/Containers/FormField.tsx +++ b/examples/react/src/chakra-ui/components/Containers/FormField.tsx @@ -2,5 +2,5 @@ import React from "react"; import { Box } from "@chakra-ui/react"; export const FormField = ({ children, ...props }: any) => { - return {children}; + return {children}; }; \ No newline at end of file diff --git a/examples/react/src/chakra-ui/components/Containers/FormSectionGroup.tsx b/examples/react/src/chakra-ui/components/Containers/FormSectionGroup.tsx index 7bb2079..f365584 100644 --- a/examples/react/src/chakra-ui/components/Containers/FormSectionGroup.tsx +++ b/examples/react/src/chakra-ui/components/Containers/FormSectionGroup.tsx @@ -1,15 +1,13 @@ import React from "react"; -import { Grid, GridItem } from "@chakra-ui/react"; +import { Grid } from "@chakra-ui/react"; export const FormSectionGroup = ({ children, 'x-component-props': xComponentProps, ...props }: any) => { const columns = xComponentProps?.columns || '1fr 1fr'; const gridColumns = columns === '1fr' ? 1 : 2; return ( - - {React.Children.map(children, (child, index) => ( - {child} - ))} + + {children} ); }; \ No newline at end of file diff --git a/examples/react/src/chakra-ui/components/Containers/FormSectionGroupContainer.tsx b/examples/react/src/chakra-ui/components/Containers/FormSectionGroupContainer.tsx index 45194be..d6ac7bf 100644 --- a/examples/react/src/chakra-ui/components/Containers/FormSectionGroupContainer.tsx +++ b/examples/react/src/chakra-ui/components/Containers/FormSectionGroupContainer.tsx @@ -2,5 +2,5 @@ import React from "react"; import { Box } from "@chakra-ui/react"; export const FormSectionGroupContainer = ({ children, ...props }: any) => { - return {children}; + return {children}; }; \ No newline at end of file diff --git a/examples/react/src/chakra-ui/components/Inputs/InputCheckbox.tsx b/examples/react/src/chakra-ui/components/Inputs/InputCheckbox.tsx index cc70e2b..fdf0341 100644 --- a/examples/react/src/chakra-ui/components/Inputs/InputCheckbox.tsx +++ b/examples/react/src/chakra-ui/components/Inputs/InputCheckbox.tsx @@ -2,12 +2,12 @@ import React from "react"; import { Checkbox } from "@chakra-ui/react"; export const InputCheckbox = React.forwardRef((props, ref) => { - const { label, name, checked, onChange, ...rest } = props; + const { label, name, value, onChange, ...rest } = props; return ( onChange?.(e.target.checked)} data-test-id={name} {...rest} diff --git a/examples/react/src/index.css b/examples/react/src/index.css index ad5c070..83bfa71 100644 --- a/examples/react/src/index.css +++ b/examples/react/src/index.css @@ -12,3 +12,7 @@ code { monospace; } +*, *::before, *::after { + box-sizing: border-box; +} + diff --git a/examples/react/src/material-ui/components/Containers/FormField.tsx b/examples/react/src/material-ui/components/Containers/FormField.tsx index 32c3ef5..aba9d70 100644 --- a/examples/react/src/material-ui/components/Containers/FormField.tsx +++ b/examples/react/src/material-ui/components/Containers/FormField.tsx @@ -2,5 +2,5 @@ import React from "react"; import { Box } from "@mui/material"; export const FormField = ({ children, ...props }: any) => { - return {children}; + return {children}; }; diff --git a/examples/react/src/material-ui/components/Containers/FormSectionGroup.tsx b/examples/react/src/material-ui/components/Containers/FormSectionGroup.tsx index bb357dc..2017d52 100644 --- a/examples/react/src/material-ui/components/Containers/FormSectionGroup.tsx +++ b/examples/react/src/material-ui/components/Containers/FormSectionGroup.tsx @@ -1,5 +1,5 @@ import React from "react"; -import { Grid } from "@mui/material"; +import { Box, Grid } from "@mui/material"; export const FormSectionGroup = ({ children, @@ -7,23 +7,17 @@ export const FormSectionGroup = ({ ...props }: any) => { const columns = xComponentProps?.columns || "1fr 1fr"; - const gridColumns = columns === "1fr" ? 12 : 6; + const gridColumns = columns === "1fr" ? 1 : 2; return ( - - {React.Children.map(children, (child) => ( - - {child} - - ))} - + {children} + ); }; diff --git a/examples/react/src/material-ui/components/Containers/FormSectionGroupContainer.tsx b/examples/react/src/material-ui/components/Containers/FormSectionGroupContainer.tsx index f02690b..397bb78 100644 --- a/examples/react/src/material-ui/components/Containers/FormSectionGroupContainer.tsx +++ b/examples/react/src/material-ui/components/Containers/FormSectionGroupContainer.tsx @@ -2,5 +2,5 @@ import React from "react"; import { Box } from "@mui/material"; export const FormSectionGroupContainer = ({ children, ...props }: any) => { - return {children}; + return {children}; }; diff --git a/examples/react/src/material-ui/components/Inputs/InputCheckbox.tsx b/examples/react/src/material-ui/components/Inputs/InputCheckbox.tsx index cb83cc7..e152c6a 100644 --- a/examples/react/src/material-ui/components/Inputs/InputCheckbox.tsx +++ b/examples/react/src/material-ui/components/Inputs/InputCheckbox.tsx @@ -4,14 +4,14 @@ import { Checkbox } from "@mui/material"; export const InputCheckbox = React.forwardRef( (props, ref) => { - const { label, name, checked, onChange, ...rest } = props; + const { label, name, value, onChange, ...rest } = props; return ( onChange?.(e.target.checked)} data-test-id={name} {...rest} diff --git a/instances/form/complex-form.json b/instances/form/complex-form.json index 70d7102..bde79a7 100644 --- a/instances/form/complex-form.json +++ b/instances/form/complex-form.json @@ -23,15 +23,12 @@ "basicInfo": { "type": "object", "x-component": "FormSectionGroup", - "x-component-props": { - "columns": "1fr 1fr" - }, "properties": { "email": { "type": "object", "x-component": "FormField", "x-ui": { - "order": 1 + "order": 3 }, "properties": { "email": { @@ -40,11 +37,10 @@ "x-component-props": { "type": "email", "label": "Email Address", - "placeholder": "example@email.com" - }, - "x-rules": { + "placeholder": "example@email.com", "required": true } + } } }, @@ -52,7 +48,7 @@ "type": "object", "x-component": "FormField", "x-ui": { - "order": 2 + "order": 4 }, "properties": { "phone": { @@ -60,7 +56,8 @@ "x-component": "InputPhone", "x-component-props": { "label": "Phone Number", - "placeholder": "(123) 456-7890" + "placeholder": "(123) 456-7890", + "required": true } } } @@ -90,10 +87,43 @@ "basicInfo": { "type": "object", "x-component": "FormSectionGroup", - "x-component-props": { - "columns": "1fr 1fr" - }, "properties": { + "firstName": { + "type": "object", + "x-component": "FormField", + "x-ui": { + "order": 1 + }, + "properties": { + "firstName": { + "type": "string", + "x-component": "InputText", + "x-component-props": { + "label": "First Name", + "placeholder": "Enter your first name", + "required": true + } + } + } + }, + "lastName": { + "type": "object", + "x-component": "FormField", + "x-ui": { + "order": 2 + }, + "properties": { + "lastName": { + "type": "string", + "x-component": "InputText", + "x-component-props": { + "label": "Last Name", + "placeholder": "Enter your last name", + "required": true + } + } + } + }, "userType": { "type": "object", "x-component": "FormField", @@ -136,9 +166,6 @@ "additionalInfo": { "type": "object", "x-component": "FormSectionGroup", - "x-component-props": { - "columns": "1fr" - }, "properties": { "bio": { "type": "object", @@ -157,28 +184,15 @@ } } }, - "age": { - "type": "object", - "x-component": "FormField", - "x-ui": { - "order": 6 - }, - "properties": { - "age": { - "type": "number", - "x-component": "InputNumber", - "x-component-props": { - "label": "Age", - "placeholder": "Enter your age", - "min": 0, - "max": 120 - } - } - } - }, "acceptTerms": { "type": "object", "x-component": "FormField", + "x-component-props": { + "style": { + "display": "flex", + "alignItems": "center" + } + }, "x-ui": { "order": 7 }, diff --git a/packages/core/src/registry/renderer-registry.ts b/packages/core/src/registry/renderer-registry.ts index 041eefb..5526302 100644 --- a/packages/core/src/registry/renderer-registry.ts +++ b/packages/core/src/registry/renderer-registry.ts @@ -37,7 +37,9 @@ export const defaultTypeRenderers: Record = { return runtime.create(spec, propsWithChildren); }, 'container': (spec, props, runtime, children) => { - const sanitized = sanitizePropsForDOM(props); + const xComponentProps = props['x-component-props'] || {}; + const mergedProps = { ...props, ...xComponentProps }; + const sanitized = sanitizePropsForDOM(mergedProps); const propsWithChildren = children && children.length > 0 ? { ...sanitized, children } : sanitized; diff --git a/packages/core/src/schema/schema-types.ts b/packages/core/src/schema/schema-types.ts index 4ab8b35..801d8ca 100644 --- a/packages/core/src/schema/schema-types.ts +++ b/packages/core/src/schema/schema-types.ts @@ -1,201 +1,221 @@ /** - * Schema Types - * - * Type definitions for form and menu schemas. - * Compatible with JSON Schema format. - */ - -/** - * Base schema property - */ -export interface BaseSchemaProperty { - type: string; - 'x-component'?: string; - 'x-component-props'?: Record; - 'x-rules'?: Record; - 'x-reactions'?: Record; - 'x-ui'?: { - order?: number; - [key: string]: any; - }; - 'x-content'?: any; - 'x-slots'?: Record; -} - -/** - * Form schema structure - */ -export interface FormSchema extends BaseSchemaProperty { - type: 'object'; - $id?: string; - $schema?: string; - properties?: Record; - required?: string[]; -} - -/** - * Form section container - */ -export interface FormSectionContainer extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormSectionContainer'; - properties?: Record; -} - -/** - * Form section title - */ -export interface FormSectionTitle extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormSectionTitle'; - 'x-content': string; -} - -/** - * Form section group container - */ -export interface FormSectionGroupContainer extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormSectionGroupContainer'; - properties?: Record; -} - -/** - * Form section group - */ -export interface FormSectionGroup extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormSectionGroup'; - properties?: Record; -} - -/** - * Form field - */ -export interface FormField extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'FormField'; - properties?: Record; -} - -/** - * Field component types - */ -export type FieldComponent = - | InputText - | InputSelect - | InputCheckbox - | InputDate - | InputCpf - | InputPhone - | InputTextarea - | InputNumber - | InputAutocomplete; - -/** - * Input text - */ -export interface InputText extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputText'; -} - -/** - * Input select - */ -export interface InputSelect extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputSelect'; -} - -/** - * Input checkbox - */ -export interface InputCheckbox extends BaseSchemaProperty { - type: 'boolean'; - 'x-component': 'InputCheckbox'; -} - -/** - * Date picker - */ -export interface InputDate extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputDate'; -} - -/** - * Input CPF - */ -export interface InputCpf extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputCpf'; -} - -/** - * Input phone - */ -export interface InputPhone extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputPhone'; -} - -/** - * Input autocomplete - */ -export interface InputAutocomplete extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputAutocomplete'; -} - -/** - * Input textarea - */ -export interface InputTextarea extends BaseSchemaProperty { - type: 'string'; - 'x-component': 'InputTextarea'; -} - -/** - * Input number - */ -export interface InputNumber extends BaseSchemaProperty { - type: 'number'; - 'x-component': 'InputNumber'; -} - -/** - * Menu schema structure - */ -export interface MenuSchema extends BaseSchemaProperty { - type: 'object'; - properties?: Record; -} - -/** - * Menu container - */ -export interface MenuContainer extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'MenuContainer'; - 'x-content'?: { - items?: MenuItem[]; - }; -} - -/** - * Menu item - */ -export interface MenuItem extends BaseSchemaProperty { - type: 'object'; - 'x-component': 'MenuLink' | 'MenuButton'; - 'x-component-props'?: { - href?: string; - onClick?: string; - [key: string]: any; - }; - 'x-content'?: { - text?: string; - }; + * Schema de formulário + */ +export interface FormSchema { + $id: string + $schema?: string + type: "object" + properties: { + [k: string]: FormSectionContainer + } + "x-component": "FormContainer" +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ +export interface FormSectionContainer { + type: "object" + "x-ui": { + /** + * Order of the section in the form (lower values appear first) + */ + order: number + } + properties: { + /** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ + [k: string]: FormSectionTitle | FormSectionGroupContainer + } + "x-component": "FormSectionContainer" + "x-component-props"?: { + [k: string]: unknown + } +} +export interface FormSectionTitle { + type: "object" + "x-slots"?: { + [k: string]: unknown + } + "x-content": string + "x-component": "FormSectionTitle" + "x-component-props"?: { + [k: string]: unknown + } +} +export interface FormSectionGroupContainer { + type: "object" + properties: { + [k: string]: FormSectionGroup + } + "x-component": "FormSectionGroupContainer" + "x-component-props"?: { + [k: string]: unknown + } +} +/** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ +export interface FormSectionGroup { + type: "object" + properties: { + [k: string]: FormField + } + "x-component": "FormSectionGroup" + "x-component-props"?: { + [k: string]: unknown + } +} +/** + * FormField wrapper (Grid) reutilizável para qualquer tipo de input + * + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^.*$". + */ +export interface FormField { + type: "object" + "x-component": "FormField" + "x-ui": { + /** + * Order of the field in the form (lower values appear first) + */ + order: number + } + properties: { + /** + * This interface was referenced by `undefined`'s JSON-Schema definition + * via the `patternProperty` "^[a-zA-Z_][a-zA-Z0-9_]*$". + */ + [k: string]: + | InputText + | InputSelect + | InputCheckbox + | InputDate + | InputPhone + | InputAutocomplete + | InputTextarea + | InputNumber + } + "x-component-props"?: { + [k: string]: unknown + } +} +export interface InputText { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputText" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputSelect { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputSelect" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputCheckbox { + type: "boolean" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputCheckbox" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputDate { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputDate" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputPhone { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputPhone" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputAutocomplete { + type: "string" + "x-rules"?: { + [k: string]: unknown + } + "x-component": "InputAutocomplete" + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + label?: string + placeholder?: string + [k: string]: unknown + } +} +export interface InputTextarea { + type: "string" + "x-component": "InputTextarea" + "x-rules"?: { + [k: string]: unknown + } + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + [k: string]: unknown + } +} +export interface InputNumber { + type: "number" + "x-component": "InputNumber" + "x-rules"?: { + [k: string]: unknown + } + "x-reactions"?: { + [k: string]: unknown + } + "x-component-props"?: { + [k: string]: unknown + } } - diff --git a/tests/e2e/chakra-ui.spec.ts b/tests/e2e/chakra-ui.spec.ts deleted file mode 100644 index e568318..0000000 --- a/tests/e2e/chakra-ui.spec.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { test, expect } from '@playwright/test'; -import simpleFormSchema from '../../instances/form/simple-form.json'; -import complexFormSchema from '../../instances/form/complex-form.json'; - -test.describe('Chakra UI Form Factory', () => { - test.beforeEach(async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3002'}/`); - }); - - test('should render simple form with Chakra UI components', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - - await expect(firstNameField).toBeVisible(); - await expect(lastNameField).toBeVisible(); - }); - - test('should render complex form with all field types', async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3002'}/complex`); - - await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); - - await expect(page.locator('[data-test-id*="email"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="phone"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="userType"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="acceptTerms"]').first()).toBeVisible(); - }); - - test('should handle form input', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // Chakra UI Input component renders directly as an INPUT element - // Get the input element directly by its data-test-id - const firstNameInput = page.locator('input[data-test-id*="firstName"]').first(); - - // Wait for the element to be visible and enabled - await expect(firstNameInput).toBeVisible(); - await expect(firstNameInput).toBeEnabled(); - - // Get initial value - const initialValue = await firstNameInput.inputValue(); - console.log('Initial value:', initialValue); - - // Try to fill the input - try { - await firstNameInput.fill('John'); - console.log('Fill completed'); - } catch (error) { - console.log('Fill error:', error); - throw error; - } - - // Wait a bit for the value to be set - await page.waitForTimeout(500); - - // Check the value after fill - const valueAfterFill = await firstNameInput.inputValue(); - console.log('Value after fill:', valueAfterFill); - - // This will fail if the input is not filled - expect(valueAfterFill).toBe('John'); - }); -}); - diff --git a/tests/e2e/material-ui.spec.ts b/tests/e2e/material-ui.spec.ts deleted file mode 100644 index 4ba7081..0000000 --- a/tests/e2e/material-ui.spec.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { test, expect } from '@playwright/test'; -import simpleFormSchema from '../../instances/form/simple-form.json'; -import complexFormSchema from '../../instances/form/complex-form.json'; - -test.describe('Material UI Form Factory', () => { - test.beforeEach(async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3001'}/`); - }); - - test('should render simple form with Material UI components', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - - await expect(firstNameField).toBeVisible(); - await expect(lastNameField).toBeVisible(); - - // Check if Material UI styling is applied - const input = firstNameField.locator('input'); - await expect(input).toBeVisible(); - }); - - test('should render complex form with all field types', async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3001'}/complex`); - - await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); - - await expect(page.locator('[data-test-id*="email"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="phone"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="userType"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="acceptTerms"]').first()).toBeVisible(); - }); - - test('should handle form input', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"] input').first(); - await firstNameField.fill('John'); - - await expect(firstNameField).toHaveValue('John'); - }); -}); - diff --git a/tests/e2e/provider.spec.ts b/tests/e2e/provider.spec.ts deleted file mode 100644 index 709029f..0000000 --- a/tests/e2e/provider.spec.ts +++ /dev/null @@ -1,99 +0,0 @@ -import { test, expect } from '@playwright/test'; - -// Only run provider tests for React (provider example is only in React until we have a provider example for other frameworks) -// TODO: atualizar test pois nao temos mais um exemplo direto de provider, agora ele circunda tudo -test.describe('Provider E2E Tests', () => { - test.use({ - // @ts-ignore - project is a valid option in Playwright config - project: 'react' - }); - - test.beforeEach(async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3000'}/provider`); - }); - - test('should render form using components from provider', async ({ page }) => { - // Wait for form to be rendered - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // Verify form container exists (from provider) - const formContainer = page.locator('[data-test-id="FormContainer"]'); - await expect(formContainer).toBeVisible(); - - // Verify form fields are present (components from provider) - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - - await expect(firstNameField).toBeVisible(); - await expect(lastNameField).toBeVisible(); - }); - - test('should apply middleware from provider to form fields', async ({ page }) => { - // Wait for form to be rendered - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // The middleware adds "[Provider]" prefix to labels - // Verify that labels contain the prefix - const firstNameLabel = page.locator('label[for*="firstName"]'); - await expect(firstNameLabel).toBeVisible(); - - const labelText = await firstNameLabel.textContent(); - expect(labelText).toContain('[Provider]'); - }); - - test('should use externalContext from provider', async ({ page }) => { - // Wait for form to be rendered - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // The schema has: "label": "{{ $externalContext.user.name }}" - // Which should be replaced with "Provider User" from the provider's externalContext - // Note: The middleware adds "[Provider]" prefix, so the label will be "[Provider] Provider User" - const firstNameLabel = page.locator('label[for*="firstName"]'); - await expect(firstNameLabel).toBeVisible(); - - const labelText = await firstNameLabel.textContent(); - // Verify that the template expression was resolved to "Provider User" - expect(labelText).toContain('Provider User'); - // Verify that the middleware prefix is also applied - expect(labelText).toContain('[Provider]'); - }); - - test('should handle form input with provider components', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - await firstNameField.fill('Provider Test'); - - await expect(firstNameField).toHaveValue('Provider Test'); - }); - - test('should submit form and display submitted values', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - // Fill form fields - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - await firstNameField.fill('Jane'); - - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - await lastNameField.fill('Smith'); - - // Submit form - const submitButton = page.locator('[data-test-id="submit-button"]'); - await submitButton.click(); - - // Wait for submitted values section - await page.waitForSelector('text=Valores Submetidos', { timeout: 5000 }); - - // Verify submitted values are displayed - const submittedSection = page.locator('text=Valores Submetidos'); - await expect(submittedSection).toBeVisible(); - - // Verify values in the JSON output - const preElement = page.locator('pre'); - const jsonContent = await preElement.textContent(); - expect(jsonContent).toContain('Jane'); - expect(jsonContent).toContain('Smith'); - }); - -}); - diff --git a/tests/e2e/react.spec.ts b/tests/e2e/react.spec.ts index ee786d2..55c77a2 100644 --- a/tests/e2e/react.spec.ts +++ b/tests/e2e/react.spec.ts @@ -1,55 +1,87 @@ import { test, expect } from '@playwright/test'; import simpleFormSchema from '../../instances/form/simple-form.json'; import complexFormSchema from '../../instances/form/complex-form.json'; +import { extractFormFields, extractRequiredFields } from 'tests/utils/extractJsonFields'; + + test.describe('React Form Factory', () => { test.beforeEach(async ({ page, baseURL }) => { - await page.goto(baseURL || 'http://localhost:3000/'); + await page.goto(`${baseURL || 'http://localhost:3000'}/basic`); }); test('should render simple form', async ({ page }) => { + const fields = extractFormFields(simpleFormSchema); + // Wait for form to be rendered await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - // Check if form fields are present - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - const lastNameField = page.locator('[data-test-id*="lastName"]').first(); - - await expect(firstNameField).toBeVisible(); - await expect(lastNameField).toBeVisible(); + // Check if all form fields from schema are present + for (const field of fields) { + await expect(page.locator(`[data-test-id*="${field}"]`)).toBeVisible(); + } }); test('should render complex form with all field types', async ({ page, baseURL }) => { - await page.goto(`${baseURL || 'http://localhost:3000'}/complex`); + await page.click('[data-test-id*="complex-form-tab"]'); + + const fields = extractFormFields(complexFormSchema); + console.log('Extracted fields from complex schema:', fields); // Wait for form to be rendered await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); - // Check different field types - await expect(page.locator('[data-test-id*="email"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="phone"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="userType"]').first()).toBeVisible(); - await expect(page.locator('[data-test-id*="acceptTerms"]').first()).toBeVisible(); + // Check if all form fields from schema are present + for (const field of fields) { + await expect(page.locator(`[data-test-id*="${field}"]`).first()).toBeVisible(); + } }); - test('should handle form input', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - await firstNameField.fill('John'); - - await expect(firstNameField).toHaveValue('John'); + test('should fill form fields', async ({ page }) => { + const fields = extractFormFields(complexFormSchema); + const firstNameField = fields.find(f => f === 'firstName'); + const lastNameField = fields.find(f => f === 'lastName'); + const emailField = fields.find(f => f === 'email'); + const phoneField = fields.find(f => f === 'phone'); + const birthDateField = fields.find(f => f === 'birthDate'); + const userTypeField = fields.find(f => f === 'userType'); + const bioField = fields.find(f => f === 'bio'); + const acceptTermsField = fields.find(f => f === 'acceptTerms'); + + await page.click('[data-test-id*="complex-form-tab"]'); + await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); + + await page.locator(`[data-test-id*="${emailField}"]`).first().fill('john.doe@example.com'); + await page.locator(`[data-test-id*="${phoneField}"]`).first().fill('(123) 456-7890'); + await page.locator(`[data-test-id*="${firstNameField}"]`).first().fill('John'); + await page.locator(`[data-test-id*="${lastNameField}"]`).first().fill('Doe'); + await page.locator(`[data-test-id*="${userTypeField}"]`).first().selectOption('individual'); + await page.locator(`[data-test-id*="${birthDateField}"]`).first().fill('1990-01-01'); + await page.locator(`[data-test-id*="${bioField}"]`).first().fill('I am a software engineer'); + await page.locator(`[data-test-id*="${acceptTermsField}"]`).first().check(); + + await expect(page.locator(`[data-test-id*="${emailField}"]`).first()).toHaveValue('john.doe@example.com'); + await expect(page.locator(`[data-test-id*="${phoneField}"]`).first()).toHaveValue('(123) 456-7890'); + await expect(page.locator(`[data-test-id*="${firstNameField}"]`).first()).toHaveValue('John'); + await expect(page.locator(`[data-test-id*="${lastNameField}"]`).first()).toHaveValue('Doe'); + await expect(page.locator(`[data-test-id*="${userTypeField}"]`).first()).toHaveValue('individual'); + await expect(page.locator(`[data-test-id*="${birthDateField}"]`).first()).toHaveValue('1990-01-01'); + await expect(page.locator(`[data-test-id*="${bioField}"]`).first()).toHaveValue('I am a software engineer'); + await expect(page.locator(`[data-test-id*="${acceptTermsField}"]`).first()).toBeChecked(); + }); test('should validate required fields', async ({ page }) => { - await page.waitForSelector('[data-test-id*="firstName"]', { timeout: 10000 }); - - const firstNameField = page.locator('[data-test-id*="firstName"]').first(); - await firstNameField.focus(); - await firstNameField.blur(); - - // Check if validation error appears (implementation dependent) - // This is a placeholder - adjust based on actual validation UI + const requiredFields = extractRequiredFields(complexFormSchema); + + await page.click('[data-test-id*="complex-form-tab"]'); + await page.waitForSelector('[data-test-id*="email"]', { timeout: 10000 }); + + for (const field of requiredFields) { + const fieldLocator = page.locator(`[data-test-id*="${field}"]`).first(); + await expect(fieldLocator).toHaveAttribute('required', ''); + } + }); }); diff --git a/tests/playwright.config.ts b/tests/playwright.config.ts index f35f186..8025c2c 100644 --- a/tests/playwright.config.ts +++ b/tests/playwright.config.ts @@ -20,35 +20,7 @@ export default defineConfig({ ...devices['Desktop Chrome'], baseURL: 'http://localhost:3000', }, - }, - { - name: 'material-ui', - use: { - ...devices['Desktop Chrome'], - baseURL: 'http://localhost:3001', - }, - }, - { - name: 'chakra-ui', - use: { - ...devices['Desktop Chrome'], - baseURL: 'http://localhost:3002', - }, - }, - { - name: 'vue', - use: { - ...devices['Desktop Chrome'], - baseURL: 'http://localhost:3010', - }, - }, - { - name: 'vue-vuetify', - use: { - ...devices['Desktop Chrome'], - baseURL: 'http://localhost:3011', - }, - }, + } ], webServer: [ { @@ -58,39 +30,7 @@ export default defineConfig({ timeout: 120 * 1000, stdout: 'ignore', stderr: 'pipe', - }, - { - command: 'pnpm --filter examples-react-material-ui dev', - url: 'http://localhost:3001', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - stdout: 'ignore', - stderr: 'pipe', - }, - { - command: 'pnpm --filter examples-react-chakra-ui dev', - url: 'http://localhost:3002', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - stdout: 'ignore', - stderr: 'pipe', - }, - { - command: 'pnpm --filter examples-vue dev', - url: 'http://localhost:3010', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - stdout: 'ignore', - stderr: 'pipe', - }, - { - command: 'pnpm --filter examples-vue-vuetify dev', - url: 'http://localhost:3011', - reuseExistingServer: !process.env.CI, - timeout: 120 * 1000, - stdout: 'ignore', - stderr: 'pipe', - }, + } ], }); diff --git a/tests/tsconfig.json b/tests/tsconfig.json new file mode 100644 index 0000000..ec16afb --- /dev/null +++ b/tests/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../tsconfig.base.json", + "compilerOptions": { + "outDir": "./dist", + "jsx": "preserve", + "baseUrl": "..", + "paths": { + "@schepta/core": ["./packages/core/src"], + "@schepta/adapter-vanilla": ["./packages/adapters/vanilla/src"], + "@schepta/adapter-react": ["./packages/adapters/react/src"], + "@schepta/adapter-vue": ["./packages/adapters/vue/src"], + "@schepta/factory-vanilla": ["./packages/factories/vanilla/src"], + "@schepta/factory-react": ["./packages/factories/react/src"], + "@schepta/factory-vue": ["./packages/factories/vue/src"] + } + }, + "include": ["**/*"], + "exclude": ["node_modules", "dist"] + } \ No newline at end of file diff --git a/tests/utils/extractJsonFields.ts b/tests/utils/extractJsonFields.ts new file mode 100644 index 0000000..95631f0 --- /dev/null +++ b/tests/utils/extractJsonFields.ts @@ -0,0 +1,93 @@ +import type { FormSchema } from '@schepta/core'; + +function isInputComponent(component: string): boolean { + return component.startsWith('Input') || + component === 'InputText' || + component === 'InputSelect' || + component === 'InputPhone' || + component === 'InputDate' || + component === 'InputTextarea' || + component === 'InputNumber' || + component === 'InputCheckbox'; +} + +/** + * Recursively extracts all form field names from a schema JSON + * A field is identified by having x-component: "FormField" and containing + * a property with an input component (InputText, InputSelect, etc.) + */ +export function extractFormFields(schema: FormSchema | any): string[] { + const fields: string[] = []; + + function traverse(obj: any, path: string[] = []) { + if (!obj || typeof obj !== 'object') { + return; + } + + // Check if this is a FormField component + if (obj['x-component'] === 'FormField' && obj.properties) { + // Look for the actual input field inside the FormField + for (const [key, value] of Object.entries(obj.properties)) { + if (value && typeof value === 'object' && 'x-component' in value) { + const component = (value as any)['x-component']; + // Check if it's an input component + if (component && isInputComponent(component)) { + fields.push(key); + } + } + } + } + + // Recursively traverse properties + if (obj.properties) { + for (const [key, value] of Object.entries(obj.properties)) { + if (value && typeof value === 'object') { + traverse(value, [...path, key]); + } + } + } + } + + traverse(schema); + return fields; +} + +/** + * Extracts required fields from schema based on x-rules.required or required prop + */ +export function extractRequiredFields(schema: FormSchema | any): string[] { + const requiredFields: string[] = []; + + function traverse(obj: any, currentFieldName: string | null = null) { + if (!obj || typeof obj !== 'object') { + return; + } + + // Check if this is an input component with required validation + if (obj['x-component'] && ( + isInputComponent(obj['x-component']) + )) { + // Check for required in x-rules or x-component-props + const isRequired = + obj['x-component-props']?.required === true; + + if (isRequired && currentFieldName) { + requiredFields.push(currentFieldName); + } + } + + // Recursively traverse properties + if (obj.properties) { + for (const [key, value] of Object.entries(obj.properties)) { + if (value && typeof value === 'object') { + // Use the key as field name if we're inside a FormField + const fieldName = obj['x-component'] === 'FormField' ? key : currentFieldName; + traverse(value, fieldName); + } + } + } + } + + traverse(schema); + return requiredFields; +} \ No newline at end of file