From 37b22945df8005869885408d8783d24bd41ac0ba Mon Sep 17 00:00:00 2001 From: lsprr <16653744+lsprr@users.noreply.github.com> Date: Thu, 29 Feb 2024 19:27:17 -0500 Subject: [PATCH 1/3] test: switch table tests to react testing library --- .../__tests__/src/components/Table/index.js | 278 ---------------- .../react/src/components/Table/index.test.tsx | 314 ++++++++++++++++++ 2 files changed, 314 insertions(+), 278 deletions(-) delete mode 100644 packages/react/__tests__/src/components/Table/index.js create mode 100644 packages/react/src/components/Table/index.test.tsx diff --git a/packages/react/__tests__/src/components/Table/index.js b/packages/react/__tests__/src/components/Table/index.js deleted file mode 100644 index 233cf0a85..000000000 --- a/packages/react/__tests__/src/components/Table/index.js +++ /dev/null @@ -1,278 +0,0 @@ -import React from 'react'; -import { shallow, mount } from 'enzyme'; -import { spy } from 'sinon'; -import Table, { - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, - TableFooter -} from '../../../../src/components/Table'; -import axe from '../../../axe'; - -const renderDefaultTable = () => - mount( - - - - - A - - - B - - - - - - - 1 - - 2 - - - - - - foo - - bar - - -
- ); - -describe('Table components', () => { - test('render children', () => { - const table = renderDefaultTable(); - const tableHead = table.find('TableHead'); - const tableRow = table.find('TableRow').at(0); - const tableHeader = table.find('TableHeader').at(0); - const tableBody = table.find('TableBody'); - const tableCell = table.find('TableCell').at(0); - const tableFooter = table.find('TableFooter').at(0); - - const tableItems = [ - table, - tableHead, - tableRow, - tableHeader, - tableBody, - tableCell, - tableFooter - ]; - - tableItems.forEach(wrapper => { - expect(!!wrapper.children().length).toBe(true); - }); - }); - - test('passes classNames through', () => { - const table = renderDefaultTable(); - const tableHead = table.find('TableHead'); - const tableRow = table.find('TableRow').at(0); - const tableHeader = table.find('TableHeader').at(0); - const tableBody = table.find('TableBody'); - const tableCell = table.find('TableCell').at(0); - const tableFooter = table.find('TableFooter').at(0); - - expect(table.is('.my-table')).toBe(true); - expect(tableHead.is('.my-table-head')).toBe(true); - expect(tableRow.is('.my-table-row')).toBe(true); - expect(tableHeader.is('.my-table-header')).toBe(true); - expect(tableBody.is('.my-table-body')).toBe(true); - expect(tableCell.is('.my-table-cell')).toBe(true); - expect(tableFooter.is('.my-table-footer')).toBe(true); - }); - - test('passes arbitrary props through', () => { - const table = renderDefaultTable(); - const tableHead = table.find('TableHead'); - const tableRow = table.find('TableRow').at(0); - const tableHeader = table.find('TableHeader').at(0); - const tableBody = table.find('TableBody'); - const tableCell = table.find('TableCell').at(0); - const tableFooter = table.find('TableFooter').at(0); - - const tableItems = [ - table, - tableHead, - tableRow, - tableHeader, - tableBody, - tableCell, - tableFooter - ]; - - tableItems.forEach(wrapper => { - expect(wrapper.is('[data-foo="true"]')).toBe(true); - }); - - expect(tableHeader.is('[scope="col"]')).toBe(true); - }); - - test('renders the expected semantic HTML elements', () => { - const table = shallow(a
); - const body = shallow(a); - const cell = shallow(a); - const head = shallow(a); - const header = shallow(a); - const row = shallow(a); - const footer = shallow(a); - - expect(table.is('table')).toBe(true); - expect(body.is('tbody')).toBe(true); - expect(cell.is('td')).toBe(true); - expect(head.is('thead')).toBe(true); - expect(header.is('th')).toBe(true); - expect(footer.is('tfoot')).toBe(true); - expect(row.is('tr')).toBe(true); - }); - - test('renders border variant', () => { - const wrapper = mount( - - - - Header - - - - - Cell - - -
- ); - - expect(wrapper.find('.Table--border').exists()).toBe(true); - }); - - describe('Sortable Table', () => { - test('renders sort button and icons when passing in sortDirection and onSort in TableHeader', () => { - const wrapper = mount( - - - - null}> - Sortable Header - - - -
- ); - - expect(wrapper.find('button').exists()).toBe(true); - expect(wrapper.find('.Icon--sort-triangle').exists()).toBe(true); - expect(wrapper.find('Offscreen').text()).toBe(''); - }); - - test('render className TableHeader--sorting when a TableHeader is actively sorting', () => { - const wrapper = mount( - - - - null}> - Sortable Header - - - -
- ); - - expect(wrapper.find('.TableHeader--sort-ascending').exists()).toBe(true); - }); - - test('renders triangle up Icon and ascending message when sortDirection is ascending', () => { - const wrapper = mount( - - - - null} - > - Sortable Header - - - -
- ); - - expect(wrapper.find('Offscreen').text()).toBe('up and away'); - expect(wrapper.find('.Icon--table-sort-ascending').exists()).toBe(true); - }); - - test('renders triangle down Icon and descending message when sortDirection is descending', () => { - const wrapper = mount( - - - - null} - > - Sortable Header - - - -
- ); - - expect(wrapper.find('Offscreen').text()).toBe('down below'); - expect(wrapper.find('.Icon--table-sort-descending').exists()).toBe(true); - }); - - test('calls onSort when sort button is clicked', () => { - const onSortSpy = spy(); - const wrapper = mount( - - - - - Sortable Header - - - -
- ); - - wrapper.find('button').simulate('click'); - - expect(onSortSpy.calledOnce).toBe(true); - }); - - test('focus stays on the sort button after it is clicked', () => { - const wrapper = mount( - - - - null} - > - Sortable Header - - - -
- ); - - wrapper.find('button').simulate('click'); - wrapper.update(); - - expect(document.activeElement.id).toBe( - wrapper.find('button').getDOMNode().id - ); - }); - }); -}); - -test('returns 0 axe violations', async () => { - const table = renderDefaultTable(); - expect(await axe(table.html())).toHaveNoViolations(); -}); diff --git a/packages/react/src/components/Table/index.test.tsx b/packages/react/src/components/Table/index.test.tsx new file mode 100644 index 000000000..64953dd59 --- /dev/null +++ b/packages/react/src/components/Table/index.test.tsx @@ -0,0 +1,314 @@ +import React from 'react'; +import { render, screen, waitFor } from '@testing-library/react'; +import userEvent from '@testing-library/user-event'; +import Table, { + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + TableFooter +} from './'; +import axe from '../../axe'; + +const renderDefaultTable = () => { + return render( + + + + + A + + + B + + + + + + + 1 + + 2 + + + + + + foo + + bar + + +
+ ); +}; + +test('should render table and its components correctly', () => { + renderDefaultTable(); + + const table = screen.getByRole('table'); + const tableHead = screen.getByTestId('thead'); + const tableBody = screen.getByTestId('tbody'); + const tableRows = screen.getAllByRole('row'); + const tableHeaders = screen.getAllByRole('columnheader'); + const tableCells = screen.getAllByRole('cell'); + const tableFooter = screen.getByTestId('tfoot'); + + // Accessing the first elements of each type for more detailed assertions + const firstTableRow = tableRows[0]; + const firstTableHeader = tableHeaders[0]; + const firstTableCell = tableCells[0]; + + // Assertions on the lengths of various table components + expect(tableRows).toHaveLength(3); // Assuming there are three rows + expect(tableHeaders).toHaveLength(2); // Assuming there are two column headers + expect(tableCells).toHaveLength(4); // Assuming there are four cells + + const tableItems = [ + table, + tableHead, + tableBody, + firstTableRow, + firstTableHeader, + firstTableCell, + tableFooter + ]; + + tableItems.forEach((element) => { + expect(element).toBeInTheDocument(); + }); +}); + +test('should pass classNames through', () => { + renderDefaultTable(); + + const table = screen.getByRole('table'); + const tableHead = screen.getByTestId('thead'); + const tableBody = screen.getByTestId('tbody'); + const tableRows = screen.getAllByRole('row'); + const tableHeaders = screen.getAllByRole('columnheader'); + const tableCells = screen.getAllByRole('cell'); + const tableFooter = screen.getByTestId('tfoot'); + + const firstTableRow = tableRows[0]; + const firstTableHeader = tableHeaders[0]; + const firstTableCell = tableCells[0]; + + expect(table).toHaveClass('my-table'); + expect(tableHead).toHaveClass('my-table-head'); + expect(tableBody).toHaveClass('my-table-body'); + expect(firstTableRow).toHaveClass('my-table-row'); + expect(firstTableHeader).toHaveClass('my-table-header'); + expect(firstTableCell).toHaveClass('my-table-cell'); + expect(tableFooter).toHaveClass('my-table-footer'); +}); + +test('should pass arbitrary props through', () => { + renderDefaultTable(); + + const table = screen.getByRole('table'); + const tableHead = screen.getByTestId('thead'); + const tableBody = screen.getByTestId('tbody'); + const tableRows = screen.getAllByRole('row'); + const tableHeaders = screen.getAllByRole('columnheader'); + const tableCells = screen.getAllByRole('cell'); + const tableFooter = screen.getByTestId('tfoot'); + + const firstTableRow = tableRows[0]; + const firstTableHeader = tableHeaders[0]; + const firstTableCell = tableCells[0]; + + const tableItems = [ + table, + tableHead, + tableBody, + firstTableRow, + firstTableHeader, + firstTableCell, + tableFooter + ]; + + tableItems.forEach((element) => { + expect(element).toHaveAttribute('data-foo', 'true'); + }); + + expect(firstTableHeader).toHaveAttribute('scope', 'col'); +}); + +test('should render the expected semantic HTML elements', () => { + renderDefaultTable(); + + const table = screen.getByRole('table'); + const tableHead = screen.getByTestId('thead'); + const tableBody = screen.getByTestId('tbody'); + const tableRows = screen.getAllByRole('row'); + const tableHeaders = screen.getAllByRole('columnheader'); + const tableCells = screen.getAllByRole('cell'); + const tableFooter = screen.getByTestId('tfoot'); + + const firstTableRow = tableRows[0]; + const firstTableHeader = tableHeaders[0]; + const firstTableCell = tableCells[0]; + + expect(table.tagName).toBe('TABLE'); + expect(tableHead.tagName).toBe('THEAD'); + expect(tableBody.tagName).toBe('TBODY'); + expect(firstTableRow.tagName).toBe('TR'); + expect(firstTableHeader.tagName).toBe('TH'); + expect(firstTableCell.tagName).toBe('TD'); + expect(tableFooter.tagName).toBe('TFOOT'); +}); + +test('should render with border variant', () => { + render( + + + + Header + + + + + Cell + + +
+ ); + + expect(screen.getByRole('table')).toHaveClass('Table--border'); +}); + +test('should render sort button and icons with sortDirection and onSort in Table', () => { + render( + + + + null}> + Sortable Header + + + +
+ ); + + expect(screen.getByRole('button')).toBeInTheDocument(); + expect(screen.getByRole('status').closest('.Icon--sort-triangle')); + expect(screen.getByRole('status')).toHaveTextContent(''); +}); + +test('should render className "TableHeader--sorting" when actively sorting', () => { + render( + + + + null}> + Sortable Header + + + +
+ ); + + expect(screen.getByRole('status').closest('.TableHeader--sort-ascending')); +}); + +test('should render triangle up Icon and ascending message when sortDirection is ascending', () => { + render( + + + + null} + > + Sortable Header + + + +
+ ); + + expect(screen.getByText('up and away')).toBeInTheDocument(); + expect(screen.getByRole('status').closest('.Icon--table-sort-ascending')); +}); + +test('should render triangle down Icon and descending message when sortDirection is descending', () => { + render( + + + + null} + > + Sortable Header + + + +
+ ); + + expect(screen.getByText('down below')).toBeInTheDocument(); + expect(screen.getByRole('status').closest('.Icon--table-sort-descending')); +}); + +test('should call onSort when sort button is clicked', () => { + const onSortMock = jest.fn(); + + render( + + + + + Sortable Header + + + +
+ ); + + const button = screen.getByRole('button'); + + userEvent.click(button); + waitFor(() => { + expect(onSortMock).toHaveBeenCalledTimes(1); + }); +}); + +test('should maintain focus on the sort button after it is clicked', () => { + render( + + + + null} + > + Sortable Header + + + +
+ ); + + const button = screen.getByRole('button'); + + userEvent.click(button); + waitFor(() => { + expect(button).toHaveFocus(); + }); +}); + +test('returns 0 axe violations', async () => { + const { container } = renderDefaultTable(); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); From f727cd58f6eefcae2f65b575860fca9aa79853dc Mon Sep 17 00:00:00 2001 From: lsprr <16653744+lsprr@users.noreply.github.com> Date: Fri, 1 Mar 2024 23:34:04 -0500 Subject: [PATCH 2/3] Minor corrections --- packages/react/src/components/Table/index.test.tsx | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/react/src/components/Table/index.test.tsx b/packages/react/src/components/Table/index.test.tsx index 64953dd59..c0171fc93 100644 --- a/packages/react/src/components/Table/index.test.tsx +++ b/packages/react/src/components/Table/index.test.tsx @@ -259,7 +259,7 @@ test('should render triangle down Icon and descending message when sortDirection expect(screen.getByRole('status').closest('.Icon--table-sort-descending')); }); -test('should call onSort when sort button is clicked', () => { +test('should call onSort when sort button is clicked', async () => { const onSortMock = jest.fn(); render( @@ -276,13 +276,13 @@ test('should call onSort when sort button is clicked', () => { const button = screen.getByRole('button'); - userEvent.click(button); - waitFor(() => { + await userEvent.click(button); + await waitFor(() => { expect(onSortMock).toHaveBeenCalledTimes(1); }); }); -test('should maintain focus on the sort button after it is clicked', () => { +test('should maintain focus on the sort button after it is clicked', async () => { render( @@ -301,8 +301,8 @@ test('should maintain focus on the sort button after it is clicked', () => { const button = screen.getByRole('button'); - userEvent.click(button); - waitFor(() => { + await userEvent.click(button); + await waitFor(() => { expect(button).toHaveFocus(); }); }); From 0a725fee15cacd8e8cfe12d3c15cb10f126e6bba Mon Sep 17 00:00:00 2001 From: lsprr <16653744+lsprr@users.noreply.github.com> Date: Mon, 18 Mar 2024 16:10:15 -0400 Subject: [PATCH 3/3] Refactor and fix issues based on feedback --- .../react/src/components/Table/index.test.tsx | 77 ++++++++++++++++--- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/packages/react/src/components/Table/index.test.tsx b/packages/react/src/components/Table/index.test.tsx index c0171fc93..752a8c499 100644 --- a/packages/react/src/components/Table/index.test.tsx +++ b/packages/react/src/components/Table/index.test.tsx @@ -99,13 +99,13 @@ test('should pass classNames through', () => { const firstTableHeader = tableHeaders[0]; const firstTableCell = tableCells[0]; - expect(table).toHaveClass('my-table'); - expect(tableHead).toHaveClass('my-table-head'); - expect(tableBody).toHaveClass('my-table-body'); - expect(firstTableRow).toHaveClass('my-table-row'); - expect(firstTableHeader).toHaveClass('my-table-header'); - expect(firstTableCell).toHaveClass('my-table-cell'); - expect(tableFooter).toHaveClass('my-table-footer'); + expect(table).toHaveClass('Table', 'my-table'); + expect(tableHead).toHaveClass('TableHead', 'my-table-head'); + expect(tableBody).toHaveClass('TableBody', 'my-table-body'); + expect(firstTableRow).toHaveClass('TableRow', 'my-table-row'); + expect(firstTableHeader).toHaveClass('TableHeader', 'my-table-header'); + expect(firstTableCell).toHaveClass('TableCell', 'my-table-cell'); + expect(tableFooter).toHaveClass('TableFooter', 'my-table-footer'); }); test('should pass arbitrary props through', () => { @@ -180,7 +180,7 @@ test('should render with border variant', () => {
); - expect(screen.getByRole('table')).toHaveClass('Table--border'); + expect(screen.getByRole('table')).toHaveClass('Table', 'Table--border'); }); test('should render sort button and icons with sortDirection and onSort in Table', () => { @@ -214,7 +214,14 @@ test('should render className "TableHeader--sorting" when actively sorting', () ); - expect(screen.getByRole('status').closest('.TableHeader--sort-ascending')); + expect(screen.getByRole('columnheader')).toHaveAttribute( + 'aria-sort', + 'ascending' + ); + expect(screen.getByRole('columnheader')).toHaveClass( + 'TableHeader', + 'TableHeader--sort-ascending' + ); }); test('should render triangle up Icon and ascending message when sortDirection is ascending', () => { @@ -234,7 +241,7 @@ test('should render triangle up Icon and ascending message when sortDirection is ); - expect(screen.getByText('up and away')).toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent('up and away'); expect(screen.getByRole('status').closest('.Icon--table-sort-ascending')); }); @@ -255,7 +262,7 @@ test('should render triangle down Icon and descending message when sortDirection ); - expect(screen.getByText('down below')).toBeInTheDocument(); + expect(screen.getByRole('status')).toHaveTextContent('down below'); expect(screen.getByRole('status').closest('.Icon--table-sort-descending')); }); @@ -312,3 +319,51 @@ test('returns 0 axe violations', async () => { const results = await axe(container); expect(results).toHaveNoViolations(); }); + +test('returns 0 axe violations without any sorting', async () => { + const { container } = render( + + + + null}> + Sortable Header + + + +
+ ); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); + +test('returns 0 axe violations with ascending sorting', async () => { + const { container } = render( + + + + null}> + Sortable Header + + + +
+ ); + const results = await axe(container); + expect(results).toHaveNoViolations(); +}); + +test('returns 0 axe violations with descending sorting', async () => { + const { container } = render( + + + + null}> + Sortable Header + + + +
+ ); + const results = await axe(container); + expect(results).toHaveNoViolations(); +});