@nodebug/browser-element-finder 1.0.8 → 1.1.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -9,26 +9,35 @@
9
9
  Inject the library and find elements in any browser context (Selenium, Playwright, Puppeteer, or browser console):
10
10
 
11
11
  ```js
12
- // Find all visible buttons
13
- const results = ElementFinder.findElement('button')
12
+ // Find all buttons
13
+ const results = ElementFinder.findElements('button')
14
14
 
15
15
  // Find a button by text (substring match)
16
- const results = ElementFinder.findElement('button', 'Submit')
16
+ const results = ElementFinder.findElements('button', 'Submit')
17
17
 
18
18
  // Find by text only (any type)
19
- const results = ElementFinder.findElement(null, 'seleniumbase')
19
+ const results = ElementFinder.findElements(null, 'seleniumbase')
20
20
 
21
21
  // Find in all frames (default)
22
- const results = ElementFinder.findElement('button')
22
+ const results = ElementFinder.findElements('button')
23
+
24
+ // Find with fallback to nearby elements
25
+ const results = ElementFinder.findProbableElements('button', 'Click Me')
26
+ // Returns button even if "Click Me" is in a nearby label
23
27
 
24
28
  // Highlight found elements
25
29
  ElementFinder.highlight(results.elements.map((e) => e.element))
26
30
 
27
31
  // Remove highlight
28
32
  ElementFinder.unhighlight(results.elements.map((e) => e.element))
33
+
34
+ // Check element properties
35
+ results.elements.forEach((e) => {
36
+ console.log('Tag:', e.tagName, 'Frame:', e.frameIndex)
37
+ })
29
38
  ```
30
39
 
31
- **Agent/Automation Best Practices:**
40
+ **Agent/Automation Best Practices**:
32
41
 
33
42
  - Always check `frameIndex`: `-1` = main frame, `0+` = iframe (see below for iframe handling)
34
43
  - For iframe results, switch context before interacting (see Selenium/Playwright docs)
@@ -43,10 +52,20 @@ ElementFinder.unhighlight(results.elements.map((e) => e.element))
43
52
  - **Text content search**: Search within element text, attributes, and placeholders
44
53
  - **Shadow DOM support**: Automatically traverses shadow roots to find nested elements
45
54
  - **Iframe support**: Automatically searches all frames (main document + iframes) by default
46
- - **Visibility filtering**: Optionally include or exclude hidden elements
55
+ - **Visibility detection**: All elements returned with `isVisible` property (`true`/`false`)
47
56
  - **Bounding box data**: Returns position and dimensions for each found element
48
57
  - **XPath-like type definitions**: Extensible element type matching using XPath-like expressions
49
- - **Optimized performance**: O(n) innermost element filtering and efficient Set-based lookups
58
+ - **Optimized performance**: Pre-compiled type matchers, O(n) innermost element filtering, and efficient Set-based lookups
59
+
60
+ ## Performance Optimizations
61
+
62
+ The library includes several performance improvements:
63
+
64
+ - **Pre-compiled type matchers**: Type definitions are compiled into cached matcher functions at module load time, avoiding XPath re-parsing for every element
65
+ - **O(n) innermost element filtering**: Set-based lookups instead of nested loops
66
+ - **Map-based column expansion**: O(1) element-to-column-position lookups for table cells
67
+ - **Optimized text content matching**: Direct text node iteration instead of expensive textContent calls
68
+ - **Loop optimizations**: Traditional for-loops with cached array lengths for hot paths
50
69
 
51
70
  ## Installation
52
71
 
@@ -58,10 +77,7 @@ npm install @nodebug/browser-element-finder
58
77
 
59
78
  ```
60
79
  browser-element-finder/
61
- ├── index.js # Browser-injected library (generated)
62
- ├── build.js # Build script to generate index.js
63
80
  ├── src/
64
- │ ├── element-finder.js # Canonical source (ES module)
65
81
  │ ├── element-definitions.json # XPath-like type definitions
66
82
  │ └── searchable-attributes.json # Attributes searched for text matching
67
83
  ├── tests/
@@ -75,14 +91,16 @@ browser-element-finder/
75
91
  ### In Browser Console or Automation Script
76
92
 
77
93
  ```js
