A robust, agent-friendly JavaScript library for identifying DOM elements by type and/or text content, with full support for shadow DOM, iframes, and automation workflows.
Inject the library and find elements in any browser context (Selenium, Playwright, Puppeteer, or browser console):
// Find all visible buttons
const results = ElementFinder.findElement('button')
// Find a button by text (substring match)
const results = ElementFinder.findElement('button', 'Submit')
// Find by text only (any type)
const results = ElementFinder.findElement(null, 'seleniumbase')
// Find in all frames (default)
const results = ElementFinder.findElement('button')
// Highlight found elements
ElementFinder.highlight(results.elements.map((e) => e.element))
// Remove highlight
ElementFinder.unhighlight(results.elements.map((e) => e.element))Agent/Automation Best Practices:
- Always check
frameIndex:-1= main frame,0+= iframe (see below for iframe handling) - For iframe results, switch context before interacting (see Selenium/Playwright docs)
- Use
getValidTypes()to enumerate all supported semantic types - Use
getSearchableAttributes()to see which attributes are searched for text
- Type-based element finding: Find elements by semantic type (button, textbox, link, dropdown, etc.)
- Text content search: Search within element text, attributes, and placeholders
- Shadow DOM support: Automatically traverses shadow roots to find nested elements
- Iframe support: Automatically searches all frames (main document + iframes) by default
- Visibility filtering: Optionally include or exclude hidden elements
- Bounding box data: Returns position and dimensions for each found element
- XPath-like type definitions: Extensible element type matching using XPath-like expressions
- Optimized performance: O(n) innermost element filtering and efficient Set-based lookups
npm install @nodebug/browser-element-finderbrowser-element-finder/
├── index.js # Browser-injected library (generated)
├── build.js # Build script to generate index.js
├── src/
│ ├── element-finder.js # Canonical source (ES module)
│ ├── element-definitions.json # XPath-like type definitions
│ └── searchable-attributes.json # Attributes searched for text matching
├── tests/
│ ├── unit/ # Unit tests
│ └── integration/ # Integration tests with HTML fixtures
└── coverage/ # Test coverage reports
// Find all visible buttons
const results = ElementFinder.findElement('button')
// Find by text
const results = ElementFinder.findElement('button', 'Submit')
// Find by text only
const results = ElementFinder.findElement(null, 'seleniumbase')
// Include hidden elements
const results = ElementFinder.findElement('button', null, false, true)The library automatically searches all frames (main + iframes). For agent/automation use:
- Main frame:
item.frameIndex === -1anditem.elementis available for direct interaction. - Iframe:
item.frameIndex >= 0anditem.elementisundefined. UseframeIndexto switch context, then re-runfindElementinside the iframe to get interactable elements.
Example:
const results = ElementFinder.findElement('button')
for (const item of results.elements) {
if (item.frameIndex === -1 && item.element) {
// Interact directly
item.element.click()
} else if (item.frameIndex >= 0) {
// Switch to iframe, then re-query
// (agent/driver-specific code here)
}
}import {
findElement,
highlight,
unhighlight,
getValidTypes,
} from '@nodebug/browser-element-finder/src/element-finder.js'
// Find elements (requires DOM environment)
const results = findElement('button', 'Submit')
// Access metadata from results
results.elements.forEach((item) => {
console.log('Tag:', item.tagName)
console.log('Position:', item.boundingBox.x, item.boundingBox.y)
})
// Highlight elements (extract DOM elements from wrapper objects)
highlight(results.elements.map((e) => e.element))const {
findElement,
highlight,
unhighlight,
getValidTypes,
} = require('@nodebug/browser-element-finder/src/element-finder.js')The package exports JSON files containing element type definitions and searchable attributes:
// ESM - Import JSON directly
import ELEMENT_DEFINITIONS from '@nodebug/browser-element-finder/element-definitions.json' assert { type: 'json' }
import SEARCHABLE_ATTRIBUTES from '@nodebug/browser-element-finder/searchable-attributes.json' assert { type: 'json' }
// Get all valid element types
console.log(Object.keys(ELEMENT_DEFINITIONS)) // ['button', 'checkbox', 'dropdown', ...]
// Get searchable attributes
console.log(SEARCHABLE_ATTRIBUTES) // ['placeholder', 'value', 'data-test-id', ...]// CommonJS - Use require
const ELEMENT_DEFINITIONS = require('@nodebug/browser-element-finder/element-definitions.json')
const SEARCHABLE_ATTRIBUTES = require('@nodebug/browser-element-finder/searchable-attributes.json')| Function | Description |
|---|---|
findElement(type, text, exact, includeHidden, parent) |
Find elements by type/text, returns { elements: [...] } |
highlight(elements, color, width) |
Highlight elements with outline |
unhighlight(elements) |
Remove highlight |
getValidTypes() |
List all supported element types |
getBoundingBox(element) |
Get bounding box for an element |
setSearchableAttributes(attributes) |
Set custom attributes for text search |
getSearchableAttributes() |
Get current searchable attributes |
matchesType(el, type) |
Check if element matches a type |
matchesContent(el, value, exact) |
Check if element matches text |
getAllElements(root) |
Get all elements (with shadow DOM) |
getAllFrames(root, maxFrames) |
Get all frames (main + iframes) |
parseXPath(expr, el, depth) |
Parse XPath-like type expressions |
splitByOperator(expr, op) |
Split XPath by operator |
findElement(type, text, exact, includeHidden, parent)
Finds elements matching the specified criteria. Searches all frames (main document + iframes) by default.
| Parameter | Type | Default | Description |
|---|---|---|---|
type |
string |
"element" |
Element type (see supported types below). Must be a string; throws TypeError for non-string values. |
text |
string |
null |
Text to search for in content/attributes |
exact |
boolean |
false |
Exact text match vs substring |
includeHidden |
boolean |
false |
Include hidden elements |
parent |
Element |
null |
Parent element to search within |
Returns: { elements: [{ element, boundingBox, tagName, frameIndex }] }
element: Raw DOM element (main frame only; for iframes, useframeIndexand re-query after switching context)frameIndex:-1for main frame,0, 1, 2...for iframes
Agent/Automation Note: Iframe elements cannot be interacted with directly. Use frameIndex to switch context, then re-run findElement inside the iframe.
Highlights elements on the page with a colored outline.
| Parameter | Type | Default | Description |
|---|---|---|---|
elements |
Array |
- | Elements to highlight |
color |
string |
'red' |
Outline color |
width |
number |
3 |
Outline width in pixels |
Removes highlighting from elements.
Returns an array of all valid element type names.
Returns the bounding box for an element.
Sets custom searchable attributes.
Returns the current searchable attributes array.
Checks if an element matches the specified type definition.
Checks if an element matches the specified text content. Safely handles edge case elements that may throw errors on attribute access.
Gets all elements including shadow DOM contents.
Gets all frames (main document + iframes) in the window. Returns array with frameIndex (-1 for main, 0+ for iframes). Cross-origin iframes (SecurityError) are automatically skipped with a specific warning message, while other errors are logged separately.
Returns the current configuration object.
Parses XPath-like expressions for element type matching. The depth parameter is used internally for recursion tracking and has a maximum limit of 100 to prevent stack overflow from deeply nested expressions.
Splits XPath expressions by operator (and/or).
The library automatically searches all frames (main document + iframes) by default. However, there are important limitations when working with iframe elements:
const results = ElementFinder.findElement('button')
results.elements.forEach((item) => {
if (item.frameIndex === -1) {
// Main frame element - can interact directly
console.log('Main frame element:', item.element)
item.element.click() // Works
} else {
// Iframe element - element property is undefined
console.log('Iframe element at frameIndex:', item.frameIndex)
console.log('Bounding box:', item.boundingBox)
// item.element is undefined - cannot interact directly
}
})To interact with elements inside an iframe, you must switch the Selenium driver context:
// Find iframe elements
const results = await driver.executeScript(`
return ElementFinder.findElement('button');
`)
// Switch to iframe and interact
const iframeElements = results.elements.filter((e) => e.frameIndex >= 0)
if (iframeElements.length > 0) {
// Switch to the iframe (frameIndex 0 = first iframe)
await driver.switchTo().frame(iframeElements[0].frameIndex)
// Now find and interact with elements in the iframe
const iframeResults = await driver.executeScript(`
return ElementFinder.findElement('button');
`)
// These elements will have the element property since we're in the iframe context
}| Type | Description |
|---|---|
button |
<button>, [role="button"], [type="button"] |
checkbox |
<input type="checkbox">, [role="checkbox"] |
switch |
Toggle switches, checkboxes with switch role |
slider |
<input type="range">, [role="slider"] |
radio |
<input type="radio">, [role="radio"] |
dropdown |
<select>, [role="combobox"], [role="listbox"] |
textbox |
<input>, <textarea>, [role="textbox"] |
link |
<a>, [role="link"], [href] |
heading |
<h1>-<h6>, [role="heading"] |
navigation |
<nav>, [role="navigation"] |
list |
<ul>, <ol>, [role="list"] |
listitem |
<li>, [role="listitem"] |
menu |
<menu>, [role="menu"] |
menuitem |
[role="menuitem"] |
toolbar |
[role="toolbar"] |
dialog |
[role="dialog"] |
table |
<table>, [role="table"] |
row |
<tr>, [role="row"] |
column |
<td>, <th>, [role="cell"] |
image |
<img>, [role="img"] |
file |
<input type="file"] |
element |
Matches all elements |
By default, the library searches these attributes (in priority order):
placeholder,value,data-test-id,data-testid,idresource-id,name,aria-label,class,hinttitle,tooltip,alt,src,aria-labelledby
The library is optimized for large DOM trees with efficient algorithms:
- Innermost element filtering: O(n) algorithm using Set-based lookups instead of O(n²) nested loops
- Column expansion: O(n) algorithm using Map-based column position lookups instead of O(n²) cell iteration
- Memory efficient: Minimal object creation during traversal
For pages with many matching elements, these optimizations significantly reduce search time.
# Run all tests
npm test
# Run tests in watch mode
npm run test:watch
# Run tests with coverage
npm run test:coverageNote: The tests/integration/helpers/ folder is excluded from vitest runs as it contains helper utilities r
npm run lintThe library includes a Node.js-compatible module (src/element-finder.js) that provides the same functionality as the browser-injected index.js for coverage testing. This module is fully covered by unit tests.
The original index.js is browser-injected code executed via Selenium's executeScript. Coverage for browser-injected code requires browser-based tools like Istanbul or running tests in a browser environment.
MIT