/**
* 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 } ) => (