Skip to content

Commit

Permalink
feat(app): Add StepInfo to ErrorRecovery (#15447)
Browse files Browse the repository at this point in the history
Closes EXEC-550

Add the StepInfo component, a wrapper around "At step <xxx/yyy>: [CommandText]", which is used in several places in Error Recovery flows.
  • Loading branch information
mjhuff authored Jun 18, 2024
1 parent 0333837 commit adc2c1d
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 39 deletions.
1 change: 1 addition & 0 deletions app/src/assets/localization/en/error_recovery.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"at_step": "At step",
"are_you_sure_you_want_to_cancel": "Are you sure you want to cancel?",
"are_you_sure_you_want_to_resume": "Are you sure you want to resume?",
"back_to_menu": "Back to menu",
Expand Down
5 changes: 1 addition & 4 deletions app/src/organisms/ErrorRecoveryFlows/ErrorRecoveryWizard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,7 @@ export function useERWizard(): UseERWizardResult {
return { showERWizard, toggleERWizard, hasLaunchedRecovery }
}

export type ErrorRecoveryWizardProps = Omit<
ErrorRecoveryFlowsProps,
'protocolAnalysis'
> &
export type ErrorRecoveryWizardProps = ErrorRecoveryFlowsProps &
ERUtilsResults & {
robotType: RobotType
}
Expand Down
46 changes: 20 additions & 26 deletions app/src/organisms/ErrorRecoveryFlows/RunPausedSplash.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,34 +16,34 @@ import {
OVERFLOW_WRAP_BREAK_WORD,
DISPLAY_FLEX,
JUSTIFY_SPACE_BETWEEN,
TEXT_ALIGN_CENTER,
} from '@opentrons/components'

import { getIsOnDevice } from '../../redux/config'
import { getErrorKind, useErrorMessage, useErrorName } from './hooks'
import { getErrorKind, useErrorName } from './hooks'
import { LargeButton } from '../../atoms/buttons'
import { RECOVERY_MAP } from './constants'
import { StepInfo } from './shared'

import type { FailedCommand } from './types'
import type { UseRouteUpdateActionsResult } from './hooks'
import type { RobotType } from '@opentrons/shared-data'
import type { ErrorRecoveryFlowsProps } from '.'
import type { ERUtilsResults } from './hooks'

export function useRunPausedSplash(): boolean {
return useSelector(getIsOnDevice)
}

interface RunPausedSplashProps {
type RunPausedSplashProps = ERUtilsResults & {
failedCommand: ErrorRecoveryFlowsProps['failedCommand']
protocolAnalysis: ErrorRecoveryFlowsProps['protocolAnalysis']
robotType: RobotType
toggleERWiz: (launchER: boolean) => Promise<void>
routeUpdateActions: UseRouteUpdateActionsResult
failedCommand: FailedCommand | null
}
export function RunPausedSplash({
toggleERWiz,
routeUpdateActions,
failedCommand,
}: RunPausedSplashProps): JSX.Element {
export function RunPausedSplash(props: RunPausedSplashProps): JSX.Element {
const { toggleERWiz, routeUpdateActions, failedCommand } = props
const { t } = useTranslation('error_recovery')
const errorKind = getErrorKind(failedCommand?.error?.errorType)
const title = useErrorName(errorKind)
const subText = useErrorMessage(errorKind)

const { proceedToRouteAndStep } = routeUpdateActions

Expand Down Expand Up @@ -82,7 +82,14 @@ export function RunPausedSplash({
<SplashHeader>{title}</SplashHeader>
</Flex>
<Flex width="49rem" justifyContent={JUSTIFY_CENTER}>
<SplashBody>{subText}</SplashBody>
<StepInfo
{...props}
as="h3Bold"
overflow="hidden"
overflowWrap={OVERFLOW_WRAP_BREAK_WORD}
color={COLORS.white}
textAlign={TEXT_ALIGN_CENTER}
/>
</Flex>
</SplashFrame>
<Flex justifyContent={JUSTIFY_SPACE_BETWEEN} gridGap={SPACING.spacing16}>
Expand Down Expand Up @@ -112,19 +119,6 @@ const SplashHeader = styled.h1`
line-height: ${TYPOGRAPHY.lineHeight96};
color: ${COLORS.white};
`
const SplashBody = styled.h4`
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 4;
overflow: hidden;
overflow-wrap: ${OVERFLOW_WRAP_BREAK_WORD};
font-weight: ${TYPOGRAPHY.fontWeightSemiBold};
text-align: ${TYPOGRAPHY.textAlignCenter};
text-transform: ${TYPOGRAPHY.textTransformCapitalize};
font-size: ${TYPOGRAPHY.fontSize32};
line-height: ${TYPOGRAPHY.lineHeight42};
color: ${COLORS.white};
`

const SplashFrame = styled(Flex)`
width: 100%;
Expand Down
2 changes: 2 additions & 0 deletions app/src/organisms/ErrorRecoveryFlows/__fixtures__/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ export const mockRecoveryContentProps: RecoveryContentProps = {
failedLabwareUtils: { pickUpTipLabware: mockPickUpTipLabware } as any,
failedPipetteInfo: {} as any,
recoveryMapUtils: {} as any,
stepCounts: {} as any,
protocolAnalysis: { commands: [mockFailedCommand] } as any,
trackExternalMap: () => null,
hasLaunchedRecovery: true,
getRecoveryOptionCopy: () => 'MOCK_COPY',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,17 @@ import { COLORS } from '@opentrons/components'

import { renderWithProviders } from '../../../__testing-utils__'
import { i18n } from '../../../i18n'
import { mockFailedCommand } from '../__fixtures__'
import { mockRecoveryContentProps } from '../__fixtures__'
import { getIsOnDevice } from '../../../redux/config'
import { useRunPausedSplash, RunPausedSplash } from '../RunPausedSplash'
import { StepInfo } from '../shared'

import type { Store } from 'redux'
import { QueryClient, QueryClientProvider } from 'react-query'
import { Provider } from 'react-redux'

vi.mock('../../../redux/config')
vi.mock('../shared')

const store: Store<any> = createStore(vi.fn(), {})

Expand Down Expand Up @@ -51,7 +53,7 @@ const render = (props: React.ComponentProps<typeof RunPausedSplash>) => {
)
}

describe('ConfirmCancelRunModal', () => {
describe('RunPausedSplash', () => {
let props: React.ComponentProps<typeof RunPausedSplash>
const mockToggleERWiz = vi.fn(() => Promise.resolve())
const mockProceedToRouteAndStep = vi.fn()
Expand All @@ -61,10 +63,12 @@ describe('ConfirmCancelRunModal', () => {

beforeEach(() => {
props = {
...mockRecoveryContentProps,
toggleERWiz: mockToggleERWiz,
routeUpdateActions: mockRouteUpdateActions,
failedCommand: mockFailedCommand,
}

vi.mocked(StepInfo).mockReturnValue(<div>MOCK STEP INFO</div>)
})

afterEach(() => {
Expand All @@ -74,7 +78,20 @@ describe('ConfirmCancelRunModal', () => {
it('should render a generic paused screen if there is no handled errorType', () => {
render(props)
screen.getByText('Error')
screen.getByText('<Placeholder>')
screen.getByText('MOCK STEP INFO')
})

it('should render an overpressure error type if the errorType is overpressure', () => {
props = {
...props,
failedCommand: {
...props.failedCommand,
error: { errorType: 'overpressure' },
} as any,
}
render(props)
screen.getByText('Pipette overpressure')
screen.getByText('MOCK STEP INFO')
})

it('should contain buttons with expected appearance and behavior', async () => {
Expand Down
9 changes: 8 additions & 1 deletion app/src/organisms/ErrorRecoveryFlows/hooks/useERUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
useNotifyRunQuery,
} from '../../../resources/runs'
import { useRecoveryOptionCopy } from './useRecoveryOptionCopy'
import { useRunningStepCounts } from '../../../resources/protocols/hooks'

import type { PipetteData } from '@opentrons/api-client'
import type { IRecoveryMap } from '../types'
Expand All @@ -22,6 +23,7 @@ import type { RecoveryTipStatusUtils } from './useRecoveryTipStatus'
import type { UseFailedLabwareUtilsResult } from './useFailedLabwareUtils'
import type { UseRecoveryMapUtilsResult } from './useRecoveryMapUtils'
import type { CurrentRecoveryOptionUtils } from './useRecoveryRouting'
import type { StepCounts } from '../../../resources/protocols/hooks'

type ERUtilsProps = ErrorRecoveryFlowsProps & {
toggleERWizard: (launchER: boolean) => Promise<void>
Expand All @@ -40,6 +42,7 @@ export interface ERUtilsResults {
failedPipetteInfo: PipetteData | null
hasLaunchedRecovery: boolean
trackExternalMap: (map: Record<string, any>) => void
stepCounts: StepCounts
}

// Builds various Error Recovery utilities.
Expand All @@ -54,7 +57,8 @@ export function useERUtils({
const { data: attachedInstruments } = useInstrumentsQuery()
const { data: runRecord } = useNotifyRunQuery(runId)
// TODO(jh, 06-04-24): Refactor the utilities that derive info
// from runCommands once the server yields that info directly on an existing/new endpoint.
// from runCommands once the server yields that info directly on an existing/new endpoint. We'll still need this with a
// pageLength of 1 though for stepCount things.
// Note that pageLength: 999 is ok only because we fetch this on mount. We use 999 because it should hopefully
// provide the commands necessary for ER without taxing the server too heavily. This is NOT intended for produciton!
const { data: runCommands } = useNotifyAllCommandsQuery(runId, {
Expand Down Expand Up @@ -111,6 +115,8 @@ export function useERUtils({
failedLabwareUtils,
})

const stepCounts = useRunningStepCounts(runId, runCommands)

// TODO(jh, 06-14-24): Ensure other string build utilities that are internal to ErrorRecoveryFlows are exported under
// one utility object in useERUtils.
const getRecoveryOptionCopy = useRecoveryOptionCopy()
Expand All @@ -127,5 +133,6 @@ export function useERUtils({
failedPipetteInfo,
recoveryMapUtils,
getRecoveryOptionCopy,
stepCounts,
}
}
9 changes: 5 additions & 4 deletions app/src/organisms/ErrorRecoveryFlows/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,7 @@ export function ErrorRecoveryFlows(
toggleERWizard,
})

const { protocolAnalysis, ...restProps } = props
const { protocolAnalysis } = props
const robotType = protocolAnalysis?.robotType ?? OT2_ROBOT_TYPE

if (!enableRunNotes) {
Expand All @@ -117,16 +117,17 @@ export function ErrorRecoveryFlows(
<>
{showERWizard ? (
<ErrorRecoveryWizard
{...restProps}
{...props}
{...recoveryUtils}
robotType={robotType}
/>
) : null}
{showSplash ? (
<RunPausedSplash
failedCommand={props.failedCommand}
{...props}
{...recoveryUtils}
robotType={robotType}
toggleERWiz={toggleERWizard}
routeUpdateActions={recoveryUtils.routeUpdateActions}
/>
) : null}
</>
Expand Down
54 changes: 54 additions & 0 deletions app/src/organisms/ErrorRecoveryFlows/shared/StepInfo.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
import * as React from 'react'

import { useTranslation } from 'react-i18next'

import { Flex, StyledText, DISPLAY_INLINE } from '@opentrons/components'

import { CommandText } from '../../../molecules/Command'

import type { StyleProps } from '@opentrons/components'
import type { RecoveryContentProps } from '../types'

interface StepInfoProps extends StyleProps {
as: React.ComponentProps<typeof StyledText>['as']
stepCounts: RecoveryContentProps['stepCounts']
failedCommand: RecoveryContentProps['failedCommand']
robotType: RecoveryContentProps['robotType']
protocolAnalysis: RecoveryContentProps['protocolAnalysis']
}

export function StepInfo({
as,
stepCounts,
failedCommand,
robotType,
protocolAnalysis,
...styleProps
}: StepInfoProps): JSX.Element {
const { t } = useTranslation('error_recovery')
const { currentStepNumber, totalStepCount } = stepCounts

const analysisCommand = protocolAnalysis?.commands.find(
command => command.key === failedCommand?.key
)

const currentCopy = currentStepNumber ?? '?'
const totalCopy = totalStepCount ?? '?'

// TODO(jh 06-17-24): Once design decides what to do with CommandText, update it here.
return (
<Flex display={DISPLAY_INLINE} {...styleProps}>
<StyledText as={as} display={DISPLAY_INLINE}>
{`${t('at_step')} ${currentCopy}/${totalCopy}: `}
</StyledText>
{analysisCommand != null && protocolAnalysis != null ? (
<CommandText
command={analysisCommand}
commandTextData={protocolAnalysis}
robotType={robotType}
display={DISPLAY_INLINE}
/>
) : null}
</Flex>
)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
import * as React from 'react'
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { screen } from '@testing-library/react'

import { renderWithProviders } from '../../../../__testing-utils__'
import { mockRecoveryContentProps } from '../../__fixtures__'
import { i18n } from '../../../../i18n'
import { StepInfo } from '../StepInfo'
import { CommandText } from '../../../../molecules/Command'

vi.mock('../../../../molecules/Command')

const render = (props: React.ComponentProps<typeof StepInfo>) => {
return renderWithProviders(<StepInfo {...props} />, {
i18nInstance: i18n,
})[0]
}

describe('StepInfo', () => {
let props: React.ComponentProps<typeof StepInfo>

beforeEach(() => {
props = {
...mockRecoveryContentProps,
as: 'h4',
stepCounts: {
currentStepNumber: 5,
totalStepCount: 10,
hasRunDiverged: false,
},
}

vi.mocked(CommandText).mockReturnValue(<div>MOCK COMMAND TEXT</div>)
})

it('renders the step information with the current step and total steps', () => {
render(props)

screen.getByText('At step 5/10:')
})

it('renders "?" for current step and total steps when they are not available', () => {
props = {
...props,
stepCounts: {
currentStepNumber: null,
totalStepCount: null,
} as any,
}
render(props)

screen.getByText('At step ?/?:')
})

it('renders the CommandText component when the analysis command is found', () => {
render(props)

screen.getByText('MOCK COMMAND TEXT')
})

it('does not render the CommandText component when the analysis command is not found', () => {
render(props)

expect(screen.queryByText('Failed Command')).not.toBeInTheDocument()
})

it('does not render the CommandText component when protocolAnalysis is not available', () => {
props.protocolAnalysis = null
render(props)

expect(screen.queryByText('Failed Command')).not.toBeInTheDocument()
})
})
1 change: 1 addition & 0 deletions app/src/organisms/ErrorRecoveryFlows/shared/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ export { RecoveryMap, LabwareHighlight } from './RecoveryMap'
export { LeftColumnTipInfo } from './LeftColumnTipInfo'
export { TipSelection } from './TipSelection'
export { TipSelectionModal } from './TipSelectionModal'
export { StepInfo } from './StepInfo'

0 comments on commit adc2c1d

Please sign in to comment.