@govtechsg/oobee 0.10.83 → 0.10.85
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/README.md +6 -1
- package/dist/cli.js +7 -6
- package/dist/constants/common.js +13 -1
- package/dist/crawlers/crawlDomain.js +220 -120
- package/dist/crawlers/crawlIntelligentSitemap.js +22 -7
- package/dist/crawlers/custom/utils.js +81 -40
- package/dist/crawlers/runCustom.js +13 -5
- package/dist/mergeAxeResults/itemReferences.js +55 -0
- package/dist/mergeAxeResults/jsonArtifacts.js +335 -0
- package/dist/mergeAxeResults/scanPages.js +159 -0
- package/dist/mergeAxeResults/sentryTelemetry.js +152 -0
- package/dist/mergeAxeResults/types.js +1 -0
- package/dist/mergeAxeResults/writeCsv.js +125 -0
- package/dist/mergeAxeResults/writeScanDetailsCsv.js +35 -0
- package/dist/mergeAxeResults/writeSitemap.js +10 -0
- package/dist/mergeAxeResults.js +64 -950
- package/dist/proxyService.js +90 -5
- package/dist/utils.js +20 -7
- package/package.json +6 -6
- package/src/cli.ts +20 -15
- package/src/constants/common.ts +13 -1
- package/src/crawlers/crawlDomain.ts +248 -137
- package/src/crawlers/crawlIntelligentSitemap.ts +22 -8
- package/src/crawlers/custom/utils.ts +103 -48
- package/src/crawlers/runCustom.ts +18 -5
- package/src/mergeAxeResults/itemReferences.ts +62 -0
- package/src/mergeAxeResults/jsonArtifacts.ts +451 -0
- package/src/mergeAxeResults/scanPages.ts +207 -0
- package/src/mergeAxeResults/sentryTelemetry.ts +183 -0
- package/src/mergeAxeResults/types.ts +99 -0
- package/src/mergeAxeResults/writeCsv.ts +145 -0
- package/src/mergeAxeResults/writeScanDetailsCsv.ts +51 -0
- package/src/mergeAxeResults/writeSitemap.ts +13 -0
- package/src/mergeAxeResults.ts +125 -1344
- package/src/proxyService.ts +96 -4
- package/src/utils.ts +19 -7
|
@@ -22,13 +22,25 @@ const crawlIntelligentSitemap = async (url, randomToken, host, viewportSettings,
|
|
|
22
22
|
async function findSitemap(link, userDataDirectory, extraHTTPHeaders) {
|
|
23
23
|
const homeUrl = getHomeUrl(link);
|
|
24
24
|
let sitemapLink = '';
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
25
|
+
const launchOptions = getPlaywrightLaunchOptions(browser);
|
|
26
|
+
let context;
|
|
27
|
+
let browserInstance;
|
|
28
|
+
if (process.env.CRAWLEE_HEADLESS === '1') {
|
|
29
|
+
const effectiveUserDataDirectory = userDataDirectory || '';
|
|
30
|
+
context = await constants.launcher.launchPersistentContext(effectiveUserDataDirectory, {
|
|
31
|
+
...launchOptions,
|
|
32
|
+
...(extraHTTPHeaders && { extraHTTPHeaders }),
|
|
33
|
+
});
|
|
34
|
+
register(context);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// In headful mode, avoid launchPersistentContext to prevent "Browser window not found"
|
|
38
|
+
browserInstance = await constants.launcher.launch(launchOptions);
|
|
39
|
+
register(browserInstance);
|
|
40
|
+
context = await browserInstance.newContext({
|
|
41
|
+
...(extraHTTPHeaders && { extraHTTPHeaders }),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
32
44
|
const page = await context.newPage();
|
|
33
45
|
for (const path of sitemapPaths) {
|
|
34
46
|
sitemapLink = homeUrl + path;
|
|
@@ -39,6 +51,9 @@ const crawlIntelligentSitemap = async (url, randomToken, host, viewportSettings,
|
|
|
39
51
|
}
|
|
40
52
|
await page.close();
|
|
41
53
|
await context.close().catch(() => { });
|
|
54
|
+
if (browserInstance) {
|
|
55
|
+
await browserInstance.close().catch(() => { });
|
|
56
|
+
}
|
|
42
57
|
return sitemapExist ? sitemapLink : '';
|
|
43
58
|
}
|
|
44
59
|
const checkUrlExists = async (page, parsedUrl) => {
|
|
@@ -1,12 +1,44 @@
|
|
|
1
|
-
/* eslint-disable no-shadow */
|
|
2
1
|
/* eslint-disable no-alert */
|
|
3
2
|
/* eslint-disable no-param-reassign */
|
|
4
3
|
/* eslint-env browser */
|
|
5
4
|
import path from 'path';
|
|
5
|
+
import { getDomain } from 'tldts';
|
|
6
6
|
import { runAxeScript } from '../commonCrawlerFunc.js';
|
|
7
7
|
import { consoleLogger, guiInfoLog } from '../../logs.js';
|
|
8
8
|
import { guiInfoStatusTypes } from '../../constants/constants.js';
|
|
9
9
|
import { isSkippedUrl, validateCustomFlowLabel } from '../../constants/common.js';
|
|
10
|
+
const sameRegistrableDomain = (hostA, hostB) => {
|
|
11
|
+
const domainA = getDomain(hostA);
|
|
12
|
+
const domainB = getDomain(hostB);
|
|
13
|
+
if (!domainA || !domainB)
|
|
14
|
+
return hostA === hostB;
|
|
15
|
+
return domainA === domainB;
|
|
16
|
+
};
|
|
17
|
+
const parseBoolEnv = (val, defaultVal) => {
|
|
18
|
+
if (val == null)
|
|
19
|
+
return defaultVal;
|
|
20
|
+
const v = String(val).trim().toLowerCase();
|
|
21
|
+
if (['1', 'true', 'yes', 'y', 'on'].includes(v))
|
|
22
|
+
return true;
|
|
23
|
+
if (['0', 'false', 'no', 'n', 'off'].includes(v))
|
|
24
|
+
return false;
|
|
25
|
+
return defaultVal;
|
|
26
|
+
};
|
|
27
|
+
const RESTRICT_OVERLAY_TO_ENTRY_DOMAIN = parseBoolEnv(process.env.RESTRICT_OVERLAY_TO_ENTRY_DOMAIN, false);
|
|
28
|
+
const isOverlayAllowed = (currentUrl, entryUrl) => {
|
|
29
|
+
try {
|
|
30
|
+
const cur = new URL(currentUrl);
|
|
31
|
+
if (cur.protocol !== 'http:' && cur.protocol !== 'https:')
|
|
32
|
+
return false;
|
|
33
|
+
if (!RESTRICT_OVERLAY_TO_ENTRY_DOMAIN)
|
|
34
|
+
return true;
|
|
35
|
+
const base = new URL(entryUrl);
|
|
36
|
+
return sameRegistrableDomain(cur.hostname, base.hostname);
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
10
42
|
//! For Cypress Test
|
|
11
43
|
// env to check if Cypress test is running
|
|
12
44
|
const isCypressTest = process.env.IS_CYPRESS_TEST === 'true';
|
|
@@ -33,9 +65,7 @@ export const screenshotFullPage = async (page, screenshotsDir, screenshotIdx) =>
|
|
|
33
65
|
const isLoadMoreContent = async () => new Promise(resolve => {
|
|
34
66
|
setTimeout(async () => {
|
|
35
67
|
await page.waitForLoadState('domcontentloaded');
|
|
36
|
-
const newHeight = await page.evaluate(
|
|
37
|
-
// eslint-disable-next-line no-shadow
|
|
38
|
-
() => document.body.scrollHeight);
|
|
68
|
+
const newHeight = await page.evaluate(() => document.body.scrollHeight);
|
|
39
69
|
const result = newHeight > prevHeight;
|
|
40
70
|
resolve(result);
|
|
41
71
|
}, 2500);
|
|
@@ -157,7 +187,7 @@ export const MENU_POSITION = {
|
|
|
157
187
|
export const updateMenu = async (page, urlsCrawled) => {
|
|
158
188
|
log(`Overlay menu: updating: ${page.url()}`);
|
|
159
189
|
await page.evaluate(vars => {
|
|
160
|
-
const shadowHost = document.querySelector('#
|
|
190
|
+
const shadowHost = document.querySelector('#oobeeShadowHost');
|
|
161
191
|
if (shadowHost) {
|
|
162
192
|
const p = shadowHost.shadowRoot.querySelector('#oobee-p-pages-scanned');
|
|
163
193
|
if (p) {
|
|
@@ -200,7 +230,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
200
230
|
</svg>
|
|
201
231
|
`;
|
|
202
232
|
minBtn.innerHTML = MINBTN_SVG;
|
|
203
|
-
let currentPos =
|
|
233
|
+
let currentPos = vars.menuPos || 'RIGHT';
|
|
204
234
|
const isCollapsed = () => panel.classList.contains('collapsed');
|
|
205
235
|
const setPosClass = (pos) => {
|
|
206
236
|
panel.classList.remove('pos-left', 'pos-right');
|
|
@@ -217,7 +247,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
217
247
|
setDraggableSidebarMenu();
|
|
218
248
|
};
|
|
219
249
|
const toggleCollapsed = (force) => {
|
|
220
|
-
const willCollapse =
|
|
250
|
+
const willCollapse = typeof force === 'boolean' ? force : !isCollapsed();
|
|
221
251
|
if (willCollapse) {
|
|
222
252
|
panel.classList.add('collapsed');
|
|
223
253
|
localStorage.setItem('oobee:overlay-collapsed', '1');
|
|
@@ -292,12 +322,12 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
292
322
|
}
|
|
293
323
|
const ol = document.createElement('ol');
|
|
294
324
|
ol.className = 'oobee-ol';
|
|
295
|
-
scanned.forEach(
|
|
325
|
+
scanned.forEach(item => {
|
|
296
326
|
const li = document.createElement('li');
|
|
297
327
|
li.className = 'oobee-li';
|
|
298
328
|
const title = document.createElement('div');
|
|
299
329
|
title.className = 'oobee-item-title';
|
|
300
|
-
title.textContent =
|
|
330
|
+
title.textContent = item.pageTitle && item.pageTitle.trim() ? item.pageTitle : item.url;
|
|
301
331
|
const url = document.createElement('div');
|
|
302
332
|
url.className = 'oobee-item-url';
|
|
303
333
|
url.textContent = item.url;
|
|
@@ -596,8 +626,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
596
626
|
if (!icon)
|
|
597
627
|
return;
|
|
598
628
|
const closed = isCollapsed();
|
|
599
|
-
const arrowPointsRight = (currentPos === 'RIGHT' && !closed) ||
|
|
600
|
-
(currentPos === 'LEFT' && closed);
|
|
629
|
+
const arrowPointsRight = (currentPos === 'RIGHT' && !closed) || (currentPos === 'LEFT' && closed);
|
|
601
630
|
icon.classList.toggle('is-left', !arrowPointsRight);
|
|
602
631
|
minBtn.setAttribute('aria-label', closed ? 'Expand panel' : 'Collapse panel');
|
|
603
632
|
}
|
|
@@ -652,7 +681,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
652
681
|
borderRadius: '16px',
|
|
653
682
|
overflow: 'hidden',
|
|
654
683
|
boxShadow: '0 10px 40px rgba(0,0,0,.35)',
|
|
655
|
-
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif'
|
|
684
|
+
fontFamily: 'system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif',
|
|
656
685
|
});
|
|
657
686
|
const dialogSheet = new CSSStyleSheet();
|
|
658
687
|
dialogSheet.replaceSync(`
|
|
@@ -689,12 +718,17 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
689
718
|
display: 'flex',
|
|
690
719
|
alignItems: 'center',
|
|
691
720
|
justifyContent: 'space-between',
|
|
692
|
-
gap: '8px'
|
|
721
|
+
gap: '8px',
|
|
693
722
|
});
|
|
694
723
|
const title = document.createElement('h2');
|
|
695
724
|
title.id = 'oobee-stop-title';
|
|
696
725
|
title.textContent = 'Are you sure you want to stop this scan?';
|
|
697
|
-
Object.assign(title.style, {
|
|
726
|
+
Object.assign(title.style, {
|
|
727
|
+
margin: '0',
|
|
728
|
+
fontSize: '22px',
|
|
729
|
+
fontWeight: '700',
|
|
730
|
+
lineHeight: '1.25',
|
|
731
|
+
});
|
|
698
732
|
const closeX = document.createElement('button');
|
|
699
733
|
closeX.type = 'button';
|
|
700
734
|
closeX.setAttribute('aria-label', 'Close');
|
|
@@ -711,13 +745,13 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
711
745
|
height: '36px',
|
|
712
746
|
borderRadius: '12px',
|
|
713
747
|
display: 'grid',
|
|
714
|
-
placeItems: 'center'
|
|
748
|
+
placeItems: 'center',
|
|
715
749
|
});
|
|
716
750
|
head.appendChild(title);
|
|
717
751
|
head.appendChild(closeX);
|
|
718
752
|
const bodyWrap = document.createElement('div');
|
|
719
753
|
Object.assign(bodyWrap.style, {
|
|
720
|
-
padding: '12px 20px 20px 20px'
|
|
754
|
+
padding: '12px 20px 20px 20px',
|
|
721
755
|
});
|
|
722
756
|
const form = document.createElement('form');
|
|
723
757
|
form.noValidate = true;
|
|
@@ -725,7 +759,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
725
759
|
Object.assign(form.style, {
|
|
726
760
|
display: 'grid',
|
|
727
761
|
gridTemplateColumns: '1fr',
|
|
728
|
-
rowGap: '12px'
|
|
762
|
+
rowGap: '12px',
|
|
729
763
|
});
|
|
730
764
|
const label = document.createElement('label');
|
|
731
765
|
label.setAttribute('for', 'oobee-stop-input');
|
|
@@ -741,7 +775,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
741
775
|
padding: '12px 14px',
|
|
742
776
|
fontSize: '14px',
|
|
743
777
|
outline: 'none',
|
|
744
|
-
boxSizing: 'border-box'
|
|
778
|
+
boxSizing: 'border-box',
|
|
745
779
|
});
|
|
746
780
|
input.addEventListener('focus', () => {
|
|
747
781
|
input.style.borderColor = '#7b4dff';
|
|
@@ -765,7 +799,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
765
799
|
fontWeight: '600',
|
|
766
800
|
color: '#fff',
|
|
767
801
|
background: '#9021A6',
|
|
768
|
-
cursor: 'pointer'
|
|
802
|
+
cursor: 'pointer',
|
|
769
803
|
});
|
|
770
804
|
const cancel = document.createElement('button');
|
|
771
805
|
cancel.type = 'button';
|
|
@@ -777,7 +811,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
777
811
|
fontSize: '14px',
|
|
778
812
|
justifySelf: 'center',
|
|
779
813
|
cursor: 'pointer',
|
|
780
|
-
padding: '6px'
|
|
814
|
+
padding: '6px',
|
|
781
815
|
});
|
|
782
816
|
actions.appendChild(primary);
|
|
783
817
|
actions.appendChild(cancel);
|
|
@@ -792,10 +826,13 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
792
826
|
stopDialog.appendChild(bodyWrap);
|
|
793
827
|
shadowRoot.appendChild(stopDialog);
|
|
794
828
|
let stopResolver = null;
|
|
795
|
-
const hideStop = () => {
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
829
|
+
const hideStop = () => {
|
|
830
|
+
try {
|
|
831
|
+
stopDialog.close();
|
|
832
|
+
}
|
|
833
|
+
catch { }
|
|
834
|
+
stopResolver = null;
|
|
835
|
+
};
|
|
799
836
|
const showStop = () => {
|
|
800
837
|
if (!shouldHideInput)
|
|
801
838
|
input.value = '';
|
|
@@ -813,7 +850,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
813
850
|
});
|
|
814
851
|
}
|
|
815
852
|
};
|
|
816
|
-
form.addEventListener('submit',
|
|
853
|
+
form.addEventListener('submit', e => {
|
|
817
854
|
e.preventDefault();
|
|
818
855
|
const v = (input.value || '').trim();
|
|
819
856
|
if (stopResolver)
|
|
@@ -830,13 +867,13 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
830
867
|
stopResolver({ confirmed: false, label: '' });
|
|
831
868
|
hideStop();
|
|
832
869
|
});
|
|
833
|
-
stopDialog.addEventListener('cancel',
|
|
870
|
+
stopDialog.addEventListener('cancel', e => {
|
|
834
871
|
e.preventDefault();
|
|
835
872
|
if (stopResolver)
|
|
836
873
|
stopResolver({ confirmed: false, label: '' });
|
|
837
874
|
hideStop();
|
|
838
875
|
});
|
|
839
|
-
customWindow.oobeeShowStopModal = () => new Promise(
|
|
876
|
+
customWindow.oobeeShowStopModal = () => new Promise(resolve => {
|
|
840
877
|
stopResolver = resolve;
|
|
841
878
|
showStop();
|
|
842
879
|
});
|
|
@@ -861,7 +898,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
|
|
|
861
898
|
log('Overlay menu: successfully added');
|
|
862
899
|
})
|
|
863
900
|
.catch(error => {
|
|
864
|
-
error('Overlay menu: failed to add', error);
|
|
901
|
+
consoleLogger.error('Overlay menu: failed to add', error);
|
|
865
902
|
});
|
|
866
903
|
};
|
|
867
904
|
export const removeOverlayMenu = async (page) => {
|
|
@@ -910,11 +947,17 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
|
|
|
910
947
|
await processPage(page, processPageParams);
|
|
911
948
|
log('Scan: success');
|
|
912
949
|
pagesDict[pageId].isScanning = false;
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
950
|
+
const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
|
|
951
|
+
if (allowed) {
|
|
952
|
+
await addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
|
|
953
|
+
inProgress: false,
|
|
954
|
+
collapsed: !!pagesDict[pageId]?.collapsed,
|
|
955
|
+
hideStopInput: !!processPageParams.customFlowLabel,
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
else {
|
|
959
|
+
await removeOverlayMenu(page);
|
|
960
|
+
}
|
|
918
961
|
}
|
|
919
962
|
catch (error) {
|
|
920
963
|
log(`Scan failed ${error}`);
|
|
@@ -977,6 +1020,11 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
|
|
|
977
1020
|
};
|
|
978
1021
|
page.on('domcontentloaded', async () => {
|
|
979
1022
|
try {
|
|
1023
|
+
const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
|
|
1024
|
+
if (!allowed) {
|
|
1025
|
+
await removeOverlayMenu(page);
|
|
1026
|
+
return;
|
|
1027
|
+
}
|
|
980
1028
|
const existingOverlay = await page.evaluate(() => {
|
|
981
1029
|
return document.querySelector('#oobeeShadowHost');
|
|
982
1030
|
});
|
|
@@ -989,11 +1037,6 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
|
|
|
989
1037
|
hideStopInput: !!processPageParams.customFlowLabel,
|
|
990
1038
|
});
|
|
991
1039
|
}
|
|
992
|
-
setTimeout(() => {
|
|
993
|
-
// Timeout here to slow things down a little
|
|
994
|
-
}, 1000);
|
|
995
|
-
//! For Cypress Test
|
|
996
|
-
// Auto-clicks 'Scan this page' button only once
|
|
997
1040
|
if (isCypressTest) {
|
|
998
1041
|
try {
|
|
999
1042
|
await handleOnScanClick();
|
|
@@ -1003,11 +1046,9 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
|
|
|
1003
1046
|
consoleLogger.info(`Error in calling handleOnScanClick, isCypressTest: ${isCypressTest}`);
|
|
1004
1047
|
}
|
|
1005
1048
|
}
|
|
1006
|
-
consoleLogger.info(`Overlay state: ${existingOverlay}`);
|
|
1007
1049
|
}
|
|
1008
1050
|
catch {
|
|
1009
1051
|
consoleLogger.info('Error in adding overlay menu to page');
|
|
1010
|
-
consoleLogger.info('Error in adding overlay menu to page');
|
|
1011
1052
|
}
|
|
1012
1053
|
});
|
|
1013
1054
|
await page.exposeFunction('handleOnScanClick', handleOnScanClick);
|
|
@@ -6,6 +6,7 @@ import constants, { getIntermediateScreenshotsPath, guiInfoStatusTypes, } from '
|
|
|
6
6
|
import { initNewPage, log } from './custom/utils.js';
|
|
7
7
|
import { guiInfoLog } from '../logs.js';
|
|
8
8
|
import { addUrlGuardScript } from './guards/urlGuard.js';
|
|
9
|
+
import { getPlaywrightLaunchOptions } from '../constants/common.js';
|
|
9
10
|
// Export of classes
|
|
10
11
|
export class ProcessPageParams {
|
|
11
12
|
constructor(scannedIdx, blacklistedPatterns, includeScreenshots, dataset, intermediateScreenshotsPath, urlsCrawled, randomToken) {
|
|
@@ -26,6 +27,7 @@ const runCustom = async (url, randomToken, viewportSettings, blacklistedPatterns
|
|
|
26
27
|
const intermediateScreenshotsPath = getIntermediateScreenshotsPath(randomToken);
|
|
27
28
|
const processPageParams = new ProcessPageParams(0, // scannedIdx
|
|
28
29
|
blacklistedPatterns, includeScreenshots, dataset, intermediateScreenshotsPath, urlsCrawled, randomToken);
|
|
30
|
+
processPageParams.entryUrl = url;
|
|
29
31
|
if (initialCustomFlowLabel && initialCustomFlowLabel.trim()) {
|
|
30
32
|
processPageParams.customFlowLabel = initialCustomFlowLabel.trim();
|
|
31
33
|
}
|
|
@@ -34,11 +36,18 @@ const runCustom = async (url, randomToken, viewportSettings, blacklistedPatterns
|
|
|
34
36
|
try {
|
|
35
37
|
const deviceConfig = viewportSettings.playwrightDeviceDetailsObject;
|
|
36
38
|
const hasCustomViewport = !!deviceConfig;
|
|
39
|
+
const baseLaunchOptions = getPlaywrightLaunchOptions();
|
|
40
|
+
// Merge base args with custom flow specific args
|
|
41
|
+
const baseArgs = baseLaunchOptions.args || [];
|
|
42
|
+
const customArgs = hasCustomViewport ? ['--window-size=1920,1040'] : ['--start-maximized'];
|
|
43
|
+
const mergedArgs = [
|
|
44
|
+
...baseArgs.filter(a => !a.startsWith('--window-size') && a !== '--start-maximized'),
|
|
45
|
+
...customArgs,
|
|
46
|
+
];
|
|
37
47
|
const browser = await chromium.launch({
|
|
38
|
-
|
|
48
|
+
...baseLaunchOptions,
|
|
49
|
+
args: mergedArgs,
|
|
39
50
|
headless: false,
|
|
40
|
-
channel: 'chrome',
|
|
41
|
-
// bypassCSP: true,
|
|
42
51
|
});
|
|
43
52
|
const context = await browser.newContext({
|
|
44
53
|
ignoreHTTPSErrors: true,
|
|
@@ -52,8 +61,7 @@ const runCustom = async (url, randomToken, viewportSettings, blacklistedPatterns
|
|
|
52
61
|
await context.close().catch(() => { });
|
|
53
62
|
await browser.close().catch(() => { });
|
|
54
63
|
}
|
|
55
|
-
catch {
|
|
56
|
-
}
|
|
64
|
+
catch { }
|
|
57
65
|
};
|
|
58
66
|
// For handling closing playwright browser and continue generate artifacts etc
|
|
59
67
|
registerSoftClose(processPageParams.stopAll);
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Builds pre-computed HTML groups to optimize Group by HTML Element functionality.
|
|
3
|
+
* Keys are composite "html\x00xpath" strings to ensure unique matching per element instance.
|
|
4
|
+
*/
|
|
5
|
+
export const buildHtmlGroups = (rule, items, pageUrl) => {
|
|
6
|
+
if (!rule.htmlGroups) {
|
|
7
|
+
rule.htmlGroups = {};
|
|
8
|
+
}
|
|
9
|
+
items.forEach(item => {
|
|
10
|
+
// Use composite key of html + xpath for precise matching
|
|
11
|
+
const htmlKey = `${item.html || 'No HTML element'}\x00${item.xpath || ''}`;
|
|
12
|
+
if (!rule.htmlGroups[htmlKey]) {
|
|
13
|
+
// Create new group with the first occurrence
|
|
14
|
+
rule.htmlGroups[htmlKey] = {
|
|
15
|
+
html: item.html || '',
|
|
16
|
+
xpath: item.xpath || '',
|
|
17
|
+
message: item.message || '',
|
|
18
|
+
screenshotPath: item.screenshotPath || '',
|
|
19
|
+
displayNeedsReview: item.displayNeedsReview,
|
|
20
|
+
pageUrls: [],
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
if (!rule.htmlGroups[htmlKey].pageUrls.includes(pageUrl)) {
|
|
24
|
+
rule.htmlGroups[htmlKey].pageUrls.push(pageUrl);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* Converts items in pagesAffected to references (html\x00xpath composite keys) for embedding in HTML report.
|
|
30
|
+
* Additionally, it deep-clones allIssues, replaces page.items objects with composite reference keys.
|
|
31
|
+
* Those refs are specifically for htmlGroups lookup (html + xpath).
|
|
32
|
+
*/
|
|
33
|
+
export const convertItemsToReferences = (allIssues) => {
|
|
34
|
+
const cloned = JSON.parse(JSON.stringify(allIssues));
|
|
35
|
+
['mustFix', 'goodToFix', 'needsReview', 'passed'].forEach(category => {
|
|
36
|
+
if (!cloned.items[category]?.rules)
|
|
37
|
+
return;
|
|
38
|
+
cloned.items[category].rules.forEach((rule) => {
|
|
39
|
+
if (!rule.pagesAffected || !rule.htmlGroups)
|
|
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;
|
|
55
|
+
};
|