@govtechsg/oobee 0.10.21 → 0.10.28

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 +40 -25
  3. package/Dockerfile +41 -47
  4. package/LICENSE-3RD-PARTY-REPORT.txt +448 -0
  5. package/LICENSE-3RD-PARTY.txt +19913 -0
  6. package/README.md +10 -2
  7. package/__mocks__/mock-report.html +1503 -1360
  8. package/package.json +8 -4
  9. package/scripts/decodeUnzipParse.js +29 -0
  10. package/scripts/install_oobee_dependencies.command +2 -2
  11. package/scripts/install_oobee_dependencies.ps1 +3 -3
  12. package/src/cli.ts +3 -2
  13. package/src/constants/cliFunctions.ts +16 -2
  14. package/src/constants/common.ts +29 -5
  15. package/src/constants/constants.ts +28 -26
  16. package/src/constants/questions.ts +4 -1
  17. package/src/crawlers/commonCrawlerFunc.ts +114 -152
  18. package/src/crawlers/crawlDomain.ts +25 -25
  19. package/src/crawlers/crawlIntelligentSitemap.ts +7 -1
  20. package/src/crawlers/crawlLocalFile.ts +1 -1
  21. package/src/crawlers/crawlSitemap.ts +1 -1
  22. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +546 -472
  23. package/src/crawlers/customAxeFunctions.ts +1 -1
  24. package/src/index.ts +0 -2
  25. package/src/mergeAxeResults.ts +569 -219
  26. package/src/screenshotFunc/pdfScreenshotFunc.ts +3 -3
  27. package/src/static/ejs/partials/components/wcagCompliance.ejs +10 -29
  28. package/src/static/ejs/partials/footer.ejs +10 -13
  29. package/src/static/ejs/partials/scripts/categorySummary.ejs +2 -2
  30. package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  31. package/src/static/ejs/partials/scripts/reportSearch.ejs +1 -0
  32. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +54 -52
  33. package/src/static/ejs/partials/styles/styles.ejs +4 -0
  34. package/src/static/ejs/partials/summaryMain.ejs +15 -42
  35. package/src/static/ejs/report.ejs +21 -12
  36. package/src/utils.ts +10 -2
  37. package/src/xPathToCss.ts +186 -0
  38. package/a11y-scan-results.zip +0 -0
  39. package/src/types/xpath-to-css.d.ts +0 -3
