How to Debug Element Resolution Failures with the @N Reference System in Ego Lite
To debug element resolution failures with the @N reference system, inspect the RefMap entry for the numeric ID, check whether the ElementResolutionError is transient (retry with fresh snapshot) or permanent (fix locator specificity), and verify the fallback role/name lookup in the Accessibility tree when backendNodeId is stale.
The Ego Lite browser harness uses a numeric reference system (@N) to track UI elements across browser sessions via the RefMap data structure. When automation scripts fail to resolve these references, understanding the distinction between transient DOM changes and permanent locator issues is essential for stable test execution. This guide walks through the resolution pipeline implemented in citrolabs/ego-lite to help you diagnose and fix these failures quickly.
Understanding the @N Reference Resolution Pipeline
The resolution flow in element-resolver.ts follows a strict five-step pipeline when locating elements via numeric references:
- Parse the reference string – The
parseReffunction inref-map.tsstrips the leading@orref=prefix and validates that the remaining string consists only of digits. - Lookup in RefMap – The system calls
RefMap.get(refId)to retrieve the stored entry containingbackendNodeId, ARIArole, accessiblename, and optional frame information. - Attempt cached backendNodeId resolution – If a
backendNodeIdexists, the resolver invokes CDP methods likeDOM.getBoxModelorDOM.resolveNode. If this throws anElementResolutionErrordue to a missing box model (element not rendered), the error is re-thrown for retry logic. If the node is stale (DOM changed), execution silently proceeds to step 4. - Execute role/name fallback – The
findBackendNodeIdByRoleNamefunction traverses the Accessibility tree to locate the element by its stored ARIA role and name. Failure here generates a transientElementResolutionError. - Return geometry or objectId – Upon successful resolution, the system calculates the element center via
boxModelCenteror returns anobjectIdfor subsequent CDP commands.
Distinguishing Transient from Permanent Errors
The resolver categorizes failures into two distinct kinds that dictate your debugging strategy:
Transient Errors occur when:
- The reference ID is unknown (missing from current snapshot)
- The cached
backendNodeIdis stale due to DOM mutations - The element lacks a box model because it is not yet rendered or is zero-sized
Action: Retry the operation. The harness automatically triggers a fresh snapshot and re-attempts resolution.
Permanent Errors occur when:
- A selector matches multiple elements when uniqueness is required
- The selector syntax is invalid
- The AX node lacks a
backendDOMNodeId(non-renderable elements)
Action: Fix the locator to be more specific or adjust your reference usage to target renderable elements only.
Step-by-Step Debugging Workflow
When an @N reference fails, follow this systematic approach to identify the root cause:
Inspect the RefMap Entry
Use RefMap.get(id) to examine the stored data for the failing reference. If backendNodeId is undefined, the system will rely entirely on the role/name fallback.
console.log('RefMap entry for @5:', refMap.get('5'));
// Output: { backendNodeId: 11234, role: 'textbox', name: 'Search', ... }
Check the Error Kind
Catch ElementResolutionError and examine the kind property. Transient errors warrant an automatic retry, while permanent errors require locator modification.
if (e instanceof ElementResolutionError) {
if (e.kind === 'transient') {
await snapshot(); // Force fresh snapshot
} else {
console.error('Permanent error:', e.message);
}
}
Validate the Snapshot
Force a fresh snapshot using await snapshot() (exposed via the helper context) to capture the current DOM state. This is particularly useful when elements load asynchronously.
Confirm Role-Name Pairs
When the fallback logic activates, verify that the element's ARIA role and accessible name match the RefMap entry. The lookup is performed by findBackendNodeIdsByRoleName, which filters the AX tree for matching nodes.
Review Resolver Logs
The resolver provides specific error messages through the ElementResolutionError constructor:
Could not locate element with role=${role} name=${name}Element has no box model (not rendered or zero-sized)
Practical Code Examples
Basic Reference Resolution
// Resolve center coordinates for element @12
const { x, y, sessionId } = await resolveElementCenter(
cdp,
currentSessionId,
refMap,
'@12'
);
If the reference is unknown or stale, this call throws an ElementResolutionError with kind 'transient', triggering automatic retry logic in the harness.
Manual Error Handling
try {
const center = await resolveElementCenter(cdp, sid, refMap, '@7');
console.log('Clicked at', center.x, center.y);
} catch (e) {
if (e instanceof ElementResolutionError) {
if (e.kind === 'transient') {
console.warn('Transient failure – retrying after snapshot');
await snapshot();
// Retry logic here
} else {
console.error('Permanent resolution error:', e.message);
}
} else {
throw e;
}
}
Understanding the Role-Based Fallback
When backendNodeId is missing or stale, the resolver automatically falls back to the stored role/name pair:
// RefMap contains: { role: 'button', name: 'Submit' }
await resolveElementCenter(cdp, sid, refMap, '@3');
// Internally invokes:
// findBackendNodeIdByRoleName(cdp, sid, 'button', 'Submit')
// followed by DOM.getBoxModel on the resolved node
Key Source Files for Debugging
| File | Purpose |
|---|---|
element-resolver.ts |
Core implementation of resolution logic, error handling, and fallback mechanisms |
ref-map.ts |
Stores snapshot-to-reference mappings and implements parseRef |
element-resolver.test.mjs |
Test suite demonstrating success and failure scenarios |
driver/observe.ts |
High-level helper usage showing integration patterns |
Summary
- The @N system relies on RefMap entries containing
backendNodeId, role, and name to locate elements across browser sessions. - Transient errors indicate timing or staleness issues; retry with a fresh snapshot via
snapshot(). - Permanent errors indicate locator ambiguity or invalid selectors; refine your reference criteria.
- Fallback logic traverses the Accessibility tree by role/name when cached node IDs fail.
- Debug via RefMap inspection and
ElementResolutionErrorkind classification to determine appropriate corrective action.
Frequently Asked Questions
What causes a transient ElementResolutionError?
A transient error occurs when the RefMap lacks the requested ID, the cached backendNodeId is stale due to DOM changes, or the element has no box model because it is not yet rendered. According to the source code in element-resolver.ts, these conditions prompt an automatic retry after a fresh snapshot.
How does the fallback mechanism work when backendNodeId is stale?
When CDP methods like DOM.getBoxModel fail due to a stale node ID, the resolver silently invokes findBackendNodeIdByRoleName to search the Accessibility tree using the stored ARIA role and name. If this lookup succeeds, the resolver proceeds with the fresh node; otherwise, it throws a transient error.
What is the difference between @N references and CSS selectors?
The @N system uses numeric references that map to cached backendNodeId values and accessibility metadata in the RefMap, enabling fast re-resolution across page changes. CSS selectors are parsed as locator strings and resolved via querySelector without the caching and fallback layers provided by the reference system.
How can I force a fresh snapshot to resolve a stale reference?
Expose the snapshot() function from your helper context and invoke it manually when catching a transient error. This updates the RefMap with current backendNodeId values and accessibility data, allowing subsequent resolveElementCenter calls to succeed.
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 →