@govtechsg/oobee 0.10.86 → 0.10.88

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 (61) hide show
  1. package/.github/workflows/docker-push-ghcr.yml +49 -0
  2. package/.github/workflows/image.yml +2 -3
  3. package/DETAILS_OUTPUT_EXAMPLES.md +178 -0
  4. package/Dockerfile +6 -7
  5. package/dist/cli.js +18 -5
  6. package/dist/combine.js +3 -0
  7. package/dist/constants/cliFunctions.js +2 -2
  8. package/dist/constants/common.js +55 -13
  9. package/dist/crawlers/commonCrawlerFunc.js +523 -2
  10. package/dist/crawlers/crawlDomain.js +38 -13
  11. package/dist/crawlers/crawlIntelligentSitemap.js +62 -30
  12. package/dist/crawlers/crawlLocalFile.js +2 -2
  13. package/dist/crawlers/crawlSitemap.js +44 -5
  14. package/dist/crawlers/custom/extractAndGradeText.js +1 -1
  15. package/dist/crawlers/custom/getAxeConfiguration.js +26 -21
  16. package/dist/crawlers/custom/gradeReadability.js +1 -1
  17. package/dist/crawlers/custom/utils.js +81 -40
  18. package/dist/generateHtmlReport.js +18 -11
  19. package/dist/mergeAxeResults/itemReferences.js +60 -25
  20. package/dist/mergeAxeResults/sentryTelemetry.js +4 -1
  21. package/dist/mergeAxeResults.js +18 -9
  22. package/dist/npmIndex.js +16 -12
  23. package/dist/screenshotFunc/htmlScreenshotFunc.js +67 -0
  24. package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
  25. package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +45 -6
  26. package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +8 -5
  27. package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
  28. package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +2 -1
  29. package/dist/static/ejs/summary.ejs +18 -12
  30. package/dist/utils.js +4 -3
  31. package/examples/oobee-test-details-runner.js +214 -0
  32. package/examples/test-violations.html +42 -0
  33. package/fix-summary-html-oom-pr.md +62 -0
  34. package/package.json +5 -5
  35. package/src/cli.ts +19 -5
  36. package/src/combine.ts +3 -0
  37. package/src/constants/cliFunctions.ts +2 -2
  38. package/src/constants/common.ts +65 -12
  39. package/src/crawlers/commonCrawlerFunc.ts +625 -2
  40. package/src/crawlers/crawlDomain.ts +39 -13
  41. package/src/crawlers/crawlIntelligentSitemap.ts +63 -30
  42. package/src/crawlers/crawlLocalFile.ts +4 -1
  43. package/src/crawlers/crawlSitemap.ts +50 -3
  44. package/src/crawlers/custom/extractAndGradeText.ts +1 -1
  45. package/src/crawlers/custom/getAxeConfiguration.ts +25 -23
  46. package/src/crawlers/custom/gradeReadability.ts +1 -1
  47. package/src/crawlers/custom/utils.ts +99 -43
  48. package/src/generateHtmlReport.ts +21 -11
  49. package/src/mergeAxeResults/itemReferences.ts +70 -26
  50. package/src/mergeAxeResults/sentryTelemetry.ts +4 -1
  51. package/src/mergeAxeResults.ts +21 -11
  52. package/src/npmIndex.ts +17 -12
  53. package/src/screenshotFunc/htmlScreenshotFunc.ts +81 -1
  54. package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
  55. package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +45 -6
  56. package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +8 -5
  57. package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
  58. package/src/static/ejs/partials/scripts/ruleModal/utilities.ejs +2 -1
  59. package/src/static/ejs/summary.ejs +18 -12
  60. package/src/utils.ts +4 -3
  61. package/testStaticJSScanner.html +1 -1
@@ -28,8 +28,11 @@ async function decodeUnzipParse(input) {
28
28
  offset += arr.length;
29
29
  }
30
30
 