78
- // Find all visible buttons
94
+ // Find all elements (visible and hidden)
79
95
  const results = ElementFinder.findElement('button')
80
96
  // Find by text
81
97
  const results = ElementFinder.findElement('button', 'Submit')
82
98
  // Find by text only
83
99
  const results = ElementFinder.findElement(null, 'seleniumbase')
84
- // Include hidden elements
85
- const results = ElementFinder.findElement('button', null, false, true)
100
+ // Check visibility of found elements
101
+ results.elements.forEach((e) => {
102
+ console.log('Visible:', e.isVisible)
103
+ })
86
104
  ```
87
105
 
88
106
  ## Working with Iframes (Agent Pattern)
@@ -92,7 +110,7 @@ The library automatically searches all frames (main + iframes). For agent/automa
92
110
  - **Main frame**: `item.frameIndex === -1` and `item.element` is available for direct interaction.
93
111
  - **Iframe**: `item.frameIndex >= 0` and `item.element` is `undefined`. Use `frameIndex` to switch context, then re-run `findElement` inside the iframe to get interactable elements.
94
112
 
95
- **Example:**
113
+ **Example**:
96
114
 
97
115
  ```js
98
116
  const results = ElementFinder.findElement('button')
@@ -107,40 +125,6 @@ for (const item of results.elements) {
107
125
  }
