diff --git a/eslint.config.js b/eslint.config.js index 285ca1abb30..2860db1a2c7 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -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', }, }, { diff --git a/packages/grafana-ui/src/components/BigValue/BigValue.test.tsx b/packages/grafana-ui/src/components/BigValue/BigValue.test.tsx index e7819cd110d..d69bc2d2635 100644 --- a/packages/grafana-ui/src/components/BigValue/BigValue.test.tsx +++ b/packages/grafana-ui/src/components/BigValue/BigValue.test.tsx @@ -46,7 +46,7 @@ describe('BigValue', () => { it('should render without percent change', () => { render(); - expect(screen.queryByText('%')).toBeNull(); + expect(screen.queryByText('%')).not.toBeInTheDocument(); }); }); }); diff --git a/packages/grafana-ui/src/components/Card/Card.test.tsx b/packages/grafana-ui/src/components/Card/Card.test.tsx index 86e81c784aa..a602ead887f 100644 --- a/packages/grafana-ui/src/components/Card/Card.test.tsx +++ b/packages/grafana-ui/src/components/Card/Card.test.tsx @@ -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( Test Heading ); - 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', () => { ); - 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( @@ -78,8 +80,8 @@ describe('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', () => { ); - 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(); }); diff --git a/packages/grafana-ui/src/components/Carousel/Carousel.test.tsx b/packages/grafana-ui/src/components/Carousel/Carousel.test.tsx index 0966fd029e9..1fba66ce5b3 100644 --- a/packages/grafana-ui/src/components/Carousel/Carousel.test.tsx +++ b/packages/grafana-ui/src/components/Carousel/Carousel.test.tsx @@ -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(); }); diff --git a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx index 9f13e67918b..7a792920723 100644 --- a/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/ColorPickerInput.test.tsx @@ -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 () => { diff --git a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx index 091e6fed227..537e26df1c9 100644 --- a/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/Combobox.test.tsx @@ -30,6 +30,12 @@ const numericOptions: Array> = [ ]; describe('Combobox', () => { + let user: ReturnType; + + 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(); - 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(); @@ -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; diff --git a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx index 5be7bc09098..f92371b6e3b 100644 --- a/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx +++ b/packages/grafana-ui/src/components/Combobox/MultiCombobox.test.tsx @@ -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(); }); diff --git a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx index 98b0707a9c7..cd6f8596d0e 100644 --- a/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx +++ b/packages/grafana-ui/src/components/ConfirmButton/ConfirmButton.test.tsx @@ -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 () => { diff --git a/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx index a667bc2ee80..18fc8d5deb3 100644 --- a/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx +++ b/packages/grafana-ui/src/components/ConfirmModal/ConfirmModal.test.tsx @@ -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(() => { diff --git a/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.test.tsx b/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.test.tsx index 4ceb92b0bbe..7dfa33b231c 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.test.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/AlertingSettings.test.tsx @@ -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', () => { diff --git a/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.test.tsx b/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.test.tsx index 007d74cbce6..828cf481ef7 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.test.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/CustomHeadersSettings.test.tsx @@ -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 () => { diff --git a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.test.tsx b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.test.tsx index bc73c393965..3b399650916 100644 --- a/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.test.tsx +++ b/packages/grafana-ui/src/components/DataSourceSettings/DataSourceHttpSettings.test.tsx @@ -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', () => { diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.test.tsx index b0aeabb0e2e..7b46b665442 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePicker/DatePicker.test.tsx @@ -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(); 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(); expect(onClose).not.toHaveBeenCalled(); - fireEvent.click(document); + await user.click(document.body); expect(onClose).toHaveBeenCalledTimes(1); }); diff --git a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.test.tsx index 11c27cff02e..4dc244b9991 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DatePickerWithInput/DatePickerWithInput.test.tsx @@ -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; + + beforeEach(() => { + user = userEvent.setup({ applyAccept: false }); + }); + it('renders date input', () => { render(); @@ -24,43 +31,42 @@ describe('DatePickerWithInput', () => { }); describe('input is clicked', () => { - it('renders input', () => { + it('renders input', async () => { render(); - - fireEvent.click(screen.getByPlaceholderText('Date')); + await user.click(screen.getByPlaceholderText('Date')); expect(screen.getByPlaceholderText('Date')).toBeInTheDocument(); }); - it('renders calendar', () => { + it('renders calendar', async () => { render(); - 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(); // 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(); // 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(); }); diff --git a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx index ca99959c7d0..ad5e60ce13a 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/DateTimePicker/DateTimePicker.test.tsx @@ -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) => { diff --git a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.test.tsx index 9d6a45c6f29..d88d4789ed4 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/RelativeTimeRangePicker/RelativeTimeRangePicker.test.tsx @@ -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 }); }); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeContext.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeContext.test.tsx index 4ef66005aa7..e10502e24c1 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangeContext.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangeContext.test.tsx @@ -86,7 +86,7 @@ describe('TimeRangeProvider', () => { context2 = val; } - const renderContext = render( + const { rerender } = render( @@ -104,7 +104,7 @@ describe('TimeRangeProvider', () => { syncedValue: timeRange, }); - renderContext.rerender( + rerender( diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx index 8fa3cb66231..0b2284e875f 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker.test.tsx @@ -20,7 +20,7 @@ const value: TimeRange = { describe('TimePicker', () => { it('renders buttons correctly', () => { - const container = render( + render( {}} 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(
{}} @@ -107,7 +107,7 @@ it('does not submit wrapping forms', async () => { ); - const clicks = container.getAllByRole('button').map((button) => userEvent.click(button)); + const clicks = screen.getAllByRole('button').map((button) => userEvent.click(button)); await Promise.all(clicks); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx index 18fa0cf2f24..1ff6e8dee1f 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimePickerContent.test.tsx @@ -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', () => { diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx index 90777bfa3aa..7a130f08950 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/TimeRangeContent.test.tsx @@ -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( - {}} timeZone={timeZone} /> - ); - +function setup(initial: TimeRange = defaultTimeRange, timeZone = 'utc') { return { - ...result, + ...render( {}} 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; + 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 }) ); diff --git a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/hooks.test.tsx b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/hooks.test.tsx index 38fe7529625..98d9eb0588a 100644 --- a/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/hooks.test.tsx +++ b/packages/grafana-ui/src/components/DateTimePickers/TimeRangePicker/hooks.test.tsx @@ -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; + + beforeEach(() => { + user = userEvent.setup(); + }); + const testid = 'test'; const getListElement = ( ref: RefObject, @@ -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(); 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(); 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(); }); diff --git a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx index d14867aef54..edcec107da7 100644 --- a/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx +++ b/packages/grafana-ui/src/components/ErrorBoundary/ErrorBoundary.test.tsx @@ -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( diff --git a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx index 23d965f6f10..450afdbe1bf 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileDropzone.test.tsx @@ -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(); 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 () => { diff --git a/packages/grafana-ui/src/components/FileDropzone/FileListItem.test.tsx b/packages/grafana-ui/src/components/FileDropzone/FileListItem.test.tsx index 587756b25b9..0de14953fea 100644 --- a/packages/grafana-ui/src/components/FileDropzone/FileListItem.test.tsx +++ b/packages/grafana-ui/src/components/FileDropzone/FileListItem.test.tsx @@ -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; + + beforeEach(() => { + user = userEvent.setup({ applyAccept: false }); + }); + it('should show an error message when error prop is not null', () => { render(); @@ -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(); - 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(); - 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(); - fireEvent.click(screen.getByRole('button', { name: REMOVE_FILE })); + await user.click(screen.getByRole('button', { name: REMOVE_FILE })); expect(removeFile).toHaveBeenCalledWith(customFile); }); diff --git a/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx b/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx index 8b76e4ab435..981f8e00bac 100644 --- a/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx +++ b/packages/grafana-ui/src/components/FileUpload/FileUpload.test.tsx @@ -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; + + beforeEach(() => { + user = userEvent.setup({ applyAccept: false }); + }); + it('should render upload button with default text and no file name', () => { render( {}} />); 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( {}} />); + render( {}} />); 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(); - let uploader = getByTestId(selectors.components.FileUpload.inputField); - await waitFor(() => - fireEvent.change(uploader, { - target: { files: [file] }, - }) - ); - let uploaderLabel = getByTestId(selectors.components.FileUpload.fileNameSpan); + render(); + 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(); - let uploader = getByTestId(selectors.components.FileUpload.inputField); - await waitFor(() => - fireEvent.change(uploader, { - target: { files: [file] }, - }) - ); - let uploaderLabel = getByTestId(selectors.components.FileUpload.fileNameSpan); + render(); + 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'); }); }); diff --git a/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.test.tsx b/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.test.tsx index dbd4ca81323..d1ba43f4a81 100644 --- a/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.test.tsx +++ b/packages/grafana-ui/src/components/Forms/Legacy/Input/Input.test.tsx @@ -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 () => { diff --git a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx index 6526c22a0cd..651532409af 100644 --- a/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx +++ b/packages/grafana-ui/src/components/InteractiveTable/InteractiveTable.test.tsx @@ -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(); expect(screen.queryByRole('button', { name: /next/i })).not.toBeInTheDocument(); diff --git a/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.test.tsx b/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.test.tsx index 67abc701345..7646f67735c 100644 --- a/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.test.tsx +++ b/packages/grafana-ui/src/components/MatchersUI/FieldsByFrameRefIdMatcher.test.tsx @@ -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) => { const editorProps = { ...props, ...testProps }; - return render(); + return { + ...render(), + user: userEvent.setup(), + }; }; const multiSetup = (testProps?: Partial) => { @@ -57,10 +61,10 @@ const multiSetup = (testProps?: Partial) => { 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 */ }); }); diff --git a/packages/grafana-ui/src/components/Menu/MenuItem.test.tsx b/packages/grafana-ui/src/components/Menu/MenuItem.test.tsx index 1011634810e..569b4f1dfc9 100644 --- a/packages/grafana-ui/src/components/Menu/MenuItem.test.tsx +++ b/packages/grafana-ui/src/components/Menu/MenuItem.test.tsx @@ -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; + + beforeEach(() => { + user = userEvent.setup(); + }); + const getMenuItem = (props?: Partial) => ( ); @@ -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(); }); diff --git a/packages/grafana-ui/src/components/Menu/hooks.test.tsx b/packages/grafana-ui/src/components/Menu/hooks.test.tsx index 9ad532b14ca..7b7e161194f 100644 --- a/packages/grafana-ui/src/components/Menu/hooks.test.tsx +++ b/packages/grafana-ui/src/components/Menu/hooks.test.tsx @@ -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; + + beforeEach(() => { + user = userEvent.setup(); + }); + const testid = 'test'; const getMenuElement = ( ref: RefObject, @@ -23,7 +30,7 @@ describe('useMenuFocus', () => { ); - it('sets correct focused item on keydown', () => { + it('sets correct focused item on keydown', async () => { const ref = createRef(); 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(); 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(); 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(); 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(); 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); }); diff --git a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx index a13c38567b0..3032099a069 100644 --- a/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx +++ b/packages/grafana-ui/src/components/PanelChrome/PanelChrome.test.tsx @@ -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) => { }; Object.assign(props, propOverrides); - return render(); + return { + ...render(), + user: userEvent.setup(), + }; }; const setupWithToggleCollapsed = (propOverrides?: Partial) => { @@ -37,7 +41,10 @@ const setupWithToggleCollapsed = (propOverrides?: Partial) => return ; }; - return render(); + return { + ...render(), + 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'); diff --git a/packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx b/packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx index aeb9adf4cef..1d59862fd1d 100644 --- a/packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx +++ b/packages/grafana-ui/src/components/SecretInput/SecretInput.test.tsx @@ -13,7 +13,7 @@ describe('', () => { // 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('', () => { 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('', () => { // 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 })); diff --git a/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.test.tsx b/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.test.tsx index f2543a5002c..ac2902c6d85 100644 --- a/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.test.tsx +++ b/packages/grafana-ui/src/components/SecretTextArea/SecretTextArea.test.tsx @@ -15,7 +15,7 @@ describe('', () => { // 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('', () => { 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('', () => { // 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 })); diff --git a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx index 3c0dc9446a2..1279d2fd896 100644 --- a/packages/grafana-ui/src/components/Select/SelectBase.test.tsx +++ b/packages/grafana-ui/src/components/Select/SelectBase.test.tsx @@ -26,7 +26,7 @@ describe('SelectBase', () => { ]; it('renders without error', () => { - render(); + expect(() => render()).not.toThrow(); }); it('renders empty options information', async () => { @@ -60,7 +60,7 @@ describe('SelectBase', () => { }; render(); - 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(); - 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); diff --git a/packages/grafana-ui/src/components/Table/Table.test.tsx b/packages/grafana-ui/src/components/Table/Table.test.tsx index f8078e9b27a..8983273534c 100644 --- a/packages/grafana-ui/src/components/Table/Table.test.tsx +++ b/packages/grafana-ui/src/components/Table/Table.test.tsx @@ -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(); - 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' }); }); }); diff --git a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx index 7cebf48d1ad..5108f104c71 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx +++ b/packages/grafana-ui/src/components/Table/TableNG/TableNG.test.tsx @@ -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; + 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( @@ -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 diff --git a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts index 46b47a27929..8e6d662f19f 100644 --- a/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts +++ b/packages/grafana-ui/src/components/Table/TableNG/utils.test.ts @@ -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', () => { diff --git a/packages/grafana-ui/src/components/TagsInput/TagsInput.test.tsx b/packages/grafana-ui/src/components/TagsInput/TagsInput.test.tsx index 2cd8a0f7b3a..7721897de31 100644 --- a/packages/grafana-ui/src/components/TagsInput/TagsInput.test.tsx +++ b/packages/grafana-ui/src/components/TagsInput/TagsInput.test.tsx @@ -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(); - 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(); - 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(); }); diff --git a/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx b/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx index 347f2273624..5d503426735 100644 --- a/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx +++ b/packages/grafana-ui/src/components/Toggletip/Toggletip.test.tsx @@ -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); }); diff --git a/packages/grafana-ui/src/components/uPlot/Plot.test.tsx b/packages/grafana-ui/src/components/uPlot/Plot.test.tsx index 045b27a576d..8669061afaf 100644 --- a/packages/grafana-ui/src/components/uPlot/Plot.test.tsx +++ b/packages/grafana-ui/src/components/uPlot/Plot.test.tsx @@ -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( ); - expect(queryAllByTestId('uplot-main-div')).toHaveLength(1); + expect(screen.queryAllByTestId('uplot-main-div')).toHaveLength(1); expect(uPlot).not.toBeCalled(); });