testilo 26.0.0 → 28.0.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/README.md CHANGED
@@ -768,7 +768,43 @@ node call summarize divisions 2411
768
768
 
769
769
  When a user invokes `summarize` in this example, the `call` module:
770
770
  - gets all the reports in the `scored` subdirectory of the `REPORTDIR` directory, or (if the third argument is present) all those whose file names begin with `2411`.
771
- - writes the summary as a JSON file named `divisions.json` to the `summarized` subdirectory of the `REPORTDIR` directory.
771
+ - writes the summary, with `divisions` as its description, in JSON format to the `summarized` subdirectory of the `REPORTDIR` directory.
772
+
773
+ ### Track
774
+
775
+ The `track` module of Testilo selects, organizes, and presents data from summaries to show changes over time in total scores. The module produces a web page, showing changes in a table and (in the future) also in a line graph.
776
+
777
+ #### Invocation
778
+
779
+ ##### By a module
780
+
781
+ A module can invoke `track()` in this way:
782
+
783
+ ```javaScript
784
+ const {track} = require('testilo/track');
785
+ const trackerDir = `${process.env.FUNCTIONDIR}/track/ttp99a`;
786
+ const {tracker} = require(`${trackerDir}/index`);
787
+ const summary = …;
788
+ const [reportID, trackReport] = track(tracker, summary);
789
+ ```
790
+
791
+ The `track()` function returns an ID and an HTML tracking report that shows data for all of the results in the summary. The invoking module can further dispose of the report as needed.
792
+
793
+ ##### By a user
794
+
795
+ A user can invoke `track()` in this way:
796
+
797
+ ```javaScript
798
+ node call track ttp99a 241016T2045-Uf-0 4 'ABC Foundation'
799
+ ```
800
+
801
+ When a user invokes `track()` in this example, the `call` module:
802
+ - gets the summary from the `241016T2045-Uf-0.json` file in the `summarized` subdirectory of the `REPORTDIR` directory.
803
+ - selects the summarized data for all audits with the `order` value of `'4'` and the `target.what` value of `'ABC Foundation'`. If the third or fourth argument to `call()` is `null` (or omitted), then `call()` does not select audits by `order` or by `target.what`, respectively.
804
+ - uses tracker `ttp99a` to create a tracking report.
805
+ - writes the tracking report to the `tracking` subdirectory of the `REPORTDIR` directory.
806
+
807
+ The tracking reports created by `track()` are HTML files, and they expect a `style.css` file to exist in their directory. The `reports/tracking/style.css` file in Testilo is an appropriate stylesheet to be copied into the directory where tracking reports are written.
772
808
 
773
809
  ## Origin
774
810
 
package/call.js CHANGED
@@ -36,6 +36,8 @@ const {difgest} = require('./difgest');
36
36
  const {compare} = require('./compare');
37
37
  // Function to summarize reports.
38
38
  const {summarize} = require('./summarize');
39
+ // Function to track audits.
40
+ const {track} = require('./track');
39
41
 
40
42
  // ########## CONSTANTS
41
43
 
