@govtechsg/oobee 0.10.87 → 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.
- package/.github/workflows/docker-push-ghcr.yml +49 -0
- package/DETAILS_OUTPUT_EXAMPLES.md +178 -0
- package/Dockerfile +6 -7
- package/dist/combine.js +1 -0
- package/dist/crawlers/commonCrawlerFunc.js +523 -2
- package/dist/crawlers/crawlLocalFile.js +2 -2
- package/dist/crawlers/custom/extractAndGradeText.js +1 -1
- package/dist/crawlers/custom/getAxeConfiguration.js +26 -21
- package/dist/crawlers/custom/gradeReadability.js +1 -1
- package/dist/npmIndex.js +16 -12
- package/dist/screenshotFunc/htmlScreenshotFunc.js +67 -0
- package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +7 -4
- package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +7 -4
- package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +2 -1
- package/examples/oobee-test-details-runner.js +214 -0
- package/examples/test-violations.html +42 -0
- package/package.json +1 -1
- package/src/combine.ts +1 -0
- package/src/crawlers/commonCrawlerFunc.ts +625 -2
- package/src/crawlers/crawlLocalFile.ts +4 -1
- package/src/crawlers/custom/extractAndGradeText.ts +1 -1
- package/src/crawlers/custom/getAxeConfiguration.ts +25 -23
- package/src/crawlers/custom/gradeReadability.ts +1 -1
- package/src/npmIndex.ts +17 -12
- package/src/screenshotFunc/htmlScreenshotFunc.ts +81 -1
- package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +7 -4
- package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +7 -4
- package/src/static/ejs/partials/scripts/ruleModal/utilities.ejs +2 -1
|
@@ -8,6 +8,7 @@ import constants, {
|
|
|
8
8
|
UrlsCrawled,
|
|
9
9
|
STATUS_CODE_METADATA,
|
|
10
10
|
FileTypes,
|
|
11
|
+
RuleFlags,
|
|
11
12
|
} from '../constants/constants.js';
|
|
12
13
|
import { ViewportSettingsClass } from '../combine.js';
|
|
13
14
|
import {
|
|
@@ -35,6 +36,7 @@ export const crawlLocalFile = async ({
|
|
|
35
36
|
includeScreenshots,
|
|
36
37
|
extraHTTPHeaders,
|
|
37
38
|
scanDuration = 0,
|
|
39
|
+
ruleset = [],
|
|
38
40
|
fromCrawlIntelligentSitemap = false,
|
|
39
41
|
userUrlInputFromIntelligent = null,
|
|
40
42
|
datasetFromIntelligent = null,
|
|
@@ -53,6 +55,7 @@ export const crawlLocalFile = async ({
|
|
|
53
55
|
includeScreenshots: boolean;
|
|
54
56
|
extraHTTPHeaders: Record<string, string>;
|
|
55
57
|
scanDuration?: number;
|
|
58
|
+
ruleset?: RuleFlags[];
|
|
56
59
|
fromCrawlIntelligentSitemap?: boolean;
|
|
57
60
|
userUrlInputFromIntelligent?: string | null;
|
|
58
61
|
datasetFromIntelligent?: Dataset | null;
|
|
@@ -178,7 +181,7 @@ export const crawlLocalFile = async ({
|
|
|
178
181
|
return urlsCrawled;
|
|
179
182
|
}
|
|
180
183
|
|
|
181
|
-
const results = await runAxeScript({ includeScreenshots, page, randomToken });
|
|
184
|
+
const results = await runAxeScript({ includeScreenshots, page, randomToken, ruleset });
|
|
182
185
|
|
|
183
186
|
const actualUrl = page.url() || request.loadedUrl || url;
|
|
184
187
|
|
|
@@ -45,7 +45,7 @@ export async function extractAndGradeText(page: Page): Promise<string> {
|
|
|
45
45
|
|
|
46
46
|
// Determine the return value
|
|
47
47
|
const result =
|
|
48
|
-
readabilityScore
|
|
48
|
+
readabilityScore <= 0 || readabilityScore > 50 ? '' : readabilityScore.toString();
|
|
49
49
|
|
|
50
50
|
return result;
|
|
51
51
|
} catch (error) {
|
|
@@ -10,6 +10,12 @@ export function getAxeConfiguration({
|
|
|
10
10
|
gradingReadabilityFlag?: string;
|
|
11
11
|
disableOobee?: boolean;
|
|
12
12
|
}) {
|
|
13
|
+
function getReadabilityInterpretation(score: string): string {
|
|
14
|
+
const num = parseFloat(score);
|
|
15
|
+
if (Number.isNaN(num)) return '';
|
|
16
|
+
if (num > 30) return 'It is targeted for junior college (JC) level comprehension and above.';
|
|
17
|
+
return 'It is targeted for university graduate level comprehension and above.';
|
|
18
|
+
}
|
|
13
19
|
return {
|
|
14
20
|
branding: {
|
|
15
21
|
application: 'oobee',
|
|
@@ -39,7 +45,7 @@ export function getAxeConfiguration({
|
|
|
39
45
|
return !node.dataset.flagged; // fail any element with a data-flagged attribute set to true
|
|
40
46
|
},
|
|
41
47
|
},
|
|
42
|
-
...(enableWcagAaa
|
|
48
|
+
...((enableWcagAaa && gradingReadabilityFlag !== '')
|
|
43
49
|
? [
|
|
44
50
|
{
|
|
45
51
|
id: 'oobee-grading-text-contents',
|
|
@@ -47,17 +53,11 @@ export function getAxeConfiguration({
|
|
|
47
53
|
impact: 'moderate' as ImpactValue,
|
|
48
54
|
messages: {
|
|
49
55
|
pass: 'The text content is easy to understand.',
|
|
50
|
-
fail:
|
|
51
|
-
incomplete: `
|
|
52
|
-
}.\nThe target passing score is above 50, indicating content readable by university students and lower grade levels.\nA higher score reflects better readability.`,
|
|
56
|
+
fail: `Text content is potentially difficult to read.\n It scored ${gradingReadabilityFlag} out of 50 on the Flesch-Kincaid Readability Test.\n ${getReadabilityInterpretation(gradingReadabilityFlag)}`,
|
|
57
|
+
incomplete: `Text content is potentially difficult to read.\n It scored ${gradingReadabilityFlag} out of 50 on the Flesch-Kincaid Readability Test.\n ${getReadabilityInterpretation(gradingReadabilityFlag)}`,
|
|
53
58
|
},
|
|
54
59
|
},
|
|
55
|
-
evaluate: (_node: HTMLElement) =>
|
|
56
|
-
if (gradingReadabilityFlag === '') {
|
|
57
|
-
return true; // Pass if no readability issues
|
|
58
|
-
}
|
|
59
|
-
// Fail if readability issues are detected
|
|
60
|
-
},
|
|
60
|
+
evaluate: (_node: HTMLElement) => false,
|
|
61
61
|
},
|
|
62
62
|
]
|
|
63
63
|
: []),
|
|
@@ -88,19 +88,21 @@ export function getAxeConfiguration({
|
|
|
88
88
|
helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
|
|
89
89
|
},
|
|
90
90
|
},
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
91
|
+
...((enableWcagAaa && gradingReadabilityFlag !== '')
|
|
92
|
+
? [{
|
|
93
|
+
id: 'oobee-grading-text-contents',
|
|
94
|
+
selector: 'html',
|
|
95
|
+
enabled: true,
|
|
96
|
+
any: ['oobee-grading-text-contents'],
|
|
97
|
+
tags: ['wcag2aaa', 'wcag315'],
|
|
98
|
+
metadata: {
|
|
99
|
+
description:
|
|
100
|
+
'Text content should be easy to understand for individuals with education levels up to university graduates. If the text content is difficult to understand, provide supplemental content or a version that is easy to understand.',
|
|
101
|
+
help: 'Text content should be clear and plain to ensure that it is easily understood.',
|
|
102
|
+
helpUrl: 'https://www.wcag.com/uncategorized/3-1-5-reading-level/',
|
|
103
|
+
},
|
|
104
|
+
}]
|
|
105
|
+
: []),
|
|
104
106
|
]
|
|
105
107
|
.filter(rule => (disableOobee ? !rule.id.startsWith('oobee') : true))
|
|
106
108
|
.concat(
|
|
@@ -20,7 +20,7 @@ export function gradeReadability(sentences: string[]): string {
|
|
|
20
20
|
|
|
21
21
|
// Determine the return value
|
|
22
22
|
const result =
|
|
23
|
-
readabilityScore
|
|
23
|
+
readabilityScore <= 0 || readabilityScore > 50 ? '' : readabilityScore.toString();
|
|
24
24
|
|
|
25
25
|
return result;
|
|
26
26
|
} catch (error) {
|
package/src/npmIndex.ts
CHANGED
|
@@ -12,10 +12,10 @@ import {
|
|
|
12
12
|
getPlaywrightLaunchOptions,
|
|
13
13
|
submitForm,
|
|
14
14
|
} from './constants/common.js';
|
|
15
|
-
import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
|
15
|
+
import { createCrawleeSubFolders, enrichViolationMessages, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
|
16
16
|
import { createAndUpdateResultsFolders, getVersion } from './utils.js';
|
|
17
17
|
import generateArtifacts, { createBasicFormHTMLSnippet, sendWcagBreakdownToSentry } from './mergeAxeResults.js';
|
|
18
|
-
import { takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
|
|
18
|
+
import { enrichColorContrastDOMContext, takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
|
|
19
19
|
import { consoleLogger, silentLogger } from './logs.js';
|
|
20
20
|
import { alertMessageOptions } from './constants/cliFunctions.js';
|
|
21
21
|
import { evaluateAltText } from './crawlers/custom/evaluateAltText.js';
|
|
@@ -86,6 +86,13 @@ const getOobeeFunctionsScript = (disableOobee: boolean, enableWcagAaa: boolean)
|
|
|
86
86
|
window.xPathToCss = ${xPathToCss.toString()};
|
|
87
87
|
window.extractText = ${extractText.toString()};
|
|
88
88
|
|
|
89
|
+
function getReadabilityInterpretation(score) {
|
|
90
|
+
const num = parseFloat(score);
|
|
91
|
+
if (Number.isNaN(num)) return '';
|
|
92
|
+
if (num > 30) return 'It is targeted for junior college (JC) level comprehension and above.';
|
|
93
|
+
return 'It is targeted for university graduate level comprehension and above.';
|
|
94
|
+
}
|
|
95
|
+
|
|
89
96
|
function getAxeConfiguration({
|
|
90
97
|
enableWcagAaa = false,
|
|
91
98
|
gradingReadabilityFlag = '',
|
|
@@ -120,7 +127,7 @@ const getOobeeFunctionsScript = (disableOobee: boolean, enableWcagAaa: boolean)
|
|
|
120
127
|
return !node.dataset.flagged; // fail any element with a data-flagged attribute set to true
|
|
121
128
|
},
|
|
122
129
|
},
|
|
123
|
-
...((enableWcagAaa && !disableOobee)
|
|
130
|
+
...((enableWcagAaa && !disableOobee && gradingReadabilityFlag !== '')
|
|
124
131
|
? [
|
|
125
132
|
{
|
|
126
133
|
id: 'oobee-grading-text-contents',
|
|
@@ -128,16 +135,11 @@ const getOobeeFunctionsScript = (disableOobee: boolean, enableWcagAaa: boolean)
|
|
|
128
135
|
impact: 'moderate',
|
|
129
136
|
messages: {
|
|
130
137
|
pass: 'The text content is easy to understand.',
|
|
131
|
-
fail:
|
|
132
|
-
incomplete: \`
|
|
138
|
+
fail: \`Text content is potentially difficult to read.\n It scored \${gradingReadabilityFlag} out of 50 on the Flesch-Kincaid Readability Test.\n \${getReadabilityInterpretation(gradingReadabilityFlag)}\`,
|
|
139
|
+
incomplete: \`Text content is potentially difficult to read.\n It scored \${gradingReadabilityFlag} out of 50 on the Flesch-Kincaid Readability Test.\n \${getReadabilityInterpretation(gradingReadabilityFlag)}\`,
|
|
133
140
|
},
|
|
134
141
|
},
|
|
135
|
-
evaluate: (_node) =>
|
|
136
|
-
if (gradingReadabilityFlag === '') {
|
|
137
|
-
return true; // Pass if no readability issues
|
|
138
|
-
}
|
|
139
|
-
// Fail if readability issues are detected
|
|
140
|
-
},
|
|
142
|
+
evaluate: (_node) => false,
|
|
141
143
|
},
|
|
142
144
|
]
|
|
143
145
|
: []),
|
|
@@ -168,7 +170,7 @@ const getOobeeFunctionsScript = (disableOobee: boolean, enableWcagAaa: boolean)
|
|
|
168
170
|
helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
|
|
169
171
|
},
|
|
170
172
|
},
|
|
171
|
-
...((enableWcagAaa && !disableOobee)
|
|
173
|
+
...((enableWcagAaa && !disableOobee && gradingReadabilityFlag !== '')
|
|
172
174
|
? [
|
|
173
175
|
{
|
|
174
176
|
id: 'oobee-grading-text-contents',
|
|
@@ -864,6 +866,9 @@ export const scanPage = async (
|
|
|
864
866
|
return window.runA11yScan();
|
|
865
867
|
});
|
|
866
868
|
|
|
869
|
+
await enrichViolationMessages(scanResult.axeScanResults, page);
|
|
870
|
+
await enrichColorContrastDOMContext(scanResult.axeScanResults.violations, page);
|
|
871
|
+
|
|
867
872
|
scanData.push({
|
|
868
873
|
axeScanResults: scanResult.axeScanResults,
|
|
869
874
|
pageUrl: page.url(),
|
|
@@ -5,10 +5,89 @@ import path from 'path';
|
|
|
5
5
|
// import { silentLogger } from '../logs.js';
|
|
6
6
|
import { Result } from 'axe-core';
|
|
7
7
|
import { Page } from 'playwright';
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
ContrastDOMContext,
|
|
10
|
+
NodeResultWithScreenshot,
|
|
11
|
+
ResultWithScreenshot,
|
|
12
|
+
} from '../crawlers/commonCrawlerFunc.js';
|
|
9
13
|
|
|
10
14
|
const screenshotMap: Record<string, string> = {}; // Map of screenshot hashkey to its buffer value and screenshot path
|
|
11
15
|
|
|
16
|
+
export const enrichColorContrastDOMContext = async (
|
|
17
|
+
violations: Result[],
|
|
18
|
+
page: Page,
|
|
19
|
+
): Promise<void> => {
|
|
20
|
+
for (const violation of violations) {
|
|
21
|
+
if (violation.id !== 'color-contrast' && violation.id !== 'color-contrast-enhanced') continue;
|
|
22
|
+
|
|
23
|
+
for (const node of violation.nodes) {
|
|
24
|
+
const { target } = node;
|
|
25
|
+
const selector = target.length === 1 && typeof target[0] === 'string' ? target[0] : null;
|
|
26
|
+
if (!selector) continue;
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
const domContext = await page
|
|
30
|
+
.evaluate((sel: string): ContrastDOMContext | null => {
|
|
31
|
+
const el = document.querySelector(sel);
|
|
32
|
+
if (!el) return null;
|
|
33
|
+
|
|
34
|
+
const style = window.getComputedStyle(el);
|
|
35
|
+
const bgImage = style.backgroundImage;
|
|
36
|
+
const hasGradient = bgImage !== 'none' && bgImage.includes('gradient');
|
|
37
|
+
const hasBackgroundImage = bgImage !== 'none' && !hasGradient;
|
|
38
|
+
|
|
39
|
+
let hasReducedOpacity = parseFloat(style.opacity) < 1;
|
|
40
|
+
let ancestorHasGradient = false;
|
|
41
|
+
let ancestorHasBackgroundImage = false;
|
|
42
|
+
|
|
43
|
+
let ancestor = el.parentElement;
|
|
44
|
+
while (ancestor && ancestor.tagName !== 'HTML') {
|
|
45
|
+
const anStyle = window.getComputedStyle(ancestor);
|
|
46
|
+
if (!hasReducedOpacity && parseFloat(anStyle.opacity) < 1) {
|
|
47
|
+
hasReducedOpacity = true;
|
|
48
|
+
}
|
|
49
|
+
const anBgImg = anStyle.backgroundImage;
|
|
50
|
+
if (anBgImg !== 'none') {
|
|
51
|
+
if (!ancestorHasGradient && anBgImg.includes('gradient')) {
|
|
52
|
+
ancestorHasGradient = true;
|
|
53
|
+
} else if (!ancestorHasBackgroundImage) {
|
|
54
|
+
ancestorHasBackgroundImage = true;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
ancestor = ancestor.parentElement;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const mixBlendMode = style.mixBlendMode !== 'normal' ? style.mixBlendMode : null;
|
|
61
|
+
const backdropFilter =
|
|
62
|
+
style.backdropFilter && style.backdropFilter !== 'none'
|
|
63
|
+
? style.backdropFilter
|
|
64
|
+
: null;
|
|
65
|
+
const filter = style.filter && style.filter !== 'none' ? style.filter : null;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
backgroundImage: bgImage !== 'none' ? bgImage : '',
|
|
69
|
+
hasGradient,
|
|
70
|
+
hasBackgroundImage,
|
|
71
|
+
ancestorHasGradient,
|
|
72
|
+
ancestorHasBackgroundImage,
|
|
73
|
+
hasReducedOpacity,
|
|
74
|
+
mixBlendMode,
|
|
75
|
+
backdropFilter,
|
|
76
|
+
filter,
|
|
77
|
+
};
|
|
78
|
+
}, selector)
|
|
79
|
+
.catch(() => null);
|
|
80
|
+
|
|
81
|
+
if (domContext) {
|
|
82
|
+
(node as NodeResultWithScreenshot).contrastDOMContext = domContext;
|
|
83
|
+
}
|
|
84
|
+
} catch {
|
|
85
|
+
// Non-critical; proceed without DOM context
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
};
|
|
90
|
+
|
|
12
91
|
export const takeScreenshotForHTMLElements = async (
|
|
13
92
|
violations: Result[],
|
|
14
93
|
page: Page,
|
|
@@ -67,6 +146,7 @@ export const takeScreenshotForHTMLElements = async (
|
|
|
67
146
|
} catch (e) {
|
|
68
147
|
// consoleLogger.info(`Unable to take element screenshot at ${selector}`);
|
|
69
148
|
}
|
|
149
|
+
|
|
70
150
|
}
|
|
71
151
|
newViolationNodes.push(nodeWithScreenshotPath);
|
|
72
152
|
}
|
|
@@ -239,14 +239,17 @@
|
|
|
239
239
|
${item.xpath ? createXpathSection(item.xpath) : ''}
|
|
240
240
|
${createElementSection(item)}
|
|
241
241
|
${
|
|
242
|
-
|
|
243
|
-
|
|
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">
|
|
244
246
|
<div class="fw-semibold page-item-card-section-title">Details</div>
|
|
245
247
|
<div class="page-item-card-section-content">
|
|
246
|
-
${generateItemMessageElement(item.displayNeedsReview, item.message)}
|
|
248
|
+
${generateItemMessageElement(item.displayNeedsReview || true, item.message)}
|
|
247
249
|
</div>
|
|
248
250
|
</div>`
|
|
249
|
-
|
|
251
|
+
: '';
|
|
252
|
+
})()
|
|
250
253
|
}
|
|
251
254
|
${isPurpleAiRule ? createAiSuggestionSection(item, oobeeAiQueryLabel, aiConfig) : ''}
|
|
252
255
|
${showGenAiUI ? createGenAiSuggestFixSection(item, aiConfig) : ''}
|
|
@@ -393,14 +393,17 @@
|
|
|
393
393
|
</div>
|
|
394
394
|
|
|
395
395
|
${
|
|
396
|
-
|
|
397
|
-
|
|
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>
|
|
@@ -53,7 +53,8 @@
|
|
|
53
53
|
if (htmlEscapedMessageArray.length === 1) {
|
|
54
54
|
return `<p class="mb-0">${htmlEscapedMessageArray[0]}</p>`;
|
|
55
55
|
} else {
|
|
56
|
-
|
|
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;
|