Skip to content

Conversation

amhsirak
Copy link
Member

@amhsirak amhsirak commented Aug 23, 2025

closes #738

Summary by CodeRabbit

  • New Features

    • Selector generation now provides primary and fallback selectors, including for group containers and highlighter.
    • Dialog/modal-aware selection prioritizes meaningful content with improved shadow DOM/frame handling.
  • Refactor

    • Extensive caching, batching, and basic spatial indexing for faster, more deterministic selector generation.
    • Rewritten descendant gathering and XPath generation with strict limits and optimized paths.
  • Chores

    • Added debug logging and resilience guards.
  • Note

    • Public API updated: selector methods now return objects with primary and fallback selectors.

Copy link

coderabbitai bot commented Aug 23, 2025

Walkthrough

Adds caching, dialog/modal-aware element selection, optimized descendant/XPath generation, spatial indexing, and explicit primary/fallback selector computation. Public APIs now return both primary and fallback selectors, and highlighter data includes a fallback. Cleanup clears new caches. Logging and guards added for shadow DOM and frames.

Changes

Cohort / File(s) Summary
Selector generation and APIs
src/helpers/clientSelectorGenerator.ts
Introduces multiple caches; dialog/modal prioritization and helpers; rewritten descendant collection with limits; optimized structural/child XPath generation; spatial indexing utilities; deterministic matching helpers; explicit primary/fallback selector construction; public API return types updated; highlighter data extended; cleanup updated; added guards/logging.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant UI as Caller
  participant CSG as ClientSelectorGenerator
  participant DOM as iframeDocument
  participant Cache as Caches
  participant HL as Highlighter

  UI->>CSG: generateSelector(doc, coords, action)
  CSG->>DOM: elementsFromPoint / dialog-aware deepest lookup
  CSG->>Cache: check selector/meaningful/path caches
  CSG->>DOM: collect meaningful descendants (limits, shadow)
  CSG->>CSG: buildOptimizedAbsoluteXPath -> primary, fallback
  CSG-->>UI: { primary, fallback }

  UI->>CSG: generateDataForHighlighter(coords, doc, isDOMMode)
  CSG->>DOM: resolve element(s) (dialog-first if present)
  CSG->>CSG: compute selector + fallback
  CSG-->>HL: { rect, selector, fallbackSelector, elementInfo }
  note right of HL: Highlighter can use fallback if primary fails
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Assessment against linked issues

