From 00185b80ca5201141ade033671d0e20eab0f9d82 Mon Sep 17 00:00:00 2001 From: AllenFang Date: Wed, 13 Dec 2017 22:54:59 +0800 Subject: [PATCH] add react-bootstrap-table2-filter --- .../.storybook/webpack.config.js | 3 +- .../package.json | 3 +- .../src/components/text.js | 34 +++- .../test/components/text.test.js | 190 ++++++++++++++++++ .../test/filter.test.js | 60 ++++++ .../test/wrapper.test.js | 168 ++++++++++++++++ 6 files changed, 446 insertions(+), 12 deletions(-) create mode 100644 packages/react-bootstrap-table2-filter/test/components/text.test.js create mode 100644 packages/react-bootstrap-table2-filter/test/filter.test.js create mode 100644 packages/react-bootstrap-table2-filter/test/wrapper.test.js diff --git a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js index 8ebc72a..25f09d8 100644 --- a/packages/react-bootstrap-table2-example/.storybook/webpack.config.js +++ b/packages/react-bootstrap-table2-example/.storybook/webpack.config.js @@ -3,6 +3,7 @@ const path = require('path'); const sourcePath = path.join(__dirname, '../../react-bootstrap-table2/src'); const paginationSourcePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/src'); const overlaySourcePath = path.join(__dirname, '../../react-bootstrap-table2-overlay/src'); +const filterSourcePath = path.join(__dirname, '../../react-bootstrap-table2-filter/src'); const sourceStylePath = path.join(__dirname, '../../react-bootstrap-table2/style'); const paginationStylePath = path.join(__dirname, '../../react-bootstrap-table2-paginator/style'); const storyPath = path.join(__dirname, '../stories'); @@ -26,7 +27,7 @@ const loaders = [{ test: /\.js?$/, use: ['babel-loader'], exclude: /node_modules/, - include: [sourcePath, paginationSourcePath, overlaySourcePath, storyPath], + include: [sourcePath, paginationSourcePath, overlaySourcePath, filterSourcePath, storyPath], }, { test: /\.css$/, use: ['style-loader', 'css-loader'], diff --git a/packages/react-bootstrap-table2-example/package.json b/packages/react-bootstrap-table2-example/package.json index a726ef5..419f884 100644 --- a/packages/react-bootstrap-table2-example/package.json +++ b/packages/react-bootstrap-table2-example/package.json @@ -19,7 +19,8 @@ "bootstrap": "^3.3.7", "react-bootstrap-table2": "0.0.1", "react-bootstrap-table2-paginator": "0.0.1", - "react-bootstrap-table2-overlay": "0.0.1" + "react-bootstrap-table2-overlay": "0.0.1", + "react-bootstrap-table2-filter": "0.0.1" }, "devDependencies": { "@storybook/addon-console": "^1.0.0", diff --git a/packages/react-bootstrap-table2-filter/src/components/text.js b/packages/react-bootstrap-table2-filter/src/components/text.js index 48ab658..1c102c3 100644 --- a/packages/react-bootstrap-table2-filter/src/components/text.js +++ b/packages/react-bootstrap-table2-filter/src/components/text.js @@ -1,5 +1,5 @@ /* eslint react/require-default-props: 0 */ -/* eslint react/no-unused-prop-types: 0 */ +/* eslint react/prop-types: 0 */ /* eslint no-return-assign: 0 */ import React, { Component } from 'react'; import { PropTypes } from 'prop-types'; @@ -11,6 +11,7 @@ class TextFilter extends Component { constructor(props) { super(props); this.filter = this.filter.bind(this); + this.handleClick = this.handleClick.bind(this); this.timeout = null; this.state = { value: props.defaultValue @@ -30,14 +31,12 @@ class TextFilter extends Component { } componentWillUnmount() { - clearTimeout(this.timeout); + this.cleanTimer(); } filter(e) { e.stopPropagation(); - if (this.timeout) { - clearTimeout(this.timeout); - } + this.cleanTimer(); const filterValue = e.target.value; this.setState(() => ({ value: filterValue })); this.timeout = setTimeout(() => { @@ -45,6 +44,12 @@ class TextFilter extends Component { }, this.props.delay); } + cleanTimer() { + if (this.timeout) { + clearTimeout(this.timeout); + } + } + cleanFiltered() { const value = this.props.defaultValue; this.setState(() => ({ value })); @@ -56,17 +61,25 @@ class TextFilter extends Component { this.props.onFilter(this.props.column, filterText, FILTER_TYPE.TEXT); } + handleClick(e) { + e.stopPropagation(); + if (this.props.onClick) { + this.props.onClick(e); + } + } + render() { - const { placeholder, column: { text }, style } = this.props; + const { placeholder, column: { text }, style, className, onFilter, ...rest } = this.props; // stopPropagation for onClick event is try to prevent sort was triggered. return ( this.input = n } type="text" - className="filter text-filter form-control" + className={ `filter text-filter form-control ${className}` } style={ style } onChange={ this.filter } - onClick={ e => e.stopPropagation() } + onClick={ this.handleClick } placeholder={ placeholder || `Enter ${text}...` } value={ this.state.value } /> @@ -76,12 +89,13 @@ class TextFilter extends Component { TextFilter.propTypes = { onFilter: PropTypes.func.isRequired, + column: PropTypes.object.isRequired, comparator: PropTypes.oneOf([LIKE, EQ]), defaultValue: PropTypes.string, delay: PropTypes.number, placeholder: PropTypes.string, - column: PropTypes.object, - style: PropTypes.object + style: PropTypes.object, + className: PropTypes.string }; TextFilter.defaultProps = { diff --git a/packages/react-bootstrap-table2-filter/test/components/text.test.js b/packages/react-bootstrap-table2-filter/test/components/text.test.js new file mode 100644 index 0000000..4a9c282 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/test/components/text.test.js @@ -0,0 +1,190 @@ +import 'jsdom-global/register'; +import React from 'react'; +import sinon from 'sinon'; +import { mount } from 'enzyme'; +import TextFilter from '../../src/components/text'; +import { FILTER_TYPE } from '../../src/const'; + +jest.useFakeTimers(); +describe('Text Filter', () => { + let wrapper; + let instance; + const onFilter = sinon.stub(); + const column = { + dataField: 'price', + text: 'Price' + }; + + afterEach(() => { + onFilter.reset(); + }); + + describe('initialization', () => { + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + }); + + it('should have correct state', () => { + expect(instance.state.value).toEqual(instance.props.defaultValue); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('input[type="text"]')).toHaveLength(1); + expect(instance.input.getAttribute('placeholder')).toEqual(`Enter ${column.text}...`); + }); + }); + + describe('when defaultValue is defined', () => { + const defaultValue = '123'; + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + }); + + it('should have correct state', () => { + expect(instance.state.value).toEqual(defaultValue); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(instance.input.value).toEqual(defaultValue); + }); + + it('should calling onFilter on componentDidMount', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith(column, defaultValue, FILTER_TYPE.TEXT)).toBeTruthy(); + }); + }); + + describe('when placeholder is defined', () => { + const placeholder = 'test'; + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(instance.input.getAttribute('placeholder')).toEqual(placeholder); + }); + }); + + describe('when style is defined', () => { + const style = { backgroundColor: 'red' }; + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + }); + + it('should rendering component successfully', () => { + expect(wrapper).toHaveLength(1); + expect(wrapper.find('input').prop('style')).toEqual(style); + }); + }); + + describe('componentWillReceiveProps', () => { + const nextDefaultValue = 'tester'; + const nextProps = { + onFilter, + column, + defaultValue: nextDefaultValue + }; + + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting state correctly when props.defaultValue is changed', () => { + expect(instance.state.value).toEqual(nextDefaultValue); + }); + + it('should calling onFilter correctly when props.defaultValue is changed', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith(column, nextDefaultValue, FILTER_TYPE.TEXT)).toBeTruthy(); + }); + }); + + describe('cleanFiltered', () => { + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + instance.cleanFiltered(); + }); + + it('should setting state correctly', () => { + expect(instance.state.value).toEqual(instance.props.defaultValue); + }); + + it('should calling onFilter correctly', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith( + column, instance.props.defaultValue, FILTER_TYPE.TEXT)).toBeTruthy(); + }); + }); + + describe('applyFilter', () => { + const filterText = 'test'; + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + instance.applyFilter(filterText); + }); + + it('should setting state correctly', () => { + expect(instance.state.value).toEqual(filterText); + }); + + it('should calling onFilter correctly', () => { + expect(onFilter.calledOnce).toBeTruthy(); + expect(onFilter.calledWith(column, filterText, FILTER_TYPE.TEXT)).toBeTruthy(); + }); + }); + + describe('filter', () => { + const event = { stopPropagation: sinon.stub(), target: { value: 'tester' } }; + + beforeEach(() => { + wrapper = mount( + + ); + instance = wrapper.instance(); + instance.filter(event); + }); + + afterEach(() => { + setTimeout.mockClear(); + }); + + it('should calling e.stopPropagation', () => { + expect(event.stopPropagation.calledOnce).toBeTruthy(); + }); + + it('should setting state correctly', () => { + expect(instance.state.value).toEqual(event.target.value); + }); + + it('should calling setTimeout correctly', () => { + expect(setTimeout.mock.calls).toHaveLength(1); + expect(setTimeout.mock.calls[0]).toHaveLength(2); + expect(setTimeout.mock.calls[0][1]).toEqual(instance.props.delay); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-filter/test/filter.test.js b/packages/react-bootstrap-table2-filter/test/filter.test.js new file mode 100644 index 0000000..7f55a55 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/test/filter.test.js @@ -0,0 +1,60 @@ +import _ from 'react-bootstrap-table2/src/utils'; +import Store from 'react-bootstrap-table2/src/store'; + +import { filters } from '../src/filter'; +import { FILTER_TYPE } from '../src/const'; +import { LIKE, EQ } from '../src/comparison'; + +const data = []; +for (let i = 0; i < 20; i += 1) { + data.push({ + id: i, + name: `itme name ${i}`, + price: 200 + i + }); +} + +describe('filter', () => { + let store; + let filterFn; + let currFilters; + + beforeEach(() => { + store = new Store('id'); + store.data = data; + currFilters = {}; + }); + + describe('text filter', () => { + beforeEach(() => { + filterFn = filters(store, _); + }); + + describe(`when default comparator is ${LIKE}`, () => { + it('should returning correct result', () => { + currFilters.name = { + filterVal: '3', + filterType: FILTER_TYPE.TEXT + }; + + const result = filterFn(currFilters); + expect(result).toBeDefined(); + expect(result).toHaveLength(2); + }); + }); + + describe(`when default comparator is ${EQ}`, () => { + it('should returning correct result', () => { + currFilters.name = { + filterVal: 'itme name 3', + filterType: FILTER_TYPE.TEXT, + comparator: EQ + }; + + const result = filterFn(currFilters); + expect(result).toBeDefined(); + expect(result).toHaveLength(1); + }); + }); + }); +}); diff --git a/packages/react-bootstrap-table2-filter/test/wrapper.test.js b/packages/react-bootstrap-table2-filter/test/wrapper.test.js new file mode 100644 index 0000000..c877c62 --- /dev/null +++ b/packages/react-bootstrap-table2-filter/test/wrapper.test.js @@ -0,0 +1,168 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import _ from 'react-bootstrap-table2/src/utils'; +import BootstrapTable from 'react-bootstrap-table2/src/bootstrap-table'; +import Store from 'react-bootstrap-table2/src/store'; +import filter, { textFilter } from '../src'; +import FilterWrapper from '../src/wrapper'; +import { FILTER_TYPE } from '../src/const'; + +const data = []; +for (let i = 0; i < 20; i += 1) { + data.push({ + id: i, + name: `itme name ${i}`, + price: 200 + i + }); +} + +describe('Wrapper', () => { + let wrapper; + let instance; + + + const createTableProps = () => { + const tableProps = { + keyField: 'id', + columns: [{ + dataField: 'id', + text: 'ID' + }, { + dataField: 'name', + text: 'Name', + filter: textFilter() + }, { + dataField: 'price', + text: 'Price', + filter: textFilter() + }], + data, + filter: filter(), + _, + store: new Store('id') + }; + tableProps.store.data = data; + return tableProps; + }; + + const pureTable = props => (); + + const createFilterWrapper = (props, renderFragment = true) => { + wrapper = shallow(); + instance = wrapper.instance(); + if (renderFragment) { + const fragment = instance.render(); + wrapper = shallow(
{ fragment }
); + } + }; + + describe('default filter wrapper', () => { + const props = createTableProps(); + + beforeEach(() => { + createFilterWrapper(props); + }); + + it('should rendering correctly', () => { + expect(wrapper.length).toBe(1); + }); + + it('should initializing state correctly', () => { + expect(instance.state.isDataChanged).toBeFalsy(); + expect(instance.state.currFilters).toEqual({}); + }); + + it('should rendering BootstraTable correctly', () => { + const table = wrapper.find(BootstrapTable); + expect(table.length).toBe(1); + expect(table.prop('onFilter')).toBeDefined(); + expect(table.prop('isDataChanged')).toEqual(instance.state.isDataChanged); + }); + }); + + describe('componentWillReceiveProps', () => { + let nextProps; + + beforeEach(() => { + nextProps = createTableProps(); + instance.componentWillReceiveProps(nextProps); + }); + + it('should setting isDataChanged as false always(Temporary solution)', () => { + expect(instance.state.isDataChanged).toBeFalsy(); + }); + }); + + describe('onFilter', () => { + let props; + + beforeEach(() => { + props = createTableProps(); + createFilterWrapper(props); + }); + + describe('when filterVal is empty or undefined', () => { + const filterVals = ['', undefined]; + + it('should setting store object correctly', () => { + filterVals.forEach((filterVal) => { + instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeFalsy(); + }); + }); + + it('should setting state correctly', () => { + filterVals.forEach((filterVal) => { + instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(0); + }); + }); + }); + + describe('when filterVal is existing', () => { + const filterVal = '3'; + + it('should setting store object correctly', () => { + instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + }); + + it('should setting state correctly', () => { + instance.onFilter(props.columns[1], filterVal, FILTER_TYPE.TEXT); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(1); + }); + }); + + describe('combination', () => { + it('should setting store object correctly', () => { + instance.onFilter(props.columns[1], '3', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(1); + + instance.onFilter(props.columns[1], '2', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(1); + + instance.onFilter(props.columns[2], '2', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(2); + + instance.onFilter(props.columns[2], '', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeTruthy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(1); + + instance.onFilter(props.columns[1], '', FILTER_TYPE.TEXT); + expect(props.store.filtering).toBeFalsy(); + expect(instance.state.isDataChanged).toBeTruthy(); + expect(Object.keys(instance.state.currFilters)).toHaveLength(0); + }); + }); + }); +});