testilo 41.0.5 → 41.2.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
@@ -669,10 +669,9 @@ To test the `rescore` module, in the project directory you can execute the state
669
669
 
670
670
  Reports from Testaro are JavaScript objects. When represented as JSON, they are human-readable, but not human-friendly. They are basically designed for machine tractability. This is equally true for reports that have been scored by Testilo. But Testilo can _digest_ a scored report, converting it to a human-oriented HTML document, or _digest_.
671
671
 
672
- The `digest` module digests a scored report. Its `digest()` function takes three arguments:
672
+ The `digest` module digests a scored report. Its `digest()` function takes two arguments:
673
673
  - a digester (a digesting function)
674
674
  - a scored report object
675
- - the URL of a directory containing the scored reports
676
675
 
677
676
  The digester populates an HTML digest template. A copy of the template, with its placeholders replaced by computed values, becomes the digest. The digester defines the rules for replacing the placeholders with values. The Testilo package contains a `procs/digest` directory, in which there are subdirectories, each containing a template and a module that exports a digester. You can use one of those modules, or you can create your own.
678
677
 
@@ -717,9 +716,9 @@ When a user invokes `digest()` in this example, the `call` module:
717
716
  - writes the digested reports to the `digested` subdirectory of the `REPORTDIR` directory.
718
717
  - includes in each digest a link to the scored report, with the link destination being based on `SCORED_REPORT_URL`.
719
718
 
720
- The included digesters create digests that have links. The server that serves such a digest must also respond correctly when a user activates one of the links. One link is to the scored report. Other links are to collections of standard instances of violations of particular rules from the scored report.
719
+ The included digesters create digests that have links. The server that serves such a digest must also respond correctly when a user activates one of the links. One link is to the scored report. Other links are to World Wide Web Consortium documents on WCAG principles, guidelines, and success criteria.
721
720
 
722
- The digests created by `digest()` are HTML files, and they expect a `style.css` file to exist in their directory. The `reports/digested/style.css` file in Testilo is an appropriate stylesheet to be copied into the directory where digested reports are written.
721
+ The digests created by `digest()` are HTML files, and they expect a `style.css` file to exist in their directory. If you use an included digester, the `reports/digested/style.css` file in Testilo is an appropriate stylesheet to be copied into the directory where digested reports are written.
723
722
 
724
723
  ### Difgesting
725
724
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "testilo",
3
- "version": "41.0.5",
3
+ "version": "41.2.0",
4
4
  "description": "Prepares Testaro jobs and processes Testaro reports",
5
5
  "main": "call.js",
