Skip to content

support iframe and nested iframes. use playwright instead of patchright - #57

Merged
Dingway98 merged 8 commits into
mainfrom
support-iframe
Nov 6, 2025
Merged

support iframe and nested iframes. use playwright instead of patchright#57
Dingway98 merged 8 commits into
mainfrom
support-iframe

Conversation

@Dingway98

@Dingway98 Dingway98 commented Nov 4, 2025

Copy link
Copy Markdown
Contributor

Note

Replaces Patchright with Playwright Core and overhauls a11y DOM to reliably handle same-origin/OOPIF iframes, with improved element resolution, scrolling, and debug data.

  • Core/Infra:
    • Migrate all browser types and imports from patchright to playwright-core.
    • Add playwright-core dependency.
  • A11y DOM (multi-frame/iframe support):
    • Build frame-aware maps (frameMap, accessibleNameMap) from DOM; tag elements by frameIndex.
    • Fetch and merge AX trees for main frame, same-origin iframes, and OOPIFs (via per-frame CDP sessions), with DOM fallbacks when AX lacks interactives.
    • Match IframeInfo to Playwright Frames for reliable locator resolution; return combined elements, xpathMap, and optional frameDebugInfo.
    • Detect and mark scrollable containers; expose utilities for Page/Frame.
  • Agent:
    • aiAction element finding uses new a11y state; retries; writes enriched debug artifacts (including frame info).
    • Locator resolution supports iframes via frameMap and frame-scoped XPath; improved scrollTo behavior.
    • Stricter extract output checks; quieter MCP logs under debug.
  • Browser Providers:
    • Update Hyperbrowser/local providers to Playwright Chromium (connectOverCDP/launch).
  • Utilities/Messages:
    • Update waitForSettledDOM, DOM/context providers, and message builders to Playwright types.
  • Example Script:
    • Replace free-form ai call with stepwise aiAction flow for Google Flights demo.

Written by Cursor Bugbot for commit 80282da. This will update automatically on new commits. Configure here.

cursor[bot]

This comment was marked as outdated.

