Skip to content

Commit

Permalink
testing
Browse files Browse the repository at this point in the history
  • Loading branch information
adghayes committed Oct 23, 2023
1 parent 624568f commit 153285f
Show file tree
Hide file tree
Showing 4 changed files with 251 additions and 37 deletions.
184 changes: 148 additions & 36 deletions apps/admin/backend/src/app.tally_report_csv_export.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ it('logs failure if export fails for some reason', async () => {
);
});

it('incorporates wia and manual data', async () => {
it('incorporates wia and manual data (grouping by voting method)', async () => {
const { electionDefinition, castVoteRecordExport } =
electionGridLayoutNewHampshireAmherstFixtures;
const { election } = electionDefinition;
Expand All @@ -180,105 +180,217 @@ it('incorporates wia and manual data', async () => {
const candidateContestId =
'State-Representatives-Hillsborough-District-34-b1012d38';
const officialCandidateId = 'Obadiah-Carrigan-5c95145a';
const officialCandidateName = 'Obadiah Carrigan';

function rowExists(
rows: ReturnType<typeof parseCsv>['rows'],
selectionId: string,
votingMethod: string,
votes: number
{
selection,
selectionId,
votingMethod,
totalVotes,
scannedVotes,
manualVotes,
}: {
selection: string;
selectionId: string;
votingMethod: string;
totalVotes: number;
scannedVotes?: number;
manualVotes?: number;
}
): boolean {
return rows.some(
(row) =>
row['Selection'] === selection &&
row['Selection ID'] === selectionId &&
row['Voting Method'] === votingMethod &&
row['Total Votes'] === votes.toString()
row['Total Votes'] === totalVotes.toString() &&
(!scannedVotes || row['Scanned Votes'] === scannedVotes.toString()) &&
(!manualVotes || row['Manual Votes'] === manualVotes.toString())
);
}

// check initial export, without wia and manual data
const { rows: rowsInitial } = await getParsedExport({
const { headers: headersInitial, rows: rowsInitial } = await getParsedExport({
apiClient,
groupBy,
});
expect(headersInitial).toEqual([
'Voting Method',
'Contest',
'Contest ID',
'Selection',
'Selection ID',
'Total Votes',
]);

// initial official candidate counts
expect(
rowExists(rowsInitial, officialCandidateId, 'Precinct', 30)
rowExists(rowsInitial, {
selection: officialCandidateName,
selectionId: officialCandidateId,
votingMethod: 'Precinct',
totalVotes: 30,
})
).toBeTruthy();
expect(
rowExists(rowsInitial, officialCandidateId, 'Absentee', 30)
rowExists(rowsInitial, {
selection: officialCandidateName,
selectionId: officialCandidateId,
votingMethod: 'Precinct',
totalVotes: 30,
})
).toBeTruthy();

// initial generic write-in counts
expect(rowExists(rowsInitial, 'write-in', 'Absentee', 28)).toBeTruthy();
expect(rowExists(rowsInitial, 'write-in', 'Precinct', 28)).toBeTruthy();
expect(
rowExists(rowsInitial, {
selection: Tabulation.PENDING_WRITE_IN_NAME,
selectionId: Tabulation.PENDING_WRITE_IN_ID,
votingMethod: 'Precinct',
totalVotes: 28,
})
).toBeTruthy();
expect(
rowExists(rowsInitial, {
selection: Tabulation.PENDING_WRITE_IN_NAME,
selectionId: Tabulation.PENDING_WRITE_IN_ID,
votingMethod: 'Absentee',
totalVotes: 28,
})
).toBeTruthy();

// adjudicate write-ins for unofficial candidate
const writeInCandidate = await apiClient.addWriteInCandidate({
contestId: candidateContestId,
name: 'Mr. Pickles',
});
const writeInIds = await apiClient.getWriteInAdjudicationQueue({
contestId: candidateContestId,
});
for (const writeInId of writeInIds) {
await apiClient.adjudicateWriteIn({
writeInId,
type: 'write-in-candidate',
candidateId: writeInCandidate.id,
});
}

// add manual data
const manualOnlyWriteInCandidate = await apiClient.addWriteInCandidate({
contestId: candidateContestId,
name: 'Ms. Bean',
});
await apiClient.setManualResults({
precinctId: election.precincts[0]!.id,
votingMethod: 'absentee',
ballotStyleId: election.ballotStyles[0]!.id,
manualResults: buildManualResultsFixture({
election,
ballotCount: 10,
ballotCount: 20,
contestResultsSummaries: {
[candidateContestId]: {
type: 'candidate',
ballots: 10,
ballots: 20,
overvotes: 0,
undervotes: 0,
officialOptionTallies: {
[officialCandidateId]: 10,
},
writeInOptionTallies: {
[writeInCandidate.id]: {
name: writeInCandidate.name,
tally: 5,
},
[manualOnlyWriteInCandidate.id]: {
name: manualOnlyWriteInCandidate.name,
tally: 5,
},
},
},
},
}),
});

// adjudicate write-ins for unofficial candidate
const writeInCandidate = await apiClient.addWriteInCandidate({
contestId: candidateContestId,
name: 'Mr. Pickles',
});
const writeInIds = await apiClient.getWriteInAdjudicationQueue({
contestId: candidateContestId,
});
for (const writeInId of writeInIds) {
await apiClient.adjudicateWriteIn({
writeInId,
type: 'write-in-candidate',
candidateId: writeInCandidate.id,
});
}

// check final export, with wia and manual data
const { rows: rowsFinal } = await getParsedExport({
const { headers: headersFinal, rows: rowsFinal } = await getParsedExport({
apiClient,
groupBy,
});
expect(headersFinal).toEqual([
'Voting Method',
'Contest',
'Contest ID',
'Selection',
'Selection ID',
'Manual Votes',
'Scanned Votes',
'Total Votes',
]);

// final official candidate counts
expect(
rowExists(rowsFinal, officialCandidateId, 'Precinct', 30)
rowExists(rowsFinal, {
selection: officialCandidateName,
selectionId: officialCandidateId,
votingMethod: 'Precinct',
manualVotes: 0,
scannedVotes: 30,
totalVotes: 30,
})
).toBeTruthy();
expect(
rowExists(rowsFinal, officialCandidateId, 'Absentee', 40)
rowExists(rowsFinal, {
selection: officialCandidateName,
selectionId: officialCandidateId,
votingMethod: 'Absentee',
manualVotes: 10,
scannedVotes: 30,
totalVotes: 40,
})
).toBeTruthy(); // manual data reflected

// added write-in candidate counts
// adjudicated write-in candidate counts
expect(
rowExists(rowsFinal, writeInCandidate.id, 'Absentee', 28)
rowExists(rowsFinal, {
selection: writeInCandidate.name,
selectionId: writeInCandidate.id,
votingMethod: 'Precinct',
manualVotes: 0,
scannedVotes: 28,
totalVotes: 28,
})
).toBeTruthy();
expect(
rowExists(rowsFinal, writeInCandidate.id, 'Precinct', 28)
rowExists(rowsFinal, {
selection: writeInCandidate.name,
selectionId: writeInCandidate.id,
votingMethod: 'Absentee',
manualVotes: 5,
scannedVotes: 28,
totalVotes: 33,
})
).toBeTruthy();

// manual-only write-in candidate counts
expect(
rowExists(rowsFinal, {
selection: manualOnlyWriteInCandidate.name,
selectionId: manualOnlyWriteInCandidate.id,
votingMethod: 'Absentee',
manualVotes: 5,
scannedVotes: 0,
totalVotes: 5,
})
).toBeTruthy();

// generic write-in counts should be gone
// pending write-in counts should be gone
expect(
rowsFinal.some(
(r) =>
r['Contest ID'] === candidateContestId &&
r['Selection ID'] === 'write-in'
r['Selection ID'] === Tabulation.PENDING_WRITE_IN_ID
)
).toBeFalsy();
});
3 changes: 2 additions & 1 deletion apps/admin/backend/src/exports/batch_results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,11 @@ function generateResultsRow(
}
if (contest.allowWriteIns) {
contestVoteTotals.push(
/* c8 ignore next - trivial fallback case */
/* c8 ignore start - trivial fallback case */
contestResults.tallies[
Tabulation.GENERIC_WRITE_IN_ID
]?.tally.toString() ?? '0'
/* c8 ignore end */
);
}
} else if (contest.type === 'yesno') {
Expand Down
100 changes: 100 additions & 0 deletions apps/admin/backend/src/exports/csv_tally_report.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { electionTwoPartyPrimaryFixtures } from '@votingworks/fixtures';
import { DEFAULT_SYSTEM_SETTINGS, Tabulation } from '@votingworks/types';
import { find } from '@votingworks/basics';
import { buildManualResultsFixture } from '@votingworks/utils';
import {
MockCastVoteRecordFile,
addMockCvrFileToStore,
Expand Down Expand Up @@ -348,3 +349,102 @@ test('does not include results groups when they are excluded by the filter', asy
precinctRows.some((r) => r['Voting Method'] === 'Precinct')
).toBeTruthy();
});

test('incorporates manual data', async () => {
const store = Store.memoryStore();
const { electionDefinition } = electionTwoPartyPrimaryFixtures;
const { election, electionData } = electionDefinition;
const electionId = store.addElection({
electionData,
systemSettingsData: JSON.stringify(DEFAULT_SYSTEM_SETTINGS),
});
store.setCurrentElectionId(electionId);

// add some mock cast vote records with one vote each
const mockCastVoteRecordFile: MockCastVoteRecordFile = [
{
ballotStyleId: '1M',
batchId: 'batch-1',
scannerId: 'scanner-1',
precinctId: 'precinct-1',
votingMethod: 'precinct',
votes: {
'zoo-council-mammal': ['lion', 'kangaroo', 'elephant'],
fishing: ['ban-fishing'],
},
card: { type: 'bmd' },
multiplier: 1,
},
];
addMockCvrFileToStore({ electionId, mockCastVoteRecordFile, store });

store.setManualResults({
electionId,
precinctId: 'precinct-1',
ballotStyleId: '1M',
votingMethod: 'absentee',
manualResults: buildManualResultsFixture({
election,
ballotCount: 20,
contestResultsSummaries: {
'zoo-council-mammal': {
type: 'candidate',
ballots: 20,
overvotes: 3,
undervotes: 2,
officialOptionTallies: {
lion: 10,
kangaroo: 5,
},
},
fishing: {
type: 'yesno',
ballots: 20,
undervotes: 6,
overvotes: 4,
yesTally: 1,
noTally: 9,
},
},
}),
});

const stream = await generateTallyReportCsv({
store,
});
const fileContents = await streamToString(stream);
const { rows } = parseCsv(fileContents);
expect(
rows
.filter((r) => r['Contest ID'] === 'zoo-council-mammal')
.map((row) => [
row['Selection ID'],
row['Manual Votes'],
row['Scanned Votes'],
row['Total Votes'],
])
).toEqual([
['zebra', '0', '0', '0'],
['lion', '10', '1', '11'],
['kangaroo', '5', '1', '6'],
['elephant', '0', '1', '1'],
['overvotes', '3', '0', '3'],
['undervotes', '2', '0', '2'],
]);

expect(
rows
.filter((r) => r['Contest ID'] === 'fishing')
.map((row) => [
row['Selection ID'],
row['Manual Votes'],
row['Scanned Votes'],
row['Total Votes'],
])
).toEqual([
['ban-fishing', '1', '1', '2'],
['allow-fishing', '9', '0', '9'],
['overvotes', '4', '0', '4'],
['undervotes', '6', '0', '6'],
]);
});
1 change: 1 addition & 0 deletions apps/admin/backend/src/tabulation/full_results.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,7 @@ export async function tabulateElectionResults({
});
}
);
/* c8 ignore next 3 - debug only */
} else {
debug('filter or group by is not compatible with manual results');
}
Expand Down

0 comments on commit 153285f

Please sign in to comment.