diff --git a/jest.config.js b/jest.config.js new file mode 100644 index 0000000..a5c4be7 --- /dev/null +++ b/jest.config.js @@ -0,0 +1,13 @@ +// jest.config.js +module.exports = { + collectCoverageFrom: ['src/**/*.{js,jsx}'], + coveragePathIgnorePatterns: [ + '/node_modules/', + 'src/serviceWorker.js', + 'src/configureStore.js', + 'src/bees.js', + 'src/index.js', + 'src/tests/*.{js,jsx}', + 'src/__tests__/testing-helpers.js' + ] +} diff --git a/package.json b/package.json index e3202d3..cb65595 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,7 @@ "scripts": { "start": "react-scripts start", "build": "react-scripts build", - "test": "react-scripts test", + "test": "react-scripts test --testPathIgnorePatterns=src/__tests__/testing-helpers.js --env=jsdom", "test:coverage": "react-scripts test --coverage --watchAll=false", "eject": "react-scripts eject", "lint": "eslint './src/**/*.{js,jsx}'", @@ -24,19 +24,6 @@ "eslintConfig": { "extends": "react-app" }, - "jest": { - "collectCoverageFrom": [ - "src/**/*.{js,jsx}" - ], - "coveragePathIgnorePatterns": [ - "/node_modules/", - "src/serviceWorker.js", - "src/configureStore.js", - "src/bees.js", - "src/index.js", - "src/tests/*.{js,jsx}" - ] - }, "browserslist": { "production": [ ">0.2%", @@ -50,6 +37,8 @@ ] }, "devDependencies": { + "@testing-library/jest-dom": "^5.1.1", + "@testing-library/react": "^9.4.0", "babel-loader": "^8.0.6", "cross-fetch": "^3.0.4", "eslint": "^6.4.0", diff --git a/src/__tests__/App.test.js b/src/__tests__/App.test.js index 808228a..3b88e09 100644 --- a/src/__tests__/App.test.js +++ b/src/__tests__/App.test.js @@ -1,10 +1,7 @@ import React from 'react' -import ReactDOM from 'react-dom' import App from '../containers/App' +import { renderWithRedux } from './testing-helpers' -/** - * This needs to be implemented to handle working with the Redux Store - */ -it('renders the component on the DOM', () => { - // To be implemented +it('renders without crashing', () => { + expect(renderWithRedux()) }) diff --git a/src/__tests__/AuthButton.test.js b/src/__tests__/AuthButton.test.js index c1583d0..cceddf4 100644 --- a/src/__tests__/AuthButton.test.js +++ b/src/__tests__/AuthButton.test.js @@ -1,6 +1,6 @@ import React from 'react' -import ReactDOM from 'react-dom' import AuthButton from '../containers/AuthButton' +import { render } from '@testing-library/react' describe('', () => { let defaultProps = { @@ -8,15 +8,24 @@ describe('', () => { authorizationUrl: 'http://cloud.org/authorize' } + it('renders without crashing', () => { + expect(render()) + }) + + it('renders the auth button', () => { + const { getByTestId, debug } = render() + expect(getByTestId('auth-button')).toBeInTheDocument() + }) + it('renders the Sign in', () => { - // const wrapper = mount(); - // expect(wrapper).toBeTruthy; - // expect(wrapper.text()).toEqual('Sign In'); + const { getByTestId, debug } = render() + expect(getByTestId('auth-button')).toHaveTextContent(/sign in/i) }) it('renders the Authorized', () => { - // const wrapper = mount(); - // expect(wrapper).toBeTruthy; - // expect(wrapper.text()).toEqual('Authorized'); + const { getByTestId, debug } = render( + + ) + expect(getByTestId('auth-button')).toHaveTextContent(/authorized/i) }) }) diff --git a/src/__tests__/ResourceNode.test.js b/src/__tests__/ResourceNode.test.js index d917f93..820c69f 100644 --- a/src/__tests__/ResourceNode.test.js +++ b/src/__tests__/ResourceNode.test.js @@ -1,5 +1,26 @@ import React from 'react' -import ReactDOM from 'react-dom' import ResourceNode from '../containers/ResourceNode' +import { render } from '@testing-library/react' -it('renders the component on the DOM', () => {}) +const mockDispatch = jest.fn() + +const defaultProps = { + selected: false, + label: 'Ima label', + bytestream: { foo: 'bar ' }, + dispatch: mockDispatch +} + +it('renders without crashing', () => { + expect(render()) +}) + +it('renders the resource node checkbox', () => { + const { getByTestId } = render() + expect(getByTestId('resource-node-checkbox')).toBeInTheDocument() +}) + +it('renders expand/collapse button', () => { + const { getByTestId } = render() + expect(getByTestId('expand-collapse-button')).toBeInTheDocument() +}) diff --git a/src/__tests__/ResourceTree.test.js b/src/__tests__/ResourceTree.test.js index 3ba2524..8af3847 100644 --- a/src/__tests__/ResourceTree.test.js +++ b/src/__tests__/ResourceTree.test.js @@ -1,5 +1,35 @@ import React from 'react' -import ReactDOM from 'react-dom' import ResourceTree from '../containers/ResourceTree' +import { render } from '@testing-library/react' -it('renders the component on the DOM', () => {}) +const mockDispatch = jest.fn() + +const defaultProps = { + selected: false, + root: false, + label: 'Ima label', + dispatch: mockDispatch, + container: { foo: 'bar' } +} + +it('renders without crashing', () => { + expect(render()) +}) + +it('renders resource tree wrapper element', () => { + const { getByTestId } = render() + expect(getByTestId('resource-tree-wrapper')).toBeInTheDocument() +}) + +it('renders primary checkbox', () => { + const { getByTestId } = render() + expect(getByTestId('primary-checkbox')).toBeInTheDocument() +}) + +it('renders expand/collapse button', () => { + const { getByTestId } = render() + expect(getByTestId('expand-collapse-button')).toBeInTheDocument() +}) + +// Not really sure yet how to test and +// < ResourceNode /> presence in this component diff --git a/src/__tests__/SelectProvider.test.js b/src/__tests__/SelectProvider.test.js index 37971b2..88fbad0 100644 --- a/src/__tests__/SelectProvider.test.js +++ b/src/__tests__/SelectProvider.test.js @@ -1,5 +1,25 @@ import React from 'react' -import ReactDOM from 'react-dom' import SelectProvider from '../containers/SelectProvider' +import { render } from '@testing-library/react' -it('renders the component on the DOM', () => {}) +const mockFn = jest.fn() + +const defaultProps = { + selectedProvider: { foo: 'bar' }, + providers: { items: [], id: 'ABC123', name: 'Ima provider 1' }, + handleChange: mockFn +} + +it('renders without crashing', () => { + expect(render()) +}) + +it('renders the select provider form control', () => { + const { getByTestId } = render() + expect(getByTestId('select-provider-wrapper')).toBeInTheDocument() +}) + +it('renders the select element', () => { + const { getByTestId } = render() + expect(getByTestId('select-provider')).toBeInTheDocument() +}) diff --git a/src/__tests__/UploadForm.test.js b/src/__tests__/UploadForm.test.js index 90c3f33..ac35d10 100644 --- a/src/__tests__/UploadForm.test.js +++ b/src/__tests__/UploadForm.test.js @@ -1,5 +1,41 @@ import React from 'react' -import ReactDOM from 'react-dom' import UploadForm from '../containers/UploadForm' +import { render } from '@testing-library/react' -it('renders the component on the DOM', () => {}) +const mockOnUpload = jest.fn() +const mockDispatch = jest.fn() + +const defaultProps = { + selectedProvider: { foo: 'bar' }, + providers: { items: [], id: 'ABC123', name: 'Ima provider 1' }, + currentAuthToken: { foo: 'bar ' }, + currentSession: {}, + rootContainer: {}, + currentUpload: {}, + dispatch: mockDispatch, + onUpload: mockOnUpload +} + +it('renders without crashing', () => { + expect(render()) +}) + +it('renders the form', () => { + const { getByTestId } = render() + expect(getByTestId('upload-form')).toBeInTheDocument() +}) + +it('renders the Select Providers section', () => { + const { getByTestId } = render() + expect(getByTestId('select-provider-wrapper')).toBeInTheDocument() +}) + +it('renders the resource tree section', () => { + const { getByTestId } = render() + expect(getByTestId('resource-tree-wrapper')).toBeInTheDocument() +}) + +it('renders the submit button', () => { + const { getByTestId } = render() + expect(getByTestId('upload-submit-button')).toBeInTheDocument() +}) diff --git a/src/__tests__/testing-helpers.js b/src/__tests__/testing-helpers.js new file mode 100644 index 0000000..a526d46 --- /dev/null +++ b/src/__tests__/testing-helpers.js @@ -0,0 +1,21 @@ +import React from 'react' +import { Provider } from 'react-redux' +import { render } from '@testing-library/react' +import reducer from '../reducers' +import configureStore from '../configureStore' + +// this is a handy function that I normally make available for all my tests +// that deal with connected components. +// you can provide initialState for the entire store that the ui is rendered with +export function renderWithRedux( + ui, + { initialState, store = configureStore(initialState) } = {} +) { + return { + ...render({ui}), + // adding `store` to the returned utilities to allow us + // to reference it in our tests (just try to avoid using + // this to test implementation details). + store + } +} diff --git a/src/containers/AuthButton.js b/src/containers/AuthButton.js index 5a772ca..0b94870 100644 --- a/src/containers/AuthButton.js +++ b/src/containers/AuthButton.js @@ -1,36 +1,37 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React from 'react' +import PropTypes from 'prop-types' +import Button from '@material-ui/core/Button' +import { makeStyles } from '@material-ui/core/styles' -import Button from '@material-ui/core/Button'; -import { withStyles } from '@material-ui/core/styles'; - -class AuthButton extends React.Component { - render() { - const textContent = this.props.disabled ? 'Authorized' : 'Sign In'; - return ( - - ); +const useStyles = makeStyles({ + root: { + alignSelf: 'center' } +}) + +const AuthButton = ({ handleClick, authorizationUrl, disabled }) => { + const classes = useStyles + const textContent = disabled ? 'Authorized' : 'Sign In' + + return ( + + ) } AuthButton.propTypes = { - classes: PropTypes.object.isRequired, handleClick: PropTypes.func.isRequired, authorizationUrl: PropTypes.string.isRequired, disabled: PropTypes.bool -}; - -const styles = { - root: { - alignSelf: 'center' - } -}; +} -export default withStyles(styles)(AuthButton); +export default AuthButton diff --git a/src/containers/ResourceNode.js b/src/containers/ResourceNode.js index 13fa24e..143b853 100644 --- a/src/containers/ResourceNode.js +++ b/src/containers/ResourceNode.js @@ -1,6 +1,5 @@ import React from 'react' import PropTypes from 'prop-types' - import Checkbox from '@material-ui/core/Checkbox' import IconButton from '@material-ui/core/IconButton' import InsertDriveFileIcon from '@material-ui/icons/InsertDriveFile' @@ -35,6 +34,7 @@ class ResourceNode extends React.Component { return (
- + diff --git a/src/containers/ResourceTree.js b/src/containers/ResourceTree.js index fd5d5b6..7b1cf17 100644 --- a/src/containers/ResourceTree.js +++ b/src/containers/ResourceTree.js @@ -1,12 +1,10 @@ import React from 'react' import PropTypes from 'prop-types' - import Checkbox from '@material-ui/core/Checkbox' import FolderIcon from '@material-ui/icons/Folder' import FolderOpenIcon from '@material-ui/icons/FolderOpen' import IconButton from '@material-ui/core/IconButton' import { withStyles } from '@material-ui/core/styles' - import { getContainer, selectContainerForUpload, @@ -52,11 +50,15 @@ class ResourceTree extends React.Component { render() { return ( -
+
{!this.props.root && ( - {inputLabel} - - - ); +const useStyles = makeStyles({ + root: { + display: 'flex', + flexWrap: 'wrap' } +}) + +const SelectProvider = ({ selectedProvider, providers, handleChange }) => { + const classes = useStyles() + const value = selectedProvider.id ? selectedProvider.id : '' + const inputLabel = providers.isRequesting + ? 'Loading providers...' + : 'Select a storage provider' + + return ( + + {inputLabel} + + + ) } SelectProvider.propTypes = { - classes: PropTypes.object.isRequired, selectedProvider: PropTypes.object.isRequired, - providers: PropTypes.object.isRequired, + providers: PropTypes.shape({ + items: PropTypes.array, + id: PropTypes.string, + name: PropTypes.string + }), handleChange: PropTypes.func.isRequired -}; - -const styles = { - root: { - display: 'flex', - flexWrap: 'wrap' - } -}; +} -export default withStyles(styles)(SelectProvider); +export default SelectProvider diff --git a/src/containers/UploadForm.js b/src/containers/UploadForm.js index 2aca335..f435524 100644 --- a/src/containers/UploadForm.js +++ b/src/containers/UploadForm.js @@ -1,17 +1,16 @@ -import React from 'react'; -import PropTypes from 'prop-types'; +import React from 'react' +import PropTypes from 'prop-types' -import './UploadForm.css'; +import './UploadForm.css' -import Button from '@material-ui/core/Button'; -import Grid from '@material-ui/core/Grid'; -import Paper from '@material-ui/core/Paper'; -import Typography from '@material-ui/core/Typography'; -import { withStyles } from '@material-ui/core/styles'; +import Button from '@material-ui/core/Button' +import Grid from '@material-ui/core/Grid' +import Typography from '@material-ui/core/Typography' +import { withStyles } from '@material-ui/core/styles' -import SelectProvider from './SelectProvider'; -import AuthButton from './AuthButton'; -import ResourceTree from './ResourceTree'; +import SelectProvider from './SelectProvider' +import AuthButton from './AuthButton' +import ResourceTree from './ResourceTree' import { selectProvider, updateProviders, @@ -21,7 +20,7 @@ import { authorize, createAuthorization, createUpload -} from '../actions'; +} from '../actions' class UploadForm extends React.Component { // This should be refactored @@ -30,26 +29,26 @@ class UploadForm extends React.Component { currentSessionEmpty: true, rootContainerEmpty: true, currentUploadEmpty: true - }; + } constructor(props) { - super(props); - this.handleChangeProvider = this.handleChangeProvider.bind(this); - this.handleClickAuthButton = this.handleClickAuthButton.bind(this); - this.handleAuthorize = this.handleAuthorize.bind(this); + super(props) + this.handleChangeProvider = this.handleChangeProvider.bind(this) + this.handleClickAuthButton = this.handleClickAuthButton.bind(this) + this.handleAuthorize = this.handleAuthorize.bind(this) /** @todo Investigate why
isn't being dispatched */ - this.handleClickSubmit = this.handleClickSubmit.bind(this); + this.handleClickSubmit = this.handleClickSubmit.bind(this) } /** * This changes the provider when users select from the dropdown */ handleChangeProvider(event) { - const providerId = event.target.value; + const providerId = event.target.value const provider = this.props.providers.items.find( provider => provider.id === providerId - ); - this.props.dispatch(selectProvider(provider)); + ) + this.props.dispatch(selectProvider(provider)) } /** @@ -57,43 +56,43 @@ class UploadForm extends React.Component { */ handleClickAuthButton(event) { // This opens the new window for the OAuth - event.preventDefault(); - window.open(this.props.selectedProvider.authorizationUrl); + event.preventDefault() + window.open(this.props.selectedProvider.authorizationUrl) } handleAuthorize(event) { // Update the state with the authorization if (event.data && event.data.authToken) { - this.props.dispatch(authorize(event.data.authToken)); + this.props.dispatch(authorize(event.data.authToken)) } } handleClickSubmit() { - this.props.dispatch(createUpload(this.props.currentAuthToken.authToken)); + this.props.dispatch(createUpload(this.props.currentAuthToken.authToken)) } componentDidMount() { // Once the component mounts the DOM, retrieve all Providers from the API - this.props.dispatch(updateProviders()); - window.addEventListener('message', this.handleAuthorize); + this.props.dispatch(updateProviders()) + window.addEventListener('message', this.handleAuthorize) } updateCurrentSessionEmpty(currentSessionEmpty) { if (this.state.currentSessionEmpty !== currentSessionEmpty) { - this.setState({ currentSessionEmpty: currentSessionEmpty }); + this.setState({ currentSessionEmpty: currentSessionEmpty }) } } updateRootContainerEmpty(rootContainerEmpty) { // Request the root container if the Session is already established if (this.state.rootContainerEmpty !== rootContainerEmpty) { - this.setState({ rootContainerEmpty: rootContainerEmpty }); + this.setState({ rootContainerEmpty: rootContainerEmpty }) } } updateCurrentUploadEmpty(currentUploadEmpty) { if (this.state.currentUploadEmpty !== currentUploadEmpty) { - this.setState({ currentUploadEmpty: currentUploadEmpty }); + this.setState({ currentUploadEmpty: currentUploadEmpty }) } } @@ -101,7 +100,7 @@ class UploadForm extends React.Component { // Update the state when a provider has been selected which supports/does // not support authentication if (providerSupportsAuth !== this.state.providerSupportsAuth) { - this.setState({ providerSupportsAuth: providerSupportsAuth }); + this.setState({ providerSupportsAuth: providerSupportsAuth }) } } @@ -114,17 +113,17 @@ class UploadForm extends React.Component { */ componentDidUpdate() { const currentSessionEmpty = - Object.keys(this.props.currentSession.item).length === 0; - this.updateCurrentSessionEmpty(currentSessionEmpty); + Object.keys(this.props.currentSession.item).length === 0 + this.updateCurrentSessionEmpty(currentSessionEmpty) const rootContainerEmpty = - Object.keys(this.props.rootContainer.item).length === 0; - this.updateRootContainerEmpty(rootContainerEmpty); + Object.keys(this.props.rootContainer.item).length === 0 + this.updateRootContainerEmpty(rootContainerEmpty) const currentUploadEmpty = this.props.currentUpload.item.containers.length === 0 && - this.props.currentUpload.item.bytestreams.length === 0; - this.updateCurrentUploadEmpty(currentUploadEmpty); + this.props.currentUpload.item.bytestreams.length === 0 + this.updateCurrentUploadEmpty(currentUploadEmpty) if (!this.state.currentSessionEmpty && this.props.currentUpload.item.id) { /** @@ -132,10 +131,10 @@ class UploadForm extends React.Component { */ const uploadEvent = new CustomEvent('browseEverything.upload', { detail: this.props.currentUpload.item - }); - window.dispatchEvent(uploadEvent); + }) + window.dispatchEvent(uploadEvent) if (this.props.onUpload) { - this.props.onUpload.call(this, uploadEvent); + this.props.onUpload.call(this, uploadEvent) } // Reinitializing the state does not re-render the components @@ -145,8 +144,8 @@ class UploadForm extends React.Component { currentSessionEmpty: true, rootContainerEmpty: true, currentUploadEmpty: true - }); - this.props.dispatch(clearSession()); + }) + this.props.dispatch(clearSession()) } else if ( !this.state.currentSessionEmpty && this.state.rootContainerEmpty @@ -161,7 +160,7 @@ class UploadForm extends React.Component { this.props.currentSession.item, this.props.currentAuthToken.authToken ) - ); + ) } } else if ( this.state.currentSessionEmpty && @@ -173,14 +172,14 @@ class UploadForm extends React.Component { */ const requestedProvider = this.props.providers.items.find( provider => provider.id === this.props.selectedProvider.id - ); + ) if (!requestedProvider) { throw new Error( `Unsupported provider selected: ${this.props.selectedProvider.id}` - ); + ) } - const providerSupportsAuth = !!requestedProvider.authorizationUrl; - this.updateProviderSupportsAuth(providerSupportsAuth); + const providerSupportsAuth = !!requestedProvider.authorizationUrl + this.updateProviderSupportsAuth(providerSupportsAuth) if (this.props.currentAuthToken.authToken) { // We only want to request a new session if one is not already being @@ -191,18 +190,18 @@ class UploadForm extends React.Component { this.props.selectedProvider, this.props.currentAuthToken.authToken ) - ); + ) } } else if (!providerSupportsAuth) { if (!this.props.currentAuthToken.isRequesting) { - this.props.dispatch(createAuthorization()); + this.props.dispatch(createAuthorization()) } } } } render() { - let resourceTree; + let resourceTree if ( !this.props.currentUpload.isRequesting && @@ -214,14 +213,14 @@ class UploadForm extends React.Component { container={this.props.rootContainer.item} dispatch={this.props.dispatch} /> - ); + ) } else { - let rootContainerText = 'Please select a provider'; + let rootContainerText = 'Please select a provider' if (this.props.currentUpload.isRequesting) { - rootContainerText = 'Uploading files...'; + rootContainerText = 'Uploading files...' } else if (!!this.props.currentAuthToken.authToken) { - rootContainerText = 'Loading content...'; + rootContainerText = 'Loading content...' } resourceTree = ( @@ -232,11 +231,11 @@ class UploadForm extends React.Component { > {rootContainerText} - ); + ) } return ( - + -
{resourceTree}
+
{resourceTree}