diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index dfd05979524..568859d205d 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -14,6 +14,7 @@ import { WRAP, } from '@opentrons/components' import { + ABSORBANCE_READER_TYPE, ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, getModuleDisplayName, @@ -70,6 +71,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { TEMPERATURE_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, + ABSORBANCE_READER_TYPE, ] const handleAddModule = ( diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index 4c41a8f8464..2f29fbd416c 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -18,6 +18,7 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { + ABSORBANCE_READER_TYPE, ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, @@ -36,6 +37,7 @@ import { deleteDeckFixture, } from '../../../step-forms/actions/additionalItems' import { createModule, deleteModule } from '../../../step-forms/actions' +import { getAdditionalEquipment } from '../../../step-forms/selectors' import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { createContainer, @@ -110,6 +112,10 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const [changeModuleWarningInfo, displayModuleWarning] = useState( false ) + const additionalEquipment = useSelector(getAdditionalEquipment) + const isGripperAttached = Object.values(additionalEquipment).some( + equipment => equipment?.name === 'gripper' + ) const [selectedHardware, setSelectedHardware] = useState< ModuleModel | Fixture | null >(null) @@ -320,13 +326,20 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { } if (selectedModuleModel != null) { // create module - dispatch( - createModule({ - slot, - type: getModuleType(selectedModuleModel), - model: selectedModuleModel, - }) - ) + const moduleType = getModuleType(selectedModuleModel) + // enforce gripper present in order to add plate reader + if (moduleType === ABSORBANCE_READER_TYPE && !isGripperAttached) { + makeSnackbar('Gripper required to add absorbance reader') + return + } else { + dispatch( + createModule({ + slot, + type: moduleType, + model: selectedModuleModel, + }) + ) + } } if ( (slot === 'offDeck' && selectedLabwareDefUri != null) || diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index 5eab480710e..6a0c7d03c87 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -2,6 +2,7 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { fireEvent, screen } from '@testing-library/react' import { + ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, HEATERSHAKER_MODULE_V1, fixture96Plate, @@ -9,7 +10,9 @@ import { import { i18n } from '../../../../assets/localization' import { renderWithProviders } from '../../../../__testing-utils__' import { deleteContainer } from '../../../../labware-ingred/actions' +import { useKitchen } from '../../../../organisms/Kitchen/hooks' import { deleteModule } from '../../../../step-forms/actions' +import { getAdditionalEquipment } from '../../../../step-forms/selectors' import { getRobotType } from '../../../../file-data/selectors' import { getEnableAbsorbanceReader } from '../../../../feature-flags/selectors' import { deleteDeckFixture } from '../../../../step-forms/actions/additionalItems' @@ -31,12 +34,16 @@ vi.mock('../../../../step-forms/actions') vi.mock('../../../../step-forms/actions/additionalItems') vi.mock('../../../../labware-ingred/selectors') vi.mock('../../../../tutorial/selectors') +vi.mock('../../../../step-forms/selectors') +vi.mock('../../../../organisms/Kitchen/hooks') const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, })[0] } +const mockMakeSnackbar = vi.fn() + describe('DeckSetupTools', () => { let props: React.ComponentProps @@ -66,6 +73,12 @@ describe('DeckSetupTools', () => { pipettes: {}, }) vi.mocked(getDismissedHints).mockReturnValue([]) + vi.mocked(getAdditionalEquipment).mockReturnValue({}) + vi.mocked(useKitchen).mockReturnValue({ + makeSnackbar: mockMakeSnackbar, + bakeToast: vi.fn(), + eatToast: vi.fn(), + }) }) afterEach(() => { vi.resetAllMocks() @@ -164,4 +177,32 @@ describe('DeckSetupTools', () => { fireEvent.click(screen.getByText('Done')) expect(props.onCloseClick).toHaveBeenCalled() }) + it('should save plate reader if gripper configured', () => { + vi.mocked(getAdditionalEquipment).mockReturnValue({ + gripperUri: { name: 'gripper', id: 'gripperId' }, + }) + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: ABSORBANCE_READER_V1, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + render(props) + fireEvent.click(screen.getByText('Done')) + expect(props.onCloseClick).toHaveBeenCalled() + }) + it('should prevent saving plate reader and make toast if gripper not configured', () => { + vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ + selectedLabwareDefUri: null, + selectedNestedLabwareDefUri: null, + selectedFixture: null, + selectedModuleModel: ABSORBANCE_READER_V1, + selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, + }) + render(props) + fireEvent.click(screen.getByText('Done')) + expect(props.onCloseClick).not.toHaveBeenCalled() + expect(mockMakeSnackbar).toHaveBeenCalled() + }) })