Skip to content

Commit

Permalink
feat(app): Add "Tip Not Detected" error to Error Recovery (#16346)
Browse files Browse the repository at this point in the history
Closes EXEC-721

This officially canonizes the tip pickup failed error, giving the error a dedicated error kind, a new error details InlineNotification, some unique copy, proper recovery options, and fully separates it from being the "general error" that it was in 8.0.0.
  • Loading branch information
mjhuff authored Sep 25, 2024
1 parent f1c980f commit bd3f65d
Show file tree
Hide file tree
Showing 18 changed files with 192 additions and 62 deletions.
7 changes: 5 additions & 2 deletions app/src/assets/localization/en/error_recovery.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
"error_on_robot": "Error on {{robot}}",
"failed_dispense_step_not_completed": "<block>The failed dispense step will not be completed. The run will continue from the next step with the attached tips.</block><block>Close the robot door before proceeding.</block>",
"failed_step": "Failed step",
"first_take_any_necessary_actions": "<block>First, take any necessary actions to prepare the robot to retry the failed step.</block><block>Then, close the robot door before proceeding.</block>",
"go_back": "Go back",
"homing_pipette_dangerous": "Homing the <bold>{{mount}} pipette</bold> with liquid in the tips may damage it. You must remove all tips before using the pipette again.",
"if_issue_persists": " If the issue persists, cancel the run and make the necessary changes to the protocol",
"if_issue_persists_overpressure": " If the issue persists, cancel the run and make the necessary changes to the protocol",
"if_issue_persists_tip_not_detected": " If the issue persists, cancel the run and initiate Labware Position Check",
"if_tips_are_attached": "If tips are attached, you can choose to blow out any aspirated liquid and drop tips before the run is terminated.",
"ignore_all_errors_of_this_type": "Ignore all errors of this type",
"ignore_error_and_skip": "Ignore error and skip to next step",
Expand Down Expand Up @@ -76,9 +76,12 @@
"stand_back_resuming": "Stand back, resuming current step",
"stand_back_retrying": "Stand back, retrying failed step",
"stand_back_skipping_to_next_step": "Stand back, skipping to next step",
"take_necessary_actions": "<block>First, take any necessary actions to prepare the robot to retry the failed step.</block><block>Then, close the robot door before proceeding.</block>",
"take_necessary_actions_failed_pickup": "<block>First, take any necessary actions to prepare the robot to retry the failed tip pickup.</block><block>Then, close the robot door before proceeding.</block>",
"terminate_remote_activity": "Terminate remote activity",
"tip_drop_failed": "Tip drop failed",
"tip_not_detected": "Tip not detected",
"tip_presence_errors_are_caused": "Tip presence errors are usually caused by improperly placed labware or inaccurate labware offsets",
"view_error_details": "View error details",
"view_recovery_options": "View recovery options",
"you_can_still_drop_tips": "You can still drop the attached tips before proceeding to tip selection."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,7 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element {
return buildSelectRecoveryOption()
case RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE:
return buildRecoveryError()
case RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE:
case RECOVERY_MAP.RETRY_STEP.ROUTE:
return buildResumeRun()
case RECOVERY_MAP.CANCEL_RUN.ROUTE:
return buildCancelRun()
Expand Down
18 changes: 13 additions & 5 deletions app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { Trans, useTranslation } from 'react-i18next'

import { LegacyStyledText } from '@opentrons/components'

import { RECOVERY_MAP } from '../constants'
import { ERROR_KINDS, RECOVERY_MAP } from '../constants'
import { TwoColTextAndFailedStepNextStep } from '../shared'
import { SelectRecoveryOption } from './SelectRecoveryOption'

Expand All @@ -12,11 +12,11 @@ import type { RecoveryContentProps } from '../types'
export function RetryStep(props: RecoveryContentProps): JSX.Element {
const { recoveryMap } = props
const { step, route } = recoveryMap
const { RETRY_FAILED_COMMAND } = RECOVERY_MAP
const { RETRY_STEP } = RECOVERY_MAP

const buildContent = (): JSX.Element => {
switch (step) {
case RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY:
case RETRY_STEP.STEPS.CONFIRM_RETRY:
return <RetryStepInfo {...props} />
default:
console.warn(`${step} in ${route} not explicitly handled. Rerouting.`)
Expand All @@ -28,7 +28,7 @@ export function RetryStep(props: RecoveryContentProps): JSX.Element {
}

export function RetryStepInfo(props: RecoveryContentProps): JSX.Element {
const { routeUpdateActions, recoveryCommands } = props
const { routeUpdateActions, recoveryCommands, errorKind } = props
const { ROBOT_RETRYING_STEP } = RECOVERY_MAP
const { t } = useTranslation('error_recovery')

Expand All @@ -43,11 +43,19 @@ export function RetryStepInfo(props: RecoveryContentProps): JSX.Element {
})
}

const buildBodyCopyKey = (): string => {
switch (errorKind) {
case ERROR_KINDS.TIP_NOT_DETECTED:
return 'take_necessary_actions_failed_pickup'
default:
return 'take_necessary_actions'
}
}
const buildBodyText = (): JSX.Element => {
return (
<Trans
t={t}
i18nKey="first_take_any_necessary_actions"
i18nKey={buildBodyCopyKey()}
components={{
block: <LegacyStyledText as="p" />,
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,8 @@ export function getRecoveryOptions(errorKind: ErrorKind): RecoveryRoute[] {
return OVERPRESSURE_WHILE_ASPIRATING_OPTIONS
case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING:
return OVERPRESSURE_WHILE_DISPENSING_OPTIONS
case ERROR_KINDS.TIP_NOT_DETECTED:
return TIP_NOT_DETECTED_OPTIONS
case ERROR_KINDS.GENERAL_ERROR:
return GENERAL_ERROR_OPTIONS
}
Expand Down Expand Up @@ -221,8 +223,14 @@ export const OVERPRESSURE_WHILE_DISPENSING_OPTIONS: RecoveryRoute[] = [
RECOVERY_MAP.CANCEL_RUN.ROUTE,
]

export const TIP_NOT_DETECTED_OPTIONS: RecoveryRoute[] = [
RECOVERY_MAP.RETRY_STEP.ROUTE,
RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE,
RECOVERY_MAP.CANCEL_RUN.ROUTE,
]

export const GENERAL_ERROR_OPTIONS: RecoveryRoute[] = [
RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE,
RECOVERY_MAP.RETRY_STEP.ROUTE,
RECOVERY_MAP.CANCEL_RUN.ROUTE,
]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { mockRecoveryContentProps } from '../../__fixtures__'
import { renderWithProviders } from '/app/__testing-utils__'
import { i18n } from '/app/i18n'
import { RetryStep, RetryStepInfo } from '../RetryStep'
import { RECOVERY_MAP } from '../../constants'
import { ERROR_KINDS, RECOVERY_MAP } from '../../constants'
import { SelectRecoveryOption } from '../SelectRecoveryOption'

import { clickButtonLabeled } from '../../__tests__/util'
Expand Down Expand Up @@ -47,12 +47,12 @@ describe('RetryStep', () => {
vi.resetAllMocks()
})

it(`renders RetryStepInfo when step is ${RECOVERY_MAP.RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY}`, () => {
it(`renders RetryStepInfo when step is ${RECOVERY_MAP.RETRY_STEP.STEPS.CONFIRM_RETRY}`, () => {
props = {
...props,
recoveryMap: {
...props.recoveryMap,
step: RECOVERY_MAP.RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY,
step: RECOVERY_MAP.RETRY_STEP.STEPS.CONFIRM_RETRY,
},
}
render(props)
Expand Down Expand Up @@ -99,7 +99,16 @@ describe('RetryStepInfo', () => {
vi.resetAllMocks()
})

it('renders the component with the correct text', () => {
it(`renders the component with the correct text for ${ERROR_KINDS.TIP_NOT_DETECTED} `, () => {
renderRetryStepInfo({ ...props, errorKind: ERROR_KINDS.TIP_NOT_DETECTED })
screen.getByText('Retry step')
screen.queryByText(
'First, take any necessary actions to prepare the robot to retry the failed tip pickup.'
)
screen.queryByText('Then, close the robot door before proceeding.')
})

it('renders the component with the correct text for not specifically handled error kinds', () => {
renderRetryStepInfo(props)
screen.getByText('Retry step')
screen.queryByText(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
OVERPRESSURE_PREPARE_TO_ASPIRATE,
OVERPRESSURE_WHILE_DISPENSING_OPTIONS,
NO_LIQUID_DETECTED_OPTIONS,
TIP_NOT_DETECTED_OPTIONS,
} from '../SelectRecoveryOption'
import { RECOVERY_MAP, ERROR_KINDS } from '../../constants'
import { clickButtonLabeled } from '../../__tests__/util'
Expand Down Expand Up @@ -49,7 +50,7 @@ const renderDesktopRecoveryOptions = (
}

describe('SelectRecoveryOption', () => {
const { RETRY_FAILED_COMMAND, RETRY_NEW_TIPS } = RECOVERY_MAP
const { RETRY_STEP, RETRY_NEW_TIPS } = RECOVERY_MAP
let props: React.ComponentProps<typeof SelectRecoveryOption>
let mockProceedToRouteAndStep: Mock
let mockSetSelectedRecoveryOption: Mock
Expand All @@ -67,8 +68,8 @@ describe('SelectRecoveryOption', () => {
...mockRecoveryContentProps,
routeUpdateActions: mockRouteUpdateActions,
recoveryMap: {
route: RETRY_FAILED_COMMAND.ROUTE,
step: RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY,
route: RETRY_STEP.ROUTE,
step: RETRY_STEP.STEPS.CONFIRM_RETRY,
},
tipStatusUtils: { determineTipStatus: vi.fn() } as any,
currentRecoveryOptionUtils: {
Expand All @@ -78,7 +79,7 @@ describe('SelectRecoveryOption', () => {
}

when(mockGetRecoveryOptionCopy)
.calledWith(RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE)
.calledWith(RECOVERY_MAP.RETRY_STEP.ROUTE)
.thenReturn('Retry step')
when(mockGetRecoveryOptionCopy)
.calledWith(RECOVERY_MAP.CANCEL_RUN.ROUTE)
Expand All @@ -102,9 +103,7 @@ describe('SelectRecoveryOption', () => {

clickButtonLabeled('Continue')

expect(mockSetSelectedRecoveryOption).toHaveBeenCalledWith(
RETRY_FAILED_COMMAND.ROUTE
)
expect(mockSetSelectedRecoveryOption).toHaveBeenCalledWith(RETRY_STEP.ROUTE)
})

it('renders appropriate "General Error" copy and click behavior', () => {
Expand All @@ -121,9 +120,7 @@ describe('SelectRecoveryOption', () => {
fireEvent.click(retryStepOption[0])
clickButtonLabeled('Continue')

expect(mockProceedToRouteAndStep).toHaveBeenCalledWith(
RETRY_FAILED_COMMAND.ROUTE
)
expect(mockProceedToRouteAndStep).toHaveBeenCalledWith(RETRY_STEP.ROUTE)
})

it('renders appropriate "Overpressure while aspirating" copy and click behavior', () => {
Expand Down Expand Up @@ -238,7 +235,7 @@ describe('SelectRecoveryOption', () => {
}

when(mockGetRecoveryOptionCopy)
.calledWith(RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE)
.calledWith(RECOVERY_MAP.RETRY_STEP.ROUTE)
.thenReturn('Retry step')
when(mockGetRecoveryOptionCopy)
.calledWith(RECOVERY_MAP.CANCEL_RUN.ROUTE)
Expand Down Expand Up @@ -372,4 +369,11 @@ describe('getRecoveryOptions', () => {
OVERPRESSURE_WHILE_DISPENSING_OPTIONS
)
})

it(`returns valid options when the errorKind is ${ERROR_KINDS.TIP_NOT_DETECTED}`, () => {
const overpressureWhileDispensingOptions = getRecoveryOptions(
ERROR_KINDS.TIP_NOT_DETECTED
)
expect(overpressureWhileDispensingOptions).toBe(TIP_NOT_DETECTED_OPTIONS)
})
})
4 changes: 2 additions & 2 deletions app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,13 @@ export const mockFailedCommand: FailedCommand = {
error: {
createdAt: '2024-05-24T13:55:32.595751+00:00',
detail: 'No tip detected.',
isDefined: false,
isDefined: true,
errorCode: '3003',
errorType: 'tipPhysicallyMissing',
errorInfo: {},
wrappedErrors: [],
id: '123',
},
} as any,
startedAt: '2024-05-24T13:55:19.016799+00:00',
id: '1',
params: {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,7 @@ const renderRecoveryContent = (
describe('ErrorRecoveryContent', () => {
const {
OPTION_SELECTION,
RETRY_FAILED_COMMAND,
RETRY_STEP,
ROBOT_CANCELING,
ROBOT_RESUMING,
ROBOT_IN_MOTION,
Expand Down Expand Up @@ -215,12 +215,12 @@ describe('ErrorRecoveryContent', () => {
screen.getByText('MOCK_SELECT_RECOVERY_OPTION')
})

it(`returns ResumeRun when the route is ${RETRY_FAILED_COMMAND.ROUTE}`, () => {
it(`returns ResumeRun when the route is ${RETRY_STEP.ROUTE}`, () => {
props = {
...props,
recoveryMap: {
...props.recoveryMap,
route: RETRY_FAILED_COMMAND.ROUTE,
route: RETRY_STEP.ROUTE,
},
}
renderRecoveryContent(props)
Expand Down
14 changes: 8 additions & 6 deletions app/src/organisms/ErrorRecoveryFlows/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import type { RecoveryRouteStepMetadata, StepOrder } from './types'
export const DEFINED_ERROR_TYPES = {
OVERPRESSURE: 'overpressure',
LIQUID_NOT_FOUND: 'liquidNotFound',
TIP_PHYSICALLY_MISSING: 'tipPhysicallyMissing',
}

// Client-defined error-handling flows.
Expand All @@ -26,6 +27,7 @@ export const ERROR_KINDS = {
OVERPRESSURE_PREPARE_TO_ASPIRATE: 'OVERPRESSURE_PREPARE_TO_ASPIRATE',
OVERPRESSURE_WHILE_ASPIRATING: 'OVERPRESSURE_WHILE_ASPIRATING',
OVERPRESSURE_WHILE_DISPENSING: 'OVERPRESSURE_WHILE_DISPENSING',
TIP_NOT_DETECTED: 'TIP_NOT_DETECTED',
} as const

// TODO(jh, 05-09-24): Refactor to a directed graph. EXEC-430.
Expand Down Expand Up @@ -110,8 +112,8 @@ export const RECOVERY_MAP = {
STEPS: { MANUALLY_FILL: 'manually-fill', SKIP: 'skip' },
},
REFILL_AND_RESUME: { ROUTE: 'refill-and-resume', STEPS: {} },
RETRY_FAILED_COMMAND: {
ROUTE: 'retry-failed-command',
RETRY_STEP: {
ROUTE: 'retry-step',
STEPS: { CONFIRM_RETRY: 'confirm-retry' },
},
RETRY_NEW_TIPS: {
Expand Down Expand Up @@ -148,7 +150,7 @@ export const RECOVERY_MAP = {

const {
OPTION_SELECTION,
RETRY_FAILED_COMMAND,
RETRY_STEP,
ROBOT_CANCELING,
ROBOT_PICKING_UP_TIPS,
ROBOT_RESUMING,
Expand All @@ -171,7 +173,7 @@ const {
// The deterministic ordering of steps for a given route.
export const STEP_ORDER: StepOrder = {
[OPTION_SELECTION.ROUTE]: [OPTION_SELECTION.STEPS.SELECT],
[RETRY_FAILED_COMMAND.ROUTE]: [RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY],
[RETRY_STEP.ROUTE]: [RETRY_STEP.STEPS.CONFIRM_RETRY],
[RETRY_NEW_TIPS.ROUTE]: [
RETRY_NEW_TIPS.STEPS.DROP_TIPS,
RETRY_NEW_TIPS.STEPS.REPLACE_TIPS,
Expand Down Expand Up @@ -283,8 +285,8 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = {
[FILL_MANUALLY_AND_SKIP.STEPS.SKIP]: { allowDoorOpen: true },
},
[REFILL_AND_RESUME.ROUTE]: {},
[RETRY_FAILED_COMMAND.ROUTE]: {
[RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY]: {
[RETRY_STEP.ROUTE]: {
[RETRY_STEP.STEPS.CONFIRM_RETRY]: {
allowDoorOpen: false,
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,8 @@ const render = (props: React.ComponentProps<typeof MockRenderCmpt>) => {
}

describe('useRecoveryOptionCopy', () => {
it(`renders the correct copy for ${RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE}`, () => {
render({ route: RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE })
it(`renders the correct copy for ${RECOVERY_MAP.RETRY_STEP.ROUTE}`, () => {
render({ route: RECOVERY_MAP.RETRY_STEP.ROUTE })

screen.getByText('Retry step')
})
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,8 @@ describe('useRouteUpdateActions', () => {
hasLaunchedRecovery: true,
toggleERWizAsActiveUser: mockToggleERWizard,
recoveryMap: {
route: RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE,
step: RECOVERY_MAP.RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY,
route: RECOVERY_MAP.RETRY_STEP.ROUTE,
step: RECOVERY_MAP.RETRY_STEP.STEPS.CONFIRM_RETRY,
},
setRecoveryMap: mockSetRecoveryMap,
doorStatusUtils: { isProhibitedDoorOpen: false, isDoorOpen: false },
Expand Down Expand Up @@ -162,8 +162,8 @@ describe('useRouteUpdateActions', () => {
void handleMotionRouting(false)
rerender()
expect(mockSetRecoveryMap).toHaveBeenCalledWith({
route: RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE,
step: RECOVERY_MAP.RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY,
route: RECOVERY_MAP.RETRY_STEP.ROUTE,
step: RECOVERY_MAP.RETRY_STEP.STEPS.CONFIRM_RETRY,
})
})

Expand All @@ -187,12 +187,12 @@ describe('useRouteUpdateActions', () => {
const { proceedToRouteAndStep } = result.current

void proceedToRouteAndStep(
RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE,
RECOVERY_MAP.RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY
RECOVERY_MAP.RETRY_STEP.ROUTE,
RECOVERY_MAP.RETRY_STEP.STEPS.CONFIRM_RETRY
)
expect(mockSetRecoveryMap).toHaveBeenCalledWith({
route: RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE,
step: RECOVERY_MAP.RETRY_FAILED_COMMAND.STEPS.CONFIRM_RETRY,
route: RECOVERY_MAP.RETRY_STEP.ROUTE,
step: RECOVERY_MAP.RETRY_STEP.STEPS.CONFIRM_RETRY,
})
})

Expand Down
5 changes: 3 additions & 2 deletions app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,9 @@ export function useErrorName(errorKind: ErrorKind): string {
return t('pipette_overpressure')
case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING:
return t('pipette_overpressure')
// The only "general error" case currently is tipPhysicallyMissing.
default:
case ERROR_KINDS.TIP_NOT_DETECTED:
return t('tip_not_detected')
default:
return t('error')
}
}
Loading

0 comments on commit bd3f65d

Please sign in to comment.