Skip to content

Commit

Permalink
feat(app): Add toggle to turn on/off robot rail lights
Browse files Browse the repository at this point in the history
Closes #1684
  • Loading branch information
mcous committed Jun 15, 2018
1 parent edc200e commit 6fc80ba
Show file tree
Hide file tree
Showing 5 changed files with 232 additions and 44 deletions.
56 changes: 45 additions & 11 deletions app/src/components/RobotSettings/ControlsCard.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,47 @@
import * as React from 'react'
import {connect} from 'react-redux'

import {Card} from '@opentrons/components'
import {
fetchRobotLights,
setRobotLights,
makeGetRobotLights
} from '../../http-api-client'

import {RefreshCard} from '@opentrons/components'
import {LabeledToggle, LabeledButton} from '../controls'

import type {State, Dispatch} from '../../types'
import type {Robot} from '../../robot'

type OP = Robot

type SP = {}
type SP = {
lightsOn: boolean,
}

type DP = {}
type DP = {
dispatch: Dispatch
}

type Props = OP & SP & DP
type Props = OP & SP & {
fetchLights: () => mixed,
toggleLights: () => mixed
}

const TITLE = 'Robot Controls'

export default connect(makeMakeStateToProps, mapDispatchToProps)(ControlsCard)
export default connect(makeMakeStateToProps, null, mergeProps)(ControlsCard)

function ControlsCard (props: Props) {
const {name, lightsOn, fetchLights, toggleLights} = props

return (
<Card title={TITLE} column>
<LabeledToggle label='Lights' toggledOn={false} onClick={() => {}}>
<RefreshCard title={TITLE} watch={name} refresh={fetchLights} column>
<LabeledToggle
label='Lights'
toggledOn={lightsOn}
onClick={toggleLights}
>
<p>Control lights on deck.</p>
</LabeledToggle>
<LabeledButton
Expand All @@ -33,14 +52,29 @@ function ControlsCard (props: Props) {
>
<p>Return robot to starting position.</p>
</LabeledButton>
</Card>
</RefreshCard>
)
}

function makeMakeStateToProps (): (state: State, ownProps: OP) => SP {
return (state, ownProps) => ({})
const getRobotLights = makeGetRobotLights()

return (state, ownProps) => {
const lights = getRobotLights(state, ownProps)
const lightsOn = !!(lights && lights.response && lights.response.on)

return {lightsOn}
}
}

function mapDispatchToProps (dispatch: Dispatch, ownProps: OP): DP {
return {}
function mergeProps (stateProps: SP, dispatchProps: DP, ownProps: OP): Props {
const {lightsOn} = stateProps
const {dispatch} = dispatchProps

return {
...ownProps,
...stateProps,
fetchLights: () => dispatch(fetchRobotLights(ownProps)),
toggleLights: () => dispatch(setRobotLights(ownProps, !lightsOn))
}
}
100 changes: 98 additions & 2 deletions app/src/http-api-client/__tests__/robot.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,12 @@ import client from '../client'
import {
moveRobotTo,
home,
fetchRobotLights,
setRobotLights,
reducer,
makeGetRobotMove,
makeGetRobotHome
makeGetRobotHome,
makeGetRobotLights
} from '..'

jest.mock('../client')
Expand Down Expand Up @@ -139,6 +142,86 @@ describe('robot/*', () => {
})
})

