elementus-ai 1.2.0 → 1.4.0
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/CHANGELOG.md +28 -0
- package/README.md +96 -1
- package/elementus.js +44 -2
- package/index.d.ts +23 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,34 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are documented here. This project adheres to [Semantic Versioning](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.4.0
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `HealEvent.resolved` — `{ tag, text, role }` of the element a heal resolved to (when known), so a
|
|
10
|
+
consumer can compose a **suggested replacement locator** (e.g. `getByRole(role, { name: text })`) and
|
|
11
|
+
show "selector X failed → replace with Y" in reports. Absent on coordinate-only vision heals.
|
|
12
|
+
|
|
13
|
+
### Changed
|
|
14
|
+
|
|
15
|
+
- Additive on the existing `onHeal` event; no behavior change.
|
|
16
|
+
|
|
17
|
+
## 1.3.0
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `onHeal(event)` config callback — fires once per heal with `{ description, selector, method, framework }`
|
|
22
|
+
so consumers can surface healed locators (report annotations, CI fail-on-heal). Framework-agnostic:
|
|
23
|
+
elementus emits the event; wiring it to a reporter is consumer-side (README "Detecting heals" has
|
|
24
|
+
Playwright + WebdriverIO/Appium recipes).
|
|
25
|
+
- `HealEvent` exported type (`index.d.ts`); `onHeal` added to `ElementusOptions`.
|
|
26
|
+
- Test `T47` covering onHeal; docs in README, `docs/API.md`, `docs/ARCHITECTURE.md`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- No change to existing behavior. `onHeal` is opt-in; unset ⇒ byte-for-byte the same as 1.2.0
|
|
31
|
+
(zero-overhead path untouched, single export preserved).
|
|
32
|
+
|
|
5
33
|
## 1.2.0
|
|
6
34
|
|
|
7
35
|
### Added
|
package/README.md
CHANGED
|
@@ -34,11 +34,24 @@ I just installed the npm package "elementus-ai" — a self-healing element resol
|
|
|
34
34
|
|
|
35
35
|
3. INTEGRATE BASED ON MY FRAMEWORK
|
|
36
36
|
|
|
37
|
+
First create the elementus instance using the provider chosen in step 2:
|
|
38
|
+
const { createElementus } = require('elementus-ai') // ESM/TS: import { createElementus } from 'elementus-ai'
|
|
39
|
+
const el = createElementus({ provider: 'gemini', geminiApiKey: process.env.GEMINI_API_KEY })
|
|
40
|
+
// LM Studio instead: createElementus({ provider: 'lmstudio', lmStudioUrl: 'http://localhost:1234/v1/chat/completions', model: 'holo-3.1-9b' })
|
|
41
|
+
For exact, copy-pasteable code (TypeScript fixture, onHeal recipe), read the installed package's
|
|
42
|
+
node_modules/elementus-ai/README.md ("Quick Start", "TypeScript", "Detecting heals") and docs/API.md.
|
|
43
|
+
|
|
37
44
|
For Playwright:
|
|
38
|
-
- Create or update a fixtures file that
|
|
45
|
+
- Create or update a fixtures file that overrides the page fixture with el.wrapPage(page)
|
|
39
46
|
- Make sure all tests import from the fixtures file instead of @playwright/test
|
|
40
47
|
- TypeScript projects: use import/export and type the override as base.extend<{ page: ElementusPage }>({ ... }) (import ElementusPage from elementus-ai) so { ai } is autocompleted and documented. Types are bundled — do NOT add @types or a "declare module" shim
|
|
41
48
|
- Set actionTimeout: 10000 in playwright config (Elementus respects framework timeouts)
|
|
49
|
+
- Optional heal visibility: only if the user wants healed locators surfaced, wire onHeal to the
|
|
50
|
+
framework's reporter. Build a suggested replacement from e.resolved ({ tag, text, role }), e.g.
|
|
51
|
+
getByRole(role, { name: text }). Playwright: push a testInfo "healed" annotation (HTML report) AND
|
|
52
|
+
testInfo.attach (so it also reaches Allure + custom reporters); optionally fail CI on heal via an
|
|
53
|
+
env-gated afterEach/fixture. WebdriverIO/Appium: collect in onHeal, fail in afterTest. See
|
|
54
|
+
"Detecting heals" in the README. Default off
|
|
42
55
|
|
|
43
56
|
For WebDriverIO:
|
|
44
57
|
- In wdio.conf.js before hook, wrap browser and override global $:
|
|
@@ -320,9 +333,91 @@ createElementus({
|
|
|
320
333
|
|
|
321
334
|
// Custom stop words
|
|
322
335
|
stopWords: null, // Set of words to ignore in descriptions
|
|
336
|
+
|
|
337
|
+
// Heal telemetry (opt-in) — called once per heal with { description, selector, method, framework, resolved }.
|
|
338
|
+
// Best-effort & isolated: a throwing callback never breaks a test. See "Detecting heals" below.
|
|
339
|
+
onHeal: null, // e.g. (e) => console.log('healed', e.selector, '→', e.description)
|
|
323
340
|
})
|
|
324
341
|
```
|
|
325
342
|
|
|
343
|
+
## Detecting heals (reporting & CI)
|
|
344
|
+
|
|
345
|
+
A heal is silent by default — the test still passes. To surface it, pass an `onHeal(event)` callback;
|
|
346
|
+
elementus calls it **once per heal** with `{ description, selector, method, framework, resolved }`. The library only
|
|
347
|
+
*emits* the event — turning it into a report annotation or a CI failure is framework-specific (a few lines
|
|
348
|
+
on your side). It fires when the AI resolves a replacement element, so it signals "the selector drifted and
|
|
349
|
+
AI stepped in", not "the action ultimately passed".
|
|
350
|
+
|
|
351
|
+
**Playwright** — annotate the test (shows in the Playwright HTML report), and optionally fail
|
|
352
|
+
CI on heal:
|
|
353
|
+
|
|
354
|
+
```javascript
|
|
355
|
+
// fixtures.js
|
|
356
|
+
const { test: base } = require('@playwright/test')
|
|
357
|
+
const { createElementus } = require('elementus-ai')
|
|
358
|
+
|
|
359
|
+
const el = createElementus({
|
|
360
|
+
provider: 'gemini',
|
|
361
|
+
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
362
|
+
onHeal: (e) => {
|
|
363
|
+
// Build a suggested replacement from the element elementus resolved to (e.resolved)
|
|
364
|
+
const r = e.resolved
|
|
365
|
+
const role = r && (r.role ?? { a: 'link', button: 'button' }[r.tag])
|
|
366
|
+
const suggestion = !r ? '(located visually — no DOM suggestion)'
|
|
367
|
+
: role && r.text ? `getByRole('${role}', { name: '${r.text}' })`
|
|
368
|
+
: r.text ? `getByText('${r.text}')`
|
|
369
|
+
: `locator('${r.tag}')`
|
|
370
|
+
const line = `${e.selector} → ${suggestion}` // failed locator → suggested replacement
|
|
371
|
+
try {
|
|
372
|
+
const info = base.info()
|
|
373
|
+
info.annotations.push({ type: 'healed', description: line }) // Playwright HTML report
|
|
374
|
+
void info.attach('healed', { body: line, contentType: 'text/plain' }) // Allure + custom reporters
|
|
375
|
+
} catch {} // ignored outside a running test
|
|
376
|
+
},
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
const test = base.extend({
|
|
380
|
+
page: async ({ page }, use) => { await use(el.wrapPage(page)) },
|
|
381
|
+
// Optional: fail CI when a selector only passed via self-healing (drift you should fix).
|
|
382
|
+
failOnHeal: [async ({}, use, testInfo) => {
|
|
383
|
+
await use()
|
|
384
|
+
const heals = testInfo.annotations.filter(a => a.type === 'healed')
|
|
385
|
+
if (process.env.ELEMENTUS_FAIL_ON_HEAL && heals.length) {
|
|
386
|
+
throw new Error('Passed only via self-healing — fix the selector(s):\n ' + heals.map(h => h.description).join('\n '))
|
|
387
|
+
}
|
|
388
|
+
}, { auto: true }],
|
|
389
|
+
})
|
|
390
|
+
module.exports = { test, expect: require('@playwright/test').expect }
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
(TypeScript: import the `HealEvent` type from `elementus-ai`; the annotation/fixture code is identical.)
|
|
394
|
+
|
|
395
|
+
**WebdriverIO / Appium** — collect in `onHeal`, fail (or report) in `afterTest`:
|
|
396
|
+
|
|
397
|
+
```javascript
|
|
398
|
+
// wdio.conf.js
|
|
399
|
+
const { createElementus } = require('elementus-ai')
|
|
400
|
+
let heals = []
|
|
401
|
+
const el = createElementus({ provider: 'gemini', geminiApiKey: '...', onHeal: (e) => heals.push(e) })
|
|
402
|
+
|
|
403
|
+
exports.config = {
|
|
404
|
+
beforeTest() { heals = [] },
|
|
405
|
+
afterTest() {
|
|
406
|
+
if (process.env.ELEMENTUS_FAIL_ON_HEAL && heals.length) {
|
|
407
|
+
throw new Error('healed: ' + heals.map(h => h.description).join(', '))
|
|
408
|
+
}
|
|
409
|
+
},
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
**Where each report shows it:** the annotation appears in the **Playwright HTML report** (test → _Annotations_).
|
|
414
|
+
In **Allure**, the heal shows in the test's **step tree** (the broken locator → the healed `[data-elementus=…]`
|
|
415
|
+
element) and, with fail-on-heal on, the **failure message** — note `allure-playwright` does not convert
|
|
416
|
+
runtime annotations into labels/steps, so for a dedicated Allure marker also
|
|
417
|
+
`testInfo.attach('healed', { body: \`${e.selector} → ${e.description}\`, contentType: 'text/plain' })`
|
|
418
|
+
(attachments surface in Allure and most custom reporters). A custom HTML reporter surfaces the heal via the
|
|
419
|
+
failure message and captured stdout.
|
|
420
|
+
|
|
326
421
|
## Security Notes
|
|
327
422
|
|
|
328
423
|
- **Debug screenshots** capture the full page — including any sensitive data visible on it. Keep `debugDir` out of version control.
|
package/elementus.js
CHANGED
|
@@ -377,6 +377,7 @@ const REGION_LABELS = [
|
|
|
377
377
|
* @param {number} [userConfig.visionMaxWidth=1280] - max screenshot width (px) sent to vision LLM
|
|
378
378
|
* @param {string|null} [userConfig.cacheFile=null] - opt-in fingerprint cache file (e.g. './elementus-cache.json')
|
|
379
379
|
* @param {string|null} [userConfig.embeddingModel=null] - opt-in embedding model for semantic matching
|
|
380
|
+
* @param {(event: { description: string, selector: string, method: string, framework: string, resolved?: { tag: string, text: string, role: string|null } }) => void} [userConfig.onHeal] - called after a heal resolves a replacement element (original selector failed, AI found it). `resolved` is the element it healed to (when known), for suggesting a replacement. Best-effort; throwing never breaks a test.
|
|
380
381
|
* @returns {{ wrap, wrapPage, wrapBrowser, locate, find, click }}
|
|
381
382
|
*/
|
|
382
383
|
function createElementus(userConfig = {}) {
|
|
@@ -1988,6 +1989,31 @@ function createElementus(userConfig = {}) {
|
|
|
1988
1989
|
return { record: null, somCandidates: out.somCandidates || null }
|
|
1989
1990
|
}
|
|
1990
1991
|
|
|
1992
|
+
// Facts about the element the last heal resolved to (tag/text/role) — set during resolution,
|
|
1993
|
+
// read once by _notifyHeal. Framework-agnostic; consumers format a suggested replacement locator.
|
|
1994
|
+
let _lastResolved = null
|
|
1995
|
+
function _resolvedFacts(record) {
|
|
1996
|
+
if (!record) return null
|
|
1997
|
+
return { tag: record.tag || '', text: (record.text || '').trim(), role: record.role || null }
|
|
1998
|
+
}
|
|
1999
|
+
|
|
2000
|
+
// Fire the user's onHeal(event) after a real heal — the original selector failed and the AI
|
|
2001
|
+
// pipeline resolved a replacement. Best-effort and fully isolated: a throwing or rejecting
|
|
2002
|
+
// callback must never break a test. No-op with zero overhead when onHeal is not configured.
|
|
2003
|
+
function _notifyHeal(ctx, description, selectorKey, method) {
|
|
2004
|
+
const resolved = _lastResolved
|
|
2005
|
+
_lastResolved = null
|
|
2006
|
+
const cb = config.onHeal
|
|
2007
|
+
if (typeof cb !== 'function') return
|
|
2008
|
+
const framework = _isNative(ctx) ? 'appium' : _isPlaywright(ctx) ? 'playwright' : 'wdio'
|
|
2009
|
+
try {
|
|
2010
|
+
const event = { description, selector: selectorKey, method, framework }
|
|
2011
|
+
if (resolved) event.resolved = resolved
|
|
2012
|
+
const r = cb(event)
|
|
2013
|
+
if (r && typeof r.then === 'function') r.then(undefined, () => {})
|
|
2014
|
+
} catch {}
|
|
2015
|
+
}
|
|
2016
|
+
|
|
1991
2017
|
async function _findByDescription(ctx, description, selectorKey = '') {
|
|
1992
2018
|
const { record, somCandidates } = await _resolveElement(ctx, description, selectorKey)
|
|
1993
2019
|
if (record) {
|
|
@@ -1995,6 +2021,7 @@ function createElementus(userConfig = {}) {
|
|
|
1995
2021
|
const mark = {}
|
|
1996
2022
|
const locator = record._locator || await markByElement(ctx, record, mark)
|
|
1997
2023
|
await _cacheStore(ctx, description, selectorKey, record, record._uid || mark.uid || null)
|
|
2024
|
+
_lastResolved = _resolvedFacts(record)
|
|
1998
2025
|
return locator
|
|
1999
2026
|
} catch (err) {
|
|
2000
2027
|
console.log(`[Resolve] Mark failed (${err.message}) — trying vision`)
|
|
@@ -2006,11 +2033,13 @@ function createElementus(userConfig = {}) {
|
|
|
2006
2033
|
const mark = {}
|
|
2007
2034
|
const locator = await markByElement(ctx, result.element, mark)
|
|
2008
2035
|
await _cacheStore(ctx, description, selectorKey, result.element, mark.uid || null)
|
|
2036
|
+
_lastResolved = _resolvedFacts(result.element)
|
|
2009
2037
|
return locator
|
|
2010
2038
|
}
|
|
2011
2039
|
const mark = {}
|
|
2012
2040
|
const locator = await markAtCoordinates(ctx, result.coords.docX, result.coords.docY, mark)
|
|
2013
2041
|
await _cacheStore(ctx, description, selectorKey, result.coords, mark.uid || null)
|
|
2042
|
+
_lastResolved = null
|
|
2014
2043
|
return locator
|
|
2015
2044
|
} catch (err) {
|
|
2016
2045
|
throw new Error(`All fallback paths exhausted for "${description}": ${err.message}`)
|
|
@@ -2038,7 +2067,10 @@ function createElementus(userConfig = {}) {
|
|
|
2038
2067
|
} catch {
|
|
2039
2068
|
console.log(`\u2717 Locator failed \u2014 searching for: "${description}"`)
|
|
2040
2069
|
}
|
|
2041
|
-
|
|
2070
|
+
const selectorKey = _selectorKey(locator)
|
|
2071
|
+
const resolved = await _findByDescription(ctx, description, selectorKey)
|
|
2072
|
+
_notifyHeal(ctx, description, selectorKey, 'locate')
|
|
2073
|
+
return resolved
|
|
2042
2074
|
}
|
|
2043
2075
|
|
|
2044
2076
|
/**
|
|
@@ -2088,6 +2120,8 @@ function createElementus(userConfig = {}) {
|
|
|
2088
2120
|
// Store before clicking \u2014 the click may navigate away from the page
|
|
2089
2121
|
await _cacheStore(ctx, description, selectorKey, record)
|
|
2090
2122
|
await scrollAndClick(ctx, record)
|
|
2123
|
+
_lastResolved = _resolvedFacts(record)
|
|
2124
|
+
_notifyHeal(ctx, description, selectorKey, 'click')
|
|
2091
2125
|
return
|
|
2092
2126
|
}
|
|
2093
2127
|
try {
|
|
@@ -2095,9 +2129,13 @@ function createElementus(userConfig = {}) {
|
|
|
2095
2129
|
if (result.element) {
|
|
2096
2130
|
await _cacheStore(ctx, description, selectorKey, result.element)
|
|
2097
2131
|
await scrollAndClick(ctx, result.element)
|
|
2132
|
+
_lastResolved = _resolvedFacts(result.element)
|
|
2133
|
+
_notifyHeal(ctx, description, selectorKey, 'click')
|
|
2098
2134
|
return
|
|
2099
2135
|
}
|
|
2100
2136
|
await clickAtCoords(ctx, result.coords)
|
|
2137
|
+
_lastResolved = null
|
|
2138
|
+
_notifyHeal(ctx, description, selectorKey, 'click')
|
|
2101
2139
|
} catch (err) {
|
|
2102
2140
|
throw new Error(`All fallback paths exhausted for "${description}": ${err.message}`)
|
|
2103
2141
|
}
|
|
@@ -2160,6 +2198,7 @@ function createElementus(userConfig = {}) {
|
|
|
2160
2198
|
if (!_resolved) {
|
|
2161
2199
|
console.log(`[AI] ${prop}() \u2014 resolving via AI first for "${description}"`)
|
|
2162
2200
|
_resolved = await _findByDescription(driverContext, description, wrapSelectorKey)
|
|
2201
|
+
_notifyHeal(driverContext, description, wrapSelectorKey, String(prop))
|
|
2163
2202
|
}
|
|
2164
2203
|
return _resolved[prop](...args)
|
|
2165
2204
|
}
|
|
@@ -2168,7 +2207,10 @@ function createElementus(userConfig = {}) {
|
|
|
2168
2207
|
return await original.apply(target, args)
|
|
2169
2208
|
} catch (firstError) {
|
|
2170
2209
|
console.log(`[AI] ${String(prop)}() failed \u2014 AI fallback for "${description}"`)
|
|
2171
|
-
if (!_resolved)
|
|
2210
|
+
if (!_resolved) {
|
|
2211
|
+
_resolved = await _findByDescription(driverContext, description, wrapSelectorKey)
|
|
2212
|
+
_notifyHeal(driverContext, description, wrapSelectorKey, String(prop))
|
|
2213
|
+
}
|
|
2172
2214
|
|
|
2173
2215
|
const resolvedMethod = _resolved[prop]
|
|
2174
2216
|
if (typeof resolvedMethod !== 'function') {
|
package/index.d.ts
CHANGED
|
@@ -36,6 +36,29 @@ export interface ElementusOptions {
|
|
|
36
36
|
cacheFile?: string | null
|
|
37
37
|
/** Opt-in embedding model for semantic paraphrase matching. @default null */
|
|
38
38
|
embeddingModel?: string | null
|
|
39
|
+
/**
|
|
40
|
+
* Called after a heal resolves a replacement element (the original selector failed and the AI
|
|
41
|
+
* pipeline found a match). Best-effort and isolated — a throwing/rejecting callback never breaks
|
|
42
|
+
* a test. Fires at resolution time, so it does NOT guarantee the subsequent action succeeded.
|
|
43
|
+
*/
|
|
44
|
+
onHeal?: (event: HealEvent) => void | Promise<void>
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Event passed to `onHeal` when a broken selector is healed. */
|
|
48
|
+
export interface HealEvent {
|
|
49
|
+
/** The natural-language `{ ai }` description that resolved the element. */
|
|
50
|
+
description: string
|
|
51
|
+
/** The original selector that failed (selector key). */
|
|
52
|
+
selector: string
|
|
53
|
+
/** The action that triggered the heal: 'click', 'fill', 'isVisible', 'locate', etc. */
|
|
54
|
+
method: string
|
|
55
|
+
/** Which driver the heal happened on. */
|
|
56
|
+
framework: 'playwright' | 'wdio' | 'appium'
|
|
57
|
+
/**
|
|
58
|
+
* The element elementus resolved to (when known — absent on coordinate-only vision heals).
|
|
59
|
+
* Use it to compose a suggested replacement locator, e.g. getByRole(role, { name: text }).
|
|
60
|
+
*/
|
|
61
|
+
resolved?: { tag: string; text: string; role: string | null }
|
|
39
62
|
}
|
|
40
63
|
|
|
41
64
|
/**
|
package/package.json
CHANGED