@govtechsg/oobee 0.10.21 → 0.10.29
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.
- package/.github/workflows/docker-test.yml +1 -1
- package/DETAILS.md +40 -25
- package/Dockerfile +41 -47
- package/INSTALLATION.md +1 -1
- package/LICENSE-3RD-PARTY-REPORT.txt +448 -0
- package/LICENSE-3RD-PARTY.txt +19913 -0
- package/README.md +10 -2
- package/__mocks__/mock-report.html +1503 -1360
- package/package.json +8 -4
- package/scripts/decodeUnzipParse.js +29 -0
- package/scripts/install_oobee_dependencies.command +2 -2
- package/scripts/install_oobee_dependencies.ps1 +3 -3
- package/src/cli.ts +3 -2
- package/src/combine.ts +1 -0
- package/src/constants/cliFunctions.ts +17 -3
- package/src/constants/common.ts +29 -5
- package/src/constants/constants.ts +28 -26
- package/src/constants/questions.ts +4 -1
- package/src/crawlers/commonCrawlerFunc.ts +159 -187
- package/src/crawlers/crawlDomain.ts +29 -30
- package/src/crawlers/crawlIntelligentSitemap.ts +7 -1
- package/src/crawlers/crawlLocalFile.ts +1 -1
- package/src/crawlers/crawlSitemap.ts +1 -1
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +546 -472
- package/src/crawlers/customAxeFunctions.ts +2 -2
- package/src/index.ts +0 -2
- package/src/mergeAxeResults.ts +608 -220
- package/src/screenshotFunc/pdfScreenshotFunc.ts +3 -3
- package/src/static/ejs/partials/components/wcagCompliance.ejs +10 -29
- package/src/static/ejs/partials/footer.ejs +10 -13
- package/src/static/ejs/partials/scripts/categorySummary.ejs +2 -2
- package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +3 -0
- package/src/static/ejs/partials/scripts/reportSearch.ejs +1 -0
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +54 -52
- package/src/static/ejs/partials/styles/styles.ejs +4 -0
- package/src/static/ejs/partials/summaryMain.ejs +15 -42
- package/src/static/ejs/report.ejs +21 -12
- package/src/utils.ts +10 -2
- package/src/xPathToCss.ts +186 -0
- package/a11y-scan-results.zip +0 -0
- 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.
|
4
|
+
"version": "0.10.29",
|
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#
|
24
|
-
"playwright": "1.
|
25
|
+
"pdfjs-dist": "github:veraPDF/pdfjs-dist#v4.4.168-taggedPdf-0.1.20",
|
26
|
+
"playwright": "1.50.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="
|
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/
|
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-
|
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,
|
package/src/combine.ts
CHANGED
@@ -97,6 +97,7 @@ const combineRun = async (details: Data, deviceToScan: string) => {
|
|
97
97
|
isEnableWcagAaa: envDetails.ruleset,
|
98
98
|
isSlowScanMode: envDetails.specifiedMaxConcurrency,
|
99
99
|
isAdhereRobots: envDetails.followRobots,
|
100
|
+
deviceChosen: deviceToScan,
|
100
101
|
};
|
101
102
|
|
102
103
|
const viewportSettings: ViewportSettingsClass = new ViewportSettingsClass(
|
@@ -269,7 +269,7 @@ export const cliOptions: { [key: string]: Options } = {
|
|
269
269
|
default: 'default',
|
270
270
|
coerce: option => {
|
271
271
|
const validChoices = Object.values(RuleFlags);
|
272
|
-
const userChoices: string[] = option.split(',');
|
272
|
+
const userChoices: string[] = String(option).split(',');
|
273
273
|
const invalidUserChoices = userChoices.filter(
|
274
274
|
choice => !validChoices.includes(choice as RuleFlags),
|
275
275
|
);
|
@@ -298,8 +298,22 @@ export const cliOptions: { [key: string]: Options } = {
|
|
298
298
|
},
|
299
299
|
g: {
|
300
300
|
alias: 'generateJsonFiles',
|
301
|
-
describe:
|
302
|
-
|
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',
|
package/src/constants/common.ts
CHANGED
@@ -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
|
-
...
|
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
|
-
|
463
|
-
|
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/
|
309
|
-
'WCAG 1.2.2': 'https://www.w3.org/TR/
|
310
|
-
'WCAG 1.3.1': 'https://www.w3.org/TR/
|
311
|
-
// 'WCAG 1.3.4': 'https://www.w3.org/TR/
|
312
|
-
'WCAG 1.3.5': 'https://www.w3.org/TR/
|
313
|
-
'WCAG 1.4.1': 'https://www.w3.org/TR/
|
314
|
-
'WCAG 1.4.2': 'https://www.w3.org/TR/
|
315
|
-
'WCAG 1.4.3': 'https://www.w3.org/TR/
|
316
|
-
'WCAG 1.4.4': 'https://www.w3.org/TR/
|
317
|
-
'WCAG 1.4.6': 'https://www.w3.org/TR/
|
318
|
-
// 'WCAG 1.4.10': 'https://www.w3.org/TR/
|
319
|
-
'WCAG 1.4.12': 'https://www.w3.org/TR/
|
320
|
-
'WCAG 2.1.1': 'https://www.w3.org/TR/
|
321
|
-
'WCAG 2.2.1': 'https://www.w3.org/TR/
|
322
|
-
'WCAG 2.2.2': 'https://www.w3.org/TR/
|
323
|
-
'WCAG 2.2.4': 'https://www.w3.org/TR/
|
324
|
-
'WCAG 2.4.1': 'https://www.w3.org/TR/
|
325
|
-
'WCAG 2.4.2': 'https://www.w3.org/TR/
|
326
|
-
'WCAG 2.4.
|
327
|
-
'WCAG 2.4.
|
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/
|
331
|
-
'WCAG 3.1.2': 'https://www.w3.org/TR/
|
332
|
-
'WCAG 3.
|
333
|
-
'WCAG
|
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/
|
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,
|