Skip to content

Commit 14228ac

Browse files
authored
fix(app): connect pipette settings form's submit button (#11288)
1 parent 75df461 commit 14228ac

File tree

9 files changed

+304
-35
lines changed

9 files changed

+304
-35
lines changed

app/src/organisms/ConfigurePipette/ConfigForm.tsx

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ export interface ConfigFormProps {
4040
settings: PipetteSettingsFieldsMap
4141
updateInProgress: boolean
4242
updateSettings: (fields: PipetteSettingsFieldsUpdate) => unknown
43-
closeModal: () => unknown
4443
groupLabels: string[]
44+
formId: string
4545
__showHiddenFields: boolean
4646
}
4747

@@ -184,7 +184,7 @@ export class ConfigForm extends React.Component<ConfigFormProps> {
184184
}
185185

186186
render(): JSX.Element {
187-
const { updateInProgress } = this.props
187+
const { updateInProgress, formId } = this.props
188188
const fields = this.getVisibleFields()
189189
const UNKNOWN_KEYS = this.getUnknownKeys()
190190
const plungerFields = this.getFieldsByKey(PLUNGER_KEYS, fields)
@@ -217,7 +217,7 @@ export class ConfigForm extends React.Component<ConfigFormProps> {
217217
}
218218
return (
219219
<Box overflowY="scroll">
220-
<Form>
220+
<Form id={formId}>
221221
<ConfigFormResetButton
222222
onClick={handleReset}
223223
disabled={updateInProgress}

app/src/organisms/ConfigurePipette/ConfigFormSubmitButton.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,13 @@ import {
99
import { PrimaryButton } from '../../atoms/buttons'
1010
export interface ConfigFormSubmitButtonProps {
1111
disabled: boolean
12+
formId: string
1213
}
1314

1415
export function ConfigFormSubmitButton(
1516
props: ConfigFormSubmitButtonProps
1617
): JSX.Element {
17-
const { disabled } = props
18+
const { disabled, formId } = props
1819
const { t } = useTranslation('shared')
1920

2021
return (
@@ -24,7 +25,7 @@ export function ConfigFormSubmitButton(
2425
textTransform={TEXT_TRANSFORM_UPPERCASE}
2526
boxShadow={'0px -4px 12px rgba(0, 0, 0, 0.15)'}
2627
>
27-
<PrimaryButton type={'submit'} disabled={disabled}>
28+
<PrimaryButton type="submit" form={formId} disabled={disabled}>
2829
{t('confirm')}
2930
</PrimaryButton>
3031
</Flex>

app/src/organisms/ConfigurePipette/__tests__/ConfigFormSubmitButton.test.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ describe('ConfigFormSubmitButton', () => {
1414
beforeEach(() => {
1515
props = {
1616
disabled: false,
17+
formId: 'id',
1718
}
1819
})
1920
afterEach(() => {
@@ -27,6 +28,7 @@ describe('ConfigFormSubmitButton', () => {
2728
it('renders bottom button text and disabled', () => {
2829
props = {
2930
disabled: true,
31+
formId: 'id',
3032
}
3133
const { getByRole } = render(props)
3234
const button = getByRole('button', { name: 'Confirm' })
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import * as React from 'react'
2+
import { when, resetAllWhenMocks } from 'jest-when'
3+
import { renderWithProviders } from '@opentrons/components'
4+
import { i18n } from '../../../i18n'
5+
import * as RobotApi from '../../../redux/robot-api'
6+
import { ConfigurePipette } from '../../ConfigurePipette'
7+
import { getAttachedPipetteSettingsFieldsById } from '../../../redux/pipettes'
8+
import { mockPipetteSettingsFieldsMap } from '../../../redux/pipettes/__fixtures__'
9+
import { getConfig } from '../../../redux/config'
10+
11+
import type { DispatchApiRequestType } from '../../../redux/robot-api'
12+
import type { State } from '../../../redux/types'
13+
14+
jest.mock('../../../redux/robot-api')
15+
jest.mock('../../../redux/config')
16+
jest.mock('../../../redux/pipettes')
17+
18+
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>
19+
const mockUseDispatchApiRequest = RobotApi.useDispatchApiRequest as jest.MockedFunction<
20+
typeof RobotApi.useDispatchApiRequest
21+
>
22+
const mockGetRequestById = RobotApi.getRequestById as jest.MockedFunction<
23+
typeof RobotApi.getRequestById
24+
>
25+
const mockGetAttachedPipetteSettingsFieldsById = getAttachedPipetteSettingsFieldsById as jest.MockedFunction<
26+
typeof getAttachedPipetteSettingsFieldsById
27+
>
28+
29+
const render = (props: React.ComponentProps<typeof ConfigurePipette>) => {
30+
return renderWithProviders(<ConfigurePipette {...props} />, {
31+
i18nInstance: i18n,
32+
})[0]
33+
}
34+
35+
const mockRobotName = 'mockRobotName'
36+
37+
describe('ConfigurePipette', () => {
38+
let dispatchApiRequest: DispatchApiRequestType
39+
let props: React.ComponentProps<typeof ConfigurePipette>
40+
41+
beforeEach(() => {
42+
props = {
43+
pipetteId: 'id',
44+
robotName: mockRobotName,
45+
updateRequest: { status: 'pending' },
46+
updateSettings: jest.fn(),
47+
closeModal: jest.fn(),
48+
formId: 'id',
49+
}
50+
when(mockGetRequestById)
51+
.calledWith((undefined as any) as State, 'id')
52+
.mockReturnValue({
53+
status: RobotApi.SUCCESS,
54+
response: {
55+
method: 'POST',
56+
ok: true,
57+
path: '/',
58+
status: 200,
59+
},
60+
})
61+
mockGetConfig.mockReturnValue({} as any)
62+
when(mockGetAttachedPipetteSettingsFieldsById)
63+
.calledWith((undefined as any) as State, mockRobotName, 'id')
64+
.mockReturnValue(mockPipetteSettingsFieldsMap)
65+
dispatchApiRequest = jest.fn()
66+
when(mockUseDispatchApiRequest)
67+
.calledWith()
68+
.mockReturnValue([dispatchApiRequest, ['id']])
69+
})
70+
afterEach(() => {
71+
jest.resetAllMocks()
72+
resetAllWhenMocks()
73+
})
74+
75+
it('renders correct number of text boxes given the pipette settings data supplied by getAttachedPipetteSettingsFieldsById', () => {
76+
const { getAllByRole } = render(props)
77+
78+
const inputs = getAllByRole('textbox')
79+
expect(inputs.length).toBe(9)
80+
})
81+
})

app/src/organisms/ConfigurePipette/index.tsx

Lines changed: 20 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as React from 'react'
2+
import { useSelector } from 'react-redux'
23
import { useTranslation } from 'react-i18next'
34
import { Box } from '@opentrons/components'
4-
import { usePipetteSettingsQuery } from '@opentrons/react-api-client'
55
import { SUCCESS, FAILURE, PENDING } from '../../redux/robot-api'
66
import { useFeatureFlag } from '../../redux/config'
77
import { ConfigForm } from './ConfigForm'
@@ -10,22 +10,33 @@ import type {
1010
AttachedPipette,
1111
PipetteSettingsFieldsUpdate,
1212
} from '../../redux/pipettes/types'
13+
import { getAttachedPipetteSettingsFieldsById } from '../../redux/pipettes'
1314
import type { RequestState } from '../../redux/robot-api/types'
15+
import type { State } from '../../redux/types'
1416

15-
const PIPETTE_SETTINGS_POLL_MS = 5000
1617
interface Props {
17-
closeModal: () => unknown
18+
closeModal: () => void
1819
pipetteId: AttachedPipette['id']
1920
updateRequest: RequestState | null
2021
updateSettings: (fields: PipetteSettingsFieldsUpdate) => void
22+
robotName: string
23+
formId: string
2124
}
2225

2326
export function ConfigurePipette(props: Props): JSX.Element {
24-
const { closeModal, pipetteId, updateRequest, updateSettings } = props
27+
const {
28+
closeModal,
29+
pipetteId,
30+
updateRequest,
31+
updateSettings,
32+
robotName,
33+
formId,
34+
} = props
2535
const { t } = useTranslation('device_details')
26-
const settings = usePipetteSettingsQuery({
27-
refetchInterval: PIPETTE_SETTINGS_POLL_MS,
28-
})?.data
36+
37+
const settings = useSelector((state: State) =>
38+
getAttachedPipetteSettingsFieldsById(state, robotName, pipetteId)
39+
)
2940
const groupLabels = [
3041
t('plunger_positions'),
3142
t('tip_pickup_drop'),
@@ -54,12 +65,11 @@ export function ConfigurePipette(props: Props): JSX.Element {
5465
{updateError && <ConfigErrorBanner message={updateError} />}
5566
{settings != null && pipetteId != null && (
5667
<ConfigForm
57-
// @ts-expect-error: pipetteId and settings should not be undefined
58-
settings={settings[pipetteId].fields}
68+
settings={settings}
5969
updateInProgress={updateRequest?.status === PENDING}
6070
updateSettings={updateSettings}
61-
closeModal={closeModal}
6271
groupLabels={groupLabels}
72+
formId={formId}
6373
__showHiddenFields={__showHiddenFields}
6474
/>
6575
)}

app/src/organisms/Devices/PipetteCard/PipetteSettingsSlideout.tsx

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ const FETCH_PIPETTES_INTERVAL_MS = 5000
2828
interface PipetteSettingsSlideoutProps {
2929
robotName: string
3030
pipetteName: PipetteModelSpecs['displayName']
31-
onCloseClick: () => unknown
31+
onCloseClick: () => void
3232
isExpanded: boolean
3333
pipetteId: AttachedPipette['id']
3434
}
@@ -47,6 +47,7 @@ export const PipetteSettingsSlideout = (
4747
const updateRequest = useSelector((state: State) =>
4848
latestRequestId != null ? getRequestById(state, latestRequestId) : null
4949
)
50+
const FORM_ID = `configurePipetteForm_${pipetteId}`
5051

5152
useInterval(
5253
() => {
@@ -62,7 +63,10 @@ export const PipetteSettingsSlideout = (
6263
onCloseClick={onCloseClick}
6364
isExpanded={isExpanded}
6465
footer={
65-
<ConfigFormSubmitButton disabled={updateRequest?.status === PENDING} />
66+
<ConfigFormSubmitButton
67+
disabled={updateRequest?.status === PENDING}
68+
formId={FORM_ID}
69+
/>
6670
}
6771
>
6872
<Flex data-testid={`PipetteSettingsSlideout_${robotName}_${pipetteId}`}>
@@ -71,6 +75,8 @@ export const PipetteSettingsSlideout = (
7175
pipetteId={pipetteId}
7276
updateRequest={updateRequest}
7377
updateSettings={updateSettings}
78+
robotName={robotName}
79+
formId={FORM_ID}
7480
/>
7581
</Flex>
7682
</Slideout>

app/src/organisms/Devices/PipetteCard/__tests__/PipetteSettingsSlideout.test.tsx

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,42 @@
11
import * as React from 'react'
2-
import { resetAllWhenMocks } from 'jest-when'
3-
import { renderWithProviders } from '@opentrons/components'
2+
import { resetAllWhenMocks, when } from 'jest-when'
3+
import { waitFor } from '@testing-library/dom'
44
import { fireEvent } from '@testing-library/react'
5+
import { renderWithProviders } from '@opentrons/components'
56
import { i18n } from '../../../../i18n'
67
import * as RobotApi from '../../../../redux/robot-api'
7-
import { ConfigurePipette } from '../../../ConfigurePipette'
8+
import {
9+
getAttachedPipetteSettingsFieldsById,
10+
updatePipetteSettings,
11+
} from '../../../../redux/pipettes'
12+
import { getConfig } from '../../../../redux/config'
813
import { PipetteSettingsSlideout } from '../PipetteSettingsSlideout'
914

10-
import { mockLeftSpecs } from '../../../../redux/pipettes/__fixtures__'
15+
import {
16+
mockLeftSpecs,
17+
mockPipetteSettingsFieldsMap,
18+
} from '../../../../redux/pipettes/__fixtures__'
1119

1220
import type { DispatchApiRequestType } from '../../../../redux/robot-api'
21+
import type { UpdatePipetteSettingsAction } from '../../../../redux/pipettes/types'
1322

14-
jest.mock('../../../ConfigurePipette')
1523
jest.mock('../../../../redux/robot-api')
24+
jest.mock('../../../../redux/config')
25+
jest.mock('../../../../redux/pipettes')
1626

17-
const mockConfigurePipette = ConfigurePipette as jest.MockedFunction<
18-
typeof ConfigurePipette
19-
>
27+
const mockGetConfig = getConfig as jest.MockedFunction<typeof getConfig>
2028
const mockUseDispatchApiRequest = RobotApi.useDispatchApiRequest as jest.MockedFunction<
2129
typeof RobotApi.useDispatchApiRequest
2230
>
2331
const mockGetRequestById = RobotApi.getRequestById as jest.MockedFunction<
2432
typeof RobotApi.getRequestById
2533
>
34+
const mockGetAttachedPipetteSettingsFieldsById = getAttachedPipetteSettingsFieldsById as jest.MockedFunction<
35+
typeof getAttachedPipetteSettingsFieldsById
36+
>
37+
const mockUpdatePipetteSettings = updatePipetteSettings as jest.MockedFunction<
38+
typeof updatePipetteSettings
39+
>
2640

2741
const render = (
2842
props: React.ComponentProps<typeof PipetteSettingsSlideout>
@@ -32,7 +46,7 @@ const render = (
3246
})[0]
3347
}
3448

35-
const mockRobotName = 'mock robotName'
49+
const mockRobotName = 'mockRobotName'
3650

3751
describe('PipetteSettingsSlideout', () => {
3852
let dispatchApiRequest: DispatchApiRequestType
@@ -56,24 +70,65 @@ describe('PipetteSettingsSlideout', () => {
5670
status: 200,
5771
},
5872
})
73+
mockGetConfig.mockReturnValue({} as any)
74+
mockGetAttachedPipetteSettingsFieldsById.mockReturnValue(
75+
mockPipetteSettingsFieldsMap
76+
)
5977
dispatchApiRequest = jest.fn()
60-
mockUseDispatchApiRequest.mockReturnValue([dispatchApiRequest, ['id']])
61-
mockConfigurePipette.mockReturnValue(<div>mock configure pipette</div>)
78+
when(mockUseDispatchApiRequest)
79+
.calledWith()
80+
.mockReturnValue([dispatchApiRequest, ['id']])
6281
})
6382
afterEach(() => {
6483
jest.resetAllMocks()
65-
})
66-
afterEach(() => {
6784
resetAllWhenMocks()
6885
})
6986

70-
it('renders correct text', () => {
71-
const { getByText, getByRole } = render(props)
87+
it('renders correct heading and number of text boxes', () => {
88+
const { getByRole, getAllByRole } = render(props)
89+
90+
getByRole('heading', { name: 'Left Pipette Settings' })
91+
const inputs = getAllByRole('textbox')
92+
expect(inputs.length).toBe(9)
93+
})
94+
95+
it('renders close button that calls props.onCloseClick when clicked', () => {
96+
const { getByRole } = render(props)
7297

73-
getByText('Left Pipette Settings')
74-
getByText('mock configure pipette')
7598
const button = getByRole('button', { name: /exit/i })
7699
fireEvent.click(button)
77100
expect(props.onCloseClick).toHaveBeenCalled()
78101
})
102+
103+
it('renders confirm button and calls dispatchApiRequest with updatePipetteSettings action object when clicked', async () => {
104+
const { getByRole } = render(props)
105+
const button = getByRole('button', { name: 'Confirm' })
106+
107+
when(mockUpdatePipetteSettings)
108+
.calledWith(
109+
mockRobotName,
110+
props.pipetteId,
111+
expect.objectContaining({
112+
blowout: 2,
113+
bottom: 3,
114+
dropTip: 1,
115+
dropTipCurrent: null,
116+
dropTipSpeed: null,
117+
pickUpCurrent: null,
118+
pickUpDistance: null,
119+
plungerCurrent: null,
120+
top: 4,
121+
})
122+
)
123+
.mockReturnValue({
124+
type: 'pipettes:UPDATE_PIPETTE_SETTINGS',
125+
} as UpdatePipetteSettingsAction)
126+
127+
fireEvent.click(button)
128+
await waitFor(() => {
129+
expect(dispatchApiRequest).toHaveBeenCalledWith({
130+
type: 'pipettes:UPDATE_PIPETTE_SETTINGS',
131+
})
132+
})
133+
})
79134
})

0 commit comments

Comments
 (0)