@govtechsg/oobee 0.10.85 → 0.10.87

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.
Files changed (62) hide show
  1. package/.github/workflows/publish.yml +10 -0
  2. package/DETAILS.md +29 -0
  3. package/dist/cli.js +18 -5
  4. package/dist/combine.js +3 -1
  5. package/dist/constants/cliFunctions.js +2 -2
  6. package/dist/constants/common.js +70 -17
  7. package/dist/constants/constants.js +604 -1
  8. package/dist/crawlers/commonCrawlerFunc.js +3 -2
  9. package/dist/crawlers/crawlDomain.js +38 -13
  10. package/dist/crawlers/crawlIntelligentSitemap.js +62 -30
  11. package/dist/crawlers/crawlSitemap.js +141 -84
  12. package/dist/crawlers/custom/utils.js +218 -71
  13. package/dist/crawlers/guards/urlGuard.js +8 -15
  14. package/dist/crawlers/runCustom.js +18 -11
  15. package/dist/generateHtmlReport.js +18 -11
  16. package/dist/generateOobeeClientScanner.js +570 -0
  17. package/dist/mergeAxeResults/itemReferences.js +60 -25
  18. package/dist/mergeAxeResults/sentryTelemetry.js +4 -1
  19. package/dist/mergeAxeResults.js +23 -13
  20. package/dist/npmIndex.js +10 -2
  21. package/dist/proxyService.js +18 -3
  22. package/dist/services/s3Uploader.js +21 -10
  23. package/dist/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
  24. package/dist/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  25. package/dist/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  26. package/dist/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +38 -2
  27. package/dist/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +1 -1
  28. package/dist/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
  29. package/dist/static/ejs/summary.ejs +19 -8
  30. package/dist/utils.js +4 -3
  31. package/fix-summary-html-oom-pr.md +62 -0
  32. package/oobee-client-scanner.js +34992 -0
  33. package/package.json +5 -5
  34. package/src/cli.ts +19 -5
  35. package/src/combine.ts +5 -1
  36. package/src/constants/cliFunctions.ts +2 -2
  37. package/src/constants/common.ts +87 -22
  38. package/src/constants/constants.ts +602 -1
  39. package/src/crawlers/commonCrawlerFunc.ts +4 -3
  40. package/src/crawlers/crawlDomain.ts +39 -13
  41. package/src/crawlers/crawlIntelligentSitemap.ts +63 -30
  42. package/src/crawlers/crawlSitemap.ts +165 -100
  43. package/src/crawlers/custom/utils.ts +241 -80
  44. package/src/crawlers/guards/urlGuard.ts +24 -31
  45. package/src/crawlers/runCustom.ts +29 -11
  46. package/src/generateHtmlReport.ts +21 -11
  47. package/src/generateOobeeClientScanner.ts +591 -0
  48. package/src/mergeAxeResults/itemReferences.ts +70 -26
  49. package/src/mergeAxeResults/sentryTelemetry.ts +4 -1
  50. package/src/mergeAxeResults.ts +26 -14
  51. package/src/npmIndex.ts +12 -2
  52. package/src/proxyService.ts +25 -4
  53. package/src/services/s3Uploader.ts +23 -11
  54. package/src/static/ejs/partials/scripts/decodeUnzipParse.ejs +6 -3
  55. package/src/static/ejs/partials/scripts/header/aboutScanModal/ScanConfiguration.ejs +2 -2
  56. package/src/static/ejs/partials/scripts/ruleModal/constants.ejs +1 -761
  57. package/src/static/ejs/partials/scripts/ruleModal/itemCardRenderer.ejs +38 -2
  58. package/src/static/ejs/partials/scripts/ruleModal/pageAccordionBuilder.ejs +1 -1
  59. package/src/static/ejs/partials/scripts/ruleModal/ruleOffcanvas.ejs +4 -4
  60. package/src/static/ejs/summary.ejs +19 -8
  61. package/src/utils.ts +4 -3
  62. package/testStaticJSScanner.html +534 -0
@@ -25,6 +25,7 @@ const parseBoolEnv = (val, defaultVal) => {
25
25
  return defaultVal;
26
26
  };
27
27
  const RESTRICT_OVERLAY_TO_ENTRY_DOMAIN = parseBoolEnv(process.env.RESTRICT_OVERLAY_TO_ENTRY_DOMAIN, false);
