diff --git a/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js b/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js index d32a5b647f..085d7a01a1 100644 --- a/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js +++ b/src/openforms/js/components/admin/form_design/logic/actions/dmn/DMNActionConfig.stories.js @@ -1,4 +1,4 @@ -import {expect, fireEvent, fn, userEvent, waitFor, within} from '@storybook/test'; +import {expect, findByRole, fireEvent, fn, userEvent, waitFor, within} from '@storybook/test'; import selectEvent from 'react-select-event'; import { @@ -8,7 +8,7 @@ import { } from 'components/admin/form_design/mocks'; import {FormDecorator} from 'components/admin/form_design/story-decorators'; import {serializeValue} from 'components/admin/forms/VariableMapping'; -import {getReactSelectInput} from 'utils/storybookTestHelpers'; +import {getReactSelectContainer} from 'utils/storybookTestHelpers'; import DMNActionConfig from './DMNActionConfig'; @@ -187,7 +187,7 @@ export const Empty = { outputMapping: [], }, }, - play: async ({canvasElement, step}) => { + play: async ({canvasElement, step, args}) => { const canvas = within(canvasElement); const originalConfirm = window.confirm; window.confirm = () => true; @@ -230,16 +230,25 @@ export const Empty = { const dropdowns = within(document.querySelector('.logic-dmn__mapping-config')).getAllByRole( 'combobox' ); - - await expect(dropdowns.length).toBe(2); + expect(dropdowns.length).toBe(2); const [formVarsDropdowns, dmnVarsDropdown] = dropdowns; await selectEvent.select(formVarsDropdowns, 'Name'); - await userEvent.selectOptions(dmnVarsDropdown, 'Camunda variable'); + // this is super flaky for some reason on both Chromium and Firefox :/ + await waitFor(async () => { + await userEvent.selectOptions(dmnVarsDropdown, 'Camunda variable'); + expect(dmnVarsDropdown).toHaveValue(serializeValue('camundaVar')); + }); - await expect(getReactSelectInput(formVarsDropdowns)).toHaveValue('name'); - await expect(dmnVarsDropdown).toHaveValue(serializeValue('camundaVar')); + await userEvent.click(canvas.getByRole('button', {name: 'Save'})); + expect(args.onSave).toHaveBeenCalledWith({ + pluginId: 'camunda7', + decisionDefinitionId: 'approve-payment', + decisionDefinitionVersion: '2', + inputMapping: [{formVariable: 'name', dmnVariable: 'camundaVar'}], + outputMapping: [], + }); }); await step('Changing plugin clears decision definition, version and DMN vars', async () => { @@ -320,9 +329,15 @@ export const withInitialValues = { const formVariableDropdowns = await canvas.findAllByLabelText('Formuliervariabele'); await waitFor(async () => { - await expect(getReactSelectInput(formVariableDropdowns[0])).toHaveValue('name'); - await expect(getReactSelectInput(formVariableDropdowns[1])).toHaveValue('surname'); - await expect(getReactSelectInput(formVariableDropdowns[2])).toHaveValue('canApply'); + expect( + await within(getReactSelectContainer(formVariableDropdowns[0])).findByText('Name') + ).toBeVisible(); + expect( + await within(getReactSelectContainer(formVariableDropdowns[1])).findByText('Surname') + ).toBeVisible(); + expect( + await within(getReactSelectContainer(formVariableDropdowns[2])).findByText('Can apply?') + ).toBeVisible(); }); }); diff --git a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js index e4d1692372..9f2dbc27b6 100644 --- a/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js +++ b/src/openforms/js/components/admin/form_design/variables/VariablesEditor.stories.js @@ -6,7 +6,6 @@ import { } from 'components/admin/form_design/mocks'; import {BACKEND_OPTIONS_FORMS} from 'components/admin/form_design/registrations'; import {mockTargetPathsPost} from 'components/admin/form_design/registrations/objectsapi/mocks'; -import {getReactSelectInput} from 'utils/storybookTestHelpers'; import {serializeValue} from '../../forms/VariableMapping'; import {mockObjecttypeVersionsGet, mockObjecttypesGet} from '../registrations/objectsapi/mocks'; @@ -590,32 +589,39 @@ export const ConfigurePrefill = { }; export const ConfigurePrefillObjectsAPI = { - play: async ({canvasElement}) => { + play: async ({canvasElement, step}) => { const canvas = within(canvasElement); - const userDefinedVarsTab = await canvas.findByRole('tab', {name: 'Gebruikersvariabelen'}); - expect(userDefinedVarsTab).toBeVisible(); - await userEvent.click(userDefinedVarsTab); + await step('Open configuration modal', async () => { + const userDefinedVarsTab = await canvas.findByRole('tab', {name: 'Gebruikersvariabelen'}); + expect(userDefinedVarsTab).toBeVisible(); + await userEvent.click(userDefinedVarsTab); - // open modal for configuration - const editIcon = canvas.getByTitle('Prefill instellen'); - await userEvent.click(editIcon); - - const pluginDropdown = await screen.findByLabelText('Plugin'); - expect(pluginDropdown).toBeVisible(); - - await userEvent.selectOptions(pluginDropdown, 'Objects API'); - - const variableSelect = await screen.findByLabelText('Formuliervariabele'); - await expect(getReactSelectInput(variableSelect)).toHaveValue('formioComponent'); + // open modal for configuration + const editIcon = canvas.getByTitle('Prefill instellen'); + await userEvent.click(editIcon); + expect(await screen.findByRole('dialog')).toBeVisible(); + }); - // Wait until the API call to retrieve the prefillAttributes is done - await waitFor(async () => { - const prefillPropertySelect = await screen.findByLabelText( - 'Select a property from the object type' - ); - expect(prefillPropertySelect).toBeVisible(); - expect(prefillPropertySelect).toHaveValue(serializeValue(['firstName'])); + await step('Configure Objects API prefill', async () => { + const modal = within(await screen.findByRole('dialog')); + const pluginDropdown = await screen.findByLabelText('Plugin'); + expect(pluginDropdown).toBeVisible(); + await userEvent.selectOptions(pluginDropdown, 'Objects API'); + + // check mappings + const variableSelect = await screen.findByLabelText('Formuliervariabele'); + expect(variableSelect).toBeVisible(); + expect(modal.getByText('Form.io component')).toBeVisible(); + + // Wait until the API call to retrieve the prefillAttributes is done + await waitFor(async () => { + const prefillPropertySelect = await screen.findByLabelText( + 'Select a property from the object type' + ); + expect(prefillPropertySelect).toBeVisible(); + expect(prefillPropertySelect).toHaveValue(serializeValue(['firstName'])); + }); }); }, }; diff --git a/src/openforms/js/components/admin/forms/ReactSelect.js b/src/openforms/js/components/admin/forms/ReactSelect.js index 62b3f1969a..263d9c838f 100644 --- a/src/openforms/js/components/admin/forms/ReactSelect.js +++ b/src/openforms/js/components/admin/forms/ReactSelect.js @@ -32,23 +32,38 @@ const styles = { }), }; +/** + * Find the option by `value` in the list of possible options. + * + * `options` are the options passed to ReactSelect, which may be a list of options or + * a list of option groups: `(Option | Group)[]`. If an item is a group, it has a nested + * property `options` of type `Option[]`. + * + * See https://github.com/JedWatson/react-select/blob/a8b8f4342cc113e921bb238de2dd69a2d345b5f8/packages/react-select/src/Select.tsx#L406 + * for a reference. + */ const getValue = (options, value) => { - if (!value) { + // 0 could be an actual selected value (!) + if (value == null || value === '') { return null; } - // We support grouped ReactSelect options. - // So we need to look in the first, and possible second level of options - for (let index = 0; index < options.length; index++) { - const option = options[index]; - if ('value' in option && option.value === value) { - return option; - } - const foundOption = (option?.options || []).find(opt => opt.value === value); - if (foundOption) { - return foundOption; + // check all the options until we find a match on the `value` property for the option. + // ReactSelect allows using different properties for the value, but let's handle that + // when we actually need it. + for (const groupOrOption of options) { + if ('options' in groupOrOption) { + const hit = getValue(groupOrOption.options, value); + if (hit) return hit; // otherwise continue with the rest + } else { + // we're dealing with a plain option, compare the value property + if (groupOrOption.value === value) { + return groupOrOption; + } } } + + return null; }; /** @@ -59,27 +74,22 @@ const getValue = (options, value) => { * @deprecated - if possible, refactor the form to use Formik and use the Formik-enabled * variant. */ -const SelectWithoutFormik = ({name, options, value, className, onChange, ...props}) => { - const classes = classNames('admin-react-select', { - [`${className}`]: className, - }); - return ( - { - onChange(selectedOption === null ? undefined : selectedOption.value); - }} - {...props} - /> - ); -}; +const SelectWithoutFormik = ({name, options, value, className, onChange, ...props}) => ( + { + onChange(selectedOption === null ? undefined : selectedOption.value); + }} + {...props} + /> +); /** * A select dropdown backed by react-select for Formik forms. @@ -90,13 +100,10 @@ const SelectWithFormik = ({name, options, className, ...props}) => { const [fieldProps, , fieldHelpers] = useField(name); const {value} = fieldProps; const {setValue} = fieldHelpers; - const classes = classNames('admin-react-select', { - [`${className}`]: className, - }); return ( ( <> - + ), + args: { + includeStaticVariables: true, + }, + parameters: { formik: { onSubmit: fn(), @@ -140,18 +147,23 @@ export const SelectOptions = { await step('Check rendered values', async () => { const formVariableDropdown = canvas.getByLabelText('Formuliervariabele'); - await selectEvent.openMenu(formVariableDropdown); - const variableOptions = getReactSelectOptions(formVariableDropdown); + selectEvent.openMenu(formVariableDropdown); + + const formVarOptions = await within( + getReactSelectContainer(formVariableDropdown) + ).findAllByRole('option'); - await expect(variableOptions).toHaveLength(2); - await expect(variableOptions[1]).toHaveTextContent('(key2)'); + expect(formVarOptions).toHaveLength(2); + expect(formVarOptions[0]).toHaveTextContent('key2'); + expect(formVarOptions[0]).toHaveTextContent('Name 2'); + await userEvent.click(formVariableDropdown); // close the menu again const propertyDropdown = canvas.getByLabelText('Pick a property'); const propertyOptions = within(propertyDropdown).getAllByRole('option'); - await expect(propertyOptions).toHaveLength(3); - await expect(propertyOptions[1]).toHaveValue(serializeValue('option_1')); - await expect(propertyOptions[2]).toHaveValue(serializeValue('option_2')); + expect(propertyOptions).toHaveLength(3); + expect(propertyOptions[1]).toHaveValue(serializeValue('option_1')); + expect(propertyOptions[2]).toHaveValue(serializeValue('option_2')); }); await step('Select different option and submit', async () => { diff --git a/src/openforms/js/utils/storybookTestHelpers.js b/src/openforms/js/utils/storybookTestHelpers.js index 8d478f46af..09fb2a8849 100644 --- a/src/openforms/js/utils/storybookTestHelpers.js +++ b/src/openforms/js/utils/storybookTestHelpers.js @@ -1,11 +1,19 @@ -import {within} from '@storybook/test'; - -const getReactSelectInput = SelectElement => { - return SelectElement.closest('.admin-react-select').querySelector('input[type="hidden"]'); -}; - -const getReactSelectOptions = SelectElement => { - return within(SelectElement.closest('.admin-react-select')).getAllByRole('option'); +/** + * From the input field (retrieved by accessible queries), find the react-select container. + * + * Equivalent of https://github.com/romgain/react-select-event/blob/8619e8b3da349eadfa7321ea4aa2b7eee7209f9f/src/index.ts#L14, + * however instead of relying on the DOM structure we can leverage class names that are + * guaranteed to be set by us. + * + * Usage: + * + * const dropdown = canvas.getByLabelText('My dropdown'); + * const container = getReactSelectContainer(dropdown); + * const options = within(container).queryByRole('option'); + */ +const getReactSelectContainer = comboboxInput => { + const container = comboboxInput.closest('.admin-react-select'); + return container; }; -export {getReactSelectInput, getReactSelectOptions}; +export {getReactSelectContainer};