@govtechsg/oobee 0.10.20 → 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 (42) 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 +26 -0
  7. package/__mocks__/mock-report.html +1503 -1360
  8. package/package.json +9 -5
  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 +9 -7
  13. package/src/combine.ts +13 -5
  14. package/src/constants/cliFunctions.ts +38 -1
  15. package/src/constants/common.ts +31 -5
  16. package/src/constants/constants.ts +28 -26
  17. package/src/constants/questions.ts +4 -1
  18. package/src/crawlers/commonCrawlerFunc.ts +114 -152
  19. package/src/crawlers/crawlDomain.ts +25 -32
  20. package/src/crawlers/crawlIntelligentSitemap.ts +7 -1
  21. package/src/crawlers/crawlLocalFile.ts +1 -1
  22. package/src/crawlers/crawlSitemap.ts +1 -1
  23. package/src/crawlers/custom/flagUnlabelledClickableElements.ts +546 -472
  24. package/src/crawlers/customAxeFunctions.ts +1 -1
  25. package/src/index.ts +2 -2
  26. package/src/mergeAxeResults.ts +590 -214
  27. package/src/screenshotFunc/pdfScreenshotFunc.ts +3 -3
  28. package/src/static/ejs/partials/components/scanAbout.ejs +65 -0
  29. package/src/static/ejs/partials/components/wcagCompliance.ejs +10 -29
  30. package/src/static/ejs/partials/footer.ejs +10 -13
  31. package/src/static/ejs/partials/scripts/categorySummary.ejs +2 -2
  32. package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
  33. package/src/static/ejs/partials/scripts/reportSearch.ejs +1 -0
  34. package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +54 -52
  35. package/src/static/ejs/partials/scripts/scanAboutScript.ejs +38 -0
  36. package/src/static/ejs/partials/styles/styles.ejs +26 -1
  37. package/src/static/ejs/partials/summaryMain.ejs +15 -42
  38. package/src/static/ejs/report.ejs +22 -12
  39. package/src/utils.ts +10 -2
  40. package/src/xPathToCss.ts +186 -0
  41. package/a11y-scan-results.zip +0 -0
  42. 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.20",
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": {
@@ -93,4 +97,4 @@
93
97
  "url": "https://github.com/GovTechSG/oobee/issues"
94
98
  },
95
99
  "homepage": "https://github.com/GovTechSG/oobee#readme"
