From 56d67f9ffc452ccb7f194f577e3f54d2c4a3763b Mon Sep 17 00:00:00 2001 From: Collin Fingar Date: Tue, 13 May 2025 11:11:29 -0400 Subject: [PATCH] GrafanaUI: Added Unit Tests to Components (#105302) * GrafanaUI: Added Unit Tests to Components * Updates per feedback / lint fix --- .../TabbedContainer/TabbedContainer.test.tsx | 101 ++++++++++++++++++ .../src/components/Tags/Tag.test.tsx | 50 +++++++++ .../src/components/Tags/TagList.test.tsx | 55 ++++++++++ .../ValuePicker/ValuePicker.test.tsx | 54 ++++++++++ 4 files changed, 260 insertions(+) create mode 100644 packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.test.tsx create mode 100644 packages/grafana-ui/src/components/Tags/Tag.test.tsx create mode 100644 packages/grafana-ui/src/components/Tags/TagList.test.tsx create mode 100644 packages/grafana-ui/src/components/ValuePicker/ValuePicker.test.tsx diff --git a/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.test.tsx b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.test.tsx new file mode 100644 index 00000000000..c28e2b37a39 --- /dev/null +++ b/packages/grafana-ui/src/components/TabbedContainer/TabbedContainer.test.tsx @@ -0,0 +1,101 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { IconName } from '@grafana/data'; + +import { TabbedContainer } from './TabbedContainer'; + +const icon: IconName = 'info-circle'; +const icon2: IconName = 'user'; +const icon3: IconName = 'cog'; + +const mockTabs = [ + { + label: 'Tab 1', + value: 'tab1', + content:
Tab 1 Content
, + icon: icon, + }, + { + label: 'Tab 2', + value: 'tab2', + content:
Tab 2 Content
, + icon: icon2, + }, + { + label: 'Tab 3', + value: 'tab3', + content:
Tab 3 Content
, + icon: icon3, + }, +]; + +const mockOnClose = jest.fn(); +const defaultProps = { + tabs: mockTabs, + onClose: mockOnClose, +}; + +const setup = (jsx: JSX.Element) => { + return { + user: userEvent.setup(), + ...render(jsx), + }; +}; + +describe('TabbedContainer', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render all tabs with their labels and icons', () => { + render(); + + mockTabs.forEach((tab) => { + expect(screen.getByText(tab.label)).toBeInTheDocument(); + expect(screen.getByText(tab.label).closest('button')?.querySelector('svg')).toBeInTheDocument(); + }); + }); + + it('should render the first tab content by default', () => { + render(); + + expect(screen.getByTestId('tab1-content')).toBeInTheDocument(); + expect(screen.queryByTestId('tab2-content')).not.toBeInTheDocument(); + expect(screen.queryByTestId('tab3-content')).not.toBeInTheDocument(); + }); + + it('should render the default tab content when specified', () => { + render(); + + expect(screen.queryByTestId('tab1-content')).not.toBeInTheDocument(); + expect(screen.getByTestId('tab2-content')).toBeInTheDocument(); + expect(screen.queryByTestId('tab3-content')).not.toBeInTheDocument(); + }); + + it('should switch content when clicking on different tabs', async () => { + const { user } = setup(); + + expect(screen.getByTestId('tab1-content')).toBeInTheDocument(); + + await user.click(screen.getByText('Tab 2')); + + expect(screen.queryByTestId('tab1-content')).not.toBeInTheDocument(); + expect(screen.getByTestId('tab2-content')).toBeInTheDocument(); + + await user.click(screen.getByText('Tab 3')); + + expect(screen.queryByTestId('tab1-content')).not.toBeInTheDocument(); + expect(screen.queryByTestId('tab2-content')).not.toBeInTheDocument(); + expect(screen.getByTestId('tab3-content')).toBeInTheDocument(); + }); + + it('should call onClose when close button is clicked', async () => { + const { user } = setup(); + + const closeButton = screen.getByRole('button', { name: /close/i }); + await user.click(closeButton); + + expect(mockOnClose).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/grafana-ui/src/components/Tags/Tag.test.tsx b/packages/grafana-ui/src/components/Tags/Tag.test.tsx new file mode 100644 index 00000000000..48fce39b162 --- /dev/null +++ b/packages/grafana-ui/src/components/Tags/Tag.test.tsx @@ -0,0 +1,50 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { Tag } from './Tag'; + +const setup = (jsx: JSX.Element) => { + return { + user: userEvent.setup(), + ...render(jsx), + }; +}; + +const mockOnClick = jest.fn(); + +describe('Tag', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should render with icon when provided', () => { + render(); + + const tag = screen.getByText('test-tag'); + expect(tag).toBeInTheDocument(); + expect(tag.parentElement?.querySelector('svg')).toBeInTheDocument(); + }); + + it('should handle click events', async () => { + const { user } = setup(); + + const tag = screen.getByText('test-tag'); + await user.click(tag); + + expect(mockOnClick).toHaveBeenCalledWith('test-tag', expect.any(Object)); + }); + + it('should render as button when onClick is provided', () => { + render(); + + const tag = screen.getByRole('button', { name: 'test-tag' }); + expect(tag).toBeInTheDocument(); + }); + + it('should not render as button when onClick is not provided', () => { + render(); + + const tag = screen.queryByRole('button', { name: 'test-tag' }); + expect(tag).not.toBeInTheDocument(); + }); +}); diff --git a/packages/grafana-ui/src/components/Tags/TagList.test.tsx b/packages/grafana-ui/src/components/Tags/TagList.test.tsx new file mode 100644 index 00000000000..7c0d8a0314b --- /dev/null +++ b/packages/grafana-ui/src/components/Tags/TagList.test.tsx @@ -0,0 +1,55 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { TagList } from './TagList'; + +const setup = (jsx: JSX.Element) => { + return { + user: userEvent.setup(), + ...render(jsx), + }; +}; + +const mockTags = ['tag1', 'tag2', 'tag3', 'tag4', 'tag5']; +const mockOnClick = jest.fn(); + +describe('TagList', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + it('should render all tags when displayMax is not provided', () => { + render(); + + mockTags.forEach((tag) => { + expect(screen.getByText(tag)).toBeInTheDocument(); + }); + }); + + it('should limit displayed tags when displayMax is provided', () => { + const displayMax = 3; + render(); + + for (let i = 0; i < displayMax; i++) { + expect(screen.getByText(mockTags[i])).toBeInTheDocument(); + } + + expect(screen.getByText(`+ ${mockTags.length - displayMax}`)).toBeInTheDocument(); + }); + + it('should handle tag clicks correctly', async () => { + const { user } = setup(); + + const firstTag = screen.getByText(mockTags[0]); + await user.click(firstTag); + + expect(mockOnClick).toHaveBeenCalledWith(mockTags[0], expect.any(Object)); + }); + + it('should apply custom aria labels when provided', () => { + const getAriaLabel = (name: string, index: number) => `Custom label for ${name} at ${index}`; + render(); + + const firstTag = screen.getByLabelText('Custom label for tag1 at 0'); + expect(firstTag).toBeInTheDocument(); + }); +}); diff --git a/packages/grafana-ui/src/components/ValuePicker/ValuePicker.test.tsx b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.test.tsx new file mode 100644 index 00000000000..e5cf2ccce1d --- /dev/null +++ b/packages/grafana-ui/src/components/ValuePicker/ValuePicker.test.tsx @@ -0,0 +1,54 @@ +import { render, screen } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; + +import { ValuePicker } from './ValuePicker'; + +const mockOptions = [ + { label: 'Option 1', value: 'opt1' }, + { label: 'Option 2', value: 'opt2' }, + { label: 'Option 3', value: 'opt3' }, +]; + +const mockOnChange = jest.fn(); +const defaultProps = { + label: 'Add Option', + options: mockOptions, + onChange: mockOnChange, +}; + +const setup = (jsx: JSX.Element) => { + return { + user: userEvent.setup(), + ...render(jsx), + }; +}; + +describe('ValuePicker', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should switch to select view when button is clicked', async () => { + const { user } = setup(); + + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); + + await user.click(screen.getByRole('button')); + + expect(screen.queryByRole('button')).not.toBeInTheDocument(); + expect(screen.getByRole('combobox')).toBeInTheDocument(); + }); + + it('should call onChange and switch back to button view when option is selected', async () => { + const { user } = setup(); + + await user.click(screen.getByRole('button')); + await user.keyboard('{ArrowDown}{Enter}'); + + expect(mockOnChange).toHaveBeenCalledWith(mockOptions[0]); + + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.queryByRole('combobox')).not.toBeInTheDocument(); + }); +});