From 9b0d58d318e27eb456613a6d1843edb60fba19a0 Mon Sep 17 00:00:00 2001 From: Amal K Joy <153802538+amal-k-joy@users.noreply.github.com> Date: Wed, 17 Jul 2024 18:56:27 +0530 Subject: [PATCH] feat(ConditionBuilder): adding test cases and issue fixes (#5685) * feat(ConditionBuilder): adding testcases and issue fixes * feat(ConditionBuilder): adding testcases for tree variant --- .../ConditionBuilder.stories.jsx | 2 +- .../ConditionBuilder/ConditionBuilder.test.js | 1227 ++++++++++++++++- .../ConditionBuilderContent.js | 106 +- .../translationObject.js | 1 + .../ConditionGroupBuilder.js | 22 +- .../ConditionBuilder/assets/sampleInput.js | 82 +- .../utils/handleKeyboardEvents.js | 5 +- .../components/ConditionBuilder/utils/util.js | 14 - 8 files changed, 1314 insertions(+), 145 deletions(-) diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilder.stories.jsx b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilder.stories.jsx index 88ac2fdacc..43eb5e3144 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilder.stories.jsx +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilder.stories.jsx @@ -181,7 +181,7 @@ const getOptions = async (conditionState, { property }) => { } }; const requiredProps = { - startConditionLabel: 'Add Condition', + startConditionLabel: 'Add condition', popOverSearchThreshold: 4, getConditionState: (rootState) => { console.log(rootState); diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilder.test.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilder.test.js index c1bf302bec..1a1c9de579 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilder.test.js +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilder.test.js @@ -5,14 +5,22 @@ * LICENSE file in the root directory of this source tree. */ -import React from 'react'; -import { render, screen } from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro +import React, { act } from 'react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; // https://testing-library.com/docs/react-testing-library/intro import { pkg } from '../../settings'; import uuidv4 from '../../global/js/utils/uuidv4'; +import { Earth } from '@carbon/react/icons'; import { ConditionBuilder } from '.'; import cx from 'classnames'; +import userEvent from '@testing-library/user-event'; + +import { inputData, inputDataDynamicOptions } from './assets/sampleInput'; +import { + sampleDataStructure_sentence, + // sampleDataStructure_tree, +} from './assets/SampleData'; const blockClass = `${pkg.prefix}--condition-builder`; const componentName = ConditionBuilder.displayName; @@ -22,68 +30,1223 @@ const className = `class-${uuidv4()}`; const dataTestId = uuidv4(); const defaultProps = { inputConfig: {}, - startConditionLabel: 'Add Condition', + startConditionLabel: 'Add condition', popOverSearchThreshold: 4, getConditionState: () => {}, variant: 'sentence', }; +const inputConfigOptionType = { + properties: [ + { + id: 'continent', + label: 'Continent', + icon: Earth, + type: 'option', + config: { + options: [ + { + label: 'Africa', + id: 'Africa', + }, + { + label: 'Antarctica', + id: 'Antarctica', + }, + ], + }, + }, + ], +}; + +const getContinents = () => { + return [ + { + label: 'Africa', + id: 'Africa', + }, + { + label: 'Antarctica', + id: 'Antarctica', + }, + { + label: 'Asia', + id: 'Asia', + }, + { + label: 'Australia', + id: 'Australia', + }, + { + label: 'Europe', + id: 'Europe', + }, + ]; +}; + +const getOptions = async (conditionState, { property }) => { + switch (property) { + case 'continent': + return new Promise((resolve) => { + setTimeout(() => { + resolve(getContinents()); + }, 2000); + }); + + default: + return []; + } +}; + describe(componentName, () => { it('renders a component ConditionBuilder', async () => { - render( ); + render(); expect(screen.getByRole('main')).toHaveClass(cx(blockClass)); }); it('has no accessibility violations', async () => { - const { container } = render( - - ); + const { container } = render(); expect(container).toBeAccessible(componentName); expect(container).toHaveNoAxeViolations(); }); - // it(`renders children`, async () => { - // render({children}); - // screen.getByText(children); - // }); - it('applies className to the containing node', async () => { - render( - - {' '} - - ); + render(); expect(screen.getByRole('main')).toHaveClass(className); }); it('adds additional props to the containing node', async () => { - render( - - {' '} - - ); + render(); screen.getByTestId(dataTestId); }); it('forwards a ref to an appropriate node', async () => { const ref = React.createRef(); - render( - - {' '} - - ); + render(); expect(ref.current).toHaveClass(blockClass); }); it('adds the Devtools attribute to the containing node', async () => { - render( - - {' '} - - ); + render(); expect(screen.getByTestId(dataTestId)).toHaveDevtoolsAttribute( componentName ); }); + + //test cases for sentence variant + it('should render the component with provided label to start condition builder', async () => { + const startConditionLabel = 'Add condition'; + render( + + ); + + expect(screen.getByText(startConditionLabel)); + }); + + it('render the component with input type as single select option', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + expect(screen.getByRole('option', { name: 'Continent' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'Continent' })) + ); + + expect(screen.getByRole('option', { name: 'is' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'is' })) + ); + + expect(screen.getByRole('option', { name: 'Africa' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'Africa' })) + ); + + const selectedItem = screen.getByRole('button', { name: 'Africa' }); + + expect(selectedItem); + }); + + it('render the component with input type as multiselect option', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + expect(screen.getByRole('option', { name: 'Continent' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'Continent' })) + ); + + expect(screen.getByRole('option', { name: 'is one of' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'is one of' })) + ); + + //selection option 1 + expect(screen.getByRole('option', { name: 'Africa' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'Africa' })) + ); + + //selection option 2 + + expect(screen.getByRole('option', { name: 'Antarctica' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'Antarctica' })) + ); + + //selecting and deselecting option 3 + expect(screen.getByRole('option', { name: 'Asia' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'Asia' })) + ); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'Asia' })) + ); + + //clicking outside + const container = document.querySelector(`.${blockClass}`); + await act(() => userEvent.click(container)); + + const selectedItem = screen.getByRole('button', { + name: 'Africa, Antarctica', + }); + expect(selectedItem); + }); + + it('checking select/deselect all functionality for the input type option with multiselect', async () => { + render( + + ); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + expect(screen.getByRole('option', { name: 'Continent' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'Continent' })) + ); + + expect(screen.getByRole('option', { name: 'is one of' })); + + await act(() => + userEvent.click(screen.getByRole('option', { name: 'is one of' })) + ); + + //selecting all + const selectAllButton = screen.getByRole('button', { + name: 'Select all', + }); + + await act(() => userEvent.click(selectAllButton)); + + const selectedItems = screen.getByRole('button', { + name: 'Africa, Antarctica', + }); + expect(selectedItems); + + //de-selecting all + const deSelectAllButton = screen.getByRole('button', { + name: 'Deselect all', + }); + + await act(() => userEvent.click(deSelectAllButton)); + + //selecting one + expect(screen.getByText('Antarctica')); + + await act(() => userEvent.click(screen.getByText('Antarctica'))); + + //clicking outside + const container = document.querySelector(`.${blockClass}`); + await act(() => userEvent.click(container)); + + const selectedItem = screen.getByRole('button', { + name: 'Antarctica', + }); + expect(selectedItem); + }); + + it('check search feature is functioning in popover', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + expect(screen.getByText('Continent')); + + await act(() => userEvent.click(screen.getByText('Continent'))); + + expect(screen.getByText('is one of')); + + await act(() => userEvent.click(screen.getByText('is one of'))); + + const searchInput = screen.getByRole('searchbox'); + expect(searchInput); + + fireEvent.change(searchInput, { target: { value: 'Antarctica' } }); + + expect(screen.getByText('Antarctica')).toBeInTheDocument(); + }); + + it('checking Add condition and close condition functionality', async () => { + render( + + ); + //start builder + fireEvent.click(screen.getByText('Add condition')); + + //add first condition + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Continent', + }) + ) + ); + + fireEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ); + + fireEvent.click( + screen.getByRole('option', { + name: 'Africa', + }) + ); + + const addButton = document.querySelector(`.${blockClass}__add-button`); + expect(addButton); + await act(() => userEvent.click(addButton)); + + //add second condition + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Continent', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + await act(() => userEvent.click(screen.getByText('Antarctica'))); + + const selectedItem2 = screen.getByRole('button', { name: 'Antarctica' }); + + expect(selectedItem2); + + const firstCloseButton = document.querySelector( + `.${blockClass}__close-condition` + ); + expect(firstCloseButton); + fireEvent.click(firstCloseButton); + + expect(screen.queryByText('Africa')).not.toBeInTheDocument(); + }); + + it('changing condition connector and all connectors within a group should be synchronize', async () => { + render( + + ); + //start builder + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + await act(() => + userEvent.click(screen.getAllByRole('button', { name: 'and' })[0]) + ); + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'or', + }) + ) + ); + + expect(screen.queryByText('and')).not.toBeInTheDocument(); + }); + + it('render the component with input type text', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'ID', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + const inputText = document.querySelector('#id'); + fireEvent.change(inputText, { target: { value: 'testID123' } }); + + const container = document.querySelector(`.${blockClass}`); + await act(() => userEvent.click(container)); + + const selectedItem = screen.getByRole('button', { name: 'testID123' }); + + expect(selectedItem); + }); + + it('render the component with input type textarea', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Id Long', + }) + ) + ); + + const isOperator = screen.getByRole('option', { + name: 'is', + }); + await act(() => userEvent.click(isOperator)); + + const inputText = document.querySelector('#id_long'); + fireEvent.change(inputText, { target: { value: 'testID123' } }); + + const container = document.querySelector(`.${blockClass}`); + await act(() => userEvent.click(container)); + + const selectedItem = screen.getByRole('button', { name: 'testID123' }); + + expect(selectedItem); + }); + + it('render the component with input type number', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Price', + }) + ) + ); + + const isOperator = screen.getByRole('option', { + name: 'is', + }); + await act(() => userEvent.click(isOperator)); + + let inputText = document.querySelector('#price'); + fireEvent.change(inputText, { target: { value: '123' } }); + + const container = document.querySelector(`.${blockClass}`); + await act(() => userEvent.click(container)); + + const selectedItem = screen.getByRole('button', { name: '123 Dollars' }); + + expect(selectedItem); + + await act(() => userEvent.click(selectedItem)); + inputText = document.querySelector('#price'); + fireEvent.change(inputText, { target: { value: '-123' } }); + + await act(() => userEvent.click(container)); + + expect(screen.getByRole('button', { name: 'Incomplete' })); + }); + + it('render the component with input type date', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Date', + }) + ) + ); + + const isOperator = screen.getByRole('option', { + name: 'is', + }); + await act(() => userEvent.click(isOperator)); + + const inputElement = document.querySelector('#datePicker'); + await act(() => userEvent.type(inputElement, '12/06/2024{enter}')); + + await act(() => userEvent.keyboard('{escape}')); + + const selectedItem = screen.getByRole('button', { name: '12/06/2024' }); + + expect(selectedItem); + }); + + it('render the component with input type time', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Time', + }) + ) + ); + + const isOperator = screen.getByRole('option', { + name: 'is', + }); + await act(() => userEvent.click(isOperator)); + + const timeElement = document.querySelector('#time-picker'); + await act(() => userEvent.type(timeElement, '12:30')); + + const dayZoneElement = document.querySelector('#time-picker-day-zone'); + await act(() => + fireEvent.change(dayZoneElement, { target: { value: 'PM' } }) + ); + expect(dayZoneElement).toHaveValue('PM'); + + const timeZoneElement = document.querySelector('#time-picker-time-zone'); + await act(() => + fireEvent.change(timeZoneElement, { target: { value: 'UTC' } }) + ); + expect(timeZoneElement).toHaveValue('UTC'); + + await act(() => userEvent.keyboard('{escape}')); + + const selectedItem = screen.getByRole('button', { name: '12:30 PM UTC' }); + + expect(selectedItem); + }); + + it('fetch options dynamically', async () => { + render( + + ); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + expect(screen.getByText('Continent')); + + await act(() => userEvent.click(screen.getByText('Continent'))); + + expect(screen.getByText('is')); + + await act(() => userEvent.click(screen.getByText('is'))); + + //fetching the options dynamically and it will be resolved after 2 seconds + + await waitFor(() => screen.getByText('Africa'), { timeout: 2500 }); + + await act(() => userEvent.click(screen.getByText('Africa'))); + + const selectedItem = screen.getByRole('button', { name: 'Africa' }); + + expect(selectedItem); + }); + + it('check translation are working as expected', async () => { + const translateWithId = (key) => { + const translationsObject = { + conditionHeadingText: 'Condition Heading', + }; + + return translationsObject[key]; + }; + + render( + + ); + //start builder + await act(() => userEvent.click(screen.getByText('Add condition'))); + + expect(screen.getByText('Condition Heading')); + }); + + //test cases for tree variant + it('render the tree variant with 3 conditions and 1 subgroup', async () => { + render( + + ); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + //adding condition 1 + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Continent', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + await act(() => userEvent.click(screen.getByText('Africa'))); + + //adding condition 2 + + let addButton = document.querySelector(`.${blockClass}__add-button`); + expect(addButton); + await act(() => userEvent.click(addButton)); + + const regionOption = screen.getByRole('option', { + name: 'Region', + }); + await act(() => userEvent.click(regionOption)); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'India', + }) + ) + ); + + //adding a subgroup + + let addSubGroupButton = document.querySelector( + `.${blockClass}__add-condition-sub-group` + ); + expect(addSubGroupButton); + await act(() => userEvent.click(addSubGroupButton)); + + //add third condition + + const colorOption = screen.getByRole('option', { + name: 'Color', + }); + await act(() => userEvent.click(colorOption)); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'black', + }) + ) + ); + const subGroups = screen.getAllByText('if'); + expect(subGroups).toHaveLength(2); + }); + + it('render the tree variant with 2 groups', async () => { + render( + + ); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + //group 1 + //adding condition 1 + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Continent', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + await act(() => userEvent.click(screen.getByText('Africa'))); + + //adding condition 2 + + let addButton = document.querySelector(`.${blockClass}__add-button`); + expect(addButton); + await act(() => userEvent.click(addButton)); + + const regionOption = screen.getByRole('option', { + name: 'Region', + }); + await act(() => userEvent.click(regionOption)); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'India', + }) + ) + ); + + //adding a subgroup + + let addSubGroupButton = document.querySelector( + `.${blockClass}__add-condition-sub-group` + ); + expect(addSubGroupButton); + await act(() => userEvent.click(addSubGroupButton)); + + //add third condition + + const colorOption = screen.getByRole('option', { + name: 'Color', + }); + await act(() => userEvent.click(colorOption)); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'black', + }) + ) + ); + const subGroups = screen.getAllByText('if'); + expect(subGroups).toHaveLength(2); + + //group 2 + + const addGroupButton = document.querySelector( + `.${blockClass}__add-condition-group` + ); + expect(addGroupButton); + await act(() => userEvent.click(addGroupButton)); + //adding condition 1 + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Continent', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Africa', + }) + ) + ); + + const ifStatements = screen.getAllByRole('button', { name: 'if' }); + expect(ifStatements).toHaveLength(3); + + const groupConnector = screen.getAllByRole('button', { name: 'or' }); + expect(groupConnector).toHaveLength(1); + }); + + it('check the next/previous close button is focussed on remove condition', async () => { + render( + + ); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + let closeButtons = document.querySelectorAll( + `.${blockClass}__close-condition` + ); + expect(closeButtons).toHaveLength(3); + //click first close button + await act(() => userEvent.click(closeButtons[0])); + + closeButtons = document.querySelectorAll(`.${blockClass}__close-condition`); + expect(closeButtons).toHaveLength(2); + + expect(closeButtons[0]).toHaveFocus(); + + //click last close button + + await act(() => userEvent.click(closeButtons[1])); + closeButtons = document.querySelectorAll(`.${blockClass}__close-condition`); + expect(closeButtons).toHaveLength(1); + expect(closeButtons[0]).toHaveFocus(); + }); + + it('check the next/previous close button is focussed on remove condition for tree variant', async () => { + const sampleDataStructure = { + operator: 'or', + groups: [ + { + groupOperator: 'and', //'and|or', + statement: 'if', // 'if|exclude if', + id: uuidv4(), + conditions: [ + { + property: 'region', + operator: 'is', + value: 'IL', + id: uuidv4(), + }, + { + property: 'delivery', + operator: 'is', + value: 'processing', + id: uuidv4(), + }, + { + property: 'delivery', + operator: 'is', + value: 'processing', + id: uuidv4(), + }, + { + property: 'delivery', + operator: 'is', + value: 'processing', + id: uuidv4(), + }, + { + groupOperator: 'and', + statement: 'if', + id: uuidv4(), + conditions: [ + { + property: 'region', + operator: 'is', + value: 'IL', + id: uuidv4(), + }, + { + property: 'delivery', + operator: 'is', + value: 'processing', + id: uuidv4(), + }, + { + property: 'delivery', + operator: 'is', + value: 'processing', + id: uuidv4(), + }, + ], + }, + ], + }, + ], + }; + + render( + + ); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + let closeButtons = document.querySelectorAll( + `.${blockClass}__close-condition` + ); + expect(closeButtons).toHaveLength(7); + //click first close button + await act(() => userEvent.click(closeButtons[0])); + + closeButtons = document.querySelectorAll(`.${blockClass}__close-condition`); + expect(closeButtons).toHaveLength(6); + + expect(closeButtons[0]).toHaveFocus(); + + //click 4th(first condition in first subgroup) close button + + await act(() => userEvent.click(closeButtons[3])); + closeButtons = document.querySelectorAll(`.${blockClass}__close-condition`); + expect(closeButtons).toHaveLength(5); + expect(closeButtons[3]).toHaveFocus(); + + //close all conditions of the subgroup + + await act(() => userEvent.click(closeButtons[4])); + closeButtons = document.querySelectorAll(`.${blockClass}__close-condition`); + await act(() => userEvent.click(closeButtons[3])); + closeButtons = document.querySelectorAll(`.${blockClass}__close-condition`); + expect(closeButtons).toHaveLength(3); + + //when all conditions of a subgroup is closed , it will focus the previous row + const row = document.querySelectorAll( + '[role="row"][aria-level="2"][aria-posinset="3"]' + ); + expect(row).toHaveLength(1); + expect(row[0]).toHaveFocus(); + }); + + it('check the add/remove actions ', async () => { + const sampleDataStructure = { + operator: 'or', + groups: [ + { + groupOperator: 'and', //'and|or', + statement: 'if', // 'if|exclude if', + id: uuidv4(), + conditions: [ + { + property: 'region', + operator: 'is', + value: 'IL', + id: uuidv4(), + }, + ], + }, + ], + }; + + const actions = [ + { + id: uuidv4(), + label: 'Add item to cart', + }, + { id: uuidv4(), label: 'Proceed item to checkout' }, + ]; + + render( + + ); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + //click on add action button + await act(() => + userEvent.click( + document.querySelector( + `.${blockClass}__actions-container .${blockClass}__add-button` + ) + ) + ); + + expect( + screen.getByRole('option', { + name: 'Add item to cart', + }) + ); + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Add item to cart', + }) + ) + ); + + expect( + screen.getByRole('button', { + name: 'Add item to cart', + }) + ); + + //add second action + await act(() => + userEvent.click( + document.querySelector( + `.${blockClass}__actions-container .${blockClass}__add-button` + ) + ) + ); + + expect( + screen.getByRole('option', { + name: 'Proceed item to checkout', + }) + ); + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Proceed item to checkout', + }) + ) + ); + + expect( + screen.getByRole('button', { + name: 'Proceed item to checkout', + }) + ); + + //add third action + await act(() => + userEvent.click( + document.querySelector( + `.${blockClass}__actions-container .${blockClass}__add-button` + ) + ) + ); + + expect( + screen.getByRole('option', { + name: 'Add item to cart', + }) + ); + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Add item to cart', + }) + ) + ); + + expect( + screen.getAllByRole('button', { + name: 'Add item to cart', + }) + ).toHaveLength(2); + + //removing conditions + let closeConditions = document.querySelectorAll( + `.${blockClass}__actions-container .${blockClass}__close-condition` + ); + expect(closeConditions).toHaveLength(3); + + await act(() => userEvent.click(closeConditions[2])); + + closeConditions = document.querySelectorAll( + `.${blockClass}__actions-container .${blockClass}__close-condition` + ); + expect(closeConditions).toHaveLength(2); + expect( + screen.getAllByRole('button', { + name: 'Add item to cart', + }) + ).toHaveLength(1); + expect( + screen.getAllByRole('button', { + name: 'Proceed item to checkout', + }) + ).toHaveLength(1); + + await act(() => userEvent.click(closeConditions[1])); + + closeConditions = document.querySelectorAll( + `.${blockClass}__actions-container .${blockClass}__close-condition` + ); + expect(closeConditions).toHaveLength(1); + expect( + screen.getAllByRole('button', { + name: 'Add item to cart', + }) + ).toHaveLength(1); + expect( + screen.queryByText('Proceed item to checkout') + ).not.toBeInTheDocument(); + }); + + it(' remove all conditions in a group keeping only subgroups', async () => { + const sampleDataStructure = { + operator: 'or', + groups: [ + { + groupOperator: 'and', + statement: 'if', + id: '686c62a9-e33d-4e31-817b-4fd319168935', + conditions: [ + { + property: 'region', + operator: 'is', + value: { + label: 'Afghanistan', + id: 'AF', + icon: { + propTypes: {}, + }, + }, + id: '87b6cc99-b463-45e2-ab88-44a2a2069a25', + }, + { + groupOperator: 'and', + statement: 'if', + conditions: [ + { + property: 'region', + operator: 'is', + value: { + label: 'Afghanistan', + id: 'AF', + icon: { + propTypes: {}, + }, + }, + id: 'b1ab21df-1791-4955-a9f4-5e257b1d8ee2', + }, + { + groupOperator: 'and', + statement: 'if', + conditions: [ + { + property: 'color', + operator: 'is', + value: { + label: 'black', + id: 'black', + }, + id: '3dc4a2d9-c83d-4b56-8e24-d0dd0ec1e7a4', + }, + ], + id: '88fe784e-d748-4dfd-818c-63d1167bf60e', + }, + ], + id: '09e9feb8-a4a6-485f-9ac0-5b52d1dc82e4', + }, + ], + }, + ], + }; + render( + + ); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + + expect(screen.getAllByRole('button', { name: 'if' })).toHaveLength(3); + + await act(() => + userEvent.click(document.querySelector(`.${blockClass}__close-condition`)) + ); + + expect(screen.getAllByRole('button', { name: 'if' })).toHaveLength(2); + }); + + it('check the custom input type', async () => { + render(); + + await act(() => userEvent.click(screen.getByText('Add condition'))); + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'Product', + }) + ) + ); + + await act(() => + userEvent.click( + screen.getByRole('option', { + name: 'is greater than', + }) + ) + ); + + const inputText = document.querySelector('#customInput'); + fireEvent.change(inputText, { target: { value: 'testID123' } }); + + const container = document.querySelector(`.${blockClass}`); + await act(() => userEvent.click(container)); + + const selectedItem = screen.getByRole('button', { name: 'testID123' }); + + expect(selectedItem); + }); }); diff --git a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.js b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.js index 1574cc9c7d..22ffbb79a2 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.js +++ b/packages/ibm-products/src/components/ConditionBuilder/ConditionBuilderContent/ConditionBuilderContent.js @@ -32,7 +32,10 @@ const ConditionBuilderContent = ({ const [showConditionGroupPreview, setShowConditionGroupPreview] = useState(false); - const [addConditionGroupText] = useTranslations(['addConditionGroupText']); + const [addConditionGroupText, conditionHeadingText] = useTranslations([ + 'addConditionGroupText', + 'conditionHeadingText', + ]); const showConditionGroupPreviewHandler = () => { setShowConditionGroupPreview(true); }; @@ -113,25 +116,26 @@ const ConditionBuilderContent = ({ }); }; + if (!isConditionBuilderActive) { + return ( + + ); + } + return ( <> - {!isConditionBuilderActive && ( - - )} - {isConditionBuilderActive && ( -
- Condition -
- )} +
+ {conditionHeadingText} +
- {variant == 'tree' && ( -
- { - - } -
- )} - {showConditionGroupPreview && ( - + { + - )} - + } +
+ )} + {showConditionGroupPreview && ( + )} - {isConditionBuilderActive && actions && ( + {actions && ( { if (item.conditions) { @@ -184,9 +189,20 @@ const ConditionGroupBuilder = ({ `[aria-level="${Number(currentLevel) + 1}"][role="row"]` ); if (nextRow) { - manageTabIndexAndFocus(nextRow, conditionBuilderRef); + //since there are no condition in current group, this group will move one level up + + const rowIdentity = { + ariaLevel: Number(nextRow.ariaLevel) - 1, + ariaPosInSet: nextRow.ariaPosInSet, + }; + setTimeout(() => { + const currentRowToFocus = + conditionBuilderContentRef.current.querySelector( + `[role="row"][aria-level="${rowIdentity.ariaLevel}"][aria-posinset="${rowIdentity.ariaPosInSet}"]` + ); + manageTabIndexAndFocus(currentRowToFocus, conditionBuilderRef); + }, 0); } else if (prevRows?.length > 1) { - // prevRows[prevRows.length - 2].setAttribute('tabindex', '0'); manageTabIndexAndFocus( prevRows[prevRows.length - 2], conditionBuilderRef diff --git a/packages/ibm-products/src/components/ConditionBuilder/assets/sampleInput.js b/packages/ibm-products/src/components/ConditionBuilder/assets/sampleInput.js index e1ce850c4a..ad14e3928c 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/assets/sampleInput.js +++ b/packages/ibm-products/src/components/ConditionBuilder/assets/sampleInput.js @@ -14,47 +14,47 @@ import CustomInput from './CustomInput'; //keeping this , an alternative way to give support for dynamic options. //instead of supplying getOptions callback, we keep option property in inputConfig always as a async method instead to array as below. -export const inputDataForAsyncOptions = { - properties: [ - { - id: 'continent', - label: 'Continent', - icon: Earth, - type: 'option', - config: { - options: async () => { - let returnVal = [ - { - label: 'Africa', - id: 'Africa', - }, - { - label: 'Antarctica', - id: 'Antarctica', - }, - { - label: 'Asia', - id: 'Asia', - }, - { - label: 'Australia', - id: 'Australia', - }, - { - label: 'Europe', - id: 'Europe', - }, - ]; - return new Promise((resolve) => { - setTimeout(() => { - resolve(returnVal); - }, 2000); - }); - }, - }, - }, - ], -}; +// export const inputDataForAsyncOptions = { +// properties: [ +// { +// id: 'continent', +// label: 'Continent', +// icon: Earth, +// type: 'option', +// config: { +// options: async () => { +// let returnVal = [ +// { +// label: 'Africa', +// id: 'Africa', +// }, +// { +// label: 'Antarctica', +// id: 'Antarctica', +// }, +// { +// label: 'Asia', +// id: 'Asia', +// }, +// { +// label: 'Australia', +// id: 'Australia', +// }, +// { +// label: 'Europe', +// id: 'Europe', +// }, +// ]; +// return new Promise((resolve) => { +// setTimeout(() => { +// resolve(returnVal); +// }, 2000); +// }); +// }, +// }, +// }, +// ], +// }; const customOperators = [ { diff --git a/packages/ibm-products/src/components/ConditionBuilder/utils/handleKeyboardEvents.js b/packages/ibm-products/src/components/ConditionBuilder/utils/handleKeyboardEvents.js index 7fa0553e53..417dcc2e55 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/utils/handleKeyboardEvents.js +++ b/packages/ibm-products/src/components/ConditionBuilder/utils/handleKeyboardEvents.js @@ -311,7 +311,10 @@ const navigateToNextRowCell = ( const nextRow = rows[nextRowIndex]; const itemName = evt.target.dataset.name; if (nextRow?.querySelector(`[data-name="${itemName}"]`)) { - nextRow?.querySelector(`[data-name="${itemName}"]`)?.focus(); + manageTabIndexAndFocus( + nextRow?.querySelector(`[data-name="${itemName}"]`), + conditionBuilderRef + ); } else if (variant === 'tree') { //when the next row is a if statement , then that row is focused. From any cell of last row of an group , arrow down select the next row (if) manageTabIndexAndFocus(nextRow, conditionBuilderRef); diff --git a/packages/ibm-products/src/components/ConditionBuilder/utils/util.js b/packages/ibm-products/src/components/ConditionBuilder/utils/util.js index 818c3f6769..3eee3fb201 100644 --- a/packages/ibm-products/src/components/ConditionBuilder/utils/util.js +++ b/packages/ibm-products/src/components/ConditionBuilder/utils/util.js @@ -76,20 +76,6 @@ export const checkForHoldingKey = (evt, key) => { return evt[key]; }; -export const checkDuplicateAction = ( - actionState, - selectedId, - currentActionId -) => { - if ( - selectedId !== currentActionId && - actionState.find((eachAction) => eachAction.id === selectedId) - ) { - return true; - } - return false; -}; - export const checkIsValid = (item) => { return item && item !== 'INVALID'; };