testilo 3.6.1 → 3.6.4
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/compare.js +3 -3
- package/digest.js +9 -6
- package/package.json +1 -1
- package/procs/compare/cp12a/index.html +47 -0
- package/procs/compare/cp12a/index.js +71 -0
- package/procs/digest/dp11a/index.js +15 -15
- package/procs/digest/dp12a/index.html +78 -0
- package/procs/digest/dp12a/index.js +126 -0
- package/procs/score/sp12a.js +426 -35
- /package/scoring/data/packageRules/{aatt.js → htmlcs.js} +0 -0
package/compare.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/*
|
|
2
2
|
compare.js
|
|
3
3
|
Testilo comparison script.
|
|
4
|
-
Usage example: node compare
|
|
4
|
+
Usage example: node compare cp12a candidates
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
// ########## IMPORTS
|
|
@@ -14,8 +14,8 @@ const fs = require('fs/promises');
|
|
|
14
14
|
// ########## CONSTANTS
|
|
15
15
|
|
|
16
16
|
const comparisonDir = process.env.COMPARISONDIR || 'reports/comparative';
|
|
17
|
-
const
|
|
18
|
-
const
|
|
17
|
+
const compareProcID = process.argv[2];
|
|
18
|
+
const comparisonNameBase = process.argv[3];
|
|
19
19
|
|
|
20
20
|
// ########## FUNCTIONS
|
|
21
21
|
|
package/digest.js
CHANGED
|
@@ -15,8 +15,8 @@ const fs = require('fs/promises');
|
|
|
15
15
|
|
|
16
16
|
const reportDirScored = process.env.REPORTDIR_SCORED || 'reports/scored';
|
|
17
17
|
const reportDirDigested = process.env.REPORTDIR_DIGESTED || 'reports/digested';
|
|
18
|
-
const
|
|
19
|
-
const
|
|
18
|
+
const digesterID = process.argv[2];
|
|
19
|
+
const reportIDStart = process.argv[3];
|
|
20
20
|
|
|
21
21
|
// ########## FUNCTIONS
|
|
22
22
|
|
|
@@ -26,11 +26,14 @@ const replaceHolders = (content, query) => content
|
|
|
26
26
|
// Creates a digest.
|
|
27
27
|
const digest = async () => {
|
|
28
28
|
const reportDirScoredAbs = `${__dirname}/${reportDirScored}`;
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
29
|
+
let reportFileNames = await fs.readdir(reportDirScoredAbs);
|
|
30
|
+
reportFileNames = reportFileNames.filter(fileName => fileName.endsWith('.json'));
|
|
31
|
+
if (reportIDStart) {
|
|
32
|
+
reportFileNames = reportFileNames
|
|
33
|
+
.filter(fileName => fileName.startsWith(reportIDStart));
|
|
34
|
+
}
|
|
32
35
|
const {makeQuery} = require(`${__dirname}/procs/digest/${digesterID}/index.js`);
|
|
33
|
-
for (const fileName of
|
|
36
|
+
for (const fileName of reportFileNames) {
|
|
34
37
|
const reportJSON = await fs.readFile(`${reportDirScoredAbs}/${fileName}`, 'utf8');
|
|
35
38
|
const report = JSON.parse(reportJSON);
|
|
36
39
|
const query = {};
|
package/package.json
CHANGED
|
@@ -0,0 +1,47 @@
|
|
|
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="comparison of accessibility scores">
|
|
10
|
+
<meta name="keywords" content="accessibility a11y web testing">
|
|
11
|
+
<title>Accessibility score comparison</title>
|
|
12
|
+
<link rel="icon" href="favicon.png">
|
|
13
|
+
<link rel="stylesheet" href="style.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main>
|
|
17
|
+
<header>
|
|
18
|
+
<h1>Accessibility score comparison</h1>
|
|
19
|
+
</header>
|
|
20
|
+
<h2>Introduction</h2>
|
|
21
|
+
<p>The table below compares __pageCount__ web pages on <a href="https://www.w3.org/WAI/fundamentals/accessibility-intro/">accessibility</a>. The page names link to the pages on the web. The scores link to digests that explain in detail how the scores were computed.</p>
|
|
22
|
+
<p>The pages were:</p>
|
|
23
|
+
<ol id="summary">
|
|
24
|
+
<li>Tested by <a href="https://www.npmjs.com/package/testaro">Testaro</a> with procedure <code>tp12</code></li>
|
|
25
|
+
<li>Scored by <a href="https://www.npmjs.com/package/testilo">Testilo</a> with procedure <code>sp12a</code></li>
|
|
26
|
+
<li>Digested by Testilo with procedure <code>dp12a</code></li>
|
|
27
|
+
<li>Compared by Testilo with procedure <code>cp12a</code></li>
|
|
28
|
+
</ol>
|
|
29
|
+
<p>The Testaro procedure performs 808 tests on each page. Of these, 16 tests are custom tests defined by Testaro, and the others belong to packages of tests created by others.</p>
|
|
30
|
+
<h2>Comparison</h2>
|
|
31
|
+
<table class="allBorder">
|
|
32
|
+
<caption>Accessibility scores of web pages</caption>
|
|
33
|
+
<thead>
|
|
34
|
+
<tr><th scope="col">Page</th><th scope="col" colspan="2">Score (lower is better)</tr>
|
|
35
|
+
</thead>
|
|
36
|
+
<tbody class="linkSmaller secondCellRight">
|
|
37
|
+
__tableBody__
|
|
38
|
+
</tbody>
|
|
39
|
+
</table>
|
|
40
|
+
<h2>Discussion</h2>
|
|
41
|
+
<p>Tests and scoring formulae are fallible and subjective. The reported faults merit investigation as potential opportunities for improved accessibility. But some may not actually harm accessibility, and some other accessibility faults are not detectable with these tests. Different reasonable procedures could yield different test results and different scores. Testaro and Testilo can be customized to fit different definitions and weightings of types of accessibility.</p>
|
|
42
|
+
<footer>
|
|
43
|
+
<p class="date">Produced <time itemprop="datePublished" datetime="__dateISO__">__dateSlash__</time></p>
|
|
44
|
+
</footer>
|
|
45
|
+
</main>
|
|
46
|
+
</body>
|
|
47
|
+
</html>
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/*
|
|
2
|
+
cp12a.js
|
|
3
|
+
Returns a query for an HTML page including a bar-graph table.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
// ########## IMPORTS
|
|
7
|
+
|
|
8
|
+
// Module to keep secrets local.
|
|
9
|
+
require('dotenv').config();
|
|
10
|
+
// Module to access files.
|
|
11
|
+
const fs = require('fs/promises');
|
|
12
|
+
|
|
13
|
+
// ########## CONSTANTS
|
|
14
|
+
|
|
15
|
+
const reportDirScored = process.env.REPORTDIR_SCORED || 'reports/scored';
|
|
16
|
+
const query = {};
|
|
17
|
+
|
|
18
|
+
// ########## FUNCTIONS
|
|
19
|
+
|
|
20
|
+
// Returns data on the hosts in the report directory.
|
|
21
|
+
const getData = async () => {
|
|
22
|
+
const reportDirAbs = `${__dirname}/../../../${reportDirScored}`;
|
|
23
|
+
const reportFileNamesAll = await fs.readdir(reportDirAbs);
|
|
24
|
+
const reportFileNamesSource = reportFileNamesAll.filter(fileName => fileName.endsWith('.json'));
|
|
25
|
+
const pageCount = reportFileNamesSource.length;
|
|
26
|
+
const bodyData = [];
|
|
27
|
+
for (const fileName of reportFileNamesSource) {
|
|
28
|
+
const fileJSON = await fs.readFile(`${reportDirAbs}/${fileName}`, 'utf8');
|
|
29
|
+
const file = JSON.parse(fileJSON);
|
|
30
|
+
const {id, host, score} = file;
|
|
31
|
+
bodyData.push({
|
|
32
|
+
id,
|
|
33
|
+
org: host.what,
|
|
34
|
+
url: host.which,
|
|
35
|
+
score: score.summary.total
|
|
36
|
+
});
|
|
37
|
+
};
|
|
38
|
+
return {
|
|
39
|
+
pageCount,
|
|
40
|
+
bodyData
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
// Returns the maximum score.
|
|
44
|
+
const getMaxScore = tableData => tableData.reduce((max, item) => Math.max(max, item.score), 0);
|
|
45
|
+
// Converts report data to a table body.
|
|
46
|
+
const getTableBody = async bodyData => {
|
|
47
|
+
const maxScore = getMaxScore(bodyData);
|
|
48
|
+
const rows = bodyData
|
|
49
|
+
.sort((a, b) => a.score - b.score)
|
|
50
|
+
.map(item => {
|
|
51
|
+
const {id, org, url, score} = item;
|
|
52
|
+
const pageCell = `<th scope="row"><a href="${url}">${org}</a></th>`;
|
|
53
|
+
const numCell = `<td><a href="digests/${id}.html">${score}</a></td>`;
|
|
54
|
+
const barWidth = 100 * score / maxScore;
|
|
55
|
+
const bar = `<rect height="100%" width="${barWidth}%" fill="red"></rect>`;
|
|
56
|
+
const barCell = `<td aria-hidden="true"><svg width="100%" height="0.7em">${bar}</svg></td>`;
|
|
57
|
+
const row = `<tr>${pageCell}${numCell}${barCell}</tr>`;
|
|
58
|
+
return row;
|
|
59
|
+
});
|
|
60
|
+
return rows.join('\n ');
|
|
61
|
+
};
|
|
62
|
+
// Returns a query for a comparative table.
|
|
63
|
+
exports.getQuery = async () => {
|
|
64
|
+
const data = await getData();
|
|
65
|
+
query.pageCount = data.pageCount;
|
|
66
|
+
query.tableBody = await getTableBody(data.bodyData);
|
|
67
|
+
const date = new Date();
|
|
68
|
+
query.dateISO = date.toISOString().slice(0, 10);
|
|
69
|
+
query.dateSlash = query.dateISO.replace(/-/g, '/');
|
|
70
|
+
return query;
|
|
71
|
+
};
|
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
|
|
7
7
|
// CONSTANTS
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
9
|
+
// Newlines with indentations.
|
|
10
|
+
const joiner = '\n ';
|
|
11
|
+
const innerJoiner = '\n ';
|
|
12
|
+
const specialMessages = {
|
|
13
|
+
log: 'This is based on the amount of browser logging during the tests. Browsers usually log messages only when pages contain erroneous code.',
|
|
14
|
+
preventions: 'This is based on tests that the page did not allow to be run. That impedes accessibility progress and risks interfering with tools that users with disabilities need.',
|
|
15
|
+
solos: 'This is based on issues reported by unclassified tests. Details are in the report.'
|
|
16
|
+
};
|
|
17
17
|
|
|
18
18
|
// FUNCTIONS
|
|
19
19
|
|
|
@@ -26,11 +26,11 @@ const htmlEscape = textOrNumber => textOrNumber
|
|
|
26
26
|
const getScoreRow = (component, score) => `<tr><th>${component}</th><td>${score}</td></tr>`;
|
|
27
27
|
// Gets the start of a paragraph about a special score.
|
|
28
28
|
const getSpecialPStart = (summary, scoreID) =>
|
|
29
|
-
`<p><span class="componentID">${scoreID}</span>: Score ${summary[scoreID]}.`;
|
|
29
|
+
`<p><span class="componentID">${scoreID}</span>: Score ${summary[scoreID]}.`;
|
|
30
30
|
// Adds parameters to a query for a digest.
|
|
31
31
|
exports.makeQuery = (report, query) => {
|
|
32
32
|
// Add an HTML-safe copy of the host report to the query to be appended to the digest.
|
|
33
|
-
const {script, host, score} = report;
|
|
33
|
+
const { script, host, score } = report;
|
|
34
34
|
const reportJSON = JSON.stringify(report, null, 2);
|
|
35
35
|
const reportJSONSafe = htmlEscape(reportJSON);
|
|
36
36
|
query.report = reportJSONSafe;
|
|
@@ -52,7 +52,7 @@ exports.makeQuery = (report, query) => {
|
|
|
52
52
|
return;
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
|
-
const {groupDetails, summary} = score;
|
|
55
|
+
const { groupDetails, summary } = score;
|
|
56
56
|
if (typeof summary.total === 'number') {
|
|
57
57
|
query.totalScore = summary.total;
|
|
58
58
|
}
|
|
@@ -69,7 +69,7 @@ exports.makeQuery = (report, query) => {
|
|
|
69
69
|
}
|
|
70
70
|
});
|
|
71
71
|
// Add the group rows of the score-summary table to the query.
|
|
72
|
-
const {groups} = summary;
|
|
72
|
+
const { groups } = summary;
|
|
73
73
|
const groupIDs = Object.keys(groups);
|
|
74
74
|
groupIDs.sort((a, b) => groups[b] - groups[a]);
|
|
75
75
|
groupIDs.forEach(groupID => {
|
|
@@ -89,7 +89,7 @@ exports.makeQuery = (report, query) => {
|
|
|
89
89
|
// Otherwise, i.e. if the score has no special components:
|
|
90
90
|
else {
|
|
91
91
|
// Add a paragraph stating this for the issue summary to the query.
|
|
92
|
-
query.specialSummary = '<p>No special issues contributed to the score.</p>'
|
|
92
|
+
query.specialSummary = '<p>No special issues contributed to the score.</p>';
|
|
93
93
|
}
|
|
94
94
|
// If the score has any classified issues as components:
|
|
95
95
|
if (groupIDs.length) {
|
|
@@ -104,7 +104,7 @@ exports.makeQuery = (report, query) => {
|
|
|
104
104
|
const testIDs = Object.keys(groupData[packageID]);
|
|
105
105
|
testIDs.forEach(testID => {
|
|
106
106
|
const testData = groupData[packageID][testID];
|
|
107
|
-
const {issueCount} = testData;
|
|
107
|
+
const { issueCount } = testData;
|
|
108
108
|
const issueNoun = issueCount !== 1 ? 'issues' : 'issue';
|
|
109
109
|
const listItem = `<li>${issueCount} ${issueNoun} reported by package <code>${packageID}</code>, test <code>${testID}</code> (${testData.what})</li>`;
|
|
110
110
|
groupListItems.push(listItem);
|
|
@@ -122,6 +122,6 @@ exports.makeQuery = (report, query) => {
|
|
|
122
122
|
// Otherwise, i.e. if the score has no classified issues as components:
|
|
123
123
|
else {
|
|
124
124
|
// Add a paragraph stating this for the group summary to the query.
|
|
125
|
-
query.groupSummary = '<p>No classified issues contributed to the score.</p>'
|
|
125
|
+
query.groupSummary = '<p>No classified issues contributed to the score.</p>';
|
|
126
126
|
}
|
|
127
127
|
};
|
|
@@ -0,0 +1,78 @@
|
|
|
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 a web page">
|
|
10
|
+
<meta name="keywords" content="accessibility a11y web testing">
|
|
11
|
+
<title>Accessibility test digest</title>
|
|
12
|
+
<link rel="icon" href="favicon.png">
|
|
13
|
+
<link rel="stylesheet" href="style.css">
|
|
14
|
+
</head>
|
|
15
|
+
<body>
|
|
16
|
+
<main>
|
|
17
|
+
<header>
|
|
18
|
+
<h1>Accessibility test digest</h1>
|
|
19
|
+
<h2>Synopsis</h2>
|
|
20
|
+
<div id="synopsis">
|
|
21
|
+
<p><strong>Page</strong>: __org__</p>
|
|
22
|
+
<p><strong>URL</strong>: __url__</p>
|
|
23
|
+
<p><strong>Score</strong>: __totalScore__</p>
|
|
24
|
+
<p><strong>Tested by</strong>: Testaro, procedure <code>tp12</code></p>
|
|
25
|
+
<p><strong>Scored by</strong>: Testilo, procedure <code>sp12a</code></p>
|
|
26
|
+
<p><strong>Digested by</strong>: Testilo, procedure <code>dp12a</code></p>
|
|
27
|
+
</div>
|
|
28
|
+
</header>
|
|
29
|
+
<h2>Introduction</h2>
|
|
30
|
+
<p>The <a href="https://www.npmjs.com/package/testaro">Testaro</a> application used its <code>tp12</code> testing procedure to test the <a href="https://www.w3.org/WAI/fundamentals/accessibility-intro/"><dfn>accessibility</dfn></a> (barrier-free design and coding) of the __org__ web page at <a href="__url__">__url__</a> on __dateSlash__. The procedure performed 808 tests. Of these, 16 are custom tests defined by Testaro, and the others belong to these six other packages (programs that perform collections of tests):</p>
|
|
31
|
+
<ul>
|
|
32
|
+
<li><a href="https://github.com/Siteimprove/alfa">Alfa</a> by Siteimprove</li>
|
|
33
|
+
<li><a href="https://www.npmjs.com/package/axe-core">Axe-core</a> by Deque</li>
|
|
34
|
+
<li>
|
|
35
|
+
<a href="https://www.npmjs.com/package/html_codesniffer">HTML CodeSniffer</a> by Squiz Labs
|
|
36
|
+
</li>
|
|
37
|
+
<li><a href="https://github.com/IBMa/equal-access">Equal Access</a> by IBM</li>
|
|
38
|
+
<li><a href="https://tenon.io/documentation/apiv2.php">Tenon</a> by Level Access</li>
|
|
39
|
+
<li><a href="https://wave.webaim.org/api/">WAVE</a> by WebAIM</li>
|
|
40
|
+
</ul>
|
|
41
|
+
<p>Testaro produced a report enumerating the test results.</p>
|
|
42
|
+
<p><a href="https://www.npmjs.com/package/testilo">Testilo</a> processed the report and used the <code>sp12a</code> scoring procedure to compute partial and total scores for the page. The total score is __totalScore__ (where 0 is the best possible score). The scored report is appended below.</p>
|
|
43
|
+
<p>Finally, Testilo used procedure <code>dp12a</code> to produce this digest, briefly explaining how <code>sp12a</code> computed the scores.</p>
|
|
44
|
+
<h2>Score summary</h2>
|
|
45
|
+
<table class="allBorder secondCellRight">
|
|
46
|
+
<caption>Score components</caption>
|
|
47
|
+
<tbody class="headersLeft">
|
|
48
|
+
__scoreRows__
|
|
49
|
+
</tbody>
|
|
50
|
+
</table>
|
|
51
|
+
<h2>Issue summary</h2>
|
|
52
|
+
<h3>Special issues</h3>
|
|
53
|
+
__specialSummary__
|
|
54
|
+
<h3>Classified issues</h3>
|
|
55
|
+
__groupSummary__
|
|
56
|
+
<h2>Discussion</h2>
|
|
57
|
+
<p>Although there are widely accepted <a href="https://www.w3.org/WAI/standards-guidelines/">accessibility standards</a>, there is no unanimity about how to define, test, and quantify accessibility. The failures reported in this digest merit investigation as potential opportunities for improved accessibility. Investigation may lead you to conclude that some of the reported failures do not actually harm accessibility. Conversely, some substantial accessibility faults can escape detection by any of these tests. You may question the attempt to assign an accessibility score to a web page, or you may prefer weightings and formulas different from those used by <code>sp12a</code>. You can modify and extend Testaro and Testilo to fit other theories and priorities.</p>
|
|
58
|
+
<p>Here, in brief, is how <code>sp12a</code> computes a score for a page.</p>
|
|
59
|
+
<ul>
|
|
60
|
+
<li>It finds all the defects and warnings (let’s call them <q>issues</q>) recorded in the report.</li>
|
|
61
|
+
<li>It classifies them according to type. For example, a link that looks like the text around it is one issue category, while a video that has no captions is another issue category.</li>
|
|
62
|
+
<li>It also classifies the issues according to severity. For example, an issue that prevents a transaction is more severe than an issue that only complicates the transaction, and a warning about a possible issue is less severe than a definite finding of an issue. (Some packages rate the severity of each issue; for the other packages, <code>sp12a</code> assigns a severity weight to the issue type and uses that weight.)</li>
|
|
63
|
+
<li>It assigns quality ratings to particular tests that are judged abnormally reliable or unreliable.</li>
|
|
64
|
+
<li>It assigns a score to each issue reported by each test of each package.</li>
|
|
65
|
+
<li>It aggregates the issue scores, weighting them by severity, test quality, and redundancy. Redundancy occurs, and causes downweighting, when two or more packages contain tests that are designed to discover the same or mostly the same issues. So the score for a category is not simply the sum of the scores of the tests in that category.</li>
|
|
66
|
+
<li>It assigns a score for issues in the page logged by the browser.</li>
|
|
67
|
+
<li>It assigns an estimated score each time the page prevents one of the packages or one of the Testaro tests from being run on the page.</li>
|
|
68
|
+
<li>It adds the scores together to obtain a total score.</li>
|
|
69
|
+
</ul>
|
|
70
|
+
<p>The precise rules of <code>sp12a</code> are found in the <a href="https://github.com/jrpool/testilo/blob/main/procs/score/sp12a.js">code itself</a>.</p>
|
|
71
|
+
<h2>Report</h2>
|
|
72
|
+
<pre>__report__</pre>
|
|
73
|
+
<footer>
|
|
74
|
+
<p class="date">Produced <time itemprop="datePublished" datetime="__dateISO__">__dateSlash__</time></p>
|
|
75
|
+
</footer>
|
|
76
|
+
</main>
|
|
77
|
+
</body>
|
|
78
|
+
</html>
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/*
|
|
2
|
+
index: digester for scoring procedure sp12a.
|
|
3
|
+
Creator of parameters for substitution into index.html.
|
|
4
|
+
Usage example for selected files: node digest dp12a 35k1r
|
|
5
|
+
Usage example for all files in REPORTDIR_SCORED: node digest dp12a
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// CONSTANTS
|
|
9
|
+
|
|
10
|
+
// Newlines with indentations.
|
|
11
|
+
const joiner = '\n ';
|
|
12
|
+
const innerJoiner = '\n ';
|
|
13
|
+
const specialMessages = {
|
|
14
|
+
log: 'This is based on the amount of browser error logging and miscellaneous logging during the tests.',
|
|
15
|
+
preventions: 'This is based on tests that the page did not allow to be run. That impedes accessibility progress and risks interfering with tools that users with disabilities need.',
|
|
16
|
+
solos: 'This is based on issues reported by unclassified tests. Details are in the report.'
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
// FUNCTIONS
|
|
20
|
+
|
|
21
|
+
// Makes strings HTML-safe.
|
|
22
|
+
const htmlEscape = textOrNumber => textOrNumber
|
|
23
|
+
.toString()
|
|
24
|
+
.replace(/&/g, '&')
|
|
25
|
+
.replace(/</g, '<');
|
|
26
|
+
// Gets a row of the score-summary table.
|
|
27
|
+
const getScoreRow = (component, score) => `<tr><th>${component}</th><td>${score}</td></tr>`;
|
|
28
|
+
// Gets the start of a paragraph about a special score.
|
|
29
|
+
const getSpecialPStart = (summary, scoreID) =>
|
|
30
|
+
`<p><span class="componentID">${scoreID}</span>: Score ${summary[scoreID]}.`;
|
|
31
|
+
// Adds parameters to a query for a digest.
|
|
32
|
+
exports.makeQuery = (report, query) => {
|
|
33
|
+
// Add an HTML-safe copy of the host report to the query to be appended to the digest.
|
|
34
|
+
const {script, host, score} = report;
|
|
35
|
+
const reportJSON = JSON.stringify(report, null, 2);
|
|
36
|
+
const reportJSONSafe = htmlEscape(reportJSON);
|
|
37
|
+
query.report = reportJSONSafe;
|
|
38
|
+
// Add the job data to the query.
|
|
39
|
+
query.dateISO = report.endTime.slice(0, 10);
|
|
40
|
+
query.dateSlash = query.dateISO.replace(/-/g, '/');
|
|
41
|
+
if (host && host.what && host.which) {
|
|
42
|
+
query.org = host.what;
|
|
43
|
+
query.url = host.which;
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
const firstURLCommand = script.commands.find(command => command.type === 'url');
|
|
47
|
+
if (firstURLCommand && firstURLCommand.what && firstURLCommand.which) {
|
|
48
|
+
query.org = firstURLCommand.what;
|
|
49
|
+
query.url = firstURLCommand.which;
|
|
50
|
+
}
|
|
51
|
+
else {
|
|
52
|
+
console.log('ERROR: host missing or invalid');
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
const {groupDetails, summary} = score;
|
|
57
|
+
const {total, groups} = summary;
|
|
58
|
+
if (typeof total === 'number') {
|
|
59
|
+
query.totalScore = total;
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
console.log('ERROR: missing or invalid total score');
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Add the total and any special rows of the score-summary table to the query.
|
|
66
|
+
const scoreRows = [];
|
|
67
|
+
const specialComponentIDs = ['log', 'preventions', 'solos'];
|
|
68
|
+
['total'].concat(specialComponentIDs).forEach(item => {
|
|
69
|
+
if (summary[item]) {
|
|
70
|
+
scoreRows.push(getScoreRow(item, summary[item]));
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
// Add the group rows of the score-summary table to the query.
|
|
74
|
+
groups.forEach(group => {
|
|
75
|
+
scoreRows.push(getScoreRow(`${group.groupName}`, group.score));
|
|
76
|
+
});
|
|
77
|
+
query.scoreRows = scoreRows.join(innerJoiner);
|
|
78
|
+
// If the score has any special components:
|
|
79
|
+
const scoredSpecialIDs = specialComponentIDs.filter(item => summary[item]);
|
|
80
|
+
if (scoredSpecialIDs.length) {
|
|
81
|
+
// Add paragraphs about them for the issue summary to the query.
|
|
82
|
+
const specialPs = [];
|
|
83
|
+
scoredSpecialIDs.forEach(id => {
|
|
84
|
+
specialPs.push(`${getSpecialPStart(summary, id)} ${specialMessages[id]}`);
|
|
85
|
+
});
|
|
86
|
+
query.specialSummary = specialPs.join(joiner);
|
|
87
|
+
}
|
|
88
|
+
// Otherwise, i.e. if the score has no special components:
|
|
89
|
+
else {
|
|
90
|
+
// Add a paragraph stating this for the issue summary to the query.
|
|
91
|
+
query.specialSummary = '<p>No special issues contributed to the score.</p>'
|
|
92
|
+
}
|
|
93
|
+
// If the score has any classified issues as components:
|
|
94
|
+
if (groups.length) {
|
|
95
|
+
// Add paragraphs about them for the special summary to the query.
|
|
96
|
+
const groupSummaryItems = [];
|
|
97
|
+
groups.forEach(group => {
|
|
98
|
+
const {groupName, score} = group;
|
|
99
|
+
const groupP = `<p><span class="componentID">${groupName}</span>: Score ${score}. Issues reported by tests in this category:</p>`;
|
|
100
|
+
const groupListItems = [];
|
|
101
|
+
const groupData = groupDetails.groups[groupName];
|
|
102
|
+
const packageIDs = Object.keys(groupData);
|
|
103
|
+
packageIDs.forEach(packageID => {
|
|
104
|
+
const testIDs = Object.keys(groupData[packageID]);
|
|
105
|
+
testIDs.forEach(testID => {
|
|
106
|
+
const testData = groupData[packageID][testID];
|
|
107
|
+
const {score, what} = testData;
|
|
108
|
+
const listItem = `<li>Package <code>${packageID}</code>, test <code>${testID}</code>, score ${score} (${what})</li>`;
|
|
109
|
+
groupListItems.push(listItem);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
const groupList = [
|
|
113
|
+
'<ul>',
|
|
114
|
+
groupListItems.join('\n '),
|
|
115
|
+
'</ul>'
|
|
116
|
+
].join(joiner);
|
|
117
|
+
groupSummaryItems.push(groupP, groupList);
|
|
118
|
+
});
|
|
119
|
+
query.groupSummary = groupSummaryItems.join(joiner);
|
|
120
|
+
}
|
|
121
|
+
// Otherwise, i.e. if the score has no classified issues as components:
|
|
122
|
+
else {
|
|
123
|
+
// Add a paragraph stating this for the group summary to the query.
|
|
124
|
+
query.groupSummary = '<p>No classified issues contributed to the score.</p>'
|
|
125
|
+
}
|
|
126
|
+
};
|
package/procs/score/sp12a.js
CHANGED
|
@@ -62,13 +62,17 @@ const groups = {
|
|
|
62
62
|
alfa: {
|
|
63
63
|
r3: {
|
|
64
64
|
quality: 1,
|
|
65
|
-
what: 'Element
|
|
65
|
+
what: 'Element id attribute value is not unique'
|
|
66
66
|
}
|
|
67
67
|
},
|
|
68
68
|
axe: {
|
|
69
69
|
'duplicate-id': {
|
|
70
70
|
quality: 1,
|
|
71
|
-
what: '
|
|
71
|
+
what: 'id attribute value is not unique'
|
|
72
|
+
},
|
|
73
|
+
'duplicate-id-active': {
|
|
74
|
+
quality: 1,
|
|
75
|
+
what: 'id attribute value of the active element is not unique'
|
|
72
76
|
}
|
|
73
77
|
},
|
|
74
78
|
htmlcs: {
|
|
@@ -80,18 +84,43 @@ const groups = {
|
|
|
80
84
|
ibm: {
|
|
81
85
|
RPT_Elem_UniqueId: {
|
|
82
86
|
quality: 1,
|
|
83
|
-
what: 'Element id attribute
|
|
87
|
+
what: 'Element id attribute value is not unique within the document'
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
formFieldNoText: {
|
|
93
|
+
weight: 4,
|
|
94
|
+
packages: {
|
|
95
|
+
alfa: {
|
|
96
|
+
r8: {
|
|
97
|
+
quality: 1,
|
|
98
|
+
what: 'Form field has no accessible name'
|
|
84
99
|
}
|
|
85
100
|
}
|
|
86
101
|
}
|
|
87
102
|
},
|
|
88
|
-
|
|
103
|
+
inputNoText: {
|
|
89
104
|
weight: 4,
|
|
90
105
|
packages: {
|
|
106
|
+
axe: {
|
|
107
|
+
'aria-input-field-name': {
|
|
108
|
+
quality: 1,
|
|
109
|
+
what: 'ARIA input field has no accessible name'
|
|
110
|
+
}
|
|
111
|
+
},
|
|
91
112
|
htmlcs: {
|
|
92
113
|
'e:AA.4_1_2.H91.InputText.Name': {
|
|
93
114
|
quality: 1,
|
|
94
115
|
what: 'Text input has no accessible name'
|
|
116
|
+
},
|
|
117
|
+
'e:AA.4_1_2.H91.InputEmail.Name': {
|
|
118
|
+
quality: 1,
|
|
119
|
+
what: 'Email input has no accessible name'
|
|
120
|
+
},
|
|
121
|
+
'e:AA.4_1_2.H91.InputNumber.Name': {
|
|
122
|
+
quality: 1,
|
|
123
|
+
what: 'Number input has no accessible name'
|
|
95
124
|
}
|
|
96
125
|
}
|
|
97
126
|
}
|
|
@@ -120,7 +149,7 @@ const groups = {
|
|
|
120
149
|
ibm: {
|
|
121
150
|
'v:WCAG20_Input_ExplicitLabelImage': {
|
|
122
151
|
quality: 1,
|
|
123
|
-
what: 'Input element of type image
|
|
152
|
+
what: 'Input element of type image has no text alternative'
|
|
124
153
|
}
|
|
125
154
|
},
|
|
126
155
|
wave: {
|
|
@@ -177,6 +206,17 @@ const groups = {
|
|
|
177
206
|
}
|
|
178
207
|
}
|
|
179
208
|
},
|
|
209
|
+
imageTextLong: {
|
|
210
|
+
weight: 2,
|
|
211
|
+
packages: {
|
|
212
|
+
wave: {
|
|
213
|
+
'a:alt_long': {
|
|
214
|
+
quality: 1,
|
|
215
|
+
what: 'Long text alternative'
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
180
220
|
imageTextRisk: {
|
|
181
221
|
weight: 1,
|
|
182
222
|
packages: {
|
|
@@ -199,6 +239,32 @@ const groups = {
|
|
|
199
239
|
}
|
|
200
240
|
}
|
|
201
241
|
},
|
|
242
|
+
presentationConflict: {
|
|
243
|
+
weight: 4,
|
|
244
|
+
packages: {
|
|
245
|
+
axe: {
|
|
246
|
+
'presentation-role-conflict': {
|
|
247
|
+
quality: 1,
|
|
248
|
+
what: 'Element has a none/presentation role but is focusable or has a global ARIA state or property'
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
decorativeElementExposed: {
|
|
254
|
+
weight: 1,
|
|
255
|
+
packages: {
|
|
256
|
+
alfa: {
|
|
257
|
+
r67: {
|
|
258
|
+
quality: 1,
|
|
259
|
+
what: 'Image marked as decorative is in the accessibility tree or has no none/presentation role'
|
|
260
|
+
},
|
|
261
|
+
r86: {
|
|
262
|
+
quality: 1,
|
|
263
|
+
what: 'Element marked as decorative is in the accessibility tree or has no none/presentation role'
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
},
|
|
202
268
|
pageLanguage: {
|
|
203
269
|
weight: 4,
|
|
204
270
|
packages: {
|
|
@@ -215,9 +281,9 @@ const groups = {
|
|
|
215
281
|
}
|
|
216
282
|
},
|
|
217
283
|
htmlcs: {
|
|
218
|
-
'e:H57': {
|
|
284
|
+
'e:AA.3_1_1.H57.2': {
|
|
219
285
|
quality: 1,
|
|
220
|
-
what: '
|
|
286
|
+
what: 'html element has no lang or xml:lang attribute'
|
|
221
287
|
}
|
|
222
288
|
},
|
|
223
289
|
ibm: {
|
|
@@ -319,7 +385,7 @@ const groups = {
|
|
|
319
385
|
}
|
|
320
386
|
},
|
|
321
387
|
htmlcs: {
|
|
322
|
-
'e:H24': {
|
|
388
|
+
'e:AA.1_1_1.H24': {
|
|
323
389
|
quality: 1,
|
|
324
390
|
what: 'Area element in an image map missing an alt attribute'
|
|
325
391
|
}
|
|
@@ -342,13 +408,13 @@ const groups = {
|
|
|
342
408
|
}
|
|
343
409
|
}
|
|
344
410
|
},
|
|
345
|
-
|
|
346
|
-
weight:
|
|
411
|
+
eventKeyboardRisk: {
|
|
412
|
+
weight: 1,
|
|
347
413
|
packages: {
|
|
348
414
|
htmlcs: {
|
|
349
|
-
'w:G90': {
|
|
415
|
+
'w:AA.2_1_1.G90': {
|
|
350
416
|
quality: 1,
|
|
351
|
-
what: 'Event handler functionality not available by keyboard'
|
|
417
|
+
what: 'Event handler functionality may not be available by keyboard'
|
|
352
418
|
}
|
|
353
419
|
},
|
|
354
420
|
wave: {
|
|
@@ -372,6 +438,10 @@ const groups = {
|
|
|
372
438
|
'a:label_orphaned': {
|
|
373
439
|
quality: 1,
|
|
374
440
|
what: 'Orphaned form label'
|
|
441
|
+
},
|
|
442
|
+
'a:link_internal_broken': {
|
|
443
|
+
quality: 1,
|
|
444
|
+
what: 'Broken same-page link'
|
|
375
445
|
}
|
|
376
446
|
}
|
|
377
447
|
}
|
|
@@ -433,7 +503,7 @@ const groups = {
|
|
|
433
503
|
ibm: {
|
|
434
504
|
'v:WCAG20_A_HasText': {
|
|
435
505
|
quality: 1,
|
|
436
|
-
what: '
|
|
506
|
+
what: 'Hyperlink has no text description'
|
|
437
507
|
}
|
|
438
508
|
},
|
|
439
509
|
tenon: {
|
|
@@ -446,7 +516,11 @@ const groups = {
|
|
|
446
516
|
'e:link_empty': {
|
|
447
517
|
quality: 1,
|
|
448
518
|
what: 'Link contains no text'
|
|
449
|
-
}
|
|
519
|
+
},
|
|
520
|
+
'e:alt_link_missing': {
|
|
521
|
+
quality: 1,
|
|
522
|
+
what: 'Linked image has no text alternative'
|
|
523
|
+
},
|
|
450
524
|
}
|
|
451
525
|
}
|
|
452
526
|
},
|
|
@@ -461,6 +535,17 @@ const groups = {
|
|
|
461
535
|
}
|
|
462
536
|
}
|
|
463
537
|
},
|
|
538
|
+
pdfLink: {
|
|
539
|
+
weight: 1,
|
|
540
|
+
packages: {
|
|
541
|
+
wave: {
|
|
542
|
+
'a:link_pdf': {
|
|
543
|
+
quality: 1,
|
|
544
|
+
what: 'Link to PDF document'
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
},
|
|
464
549
|
destinationLink: {
|
|
465
550
|
weight: 2,
|
|
466
551
|
packages: {
|
|
@@ -494,6 +579,17 @@ const groups = {
|
|
|
494
579
|
}
|
|
495
580
|
}
|
|
496
581
|
},
|
|
582
|
+
linkDestinationsSame: {
|
|
583
|
+
weight: 2,
|
|
584
|
+
packages: {
|
|
585
|
+
tenon: {
|
|
586
|
+
184: {
|
|
587
|
+
quality: 1,
|
|
588
|
+
what: 'Adjacent links point to the same destination'
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
},
|
|
497
593
|
linkConfusionRisk: {
|
|
498
594
|
weight: 1,
|
|
499
595
|
packages: {
|
|
@@ -516,6 +612,17 @@ const groups = {
|
|
|
516
612
|
}
|
|
517
613
|
}
|
|
518
614
|
},
|
|
615
|
+
formNewWindow: {
|
|
616
|
+
weight: 2,
|
|
617
|
+
packages: {
|
|
618
|
+
tenon: {
|
|
619
|
+
214: {
|
|
620
|
+
quality: 1,
|
|
621
|
+
what: 'Form submission opens a new window'
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
},
|
|
519
626
|
linkForcesNewWindow: {
|
|
520
627
|
weight: 3,
|
|
521
628
|
packages: {
|
|
@@ -527,7 +634,7 @@ const groups = {
|
|
|
527
634
|
}
|
|
528
635
|
}
|
|
529
636
|
},
|
|
530
|
-
|
|
637
|
+
linkWindowSurpriseRisk: {
|
|
531
638
|
weight: 1,
|
|
532
639
|
packages: {
|
|
533
640
|
htmlcs: {
|
|
@@ -550,13 +657,21 @@ const groups = {
|
|
|
550
657
|
axe: {
|
|
551
658
|
'aria-command-name': {
|
|
552
659
|
quality: 1,
|
|
553
|
-
what: 'ARIA
|
|
660
|
+
what: 'ARIA command has no accessible name'
|
|
661
|
+
},
|
|
662
|
+
'button-name': {
|
|
663
|
+
quality: 1,
|
|
664
|
+
what: 'Button has no discernible text'
|
|
554
665
|
}
|
|
555
666
|
},
|
|
556
667
|
htmlcs: {
|
|
668
|
+
'e:AA.4_1_2.H91.A.Name': {
|
|
669
|
+
quality: 1,
|
|
670
|
+
what: 'Link with button role has no accessible name'
|
|
671
|
+
},
|
|
557
672
|
'e:AA.4_1_2.H91.Button.Name': {
|
|
558
673
|
quality: 1,
|
|
559
|
-
what: '
|
|
674
|
+
what: 'Button element has no accessible name'
|
|
560
675
|
}
|
|
561
676
|
},
|
|
562
677
|
wave: {
|
|
@@ -601,6 +716,28 @@ const groups = {
|
|
|
601
716
|
}
|
|
602
717
|
}
|
|
603
718
|
},
|
|
719
|
+
cssBansRotate: {
|
|
720
|
+
weight: 4,
|
|
721
|
+
packages: {
|
|
722
|
+
axe: {
|
|
723
|
+
'css-orientation-lock': {
|
|
724
|
+
quality: 1,
|
|
725
|
+
what: 'CSS media query locks display orientation'
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
},
|
|
730
|
+
textRotated: {
|
|
731
|
+
weight: 2,
|
|
732
|
+
packages: {
|
|
733
|
+
tenon: {
|
|
734
|
+
271: {
|
|
735
|
+
quality: 1,
|
|
736
|
+
what: 'Text is needlessly rotated 60+ degrees or more, hurting comprehension'
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
},
|
|
604
741
|
metaBansZoom: {
|
|
605
742
|
weight: 4,
|
|
606
743
|
packages: {
|
|
@@ -613,7 +750,11 @@ const groups = {
|
|
|
613
750
|
axe: {
|
|
614
751
|
'meta-viewport': {
|
|
615
752
|
quality: 1,
|
|
616
|
-
what: 'Zooming and scaling
|
|
753
|
+
what: 'Zooming and scaling are disabled'
|
|
754
|
+
},
|
|
755
|
+
'meta-viewport-large': {
|
|
756
|
+
quality: 1,
|
|
757
|
+
what: 'User cannot zoom and scale the text up to 500%'
|
|
617
758
|
}
|
|
618
759
|
}
|
|
619
760
|
}
|
|
@@ -747,7 +888,7 @@ const groups = {
|
|
|
747
888
|
axe: {
|
|
748
889
|
'frame-title': {
|
|
749
890
|
quality: 1,
|
|
750
|
-
what: '
|
|
891
|
+
what: 'Frame has no accessible name'
|
|
751
892
|
},
|
|
752
893
|
'frame-title-unique': {
|
|
753
894
|
quality: 1,
|
|
@@ -868,7 +1009,7 @@ const groups = {
|
|
|
868
1009
|
}
|
|
869
1010
|
},
|
|
870
1011
|
autocompleteBad: {
|
|
871
|
-
weight:
|
|
1012
|
+
weight: 3,
|
|
872
1013
|
packages: {
|
|
873
1014
|
alfa: {
|
|
874
1015
|
r10: {
|
|
@@ -1006,10 +1147,16 @@ const groups = {
|
|
|
1006
1147
|
headingEmpty: {
|
|
1007
1148
|
weight: 3,
|
|
1008
1149
|
packages: {
|
|
1150
|
+
alfa: {
|
|
1151
|
+
r64: {
|
|
1152
|
+
quality: 1,
|
|
1153
|
+
what: 'Heading has no non-empty accessible name'
|
|
1154
|
+
}
|
|
1155
|
+
},
|
|
1009
1156
|
axe: {
|
|
1010
1157
|
'empty-heading': {
|
|
1011
1158
|
quality: 1,
|
|
1012
|
-
what: '
|
|
1159
|
+
what: 'Heading empty'
|
|
1013
1160
|
}
|
|
1014
1161
|
},
|
|
1015
1162
|
htmlcs: {
|
|
@@ -1032,6 +1179,17 @@ const groups = {
|
|
|
1032
1179
|
}
|
|
1033
1180
|
}
|
|
1034
1181
|
},
|
|
1182
|
+
headingOfNothing: {
|
|
1183
|
+
weight: 2,
|
|
1184
|
+
packages: {
|
|
1185
|
+
alfa: {
|
|
1186
|
+
r78: {
|
|
1187
|
+
quality: 1,
|
|
1188
|
+
what: 'No content between two headings of the same level'
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
},
|
|
1035
1193
|
imageTextRedundant: {
|
|
1036
1194
|
weight: 1,
|
|
1037
1195
|
packages: {
|
|
@@ -1044,7 +1202,19 @@ const groups = {
|
|
|
1044
1202
|
ibm: {
|
|
1045
1203
|
'v:WCAG20_Img_LinkTextNotRedundant': {
|
|
1046
1204
|
quality: 1,
|
|
1047
|
-
what: 'Text alternative for image
|
|
1205
|
+
what: 'Text alternative for the image in a link repeats text of the same or an adjacent link'
|
|
1206
|
+
}
|
|
1207
|
+
},
|
|
1208
|
+
tenon: {
|
|
1209
|
+
138: {
|
|
1210
|
+
quality: 1,
|
|
1211
|
+
what: 'Image link alternative text repeats text in the link'
|
|
1212
|
+
}
|
|
1213
|
+
},
|
|
1214
|
+
wave: {
|
|
1215
|
+
'a:alt_redundant': {
|
|
1216
|
+
quality: 1,
|
|
1217
|
+
what: 'Redundant text alternative'
|
|
1048
1218
|
}
|
|
1049
1219
|
}
|
|
1050
1220
|
}
|
|
@@ -1156,13 +1326,30 @@ const groups = {
|
|
|
1156
1326
|
}
|
|
1157
1327
|
}
|
|
1158
1328
|
},
|
|
1329
|
+
pseudoParagraphRisk: {
|
|
1330
|
+
weight: 1,
|
|
1331
|
+
packages: {
|
|
1332
|
+
tenon: {
|
|
1333
|
+
242: {
|
|
1334
|
+
quality: 1,
|
|
1335
|
+
what: 'Multiple consecutive br elements may simulate paragraphs'
|
|
1336
|
+
}
|
|
1337
|
+
}
|
|
1338
|
+
}
|
|
1339
|
+
},
|
|
1159
1340
|
pseudoHeadingRisk: {
|
|
1160
1341
|
weight: 1,
|
|
1161
1342
|
packages: {
|
|
1343
|
+
axe: {
|
|
1344
|
+
'p-as-heading': {
|
|
1345
|
+
quality: 1,
|
|
1346
|
+
what: 'Styled p element may be misused as a heading'
|
|
1347
|
+
}
|
|
1348
|
+
},
|
|
1162
1349
|
htmlcs: {
|
|
1163
1350
|
'w:AA.1_3_1.H42': {
|
|
1164
1351
|
quality: 1,
|
|
1165
|
-
what: 'Heading coding
|
|
1352
|
+
what: 'Heading coding is not used but the element may be intended as a heading'
|
|
1166
1353
|
}
|
|
1167
1354
|
},
|
|
1168
1355
|
wave: {
|
|
@@ -1184,6 +1371,28 @@ const groups = {
|
|
|
1184
1371
|
}
|
|
1185
1372
|
}
|
|
1186
1373
|
},
|
|
1374
|
+
listChild: {
|
|
1375
|
+
weight: 4,
|
|
1376
|
+
packages: {
|
|
1377
|
+
axe: {
|
|
1378
|
+
'list': {
|
|
1379
|
+
quality: 1,
|
|
1380
|
+
what: 'List element ul or ol has a child element other than li, script, and template'
|
|
1381
|
+
}
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
},
|
|
1385
|
+
listItemOrphan: {
|
|
1386
|
+
weight: 4,
|
|
1387
|
+
packages: {
|
|
1388
|
+
axe: {
|
|
1389
|
+
listitem: {
|
|
1390
|
+
quality: 1,
|
|
1391
|
+
what: 'li element is not contained by a ul or ol element'
|
|
1392
|
+
}
|
|
1393
|
+
}
|
|
1394
|
+
}
|
|
1395
|
+
},
|
|
1187
1396
|
pseudoOrderedListRisk: {
|
|
1188
1397
|
weight: 1,
|
|
1189
1398
|
packages: {
|
|
@@ -1212,13 +1421,17 @@ const groups = {
|
|
|
1212
1421
|
axe: {
|
|
1213
1422
|
'select-name': {
|
|
1214
1423
|
quality: 1,
|
|
1215
|
-
what: 'Select element
|
|
1424
|
+
what: 'Select element has no accessible name'
|
|
1216
1425
|
}
|
|
1217
1426
|
},
|
|
1218
1427
|
htmlcs: {
|
|
1219
|
-
'
|
|
1428
|
+
'e:AA.4_1_2.H91.Select.Name': {
|
|
1220
1429
|
quality: 1,
|
|
1221
|
-
what: 'Select element has no
|
|
1430
|
+
what: 'Select element has no accessible name'
|
|
1431
|
+
},
|
|
1432
|
+
'w:AA.4_1_2.H91.Select.Value': {
|
|
1433
|
+
quality: 1,
|
|
1434
|
+
what: 'Select element value has no accessible name'
|
|
1222
1435
|
}
|
|
1223
1436
|
},
|
|
1224
1437
|
wave: {
|
|
@@ -1288,12 +1501,24 @@ const groups = {
|
|
|
1288
1501
|
quality: 1,
|
|
1289
1502
|
what: 'Fieldset has no legend element'
|
|
1290
1503
|
}
|
|
1504
|
+
},
|
|
1505
|
+
wave: {
|
|
1506
|
+
'a:legend_missing': {
|
|
1507
|
+
quality: 1,
|
|
1508
|
+
what: 'Fieldset has no legend element'
|
|
1509
|
+
}
|
|
1291
1510
|
}
|
|
1292
1511
|
}
|
|
1293
1512
|
},
|
|
1294
|
-
|
|
1513
|
+
groupName: {
|
|
1295
1514
|
weight: 3,
|
|
1296
1515
|
packages: {
|
|
1516
|
+
alfa: {
|
|
1517
|
+
r60: {
|
|
1518
|
+
quality: 1,
|
|
1519
|
+
what: 'Form-control group has no accessible name'
|
|
1520
|
+
}
|
|
1521
|
+
},
|
|
1297
1522
|
htmlcs: {
|
|
1298
1523
|
'e:AA.4_1_2.H91.Fieldset.Name': {
|
|
1299
1524
|
quality: 1,
|
|
@@ -1333,6 +1558,18 @@ const groups = {
|
|
|
1333
1558
|
invisibleLabel: {
|
|
1334
1559
|
weight: 3,
|
|
1335
1560
|
packages: {
|
|
1561
|
+
alfa: {
|
|
1562
|
+
r14: {
|
|
1563
|
+
quality: 1,
|
|
1564
|
+
what: 'Visible label is not in the accessible name'
|
|
1565
|
+
}
|
|
1566
|
+
},
|
|
1567
|
+
axe: {
|
|
1568
|
+
'label-content-name-mismatch': {
|
|
1569
|
+
quality: 1,
|
|
1570
|
+
what: 'Element visible text is not part of its accessible name'
|
|
1571
|
+
}
|
|
1572
|
+
},
|
|
1336
1573
|
htmlcs: {
|
|
1337
1574
|
'w:AA.2_5_3.F96': {
|
|
1338
1575
|
quality: 1,
|
|
@@ -1405,6 +1642,12 @@ const groups = {
|
|
|
1405
1642
|
allCaps: {
|
|
1406
1643
|
weight: 1,
|
|
1407
1644
|
packages: {
|
|
1645
|
+
alfa: {
|
|
1646
|
+
r72: {
|
|
1647
|
+
quality: 1,
|
|
1648
|
+
what: 'Paragraph text is uppercased'
|
|
1649
|
+
}
|
|
1650
|
+
},
|
|
1408
1651
|
tenon: {
|
|
1409
1652
|
153: {
|
|
1410
1653
|
quality: 1,
|
|
@@ -1413,13 +1656,52 @@ const groups = {
|
|
|
1413
1656
|
}
|
|
1414
1657
|
}
|
|
1415
1658
|
},
|
|
1416
|
-
|
|
1659
|
+
allItalics: {
|
|
1660
|
+
weight: 1,
|
|
1661
|
+
packages: {
|
|
1662
|
+
tenon: {
|
|
1663
|
+
154: {
|
|
1664
|
+
quality: 1,
|
|
1665
|
+
what: 'Long string of text is italic'
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
},
|
|
1670
|
+
contentBeyondLandmarks: {
|
|
1417
1671
|
weight: 2,
|
|
1418
1672
|
packages: {
|
|
1419
1673
|
alfa: {
|
|
1420
1674
|
r57: {
|
|
1421
1675
|
quality: 1,
|
|
1422
|
-
what: 'Perceivable text content not included in any landmark'
|
|
1676
|
+
what: 'Perceivable text content is not included in any landmark'
|
|
1677
|
+
}
|
|
1678
|
+
},
|
|
1679
|
+
axe: {
|
|
1680
|
+
region: {
|
|
1681
|
+
quality: 1,
|
|
1682
|
+
what: 'Some page content is not contained by landmarks'
|
|
1683
|
+
}
|
|
1684
|
+
}
|
|
1685
|
+
}
|
|
1686
|
+
},
|
|
1687
|
+
footerTopLandmark: {
|
|
1688
|
+
weight: 1,
|
|
1689
|
+
packages: {
|
|
1690
|
+
axe: {
|
|
1691
|
+
'landmark-contentinfo-is-top-level': {
|
|
1692
|
+
quality: 1,
|
|
1693
|
+
what: 'contentinfo landmark (footer) is contained in another landmark'
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1696
|
+
}
|
|
1697
|
+
},
|
|
1698
|
+
asideTopLandmark: {
|
|
1699
|
+
weight: 2,
|
|
1700
|
+
packages: {
|
|
1701
|
+
axe: {
|
|
1702
|
+
'landmark-complementary-is-top-level': {
|
|
1703
|
+
quality: 1,
|
|
1704
|
+
what: 'complementary landmark (aside) is contained in another landmark'
|
|
1423
1705
|
}
|
|
1424
1706
|
}
|
|
1425
1707
|
}
|
|
@@ -1435,10 +1717,14 @@ const groups = {
|
|
|
1435
1717
|
}
|
|
1436
1718
|
}
|
|
1437
1719
|
},
|
|
1438
|
-
|
|
1720
|
+
mainLandmark: {
|
|
1439
1721
|
weight: 2,
|
|
1440
1722
|
packages: {
|
|
1441
1723
|
axe: {
|
|
1724
|
+
'landmark-one-main': {
|
|
1725
|
+
quality: 1,
|
|
1726
|
+
what: 'page has no main landmark'
|
|
1727
|
+
},
|
|
1442
1728
|
'landmark-no-duplicate-main': {
|
|
1443
1729
|
quality: 1,
|
|
1444
1730
|
what: 'page has more than 1 main landmark'
|
|
@@ -1446,6 +1732,28 @@ const groups = {
|
|
|
1446
1732
|
}
|
|
1447
1733
|
}
|
|
1448
1734
|
},
|
|
1735
|
+
footerMultiple: {
|
|
1736
|
+
weight: 2,
|
|
1737
|
+
packages: {
|
|
1738
|
+
axe: {
|
|
1739
|
+
'landmark-no-duplicate-contentinfo': {
|
|
1740
|
+
quality: 1,
|
|
1741
|
+
what: 'page has more than 1 contentinfo landmark (footer)'
|
|
1742
|
+
}
|
|
1743
|
+
}
|
|
1744
|
+
}
|
|
1745
|
+
},
|
|
1746
|
+
landmarkConfusion: {
|
|
1747
|
+
weight: 3,
|
|
1748
|
+
packages: {
|
|
1749
|
+
axe: {
|
|
1750
|
+
'landmark-unique': {
|
|
1751
|
+
quality: 1,
|
|
1752
|
+
what: 'Landmark has a role and an accessible name that are identical to another'
|
|
1753
|
+
}
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
},
|
|
1449
1757
|
focusableOperable: {
|
|
1450
1758
|
weight: 3,
|
|
1451
1759
|
packages: {
|
|
@@ -1485,6 +1793,17 @@ const groups = {
|
|
|
1485
1793
|
}
|
|
1486
1794
|
}
|
|
1487
1795
|
},
|
|
1796
|
+
labeledHidden: {
|
|
1797
|
+
weight: 2,
|
|
1798
|
+
packages: {
|
|
1799
|
+
htmlcs: {
|
|
1800
|
+
'w:AA.1_3_1.F68.Hidden': {
|
|
1801
|
+
quality: 1,
|
|
1802
|
+
what: 'Hidden form field is needlessly labeled.'
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
},
|
|
1488
1807
|
hiddenContentRisk: {
|
|
1489
1808
|
weight: 1,
|
|
1490
1809
|
packages: {
|
|
@@ -1540,13 +1859,47 @@ const groups = {
|
|
|
1540
1859
|
}
|
|
1541
1860
|
}
|
|
1542
1861
|
},
|
|
1543
|
-
|
|
1862
|
+
linkComprehensionRisk: {
|
|
1863
|
+
weight: 1,
|
|
1864
|
+
packages: {
|
|
1865
|
+
wave: {
|
|
1866
|
+
'a:link_suspicious': {
|
|
1867
|
+
quality: 1,
|
|
1868
|
+
what: 'Suspicious link text'
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
},
|
|
1873
|
+
linkVague: {
|
|
1874
|
+
weight: 3,
|
|
1875
|
+
packages: {
|
|
1876
|
+
tenon: {
|
|
1877
|
+
73: {
|
|
1878
|
+
quality: 1,
|
|
1879
|
+
what: 'Link text is too generic to communicate the purpose or destination'
|
|
1880
|
+
}
|
|
1881
|
+
}
|
|
1882
|
+
}
|
|
1883
|
+
},
|
|
1884
|
+
linkIndication: {
|
|
1544
1885
|
weight: 2,
|
|
1545
1886
|
packages: {
|
|
1887
|
+
alfa: {
|
|
1888
|
+
r62: {
|
|
1889
|
+
quality: 1,
|
|
1890
|
+
what: 'Inline link is not distinct from the surrounding text except by color'
|
|
1891
|
+
}
|
|
1892
|
+
},
|
|
1893
|
+
axe: {
|
|
1894
|
+
'link-in-text-block': {
|
|
1895
|
+
quality: 1,
|
|
1896
|
+
what: 'Link is not distinct from surrounding text without reliance on color'
|
|
1897
|
+
}
|
|
1898
|
+
},
|
|
1546
1899
|
testaro: {
|
|
1547
1900
|
linkUl: {
|
|
1548
1901
|
quality: 1,
|
|
1549
|
-
what: 'Non-underlined
|
|
1902
|
+
what: 'Non-underlined adjacent links'
|
|
1550
1903
|
}
|
|
1551
1904
|
}
|
|
1552
1905
|
}
|
|
@@ -1606,6 +1959,34 @@ const groups = {
|
|
|
1606
1959
|
}
|
|
1607
1960
|
}
|
|
1608
1961
|
},
|
|
1962
|
+
tabIndexPositive: {
|
|
1963
|
+
weight: 1,
|
|
1964
|
+
packages: {
|
|
1965
|
+
axe: {
|
|
1966
|
+
tabindex: {
|
|
1967
|
+
quality: 1,
|
|
1968
|
+
what: 'Positive tabIndex risks creating a confusing focus order'
|
|
1969
|
+
}
|
|
1970
|
+
},
|
|
1971
|
+
wave: {
|
|
1972
|
+
'a:tabindex': {
|
|
1973
|
+
quality: 1,
|
|
1974
|
+
what: 'tabIndex value positive'
|
|
1975
|
+
}
|
|
1976
|
+
}
|
|
1977
|
+
}
|
|
1978
|
+
},
|
|
1979
|
+
tabIndexMissing: {
|
|
1980
|
+
weight: 4,
|
|
1981
|
+
packages: {
|
|
1982
|
+
tenon: {
|
|
1983
|
+
190: {
|
|
1984
|
+
quality: 1,
|
|
1985
|
+
what: 'Interactive item is not natively actionable, but has no tabindex=0 attribute'
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
},
|
|
1609
1990
|
videoCaptionMissing: {
|
|
1610
1991
|
weight: 4,
|
|
1611
1992
|
packages: {
|
|
@@ -1697,10 +2078,16 @@ const groups = {
|
|
|
1697
2078
|
obsoleteElement: {
|
|
1698
2079
|
weight: 1,
|
|
1699
2080
|
packages: {
|
|
2081
|
+
alfa: {
|
|
2082
|
+
r70: {
|
|
2083
|
+
quality: 1,
|
|
2084
|
+
what: 'Element is obsolete or deprecated'
|
|
2085
|
+
}
|
|
2086
|
+
},
|
|
1700
2087
|
htmlcs: {
|
|
1701
2088
|
'e:AA.1_3_1.H49.Center': {
|
|
1702
2089
|
quality: 1,
|
|
1703
|
-
what: '
|
|
2090
|
+
what: 'Element is obsolete'
|
|
1704
2091
|
}
|
|
1705
2092
|
}
|
|
1706
2093
|
}
|
|
@@ -2120,7 +2507,11 @@ exports.scorer = async report => {
|
|
|
2120
2507
|
weightedScore *= groups[groupName].packages[packageName][testID].quality;
|
|
2121
2508
|
// Round the score, but not to less than 1.
|
|
2122
2509
|
const roundedScore = Math.max(Math.round(weightedScore), 1);
|
|
2123
|
-
|
|
2510
|
+
// Add the rounded score and the test description to the group details.
|
|
2511
|
+
groupDetails.groups[groupName][packageName][testID] = {
|
|
2512
|
+
score: roundedScore,
|
|
2513
|
+
what: groups[groupName].packages[packageName][testID].what
|
|
2514
|
+
};
|
|
2124
2515
|
}
|
|
2125
2516
|
// Otherwise, i.e. if the test is solo:
|
|
2126
2517
|
else {
|
|
@@ -2143,7 +2534,7 @@ exports.scorer = async report => {
|
|
|
2143
2534
|
groupPackageData.forEach(packageObj => {
|
|
2144
2535
|
// Get the sum of the scores of the tests of the package in the group.
|
|
2145
2536
|
const scoreSum = Object.values(packageObj).reduce(
|
|
2146
|
-
(sum, current) => sum + current,
|
|
2537
|
+
(sum, current) => sum + current.score,
|
|
2147
2538
|
0
|
|
2148
2539
|
);
|
|
2149
2540
|
// Add the sum to the list of package scores in the group.
|
|
File without changes
|