Objective Addressed Explanation
Modal capture list not highlighting (#738)

Possibly related PRs

Suggested labels

Type: Enhancement, Scope: Recorder

Poem

In moonlit modals, I hop and peer,
A primary path, a fallback near.
Through shadow roots I softly tread,
Caches warm my whiskered head.
Highlight bright, no more delay—
Selector trails to light the way.
(thump-thump)

Tip

🔌 Remote MCP (Model Context Protocol) integration is now available!

Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats.

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch modal-highlight

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

@amhsirak amhsirak requested a review from RohitR311 August 23, 2025 09:47
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/helpers/clientSelectorGenerator.ts (4)

2613-2616: Bug: overlay-controls filter condition is always truthy

Operator precedence makes !hoveredElement.closest(...) != null always true. Use a direct null check.

Apply this diff:

-      if (
-        hoveredElement != null &&
-        !hoveredElement.closest("#overlay-controls") != null
-      ) {
+      if (
+        hoveredElement != null &&
+        hoveredElement.closest("#overlay-controls") === null
+      ) {

2837-2927: Attribute quoting in XPath step generation

For attributes like role/type/name/src/aria-label and data-* below, direct interpolation can break XPath on quotes. Use quoteXPathLiteral.

Apply this diff:

-      for (const attrName of meaningfulAttrs) {
-        if (element.hasAttribute(attrName)) {
-          const value = element.getAttribute(attrName)!.replace(/'/g, "\\'");
-          return `${tagName}[@${attrName}='${value}']`;
-        }
-      }
+      for (const attrName of meaningfulAttrs) {
+        if (element.hasAttribute(attrName)) {
+          const value = element.getAttribute(attrName)!;
+          return `${tagName}[@${attrName}=${this.quoteXPathLiteral(value)}]`;
+        }
+      }
...
-      for (const attr of Array.from(element.attributes)) {
+      for (const attr of Array.from(element.attributes)) {
         if (
           attr.name.startsWith("data-") &&
           attr.name !== "data-testid" &&
           attr.name !== "data-mx-id" &&
           attr.value
         ) {
-          return `${tagName}[@${attr.name}='${attr.value}']`;
+          return `${tagName}[@${attr.name}=${this.quoteXPathLiteral(attr.value)}]`;
         }
       }

3602-3623: Choose the correct list container for target

buildTargetXPath assumes parentElements[0] contains target. Use findListElementForTarget() first.

Apply this diff:

-      const parentElements = this.evaluateXPath(listSelector, document);
-      const containingParent = parentElements[0];
+      const parentElements = this.evaluateXPath(listSelector, document);
+      const containingParent =
+        this.findListElementForTarget(targetElement, listSelector, document) ||
+        parentElements[0];

4536-4611: Refactor clientSelectorGenerator usage to handle new { primary, fallback } return shape

The recent change to generateSelector in src/helpers/clientSelectorGenerator.ts now returns an object { primary: string | null; fallback: string | null } instead of a plain string. All call sites still treat its return value as a single string, which will break at runtime. You must update these consumers to:

  • Destructure the result and choose the appropriate selector (e.g. primary || fallback).
  • Update any truthiness checks (if (selector)) to inspect primary (or both).
  • Pass the correct string into events or APIs instead of the whole object.

Specifically, in src/components/recorder/DOMBrowserRenderer.tsx around lines 443–450, 473–480 and 631–637:

- const selector = clientSelectorGenerator.generateSelector(
-   iframeDoc,
-   { x: iframeX, y: iframeY },
-   ActionType.Click
- );
+ const { primary, fallback } = clientSelectorGenerator.generateSelector(
+   iframeDoc,
+   { x: iframeX, y: iframeY },
+   ActionType.Click
+ );
+ const selector = primary || fallback;

Then update subsequent checks and the payload:

- if (selector && socket) {
+ if (selector && socket) {
    socket.emit("dom:click", {
-     selector,
+     selector,
      …
    });

Repeat this pattern for all three call sites in that file.

Additionally, note that there are multiple ActionType definitions in the repo (client helper enum, React context union, server/types). To avoid confusion and accidental import collisions, consider aliasing the client export:

import { ActionType as SelectorActionType, clientSelectorGenerator } from "../../helpers/clientSelectorGenerator";

—and update references to SelectorActionType.Click accordingly.

🧹 Nitpick comments (5)
src/helpers/clientSelectorGenerator.ts (5)

512-526: Dialog prioritization is helpful; consider narrowing the scan

findAllDialogElements traverses the entire DOM on each new analysis. It's gated by getList/listSelector, but still heavy on large pages. Consider early-filtering with a targeted query ("dialog,[role='dialog'],.modal,.dialog,.popup,.overlay") and only if elementsFromPoint() intersects a likely overlay. This would avoid scanning “*” repeatedly.


663-697: Include textarea in “fast meaningful” set

Fast path omits textarea. Add it to avoid missing obvious actionable inputs.

Apply this diff:

-    if (["a", "img", "input", "button", "select"].includes(tagName)) {
+    if (["a", "img", "input", "button", "select", "textarea"].includes(tagName)) {
       return true;
     }

1047-1049: Remove noisy console logs or behind a debug flag

These logs will spam the console on every mouse move. Remove or guard via an instance-level debug flag.

Apply this diff:

-    console.log("Elements at point:", elementsAtPoint);
-    console.log("Grouped elements:", this.groupedElements);
+    // debug: uncomment if needed
+    // console.debug("Elements at point:", elementsAtPoint, "grouped:", this.groupedElements);

2645-2653: Selector caching looks good; consider making parent limit configurable

Cache key includes document URL which is enough. The hard-coded limit of 10 parent elements could be part of performanceConfig for tuning.

Also applies to: 2665-2668


3232-3233: Gate console.time/timeEnd behind a debug flag

These timers will clutter consoles in production. Suggest guarding with a debug toggle.

Also applies to: 3270-3271

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between ca29967 and addc71e.

📒 Files selected for processing (1)
  • src/helpers/clientSelectorGenerator.ts (47 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/helpers/clientSelectorGenerator.ts (3)
maxun-core/src/browserSide/scraper.js (9)
  • parentElement (914-914)
  • pathParts (569-573)
  • current (1010-1010)
  • tagName (450-450)
  • position (1114-1114)
  • fallbackSelector (1117-1120)
  • MAX_DEPTH (955-955)
  • doc (225-225)
  • shadowRoot (992-992)
server/src/types/index.ts (2)
  • Selectors (147-159)
  • Action (278-288)
src/context/browserActions.tsx (1)
  • ActionType (9-9)
🔇 Additional comments (8)
src/helpers/clientSelectorGenerator.ts (8)

112-116: Good caching additions; verified cleanup clears them

The new WeakMaps/Maps are appropriate, and cleanup() resets them. Nice.


184-224: XPath fallback: quote attribute values safely (single quotes break current output)

You interpolate data-mx-id into XPath using single quotes. Values containing a single quote (or odd characters) will generate invalid XPath. Move to a robust literal quoting helper and use it here; also consider downgrading console.warn to debug to avoid log spam.
[submit_essential_refactor]
Apply this diff:

-      if (childMxId) {
-        return `//${parentTagName}[@data-mx-id='${parentMxId}']//${childTagName}[@data-mx-id='${childMxId}']`;
-      } else {
+      const parentPredicate = `@data-mx-id=${this.quoteXPathLiteral(parentMxId)}`;
+      if (childMxId) {
+        const childPredicate = `@data-mx-id=${this.quoteXPathLiteral(childMxId)}`;
+        return `//${parentTagName}[${parentPredicate}]//${childTagName}[${childPredicate}]`;
+      } else {
         const pathElements = this.getMandatoryFallbackPath(
           childElement,
           parentElement
         );
         if (pathElements.length > 0) {
-          const parentPath = `//${parentTagName}[@data-mx-id='${parentMxId}']`;
+          const parentPath = `//${parentTagName}[${parentPredicate}]`;
           const childPath = pathElements.join("/");
           return `${parentPath}/${childPath}`;
         }
       }

Add these helpers once in the class (outside the touched ranges):

// Safely embed an arbitrary string as an XPath literal
private quoteXPathLiteral(value: string): string {
  if (value.indexOf("'") === -1) return `'${value}'`;
  const parts = value.split("'");
  return `concat(${parts.map(p => `'${p}'`).join(`, "'", `)})`;
}

// Minimal escaping for CSS attribute values inside double quotes
private escapeCssAttrValue(value: string): string {
  return value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
}

616-616: Depth guard is prudent

Depth limit in getMeaningfulChildren prevents runaway recursion. Good.


646-658: Meaningfulness caching LGTM

WeakMap cache is appropriate and correctly populated.


1807-1810: Good: exclude data-mx-id from general attribute-based CSS generation

Prevents primary selectors from leaning on internal IDs reserved for fallbacks.


2703-2787: Descendant BFS with shadow support + caps looks solid

Nice balance between breadth, depth, and caps; caching meaningfulness avoids redundant work. Good.


4180-4201: Dialog prioritization: smart guard to ensure pointer is inside the dialog

The click-area check prevents selecting backdrop overlays. Nice improvement.


4529-4533: Cleanup extended to new caches

Good attention to memory safety; all caches cleared.

Comment on lines +229 to +256
private getMandatoryFallbackPath(
targetElement: HTMLElement,
rootElement: HTMLElement
): string[] {
const pathParts: string[] = [];
let current = targetElement;

while (current && current !== rootElement && current.parentElement) {
const mxId = current.getAttribute("data-mx-id");
const tagName = current.tagName.toLowerCase();

if (mxId) {
// Always prefer data-mx-id when available
pathParts.unshift(`${tagName}[@data-mx-id='${mxId}']`);
} else {
// Use position as fallback when data-mx-id is not available
const position =
Array.from(current.parentElement.children)
.filter((child) => child.tagName === current.tagName)
.indexOf(current) + 1;
pathParts.unshift(`${tagName}[${position}]`);
}

current = current.parentElement;
}

return pathParts;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Path builder: same XPath quoting issue as above

When mxId is present, use the quoting helper; current string interpolation will break on single quotes.

Apply this diff:

-      if (mxId) {
-        // Always prefer data-mx-id when available
-        pathParts.unshift(`${tagName}[@data-mx-id='${mxId}']`);
+      if (mxId) {
+        // Always prefer data-mx-id when available
+        pathParts.unshift(`${tagName}[@data-mx-id=${this.quoteXPathLiteral(mxId)}]`);
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private getMandatoryFallbackPath(
targetElement: HTMLElement,
rootElement: HTMLElement
): string[] {
const pathParts: string[] = [];
let current = targetElement;
while (current && current !== rootElement && current.parentElement) {
const mxId = current.getAttribute("data-mx-id");
const tagName = current.tagName.toLowerCase();
if (mxId) {
// Always prefer data-mx-id when available
pathParts.unshift(`${tagName}[@data-mx-id='${mxId}']`);
} else {
// Use position as fallback when data-mx-id is not available
const position =
Array.from(current.parentElement.children)
.filter((child) => child.tagName === current.tagName)
.indexOf(current) + 1;
pathParts.unshift(`${tagName}[${position}]`);
}
current = current.parentElement;
}
return pathParts;
}
private getMandatoryFallbackPath(
targetElement: HTMLElement,
rootElement: HTMLElement
): string[] {
const pathParts: string[] = [];
let current = targetElement;
while (current && current !== rootElement && current.parentElement) {
const mxId = current.getAttribute("data-mx-id");
const tagName = current.tagName.toLowerCase();
if (mxId) {
// Always prefer data-mx-id when available
pathParts.unshift(`${tagName}[@data-mx-id=${this.quoteXPathLiteral(mxId)}]`);
} else {
// Use position as fallback when data-mx-id is not available
const position =
Array.from(current.parentElement.children)
.filter((child) => child.tagName === current.tagName)
.indexOf(current) + 1;
pathParts.unshift(`${tagName}[${position}]`);
}
current = current.parentElement;
}
return pathParts;
}
🤖 Prompt for AI Agents
In src/helpers/clientSelectorGenerator.ts around lines 229 to 256, the XPath
fragment built when data-mx-id is present interpolates the raw mxId inside
single quotes which will break if mxId contains single quotes; use the project’s
XPath-quoting helper to escape/wrap the mxId value (e.g., produce a properly
quoted string like "'...'" or concat(...) as the helper returns) and insert that
quoted value into the selector (producing tagName[@data-mx-id=<quotedValue>]);
if the helper is in another module, import it at the top of the file.

Comment on lines +261 to +289
private generateMandatoryGroupFallback(
elements: HTMLElement[]
): string | null {
try {
if (!elements || elements.length === 0) return null;

const tagName = elements[0].tagName.toLowerCase();

const mxIds = elements
.map((el) => el.getAttribute("data-mx-id"))
.filter(Boolean);

if (mxIds.length > 0) {
if (mxIds.length === 1) {
return `//${tagName}[@data-mx-id='${mxIds[0]}']`;
} else {
const mxIdConditions = mxIds
.map((id) => `@data-mx-id='${id}'`)
.join(" or ");
return `//${tagName}[${mxIdConditions}]`;
}
}

return null;
} catch (error) {
console.error("Error generating mandatory group fallback:", error);
return null;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Group fallback XPath: quote each mx-id safely

Same XPath quoting concern; also guard ids typed as strings.

Apply this diff:

-      if (mxIds.length > 0) {
+      if (mxIds.length > 0) {
         if (mxIds.length === 1) {
-          return `//${tagName}[@data-mx-id='${mxIds[0]}']`;
+          return `//${tagName}[@data-mx-id=${this.quoteXPathLiteral(String(mxIds[0]))}]`;
         } else {
-          const mxIdConditions = mxIds
-            .map((id) => `@data-mx-id='${id}'`)
+          const mxIdConditions = mxIds
+            .map((id) => `@data-mx-id=${this.quoteXPathLiteral(String(id))}`)
             .join(" or ");
           return `//${tagName}[${mxIdConditions}]`;
         }
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private generateMandatoryGroupFallback(
elements: HTMLElement[]
): string | null {
try {
if (!elements || elements.length === 0) return null;
const tagName = elements[0].tagName.toLowerCase();
const mxIds = elements
.map((el) => el.getAttribute("data-mx-id"))
.filter(Boolean);
if (mxIds.length > 0) {
if (mxIds.length === 1) {
return `//${tagName}[@data-mx-id='${mxIds[0]}']`;
} else {
const mxIdConditions = mxIds
.map((id) => `@data-mx-id='${id}'`)
.join(" or ");
return `//${tagName}[${mxIdConditions}]`;
}
}
return null;
} catch (error) {
console.error("Error generating mandatory group fallback:", error);
return null;
}
}
private generateMandatoryGroupFallback(
elements: HTMLElement[]
): string | null {
try {
if (!elements || elements.length === 0) return null;
const tagName = elements[0].tagName.toLowerCase();
const mxIds = elements
.map((el) => el.getAttribute("data-mx-id"))
.filter(Boolean);
if (mxIds.length > 0) {
if (mxIds.length === 1) {
return `//${tagName}[@data-mx-id=${this.quoteXPathLiteral(String(mxIds[0]))}]`;
} else {
const mxIdConditions = mxIds
.map((id) => `@data-mx-id=${this.quoteXPathLiteral(String(id))}`)
.join(" or ");
return `//${tagName}[${mxIdConditions}]`;
}
}
return null;
} catch (error) {
console.error("Error generating mandatory group fallback:", error);
return null;
}
}
🤖 Prompt for AI Agents
In src/helpers/clientSelectorGenerator.ts around lines 261 to 289, the XPath
builder uses raw mx-id values which can break selectors when IDs contain quotes
and doesn't ensure IDs are strings; change the code to coerce each id to a
string (e.g. String(id)) and use a helper that returns a safe XPath literal: if
the id contains no single quotes wrap in single quotes, else if it contains no
double quotes wrap in double quotes, else build an XPath concat(...) expression
to represent the value; use that helper when constructing each @data-mx-id='...'
clause so all mx-id conditions are safely quoted and robust.

Comment on lines +1559 to +1576
/**
* Generate CSS fallback selector using data-mx-id attribute
*/
private generateMandatoryCSSFallback(element: HTMLElement): string | null {
try {
const mxId = element.getAttribute("data-mx-id");
if (!mxId) {
console.warn("Element missing data-mx-id attribute for CSS fallback");
return null;
}

const tagName = element.tagName.toLowerCase();
return `${tagName}[data-mx-id='${mxId}']`;
} catch (error) {
console.error("Error generating mandatory CSS fallback:", error);
return null;
}
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

CSS fallback: escape attribute values properly

data-mx-id values containing double quotes will break the selector. Use the CSS escaping helper with double-quoted attribute values.

Apply this diff:

-  private generateMandatoryCSSFallback(element: HTMLElement): string | null {
+  private generateMandatoryCSSFallback(element: HTMLElement): string | null {
     try {
       const mxId = element.getAttribute("data-mx-id");
       if (!mxId) {
         console.warn("Element missing data-mx-id attribute for CSS fallback");
         return null;
       }

       const tagName = element.tagName.toLowerCase();
-      return `${tagName}[data-mx-id='${mxId}']`;
+      return `${tagName}[data-mx-id="${this.escapeCssAttrValue(mxId)}"]`;
     } catch (error) {
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/**
* Generate CSS fallback selector using data-mx-id attribute
*/
private generateMandatoryCSSFallback(element: HTMLElement): string | null {
try {
const mxId = element.getAttribute("data-mx-id");
if (!mxId) {
console.warn("Element missing data-mx-id attribute for CSS fallback");
return null;
}
const tagName = element.tagName.toLowerCase();
return `${tagName}[data-mx-id='${mxId}']`;
} catch (error) {
console.error("Error generating mandatory CSS fallback:", error);
return null;
}
}
/**
* Generate CSS fallback selector using data-mx-id attribute
*/
private generateMandatoryCSSFallback(element: HTMLElement): string | null {
try {
const mxId = element.getAttribute("data-mx-id");
if (!mxId) {
console.warn("Element missing data-mx-id attribute for CSS fallback");
return null;
}
const tagName = element.tagName.toLowerCase();
return `${tagName}[data-mx-id="${this.escapeCssAttrValue(mxId)}"]`;
} catch (error) {
console.error("Error generating mandatory CSS fallback:", error);
return null;
}
}
🤖 Prompt for AI Agents
In src/helpers/clientSelectorGenerator.ts around lines 1559 to 1576, the current
generateMandatoryCSSFallback builds an attribute selector using raw mxId which
breaks when the value contains quotes; update the method to properly escape the
attribute value and use double-quoted attribute syntax: call the standard
CSS.escape(mxId) (or a local polyfill if CSS.escape is unavailable) and return
`${tagName}[data-mx-id="${CSS.escape(mxId)}"]`, keeping the null checks and
error handling intact.

Comment on lines +3027 to +3030
if (this.pathCache.has(targetElement)) {
return this.pathCache.get(targetElement)!;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Cache key bug: pathCache ignores rootElement, causing cross-root mis-hits

getOptimizedStructuralPath caches by targetElement only, but the path depends on rootElement and even otherListElements. This can return a wrong path on subsequent calls with a different root. Cache per (target, root).

Apply this diff:

-  private pathCache = new WeakMap<HTMLElement, string | null>();
+  private pathCache = new WeakMap<HTMLElement, Map<HTMLElement, string | null>>();
-    if (this.pathCache.has(targetElement)) {
-      return this.pathCache.get(targetElement)!;
-    }
+    const rootMap = this.pathCache.get(targetElement);
+    if (rootMap && rootMap.has(rootElement)) {
+      return rootMap.get(rootElement)!;
+    }
-    if (current !== rootElement) {
-      this.pathCache.set(targetElement, null);
+    if (current !== rootElement) {
+      let map = this.pathCache.get(targetElement);
+      if (!map) {
+        map = new Map<HTMLElement, string | null>();
+        this.pathCache.set(targetElement, map);
+      }
+      map.set(rootElement, null);
       return null;
     }
-    const result = pathParts.length > 0 ? "/" + pathParts.join("/") : null;
-
-    this.pathCache.set(targetElement, result);
-    
-    return result;
+    const result = pathParts.length > 0 ? "/" + pathParts.join("/") : null;
+    let map = this.pathCache.get(targetElement);
+    if (!map) {
+      map = new Map<HTMLElement, string | null>();
+      this.pathCache.set(targetElement, map);
+    }
+    map.set(rootElement, result);
+    return result;

Also update the reset:

-      this.pathCache = new WeakMap<HTMLElement, string | null>();
+      this.pathCache = new WeakMap<HTMLElement, Map<HTMLElement, string | null>>();

(lines 2651-2653 and in cleanup at 4530-4533)

Also applies to: 3082-3091

🤖 Prompt for AI Agents
In src/helpers/clientSelectorGenerator.ts around lines 3027-3030, the pathCache
currently keys only by targetElement causing incorrect cache hits across
different rootElement contexts; change the cache key to include both
targetElement and rootElement (and otherListElements if relevant) when storing
and retrieving paths so entries are unique per (target, root) combination.
Update all related accesses (also at ~3082-3091) to compute and use the
composite key consistently. Additionally update cache reset/cleanup code at
lines ~2651-2653 and ~4530-4533 to clear entries keyed by the composite key or
to rebuild the cache appropriately when a root changes. Ensure the composite key
generation is deterministic and avoids memory leaks (e.g., use a string/id
serialization or a nested Map keyed by root then target).

Comment on lines +3098 to 3116
if (otherListElements.length === 0) {
return this.normalizeClasses(targetElement.classList).split(" ").filter(Boolean);
}

const targetClasses = this.normalizeClasses(targetElement.classList).split(" ").filter(Boolean);

// Fast path for elements with no classes
if (targetClasses.length === 0) {
return [];
}

const otherListsKey = otherListElements.map(el => `${el.tagName}-${el.className}`).sort().join('|');
const cacheKey = `${targetElement.tagName}-${targetClasses.sort().join(',')}-${otherListsKey}`;
// Create optimized cache key
const cacheKey = `${targetElement.tagName}_${targetClasses.join(',')}_${otherListElements.length}`;

// Check cache first
if (this.classCache.has(cacheKey)) {
return this.classCache.get(cacheKey)!;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

classCache key is lossy; avoid caching when otherListElements is non-empty

The cache key uses only tagName, target classes, and the count of otherListElements. Different sets with the same count can collide, returning wrong classes. Easiest fix: bypass cache entirely when otherListElements.length > 0.

Apply this diff:

-    const cacheKey = `${targetElement.tagName}_${targetClasses.join(',')}_${otherListElements.length}`;
+    const cacheKey = `${targetElement.tagName}_${targetClasses.join(',')}_${otherListElements.length}`;
...
-    if (this.classCache.has(cacheKey)) {
+    if (otherListElements.length === 0 && this.classCache.has(cacheKey)) {
       return this.classCache.get(cacheKey)!;
     }
...
-      this.classCache.set(cacheKey, targetClasses);
+      if (otherListElements.length === 0) this.classCache.set(cacheKey, targetClasses);
...
-      this.classCache.set(cacheKey, targetClasses);
+      if (otherListElements.length === 0) this.classCache.set(cacheKey, targetClasses);
...
-    this.classCache.set(cacheKey, commonClasses);
+    if (otherListElements.length === 0) this.classCache.set(cacheKey, commonClasses);

Also applies to: 3135-3176

Comment on lines +4045 to 4111
private generateGroupContainerSelector(group: ElementGroup): {
primary: string;
fallback: string | null;
} {
const { elements } = group;

if (!elements || elements.length === 0) return "";

// 1. Tag name (ensure all tags match first)
const tagName = elements[0].tagName.toLowerCase();
if (!elements.every((el) => el.tagName.toLowerCase() === tagName)) {
throw new Error("Inconsistent tag names in group.");
if (!elements || elements.length === 0) {
return { primary: "", fallback: null };
}

let xpath = `//${tagName}`;
const predicates: string[] = [];
// ALWAYS generate primary XPath
let primary = "";
try {
const tagName = elements[0].tagName.toLowerCase();
if (!elements.every((el) => el.tagName.toLowerCase() === tagName)) {
throw new Error("Inconsistent tag names in group.");
}

// 2. Get common classes
const commonClasses = this.getCommonStrings(
elements.map((el) =>
(el.getAttribute("class") || "").split(/\s+/).filter(Boolean)
)
);
if (commonClasses.length > 0) {
predicates.push(
...commonClasses.map((cls) => `contains(@class, '${cls}')`)
let xpath = `//${tagName}`;
const predicates: string[] = [];

// Get common classes
const commonClasses = this.getCommonStrings(
elements.map((el) =>
(el.getAttribute("class") || "").split(/\s+/).filter(Boolean)
)
);
}
if (commonClasses.length > 0) {
predicates.push(
...commonClasses.map((cls) => `contains(@class, '${cls}')`)
);
}

// 3. Get common attributes (excluding id, style, dynamic ones)
const commonAttributes = this.getCommonAttributes(elements, [
"id",
"style",
"class"
]);
for (const [attr, value] of Object.entries(commonAttributes)) {
predicates.push(`@${attr}='${value}'`);
}
// Get common attributes (excluding id, style, data-mx-id, class)
const commonAttributes = this.getCommonAttributes(elements, [
"id",
"style",
"data-mx-id",
"class",
]);
for (const [attr, value] of Object.entries(commonAttributes)) {
predicates.push(`@${attr}='${value}'`);
}

// 4. Optional: Common child count
const childrenCountSet = new Set(elements.map((el) => el.children.length));
if (childrenCountSet.size === 1) {
predicates.push(`count(*)=${[...childrenCountSet][0]}`);
}
// Optional: Common child count
const childrenCountSet = new Set(
elements.map((el) => el.children.length)
);
if (childrenCountSet.size === 1) {
predicates.push(`count(*)=${[...childrenCountSet][0]}`);
}

// 5. Build XPath
if (predicates.length > 0) {
xpath += `[${predicates.join(" and ")}]`;
if (predicates.length > 0) {
xpath += `[${predicates.join(" and ")}]`;
}

primary = xpath;
} catch (error) {
console.warn("Primary group XPath generation failed:", error);
const tagName = elements[0].tagName.toLowerCase();
primary = `//${tagName}`;
}

return xpath;
const fallback = this.generateMandatoryGroupFallback(elements);

return { primary, fallback };
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

Group selector: quote class and attribute values in XPath

contains(@Class, '...') and @attr='...' both need robust quoting. Use quoteXPathLiteral.

Apply this diff:

-      if (commonClasses.length > 0) {
-        predicates.push(
-          ...commonClasses.map((cls) => `contains(@class, '${cls}')`)
-        );
+      if (commonClasses.length > 0) {
+        predicates.push(
+          ...commonClasses.map((cls) => `contains(@class, ${this.quoteXPathLiteral(cls)})`)
+        );
       }
...
-      for (const [attr, value] of Object.entries(commonAttributes)) {
-        predicates.push(`@${attr}='${value}'`);
+      for (const [attr, value] of Object.entries(commonAttributes)) {
+        predicates.push(`@${attr}=${this.quoteXPathLiteral(value)}`);
       }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
private generateGroupContainerSelector(group: ElementGroup): {
primary: string;
fallback: string | null;
} {
const { elements } = group;
if (!elements || elements.length === 0) return "";
// 1. Tag name (ensure all tags match first)
const tagName = elements[0].tagName.toLowerCase();
if (!elements.every((el) => el.tagName.toLowerCase() === tagName)) {
throw new Error("Inconsistent tag names in group.");
if (!elements || elements.length === 0) {
return { primary: "", fallback: null };
}
let xpath = `//${tagName}`;
const predicates: string[] = [];
// ALWAYS generate primary XPath
let primary = "";
try {
const tagName = elements[0].tagName.toLowerCase();
if (!elements.every((el) => el.tagName.toLowerCase() === tagName)) {
throw new Error("Inconsistent tag names in group.");
}
// 2. Get common classes
const commonClasses = this.getCommonStrings(
elements.map((el) =>
(el.getAttribute("class") || "").split(/\s+/).filter(Boolean)
)
);
if (commonClasses.length > 0) {
predicates.push(
...commonClasses.map((cls) => `contains(@class, '${cls}')`)
let xpath = `//${tagName}`;
const predicates: string[] = [];
// Get common classes
const commonClasses = this.getCommonStrings(
elements.map((el) =>
(el.getAttribute("class") || "").split(/\s+/).filter(Boolean)
)
);
}
if (commonClasses.length > 0) {
predicates.push(
...commonClasses.map((cls) => `contains(@class, '${cls}')`)
);
}
// 3. Get common attributes (excluding id, style, dynamic ones)
const commonAttributes = this.getCommonAttributes(elements, [
"id",
"style",
"class"
]);
for (const [attr, value] of Object.entries(commonAttributes)) {
predicates.push(`@${attr}='${value}'`);
}
// Get common attributes (excluding id, style, data-mx-id, class)
const commonAttributes = this.getCommonAttributes(elements, [
"id",
"style",
"data-mx-id",
"class",
]);
for (const [attr, value] of Object.entries(commonAttributes)) {
predicates.push(`@${attr}='${value}'`);
}
// 4. Optional: Common child count
const childrenCountSet = new Set(elements.map((el) => el.children.length));
if (childrenCountSet.size === 1) {
predicates.push(`count(*)=${[...childrenCountSet][0]}`);
}
// Optional: Common child count
const childrenCountSet = new Set(
elements.map((el) => el.children.length)
);
if (childrenCountSet.size === 1) {
predicates.push(`count(*)=${[...childrenCountSet][0]}`);
}
// 5. Build XPath
if (predicates.length > 0) {
xpath += `[${predicates.join(" and ")}]`;
if (predicates.length > 0) {
xpath += `[${predicates.join(" and ")}]`;
}
primary = xpath;
} catch (error) {
console.warn("Primary group XPath generation failed:", error);
const tagName = elements[0].tagName.toLowerCase();
primary = `//${tagName}`;
}
return xpath;
const fallback = this.generateMandatoryGroupFallback(elements);
return { primary, fallback };
}
private generateGroupContainerSelector(group: ElementGroup): {
primary: string;
fallback: string | null;
} {
const { elements } = group;
if (!elements || elements.length === 0) {
return { primary: "", fallback: null };
}
// ALWAYS generate primary XPath
let primary = "";
try {
const tagName = elements[0].tagName.toLowerCase();
if (!elements.every((el) => el.tagName.toLowerCase() === tagName)) {
throw new Error("Inconsistent tag names in group.");
}
let xpath = `//${tagName}`;
const predicates: string[] = [];
// Get common classes
const commonClasses = this.getCommonStrings(
elements.map((el) =>
(el.getAttribute("class") || "").split(/\s+/).filter(Boolean)
)
);
if (commonClasses.length > 0) {
predicates.push(
...commonClasses.map((cls) =>
// Use quoteXPathLiteral for robust quoting
`contains(@class, ${this.quoteXPathLiteral(cls)})`
)
);
}
// Get common attributes (excluding id, style, data-mx-id, class)
const commonAttributes = this.getCommonAttributes(elements, [
"id",
"style",
"data-mx-id",
"class",
]);
for (const [attr, value] of Object.entries(commonAttributes)) {
// Use quoteXPathLiteral here as well
predicates.push(`@${attr}=${this.quoteXPathLiteral(value)}`);
}
// Optional: Common child count
const childrenCountSet = new Set(
elements.map((el) => el.children.length)
);
if (childrenCountSet.size === 1) {
predicates.push(`count(*)=${[...childrenCountSet][0]}`);
}
if (predicates.length > 0) {
xpath += `[${predicates.join(" and ")}]`;
}
primary = xpath;
} catch (error) {
console.warn("Primary group XPath generation failed:", error);
const tagName = elements[0].tagName.toLowerCase();
primary = `//${tagName}`;
}
const fallback = this.generateMandatoryGroupFallback(elements);
return { primary, fallback };
}
🤖 Prompt for AI Agents
In src/helpers/clientSelectorGenerator.ts around lines 4045 to 4111, the XPath
predicates currently interpolate class and attribute values with single quotes
which can break for values containing quotes; update predicates to use
quoteXPathLiteral for safe quoting. Replace contains(@class, '${cls}') with
contains(@class, ${quoteXPathLiteral(cls)}) when mapping commonClasses, and
replace @${attr}='${value}' with @${attr}=${quoteXPathLiteral(value)} when
adding commonAttributes; ensure quoteXPathLiteral is imported/available in this
module and use it in both places so all string literals in XPath predicates are
robustly quoted.

@amhsirak
Copy link
Member Author

@RohitR311 sync cloud changes here

@amhsirak amhsirak added Type: Bug Something isn't working Scope: Recorder All issues/PRs related to recorder labels Aug 23, 2025
@amhsirak
Copy link
Member Author

closing as covered in #751

@amhsirak amhsirak closed this Aug 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Scope: Recorder All issues/PRs related to recorder Type: Bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Modal capture list not highlighting

1 participant