Skip to content

Commit

Permalink
refactor(app): move robot lights controls to state.robotControls (#4631)
Browse files Browse the repository at this point in the history
  • Loading branch information
mcous authored Dec 18, 2019
1 parent a947a14 commit 0920dc8
Show file tree
Hide file tree
Showing 26 changed files with 1,044 additions and 287 deletions.
139 changes: 36 additions & 103 deletions app/src/components/RobotSettings/ControlsCard.js
Original file line number Diff line number Diff line change
@@ -1,92 +1,56 @@
// @flow
// "Robot Controls" card
import * as React from 'react'
import { connect } from 'react-redux'
import { useDispatch, useSelector } from 'react-redux'
import { push } from 'connected-react-router'

import {
home,
fetchRobotLights,
setRobotLights,
makeGetRobotLights,
startDeckCalibration,
} from '../../http-api-client'

import { home, startDeckCalibration } from '../../http-api-client'
import { fetchLights, updateLights, getLightsOn } from '../../robot-controls'
import { restartRobot } from '../../robot-admin'
import { selectors as robotSelectors } from '../../robot'
import { CONNECTABLE } from '../../discovery'

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

import type { State, Dispatch } from '../../types'
import type { ViewableRobot } from '../../discovery/types'

type OP = {|
type Props = {|
robot: ViewableRobot,
calibrateDeckUrl: string,
|}

type SP = {|
lightsOn: boolean,
homeEnabled: boolean,
restartEnabled: boolean,
|}

type DP = {|
dispatch: Dispatch,
|}

type Props = {
...OP,
...SP,
homeAll: () => mixed,
restartRobot: () => mixed,
fetchLights: () => mixed,
toggleLights: () => mixed,
start: () => mixed,
}

const TITLE = 'Robot Controls'

export default connect<Props, OP, SP, {||}, State, Dispatch>(
makeMakeStateToProps,
null,
mergeProps
)(ControlsCard)

const CALIBRATE_DECK_DESCRIPTION =
"Calibrate the position of the robot's deck. Recommended for all new robots and after moving robots."

function ControlsCard(props: Props) {
const {
lightsOn,
fetchLights,
toggleLights,
homeAll,
homeEnabled,
restartRobot,
restartEnabled,
start,
} = props
const { name, status } = props.robot
const disabled = status !== CONNECTABLE
export function ControlsCard(props: Props) {
const dispatch = useDispatch<Dispatch>()
const { robot, calibrateDeckUrl } = props
const { name: robotName, status } = robot
const lightsOn = useSelector((state: State) => getLightsOn(state, robotName))
const isRunning = useSelector(robotSelectors.getIsRunning)
const notConnectable = status !== CONNECTABLE
const toggleLights = () => dispatch(updateLights(robotName, !lightsOn))
const canControl = robot.connected && !isRunning

const startCalibration = () =>
dispatch(startDeckCalibration(robot)).then(() =>
dispatch(push(calibrateDeckUrl))
)

React.useEffect(() => {
dispatch(fetchLights(robotName))
}, [dispatch, robotName])

return (
<RefreshCard
title={TITLE}
watch={name}
refresh={fetchLights}
disabled={disabled}
>
<Card title={TITLE} disabled={notConnectable}>
<LabeledButton
label="Calibrate deck"
buttonProps={{
onClick: start,
disabled: disabled,
onClick: startCalibration,
disabled: notConnectable || !canControl,
children: 'Calibrate',
}}
>
Expand All @@ -95,8 +59,8 @@ function ControlsCard(props: Props) {
<LabeledButton
label="Home all axes"
buttonProps={{
onClick: homeAll,
disabled: disabled || !homeEnabled,
onClick: () => dispatch(home(robot)),
disabled: notConnectable || !canControl,
children: 'Home',
}}
>
Expand All @@ -105,51 +69,20 @@ function ControlsCard(props: Props) {
<LabeledButton
label="Restart robot"
buttonProps={{
onClick: restartRobot,
disabled: disabled || !restartEnabled,
onClick: () => dispatch(restartRobot(robotName)),
disabled: notConnectable || !canControl,
children: 'Restart',
}}
>
<p>Restart robot.</p>
</LabeledButton>
<LabeledToggle label="Lights" toggledOn={lightsOn} onClick={toggleLights}>
<LabeledToggle
label="Lights"
toggledOn={Boolean(lightsOn)}
onClick={toggleLights}
>
<p>Control lights on deck.</p>
</LabeledToggle>
</RefreshCard>
</Card>
)
}

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

return (state, ownProps) => {
const { robot } = ownProps
const lights = getRobotLights(state, robot)
const isRunning = robotSelectors.getIsRunning(state)

return {
lightsOn: !!(lights && lights.response && lights.response.on),
homeEnabled: robot.connected === true && !isRunning,
restartEnabled: robot.connected === true && !isRunning,
}
}
}

function mergeProps(stateProps: SP, dispatchProps: DP, ownProps: OP): Props {
const { robot, calibrateDeckUrl } = ownProps
const { lightsOn } = stateProps
const { dispatch } = dispatchProps

return {
...ownProps,
...stateProps,
homeAll: () => dispatch(home(robot)),
restartRobot: () => dispatch(restartRobot(robot.name)),
fetchLights: () => dispatch(fetchRobotLights(robot)),
toggleLights: () => dispatch(setRobotLights(robot, !lightsOn)),
start: () =>
dispatch(startDeckCalibration(robot)).then(() =>
dispatch(push(calibrateDeckUrl))
),
}
}
195 changes: 195 additions & 0 deletions app/src/components/RobotSettings/__tests__/ControlsCard.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,195 @@
// @flow
import * as React from 'react'
import { Provider } from 'react-redux'
import { mount } from 'enzyme'

import * as RobotControls from '../../../robot-controls'
import * as RobotAdmin from '../../../robot-admin'
import * as RobotSelectors from '../../../robot/selectors'
import { ControlsCard } from '../ControlsCard'
import { LabeledToggle, LabeledButton } from '@opentrons/components'
import { CONNECTABLE, REACHABLE } from '../../../discovery'

import type { State } from '../../../types'
import type { ViewableRobot } from '../../../discovery/types'

jest.mock('../../../robot-controls/selectors')
jest.mock('../../../robot/selectors')

const mockRobot: ViewableRobot = ({
name: 'robot-name',
connected: true,
status: CONNECTABLE,
}: any)

const mockUnconnectableRobot: ViewableRobot = ({
name: 'robot-name',
connected: true,
status: REACHABLE,
}: any)

const mockGetLightsOn: JestMockFn<
[State, string],
$Call<typeof RobotControls.getLightsOn, State, string>
> = RobotControls.getLightsOn

const mockGetIsRunning: JestMockFn<
[State],
$Call<typeof RobotSelectors.getIsRunning, State>
> = RobotSelectors.getIsRunning

describe('ControlsCard', () => {
let mockStore

const getDeckCalButton = wrapper =>
wrapper
.find({ label: 'Calibrate deck' })
.find(LabeledButton)
.find('button')

const getHomeButton = wrapper =>
wrapper
.find({ label: 'Home all axes' })
.find(LabeledButton)
.find('button')

const getRestartButton = wrapper =>
wrapper
.find({ label: 'Restart robot' })
.find(LabeledButton)
.find('button')

const getLightsButton = wrapper =>
wrapper.find({ label: 'Lights' }).find(LabeledToggle)

beforeEach(() => {
mockStore = {
subscribe: () => {},
getState: () => ({ mockState: true }),
dispatch: jest.fn(),
}
})

afterEach(() => {
jest.resetAllMocks()
})

test('calls fetchLights on mount', () => {
mount(
<Provider store={mockStore}>
<ControlsCard robot={mockRobot} calibrateDeckUrl="/deck/calibrate" />
</Provider>
)

expect(mockStore.dispatch).toHaveBeenCalledWith(
RobotControls.fetchLights(mockRobot.name)
)
})

test('calls updateLights with toggle on button click', () => {
mockGetLightsOn.mockReturnValue(true)

const wrapper = mount(
<Provider store={mockStore}>
<ControlsCard robot={mockRobot} calibrateDeckUrl="/deck/calibrate" />
</Provider>
)

getLightsButton(wrapper).invoke('onClick')()

expect(mockStore.dispatch).toHaveBeenCalledWith(
RobotControls.updateLights(mockRobot.name, false)
)
})

test('calls restartRobot on button click', () => {
const wrapper = mount(
<Provider store={mockStore}>
<ControlsCard robot={mockRobot} calibrateDeckUrl="/deck/calibrate" />
</Provider>
)

getRestartButton(wrapper).invoke('onClick')()

expect(mockStore.dispatch).toHaveBeenCalledWith(
RobotAdmin.restartRobot(mockRobot.name)
)
})

// TODO(mc, 2019-12-17): enable test when POST /robot/home is epic-based
test.skip('calls home on button click', () => {
const wrapper = mount(
<Provider store={mockStore}>
<ControlsCard robot={mockRobot} calibrateDeckUrl="/deck/calibrate" />
</Provider>
)

getHomeButton(wrapper).invoke('onClick')()

// expect(mockStore.dispatch).toHaveBeenCalledWith(RobotControls.home(mockRobot.name))
})

test('DC, home, and restart buttons enabled if connected and not running', () => {
mockGetIsRunning.mockReturnValue(false)

const wrapper = mount(
<Provider store={mockStore}>
<ControlsCard
robot={mockUnconnectableRobot}
calibrateDeckUrl="/deck/calibrate"
/>
</Provider>
)

expect(getDeckCalButton(wrapper).prop('disabled')).toBe(true)
expect(getHomeButton(wrapper).prop('disabled')).toBe(true)
expect(getRestartButton(wrapper).prop('disabled')).toBe(true)
})

test('DC, home, and restart buttons disabled if not connectable', () => {
const wrapper = mount(
<Provider store={mockStore}>
<ControlsCard
robot={mockUnconnectableRobot}
calibrateDeckUrl="/deck/calibrate"
/>
</Provider>
)

expect(getDeckCalButton(wrapper).prop('disabled')).toBe(true)
expect(getHomeButton(wrapper).prop('disabled')).toBe(true)
expect(getRestartButton(wrapper).prop('disabled')).toBe(true)
})

test('DC, home, and restart buttons disabled if not connected', () => {
const mockRobot: ViewableRobot = ({
name: 'robot-name',
connected: false,
status: CONNECTABLE,
}: any)

const wrapper = mount(
<Provider store={mockStore}>
<ControlsCard robot={mockRobot} calibrateDeckUrl="/deck/calibrate" />
</Provider>
)

expect(getDeckCalButton(wrapper).prop('disabled')).toBe(true)
expect(getHomeButton(wrapper).prop('disabled')).toBe(true)
expect(getRestartButton(wrapper).prop('disabled')).toBe(true)
})

test('DC, home, and restart buttons disabled if protocol running', () => {
mockGetIsRunning.mockReturnValue(true)

const wrapper = mount(
<Provider store={mockStore}>
<ControlsCard robot={mockRobot} calibrateDeckUrl="/deck/calibrate" />
</Provider>
)

expect(getDeckCalButton(wrapper).prop('disabled')).toBe(true)
expect(getHomeButton(wrapper).prop('disabled')).toBe(true)
expect(getRestartButton(wrapper).prop('disabled')).toBe(true)
})
})
2 changes: 1 addition & 1 deletion app/src/components/RobotSettings/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import * as React from 'react'
import { CardContainer, CardRow } from '../layout'
import StatusCard from './StatusCard'
import InformationCard from './InformationCard'
import ControlsCard from './ControlsCard'
import { ControlsCard } from './ControlsCard'
import ConnectionCard from './ConnectionCard'
import AdvancedSettingsCard from './AdvancedSettingsCard'
import ConnectAlertModal from './ConnectAlertModal'
Expand Down
Loading

0 comments on commit 0920dc8

Please sign in to comment.