| /** |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF 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 { styledMount as mount } from 'spec/helpers/theming'; |
| import { act } from 'react-dom/test-utils'; |
| import { QueryParamProvider } from 'use-query-params'; |
| import { supersetTheme, ThemeProvider } from '@superset-ui/core'; |
| |
| import Button from 'src/components/Button'; |
| import { Empty } from 'src/common/components'; |
| import CardCollection from 'src/components/ListView/CardCollection'; |
| import { CardSortSelect } from 'src/components/ListView/CardSortSelect'; |
| import IndeterminateCheckbox from 'src/components/IndeterminateCheckbox'; |
| import ListView from 'src/components/ListView/ListView'; |
| import ListViewFilters from 'src/components/ListView/Filters'; |
| import ListViewPagination from 'src/components/dataViewCommon/Pagination'; |
| import TableCollection from 'src/components/dataViewCommon/TableCollection'; |
| import Pagination from 'src/components/Pagination'; |
| |
| import waitForComponentToPaint from 'spec/helpers/waitForComponentToPaint'; |
| |
| function makeMockLocation(query) { |
| const queryStr = encodeURIComponent(query); |
| return { |
| protocol: 'http:', |
| host: 'localhost', |
| pathname: '/', |
| search: queryStr.length ? `?${queryStr}` : '', |
| }; |
| } |
| |
| const fetchSelectsMock = jest.fn(() => []); |
| const mockedProps = { |
| title: 'Data Table', |
| columns: [ |
| { |
| accessor: 'id', |
| Header: 'ID', |
| sortable: true, |
| }, |
| { |
| accessor: 'age', |
| Header: 'Age', |
| }, |
| { |
| accessor: 'name', |
| Header: 'Name', |
| }, |
| { |
| accessor: 'time', |
| Header: 'Time', |
| }, |
| ], |
| filters: [ |
| { |
| Header: 'ID', |
| id: 'id', |
| input: 'select', |
| selects: [{ label: 'foo', value: 'bar' }], |
| operator: 'eq', |
| }, |
| { |
| Header: 'Name', |
| id: 'name', |
| input: 'search', |
| operator: 'ct', |
| }, |
| { |
| Header: 'Age', |
| id: 'age', |
| input: 'select', |
| fetchSelects: fetchSelectsMock, |
| paginate: true, |
| operator: 'eq', |
| }, |
| { |
| Header: 'Time', |
| id: 'time', |
| input: 'datetime_range', |
| operator: 'between', |
| }, |
| ], |
| data: [ |
| { id: 1, name: 'data 1', age: 10, time: '2020-11-18T07:53:45.354Z' }, |
| { id: 2, name: 'data 2', age: 1, time: '2020-11-18T07:53:45.354Z' }, |
| ], |
| count: 2, |
| pageSize: 1, |
| fetchData: jest.fn(() => []), |
| loading: false, |
| bulkSelectEnabled: true, |
| disableBulkSelect: jest.fn(), |
| bulkActions: [ |
| { |
| key: 'something', |
| name: 'do something', |
| style: 'danger', |
| onSelect: jest.fn(), |
| }, |
| ], |
| cardSortSelectOptions: [ |
| { |
| desc: false, |
| id: 'something', |
| label: 'Alphabetical', |
| value: 'alphabetical', |
| }, |
| ], |
| }; |
| |
| const factory = (props = mockedProps) => |
| mount( |
| <QueryParamProvider location={makeMockLocation()}> |
| <ListView {...props} /> |
| </QueryParamProvider>, |
| { |
| wrappingComponent: ThemeProvider, |
| wrappingComponentProps: { theme: supersetTheme }, |
| }, |
| ); |
| |
| describe('ListView', () => { |
| let wrapper = beforeAll(async () => { |
| wrapper = factory(); |
| await waitForComponentToPaint(wrapper); |
| }); |
| |
| afterEach(() => { |
| mockedProps.fetchData.mockClear(); |
| mockedProps.bulkActions.forEach(ba => { |
| ba.onSelect.mockClear(); |
| }); |
| }); |
| |
| it('calls fetchData on mount', () => { |
| expect(wrapper.find(ListView)).toExist(); |
| expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot( |
| ` |
| Array [ |
| Object { |
| "filters": Array [], |
| "pageIndex": 0, |
| "pageSize": 1, |
| "sortBy": Array [], |
| }, |
| ] |
| `, |
| ); |
| }); |
| |
| it('calls fetchData on sort', () => { |
| wrapper.find('[data-test="sort-header"]').at(1).simulate('click'); |
| expect(mockedProps.fetchData).toHaveBeenCalled(); |
| expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot( |
| ` |
| Array [ |
| Object { |
| "filters": Array [], |
| "pageIndex": 0, |
| "pageSize": 1, |
| "sortBy": Array [ |
| Object { |
| "desc": false, |
| "id": "id", |
| }, |
| ], |
| }, |
| ] |
| `, |
| ); |
| }); |
| |
| it('renders pagination controls', () => { |
| expect(wrapper.find(Pagination)).toExist(); |
| expect(wrapper.find(Pagination.Prev)).toExist(); |
| expect(wrapper.find(Pagination.Item)).toExist(); |
| expect(wrapper.find(Pagination.Next)).toExist(); |
| }); |
| |
| it('calls fetchData on page change', () => { |
| act(() => { |
| wrapper.find(ListViewPagination).prop('onChange')(2); |
| }); |
| wrapper.update(); |
| |
| expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` |
| Array [ |
| Object { |
| "filters": Array [], |
| "pageIndex": 1, |
| "pageSize": 1, |
| "sortBy": Array [ |
| Object { |
| "desc": false, |
| "id": "id", |
| }, |
| ], |
| }, |
| ] |
| `); |
| }); |
| |
| it('handles bulk actions on 1 row', () => { |
| act(() => { |
| wrapper.find('input[id="0"]').at(0).prop('onChange')({ |
| target: { value: 'on' }, |
| }); |
| }); |
| wrapper.update(); |
| |
| act(() => { |
| wrapper |
| .find('[data-test="bulk-select-controls"]') |
| .find(Button) |
| .props() |
| .onClick(); |
| }); |
| |
| expect(mockedProps.bulkActions[0].onSelect.mock.calls[0]) |
| .toMatchInlineSnapshot(` |
| Array [ |
| Array [ |
| Object { |
| "age": 10, |
| "id": 1, |
| "name": "data 1", |
| "time": "2020-11-18T07:53:45.354Z", |
| }, |
| ], |
| ] |
| `); |
| }); |
| |
| it('handles bulk actions on all rows', () => { |
| act(() => { |
| wrapper.find('input[id="header-toggle-all"]').at(0).prop('onChange')({ |
| target: { value: 'on' }, |
| }); |
| }); |
| wrapper.update(); |
| |
| act(() => { |
| wrapper |
| .find('[data-test="bulk-select-controls"]') |
| .find(Button) |
| .props() |
| .onClick(); |
| }); |
| |
| expect(mockedProps.bulkActions[0].onSelect.mock.calls[0]) |
| .toMatchInlineSnapshot(` |
| Array [ |
| Array [ |
| Object { |
| "age": 10, |
| "id": 1, |
| "name": "data 1", |
| "time": "2020-11-18T07:53:45.354Z", |
| }, |
| Object { |
| "age": 1, |
| "id": 2, |
| "name": "data 2", |
| "time": "2020-11-18T07:53:45.354Z", |
| }, |
| ], |
| ] |
| `); |
| }); |
| |
| it('allows deselecting all', async () => { |
| act(() => { |
| wrapper.find('[data-test="bulk-select-deselect-all"]').props().onClick(); |
| }); |
| await waitForComponentToPaint(wrapper); |
| wrapper.update(); |
| wrapper.find(IndeterminateCheckbox).forEach(input => { |
| expect(input.props().checked).toBe(false); |
| }); |
| }); |
| |
| it('allows disabling bulkSelect', () => { |
| wrapper.find('[data-test="bulk-select-controls"]').at(0).props().onClose(); |
| expect(mockedProps.disableBulkSelect).toHaveBeenCalled(); |
| }); |
| |
| it('disables bulk select based on prop', async () => { |
| const wrapper2 = factory({ ...mockedProps, bulkSelectEnabled: false }); |
| await waitForComponentToPaint(wrapper2); |
| expect(wrapper2.find('[data-test="bulk-select-controls"]').exists()).toBe( |
| false, |
| ); |
| }); |
| |
| it('disables card view based on prop', async () => { |
| expect(wrapper.find(CardCollection).exists()).toBe(false); |
| expect(wrapper.find(CardSortSelect).exists()).toBe(false); |
| expect(wrapper.find(TableCollection).exists()).toBe(true); |
| }); |
| |
| it('enables card view based on prop', async () => { |
| const wrapper2 = factory({ |
| ...mockedProps, |
| renderCard: jest.fn(), |
| initialSort: [{ id: 'something' }], |
| }); |
| await waitForComponentToPaint(wrapper2); |
| expect(wrapper2.find(CardCollection).exists()).toBe(true); |
| expect(wrapper2.find(CardSortSelect).exists()).toBe(true); |
| expect(wrapper2.find(TableCollection).exists()).toBe(false); |
| }); |
| |
| it('allows setting the default view mode', async () => { |
| const wrapper2 = factory({ |
| ...mockedProps, |
| renderCard: jest.fn(), |
| defaultViewMode: 'card', |
| initialSort: [{ id: 'something' }], |
| }); |
| await waitForComponentToPaint(wrapper2); |
| expect(wrapper2.find(CardCollection).exists()).toBe(true); |
| |
| const wrapper3 = factory({ |
| ...mockedProps, |
| renderCard: jest.fn(), |
| defaultViewMode: 'table', |
| initialSort: [{ id: 'something' }], |
| }); |
| await waitForComponentToPaint(wrapper3); |
| expect(wrapper3.find(TableCollection).exists()).toBe(true); |
| }); |
| |
| it('Throws an exception if filter missing in columns', () => { |
| expect.assertions(1); |
| const props = { |
| ...mockedProps, |
| filters: [...mockedProps.filters, { id: 'some_column' }], |
| }; |
| expect(() => { |
| mount(<ListView {...props} />, { |
| wrappingComponent: ThemeProvider, |
| wrappingComponentProps: { theme: supersetTheme }, |
| }); |
| }).toThrowErrorMatchingInlineSnapshot( |
| '"Invalid filter config, some_column is not present in columns"', |
| ); |
| }); |
| |
| it('renders and empty state when there is no data', async () => { |
| const props = { |
| ...mockedProps, |
| data: [], |
| }; |
| |
| const wrapper2 = factory(props); |
| await waitForComponentToPaint(wrapper2); |
| expect(wrapper2.find(Empty)).toExist(); |
| }); |
| |
| it('renders UI filters', () => { |
| expect(wrapper.find(ListViewFilters)).toExist(); |
| }); |
| |
| it('fetched async filter values on mount', () => { |
| expect(fetchSelectsMock).toHaveBeenCalled(); |
| }); |
| |
| it('calls fetchData on filter', () => { |
| act(() => { |
| wrapper |
| .find('[data-test="filters-select"]') |
| .first() |
| .props() |
| .onChange({ value: 'bar' }); |
| }); |
| |
| act(() => { |
| wrapper |
| .find('[data-test="filters-search"]') |
| .first() |
| .props() |
| .onChange({ currentTarget: { value: 'something' } }); |
| }); |
| |
| wrapper.update(); |
| |
| act(() => { |
| wrapper.find('[data-test="search-input"]').last().props().onBlur(); |
| }); |
| |
| expect(mockedProps.fetchData.mock.calls[0]).toMatchInlineSnapshot(` |
| Array [ |
| Object { |
| "filters": Array [ |
| Object { |
| "id": "id", |
| "operator": "eq", |
| "value": "bar", |
| }, |
| ], |
| "pageIndex": 0, |
| "pageSize": 1, |
| "sortBy": Array [ |
| Object { |
| "desc": false, |
| "id": "id", |
| }, |
| ], |
| }, |
| ] |
| `); |
| |
| expect(mockedProps.fetchData.mock.calls[1]).toMatchInlineSnapshot(` |
| Array [ |
| Object { |
| "filters": Array [ |
| Object { |
| "id": "id", |
| "operator": "eq", |
| "value": "bar", |
| }, |
| Object { |
| "id": "name", |
| "operator": "ct", |
| "value": "something", |
| }, |
| ], |
| "pageIndex": 0, |
| "pageSize": 1, |
| "sortBy": Array [ |
| Object { |
| "desc": false, |
| "id": "id", |
| }, |
| ], |
| }, |
| ] |
| `); |
| }); |
| |
| it('calls fetchData on card view sort', async () => { |
| const wrapper2 = factory({ |
| ...mockedProps, |
| renderCard: jest.fn(), |
| initialSort: [{ id: 'something' }], |
| }); |
| |
| await act(async () => { |
| wrapper2.find('[data-test="card-sort-select"]').first().props().onChange({ |
| desc: false, |
| id: 'something', |
| label: 'Alphabetical', |
| value: 'alphabetical', |
| }); |
| }); |
| |
| expect(mockedProps.fetchData).toHaveBeenCalled(); |
| }); |
| }); |