encodedId = createEncodedId(frameIndex, node.backendDOMNodeId);
console.log(
` ✗ No match for frame ${frameIndex}, using fallback encodedId="${encodedId}"`
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Unconditional Console Log Causes Production Spam

The console.log statement for iframe detection in buildBackendIdMaps runs unconditionally. This can lead to console spam in production environments, as it bypasses the debug parameter.

Additional Locations (1)

Fix in Cursor Fix in Web

@NikhilShahi

Copy link
Copy Markdown
Contributor

@claude
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns

Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.

        Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.

1 similar comment
@NikhilShahi

Copy link
Copy Markdown
Contributor

@claude
Please review this pull request and provide feedback on:
- Code quality and best practices
- Potential bugs or issues
- Performance considerations
- Security concerns

Use the repository's CLAUDE.md for guidance on style and conventions. Be constructive and helpful in your feedback.

        Use `gh pr comment` with your Bash tool to leave your review as a comment on the PR.

@claude

claude Bot commented Nov 4, 2025

Copy link
Copy Markdown

Claude finished @NikhilShahi's task —— View job


Pull Request Code Review: iframe Support & Playwright Migration

I've completed a comprehensive review of this PR. Below is my detailed feedback on code quality, potential bugs, performance, and security considerations.


Critical Issues

1. Out-of-Bounds Frame Index Bug (Confirmed)

Location: src/agent/index.ts:1006-1027

The existing review correctly identifies a critical bug. The findFrameByXPath method iterates through ALL frames returned by page.frames() but attempts to find a match by checking if each frame's iframe element matches the XPath in the parent document. However, there's no bounds checking or validation that the matched frame actually corresponds to the expected frameIndex.

Issue: If multiple iframes have similar structure or the frame collection order changes, the wrong frame could be matched, leading to element targeting failures.

Recommendation: Add validation to ensure the matched frame's index corresponds to the expected frameIndex from the frameMap.

2. Unconditional Console Logging (Confirmed)

Locations:

  • src/context-providers/a11y-dom/build-tree.ts:96-120
  • src/context-providers/a11y-dom/build-maps.ts:153

Multiple console.log statements execute unconditionally, bypassing the debug parameter. This will cause production log spam.

Examples:

// build-tree.ts:96
console.log(
  `[buildHierarchicalTree] Frame ${frameIndex}: backendNodeId=${node.backendDOMNodeId} not in DOM map...`
);

// build-maps.ts:153
console.log(
  `[DOM] Iframe detected: frameIndex=${iframeFrameIndex}...`
);

Recommendation: Guard all console.log statements with if (debug) checks.


Major Issues

3. Potential Memory Leaks in Frame Resolution

Location: src/agent/index.ts:892-928

The findFrameByXPath method evaluates JavaScript in every frame on every lookup. For pages with many iframes, this could cause performance degradation and memory pressure.

Issue:

for (const frame of allFrames) {
  const iframeElement = await frame.frameElement();
  const matches = await parentFrame.evaluate(...); // Evaluates in parent for each frame
}

Recommendation: Consider caching frame resolutions or building a frame index once rather than searching on every element lookup.

4. Race Condition in Frame Loading

Location: src/agent/index.ts:1045-1055

The code attempts to wait for iframe content with a timeout, but continues execution if the wait fails:

try {
  await targetFrame.waitForLoadState("domcontentloaded", { timeout: 5000 });
} catch {
  // Continue anyway - frame might already be loaded
}

Issue: This could lead to element targeting failures if the frame isn't actually loaded yet. The catch block silently swallows all errors without differentiating between "already loaded" and "failed to load" scenarios.

Recommendation: Check the frame's load state before attempting to wait, or add better error handling to distinguish between timeout and actual errors.

5. Missing Error Handling in CDP Session

Location: src/context-providers/a11y-dom/index.ts:234-305

The CDP session is created but error handling during accessibility tree fetching is limited:

const client = await page.context().newCDPSession(page);
try {
  await client.send("Accessibility.enable");
  // ... operations
} finally {
  await client.detach();
}

Issue: If client.detach() fails in the finally block, it could leave CDP sessions open, potentially causing resource leaks.

Recommendation: Wrap client.detach() in a try-catch to ensure it doesn't throw.


Code Quality Issues

6. Inconsistent Frame Index Typing

Location: Multiple files

Frame indices are inconsistently typed as number in some places and parsed from strings in others:

// build-maps.ts:990
const [frameIndexStr] = encodedId.split("-");
const frameIndex = parseInt(frameIndexStr!, 10);

// agent/index.ts:1114
const frameIndex = id.split("-")[0];

Issue: The second example doesn't parse to number, keeping it as a string, which could cause bugs in comparisons.

Recommendation: Create a utility function to parse encoded IDs consistently and return typed results.

7. Large Configuration Constants at Class Level

Location: src/agent/index.ts:47-55

Configuration constants are defined as static readonly, but they're never modified or overridden:

private static readonly AIACTION_CONFIG = {
  MAX_RETRIES: 10,
  RETRY_DELAY_MS: 1000,
  // ...
};

Issue: This makes configuration inflexible. Users cannot adjust retry behavior without modifying code.

Recommendation: Consider moving these to constructor parameters or a separate configuration object that can be passed in.

8. Debug Info Collection Always Runs

Location: src/context-providers/a11y-dom/build-maps.ts:38-42

Debug-related maps are created conditionally but the logic is verbose:

const domNodeCounts = debug ? new Map<number, number>() : null;
const inputElementsByFrame = debug ? new Map<number, number>() : null;

Issue: Every check for these maps requires null-checking: if (debug && domNodeCounts).

Recommendation: Consider a cleaner pattern, such as always creating the maps but only using/logging them when debug is true.


Performance Considerations

9. Excessive DOM Retries

Location: src/agent/index.ts:468-620

The findElementWithRetry method retries up to 10 times with 1-second delays, waiting for DOM to settle and refetching the entire accessibility tree each time:

for (let attempt = 0; attempt < maxRetries; attempt++) {
  await waitForSettledDOM(page);
  domState = await getA11yDOM(page, this.debug);
  // ... check if element found
  if (attempt < maxRetries - 1) {
    await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
  }
}

Issue: For pages with large DOM trees and many iframes, fetching the full accessibility tree 10 times could add 10+ seconds of latency per action.

Recommendation:

  • Reduce default retries to 3-5
  • Implement exponential backoff
  • Consider incremental DOM updates rather than full refetches

10. Synchronous Frame Tree Traversal

Location: src/agent/index.ts:948-961

The getParentFrame method recursively resolves parent frames synchronously, which could be slow for deeply nested iframes:

private async getParentFrame(
  page: Page,
  parentFrameIndex: number,
  frameMap: Map<number, IframeInfo>
): Promise<ReturnType<Page["frames"]>[number] | null> {
  // Recursively get grandparent frame
  const grandparentFrame = await this.getParentFrame(
    page,
    parentInfo.parentFrameIndex,
    frameMap
  );
  // ...
}

Issue: Each recursive call awaits the previous one. For 5 levels of nesting, this is 5 sequential async operations.

Recommendation: Build the full frame path iteratively rather than recursively, or cache resolved frames.


Security Considerations

11. No Validation of Frame Content Origin

Location: src/context-providers/a11y-dom/index.ts:56-114

The code fetches accessibility trees from all iframes without checking their origin:

for (const [frameIndex, frameInfo] of maps.frameMap?.entries() ?? []) {
  const result = await client.send("Accessibility.getPartialAXTree", {
    backendNodeId: contentDocumentBackendNodeId,
    fetchRelatives: true,
  });
}

Issue: Cross-origin iframes may not allow CDP access, potentially causing errors or exposing security boundaries.

Recommendation: Add try-catch for cross-origin access errors and log warnings when frames cannot be accessed due to security restrictions.

12. XPath Injection Risk

Location: src/agent/index.ts:986

XPath strings from the map are used directly in locators:

const xpath = rawXpath.replace(/\/text\(\)(\[\d+\])?$/iu, "");
return { locator: page.locator(`xpath=${xpath}`), xpath };

Issue: If the xpath map is ever populated from untrusted sources, this could lead to XPath injection.

Recommendation: While this appears safe in current usage (xpaths are generated internally), add a comment documenting that xpaths must never come from user input.


Best Practices & Suggestions

13. Magic Numbers in Code

Location: Multiple locations

Various magic numbers appear without explanation:

// build-tree.ts:190
const sampleNodes = debugFrame.totalNodes <= 15 ? debugFrame.rawNodes.slice(0, 15) : undefined;

// agent/index.ts:1047
timeout: 5000  // Why 5 seconds?

Recommendation: Extract to named constants with comments explaining the rationale.

14. Incomplete Error Messages

Location: src/agent/index.ts:1030

Error messages could be more helpful:

throw new HyperagentError(
  `Frame not found for element ${elementId} using XPath: ${iframeInfo.xpath}`,
  404
);

Recommendation: Include more context in error messages, such as the parent frame index, available frames, and whether this is a same-origin or cross-origin frame.

15. Missing TypeScript Strict Checks

Location: Multiple files

Several places use non-null assertions (!) that could be avoided with better type guards:

// agent/index.ts:990
const frameIndex = parseInt(frameIndexStr!, 10);

// build-tree.ts:210
path: joinStep(path, segments[i]!),

Recommendation: Add runtime checks or refactor to avoid non-null assertions where possible.


Positive Aspects

  1. Comprehensive iframe support: The implementation handles nested iframes correctly using frame indices and parent relationships.
  2. Good debugging infrastructure: The frame debug info collection is thorough and will help diagnose issues.
  3. Fallback mechanisms: DOM fallback nodes when accessibility tree is incomplete shows good defensive programming.
  4. Clean separation: The frame resolution logic is well separated into helper methods.
  5. Playwright migration: Clean migration from Patchright to Playwright with minimal breaking changes.

Summary

This PR adds substantial new functionality for iframe support, but contains several critical bugs that should be fixed before merging:

Must Fix:

  1. Frame index out-of-bounds bug (review comment Better click action impl #1)
  2. Unconditional console.log spam (review comment [FEATURE] Add HyperPage #2)
  3. Race condition in frame loading
  4. Memory leak potential in CDP session cleanup

Should Fix:
5. Performance issues with excessive retries and frame resolution
6. Inconsistent frame index parsing
7. Missing cross-origin frame error handling

Nice to Have:
8. Configurable retry behavior
9. Better error messages
10. Reduced magic numbers

Overall, the architecture is sound and the implementation is comprehensive. Once the critical bugs are addressed, this will be a solid addition to the codebase.


Comment thread scripts/test-page-ai.ts
Comment thread scripts/test-page-ai.ts
const page = await agent.newPage();
page.ai(
"Go to https://flights.google.com and find a round-trip flight from Rio de Janeiro to Los Angeles, leaving on November 11, 2025, and returning on November 22, 2025, and select the option with the least carbon dioxide emissions."
page.goto("https://flights.google.com");

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Async navigation mis-timed due to missing await

Missing await keyword before page.goto(). The goto method is asynchronous and returns a Promise. Without await, the subsequent aiAction calls will execute before the page has finished navigating to the URL, causing the actions to fail or execute on the wrong page. This should be await page.goto("https://flights.google.com");.

Fix in Cursor Fix in Web

Comment thread src/agent/index.ts Outdated
grandparentFrame,
parentInfo.xpath,
page.frames()
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Fix OOPIF frame resolution by bypassing XPath lookup

The getParentFrame method doesn't handle OOPIF (Out-of-Process iframe) frames correctly. For OOPIF frames, the xpath field is set to an empty string (as seen in src/context-providers/a11y-dom/index.ts:312), but the method tries to use findFrameByXPath with this empty xpath at line 968-971, which will fail to match any iframe element. The method should check if parentInfo.playwrightFrame exists and return it directly for OOPIF frames, instead of trying to use XPath-based lookup. This will cause frame resolution to fail for elements inside OOPIF frames.

Fix in Cursor Fix in Web

Comment thread src/context-providers/a11y-dom/index.ts Outdated

// Update nextFrameIndex to account for any nested frames we just merged
// This prevents frameIndex collisions when processing subsequent OOPIF frames
nextFrameIndex = Math.max(nextFrameIndex, ...Array.from(maps.frameMap?.keys() || [0])) + 1;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Bug: Incorrect Fallback Causes Frame Index Skipping

Incorrect fallback value in nextFrameIndex calculation causes frame index skipping. When maps.frameMap is undefined, the expression maps.frameMap?.keys() || [0] evaluates to [0], which incorrectly includes frame index 0 in the Math.max calculation. This can cause frame indices to be skipped. For example, if nextFrameIndex is 2 and maps.frameMap is undefined, the calculation becomes Math.max(2, 0) + 1 = 3, skipping frame index 2. The fallback should be an empty array [] instead of [0], or use nullish coalescing: maps.frameMap?.keys() ?? [].

Fix in Cursor Fix in Web

@Dingway98

Copy link
Copy Markdown
Contributor Author

@claude
Please review this pull request and provide feedback on:

  • Code quality and best practices
  • Potential bugs or issues
  • Performance considerations
  • Security concerns

@claude

claude Bot commented Nov 5, 2025

Copy link
Copy Markdown

Claude finished @Dingway98's task —— View job


Pull Request Code Review: iframe Support & Playwright Migration

I've completed a comprehensive review of this PR. Below is my detailed feedback on code quality, potential bugs, performance, and security considerations.


Summary

This PR introduces substantial improvements for iframe support (both same-origin and OOPIF) and migrates from Patchright to Playwright Core. The architecture is sound with frame-aware accessibility tree building, but several critical bugs and performance issues need addressing before merge.


Critical Issues

1. Missing await in test script causes race condition

Location: scripts/test-page-ai.ts:29

page.goto("https://flights.google.com");  // Missing await!
await page.aiAction("click source location box");

Issue: The goto() call is missing await, causing subsequent aiAction calls to execute before the page finishes loading. This is a critical bug that will cause test failures and unreliable behavior.

Fix: Change to await page.goto("https://flights.google.com");


2. Frame index calculation bug causes collisions

Location: src/context-providers/a11y-dom/index.ts:470

nextFrameIndex = Math.max(nextFrameIndex, ...Array.from(maps.frameMap?.keys() || [0])) + 1;

Issue: The fallback value [0] is incorrect. When frameMap is undefined, this evaluates to Math.max(nextFrameIndex, 0) + 1, which can skip frame indices. For example, if nextFrameIndex is 2 and frameMap is undefined, this becomes max(2, 0) + 1 = 3, skipping index 2.

Fix: Use empty array as fallback: ...Array.from(maps.frameMap?.keys() ?? [])


3. Unconditional console logging bypasses debug flag

Locations:

  • src/context-providers/a11y-dom/build-tree.ts:96-100, 136-139
  • src/context-providers/a11y-dom/build-maps.ts:190-194

Issue: Multiple console.log and console.error statements execute unconditionally, ignoring the debug parameter. This will spam production logs.

Examples:

// build-tree.ts:98
console.log(
  `[buildHierarchicalTree] Frame ${frameIndex}: backendNodeId=${node.backendDOMNodeId} not in DOM map...`
);

// build-tree.ts:137
console.error(
  `[buildHierarchicalTree] ⚠️ DUPLICATE encodedId assignment...`
);

Fix: Guard all logging with if (debug) checks, or use console.warn for genuine warnings that should always appear.


4. CDP session detach error handling missing

Location: src/context-providers/a11y-dom/index.ts:477, 677

Issue: The finally blocks call await client.detach() without error handling. If detachment fails, it could throw and mask the original error, potentially leaving CDP sessions open.

} finally {
  await oopifSession.detach();  // Could throw!
}

Fix: Wrap in try-catch:

} finally {
  try {
    await oopifSession.detach();
  } catch (err) {
    console.warn('[A11y] Failed to detach CDP session:', err);
  }
}

Major Issues

5. Excessive DOM refetch retries hurt performance

Location: src/agent/index.ts:468-620

Issue: The findElementWithRetry method retries up to 10 times with 1-second delays, refetching the entire accessibility tree (including all iframes) on each attempt. For pages with large DOM trees, this can add 10+ seconds of latency per action.

for (let attempt = 0; attempt < maxRetries; attempt++) {
  await waitForSettledDOM(page);
  domState = await getA11yDOM(page, this.debug);  // Full refetch every time!
  // ...
  await new Promise((resolve) => setTimeout(resolve, retryDelayMs));  // 1 second delay
}

Recommendations:

  • Reduce default MAX_RETRIES from 10 to 3-5
  • Implement exponential backoff instead of fixed 1s delay
  • Consider incremental DOM updates instead of full refetches

6. Frame load state race condition

Location: src/agent/index.ts:972-981

Issue: The code waits for iframe load state but silently continues on timeout, which could lead to element targeting failures:

try {
  await targetFrame.waitForLoadState("domcontentloaded", { timeout: 5000 });
} catch {
  // Continue anyway - frame might already be loaded
}

Problem: The catch block doesn't distinguish between "already loaded" and "failed to load" scenarios. If the frame genuinely isn't loaded yet, subsequent locator operations will fail.

Recommendation: Check the frame's load state before waiting:

if (targetFrame.url() === 'about:blank' || !targetFrame.url()) {
  await targetFrame.waitForLoadState("domcontentloaded", { timeout: 5000 });
}

7. Hardcoded configuration prevents customization

Location: src/agent/index.ts:47-55

Issue: Configuration constants are defined as static readonly, making them inflexible:

private static readonly AIACTION_CONFIG = {
  MAX_RETRIES: 10,
  RETRY_DELAY_MS: 1000,
  // ...
};

Problem: Users cannot adjust retry behavior, timeouts, or debug limits without modifying source code.

Recommendation: Move these to constructor parameters or a separate configuration object that can be passed in during agent initialization.


Code Quality Issues

8. Inconsistent frame index parsing

Locations: Multiple files

Frame indices are inconsistently parsed:

// build-maps.ts:927
const frameIndex = parseInt(frameIndexStr!, 10);  // ✓ Parsed to number

// agent/index.ts:1041
const frameIndex = id.split("-")[0];  // ✗ Kept as string!

Issue: The second example doesn't parse to number, which could cause bugs in numeric comparisons or Map lookups.

Recommendation: Create a utility function to parse encoded IDs consistently:

function parseEncodedId(encodedId: EncodedId): { frameIndex: number; nodeId: number } {
  const [frameIndexStr, nodeIdStr] = encodedId.split("-");
  return {
    frameIndex: parseInt(frameIndexStr!, 10),
    nodeId: parseInt(nodeIdStr!, 10)
  };
}

9. Magic numbers without explanation

Locations: Multiple

Various magic numbers appear without explanation:

// build-tree.ts:555
const sampleNodes = debugFrame.totalNodes <= 15 ? debugFrame.rawNodes.slice(0, 15) : undefined;

// agent/index.ts:973
timeout: 5000  // Why 5 seconds?

// agent/index.ts:731
timeout: HyperAgent.AIACTION_CONFIG.CLICK_TIMEOUT,  // Better, but CLICK_TIMEOUT=3500 has no explanation

Recommendation: Extract to named constants with comments explaining the rationale.


10. Unsafe non-null assertions

Locations: Multiple files

Several places use non-null assertions (!) that could be avoided:

// agent/index.ts:928
const frameIndex = parseInt(frameIndexStr!, 10);  // What if frameIndexStr is undefined?

// build-tree.ts:248
path: joinStep(path, segments[i]!),  // What if i is out of bounds?

Recommendation: Add runtime checks or refactor to avoid non-null assertions where possible. TypeScript strict mode would catch many of these.


11. Frame matching could fail silently

Location: src/context-providers/a11y-dom/index.ts:82-172

Issue: The matchPlaywrightFramesToFrameMap function logs warnings when frames can't be matched but doesn't track which frames failed. Later, when accessing playwrightFrame, the code checks for null but doesn't provide good diagnostics.

Recommendation: Return or store a list of unmatched frames for better error reporting:

const unmatchedFrames: number[] = [];
// ... matching logic ...
if (!matchedFrame) {
  unmatchedFrames.push(frameIndex);
  if (debug) console.warn(...);
}
return unmatchedFrames;

Performance Considerations

12. Synchronous frame tree traversal

Location: src/agent/index.ts:948-992 (implied by recursive parent resolution)

Issue: While not visible in current code, if parent frame resolution is implemented recursively (as mentioned in previous review comments), it would be inefficient for deeply nested iframes.

Current code is actually good: The getElementLocator method uses stored playwrightFrame directly instead of recursive resolution, which is efficient. No issue here.


13. Excessive frame iteration in verification

Location: src/context-providers/a11y-dom/index.ts:122-157

Issue: For each frameInfo entry, the code iterates through ALL Playwright frames to find a match:

for (const [frameIndex, frameInfo] of frameMap) {
  for (const pwFrame of allFrames) {  // Nested loop - O(n²)
    // ... verification logic ...
  }
}

Impact: For pages with many iframes (e.g., 50 iframes), this becomes 2,500 iterations.

Recommendation: Build a lookup map first to avoid nested loops, or at least track which frames have been matched to skip them earlier.


Security Considerations

14. No origin validation for OOPIF access

Location: src/context-providers/a11y-dom/index.ts:280-296

Issue: The code attempts to create CDP sessions for all frames without checking if they're cross-origin first:

for (const playwrightFrame of allPlaywrightFrames) {
  try {
    oopifSession = await page.context().newCDPSession(playwrightFrame);
  } catch {
    continue;  // Silently skip - is this safe?
  }
}

Concern: While the try-catch handles failures, the code doesn't distinguish between "cross-origin blocked" vs "other error". This could mask genuine issues.

Recommendation: Add more specific error handling:

} catch (error) {
  if (debug) {
    const errorMsg = error instanceof Error ? error.message : String(error);
    if (errorMsg.includes('cross-origin') || errorMsg.includes('security')) {
      console.log(`[A11y] Skipping cross-origin frame: ${playwrightFrame.url()}`);
    } else {
      console.warn(`[A11y] Unexpected error creating CDP session:`, error);
    }
  }
  continue;
}

15. XPath injection risk (low severity)

Location: src/agent/index.ts:924, 992

Issue: XPath strings from the map are used directly in locators:

const xpath = rawXpath.replace(/\/text\(\)(\[\d+\])?$/iu, "");
return { locator: page.locator(`xpath=${xpath}`), xpath };

Current status: Safe in current implementation (xpaths are generated internally).

Recommendation: Add a comment documenting that xpaths must never come from user input:

// SECURITY: xpaths are generated internally from DOM traversal.
// Never use user-provided xpaths here as they could lead to XPath injection.

Positive Aspects

  1. Excellent iframe architecture: Frame-aware maps with frameIndex tagging provide reliable multi-frame support
  2. OOPIF handling: Proper use of per-frame CDP sessions for cross-origin iframes
  3. Comprehensive debugging: Frame debug info collection helps diagnose issues
  4. Good fallback mechanisms: DOM fallback nodes when accessibility tree lacks interactives
  5. Clean separation: Frame resolution logic well separated into helper methods
  6. Proper cleanup: CDP session detachment in finally blocks (just needs error handling)

Required Changes Before Merge

Must Fix:

  1. ✅ Missing await in test script (scripts/test-page-ai.ts:29)
  2. ✅ Frame index calculation bug (src/context-providers/a11y-dom/index.ts:470)
  3. ✅ Unconditional console logging (multiple files)
  4. ✅ CDP session detach error handling

Should Fix:
5. Excessive retry count and delay (reduce MAX_RETRIES to 3-5, add exponential backoff)
6. Frame load state race condition (check load state before waiting)
7. Hardcoded configuration (make configurable via constructor)

Nice to Have:
8. Consistent frame index parsing utility
9. Named constants for magic numbers
10. Reduce non-null assertions
11. Better cross-origin error messages


Overall Assessment

This is a substantial and well-architected addition to the codebase. The iframe support implementation is comprehensive and handles both same-origin and OOPIF scenarios correctly. However, the PR contains several critical bugs that must be fixed before merging, particularly:

  • The missing await in the test script (will cause immediate test failures)
  • Frame index calculation bug (could cause frame targeting failures)
  • Unconditional logging (will spam production logs)

Once these critical issues are addressed, this will be a solid enhancement to HyperAgent's capabilities.

Recommendation: Request changes to fix critical issues before approving.


@Dingway98
Dingway98 merged commit fe33e12 into main Nov 6, 2025
2 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.

2 participants