Skip to content

Conversation

@seanmcguire12
Copy link
Member

@seanmcguire12 seanmcguire12 commented Dec 17, 2025

why

  • snapshot.ts grew to a large file that was responsible for DOM fetching, Accessibility tree processing, XPath utilities, hit-testing, and snapshot orchestration. there were also a lot of inlined types
  • this made the code hard to understand/read, and hard to unit test

what changed

this PR breaks the file apart into sub modules, and puts types into a single file (types/private/snapshot.ts). The submodules are:

  • capture.ts: orchestrates snapshot building flow, and is now written with composable helpers (buildFrameContext(), tryScopedSnapshot(), buildSessionIndexes(), collectPerFrameMaps(), computeFramePrefixes(), mergeFramesIntoSnapshot()) with detailed JSDoc and inline comments
  • coordinateResolver.ts: holds the resolveXpathForLocation() function which performs coordinate -> XPath hit-testing across nested frames.
  • activeElement.ts: holds the computeActiveElementXpath() function, which resolves the currently focused element’s absolute XPath.
  • domTree.ts: handles DOM.getDocument hydration, CBOR-aware retries, and session-wide DOM indexing.
  • a11yTree.ts: gets the accessibility tree via CDP, applies selector scoping, decorates roles, and formats the actual stringified tree
  • focusSelectors.ts: parses cross-frame XPath/CSS selectors and resolves iframe hops.
  • xpathUtils.ts: shared XPath helpers (absolute path resolution, child segment building, normalization).
  • sessions.ts: encapsulates owner/parent session lookups for frames.
  • treeFormatUtils.ts: pure formatting utilities (outline rendering, diffing, subtree injection, whitespace normalization).

test plan

  • this is not a behavioural change, so existing tests should suffice. will fast follow with more unit tests

Summary by cubic

Split the monolithic a11y snapshot implementation into focused submodules and centralized types to improve readability and unit testability. No behavior changes; addresses Linear STG-1085.

  • Refactors
    • Split snapshot.ts into modules for capture orchestration, a11y tree, DOM hydration/indexing, cross-frame focus selectors, coordinate hit-testing, XPath helpers, session helpers, and formatting.
    • Moved snapshot types to types/private/snapshot.ts and re-exported from types/private/index.ts.
    • Added snapshot barrel exports: captureHybridSnapshot, computeActiveElementXpath, resolveXpathForLocation, diffCombinedTrees.
    • Capture flow is now composed of small helpers with clear JSDoc for easier testing.

Written for commit 81c3d13. Summary will update automatically on new commits.

@changeset-bot
Copy link

changeset-bot bot commented Dec 17, 2025

⚠️ No Changeset found

Latest commit: 81c3d13

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

3 issues found across 13 files

Prompt for AI agents (all 3 issues)

Check if these issues are valid — if so, understand the root cause of each and fix them.


<file name="packages/core/lib/v3/understudy/a11y/snapshot/focusSelectors.ts">

<violation number="1" location="packages/core/lib/v3/understudy/a11y/snapshot/focusSelectors.ts:150">
P2: `absPrefix` is declared as `const` and never updated, unlike the XPath version where it&#39;s built up during iframe traversal. This will always return an empty string, which may cause incorrect XPath prefixes when resolving CSS selectors across iframes.</violation>
</file>

<file name="packages/core/lib/v3/understudy/a11y/snapshot/domTree.ts">

<violation number="1" location="packages/core/lib/v3/understudy/a11y/snapshot/domTree.ts:341">
P2: `findNodeByBackendId` is missing traversal of `contentDocument`, making it inconsistent with other traversal functions like `collectDomTraversalTargets`. This could cause the function to miss nodes inside iframe content documents.</violation>
</file>

<file name="packages/core/lib/v3/understudy/a11y/snapshot/activeElement.ts">

