- {protocolRunDetailsContent}
+ {content}
+ {backToTop}
>
)
}
diff --git a/app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx b/app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx
new file mode 100644
index 00000000000..1757704e597
--- /dev/null
+++ b/app/src/pages/ProtocolSetup/ConfirmSetupStepsCompleteModal.tsx
@@ -0,0 +1,68 @@
+import * as React from 'react'
+import { useTranslation } from 'react-i18next'
+
+import {
+ DIRECTION_COLUMN,
+ Flex,
+ SPACING,
+ LegacyStyledText,
+} from '@opentrons/components'
+
+import { SmallButton } from '../../atoms/buttons'
+import { Modal } from '../../molecules/Modal'
+
+import type { ModalHeaderBaseProps } from '../../molecules/Modal/types'
+
+interface ConfirmSetupStepsCompleteModalProps {
+ onCloseClick: () => void
+ onConfirmClick: () => void
+ missingSteps: string[]
+}
+
+export function ConfirmSetupStepsCompleteModal({
+ onCloseClick,
+ missingSteps,
+ onConfirmClick,
+}: ConfirmSetupStepsCompleteModalProps): JSX.Element {
+ const { i18n, t } = useTranslation(['protocol_setup', 'shared'])
+ const modalHeader: ModalHeaderBaseProps = {
+ title: t('are_you_sure_you_want_to_proceed'),
+ hasExitIcon: true,
+ }
+
+ const handleStartRun = (): void => {
+ onConfirmClick()
+ onCloseClick()
+ }
+
+ return (
+
+
+
+ {t('you_havent_confirmed', {
+ missingSteps: new Intl.ListFormat('en', {
+ style: 'short',
+ type: 'conjunction',
+ }).format(missingSteps),
+ })}
+
+
+ {
+ onCloseClick()
+ }}
+ />
+
+
+
+
+ )
+}
diff --git a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx
index 1be58ae82f8..5479f4693bd 100644
--- a/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx
+++ b/app/src/pages/ProtocolSetup/__tests__/ProtocolSetup.test.tsx
@@ -2,7 +2,7 @@ import * as React from 'react'
import { Route, MemoryRouter, Routes } from 'react-router-dom'
import { fireEvent, screen } from '@testing-library/react'
import { when } from 'vitest-when'
-import { vi, it, describe, expect, beforeEach, afterEach } from 'vitest'
+import { vi, it, describe, expect, beforeEach } from 'vitest'
import { RUN_STATUS_IDLE, RUN_STATUS_STOPPED } from '@opentrons/api-client'
import {
@@ -39,10 +39,13 @@ import { ANALYTICS_PROTOCOL_RUN_ACTION } from '../../../redux/analytics'
import { ProtocolSetupLiquids } from '../../../organisms/ProtocolSetupLiquids'
import { getProtocolModulesInfo } from '../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo'
import { ProtocolSetupModulesAndDeck } from '../../../organisms/ProtocolSetupModulesAndDeck'
+import { ProtocolSetupLabware } from '../../../organisms/ProtocolSetupLabware'
+import { ProtocolSetupOffsets } from '../../../organisms/ProtocolSetupOffsets'
import { getUnmatchedModulesForProtocol } from '../../../organisms/ProtocolSetupModulesAndDeck/utils'
import { useLaunchLPC } from '../../../organisms/LabwarePositionCheck/useLaunchLPC'
import { ConfirmCancelRunModal } from '../../../organisms/OnDeviceDisplay/RunningProtocol'
import { mockProtocolModuleInfo } from '../../../organisms/ProtocolSetupInstruments/__fixtures__'
+import { getIncompleteInstrumentCount } from '../../../organisms/ProtocolSetupInstruments/utils'
import {
useProtocolHasRunTimeParameters,
useRunControls,
@@ -51,6 +54,7 @@ import {
import { useIsHeaterShakerInProtocol } from '../../../organisms/ModuleCard/hooks'
import { useDeckConfigurationCompatibility } from '../../../resources/deck_configuration/hooks'
import { ConfirmAttachedModal } from '../../../pages/ProtocolSetup/ConfirmAttachedModal'
+import { ConfirmSetupStepsCompleteModal } from '../../../pages/ProtocolSetup/ConfirmSetupStepsCompleteModal'
import { ProtocolSetup } from '../../../pages/ProtocolSetup'
import { useNotifyRunQuery } from '../../../resources/runs'
import { ViewOnlyParameters } from '../../../organisms/ProtocolSetupParameters/ViewOnlyParameters'
@@ -99,12 +103,15 @@ vi.mock('../../../organisms/ProtocolSetupParameters/ViewOnlyParameters')
vi.mock(
'../../../organisms/LabwarePositionCheck/useMostRecentCompletedAnalysis'
)
+vi.mock('../../../organisms/ProtocolSetupInstruments/utils')
vi.mock('../../../organisms/Devices/ProtocolRun/utils/getProtocolModulesInfo')
vi.mock('../../../organisms/ProtocolSetupModulesAndDeck')
vi.mock('../../../organisms/ProtocolSetupModulesAndDeck/utils')
vi.mock('../../../organisms/OnDeviceDisplay/RunningProtocol')
vi.mock('../../../organisms/RunTimeControl/hooks')
vi.mock('../../../organisms/ProtocolSetupLiquids')
+vi.mock('../../../organisms/ProtocolSetupLabware')
+vi.mock('../../../organisms/ProtocolSetupOffsets')
vi.mock('../../../organisms/ModuleCard/hooks')
vi.mock('../../../redux/discovery/selectors')
vi.mock('../ConfirmAttachedModal')
@@ -112,6 +119,7 @@ vi.mock('../../../organisms/ToasterOven')
vi.mock('../../../resources/deck_configuration/hooks')
vi.mock('../../../resources/runs')
vi.mock('../../../resources/deck_configuration')
+vi.mock('../ConfirmSetupStepsCompleteModal')
const render = (path = '/') => {
return renderWithProviders(
@@ -126,6 +134,12 @@ const render = (path = '/') => {
)
}
+const MockProtocolSetupLabware = vi.mocked(ProtocolSetupLabware)
+const MockProtocolSetupLiquids = vi.mocked(ProtocolSetupLiquids)
+const MockProtocolSetupOffsets = vi.mocked(ProtocolSetupOffsets)
+const MockConfirmSetupStepsCompleteModal = vi.mocked(
+ ConfirmSetupStepsCompleteModal
+)
const ROBOT_NAME = 'fake-robot-name'
const RUN_ID = 'my-run-id'
const ROBOT_SERIAL_NUMBER = 'OT123'
@@ -192,6 +206,30 @@ describe('ProtocolSetup', () => {
beforeEach(() => {
mockLaunchLPC = vi.fn()
mockNavigate = vi.fn()
+ MockProtocolSetupLiquids.mockImplementation(
+ vi.fn(({ setIsConfirmed, setSetupScreen }) => {
+ setIsConfirmed(true)
+ setSetupScreen('prepare to run')
+ return Mock ProtocolSetupLiquids
+ })
+ )
+ MockProtocolSetupLabware.mockImplementation(
+ vi.fn(({ setIsConfirmed, setSetupScreen }) => {
+ setIsConfirmed(true)
+ setSetupScreen('prepare to run')
+ return Mock ProtocolSetupLabware
+ })
+ )
+ MockProtocolSetupOffsets.mockImplementation(
+ vi.fn(({ setIsConfirmed, setSetupScreen }) => {
+ setIsConfirmed(true)
+ setSetupScreen('prepare to run')
+ return Mock ProtocolSetupOffsets
+ })
+ )
+ MockConfirmSetupStepsCompleteModal.mockReturnValue(
+ Mock ConfirmSetupStepsCompleteModal
+ )
vi.mocked(useLPCDisabledReason).mockReturnValue(null)
vi.mocked(useAttachedModules).mockReturnValue([])
vi.mocked(useModuleCalibrationStatus).mockReturnValue({ complete: true })
@@ -290,10 +328,6 @@ describe('ProtocolSetup', () => {
.thenReturn({ trackProtocolRunEvent: mockTrackProtocolRunEvent })
})
- afterEach(() => {
- vi.resetAllMocks()
- })
-
it('should render text, image, and buttons', () => {
render(`/runs/${RUN_ID}/setup/`)
screen.getByText('Prepare to run')
@@ -305,9 +339,47 @@ describe('ProtocolSetup', () => {
})
it('should play protocol when click play button', () => {
+ vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({
+ data: { ...mockRobotSideAnalysis, liquids: mockLiquids },
+ } as any)
+ when(vi.mocked(getProtocolModulesInfo))
+ .calledWith(
+ { ...mockRobotSideAnalysis, liquids: mockLiquids },
+ flexDeckDefV5 as any
+ )
+ .thenReturn(mockProtocolModuleInfo)
+ when(vi.mocked(getUnmatchedModulesForProtocol))
+ .calledWith([], mockProtocolModuleInfo)
+ .thenReturn({ missingModuleIds: [], remainingAttachedModules: [] })
+ vi.mocked(getIncompleteInstrumentCount).mockReturnValue(0)
+ MockProtocolSetupLiquids.mockImplementation(
+ vi.fn(({ setIsConfirmed, setSetupScreen }) => {
+ setIsConfirmed(true)
+ setSetupScreen('prepare to run')
+ return Mock ProtocolSetupLiquids
+ })
+ )
+ MockProtocolSetupLabware.mockImplementation(
+ vi.fn(({ setIsConfirmed, setSetupScreen }) => {
+ setIsConfirmed(true)
+ setSetupScreen('prepare to run')
+ return Mock ProtocolSetupLabware
+ })
+ )
+ MockProtocolSetupOffsets.mockImplementation(
+ vi.fn(({ setIsConfirmed, setSetupScreen }) => {
+ setIsConfirmed(true)
+ setSetupScreen('prepare to run')
+ return Mock ProtocolSetupOffsets
+ })
+ )
render(`/runs/${RUN_ID}/setup/`)
+ fireEvent.click(screen.getByText('Labware Position Check'))
+ fireEvent.click(screen.getByText('Labware'))
+ fireEvent.click(screen.getByText('Liquids'))
expect(mockPlay).toBeCalledTimes(0)
fireEvent.click(screen.getByRole('button', { name: 'play' }))
+ expect(MockConfirmSetupStepsCompleteModal).toBeCalledTimes(0)
expect(mockPlay).toBeCalledTimes(1)
})
@@ -348,7 +420,25 @@ describe('ProtocolSetup', () => {
render(`/runs/${RUN_ID}/setup/`)
screen.getByText('1 initial liquid')
fireEvent.click(screen.getByText('Liquids'))
- expect(vi.mocked(ProtocolSetupLiquids)).toHaveBeenCalled()
+ expect(MockProtocolSetupLiquids).toHaveBeenCalled()
+ })
+
+ it('should launch protocol setup labware screen when click labware', () => {
+ vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({
+ data: { ...mockRobotSideAnalysis, liquids: mockLiquids },
+ } as any)
+ when(vi.mocked(getProtocolModulesInfo))
+ .calledWith(
+ { ...mockRobotSideAnalysis, liquids: mockLiquids },
+ flexDeckDefV5 as any
+ )
+ .thenReturn(mockProtocolModuleInfo)
+ when(vi.mocked(getUnmatchedModulesForProtocol))
+ .calledWith([], mockProtocolModuleInfo)
+ .thenReturn({ missingModuleIds: [], remainingAttachedModules: [] })
+ render(`/runs/${RUN_ID}/setup`)
+ fireEvent.click(screen.getByTestId('SetupButton_Labware'))
+ expect(MockProtocolSetupLabware).toHaveBeenCalled()
})
it('should launch view only parameters screen when click parameters', () => {
@@ -376,14 +466,14 @@ describe('ProtocolSetup', () => {
expect(vi.mocked(ViewOnlyParameters)).toHaveBeenCalled()
})
- it('should launch LPC when clicked', () => {
- vi.mocked(useLPCDisabledReason).mockReturnValue(null)
+ it('should launch offsets screen when click offsets', () => {
+ MockProtocolSetupOffsets.mockImplementation(
+ vi.fn(() => Mock ProtocolSetupOffsets
)
+ )
render(`/runs/${RUN_ID}/setup/`)
- screen.getByText(/Recommended/)
- screen.getByText(/1 offset applied/)
fireEvent.click(screen.getByText('Labware Position Check'))
- expect(mockLaunchLPC).toHaveBeenCalled()
- screen.getByText('mock LPC Wizard')
+ expect(MockProtocolSetupOffsets).toHaveBeenCalled()
+ screen.getByText(/Mock ProtocolSetupOffsets/)
})
it('should render a confirmation modal when heater-shaker is in a protocol and it is not shaking', () => {
@@ -416,7 +506,21 @@ describe('ProtocolSetup', () => {
})
it('calls trackProtocolRunEvent when tapping play button', () => {
+ vi.mocked(useProtocolAnalysisAsDocumentQuery).mockReturnValue({
+ data: { ...mockRobotSideAnalysis, liquids: mockLiquids },
+ } as any)
+ when(vi.mocked(getProtocolModulesInfo))
+ .calledWith(
+ { ...mockRobotSideAnalysis, liquids: mockLiquids },
+ flexDeckDefV5 as any
+ )
+ .thenReturn(mockProtocolModuleInfo)
+ when(vi.mocked(getUnmatchedModulesForProtocol))
+ .calledWith([], mockProtocolModuleInfo)
+ .thenReturn({ missingModuleIds: [], remainingAttachedModules: [] })
+ vi.mocked(getIncompleteInstrumentCount).mockReturnValue(0)
render(`/runs/${RUN_ID}/setup/`)
+
fireEvent.click(screen.getByRole('button', { name: 'play' }))
expect(mockTrackProtocolRunEvent).toBeCalledTimes(1)
expect(mockTrackProtocolRunEvent).toHaveBeenCalledWith({
diff --git a/app/src/pages/ProtocolSetup/index.tsx b/app/src/pages/ProtocolSetup/index.tsx
index 36ce4220bcb..f152b0cc44a 100644
--- a/app/src/pages/ProtocolSetup/index.tsx
+++ b/app/src/pages/ProtocolSetup/index.tsx
@@ -60,6 +60,7 @@ import { getProtocolModulesInfo } from '../../organisms/Devices/ProtocolRun/util
import { ProtocolSetupLabware } from '../../organisms/ProtocolSetupLabware'
import { ProtocolSetupModulesAndDeck } from '../../organisms/ProtocolSetupModulesAndDeck'
import { ProtocolSetupLiquids } from '../../organisms/ProtocolSetupLiquids'
+import { ProtocolSetupOffsets } from '../../organisms/ProtocolSetupOffsets'
import { ProtocolSetupInstruments } from '../../organisms/ProtocolSetupInstruments'
import { ProtocolSetupDeckConfiguration } from '../../organisms/ProtocolSetupDeckConfiguration'
import { useLaunchLPC } from '../../organisms/LabwarePositionCheck/useLaunchLPC'
@@ -85,6 +86,7 @@ import {
} from '../../redux/analytics'
import { getIsHeaterShakerAttached } from '../../redux/config'
import { ConfirmAttachedModal } from './ConfirmAttachedModal'
+import { ConfirmSetupStepsCompleteModal } from './ConfirmSetupStepsCompleteModal'
import { getLatestCurrentOffsets } from '../../organisms/Devices/ProtocolRun/SetupLabwarePositionCheck/utils'
import { CloseButton, PlayButton } from './Buttons'
import { useDeckConfigurationCompatibility } from '../../resources/deck_configuration/hooks'
@@ -118,6 +120,8 @@ interface ProtocolSetupStepProps {
subDetail?: string | null
// disallow click handler, disabled styling
disabled?: boolean
+ // disallow click handler, don't show CTA icons, allow styling
+ interactionDisabled?: boolean
// display the reason the setup step is disabled
disabledReason?: string | null
// optional description
@@ -137,12 +141,14 @@ export function ProtocolSetupStep({
detail,
subDetail,
disabled = false,
+ interactionDisabled = false,
disabledReason,
description,
hasRightIcon = true,
hasLeftIcon = true,
fontSize = 'p',
}: ProtocolSetupStepProps): JSX.Element {
+ const isInteractionDisabled = interactionDisabled || disabled
const backgroundColorByStepStatus = {
ready: COLORS.green35,
'not ready': COLORS.yellow35,
@@ -185,9 +191,12 @@ export function ProtocolSetupStep({
return (
{
- !disabled ? onClickSetupStep() : makeDisabledReasonSnackbar()
+ !isInteractionDisabled
+ ? onClickSetupStep()
+ : makeDisabledReasonSnackbar()
}}
width="100%"
+ data-testid={`SetupButton_${title}`}
>
{detail}
@@ -249,7 +257,7 @@ export function ProtocolSetupStep({
{subDetail}
- {disabled || !hasRightIcon ? null : (
+ {interactionDisabled || !hasRightIcon ? null : (
>
confirmAttachment: () => void
+ confirmStepsComplete: () => void
play: () => void
robotName: string
runRecord: Run | null
+ labwareConfirmed: boolean
+ liquidsConfirmed: boolean
+ offsetsConfirmed: boolean
}
function PrepareToRun({
@@ -280,6 +292,10 @@ function PrepareToRun({
play,
robotName,
runRecord,
+ labwareConfirmed,
+ liquidsConfirmed,
+ offsetsConfirmed,
+ confirmStepsComplete,
}: PrepareToRunProps): JSX.Element {
const { t, i18n } = useTranslation(['protocol_setup', 'shared'])
const navigate = useNavigate()
@@ -335,7 +351,6 @@ function PrepareToRun({
}, [mostRecentAnalysis?.status])
const robotType = useRobotType(robotName)
- const { launchLPC, LPCWizard } = useLaunchLPC(runId, robotType, protocolName)
const onConfirmCancelClose = (): void => {
setShowConfirmCancelModal(false)
@@ -381,12 +396,7 @@ function PrepareToRun({
: null
const isMissingModules = missingModuleIds.length > 0
- const lpcDisabledReason = useLPCDisabledReason({
- runId,
- hasMissingModulesForOdd: isMissingModules,
- hasMissingCalForOdd:
- incompleteInstrumentCount != null && incompleteInstrumentCount > 0,
- })
+
const moduleCalibrationStatus = useModuleCalibrationStatus(robotName, runId)
const runTimeParameters = mostRecentAnalysis?.runTimeParameters ?? []
@@ -510,24 +520,25 @@ function PrepareToRun({
if (isDoorOpen) {
makeSnackbar(t('shared:close_robot_door') as string)
} else {
- if (
- isHeaterShakerInProtocol &&
- isReadyToRun &&
- runStatus === RUN_STATUS_IDLE
- ) {
- confirmAttachment()
- } else {
- if (isReadyToRun) {
+ if (isReadyToRun) {
+ if (runStatus === RUN_STATUS_IDLE && isHeaterShakerInProtocol) {
+ confirmAttachment()
+ } else if (
+ runStatus === RUN_STATUS_IDLE &&
+ !(labwareConfirmed && offsetsConfirmed && liquidsConfirmed)
+ ) {
+ confirmStepsComplete()
+ } else {
play()
trackProtocolRunEvent({
name: ANALYTICS_PROTOCOL_RUN_ACTION.START,
properties: robotAnalyticsData ?? {},
})
- } else {
- makeSnackbar(
- i18n.format(t('complete_setup_before_proceeding'), 'capitalize')
- )
}
+ } else {
+ makeSnackbar(
+ i18n.format(t('complete_setup_before_proceeding'), 'capitalize')
+ )
}
}
}
@@ -752,22 +763,16 @@ function PrepareToRun({
/>
{
- launchLPC()
+ setSetupScreen('offsets')
}}
title={t('labware_position_check')}
- detail={t(
- lpcDisabledReason != null
- ? 'currently_unavailable'
- : 'recommended'
- )}
+ detail={t('recommended')}
subDetail={
latestCurrentOffsets.length > 0
? t('offsets_applied', { count: latestCurrentOffsets.length })
: null
}
- status="general"
- disabled={lpcDisabledReason != null}
- disabledReason={lpcDisabledReason}
+ status={offsetsConfirmed ? 'ready' : 'general'}
/>
{
@@ -776,25 +781,25 @@ function PrepareToRun({
title={t('parameters')}
detail={parametersDetail}
subDetail={null}
- status="general"
- disabled={!hasRunTimeParameters}
+ status="ready"
+ interactionDisabled={!hasRunTimeParameters}
/>
{
setSetupScreen('labware')
}}
- title={t('labware')}
+ title={i18n.format(t('labware'), 'capitalize')}
detail={labwareDetail}
subDetail={labwareSubDetail}
- status="general"
+ status={labwareConfirmed ? 'ready' : 'general'}
disabled={labwareDetail == null}
/>
{
setSetupScreen('liquids')
}}
- title={t('liquids')}
- status="general"
+ title={i18n.format(t('liquids'), 'capitalize')}
+ status={liquidsConfirmed ? 'ready' : 'general'}
detail={
liquidsInProtocol.length > 0
? t('initial_liquids_num', {
@@ -809,7 +814,6 @@ function PrepareToRun({
)}