Skip to content

Commit 6031ab8

Browse files
authored
feat(app): update touch tip screen (#18491)
* feat(app): update touch tip screen
1 parent 0e1ba8f commit 6031ab8

File tree

10 files changed

+310
-22
lines changed

10 files changed

+310
-22
lines changed

app/src/assets/localization/en/quick_transfer.json

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
"aspirate_volume_µL": "Aspirate volume per well (µL)",
2323
"aspirate_volume": "Aspirate volume per well",
2424
"aspirate": "Aspirate",
25+
"touch_tip_description_aspirating": "Touch tip to each side of the well after aspirating",
26+
"touch_tip_description_dispensing": "Touch tip to each side of the well after dispensing",
2527
"attach_pipette": "Attach pipette",
2628
"blow_out_after_dispensing": "Blowout after dispensing",
2729
"blow_out_description": "Blow extra air through the tip",
@@ -168,9 +170,9 @@
168170
"too_many_pins_body": "Remove a quick transfer in order to add more transfers to your pinned list.",
169171
"too_many_pins_header": "You've hit your max!",
170172
"touch_tip_after_aspirating": "Touch tip after aspirating",
171-
"touch_tip_before_dispensing": "Touch tip before dispensing",
173+
"touch_tip_after_dispensing": "Touch tip after dispensing",
172174
"touch_tip_position_mm": "Touch tip position from top of well (mm)",
173-
"touch_tip_value": "{{position}} mm from bottom",
175+
"touch_tip_value": "{{speed}}mm/s, {{position}} mm from bottom",
174176
"touch_tip": "Touch tip",
175177
"transfer_analysis_failed": "quick transfer analysis failed",
176178
"transfer_name": "Transfer Name",

app/src/organisms/ODD/QuickTransferFlow/Aspirate/hooks/useAspirateSettingsConfig.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,10 @@ export function useAspirateSettingsConfig({
134134
copy: t('touch_tip'),
135135
value:
136136
state.touchTipAspirate !== undefined
137-
? t('touch_tip_value', { position: state.touchTipAspirate })
137+
? t('touch_tip_value', {
138+
speed: state.touchTipAspirateSpeed,
139+
position: state.touchTipAspirate,
140+
})
138141
: '',
139142
enabled: !sourceIsReservoir,
140143
onClick: () => {

app/src/organisms/ODD/QuickTransferFlow/Dispense/hooks/useDispenseSettingsConfig.ts

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,21 @@ export function useDispenseSettingsConfig({
164164
}
165165
},
166166
},
167+
{
168+
option: 'dispense_touch_tip',
169+
copy: t('touch_tip'),
170+
value:
171+
state.touchTipDispense !== undefined
172+
? t('touch_tip_value', {
173+
speed: state.touchTipDispenseSpeed,
174+
position: state.touchTipDispense,
175+
})
176+
: '',
177+
enabled: true,
178+
onClick: () => {
179+
setSelectedSetting('dispense_touch_tip')
180+
},
181+
},
167182
{
168183
option: 'dispense_air_gap',
169184
copy: t('air_gap'),

app/src/organisms/ODD/QuickTransferFlow/QuickTransferAdvancedSettings/TouchTip.tsx

Lines changed: 80 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
POSITION_FIXED,
1212
RadioButton,
1313
SPACING,
14+
StyledText,
1415
} from '@opentrons/components'
1516

1617
import { getTopPortalEl } from '/app/App/portal'
@@ -47,6 +48,9 @@ export function TouchTip(props: TouchTipProps): JSX.Element {
4748
? state.touchTipAspirate != null
4849
: state.touchTipDispense != null
4950
)
51+
const initialSpeed =
52+
kind === 'aspirate' ? state.touchTipAspirate : state.touchTipDispense
53+
const [speed, setSpeed] = useState<number | null>(initialSpeed ?? null)
5054
const [currentStep, setCurrentStep] = useState<number>(1)
5155
const touchTipAspirate =
5256
state.touchTipAspirate != null ? state.touchTipAspirate.toString() : null
@@ -97,9 +101,14 @@ export function TouchTip(props: TouchTipProps): JSX.Element {
97101
setCurrentStep(2)
98102
}
99103
} else if (currentStep === 2) {
104+
setCurrentStep(3)
105+
} else if (currentStep === 3) {
100106
dispatch({
101107
type: touchTipAction,
102108
position: position != null ? parseInt(position) : undefined,
109+
[kind === 'aspirate'
110+
? 'touchTipAspirateSpeed'
111+
: 'touchTipDispenseSpeed']: speed,
103112
})
104113
trackEventWithRobotSerial({
105114
name: ANALYTICS_QUICK_TRANSFER_SETTING_SAVED,
@@ -112,7 +121,7 @@ export function TouchTip(props: TouchTipProps): JSX.Element {
112121
}
113122

114123
const setSaveOrContinueButtonText =
115-
touchTipIsEnabled && currentStep < 2
124+
touchTipIsEnabled && currentStep < 3
116125
? t('shared:continue')
117126
: t('shared:save')
118127

@@ -151,16 +160,27 @@ export function TouchTip(props: TouchTipProps): JSX.Element {
151160

152161
let buttonIsDisabled = false
153162
if (currentStep === 2) {
163+
buttonIsDisabled = speed == null
164+
}
165+
if (currentStep === 3) {
154166
buttonIsDisabled = position == null || positionError != null
155167
}
156168

169+
const handleSpeedChange = (userInput: string): void => {
170+
if (userInput === '') {
171+
setSpeed(null)
172+
}
173+
const parsedSpeed = parseInt(userInput)
174+
setSpeed(!isNaN(parsedSpeed) ? parsedSpeed : null)
175+
}
176+
157177
return createPortal(
158178
<Flex position={POSITION_FIXED} backgroundColor={COLORS.white} width="100%">
159179
<ChildNavigation
160180
header={
161181
kind === 'aspirate'
162182
? t('touch_tip_after_aspirating')
163-
: t('touch_tip_before_dispensing')
183+
: t('touch_tip_after_dispensing')
164184
}
165185
buttonText={i18n.format(setSaveOrContinueButtonText, 'capitalize')}
166186
onClickBack={handleClickBackOrExit}
@@ -173,22 +193,69 @@ export function TouchTip(props: TouchTipProps): JSX.Element {
173193
marginTop={SPACING.spacing120}
174194
flexDirection={DIRECTION_COLUMN}
175195
padding={`${SPACING.spacing16} ${SPACING.spacing60} ${SPACING.spacing40} ${SPACING.spacing60}`}
176-
gridGap={SPACING.spacing4}
196+
gridGap={SPACING.spacing24}
177197
width="100%"
178198
>
179-
{enableTouchTipDisplayItems.map(displayItem => (
180-
<RadioButton
181-
key={displayItem.description}
182-
isSelected={touchTipIsEnabled === displayItem.option}
183-
onChange={displayItem.onClick}
184-
buttonValue={displayItem.description}
185-
buttonLabel={displayItem.description}
186-
radioButtonType="large"
187-
/>
188-
))}
199+
<StyledText oddStyle="level4HeaderRegular">
200+
{kind === 'aspirate'
201+
? t('touch_tip_description_aspirating')
202+
: t('touch_tip_description_dispensing')}
203+
</StyledText>
204+
<Flex flexDirection={DIRECTION_COLUMN} gridGap={SPACING.spacing8}>
205+
{enableTouchTipDisplayItems.map(displayItem => (
206+
<RadioButton
207+
key={displayItem.description}
208+
isSelected={touchTipIsEnabled === displayItem.option}
209+
onChange={displayItem.onClick}
210+
buttonValue={displayItem.description}
211+
buttonLabel={displayItem.description}
212+
radioButtonType="large"
213+
/>
214+
))}
215+
</Flex>
189216
</Flex>
190217
) : null}
191218
{currentStep === 2 ? (
219+
<Flex
220+
alignSelf={ALIGN_CENTER}
221+
gridGap={SPACING.spacing48}
222+
paddingX={SPACING.spacing40}
223+
padding={`${SPACING.spacing16} ${SPACING.spacing40} ${SPACING.spacing40}`}
224+
marginTop="7.75rem" // using margin rather than justify due to content moving with error message
225+
alignItems={ALIGN_CENTER}
226+
height="22rem"
227+
>
228+
<Flex
229+
width="30.5rem"
230+
height="100%"
231+
gridGap={SPACING.spacing24}
232+
flexDirection={DIRECTION_COLUMN}
233+
marginTop={SPACING.spacing68}
234+
>
235+
<InputField
236+
type="text"
237+
value={String(speed ?? '')}
238+
title={t('speed')}
239+
readOnly
240+
/>
241+
</Flex>
242+
<Flex
243+
paddingX={SPACING.spacing24}
244+
height="21.25rem"
245+
marginTop="7.75rem"
246+
borderRadius="0"
247+
>
248+
<NumericalKeyboard
249+
keyboardRef={keyboardRef}
250+
initialValue={String(speed ?? '')}
251+
onChange={e => {
252+
handleSpeedChange(e)
253+
}}
254+
/>
255+
</Flex>
256+
</Flex>
257+
) : null}
258+
{currentStep === 3 ? (
192259
<Flex
193260
alignSelf={ALIGN_CENTER}
194261
gridGap={SPACING.spacing48}
Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,180 @@
1+
import { fireEvent, screen } from '@testing-library/react'
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
3+
4+
import { renderWithProviders } from '/app/__testing-utils__'
5+
import { i18n } from '/app/i18n'
6+
import { useTrackEventWithRobotSerial } from '/app/redux-resources/analytics'
7+
8+
import QuickTransferState from '../__fixtures__/QuickTransferState.json'
9+
import { TouchTip } from '../TouchTip'
10+
11+
import type { ComponentProps } from 'react'
12+
13+
vi.mock('/app/redux-resources/analytics')
14+
15+
const render = (props: ComponentProps<typeof TouchTip>) => {
16+
return renderWithProviders(<TouchTip {...props} />, {
17+
i18nInstance: i18n,
18+
})
19+
}
20+
let mockTrackEventWithRobotSerial: any
21+
22+
describe('TouchTip', () => {
23+
let props: ComponentProps<typeof TouchTip>
24+
25+
beforeEach(() => {
26+
props = {
27+
onBack: vi.fn(),
28+
state: QuickTransferState as any,
29+
dispatch: vi.fn(),
30+
kind: 'aspirate',
31+
}
32+
mockTrackEventWithRobotSerial = vi.fn(
33+
() => new Promise(resolve => resolve({}))
34+
)
35+
vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({
36+
trackEventWithRobotSerial: mockTrackEventWithRobotSerial,
37+
})
38+
})
39+
40+
afterEach(() => {
41+
vi.resetAllMocks()
42+
})
43+
44+
it('renders text, buttons for touch tip aspirate', () => {
45+
render(props)
46+
screen.getByText('Touch tip after aspirating')
47+
screen.getByText('Save')
48+
screen.getByText('Touch tip to each side of the well after aspirating')
49+
screen.getByText('Enabled')
50+
screen.getByText('Disabled')
51+
fireEvent.click(screen.getByText('Enabled'))
52+
screen.getByText('Continue')
53+
})
54+
55+
it('renders text, buttons for touch tip dispense', () => {
56+
props.kind = 'dispense'
57+
render(props)
58+
screen.getByText('Touch tip after dispensing')
59+
screen.getByText('Save')
60+
screen.getByText('Touch tip to each side of the well after dispensing')
61+
screen.getByText('Enabled')
62+
screen.getByText('Disabled')
63+
fireEvent.click(screen.getByText('Enabled'))
64+
screen.getByText('Continue')
65+
})
66+
67+
it('renders text, buttons for touch tip speed - aspirate', () => {
68+
render(props)
69+
fireEvent.click(screen.getByText('Enabled'))
70+
fireEvent.click(screen.getByText('Continue'))
71+
screen.getByText('Speed (mm/second)')
72+
screen.getByRole('button', { name: '1' })
73+
screen.getByRole('button', { name: '5' })
74+
screen.getByRole('button', { name: '9' })
75+
screen.getByRole('button', { name: 'del' })
76+
screen.getByText('Continue')
77+
})
78+
79+
it('renders text, buttons, input field, and keyboard for touch tip - aspirate', () => {
80+
render(props)
81+
fireEvent.click(screen.getByText('Enabled'))
82+
fireEvent.click(screen.getByText('Continue'))
83+
screen.getByText('Speed (mm/second)')
84+
screen.getByRole('button', { name: '1' })
85+
screen.getByRole('button', { name: '5' })
86+
screen.getByRole('button', { name: '9' })
87+
screen.getByRole('button', { name: 'del' })
88+
fireEvent.click(screen.getByRole('button', { name: '1' }))
89+
fireEvent.click(screen.getByRole('button', { name: '1' }))
90+
})
91+
92+
it('renders text, buttons, input field, and keyboard for touch tip position- aspirate', () => {
93+
render(props)
94+
fireEvent.click(screen.getByText('Enabled'))
95+
fireEvent.click(screen.getByText('Continue'))
96+
screen.getByText('Speed (mm/second)')
97+
screen.getByRole('button', { name: '1' })
98+
screen.getByRole('button', { name: '5' })
99+
screen.getByRole('button', { name: '9' })
100+
screen.getByRole('button', { name: 'del' })
101+
fireEvent.click(screen.getByRole('button', { name: '1' }))
102+
fireEvent.click(screen.getByRole('button', { name: '1' }))
103+
fireEvent.click(screen.getByText('Continue'))
104+
screen.getByText('Touch tip position from top of well (mm)')
105+
screen.getByRole('button', { name: '1' })
106+
screen.getByRole('button', { name: '5' })
107+
screen.getByRole('button', { name: '9' })
108+
screen.getByRole('button', { name: 'del' })
109+
fireEvent.click(screen.getByRole('button', { name: '0' }))
110+
})
111+
112+
it('should call dispatch when clicking save button - aspirate', () => {
113+
render(props)
114+
fireEvent.click(screen.getByText('Enabled'))
115+
fireEvent.click(screen.getByText('Continue'))
116+
screen.getByText('Speed (mm/second)')
117+
screen.getByRole('button', { name: '1' })
118+
screen.getByRole('button', { name: '5' })
119+
screen.getByRole('button', { name: '9' })
120+
screen.getByRole('button', { name: 'del' })
121+
fireEvent.click(screen.getByRole('button', { name: '1' }))
122+
fireEvent.click(screen.getByText('Continue'))
123+
screen.getByText('Touch tip position from top of well (mm)')
124+
screen.getByRole('button', { name: '1' })
125+
screen.getByRole('button', { name: '5' })
126+
screen.getByRole('button', { name: '9' })
127+
screen.getByRole('button', { name: 'del' })
128+
fireEvent.click(screen.getByRole('button', { name: '0' }))
129+
fireEvent.click(screen.getByText('Save'))
130+
expect(props.dispatch).toHaveBeenCalledWith({
131+
type: 'SET_TOUCH_TIP_ASPIRATE',
132+
position: 0,
133+
touchTipAspirateSpeed: 1,
134+
})
135+
expect(mockTrackEventWithRobotSerial).toHaveBeenCalledWith({
136+
name: 'quickTransferSettingSaved',
137+
properties: {
138+
setting: 'TouchTip_aspirate',
139+
},
140+
})
141+
})
142+
143+
it('should call dispatch when clicking save button - dispense', () => {
144+
props.kind = 'dispense'
145+
render(props)
146+
fireEvent.click(screen.getByText('Enabled'))
147+
fireEvent.click(screen.getByText('Continue'))
148+
screen.getByText('Speed (mm/second)')
149+
screen.getByRole('button', { name: '1' })
150+
screen.getByRole('button', { name: '5' })
151+
screen.getByRole('button', { name: '9' })
152+
screen.getByRole('button', { name: 'del' })
153+
fireEvent.click(screen.getByRole('button', { name: '1' }))
154+
fireEvent.click(screen.getByText('Continue'))
155+
screen.getByText('Touch tip position from top of well (mm)')
156+
screen.getByRole('button', { name: '1' })
157+
screen.getByRole('button', { name: '5' })
158+
screen.getByRole('button', { name: '9' })
159+
screen.getByRole('button', { name: 'del' })
160+
fireEvent.click(screen.getByRole('button', { name: '0' }))
161+
fireEvent.click(screen.getByText('Save'))
162+
expect(props.dispatch).toHaveBeenCalledWith({
163+
type: 'SET_TOUCH_TIP_DISPENSE',
164+
position: 0,
165+
touchTipDispenseSpeed: 1,
166+
})
167+
expect(mockTrackEventWithRobotSerial).toHaveBeenCalledWith({
168+
name: 'quickTransferSettingSaved',
169+
properties: {
170+
setting: 'TouchTip_dispense',
171+
},
172+
})
173+
})
174+
175+
it('should call mock function when clicking back button', () => {
176+
render(props)
177+
fireEvent.click(screen.getByTestId('ChildNavigation_Back_Button'))
178+
expect(props.onBack).toHaveBeenCalled()
179+
})
180+
})

app/src/organisms/ODD/QuickTransferFlow/__tests__/Dispense/Dispense.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ describe('Dispense', () => {
9393

9494
it('renders mock components and reset button', () => {
9595
render(props)
96-
expect(screen.getAllByText('mock DispenseSettingItem').length).toBe(9)
96+
expect(screen.getAllByText('mock DispenseSettingItem').length).toBe(10)
9797
screen.getByText('mock DispenseSettingDetail')
9898
screen.getByRole('button', { name: 'Reset dispense settings' })
9999
})

0 commit comments

Comments
 (0)