diff --git a/app/src/assets/localization/en/labware_position_check.json b/app/src/assets/localization/en/labware_position_check.json index b466fd7f3442..822963459ce3 100644 --- a/app/src/assets/localization/en/labware_position_check.json +++ b/app/src/assets/localization/en/labware_position_check.json @@ -52,6 +52,7 @@ "ensure_nozzle_position_desktop": "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.", "ensure_nozzle_position_odd": "Ensure that the {{tip_type}} is centered above and level with {{item_location}}. If it isn't, tap Move pipette and then jog the pipette until it is properly aligned.", "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", diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/PrepareLabware/PlaceItemInstruction.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/PrepareLabware/PlaceItemInstruction.tsx index 9033ca4bff1f..e1dd952299bf 100644 --- a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/PrepareLabware/PlaceItemInstruction.tsx +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/PrepareLabware/PlaceItemInstruction.tsx @@ -1,7 +1,15 @@ 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, @@ -9,6 +17,7 @@ import { OFFSET_KIND_DEFAULT, selectLwDisplayName, getFlexSlotNameOnly, + selectActivePipetteChannelCount, } from '/app/redux/protocol-runs' import { UnorderedList } from '/app/molecules/UnorderedList' import { DescriptionContent } from '/app/molecules/InterventionModal' @@ -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 @@ -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', { @@ -57,35 +70,52 @@ export function PlaceItemInstruction( ) as LabwareStackupDetail[] return ( - , - ...lwOnlyLocSeq.map((component, index) => ( - + - )), - ]} + />, + ...lwOnlyLocSeq.map((component, index) => ( + + )), + ]} + /> + } + /> + {isActivePipette96ch && isDefaultOffset && ( + - } - /> + )} + ) } +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 diff --git a/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/PrepareLabware/__tests__/PlaceItemInstruction.test.tsx b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/PrepareLabware/__tests__/PlaceItemInstruction.test.tsx new file mode 100644 index 000000000000..0626a724ad1d --- /dev/null +++ b/app/src/organisms/LabwarePositionCheck/steps/HandleLabware/EditOffset/PrepareLabware/__tests__/PlaceItemInstruction.test.tsx @@ -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(, { + 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() + }) +})