package/package.json CHANGED
@@ -1,13 +1,15 @@
1
1
  {
2
2
  "name": "@govtechsg/oobee",
3
3
  "main": "dist/npmIndex.js",
4
- "version": "0.10.21",
4
+ "version": "0.10.28",
5
5
  "type": "module",
6
+ "author": "Government Technology Agency <info@tech.gov.sg>",
6
7
  "dependencies": {
7
8
  "@json2csv/node": "^7.0.3",
8
9
  "@napi-rs/canvas": "^0.1.53",
9
10
  "axe-core": "^4.10.2",
10
11
  "axios": "^1.7.4",
12
+ "base64-stream": "^1.0.0",
11
13
  "cheerio": "^1.0.0-rc.12",
12
14
  "crawlee": "^3.11.1",
13
15
  "ejs": "^3.1.9",
@@ -20,8 +22,8 @@
20
22
  "lodash": "^4.17.21",
21
23
  "mime-types": "^2.1.35",
22
24
  "minimatch": "^9.0.3",
23
- "pdfjs-dist": "github:veraPDF/pdfjs-dist#v2.14.305-taggedPdf-0.1.11",
24
- "playwright": "1.46.1",
25
+ "pdfjs-dist": "github:veraPDF/pdfjs-dist#v4.4.168-taggedPdf-0.1.20",
26
+ "playwright": "1.49.1",
25
27
  "prettier": "^3.1.0",
26
28
  "print-message": "^3.0.1",
27
29
  "safe-regex": "^2.1.1",
@@ -39,6 +41,7 @@
39
41
  "devDependencies": {
40
42
  "@eslint/eslintrc": "^3.0.2",
41
43
  "@eslint/js": "^9.6.0",
44
+ "@types/base64-stream": "^1.0.5",
42
45
  "@types/eslint__js": "^8.42.3",
43
46
  "@types/fs-extra": "^11.0.4",
44
47
  "@types/inquirer": "^9.0.7",
@@ -47,6 +50,7 @@
47
50
  "@types/validator": "^13.11.10",
48
51
  "@types/which": "^3.0.4",
49
52
  "@types/xml2js": "^0.4.14",
53
+ "browserify-zlib": "^0.2.0",
50
54
  "eslint": "^8.57.0",
51
55
  "eslint-config-airbnb-base": "^15.0.0",
52
56
  "eslint-config-prettier": "^8.6.0",
@@ -54,6 +58,7 @@
54
58
  "eslint-plugin-prettier": "^5.0.0",
55
59
  "globals": "^15.2.0",
56
60
  "jest": "^29.7.0",
61
+ "readable-stream": "^4.7.0",
57
62
  "typescript-eslint": "^8.3.0"
58
63
  },
59
64
  "overrides": {
@@ -82,7 +87,6 @@
82
87
  "lint": "eslint . --report-unused-disable-directives --max-warnings 0",
83
88
  "lint:fix": "eslint . --fix --report-unused-disable-directives --max-warnings 0"
84
89
  },
85
- "author": "",
86
90
  "license": "MIT",
87
91
  "description": "Oobee is a customisable, automated accessibility testing tool that allows software development teams to assess whether their products are user-friendly to persons with disabilities (PWDs).",
88
92
  "repository": {
@@ -0,0 +1,29 @@
1
+ /* Run the below command to get /src/static/ejs/partials/scripts/decodeUnzipParse.ejs */
2
+ /* ( echo '<script>'; npx browserify decodeUnzipParse.js --standalone decodeUnzipParse | npx uglify-js; echo '</script>' ) > decodeUnzipParse.ejs */
3
+ const { Readable } = require('readable-stream');
4
+ const { Base64Decode } = require('base64-stream');
5
+ const { createGunzip } = require('browserify-zlib');
6
+ const { parser } = require('stream-json');
7
+ const Asm = require('stream-json/Assembler');
8
+
9
+ module.exports = function parseGzipBase64Json(base64String) {
10
+ return new Promise((resolve, reject) => {
11
+ const pipeline = Readable.from([base64String])
12
+ .pipe(new Base64Decode())
13
+ .pipe(createGunzip())
14
+ .pipe(parser());
15
+
16
+ // Assembler to assemble the streamed JSON tokens
17
+ const assembler = Asm.connectTo(pipeline);
18
+
19
+ // On 'done' event, the entire JSON object is ready
20
+ assembler.on('done', asm => {
21
+ resolve(asm.current);
22
+ });
23
+
24
+ // If anything goes wrong at any stage, reject the promise
25
+ pipeline.on('error', err => {
26
+ reject(err);
27
+ });
28
+ });
29
+ };
@@ -1,6 +1,6 @@
1
1
  #!/bin/bash
2
2
 
3
- NODE_VERSION="20.10.0"
3
+ NODE_VERSION="22.13.1"
4
4
 
5
5
  # Get current shell command
6
6
  SHELL_COMMAND=$(ps -o comm= -p $$)
@@ -98,4 +98,4 @@ if [ "$(uname -m)" = "arm64" ] && /usr/bin/pgrep oahd >/dev/null 2>&1; then
98
98
  fi
99
99
 
100
100
  echo "Build TypeScript"
101
- npm run build || true
101
+ npm run build || true
@@ -9,11 +9,11 @@ $ErrorActionPreference = 'Stop'
9
9
  # Install NodeJS binaries
10
10
  if (-Not (Test-Path nodejs-win\node.exe)) {
11
11
  Write-Output "Downloading Node"
12
- Invoke-WebRequest -o ./nodejs-win.zip "https://nodejs.org/dist/v20.10.0/node-v20.10.0-win-x64.zip"
12
+ Invoke-WebRequest -o ./nodejs-win.zip "https://nodejs.org/dist/v22.13.1/node-v22.13.1-win-x64.zip"
13
13
 
14
14
  Write-Output "Unzip Node"
15
15
  Expand-Archive .\nodejs-win.zip -DestinationPath .
16
- Rename-Item node-v20.10.0-win-x64 -NewName nodejs-win
16
+ Rename-Item node-v22.13.1-win-x64 -NewName nodejs-win
17
17
  Remove-Item -Force .\nodejs-win.zip
18
18
  }
19
19
 
@@ -107,4 +107,4 @@ if (Test-Path oobee) {
107
107
  } else {
108
108
  Write-Output "Could not find oobee"
109
109
  }
110
- }
110
+ }
package/src/cli.ts CHANGED
@@ -208,6 +208,9 @@ const scanInit = async (argvs: Answers): Promise<string> => {
208
208
 
209
209
  const updatedArgvs = { ...argvs };
210
210
 
211
+ // Cannot use data.browser and data.isHeadless as the connectivity check comes first before prepareData
212
+ setHeadlessMode(updatedArgvs.browserToRun, updatedArgvs.headless);
213
+
211
214
  // let chromeDataDir = null;
212
215
  // let edgeDataDir = null;
213
216
  // Empty string for profile directory will use incognito mode in playwright
@@ -337,8 +340,6 @@ const scanInit = async (argvs: Answers): Promise<string> => {
337
340
  }
338
341
  }
339
342
 
340
- setHeadlessMode(data.browser, data.isHeadless);
341
-
342
343
  const screenToScan = getScreenToScan(
343
344
  updatedArgvs.deviceChosen,
344
345
  updatedArgvs.customDevice,
@@ -298,8 +298,22 @@ export const cliOptions: { [key: string]: Options } = {
298
298
  },
299
299
  g: {
300
300
  alias: 'generateJsonFiles',
301
- describe:
302
- 'Generate JSON files in the results folder. Accepts "yes", "no", "y", or "n". Default is "no".',
301
+ describe: `Generate two gzipped and base64-encoded JSON files containing the results of the accessibility scan:\n
302
+ 1. scanData.json.gz.b64: Provides an overview of the scan, including:
303
+ - WCAG compliance score
304
+ - Violated WCAG clauses
305
+ - Metadata (e.g., scan start and end times)
306
+ - Pages scanned and skipped
307
+ 2. scanItems.json.gz.b64: Contains detailed information about detected accessibility issues, including:
308
+ - Severity levels
309
+ - Issue descriptions
310
+ - Related WCAG guidelines
311
+ - URL of the pages violated the WCAG clauses
312
+ Useful for in-depth analysis or integration with external reporting tools.\n
313
+ To obtain the JSON files, you need to base64-decode the file followed by gunzip. For example:\n
314
+ (macOS) base64 -D -i scanData.json.gz.b64 | gunzip > scanData.json\n
315
+ (linux) base64 -d scanData.json.gz.b64 | gunzip > scanData.json\n
316
+ `,
303
317
  type: 'string',
304
318
  requiresArg: true,
305
319
  default: 'no',
@@ -402,8 +402,16 @@ const checkUrlConnectivityWithBrowser = async (
402
402
  let browserContext;
403
403
 
404
404
  try {
405
+ // Temporary browserContextLaunchOptions to force headless mode during connectivity check
406
+ // If a user selects cli options h=no, the connectivity check should still proceed in headless
407
+ const launchOptions = getPlaywrightLaunchOptions(browserToRun);
408
+ const browserContextLaunchOptions = {
409
+ ...launchOptions,
410
+ args: [...launchOptions.args, '--headless=new'],
411
+ };
412
+
405
413
  browserContext = await constants.launcher.launchPersistentContext(clonedDataDir, {
406
- ...getPlaywrightLaunchOptions(browserToRun),
414
+ ...browserContextLaunchOptions,
407
415
  ...(viewport && { viewport }),
408
416
  ...(userAgent && { userAgent }),
409
417
  ...(extraHTTPHeaders && { extraHTTPHeaders }),
@@ -437,6 +445,7 @@ const checkUrlConnectivityWithBrowser = async (
437
445
  silentLogger.info('Unable to detect networkidle');
438
446
  }
439
447
 
448
+ // This response state doesn't seem to work with the new headless=new flag
440
449
  if (response.status() === 401) {
441
450
  res.status = constants.urlCheckStatuses.unauthorised.code;
442
451
  } else {
@@ -459,8 +468,14 @@ const checkUrlConnectivityWithBrowser = async (
459
468
  res.content = responseFromUrl.content;
460
469
  }
461
470
  } catch (error) {
462
- silentLogger.error(error);
463
- res.status = constants.urlCheckStatuses.systemError.code;
471
+
472
+ // But this does work with the headless=new flag
473
+ if (error.message.includes('net::ERR_INVALID_AUTH_CREDENTIALS')) {
474
+ res.status = constants.urlCheckStatuses.unauthorised.code;
475
+ } else {
476
+ // enters here if input is not a URL or not using http/https protocols
477
+ res.status = constants.urlCheckStatuses.systemError.code;
478
+ }
464
479
  } finally {
465
480
  await browserContext.close();
466
481
  }
@@ -1770,15 +1785,24 @@ export const getPlaywrightLaunchOptions = (browser?: string): LaunchOptions => {
1770
1785
  if (browser) {
1771
1786
  channel = browser;
1772
1787
  }
1788
+
1789
+ // Set new headless mode as Chrome 132 does not support headless=old
1790
+ if (process.env.CRAWLEE_HEADLESS === '1') constants.launchOptionsArgs.push('--headless=new');
1791
+
1773
1792
  const options: LaunchOptions = {
1774
1793
  // Drop the --use-mock-keychain flag to allow MacOS devices
1775
1794
  // to use the cloned cookies.
1776
- ignoreDefaultArgs: ['--use-mock-keychain'],
1795
+ ignoreDefaultArgs: ['--use-mock-keychain', '--headless'],
1796
+ // necessary from Chrome 132 to use our own headless=new flag
1777
1797
  args: constants.launchOptionsArgs,
1798
+ headless: false,
1778
1799
  ...(channel && { channel }), // Having no channel is equivalent to "chromium"
1779
1800
  };
1801
+
1802
+ // Necessary as Chrome 132 does not support headless=old
1803
+ options.headless = false;
1804
+
1780
1805
  if (proxy) {
1781
- options.headless = false;
1782
1806
  options.slowMo = 1000; // To ensure server-side rendered proxy page is loaded
1783
1807
  } else if (browser === BrowserTypes.EDGE && os.platform() === 'win32') {
1784
1808
  // edge should be in non-headless mode
@@ -304,33 +304,35 @@ export const sitemapPaths = [
304
304
  '/sitemap_index.xml.xz',
305
305
  ];
306
306
 
307
+ // Remember to update getWcagPassPercentage() in src/utils/utils.ts if you change this
307
308
  const wcagLinks = {
308
- 'WCAG 1.1.1': 'https://www.w3.org/TR/WCAG21/#non-text-content',
309
- 'WCAG 1.2.2': 'https://www.w3.org/TR/WCAG21/#captions-prerecorded',
310
- 'WCAG 1.3.1': 'https://www.w3.org/TR/WCAG21/#info-and-relationships',
311
- // 'WCAG 1.3.4': 'https://www.w3.org/TR/WCAG21/#orientation', - TODO: review for veraPDF
312
- 'WCAG 1.3.5': 'https://www.w3.org/TR/WCAG21/#use-of-color',
313
- 'WCAG 1.4.1': 'https://www.w3.org/TR/WCAG21/#use-of-color',
314
- 'WCAG 1.4.2': 'https://www.w3.org/TR/WCAG21/#audio-control',
315
- 'WCAG 1.4.3': 'https://www.w3.org/TR/WCAG21/#contrast-minimum',
316
- 'WCAG 1.4.4': 'https://www.w3.org/TR/WCAG21/#resize-text',
317
- 'WCAG 1.4.6': 'https://www.w3.org/TR/WCAG21/#contrast-enhanced',
318
- // 'WCAG 1.4.10': 'https://www.w3.org/TR/WCAG21/#reflow', - TODO: review for veraPDF
319
- 'WCAG 1.4.12': 'https://www.w3.org/TR/WCAG21/#text-spacing',
320
- 'WCAG 2.1.1': 'https://www.w3.org/TR/WCAG21/#pause-stop-hide',
321
- 'WCAG 2.2.1': 'https://www.w3.org/TR/WCAG21/#timing-adjustable',
322
- 'WCAG 2.2.2': 'https://www.w3.org/TR/WCAG21/#pause-stop-hide',
323
- 'WCAG 2.2.4': 'https://www.w3.org/TR/WCAG21/#interruptions',
324
- 'WCAG 2.4.1': 'https://www.w3.org/TR/WCAG21/#bypass-blocks',
325
- 'WCAG 2.4.2': 'https://www.w3.org/TR/WCAG21/#page-titled',
326
- 'WCAG 2.4.3': 'https://www.w3.org/TR/WCAG21/#focus-order',
327
- 'WCAG 2.4.4': 'https://www.w3.org/TR/WCAG21/#link-purpose-in-context',
328
- 'WCAG 2.4.9': 'https://www.w3.org/TR/WCAG21/#link-purpose-link-only',
309
+ 'WCAG 1.1.1': 'https://www.w3.org/TR/WCAG22/#non-text-content',
310
+ 'WCAG 1.2.2': 'https://www.w3.org/TR/WCAG22/#captions-prerecorded',
311
+ 'WCAG 1.3.1': 'https://www.w3.org/TR/WCAG22/#info-and-relationships',
312
+ // 'WCAG 1.3.4': 'https://www.w3.org/TR/WCAG22/#orientation', - TODO: review for veraPDF
313
+ 'WCAG 1.3.5': 'https://www.w3.org/TR/WCAG22/#use-of-color',
314
+ 'WCAG 1.4.1': 'https://www.w3.org/TR/WCAG22/#use-of-color',
315
+ 'WCAG 1.4.2': 'https://www.w3.org/TR/WCAG22/#audio-control',
316
+ 'WCAG 1.4.3': 'https://www.w3.org/TR/WCAG22/#contrast-minimum',
317
+ 'WCAG 1.4.4': 'https://www.w3.org/TR/WCAG22/#resize-text',
318
+ 'WCAG 1.4.6': 'https://www.w3.org/TR/WCAG22/#contrast-enhanced', // AAA
319
+ // 'WCAG 1.4.10': 'https://www.w3.org/TR/WCAG22/#reflow', - TODO: review for veraPDF
320
+ 'WCAG 1.4.12': 'https://www.w3.org/TR/WCAG22/#text-spacing',
321
+ 'WCAG 2.1.1': 'https://www.w3.org/TR/WCAG22/#pause-stop-hide',
322
+ 'WCAG 2.2.1': 'https://www.w3.org/TR/WCAG22/#timing-adjustable',
323
+ 'WCAG 2.2.2': 'https://www.w3.org/TR/WCAG22/#pause-stop-hide',
324
+ 'WCAG 2.2.4': 'https://www.w3.org/TR/WCAG22/#interruptions', // AAA
325
+ 'WCAG 2.4.1': 'https://www.w3.org/TR/WCAG22/#bypass-blocks',
326
+ 'WCAG 2.4.2': 'https://www.w3.org/TR/WCAG22/#page-titled',
327
+ 'WCAG 2.4.4': 'https://www.w3.org/TR/WCAG22/#link-purpose-in-context',
328
+ 'WCAG 2.4.9': 'https://www.w3.org/TR/WCAG22/#link-purpose-link-only', // AAA
329
329
  'WCAG 2.5.8': 'https://www.w3.org/TR/WCAG22/#target-size-minimum',
330
- 'WCAG 3.1.1': 'https://www.w3.org/TR/WCAG21/#language-of-page',
331
- 'WCAG 3.1.2': 'https://www.w3.org/TR/WCAG21/#labels-or-instructions',
332
- 'WCAG 3.2.5': 'https://www.w3.org/TR/WCAG21/#change-on-request',
333
- 'WCAG 4.1.2': 'https://www.w3.org/TR/WCAG21/#name-role-value',
330
+ 'WCAG 3.1.1': 'https://www.w3.org/TR/WCAG22/#language-of-page',
331
+ 'WCAG 3.1.2': 'https://www.w3.org/TR/WCAG22/#labels-or-instructions',
332
+ 'WCAG 3.1.5': 'https://www.w3.org/TR/WCAG22/#reading-level', // AAA
333
+ 'WCAG 3.2.5': 'https://www.w3.org/TR/WCAG22/#change-on-request', // AAA
334
+ 'WCAG 3.3.2': 'https://www.w3.org/TR/WCAG22/#labels-or-instructions',
335
+ 'WCAG 4.1.2': 'https://www.w3.org/TR/WCAG22/#name-role-value',
334
336
  };
335
337
 
336
338
  const urlCheckStatuses = {
@@ -423,7 +425,7 @@ export default {
423
425
  };
424
426
 
425
427
  export const rootPath = dirname;
426
- export const wcagWebPage = 'https://www.w3.org/TR/WCAG21/';
428
+ export const wcagWebPage = 'https://www.w3.org/TR/WCAG22/';
427
429
  const latestAxeVersion = '4.9';
428
430
  export const axeVersion = latestAxeVersion;
429
431
  export const axeWebPage = `https://dequeuniversity.com/rules/axe/${latestAxeVersion}/`;
@@ -1,6 +1,6 @@
1
1
  import { Question } from 'inquirer';
2
2
  import { Answers } from '../index.js';
3
- import { getUserDataTxt } from '../utils.js';
3
+ import { getUserDataTxt, setHeadlessMode } from '../utils.js';
4
4
  import {
5
5
  checkUrl,
6
6
  deleteClonedProfiles,
@@ -79,6 +79,9 @@ const startScanQuestions = [
79
79
 
80
80
  const statuses = constants.urlCheckStatuses;
81
81
  const { browserToRun, clonedBrowserDataDir } = getBrowserToRun(BrowserTypes.CHROME);
82
+
83
+ setHeadlessMode(browserToRun, answers.headless);
84
+
82
85
  const playwrightDeviceDetailsObject = getPlaywrightDeviceDetailsObject(
83
86
  answers.deviceChosen,
84
87
  answers.customDevice,