28
+ const OVERLAY_OPERATION_TIMEOUT_MS = 5000;
28
29
  const isOverlayAllowed = (currentUrl, entryUrl) => {
29
30
  try {
30
31
  const cur = new URL(currentUrl);
@@ -62,14 +63,19 @@ export const screenshotFullPage = async (page, screenshotsDir, screenshotIdx) =>
62
63
  await page.evaluate(() => {
63
64
  window.scrollTo(0, document.body.scrollHeight);
64
65
  });
65
- const isLoadMoreContent = async () => new Promise(resolve => {
66
- setTimeout(async () => {
66
+ const isLoadMoreContent = async () => {
67
+ await new Promise(resolve => setTimeout(resolve, 2500));
68
+ if (page.isClosed())
69
+ return false;
70
+ try {
67
71
  await page.waitForLoadState('domcontentloaded');
68
72
  const newHeight = await page.evaluate(() => document.body.scrollHeight);
69
- const result = newHeight > prevHeight;
70
- resolve(result);
71
- }, 2500);
72
- });
73
+ return newHeight > prevHeight;
74
+ }
75
+ catch {
76
+ return false;
77
+ }
78
+ };
73
79
  const result = await isLoadMoreContent();
74
80
  return result;
75
81
  };
@@ -201,7 +207,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
201
207
  inProgress: false,
202
208
  collapsed: false,
203
209
  }) => {
204
- await page.waitForLoadState('domcontentloaded');
210
+ await page.waitForLoadState('domcontentloaded', { timeout: OVERLAY_OPERATION_TIMEOUT_MS });
205
211
  consoleLogger.info(`Overlay menu: adding to ${menuPos}...`);
206
212
  // Add the overlay menu with initial styling
207
213
  return page
@@ -291,22 +297,60 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
291
297
  const h2 = document.createElement('h2');
292
298
  h2.id = 'oobeeHPagesScanned';
293
299
  h2.className = 'oobee-section-title';
294
- h2.textContent = 'Pages Scanned';
300
+ h2.textContent = `Pages Scanned (${vars.urlsCrawled.scanned.length || 0})`;
301
+ const scanIcon = document.createElement('span');
302
+ scanIcon.className = 'oobee-btn-icon';
303
+ const SCAN_SVG = `
304
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
305
+ <g clip-path="url(#clip0_1421_431)">
306
+ <path d="M12.5763 11.5472L12.2958 11.2857L12.1037 11.1005C12.776 10.3183 12.9194 9.56432 12.9194 8.45969C12.9194 5.99657 10.9228 4 8.45969 4C5.99657 4 4 5.99657 4 8.45969C4 10.9228 5.99657 12.9194 8.45969 12.9194C9.56432 12.9194 10.3183 12.776 11.1005 12.1037L11.2857 12.2958L11.5472 12.5763L14.9777 16L16 14.9777L12.5763 11.5472ZM8.45969 11.5472C6.75129 11.5472 5.37221 10.1681 5.37221 8.45969C5.37221 6.75129 6.75129 5.37221 8.45969 5.37221C10.1681 5.37221 11.5472 6.75129 11.5472 8.45969C11.5472 10.1681 10.1681 11.5472 8.45969 11.5472Z" fill="white"/>
307
+ <path d="M18.5 0H19.5C19.7761 0 20 0.223858 20 0.5V5H18.5V0Z" fill="white"/>
308
+ <path d="M19.5 2.18552e-08L19.5 1.5L15 1.5L15 -2.18556e-07L19.5 2.18552e-08Z" fill="white"/>
309
+ <path d="M1.5 0H0.5C0.223858 0 0 0.223858 0 0.5V5H1.5V0Z" fill="white"/>
310
+ <path d="M0.5 2.18552e-08L0.5 1.5L5 1.5L5 -2.18556e-07L0.5 2.18552e-08Z" fill="white"/>
311
+ <path d="M1.5 20H0.5C0.223858 20 0 19.7761 0 19.5V15H1.5V20Z" fill="white"/>
312
+ <path d="M0.5 20L0.5 18.5L5 18.5L5 20L0.5 20Z" fill="white"/>
313
+ <path d="M18.5 20H19.5C19.7761 20 20 19.7761 20 19.5V15H18.5V20Z" fill="white"/>
314
+ <path d="M19.5 20L19.5 18.5L15 18.5L15 20L19.5 20Z" fill="white"/>
315
+ </g>
316
+ <defs>
317
+ <clipPath id="clip0_1421_431">
318
+ <rect width="20" height="20" fill="white"/>
319
+ </clipPath>
320
+ </defs>
321
+ </svg>
322
+ `;
323
+ scanIcon.innerHTML = SCAN_SVG;
295
324
  const scanBtn = document.createElement('button');
296
325
  scanBtn.id = 'oobeeBtnScan';
297
326
  scanBtn.className = 'oobee-btn oobee-btn-primary';
298
- scanBtn.innerText = 'Scan this page';
299
327
  scanBtn.disabled = inProgress;
328
+ scanBtn.appendChild(scanIcon);
329
+ const scanText = document.createElement('span');
330
+ scanText.className = 'oobee-btn-text';
331
+ scanText.innerText = 'Scan page';
332
+ scanBtn.appendChild(scanText);
300
333
  scanBtn.addEventListener('click', async () => customWindow.handleOnScanClick?.());
301
- const stopBtn = document.createElement('button');
302
- stopBtn.id = 'oobeeBtnStop';
303
- stopBtn.className = 'oobee-btn oobee-btn-secondary';
304
- stopBtn.innerText = 'Stop scan';
305
- stopBtn.addEventListener('click', async () => customWindow.handleOnStopClick?.());
334
+ const endScanIcon = document.createElement('span');
335
+ endScanIcon.className = 'oobee-btn-icon';
336
+ const ENDSCAN_SVG = `<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20" fill="none">
337
+ <path d="M10 0C4.47 0 0 4.47 0 10C0 15.53 4.47 20 10 20C15.53 20 20 15.53 20 10C20 4.47 15.53 0 10 0ZM10 18C5.59 18 2 14.41 2 10C2 5.59 5.59 2 10 2C14.41 2 18 5.59 18 10C18 14.41 14.41 18 10 18ZM13.59 5L10 8.59L6.41 5L5 6.41L8.59 10L5 13.59L6.41 15L10 11.41L13.59 15L15 13.59L11.41 10L15 6.41L13.59 5Z" fill="#9021A6"/>
338
+ </svg>
339
+ `;
340
+ endScanIcon.innerHTML = ENDSCAN_SVG;
341
+ const endScanBtn = document.createElement('button');
342
+ endScanBtn.id = 'oobeeBtnEndScan';
343
+ endScanBtn.className = 'oobee-btn oobee-btn-secondary';
344
+ endScanBtn.appendChild(endScanIcon);
345
+ const endScanText = document.createElement('span');
346
+ endScanText.className = 'oobee-btn-text';
347
+ endScanText.innerText = 'End scan';
348
+ endScanBtn.appendChild(endScanText);
349
+ endScanBtn.addEventListener('click', async () => customWindow.handleOnStopClick?.());
306
350
  const btnGroup = document.createElement('div');
307
351
  btnGroup.className = 'oobee-actions';
308
352
  btnGroup.appendChild(scanBtn);
309
- btnGroup.appendChild(stopBtn);
353
+ btnGroup.appendChild(endScanBtn);
310
354
  const listWrap = document.createElement('div');
311
355
  listWrap.id = 'oobeeList';
312
356
  listWrap.className = 'oobee-list';
@@ -370,7 +414,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
370
414
  border-right: 1px solid rgba(0,0,0,.08)
371
415
  }
