+ beforeEach(() => {
+ props = {
+ ...mockRecoveryContentProps,
+ currentRecoveryOptionUtils: {
+ ...mockRecoveryContentProps.currentRecoveryOptionUtils,
+ selectedRecoveryOption: RECOVERY_MAP.HOME_AND_RETRY.ROUTE,
+ },
+ }
+ vi.mocked(SelectRecoveryOption).mockReturnValue(
+ MOCK_SELECT_RECOVERY_OPTION
+ )
+ vi.mocked(TipSelection).mockReturnValue(WELL_SELECTION
)
+ })
+ afterEach(() => {
+ vi.resetAllMocks()
+ })
+ it(`renders PrepareDeckForHome when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME}`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME,
+ },
+ }
+ render(props)
+ screen.getByText('Prepare deck for homing')
+ })
+ it(`renders ManageTips when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE}`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE,
+ },
+ tipStatusUtils: {
+ ...props.tipStatusUtils,
+ aPipetteWithTip: {
+ mount: 'left',
+ } as any,
+ },
+ }
+ render(props)
+ screen.getByText('Remove any attached tips')
+ })
+ it(`renders labware info when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.REPLACE_TIPS}`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.REPLACE_TIPS,
+ },
+ failedLabwareUtils: {
+ ...props.failedLabwareUtils,
+ relevantWellName: 'A2',
+ failedLabwareLocations: {
+ ...props.failedLabwareUtils.failedLabwareLocations,
+ displayNameCurrentLoc: 'B2',
+ },
+ },
+ }
+
+ render(props)
+ screen.getByText('Replace used tips in rack location A2 in B2')
+ })
+ it(`renders SelectTips when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.SELECT_TIPS}`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.SELECT_TIPS,
+ },
+ failedLabwareUtils: {
+ ...props.failedLabwareUtils,
+ failedLabwareLocations: {
+ ...props.failedLabwareUtils.failedLabwareLocations,
+ displayNameCurrentLoc: 'B2',
+ },
+ },
+ }
+ render(props)
+ screen.getByText('Select tip pick-up location')
+ })
+ it(`renders HomeGantryBeforeRetry when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY}`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY,
+ },
+ }
+ render(props)
+ screen.getByText('Home gantry')
+ })
+ it(`renders the special door open handler when step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME}`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME,
+ },
+ doorStatusUtils: {
+ ...props.doorStatusUtils,
+ isDoorOpen: true,
+ },
+ }
+ render(props)
+ screen.getByText('Close the robot door')
+ })
+ it(`renders RetryAfterHome awhen step is ${RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY}`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ step: RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY,
+ },
+ }
+ render(props)
+ screen.getByText('Retry step')
+ })
+ it(`renders SelectRecoveryOption as a fallback`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ step: 'UNKNOWN_STEP' as any,
+ },
+ }
+ render(props)
+ screen.getByText('MOCK_SELECT_RECOVERY_OPTION')
+ })
+})
diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx
index a0dd0c778ca..62fe8eea3c8 100644
--- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/__tests__/SelectRecoveryOptions.test.tsx
@@ -18,6 +18,7 @@ import {
TIP_NOT_DETECTED_OPTIONS,
TIP_DROP_FAILED_OPTIONS,
GRIPPER_ERROR_OPTIONS,
+ STALL_OR_COLLISION_OPTIONS,
} from '../SelectRecoveryOption'
import { RECOVERY_MAP, ERROR_KINDS } from '../../constants'
import { clickButtonLabeled } from '../../__tests__/util'
@@ -95,6 +96,9 @@ describe('SelectRecoveryOption', () => {
expect.any(String)
)
.thenReturn('Skip to next step with same tips')
+ when(mockGetRecoveryOptionCopy)
+ .calledWith(RECOVERY_MAP.HOME_AND_RETRY.ROUTE, expect.any(String))
+ .thenReturn('Home gantry and retry')
})
it('sets the selected recovery option when clicking continue', () => {
@@ -231,6 +235,22 @@ describe('SelectRecoveryOption', () => {
RECOVERY_MAP.RETRY_STEP.ROUTE
)
})
+ it('renders appropriate "Stall or collision" copy and click behavior', () => {
+ props = {
+ ...props,
+ errorKind: ERROR_KINDS.STALL_OR_COLLISION,
+ }
+ renderSelectRecoveryOption(props)
+ screen.getByText('Choose a recovery action')
+ const homeGantryAndRetry = screen.getAllByRole('label', {
+ name: 'Home gantry and retry',
+ })
+ fireEvent.click(homeGantryAndRetry[0])
+ clickButtonLabeled('Continue')
+ expect(mockProceedToRouteAndStep).toHaveBeenCalledWith(
+ RECOVERY_MAP.HOME_AND_RETRY.ROUTE
+ )
+ })
})
describe('RecoveryOptions', () => {
let props: React.ComponentProps
@@ -292,6 +312,9 @@ describe('RecoveryOptions', () => {
expect.any(String)
)
.thenReturn('Manually replace labware on deck and retry step')
+ when(mockGetRecoveryOptionCopy)
+ .calledWith(RECOVERY_MAP.HOME_AND_RETRY.ROUTE, expect.any(String))
+ .thenReturn('Home gantry and retry')
})
it('renders valid recovery options for a general error errorKind', () => {
@@ -415,6 +438,17 @@ describe('RecoveryOptions', () => {
})
screen.getByRole('label', { name: 'Cancel run' })
})
+ it(`renders valid recovery options for a ${ERROR_KINDS.STALL_OR_COLLISION} errorKind`, () => {
+ props = {
+ ...props,
+ validRecoveryOptions: STALL_OR_COLLISION_OPTIONS,
+ }
+ renderRecoveryOptions(props)
+ screen.getByRole('label', {
+ name: 'Home gantry and retry',
+ })
+ screen.getByRole('label', { name: 'Cancel run' })
+ })
})
describe('getRecoveryOptions', () => {
@@ -475,4 +509,11 @@ describe('getRecoveryOptions', () => {
)
expect(overpressureWhileDispensingOptions).toBe(GRIPPER_ERROR_OPTIONS)
})
+
+ it(`returns valid options when the errorKind is ${ERROR_KINDS.STALL_OR_COLLISION}`, () => {
+ const stallOrCollisionOptions = getRecoveryOptions(
+ ERROR_KINDS.STALL_OR_COLLISION
+ )
+ expect(stallOrCollisionOptions).toBe(STALL_OR_COLLISION_OPTIONS)
+ })
})
diff --git a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts
index 0e50d054523..0ad8f530709 100644
--- a/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/RecoveryOptions/index.ts
@@ -10,3 +10,4 @@ export { SkipStepNewTips } from './SkipStepNewTips'
export { IgnoreErrorSkipStep } from './IgnoreErrorSkipStep'
export { ManualMoveLwAndSkip } from './ManualMoveLwAndSkip'
export { ManualReplaceLwAndRetry } from './ManualReplaceLwAndRetry'
+export { HomeAndRetry } from './HomeAndRetry'
diff --git a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx
index dd915b72afb..d97072e45f3 100644
--- a/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/__tests__/ErrorRecoveryWizard.test.tsx
@@ -24,6 +24,7 @@ import {
IgnoreErrorSkipStep,
ManualReplaceLwAndRetry,
ManualMoveLwAndSkip,
+ HomeAndRetry,
} from '../RecoveryOptions'
import { RecoveryInProgress } from '../RecoveryInProgress'
import { RecoveryError } from '../RecoveryError'
@@ -188,6 +189,7 @@ describe('ErrorRecoveryContent', () => {
ROBOT_RELEASING_LABWARE,
MANUAL_REPLACE_AND_RETRY,
MANUAL_MOVE_AND_SKIP,
+ HOME_AND_RETRY,
} = RECOVERY_MAP
let props: React.ComponentProps
@@ -225,6 +227,7 @@ describe('ErrorRecoveryContent', () => {
vi.mocked(RecoveryDoorOpenSpecial).mockReturnValue(
MOCK_DOOR_OPEN_SPECIAL
)
+ vi.mocked(HomeAndRetry).mockReturnValue(MOCK_HOME_AND_RETRY
)
})
it(`returns SelectRecoveryOption when the route is ${OPTION_SELECTION.ROUTE}`, () => {
@@ -505,4 +508,17 @@ describe('ErrorRecoveryContent', () => {
screen.getByText('MOCK_DOOR_OPEN_SPECIAL')
})
+
+ it(`returns HomeAndRetry when the route is ${HOME_AND_RETRY.ROUTE}`, () => {
+ props = {
+ ...props,
+ recoveryMap: {
+ ...props.recoveryMap,
+ route: HOME_AND_RETRY.ROUTE,
+ },
+ }
+ renderRecoveryContent(props)
+
+ screen.getByText('MOCK_HOME_AND_RETRY')
+ })
})
diff --git a/app/src/organisms/ErrorRecoveryFlows/constants.ts b/app/src/organisms/ErrorRecoveryFlows/constants.ts
index 75835fd29f3..8be1b6adbe1 100644
--- a/app/src/organisms/ErrorRecoveryFlows/constants.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/constants.ts
@@ -20,6 +20,7 @@ export const DEFINED_ERROR_TYPES = {
TIP_PHYSICALLY_MISSING: 'tipPhysicallyMissing',
TIP_PHYSICALLY_ATTACHED: 'tipPhysicallyAttached',
GRIPPER_MOVEMENT: 'gripperMovement',
+ STALL_OR_COLLISION: 'stallOrCollision',
}
// Client-defined error-handling flows.
@@ -32,6 +33,7 @@ export const ERROR_KINDS = {
TIP_NOT_DETECTED: 'TIP_NOT_DETECTED',
TIP_DROP_FAILED: 'TIP_DROP_FAILED',
GRIPPER_ERROR: 'GRIPPER_ERROR',
+ STALL_OR_COLLISION: 'STALL_OR_COLLISION',
} as const
// TODO(jh, 06-14-24): Consolidate motion routes to a single route with several steps.
@@ -55,6 +57,18 @@ export const RECOVERY_MAP = {
DROP_TIP_GENERAL_ERROR: 'drop-tip-general-error',
},
},
+ HOME_AND_RETRY: {
+ ROUTE: 'home-and-retry',
+ STEPS: {
+ PREPARE_DECK_FOR_HOME: 'prepare-deck-for-home',
+ REMOVE_TIPS_FROM_PIPETTE: 'remove-tips-from-pipette',
+ REPLACE_TIPS: 'replace-tips',
+ SELECT_TIPS: 'select-tips',
+ HOME_BEFORE_RETRY: 'home-before-retry',
+ CLOSE_DOOR_AND_HOME: 'close-door-and-home',
+ CONFIRM_RETRY: 'confirm-retry',
+ },
+ },
ROBOT_CANCELING: {
ROUTE: 'robot-cancel-run',
STEPS: {
@@ -210,6 +224,7 @@ const {
MANUAL_REPLACE_AND_RETRY,
SKIP_STEP_WITH_NEW_TIPS,
SKIP_STEP_WITH_SAME_TIPS,
+ HOME_AND_RETRY,
} = RECOVERY_MAP
// The deterministic ordering of steps for a given route.
@@ -277,6 +292,15 @@ export const STEP_ORDER: StepOrder = {
ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_TIP_DROP_FAILED,
ERROR_WHILE_RECOVERING.STEPS.DROP_TIP_BLOWOUT_FAILED,
],
+ [HOME_AND_RETRY.ROUTE]: [
+ HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME,
+ HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE,
+ HOME_AND_RETRY.STEPS.REPLACE_TIPS,
+ HOME_AND_RETRY.STEPS.SELECT_TIPS,
+ HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY,
+ HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME,
+ HOME_AND_RETRY.STEPS.CONFIRM_RETRY,
+ ],
}
// Contains metadata specific to all routes and/or steps.
@@ -333,6 +357,15 @@ export const RECOVERY_MAP_METADATA: RecoveryRouteStepMetadata = {
[ROBOT_DOOR_OPEN.ROUTE]: {
[ROBOT_DOOR_OPEN.STEPS.DOOR_OPEN]: { allowDoorOpen: false },
},
+ [HOME_AND_RETRY.ROUTE]: {
+ [HOME_AND_RETRY.STEPS.PREPARE_DECK_FOR_HOME]: { allowDoorOpen: true },
+ [HOME_AND_RETRY.STEPS.REMOVE_TIPS_FROM_PIPETTE]: { allowDoorOpen: true },
+ [HOME_AND_RETRY.STEPS.REPLACE_TIPS]: { allowDoorOpen: true },
+ [HOME_AND_RETRY.STEPS.SELECT_TIPS]: { allowDoorOpen: true },
+ [HOME_AND_RETRY.STEPS.HOME_BEFORE_RETRY]: { allowDoorOpen: true },
+ [HOME_AND_RETRY.STEPS.CLOSE_DOOR_AND_HOME]: { allowDoorOpen: true },
+ [HOME_AND_RETRY.STEPS.CONFIRM_RETRY]: { allowDoorOpen: true },
+ },
[ROBOT_DOOR_OPEN_SPECIAL.ROUTE]: {
[ROBOT_DOOR_OPEN_SPECIAL.STEPS.DOOR_OPEN]: { allowDoorOpen: true },
},
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx
index 62a810cd96e..11e8a574246 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/__tests__/useRecoveryOptionCopy.test.tsx
@@ -111,6 +111,11 @@ describe('useRecoveryOptionCopy', () => {
screen.getByText('Manually replace labware on deck and retry step')
})
+ it(`renders the correct copy for ${RECOVERY_MAP.HOME_AND_RETRY.ROUTE}`, () => {
+ render({ route: RECOVERY_MAP.HOME_AND_RETRY.ROUTE })
+ screen.getByText('Home gantry and retry step')
+ })
+
it('renders "Unknown action" for an unknown recovery option', () => {
render({ route: 'unknown_route' as RecoveryRoute })
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts
index 6acd0df2f45..0279b8b675a 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useErrorName.ts
@@ -23,6 +23,8 @@ export function useErrorName(errorKind: ErrorKind): string {
return t('tip_drop_failed')
case ERROR_KINDS.GRIPPER_ERROR:
return t('gripper_error')
+ case ERROR_KINDS.STALL_OR_COLLISION:
+ return t('stall_or_collision_error')
default:
return t('error')
}
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts
index bc077d4c624..f1a57aa965f 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useFailedLabwareUtils.ts
@@ -148,6 +148,7 @@ export function getRelevantFailedLabwareCmdFrom({
case ERROR_KINDS.OVERPRESSURE_PREPARE_TO_ASPIRATE:
case ERROR_KINDS.OVERPRESSURE_WHILE_ASPIRATING:
case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING:
+ case ERROR_KINDS.STALL_OR_COLLISION:
return getRelevantPickUpTipCommand(failedCommandByRunRecord, runCommands)
case ERROR_KINDS.GRIPPER_ERROR:
return failedCommandByRunRecord as MoveLabwareRunTimeCommand
@@ -155,7 +156,7 @@ export function getRelevantFailedLabwareCmdFrom({
return null
default:
console.error(
- 'No labware associated with failed command. Handle case explicitly.'
+ `useFailedLabwareUtils: No labware associated with error kind ${errorKind}. Handle case explicitly.`
)
return null
}
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts
index 3e4b20225c5..01f5c4a7c94 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryCommands.ts
@@ -70,6 +70,8 @@ export interface UseRecoveryCommandsResult {
homeExceptPlungers: () => Promise
/* A non-terminal recovery command */
moveLabwareWithoutPause: () => Promise
+ /* A non-terminal recovery-command */
+ homeAll: () => Promise
}
// TODO(jh, 07-24-24): Create tighter abstractions for terminal vs. non-terminal commands.
@@ -307,6 +309,10 @@ export function useRecoveryCommands({
return chainRunRecoveryCommands([HOME_EXCEPT_PLUNGERS])
}, [chainRunRecoveryCommands])
+ const homeAll = useCallback((): Promise => {
+ return chainRunRecoveryCommands([HOME_ALL])
+ }, [chainRunRecoveryCommands])
+
const moveLabwareWithoutPause = useCallback((): Promise => {
const moveLabwareCmd = buildMoveLabwareWithoutPause(
unvalidatedFailedCommand
@@ -329,6 +335,7 @@ export function useRecoveryCommands({
moveLabwareWithoutPause,
skipFailedCommand,
ignoreErrorKindThisRun,
+ homeAll,
}
}
@@ -371,6 +378,11 @@ export const HOME_EXCEPT_PLUNGERS: CreateCommand = {
},
}
+export const HOME_ALL: CreateCommand = {
+ commandType: 'home',
+ params: {},
+}
+
const buildMoveLabwareWithoutPause = (
failedCommand: FailedCommand | null
): CreateCommand | null => {
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx
index b364af7f9d5..6c7f2f8fc94 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryOptionCopy.tsx
@@ -26,6 +26,8 @@ export function useRecoveryOptionCopy(): (
}
case RECOVERY_MAP.CANCEL_RUN.ROUTE:
return t('cancel_run')
+ case RECOVERY_MAP.HOME_AND_RETRY.ROUTE:
+ return t('home_and_retry')
case RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE:
return t('retry_with_new_tips')
case RECOVERY_MAP.RETRY_SAME_TIPS.ROUTE:
diff --git a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts
index 9fef84caca9..533b9877f72 100644
--- a/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/hooks/useRecoveryToasts.ts
@@ -171,6 +171,7 @@ function handleRecoveryOptionAction(
case RECOVERY_MAP.RETRY_NEW_TIPS.ROUTE:
case RECOVERY_MAP.RETRY_STEP.ROUTE:
case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE:
+ case RECOVERY_MAP.HOME_AND_RETRY.ROUTE:
return currentStepReturnVal
default: {
return null
diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx
index 7eb207a9fe7..603ac5af6c3 100644
--- a/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/shared/ErrorDetailsModal.tsx
@@ -67,6 +67,7 @@ export function ErrorDetailsModal(props: ErrorDetailsModalProps): JSX.Element {
case ERROR_KINDS.OVERPRESSURE_WHILE_DISPENSING:
case ERROR_KINDS.TIP_NOT_DETECTED:
case ERROR_KINDS.GRIPPER_ERROR:
+ case ERROR_KINDS.STALL_OR_COLLISION:
return true
default:
return false
@@ -213,6 +214,8 @@ export function NotificationBanner({
return
case ERROR_KINDS.GRIPPER_ERROR:
return
+ case ERROR_KINDS.STALL_OR_COLLISION:
+ return
default:
console.error('Handle error kind notification banners explicitly.')
return
@@ -258,6 +261,18 @@ export function GripperErrorBanner(): JSX.Element {
)
}
+export function StallErrorBanner(): JSX.Element {
+ const { t } = useTranslation('error_recovery')
+
+ return (
+
+ )
+}
+
// TODO(jh, 07-24-24): Using shared height/width constants for intervention modal sizing and the ErrorDetailsModal sizing
// would be ideal.
const DESKTOP_STEP_INFO_STYLE = css`
diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx
index 98744985225..00b64839b90 100644
--- a/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/shared/RecoveryDoorOpenSpecial.tsx
@@ -52,6 +52,7 @@ export function RecoveryDoorOpenSpecial({
switch (selectedRecoveryOption) {
case RECOVERY_MAP.MANUAL_REPLACE_AND_RETRY.ROUTE:
case RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.ROUTE:
+ case RECOVERY_MAP.HOME_AND_RETRY.ROUTE:
return t('door_open_robot_home')
default: {
console.error(
@@ -62,6 +63,16 @@ export function RecoveryDoorOpenSpecial({
}
}
+ const handleHomeAllAndRoute = (
+ route: RecoveryRoute,
+ step?: RouteStep
+ ): void => {
+ void handleMotionRouting(true, RECOVERY_MAP.ROBOT_IN_MOTION.ROUTE)
+ .then(() => recoveryCommands.homeAll())
+ .finally(() => handleMotionRouting(false))
+ .then(() => proceedToRouteAndStep(route, step))
+ }
+
const handleHomeExceptPlungersAndRoute = (
route: RecoveryRoute,
step?: RouteStep
@@ -87,6 +98,12 @@ export function RecoveryDoorOpenSpecial({
RECOVERY_MAP.MANUAL_MOVE_AND_SKIP.STEPS.MANUAL_MOVE
)
break
+ case RECOVERY_MAP.HOME_AND_RETRY.ROUTE:
+ handleHomeAllAndRoute(
+ RECOVERY_MAP.HOME_AND_RETRY.ROUTE,
+ RECOVERY_MAP.HOME_AND_RETRY.STEPS.CONFIRM_RETRY
+ )
+ break
default: {
console.error(
`Unhandled special-cased door open on route ${selectedRecoveryOption}.`
diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/RetryStepInfo.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/RetryStepInfo.tsx
index c9f7567ee94..fd7a1dbaf5f 100644
--- a/app/src/organisms/ErrorRecoveryFlows/shared/RetryStepInfo.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/shared/RetryStepInfo.tsx
@@ -7,7 +7,9 @@ import { TwoColTextAndFailedStepNextStep } from './TwoColTextAndFailedStepNextSt
import type { RecoveryContentProps } from '../types'
-export function RetryStepInfo(props: RecoveryContentProps): JSX.Element {
+export function RetryStepInfo(
+ props: RecoveryContentProps & { secondaryBtnOnClickOverride?: () => void }
+): JSX.Element {
const { routeUpdateActions, recoveryCommands, errorKind } = props
const { ROBOT_RETRYING_STEP } = RECOVERY_MAP
const { t } = useTranslation('error_recovery')
diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx
index 00fa95072c1..9bf8f12bc22 100644
--- a/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/shared/TwoColLwInfoAndDeck.tsx
@@ -34,6 +34,7 @@ export function TwoColLwInfoAndDeck(
SKIP_STEP_WITH_NEW_TIPS,
MANUAL_MOVE_AND_SKIP,
MANUAL_REPLACE_AND_RETRY,
+ HOME_AND_RETRY,
} = RECOVERY_MAP
const { selectedRecoveryOption } = currentRecoveryOptionUtils
const { relevantWellName, failedLabware } = failedLabwareUtils
@@ -55,6 +56,7 @@ export function TwoColLwInfoAndDeck(
return t('manually_move_lw_on_deck')
case MANUAL_REPLACE_AND_RETRY.ROUTE:
return t('manually_replace_lw_on_deck')
+ case HOME_AND_RETRY.ROUTE:
case RETRY_NEW_TIPS.ROUTE:
case SKIP_STEP_WITH_NEW_TIPS.ROUTE: {
// Only special case the "full" 96-channel nozzle config.
@@ -72,7 +74,7 @@ export function TwoColLwInfoAndDeck(
}
default:
console.error(
- 'Unexpected recovery option. Handle retry step copy explicitly.'
+ `TwoColLwInfoAndDeck: Unexpected recovery option: ${selectedRecoveryOption}. Handle retry step copy explicitly.`
)
return 'UNEXPECTED RECOVERY OPTION'
}
@@ -84,14 +86,15 @@ export function TwoColLwInfoAndDeck(
case MANUAL_REPLACE_AND_RETRY.ROUTE:
return t('ensure_lw_is_accurately_placed')
case RETRY_NEW_TIPS.ROUTE:
- case SKIP_STEP_WITH_NEW_TIPS.ROUTE: {
+ case SKIP_STEP_WITH_NEW_TIPS.ROUTE:
+ case HOME_AND_RETRY.ROUTE: {
return isPartialTipConfigValid
? t('replace_tips_and_select_loc_partial_tip')
: t('replace_tips_and_select_location')
}
default:
console.error(
- 'Unexpected recovery option. Handle retry step copy explicitly.'
+ `TwoColLwInfoAndDeck:buildBannerText: Unexpected recovery option ${selectedRecoveryOption}. Handle retry step copy explicitly.`
)
return 'UNEXPECTED RECOVERY OPTION'
}
diff --git a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx
index d759aaf3d78..ce754df9cfa 100644
--- a/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx
+++ b/app/src/organisms/ErrorRecoveryFlows/shared/__tests__/ErrorDetailsModal.test.tsx
@@ -14,6 +14,7 @@ import {
OverpressureBanner,
TipNotDetectedBanner,
GripperErrorBanner,
+ StallErrorBanner,
} from '../ErrorDetailsModal'
vi.mock('react-dom', () => ({
@@ -201,3 +202,25 @@ describe('GripperErrorBanner', () => {
)
})
})
+
+describe('StallErrorBanner', () => {
+ 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:
+ "A stall or collision is detected when the robot's motors are blocked",
+ message: 'The robot must return to its home position before proceeding',
+ }),
+ {}
+ )
+ })
+})
diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts
index e9b5722ffa8..fb3637c0eb5 100644
--- a/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/utils/__tests__/getErrorKind.test.ts
@@ -68,6 +68,21 @@ describe('getErrorKind', () => {
errorType: 'someHithertoUnknownDefinedErrorType',
expectedError: ERROR_KINDS.GENERAL_ERROR,
},
+ ...([
+ 'aspirate',
+ 'dispense',
+ 'blowOut',
+ 'moveToWell',
+ 'moveToAddressableArea',
+ 'dropTip',
+ 'pickUpTip',
+ 'prepareToAspirate',
+ ] as const).map(cmd => ({
+ commandType: cmd,
+ errorType: DEFINED_ERROR_TYPES.STALL_OR_COLLISION,
+ expectedError: ERROR_KINDS.STALL_OR_COLLISION,
+ isDefined: true,
+ })),
])(
'returns $expectedError for $commandType with errorType $errorType',
({ commandType, errorType, expectedError, isDefined = true }) => {
diff --git a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts
index 1dc5e023a6c..73fe862eb3b 100644
--- a/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts
+++ b/app/src/organisms/ErrorRecoveryFlows/utils/getErrorKind.ts
@@ -54,6 +54,8 @@ export function getErrorKind(
errorType === DEFINED_ERROR_TYPES.GRIPPER_MOVEMENT
) {
return ERROR_KINDS.GRIPPER_ERROR
+ } else if (errorType === DEFINED_ERROR_TYPES.STALL_OR_COLLISION) {
+ return ERROR_KINDS.STALL_OR_COLLISION
}
}