@govtechsg/oobee 0.10.86 → 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/.github/workflows/image.yml +2 -3
- package/DETAILS_OUTPUT_EXAMPLES.md +178 -0
- package/Dockerfile +6 -7
- package/dist/cli.js +18 -5
- package/dist/combine.js +3 -0
- package/dist/constants/cliFunctions.js +2 -2
- package/dist/constants/common.js +55 -13
- package/dist/crawlers/commonCrawlerFunc.js +523 -2
- package/dist/crawlers/crawlDomain.js +38 -13
- package/dist/crawlers/crawlIntelligentSitemap.js +62 -30
- package/dist/crawlers/crawlLocalFile.js +2 -2
- package/dist/crawlers/crawlSitemap.js +44 -5
- 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/crawlers/custom/utils.js +81 -40
- package/dist/generateHtmlReport.js +18 -11
- package/dist/mergeAxeResults/itemReferences.js +60 -25
- package/dist/mergeAxeResults/sentryTelemetry.js +4 -1
- package/dist/mergeAxeResults.js +18 -9
- package/dist/npmIndex.js +16 -12
- package/dist/screenshotFunc/htmlScreenshotFunc.js +67 -0
- package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
- package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +45 -6
- package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +8 -5
- package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
- package/dist/static/ejs/partials/scripts/ruleModal/utilities.ejs +2 -1
- package/dist/static/ejs/summary.ejs +18 -12
- package/dist/utils.js +4 -3
- package/examples/oobee-test-details-runner.js +214 -0
- package/examples/test-violations.html +42 -0
- package/fix-summary-html-oom-pr.md +62 -0
- package/package.json +5 -5
- package/src/cli.ts +19 -5
- package/src/combine.ts +3 -0
- package/src/constants/cliFunctions.ts +2 -2
- package/src/constants/common.ts +65 -12
- package/src/crawlers/commonCrawlerFunc.ts +625 -2
- package/src/crawlers/crawlDomain.ts +39 -13
- package/src/crawlers/crawlIntelligentSitemap.ts +63 -30
- package/src/crawlers/crawlLocalFile.ts +4 -1
- package/src/crawlers/crawlSitemap.ts +50 -3
- 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/crawlers/custom/utils.ts +99 -43
- package/src/generateHtmlReport.ts +21 -11
- package/src/mergeAxeResults/itemReferences.ts +70 -26
- package/src/mergeAxeResults/sentryTelemetry.ts +4 -1
- package/src/mergeAxeResults.ts +21 -11
- package/src/npmIndex.ts +17 -12
- package/src/screenshotFunc/htmlScreenshotFunc.ts +81 -1
- package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
- package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +45 -6
- package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +8 -5
- package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
- package/src/static/ejs/partials/scripts/ruleModal/utilities.ejs +2 -1
- package/src/static/ejs/summary.ejs +18 -12
- package/src/utils.ts +4 -3
- package/testStaticJSScanner.html +1 -1
|
@@ -25,31 +25,66 @@ export const buildHtmlGroups = (rule, items, pageUrl) => {
|
|
|
25
25
|
}
|
|
26
26
|
});
|
|
27
27
|
};
|
|
28
|
+
/*
|
|
29
|
+
// Commenting this out for now as we are not including htmlGroups in the embedded report payload to keep it lean.
|
|
30
|
+
// We can revisit this if we want to include htmlGroups in the future and need a reference builder for it.
|
|
31
|
+
const toHtmlGroupReference = (item: any) => {
|
|
32
|
+
if (typeof item === 'string') {
|
|
33
|
+
return item;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return `${item?.html || 'No HTML element'}\x00${item?.xpath || ''}`;
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const cloneCategoryWithReferenceItems = (category: ScanCategory): ScanCategory =>
|
|
40
|
+
({
|
|
41
|
+
...category,
|
|
42
|
+
rules: category.rules.map(
|
|
43
|
+
rule =>
|
|
44
|
+
({
|
|
45
|
+
...rule,
|
|
46
|
+
pagesAffected: rule.pagesAffected.map(
|
|
47
|
+
page => {
|
|
48
|
+
const { items, ...pageWithoutItems } = page;
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
...pageWithoutItems,
|
|
52
|
+
itemsCount: page.itemsCount ?? (Array.isArray(items) ? items.length : 0),
|
|
53
|
+
items: Array.isArray(items) ? items.map(toHtmlGroupReference) : items,
|
|
54
|
+
} as any;
|
|
55
|
+
},
|
|
56
|
+
),
|
|
57
|
+
}) as any,
|
|
58
|
+
),
|
|
59
|
+
}) as ScanCategory;
|
|
60
|
+
*/
|
|
61
|
+
const cloneCategoryLight = (category, includeHtmlGroups) => ({
|
|
62
|
+
...category,
|
|
63
|
+
rules: category.rules.map(rule => ({
|
|
64
|
+
rule: rule.rule,
|
|
65
|
+
description: rule.description,
|
|
66
|
+
helpUrl: rule.helpUrl,
|
|
67
|
+
conformance: rule.conformance,
|
|
68
|
+
totalItems: rule.totalItems,
|
|
69
|
+
axeImpact: rule.axeImpact,
|
|
70
|
+
...(includeHtmlGroups && rule.htmlGroups ? { htmlGroups: rule.htmlGroups } : {}),
|
|
71
|
+
pagesAffected: rule.pagesAffected.map(page => ({
|
|
72
|
+
url: page.url,
|
|
73
|
+
pageTitle: page.pageTitle,
|
|
74
|
+
itemsCount: page.itemsCount ?? (Array.isArray(page.items) ? page.items.length : 0),
|
|
75
|
+
})),
|
|
76
|
+
})),
|
|
77
|
+
});
|
|
28
78
|
/**
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
79
|
+
* Builds the embedded HTML-report payload from the full scan items.
|
|
80
|
+
* Includes htmlGroups for non-passed categories (Group by HTML Element),
|
|
81
|
+
* excludes them from passed to keep payload within browser memory limits.
|
|
32
82
|
*/
|
|
33
|
-
export const convertItemsToReferences = (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
return;
|
|
41
|
-
rule.pagesAffected.forEach((page) => {
|
|
42
|
-
if (!page.items)
|
|
43
|
-
return;
|
|
44
|
-
page.items = page.items.map((item) => {
|
|
45
|
-
if (typeof item === 'string')
|
|
46
|
-
return item; // Already a reference
|
|
47
|
-
// Use composite key matching buildHtmlGroups
|
|
48
|
-
const htmlKey = `${item.html || 'No HTML element'}\x00${item.xpath || ''}`;
|
|
49
|
-
return htmlKey;
|
|
50
|
-
});
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
});
|
|
54
|
-
return cloned;
|
|
83
|
+
export const convertItemsToReferences = (source) => {
|
|
84
|
+
return {
|
|
85
|
+
mustFix: cloneCategoryLight(source.items.mustFix, true),
|
|
86
|
+
goodToFix: cloneCategoryLight(source.items.goodToFix, true),
|
|
87
|
+
needsReview: cloneCategoryLight(source.items.needsReview, true),
|
|
88
|
+
passed: cloneCategoryLight(source.items.passed, false),
|
|
89
|
+
};
|
|
55
90
|
};
|
|
@@ -110,7 +110,10 @@ const sendWcagBreakdownToSentry = async (appVersion, wcagBreakdown, ruleIdJson,
|
|
|
110
110
|
event_type: 'accessibility_scan',
|
|
111
111
|
scanType: scanInfo.scanType,
|
|
112
112
|
browser: scanInfo.browser,
|
|
113
|
-
entryUrl: scanInfo.entryUrl,
|
|
113
|
+
entryUrl: process.env.OOBEE_SCAN_METADATA ?? scanInfo.entryUrl,
|
|
114
|
+
...(process.env.OOBEE_SCAN_PRODUCT && {
|
|
115
|
+
scanProduct: process.env.OOBEE_SCAN_PRODUCT,
|
|
116
|
+
}),
|
|
114
117
|
},
|
|
115
118
|
user: {
|
|
116
119
|
...(scanInfo.email && scanInfo.name
|
package/dist/mergeAxeResults.js
CHANGED
|
@@ -119,10 +119,10 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
|
|
|
119
119
|
const { topFilePath, bottomFilePath } = await splitHtmlAndCreateFiles(htmlFilePath, storagePath);
|
|
120
120
|
const prefixData = fs.readFileSync(path.join(storagePath, 'report-partial-top.htm.txt'), 'utf-8');
|
|
121
121
|
const suffixData = fs.readFileSync(path.join(storagePath, 'report-partial-bottom.htm.txt'), 'utf-8');
|
|
122
|
-
// Create lighter
|
|
123
|
-
const
|
|
122
|
+
// Create the lighter scanItems payload for embedding in the HTML report.
|
|
123
|
+
const lightScanItemsPayload = convertItemsToReferences(allIssues);
|
|
124
124
|
// Write the lighter items to a file and get the base64 path
|
|
125
|
-
const { jsonFilePath:
|
|
125
|
+
const { jsonFilePath: lightScanItemsPayloadJsonFilePath, base64FilePath: lightScanItemsPayloadBase64FilePath, } = await writeJsonFileAndCompressedJsonFile(lightScanItemsPayload, storagePath, 'scanItems-light');
|
|
126
126
|
return new Promise((resolve, reject) => {
|
|
127
127
|
const scanDetailsReadStream = fs.createReadStream(scanDetailsFilePath, {
|
|
128
128
|
encoding: 'utf8',
|
|
@@ -135,8 +135,8 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
|
|
|
135
135
|
await Promise.all([
|
|
136
136
|
fs.promises.unlink(topFilePath),
|
|
137
137
|
fs.promises.unlink(bottomFilePath),
|
|
138
|
-
fs.promises.unlink(
|
|
139
|
-
fs.promises.unlink(
|
|
138
|
+
fs.promises.unlink(lightScanItemsPayloadBase64FilePath),
|
|
139
|
+
fs.promises.unlink(lightScanItemsPayloadJsonFilePath),
|
|
140
140
|
]);
|
|
141
141
|
}
|
|
142
142
|
catch (err) {
|
|
@@ -172,22 +172,28 @@ const writeHTML = async (allIssues, storagePath, htmlFilename = 'report', scanDe
|
|
|
172
172
|
} else {
|
|
173
173
|
console.warn('Skipping fetch GenAI feature as it is local report');
|
|
174
174
|
}
|
|
175
|
+
|
|
176
|
+
var scanData = null;
|
|
177
|
+
var scanItems = null;
|
|
175
178
|
\n`);
|
|
176
179
|
outputStream.write('</script>\n<script type="text/plain" id="scanDataRaw">');
|
|
177
180
|
scanDetailsReadStream.pipe(outputStream, { end: false });
|
|
178
181
|
scanDetailsReadStream.on('end', async () => {
|
|
179
182
|
outputStream.write('</script>\n<script>\n');
|
|
180
|
-
outputStream.write("var scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse(document.getElementById('scanDataRaw').textContent); })();\n");
|
|
183
|
+
outputStream.write("var scanDataPromise = (async () => { console.log('Loading scanData...'); scanData = await decodeUnzipParse(document.getElementById('scanDataRaw').textContent); console.log('[report] scanData loaded'); })();\n");
|
|
181
184
|
outputStream.write('</script>\n');
|
|
182
185
|
// Write scanItems in 2MB chunks using a stream to avoid loading entire file into memory
|
|
183
186
|
try {
|
|
184
187
|
let chunkIndex = 1;
|
|
185
|
-
const scanItemsStream = fs.createReadStream(
|
|
188
|
+
const scanItemsStream = fs.createReadStream(lightScanItemsPayloadBase64FilePath, {
|
|
186
189
|
encoding: 'utf8',
|
|
187
190
|
highWaterMark: CHUNK_SIZE,
|
|
188
191
|
});
|
|
189
192
|
for await (const chunk of scanItemsStream) {
|
|
190
|
-
outputStream.write(`<script type="text/plain" id="scanItemsRaw${chunkIndex}">${chunk}</script>\n`);
|
|
193
|
+
const ok = outputStream.write(`<script type="text/plain" id="scanItemsRaw${chunkIndex}">${chunk}</script>\n`);
|
|
194
|
+
if (!ok) {
|
|
195
|
+
await new Promise(resolve => outputStream.once('drain', resolve));
|
|
196
|
+
}
|
|
191
197
|
chunkIndex++;
|
|
192
198
|
}
|
|
193
199
|
outputStream.write('<script>\n');
|
|
@@ -203,6 +209,7 @@ var scanItemsPromise = (async () => {
|
|
|
203
209
|
i++;
|
|
204
210
|
}
|
|
205
211
|
scanItems = await decodeUnzipParse(chunks);
|
|
212
|
+
console.log('[report] scanItems loaded');
|
|
206
213
|
})();\n`);
|
|
207
214
|
outputStream.write(suffixData);
|
|
208
215
|
outputStream.end();
|
|
@@ -722,11 +729,13 @@ generateJsonFiles = false) => {
|
|
|
722
729
|
const browserChannel = getBrowserToRun(randomToken, BrowserTypes.CHROME, false).browserToRun;
|
|
723
730
|
// Should consider refactor constants.userDataDirectory to be a parameter in future
|
|
724
731
|
await retryFunction(() => writeSummaryPdf(storagePath, pagesScanned.length, 'summary', browserChannel, constants.userDataDirectory), 1);
|
|
732
|
+
// Brief delay to allow lingering async crawlee storage operations to flush
|
|
733
|
+
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
725
734
|
try {
|
|
726
735
|
await fs.promises.rm(path.join(storagePath, 'crawlee'), { recursive: true, force: true });
|
|
727
736
|
}
|
|
728
737
|
catch (error) {
|
|
729
|
-
|
|
738
|
+
// Silently ignore — folder may already be gone or still locked
|
|
730
739
|
}
|
|
731
740
|
try {
|
|
732
741
|
await fs.promises.rm(path.join(storagePath, 'pdfs'), { recursive: true, force: true });
|
package/dist/npmIndex.js
CHANGED
|
@@ -6,10 +6,10 @@ import { fileURLToPath } from 'url';
|
|
|
6
6
|
import { EnqueueStrategy } from 'crawlee';
|
|
7
7
|
import constants, { BrowserTypes, RuleFlags, ScannerTypes, a11yRuleShortDescriptionMap, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide } from './constants/constants.js';
|
|
8
8
|
import { deleteClonedProfiles, getBrowserToRun, getPlaywrightLaunchOptions, submitForm, } from './constants/common.js';
|
|
9
|
-
import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
|
9
|
+
import { createCrawleeSubFolders, enrichViolationMessages, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
|
10
10
|
import { createAndUpdateResultsFolders, getVersion } from './utils.js';
|
|
11
11
|
import generateArtifacts, { createBasicFormHTMLSnippet, sendWcagBreakdownToSentry } from './mergeAxeResults.js';
|
|
12
|
-
import { takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
|
|
12
|
+
import { enrichColorContrastDOMContext, takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
|
|
13
13
|
import { consoleLogger } from './logs.js';
|
|
14
14
|
import { alertMessageOptions } from './constants/cliFunctions.js';
|
|
15
15
|
import { evaluateAltText } from './crawlers/custom/evaluateAltText.js';
|
|
@@ -50,6 +50,13 @@ const getOobeeFunctionsScript = (disableOobee, enableWcagAaa) => {
|
|
|
50
50
|
window.xPathToCss = ${xPathToCss.toString()};
|
|
51
51
|
window.extractText = ${extractText.toString()};
|
|
52
52
|
|
|
53
|
+
function getReadabilityInterpretation(score) {
|
|
54
|
+
const num = parseFloat(score);
|
|
55
|
+
if (Number.isNaN(num)) return '';
|
|
56
|
+
if (num > 30) return 'It is targeted for junior college (JC) level comprehension and above.';
|
|
57
|
+
return 'It is targeted for university graduate level comprehension and above.';
|
|
58
|
+
}
|
|
59
|
+
|
|
53
60
|
function getAxeConfiguration({
|
|
54
61
|
enableWcagAaa = false,
|
|
55
62
|
gradingReadabilityFlag = '',
|
|
@@ -84,7 +91,7 @@ const getOobeeFunctionsScript = (disableOobee, enableWcagAaa) => {
|
|
|
84
91
|
return !node.dataset.flagged; // fail any element with a data-flagged attribute set to true
|
|
85
92
|
},
|
|
86
93
|
},
|
|
87
|
-
...((enableWcagAaa && !disableOobee)
|
|
94
|
+
...((enableWcagAaa && !disableOobee && gradingReadabilityFlag !== '')
|
|
88
95
|
? [
|
|
89
96
|
{
|
|
90
97
|
id: 'oobee-grading-text-contents',
|
|
@@ -92,16 +99,11 @@ const getOobeeFunctionsScript = (disableOobee, enableWcagAaa) => {
|
|
|
92
99
|
impact: 'moderate',
|
|
93
100
|
messages: {
|
|
94
101
|
pass: 'The text content is easy to understand.',
|
|
95
|
-
fail:
|
|
96
|
-
incomplete: \`
|
|
102
|
+
fail: \`Text content is potentially difficult to read.\n It scored \${gradingReadabilityFlag} out of 50 on the Flesch-Kincaid Readability Test.\n \${getReadabilityInterpretation(gradingReadabilityFlag)}\`,
|
|
103
|
+
incomplete: \`Text content is potentially difficult to read.\n It scored \${gradingReadabilityFlag} out of 50 on the Flesch-Kincaid Readability Test.\n \${getReadabilityInterpretation(gradingReadabilityFlag)}\`,
|
|
97
104
|
},
|
|
98
105
|
},
|
|
99
|
-
evaluate: (_node) =>
|
|
100
|
-
if (gradingReadabilityFlag === '') {
|
|
101
|
-
return true; // Pass if no readability issues
|
|
102
|
-
}
|
|
103
|
-
// Fail if readability issues are detected
|
|
104
|
-
},
|
|
106
|
+
evaluate: (_node) => false,
|
|
105
107
|
},
|
|
106
108
|
]
|
|
107
109
|
: []),
|
|
@@ -132,7 +134,7 @@ const getOobeeFunctionsScript = (disableOobee, enableWcagAaa) => {
|
|
|
132
134
|
helpUrl: 'https://www.deque.com/blog/accessible-aria-buttons',
|
|
133
135
|
},
|
|
134
136
|
},
|
|
135
|
-
...((enableWcagAaa && !disableOobee)
|
|
137
|
+
...((enableWcagAaa && !disableOobee && gradingReadabilityFlag !== '')
|
|
136
138
|
? [
|
|
137
139
|
{
|
|
138
140
|
id: 'oobee-grading-text-contents',
|
|
@@ -629,6 +631,8 @@ export const scanPage = async (pages, config) => {
|
|
|
629
631
|
const scanResult = await page.evaluate(async () => {
|
|
630
632
|
return window.runA11yScan();
|
|
631
633
|
});
|
|
634
|
+
await enrichViolationMessages(scanResult.axeScanResults, page);
|
|
635
|
+
await enrichColorContrastDOMContext(scanResult.axeScanResults.violations, page);
|
|
632
636
|
scanData.push({
|
|
633
637
|
axeScanResults: scanResult.axeScanResults,
|
|
634
638
|
pageUrl: page.url(),
|
|
@@ -3,6 +3,73 @@ import { createHash } from 'crypto';
|
|
|
3
3
|
import fs from 'fs';
|
|
4
4
|
import path from 'path';
|
|
5
5
|
const screenshotMap = {}; // Map of screenshot hashkey to its buffer value and screenshot path
|
|
6
|
+
export const enrichColorContrastDOMContext = async (violations, page) => {
|
|
7
|
+
for (const violation of violations) {
|
|
8
|
+
if (violation.id !== 'color-contrast' && violation.id !== 'color-contrast-enhanced')
|
|
9
|
+
continue;
|
|
10
|
+
for (const node of violation.nodes) {
|
|
11
|
+
const { target } = node;
|
|
12
|
+
const selector = target.length === 1 && typeof target[0] === 'string' ? target[0] : null;
|
|
13
|
+
if (!selector)
|
|
14
|
+
continue;
|
|
15
|
+
try {
|
|
16
|
+
const domContext = await page
|
|
17
|
+
.evaluate((sel) => {
|
|
18
|
+
const el = document.querySelector(sel);
|
|
19
|
+
if (!el)
|
|
20
|
+
return null;
|
|
21
|
+
const style = window.getComputedStyle(el);
|
|
22
|
+
const bgImage = style.backgroundImage;
|
|
23
|
+
const hasGradient = bgImage !== 'none' && bgImage.includes('gradient');
|
|
24
|
+
const hasBackgroundImage = bgImage !== 'none' && !hasGradient;
|
|
25
|
+
let hasReducedOpacity = parseFloat(style.opacity) < 1;
|
|
26
|
+
let ancestorHasGradient = false;
|
|
27
|
+
let ancestorHasBackgroundImage = false;
|
|
28
|
+
let ancestor = el.parentElement;
|
|
29
|
+
while (ancestor && ancestor.tagName !== 'HTML') {
|
|
30
|
+
const anStyle = window.getComputedStyle(ancestor);
|
|
31
|
+
if (!hasReducedOpacity && parseFloat(anStyle.opacity) < 1) {
|
|
32
|
+
hasReducedOpacity = true;
|
|
33
|
+
}
|
|
34
|
+
const anBgImg = anStyle.backgroundImage;
|
|
35
|
+
if (anBgImg !== 'none') {
|
|
36
|
+
if (!ancestorHasGradient && anBgImg.includes('gradient')) {
|
|
37
|
+
ancestorHasGradient = true;
|
|
38
|
+
}
|
|
39
|
+
else if (!ancestorHasBackgroundImage) {
|
|
40
|
+
ancestorHasBackgroundImage = true;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
ancestor = ancestor.parentElement;
|
|
44
|
+
}
|
|
45
|
+
const mixBlendMode = style.mixBlendMode !== 'normal' ? style.mixBlendMode : null;
|
|
46
|
+
const backdropFilter = style.backdropFilter && style.backdropFilter !== 'none'
|
|
47
|
+
? style.backdropFilter
|
|
48
|
+
: null;
|
|
49
|
+
const filter = style.filter && style.filter !== 'none' ? style.filter : null;
|
|
50
|
+
return {
|
|
51
|
+
backgroundImage: bgImage !== 'none' ? bgImage : '',
|
|
52
|
+
hasGradient,
|
|
53
|
+
hasBackgroundImage,
|
|
54
|
+
ancestorHasGradient,
|
|
55
|
+
ancestorHasBackgroundImage,
|
|
56
|
+
hasReducedOpacity,
|
|
57
|
+
mixBlendMode,
|
|
58
|
+
backdropFilter,
|
|
59
|
+
filter,
|
|
60
|
+
};
|
|
61
|
+
}, selector)
|
|
62
|
+
.catch(() => null);
|
|
63
|
+
if (domContext) {
|
|
64
|
+
node.contrastDOMContext = domContext;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
catch {
|
|
68
|
+
// Non-critical; proceed without DOM context
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
};
|
|
6
73
|
export const takeScreenshotForHTMLElements = async (violations, page, randomToken, locatorTimeout = 2000, maxScreenshots = 100) => {
|
|
7
74
|
const newViolations = [];
|
|
8
75
|
let screenshotCount = 0;
|
|
@@ -28,8 +28,11 @@ async function decodeUnzipParse(input) {
|
|
|
28
28
|
offset += arr.length;
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
-
// Step 2: Decompress with pako (GZIP)
|
|
32
|
-
|
|
31
|
+
// Step 2: Decompress with pako (GZIP) to bytes first to avoid large-string
|
|
32
|
+
// construction inside pako for very large payloads.
|
|
33
|
+
const decompressedBytes = pako.ungzip(merged);
|
|
34
|
+
|
|
35
|
+
const decompressed = new TextDecoder().decode(decompressedBytes);
|
|
33
36
|
|
|
34
37
|
// Step 3: Parse JSON
|
|
35
38
|
return JSON.parse(decompressed);
|
|
@@ -37,4 +40,4 @@ async function decodeUnzipParse(input) {
|
|
|
37
40
|
throw new Error(`Failed to decode/unzip/parse: ${err.message}`);
|
|
38
41
|
}
|
|
39
42
|
}
|
|
40
|
-
</script>
|
|
43
|
+
</script>
|
|
@@ -1,10 +1,44 @@
|
|
|
1
1
|
<script>
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Rebuilds the item list for a page from pre-computed htmlGroups when the light report omits page.items.
|
|
4
|
+
*/
|
|
5
|
+
function buildItemsFromHtmlGroupsForPage(page, ruleInCategory) {
|
|
6
|
+
const htmlGroups = ruleInCategory.htmlGroups || {};
|
|
7
|
+
const resolvedItems = [];
|
|
8
|
+
|
|
9
|
+
Object.values(htmlGroups).forEach(groupData => {
|
|
10
|
+
if (!Array.isArray(groupData.pageUrls) || !groupData.pageUrls.includes(page.url)) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
resolvedItems.push({
|
|
15
|
+
html: groupData.html,
|
|
16
|
+
xpath: groupData.xpath,
|
|
17
|
+
message: groupData.message,
|
|
18
|
+
screenshotPath: groupData.screenshotPath,
|
|
19
|
+
displayNeedsReview: groupData.displayNeedsReview,
|
|
20
|
+
pageUrl: page.url,
|
|
21
|
+
pageTitle: page.pageTitle || page.metadata
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return resolvedItems;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* The embedded report payload now omits page.items and rebuilds occurrences from
|
|
30
|
+
* htmlGroups + page metadata. Keep the older page.items resolution logic below
|
|
31
|
+
* commented for an easy rollback if we need to restore mixed payload support.
|
|
4
32
|
*/
|
|
5
33
|
function resolveItemReferencesForPage(page, ruleInCategory) {
|
|
34
|
+
return buildItemsFromHtmlGroupsForPage(page, ruleInCategory);
|
|
35
|
+
|
|
36
|
+
/*
|
|
6
37
|
const items = page.items || [];
|
|
7
|
-
|
|
38
|
+
|
|
39
|
+
if (items.length === 0) {
|
|
40
|
+
return buildItemsFromHtmlGroupsForPage(page, ruleInCategory);
|
|
41
|
+
}
|
|
8
42
|
|
|
9
43
|
const isReference = typeof items[0] === 'string';
|
|
10
44
|
|
|
@@ -27,6 +61,7 @@
|
|
|
27
61
|
pageTitle: page.pageTitle || page.metadata
|
|
28
62
|
};
|
|
29
63
|
}
|
|
64
|
+
|
|
30
65
|
// Fallback: parse composite key
|
|
31
66
|
const nullByteIndex = compositeKey.indexOf('\x00');
|
|
32
67
|
const html = nullByteIndex !== -1 ? compositeKey.slice(0, nullByteIndex) : compositeKey;
|
|
@@ -40,6 +75,7 @@
|
|
|
40
75
|
pageTitle: page.pageTitle || page.metadata
|
|
41
76
|
};
|
|
42
77
|
});
|
|
78
|
+
*/
|
|
43
79
|
}
|
|
44
80
|
|
|
45
81
|
function buildItemCardsWithPagination(accordionId, category, ruleInCategory, page, index) {
|
|
@@ -203,14 +239,17 @@
|
|
|
203
239
|
${item.xpath ? createXpathSection(item.xpath) : ''}
|
|
204
240
|
${createElementSection(item)}
|
|
205
241
|
${
|
|
206
|
-
|
|
207
|
-
|
|
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">
|
|
208
246
|
<div class="fw-semibold page-item-card-section-title">Details</div>
|
|
209
247
|
<div class="page-item-card-section-content">
|
|
210
|
-
${generateItemMessageElement(item.displayNeedsReview, item.message)}
|
|
248
|
+
${generateItemMessageElement(item.displayNeedsReview || true, item.message)}
|
|
211
249
|
</div>
|
|
212
250
|
</div>`
|
|
213
|
-
|
|
251
|
+
: '';
|
|
252
|
+
})()
|
|
214
253
|
}
|
|
215
254
|
${isPurpleAiRule ? createAiSuggestionSection(item, oobeeAiQueryLabel, aiConfig) : ''}
|
|
216
255
|
${showGenAiUI ? createGenAiSuggestFixSection(item, aiConfig) : ''}
|
|
@@ -86,7 +86,7 @@
|
|
|
86
86
|
// Use pre-computed htmlGroups for count if available, otherwise use pages
|
|
87
87
|
const count = isHtmlGrouping && selectedCategory.htmlGroups
|
|
88
88
|
? Object.keys(selectedCategory.htmlGroups).length
|
|
89
|
-
: selectedCategory.pagesAffected.length;
|
|
89
|
+
: (selectedCategory.pagesAffectedCount || selectedCategory.pagesAffected.length);
|
|
90
90
|
if (isHtmlGrouping) {
|
|
91
91
|
dropdownTitle.innerText = `HTML elements affected by this issue (${count})`;
|
|
92
92
|
} else {
|
|
@@ -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>
|
|
@@ -270,8 +270,8 @@ include('./pageAccordionBuilder') %> <%- include('./constants') %>
|
|
|
270
270
|
if (!Array.isArray(rule.pagesAffected)) return;
|
|
271
271
|
|
|
272
272
|
rule.pagesAffected.sort((a, b) => {
|
|
273
|
-
const lenA = Array.isArray(a.items) ? a.items.length : 0;
|
|
274
|
-
const lenB = Array.isArray(b.items) ? b.items.length : 0;
|
|
273
|
+
const lenA = Array.isArray(a.items) ? a.items.length : a.itemsCount || 0;
|
|
274
|
+
const lenB = Array.isArray(b.items) ? b.items.length : b.itemsCount || 0;
|
|
275
275
|
return lenB - lenA; // DESC
|
|
276
276
|
});
|
|
277
277
|
});
|
|
@@ -295,10 +295,10 @@ include('./pageAccordionBuilder') %> <%- include('./constants') %>
|
|
|
295
295
|
dropdownToggle.innerText = `${ruleInCategory.totalItems} Total occ.`;
|
|
296
296
|
dropdownToggle.setAttribute('aria-label', occurrencesText);
|
|
297
297
|
document.getElementById('expandedRuleDropdownTitle').innerText =
|
|
298
|
-
`Pages affected by this issue (${ruleInCategory.pagesAffected.length})`;
|
|
298
|
+
`Pages affected by this issue (${(ruleInCategory.pagesAffectedCount || ruleInCategory.pagesAffected.length)})`;
|
|
299
299
|
buildExpandedRuleCategoryContent(category, ruleInCategory);
|
|
300
300
|
document.getElementById('expandedRulePageContent').innerText =
|
|
301
|
-
`Total ${ruleInCategory.pagesAffected.length} affected pages`;
|
|
301
|
+
`Total ${(ruleInCategory.pagesAffectedCount || ruleInCategory.pagesAffected.length)} affected pages`;
|
|
302
302
|
}
|
|
303
303
|
}
|
|
304
304
|
});
|
|
@@ -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;
|
|
@@ -21,18 +21,24 @@
|
|
|
21
21
|
%>
|
|
22
22
|
<script>
|
|
23
23
|
const scanItems = <%- JSON.stringify(
|
|
24
|
-
{
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
24
|
+
['mustFix','goodToFix','needsReview','passed'].reduce((acc, cat) => {
|
|
25
|
+
if (items[cat]) {
|
|
26
|
+
acc[cat] = {
|
|
27
|
+
description: items[cat].description,
|
|
28
|
+
totalItems: items[cat].totalItems,
|
|
29
|
+
totalRuleIssues: items[cat].totalRuleIssues,
|
|
30
|
+
rules: (items[cat].rules || []).map(rule => ({
|
|
31
|
+
rule: rule.rule,
|
|
32
|
+
description: rule.description,
|
|
33
|
+
helpUrl: rule.helpUrl,
|
|
34
|
+
conformance: rule.conformance,
|
|
35
|
+
totalItems: rule.totalItems,
|
|
36
|
+
pagesAffected: { length: (rule.pagesAffected || []).length },
|
|
37
|
+
})),
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
return acc;
|
|
41
|
+
}, {})
|
|
36
42
|
).replace(/<\//g, '<\\/') %>
|
|
37
43
|
</script>
|
|
38
44
|
<%- include('partials/scripts/summaryTable') %>
|
package/dist/utils.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from 'fs-extra';
|
|
|
4
4
|
import axe from 'axe-core';
|
|
5
5
|
import { v4 as uuidv4 } from 'uuid';
|
|
6
6
|
import { getDomain } from 'tldts';
|
|
7
|
+
import { normalizeUrl } from '@apify/utilities';
|
|
7
8
|
import constants, { destinationPath, getIntermediateScreenshotsPath, } from './constants/constants.js';
|
|
8
9
|
import { consoleLogger, errorsTxtPath } from './logs.js';
|
|
9
10
|
import { getAxeConfiguration } from './crawlers/custom/getAxeConfiguration.js';
|
|
@@ -852,13 +853,13 @@ export const randomThreeDigitNumberString = () => {
|
|
|
852
853
|
const threeDigitNumber = Math.floor(scaledDecimal) + 100;
|
|
853
854
|
return String(threeDigitNumber);
|
|
854
855
|
};
|
|
856
|
+
export const normUrl = (u) => (u ? normalizeUrl(u) || u : '');
|
|
855
857
|
export const isFollowStrategy = (link1, link2, rule) => {
|
|
858
|
+
if (rule === 'all')
|
|
859
|
+
return true;
|
|
856
860
|
try {
|
|
857
861
|
const parsedLink1 = new URL(link1);
|
|
858
862
|
const parsedLink2 = new URL(link2);
|
|
859
|
-
if (rule === 'all') {
|
|
860
|
-
return true;
|
|
861
|
-
}
|
|
862
863
|
if (rule === 'same-origin') {
|
|
863
864
|
return parsedLink1.origin === parsedLink2.origin;
|
|
864
865
|
}
|