@@ -153,8 +155,7 @@ const callDigest = async (digesterID, selector = '') => {
153
155
  // If any exist:
154
156
  if (reports.length) {
155
157
  // Get the digester.
156
- const digesterDir = `${functionDir}/digest/${digesterID}`;
157
- const {digester} = require(`${digesterDir}/index`);
158
+ const {digester} = require(`${functionDir}/digest/${digesterID}/index`);
158
159
  // Digest the reports.
159
160
  const digestedReports = await digest(digester, reports);
160
161
  const digestedReportDir = `${reportDir}/digested`;
@@ -172,7 +173,7 @@ const callDigest = async (digesterID, selector = '') => {
172
173
  console.log('ERROR: No scored reports to be digested');
173
174
  }
174
175
  };
175
- // Fulfills a digesting request.
176
+ // Fulfills a difgesting request.
176
177
  const callDifgest = async (difgesterID, reportAID, reportBID) => {
177
178
  // Get the scored reports to be difgested.
178
179
  const reportAArray = await getReports('scored', reportAID);
@@ -240,7 +241,7 @@ const callCredit = async (tallyID, selector = '') => {
240
241
  console.log('ERROR: No scored reports to be tallied');
241
242
  }
242
243
  };
243
- // Fulfills a summarize request.
244
+ // Fulfills a summarization request.
244
245
  const callSummarize = async (what, selector = '') => {
245
246
  // Get the scored reports to be summarized.
246
247
  const reports = await getReports('scored', selector);
@@ -255,7 +256,7 @@ const callSummarize = async (what, selector = '') => {
255
256
  // Save the summary.
256
257
  const summaryDir = `${reportDir}/summarized`;
257
258
  await fs.mkdir(summaryDir, {recursive: true});
258
- const filePath = `${summaryDir}/${summary.timeStamp}-${getRandomString(2)}-0.json`;
259
+ const filePath = `${summaryDir}/${summary.id}.json`;
259
260
  await fs.writeFile(filePath, `${JSON.stringify(summary, null, 2)}\n`);
260
261
  console.log(`Reports summarized and summary saved as ${filePath}`);
261
262
  }
@@ -265,6 +266,43 @@ const callSummarize = async (what, selector = '') => {
265
266
  console.log('ERROR: No scored reports to be summarized');
266
267
  }
267
268
  };
269
+ // Fulfills a tracking request.
270
+ const callTrack = async (trackerID, summaryID, orderID, targetWhat) => {
271
+ // Get the summary.
272
+ try {
273
+ const summaryJSON = await fs.readFile(`${reportDir}/summarized/${summaryID}.json`, 'utf8');
274
+ const summary = JSON.parse(summaryJSON);
275
+ // Remove unwanted audits from it.
276
+ summary.data = summary.data.filter(audit => {
277
+ if (orderID && audit.order !== orderID) {
278
+ return false;
279
+ }
280
+ if (targetWhat && audit.target && audit.target.what !== targetWhat) {
281
+ return false;
282
+ }
283
+ return true;
284
+ });
285
+ // If any audits remain:
286
+ if (summary.data.length) {
287
+ // Get the tracker.
288
+ const {tracker} = require(`${functionDir}/track/${trackerID}/index`);
289
+ // Track the audits.
290
+ const [reportID, trackingReport] = await track(tracker, summary);
291
+ // Save the tracking report.
292
+ await fs.mkdir(`${reportDir}/tracking`, {recursive: true});
293
+ const reportPath = `${reportDir}/tracking/${reportID}.html`;
294
+ await fs.writeFile(reportPath, trackingReport);
295
+ console.log(`Tracking report saved in ${reportPath}`);
296
+ }
297
+ // Otherwise, i.e. if no audits remain:
298
+ else {
299
+ console.log('ERROR: No audits match the request');
300
+ }
301
+ }
302
+ catch(error) {
303
+ console.log(`ERROR: Tracking request invalid (${error.message})`);
304
+ }
305
+ };
268
306
 
269
307
  // ########## OPERATION
270
308
 
@@ -329,6 +367,12 @@ else if (fn === 'summarize' && fnArgs.length > 0 && fnArgs.length < 3) {
329
367
  console.log('Execution completed');
330
368
  });
331
369
  }
