Skip to content

Commit

Permalink
fix(app): check module calibration status before enabling protocol run (
Browse files Browse the repository at this point in the history
#13760)

Block a protocol run if bad calibration data for any required module to mirror pipette checks before
a protocol run
  • Loading branch information
ncdiehl11 authored Oct 11, 2023
1 parent 1437d61 commit e8ba506
Show file tree
Hide file tree
Showing 12 changed files with 319 additions and 16 deletions.
1 change: 1 addition & 0 deletions app/src/assets/localization/en/protocol_setup.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"calibrate_deck_to_proceed_to_tip_length_calibration": "Calibrate your deck in order to proceed to tip length calibration",
"calibrate_gripper_failure_reason": "Calibrate the required gripper to continue",
"calibrate_now": "Calibrate now",
"calibrate_module_failure_reason": "Calibrate the required modules(s) to continue",
"calibrate_pipette_before_module_calibration": "Calibrate pipette before running module calibration",
"calibrate_pipette_failure_reason": "Calibrate the required pipette(s) to continue",
"calibrate_tiprack_failure_reason": "Calibrate the required tip lengths to continue",
Expand Down
10 changes: 9 additions & 1 deletion app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ import {
useTrackProtocolRunEvent,
useRobotAnalyticsData,
useIsOT3,
useModuleCalibrationStatus,
} from '../hooks'
import { formatTimestamp } from '../utils'
import { RunTimer } from './RunTimer'
Expand Down Expand Up @@ -476,10 +477,17 @@ function ActionButton(props: ActionButtonProps): JSX.Element {
robotName,
runId
)
const { complete: isModuleCalibrationComplete } = useModuleCalibrationStatus(
robotName,
runId
)
const [showIsShakingModal, setShowIsShakingModal] = React.useState<boolean>(
false
)
const isSetupComplete = isCalibrationComplete && missingModuleIds.length === 0
const isSetupComplete =
isCalibrationComplete &&
isModuleCalibrationComplete &&
missingModuleIds.length === 0
const isRobotOnWrongVersionOfSoftware = ['upgrade', 'downgrade'].includes(
useSelector((state: State) => {
return getRobotUpdateDisplayInfo(state, robotName)
Expand Down
47 changes: 36 additions & 11 deletions app/src/organisms/Devices/ProtocolRun/ProtocolRunSetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import {
useRunHasStarted,
useProtocolAnalysisErrors,
useStoredProtocolAnalysis,
useModuleCalibrationStatus,
ProtocolCalibrationStatus,
} from '../hooks'
import { useMostRecentCompletedAnalysis } from '../../LabwarePositionCheck/useMostRecentCompletedAnalysis'
Expand Down Expand Up @@ -67,8 +68,9 @@ export function ProtocolRunSetup({
const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis
const modules = parseAllRequiredModuleModels(protocolData?.commands ?? [])
const robot = useRobot(robotName)
const calibrationStatus = useRunCalibrationStatus(robotName, runId)
const isOT3 = useIsOT3(robotName)
const calibrationStatusRobot = useRunCalibrationStatus(robotName, runId)
const calibrationStatusModules = useModuleCalibrationStatus(robotName, runId)
const isFlex = useIsOT3(robotName)
const runHasStarted = useRunHasStarted(runId)
const { analysisErrors } = useProtocolAnalysisErrors(runId)
const [expandedStepKey, setExpandedStepKey] = React.useState<StepKey | null>(
Expand Down Expand Up @@ -129,11 +131,11 @@ export function ProtocolRunSetup({
]
}
expandStep={setExpandedStepKey}
calibrationStatus={calibrationStatus}
calibrationStatus={calibrationStatusRobot}
/>
),
// change description for OT-3
description: isOT3
description: isFlex
? t(`${ROBOT_CALIBRATION_STEP_KEY}_description_pipettes_only`)
: t(`${ROBOT_CALIBRATION_STEP_KEY}_description`),
},
Expand Down Expand Up @@ -229,7 +231,13 @@ export function ProtocolRunSetup({
}
rightElement={
<StepRightElement
{...{ stepKey, runHasStarted, calibrationStatus }}
{...{
stepKey,
runHasStarted,
calibrationStatusRobot,
calibrationStatusModules,
isFlex,
}}
/>
}
>
Expand All @@ -254,25 +262,42 @@ export function ProtocolRunSetup({

interface StepRightElementProps {
stepKey: StepKey
calibrationStatus: ProtocolCalibrationStatus
calibrationStatusRobot: ProtocolCalibrationStatus
calibrationStatusModules?: ProtocolCalibrationStatus
runHasStarted: boolean
isFlex: boolean
}
function StepRightElement(props: StepRightElementProps): JSX.Element | null {
const { stepKey, calibrationStatus, runHasStarted } = props
const {
stepKey,
runHasStarted,
calibrationStatusRobot,
calibrationStatusModules,
isFlex,
} = props
const { t } = useTranslation('protocol_setup')

if (stepKey === ROBOT_CALIBRATION_STEP_KEY && !runHasStarted) {
if (
!runHasStarted &&
(stepKey === ROBOT_CALIBRATION_STEP_KEY ||
(stepKey === MODULE_SETUP_KEY && isFlex))
) {
const calibrationStatus =
stepKey === ROBOT_CALIBRATION_STEP_KEY
? calibrationStatusRobot
: calibrationStatusModules

return (
<Flex flexDirection={DIRECTION_ROW} alignItems={ALIGN_CENTER}>
<Icon
size={SIZE_1}
color={
calibrationStatus.complete
calibrationStatus?.complete
? COLORS.successEnabled
: COLORS.warningEnabled
}
marginRight={SPACING.spacing8}
name={calibrationStatus.complete ? 'ot-check' : 'alert-circle'}
name={calibrationStatus?.complete ? 'ot-check' : 'alert-circle'}
id="RunSetupCard_calibrationIcon"
/>
<StyledText
Expand All @@ -282,7 +307,7 @@ function StepRightElement(props: StepRightElementProps): JSX.Element | null {
textTransform={TYPOGRAPHY.textTransformCapitalize}
id="RunSetupCard_calibrationText"
>
{calibrationStatus.complete
{calibrationStatus?.complete
? t('calibration_ready')
: t('calibration_needed')}
</StyledText>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { SetupModulesMap } from '../SetupModulesMap'
import {
useRunHasStarted,
useUnmatchedModulesForProtocol,
useModuleCalibrationStatus,
} from '../../../hooks'
import { mockTemperatureModule } from '../../../../../redux/modules/__fixtures__'

Expand All @@ -22,6 +23,9 @@ const mockUseRunHasStarted = useRunHasStarted as jest.MockedFunction<
const mockUseUnmatchedModulesForProtocol = useUnmatchedModulesForProtocol as jest.MockedFunction<
typeof useUnmatchedModulesForProtocol
>
const mockUseModuleCalibrationStatus = useModuleCalibrationStatus as jest.MockedFunction<
typeof useModuleCalibrationStatus
>
const mockSetupModulesList = SetupModulesList as jest.MockedFunction<
typeof SetupModulesList
>
Expand Down Expand Up @@ -55,6 +59,9 @@ describe('SetupModules', () => {
missingModuleIds: [],
remainingAttachedModules: [],
})
when(mockUseModuleCalibrationStatus)
.calledWith(MOCK_ROBOT_NAME, MOCK_RUN_ID)
.mockReturnValue({ complete: true })
})

it('renders the list and map view buttons', () => {
Expand Down Expand Up @@ -85,6 +92,17 @@ describe('SetupModules', () => {
expect(button).toBeDisabled()
})

it('should render a disabled Proceed to labware setup CTA if the protocol requests modules they are not all calibrated', () => {
when(mockUseModuleCalibrationStatus)
.calledWith(MOCK_ROBOT_NAME, MOCK_RUN_ID)
.mockReturnValue({ complete: false })
const { getByRole } = render(props)
const button = getByRole('button', {
name: 'Proceed to labware position check',
})
expect(button).toBeDisabled()
})

it('should render the SetupModulesList component when clicking List View', () => {
const { getByRole, getByText } = render(props)
const button = getByRole('button', { name: 'List View' })
Expand Down
23 changes: 19 additions & 4 deletions app/src/organisms/Devices/ProtocolRun/SetupModules/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,11 @@ import {
useHoverTooltip,
PrimaryButton,
} from '@opentrons/components'
import { useRunHasStarted, useUnmatchedModulesForProtocol } from '../../hooks'
import {
useRunHasStarted,
useUnmatchedModulesForProtocol,
useModuleCalibrationStatus,
} from '../../hooks'
import { useToggleGroup } from '../../../../molecules/ToggleGroup/useToggleGroup'
import { Tooltip } from '../../../../atoms/Tooltip'
import { SetupModulesMap } from './SetupModulesMap'
Expand All @@ -33,6 +37,9 @@ export const SetupModules = ({
const { missingModuleIds } = useUnmatchedModulesForProtocol(robotName, runId)
const runHasStarted = useRunHasStarted(runId)
const [targetProps, tooltipProps] = useHoverTooltip()

const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId)

return (
<>
<Flex flexDirection={DIRECTION_COLUMN} marginTop={SPACING.spacing32}>
Expand All @@ -45,7 +52,11 @@ export const SetupModules = ({
</Flex>
<Flex justifyContent={JUSTIFY_CENTER}>
<PrimaryButton
disabled={missingModuleIds.length > 0 || runHasStarted}
disabled={
missingModuleIds.length > 0 ||
runHasStarted ||
!moduleCalibrationStatus.complete
}
onClick={expandLabwarePositionCheckStep}
id="ModuleSetup_proceedToLabwarePositionCheck"
padding={`${SPACING.spacing8} ${SPACING.spacing16}`}
Expand All @@ -54,11 +65,15 @@ export const SetupModules = ({
{t('proceed_to_labware_position_check')}
</PrimaryButton>
</Flex>
{missingModuleIds.length > 0 || runHasStarted ? (
{missingModuleIds.length > 0 ||
runHasStarted ||
!moduleCalibrationStatus.complete ? (
<Tooltip tooltipProps={tooltipProps}>
{runHasStarted
? t('protocol_run_started')
: t('plug_in_required_module', { count: missingModuleIds.length })}
: missingModuleIds.length > 0
? t('plug_in_required_module', { count: missingModuleIds.length })
: t('calibrate_module_failure_reason')}
</Tooltip>
) : null}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ import {
useTrackProtocolRunEvent,
useRunCalibrationStatus,
useRunCreatedAtTimestamp,
useModuleCalibrationStatus,
useUnmatchedModulesForProtocol,
useIsRobotViewable,
useIsOT3,
Expand Down Expand Up @@ -148,6 +149,9 @@ const mockUseUnmatchedModulesForProtocol = useUnmatchedModulesForProtocol as jes
const mockUseRunCalibrationStatus = useRunCalibrationStatus as jest.MockedFunction<
typeof useRunCalibrationStatus
>
const mockUseModuleCalibrationStatus = useModuleCalibrationStatus as jest.MockedFunction<
typeof useModuleCalibrationStatus
>
const mockUseRunCreatedAtTimestamp = useRunCreatedAtTimestamp as jest.MockedFunction<
typeof useRunCreatedAtTimestamp
>
Expand Down Expand Up @@ -363,6 +367,9 @@ describe('ProtocolRunHeader', () => {
when(mockUseRunCalibrationStatus)
.calledWith(ROBOT_NAME, RUN_ID)
.mockReturnValue({ complete: true })
when(mockUseModuleCalibrationStatus)
.calledWith(ROBOT_NAME, RUN_ID)
.mockReturnValue({ complete: true })
when(mockUseIsOT3).calledWith(ROBOT_NAME).mockReturnValue(true)
mockRunFailedModal.mockReturnValue(<div>mock RunFailedModal</div>)
mockUseEstopQuery.mockReturnValue({ data: mockEstopStatus } as any)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
useIsOT3,
useRobot,
useRunCalibrationStatus,
useModuleCalibrationStatus,
useRunHasStarted,
useProtocolAnalysisErrors,
useStoredProtocolAnalysis,
Expand Down Expand Up @@ -52,6 +53,9 @@ const mockUseRobot = useRobot as jest.MockedFunction<typeof useRobot>
const mockUseRunCalibrationStatus = useRunCalibrationStatus as jest.MockedFunction<
typeof useRunCalibrationStatus
>
const mockUseModuleCalibrationStatus = useModuleCalibrationStatus as jest.MockedFunction<
typeof useModuleCalibrationStatus
>
const mockUseRunHasStarted = useRunHasStarted as jest.MockedFunction<
typeof useRunHasStarted
>
Expand Down Expand Up @@ -266,11 +270,43 @@ describe('ProtocolRunSetup', () => {
...MOCK_ROTOCOL_LIQUID_KEY,
} as any)
when(mockUseRunHasStarted).calledWith(RUN_ID).mockReturnValue(false)
when(mockUseModuleCalibrationStatus)
.calledWith(ROBOT_NAME, RUN_ID)
.mockReturnValue({ complete: true })
})
afterEach(() => {
resetAllWhenMocks()
})

it('renders calibration ready if robot is Flex and modules are calibrated', () => {
when(mockUseIsOT3).calledWith(ROBOT_NAME).mockReturnValue(true)
when(mockUseModuleCalibrationStatus)
.calledWith(ROBOT_NAME, RUN_ID)
.mockReturnValue({ complete: true })

const { getAllByText } = render()
expect(getAllByText('Calibration ready').length).toEqual(2)
})

it('renders calibration needed if robot is Flex and modules are not calibrated', () => {
when(mockUseIsOT3).calledWith(ROBOT_NAME).mockReturnValue(true)
when(mockUseModuleCalibrationStatus)
.calledWith(ROBOT_NAME, RUN_ID)
.mockReturnValue({ complete: false })

const { getByText } = render()
getByText('STEP 2')
getByText('Modules')
getByText('Calibration needed')
})

it('does not render calibration element if robot is OT-2', () => {
when(mockUseIsOT3).calledWith(ROBOT_NAME).mockReturnValue(false)

const { getAllByText } = render()
expect(getAllByText('Calibration ready').length).toEqual(1)
})

it('renders module setup and allows the user to proceed to labware setup', () => {
const { getByText } = render()
const moduleSetup = getByText('Modules')
Expand Down
Loading

0 comments on commit e8ba506

Please sign in to comment.