108
126
  ```
109
127
 
110
- ### ESM Import
111
-
112
- ```js
113
- import {
114
- findElement,
115
- highlight,
116
- unhighlight,
117
- getValidTypes,
118
- } from '@nodebug/browser-element-finder/src/element-finder.js'
119
-
120
- // Find elements (requires DOM environment)
121
- const results = findElement('button', 'Submit')
122
-
123
- // Access metadata from results
124
- results.elements.forEach((item) => {
125
- console.log('Tag:', item.tagName)
126
- console.log('Position:', item.boundingBox.x, item.boundingBox.y)
127
- })
128
-
129
- // Highlight elements (extract DOM elements from wrapper objects)
130
- highlight(results.elements.map((e) => e.element))
131
- ```
132
-
133
- ### CommonJS Import
134
-
135
- ```js
136
- const {
137
- findElement,
138
- highlight,
139
- unhighlight,
140
- getValidTypes,
141
- } = require('@nodebug/browser-element-finder/src/element-finder.js')
142
- ```
143
-
144
128
  ### Accessing Element Definitions and Searchable Attributes
145
129
 
146
130
  The package exports JSON files containing element type definitions and searchable attributes:
@@ -167,42 +151,101 @@ const SEARCHABLE_ATTRIBUTES = require('@nodebug/browser-element-finder/searchabl
167
151
 
168
152
  ## API Summary
169
153
 
170
- | Function | Description |
171
- | ------------------------------------------------------- | --------------------------------------------------------- |
172
- | `findElement(type, text, exact, includeHidden, parent)` | Find elements by type/text, returns `{ elements: [...] }` |
173
- | `highlight(elements, color, width)` | Highlight elements with outline |
174
- | `unhighlight(elements)` | Remove highlight |
175
- | `getValidTypes()` | List all supported element types |
176
- | `getBoundingBox(element)` | Get bounding box for an element |
177
- | `setSearchableAttributes(attributes)` | Set custom attributes for text search |
178
- | `getSearchableAttributes()` | Get current searchable attributes |
179
- | `matchesType(el, type)` | Check if element matches a type |
180
- | `matchesContent(el, value, exact)` | Check if element matches text |
181
- | `getAllElements(root)` | Get all elements (with shadow DOM) |
182
- | `getAllFrames(root, maxFrames)` | Get all frames (main + iframes) |
183
- | `parseXPath(expr, el, depth)` | Parse XPath-like type expressions |
184
- | `splitByOperator(expr, op)` | Split XPath by operator |
154
+ | Function | Description |
155
+ | ------------------------------------------------- | ----------------------------------------------------------------------------- |
156
+ | `findElements(type, text, exact, parent)` | Find elements by type/text, returns `{ elements: [...] }` |
157
+ | `findElementByType(type, parent)` | Find elements by type only, returns `{ elements: [...] }` |
158
+ | `findElementByAttributes(value, exact, parent)` | Find elements by text/attribute, returns `{ elements: [...] }` |
159
+ | `findProbableElements(type, text, exact, parent)` | Find elements with fallback to nearby elements, returns `{ elements: [...] }` |
160
+ | `highlight(elements, color, width)` | Highlight elements with outline |
161
+ | `unhighlight(elements)` | Remove highlight |
162
+ | `getValidTypes()` | List all supported element types |
163
+ | `getBoundingBox(element)` | Get bounding box for an element |
164
+ | `setSearchableAttributes(attributes)` | Set custom attributes for text search |
165
+ | `getSearchableAttributes()` | Get current searchable attributes |
166
+ | `matchesType(el, type)` | Check if element matches a type |
167
+ | `matchesAttribute(el, value, exact)` | Check if element matches text/attribute |
168
+ | `getAllElements(root)` | Get all elements (with shadow DOM) |
169
+ | `getAllFrames(root)` | Get all frames (main + iframes) |
170
+ | `parseXPath(expr, el, depth)` | Parse XPath-like type expressions |
171
+ | `splitByOperator(expr, op)` | Split XPath by operator |
185
172
 
186
173
  ---
187
174
 
188
- ### `findElement(type, text, exact, includeHidden, parent)`
175
+ ### `findElements(type, text, exact, parent)`
189
176
 
190
- Finds elements matching the specified criteria. Searches all frames (main document + iframes) by default.
177
+ Finds elements matching the specified type and/or text. Combines type and attribute matching in a single call. Searches all frames (main document + iframes) by default.
191
178
 
192
- | Parameter | Type | Default | Description |
193
- | --------------- | --------- | ----------- | ----------------------------------------------------------------------------------------------------- |
194
- | `type` | `string` | `"element"` | Element type (see supported types below). Must be a string; throws `TypeError` for non-string values. |
195
- | `text` | `string` | `null` | Text to search for in content/attributes |
196
- | `exact` | `boolean` | `false` | Exact text match vs substring |
197
- | `includeHidden` | `boolean` | `false` | Include hidden elements |
198
- | `parent` | `Element` | `null` | Parent element to search within |
179
+ | Parameter | Type | Default | Description |
180
+ | --------- | --------- | ------- | ---------------------------------------------------------------------------------------------------------------- |
181
+ | `type` | `string` | `null` | Element type (see supported types below). If `null`, matches any type. Throws `TypeError` for non-string values. |
182
+ | `text` | `string` | `null` | Text to search for in content/attributes. If `null`/`''`/`undefined`, matches any text. |
183
+ | `exact` | `boolean` | `false` | Exact text match vs substring (only used when text is provided) |
184
+ | `parent` | `Element` | `null` | Parent element to search within |
199
185
 
200
186
  **Returns**: `{ elements: [{ element, boundingBox, tagName, frameIndex }] }`
201
187
 
202
188
  - `element`: Raw DOM element (main frame only; for iframes, use `frameIndex` and re-query after switching context)
203
189
  - `frameIndex`: `-1` for main frame, `0, 1, 2...` for iframes
204
190
 
205
- **Agent/Automation Note:** Iframe elements cannot be interacted with directly. Use `frameIndex` to switch context, then re-run `findElement` inside the iframe.
191
+ **Agent/Automation Note**: Iframe elements cannot be interacted with directly. Use `frameIndex` to switch context, then re-run `findElements` inside the iframe.
192
+
193
+ ### `findProbableElements(type, text, exact, parent)`
194
+
195
+ Finds elements matching the specified type with intelligent fallback to nearby elements. This function first attempts a direct match (element contains both type and text), then falls back to finding elements of the specified type near elements that match the text.
196
+
197
+ **Use Case**: When UI patterns separate content from interactive elements (e.g., a label with text "Email" next to an input field), `findProbableElements` will find the input even though the text isn't inside it.
198
+
199
+ | Parameter | Type | Default | Description |
200
+ | --------- | --------- | ------- | --------------------------------------------------------------------------------------------------------------------------------- |
201
+ | `type` | `string` | `null` | Element type (see supported types below). If `null`/`undefined`/`''`, matches any type. Throws `TypeError` for non-string values. |
202
+ | `text` | `string` | `null` | Text to search for in content/attributes. If `null`/`undefined`/`''`, matches any text. Throws `TypeError` for non-string values. |
203
+ | `exact` | `boolean` | `false` | Exact text match vs substring (only used when text is provided) |
204
+ | `parent` | `Element` | `null` | Parent element to search within |
205
+
206
+ **Returns**: `{ elements: [{ element, boundingBox, tagName, frameIndex }] }`
207
+
208
+ **Behavior**:
209
+
210
+ - If only `type` is provided: delegates to `findElementByType(type, parent)`
211
+ - If only `text` is provided: delegates to `findElementByAttributes(text, exact, parent)`
212
+ - If both are provided: attempts direct match, then falls back to nearby elements
213
+
214
+ **Fallback Strategy**: When no element matches both type and text directly, searches for nearby elements in this order:
215
+
216
+ 1. Parent elements (walk up the DOM tree)
217
+ 2. Sibling elements (same parent)
218
+ 3. Child elements (descendants)
219
+
220
+ **Example**:
221
+
222
+ ```javascript
223
+ // Type-only search (delegates to findElementByType)
224
+ const result1 = ElementFinder.findProbableElements('button')
225
+ // Returns all buttons on the page
226
+
227
+ // Text-only search (delegates to findElementByAttributes)
228
+ const result2 = ElementFinder.findProbableElements(null, 'Submit')
229
+ // Returns all elements containing "Submit"
230
+
231
+ // Direct match - element contains text
232
+ const result3 = ElementFinder.findProbableElements('button', 'Submit')
233
+ // Returns button with text "Submit" inside it
234
+
235
+ // Fallback match - text in nearby element
236
+ const result4 = ElementFinder.findProbableElements('textbox', 'Email')
237
+ // Returns input element when "Email" text is in a nearby label
238
+
239
+ // Fallback match - text in parent
240
+ const result5 = ElementFinder.findProbableElements('button', 'Menu Item 1')
241
+ // Returns button when "Menu Item 1" is in a child span
242
+ ```
243
+
244
+ **When to use**:
245
+
246
+ - Use `findElements` when you need strict matching (element must contain the text)
247
+ - Use `findProbableElements` when text might be in a nearby element (labels, icons, wrappers)
248
+ - Both functions search all frames by default
206
249
 
207
250
  ### `highlight(elements, color, width)`
208
251
 
@@ -238,22 +281,37 @@ Returns the current searchable attributes array.
238
281
 
239
282
  Checks if an element matches the specified type definition.
240
283
 
241
- ### `matchesContent(el, value, exact)`
284
+ ### `matchesAttribute(el, value, exact)`
285
+
286
+ Checks if an element matches the specified text/attribute value. Safely handles edge case elements that may throw errors on attribute access.
287
+
288
+ ### `findElementByType(type, parent)`
289
+
290
+ Finds elements by type only. Searches all frames by default.
291
+
292
+ | Parameter | Type | Default | Description |
293
+ | --------- | --------- | ----------- | ----------------------------------------------------------------------------------- |
294
+ | `type` | `string` | `"element"` | Element type (see supported types below). Throws `TypeError` for non-string values. |
295
+ | `parent` | `Element` | `null` | Parent element to search within |
242
296
 
243
- Checks if an element matches the specified text content. Safely handles edge case elements that may throw errors on attribute access.
297
+ ### `findElementByAttributes(value, exact, parent)`
298
+
299
+ Finds elements by text/attribute value only. Searches all frames by default.
300
+
301
+ | Parameter | Type | Default | Description |
302
+ | --------- | --------- | ------- | -------------------------------------------------------------------------------------------------- |
303
+ | `value` | `string` | `''` | Text/attribute value to search for. If `null`/`undefined`, defaults to empty string (matches all). |
304
+ | `exact` | `boolean` | `false` | Exact text match vs substring |
305
+ | `parent` | `Element` | `null` | Parent element to search within |
244
306
 
245
307
  ### `getAllElements(root)`
246
308
 
247
309
  Gets all elements including shadow DOM contents.
248
310
 
249
- ### `getAllFrames(root, maxFrames)`
311
+ ### `getAllFrames(root)`
250
312
 
251
313
  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.
252
314
 
253
- ### `getConfig()`
254
-
255
- Returns the current configuration object.
256
-
257
315
  ### `parseXPath(expr, el, depth)`
258
316
 
259
317
  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.
@@ -262,9 +320,11 @@ Parses XPath-like expressions for element type matching. The `depth` parameter i
262
320
 
263
321
  Splits XPath expressions by operator (and/or).
264
322
 
323
+ ---
324
+
265
325
  ## Working with Iframes
266
326
 
267
- The library automatically searches all frames (main document + iframes) by default. However, there are important limitations when working with iframe elements:
327
+ The library automatically searches all frames (main + iframes) by default. However, there are important limitations when working with iframe elements:
268
328
 
269
329
  ### Iframe Element Limitations
270
330
 
@@ -309,32 +369,70 @@ if (iframeElements.length > 0) {
309
369
  }
310
370
  ```
