Skip to content

Commit 59a325b

Browse files
authored
fix(app): fix TipPickup layout in the second window (#20339)
* fix(app): fix TipPickup layout in the second window
1 parent daadb45 commit 59a325b

File tree

4 files changed

+244
-2
lines changed

4 files changed

+244
-2
lines changed
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
import { screen } from '@testing-library/react'
2+
import { beforeEach, describe, it, vi } from 'vitest'
3+
4+
import { fixtureTiprack300ul } from '@opentrons/shared-data'
5+
import { CLEAN, DIRTY, EMPTY } from '@opentrons/step-generation'
6+
7+
import { renderWithProviders } from '/app/__testing-utils__'
8+
import { i18n } from '/app/i18n'
9+
10+
import { TipPickupSlot } from './'
11+
12+
import type { ComponentProps } from 'react'
13+
import type * as OpentronsComponents from '@opentrons/components'
14+
import type { LabwareDefinition2 } from '@opentrons/shared-data'
15+
import type { LabwareEntity, RobotState } from '@opentrons/step-generation'
16+
17+
vi.mock('@opentrons/components', async importOriginal => {
18+
const actual = await importOriginal<typeof OpentronsComponents>()
19+
return {
20+
...actual,
21+
LabwareRender: vi.fn(() => <div>mock LabwareRender</div>),
22+
RobotWorkSpace: vi.fn(({ children }) => (
23+
<div data-testid="robot-workspace">{children()}</div>
24+
)),
25+
}
26+
})
27+
28+
const MOCK_TIPRACK_ID = 'mockTiprackId'
29+
const MOCK_SLOT = 'A1'
30+
31+
const createMockLabwareDef = (): LabwareDefinition2 => {
32+
return {
33+
...fixtureTiprack300ul,
34+
metadata: {
35+
...fixtureTiprack300ul.metadata,
36+
displayName: 'Mock 300µL Tiprack',
37+
},
38+
} as LabwareDefinition2
39+
}
40+
41+
const createMockTiprackEntity = (): LabwareEntity => {
42+
return {
43+
id: MOCK_TIPRACK_ID,
44+
labwareDefURI: 'opentrons/fixture_tiprack_300_ul/1',
45+
def: createMockLabwareDef(),
46+
pythonName: 'mock_tiprack',
47+
}
48+
}
49+
50+
const createMockRobotState = (
51+
tipStateOverrides?: Record<string, 'CLEAN' | 'DIRTY' | 'EMPTY'>
52+
): RobotState => {
53+
return {
54+
labware: {
55+
[MOCK_TIPRACK_ID]: {
56+
stack: [MOCK_TIPRACK_ID, MOCK_SLOT],
57+
},
58+
},
59+
pipettes: {},
60+
modules: {},
61+
liquidState: {
62+
pipettes: {},
63+
labware: {},
64+
trashBins: {},
65+
wasteChute: {},
66+
},
67+
tipState: {
68+
pipettes: {},
69+
tipracks:
70+
tipStateOverrides != null
71+
? {
72+
[MOCK_TIPRACK_ID]: tipStateOverrides,
73+
}
74+
: {},
75+
},
76+
} as RobotState
77+
}
78+
79+
const render = (props: ComponentProps<typeof TipPickupSlot>) => {
80+
return renderWithProviders(<TipPickupSlot {...props} />, {
81+
i18nInstance: i18n,
82+
})
83+
}
84+
85+
describe('TipPickupSlot', () => {
86+
let props: ComponentProps<typeof TipPickupSlot>
87+
88+
beforeEach(() => {
89+
props = {
90+
tiprackEntity: createMockTiprackEntity(),
91+
robotState: createMockRobotState(),
92+
}
93+
})
94+
95+
it('should render TipPickupSlot', () => {
96+
render(props)
97+
screen.getByText(MOCK_SLOT)
98+
screen.getByText('Mock 300µL Tiprack')
99+
screen.getByText('mock LabwareRender')
100+
screen.getByTestId('robot-workspace')
101+
})
102+
103+
it('should display tips remaining count when tips are present', () => {
104+
props.robotState = createMockRobotState({
105+
A1: CLEAN,
106+
A2: CLEAN,
107+
A3: DIRTY,
108+
B1: EMPTY,
109+
})
110+
render(props)
111+
screen.getByText('Tips remaining')
112+
screen.getByText('3 tips')
113+
})
114+
115+
it('should display zero tips remaining when all tips are empty', () => {
116+
props.robotState = createMockRobotState({
117+
A1: EMPTY,
118+
A2: EMPTY,
119+
})
120+
render(props)
121+
screen.getByText('0 tips')
122+
})
123+
124+
it('should handle tiprack with no tipState info', () => {
125+
props.robotState = createMockRobotState()
126+
render(props)
127+
screen.getByText('0 tips')
128+
})
129+
})
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import { useTranslation } from 'react-i18next'
2+
3+
import {
4+
COLORS,
5+
LabwareRender,
6+
NO,
7+
RobotInfoLabel,
8+
RobotWorkSpace,
9+
StyledText,
10+
tipStateToTipType,
11+
} from '@opentrons/components'
12+
import { getLabwareViewBox } from '@opentrons/shared-data'
13+
import { getSlotInLocationStack } from '@opentrons/step-generation'
14+
15+
import { getMissingTips } from '../../utils/getMissingTips'
16+
import styles from './tippickupslot.module.css'
17+
18+
import type { TipType } from '@opentrons/components'
19+
import type { LabwareEntity, RobotState } from '@opentrons/step-generation'
20+
21+
interface TipPickupSlotProps {
22+
tiprackEntity: LabwareEntity
23+
robotState: RobotState
24+
}
25+
26+
export function TipPickupSlot(props: TipPickupSlotProps): JSX.Element {
27+
const { tiprackEntity, robotState } = props
28+
const { t } = useTranslation('protocol_visualization')
29+
const { id, def } = tiprackEntity
30+
const { tipState, labware } = robotState
31+
const tipStateInfo = tipState.tipracks[id]
32+
const tipStatusByWellName =
33+
tipStateInfo != null
34+
? Object.entries(tipStateInfo).reduce<Record<string, TipType>>(
35+
(acc, [wellName, state]) => ({
36+
...acc,
37+
[wellName]: tipStateToTipType[state],
38+
}),
39+
{}
40+
)
41+
: {}
42+
const labwareViewBox = getLabwareViewBox(def)
43+
const missingTips = getMissingTips(tipState, id)
44+
const slot = getSlotInLocationStack(labware[id].stack)
45+
const tipsRemaining = Object.values(tipStatusByWellName).filter(
46+
state => state !== NO
47+
).length
48+
49+
return (
50+
<div className={styles.container}>
51+
<div className={styles.header}>
52+
<RobotInfoLabel deckLabel={slot} />
53+
<StyledText desktopStyle="bodyDefaultRegular" color={COLORS.grey60}>
54+
{def.metadata.displayName}
55+
</StyledText>
56+
</div>
57+
<div className={styles.main_content}>
58+
<RobotWorkSpace
59+
key={id}
60+
viewBox={`${labwareViewBox.minX} ${labwareViewBox.minY} ${labwareViewBox.xDimension} ${labwareViewBox.yDimension}`}
61+
>
62+
{() => (
63+
<g>
64+
<LabwareRender
65+
definition={def}
66+
positioningMode="offsetInSlot"
67+
missingTips={missingTips}
68+
tipStatusByWellName={tipStatusByWellName}
69+
/>
70+
</g>
71+
)}
72+
</RobotWorkSpace>
73+
</div>
74+
<div className={styles.footer}>
75+
<StyledText desktopStyle="bodyDefaultRegular" color={COLORS.grey60}>
76+
{t('tips_remaining')}
77+
</StyledText>
78+
<StyledText desktopStyle="bodyDefaultRegular">
79+
{t('remaining_tips', { remaining: tipsRemaining })}
80+
</StyledText>
81+
</div>
82+
</div>
83+
)
84+
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
.container {
2+
display: flex;
3+
flex-direction: column;
4+
padding: var(--spacing-16) var(--spacing-20) var(--spacing-20);
5+
gap: var(--spacing-16);
6+
}
7+
8+
.header {
9+
display: flex;
10+
flex-direction: column;
11+
gap: var(--spacing-4);
12+
}
13+
14+
.main_content {
15+
display: flex;
16+
flex-grow: 1;
17+
align-items: center;
18+
justify-content: center;
19+
padding: var(--spacing-16) var(--spacing-24);
20+
border-radius: var(--border-radius-4);
21+
background-color: var(--grey-10);
22+
}
23+
24+
.footer {
25+
display: flex;
26+
flex-direction: row;
27+
align-items: center;
28+
justify-content: space-between;
29+
}

app/src/organisms/Desktop/ProtocolVisualization/SlotDetails/index.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import { SlotDetailsEmptyState } from '/app/molecules/SlotDetailsEmptyState'
55

66
import { ModuleContainer } from '../ModuleContainer'
77
import { LabwareSlot } from '../SecondWindow/LabwareSlot'
8+
import { TipPickupSlot } from '../SecondWindow/TipPickupSlot'
89
import { TipDisposalContainer } from '../TipDisposalContainer'
9-
import { TipPickupContainer } from '../TipPickupContainer'
1010
import styles from './slotdetails.module.css'
1111

1212
import type {
@@ -75,7 +75,7 @@ export function SlotDetails(props: SlotDetailsProps): JSX.Element {
7575
switch (labwareType) {
7676
case 'tiprack':
7777
return (
78-
<TipPickupContainer
78+
<TipPickupSlot
7979
tiprackEntity={labwareEntities[topMostLabwareOnSlot]}
8080
robotState={robotState}
8181
/>

0 commit comments

Comments
 (0)