Skip to content

Commit 9a4c7fb

Browse files
authored
fix(app): move to initial calibration position after cancelling detach probe during desktop lpc (#18300)
Closes RQA-4177 On specifically the desktop app, it's possible to exit LPC on any view. During the detach view, we move the pipette to a maintenance position. If a user decides to cancel the detachment, we home the gantry and then return to whatever view the user previously visited. This is all well and good except in one particular case: while actively calibrating an offset. We should move the pipette back to the initial position instead of the home position. This isn't currently a breaking bug, because we have the "start over" button on the desktop app as a workaround, but doing the correct movement implicitly is much nicer UX. Although this seems like a decent amount of code, it's pretty straightforward: on specifically this calibrate offset view, we do the special homing behavior. We need a new command for this as well, because we don't want to reuse the existing command for moving to the initial position, since that one also does some labware loading.
1 parent bf4de79 commit 9a4c7fb

File tree

4 files changed

+198
-9
lines changed

4 files changed

+198
-9
lines changed

app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/__tests__/useHandleConfirmLwModulePlacement.test.ts

Lines changed: 59 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,34 @@ describe('useHandleConfirmLwModulePlacement', () => {
142142
expect(position).toEqual(mockPosition)
143143
})
144144

145+
it('should chain commands in the correct order when handleMoveToInitialOffsetPosition is called', async () => {
146+
const { result } = renderHook(() =>
147+
useHandleConfirmLwModulePlacement(mockProps)
148+
)
149+
150+
const position = await result.current.handleMoveToInitialOffsetPosition(
151+
mockOffsetLocationDetails,
152+
mockPipetteId,
153+
mockInitialVectorOffset
154+
)
155+
156+
expect(moveToWellCommands).toHaveBeenCalledWith(
157+
mockOffsetLocationDetails,
158+
mockPipetteId,
159+
mockInitialVectorOffset
160+
)
161+
expect(savePositionCommands).toHaveBeenCalledWith(mockPipetteId)
162+
163+
expect(mockChainLPCCommands).toHaveBeenCalled()
164+
const commandsArg = mockChainLPCCommands.mock.calls[0][0]
165+
166+
expect(commandsArg.length).toBe(2)
167+
expect(commandsArg[0].commandType).toBe('moveToWell')
168+
expect(commandsArg[1].commandType).toBe('savePosition')
169+
170+
expect(position).toEqual(mockPosition)
171+
})
172+
145173
it('should handle labware placement on deck when no module or labware beneath', async () => {
146174
const mockOffsetLocationDetailsNoPriorItem = {
147175
...mockOffsetLocationDetails,
@@ -173,7 +201,7 @@ describe('useHandleConfirmLwModulePlacement', () => {
173201
})
174202
})
175203

176-
it('should reject with error when final command response is incorrect', async () => {
204+
it('should reject with error when final command response is incorrect for handleConfirmLwModulePlacement', async () => {
177205
mockChainLPCCommands.mockResolvedValueOnce([
178206
{ data: { commandType: 'moveLabware' } },
179207
{ data: { commandType: 'moduleInitDuringLPC' } },
@@ -204,6 +232,36 @@ describe('useHandleConfirmLwModulePlacement', () => {
204232
)
205233
})
206234

235+
it('should reject with error when final command response is incorrect for handleMoveToInitialOffsetPosition', async () => {
236+
mockChainLPCCommands.mockResolvedValueOnce([
237+
{ data: { commandType: 'moveToWell' } },
238+
{
239+
data: {
240+
commandType: 'unknownCommand',
241+
result: null,
242+
},
243+
},
244+
])
245+
246+
const { result } = renderHook(() =>
247+
useHandleConfirmLwModulePlacement(mockProps)
248+
)
249+
250+
await expect(
251+
result.current.handleMoveToInitialOffsetPosition(
252+
mockOffsetLocationDetails,
253+
mockPipetteId,
254+
mockInitialVectorOffset
255+
)
256+
).rejects.toThrow(
257+
'CheckItem failed to save position for initial placement.'
258+
)
259+
260+
expect(mockSetErrorMessage).toHaveBeenCalledWith(
261+
'CheckItem failed to save position for initial placement.'
262+
)
263+
})
264+
207265
it('should reject with error when result is null', async () => {
208266
mockChainLPCCommands.mockResolvedValueOnce([
209267
{ data: { commandType: 'moveLabware' } },

app/src/organisms/LabwarePositionCheck/hooks/useLPCCommands/useHandleConfirmLwModulePlacement.ts

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ export interface UseHandleConfirmPlacementResult {
2727
pipetteId: string,
2828
initialVectorOffset?: VectorOffset | null
2929
) => Promise<Coordinates>
30+
handleMoveToInitialOffsetPosition: (
31+
offsetLocationDetails: OffsetLocationDetails,
32+
pipetteId: string,
33+
initialVectorOffset: VectorOffset | null
34+
) => Promise<Coordinates>
3035
}
3136

3237
export function useHandleConfirmLwModulePlacement({
@@ -70,7 +75,41 @@ export function useHandleConfirmLwModulePlacement({
7075
})
7176
}
7277

73-
return { handleConfirmLwModulePlacement }
78+
const handleMoveToInitialOffsetPosition = (
79+
offsetLocationDetails: OffsetLocationDetails,
80+
pipetteId: string,
81+
initialVectorOffset: VectorOffset | null
82+
): Promise<Coordinates> => {
83+
const moveCommands: CreateCommand[] = [
84+
...moveToWellCommands(
85+
offsetLocationDetails,
86+
pipetteId,
87+
initialVectorOffset
88+
),
89+
...savePositionCommands(pipetteId),
90+
]
91+
92+
return chainLPCCommands(moveCommands, false).then(responses => {
93+
const finalResponse = responses[responses.length - 1]
94+
if (
95+
finalResponse.data.commandType === 'savePosition' &&
96+
finalResponse.data.result != null
97+
) {
98+
const { position } = finalResponse.data.result
99+
100+
return Promise.resolve(position)
101+
} else {
102+
setErrorMessage(
103+
'CheckItem failed to save position for initial placement.'
104+
)
105+
return Promise.reject(
106+
new Error('CheckItem failed to save position for initial placement.')
107+
)
108+
}
109+
})
110+
}
111+
112+
return { handleConfirmLwModulePlacement, handleMoveToInitialOffsetPosition }
74113
}
75114

76115
function buildMoveLabwareCommand(

app/src/organisms/LabwarePositionCheck/steps/DetachProbe.tsx

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,27 +5,69 @@ import { useSelector } from 'react-redux'
55
import { LegacyStyledText, StyledText } from '@opentrons/components'
66

77
import { LPCContentContainer } from '/app/organisms/LabwarePositionCheck/LPCContentContainer'
8-
import { selectActivePipetteChannelCount } from '/app/redux/protocol-runs'
8+
import {
9+
selectActivePipette,
10+
selectActivePipetteChannelCount,
11+
selectCurrentSubstep,
12+
selectSelectedLwOverview,
13+
selectSelectedLwWithOffsetDetailsMostRecentVectorOffset,
14+
} from '/app/redux/protocol-runs'
915
import { DescriptionContent, TwoColumn } from '/app/molecules/InterventionModal'
1016

17+
import type { LoadedPipette } from '@opentrons/shared-data'
18+
import type {
19+
OffsetLocationDetails,
20+
SelectedLwOverview,
21+
} from '/app/redux/protocol-runs'
1122
import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types'
1223

1324
import detachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm'
1425
import detachProbe8 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_8.webm'
1526
import detachProbe96 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_96.webm'
1627

1728
export function DetachProbe(props: LPCWizardContentProps): JSX.Element {
18-
const { proceedStep, goBackLastStep, commandUtils } = props
29+
const { proceedStep, goBackLastStep, commandUtils, runId } = props
1930
const { t } = useTranslation('labware_position_check')
31+
const {
32+
toggleRobotMoving,
33+
handleMoveToInitialOffsetPosition,
34+
home,
35+
} = commandUtils
36+
37+
const currentSubstep = useSelector(selectCurrentSubstep(runId))
38+
const pipette = useSelector(selectActivePipette(runId)) as LoadedPipette
39+
const pipetteId = pipette.id
40+
const selectedLwInfo = useSelector(
41+
selectSelectedLwOverview(runId)
42+
) as SelectedLwOverview
43+
const mostRecentVectorOffset = useSelector(
44+
selectSelectedLwWithOffsetDetailsMostRecentVectorOffset(runId)
45+
)
46+
const offsetLocationDetails = selectedLwInfo.offsetLocationDetails as OffsetLocationDetails
2047

2148
const handleGoBack = (): void => {
22-
void commandUtils
23-
.toggleRobotMoving(true)
24-
.then(() => commandUtils.home())
49+
void toggleRobotMoving(true)
50+
.then(() => {
51+
// On the desktop app, ensure the robot returns to the initial offset position instead of the home position
52+
// when actively calibrating an offset.
53+
if (currentSubstep === 'handle-lw/edit-offset/check-labware') {
54+
return home()
55+
.then(() =>
56+
handleMoveToInitialOffsetPosition(
57+
offsetLocationDetails,
58+
pipetteId,
59+
mostRecentVectorOffset
60+
)
61+
)
62+
.then(() => Promise.resolve())
63+
} else {
64+
return home()
65+
}
66+
})
2567
.then(() => {
2668
goBackLastStep()
2769
})
28-
.then(() => commandUtils.toggleRobotMoving(false))
70+
.then(() => toggleRobotMoving(false))
2971
}
3072

3173
const channelCount = useSelector(selectActivePipetteChannelCount(props.runId))

app/src/organisms/LabwarePositionCheck/steps/__tests__/DetachProbe.test.tsx

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ import { DetachProbe } from '/app/organisms/LabwarePositionCheck/steps'
99
import {
1010
selectStepInfo,
1111
selectActivePipetteChannelCount,
12+
selectActivePipette,
13+
selectCurrentSubstep,
14+
selectSelectedLwOverview,
15+
selectSelectedLwWithOffsetDetailsMostRecentVectorOffset,
1216
} from '/app/redux/protocol-runs'
1317

1418
import detachProbe1 from '/app/assets/videos/pipette-wizard-flows/Pipette_Detach_Probe_1.webm'
@@ -26,7 +30,8 @@ vi.mock('/app/redux/protocol-runs')
2630

2731
const render = (
2832
props: ComponentProps<typeof DetachProbe>,
29-
channelCount = 1
33+
channelCount = 1,
34+
currentSubstep = 'default-substep'
3035
) => {
3136
const mockState = {
3237
[props.runId]: {
@@ -36,8 +41,21 @@ const render = (
3641
protocolName: 'MOCK_PROTOCOL',
3742
},
3843
activePipette: {
44+
id: 'mock-pipette-id',
3945
channelCount: channelCount,
4046
},
47+
currentSubstep: currentSubstep,
48+
selectedLwOverview: {
49+
offsetLocationDetails: {
50+
labwareId: 'mock-labware-id',
51+
well: 'A1',
52+
},
53+
},
54+
selectedLwWithOffsetDetailsMostRecentVectorOffset: {
55+
x: 1,
56+
y: 2,
57+
z: 3,
58+
},
4159
},
4260
}
4361

@@ -50,9 +68,17 @@ const render = (
5068
describe('DetachProbe', () => {
5169
let props: ComponentProps<typeof DetachProbe>
5270
let mockHandleProceed: Mock
71+
let mockToggleRobotMoving: Mock
72+
let mockHome: Mock
73+
let mockHandleMoveToInitialOffsetPosition: Mock
74+
let mockGoBackLastStep: Mock
5375

5476
beforeEach(() => {
5577
mockHandleProceed = vi.fn()
78+
mockToggleRobotMoving = vi.fn().mockResolvedValue(undefined)
79+
mockHome = vi.fn().mockResolvedValue(undefined)
80+
mockHandleMoveToInitialOffsetPosition = vi.fn().mockResolvedValue(undefined)
81+
mockGoBackLastStep = vi.fn()
5682

5783
vi.mocked(
5884
selectStepInfo
@@ -62,6 +88,26 @@ describe('DetachProbe', () => {
6288
).mockImplementation((runId: string) => (state: any) =>
6389
state[runId]?.activePipette?.channelCount || 1
6490
)
91+
vi.mocked(
92+
selectActivePipette
93+
).mockImplementation((runId: string) => (state: any) =>
94+
state[runId]?.activePipette
95+
)
96+
vi.mocked(
97+
selectCurrentSubstep
98+
).mockImplementation((runId: string) => (state: any) =>
99+
state[runId]?.currentSubstep
100+
)
101+
vi.mocked(
102+
selectSelectedLwOverview
103+
).mockImplementation((runId: string) => (state: any) =>
104+
state[runId]?.selectedLwOverview
105+
)
106+
vi.mocked(
107+
selectSelectedLwWithOffsetDetailsMostRecentVectorOffset
108+
).mockImplementation((runId: string) => (state: any) =>
109+
state[runId]?.selectedLwWithOffsetDetailsMostRecentVectorOffset
110+
)
65111

66112
props = {
67113
...mockLPCContentProps,
@@ -71,7 +117,11 @@ describe('DetachProbe', () => {
71117
...mockLPCContentProps.commandUtils.headerCommands,
72118
handleProceed: mockHandleProceed,
73119
},
120+
toggleRobotMoving: mockToggleRobotMoving,
121+
home: mockHome,
122+
handleMoveToInitialOffsetPosition: mockHandleMoveToInitialOffsetPosition,
74123
},
124+
goBackLastStep: mockGoBackLastStep,
75125
}
76126
})
77127

0 commit comments

Comments
 (0)