311
371
 
372
+ ---
373
+
312
374
  ## Supported Element Types
313
375
 
314
- | Type | Description |
315
- | ------------ | --------------------------------------------------- |
316
- | `button` | `<button>`, `[role="button"]`, `[type="button"]` |
317
- | `checkbox` | `<input type="checkbox">`, `[role="checkbox"]` |
318
- | `switch` | Toggle switches, checkboxes with switch role |
319
- | `slider` | `<input type="range">`, `[role="slider"]` |
320
- | `radio` | `<input type="radio">`, `[role="radio"]` |
321
- | `dropdown` | `<select>`, `[role="combobox"]`, `[role="listbox"]` |
322
- | `textbox` | `<input>`, `<textarea>`, `[role="textbox"]` |
323
- | `link` | `<a>`, `[role="link"]`, `[href]` |
324
- | `heading` | `<h1>-<h6>`, `[role="heading"]` |
325
- | `navigation` | `<nav>`, `[role="navigation"]` |
326
- | `list` | `<ul>`, `<ol>`, `[role="list"]` |
327
- | `listitem` | `<li>`, `[role="listitem"]` |
328
- | `menu` | `<menu>`, `[role="menu"]` |
329
- | `menuitem` | `[role="menuitem"]` |
330
- | `toolbar` | `[role="toolbar"]` |
331
- | `dialog` | `[role="dialog"]` |
332
- | `table` | `<table>`, `[role="table"]` |
333
- | `row` | `<tr>`, `[role="row"]` |
334
- | `column` | `<td>`, `<th>`, `[role="cell"]` |
335
- | `image` | `<img>`, `[role="img"]` |
336
- | `file` | `<input type="file"]` |
337
- | `element` | Matches all elements |
376
+ | Type | Description |
377
+ | ------------- | ------------------------------------------------------- |
378
+ | `button` | `<button>`, `[role="button"]`, `[type="button"]` |
379
+ | `checkbox` | `<input type="checkbox">`, `[role="checkbox"]` |
380
+ | `switch` | Toggle switches, checkboxes with switch role |
381
+ | `slider` | `<input type="range">`, `[role="slider"]` |
382
+ | `datepicker` | `<input type="date">` |
383
+ | `colorpicker` | `<input type="color">` |
384
+ | `radio` | `<input type="radio">`, `[role="radio"]` |
385
+ | `dropdown` | `<select>`, `[role="combobox"]`, `[role="listbox"]` |
386
+ | `textbox` | `<input>`, `<textarea>`, `[role="textbox"]` |
387
+ | `link` | `<a>`, `[role="link"]`, `[href]` |
388
+ | `heading` | `<h1>-<h6>`, `[role="heading"]` |
389
+ | `navigation` | `<nav>`, `[role="navigation"]` |
390
+ | `list` | `<ul>`, `<ol>`, `[role="list"]` |
391
+ | `listitem` | `<li>`, `[role="listitem"]` |
392
+ | `menu` | `<menu>`, `[role="menu"]` |
393
+ | `menuitem` | `[role="menuitem"]` |
394
+ | `toolbar` | `[role="toolbar"]` |
395
+ | `dialog` | `[role="dialog"]` |
396
+ | `table` | `<table>`, `[role="table"]` |
397
+ | `row` | `<tr>`, `[role="row"]` |
398
+ | `column` | `<td>`, `<th>`, `[role="cell"]` |
399
+ | `cell` | `<td>`, `[role="cell"]` (data cells only, no expansion) |
400
+ | `image` | `<img>`, `[role="img"]` |
401
+ | `file` | `<input type="file"]` |
402
+ | `element` | Matches all elements |
403
+
404
+ ---
405
+
406
+ ## Table Element Types: `column` vs `cell`
407
+
408
+ Both `column` and `cell` types find table cells, but they behave differently:
409
+
410
+ | Type | Matches | With Text Search |
411
+ | -------- | ----------------------- | --------------------------------------------------------- |
412
+ | `column` | `<td>`, `<th>` elements | Returns **all cells** in the column (header + data cells) |
413
+ | `cell` | `<td>` elements only | Returns **only the specific cell** (no expansion) |
414
+
415
+ **Example**:
416
+
417
+ ```javascript
418
+ // Find all cells in the "City" column (header + 3 data cells = 4 total)
419
+ const columnResult = ElementFinder.findElement('column', 'City')
420
+ // Returns: [th:City, td:New York, td:London, td:Paris]
421
+
422
+ // Find all cells when searching for a data cell value
423
+ const columnResult2 = ElementFinder.findElement('column', 'Paris')
424
+ // Returns: [th:City, td:New York, td:London, td:Paris]
425
+
426
+ // Find only the specific cell containing "Paris"
427
+ const cellResult = ElementFinder.findElement('cell', 'Paris')
428
+ // Returns: [td:Paris]
429
+
430
+ // Find by header text with cell type - returns only the header cell
431
+ const headerCell = ElementFinder.findElement('cell', 'City')
432
+ // Returns: [] (no td elements match "City" header text)
433
+ ```
434
+
435
+ ---
338
436
 
