import { test, expect, vi } from 'vitest'; import { render, screen, within, fireEvent } from '@testing-library/react'; import Pagination, { Page, Ellipsis } from './Pagination'; const PAGE_ARIA_LABEL = 'Page 1'; const PAGE_HREF = '#foo'; const PAGE_TEXT = 1; const PAGE_LABEL = {PAGE_TEXT}; const CURRENT_PAGE = 10; const TOTAL_PAGES = 21; test('pagination page renders correctly', () => { render( {PAGE_LABEL} ); const item = screen.getByRole('listitem'); const link = within(item).getByRole('link'); const span = within(link).getByText(PAGE_TEXT); expect(item).toHaveClass('ds_pagination__item'); expect(link).toHaveClass('ds_pagination__link'); expect(link).toHaveAttribute('aria-label', PAGE_ARIA_LABEL); expect(link).toHaveAttribute('href', PAGE_HREF); expect(link.tagName).toEqual('A'); expect(span).toHaveClass('ds_pagination__link-label'); }); test('pagination page renders with custom link component', () => { render( ( )} >{PAGE_LABEL} ); const item = screen.getByRole('listitem'); const link = within(item).getByRole('link'); const span = within(link).getByText(PAGE_TEXT); expect(link.tagName).toEqual('SPAN'); expect(span).toHaveClass('ds_pagination__link-label'); }); test('current pagination page', () => { render( {PAGE_LABEL} ); const item = screen.getByRole('listitem'); const link = within(item).getByRole('link'); expect(link).toHaveClass('ds_current'); expect(link).toHaveAttribute('aria-current', 'page'); }); test('pagination page with click event', () => { const ONCLICK_FUNCTION = vi.fn(); render( {PAGE_LABEL} ); const item = screen.getByRole('listitem'); const link = within(item).getByRole('link'); fireEvent.click(link); expect(ONCLICK_FUNCTION).toHaveBeenCalled(); }); test('pagination page with onClick that is not a function', () => { render( {PAGE_LABEL} ); const item = screen.getByRole('listitem'); const link = within(item).getByRole('link'); fireEvent.click(link); // todo: assertion // success indicated by no errors thrown // error would be thrown on an untestable thread }); test('Ellipsis item renders correctly', () => { render( ); const item = screen.getByRole('listitem', {hidden: true}); const link = within(item).getByText('…'); expect(item).toHaveClass('ds_pagination__item'); expect(item).toHaveAttribute('aria-hidden', 'true'); expect(link).toHaveClass('ds_pagination__link', 'ds_pagination__link--ellipsis'); }); test('pagination renders correctly', () => { const CURRENT_PAGE = 10; const TOTAL_PAGES = 21; render( ); const paginationNav = screen.getByRole('navigation'); const paginationList = within(paginationNav).getByRole('list'); const prevLabel = within(paginationList).getByText('Previous'); const prevLink = prevLabel.parentElement; const prevIcon = prevLabel.previousElementSibling; const prevItem = prevLink?.parentElement; const nextLabel = within(paginationList).getByText('Next'); const nextLink = nextLabel.parentElement; const nextIcon = nextLabel.nextElementSibling; const nextItem = nextLink?.parentElement; const firstPageLabel = within(paginationList).getByText('1'); const firstPageLink = firstPageLabel.parentElement; const firstPageItem = firstPageLink?.parentElement; const lastPageLabel = within(paginationList).getByText(TOTAL_PAGES); const lastPageLink = lastPageLabel.parentElement; const lastPageItem = lastPageLink?.parentElement; const firstPageEllipsisItem = firstPageItem?.nextElementSibling; const firstPageEllipsis = firstPageEllipsisItem?.children[0]; const lastPageEllipsisItem = lastPageItem?.previousElementSibling; const lastPageEllipsis = lastPageEllipsisItem?.children[0]; const currentPageLink = document.querySelector('.ds_current'); const currentPageItem = currentPageLink?.parentElement; const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true }); expect(paginationNav).toHaveClass('ds_pagination'); expect(paginationNav).toHaveAttribute('aria-label', 'Pages'); expect(paginationList).toHaveClass('ds_pagination__list'); expect(paginationList.tagName).toEqual('UL'); expect(prevItem).toHaveClass('ds_pagination__item'); expect(prevItem?.tagName).toEqual('LI'); expect(prevItem?.parentElement).toEqual(paginationList); expect(prevLink).toHaveClass('ds_pagination__link', 'ds_pagination__link--text', 'ds_pagination__link--icon') expect(prevLink).toHaveAttribute('aria-label', 'Previous page'); expect(prevLink).toHaveAttribute('href', `/search?page=${CURRENT_PAGE - 1}`); expect(prevLink?.tagName).toEqual('A'); expect(prevIcon).toHaveClass('ds_icon'); expect(prevIcon).toHaveAttribute('aria-hidden', 'true') expect(prevIcon?.tagName).toEqual('svg'); expect(prevLabel).toHaveClass('ds_pagination__link-label'); expect(prevLabel?.tagName).toEqual('SPAN'); expect(nextItem).toHaveClass('ds_pagination__item'); expect(nextItem?.tagName).toEqual('LI'); expect(nextItem?.parentElement).toEqual(paginationList); expect(nextLink).toHaveClass('ds_pagination__link', 'ds_pagination__link--text', 'ds_pagination__link--icon') expect(nextLink).toHaveAttribute('aria-label', 'Next page'); expect(nextLink).toHaveAttribute('href', `/search?page=${CURRENT_PAGE + 1}`); expect(nextLink?.tagName).toEqual('A'); expect(nextIcon).toHaveClass('ds_icon'); expect(nextIcon).toHaveAttribute('aria-hidden', 'true') expect(nextIcon?.tagName).toEqual('svg'); expect(nextLabel).toHaveClass('ds_pagination__link-label'); expect(nextLabel?.tagName).toEqual('SPAN'); expect(firstPageItem).toHaveClass('ds_pagination__item'); expect(firstPageItem?.tagName).toEqual('LI'); expect(firstPageItem?.parentElement).toEqual(paginationList); expect(firstPageLink).toHaveClass('ds_pagination__link') expect(firstPageLink).toHaveAttribute('aria-label', 'Page 1'); expect(firstPageLink).toHaveAttribute('href', `/search?page=1`); expect(firstPageLink?.tagName).toEqual('A'); expect(firstPageLabel).toHaveClass('ds_pagination__link-label'); expect(firstPageLabel?.tagName).toEqual('SPAN'); expect(lastPageItem).toHaveClass('ds_pagination__item'); expect(lastPageItem?.tagName).toEqual('LI'); expect(lastPageItem?.parentElement).toEqual(paginationList); expect(lastPageLink).toHaveClass('ds_pagination__link') expect(lastPageLink).toHaveAttribute('aria-label', `Page ${TOTAL_PAGES}`); expect(lastPageLink).toHaveAttribute('href', `/search?page=${TOTAL_PAGES}`); expect(lastPageLink?.tagName).toEqual('A'); expect(lastPageLabel).toHaveClass('ds_pagination__link-label'); expect(lastPageLabel?.tagName).toEqual('SPAN'); // dev note: by this point I'd started to wonder if it would just be better to do a single string compare of the rendered output versus the expected output expect(firstPageEllipsisItem).toHaveClass('ds_pagination__item'); expect(firstPageEllipsisItem).toHaveAttribute('aria-hidden', 'true'); expect(firstPageEllipsisItem?.tagName).toEqual('LI'); expect(firstPageEllipsis).toHaveClass('ds_pagination__link', 'ds_pagination__link--ellipsis'); expect(firstPageEllipsis?.tagName).toEqual('SPAN'); expect(firstPageEllipsis?.textContent).toEqual('…'); expect(lastPageEllipsisItem).toHaveClass('ds_pagination__item'); expect(lastPageEllipsisItem).toHaveAttribute('aria-hidden', 'true'); expect(lastPageEllipsisItem?.tagName).toEqual('LI'); expect(lastPageEllipsis).toHaveClass('ds_pagination__link', 'ds_pagination__link--ellipsis'); expect(lastPageEllipsis?.tagName).toEqual('SPAN'); expect(lastPageEllipsis?.textContent).toEqual('…'); expect(currentPageItem).toHaveClass('ds_pagination__item'); expect(currentPageLink).toHaveClass('ds_pagination__link', 'ds_current'); expect(currentPageLink?.textContent).toEqual(CURRENT_PAGE.toString()); // expect one link either side of the current (default padding) expect(currentPageItem?.previousElementSibling?.querySelector('a')).toHaveAttribute('aria-label', 'Page 9'); expect(currentPageItem?.previousElementSibling?.previousElementSibling?.querySelector('a')).toBeNull(); expect(currentPageItem?.nextElementSibling?.querySelector('a')).toHaveAttribute('aria-label', 'Page 11'); expect(currentPageItem?.nextElementSibling?.nextElementSibling?.querySelector('a')).toBeNull(); // 9 is: previous, first, ellipsis, current page and 1 padding either side, ellipsis, last, next expect(paginationItems.length).toEqual(9); }); test('pagination with 2 padding', () => { const PADDING = 2; render( ); const paginationNav = screen.getByRole('navigation'); const paginationList = within(paginationNav).getByRole('list'); const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true }); // 11 is: previous, first, ellipsis, current page and 2 padding either side, ellipsis, last, next expect(paginationItems.length).toEqual(11); }); test('pagination with custom aria label', () => { const ARIA_LABEL = 'My label'; render( ); const paginationNav = screen.getByRole('navigation'); expect(paginationNav).toHaveAttribute('aria-label', ARIA_LABEL); }); test('pagination passes onclick event to child links', () => { const ONCLICK_FUNCTION = vi.fn(); render( ); // pick an arbitrary link const link = [].slice.call(document.querySelectorAll('.ds_pagination__link'))[4]; link.setAttribute('href', '#foo'); fireEvent.click(link); expect(ONCLICK_FUNCTION).toHaveBeenCalled(); }); test('pagination modifies an input pattern for its link format', () => { render( ); // pick an arbitrary link const link = document.querySelector('.ds_pagination__link.ds_current'); expect(link).toHaveAttribute('href', 'My/Link/Format?Page=10#foo'); }); test('pagination at an early link in the list', () => { render( ); const paginationNav = screen.getByRole('navigation'); const paginationList = within(paginationNav).getByRole('list'); const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true }); // 7 is: current page and 3 subsequent items (padding plus 2), ellipsis, last, next expect(paginationItems.length).toEqual(7); expect(paginationNav.textContent).toEqual('1234…21Next'); }); test('pagination at an early link in the list, increased padding', () => { render( ); const paginationNav = screen.getByRole('navigation'); const paginationList = within(paginationNav).getByRole('list'); const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true }); // 8 is: current page and 4 subsequent items (padding plus 2), ellipsis, last, next expect(paginationItems.length).toEqual(8); expect(paginationNav.textContent).toEqual('12345…21Next'); }); test('pagination at late link in the list', () => { render( ); const paginationNav = screen.getByRole('navigation'); const paginationList = within(paginationNav).getByRole('list'); const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true }); // 7 is: current page and 3 subsequent items (padding plus 2), ellipsis, last, next expect(paginationItems.length).toEqual(7); expect(paginationNav.textContent).toEqual('Previous1…18192021'); }); test('pagination at late link in the list, increased padding', () => { render( ); const paginationNav = screen.getByRole('navigation'); const paginationList = within(paginationNav).getByRole('list'); const paginationItems = within(paginationList).getAllByRole('listitem', { hidden: true }); // 8 is: current page and 4 subsequent items (padding plus 2), ellipsis, last, next expect(paginationItems.length).toEqual(8); expect(paginationNav.textContent).toEqual('Previous1…1718192021'); }); test('passing additional props', () => { render( ); const paginationNav = screen.getByRole('navigation'); expect(paginationNav?.dataset.test).toEqual('foo'); }); test('passing additional CSS classes', () => { render( ) const paginationNav = screen.getByRole('navigation'); expect(paginationNav).toHaveClass('foo'); });