+ beforeEach(() => {
+ props = {
+ currentOffsets: mockCurrentOffsets,
+ commands: protocolWithTC.commands,
+ onCloseClick: jest.fn(),
+ runId: RUN_ID_1,
+ handleRelaunchLPC: jest.fn(),
+ robotName: 'otie',
+ }
+ mockUseLPCDisabledReason.mockReturnValue(null)
+ mockGetLoadedLabwareDefinitionsByUri.mockReturnValue(
+ protocolWithTC.labware as any
+ )
+ mockLabwarePositionCheck.mockReturnValue(
+ mock labware position check
+ )
+ mockGetIsLabwareOffsetCodeSnippetsOn.mockReturnValue(false)
+ mockGetLatestCurrentOffsets.mockReturnValue([
+ {
+ createdAt: '2022-12-20T14:06:23.562082+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: 'dceac542-bca4-4313-82ba-d54a19dab204',
+ location: { slotName: '2' },
+ vector: { x: 0, y: -0.09999999999999432, z: 0 },
+ },
+ ])
+ })
+ it('renders the correct text and buttons CTA work', () => {
+ const { getByText } = render(props)
+ getByText('Applied Labware Offset data')
+ getByText('location')
+ getByText('labware')
+ getByText('labware offset data')
+ getByText('cancel').click()
+ expect(props.onCloseClick).toHaveBeenCalled()
+ getByText('run labware position check').click()
+ expect(props.handleRelaunchLPC).toHaveBeenCalled()
+ })
+
+ it('renders 1 offset with the correct information', () => {
+ const { getByText } = render(props)
+ getByText('opentrons/opentrons_96_tiprack_10ul/1')
+ getByText('Slot 2')
+ })
+ // TODO(jr, 12/20/22): finish this test when we add the jupyter snippet info
+ it('renders the Get labware offset data button, clicking on it renders the juypter snippet', () => {
+ mockGetIsLabwareOffsetCodeSnippetsOn.mockReturnValue(true)
+ const { getByText } = render(props)
+ getByText('Get Labware Offset Data').click()
+ getByText('TODO ADD JUPYTER/CLI SNIPPET SUPPORT')
+ })
+ it('renders the LPC button as disabled when there is a disabled reason', () => {
+ mockUseLPCDisabledReason.mockReturnValue('mockDisabledReason')
+ const { getByText } = render(props)
+ expect(getByText('run labware position check')).toBeDisabled()
+ })
+})
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx
index 018a6d2906d..47cc07fbf55 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx
+++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/SetupLabware.test.tsx
@@ -13,6 +13,7 @@ import {
useFeatureFlag,
} from '../../../../../redux/config'
import {
+ useLPCDisabledReason,
useProtocolDetailsForRun,
useRunCalibrationStatus,
useRunHasStarted,
@@ -64,7 +65,10 @@ const mockSetupLabwareList = SetupLabwareList as jest.MockedFunction<
const mockSetupLabwareMap = SetupLabwareMap as jest.MockedFunction<
typeof SetupLabwareMap
>
-
+const mockUseLPCDisabledReason = useLPCDisabledReason as jest.MockedFunction<
+ typeof useLPCDisabledReason
+>
+const DISABLED_REASON = 'MOCK_DISABLED_REASON'
const ROBOT_NAME = 'otie'
const RUN_ID = '1'
const PICKUP_TIP_LABWARE_ID = 'PICKUP_TIP_LABWARE_ID'
@@ -107,7 +111,7 @@ const render = () => {
)[0]
}
-describe('SetupLabwareMap', () => {
+describe('SetupLabware', () => {
beforeEach(() => {
when(mockGetModuleTypesThatRequireExtraAttention)
.calledWith(expect.anything())
@@ -169,6 +173,7 @@ describe('SetupLabwareMap', () => {
when(mockSetupLabwareList).mockReturnValue(
mock setup labware list
)
+ when(mockUseLPCDisabledReason).mockReturnValue(null)
})
afterEach(() => {
@@ -189,14 +194,14 @@ describe('SetupLabwareMap', () => {
)
it('should render LPC button and clicking should launch modal', () => {
const { getByRole, getByText } = render()
- const button = getByRole('button', {
+ getByRole('button', {
name: 'run labware position check',
- })
- fireEvent.click(button)
+ }).click()
getByText('mock Labware Position Check')
})
it('should render a disabled LPC button when a run has started', () => {
when(mockUseRunHasStarted).calledWith(RUN_ID).mockReturnValue(true)
+ when(mockUseLPCDisabledReason).mockReturnValue(DISABLED_REASON)
const { getByRole, queryByText } = render()
const button = getByRole('button', {
name: 'run labware position check',
@@ -224,6 +229,7 @@ describe('SetupLabwareMap', () => {
.mockReturnValue({
protocolData: null,
} as any)
+ when(mockUseLPCDisabledReason).mockReturnValue(DISABLED_REASON)
const { getByRole } = render()
const button = getByRole('button', {
name: 'run labware position check',
@@ -236,6 +242,7 @@ describe('SetupLabwareMap', () => {
.mockReturnValue({
protocolData: { labware: {}, pipettes: {} },
} as any)
+ when(mockUseLPCDisabledReason).mockReturnValue(DISABLED_REASON)
const { getByRole } = render()
const button = getByRole('button', {
name: 'run labware position check',
@@ -248,6 +255,7 @@ describe('SetupLabwareMap', () => {
.mockReturnValue({
complete: false,
})
+ when(mockUseLPCDisabledReason).mockReturnValue(DISABLED_REASON)
const { getByRole } = render()
const button = getByRole('button', {
name: 'run labware position check',
@@ -261,6 +269,7 @@ describe('SetupLabwareMap', () => {
missingModuleIds: ['temperatureModuleV1'],
remainingAttachedModules: [],
})
+ when(mockUseLPCDisabledReason).mockReturnValue(DISABLED_REASON)
const { getByRole } = render()
const button = getByRole('button', {
name: 'run labware position check',
@@ -279,6 +288,7 @@ describe('SetupLabwareMap', () => {
missingModuleIds: ['temperatureModuleV1'],
remainingAttachedModules: [],
})
+ when(mockUseLPCDisabledReason).mockReturnValue(DISABLED_REASON)
const { getByRole } = render()
const button = getByRole('button', {
name: 'run labware position check',
@@ -308,6 +318,7 @@ describe('SetupLabwareMap', () => {
},
},
} as any)
+ when(mockUseLPCDisabledReason).mockReturnValue(DISABLED_REASON)
const { getByRole } = render()
const button = getByRole('button', {
name: 'run labware position check',
@@ -338,6 +349,7 @@ describe('SetupLabwareMap', () => {
commands: [],
},
} as any)
+ when(mockUseLPCDisabledReason).mockReturnValue(DISABLED_REASON)
const { getByRole } = render()
const button = getByRole('button', {
name: 'run labware position check',
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/utils.test.ts b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/utils.test.ts
index c67b58db8ee..908e6aac528 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/utils.test.ts
+++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/__tests__/utils.test.ts
@@ -1,3 +1,183 @@
+import { getLatestCurrentOffsets } from '../utils'
+import type { LabwareOffset } from '@opentrons/api-client'
+
describe('getLabwareSetupItemGroups', () => {
it.todo('should thoroughly test this fn')
})
+
+describe('getLatestCurrentOffsets', () => {
+ it('should return the latest offsets when there are multiple offsets', () => {
+ const mockCurrentOffsets: LabwareOffset[] = [
+ {
+ createdAt: '2022-12-20T14:06:23.562082+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: 'dceac542-bca4-4313-82ba-d54a19dab204',
+ location: { slotName: '2' },
+ vector: { x: 1, y: 2, z: 3 },
+ },
+ {
+ createdAt: '2022-12-20T14:06:23.562878+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: '70ae2e31-716b-4e1f-a90c-9b0dfd4d7feb',
+ location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 4, y: 5, z: 6 },
+ },
+ {
+ createdAt: '2022-12-20T14:09:08.688756+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: '494ec3d1-224f-4f9a-a83b-3dd3060ea332',
+ location: { slotName: '2' },
+ vector: { x: 7, y: 8, z: 9 },
+ },
+ {
+ createdAt: '2022-12-20T14:09:08.689813+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: 'd39b972e-9b2d-436c-a597-3bc81aabc634',
+ location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 10, y: 11, z: 12 },
+ },
+ ]
+ const mockLatestCurrentOffsets = [
+ {
+ createdAt: '2022-12-20T14:09:08.688756+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: '494ec3d1-224f-4f9a-a83b-3dd3060ea332',
+ location: { slotName: '2' },
+ vector: { x: 7, y: 8, z: 9 },
+ },
+ {
+ createdAt: '2022-12-20T14:09:08.689813+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: 'd39b972e-9b2d-436c-a597-3bc81aabc634',
+ location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 10, y: 11, z: 12 },
+ },
+ ]
+
+ expect(getLatestCurrentOffsets(mockCurrentOffsets)).toStrictEqual(
+ mockLatestCurrentOffsets
+ )
+ })
+ it('should return empty array when the labware vector values are 0', () => {
+ const mockCurrentOffsets: LabwareOffset[] = [
+ {
+ createdAt: '2022-12-20T14:06:23.562878+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: '70ae2e31-716b-4e1f-a90c-9b0dfd4d7feb',
+ location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 0, y: 0, z: 0 },
+ },
+ ]
+
+ expect(getLatestCurrentOffsets(mockCurrentOffsets)).toStrictEqual([])
+ })
+ it('should return the correct offsets when there are multiples in random order and modules in the same slot as labware', () => {
+ const mockCurrentOffsets: LabwareOffset[] = [
+ {
+ createdAt: '2022-12-20T14:06:23.562082+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: 'dceac542-bca4-4313-82ba-d54a19dab204',
+ location: { slotName: '4' },
+ vector: { x: 1, y: 2, z: 3 },
+ },
+ {
+ createdAt: '2022-12-20T14:06:23.562878+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: '70ae2e31-716b-4e1f-a90c-9b0dfd4d7feb',
+ location: { slotName: '8', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 4, y: 5, z: 6 },
+ },
+ {
+ createdAt: '2022-12-20T18:09:08.688756+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: '494ec3d1-224f-4f9a-a83b-3dd3060ea332',
+ location: { slotName: '1' },
+ vector: { x: 7, y: 8, z: 9 },
+ },
+ {
+ createdAt: '2022-12-20T19:09:08.689813+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: 'd39b972e-9b2d-436c-a597-3bc81aabc634',
+ location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 10, y: 11, z: 12 },
+ },
+ {
+ createdAt: '2022-12-20T20:09:08.688756+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: '494ec3d1-224f-4f9a-a83b-3dd3060ea332',
+ location: { slotName: '1' },
+ vector: { x: 13, y: 14, z: 15 },
+ },
+ {
+ createdAt: '2023-12-20T20:09:08.688756+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: '494ec3d1-224f-4f9a-a83b-3dd3060ea332',
+ location: { slotName: '1' },
+ vector: { x: 16, y: 17, z: 18 },
+ },
+ ]
+ const mockLatestCurrentOffsets = [
+ {
+ createdAt: '2022-12-20T14:06:23.562082+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: 'dceac542-bca4-4313-82ba-d54a19dab204',
+ location: { slotName: '4' },
+ vector: { x: 1, y: 2, z: 3 },
+ },
+ {
+ createdAt: '2022-12-20T14:06:23.562878+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: '70ae2e31-716b-4e1f-a90c-9b0dfd4d7feb',
+ location: { slotName: '8', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 4, y: 5, z: 6 },
+ },
+ {
+ createdAt: '2022-12-20T19:09:08.689813+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: 'd39b972e-9b2d-436c-a597-3bc81aabc634',
+ location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 10, y: 11, z: 12 },
+ },
+ {
+ createdAt: '2023-12-20T20:09:08.688756+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: '494ec3d1-224f-4f9a-a83b-3dd3060ea332',
+ location: { slotName: '1' },
+ vector: { x: 16, y: 17, z: 18 },
+ },
+ ]
+ expect(getLatestCurrentOffsets(mockCurrentOffsets)).toStrictEqual(
+ mockLatestCurrentOffsets
+ )
+ })
+ it('should return 2 offsets of the same slotname but 1 has a module and 1 does not', () => {
+ const mockCurrentOffsets: LabwareOffset[] = [
+ {
+ createdAt: '2022-12-20T19:09:08.689813+00:00',
+ definitionUri:
+ 'opentrons/opentrons_96_flat_bottom_adapter_nest_wellplate_200ul_flat/1',
+ id: 'd39b972e-9b2d-436c-a597-3bc81aabc634',
+ location: { slotName: '1', moduleModel: 'heaterShakerModuleV1' },
+ vector: { x: 10, y: 11, z: 12 },
+ },
+ {
+ createdAt: '2022-12-20T20:09:08.688756+00:00',
+ definitionUri: 'opentrons/opentrons_96_tiprack_10ul/1',
+ id: '494ec3d1-224f-4f9a-a83b-3dd3060ea332',
+ location: { slotName: '1' },
+ vector: { x: 13, y: 14, z: 15 },
+ },
+ ]
+ expect(getLatestCurrentOffsets(mockCurrentOffsets)).toStrictEqual(
+ mockCurrentOffsets
+ )
+ })
+})
diff --git a/app/src/organisms/Devices/ProtocolRun/SetupLabware/utils.ts b/app/src/organisms/Devices/ProtocolRun/SetupLabware/utils.ts
index 2fb01cdd3ee..887419bdf37 100644
--- a/app/src/organisms/Devices/ProtocolRun/SetupLabware/utils.ts
+++ b/app/src/organisms/Devices/ProtocolRun/SetupLabware/utils.ts
@@ -1,9 +1,11 @@
import partition from 'lodash/partition'
-import { getLabwareDisplayName } from '@opentrons/shared-data'
+import isEqual from 'lodash/isEqual'
+import { getLabwareDisplayName, IDENTITY_VECTOR } from '@opentrons/shared-data'
-import { GroupedLabwareSetupItems, LabwareSetupItem } from './types'
-import { LoadModuleRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV6/command/setup'
+import type { LoadModuleRunTimeCommand } from '@opentrons/shared-data/protocol/types/schemaV6/command/setup'
import type { RunTimeCommand } from '@opentrons/shared-data'
+import type { LabwareOffset } from '@opentrons/api-client'
+import type { GroupedLabwareSetupItems, LabwareSetupItem } from './types'
const LABWARE_ACCESS_COMMAND_TYPES = [
'moveToWell',
@@ -85,3 +87,36 @@ export function getLabwareSetupItemGroups(
)
return { onDeckItems, offDeckItems }
}
+
+export function getLatestCurrentOffsets(
+ currentOffsets: LabwareOffset[]
+): LabwareOffset[] {
+ const latestCurrentOffsets = currentOffsets.reduce(
+ (acc, offset) => {
+ const previousMatchIndex = acc.findIndex(
+ currentLabwareOffsets =>
+ isEqual(offset.location, currentLabwareOffsets.location) &&
+ isEqual(offset.definitionUri, currentLabwareOffsets.definitionUri)
+ )
+ if (
+ previousMatchIndex >= 0 &&
+ new Date(acc[previousMatchIndex].createdAt) < new Date(offset.createdAt)
+ ) {
+ return [
+ ...acc.slice(0, previousMatchIndex),
+ ...acc.slice(previousMatchIndex + 1),
+ offset,
+ ]
+ } else {
+ return [...acc, offset]
+ }
+ },
+ []
+ )
+
+ const nonIdentityOffsets = latestCurrentOffsets.filter(
+ currentOffset => !isEqual(currentOffset.vector, IDENTITY_VECTOR)
+ )
+
+ return nonIdentityOffsets
+}
diff --git a/app/src/organisms/Devices/hooks/__tests__/useLPCDisabledReason.test.tsx b/app/src/organisms/Devices/hooks/__tests__/useLPCDisabledReason.test.tsx
new file mode 100644
index 00000000000..0001e8f06c8
--- /dev/null
+++ b/app/src/organisms/Devices/hooks/__tests__/useLPCDisabledReason.test.tsx
@@ -0,0 +1,155 @@
+import * as React from 'react'
+import { renderHook } from '@testing-library/react-hooks'
+import { Provider } from 'react-redux'
+import { I18nextProvider } from 'react-i18next'
+import { createStore } from 'redux'
+import _uncastedSimpleV6Protocol from '@opentrons/shared-data/protocol/fixtures/6/simpleV6.json'
+import { i18n } from '../../../../i18n'
+import { RUN_ID_1 } from '../../../RunTimeControl/__fixtures__'
+import { useLPCDisabledReason } from '../useLPCDisabledReason'
+import {
+ useProtocolDetailsForRun,
+ useRunCalibrationStatus,
+ useRunHasStarted,
+ useStoredProtocolAnalysis,
+ useUnmatchedModulesForProtocol,
+} from '..'
+import type { Store } from 'redux'
+import type { LegacySchemaAdapterOutput } from '@opentrons/shared-data'
+import type { State } from '../../../../redux/types'
+import type { StoredProtocolAnalysis } from '..'
+
+jest.mock('..')
+
+const mockUseProtocolDetailsForRun = useProtocolDetailsForRun as jest.MockedFunction<
+ typeof useProtocolDetailsForRun
+>
+const mockUseStoredProtocolAnalysis = useStoredProtocolAnalysis as jest.MockedFunction<
+ typeof useStoredProtocolAnalysis
+>
+const mockUseRunHasStarted = useRunHasStarted as jest.MockedFunction<
+ typeof useRunHasStarted
+>
+const mockUseRunCalibrationStatus = useRunCalibrationStatus as jest.MockedFunction<
+ typeof useRunCalibrationStatus
+>
+const mockUseUnmatchedModulesForProtocol = useUnmatchedModulesForProtocol as jest.MockedFunction<
+ typeof useUnmatchedModulesForProtocol
+>
+const simpleV6Protocol = (_uncastedSimpleV6Protocol as unknown) as LegacySchemaAdapterOutput
+
+describe('useLPCDisabledReason', () => {
+ const store: Store = createStore(jest.fn(), {})
+ const wrapper: React.FunctionComponent<{}> = ({ children }) => (
+
+ {children}
+
+ )
+ beforeEach(() => {
+ store.dispatch = jest.fn()
+ mockUseProtocolDetailsForRun.mockReturnValue({
+ protocolData: simpleV6Protocol,
+ } as any)
+ mockUseStoredProtocolAnalysis.mockReturnValue(
+ (simpleV6Protocol as unknown) as StoredProtocolAnalysis
+ )
+ mockUseRunHasStarted.mockReturnValue(false)
+ mockUseRunCalibrationStatus.mockReturnValue({ complete: true })
+ mockUseUnmatchedModulesForProtocol.mockReturnValue({
+ missingModuleIds: [],
+ remainingAttachedModules: [],
+ })
+ })
+ afterEach(() => {
+ jest.resetAllMocks()
+ })
+ it('renders no disabled reason', () => {
+ const { result } = renderHook(
+ () => useLPCDisabledReason('otie', RUN_ID_1),
+ { wrapper }
+ )
+ expect(result.current).toBeNull()
+ })
+ it('renders disabled reason for calibration incomponent', () => {
+ mockUseRunCalibrationStatus.mockReturnValue({ complete: false })
+ const { result } = renderHook(
+ () => useLPCDisabledReason('otie', RUN_ID_1),
+ { wrapper }
+ )
+ expect(result.current).toStrictEqual(
+ 'Make sure robot calibration is complete before running Labware Position Check'
+ )
+ })
+ it('renders disabled reason for missing modules', () => {
+ mockUseUnmatchedModulesForProtocol.mockReturnValue({
+ missingModuleIds: ['mockId'],
+ remainingAttachedModules: [],
+ })
+ const { result } = renderHook(
+ () => useLPCDisabledReason('otie', RUN_ID_1),
+ { wrapper }
+ )
+ expect(result.current).toStrictEqual(
+ 'Make sure all modules are connected before running Labware Position Check'
+ )
+ })
+ it('renders disabled reason for run has started', () => {
+ mockUseRunHasStarted.mockReturnValue(true)
+
+ const { result } = renderHook(
+ () => useLPCDisabledReason('otie', RUN_ID_1),
+ { wrapper }
+ )
+ expect(result.current).toStrictEqual(
+ 'Labware Position Check is not available after run has started'
+ )
+ })
+ it('renders disabled reason if robot protocol anaylsis is null', () => {
+ mockUseProtocolDetailsForRun.mockReturnValue({
+ protocolData: null,
+ } as any)
+ const { result } = renderHook(
+ () => useLPCDisabledReason('otie', RUN_ID_1),
+ { wrapper }
+ )
+ expect(result.current).toStrictEqual(
+ 'Labware Position Check is not available while protocol is analyzing on robot'
+ )
+ })
+ it('renders disabled reason if no pipettes in protocol', () => {
+ mockUseProtocolDetailsForRun.mockReturnValue({
+ protocolData: { ...simpleV6Protocol, pipettes: {} },
+ } as any)
+ const { result } = renderHook(
+ () => useLPCDisabledReason('otie', RUN_ID_1),
+ { wrapper }
+ )
+ expect(result.current).toStrictEqual(
+ 'Labware Position Check requires that the protocol loads labware and pipettes'
+ )
+ })
+ it('renders disabled reason if no tipracks in protocols', () => {
+ mockUseProtocolDetailsForRun.mockReturnValue({
+ protocolData: { ...simpleV6Protocol, labwareDefinitions: {} },
+ } as any)
+ const { result } = renderHook(
+ () => useLPCDisabledReason('otie', RUN_ID_1),
+ { wrapper }
+ )
+ expect(result.current).toStrictEqual(
+ 'Labware Position Check requires that the protocol loads a tip rack'
+ )
+ })
+ it('renders disabled reason if no tips are being used in the protocols', () => {
+ mockUseProtocolDetailsForRun.mockReturnValue({
+ protocolData: { ...simpleV6Protocol, commands: {} },
+ } as any)
+ const { result } = renderHook(
+ () => useLPCDisabledReason('otie', RUN_ID_1),
+ { wrapper }
+ )
+ expect(result.current).toStrictEqual(
+ 'Labware Position Check requires that the protocol has at least one pipette that picks up a tip'
+ )
+ })
+})
diff --git a/app/src/organisms/Devices/hooks/index.ts b/app/src/organisms/Devices/hooks/index.ts
index 4645fd29efd..8d03b6b820b 100644
--- a/app/src/organisms/Devices/hooks/index.ts
+++ b/app/src/organisms/Devices/hooks/index.ts
@@ -9,6 +9,7 @@ export * from './useIsRobotBusy'
export * from './useIsRobotViewable'
export * from './useLabwareRenderInfoForRunById'
export * from './useLights'
+export * from './useLPCDisabledReason'
export * from './useLPCSuccessToast'
export * from './useModuleRenderInfoForProtocolById'
export * from './usePipetteOffsetCalibrations'
diff --git a/app/src/organisms/Devices/hooks/useLPCDisabledReason.tsx b/app/src/organisms/Devices/hooks/useLPCDisabledReason.tsx
new file mode 100644
index 00000000000..7d65bf7a6f7
--- /dev/null
+++ b/app/src/organisms/Devices/hooks/useLPCDisabledReason.tsx
@@ -0,0 +1,73 @@
+import isEmpty from 'lodash/isEmpty'
+import some from 'lodash/some'
+import { useTranslation } from 'react-i18next'
+import {
+ useProtocolDetailsForRun,
+ useRunCalibrationStatus,
+ useRunHasStarted,
+ useStoredProtocolAnalysis,
+ useUnmatchedModulesForProtocol,
+} from '.'
+
+export function useLPCDisabledReason(
+ robotName: string,
+ runId: string
+): string | null {
+ const { t } = useTranslation('protocol_setup')
+ const runHasStarted = useRunHasStarted(runId)
+ const { complete: isCalibrationComplete } = useRunCalibrationStatus(
+ robotName,
+ runId
+ )
+ const unmatchedModuleResults = useUnmatchedModulesForProtocol(
+ robotName,
+ runId
+ )
+ const { protocolData: robotProtocolAnalysis } = useProtocolDetailsForRun(
+ runId
+ )
+ const storedProtocolAnalysis = useStoredProtocolAnalysis(runId)
+ const protocolData = robotProtocolAnalysis ?? storedProtocolAnalysis
+ const { missingModuleIds } = unmatchedModuleResults
+ const calibrationIncomplete =
+ missingModuleIds.length === 0 && !isCalibrationComplete
+ const moduleSetupIncomplete =
+ missingModuleIds.length > 0 && isCalibrationComplete
+ const moduleAndCalibrationIncomplete =
+ missingModuleIds.length > 0 && !isCalibrationComplete
+
+ const tipRackLoadedInProtocol: boolean = some(
+ protocolData?.labwareDefinitions,
+ def => def.parameters?.isTiprack
+ )
+ const tipsArePickedUp: boolean = some(
+ protocolData?.commands,
+ command => command.commandType === 'pickUpTip'
+ )
+ let lpcDisabledReason: string | null = null
+
+ if (moduleAndCalibrationIncomplete) {
+ lpcDisabledReason = t('lpc_disabled_modules_and_calibration_not_complete')
+ } else if (calibrationIncomplete) {
+ lpcDisabledReason = t('lpc_disabled_calibration_not_complete')
+ } else if (moduleSetupIncomplete) {
+ lpcDisabledReason = t('lpc_disabled_modules_not_connected')
+ } else if (runHasStarted) {
+ lpcDisabledReason = t('labware_position_check_not_available')
+ } else if (robotProtocolAnalysis == null) {
+ lpcDisabledReason = t(
+ 'labware_position_check_not_available_analyzing_on_robot'
+ )
+ } else if (
+ isEmpty(protocolData?.pipettes) ||
+ isEmpty(protocolData?.labware)
+ ) {
+ lpcDisabledReason = t('labware_position_check_not_available_empty_protocol')
+ } else if (!tipRackLoadedInProtocol) {
+ lpcDisabledReason = t('lpc_disabled_no_tipracks_loaded')
+ } else if (!tipsArePickedUp) {
+ lpcDisabledReason = t('lpc_disabled_no_tipracks_used')
+ }
+
+ return lpcDisabledReason
+}