6
6
  "scripts": {
@@ -11,6 +11,43 @@
11
11
  <title>Accessibility digest</title>
12
12
  <link rel="icon" href="favicon.ico">
13
13
  <link rel="stylesheet" href="style.css">
14
+ <script id="script" type="module">
15
+ const sortButton = document.getElementById('sortButton');
16
+ const sortChangeSpan = document.getElementById('sortChange');
17
+ const sumBody = document.getElementById('sumBody');
18
+ const rows = Array.from(sumBody.children);
19
+ const sortRowsBy = basis => {
20
+ if (basis === 'wcag') {
21
+ rows.sort((a, b) => {
22
+ const sorters = [a, b].map(row => {
23
+ const wcagParts = row.children[1].textContent.split('.');
24
+ const wcagNums = wcagParts.map(part => Number.parseInt(part, 10));
25
+ return 100 * (wcagNums[0] || 0) + 20 * (wcagNums[1] || 0) + (wcagNums[2] || 0);
26
+ });
27
+ return sorters[0] - sorters[1];
28
+ });
29
+ }
30
+ else if (basis === 'score') {
31
+ rows.sort((a, b) => {
32
+ const sorters = [a, b].map(row => Number.parseInt(row.children[2].textContent));
33
+ return sorters[1] - sorters[0];
34
+ });
35
+ }
36
+ sumBody.textContent = '';
37
+ rows.forEach(row => {
38
+ sumBody.appendChild(row);
39
+ });
40
+ };
41
+ sortButton.addEventListener('click', event => {
42
+ // Add the new sorting basis to the page.
43
+ sortChangeSpan.textContent = sortChangeSpan.textContent === 'score to WCAG'
44
+ ? 'WCAG to score'
45
+ : 'score to WCAG';
46
+ const newBasis = sortChangeSpan.textContent === 'score to WCAG' ? 'score' : 'wcag';
47
+ // Re-sort the table.
48
+ sortRowsBy(newBasis);
49
+ });
50
+ </script>
14
51
  </head>
15
52
  <body>
16
53
  <main>
@@ -53,14 +90,23 @@
53
90
  <p>This digest can help answer that question. Ten different tools (Alfa, ASLint, Axe, Editoria11y, Equal Access, HTML CodeSniffer, Nu Html Checker, QualWeb, Testaro, and WAVE) tested the page to check its compliance with their accessibility rules. In all, the tools define about 990 rules, which are classified here into about 310 accessibility issues.</p>
54
91
  <p>The results were interpreted to yield a score, with 0 being ideal. The score for this page was __total__, the sum of __issueCount__ for the count of issues, __issue__ for specific issues, __solo__ for unclassified rule violations, __tool__ for tool-by-tool ratings, __element__ for the count of violating elements, __prevention__ for the page preventing tools from running, __log__ for browser warnings, and __latency__ for delayed page responses.</p>
55
92
  <h2 id="summary">Issue summary</h2>
56
- <p>This table shows the numbers of rule violations (<q>instances</q>) reported by one or more tools, classified by issue. When an instance count is 0, that means the tool has a rule belonging to the issue but reported no violations of that rule.</p>
57
- <p>Tools do not always agree on instance counts. Disagreements may be due to non-equivalent rules or invalid tests. You can inspect the <a href="__reportURL__">full report</a> to diagnose differences.</p>
93
+ <h3>Details about this summary</h3>
94
+ <ul>
95
+ <li>This table shows the numbers of rule violations (<q>instances</q>) reported by one or more tools, classified by issue.</li>
96
+ <li>Tools often disagree on instance counts, because of non-equivalent rules or invalid tests. You can inspect the <a href="__reportURL__">full report</a> to diagnose differences.</li>
97
+ <li>The <q>WCAG</q> value is the principle, guideline, or success criterion of the <a href="https://www.w3.org/TR/WCAG22/">Web Content Accessibility Guidelines</a> most relevant to the issue.</li>
98
+ <li>The <q>Score</q> value is the contribution of the issue to the page score.</li>
99
+ <li>An instance count of 0 means the tool has a rule belonging to the issue but reported no violations of that rule, although at least one tool reported at least one violation.</li>
100
+ <li>You can sort this table by WCAG or score.</li>
101
+ </ul>
102
+ <h3>The summary</h3>
103
+ <p><button id="sortButton" type="button">Change sorting from <span id="sortChange">score to WCAG</span></button></p>
58
104
  <table class="allBorder thirdCellRight">
59
105
  <caption>How many violations each tool reported, by issue</caption>
60
106
  <thead>
61
107
  <tr><th>Issue</th><th>WCAG</th><th>Score</th><th>Instance counts</th></tr>
62
108
  </thead>
63
- <tbody class="headersLeft">
109
+ <tbody id="sumBody" class="headersLeft">
64
110
  __issueRows__
65
111
  </tbody>
66
112
  </table>
@@ -36,27 +36,75 @@ const {getNowDate, getNowDateSlash} = require('../../util');
36
36
  // CONSTANTS
37
37
 
38
38
  // Digester ID.
39
- const digesterID = 'tdp43e';
39
+ const digesterID = 'tdp45';
40
40
  // Newline with indentations.
41
41
  const innerJoiner = '\n ';
42
42
  const outerJoiner = '\n ';
43
+ // Directory of WCAG links.
44
+ const wcagPhrases = {};
43
45
 
44
46
  // FUNCTIONS
45
47
 
46
48
  // Gets a row of the score-summary table.
47
49
  const getScoreRow = (componentName, score) => `<tr><th>${componentName}</th><td>${score}</td></tr>`;
50
+ // Gets a WCAG link or, if not obtainable, a numeric identifier.
51
+ const getWCAGTerm = wcag => {
52
+ const wcagPhrase = wcagPhrases[wcag];
53
+ const wcagTerm = wcagPhrase
54
+ ? `<a href="https://www.w3.org/WAI/WCAG22/Understanding/${wcagPhrase}.html">${wcag}</a>`
55
+ : wcag;
56
+ return wcagTerm;
57
+ };
48
58
  // Gets a row of the issue-score-summary table.
49
59
  const getIssueScoreRow = (issueConstants, issueDetails) => {
50
60
  const {summary, wcag} = issueConstants;
61
+ const wcagTerm = getWCAGTerm(wcag);
51
62
  const {instanceCounts, score} = issueDetails;
52
63
  const toolList = Object
53
64
  .keys(instanceCounts)
54
65
  .map(tool => `<code>${tool}</code>:${instanceCounts[tool]}`)
55
66
  .join(', ');
56
- return `<tr><th>${summary}</th><td class="center">${wcag}<td class="right num">${score}</td><td>${toolList}</td></tr>`;
67
+ return `<tr><th>${summary}</th><td class="center">${wcagTerm}<td class="right num">${score}</td><td>${toolList}</td></tr>`;
68
+ };
69
+ // Populates the directory of WCAG understanding verbal IDs.
70
+ const getWCAGPhrases = async () => {
71
+ // Get the copy of file https://raw.githubusercontent.com/w3c/wcag/main/guidelines/wcag.json.
72
+ const wcagJSON = await fs.readFile(`${__dirname}/../../../wcag.json`, 'utf8');
73
+ const wcag = JSON.parse(wcagJSON);
74
+ const {principles} = wcag;
75
+ // For each principle in it:
76
+ principles.forEach(principle => {
77
+ // If it is usable:
78
+ if (principle.num && principle.id && principle.id.startsWith('WCAG2:')) {
79
+ // Add it to the directory.
80
+ wcagPhrases[principle.num] = principle.id.slice(6);
81
+ const {guidelines} = principle;
82
+ // For each guideline in the principle:
83
+ guidelines.forEach(guideline => {
84
+ // If it is usable:
85
+ if (guideline.num && guideline.id && guideline.id.startsWith('WCAG2:')) {
86
+ // Add it to the directory.
87
+ wcagPhrases[guideline.num] = guideline.id.slice(6);
88
+ const {successcriteria} = guideline;
89
+ // For each success criterion in the guideline:
90
+ successcriteria.forEach(successCriterion => {
91
+ // If it is usable:
92
+ if (
93
+ successCriterion.num
94
+ && successCriterion.id
95
+ && successCriterion.id.startsWith('WCAG2:')
96
+ ) {
97
+ // Add it to the directory.
98
+ wcagPhrases[successCriterion.num] = successCriterion.id.slice(6);
99
+ }
100
+ });
101
+ }
102
+ });
103
+ }
104
+ });
57
105
  };
58
106
  // Adds parameters to a query for a digest.
59
- const populateQuery = (report, query) => {
107
+ const populateQuery = async (report, query) => {
60
108
  const {
61
109
  browserID, device, id, isolate, lowMotion, score, sources, standard, strict, target
62
110
  } = report;
@@ -80,6 +128,8 @@ const populateQuery = (report, query) => {
80
128
  query.browser = browserID;
81
129
  query.agent = agent ? ` on agent ${agent}` : '';
82
130
  query.reportURL = process.env.SCORED_REPORT_URL.replace('__id__', id);
131
+ // Populate the WCAG phrase directory.
132
+ await getWCAGPhrases();
83
133
  // Add values for the score-summary table to the query.
84
134
  const rows = {
85
135
  summaryRows: [],
@@ -112,7 +162,9 @@ const populateQuery = (report, query) => {
112
162
  const issueSummary = issues[issueID].summary;
113
163
  issueDetailRows.push(`<h3 class="bars">Issue: ${issueSummary}</h3>`);
114
164
  issueDetailRows.push(`<p>Impact: ${issues[issueID].why || 'N/A'}</p>`);
115
- issueDetailRows.push(`<p>WCAG: ${issues[issueID].wcag || 'N/A'}</p>`);
165
+ const wcag = issues[issueID].wcag;
166
+ const wcagTerm = wcag ? getWCAGTerm(wcag) : 'N/A';
167
+ issueDetailRows.push(`<p>WCAG: ${wcagTerm}</p>`);
116
168
  const issueData = details.issue[issueID];
117
169
  issueDetailRows.push(`<p>Score: ${issueData.score}</p>`);
118
170
  issueDetailRows.push('<h4>Elements</h4>');
@@ -176,7 +228,7 @@ const populateQuery = (report, query) => {
176
228
  exports.digester = async report => {
177
229
  // Create a query to replace placeholders.
178
230
  const query = {};
179
- populateQuery(report, query);
231
+ await populateQuery(report, query);
180
232
  // Get the template.
181
233
  let template = await fs.readFile(`${__dirname}/index.html`, 'utf8');
182
234
  // Replace its placeholders.
@@ -11,11 +11,55 @@
11
11
  <title>Accessibility digest</title>
12
12
  <link rel="icon" href="favicon.ico">
13
13
  <link rel="stylesheet" href="style.css">
14
+ <script id="script" type="module">
15
+ const sortButton = document.getElementById('sortButton');
16
+ const sortChangeSpan = document.getElementById('sortChange');
17
+ const sumBody = document.getElementById('sumBody');
18
+ const rows = Array.from(sumBody.children);
19
+ const sortRowsBy = basis => {
20
+ if (basis === 'wcag') {
21
+ rows.sort((a, b) => {
22
+ const sorters = [a, b].map(row => {
23
+ const wcagParts = row.children[1].textContent.split('.');
24
+ const wcagNums = wcagParts.map(part => Number.parseInt(part, 10));
25
+ return 100 * (wcagNums[0] || 0) + 20 * (wcagNums[1] || 0) + (wcagNums[2] || 0);
26
+ });
27
+ return sorters[0] - sorters[1];
28
+ });
29
+ }
30
+ else if (basis === 'score') {
31
+ rows.sort((a, b) => {
32
+ const sorters = [a, b].map(row => Number.parseInt(row.children[2].textContent));
33
+ return sorters[1] - sorters[0];
34
+ });
35
+ }
36
+ sumBody.textContent = '';
37
+ rows.forEach(row => {
38
+ sumBody.appendChild(row);
39
+ });
40
+ };
41
+ sortButton.addEventListener('click', event => {
42
+ // Add the new sorting basis to the page.
43
+ sortChangeSpan.textContent = sortChangeSpan.textContent === 'score to WCAG'
44
+ ? 'WCAG to score'
45
+ : 'score to WCAG';
46
+ const newBasis = sortChangeSpan.textContent === 'score to WCAG' ? 'score' : 'wcag';
47
+ // Re-sort the table.
48
+ sortRowsBy(newBasis);
49
+ });
50
+ </script>
14
51
  </head>
15
52
  <body>
16
53
  <main>
17
54
  <header>
18
55
  <h1>Accessibility digest</h1>
56
+ <h2>Contents</h2>
57
+ <ul>
58
+ <li><a href="#intro">Introduction</a></li>
59
+ <li><a href="#summary">Issue summary</a></li>
60
+ <li><a href="#itemization">Itemized issues</a></li>
61
+ <li><a href="#elements">Elements with issues</a></li>
62
+ </ul>
19
63
  <table class="allBorder">
20
64
  <caption>Synopsis</caption>
21
65
  <tr><th>Page</th><td>__org__</td></tr>
@@ -41,28 +85,37 @@
41
85
  </tr>
42
86
  </table>
43
87
  </header>
44
- <h2>Introduction</h2>
88
+ <h2 id="intro">Introduction</h2>
45
89
  <p>How <a href="https://www.w3.org/WAI/">accessible</a> is the __org__ web page at <a href="__url__"><code>__url__</code></a>?</p>
46
90
  <p>This digest can help answer that question. Ten different tools (Alfa, ASLint, Axe, Editoria11y, Equal Access, HTML CodeSniffer, Nu Html Checker, QualWeb, Testaro, and WAVE) tested the page to check its compliance with their accessibility rules. In all, the tools define about 990 rules, which are classified here into about 310 accessibility issues.</p>
47
91
  <p>The results were interpreted to yield a score, with 0 being ideal. The score for this page was __total__, the sum of __issueCount__ for the count of issues, __issue__ for specific issues, __solo__ for unclassified rule violations, __tool__ for tool-by-tool ratings, __element__ for the count of violating elements, __prevention__ for the page preventing tools from running, __log__ for browser warnings, and __latency__ for delayed page responses.</p>
48
- <h2>Issue summary</h2>
49
- <p>This table shows the issues discovered by one or more tools. When an instance count is 0, that means the tool has a test for the issue but the page passed that test.</p>
50
- <p>Why do instance counts differ among tools? Possibly because the tests are not really equivalent, or a test is unreliable. You can inspect the <a href="__reportURL__">full report</a> to diagnose differences.</p>
92
+ <h2 id="summary">Issue summary</h2>
93
+ <h3>Details about this summary</h3>
94
+ <ul>
95
+ <li>This table shows the numbers of rule violations (<q>instances</q>) reported by one or more tools, classified by issue.</li>
96
+ <li>Tools often disagree on instance counts, because of non-equivalent rules or invalid tests. You can inspect the <a href="__reportURL__">full report</a> to diagnose differences.</li>
97
+ <li>The <q>WCAG</q> value is the principle, guideline, or success criterion of the <a href="https://www.w3.org/TR/WCAG22/">Web Content Accessibility Guidelines</a> most relevant to the issue.</li>
98
+ <li>The <q>Score</q> value is the contribution of the issue to the page score.</li>
99
+ <li>An instance count of 0 means the tool has a rule belonging to the issue but reported no violations of that rule, although at least one tool reported at least one violation.</li>
100
+ <li>You can sort this table by WCAG or score.</li>
101
+ </ul>
102
+ <h3>The summary</h3>
103
+ <p><button id="sortButton" type="button">Change sorting from <span id="sortChange">score to WCAG</span></button></p>
51
104
  <table class="allBorder thirdCellRight">
52
- <caption>Summary of issues</caption>
105
+ <caption>How many violations each tool reported, by issue</caption>
53
106
  <thead>
54
107
  <tr><th>Issue</th><th>WCAG</th><th>Score</th><th>Instance counts</th></tr>
55
108
  </thead>
56
- <tbody class="headersLeft">
109
+ <tbody id="sumBody" class="headersLeft">
57
110
  __issueRows__
58
111
  </tbody>
59
112
  </table>
60
- <h2>Itemized issues</h2>
113
+ <h2 id="itemization">Itemized issues</h2>
61
114
  <p>The reported rule violations are itemized below, issue by issue. Additional details can be inspected in the <a href="__reportURL__">full report</a>.</p>
62
115
  __issueDetailRows__
63
- <h2>Multi-issue elements</h2>
64
- <p>Elements exhibiting more than 1 issue:</p>
65
- __multiIssueElementRows__
116
+ <h2 id="elements">Elements with issues</h2>
117
+ <p>Elements exhibiting issues:</p>
118
+ __elementRows__
66
119
  <footer>
67
120
  <p class="date">Produced <time itemprop="datePublished" datetime="__dateISO__">__dateSlash__</time></p>
68
121
  </footer>
@@ -36,27 +36,75 @@ const {getNowDate, getNowDateSlash} = require('../../util');
36
36
  // CONSTANTS
37
37
 
38
38
  // Digester ID.
39
- const digesterID = 'tdp44';
39
+ const digesterID = 'tdp45';
40
40
  // Newline with indentations.
41
41
  const innerJoiner = '\n ';
42
42
  const outerJoiner = '\n ';
43
+ // Directory of WCAG links.
44
+ const wcagPhrases = {};
43
45
 
44
46
  // FUNCTIONS
45
47
 
46
48
  // Gets a row of the score-summary table.
47
49
  const getScoreRow = (componentName, score) => `<tr><th>${componentName}</th><td>${score}</td></tr>`;
50
+ // Gets a WCAG link or, if not obtainable, a numeric identifier.
51
+ const getWCAGTerm = wcag => {
52
+ const wcagPhrase = wcagPhrases[wcag];
53
+ const wcagTerm = wcagPhrase
54
+ ? `<a href="https://www.w3.org/WAI/WCAG22/Understanding/${wcagPhrase}.html">${wcag}</a>`
55
+ : wcag;
56
+ return wcagTerm;
57
+ };
48
58
  // Gets a row of the issue-score-summary table.
49
59
  const getIssueScoreRow = (issueConstants, issueDetails) => {
50
60
  const {summary, wcag} = issueConstants;
61
+ const wcagTerm = getWCAGTerm(wcag);
51
62
  const {instanceCounts, score} = issueDetails;
52
63
  const toolList = Object
53
64
  .keys(instanceCounts)
54
65
  .map(tool => `<code>${tool}</code>:${instanceCounts[tool]}`)
55
66
  .join(', ');
56
- return `<tr><th>${summary}</th><td class="center">${wcag}<td class="right num">${score}</td><td>${toolList}</td></tr>`;
67
+ return `<tr><th>${summary}</th><td class="center">${wcagTerm}<td class="right num">${score}</td><td>${toolList}</td></tr>`;
68
+ };
69
+ // Populates the directory of WCAG understanding verbal IDs.
70
+ const getWCAGPhrases = async () => {
71
+ // Get the copy of file https://raw.githubusercontent.com/w3c/wcag/main/guidelines/wcag.json.
72
+ const wcagJSON = await fs.readFile(`${__dirname}/../../../wcag.json`, 'utf8');
73
+ const wcag = JSON.parse(wcagJSON);
74
+ const {principles} = wcag;
75
+ // For each principle in it:
76
+ principles.forEach(principle => {
77
+ // If it is usable:
78
+ if (principle.num && principle.id && principle.id.startsWith('WCAG2:')) {
79
+ // Add it to the directory.
80
+ wcagPhrases[principle.num] = principle.id.slice(6);
81
+ const {guidelines} = principle;
82
+ // For each guideline in the principle:
83
+ guidelines.forEach(guideline => {
84
+ // If it is usable:
85
+ if (guideline.num && guideline.id && guideline.id.startsWith('WCAG2:')) {
86
+ // Add it to the directory.
87
+ wcagPhrases[guideline.num] = guideline.id.slice(6);
88
+ const {successcriteria} = guideline;
89
+ // For each success criterion in the guideline:
90
+ successcriteria.forEach(successCriterion => {
91
+ // If it is usable:
92
+ if (
93
+ successCriterion.num
94
+ && successCriterion.id
95
+ && successCriterion.id.startsWith('WCAG2:')
96
+ ) {
97
+ // Add it to the directory.
98
+ wcagPhrases[successCriterion.num] = successCriterion.id.slice(6);
99
+ }
100
+ });
101
+ }
102
+ });
103
+ }
104
+ });
57
105
  };
58
106
  // Adds parameters to a query for a digest.
59
- const populateQuery = (report, query) => {
107
+ const populateQuery = async (report, query) => {
60
108
  const {
61
109
  browserID, device, id, isolate, lowMotion, score, sources, standard, strict, target
62
110
  } = report;
@@ -80,6 +128,8 @@ const populateQuery = (report, query) => {
80
128
  query.browser = browserID;
81
129
  query.agent = agent ? ` on agent ${agent}` : '';
82
130
  query.reportURL = process.env.SCORED_REPORT_URL.replace('__id__', id);
131
+ // Populate the WCAG phrase directory.
132
+ await getWCAGPhrases();
83
133
  // Add values for the score-summary table to the query.
84
134
  const rows = {
85
135
  summaryRows: [],
@@ -112,7 +162,9 @@ const populateQuery = (report, query) => {
112
162
  const issueSummary = issues[issueID].summary;
113
163
  issueDetailRows.push(`<h3 class="bars">Issue: ${issueSummary}</h3>`);
114
164
  issueDetailRows.push(`<p>Impact: ${issues[issueID].why || 'N/A'}</p>`);
115
- issueDetailRows.push(`<p>WCAG: ${issues[issueID].wcag || 'N/A'}</p>`);
165
+ const wcag = issues[issueID].wcag;
166
+ const wcagTerm = wcag ? getWCAGTerm(wcag) : 'N/A';
167
+ issueDetailRows.push(`<p>WCAG: ${wcagTerm}</p>`);
116
168
  const issueData = details.issue[issueID];
117
169
  issueDetailRows.push(`<p>Score: ${issueData.score}</p>`);
118
170
  issueDetailRows.push('<h4>Elements</h4>');
@@ -148,8 +200,8 @@ const populateQuery = (report, query) => {
148
200
  });
149
201
  });
150
202
  query.issueDetailRows = issueDetailRows.join(outerJoiner);
151
- // Add paragraphs about the multi-issue elements to the query.
152
- const multiIssueElementRows = [];
203
+ // Add paragraphs about the elements to the query.
204
+ const elementRows = [];
153
205
  const issueElements = {};
154
206
  Object.keys(details.element).forEach(issueID => {
155
207
  const pathIDs = details.element[issueID];
@@ -161,8 +213,8 @@ const populateQuery = (report, query) => {
161
213
  const sortedPathIDs = Object.keys(issueElements).sort();
162
214
  sortedPathIDs.forEach(pathID => {
163
215
  const elementIssues = issueElements[pathID];
164
- if (elementIssues && elementIssues.length > 1) {
165
- multiIssueElementRows.push(
216
+ if (elementIssues) {
217
+ elementRows.push(
166
218
  `<h5>Element <code>${pathID}</code></h5>`,
167
219
  '<ul>',
168
220
  ... elementIssues.map(issueID => ` <li>${issues[issueID].summary}</li>`).sort(),
@@ -170,13 +222,13 @@ const populateQuery = (report, query) => {
170
222
  );
171
223
  }
172
224
  });
173
- query.multiIssueElementRows = multiIssueElementRows.join(outerJoiner);
225
+ query.elementRows = elementRows.join(outerJoiner);
174
226
  };
175
227
  // Returns a digested report.
176
228
  exports.digester = async report => {
177
229
  // Create a query to replace placeholders.
178
230
  const query = {};
179
- populateQuery(report, query);
231
+ await populateQuery(report, query);
180
232
  // Get the template.
181
233
  let template = await fs.readFile(`${__dirname}/index.html`, 'utf8');
182
234
  // Replace its placeholders.