Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(app): wire up heater shaker wizard TestShake page #9833

Merged
merged 8 commits into from
Apr 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;
}
Comment on lines +116 to +120
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This additional css hides the up/down arrows when the InputField component strictly takes in numbers.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why do we want to hide arrows when the input field only takes numbers? wouldn't we want the opposite?

also do we need two lines with input[type='number']::-webkit-inner-spin-button?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like we need to put both for the inner and one for the outer to hide the arrows.

`

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