370
+ else if (fn === 'track' && fnArgs.length > 1 && fnArgs.length < 5) {
371
+ callTrack(... fnArgs)
372
+ .then(() => {
373
+ console.log('Execution completed');
374
+ });
375
+ }
332
376
  else {
333
377
  console.log('ERROR: Invalid statement');
334
378
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "26.0.0",
3
+ "version": "28.0.0",
4
4
  "description": "Prepares and processes Testaro reports",
5
5
  "main": "call.js",
6
6
  "scripts": {
@@ -7,7 +7,7 @@
7
7
 
8
8
  // Module to access files.
9
9
  const fs = require('fs/promises');
10
- const {getBarCell} = require('../../util');
10
+ const {getBarCell, getNowDate, getNowDateSlash} = require('../../util');
11
11
 
12
12
  // CONSTANTS
13
13
 
@@ -63,9 +63,8 @@ const populateQuery = async (scoredReports, query) => {
63
63
  const data = await getData(scoredReports);
64
64
  query.pageCount = data.pageCount;
65
65
  query.tableBody = await getTableBody(data.bodyData);
66
- const date = new Date();
67
- query.dateISO = date.toISOString().slice(0, 10);
68
- query.dateSlash = query.dateISO.replace(/-/g, '/');
66
+ query.dateISO = getNowDate();
67
+ query.dateSlash = getNowDateSlash();
69
68
  };
70
69
  // Returns a comparative report.
71
70
  exports.comparer = async scoredReports => {
@@ -9,7 +9,7 @@ const {issues} = require('../../score/tic40');
9
9
  // Module to process files.
10
10
  const fs = require('fs/promises');
11
11
  // Utility module.
12
- const {getBarCell} = require('../../util');
12
+ const {getBarCell, getNowDate, getNowDateSlash} = require('../../util');
13
13
 
14
14
  // CONSTANTS
15
15
 
@@ -46,8 +46,8 @@ const getIssueScoreRow = (summary, wcag, scoreA, scoreB, aSuperiorityMax, bSuper
46
46
  const populateQuery = (reportA, reportB, query) => {
47
47
  // General parameters.
48
48
  query.fp = id;
49
- query.dateISO = new Date().toISOString().slice(0, 10);
50
- query.dateSlash = query.dateISO.replace(/-/g, '/');
49
+ query.dateISO = getNowDate();
50
+ query.dateSlash = getNowDateSlash();
51
51
  // For each report:
52
52
  const issueIDs = new Set();
53
53
  [reportA, reportB].forEach((report, index) => {
@@ -8,6 +8,8 @@ require('dotenv').config();
8
8
  const {issues} = require('../../score/tic40');
9
9
  // Module to process files.
10
10
  const fs = require('fs/promises');
11
+ // Utility module.
12
+ const {getNowDate, getNowDateSlash} = require('../../util');
11
13
 
12
14
  // CONSTANTS
13
15
 
@@ -34,8 +36,8 @@ const populateQuery = (report, query) => {
34
36
  query.sp = scoreProcID;
35
37
  query.dp = digesterID;
36
38
  // Add the job data to the query.
37
- query.dateISO = jobData.endTime.slice(0, 8);
38
- query.dateSlash = query.dateISO.replace(/-/g, '/');
39
+ query.dateISO = getNowDate();
40
+ query.dateSlash = getNowDateSlash();
39
41
  query.org = target.what;
40
42
  query.url = target.which;
41
43
  query.requester = requester;
@@ -0,0 +1,83 @@
1
+ <!DOCTYPE HTML>
2
+ <html lang="en-US">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1">
6
+ <meta name="author" content="Testilo">
7
+ <meta name="creator" content="Testilo">
8
+ <meta name="publisher" name="Testilo">
9
+ <meta name="description" content="report of accessibility testing of web pages">
10
+ <meta name="keywords" content="accessibility a11y web testing">
11
+ <title>Accessibility tracking report</title>
12
+ <link rel="icon" href="favicon.ico">
13
+ <link rel="stylesheet" href="style.css">
14
+ </head>
15
+ <body>
16
+ <main>
17
+ <header>
18
+ <h1>Accessibility tracking report</h1>
19
+ </header>
20
+ <h2>Introduction</h2>
21
+ <p>This is tracking report <code>__id__</code>.</p>
22
+ <p>It tracks accessibility scores over time. A perfect score is 0. The tracking was performed by Testilo procedure __tp__.</p>
23
+ <p>The results are presented first as a graph, and then as a table.</p>
24
+ <h2>Results as a graph</h2>
25
+ <figure id="graph">
26
+ <figcaption>Accessibility scores</figcaption>
27
+ </figure>
28
+ <script src="https://cdn.jsdelivr.net/npm/d3@7"></script>
29
+ <script src="https://cdn.jsdelivr.net/npm/@observablehq/plot@0.6"></script>
30
+ <script type="module">
31
+ const summaryJSON = '__summaryJSON__';
32
+ const summary = JSON.parse(summaryJSON);
33
+ const graphData = [];
34
+ summary.data.forEach(result => {
35
+ graphData.push({
36
+ target: result.target.what,
37
+ time: new Date(`20${result.endTime}Z`),
38
+ score: result.score
39
+ });
40
+ });
41
+ const svg = Plot.plot({
42
+ style: 'overflow: visible;',
43
+ y: {grid: true},
44
+ marks: [
45
+ Plot.ruleY([0]),
46
+ Plot.lineY(graphData, {
47
+ x: 'time',
48
+ y: 'score',
49
+ stroke: 'target'
50
+ }),
51
+ Plot.text(graphData, Plot.selectLast({
52
+ x: 'time',
53
+ y: 'score',
54
+ z: 'target',
55
+ text: 'target',
56
+ textAnchor: 'start',
57
+ dx: 3
58
+ }))
59
+ ]
60
+ });
61
+ document.getElementById('graph').insertAdjacentElement('beforeend', svg);
62
+ </script>
63
+ <h2>Results as a table</h2>
64
+ <table class="allBorder secondCellRight thirdCellRight">
65
+ <caption>Accessibility scores</caption>
66
+ <thead>
67
+ <tr>
68
+ <th>Date and time</th>
69
+ <th>Score</th>
70
+ <th>Order</th>
71
+ <th>Target</th>
72
+ </tr>
73
+ </thead>
74
+ <tbody>
75
+ __scoreRows__
76
+ </tbody>
77
+ </table>
78
+ <footer>
79
+ <p class="date">Produced <time itemprop="datePublished" datetime="__dateISO__">__dateSlash__</time></p>
80
+ </footer>
81
+ </main>
82
+ </body>
83
+ </html>
@@ -0,0 +1,70 @@
1
+ // index: tracker for tracking procedure ttp40.
2
+
3
+ // IMPORTS
4
+
5
+ // Module to keep secrets.
6
+ require('dotenv').config();
7
+ // Module to process files.
8
+ const fs = require('fs/promises');
9
+ // Utility module.
10
+ const {alphaNumOf, getNowDate, getNowDateSlash} = require('../../util');
11
+
12
+ // CONSTANTS
13
+
14
+ // Tracker ID.
15
+ const trackerID = 'ttp40';
16
+ // Newline with indentations.
17
+ const innerJoiner = '\n ';
18
+ // Digest URL.
19
+ const digestURL = process.env.DIGEST_URL;
20
+
21
+ // FUNCTIONS
22
+
23
+ // Adds parameters to a query for a tracking report.
24
+ const populateQuery = async (id, summary, query) => {
25
+ // General parameters.
26
+ query.id = id;
27
+ query.tp = trackerID;
28
+ query.dateISO = getNowDate();
29
+ query.dateSlash = getNowDateSlash();
30
+ // JSON of pruned summary.
31
+ summary.data.forEach(result => {
32
+ delete result.id;
33
+ delete result.target.id;
34
+ delete result.target.which;
35
+ });
36
+ query.summaryJSON = JSON.stringify(summary);
37
+ // For each score:
38
+ const rows = [];
39
+ const results = summary.data;
40
+ const targetWhats = Array.from(new Set(results.map(result => result.target.what))).sort();
41
+ summary.data.forEach(result => {
42
+ // Create an HTML table row for it.
43
+ const timeCell = `<td>${result.endTime}</td>`;
44
+ const digestLinkDestination = digestURL.replace('__id__', result.id);
45
+ const scoreCell = `<td><a href=${digestLinkDestination}>${result.score}</a></td>`;
46
+ const orderCell = `<td class="center">${result.order}</td>`;
47
+ const targetID = alphaNumOf(targetWhats.indexOf(result.target.what));
48
+ const targetLink = `<a href="${result.target.which}">${result.target.what}</a>`;
49
+ const targetCell = `<td>${targetID}: ${targetLink}</td>`;
50
+ const row = `<tr>${[timeCell, scoreCell, orderCell, targetCell].join('')}</tr>`;
51
+ // Add the row to the array of rows.
52
+ rows.push(row);
53
+ });
54
+ // Add the rows to the query.
55
+ query.scoreRows = rows.join(innerJoiner);
56
+ };
57
+ // Returns a tracking report.
58
+ exports.tracker = async (id, summary) => {
59
+ // Create a query to replace placeholders.
60
+ const query = {};
61
+ await populateQuery(id, summary, query);
62
+ // Get the template.
63
+ let template = await fs.readFile(`${__dirname}/index.html`, 'utf8');
64
+ // Replace its placeholders.
65
+ Object.keys(query).forEach(param => {
66
+ template = template.replace(new RegExp(`__${param}__`, 'g'), query[param]);
67
+ });
68
+ // Return the tracking report.
69
+ return template;
70
+ };
package/procs/util.js CHANGED
@@ -18,7 +18,11 @@ const alphaNumChars = (() => {
18
18
  // Returns a string representing a date and time.
19
19
  const getTimeString = date => date.toISOString().slice(0, 19);
20
20
  // Returns a string representing the date and time.
21
- exports.getNowString = () => getTimeString(new Date());
21
+ const getNowString = exports.getNowString = () => getTimeString(new Date());
22
+ // Returns a string representing an ISO date.
23
+ const getNowDate = exports.getNowDate = () => getNowString().slice(0, 10);
24
+ // Returns a string representing a date with slashes.
25
+ exports.getNowDateSlash = () => getNowDate().replace(/-/g, '/');
22
26
  // Returns a time stamp representing a date and time.
23
27
  const getTimeStamp
24
28
  = exports.getTimeStamp
@@ -0,0 +1,115 @@
1
+ *:focus {
2
+ outline: solid 0.2rem #00f;
3
+ outline-offset: 0.2rem;
4
+ }
5
+ body {
6
+ margin: 2rem;
7
+ font-size: large;
8
+ }
9
+ .bold, .componentID {
10
+ font-weight: 700;
11
+ }
12
+ button {
13
+ min-height: 44px;
14
+ min-width: 44px;
15
+ padding: 0.5rem;
16
+ border: solid 0.2rem #03a;
17
+ border-radius: 1rem;
18
+ font-size: inherit;
19
+ font-weight: 700;
20
+ color: #fff;
21
+ background-color: #03a;
22
+ }
23
+ button:hover {
24
+ color: #000;
25
+ background-color: #9df;
26
+ }
27
+ caption {
28
+ margin: 1rem 0;
29
+ font-weight: 700;
30
+ font-style: italic;
31
+ }
32
+ code {
33
+ font-family: sans-serif;
34
+ color: #832703;
35
+ }
36
+ .error {
37
+ font-weight: 700;
38
+ color: #c00;
39
+ }
40
+ fieldset {
41
+ width: max-content;
42
+ margin-top: 1rem;
43
+ }
44
+ .firstCellRight td:nth-child(1) {
45
+ text-align: right;
46
+ }
47
+ form {
48
+ margin-top: 1rem;
49
+ }
50
+ h1, h2, h3, h4, h5, h6 {
51
+ margin-bottom: 0;
52
+ }
53
+ h2 {
54
+ font-size: 1.8rem;
55
+ margin: 1rem auto 0.5rem auto;
56
+ color: #006666;
57
+ }
58
+ h3 {
59
+ font-size: 1.6rem;
60
+ }
61
+ h3.bars {
62
+ border-top: solid black 0.1rem;
63
+ padding-top: 0.3rem;
64
+ }
65
+ h4 {
66
+ font-size: 1.4rem;
67
+ }
68
+ h5 {
69
+ font-size: 1.2rem;
70
+ }
71
+ h6 {
72
+ font-size: 1rem;
73
+ }
74
+ input {
75
+ margin: 0.5rem 0;
76
+ font-size: inherit;
77
+ }
78
+ legend {
79
+ font-weight: 700;
80
+ }
81
+ pre {
82
+ white-space: break-spaces;
83
+ }
84
+ .secondCellRight td:nth-child(2) {
85
+ text-align: right;
86
+ }
87
+ section:not(.wide) {
88
+ max-width: 36rem;
89
+ }
90
+ #synopsis > p {
91
+ margin: 0;
92
+ padding: 0;
93
+ }
94
+ table {
95
+ border-collapse: collapse;
96
+ }
97
+ table.allBorder th {
98
+ padding-bottom: 0;
99
+ }
100
+ table.allBorder th, table.allBorder td, tbody td {
101
+ border: 1px gray solid;
102
+ }
103
+ tbody th {
104
+ padding-right: 1rem;
105
+ text-align: right;
106
+ }
107
+ td, th {
108
+ padding: 0 0.5rem;
109
+ }
110
+ thead th {
111
+ padding-bottom: 1rem;
112
+ }
113
+ .thirdCellRight td:nth-child(3) {
114
+ text-align: right;
115
+ }
package/summarize.js CHANGED
@@ -8,7 +8,7 @@
8
8
  // Module to keep secrets.
9
9
  require('dotenv').config();
10
10
  // Module to perform common operations.
11
- const {getNowStamp} = require('./procs/util');
11
+ const {getFileID} = require('./procs/util');
12
12
 
13
13
  // ########## FUNCTIONS
14
14
 
@@ -27,8 +27,8 @@ exports.summarize = (what, reports) => {
27
27
  };
28
28
  });
29
29
  const summary = {
30
+ id: getFileID(2),
30
31
  what,
31
- timeStamp: getNowStamp(),
32
32
  data
33
33
  };
34
34
  return summary;
package/track.js ADDED
@@ -0,0 +1,23 @@
1
+ /*
2
+ track.js
3
+ Creates tracking reports from report summaries.
4
+ Arguments:
5
+ 0. Tracking function.
6
+ 1. Summary.
7
+ */
8
+
9
+ // ########## IMPORTS
10
+
11
+ // Module to perform common operations.
12
+ const {getFileID} = require('./procs/util');
13
+
14
+ // ########## FUNCTIONS
15
+
16
+ // Creates and returns a tracking report from a summary.
17
+ exports.track = async (tracker, summary) => {
18
+ // Use the tracker to create a tracking report.
19
+ const id = getFileID(2);
20
+ const trackingReport = await tracker(id, summary);
21
+ console.log(`Tracking report ${id} created`);
22
+ return [id, trackingReport];
23
+ };