31
- // Step 2: Decompress with pako (GZIP)
32
- const decompressed = pako.ungzip(merged, { to: 'string' });
31
+ // Step 2: Decompress with pako (GZIP) to bytes first to avoid large-string
32
+ // construction inside pako for very large payloads.
33
+ const decompressedBytes = pako.ungzip(merged);
34
+
35
+ const decompressed = new TextDecoder().decode(decompressedBytes);
33
36
 
34
37
  // Step 3: Parse JSON
35
38
  return JSON.parse(decompressed);
@@ -37,4 +40,4 @@ async function decodeUnzipParse(input) {
37
40
  throw new Error(`Failed to decode/unzip/parse: ${err.message}`);
38
41
  }
39
42
  }
40
- </script>
43
+ </script>
@@ -1,10 +1,44 @@
1
1
  <script>
2
2
  /**
3
- * Resolves item references (composite "html\x00xpath" strings) to full item data using htmlGroups.
3
+ * Rebuilds the item list for a page from pre-computed htmlGroups when the light report omits page.items.
4
+ */
5
+ function buildItemsFromHtmlGroupsForPage(page, ruleInCategory) {
6
+ const htmlGroups = ruleInCategory.htmlGroups || {};
7
+ const resolvedItems = [];
8
+
9
+ Object.values(htmlGroups).forEach(groupData => {
10
+ if (!Array.isArray(groupData.pageUrls) || !groupData.pageUrls.includes(page.url)) {
11
+ return;
12
+ }
13
+
14
+ resolvedItems.push({
15
+ html: groupData.html,
16
+ xpath: groupData.xpath,
17
+ message: groupData.message,
18
+ screenshotPath: groupData.screenshotPath,
19
+ displayNeedsReview: groupData.displayNeedsReview,
20
+ pageUrl: page.url,
21
+ pageTitle: page.pageTitle || page.metadata
22
+ });
23
+ });
24
+
25
+ return resolvedItems;
26
+ }
27
+
28
+ /**
29
+ * The embedded report payload now omits page.items and rebuilds occurrences from
30
+ * htmlGroups + page metadata. Keep the older page.items resolution logic below
31
+ * commented for an easy rollback if we need to restore mixed payload support.
4
32
  */
5
33
  function resolveItemReferencesForPage(page, ruleInCategory) {
34
+ return buildItemsFromHtmlGroupsForPage(page, ruleInCategory);
35
+
36
+ /*
6
37
  const items = page.items || [];
7
- if (items.length === 0) return [];
38
+
39
+ if (items.length === 0) {
40
+ return buildItemsFromHtmlGroupsForPage(page, ruleInCategory);
41
+ }
8
42
 
9
43
  const isReference = typeof items[0] === 'string';
10
44
 
@@ -27,6 +61,7 @@
27
61
  pageTitle: page.pageTitle || page.metadata
28
62
  };
29
63
  }
64
+
30
65
  // Fallback: parse composite key
31
66
  const nullByteIndex = compositeKey.indexOf('\x00');
32
67
  const html = nullByteIndex !== -1 ? compositeKey.slice(0, nullByteIndex) : compositeKey;
@@ -40,6 +75,7 @@
40
75
  pageTitle: page.pageTitle || page.metadata
41
76
  };
42
77
  });
78
+ */
43
79
  }
44
80
 
