Chore: Apply testing-library rules to grafana-ui package tests (#105140)

* Apply testing-library rules to grafana-ui package tests

* Apply auto fixes

* Fix eslint test issues in grafana-ui package

* Fix prettier issues

* Address review feedback
pull/105195/head
Tom Ratcliffe 2 months ago committed by GitHub
parent 5795ba34f9
commit e2cb3e74f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 17
      eslint.config.js
  2. 2
      packages/grafana-ui/src/components/BigValue/BigValue.test.tsx
  3. 20
      packages/grafana-ui/src/components/Card/Card.test.tsx
  4. 8
      packages/grafana-ui/src/components/Carousel/Carousel.test.tsx
  5. 2
      packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx
  6. 17
      packages/grafana-ui/src/components/Combobox/Combobox.test.tsx
  7. 2
      packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx
  8. 2
      packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx
  9. 4
      packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx
  10. 2
      packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.test.tsx
  11. 2
      packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.test.tsx
  12. 2
      packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.test.tsx
  13. 13
      packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.test.tsx
  14. 32
      packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.test.tsx
  15. 2
      packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx
  16. 32
      packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.test.tsx
  17. 4
      packages/grafana-ui/src/components/DateTimePickers/TimeRangeContext.test.tsx
  18. 8
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx
  19. 26
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx
  20. 61
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx
  21. 38
      packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/hooks.test.tsx
  22. 1
      packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx
  23. 6
      packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx
  24. 21
      packages/grafana-ui/src/components/FileDropzone/FileListItem.test.tsx
  25. 38
      packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx
  26. 2
      packages/grafana-ui/src/components/Forms/Legacy/Input/Input.test.tsx
  27. 17
      packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx
  28. 13
      packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.test.tsx
  29. 21
      packages/grafana-ui/src/components/Menu/MenuItem.test.tsx
  30. 65
      packages/grafana-ui/src/components/Menu/hooks.test.tsx
  31. 29
      packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx
  32. 6
      packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx
  33. 6
      packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.test.tsx
  34. 24
      packages/grafana-ui/src/components/Select/SelectBase.test.tsx
  35. 36
      packages/grafana-ui/src/components/Table/Table.test.tsx
  36. 128
      packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx
  37. 8
      packages/grafana-ui/src/components/Table/TableNG/utils.test.ts
  38. 9
      packages/grafana-ui/src/components/TagsInput/TagsInput.test.tsx
  39. 2
      packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx
  40. 6
      packages/grafana-ui/src/components/uPlot/Plot.test.tsx

@ -299,7 +299,7 @@ module.exports = [
}, },
}, },
{ {
name: 'grafana/alerting-test-overrides', name: 'grafana/tests',
plugins: { plugins: {
'testing-library': testingLibraryPlugin, 'testing-library': testingLibraryPlugin,
'jest-dom': jestDomPlugin, 'jest-dom': jestDomPlugin,
@ -307,12 +307,25 @@ module.exports = [
files: [ files: [
'public/app/features/alerting/**/__tests__/**/*.[jt]s?(x)', 'public/app/features/alerting/**/__tests__/**/*.[jt]s?(x)',
'public/app/features/alerting/**/?(*.)+(spec|test).[jt]s?(x)', 'public/app/features/alerting/**/?(*.)+(spec|test).[jt]s?(x)',
'packages/grafana-ui/**/*.{spec,test}.{ts,tsx}',
], ],
rules: { rules: {
...testingLibraryPlugin.configs['flat/react'].rules, ...testingLibraryPlugin.configs['flat/react'].rules,
...jestDomPlugin.configs['flat/recommended'].rules, ...jestDomPlugin.configs['flat/recommended'].rules,
'testing-library/prefer-user-event': 'error', 'testing-library/prefer-user-event': 'error',
'jest/expect-expect': ['error', { assertFunctionNames: ['expect*', 'reducerTester'] }], 'jest/expect-expect': ['error', { assertFunctionNames: ['expect*', 'assert*', 'reducerTester'] }],
},
},
{
name: 'grafana/test-overrides-to-fix',
plugins: {
'testing-library': testingLibraryPlugin,
},
files: ['packages/grafana-ui/**/*.{spec,test}.{ts,tsx}'],
rules: {
// grafana-ui has lots of violations of direct node access and container methods, so disabling for now
'testing-library/no-node-access': 'off',
'testing-library/no-container': 'off',
}, },
}, },
{ {

@ -46,7 +46,7 @@ describe('BigValue', () => {
it('should render without percent change', () => { it('should render without percent change', () => {
render(<BigValue {...getProps()} />); render(<BigValue {...getProps()} />);
expect(screen.queryByText('%')).toBeNull(); expect(screen.queryByText('%')).not.toBeInTheDocument();
}); });
}); });
}); });

