aboutsummaryrefslogtreecommitdiffstats
path: root/server/sonar-web/design-system/src/components/input/__tests__
diff options
context:
space:
mode:
authorRevanshu Paliwal <revanshu.paliwal@sonarsource.com>2023-06-27 12:16:58 +0200
committersonartech <sonartech@sonarsource.com>2023-07-04 20:03:09 +0000
commita2124bcdd58dadee846cb87eaa77f4d9c2cd331a (patch)
tree2a433f281d7bbc7a6b16841b7a09fabd0eefb56f /server/sonar-web/design-system/src/components/input/__tests__
parentebece82669c46e8657d10407ee1d5dc957af3a6c (diff)
downloadsonarqube-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__')
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/DatePicker-test.tsx145
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/DateRangePicker-test.tsx93
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/DiscreetSelect-test.tsx59
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/FormField-test.tsx46
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/InputField-test.tsx36
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/InputMultiSelect-test.tsx40
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/InputSearch-test.tsx90
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/InputSelect-test.tsx58
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/MultiSelectMenu-test.tsx114
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/RadioButton-test.tsx62
-rw-r--r--server/sonar-web/design-system/src/components/input/__tests__/SearchSelectDropdown-test.tsx99
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}
+ />
+ );
+}