@govtechsg/oobee 0.10.76 → 0.10.78-alpha1

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 (137) hide show
  1. package/.github/workflows/publish.yml +8 -1
  2. package/INTEGRATION.md +50 -3
  3. package/dist/cli.js +252 -0
  4. package/dist/combine.js +221 -0
  5. package/dist/constants/cliFunctions.js +306 -0
  6. package/dist/constants/common.js +1669 -0
  7. package/dist/constants/constants.js +913 -0
  8. package/dist/constants/errorMeta.json +319 -0
  9. package/dist/constants/itemTypeDescription.js +7 -0
  10. package/dist/constants/oobeeAi.js +121 -0
  11. package/dist/constants/questions.js +151 -0
  12. package/dist/constants/sampleData.js +176 -0
  13. package/dist/crawlers/commonCrawlerFunc.js +428 -0
  14. package/dist/crawlers/crawlDomain.js +613 -0
  15. package/dist/crawlers/crawlIntelligentSitemap.js +135 -0
  16. package/dist/crawlers/crawlLocalFile.js +151 -0
  17. package/dist/crawlers/crawlSitemap.js +303 -0
  18. package/dist/crawlers/custom/escapeCssSelector.js +10 -0
  19. package/dist/crawlers/custom/evaluateAltText.js +11 -0
  20. package/dist/crawlers/custom/extractAndGradeText.js +44 -0
  21. package/dist/crawlers/custom/extractText.js +27 -0
  22. package/dist/crawlers/custom/findElementByCssSelector.js +36 -0
  23. package/dist/crawlers/custom/flagUnlabelledClickableElements.js +963 -0
  24. package/dist/crawlers/custom/framesCheck.js +37 -0
  25. package/dist/crawlers/custom/getAxeConfiguration.js +111 -0
  26. package/dist/crawlers/custom/gradeReadability.js +23 -0
  27. package/dist/crawlers/custom/utils.js +1024 -0
  28. package/dist/crawlers/custom/xPathToCss.js +147 -0
  29. package/dist/crawlers/guards/urlGuard.js +71 -0
  30. package/dist/crawlers/pdfScanFunc.js +276 -0
  31. package/dist/crawlers/runCustom.js +89 -0
  32. package/dist/exclusions.txt +7 -0
  33. package/dist/generateHtmlReport.js +144 -0
  34. package/dist/index.js +62 -0
  35. package/dist/logs.js +84 -0
  36. package/dist/mergeAxeResults.js +1588 -0
  37. package/dist/npmIndex.js +640 -0
  38. package/dist/proxyService.js +360 -0
  39. package/dist/runGenerateJustHtmlReport.js +16 -0
  40. package/dist/screenshotFunc/htmlScreenshotFunc.js +355 -0
  41. package/dist/screenshotFunc/pdfScreenshotFunc.js +645 -0
  42. package/dist/services/s3Uploader.js +127 -0
  43. package/dist/static/ejs/partials/components/allIssues/AllIssues.ejs +9 -0
  44. package/dist/static/ejs/partials/components/allIssues/CategoryBadges.ejs +82 -0
  45. package/dist/static/ejs/partials/components/allIssues/FilterBar.ejs +33 -0
  46. package/dist/static/ejs/partials/components/allIssues/IssuesTable.ejs +41 -0
  47. package/dist/static/ejs/partials/components/header/SiteInfo.ejs +119 -0
  48. package/dist/static/ejs/partials/components/header/aboutScanModal/AboutScanModal.ejs +15 -0
  49. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanConfiguration.ejs +44 -0
  50. package/dist/static/ejs/partials/components/header/aboutScanModal/ScanDetails.ejs +142 -0
  51. package/dist/static/ejs/partials/components/prioritiseIssues/IssueDetailCard.ejs +36 -0
  52. package/dist/static/ejs/partials/components/prioritiseIssues/PrioritiseIssues.ejs +47 -0
  53. package/dist/static/ejs/partials/components/ruleModal/ruleOffcanvas.ejs +196 -0
  54. package/dist/static/ejs/partials/components/scannedPagesSegmentedTabs.ejs +48 -0
  55. package/dist/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
  56. package/dist/static/ejs/partials/components/shared/InfoAlert.ejs +3 -0
  57. package/dist/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
  58. package/dist/static/ejs/partials/components/summaryScanResults.ejs +16 -0
  59. package/dist/static/ejs/partials/components/summaryTable.ejs +20 -0
  60. package/dist/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
  61. package/dist/static/ejs/partials/components/topTen.ejs +6 -0
  62. package/dist/static/ejs/partials/components/wcagCompliance/FailedCriteria.ejs +47 -0
  63. package/dist/static/ejs/partials/components/wcagCompliance/WcagCompliance.ejs +16 -0
  64. package/dist/static/ejs/partials/components/wcagCompliance/WcagGaugeBar.ejs +16 -0
  65. package/dist/static/ejs/partials/components/wcagCoverageDetails.ejs +18 -0
  66. package/dist/static/ejs/partials/footer.ejs +24 -0
  67. package/dist/static/ejs/partials/header.ejs +14 -0
  68. package/dist/static/ejs/partials/main.ejs +29 -0
  69. package/dist/static/ejs/partials/scripts/allIssues/AllIssues.ejs +376 -0
  70. package/dist/static/ejs/partials/scripts/bootstrap.ejs +8 -0
  71. package/dist/static/ejs/partials/scripts/categorySummary.ejs +141 -0
  72. package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  73. package/dist/static/ejs/partials/scripts/header/SiteInfo.ejs +44 -0
  74. package/dist/static/ejs/partials/scripts/header/aboutScanModal/AboutScanModal.ejs +51 -0
  75. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +127 -0
  76. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanDetails.ejs +60 -0
  77. package/dist/static/ejs/partials/scripts/highlightjs.ejs +335 -0
  78. package/dist/static/ejs/partials/scripts/popper.ejs +7 -0
  79. package/dist/static/ejs/partials/scripts/prioritiseIssues/IssueDetailCard.ejs +137 -0
  80. package/dist/static/ejs/partials/scripts/prioritiseIssues/PrioritiseIssues.ejs +214 -0
  81. package/dist/static/ejs/partials/scripts/prioritiseIssues/wcagSvgMap.ejs +861 -0
  82. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +957 -0
  83. package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +353 -0
  84. package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +468 -0
  85. package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +306 -0
  86. package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +483 -0
  87. package/dist/static/ejs/partials/scripts/scannedPagesSegmentedTabs.ejs +35 -0
  88. package/dist/static/ejs/partials/scripts/screenshotLightbox.ejs +75 -0
  89. package/dist/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
  90. package/dist/static/ejs/partials/scripts/summaryTable.ejs +78 -0
  91. package/dist/static/ejs/partials/scripts/topTen.ejs +61 -0
  92. package/dist/static/ejs/partials/scripts/utils.ejs +453 -0
  93. package/dist/static/ejs/partials/scripts/wcagCompliance/FailedCriteria.ejs +103 -0
  94. package/dist/static/ejs/partials/scripts/wcagCompliance/WcagGaugeBar.ejs +47 -0
  95. package/dist/static/ejs/partials/scripts/wcagCompliance.ejs +15 -0
  96. package/dist/static/ejs/partials/scripts/wcagCoverageDetails.ejs +75 -0
  97. package/dist/static/ejs/partials/styles/allIssues/AllIssues.ejs +384 -0
  98. package/dist/static/ejs/partials/styles/bootstrap.ejs +12391 -0
  99. package/dist/static/ejs/partials/styles/header/SiteInfo.ejs +121 -0
  100. package/dist/static/ejs/partials/styles/header/aboutScanModal/AboutScanModal.ejs +82 -0
  101. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanConfiguration.ejs +50 -0
  102. package/dist/static/ejs/partials/styles/header/aboutScanModal/ScanDetails.ejs +149 -0
  103. package/dist/static/ejs/partials/styles/header.ejs +7 -0
  104. package/dist/static/ejs/partials/styles/highlightjs.ejs +54 -0
  105. package/dist/static/ejs/partials/styles/prioritiseIssues/IssueDetailCard.ejs +141 -0
  106. package/dist/static/ejs/partials/styles/prioritiseIssues/PrioritiseIssues.ejs +204 -0
  107. package/dist/static/ejs/partials/styles/ruleModal/ruleOffcanvas.ejs +456 -0
  108. package/dist/static/ejs/partials/styles/scannedPagesSegmentedTabs.ejs +46 -0
  109. package/dist/static/ejs/partials/styles/shared/InfoAlert.ejs +12 -0
  110. package/dist/static/ejs/partials/styles/styles.ejs +1607 -0
  111. package/dist/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
  112. package/dist/static/ejs/partials/styles/topTenCard.ejs +44 -0
  113. package/dist/static/ejs/partials/styles/wcagCompliance/FailedCriteria.ejs +59 -0
  114. package/dist/static/ejs/partials/styles/wcagCompliance/WcagGaugeBar.ejs +62 -0
  115. package/dist/static/ejs/partials/styles/wcagCompliance.ejs +36 -0
  116. package/dist/static/ejs/partials/styles/wcagCoverageDetails.ejs +33 -0
  117. package/dist/static/ejs/partials/summaryHeader.ejs +70 -0
  118. package/dist/static/ejs/partials/summaryMain.ejs +49 -0
  119. package/dist/static/ejs/report.ejs +226 -0
  120. package/dist/static/ejs/summary.ejs +47 -0
  121. package/dist/types/types.js +1 -0
  122. package/dist/utils.js +1070 -0
  123. package/examples/oobee-cypress-integration-js/cypress/support/e2e.js +36 -6
  124. package/examples/oobee-cypress-integration-js/cypress.config.js +45 -1
  125. package/examples/oobee-cypress-integration-ts/cypress.config.ts +47 -1
  126. package/examples/oobee-cypress-integration-ts/src/cypress/support/e2e.ts +36 -6
  127. package/examples/oobee-playwright-integration-js/oobee-playwright-demo.js +2 -1
  128. package/examples/oobee-playwright-integration-ts/src/oobee-playwright-demo.ts +2 -1
  129. package/examples/oobee-scan-html-demo.js +51 -0
  130. package/examples/oobee-scan-page-demo.js +40 -0
  131. package/package.json +9 -3
  132. package/src/constants/common.ts +2 -2
  133. package/src/constants/constants.ts +3 -1
  134. package/src/crawlers/crawlDomain.ts +1 -0
  135. package/src/crawlers/runCustom.ts +0 -1
  136. package/src/mergeAxeResults.ts +43 -22
  137. package/src/npmIndex.ts +500 -131
