/* * SPDX-License-Identifier: Apache-2.0 * * The OpenSearch Contributors require contributions made to * this file be licensed under the Apache-2.0 license or a * compatible open source license. * * Modifications Copyright OpenSearch Contributors. See * GitHub history for details. */ /* * Licensed to Elasticsearch B.V. under one or more contributor * license agreements. See the NOTICE file distributed with * this work for additional information regarding copyright * ownership. Elasticsearch B.V. licenses this file to you under * the Apache License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ import React from 'react'; import { mount, shallow } from 'enzyme'; import { requiredProps } from '../../test'; import { OuiInMemoryTable, OuiInMemoryTableProps } from './in_memory_table'; import { keys, SortDirection } from '../../services'; import { SearchFilterConfig } from '../search_bar/filters'; interface BasicItem { id: number | string; name: string; } interface StateItem { active: boolean; name: string; } interface ComplexItem { active: boolean; complex: { name: string; }; } describe('OuiInMemoryTable', () => { test('empty array', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with message', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], message: 'where my items at?', }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with message and loading', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], message: 'Loading items....', loading: true, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with executeQueryOptions', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], executeQueryOptions: { defaultFields: ['name'], }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with items', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with items and expanded item', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', }, ], itemIdToExpandedRowMap: { '1':
expanded row content
, }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with items and message - expecting to show the items', () => { const props: OuiInMemoryTableProps = { ...requiredProps, message: 'show me!', items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], pagination: { pageSizeOptions: [2, 4, 6], }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination and default page size and index', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], pagination: { initialPageIndex: 1, initialPageSize: 2, pageSizeOptions: [1, 2, 3], }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination, default page size and error', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [{ id: '1', name: 'name1' }], error: 'ouch!', columns: [ { field: 'name', name: 'Name', description: 'description', }, ], pagination: { initialPageSize: 4, pageSizeOptions: [2, 4, 6], }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination, hiding the per page options', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], pagination: { hidePerPageOptions: true, }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); describe('sorting', () => { test('with field sorting (off by default)', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '3', name: 'name3' }, { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, ], sorting: true, }; const component = mount(); expect( component .find('tbody .ouiTableCellContent__text') .map((cell) => cell.text()) ).toEqual(['name3', 'name1', 'name2']); }); test('with field sorting (on by default)', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '3', name: 'name3' }, { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, ], sorting: { sort: { field: 'name', direction: SortDirection.ASC, }, }, }; const component = mount(); expect( component .find('tbody .ouiTableCellContent__text') .map((cell) => cell.text()) ).toEqual(['name1', 'name2', 'name3']); }); test('with name sorting', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '3', name: 'name3' }, { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, ], sorting: { sort: { field: 'Name', direction: SortDirection.DESC, }, }, }; const component = mount(); expect( component .find('tbody .ouiTableCellContent__text') .map((cell) => cell.text()) ).toEqual(['name3', 'name2', 'name1']); }); test('verify field sorting precedes name sorting', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name3' }, { id: '3', name: 'name1' }, { id: '2', name: 'name2' }, ], columns: [ { field: 'name', name: 'Column 1', description: 'description', sortable: true, }, { field: 'id', name: 'name', description: 'description', sortable: true, }, ], sorting: { sort: { field: 'name', direction: SortDirection.DESC, }, }, }; const component = mount(); // name TDs should be sorted desc, id TDs should be asc, expect( component .find('tbody .ouiTableCellContent__text') .map((cell) => cell.text()) ).toEqual(['name3', '1', 'name2', '2', 'name1', '3']); }); test('verify an invalid sort field does not blow everything up', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '3', name: 'name3' }, { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, ], sorting: { sort: { field: 'something_nonexistant', direction: SortDirection.ASC, }, }, }; expect(() => { mount(); }).not.toThrow(); }); }); test('with initial sorting', () => { const items = [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ]; // copy the array to ensure the `items` prop doesn't mutate const itemsProp = items.slice(0); const props: OuiInMemoryTableProps = { ...requiredProps, items: itemsProp, columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, ], sorting: { sort: { field: 'name', direction: SortDirection.DESC, }, }, }; const component = shallow(); expect(component).toMatchSnapshot(); expect(itemsProp).toEqual(items); }); test('with initial selection', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', }, ], selection: { onSelectionChange: () => undefined, initialSelected: [{ id: '1', name: 'name1' }], }, }; const component = mount(); expect(component).toMatchSnapshot(); }); test('with pagination and selection', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', }, ], pagination: true, selection: { onSelectionChange: () => undefined, }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination, selection and sorting', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, ], pagination: true, sorting: true, selection: { onSelectionChange: () => undefined, }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination, selection, sorting and column renderer', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, render: (name: any) => name.toUpperCase(), }, ], pagination: { pageSizeOptions: [2, 4, 6], }, sorting: true, selection: { onSelectionChange: () => undefined, }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination, selection, sorting and a single record action', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, { name: 'Actions', actions: [ { name: 'Edit', description: 'edit', onClick: () => undefined, }, ], }, ], pagination: true, sorting: true, selection: { onSelectionChange: () => undefined, }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination, selection, sorting and simple search', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, { name: 'Actions', actions: [ { name: 'Edit', description: 'edit', onClick: () => undefined, }, ], }, ], pagination: true, sorting: true, search: true, selection: { onSelectionChange: () => undefined, }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with search and component between search and table', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, { name: 'Actions', actions: [ { name: 'Edit', description: 'edit', onClick: () => undefined, }, ], }, ], search: true, childrenBetween:
Children Between
, }; const component = shallow(); expect(component).toMatchSnapshot(); }); test('with pagination, selection, sorting and configured search', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, { name: 'Actions', actions: [ { name: 'Edit', description: 'edit', onClick: () => undefined, }, ], }, ], pagination: true, sorting: true, search: { onChange: () => {}, defaultQuery: 'name:name1', box: { incremental: true, }, filters: [ { type: 'field_value_toggle', field: 'name', value: 'name1', name: 'Name1', negatedName: 'Not Name1', }, ] as SearchFilterConfig[], }, selection: { onSelectionChange: () => undefined, }, }; const component = shallow(); expect(component).toMatchSnapshot(); }); describe('search interaction & functionality', () => { it('updates the results as based on the entered query', () => { const props: OuiInMemoryTableProps = { items: [ { active: true, name: 'Kansas', }, { active: true, name: 'North Dakota', }, { active: false, name: 'Florida', }, ], columns: [ { field: 'active', name: 'Is Active', }, { field: 'name', name: 'Name', }, ], search: { onChange: () => true, }, className: 'testTable', }; const component = mount(); // should render with all three results visible expect(component.find('.testTable OuiTableRow').length).toBe(3); const searchField = component.find('OuiFieldSearch input[type="search"]'); searchField.simulate('keyUp', { target: { value: 'is:active', }, key: keys.ENTER, }); component.update(); // should render with the two active results expect(component.find('.testTable OuiTableRow').length).toBe(2); searchField.simulate('keyUp', { target: { value: 'active:false', }, key: keys.ENTER, }); component.update(); // should render with the one inactive result expect(component.find('.testTable OuiTableRow').length).toBe(1); }); it('passes down the executeQueryOptions properly', () => { const props: OuiInMemoryTableProps = { items: [ { active: true, complex: { name: 'Kansas', }, }, { active: true, complex: { name: 'North Dakota', }, }, { active: false, complex: { name: 'Florida', }, }, ], columns: [ { field: 'active', name: 'Is Active', }, { field: 'complex.name', name: 'Name', }, ], search: { onChange: () => {}, defaultQuery: 'No', }, className: 'testTable', message: No items found!, }; const noDefaultFieldsComponent = mount(); // should render with the no items found text expect(noDefaultFieldsComponent.find('.customMessage').length).toBe(1); // With defaultFields and a search query, we should only see one const props2: OuiInMemoryTableProps = { items: [ { active: true, complex: { name: 'Kansas', }, }, { active: true, complex: { name: 'North Dakota', }, }, { active: false, complex: { name: 'Florida', }, }, ], columns: [ { field: 'active', name: 'Is Active', }, { field: 'complex.name', name: 'Name', }, ], search: { onChange: () => {}, defaultQuery: 'No', }, className: 'testTable', message: No items found!, }; const defaultFieldComponent = mount(); expect(defaultFieldComponent.find('.testTable OuiTableRow').length).toBe( 1 ); }); }); describe('custom column sorting', () => { it('calls the sortable function and uses its return value for sorting', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: 7, name: 'Alfred' }, { id: 3, name: 'Betty' }, { id: 5, name: 'Charlie' }, ], itemId: 'id', columns: [ { field: 'name', name: 'Name', sortable: ({ id }: any) => id, }, ], sorting: { sort: { field: 'name', direction: SortDirection.ASC, }, }, }; const component = mount(); expect((component.find('OuiBasicTable').props() as any).items).toEqual([ { id: 3, name: 'Betty' }, { id: 5, name: 'Charlie' }, { id: 7, name: 'Alfred' }, ]); }); }); describe('behavior', () => { test('pagination', async () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, { id: '4', name: 'name4' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', }, ], pagination: { pageSizeOptions: [2, 4, 6], }, }; const component = mount(); component .find('OuiButtonEmpty[data-test-subj="pagination-button-1"]') .simulate('click'); // forces OuiInMemoryTable's getDerivedStateFromProps to re-execute // this is specifically testing regression against https://github.com/elastic/eui/issues/1007 component.setProps({}); expect(component).toMatchSnapshot(); }); test('pagination with actions column and sorting set to true', async () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, { id: '4', name: 'name4' }, ], columns: [ { name: 'Actions', actions: [], }, ], sorting: true, pagination: { pageSizeOptions: [2, 4, 6], }, }; const component = mount(); component .find('OuiButtonEmpty[data-test-subj="pagination-button-1"]') .simulate('click'); }); test('onTableChange callback', () => { const props: OuiInMemoryTableProps = { ...requiredProps, items: [ { id: '1', name: 'name1' }, { id: '2', name: 'name2' }, { id: '3', name: 'name3' }, { id: '4', name: 'name4' }, ], columns: [ { field: 'name', name: 'Name', description: 'description', sortable: true, }, ], sorting: true, pagination: { pageSizeOptions: [2, 4, 6], }, onTableChange: jest.fn(), }; const component = mount(); expect(props.onTableChange).toHaveBeenCalledTimes(0); component .find('OuiButtonEmpty[data-test-subj="pagination-button-1"]') .simulate('click'); expect(props.onTableChange).toHaveBeenCalledTimes(1); expect(props.onTableChange).toHaveBeenCalledWith({ sort: {}, page: { index: 1, size: 2, }, }); (props.onTableChange as jest.Mock).mockClear(); component .find( '[data-test-subj*="tableHeaderCell_name_0"] [data-test-subj="tableHeaderSortButton"]' ) .simulate('click'); expect(props.onTableChange).toHaveBeenCalledTimes(1); expect(props.onTableChange).toHaveBeenCalledWith({ sort: { direction: SortDirection.ASC, field: 'name', }, page: { index: 0, size: 2, }, }); }); }); describe('controlled pagination', () => { it('respects pageIndex', () => { const pagination = { initialPageIndex: 2, pageIndex: 1, pageSizeOptions: [2], }; const items = [ { index: 0 }, { index: 1 }, { index: 2 }, { index: 3 }, { index: 4 }, { index: 5 }, ]; const columns = [ { field: 'index', name: 'Index', }, ]; const onTableChange = jest.fn(); const component = mount( ); // ensure table is on 2nd page (pageIndex=1) expect( component.find('button[data-test-subj="pagination-button-1"][disabled]') .length ).toBe(1); expect(component.find('td').at(0).text()).toBe('Index2'); expect(component.find('td').at(1).text()).toBe('Index3'); // click the first pagination button component .find('OuiButtonEmpty[data-test-subj="pagination-button-0"]') .simulate('click'); expect(onTableChange).toHaveBeenCalledTimes(1); expect(onTableChange).toHaveBeenCalledWith({ sort: {}, page: { index: 0, size: 2, }, }); // ensure table is still on the 2nd page (pageIndex=1) expect( component.find('button[data-test-subj="pagination-button-1"][disabled]') .length ).toBe(1); expect(component.find('td').at(0).text()).toBe('Index2'); expect(component.find('td').at(1).text()).toBe('Index3'); // re-render with an updated `pageIndex` value pagination.pageIndex = 2; component.setProps({ pagination }); // ensure table is on 3rd page (pageIndex=2) expect( component.find('button[data-test-subj="pagination-button-2"][disabled]') .length ).toBe(1); expect(component.find('td').at(0).text()).toBe('Index4'); expect(component.find('td').at(1).text()).toBe('Index5'); }); it('respects pageSize', () => { const pagination = { pageSize: 2, initialPageSize: 4, pageSizeOptions: [1, 2, 4], }; const items = [ { index: 0 }, { index: 1 }, { index: 2 }, { index: 3 }, { index: 4 }, { index: 5 }, ]; const columns = [ { field: 'index', name: 'Index', }, ]; const onTableChange = jest.fn(); const component = mount( ); // check that the first 2 items rendered expect(component.find('td').length).toBe(2); expect(component.find('td').at(0).text()).toBe('Index0'); expect(component.find('td').at(1).text()).toBe('Index1'); // change the page size component .find('button[data-test-subj="tablePaginationPopoverButton"]') .simulate('click'); component.update(); component .find('button[data-test-subj="tablePagination-4-rows"]') .simulate('click'); // check callback expect(onTableChange).toHaveBeenCalledTimes(1); expect(onTableChange).toHaveBeenCalledWith({ sort: {}, page: { index: 0, size: 4, }, }); // verify still only rendering the first 2 rows expect(component.find('td').length).toBe(2); expect(component.find('td').at(0).text()).toBe('Index0'); expect(component.find('td').at(1).text()).toBe('Index1'); // update the controlled page size pagination.pageSize = 4; component.setProps({ pagination }); // verify it now renders 4 rows expect(component.find('td').length).toBe(4); expect(component.find('td').at(0).text()).toBe('Index0'); expect(component.find('td').at(1).text()).toBe('Index1'); expect(component.find('td').at(2).text()).toBe('Index2'); expect(component.find('td').at(3).text()).toBe('Index3'); }); }); });