describe('fetchRobotLights action creator', () => {
const path = 'lights'
const response = {on: true}

test('calls GET /robot/lights', () => {
client.__setMockResponse(response)

return store.dispatch(fetchRobotLights(robot))
.then(() => expect(client)
.toHaveBeenCalledWith(robot, 'GET', 'robot/lights'))
})

test('dispatches ROBOT_REQUEST and ROBOT_SUCCESS', () => {
const expectedActions = [
{type: 'api:ROBOT_REQUEST', payload: {robot, path}},
{type: 'api:ROBOT_SUCCESS', payload: {robot, response, path}}
]

client.__setMockResponse(response)

return store.dispatch(fetchRobotLights(robot))
.then(() => expect(store.getActions()).toEqual(expectedActions))
})

test('dispatches ROBOT_REQUEST and ROBOT_FAILURE', () => {
const error = {name: 'ResponseError', status: '400'}
const expectedActions = [
{type: 'api:ROBOT_REQUEST', payload: {robot, path}},
{type: 'api:ROBOT_FAILURE', payload: {robot, error, path}}
]

client.__setMockError(error)

return store.dispatch(fetchRobotLights(robot))
.then(() => expect(store.getActions()).toEqual(expectedActions))
})
})

describe('setRobotLights action creator', () => {
const path = 'lights'
const response = {on: false}

test('calls POST /robot/home to home robot', () => {
const expectedBody = {on: false}

client.__setMockResponse(response)

return store.dispatch(setRobotLights(robot, false))
.then(() => expect(client)
.toHaveBeenCalledWith(robot, 'POST', 'robot/lights', expectedBody))
})

test('dispatches ROBOT_REQUEST and ROBOT_SUCCESS', () => {
const request = {on: true}
const expectedActions = [
{type: 'api:ROBOT_REQUEST', payload: {robot, request, path}},
{type: 'api:ROBOT_SUCCESS', payload: {robot, response, path}}
]

client.__setMockResponse(response)

return store.dispatch(setRobotLights(robot, true))
.then(() => expect(store.getActions()).toEqual(expectedActions))
})

test('dispatches ROBOT_REQUEST and ROBOT_FAILURE', () => {
const request = {on: false}
const error = {name: 'ResponseError', status: '400'}
const expectedActions = [
{type: 'api:ROBOT_REQUEST', payload: {robot, request, path}},
{type: 'api:ROBOT_FAILURE', payload: {robot, error, path}}
]

client.__setMockError(error)

return store.dispatch(setRobotLights(robot, false))
.then(() => expect(store.getActions()).toEqual(expectedActions))
})
})

