@govtechsg/oobee 0.10.34 → 0.10.39
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/.vscode/settings.json +1 -1
- package/DETAILS.md +58 -42
- package/INTEGRATION.md +142 -53
- package/README.md +15 -0
- package/__mocks__/mock-report.html +1 -1
- package/exclusions.txt +4 -1
- package/package.json +2 -2
- package/src/constants/cliFunctions.ts +0 -7
- package/src/constants/common.ts +39 -1
- package/src/constants/constants.ts +9 -8
- package/src/constants/itemTypeDescription.ts +3 -3
- package/src/crawlers/commonCrawlerFunc.ts +67 -214
- package/src/crawlers/crawlDomain.ts +6 -2
- package/src/crawlers/crawlLocalFile.ts +2 -0
- package/src/crawlers/crawlSitemap.ts +5 -3
- package/src/crawlers/custom/escapeCssSelector.ts +10 -0
- package/src/crawlers/custom/evaluateAltText.ts +13 -0
- package/src/crawlers/custom/extractAndGradeText.ts +0 -2
- package/src/crawlers/custom/extractText.ts +28 -0
- package/src/crawlers/custom/findElementByCssSelector.ts +46 -0
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +1006 -901
- package/src/crawlers/custom/framesCheck.ts +51 -0
- package/src/crawlers/custom/getAxeConfiguration.ts +126 -0
- package/src/crawlers/custom/gradeReadability.ts +30 -0
- package/src/crawlers/custom/xPathToCss.ts +178 -0
- package/src/mergeAxeResults.ts +503 -132
- package/src/npmIndex.ts +130 -62
- package/src/static/ejs/partials/components/ruleOffcanvas.ejs +1 -1
- package/src/static/ejs/partials/components/scanAbout.ejs +1 -1
- package/src/static/ejs/partials/components/summaryScanResults.ejs +1 -1
- package/src/static/ejs/partials/components/wcagCompliance.ejs +3 -2
- package/src/static/ejs/partials/footer.ejs +13 -7
- package/src/static/ejs/partials/scripts/reportSearch.ejs +112 -74
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +2 -2
- package/src/static/ejs/partials/scripts/utils.ejs +1 -1
- package/src/static/ejs/partials/summaryMain.ejs +6 -6
- package/src/static/ejs/report.ejs +5 -5
- package/src/utils.ts +29 -10
- package/src/xPathToCssCypress.ts +178 -0
- package/src/crawlers/customAxeFunctions.ts +0 -82
@@ -47,7 +47,7 @@
|
|
47
47
|
// Scan DATA FUNCTION TO REPLACE NA
|
48
48
|
const scanDataWCAGCompliance = () => {
|
49
49
|
const passPecentage = document.getElementById('passPercentage');
|
50
|
-
passPecentage.innerHTML = scanData.wcagPassPercentage.
|
50
|
+
passPecentage.innerHTML = (scanData.wcagPassPercentage.totalWcagChecksAA - scanData.wcagPassPercentage.totalWcagViolationsAA) + ' / ' + scanData.wcagPassPercentage.totalWcagChecksAA + ' of automated checks';
|
51
51
|
const wcagBarProgess = document.getElementById('wcag-compliance-passes-bar-progress');
|
52
52
|
wcagBarProgess.style.width = `${scanData.wcagPassPercentage.passPercentageAA}%`; // Set this to your desired width
|
53
53
|
|
@@ -94,7 +94,7 @@
|
|
94
94
|
const formattedCategoryTitles = {
|
95
95
|
mustFix: 'Must Fix',
|
96
96
|
goodToFix: 'Good to Fix',
|
97
|
-
needsReview: '
|
97
|
+
needsReview: 'Manual Review Required',
|
98
98
|
passed: 'Passed',
|
99
99
|
};
|
100
100
|
|
@@ -249,8 +249,8 @@
|
|
249
249
|
<a href="#" id="createPassedItemsFile">${passedItems} ${passedItems === 1 ? 'occurrence' : 'occurrences'} passed</a>`;
|
250
250
|
itemsElement.innerHTML = itemsContent;
|
251
251
|
|
252
|
-
var phAppVersionElement = document.getElementById('
|
253
|
-
var versionContent = 'Oobee Version ' + scanData.
|
252
|
+
var phAppVersionElement = document.getElementById('oobeeAppVersion');
|
253
|
+
var versionContent = 'Oobee Version ' + scanData.oobeeAppVersion;
|
254
254
|
phAppVersionElement.innerHTML = versionContent;
|
255
255
|
|
256
256
|
var isCustomFlow = scanData.isCustomFlow;
|
@@ -298,7 +298,7 @@
|
|
298
298
|
|
299
299
|
pagesNotScanned.forEach((page, index) => {
|
300
300
|
var listItem = document.createElement('li');
|
301
|
-
listItem.innerHTML = `<a class="not-scanned-url" href="${page.url}" target="_blank">${page.url}</a>`;
|
301
|
+
listItem.innerHTML = `<a class="not-scanned-url" href="${page.url || page }" target="_blank">${page.url || page }</a>`;
|
302
302
|
pagesNotScannedList.appendChild(listItem);
|
303
303
|
});
|
304
304
|
}
|
package/src/utils.ts
CHANGED
@@ -190,22 +190,41 @@ export const cleanUp = async pathToDelete => {
|
|
190
190
|
// timeZoneName: "longGeneric",
|
191
191
|
// });
|
192
192
|
|
193
|
-
export const getWcagPassPercentage = (
|
193
|
+
export const getWcagPassPercentage = (
|
194
|
+
wcagViolations: string[],
|
195
|
+
showEnableWcagAaa: boolean
|
196
|
+
): {
|
197
|
+
passPercentageAA: string;
|
198
|
+
totalWcagChecksAA: number;
|
199
|
+
totalWcagViolationsAA: number;
|
200
|
+
passPercentageAAandAAA: string;
|
201
|
+
totalWcagChecksAAandAAA: number;
|
202
|
+
totalWcagViolationsAAandAAA: number;
|
203
|
+
} => {
|
194
204
|
|
195
205
|
// These AAA rules should not be counted as WCAG Pass Percentage only contains A and AA
|
196
|
-
const
|
197
|
-
|
198
|
-
|
199
|
-
const
|
200
|
-
|
201
|
-
|
202
|
-
const
|
203
|
-
const
|
206
|
+
const wcagAAALinks = ['WCAG 1.4.6', 'WCAG 2.2.4', 'WCAG 2.4.9', 'WCAG 3.1.5', 'WCAG 3.2.5'];
|
207
|
+
const wcagAAA = ['wcag146', 'wcag224', 'wcag249', 'wcag315', 'wcag325'];
|
208
|
+
|
209
|
+
const wcagLinksAAandAAA = constants.wcagLinks;
|
210
|
+
|
211
|
+
const wcagViolationsAAandAAA = showEnableWcagAaa ? wcagViolations.length : null;
|
212
|
+
const totalChecksAAandAAA = showEnableWcagAaa ? Object.keys(wcagLinksAAandAAA).length : null;
|
213
|
+
const passedChecksAAandAAA = showEnableWcagAaa ? totalChecksAAandAAA - wcagViolationsAAandAAA : null;
|
214
|
+
const passPercentageAAandAAA = showEnableWcagAaa ? (totalChecksAAandAAA === 0 ? 0 : (passedChecksAAandAAA / totalChecksAAandAAA) * 100) : null;
|
215
|
+
|
216
|
+
const wcagViolationsAA = wcagViolations.filter(violation => !wcagAAA.includes(violation)).length;
|
217
|
+
const totalChecksAA = Object.keys(wcagLinksAAandAAA).filter(key => !wcagAAALinks.includes(key)).length;
|
218
|
+
const passedChecksAA = totalChecksAA - wcagViolationsAA;
|
219
|
+
const passPercentageAA = totalChecksAA === 0 ? 0 : (passedChecksAA / totalChecksAA) * 100;
|
204
220
|
|
205
221
|
return {
|
206
222
|
passPercentageAA: passPercentageAA.toFixed(2), // toFixed returns a string, which is correct here
|
207
223
|
totalWcagChecksAA: totalChecksAA,
|
208
|
-
totalWcagViolationsAA:
|
224
|
+
totalWcagViolationsAA: wcagViolationsAA,
|
225
|
+
passPercentageAAandAAA: passPercentageAAandAAA ? passPercentageAAandAAA.toFixed(2) : null, // toFixed returns a string, which is correct here
|
226
|
+
totalWcagChecksAAandAAA: totalChecksAAandAAA,
|
227
|
+
totalWcagViolationsAAandAAA: wcagViolationsAAandAAA,
|
209
228
|
};
|
210
229
|
};
|
211
230
|
|
@@ -0,0 +1,178 @@
|
|
1
|
+
export function xPathToCss(expr: string) {
|
2
|
+
const isValidXPath = expr =>
|
3
|
+
typeof expr !== 'undefined' &&
|
4
|
+
expr.replace(/[\s-_=]/g, '') !== '' &&
|
5
|
+
expr.length ===
|
6
|
+
expr.replace(
|
7
|
+
/[-_\w:.]+\(\)\s*=|=\s*[-_\w:.]+\(\)|\sor\s|\sand\s|\[(?:[^\/\]]+[\/\[]\/?.+)+\]|starts-with\(|\[.*last\(\)\s*[-\+<>=].+\]|number\(\)|not\(|count\(|text\(|first\(|normalize-space|[^\/]following-sibling|concat\(|descendant::|parent::|self::|child::|/gi,
|
8
|
+
'',
|
9
|
+
).length;
|
10
|
+
|
11
|
+
const getValidationRegex = () => {
|
12
|
+
let regex =
|
13
|
+
'(?P<node>' +
|
14
|
+
'(' +
|
15
|
+
'^id\\(["\\\']?(?P<idvalue>%(value)s)["\\\']?\\)' + // special case! `id(idValue)`
|
16
|
+
'|' +
|
17
|
+
'(?P<nav>//?(?:following-sibling::)?)(?P<tag>%(tag)s)' + // `//div`
|
18
|
+
'(\\[(' +
|
19
|
+
'(?P<matched>(?P<mattr>@?%(attribute)s=["\\\'](?P<mvalue>%(value)s))["\\\']' + // `[@id="well"]` supported and `[text()="yes"]` is not
|
20
|
+
'|' +
|
21
|
+
'(?P<contained>contains\\((?P<cattr>@?%(attribute)s,\\s*["\\\'](?P<cvalue>%(value)s)["\\\']\\))' + // `[contains(@id, "bleh")]` supported and `[contains(text(), "some")]` is not
|
22
|
+
')\\])?' +
|
23
|
+
'(\\[\\s*(?P<nth>\\d+|last\\(\\s*\\))\\s*\\])?' +
|
24
|
+
')' +
|
25
|
+
')';
|
26
|
+
|
27
|
+
const subRegexes = {
|
28
|
+
tag: '([a-zA-Z][a-zA-Z0-9:-]*|\\*)',
|
29
|
+
attribute: '[.a-zA-Z_:][-\\w:.]*(\\(\\))?)',
|
30
|
+
value: '\\s*[\\w/:][-/\\w\\s,:;.]*',
|
31
|
+
};
|
32
|
+
|
33
|
+
Object.keys(subRegexes).forEach(key => {
|
34
|
+
regex = regex.replace(new RegExp(`%\\(${key}\\)s`, 'gi'), subRegexes[key]);
|
35
|
+
});
|
36
|
+
|
37
|
+
regex = regex.replace(
|
38
|
+
/\?P<node>|\?P<idvalue>|\?P<nav>|\?P<tag>|\?P<matched>|\?P<mattr>|\?P<mvalue>|\?P<contained>|\?P<cattr>|\?P<cvalue>|\?P<nth>/gi,
|
39
|
+
'',
|
40
|
+
);
|
41
|
+
|
42
|
+
return new RegExp(regex, 'gi');
|
43
|
+
};
|
44
|
+
|
45
|
+
const preParseXpath = expr =>
|
46
|
+
expr.replace(
|
47
|
+
/contains\s*\(\s*concat\(["']\s+["']\s*,\s*@class\s*,\s*["']\s+["']\)\s*,\s*["']\s+([a-zA-Z0-9-_]+)\s+["']\)/gi,
|
48
|
+
'@class="$1"',
|
49
|
+
);
|
50
|
+
|
51
|
+
function escapeCssIdSelectors(cssSelector) {
|
52
|
+
return cssSelector.replace(/#([^ >]+)/g, (match, id) => {
|
53
|
+
// Escape special characters in the id part
|
54
|
+
return `#${id.replace(/[!"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g, '\\$&')}`;
|
55
|
+
});
|
56
|
+
}
|
57
|
+
if (!expr) {
|
58
|
+
throw new Error('Missing XPath expression');
|
59
|
+
}
|
60
|
+
|
61
|
+
expr = preParseXpath(expr);
|
62
|
+
|
63
|
+
if (!isValidXPath(expr)) {
|
64
|
+
console.error(`Invalid or unsupported XPath: ${expr}`);
|
65
|
+
// do not throw error so that this function proceeds to convert xpath that it does not support
|
66
|
+
// for example, //*[@id="google_ads_iframe_/4654/dweb/imu1/homepage/landingpage/na_0"]/html/body/div[1]/a
|
67
|
+
// becomes #google_ads_iframe_/4654/dweb/imu1/homepage/landingpage/na_0 > html > body > div:first-of-type > div > a
|
68
|
+
// which is invalid because the slashes in the id selector are not escaped
|
69
|
+
// throw new Error('Invalid or unsupported XPath: ' + expr);
|
70
|
+
}
|
71
|
+
|
72
|
+
const xPathArr = expr.split('|');
|
73
|
+
const prog = getValidationRegex();
|
74
|
+
const cssSelectors = [];
|
75
|
+
let xindex = 0;
|
76
|
+
|
77
|
+
while (xPathArr[xindex]) {
|
78
|
+
const css = [];
|
79
|
+
let position = 0;
|
80
|
+
let nodes;
|
81
|
+
|
82
|
+
while ((nodes = prog.exec(xPathArr[xindex]))) {
|
83
|
+
let attr;
|
84
|
+
|
85
|
+
if (!nodes && position === 0) {
|
86
|
+
throw new Error(`Invalid or unsupported XPath: ${expr}`);
|
87
|
+
}
|
88
|
+
|
89
|
+
const match = {
|
90
|
+
node: nodes[5],
|
91
|
+
idvalue: nodes[12] || nodes[3],
|
92
|
+
nav: nodes[4],
|
93
|
+
tag: nodes[5],
|
94
|
+
matched: nodes[7],
|
95
|
+
mattr: nodes[10] || nodes[14],
|
96
|
+
mvalue: nodes[12] || nodes[16],
|
97
|
+
contained: nodes[13],
|
98
|
+
cattr: nodes[14],
|
99
|
+
cvalue: nodes[16],
|
100
|
+
nth: nodes[18],
|
101
|
+
};
|
102
|
+
|
103
|
+
let nav = '';
|
104
|
+
|
105
|
+
if (position != 0 && match.nav) {
|
106
|
+
if (~match.nav.indexOf('following-sibling::')) {
|
107
|
+
nav = ' + ';
|
108
|
+
} else {
|
109
|
+
nav = match.nav == '//' ? ' ' : ' > ';
|
110
|
+
}
|
111
|
+
}
|
112
|
+
|
113
|
+
const tag = match.tag === '*' ? '' : match.tag || '';
|
114
|
+
|
115
|
+
if (match.contained) {
|
116
|
+
if (match.cattr.indexOf('@') === 0) {
|
117
|
+
attr = `[${match.cattr.replace(/^@/, '')}*="${match.cvalue}"]`;
|
118
|
+
} else {
|
119
|
+
throw new Error(`Invalid or unsupported XPath attribute: ${match.cattr}`);
|
120
|
+
}
|
121
|
+
} else if (match.matched) {
|
122
|
+
switch (match.mattr) {
|
123
|
+
case '@id':
|
124
|
+
attr = `#${match.mvalue.replace(/^\s+|\s+$/, '').replace(/\s/g, '#')}`;
|
125
|
+
break;
|
126
|
+
case '@class':
|
127
|
+
attr = `.${match.mvalue.replace(/^\s+|\s+$/, '').replace(/\s/g, '.')}`;
|
128
|
+
break;
|
129
|
+
case 'text()':
|
130
|
+
case '.':
|
131
|
+
throw new Error(`Invalid or unsupported XPath attribute: ${match.mattr}`);
|
132
|
+
default:
|
133
|
+
if (match.mattr.indexOf('@') !== 0) {
|
134
|
+
throw new Error(`Invalid or unsupported XPath attribute: ${match.mattr}`);
|
135
|
+
}
|
136
|
+
if (match.mvalue.indexOf(' ') !== -1) {
|
137
|
+
match.mvalue = `\"${match.mvalue.replace(/^\s+|\s+$/, '')}\"`;
|
138
|
+
}
|
139
|
+
attr = `[${match.mattr.replace('@', '')}="${match.mvalue}"]`;
|
140
|
+
break;
|
141
|
+
}
|
142
|
+
} else if (match.idvalue) {
|
143
|
+
attr = `#${match.idvalue.replace(/\s/, '#')}`;
|
144
|
+
} else {
|
145
|
+
attr = '';
|
146
|
+
}
|
147
|
+
|
148
|
+
let nth = '';
|
149
|
+
|
150
|
+
if (match.nth) {
|
151
|
+
if (match.nth.indexOf('last') === -1) {
|
152
|
+
if (isNaN(parseInt(match.nth, 10))) {
|
153
|
+
throw new Error(`Invalid or unsupported XPath attribute: ${match.nth}`);
|
154
|
+
}
|
155
|
+
nth = parseInt(match.nth, 10) !== 1 ? `:nth-of-type(${match.nth})` : ':first-of-type';
|
156
|
+
} else {
|
157
|
+
nth = ':last-of-type';
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
css.push(nav + tag + attr + nth);
|
162
|
+
position++;
|
163
|
+
}
|
164
|
+
|
165
|
+
const result = css.join('');
|
166
|
+
|
167
|
+
if (result === '') {
|
168
|
+
throw new Error('Invalid or unsupported XPath');
|
169
|
+
}
|
170
|
+
|
171
|
+
cssSelectors.push(result);
|
172
|
+
xindex++;
|
173
|
+
}
|
174
|
+
|
175
|
+
// return cssSelectors.join(', ');
|
176
|
+
const originalResult = cssSelectors.join(', ');
|
177
|
+
return escapeCssIdSelectors(originalResult);
|
178
|
+
}
|
@@ -1,82 +0,0 @@
|
|
1
|
-
import { Spec } from 'axe-core';
|
2
|
-
|
3
|
-
// Custom Axe Functions for axe.config
|
4
|
-
export const customAxeConfig: Spec = {
|
5
|
-
branding: {
|
6
|
-
application: 'oobee',
|
7
|
-
},
|
8
|
-
checks: [
|
9
|
-
{
|
10
|
-
id: 'oobee-confusing-alt-text',
|
11
|
-
metadata: {
|
12
|
-
impact: 'serious',
|
13
|
-
messages: {
|
14
|
-
pass: 'The image alt text is probably useful.',
|
15
|
-
fail: "The image alt text set as 'img', 'image', 'picture', 'photo', or 'graphic' is confusing or not useful.",
|
16
|
-
},
|
17
|
-
},
|
18
|
-
},
|
19
|
-
{
|
20
|
-
id: 'oobee-accessible-label',
|
21
|
-
metadata: {
|
22
|
-
impact: 'serious',
|
23
|
-
messages: {
|
24
|
-
pass: 'The clickable element has an accessible label.',
|
25
|
-
fail: 'The clickable element does not have an accessible label.',
|
26
|
-
},
|
27
|
-
},
|
28
|
-
},
|
29
|
-
{
|
30
|
-
id: 'oobee-grading-text-contents',
|
31
|
-
metadata: {
|
32
|
-
impact: 'moderate',
|
33
|
-
messages: {
|
34
|
-
pass: 'The text content is easy to understand.',
|
35
|
-
fail: 'The text content is potentially difficult to undersatnd.',
|
36
|
-
},
|
37
|
-
},
|
38
|
-
},
|
39
|
-
],
|
40
|
-
rules: [
|
41
|
-
{ id: 'target-size', enabled: true },
|
42
|
-
{
|
43
|
-
id: 'oobee-confusing-alt-text',
|
44
|
-
selector: 'img[alt]',
|
45
|
-
enabled: true,
|
46
|
-
any: ['oobee-confusing-alt-text'],
|
47
|
-
tags: ['wcag2a', 'wcag111'],
|
48
|
-
metadata: {
|
49
|
-
description: 'Ensures image alt text is clear and useful.',
|
50
|
-
help: 'Image alt text must not be vague or unhelpful.',
|
51
|
-
helpUrl: 'https://www.deque.com/blog/great-alt-text-introduction/',
|
52
|
-
},
|
53
|
-
},
|
54
|
-
{
|
55
|
-
id: 'oobee-accessible-label',
|
56
|
-
// selector: '*', // to be set with the checker function output xpaths converted to css selectors
|
57
|
-
enabled: true,
|
58
|
-
any: ['oobee-accessible-label'],
|
59
|
-
tags: ['wcag2a', 'wcag211', 'wcag412'],
|
60
|
-
metadata: {
|
61
|
-
description: 'Ensures clickable elements have an accessible label.',
|
62
|
-
help: 'Clickable elements must have accessible labels.',
|
63
|
-
helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
|
64
|
-
},
|
65
|
-
},
|
66
|
-
{
|
67
|
-
id: 'oobee-grading-text-contents',
|
68
|
-
selector: 'html',
|
69
|
-
enabled: true,
|
70
|
-
any: ['oobee-grading-text-contents'],
|
71
|
-
tags: ['wcag2aaa', 'wcag315'],
|
72
|
-
metadata: {
|
73
|
-
description:
|
74
|
-
'Text content should be easy to understand for individuals with education levels up to university graduates. If the text content is difficult to understand, provide supplemental content or a version that is easy to understand.',
|
75
|
-
help: 'Text content should be clear and plain to ensure that it is easily understood.',
|
76
|
-
helpUrl: 'https://www.wcag.com/uncategorized/3-1-5-reading-level/',
|
77
|
-
},
|
78
|
-
},
|
79
|
-
],
|
80
|
-
};
|
81
|
-
|
82
|
-
export default customAxeConfig;
|