diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json
index dd483a3daac..f9e9253a8a6 100644
--- a/app/src/assets/localization/en/error_recovery.json
+++ b/app/src/assets/localization/en/error_recovery.json
@@ -22,10 +22,10 @@
"error_on_robot": "Error on {{robot}}",
"failed_dispense_step_not_completed": "The failed dispense step will not be completed. The run will continue from the next step with the attached tips.Close the robot door before proceeding.",
"failed_step": "Failed step",
- "first_take_any_necessary_actions": "First, take any necessary actions to prepare the robot to retry the failed step.Then, close the robot door before proceeding.",
"go_back": "Go back",
"homing_pipette_dangerous": "Homing the {{mount}} pipette 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",
@@ -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": "First, take any necessary actions to prepare the robot to retry the failed step.Then, close the robot door before proceeding.",
+ "take_necessary_actions_failed_pickup": "First, take any necessary actions to prepare the robot to retry the failed tip pickup.Then, close the robot door before proceeding.",
"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."
diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx
index 9d63e5c1acf..005665445ed 100644
--- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx
@@ -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()
diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx
index 0a18b1a56ae..5831fd994f7 100644
--- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/RetryStep.tsx
@@ -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'
@@ -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
default:
console.warn(`${step} in ${route} not explicitly handled. Rerouting.`)
@@ -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')
@@ -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 (
,
}}
diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx
index c56a7ede638..7ababdcb4f8 100644
--- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/SelectRecoveryOption.tsx
@@ -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
}
@@ -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,
]
diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx
index 760c8dcfebf..c70f6a10af4 100644
--- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/RetryStep.test.tsx
@@ -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'
@@ -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)
@@ -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(
diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx
index 7f4e8932070..0984efbe8cc 100644
--- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx
@@ -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'
@@ -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
let mockProceedToRouteAndStep: Mock
let mockSetSelectedRecoveryOption: Mock
@@ -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: {
@@ -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)
@@ -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', () => {
@@ -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', () => {
@@ -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)
@@ -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)
+ })
})
diff --git a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts
index c1190724d60..cd8f2c02515 100644
--- a/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts
@@ -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: {
diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx
index 85c731dbf0c..7e800e817f1 100644
--- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx
@@ -162,7 +162,7 @@ const renderRecoveryContent = (
describe('ErrorRecoveryContent', () => {
const {
OPTION_SELECTION,
- RETRY_FAILED_COMMAND,
+ RETRY_STEP,
ROBOT_CANCELING,
ROBOT_RESUMING,
ROBOT_IN_MOTION,
@@ -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)
diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts
index 4c57d38d35e..f7cea32eb03 100644
--- a/app/src/organisms/ErrorRecoveryFlows/constants.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts
@@ -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.
@@ -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.
@@ -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: {
@@ -148,7 +150,7 @@ export const RECOVERY_MAP = {
const {
OPTION_SELECTION,
- RETRY_FAILED_COMMAND,
+ RETRY_STEP,
ROBOT_CANCELING,
ROBOT_PICKING_UP_TIPS,
ROBOT_RESUMING,
@@ -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,
@@ -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,
},
},
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx
index 0a0b29e405d..d69417d666c 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx
@@ -27,8 +27,8 @@ const render = (props: React.ComponentProps) => {
}
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')
})
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRouteUpdateActions.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRouteUpdateActions.test.ts
index a5ace9d2218..6e22b929b4c 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRouteUpdateActions.test.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRouteUpdateActions.test.ts
@@ -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 },
@@ -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,
})
})
@@ -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,
})
})
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts
index bde35d89fbf..9827ce202a8 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts
@@ -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')
}
}
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx
index 4e636f6fd78..47b7782c094 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx
@@ -14,7 +14,7 @@ export function useRecoveryOptionCopy(): (
recoveryOption: RecoveryRoute | null
): string => {
switch (recoveryOption) {
- case RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE:
+ case RECOVERY_MAP.RETRY_STEP.ROUTE:
return t('retry_step')
case RECOVERY_MAP.CANCEL_RUN.ROUTE:
return t('cancel_run')
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts
index ea63447c78f..0ab5d0806c0 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts
@@ -154,7 +154,7 @@ function handleRecoveryOptionAction(
case RECOVERY_MAP.CANCEL_RUN.ROUTE:
case RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE:
case RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE:
- case RECOVERY_MAP.RETRY_FAILED_COMMAND.ROUTE:
+ case RECOVERY_MAP.RETRY_STEP.ROUTE:
return currentStepReturnVal
default:
return 'HANDLE RECOVERY TOAST OPTION EXPLICITLY.'
diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx
index ba898d94d7e..e81c4c2106b 100644
--- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx
@@ -27,7 +27,7 @@ import type { IconProps } from '@opentrons/components'
import type { OddModalHeaderBaseProps } from '/app/molecules/OddModal/types'
import type { ERUtilsResults, useRetainedFailedCommandBySource } from '../hooks'
import type { ErrorRecoveryFlowsProps } from '..'
-import type { DesktopSizeType } from '../types'
+import type { DesktopSizeType, ErrorKind } from '../types'
export function useErrorDetailsModal(): {
showModal: boolean
@@ -59,11 +59,12 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element {
const errorKind = getErrorKind(failedCommand?.byRunRecord ?? null)
const errorName = useErrorName(errorKind)
- const getIsOverpressureErrorKind = (): boolean => {
+ const isNotificationErrorKind = (): boolean => {
switch (errorKind) {
case ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE:
case ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING:
case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING:
+ case ERROR_KINDS.TIP_NOT_DETECTED:
return true
default:
return false
@@ -83,7 +84,9 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element {
toggleModal={toggleModal}
modalHeader={modalHeader}
>
- {getIsOverpressureErrorKind() ? : null}
+ {isNotificationErrorKind() ? (
+
+ ) : null}
,
getTopPortalEl()
)
@@ -94,7 +97,9 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element {
toggleModal={toggleModal}
modalHeader={modalHeader}
>
- {getIsOverpressureErrorKind() ? : null}
+ {isNotificationErrorKind() ? (
+
+ ) : null}
,
getModalPortalEl()
)
@@ -191,14 +196,48 @@ export function ErrorDetailsModalODD(
)
}
-export function OverpressureBanner(): JSX.Element | null {
+export function NotificationBanner({
+ errorKind,
+}: {
+ errorKind: ErrorKind
+}): JSX.Element {
+ const buildContent = (): JSX.Element => {
+ switch (errorKind) {
+ case ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE:
+ case ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING:
+ case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING:
+ return
+ case ERROR_KINDS.TIP_NOT_DETECTED:
+ return
+ default:
+ console.error('Handle error kind notification banners explicitly.')
+ return
+ }
+ }
+
+ return buildContent()
+}
+
+export function OverpressureBanner(): JSX.Element {
const { t } = useTranslation('error_recovery')
return (
+ )
+}
+
+export function TipNotDetectedBanner(): JSX.Element {
+ const { t } = useTranslation('error_recovery')
+
+ return (
+
)
}
diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx
index 6a88cdc8ccc..c2f80c429b1 100644
--- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx
@@ -12,6 +12,7 @@ import {
useErrorDetailsModal,
ErrorDetailsModal,
OverpressureBanner,
+ TipNotDetectedBanner,
} from '../ErrorDetailsModal'
vi.mock('react-dom', () => ({
@@ -92,7 +93,7 @@ describe('ErrorDetailsModal', () => {
})
IS_ODD.forEach(isOnDevice => {
- it('renders the OverpressureBanner when the error kind is an overpressure error', () => {
+ it('renders an inline banner when the error kind is an overpressure error', () => {
props.failedCommand = {
...props.failedCommand,
byRunRecord: {
@@ -106,9 +107,23 @@ describe('ErrorDetailsModal', () => {
screen.getByText('MOCK_INLINE_NOTIFICATION')
})
- it('does not render the OverpressureBanner when the error kind is not an overpressure error', () => {
+ it('renders an inline banner when the error kind is a tip not detected error', () => {
+ props.failedCommand = {
+ ...props.failedCommand,
+ byRunRecord: {
+ ...props.failedCommand?.byRunRecord,
+ commandType: 'pickUpTip',
+ error: { isDefined: true, errorType: 'tipPhysicallyMissing' },
+ },
+ } as any
render({ ...props, isOnDevice })
+ screen.getByText('MOCK_INLINE_NOTIFICATION')
+ })
+
+ it('does not render a banner when the error kind is not explicitly handled', () => {
+ render({ ...props, isOnDevice, failedCommand: {} as any })
+
expect(screen.queryByText('MOCK_INLINE_NOTIFICATION')).toBeNull()
})
})
@@ -137,3 +152,27 @@ describe('OverpressureBanner', () => {
)
})
})
+
+describe('TipNotDetectedBanner', () => {
+ beforeEach(() => {
+ vi.mocked(InlineNotification).mockReturnValue(
+ MOCK_INLINE_NOTIFICATION
+ )
+ })
+
+ it('renders the InlineNotification', () => {
+ renderWithProviders(, {
+ i18nInstance: i18n,
+ })
+ expect(vi.mocked(InlineNotification)).toHaveBeenCalledWith(
+ expect.objectContaining({
+ type: 'alert',
+ heading:
+ 'Tip presence errors are usually caused by improperly placed labware or inaccurate labware offsets',
+ message:
+ ' If the issue persists, cancel the run and initiate Labware Position Check',
+ }),
+ {}
+ )
+ })
+})
diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts
index adad317fd2a..162c29d2566 100644
--- a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts
@@ -39,6 +39,17 @@ describe('getErrorKind', () => {
expect(result).toEqual(ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING)
})
+ it(`returns ${ERROR_KINDS.TIP_NOT_DETECTED} for ${DEFINED_ERROR_TYPES.TIP_PHYSICALLY_MISSING} errorType`, () => {
+ const result = getErrorKind({
+ commandType: 'pickUpTip',
+ error: {
+ isDefined: true,
+ errorType: DEFINED_ERROR_TYPES.TIP_PHYSICALLY_MISSING,
+ } as RunCommandError,
+ } as RunTimeCommand)
+ expect(result).toEqual(ERROR_KINDS.TIP_NOT_DETECTED)
+ })
+
it(`returns ${ERROR_KINDS.GENERAL_ERROR} for undefined errors`, () => {
const result = getErrorKind({
commandType: 'aspirate',
diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts
index bc16e11619c..89475f34c93 100644
--- a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts
@@ -16,18 +16,24 @@ export function getErrorKind(failedCommand: RunTimeCommand | null): ErrorKind {
if (
commandType === 'aspirate' &&
errorType === DEFINED_ERROR_TYPES.OVERPRESSURE
- )
+ ) {
return ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING
- else if (
+ } else if (
commandType === 'dispense' &&
errorType === DEFINED_ERROR_TYPES.OVERPRESSURE
- )
+ ) {
return ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING
- else if (
+ } else if (
commandType === 'liquidProbe' &&
errorType === DEFINED_ERROR_TYPES.LIQUID_NOT_FOUND
- )
+ ) {
return ERROR_KINDS.NO_LIQUID_DETECTED
+ } else if (
+ commandType === 'pickUpTip' &&
+ errorType === DEFINED_ERROR_TYPES.TIP_PHYSICALLY_MISSING
+ ) {
+ return ERROR_KINDS.TIP_NOT_DETECTED
+ }
// todo(mm, 2024-07-02): Also handle aspirateInPlace and dispenseInPlace.
// https://opentrons.atlassian.net/browse/EXEC-593
}