aurora-commits mailing list archives

Site index · List index
Message view « Date » · « Thread »
Top « Date » · « Thread »
From dmclaugh...@apache.org
Subject aurora git commit: Replace Preact and custom testing with React + Enzyme
Date Wed, 27 Sep 2017 21:42:53 GMT
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 <david@dmclaughlin.com>
Authored: Wed Sep 27 14:34:02 2017 -0700
Committer: David McLaughlin <david@dmclaughlin.com>
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)}>&laquo;</a></li>
+    : '';
+  const nextPage = (currentPage < numPages)
+    ? <li key='next'><a onClick={(e) => onClick(currentPage + 1)}>&raquo;</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>&raquo;</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>&laquo;</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>&laquo;</a></li>,
+      <li><a>&raquo;</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']
   },


Mime
View raw message