Skip to content

Commit

Permalink
feat(app): Parse JSON protocols into state (#2231)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous authored Sep 13, 2018
1 parent 55d2cd5 commit b5f3666
Show file tree
Hide file tree
Showing 26 changed files with 485 additions and 196 deletions.
9 changes: 6 additions & 3 deletions app/src/analytics/__tests__/make-event.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,10 +61,13 @@ describe('analytics events map', () => {
})
})

test('robot:SESSION_RESPONSE error -> protocolUpload event', () => {
test('robot:SESSION_RESPONSE/ERROR -> protocolUpload event', () => {
const state = {}
const success = {type: 'robot:SESSION_RESPONSE'}
const failure = {type: 'robot:SESSION_RESPONSE', error: new Error('AH')}
const success = {type: 'robot:SESSION_RESPONSE', payload: {}}
const failure = {
type: 'robot:SESSION_ERROR',
payload: {error: new Error('AH')},
}

expect(makeEvent(state, success)).toEqual({
name: 'protocolUpload',
Expand Down
6 changes: 3 additions & 3 deletions app/src/analytics/make-event.js
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,14 @@ export default function makeEvent (state: State, action: Action): ?Event {
},
}

// $FlowFixMe(ka, 2018-06-5): flow type robot:SESSION_RESPONSE
case 'robot:SESSION_RESPONSE':
case 'robot:SESSION_ERROR':
// TODO (ka, 2018-6-6): add file open type 'button' | 'drag-n-drop' (work required in action meta)
return {
name: 'protocolUpload',
properties: {
success: !action.error,
error: (action.error && action.error.message) || '',
success: action.type === 'robot:SESSION_RESPONSE',
error: (action.payload.error && action.payload.error.message) || '',
},
}

Expand Down
6 changes: 2 additions & 4 deletions app/src/components/SessionHeader/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,7 @@ import React from 'react'
import {connect} from 'react-redux'
import {Link} from 'react-router-dom'

import {
selectors as robotSelectors,
} from '../../robot'
import {getProtocolFilename} from '../../protocol'

export default connect(mapStateToProps)(SessionHeader)

Expand All @@ -18,6 +16,6 @@ function SessionHeader (props) {

function mapStateToProps (state) {
return {
sessionName: robotSelectors.getSessionName(state),
sessionName: getProtocolFilename(state),
}
}
8 changes: 3 additions & 5 deletions app/src/components/UploadPanel/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@ import React from 'react'
import {connect} from 'react-redux'
import {push} from 'react-router-redux'

import {
actions as robotActions,
selectors as robotSelectors,
} from '../../robot'
import {selectors as robotSelectors} from '../../robot'
import {openProtocol} from '../../protocol'

import {SidePanel} from '@opentrons/components'
import Upload from './Upload'
Expand Down Expand Up @@ -38,6 +36,6 @@ function mapStateToProps (state) {
function mapDispatchToProps (dispatch) {
return {
confirmUpload: () => dispatch(push('/upload/confirm')),
createSession: (file) => dispatch(robotActions.session(file)),
createSession: (file) => dispatch(openProtocol(file)),
}
}
21 changes: 0 additions & 21 deletions app/src/pages/Upload/UploadStatus.js

This file was deleted.

30 changes: 20 additions & 10 deletions app/src/pages/Upload/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,18 @@ import {connect} from 'react-redux'
import {withRouter, Route, Switch, Redirect, type Match} from 'react-router'
import type {State} from '../../types'
import type {Robot} from '../../robot'

import {selectors as robotSelectors} from '../../robot'
import {getProtocolFilename} from '../../protocol'

import {Splash, SpinnerModal} from '@opentrons/components'
import Page from '../../components/Page'
import UploadStatus from './UploadStatus'
import UploadError from '../../components/UploadError'
import FileInfo from './FileInfo'

type SP = {
robot: ?Robot,
name: string,
name: ?string,
uploadInProgress: boolean,
uploadError: ?{message: string},
protocolRunning: boolean,
Expand All @@ -32,7 +35,7 @@ function mapStateToProps (state: State, ownProps: OP): SP {
const connectedRobot = robotSelectors.getConnectedRobot(state)
return {
robot: connectedRobot,
name: robotSelectors.getSessionName(state),
name: getProtocolFilename(state),
uploadInProgress: robotSelectors.getSessionLoadInProgress(state),
uploadError: robotSelectors.getUploadError(state),
protocolRunning: robotSelectors.getIsRunning(state),
Expand All @@ -42,19 +45,26 @@ function mapStateToProps (state: State, ownProps: OP): SP {

function UploadPage (props: Props) {
const {robot, name, uploadInProgress, uploadError, match: {path}} = props
const fileInfoPath = `${path}/file-info`

if (!robot) return (<Redirect to='/robots' />)
if (!name && !uploadInProgress && !uploadError) return (<Page><Splash /></Page>)
if (uploadInProgress) return (<Page><SpinnerModal message='Upload in Progress'/></Page>)
if (!name) return (<Page><Splash /></Page>)

if (uploadInProgress) {
return (<Page><SpinnerModal message='Upload in Progress'/></Page>)
}

if (uploadError) {
return (<Page><UploadError name={name} uploadError={uploadError}/></Page>)
}

return (
<Switch>
<Redirect exact from={path} to={fileInfoPath} />
<Route
path={`${path}/file-info`}
path={fileInfoPath}
render={props => (<FileInfo name={name} robot={robot} />)}
/>
<Route
path={path}
render={() => (<UploadStatus {...props}/>)}
/>
</Switch>
)
}
89 changes: 89 additions & 0 deletions app/src/protocol/__tests__/actions.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// protocol actions tests
import configureMockStore from 'redux-mock-store'
import thunk from 'redux-thunk'

import {openProtocol} from '..'

const middlewares = [thunk]
const mockStore = configureMockStore(middlewares)

describe('protocol actions', () => {
let store
let _FileReader
let mockReader

beforeEach(() => {
_FileReader = global.FileReader
mockReader = {readAsText: jest.fn()}

store = mockStore({})
global.FileReader = jest.fn(() => mockReader)
})

afterEach(() => {
jest.resetAllMocks()
global.FileReader = _FileReader
})

describe('openProtocol with python protocol', () => {
const pythonFile = {
name: 'foobar.py',
type: 'application/x-python-code',
lastModified: 123,
}

const jsonFile = {
name: 'foobar.json',
type: 'application/json',
lastModified: 456,
}

test('dispatches a protocol:OPEN', () => {
const result = store.dispatch(openProtocol(pythonFile))
const expected = {
type: 'protocol:OPEN',
payload: {file: pythonFile},
}

expect(result).toEqual(expected)
expect(store.getActions()).toEqual([expected])
})

test('reads a file', () => {
store.dispatch(openProtocol(pythonFile))
expect(mockReader.readAsText).toHaveBeenCalledWith(pythonFile)
expect(mockReader.onload).toEqual(expect.any(Function))
})

test('dispatches protocol:UPLOAD on python read completion', () => {
store.dispatch(openProtocol(pythonFile))
mockReader.result = 'file contents'
mockReader.onload()

const actions = store.getActions()
expect(actions).toHaveLength(2)
expect(actions[1]).toEqual({
type: 'protocol:UPLOAD',
meta: {robot: true},
payload: {contents: 'file contents', data: null},
})
})

test('dispatches protocol:UPLOAD on JSON read completion', () => {
const protocol = {metadata: {}}

store.dispatch(openProtocol(jsonFile))
mockReader.result = JSON.stringify(protocol)
mockReader.onload()

expect(store.getActions()[1]).toEqual({
type: 'protocol:UPLOAD',
meta: {robot: true},
payload: {
contents: mockReader.result,
data: protocol,
},
})
})
})
})
68 changes: 68 additions & 0 deletions app/src/protocol/__tests__/reducer.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// protocol state reducer tests

import {protocolReducer} from '..'

describe('protocolReducer', () => {
test('initial state', () => {
expect(protocolReducer(undefined, {})).toEqual({
file: null,
contents: null,
data: null,
})
})

const SPECS = [
{
name: 'handles protocol:OPEN',
action: {type: 'protocol:OPEN', payload: {file: {name: 'proto.py'}}},
initialState: {file: {}, contents: 'foobar', data: {}},
expectedState: {file: {name: 'proto.py'}, contents: null, data: null},
},
{
name: 'handles protocol:UPLOAD',
action: {type: 'protocol:UPLOAD', payload: {contents: 'foo', data: {}}},
initialState: {file: {name: 'proto.py'}, contents: null, data: null},
expectedState: {file: {name: 'proto.py'}, contents: 'foo', data: {}},
},
{
name: 'handles robot:SESSION_RESPONSE with non-JSON protocol',
action: {
type: 'robot:SESSION_RESPONSE',
payload: {name: 'foo', protocolText: 'bar'},
},
initialState: {file: null, contents: null, data: null},
expectedState: {
file: {name: 'foo', type: null, lastModified: null},
contents: 'bar',
data: null,
},
},
{
name: 'handles robot:SESSION_RESPONSE with JSON protocol',
action: {
type: 'robot:SESSION_RESPONSE',
payload: {name: 'foo.json', protocolText: '{"metadata": {}}'},
},
initialState: {file: null, contents: null, data: null},
expectedState: {
file: {name: 'foo.json', type: 'application/json', lastModified: null},
contents: '{"metadata": {}}',
data: {metadata: {}},
},
},
{
name: 'handles robot:DISCONNECT by clearing state',
action: {type: 'robot:DISCONNECT_RESPONSE'},
initialState: {file: {name: 'proto.py'}, contents: 'foo', data: {}},
expectedState: {file: null, contents: null, data: null},
},
]

SPECS.forEach(spec => {
const {name, action, initialState, expectedState} = spec

test(name, () => {
expect(protocolReducer(initialState, action)).toEqual(expectedState)
})
})
})
31 changes: 31 additions & 0 deletions app/src/protocol/__tests__/selectors.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// protocol state selector tests

import * as protocol from '..'

const SPECS = [
{
name: 'getProtocolFile',
selector: protocol.getProtocolFile,
state: {protocol: {file: {name: 'proto.json'}}},
expected: {name: 'proto.json'},
},
{
name: 'getProtocolFilename with no file',
selector: protocol.getProtocolFilename,
state: {protocol: {file: null}},
expected: null,
},
{
name: 'getProtocolFilename',
selector: protocol.getProtocolFilename,
state: {protocol: {file: {name: 'proto.json'}}},
expected: 'proto.json',
},
]

describe('protocol selectors', () => {
SPECS.forEach(spec => {
const {name, selector, state, expected} = spec
test(name, () => expect(selector(state)).toEqual(expected))
})
})
Loading

0 comments on commit b5f3666

Please sign in to comment.