From f56d60efe7ebf7ca2e0fc5127db8d7739e2aab3c Mon Sep 17 00:00:00 2001 From: smb2268 Date: Mon, 16 Sep 2024 17:17:37 -0400 Subject: [PATCH] test(app): add tests for individual advanced setting files --- .../AirGap.test.tsx | 275 +++++++++++++++++ .../BlowOut.test.tsx | 171 ++++++++++ .../Delay.test.tsx | 292 ++++++++++++++++++ .../FlowRate.test.tsx | 157 ++++++++++ .../Mix.test.tsx | 262 ++++++++++++++++ .../PipettePath.test.tsx | 229 ++++++++++++++ .../TipPosition.test.tsx | 176 +++++++++++ .../TouchTip.test.tsx | 240 ++++++++++++++ 8 files changed, 1802 insertions(+) create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/BlowOut.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Delay.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/FlowRate.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Mix.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/PipettePath.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx create mode 100644 app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TouchTip.test.tsx diff --git a/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx new file mode 100644 index 00000000000..85fec789323 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/AirGap.test.tsx @@ -0,0 +1,275 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { InputField } from '../../../../atoms/InputField' +import { useTrackEventWithRobotSerial } from '../../../Devices/hooks' +import { AirGap } from '../../QuickTransferAdvancedSettings/AirGap' +import type { QuickTransferSummaryState } from '../../types' + +vi.mock('../../../Devices/hooks') +vi.mock('../utils') + +vi.mock('../../../../atoms/InputField', async importOriginal => { + const actualComponents = await importOriginal() + return { + ...actualComponents, + InputField: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +let mockTrackEventWithRobotSerial: any + +describe('AirGap', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onBack: vi.fn(), + kind: 'aspirate', + state: { + mount: 'left', + pipette: { + channels: 1, + liquids: [ + { + maxVolume: 1000, + minVolume: 5, + }, + ] as any, + } as any, + tipRack: { + wells: { + A1: { + totalLiquidVolume: 200, + }, + } as any, + } as any, + sourceWells: ['A1'], + destinationWells: ['A1'], + transferType: 'transfer', + volume: 20, + path: 'single', + } as QuickTransferSummaryState, + dispatch: vi.fn(), + } + mockTrackEventWithRobotSerial = vi.fn( + () => new Promise(resolve => resolve({})) + ) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEventWithRobotSerial, + }) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the first air gap screen, continue, and back buttons', () => { + render(props) + screen.getByText('Air gap before aspirating') + screen.getByTestId('ChildNavigation_Primary_Button') + screen.getByText('Enabled') + screen.getByText('Disabled') + const exitBtn = screen.getByTestId('ChildNavigation_Back_Button') + fireEvent.click(exitBtn) + expect(props.onBack).toHaveBeenCalled() + }) + + it('renders save button if you select enabled, then moves to second screen', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Air gap volume (µL)', + error: null, + readOnly: true, + type: 'number', + value: null, + }, + {} + ) + }) + + it('calls dispatch button if you select disabled and save', () => { + render(props) + const disabledBtn = screen.getByText('Disabled') + fireEvent.click(disabledBtn) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('has correct range for aspirate with a single pipette path', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const numButton = screen.getByText('0') + fireEvent.click(numButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Air gap volume (µL)', + error: 'Value must be between 1-180', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('has correct range for aspirate with a multiAspirate pipette path', () => { + props = { + ...props, + state: { + ...props.state, + path: 'multiAspirate', + }, + } + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const numButton = screen.getByText('0') + fireEvent.click(numButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Air gap volume (µL)', + error: 'Value must be between 1-80', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + }) + + it('has correct range for aspirate with a multiDispense pipette path', () => { + props = { + ...props, + state: { + ...props.state, + path: 'multiDispense', + }, + } + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const numButton = screen.getByText('0') + fireEvent.click(numButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Air gap volume (µL)', + error: 'Value must be between 1-140', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + }) + + it('has correct range for and text for a dispense', () => { + props = { + ...props, + kind: 'dispense', + } + render(props) + screen.getByText('Air gap before dispensing') + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const numButton = screen.getByText('0') + fireEvent.click(numButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Air gap volume (µL)', + error: 'Value must be between 1-200', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + }) + + it('calls dispatch when an in range value is entered and saved', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const numButton = screen.getByText('1') + fireEvent.click(numButton) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('persists existing values if they are in state for aspirate', () => { + props = { + ...props, + state: { + ...props.state, + airGapAspirate: 4, + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Air gap volume (µL)', + error: null, + readOnly: true, + type: 'number', + value: 4, + }, + {} + ) + }) + + it('persists existing values if they are in state for dispense', () => { + props = { + ...props, + kind: 'dispense', + state: { + ...props.state, + airGapAspirate: 4, + airGapDispense: 16, + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Air gap volume (µL)', + error: null, + readOnly: true, + type: 'number', + value: 16, + }, + {} + ) + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/BlowOut.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/BlowOut.test.tsx new file mode 100644 index 00000000000..7e95308ee86 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/BlowOut.test.tsx @@ -0,0 +1,171 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { useNotifyDeckConfigurationQuery } from '../../../../resources/deck_configuration' +import { useTrackEventWithRobotSerial } from '../../../Devices/hooks' +import { BlowOut } from '../../QuickTransferAdvancedSettings/BlowOut' +import type { QuickTransferSummaryState } from '../../types' + +vi.mock('../../../../resources/deck_configuration') +vi.mock('../../../Devices/hooks') +vi.mock('../utils') + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +let mockTrackEventWithRobotSerial: any + +describe('BlowOut', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + kind: 'aspirate', + onBack: vi.fn(), + state: { + mount: 'left', + pipette: { + channels: 1, + liquids: [ + { + maxVolume: 1000, + minVolume: 5, + }, + ] as any, + } as any, + tipRack: { + wells: { + A1: { + totalLiquidVolume: 200, + }, + } as any, + } as any, + sourceWells: ['A1'], + destinationWells: ['A1'], + transferType: 'transfer', + volume: 20, + path: 'single', + } as QuickTransferSummaryState, + dispatch: vi.fn(), + } + mockTrackEventWithRobotSerial = vi.fn( + () => new Promise(resolve => resolve({})) + ) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEventWithRobotSerial, + }) + vi.mocked(useNotifyDeckConfigurationQuery).mockReturnValue({ + data: [ + { + cutoutId: 'cutoutC3', + cutoutFixtureId: 'wasteChuteRightAdapterCovered', + }, + { + cutoutId: 'cutoutA3', + cutoutFixtureId: 'trashBinAdapter', + }, + ], + } as any) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the first blow out screen, continue, and back buttons', () => { + render(props) + screen.getByText('Blowout after dispensing') + screen.getByTestId('ChildNavigation_Primary_Button') + screen.getByText('Enabled') + screen.getByText('Disabled') + const exitBtn = screen.getByTestId('ChildNavigation_Back_Button') + fireEvent.click(exitBtn) + expect(props.onBack).toHaveBeenCalled() + }) + + it('calls dispatch button if you select disabled and save', () => { + render(props) + const disabledBtn = screen.getByText('Disabled') + fireEvent.click(disabledBtn) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + }) + + it('second screen renders both source and destination wells and deck config trash options for transfer', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + screen.getByText('Source well') + screen.getByText('Destination well') + screen.getByText('Trash bin in A3') + screen.getByText('Waste chute in C3') + }) + + it('second screen renders trash bin in A3 if deck config is empty', () => { + vi.mocked(useNotifyDeckConfigurationQuery).mockReturnValue({ + data: [] as any, + } as any) + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + screen.getByText('Trash bin in A3') + }) + + it('second screen renders source well but not dest well for distribute', () => { + props = { + ...props, + state: { + ...props.state, + transferType: 'distribute', + }, + } + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + screen.getByText('Source well') + expect(screen.queryByText('Destination well')).not.toBeInTheDocument() + }) + + it('second screen renders dest well but not source well for consolidate', () => { + props = { + ...props, + state: { + ...props.state, + transferType: 'consolidate', + }, + } + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + screen.getByText('Destination well') + expect(screen.queryByText('Source well')).not.toBeInTheDocument() + }) + + it('enables save button when you make a destination selection and calls dispatch when saved', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + const destBtn = screen.getByText('Destination well') + fireEvent.click(destBtn) + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Delay.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Delay.test.tsx new file mode 100644 index 00000000000..1d0ac5f2656 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Delay.test.tsx @@ -0,0 +1,292 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { InputField } from '../../../../atoms/InputField' +import { useTrackEventWithRobotSerial } from '../../../Devices/hooks' +import { Delay } from '../../QuickTransferAdvancedSettings/Delay' +import type { QuickTransferSummaryState } from '../../types' + +vi.mock('../../../Devices/hooks') +vi.mock('../utils') + +vi.mock('../../../../atoms/InputField', async importOriginal => { + const actualComponents = await importOriginal() + return { + ...actualComponents, + InputField: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +let mockTrackEventWithRobotSerial: any + +describe('Delay', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onBack: vi.fn(), + kind: 'aspirate', + state: { + mount: 'left', + pipette: { + channels: 1, + liquids: [ + { + maxVolume: 1000, + minVolume: 5, + }, + ] as any, + } as any, + source: { + wells: { + A1: { + totalLiquidVolume: 200, + depth: 50, + }, + } as any, + } as any, + destination: { + wells: { + A1: { + totalLiquidVolume: 200, + depth: 200, + }, + } as any, + } as any, + sourceWells: ['A1'], + destinationWells: ['A1'], + transferType: 'transfer', + volume: 20, + path: 'single', + } as QuickTransferSummaryState, + dispatch: vi.fn(), + } + mockTrackEventWithRobotSerial = vi.fn( + () => new Promise(resolve => resolve({})) + ) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEventWithRobotSerial, + }) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the first delay screen, continue, and back buttons', () => { + render(props) + screen.getByText('Delay before aspirating') + screen.getByTestId('ChildNavigation_Primary_Button') + screen.getByText('Enabled') + screen.getByText('Disabled') + const exitBtn = screen.getByTestId('ChildNavigation_Back_Button') + fireEvent.click(exitBtn) + expect(props.onBack).toHaveBeenCalled() + }) + + it('renders save button if you select enabled, then moves to second screen', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Delay duration (seconds)', + error: null, + readOnly: true, + type: 'number', + value: null, + }, + {} + ) + }) + + it('calls dispatch button if you select disabled and save', () => { + render(props) + const disabledBtn = screen.getByText('Disabled') + fireEvent.click(disabledBtn) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('has correct delay duration range', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const oneButton = screen.getByText('0') + fireEvent.click(oneButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Delay duration (seconds)', + error: 'Value must be between 1-9999999999', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const nextBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(nextBtn).toBeDisabled() + }) + + it('has correct range for delay height for aspirate', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const oneButton = screen.getByText('1') + fireEvent.click(oneButton) + const nextBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(nextBtn) + const zeroButton = screen.getByText('0') + fireEvent.click(zeroButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Delay position from bottom of well (mm)', + error: 'Value must be between 1-100', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('has correct range for delay height for dispense', () => { + props = { + ...props, + kind: 'dispense', + } + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const oneButton = screen.getByText('1') + fireEvent.click(oneButton) + const nextBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(nextBtn) + const zeroButton = screen.getByText('0') + fireEvent.click(zeroButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Delay position from bottom of well (mm)', + error: 'Value must be between 1-400', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('calls dispatch when an in range value is entered and saved', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const oneButton = screen.getByText('1') + fireEvent.click(oneButton) + const nextBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(nextBtn) + const twoButton = screen.getByText('2') + fireEvent.click(twoButton) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('persists previously set value saved in state for aspirate', () => { + props = { + ...props, + state: { + ...props.state, + delayAspirate: { + delayDuration: 15, + positionFromBottom: 55, + }, + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Delay duration (seconds)', + error: null, + readOnly: true, + type: 'number', + value: 15, + }, + {} + ) + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Delay position from bottom of well (mm)', + error: null, + readOnly: true, + type: 'number', + value: 55, + }, + {} + ) + }) + + it('persists previously set value saved in state for dispense', () => { + props = { + ...props, + kind: 'dispense', + state: { + ...props.state, + delayDispense: { + delayDuration: 20, + positionFromBottom: 84, + }, + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Delay duration (seconds)', + error: null, + readOnly: true, + type: 'number', + value: 20, + }, + {} + ) + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Delay position from bottom of well (mm)', + error: null, + readOnly: true, + type: 'number', + value: 84, + }, + {} + ) + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/FlowRate.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/FlowRate.test.tsx new file mode 100644 index 00000000000..90b87fd5f4f --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/FlowRate.test.tsx @@ -0,0 +1,157 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { InputField } from '../../../../atoms/InputField' +import { useTrackEventWithRobotSerial } from '../../../Devices/hooks' +import { FlowRateEntry } from '../../QuickTransferAdvancedSettings/FlowRate' +import type { QuickTransferSummaryState } from '../../types' + +vi.mock('../../../Devices/hooks') +vi.mock('../utils') + +vi.mock('../../../../atoms/InputField', async importOriginal => { + const actualComponents = await importOriginal() + return { + ...actualComponents, + InputField: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +let mockTrackEventWithRobotSerial: any + +describe('FlowRate', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onBack: vi.fn(), + kind: 'aspirate', + state: { + mount: 'left', + pipette: { + model: 'p50', + channels: 1, + liquids: { + default: { + maxVolume: 1000, + minVolume: 5, + supportedTips: { + t50: { + uiMaxFlowRate: 92, + defaultAspirateFlowRate: { + default: 30, + }, + defaultDispenseFlowRate: { + default: 80, + }, + }, + }, + }, + } as any, + } as any, + tipRack: { + wells: { + A1: { + totalLiquidVolume: 50, + }, + } as any, + } as any, + sourceWells: ['A1'], + destinationWells: ['A1'], + transferType: 'transfer', + volume: 20, + path: 'single', + aspirateFlowRate: 35, + dispenseFlowRate: 62, + } as QuickTransferSummaryState, + dispatch: vi.fn(), + } + mockTrackEventWithRobotSerial = vi.fn( + () => new Promise(resolve => resolve({})) + ) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEventWithRobotSerial, + }) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the flow rate aspirate screen, continue, and back buttons', () => { + render(props) + screen.getByText('Aspirate flow rate') + screen.getByTestId('ChildNavigation_Primary_Button') + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Aspirate flow rate (µL/s)', + error: null, + readOnly: true, + type: 'number', + value: 35, + }, + {} + ) + const exitBtn = screen.getByTestId('ChildNavigation_Back_Button') + fireEvent.click(exitBtn) + expect(props.onBack).toHaveBeenCalled() + }) + + it('renders the flow rate dispense screen', () => { + props = { + ...props, + kind: 'dispense', + } + render(props) + screen.getByText('Dispense flow rate') + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Dispense flow rate (µL/s)', + error: null, + readOnly: true, + type: 'number', + value: 62, + }, + {} + ) + }) + + it('renders correct range if you enter incorrect value', () => { + render(props) + const deleteBtn = screen.getByText('del') + fireEvent.click(deleteBtn) + fireEvent.click(deleteBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Aspirate flow rate (µL/s)', + error: 'Value must be between 1-92', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('calls dispatch when an in range value is entered and saved', () => { + render(props) + const deleteBtn = screen.getByText('del') + fireEvent.click(deleteBtn) + fireEvent.click(deleteBtn) + const numButton = screen.getByText('1') + fireEvent.click(numButton) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Mix.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Mix.test.tsx new file mode 100644 index 00000000000..f3998287c20 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/Mix.test.tsx @@ -0,0 +1,262 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { InputField } from '../../../../atoms/InputField' +import { useTrackEventWithRobotSerial } from '../../../Devices/hooks' +import { Mix } from '../../QuickTransferAdvancedSettings/Mix' +import type { QuickTransferSummaryState } from '../../types' + +vi.mock('../../../Devices/hooks') +vi.mock('../utils') + +vi.mock('../../../../atoms/InputField', async importOriginal => { + const actualComponents = await importOriginal() + return { + ...actualComponents, + InputField: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +let mockTrackEventWithRobotSerial: any + +describe('Mix', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onBack: vi.fn(), + kind: 'aspirate', + state: { + mount: 'left', + pipette: { + channels: 1, + liquids: [ + { + maxVolume: 1000, + minVolume: 5, + }, + ] as any, + } as any, + tipRack: { + wells: { + A1: { + totalLiquidVolume: 200, + }, + } as any, + } as any, + sourceWells: ['A1'], + destinationWells: ['A1'], + transferType: 'transfer', + volume: 20, + path: 'single', + } as QuickTransferSummaryState, + dispatch: vi.fn(), + } + mockTrackEventWithRobotSerial = vi.fn( + () => new Promise(resolve => resolve({})) + ) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEventWithRobotSerial, + }) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the first Mix screen, continue, and back buttons', () => { + render(props) + screen.getByText('Mix before aspirating') + screen.getByTestId('ChildNavigation_Primary_Button') + screen.getByText('Enabled') + screen.getByText('Disabled') + const exitBtn = screen.getByTestId('ChildNavigation_Back_Button') + fireEvent.click(exitBtn) + expect(props.onBack).toHaveBeenCalled() + }) + + it('renders the different copy for Mix on dispense', () => { + props = { + ...props, + kind: 'dispense', + } + render(props) + screen.getByText('Mix before dispensing') + }) + + it('renders save button if you select enabled, then moves to second screen', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Mix volume (µL)', + error: null, + readOnly: true, + type: 'number', + value: null, + }, + {} + ) + }) + + it('calls dispatch button if you select disabled and save', () => { + render(props) + const disabledBtn = screen.getByText('Disabled') + fireEvent.click(disabledBtn) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('has correct Mix volume range', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const oneButton = screen.getByText('0') + fireEvent.click(oneButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Mix volume (µL)', + error: 'Value must be between 1-200', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const nextBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(nextBtn).toBeDisabled() + }) + + it('has correct range for Mix repitition range', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const oneButton = screen.getByText('1') + fireEvent.click(oneButton) + const nextBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(nextBtn) + const zeroButton = screen.getByText('0') + fireEvent.click(zeroButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Mix repetitions', + error: 'Value must be between 1-999', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('calls dispatch when an in range value is entered and saved', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const oneButton = screen.getByText('1') + fireEvent.click(oneButton) + const nextBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(nextBtn) + const twoButton = screen.getByText('2') + fireEvent.click(twoButton) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('persists previously set value saved in state for aspirate', () => { + props = { + ...props, + state: { + ...props.state, + mixOnAspirate: { + mixVolume: 15, + repititions: 55, + }, + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Mix volume (µL)', + error: null, + readOnly: true, + type: 'number', + value: 15, + }, + {} + ) + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Mix repetitions', + error: null, + readOnly: true, + type: 'number', + value: 55, + }, + {} + ) + }) + + it('persists previously set value saved in state for dispense', () => { + props = { + ...props, + kind: 'dispense', + state: { + ...props.state, + mixOnDispense: { + mixVolume: 18, + repititions: 2, + }, + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Mix volume (µL)', + error: null, + readOnly: true, + type: 'number', + value: 18, + }, + {} + ) + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Mix repetitions', + error: null, + readOnly: true, + type: 'number', + value: 2, + }, + {} + ) + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/PipettePath.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/PipettePath.test.tsx new file mode 100644 index 00000000000..79bbb82d44e --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/PipettePath.test.tsx @@ -0,0 +1,229 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { InputField } from '../../../../atoms/InputField' +import { useTrackEventWithRobotSerial } from '../../../Devices/hooks' +import { PipettePath } from '../../QuickTransferAdvancedSettings/PipettePath' +import { useBlowOutLocationOptions } from '../../QuickTransferAdvancedSettings/BlowOut' +import type { QuickTransferSummaryState } from '../../types' + +vi.mock('../../../Devices/hooks') +vi.mock('../utils') +vi.mock('../../QuickTransferAdvancedSettings/BlowOut') + +vi.mock('../../../../atoms/InputField', async importOriginal => { + const actualComponents = await importOriginal() + return { + ...actualComponents, + InputField: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +let mockTrackEventWithRobotSerial: any + +describe('PipettePath', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onBack: vi.fn(), + state: { + mount: 'left', + pipette: { + channels: 1, + liquids: [ + { + maxVolume: 1000, + minVolume: 5, + }, + ] as any, + } as any, + tipRack: { + wells: { + A1: { + totalLiquidVolume: 200, + }, + } as any, + } as any, + transferType: 'consolidate', + volume: 20, + path: 'multiAspirate', + } as QuickTransferSummaryState, + dispatch: vi.fn(), + } + mockTrackEventWithRobotSerial = vi.fn( + () => new Promise(resolve => resolve({})) + ) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEventWithRobotSerial, + }) + vi.mocked(useBlowOutLocationOptions).mockReturnValue([ + { + location: 'source_well', + description: 'Source well', + }, + ]) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the first pipette path screen, continue, back buttons', () => { + render(props) + screen.getByText('Pipette path') + screen.getByTestId('ChildNavigation_Primary_Button') + const exitBtn = screen.getByTestId('ChildNavigation_Back_Button') + fireEvent.click(exitBtn) + expect(props.onBack).toHaveBeenCalled() + }) + + it('renders multi aspirate and single options for consolidate if there is room in the tip', () => { + render(props) + screen.getByText('Single transfers') + screen.getByText('Multi-aspirate') + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('renders single option only for consolidate if there is not room in the tip', () => { + props = { + ...props, + state: { + ...props.state, + volume: 101, + }, + } + render(props) + screen.getByText('Single transfers') + expect(screen.queryByText('Multi-aspirate')).not.toBeInTheDocument() + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('renders multi dispense and single options for distribute if there is room in the tip', () => { + props = { + ...props, + state: { + ...props.state, + transferType: 'distribute', + }, + } + render(props) + screen.getByText('Single transfers') + screen.getByText('Multi-dispense') + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('renders single option only for distribute if there is not room in the tip', () => { + props = { + ...props, + state: { + ...props.state, + transferType: 'distribute', + volume: 67, + }, + } + render(props) + screen.getByText('Single transfers') + expect(screen.queryByText('Multi-dispense')).not.toBeInTheDocument() + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('renders next cta and disposal volume screen if you choose multi dispense', () => { + props = { + ...props, + state: { + ...props.state, + transferType: 'distribute', + disposalVolume: 20, + blowOut: 'source_well', + }, + } + render(props) + const multiDispenseBtn = screen.getByText('Multi-dispense') + fireEvent.click(multiDispenseBtn) + const continueBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(continueBtn) + + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Disposal volume (µL)', + error: null, + readOnly: true, + type: 'number', + value: 20, + }, + {} + ) + }) + + it('renders error on disposal volume screen if you select an out of range value', () => { + props = { + ...props, + state: { + ...props.state, + transferType: 'distribute', + path: 'multiDispense', + disposalVolume: 20, + blowOut: 'source_well', + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const oneButton = screen.getByText('1') + fireEvent.click(oneButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Disposal volume (µL)', + error: 'Value must be between 1-160', + readOnly: true, + type: 'number', + value: 201, + }, + {} + ) + const nextBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(nextBtn).toBeDisabled() + }) + + it('renders blowout options on third screen and calls dispatch when saved', () => { + props = { + ...props, + state: { + ...props.state, + transferType: 'distribute', + path: 'multiDispense', + disposalVolume: 20, + blowOut: 'source_well', + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + fireEvent.click(continueBtn) + screen.getByText('Source well') + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx new file mode 100644 index 00000000000..a5cc00c9c7a --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TipPosition.test.tsx @@ -0,0 +1,176 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { InputField } from '../../../../atoms/InputField' +import { useTrackEventWithRobotSerial } from '../../../Devices/hooks' +import { TipPositionEntry } from '../../QuickTransferAdvancedSettings/TipPosition' +import type { QuickTransferSummaryState } from '../../types' + +vi.mock('../../../Devices/hooks') +vi.mock('../utils') + +vi.mock('../../../../atoms/InputField', async importOriginal => { + const actualComponents = await importOriginal() + return { + ...actualComponents, + InputField: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +let mockTrackEventWithRobotSerial: any + +describe('TipPosition', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onBack: vi.fn(), + kind: 'aspirate', + state: { + mount: 'left', + pipette: { + channels: 1, + liquids: [ + { + maxVolume: 1000, + minVolume: 5, + }, + ] as any, + } as any, + source: { + wells: { + A1: { + totalLiquidVolume: 200, + depth: 50, + }, + } as any, + } as any, + destination: { + wells: { + A1: { + totalLiquidVolume: 200, + depth: 200, + }, + } as any, + } as any, + sourceWells: ['A1'], + destinationWells: ['A1'], + transferType: 'transfer', + volume: 20, + path: 'single', + tipPositionAspirate: 10, + tipPositionDispense: 75, + } as QuickTransferSummaryState, + dispatch: vi.fn(), + } + mockTrackEventWithRobotSerial = vi.fn( + () => new Promise(resolve => resolve({})) + ) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEventWithRobotSerial, + }) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the tip position aspirate screen, continue, and back buttons', () => { + render(props) + screen.getByText('Aspirate tip position') + screen.getByTestId('ChildNavigation_Primary_Button') + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Distance from bottom of well (mm)', + error: null, + readOnly: true, + type: 'text', + value: 10, + }, + {} + ) + const exitBtn = screen.getByTestId('ChildNavigation_Back_Button') + fireEvent.click(exitBtn) + expect(props.onBack).toHaveBeenCalled() + }) + + it('renders the tip position dispense screen', () => { + props = { + ...props, + kind: 'dispense', + } + render(props) + screen.getByText('Dispense tip position') + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Distance from bottom of well (mm)', + error: null, + readOnly: true, + type: 'text', + value: 75, + }, + {} + ) + }) + + it('renders correct range if you enter incorrect value for aspirate', () => { + render(props) + const deleteBtn = screen.getByText('del') + fireEvent.click(deleteBtn) + fireEvent.click(deleteBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Distance from bottom of well (mm)', + error: 'Value must be between 1-100', + readOnly: true, + type: 'text', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('renders correct range if you enter incorrect value for dispense', () => { + props = { + ...props, + kind: 'dispense', + } + render(props) + const deleteBtn = screen.getByText('del') + fireEvent.click(deleteBtn) + fireEvent.click(deleteBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Distance from bottom of well (mm)', + error: 'Value must be between 1-400', + readOnly: true, + type: 'text', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('calls dispatch when an in range value is entered and saved', () => { + render(props) + const deleteBtn = screen.getByText('del') + fireEvent.click(deleteBtn) + const numButton = screen.getByText('1') + fireEvent.click(numButton) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) +}) diff --git a/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TouchTip.test.tsx b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TouchTip.test.tsx new file mode 100644 index 00000000000..e05b91648a2 --- /dev/null +++ b/app/src/organisms/QuickTransferFlow/__tests__/QuickTransferAdvancedSettings/TouchTip.test.tsx @@ -0,0 +1,240 @@ +import * as React from 'react' +import { fireEvent, screen } from '@testing-library/react' +import { describe, it, expect, afterEach, vi, beforeEach } from 'vitest' + +import { renderWithProviders } from '../../../../__testing-utils__' +import { i18n } from '../../../../i18n' +import { InputField } from '../../../../atoms/InputField' +import { useTrackEventWithRobotSerial } from '../../../Devices/hooks' +import { TouchTip } from '../../QuickTransferAdvancedSettings/TouchTip' +import type { QuickTransferSummaryState } from '../../types' + +vi.mock('../../../Devices/hooks') +vi.mock('../utils') + +vi.mock('../../../../atoms/InputField', async importOriginal => { + const actualComponents = await importOriginal() + return { + ...actualComponents, + InputField: vi.fn(), + } +}) + +const render = (props: React.ComponentProps) => { + return renderWithProviders(, { + i18nInstance: i18n, + }) +} +let mockTrackEventWithRobotSerial: any + +describe('TouchTip', () => { + let props: React.ComponentProps + + beforeEach(() => { + props = { + onBack: vi.fn(), + kind: 'aspirate', + state: { + mount: 'left', + pipette: { + channels: 1, + liquids: [ + { + maxVolume: 1000, + minVolume: 5, + }, + ] as any, + } as any, + source: { + wells: { + A1: { + totalLiquidVolume: 200, + depth: 50, + }, + } as any, + } as any, + destination: { + wells: { + A1: { + totalLiquidVolume: 200, + depth: 200, + }, + } as any, + } as any, + sourceWells: ['A1'], + destinationWells: ['A1'], + transferType: 'transfer', + volume: 20, + path: 'single', + } as QuickTransferSummaryState, + dispatch: vi.fn(), + } + mockTrackEventWithRobotSerial = vi.fn( + () => new Promise(resolve => resolve({})) + ) + vi.mocked(useTrackEventWithRobotSerial).mockReturnValue({ + trackEventWithRobotSerial: mockTrackEventWithRobotSerial, + }) + }) + afterEach(() => { + vi.resetAllMocks() + }) + + it('renders the first touch tip screen, continue, and back buttons', () => { + render(props) + screen.getByText('Touch tip before aspirating') + screen.getByTestId('ChildNavigation_Primary_Button') + screen.getByText('Enabled') + screen.getByText('Disabled') + const exitBtn = screen.getByTestId('ChildNavigation_Back_Button') + fireEvent.click(exitBtn) + expect(props.onBack).toHaveBeenCalled() + }) + + it('renders the correct copy for dispense', () => { + props = { + ...props, + kind: 'dispense', + } + render(props) + screen.getByText('Touch tip before dispensing') + }) + + it('renders save button if you select enabled, then moves to second screen', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Touch tip position from bottom of well (mm)', + error: null, + readOnly: true, + type: 'number', + value: null, + }, + {} + ) + }) + + it('calls dispatch button if you select disabled and save', () => { + render(props) + const disabledBtn = screen.getByText('Disabled') + fireEvent.click(disabledBtn) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('has correct range for aspirate', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const numButton = screen.getByText('0') + fireEvent.click(numButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Touch tip position from bottom of well (mm)', + error: 'Value must be between 25-50', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('has correct range for dispense', () => { + props = { + ...props, + kind: 'dispense', + } + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const numButton = screen.getByText('0') + fireEvent.click(numButton) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Touch tip position from bottom of well (mm)', + error: 'Value must be between 100-200', + readOnly: true, + type: 'number', + value: 0, + }, + {} + ) + const saveBtn = screen.getByTestId('ChildNavigation_Primary_Button') + expect(saveBtn).toBeDisabled() + }) + + it('calls dispatch when an in range value is entered and saved', () => { + render(props) + const enabledBtn = screen.getByText('Enabled') + fireEvent.click(enabledBtn) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + const numButton = screen.getByText('4') + fireEvent.click(numButton) + fireEvent.click(numButton) + const saveBtn = screen.getByText('Save') + fireEvent.click(saveBtn) + expect(props.dispatch).toHaveBeenCalled() + expect(mockTrackEventWithRobotSerial).toHaveBeenCalled() + }) + + it('renders previously set value saved in state for aspirate', () => { + props = { + ...props, + state: { + ...props.state, + touchTipAspirate: 32, + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Touch tip position from bottom of well (mm)', + error: null, + readOnly: true, + type: 'number', + value: 32, + }, + {} + ) + }) + + it('renders previously set value saved in state for dispense', () => { + props = { + ...props, + kind: 'dispense', + state: { + ...props.state, + touchTipDispense: 118, + }, + } + render(props) + const continueBtn = screen.getByText('Continue') + fireEvent.click(continueBtn) + expect(vi.mocked(InputField)).toHaveBeenCalledWith( + { + title: 'Touch tip position from bottom of well (mm)', + error: null, + readOnly: true, + type: 'number', + value: 118, + }, + {} + ) + }) +})