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 }