diff --git a/.env b/.env index 35d4a2514a..8ad756b757 100644 --- a/.env +++ b/.env @@ -14,4 +14,4 @@ REACT_APP_VX_SKIP_CAST_VOTE_RECORDS_AUTHENTICATION=FALSE REACT_APP_VX_DISABLE_CVR_ORIGINAL_SNAPSHOTS=FALSE REACT_APP_VX_CONVERTER=ms-sems REACT_APP_VX_SCREEN_ORIENTATION=portrait -REACT_APP_VX_PRECINCT_REPORT_DESTINATION=smartcard +REACT_APP_VX_PRECINCT_REPORT_DESTINATION=thermal-sheet-printer diff --git a/apps/scan/backend/src/app.ts b/apps/scan/backend/src/app.ts index 69b9398830..56b21c08d5 100644 --- a/apps/scan/backend/src/app.ts +++ b/apps/scan/backend/src/app.ts @@ -12,8 +12,6 @@ import { } from '@votingworks/types'; import { BooleanEnvironmentVariableName, - ScannerReportData, - ScannerReportDataSchema, isElectionManagerAuth, isFeatureFlagEnabled, singlePrecinctSelectionFor, @@ -25,7 +23,7 @@ import { ExportCastVoteRecordReportToUsbDriveError, readBallotPackageFromUsb, } from '@votingworks/backend'; -import { assert, err, iter, ok, Result } from '@votingworks/basics'; +import { assert, iter, ok, Result } from '@votingworks/basics'; import { ArtifactAuthenticatorApi, InsertedSmartCardAuthApi, @@ -355,24 +353,6 @@ function buildApi( supportsUltrasonic(): boolean { return machine.supportsUltrasonic(); }, - - async saveScannerReportDataToCard(input: { - scannerReportData: ScannerReportData; - }): Promise> { - const machineState = constructAuthMachineState(workspace); - const authStatus = await auth.getAuthStatus(machineState); - if (authStatus.status !== 'logged_in') { - return err(new Error('User is not logged in')); - } - if (authStatus.user.role !== 'poll_worker') { - return err(new Error('User is not a poll worker')); - } - - return await auth.writeCardData(machineState, { - data: input.scannerReportData, - schema: ScannerReportDataSchema, - }); - }, }); } diff --git a/apps/scan/backend/src/scanners/custom/app_scan.test.ts b/apps/scan/backend/src/scanners/custom/app_scan.test.ts index d6911dc8c2..e87c4a5dc5 100644 --- a/apps/scan/backend/src/scanners/custom/app_scan.test.ts +++ b/apps/scan/backend/src/scanners/custom/app_scan.test.ts @@ -1,27 +1,7 @@ -import { - AdjudicationReason, - AdjudicationReasonInfo, - DEFAULT_SYSTEM_SETTINGS, - TEST_JURISDICTION, -} from '@votingworks/types'; +import { AdjudicationReason, AdjudicationReasonInfo } from '@votingworks/types'; import waitForExpect from 'wait-for-expect'; -import { err, ok, Result, sleep, typedAs } from '@votingworks/basics'; -import { - fakeElectionManagerUser, - fakePollWorkerUser, - fakeSessionExpiresAt, - mockOf, -} from '@votingworks/test-utils'; -import { - electionFamousNames2021Fixtures, - electionGridLayoutNewHampshireAmherstFixtures, -} from '@votingworks/fixtures'; -import { - ALL_PRECINCTS_SELECTION, - ReportSourceMachineType, - ScannerReportData, - ScannerReportDataSchema, -} from '@votingworks/utils'; +import { err, ok, sleep, typedAs } from '@votingworks/basics'; +import { electionGridLayoutNewHampshireAmherstFixtures } from '@votingworks/fixtures'; import { Logger } from '@votingworks/logging'; import { ErrorCode, mocks } from '@votingworks/custom-scanner'; import { MAX_FAILED_SCAN_ATTEMPTS } from './state_machine'; @@ -72,8 +52,6 @@ function checkLogs(logger: Logger): void { ); } -const jurisdiction = TEST_JURISDICTION; - test('configure and scan hmpb', async () => { await withApp( {}, @@ -433,56 +411,3 @@ test('scanning time out', async () => { } ); }); - -test('write scanner report data to card', async () => { - await withApp({}, async ({ apiClient, mockAuth, mockUsbDrive }) => { - await configureApp(apiClient, mockAuth, mockUsbDrive); - - mockOf(mockAuth.writeCardData).mockResolvedValue(ok()); - - const { electionDefinition } = electionFamousNames2021Fixtures; - const { electionHash } = electionDefinition; - const scannerReportData: ScannerReportData = { - ballotCounts: {}, - isLiveMode: false, - machineId: '0000', - pollsTransition: 'close_polls', - precinctSelection: ALL_PRECINCTS_SELECTION, - tally: [], - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - timePollsTransitioned: 0, - timeSaved: 0, - totalBallotsScanned: 0, - }; - let result: Result; - - mockOf(mockAuth.getAuthStatus).mockResolvedValue({ - status: 'logged_out', - reason: 'no_card', - }); - result = await apiClient.saveScannerReportDataToCard({ scannerReportData }); - expect(result).toEqual(err(new Error('User is not logged in'))); - - mockOf(mockAuth.getAuthStatus).mockResolvedValue({ - status: 'logged_in', - user: fakeElectionManagerUser(electionDefinition), - sessionExpiresAt: fakeSessionExpiresAt(), - }); - result = await apiClient.saveScannerReportDataToCard({ scannerReportData }); - expect(result).toEqual(err(new Error('User is not a poll worker'))); - - mockOf(mockAuth.getAuthStatus).mockResolvedValue({ - status: 'logged_in', - user: fakePollWorkerUser(electionDefinition), - sessionExpiresAt: fakeSessionExpiresAt(), - }); - result = await apiClient.saveScannerReportDataToCard({ scannerReportData }); - expect(result).toEqual(ok()); - expect(mockAuth.writeCardData).toHaveBeenCalledTimes(1); - expect(mockAuth.writeCardData).toHaveBeenNthCalledWith( - 1, - { ...DEFAULT_SYSTEM_SETTINGS, electionHash, jurisdiction }, - { data: scannerReportData, schema: ScannerReportDataSchema } - ); - }); -}); diff --git a/apps/scan/frontend/src/api.ts b/apps/scan/frontend/src/api.ts index 0528830cbd..58fdad8b3e 100644 --- a/apps/scan/frontend/src/api.ts +++ b/apps/scan/frontend/src/api.ts @@ -347,10 +347,3 @@ export const supportsUltrasonic = { return useQuery(this.queryKey(), () => apiClient.supportsUltrasonic()); }, } as const; - -export const saveScannerReportDataToCard = { - useMutation() { - const apiClient = useApiClient(); - return useMutation(apiClient.saveScannerReportDataToCard); - }, -} as const; diff --git a/apps/scan/frontend/src/app.test.tsx b/apps/scan/frontend/src/app.test.tsx index e0aab142cc..068890b747 100644 --- a/apps/scan/frontend/src/app.test.tsx +++ b/apps/scan/frontend/src/app.test.tsx @@ -1,9 +1,4 @@ -import { - ALL_PRECINCTS_SELECTION, - ReportSourceMachineType, - singlePrecinctSelectionFor, - MemoryHardware, -} from '@votingworks/utils'; +import { singlePrecinctSelectionFor, MemoryHardware } from '@votingworks/utils'; import { fakeLogger, LogEventId } from '@votingworks/logging'; import userEvent from '@testing-library/user-event'; import { @@ -13,10 +8,11 @@ import { generateCvr, fakeSystemAdministratorUser, mockOf, + hasTextAcrossElements, } from '@votingworks/test-utils'; import { electionSampleDefinition, - electionSample, + electionMinimalExhaustiveSampleDefinition, } from '@votingworks/fixtures'; import { AdjudicationReason, getDisplayElectionHash } from '@votingworks/types'; import { err, ok } from '@votingworks/basics'; @@ -56,7 +52,7 @@ let kiosk = fakeKiosk(); function renderApp(props: Partial = {}) { const hardware = MemoryHardware.build({ - connectPrinter: false, + connectPrinter: true, connectCardReader: true, connectPrecinctScanner: true, }); @@ -116,7 +112,7 @@ test('shows setup card reader screen when there is no card reader', async () => await screen.findByText('Card Reader Not Detected'); }); -test('shows insert USB Drive screen when there is no card reader', async () => { +test('shows insert USB Drive screen when there is no USB drive', async () => { apiMock.expectGetConfig(); apiMock.expectGetUsbDriveStatus('no_drive'); apiMock.expectGetScannerStatus(statusNoPaper); @@ -275,9 +271,9 @@ test('election manager and poll worker configuration', async () => { config = { ...config, pollsState: 'polls_open' }; apiMock.expectGetConfig(config); userEvent.click(await screen.findByText('Yes, Open the Polls')); - await advanceTimersAndPromises(1); + await expectPrint(); await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' + 'Remove the poll worker card if you have printed all necessary reports.' ); apiMock.removeCard(); await advanceTimersAndPromises(1); @@ -329,8 +325,9 @@ test('election manager and poll worker configuration', async () => { await hackActuallyCleanUpReactModal(); + await expectPrint(); await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' + 'Remove the poll worker card if you have printed all necessary reports.' ); apiMock.removeCard(); await advanceTimersAndPromises(1); @@ -403,21 +400,23 @@ async function scanBallot() { } test('voter can cast a ballot that scans successfully ', async () => { + const electionDefinition = electionMinimalExhaustiveSampleDefinition; apiMock.expectCheckUltrasonicSupported(false); apiMock.expectGetConfig({ pollsState: 'polls_open', + electionDefinition, }); apiMock.expectGetUsbDriveStatus('mounted'); apiMock.expectGetScannerStatus(statusNoPaper); renderApp(); await screen.findByText(/Insert Your Ballot/i); screen.getByText('Scan one ballot sheet at a time.'); - screen.getByText('General Election'); - screen.getByText(/Franklin County/); - screen.getByText(/State of Hamilton/); + screen.getByText('Example Primary Election'); + screen.getByText(/Sample County/); + screen.getByText(/State of Sample/); screen.getByText('Election ID'); within(screen.getByText('Election ID').parentElement!).getByText( - getDisplayElectionHash(electionSampleDefinition) + getDisplayElectionHash(electionDefinition) ); await scanBallot(); @@ -426,17 +425,13 @@ test('voter can cast a ballot that scans successfully ', async () => { apiMock.expectGetScannerStatus(statusBallotCounted); const mockCvrs = [ generateCvr( - electionSample, + electionDefinition.election, { - president: ['cramer-vuocolo'], - senator: [], - 'secretary-of-state': ['shamsi', 'talarico'], - 'county-registrar-of-wills': ['write-in'], - 'judicial-robert-demergue': ['yes'], + 'best-animal-mammal': ['horse'], }, { - precinctId: '23', - ballotStyleId: '12', + precinctId: 'precinct-1', + ballotStyleId: '1M', } ), ]; @@ -453,27 +448,21 @@ test('voter can cast a ballot that scans successfully ', async () => { userEvent.click(await screen.findByText('Yes, Close the Polls')); await screen.findByText('Closing Polls…'); await screen.findByText('Polls are closed.'); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenNthCalledWith(1, { - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 1, - machineId: '0002', - timeSaved: expect.anything(), - precinctSelection: ALL_PRECINCTS_SELECTION, - tally: expect.arrayContaining([ - [0, 0, 1, 0, 1, 0, 0, 0, 0, 0], // President expected tally - [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], // Senator expected tally - [0, 1, 1, 0, 0, 0], // Secretary of State expected tally - [0, 0, 1, 0, 1], // County Registrar of Wills expected tally - [0, 0, 1, 1, 0], // Judicial Robert Demergue expected tally - ]), - }), + await expectPrint((printResult) => { + // confirm we have all pages + for (const precinct of electionDefinition.election.precincts) { + expect( + printResult.getAllByText( + `TEST Polls Closed Report for ${precinct.name}` + ) + ).toHaveLength(3); // report for each party and a non-partisan report + } + + // confirm scanned results are in report + const precinct1MammalReport = printResult.getByTestId( + 'tally-report-0-precinct-1' + ); + within(precinct1MammalReport).getByText(hasTextAcrossElements('Horse1')); }); // Simulate unmounted usb drive @@ -641,6 +630,7 @@ test('scanning is not triggered when polls closed or cards present', async () => apiMock.expectSetPollsState('polls_open'); apiMock.expectGetConfig({ pollsState: 'polls_open' }); userEvent.click(screen.getByText('Yes, Open the Polls')); + await expectPrint(); await screen.findByText('Polls are open.'); // Once we remove the poll worker card, scanning should start @@ -651,50 +641,11 @@ test('scanning is not triggered when polls closed or cards present', async () => await screen.findByText(/Please wait/); }); -test('no printer: poll worker can open and close polls without scanning any ballots', async () => { - apiMock.expectGetConfig(); - apiMock.expectGetUsbDriveStatus('mounted'); - apiMock.expectGetScannerStatus(statusNoPaper); - renderApp(); - await screen.findByText('Polls Closed'); - - // Open Polls Flow - apiMock.expectGetCastVoteRecordsForTally([]); - apiMock.authenticateAsPollWorker(electionSampleDefinition); - await screen.findByText('Do you want to open the polls?'); - apiMock.expectSetPollsState('polls_open'); - apiMock.expectGetConfig({ pollsState: 'polls_open' }); - userEvent.click(await screen.findByText('Yes, Open the Polls')); - await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' - ); - apiMock.removeCard(); - await screen.findByText(/Insert Your Ballot/i); - - // Close Polls Flow - apiMock.expectGetCastVoteRecordsForTally([]); - apiMock.authenticateAsPollWorker(electionSampleDefinition); - await screen.findByText('Do you want to close the polls?'); - apiMock.expectSetPollsState('polls_closed_final'); - apiMock.expectGetConfig({ pollsState: 'polls_closed_final' }); - userEvent.click(await screen.findByText('Yes, Close the Polls')); - await screen.findByText('Closing Polls…'); - await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' - ); - apiMock.removeCard(); - await screen.findByText('Polls Closed'); -}); - -test('with printer: poll worker can open and close polls without scanning any ballots', async () => { +test('poll worker can open and close polls without scanning any ballots', async () => { apiMock.expectGetConfig({ pollsState: 'polls_closed_initial' }); apiMock.expectGetUsbDriveStatus('mounted'); apiMock.expectGetScannerStatus(statusNoPaper); - const hardware = MemoryHardware.build({ - connectCardReader: true, - connectPrinter: true, - }); - renderApp({ hardware, precinctReportDestination: 'laser-printer' }); + renderApp(); await screen.findByText('Polls Closed'); // Open Polls Flow @@ -740,26 +691,26 @@ test('with printer: poll worker can open and close polls without scanning any ba await screen.findByText('Polls Closed'); }); -test('no printer: open polls, scan ballot, close polls, save results', async () => { - apiMock.expectGetConfig(); +test('open polls, scan ballot, close polls, save results', async () => { + const electionDefinition = electionMinimalExhaustiveSampleDefinition; + apiMock.expectGetConfig({ + electionDefinition, + }); apiMock.expectGetUsbDriveStatus('mounted'); apiMock.expectGetScannerStatus(statusNoPaper); renderApp(); await screen.findByText('Polls Closed'); - // Open Polls Flow apiMock.expectGetCastVoteRecordsForTally([]); - apiMock.authenticateAsPollWorker(electionSampleDefinition); + apiMock.authenticateAsPollWorker(electionDefinition); await screen.findByText('Do you want to open the polls?'); apiMock.expectSetPollsState('polls_open'); - apiMock.expectGetConfig({ pollsState: 'polls_open' }); + apiMock.expectGetConfig({ pollsState: 'polls_open', electionDefinition }); userEvent.click(await screen.findByText('Yes, Open the Polls')); + await expectPrint(); await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' + 'Remove the poll worker card if you have printed all necessary reports.' ); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); apiMock.removeCard(); await screen.findByText(/Insert Your Ballot/i); @@ -768,53 +719,46 @@ test('no printer: open polls, scan ballot, close polls, save results', async () // Close Polls const mockCvrs = [ generateCvr( - electionSample, + electionDefinition.election, { - president: ['cramer-vuocolo'], - senator: [], - 'secretary-of-state': ['shamsi', 'talarico'], - 'county-registrar-of-wills': ['write-in'], - 'judicial-robert-demergue': ['yes'], + 'best-animal-mammal': ['horse'], }, { - precinctId: '23', - ballotStyleId: '12', + precinctId: 'precinct-1', + ballotStyleId: '1M', } ), ]; apiMock.expectGetCastVoteRecordsForTally(mockCvrs); apiMock.expectExportCastVoteRecordsToUsbDrive(); - apiMock.authenticateAsPollWorker(electionSampleDefinition); + apiMock.authenticateAsPollWorker(electionDefinition); await screen.findByText('Do you want to close the polls?'); apiMock.expectSetPollsState('polls_closed_final'); - apiMock.expectGetConfig({ pollsState: 'polls_closed_final' }); + apiMock.expectGetConfig({ + pollsState: 'polls_closed_final', + electionDefinition, + }); userEvent.click(await screen.findByText('Yes, Close the Polls')); await screen.findByText('Closing Polls…'); await screen.findByText('Polls are closed.'); await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' - ); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(2); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenNthCalledWith(2, { - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 1, - machineId: '0002', - timeSaved: expect.anything(), - precinctSelection: ALL_PRECINCTS_SELECTION, - tally: expect.arrayContaining([ - [0, 0, 1, 0, 1, 0, 0, 0, 0, 0], // President expected tally - [1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0], // Senator expected tally - [0, 1, 1, 0, 0, 0], // Secretary of State expected tally - [0, 0, 1, 0, 1], // County Registrar of Wills expected tally - [0, 0, 1, 1, 0], // Judicial Robert Demergue expected tally - ]), - }), + 'Remove the poll worker card if you have printed all necessary reports.' + ); + await expectPrint((printResult) => { + // confirm we have all pages + for (const precinct of electionDefinition.election.precincts) { + expect( + printResult.getAllByText( + `TEST Polls Closed Report for ${precinct.name}` + ) + ).toHaveLength(3); // report for each party and a non-partisan report + } + + // confirm scanned results are in report + const precinct1MammalReport = printResult.getByTestId( + 'tally-report-0-precinct-1' + ); + within(precinct1MammalReport).getByText(hasTextAcrossElements('Horse1')); }); apiMock.expectGetScannerStatus(statusNoPaper); @@ -836,8 +780,9 @@ test('poll worker can open, pause, unpause, and close poll without scanning any apiMock.expectSetPollsState('polls_open'); apiMock.expectGetConfig({ pollsState: 'polls_open' }); userEvent.click(await screen.findByText('Yes, Open the Polls')); + await expectPrint(); await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' + 'Remove the poll worker card if you have printed all necessary reports.' ); apiMock.removeCard(); await screen.findByText(/Insert Your Ballot/i); @@ -851,8 +796,9 @@ test('poll worker can open, pause, unpause, and close poll without scanning any apiMock.expectGetConfig({ pollsState: 'polls_paused' }); userEvent.click(await screen.findByText('Pause Voting')); await screen.findByText('Pausing Voting…'); + await expectPrint(); await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' + 'Remove the poll worker card if you have printed all necessary reports.' ); apiMock.removeCard(); await screen.findByText('Polls Paused'); @@ -864,8 +810,9 @@ test('poll worker can open, pause, unpause, and close poll without scanning any apiMock.expectSetPollsState('polls_open'); apiMock.expectGetConfig({ pollsState: 'polls_open' }); userEvent.click(await screen.findByText('Yes, Resume Voting')); + await expectPrint(); await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' + 'Remove the poll worker card if you have printed all necessary reports.' ); apiMock.removeCard(); await screen.findByText(/Insert Your Ballot/i); @@ -878,8 +825,9 @@ test('poll worker can open, pause, unpause, and close poll without scanning any apiMock.expectGetConfig({ pollsState: 'polls_closed_final' }); userEvent.click(await screen.findByText('Yes, Close the Polls')); await screen.findByText('Closing Polls…'); + await expectPrint(); await screen.findByText( - 'Insert poll worker card into VxMark to print the report.' + 'Remove the poll worker card if you have printed all necessary reports.' ); apiMock.removeCard(); await screen.findByText('Polls Closed'); diff --git a/apps/scan/frontend/src/app.tsx b/apps/scan/frontend/src/app.tsx index 7522297f89..5c992d6bff 100644 --- a/apps/scan/frontend/src/app.tsx +++ b/apps/scan/frontend/src/app.tsx @@ -32,7 +32,7 @@ import { DisplaySettingsScreen } from './screens/display_settings_screen'; import { DisplaySettingsManager } from './components/display_settings_manager'; const DEFAULT_PRECINCT_REPORT_DESTINATION: PrecinctReportDestination = - 'smartcard'; + 'laser-printer'; const envPrecinctReportDestination = getEnvironmentVariable( StringEnvironmentVariableName.PRECINCT_REPORT_DESTINATION ) as Optional; diff --git a/apps/scan/frontend/src/app_tally_report_paths.test.tsx b/apps/scan/frontend/src/app_tally_report_paths.test.tsx index 98018b77e2..3e7a568573 100644 --- a/apps/scan/frontend/src/app_tally_report_paths.test.tsx +++ b/apps/scan/frontend/src/app_tally_report_paths.test.tsx @@ -9,21 +9,17 @@ import { import { advanceTimersAndPromises, generateCvr, - getZeroCompressedTally, expectPrint, hasTextAcrossElements, fakeKiosk, } from '@votingworks/test-utils'; import { ALL_PRECINCTS_SELECTION, - BallotCountDetails, singlePrecinctSelectionFor, - ReportSourceMachineType, MemoryHardware, } from '@votingworks/utils'; import { CastVoteRecord, - CompressedTally, ContestId, Dictionary, ElectionDefinition, @@ -35,7 +31,6 @@ import { import userEvent from '@testing-library/user-event'; import MockDate from 'mockdate'; import { fakeLogger } from '@votingworks/logging'; -import { err } from '@votingworks/basics'; import { act, render, @@ -155,7 +150,7 @@ async function closePolls({ } } -test('printing: polls open, All Precincts, primary election + check additional report', async () => { +test('polls open, All Precincts, primary election + check additional report', async () => { const electionDefinition = electionMinimalExhaustiveSampleDefinition; const { election } = electionDefinition; apiMock.expectGetConfig({ electionDefinition }); @@ -211,107 +206,6 @@ test('printing: polls open, All Precincts, primary election + check additional r await checkReport(); }); -test('saving to card: polls open, All Precincts, primary election + test failed card write', async () => { - const electionDefinition = electionMinimalExhaustiveSampleDefinition; - apiMock.expectGetConfig({ electionDefinition }); - apiMock.expectGetScannerStatus(statusNoPaper); - renderApp({ connectPrinter: false }); - await screen.findByText('Polls Closed'); - - // Open the polls - apiMock.expectGetCastVoteRecordsForTally([]); - apiMock.authenticateAsPollWorker(electionDefinition); - await screen.findByText('Do you want to open the polls?'); - // Mimic what would happen if the tallies by precinct didn't fit on the card but the overall tally does - apiMock.mockApiClient.saveScannerReportDataToCard.mockImplementationOnce(() => - err(new Error('Whoa!')) - ); - apiMock.expectSetPollsState('polls_open'); - apiMock.expectGetConfig({ electionDefinition, pollsState: 'polls_open' }); - userEvent.click(screen.getByText('Yes, Open the Polls')); - await screen.findByText('Polls are open.'); - apiMock.removeCard(); - await advanceTimersAndPromises(1); - - const expectedCombinedTally: CompressedTally = [ - [0, 0, 0, 0, 0, 0, 0], // best animal mammal - [0, 0, 0, 0, 0, 0], // best animal fish - [0, 0, 0, 0, 0, 0, 0, 0], // zoo council - [0, 0, 0, 0, 0, 0, 0, 0], // aquarium council - [0, 0, 0, 0, 0], // new zoo either neither - [0, 0, 0, 0, 0], // new zoo pick one - [0, 0, 0, 0, 0], // fishing ban yes no - ]; - const expectedTalliesByPrecinct: Dictionary = { - 'precinct-1': [ - [0, 0, 0, 0, 0, 0, 0], // best animal mammal - [0, 0, 0, 0, 0, 0], // best animal fish - [0, 0, 0, 0, 0, 0, 0, 0], // zoo council - [0, 0, 0, 0, 0, 0, 0, 0], // aquarium council - [0, 0, 0, 0, 0], // new zoo either neither - [0, 0, 0, 0, 0], // new zoo pick one - [0, 0, 0, 0, 0], // fishing ban yes no - ], - 'precinct-2': [ - [0, 0, 0, 0, 0, 0, 0], // best animal mammal - [0, 0, 0, 0, 0, 0], // best animal fish - [0, 0, 0, 0, 0, 0, 0, 0], // zoo council - [0, 0, 0, 0, 0, 0, 0, 0], // aquarium council - [0, 0, 0, 0, 0], // new zoo either neither - [0, 0, 0, 0, 0], // new zoo pick one - [0, 0, 0, 0, 0], // fishing ban yes no - ], - }; - const expectedBallotCounts: Dictionary = { - '0,__ALL_PRECINCTS': [0, 0], - '0,precinct-1': [0, 0], - '0,precinct-2': [0, 0], - '1,__ALL_PRECINCTS': [0, 0], - '1,precinct-1': [0, 0], - '1,precinct-2': [0, 0], - 'undefined,precinct-1': [0, 0], - 'undefined,precinct-2': [0, 0], - }; - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(2); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenNthCalledWith(1, { - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 0, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: ALL_PRECINCTS_SELECTION, - tally: expectedCombinedTally, - talliesByPrecinct: expectedTalliesByPrecinct, - ballotCounts: expectedBallotCounts, - pollsTransition: 'open_polls', - }), - }); - // Expect the final call to have an empty tallies by precinct dictionary - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenNthCalledWith(2, { - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 0, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: ALL_PRECINCTS_SELECTION, - tally: expectedCombinedTally, - talliesByPrecinct: undefined, - ballotCounts: expectedBallotCounts, - pollsTransition: 'open_polls', - }), - }); -}); - const PRIMARY_ALL_PRECINCTS_CVRS = [ generateCvr( electionMinimalExhaustiveSampleWithReportingUrl, @@ -356,7 +250,7 @@ const PRIMARY_ALL_PRECINCTS_CVRS = [ ), ]; -test('printing: polls closed, primary election, all precincts + quickresults on', async () => { +test('polls closed, primary election, all precincts + quickresults on', async () => { const electionDefinition = electionMinimalExhaustiveSampleWithReportingUrlDefinition; const { election } = electionDefinition; @@ -632,82 +526,6 @@ test('printing: polls closed, primary election, all precincts + quickresults on' }); }); -test('saving to card: polls closed, primary election, all precincts', async () => { - const electionDefinition = - electionMinimalExhaustiveSampleWithReportingUrlDefinition; - apiMock.expectGetConfig({ electionDefinition, pollsState: 'polls_open' }); - apiMock.expectGetScannerStatus({ ...statusNoPaper, ballotsCounted: 3 }); - renderApp({ connectPrinter: false }); - await screen.findByText(/Insert Your Ballot/i); - - // Close the polls - await closePolls({ - electionDefinition, - castVoteRecords: PRIMARY_ALL_PRECINCTS_CVRS, - precinctSelection: ALL_PRECINCTS_SELECTION, - }); - - const expectedCombinedTally: CompressedTally = [ - [0, 1, 2, 0, 1, 0, 0], // best animal mammal - [0, 0, 1, 1, 0, 0], // best animal fish - [3, 0, 2, 1, 0, 0, 1, 1], // zoo council - [0, 0, 1, 1, 0, 0, 1, 0], // aquarium council - [1, 0, 3, 2, 0], // new zoo either neither - [2, 0, 3, 0, 1], // new zoo pick one - [2, 0, 3, 0, 1], // fishing ban yes no - ]; - const expectedTalliesByPrecinct: Dictionary = { - 'precinct-1': [ - [0, 0, 1, 0, 1, 0, 0], // best animal mammal - [0, 0, 0, 0, 0, 0], // best animal fish - [1, 0, 1, 1, 0, 0, 0, 1], // zoo council - [0, 0, 0, 0, 0, 0, 0, 0], // aquarium council - [0, 0, 1, 1, 0], // new zoo either neither - [1, 0, 1, 0, 0], // new zoo pick one - [1, 0, 1, 0, 0], // fishing ban yes no - ], - 'precinct-2': [ - [0, 1, 1, 0, 0, 0, 0], // best animal mammal - [0, 0, 1, 1, 0, 0], // best animal fish - [2, 0, 1, 0, 0, 0, 1, 0], // zoo council - [0, 0, 1, 1, 0, 0, 1, 0], // aquarium council - [1, 0, 2, 1, 0], // new zoo either neither - [1, 0, 2, 0, 1], // new zoo pick one - [1, 0, 2, 0, 1], // fishing ban yes no - ], - }; - const expectedBallotCounts: Dictionary = { - '0,__ALL_PRECINCTS': [1, 1], - '0,precinct-1': [1, 0], - '0,precinct-2': [0, 1], - '1,__ALL_PRECINCTS': [1, 0], - '1,precinct-1': [0, 0], - '1,precinct-2': [1, 0], - 'undefined,precinct-1': [1, 0], - 'undefined,precinct-2': [1, 1], - }; - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledWith({ - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 3, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: ALL_PRECINCTS_SELECTION, - tally: expectedCombinedTally, - talliesByPrecinct: expectedTalliesByPrecinct, - ballotCounts: expectedBallotCounts, - pollsTransition: 'close_polls', - }), - }); -}); - const PRIMARY_SINGLE_PRECINCT_CVRS = [ generateCvr( electionMinimalExhaustiveSample, @@ -752,7 +570,7 @@ const PRIMARY_SINGLE_PRECINCT_CVRS = [ ), ]; -test('printing: polls closed, primary election, single precinct + check additional report', async () => { +test('polls closed, primary election, single precinct + check additional report', async () => { const electionDefinition = electionMinimalExhaustiveSampleDefinition; const { election } = electionDefinition; const precinctSelection = singlePrecinctSelectionFor('precinct-1'); @@ -904,66 +722,6 @@ test('printing: polls closed, primary election, single precinct + check addition await checkReport(); }); -test('saving to card: polls closed, primary election, single precinct', async () => { - const electionDefinition = electionMinimalExhaustiveSampleDefinition; - const precinctSelection = singlePrecinctSelectionFor('precinct-1'); - apiMock.expectGetConfig({ - electionDefinition, - precinctSelection, - pollsState: 'polls_open', - }); - apiMock.expectGetScannerStatus({ ...statusNoPaper, ballotsCounted: 3 }); - renderApp({ connectPrinter: false }); - await screen.findByText(/Insert Your Ballot/i); - - // Close the polls - await closePolls({ - electionDefinition, - castVoteRecords: PRIMARY_SINGLE_PRECINCT_CVRS, - precinctSelection, - }); - - const expectedCombinedTally: CompressedTally = [ - [0, 1, 2, 0, 1, 0, 0], // best animal mammal - [0, 0, 1, 1, 0, 0], // best animal fish - [3, 0, 2, 1, 0, 0, 1, 1], // zoo council - [0, 0, 1, 1, 0, 0, 1, 0], // aquarium council - [1, 0, 3, 2, 0], // new zoo either neither - [2, 0, 3, 0, 1], // new zoo pick one - [2, 0, 3, 0, 1], // fishing ban yes no - ]; - const expectedTalliesByPrecinct: Dictionary = { - 'precinct-1': expectedCombinedTally, - }; - const expectedBallotCounts: Dictionary = { - '0,__ALL_PRECINCTS': [1, 1], - '0,precinct-1': [1, 1], - '1,__ALL_PRECINCTS': [1, 0], - '1,precinct-1': [1, 0], - 'undefined,precinct-1': [2, 1], - }; - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledWith({ - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 3, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: singlePrecinctSelectionFor('precinct-1'), - tally: expectedCombinedTally, - talliesByPrecinct: expectedTalliesByPrecinct, - ballotCounts: expectedBallotCounts, - pollsTransition: 'close_polls', - }), - }); -}); - const GENERAL_ALL_PRECINCTS_CVRS = [ generateCvr( electionSample2, @@ -991,7 +749,7 @@ const GENERAL_ALL_PRECINCTS_CVRS = [ ), ]; -test('printing: polls closed, general election, all precincts', async () => { +test('polls closed, general election, all precincts', async () => { const electionDefinition = electionSample2Definition; apiMock.expectGetConfig({ electionDefinition, pollsState: 'polls_open' }); apiMock.expectGetScannerStatus({ ...statusNoPaper, ballotsCounted: 2 }); @@ -1081,64 +839,6 @@ test('printing: polls closed, general election, all precincts', async () => { }); }); -test('saving to card: polls closed, general election, all precincts', async () => { - const electionDefinition = electionSample2Definition; - const { election } = electionDefinition; - apiMock.expectGetConfig({ electionDefinition, pollsState: 'polls_open' }); - apiMock.expectGetScannerStatus({ ...statusNoPaper, ballotsCounted: 2 }); - renderApp({ connectPrinter: false }); - await screen.findByText(/Insert Your Ballot/i); - - // Close the polls - await closePolls({ - electionDefinition, - castVoteRecords: GENERAL_ALL_PRECINCTS_CVRS, - precinctSelection: ALL_PRECINCTS_SELECTION, - }); - - const expectedCombinedTally = election.contests.map(() => expect.anything()); - expectedCombinedTally[0] = [0, 0, 2, 1, 0, 0, 1, 0, 0]; // president - const index102 = election.contests.findIndex((c) => c.id === 'prop-1'); - expectedCombinedTally[index102] = [1, 0, 2, 1, 0]; // measure 102 - const expectedTallyCenter = election.contests.map(() => expect.anything()); - expectedTallyCenter[0] = [0, 0, 1, 0, 0, 0, 1, 0, 0]; // president - expectedTallyCenter[index102] = [0, 0, 1, 1, 0]; // measure 102 - const expectedTallyNorth = election.contests.map(() => expect.anything()); - expectedTallyNorth[0] = [0, 0, 1, 1, 0, 0, 0, 0, 0]; // president - expectedTallyNorth[index102] = [1, 0, 1, 0, 0]; // measure 102 - const expectedTalliesByPrecinct: Dictionary = { - '23': expectedTallyCenter, - '20': getZeroCompressedTally(election), - '21': expectedTallyNorth, - }; - const expectedBallotCounts: Dictionary = { - 'undefined,__ALL_PRECINCTS': [1, 1], - 'undefined,23': [1, 0], - 'undefined,21': [0, 1], - 'undefined,20': [0, 0], - }; - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledWith({ - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 2, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: ALL_PRECINCTS_SELECTION, - tally: expectedCombinedTally, - talliesByPrecinct: expectedTalliesByPrecinct, - ballotCounts: expectedBallotCounts, - pollsTransition: 'close_polls', - }), - }); -}); - const GENERAL_SINGLE_PRECINCT_CVRS = [ generateCvr( electionSample2, @@ -1166,7 +866,7 @@ const GENERAL_SINGLE_PRECINCT_CVRS = [ ), ]; -test('printing: polls closed, general election, single precinct', async () => { +test('polls closed, general election, single precinct', async () => { const electionDefinition = electionSample2Definition; const precinctSelection = singlePrecinctSelectionFor('23'); apiMock.expectGetConfig({ @@ -1233,60 +933,7 @@ test('printing: polls closed, general election, single precinct', async () => { }); }); -test('saving to card: polls closed, general election, single precinct', async () => { - const electionDefinition = electionSample2Definition; - const { election } = electionDefinition; - const precinctSelection = singlePrecinctSelectionFor('23'); - apiMock.expectGetConfig({ - electionDefinition, - precinctSelection, - pollsState: 'polls_open', - }); - apiMock.expectGetScannerStatus({ ...statusNoPaper, ballotsCounted: 2 }); - renderApp({ connectPrinter: false }); - await screen.findByText(/Insert Your Ballot/i); - - // Close the polls - await closePolls({ - electionDefinition, - castVoteRecords: GENERAL_SINGLE_PRECINCT_CVRS, - precinctSelection, - }); - - const expectedCombinedTally = election.contests.map(() => expect.anything()); - expectedCombinedTally[0] = [0, 0, 2, 1, 0, 0, 1, 0, 0]; // president - const index102 = election.contests.findIndex((c) => c.id === 'prop-1'); - expectedCombinedTally[index102] = [1, 0, 2, 1, 0]; // measure 102 - const expectedTalliesByPrecinct: Dictionary = { - '23': expectedCombinedTally, - }; - const expectedBallotCounts: Dictionary = { - 'undefined,__ALL_PRECINCTS': [1, 1], - 'undefined,23': [1, 1], - }; - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledWith({ - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 2, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: singlePrecinctSelectionFor('23'), - tally: expectedCombinedTally, - talliesByPrecinct: expectedTalliesByPrecinct, - ballotCounts: expectedBallotCounts, - pollsTransition: 'close_polls', - }), - }); -}); - -test('printing: polls paused', async () => { +test('polls paused', async () => { MockDate.set('2022-10-31T16:23:00.000Z'); const electionDefinition = electionSample2Definition; apiMock.expectGetConfig({ @@ -1330,53 +977,7 @@ test('printing: polls paused', async () => { }); }); -test('saving to card: polls paused', async () => { - const electionDefinition = electionSample2Definition; - apiMock.expectGetConfig({ - electionDefinition, - precinctSelection: singlePrecinctSelectionFor('23'), - pollsState: 'polls_open', - }); - apiMock.expectGetScannerStatus({ ...statusNoPaper, ballotsCounted: 2 }); - renderApp({ connectPrinter: false }); - await screen.findByText(/Insert Your Ballot/i); - - // Pause the polls - apiMock.expectGetCastVoteRecordsForTally(GENERAL_SINGLE_PRECINCT_CVRS); - apiMock.authenticateAsPollWorker(electionDefinition); - await screen.findByText('Do you want to close the polls?'); - userEvent.click(screen.getByText('No')); - apiMock.expectSetPollsState('polls_paused'); - apiMock.expectGetConfig({ - electionDefinition, - precinctSelection: singlePrecinctSelectionFor('23'), - pollsState: 'polls_paused', - }); - userEvent.click(await screen.findByText('Pause Voting')); - await screen.findByText('Voting paused.'); - apiMock.removeCard(); - await advanceTimersAndPromises(1); - - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledWith({ - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 2, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: singlePrecinctSelectionFor('23'), - pollsTransition: 'pause_voting', - }), - }); -}); - -test('printing: polls unpaused', async () => { +test('polls unpaused', async () => { MockDate.set('2022-10-31T16:23:00.000Z'); const electionDefinition = electionSample2Definition; apiMock.expectGetConfig({ @@ -1419,51 +1020,7 @@ test('printing: polls unpaused', async () => { }); }); -test('saving to card: polls unpaused', async () => { - const electionDefinition = electionSample2Definition; - apiMock.expectGetConfig({ - electionDefinition, - precinctSelection: singlePrecinctSelectionFor('23'), - pollsState: 'polls_paused', - }); - apiMock.expectGetScannerStatus({ ...statusNoPaper, ballotsCounted: 2 }); - renderApp({ connectPrinter: false }); - - // Unpause the polls - apiMock.expectGetCastVoteRecordsForTally(GENERAL_SINGLE_PRECINCT_CVRS); - apiMock.authenticateAsPollWorker(electionDefinition); - await screen.findByText('Do you want to resume voting?'); - userEvent.click(screen.getByText('Yes, Resume Voting')); - apiMock.expectSetPollsState('polls_open'); - apiMock.expectGetConfig({ - electionDefinition, - precinctSelection: singlePrecinctSelectionFor('23'), - pollsState: 'polls_open', - }); - await screen.findByText('Voting resumed.'); - apiMock.removeCard(); - await advanceTimersAndPromises(1); - - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledWith({ - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 2, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: singlePrecinctSelectionFor('23'), - pollsTransition: 'resume_voting', - }), - }); -}); - -test('printing: polls closed from paused, general election, single precinct', async () => { +test('polls closed from paused, general election, single precinct', async () => { const electionDefinition = electionSample2Definition; apiMock.expectGetConfig({ electionDefinition, @@ -1537,69 +1094,7 @@ test('printing: polls closed from paused, general election, single precinct', as }); }); -test('saving to card: polls closed from paused, general election, single precinct', async () => { - const electionDefinition = electionSample2Definition; - const { election } = electionDefinition; - apiMock.expectGetConfig({ - electionDefinition, - precinctSelection: singlePrecinctSelectionFor('23'), - pollsState: 'polls_paused', - }); - apiMock.expectGetScannerStatus({ ...statusNoPaper, ballotsCounted: 2 }); - renderApp({ connectPrinter: false }); - await screen.findByText('Polls Paused'); - - // Close the polls - apiMock.expectGetCastVoteRecordsForTally(GENERAL_SINGLE_PRECINCT_CVRS); - apiMock.expectExportCastVoteRecordsToUsbDrive(); - apiMock.authenticateAsPollWorker(electionDefinition); - await screen.findByText('Do you want to resume voting?'); - userEvent.click(screen.getByText('No')); - apiMock.expectSetPollsState('polls_closed_final'); - apiMock.expectGetConfig({ - electionDefinition, - precinctSelection: singlePrecinctSelectionFor('23'), - pollsState: 'polls_closed_final', - }); - userEvent.click(await screen.findByText('Close Polls')); - await screen.findByText('Polls are closed.'); - apiMock.removeCard(); - await advanceTimersAndPromises(1); - - const expectedCombinedTally = election.contests.map(() => expect.anything()); - expectedCombinedTally[0] = [0, 0, 2, 1, 0, 0, 1, 0, 0]; // president - const index102 = election.contests.findIndex((c) => c.id === 'prop-1'); - expectedCombinedTally[index102] = [1, 0, 2, 1, 0]; // measure 102 - const expectedTalliesByPrecinct: Dictionary = { - '23': expectedCombinedTally, - }; - const expectedBallotCounts: Dictionary = { - 'undefined,__ALL_PRECINCTS': [1, 1], - 'undefined,23': [1, 1], - }; - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledWith({ - scannerReportData: expect.objectContaining({ - isLiveMode: false, - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - totalBallotsScanned: 2, - machineId: '0002', - timePollsTransitioned: expect.anything(), - timeSaved: expect.anything(), - precinctSelection: singlePrecinctSelectionFor('23'), - tally: expectedCombinedTally, - talliesByPrecinct: expectedTalliesByPrecinct, - ballotCounts: expectedBallotCounts, - pollsTransition: 'close_polls', - }), - }); -}); - -test('printing: must have printer attached to open polls (thermal printer)', async () => { +test('must have printer attached to open polls (thermal printer)', async () => { const electionDefinition = electionSample2Definition; apiMock.expectGetConfig({ electionDefinition, @@ -1631,7 +1126,7 @@ test('printing: must have printer attached to open polls (thermal printer)', asy ).not.toBeInTheDocument(); }); -test('printing: must have printer attached to close polls (thermal printer)', async () => { +test('must have printer attached to close polls (thermal printer)', async () => { const electionDefinition = electionSample2Definition; apiMock.expectGetConfig({ electionDefinition, diff --git a/apps/scan/frontend/src/app_unhappy_paths.test.tsx b/apps/scan/frontend/src/app_unhappy_paths.test.tsx index e12ed7ef93..bf0982e732 100644 --- a/apps/scan/frontend/src/app_unhappy_paths.test.tsx +++ b/apps/scan/frontend/src/app_unhappy_paths.test.tsx @@ -2,10 +2,10 @@ import { electionSampleDefinition } from '@votingworks/fixtures'; import { advanceTimersAndPromises, + expectPrint, fakeKiosk, suppressingConsoleOutput, } from '@votingworks/test-utils'; -import { MemoryHardware } from '@votingworks/utils'; import userEvent from '@testing-library/user-event'; @@ -19,15 +19,12 @@ import { statusNoPaper, } from '../test/helpers/mock_api_client'; import { App, AppProps } from './app'; +import { buildStandardScanHardware } from '../test/helpers/build_app'; let apiMock: ApiMock; function renderApp(props: Partial = {}) { - const hardware = MemoryHardware.build({ - connectPrinter: false, - connectCardReader: true, - connectPrecinctScanner: true, - }); + const hardware = buildStandardScanHardware(); const logger = fakeLogger(); render( test('shows internal wiring message when there is no scanner, but tablet is plugged in', async () => { apiMock.expectGetConfig(); - const hardware = MemoryHardware.buildStandard(); + const hardware = buildStandardScanHardware(); hardware.setPrecinctScannerConnected(false); hardware.setBatteryDischarging(false); apiMock.expectGetScannerStatus({ @@ -175,7 +172,7 @@ test('shows internal wiring message when there is no scanner, but tablet is plug test('shows power cable message when there is no scanner and tablet is not plugged in', async () => { apiMock.expectGetConfig(); - const hardware = MemoryHardware.buildStandard(); + const hardware = buildStandardScanHardware(); hardware.setPrecinctScannerConnected(false); hardware.setBatteryDischarging(true); apiMock.expectGetScannerStatus({ @@ -193,7 +190,7 @@ test('shows power cable message when there is no scanner and tablet is not plugg test('shows instructions to restart when the scanner client crashed', async () => { apiMock.expectGetConfig({ pollsState: 'polls_open' }); - const hardware = MemoryHardware.buildStandard(); + const hardware = buildStandardScanHardware(); hardware.setPrecinctScannerConnected(false); apiMock.expectGetScannerStatus({ ...statusNoPaper, @@ -206,8 +203,7 @@ test('shows instructions to restart when the scanner client crashed', async () = test('App shows warning message to connect to power when disconnected', async () => { apiMock.expectGetConfig(); - const hardware = MemoryHardware.buildStandard(); - hardware.setPrinterConnected(false); + const hardware = buildStandardScanHardware(); hardware.setBatteryDischarging(true); hardware.setBatteryLevel(0.9); const kiosk = fakeKiosk(); @@ -234,15 +230,8 @@ test('App shows warning message to connect to power when disconnected', async () apiMock.expectSetPollsState('polls_open'); apiMock.expectGetConfig({ pollsState: 'polls_open' }); userEvent.click(await screen.findByText('Yes, Open the Polls')); + await expectPrint(); await screen.findByText('Polls are open.'); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenCalledTimes(1); - expect( - apiMock.mockApiClient.saveScannerReportDataToCard - ).toHaveBeenNthCalledWith(1, { - scannerReportData: expect.anything(), - }); // Remove pollworker card apiMock.removeCard(); 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 e857d43f04..0364285469 100644 --- a/apps/scan/frontend/src/screens/poll_worker_screen.test.tsx +++ b/apps/scan/frontend/src/screens/poll_worker_screen.test.tsx @@ -1,4 +1,9 @@ -import { fakeKiosk, mockOf } from '@votingworks/test-utils'; +import { + expectPrint, + fakeKiosk, + fakePrinterInfo, + mockOf, +} from '@votingworks/test-utils'; import { ALL_PRECINCTS_SELECTION, isFeatureFlagEnabled, @@ -58,9 +63,9 @@ function renderScreen( scannedBallotCount={0} pollsState="polls_closed_initial" isLiveMode - printerInfo={undefined} + printerInfo={fakePrinterInfo()} logger={fakeLogger()} - precinctReportDestination="smartcard" + precinctReportDestination="laser-printer" {...props} /> ) @@ -114,6 +119,7 @@ describe('transitions from polls closed', () => { apiMock.expectGetConfig({ pollsState: 'polls_open' }); userEvent.click(screen.getByText('Yes, Open the Polls')); await screen.findByText('Opening Polls…'); + await expectPrint(); await screen.findByText('Polls are open.'); expect(logger.log).toHaveBeenCalledWith( LogEventId.PollsOpened, @@ -131,6 +137,7 @@ describe('transitions from polls closed', () => { userEvent.click(screen.getByText('No')); userEvent.click(await screen.findByText('Open Polls')); await screen.findByText('Opening Polls…'); + await expectPrint(); await screen.findByText('Polls are open.'); expect(logger.log).toHaveBeenCalledWith( LogEventId.PollsOpened, @@ -161,6 +168,7 @@ describe('transitions from polls open', () => { apiMock.expectGetConfig({ pollsState: 'polls_closed_final' }); userEvent.click(screen.getByText('Yes, Close the Polls')); await screen.findByText('Closing Polls…'); + await expectPrint(); await screen.findByText('Polls are closed.'); expect(logger.log).toHaveBeenCalledWith( LogEventId.PollsClosed, @@ -179,6 +187,7 @@ describe('transitions from polls open', () => { userEvent.click(screen.getByText('No')); userEvent.click(await screen.findByText('Close Polls')); await screen.findByText('Closing Polls…'); + await expectPrint(); await screen.findByText('Polls are closed.'); expect(logger.log).toHaveBeenCalledWith( LogEventId.PollsClosed, @@ -196,6 +205,7 @@ describe('transitions from polls open', () => { userEvent.click(screen.getByText('No')); userEvent.click(await screen.findByText('Pause Voting')); await screen.findByText('Pausing Voting…'); + await expectPrint(); await screen.findByText('Voting paused.'); expect(logger.log).toHaveBeenCalledWith( LogEventId.VotingPaused, @@ -225,6 +235,7 @@ describe('transitions from polls paused', () => { apiMock.expectGetConfig({ pollsState: 'polls_open' }); userEvent.click(screen.getByText('Yes, Resume Voting')); await screen.findByText('Resuming Voting…'); + await expectPrint(); await screen.findByText('Voting resumed.'); expect(logger.log).toHaveBeenCalledWith( LogEventId.VotingResumed, @@ -242,6 +253,7 @@ describe('transitions from polls paused', () => { userEvent.click(screen.getByText('No')); userEvent.click(await screen.findByText('Resume Voting')); await screen.findByText('Resuming Voting…'); + await expectPrint(); await screen.findByText('Voting resumed.'); expect(logger.log).toHaveBeenCalledWith( LogEventId.VotingResumed, @@ -260,6 +272,7 @@ describe('transitions from polls paused', () => { userEvent.click(screen.getByText('No')); userEvent.click(await screen.findByText('Close Polls')); await screen.findByText('Closing Polls…'); + await expectPrint(); await screen.findByText('Polls are closed.'); expect(logger.log).toHaveBeenCalledWith( LogEventId.PollsClosed, diff --git a/apps/scan/frontend/src/screens/poll_worker_screen.tsx b/apps/scan/frontend/src/screens/poll_worker_screen.tsx index d1912a7b71..54eb5792ba 100644 --- a/apps/scan/frontend/src/screens/poll_worker_screen.tsx +++ b/apps/scan/frontend/src/screens/poll_worker_screen.tsx @@ -19,30 +19,19 @@ import { Icons, } from '@votingworks/ui'; import { - BallotCountDetails, compressTally, computeTallyWithPrecomputedCategories, BooleanEnvironmentVariableName, getSubTalliesByPartyAndPrecinct, - getTallyIdentifier, isFeatureFlagEnabled, - ScannerReportData, - ReportSourceMachineType, getPollsTransitionDestinationState, getPollsReportTitle, - ScannerBallotCountReportData, isPollsSuspensionTransition, - ScannerTallyReportData, - ScannerReportDataBase, } from '@votingworks/utils'; import { - VotingMethod, TallyCategory, FullElectionTally, Tally, - Dictionary, - CompressedTally, - getPartyIdsInBallotStyles, PollsState, PollsTransition, ElectionDefinition, @@ -57,7 +46,6 @@ import { import { assert, Optional, - Result, sleep, throwIllegalValue, } from '@votingworks/basics'; @@ -69,7 +57,6 @@ import { exportCastVoteRecordsToUsbDrive, getCastVoteRecordsForTally, setPollsState, - saveScannerReportDataToCard as saveScannerReportDataToCardBase, } from '../api'; import { MachineConfig } from '../config/types'; import { FullScreenPromptLayout } from '../components/full_screen_prompt_layout'; @@ -155,26 +142,8 @@ export function PollWorkerScreen({ isShowingBallotsAlreadyScannedScreen, setIsShowingBallotsAlreadyScannedScreen, ] = useState(false); - const needsToAttachPrinterToTransitionPolls = - precinctReportDestination !== 'smartcard' && !printerInfo && !!window.kiosk; + const needsToAttachPrinterToTransitionPolls = !printerInfo && !!window.kiosk; const { election } = electionDefinition; - const saveScannerReportDataToCardMutation = - saveScannerReportDataToCardBase.useMutation(); - - async function saveScannerReportDataToCard( - scannerReportData: ScannerReportData - ): Promise { - let result: Result; - try { - result = await saveScannerReportDataToCardMutation.mutateAsync({ - scannerReportData, - }); - } catch { - // Handled by default query client error handling - return false; - } - return result.isOk(); - } function initialPollWorkerFlowState(): Optional { switch (pollsState) { @@ -201,11 +170,6 @@ export function PollWorkerScreen({ [election, currentTally] ); - const parties = useMemo( - () => getPartyIdsInBallotStyles(election), - [election] - ); - getCastVoteRecordsForTally.useQuery({ onSuccess: (castVoteRecords) => { const tally = computeTallyWithPrecomputedCategories( @@ -237,107 +201,6 @@ export function PollWorkerScreen({ }, }); - async function exportReportDataToCard( - pollsTransition: PollsTransition, - timePollsTransitioned: number - ) { - const reportBasicData: ScannerReportDataBase = { - tallyMachineType: ReportSourceMachineType.PRECINCT_SCANNER, - machineId: machineConfig.machineId, - isLiveMode, - precinctSelection, - totalBallotsScanned: scannedBallotCount, - timeSaved: Date.now(), - timePollsTransitioned, - }; - - if (isPollsSuspensionTransition(pollsTransition)) { - const ballotCountReportData: ScannerBallotCountReportData = { - ...reportBasicData, - pollsTransition, - }; - // TODO: Handle when this returns false - await saveScannerReportDataToCard(ballotCountReportData); - return; - } - - assert(currentTally); - assert(currentCompressedTally); - let compressedTalliesByPrecinct: Dictionary = {}; - // We only need to save tallies by precinct if the precinct scanner is configured for all precincts - if (precinctSelection.kind === 'AllPrecincts') { - const talliesByPrecinct = currentTally.resultsByCategory.get( - TallyCategory.Precinct - ); - assert(talliesByPrecinct); - compressedTalliesByPrecinct = Object.keys(talliesByPrecinct).reduce( - (input: Dictionary, key) => { - const tally = talliesByPrecinct[key]; - assert(tally); - return { - ...input, - [key]: compressTally(election, tally), - }; - }, - {} - ); - } else { - compressedTalliesByPrecinct[precinctSelection.precinctId] = compressTally( - election, - currentTally.overallTally - ); - } - - const ballotCountBreakdowns = [...currentSubTallies.entries()].reduce< - Dictionary - >((input, [key, subTally]) => { - const bcDictionary = subTally.ballotCountsByVotingMethod; - const newRow: BallotCountDetails = [ - bcDictionary[VotingMethod.Precinct] ?? 0, - bcDictionary[VotingMethod.Absentee] ?? 0, - ]; - return { - ...input, - [key]: newRow, - }; - }, {}); - const talliesByParty = currentTally.resultsByCategory.get( - TallyCategory.Party - ); - assert(talliesByParty); - for (const partyId of parties) { - const subTally = partyId - ? talliesByParty[partyId] - : currentTally.overallTally; - assert(subTally); - ballotCountBreakdowns[getTallyIdentifier(partyId)] = [ - subTally.ballotCountsByVotingMethod[VotingMethod.Precinct] ?? 0, - subTally.ballotCountsByVotingMethod[VotingMethod.Absentee] ?? 0, - ]; - } - - const tallyReportData: ScannerTallyReportData = { - ...reportBasicData, - pollsTransition, - tally: currentCompressedTally, - talliesByPrecinct: compressedTalliesByPrecinct, - ballotCounts: ballotCountBreakdowns, - }; - - const success = await saveScannerReportDataToCard(tallyReportData); - if (!success) { - debug( - 'Error saving tally information to card, trying again without precinct-specific data' - ); - // TODO: Handle when this second attempt returns false - await saveScannerReportDataToCard({ - ...tallyReportData, - talliesByPrecinct: undefined, - timeSaved: Date.now(), - }); - } - } - function showAllPollWorkerActions() { return setPollWorkerFlowState(undefined); } @@ -400,24 +263,6 @@ export function PollWorkerScreen({ } } - async function dispatchReport( - pollsTransition: PollsTransition, - timePollsTransitioned: number - ) { - if (precinctReportDestination === 'smartcard') { - await exportReportDataToCard(pollsTransition, timePollsTransitioned); - } else { - await printReport( - pollsTransition, - timePollsTransitioned, - /* istanbul ignore next - prototype */ - precinctReportDestination === 'thermal-sheet-printer' - ? 1 - : DEFAULT_NUMBER_POLL_REPORT_COPIES - ); - } - } - async function transitionPolls(pollsTransition: PollsTransition) { try { // In compliance with VVSG 2.0 1.1.3-B, confirm there are no scanned @@ -437,7 +282,14 @@ export function PollWorkerScreen({ const timePollsTransitioned = Date.now(); setCurrentPollsTransition(pollsTransition); setPollWorkerFlowState('polls_transition_processing'); - await dispatchReport(pollsTransition, timePollsTransitioned); + await printReport( + pollsTransition, + timePollsTransitioned, + /* istanbul ignore next - prototype */ + precinctReportDestination === 'thermal-sheet-printer' + ? 1 + : DEFAULT_NUMBER_POLL_REPORT_COPIES + ); if (pollsTransition === 'close_polls' && scannedBallotCount > 0) { (await exportCastVoteRecordsMutation.mutateAsync()).unsafeUnwrap(); } @@ -610,37 +462,33 @@ export function PollWorkerScreen({

{pollsTransitionCompleteText}

- {precinctReportDestination === 'smartcard' ? ( -

Insert poll worker card into VxMark to print the report.

- ) : ( - - {/* istanbul ignore next - prototype */} - {precinctReportDestination === 'thermal-sheet-printer' && ( -

- Insert{' '} - {numReportPages - ? `${numReportPages} ${pluralize( - 'sheet', - numReportPages - )} of paper` - : 'paper'}{' '} - into the printer to print the report. -

- )} -

- -

+ + {/* istanbul ignore next - prototype */} + {precinctReportDestination === 'thermal-sheet-printer' && (

- Remove the poll worker card if you have printed all necessary - reports. + Insert{' '} + {numReportPages + ? `${numReportPages} ${pluralize( + 'sheet', + numReportPages + )} of paper` + : 'paper'}{' '} + into the printer to print the report.

-
- )} + )} +

+ +

+

+ Remove the poll worker card if you have printed all necessary + reports. +

+
); diff --git a/apps/scan/frontend/test/helpers/build_app.ts b/apps/scan/frontend/test/helpers/build_app.ts index 561f515f08..d69d4af7be 100644 --- a/apps/scan/frontend/test/helpers/build_app.ts +++ b/apps/scan/frontend/test/helpers/build_app.ts @@ -3,16 +3,20 @@ import { MemoryHardware } from '@votingworks/utils'; import { render, RenderResult } from '../react_testing_library'; import { App } from '../../src/app'; -export function buildApp(connectPrinter = false): { +export function buildStandardScanHardware(): MemoryHardware { + return MemoryHardware.build({ + connectPrinter: true, + connectCardReader: true, + connectPrecinctScanner: true, + }); +} + +export function buildApp(): { hardware: MemoryHardware; logger: Logger; renderApp: () => RenderResult; } { - const hardware = MemoryHardware.build({ - connectPrinter, - connectCardReader: true, - connectPrecinctScanner: true, - }); + const hardware = buildStandardScanHardware(); const logger = fakeLogger(); function renderApp() { return render(App({ hardware, logger })); diff --git a/apps/scan/frontend/test/helpers/mock_api_client.tsx b/apps/scan/frontend/test/helpers/mock_api_client.tsx index dfeda171ec..aade9f571c 100644 --- a/apps/scan/frontend/test/helpers/mock_api_client.tsx +++ b/apps/scan/frontend/test/helpers/mock_api_client.tsx @@ -10,7 +10,7 @@ import { PollsState, PrecinctSelection, } from '@votingworks/types'; -import { createMockClient, MockClient } from '@votingworks/grout-test-utils'; +import { createMockClient } from '@votingworks/grout-test-utils'; import type { Api, MachineConfig, @@ -51,30 +51,13 @@ export const statusNoPaper: PrecinctScannerStatus = { ballotsCounted: 0, }; -type MockApiClient = Omit, 'saveScannerReportDataToCard'> & { - // Because the values passed to this are so complex, we opt for a standard jest mock instead of a - // libs/test-utils mock since the latter requires exact input matching and doesn't support - // matchers like expect.objectContaining - saveScannerReportDataToCard: jest.Mock; -}; - -function createMockApiClient(): MockApiClient { - const mockApiClient = createMockClient(); - // Because mockApiClient uses a Proxy under the hood, we add an explicit field - // to the object to override the Proxy implementation. - (mockApiClient.saveScannerReportDataToCard as unknown as jest.Mock) = jest.fn( - () => Promise.resolve(ok()) - ); - return mockApiClient as unknown as MockApiClient; -} - /** * Creates a VxScan specific wrapper around commonly used methods from the Grout * mock API client to make it easier to use for our specific test needs */ // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types export function createApiMock() { - const mockApiClient = createMockApiClient(); + const mockApiClient = createMockClient(); function setAuthStatus(authStatus: InsertedSmartCardAuth.AuthStatus): void { mockApiClient.getAuthStatus.expectRepeatedCallsWith().resolves(authStatus); diff --git a/libs/types/src/printing.ts b/libs/types/src/printing.ts index 1f4cc58a97..165d31d89d 100644 --- a/libs/types/src/printing.ts +++ b/libs/types/src/printing.ts @@ -8,13 +8,8 @@ export interface Printer { } export type PrecinctReportDestination = - | 'smartcard' | 'laser-printer' | 'thermal-sheet-printer'; export const PrecinctReportDestinationSchema: z.ZodSchema = - z.union([ - z.literal('smartcard'), - z.literal('laser-printer'), - z.literal('thermal-sheet-printer'), - ]); + z.union([z.literal('laser-printer'), z.literal('thermal-sheet-printer')]); diff --git a/libs/utils/src/environment_variable.ts b/libs/utils/src/environment_variable.ts index d60f24cf5d..d8ef012afe 100644 --- a/libs/utils/src/environment_variable.ts +++ b/libs/utils/src/environment_variable.ts @@ -211,7 +211,7 @@ export function getStringEnvVarConfig( case StringEnvironmentVariableName.PRECINCT_REPORT_DESTINATION: return { name, - defaultValue: 'smartcard', + defaultValue: 'thermal-sheet-printer', zodSchema: PrecinctReportDestinationSchema, }; /* c8 ignore next 2 */