-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(app): add ODD modal for incompatible modules
This is a blocking modal takeover that pops when you have incompatible modules (as determined by the machine) connected to your flex, and goes away when you remove them. This also adds a test id to the modal and app root elements (it's just the id again) because testing-library/react doesn't offer a way to search just by id, and it's nice to be able to test that your component is hanging off the right root. Closes RSQ-6
- Loading branch information
Showing
15 changed files
with
827 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
{ | ||
"incompatible_modules_attached": "incompatible module detected", | ||
"remove_before_running_protocol": "Remove the following hardware before running a protocol:" | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
56 changes: 56 additions & 0 deletions
56
app/src/organisms/IncompatibleModuleModal/IncompatibleModuleModalBody.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,56 @@ | ||
import * as React from 'react' | ||
import { useTranslation, Trans } from 'react-i18next' | ||
import capitalize from 'lodash/capitalize' | ||
import { | ||
DIRECTION_COLUMN, | ||
Flex, | ||
SPACING, | ||
StyledText, | ||
TYPOGRAPHY, | ||
OVERFLOW_SCROLL, | ||
} from '@opentrons/components' | ||
import { getModuleDisplayName } from '@opentrons/shared-data' | ||
import type { AttachedModule } from '@opentrons/api-client' | ||
import { Modal } from '../../molecules/Modal' | ||
import { ListItem } from '../../atoms/ListItem' | ||
import type { ModalHeaderBaseProps } from '../../molecules/Modal/types' | ||
export interface IncompatibleModuleModalBodyProps { | ||
modules: AttachedModule[] | ||
} | ||
|
||
export function IncompatibleModuleModalBody( | ||
props: IncompatibleModuleModalBodyProps | ||
): JSX.Element { | ||
const { t } = useTranslation('incompatible_modules') | ||
const incompatibleModuleHeader: ModalHeaderBaseProps = { | ||
title: capitalize(t('incompatible_modules_attached')), | ||
} | ||
const { modules } = props | ||
return ( | ||
<Modal header={incompatibleModuleHeader}> | ||
<Flex flexDirection={DIRECTION_COLUMN} width="100%"> | ||
<StyledText as="p" marginBottom={SPACING.spacing32}> | ||
<Trans t={t} i18nKey="remove_before_running_protocol" /> | ||
</StyledText> | ||
<Flex | ||
overflowY={OVERFLOW_SCROLL} | ||
flexDirection={DIRECTION_COLUMN} | ||
gridGap={SPACING.spacing8} | ||
maxHeight="196px" | ||
> | ||
{...modules.map(module => ( | ||
<ListItem key={module.id} type="noActive"> | ||
<StyledText | ||
as="p" | ||
key={module.id} | ||
fontWeight={TYPOGRAPHY.fontWeightSemiBold} | ||
> | ||
{getModuleDisplayName(module.moduleModel)} | ||
</StyledText> | ||
</ListItem> | ||
))} | ||
</Flex> | ||
</Flex> | ||
</Modal> | ||
) | ||
} |
23 changes: 23 additions & 0 deletions
23
app/src/organisms/IncompatibleModuleModal/IncompatibleModuleTakeover.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,23 @@ | ||
import * as React from 'react' | ||
import { createPortal } from 'react-dom' | ||
import { IncompatibleModuleModalBody } from './IncompatibleModuleModalBody' | ||
import { getTopPortalEl } from '../../App/portal' | ||
import { useIncompatibleModulesAttached } from './hooks' | ||
|
||
const POLL_INTERVAL_MS = 5000 | ||
|
||
export function IncompatibleModuleTakeover(): JSX.Element { | ||
const incompatibleModules = useIncompatibleModulesAttached({ | ||
refetchInterval: POLL_INTERVAL_MS, | ||
}) | ||
return ( | ||
<> | ||
{incompatibleModules.length !== 0 | ||
? createPortal( | ||
<IncompatibleModuleModalBody modules={incompatibleModules} />, | ||
getTopPortalEl() | ||
) | ||
: null} | ||
</> | ||
) | ||
} |
154 changes: 154 additions & 0 deletions
154
app/src/organisms/IncompatibleModuleModal/__fixtures__/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,154 @@ | ||
export const oneIncompatibleModule = [ | ||
{ | ||
id: '3feb840a3fa2dac2409b977f1e330f54f50e6231', | ||
serialNumber: 'dummySerialTC', | ||
firmwareVersion: 'dummyVersionTC', | ||
hardwareRevision: 'dummyModelTC', | ||
hasAvailableUpdate: false, | ||
moduleType: 'thermocyclerModuleType', | ||
moduleModel: 'thermocyclerModuleV1', | ||
compatibleWithRobot: false, | ||
data: { | ||
status: 'holding at target', | ||
currentTemperature: 3.0, | ||
targetTemperature: 3.0, | ||
lidStatus: 'open', | ||
lidTemperature: 4.0, | ||
lidTargetTemperature: 4.0, | ||
holdTime: 121.0, | ||
}, | ||
usbPort: { | ||
port: 0, | ||
path: '', | ||
hub: false, | ||
portGroup: 'unknown', | ||
}, | ||
}, | ||
] | ||
export const manyIncompatibleModules = [ | ||
{ | ||
id: '3feb840a3fa2dac2409b977f1e330f54f50e6231', | ||
serialNumber: 'dummySerialTC', | ||
firmwareVersion: 'dummyVersionTC', | ||
hardwareRevision: 'dummyModelTC', | ||
hasAvailableUpdate: false, | ||
moduleType: 'thermocyclerModuleType', | ||
moduleModel: 'thermocyclerModuleV1', | ||
compatibleWithRobot: false, | ||
data: { | ||
status: 'holding at target', | ||
currentTemperature: 3.0, | ||
targetTemperature: 3.0, | ||
lidStatus: 'open', | ||
lidTemperature: 4.0, | ||
lidTargetTemperature: 4.0, | ||
holdTime: 121.0, | ||
}, | ||
usbPort: { | ||
port: 0, | ||
path: '', | ||
hub: false, | ||
portGroup: 'unknown', | ||
}, | ||
}, | ||
{ | ||
id: 'aojfhkalshdaoahosifhoaisdada', | ||
serialNumber: 'dummySerialTC', | ||
firmwareVersion: 'dummyVersionTC', | ||
hardwareRevision: 'dummyModelTC', | ||
hasAvailableUpdate: false, | ||
moduleType: 'thermocyclerModuleType', | ||
moduleModel: 'thermocyclerModuleV1', | ||
compatibleWithRobot: false, | ||
data: { | ||
status: 'holding at target', | ||
currentTemperature: 3.0, | ||
targetTemperature: 3.0, | ||
lidStatus: 'open', | ||
lidTemperature: 4.0, | ||
lidTargetTemperature: 4.0, | ||
holdTime: 121.0, | ||
}, | ||
usbPort: { | ||
port: 0, | ||
path: '', | ||
hub: false, | ||
portGroup: 'unknown', | ||
}, | ||
}, | ||
{ | ||
id: 'asojhfaohsoihfjaoisodaalallala', | ||
serialNumber: 'dummySerialTC', | ||
firmwareVersion: 'dummyVersionTC', | ||
hardwareRevision: 'dummyModelTC', | ||
hasAvailableUpdate: false, | ||
moduleType: 'thermocyclerModuleType', | ||
moduleModel: 'thermocyclerModuleV1', | ||
compatibleWithRobot: false, | ||
data: { | ||
status: 'holding at target', | ||
currentTemperature: 3.0, | ||
targetTemperature: 3.0, | ||
lidStatus: 'open', | ||
lidTemperature: 4.0, | ||
lidTargetTemperature: 4.0, | ||
holdTime: 121.0, | ||
}, | ||
usbPort: { | ||
port: 0, | ||
path: '', | ||
hub: false, | ||
portGroup: 'unknown', | ||
}, | ||
}, | ||
{ | ||
id: 'sfaoisdfolasda09sd09aaaaaaaaaa', | ||
serialNumber: 'dummySerialTC', | ||
firmwareVersion: 'dummyVersionTC', | ||
hardwareRevision: 'dummyModelTC', | ||
hasAvailableUpdate: false, | ||
moduleType: 'thermocyclerModuleType', | ||
moduleModel: 'thermocyclerModuleV1', | ||
compatibleWithRobot: false, | ||
data: { | ||
status: 'holding at target', | ||
currentTemperature: 3.0, | ||
targetTemperature: 3.0, | ||
lidStatus: 'open', | ||
lidTemperature: 4.0, | ||
lidTargetTemperature: 4.0, | ||
holdTime: 121.0, | ||
}, | ||
usbPort: { | ||
port: 0, | ||
path: '', | ||
hub: false, | ||
portGroup: 'unknown', | ||
}, | ||
}, | ||
{ | ||
id: 'oasihfa980109109dm011', | ||
serialNumber: 'dummySerialTC', | ||
firmwareVersion: 'dummyVersionTC', | ||
hardwareRevision: 'dummyModelTC', | ||
hasAvailableUpdate: false, | ||
moduleType: 'thermocyclerModuleType', | ||
moduleModel: 'thermocyclerModuleV1', | ||
compatibleWithRobot: false, | ||
data: { | ||
status: 'holding at target', | ||
currentTemperature: 3.0, | ||
targetTemperature: 3.0, | ||
lidStatus: 'open', | ||
lidTemperature: 4.0, | ||
lidTargetTemperature: 4.0, | ||
holdTime: 121.0, | ||
}, | ||
usbPort: { | ||
port: 0, | ||
path: '', | ||
hub: false, | ||
portGroup: 'unknown', | ||
}, | ||
}, | ||
] |
45 changes: 45 additions & 0 deletions
45
app/src/organisms/IncompatibleModuleModal/__tests__/IncompatibleModuleModalBody.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import React from 'react' | ||
import { screen } from '@testing-library/react' | ||
import { describe, it, beforeEach, expect } from 'vitest' | ||
import '@testing-library/jest-dom/vitest' | ||
import { renderWithProviders } from '../../../__testing-utils__' | ||
import { i18n } from '../../../i18n' | ||
import { IncompatibleModuleModalBody } from '../IncompatibleModuleModalBody' | ||
import * as Fixtures from '../__fixtures__' | ||
|
||
const render = ( | ||
props: React.ComponentProps<typeof IncompatibleModuleModalBody> | ||
) => { | ||
return renderWithProviders(<IncompatibleModuleModalBody {...props} />, { | ||
i18nInstance: i18n, | ||
})[0] | ||
} | ||
|
||
describe('IncompatibleModuleModalBody', () => { | ||
let props: React.ComponentProps<typeof IncompatibleModuleModalBody> | ||
beforeEach(() => { | ||
props = { | ||
modules: [], | ||
} | ||
}) | ||
|
||
it('should render i18nd header text', () => { | ||
props = { ...props, modules: Fixtures.oneIncompatibleModule as any } | ||
render(props) | ||
screen.getByText('Incompatible module detected') | ||
screen.getByText('Remove the following hardware before running a protocol:') | ||
}) | ||
|
||
it('should render a module card', () => { | ||
props = { ...props, modules: Fixtures.oneIncompatibleModule as any } | ||
render(props) | ||
screen.getByText('Thermocycler Module GEN1') | ||
}) | ||
|
||
it('should overflow via scroll', () => { | ||
props = { ...props, modules: Fixtures.manyIncompatibleModules as any } | ||
render(props) | ||
const labels = screen.getAllByText('Thermocycler Module GEN1') | ||
expect(labels).toHaveLength(Fixtures.manyIncompatibleModules.length) | ||
}) | ||
}) |
59 changes: 59 additions & 0 deletions
59
app/src/organisms/IncompatibleModuleModal/__tests__/IncompatibleModuleTakeover.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,59 @@ | ||
import React from 'react' | ||
import { screen } from '@testing-library/react' | ||
import { describe, it, beforeEach, afterEach, expect, vi } from 'vitest' | ||
import { when } from 'vitest-when' | ||
import '@testing-library/jest-dom/vitest' | ||
import { renderWithProviders } from '../../../__testing-utils__' | ||
import { i18n } from '../../../i18n' | ||
import { IncompatibleModuleTakeover } from '../IncompatibleModuleTakeover' | ||
import { IncompatibleModuleModalBody } from '../IncompatibleModuleModalBody' | ||
import { useIncompatibleModulesAttached } from '../hooks' | ||
import type { AttachedModule } from '@opentrons/api-client' | ||
import { PortalRoot, MODAL_PORTAL_ID } from '../../../App/portal' | ||
|
||
vi.mock('../hooks') | ||
vi.mock('../IncompatibleModuleModalBody') | ||
|
||
import * as Fixtures from '../__fixtures__' | ||
|
||
const getRenderer = (incompatibleModules: AttachedModule[]) => { | ||
when(useIncompatibleModulesAttached) | ||
.calledWith(expect.anything()) | ||
.thenReturn(incompatibleModules) | ||
vi.mocked(IncompatibleModuleModalBody).mockReturnValue( | ||
<div>TEST ELEMENT</div> | ||
) | ||
return (props: React.ComponentProps<typeof IncompatibleModuleTakeover>) => { | ||
return renderWithProviders( | ||
<> | ||
<PortalRoot /> | ||
<IncompatibleModuleTakeover {...(props as any)} /> | ||
</>, | ||
{ | ||
i18nInstance: i18n, | ||
} | ||
)[0] | ||
} | ||
} | ||
|
||
describe('IncompatibleModuleTakeover', () => { | ||
let props: React.ComponentProps<typeof IncompatibleModuleTakeover> | ||
beforeEach(() => { | ||
props = {} | ||
}) | ||
|
||
afterEach(() => { | ||
vi.restoreAllMocks() | ||
}) | ||
|
||
it('should render nothing when no incompatible modules are attached', () => { | ||
getRenderer([])(props) | ||
expect(screen.findByTestId(MODAL_PORTAL_ID)).resolves.toBeEmptyDOMElement() | ||
}) | ||
|
||
it('should render the module body when incompatible modules are attached', async () => { | ||
getRenderer(Fixtures.oneIncompatibleModule as any)(props) | ||
const container = await screen.findByTestId(MODAL_PORTAL_ID) | ||
await screen.findByText('TEST ELEMENT', {}, { container }) | ||
}) | ||
}) |
Oops, something went wrong.