From 4a4e2ec20792cc2cd8b61512a040cde815a1e047 Mon Sep 17 00:00:00 2001 From: Michael Purdy <0mpurdy@gmail.com> Date: Thu, 5 Dec 2024 09:21:55 +0000 Subject: [PATCH] react-material: Fix table cells respecting validation mode (#2400) * Add validation mode to examples * Add tests for react material array cell validation mode * Material cell should respect ValidationMode Fix #2398 --- packages/examples-react/src/App.css | 1 + packages/examples-react/src/App.tsx | 27 +++ .../src/complex/MaterialTableControl.tsx | 9 +- .../renderers/MaterialTableControl.test.tsx | 228 ++++++++++++++++++ .../material-renderers/test/renderers/util.ts | 3 +- 5 files changed, 262 insertions(+), 6 deletions(-) create mode 100644 packages/material-renderers/test/renderers/MaterialTableControl.test.tsx diff --git a/packages/examples-react/src/App.css b/packages/examples-react/src/App.css index d5035310b7..0d0027a400 100644 --- a/packages/examples-react/src/App.css +++ b/packages/examples-react/src/App.css @@ -31,6 +31,7 @@ body { justify-content: center; align-items: center; margin-top: 20px; + gap: 20px; } .tools .example-selector h4 { margin: 0 10px 0 0; diff --git a/packages/examples-react/src/App.tsx b/packages/examples-react/src/App.tsx index 1ca8de9bfe..269ccb3d6e 100644 --- a/packages/examples-react/src/App.tsx +++ b/packages/examples-react/src/App.tsx @@ -29,6 +29,7 @@ import { ExampleDescription } from '@jsonforms/examples'; import { JsonFormsCellRendererRegistryEntry, JsonFormsRendererRegistryEntry, + ValidationMode, } from '@jsonforms/core'; import { Tab, Tabs, TabList, TabPanel } from 'react-tabs'; import Highlight from 'react-highlight'; @@ -85,6 +86,9 @@ const App = ({ getProps(currentExample, cells, renderers) ); const [showPanel, setShowPanel] = useState(true); + const [validationMode, setValidationMode] = useState< + ValidationMode | undefined + >(undefined); const schemaAsString = useMemo( () => JSON.stringify(exampleProps.schema, null, 2), [exampleProps.schema] @@ -145,6 +149,28 @@ const App = ({ ) )} +

Select ValidationMode:

+
changeData(data)} /> diff --git a/packages/material-renderers/src/complex/MaterialTableControl.tsx b/packages/material-renderers/src/complex/MaterialTableControl.tsx index 00239fc6c0..bd14a95b94 100644 --- a/packages/material-renderers/src/complex/MaterialTableControl.tsx +++ b/packages/material-renderers/src/complex/MaterialTableControl.tsx @@ -47,7 +47,7 @@ import { import { ArrayLayoutProps, ControlElement, - errorsAt, + errorAt, formatErrorMessage, JsonSchema, Paths, @@ -177,11 +177,10 @@ const ctxToNonEmptyCellProps = ( (ownProps.schema.type === 'object' ? '.' + ownProps.propName : ''); const errors = formatErrorMessage( union( - errorsAt( + errorAt( path, - ownProps.schema, - (p) => p === path - )(ctx.core.errors).map((error: ErrorObject) => error.message) + ownProps.schema + )(ctx.core).map((error: ErrorObject) => error.message) ) ); return { diff --git a/packages/material-renderers/test/renderers/MaterialTableControl.test.tsx b/packages/material-renderers/test/renderers/MaterialTableControl.test.tsx new file mode 100644 index 0000000000..8e5d01a958 --- /dev/null +++ b/packages/material-renderers/test/renderers/MaterialTableControl.test.tsx @@ -0,0 +1,228 @@ +/* + The MIT License + + Copyright (c) 2017-2019 EclipseSource Munich + https://github.com/eclipsesource/jsonforms + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ +import { JsonSchema7 } from '@jsonforms/core'; +import * as React from 'react'; + +import { materialCells, materialRenderers } from '../../src'; +import Enzyme, { mount, ReactWrapper } from 'enzyme'; +import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; +import { JsonForms } from '@jsonforms/react'; +import { FormHelperText } from '@mui/material'; + +Enzyme.configure({ adapter: new Adapter() }); + +const dataWithEmptyMessage = { + nested: [ + { + message: '', + }, + ], +}; + +const dataWithNullMessage = { + nested: [ + { + message: null as string | null, + }, + ], +}; + +const dataWithUndefinedMessage = { + nested: [ + { + message: undefined as string | undefined, + }, + ], +}; + +const schemaWithMinLength: JsonSchema7 = { + type: 'object', + properties: { + nested: { + type: 'array', + items: { + type: 'object', + properties: { + message: { type: 'string', minLength: 3 }, + done: { type: 'boolean' }, + }, + }, + }, + }, +}; + +const schemaWithRequired: JsonSchema7 = { + type: 'object', + properties: { + nested: { + type: 'array', + items: { + type: 'object', + properties: { + message: { type: 'string' }, + done: { type: 'boolean' }, + }, + required: ['message'], + }, + }, + }, +}; + +const uischema = { + type: 'VerticalLayout', + elements: [ + { + type: 'Control', + scope: '#/properties/nested', + }, + ], +}; + +describe('Material table control', () => { + let wrapper: ReactWrapper; + + const validSchemaDataPairs = [ + { + schema: schemaWithRequired, + data: dataWithEmptyMessage, + }, + { + schema: schemaWithMinLength, + data: dataWithUndefinedMessage, + }, + ]; + + const invalidSchemaDataPairs = [ + { + schema: schemaWithRequired, + data: dataWithNullMessage, + message: 'must be string', + }, + { + schema: schemaWithRequired, + data: dataWithUndefinedMessage, + message: "must have required property 'message'", + }, + { + schema: schemaWithMinLength, + data: dataWithEmptyMessage, + message: 'must NOT have fewer than 3 characters', + }, + ]; + + afterEach(() => wrapper.unmount()); + + it.each(invalidSchemaDataPairs)( + 'should show error message for invalid property with validation mode ValidateAndShow', + ({ schema, data, message }) => { + wrapper = mount( + + ); + const messageFormHelperText = wrapper.find(FormHelperText).at(0); + expect(messageFormHelperText.text()).toBe(message); + expect(messageFormHelperText.props().error).toBe(true); + + const doneFormHelperText = wrapper.find(FormHelperText).at(1); + expect(doneFormHelperText.text()).toBe(''); + expect(doneFormHelperText.props().error).toBe(false); + } + ); + + it.each(invalidSchemaDataPairs)( + 'should not show error message for invalid property with validation mode ValidateAndHide', + ({ schema, data }) => { + wrapper = mount( + + ); + const messageFormHelperText = wrapper.find(FormHelperText).at(0); + expect(messageFormHelperText.text()).toBe(''); + expect(messageFormHelperText.props().error).toBe(false); + + const doneFormHelperText = wrapper.find(FormHelperText).at(1); + expect(doneFormHelperText.text()).toBe(''); + expect(doneFormHelperText.props().error).toBe(false); + } + ); + + it.each(invalidSchemaDataPairs)( + 'should not show error message for invalid property with validation mode NoValidation', + ({ schema, data }) => { + wrapper = mount( + + ); + const messageFormHelperText = wrapper.find(FormHelperText).at(0); + expect(messageFormHelperText.text()).toBe(''); + expect(messageFormHelperText.props().error).toBe(false); + + const doneFormHelperText = wrapper.find(FormHelperText).at(1); + expect(doneFormHelperText.text()).toBe(''); + expect(doneFormHelperText.props().error).toBe(false); + } + ); + + it.each(validSchemaDataPairs)( + 'should not show error message for valid property', + ({ schema, data }) => { + wrapper = mount( + + ); + const messageFormHelperText = wrapper.find(FormHelperText).at(0); + expect(messageFormHelperText.text()).toBe(''); + expect(messageFormHelperText.props().error).toBe(false); + + const doneFormHelperText = wrapper.find(FormHelperText).at(1); + expect(doneFormHelperText.text()).toBe(''); + expect(doneFormHelperText.props().error).toBe(false); + } + ); +}); diff --git a/packages/material-renderers/test/renderers/util.ts b/packages/material-renderers/test/renderers/util.ts index cb5072d579..9b2092d79f 100644 --- a/packages/material-renderers/test/renderers/util.ts +++ b/packages/material-renderers/test/renderers/util.ts @@ -25,6 +25,7 @@ import { createAjv, + JsonFormsCore, JsonSchema, TesterContext, Translator, @@ -37,7 +38,7 @@ export const initCore = ( schema: JsonSchema, uischema: UISchemaElement, data?: any -) => { +): JsonFormsCore => { return { schema, uischema, data, ajv: createAjv() }; };