45
81
  function buildItemCardsWithPagination(accordionId, category, ruleInCategory, page, index) {
@@ -203,14 +239,17 @@
203
239
  ${item.xpath ? createXpathSection(item.xpath) : ''}
204
240
  ${createElementSection(item)}
205
241
  ${
206
- item.displayNeedsReview
207
- ? `<div class="d-flex justify-content-between g-one">
242
+ (() => {
243
+ const showDetails = item.displayNeedsReview || ['color-contrast', 'color-contrast-enhanced', 'oobee-grading-text-contents', 'target-size', 'valid-lang'].includes(aiConfig.ruleInCategory.rule);
244
+ return showDetails && item.message
245
+ ? `<div class="d-flex justify-content-between g-one">
208
246
  <div class="fw-semibold page-item-card-section-title">Details</div>
209
247
  <div class="page-item-card-section-content">
210
- ${generateItemMessageElement(item.displayNeedsReview, item.message)}
248
+ ${generateItemMessageElement(item.displayNeedsReview || true, item.message)}
211
249
  </div>
212
250
  </div>`
213
- : ''
251
+ : '';
252
+ })()
214
253
  }
215
254
  ${isPurpleAiRule ? createAiSuggestionSection(item, oobeeAiQueryLabel, aiConfig) : ''}
216
255
  ${showGenAiUI ? createGenAiSuggestFixSection(item, aiConfig) : ''}
@@ -86,7 +86,7 @@
86
86
  // Use pre-computed htmlGroups for count if available, otherwise use pages
87
87
  const count = isHtmlGrouping && selectedCategory.htmlGroups
88
88
  ? Object.keys(selectedCategory.htmlGroups).length
89
- : selectedCategory.pagesAffected.length;
89
+ : (selectedCategory.pagesAffectedCount || selectedCategory.pagesAffected.length);
90
90
  if (isHtmlGrouping) {
91
91
  dropdownTitle.innerText = `HTML elements affected by this issue (${count})`;
92
92
  } else {
@@ -393,14 +393,17 @@
393
393
  </div>
394
394
 
395
395
  ${
396
- item.displayNeedsReview
397
- ? `<div class="d-flex justify-content-between g-one modal-view">
396
+ (() => {
397
+ const showDetails = item.displayNeedsReview || ['color-contrast', 'color-contrast-enhanced', 'oobee-grading-text-contents', 'target-size', 'valid-lang'].includes(aiConfig.ruleInCategory.rule);
398
+ return showDetails && item.message
399
+ ? `<div class="d-flex justify-content-between g-one modal-view">
398
400
  <div class="fw-semibold page-item-card-section-title">Details</div>
399
401
  <div class="page-item-card-section-content">
400
- ${generateItemMessageElement(item.displayNeedsReview, item.message)}
402
+ ${generateItemMessageElement(item.displayNeedsReview || true, item.message)}
401
403
  </div>
402
404
  </div>`
403
- : ''
405
+ : '';
406
+ })()
404
407
  }
405
408
  ${isPurpleAiRule ? createAiSuggestionSection(item, oobeeAiQueryLabel, aiConfig) : ''}
406
409
  </div>
@@ -270,8 +270,8 @@ include('./pageAccordionBuilder') %> <%- include('./constants') %>
270
270
  if (!Array.isArray(rule.pagesAffected)) return;
271
271
 
272
272
  rule.pagesAffected.sort((a, b) => {
273
- const lenA = Array.isArray(a.items) ? a.items.length : 0;
274
- const lenB = Array.isArray(b.items) ? b.items.length : 0;
273
+ const lenA = Array.isArray(a.items) ? a.items.length : a.itemsCount || 0;
274
+ const lenB = Array.isArray(b.items) ? b.items.length : b.itemsCount || 0;
275
275
  return lenB - lenA; // DESC
276
276
  });
277
277
  });
@@ -295,10 +295,10 @@ include('./pageAccordionBuilder') %> <%- include('./constants') %>
295
295
  dropdownToggle.innerText = `${ruleInCategory.totalItems} Total occ.`;
296
296
  dropdownToggle.setAttribute('aria-label', occurrencesText);
297
297
  document.getElementById('expandedRuleDropdownTitle').innerText =
298
- `Pages affected by this issue (${ruleInCategory.pagesAffected.length})`;
298
+ `Pages affected by this issue (${(ruleInCategory.pagesAffectedCount || ruleInCategory.pagesAffected.length)})`;
299
299
  buildExpandedRuleCategoryContent(category, ruleInCategory);
300
300
  document.getElementById('expandedRulePageContent').innerText =
301
- `Total ${ruleInCategory.pagesAffected.length} affected pages`;
301
+ `Total ${(ruleInCategory.pagesAffectedCount || ruleInCategory.pagesAffected.length)} affected pages`;
302
302
  }
303
303
  }
304
304
  });