372
416
  .oobee-panel.collapsed {
373
- width: 56px;
417
+ width: 58px;
374
418
  overflow: hidden
375
419
  }
376
420
 
@@ -447,6 +491,12 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
447
491
  padding: 1rem;
448
492
  }
449
493
 
494
+ .oobee-panel.collapsed .oobee-actions {
495
+ display: flex;
496
+ justify-content: center;
497
+ padding: 1rem 0.7rem;
498
+ }
499
+
450
500
  /* Base button */
451
501
  .oobee-btn {
452
502
  width: 100%;
@@ -457,6 +507,10 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
457
507
  line-height: 1.2;
458
508
  font-weight: 400;
459
509
  cursor: pointer;
510
+ display: flex;
511
+ align-items: center;
512
+ justify-content: center;
513
+ gap: 10px;
460
514
  transition: {
461
515
  box-shadow .12s ease,
462
516
  transform .02s ease,
@@ -470,6 +524,19 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
470
524
  cursor:not-allowed
471
525
  }
472
526
 
527
+ .oobee-panel.collapsed .oobee-btn {
528
+ width: 44px !important;
529
+ height: 44px !important;
530
+ min-width: 44px !important;
531
+ min-height: 44px !important;
532
+ max-width: 44px !important;
533
+ max-height: 44px !important;
534
+ border-radius: 50% !important;
535
+ padding: 0 !important;
536
+ justify-content: center;
537
+ gap: 0;
538
+ }
539
+
473
540
  /* Primary (filled) */
474
541
  .oobee-btn-primary {
475
542
  background: #9021a6;
@@ -525,6 +592,25 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
525
592
  display: none;
526
593
  }
527
594
 
595
+ .oobee-btn-icon {
596
+ display: inline-flex;
597
+ align-items: center;
598
+ justify-content: center;
599
+ width: 20px;
600
+ height: 20px;
601
+ vertical-align: middle;
602
+ }
603
+
604
+ .oobee-btn-text {
605
+ display: inline;
606
+ white-space: nowrap;
607
+ vertical-align: middle;
608
+ }
609
+
610
+ .oobee-panel.collapsed .oobee-btn-text {
611
+ display: none;
612
+ }
613
+
528
614
  #oobeeStopOverlay[hidden] {
529
615
  display:none !important;
530
616
  }
