From 0711f75906a95a04750883e2afab1dbb7fe444cb Mon Sep 17 00:00:00 2001 From: pkupczyk <piotr.kupczyk@id.ethz.ch> Date: Sat, 13 Apr 2019 19:29:11 +0200 Subject: [PATCH] SSDM-7569 NEW openBIS UI - General Template/Infrastructure for Forms - Visualisation/Creation/Edit - introduce a basic form + integrate enzyme library for automated testing of React components (first test created for the Browser) --- openbis_ng_ui/package.json | 7 +- .../src/components/browser/Browser.jsx | 68 ++++++++----- .../src/components/browser/BrowserNode.jsx | 52 ++++++++++ .../src/components/browser/BrowserNodes.jsx | 39 +------- .../src/components/content/Content.jsx | 35 +++---- .../content/objectType/ObjectType.jsx | 51 +++++++++- .../{store/sagas => common}/fixture.js | 0 .../components/browser/browser.test.js | 96 +++++++++++++++++++ openbis_ng_ui/srcTest/setupTests.js | 4 + openbis_ng_ui/srcTest/store/sagas/app.test.js | 2 +- .../srcTest/store/sagas/browser.test.js | 2 +- .../srcTest/store/sagas/page.test.js | 2 +- 12 files changed, 274 insertions(+), 84 deletions(-) create mode 100644 openbis_ng_ui/src/components/browser/BrowserNode.jsx rename openbis_ng_ui/srcTest/{store/sagas => common}/fixture.js (100%) create mode 100644 openbis_ng_ui/srcTest/components/browser/browser.test.js create mode 100644 openbis_ng_ui/srcTest/setupTests.js diff --git a/openbis_ng_ui/package.json b/openbis_ng_ui/package.json index 9fb7d60bc28..1846ade5180 100644 --- a/openbis_ng_ui/package.json +++ b/openbis_ng_ui/package.json @@ -50,7 +50,9 @@ "url-loader": "1.1.1", "webpack": "4.6.0", "webpack-cli": "2.0.14", - "webpack-dev-server": "^3.1.14" + "webpack-dev-server": "^3.1.14", + "enzyme": "3.9.0", + "enzyme-adapter-react-16": "1.12.1" }, "scripts": { "dev": "webpack-dev-server --hot --config webpack.config.dev.js", @@ -65,6 +67,7 @@ "reporters": [ "default", "jest-junit" - ] + ], + "setupTestFrameworkScriptFile": "<rootDir>srcTest/setupTests.js" } } diff --git a/openbis_ng_ui/src/components/browser/Browser.jsx b/openbis_ng_ui/src/components/browser/Browser.jsx index 90956174cb7..24ef17e8aca 100644 --- a/openbis_ng_ui/src/components/browser/Browser.jsx +++ b/openbis_ng_ui/src/components/browser/Browser.jsx @@ -1,7 +1,6 @@ import React from 'react' import {connect} from 'react-redux' import logger from '../../common/logger.js' -import store from '../../store/store.js' import * as selectors from '../../store/selectors/selectors.js' import * as actions from '../../store/actions/actions.js' @@ -9,12 +8,8 @@ import Loading from '../loading/Loading.jsx' import BrowserFilter from './BrowserFilter.jsx' import BrowserNodes from './BrowserNodes.jsx' -function getCurrentPage(){ - return selectors.getCurrentPage(store.getState()) -} - function mapStateToProps(state){ - let currentPage = getCurrentPage() + let currentPage = selectors.getCurrentPage(state) return { currentPage: currentPage, filter: selectors.getBrowserFilter(state, currentPage), @@ -22,30 +17,53 @@ function mapStateToProps(state){ } } -function mapDispatchToProps(dispatch){ - return { - init: (page) => { dispatch(actions.browserInit(page)) }, - release: (page) => { dispatch(actions.browserRelease(page)) }, - filterChange: (event) => { dispatch(actions.browserFilterChange(getCurrentPage(), event.currentTarget.value)) }, - nodeSelect: (id) => { dispatch(actions.browserNodeSelect(getCurrentPage(), id)) }, - nodeExpand: (id) => { dispatch(actions.browserNodeExpand(getCurrentPage(), id)) }, - nodeCollapse: (id) => { dispatch(actions.browserNodeCollapse(getCurrentPage(), id)) } - } -} - class Browser extends React.PureComponent { + constructor(props){ + super(props) + this.init = this.init.bind(this) + this.release = this.release.bind(this) + this.filterChange = this.filterChange.bind(this) + this.nodeSelect = this.nodeSelect.bind(this) + this.nodeExpand = this.nodeExpand.bind(this) + this.nodeCollapse = this.nodeCollapse.bind(this) + } + componentDidMount(){ - this.props.init(this.props.currentPage) + this.init(this.props.currentPage) } componentDidUpdate(previousProps){ if(this.props.currentPage !== previousProps.currentPage){ - this.props.release(previousProps.currentPage) - this.props.init(this.props.currentPage) + this.release(previousProps.currentPage) + this.init(this.props.currentPage) } } + init(page){ + this.props.dispatch(actions.browserInit(page)) + } + + release(page){ + this.props.dispatch(actions.browserRelease(page)) + } + + filterChange(event){ + this.props.dispatch(actions.browserFilterChange(this.props.currentPage, event.currentTarget.value)) + } + + nodeSelect(id){ + this.props.dispatch(actions.browserNodeSelect(this.props.currentPage, id)) + } + + nodeExpand(id){ + this.props.dispatch(actions.browserNodeExpand(this.props.currentPage, id)) + } + + nodeCollapse(id){ + this.props.dispatch(actions.browserNodeCollapse(this.props.currentPage, id)) + } + render() { logger.log(logger.DEBUG, 'Browser.render') @@ -53,13 +71,13 @@ class Browser extends React.PureComponent { <Loading loading={this.props.loading}> <BrowserFilter filter={this.props.filter} - filterChange={this.props.filterChange} + filterChange={this.filterChange} /> <BrowserNodes nodes={this.props.nodes} - nodeSelect={this.props.nodeSelect} - nodeExpand={this.props.nodeExpand} - nodeCollapse={this.props.nodeCollapse} + nodeSelect={this.nodeSelect} + nodeExpand={this.nodeExpand} + nodeCollapse={this.nodeCollapse} level={0} /> </Loading>) @@ -67,4 +85,4 @@ class Browser extends React.PureComponent { } -export default connect(mapStateToProps, mapDispatchToProps)(Browser) +export default connect(mapStateToProps, null)(Browser) diff --git a/openbis_ng_ui/src/components/browser/BrowserNode.jsx b/openbis_ng_ui/src/components/browser/BrowserNode.jsx new file mode 100644 index 00000000000..5d4939aceff --- /dev/null +++ b/openbis_ng_ui/src/components/browser/BrowserNode.jsx @@ -0,0 +1,52 @@ +import React from 'react' +import ListItem from '@material-ui/core/ListItem' +import ListItemIcon from '@material-ui/core/ListItemIcon' +import ListItemText from '@material-ui/core/ListItemText' +import Collapse from '@material-ui/core/Collapse' +import ChevronRightIcon from '@material-ui/icons/ChevronRight' +import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import BrowserNodes from './BrowserNodes.jsx' +import logger from '../../common/logger.js' + +class BrowserNode extends React.Component { + + render() { + logger.log(logger.DEBUG, 'BrowserNode.render') + + const {node, level} = this.props + + return (<div> + <ListItem + button + selected={node.selected} + style={{paddingLeft: level * 20 + 'px'}}> + {this.renderIcon(node)} + {this.renderText(node)} + </ListItem> + {node.children && node.children.length > 0 && + <Collapse in={node.expanded} mountOnEnter={true} unmountOnExit={true}> + <BrowserNodes {...this.props} nodes={node.children} level={level + 1} /> + </Collapse>} + </div>) + } + + renderIcon(node){ + if(node.children && node.children.length > 0){ + if(node.expanded){ + return (<ListItemIcon><ExpandMoreIcon onClick={() => this.props.nodeCollapse(node.id)}/></ListItemIcon>) + }else{ + return (<ListItemIcon><ChevronRightIcon onClick={() => this.props.nodeExpand(node.id)}/></ListItemIcon>) + } + }else{ + return null + } + } + + renderText(node){ + logger.log(logger.DEBUG, 'BrowserNode.renderText "' + node.text + '"') + return <ListItemText primary={node.text} inset={true} onClick={() => this.props.nodeSelect(node.id)} /> + } + +} + +export default BrowserNode diff --git a/openbis_ng_ui/src/components/browser/BrowserNodes.jsx b/openbis_ng_ui/src/components/browser/BrowserNodes.jsx index 3f14f95d79e..de1a76e3646 100644 --- a/openbis_ng_ui/src/components/browser/BrowserNodes.jsx +++ b/openbis_ng_ui/src/components/browser/BrowserNodes.jsx @@ -1,12 +1,7 @@ import React from 'react' import {withStyles} from '@material-ui/core/styles' import List from '@material-ui/core/List' -import ListItem from '@material-ui/core/ListItem' -import ListItemIcon from '@material-ui/core/ListItemIcon' -import ListItemText from '@material-ui/core/ListItemText' -import Collapse from '@material-ui/core/Collapse' -import ChevronRightIcon from '@material-ui/icons/ChevronRight' -import ExpandMoreIcon from '@material-ui/icons/ExpandMore' +import BrowserNode from './BrowserNode.jsx' import logger from '../../common/logger.js' const styles = () => ({ @@ -26,42 +21,12 @@ class BrowserNodes extends React.Component { return (<List className={classes.browserList}> { this.props.nodes.map(node => { - return (<div key={node.id + '-parent'}> - <ListItem - button - key={node.id} - selected={node.selected} - style={{paddingLeft: this.props.level * 20 + 'px'}}> - {this.renderIcon(node)} - {this.renderText(node)} - </ListItem> - {node.children && node.children.length > 0 && - <Collapse key={node.id + '-collapse'} in={node.expanded} mountOnEnter={true} unmountOnExit={true}> - <BrowserNodes {...this.props} nodes={node.children} level={this.props.level + 1} /> - </Collapse>} - </div>) + return <BrowserNode {...this.props} key={node.id} node={node} level={this.props.level} /> }) } </List>) } - renderIcon(node){ - if(node.children && node.children.length > 0){ - if(node.expanded){ - return (<ListItemIcon><ExpandMoreIcon onClick={() => this.props.nodeCollapse(node.id)}/></ListItemIcon>) - }else{ - return (<ListItemIcon><ChevronRightIcon onClick={() => this.props.nodeExpand(node.id)}/></ListItemIcon>) - } - }else{ - return null - } - } - - renderText(node){ - logger.log(logger.DEBUG, 'BrowserNode.renderText "' + node.text + '"') - return <ListItemText primary={node.text} inset={true} onClick={() => this.props.nodeSelect(node.id)} /> - } - } export default withStyles(styles)(BrowserNodes) diff --git a/openbis_ng_ui/src/components/content/Content.jsx b/openbis_ng_ui/src/components/content/Content.jsx index a42a21fd6f5..c4c6979d6df 100644 --- a/openbis_ng_ui/src/components/content/Content.jsx +++ b/openbis_ng_ui/src/components/content/Content.jsx @@ -2,7 +2,6 @@ import React from 'react' import ContentTabs from './ContentTabs.jsx' import {connect} from 'react-redux' import logger from '../../common/logger.js' -import store from '../../store/store.js' import * as objectType from '../../store/consts/objectType.js' import * as selectors from '../../store/selectors/selectors.js' import * as actions from '../../store/actions/actions.js' @@ -23,26 +22,30 @@ const objectTypeToComponent = { [objectType.GROUP]: Group, } -function getCurrentPage(){ - return selectors.getCurrentPage(store.getState()) -} - function mapStateToProps(state){ - let currentPage = getCurrentPage() + let currentPage = selectors.getCurrentPage(state) return { + currentPage: currentPage, openObjects: selectors.getOpenObjects(state, currentPage), selectedObject: selectors.getSelectedObject(state, currentPage) } } -function mapDispatchToProps(dispatch){ - return { - objectSelect: (type, id) => { dispatch(actions.objectOpen(getCurrentPage(), type, id)) }, - objectClose: (type, id) => { dispatch(actions.objectClose(getCurrentPage(), type, id)) } +class Content extends React.Component { + + constructor(props){ + super(props) + this.objectSelect = this.objectSelect.bind(this) + this.objectClose = this.objectClose.bind(this) } -} -class Content extends React.Component { + objectSelect(type, id){ + this.props.dispatch(actions.objectOpen(this.props.currentPage, type, id)) + } + + objectClose(type, id){ + this.props.dispatch(actions.objectClose(this.props.currentPage, type, id)) + } render() { logger.log(logger.DEBUG, 'Content.render') @@ -54,10 +57,10 @@ class Content extends React.Component { <ContentTabs objects={this.props.openObjects} selectedObject={this.props.selectedObject} - objectSelect={this.props.objectSelect} - objectClose={this.props.objectClose} /> + objectSelect={this.objectSelect} + objectClose={this.objectClose} /> {ObjectContent && - <ObjectContent /> + <ObjectContent objectId={this.props.selectedObject.id} /> } </div> ) @@ -65,4 +68,4 @@ class Content extends React.Component { } -export default connect(mapStateToProps, mapDispatchToProps)(Content) +export default connect(mapStateToProps, null)(Content) diff --git a/openbis_ng_ui/src/components/content/objectType/ObjectType.jsx b/openbis_ng_ui/src/components/content/objectType/ObjectType.jsx index 43cf63499eb..24c2f85bd44 100644 --- a/openbis_ng_ui/src/components/content/objectType/ObjectType.jsx +++ b/openbis_ng_ui/src/components/content/objectType/ObjectType.jsx @@ -1,11 +1,60 @@ +import _ from 'lodash' import React from 'react' +import TextField from '@material-ui/core/TextField' +import Checkbox from '@material-ui/core/Checkbox' +import Button from '@material-ui/core/Button' import logger from '../../../common/logger.js' +import openbis from '../../../services/openbis.js' + class ObjectType extends React.Component { + constructor(props){ + super(props) + this.state = { + description: '', + listable: false + } + } + + componentDidMount(){ + } + + handleChange(name){ + return event => { + let value = _.has(event.target, 'checked') ? event.target.checked : event.target.value + this.setState({ [name]: value }) + } + } + render() { logger.log(logger.DEBUG, 'ObjectType.render') - return <div>ObjectType</div> + return ( + <div> + {this.props.objectId} + <form> + <div> + <TextField + label='Description' + value={this.state.description} + onChange={this.handleChange('description')} + /> + </div> + <div> + <Checkbox + checked={this.state.listable} + value='listable' + onChange={this.handleChange('listable')} + /> + </div> + <div> + <Button variant='contained' color='primary'> + Save + </Button> + </div> + </form> + </div> + ) } } diff --git a/openbis_ng_ui/srcTest/store/sagas/fixture.js b/openbis_ng_ui/srcTest/common/fixture.js similarity index 100% rename from openbis_ng_ui/srcTest/store/sagas/fixture.js rename to openbis_ng_ui/srcTest/common/fixture.js diff --git a/openbis_ng_ui/srcTest/components/browser/browser.test.js b/openbis_ng_ui/srcTest/components/browser/browser.test.js new file mode 100644 index 00000000000..25da2f03b62 --- /dev/null +++ b/openbis_ng_ui/srcTest/components/browser/browser.test.js @@ -0,0 +1,96 @@ +import React from 'react' +import { mount } from 'enzyme' +import Browser from '../../../src/components/browser/Browser.jsx' +import openbis from '../../../src/services/openbis.js' +import * as actions from '../../../src/store/actions/actions.js' +import * as pages from '../../../src/store/consts/pages.js' +import { createStore } from '../../../src/store/store.js' +import * as fixture from '../../common/fixture.js' + +jest.mock('../../../src/services/openbis.js') + +let store = null + +beforeEach(() => { + jest.resetAllMocks() + store = createStore() +}) + +describe('browser', () => { + + test('test', () => { + openbis.getUsers.mockReturnValue({ + objects: [ fixture.TEST_USER_DTO, fixture.ANOTHER_USER_DTO ] + }) + + openbis.getGroups.mockReturnValue({ + objects: [ fixture.TEST_GROUP_DTO, fixture.ANOTHER_GROUP_DTO, fixture.ALL_USERS_GROUP_DTO ] + }) + + store.dispatch(actions.currentPageChange(pages.USERS)) + store.dispatch(actions.browserInit(pages.USERS)) + + let wrapper = mount(<Browser store={store}/>) + expectFilter(wrapper, '') + expectNodes(wrapper, [ + { level: 0, text: 'Users'}, + { level: 0, text: 'Groups'} + ]) + + simulateNodeIconClick(wrapper, 'users') + + expectFilter(wrapper, '') + expectNodes(wrapper, [ + { level: 0, text: 'Users'}, + { level: 1, text: fixture.ANOTHER_USER_DTO.userId}, + { level: 1, text: fixture.TEST_USER_DTO.userId}, + { level: 0, text: 'Groups'} + ]) + + simulateFilterChange(wrapper, fixture.ANOTHER_GROUP_DTO.code.toUpperCase()) + wrapper.update() + + expectFilter(wrapper, fixture.ANOTHER_GROUP_DTO.code.toUpperCase()) + expectNodes(wrapper, [ + { level: 0, text: 'Users'}, + { level: 1, text: fixture.ANOTHER_USER_DTO.userId}, + { level: 2, text: fixture.ANOTHER_GROUP_DTO.code}, + { level: 0, text: 'Groups'}, + { level: 1, text: fixture.ANOTHER_GROUP_DTO.code} + ]) + }) + +}) + +function simulateNodeIconClick(wrapper, id){ + wrapper.findWhere(node => { + return node.name() === 'BrowserNode' && node.prop('node').id === id + }).find('ListItemIcon').first().simulate('click') +} + +function simulateFilterChange(wrapper, filter){ + let input = wrapper.find('BrowserFilter').find('input') + input.instance().value = filter + input.simulate('change') +} + +function expectFilter(wrapper, expectedFilter){ + const actualFilter = wrapper.find('BrowserFilter').map(node => { + return node.prop('filter') + })[0] + expect(actualFilter).toEqual(expectedFilter) +} + +function expectNodes(wrapper, expectedNodes){ + const actualNodes = wrapper.find('BrowserNode').map(node => { + const text = node.prop('node').text + const selected = node.prop('node').selected + const level = node.prop('level') + return { + text, + level, + selected + } + }) + expect(actualNodes).toMatchObject(expectedNodes) +} diff --git a/openbis_ng_ui/srcTest/setupTests.js b/openbis_ng_ui/srcTest/setupTests.js new file mode 100644 index 00000000000..3d6cd1d53a1 --- /dev/null +++ b/openbis_ng_ui/srcTest/setupTests.js @@ -0,0 +1,4 @@ +import { configure } from 'enzyme' +import Adapter from 'enzyme-adapter-react-16' + +configure({ adapter: new Adapter() }) diff --git a/openbis_ng_ui/srcTest/store/sagas/app.test.js b/openbis_ng_ui/srcTest/store/sagas/app.test.js index f65389c2a78..331ffcf0790 100644 --- a/openbis_ng_ui/srcTest/store/sagas/app.test.js +++ b/openbis_ng_ui/srcTest/store/sagas/app.test.js @@ -3,7 +3,7 @@ import * as actions from '../../../src/store/actions/actions.js' import * as selectors from '../../../src/store/selectors/selectors.js' import * as pages from '../../../src/store/consts/pages.js' import { createStore } from '../../../src/store/store.js' -import * as fixture from './fixture.js' +import * as fixture from '../../common/fixture.js' jest.mock('../../../src/services/openbis.js') diff --git a/openbis_ng_ui/srcTest/store/sagas/browser.test.js b/openbis_ng_ui/srcTest/store/sagas/browser.test.js index 924d97c12cd..6af2867a30f 100644 --- a/openbis_ng_ui/srcTest/store/sagas/browser.test.js +++ b/openbis_ng_ui/srcTest/store/sagas/browser.test.js @@ -6,7 +6,7 @@ import * as pages from '../../../src/store/consts/pages.js' import * as objectType from '../../../src/store/consts/objectType.js' import * as common from '../../../src/store/common/browser.js' import { createStore } from '../../../src/store/store.js' -import * as fixture from './fixture.js' +import * as fixture from '../../common/fixture.js' jest.mock('../../../src/services/openbis.js') diff --git a/openbis_ng_ui/srcTest/store/sagas/page.test.js b/openbis_ng_ui/srcTest/store/sagas/page.test.js index bb59b99375e..162488835d0 100644 --- a/openbis_ng_ui/srcTest/store/sagas/page.test.js +++ b/openbis_ng_ui/srcTest/store/sagas/page.test.js @@ -3,7 +3,7 @@ import * as selectors from '../../../src/store/selectors/selectors.js' import * as objectType from '../../../src/store/consts/objectType.js' import * as pages from '../../../src/store/consts/pages.js' import { createStore } from '../../../src/store/store.js' -import * as fixture from './fixture.js' +import * as fixture from '../../common/fixture.js' jest.mock('../../../src/services/openbis.js') -- GitLab