diff --git a/apps/mark-scan/frontend/src/app_root.tsx b/apps/mark-scan/frontend/src/app_root.tsx index 356d61d9926..9d261ec3a1d 100644 --- a/apps/mark-scan/frontend/src/app_root.tsx +++ b/apps/mark-scan/frontend/src/app_root.tsx @@ -81,6 +81,7 @@ import { PatDeviceCalibrationPage } from './pages/pat_device_identification/pat_ import { CastingBallotPage } from './pages/casting_ballot_page'; import { BallotSuccessfullyCastPage } from './pages/ballot_successfully_cast_page'; import { EmptyBallotBoxPage } from './pages/empty_ballot_box_page'; +import { PollWorkerAuthEndedUnexpectedlyPage } from './pages/poll_worker_auth_ended_unexpectedly_page'; interface VotingState { votes?: VotesDict; @@ -439,6 +440,18 @@ export function AppRoot({ return ; } + const isInPaperLoadingStage = + stateMachineState === 'accepting_paper' || + stateMachineState === 'loading_paper'; + if ( + stateMachineState === 'poll_worker_auth_ended_unexpectedly' || + // Handle when the frontend auth state is up to date but the state machine state is not + (isInPaperLoadingStage && + (isCardlessVoterAuth(authStatus) || authStatus.status === 'logged_out')) + ) { + return ; + } + if (optionalElectionDefinition && precinctSelection) { if ( authStatus.status === 'logged_out' && diff --git a/apps/mark-scan/frontend/src/app_states.test.tsx b/apps/mark-scan/frontend/src/app_states.test.tsx index ef90c26da2b..68d6c9f9f06 100644 --- a/apps/mark-scan/frontend/src/app_states.test.tsx +++ b/apps/mark-scan/frontend/src/app_states.test.tsx @@ -222,14 +222,14 @@ test('`empty_ballot_box` state renders EmptyBallotBoxPage', async () => { await screen.findByText('Ballot Box Full'); }); -const testSpecs: Array<{ +const ballotCastPageTestSpecs: Array<{ state: SimpleServerStatus; }> = [ { state: 'ballot_accepted' }, { state: 'resetting_state_machine_after_success' }, ]; -test.each(testSpecs)( +test.each(ballotCastPageTestSpecs)( '$state state renders BallotSuccessfullyCastPage', async ({ state }) => { apiMock.mockApiClient.getElectionState.reset(); @@ -253,3 +253,41 @@ test.each(testSpecs)( await screen.findByText('Thank you for voting.'); } ); + +const authEndedEarlyPageTestSpecs: Array<{ + state: SimpleServerStatus; + auth: 'cardless_voter' | 'logged_out'; +}> = [ + { state: 'poll_worker_auth_ended_unexpectedly', auth: 'cardless_voter' }, + { state: 'poll_worker_auth_ended_unexpectedly', auth: 'logged_out' }, + { state: 'accepting_paper', auth: 'cardless_voter' }, + { state: 'accepting_paper', auth: 'logged_out' }, + { state: 'loading_paper', auth: 'cardless_voter' }, + { state: 'loading_paper', auth: 'logged_out' }, +]; + +test.each(authEndedEarlyPageTestSpecs)( + '$state state renders PollWorkerAuthEndedUnexpectedlyPage', + async ({ state, auth }) => { + apiMock.setPaperHandlerState(state); + if (auth === 'cardless_voter') { + apiMock.setAuthStatusCardlessVoterLoggedInWithDefaults( + electionDefinition + ); + } else { + apiMock.setAuthStatusLoggedOut(); + } + + render( + + ); + + await screen.findByText( + 'The poll worker card was removed before paper loading completed. Please try again.' + ); + } +); diff --git a/apps/mark-scan/frontend/src/pages/poll_worker_auth_ended_unexpectedly_page.tsx b/apps/mark-scan/frontend/src/pages/poll_worker_auth_ended_unexpectedly_page.tsx new file mode 100644 index 00000000000..616e8d1c41d --- /dev/null +++ b/apps/mark-scan/frontend/src/pages/poll_worker_auth_ended_unexpectedly_page.tsx @@ -0,0 +1,10 @@ +import { appStrings, P } from '@votingworks/ui'; +import { CenteredPageLayout } from '../components/centered_page_layout'; + +export function PollWorkerAuthEndedUnexpectedlyPage(): JSX.Element { + return ( + +

{appStrings.notePollWorkerAuthEndedBeforePaperLoadComplete()}

+
+ ); +} diff --git a/libs/ui/src/ui_strings/app_strings.tsx b/libs/ui/src/ui_strings/app_strings.tsx index 4b67dc9484e..d3eafa8a4d0 100644 --- a/libs/ui/src/ui_strings/app_strings.tsx +++ b/libs/ui/src/ui_strings/app_strings.tsx @@ -780,6 +780,13 @@ export const appStrings = { Audio is on ), + notePollWorkerAuthEndedBeforePaperLoadComplete: () => ( + + The poll worker card was removed before paper loading completed. Please + try again. + + ), + promptBmdConfirmRemoveWriteIn: () => ( Do you want to deselect and remove your write-in candidate? diff --git a/libs/ui/src/ui_strings/app_strings_catalog/latest.json b/libs/ui/src/ui_strings/app_strings_catalog/latest.json index 94ef7eda469..e71151787dd 100644 --- a/libs/ui/src/ui_strings/app_strings_catalog/latest.json +++ b/libs/ui/src/ui_strings/app_strings_catalog/latest.json @@ -158,6 +158,7 @@ "noteBmdPatCalibrationStep2": "Step 2 of 3", "noteBmdPatCalibrationStep3": "Step 3 of 3", "noteBmdSessionRestart": "Your voting session will restart shortly.", + "notePollWorkerAuthEndedBeforePaperLoadComplete": "The poll worker card was removed before paper loading completed. Please try again.", "noteScannerBlankContestsCardPlural": "Did you mean to leave these contests blank?", "noteScannerBlankContestsCardSingular": "Did you mean to leave this contest blank?", "noteScannerOvervoteContestsCardPlural": "Your votes in these contests will not be counted.",