@empiricalrun/test-gen 0.38.6 → 0.38.7
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/CHANGELOG.md
CHANGED
|
@@ -410,460 +410,6 @@ function annotateClickableElements(options = {}) {
|
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
|
|
413
|
-
// Initialize and enable annotations
|
|
414
|
-
function enable() {
|
|
415
|
-
clearAnnotations();
|
|
416
|
-
if (!annotationsContainer) {
|
|
417
|
-
annotationsContainer = document.createElement("div");
|
|
418
|
-
annotationsContainer.className = "annotations";
|
|
419
|
-
Object.assign(annotationsContainer.style, {
|
|
420
|
-
position: "absolute",
|
|
421
|
-
top: "0",
|
|
422
|
-
left: "0",
|
|
423
|
-
width: "100%",
|
|
424
|
-
height: "100%",
|
|
425
|
-
pointerEvents: "none",
|
|
426
|
-
zIndex: "9999",
|
|
427
|
-
});
|
|
428
|
-
document.body.appendChild(annotationsContainer);
|
|
429
|
-
initializeAnnotations(window, null, []);
|
|
430
|
-
} else {
|
|
431
|
-
annotationsContainer.style.display = "block";
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Destroy annotations
|
|
436
|
-
function destroy() {
|
|
437
|
-
if (annotationsContainer) {
|
|
438
|
-
clearAnnotations();
|
|
439
|
-
Object.values(annotationsMap).forEach((annotation) => {
|
|
440
|
-
annotation.node.style.boxShadow = "none";
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
annotationsContainer.parentNode.removeChild(annotationsContainer);
|
|
444
|
-
annotationsContainer = null;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
enable();
|
|
449
|
-
|
|
450
|
-
return {
|
|
451
|
-
annotations: annotationsMap,
|
|
452
|
-
destroy,
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
/* eslint-disable no-undef */
|
|
456
|
-
/**
|
|
457
|
-
* Annotates all clickable elements on the page with unique hint markers.
|
|
458
|
-
* Returns an object containing annotations and methods to enable/disable them.
|
|
459
|
-
*
|
|
460
|
-
* @param {Object} options - Configuration options for hint markers.
|
|
461
|
-
* @param {string} options.hintCharacterSet - Characters to use for generating hint identifiers.
|
|
462
|
-
* @param {number} options.maxHints - Maximum number of hints to generate.
|
|
463
|
-
* @param {string} options.markerClass - CSS class to apply to hint markers.
|
|
464
|
-
* @returns {Object} An object containing annotations map and enable/disable methods.
|
|
465
|
-
*/
|
|
466
|
-
function annotateClickableElements(options = {}) {
|
|
467
|
-
const {
|
|
468
|
-
hintCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", // Default set of characters for hints
|
|
469
|
-
maxHints = 1000, // Maximum number of hints to generate
|
|
470
|
-
markerClass = "hint-marker", // CSS class for markers
|
|
471
|
-
} = options;
|
|
472
|
-
|
|
473
|
-
const document = window.document;
|
|
474
|
-
const annotationsMap = {};
|
|
475
|
-
const usedHints = new Set();
|
|
476
|
-
let annotationsContainer = null;
|
|
477
|
-
|
|
478
|
-
// Check if the element is not blocked and visible for clicking
|
|
479
|
-
function isElementClickNotBlocked(element, windowToAnnotate) {
|
|
480
|
-
const rect = element.getBoundingClientRect();
|
|
481
|
-
|
|
482
|
-
// Calculate the center point of the element
|
|
483
|
-
const centerX = rect.left + rect.width / 2;
|
|
484
|
-
const centerY = rect.top + rect.height / 2;
|
|
485
|
-
|
|
486
|
-
// check if element is within the viewport
|
|
487
|
-
if (
|
|
488
|
-
centerX < 0 ||
|
|
489
|
-
centerY < 0 ||
|
|
490
|
-
centerX > (windowToAnnotate.innerWidth || document.documentElement.clientWidth) ||
|
|
491
|
-
centerY > (windowToAnnotate.innerHeight || document.documentElement.clientHeight)
|
|
492
|
-
) {
|
|
493
|
-
// Determine the viewport dimensions
|
|
494
|
-
const viewportWidth =
|
|
495
|
-
windowToAnnotate.innerWidth || document.documentElement.clientWidth;
|
|
496
|
-
const viewportHeight =
|
|
497
|
-
windowToAnnotate.innerHeight || document.documentElement.clientHeight;
|
|
498
|
-
|
|
499
|
-
// Calculate the new scroll positions to bring the element into the center of the viewport
|
|
500
|
-
const newScrollX = centerX - viewportWidth / 2;
|
|
501
|
-
const newScrollY = centerY - viewportHeight / 2;
|
|
502
|
-
|
|
503
|
-
// Scroll the window to the new positions
|
|
504
|
-
windowToAnnotate.scrollTo({
|
|
505
|
-
top: newScrollY,
|
|
506
|
-
left: newScrollX,
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
const newRect = element.getBoundingClientRect();
|
|
510
|
-
const newCenterX = newRect.left + newRect.width / 2;
|
|
511
|
-
const newCenterY = newRect.top + newRect.height / 2;
|
|
512
|
-
const topElement = document.elementFromPoint(newCenterX, newCenterY);
|
|
513
|
-
return element.contains(topElement);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const topElement = windowToAnnotate.document.elementFromPoint(centerX, centerY);
|
|
517
|
-
|
|
518
|
-
// Check if the topmost element is the target element or one of its descendants
|
|
519
|
-
return element.contains(topElement);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function generateHintStrings(charset, max) {
|
|
523
|
-
const hints = [];
|
|
524
|
-
let length = 1;
|
|
525
|
-
|
|
526
|
-
while (hints.length < max) {
|
|
527
|
-
const combos = cartesianProduct(Array(length).fill(charset.split("")));
|
|
528
|
-
for (const combo of combos) {
|
|
529
|
-
const hint = combo.join("");
|
|
530
|
-
if (!usedHints.has(hint)) {
|
|
531
|
-
hints.push(hint);
|
|
532
|
-
usedHints.add(hint);
|
|
533
|
-
}
|
|
534
|
-
if (hints.length >= max) break;
|
|
535
|
-
}
|
|
536
|
-
length++;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return hints;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function cartesianProduct(arrays) {
|
|
543
|
-
return arrays.reduce(
|
|
544
|
-
(acc, curr) =>
|
|
545
|
-
acc
|
|
546
|
-
.map((a) => curr.map((b) => a.concat([b])))
|
|
547
|
-
.reduce((a, b) => a.concat(b), []),
|
|
548
|
-
[[]]
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Check if an element is clickable
|
|
553
|
-
function isElementClickable(element) {
|
|
554
|
-
// if (!(element instanceof Element)) return false;
|
|
555
|
-
|
|
556
|
-
const tagName = element.tagName.toLowerCase();
|
|
557
|
-
let isClickable = false;
|
|
558
|
-
|
|
559
|
-
// Check for aria-disabled
|
|
560
|
-
const ariaDisabled = element.getAttribute("aria-disabled");
|
|
561
|
-
if (ariaDisabled && ["", "true"].includes(ariaDisabled.toLowerCase())) {
|
|
562
|
-
return false; // Element should not be clickable if aria-disabled is true
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Check for visibility
|
|
566
|
-
const style = window.getComputedStyle(element);
|
|
567
|
-
if (
|
|
568
|
-
style.display === "none" ||
|
|
569
|
-
style.visibility === "hidden" ||
|
|
570
|
-
// This is done for cases where opacity is undefined
|
|
571
|
-
// parseFloat(style.opacity) === 0
|
|
572
|
-
style.pointerEvents === "none"
|
|
573
|
-
) {
|
|
574
|
-
return false;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Check if element is disabled (for applicable elements)
|
|
578
|
-
if (element.disabled) return false;
|
|
579
|
-
|
|
580
|
-
// Check for AngularJS click handlers
|
|
581
|
-
if (!isElementClickable._checkForAngularJs) {
|
|
582
|
-
isElementClickable._checkForAngularJs = (function () {
|
|
583
|
-
const angularElements = document.getElementsByClassName("ng-scope");
|
|
584
|
-
if (angularElements.length === 0) {
|
|
585
|
-
return () => false;
|
|
586
|
-
} else {
|
|
587
|
-
const ngAttributes = [];
|
|
588
|
-
for (const prefix of ["", "data-", "x-"]) {
|
|
589
|
-
for (const separator of ["-", ":", "_"]) {
|
|
590
|
-
ngAttributes.push(`${prefix}ng${separator}click`);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return function (el) {
|
|
594
|
-
for (const attribute of ngAttributes) {
|
|
595
|
-
if (el.hasAttribute(attribute)) return true;
|
|
596
|
-
}
|
|
597
|
-
return false;
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
})();
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (!isClickable && isElementClickable._checkForAngularJs(element)) {
|
|
604
|
-
isClickable = true;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Check for onclick attribute or listener
|
|
608
|
-
if (
|
|
609
|
-
element.hasAttribute("onclick") ||
|
|
610
|
-
typeof element.onclick === "function"
|
|
611
|
-
) {
|
|
612
|
-
isClickable = true;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Check for jsaction attribute (commonly used in frameworks like Google's)
|
|
616
|
-
if (!isClickable && element.hasAttribute("jsaction")) {
|
|
617
|
-
const jsactionRules = element.getAttribute("jsaction").split(";");
|
|
618
|
-
for (const jsactionRule of jsactionRules) {
|
|
619
|
-
const ruleSplit = jsactionRule.trim().split(":");
|
|
620
|
-
if (ruleSplit.length >= 1 && ruleSplit.length <= 2) {
|
|
621
|
-
const [eventType] = ruleSplit[0].trim().split(".");
|
|
622
|
-
if (eventType === "click") {
|
|
623
|
-
isClickable = true;
|
|
624
|
-
break;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Check for role attributes that imply clickability
|
|
631
|
-
if (!isClickable) {
|
|
632
|
-
const role = element.getAttribute("role");
|
|
633
|
-
const clickableRoles = [
|
|
634
|
-
"button",
|
|
635
|
-
"tab",
|
|
636
|
-
"link",
|
|
637
|
-
"checkbox",
|
|
638
|
-
"menuitem",
|
|
639
|
-
"menuitemcheckbox",
|
|
640
|
-
"menuitemradio",
|
|
641
|
-
"radio",
|
|
642
|
-
"switch",
|
|
643
|
-
];
|
|
644
|
-
if (role && clickableRoles.includes(role.toLowerCase())) {
|
|
645
|
-
isClickable = true;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Check for contentEditable
|
|
650
|
-
if (!isClickable) {
|
|
651
|
-
const contentEditable = element.getAttribute("contentEditable");
|
|
652
|
-
if (
|
|
653
|
-
contentEditable != null &&
|
|
654
|
-
["", "contenteditable", "true"].includes(contentEditable.toLowerCase())
|
|
655
|
-
) {
|
|
656
|
-
isClickable = true;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Tag-specific clickability
|
|
661
|
-
const focusableTags = [
|
|
662
|
-
"a",
|
|
663
|
-
"button",
|
|
664
|
-
"input",
|
|
665
|
-
"select",
|
|
666
|
-
"textarea",
|
|
667
|
-
"object",
|
|
668
|
-
"embed",
|
|
669
|
-
"label",
|
|
670
|
-
"details",
|
|
671
|
-
];
|
|
672
|
-
if (focusableTags.includes(tagName)) {
|
|
673
|
-
switch (tagName) {
|
|
674
|
-
case "a":
|
|
675
|
-
// Ensure it's not just a named anchor without href
|
|
676
|
-
if (element.hasAttribute("href")) {
|
|
677
|
-
isClickable = true;
|
|
678
|
-
}
|
|
679
|
-
break;
|
|
680
|
-
case "input": {
|
|
681
|
-
const type = (element.getAttribute("type") || "").toLowerCase();
|
|
682
|
-
if (
|
|
683
|
-
type !== "hidden" &&
|
|
684
|
-
!element.disabled &&
|
|
685
|
-
!(element.readOnly && isInputSelectable(element))
|
|
686
|
-
) {
|
|
687
|
-
isClickable = true;
|
|
688
|
-
}
|
|
689
|
-
break;
|
|
690
|
-
}
|
|
691
|
-
case "textarea":
|
|
692
|
-
if (!element.disabled && !element.readOnly) {
|
|
693
|
-
isClickable = true;
|
|
694
|
-
}
|
|
695
|
-
break;
|
|
696
|
-
case "button":
|
|
697
|
-
case "select":
|
|
698
|
-
if (!element.disabled) {
|
|
699
|
-
isClickable = true;
|
|
700
|
-
}
|
|
701
|
-
break;
|
|
702
|
-
case "object":
|
|
703
|
-
case "embed":
|
|
704
|
-
isClickable = true;
|
|
705
|
-
break;
|
|
706
|
-
case "label":
|
|
707
|
-
if (element.control && !element.control.disabled) {
|
|
708
|
-
isClickable = true;
|
|
709
|
-
}
|
|
710
|
-
break;
|
|
711
|
-
case "details":
|
|
712
|
-
isClickable = true;
|
|
713
|
-
break;
|
|
714
|
-
default:
|
|
715
|
-
break;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Check for class names containing 'button' as a possible click target
|
|
720
|
-
if (!isClickable) {
|
|
721
|
-
const className = element.getAttribute("class");
|
|
722
|
-
if (className && className.toLowerCase().includes("button")) {
|
|
723
|
-
isClickable = true;
|
|
724
|
-
} else if (element.classList.contains("cursor-pointer")) {
|
|
725
|
-
isClickable = true;
|
|
726
|
-
} else if (element.classList.contains("v-list-item--link")) {
|
|
727
|
-
// vue specific click handling
|
|
728
|
-
isClickable = true;
|
|
729
|
-
} else if (element.style.cursor === "pointer") {
|
|
730
|
-
isClickable = true;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Check for tabindex
|
|
735
|
-
if (!isClickable) {
|
|
736
|
-
const tabIndexValue = element.getAttribute("tabindex");
|
|
737
|
-
const tabIndex = tabIndexValue ? parseInt(tabIndexValue) : -1;
|
|
738
|
-
if (tabIndex >= 0 && !isNaN(tabIndex)) {
|
|
739
|
-
isClickable = true;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
return isClickable;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function isInputSelectable(input) {
|
|
747
|
-
const selectableTypes = [
|
|
748
|
-
"text",
|
|
749
|
-
"search",
|
|
750
|
-
"password",
|
|
751
|
-
"url",
|
|
752
|
-
"email",
|
|
753
|
-
"number",
|
|
754
|
-
"tel",
|
|
755
|
-
];
|
|
756
|
-
const type = (input.getAttribute("type") || "").toLowerCase();
|
|
757
|
-
return selectableTypes.includes(type);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
var parentElements = [];
|
|
761
|
-
|
|
762
|
-
// Create a hint marker
|
|
763
|
-
function createHintMarker(el, hint, parentElement) {
|
|
764
|
-
const rect = el.getBoundingClientRect();
|
|
765
|
-
const parentRect = parentElement.getBoundingClientRect();
|
|
766
|
-
|
|
767
|
-
// Create the marker element
|
|
768
|
-
const marker = document.createElement("div");
|
|
769
|
-
marker.textContent = hint;
|
|
770
|
-
marker.className = markerClass;
|
|
771
|
-
|
|
772
|
-
// Style the marker
|
|
773
|
-
Object.assign(marker.style, {
|
|
774
|
-
position: "absolute",
|
|
775
|
-
top: `${rect.top + parentRect.top}px`,
|
|
776
|
-
left: `${rect.left + parentRect.left}px`,
|
|
777
|
-
background:
|
|
778
|
-
"-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
|
|
779
|
-
padding: "1px 3px 0px",
|
|
780
|
-
borderRadius: "3px",
|
|
781
|
-
border: "1px solid rgb(227, 190, 35)",
|
|
782
|
-
fontSize: "11px",
|
|
783
|
-
pointerEvents: "none",
|
|
784
|
-
zIndex: "10000",
|
|
785
|
-
whiteSpace: "nowrap",
|
|
786
|
-
overflow: "hidden",
|
|
787
|
-
boxShadow: "rgba(0, 0, 0, 0.3) 0px 3px 7px 0px",
|
|
788
|
-
letterSpacing: 0,
|
|
789
|
-
minHeight: 0,
|
|
790
|
-
lineHeight: "100%",
|
|
791
|
-
color: "rgb(48, 37, 5)",
|
|
792
|
-
fontFamily: "Helvetica, Arial, sans-serif",
|
|
793
|
-
fontWeight: "bold",
|
|
794
|
-
textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// Attach the marker to the specified parent element
|
|
798
|
-
parentElement.appendChild(marker);
|
|
799
|
-
parentElements.push(parentElement);
|
|
800
|
-
return marker;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
// Clear existing annotations
|
|
805
|
-
//TODO: Handle clearing annotations
|
|
806
|
-
function clearAnnotations() {
|
|
807
|
-
parentElements.forEach((parentElement) => {
|
|
808
|
-
const markers = parentElement.querySelectorAll(`.${markerClass}`);
|
|
809
|
-
markers.forEach((marker) => marker.remove());
|
|
810
|
-
});
|
|
811
|
-
parentElements = [];
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Initialize annotations for a given window (including iframes)
|
|
815
|
-
function initializeAnnotations(windowToAnnotate, parentHints, depth) {
|
|
816
|
-
const container =
|
|
817
|
-
parentHints?.nodeName === "IFRAME"
|
|
818
|
-
? parentHints.contentWindow.document.body
|
|
819
|
-
: annotationsContainer;
|
|
820
|
-
|
|
821
|
-
// Ensure the container exists
|
|
822
|
-
if (!container) return;
|
|
823
|
-
|
|
824
|
-
// Filter for clickable elements
|
|
825
|
-
const clickableElements = Array.from(windowToAnnotate.document.querySelectorAll("*")).filter((el) => {
|
|
826
|
-
return isElementClickable(el) && isElementClickNotBlocked(el, windowToAnnotate);
|
|
827
|
-
});
|
|
828
|
-
// Generate hint strings for the clickable elements
|
|
829
|
-
const hints = generateHintStrings(hintCharacterSet, Math.min(maxHints, clickableElements.length));
|
|
830
|
-
|
|
831
|
-
// Create markers for the elements
|
|
832
|
-
clickableElements.slice(0, maxHints).forEach((el, index) => {
|
|
833
|
-
const hint = hints[index];
|
|
834
|
-
const rect = el.getBoundingClientRect();
|
|
835
|
-
|
|
836
|
-
// Use createHintMarker with the specified container
|
|
837
|
-
createHintMarker(el, hint, container);
|
|
838
|
-
el.style.boxShadow = `inset 0 0 0px 2px red`;
|
|
839
|
-
|
|
840
|
-
// Add element details to the annotations map
|
|
841
|
-
annotationsMap[hint] = {
|
|
842
|
-
node: el,
|
|
843
|
-
rect: {
|
|
844
|
-
top: rect.top + windowToAnnotate.scrollY,
|
|
845
|
-
left: rect.left + windowToAnnotate.scrollX,
|
|
846
|
-
width: rect.width,
|
|
847
|
-
height: rect.height,
|
|
848
|
-
},
|
|
849
|
-
depth: [...depth ]
|
|
850
|
-
};
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
// Process iframes recursively
|
|
854
|
-
Array.from(windowToAnnotate.document.querySelectorAll("iframe")).forEach((iframe) => {
|
|
855
|
-
try {
|
|
856
|
-
const frameWindow = iframe.contentWindow;
|
|
857
|
-
if (frameWindow && iframe.offsetWidth > 0 && iframe.offsetHeight > 0) {
|
|
858
|
-
initializeAnnotations(frameWindow, iframe, [...depth, iframe]);
|
|
859
|
-
}
|
|
860
|
-
} catch (e) {
|
|
861
|
-
console.warn("Cannot access iframe:", e);
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
|
|
867
413
|
// Initialize and enable annotations
|
|
868
414
|
function enable() {
|
|
869
415
|
clearAnnotations();
|
|
@@ -410,460 +410,6 @@ function annotateClickableElements(options = {}) {
|
|
|
410
410
|
}
|
|
411
411
|
|
|
412
412
|
|
|
413
|
-
// Initialize and enable annotations
|
|
414
|
-
function enable() {
|
|
415
|
-
clearAnnotations();
|
|
416
|
-
if (!annotationsContainer) {
|
|
417
|
-
annotationsContainer = document.createElement("div");
|
|
418
|
-
annotationsContainer.className = "annotations";
|
|
419
|
-
Object.assign(annotationsContainer.style, {
|
|
420
|
-
position: "absolute",
|
|
421
|
-
top: "0",
|
|
422
|
-
left: "0",
|
|
423
|
-
width: "100%",
|
|
424
|
-
height: "100%",
|
|
425
|
-
pointerEvents: "none",
|
|
426
|
-
zIndex: "9999",
|
|
427
|
-
});
|
|
428
|
-
document.body.appendChild(annotationsContainer);
|
|
429
|
-
initializeAnnotations(window, null, []);
|
|
430
|
-
} else {
|
|
431
|
-
annotationsContainer.style.display = "block";
|
|
432
|
-
}
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
// Destroy annotations
|
|
436
|
-
function destroy() {
|
|
437
|
-
if (annotationsContainer) {
|
|
438
|
-
clearAnnotations();
|
|
439
|
-
Object.values(annotationsMap).forEach((annotation) => {
|
|
440
|
-
annotation.node.style.boxShadow = "none";
|
|
441
|
-
});
|
|
442
|
-
|
|
443
|
-
annotationsContainer.parentNode.removeChild(annotationsContainer);
|
|
444
|
-
annotationsContainer = null;
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
enable();
|
|
449
|
-
|
|
450
|
-
return {
|
|
451
|
-
annotations: annotationsMap,
|
|
452
|
-
destroy,
|
|
453
|
-
};
|
|
454
|
-
}
|
|
455
|
-
/* eslint-disable no-undef */
|
|
456
|
-
/**
|
|
457
|
-
* Annotates all clickable elements on the page with unique hint markers.
|
|
458
|
-
* Returns an object containing annotations and methods to enable/disable them.
|
|
459
|
-
*
|
|
460
|
-
* @param {Object} options - Configuration options for hint markers.
|
|
461
|
-
* @param {string} options.hintCharacterSet - Characters to use for generating hint identifiers.
|
|
462
|
-
* @param {number} options.maxHints - Maximum number of hints to generate.
|
|
463
|
-
* @param {string} options.markerClass - CSS class to apply to hint markers.
|
|
464
|
-
* @returns {Object} An object containing annotations map and enable/disable methods.
|
|
465
|
-
*/
|
|
466
|
-
function annotateClickableElements(options = {}) {
|
|
467
|
-
const {
|
|
468
|
-
hintCharacterSet = "ABCDEFGHIJKLMNOPQRSTUVWXYZ", // Default set of characters for hints
|
|
469
|
-
maxHints = 1000, // Maximum number of hints to generate
|
|
470
|
-
markerClass = "hint-marker", // CSS class for markers
|
|
471
|
-
} = options;
|
|
472
|
-
|
|
473
|
-
const document = window.document;
|
|
474
|
-
const annotationsMap = {};
|
|
475
|
-
const usedHints = new Set();
|
|
476
|
-
let annotationsContainer = null;
|
|
477
|
-
|
|
478
|
-
// Check if the element is not blocked and visible for clicking
|
|
479
|
-
function isElementClickNotBlocked(element, windowToAnnotate) {
|
|
480
|
-
const rect = element.getBoundingClientRect();
|
|
481
|
-
|
|
482
|
-
// Calculate the center point of the element
|
|
483
|
-
const centerX = rect.left + rect.width / 2;
|
|
484
|
-
const centerY = rect.top + rect.height / 2;
|
|
485
|
-
|
|
486
|
-
// check if element is within the viewport
|
|
487
|
-
if (
|
|
488
|
-
centerX < 0 ||
|
|
489
|
-
centerY < 0 ||
|
|
490
|
-
centerX > (windowToAnnotate.innerWidth || document.documentElement.clientWidth) ||
|
|
491
|
-
centerY > (windowToAnnotate.innerHeight || document.documentElement.clientHeight)
|
|
492
|
-
) {
|
|
493
|
-
// Determine the viewport dimensions
|
|
494
|
-
const viewportWidth =
|
|
495
|
-
windowToAnnotate.innerWidth || document.documentElement.clientWidth;
|
|
496
|
-
const viewportHeight =
|
|
497
|
-
windowToAnnotate.innerHeight || document.documentElement.clientHeight;
|
|
498
|
-
|
|
499
|
-
// Calculate the new scroll positions to bring the element into the center of the viewport
|
|
500
|
-
const newScrollX = centerX - viewportWidth / 2;
|
|
501
|
-
const newScrollY = centerY - viewportHeight / 2;
|
|
502
|
-
|
|
503
|
-
// Scroll the window to the new positions
|
|
504
|
-
windowToAnnotate.scrollTo({
|
|
505
|
-
top: newScrollY,
|
|
506
|
-
left: newScrollX,
|
|
507
|
-
});
|
|
508
|
-
|
|
509
|
-
const newRect = element.getBoundingClientRect();
|
|
510
|
-
const newCenterX = newRect.left + newRect.width / 2;
|
|
511
|
-
const newCenterY = newRect.top + newRect.height / 2;
|
|
512
|
-
const topElement = document.elementFromPoint(newCenterX, newCenterY);
|
|
513
|
-
return element.contains(topElement);
|
|
514
|
-
}
|
|
515
|
-
|
|
516
|
-
const topElement = windowToAnnotate.document.elementFromPoint(centerX, centerY);
|
|
517
|
-
|
|
518
|
-
// Check if the topmost element is the target element or one of its descendants
|
|
519
|
-
return element.contains(topElement);
|
|
520
|
-
}
|
|
521
|
-
|
|
522
|
-
function generateHintStrings(charset, max) {
|
|
523
|
-
const hints = [];
|
|
524
|
-
let length = 1;
|
|
525
|
-
|
|
526
|
-
while (hints.length < max) {
|
|
527
|
-
const combos = cartesianProduct(Array(length).fill(charset.split("")));
|
|
528
|
-
for (const combo of combos) {
|
|
529
|
-
const hint = combo.join("");
|
|
530
|
-
if (!usedHints.has(hint)) {
|
|
531
|
-
hints.push(hint);
|
|
532
|
-
usedHints.add(hint);
|
|
533
|
-
}
|
|
534
|
-
if (hints.length >= max) break;
|
|
535
|
-
}
|
|
536
|
-
length++;
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
return hints;
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
function cartesianProduct(arrays) {
|
|
543
|
-
return arrays.reduce(
|
|
544
|
-
(acc, curr) =>
|
|
545
|
-
acc
|
|
546
|
-
.map((a) => curr.map((b) => a.concat([b])))
|
|
547
|
-
.reduce((a, b) => a.concat(b), []),
|
|
548
|
-
[[]]
|
|
549
|
-
);
|
|
550
|
-
}
|
|
551
|
-
|
|
552
|
-
// Check if an element is clickable
|
|
553
|
-
function isElementClickable(element) {
|
|
554
|
-
// if (!(element instanceof Element)) return false;
|
|
555
|
-
|
|
556
|
-
const tagName = element.tagName.toLowerCase();
|
|
557
|
-
let isClickable = false;
|
|
558
|
-
|
|
559
|
-
// Check for aria-disabled
|
|
560
|
-
const ariaDisabled = element.getAttribute("aria-disabled");
|
|
561
|
-
if (ariaDisabled && ["", "true"].includes(ariaDisabled.toLowerCase())) {
|
|
562
|
-
return false; // Element should not be clickable if aria-disabled is true
|
|
563
|
-
}
|
|
564
|
-
|
|
565
|
-
// Check for visibility
|
|
566
|
-
const style = window.getComputedStyle(element);
|
|
567
|
-
if (
|
|
568
|
-
style.display === "none" ||
|
|
569
|
-
style.visibility === "hidden" ||
|
|
570
|
-
// This is done for cases where opacity is undefined
|
|
571
|
-
// parseFloat(style.opacity) === 0
|
|
572
|
-
style.pointerEvents === "none"
|
|
573
|
-
) {
|
|
574
|
-
return false;
|
|
575
|
-
}
|
|
576
|
-
|
|
577
|
-
// Check if element is disabled (for applicable elements)
|
|
578
|
-
if (element.disabled) return false;
|
|
579
|
-
|
|
580
|
-
// Check for AngularJS click handlers
|
|
581
|
-
if (!isElementClickable._checkForAngularJs) {
|
|
582
|
-
isElementClickable._checkForAngularJs = (function () {
|
|
583
|
-
const angularElements = document.getElementsByClassName("ng-scope");
|
|
584
|
-
if (angularElements.length === 0) {
|
|
585
|
-
return () => false;
|
|
586
|
-
} else {
|
|
587
|
-
const ngAttributes = [];
|
|
588
|
-
for (const prefix of ["", "data-", "x-"]) {
|
|
589
|
-
for (const separator of ["-", ":", "_"]) {
|
|
590
|
-
ngAttributes.push(`${prefix}ng${separator}click`);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return function (el) {
|
|
594
|
-
for (const attribute of ngAttributes) {
|
|
595
|
-
if (el.hasAttribute(attribute)) return true;
|
|
596
|
-
}
|
|
597
|
-
return false;
|
|
598
|
-
};
|
|
599
|
-
}
|
|
600
|
-
})();
|
|
601
|
-
}
|
|
602
|
-
|
|
603
|
-
if (!isClickable && isElementClickable._checkForAngularJs(element)) {
|
|
604
|
-
isClickable = true;
|
|
605
|
-
}
|
|
606
|
-
|
|
607
|
-
// Check for onclick attribute or listener
|
|
608
|
-
if (
|
|
609
|
-
element.hasAttribute("onclick") ||
|
|
610
|
-
typeof element.onclick === "function"
|
|
611
|
-
) {
|
|
612
|
-
isClickable = true;
|
|
613
|
-
}
|
|
614
|
-
|
|
615
|
-
// Check for jsaction attribute (commonly used in frameworks like Google's)
|
|
616
|
-
if (!isClickable && element.hasAttribute("jsaction")) {
|
|
617
|
-
const jsactionRules = element.getAttribute("jsaction").split(";");
|
|
618
|
-
for (const jsactionRule of jsactionRules) {
|
|
619
|
-
const ruleSplit = jsactionRule.trim().split(":");
|
|
620
|
-
if (ruleSplit.length >= 1 && ruleSplit.length <= 2) {
|
|
621
|
-
const [eventType] = ruleSplit[0].trim().split(".");
|
|
622
|
-
if (eventType === "click") {
|
|
623
|
-
isClickable = true;
|
|
624
|
-
break;
|
|
625
|
-
}
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
}
|
|
629
|
-
|
|
630
|
-
// Check for role attributes that imply clickability
|
|
631
|
-
if (!isClickable) {
|
|
632
|
-
const role = element.getAttribute("role");
|
|
633
|
-
const clickableRoles = [
|
|
634
|
-
"button",
|
|
635
|
-
"tab",
|
|
636
|
-
"link",
|
|
637
|
-
"checkbox",
|
|
638
|
-
"menuitem",
|
|
639
|
-
"menuitemcheckbox",
|
|
640
|
-
"menuitemradio",
|
|
641
|
-
"radio",
|
|
642
|
-
"switch",
|
|
643
|
-
];
|
|
644
|
-
if (role && clickableRoles.includes(role.toLowerCase())) {
|
|
645
|
-
isClickable = true;
|
|
646
|
-
}
|
|
647
|
-
}
|
|
648
|
-
|
|
649
|
-
// Check for contentEditable
|
|
650
|
-
if (!isClickable) {
|
|
651
|
-
const contentEditable = element.getAttribute("contentEditable");
|
|
652
|
-
if (
|
|
653
|
-
contentEditable != null &&
|
|
654
|
-
["", "contenteditable", "true"].includes(contentEditable.toLowerCase())
|
|
655
|
-
) {
|
|
656
|
-
isClickable = true;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
|
|
660
|
-
// Tag-specific clickability
|
|
661
|
-
const focusableTags = [
|
|
662
|
-
"a",
|
|
663
|
-
"button",
|
|
664
|
-
"input",
|
|
665
|
-
"select",
|
|
666
|
-
"textarea",
|
|
667
|
-
"object",
|
|
668
|
-
"embed",
|
|
669
|
-
"label",
|
|
670
|
-
"details",
|
|
671
|
-
];
|
|
672
|
-
if (focusableTags.includes(tagName)) {
|
|
673
|
-
switch (tagName) {
|
|
674
|
-
case "a":
|
|
675
|
-
// Ensure it's not just a named anchor without href
|
|
676
|
-
if (element.hasAttribute("href")) {
|
|
677
|
-
isClickable = true;
|
|
678
|
-
}
|
|
679
|
-
break;
|
|
680
|
-
case "input": {
|
|
681
|
-
const type = (element.getAttribute("type") || "").toLowerCase();
|
|
682
|
-
if (
|
|
683
|
-
type !== "hidden" &&
|
|
684
|
-
!element.disabled &&
|
|
685
|
-
!(element.readOnly && isInputSelectable(element))
|
|
686
|
-
) {
|
|
687
|
-
isClickable = true;
|
|
688
|
-
}
|
|
689
|
-
break;
|
|
690
|
-
}
|
|
691
|
-
case "textarea":
|
|
692
|
-
if (!element.disabled && !element.readOnly) {
|
|
693
|
-
isClickable = true;
|
|
694
|
-
}
|
|
695
|
-
break;
|
|
696
|
-
case "button":
|
|
697
|
-
case "select":
|
|
698
|
-
if (!element.disabled) {
|
|
699
|
-
isClickable = true;
|
|
700
|
-
}
|
|
701
|
-
break;
|
|
702
|
-
case "object":
|
|
703
|
-
case "embed":
|
|
704
|
-
isClickable = true;
|
|
705
|
-
break;
|
|
706
|
-
case "label":
|
|
707
|
-
if (element.control && !element.control.disabled) {
|
|
708
|
-
isClickable = true;
|
|
709
|
-
}
|
|
710
|
-
break;
|
|
711
|
-
case "details":
|
|
712
|
-
isClickable = true;
|
|
713
|
-
break;
|
|
714
|
-
default:
|
|
715
|
-
break;
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
|
|
719
|
-
// Check for class names containing 'button' as a possible click target
|
|
720
|
-
if (!isClickable) {
|
|
721
|
-
const className = element.getAttribute("class");
|
|
722
|
-
if (className && className.toLowerCase().includes("button")) {
|
|
723
|
-
isClickable = true;
|
|
724
|
-
} else if (element.classList.contains("cursor-pointer")) {
|
|
725
|
-
isClickable = true;
|
|
726
|
-
} else if (element.classList.contains("v-list-item--link")) {
|
|
727
|
-
// vue specific click handling
|
|
728
|
-
isClickable = true;
|
|
729
|
-
} else if (element.style.cursor === "pointer") {
|
|
730
|
-
isClickable = true;
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
|
|
734
|
-
// Check for tabindex
|
|
735
|
-
if (!isClickable) {
|
|
736
|
-
const tabIndexValue = element.getAttribute("tabindex");
|
|
737
|
-
const tabIndex = tabIndexValue ? parseInt(tabIndexValue) : -1;
|
|
738
|
-
if (tabIndex >= 0 && !isNaN(tabIndex)) {
|
|
739
|
-
isClickable = true;
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
|
|
743
|
-
return isClickable;
|
|
744
|
-
}
|
|
745
|
-
|
|
746
|
-
function isInputSelectable(input) {
|
|
747
|
-
const selectableTypes = [
|
|
748
|
-
"text",
|
|
749
|
-
"search",
|
|
750
|
-
"password",
|
|
751
|
-
"url",
|
|
752
|
-
"email",
|
|
753
|
-
"number",
|
|
754
|
-
"tel",
|
|
755
|
-
];
|
|
756
|
-
const type = (input.getAttribute("type") || "").toLowerCase();
|
|
757
|
-
return selectableTypes.includes(type);
|
|
758
|
-
}
|
|
759
|
-
|
|
760
|
-
var parentElements = [];
|
|
761
|
-
|
|
762
|
-
// Create a hint marker
|
|
763
|
-
function createHintMarker(el, hint, parentElement) {
|
|
764
|
-
const rect = el.getBoundingClientRect();
|
|
765
|
-
const parentRect = parentElement.getBoundingClientRect();
|
|
766
|
-
|
|
767
|
-
// Create the marker element
|
|
768
|
-
const marker = document.createElement("div");
|
|
769
|
-
marker.textContent = hint;
|
|
770
|
-
marker.className = markerClass;
|
|
771
|
-
|
|
772
|
-
// Style the marker
|
|
773
|
-
Object.assign(marker.style, {
|
|
774
|
-
position: "absolute",
|
|
775
|
-
top: `${rect.top + parentRect.top}px`,
|
|
776
|
-
left: `${rect.left + parentRect.left}px`,
|
|
777
|
-
background:
|
|
778
|
-
"-webkit-gradient(linear, 0% 0%, 0% 100%, from(rgb(255, 247, 133)), to(rgb(255, 197, 66)))",
|
|
779
|
-
padding: "1px 3px 0px",
|
|
780
|
-
borderRadius: "3px",
|
|
781
|
-
border: "1px solid rgb(227, 190, 35)",
|
|
782
|
-
fontSize: "11px",
|
|
783
|
-
pointerEvents: "none",
|
|
784
|
-
zIndex: "10000",
|
|
785
|
-
whiteSpace: "nowrap",
|
|
786
|
-
overflow: "hidden",
|
|
787
|
-
boxShadow: "rgba(0, 0, 0, 0.3) 0px 3px 7px 0px",
|
|
788
|
-
letterSpacing: 0,
|
|
789
|
-
minHeight: 0,
|
|
790
|
-
lineHeight: "100%",
|
|
791
|
-
color: "rgb(48, 37, 5)",
|
|
792
|
-
fontFamily: "Helvetica, Arial, sans-serif",
|
|
793
|
-
fontWeight: "bold",
|
|
794
|
-
textShadow: "rgba(255, 255, 255, 0.6) 0px 1px 0px",
|
|
795
|
-
});
|
|
796
|
-
|
|
797
|
-
// Attach the marker to the specified parent element
|
|
798
|
-
parentElement.appendChild(marker);
|
|
799
|
-
parentElements.push(parentElement);
|
|
800
|
-
return marker;
|
|
801
|
-
}
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
// Clear existing annotations
|
|
805
|
-
//TODO: Handle clearing annotations
|
|
806
|
-
function clearAnnotations() {
|
|
807
|
-
parentElements.forEach((parentElement) => {
|
|
808
|
-
const markers = parentElement.querySelectorAll(`.${markerClass}`);
|
|
809
|
-
markers.forEach((marker) => marker.remove());
|
|
810
|
-
});
|
|
811
|
-
parentElements = [];
|
|
812
|
-
}
|
|
813
|
-
|
|
814
|
-
// Initialize annotations for a given window (including iframes)
|
|
815
|
-
function initializeAnnotations(windowToAnnotate, parentHints, depth) {
|
|
816
|
-
const container =
|
|
817
|
-
parentHints?.nodeName === "IFRAME"
|
|
818
|
-
? parentHints.contentWindow.document.body
|
|
819
|
-
: annotationsContainer;
|
|
820
|
-
|
|
821
|
-
// Ensure the container exists
|
|
822
|
-
if (!container) return;
|
|
823
|
-
|
|
824
|
-
// Filter for clickable elements
|
|
825
|
-
const clickableElements = Array.from(windowToAnnotate.document.querySelectorAll("*")).filter((el) => {
|
|
826
|
-
return isElementClickable(el) && isElementClickNotBlocked(el, windowToAnnotate);
|
|
827
|
-
});
|
|
828
|
-
// Generate hint strings for the clickable elements
|
|
829
|
-
const hints = generateHintStrings(hintCharacterSet, Math.min(maxHints, clickableElements.length));
|
|
830
|
-
|
|
831
|
-
// Create markers for the elements
|
|
832
|
-
clickableElements.slice(0, maxHints).forEach((el, index) => {
|
|
833
|
-
const hint = hints[index];
|
|
834
|
-
const rect = el.getBoundingClientRect();
|
|
835
|
-
|
|
836
|
-
// Use createHintMarker with the specified container
|
|
837
|
-
createHintMarker(el, hint, container);
|
|
838
|
-
el.style.boxShadow = `inset 0 0 0px 2px red`;
|
|
839
|
-
|
|
840
|
-
// Add element details to the annotations map
|
|
841
|
-
annotationsMap[hint] = {
|
|
842
|
-
node: el,
|
|
843
|
-
rect: {
|
|
844
|
-
top: rect.top + windowToAnnotate.scrollY,
|
|
845
|
-
left: rect.left + windowToAnnotate.scrollX,
|
|
846
|
-
width: rect.width,
|
|
847
|
-
height: rect.height,
|
|
848
|
-
},
|
|
849
|
-
depth: [...depth ]
|
|
850
|
-
};
|
|
851
|
-
});
|
|
852
|
-
|
|
853
|
-
// Process iframes recursively
|
|
854
|
-
Array.from(windowToAnnotate.document.querySelectorAll("iframe")).forEach((iframe) => {
|
|
855
|
-
try {
|
|
856
|
-
const frameWindow = iframe.contentWindow;
|
|
857
|
-
if (frameWindow && iframe.offsetWidth > 0 && iframe.offsetHeight > 0) {
|
|
858
|
-
initializeAnnotations(frameWindow, iframe, [...depth, iframe]);
|
|
859
|
-
}
|
|
860
|
-
} catch (e) {
|
|
861
|
-
console.warn("Cannot access iframe:", e);
|
|
862
|
-
}
|
|
863
|
-
});
|
|
864
|
-
}
|
|
865
|
-
|
|
866
|
-
|
|
867
413
|
// Initialize and enable annotations
|
|
868
414
|
function enable() {
|
|
869
415
|
clearAnnotations();
|