Skip to content

Commit

Permalink
refactor(app): wire up heater shaker wizard TestShake page (#9833)
Browse files Browse the repository at this point in the history
* refactor(app): wire up heater shaker wizard TestShake page

This PR wires up the buttons and input for the testshake page of the heater shaker wizard.

closes #9613
  • Loading branch information
sakibh authored Apr 7, 2022
1 parent 81914f6 commit 1f34210
Show file tree
Hide file tree
Showing 11 changed files with 473 additions and 71 deletions.
6 changes: 4 additions & 2 deletions app/src/assets/localization/en/heater_shaker.json
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@
"check_alignment": "Check alignment.",
"a_properly_attached_adapter": "A properly attached adapter will sit evenly on the module.",
"check_alignment_instructions": "Check attachment by rocking the adapter back and forth.",
"modal_title": "{{name}} - Attach Heater Shaker Module",
"intro_title": "Use this guide to attach the Heater Shaker Module to your robot’s deck for secure shaking.",
"intro_subtitle": "You will need:",
"intro_heater_shaker_mod": "Heater Shaker Module",
Expand Down Expand Up @@ -58,6 +57,7 @@
"heater_shaker_anchor_description": "<block>The 2 <strong>Anchors</strong> keep the module attached to the deck while it is shaking.</block> <block>The screw above each anchor is used to extend and retract them. See animation below.</block> <block>Extending the bolts slightly increases the module’s footprint, which allows it to be more firmly attached to the edges of a slot.</block>",
"step_4_of_4": "Step 4 of 4: Test shake",
"test_shake_banner_information": "If you want to add labware to the module before doing a test shake, you can use the labware latch controls to hold the latches open.",
"test_shake_banner_labware_information": "If you want to add the <strong>{{labware}}</strong> to the module before doing a test shake, you can use the labware latch controls.",
"open_labware_latch": "Open Labware Latch",
"close_labware_latch": "Close Labware Latch",
"labware_latch": "Labware Latch",
Expand Down Expand Up @@ -89,5 +89,7 @@
"module_anchors_extended": "Before the run begins, module should have both anchors fully extended for a firm attachment to Slot {{slot}}.",
"thermal_adapter_attached_to_module": "The thermal adapter should be attached to the module.",
"proceed_to_run": "Proceed to run",
"confirm_attachment": "Confirm attachment"
"confirm_attachment": "Confirm attachment",
"closed": "Closed",
"closed_and_locked": "Closed and Locked"
}
5 changes: 5 additions & 0 deletions app/src/atoms/InputField/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,11 @@ function Input(props: InputFieldProps): JSX.Element {
&:disabled {
border: ${SPACING.spacingXXS} ${BORDERS.styleSolid} ${COLORS.greyDisabled};
}
input[type='number']::-webkit-inner-spin-button,
input[type='number']::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
`

return (
Expand Down
137 changes: 119 additions & 18 deletions app/src/organisms/Devices/HeaterShakerWizard/TestShake.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import React from 'react'
import { useTranslation } from 'react-i18next'
import { Trans, useTranslation } from 'react-i18next'
import { useCreateLiveCommandMutation } from '@opentrons/react-api-client'
import {
ALIGN_CENTER,
ALIGN_FLEX_START,
Expand All @@ -8,30 +9,82 @@ import {
DIRECTION_ROW,
Flex,
Icon,
InputField,
SIZE_AUTO,
SPACING,
Text,
TYPOGRAPHY,
Tooltip,
useHoverTooltip,
} from '@opentrons/components'
import { RPM } from '@opentrons/shared-data'
import { RPM, HS_RPM_MAX, HS_RPM_MIN } from '@opentrons/shared-data'
import {
useHeaterShakerFromProtocol,
useLatchCommand,
} from '../ModuleCard/hooks'
import { HeaterShakerModuleCard } from './HeaterShakerModuleCard'
import { TertiaryButton } from '../../../atoms/Buttons'
import { CollapsibleStep } from '../../ProtocolSetup/RunSetupCard/CollapsibleStep'
import { Divider } from '../../../atoms/structure'
import { InputField } from '../../../atoms/InputField'

import type { HeaterShakerModule } from '../../../redux/modules/types'
import type {
HeaterShakerSetTargetShakeSpeedCreateCommand,
HeaterShakerStopShakeCreateCommand,
} from '@opentrons/shared-data/protocol/types/schemaV6/command/module'

interface TestShakeProps {
module: HeaterShakerModule
setCurrentPage: React.Dispatch<React.SetStateAction<number>>
hasProtocol: boolean | undefined
}

export function TestShake(props: TestShakeProps): JSX.Element {
const { module, setCurrentPage } = props
const { t } = useTranslation('heater_shaker')

const { module, setCurrentPage, hasProtocol } = props
const { t } = useTranslation(['heater_shaker', 'device_details'])
const { createLiveCommand } = useCreateLiveCommandMutation()
const heaterShakerFromProtocol = useHeaterShakerFromProtocol()
const [isExpanded, setExpanded] = React.useState(false)
const [shakeValue, setShakeValue] = React.useState<string | null>(null)
const [targetProps, tooltipProps] = useHoverTooltip()
const { toggleLatch, isLatchClosed } = useLatchCommand(module)
const isShaking = module.data.speedStatus !== 'idle'

const setShakeCommand: HeaterShakerSetTargetShakeSpeedCreateCommand = {
commandType: 'heaterShakerModule/setTargetShakeSpeed',
params: {
moduleId: module.id,
rpm: shakeValue !== null ? parseInt(shakeValue) : 0,
},
}

const stopShakeCommand: HeaterShakerStopShakeCreateCommand = {
commandType: 'heaterShakerModule/stopShake',
params: {
moduleId: module.id,
},
}

const handleShakeCommand = (): void => {
if (shakeValue !== null) {
createLiveCommand({
command: isShaking ? stopShakeCommand : setShakeCommand,
}).catch((e: Error) => {
console.error(
`error setting module status with command type ${
stopShakeCommand.commandType ?? setShakeCommand.commandType
}: ${e.message}`
)
})
}
setShakeValue(null)
}

const errorMessage =
shakeValue != null &&
(parseInt(shakeValue) < HS_RPM_MIN || parseInt(shakeValue) > HS_RPM_MAX)
? t('input_out_of_range', { ns: 'device_details' })
: null

return (
<Flex flexDirection={DIRECTION_COLUMN}>
Expand Down Expand Up @@ -65,8 +118,26 @@ export function TestShake(props: TestShakeProps): JSX.Element {
paddingBottom={SPACING.spacing4}
>
<Text fontWeight={TYPOGRAPHY.fontWeightRegular}>
{/* TODO(sh, 2022-02-22): Dynamically render this text if a labware/protocol exists */}
{t('test_shake_banner_information')}
<Trans
t={t}
i18nKey={
hasProtocol && heaterShakerFromProtocol !== null
? 'test_shake_banner_labware_information'
: 'test_shake_banner_information'
}
values={{
labware: heaterShakerFromProtocol?.nestedLabwareDisplayName,
}}
components={{
bold: <strong />,
block: (
<Text
fontSize={TYPOGRAPHY.fontSizeH2}
marginBottom={SPACING.spacing5}
/>
),
}}
/>
</Text>
</Flex>
</Flex>
Expand All @@ -76,31 +147,61 @@ export function TestShake(props: TestShakeProps): JSX.Element {
fontSize={TYPOGRAPHY.fontSizeCaption}
>
<HeaterShakerModuleCard module={module} />
<TertiaryButton marginLeft={SIZE_AUTO} marginTop={SPACING.spacing4}>
{t('open_labware_latch')}
<TertiaryButton
marginLeft={SIZE_AUTO}
marginTop={SPACING.spacing4}
onClick={toggleLatch}
disabled={isShaking}
{...targetProps}
>
{isLatchClosed ? t('open_labware_latch') : t('close_labware_latch')}
</TertiaryButton>
{isShaking ? (
<Tooltip {...tooltipProps}>
{t('cannot_open_latch', { ns: 'heater_shaker' })}
</Tooltip>
) : null}
<Flex
flexDirection={DIRECTION_ROW}
marginY={SPACING.spacingL}
alignItems={ALIGN_FLEX_START}
>
<Flex flexDirection={DIRECTION_COLUMN} maxWidth={'6.25rem'}>
<Text fontSize={TYPOGRAPHY.fontSizeCaption}>
<Text
fontSize={TYPOGRAPHY.fontSizeCaption}
color={COLORS.darkGreyEnabled}
>
{t('set_shake_speed')}
</Text>
{/* TODO(sh, 2022-02-22): Wire up input when end points are updated */}
<InputField units={RPM} value={'1000'} readOnly />
<Text fontSize={TYPOGRAPHY.fontSizeCaption}>
{t('min_max_rpm', { min: '200', max: '1800' })}
</Text>
<InputField
data-testid={`TestShake_shake_input`}
units={RPM}
value={shakeValue}
onChange={e => setShakeValue(e.target.value)}
type="number"
caption={t('min_max_rpm', {
ns: 'heater_shaker',
min: HS_RPM_MIN,
max: HS_RPM_MAX,
})}
error={errorMessage}
/>
</Flex>
<TertiaryButton
fontSize={TYPOGRAPHY.fontSizeCaption}
marginLeft={SIZE_AUTO}
marginTop={SPACING.spacing3}
marginTop={SPACING.spacing4}
onClick={handleShakeCommand}
disabled={!isLatchClosed}
{...targetProps}
>
{t('start_shaking')}
{isShaking ? t('stop_shaking') : t('start_shaking')}
</TertiaryButton>
{!isLatchClosed ? (
<Tooltip {...tooltipProps}>
{t('cannot_shake', { ns: 'heater_shaker' })}
</Tooltip>
) : null}
</Flex>
</Flex>
<Divider marginY={SPACING.spacing4} />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as React from 'react'
import { MemoryRouter } from 'react-router-dom'
import { fireEvent } from '@testing-library/react'
import { renderWithProviders } from '@opentrons/components'
import { i18n } from '../../../../i18n'
import { getAttachedModules } from '../../../../redux/modules'
import { getConnectedRobotName } from '../../../../redux/robot/selectors'
import { useAttachedModules } from '../../hooks'
import { mockHeaterShaker } from '../../../../redux/modules/__fixtures__'
import { HeaterShakerWizard } from '..'
import { Introduction } from '../Introduction'
Expand All @@ -13,17 +13,16 @@ import { AttachAdapter } from '../AttachAdapter'
import { PowerOn } from '../PowerOn'
import { TestShake } from '../TestShake'

jest.mock('../../../../redux/robot/selectors')
jest.mock('../../hooks')
jest.mock('../Introduction')
jest.mock('../KeyParts')
jest.mock('../AttachModule')
jest.mock('../AttachAdapter')
jest.mock('../PowerOn')
jest.mock('../TestShake')
jest.mock('../../../../redux/modules')

const mockGetConnectedRobotName = getConnectedRobotName as jest.MockedFunction<
typeof getConnectedRobotName
const mockUseAttachedModules = useAttachedModules as jest.MockedFunction<
typeof useAttachedModules
>
const mockIntroduction = Introduction as jest.MockedFunction<
typeof Introduction
Expand All @@ -37,34 +36,38 @@ const mockAttachAdapter = AttachAdapter as jest.MockedFunction<
>
const mockPowerOn = PowerOn as jest.MockedFunction<typeof PowerOn>
const mockTestShake = TestShake as jest.MockedFunction<typeof TestShake>
const mockGetAttachedModules = getAttachedModules as jest.MockedFunction<
typeof getAttachedModules
>

const render = (props: React.ComponentProps<typeof HeaterShakerWizard>) => {
return renderWithProviders(<HeaterShakerWizard {...props} />, {
i18nInstance: i18n,
})[0]
const render = (
props: React.ComponentProps<typeof HeaterShakerWizard>,
path = '/'
) => {
return renderWithProviders(
<MemoryRouter initialEntries={[path]} initialIndex={0}>
<HeaterShakerWizard {...props} />
</MemoryRouter>,
{
i18nInstance: i18n,
}
)[0]
}

describe('HeaterShakerWizard', () => {
const props: React.ComponentProps<typeof HeaterShakerWizard> = {
onCloseClick: jest.fn(),
}
beforeEach(() => {
mockGetConnectedRobotName.mockReturnValue('Mock Robot')
mockUseAttachedModules.mockReturnValue([mockHeaterShaker])
mockIntroduction.mockReturnValue(<div>Mock Introduction</div>)
mockKeyParts.mockReturnValue(<div>Mock Key Parts</div>)
mockAttachModule.mockReturnValue(<div>Mock Attach Module</div>)
mockAttachAdapter.mockReturnValue(<div>Mock Attach Adapter</div>)
mockPowerOn.mockReturnValue(<div>Mock Power On</div>)
mockTestShake.mockReturnValue(<div>Mock Test Shake</div>)
mockGetAttachedModules.mockReturnValue([mockHeaterShaker])
})

it('renders the main modal component of the wizard', () => {
const { getByText } = render(props)
getByText('Mock Robot - Attach Heater Shaker Module')
getByText(/Attach Heater Shaker Module/i)
getByText('Mock Introduction')
})

Expand Down Expand Up @@ -95,7 +98,7 @@ describe('HeaterShakerWizard', () => {
})

it('renders power on component and the test shake button is not disabled', () => {
mockGetAttachedModules.mockReturnValue([])
mockUseAttachedModules.mockReturnValue([])

const { getByText, getByRole } = render(props)

Expand Down
Loading

0 comments on commit 1f34210

Please sign in to comment.