@govtechsg/oobee 0.10.36 → 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/DETAILS.md +3 -3
- package/INTEGRATION.md +142 -53
- package/README.md +15 -0
- 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/crawlers/commonCrawlerFunc.ts +66 -219
- 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 +467 -129
- 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/footer.ejs +3 -3
- 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/summaryMain.ejs +3 -3
- package/src/static/ejs/report.ejs +3 -3
- package/src/xPathToCssCypress.ts +178 -0
- package/src/crawlers/customAxeFunctions.ts +0 -82
package/src/npmIndex.ts
CHANGED
@@ -1,9 +1,10 @@
|
|
1
1
|
import fs from 'fs';
|
2
2
|
import path from 'path';
|
3
3
|
import printMessage from 'print-message';
|
4
|
-
import axe from 'axe-core';
|
4
|
+
import axe, { ImpactValue } from 'axe-core';
|
5
5
|
import { fileURLToPath } from 'url';
|
6
|
-
import
|
6
|
+
import { EnqueueStrategy } from 'crawlee';
|
7
|
+
import constants, { BrowserTypes, RuleFlags, ScannerTypes } from './constants/constants.js';
|
7
8
|
import {
|
8
9
|
deleteClonedProfiles,
|
9
10
|
getBrowserToRun,
|
@@ -17,21 +18,53 @@ import generateArtifacts from './mergeAxeResults.js';
|
|
17
18
|
import { takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
|
18
19
|
import { silentLogger } from './logs.js';
|
19
20
|
import { alertMessageOptions } from './constants/cliFunctions.js';
|
21
|
+
import { evaluateAltText } from './crawlers/custom/evaluateAltText.js';
|
22
|
+
import { escapeCssSelector } from './crawlers/custom/escapeCssSelector.js';
|
23
|
+
import { framesCheck } from './crawlers/custom/framesCheck.js';
|
24
|
+
import { findElementByCssSelector } from './crawlers/custom/findElementByCssSelector.js';
|
25
|
+
import { getAxeConfiguration } from './crawlers/custom/getAxeConfiguration.js';
|
26
|
+
import { flagUnlabelledClickableElements } from './crawlers/custom/flagUnlabelledClickableElements.js';
|
27
|
+
import { xPathToCss } from './crawlers/custom/xPathToCss.js';
|
28
|
+
import { extractText } from './crawlers/custom/extractText.js';
|
29
|
+
import { gradeReadability } from './crawlers/custom/gradeReadability.js';
|
20
30
|
|
21
31
|
const filename = fileURLToPath(import.meta.url);
|
22
32
|
const dirname = path.dirname(filename);
|
23
33
|
|
24
|
-
export const init = async (
|
34
|
+
export const init = async ({
|
25
35
|
entryUrl,
|
26
36
|
testLabel,
|
27
|
-
name
|
28
|
-
email
|
37
|
+
name,
|
38
|
+
email,
|
29
39
|
includeScreenshots = false,
|
30
40
|
viewportSettings = { width: 1000, height: 660 }, // cypress' default viewport settings
|
31
41
|
thresholds = { mustFix: undefined, goodToFix: undefined },
|
32
42
|
scanAboutMetadata = undefined,
|
33
|
-
zip =
|
34
|
-
|
43
|
+
zip = 'oobee-scan-results',
|
44
|
+
deviceChosen,
|
45
|
+
strategy = EnqueueStrategy.All,
|
46
|
+
ruleset = [RuleFlags.DEFAULT],
|
47
|
+
specifiedMaxConcurrency = 25,
|
48
|
+
followRobots = false,
|
49
|
+
}: {
|
50
|
+
entryUrl: string;
|
51
|
+
testLabel: string;
|
52
|
+
name: string;
|
53
|
+
email: string;
|
54
|
+
includeScreenshots?: boolean;
|
55
|
+
viewportSettings?: { width: number; height: number };
|
56
|
+
thresholds?: { mustFix: number; goodToFix: number };
|
57
|
+
scanAboutMetadata?: {
|
58
|
+
browser?: string;
|
59
|
+
viewport?: { width: number; height: number };
|
60
|
+
};
|
61
|
+
zip?: string;
|
62
|
+
deviceChosen?: string;
|
63
|
+
strategy?: EnqueueStrategy;
|
64
|
+
ruleset?: RuleFlags[];
|
65
|
+
specifiedMaxConcurrency?: number;
|
66
|
+
followRobots?: boolean;
|
67
|
+
}) => {
|
35
68
|
console.log('Starting Oobee');
|
36
69
|
|
37
70
|
const [date, time] = new Date().toLocaleString('sv').replaceAll(/-|:/g, '').split(' ');
|
@@ -39,6 +72,9 @@ export const init = async (
|
|
39
72
|
const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
|
40
73
|
const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
|
41
74
|
|
75
|
+
const disableOobee = ruleset.includes(RuleFlags.DISABLE_OOBEE);
|
76
|
+
const enableWcagAaa = ruleset.includes(RuleFlags.ENABLE_WCAG_AAA);
|
77
|
+
|
42
78
|
// max numbers of mustFix/goodToFix occurrences before test returns a fail
|
43
79
|
const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
|
44
80
|
|
@@ -47,9 +83,16 @@ export const init = async (
|
|
47
83
|
const scanDetails = {
|
48
84
|
startTime: new Date(),
|
49
85
|
endTime: new Date(),
|
50
|
-
|
86
|
+
deviceChosen,
|
87
|
+
crawlType: ScannerTypes.CUSTOM,
|
51
88
|
requestUrl: entryUrl,
|
52
89
|
urlsCrawled: { ...constants.urlsCrawledObj },
|
90
|
+
isIncludeScreenshots: includeScreenshots,
|
91
|
+
isAllowSubdomains: strategy,
|
92
|
+
isEnableCustomChecks: ruleset,
|
93
|
+
isEnableWcagAaa: ruleset,
|
94
|
+
isSlowScanMode: specifiedMaxConcurrency,
|
95
|
+
isAdhereRobots: followRobots,
|
53
96
|
};
|
54
97
|
|
55
98
|
const urlsCrawled = { ...constants.urlsCrawledObj };
|
@@ -73,66 +116,85 @@ export const init = async (
|
|
73
116
|
path.join(dirname, '../node_modules/axe-core/axe.min.js'),
|
74
117
|
'utf-8',
|
75
118
|
);
|
76
|
-
async function runA11yScan(elementsToScan = []) {
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
return false; // Fail the check if the alt text is confusing or not useful
|
94
|
-
}
|
95
|
-
}
|
96
|
-
|
97
|
-
return true; // Pass the check if the alt text seems appropriate
|
98
|
-
},
|
99
|
-
metadata: {
|
100
|
-
impact: 'serious', // Set the severity to serious
|
101
|
-
messages: {
|
102
|
-
pass: 'The image alt text is probably useful',
|
103
|
-
fail: "The image alt text set as 'img', 'image', 'picture', 'photo', or 'graphic' is confusing or not useful",
|
104
|
-
},
|
105
|
-
},
|
106
|
-
},
|
107
|
-
],
|
108
|
-
rules: [
|
109
|
-
{ id: 'target-size', enabled: true },
|
110
|
-
{
|
111
|
-
id: 'oobee-confusing-alt-text',
|
112
|
-
selector: 'img[alt]',
|
113
|
-
enabled: true,
|
114
|
-
any: ['oobee-confusing-alt-text'],
|
115
|
-
all: [],
|
116
|
-
none: [],
|
117
|
-
tags: ['wcag2a', 'wcag111'],
|
118
|
-
metadata: {
|
119
|
-
description: 'Ensures image alt text is clear and useful',
|
120
|
-
help: 'Image alt text must not be vague or unhelpful',
|
121
|
-
helpUrl: 'https://www.deque.com/blog/great-alt-text-introduction/',
|
122
|
-
},
|
123
|
-
},
|
124
|
-
],
|
125
|
-
});
|
119
|
+
async function runA11yScan(elementsToScan = [], gradingReadabilityFlag = '') {
|
120
|
+
const oobeeAccessibleLabelFlaggedXpaths = disableOobee
|
121
|
+
? []
|
122
|
+
: (await flagUnlabelledClickableElements()).map(item => item.xpath);
|
123
|
+
const oobeeAccessibleLabelFlaggedCssSelectors = oobeeAccessibleLabelFlaggedXpaths
|
124
|
+
.map(xpath => {
|
125
|
+
try {
|
126
|
+
const cssSelector = xPathToCss(xpath);
|
127
|
+
return cssSelector;
|
128
|
+
} catch (e) {
|
129
|
+
console.error('Error converting XPath to CSS: ', xpath, e);
|
130
|
+
return '';
|
131
|
+
}
|
132
|
+
})
|
133
|
+
.filter(item => item !== '');
|
134
|
+
|
135
|
+
axe.configure(getAxeConfiguration({ disableOobee, enableWcagAaa, gradingReadabilityFlag }));
|
126
136
|
const axeScanResults = await axe.run(elementsToScan, {
|
127
137
|
resultTypes: ['violations', 'passes', 'incomplete'],
|
128
138
|
});
|
139
|
+
|
140
|
+
// add custom Oobee violations
|
141
|
+
if (!disableOobee) {
|
142
|
+
// handle css id selectors that start with a digit
|
143
|
+
const escapedCssSelectors = oobeeAccessibleLabelFlaggedCssSelectors.map(escapeCssSelector);
|
144
|
+
|
145
|
+
// Add oobee violations to Axe's report
|
146
|
+
const oobeeAccessibleLabelViolations = {
|
147
|
+
id: 'oobee-accessible-label',
|
148
|
+
impact: 'serious' as ImpactValue,
|
149
|
+
tags: ['wcag2a', 'wcag211', 'wcag412'],
|
150
|
+
description: 'Ensures clickable elements have an accessible label.',
|
151
|
+
help: 'Clickable elements (i.e. elements with mouse-click interaction) must have accessible labels.',
|
152
|
+
helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
|
153
|
+
nodes: escapedCssSelectors
|
154
|
+
.map(cssSelector => ({
|
155
|
+
html: findElementByCssSelector(cssSelector),
|
156
|
+
target: [cssSelector],
|
157
|
+
impact: 'serious' as ImpactValue,
|
158
|
+
failureSummary:
|
159
|
+
'Fix any of the following:\n The clickable element does not have an accessible label.',
|
160
|
+
any: [
|
161
|
+
{
|
162
|
+
id: 'oobee-accessible-label',
|
163
|
+
data: null,
|
164
|
+
relatedNodes: [],
|
165
|
+
impact: 'serious',
|
166
|
+
message: 'The clickable element does not have an accessible label.',
|
167
|
+
},
|
168
|
+
],
|
169
|
+
all: [],
|
170
|
+
none: [],
|
171
|
+
}))
|
172
|
+
.filter(item => item.html),
|
173
|
+
};
|
174
|
+
|
175
|
+
axeScanResults.violations = [...axeScanResults.violations, oobeeAccessibleLabelViolations];
|
176
|
+
}
|
177
|
+
|
129
178
|
return {
|
130
179
|
pageUrl: window.location.href,
|
131
180
|
pageTitle: document.title,
|
132
181
|
axeScanResults,
|
133
182
|
};
|
134
183
|
}
|
135
|
-
return
|
184
|
+
return `
|
185
|
+
${axeScript}
|
186
|
+
${evaluateAltText.toString()}
|
187
|
+
${escapeCssSelector.toString()}
|
188
|
+
${framesCheck.toString()}
|
189
|
+
${findElementByCssSelector.toString()}
|
190
|
+
${flagUnlabelledClickableElements.toString()}
|
191
|
+
${xPathToCss.toString()}
|
192
|
+
${getAxeConfiguration.toString()}
|
193
|
+
${runA11yScan.toString()}
|
194
|
+
${extractText.toString()}
|
195
|
+
disableOobee=${disableOobee};
|
196
|
+
enableWcagAaa=${enableWcagAaa};
|
197
|
+
`;
|
136
198
|
};
|
137
199
|
|
138
200
|
const pushScanResults = async (res, metadata, elementsToClick) => {
|
@@ -142,7 +204,7 @@ export const init = async (
|
|
142
204
|
const { browserToRun, clonedBrowserDataDir } = getBrowserToRun(BrowserTypes.CHROME);
|
143
205
|
const browserContext = await constants.launcher.launchPersistentContext(
|
144
206
|
clonedBrowserDataDir,
|
145
|
-
{ viewport:
|
207
|
+
{ viewport: viewportSettings, ...getPlaywrightLaunchOptions(browserToRun) },
|
146
208
|
);
|
147
209
|
const page = await browserContext.newPage();
|
148
210
|
await page.goto(res.pageUrl);
|
@@ -210,16 +272,21 @@ export const init = async (
|
|
210
272
|
const pagesNotScanned = [
|
211
273
|
...scanDetails.urlsCrawled.error,
|
212
274
|
...scanDetails.urlsCrawled.invalid,
|
275
|
+
...scanDetails.urlsCrawled.forbidden,
|
276
|
+
...scanDetails.urlsCrawled.userExcluded,
|
213
277
|
];
|
214
278
|
const updatedScanAboutMetadata = {
|
215
|
-
viewport:
|
279
|
+
viewport: {
|
280
|
+
width: viewportSettings.width,
|
281
|
+
height: viewportSettings.height,
|
282
|
+
},
|
216
283
|
...scanAboutMetadata,
|
217
284
|
};
|
218
285
|
const basicFormHTMLSnippet = await generateArtifacts(
|
219
286
|
randomToken,
|
220
287
|
scanDetails.requestUrl,
|
221
288
|
scanDetails.crawlType,
|
222
|
-
|
289
|
+
deviceChosen,
|
223
290
|
scanDetails.urlsCrawled.scanned,
|
224
291
|
pagesNotScanned,
|
225
292
|
testLabel,
|
@@ -273,6 +340,7 @@ export const init = async (
|
|
273
340
|
|
274
341
|
return {
|
275
342
|
getScripts,
|
343
|
+
gradeReadability,
|
276
344
|
pushScanResults,
|
277
345
|
terminate,
|
278
346
|
scanDetails,
|
@@ -30,7 +30,7 @@
|
|
30
30
|
const urlScannedField = '64d49b567c3c460011feb8b5';
|
31
31
|
const encodedUrlScanned = encodeURIComponent(urlScanned);
|
32
32
|
const versionNumberField = '64dae8bca2eb61001284298f';
|
33
|
-
const encodedVersionNumber = encodeURIComponent(
|
33
|
+
const encodedVersionNumber = encodeURIComponent(oobeeAppVersion);
|
34
34
|
const aiFeedbackForm = `https://form.gov.sg/64d4a74da3a1e10012fd16a3/?${urlScannedField}=${encodedUrlScanned}&${versionNumberField}=${encodedVersionNumber}`;
|
35
35
|
%>
|
36
36
|
<a target="_blank" href="<%=aiFeedbackForm%>">Let us know if it helps!</a>
|
@@ -301,7 +301,7 @@
|
|
301
301
|
>
|
302
302
|
<path d="M6.49971 0.570312C1.32598 0.570312 0.544922 2.8166 0.544922 6.35557V10.5272C0.544922 14.0662 1.32598 16.3125 6.49971 16.3125C11.6734 16.3125 12.4545 14.0662 12.4545 10.5272V6.35557C12.4545 2.8166 11.6734 0.570312 6.49971 0.570312ZM7.37764 4.33027V13.2761H5.62178V3.60674H8.43721L7.37764 4.33027Z" fill="#9021A6"/>
|
303
303
|
</svg>
|
304
|
-
<span id="
|
304
|
+
<span id="oobeeAppVersion">N/A</span>
|
305
305
|
</li>
|
306
306
|
<li id = "cypressScanAboutMetadata">
|
307
307
|
</li>
|
@@ -3,12 +3,12 @@
|
|
3
3
|
<div class="col-sm-6 text-sm-start">
|
4
4
|
<%
|
5
5
|
const encodedUrlScanned = encodeURIComponent(urlScanned);
|
6
|
-
const encodedVersionNumber = encodeURIComponent(
|
6
|
+
const encodedVersionNumber = encodeURIComponent(oobeeAppVersion);
|
7
7
|
|
8
8
|
// Use %0A for line breaks in the body
|
9
9
|
const mailtoAddress = `oobee@wogaa.gov.sg`;
|
10
|
-
const mailtoSubject = encodeURIComponent(`Support Request - Oobee Version ${
|
11
|
-
const mailtoBody = encodeURIComponent(`URL Scanned: ${urlScanned}\nVersion: ${
|
10
|
+
const mailtoSubject = encodeURIComponent(`Support Request - Oobee Version ${oobeeAppVersion}`);
|
11
|
+
const mailtoBody = encodeURIComponent(`URL Scanned: ${urlScanned}\nVersion: ${oobeeAppVersion}`);
|
12
12
|
const feedbackEmail = `mailto:${mailtoAddress}?subject=${mailtoSubject}&body=${mailtoBody}`;
|
13
13
|
%>
|
14
14
|
|
@@ -93,104 +93,139 @@
|
|
93
93
|
}
|
94
94
|
|
95
95
|
function searchIssueDescription(category, filteredItems, isExactSearch, normalizedSearchVal) {
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
96
|
+
if (Array.isArray(filteredItems[category]?.rules)) {
|
97
|
+
filteredItems[category].rules = filteredItems[category].rules.filter(item => {
|
98
|
+
let normalizedDescription = item.description ? item.description.toLowerCase() : '';
|
99
|
+
return isExactSearch
|
100
|
+
? normalizedDescription === normalizedSearchVal.slice(1, -1)
|
101
|
+
: normalizedDescription.includes(normalizedSearchVal);
|
102
|
+
});
|
103
|
+
} else {
|
104
|
+
filteredItems[category].rules = [];
|
105
|
+
}
|
102
106
|
}
|
103
107
|
|
104
108
|
function searchPages(category, filteredItems, isExactSearch, normalizedSearchVal) {
|
105
|
-
|
106
|
-
|
107
|
-
|
109
|
+
normalizedSearchVal = normalizedSearchVal.trim().toLowerCase();
|
110
|
+
const exactSearchVal = normalizedSearchVal.slice(1, -1).trim();
|
111
|
+
|
112
|
+
// Split search terms into individual words for partial matching
|
113
|
+
const searchWords = normalizedSearchVal.split(/\s+/);
|
114
|
+
|
115
|
+
if (Array.isArray(filteredItems[category]?.rules)) {
|
116
|
+
filteredItems[category].rules = filteredItems[category].rules
|
117
|
+
.map(item => {
|
118
|
+
if (Array.isArray(item.pagesAffected)) {
|
119
|
+
item.pagesAffected = item.pagesAffected.filter(page => {
|
120
|
+
let normalizedPageUrl = page.url ? page.url.toLowerCase() : '';
|
121
|
+
let normalizedPageTitle = page.title ? page.title.toLowerCase() : '';
|
122
|
+
|
123
|
+
if (isExactSearch) {
|
124
|
+
return (
|
125
|
+
normalizedPageUrl === exactSearchVal ||
|
126
|
+
normalizedPageTitle === exactSearchVal
|
127
|
+
);
|
128
|
+
} else {
|
129
|
+
// Check each word separately for partial search
|
130
|
+
return searchWords.every(word =>
|
131
|
+
normalizedPageUrl.includes(word) || normalizedPageTitle.includes(word)
|
132
|
+
);
|
133
|
+
}
|
134
|
+
});
|
135
|
+
|
136
|
+
item.totalItems = item.pagesAffected.reduce(
|
137
|
+
(sum, page) => sum + (Array.isArray(page.items) ? page.items.length : 0),
|
138
|
+
0,
|
139
|
+
);
|
140
|
+
} else {
|
141
|
+
item.pagesAffected = [];
|
142
|
+
item.totalItems = 0;
|
143
|
+
}
|
144
|
+
return item;
|
145
|
+
})
|
146
|
+
.filter(item => item.pagesAffected.length > 0);
|
147
|
+
|
148
|
+
filteredItems[category].totalItems = filteredItems[category].rules.reduce(
|
149
|
+
(sum, rule) => sum + rule.totalItems,
|
150
|
+
0,
|
151
|
+
);
|
152
|
+
} else {
|
153
|
+
filteredItems[category].rules = [];
|
154
|
+
filteredItems[category].totalItems = 0;
|
155
|
+
}
|
156
|
+
}
|
157
|
+
|
158
|
+
function searchHtml(category, filteredItems, isExactSearch, normalizedSearchVal) {
|
159
|
+
normalizedSearchVal = normalizedSearchVal.replace(/\s+/g, '');
|
160
|
+
if (Array.isArray(filteredItems[category]?.rules)) {
|
161
|
+
filteredItems[category].rules.forEach(item => {
|
108
162
|
if (Array.isArray(item.pagesAffected)) {
|
109
|
-
item.pagesAffected
|
110
|
-
|
111
|
-
|
112
|
-
?
|
113
|
-
|
163
|
+
item.pagesAffected.forEach(page => {
|
164
|
+
// Update items array to only include items with xpath or html starting with searchVal
|
165
|
+
page.items = Array.isArray(page.items)
|
166
|
+
? page.items.filter(item => {
|
167
|
+
let normalizedHtml = item.html ? item.html.replace(/\s+/g, '').toLowerCase() : '';
|
168
|
+
let normalizedXpath = item.xpath ? item.xpath.replace(/\s+/g, '').toLowerCase() : '';
|
169
|
+
let filterHtml;
|
170
|
+
if (isExactSearch) {
|
171
|
+
filterHtml =
|
172
|
+
normalizedXpath === normalizedSearchVal.slice(1, -1) ||
|
173
|
+
normalizedHtml === normalizedSearchVal.slice(1, -1);
|
174
|
+
} else {
|
175
|
+
filterHtml =
|
176
|
+
normalizedXpath.includes(normalizedSearchVal) ||
|
177
|
+
normalizedHtml.includes(normalizedSearchVal);
|
178
|
+
}
|
179
|
+
return filterHtml;
|
180
|
+
})
|
181
|
+
: [];
|
114
182
|
});
|
183
|
+
// Update totalItems to be the sum of the number of elements in the items array
|
115
184
|
item.totalItems = item.pagesAffected.reduce(
|
116
185
|
(sum, page) => sum + (Array.isArray(page.items) ? page.items.length : 0),
|
117
186
|
0,
|
118
187
|
);
|
119
|
-
} else {
|
120
|
-
item.pagesAffected = [];
|
121
|
-
item.totalItems = 0;
|
122
188
|
}
|
123
|
-
|
124
|
-
})
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
// Update items array to only include items with xpath or html starting with searchVal
|
139
|
-
page.items = Array.isArray(page.items)
|
140
|
-
? page.items.filter(item => {
|
141
|
-
let normalizedHtml = item.html.replace(/\s+/g, '').toLowerCase();
|
142
|
-
let normalizedXpath = item.xpath.replace(/\s+/g, '').toLowerCase();
|
143
|
-
let filterHtml;
|
144
|
-
if (isExactSearch) {
|
145
|
-
filterHtml =
|
146
|
-
normalizedXpath === normalizedSearchVal.slice(1, -1) ||
|
147
|
-
normalizedHtml === normalizedSearchVal.slice(1, -1);
|
148
|
-
} else {
|
149
|
-
filterHtml =
|
150
|
-
normalizedXpath.includes(normalizedSearchVal) ||
|
151
|
-
normalizedHtml.includes(normalizedSearchVal);
|
152
|
-
}
|
153
|
-
return filterHtml;
|
154
|
-
})
|
155
|
-
: [];
|
156
|
-
});
|
157
|
-
// Update totalItems to be the sum of the number of elements in the items array
|
158
|
-
item.totalItems = item.pagesAffected.reduce(
|
159
|
-
(sum, page) => sum + (Array.isArray(page.items) ? page.items.length : 0),
|
160
|
-
0,
|
161
|
-
);
|
162
|
-
}
|
163
|
-
});
|
164
|
-
filteredItems[category].rules = filteredItems[category].rules.filter(
|
165
|
-
rule => rule.totalItems > 0,
|
166
|
-
);
|
167
|
-
// Update the totalItems value for the category
|
168
|
-
filteredItems[category].totalItems = filteredItems[category].rules.reduce(
|
169
|
-
(sum, rule) => sum + rule.totalItems,
|
170
|
-
0,
|
171
|
-
);
|
189
|
+
|
190
|
+
});
|
191
|
+
|
192
|
+
filteredItems[category].rules = filteredItems[category].rules.filter(
|
193
|
+
rule => rule.totalItems > 0,
|
194
|
+
);
|
195
|
+
// Update the totalItems value for the category
|
196
|
+
filteredItems[category].totalItems = filteredItems[category].rules.reduce(
|
197
|
+
(sum, rule) => sum + rule.totalItems,
|
198
|
+
0,
|
199
|
+
);
|
200
|
+
} else {
|
201
|
+
filteredItems[category].rules = [];
|
202
|
+
filteredItems[category].totalItems = 0;
|
203
|
+
}
|
172
204
|
}
|
173
205
|
|
174
206
|
function updateIssueOccurrence(category, filteredItems) {
|
175
207
|
//update no. of issues/occurances for each category
|
176
|
-
let rules = filteredItems[category].rules;
|
208
|
+
let rules = Array.isArray(filteredItems[category]?.rules) ? filteredItems[category].rules : [];
|
177
209
|
let totalItemsSum = rules.reduce((sum, rule) => sum + rule.totalItems, 0);
|
178
210
|
filteredItems[category].totalItems = totalItemsSum;
|
179
211
|
let updatedIssueOccurrence = '';
|
180
212
|
|
181
213
|
// Determine the correct singular/plural form for 'issue' and 'occurrence'
|
182
|
-
const issueLabel = filteredItems[category].rules.length === 1 ? 'issue' : 'issues';
|
214
|
+
const issueLabel = Array.isArray(filteredItems[category].rules) && filteredItems[category].rules.length === 1 ? 'issue' : 'issues';
|
183
215
|
const occurrenceLabel = filteredItems[category].totalItems === 1 ? 'occurrence' : 'occurrences';
|
184
216
|
|
185
217
|
if (category !== 'passed' && filteredItems[category].totalItems !== 0) {
|
186
|
-
|
218
|
+
const rulesLength = filteredItems[category].rules ? filteredItems[category].rules.length : 0;
|
219
|
+
updatedIssueOccurrence = `<strong style="color: #006b8c;">${rulesLength}</strong> ${issueLabel} / <strong style="color: #006b8c;">${filteredItems[category].totalItems}</strong> ${occurrenceLabel}`;
|
187
220
|
} else if (category !== 'passed' && filteredItems[category].totalItems === 0) {
|
188
221
|
updatedIssueOccurrence = `<strong style="color: #006b8c;">0</strong> issues`;
|
189
222
|
} else {
|
190
223
|
updatedIssueOccurrence = `<strong style="color: #006b8c;">${filteredItems[category].totalItems}</strong> ${occurrenceLabel}`;
|
191
224
|
}
|
192
|
-
if (category !== 'passed')
|
193
|
-
document.getElementById(`${category}ItemsInformation`)
|
225
|
+
if (category !== 'passed') {
|
226
|
+
const element = document.getElementById(`${category}ItemsInformation`);
|
227
|
+
if (element) element.innerHTML = updatedIssueOccurrence;
|
228
|
+
}
|
194
229
|
}
|
195
230
|
|
196
231
|
function resetIssueOccurrence(filteredItems) {
|
@@ -207,8 +242,11 @@
|
|
207
242
|
} else {
|
208
243
|
updatedIssueOccurrence = `${filteredItems[category].totalItems} ${occurrenceLabel}`;
|
209
244
|
}
|
210
|
-
|
211
|
-
|
245
|
+
|
246
|
+
if (category !== 'passed') {
|
247
|
+
const elem = document.getElementById(`${category}ItemsInformation`);
|
248
|
+
if (elem) elem.innerHTML = updatedIssueOccurrence;
|
249
|
+
}
|
212
250
|
}
|
213
251
|
}
|
214
252
|
|
@@ -246,4 +284,4 @@
|
|
246
284
|
document.getElementById('expandedRuleSearchWarning').appendChild(warningDiv);
|
247
285
|
}
|
248
286
|
}
|
249
|
-
</script>
|
287
|
+
</script>
|
@@ -113,8 +113,8 @@ category summary is clicked %>
|
|
113
113
|
const comboboxCategorySelectors = [];
|
114
114
|
|
115
115
|
Object.keys(filteredItems).forEach(category => {
|
116
|
-
const ruleInCategory = filteredItems[category]
|
117
|
-
|
116
|
+
const ruleInCategory = filteredItems[category]?.rules?.find(r => r.rule === selectedRule.rule);
|
117
|
+
|
118
118
|
if (ruleInCategory !== undefined && category !== 'passed') {
|
119
119
|
if (category !== 'passed') {
|
120
120
|
availableFixCategories.push(category);
|
@@ -37,8 +37,8 @@
|
|
37
37
|
<h2 class="mb-2">Summary of issues:</h2>
|
38
38
|
<p>
|
39
39
|
Only
|
40
|
-
<a href="https://go.gov.sg/oobee-details" target="
|
41
|
-
|
40
|
+
<a href="https://go.gov.sg/oobee-details" target="_blank">20 WCAG 2.2 Success Criteria (A & AA)</a>
|
41
|
+
can be automatically checked so
|
42
42
|
<a aria-label="Manual testing guide" href="https://go.gov.sg/a11y-manual-testing" target="_blank">manual
|
43
43
|
testing</a>
|
44
44
|
is still required. For more details, please refer to the HTML report.
|
@@ -46,4 +46,4 @@
|
|
46
46
|
</div>
|
47
47
|
<%- include("components/summaryTable") %>
|
48
48
|
</main>
|
49
|
-
</div>
|
49
|
+
</div>
|
@@ -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
|
}
|