<violation number="1" location="packages/core/lib/v3/understudy/a11y/snapshot/activeElement.ts:125">
P1: Arguments to `prefixXPath` are in wrong order for bottom-up traversal. When walking from focused frame to root, each ancestor iframe path should prefix (not suffix) the accumulated path. This will produce incorrect XPaths for elements in nested iframes.</violation>
</file>

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

.map((s) => s.trim())
.filter(Boolean);
let ctxFrameId = rootId;
const absPrefix = "";
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 17, 2025

Choose a reason for hiding this comment

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

P2: absPrefix is declared as const and never updated, unlike the XPath version where it's built up during iframe traversal. This will always return an empty string, which may cause incorrect XPath prefixes when resolving CSS selectors across iframes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/understudy/a11y/snapshot/focusSelectors.ts, line 150:

<comment>`absPrefix` is declared as `const` and never updated, unlike the XPath version where it&#39;s built up during iframe traversal. This will always return an empty string, which may cause incorrect XPath prefixes when resolving CSS selectors across iframes.</comment>

<file context>
@@ -0,0 +1,292 @@
+    .map((s) =&gt; s.trim())
+    .filter(Boolean);
+  let ctxFrameId = rootId;
+  const absPrefix = &quot;&quot;;
+
+  for (let i = 0; i &lt; Math.max(0, parts.length - 1); i++) {
</file context>
Fix with Cubic

const n = stack.pop()!;
if (n.backendNodeId === backendNodeId) return n;
if (n.children) for (const c of n.children) stack.push(c);
if (n.shadowRoots) for (const s of n.shadowRoots) stack.push(s);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 17, 2025

Choose a reason for hiding this comment

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

P2: findNodeByBackendId is missing traversal of contentDocument, making it inconsistent with other traversal functions like collectDomTraversalTargets. This could cause the function to miss nodes inside iframe content documents.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/understudy/a11y/snapshot/domTree.ts, line 341:

<comment>`findNodeByBackendId` is missing traversal of `contentDocument`, making it inconsistent with other traversal functions like `collectDomTraversalTargets`. This could cause the function to miss nodes inside iframe content documents.</comment>

<file context>
@@ -0,0 +1,344 @@
+    const n = stack.pop()!;
+    if (n.backendNodeId === backendNodeId) return n;
+    if (n.children) for (const c of n.children) stack.push(c);
+    if (n.shadowRoots) for (const s of n.shadowRoots) stack.push(s);
+  }
+  return undefined;
</file context>
Fix with Cubic

}>("DOM.getFrameOwner", { frameId: cur });
if (typeof backendNodeId === "number") {
const xp = await absoluteXPathForBackendNode(parentSess, backendNodeId);
if (xp) prefix = prefix ? prefixXPath(prefix, xp) : normalizeXPath(xp);
Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot Dec 17, 2025

Choose a reason for hiding this comment

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

P1: Arguments to prefixXPath are in wrong order for bottom-up traversal. When walking from focused frame to root, each ancestor iframe path should prefix (not suffix) the accumulated path. This will produce incorrect XPaths for elements in nested iframes.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/core/lib/v3/understudy/a11y/snapshot/activeElement.ts, line 125:

<comment>Arguments to `prefixXPath` are in wrong order for bottom-up traversal. When walking from focused frame to root, each ancestor iframe path should prefix (not suffix) the accumulated path. This will produce incorrect XPaths for elements in nested iframes.</comment>

<file context>
@@ -0,0 +1,134 @@
+      }&gt;(&quot;DOM.getFrameOwner&quot;, { frameId: cur });
+      if (typeof backendNodeId === &quot;number&quot;) {
+        const xp = await absoluteXPathForBackendNode(parentSess, backendNodeId);
+        if (xp) prefix = prefix ? prefixXPath(prefix, xp) : normalizeXPath(xp);
+      }
+    } catch {
</file context>
Fix with Cubic

@seanmcguire12 seanmcguire12 merged commit c632837 into main Dec 18, 2025
46 of 48 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants