From 08464ae352cba8e85c38f06824f09c11957925a9 Mon Sep 17 00:00:00 2001 From: smb2268 Date: Wed, 20 Mar 2024 13:43:13 -0400 Subject: [PATCH] fix(app): refactor Flex pipette card to reference /instruments not /pipettes fix RQA-2433 --- app/src/molecules/InstrumentCard/index.tsx | 21 +- .../Devices/InstrumentsAndModules.tsx | 116 ++++--- .../PipetteCard/AboutPipetteSlideout.tsx | 24 +- .../Devices/PipetteCard/FlexPipetteCard.tsx | 282 +++++++++++++++++ .../PipetteCard/PipetteOverflowMenu.tsx | 27 +- .../__tests__/AboutPipetteSlideout.test.tsx | 26 +- .../__tests__/FlexPipetteCard.test.tsx | 256 +++++++++++++++ .../__tests__/PipetteCard.test.tsx | 140 +-------- .../__tests__/PipetteOverflowMenu.test.tsx | 72 ----- .../organisms/Devices/PipetteCard/index.tsx | 296 ++++-------------- .../__tests__/InstrumentsAndModules.test.tsx | 101 +++--- 11 files changed, 724 insertions(+), 637 deletions(-) create mode 100644 app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx create mode 100644 app/src/organisms/Devices/PipetteCard/__tests__/FlexPipetteCard.test.tsx diff --git a/app/src/molecules/InstrumentCard/index.tsx b/app/src/molecules/InstrumentCard/index.tsx index d0d4bef491a4..98fb1e80a9fe 100644 --- a/app/src/molecules/InstrumentCard/index.tsx +++ b/app/src/molecules/InstrumentCard/index.tsx @@ -5,6 +5,7 @@ import { Flex, InstrumentDiagram, ALIGN_FLEX_START, + ALIGN_CENTER, BORDERS, COLORS, DIRECTION_COLUMN, @@ -80,13 +81,19 @@ export function InstrumentCard(props: InstrumentCardProps): JSX.Element { ) : null} {instrumentDiagramProps?.pipetteSpecs != null ? ( - + + + ) : null} - - {isFlex && ( - + ) : ( + <> + + + )} {leftColumnModules.map((module, index) => ( - {!Boolean(is96ChannelAttached) && ( + {!isFlex && ( )} + {isFlex && !is96ChannelAttached ? ( + + ) : null} {rightColumnModules.map((module, index) => ( unknown isExpanded: boolean } @@ -27,14 +25,14 @@ interface AboutPipetteSlideoutProps { export const AboutPipetteSlideout = ( props: AboutPipetteSlideoutProps ): JSX.Element | null => { - const { pipetteId, pipetteName, isExpanded, mount, onCloseClick } = props + const { + pipetteId, + pipetteName, + isExpanded, + firmwareVersion, + onCloseClick, + } = props const { i18n, t } = useTranslation(['device_details', 'shared']) - const { data: attachedInstruments } = useInstrumentsQuery() - const instrumentInfo = - attachedInstruments?.data?.find( - (i): i is PipetteData => - i.instrumentType === 'pipette' && i.ok && i.mount === mount - ) ?? null return ( - {instrumentInfo?.firmwareVersion != null && ( + {firmwareVersion != null && ( <> - {instrumentInfo.firmwareVersion} + {firmwareVersion} )} diff --git a/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx b/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx new file mode 100644 index 000000000000..e235177197da --- /dev/null +++ b/app/src/organisms/Devices/PipetteCard/FlexPipetteCard.tsx @@ -0,0 +1,282 @@ +import * as React from 'react' +import { Trans, useTranslation } from 'react-i18next' +import { css } from 'styled-components' +import { SPACING, TYPOGRAPHY } from '@opentrons/components' +import { + NINETY_SIX_CHANNEL, + SINGLE_MOUNT_PIPETTES, + FLEX_ROBOT_TYPE, + LEFT, +} from '@opentrons/shared-data' +import { + useCurrentSubsystemUpdateQuery, + useHost, +} from '@opentrons/react-api-client' +import { Banner } from '../../../atoms/Banner' +import { StyledText } from '../../../atoms/text' +import { InstrumentCard } from '../../../molecules/InstrumentCard' +import { ChoosePipette } from '../../PipetteWizardFlows/ChoosePipette' +import { FLOWS } from '../../PipetteWizardFlows/constants' +import { handlePipetteWizardFlows } from '../../PipetteWizardFlows' +import { DropTipWizard } from '../../DropTipWizard' + +import { AboutPipetteSlideout } from './AboutPipetteSlideout' + +import type { + BadPipette, + Mount, + PipetteData, + HostConfig, +} from '@opentrons/api-client' +import type { PipetteModelSpecs } from '@opentrons/shared-data' +import type { + PipetteWizardFlow, + SelectablePipettes, +} from '../../PipetteWizardFlows/types' + +interface FlexPipetteCardProps { + attachedPipette: PipetteData | BadPipette | null + pipetteModelSpecs: PipetteModelSpecs | null + mount: Mount + isRunActive: boolean + isEstopNotDisengaged: boolean +} +const BANNER_LINK_CSS = css` + text-decoration: ${TYPOGRAPHY.textDecorationUnderline}; + cursor: pointer; + margin-left: ${SPACING.spacing8}; +` + +const INSTRUMENT_CARD_STYLE = css` + p { + text-transform: lowercase; + } + + p::first-letter { + text-transform: uppercase; + } +` + +const POLL_DURATION_MS = 5000 + +export function FlexPipetteCard({ + pipetteModelSpecs, + attachedPipette, + mount, + isRunActive, + isEstopNotDisengaged, +}: FlexPipetteCardProps): JSX.Element { + const { t, i18n } = useTranslation(['device_details', 'shared']) + const host = useHost() as HostConfig + + const [ + showAboutPipetteSlideout, + setShowAboutPipetteSlideout, + ] = React.useState(false) + const [showChoosePipette, setShowChoosePipette] = React.useState(false) + const [showDropTipWizard, setShowDropTipWizard] = React.useState(false) + const [ + selectedPipette, + setSelectedPipette, + ] = React.useState(SINGLE_MOUNT_PIPETTES) + const attachedPipetteIs96Channel = + attachedPipette?.ok && attachedPipette.instrumentName === 'p1000_96' + const selectedPipetteForWizard = attachedPipetteIs96Channel + ? NINETY_SIX_CHANNEL + : selectedPipette + const setCloseFlow = (): void => { + setSelectedPipette(SINGLE_MOUNT_PIPETTES) + } + + const handleLaunchPipetteWizardFlows = (flowType: PipetteWizardFlow): void => + handlePipetteWizardFlows({ + flowType, + mount, + closeFlow: setCloseFlow, + selectedPipette: selectedPipetteForWizard, + host, + }) + const handleChoosePipette: React.MouseEventHandler = () => { + setShowChoosePipette(true) + } + const handleAttach = () => { + setShowChoosePipette(false) + handleLaunchPipetteWizardFlows(FLOWS.ATTACH) + } + + const handleDetach: React.MouseEventHandler = () => { + handleLaunchPipetteWizardFlows(FLOWS.DETACH) + } + + const handleCalibrate: React.MouseEventHandler = () => { + handleLaunchPipetteWizardFlows(FLOWS.CALIBRATE) + } + const handleDropTip = (): void => { + setShowDropTipWizard(true) + } + + const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = React.useState( + false + ) + const subsystem = attachedPipette?.subsystem ?? null + const { data: subsystemUpdateData } = useCurrentSubsystemUpdateQuery( + subsystem, + { + enabled: pollForSubsystemUpdate, + refetchInterval: POLL_DURATION_MS, + } + ) + const pipetteDisplayName = pipetteModelSpecs?.displayName + // we should poll for a subsystem update from the time a bad instrument is + // detected until the update has been done for 5 seconds + // this gives the instruments endpoint time to start reporting + // a good instrument + React.useEffect(() => { + if (attachedPipette?.ok === false) { + setPollForSubsystemUpdate(true) + } else if ( + subsystemUpdateData != null && + subsystemUpdateData.data.updateStatus === 'done' + ) { + setTimeout(() => { + setPollForSubsystemUpdate(false) + }, POLL_DURATION_MS) + } + }, [attachedPipette?.ok, subsystemUpdateData]) + + const menuOverlayItems = + attachedPipette == null || !attachedPipette.ok + ? [ + { + label: t('attach_pipette'), + disabled: attachedPipette != null || isRunActive, + onClick: handleChoosePipette, + }, + ] + : [ + { + label: + attachedPipette.data.calibratedOffset?.last_modified != null + ? t('recalibrate_pipette') + : t('calibrate_pipette'), + disabled: attachedPipette == null || isRunActive, + onClick: handleCalibrate, + }, + { + label: t('detach_pipette'), + disabled: attachedPipette == null || isRunActive, + onClick: handleDetach, + }, + { + label: t('about_pipette'), + disabled: attachedPipette == null, + onClick: () => setShowAboutPipetteSlideout(true), + }, + { + label: i18n.format(t('drop_tips'), 'capitalize'), + disabled: attachedPipette == null || isRunActive, + onClick: () => handleDropTip(), + }, + ] + return ( + <> + {(attachedPipette == null || attachedPipette.ok) && + subsystemUpdateData == null ? ( + + {isEstopNotDisengaged ? ( + + {t('calibration_needed_without_link')} + + ) : ( + + ), + }} + /> + )} + + ) : null + } + label={ + attachedPipetteIs96Channel + ? t('both_mounts') + : t('mount', { + side: mount === LEFT ? t('left') : t('right'), + }) + } + menuOverlayItems={menuOverlayItems} + isEstopNotDisengaged={isEstopNotDisengaged} + /> + ) : null} + {attachedPipette?.ok === false || + (subsystemUpdateData != null && pollForSubsystemUpdate) ? ( + + + + } + isEstopNotDisengaged={isEstopNotDisengaged} + /> + ) : null} + {showDropTipWizard && pipetteModelSpecs != null ? ( + setShowDropTipWizard(false)} + /> + ) : null} + {attachedPipette?.ok && showAboutPipetteSlideout && ( + setShowAboutPipetteSlideout(false)} + /> + )} + {showChoosePipette ? ( + setShowChoosePipette(false)} + mount={mount} + /> + ) : null} + + ) +} diff --git a/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx b/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx index 4319a18b44cc..8c21af89c5dc 100644 --- a/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx +++ b/app/src/organisms/Devices/PipetteCard/PipetteOverflowMenu.tsx @@ -10,11 +10,7 @@ import { SPACING, DIRECTION_COLUMN, } from '@opentrons/components' -import { - isFlexPipette, - PipetteModelSpecs, - PipetteName, -} from '@opentrons/shared-data' +import { PipetteModelSpecs } from '@opentrons/shared-data' import { MenuItem } from '../../../atoms/MenuList/MenuItem' import { Divider } from '../../../atoms/structure' @@ -28,10 +24,8 @@ interface PipetteOverflowMenuProps { mount: Mount handleChangePipette: () => void handleDropTip: () => void - handleCalibrate: () => void handleAboutSlideout: () => void handleSettingsSlideout: () => void - isPipetteCalibrated: boolean isRunActive: boolean } @@ -45,18 +39,13 @@ export const PipetteOverflowMenu = ( pipetteSettings, handleChangePipette, handleDropTip, - handleCalibrate, handleAboutSlideout, handleSettingsSlideout, - isPipetteCalibrated, isRunActive, } = props - const pipetteName = - pipetteSpecs?.name != null ? pipetteSpecs.name : t('empty') const pipetteDisplayName = pipetteSpecs?.displayName != null ? pipetteSpecs.displayName : t('empty') - const isFlexPipetteAttached = isFlexPipette(pipetteName as PipetteName) return ( @@ -80,18 +69,6 @@ export const PipetteOverflowMenu = ( ) : ( <> - {isFlexPipetteAttached ? ( - handleCalibrate()} - disabled={isRunActive} - > - {t( - isPipetteCalibrated - ? 'recalibrate_pipette' - : 'calibrate_pipette' - )} - - ) : null} handleChangePipette()} disabled={isRunActive} @@ -105,7 +82,7 @@ export const PipetteOverflowMenu = ( {i18n.format(t('drop_tips'), 'capitalize')} - {!isFlexPipetteAttached && pipetteSettings != null ? ( + {pipetteSettings != null ? ( handleSettingsSlideout()} diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/AboutPipetteSlideout.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/AboutPipetteSlideout.test.tsx index 1c1c8d8ee4b3..6417775d2e6f 100644 --- a/app/src/organisms/Devices/PipetteCard/__tests__/AboutPipetteSlideout.test.tsx +++ b/app/src/organisms/Devices/PipetteCard/__tests__/AboutPipetteSlideout.test.tsx @@ -2,12 +2,10 @@ import * as React from 'react' import { describe, it, vi, beforeEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../../__testing-utils__' -import { useInstrumentsQuery } from '@opentrons/react-api-client' import { fireEvent, screen } from '@testing-library/react' import { i18n } from '../../../../i18n' import { AboutPipetteSlideout } from '../AboutPipetteSlideout' import { mockLeftSpecs } from '../../../../redux/pipettes/__fixtures__' -import { LEFT } from '../../../../redux/pipettes' vi.mock('@opentrons/react-api-client') @@ -23,13 +21,9 @@ describe('AboutPipetteSlideout', () => { props = { pipetteId: '123', pipetteName: mockLeftSpecs.displayName, - mount: LEFT, isExpanded: true, onCloseClick: vi.fn(), } - vi.mocked(useInstrumentsQuery).mockReturnValue({ - data: { data: [] }, - } as any) }) it('renders correct info', () => { @@ -43,19 +37,13 @@ describe('AboutPipetteSlideout', () => { expect(props.onCloseClick).toHaveBeenCalled() }) it('renders the firmware version if it exists', () => { - vi.mocked(useInstrumentsQuery).mockReturnValue({ - data: { - data: [ - { - instrumentType: 'pipette', - mount: LEFT, - ok: true, - firmwareVersion: 12, - } as any, - ], - }, - } as any) - + props = { + pipetteId: '123', + pipetteName: mockLeftSpecs.displayName, + isExpanded: true, + firmwareVersion: '12', + onCloseClick: vi.fn(), + } render(props) screen.getByText('CURRENT VERSION') diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/FlexPipetteCard.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/FlexPipetteCard.test.tsx new file mode 100644 index 000000000000..d9ce76ec2e5e --- /dev/null +++ b/app/src/organisms/Devices/PipetteCard/__tests__/FlexPipetteCard.test.tsx @@ -0,0 +1,256 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' +import { renderWithProviders } from '../../../../__testing-utils__' +import { useCurrentSubsystemUpdateQuery } from '@opentrons/react-api-client' +import { i18n } from '../../../../i18n' +import { handlePipetteWizardFlows } from '../../../PipetteWizardFlows' +import { AboutPipetteSlideout } from '../AboutPipetteSlideout' +import { FlexPipetteCard } from '../FlexPipetteCard' +import { ChoosePipette } from '../../../PipetteWizardFlows/ChoosePipette' +import { DropTipWizard } from '../../../DropTipWizard' + +import type { PipetteData } from '@opentrons/api-client' +import { mockLeftSpecs } from '../../../../redux/pipettes/__fixtures__' + +vi.mock('../../../PipetteWizardFlows') +vi.mock('../../../PipetteWizardFlows/ChoosePipette') +vi.mock('../AboutPipetteSlideout') +vi.mock('../../../DropTipWizard') +vi.mock('@opentrons/react-api-client') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + })[0] +} + +describe('FlexPipetteCard', () => { + let props: React.ComponentProps + beforeEach(() => { + props = { + pipetteModelSpecs: mockLeftSpecs, + attachedPipette: { + instrumentType: 'pipette', + instrumentName: 'pipette', + subsystem: 'pipette_left', + mount: 'left', + instrumentModel: 'p50_single_v3.1', + serialNumber: '123', + firmwareVersion: '12', + ok: true, + data: { + channels: 1, + min_volume: 5, + max_volume: 50, + calibratedOffset: { + offset: { x: 1, y: 2, z: 3 }, + source: 'default', + last_modified: '12/2/4', + }, + }, + } as PipetteData, + mount: 'left', + isRunActive: false, + isEstopNotDisengaged: false, + } + vi.mocked(useCurrentSubsystemUpdateQuery).mockReturnValue({ + data: undefined, + } as any) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders correct info when gripper is attached', () => { + render(props) + screen.getByText('left Mount') + screen.getByText('Left Pipette') + const overflowButton = screen.getByRole('button', { + name: /overflow/i, + }) + fireEvent.click(overflowButton) + screen.getByText('Recalibrate pipette') + screen.getByText('Detach pipette') + screen.getByText('Drop tips') + screen.getByText('About pipette') + }) + it('renders correct info when 96 channel is attached', () => { + props = { + pipetteModelSpecs: mockLeftSpecs, + attachedPipette: { + instrumentType: 'pipette', + instrumentName: 'p1000_96', + subsystem: 'pipette_left', + mount: 'left', + instrumentModel: 'p50_single_v3.1', + serialNumber: '123', + firmwareVersion: '12', + ok: true, + data: { + channels: 1, + min_volume: 5, + max_volume: 50, + calibratedOffset: { + offset: { x: 1, y: 2, z: 3 }, + source: 'default', + last_modified: '12/2/4', + }, + }, + } as PipetteData, + mount: 'left', + isRunActive: false, + isEstopNotDisengaged: false, + } + render(props) + screen.getByText('Both Mounts') + screen.getByText('Left Pipette') + }) + it('renders recalibrate banner when no calibration data is present', () => { + props = { + pipetteModelSpecs: mockLeftSpecs, + attachedPipette: { + instrumentType: 'pipette', + instrumentName: 'pipette', + subsystem: 'pipette_left', + mount: 'left', + instrumentModel: 'p50_single_v3.1', + serialNumber: '123', + firmwareVersion: '12', + ok: true, + data: { + channels: 1, + min_volume: 5, + max_volume: 50, + }, + } as PipetteData, + mount: 'left', + isRunActive: false, + isEstopNotDisengaged: false, + } + + render(props) + screen.getByText('Calibration needed.') + screen.getByText('Calibrate now') + }) + + it('renders recalibrate banner without calibrate now when no calibration data is present and e-stop is pressed', () => { + props = { + pipetteModelSpecs: mockLeftSpecs, + attachedPipette: { + instrumentType: 'pipette', + instrumentName: 'pipette', + subsystem: 'pipette_left', + mount: 'left', + instrumentModel: 'p50_single_v3.1', + serialNumber: '123', + firmwareVersion: '12', + ok: true, + data: { + channels: 1, + min_volume: 5, + max_volume: 50, + }, + } as PipetteData, + mount: 'left', + isRunActive: false, + isEstopNotDisengaged: true, + } + + render(props) + screen.getByText('Calibration needed.') + }) + + it('opens the about pipette slideout when button is pressed', () => { + render(props) + const overflowButton = screen.getByRole('button', { + name: /overflow/i, + }) + fireEvent.click(overflowButton) + const aboutPipetteButton = screen.getByText('About pipette') + fireEvent.click(aboutPipetteButton) + expect(vi.mocked(AboutPipetteSlideout)).toHaveBeenCalled() + }) + it('renders choose pipette modal when attach button is pressed', () => { + props = { + mount: 'left', + attachedPipette: null, + pipetteModelSpecs: null, + isRunActive: false, + isEstopNotDisengaged: false, + } + render(props) + const overflowButton = screen.getByRole('button', { + name: /overflow/i, + }) + fireEvent.click(overflowButton) + const attachPipetteButton = screen.getByText('Attach pipette') + fireEvent.click(attachPipetteButton) + expect(vi.mocked(ChoosePipette)).toHaveBeenCalled() + }) + it('renders wizard flow when recalibrate button is pressed', () => { + render(props) + const overflowButton = screen.getByRole('button', { + name: /overflow/i, + }) + fireEvent.click(overflowButton) + const recalibratePipetteButton = screen.getByText('Recalibrate pipette') + fireEvent.click(recalibratePipetteButton) + expect(vi.mocked(handlePipetteWizardFlows)).toHaveBeenCalled() + }) + it('renders wizard flow when detach button is pressed', () => { + render(props) + const overflowButton = screen.getByRole('button', { + name: /InstrumentCard_overflowMenu/i, + }) + fireEvent.click(overflowButton) + const dropTipButton = screen.getByText('Detach pipette') + fireEvent.click(dropTipButton) + expect(vi.mocked(handlePipetteWizardFlows)).toHaveBeenCalled() + }) + it('renders drop tip wizard when drop tip button is pressed', () => { + render(props) + const overflowButton = screen.getByRole('button', { + name: /InstrumentCard_overflowMenu/i, + }) + fireEvent.click(overflowButton) + const dropTipButton = screen.getByText('Drop tips') + fireEvent.click(dropTipButton) + expect(vi.mocked(DropTipWizard)).toHaveBeenCalled() + }) + it('renders firmware update needed state if pipette is bad', () => { + props = { + attachedPipette: { + ok: false, + } as any, + mount: 'left', + pipetteModelSpecs: null, + isRunActive: false, + isEstopNotDisengaged: false, + } + render(props) + screen.getByText('Left mount') + screen.getByText('Instrument attached') + screen.getByText( + `Instrument firmware update needed. Start the update on the robot's touchscreen.` + ) + }) + it('renders firmware update in progress state if gripper is bad and update in progress', () => { + vi.mocked(useCurrentSubsystemUpdateQuery).mockReturnValue({ + data: { data: { updateProgress: 50 } as any }, + } as any) + props = { + attachedPipette: { + ok: false, + } as any, + mount: 'left', + pipetteModelSpecs: null, + isRunActive: false, + isEstopNotDisengaged: false, + } + render(props) + screen.getByText('Left mount') + screen.getByText('Instrument attached') + screen.getByText('Firmware update in progress...') + }) +}) diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx index 67cd500763e2..05e0ad816f19 100644 --- a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx +++ b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteCard.test.tsx @@ -5,16 +5,10 @@ import { describe, it, vi, beforeEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../../__testing-utils__' import { LEFT, RIGHT } from '@opentrons/shared-data' -import { - useCurrentSubsystemUpdateQuery, - usePipetteSettingsQuery, -} from '@opentrons/react-api-client' +import { usePipetteSettingsQuery } from '@opentrons/react-api-client' import { i18n } from '../../../../i18n' import { getHasCalibrationBlock } from '../../../../redux/config' import { useDispatchApiRequest } from '../../../../redux/robot-api' -import { AskForCalibrationBlockModal } from '../../../CalibrateTipLength' -import { useCalibratePipetteOffset } from '../../../CalibratePipetteOffset/useCalibratePipetteOffset' -import { useDeckCalibrationData, useIsFlex } from '../../hooks' import { PipetteOverflowMenu } from '../PipetteOverflowMenu' import { AboutPipetteSlideout } from '../AboutPipetteSlideout' import { PipetteCard } from '..' @@ -23,15 +17,11 @@ import { mockLeftSpecs, mockRightSpecs, } from '../../../../redux/pipettes/__fixtures__' -import { mockDeckCalData } from '../../../../redux/calibration/__fixtures__' import type { DispatchApiRequestType } from '../../../../redux/robot-api' vi.mock('../PipetteOverflowMenu') vi.mock('../../../../redux/config') -vi.mock('../../../CalibratePipetteOffset/useCalibratePipetteOffset') -vi.mock('../../../CalibrateTipLength') -vi.mock('../../hooks') vi.mock('../AboutPipetteSlideout') vi.mock('../../../../redux/robot-api') vi.mock('@opentrons/react-api-client') @@ -57,35 +47,20 @@ describe('PipetteCard', () => { mount: LEFT, robotName: mockRobotName, pipetteId: 'id', - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: false, isRunActive: false, isEstopNotDisengaged: false, } - when(useIsFlex).calledWith(mockRobotName).thenReturn(false) vi.mocked(AboutPipetteSlideout).mockReturnValue(
mock about slideout
) - when(useDeckCalibrationData).calledWith(mockRobotName).thenReturn({ - isDeckCalibrated: true, - deckCalibrationData: mockDeckCalData, - }) vi.mocked(PipetteOverflowMenu).mockReturnValue(
mock pipette overflow menu
) vi.mocked(getHasCalibrationBlock).mockReturnValue(null) - vi.mocked(useCalibratePipetteOffset).mockReturnValue([startWizard, null]) - vi.mocked(AskForCalibrationBlockModal).mockReturnValue( -
Mock AskForCalibrationBlockModal
- ) vi.mocked(useDispatchApiRequest).mockReturnValue([ dispatchApiRequest, ['id'], ]) - vi.mocked(useCurrentSubsystemUpdateQuery).mockReturnValue({ - data: undefined, - } as any) when(usePipetteSettingsQuery) .calledWith({ refetchInterval: 5000, enabled: true }) .thenReturn({} as any) @@ -97,9 +72,6 @@ describe('PipetteCard', () => { mount: LEFT, robotName: mockRobotName, pipetteId: 'id', - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: false, isRunActive: false, isEstopNotDisengaged: false, } @@ -107,48 +79,6 @@ describe('PipetteCard', () => { screen.getByText('left Mount') screen.getByText('Left Pipette') }) - it('renders information for a 96 channel pipette with overflow menu button not disabled', () => { - props = { - pipetteModelSpecs: mockLeftSpecs, - mount: LEFT, - robotName: mockRobotName, - pipetteId: 'id', - pipetteIs96Channel: true, - isPipetteCalibrated: false, - pipetteIsBad: false, - isRunActive: false, - isEstopNotDisengaged: false, - } - render(props) - screen.getByText('Both Mounts') - const overflowButton = screen.getByRole('button', { - name: /overflow/i, - }) - fireEvent.click(overflowButton) - expect(overflowButton).not.toBeDisabled() - screen.getByText('mock pipette overflow menu') - }) - - it('renders information for a 96 channel pipette with overflow menu button disabled when e-stop is pressed', () => { - props = { - pipetteModelSpecs: mockLeftSpecs, - mount: LEFT, - robotName: mockRobotName, - pipetteId: 'id', - pipetteIs96Channel: true, - isPipetteCalibrated: false, - pipetteIsBad: false, - isRunActive: false, - isEstopNotDisengaged: true, - } - render(props) - screen.getByText('Both Mounts') - const overflowButton = screen.getByRole('button', { - name: /overflow/i, - }) - fireEvent.click(overflowButton) - expect(overflowButton).toBeDisabled() - }) it('renders information for a right pipette', () => { props = { @@ -156,9 +86,6 @@ describe('PipetteCard', () => { mount: RIGHT, robotName: mockRobotName, pipetteId: 'id', - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: false, isRunActive: false, isEstopNotDisengaged: false, } @@ -171,9 +98,6 @@ describe('PipetteCard', () => { pipetteModelSpecs: null, mount: RIGHT, robotName: mockRobotName, - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: false, isRunActive: false, isEstopNotDisengaged: false, } @@ -186,9 +110,6 @@ describe('PipetteCard', () => { pipetteModelSpecs: null, mount: LEFT, robotName: mockRobotName, - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: false, isRunActive: false, isEstopNotDisengaged: false, } @@ -197,43 +118,21 @@ describe('PipetteCard', () => { screen.getByText('Empty') }) it('does not render banner to calibrate for ot2 pipette if not calibrated', () => { - when(useIsFlex).calledWith(mockRobotName).thenReturn(false) props = { pipetteModelSpecs: mockLeftSpecs, mount: LEFT, robotName: mockRobotName, - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: false, isRunActive: false, isEstopNotDisengaged: false, } render(props) expect(screen.queryByText('Calibrate now')).toBeNull() }) - it('renders banner to calibrate for ot3 pipette if not calibrated', () => { - when(useIsFlex).calledWith(mockRobotName).thenReturn(true) - props = { - pipetteModelSpecs: { ...mockLeftSpecs, name: 'p300_single_flex' }, - mount: LEFT, - robotName: mockRobotName, - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: false, - isRunActive: false, - isEstopNotDisengaged: false, - } - render(props) - screen.getByText('Calibrate now') - }) it('renders kebab icon, opens and closes overflow menu on click', () => { props = { pipetteModelSpecs: mockRightSpecs, mount: RIGHT, robotName: mockRobotName, - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: false, isRunActive: false, isEstopNotDisengaged: false, } @@ -249,43 +148,6 @@ describe('PipetteCard', () => { fireEvent.click(overflowMenu) expect(screen.queryByText('mock pipette overflow menu')).toBeNull() }) - it('renders firmware update needed state if pipette is bad', () => { - props = { - pipetteModelSpecs: mockRightSpecs, - mount: RIGHT, - robotName: mockRobotName, - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: true, - isRunActive: false, - isEstopNotDisengaged: false, - } - render(props) - screen.getByText('Right mount') - screen.getByText('Instrument attached') - screen.getByText( - `Instrument firmware update needed. Start the update on the robot's touchscreen.` - ) - }) - it('renders firmware update in progress state if pipette is bad and update in progress', () => { - vi.mocked(useCurrentSubsystemUpdateQuery).mockReturnValue({ - data: { data: { updateProgress: 50 } as any }, - } as any) - props = { - pipetteModelSpecs: mockRightSpecs, - mount: RIGHT, - robotName: mockRobotName, - pipetteIs96Channel: false, - isPipetteCalibrated: false, - pipetteIsBad: true, - isRunActive: false, - isEstopNotDisengaged: false, - } - render(props) - screen.getByText('Right mount') - screen.getByText('Instrument attached') - screen.getByText('Firmware update in progress...') - }) it('does not render a pipette settings slideout card if the pipette has no settings', () => { render(props) expect( diff --git a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx index 9545df087fc8..c6f7fef1d5be 100644 --- a/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx +++ b/app/src/organisms/Devices/PipetteCard/__tests__/PipetteOverflowMenu.test.tsx @@ -40,10 +40,8 @@ describe('PipetteOverflowMenu', () => { mount: LEFT, handleDropTip: vi.fn(), handleChangePipette: vi.fn(), - handleCalibrate: vi.fn(), handleAboutSlideout: vi.fn(), handleSettingsSlideout: vi.fn(), - isPipetteCalibrated: false, isRunActive: false, } }) @@ -73,52 +71,6 @@ describe('PipetteOverflowMenu', () => { fireEvent.click(btn) expect(props.handleChangePipette).toHaveBeenCalled() }) - it('renders recalibrate pipette text for Flex pipette', () => { - vi.mocked(isFlexPipette).mockReturnValue(true) - props = { - ...props, - isPipetteCalibrated: true, - } - render(props) - const recalibrate = screen.getByRole('button', { - name: 'Recalibrate pipette', - }) - fireEvent.click(recalibrate) - expect(props.handleCalibrate).toHaveBeenCalled() - }) - - it('should render recalibrate pipette text for Flex pipette', () => { - vi.mocked(isFlexPipette).mockReturnValue(true) - props = { - ...props, - isPipetteCalibrated: true, - } - render(props) - screen.getByRole('button', { - name: 'Recalibrate pipette', - }) - }) - - it('renders only the about pipette button if FLEX pipette is attached', () => { - vi.mocked(isFlexPipette).mockReturnValue(true) - - render(props) - - const calibrate = screen.getByRole('button', { - name: 'Calibrate pipette', - }) - const detach = screen.getByRole('button', { name: 'Detach pipette' }) - const settings = screen.queryByRole('button', { name: 'Pipette Settings' }) - const about = screen.getByRole('button', { name: 'About pipette' }) - - fireEvent.click(calibrate) - expect(props.handleCalibrate).toHaveBeenCalled() - fireEvent.click(detach) - expect(props.handleChangePipette).toHaveBeenCalled() - expect(settings).toBeNull() - fireEvent.click(about) - expect(props.handleAboutSlideout).toHaveBeenCalled() - }) it('does not render the pipette settings button if the pipette has no settings', () => { vi.mocked(isFlexPipette).mockReturnValue(false) @@ -132,30 +84,6 @@ describe('PipetteOverflowMenu', () => { expect(settings).not.toBeInTheDocument() }) - it('should disable certain menu items if a run is active for Flex pipette', () => { - vi.mocked(isFlexPipette).mockReturnValue(true) - props = { - ...props, - isRunActive: true, - } - render(props) - expect( - screen.getByRole('button', { - name: 'Calibrate pipette', - }) - ).toBeDisabled() - expect( - screen.getByRole('button', { - name: 'Detach pipette', - }) - ).toBeDisabled() - expect( - screen.getByRole('button', { - name: 'Drop tips', - }) - ).toBeDisabled() - }) - it('should disable certain menu items if a run is active for OT-2 pipette', () => { vi.mocked(isFlexPipette).mockReturnValue(false) props = { diff --git a/app/src/organisms/Devices/PipetteCard/index.tsx b/app/src/organisms/Devices/PipetteCard/index.tsx index 03cb1483cb2d..8fa8d6f9dac7 100644 --- a/app/src/organisms/Devices/PipetteCard/index.tsx +++ b/app/src/organisms/Devices/PipetteCard/index.tsx @@ -1,6 +1,5 @@ import * as React from 'react' -import { Trans, useTranslation } from 'react-i18next' -import { css } from 'styled-components' +import { useTranslation } from 'react-i18next' import { Box, @@ -16,77 +15,40 @@ import { BORDERS, ALIGN_CENTER, } from '@opentrons/components' -import { - FLEX_ROBOT_TYPE, - isFlexPipette, - NINETY_SIX_CHANNEL, - OT2_ROBOT_TYPE, - SINGLE_MOUNT_PIPETTES, -} from '@opentrons/shared-data' -import { - useCurrentSubsystemUpdateQuery, - usePipetteSettingsQuery, - useHost, -} from '@opentrons/react-api-client' +import { OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { usePipetteSettingsQuery } from '@opentrons/react-api-client' import { LEFT } from '../../../redux/pipettes' import { OverflowBtn } from '../../../atoms/MenuList/OverflowBtn' import { StyledText } from '../../../atoms/text' -import { Banner } from '../../../atoms/Banner' import { useMenuHandleClickOutside } from '../../../atoms/MenuList/hooks' -import { InstrumentCard } from '../../../molecules/InstrumentCard' import { ChangePipette } from '../../ChangePipette' -import { FLOWS } from '../../PipetteWizardFlows/constants' -import { handlePipetteWizardFlows } from '../../PipetteWizardFlows' -import { ChoosePipette } from '../../PipetteWizardFlows/ChoosePipette' -import { useIsFlex } from '../hooks' import { PipetteOverflowMenu } from './PipetteOverflowMenu' import { PipetteSettingsSlideout } from './PipetteSettingsSlideout' import { AboutPipetteSlideout } from './AboutPipetteSlideout' -import type { PipetteModelSpecs, PipetteName } from '@opentrons/shared-data' +import type { PipetteModelSpecs } from '@opentrons/shared-data' import type { AttachedPipette, Mount } from '../../../redux/pipettes/types' -import type { - PipetteWizardFlow, - SelectablePipettes, -} from '../../PipetteWizardFlows/types' import { DropTipWizard } from '../../DropTipWizard' -import { HostConfig } from '@opentrons/api-client' interface PipetteCardProps { pipetteModelSpecs: PipetteModelSpecs | null pipetteId?: AttachedPipette['id'] | null - isPipetteCalibrated: boolean mount: Mount robotName: string - pipetteIs96Channel: boolean - pipetteIsBad: boolean isRunActive: boolean isEstopNotDisengaged: boolean } -const INSTRUMENT_CARD_STYLE = css` - p { - text-transform: lowercase; - } - - p::first-letter { - text-transform: uppercase; - } -` - const POLL_DURATION_MS = 5000 export const PipetteCard = (props: PipetteCardProps): JSX.Element => { - const { t, i18n } = useTranslation(['device_details', 'protocol_setup']) + const { t } = useTranslation(['device_details', 'protocol_setup']) const { pipetteModelSpecs, - isPipetteCalibrated, mount, robotName, pipetteId, - pipetteIs96Channel, - pipetteIsBad, isRunActive, isEstopNotDisengaged, } = props @@ -96,10 +58,6 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { showOverflowMenu, setShowOverflowMenu, } = useMenuHandleClickOutside() - const isFlex = useIsFlex(robotName) - const host = useHost() as HostConfig - const pipetteName = pipetteModelSpecs?.name - const isFlexPipetteAttached = isFlexPipette(pipetteName as PipetteName) const pipetteDisplayName = pipetteModelSpecs?.displayName const pipetteOverflowWrapperRef = useOnClickOutside({ onClickOutside: () => setShowOverflowMenu(false), @@ -107,35 +65,7 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { const [showChangePipette, setChangePipette] = React.useState(false) const [showDropTipWizard, setShowDropTipWizard] = React.useState(false) const [showSlideout, setShowSlideout] = React.useState(false) - const [showAttachPipette, setShowAttachPipette] = React.useState(false) const [showAboutSlideout, setShowAboutSlideout] = React.useState(false) - const subsystem = mount === LEFT ? 'pipette_left' : 'pipette_right' - const [pollForSubsystemUpdate, setPollForSubsystemUpdate] = React.useState( - false - ) - const { data: subsystemUpdateData } = useCurrentSubsystemUpdateQuery( - subsystem, - { - enabled: pollForSubsystemUpdate, - refetchInterval: POLL_DURATION_MS, - } - ) - // we should poll for a subsystem update from the time a bad instrument is - // detected until the update has been done for 5 seconds - // this gives the instruments endpoint time to start reporting - // a good instrument - React.useEffect(() => { - if (pipetteIsBad && isFlex) { - setPollForSubsystemUpdate(true) - } else if ( - subsystemUpdateData != null && - subsystemUpdateData.data.updateStatus === 'done' - ) { - setTimeout(() => { - setPollForSubsystemUpdate(false) - }, POLL_DURATION_MS) - } - }, [pipetteIsBad, subsystemUpdateData, isFlex]) const settings = usePipetteSettingsQuery({ @@ -143,52 +73,18 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { enabled: pipetteId != null, })?.data?.[pipetteId ?? '']?.fields ?? null - const [ - selectedPipette, - setSelectedPipette, - ] = React.useState(SINGLE_MOUNT_PIPETTES) - const selectedPipetteForWizard = - pipetteName === 'p1000_96' ? NINETY_SIX_CHANNEL : selectedPipette - const handleChangePipette = (): void => { - if (isFlexPipetteAttached && isFlex) { - handleLaunchPipetteWizardFlows(FLOWS.DETACH) - } else if (!isFlexPipetteAttached && isFlex) { - setShowAttachPipette(true) - } else { - setChangePipette(true) - } + setChangePipette(true) } const handleDropTip = (): void => { setShowDropTipWizard(true) } - const handleCalibrate = (): void => { - if (isFlexPipetteAttached) { - handleLaunchPipetteWizardFlows(FLOWS.CALIBRATE) - } - } const handleAboutSlideout = (): void => { setShowAboutSlideout(true) } const handleSettingsSlideout = (): void => { setShowSlideout(true) } - const handleAttachPipette = (): void => { - setShowAttachPipette(false) - handleLaunchPipetteWizardFlows(FLOWS.ATTACH) - } - const setCloseFlow = (): void => { - setSelectedPipette(SINGLE_MOUNT_PIPETTES) - } - const handleLaunchPipetteWizardFlows = (flowType: PipetteWizardFlow): void => - handlePipetteWizardFlows({ - flowType, - mount, - closeFlow: setCloseFlow, - selectedPipette: selectedPipetteForWizard, - host, - }) - return ( { width="100%" data-testid={`PipetteCard_${String(pipetteDisplayName)}`} > - {showAttachPipette ? ( - setShowAttachPipette(false)} - mount={mount} - /> - ) : null} {showChangePipette && ( { )} {showDropTipWizard && pipetteModelSpecs != null ? ( setShowDropTipWizard(false)} @@ -237,127 +124,66 @@ export const PipetteCard = (props: PipetteCardProps): JSX.Element => { setShowAboutSlideout(false)} isExpanded={true} /> )} - {!pipetteIsBad && subsystemUpdateData == null && ( - <> - - - {pipetteModelSpecs !== null ? ( - - - - ) : null} - - {isFlexPipetteAttached && !isPipetteCalibrated ? ( - - {isEstopNotDisengaged ? ( - - {t('calibration_needed_without_link')} - - ) : ( - - ), - }} - /> - )} - - ) : null} - - {pipetteIs96Channel - ? t('both_mounts') - : t('mount', { - side: mount === LEFT ? t('left') : t('right'), - })} + <> + + + {pipetteModelSpecs !== null ? ( + + + + ) : null} + + + {t('mount', { + side: mount === LEFT ? t('left') : t('right'), + })} + + + + {pipetteDisplayName ?? t('empty')} - - - {pipetteDisplayName ?? t('empty')} - - - - - - - - )} - {(pipetteIsBad || - (subsystemUpdateData != null && pollForSubsystemUpdate)) && ( - - - - } - isEstopNotDisengaged={isEstopNotDisengaged} - /> - )} + + + + + + {showOverflowMenu && ( <> { handleDropTip={handleDropTip} handleSettingsSlideout={handleSettingsSlideout} handleAboutSlideout={handleAboutSlideout} - handleCalibrate={handleCalibrate} - isPipetteCalibrated={isPipetteCalibrated} pipetteSettings={settings} isRunActive={isRunActive} /> diff --git a/app/src/organisms/Devices/__tests__/InstrumentsAndModules.test.tsx b/app/src/organisms/Devices/__tests__/InstrumentsAndModules.test.tsx index 400d89e2ec08..4c2774ea2bd5 100644 --- a/app/src/organisms/Devices/__tests__/InstrumentsAndModules.test.tsx +++ b/app/src/organisms/Devices/__tests__/InstrumentsAndModules.test.tsx @@ -1,16 +1,14 @@ import * as React from 'react' import { when } from 'vitest-when' import { screen } from '@testing-library/react' -import { describe, it, vi, beforeEach, expect } from 'vitest' +import { describe, it, vi, beforeEach, afterEach, expect } from 'vitest' import '@testing-library/jest-dom/vitest' import { renderWithProviders } from '../../../__testing-utils__' import { - useAllPipetteOffsetCalibrationsQuery, useModulesQuery, useInstrumentsQuery, usePipettesQuery, } from '@opentrons/react-api-client' -import { instrumentsResponseFixture } from '@opentrons/api-client' import { i18n } from '../../../i18n' import { Banner } from '../../../atoms/Banner' @@ -21,16 +19,12 @@ import { ModuleCard } from '../../ModuleCard' import { InstrumentsAndModules } from '../InstrumentsAndModules' import { GripperCard } from '../../GripperCard' import { PipetteCard } from '../PipetteCard' +import { FlexPipetteCard } from '../PipetteCard/FlexPipetteCard' import { PipetteRecalibrationWarning } from '../PipetteCard/PipetteRecalibrationWarning' import { getIs96ChannelPipetteAttached, getShowPipetteCalibrationWarning, - getOffsetCalibrationForMount, } from '../utils' -import { - mockPipetteOffsetCalibration1, - mockPipetteOffsetCalibration2, -} from '../../../redux/calibration/pipette-offset/__fixtures__' import { useIsEstopNotDisengaged } from '../../../resources/devices/hooks/useIsEstopNotDisengaged' import type * as Components from '@opentrons/components' @@ -46,6 +40,7 @@ vi.mock('../hooks') vi.mock('../../GripperCard') vi.mock('../../ModuleCard') vi.mock('../PipetteCard') +vi.mock('../PipetteCard/FlexPipetteCard') vi.mock('../PipetteCard/PipetteRecalibrationWarning') vi.mock('../../ProtocolUpload/hooks') vi.mock('../../../atoms/Banner') @@ -72,18 +67,15 @@ describe('InstrumentsAndModules', () => { }) vi.mocked(getIs96ChannelPipetteAttached).mockReturnValue(false) vi.mocked(getShowPipetteCalibrationWarning).mockReturnValue(false) - vi.mocked(getOffsetCalibrationForMount).mockReturnValue(null) vi.mocked(useInstrumentsQuery).mockReturnValue({ data: { data: [] }, } as any) - vi.mocked(PipetteCard).mockReturnValue(
Mock PipetteCard
) - vi.mocked(GripperCard).mockReturnValue(
Mock GripperCard
) - vi.mocked(ModuleCard).mockReturnValue(
Mock ModuleCard
) when(useIsFlex).calledWith(ROBOT_NAME).thenReturn(false) when(useIsEstopNotDisengaged).calledWith(ROBOT_NAME).thenReturn(false) - vi.mocked(PipetteRecalibrationWarning).mockReturnValue( -
Mock PipetteRecalibrationWarning
- ) + }) + + afterEach(() => { + vi.resetAllMocks() }) it('renders an empty state message when robot is not on the network', () => { @@ -107,10 +99,9 @@ describe('InstrumentsAndModules', () => { }, } as any) render() - - screen.getByText('Mock ModuleCard') + expect(vi.mocked(ModuleCard)).toHaveBeenCalled() }) - it('renders pipette cards when a robot is viewable', () => { + it('renders pipette cards when a ot2 robot is viewable', () => { vi.mocked(useIsRobotViewable).mockReturnValue(true) vi.mocked(useModulesQuery).mockReturnValue({ data: { data: [mockMagneticModule] }, @@ -122,78 +113,56 @@ describe('InstrumentsAndModules', () => { }, } as any) render() - screen.getAllByText('Mock PipetteCard') + expect(vi.mocked(PipetteCard)).toHaveBeenCalledTimes(2) }) - it('renders gripper cards when a robot is Flex', () => { + it('renders gripper and flex pipette cards when a robot is Flex', () => { when(useIsFlex).calledWith(ROBOT_NAME).thenReturn(true) vi.mocked(useIsRobotViewable).mockReturnValue(true) - vi.mocked(useModulesQuery).mockReturnValue({ data: { data: [] } } as any) - vi.mocked(usePipettesQuery).mockReturnValue({ - data: { left: null, right: null }, - } as any) - vi.mocked(useInstrumentsQuery).mockReturnValue({ - data: { data: [instrumentsResponseFixture.data[0]] }, - } as any) render() - screen.getByText('Mock GripperCard') + expect(vi.mocked(GripperCard)).toHaveBeenCalled() + expect(vi.mocked(FlexPipetteCard)).toHaveBeenCalledTimes(2) }) it('renders the protocol loaded banner when protocol is loaded and not terminal state', () => { vi.mocked(useCurrentRunId).mockReturnValue('RUNID') - vi.mocked(Banner).mockReturnValue(
mock Banner
) render() - - screen.getByText('mock Banner') + expect(vi.mocked(Banner)).toHaveBeenCalled() }) it('renders 1 pipette card when a 96 channel is attached', () => { + when(useIsFlex).calledWith(ROBOT_NAME).thenReturn(true) vi.mocked(getIs96ChannelPipetteAttached).mockReturnValue(true) vi.mocked(useIsRobotViewable).mockReturnValue(true) render() - screen.getByText('Mock PipetteCard') + expect(vi.mocked(FlexPipetteCard)).toHaveBeenCalledTimes(1) }) it('renders pipette recalibration recommendation banner when offsets fail reasonability checks', () => { vi.mocked(getShowPipetteCalibrationWarning).mockReturnValue(true) vi.mocked(useIsRobotViewable).mockReturnValue(true) render() - screen.getByText('Mock PipetteRecalibrationWarning') + expect(vi.mocked(PipetteRecalibrationWarning)).toHaveBeenCalled() }) - it('fetches offset calibrations on long poll and pipettes, instruments, and modules on short poll', () => { - const { pipette: pipette1 } = mockPipetteOffsetCalibration1 - const { pipette: pipette2 } = mockPipetteOffsetCalibration2 - - vi.mocked(usePipettesQuery).mockReturnValue({ - data: { - left: { - id: pipette1, - name: `test-${pipette1}`, - model: 'p10_single_v1', - tip_length: 0, - mount_axis: 'z', - plunger_axis: 'b', - }, - right: { - id: pipette2, - name: `test-${pipette2}`, - model: 'p10_single_v1', - tip_length: 0, - mount_axis: 'y', - plunger_axis: 'a', - }, - }, - } as any) - vi.mocked(useAllPipetteOffsetCalibrationsQuery).mockReturnValue({ - data: { - data: [mockPipetteOffsetCalibration1, mockPipetteOffsetCalibration2], - }, - } as any) + it('fetches pipette and modules on short poll for ot2', () => { render() - expect(useAllPipetteOffsetCalibrationsQuery).toHaveBeenCalledWith({ - refetchInterval: 30000, - enabled: true, + expect(usePipettesQuery).toHaveBeenCalledWith( + {}, + { refetchInterval: 5000, enabled: true } + ) + expect(useModulesQuery).toHaveBeenCalledWith({ refetchInterval: 5000 }) + expect(useInstrumentsQuery).toHaveBeenCalledWith({ + refetchInterval: 5000, + enabled: false, }) - expect(usePipettesQuery).toHaveBeenCalledWith({}, { refetchInterval: 5000 }) + }) + it('fetches instruments and modules on short poll for flex', () => { + when(useIsFlex).calledWith(ROBOT_NAME).thenReturn(true) + render() + expect(usePipettesQuery).toHaveBeenCalledWith( + {}, + { refetchInterval: 5000, enabled: false } + ) expect(useModulesQuery).toHaveBeenCalledWith({ refetchInterval: 5000 }) expect(useInstrumentsQuery).toHaveBeenCalledWith({ refetchInterval: 5000, + enabled: true, }) }) })