@govtechsg/oobee 0.10.36 → 0.10.42

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.
Files changed (39) hide show
  1. package/.github/workflows/docker-test.yml +1 -1
  2. package/DETAILS.md +3 -3
  3. package/INTEGRATION.md +142 -53
  4. package/README.md +17 -0
  5. package/REPORTS.md +362 -0
  6. package/exclusions.txt +4 -1
  7. package/package.json +2 -2
  8. package/src/constants/cliFunctions.ts +0 -7
  9. package/src/constants/common.ts +39 -1
  10. package/src/constants/constants.ts +9 -8
  11. package/src/crawlers/commonCrawlerFunc.ts +95 -220
  12. package/src/crawlers/crawlDomain.ts +10 -23
  13. package/src/crawlers/crawlLocalFile.ts +2 -0
  14. package/src/crawlers/crawlSitemap.ts +6 -4
  15. package/src/crawlers/custom/escapeCssSelector.ts +10 -0
  16. package/src/crawlers/custom/evaluateAltText.ts +13 -0
  17. package/src/crawlers/custom/extractAndGradeText.ts +0 -2
  18. package/src/crawlers/custom/extractText.ts +28 -0
  19. package/src/crawlers/custom/findElementByCssSelector.ts +46 -0
  20. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +982 -842
  21. package/src/crawlers/custom/framesCheck.ts +51 -0
  22. package/src/crawlers/custom/getAxeConfiguration.ts +126 -0
  23. package/src/crawlers/custom/gradeReadability.ts +30 -0
  24. package/src/crawlers/custom/xPathToCss.ts +178 -0
  25. package/src/crawlers/pdfScanFunc.ts +67 -26
  26. package/src/mergeAxeResults.ts +535 -132
  27. package/src/npmIndex.ts +130 -62
  28. package/src/screenshotFunc/htmlScreenshotFunc.ts +1 -1
  29. package/src/screenshotFunc/pdfScreenshotFunc.ts +34 -1
  30. package/src/static/ejs/partials/components/ruleOffcanvas.ejs +1 -1
  31. package/src/static/ejs/partials/components/scanAbout.ejs +1 -1
  32. package/src/static/ejs/partials/footer.ejs +3 -3
  33. package/src/static/ejs/partials/scripts/reportSearch.ejs +112 -74
  34. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +2 -2
  35. package/src/static/ejs/partials/summaryMain.ejs +3 -3
  36. package/src/static/ejs/report.ejs +3 -3
  37. package/src/utils.ts +289 -13
  38. package/src/xPathToCssCypress.ts +178 -0
  39. 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 constants, { BrowserTypes } from './constants/constants.js';
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 = 'Your Name',
28
- email = 'email@domain.com',
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 = undefined,
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
- crawlType: 'Custom',
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
- axe.configure({
78
- branding: {
79
- application: 'oobee',
80
- },
81
- // Add custom img alt text check
82
- checks: [
83
- {
84
- id: 'oobee-confusing-alt-text',
85
- evaluate(node: HTMLElement) {
86
- const altText = node.getAttribute('alt');
87
- const confusingTexts = ['img', 'image', 'picture', 'photo', 'graphic'];
88
-
89
- if (altText) {
90
- const trimmedAltText = altText.trim().toLowerCase();
91
- // Check if the alt text exactly matches one of the confusingTexts
92
- if (confusingTexts.some(text => text === trimmedAltText)) {
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 `${axeScript} ${runA11yScan.toString()}`;
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: scanAboutMetadata.viewport, ...getPlaywrightLaunchOptions(browserToRun) },
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: `${viewportSettings.width} x ${viewportSettings.height}`,
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
- updatedScanAboutMetadata.viewport,
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,
@@ -14,7 +14,7 @@ export const takeScreenshotForHTMLElements = async (
14
14
  page: Page,
15
15
  randomToken: string,
16
16
  locatorTimeout = 2000,
17
- maxScreenshots = 50,
17
+ maxScreenshots = 100,
18
18
  ): Promise<ResultWithScreenshot[]> => {
19
19
  const newViolations: ResultWithScreenshot[] = [];
20
20
  let screenshotCount = 0;
@@ -1,3 +1,10 @@
1
+ // Monkey patch Path2D to avoid PDF.js crashing
2
+ (globalThis as any).Path2D = class {
3
+ constructor(_path?: string) {}
4
+ rect(_x: number, _y: number, _width: number, _height: number) {}
5
+ addPath(_path: any, _transform?: any) {}
6
+ };
7
+
1
8
  import _ from 'lodash';
2
9
  import { getDocument, PDFPageProxy } from 'pdfjs-dist';
3
10
  import fs from 'fs';
@@ -25,11 +32,36 @@ interface pathObject {
25
32
  annot?: number;
26
33
  }
27
34
 
35
+ // Use safe canvas to avoid Path2D issues
36
+ function createSafeCanvas(width: number, height: number) {
37
+ const canvas = createCanvas(width, height);
38
+ const ctx = canvas.getContext('2d');
39
+
40
+ // Patch clip/stroke/fill/etc. to skip if Path2D is passed
41
+ const wrapIgnorePath2D = (fn: Function) =>
42
+ function (...args: any[]) {
43
+ if (args.length > 0 && args[0] instanceof (globalThis as any).Path2D) {
44
+ // Skip the operation
45
+ return;
46
+ }
47
+ return fn.apply(this, args);
48
+ };
49
+
50
+ ctx.clip = wrapIgnorePath2D(ctx.clip);
51
+ ctx.fill = wrapIgnorePath2D(ctx.fill);
52
+ ctx.stroke = wrapIgnorePath2D(ctx.stroke);
53
+ ctx.isPointInPath = wrapIgnorePath2D(ctx.isPointInPath);
54
+ ctx.isPointInStroke = wrapIgnorePath2D(ctx.isPointInStroke);
55
+
56
+ return canvas;
57
+ }
58
+
59
+ // CanvasFactory for Node.js
28
60
  function NodeCanvasFactory() {}
29
61
  NodeCanvasFactory.prototype = {
30
62
  create: function NodeCanvasFactory_create(width: number, height: number) {
31
63
  assert(width > 0 && height > 0, 'Invalid canvas size');
32
- const canvas = createCanvas(width, height);
64
+ const canvas = createSafeCanvas(width, height);
33
65
  const context = canvas.getContext('2d');
34
66
  return {
35
67
  canvas,
@@ -69,6 +101,7 @@ export async function getPdfScreenshots(
69
101
  const newItems = _.cloneDeep(items);
70
102
  const loadingTask = getDocument({
71
103
  url: pdfFilePath,
104
+ canvasFactory,
72
105
  standardFontDataUrl: path.join(dirname, '../node_modules/pdfjs-dist/standard_fonts/'),
73
106
  disableFontFace: true,
74
107
  verbosity: 0,
@@ -30,7 +30,7 @@
30
30
  const urlScannedField = '64d49b567c3c460011feb8b5';
31
31
  const encodedUrlScanned = encodeURIComponent(urlScanned);
32
32
  const versionNumberField = '64dae8bca2eb61001284298f';
33
- const encodedVersionNumber = encodeURIComponent(phAppVersion);
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="phAppVersion">N/A</span>
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(phAppVersion);
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 ${phAppVersion}`);
11
- const mailtoBody = encodeURIComponent(`URL Scanned: ${urlScanned}\nVersion: ${phAppVersion}`);
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
- filteredItems[category].rules = filteredItems[category].rules.filter(item => {
97
- let normalizedDescription = item.description.toLowerCase();
98
- return isExactSearch
99
- ? normalizedDescription === normalizedSearchVal.slice(1, -1)
100
- : normalizedDescription.includes(normalizedSearchVal);
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
- // Filter pagesAffected array to only include pages with URLs that match the searchTerm
106
- filteredItems[category].rules = filteredItems[category].rules
107
- .map(item => {
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 = item.pagesAffected.filter(page => {
110
- let normalizedPageUrl = page.url.toLowerCase();
111
- return isExactSearch
112
- ? normalizedPageUrl === normalizedSearchVal.slice(1, -1)
113
- : normalizedPageUrl.includes(normalizedSearchVal);
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
- return item;
124
- })
125
- .filter(item => item.pagesAffected.length > 0);
126
- // Update the totalItems value for the category
127
- filteredItems[category].totalItems = filteredItems[category].rules.reduce(
128
- (sum, rule) => sum + rule.totalItems,
129
- 0,
130
- );
131
- }
132
-
133
- function searchHtml(category, filteredItems, isExactSearch, normalizedSearchVal) {
134
- normalizedSearchVal = normalizedSearchVal.replace(/\s+/g, '');
135
- filteredItems[category].rules.forEach(item => {
136
- if (Array.isArray(item.pagesAffected)) {
137
- item.pagesAffected.forEach(page => {
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
- updatedIssueOccurrence = `<strong style="color: #006b8c;">${filteredItems[category].rules.length}</strong> ${issueLabel} / <strong style="color: #006b8c;">${filteredItems[category].totalItems}</strong> ${occurrenceLabel}`;
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`).innerHTML = updatedIssueOccurrence;
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
- if (category !== 'passed')
211
- document.getElementById(`${category}ItemsInformation`).innerHTML = updatedIssueOccurrence;
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].rules.find(r => r.rule === selectedRule.rule);
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=" _blank">20 WCAG 2.2</a>
41
- Success Criteria (A & AA) can be automatically checked so
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('phAppVersion');
253
- var versionContent = 'Oobee Version ' + scanData.phAppVersion;
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
  }