@govtechsg/oobee 0.10.20
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/.dockerignore +22 -0
- package/.github/pull_request_template.md +11 -0
- package/.github/workflows/docker-test.yml +54 -0
- package/.github/workflows/image.yml +107 -0
- package/.github/workflows/publish.yml +18 -0
- package/.idea/modules.xml +8 -0
- package/.idea/purple-a11y.iml +9 -0
- package/.idea/vcs.xml +6 -0
- package/.prettierrc.json +12 -0
- package/.vscode/extensions.json +5 -0
- package/.vscode/settings.json +10 -0
- package/CODE_OF_CONDUCT.md +128 -0
- package/DETAILS.md +163 -0
- package/Dockerfile +60 -0
- package/INSTALLATION.md +146 -0
- package/INTEGRATION.md +785 -0
- package/LICENSE +22 -0
- package/README.md +587 -0
- package/SECURITY.md +5 -0
- package/__mocks__/mock-report.html +1431 -0
- package/__mocks__/mockFunctions.ts +32 -0
- package/__mocks__/mockIssues.ts +64 -0
- package/__mocks__/mock_all_issues/000000001.json +64 -0
- package/__mocks__/mock_all_issues/000000002.json +53 -0
- package/__mocks__/mock_all_issues/fake-file.txt +0 -0
- package/__tests__/logs.test.ts +25 -0
- package/__tests__/mergeAxeResults.test.ts +278 -0
- package/__tests__/utils.test.ts +118 -0
- package/a11y-scan-results.zip +0 -0
- package/eslint.config.js +53 -0
- package/exclusions.txt +2 -0
- package/gitlab-pipeline-template.yml +54 -0
- package/jest.config.js +1 -0
- package/package.json +96 -0
- package/scripts/copyFiles.js +44 -0
- package/scripts/install_oobee_dependencies.cmd +13 -0
- package/scripts/install_oobee_dependencies.command +101 -0
- package/scripts/install_oobee_dependencies.ps1 +110 -0
- package/scripts/oobee_shell.cmd +13 -0
- package/scripts/oobee_shell.command +11 -0
- package/scripts/oobee_shell.sh +55 -0
- package/scripts/oobee_shell_ps.ps1 +54 -0
- package/src/cli.ts +401 -0
- package/src/combine.ts +240 -0
- package/src/constants/__tests__/common.test.ts +44 -0
- package/src/constants/cliFunctions.ts +305 -0
- package/src/constants/common.ts +1840 -0
- package/src/constants/constants.ts +443 -0
- package/src/constants/errorMeta.json +319 -0
- package/src/constants/itemTypeDescription.ts +11 -0
- package/src/constants/oobeeAi.ts +141 -0
- package/src/constants/questions.ts +181 -0
- package/src/constants/sampleData.ts +187 -0
- package/src/crawlers/__tests__/commonCrawlerFunc.test.ts +51 -0
- package/src/crawlers/commonCrawlerFunc.ts +656 -0
- package/src/crawlers/crawlDomain.ts +877 -0
- package/src/crawlers/crawlIntelligentSitemap.ts +156 -0
- package/src/crawlers/crawlLocalFile.ts +193 -0
- package/src/crawlers/crawlSitemap.ts +356 -0
- package/src/crawlers/custom/extractAndGradeText.ts +57 -0
- package/src/crawlers/custom/flagUnlabelledClickableElements.ts +964 -0
- package/src/crawlers/custom/utils.ts +486 -0
- package/src/crawlers/customAxeFunctions.ts +82 -0
- package/src/crawlers/pdfScanFunc.ts +468 -0
- package/src/crawlers/runCustom.ts +117 -0
- package/src/index.ts +173 -0
- package/src/logs.ts +66 -0
- package/src/mergeAxeResults.ts +964 -0
- package/src/npmIndex.ts +284 -0
- package/src/screenshotFunc/htmlScreenshotFunc.ts +411 -0
- package/src/screenshotFunc/pdfScreenshotFunc.ts +762 -0
- package/src/static/ejs/partials/components/categorySelector.ejs +4 -0
- package/src/static/ejs/partials/components/categorySelectorDropdown.ejs +57 -0
- package/src/static/ejs/partials/components/pagesScannedModal.ejs +70 -0
- package/src/static/ejs/partials/components/reportSearch.ejs +47 -0
- package/src/static/ejs/partials/components/ruleOffcanvas.ejs +105 -0
- package/src/static/ejs/partials/components/scanAbout.ejs +263 -0
- package/src/static/ejs/partials/components/screenshotLightbox.ejs +13 -0
- package/src/static/ejs/partials/components/summaryScanAbout.ejs +141 -0
- package/src/static/ejs/partials/components/summaryScanResults.ejs +16 -0
- package/src/static/ejs/partials/components/summaryTable.ejs +20 -0
- package/src/static/ejs/partials/components/summaryWcagCompliance.ejs +94 -0
- package/src/static/ejs/partials/components/topFive.ejs +6 -0
- package/src/static/ejs/partials/components/wcagCompliance.ejs +70 -0
- package/src/static/ejs/partials/footer.ejs +21 -0
- package/src/static/ejs/partials/header.ejs +230 -0
- package/src/static/ejs/partials/main.ejs +40 -0
- package/src/static/ejs/partials/scripts/bootstrap.ejs +8 -0
- package/src/static/ejs/partials/scripts/categorySelectorDropdownScript.ejs +190 -0
- package/src/static/ejs/partials/scripts/categorySummary.ejs +141 -0
- package/src/static/ejs/partials/scripts/highlightjs.ejs +335 -0
- package/src/static/ejs/partials/scripts/popper.ejs +7 -0
- package/src/static/ejs/partials/scripts/reportSearch.ejs +248 -0
- package/src/static/ejs/partials/scripts/ruleOffcanvas.ejs +801 -0
- package/src/static/ejs/partials/scripts/screenshotLightbox.ejs +71 -0
- package/src/static/ejs/partials/scripts/summaryScanResults.ejs +14 -0
- package/src/static/ejs/partials/scripts/summaryTable.ejs +78 -0
- package/src/static/ejs/partials/scripts/utils.ejs +441 -0
- package/src/static/ejs/partials/styles/bootstrap.ejs +12375 -0
- package/src/static/ejs/partials/styles/highlightjs.ejs +54 -0
- package/src/static/ejs/partials/styles/styles.ejs +1843 -0
- package/src/static/ejs/partials/styles/summaryBootstrap.ejs +12458 -0
- package/src/static/ejs/partials/summaryHeader.ejs +70 -0
- package/src/static/ejs/partials/summaryMain.ejs +75 -0
- package/src/static/ejs/report.ejs +420 -0
- package/src/static/ejs/summary.ejs +47 -0
- package/src/static/mustache/.prettierrc +4 -0
- package/src/static/mustache/Attention Deficit.mustache +11 -0
- package/src/static/mustache/Blind.mustache +11 -0
- package/src/static/mustache/Cognitive.mustache +7 -0
- package/src/static/mustache/Colorblindness.mustache +20 -0
- package/src/static/mustache/Deaf.mustache +12 -0
- package/src/static/mustache/Deafblind.mustache +7 -0
- package/src/static/mustache/Dyslexia.mustache +14 -0
- package/src/static/mustache/Low Vision.mustache +7 -0
- package/src/static/mustache/Mobility.mustache +15 -0
- package/src/static/mustache/Sighted Keyboard Users.mustache +42 -0
- package/src/static/mustache/report.mustache +1709 -0
- package/src/types/print-message.d.ts +28 -0
- package/src/types/types.ts +46 -0
- package/src/types/xpath-to-css.d.ts +3 -0
- package/src/utils.ts +332 -0
- package/tsconfig.json +15 -0
package/src/npmIndex.ts
ADDED
@@ -0,0 +1,284 @@
|
|
1
|
+
import fs from 'fs';
|
2
|
+
import path from 'path';
|
3
|
+
import printMessage from 'print-message';
|
4
|
+
import axe from 'axe-core';
|
5
|
+
import { fileURLToPath } from 'url';
|
6
|
+
import constants, { BrowserTypes } from './constants/constants.js';
|
7
|
+
import {
|
8
|
+
deleteClonedProfiles,
|
9
|
+
getBrowserToRun,
|
10
|
+
getPlaywrightLaunchOptions,
|
11
|
+
submitForm,
|
12
|
+
urlWithoutAuth,
|
13
|
+
} from './constants/common.js';
|
14
|
+
import { createCrawleeSubFolders, filterAxeResults } from './crawlers/commonCrawlerFunc.js';
|
15
|
+
import { createAndUpdateResultsFolders, createDetailsAndLogs } from './utils.js';
|
16
|
+
import generateArtifacts from './mergeAxeResults.js';
|
17
|
+
import { takeScreenshotForHTMLElements } from './screenshotFunc/htmlScreenshotFunc.js';
|
18
|
+
import { silentLogger } from './logs.js';
|
19
|
+
import { alertMessageOptions } from './constants/cliFunctions.js';
|
20
|
+
|
21
|
+
const filename = fileURLToPath(import.meta.url);
|
22
|
+
const dirname = path.dirname(filename);
|
23
|
+
|
24
|
+
export const init = async (
|
25
|
+
entryUrl,
|
26
|
+
testLabel,
|
27
|
+
name = 'Your Name',
|
28
|
+
email = 'email@domain.com',
|
29
|
+
includeScreenshots = false,
|
30
|
+
viewportSettings = { width: 1000, height: 660 }, // cypress' default viewport settings
|
31
|
+
thresholds = { mustFix: undefined, goodToFix: undefined },
|
32
|
+
scanAboutMetadata = undefined,
|
33
|
+
zip = undefined,
|
34
|
+
) => {
|
35
|
+
console.log('Starting Oobee');
|
36
|
+
|
37
|
+
const [date, time] = new Date().toLocaleString('sv').replaceAll(/-|:/g, '').split(' ');
|
38
|
+
const domain = new URL(entryUrl).hostname;
|
39
|
+
const sanitisedLabel = testLabel ? `_${testLabel.replaceAll(' ', '_')}` : '';
|
40
|
+
const randomToken = `${date}_${time}${sanitisedLabel}_${domain}`;
|
41
|
+
|
42
|
+
// max numbers of mustFix/goodToFix occurrences before test returns a fail
|
43
|
+
const { mustFix: mustFixThreshold, goodToFix: goodToFixThreshold } = thresholds;
|
44
|
+
|
45
|
+
process.env.CRAWLEE_STORAGE_DIR = randomToken;
|
46
|
+
|
47
|
+
const scanDetails = {
|
48
|
+
startTime: new Date(),
|
49
|
+
endTime: new Date(),
|
50
|
+
crawlType: 'Custom',
|
51
|
+
requestUrl: entryUrl,
|
52
|
+
urlsCrawled: { ...constants.urlsCrawledObj },
|
53
|
+
};
|
54
|
+
|
55
|
+
const urlsCrawled = { ...constants.urlsCrawledObj };
|
56
|
+
|
57
|
+
const { dataset } = await createCrawleeSubFolders(randomToken);
|
58
|
+
|
59
|
+
let mustFixIssues = 0;
|
60
|
+
let goodToFixIssues = 0;
|
61
|
+
|
62
|
+
let isInstanceTerminated = false;
|
63
|
+
|
64
|
+
const throwErrorIfTerminated = () => {
|
65
|
+
if (isInstanceTerminated) {
|
66
|
+
throw new Error('This instance of Oobee was terminated. Please start a new instance.');
|
67
|
+
}
|
68
|
+
};
|
69
|
+
|
70
|
+
const getScripts = () => {
|
71
|
+
throwErrorIfTerminated();
|
72
|
+
const axeScript = fs.readFileSync(
|
73
|
+
path.join(dirname, '../node_modules/axe-core/axe.min.js'),
|
74
|
+
'utf-8',
|
75
|
+
);
|
76
|
+
async function runA11yScan(elementsToScan = []) {
|
77
|
+
axe.configure({
|
78
|
+
branding: {
|
79
|
+
application: 'oobee',
|
80
|
+
},
|
81
|
+
// Add custom img alt text check
|
82
|
+
checks: [
|
83
|
+
{
|
84
|
+
id: 'oobee-confusing-alt-text',
|
85
|
+
evaluate(node: HTMLElement) {
|
86
|
+
const altText = node.getAttribute('alt');
|
87
|
+
const confusingTexts = ['img', 'image', 'picture', 'photo', 'graphic'];
|
88
|
+
|
89
|
+
if (altText) {
|
90
|
+
const trimmedAltText = altText.trim().toLowerCase();
|
91
|
+
// Check if the alt text exactly matches one of the confusingTexts
|
92
|
+
if (confusingTexts.some(text => text === trimmedAltText)) {
|
93
|
+
return false; // Fail the check if the alt text is confusing or not useful
|
94
|
+
}
|
95
|
+
}
|
96
|
+
|
97
|
+
return true; // Pass the check if the alt text seems appropriate
|
98
|
+
},
|
99
|
+
metadata: {
|
100
|
+
impact: 'serious', // Set the severity to serious
|
101
|
+
messages: {
|
102
|
+
pass: 'The image alt text is probably useful',
|
103
|
+
fail: "The image alt text set as 'img', 'image', 'picture', 'photo', or 'graphic' is confusing or not useful",
|
104
|
+
},
|
105
|
+
},
|
106
|
+
},
|
107
|
+
],
|
108
|
+
rules: [
|
109
|
+
{ id: 'target-size', enabled: true },
|
110
|
+
{
|
111
|
+
id: 'oobee-confusing-alt-text',
|
112
|
+
selector: 'img[alt]',
|
113
|
+
enabled: true,
|
114
|
+
any: ['oobee-confusing-alt-text'],
|
115
|
+
all: [],
|
116
|
+
none: [],
|
117
|
+
tags: ['wcag2a', 'wcag111'],
|
118
|
+
metadata: {
|
119
|
+
description: 'Ensures image alt text is clear and useful',
|
120
|
+
help: 'Image alt text must not be vague or unhelpful',
|
121
|
+
helpUrl: 'https://www.deque.com/blog/great-alt-text-introduction/',
|
122
|
+
},
|
123
|
+
},
|
124
|
+
],
|
125
|
+
});
|
126
|
+
const axeScanResults = await axe.run(elementsToScan, {
|
127
|
+
resultTypes: ['violations', 'passes', 'incomplete'],
|
128
|
+
});
|
129
|
+
return {
|
130
|
+
pageUrl: window.location.href,
|
131
|
+
pageTitle: document.title,
|
132
|
+
axeScanResults,
|
133
|
+
};
|
134
|
+
}
|
135
|
+
return `${axeScript} ${runA11yScan.toString()}`;
|
136
|
+
};
|
137
|
+
|
138
|
+
const pushScanResults = async (res, metadata, elementsToClick) => {
|
139
|
+
throwErrorIfTerminated();
|
140
|
+
if (includeScreenshots) {
|
141
|
+
// use chrome by default
|
142
|
+
const { browserToRun, clonedBrowserDataDir } = getBrowserToRun(BrowserTypes.CHROME);
|
143
|
+
const browserContext = await constants.launcher.launchPersistentContext(
|
144
|
+
clonedBrowserDataDir,
|
145
|
+
{ viewport: scanAboutMetadata.viewport, ...getPlaywrightLaunchOptions(browserToRun) },
|
146
|
+
);
|
147
|
+
const page = await browserContext.newPage();
|
148
|
+
await page.goto(res.pageUrl);
|
149
|
+
await page.waitForLoadState('networkidle');
|
150
|
+
|
151
|
+
// click on elements to reveal hidden elements so screenshots can be taken
|
152
|
+
elementsToClick?.forEach(async elem => {
|
153
|
+
try {
|
154
|
+
await page.locator(elem).click();
|
155
|
+
} catch (e) {
|
156
|
+
silentLogger.info(e);
|
157
|
+
}
|
158
|
+
});
|
159
|
+
|
160
|
+
res.axeScanResults.violations = await takeScreenshotForHTMLElements(
|
161
|
+
res.axeScanResults.violations,
|
162
|
+
page,
|
163
|
+
randomToken,
|
164
|
+
3000,
|
165
|
+
);
|
166
|
+
res.axeScanResults.incomplete = await takeScreenshotForHTMLElements(
|
167
|
+
res.axeScanResults.incomplete,
|
168
|
+
page,
|
169
|
+
randomToken,
|
170
|
+
3000,
|
171
|
+
);
|
172
|
+
|
173
|
+
await browserContext.close();
|
174
|
+
deleteClonedProfiles(browserToRun);
|
175
|
+
}
|
176
|
+
const pageIndex = urlsCrawled.scanned.length + 1;
|
177
|
+
const filteredResults = filterAxeResults(res.axeScanResults, res.pageTitle, {
|
178
|
+
pageIndex,
|
179
|
+
metadata,
|
180
|
+
});
|
181
|
+
urlsCrawled.scanned.push({
|
182
|
+
url: urlWithoutAuth(res.pageUrl).toString(),
|
183
|
+
actualUrl: 'tbd',
|
184
|
+
pageTitle: `${pageIndex}: ${res.pageTitle}`,
|
185
|
+
});
|
186
|
+
|
187
|
+
mustFixIssues += filteredResults.mustFix ? filteredResults.mustFix.totalItems : 0;
|
188
|
+
goodToFixIssues += filteredResults.goodToFix ? filteredResults.goodToFix.totalItems : 0;
|
189
|
+
await dataset.pushData(filteredResults);
|
190
|
+
|
191
|
+
// return counts for users to perform custom assertions if needed
|
192
|
+
return {
|
193
|
+
mustFix: filteredResults.mustFix ? filteredResults.mustFix.totalItems : 0,
|
194
|
+
goodToFix: filteredResults.goodToFix ? filteredResults.goodToFix.totalItems : 0,
|
195
|
+
};
|
196
|
+
};
|
197
|
+
|
198
|
+
const terminate = async () => {
|
199
|
+
throwErrorIfTerminated();
|
200
|
+
console.log('Stopping Oobee');
|
201
|
+
isInstanceTerminated = true;
|
202
|
+
scanDetails.endTime = new Date();
|
203
|
+
scanDetails.urlsCrawled = urlsCrawled;
|
204
|
+
|
205
|
+
if (urlsCrawled.scanned.length === 0) {
|
206
|
+
printMessage([`No pages were scanned.`], alertMessageOptions);
|
207
|
+
} else {
|
208
|
+
await createDetailsAndLogs(randomToken);
|
209
|
+
await createAndUpdateResultsFolders(randomToken);
|
210
|
+
const pagesNotScanned = [
|
211
|
+
...scanDetails.urlsCrawled.error,
|
212
|
+
...scanDetails.urlsCrawled.invalid,
|
213
|
+
];
|
214
|
+
const updatedScanAboutMetadata = {
|
215
|
+
viewport: `${viewportSettings.width} x ${viewportSettings.height}`,
|
216
|
+
...scanAboutMetadata,
|
217
|
+
};
|
218
|
+
const basicFormHTMLSnippet = await generateArtifacts(
|
219
|
+
randomToken,
|
220
|
+
scanDetails.requestUrl,
|
221
|
+
scanDetails.crawlType,
|
222
|
+
updatedScanAboutMetadata.viewport,
|
223
|
+
scanDetails.urlsCrawled.scanned,
|
224
|
+
pagesNotScanned,
|
225
|
+
testLabel,
|
226
|
+
updatedScanAboutMetadata,
|
227
|
+
scanDetails,
|
228
|
+
zip,
|
229
|
+
);
|
230
|
+
|
231
|
+
await submitForm(
|
232
|
+
BrowserTypes.CHROMIUM, // browserToRun
|
233
|
+
'', // userDataDirectory
|
234
|
+
scanDetails.requestUrl, // scannedUrl
|
235
|
+
null, // entryUrl
|
236
|
+
scanDetails.crawlType, // scanType
|
237
|
+
email, // email
|
238
|
+
name, // name
|
239
|
+
JSON.stringify(basicFormHTMLSnippet), // scanResultsKson
|
240
|
+
urlsCrawled.scanned.length, // numberOfPagesScanned
|
241
|
+
0,
|
242
|
+
0,
|
243
|
+
'{}',
|
244
|
+
);
|
245
|
+
}
|
246
|
+
|
247
|
+
return randomToken;
|
248
|
+
};
|
249
|
+
|
250
|
+
const testThresholds = () => {
|
251
|
+
// check against thresholds to fail tests
|
252
|
+
let isThresholdExceeded = false;
|
253
|
+
let thresholdFailMessage = 'Exceeded thresholds:\n';
|
254
|
+
if (mustFixThreshold !== undefined && mustFixIssues > mustFixThreshold) {
|
255
|
+
isThresholdExceeded = true;
|
256
|
+
thresholdFailMessage += `mustFix occurrences found: ${mustFixIssues} > ${mustFixThreshold}\n`;
|
257
|
+
}
|
258
|
+
|
259
|
+
if (goodToFixThreshold !== undefined && goodToFixIssues > goodToFixThreshold) {
|
260
|
+
isThresholdExceeded = true;
|
261
|
+
thresholdFailMessage += `goodToFix occurrences found: ${goodToFixIssues} > ${goodToFixThreshold}\n`;
|
262
|
+
}
|
263
|
+
|
264
|
+
// uncomment to reset counts if you do not want violations count to be cumulative across other pages
|
265
|
+
// mustFixIssues = 0;
|
266
|
+
// goodToFixIssues = 0;
|
267
|
+
|
268
|
+
if (isThresholdExceeded) {
|
269
|
+
terminate(); // terminate if threshold exceeded
|
270
|
+
throw new Error(thresholdFailMessage);
|
271
|
+
}
|
272
|
+
};
|
273
|
+
|
274
|
+
return {
|
275
|
+
getScripts,
|
276
|
+
pushScanResults,
|
277
|
+
terminate,
|
278
|
+
scanDetails,
|
279
|
+
randomToken,
|
280
|
+
testThresholds,
|
281
|
+
};
|
282
|
+
};
|
283
|
+
|
284
|
+
export default init;
|
@@ -0,0 +1,411 @@
|
|
1
|
+
// import { JSDOM } from "jsdom";
|
2
|
+
import { createHash } from 'crypto';
|
3
|
+
import fs from 'fs';
|
4
|
+
import path from 'path';
|
5
|
+
import { consoleLogger } from '../logs.js';
|
6
|
+
import { Result } from 'axe-core';
|
7
|
+
import { Page } from 'playwright';
|
8
|
+
import { NodeResultWithScreenshot, ResultWithScreenshot } from '../crawlers/commonCrawlerFunc.js';
|
9
|
+
|
10
|
+
const screenshotMap = {}; // Map of screenshot hashkey to its buffer value and screenshot path
|
11
|
+
|
12
|
+
export const takeScreenshotForHTMLElements = async (
|
13
|
+
violations: Result[],
|
14
|
+
page: Page,
|
15
|
+
randomToken: string,
|
16
|
+
locatorTimeout = 2000,
|
17
|
+
maxScreenshots = 50,
|
18
|
+
): Promise<ResultWithScreenshot[]> => {
|
19
|
+
const newViolations: ResultWithScreenshot[] = [];
|
20
|
+
let screenshotCount = 0;
|
21
|
+
|
22
|
+
for (const violation of violations) {
|
23
|
+
if (screenshotCount >= maxScreenshots) {
|
24
|
+
consoleLogger.warn(
|
25
|
+
`Skipping screenshots for ${violation.id} as maxScreenshots (${maxScreenshots}) exceeded. You can increase it by specifying a higher value when calling takeScreenshotForHTMLElements.`,
|
26
|
+
);
|
27
|
+
newViolations.push(violation);
|
28
|
+
continue;
|
29
|
+
}
|
30
|
+
|
31
|
+
const { id: rule } = violation;
|
32
|
+
|
33
|
+
// Check if rule ID is 'oobee-grading-text-contents' and skip screenshot logic
|
34
|
+
if (rule === 'oobee-grading-text-contents') {
|
35
|
+
consoleLogger.info('Skipping screenshot for rule oobee-grading-text-contents');
|
36
|
+
newViolations.push(violation); // Make sure it gets added
|
37
|
+
continue;
|
38
|
+
}
|
39
|
+
|
40
|
+
const newViolationNodes: NodeResultWithScreenshot[] = [];
|
41
|
+
for (const node of violation.nodes) {
|
42
|
+
const nodeWithScreenshotPath: NodeResultWithScreenshot = node;
|
43
|
+
const { target } = node;
|
44
|
+
const hasValidSelector = target.length === 1 && typeof target[0] === 'string';
|
45
|
+
const selector = hasValidSelector ? (target[0] as string) : null;
|
46
|
+
if (selector) {
|
47
|
+
try {
|
48
|
+
const locator = page.locator(selector);
|
49
|
+
const locators = await locator.all();
|
50
|
+
for (const currLocator of locators) {
|
51
|
+
await currLocator.scrollIntoViewIfNeeded({ timeout: locatorTimeout });
|
52
|
+
const isVisible = await currLocator.isVisible();
|
53
|
+
|
54
|
+
if (isVisible) {
|
55
|
+
const buffer = await currLocator.screenshot({ timeout: locatorTimeout });
|
56
|
+
const screenshotPath = getScreenshotPath(buffer, randomToken);
|
57
|
+
nodeWithScreenshotPath.screenshotPath = screenshotPath;
|
58
|
+
screenshotCount++;
|
59
|
+
} else {
|
60
|
+
consoleLogger.info(`Element at ${currLocator} is not visible`);
|
61
|
+
}
|
62
|
+
|
63
|
+
break; // Stop looping after finding the first visible locator
|
64
|
+
}
|
65
|
+
} catch (e) {
|
66
|
+
consoleLogger.info(`Unable to take element screenshot at ${selector}`);
|
67
|
+
}
|
68
|
+
}
|
69
|
+
newViolationNodes.push(nodeWithScreenshotPath);
|
70
|
+
}
|
71
|
+
violation.nodes = newViolationNodes;
|
72
|
+
newViolations.push(violation);
|
73
|
+
}
|
74
|
+
// console.log('Processed Violations (after screenshots):', JSON.stringify(newViolations, null, 2));
|
75
|
+
return newViolations;
|
76
|
+
};
|
77
|
+
|
78
|
+
const generateBufferHash = (buffer: Buffer) => {
|
79
|
+
const hash = createHash('sha256');
|
80
|
+
hash.update(buffer);
|
81
|
+
return hash.digest('hex');
|
82
|
+
};
|
83
|
+
|
84
|
+
const isSameBufferHash = (buffer: Buffer, hash: string) => {
|
85
|
+
const bufferHash = generateBufferHash(buffer);
|
86
|
+
return hash === bufferHash;
|
87
|
+
};
|
88
|
+
|
89
|
+
const getIdenticalScreenshotKey = (buffer: Buffer) => {
|
90
|
+
for (const hashKey in screenshotMap) {
|
91
|
+
const isIdentical = isSameBufferHash(buffer, hashKey);
|
92
|
+
if (isIdentical) return hashKey;
|
93
|
+
}
|
94
|
+
return undefined;
|
95
|
+
};
|
96
|
+
|
97
|
+
const getScreenshotPath = (buffer: Buffer, randomToken: string) => {
|
98
|
+
let hashKey = getIdenticalScreenshotKey(buffer);
|
99
|
+
// If exists identical entry in screenshot map, get its filepath
|
100
|
+
if (hashKey) {
|
101
|
+
return screenshotMap[hashKey];
|
102
|
+
}
|
103
|
+
// Create new entry in screenshot map
|
104
|
+
hashKey = generateBufferHash(buffer);
|
105
|
+
const path = generateScreenshotPath(hashKey);
|
106
|
+
screenshotMap[hashKey] = path;
|
107
|
+
|
108
|
+
// Save image file to local storage
|
109
|
+
saveImageBufferToFile(buffer, `${randomToken}/${path}`);
|
110
|
+
|
111
|
+
return path;
|
112
|
+
};
|
113
|
+
|
114
|
+
const generateScreenshotPath = (hashKey: string) => {
|
115
|
+
return `elemScreenshots/html/${hashKey}.jpeg`;
|
116
|
+
};
|
117
|
+
|
118
|
+
const saveImageBufferToFile = (buffer: Buffer, fileName: string) => {
|
119
|
+
if (!fileName) return;
|
120
|
+
// Find and create parent directories recursively if not exist
|
121
|
+
const absPath = path.resolve(fileName);
|
122
|
+
const dir = path.dirname(absPath);
|
123
|
+
fs.mkdir(dir, { recursive: true }, err => {
|
124
|
+
if (err) console.log('Error trying to create parent directory(s):', err);
|
125
|
+
// Write the image buffer to file
|
126
|
+
fs.writeFile(absPath, buffer, err => {
|
127
|
+
if (err) console.log('Error trying to write file:', err);
|
128
|
+
});
|
129
|
+
});
|
130
|
+
};
|
131
|
+
|
132
|
+
// const hasMultipleLocators = async (locator) => await locator.count() > 1;
|
133
|
+
|
134
|
+
// const resolveMultipleLocators = async (page, locator, html) => {
|
135
|
+
// const { tag, classAttrib, hrefAttrib, textContent } = generateAttribs(html);
|
136
|
+
|
137
|
+
// const allLocators = await locator.all();
|
138
|
+
// // console.log('locator before: ', locator);
|
139
|
+
// allLocators.forEach(async currLocator => {
|
140
|
+
// console.log('curr locator: ', currLocator);
|
141
|
+
// let hrefIsExactMatch, containsTextContent;
|
142
|
+
// // if (hrefAttrib) hrefIsExactMatch = (await currLocator.getAttribute('href')) === hrefAttrib;
|
143
|
+
// if (textContent) containsTextContent = (await currLocator.innerText()).includes(textContent);
|
144
|
+
|
145
|
+
// // if (hrefAttrib && textContent) {
|
146
|
+
// // if (hrefIsExactMatch && containsTextContent) {
|
147
|
+
// // locator = currLocator;
|
148
|
+
// // console.log('1: ', locator);
|
149
|
+
// // }
|
150
|
+
// // } else if (hrefAttrib) {
|
151
|
+
// // if (hrefIsExactMatch) {
|
152
|
+
// // locator = currLocator;
|
153
|
+
// // console.log('2: ', locator);
|
154
|
+
// // }
|
155
|
+
// // } else
|
156
|
+
|
157
|
+
// if (textContent) {
|
158
|
+
// if (containsTextContent) {
|
159
|
+
// locator = currLocator;
|
160
|
+
// console.log('3: ', locator);
|
161
|
+
// }
|
162
|
+
// } else {
|
163
|
+
// locator = null;
|
164
|
+
// }
|
165
|
+
// })
|
166
|
+
// console.log('final locator: ', locator);
|
167
|
+
// return locator;
|
168
|
+
// }
|
169
|
+
|
170
|
+
// const generateAttribs = (html) => {
|
171
|
+
// const processedHTMLString = html.replaceAll('\n', '');
|
172
|
+
// const tagNamesRegex = /(?<=[<])\s*([a-zA-Z][^\s>/]*)\b/g;
|
173
|
+
// const tag = processedHTMLString.match(tagNamesRegex)[0];
|
174
|
+
|
175
|
+
// const dom = new JSDOM(processedHTMLString);
|
176
|
+
// const elem = dom.window.document.querySelector(tag);
|
177
|
+
|
178
|
+
// const textContent = elem.textContent.trim();
|
179
|
+
// const classAttrib = elem.getAttribute('class')?.trim();
|
180
|
+
// const hrefAttrib = (tag === 'a') ? elem.getAttribute('href') : null;
|
181
|
+
// console.log('text content: ', textContent);
|
182
|
+
|
183
|
+
// return {
|
184
|
+
// tag,
|
185
|
+
// ...(classAttrib && {classAttrib}),
|
186
|
+
// ...(hrefAttrib && {hrefAttrib}),
|
187
|
+
// ...(textContent && {textContent})
|
188
|
+
// }
|
189
|
+
// }
|
190
|
+
|
191
|
+
// export const takeScreenshotForHTMLElements = async (screenshotData, storagePath, browserToRun) => {
|
192
|
+
// const screenshotDir = `${storagePath}/screenshots`;
|
193
|
+
// let screenshotItems = [];
|
194
|
+
// let randomToken = `cloned-${Date.now()}`;
|
195
|
+
// const clonedDir = getClonedProfilesWithRandomToken(browserToRun, randomToken);
|
196
|
+
// const browser = await constants.launcher.launchPersistentContext(
|
197
|
+
// clonedDir,
|
198
|
+
// {
|
199
|
+
// headless: false,
|
200
|
+
// ...getPlaywrightLaunchOptions(browserToRun)
|
201
|
+
// }
|
202
|
+
// );
|
203
|
+
|
204
|
+
// for (const item of screenshotData) {
|
205
|
+
// const domain = item.url.replaceAll("https://", '').replaceAll('/', '_');
|
206
|
+
// item.htmlItems = generateSelectors(item.htmlItems);
|
207
|
+
// const page = await browser.newPage();
|
208
|
+
// await page.goto(item.url);
|
209
|
+
// let htmlItemsWithScreenshotPath = [];
|
210
|
+
// for (const htmlItem of item.htmlItems) {
|
211
|
+
// const { rule, category, selector } = htmlItem;
|
212
|
+
// const locator = await getLocators(page, selector);
|
213
|
+
// const screenshotFilePath = `${domain}/${category}/${rule}/${selector.tag}-${htmlItemsWithScreenshotPath.length}.png`;
|
214
|
+
// if (locator) {
|
215
|
+
// await locator.screenshot({ path: `${screenshotDir}/${screenshotFilePath}` });
|
216
|
+
// htmlItem.screenshotPath = `screenshots/${screenshotFilePath}`;
|
217
|
+
// }
|
218
|
+
// delete htmlItem.selector;
|
219
|
+
// htmlItemsWithScreenshotPath.push(htmlItem);
|
220
|
+
// }
|
221
|
+
// screenshotItems.push({url: item.url, htmlItems: htmlItemsWithScreenshotPath});
|
222
|
+
// await page.close();
|
223
|
+
// }
|
224
|
+
// await browser.close();
|
225
|
+
// deleteClonedProfiles(browserToRun)
|
226
|
+
// return screenshotItems;
|
227
|
+
// }
|
228
|
+
|
229
|
+
// export const processScreenshotData = (allIssues) => {
|
230
|
+
// const scannedUrls = allIssues.pagesScanned.map(page => page.url);
|
231
|
+
// const screenshotData = scannedUrls.map(scannedUrl => {
|
232
|
+
// let htmlItems = [];
|
233
|
+
// ['mustFix', 'goodToFix'].map((category) => {
|
234
|
+
// const ruleItems = allIssues.items[category].rules;
|
235
|
+
// ruleItems.map(ruleItem => {
|
236
|
+
// const { rule, pagesAffected } = ruleItem;
|
237
|
+
// pagesAffected.map(affectedPage => {
|
238
|
+
// const { url, items } = affectedPage;
|
239
|
+
// if (scannedUrl === url) {
|
240
|
+
// items.forEach(item => {if (item.html) htmlItems.push({html: item.html, rule, category})});
|
241
|
+
// }
|
242
|
+
// })
|
243
|
+
// })
|
244
|
+
// })
|
245
|
+
// return {url: scannedUrl, htmlItems};
|
246
|
+
// })
|
247
|
+
// return screenshotData;
|
248
|
+
// }
|
249
|
+
|
250
|
+
// export const getScreenshotPaths = (screenshotItems, allIssues) => {
|
251
|
+
// screenshotItems.forEach(screenshotItem => {
|
252
|
+
// const { url: ssUrl, htmlItems: ssHtmlItems } = screenshotItem;
|
253
|
+
// ssHtmlItems.map(ssHtmlItem => {
|
254
|
+
// const {
|
255
|
+
// category: ssCategory,
|
256
|
+
// rule: ssRule,
|
257
|
+
// html: ssHtml,
|
258
|
+
// screenshotPath: ssPath
|
259
|
+
// } = ssHtmlItem;
|
260
|
+
// allIssues.items[ssCategory].rules = allIssues.items[ssCategory].rules
|
261
|
+
// .map(ruleItem => {
|
262
|
+
// const { rule, pagesAffected } = ruleItem;
|
263
|
+
// if (rule === ssRule) {
|
264
|
+
// ruleItem.pagesAffected = pagesAffected.map(affectedPage => {
|
265
|
+
// const { url, items } = affectedPage;
|
266
|
+
// if (ssUrl === url) {
|
267
|
+
// affectedPage.items = items.map(htmlItem => {
|
268
|
+
// const { html } = htmlItem;
|
269
|
+
// if (ssHtml === html) htmlItem.screenshotPath = ssPath;
|
270
|
+
// return htmlItem;
|
271
|
+
// })
|
272
|
+
// }
|
273
|
+
// return affectedPage;
|
274
|
+
// })
|
275
|
+
// }
|
276
|
+
// return ruleItem;
|
277
|
+
// })
|
278
|
+
// })
|
279
|
+
// });
|
280
|
+
// }
|
281
|
+
|
282
|
+
// const generateSelectors = (htmlItems) => {
|
283
|
+
// const htmlItemsWithSelectors = htmlItems.map((htmlItem) => {
|
284
|
+
// const { html } = htmlItem;
|
285
|
+
// const processedHTMLString = html.replaceAll('\n', '');
|
286
|
+
// const tagnameRegex = /(?<=[<])\s*([a-zA-Z][^\s>/]*)\b/g;
|
287
|
+
// const tagNames = processedHTMLString.match(tagnameRegex);
|
288
|
+
|
289
|
+
// const dom = new JSDOM(processedHTMLString);
|
290
|
+
// const tag = tagNames[0]
|
291
|
+
// const elem = dom.window.document.querySelector(tag);
|
292
|
+
|
293
|
+
// const classAttrib = elem.getAttribute('class')?.trim();
|
294
|
+
// const idAttrib = elem.getAttribute('id');
|
295
|
+
// const titleAttrib = elem.getAttribute('title');
|
296
|
+
// const placeholderAttrib = elem.getAttribute('placeholder');
|
297
|
+
// const altAttrib = (tag === 'img') ? elem.getAttribute('alt') : null;
|
298
|
+
// const hrefAttrib = (tag === 'a') ? elem.getAttribute('href') : null;
|
299
|
+
|
300
|
+
// let children;
|
301
|
+
// if (tagNames.length > 1) {
|
302
|
+
// const childrenHTMLItems = Array.from(elem.children).map(child => {
|
303
|
+
// return {
|
304
|
+
// html: child.outerHTML,
|
305
|
+
// rule: htmlItem.rule,
|
306
|
+
// category: htmlItem.category
|
307
|
+
// }
|
308
|
+
// });
|
309
|
+
// children = generateSelectors(childrenHTMLItems);
|
310
|
+
// }
|
311
|
+
|
312
|
+
// let textContent = elem.textContent.trim();
|
313
|
+
// let allTextContents = [];
|
314
|
+
// children?.map((child) => {
|
315
|
+
// if (child?.selector.allTextContents) allTextContents = [...allTextContents, ...child.selector.allTextContents]
|
316
|
+
// })
|
317
|
+
|
318
|
+
// if (allTextContents.includes(textContent)) {
|
319
|
+
// textContent = null;
|
320
|
+
// } else {
|
321
|
+
// if (textContent) allTextContents = [textContent, ...allTextContents];
|
322
|
+
// }
|
323
|
+
|
324
|
+
// const selector = {
|
325
|
+
// tag,
|
326
|
+
// processedHTMLString,
|
327
|
+
// ...(textContent && {textContent}),
|
328
|
+
// ...(allTextContents.length > 0 && {allTextContents}),
|
329
|
+
// ...(classAttrib && {classAttrib}),
|
330
|
+
// ...(idAttrib && {idAttrib}),
|
331
|
+
// ...(titleAttrib && {titleAttrib}),
|
332
|
+
// ...(placeholderAttrib && {placeholderAttrib}),
|
333
|
+
// ...(altAttrib && {altAttrib}),
|
334
|
+
// ...(hrefAttrib && {hrefAttrib}),
|
335
|
+
// ...(children && {children}),
|
336
|
+
// }
|
337
|
+
|
338
|
+
// htmlItem.selector = selector;
|
339
|
+
// return htmlItem;
|
340
|
+
// })
|
341
|
+
// return htmlItemsWithSelectors;
|
342
|
+
// }
|
343
|
+
|
344
|
+
// const generateInitialLocator = (page, selector) => {
|
345
|
+
// const {
|
346
|
+
// tag,
|
347
|
+
// textContent,
|
348
|
+
// classAttrib,
|
349
|
+
// idAttrib,
|
350
|
+
// titleAttrib,
|
351
|
+
// placeholderAttrib,
|
352
|
+
// altAttrib,
|
353
|
+
// children
|
354
|
+
// } = selector;
|
355
|
+
|
356
|
+
// let locator = page.locator(tag);
|
357
|
+
// if (classAttrib) {
|
358
|
+
// const classSelector = classAttrib.replaceAll(/\s+/g, '.').replace(/^/, '.').replaceAll(':', '\\:').replaceAll('(', '\\(').replaceAll(')', '\\)');
|
359
|
+
// locator = locator.and(page.locator(classSelector))
|
360
|
+
// }
|
361
|
+
// if (idAttrib) locator = locator.and(page.locator(`#${idAttrib}`));
|
362
|
+
// if (textContent) locator = locator.and(page.getByText(textContent));
|
363
|
+
// if (titleAttrib) locator = locator.and(page.getByTitle(titleAttrib));
|
364
|
+
// if (placeholderAttrib) locator = locator.and(page.getByPlaceHolder(placeholderAttrib));
|
365
|
+
// if (altAttrib) locator = locator.and(page.getByAltText(altAttrib));
|
366
|
+
|
367
|
+
// if (children) {
|
368
|
+
// let currLocator = locator;
|
369
|
+
// for (const child of children) {
|
370
|
+
// const childLocator = generateInitialLocator(page, child.selector);
|
371
|
+
// locator = locator.and(currLocator.filter({ has: childLocator })); // figure this out tmr!
|
372
|
+
// }
|
373
|
+
// }
|
374
|
+
// return locator;
|
375
|
+
// }
|
376
|
+
|
377
|
+
// const resolveLocators = async (locator, classAttrib, hrefAttrib) => {
|
378
|
+
// const locatorCount = await locator.count();
|
379
|
+
// if (locatorCount > 1) {
|
380
|
+
// let locators = [];
|
381
|
+
// const allLocators = await locator.all();
|
382
|
+
// for (let nth = 0; nth < locatorCount; nth++) {
|
383
|
+
// const currLocator = allLocators[nth];
|
384
|
+
// const isVisible = await currLocator.isVisible();
|
385
|
+
// if (isVisible) {
|
386
|
+
// let classIsExactMatch, hrefIsExactMatch;
|
387
|
+
// if (classAttrib) classIsExactMatch = (await currLocator.getAttribute('class')) === classAttrib;
|
388
|
+
// if (hrefAttrib) hrefIsExactMatch = (await currLocator.getAttribute('href')) === hrefAttrib;
|
389
|
+
|
390
|
+
// if (classAttrib && hrefAttrib) {
|
391
|
+
// if (classIsExactMatch && hrefIsExactMatch) locators.push(currLocator);
|
392
|
+
// } else if (classAttrib) {
|
393
|
+
// if (classIsExactMatch) locators.push(currLocator);
|
394
|
+
// } else if (hrefAttrib) {
|
395
|
+
// if (hrefIsExactMatch) locators.push(currLocator);
|
396
|
+
// } else {
|
397
|
+
// locators.push(currLocator);
|
398
|
+
// }
|
399
|
+
// }
|
400
|
+
// }
|
401
|
+
// return locators.length === 1 ? locators[0] : null;
|
402
|
+
// } else {
|
403
|
+
// return locator;
|
404
|
+
// }
|
405
|
+
// }
|
406
|
+
|
407
|
+
// const getLocators = async (page, selector) => {
|
408
|
+
// const locator = generateInitialLocator(page, selector);
|
409
|
+
// const locators = await resolveLocators(locator, selector.classAttrib, selector.hrefAttrib);
|
410
|
+
// return locators;
|
411
|
+
// }
|