@ -1,4 +1,5 @@
import { render, screen, fireEvent } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { Button } from '../Button'; import { Button } from '../Button';
import { IconButton } from '../IconButton/IconButton'; import { IconButton } from '../IconButton/IconButton';
@ -6,15 +7,16 @@ import { IconButton } from '../IconButton/IconButton';
import { Card } from './Card'; import { Card } from './Card';
describe('Card', () => { describe('Card', () => {
it('should execute callback when clicked', () => { it('should execute callback when clicked', async () => {
const user = userEvent.setup();
const callback = jest.fn(); const callback = jest.fn();
render( render(
<Card onClick={callback}> <Card onClick={callback}>
<Card.Heading>Test Heading</Card.Heading> <Card.Heading>Test Heading</Card.Heading>
</Card> </Card>
); );
fireEvent.click(screen.getByText('Test Heading')); await user.click(screen.getByText('Test Heading'));
expect(callback).toBeCalledTimes(1); expect(callback).toHaveBeenCalledTimes(1);
}); });
describe('Card Actions', () => { describe('Card Actions', () => {
@ -31,8 +33,8 @@ describe('Card', () => {
</Card> </Card>
); );
expect(screen.getByRole('button', { name: 'Click Me' })).not.toBeDisabled(); expect(screen.getByRole('button', { name: 'Click Me' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Delete' })).not.toBeDisabled(); expect(screen.getByRole('button', { name: 'Delete' })).toBeEnabled();
rerender( rerender(
<Card disabled> <Card disabled>
@ -78,8 +80,8 @@ describe('Card', () => {
</Card> </Card>
); );
expect(screen.getByRole('button', { name: 'Click Me' })).not.toBeDisabled(); expect(screen.getByRole('button', { name: 'Click Me' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Delete' })).not.toBeDisabled(); expect(screen.getByRole('button', { name: 'Delete' })).toBeEnabled();
}); });
it('Children should be conditional', () => { it('Children should be conditional', () => {
@ -97,7 +99,7 @@ describe('Card', () => {
</Card> </Card>
); );
expect(screen.getByRole('button', { name: 'Click Me' })).not.toBeDisabled(); expect(screen.getByRole('button', { name: 'Click Me' })).toBeEnabled();
expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: 'Delete' })).not.toBeInTheDocument();
}); });

@ -111,13 +111,13 @@ describe('Carousel', () => {
await user.click(screen.getByText('Alert rule')); await user.click(screen.getByText('Alert rule'));
fireEvent.keyDown(screen.getByTestId('carousel-full-screen'), { key: 'ArrowRight' }); await user.keyboard('{ArrowRight}');
let previewImage = screen.getByTestId('carousel-full-image').querySelector('img'); let previewImage = screen.getByTestId('carousel-full-image').querySelector('img');
expect(previewImage).toHaveAttribute('src', testImages[1].path); expect(previewImage).toHaveAttribute('src', testImages[1].path);
fireEvent.keyDown(screen.getByTestId('carousel-full-screen'), { key: 'ArrowLeft' }); await user.keyboard('{ArrowLeft}');
fireEvent.keyDown(screen.getByTestId('carousel-full-screen'), { key: 'ArrowLeft' }); await user.keyboard('{ArrowLeft}');
previewImage = screen.getByTestId('carousel-full-image').querySelector('img'); previewImage = screen.getByTestId('carousel-full-image').querySelector('img');
expect(previewImage).toHaveAttribute('src', testImages[3].path); expect(previewImage).toHaveAttribute('src', testImages[3].path);
@ -129,7 +129,7 @@ describe('Carousel', () => {
await user.click(screen.getByText('Alert rule')); await user.click(screen.getByText('Alert rule'));
expect(screen.getByTestId('carousel-full-screen')).toBeInTheDocument(); expect(screen.getByTestId('carousel-full-screen')).toBeInTheDocument();
fireEvent.keyDown(screen.getByTestId('carousel-full-screen'), { key: 'Escape' }); await user.keyboard('{Escape}');
expect(screen.queryByTestId('carousel-full-screen')).not.toBeInTheDocument(); expect(screen.queryByTestId('carousel-full-screen')).not.toBeInTheDocument();
}); });

@ -27,7 +27,7 @@ describe('ColorPickerInput', () => {
await userEvent.click(screen.getByRole('textbox')); await userEvent.click(screen.getByRole('textbox'));
expect(screen.getByTestId('color-popover')).toBeInTheDocument(); expect(screen.getByTestId('color-popover')).toBeInTheDocument();
await userEvent.click(screen.getAllByRole('slider')[0]); await userEvent.click(screen.getAllByRole('slider')[0]);
expect(screen.queryByTestId('color-popover')).toBeInTheDocument(); expect(screen.getByTestId('color-popover')).toBeInTheDocument();
}); });
it('should pass correct color to onChange callback', async () => { it('should pass correct color to onChange callback', async () => {

@ -30,6 +30,12 @@ const numericOptions: Array<ComboboxOption<number>> = [
]; ];
describe('Combobox', () => { describe('Combobox', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup({ applyAccept: false });
});
const onChangeHandler = jest.fn(); const onChangeHandler = jest.fn();
beforeAll(() => { beforeAll(() => {
const mockGetBoundingClientRect = jest.fn(() => ({ const mockGetBoundingClientRect = jest.fn(() => ({
@ -116,13 +122,13 @@ describe('Combobox', () => {
await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}'); // Focus is at index 0 to start with await userEvent.keyboard('{ArrowDown}{ArrowDown}{Enter}'); // Focus is at index 0 to start with
expect(onChangeHandler).toHaveBeenCalledWith(options[2]); expect(onChangeHandler).toHaveBeenCalledWith(options[2]);
expect(screen.queryByDisplayValue('Option 3')).toBeInTheDocument(); expect(screen.getByDisplayValue('Option 3')).toBeInTheDocument();
}); });
it('clears selected value', async () => { it('clears selected value', async () => {
render(<Combobox options={options} value={options[1].value} onChange={onChangeHandler} isClearable />); render(<Combobox options={options} value={options[1].value} onChange={onChangeHandler} isClearable />);
expect(screen.queryByDisplayValue('Option 2')).toBeInTheDocument(); expect(screen.getByDisplayValue('Option 2')).toBeInTheDocument();
const input = screen.getByRole('combobox'); const input = screen.getByRole('combobox');
await userEvent.click(input); await userEvent.click(input);
@ -158,7 +164,7 @@ describe('Combobox', () => {
const input = screen.getByRole('combobox'); const input = screen.getByRole('combobox');
await userEvent.click(input); await userEvent.click(input);
await userEvent.click(screen.getByRole('option', { name: 'Default' })); await userEvent.click(screen.getByRole('option', { name: 'Default' }));
expect(screen.queryByDisplayValue('Default')).toBeInTheDocument(); expect(screen.getByDisplayValue('Default')).toBeInTheDocument();
await userEvent.click(input); await userEvent.click(input);
@ -252,6 +258,7 @@ describe('Combobox', () => {
}); });
describe('size support', () => { describe('size support', () => {
// eslint-disable-next-line jest/expect-expect
it('should require minWidth to be set with auto width', () => { it('should require minWidth to be set with auto width', () => {
// @ts-expect-error // @ts-expect-error
render(<Combobox options={options} value={null} onChange={onChangeHandler} width="auto" />); render(<Combobox options={options} value={null} onChange={onChangeHandler} width="auto" />);
@ -264,7 +271,7 @@ describe('Combobox', () => {
const inputWrapper = screen.getByTestId('input-wrapper'); const inputWrapper = screen.getByTestId('input-wrapper');
const initialWidth = getComputedStyle(inputWrapper).width; const initialWidth = getComputedStyle(inputWrapper).width;
fireEvent.change(input, { target: { value: 'very very long value' } }); await user.type(input, 'very very long value');
const newWidth = getComputedStyle(inputWrapper).width; const newWidth = getComputedStyle(inputWrapper).width;
@ -278,7 +285,7 @@ describe('Combobox', () => {
const inputWrapper = screen.getByTestId('input-wrapper'); const inputWrapper = screen.getByTestId('input-wrapper');
const initialWidth = getComputedStyle(inputWrapper).width; const initialWidth = getComputedStyle(inputWrapper).width;
fireEvent.change(input, { target: { value: 'very very long value' } }); await user.type(input, 'very very long value');
const newWidth = getComputedStyle(inputWrapper).width; const newWidth = getComputedStyle(inputWrapper).width;

@ -348,7 +348,7 @@ describe('MultiCombobox', () => {
jest.advanceTimersByTime(1500); // Resolve the first request, should be ignored jest.advanceTimersByTime(1500); // Resolve the first request, should be ignored
expect(screen.queryByRole('option', { name: 'first' })).not.toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'first' })).not.toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'second' })).not.toBeInTheDocument(); expect(screen.queryByRole('option', { name: 'second' })).not.toBeInTheDocument();
expect(screen.queryByRole('option', { name: 'third' })).toBeInTheDocument(); expect(screen.getByRole('option', { name: 'third' })).toBeInTheDocument();
jest.clearAllTimers(); jest.clearAllTimers();
}); });

@ -23,7 +23,7 @@ describe('ConfirmButton', () => {
expect(onConfirm).toHaveBeenCalled(); expect(onConfirm).toHaveBeenCalled();
// Confirm button should be visible if closeOnConfirm is false // Confirm button should be visible if closeOnConfirm is false
expect(screen.queryByRole('button', { name: 'Confirm delete' })).toBeInTheDocument(); expect(screen.getByRole('button', { name: 'Confirm delete' })).toBeInTheDocument();
}); });
it('should hide confirm delete when closeOnConfirm is true', async () => { it('should hide confirm delete when closeOnConfirm is true', async () => {

@ -1,4 +1,4 @@
import { act, fireEvent, render, screen, waitFor } from '@testing-library/react'; import { act, render, screen, waitFor } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { useState } from 'react'; import { useState } from 'react';
@ -207,7 +207,7 @@ describe('ConfirmModal', () => {
expect(confirmButton).toBeEnabled(); expect(confirmButton).toBeEnabled();
fireEvent.click(confirmButton); await user.click(confirmButton);
// Ensure React processes the state update and calls useEffect in ConfirmModal // Ensure React processes the state update and calls useEffect in ConfirmModal
await act(() => { await act(() => {

@ -38,7 +38,7 @@ describe('Alerting Settings', () => {
//see https://github.com/grafana/grafana/issues/51417 //see https://github.com/grafana/grafana/issues/51417
it('should not show the option to select alertmanager data sources', () => { it('should not show the option to select alertmanager data sources', () => {
setup(); setup();
expect(screen.queryByText('Alertmanager data source')).toBeNull(); expect(screen.queryByText('Alertmanager data source')).not.toBeInTheDocument();
}); });
it('should show the option to manager alerts', () => { it('should show the option to manager alerts', () => {

@ -66,7 +66,7 @@ describe('Render', () => {
setup(); setup();
const b = screen.getByRole('button', { name: 'Add header' }); const b = screen.getByRole('button', { name: 'Add header' });
expect(b).toBeInTheDocument(); expect(b).toBeInTheDocument();
expect(b.getAttribute('type')).toBe('button'); expect(b).toHaveAttribute('type', 'button');
}); });
it('should remove a header', async () => { it('should remove a header', async () => {

@ -54,7 +54,7 @@ describe('DataSourceHttpSettings', () => {
it('should not render SIGV4 label if SIGV4 is not enabled', () => { it('should not render SIGV4 label if SIGV4 is not enabled', () => {
setup({ sigV4AuthToggleEnabled: false }); setup({ sigV4AuthToggleEnabled: false });
expect(screen.queryByText('SigV4 auth')).toBeNull(); expect(screen.queryByText('SigV4 auth')).not.toBeInTheDocument();
}); });
it('should render SIGV4 editor if provided and SIGV4 is enabled', () => { it('should render SIGV4 editor if provided and SIGV4 is enabled', () => {

@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { DatePicker } from './DatePicker'; import { DatePicker } from './DatePicker';
@ -27,27 +28,29 @@ describe('DatePicker', () => {
expect(screen.getByText('December 2020')).toBeInTheDocument(); expect(screen.getByText('December 2020')).toBeInTheDocument();
}); });
it('calls onChange when date is selected', () => { it('calls onChange when date is selected', async () => {
const onChange = jest.fn(); const onChange = jest.fn();
const user = userEvent.setup();
render(<DatePicker isOpen={true} onChange={onChange} onClose={jest.fn()} />); render(<DatePicker isOpen={true} onChange={onChange} onClose={jest.fn()} />);
expect(onChange).not.toHaveBeenCalled(); expect(onChange).not.toHaveBeenCalled();
// clicking the date // clicking the date
fireEvent.click(screen.getByText('14')); await user.click(screen.getByText('14'));
expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledTimes(1);
}); });
it('calls onClose when outside of wrapper is clicked', () => { it('calls onClose when outside of wrapper is clicked', async () => {
const onClose = jest.fn(); const onClose = jest.fn();
const user = userEvent.setup();
render(<DatePicker isOpen={true} onChange={jest.fn()} onClose={onClose} />); render(<DatePicker isOpen={true} onChange={jest.fn()} onClose={onClose} />);
expect(onClose).not.toHaveBeenCalled(); expect(onClose).not.toHaveBeenCalled();
fireEvent.click(document); await user.click(document.body);
expect(onClose).toHaveBeenCalledTimes(1); expect(onClose).toHaveBeenCalledTimes(1);
}); });

@ -1,10 +1,17 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { dateTimeFormat } from '@grafana/data'; import { dateTimeFormat } from '@grafana/data';
import { DatePickerWithInput } from './DatePickerWithInput'; import { DatePickerWithInput } from './DatePickerWithInput';
describe('DatePickerWithInput', () => { describe('DatePickerWithInput', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup({ applyAccept: false });
});
it('renders date input', () => { it('renders date input', () => {
render(<DatePickerWithInput onChange={jest.fn()} value={new Date(1400000000000)} />); render(<DatePickerWithInput onChange={jest.fn()} value={new Date(1400000000000)} />);
@ -24,43 +31,42 @@ describe('DatePickerWithInput', () => {
}); });
describe('input is clicked', () => { describe('input is clicked', () => {
it('renders input', () => { it('renders input', async () => {
render(<DatePickerWithInput onChange={jest.fn()} />); render(<DatePickerWithInput onChange={jest.fn()} />);
await user.click(screen.getByPlaceholderText('Date'));
fireEvent.click(screen.getByPlaceholderText('Date'));
expect(screen.getByPlaceholderText('Date')).toBeInTheDocument(); expect(screen.getByPlaceholderText('Date')).toBeInTheDocument();
}); });
it('renders calendar', () => { it('renders calendar', async () => {
render(<DatePickerWithInput onChange={jest.fn()} />); render(<DatePickerWithInput onChange={jest.fn()} />);
fireEvent.click(screen.getByPlaceholderText('Date')); await user.click(screen.getByPlaceholderText('Date'));
expect(screen.queryByTestId('date-picker')).toBeInTheDocument(); expect(screen.getByTestId('date-picker')).toBeInTheDocument();
}); });
}); });
it('calls onChange after date is selected', () => { it('calls onChange after date is selected', async () => {
const onChange = jest.fn(); const onChange = jest.fn();
render(<DatePickerWithInput onChange={onChange} />); render(<DatePickerWithInput onChange={onChange} />);
// open calendar and select a date // open calendar and select a date
fireEvent.click(screen.getByPlaceholderText('Date')); await user.click(screen.getByPlaceholderText('Date'));
fireEvent.click(screen.getByText('14')); await user.click(screen.getByText('14'));
expect(onChange).toHaveBeenCalledTimes(1); expect(onChange).toHaveBeenCalledTimes(1);
}); });
it('closes calendar after outside wrapper is clicked', () => { it('closes calendar after outside wrapper is clicked', async () => {
render(<DatePickerWithInput onChange={jest.fn()} />); render(<DatePickerWithInput onChange={jest.fn()} />);
// open calendar and click outside // open calendar and click outside
fireEvent.click(screen.getByPlaceholderText('Date')); await user.click(screen.getByPlaceholderText('Date'));
expect(screen.getByTestId('date-picker')).toBeInTheDocument(); expect(screen.getByTestId('date-picker')).toBeInTheDocument();
fireEvent.click(document); await user.click(document.body);
expect(screen.queryByTestId('date-picker')).not.toBeInTheDocument(); expect(screen.queryByTestId('date-picker')).not.toBeInTheDocument();
}); });

@ -30,7 +30,7 @@ describe('Date time picker', () => {
it('should render component', () => { it('should render component', () => {
renderDatetimePicker(); renderDatetimePicker();
expect(screen.queryByTestId('date-time-picker')).toBeInTheDocument(); expect(screen.getByTestId('date-time-picker')).toBeInTheDocument();
}); });
it.each(TEST_TIMEZONES)('input should have a value (timezone: %s)', (timeZone) => { it.each(TEST_TIMEZONES)('input should have a value (timezone: %s)', (timeZone) => {

@ -1,4 +1,4 @@
import { render, RenderResult } from '@testing-library/react'; import { render, RenderResult, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { useState } from 'react'; import { useState } from 'react';
@ -17,34 +17,34 @@ function setup(initial: RelativeTimeRange = { from: 900, to: 0 }): RenderResult
describe('RelativeTimePicker', () => { describe('RelativeTimePicker', () => {
it('should render the picker button with an user friendly text', () => { it('should render the picker button with an user friendly text', () => {
const { getByText } = setup({ from: 900, to: 0 }); setup({ from: 900, to: 0 });
expect(getByText('now-15m to now')).toBeInTheDocument(); expect(screen.getByText('now-15m to now')).toBeInTheDocument();
}); });
it('should open the picker when clicking the button', async () => { it('should open the picker when clicking the button', async () => {
const { getByText } = setup({ from: 900, to: 0 }); setup({ from: 900, to: 0 });
await userEvent.click(getByText('now-15m to now')); await userEvent.click(screen.getByText('now-15m to now'));
expect(getByText('Specify time range')).toBeInTheDocument(); expect(screen.getByText('Specify time range')).toBeInTheDocument();
expect(getByText('Example time ranges')).toBeInTheDocument(); expect(screen.getByText('Example time ranges')).toBeInTheDocument();
}); });
it('should not have open picker without clicking the button', () => { it('should not have open picker without clicking the button', () => {
const { queryByText } = setup({ from: 900, to: 0 }); setup({ from: 900, to: 0 });
expect(queryByText('Specify time range')).toBeNull(); expect(screen.queryByText('Specify time range')).not.toBeInTheDocument();
expect(queryByText('Example time ranges')).toBeNull(); expect(screen.queryByText('Example time ranges')).not.toBeInTheDocument();
}); });
it('should not be able to apply range via quick options', async () => { it('should not be able to apply range via quick options', async () => {
const { getByText, queryByText } = setup({ from: 900, to: 0 }); setup({ from: 900, to: 0 });
await userEvent.click(getByText('now-15m to now')); // open the picker await userEvent.click(screen.getByText('now-15m to now')); // open the picker
await userEvent.click(getByText('Last 30 minutes')); // select the quick range, should close picker. await userEvent.click(screen.getByText('Last 30 minutes')); // select the quick range, should close picker.
expect(queryByText('Specify time range')).toBeNull(); expect(screen.queryByText('Specify time range')).not.toBeInTheDocument();
expect(queryByText('Example time ranges')).toBeNull(); expect(screen.queryByText('Example time ranges')).not.toBeInTheDocument();
expect(getByText('now-30m to now')).toBeInTheDocument(); // new text on picker button expect(screen.getByText('now-30m to now')).toBeInTheDocument(); // new text on picker button
}); });
}); });

@ -86,7 +86,7 @@ describe('TimeRangeProvider', () => {
context2 = val; context2 = val;
} }
const renderContext = render( const { rerender } = render(
<TimeRangeProvider> <TimeRangeProvider>
<TestComponent onContextChange={onContextChange} /> <TestComponent onContextChange={onContextChange} />
<TestComponent onContextChange={onContextChange2} /> <TestComponent onContextChange={onContextChange2} />
@ -104,7 +104,7 @@ describe('TimeRangeProvider', () => {
syncedValue: timeRange, syncedValue: timeRange,
}); });
renderContext.rerender( rerender(
<TimeRangeProvider> <TimeRangeProvider>
<TestComponent onContextChange={onContextChange2} /> <TestComponent onContextChange={onContextChange2} />
</TimeRangeProvider> </TimeRangeProvider>

@ -20,7 +20,7 @@ const value: TimeRange = {
describe('TimePicker', () => { describe('TimePicker', () => {
it('renders buttons correctly', () => { it('renders buttons correctly', () => {
const container = render( render(
<TimeRangePicker <TimeRangePicker
onChangeTimeZone={() => {}} onChangeTimeZone={() => {}}
onChange={(value) => {}} onChange={(value) => {}}
@ -31,7 +31,7 @@ describe('TimePicker', () => {
/> />
); );
expect(container.queryByLabelText(/Time range selected/i)).toBeInTheDocument(); expect(screen.getByLabelText(/Time range selected/i)).toBeInTheDocument();
}); });
it('switches overlay content visibility when toolbar button is clicked twice', async () => { it('switches overlay content visibility when toolbar button is clicked twice', async () => {
@ -94,7 +94,7 @@ describe('TimePicker', () => {
it('does not submit wrapping forms', async () => { it('does not submit wrapping forms', async () => {
const onSubmit = jest.fn(); const onSubmit = jest.fn();
const container = render( render(
<form onSubmit={onSubmit}> <form onSubmit={onSubmit}>
<TimeRangePicker <TimeRangePicker
onChangeTimeZone={() => {}} onChangeTimeZone={() => {}}
@ -107,7 +107,7 @@ it('does not submit wrapping forms', async () => {
</form> </form>
); );
const clicks = container.getAllByRole('button').map((button) => userEvent.click(button)); const clicks = screen.getAllByRole('button').map((button) => userEvent.click(button));
await Promise.all(clicks); await Promise.all(clicks);

@ -15,16 +15,16 @@ describe('TimePickerContent', () => {
describe('Wide Screen', () => { describe('Wide Screen', () => {
it('renders with history', () => { it('renders with history', () => {
renderComponent({ value: absoluteValue, history }); renderComponent({ value: absoluteValue, history });
expect(screen.queryByText(/recently used absolute ranges/i)).toBeInTheDocument(); expect(screen.getByText(/recently used absolute ranges/i)).toBeInTheDocument();
expect(screen.queryByText(/2019-12-17 07:48:27 to 2019-12-17 07:49:27/i)).toBeInTheDocument(); expect(screen.getByText(/2019-12-17 07:48:27 to 2019-12-17 07:49:27/i)).toBeInTheDocument();
expect(screen.queryByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).toBeInTheDocument(); expect(screen.getByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).toBeInTheDocument();
}); });
it('renders with empty history', () => { it('renders with empty history', () => {
renderComponent({ value: absoluteValue }); renderComponent({ value: absoluteValue });
expect(screen.queryByText(/recently used absolute ranges/i)).not.toBeInTheDocument(); expect(screen.queryByText(/recently used absolute ranges/i)).not.toBeInTheDocument();
expect( expect(
screen.queryByText( screen.getByText(
/it looks like you haven't used this time picker before\. as soon as you enter some time intervals, recently used intervals will appear here\./i /it looks like you haven't used this time picker before\. as soon as you enter some time intervals, recently used intervals will appear here\./i
) )
).toBeInTheDocument(); ).toBeInTheDocument();
@ -39,7 +39,7 @@ describe('TimePickerContent', () => {
it('renders with relative picker', () => { it('renders with relative picker', () => {
renderComponent({ value: absoluteValue }); renderComponent({ value: absoluteValue });
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument(); expect(screen.getByText(/Last 5 minutes/i)).toBeInTheDocument();
}); });
it('renders without relative picker', () => { it('renders without relative picker', () => {
@ -49,7 +49,7 @@ describe('TimePickerContent', () => {
it('renders with timezone picker', () => { it('renders with timezone picker', () => {
renderComponent({ value: absoluteValue, hideTimeZone: false }); renderComponent({ value: absoluteValue, hideTimeZone: false });
expect(screen.queryByText(/coordinated universal time/i)).toBeInTheDocument(); expect(screen.getByText(/coordinated universal time/i)).toBeInTheDocument();
}); });
it('renders without timezone picker', () => { it('renders without timezone picker', () => {
@ -61,9 +61,9 @@ describe('TimePickerContent', () => {
describe('Narrow Screen', () => { describe('Narrow Screen', () => {
it('renders with history', () => { it('renders with history', () => {
renderComponent({ value: absoluteValue, history, isFullscreen: false }); renderComponent({ value: absoluteValue, history, isFullscreen: false });
expect(screen.queryByText(/recently used absolute ranges/i)).toBeInTheDocument(); expect(screen.getByText(/recently used absolute ranges/i)).toBeInTheDocument();
expect(screen.queryByText(/2019-12-17 07:48:27 to 2019-12-17 07:49:27/i)).toBeInTheDocument(); expect(screen.getByText(/2019-12-17 07:48:27 to 2019-12-17 07:49:27/i)).toBeInTheDocument();
expect(screen.queryByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).toBeInTheDocument(); expect(screen.getByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).toBeInTheDocument();
}); });
it('renders with empty history', () => { it('renders with empty history', () => {
@ -85,7 +85,7 @@ describe('TimePickerContent', () => {
it('renders with relative picker', () => { it('renders with relative picker', () => {
renderComponent({ value: absoluteValue, isFullscreen: false }); renderComponent({ value: absoluteValue, isFullscreen: false });
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument(); expect(screen.getByText(/Last 5 minutes/i)).toBeInTheDocument();
}); });
it('renders without relative picker', () => { it('renders without relative picker', () => {
@ -95,12 +95,12 @@ describe('TimePickerContent', () => {
it('renders with absolute picker when absolute value and quick ranges are visible', () => { it('renders with absolute picker when absolute value and quick ranges are visible', () => {
renderComponent({ value: absoluteValue, isFullscreen: false }); renderComponent({ value: absoluteValue, isFullscreen: false });
expect(screen.queryByLabelText('From')).toBeInTheDocument(); expect(screen.getByLabelText('From')).toBeInTheDocument();
}); });
it('renders with absolute picker when absolute value and quick ranges are hidden', () => { it('renders with absolute picker when absolute value and quick ranges are hidden', () => {
renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true }); renderComponent({ value: absoluteValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByLabelText('From')).toBeInTheDocument(); expect(screen.getByLabelText('From')).toBeInTheDocument();
}); });
it('renders without absolute picker when narrow screen and quick ranges are visible', () => { it('renders without absolute picker when narrow screen and quick ranges are visible', () => {
@ -110,7 +110,7 @@ describe('TimePickerContent', () => {
it('renders with absolute picker when narrow screen and quick ranges are hidden', () => { it('renders with absolute picker when narrow screen and quick ranges are hidden', () => {
renderComponent({ value: relativeValue, isFullscreen: false, hideQuickRanges: true }); renderComponent({ value: relativeValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByLabelText('From')).toBeInTheDocument(); expect(screen.getByLabelText('From')).toBeInTheDocument();
}); });
it('renders without timezone picker', () => { it('renders without timezone picker', () => {

@ -1,4 +1,4 @@
import { fireEvent, render, RenderResult } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { dateTimeParse, systemDateFormats, TimeRange } from '@grafana/data'; import { dateTimeParse, systemDateFormats, TimeRange } from '@grafana/data';
@ -6,19 +6,11 @@ import { selectors } from '@grafana/e2e-selectors';
import { TimeRangeContent } from './TimeRangeContent'; import { TimeRangeContent } from './TimeRangeContent';
type TimeRangeFormRenderResult = RenderResult & {
getCalendarDayByLabelText(label: string): HTMLButtonElement;
};
const mockClipboard = { const mockClipboard = {
writeText: jest.fn(), writeText: jest.fn(),
readText: jest.fn(), readText: jest.fn(),
}; };
Object.defineProperty(global.navigator, 'clipboard', {
value: mockClipboard,
});
const defaultTimeRange: TimeRange = { const defaultTimeRange: TimeRange = {
from: dateTimeParse('2021-06-17 00:00:00', { timeZone: 'utc' }), from: dateTimeParse('2021-06-17 00:00:00', { timeZone: 'utc' }),
to: dateTimeParse('2021-06-19 23:59:00', { timeZone: 'utc' }), to: dateTimeParse('2021-06-19 23:59:00', { timeZone: 'utc' }),
@ -33,21 +25,25 @@ const customRawTimeRange = {
to: '2023-06-19 23:59:00', to: '2023-06-19 23:59:00',
}; };
function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc'): TimeRangeFormRenderResult { function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc') {
const result = render(
<TimeRangeContent isFullscreen={true} value={initial} onApply={() => {}} timeZone={timeZone} />
);
return { return {
...result, ...render(<TimeRangeContent isFullscreen={true} value={initial} onApply={() => {}} timeZone={timeZone} />),
getCalendarDayByLabelText: (label: string) => { getCalendarDayByLabelText: (label: string) => {
const item = result.getByLabelText(label); const item = screen.getByLabelText(label);
return item?.parentElement as HTMLButtonElement; return item?.parentElement as HTMLButtonElement;
}, },
}; };
} }
describe('TimeRangeForm', () => { describe('TimeRangeForm', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
Object.defineProperty(global.navigator, 'clipboard', {
value: mockClipboard,
});
});
it('should render form correctly', () => { it('should render form correctly', () => {
const { getByLabelText, getByText, getAllByRole } = setup(); const { getByLabelText, getByText, getAllByRole } = setup();
@ -57,13 +53,14 @@ describe('TimeRangeForm', () => {
expect(getByLabelText('To')).toBeInTheDocument(); expect(getByLabelText('To')).toBeInTheDocument();
}); });
it('should display calendar when clicking the calendar icon', () => { it('should display calendar when clicking the calendar icon', async () => {
const { getByLabelText, getAllByRole } = setup(); const user = userEvent.setup();
setup();
const { TimePicker } = selectors.components; const { TimePicker } = selectors.components;
const openCalendarButton = getAllByRole('button', { name: 'Open calendar' }); const openCalendarButton = screen.getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[0]); await user.click(openCalendarButton[0]);
expect(getByLabelText(TimePicker.calendar.label)).toBeInTheDocument(); expect(screen.getByLabelText(TimePicker.calendar.label)).toBeInTheDocument();
}); });
it('should have passed time range entered in form', () => { it('should have passed time range entered in form', () => {
@ -121,30 +118,30 @@ describe('TimeRangeForm', () => {
}); });
}); });
it('should close calendar when clicking the close icon', () => { it('should close calendar when clicking the close icon', async () => {
const { queryByLabelText, getAllByRole, getByRole } = setup(); const { queryByLabelText, getAllByRole, getByRole } = setup();
const { TimePicker } = selectors.components; const { TimePicker } = selectors.components;
const openCalendarButton = getAllByRole('button', { name: 'Open calendar' }); const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[0]); await user.click(openCalendarButton[0]);
expect(getByRole('button', { name: 'Close calendar' })).toBeInTheDocument(); expect(getByRole('button', { name: 'Close calendar' })).toBeInTheDocument();
fireEvent.click(getByRole('button', { name: 'Close calendar' })); await user.click(getByRole('button', { name: 'Close calendar' }));
expect(queryByLabelText(TimePicker.calendar.label)).toBeNull(); expect(queryByLabelText(TimePicker.calendar.label)).not.toBeInTheDocument();
}); });
it('should not display calendar without clicking the calendar icon', () => { it('should not display calendar without clicking the calendar icon', () => {
const { queryByLabelText } = setup(); const { queryByLabelText } = setup();
const { TimePicker } = selectors.components; const { TimePicker } = selectors.components;
expect(queryByLabelText(TimePicker.calendar.label)).toBeNull(); expect(queryByLabelText(TimePicker.calendar.label)).not.toBeInTheDocument();
}); });
it('should have passed time range selected in calendar', () => { it('should have passed time range selected in calendar', async () => {
const { getAllByRole, getCalendarDayByLabelText } = setup(); const { getAllByRole, getCalendarDayByLabelText } = setup();
const openCalendarButton = getAllByRole('button', { name: 'Open calendar' }); const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[0]); await user.click(openCalendarButton[0]);
const from = getCalendarDayByLabelText('June 17, 2021'); const from = getCalendarDayByLabelText('June 17, 2021');
const to = getCalendarDayByLabelText('June 19, 2021'); const to = getCalendarDayByLabelText('June 19, 2021');
@ -152,11 +149,11 @@ describe('TimeRangeForm', () => {
expect(to).toHaveClass('react-calendar__tile--rangeEnd'); expect(to).toHaveClass('react-calendar__tile--rangeEnd');
}); });
it('should select correct time range in calendar when having a custom time zone', () => { it('should select correct time range in calendar when having a custom time zone', async () => {
const { getAllByRole, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo'); const { getAllByRole, getCalendarDayByLabelText } = setup(defaultTimeRange, 'Asia/Tokyo');
const openCalendarButton = getAllByRole('button', { name: 'Open calendar' }); const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[1]); await user.click(openCalendarButton[1]);
const from = getCalendarDayByLabelText('June 17, 2021'); const from = getCalendarDayByLabelText('June 17, 2021');
const to = getCalendarDayByLabelText('June 19, 2021'); const to = getCalendarDayByLabelText('June 19, 2021');
@ -165,9 +162,9 @@ describe('TimeRangeForm', () => {
}); });
it('should copy time range to clipboard', async () => { it('should copy time range to clipboard', async () => {
const { getByTestId } = setup(); setup();
await userEvent.click(getByTestId('data-testid TimePicker copy button')); await user.click(screen.getByTestId('data-testid TimePicker copy button'));
expect(global.navigator.clipboard.writeText).toHaveBeenCalledWith( expect(global.navigator.clipboard.writeText).toHaveBeenCalledWith(
JSON.stringify({ from: defaultTimeRange.raw.from, to: defaultTimeRange.raw.to }) JSON.stringify({ from: defaultTimeRange.raw.from, to: defaultTimeRange.raw.to })
); );

@ -1,9 +1,16 @@
import { act, fireEvent, render, renderHook, screen } from '@testing-library/react'; import { render, renderHook, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createRef, KeyboardEvent, RefObject } from 'react'; import { createRef, KeyboardEvent, RefObject } from 'react';
import { useListFocus } from './hooks'; import { useListFocus } from './hooks';
describe('useListFocus', () => { describe('useListFocus', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
});
const testid = 'test'; const testid = 'test';
const getListElement = ( const getListElement = (
ref: RefObject<HTMLUListElement>, ref: RefObject<HTMLUListElement>,
@ -26,7 +33,7 @@ describe('useListFocus', () => {
{ from: 'now-7d', to: 'now', display: 'Last 7 days' }, { from: 'now-7d', to: 'now', display: 'Last 7 days' },
]; ];
it('sets correct focused item on keydown', () => { it('sets correct focused item on keydown', async () => {
const ref = createRef<HTMLUListElement>(); const ref = createRef<HTMLUListElement>();
const { rerender } = render(getListElement(ref)); const { rerender } = render(getListElement(ref));
@ -38,10 +45,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 6 hours').tabIndex).toBe(-1); expect(screen.getByText('Last 6 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1); expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1); expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
await user.type(screen.getByTestId(testid), '{ArrowDown}');
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
const [handleKeys2] = result.current; const [handleKeys2] = result.current;
rerender(getListElement(ref, handleKeys2)); rerender(getListElement(ref, handleKeys2));
@ -51,9 +55,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1); expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1); expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowDown}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
const [handleKeys3] = result.current; const [handleKeys3] = result.current;
rerender(getListElement(ref, handleKeys3)); rerender(getListElement(ref, handleKeys3));
@ -63,9 +65,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 24 hours').tabIndex).toBe(0); expect(screen.getByText('Last 24 hours').tabIndex).toBe(0);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1); expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowUp}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
const [handleKeys4] = result.current; const [handleKeys4] = result.current;
rerender(getListElement(ref, handleKeys4)); rerender(getListElement(ref, handleKeys4));
@ -75,9 +75,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1); expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1); expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowUp}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
const [handleKeys5] = result.current; const [handleKeys5] = result.current;
rerender(getListElement(ref, handleKeys5)); rerender(getListElement(ref, handleKeys5));
@ -87,9 +85,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1); expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1); expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowUp}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
const [handleKeys6] = result.current; const [handleKeys6] = result.current;
rerender(getListElement(ref, handleKeys6)); rerender(getListElement(ref, handleKeys6));
@ -100,7 +96,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 7 days').tabIndex).toBe(0); expect(screen.getByText('Last 7 days').tabIndex).toBe(0);
}); });
it('clicks focused item when Enter key is pressed', () => { it('clicks focused item when Enter key is pressed', async () => {
const ref = createRef<HTMLUListElement>(); const ref = createRef<HTMLUListElement>();
const onClick = jest.fn(); const onClick = jest.fn();
const { rerender } = render(getListElement(ref)); const { rerender } = render(getListElement(ref));
@ -109,9 +105,7 @@ describe('useListFocus', () => {
const [handleKeys] = result.current; const [handleKeys] = result.current;
rerender(getListElement(ref, handleKeys, onClick)); rerender(getListElement(ref, handleKeys, onClick));
act(() => { await user.type(screen.getByTestId(testid), '{Enter}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Enter' });
});
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
}); });

@ -67,6 +67,7 @@ describe('ErrorBoundary', () => {
await screen.findByText(problem.message); await screen.findByText(problem.message);
expect(renderCount).toBeGreaterThan(0); expect(renderCount).toBeGreaterThan(0);
// eslint-disable-next-line testing-library/render-result-naming-convention
const oldRenderCount = renderCount; const oldRenderCount = renderCount;
rerender( rerender(

@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FileDropzone } from './FileDropzone'; import { FileDropzone } from './FileDropzone';
import { REMOVE_FILE } from './FileListItem'; import { REMOVE_FILE } from './FileListItem';
@ -53,13 +54,14 @@ describe('The FileDropzone component', () => {
}); });
it('should handle file removal from the list', async () => { it('should handle file removal from the list', async () => {
const user = userEvent.setup();
render(<FileDropzone />); render(<FileDropzone />);
dispatchEvt(screen.getByTestId('dropzone'), 'drop', mockData(files)); dispatchEvt(screen.getByTestId('dropzone'), 'drop', mockData(files));
expect(await screen.findAllByLabelText(REMOVE_FILE)).toHaveLength(3); expect(await screen.findAllByLabelText(REMOVE_FILE)).toHaveLength(3);
fireEvent.click(screen.getAllByLabelText(REMOVE_FILE)[0]); await user.click(screen.getAllByLabelText(REMOVE_FILE)[0]);
expect(await screen.findAllByLabelText(REMOVE_FILE)).toHaveLength(2); expect(await screen.findAllByLabelText(REMOVE_FILE)).toHaveLength(2);
}); });
@ -119,7 +121,7 @@ describe('The FileDropzone component', () => {
); );
render(component); render(component);
screen.getByText('Custom dropzone text'); expect(screen.getByText('Custom dropzone text')).toBeInTheDocument();
}); });
it('should handle file list overwrite when fileListRenderer is passed', async () => { it('should handle file list overwrite when fileListRenderer is passed', async () => {

@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FileListItem, REMOVE_FILE } from './FileListItem'; import { FileListItem, REMOVE_FILE } from './FileListItem';
@ -9,6 +10,12 @@ const file = ({
}) => new File([fileBits], fileName, options); }) => new File([fileBits], fileName, options);
describe('The FileListItem component', () => { describe('The FileListItem component', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup({ applyAccept: false });
});
it('should show an error message when error prop is not null', () => { it('should show an error message when error prop is not null', () => {
render(<FileListItem file={{ file: file({}), id: '1', error: new DOMException('error') }} />); render(<FileListItem file={{ file: file({}), id: '1', error: new DOMException('error') }} />);
@ -16,11 +23,11 @@ describe('The FileListItem component', () => {
expect(screen.queryByLabelText('Retry')).not.toBeInTheDocument(); expect(screen.queryByLabelText('Retry')).not.toBeInTheDocument();
}); });
it('should show a retry icon when error is not null and retryUpload prop is passed', () => { it('should show a retry icon when error is not null and retryUpload prop is passed', async () => {
const retryUpload = jest.fn(); const retryUpload = jest.fn();
render(<FileListItem file={{ file: file({}), id: '1', error: new DOMException('error'), retryUpload }} />); render(<FileListItem file={{ file: file({}), id: '1', error: new DOMException('error'), retryUpload }} />);
fireEvent.click(screen.getByLabelText('Retry')); await user.click(screen.getByLabelText('Retry'));
expect(screen.getByText('error')).toBeInTheDocument(); expect(screen.getByText('error')).toBeInTheDocument();
expect(screen.getByLabelText('Retry')); expect(screen.getByLabelText('Retry'));
@ -41,21 +48,21 @@ describe('The FileListItem component', () => {
expect(screen.queryByRole('progressbar')).not.toBeInTheDocument(); expect(screen.queryByRole('progressbar')).not.toBeInTheDocument();
}); });
it('should show a Cancel button when abortUpload prop is passed', () => { it('should show a Cancel button when abortUpload prop is passed', async () => {
const abortUpload = jest.fn(); const abortUpload = jest.fn();
render(<FileListItem file={{ file: file({}), id: '1', error: null, progress: 6, abortUpload }} />); render(<FileListItem file={{ file: file({}), id: '1', error: null, progress: 6, abortUpload }} />);
fireEvent.click(screen.getByRole('button', { name: /cancel/i })); await user.click(screen.getByRole('button', { name: /cancel/i }));
expect(abortUpload).toBeCalledTimes(1); expect(abortUpload).toBeCalledTimes(1);
}); });
it('should show a Remove icon when removeFile prop is passed', () => { it('should show a Remove icon when removeFile prop is passed', async () => {
const removeFile = jest.fn(); const removeFile = jest.fn();
const customFile = { file: file({}), id: '1', error: null }; const customFile = { file: file({}), id: '1', error: null };
render(<FileListItem file={customFile} removeFile={removeFile} />); render(<FileListItem file={customFile} removeFile={removeFile} />);
fireEvent.click(screen.getByRole('button', { name: REMOVE_FILE })); await user.click(screen.getByRole('button', { name: REMOVE_FILE }));
expect(removeFile).toHaveBeenCalledWith(customFile); expect(removeFile).toHaveBeenCalledWith(customFile);
}); });

@ -1,4 +1,4 @@
import { render, waitFor, fireEvent, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@ -6,17 +6,23 @@ import { selectors } from '@grafana/e2e-selectors';
import { FileUpload } from './FileUpload'; import { FileUpload } from './FileUpload';
describe('FileUpload', () => { describe('FileUpload', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup({ applyAccept: false });
});
it('should render upload button with default text and no file name', () => { it('should render upload button with default text and no file name', () => {
render(<FileUpload onFileUpload={() => {}} />); render(<FileUpload onFileUpload={() => {}} />);
expect(screen.getByText('Upload file')).toBeInTheDocument(); expect(screen.getByText('Upload file')).toBeInTheDocument();
expect(screen.queryByLabelText('File name')).toBeNull(); expect(screen.queryByLabelText('File name')).not.toBeInTheDocument();
}); });
it('clicking the button should trigger the input', async () => { it('clicking the button should trigger the input', async () => {
const mockInputOnClick = jest.fn(); const mockInputOnClick = jest.fn();
const { getByTestId } = render(<FileUpload onFileUpload={() => {}} />); render(<FileUpload onFileUpload={() => {}} />);
const button = screen.getByText('Upload file'); const button = screen.getByText('Upload file');
const input = getByTestId(selectors.components.FileUpload.inputField); const input = screen.getByTestId(selectors.components.FileUpload.inputField);
// attach a click listener to the input // attach a click listener to the input
input.onclick = mockInputOnClick; input.onclick = mockInputOnClick;
@ -29,14 +35,10 @@ describe('FileUpload', () => {
const testFileName = 'grafana.png'; const testFileName = 'grafana.png';
const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' }); const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' });
const onFileUpload = jest.fn(); const onFileUpload = jest.fn();
const { getByTestId } = render(<FileUpload onFileUpload={onFileUpload} showFileName={true} />); render(<FileUpload onFileUpload={onFileUpload} showFileName={true} />);
let uploader = getByTestId(selectors.components.FileUpload.inputField); const uploader = await screen.findByTestId(selectors.components.FileUpload.inputField);
await waitFor(() => await user.upload(uploader, file);
fireEvent.change(uploader, { const uploaderLabel = await screen.findByTestId(selectors.components.FileUpload.fileNameSpan);
target: { files: [file] },
})
);
let uploaderLabel = getByTestId(selectors.components.FileUpload.fileNameSpan);
expect(uploaderLabel).toHaveTextContent(testFileName); expect(uploaderLabel).toHaveTextContent(testFileName);
}); });
@ -44,14 +46,10 @@ describe('FileUpload', () => {
const testFileName = 'longFileName.something.png'; const testFileName = 'longFileName.something.png';
const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' }); const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' });
const onFileUpload = jest.fn(); const onFileUpload = jest.fn();
const { getByTestId } = render(<FileUpload onFileUpload={onFileUpload} showFileName={true} />); render(<FileUpload onFileUpload={onFileUpload} showFileName={true} />);
let uploader = getByTestId(selectors.components.FileUpload.inputField); const uploader = screen.getByTestId(selectors.components.FileUpload.inputField);
await waitFor(() => await user.upload(uploader, file);
fireEvent.change(uploader, { const uploaderLabel = screen.getByTestId(selectors.components.FileUpload.fileNameSpan);
target: { files: [file] },
})
);
let uploaderLabel = getByTestId(selectors.components.FileUpload.fileNameSpan);
expect(uploaderLabel).toHaveTextContent('longFileName.som....png'); expect(uploaderLabel).toHaveTextContent('longFileName.som....png');
}); });
}); });

@ -30,7 +30,7 @@ describe('Input', () => {
await userEvent.type(inputEl, 'abcde'); await userEvent.type(inputEl, 'abcde');
// blur the field // blur the field
await userEvent.click(document.body); await userEvent.click(document.body);
await screen.findByText(TEST_ERROR_MESSAGE); expect(await screen.findByText(TEST_ERROR_MESSAGE)).toBeInTheDocument();
}); });
it('should validate without error onBlur', async () => { it('should validate without error onBlur', async () => {

@ -1,4 +1,4 @@
import { getByRole, render, screen, cleanup } from '@testing-library/react'; import { render, screen, within } from '@testing-library/react';
import userEvent from '@testing-library/user-event'; import userEvent from '@testing-library/user-event';
import * as React from 'react'; import * as React from 'react';
@ -55,8 +55,8 @@ describe('InteractiveTable', () => {
const valueColumnHeader = screen.getByRole('columnheader', { name: 'Value' }); const valueColumnHeader = screen.getByRole('columnheader', { name: 'Value' });
const countryColumnHeader = screen.getByRole('columnheader', { name: 'Country' }); const countryColumnHeader = screen.getByRole('columnheader', { name: 'Country' });
const valueColumnSortButton = getByRole(valueColumnHeader, 'button'); const valueColumnSortButton = within(valueColumnHeader).getByRole('button');
const countryColumnSortButton = getByRole(countryColumnHeader, 'button'); const countryColumnSortButton = within(countryColumnHeader).getByRole('button');
expect(valueColumnHeader).not.toHaveAttribute('aria-sort'); expect(valueColumnHeader).not.toHaveAttribute('aria-sort');
expect(countryColumnHeader).not.toHaveAttribute('aria-sort'); expect(countryColumnHeader).not.toHaveAttribute('aria-sort');
@ -95,8 +95,9 @@ describe('InteractiveTable', () => {
expect(screen.getByTestId('test-1')).toHaveTextContent('Sweden'); expect(screen.getByTestId('test-1')).toHaveTextContent('Sweden');
expect(expanderButton.getAttribute('aria-controls')).toBe( expect(expanderButton).toHaveAttribute(
// ancestor tr's id should match the expander button's aria-controls attribute // ancestor tr's id should match the expander button's aria-controls attribute
'aria-controls',
screen.getByTestId('test-1').parentElement?.parentElement?.id screen.getByTestId('test-1').parentElement?.parentElement?.id
); );
}); });
@ -168,9 +169,9 @@ describe('InteractiveTable', () => {
const expandAllButton = screen.getByRole('button', { name: 'Expand all rows' }); const expandAllButton = screen.getByRole('button', { name: 'Expand all rows' });
await user.click(expandAllButton); await user.click(expandAllButton);
expect(screen.queryByTestId('test-1')).toBeInTheDocument(); expect(screen.getByTestId('test-1')).toBeInTheDocument();
expect(screen.queryByTestId('test-2')).toBeInTheDocument(); expect(screen.getByTestId('test-2')).toBeInTheDocument();
expect(screen.queryByTestId('test-3')).toBeInTheDocument(); expect(screen.getByTestId('test-3')).toBeInTheDocument();
}); });
}); });
describe('pagination', () => { describe('pagination', () => {
@ -182,8 +183,6 @@ describe('InteractiveTable', () => {
expect(screen.queryByRole('button', { name: /next/i })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /next/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /previous/i })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /previous/i })).not.toBeInTheDocument();
cleanup();
render(<InteractiveTable columns={columns} data={data} getRowId={getRowId} pageSize={0} />); render(<InteractiveTable columns={columns} data={data} getRowId={getRowId} pageSize={0} />);
expect(screen.queryByRole('button', { name: /next/i })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: /next/i })).not.toBeInTheDocument();

@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { toDataFrame, FieldType } from '@grafana/data'; import { toDataFrame, FieldType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
@ -47,7 +48,10 @@ const multiProps: MultiProps = {
const setup = (testProps?: Partial<Props>) => { const setup = (testProps?: Partial<Props>) => {
const editorProps = { ...props, ...testProps }; const editorProps = { ...props, ...testProps };
return render(<RefIDPicker {...editorProps} />); return {
...render(<RefIDPicker {...editorProps} />),
user: userEvent.setup(),
};
}; };
const multiSetup = (testProps?: Partial<MultiProps>) => { const multiSetup = (testProps?: Partial<MultiProps>) => {
@ -57,10 +61,10 @@ const multiSetup = (testProps?: Partial<MultiProps>) => {
describe('RefIDPicker', () => { describe('RefIDPicker', () => {
it('Should be able to select frame', async () => { it('Should be able to select frame', async () => {
setup(); const { user } = setup();
const select = await screen.findByRole('combobox'); const select = await screen.findByRole('combobox');
fireEvent.keyDown(select, { keyCode: 40 }); await user.type(select, '{ArrowDown}');
const selectOptions = screen.getAllByTestId(selectors.components.Select.option); const selectOptions = screen.getAllByTestId(selectors.components.Select.option);
@ -85,6 +89,8 @@ describe('RefIDMultiPicker', () => {
}); });
it('Should be able to select frame', async () => { it('Should be able to select frame', async () => {
// TODO: Work out why this doesn't work correctly with userEvent
/* eslint-disable testing-library/prefer-user-event */
multiSetup(); multiSetup();
const select = await screen.findByRole('combobox'); const select = await screen.findByRole('combobox');
@ -101,5 +107,6 @@ describe('RefIDMultiPicker', () => {
fireEvent.keyDown(select, { keyCode: 13 }); fireEvent.keyDown(select, { keyCode: 13 });
expect(mockOnChange).toHaveBeenLastCalledWith(['A', 'B']); expect(mockOnChange).toHaveBeenLastCalledWith(['A', 'B']);
/* eslint-enable testing-library/prefer-user-event */
}); });
}); });

@ -1,10 +1,17 @@
import { fireEvent, render, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { selectors } from '@grafana/e2e-selectors'; import { selectors } from '@grafana/e2e-selectors';
import { MenuItem, MenuItemProps } from './MenuItem'; import { MenuItem, MenuItemProps } from './MenuItem';
describe('MenuItem', () => { describe('MenuItem', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
});
const getMenuItem = (props?: Partial<MenuItemProps>) => ( const getMenuItem = (props?: Partial<MenuItemProps>) => (
<MenuItem ariaLabel={selectors.components.Menu.MenuItem('Test')} label="item1" icon="history" {...props} /> <MenuItem ariaLabel={selectors.components.Menu.MenuItem('Test')} label="item1" icon="history" {...props} />
); );
@ -19,12 +26,12 @@ describe('MenuItem', () => {
expect(screen.getByLabelText(selectors.components.Menu.MenuItem('Test')).nodeName).toBe('A'); expect(screen.getByLabelText(selectors.components.Menu.MenuItem('Test')).nodeName).toBe('A');
}); });
it('calls onClick when item is clicked', () => { it('calls onClick when item is clicked', async () => {
const onClick = jest.fn(); const onClick = jest.fn();
render(getMenuItem({ onClick })); render(getMenuItem({ onClick }));
fireEvent.click(screen.getByLabelText(selectors.components.Menu.MenuItem('Test'))); await user.click(screen.getByLabelText(selectors.components.Menu.MenuItem('Test')));
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
}); });
@ -41,7 +48,7 @@ describe('MenuItem', () => {
expect(screen.getByTestId(selectors.components.Menu.SubMenu.icon)).toBeInTheDocument(); expect(screen.getByTestId(selectors.components.Menu.SubMenu.icon)).toBeInTheDocument();
expect(screen.queryByTestId(selectors.components.Menu.SubMenu.container)).not.toBeInTheDocument(); expect(screen.queryByTestId(selectors.components.Menu.SubMenu.container)).not.toBeInTheDocument();
fireEvent.mouseOver(screen.getByLabelText(selectors.components.Menu.MenuItem('Test'))); await user.hover(screen.getByLabelText(selectors.components.Menu.MenuItem('Test')));
const subMenuContainer = await screen.findByTestId(selectors.components.Menu.SubMenu.container); const subMenuContainer = await screen.findByTestId(selectors.components.Menu.SubMenu.container);
@ -57,10 +64,10 @@ describe('MenuItem', () => {
render(getMenuItem({ childItems, disabled: true })); render(getMenuItem({ childItems, disabled: true }));
fireEvent.mouseOver(screen.getByLabelText(selectors.components.Menu.MenuItem('Test'))); await user.hover(screen.getByLabelText(selectors.components.Menu.MenuItem('Test')));
const subMenuContainer = screen.queryByLabelText(selectors.components.Menu.SubMenu.container); const subMenuContainer = screen.queryByLabelText(selectors.components.Menu.SubMenu.container);
expect(subMenuContainer).toBe(null); expect(subMenuContainer).not.toBeInTheDocument();
}); });
it('opens subMenu on ArrowRight', async () => { it('opens subMenu on ArrowRight', async () => {
@ -73,7 +80,7 @@ describe('MenuItem', () => {
expect(screen.queryByTestId(selectors.components.Menu.SubMenu.container)).not.toBeInTheDocument(); expect(screen.queryByTestId(selectors.components.Menu.SubMenu.container)).not.toBeInTheDocument();
fireEvent.keyDown(screen.getByLabelText(selectors.components.Menu.MenuItem('Test')), { key: 'ArrowRight' }); await user.type(screen.getByLabelText(selectors.components.Menu.MenuItem('Test')), '{ArrowRight}');
expect(await screen.findByTestId(selectors.components.Menu.SubMenu.container)).toBeInTheDocument(); expect(await screen.findByTestId(selectors.components.Menu.SubMenu.container)).toBeInTheDocument();
}); });

@ -1,9 +1,16 @@
import { act, fireEvent, render, renderHook, screen } from '@testing-library/react'; import { render, renderHook, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { createRef, KeyboardEvent, RefObject } from 'react'; import { createRef, KeyboardEvent, RefObject } from 'react';
import { useMenuFocus } from './hooks'; import { useMenuFocus } from './hooks';
describe('useMenuFocus', () => { describe('useMenuFocus', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
});
const testid = 'test'; const testid = 'test';
const getMenuElement = ( const getMenuElement = (
ref: RefObject<HTMLDivElement>, ref: RefObject<HTMLDivElement>,
@ -23,7 +30,7 @@ describe('useMenuFocus', () => {
</div> </div>
); );
it('sets correct focused item on keydown', () => { it('sets correct focused item on keydown', async () => {
const ref = createRef<HTMLDivElement>(); const ref = createRef<HTMLDivElement>();
const { result } = renderHook(() => useMenuFocus({ localRef: ref })); const { result } = renderHook(() => useMenuFocus({ localRef: ref }));
const [handleKeys] = result.current; const [handleKeys] = result.current;
@ -34,9 +41,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1); expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1); expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowDown}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
const [handleKeys2] = result.current; const [handleKeys2] = result.current;
rerender(getMenuElement(ref, handleKeys2)); rerender(getMenuElement(ref, handleKeys2));
@ -46,9 +51,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1); expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1); expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowDown}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
const [handleKeys3] = result.current; const [handleKeys3] = result.current;
rerender(getMenuElement(ref, handleKeys3)); rerender(getMenuElement(ref, handleKeys3));
@ -58,9 +61,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1); expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1); expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowUp}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
const [handleKeys4] = result.current; const [handleKeys4] = result.current;
rerender(getMenuElement(ref, handleKeys4)); rerender(getMenuElement(ref, handleKeys4));
@ -70,9 +71,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1); expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1); expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowUp}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
const [handleKeys5] = result.current; const [handleKeys5] = result.current;
rerender(getMenuElement(ref, handleKeys5)); rerender(getMenuElement(ref, handleKeys5));
@ -82,9 +81,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1); expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(0); expect(screen.getByText('Item 4').tabIndex).toBe(0);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowUp}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
const [handleKeys6] = result.current; const [handleKeys6] = result.current;
rerender(getMenuElement(ref, handleKeys6)); rerender(getMenuElement(ref, handleKeys6));
@ -95,16 +92,14 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 4').tabIndex).toBe(-1); expect(screen.getByText('Item 4').tabIndex).toBe(-1);
}); });
it('calls close on ArrowLeft and unfocuses all items', () => { it('calls close on ArrowLeft and unfocuses all items', async () => {
const ref = createRef<HTMLDivElement>(); const ref = createRef<HTMLDivElement>();
const close = jest.fn(); const close = jest.fn();
const { result } = renderHook(() => useMenuFocus({ localRef: ref, close })); const { result } = renderHook(() => useMenuFocus({ localRef: ref, close }));
const [handleKeys] = result.current; const [handleKeys] = result.current;
const { rerender } = render(getMenuElement(ref, handleKeys)); const { rerender } = render(getMenuElement(ref, handleKeys));
act(() => { await user.type(screen.getByTestId(testid), '{ArrowDown}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
const [handleKeys2] = result.current; const [handleKeys2] = result.current;
rerender(getMenuElement(ref, handleKeys2)); rerender(getMenuElement(ref, handleKeys2));
@ -113,9 +108,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 2').tabIndex).toBe(-1); expect(screen.getByText('Item 2').tabIndex).toBe(-1);
expect(screen.getByText('Item 3').tabIndex).toBe(-1); expect(screen.getByText('Item 3').tabIndex).toBe(-1);
act(() => { await user.type(screen.getByTestId(testid), '{ArrowLeft}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowLeft' });
});
expect(close).toHaveBeenCalled(); expect(close).toHaveBeenCalled();
expect(screen.getByText('Item 1').tabIndex).toBe(-1); expect(screen.getByText('Item 1').tabIndex).toBe(-1);
@ -123,7 +116,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1); expect(screen.getByText('Item 3').tabIndex).toBe(-1);
}); });
it('forwards keydown and open events', () => { it('forwards keydown and open events', async () => {
const ref = createRef<HTMLDivElement>(); const ref = createRef<HTMLDivElement>();
const onOpen = jest.fn(); const onOpen = jest.fn();
const onKeyDown = jest.fn(); const onKeyDown = jest.fn();
@ -132,10 +125,7 @@ describe('useMenuFocus', () => {
render(getMenuElement(ref, handleKeys)); render(getMenuElement(ref, handleKeys));
act(() => { await user.type(screen.getByTestId(testid), '{ArrowDown}{Home}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Home' });
});
expect(onOpen).toHaveBeenCalled(); expect(onOpen).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalledTimes(2); expect(onKeyDown).toHaveBeenCalledTimes(2);
@ -152,28 +142,24 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 1').tabIndex).toBe(0); expect(screen.getByText('Item 1').tabIndex).toBe(0);
}); });
it('clicks focused item when Enter key is pressed', () => { it('clicks focused item when Enter key is pressed', async () => {
const ref = createRef<HTMLDivElement>(); const ref = createRef<HTMLDivElement>();
const onClick = jest.fn(); const onClick = jest.fn();
const { result } = renderHook(() => useMenuFocus({ localRef: ref })); const { result } = renderHook(() => useMenuFocus({ localRef: ref }));
const [handleKeys] = result.current; const [handleKeys] = result.current;
const { rerender } = render(getMenuElement(ref, handleKeys, undefined, onClick)); const { rerender } = render(getMenuElement(ref, handleKeys, undefined, onClick));
act(() => { await user.type(screen.getByTestId(testid), '{ArrowDown}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
const [handleKeys2] = result.current; const [handleKeys2] = result.current;
rerender(getMenuElement(ref, handleKeys2, undefined, onClick)); rerender(getMenuElement(ref, handleKeys2, undefined, onClick));
act(() => { await user.type(screen.getByTestId(testid), '{Enter}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Enter' });
});
expect(onClick).toHaveBeenCalled(); expect(onClick).toHaveBeenCalled();
}); });
it('calls onClose on Tab or Escape', () => { it('calls onClose on Tab or Escape', async () => {
const ref = createRef<HTMLDivElement>(); const ref = createRef<HTMLDivElement>();
const onClose = jest.fn(); const onClose = jest.fn();
const { result } = renderHook(() => useMenuFocus({ localRef: ref, onClose })); const { result } = renderHook(() => useMenuFocus({ localRef: ref, onClose }));
@ -181,10 +167,7 @@ describe('useMenuFocus', () => {
render(getMenuElement(ref, handleKeys)); render(getMenuElement(ref, handleKeys));
act(() => { await user.type(screen.getByTestId(testid), '{Tab}{Escape}');
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Tab' });
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Escape' });
});
expect(onClose).toHaveBeenCalledTimes(2); expect(onClose).toHaveBeenCalledTimes(2);
}); });

@ -1,4 +1,5 @@
import { screen, render, fireEvent } from '@testing-library/react'; import { screen, render } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { useToggle } from 'react-use'; import { useToggle } from 'react-use';
import { LoadingState } from '@grafana/data'; import { LoadingState } from '@grafana/data';
@ -16,7 +17,10 @@ const setup = (propOverrides?: Partial<PanelChromeProps>) => {
}; };
Object.assign(props, propOverrides); Object.assign(props, propOverrides);
return render(<PanelChrome {...props} />); return {
...render(<PanelChrome {...props} />),
user: userEvent.setup(),
};
}; };
const setupWithToggleCollapsed = (propOverrides?: Partial<PanelChromeProps>) => { const setupWithToggleCollapsed = (propOverrides?: Partial<PanelChromeProps>) => {
@ -37,7 +41,10 @@ const setupWithToggleCollapsed = (propOverrides?: Partial<PanelChromeProps>) =>
return <PanelChrome {...props} collapsed={collapsed} onToggleCollapse={toggleCollapsed} />; return <PanelChrome {...props} collapsed={collapsed} onToggleCollapse={toggleCollapsed} />;
}; };
return render(<ControlledCollapseComponent />); return {
...render(<ControlledCollapseComponent />),
user: userEvent.setup(),
};
}; };
it('renders an empty panel with required props only', () => { it('renders an empty panel with required props only', () => {
@ -154,17 +161,17 @@ it('renders streaming indicator in the panel header if loadingState is streaming
expect(screen.getByTestId('panel-streaming')).toBeInTheDocument(); expect(screen.getByTestId('panel-streaming')).toBeInTheDocument();
}); });
it('collapses the controlled panel when user clicks on the chevron or the title', () => { it('collapses the controlled panel when user clicks on the chevron or the title', async () => {
setupWithToggleCollapsed({ title: 'Default title' }); const { user } = setupWithToggleCollapsed({ title: 'Default title' });
expect(screen.getByText("Panel's Content")).toBeInTheDocument(); expect(screen.getByText("Panel's Content")).toBeInTheDocument();
const button = screen.getByRole('button', { name: 'Default title' }); const button = screen.getByRole('button', { name: 'Default title' });
const content = screen.getByTestId(selectors.components.Panels.Panel.content); const content = screen.getByTestId(selectors.components.Panels.Panel.content);
// collapse button should have same aria-controls as the panel's content // collapse button should have same aria-controls as the panel's content
expect(button.getAttribute('aria-controls')).toBe(content.id); expect(button).toHaveAttribute('aria-controls', content.id);
fireEvent.click(button); await user.click(button);
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument(); expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
// aria-controls should be removed when panel is collapsed // aria-controls should be removed when panel is collapsed
@ -172,8 +179,8 @@ it('collapses the controlled panel when user clicks on the chevron or the title'
expect(screen.queryByTestId(selectors.components.Panels.Panel.content)?.id).toBe(undefined); expect(screen.queryByTestId(selectors.components.Panels.Panel.content)?.id).toBe(undefined);
}); });
it('collapses the uncontrolled panel when user clicks on the chevron or the title', () => { it('collapses the uncontrolled panel when user clicks on the chevron or the title', async () => {
setup({ title: 'Default title', collapsible: true }); const { user } = setup({ title: 'Default title', collapsible: true });
expect(screen.getByText("Panel's Content")).toBeInTheDocument(); expect(screen.getByText("Panel's Content")).toBeInTheDocument();
@ -181,9 +188,9 @@ it('collapses the uncontrolled panel when user clicks on the chevron or the titl
const content = screen.getByTestId(selectors.components.Panels.Panel.content); const content = screen.getByTestId(selectors.components.Panels.Panel.content);
// collapse button should have same aria-controls as the panel's content // collapse button should have same aria-controls as the panel's content
expect(button.getAttribute('aria-controls')).toBe(content.id); expect(button).toHaveAttribute('aria-controls', content.id);
fireEvent.click(button); await user.click(button);
expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument(); expect(screen.queryByText("Panel's Content")).not.toBeInTheDocument();
// aria-controls should be removed when panel is collapsed // aria-controls should be removed when panel is collapsed
expect(button).not.toHaveAttribute('aria-controlls'); expect(button).not.toHaveAttribute('aria-controlls');

@ -13,7 +13,7 @@ describe('<SecretInput />', () => {
// Should show an enabled input // Should show an enabled input
expect(input).toBeInTheDocument(); expect(input).toBeInTheDocument();
expect(input).not.toBeDisabled(); expect(input).toBeEnabled();
// Should not show a "Reset" button // Should not show a "Reset" button
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).not.toBeInTheDocument();
@ -30,7 +30,7 @@ describe('<SecretInput />', () => {
expect(input).toHaveValue(CONFIGURED_TEXT); expect(input).toHaveValue(CONFIGURED_TEXT);
// Should show a reset button // Should show a reset button
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument(); expect(screen.getByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument();
}); });
it('should be possible to reset a configured secret', async () => { it('should be possible to reset a configured secret', async () => {
@ -40,7 +40,7 @@ describe('<SecretInput />', () => {
// Should show a reset button and a disabled input // Should show a reset button and a disabled input
expect(screen.queryByPlaceholderText(PLACEHOLDER_TEXT)).toBeDisabled(); expect(screen.queryByPlaceholderText(PLACEHOLDER_TEXT)).toBeDisabled();
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument(); expect(screen.getByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument();
// Click on "Reset" // Click on "Reset"
await userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT })); await userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT }));

@ -15,7 +15,7 @@ describe('<SecretTextArea />', () => {
// Should show an enabled input // Should show an enabled input
expect(input).toBeInTheDocument(); expect(input).toBeInTheDocument();
expect(input).not.toBeDisabled(); expect(input).toBeEnabled();
// Should not show a "Reset" button // Should not show a "Reset" button
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).not.toBeInTheDocument(); expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).not.toBeInTheDocument();
@ -34,7 +34,7 @@ describe('<SecretTextArea />', () => {
expect(textArea).toHaveValue(CONFIGURED_TEXT); expect(textArea).toHaveValue(CONFIGURED_TEXT);
// Should show a reset button // Should show a reset button
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument(); expect(screen.getByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument();
}); });
it('should be possible to reset a configured secret', async () => { it('should be possible to reset a configured secret', async () => {
@ -44,7 +44,7 @@ describe('<SecretTextArea />', () => {
// Should show a reset button and a disabled input // Should show a reset button and a disabled input
expect(screen.queryByPlaceholderText(PLACEHOLDER_TEXT)).toBeDisabled(); expect(screen.queryByPlaceholderText(PLACEHOLDER_TEXT)).toBeDisabled();
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument(); expect(screen.getByRole('button', { name: RESET_BUTTON_TEXT })).toBeInTheDocument();
// Click on "Reset" // Click on "Reset"
await userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT })); await userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT }));

@ -26,7 +26,7 @@ describe('SelectBase', () => {
]; ];
it('renders without error', () => { it('renders without error', () => {
render(<SelectBase onChange={onChangeHandler} />); expect(() => render(<SelectBase onChange={onChangeHandler} />)).not.toThrow();
}); });
it('renders empty options information', async () => { it('renders empty options information', async () => {
@ -60,7 +60,7 @@ describe('SelectBase', () => {
}; };
render(<Test />); render(<Test />);
expect(screen.queryByText('Test label')).toBeInTheDocument(); expect(screen.getByText('Test label')).toBeInTheDocument();
await userEvent.click(screen.getByText('clear value')); await userEvent.click(screen.getByText('clear value'));
expect(screen.queryByText('Test label')).not.toBeInTheDocument(); expect(screen.queryByText('Test label')).not.toBeInTheDocument();
}); });
@ -76,13 +76,15 @@ describe('SelectBase', () => {
describe('is not provided', () => { describe('is not provided', () => {
it.each` it.each`
key key
${'ArrowDown'} ${'{ArrowDown}'}
${'ArrowUp'} ${'{ArrowUp}'}
${' '} ${' '}
`('opens on arrow down/up or space', ({ key }) => { `('opens on arrow down/up or space', async ({ key }) => {
const user = userEvent.setup();
render(<SelectBase onChange={onChangeHandler} />); render(<SelectBase onChange={onChangeHandler} />);
fireEvent.focus(screen.getByRole('combobox'));
fireEvent.keyDown(screen.getByRole('combobox'), { key }); await user.type(screen.getByRole('combobox'), key);
expect(screen.queryByText(/no options found/i)).toBeVisible(); expect(screen.queryByText(/no options found/i)).toBeVisible();
}); });
}); });
@ -293,7 +295,7 @@ describe('SelectBase', () => {
); );
await userEvent.click(screen.getByText(/Option 1/i)); await userEvent.click(screen.getByText(/Option 1/i));
const toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); const toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions);
expect(toggleAllOptions.textContent).toBe('Selected (1)'); expect(toggleAllOptions).toHaveTextContent('Selected (1)');
}); });
it('correctly removes all selected options when in indeterminate state', async () => { it('correctly removes all selected options when in indeterminate state', async () => {
@ -308,7 +310,7 @@ describe('SelectBase', () => {
); );
await userEvent.click(screen.getByText(/Option 1/i)); await userEvent.click(screen.getByText(/Option 1/i));
let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions);
expect(toggleAllOptions.textContent).toBe('Selected (1)'); expect(toggleAllOptions).toHaveTextContent('Selected (1)');
// Toggle all unselected when in indeterminate state // Toggle all unselected when in indeterminate state
await userEvent.click(toggleAllOptions); await userEvent.click(toggleAllOptions);
@ -327,7 +329,7 @@ describe('SelectBase', () => {
); );
await userEvent.click(screen.getByText(/Option 1/i)); await userEvent.click(screen.getByText(/Option 1/i));
let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions);
expect(toggleAllOptions.textContent).toBe('Selected (2)'); expect(toggleAllOptions).toHaveTextContent('Selected (2)');
// Toggle all unselected when in indeterminate state // Toggle all unselected when in indeterminate state
await userEvent.click(toggleAllOptions); await userEvent.click(toggleAllOptions);
@ -346,7 +348,7 @@ describe('SelectBase', () => {
); );
await userEvent.click(screen.getByText(/Choose/i)); await userEvent.click(screen.getByText(/Choose/i));
let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions); let toggleAllOptions = screen.getByTestId(selectors.components.Select.toggleAllOptions);
expect(toggleAllOptions.textContent).toBe('Selected (0)'); expect(toggleAllOptions).toHaveTextContent('Selected (0)');
// Toggle all unselected when in indeterminate state // Toggle all unselected when in indeterminate state
await userEvent.click(toggleAllOptions); await userEvent.click(toggleAllOptions);

@ -308,13 +308,13 @@ describe('Table', () => {
}), }),
}); });
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('7'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('7');
await userEvent.click(within(getColumnHeader(/number/)).getByRole('button', { name: '' })); await userEvent.click(within(getColumnHeader(/number/)).getByRole('button', { name: '' }));
await userEvent.click(screen.getByLabelText('1')); await userEvent.click(screen.getByLabelText('1'));
await userEvent.click(screen.getByText('Ok')); await userEvent.click(screen.getByText('Ok'));
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('3'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('3');
}); });
it('should filter rows and recalculate footer values when multiple filter values are selected', async () => { it('should filter rows and recalculate footer values when multiple filter values are selected', async () => {
@ -338,7 +338,7 @@ describe('Table', () => {
}); });
expect(within(getTable()).getAllByRole('row')).toHaveLength(8); expect(within(getTable()).getAllByRole('row')).toHaveLength(8);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('13'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('13');
await userEvent.click(within(getColumnHeader(/number/)).getByRole('button', { name: '' })); await userEvent.click(within(getColumnHeader(/number/)).getByRole('button', { name: '' }));
await userEvent.click(screen.getByLabelText('2')); await userEvent.click(screen.getByLabelText('2'));
@ -347,7 +347,7 @@ describe('Table', () => {
//4 + header row //4 + header row
expect(within(getTable()).getAllByRole('row')).toHaveLength(5); expect(within(getTable()).getAllByRole('row')).toHaveLength(5);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('10'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('10');
}); });
it('should reset when clear filters button is pressed', async () => { it('should reset when clear filters button is pressed', async () => {
@ -376,14 +376,14 @@ describe('Table', () => {
//3 + header row //3 + header row
expect(within(getTable()).getAllByRole('row')).toHaveLength(4); expect(within(getTable()).getAllByRole('row')).toHaveLength(4);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('3'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('3');
await userEvent.click(within(getColumnHeader(/number/)).getByRole('button', { name: '' })); await userEvent.click(within(getColumnHeader(/number/)).getByRole('button', { name: '' }));
await userEvent.click(screen.getByText('Clear filter')); await userEvent.click(screen.getByText('Clear filter'));
//5 + header row //5 + header row
expect(within(getTable()).getAllByRole('row')).toHaveLength(6); expect(within(getTable()).getAllByRole('row')).toHaveLength(6);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('7'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('7');
}); });
}); });
@ -410,7 +410,7 @@ describe('Table', () => {
//5 + header row //5 + header row
expect(within(getTable()).getAllByRole('row')).toHaveLength(6); expect(within(getTable()).getAllByRole('row')).toHaveLength(6);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('7'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('7');
const onSortByChange = jest.fn(); const onSortByChange = jest.fn();
const onCellFilterAdded = jest.fn(); const onCellFilterAdded = jest.fn();
@ -450,7 +450,7 @@ describe('Table', () => {
//4 + header row //4 + header row
expect(within(getTable()).getAllByRole('row')).toHaveLength(5); expect(within(getTable()).getAllByRole('row')).toHaveLength(5);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('5'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('5');
}); });
}); });
@ -500,7 +500,7 @@ describe('Table', () => {
}), }),
}); });
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('4'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('4');
}); });
it('should show count of rows when `count rows` is selected', async () => { it('should show count of rows when `count rows` is selected', async () => {
@ -523,10 +523,8 @@ describe('Table', () => {
}), }),
}); });
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual( expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('Count');
'Count' expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1]).toHaveTextContent('5');
);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');
}); });
it('should show correct counts when turning `count rows` on and off', async () => { it('should show correct counts when turning `count rows` on and off', async () => {
@ -549,10 +547,8 @@ describe('Table', () => {
}), }),
}); });
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual( expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('Count');
'Count' expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1]).toHaveTextContent('5');
);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');
const onSortByChange = jest.fn(); const onSortByChange = jest.fn();
const onCellFilterAdded = jest.fn(); const onCellFilterAdded = jest.fn();
@ -590,7 +586,7 @@ describe('Table', () => {
rerender(<Table {...props} />); rerender(<Table {...props} />);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual('4'); expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('4');
}); });
}); });
@ -714,7 +710,7 @@ describe('Table', () => {
tables = screen.getAllByRole('table'); tables = screen.getAllByRole('table');
expect(tables).toHaveLength(3); expect(tables).toHaveLength(3);
let subTable = screen.getAllByRole('table')[2]; let subTable = screen.getAllByRole('table')[2];
expect(subTable.style.height).toBe('108px'); expect(subTable).toHaveStyle({ height: '108px' });
// Sort again rows // Sort again rows
tables = screen.getAllByRole('table'); tables = screen.getAllByRole('table');
@ -732,7 +728,7 @@ describe('Table', () => {
rows = within(getTable()).getAllByRole('row'); rows = within(getTable()).getAllByRole('row');
await userEvent.click(within(rows[1]).getByLabelText('Expand row')); await userEvent.click(within(rows[1]).getByLabelText('Expand row'));
subTable = screen.getAllByRole('table')[2]; subTable = screen.getAllByRole('table')[2];
expect(subTable.style.height).toBe('108px'); expect(subTable).toHaveStyle({ height: '108px' });
}); });
}); });

@ -1,4 +1,5 @@
import { render, screen, fireEvent, act } from '@testing-library/react'; import { render, screen, fireEvent } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame, EventBus } from '@grafana/data'; import { applyFieldOverrides, createTheme, DataFrame, FieldType, toDataFrame, EventBus } from '@grafana/data';
import { TableCellDisplayMode } from '@grafana/schema'; import { TableCellDisplayMode } from '@grafana/schema';
@ -288,6 +289,40 @@ const createTimeDataFrame = (): DataFrame => {
}; };
describe('TableNG', () => { describe('TableNG', () => {
let user: ReturnType<typeof userEvent.setup>;
let origResizeObserver = global.ResizeObserver;
let origScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
beforeEach(() => {
user = userEvent.setup();
origResizeObserver = global.ResizeObserver;
origScrollIntoView = window.HTMLElement.prototype.scrollIntoView;
// Mock ResizeObserver
global.ResizeObserver = class ResizeObserver {
constructor(callback: any) {
// Store the callback
this.callback = callback;
}
callback: any;
observe() {
// Do nothing
}
unobserve() {
// Do nothing
}
disconnect() {
// Do nothing
}
};
window.HTMLElement.prototype.scrollIntoView = jest.fn();
});
afterEach(() => {
global.ResizeObserver = origResizeObserver;
window.HTMLElement.prototype.scrollIntoView = origScrollIntoView;
});
describe('Basic TableNG rendering', () => { describe('Basic TableNG rendering', () => {
it('renders a simple table with columns and rows', () => { it('renders a simple table with columns and rows', () => {
const { container } = render( const { container } = render(
@ -332,7 +367,7 @@ describe('TableNG', () => {
expect(expandIcons.length).toBeGreaterThan(0); expect(expandIcons.length).toBeGreaterThan(0);
}); });
it('expands nested data when clicking expand button', () => { it('expands nested data when clicking expand button', async () => {
// Mock scrollIntoView // Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn();
@ -356,7 +391,7 @@ describe('TableNG', () => {
// Click the expand button // Click the expand button
if (expandButton) { if (expandButton) {
fireEvent.click(expandButton); await user.click(expandButton);
// After expansion, we should have more rows // After expansion, we should have more rows
const expandedRows = container.querySelectorAll('[role="row"]'); const expandedRows = container.querySelectorAll('[role="row"]');
@ -581,7 +616,7 @@ describe('TableNG', () => {
if (nextButton) { if (nextButton) {
// Click to go to the next page // Click to go to the next page
fireEvent.click(nextButton); await user.click(nextButton);
// Get all cell content on the second page // Get all cell content on the second page
const newCells = container.querySelectorAll('[role="gridcell"]'); const newCells = container.querySelectorAll('[role="gridcell"]');
@ -597,7 +632,7 @@ describe('TableNG', () => {
// Check that the pagination summary shows we're on a different page // Check that the pagination summary shows we're on a different page
// The format appears to be "X - Y of Z rows" where X and Y are the row range // The format appears to be "X - Y of Z rows" where X and Y are the row range
expect(container.textContent).toMatch(/\d+ - \d+ of 100 rows/); expect(container).toHaveTextContent(/\d+ - \d+ of 100 rows/);
// Verify that the pagination summary has changed // Verify that the pagination summary has changed
const paginationSummary = container.querySelector('.paginationSummary, [class*="paginationSummary"]'); const paginationSummary = container.querySelector('.paginationSummary, [class*="paginationSummary"]');
@ -606,7 +641,7 @@ describe('TableNG', () => {
expect(summaryText).toContain('of 100 rows'); expect(summaryText).toContain('of 100 rows');
} else { } else {
// If we can't find the pagination summary by class, just check the container text // If we can't find the pagination summary by class, just check the container text
expect(container.textContent).toContain('of 100 rows'); expect(container).toHaveTextContent(/of 100 rows/);
} }
} }
}); });
@ -634,7 +669,7 @@ describe('TableNG', () => {
const sortButton = columnHeader.querySelector('button') || columnHeader; const sortButton = columnHeader.querySelector('button') || columnHeader;
// Click the sort button // Click the sort button
fireEvent.click(sortButton); await user.click(sortButton);
// After clicking, the header should have an aria-sort attribute // After clicking, the header should have an aria-sort attribute
const newSortAttribute = columnHeader.getAttribute('aria-sort'); const newSortAttribute = columnHeader.getAttribute('aria-sort');
@ -666,7 +701,7 @@ describe('TableNG', () => {
} }
}); });
it('cycles through ascending, descending, and no sort states', () => { it('cycles through ascending, descending, and no sort states', async () => {
// Mock scrollIntoView // Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn();
@ -682,23 +717,23 @@ describe('TableNG', () => {
const sortButton = columnHeader.querySelector('button') || columnHeader; const sortButton = columnHeader.querySelector('button') || columnHeader;
// Initial state - no sort // Initial state - no sort
expect(columnHeader.getAttribute('aria-sort')).toBeNull(); expect(columnHeader).not.toHaveAttribute('aria-sort');
// First click - should sort ascending // First click - should sort ascending
fireEvent.click(sortButton); await user.click(sortButton);
expect(columnHeader.getAttribute('aria-sort')).toBe('ascending'); expect(columnHeader).toHaveAttribute('aria-sort', 'ascending');
// Second click - should sort descending // Second click - should sort descending
fireEvent.click(sortButton); await user.click(sortButton);
expect(columnHeader.getAttribute('aria-sort')).toBe('descending'); expect(columnHeader).toHaveAttribute('aria-sort', 'descending');
// Third click - should remove sort // Third click - should remove sort
fireEvent.click(sortButton); await user.click(sortButton);
expect(columnHeader.getAttribute('aria-sort')).toBeNull(); expect(columnHeader).not.toHaveAttribute('aria-sort');
} }
}); });
it('supports multi-column sorting with shift key', () => { it('supports multi-column sorting with shift key', async () => {
// Mock scrollIntoView // Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = jest.fn(); window.HTMLElement.prototype.scrollIntoView = jest.fn();
@ -761,7 +796,7 @@ describe('TableNG', () => {
const valueColumnButton = columnHeaders[1].querySelector('button') || columnHeaders[1]; const valueColumnButton = columnHeaders[1].querySelector('button') || columnHeaders[1];
// 1. First sort by Category (ascending) // 1. First sort by Category (ascending)
fireEvent.click(categoryColumnButton); await user.click(categoryColumnButton);
// Check data is sorted by Category // Check data is sorted by Category
const categoryOnlySortedRows = getCellTextContent(); const categoryOnlySortedRows = getCellTextContent();
@ -796,7 +831,8 @@ describe('TableNG', () => {
expect(categoryBValues).toContain('4'); expect(categoryBValues).toContain('4');
// 2. Now add second sort column (Value) with shift key // 2. Now add second sort column (Value) with shift key
fireEvent.click(valueColumnButton, { shiftKey: true }); await user.keyboard('{Shift>}');
await user.click(valueColumnButton);
// Check data is sorted by Category and then by Value // Check data is sorted by Category and then by Value
const multiSortedRows = getCellTextContent(); const multiSortedRows = getCellTextContent();
@ -830,7 +866,8 @@ describe('TableNG', () => {
expect(multiSortedRows[4][2]).toBe('Alice'); expect(multiSortedRows[4][2]).toBe('Alice');
// 3. Change Value sort direction to descending // 3. Change Value sort direction to descending
fireEvent.click(valueColumnButton, { shiftKey: true }); await user.keyboard('{Shift>}');
await user.click(valueColumnButton);
// Check data is sorted by Category (asc) and then by Value (desc) // Check data is sorted by Category (asc) and then by Value (desc)
const multiSortedRowsDesc = getCellTextContent(); const multiSortedRowsDesc = getCellTextContent();
@ -864,7 +901,8 @@ describe('TableNG', () => {
expect(multiSortedRowsDesc[4][2]).toBe('Jane'); expect(multiSortedRowsDesc[4][2]).toBe('Jane');
// 4. Test removing the secondary sort by clicking a third time // 4. Test removing the secondary sort by clicking a third time
fireEvent.click(valueColumnButton, { shiftKey: true }); await user.keyboard('{Shift>}');
await user.click(valueColumnButton);
// The data should still be sorted by Category only // The data should still be sorted by Category only
const singleSortRows = getCellTextContent(); const singleSortRows = getCellTextContent();
@ -879,7 +917,7 @@ describe('TableNG', () => {
expect(singleSortRows[4][0]).toBe('B'); expect(singleSortRows[4][0]).toBe('B');
}); });
it('correctly sorts different data types', () => { it('correctly sorts different data types', async () => {
// Create a data frame with different data types // Create a data frame with different data types
const mixedDataFrame = toDataFrame({ const mixedDataFrame = toDataFrame({
name: 'MixedData', name: 'MixedData',
@ -918,7 +956,7 @@ describe('TableNG', () => {
// Test string column sorting // Test string column sorting
const stringColumnButton = columnHeaders[0].querySelector('button') || columnHeaders[0]; const stringColumnButton = columnHeaders[0].querySelector('button') || columnHeaders[0];
fireEvent.click(stringColumnButton); await user.click(stringColumnButton);
// Get cell values after sorting // Get cell values after sorting
let cells = container.querySelectorAll('[role="gridcell"]'); let cells = container.querySelectorAll('[role="gridcell"]');
@ -930,7 +968,7 @@ describe('TableNG', () => {
// Test number column sorting // Test number column sorting
const numberColumnButton = columnHeaders[1].querySelector('button') || columnHeaders[1]; const numberColumnButton = columnHeaders[1].querySelector('button') || columnHeaders[1];
fireEvent.click(numberColumnButton); await user.click(numberColumnButton);
// Get cell values after sorting // Get cell values after sorting
cells = container.querySelectorAll('[role="gridcell"]'); cells = container.querySelectorAll('[role="gridcell"]');
@ -1138,11 +1176,16 @@ describe('TableNG', () => {
// Find resize handle // Find resize handle
const resizeHandles = container.querySelectorAll('.rdg-header-row > [role="columnheader"] .rdg-resizer'); const resizeHandles = container.querySelectorAll('.rdg-header-row > [role="columnheader"] .rdg-resizer');
// TODO: This `if` doesn't even trigger - the test is evergreen.
// We should work out a reliable way to actually find and trigger the resize methods
// The querySelector doesn't return anything!
if (resizeHandles.length > 0) { if (resizeHandles.length > 0) {
// Simulate resize by triggering mousedown, mousemove, mouseup // Simulate resize by triggering mousedown, mousemove, mouseup
/* eslint-disable testing-library/prefer-user-event */
fireEvent.mouseDown(resizeHandles[0]); fireEvent.mouseDown(resizeHandles[0]);
fireEvent.mouseMove(resizeHandles[0], { clientX: 250 }); fireEvent.mouseMove(resizeHandles[0], { clientX: 250 });
fireEvent.mouseUp(resizeHandles[0]); fireEvent.mouseUp(resizeHandles[0]);
/* eslint-enable testing-library/prefer-user-event */
// Check that onColumnResize was called // Check that onColumnResize was called
expect(onColumnResize).toHaveBeenCalled(); expect(onColumnResize).toHaveBeenCalled();
@ -1202,28 +1245,6 @@ describe('TableNG', () => {
}); });
describe('Context menu', () => { describe('Context menu', () => {
beforeEach(() => {
// Mock ResizeObserver
global.ResizeObserver = class ResizeObserver {
constructor(callback: any) {
// Store the callback
this.callback = callback;
}
callback: any;
observe() {
// Do nothing
}
unobserve() {
// Do nothing
}
disconnect() {
// Do nothing
}
};
window.HTMLElement.prototype.scrollIntoView = jest.fn();
});
it('should show context menu on right-click', async () => { it('should show context menu on right-click', async () => {
const { container } = render( const { container } = render(
<TableNG enableVirtualization={false} data={createBasicDataFrame()} width={400} height={400} /> <TableNG enableVirtualization={false} data={createBasicDataFrame()} width={400} height={400} />
@ -1248,7 +1269,7 @@ describe('TableNG', () => {
}); });
describe('Cell inspection', () => { describe('Cell inspection', () => {
it('shows inspect icon when hovering over a cell with inspection enabled', () => { it('shows inspect icon when hovering over a cell with inspection enabled', async () => {
const inspectDataFrame = { const inspectDataFrame = {
...createBasicDataFrame(), ...createBasicDataFrame(),
fields: createBasicDataFrame().fields.map((field) => ({ fields: createBasicDataFrame().fields.map((field) => ({
@ -1285,7 +1306,7 @@ describe('TableNG', () => {
if (cellContent) { if (cellContent) {
// Trigger mouse enter on the cell content // Trigger mouse enter on the cell content
fireEvent.mouseEnter(cellContent); await user.hover(cellContent);
// Look for the inspect icon // Look for the inspect icon
const inspectIcon = container.querySelector('[aria-label="Inspect value"]'); const inspectIcon = container.querySelector('[aria-label="Inspect value"]');
@ -1491,13 +1512,12 @@ describe('TableNG', () => {
const dataGrid = screen.getByRole('grid'); const dataGrid = screen.getByRole('grid');
// Simulate scrolling // Simulate scrolling
act(() => {
fireEvent.scroll(dataGrid, { fireEvent.scroll(dataGrid, {
target: { target: {
scrollLeft: 100, scrollLeft: 100,
scrollTop: 50, scrollTop: 50,
}, },
});
}); });
// Rerender with the same data but different fieldConfig to trigger revId change // Rerender with the same data but different fieldConfig to trigger revId change

@ -1781,18 +1781,18 @@ describe('TableNG utils', () => {
const props = createMockProps(1, false, 0); const props = createMockProps(1, false, 0);
const expandedRows: number[] = []; // No expanded rows const expandedRows: number[] = []; // No expanded rows
const result = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false); const view = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false);
expect(result).toBeNull(); expect(view).toBeNull();
}); });
it('renders child rows when parent is expanded', () => { it('renders child rows when parent is expanded', () => {
const props = createMockProps(1, false, 0); const props = createMockProps(1, false, 0);
const expandedRows: number[] = [0]; // Row 0 is expanded const expandedRows: number[] = [0]; // Row 0 is expanded
const result = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false); const view = myRowRenderer('key-0', props, expandedRows, mockPanelContext, mockData, false);
expect(result).not.toBeNull(); expect(view).not.toBeNull();
}); });
it('adds aria-expanded attribute to parent rows with nested data', () => { it('adds aria-expanded attribute to parent rows with nested data', () => {

@ -1,22 +1,25 @@
import { render, fireEvent, screen } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { TagsInput } from './TagsInput'; import { TagsInput } from './TagsInput';
describe('TagsInput', () => { describe('TagsInput', () => {
it('removes tag when clicking on remove button', async () => { it('removes tag when clicking on remove button', async () => {
const user = userEvent.setup();
const onChange = jest.fn(); const onChange = jest.fn();
render(<TagsInput onChange={onChange} tags={['One', 'Two']} />); render(<TagsInput onChange={onChange} tags={['One', 'Two']} />);
fireEvent.click(await screen.findByRole('button', { name: /Remove tag: One/i })); await user.click(await screen.findByRole('button', { name: /Remove tag: One/i }));
expect(onChange).toHaveBeenCalledWith(['Two']); expect(onChange).toHaveBeenCalledWith(['Two']);
}); });
it('does NOT remove tag when clicking on remove button when disabled', async () => { it('does NOT remove tag when clicking on remove button when disabled', async () => {
const user = userEvent.setup();
const onChange = jest.fn(); const onChange = jest.fn();
render(<TagsInput onChange={onChange} tags={['One', 'Two']} disabled />); render(<TagsInput onChange={onChange} tags={['One', 'Two']} disabled />);
fireEvent.click(await screen.findByRole('button', { name: /Remove tag: One/i })); await user.click(await screen.findByRole('button', { name: /Remove tag: One/i }));
expect(onChange).not.toHaveBeenCalled(); expect(onChange).not.toHaveBeenCalled();
}); });

@ -75,7 +75,7 @@ describe('Toggletip', () => {
const button = screen.getByTestId('myButton'); const button = screen.getByTestId('myButton');
await userEvent.click(button); await userEvent.click(button);
expect(await screen.queryByTestId('toggletip-content')).not.toBeInTheDocument(); expect(screen.queryByTestId('toggletip-content')).not.toBeInTheDocument();
expect(onOpen).toHaveBeenCalledTimes(1); expect(onOpen).toHaveBeenCalledTimes(1);
}); });

@ -1,4 +1,4 @@
import { render } from '@testing-library/react'; import { render, screen } from '@testing-library/react';
import createMockRaf from 'mock-raf'; import createMockRaf from 'mock-raf';
import uPlot from 'uplot'; import uPlot from 'uplot';
@ -115,11 +115,11 @@ describe('UPlotChart', () => {
describe('config update', () => { describe('config update', () => {
it('skips uPlot intialization for width and height equal 0', async () => { it('skips uPlot intialization for width and height equal 0', async () => {
const { data, config } = mockData(); const { data, config } = mockData();
const { queryAllByTestId } = render( render(
<UPlotChart data={preparePlotData2(data, getStackingGroups(data))} config={config} width={0} height={0} /> <UPlotChart data={preparePlotData2(data, getStackingGroups(data))} config={config} width={0} height={0} />
); );
expect(queryAllByTestId('uplot-main-div')).toHaveLength(1); expect(screen.queryAllByTestId('uplot-main-div')).toHaveLength(1);
expect(uPlot).not.toBeCalled(); expect(uPlot).not.toBeCalled();
}); });

Loading…
Cancel
Save