|
| 1 | +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. |
| 2 | +// SPDX-License-Identifier: Apache-2.0 |
| 3 | +import React, { createRef } from 'react'; |
| 4 | +import { render } from '@testing-library/react'; |
| 5 | + |
| 6 | +import Select from '../../../lib/components/select'; |
| 7 | +import createWrapper from '../../../lib/components/test-utils/dom'; |
| 8 | +import { renderHook } from '../../__tests__/render-hook'; |
| 9 | +import { singleSelection } from '../../internal/components/listbox/selection/single-selection'; |
| 10 | +import { flattenOptions } from '../../internal/components/option/utils/flatten-options'; |
| 11 | +import { OptionDefinition, OptionGroup } from '../../types/option'; |
| 12 | +import { SelectProps } from '../interfaces'; |
| 13 | +import { useSelect } from '../utils/use-select'; |
| 14 | + |
| 15 | +import selectableStyles from '../../../lib/components/internal/components/selectable-item/styles.css.js'; |
| 16 | + |
| 17 | +const values = (...values: string[]) => values.map(value => ({ value })); |
| 18 | + |
| 19 | +const renderUseSelect = ( |
| 20 | + options: ReadonlyArray<OptionDefinition | OptionGroup>, |
| 21 | + selectedOptions: ReadonlyArray<OptionDefinition>, |
| 22 | + withSelection = true |
| 23 | +) => { |
| 24 | + const { flatOptions } = flattenOptions(options); |
| 25 | + const hook = renderHook(useSelect, { |
| 26 | + initialProps: { |
| 27 | + selectedOptions, |
| 28 | + selection: withSelection ? singleSelection(selectedOptions) : undefined, |
| 29 | + updateSelectedOption: () => {}, |
| 30 | + options: flatOptions, |
| 31 | + externalRef: createRef(), |
| 32 | + filteringType: 'none', |
| 33 | + fireLoadItems: () => {}, |
| 34 | + setFilteringValue: () => {}, |
| 35 | + statusType: 'finished' as const, |
| 36 | + }, |
| 37 | + }); |
| 38 | + const { getOptionProps } = hook.result.current; |
| 39 | + return { flatOptions, getOptionProps }; |
| 40 | +}; |
| 41 | + |
| 42 | +const groupStates = ( |
| 43 | + options: ReadonlyArray<OptionDefinition | OptionGroup>, |
| 44 | + selectedOptions: ReadonlyArray<OptionDefinition> |
| 45 | +) => { |
| 46 | + const { flatOptions, getOptionProps } = renderUseSelect(options, selectedOptions); |
| 47 | + return flatOptions |
| 48 | + .map((option, index) => ({ option, props: getOptionProps(option, index) })) |
| 49 | + .filter(({ option }) => option.type === 'parent') |
| 50 | + .map(({ props }) => ({ selected: props.selected, indeterminate: props.indeterminate })); |
| 51 | +}; |
| 52 | + |
| 53 | +describe('useSelect group state with single selection', () => { |
| 54 | + test('a group with one of several children selected is indeterminate, not selected', () => { |
| 55 | + expect(groupStates([{ label: 'Group', options: values('a', 'b', 'c') }], values('b'))).toEqual([ |
| 56 | + { selected: false, indeterminate: true }, |
| 57 | + ]); |
| 58 | + }); |
| 59 | + |
| 60 | + test('the same value in two groups makes both groups indeterminate', () => { |
| 61 | + expect( |
| 62 | + groupStates( |
| 63 | + [ |
| 64 | + { label: 'Group 1', options: values('a', 'shared') }, |
| 65 | + { label: 'Group 2', options: values('shared', 'b') }, |
| 66 | + ], |
| 67 | + values('shared') |
| 68 | + ) |
| 69 | + ).toEqual([ |
| 70 | + { selected: false, indeterminate: true }, |
| 71 | + { selected: false, indeterminate: true }, |
| 72 | + ]); |
| 73 | + }); |
| 74 | + |
| 75 | + test('a selected value that is not in the options leaves groups unselected', () => { |
| 76 | + expect(groupStates([{ label: 'Group', options: values('a', 'b') }], values('x'))).toEqual([ |
| 77 | + { selected: false, indeterminate: false }, |
| 78 | + ]); |
| 79 | + }); |
| 80 | + |
| 81 | + test('omitting selection gives the same option props as passing single selection', () => { |
| 82 | + const options = [{ value: 'top' }, { label: 'Group', options: values('a', 'b', 'c') }]; |
| 83 | + const selectedOptions = values('b'); |
| 84 | + const withModel = renderUseSelect(options, selectedOptions, true); |
| 85 | + const withoutModel = renderUseSelect(options, selectedOptions, false); |
| 86 | + // Ids come from a per-hook unique id, so they're left out of the comparison. |
| 87 | + const propsWithoutId = ({ getOptionProps, flatOptions }: ReturnType<typeof renderUseSelect>) => |
| 88 | + flatOptions.map((option, index) => { |
| 89 | + const props = { ...getOptionProps(option, index) }; |
| 90 | + delete props.id; |
| 91 | + return props; |
| 92 | + }); |
| 93 | + expect(propsWithoutId(withoutModel)).toEqual(propsWithoutId(withModel)); |
| 94 | + }); |
| 95 | +}); |
| 96 | + |
| 97 | +describe('Select group row state', () => { |
| 98 | + const renderSelect = (props: Partial<SelectProps>) => { |
| 99 | + const { container } = render(<Select options={[]} selectedOption={null} onChange={() => null} {...props} />); |
| 100 | + return createWrapper(container).findSelect()!; |
| 101 | + }; |
| 102 | + const rowOf = (element: HTMLElement) => element.closest(`.${selectableStyles['selectable-item']}`)!; |
| 103 | + |
| 104 | + test('selecting a child in a multi-child group does not mark the group header as selected', () => { |
| 105 | + const wrapper = renderSelect({ |
| 106 | + options: [{ value: 'top' }, { label: 'Group', options: values('a', 'b', 'c') }], |
| 107 | + selectedOption: { value: 'b' }, |
| 108 | + }); |
| 109 | + wrapper.openDropdown(); |
| 110 | + const dropdown = wrapper.findDropdown(); |
| 111 | + |
| 112 | + expect(rowOf(dropdown.findGroup(1)!.getElement())).not.toHaveClass(selectableStyles.selected); |
| 113 | + expect(rowOf(dropdown.findOptionByValue('top')!.getElement())).not.toHaveClass( |
| 114 | + selectableStyles['next-item-selected'] |
| 115 | + ); |
| 116 | + expect(rowOf(dropdown.findOptionByValue('a')!.getElement())).not.toHaveClass( |
| 117 | + selectableStyles['previous-item-selected'] |
| 118 | + ); |
| 119 | + expect(rowOf(dropdown.findOptionByValue('b')!.getElement())).toHaveClass(selectableStyles.selected); |
| 120 | + }); |
| 121 | + |
| 122 | + test('selecting the only child of a group does not mark the group header as selected', () => { |
| 123 | + const wrapper = renderSelect({ |
| 124 | + options: [{ label: 'Group', options: values('a') }], |
| 125 | + selectedOption: { value: 'a' }, |
| 126 | + }); |
| 127 | + wrapper.openDropdown(); |
| 128 | + const dropdown = wrapper.findDropdown(); |
| 129 | + |
| 130 | + expect(rowOf(dropdown.findGroup(1)!.getElement())).not.toHaveClass(selectableStyles.selected); |
| 131 | + expect(rowOf(dropdown.findOptionByValue('a')!.getElement())).toHaveClass(selectableStyles.selected); |
| 132 | + }); |
| 133 | +}); |
0 commit comments