const REDUCER_REQUEST_RESPONSE_TESTS = [
{
path: 'move',
Expand All @@ -149,6 +232,11 @@ describe('robot/*', () => {
path: 'home',
request: {target: 'pipette', mount: 'left'},
response: {message: 'we did it!'}
},
{
path: 'lights',
request: null,
response: {on: true}
}
]

Expand Down Expand Up @@ -239,7 +327,8 @@ describe('robot/*', () => {
beforeEach(() => {
state.api.robot[NAME] = {
home: {inProgress: true},
move: {inProgress: true}
move: {inProgress: true},
lights: {inProgress: true}
}
})

Expand All @@ -256,5 +345,12 @@ describe('robot/*', () => {
expect(getHome(state, robot)).toEqual(state.api.robot[NAME].home)
expect(getHome(state, {name: 'foo'})).toEqual({inProgress: false})
})

test('makeGetRobotLights', () => {
const getLights = makeGetRobotLights()

expect(getLights(state, robot)).toEqual(state.api.robot[NAME].lights)
expect(getLights(state, {name: 'foo'})).toEqual({inProgress: false})
})
})
})
8 changes: 6 additions & 2 deletions app/src/http-api-client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export type {

export type {
RobotMove,
RobotHome
RobotHome,
RobotLights
} from './robot'

export type {
Expand Down Expand Up @@ -112,8 +113,11 @@ export {
home,
moveRobotTo,
clearRobotMoveResponse,
fetchRobotLights,
setRobotLights,
makeGetRobotMove,
makeGetRobotHome
makeGetRobotHome,
makeGetRobotLights
} from './robot'

export {
Expand Down
94 changes: 73 additions & 21 deletions app/src/http-api-client/robot.js
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,19 @@ type RobotHomeResponse = {
message: string,
}

type RequestPath = 'move' | 'home'
type RobotLightsRequest = ?{
on: boolean
}

type RobotLightsResponse = {
on: boolean
}

type RobotRequest = RobotMoveRequest | RobotHomeRequest
type RequestPath = 'move' | 'home' | 'lights'

type RobotResponse = RobotMoveResponse | RobotHomeResponse
type RobotRequest = RobotMoveRequest | RobotHomeRequest | RobotLightsRequest

type RobotResponse = RobotMoveResponse | RobotHomeResponse | RobotLightsResponse

type RobotRequestAction = {|
type: 'api:ROBOT_REQUEST',
Expand Down Expand Up @@ -99,9 +107,12 @@ export type RobotMove = ApiCall<RobotMoveRequest, RobotMoveResponse>

export type RobotHome = ApiCall<RobotHomeRequest, RobotHomeResponse>

export type RobotLights = ApiCall<RobotLightsRequest, RobotLightsResponse>

type RobotByNameState = {
move?: RobotMove,
home?: RobotHome,
lights?: RobotLights,
}

type RobotState = {
Expand All @@ -110,6 +121,7 @@ type RobotState = {

const MOVE: RequestPath = 'move'
const HOME: RequestPath = 'home'
const LIGHTS: RequestPath = 'lights'

export function moveRobotTo (
robot: RobotService,
Expand Down Expand Up @@ -163,6 +175,37 @@ export function home (robot: RobotService, mount?: Mount): ThunkPromiseAction {
}
}

export function fetchRobotLights (robot: RobotService): ThunkPromiseAction {
return (dispatch) => {
dispatch(robotRequest(robot, LIGHTS))

return client(robot, 'GET', 'robot/lights')
.then(
(response) => robotSuccess(robot, LIGHTS, response),
(error) => robotFailure(robot, LIGHTS, error)
)
.then(dispatch)
}
}

export function setRobotLights (
robot: RobotService,
on: boolean
): ThunkPromiseAction {
const body = {on}

return (dispatch) => {
dispatch(robotRequest(robot, LIGHTS, body))

return client(robot, 'POST', 'robot/lights', body)
.then(
(response) => robotSuccess(robot, LIGHTS, response),
(error) => robotFailure(robot, LIGHTS, error)
)
.then(dispatch)
}
}

export function robotReducer (state: ?RobotState, action: Action): RobotState {
if (!state) return {}

Expand Down Expand Up @@ -235,6 +278,33 @@ export function robotReducer (state: ?RobotState, action: Action): RobotState {
return state
}

export const makeGetRobotMove = () => {
const selector: Selector<State, BaseRobot, RobotMove> = createSelector(
selectRobotState,
(state) => state.move || {inProgress: false}
)

return selector
}

export const makeGetRobotHome = () => {
const selector: Selector<State, BaseRobot, RobotHome> = createSelector(
selectRobotState,
(state) => state.home || {inProgress: false}
)

return selector
}

export const makeGetRobotLights = () => {
const selector: Selector<State, BaseRobot, RobotLights> = createSelector(
selectRobotState,
(state) => state.lights || {inProgress: false}
)

return selector
}

function robotRequest (
robot: RobotService,
path: RequestPath,
Expand Down Expand Up @@ -262,24 +332,6 @@ function robotFailure (
return {type: 'api:ROBOT_FAILURE', payload: {robot, path, error}}
}

export const makeGetRobotMove = () => {
const selector: Selector<State, BaseRobot, RobotMove> = createSelector(
selectRobotState,
(state) => state.move || {inProgress: false}
)

return selector
}

export const makeGetRobotHome = () => {
const selector: Selector<State, BaseRobot, RobotHome> = createSelector(
selectRobotState,
(state) => state.home || {inProgress: false}
)

return selector
}

function selectRobotState (state: State, props: BaseRobot): RobotByNameState {
return state.api.robot[props.name] || {}
}
Loading

0 comments on commit 6fc80ba

Please sign in to comment.