@suffolklitlab/alkiln 5.3.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +8 -0
- package/lib/scope.js +56 -18
- package/lib/steps.js +24 -1
- package/lib/utils/pdf_reader.js +113 -0
- package/npm-shrinkwrap.json +757 -8
- package/package.json +4 -2
package/CHANGELOG.md
CHANGED
|
@@ -44,6 +44,14 @@ Format:
|
|
|
44
44
|
-->
|
|
45
45
|
<!-- ## [Unreleased] -->
|
|
46
46
|
|
|
47
|
+
## [5.4.0] - 2023-10-14
|
|
48
|
+
|
|
49
|
+
### Added
|
|
50
|
+
|
|
51
|
+
- New step to compare PDFs!
|
|
52
|
+
- An example: `I expect the baseline PDF "something-in-sources.pdf" and the new PDF "downloaded.pdf" to be the same`
|
|
53
|
+
- See [#774](https://github.com/SuffolkLITLab/ALKiln/pull/774) for more details.
|
|
54
|
+
|
|
47
55
|
## [5.3.0] - 2023-10-14
|
|
48
56
|
|
|
49
57
|
### Added
|
package/lib/scope.js
CHANGED
|
@@ -9,6 +9,7 @@ const safe_filename = require("sanitize-filename");
|
|
|
9
9
|
const faker = require('@faker-js/faker/locale/en').faker;
|
|
10
10
|
|
|
11
11
|
// Ours
|
|
12
|
+
const pdf_reader = require('./utils/pdf_reader');
|
|
12
13
|
const session_vars = require('./utils/session_vars');
|
|
13
14
|
const get_base_interview_url = require(`./docassemble/get_base_interview_url`);
|
|
14
15
|
const time = require('./utils/time');
|
|
@@ -1679,22 +1680,15 @@ module.exports = {
|
|
|
1679
1680
|
return valid_date;
|
|
1680
1681
|
}, // Ends scope.today_to_date()
|
|
1681
1682
|
|
|
1682
|
-
|
|
1683
|
-
/* Try to upload one or more files to a file-type input field.
|
|
1684
|
-
* Does not submit, so no danger of timeout with server reload.
|
|
1685
|
-
*
|
|
1686
|
-
* Returns whether the row was used or not.
|
|
1687
|
-
*/
|
|
1688
|
-
|
|
1683
|
+
findFiles: async function( scope, {to_find_names}) {
|
|
1689
1684
|
// First make sure the list of files was defined. Don't remove spaces yet -
|
|
1690
1685
|
// allow a typo where someone forgot to include a space after a comma.
|
|
1691
|
-
|
|
1692
|
-
if ( uploads_names.length === 0 ) {
|
|
1686
|
+
if ( to_find_names.length === 0 ) {
|
|
1693
1687
|
reports.addToReport( scope, {
|
|
1694
1688
|
type: `error`,
|
|
1695
|
-
value: `Your list of files to
|
|
1689
|
+
value: `Your list of files to find is empty.`
|
|
1696
1690
|
});
|
|
1697
|
-
return
|
|
1691
|
+
return [];
|
|
1698
1692
|
}
|
|
1699
1693
|
|
|
1700
1694
|
let dirs = [];
|
|
@@ -1717,7 +1711,7 @@ module.exports = {
|
|
|
1717
1711
|
}
|
|
1718
1712
|
|
|
1719
1713
|
if ( session_vars.get_debug() ) {
|
|
1720
|
-
console.log( `All discovered dirs to
|
|
1714
|
+
console.log( `All discovered dirs to find files from:`, dirs );
|
|
1721
1715
|
}
|
|
1722
1716
|
|
|
1723
1717
|
// Make sure sources directory exists and has files in it
|
|
@@ -1727,14 +1721,13 @@ module.exports = {
|
|
|
1727
1721
|
value: `The test files need to be in the "sources" directory, `
|
|
1728
1722
|
+ `but ALKiln couldn't find the directory or it was empty.`
|
|
1729
1723
|
});
|
|
1730
|
-
return
|
|
1724
|
+
return [];
|
|
1731
1725
|
}
|
|
1732
1726
|
|
|
1733
1727
|
// Get the full paths of all the files
|
|
1734
|
-
let dir = null;
|
|
1735
1728
|
let paths = [];
|
|
1736
1729
|
let missing_files = []; // For any user warnings
|
|
1737
|
-
for ( let name of
|
|
1730
|
+
for ( let name of to_find_names) {
|
|
1738
1731
|
|
|
1739
1732
|
let trimmed_name = name.trim();
|
|
1740
1733
|
let matching_paths = [];
|
|
@@ -1760,9 +1753,27 @@ module.exports = {
|
|
|
1760
1753
|
});
|
|
1761
1754
|
}
|
|
1762
1755
|
|
|
1763
|
-
// If all files missing, return
|
|
1764
|
-
if ( missing_files.length ===
|
|
1765
|
-
return
|
|
1756
|
+
// If all files missing, return an empty array
|
|
1757
|
+
if ( missing_files.length === to_find_names.length ) {
|
|
1758
|
+
return [];
|
|
1759
|
+
}
|
|
1760
|
+
return paths;
|
|
1761
|
+
},
|
|
1762
|
+
|
|
1763
|
+
uploadFiles: async function ( scope, { handle, answer }) {
|
|
1764
|
+
/* Try to upload one or more files to a file-type input field.
|
|
1765
|
+
* Does not submit, so no danger of timeout with server reload.
|
|
1766
|
+
*
|
|
1767
|
+
* Returns whether the row was used or not.
|
|
1768
|
+
*/
|
|
1769
|
+
|
|
1770
|
+
// First make sure the list of files was defined. Don't remove spaces yet -
|
|
1771
|
+
// allow a typo where someone forgot to include a space after a comma.
|
|
1772
|
+
let to_find_names = answer.split(`,`);
|
|
1773
|
+
let paths = await scope.findFiles(scope, {to_find_names});
|
|
1774
|
+
if (paths.length == 0) {
|
|
1775
|
+
// The row wasn't used
|
|
1776
|
+
return false;
|
|
1766
1777
|
}
|
|
1767
1778
|
|
|
1768
1779
|
// Prepare to wait for upload to complete
|
|
@@ -2503,6 +2514,32 @@ module.exports = {
|
|
|
2503
2514
|
await scope.detectDownloadComplete( scope );
|
|
2504
2515
|
}, // Ends scope.steps.download()
|
|
2505
2516
|
|
|
2517
|
+
compare_pdfs: async function (scope, {existing_pdf_path, new_pdf_path}) {
|
|
2518
|
+
let existing_paths = await scope.findFiles(scope, {to_find_names: [existing_pdf_path]});
|
|
2519
|
+
if (existing_paths.length == 0) {
|
|
2520
|
+
// The row wasn't used
|
|
2521
|
+
let msg = `Could not find the existing PDF at ${ existing_pdf_path }`;
|
|
2522
|
+
reports.addToReport(scope, { type: `error`, value: msg });
|
|
2523
|
+
scope.failed_pdf_compares.push(msg);
|
|
2524
|
+
// Return early since we couldn't find the PDF
|
|
2525
|
+
return;
|
|
2526
|
+
}
|
|
2527
|
+
let full_existing_path = existing_paths[0];
|
|
2528
|
+
let full_new_pdf_path = `${scope.paths.scenario}/${new_pdf_path}`;
|
|
2529
|
+
let diffs = await pdf_reader.compare_pdfs(full_existing_path, full_new_pdf_path);
|
|
2530
|
+
if (diffs) {
|
|
2531
|
+
let added_str = diffs.filter(part => part.added).reduce((err_str, part) => {
|
|
2532
|
+
return err_str + `- ${ part.value }\n`
|
|
2533
|
+
}, 'The new PDF added: \n');
|
|
2534
|
+
let removed_str = diffs.filter(part => part.removed).reduce((err_str, part) => {
|
|
2535
|
+
return err_str + `- ${ part.value }\n`
|
|
2536
|
+
}, 'The new PDF removed: \n');
|
|
2537
|
+
let msg = `The PDFs were not the same.\n${ added_str }\n${removed_str}\n\n You can see the full PDFs at ${ full_existing_path } and ${ full_new_pdf_path}`;
|
|
2538
|
+
reports.addToReport(scope, { type: `error`, value: msg });
|
|
2539
|
+
scope.failed_pdf_compares.push(msg);
|
|
2540
|
+
}
|
|
2541
|
+
},
|
|
2542
|
+
|
|
2506
2543
|
set_al_name: async ( scope, var_base, name_str ) => {
|
|
2507
2544
|
/** Given a variable name and a space-separated string, fills the
|
|
2508
2545
|
* appropriate fields on an Assembly Line name question.
|
|
@@ -2930,4 +2967,5 @@ module.exports = {
|
|
|
2930
2967
|
|
|
2931
2968
|
return none_included;
|
|
2932
2969
|
},
|
|
2970
|
+
|
|
2933
2971
|
};
|
package/lib/steps.js
CHANGED
|
@@ -147,6 +147,7 @@ Before(async (scenario) => {
|
|
|
147
147
|
scope.page_id = null;
|
|
148
148
|
scope.check_all_for_a11y = false;
|
|
149
149
|
scope.passed_all_for_a11y = true;
|
|
150
|
+
scope.failed_pdf_compares = [];
|
|
150
151
|
|
|
151
152
|
// Reset default timeout
|
|
152
153
|
scope.timeout = default_timeout;
|
|
@@ -705,6 +706,19 @@ Then(/I get the(?: page's)?(?: JSON)? var(?:iable)?s and val(?:ue)?s/i, { timeou
|
|
|
705
706
|
);
|
|
706
707
|
});
|
|
707
708
|
|
|
709
|
+
Then(/I expect the baseline PDF "([^"]+)" and the new PDF "([^"]+)" to be the same/i, async (existing_pdf_path, new_pdf_path) => {
|
|
710
|
+
/** Compare the text or fields of a baseline PDF and the newly downloaded PDF.
|
|
711
|
+
*
|
|
712
|
+
* Variations:
|
|
713
|
+
* Then I expect the baseline PDF "simple-doc-Baseline.pdf" and the new PDF "simple-doc.pdf" to be the same
|
|
714
|
+
*/
|
|
715
|
+
|
|
716
|
+
return await wrapPromiseWithTimeout(
|
|
717
|
+
scope.steps.compare_pdfs(scope, {existing_pdf_path, new_pdf_path}),
|
|
718
|
+
scope.timeout
|
|
719
|
+
);
|
|
720
|
+
});
|
|
721
|
+
|
|
708
722
|
//#####################################
|
|
709
723
|
//#####################################
|
|
710
724
|
// Actions
|
|
@@ -1104,12 +1118,21 @@ After(async function(scenario) {
|
|
|
1104
1118
|
if ( scope.check_all_for_a11y && !scope.passed_all_for_a11y) {
|
|
1105
1119
|
// The fact that the scenario failed has already been noted earlier in the report
|
|
1106
1120
|
changeable_test_status = `FAILED`;
|
|
1107
|
-
|
|
1121
|
+
reports.addToReport(scope, {
|
|
1108
1122
|
type: `error`,
|
|
1109
1123
|
value: `Accessibility standards weren't met. See information above.`
|
|
1110
1124
|
});
|
|
1111
1125
|
}
|
|
1112
1126
|
|
|
1127
|
+
if ( scope.failed_pdf_compares.length > 0) {
|
|
1128
|
+
let msg = scope.failed_pdf_compares.reduce((str, new_msg) => `${ str }\n---\n${ new_msg }`)
|
|
1129
|
+
changeable_test_status = `FAILED`;
|
|
1130
|
+
reports.addToReport(scope, {
|
|
1131
|
+
type: `error`,
|
|
1132
|
+
value: `PDF comparison failed ${ scope.failed_pdf_compares.length } time(s)\n---\n${ msg }\n---\n`
|
|
1133
|
+
});
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1113
1136
|
// Record status before internal report tests can interfere with values
|
|
1114
1137
|
reports.setReportScenarioStatus( scope, { status: changeable_test_status });
|
|
1115
1138
|
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// All functions dealing with getting information from PDFs
|
|
2
|
+
|
|
3
|
+
const pdfjs_lib = require("pdfjs-dist/legacy/build/pdf.js");
|
|
4
|
+
const diff_lib = require("diff");
|
|
5
|
+
|
|
6
|
+
let _load_pdf = async function(pdf_path) {
|
|
7
|
+
// Simple wrapper to get the document
|
|
8
|
+
return await pdfjs_lib.getDocument({
|
|
9
|
+
url: pdf_path,
|
|
10
|
+
// https://github.com/mozilla/pdf.js/issues/4244#issuecomment-1479534301
|
|
11
|
+
useSystemFonts: true
|
|
12
|
+
}).promise;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
let _get_content = async function(pdf_doc) {
|
|
16
|
+
let num_pages = pdf_doc.numPages;
|
|
17
|
+
let contents = "";
|
|
18
|
+
for (var i = 1; i <= num_pages; i++) {
|
|
19
|
+
const page_contents = await (await pdf_doc.getPage(i)).getTextContent();
|
|
20
|
+
contents += "\n" + page_contents.items.map((item) => item.str).join(" ");
|
|
21
|
+
}
|
|
22
|
+
return contents;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
let pdf_reader = {};
|
|
26
|
+
|
|
27
|
+
pdf_reader.get_content = async function(pdf_path) {
|
|
28
|
+
/* Gets the full text content from the PDF.
|
|
29
|
+
*
|
|
30
|
+
* @returns str
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
let doc = await _load_pdf(pdf_path);
|
|
34
|
+
return await _get_content(doc);
|
|
35
|
+
}; // Ends get_content()
|
|
36
|
+
|
|
37
|
+
pdf_reader.get_fields = async function(pdf_path) {
|
|
38
|
+
/* Gets all of the fields in the PDF
|
|
39
|
+
*
|
|
40
|
+
* Example:
|
|
41
|
+
* {
|
|
42
|
+
* "users1_name_first": [
|
|
43
|
+
* {
|
|
44
|
+
* "id": "6R",
|
|
45
|
+
* "value": "aeu",
|
|
46
|
+
* "defaultValue": "",
|
|
47
|
+
* "multiline": false,
|
|
48
|
+
* "password": false,
|
|
49
|
+
* "charLimit": 0,
|
|
50
|
+
* "comb": false,
|
|
51
|
+
* "editable": true,
|
|
52
|
+
* "hidden": false,
|
|
53
|
+
* "name": "users1_name_first",
|
|
54
|
+
* "rect": [
|
|
55
|
+
* 143.52,
|
|
56
|
+
* 660.72,
|
|
57
|
+
* 315.11,
|
|
58
|
+
* 674.88
|
|
59
|
+
* ],
|
|
60
|
+
* "actions": null,
|
|
61
|
+
* "page": 0,
|
|
62
|
+
* "strokeColor": null,
|
|
63
|
+
* "fillColor": null,
|
|
64
|
+
* "rotation": 0,
|
|
65
|
+
* "type": "text"
|
|
66
|
+
* }
|
|
67
|
+
* ],
|
|
68
|
+
*/
|
|
69
|
+
let doc = await _load_pdf(pdf_path);
|
|
70
|
+
const field_objects = await doc.getFieldObjects();
|
|
71
|
+
return field_objects;
|
|
72
|
+
}; // Ends get_fields
|
|
73
|
+
|
|
74
|
+
pdf_reader.get_field_value = async function(pdf_path, field_name) {
|
|
75
|
+
let field_objects = await pdf_reader.get_fields(pdf_path);
|
|
76
|
+
if (field_name in field_objects) {
|
|
77
|
+
for (let inst of field_objects[field_name]) {
|
|
78
|
+
if ("value" in inst) {
|
|
79
|
+
return inst["value"];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}; // Ends get_field
|
|
85
|
+
|
|
86
|
+
pdf_reader.compare_pdfs = async function(existing_pdf_path, new_pdf_path) {
|
|
87
|
+
/**
|
|
88
|
+
* Returns the change objects between the PDF fields, if there's any diff
|
|
89
|
+
*/
|
|
90
|
+
let existing_doc = await _load_pdf(existing_pdf_path);
|
|
91
|
+
let new_doc = await _load_pdf(new_pdf_path);
|
|
92
|
+
let existing_content = await _get_content(existing_doc);
|
|
93
|
+
let new_content = await _get_content(new_doc);
|
|
94
|
+
|
|
95
|
+
// Assumption: authors won't be changing their PDF templates programmatically;
|
|
96
|
+
// so if the text of the PDFs are different, then it's actually the fields that are different,
|
|
97
|
+
// but they aren't editable / are flattened into the PDF.
|
|
98
|
+
let change = diff_lib.diffWords(existing_content, new_content);
|
|
99
|
+
if (change.some(diff => diff.added || diff.removed)) {
|
|
100
|
+
return change;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const existing_fields = await existing_doc.getFieldObjects();
|
|
104
|
+
const new_fields = await new_doc.getFieldObjects();
|
|
105
|
+
|
|
106
|
+
let changeObjs = diff_lib.diffJson(existing_fields, new_fields);
|
|
107
|
+
if (changeObjs.some(diff => diff.added || diff.removed)) {
|
|
108
|
+
return changeObjs;
|
|
109
|
+
}
|
|
110
|
+
return null;
|
|
111
|
+
}; // End compare_pdfs
|
|
112
|
+
|
|
113
|
+
module.exports = pdf_reader;
|