Skip to content

Commit

Permalink
Test suite for useSessionSettingsManager, tests for voter session man…
Browse files Browse the repository at this point in the history
…agement in app, and test for resetting voter settings on polls close
  • Loading branch information
nbhatia823 committed Oct 8, 2024
1 parent 911ba4b commit f8c585d
Show file tree
Hide file tree
Showing 3 changed files with 243 additions and 29 deletions.
109 changes: 84 additions & 25 deletions apps/scan/frontend/src/app.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,16 +28,14 @@ import {
statusNoPaper,
} from '../test/helpers/mock_api_client';
import { App, AppProps } from './app';
import { VoterSettingsManager } from './components/voter_settings_manager';
import { useSessionSettingsManager } from './utils/use_session_settings_manager';

jest.mock(
'./components/voter_settings_manager',
(): typeof import('./components/voter_settings_manager') => ({
VoterSettingsManager: jest.fn(),
})
);
jest.mock('./utils/use_session_settings_manager');

let apiMock: ApiMock;
const resetVoterSettingsMock = jest.fn();
const cacheAndResetVoterSettingsMock = jest.fn();
const restoreVoterSessionsSettingsMock = jest.fn();

jest.setTimeout(20000);

Expand All @@ -47,18 +45,19 @@ function renderApp(props: Partial<AppProps> = {}) {

beforeEach(() => {
jest.useFakeTimers();

jest.clearAllMocks();
apiMock = createApiMock();
apiMock.expectGetMachineConfig();
apiMock.removeCard(); // Set a default auth state of no card inserted.

mockOf(VoterSettingsManager).mockReturnValue(null);
mockOf(useSessionSettingsManager).mockReturnValue({
resetVoterSettings: resetVoterSettingsMock,
cacheAndResetVoterSettings: cacheAndResetVoterSettingsMock,
restoreVoterSessionsSettings: restoreVoterSessionsSettingsMock,
});
});

afterEach(() => {
apiMock.mockApiClient.assertComplete();

mockOf(VoterSettingsManager).mockReset();
});

test('shows setup card reader screen when there is no card reader', async () => {
Expand Down Expand Up @@ -861,19 +860,6 @@ test('election manager cannot auth onto machine with different election', async
await screen.findByText('Invalid Card');
});

test('renders VoterSettingsManager', async () => {
apiMock.expectGetConfig();
apiMock.expectGetPollsInfo('polls_open');
apiMock.expectGetUsbDriveStatus('mounted');
apiMock.expectGetScannerStatus(statusNoPaper);
apiMock.setPrinterStatusV4();

renderApp();
await screen.findByText(/insert your ballot/i);

expect(mockOf(VoterSettingsManager)).toBeCalled();
});

test('requires CVR sync if necessary', async () => {
apiMock.expectGetConfig();
apiMock.expectGetPollsInfo('polls_open');
Expand Down Expand Up @@ -1029,3 +1015,76 @@ test('vendor screen', async () => {
apiMock.expectRebootToVendorMenu();
userEvent.click(rebootButton);
});

test('Test voter settings are cleared when a voter finishes', async () => {
apiMock.expectGetConfig();
apiMock.expectGetPollsInfo('polls_open');
apiMock.expectGetUsbDriveStatus('mounted');
apiMock.setPrinterStatusV4();
apiMock.expectGetScannerStatus(scannerStatus({ state: 'accepted' }));

renderApp();
await screen.findByText('Your ballot was counted!');

apiMock.expectGetScannerStatus(statusBallotCounted);
jest.advanceTimersByTime(POLLING_INTERVAL_FOR_SCANNER_STATUS_MS);
await screen.findByText(/Insert Your Ballot/i);

expect(resetVoterSettingsMock).toBeCalled();
expect(restoreVoterSessionsSettingsMock).not.toBeCalled();
expect(cacheAndResetVoterSettingsMock).not.toBeCalled();
});

test('Test voter settings are cached when election official logs in and restored on log out', async () => {
apiMock.expectGetConfig();
apiMock.expectGetPollsInfo('polls_open');
apiMock.expectGetUsbDriveStatus('mounted');
apiMock.expectGetScannerStatus(statusNoPaper);
apiMock.setPrinterStatusV4();

renderApp();
await screen.findByText(/Insert Your Ballot/i);

// Auth as Election Manager
apiMock.authenticateAsElectionManager(electionGeneralDefinition);
await screen.findByText('Election Manager Settings');
expect(cacheAndResetVoterSettingsMock).toBeCalled();

// Return to voter screen
apiMock.removeCard();
await screen.findByText(/Insert Your Ballot/i);
expect(restoreVoterSessionsSettingsMock).toBeCalled();
expect(resetVoterSettingsMock).not.toBeCalled();
});

test('Test voter settings are not reset when scanner status changes from paused to no_paper', async () => {
apiMock.expectGetConfig();
apiMock.expectGetPollsInfo('polls_open');
apiMock.expectGetUsbDriveStatus('mounted');
apiMock.setPrinterStatusV4();
apiMock.expectGetScannerStatus(scannerStatus({ state: 'paused' }));
renderApp();
await screen.findByText('Ballots Scanned');

apiMock.expectGetScannerStatus(statusNoPaper);
jest.advanceTimersByTime(POLLING_INTERVAL_FOR_SCANNER_STATUS_MS);

await screen.findByText(/Insert Your Ballot/i);

expect(resetVoterSettingsMock).not.toBeCalled();
expect(restoreVoterSessionsSettingsMock).not.toBeCalled();
expect(cacheAndResetVoterSettingsMock).not.toBeCalled();
});

test('Test voter settings are not reset when voting begins', async () => {
apiMock.expectGetConfig();
apiMock.expectGetPollsInfo('polls_open');
apiMock.expectGetUsbDriveStatus('mounted');
apiMock.setPrinterStatusV4();
apiMock.expectGetScannerStatus(statusNoPaper);

renderApp();
await screen.findByText(/Insert Your Ballot/i);

expect(resetVoterSettingsMock).not.toBeCalled();
});
16 changes: 12 additions & 4 deletions apps/scan/frontend/src/screens/poll_worker_screen.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
} from '@votingworks/fixtures';
import { err } from '@votingworks/basics';
import { PollsState } from '@votingworks/types';
import { screen, RenderResult, render } from '../../test/react_testing_library';
import { screen, render } from '../../test/react_testing_library';
import { PollWorkerScreen, PollWorkerScreenProps } from './poll_worker_screen';
import {
ApiMock,
Expand All @@ -21,6 +21,7 @@ import {
import { BROTHER_THERMAL_PRINTER_CONFIG } from '../../test/helpers/fixtures';

let apiMock: ApiMock;
let resetVoterSettingsMock: jest.Mock;

const featureFlagMock = getFeatureFlagMock();

Expand All @@ -34,6 +35,7 @@ jest.mock('@votingworks/utils', (): typeof import('@votingworks/utils') => {

beforeEach(() => {
featureFlagMock.resetFeatureFlags();
resetVoterSettingsMock = jest.fn();
apiMock = createApiMock();
apiMock.expectGetMachineConfig();
apiMock.expectGetConfig();
Expand All @@ -50,14 +52,13 @@ afterEach(() => {
apiMock.mockApiClient.assertComplete();
});

function renderScreen(
props: Partial<PollWorkerScreenProps> = {}
): RenderResult {
function renderScreen(props: Partial<PollWorkerScreenProps> = {}) {
return render(
provideApi(
apiMock,
<PollWorkerScreen
electionDefinition={electionFamousNames2021Fixtures.electionDefinition}
resetVoterSettings={resetVoterSettingsMock}
scannedBallotCount={0}
{...props}
/>
Expand Down Expand Up @@ -110,6 +111,7 @@ describe('transitions from polls open', () => {
userEvent.click(screen.getByText('Yes, Close the Polls'));
await screen.findByText('Closing Polls…');
await screen.findByText('Polls are closed.');
expect(resetVoterSettingsMock).toHaveBeenCalledTimes(1);
});

test('close polls from landing screen', async () => {
Expand All @@ -120,6 +122,7 @@ describe('transitions from polls open', () => {
userEvent.click(await screen.findByText('Close Polls'));
await screen.findByText('Closing Polls…');
await screen.findByText('Polls are closed.');
expect(resetVoterSettingsMock).toHaveBeenCalledTimes(1);
});

test('pause voting', async () => {
Expand Down Expand Up @@ -169,6 +172,7 @@ describe('transitions from polls paused', () => {
userEvent.click(await screen.findByText('Close Polls'));
await screen.findByText('Closing Polls…');
await screen.findByText('Polls are closed.');
expect(resetVoterSettingsMock).toHaveBeenCalledTimes(1);
});
});

Expand Down Expand Up @@ -355,6 +359,7 @@ describe('must have printer attached to transition polls and print reports', ()
apiMock.expectGetPollsInfo('polls_closed_final');
userEvent.click(screen.getByText('Yes, Close the Polls'));
await screen.findByText('Reprint Polls Closed Report');
expect(resetVoterSettingsMock).toHaveBeenCalledTimes(1);
});

test('polls close from fallback screen', async () => {
Expand All @@ -378,6 +383,7 @@ describe('must have printer attached to transition polls and print reports', ()
apiMock.expectGetPollsInfo('polls_closed_final');
userEvent.click(screen.getByText('Close Polls'));
await screen.findByText('Reprint Polls Closed Report');
expect(resetVoterSettingsMock).toHaveBeenCalledTimes(1);
});
});

Expand Down Expand Up @@ -484,6 +490,7 @@ describe('must have usb drive attached to transition polls', () => {
apiMock.expectGetPollsInfo('polls_closed_final');
userEvent.click(screen.getByText('Yes, Close the Polls'));
await screen.findByText('Print Additional Polls Closed Report');
expect(resetVoterSettingsMock).toHaveBeenCalledTimes(1);
});

test('polls close from fallback screen', async () => {
Expand All @@ -509,6 +516,7 @@ describe('must have usb drive attached to transition polls', () => {
apiMock.expectGetPollsInfo('polls_closed_final');
userEvent.click(screen.getByText('Close Polls'));
await screen.findByText('Print Additional Polls Closed Report');
expect(resetVoterSettingsMock).toHaveBeenCalledTimes(1);
});
});

Expand Down
147 changes: 147 additions & 0 deletions apps/scan/frontend/src/utils/use_session_settings_manager.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,147 @@
import {
AppBase,
LanguageControls,
useCurrentLanguage,
VoterSettingsManagerContext,
} from '@votingworks/ui';
import { DefaultTheme, ThemeContext } from 'styled-components';
import React from 'react';
import { LanguageCode } from '@votingworks/types';
import { mockOf } from '@votingworks/test-utils';
import { useSessionSettingsManager } from './use_session_settings_manager';
import { renderHook, act } from '../../test/react_testing_library';
import { ApiMock, createApiMock } from '../../test/helpers/mock_api_client';

let apiMock: ApiMock;
const mockLanguageControls: jest.Mocked<LanguageControls> = {
reset: jest.fn(),
setLanguage: jest.fn(),
};
const mockUseCurrentLanguage = mockOf(useCurrentLanguage);

jest.mock('@votingworks/ui', (): typeof import('@votingworks/ui') => ({
...jest.requireActual('@votingworks/ui'),
useCurrentLanguage: jest.fn(),
useLanguageControls: () => mockLanguageControls,
}));

const DEFAULT_THEME = {
colorMode: 'contrastMedium',
sizeMode: 'touchMedium',
isVisualModeDisabled: false,
} as const satisfies Partial<DefaultTheme>;

function TestHookWrapper(props: { children: React.ReactNode }) {
return (
<AppBase
{...props}
defaultColorMode={DEFAULT_THEME.colorMode}
defaultSizeMode={DEFAULT_THEME.sizeMode}
defaultIsVisualModeDisabled={DEFAULT_THEME.isVisualModeDisabled ?? false}
disableFontsForTests
/>
);
}

function useTestHook() {
const theme = React.useContext(ThemeContext);
const voterSettingsManager = React.useContext(VoterSettingsManagerContext);
const [mockLanguage, setMockLanguage] = React.useState(LanguageCode.ENGLISH);
const voterSettingsControls = useSessionSettingsManager();
mockUseCurrentLanguage.mockReturnValue(mockLanguage);

return {
theme,
setMockLanguage,
voterSettingsManager,
...voterSettingsControls,
};
}

beforeEach(() => {
jest.clearAllMocks();
apiMock = createApiMock();
});

afterEach(() => {
apiMock.mockApiClient.assertComplete();
});

it('Reset voter settings when resetVoterSettings is called', () => {
const { result } = renderHook(() => useTestHook(), {
wrapper: TestHookWrapper,
});

expect(mockLanguageControls.reset).not.toHaveBeenCalled();
expect(mockLanguageControls.setLanguage).not.toHaveBeenCalled();

// Simulate changing session settings as voter:
act(() => {
result.current.voterSettingsManager.setColorMode('contrastLow');
result.current.voterSettingsManager.setSizeMode('touchExtraLarge');
});
expect(result.current.theme).toEqual(
expect.objectContaining<Partial<DefaultTheme>>({
colorMode: 'contrastLow',
sizeMode: 'touchExtraLarge',
})
);

// Reset voter settings
act(() => {
result.current.resetVoterSettings();
});

// Validate settings were reset
expect(mockLanguageControls.reset).toHaveBeenCalledTimes(1);
expect(mockLanguageControls.setLanguage).not.toHaveBeenCalled();
expect(result.current.theme).toEqual(
expect.objectContaining<Partial<DefaultTheme>>(DEFAULT_THEME)
);
});

it('First cache/clear voter settings and then restore', () => {
const { result } = renderHook(() => useTestHook(), {
wrapper: TestHookWrapper,
});

// Simulate changing session settings as voter:
act(() => {
result.current.voterSettingsManager.setColorMode('contrastLow');
result.current.voterSettingsManager.setSizeMode('touchExtraLarge');
result.current.setMockLanguage(LanguageCode.SPANISH);
});

expect(result.current.theme).toEqual(
expect.objectContaining<Partial<DefaultTheme>>({
colorMode: 'contrastLow',
sizeMode: 'touchExtraLarge',
})
);

// Cache and reset voter settings
act(() => {
result.current.cacheAndResetVoterSettings();
});

// Validate settings were reset
expect(mockLanguageControls.reset).toHaveBeenCalledTimes(1);
expect(mockLanguageControls.setLanguage).not.toHaveBeenCalled();
expect(result.current.theme).toEqual(
expect.objectContaining<Partial<DefaultTheme>>(DEFAULT_THEME)
);

// Restore voter settings
act(() => {
result.current.restoreVoterSessionsSettings();
});

// Validate settings were restored
expect(result.current.theme).toEqual(
expect.objectContaining<Partial<DefaultTheme>>({
colorMode: 'contrastLow',
sizeMode: 'touchExtraLarge',
})
);
expect(mockLanguageControls.setLanguage).toHaveBeenCalledTimes(1);
});

0 comments on commit f8c585d

Please sign in to comment.