Skip to content

Commit ccb2a3b

Browse files
authored
test(TaxForms): add fields snapshots (#1172)
1 parent 97e4e55 commit ccb2a3b

File tree

7 files changed

+133
-7
lines changed

7 files changed

+133
-7
lines changed

server/lib/pdf-lib-utils.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ type FieldTypeCombo = {
9292
transform?: (value, allValues) => string;
9393
};
9494

95+
// Field types. Make sure to update `getAllFieldsFromDefinition` when adding new field types.
96+
9597
type AdvancedPDFFieldDefinition = {
9698
formPath: string;
9799
if?: (value, allValues) => boolean;

server/lib/tax-forms/utils.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,43 @@
1+
import { mapValues } from 'lodash-es';
2+
import { isFieldTypeCombo, isFieldTypeMulti, isFieldTypeSplitText, PDFFieldDefinition } from '../pdf-lib-utils.js';
3+
14
export const getFullName = ({ firstName = undefined, middleName = undefined, lastName = undefined }): string => {
25
return [firstName, middleName, lastName].filter(Boolean).join(' ').trim();
36
};
7+
8+
const getPathsFromField = (field: PDFFieldDefinition): string[] => {
9+
if (typeof field === 'string') {
10+
return [field];
11+
} else if (isFieldTypeMulti(field) || isFieldTypeSplitText(field)) {
12+
return field.fields.map(subField => getPathsFromField(subField)).flat();
13+
} else if (isFieldTypeCombo(field)) {
14+
return Object.values(field.values)
15+
.map(subField => getPathsFromField(subField))
16+
.flat();
17+
} else if (field.formPath) {
18+
return [field.formPath];
19+
}
20+
21+
return [];
22+
};
23+
24+
/**
25+
* Get a map of all fields from the definition, including all subfields.
26+
* @returns a map like: { attributeName: ["fieldPath1", "fieldPath2"] }
27+
*/
28+
export const getAllFieldsFromDefinition = (
29+
definition: Partial<Record<string, PDFFieldDefinition>>,
30+
): Record<string, string[]> => {
31+
const fields: Record<string, Set<string>> = {};
32+
for (const [attributeName, field] of Object.entries(definition)) {
33+
const paths = getPathsFromField(field);
34+
if (paths.length > 0) {
35+
fields[attributeName] = fields[attributeName] || new Set();
36+
for (const path of paths) {
37+
fields[attributeName].add(path);
38+
}
39+
}
40+
}
41+
42+
return mapValues(fields, paths => Array.from(paths));
43+
};
679 KB
Loading
5.57 MB
Loading
5.42 MB
Loading

test/server/index.test.ts

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,93 @@
1-
import { expect, test } from 'vitest';
1+
import { expect, test, describe, assert } from 'vitest';
22
import request from 'supertest';
33
import app from '../../server/index.js';
4+
import { PDFDocument, rgb } from 'pdf-lib';
5+
import { TAX_FORMS } from '../../server/lib/tax-forms/index.js';
6+
import { getAllFieldsFromDefinition } from '../../server/lib/tax-forms/utils.js';
7+
import { snapshotPDF } from '../utils.js';
48

5-
test('returns a 404 for unknown routes', async () => {
6-
const response = await request(app).get('/unknown-route');
7-
expect(response.status).toBe(404);
8-
expect(response.body.message).toBe('Route not found');
9+
describe('Tax forms', () => {
10+
test('returns a 404 for unknown routes', async () => {
11+
const response = await request(app).get('/unknown-route');
12+
expect(response.status).toBe(404);
13+
expect(response.body.message).toBe('Route not found');
14+
});
15+
16+
describe('PDF Field Visualization', () => {
17+
for (const [formType, formConfig] of Object.entries(TAX_FORMS)) {
18+
test(`Show fields for ${formType}`, async () => {
19+
// Get all fields from the definition
20+
const allFields = getAllFieldsFromDefinition(formConfig.definition);
21+
22+
// Load the PDF
23+
const pdfDoc = await PDFDocument.load(formConfig.bytes);
24+
const form = pdfDoc.getForm();
25+
const pages = pdfDoc.getPages();
26+
const font = await pdfDoc.embedFont('Helvetica');
27+
28+
// For each field attribute, draw red boxes over all its field paths
29+
for (const [attributeName, fieldPaths] of Object.entries(allFields)) {
30+
for (const fieldPath of fieldPaths) {
31+
const field = form.getField(fieldPath);
32+
assert(field, `Field ${fieldPath} not found in ${formType}, but mapped in the definition`);
33+
34+
const widgets = field.acroField.getWidgets();
35+
36+
for (const widget of widgets) {
37+
// Get the page reference from the widget
38+
const widgetPageRef = widget.P();
39+
40+
// Find the corresponding page index
41+
const pageIndex = pages.findIndex(page => page.ref === widgetPageRef);
42+
43+
if (pageIndex !== -1) {
44+
const page = pages[pageIndex];
45+
const rect = widget.getRectangle();
46+
47+
// Draw red box around the field
48+
page.drawRectangle({
49+
x: rect.x,
50+
y: rect.y,
51+
width: rect.width,
52+
height: rect.height,
53+
borderColor: rgb(1, 0, 0),
54+
borderWidth: 2,
55+
});
56+
57+
// Draw label above the field in red
58+
const labelText = attributeName;
59+
const fontSize = 8;
60+
const padding = 2;
61+
const labelY = rect.y + padding;
62+
63+
// Calculate text dimensions
64+
const textWidth = font.widthOfTextAtSize(labelText, fontSize);
65+
const textHeight = font.heightAtSize(fontSize);
66+
67+
// Draw white background behind the label
68+
page.drawRectangle({
69+
x: rect.x - padding,
70+
y: labelY - padding,
71+
width: textWidth + padding * 2,
72+
height: textHeight + padding * 2,
73+
color: rgb(1, 1, 1),
74+
});
75+
76+
page.drawText(labelText, {
77+
x: rect.x,
78+
y: labelY,
79+
size: fontSize,
80+
color: rgb(1, 0, 0),
81+
});
82+
}
83+
}
84+
}
85+
}
86+
87+
// Save the PDF and snapshot it
88+
const pdfBytes = await pdfDoc.save();
89+
await snapshotPDF(Buffer.from(pdfBytes), `tax-form-fields-${formType.toLowerCase()}.pdf`);
90+
}, 60_000); // 60 second timeout for PDF processing (slower in CI)
91+
}
92+
});
993
});

test/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import { CompareOptions, comparePdfToSnapshot } from 'pdf-visual-diff';
22

33
export const snapshotPDF = async (
4-
pdfContent,
5-
snapshotName,
4+
pdfContent: Buffer,
5+
snapshotName: string,
66
options: Omit<CompareOptions, 'failOnMissingSnapshot'> = {},
77
) => {
88
if (

0 commit comments

Comments
 (0)