From 0d2885d61aaf99589fc74824fde5ba1d2b0ae166 Mon Sep 17 00:00:00 2001 From: Johannes Eriksson Date: Tue, 15 Sep 2026 11:19:28 +0200 Subject: [PATCH 1/2] fix(react): include props.options in ctxToMultiEnumControlProps's options memo ctxToMultiEnumControlProps memoized its options result with a useMemo call missing props.options from its dependency array, unlike its two siblings, ctxToEnumControlProps and ctxToOneOfEnumControlProps, which both correctly include it. mapStateToMultiEnumControlProps already reads ownProps.options as its own preferred source over deriving options from the schema, so this is a supported way to supply options externally - a HOC composed outside withJsonFormsMultiEnumProps that injects options asynchronously (e.g. after a remote fetch resolves) never saw that update take effect, since none of the memo's other dependencies change when the options later arrive. Also widens the props parameter type from OwnPropsOfControl to OwnPropsOfControl & OwnPropsOfEnum, matching ctxToOneOfEnumControlProps - the narrower type was masking the missing dependency, since props.options wasn't even a recognized property on it before. --- packages/react/src/JsonFormsContext.tsx | 4 +- packages/react/test/JsonFormsContext.test.tsx | 70 +++++++++++++++++++ 2 files changed, 72 insertions(+), 2 deletions(-) diff --git a/packages/react/src/JsonFormsContext.tsx b/packages/react/src/JsonFormsContext.tsx index c4b4c3a44b..9006ffbe6a 100644 --- a/packages/react/src/JsonFormsContext.tsx +++ b/packages/react/src/JsonFormsContext.tsx @@ -338,7 +338,7 @@ export const ctxToOneOfEnumControlProps = ( export const ctxToMultiEnumControlProps = ( ctx: JsonFormsStateContext, - props: OwnPropsOfControl + props: OwnPropsOfControl & OwnPropsOfEnum ) => { const enumProps = mapStateToMultiEnumControlProps( { jsonforms: { ...ctx } }, @@ -350,7 +350,7 @@ export const ctxToMultiEnumControlProps = ( */ const options = useMemo( () => enumProps.options, - [enumProps.schema, ctx.i18n?.translate] + [props.options, enumProps.schema, ctx.i18n?.translate] ); return { ...enumProps, options }; }; diff --git a/packages/react/test/JsonFormsContext.test.tsx b/packages/react/test/JsonFormsContext.test.tsx index c6ca58773d..f35216dab6 100644 --- a/packages/react/test/JsonFormsContext.test.tsx +++ b/packages/react/test/JsonFormsContext.test.tsx @@ -29,6 +29,7 @@ import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import { CellProps, ControlProps, + DispatchPropsOfMultiEnumControl, JsonSchema, NOT_APPLICABLE, OwnPropsOfEnum, @@ -41,6 +42,7 @@ import { withJsonFormsDetailProps, withJsonFormsEnumCellProps, withJsonFormsEnumProps, + withJsonFormsMultiEnumProps, } from '../src/JsonFormsContext'; Enzyme.configure({ adapter: new Adapter() }); @@ -148,6 +150,74 @@ test('withJsonFormsEnumProps - enum: should supply control and enum props', () = ]); }); +test('withJsonFormsMultiEnumProps - should update options when an externally supplied options own prop changes after mount', () => { + let setExternalOptions: (options: OwnPropsOfEnum['options']) => void; + + const MockMultiEnumControlUnwrapped = ( + _: ControlProps & OwnPropsOfEnum & DispatchPropsOfMultiEnumControl + ) => { + return <>; + }; + + const MockMultiEnumControl = withJsonFormsMultiEnumProps( + MockMultiEnumControlUnwrapped + ); + + const ExternalOptionsProvider = (ownProps: any) => { + const [options, setOptions] = React.useState([ + { value: 'red', label: 'Red' }, + ]); + setExternalOptions = setOptions; + return ; + }; + + const schema = { + type: 'object', + properties: { + colors: { + type: 'array', + items: { type: 'string' }, + }, + }, + }; + + const renderers = [ + { + tester: rankWith(1, () => true), + renderer: ExternalOptionsProvider, + }, + ]; + + const uischema = { + type: 'Control', + scope: '#/properties/colors', + }; + + const wrapper = mount( + + ); + + expect(wrapper.find(MockMultiEnumControlUnwrapped).props().options).toEqual([ + { value: 'red', label: 'Red' }, + ]); + + setExternalOptions([ + { value: 'red', label: 'Red' }, + { value: 'green', label: 'Green' }, + ]); + wrapper.update(); + + expect(wrapper.find(MockMultiEnumControlUnwrapped).props().options).toEqual([ + { value: 'red', label: 'Red' }, + { value: 'green', label: 'Green' }, + ]); +}); + test('withJsonFormsEnumCellProps - constant: should supply control and enum props', () => { const MockEnumCellUnwrapped = (_: CellProps & OwnPropsOfEnum) => { return <>; From 0e5ab7ae093a12deb69a7f49afba3822d64fa717 Mon Sep 17 00:00:00 2001 From: Johannes Eriksson Date: Tue, 15 Sep 2026 12:54:22 +0200 Subject: [PATCH 2/2] test: address review feedback on the multi-enum options memo test - wrap the external options state update in act() to avoid the React act warning, and add @types/react-dom (already used elsewhere in this monorepo) so it typechecks - capture the setter via useEffect instead of during render, so the test doesn't depend on React's render timing - give the array items schema a oneOf, so the assertions actually prove ownProps.options takes precedence over the schema-derived fallback rather than both being empty --- packages/react/package.json | 1 + packages/react/test/JsonFormsContext.test.tsx | 24 ++++++++++++++----- pnpm-lock.yaml | 7 ++++-- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index a7d2c548e8..e17488066f 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -103,6 +103,7 @@ "@types/jest": "^29.5.14", "@types/object-hash": "^1.3.0", "@types/react": "^17.0.24", + "@types/react-dom": "^17.0.9", "@types/react-redux": "^7.1.5", "@typescript-eslint/eslint-plugin": "^5.54.1", "@typescript-eslint/parser": "^5.54.1", diff --git a/packages/react/test/JsonFormsContext.test.tsx b/packages/react/test/JsonFormsContext.test.tsx index f35216dab6..469b874c29 100644 --- a/packages/react/test/JsonFormsContext.test.tsx +++ b/packages/react/test/JsonFormsContext.test.tsx @@ -25,6 +25,7 @@ import React from 'react'; import Enzyme, { mount } from 'enzyme'; +import { act } from 'react-dom/test-utils'; import Adapter from '@wojtekmaj/enzyme-adapter-react-17'; import { CellProps, @@ -167,7 +168,9 @@ test('withJsonFormsMultiEnumProps - should update options when an externally sup const [options, setOptions] = React.useState([ { value: 'red', label: 'Red' }, ]); - setExternalOptions = setOptions; + React.useEffect(() => { + setExternalOptions = setOptions; + }, []); return ; }; @@ -176,7 +179,14 @@ test('withJsonFormsMultiEnumProps - should update options when an externally sup properties: { colors: { type: 'array', - items: { type: 'string' }, + items: { + type: 'string', + oneOf: [ + { const: 'red', title: 'Schema Red' }, + { const: 'green', title: 'Schema Green' }, + { const: 'blue', title: 'Schema Blue' }, + ], + }, }, }, }; @@ -206,10 +216,12 @@ test('withJsonFormsMultiEnumProps - should update options when an externally sup { value: 'red', label: 'Red' }, ]); - setExternalOptions([ - { value: 'red', label: 'Red' }, - { value: 'green', label: 'Green' }, - ]); + act(() => { + setExternalOptions([ + { value: 'red', label: 'Red' }, + { value: 'green', label: 'Green' }, + ]); + }); wrapper.update(); expect(wrapper.find(MockMultiEnumControlUnwrapped).props().options).toEqual([ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 10fe454d85..405d5251f7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -777,6 +777,9 @@ importers: '@types/react': specifier: ^17.0.24 version: 17.0.80 + '@types/react-dom': + specifier: ^17.0.9 + version: 17.0.25 '@types/react-redux': specifier: ^7.1.5 version: 7.1.33 @@ -21061,7 +21064,7 @@ snapshots: axios@1.13.2: dependencies: - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6(debug@4.4.3) form-data: 4.0.5 proxy-from-env: 1.1.0 transitivePeerDependencies: @@ -24690,7 +24693,7 @@ snapshots: http-proxy@1.18.1: dependencies: eventemitter3: 4.0.7 - follow-redirects: 1.15.6(debug@4.3.4) + follow-redirects: 1.15.6(debug@4.4.3) requires-port: 1.0.0 transitivePeerDependencies: - debug