Skip to content

Commit b645664

Browse files
committed
feat: filter atomic child elements
1 parent 95a4d3c commit b645664

File tree

1 file changed

+151
-41
lines changed

1 file changed

+151
-41
lines changed

src/helpers/clientSelectorGenerator.ts

Lines changed: 151 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -555,36 +555,24 @@ class ClientSelectorGenerator {
555555
*/
556556
private isMeaningfulElement(element: HTMLElement): boolean {
557557
const tagName = element.tagName.toLowerCase();
558-
559-
// Fast path for common meaningful elements
560-
if (["a", "img", "input", "button", "select"].includes(tagName)) {
561-
return true;
558+
559+
if (tagName === "img") {
560+
return element.hasAttribute("src");
561+
}
562+
563+
if (element.children.length > 0) {
564+
return false;
562565
}
563566

564567
const text = (element.textContent || "").trim();
565568
const hasHref = element.hasAttribute("href");
566-
const hasSrc = element.hasAttribute("src");
567-
568-
// Quick checks first
569-
if (text.length > 0 || hasHref || hasSrc) {
569+
570+
if (text.length > 0) {
570571
return true;
571572
}
572573

573-
const isCustomElement = tagName.includes("-");
574-
575-
// For custom elements, be more lenient about what's considered meaningful
576-
if (isCustomElement) {
577-
const hasChildren = element.children.length > 0;
578-
const hasSignificantAttributes = Array.from(element.attributes).some(
579-
(attr) => !["class", "style", "id"].includes(attr.name.toLowerCase())
580-
);
581-
582-
return (
583-
hasChildren ||
584-
hasSignificantAttributes ||
585-
element.hasAttribute("role") ||
586-
element.hasAttribute("aria-label")
587-
);
574+
if (tagName === "a" && hasHref) {
575+
return true;
588576
}
589577

590578
return false;
@@ -2561,12 +2549,9 @@ class ClientSelectorGenerator {
25612549

25622550
const MAX_MEANINGFUL_ELEMENTS = 300;
25632551
const MAX_NODES_TO_CHECK = 1200;
2564-
const MAX_DEPTH = 12;
2552+
const MAX_DEPTH = 20;
25652553
let nodesChecked = 0;
25662554

2567-
let adjustedMaxDepth = MAX_DEPTH;
2568-
const elementDensityThreshold = 50;
2569-
25702555
const depths: number[] = [0];
25712556
let queueIndex = 0;
25722557

@@ -2576,14 +2561,10 @@ class ClientSelectorGenerator {
25762561
queueIndex++;
25772562
nodesChecked++;
25782563

2579-
if (currentDepth <= 3 && meaningfulDescendants.length > elementDensityThreshold) {
2580-
adjustedMaxDepth = Math.max(6, adjustedMaxDepth - 2);
2581-
}
2582-
25832564
if (
25842565
nodesChecked > MAX_NODES_TO_CHECK ||
25852566
meaningfulDescendants.length >= MAX_MEANINGFUL_ELEMENTS ||
2586-
currentDepth > adjustedMaxDepth
2567+
currentDepth > MAX_DEPTH
25872568
) {
25882569
break;
25892570
}
@@ -2592,7 +2573,7 @@ class ClientSelectorGenerator {
25922573
meaningfulDescendants.push(element);
25932574
}
25942575

2595-
if (currentDepth >= adjustedMaxDepth) {
2576+
if (currentDepth >= MAX_DEPTH) {
25962577
continue;
25972578
}
25982579

@@ -2607,7 +2588,7 @@ class ClientSelectorGenerator {
26072588
}
26082589
}
26092590

2610-
if (element.shadowRoot && currentDepth < adjustedMaxDepth - 1) {
2591+
if (element.shadowRoot && currentDepth < MAX_DEPTH - 1) {
26112592
const shadowChildren = element.shadowRoot.children;
26122593
const shadowLimit = Math.min(shadowChildren.length, 20);
26132594
for (let i = 0; i < shadowLimit; i++) {
@@ -2716,22 +2697,46 @@ class ClientSelectorGenerator {
27162697
}
27172698

27182699
if (!addPositionToAll) {
2719-
const meaningfulAttrs = ["role", "type", "name", "src", "aria-label"];
2700+
const meaningfulAttrs = ["role", "type"];
27202701
for (const attrName of meaningfulAttrs) {
27212702
if (element.hasAttribute(attrName)) {
27222703
const value = element.getAttribute(attrName)!.replace(/'/g, "\\'");
2723-
return `${tagName}[@${attrName}='${value}']`;
2704+
const isCommonAttribute = this.isAttributeCommonAcrossLists(
2705+
element,
2706+
attrName,
2707+
value,
2708+
otherListElements
2709+
);
2710+
if (isCommonAttribute) {
2711+
return `${tagName}[@${attrName}='${value}']`;
2712+
}
27242713
}
27252714
}
27262715
}
27272716

27282717
const testId = element.getAttribute("data-testid");
27292718
if (testId && !addPositionToAll) {
2730-
return `${tagName}[@data-testid='${testId}']`;
2719+
const isCommon = this.isAttributeCommonAcrossLists(
2720+
element,
2721+
"data-testid",
2722+
testId,
2723+
otherListElements
2724+
);
2725+
if (isCommon) {
2726+
return `${tagName}[@data-testid='${testId}']`;
2727+
}
27312728
}
27322729

27332730
if (element.id && !element.id.match(/^\d/) && !addPositionToAll) {
2734-
return `${tagName}[@id='${element.id}']`;
2731+
const isCommon = this.isAttributeCommonAcrossLists(
2732+
element,
2733+
"id",
2734+
element.id,
2735+
otherListElements
2736+
);
2737+
if (isCommon) {
2738+
return `${tagName}[@id='${element.id}']`;
2739+
}
27352740
}
27362741

27372742
if (!addPositionToAll) {
@@ -2742,7 +2747,15 @@ class ClientSelectorGenerator {
27422747
attr.name !== "data-mx-id" &&
27432748
attr.value
27442749
) {
2745-
return `${tagName}[@${attr.name}='${attr.value}']`;
2750+
const isCommon = this.isAttributeCommonAcrossLists(
2751+
element,
2752+
attr.name,
2753+
attr.value,
2754+
otherListElements
2755+
);
2756+
if (isCommon) {
2757+
return `${tagName}[@${attr.name}='${attr.value}']`;
2758+
}
27462759
}
27472760
}
27482761
}
@@ -2906,12 +2919,70 @@ class ClientSelectorGenerator {
29062919
const result = pathParts.length > 0 ? "/" + pathParts.join("/") : null;
29072920

29082921
this.pathCache.set(targetElement, result);
2909-
2922+
29102923
return result;
29112924
}
29122925

2926+
private isAttributeCommonAcrossLists(
2927+
targetElement: HTMLElement,
2928+
attrName: string,
2929+
attrValue: string,
2930+
otherListElements: HTMLElement[]
2931+
): boolean {
2932+
if (otherListElements.length === 0) {
2933+
return true;
2934+
}
2935+
2936+
const targetPath = this.getElementPath(targetElement);
2937+
2938+
for (const otherListElement of otherListElements) {
2939+
const correspondingElement = this.findCorrespondingElement(
2940+
otherListElement,
2941+
targetPath
2942+
);
2943+
if (correspondingElement) {
2944+
const otherValue = correspondingElement.getAttribute(attrName);
2945+
if (otherValue !== attrValue) {
2946+
return false;
2947+
}
2948+
}
2949+
}
2950+
2951+
return true;
2952+
}
2953+
2954+
private getElementPath(element: HTMLElement): number[] {
2955+
const path: number[] = [];
2956+
let current: HTMLElement | null = element;
2957+
2958+
while (current && current.parentElement) {
2959+
const siblings = Array.from(current.parentElement.children);
2960+
path.unshift(siblings.indexOf(current));
2961+
current = current.parentElement;
2962+
}
2963+
2964+
return path;
2965+
}
2966+
2967+
private findCorrespondingElement(
2968+
rootElement: HTMLElement,
2969+
path: number[]
2970+
): HTMLElement | null {
2971+
let current: HTMLElement = rootElement;
2972+
2973+
for (const index of path) {
2974+
const children = Array.from(current.children);
2975+
if (index >= children.length) {
2976+
return null;
2977+
}
2978+
current = children[index] as HTMLElement;
2979+
}
2980+
2981+
return current;
2982+
}
2983+
29132984
private getCommonClassesAcrossLists(
2914-
targetElement: HTMLElement,
2985+
targetElement: HTMLElement,
29152986
otherListElements: HTMLElement[]
29162987
): string[] {
29172988
if (otherListElements.length === 0) {
@@ -3919,9 +3990,48 @@ class ClientSelectorGenerator {
39193990
);
39203991
if (!deepestElement) return null;
39213992

3993+
if (!this.isMeaningfulElementCached(deepestElement)) {
3994+
const atomicChild = this.findAtomicChildAtPoint(deepestElement, x, y);
3995+
if (atomicChild) {
3996+
return atomicChild;
3997+
}
3998+
}
3999+
39224000
return deepestElement;
39234001
}
39244002

4003+
private findAtomicChildAtPoint(
4004+
parent: HTMLElement,
4005+
x: number,
4006+
y: number
4007+
): HTMLElement | null {
4008+
const stack: HTMLElement[] = [parent];
4009+
const visited = new Set<HTMLElement>();
4010+
4011+
while (stack.length > 0) {
4012+
const element = stack.pop()!;
4013+
if (visited.has(element)) continue;
4014+
visited.add(element);
4015+
4016+
if (element !== parent && this.isMeaningfulElementCached(element)) {
4017+
const rect = element.getBoundingClientRect();
4018+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
4019+
return element;
4020+
}
4021+
}
4022+
4023+
for (let i = element.children.length - 1; i >= 0; i--) {
4024+
const child = element.children[i] as HTMLElement;
4025+
const rect = child.getBoundingClientRect();
4026+
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
4027+
stack.push(child);
4028+
}
4029+
}
4030+
}
4031+
4032+
return null;
4033+
}
4034+
39254035
/**
39264036
* Helper methods used by the unified getDeepestElementFromPoint
39274037
*/

0 commit comments

Comments
 (0)