import React from 'react' import { ListGrid } from '.' import type { Column, GridActionsMenuFullProps, GridActionsMenuProps, GridRowMeta, } from '../../types' import { render, snapshot, waitFor, within } from '../../utils/test-utils' import userEvent from '@testing-library/user-event' import { GridCellDefault, GridCellDropdownMenu } from '../../components' import { EmptyState, ListItem, ListItemInfo, Menu } from '@planview/pv-uikit' import { TestingProvider } from '@planview/pv-utilities' /* Just ignore setPointerCapture calls, JSDOM doesn't support it */ Object.defineProperty(global.HTMLElement.prototype, 'setPointerCapture', { value: () => {}, }) type User = { id: number firstName: string lastName: string status: 'Active' | 'Pending' } const columns: Column[] = [ { id: 'firstName', label: 'First Name', }, { id: 'lastName', label: 'Last Name', }, { id: 'status', label: 'Status', cell: { Renderer({ tabIndex, value }) { return ( ) }, }, }, ] const rows: User[] = [ { id: 1, firstName: 'Joe', lastName: 'Doe', status: 'Active' }, { id: 2, firstName: 'Fran', lastName: 'Bran', status: 'Pending' }, ] const nestedRows = new Map([ [3, { id: 3, firstName: 'Mr', lastName: 'Manager', status: 'Active' }], [1, { id: 1, firstName: 'Joe', lastName: 'Doe', status: 'Active' }], [2, { id: 2, firstName: 'Fran', lastName: 'Bran', status: 'Pending' }], ]) const treeMeta = new Map>([ [3, { type: 'tree', children: [1, 2] }], ]) describe('ListGrid', () => { describe('snapshots', () => { snapshot( 'should render a simple list grid', ) snapshot( 'should render a list grid with empty content', } /> ) snapshot( 'should render a list grid without columns or rows', ) snapshot( 'should render a list grid in loading mode without columns or rows', ) snapshot( 'should render a list grid without columns', ) snapshot( 'should support flexColumnId to specify which column should flex', ) snapshot( 'should default flexColumnId to first column', ) }) it('should support an accessible label', () => { const { queryByRole } = render( ) expect(queryByRole('grid', { name: 'Users list' })).toBeInTheDocument() }) it('should render a list grid without headers', () => { const { container } = render() // ListGrid should hide headers but show them to screen readers expect(container.querySelector('thead')).toHaveStyle( 'clip: rect(0 0 0 0)' ) }) it('should render a list grid without selection checkboxes', () => { const { queryByRole } = render( ) // ListGrid forces selectionMode="none" expect(queryByRole('checkbox')).not.toBeInTheDocument() }) it('should render a list grid with more rows when used with TestingProvider', () => { const { queryAllByRole } = render( ({ id: `column-${i}`, label: `${i}`, width: 100, }))} rows={Array.from({ length: 100 }, (_, i) => ({ id: i, title: `${i}`, }))} /> ) // Should render more rows with larger container expect(queryAllByRole('row').length).toBeGreaterThan(25) }) it('should render a tree structure', async () => { const treeColumns = columns.map((c) => { if (c.id === 'firstName') { return { ...c, tree: true, } } return c }) const { queryByText, getByRole } = render( ) const treegrid = getByRole('treegrid') expect(treegrid).toBeInTheDocument() expect(treegrid).toHaveAttribute('aria-rowcount', '4') expect(queryByText('Joe')).not.toBeInTheDocument() await userEvent.click(getByRole('button', { name: 'Expand row' })) expect(queryByText('Joe')).toBeInTheDocument() }) it('should handle both rows', () => { const { getByRole } = render() expect(getByRole('gridcell', { name: /Joe/ })).toBeInTheDocument() expect(getByRole('gridcell', { name: /Fran/ })).toBeInTheDocument() }) describe('ActionsMenu', () => { const ActionsMenuItems = ({ row }: GridActionsMenuProps) => ( <> {row.firstName} {row.lastName} ) const ActionsMenu = ({ row, menuProps, }: GridActionsMenuFullProps) => ( {row.firstName} {row.lastName} ) ;[ { label: 'MenuItems', description: 'with a MenuItems component', actionsMenu: ActionsMenuItems, }, { label: 'Menu', description: 'with a Menu component', actionsMenu: { Menu: ActionsMenu }, }, ].forEach(({ label, description, actionsMenu }) => { describe(`${description}`, () => { it('should open the menu if a user right clicks on a row', async () => { const { getAllByRole, getByRole } = render( ) const renderedRows = getAllByRole('row') await userEvent.pointer({ target: renderedRows[1], keys: '[MouseRight]', }) expect( getByRole('menuitemradio', { name: 'Action one' }) ).toBeInTheDocument() expect( within(getByRole('menu')).getByText('Joe Doe') ).toBeInTheDocument() }) it('should open the menu if a user clicks on the actions button', async () => { const { getAllByRole, getByRole } = render( ) const buttons = getAllByRole('button', { name: /More actions/, expanded: false, }) await userEvent.click(buttons[0]) expect( getByRole('menuitemradio', { name: 'Action one' }) ).toBeInTheDocument() expect( within(getByRole('menu')).getByText('Joe Doe') ).toBeInTheDocument() }) it('should not open a menu if a user right clicks on a link', async () => { const { getByRole, queryByRole } = render( ) }, }, }, ]} rows={rows} actionsMenu={actionsMenu} /> ) const link = getByRole('link', { name: /Joe/, }) await userEvent.pointer({ target: link, keys: '[MouseRight]', }) expect( queryByRole('menuitemradio', { name: 'Action one' }) ).not.toBeInTheDocument() }) it('should support conditional rendering', async () => { const { getAllByRole, queryByRole } = render( ) const renderedRows = getAllByRole('row') // Button rendered on first row expect( within(renderedRows[1]).queryByRole('button', { name: /More actions/, expanded: false, }) ).toBeInTheDocument() // Button not rendered on second row expect( within(renderedRows[2]).queryByRole('button', { name: /More actions/, expanded: false, }) ).not.toBeInTheDocument() // Right click on second row, no menu should be shown await userEvent.pointer({ target: renderedRows[2], keys: '[MouseRight]', }) expect( queryByRole('menuitemradio', { name: 'Action one' }) ).not.toBeInTheDocument() // Right click on first row, menu should be shown await userEvent.pointer({ target: renderedRows[1], keys: '[MouseRight]', }) expect( queryByRole('menuitemradio', { name: 'Action one' }) ).toBeInTheDocument() }) }) }) }) describe('Navigation', () => { it('should navigate to second column, first row', async () => { const { getByText } = render( ) await userEvent.keyboard('{Tab}{ArrowRight}') expect(getByText(/Doe/)).toHaveFocus() }) it('should navigate to last column on keyboard End', async () => { const { getByRole } = render( ) await userEvent.keyboard('{Tab}{End}') await waitFor(() => expect( getByRole('button', { name: /Active/, expanded: false, }) ).toHaveFocus() ) }) it('should navigate to first column on keyboard Home', async () => { const { getByText } = render( ) await userEvent.keyboard('{Tab}{ArrowRight}') expect(getByText(/Doe/)).toHaveFocus() await userEvent.keyboard('{Home}') await waitFor(() => expect(getByText(/Joe/)).toHaveFocus()) }) it('should freeze navigation on opening dropdown', async () => { const { getByRole, getByText } = render( ) await userEvent.keyboard('{Tab}{ArrowRight}{ArrowRight}') expect( getByRole('button', { name: /Active/, expanded: false, }) ).toHaveFocus() await userEvent.keyboard(' ') await userEvent.keyboard('{ArrowLeft}{ArrowDown}{ArrowLeft}') expect(getByRole('menu')).toHaveFocus() await userEvent.keyboard('{Escape}') await userEvent.keyboard('{ArrowLeft}') expect(getByText(/Doe/)).toHaveFocus() }) it('should not select the row when clicking on a dropdown', async () => { const { getAllByRole, getByRole } = render( ) const buttons = getAllByRole('button', { name: /Active/, expanded: false, }) await userEvent.click(buttons[0]) expect(getByRole('menu')).toHaveFocus() }) }) })