/** * External dependencies */ import { fireEvent, render, screen, waitFor } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; /** * WordPress dependencies */ import { useMemo, useState } from '@wordpress/element'; /** * Internal dependencies */ import DataViews from '../index'; import { LAYOUT_ACTIVITY, LAYOUT_GRID, LAYOUT_LIST, LAYOUT_TABLE, } from '../../constants'; import type { Action, View } from '../../types'; import filterSortAndPaginate from '../../utils/filter-sort-and-paginate'; type Data = { id: number; title: string; author?: number; order?: number; }; const DEFAULT_VIEW = { type: 'table' as const, search: '', page: 1, perPage: 10, layout: {}, filters: [], }; const defaultLayouts = { [ LAYOUT_TABLE ]: {}, [ LAYOUT_GRID ]: {}, [ LAYOUT_LIST ]: {}, [ LAYOUT_ACTIVITY ]: {}, }; const fields = [ { id: 'title', label: 'Title', type: 'text' as const, }, { id: 'order', label: 'Order', type: 'integer' as const, }, { id: 'author', label: 'Author', type: 'integer' as const, elements: [ { value: 1, label: 'Jane' }, { value: 2, label: 'John' }, ], }, { label: 'Image', id: 'image', render: ( { item }: { item: Data } ) => { return ( ); }, enableSorting: false, }, ]; const actions: Action< Data >[] = [ { id: 'delete', label: 'Delete', supportsBulk: true, RenderModal: () =>
Modal Content
, }, ]; const data: Data[] = [ { id: 1, title: 'Hello World', author: 1, order: 1, }, { id: 2, title: 'Homepage', author: 2, order: 1, }, { id: 3, title: 'Posts', author: 2, order: 1, }, ]; function DataViewWrapper( { view: additionalView, ...props }: Partial< Parameters< typeof DataViews< Data > >[ 0 ] > ) { const [ view, setView ] = useState< View >( { ...DEFAULT_VIEW, fields: [ 'title', 'order', 'author' ], ...additionalView, } ); const { data: shownData, paginationInfo } = useMemo( () => { return filterSortAndPaginate( data, view, props.fields || fields ); }, [ view, props.fields ] ); const dataViewProps = { getItemId: ( item: Data ) => item.id.toString(), paginationInfo, data: shownData, view, fields, onChangeView: setView, actions: [], defaultLayouts, ...props, }; return ; } // jest.useFakeTimers(); // Tests run against a DataView which is 500px wide. const mockUseViewportMatch = jest.fn( // eslint-disable-next-line @typescript-eslint/no-unused-vars ( _viewport: string, _operator: string ) => false ); jest.mock( '@wordpress/compose', () => { return { ...jest.requireActual( '@wordpress/compose' ), useResizeObserver: jest.fn( ( callback ) => { setTimeout( () => { callback( [ { borderBoxSize: [ { inlineSize: 500 } ], }, ] ); }, 0 ); return () => {}; } ), useViewportMatch: ( viewport: string, operator: string ): boolean => mockUseViewportMatch( viewport, operator ), }; } ); describe( 'DataViews component', () => { it( 'should show "No results" if data is empty', () => { render( ); expect( screen.getByText( 'No results' ) ).toBeInTheDocument(); } ); it( 'should filter results by "search" text, if field has enableGlobalSearch set to true', async () => { const fieldsWithSearch = [ { ...fields[ 0 ], enableGlobalSearch: true, }, fields[ 1 ], ]; render( ); // Row count includes header. expect( screen.getAllByRole( 'row' ).length ).toEqual( 2 ); expect( screen.getByText( 'Hello World' ) ).toBeInTheDocument(); } ); it( 'should display matched element label if field contains elements list', () => { render( ); expect( screen.getByText( 'Tim' ) ).toBeInTheDocument(); } ); it( 'should render custom render function if defined in field definition', () => { render( { return item.title?.toUpperCase(); }, }, ] } /> ); expect( screen.getByText( 'TEST TITLE' ) ).toBeInTheDocument(); } ); it( 'should trigger infinite scroll when the layout container scrolls', async () => { const onChangeView = jest.fn(); if ( typeof global.IntersectionObserver === 'undefined' ) { ( global as any ).IntersectionObserver = jest.fn( () => ( { observe: jest.fn(), unobserve: jest.fn(), disconnect: jest.fn(), } ) ); } const { container } = render( ); // eslint-disable-next-line testing-library/no-container, testing-library/no-node-access const layoutContainer = container.querySelector( '.dataviews-layout__container' ) as HTMLDivElement; Object.defineProperties( layoutContainer, { scrollTop: { configurable: true, value: 500, }, scrollHeight: { configurable: true, value: 1000, }, clientHeight: { configurable: true, value: 500, }, } ); fireEvent.scroll( layoutContainer ); await waitFor( () => { expect( onChangeView ).toHaveBeenCalledWith( expect.objectContaining( { infiniteScrollEnabled: true, startPosition: 2, } ) ); } ); } ); describe( 'in table view', () => { it( 'should display columns for each field', () => { render( ); const displayedColumnFields = fields.filter( ( field ) => [ 'title', 'order', 'author' ].includes( field.id ) ); for ( const field of displayedColumnFields ) { expect( screen.getByRole( 'button', { name: field.label } ) ).toBeInTheDocument(); } } ); it( 'should display the passed in data', () => { render( ); for ( const item of data ) { expect( screen.getAllByText( item.title )[ 0 ] ).toBeInTheDocument(); } } ); it( 'should display title column if defined using titleField', () => { render( ); for ( const item of data ) { expect( screen.getAllByText( item.title )[ 0 ] ).toBeInTheDocument(); } } ); it( 'should render actions column if actions are supported and passed in', () => { render( ); expect( screen.getByText( 'Actions' ) ).toBeInTheDocument(); } ); it( 'should trigger the onClickItem callback if isItemClickable returns true and title field is clicked', async () => { const onClickItemCallback = jest.fn(); render( true } renderItemLink={ ( { item, ...props } ) => (