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 1 week 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: {
'testing-library': testingLibraryPlugin,
'jest-dom': jestDomPlugin,
@ -307,12 +307,25 @@ module.exports = [
files: [
'public/app/features/alerting/**/__tests__/**/*.[jt]s?(x)',
'public/app/features/alerting/**/?(*.)+(spec|test).[jt]s?(x)',
'packages/grafana-ui/**/*.{spec,test}.{ts,tsx}',
],
rules: {
...testingLibraryPlugin.configs['flat/react'].rules,
...jestDomPlugin.configs['flat/recommended'].rules,
'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', () => {
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 { IconButton } from '../IconButton/IconButton';
@ -6,15 +7,16 @@ import { IconButton } from '../IconButton/IconButton';
import { Card } from './Card';
describe('Card', () => {
it('should execute callback when clicked', () => {
it('should execute callback when clicked', async () => {
const user = userEvent.setup();
const callback = jest.fn();
render(
<Card onClick={callback}>
<Card.Heading>Test Heading</Card.Heading>
</Card>
);
fireEvent.click(screen.getByText('Test Heading'));
expect(callback).toBeCalledTimes(1);
await user.click(screen.getByText('Test Heading'));
expect(callback).toHaveBeenCalledTimes(1);
});
describe('Card Actions', () => {
@ -31,8 +33,8 @@ describe('Card', () => {
</Card>
);
expect(screen.getByRole('button', { name: 'Click Me' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Delete' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Click Me' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Delete' })).toBeEnabled();
rerender(
<Card disabled>
@ -78,8 +80,8 @@ describe('Card', () => {
</Card>
);
expect(screen.getByRole('button', { name: 'Click Me' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Delete' })).not.toBeDisabled();
expect(screen.getByRole('button', { name: 'Click Me' })).toBeEnabled();
expect(screen.getByRole('button', { name: 'Delete' })).toBeEnabled();
});
it('Children should be conditional', () => {
@ -97,7 +99,7 @@ describe('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();
});

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

@ -27,7 +27,7 @@ describe('ColorPickerInput', () => {
await userEvent.click(screen.getByRole('textbox'));
expect(screen.getByTestId('color-popover')).toBeInTheDocument();
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 () => {

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

@ -348,7 +348,7 @@ describe('MultiCombobox', () => {
jest.advanceTimersByTime(1500); // Resolve the first request, should be ignored
expect(screen.queryByRole('option', { name: 'first' })).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();
});

@ -23,7 +23,7 @@ describe('ConfirmButton', () => {
expect(onConfirm).toHaveBeenCalled();
// 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 () => {

@ -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 { useState } from 'react';
@ -207,7 +207,7 @@ describe('ConfirmModal', () => {
expect(confirmButton).toBeEnabled();
fireEvent.click(confirmButton);
await user.click(confirmButton);
// Ensure React processes the state update and calls useEffect in ConfirmModal
await act(() => {

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

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

@ -54,7 +54,7 @@ describe('DataSourceHttpSettings', () => {
it('should not render SIGV4 label if SIGV4 is not enabled', () => {
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', () => {

@ -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';
@ -27,27 +28,29 @@ describe('DatePicker', () => {
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 user = userEvent.setup();
render(<DatePicker isOpen={true} onChange={onChange} onClose={jest.fn()} />);
expect(onChange).not.toHaveBeenCalled();
// clicking the date
fireEvent.click(screen.getByText('14'));
await user.click(screen.getByText('14'));
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 user = userEvent.setup();
render(<DatePicker isOpen={true} onChange={jest.fn()} onClose={onClose} />);
expect(onClose).not.toHaveBeenCalled();
fireEvent.click(document);
await user.click(document.body);
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 { DatePickerWithInput } from './DatePickerWithInput';
describe('DatePickerWithInput', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup({ applyAccept: false });
});
it('renders date input', () => {
render(<DatePickerWithInput onChange={jest.fn()} value={new Date(1400000000000)} />);
@ -24,43 +31,42 @@ describe('DatePickerWithInput', () => {
});
describe('input is clicked', () => {
it('renders input', () => {
it('renders input', async () => {
render(<DatePickerWithInput onChange={jest.fn()} />);
fireEvent.click(screen.getByPlaceholderText('Date'));
await user.click(screen.getByPlaceholderText('Date'));
expect(screen.getByPlaceholderText('Date')).toBeInTheDocument();
});
it('renders calendar', () => {
it('renders calendar', async () => {
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();
render(<DatePickerWithInput onChange={onChange} />);
// open calendar and select a date
fireEvent.click(screen.getByPlaceholderText('Date'));
fireEvent.click(screen.getByText('14'));
await user.click(screen.getByPlaceholderText('Date'));
await user.click(screen.getByText('14'));
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()} />);
// open calendar and click outside
fireEvent.click(screen.getByPlaceholderText('Date'));
await user.click(screen.getByPlaceholderText('Date'));
expect(screen.getByTestId('date-picker')).toBeInTheDocument();
fireEvent.click(document);
await user.click(document.body);
expect(screen.queryByTestId('date-picker')).not.toBeInTheDocument();
});

@ -30,7 +30,7 @@ describe('Date time picker', () => {
it('should render component', () => {
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) => {

@ -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 { useState } from 'react';
@ -17,34 +17,34 @@ function setup(initial: RelativeTimeRange = { from: 900, to: 0 }): RenderResult
describe('RelativeTimePicker', () => {
it('should render the picker button with an user friendly text', () => {
const { getByText } = setup({ from: 900, to: 0 });
expect(getByText('now-15m to now')).toBeInTheDocument();
setup({ from: 900, to: 0 });
expect(screen.getByText('now-15m to now')).toBeInTheDocument();
});
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(getByText('Example time ranges')).toBeInTheDocument();
expect(screen.getByText('Specify time range')).toBeInTheDocument();
expect(screen.getByText('Example time ranges')).toBeInTheDocument();
});
it('should not have open picker without clicking the button', () => {
const { queryByText } = setup({ from: 900, to: 0 });
expect(queryByText('Specify time range')).toBeNull();
expect(queryByText('Example time ranges')).toBeNull();
setup({ from: 900, to: 0 });
expect(screen.queryByText('Specify time range')).not.toBeInTheDocument();
expect(screen.queryByText('Example time ranges')).not.toBeInTheDocument();
});
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(getByText('Last 30 minutes')); // select the quick range, should close picker.
await userEvent.click(screen.getByText('now-15m to now')); // open the picker
await userEvent.click(screen.getByText('Last 30 minutes')); // select the quick range, should close picker.
expect(queryByText('Specify time range')).toBeNull();
expect(queryByText('Example time ranges')).toBeNull();
expect(screen.queryByText('Specify time range')).not.toBeInTheDocument();
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;
}
const renderContext = render(
const { rerender } = render(
<TimeRangeProvider>
<TestComponent onContextChange={onContextChange} />
<TestComponent onContextChange={onContextChange2} />
@ -104,7 +104,7 @@ describe('TimeRangeProvider', () => {
syncedValue: timeRange,
});
renderContext.rerender(
rerender(
<TimeRangeProvider>
<TestComponent onContextChange={onContextChange2} />
</TimeRangeProvider>

@ -20,7 +20,7 @@ const value: TimeRange = {
describe('TimePicker', () => {
it('renders buttons correctly', () => {
const container = render(
render(
<TimeRangePicker
onChangeTimeZone={() => {}}
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 () => {
@ -94,7 +94,7 @@ describe('TimePicker', () => {
it('does not submit wrapping forms', async () => {
const onSubmit = jest.fn();
const container = render(
render(
<form onSubmit={onSubmit}>
<TimeRangePicker
onChangeTimeZone={() => {}}
@ -107,7 +107,7 @@ it('does not submit wrapping forms', async () => {
</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);

@ -15,16 +15,16 @@ describe('TimePickerContent', () => {
describe('Wide Screen', () => {
it('renders with history', () => {
renderComponent({ value: absoluteValue, history });
expect(screen.queryByText(/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.queryByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).toBeInTheDocument();
expect(screen.getByText(/recently used absolute ranges/i)).toBeInTheDocument();
expect(screen.getByText(/2019-12-17 07:48:27 to 2019-12-17 07:49: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', () => {
renderComponent({ value: absoluteValue });
expect(screen.queryByText(/recently used absolute ranges/i)).not.toBeInTheDocument();
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
)
).toBeInTheDocument();
@ -39,7 +39,7 @@ describe('TimePickerContent', () => {
it('renders with relative picker', () => {
renderComponent({ value: absoluteValue });
expect(screen.queryByText(/Last 5 minutes/i)).toBeInTheDocument();
expect(screen.getByText(/Last 5 minutes/i)).toBeInTheDocument();
});
it('renders without relative picker', () => {
@ -49,7 +49,7 @@ describe('TimePickerContent', () => {
it('renders with timezone picker', () => {
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', () => {
@ -61,9 +61,9 @@ describe('TimePickerContent', () => {
describe('Narrow Screen', () => {
it('renders with history', () => {
renderComponent({ value: absoluteValue, history, isFullscreen: false });
expect(screen.queryByText(/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.queryByText(/2019-10-18 07:50:27 to 2019-10-18 07:51:27/i)).toBeInTheDocument();
expect(screen.getByText(/recently used absolute ranges/i)).toBeInTheDocument();
expect(screen.getByText(/2019-12-17 07:48:27 to 2019-12-17 07:49: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', () => {
@ -85,7 +85,7 @@ describe('TimePickerContent', () => {
it('renders with relative picker', () => {
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', () => {
@ -95,12 +95,12 @@ describe('TimePickerContent', () => {
it('renders with absolute picker when absolute value and quick ranges are visible', () => {
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', () => {
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', () => {
@ -110,7 +110,7 @@ describe('TimePickerContent', () => {
it('renders with absolute picker when narrow screen and quick ranges are hidden', () => {
renderComponent({ value: relativeValue, isFullscreen: false, hideQuickRanges: true });
expect(screen.queryByLabelText('From')).toBeInTheDocument();
expect(screen.getByLabelText('From')).toBeInTheDocument();
});
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 { dateTimeParse, systemDateFormats, TimeRange } from '@grafana/data';
@ -6,19 +6,11 @@ import { selectors } from '@grafana/e2e-selectors';
import { TimeRangeContent } from './TimeRangeContent';
type TimeRangeFormRenderResult = RenderResult & {
getCalendarDayByLabelText(label: string): HTMLButtonElement;
};
const mockClipboard = {
writeText: jest.fn(),
readText: jest.fn(),
};
Object.defineProperty(global.navigator, 'clipboard', {
value: mockClipboard,
});
const defaultTimeRange: TimeRange = {
from: dateTimeParse('2021-06-17 00:00: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',
};
function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc'): TimeRangeFormRenderResult {
const result = render(
<TimeRangeContent isFullscreen={true} value={initial} onApply={() => {}} timeZone={timeZone} />
);
function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc') {
return {
...result,
...render(<TimeRangeContent isFullscreen={true} value={initial} onApply={() => {}} timeZone={timeZone} />),
getCalendarDayByLabelText: (label: string) => {
const item = result.getByLabelText(label);
const item = screen.getByLabelText(label);
return item?.parentElement as HTMLButtonElement;
},
};
}
describe('TimeRangeForm', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
Object.defineProperty(global.navigator, 'clipboard', {
value: mockClipboard,
});
});
it('should render form correctly', () => {
const { getByLabelText, getByText, getAllByRole } = setup();
@ -57,13 +53,14 @@ describe('TimeRangeForm', () => {
expect(getByLabelText('To')).toBeInTheDocument();
});
it('should display calendar when clicking the calendar icon', () => {
const { getByLabelText, getAllByRole } = setup();
it('should display calendar when clicking the calendar icon', async () => {
const user = userEvent.setup();
setup();
const { TimePicker } = selectors.components;
const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
const openCalendarButton = screen.getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[0]);
expect(getByLabelText(TimePicker.calendar.label)).toBeInTheDocument();
await user.click(openCalendarButton[0]);
expect(screen.getByLabelText(TimePicker.calendar.label)).toBeInTheDocument();
});
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 { TimePicker } = selectors.components;
const openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[0]);
await user.click(openCalendarButton[0]);
expect(getByRole('button', { name: 'Close calendar' })).toBeInTheDocument();
fireEvent.click(getByRole('button', { name: 'Close calendar' }));
expect(queryByLabelText(TimePicker.calendar.label)).toBeNull();
await user.click(getByRole('button', { name: 'Close calendar' }));
expect(queryByLabelText(TimePicker.calendar.label)).not.toBeInTheDocument();
});
it('should not display calendar without clicking the calendar icon', () => {
const { queryByLabelText } = setup();
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 openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[0]);
await user.click(openCalendarButton[0]);
const from = getCalendarDayByLabelText('June 17, 2021');
const to = getCalendarDayByLabelText('June 19, 2021');
@ -152,11 +149,11 @@ describe('TimeRangeForm', () => {
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 openCalendarButton = getAllByRole('button', { name: 'Open calendar' });
fireEvent.click(openCalendarButton[1]);
await user.click(openCalendarButton[1]);
const from = getCalendarDayByLabelText('June 17, 2021');
const to = getCalendarDayByLabelText('June 19, 2021');
@ -165,9 +162,9 @@ describe('TimeRangeForm', () => {
});
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(
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 { useListFocus } from './hooks';
describe('useListFocus', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
});
const testid = 'test';
const getListElement = (
ref: RefObject<HTMLUListElement>,
@ -26,7 +33,7 @@ describe('useListFocus', () => {
{ 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 { rerender } = render(getListElement(ref));
@ -38,10 +45,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 6 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
await user.type(screen.getByTestId(testid), '{ArrowDown}');
const [handleKeys2] = result.current;
rerender(getListElement(ref, handleKeys2));
@ -51,9 +55,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
await user.type(screen.getByTestId(testid), '{ArrowDown}');
const [handleKeys3] = result.current;
rerender(getListElement(ref, handleKeys3));
@ -63,9 +65,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 24 hours').tabIndex).toBe(0);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
await user.type(screen.getByTestId(testid), '{ArrowUp}');
const [handleKeys4] = result.current;
rerender(getListElement(ref, handleKeys4));
@ -75,9 +75,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
await user.type(screen.getByTestId(testid), '{ArrowUp}');
const [handleKeys5] = result.current;
rerender(getListElement(ref, handleKeys5));
@ -87,9 +85,7 @@ describe('useListFocus', () => {
expect(screen.getByText('Last 24 hours').tabIndex).toBe(-1);
expect(screen.getByText('Last 7 days').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
await user.type(screen.getByTestId(testid), '{ArrowUp}');
const [handleKeys6] = result.current;
rerender(getListElement(ref, handleKeys6));
@ -100,7 +96,7 @@ describe('useListFocus', () => {
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 onClick = jest.fn();
const { rerender } = render(getListElement(ref));
@ -109,9 +105,7 @@ describe('useListFocus', () => {
const [handleKeys] = result.current;
rerender(getListElement(ref, handleKeys, onClick));
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Enter' });
});
await user.type(screen.getByTestId(testid), '{Enter}');
expect(onClick).toHaveBeenCalled();
});

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

@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { FileDropzone } from './FileDropzone';
import { REMOVE_FILE } from './FileListItem';
@ -53,13 +54,14 @@ describe('The FileDropzone component', () => {
});
it('should handle file removal from the list', async () => {
const user = userEvent.setup();
render(<FileDropzone />);
dispatchEvt(screen.getByTestId('dropzone'), 'drop', mockData(files));
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);
});
@ -119,7 +121,7 @@ describe('The FileDropzone 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 () => {

@ -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';
@ -9,6 +10,12 @@ const file = ({
}) => new File([fileBits], fileName, options);
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', () => {
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();
});
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();
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.getByLabelText('Retry'));
@ -41,21 +48,21 @@ describe('The FileListItem component', () => {
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();
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);
});
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 customFile = { file: file({}), id: '1', error: null };
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);
});

@ -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 { selectors } from '@grafana/e2e-selectors';
@ -6,17 +6,23 @@ import { selectors } from '@grafana/e2e-selectors';
import { FileUpload } from './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', () => {
render(<FileUpload onFileUpload={() => {}} />);
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 () => {
const mockInputOnClick = jest.fn();
const { getByTestId } = render(<FileUpload onFileUpload={() => {}} />);
render(<FileUpload onFileUpload={() => {}} />);
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
input.onclick = mockInputOnClick;
@ -29,14 +35,10 @@ describe('FileUpload', () => {
const testFileName = 'grafana.png';
const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' });
const onFileUpload = jest.fn();
const { getByTestId } = render(<FileUpload onFileUpload={onFileUpload} showFileName={true} />);
let uploader = getByTestId(selectors.components.FileUpload.inputField);
await waitFor(() =>
fireEvent.change(uploader, {
target: { files: [file] },
})
);
let uploaderLabel = getByTestId(selectors.components.FileUpload.fileNameSpan);
render(<FileUpload onFileUpload={onFileUpload} showFileName={true} />);
const uploader = await screen.findByTestId(selectors.components.FileUpload.inputField);
await user.upload(uploader, file);
const uploaderLabel = await screen.findByTestId(selectors.components.FileUpload.fileNameSpan);
expect(uploaderLabel).toHaveTextContent(testFileName);
});
@ -44,14 +46,10 @@ describe('FileUpload', () => {
const testFileName = 'longFileName.something.png';
const file = new File(['(⌐□_□)'], testFileName, { type: 'image/png' });
const onFileUpload = jest.fn();
const { getByTestId } = render(<FileUpload onFileUpload={onFileUpload} showFileName={true} />);
let uploader = getByTestId(selectors.components.FileUpload.inputField);
await waitFor(() =>
fireEvent.change(uploader, {
target: { files: [file] },
})
);
let uploaderLabel = getByTestId(selectors.components.FileUpload.fileNameSpan);
render(<FileUpload onFileUpload={onFileUpload} showFileName={true} />);
const uploader = screen.getByTestId(selectors.components.FileUpload.inputField);
await user.upload(uploader, file);
const uploaderLabel = screen.getByTestId(selectors.components.FileUpload.fileNameSpan);
expect(uploaderLabel).toHaveTextContent('longFileName.som....png');
});
});

@ -30,7 +30,7 @@ describe('Input', () => {
await userEvent.type(inputEl, 'abcde');
// blur the field
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 () => {

@ -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 * as React from 'react';
@ -55,8 +55,8 @@ describe('InteractiveTable', () => {
const valueColumnHeader = screen.getByRole('columnheader', { name: 'Value' });
const countryColumnHeader = screen.getByRole('columnheader', { name: 'Country' });
const valueColumnSortButton = getByRole(valueColumnHeader, 'button');
const countryColumnSortButton = getByRole(countryColumnHeader, 'button');
const valueColumnSortButton = within(valueColumnHeader).getByRole('button');
const countryColumnSortButton = within(countryColumnHeader).getByRole('button');
expect(valueColumnHeader).not.toHaveAttribute('aria-sort');
expect(countryColumnHeader).not.toHaveAttribute('aria-sort');
@ -95,8 +95,9 @@ describe('InteractiveTable', () => {
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
'aria-controls',
screen.getByTestId('test-1').parentElement?.parentElement?.id
);
});
@ -168,9 +169,9 @@ describe('InteractiveTable', () => {
const expandAllButton = screen.getByRole('button', { name: 'Expand all rows' });
await user.click(expandAllButton);
expect(screen.queryByTestId('test-1')).toBeInTheDocument();
expect(screen.queryByTestId('test-2')).toBeInTheDocument();
expect(screen.queryByTestId('test-3')).toBeInTheDocument();
expect(screen.getByTestId('test-1')).toBeInTheDocument();
expect(screen.getByTestId('test-2')).toBeInTheDocument();
expect(screen.getByTestId('test-3')).toBeInTheDocument();
});
});
describe('pagination', () => {
@ -182,8 +183,6 @@ describe('InteractiveTable', () => {
expect(screen.queryByRole('button', { name: /next/i })).not.toBeInTheDocument();
expect(screen.queryByRole('button', { name: /previous/i })).not.toBeInTheDocument();
cleanup();
render(<InteractiveTable columns={columns} data={data} getRowId={getRowId} pageSize={0} />);
expect(screen.queryByRole('button', { name: /next/i })).not.toBeInTheDocument();

@ -1,4 +1,5 @@
import { fireEvent, render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { toDataFrame, FieldType } from '@grafana/data';
import { selectors } from '@grafana/e2e-selectors';
@ -47,7 +48,10 @@ const multiProps: MultiProps = {
const setup = (testProps?: Partial<Props>) => {
const editorProps = { ...props, ...testProps };
return render(<RefIDPicker {...editorProps} />);
return {
...render(<RefIDPicker {...editorProps} />),
user: userEvent.setup(),
};
};
const multiSetup = (testProps?: Partial<MultiProps>) => {
@ -57,10 +61,10 @@ const multiSetup = (testProps?: Partial<MultiProps>) => {
describe('RefIDPicker', () => {
it('Should be able to select frame', async () => {
setup();
const { user } = setup();
const select = await screen.findByRole('combobox');
fireEvent.keyDown(select, { keyCode: 40 });
await user.type(select, '{ArrowDown}');
const selectOptions = screen.getAllByTestId(selectors.components.Select.option);
@ -85,6 +89,8 @@ describe('RefIDMultiPicker', () => {
});
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();
const select = await screen.findByRole('combobox');
@ -101,5 +107,6 @@ describe('RefIDMultiPicker', () => {
fireEvent.keyDown(select, { keyCode: 13 });
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 { MenuItem, MenuItemProps } from './MenuItem';
describe('MenuItem', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
});
const getMenuItem = (props?: Partial<MenuItemProps>) => (
<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');
});
it('calls onClick when item is clicked', () => {
it('calls onClick when item is clicked', async () => {
const onClick = jest.fn();
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();
});
@ -41,7 +48,7 @@ describe('MenuItem', () => {
expect(screen.getByTestId(selectors.components.Menu.SubMenu.icon)).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);
@ -57,10 +64,10 @@ describe('MenuItem', () => {
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);
expect(subMenuContainer).toBe(null);
expect(subMenuContainer).not.toBeInTheDocument();
});
it('opens subMenu on ArrowRight', async () => {
@ -73,7 +80,7 @@ describe('MenuItem', () => {
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();
});

@ -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 { useMenuFocus } from './hooks';
describe('useMenuFocus', () => {
let user: ReturnType<typeof userEvent.setup>;
beforeEach(() => {
user = userEvent.setup();
});
const testid = 'test';
const getMenuElement = (
ref: RefObject<HTMLDivElement>,
@ -23,7 +30,7 @@ describe('useMenuFocus', () => {
</div>
);
it('sets correct focused item on keydown', () => {
it('sets correct focused item on keydown', async () => {
const ref = createRef<HTMLDivElement>();
const { result } = renderHook(() => useMenuFocus({ localRef: ref }));
const [handleKeys] = result.current;
@ -34,9 +41,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
await user.type(screen.getByTestId(testid), '{ArrowDown}');
const [handleKeys2] = result.current;
rerender(getMenuElement(ref, handleKeys2));
@ -46,9 +51,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
await user.type(screen.getByTestId(testid), '{ArrowDown}');
const [handleKeys3] = result.current;
rerender(getMenuElement(ref, handleKeys3));
@ -58,9 +61,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
await user.type(screen.getByTestId(testid), '{ArrowUp}');
const [handleKeys4] = result.current;
rerender(getMenuElement(ref, handleKeys4));
@ -70,9 +71,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
await user.type(screen.getByTestId(testid), '{ArrowUp}');
const [handleKeys5] = result.current;
rerender(getMenuElement(ref, handleKeys5));
@ -82,9 +81,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
expect(screen.getByText('Item 4').tabIndex).toBe(0);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowUp' });
});
await user.type(screen.getByTestId(testid), '{ArrowUp}');
const [handleKeys6] = result.current;
rerender(getMenuElement(ref, handleKeys6));
@ -95,16 +92,14 @@ describe('useMenuFocus', () => {
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 close = jest.fn();
const { result } = renderHook(() => useMenuFocus({ localRef: ref, close }));
const [handleKeys] = result.current;
const { rerender } = render(getMenuElement(ref, handleKeys));
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
await user.type(screen.getByTestId(testid), '{ArrowDown}');
const [handleKeys2] = result.current;
rerender(getMenuElement(ref, handleKeys2));
@ -113,9 +108,7 @@ describe('useMenuFocus', () => {
expect(screen.getByText('Item 2').tabIndex).toBe(-1);
expect(screen.getByText('Item 3').tabIndex).toBe(-1);
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowLeft' });
});
await user.type(screen.getByTestId(testid), '{ArrowLeft}');
expect(close).toHaveBeenCalled();
expect(screen.getByText('Item 1').tabIndex).toBe(-1);
@ -123,7 +116,7 @@ describe('useMenuFocus', () => {
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 onOpen = jest.fn();
const onKeyDown = jest.fn();
@ -132,10 +125,7 @@ describe('useMenuFocus', () => {
render(getMenuElement(ref, handleKeys));
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Home' });
});
await user.type(screen.getByTestId(testid), '{ArrowDown}{Home}');
expect(onOpen).toHaveBeenCalled();
expect(onKeyDown).toHaveBeenCalledTimes(2);
@ -152,28 +142,24 @@ describe('useMenuFocus', () => {
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 onClick = jest.fn();
const { result } = renderHook(() => useMenuFocus({ localRef: ref }));
const [handleKeys] = result.current;
const { rerender } = render(getMenuElement(ref, handleKeys, undefined, onClick));
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'ArrowDown' });
});
await user.type(screen.getByTestId(testid), '{ArrowDown}');
const [handleKeys2] = result.current;
rerender(getMenuElement(ref, handleKeys2, undefined, onClick));
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Enter' });
});
await user.type(screen.getByTestId(testid), '{Enter}');
expect(onClick).toHaveBeenCalled();
});
it('calls onClose on Tab or Escape', () => {
it('calls onClose on Tab or Escape', async () => {
const ref = createRef<HTMLDivElement>();
const onClose = jest.fn();
const { result } = renderHook(() => useMenuFocus({ localRef: ref, onClose }));
@ -181,10 +167,7 @@ describe('useMenuFocus', () => {
render(getMenuElement(ref, handleKeys));
act(() => {
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Tab' });
fireEvent.keyDown(screen.getByTestId(testid), { key: 'Escape' });
});
await user.type(screen.getByTestId(testid), '{Tab}{Escape}');
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 { LoadingState } from '@grafana/data';
@ -16,7 +17,10 @@ const setup = (propOverrides?: Partial<PanelChromeProps>) => {
};
Object.assign(props, propOverrides);
return render(<PanelChrome {...props} />);
return {
...render(<PanelChrome {...props} />),
user: userEvent.setup(),
};
};
const setupWithToggleCollapsed = (propOverrides?: Partial<PanelChromeProps>) => {
@ -37,7 +41,10 @@ const setupWithToggleCollapsed = (propOverrides?: Partial<PanelChromeProps>) =>
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', () => {
@ -154,17 +161,17 @@ it('renders streaming indicator in the panel header if loadingState is streaming
expect(screen.getByTestId('panel-streaming')).toBeInTheDocument();
});
it('collapses the controlled panel when user clicks on the chevron or the title', () => {
setupWithToggleCollapsed({ title: 'Default title' });
it('collapses the controlled panel when user clicks on the chevron or the title', async () => {
const { user } = setupWithToggleCollapsed({ title: 'Default title' });
expect(screen.getByText("Panel's Content")).toBeInTheDocument();
const button = screen.getByRole('button', { name: 'Default title' });
const content = screen.getByTestId(selectors.components.Panels.Panel.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();
// 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);
});
it('collapses the uncontrolled panel when user clicks on the chevron or the title', () => {
setup({ title: 'Default title', collapsible: true });
it('collapses the uncontrolled panel when user clicks on the chevron or the title', async () => {
const { user } = setup({ title: 'Default title', collapsible: true });
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);
// 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();
// aria-controls should be removed when panel is collapsed
expect(button).not.toHaveAttribute('aria-controlls');

@ -13,7 +13,7 @@ describe('<SecretInput />', () => {
// Should show an enabled input
expect(input).toBeInTheDocument();
expect(input).not.toBeDisabled();
expect(input).toBeEnabled();
// Should not show a "Reset" button
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).not.toBeInTheDocument();
@ -30,7 +30,7 @@ describe('<SecretInput />', () => {
expect(input).toHaveValue(CONFIGURED_TEXT);
// 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 () => {
@ -40,7 +40,7 @@ describe('<SecretInput />', () => {
// Should show a reset button and a disabled input
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"
await userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT }));

@ -15,7 +15,7 @@ describe('<SecretTextArea />', () => {
// Should show an enabled input
expect(input).toBeInTheDocument();
expect(input).not.toBeDisabled();
expect(input).toBeEnabled();
// Should not show a "Reset" button
expect(screen.queryByRole('button', { name: RESET_BUTTON_TEXT })).not.toBeInTheDocument();
@ -34,7 +34,7 @@ describe('<SecretTextArea />', () => {
expect(textArea).toHaveValue(CONFIGURED_TEXT);
// 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 () => {
@ -44,7 +44,7 @@ describe('<SecretTextArea />', () => {
// Should show a reset button and a disabled input
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"
await userEvent.click(screen.getByRole('button', { name: RESET_BUTTON_TEXT }));

@ -26,7 +26,7 @@ describe('SelectBase', () => {
];
it('renders without error', () => {
render(<SelectBase onChange={onChangeHandler} />);
expect(() => render(<SelectBase onChange={onChangeHandler} />)).not.toThrow();
});
it('renders empty options information', async () => {
@ -60,7 +60,7 @@ describe('SelectBase', () => {
};
render(<Test />);
expect(screen.queryByText('Test label')).toBeInTheDocument();
expect(screen.getByText('Test label')).toBeInTheDocument();
await userEvent.click(screen.getByText('clear value'));
expect(screen.queryByText('Test label')).not.toBeInTheDocument();
});
@ -76,13 +76,15 @@ describe('SelectBase', () => {
describe('is not provided', () => {
it.each`
key
${'ArrowDown'}
${'ArrowUp'}
${'{ArrowDown}'}
${'{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} />);
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();
});
});
@ -293,7 +295,7 @@ describe('SelectBase', () => {
);
await userEvent.click(screen.getByText(/Option 1/i));
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 () => {
@ -308,7 +310,7 @@ describe('SelectBase', () => {
);
await userEvent.click(screen.getByText(/Option 1/i));
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
await userEvent.click(toggleAllOptions);
@ -327,7 +329,7 @@ describe('SelectBase', () => {
);
await userEvent.click(screen.getByText(/Option 1/i));
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
await userEvent.click(toggleAllOptions);
@ -346,7 +348,7 @@ describe('SelectBase', () => {
);
await userEvent.click(screen.getByText(/Choose/i));
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
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(screen.getByLabelText('1'));
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 () => {
@ -338,7 +338,7 @@ describe('Table', () => {
});
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(screen.getByLabelText('2'));
@ -347,7 +347,7 @@ describe('Table', () => {
//4 + header row
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 () => {
@ -376,14 +376,14 @@ describe('Table', () => {
//3 + header row
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(screen.getByText('Clear filter'));
//5 + header row
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
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 onCellFilterAdded = jest.fn();
@ -450,7 +450,7 @@ describe('Table', () => {
//4 + header row
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 () => {
@ -523,10 +523,8 @@ describe('Table', () => {
}),
});
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0].textContent).toEqual(
'Count'
);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('Count');
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1]).toHaveTextContent('5');
});
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(
'Count'
);
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1].textContent).toEqual('5');
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[0]).toHaveTextContent('Count');
expect(within(getFooter()).getByRole('columnheader').getElementsByTagName('span')[1]).toHaveTextContent('5');
const onSortByChange = jest.fn();
const onCellFilterAdded = jest.fn();
@ -590,7 +586,7 @@ describe('Table', () => {
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');
expect(tables).toHaveLength(3);
let subTable = screen.getAllByRole('table')[2];
expect(subTable.style.height).toBe('108px');
expect(subTable).toHaveStyle({ height: '108px' });
// Sort again rows
tables = screen.getAllByRole('table');
@ -732,7 +728,7 @@ describe('Table', () => {
rows = within(getTable()).getAllByRole('row');
await userEvent.click(within(rows[1]).getByLabelText('Expand row'));
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 { TableCellDisplayMode } from '@grafana/schema';
@ -288,6 +289,40 @@ const createTimeDataFrame = (): DataFrame => {
};
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', () => {
it('renders a simple table with columns and rows', () => {
const { container } = render(
@ -332,7 +367,7 @@ describe('TableNG', () => {
expect(expandIcons.length).toBeGreaterThan(0);
});
it('expands nested data when clicking expand button', () => {
it('expands nested data when clicking expand button', async () => {
// Mock scrollIntoView
window.HTMLElement.prototype.scrollIntoView = jest.fn();
@ -356,7 +391,7 @@ describe('TableNG', () => {
// Click the expand button
if (expandButton) {
fireEvent.click(expandButton);
await user.click(expandButton);
// After expansion, we should have more rows
const expandedRows = container.querySelectorAll('[role="row"]');
@ -581,7 +616,7 @@ describe('TableNG', () => {
if (nextButton) {
// Click to go to the next page
fireEvent.click(nextButton);
await user.click(nextButton);
// Get all cell content on the second page
const newCells = container.querySelectorAll('[role="gridcell"]');
@ -597,7 +632,7 @@ describe('TableNG', () => {
// 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
expect(container.textContent).toMatch(/\d+ - \d+ of 100 rows/);
expect(container).toHaveTextContent(/\d+ - \d+ of 100 rows/);
// Verify that the pagination summary has changed
const paginationSummary = container.querySelector('.paginationSummary, [class*="paginationSummary"]');
@ -606,7 +641,7 @@ describe('TableNG', () => {
expect(summaryText).toContain('of 100 rows');
} else {
// 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;
// Click the sort button
fireEvent.click(sortButton);
await user.click(sortButton);
// After clicking, the header should have an aria-sort attribute
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
window.HTMLElement.prototype.scrollIntoView = jest.fn();
@ -682,23 +717,23 @@ describe('TableNG', () => {
const sortButton = columnHeader.querySelector('button') || columnHeader;
// Initial state - no sort
expect(columnHeader.getAttribute('aria-sort')).toBeNull();
expect(columnHeader).not.toHaveAttribute('aria-sort');
// First click - should sort ascending
fireEvent.click(sortButton);
expect(columnHeader.getAttribute('aria-sort')).toBe('ascending');
await user.click(sortButton);
expect(columnHeader).toHaveAttribute('aria-sort', 'ascending');
// Second click - should sort descending
fireEvent.click(sortButton);
expect(columnHeader.getAttribute('aria-sort')).toBe('descending');
await user.click(sortButton);
expect(columnHeader).toHaveAttribute('aria-sort', 'descending');
// Third click - should remove sort
fireEvent.click(sortButton);
expect(columnHeader.getAttribute('aria-sort')).toBeNull();
await user.click(sortButton);
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
window.HTMLElement.prototype.scrollIntoView = jest.fn();
@ -761,7 +796,7 @@ describe('TableNG', () => {
const valueColumnButton = columnHeaders[1].querySelector('button') || columnHeaders[1];
// 1. First sort by Category (ascending)
fireEvent.click(categoryColumnButton);
await user.click(categoryColumnButton);
// Check data is sorted by Category
const categoryOnlySortedRows = getCellTextContent();
@ -796,7 +831,8 @@ describe('TableNG', () => {
expect(categoryBValues).toContain('4');
// 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
const multiSortedRows = getCellTextContent();
@ -830,7 +866,8 @@ describe('TableNG', () => {
expect(multiSortedRows[4][2]).toBe('Alice');
// 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)
const multiSortedRowsDesc = getCellTextContent();
@ -864,7 +901,8 @@ describe('TableNG', () => {
expect(multiSortedRowsDesc[4][2]).toBe('Jane');
// 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
const singleSortRows = getCellTextContent();
@ -879,7 +917,7 @@ describe('TableNG', () => {
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
const mixedDataFrame = toDataFrame({
name: 'MixedData',
@ -918,7 +956,7 @@ describe('TableNG', () => {
// Test string column sorting
const stringColumnButton = columnHeaders[0].querySelector('button') || columnHeaders[0];
fireEvent.click(stringColumnButton);
await user.click(stringColumnButton);
// Get cell values after sorting
let cells = container.querySelectorAll('[role="gridcell"]');
@ -930,7 +968,7 @@ describe('TableNG', () => {
// Test number column sorting
const numberColumnButton = columnHeaders[1].querySelector('button') || columnHeaders[1];
fireEvent.click(numberColumnButton);
await user.click(numberColumnButton);
// Get cell values after sorting
cells = container.querySelectorAll('[role="gridcell"]');
@ -1138,11 +1176,16 @@ describe('TableNG', () => {
// Find resize handle
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) {
// Simulate resize by triggering mousedown, mousemove, mouseup
/* eslint-disable testing-library/prefer-user-event */
fireEvent.mouseDown(resizeHandles[0]);
fireEvent.mouseMove(resizeHandles[0], { clientX: 250 });
fireEvent.mouseUp(resizeHandles[0]);
/* eslint-enable testing-library/prefer-user-event */
// Check that onColumnResize was called
expect(onColumnResize).toHaveBeenCalled();
@ -1202,28 +1245,6 @@ describe('TableNG', () => {
});
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 () => {
const { container } = render(
<TableNG enableVirtualization={false} data={createBasicDataFrame()} width={400} height={400} />
@ -1248,7 +1269,7 @@ describe('TableNG', () => {
});
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 = {
...createBasicDataFrame(),
fields: createBasicDataFrame().fields.map((field) => ({
@ -1285,7 +1306,7 @@ describe('TableNG', () => {
if (cellContent) {
// Trigger mouse enter on the cell content
fireEvent.mouseEnter(cellContent);
await user.hover(cellContent);
// Look for the inspect icon
const inspectIcon = container.querySelector('[aria-label="Inspect value"]');
@ -1491,13 +1512,12 @@ describe('TableNG', () => {
const dataGrid = screen.getByRole('grid');
// Simulate scrolling
act(() => {
fireEvent.scroll(dataGrid, {
target: {
scrollLeft: 100,
scrollTop: 50,
},
});
fireEvent.scroll(dataGrid, {
target: {
scrollLeft: 100,
scrollTop: 50,
},
});
// 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 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', () => {
const props = createMockProps(1, false, 0);
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', () => {

@ -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';
describe('TagsInput', () => {
it('removes tag when clicking on remove button', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
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']);
});
it('does NOT remove tag when clicking on remove button when disabled', async () => {
const user = userEvent.setup();
const onChange = jest.fn();
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();
});

@ -75,7 +75,7 @@ describe('Toggletip', () => {
const button = screen.getByTestId('myButton');
await userEvent.click(button);
expect(await screen.queryByTestId('toggletip-content')).not.toBeInTheDocument();
expect(screen.queryByTestId('toggletip-content')).not.toBeInTheDocument();
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 uPlot from 'uplot';
@ -115,11 +115,11 @@ describe('UPlotChart', () => {
describe('config update', () => {
it('skips uPlot intialization for width and height equal 0', async () => {
const { data, config } = mockData();
const { queryAllByTestId } = render(
render(
<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();
});

Loading…
Cancel
Save