@@ -53,7 +53,8 @@
53
53
  if (htmlEscapedMessageArray.length === 1) {
54
54
  return `<p class="mb-0">${htmlEscapedMessageArray[0]}</p>`;
55
55
  } else {
56
- return `<ul>${htmlEscapedMessageArray.map(m => `<li>${m}</li>`).join('')}</ul>`;
56
+ const [first, ...rest] = htmlEscapedMessageArray;
57
+ return `<p class="mb-0">${first}</p><ul>${rest.map(m => `<li>${m}</li>`).join('')}</ul>`;
57
58
  }
58
59
  } else {
59
60
  let i = 0;
@@ -21,18 +21,24 @@
21
21
  %>
22
22
  <script>
23
23
  const scanItems = <%- JSON.stringify(
24
- {
25
- ...items,
26
- ...['mustFix','goodToFix','needsReview','passed'].reduce((acc, cat) => {
27
- if (items[cat]) {
28
- acc[cat] = {
29
- ...items[cat],
30
- rules: (items[cat].rules || []).map(({ htmlGroups, ...rest }) => rest),
31
- };
32
- }
33
- return acc;
34
- }, {}),
35
- }
24
+ ['mustFix','goodToFix','needsReview','passed'].reduce((acc, cat) => {
25
+ if (items[cat]) {
26
+ acc[cat] = {
27
+ description: items[cat].description,
28
+ totalItems: items[cat].totalItems,
29
+ totalRuleIssues: items[cat].totalRuleIssues,
30
+ rules: (items[cat].rules || []).map(rule => ({
31
+ rule: rule.rule,
32
+ description: rule.description,
33
+ helpUrl: rule.helpUrl,
34
+ conformance: rule.conformance,
35
+ totalItems: rule.totalItems,
36
+ pagesAffected: { length: (rule.pagesAffected || []).length },
37
+ })),
38
+ };
39
+ }
40
+ return acc;
41
+ }, {})
36
42
  ).replace(/<\//g, '<\\/') %>
37
43
  </script>
38
44
  <%- include('partials/scripts/summaryTable') %>
package/src/utils.ts CHANGED
@@ -5,6 +5,7 @@ import fs from 'fs-extra';
5
5
  import axe, { Rule } from 'axe-core';
6
6
  import { v4 as uuidv4 } from 'uuid';
7
7
  import { getDomain } from 'tldts';
8
+ import { normalizeUrl } from '@apify/utilities';
8
9
  import constants, {
9
10
  BrowserTypes,
10
11
  destinationPath,
@@ -1078,13 +1079,13 @@ export const randomThreeDigitNumberString = () => {
1078
1079
  return String(threeDigitNumber);
1079
1080
  };
1080
1081
 
1082
+ export const normUrl = (u: string): string => (u ? normalizeUrl(u) || u : '');
1083
+
1081
1084
  export const isFollowStrategy = (link1: string, link2: string, rule: string): boolean => {
1085
+ if (rule === 'all') return true;
1082
1086
  try {
1083
1087
  const parsedLink1 = new URL(link1);
1084
1088
  const parsedLink2 = new URL(link2);
1085
- if (rule === 'all') {
1086
- return true;
1087
- }
1088
1089
  if (rule === 'same-origin') {
1089
1090
  return parsedLink1.origin === parsedLink2.origin;
1090
1091
  }
@@ -186,7 +186,7 @@
186
186
  <li>
187
187
  <strong>Add the script</strong> — include the scanner before your closing
188
188
  <code>&lt;/body&gt;</code> tag:
189
- <pre>&lt;script src="https://cdn.jsdelivr.net/gh/GovTechSG/oobee@v0.10.86/oobee-client-scanner.js"&gt;&lt;/script&gt;</pre>
189
+ <pre>&lt;script src="https://cdn.jsdelivr.net/gh/GovTechSG/oobee@0.10.86/oobee-client-scanner.js"&gt;&lt;/script&gt;</pre>
190
190
 
191
191
  <br>This points to the <code>v0.10.86</code> release. Update the version tag as needed for newer releases.
192
192
  </li>