96
- }
100
+ }
@@ -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
@@ -184,11 +184,10 @@ Usage: npm run cli -- -c <crawler> -d <device> -w <viewport> -u <url> OPTIONS`,
184
184
  return option;
185
185
  })
186
186
  .check(argvs => {
187
- if (
188
- (argvs.scanner === ScannerTypes.CUSTOM || argvs.scanner === ScannerTypes.LOCALFILE) &&
189
- argvs.maxpages
190
- ) {
191
- throw new Error('-p or --maxpages is only available in website and sitemap scans.');
187
+ if (argvs.scanner === ScannerTypes.CUSTOM && argvs.maxpages) {
188
+ throw new Error(
189
+ '-p or --maxpages is only available in website, sitemap and local file scans.',
190
+ );
192
191
  }
193
192
  return true;
194
193
  })
@@ -209,6 +208,9 @@ const scanInit = async (argvs: Answers): Promise<string> => {
209
208
 
210
209
  const updatedArgvs = { ...argvs };
211
210
 
211
+ // Cannot use data.browser and data.isHeadless as the connectivity check comes first before prepareData
212
+ setHeadlessMode(updatedArgvs.browserToRun, updatedArgvs.headless);
213
+
212
214
  // let chromeDataDir = null;
213
215
  // let edgeDataDir = null;
214
216
  // Empty string for profile directory will use incognito mode in playwright
@@ -338,8 +340,6 @@ const scanInit = async (argvs: Answers): Promise<string> => {
338
340
  }
339
341
  }
340
342
 
341
- setHeadlessMode(data.browser, data.isHeadless);
342
-
343
343
  const screenToScan = getScreenToScan(
344
344
  updatedArgvs.deviceChosen,
345
345
  updatedArgvs.customDevice,
@@ -394,7 +394,9 @@ const optionsAnswer: Answers = {
394
394
  blacklistedPatternsFilename: options.blacklistedPatternsFilename,
395
395
  playwrightDeviceDetailsObject: options.playwrightDeviceDetailsObject,
396
396
  ruleset: options.ruleset,
397
+ generateJsonFiles: options.generateJsonFiles,
397
398
  };
399
+
398
400
  await scanInit(optionsAnswer);
399
401
  process.exit(0);
400
402
 
package/src/combine.ts CHANGED
@@ -48,18 +48,19 @@ const combineRun = async (details: Data, deviceToScan: string) => {
48
48
  maxRequestsPerCrawl,
49
49
  browser,
50
50
  userDataDirectory,
51
- strategy,
52
- specifiedMaxConcurrency,
51
+ strategy, // Allow subdomains: if checked, = 'same-domain'
52
+ specifiedMaxConcurrency, // Slow scan mode: if checked, = '1'
53
53
  fileTypes,
54
54
  blacklistedPatternsFilename,
55
- includeScreenshots,
56
- followRobots,
55
+ includeScreenshots, // Include screenshots: if checked, = 'true'
56
+ followRobots, // Adhere to robots.txt: if checked, = 'true'
57
57
  metadata,
58
58
  customFlowLabel = 'Custom Flow',
59
59
  extraHTTPHeaders,
60
60
  safeMode,
61
61
  zip,
62
- ruleset,
62
+ ruleset, // Enable custom checks, Enable WCAG AAA: if checked, = 'enable-wcag-aaa')
63
+ generateJsonFiles,
63
64
  } = envDetails;
64
65
 
65
66
  process.env.CRAWLEE_LOG_LEVEL = 'ERROR';
@@ -90,6 +91,12 @@ const combineRun = async (details: Data, deviceToScan: string) => {
90
91
  crawlType: type,
91
92
  requestUrl: finalUrl,
92
93
  urlsCrawled: new UrlsCrawled(),
94
+ isIncludeScreenshots: envDetails.includeScreenshots,
95
+ isAllowSubdomains: envDetails.strategy,
96
+ isEnableCustomChecks: envDetails.ruleset,
97
+ isEnableWcagAaa: envDetails.ruleset,
98
+ isSlowScanMode: envDetails.specifiedMaxConcurrency,
99
+ isAdhereRobots: envDetails.followRobots,
93
100
  };
94
101
 
95
102
  const viewportSettings: ViewportSettingsClass = new ViewportSettingsClass(
@@ -214,6 +221,7 @@ const combineRun = async (details: Data, deviceToScan: string) => {
214
221
  undefined,
215
222
  scanDetails,
216
223
  zip,
224
+ generateJsonFiles,
217
225
  );
218
226
  const [name, email] = nameEmail.split(':');
219
227
 
@@ -270,7 +270,9 @@ export const cliOptions: { [key: string]: Options } = {
270
270
  coerce: option => {
271
271
  const validChoices = Object.values(RuleFlags);
272
272
  const userChoices: string[] = option.split(',');
273
- const invalidUserChoices = userChoices.filter(choice => !validChoices.includes(choice as RuleFlags));
273
+ const invalidUserChoices = userChoices.filter(
274
+ choice => !validChoices.includes(choice as RuleFlags),
275
+ );
274
276
  if (invalidUserChoices.length > 0) {
275
277
  printMessage(
276
278
  [
@@ -294,6 +296,41 @@ export const cliOptions: { [key: string]: Options } = {
294
296
  return userChoices;
295
297
  },
296
298
  },
299
+ g: {
300
+ alias: 'generateJsonFiles',
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
+ `,
317
+ type: 'string',
318
+ requiresArg: true,
319
+ default: 'no',
320
+ demandOption: false,
321
+ coerce: value => {
322
+ const validYes = ['yes', 'y'];
323
+ const validNo = ['no', 'n'];
324
+
325
+ if (validYes.includes(value.toLowerCase())) {
326
+ return true;
327
+ }
328
+ if (validNo.includes(value.toLowerCase())) {
329
+ return false;
330
+ }
331
+ throw new Error(`Invalid value "${value}" for --generate. Use "yes", "y", "no", or "n".`);
332
+ },
333
+ },
297
334
  };
298
335
 
299
336
  export const configureReportSetting = (isEnabled: boolean): void => {
@@ -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
  }
@@ -579,6 +594,7 @@ export const prepareData = async (argv: Answers): Promise<Data> => {
579
594
  safeMode,
580
595
  zip,
581
596
  ruleset,
597
+ generateJsonFiles,
582
598
  } = argv;
583
599
 
584
600
  // construct filename for scan results
@@ -625,6 +641,7 @@ export const prepareData = async (argv: Answers): Promise<Data> => {
625
641
  safeMode,
626
642
  zip,
627
643
  ruleset,
644
+ generateJsonFiles,
628
645
  };
629
646
  };
630
647
 
@@ -1768,15 +1785,24 @@ export const getPlaywrightLaunchOptions = (browser?: string): LaunchOptions => {
1768
1785
  if (browser) {
1769
1786
  channel = browser;
1770
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
+
1771
1792
  const options: LaunchOptions = {
1772
1793
  // Drop the --use-mock-keychain flag to allow MacOS devices
1773
1794
  // to use the cloned cookies.
1774
- ignoreDefaultArgs: ['--use-mock-keychain'],
1795
+ ignoreDefaultArgs: ['--use-mock-keychain', '--headless'],
1796
+ // necessary from Chrome 132 to use our own headless=new flag
1775
1797
  args: constants.launchOptionsArgs,
1798
+ headless: false,
1776
1799
  ...(channel && { channel }), // Having no channel is equivalent to "chromium"
1777
1800
  };
1801
+
1802
+ // Necessary as Chrome 132 does not support headless=old
1803
+ options.headless = false;
1804
+
1778
1805
  if (proxy) {
1779
- options.headless = false;
1780
1806
  options.slowMo = 1000; // To ensure server-side rendered proxy page is loaded
1781
1807
  } else if (browser === BrowserTypes.EDGE && os.platform() === 'win32') {
1782
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,