Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@
"ensure_nozzle_position_desktop": "<block>Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, use the controls below or your keyboard to jog the pipette until it is properly aligned.</block>",
"ensure_nozzle_position_odd": "<block>Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap <bold>Move pipette</bold> and then jog the pipette until it is properly aligned.</block>",
"ensure_probe_attached": "Ensure it is properly attached before proceeding.",
"ensure_tip_rack_accurately_placed": "Ensure the tip rack is accurately placed in the slot as outlined above to prevent damage to your labware.",
"exit": "Exit",
"exit_screen_confirm_exit": "Exit and discard all labware offsets",
"exit_screen_go_back": "Go back to labware position check",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,23 @@
import { Trans, useTranslation } from 'react-i18next'
import { useSelector } from 'react-redux'
import { css } from 'styled-components'

import { TYPOGRAPHY, LegacyStyledText } from '@opentrons/components'
import {
TYPOGRAPHY,
LegacyStyledText,
Flex,
DIRECTION_COLUMN,
SPACING,
RESPONSIVENESS,
} from '@opentrons/components'

import {
selectIsSelectedLwTipRack,
selectSelectedLwOverview,
OFFSET_KIND_DEFAULT,
selectLwDisplayName,
getFlexSlotNameOnly,
selectActivePipetteChannelCount,
} from '/app/redux/protocol-runs'
import { UnorderedList } from '/app/molecules/UnorderedList'
import { DescriptionContent } from '/app/molecules/InterventionModal'
Expand All @@ -23,6 +32,7 @@ import type {
import type { State } from '/app/redux/types'
import type { LPCWizardContentProps } from '/app/organisms/LabwarePositionCheck/types'
import type { EditOffsetContentProps } from '/app/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset'
import { InlineNotification } from '/app/atoms/InlineNotification'

export function PlaceItemInstruction(
props: EditOffsetContentProps
Expand All @@ -33,11 +43,14 @@ export function PlaceItemInstruction(
const { protocolData } = useSelector(
(state: State) => state.protocolRuns[runId]?.lpc as LPCWizardState
)
const isActivePipette96ch =
useSelector(selectActivePipetteChannelCount(runId)) === 96
const isLwTiprack = useSelector(selectIsSelectedLwTipRack(runId))
const selectedLwInfo = useSelector(
selectSelectedLwOverview(runId)
) as SelectedLwOverview
const offsetLocationDetails = selectedLwInfo.offsetLocationDetails as OffsetLocationDetails
const isDefaultOffset = offsetLocationDetails.kind === OFFSET_KIND_DEFAULT

const buildHeader = (): string =>
t('prepare_item_in_location', {
Expand All @@ -57,35 +70,52 @@ export function PlaceItemInstruction(
) as LabwareStackupDetail[]

return (
<DescriptionContent
headline={buildHeader()}
message={
<UnorderedList
items={[
<ClearDeckCopy
{...props}
key="clear_deck"
slotOnlyDisplayLocation={slotOnlyDisplayLocation}
labwareInfo={selectedLwInfo}
/>,
...lwOnlyLocSeq.map((component, index) => (
<PlaceItemInstructionContent
key={`${slotOnlyDisplayLocation}-${index}`}
isLwTiprack={isLwTiprack}
<Flex css={CONATINER_STYLE}>
<DescriptionContent
headline={buildHeader()}
message={
<UnorderedList
items={[
<ClearDeckCopy
{...props}
key="clear_deck"
slotOnlyDisplayLocation={slotOnlyDisplayLocation}
labwareInfo={selectedLwInfo}
lwComponent={component}
isFirstItemInStackup={index === 0}
{...props}
/>
)),
]}
/>,
...lwOnlyLocSeq.map((component, index) => (
<PlaceItemInstructionContent
key={`${slotOnlyDisplayLocation}-${index}`}
isLwTiprack={isLwTiprack}
slotOnlyDisplayLocation={slotOnlyDisplayLocation}
labwareInfo={selectedLwInfo}
lwComponent={component}
isFirstItemInStackup={index === 0}
{...props}
/>
)),
]}
/>
}
/>
{isActivePipette96ch && isDefaultOffset && (
<InlineNotification
type="alert"
heading={t('ensure_tip_rack_accurately_placed')}
/>
}
/>
)}
</Flex>
)
}

const CONATINER_STYLE = css`
flex-direction: ${DIRECTION_COLUMN};
gap: ${SPACING.spacing12};

@media ${RESPONSIVENESS.touchscreenMediaQuerySpecs} {
gap: ${SPACING.spacing24};
}
`

interface PlaceItemInstructionContentProps extends LPCWizardContentProps {
isLwTiprack: boolean
slotOnlyDisplayLocation: string
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
import { describe, expect, it, beforeEach, vi } from 'vitest'
import { screen } from '@testing-library/react'
import { useSelector } from 'react-redux'

import { renderWithProviders } from '/app/__testing-utils__'
import { i18n } from '/app/i18n'
import {
selectIsSelectedLwTipRack,
selectSelectedLwOverview,
OFFSET_KIND_DEFAULT,
selectLwDisplayName,
getFlexSlotNameOnly,
selectActivePipetteChannelCount,
OFFSET_KIND_LOCATION_SPECIFIC,
} from '/app/redux/protocol-runs'
import { PlaceItemInstruction } from '../PlaceItemInstruction'

vi.mock('react-redux', async () => {
const actual = await vi.importActual('react-redux')
return {
...actual,
useSelector: vi.fn(),
}
})
vi.mock('/app/redux/protocol-runs')

describe('PlaceItemInstruction', () => {
const mockRunId = 'mock_run_id'
const mockProps = {
runId: mockRunId,
}
const mockSlotLocation = 'Slot C2'
const mockLwDisplayName = 'Mock Labware'
const mockTipRackDisplayName = 'Mock Tip Rack'
const mockLpcState = {
protocolData: {},
}

const mockTipRackStackup = {
offsetLocationDetails: {
kind: OFFSET_KIND_DEFAULT,
closestBeneathModuleModel: null,
lwModOnlyStackupDetails: [{ kind: 'labware', labwareUri: 'tiprack-uri' }],
},
}

const mockLabwareStackup = {
offsetLocationDetails: {
kind: OFFSET_KIND_DEFAULT,
closestBeneathModuleModel: null,
lwModOnlyStackupDetails: [{ kind: 'labware', labwareUri: 'labware-uri' }],
},
}

const mockLabwareWithModuleStackup = {
offsetLocationDetails: {
kind: OFFSET_KIND_LOCATION_SPECIFIC,
closestBeneathModuleModel: 'temperatureModule',
lwModOnlyStackupDetails: [
{ kind: 'module', moduleModel: 'temperatureModule' },
{ kind: 'labware', labwareUri: 'labware-uri' },
],
},
}

const render = () => {
// @ts-expect-error Not all props necessary for testing.
return renderWithProviders(<PlaceItemInstruction {...mockProps} />, {
i18nInstance: i18n,
})[0]
}

beforeEach(() => {
vi.clearAllMocks()

vi.mocked(getFlexSlotNameOnly).mockReturnValue(mockSlotLocation)
vi.mocked(selectLwDisplayName).mockReturnValue(() => mockLwDisplayName)

vi.mocked(selectIsSelectedLwTipRack).mockImplementation(() => () => false)
vi.mocked(selectSelectedLwOverview).mockImplementation(() => () =>
mockLabwareStackup as any
)
vi.mocked(selectActivePipetteChannelCount).mockImplementation(() => () => 8)

vi.mocked(useSelector).mockImplementation(selector => {
return selector({
protocolRuns: {
[mockRunId]: {
lpc: mockLpcState,
},
},
})
})
})

it('should render prepare labware instruction in slot location', () => {
render()

screen.getByText(`Prepare labware in ${mockSlotLocation}`)
})

it('should render prepare tip rack instruction in slot location', () => {
vi.mocked(selectIsSelectedLwTipRack).mockImplementation(() => () => true)
vi.mocked(selectSelectedLwOverview).mockImplementation(() => () =>
mockTipRackStackup as any
)

render()

screen.getByText(`Prepare tip rack in ${mockSlotLocation}`)
})

it('should show clear deck instruction for default offsets', () => {
render()

screen.getByText(
/Clear all deck slots of labware and remove any modules from/
)
})

it('should show clear deck but modules instruction for non-default offset with a module', () => {
vi.mocked(selectSelectedLwOverview).mockImplementation(() => () =>
mockLabwareWithModuleStackup as any
)

render()

screen.getByText(
'Clear all deck slots of labware, leaving modules in place'
)
})

it('should show a place labware instruction', () => {
render()

const listItems = screen.getAllByRole('listitem')
const labwareItem = listItems.find(
li =>
li.textContent?.includes('Place a') &&
li.textContent.includes(mockLwDisplayName) &&
li.textContent.includes(mockSlotLocation)
)

expect(labwareItem).toBeTruthy()
})

it('should show a place tip rack instruction', () => {
vi.mocked(selectIsSelectedLwTipRack).mockImplementation(() => () => true)
vi.mocked(selectSelectedLwOverview).mockImplementation(() => () =>
mockTipRackStackup as any
)
vi.mocked(selectLwDisplayName).mockReturnValue(() => mockTipRackDisplayName)

render()

const listItems = screen.getAllByRole('listitem')
const tipRackItem = listItems.find(
li =>
li.textContent?.includes('Place') &&
li.textContent.includes('full') &&
li.textContent.includes(mockTipRackDisplayName) &&
li.textContent.includes(mockSlotLocation)
)
expect(tipRackItem).toBeTruthy()
})

it('should show inline notification for 96-channel pipette when calibrating a default offset', () => {
vi.mocked(selectIsSelectedLwTipRack).mockImplementation(() => () => true)
vi.mocked(selectSelectedLwOverview).mockImplementation(() => () =>
mockTipRackStackup as any
)
vi.mocked(selectActivePipetteChannelCount).mockImplementation(() => () =>
96
)

render()

screen.getByText(
'Ensure the tip rack is accurately placed in the slot as outlined above to prevent damage to your labware.'
)
})

it('should not show inline notification for other pipettes when calibrating a default offset', () => {
render()

expect(
screen.queryByText(
'Ensure the tip rack is accurately placed in the slot as outlined above to prevent damage to your labware.'
)
).not.toBeInTheDocument()
})

it('should show next place labware instruction for the second item in a stackup', () => {
const multiItemStackup = {
offsetLocationDetails: {
kind: OFFSET_KIND_DEFAULT,
closestBeneathModuleModel: null,
lwModOnlyStackupDetails: [
{ kind: 'labware', labwareUri: 'labware-uri-1' },
{ kind: 'labware', labwareUri: 'labware-uri-2' },
],
},
} as any

vi.mocked(selectSelectedLwOverview).mockImplementation(() => () =>
multiItemStackup
)
vi.mocked(selectLwDisplayName).mockImplementation((runId, uri) => {
return () =>
uri === 'labware-uri-1' ? 'First Labware' : 'Second Labware'
})

render()

const listItems = screen.getAllByRole('listitem')

const firstLabwareItem = listItems.find(
li =>
li.textContent?.includes('Place a') &&
li.textContent?.includes('First Labware')
)

const secondLabwareItem = listItems.find(
li =>
li.textContent?.includes('Next, place a') &&
li.textContent?.includes('Second Labware')
)

expect(firstLabwareItem).toBeTruthy()
expect(secondLabwareItem).toBeTruthy()
})
})
Loading