Skip to content

Commit

Permalink
feat(odd): add Confirm Run cancel modal (#12468)
Browse files Browse the repository at this point in the history
feat(odd): add Confirm Run Cancel modal
close RCORE-562
koji authored Apr 12, 2023
1 parent 5625275 commit bc6bd6b
Showing 9 changed files with 260 additions and 17 deletions.
2 changes: 2 additions & 0 deletions app/src/molecules/Modal/OnDeviceDisplay/Modal.tsx
Original file line number Diff line number Diff line change
@@ -15,11 +15,13 @@ import type { ModalHeaderBaseProps, ModalSize } from './types'
interface ModalProps {
/** clicking anywhere outside of the modal closes it */
onOutsideClick: React.MouseEventHandler
/** modal content */
children: React.ReactNode
/** for small, medium, or large modal sizes, medium by default */
modalSize?: ModalSize
/** see ModalHeader component for more details */
header?: ModalHeaderBaseProps
/** an option for adding additional styles for an error modal */
isError?: boolean
}
export function Modal(props: ModalProps): JSX.Element {
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import * as React from 'react'
import { useTranslation } from 'react-i18next'
import { useHistory } from 'react-router-dom'

import {
COLORS,
DIRECTION_COLUMN,
DIRECTION_ROW,
Flex,
TYPOGRAPHY,
SPACING,
} from '@opentrons/components'
import { useStopRunMutation } from '@opentrons/react-api-client'

import { StyledText } from '../../../atoms/text'
import { SmallButton } from '../../../atoms/buttons/OnDeviceDisplay'
import { Modal } from '../../../molecules/Modal/OnDeviceDisplay/Modal'
import { useTrackProtocolRunEvent } from '../../../organisms/Devices/hooks'

import type { ModalHeaderBaseProps } from '../../../molecules/Modal/OnDeviceDisplay/types'

interface ConfirmCancelRunModalProps {
runId: string
setShowConfirmCancelRunModal: (showConfirmCancelRunModal: boolean) => void
}

export function ConfirmCancelRunModal({
runId,
setShowConfirmCancelRunModal,
}: ConfirmCancelRunModalProps): JSX.Element {
const { t } = useTranslation(['run_details', 'shared'])
const { stopRun } = useStopRunMutation()
const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId)
const history = useHistory()
const [isCanceling, setIsCanceling] = React.useState(false)

const modalHeader: ModalHeaderBaseProps = {
title: t('cancel_run_modal_heading'),
hasExitIcon: false,
iconName: 'ot-alert',
iconColor: COLORS.yellow_two,
}

const handleCancelRun = (): void => {
stopRun(runId, {
onSuccess: () => {
trackProtocolRunEvent({ name: 'runCancel' })
history.push('/dashboard')
},
onError: () => {
setIsCanceling(false)
},
})
}

return (
<Modal
modalSize="medium"
header={modalHeader}
onOutsideClick={() => setShowConfirmCancelRunModal(false)}
>
<Flex flexDirection={DIRECTION_COLUMN}>
<Flex flexDirection={DIRECTION_COLUMN}>
<StyledText
fontSize={TYPOGRAPHY.fontSize22}
lineHeight={TYPOGRAPHY.lineHeight28}
fontWeight={TYPOGRAPHY.fontWeightRegular}
>
{t('cancel_run_alert_info')}
</StyledText>
<StyledText
fontSize={TYPOGRAPHY.fontSize22}
lineHeight={TYPOGRAPHY.lineHeight28}
fontWeight={TYPOGRAPHY.fontWeightRegular}
>
{t('cancel_run_module_info')}
</StyledText>
</Flex>
<Flex
marginTop={SPACING.spacing6}
flexDirection={DIRECTION_ROW}
gridGap={SPACING.spacing3}
width="100%"
>
<SmallButton
buttonType="default"
buttonText={t('shared:go_back')}
onClick={() => setShowConfirmCancelRunModal(false)}
/>
<SmallButton
buttonType="alert"
buttonText={t('cancel_run')}
onClick={handleCancelRun}
disabled={isCanceling}
/>
</Flex>
</Flex>
</Modal>
)
}
Original file line number Diff line number Diff line change
@@ -84,7 +84,7 @@ interface CurrentRunningProtocolCommandProps {
runTimerInfo: RunTimerInfo
playRun: () => void
pauseRun: () => void
stopRun: () => void
setShowConfirmCancelRunModal: (showConfirmCancelRunModal: boolean) => void
trackProtocolRunEvent: TrackProtocolRunEvent
protocolName?: string
currentRunCommandIndex?: number
@@ -96,7 +96,7 @@ export function CurrentRunningProtocolCommand({
runTimerInfo,
playRun,
pauseRun,
stopRun,
setShowConfirmCancelRunModal,
trackProtocolRunEvent,
protocolName,
currentRunCommandIndex,
@@ -110,8 +110,7 @@ export function CurrentRunningProtocolCommand({
// ToDo (kj:04/09/2023 Add confirm modal)
// jira ticket RCORE-562 and RCORE-563
const onStop = (): void => {
stopRun() // from useRunActionMutations
trackProtocolRunEvent({ name: 'runCancel' })
setShowConfirmCancelRunModal(true)
}

const onTogglePlayPause = (): void => {
Original file line number Diff line number Diff line change
@@ -45,7 +45,7 @@ interface PlayPauseButtonProps {
onTogglePlayPause?: () => void
/** default size 12.5rem */
buttonSize?: string
/** default size 10rem */
/** default size 5rem */
iconSize?: string
runStatus?: RunStatus | null
}
Original file line number Diff line number Diff line change
@@ -64,7 +64,7 @@ interface RunningProtocolCommandListProps {
robotSideAnalysis: CompletedProtocolAnalysis | null
playRun: () => void
pauseRun: () => void
stopRun: () => void
setShowConfirmCancelRunModal: (showConfirmCancelRunModal: boolean) => void
trackProtocolRunEvent: TrackProtocolRunEvent
protocolName?: string
currentRunCommandIndex?: number
@@ -75,7 +75,7 @@ export function RunningProtocolCommandList({
robotSideAnalysis,
playRun,
pauseRun,
stopRun,
setShowConfirmCancelRunModal,
trackProtocolRunEvent,
protocolName,
currentRunCommandIndex,
@@ -86,9 +86,7 @@ export function RunningProtocolCommandList({
const currentRunStatus = t(`status_${runStatus}`)

const onStop = (): void => {
stopRun()
// ToDo (kj:03/28/2023) update event information name & properties
trackProtocolRunEvent({ name: 'runCancel', properties: {} })
setShowConfirmCancelRunModal(true)
}

const onTogglePlayPause = (): void => {
@@ -127,6 +125,7 @@ export function RunningProtocolCommandList({
onTogglePlayPause={onTogglePlayPause}
buttonSize="6.25rem"
runStatus={runStatus}
iconSize="2.5rem"
/>
</Flex>
</Flex>
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import * as React from 'react'
import { when, resetAllWhenMocks } from 'jest-when'
import { MemoryRouter } from 'react-router-dom'
import { fireEvent } from '@testing-library/react'

import { renderWithProviders } from '@opentrons/components'
import { useStopRunMutation } from '@opentrons/react-api-client'

import { i18n } from '../../../../i18n'
import { useTrackProtocolRunEvent } from '../../../../organisms/Devices/hooks'
import { useTrackEvent } from '../../../../redux/analytics'

import { ConfirmCancelRunModal } from '../ConfirmCancelRunModal'

jest.mock('@opentrons/react-api-client')
jest.mock('../../../../organisms/Devices/hooks')
jest.mock('../../../../redux/analytics')

const mockPush = jest.fn()
let mockStopRun: jest.Mock
let mockTrackEvent: jest.Mock
let mockTrackProtocolRunEvent: jest.Mock

jest.mock('react-router-dom', () => {
const reactRouterDom = jest.requireActual('react-router-dom')
return {
...reactRouterDom,
useHistory: () => ({ push: mockPush } as any),
}
})

const mockUseTrackProtocolRunEvent = useTrackProtocolRunEvent as jest.MockedFunction<
typeof useTrackProtocolRunEvent
>
const mockUseTrackEvent = useTrackEvent as jest.MockedFunction<
typeof useTrackEvent
>
const mockUseStopRunMutation = useStopRunMutation as jest.MockedFunction<
typeof useStopRunMutation
>

const render = (props: React.ComponentProps<typeof ConfirmCancelRunModal>) => {
return renderWithProviders(
<MemoryRouter>
<ConfirmCancelRunModal {...props} />
</MemoryRouter>,
{
i18nInstance: i18n,
}
)
}

const RUN_ID = 'mock_runID'
const mockFn = jest.fn()

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

beforeEach(() => {
props = {
runId: RUN_ID,
setShowConfirmCancelRunModal: mockFn,
}
mockTrackEvent = jest.fn()
mockStopRun = jest.fn((_runId, opts) => opts.onSuccess())
mockTrackProtocolRunEvent = jest.fn(
() => new Promise(resolve => resolve({}))
)
mockUseStopRunMutation.mockReturnValue({ stopRun: mockStopRun } as any)
mockUseTrackEvent.mockReturnValue(mockTrackEvent)
when(mockUseTrackProtocolRunEvent).calledWith(RUN_ID).mockReturnValue({
trackProtocolRunEvent: mockTrackProtocolRunEvent,
})
})

afterEach(() => {
resetAllWhenMocks()
jest.restoreAllMocks()
})

it('should render text and buttons', () => {
const [{ getByText, getAllByRole }] = render(props)
getByText('Are you sure you want to cancel this run?')
getByText(
'Doing so will terminate this run, drop any attached tips in the trash container and home your robot.'
)
getByText(
'Additionally, any hardware modules used within the protocol will remain active and maintain their current states until deactivated.'
)
expect(getAllByRole('button').length).toBe(2)
getByText('Go back')
getByText('Cancel run')
})

it('when tapping go back, the mock function is called', () => {
const [{ getByText }] = render(props)
const button = getByText('Go back')
fireEvent.click(button)
expect(mockFn).toHaveBeenCalled()
})

it('when tapping cancel run, the modal is closed', () => {
const [{ getByText }] = render(props)
const button = getByText('Cancel run')
fireEvent.click(button)
expect(mockStopRun).toHaveBeenCalled()
expect(mockTrackProtocolRunEvent).toHaveBeenCalled()
expect(mockPush).toHaveBeenCalledWith('/dashboard')
})
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react'
import { fireEvent } from '@testing-library/react'

import { renderWithProviders } from '@opentrons/components'
import { RUN_STATUS_RUNNING, RUN_STATUS_IDLE } from '@opentrons/api-client'
@@ -9,7 +10,7 @@ import { CurrentRunningProtocolCommand } from '../CurrentRunningProtocolCommand'

const mockPlayRun = jest.fn()
const mockPauseRun = jest.fn()
const mockStopRun = jest.fn()
const mockShowModal = jest.fn()

const mockRunTimer = {
runStatus: RUN_STATUS_RUNNING,
@@ -36,7 +37,7 @@ describe('CurrentRunningProtocolCommand', () => {
runTimerInfo: mockRunTimer,
playRun: mockPlayRun,
pauseRun: mockPauseRun,
stopRun: mockStopRun,
setShowConfirmCancelRunModal: mockShowModal,
trackProtocolRunEvent: jest.fn(), // temporary
protocolName: 'mockRunningProtocolName',
currentRunCommandIndex: 0,
@@ -65,4 +66,14 @@ describe('CurrentRunningProtocolCommand', () => {
const [{ getByLabelText }] = render(props)
getByLabelText('play')
})

it('when tapping stop button, the modal is showing up', () => {
const [{ getByLabelText }] = render(props)
const button = getByLabelText('stop')
fireEvent.click(button)
expect(mockShowModal).toHaveBeenCalled()
})

// ToDo (kj:04/10/2023) once we fix the track event stuff, we can implement tests
it.todo('when tapping play button, track event mock function is called')
})
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import * as React from 'react'
import { fireEvent } from '@testing-library/react'

import { renderWithProviders } from '@opentrons/components'
import { RUN_STATUS_RUNNING, RUN_STATUS_IDLE } from '@opentrons/api-client'
@@ -9,7 +10,7 @@ import { RunningProtocolCommandList } from '../RunningProtocolCommandList'

const mockPlayRun = jest.fn()
const mockPauseRun = jest.fn()
const mockStopRun = jest.fn()
const mockShowModal = jest.fn()

const render = (
props: React.ComponentProps<typeof RunningProtocolCommandList>
@@ -27,7 +28,7 @@ describe('RunningProtocolCommandList', () => {
robotSideAnalysis: mockRobotSideAnalysis,
playRun: mockPlayRun,
pauseRun: mockPauseRun,
stopRun: mockStopRun,
setShowConfirmCancelRunModal: mockShowModal,
trackProtocolRunEvent: jest.fn(), // temporary
protocolName: 'mockRunningProtocolName',
currentRunCommandIndex: 0,
@@ -50,4 +51,14 @@ describe('RunningProtocolCommandList', () => {
const [{ getByLabelText }] = render(props)
getByLabelText('play')
})

it('when tapping stop button, the modal is showing up', () => {
const [{ getByLabelText }] = render(props)
const button = getByLabelText('stop')
fireEvent.click(button)
expect(mockShowModal).toHaveBeenCalled()
})

// ToDo (kj:04/10/2023) once we fix the track event stuff, we can implement tests
it.todo('when tapping play button, track event mock function is called')
})
17 changes: 14 additions & 3 deletions app/src/pages/OnDeviceDisplay/RunningProtocol.tsx
Original file line number Diff line number Diff line change
@@ -35,6 +35,7 @@ import {
RunningProtocolSkeleton,
} from '../../organisms/OnDeviceDisplay/RunningProtocol'
import { useTrackProtocolRunEvent } from '../../organisms/Devices/hooks'
import { ConfirmCancelRunModal } from '../../organisms/OnDeviceDisplay/RunningProtocol/ConfirmCancelRunModal'

import type { OnDeviceRouteParams } from '../../App/types'

@@ -61,6 +62,10 @@ export function RunningProtocol(): JSX.Element {
const [currentOption, setCurrentOption] = React.useState<ScreenOption>(
'CurrentRunningProtocolCommand'
)
const [
showConfirmCancelRunModal,
setShowConfirmCancelRunModal,
] = React.useState<boolean>(false)
const swipe = useSwipe()
const robotSideAnalysis = useMostRecentCompletedAnalysis(runId)
const currentRunCommandKey = useLastRunCommandKey(runId)
@@ -78,7 +83,7 @@ export function RunningProtocol(): JSX.Element {
const protocolName =
protocolRecord?.data.metadata.protocolName ??
protocolRecord?.data.files[0].name
const { playRun, pauseRun, stopRun } = useRunActionMutations(runId)
const { playRun, pauseRun } = useRunActionMutations(runId)
const { trackProtocolRunEvent } = useTrackProtocolRunEvent(runId)

React.useEffect(() => {
@@ -117,6 +122,12 @@ export function RunningProtocol(): JSX.Element {
OnDevice
/>
) : null}
{showConfirmCancelRunModal ? (
<ConfirmCancelRunModal
runId={runId}
setShowConfirmCancelRunModal={setShowConfirmCancelRunModal}
/>
) : null}
<Flex
ref={swipe.ref}
padding={`1.75rem ${SPACING.spacingXXL} ${SPACING.spacingXXL}`}
@@ -127,7 +138,7 @@ export function RunningProtocol(): JSX.Element {
<CurrentRunningProtocolCommand
playRun={playRun}
pauseRun={pauseRun}
stopRun={stopRun}
setShowConfirmCancelRunModal={setShowConfirmCancelRunModal}
trackProtocolRunEvent={trackProtocolRunEvent}
protocolName={protocolName}
runStatus={runStatus}
@@ -141,7 +152,7 @@ export function RunningProtocol(): JSX.Element {
runStatus={runStatus}
playRun={playRun}
pauseRun={pauseRun}
stopRun={stopRun}
setShowConfirmCancelRunModal={setShowConfirmCancelRunModal}
trackProtocolRunEvent={trackProtocolRunEvent}
currentRunCommandIndex={currentRunCommandIndex}
robotSideAnalysis={robotSideAnalysis}

0 comments on commit bc6bd6b

Please sign in to comment.