How to Debug Element Resolution Failures in ego-browser: A Complete Guide
Element resolution failures in ego-browser throw ElementResolutionError with a kind property—transient indicates a retryable timing issue while permanent signals an invalid selector or multiple matches, allowing you to pinpoint whether to retry the call or fix the locator syntax.
When automating browser interactions with ego-browser from the citrolabs/ego-lite repository, element lookups can fail due to timing issues, invalid selectors, or ambiguous matches. Understanding the resolution flow implemented in src/element-resolver.ts lets you diagnose whether a failure requires a simple retry or a fundamental fix to your locator strategy. This guide walks through the systematic debugging approach used by the core maintainers, referencing actual source code paths and error classification logic.
Understanding the Core Resolution Architecture
The resolver operates through a layered hierarchy that attempts lookups in a specific order: refs (backend node IDs), role/name combinations via the Accessibility tree, and finally CSS/XPath/text selectors.
Key Resolution Functions
Three primary functions handle element discovery in src/element-resolver.ts:
resolveElementCenter(lines 63-71) – Returns the (x, y) coordinates of an element's center, throwingElementResolutionErrorif the element lacks a box model or cannot be found.resolveElementObjectId(lines 149-155) – Returns a Chrome DevTools Protocol (CDP)objectIdfor element handles, using the same resolution logic as the center helper.matchCountKind(lines 46-50) – Determines whether a "matched N elements" error istransientorpermanentbased on the count.ElementResolutionError(lines 4-10) – The custom error constructor that attaches thekindclassification to every resolution failure.
The Resolution Hierarchy
When you pass a locator to helpers like click() or waitForSelector(), the resolver evaluates it in this strict order:
- Ref lookup – If the selector starts with
@, it queries therefMap(managed insrc/ref-map.ts) for a cached backend node ID. - Role/Name lookup – If the locator uses
loc=role:, it queries the Accessibility tree viaAccessibility.getFullAXTree. - Raw selector evaluation – Falls back to JavaScript execution via
Runtime.evaluate, using snippets generated insrc/locator-query.ts.
Interpreting Error Kinds: Transient vs Permanent
Every ElementResolutionError carries a kind property that dictates your debugging strategy. The classification logic resides in the resolver's error handling paths.
Transient Failures
Transient errors indicate the element may become available later, typically during page load or DOM updates. These occur when:
-
The element has no box model (not rendered or zero-sized) – thrown at lines 55-62 in
element-resolver.ts:if (content.length < 8) { throw new ElementResolutionError( "Element has no box model (not rendered or zero-sized)", "transient", ); } -
The selector matches zero elements but the DOM might still be hydrating.
-
A stale backend node reference exists in the
refMap.
Permanent Failures
Permanent errors indicate the selector is fundamentally incorrect. These trigger when:
-
The selector matches multiple elements consistently. The
matchCountKindfunction (lines 46-50) classifies this:const n = m ? Number(m[1]) : 0; return n > 1 ? "permanent" : "transient"; -
The locator syntax is invalid or uses an unsupported kind.
-
A CSS or XPath selector is malformed.
Step-by-Step Debugging Workflow
Follow this systematic approach to diagnose resolution failures using the source code from citrolabs/ego-lite.
1. Capture and Inspect the Error
Wrap resolution calls in try-catch blocks to expose the error metadata:
import { resolveElementCenter, ElementResolutionError } from "ego-browser";
try {
await resolveElementCenter(cdp, sessionId, refMap, selector);
} catch (e) {
if (e instanceof ElementResolutionError) {
console.error(`Kind: ${e.kind}, Message: ${e.message}`);
}
}
2. Identify the Resolution Stage
Check which lookup path was attempted:
- Ref path: Confirm if your selector starts with
@and exists inrefMap. - Role path: Verify the
loc=role:syntax and checkAccessibility.getFullAXTreeresults. - Raw selector path: Inspect the generated JavaScript in
src/locator-query.ts, specifically thebuildLocatorFindJsfunction.
3. Inspect Underlying CDP Calls
The resolver relies on three critical CDP methods that appear in your debug logs:
DOM.getBoxModel– Fails when elements are not rendered (triggers transient errors).Accessibility.getFullAXTree– Powers role/name lookups.Runtime.evaluate– Executes CSS/XPath/text selectors generated bybuildLocatorFindJs.
4. Determine Retry vs Fix Strategy
Based on the kind property:
transient: Wrap the call in a retry loop. ThewaitForSelectorhelper insrc/helpers.tsautomatically retries transient failures until timeout.permanent: Revise your selector to be more specific or fix syntax errors.
5. Validate Locator Syntax
Use the test suite in src/element-resolver.test.mjs as a reference:
- CSS selectors must be valid for
querySelectorAll. - XPath expressions must work with
document.evaluate. - Role locators must match AX roles with optional name filters (strings, numbers, booleans, or regex).
Common Failure Scenarios and Fixes
Multiple Matches (Permanent)
When a CSS selector matches multiple elements, the resolver throws a permanent error. The test case at element-resolver.test.mjs demonstrates this behavior:
// This throws permanent: "css:.duplicate matched 2 elements"
await resolveElementCenter(cdp, sessionId, refMap, "loc=css:.duplicate");
Fix: Add an nth qualifier or refine the selector to be unique.
Degenerate Box Model (Transient)
When an element exists in the DOM but lacks layout (zero-sized or not rendered), the resolver throws a transient error at element-resolver.ts:55-62. The test suite explicitly verifies this does not fall back to role/name lookup:
await assert.rejects(
() => resolveElementCenter(cdp, undefined, refMap, "@5"),
(error) => {
assert.equal(error.kind, "transient");
assert.match(error.message, /no box model/);
return true;
},
);
Fix: Wait for the element to render or check CSS visibility properties.
Practical Debugging Example
This complete example demonstrates how to instrument resolution calls to capture CDP logs and error metadata:
import { resolveElementCenter, ElementResolutionError } from "ego-browser";
async function debugResolution(selector) {
try {
const pt = await resolveElementCenter(cdp, sessionId, refMap, selector);
console.log("✅ Element centre:", pt);
} catch (e) {
if (e instanceof ElementResolutionError) {
console.log(`❌ ${e.kind.toUpperCase()} failure: ${e.message}`);
// Inspect the CDP call that generated the error
if (cdp.calls) {
console.log("CDP call history:", cdp.calls);
}
} else {
console.error("Unexpected error:", e);
}
}
}
// Debug a problematic selector
debugResolution('loc=css:.duplicate-button');
Running this against a selector matching multiple elements produces:
❌ PERMANENT failure: Locator css:.duplicate-button matched 2 elements
CDP call history: [ [ 'Runtime.evaluate', { expression: '...' }, undefined ] ]
Summary
- Element resolution in ego-browser follows a strict hierarchy: refs → role/name → CSS/XPath/text selectors.
- Error classification via
ElementResolutionError.kinddistinguishes betweentransient(retryable) andpermanent(requires selector fix) failures. - Transient errors typically indicate timing issues or missing box models, resolved by waiting or retrying.
- Permanent errors result from invalid syntax or ambiguous selectors matching multiple elements.
- Source references in
src/element-resolver.ts(lines 46-50, 55-62, 63-71) contain the logic for error classification and resolution paths. - CDP inspection of
DOM.getBoxModel,Accessibility.getFullAXTree, andRuntime.evaluatecalls reveals exactly where lookups fail.
Frequently Asked Questions
What is the difference between transient and permanent element resolution errors?
Transient errors indicate the element might appear if you retry the operation, typically caused by the DOM still loading, stale node references, or elements without a computed box model. Permanent errors mean the selector is fundamentally invalid or matches multiple elements, and retrying will never succeed until you fix the locator syntax or specificity.
How do I debug why my role locator is not finding elements?
Check that the role matches the Accessibility tree entry exactly as exposed by the browser. The resolver uses Accessibility.getFullAXTree and compares against the role and optional name parameters via the axNameMatches function. If the element is not yet in the accessibility tree or the name matcher (string, number, boolean, or regex) is too restrictive, resolution fails with a transient error.
Where does ego-browser generate the JavaScript for CSS and XPath selectors?
The resolver generates browser-side JavaScript snippets in src/locator-query.ts via the buildLocatorFindJs function. When debugging resolution failures, inspect the expression parameter sent to Runtime.evaluate in your CDP logs to see the exact query being executed against the document.
Why does my selector work in browser DevTools but fail in ego-browser?
DevTools often auto-wait for elements or implicitly handle multiple matches, whereas ego-browser strictly enforces single-element resolution. If your selector matches multiple elements, ego-browser throws a permanent error rather than selecting the first match. Add an nth qualifier or refine the selector to be unique, or use a ref-based approach if the element was previously captured.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →