diff --git a/apps/scan/frontend/src/screens/poll_worker_screen.test.tsx b/apps/scan/frontend/src/screens/poll_worker_screen.test.tsx index eecd9a3edd..a6cd86a339 100644 --- a/apps/scan/frontend/src/screens/poll_worker_screen.test.tsx +++ b/apps/scan/frontend/src/screens/poll_worker_screen.test.tsx @@ -386,7 +386,7 @@ describe('must have printer attached to transition polls and print reports', () }); describe('must have usb drive attached to transition polls', () => { - test('polls open', async () => { + test('opening polls', async () => { apiMock.expectGetPollsInfo('polls_closed_initial'); apiMock.setPrinterStatusV3({ connected: true }); apiMock.expectGetUsbDriveStatus('no_drive'); @@ -405,7 +405,7 @@ describe('must have usb drive attached to transition polls', () => { await screen.findByText('Print Additional Polls Opened Report'); }); - test('polls open from fallback screen', async () => { + test('opening polls from fallback screen', async () => { apiMock.expectGetPollsInfo('polls_closed_initial'); apiMock.setPrinterStatusV3({ connected: true }); apiMock.expectGetUsbDriveStatus('no_drive'); @@ -431,7 +431,7 @@ describe('must have usb drive attached to transition polls', () => { await screen.findByText('Print Additional Polls Opened Report'); }); - test('polls paused', async () => { + test('resuming voting', async () => { apiMock.expectGetPollsInfo('polls_paused'); apiMock.setPrinterStatusV3({ connected: true }); apiMock.expectGetUsbDriveStatus('no_drive'); @@ -451,7 +451,7 @@ describe('must have usb drive attached to transition polls', () => { await screen.findByText('Voting Resumed'); }); - test('polls paused from fallback screen', async () => { + test('resuming voting from fallback screen', async () => { apiMock.expectGetPollsInfo('polls_paused'); apiMock.setPrinterStatusV3({ connected: true }); apiMock.expectGetUsbDriveStatus('no_drive'); @@ -476,7 +476,7 @@ describe('must have usb drive attached to transition polls', () => { await screen.findByText('Print Additional Polls Closed Report'); }); - test('polls close', async () => { + test('closing polls', async () => { apiMock.expectGetPollsInfo('polls_open'); apiMock.setPrinterStatusV3({ connected: true }); apiMock.expectGetUsbDriveStatus('no_drive'); @@ -497,7 +497,7 @@ describe('must have usb drive attached to transition polls', () => { expect(startNewVoterSessionMock).toHaveBeenCalledTimes(1); }); - test('polls close from fallback screen', async () => { + test('closing polls from fallback screen', async () => { apiMock.expectGetPollsInfo('polls_open'); apiMock.setPrinterStatusV3({ connected: true }); apiMock.expectGetUsbDriveStatus('no_drive'); @@ -524,6 +524,172 @@ describe('must have usb drive attached to transition polls', () => { }); }); +describe('does not need usb drive attached to transition polls if continuous export disabled', () => { + beforeEach(() => { + apiMock.mockApiClient.getConfig.reset(); + apiMock.expectGetConfig({ + isContinuousExportEnabled: false, + }); + }); + + test('opening polls', async () => { + apiMock.expectGetPollsInfo('polls_closed_initial'); + apiMock.setPrinterStatusV4(); + apiMock.expectGetUsbDriveStatus('no_drive'); + renderScreen({}); + + await screen.findButton('Open Polls'); + expect( + screen.queryByText('Insert a USB drive to continue.') + ).not.toBeInTheDocument(); + + apiMock.expectOpenPolls(); + apiMock.expectPrintReportV4().resolve(); + apiMock.expectGetPollsInfo('polls_open'); + userEvent.click(screen.getButton('Open Polls')); + await screen.findByText('Opening Polls…'); + await screen.findByText('Polls Opened'); + }); + + test('opening polls from fallback screen', async () => { + apiMock.expectGetPollsInfo('polls_closed_initial'); + apiMock.setPrinterStatusV4(); + apiMock.expectGetUsbDriveStatus('no_drive'); + renderScreen({}); + + await screen.findButton('Open Polls'); + expect( + screen.queryByText('Insert a USB drive to continue.') + ).not.toBeInTheDocument(); + + userEvent.click(screen.getButton('Menu')); + apiMock.expectOpenPolls(); + apiMock.expectPrintReportV4().resolve(); + apiMock.expectGetPollsInfo('polls_open'); + userEvent.click(screen.getButton('Open Polls')); + await screen.findByText('Opening Polls…'); + await screen.findByText('Polls Opened'); + }); + + test('pausing voting', async () => { + apiMock.expectGetPollsInfo('polls_open'); + apiMock.setPrinterStatusV4(); + apiMock.expectGetUsbDriveStatus('no_drive'); + renderScreen({}); + + await screen.findButton('Close Polls'); + expect( + screen.queryByText('Insert a USB drive to continue.') + ).not.toBeInTheDocument(); + + userEvent.click(screen.getButton('Menu')); + apiMock.expectPauseVoting(); + apiMock.expectPrintReportV4().resolve(); + apiMock.expectGetPollsInfo('polls_paused'); + userEvent.click(screen.getButton('Pause Voting')); + await screen.findByText('Pausing Voting…'); + await screen.findByText('Voting Paused'); + }); + + test('resuming voting', async () => { + apiMock.expectGetPollsInfo('polls_paused'); + apiMock.setPrinterStatusV4(); + apiMock.expectGetUsbDriveStatus('no_drive'); + renderScreen({}); + + await screen.findButton('Resume Voting'); + expect( + screen.queryByText('Insert a USB drive to continue.') + ).not.toBeInTheDocument(); + + apiMock.expectResumeVoting(); + apiMock.expectPrintReportV4().resolve(); + apiMock.expectGetPollsInfo('polls_open'); + userEvent.click(screen.getButton('Resume Voting')); + await screen.findByText('Resuming Voting…'); + await screen.findByText('Voting Resumed'); + }); + + test('resuming voting from fallback screen', async () => { + apiMock.expectGetPollsInfo('polls_paused'); + apiMock.setPrinterStatusV4(); + apiMock.expectGetUsbDriveStatus('no_drive'); + renderScreen({}); + + await screen.findButton('Resume Voting'); + expect( + screen.queryByText('Insert a USB drive to continue.') + ).not.toBeInTheDocument(); + + userEvent.click(screen.getButton('Menu')); + apiMock.expectResumeVoting(); + apiMock.expectPrintReportV4().resolve(); + apiMock.expectGetPollsInfo('polls_open'); + userEvent.click(screen.getButton('Resume Voting')); + await screen.findByText('Resuming Voting…'); + await screen.findByText('Voting Resumed'); + }); + + test('closing polls', async () => { + apiMock.expectGetPollsInfo('polls_open'); + apiMock.setPrinterStatusV4(); + apiMock.expectGetUsbDriveStatus('no_drive'); + renderScreen({}); + + await screen.findButton('Close Polls'); + expect( + screen.queryByText('Insert a USB drive to continue.') + ).not.toBeInTheDocument(); + + apiMock.expectClosePolls(); + apiMock.expectPrintReportV4().resolve(); + apiMock.expectGetPollsInfo('polls_closed_final'); + userEvent.click(screen.getButton('Close Polls')); + await screen.findByText('Closing Polls…'); + await screen.findByText('Polls Closed'); + }); + + test('closing polls from fallback screen', async () => { + apiMock.expectGetPollsInfo('polls_open'); + apiMock.setPrinterStatusV4(); + apiMock.expectGetUsbDriveStatus('no_drive'); + renderScreen({}); + + await screen.findButton('Close Polls'); + expect( + screen.queryByText('Insert a USB drive to continue.') + ).not.toBeInTheDocument(); + + userEvent.click(screen.getButton('Menu')); + apiMock.expectClosePolls(); + apiMock.expectPrintReportV4().resolve(); + apiMock.expectGetPollsInfo('polls_closed_final'); + userEvent.click(screen.getButton('Close Polls')); + await screen.findByText('Closing Polls…'); + await screen.findByText('Polls Closed'); + }); + + test('closing polls from voting paused', async () => { + apiMock.expectGetPollsInfo('polls_paused'); + apiMock.setPrinterStatusV4(); + apiMock.expectGetUsbDriveStatus('no_drive'); + renderScreen({}); + + await screen.findButton('Resume Voting'); + expect( + screen.queryByText('Insert a USB drive to continue.') + ).not.toBeInTheDocument(); + + userEvent.click(screen.getButton('Menu')); + apiMock.expectClosePolls(); + apiMock.expectPrintReportV4().resolve(); + apiMock.expectGetPollsInfo('polls_closed_final'); + userEvent.click(screen.getButton('Close Polls')); + await screen.findByText('Closing Polls…'); + await screen.findByText('Polls Closed'); + }); +}); + describe('hardware V4 report printing', () => { test('single report printing happy path', async () => { apiMock.setPrinterStatusV4(); diff --git a/apps/scan/frontend/src/screens/poll_worker_screen.tsx b/apps/scan/frontend/src/screens/poll_worker_screen.tsx index 7a17748b90..bc73a83e31 100644 --- a/apps/scan/frontend/src/screens/poll_worker_screen.tsx +++ b/apps/scan/frontend/src/screens/poll_worker_screen.tsx @@ -23,7 +23,6 @@ import type { PrecinctScannerPollsInfo, PrintResult, } from '@votingworks/scan-backend'; -import type { UsbDriveStatus } from '@votingworks/usb-drive'; import { getUsbDriveStatus, printReport, @@ -34,6 +33,7 @@ import { resumeVoting as resumeVotingApi, getPollsInfo, useApiClient, + getConfig, } from '../api'; import { FullScreenPromptLayout } from '../components/full_screen_prompt_layout'; import { @@ -106,11 +106,11 @@ function PrinterAlertText({ } function UsbDriveAlertText({ - usbDriveStatus, + mustInsertUsbDriveToContinue, }: { - usbDriveStatus: UsbDriveStatus; + mustInsertUsbDriveToContinue: boolean; }): JSX.Element | null { - if (usbDriveStatus.status === 'mounted') { + if (!mustInsertUsbDriveToContinue) { return null; } @@ -123,21 +123,21 @@ function UsbDriveAlertText({ function shouldAllowTogglingPolls( printerSummary: PollsFlowPrinterSummary, - usbDriveStatus: UsbDriveStatus + mustInsertUsbDriveToContinue: boolean ): boolean { - return printerSummary.ready && usbDriveStatus.status === 'mounted'; + return printerSummary.ready && !mustInsertUsbDriveToContinue; } function OpenPollsPromptScreen({ onConfirm, onClose, printerSummary, - usbDriveStatus, + mustInsertUsbDriveToContinue, }: { onConfirm: () => void; onClose: () => void; printerSummary: PollsFlowPrinterSummary; - usbDriveStatus: UsbDriveStatus; + mustInsertUsbDriveToContinue: boolean; }): JSX.Element { return ( @@ -148,13 +148,20 @@ function OpenPollsPromptScreen({

- +
); @@ -164,12 +171,12 @@ function ResumeVotingPromptScreen({ onConfirm, onClose, printerSummary, - usbDriveStatus, + mustInsertUsbDriveToContinue, }: { onConfirm: () => void; onClose: () => void; printerSummary: PollsFlowPrinterSummary; - usbDriveStatus: UsbDriveStatus; + mustInsertUsbDriveToContinue: boolean; }): JSX.Element { return ( @@ -180,13 +187,20 @@ function ResumeVotingPromptScreen({

- +
); @@ -196,12 +210,12 @@ function ClosePollsPromptScreen({ onConfirm, onClose, printerSummary, - usbDriveStatus, + mustInsertUsbDriveToContinue, }: { onConfirm: () => void; onClose: () => void; printerSummary: PollsFlowPrinterSummary; - usbDriveStatus: UsbDriveStatus; + mustInsertUsbDriveToContinue: boolean; }): JSX.Element { return ( @@ -212,13 +226,20 @@ function ClosePollsPromptScreen({

- +
); @@ -270,6 +291,7 @@ function PollWorkerScreenContents({ }): JSX.Element { const apiClient = useApiClient(); const pollsInfoQuery = getPollsInfo.useQuery(); + const configQuery = getConfig.useQuery(); const usbDriveStatusQuery = getUsbDriveStatus.useQuery(); const printerStatusQuery = getPrinterStatus.useQuery(); const openPollsMutation = openPollsApi.useMutation(); @@ -303,7 +325,8 @@ function PollWorkerScreenContents({ if ( !usbDriveStatusQuery.isSuccess || !printerStatusQuery.isSuccess || - !pollsInfoQuery.isSuccess + !pollsInfoQuery.isSuccess || + !configQuery.isSuccess ) { return ( @@ -318,6 +341,9 @@ function PollWorkerScreenContents({ const printerStatus = printerStatusQuery.data; const printerSummary = getPollsFlowPrinterSummary(printerStatus); const { pollsState } = pollsInfo; + const { isContinuousExportEnabled } = configQuery.data; + const mustInsertUsbDriveToContinue = + isContinuousExportEnabled && usbDriveStatus.status !== 'mounted'; function showAllPollWorkerActions() { return setPollWorkerFlowState(undefined); @@ -346,7 +372,7 @@ function PollWorkerScreenContents({ } async function closePolls() { - assert(usbDriveStatus.status === 'mounted'); + assert(!mustInsertUsbDriveToContinue); setPollWorkerFlowState({ type: 'polls-transitioning', transitionType: 'close_polls', @@ -427,7 +453,7 @@ function PollWorkerScreenContents({ onConfirm={openPolls} onClose={showAllPollWorkerActions} printerSummary={printerSummary} - usbDriveStatus={usbDriveStatus} + mustInsertUsbDriveToContinue={mustInsertUsbDriveToContinue} /> ); case 'resume-voting-prompt': @@ -436,7 +462,7 @@ function PollWorkerScreenContents({ onConfirm={resumeVoting} onClose={showAllPollWorkerActions} printerSummary={printerSummary} - usbDriveStatus={usbDriveStatus} + mustInsertUsbDriveToContinue={mustInsertUsbDriveToContinue} /> ); case 'close-polls-prompt': @@ -445,7 +471,7 @@ function PollWorkerScreenContents({ onConfirm={closePolls} onClose={showAllPollWorkerActions} printerSummary={printerSummary} - usbDriveStatus={usbDriveStatus} + mustInsertUsbDriveToContinue={mustInsertUsbDriveToContinue} /> ); case 'polls-transitioning': @@ -541,7 +567,10 @@ function PollWorkerScreenContents({ variant="primary" onPress={openPolls} disabled={ - !shouldAllowTogglingPolls(printerSummary, usbDriveStatus) + !shouldAllowTogglingPolls( + printerSummary, + mustInsertUsbDriveToContinue + ) } > Open Polls @@ -565,7 +594,10 @@ function PollWorkerScreenContents({ variant="primary" onPress={closePolls} disabled={ - !shouldAllowTogglingPolls(printerSummary, usbDriveStatus) + !shouldAllowTogglingPolls( + printerSummary, + mustInsertUsbDriveToContinue + ) } > Close Polls @@ -589,7 +621,10 @@ function PollWorkerScreenContents({ variant="primary" onPress={resumeVoting} disabled={ - !shouldAllowTogglingPolls(printerSummary, usbDriveStatus) + !shouldAllowTogglingPolls( + printerSummary, + mustInsertUsbDriveToContinue + ) } > Resume Voting @@ -600,7 +635,10 @@ function PollWorkerScreenContents({