@@ -542,7 +628,10 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
542
628
  }
543
629
 
544
630
  .oobee-panel.collapsed .oobee-section-title {
545
- display: none;
631
+ font-size: 14px;
632
+ display: flex;
633
+ justify-content: center;
634
+ text-align: center;
546
635
  }
547
636
 
548
637
  .oobee-ol {
@@ -899,6 +988,7 @@ export const addOverlayMenu = async (page, urlsCrawled, menuPos, opts = {
899
988
  })
900
989
  .catch(error => {
901
990
  consoleLogger.error('Overlay menu: failed to add', error);
991
+ throw error;
902
992
  });
903
993
  };
904
994
  export const removeOverlayMenu = async (page) => {
@@ -919,9 +1009,18 @@ export const removeOverlayMenu = async (page) => {
919
1009
  };
920
1010
  export const initNewPage = async (page, pageClosePromises, processPageParams, pagesDict) => {
921
1011
  let menuPos = MENU_POSITION.right;
1012
+ let overlayRefreshSeq = 0;
1013
+ let overlayRefreshChain = Promise.resolve();
922
1014
  // eslint-disable-next-line no-underscore-dangle
923
1015
  const pageId = page._guid;
924
- page.on('dialog', () => { });
1016
+ page.on('dialog', async (dialog) => {
1017
+ try {
1018
+ await dialog.dismiss();
1019
+ }
1020
+ catch {
1021
+ // dialog may already be closed
1022
+ }
1023
+ });
925
1024
  const pageClosePromise = new Promise(resolve => {
926
1025
  page.on('close', () => {
927
1026
  log(`Page: close detected: ${page.url()}`);
@@ -937,6 +1036,68 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
937
1036
  collapsed: false,
938
1037
  };
939
1038
  }
1039
+ const reconcileOverlayMenu = async (trigger) => {
1040
+ // Mark this as the latest refresh so older ones can stop.
1041
+ const refreshSeq = ++overlayRefreshSeq;
1042
+ // Serialize overlay updates so multiple navigation events do not add/remove concurrently.
1043
+ overlayRefreshChain = overlayRefreshChain
1044
+ .catch(() => { })
1045
+ .then(async () => {
1046
+ if (refreshSeq !== overlayRefreshSeq || page.isClosed())
1047
+ return;
1048
+ try {
1049
+ // `framenavigated` can fire before the new document is ready for DOM inspection/injection.
1050
+ await page.waitForLoadState('domcontentloaded', { timeout: 5000 });
1051
+ }
1052
+ catch {
1053
+ // Best effort only. The page may still be mid-navigation.
1054
+ }
1055
+ try {
1056
+ // Give fast redirect chains a brief chance to advance before we inject/remove the overlay.
1057
+ await page.waitForTimeout(300);
1058
+ }
1059
+ catch {
1060
+ // Best effort only. The page may already be closing.
1061
+ }
1062
+ // Re-check staleness after waiting because a newer navigation may have happened meanwhile.
1063
+ if (refreshSeq !== overlayRefreshSeq || page.isClosed())
1064
+ return;
1065
+ const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1066
+ if (!allowed) {
1067
+ await Promise.race([
1068
+ removeOverlayMenu(page),
1069
+ new Promise((_, reject) => {
1070
+ setTimeout(() => {
1071
+ reject(new Error(`removeOverlayMenu timed out after ${OVERLAY_OPERATION_TIMEOUT_MS}ms`));
1072
+ }, OVERLAY_OPERATION_TIMEOUT_MS);
1073
+ }),
1074
+ ]);
1075
+ return;
1076
+ }
1077
+ const hasOverlay = await page.evaluate(() => Boolean(document.querySelector('#oobeeShadowHost')));
1078
+ consoleLogger.info(`Overlay state (${trigger}): ${hasOverlay}`);
1079
+ if (!hasOverlay) {
1080
+ // Recreate the overlay after allowed redirects while preserving current UI state.
1081
+ consoleLogger.info(`Adding overlay menu to page (${trigger}): ${page.url()}`);
1082
+ await Promise.race([
1083
+ addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
1084
+ inProgress: !!pagesDict[pageId]?.isScanning,
1085
+ collapsed: !!pagesDict[pageId]?.collapsed,
1086
+ hideStopInput: !!processPageParams.customFlowLabel,
1087
+ }),
1088
+ new Promise((_, reject) => {
1089
+ setTimeout(() => {
1090
+ reject(new Error(`addOverlayMenu timed out after ${OVERLAY_OPERATION_TIMEOUT_MS}ms`));
1091
+ }, OVERLAY_OPERATION_TIMEOUT_MS);
1092
+ }),
1093
+ ]);
1094
+ }
1095
+ })
1096
+ .catch(() => {
1097
+ consoleLogger.info('Error in adding overlay menu to page');
1098
+ });
1099
+ await overlayRefreshChain;
1100
+ };
940
1101
  // Window functions exposed in browser
941
1102
  const handleOnScanClick = async () => {
942
1103
  consoleLogger.info('Scan: click detected');
@@ -947,17 +1108,9 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
947
1108
  await processPage(page, processPageParams);
948
1109
  log('Scan: success');
949
1110
  pagesDict[pageId].isScanning = false;
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
- }
1111
+ if (page.isClosed())
1112
+ return;
1113
+ await reconcileOverlayMenu('scan-click');
961
1114
  }
962
1115
  catch (error) {
963
1116
  log(`Scan failed ${error}`);
@@ -987,10 +1140,10 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
987
1140
  });
