diff --git a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx index 8d65ef71417..0bfa08ce47b 100644 --- a/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx +++ b/app/src/organisms/Devices/ProtocolRun/ProtocolRunHeader.tsx @@ -174,7 +174,6 @@ export function ProtocolRunHeader({ const [pipettesWithTip, setPipettesWithTip] = React.useState< PipettesWithTip[] >([]) - const [closeTerminalBanner, setCloseTerminalBanner] = React.useState(false) const isResetRunLoadingRef = React.useRef(false) const { data: runRecord } = useNotifyRunQuery(runId, { staleTime: Infinity }) const highestPriorityError = @@ -200,7 +199,7 @@ export function ProtocolRunHeader({ const { data: doorStatus } = useDoorQuery({ refetchInterval: EQUIPMENT_POLL_MS, }) - let isDoorOpen = false + let isDoorOpen: boolean if (isFlex) { isDoorOpen = doorStatus?.data.status === 'open' } else if (!isFlex && Boolean(doorSafetySetting?.value)) { @@ -248,7 +247,9 @@ export function ProtocolRunHeader({ } }, [protocolData, isRobotViewable, history]) + // Side effects dependent on the current run state. React.useEffect(() => { + // After a user-initiated stopped run, close the run current run automatically. if (runStatus === RUN_STATUS_STOPPED && isRunCurrent && runId != null) { trackProtocolRunEvent({ name: ANALYTICS_PROTOCOL_RUN_FINISH, @@ -260,12 +261,6 @@ export function ProtocolRunHeader({ } }, [runStatus, isRunCurrent, runId, closeCurrentRun]) - React.useEffect(() => { - if (runStatus === RUN_STATUS_IDLE) { - setCloseTerminalBanner(false) - } - }, [runStatus]) - const startedAtTimestamp = startedAt != null ? formatTimestamp(startedAt) : EMPTY_TIMESTAMP @@ -310,7 +305,6 @@ export function ProtocolRunHeader({ properties: robotAnalyticsData ?? undefined, }) closeCurrentRun() - setCloseTerminalBanner(true) } return ( @@ -375,7 +369,7 @@ export function ProtocolRunHeader({ CANCELLABLE_STATUSES.includes(runStatus) ? ( {t('shared:close_robot_door')} ) : null} - {mostRecentRunId === runId && !closeTerminalBanner ? ( + {mostRecentRunId === runId ? ( ) : null} {mostRecentRunId === runId && @@ -479,7 +474,9 @@ export function ProtocolRunHeader({ setShowDropTipWizard(false) setPipettesWithTip(prevPipettesWithTip => { const pipettesWithTip = prevPipettesWithTip.slice(1) ?? [] - if (pipettesWithTip.length === 0) closeCurrentRun() + if (pipettesWithTip.length === 0) { + closeCurrentRun() + } return pipettesWithTip }) }} @@ -570,6 +567,7 @@ interface ActionButtonProps { isResetRunLoadingRef: React.MutableRefObject } +// TODO(jh, 04-22-2024): Refactor switch cases into separate factories to increase readability and testability. function ActionButton(props: ActionButtonProps): JSX.Element { const { runId, @@ -613,9 +611,7 @@ function ActionButton(props: ActionButtonProps): JSX.Element { robotName, runId ) - const [showIsShakingModal, setShowIsShakingModal] = React.useState( - false - ) + const [showIsShakingModal, setShowIsShakingModal] = React.useState(false) const isSetupComplete = isCalibrationComplete && isModuleCalibrationComplete && @@ -804,12 +800,14 @@ function ActionButton(props: ActionButtonProps): JSX.Element { ) } +// TODO(jh 04-24-2024): Split TerminalRunBanner into a RunSuccessBanner and RunFailedBanner. interface TerminalRunProps { runStatus: RunStatus | null handleClearClick: () => void isClosingCurrentRun: boolean setShowRunFailedModal: (showRunFailedModal: boolean) => void isResetRunLoading: boolean + isRunCurrent: boolean highestPriorityError?: RunError | null } function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null { @@ -820,51 +818,64 @@ function TerminalRunBanner(props: TerminalRunProps): JSX.Element | null { setShowRunFailedModal, highestPriorityError, isResetRunLoading, + isRunCurrent, } = props const { t } = useTranslation('run_details') - const handleClick = (): void => { + const handleRunSuccessClick = (): void => { + handleClearClick() + } + + const handleFailedRunClick = (): void => { handleClearClick() setShowRunFailedModal(true) } - if ( - isResetRunLoading === false && - (runStatus === RUN_STATUS_FAILED || runStatus === RUN_STATUS_SUCCEEDED) - ) { + const buildSuccessBanner = (): JSX.Element => { return ( - <> - {runStatus === RUN_STATUS_SUCCEEDED ? ( - - - {t('run_completed')} - - - ) : ( - - - - {t('error_info', { - errorType: highestPriorityError?.errorType, - errorCode: highestPriorityError?.errorCode, - })} - + + + {t('run_completed')} + + + ) + } - - {t('view_error')} - - - - )} - + const buildErrorBanner = (): JSX.Element => { + return ( + + + + {t('error_info', { + errorType: highestPriorityError?.errorType, + errorCode: highestPriorityError?.errorCode, + })} + + + + {t('view_error')} + + + ) } - return null + + if ( + runStatus === RUN_STATUS_SUCCEEDED && + isRunCurrent && + !isResetRunLoading + ) { + return buildSuccessBanner() + } else if (runStatus === RUN_STATUS_FAILED && !isResetRunLoading) { + return buildErrorBanner() + } else { + return null + } } diff --git a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx index 65ea98c906f..3b6f0f9025b 100644 --- a/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx +++ b/app/src/organisms/Devices/ProtocolRun/__tests__/ProtocolRunHeader.test.tsx @@ -814,7 +814,7 @@ describe('ProtocolRunHeader', () => { screen.getByText('Run completed.') }) - it('clicking close on a terminal run banner closes the run context and dismisses the banner', async () => { + it('clicking close on a terminal run banner closes the run context', async () => { when(vi.mocked(useNotifyRunQuery)) .calledWith(RUN_ID) .thenReturn({ @@ -827,9 +827,20 @@ describe('ProtocolRunHeader', () => { fireEvent.click(screen.getByTestId('Banner_close-button')) expect(mockCloseCurrentRun).toBeCalled() - await waitFor(() => { - expect(screen.queryByText('Run completed.')).not.toBeInTheDocument() - }) + }) + + it('does not display the "run successful" banner if the successful run is not current', async () => { + when(vi.mocked(useNotifyRunQuery)) + .calledWith(RUN_ID) + .thenReturn({ + data: { data: { ...mockSucceededRun, current: false } }, + } as UseQueryResult) + when(vi.mocked(useRunStatus)) + .calledWith(RUN_ID) + .thenReturn(RUN_STATUS_SUCCEEDED) + render() + + expect(screen.queryByText('Run completed.')).not.toBeInTheDocument() }) it('if a heater shaker is shaking, clicking on start run should render HeaterShakerIsRunningModal', async () => {