Repository: aurora Updated Branches: refs/heads/master abd6fad61 -> 7c78519ef
Replace Preact and custom testing with React + Enzyme Reviewed at https://reviews.apache.org/r/62607/ Project: http://git-wip-us.apache.org/repos/asf/aurora/repo Commit: http://git-wip-us.apache.org/repos/asf/aurora/commit/7c78519e Tree: http://git-wip-us.apache.org/repos/asf/aurora/tree/7c78519e Diff: http://git-wip-us.apache.org/repos/asf/aurora/diff/7c78519e Branch: refs/heads/master Commit: 7c78519ef9ba08f7b8426ec9295245e9480dcf2b Parents: abd6fad Author: David McLaughlin <[email protected]> Authored: Wed Sep 27 14:34:02 2017 -0700 Committer: David McLaughlin <[email protected]> Committed: Wed Sep 27 14:34:02 2017 -0700 ---------------------------------------------------------------------- build.gradle | 4 +- ui/karma.conf.js | 23 --- ui/package.json | 30 +-- ui/src/__mocks__/react.js | 9 + ui/src/main/js/components/Pagination.js | 101 ++++++++++ ui/src/main/js/components/RoleList.js | 68 ++++--- .../js/components/__tests__/Breadcrumb-test.js | 23 +-- .../main/js/components/__tests__/Home-test.js | 8 +- .../js/components/__tests__/Pagination-test.js | 195 +++++++++++++++++++ ui/src/main/js/pages/__tests__/Home-test.js | 12 +- ui/src/main/js/utils/ShallowRender.js | 160 --------------- .../js/utils/__tests__/ShallowRender-test.js | 86 -------- ui/src/main/sass/components/_tables.scss | 73 +++---- ui/test-setup.js | 5 + ui/webpack.config.js | 4 - 15 files changed, 418 insertions(+), 383 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/build.gradle ---------------------------------------------------------------------- diff --git a/build.gradle b/build.gradle index 460500a..f9579a3 100644 --- a/build.gradle +++ b/build.gradle @@ -15,7 +15,7 @@ plugins { id 'com.eriwen.gradle.js' version '1.12.1' id 'com.github.ben-manes.versions' version '0.11.3' id 'com.github.hierynomus.license' version '0.11.0' - id 'com.moowork.node' version '1.1.1' + id 'com.moowork.node' version '1.2.0' id 'me.champeau.gradle.jmh' version '0.4.4' } @@ -155,7 +155,7 @@ project(':ui') { task lint(type: NpmTask, dependsOn: 'install') { inputs.files(fileTree('src')) outputs.files(fileTree('.')) - args = ['run-script', 'lint'] + args = ['run', 'lint'] } task webpack(type: NodeTask, dependsOn: 'install') { http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/karma.conf.js ---------------------------------------------------------------------- diff --git a/ui/karma.conf.js b/ui/karma.conf.js deleted file mode 100644 index d42cda7..0000000 --- a/ui/karma.conf.js +++ /dev/null @@ -1,23 +0,0 @@ -var webpackConfig = require('./webpack.config.js'); - -module.exports = function(config) { - config.set({ - basePath: '', - frameworks: ['jasmine'], - files: [ - 'src/**/*-test.js' - ], - preprocessors: { - 'src/**/*-test.js': ['webpack'] - }, - reporters: ['spec'], - webpack: webpackConfig, - port: 9876, - colors: true, - logLevel: config.LOG_INFO, - autoWatch: true, - browsers: ['PhantomJS'], - singleRun: true, - concurrency: Infinity - }) -} http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/package.json ---------------------------------------------------------------------- diff --git a/ui/package.json b/ui/package.json index d680202..fe0397a 100644 --- a/ui/package.json +++ b/ui/package.json @@ -4,22 +4,23 @@ "description": "UI project for Apache Aurora", "main": "index.js", "dependencies": { - "preact": "^8.2.1", - "preact-compat": "^3.17.0", - "react-router-dom": "^4.1.2", - "reactable": "^0.14.1" + "react": "^16.0.0", + "react-dom": "^16.0.0", + "react-router-dom": "^4.2.2" }, "devDependencies": { "babel-core": "^6.26.0", "babel-eslint": "^7.2.3", + "babel-jest": "^21.2.0", "babel-loader": "^7.1.1", "babel-plugin-react-transform": "^2.0.2", "babel-plugin-transform-react-jsx": "^6.24.1", "babel-preset-es2015": "^6.24.1", "babel-preset-react": "^6.24.1", - "chai": "^4.1.1", "css-loader": "^0.28.5", "deep-equal": "^1.0.1", + "enzyme": "^3.0.0", + "enzyme-adapter-react-16": "^1.0.0", "eslint": "^4.4.1", "eslint-config-standard": "^10.2.1", "eslint-config-standard-react": "^5.0.0", @@ -29,22 +30,23 @@ "eslint-plugin-promise": "^3.5.0", "eslint-plugin-react": "^7.2.1", "eslint-plugin-standard": "^3.0.1", - "jasmine-core": "^2.7.0", - "karma": "^1.7.0", - "karma-cli": "^1.0.1", - "karma-jasmine": "^1.1.0", - "karma-phantomjs-launcher": "^1.0.4", - "karma-spec-reporter": "0.0.31", - "karma-webpack": "^2.0.4", + "jest": "^21.2.0", + "jest-cli": "^21.2.0", "node-sass": "^4.5.3", - "preact-jsx-chai": "^2.2.1", + "react-test-renderer": "^16.0.0", "sass-loader": "^6.0.6", "style-loader": "^0.18.2", "webpack": "^2.6.1" }, + "jest": { + "moduleDirectories": ["./src/main/js", "node_modules"], + "setupFiles": [ + "./test-setup.js" + ] + }, "scripts": { "lint": "eslint src/main/js --ext .js", - "test": "NODE_ENV=test karma start karma.conf.js" + "test": "jest src/" }, "repository": { "type": "git", http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/__mocks__/react.js ---------------------------------------------------------------------- diff --git a/ui/src/__mocks__/react.js b/ui/src/__mocks__/react.js new file mode 100644 index 0000000..6362fe5 --- /dev/null +++ b/ui/src/__mocks__/react.js @@ -0,0 +1,9 @@ +const react = require('react'); +// Resolution for requestAnimationFrame not supported in jest error : +// https://github.com/facebook/react/issues/9102#issuecomment-283873039 +global.window = global; + window.addEventListener = () => {}; + window.requestAnimationFrame = () => { + throw new Error('requestAnimationFrame is not supported in Node'); +}; +module.exports = react; \ No newline at end of file http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/js/components/Pagination.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/Pagination.js b/ui/src/main/js/components/Pagination.js new file mode 100644 index 0000000..dec89be --- /dev/null +++ b/ui/src/main/js/components/Pagination.js @@ -0,0 +1,101 @@ +import React from 'react'; + +export function PageNavigation({currentPage, maxPages, numPages, onClick}) { + // Pad the current page on both sides by one half of maxPages + const lastPage = Math.min(currentPage + Math.round(maxPages / 2), numPages); + const firstPage = Math.max(currentPage - Math.round(maxPages / 2), 1); + + const pages = []; + for (let i = firstPage; i <= lastPage; i++) { + if (i === currentPage) { + pages.push(<li className='active' key={i}><span>{i}</span></li>); + } else { + pages.push(<li key={i}><a onClick={(e) => onClick(i)}>{i}</a></li>); + } + } + + const prevPage = (currentPage > 1) + ? <li key='prev'><a onClick={(e) => onClick(currentPage - 1)}>«</a></li> + : ''; + const nextPage = (currentPage < numPages) + ? <li key='next'><a onClick={(e) => onClick(currentPage + 1)}>»</a></li> + : ''; + + return (<ul className='pagination'> + {prevPage} + {pages} + {nextPage} + </ul>); +} + +export default class Pagination extends React.Component { + constructor(props) { + super(props); + this.state = {page: props.page || 1}; + } + + componentWillReceiveProps(nextProps) { + // Make sure to reset page when props change (caused by data change or sort change or filter) + if (this.state.page > 1) { + this.setState({page: 1}); + } + } + + changePage(page) { + this.setState({page}); + } + + filter(data) { + if (this.props.filter) { + return data.filter(this.props.filter); + } + return data; + } + + sort(data) { + const { reverseSort, sortBy } = this.props; + const gte = reverseSort ? -1 : 1; + const lte = reverseSort ? 1 : -1; + return data.sort((a, b) => { + return (a[sortBy] > b[sortBy]) ? gte : lte; + }); + } + + render() { + const that = this; + const { data, isTable, maxPages, numberPerPage, renderer } = this.props; + const { page } = this.state; + + // Apply the filter before we try to paginate. + const filtered = this.filter(data); + + // Figure out the slice of the array that represents the current page. + const firstIdx = (page - 1) * numberPerPage; + const lastIdx = firstIdx + numberPerPage; + const currentPageItems = this.sort(filtered).slice(firstIdx, lastIdx); + + // A cleaner interface would be to just pass in a React Component as the renderer: + // <Pagination ... renderer={MyItem} /> + // And then pass in the data as props to their component. Which we can support with: + // currentPage.map((item) => React.createElement(renderer, item)) + // but first attempts at this broke shallow rendering in enzyme. + const elements = currentPageItems.map(renderer); + + // The clickable page list. + const pagination = <PageNavigation + currentPage={page} + maxPages={maxPages || 8} + numPages={Math.ceil(filtered.length / numberPerPage)} + onClick={(page) => that.changePage(page)} />; + + // React/JSX statements must resolve to a single node, so we need to wrap the page in a parent. + // We need the caller to be able to signify they are paging through a table element. + if (isTable) { + return (<tbody> + {elements} + <tr className='pagination-row'><td colSpan='100%'>{pagination}</td></tr> + </tbody>); + } + return <div>{elements}{pagination}</div>; + } +} http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/js/components/RoleList.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/RoleList.js b/ui/src/main/js/components/RoleList.js index 3259560..cffb012 100644 --- a/ui/src/main/js/components/RoleList.js +++ b/ui/src/main/js/components/RoleList.js @@ -1,12 +1,35 @@ import React from 'react'; import { Link } from 'react-router-dom'; -import Reactable, { Table, Tr, Thead, Th, Td } from 'reactable'; import Icon from 'components/Icon'; +import Pagination from 'components/Pagination'; export default class RoleList extends React.Component { + constructor(props) { + super(props); + this.state = { + filter: props.filter, + reverseSort: props.reverseSort || false, + sortBy: props.sortBy || 'role' + }; + } + setFilter(e) { - this.setState({filter: e.target.value}); + this.setState({filter: e.target.value, sortBy: 'role'}); + } + + setSort(sortBy) { + // If they change sort key, it's always ascending the first time. + const reverseSort = (sortBy === this.state.sortBy) ? !this.state.reverseSort : false; + this.setState({reverseSort, sortBy}); + } + + _renderRow(r) { + return (<tr key={r.role}> + <td><Link to={`/beta/scheduler/${r.role}`}>{r.role}</Link></td> + <td>{r.jobCount}</td> + <td>{r.cronJobCount}</td> + </tr>); } render() { @@ -19,30 +42,23 @@ export default class RoleList extends React.Component { placeholder='Search for roles' type='text' /> </div> - <Table - className='aurora-table' - defaultSort={{column: 'role'}} - filterBy={this.state.filter} - filterPlaceholder='Search roles...' - filterable={['role']} - hideFilterInput - itemsPerPage={25} - noDataText={'No results found.'} - pageButtonLimit={8} - sortable={['role', - {'column': 'jobs', sortFunction: Reactable.Sort.Numeric}, - {'column': 'crons', sortFunction: Reactable.Sort.Numeric}]}> - <Thead> - <Th column='role'>Role</Th> - <Th className='number' column='jobs'>Jobs</Th> - <Th className='number' column='crons'>Crons</Th> - </Thead> - {this.props.roles.map((r) => (<Tr key={r.role}> - <Td column='role' value={r.role}><Link to={`/scheduler/${r.role}`}>{r.role}</Link></Td> - <Td className='number' column='jobs'>{r.jobCount}</Td> - <Td className='number' column='crons'>{r.cronJobCount}</Td> - </Tr>))} - </Table> + <table className='aurora-table'> + <thead> + <tr> + <th onClick={(e) => this.setSort('role')}>Role</th> + <th onClick={(e) => this.setSort('jobCount')}>Jobs</th> + <th onClick={(e) => this.setSort('cronJobCount')}>Crons</th> + </tr> + </thead> + <Pagination + data={this.props.roles} + filter={(r) => (this.state.filter) ? r.role.startsWith(this.state.filter) : true} + isTable + numberPerPage={25} + renderer={this._renderRow} + reverseSort={this.state.reverseSort} + sortBy={this.state.sortBy} /> + </table> </div>); } } http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/js/components/__tests__/Breadcrumb-test.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/__tests__/Breadcrumb-test.js b/ui/src/main/js/components/__tests__/Breadcrumb-test.js index 18af0fe..47f7afb 100644 --- a/ui/src/main/js/components/__tests__/Breadcrumb-test.js +++ b/ui/src/main/js/components/__tests__/Breadcrumb-test.js @@ -1,35 +1,32 @@ import React from 'react'; +import { shallow } from 'enzyme'; + import { Link } from 'react-router-dom'; import Breadcrumb from '../Breadcrumb'; -import shallow from 'utils/ShallowRender'; - -import chai, { expect } from 'chai'; -import assertJsx from 'preact-jsx-chai'; -chai.use(assertJsx); describe('Breadcrumb', () => { it('Should render cluster crumb', () => { const el = shallow(<Breadcrumb cluster='devcluster' />); - expect(el.contains(<Link to='/scheduler'>devcluster</Link>)).to.be.true; - expect(el.find(<Link />).length === 1).to.be.true; + expect(el.contains(<Link to='/scheduler'>devcluster</Link>)).toBe(true); + expect(el.find(Link).length).toBe(1); }); it('Should render role crumb', () => { const el = shallow(<Breadcrumb cluster='devcluster' role='www-data' />); - expect(el.contains(<Link to='/scheduler/www-data'>www-data</Link>)).to.be.true; - expect(el.find(<Link />).length === 2).to.be.true; + expect(el.contains(<Link to='/scheduler/www-data'>www-data</Link>)).toBe(true); + expect(el.find(Link).length).toBe(2); }); it('Should render env crumb', () => { const el = shallow(<Breadcrumb cluster='devcluster' env='prod' role='www-data' />); - expect(el.contains(<Link to='/scheduler/www-data/prod'>prod</Link>)).to.be.true; - expect(el.find(<Link />).length === 3).to.be.true; + expect(el.contains(<Link to='/scheduler/www-data/prod'>prod</Link>)).toBe(true); + expect(el.find(Link).length).toBe(3); }); it('Should render name crumb', () => { const el = shallow(<Breadcrumb cluster='devcluster' env='prod' name='hello' role='www-data' />); - expect(el.contains(<Link to='/scheduler/www-data/prod/hello'>hello</Link>)).to.be.true; - expect(el.find(<Link />).length === 4).to.be.true; + expect(el.contains(<Link to='/scheduler/www-data/prod/hello'>hello</Link>)).toBe(true); + expect(el.find(Link).length).toBe(4); }); }); http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/js/components/__tests__/Home-test.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/__tests__/Home-test.js b/ui/src/main/js/components/__tests__/Home-test.js index 8e6bc09..dde72fc 100644 --- a/ui/src/main/js/components/__tests__/Home-test.js +++ b/ui/src/main/js/components/__tests__/Home-test.js @@ -1,12 +1,10 @@ import React from 'react'; -import Home from '../Home'; +import { shallow } from 'enzyme'; -import chai, { expect } from 'chai'; -import assertJsx from 'preact-jsx-chai'; -chai.use(assertJsx); +import Home from '../Home'; describe('Home', () => { it('Should render Hello, World!', () => { - expect(<Home />).to.deep.equal(<div>Hello, World!</div>); + expect(shallow(<Home />).equals(<div>Hello, World!</div>)).toBe(true); }); }); http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/js/components/__tests__/Pagination-test.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/components/__tests__/Pagination-test.js b/ui/src/main/js/components/__tests__/Pagination-test.js new file mode 100644 index 0000000..f2b72e9 --- /dev/null +++ b/ui/src/main/js/components/__tests__/Pagination-test.js @@ -0,0 +1,195 @@ +import React from 'react'; +import { shallow } from 'enzyme'; + +import Pagination, { PageNavigation } from '../Pagination'; + +const data = [ + {row: 1, name: 'one'}, + {row: 2, name: 'two'}, + {row: 3, name: 'three'}, + {row: 4, name: 'four'}, + {row: 5, name: 'five'}, + {row: 6, name: 'six'}, + {row: 7, name: 'seven'}, + {row: 8, name: 'eight'}, + {row: 9, name: 'nine'}, + {row: 10, name: 'ten'} +]; + +function Row({ data }) { + return <span>{data.row} - {data.name}</span>; +} + +function render(data) { + return <Row data={data} key={data.row} />; +} + +describe('Pagination', () => { + it('Should render the first page by default', () => { + const el = shallow(<Pagination data={data} numberPerPage={3} renderer={render} />); + expect(el.find(Row).length).toBe(3); + expect(el.is('div')).toBe(true); + expect(el.containsAllMatchingElements([ + <Row data={data[0]} key={1} />, + <Row data={data[1]} key={2} />, + <Row data={data[2]} key={3} />, + <PageNavigation currentPage={1} numPages={4} />])).toBe(true); + }); + + it('Should render other pages when set as props', () => { + const el = shallow(<Pagination data={data} numberPerPage={3} page={2} renderer={render} />); + expect(el.find(Row).length).toBe(3); + expect(el.containsAllMatchingElements([ + <Row data={data[3]} key={4} />, + <Row data={data[4]} key={5} />, + <Row data={data[5]} key={6} />, + <PageNavigation currentPage={2} numPages={4} />])).toBe(true); + }); + + it('Should handle a single page', () => { + const el = shallow(<Pagination data={data} numberPerPage={25} renderer={render} />); + expect(el.find(Row).length).toBe(10); + expect( + el.containsAllMatchingElements([<PageNavigation currentPage={1} numPages={1} />])).toBe(true); + }); + + it('Should sort correctly', () => { + const el = shallow( + <Pagination data={data} numberPerPage={3} renderer={render} sortBy='name' />); + expect(el.find(Row).length).toBe(3); + expect(el.containsAllMatchingElements([ + <Row key={8} />, + <Row key={4} />, + <Row key={5} />, + <PageNavigation currentPage={1} numPages={4} />])).toBe(true); + }); + + it('Reverse sort correctly', () => { + const el = shallow( + <Pagination data={data} numberPerPage={3} renderer={render} reverseSort sortBy='name' />); + expect(el.find(Row).length).toBe(3); + expect(el.containsAllMatchingElements([ + <Row key={2} />, + <Row key={3} />, + <Row key={10} />, + <PageNavigation currentPage={1} numPages={4} />])).toBe(true); + }); + + it('Should filter correctly', () => { + const el = shallow(<Pagination + data={data} + filter={(d) => d.name === 'one'} + numberPerPage={3} + renderer={render} />); + expect(el.find(Row).length).toBe(1); + expect(el.containsAllMatchingElements([ + <Row key={1} />, + <PageNavigation currentPage={1} numPages={1} />])).toBe(true); + }); + + it('Should change page when state is updated', () => { + const el = shallow( + <Pagination data={data} numberPerPage={3} page={1} renderer={render} />); + expect(el.containsAllMatchingElements([ + <Row key={1} />, + <Row key={2} />, + <Row key={3} />, + <PageNavigation currentPage={1} numPages={4} />])).toBe(true); + el.setState({page: 2}); + expect(el.containsAllMatchingElements([ + <Row key={4} />, + <Row key={5} />, + <Row key={6} />, + <PageNavigation currentPage={2} numPages={4} />])).toBe(true); + }); + + it('Should reset pagination when *any* new props are set', () => { + const el = shallow( + <Pagination data={data} numberPerPage={3} page={2} renderer={render} />); + expect(el.containsAllMatchingElements([ + <Row key={4} />, + <Row key={5} />, + <Row key={6} />, + <PageNavigation currentPage={2} numPages={4} />])).toBe(true); + el.setProps({ + data: data, + numberPerPage: 3, + page: 2, + renderer: render + }); + expect(el.containsAllMatchingElements([ + <Row key={1} />, + <Row key={2} />, + <Row key={3} />, + <PageNavigation currentPage={1} numPages={4} />])).toBe(true); + }); + + it('Should render into a tbody when isTable is set', () => { + const el = shallow(<Pagination data={data} isTable numberPerPage={3} renderer={render} />); + expect(el.is('tbody')).toBe(true); + }); +}); + +describe('PageNavigation', () => { + it('Should handle a single page navigation', () => { + const el = shallow(<PageNavigation currentPage={1} maxPages={5} numPages={1} />); + expect(el.contains(<li className='active' key={1}><span>{1}</span></li>)).toBe(true); + }); + + it('Should handle a multi page navigation starting from 1st page', () => { + const el = shallow(<PageNavigation currentPage={1} maxPages={5} numPages={10} />); + expect(el.find('li').length).toBe(5); + expect(el.containsAllMatchingElements([ + <li className='active'><span>{1}</span></li>, + <li><a>{2}</a></li>, + <li><a>{3}</a></li>, + <li><a>{4}</a></li>, + <li><a>»</a></li> + ])).toBe(true); + }); + + it('Should handle a multi page navigation starting from last page', () => { + const el = shallow(<PageNavigation currentPage={10} maxPages={5} numPages={10} />); + expect(el.find('li').length).toBe(5); + expect(el.containsAllMatchingElements([ + <li className='active'><span>{10}</span></li>, + <li><a>{9}</a></li>, + <li><a>{8}</a></li>, + <li><a>{7}</a></li>, + <li><a>«</a></li> + ])).toBe(true); + }); + + it('Should handle a multi page navigation starting from a middle page', () => { + const el = shallow(<PageNavigation currentPage={5} maxPages={5} numPages={10} />); + expect(el.find('li').length).toBe(9); + expect(el.containsAllMatchingElements([ + <li className='active'><span>{5}</span></li>, + <li><a>{4}</a></li>, + <li><a>{3}</a></li>, + <li><a>{6}</a></li>, + <li><a>{7}</a></li>, + <li><a>{8}</a></li>, + <li><a>«</a></li>, + <li><a>»</a></li> + ])).toBe(true); + }); + + it('Should pass the correct page when an item is clicked', () => { + const tracking = {}; + const click = (page) => { + tracking.clicked = page; + }; + const el = shallow( + <PageNavigation currentPage={1} maxPages={5} numPages={3} onClick={click} />); + // Find the next page link and click it + el.find('a').last().simulate('click'); + expect(tracking.clicked).toBe(2); + // Click individual pages + el.find('a').at(1).simulate('click'); + expect(tracking.clicked).toBe(3); + // Click individual pages + el.find('a').at(0).simulate('click'); + expect(tracking.clicked).toBe(2); + }); +}); http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/js/pages/__tests__/Home-test.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/pages/__tests__/Home-test.js b/ui/src/main/js/pages/__tests__/Home-test.js index 4f13f99..78e3eb4 100644 --- a/ui/src/main/js/pages/__tests__/Home-test.js +++ b/ui/src/main/js/pages/__tests__/Home-test.js @@ -1,14 +1,10 @@ import React from 'react'; +import { shallow } from 'enzyme'; import Home from '../Home'; import Breadcrumb from 'components/Breadcrumb'; import Loading from 'components/Loading'; import RoleList from 'components/RoleList'; -import shallow from 'utils/ShallowRender'; - -import chai, { expect } from 'chai'; -import assertJsx from 'preact-jsx-chai'; -chai.use(assertJsx); const TEST_CLUSTER = 'test-cluster'; @@ -31,12 +27,12 @@ const roles = [{role: 'test', jobCount: 0, cronJobCount: 5}]; describe('Home', () => { it('Should render Loading before data is fetched', () => { - expect(<Home api={{getRoleSummary: () => {}}} />).to.deep.equal(<Loading />); + expect(shallow(<Home api={{getRoleSummary: () => {}}} />).contains(<Loading />)).toBe(true); }); it('Should render page elements when roles are fetched', () => { const home = shallow(<Home api={createMockApi(roles)} />); - expect(home.contains(<Breadcrumb cluster={TEST_CLUSTER} />)).to.be.true; - expect(home.contains(<RoleList roles={roles} />)).to.be.true; + expect(home.contains(<Breadcrumb cluster={TEST_CLUSTER} />)).toBe(true); + expect(home.contains(<RoleList roles={roles} />)).toBe(true); }); }); http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/js/utils/ShallowRender.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/utils/ShallowRender.js b/ui/src/main/js/utils/ShallowRender.js deleted file mode 100644 index 52e8bb2..0000000 --- a/ui/src/main/js/utils/ShallowRender.js +++ /dev/null @@ -1,160 +0,0 @@ -import { options, render } from 'preact'; -import deepEqual from 'deep-equal'; - -function propsForElement(el) { - return el.__preactattr_ || {}; -} - -function extractName(vnode) { - return (typeof vnode.nodeName === 'string') - ? vnode.nodeName - : (vnode.nodeName.prototype.displayName || vnode.nodeName.name); -} - -function textChildrenMatch(domNode, vnode) { - const textChildren = vnode.children.filter((c) => typeof c === 'string').map((s) => s.trim()); - if (textChildren.length === 0) { - return true; - } - return textChildren.join(' ') === domNode.innerText.replace(/ +(?= )/g, ''); -} - -function findInSiblings(domNode, vnode) { - let cursor = domNode.nextElementSibling; - while (cursor !== null) { - if (matches(cursor, vnode)) { - return cursor; - } - cursor = cursor.nextElementSibling; - } - return null; -} - -function hasSiblings(domNode, vnodes) { - let cursor = domNode; - const found = []; - vnodes.forEach((node) => { - if (cursor !== null) { - cursor = findInSiblings(cursor, node); - if (cursor) { - found.push(cursor); - } - } - }); - return found.length === vnodes.length; -} - -function vnodeChildrenPresent(domNode, vnode) { - const vnodeChildren = vnode.children.filter((c) => typeof c !== 'string'); - if (vnodeChildren.length === 0) { - return true; - } - - // for children we want to maintain two key properties when matching: - // * order of nodes must match - // * number of nodes should match - // to do this we try and find all matches for vnodeChildren[0] and then - // use the sibling API to verify the rest of the children are present at the same - // level in the DOM tree - const [head, ...tail] = vnodeChildren; - - const matches = allMatches(domNode, head); - - for (let i = 0; i < matches.length; i++) { - if (hasSiblings(matches[i], tail)) { - return true; - } - } - - return false; -} - -function childrenMatch(domNode, vnode) { - if (vnode.attributes.children.length === 0) { - return true; - } - return textChildrenMatch(domNode, vnode) && vnodeChildrenPresent(domNode, vnode); -} - -function propertiesMatch(domNode, vnode) { - const domProperties = propsForElement(domNode); - const vnodeProperties = vnode.attributes; - const defaultProperties = vnode.nodeName.defaultProps || {}; - - return Object.keys(vnodeProperties).reduce((matches, key) => { - if (key === 'children') { - return matches && childrenMatch(domNode, vnode); - } - if (defaultProperties.hasOwnProperty(key) && vnodeProperties[key] === defaultProperties[key]) { - return matches; - } - return matches && deepEqual(domProperties[key], vnodeProperties[key]); - }, true); -} - -function allMatches(dom, vnode) { - const candidates = dom.querySelectorAll(extractName(vnode)); - const matches = []; - for (let i = 0; i < candidates.length; i++) { - if (propertiesMatch(candidates[i], vnode)) { - matches.push(candidates[i]); - } - } - return matches; -} - -function domContains(dom, vnode) { - return allMatches(dom, vnode).length > 0; -} - -function matches(dom, vnode) { - if (dom.nodeName.toLowerCase() === extractName(vnode).toLowerCase()) { - return propertiesMatch(dom, vnode); - } - return false; -} - -// Renders a shallow representation of the vnode into the DOM. -function shallowRender(preactEl, domEl) { - // Override the `vnode` hook to transform composite components in the render - // output into DOM elements. - const oldVnodeHook = options.vnode; - const vnodeHook = (node) => { - if (oldVnodeHook) { - oldVnodeHook(node); - } - if (typeof node.nodeName === 'string') { - return; - } - node.nodeName = node.nodeName.name; // eslint-disable-line no-param-reassign - }; - - try { - options.vnode = vnodeHook; - const el = render(preactEl, domEl); - options.vnode = oldVnodeHook; - return el; - } catch (err) { - options.vnode = oldVnodeHook; - throw err; - } -} - -// Primary interface for testing. The idea is that the vnode you supply will be used for property -// equality comparisons and non-provided properties are ignored. i.e. it is considered a match -// whenever any element in the DOM has at least the properties of the vnode. -export default function wrapper(preactEl) { - const shallow = shallowRender(preactEl, document.createElement('div')); - return { - __element: shallow, - contains: (vnode, matchExactly = false) => { - return domContains(shallow, vnode); - }, - is: (vnode, matchExactly = false) => { - return matches(shallow, vnode); - }, - find: (vnode) => { - return allMatches(shallow, vnode); - } - }; -} http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/js/utils/__tests__/ShallowRender-test.js ---------------------------------------------------------------------- diff --git a/ui/src/main/js/utils/__tests__/ShallowRender-test.js b/ui/src/main/js/utils/__tests__/ShallowRender-test.js deleted file mode 100644 index d5663a7..0000000 --- a/ui/src/main/js/utils/__tests__/ShallowRender-test.js +++ /dev/null @@ -1,86 +0,0 @@ -import React from 'react'; - -import { expect } from 'chai'; - -import shallow from '../ShallowRender'; - -class Leaf extends React.Component { - render() { - return <div>Leaf</div>; - } -} - -class Node extends React.Component { - render() { - return <div><Leaf {...this.props} /> <span /> <div>Something Else</div></div>; - } -} - -class ThinWrapper extends React.Component { - render() { - return <Leaf {...this.props} />; - } -} - -class List extends React.Component { - render() { - return <ul><li><Leaf /></li><li><Leaf /></li></ul>; - } -} - -class GeneratedList extends React.Component { - render() { - return (<div><ul>{this.props.names.map((i) => <Leaf name={i} />)}</ul></div>); - } -} - -describe('shallow::contains', () => { - it('Should respect shallow rendering', () => { - expect(shallow(<Node />).contains(<Leaf />)).to.be.true; - }); - - it('Should handle multiple elements', () => { - const el = shallow(<div><Node name='jon' /><Node name='dany' /></div>); - expect(el.contains(<Leaf name='jon' />)).to.be.true; - expect(el.contains(<Leaf name='dany' />)).to.be.true; - }); - - it('Should match properties based on target node', () => { - const el = shallow(<Node name='jon' surname='snow' />); - expect(el.contains(<Leaf name='jon' surname='snow' />)).to.be.true; - expect(el.contains(<Leaf name='jon' />)).to.be.true; - expect(el.contains(<Leaf surname='snow' />)).to.be.true; - expect(el.contains(<Leaf />)).to.be.true; - expect(el.contains(<Leaf name='jon' surname='snow'>jon snow</Leaf>)).to.be.false; - }); - - it('Should match children with text', () => { - expect(shallow(<Node />).contains(<div>Something Else</div>)).to.be.true; - expect(shallow(<Node />).contains(<div>Not Present</div>)).to.be.false; - }); - - it('Should work with deeply nested tree', () => { - expect(shallow(<List />).contains(<li><Leaf /></li>)).to.be.true; - expect(shallow(<List />).contains(<Leaf />)).to.be.true; - }); - - it('Should respect ordering of nested items', () => { - const generated = shallow(<GeneratedList names={['jon', 'dany']} />); - expect(generated.contains(<ul><Leaf name='jon' /><Leaf name='dany' /></ul>)).to.be.true; - expect(generated.contains(<ul><Leaf name='dany' /></ul>)).to.be.true; - expect(generated.contains(<ul><Leaf name='dany' /><Leaf name='jon' /></ul>)).to.be.false; - }); -}); - -describe('shallow::is', () => { - it('Should handle standard HTML elements', () => { - expect(shallow(<ThinWrapper />).is(<Leaf />)).to.be.true; - }); - - it('Should handle lists', () => { - expect(shallow(<List />).is(<ul />)).to.be.true; - expect(shallow(<List />).is(<ul><li><Leaf /></li><li><Leaf /></li></ul>)).to.be.true; - expect(shallow(<List />) - .is(<ul><li><Leaf /></li><li><Leaf /></li><li><Leaf /></li></ul>)).to.be.false; - }); -}); http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/src/main/sass/components/_tables.scss ---------------------------------------------------------------------- diff --git a/ui/src/main/sass/components/_tables.scss b/ui/src/main/sass/components/_tables.scss index 58f176c..2ea60bc 100644 --- a/ui/src/main/sass/components/_tables.scss +++ b/ui/src/main/sass/components/_tables.scss @@ -13,61 +13,50 @@ th { padding: 5px; } -} -.reactable-data { - border: 1px solid $grid_color; + tbody { + tr + tr, tr:first-child { + border-top: 1px solid $grid_color; + } - tr + tr { - border-top: 1px solid $grid_color; - } + td { + border-left: 1px solid $grid_color; + } - td + td { - border-left: 1px solid $grid_color; - } + td:last-child { + border-right: 1px solid $grid_color; + } - tr:nth-child(even) { - background: rgba(0,0,0,0.017); - } + tr:last-child { + border-bottom: 1px solid $grid_color; + } - tr:hover { - background: #edf5fd; - } + tr:nth-child(even) { + background: rgba(0,0,0,0.017); + } - td, th { - padding: 5px; - } -} + tr:hover { + background: #edf5fd; + } -.reactable-pagination { - td { - padding: 2em 0 4em 0; - text-align: center; - } + td, th { + padding: 5px; + } - a { - padding: 6px 12px; - border: 1px solid $grid_highlight_color; - border-left: 0px; - } + .pagination-row { + background-color: $content_box_color !important; - a:first-child { - padding: 6px 12px; - border-left: 1px solid $grid_highlight_color; - } + &:hover { + background-color: $content_box_color !important; + } - a:hover { - background-color: steelblue; - border: 1px solid #FFF; - color: white; + td { + text-align: center; + } + } } } -.reactable-current-page { - font-weight: normal; - color: #222; -} - .table-input-wrapper { border-radius: 4px; padding: 5px; http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/test-setup.js ---------------------------------------------------------------------- diff --git a/ui/test-setup.js b/ui/test-setup.js new file mode 100644 index 0000000..054e7c2 --- /dev/null +++ b/ui/test-setup.js @@ -0,0 +1,5 @@ +// setup file +import { configure } from 'enzyme'; +import Adapter from 'enzyme-adapter-react-16'; + +configure({ adapter: new Adapter() }); http://git-wip-us.apache.org/repos/asf/aurora/blob/7c78519e/ui/webpack.config.js ---------------------------------------------------------------------- diff --git a/ui/webpack.config.js b/ui/webpack.config.js index e7cd672..4fd7b35 100644 --- a/ui/webpack.config.js +++ b/ui/webpack.config.js @@ -13,10 +13,6 @@ module.exports = { publicPath: '/' }, resolve: { - alias: { - react: "preact-compat", - "react-dom": "preact-compat" - }, extensions: [ '.js' ], modules: [EXTENSION_PATH, SOURCE_PATH, 'node_modules'] },
