diff options
author | Revanshu Paliwal <revanshu.paliwal@sonarsource.com> | 2023-06-27 12:16:58 +0200 |
---|---|---|
committer | sonartech <sonartech@sonarsource.com> | 2023-07-04 20:03:09 +0000 |
commit | a2124bcdd58dadee846cb87eaa77f4d9c2cd331a (patch) | |
tree | 2a433f281d7bbc7a6b16841b7a09fabd0eefb56f /server/sonar-web/design-system/src/components/input/__tests__ | |
parent | ebece82669c46e8657d10407ee1d5dc957af3a6c (diff) | |
download | sonarqube-a2124bcdd58dadee846cb87eaa77f4d9c2cd331a.tar.gz sonarqube-a2124bcdd58dadee846cb87eaa77f4d9c2cd331a.zip |
[NO-JIRA] Design system refactor: inputs and buttons
Diffstat (limited to 'server/sonar-web/design-system/src/components/input/__tests__')
11 files changed, 842 insertions, 0 deletions
diff --git a/server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx new file mode 100644 index 00000000000..81d9f167caf --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx @@ -0,0 +1,145 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { getMonth, getYear, parseISO } from 'date-fns'; +import { render } from '../../../helpers/testUtils'; +import { DatePicker } from '../DatePicker'; + +it('behaves correctly', async () => { + const user = userEvent.setup(); + + const onChange = jest.fn((_: Date) => undefined); + const currentMonth = parseISO('2022-06-13'); + renderDatePicker({ currentMonth, onChange }); + + /* + * Open the DatePicker, navigate to the previous month and choose an arbitrary day (7) + * Then check that onChange was correctly called with a date in the previous month + */ + await user.click(screen.getByRole('textbox')); + + const nav = screen.getByRole('navigation'); + expect(nav).toBeInTheDocument(); + + await user.click(within(nav).getByRole('button', { name: 'previous' })); + await user.click(screen.getByText('7')); + + expect(onChange).toHaveBeenCalled(); + const newDate = onChange.mock.calls[0][0]; // first argument of the first and only call + expect(getMonth(newDate)).toBe(getMonth(currentMonth) - 1); + + onChange.mockClear(); + + /* + * Open the DatePicker, navigate to the next month twice and choose an arbitrary day (12) + * Then check that onChange was correctly called with a date in the following month + */ + await user.click(screen.getByRole('textbox')); + const nextButton = screen.getByRole('button', { name: 'next' }); + await user.click(nextButton); + await user.click(nextButton); + await user.click(screen.getByText('12')); + + expect(onChange).toHaveBeenCalled(); + const newDate2 = onChange.mock.calls[0][0]; // first argument + expect(getMonth(newDate2)).toBe(getMonth(currentMonth) + 1); + + onChange.mockClear(); + + /* + * Open the DatePicker, select the month, select the year and choose an arbitrary day (10) + * Then check that onChange was correctly called with a date in the selected month & year + */ + await user.click(screen.getByRole('textbox')); + // Select month + await user.click(screen.getByText('Jun')); + await user.click(screen.getByText('Feb')); + + // Select year + await user.click(screen.getByText('2022')); + await user.click(screen.getByText('2019')); + + await user.click(screen.getByText('10')); + + const newDate3 = onChange.mock.calls[0][0]; // first argument + + expect(getMonth(newDate3)).toBe(1); + expect(getYear(newDate3)).toBe(2019); +}); + +it('should clear the value', async () => { + const user = userEvent.setup(); + + const onChange = jest.fn((_: Date) => undefined); + + const currentDate = parseISO('2022-06-13'); + + renderDatePicker({ + currentMonth: currentDate, + onChange, + showClearButton: true, + value: currentDate, + // eslint-disable-next-line jest/no-conditional-in-test + valueFormatter: (date?: Date) => (date ? 'formatted date' : 'no date'), + }); + + await user.click(screen.getByRole('textbox')); + + await user.click(screen.getByLabelText('clear')); + + expect(onChange).toHaveBeenCalledWith(undefined); +}); + +it.each([ + [{ highlightFrom: parseISO('2022-06-12'), value: parseISO('2022-06-14') }], + [{ alignRight: true, highlightTo: parseISO('2022-06-14'), value: parseISO('2022-06-12') }], +])('highlights the appropriate days', async (props) => { + const user = userEvent.setup(); + + const hightlightClass = 'rdp-highlighted'; + + renderDatePicker(props); + + await user.click(screen.getByRole('textbox')); + + expect(screen.getByText('11')).not.toHaveClass(hightlightClass); + expect(screen.getByText('12')).toHaveClass(hightlightClass); + expect(screen.getByText('13')).toHaveClass(hightlightClass); + expect(screen.getByText('14')).toHaveClass(hightlightClass); + expect(screen.getByText('15')).not.toHaveClass(hightlightClass); +}); + +function renderDatePicker(overrides: Partial<DatePicker['props']> = {}) { + const defaultFormatter = (date?: Date) => (date ? date.toISOString() : ''); + + render( + <DatePicker + ariaNextMonthLabel="next" + ariaPreviousMonthLabel="previous" + clearButtonLabel="clear" + onChange={jest.fn()} + placeholder="placeholder" + valueFormatter={defaultFormatter} + {...overrides} + /> + ); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/DateRangePicker-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/DateRangePicker-test.tsx new file mode 100644 index 00000000000..62902dfcb1c --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/DateRangePicker-test.tsx @@ -0,0 +1,93 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen, within } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { formatISO, parseISO } from 'date-fns'; +import { render } from '../../../helpers/testUtils'; +import { DateRangePicker } from '../DateRangePicker'; + +beforeEach(() => { + jest.useFakeTimers().setSystemTime(parseISO('2022-06-12')); +}); + +afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); +}); + +it('behaves correctly', async () => { + // Remove delay to play nice with fake timers + const user = userEvent.setup({ delay: null }); + + const onChange = jest.fn((_: { from?: Date; to?: Date }) => undefined); + renderDateRangePicker({ onChange }); + + await user.click(screen.getByRole('textbox', { name: 'from' })); + + const fromDateNav = screen.getByRole('navigation'); + expect(fromDateNav).toBeInTheDocument(); + + await user.click(within(fromDateNav).getByRole('button', { name: 'previous' })); + await user.click(screen.getByText('7')); + + expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); + + expect(onChange).toHaveBeenCalled(); + const { from } = onChange.mock.calls[0][0]; // first argument + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(formatISO(from!, { representation: 'date' })).toBe('2022-05-07'); + + onChange.mockClear(); + + jest.runAllTimers(); + + const toDateNav = await screen.findByRole('navigation'); + const previousButton = within(toDateNav).getByRole('button', { name: 'previous' }); + const nextButton = within(toDateNav).getByRole('button', { name: 'next' }); + await user.click(previousButton); + await user.click(nextButton); + await user.click(previousButton); + await user.click(screen.getByText('12')); + + expect(screen.queryByRole('navigation')).not.toBeInTheDocument(); + + expect(onChange).toHaveBeenCalled(); + const { to } = onChange.mock.calls[0][0]; // first argument + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + expect(formatISO(to!, { representation: 'date' })).toBe('2022-05-12'); +}); + +function renderDateRangePicker(overrides: Partial<DateRangePicker['props']> = {}) { + const defaultFormatter = (date?: Date) => + date ? formatISO(date, { representation: 'date' }) : ''; + + render( + <DateRangePicker + ariaNextMonthLabel="next" + ariaPreviousMonthLabel="previous" + clearButtonLabel="clear" + fromLabel="from" + onChange={jest.fn()} + toLabel="to" + valueFormatter={defaultFormatter} + {...overrides} + /> + ); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx new file mode 100644 index 00000000000..7db557af2a0 --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx @@ -0,0 +1,59 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../helpers/testUtils'; +import { FCProps } from '../../../types/misc'; +import { DiscreetSelect } from '../DiscreetSelect'; + +it('should render discreet select and invoke CB on value click', async () => { + const value = 'foo'; + const setValue = jest.fn(); + + const user = userEvent.setup(); + setupWithProps({ setValue, value }); + await user.click(screen.getByRole('combobox')); + expect(screen.getByText(/option foo-bar selected/)).toBeInTheDocument(); + expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument(); + await user.click(screen.getByText('bar-foo')); + expect(setValue).toHaveBeenCalled(); +}); + +function setupWithProps(props: Partial<FCProps<typeof DiscreetSelect>>) { + return render( + <DiscreetSelect + options={[ + { label: 'foo-bar', value: 'foo' }, + { + label: 'bar-foo', + value: 'bar', + Icon: ( + <span role="note" title="Icon"> + Icon + </span> + ), + }, + ]} + setValue={jest.fn()} + value="foo" + {...props} + /> + ); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/FormField-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/FormField-test.tsx new file mode 100644 index 00000000000..a8a4faeb025 --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/FormField-test.tsx @@ -0,0 +1,46 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ + +import { screen } from '@testing-library/react'; +import { FCProps } from '~types/misc'; +import { render } from '../../../helpers/testUtils'; +import { FormField } from '../FormField'; + +it('should render correctly', () => { + renderFormField({}, <input id="input" />); + expect(screen.getByLabelText('Hello')).toBeInTheDocument(); +}); + +it('should render with required and description', () => { + renderFormField({ description: 'some description', required: true }, <input id="input" />); + expect(screen.getByText('some description')).toBeInTheDocument(); + expect(screen.getByText('*')).toBeInTheDocument(); +}); + +function renderFormField( + props: Partial<FCProps<typeof FormField>> = {}, + children: any = <div>Fake input</div> +) { + return render( + <FormField htmlFor="input" label="Hello" {...props}> + {children} + </FormField> + ); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/InputField-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/InputField-test.tsx new file mode 100644 index 00000000000..47fcac116ca --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/InputField-test.tsx @@ -0,0 +1,36 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { render, screen } from '@testing-library/react'; +import { InputField } from '../InputField'; + +describe('Input Field', () => { + it.each([ + ['default', false, false, 'defaultStyle'], + ['invalid', true, false, 'dangerStyle'], + ['valid', false, true, 'successStyle'], + ])('should handle status %s', (_, isInvalid, isValid, expectedStyle) => { + render(<InputField isInvalid={isInvalid} isValid={isValid} />); + + // Emotion classes contain pseudo-random parts, we're interesting in the fixed part + // so we can't just check a specific class + // eslint-disable-next-line jest-dom/prefer-to-have-class + expect(screen.getByRole('textbox').className).toContain(expectedStyle); + }); +}); diff --git a/server/sonar-web/design-system/src/components/input/__tests__/InputMultiSelect-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/InputMultiSelect-test.tsx new file mode 100644 index 00000000000..d362d00db85 --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/InputMultiSelect-test.tsx @@ -0,0 +1,40 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { render, screen } from '@testing-library/react'; +import { FCProps } from '../../../types/misc'; +import { InputMultiSelect } from '../InputMultiSelect'; + +it('should render correctly', () => { + renderInputMultiSelect(); + expect(screen.getByText('select')).toBeInTheDocument(); + expect(screen.queryByText('selected')).not.toBeInTheDocument(); + expect(screen.queryByText(/\d+/)).not.toBeInTheDocument(); +}); + +it('should render correctly with a counter', () => { + renderInputMultiSelect({ count: 42 }); + expect(screen.queryByText('select')).not.toBeInTheDocument(); + expect(screen.getByText('selected')).toBeInTheDocument(); + expect(screen.getByText('42')).toBeInTheDocument(); +}); + +function renderInputMultiSelect(props: Partial<FCProps<typeof InputMultiSelect>> = {}) { + render(<InputMultiSelect placeholder="select" selectedLabel="selected" {...props} />); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/InputSearch-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/InputSearch-test.tsx new file mode 100644 index 00000000000..fa5f5c71b73 --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/InputSearch-test.tsx @@ -0,0 +1,90 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen, waitFor } from '@testing-library/react'; +import { render } from '../../../helpers/testUtils'; +import { FCProps } from '../../../types/misc'; +import { InputSearch } from '../InputSearch'; + +it('should warn when input is too short', async () => { + const { user } = setupWithProps({ value: 'f' }); + expect(screen.getByRole('note')).toBeInTheDocument(); + await user.type(screen.getByRole('searchbox'), 'oo'); + expect(screen.queryByRole('note')).not.toBeInTheDocument(); +}); + +it('should show clear button only when there is a value', async () => { + const { user } = setupWithProps({ value: 'f' }); + expect(screen.getByRole('button')).toBeInTheDocument(); + await user.clear(screen.getByRole('searchbox')); + expect(screen.queryByRole('button')).not.toBeInTheDocument(); +}); + +it('should attach ref', () => { + const ref = jest.fn() as jest.Mock<unknown, unknown[]>; + setupWithProps({ innerRef: ref }); + expect(ref).toHaveBeenCalled(); + expect(ref.mock.calls[0][0]).toBeInstanceOf(HTMLInputElement); +}); + +it('should trigger reset correctly with clear button', async () => { + const onChange = jest.fn(); + const { user } = setupWithProps({ onChange }); + await user.click(screen.getByRole('button')); + expect(onChange).toHaveBeenCalledWith(''); +}); + +it('should trigger change correctly', async () => { + const onChange = jest.fn(); + const { user } = setupWithProps({ onChange, value: 'f' }); + await user.type(screen.getByRole('searchbox'), 'oo'); + await waitFor(() => { + expect(onChange).toHaveBeenCalledWith('foo'); + }); +}); + +it('should not change when value is too short', async () => { + const onChange = jest.fn(); + const { user } = setupWithProps({ onChange, value: '', minLength: 3 }); + await user.type(screen.getByRole('searchbox'), 'fo'); + expect(onChange).not.toHaveBeenCalled(); +}); + +it('should clear input using escape', async () => { + const onChange = jest.fn(); + const { user } = setupWithProps({ onChange, value: 'foo' }); + await user.type(screen.getByRole('searchbox'), '{Escape}'); + expect(onChange).toHaveBeenCalledWith(''); +}); + +function setupWithProps(props: Partial<FCProps<typeof InputSearch>> = {}) { + return render( + <InputSearch + clearIconAriaLabel="" + maxLength={150} + minLength={2} + onChange={jest.fn()} + placeholder="placeholder" + searchInputAriaLabel="" + tooShortText="too short" + value="foo" + {...props} + /> + ); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx new file mode 100644 index 00000000000..fefc767f86b --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx @@ -0,0 +1,58 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../helpers/testUtils'; +import { FCProps } from '../../../types/misc'; +import { InputSelect } from '../InputSelect'; + +it('should render select input and be able to click and change', async () => { + const setValue = jest.fn(); + const user = userEvent.setup(); + setupWithProps({ placeholder: 'placeholder-foo', onChange: setValue }); + expect(screen.getByText('placeholder-foo')).toBeInTheDocument(); + await user.click(screen.getByRole('combobox')); + expect(screen.getByText(/option foo-bar focused/)).toBeInTheDocument(); + expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument(); + await user.click(screen.getByText('bar-foo')); + expect(setValue).toHaveBeenCalled(); + expect(screen.queryByText('placeholder-foo')).not.toBeInTheDocument(); + expect(screen.getByRole('note', { name: 'Icon' })).toBeInTheDocument(); +}); + +function setupWithProps(props: Partial<FCProps<typeof InputSelect>>) { + return render( + <InputSelect + {...props} + options={[ + { label: 'foo-bar', value: 'foo' }, + { + label: 'bar-foo', + value: 'bar', + Icon: ( + <span role="note" title="Icon"> + Icon + </span> + ), + }, + ]} + /> + ); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/MultiSelectMenu-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/MultiSelectMenu-test.tsx new file mode 100644 index 00000000000..a1c7b5800ea --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/MultiSelectMenu-test.tsx @@ -0,0 +1,114 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { MultiSelectMenu } from '../MultiSelectMenu'; + +const elements = ['foo', 'bar', 'baz']; + +beforeEach(() => { + jest.useFakeTimers(); +}); + +afterEach(() => { + jest.runOnlyPendingTimers(); + jest.useRealTimers(); +}); + +it('should allow selecting and deselecting a new option', async () => { + const user = userEvent.setup({ delay: null }); + const onSelect = jest.fn(); + const onUnselect = jest.fn(); + renderMultiselect({ elements, onSelect, onUnselect, allowNewElements: true }); + + await user.keyboard('new option'); + jest.runAllTimers(); // skip the debounce + + expect(screen.getByText('new option')).toBeInTheDocument(); + + await user.click(screen.getByText('new option')); + + expect(onSelect).toHaveBeenCalledWith('new option'); + + renderMultiselect({ + elements, + onUnselect, + allowNewElements: true, + selectedElements: ['new option'], + }); + + await user.click(screen.getByText('new option')); + expect(onUnselect).toHaveBeenCalledWith('new option'); +}); + +it('should ignore the left and right arrow keys', async () => { + const user = userEvent.setup({ delay: null }); + const onSelect = jest.fn(); + renderMultiselect({ elements, onSelect }); + + /* eslint-disable testing-library/no-node-access */ + await user.keyboard('{arrowdown}'); + expect(screen.getAllByRole('checkbox')[1].parentElement).toHaveClass('active'); + + await user.keyboard('{arrowleft}'); + expect(screen.getAllByRole('checkbox')[1].parentElement).toHaveClass('active'); + + await user.keyboard('{arrowright}'); + expect(screen.getAllByRole('checkbox')[1].parentElement).toHaveClass('active'); + + await user.keyboard('{arrowdown}'); + expect(screen.getAllByRole('checkbox')[1].parentElement).not.toHaveClass('active'); + expect(screen.getAllByRole('checkbox')[2].parentElement).toHaveClass('active'); + + await user.keyboard('{arrowup}'); + await user.keyboard('{arrowup}'); + expect(screen.getAllByRole('checkbox')[0].parentElement).toHaveClass('active'); + await user.keyboard('{arrowup}'); + expect(screen.getAllByRole('checkbox')[2].parentElement).toHaveClass('active'); + + expect(screen.getAllByRole('checkbox')[2]).not.toBeChecked(); + await user.keyboard('{enter}'); + expect(onSelect).toHaveBeenCalledWith('baz'); +}); + +it('should show no results', () => { + renderMultiselect(); + expect(screen.getByText('no results')).toBeInTheDocument(); +}); + +function renderMultiselect(props: Partial<MultiSelectMenu['props']> = {}) { + return render( + <MultiSelectMenu + clearIconAriaLabel="clear" + createElementLabel="create thing" + elements={[]} + filterSelected={jest.fn()} + listSize={10} + noResultsLabel="no results" + onSearch={jest.fn(() => Promise.resolve())} + onSelect={jest.fn()} + onUnselect={jest.fn()} + placeholder="" + searchInputAriaLabel="search" + selectedElements={[]} + {...props} + /> + ); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/RadioButton-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/RadioButton-test.tsx new file mode 100644 index 00000000000..b84f0bc377e --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/RadioButton-test.tsx @@ -0,0 +1,62 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../helpers/testUtils'; +import { FCProps } from '../../../types/misc'; +import { RadioButton } from '../RadioButton'; + +const value = 'value'; + +it('should render properly', () => { + setupWithProps(); + expect(screen.getByRole('radio')).not.toBeChecked(); +}); + +it('should render properly when checked', () => { + setupWithProps({ checked: true }); + expect(screen.getByRole('radio')).toBeChecked(); +}); + +it('should invoke callback on click', async () => { + const user = userEvent.setup(); + const onCheck = jest.fn(); + setupWithProps({ onCheck, value }); + + await user.click(screen.getByRole('radio')); + expect(onCheck).toHaveBeenCalled(); +}); + +it('should not invoke callback on click when disabled', async () => { + const user = userEvent.setup(); + const onCheck = jest.fn(); + setupWithProps({ disabled: true, onCheck }); + + await user.click(screen.getByRole('radio')); + expect(onCheck).not.toHaveBeenCalled(); +}); + +function setupWithProps(props?: Partial<FCProps<typeof RadioButton>>) { + return render( + <RadioButton checked={false} onCheck={jest.fn()} value="value" {...props}> + foo + </RadioButton> + ); +} diff --git a/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx b/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx new file mode 100644 index 00000000000..1c7d4874136 --- /dev/null +++ b/server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx @@ -0,0 +1,99 @@ +/* + * SonarQube + * Copyright (C) 2009-2023 SonarSource SA + * mailto:info AT sonarsource DOT com + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 3 of the License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program; if not, write to the Free Software Foundation, + * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + */ +import { act, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import { render } from '../../../helpers/testUtils'; +import { FCProps } from '../../../types/misc'; +import { LabelValueSelectOption } from '../InputSelect'; +import { SearchSelectDropdown } from '../SearchSelectDropdown'; + +const defaultOptions = [ + { label: 'label1', value: 'value1' }, + { label: 'different', value: 'diff1' }, +]; + +const loadOptions = ( + query: string, + cb: (options: Array<LabelValueSelectOption<string>>) => void +) => { + cb(defaultOptions.filter((o) => o.label.includes(query))); +}; + +it('should render select input and be able to search and select an option', async () => { + const user = userEvent.setup(); + const onChange = jest.fn(); + renderSearchSelectDropdown({ onChange }); + expect(screen.getByText('not assigned')).toBeInTheDocument(); + await user.click(screen.getByRole('combobox')); + expect(screen.getByText('label1')).toBeInTheDocument(); + expect(screen.getByText('different')).toBeInTheDocument(); + await user.type(screen.getByRole('combobox', { name: 'label' }), 'label'); + expect(await screen.findByText('label')).toBeInTheDocument(); + expect(screen.queryByText('different')).not.toBeInTheDocument(); + await user.click(screen.getByText('label')); + expect(onChange).toHaveBeenLastCalledWith(defaultOptions[0], { + action: 'select-option', + name: undefined, + option: undefined, + }); +}); + +it('should handle key navigation', async () => { + const user = userEvent.setup(); + renderSearchSelectDropdown(); + await user.tab(); + await user.keyboard('{Enter}'); + await user.type(screen.getByRole('combobox', { name: 'label' }), 'label'); + expect(await screen.findByText('label')).toBeInTheDocument(); + expect(screen.queryByText('different')).not.toBeInTheDocument(); + await user.keyboard('{Escape}'); + expect(await screen.findByText('different')).toBeInTheDocument(); + await act(async () => { + await user.keyboard('{Escape}'); + }); + expect(screen.queryByText('different')).not.toBeInTheDocument(); + await user.tab({ shift: true }); + await user.keyboard('{ArrowDown}'); + expect(await screen.findByText('label1')).toBeInTheDocument(); +}); + +it('behaves correctly in disabled state', async () => { + const user = userEvent.setup(); + renderSearchSelectDropdown({ isDisabled: true }); + await user.click(screen.getByRole('combobox')); + expect(screen.queryByText('label1')).not.toBeInTheDocument(); + await user.tab(); + await user.keyboard('{Enter}'); + expect(screen.queryByText('label1')).not.toBeInTheDocument(); +}); + +function renderSearchSelectDropdown(props: Partial<FCProps<typeof SearchSelectDropdown>> = {}) { + return render( + <SearchSelectDropdown + aria-label="label" + controlLabel="not assigned" + defaultOptions={defaultOptions} + isDiscreet + loadOptions={loadOptions} + placeholder="search for things" + {...props} + /> + ); +} |