-
Notifications
You must be signed in to change notification settings - Fork 0
Add PROOF ballots to the report #5
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull request overview
This PR adds “PROOF” ballot PDFs to VxQA runs and exposes them in the generated HTML report to help visually verify bubble-to-contest/option mapping (and catch issues like candidate flip/reorder when totals match).
Changes:
- Generate PROOF ballot PDFs during
runQAWorkflowand include them in the report output directory. - Update HTML report generation to render a “Ballot Proof Gallery” pairing each base ballot PDF with its PROOF counterpart.
- Add PDF snapshot testing utilities (Vitest +
jest-image-snapshot) and snapshot fixtures for proof ballot rendering.
Reviewed changes
Copilot reviewed 9 out of 16 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| vitest.config.ts | Adds a Vitest setup file to register custom matchers. |
| src/test/setup.ts | Extends Vitest expect with toMatchImageSnapshot. |
| src/test/pdf-snapshot.ts | Adds a helper to render PDF pages to images and snapshot-test them. |
| src/report/pdf-thumbnail.ts | Makes PDF thumbnail scale configurable. |
| src/report/html-generator.ts | Builds base/proof ballot pairs and renders a new “Ballot Proof Gallery”. |
| src/cli/config-runner.ts | Generates PROOF-*.pdf alongside each base ballot PDF. |
| src/ballots/proof-ballot.ts | Implements proof ballot annotation overlay generation with pdf-lib. |
| src/ballots/proof-ballot.test.ts | Adds unit + fixture-based snapshot tests for proof ballots. |
| src/ballots/image_snapshots/* | Adds snapshot PNGs for proof ballot fixture rendering. |
| package.json | Adds dev deps for image snapshot testing. |
| pnpm-lock.yaml | Locks new snapshot-testing dependencies. |
Files not reviewed (1)
- pnpm-lock.yaml: Language not supported
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
src/ballots/proof-ballot.test.ts
Outdated
| describe('generateProofBallot with real election fixture', async () => { | ||
| const tmp = await mkdtemp(join(tmpdir(), 'vx-qa-proof-test-')); | ||
| afterAll(() => rm(tmp, { recursive: true, force: true })); | ||
|
|
||
| const { electionPackage } = await loadElectionPackage(FIXTURE_PATH, tmp); | ||
| const { election } = electionPackage.electionDefinition; |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
describe blocks should be synchronous in Vitest. Using an async callback here means the awaited setup runs outside Vitest's normal suite-definition phase and can lead to tests not being registered or running in an unexpected order. Move the async setup (mkdtemp/loadElectionPackage/fixture ballot lookup) into beforeAll (or beforeEach) inside a synchronous describe, and store the loaded election/ballots in variables for the tests to use.
| const FIXTURE_PATH = join( | ||
| import.meta.dirname, | ||
| '../../test-fixtures/election-package-and-ballots-e71c80e-c4446e7.zip', | ||
| ); |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
import.meta.dirname is only available in newer Node versions, but this repo's engines.node allows any >=20.0.0. To avoid test failures on Node 20.x versions that don't support it, derive the directory from import.meta.url (via fileURLToPath(new URL('.', import.meta.url))) or tighten the Node engine range to a version that guarantees import.meta.dirname.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I believe this will be set up by vitest.
src/report/html-generator.ts
Outdated
| async function makeBallotGalleryThumbnail( | ||
| file: (typeof ballotFiles)[number], | ||
| ): Promise<string | null> { | ||
| return file.name.endsWith('.pdf') | ||
| ? await generatePdfThumbnail(file.path, { scale: 2 }) | ||
| : `data:image/png;base64,${await readFileAsBase64(file.path)}`; |
Copilot
AI
Feb 10, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Generating ballot gallery thumbnails for PDFs at scale: 2 can be significantly more CPU/memory intensive than the previous thumbnail behavior, especially if there are many ballots. Consider using a smaller default scale (or making it configurable via CLI/config) so report generation stays responsive while still being readable.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For the number of ballots we have right now, this is likely to be acceptable. A lot of this is going to be untenable if our ballot count goes beyond ~30 or so.
Overlay contest/candidate labels on each ballot PDF so a human can visually verify that bubble positions are correctly mapped. Proof PDFs are saved alongside base ballots and now appear in the HTML report's new Ballot Gallery section.
Add jest-image-snapshot with a lightweight helper that renders PDF pages via pdf-to-img and compares them pixel-by-pixel, similar to vxsuite's toMatchPdfSnapshot.
Export pure geometry/label functions for direct unit testing. Add PDF snapshot test that renders a synthetic proof ballot and compares against stored baseline images.
Filter gallery to ballots that have a PROOF- counterpart, excluding marked/scanned variants. Display each pair side-by-side for easy comparison. Render gallery thumbnails at 2x scale for readability.
Replace synthetic snapshot test with fixture-based tests that generate proof ballots from the real election package, covering both ballot styles (2-page and 4-page).
e165436 to
7c75f3e
Compare
We currently generate votes and mark ballots accordingly, then sum up those votes and compare the totals against the totals of all the CVRs. This ensures that the totals are correct for the given marking patterns, but it doesn't protect against candidate bubbles being flipped or reordered if both candidates got the same total.
As a first step in protecting against that case, this change makes VxQA produce PROOF ballots, i.e. copies of the ballots annotated to aid in validating their correctness. They have the candidate/contest option associated with the bubble shown with labels in a green box, the locations we expect the bubbles to be marked with a red X, and the area we inspect for write-in marks in a tan box.
The implementation is based on a previous version that was only ever used on a New Hampshire-specific branch: votingworks/vxsuite#5267. It's modified a bit for style and to suit the slightly different tooling in vx-qa.
Tests pull in
jest-image-snapshotto do PDF snapshotting in a similar manner to how vxsuite does it.Added: Ballot Proof Gallery (screenshot)
Example PROOF PDF:
PROOF-ballot-2_en-s3y4ua5am2y7-official-absentee.pdf