339
437
  ## Searchable Attributes
340
438
 
@@ -344,15 +442,19 @@ By default, the library searches these attributes (in priority order):
344
442
  - `resource-id`, `name`, `aria-label`, `class`, `hint`
345
443
  - `title`, `tooltip`, `alt`, `src`, `aria-labelledby`
346
444
 
445
+ ---
446
+
347
447
  ## Performance
348
448
 
349
449
  The library is optimized for large DOM trees with efficient algorithms:
350
450
 
351
- - **Innermost element filtering**: O(n) algorithm using Set-based lookups instead of O(n²) nested loops
352
- - **Column expansion**: O(n) algorithm using Map-based column position lookups instead of O(n²) cell iteration
353
- - **Memory efficient**: Minimal object creation during traversal
451
+ - **Pre-compiled type matchers**: Type definitions are compiled into cached matcher functions at module load time
452
+ - **O(n) innermost element filtering**: Set-based lookups instead of O(n²) nested loops
453
+ - **Map-based column expansion**: O(1) element-to-column-position lookups for table cells
454
+ - **Optimized text matching**: Direct text node iteration avoids expensive textContent calls
455
+ - **Loop optimizations**: Traditional for-loops with cached lengths for hot paths
354
456
 
355
- For pages with many matching elements, these optimizations significantly reduce search time.
457
+ ---
356
458
 
357
459
  ## Development
358
460
 
@@ -369,7 +471,7 @@ npm run test:watch
369
471
  npm run test:coverage
370
472
  ```
371
473
 
372
- **Note:** The `tests/integration/helpers/` folder is excluded from vitest runs as it contains helper utilities r
474
+ **Note**: The `tests/integration/helpers/` folder is excluded from vitest runs as it contains helper utilities.
373
475
 
374
476
  ### Linting
375
477
 
@@ -383,6 +485,8 @@ The library includes a Node.js-compatible module (`src/element-finder.js`) that
383
485
 
384
486
  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.
385
487
 
488
+ ---
489
+
386
490
  ## License
387
491
 
388
492
  MIT