988
1141
  if (!inputValue?.confirmed) {
989
1142
  await page.evaluate(() => {
990
- const stopBtn = document.getElementById('oobeeBtnStop');
991
- if (stopBtn) {
992
- stopBtn.disabled = false;
993
- stopBtn.textContent = 'Stop';
1143
+ const endScanBtn = document.getElementById('oobeeBtnEndScan');
1144
+ if (endScanBtn) {
1145
+ endScanBtn.disabled = false;
1146
+ endScanBtn.textContent = 'Stop';
994
1147
  }
995
1148
  });
996
1149
  return;
@@ -1019,47 +1172,41 @@ export const initNewPage = async (page, pageClosePromises, processPageParams, pa
1019
1172
  }
1020
1173
  };
1021
1174
  page.on('domcontentloaded', async () => {
1022
- try {
1023
- const allowed = isOverlayAllowed(page.url(), processPageParams.entryUrl);
1024
- if (!allowed) {
1025
- await removeOverlayMenu(page);
1026
- return;
1027
- }
1028
- const existingOverlay = await page.evaluate(() => {
1029
- return document.querySelector('#oobeeShadowHost');
1030
- });
1031
- consoleLogger.info(`Overlay state: ${existingOverlay}`);
1032
- if (!existingOverlay) {
1033
- consoleLogger.info(`Adding overlay menu to page: ${page.url()}`);
1034
- await addOverlayMenu(page, processPageParams.urlsCrawled, menuPos, {
1035
- inProgress: !!pagesDict[pageId]?.isScanning,
1036
- collapsed: !!pagesDict[pageId]?.collapsed,
1037
- hideStopInput: !!processPageParams.customFlowLabel,
1038
- });
1175
+ if (page.isClosed())
1176
+ return;
1177
+ await reconcileOverlayMenu('domcontentloaded');
1178
+ if (isCypressTest) {
1179
+ try {
1180
+ await handleOnScanClick();
1181
+ page.close();
1039
1182
  }
1040
- if (isCypressTest) {
1041
- try {
1042
- await handleOnScanClick();
1043
- page.close();
1044
- }
1045
- catch {
1046
- consoleLogger.info(`Error in calling handleOnScanClick, isCypressTest: ${isCypressTest}`);
1047
- }
1183
+ catch {
1184
+ consoleLogger.info(`Error in calling handleOnScanClick, isCypressTest: ${isCypressTest}`);
1048
1185
  }
1049
1186
  }
1050
- catch {
1051
- consoleLogger.info('Error in adding overlay menu to page');
1052
- }
1053
1187
  });
1054
- await page.exposeFunction('handleOnScanClick', handleOnScanClick);
1055
- await page.exposeFunction('handleOnStopClick', handleOnStopClick);
1056
- // Define the updateMenuPos function
1057
- const updateMenuPos = newPos => {
1058
- const prevPos = menuPos;
1059
- if (prevPos !== newPos) {
1060
- menuPos = newPos;
1061
- }
1062
- };
1063
- await page.exposeFunction('updateMenuPos', updateMenuPos);
1188
+ page.on('framenavigated', async (frame) => {
1189
+ if (frame !== page.mainFrame() || page.isClosed())
1190
+ return;
1191
+ await reconcileOverlayMenu('framenavigated');
1192
+ });
1193
+ try {
1194
+ if (page.isClosed())
1195
+ return page;
1196
+ await page.exposeFunction('handleOnScanClick', handleOnScanClick);
1197
+ await page.exposeFunction('handleOnStopClick', handleOnStopClick);
1198
+ // Define the updateMenuPos function
1199
+ const updateMenuPos = newPos => {
1200
+ const prevPos = menuPos;
1201
+ if (prevPos !== newPos) {
1202
+ menuPos = newPos;
1203
+ }
1204
+ };
1205
+ await page.exposeFunction('updateMenuPos', updateMenuPos);
1206
+ }
1207
+ catch (e) {
1208
+ log(`Error exposing functions on page: ${e}`);
1209
+ }
1210
+ await reconcileOverlayMenu('init');
1064
1211
  return page;
1065
1212
  };
@@ -2,15 +2,16 @@ const ALLOWED_PROTOCOLS = new Set(['http:', 'https:']);
2
2
  export function addUrlGuardScript(context, opts = {}) {
3
3
  const { fallbackUrl } = opts;
4
4
  const lastAllowedUrlByPage = new WeakMap();
5
- const attachGuardsToPage = (page) => {
5
+ const attachGuardsToPage = page => {
6
6
  if (!lastAllowedUrlByPage.has(page) && fallbackUrl) {
7
7
  lastAllowedUrlByPage.set(page, String(fallbackUrl));
8
8
  }
9
- page.addInitScript(() => {
10
- const isAllowedProtocol = (value) => {
9
+ page
10
+ .addInitScript(() => {
11
+ const isAllowedProtocol = value => {
11
12
  try {
12
13
  const s = value instanceof URL ? value.toString() : String(value);
13
- const protocol = new URL(s, window.location.href).protocol;
14
+ const { protocol } = new URL(s, window.location.href);
14
15
  return protocol === 'http:' || protocol === 'https:';
15
16
  }
16
17
  catch {
@@ -24,17 +25,9 @@ export function addUrlGuardScript(context, opts = {}) {
24
25
  return null;
25
26
  return openOriginal.call(this, targetUrl, ...args);
26
27
  };
27
- const assignOriginal = win.location.assign.bind(win.location);
28
- const replaceOriginal = win.location.replace.bind(win.location);
29
- win.location.assign = (nextUrl) => { if (isAllowedProtocol(nextUrl))
30
- assignOriginal(nextUrl); };
31
- win.location.replace = (nextUrl) => { if (isAllowedProtocol(nextUrl))
32
- replaceOriginal(nextUrl); };
33
- Object.defineProperty(win.location, 'href', {
34
- get() { return String(win.location.toString()); },
35
- set(nextUrl) { if (isAllowedProtocol(nextUrl))
36
- assignOriginal(nextUrl); },
37
- });
28
+ })
29
+ .catch(() => {
30
+ // page may have closed before addInitScript completed; safe to ignore
38
31
  });
39
32
  const restoreToSafeUrl = async (page, attemptedUrl) => {
40
33
  try {
@@ -1,12 +1,11 @@
1
1
  /* eslint-env browser */
2
- import { chromium } from 'playwright';
3
2
  import { createCrawleeSubFolders } from './commonCrawlerFunc.js';
4
3
  import { cleanUpAndExit, register, registerSoftClose } from '../utils.js';
5
4
  import constants, { getIntermediateScreenshotsPath, guiInfoStatusTypes, } from '../constants/constants.js';
6
5
  import { initNewPage, log } from './custom/utils.js';
7
6
  import { guiInfoLog } from '../logs.js';
8
7
  import { addUrlGuardScript } from './guards/urlGuard.js';
9
- import { getPlaywrightLaunchOptions } from '../constants/common.js';
8
+ import { getBrowserToRun, getPlaywrightLaunchOptions, initModifiedUserAgent, } from '../constants/common.js';
10
9
  // Export of classes
11
10
  export class ProcessPageParams {
12
11
  constructor(scannedIdx, blacklistedPatterns, includeScreenshots, dataset, intermediateScreenshotsPath, urlsCrawled, randomToken) {
@@ -19,7 +18,7 @@ export class ProcessPageParams {
19
18
  this.randomToken = randomToken;
20
19
  }
21
20
  }
22
- const runCustom = async (url, randomToken, viewportSettings, blacklistedPatterns, includeScreenshots, initialCustomFlowLabel) => {
21
+ const runCustom = async (url, randomToken, browserToRun, userDataDirectory, viewportSettings, blacklistedPatterns, includeScreenshots, initialCustomFlowLabel) => {
23
22
  // checks and delete datasets path if it already exists
24
23
  process.env.CRAWLEE_STORAGE_DIR = randomToken;
25
24
  const urlsCrawled = { ...constants.urlsCrawledObj };
@@ -34,9 +33,13 @@ const runCustom = async (url, randomToken, viewportSettings, blacklistedPatterns
34
33
  const pagesDict = {};
35
34
  const pageClosePromises = [];
36
35
  try {
36
+ const { browserToRun: resolvedBrowserToRun } = getBrowserToRun(randomToken, browserToRun, false);
37
37
  const deviceConfig = viewportSettings.playwrightDeviceDetailsObject;
38
38
  const hasCustomViewport = !!deviceConfig;
39
- const baseLaunchOptions = getPlaywrightLaunchOptions();
39
+ const rawDevice = (deviceConfig || {});
40
+ const { userAgent: deviceUserAgent, ...contextDeviceOptions } = rawDevice;
41
+ await initModifiedUserAgent(resolvedBrowserToRun, viewportSettings.playwrightDeviceDetailsObject);
42
+ const baseLaunchOptions = getPlaywrightLaunchOptions(resolvedBrowserToRun);
40
43
  // Merge base args with custom flow specific args
41
44
  const baseArgs = baseLaunchOptions.args || [];
42
45
  const customArgs = hasCustomViewport ? ['--window-size=1920,1040'] : ['--start-maximized'];
@@ -44,33 +47,37 @@ const runCustom = async (url, randomToken, viewportSettings, blacklistedPatterns
44
47
  ...baseArgs.filter(a => !a.startsWith('--window-size') && a !== '--start-maximized'),
45
48
  ...customArgs,
46
49
  ];
47
- const browser = await chromium.launch({
50
+ const context = await constants.launcher.launchPersistentContext(userDataDirectory, {
48
51
  ...baseLaunchOptions,
49
52
  args: mergedArgs,
50
53
  headless: false,
51
- });
52
- const context = await browser.newContext({
53
54
  ignoreHTTPSErrors: true,
54
55
  serviceWorkers: 'block',
55
56
  viewport: null,
56
- ...(hasCustomViewport ? deviceConfig : {}),
57
+ ...(hasCustomViewport ? contextDeviceOptions : {}),
58
+ userAgent: process.env.OOBEE_USER_AGENT || deviceUserAgent,
57
59
  });
58
60
  register(context);
59
61
  processPageParams.stopAll = async () => {
60
62
  try {
61
63
  await context.close().catch(() => { });
62
- await browser.close().catch(() => { });
63
64
  }
64
65
  catch { }
65
66
  };
66
67
  // For handling closing playwright browser and continue generate artifacts etc
67
68
  registerSoftClose(processPageParams.stopAll);
68
69
  addUrlGuardScript(context, { fallbackUrl: url });
70
+ const page = context.pages().find(existingPage => !existingPage.isClosed()) || (await context.newPage());
71
+ await initNewPage(page, pageClosePromises, processPageParams, pagesDict);
69
72
  // Detection of new page
70
73
  context.on('page', async (newPage) => {
71
- await initNewPage(newPage, pageClosePromises, processPageParams, pagesDict);
74
+ try {
75
+ await initNewPage(newPage, pageClosePromises, processPageParams, pagesDict);
76
+ }
77
+ catch (e) {
78
+ log(`Error initializing new page: ${e}`);
79
+ }
72
80
  });
73
- const page = await context.newPage();
74
81
  await page.goto(url, { timeout: 0 });
75
82
  // to execute and wait for all pages to close
76
83
  // idea is for promise to be pending until page.on('close') detected
@@ -1,7 +1,7 @@
1
1
  import fs from 'fs-extra';
2
2
  import path from 'path';
3
3
  import { compressJsonFileStreaming, writeHTML, flattenAndSortResults, populateScanPagesDetail, getWcagPassPercentage, getProgressPercentage, getIssuesPercentage, itemTypeDescription, oobeeAiHtmlETL, oobeeAiRules, formatAboutStartTime, convertItemsToReferences, } from './mergeAxeResults.js';
4
- import constants, { ScannerTypes, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, } from './constants/constants.js';
4
+ import constants, { ScannerTypes, WCAGclauses, a11yRuleShortDescriptionMap, disabilityBadgesMap, a11yRuleLongDescriptionMap, a11yRuleStepByStepGuide, } from './constants/constants.js';
5
5
  import { consoleLogger } from './logs.js';
6
6
  const ensureCategory = (categoryObj, categoryName) => {
7
7
  const rulesRaw = categoryObj?.rules ?? [];
@@ -23,7 +23,12 @@ const ensureCategory = (categoryObj, categoryName) => {
23
23
  rule.pagesAffected = [];
24
24
  }
25
25
  if (typeof rule.totalItems !== 'number') {
26
- rule.totalItems = rule.pagesAffected.reduce((accumulate, page) => accumulate + (Array.isArray(page.items) ? page.items.length : 0), 0);
26
+ rule.totalItems = rule.pagesAffected.reduce((accumulate, page) => accumulate +
27
+ (Array.isArray(page.items)
28
+ ? page.items.length
29
+ : typeof page.itemsCount === 'number'
30
+ ? page.itemsCount
31
+ : 0), 0);
27
32
  }
28
33
  });
29
34
  const totals = {
@@ -38,7 +43,7 @@ const ensureCategory = (categoryObj, categoryName) => {
38
43
  rules,
39
44
  };
40
45
  };
41
- export const generateHtmlReport = async (resultDir) => {
46
+ export const generateHtmlReport = async (resultDir, htmlFilename = 'report') => {
42
47
  try {
43
48
  const storagePath = path.resolve(resultDir);
44
49
  const scanDataJsonPath = path.join(storagePath, 'scanData.json');
@@ -61,17 +66,16 @@ export const generateHtmlReport = async (resultDir) => {
61
66
  }
62
67
  const scanData = JSON.parse(await fs.readFile(scanDataJsonPath, 'utf8'));
63
68
  const scanItemsAll = JSON.parse(await fs.readFile(scanItemsJsonPath, 'utf8'));
64
- // Use convertItemsToReferences to normalize items structure to match scanItemsWithHtmlGroupRefs format
65
- const scanItemsWithHtmlGroupRefs = convertItemsToReferences({
69
+ // Build the lighter scanItems payload used by the HTML report.
70
+ const lightScanItemsPayload = convertItemsToReferences({
66
71
  items: scanItemsAll,
67
- ...scanData
68
72
  });
69
- const { mustFix = {}, goodToFix = {}, needsReview = {}, passed = {}, } = scanItemsWithHtmlGroupRefs;
73
+ const { mustFix = {}, goodToFix = {}, needsReview = {}, } = lightScanItemsPayload;
70
74
  const items = {
71
75
  mustFix: ensureCategory(mustFix, 'mustFix'),
72
76
  goodToFix: ensureCategory(goodToFix, 'goodToFix'),
73
77
  needsReview: ensureCategory(needsReview, 'needsReview'),
74
- passed: ensureCategory(passed, 'passed'),
78
+ passed: ensureCategory(scanItemsAll.passed || {}, 'passed'),
75
79
  };
76
80
  const pagesScanned = Array.isArray(scanData.pagesScanned) ? scanData.pagesScanned : [];
77
81
  const pagesNotScanned = Array.isArray(scanData.pagesNotScanned) ? scanData.pagesNotScanned : [];
@@ -116,6 +120,8 @@ export const generateHtmlReport = async (resultDir) => {
116
120
  a11yRuleShortDescriptionMap,
117
121
  disabilityBadgesMap,
118
122
  a11yRuleLongDescriptionMap,
123
+ a11yRuleStepByStepGuide,
124
+ wcagCriteriaLabels: constants.wcagCriteriaLabels,
119
125
  advancedScanOptionsSummaryItems: {
120
126
  showIncludeScreenshots: !!scanData.advancedScanOptionsSummaryItems?.showIncludeScreenshots,
121
127
  showAllowSubdomains: !!scanData.advancedScanOptionsSummaryItems?.showAllowSubdomains,
@@ -137,9 +143,10 @@ export const generateHtmlReport = async (resultDir) => {
137
143
  allIssues.wcagPassPercentage = getWcagPassPercentage(allIssues.wcagViolations, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
138
144
  allIssues.progressPercentage = getProgressPercentage(allIssues.scanPagesDetail, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa);
139
145
  allIssues.issuesPercentage = await getIssuesPercentage(allIssues.scanPagesDetail, allIssues.advancedScanOptionsSummaryItems.showEnableWcagAaa, allIssues.advancedScanOptionsSummaryItems?.disableOobee);
140
- await writeHTML(allIssues, storagePath, 'report', scanDataB64Path, scanItemsB64Path);
141
- consoleLogger.info(`Report generated at: ${path.join(storagePath, 'report.html')}`);
142
- return path.join(storagePath, 'report.html');
146
+ await writeHTML(allIssues, storagePath, htmlFilename, scanDataB64Path, scanItemsB64Path);
147
+ const outputPath = path.join(storagePath, `${htmlFilename}.html`);
148
+ consoleLogger.info(`Report generated at: ${outputPath}`);
149
+ return outputPath;
143
150
  }
144
151
  catch (err) {
145
152
  consoleLogger.error(`generateHtmlReport failed: ${err?.message || err}`);