@@ -55,12 +55,42 @@ Cypress.Commands.add("runOobeeA11yScan", (items = {}) => {
55
55
  elementsToScan,
56
56
  gradingReadabilityFlag,
57
57
  );
58
- cy.task("pushOobeeA11yScanResults", {
59
- res,
60
- metadata,
61
- elementsToClick,
62
- }).then((count) => {
63
- return count;
58
+
59
+ const processNodes = (nodes) => {
60
+ if (!nodes) return;
61
+ cy.wrap(nodes).each((node, index) => {
62
+ if (node.target && node.target.length > 0) {
63
+ const selector = node.target[0];
64
+ // Generate a unique filename
65
+ const filename = `oobee-screenshot-${Date.now()}-${Math.floor(Math.random() * 1000)}-${index}.png`;
66
+
67
+ // Check existence to prevent failure, then screenshot
68
+ cy.get("body").then($body => {
69
+ if ($body.find(selector).length) {
70
+ // We use capture: 'viewport' to be safe and overwrite true
71
+ cy.get(selector).first().scrollIntoView().screenshot(filename.replace('.png', ''), { capture: 'viewport', overwrite: true });
72
+ node.screenshotFilename = filename;
73
+ }
74
+ });
75
+ }
76
+ });
77
+ };
78
+
79
+ const violations = res.axeScanResults.violations;
80
+ const incomplete = res.axeScanResults.incomplete;
81
+
82
+ cy.wrap(violations).each((v) => processNodes(v.nodes));
83
+ cy.wrap(incomplete).each((v) => processNodes(v.nodes));
84
+
85
+ // Ensure screenshots are done before pushing results
86
+ cy.then(() => {
87
+ cy.task("pushOobeeA11yScanResults", {
88
+ res,
89
+ metadata,
90
+ elementsToClick,
91
+ }).then((count) => {
92
+ return count;
93
+ });
64
94
  });
65
95
  },
66
96
  );
@@ -1,5 +1,7 @@
1
1
  import { defineConfig } from "cypress";
2
2
  import oobeeA11yInit from "@govtechsg/oobee";
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
3
5
 
4
6
  // viewport used in tests to optimise screenshots
5
7
  const viewportSettings = { width: 1920, height: 1040 };
@@ -44,7 +46,49 @@ export default defineConfig({
44
46
  return oobeeA11y.gradeReadability(sentences);
45
47
  },
46
48
  async pushOobeeA11yScanResults({ res, metadata, elementsToClick }) {
47
- return await oobeeA11y.pushScanResults(res, metadata, elementsToClick);
49
+ if (oobeeA11y.scanDetails.isIncludeScreenshots) {
50
+ const moveScreenshots = (items) => {
51
+ if (!items) return;
52
+ items.forEach(item => {
53
+ item.nodes.forEach((node) => {
54
+ if (node.screenshotFilename) {
55
+ const searchDir = 'cypress/screenshots';
56
+
57
+ const findFile = (dir, name) => {
58
+ if (!fs.existsSync(dir)) return null;
59
+ const files = fs.readdirSync(dir);
60
+ for (const file of files) {
61
+ const filePath = path.join(dir, file);
62
+ if (fs.statSync(filePath).isDirectory()) {
63
+ const found = findFile(filePath, name);
64
+ if (found) return found;
65
+ } else if (file === name) {
66
+ return filePath;
67
+ }
68
+ }
69
+ return null;
70
+ }
71
+
72
+ const srcPath = findFile(searchDir, node.screenshotFilename);
73
+ if (srcPath) {
74
+ const destDir = `results/${oobeeA11y.randomToken}/screenshots`;
75
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
76
+ const destPath = path.join(destDir, node.screenshotFilename);
77
+ fs.copyFileSync(srcPath, destPath);
78
+ // Set screenshot path for Oobee report
79
+ node.screenshotPath = node.screenshotFilename;
80
+ }
81
+ }
82
+ })
83
+ })
84
+ };
85
+
86
+ moveScreenshots(res.axeScanResults.violations);
87
+ moveScreenshots(res.axeScanResults.incomplete);
88
+ }
89
+
90
+ // Pass disableScreenshots=true to avoid opening a new Playwright browser
91
+ return await oobeeA11y.pushScanResults(res, metadata, elementsToClick, undefined, true);
48
92
  },
49
93
  returnResultsDir() {
50
94
  return `results/${oobeeA11y.randomToken}_${oobeeA11y.scanDetails.urlsCrawled.scanned.length}pages/report.html`;
@@ -1,5 +1,7 @@
1
1
  import { defineConfig } from "cypress";
2
2
  import oobeeA11yInit from "@govtechsg/oobee";
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
3
5
 
4
6
  interface ViewportSettings {
5
7
  width: number;
@@ -71,7 +73,51 @@ export default defineConfig({
71
73
  },
72
74
  async pushOobeeA11yScanResults({res, metadata, elementsToClick}: { res: any, metadata: any, elementsToClick: any[] }): Promise<{ mustFix: number, goodToFix: number }> {
73
75
  const instance = await initOobeeIfNeeded();
74
- return await instance.pushScanResults(res, metadata, elementsToClick);
76
+
77
+ if (instance.scanDetails.isIncludeScreenshots) {
78
+ const moveScreenshots = (items: any[]) => {
79
+ if (!items) return;
80
+ items.forEach(item => {
81
+ item.nodes.forEach((node: any) => {
82
+ if (node.screenshotFilename) {
83
+ const searchDir = 'cypress/screenshots';
84
+
85
+ const findFile = (dir: string, name: string): string | null => {
86
+ if (!fs.existsSync(dir)) return null;
87
+ const files = fs.readdirSync(dir);
88
+ for (const file of files) {
89
+ const filePath = path.join(dir, file);
90
+ if (fs.statSync(filePath).isDirectory()) {
91
+ const found = findFile(filePath, name);
92
+ if (found) return found;
93
+ } else if (file === name) {
94
+ return filePath;
95
+ }
96
+ }
97
+ return null;
98
+ }
99
+
100
+ const srcPath = findFile(searchDir, node.screenshotFilename);
101
+ if (srcPath) {
102
+ const destDir = `results/${instance.randomToken}/screenshots`;
103
+ if (!fs.existsSync(destDir)) fs.mkdirSync(destDir, { recursive: true });
104
+ const destPath = path.join(destDir, node.screenshotFilename);
105
+ fs.copyFileSync(srcPath, destPath);
106
+ // Set screenshot path for Oobee report
107
+ node.screenshotPath = node.screenshotFilename;
108
+ }
109
+ }
110
+ })
111
+ })
112
+ };
113
+
114
+ moveScreenshots(res.axeScanResults.violations);
115
+ moveScreenshots(res.axeScanResults.incomplete);
116
+ }
117
+
118
+ // Cypress task runs in Node.js and does not have access to the browser Page object
119
+ // Pass disableScreenshots=true to avoid opening a new Playwright browser
120
+ return await instance.pushScanResults(res, metadata, elementsToClick, undefined, true);
75
121
  },
76
122
  async returnResultsDir(): Promise<string> {
77
123
  const instance = await initOobeeIfNeeded();
@@ -55,12 +55,42 @@ Cypress.Commands.add("runOobeeA11yScan", (items: OobeeScanOptions = {}) => {
55
55
  elementsToScan,
56
56
  gradingReadabilityFlag,
57
57
  );
58
- cy.task("pushOobeeA11yScanResults", {
59
- res,
60
- metadata,
61
- elementsToClick,
62
- }).then((count) => {
63
- return count;
58
+
59
+ const processNodes = (nodes: any[]) => {
60
+ if (!nodes) return;
61
+ cy.wrap(nodes).each((node: any, index: number) => {
62
+ if (node.target && node.target.length > 0) {
63
+ const selector = node.target[0];
64
+ // Generate a unique filename
65
+ const filename = `oobee-screenshot-${Date.now()}-${Math.floor(Math.random() * 1000)}-${index}.png`;
66
+
67
+ // Check existence to prevent failure, then screenshot
68
+ cy.get("body").then($body => {
69
+ if ($body.find(selector).length) {
70
+ // We use capture: 'viewport' to be safe and overwrite true
71
+ cy.get(selector).first().scrollIntoView().screenshot(filename.replace('.png', ''), { capture: 'viewport', overwrite: true });
72
+ node.screenshotFilename = filename;
73
+ }
74
+ });
75
+ }
76
+ });
77
+ };
78
+
79
+ const violations = res.axeScanResults.violations;
80
+ const incomplete = res.axeScanResults.incomplete;
81
+
82
+ cy.wrap(violations).each((v: any) => processNodes(v.nodes));
83
+ cy.wrap(incomplete).each((v: any) => processNodes(v.nodes));
84
+
85
+ // Ensure screenshots are done before pushing results
86
+ cy.then(() => {
87
+ cy.task("pushOobeeA11yScanResults", {
88
+ res,
89
+ metadata,
90
+ elementsToClick,
91
+ }).then((count) => {
92
+ return count;
93
+ });
64
94
  });
65
95
  },
66
96
  );
@@ -40,7 +40,8 @@ const oobeeA11y = await oobeeA11yInit({
40
40
  async ({ elementsToScan, gradingReadabilityFlag }) => await runA11yScan(elementsToScan, gradingReadabilityFlag),
41
41
  { elementsToScan, gradingReadabilityFlag },
42
42
  );
43
- await oobeeA11y.pushScanResults(scanRes);
43
+ // Pass page object to allow screenshot reuse
44
+ await oobeeA11y.pushScanResults(scanRes, undefined, undefined, page);
44
45
  oobeeA11y.testThresholds(); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
45
46
  };
46
47
 
@@ -59,7 +59,8 @@ const oobeeA11y = await oobeeA11yInit({
59
59
  async ({ elementsToScan, gradingReadabilityFlag }) => await runA11yScan(elementsToScan, gradingReadabilityFlag),
60
60
  { elementsToScan, gradingReadabilityFlag },
61
61
  );
62
- await oobeeA11y.pushScanResults(scanRes);
62
+ // Pass page object to allow screenshot reuse
63
+ await oobeeA11y.pushScanResults(scanRes, undefined, undefined, page);
63
64
  oobeeA11y.testThresholds(); // test the accumulated number of issue occurrences against specified thresholds. If exceed, terminate oobeeA11y instance.
64
65
  };
65
66
 
@@ -0,0 +1,51 @@
1
+ import { scanHTML } from '../dist/npmIndex.js';
2
+
3
+ const htmlContent = `
4
+ <!DOCTYPE html>
5
+ <html lang="en">
6
+ <head>
7
+ <title>Test Page</title>
8
+ </head>
9
+ <body>
10
+ <h1>Accessibility Test</h1>
11
+ <button></button> <!-- Violation: button-name -->
12
+ <img src="test.jpg" /> <!-- Violation: image-alt -->
13
+ <div role="button">Fake</div> <!-- Violation: role-button (if interactive) -->
14
+ </body>
15
+ </html>
16
+ `;
17
+
18
+ const htmlContent2 = `
19
+ <!DOCTYPE html>
20
+ <html lang="en">
21
+ <head>
22
+ <title>Test Page 2</title>
23
+ </head>
24
+ <body>
25
+ <h1>Accessibility Test 2</h1>
26
+ <a href="#">Click me</a> <!-- Violation: link-name (if vague) or empty href issues -->
27
+ <input type="text" /> <!-- Violation: label -->
28
+ </body>
29
+ </html>
30
+ `;
31
+
32
+ (async () => {
33
+ console.log("Scanning HTML string...");
34
+ try {
35
+ // Run scanHTML without needing full Oobee init
36
+ // Pass an array of HTML strings to demonstrate batch scanning
37
+ const results = await scanHTML(
38
+ [htmlContent, htmlContent2],
39
+ {
40
+ name: "Your Name",
41
+ email: "email@domain.com",
42
+ }
43
+ );
44
+ console.log(JSON.stringify(results, null, 2));
45
+
46
+ console.log(`\nScan Complete.`);
47
+
48
+ } catch (error) {
49
+ console.error("Error during scan:", error);
50
+ }
51
+ })();
@@ -0,0 +1,40 @@
1
+ import { chromium } from 'playwright';
2
+ import { scanPage } from '../dist/npmIndex.js';
3
+
4
+ (async () => {
5
+ console.log("Launching browser...");
6
+ const browser = await chromium.launch({
7
+ headless: false,
8
+ channel: 'chrome' // Use Chrome instead of Chromium
9
+ });
10
+ const page = await browser.newPage();
11
+
12
+ console.log("Navigating to test page...");
13
+ // Using a sample page that likely has accessibility issues
14
+ await page.goto('https://govtechsg.github.io/purple-banner-embeds/purple-integrated-scan-example.htm');
15
+
16
+ const page2 = await browser.newPage();
17
+ console.log("Navigating to second test page...");
18
+ await page2.goto('https://a11y.tech.gov.sg');
19
+
20
+ console.log("Scanning page...");
21
+ try {
22
+ // Run scanPage using the existing Playwright page
23
+ const results = await scanPage(
24
+ [page, page2],
25
+ {
26
+ name: "Your Name",
27
+ email: "email@domain.com",
28
+ }
29
+ );
30
+
31
+ console.log(JSON.stringify(results, null, 2));
32
+
33
+ console.log(`\nScan Complete.`);
34
+
35
+ } catch (error) {
36
+ console.error("Error during scan:", error);
37
+ } finally {
38
+ await browser.close();
39
+ }
40
+ })();
package/package.json CHANGED
@@ -1,9 +1,12 @@
1
1
  {
2
2
  "name": "@govtechsg/oobee",
3
3
  "main": "dist/npmIndex.js",
4
- "version": "0.10.76",
4
+ "version": "0.10.78-alpha1",
5
5
  "type": "module",
6
6
  "author": "Government Technology Agency <info@tech.gov.sg>",
7
+ "bin": {
8
+ "oobee": "./dist/cli.js"
9
+ },
7
10
  "dependencies": {
8
11
  "@aws-sdk/client-s3": "^3.948.0",
9
12
  "@json2csv/node": "^7.0.3",
@@ -58,7 +61,7 @@
58
61
  "@types/which": "^3.0.4",
59
62
  "@types/xml2js": "^0.4.14",
60
63
  "browserify-zlib": "^0.2.0",
61
- "eslint": "^8.57.0",
64
+ "eslint": "^9.26.0",
62
65
  "eslint-config-airbnb-base": "^15.0.0",
63
66
  "eslint-config-prettier": "^8.6.0",
64
67
  "eslint-plugin-import": "^2.27.4",
@@ -76,7 +79,10 @@
76
79
  "micromatch": "github:micromatch/micromatch.git#4.0.8",
77
80
  "brace-expansion": "^1.1.12",
78
81
  "tmp": "0.2.4",
79
- "tar": "^7.5.6"
82
+ "tar": "^7.5.6",
83
+ "eslint-config-airbnb-base": {
84
+ "eslint": "^9.26.0"
85
+ }
80
86
  },
81
87
  "optionalDependencies": {
82
88
  "@napi-rs/canvas-darwin-arm64": "^0.1.53",
@@ -1333,14 +1333,14 @@ const cloneEdgeProfileCookieFiles = (options: GlobOptionsWithFileTypesFalse, des
1333
1333
  if (os.platform() === 'win32') {
1334
1334
  profileCookiesDir = globSync('**/Network/Cookies', {
1335
1335
  ...options,
1336
- ignore: 'oobee/**',
1336
+ ignore: 'oobee*/**',
1337
1337
  });
1338
1338
  profileNamesRegex = /User Data\\(.*?)\\Network/;
1339
1339
  } else if (os.platform() === 'darwin') {
1340
1340
  // Ignores copying cookies from the oobee directory if it exists
1341
1341
  profileCookiesDir = globSync('**/Cookies', {
1342
1342
  ...options,
1343
- ignore: 'oobee/**',
1343
+ ignore: 'oobee*/**',
1344
1344
  });
1345
1345
  profileNamesRegex = /Microsoft Edge\/(.*?)\/Cookies/;
1346
1346
  }
@@ -1,5 +1,6 @@
1
1
  import path from 'path';
2
2
  import { fileURLToPath } from 'url';
3
+ import { createRequire } from 'module';
3
4
  import fs from 'fs-extra';
4
5
  import { globSync } from 'glob';
5
6
  import which from 'which';
@@ -13,6 +14,7 @@ import { PageInfo } from '../mergeAxeResults.js';
13
14
 
14
15
  const filename = fileURLToPath(import.meta.url);
15
16
  const dirname = path.dirname(filename);
17
+ const require = createRequire(import.meta.url);
16
18
 
17
19
  const maxRequestsPerCrawl = 100;
18
20
 
@@ -216,7 +218,7 @@ export const getExecutablePath = function (dir: string, file: string): string {
216
218
  export const basicAuthRegex = /^.*\/\/.*:.*@.*$/i;
217
219
 
218
220
  // for crawlers
219
- export const axeScript = path.join(dirname, '../../node_modules/axe-core/axe.min.js');
221
+ export const axeScript = require.resolve('axe-core/axe.min.js');
220
222
  export class UrlsCrawled {
221
223
  siteName: string;
222
224
  toScan: string[] = [];
@@ -151,6 +151,7 @@ const crawlDomain = async ({
151
151
  req.skipNavigation = true;
152
152
  }
153
153
  if (isDisallowedInRobotsTxt(req.url)) return null;
154
+ if (isBlacklisted(req.url, blacklistedPatterns)) return null;
154
155
  if (isUrlPdf(req.url)) {
155
156
  // playwright headless mode does not support navigation to pdf document
156
157
  req.skipNavigation = true;
@@ -84,7 +84,6 @@ const runCustom = async (
84
84
  headless: false,
85
85
  channel: 'chrome',
86
86
  // bypassCSP: true,
87
- devtools: DEBUG,
88
87
  });
89
88
 
90
89
  const context = await browser.newContext({
@@ -12,7 +12,6 @@ import { AsyncParser, ParserOptions } from '@json2csv/node';
12
12
  import zlib from 'zlib';
13
13
  import { Base64Encode } from 'base64-stream';
14
14
  import { pipeline } from 'stream/promises';
15
- // @ts-ignore
16
15
  import * as Sentry from '@sentry/node';
17
16
  import constants, {
18
17
  BrowserTypes,
@@ -1289,34 +1288,56 @@ function updateIssuesWithOccurrences(issuesList: any[], urlOccurrencesMap: Map<s
1289
1288
  });
1290
1289
  }
1291
1290
 
1291
+ const extractRuleAiData = (ruleId: string, totalItems: number, items: any[], callback?: () => void) => {
1292
+ let snippets = [];
1293
+
1294
+ if (oobeeAiRules.includes(ruleId)) {
1295
+ const snippetsSet = new Set();
1296
+ if (items) {
1297
+ items.forEach(item => {
1298
+ snippetsSet.add(oobeeAiHtmlETL(item.html));
1299
+ });
1300
+ }
1301
+ snippets = [...snippetsSet];
1302
+ if (callback) callback();
1303
+ }
1304
+ return {
1305
+ snippets,
1306
+ occurrences: totalItems,
1307
+ };
1308
+ };
1309
+
1310
+ // This is for telemetry purposes called within mergeAxeResults.ts
1311
+ export
1292
1312
  const createRuleIdJson = allIssues => {
1293
1313
  const compiledRuleJson = {};
1294
1314
 
1295
- const ruleIterator = rule => {
1296
- const ruleId = rule.rule;
1297
- let snippets = [];
1298
-
1299
- if (oobeeAiRules.includes(ruleId)) {
1300
- const snippetsSet = new Set();
1301
- rule.pagesAffected.forEach(page => {
1302
- page.items.forEach(htmlItem => {
1303
- snippetsSet.add(oobeeAiHtmlETL(htmlItem.html));
1315
+ ['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
1316
+ allIssues.items[category].rules.forEach(rule => {
1317
+ const allItems = rule.pagesAffected.flatMap(page => page.items || []);
1318
+ compiledRuleJson[rule.rule] = extractRuleAiData(rule.rule, rule.totalItems, allItems, () => {
1319
+ rule.pagesAffected.forEach(p => {
1320
+ delete p.items;
1304
1321
  });
1305
1322
  });
1306
- snippets = [...snippetsSet];
1307
- rule.pagesAffected.forEach(p => {
1308
- delete p.items;
1323
+ });
1324
+ });
1325
+
1326
+ return compiledRuleJson;
1327
+ };
1328
+
1329
+ // This is for telemetry purposes called from npmIndex (scanPage and scanHTML) where report is not generated
1330
+ export const createBasicFormHTMLSnippet = filteredResults => {
1331
+ const compiledRuleJson = {};
1332
+
1333
+ ['mustFix', 'goodToFix', 'needsReview'].forEach(category => {
1334
+ if (filteredResults[category] && filteredResults[category].rules) {
1335
+ Object.entries(filteredResults[category].rules).forEach(([ruleId, ruleVal]: [string, any]) => {
1336
+ compiledRuleJson[ruleId] = extractRuleAiData(ruleId, ruleVal.totalItems, ruleVal.items);
1309
1337
  });
1310
1338
  }
1311
- compiledRuleJson[ruleId] = {
1312
- snippets,
1313
- occurrences: rule.totalItems,
1314
- };
1315
- };
1339
+ });
1316
1340
 
1317
- allIssues.items.mustFix.rules.forEach(ruleIterator);
1318
- allIssues.items.goodToFix.rules.forEach(ruleIterator);
1319
- allIssues.items.needsReview.rules.forEach(ruleIterator);
1320
1341
  return compiledRuleJson;
1321
1342
  };
1322
1343
 
@@ -1587,7 +1608,7 @@ function populateScanPagesDetail(allIssues: AllIssues): void {
1587
1608
  }
1588
1609
 
1589
1610
  // Send WCAG criteria breakdown to Sentry
1590
- const sendWcagBreakdownToSentry = async (
1611
+ export const sendWcagBreakdownToSentry = async (
1591
1612
  appVersion: string,
1592
1613
  wcagBreakdown: Map<string, number>,
1593
1614
  ruleIdJson: any,