diff --git a/app/src/assets/localization/en/error_recovery.json b/app/src/assets/localization/en/error_recovery.json index 261dbfd278d..6841012ebeb 100644 --- a/app/src/assets/localization/en/error_recovery.json +++ b/app/src/assets/localization/en/error_recovery.json @@ -34,12 +34,13 @@ "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_gripper_error": " If the issue persists, cancel the run and rerun gripper calibration", "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_issue_persists_tip_not_detected": " If the issue persists, cancel the run and perform 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", "ignore_only_this_error": "Ignore only this error", "ignore_similar_errors_later_in_run": "Ignore similar errors later in the run?", + "inspect_the_robot": "First, inspect the robot to ensure it's prepared to continue the run from the next step.Then, close the robot door before proceeding.", "labware_released_from_current_height": "The labware will be released from its current height.", "launch_recovery_mode": "Launch Recovery Mode", "manually_fill_liquid_in_well": "Manually fill liquid in well {{well}}", @@ -70,6 +71,7 @@ "resume": "Resume", "retry_dropping_tip": "Retry dropping tip", "retry_now": "Retry now", + "retry_picking_up_tip": "Retry picking up tip", "retry_step": "Retry step", "retry_with_new_tips": "Retry with new tips", "retry_with_same_tips": "Retry with same tips", diff --git a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx index e763766ccb9..5f38dfabf48 100644 --- a/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx @@ -188,7 +188,7 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { return } - const buildResumeRun = (): JSX.Element => { + const buildRetryStep = (): JSX.Element => { return } @@ -246,7 +246,7 @@ export function ErrorRecoveryContent(props: RecoveryContentProps): JSX.Element { case RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE: return buildRecoveryError() case RECOVERY_MAP.RETRY_STEP.ROUTE: - return buildResumeRun() + return buildRetryStep() case RECOVERY_MAP.CANCEL_RUN.ROUTE: return buildCancelRun() case RECOVERY_MAP.DROP_TIP_FLOWS.ROUTE: diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx index d01a5c343fb..dc74ed7e529 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/FillWellAndSkip.tsx @@ -89,7 +89,6 @@ export function SkipToNextStep( const { ROBOT_SKIPPING_STEP, IGNORE_AND_SKIP } = RECOVERY_MAP const { t } = useTranslation('error_recovery') - // TODO(jh, 06-18-24): EXEC-569 const secondaryBtnOnClick = (): void => { if (selectedRecoveryOption === IGNORE_AND_SKIP.ROUTE) { void proceedToRouteAndStep(IGNORE_AND_SKIP.ROUTE) diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx index dfe9226a267..c17e947853b 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/IgnoreErrorSkipStep.tsx @@ -1,4 +1,4 @@ -import * as React from 'react' +import { useState, useEffect } from 'react' import head from 'lodash/head' import { css } from 'styled-components' import { useTranslation } from 'react-i18next' @@ -17,12 +17,14 @@ import { RECOVERY_MAP, ODD_ONLY, DESKTOP_ONLY, + ERROR_KINDS, } from '../constants' import { SelectRecoveryOption } from './SelectRecoveryOption' import { RecoveryFooterButtons, RecoverySingleColumnContentWrapper, RecoveryRadioGroup, + SkipStepInfo, } from '../shared' import type { RecoveryContentProps } from '../types' @@ -36,6 +38,8 @@ export function IgnoreErrorSkipStep(props: RecoveryContentProps): JSX.Element { switch (step) { case IGNORE_AND_SKIP.STEPS.SELECT_IGNORE_KIND: return + case IGNORE_AND_SKIP.STEPS.SKIP_STEP: + return default: console.warn(`${step} in ${route} not explicitly handled. Rerouting.`) return @@ -48,34 +52,56 @@ export function IgnoreErrorSkipStep(props: RecoveryContentProps): JSX.Element { export function IgnoreErrorStepHome({ recoveryCommands, routeUpdateActions, + errorKind, }: RecoveryContentProps): JSX.Element | null { const { t } = useTranslation('error_recovery') - const { MANUAL_FILL_AND_SKIP } = RECOVERY_MAP const { ignoreErrorKindThisRun } = recoveryCommands - const { proceedToRouteAndStep, goBackPrevStep } = routeUpdateActions + const { + proceedNextStep, + proceedToRouteAndStep, + goBackPrevStep, + } = routeUpdateActions - const [selectedOption, setSelectedOption] = React.useState( + const [selectedOption, setSelectedOption] = useState( head(IGNORE_OPTIONS_IN_ORDER) as IgnoreOption ) - // It's safe to hard code the routing here, since only one route currently - // utilizes ignoring. In the future, we may have to check the selectedRecoveryOption - // and route appropriately. + // Reset client choice to ignore all errors whenever navigating back to this view. This prevents unexpected + // behavior after pressing "go back" and ending up on this screen. + useEffect(() => { + void ignoreErrorKindThisRun(false) + }, []) + + // In order to keep routing linear, all extended "skip" flows should be kept as separate recovery options with + // go back functionality that routes to this view. Those "skip" views encapsulate the generic "skip" view. + // See the "manually fill well and skip" recovery option for an example. const ignoreOnce = (): void => { - void proceedToRouteAndStep( - MANUAL_FILL_AND_SKIP.ROUTE, - MANUAL_FILL_AND_SKIP.STEPS.SKIP - ) + switch (errorKind) { + case ERROR_KINDS.NO_LIQUID_DETECTED: + void proceedToRouteAndStep( + RECOVERY_MAP.MANUAL_FILL_AND_SKIP.ROUTE, + RECOVERY_MAP.MANUAL_FILL_AND_SKIP.STEPS.SKIP + ) + break + default: + void proceedNextStep() + } } // See ignoreOnce comment. const ignoreAlways = (): void => { - void ignoreErrorKindThisRun().then(() => - proceedToRouteAndStep( - MANUAL_FILL_AND_SKIP.ROUTE, - MANUAL_FILL_AND_SKIP.STEPS.SKIP - ) - ) + void ignoreErrorKindThisRun(true).then(() => { + switch (errorKind) { + case ERROR_KINDS.NO_LIQUID_DETECTED: + void proceedToRouteAndStep( + RECOVERY_MAP.MANUAL_FILL_AND_SKIP.ROUTE, + RECOVERY_MAP.MANUAL_FILL_AND_SKIP.STEPS.SKIP + ) + break + default: + void proceedNextStep() + } + }) } const primaryOnClick = (): void => { diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx index 0e1789aa797..547334f77c4 100644 --- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/IgnoreErrorSkipStep.test.tsx @@ -10,19 +10,23 @@ import { IgnoreErrorStepHome, IgnoreOptions, } from '../IgnoreErrorSkipStep' -import { RECOVERY_MAP } from '../../constants' +import { ERROR_KINDS, RECOVERY_MAP } from '../../constants' import { SelectRecoveryOption } from '../SelectRecoveryOption' import { clickButtonLabeled } from '../../__tests__/util' +import { SkipStepInfo } from '/app/organisms/ErrorRecoveryFlows/shared' import type { Mock } from 'vitest' -vi.mock('../shared', async () => { - const actual = await vi.importActual('../shared') +vi.mock('/app/organisms/ErrorRecoveryFlows/shared', async () => { + const actual = await vi.importActual( + '/app/organisms/ErrorRecoveryFlows/shared' + ) return { ...actual, RecoverySingleColumnContentWrapper: vi.fn(({ children }) => (
{children}
)), + SkipStepInfo: vi.fn(), } }) vi.mock('../SelectRecoveryOption') @@ -47,11 +51,13 @@ describe('IgnoreErrorSkipStep', () => { beforeEach(() => { props = { ...mockRecoveryContentProps, + recoveryCommands: { ignoreErrorKindThisRun: vi.fn() } as any, } vi.mocked(SelectRecoveryOption).mockReturnValue(
MOCK_SELECT_RECOVERY_OPTION
) + vi.mocked(SkipStepInfo).mockReturnValue(
MOCK_SKIP_STEP_INFO
) }) it(`renders IgnoreErrorStepHome when step is ${RECOVERY_MAP.IGNORE_AND_SKIP.STEPS.SELECT_IGNORE_KIND}`, () => { @@ -66,6 +72,18 @@ describe('IgnoreErrorSkipStep', () => { screen.getByText('Ignore similar errors later in the run?') }) + it(`renders SkipStepInfo when step is ${RECOVERY_MAP.IGNORE_AND_SKIP.STEPS.SKIP_STEP}`, () => { + props = { + ...props, + recoveryMap: { + ...props.recoveryMap, + step: RECOVERY_MAP.IGNORE_AND_SKIP.STEPS.SKIP_STEP, + }, + } + render(props) + screen.getByText('MOCK_SKIP_STEP_INFO') + }) + it('renders SelectRecoveryOption as a fallback', () => { props = { ...props, @@ -84,26 +102,30 @@ describe('IgnoreErrorStepHome', () => { let mockIgnoreErrorKindThisRun: Mock let mockProceedToRouteAndStep: Mock let mockGoBackPrevStep: Mock + let mockProceedNextStep: Mock beforeEach(() => { mockIgnoreErrorKindThisRun = vi.fn(() => Promise.resolve()) mockProceedToRouteAndStep = vi.fn() mockGoBackPrevStep = vi.fn() + mockProceedNextStep = vi.fn() props = { ...mockRecoveryContentProps, isOnDevice: true, + errorKind: ERROR_KINDS.NO_LIQUID_DETECTED, recoveryCommands: { ignoreErrorKindThisRun: mockIgnoreErrorKindThisRun, } as any, routeUpdateActions: { proceedToRouteAndStep: mockProceedToRouteAndStep, goBackPrevStep: mockGoBackPrevStep, + proceedNextStep: mockProceedNextStep, } as any, } }) - it('calls ignoreOnce when "ignore_only_this_error" is selected and primary button is clicked', async () => { + it(`ignoreOnce correctly routes "ignore_only_this_error" is clicked and the errorKind is ${ERROR_KINDS.NO_LIQUID_DETECTED}`, async () => { renderIgnoreErrorStepHome(props) fireEvent.click(screen.queryAllByText('Ignore only this error')[0]) clickButtonLabeled('Continue') @@ -115,7 +137,19 @@ describe('IgnoreErrorStepHome', () => { }) }) - it('calls ignoreAlways when "ignore_all_errors_of_this_type" is selected and primary button is clicked', async () => { + it(`ignoreOnce correctly routes "ignore_only_this_error" is clicked and the errorKind not explicitly handled`, async () => { + renderIgnoreErrorStepHome({ + ...props, + errorKind: ERROR_KINDS.GENERAL_ERROR, + }) + fireEvent.click(screen.queryAllByText('Ignore only this error')[0]) + clickButtonLabeled('Continue') + await waitFor(() => { + expect(mockProceedNextStep).toHaveBeenCalled() + }) + }) + + it(`ignoreAlways correctly routes when "ignore_all_errors_of_this_type" is clicked and the errorKind is ${ERROR_KINDS.NO_LIQUID_DETECTED}`, async () => { renderIgnoreErrorStepHome(props) fireEvent.click(screen.queryAllByText('Ignore all errors of this type')[0]) clickButtonLabeled('Continue') @@ -130,6 +164,18 @@ describe('IgnoreErrorStepHome', () => { }) }) + it(`ignoreAlways correctly routes "ignore_all_errors_of_this_type" is clicked and the errorKind not explicitly handled`, async () => { + renderIgnoreErrorStepHome({ + ...props, + errorKind: ERROR_KINDS.GENERAL_ERROR, + }) + fireEvent.click(screen.queryAllByText('Ignore all errors of this type')[0]) + clickButtonLabeled('Continue') + await waitFor(() => { + expect(mockProceedNextStep).toHaveBeenCalled() + }) + }) + it('calls goBackPrevStep when secondary button is clicked', () => { renderIgnoreErrorStepHome(props) clickButtonLabeled('Go back') diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts index d63e69e5e22..75835fd29f3 100644 --- a/app/src/organisms/ErrorRecoveryFlows/constants.ts +++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts @@ -120,7 +120,7 @@ export const RECOVERY_MAP = { }, IGNORE_AND_SKIP: { ROUTE: 'ignore-and-skip-step', - STEPS: { SELECT_IGNORE_KIND: 'select-ignore' }, + STEPS: { SELECT_IGNORE_KIND: 'select-ignore', SKIP_STEP: 'skip-step' }, }, MANUAL_FILL_AND_SKIP: { ROUTE: 'manual-fill-well-and-skip', @@ -248,7 +248,10 @@ export const STEP_ORDER: StepOrder = { DROP_TIP_FLOWS.STEPS.CHOOSE_TIP_DROP, ], [REFILL_AND_RESUME.ROUTE]: [], - [IGNORE_AND_SKIP.ROUTE]: [IGNORE_AND_SKIP.STEPS.SELECT_IGNORE_KIND], + [IGNORE_AND_SKIP.ROUTE]: [ + IGNORE_AND_SKIP.STEPS.SELECT_IGNORE_KIND, + IGNORE_AND_SKIP.STEPS.SKIP_STEP, + ], [CANCEL_RUN.ROUTE]: [CANCEL_RUN.STEPS.CONFIRM_CANCEL], [MANUAL_FILL_AND_SKIP.ROUTE]: [ MANUAL_FILL_AND_SKIP.STEPS.MANUAL_FILL, @@ -343,6 +346,7 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = { [IGNORE_AND_SKIP.STEPS.SELECT_IGNORE_KIND]: { allowDoorOpen: false, }, + [IGNORE_AND_SKIP.STEPS.SKIP_STEP]: { allowDoorOpen: false }, }, [MANUAL_FILL_AND_SKIP.ROUTE]: { [MANUAL_FILL_AND_SKIP.STEPS.MANUAL_FILL]: { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts index 016e38be69d..11a15edfbfd 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryCommands.test.ts @@ -44,7 +44,7 @@ describe('useRecoveryCommands', () => { const mockChainRunCommands = vi.fn().mockResolvedValue([]) const mockReportActionSelectedResult = vi.fn() const mockReportRecoveredRunResult = vi.fn() - const mockUpdateErrorRecoveryPolicy = vi.fn() + const mockUpdateErrorRecoveryPolicy = vi.fn(() => Promise.resolve()) const props = { runId: mockRunId, @@ -70,7 +70,7 @@ describe('useRecoveryCommands', () => { chainRunCommands: mockChainRunCommands, } as any) vi.mocked(useUpdateErrorRecoveryPolicy).mockReturnValue({ - updateErrorRecoveryPolicy: mockUpdateErrorRecoveryPolicy, + mutateAsync: mockUpdateErrorRecoveryPolicy, } as any) }) @@ -302,12 +302,18 @@ describe('useRecoveryCommands', () => { failedCommandByRunRecord: mockFailedCommandWithError, } - const { result } = renderHook(() => useRecoveryCommands(testProps)) + const { result, rerender } = renderHook(() => + useRecoveryCommands(testProps) + ) await act(async () => { - await result.current.ignoreErrorKindThisRun() + await result.current.ignoreErrorKindThisRun(true) }) + rerender() + + result.current.skipFailedCommand() + const expectedPolicyRules = buildIgnorePolicyRules( 'aspirateInPlace', 'mockErrorType' @@ -318,16 +324,33 @@ describe('useRecoveryCommands', () => { ) }) - it('should reject with an error when failedCommand or error is null', async () => { + it('should call proceedToRouteAndStep with ERROR_WHILE_RECOVERING route when updateErrorRecoveryPolicy rejects', async () => { + const mockFailedCommandWithError = { + ...mockFailedCommand, + commandType: 'aspirateInPlace', + error: { + errorType: 'mockErrorType', + }, + } + const testProps = { ...props, - failedCommand: null, + failedCommandByRunRecord: mockFailedCommandWithError, } + mockUpdateErrorRecoveryPolicy.mockRejectedValueOnce( + new Error('Update policy failed') + ) + const { result } = renderHook(() => useRecoveryCommands(testProps)) - await expect(result.current.ignoreErrorKindThisRun()).rejects.toThrow( - 'Could not execute command. No failed command.' + await act(async () => { + await result.current.ignoreErrorKindThisRun(true) + }) + + expect(mockUpdateErrorRecoveryPolicy).toHaveBeenCalled() + expect(mockProceedToRouteAndStep).toHaveBeenCalledWith( + RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE ) }) }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx index 2c2e2b4442b..62a810cd96e 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx @@ -48,6 +48,15 @@ describe('useRecoveryOptionCopy', () => { screen.getByText('Retry dropping tip') }) + it(`renders the correct copy for ${RECOVERY_MAP.RETRY_STEP.ROUTE} when the error kind is ${ERROR_KINDS.TIP_NOT_DETECTED}`, () => { + render({ + route: RECOVERY_MAP.RETRY_STEP.ROUTE, + errorKind: ERROR_KINDS.TIP_NOT_DETECTED, + }) + + screen.getByText('Retry picking up tip') + }) + it(`renders the correct copy for ${RECOVERY_MAP.CANCEL_RUN.ROUTE}`, () => { render({ route: RECOVERY_MAP.CANCEL_RUN.ROUTE }) diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts index 777294da62d..f0962b07693 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts @@ -1,4 +1,4 @@ -import { useCallback } from 'react' +import { useCallback, useState } from 'react' import head from 'lodash/head' import { @@ -50,8 +50,10 @@ export interface UseRecoveryCommandsResult { cancelRun: () => void /* A terminal recovery command, that causes ER to exit as the run status becomes "running" */ skipFailedCommand: () => void - /* A non-terminal recovery command. Ignore this errorKind for the rest of this run. */ - ignoreErrorKindThisRun: () => Promise + /* A non-terminal recovery command. Ignore this errorKind for the rest of this run. + * The server is not informed of recovery policy changes until a terminal recovery command occurs that does not result + * in termination of the run. */ + ignoreErrorKindThisRun: (ignoreErrors: boolean) => Promise /* A non-terminal recovery command */ retryFailedCommand: () => Promise /* A non-terminal recovery command */ @@ -77,6 +79,8 @@ export function useRecoveryCommands({ analytics, selectedRecoveryOption, }: UseRecoveryCommandsParams): UseRecoveryCommandsResult { + const [ignoreErrors, setIgnoreErrors] = useState(false) + const { proceedToRouteAndStep } = routeUpdateActions const { chainRunCommands } = useChainRunCommands( runId, @@ -86,7 +90,9 @@ export function useRecoveryCommands({ mutateAsync: resumeRunFromRecovery, } = useResumeRunFromRecoveryMutation() const { stopRun } = useStopRunMutation() - const { updateErrorRecoveryPolicy } = useUpdateErrorRecoveryPolicy(runId) + const { + mutateAsync: updateErrorRecoveryPolicy, + } = useUpdateErrorRecoveryPolicy(runId) const { makeSuccessToast } = recoveryToastUtils const chainRunRecoveryCommands = useCallback( @@ -182,12 +188,59 @@ export function useRecoveryCommands({ } }, [chainRunRecoveryCommands, failedCommandByRunRecord, failedLabwareUtils]) + const ignoreErrorKindThisRun = (ignoreErrors: boolean): Promise => { + setIgnoreErrors(ignoreErrors) + return Promise.resolve() + } + + // Only send the finalized error policy to the server during a terminal recovery command that does not terminate the run. + // If the request to update the policy fails, route to the error modal. + const handleIgnoringErrorKind = useCallback((): Promise => { + if (ignoreErrors) { + if (failedCommandByRunRecord?.error != null) { + const ignorePolicyRules = buildIgnorePolicyRules( + failedCommandByRunRecord.commandType, + failedCommandByRunRecord.error.errorType + ) + + return updateErrorRecoveryPolicy(ignorePolicyRules) + .then(() => Promise.resolve()) + .catch(() => + Promise.reject(new Error('Failed to update recovery policy.')) + ) + } else { + void proceedToRouteAndStep(RECOVERY_MAP.ERROR_WHILE_RECOVERING.ROUTE) + return Promise.reject( + new Error('Could not execute command. No failed command.') + ) + } + } else { + return Promise.resolve() + } + }, [ + failedCommandByRunRecord?.error?.errorType, + failedCommandByRunRecord?.commandType, + ignoreErrors, + ]) + const resumeRun = useCallback((): void => { - void resumeRunFromRecovery(runId).then(() => { - analytics.reportActionSelectedResult(selectedRecoveryOption, 'succeeded') - makeSuccessToast() - }) - }, [runId, resumeRunFromRecovery, makeSuccessToast]) + void handleIgnoringErrorKind() + .then(() => resumeRunFromRecovery(runId)) + .then(() => { + analytics.reportActionSelectedResult( + selectedRecoveryOption, + 'succeeded' + ) + makeSuccessToast() + }) + }, [ + runId, + ignoreErrors, + resumeRunFromRecovery, + handleIgnoringErrorKind, + selectedRecoveryOption, + makeSuccessToast, + ]) const cancelRun = useCallback((): void => { analytics.reportActionSelectedResult(selectedRecoveryOption, 'succeeded') @@ -195,29 +248,21 @@ export function useRecoveryCommands({ }, [runId]) const skipFailedCommand = useCallback((): void => { - void resumeRunFromRecovery(runId).then(() => { - analytics.reportActionSelectedResult(selectedRecoveryOption, 'succeeded') - makeSuccessToast() - }) - }, [runId, resumeRunFromRecovery, makeSuccessToast]) - - const ignoreErrorKindThisRun = useCallback((): Promise => { - if (failedCommandByRunRecord?.error != null) { - const ignorePolicyRules = buildIgnorePolicyRules( - failedCommandByRunRecord.commandType, - failedCommandByRunRecord.error.errorType - ) - - updateErrorRecoveryPolicy(ignorePolicyRules) - return Promise.resolve() - } else { - return Promise.reject( - new Error('Could not execute command. No failed command.') - ) - } + void handleIgnoringErrorKind().then(() => + resumeRunFromRecovery(runId).then(() => { + analytics.reportActionSelectedResult( + selectedRecoveryOption, + 'succeeded' + ) + makeSuccessToast() + }) + ) }, [ - failedCommandByRunRecord?.error?.errorType, - failedCommandByRunRecord?.commandType, + runId, + resumeRunFromRecovery, + handleIgnoringErrorKind, + selectedRecoveryOption, + makeSuccessToast, ]) const releaseGripperJaws = useCallback((): Promise => { diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx index 18a7da7a319..b364af7f9d5 100644 --- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx @@ -19,6 +19,8 @@ export function useRecoveryOptionCopy(): ( case RECOVERY_MAP.RETRY_STEP.ROUTE: if (errorKind === ERROR_KINDS.TIP_DROP_FAILED) { return t('retry_dropping_tip') + } else if (errorKind === ERROR_KINDS.TIP_NOT_DETECTED) { + return t('retry_picking_up_tip') } else { return t('retry_step') } diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/SkipStepInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/SkipStepInfo.tsx index 1caed4f583b..4cc31e4732a 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/SkipStepInfo.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/SkipStepInfo.tsx @@ -17,6 +17,7 @@ export function SkipStepInfo(props: RecoveryContentProps): JSX.Element { SKIP_STEP_WITH_SAME_TIPS, SKIP_STEP_WITH_NEW_TIPS, MANUAL_MOVE_AND_SKIP, + IGNORE_AND_SKIP, } = RECOVERY_MAP const { selectedRecoveryOption } = currentRecoveryOptionUtils const { skipFailedCommand } = recoveryCommands @@ -43,6 +44,7 @@ export function SkipStepInfo(props: RecoveryContentProps): JSX.Element { return t('skip_to_next_step_same_tips') case SKIP_STEP_WITH_NEW_TIPS.ROUTE: return t('skip_to_next_step_new_tips') + case IGNORE_AND_SKIP.ROUTE: case MANUAL_MOVE_AND_SKIP.ROUTE: return t('skip_to_next_step') default: @@ -55,6 +57,8 @@ export function SkipStepInfo(props: RecoveryContentProps): JSX.Element { const buildBodyCopyKey = (): string => { switch (selectedRecoveryOption) { + case IGNORE_AND_SKIP.ROUTE: + return 'inspect_the_robot' case SKIP_STEP_WITH_SAME_TIPS.ROUTE: case SKIP_STEP_WITH_NEW_TIPS.ROUTE: return 'failed_dispense_step_not_completed' diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx index 11b4df62700..d759aaf3d78 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx @@ -171,7 +171,7 @@ describe('TipNotDetectedBanner', () => { 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', + ' If the issue persists, cancel the run and perform Labware Position Check', }), {} ) diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx index 77095a45a6d..e1ac4cf1adc 100644 --- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx +++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/SkipStepInfo.test.tsx @@ -58,6 +58,18 @@ describe('SkipStepInfo', () => { }) }) + it(`renders correct title and body text for ${RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE}`, () => { + props.currentRecoveryOptionUtils.selectedRecoveryOption = + RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE + render(props) + + screen.getByText('Skip to next step') + screen.getByText( + "First, inspect the robot to ensure it's prepared to continue the run from the next step." + ) + screen.getByText('Then, close the robot door before proceeding.') + }) + it(`renders correct title and body text for ${RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE}`, () => { props.currentRecoveryOptionUtils.selectedRecoveryOption = RECOVERY_MAP.SKIP_STEP_WITH_SAME_TIPS.ROUTE @@ -95,8 +107,7 @@ describe('SkipStepInfo', () => { }) it('renders error message for unexpected recovery option', () => { - props.currentRecoveryOptionUtils.selectedRecoveryOption = - RECOVERY_MAP.IGNORE_AND_SKIP.ROUTE + props.currentRecoveryOptionUtils.selectedRecoveryOption = 'UNEXPECTED_ROUTE' as any render(props) expect(screen.getAllByText('UNEXPECTED STEP')[0]).toBeInTheDocument()