elementus-ai 1.4.0 → 1.5.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 +15 -0
- package/README.md +6 -12
- package/elementus.js +35 -2
- package/index.d.ts +7 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes are documented here. This project adheres to [Semantic Versioning](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.5.0
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- `HealEvent.suggestion` — a ready-to-use, framework-native, properly-escaped suggested replacement
|
|
10
|
+
locator (`getByRole('link', { name: 'Save' })` on Playwright, `$('aria/Save')` on WDIO, `$('~Save')`
|
|
11
|
+
on Appium). Surface it verbatim. This moves suggestion-building into the library so consumers stop
|
|
12
|
+
hand-rolling it from `resolved` — hand-rolling re-introduced quote-escaping bugs (text with
|
|
13
|
+
apostrophes) and bad role mapping (form controls suggested as `getByText`). `resolved` stays for
|
|
14
|
+
consumers that want the raw facts.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Additive on the existing `onHeal` event; no behavior change.
|
|
19
|
+
|
|
5
20
|
## 1.4.0
|
|
6
21
|
|
|
7
22
|
### Added
|
package/README.md
CHANGED
|
@@ -47,8 +47,8 @@ I just installed the npm package "elementus-ai" — a self-healing element resol
|
|
|
47
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
|
|
48
48
|
- Set actionTimeout: 10000 in playwright config (Elementus respects framework timeouts)
|
|
49
49
|
- Optional heal visibility: only if the user wants healed locators surfaced, wire onHeal to the
|
|
50
|
-
framework's reporter.
|
|
51
|
-
|
|
50
|
+
framework's reporter. Use e.suggestion — a ready-to-use, escaped, framework-native replacement
|
|
51
|
+
locator — verbatim. Playwright: push a testInfo "healed" annotation (HTML report) AND
|
|
52
52
|
testInfo.attach (so it also reaches Allure + custom reporters); optionally fail CI on heal via an
|
|
53
53
|
env-gated afterEach/fixture. WebdriverIO/Appium: collect in onHeal, fail in afterTest. See
|
|
54
54
|
"Detecting heals" in the README. Default off
|
|
@@ -334,7 +334,7 @@ createElementus({
|
|
|
334
334
|
// Custom stop words
|
|
335
335
|
stopWords: null, // Set of words to ignore in descriptions
|
|
336
336
|
|
|
337
|
-
// Heal telemetry (opt-in) — called once per heal with { description, selector, method, framework, resolved }.
|
|
337
|
+
// Heal telemetry (opt-in) — called once per heal with { description, selector, method, framework, resolved, suggestion }.
|
|
338
338
|
// Best-effort & isolated: a throwing callback never breaks a test. See "Detecting heals" below.
|
|
339
339
|
onHeal: null, // e.g. (e) => console.log('healed', e.selector, '→', e.description)
|
|
340
340
|
})
|
|
@@ -343,7 +343,7 @@ createElementus({
|
|
|
343
343
|
## Detecting heals (reporting & CI)
|
|
344
344
|
|
|
345
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
|
|
346
|
+
elementus calls it **once per heal** with `{ description, selector, method, framework, resolved, suggestion }`. The library only
|
|
347
347
|
*emits* the event — turning it into a report annotation or a CI failure is framework-specific (a few lines
|
|
348
348
|
on your side). It fires when the AI resolves a replacement element, so it signals "the selector drifted and
|
|
349
349
|
AI stepped in", not "the action ultimately passed".
|
|
@@ -360,14 +360,8 @@ const el = createElementus({
|
|
|
360
360
|
provider: 'gemini',
|
|
361
361
|
geminiApiKey: process.env.GEMINI_API_KEY,
|
|
362
362
|
onHeal: (e) => {
|
|
363
|
-
//
|
|
364
|
-
const
|
|
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
|
|
363
|
+
// e.suggestion is a ready-to-use, escaped, framework-native replacement locator — use it verbatim.
|
|
364
|
+
const line = `${e.selector} → ${e.suggestion ?? '(located visually — no DOM suggestion)'}`
|
|
371
365
|
try {
|
|
372
366
|
const info = base.info()
|
|
373
367
|
info.annotations.push({ type: 'healed', description: line }) // Playwright HTML report
|
package/elementus.js
CHANGED
|
@@ -377,7 +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
|
|
380
|
+
* @param {(event: { description: string, selector: string, method: string, framework: string, resolved?: { tag: string, text: string, role: string|null }, suggestion?: string }) => void} [userConfig.onHeal] - called after a heal resolves a replacement element (original selector failed, AI found it). `resolved` is the element it healed to; `suggestion` is a ready-to-use, framework-native replacement locator (use it verbatim). Best-effort; throwing never breaks a test.
|
|
381
381
|
* @returns {{ wrap, wrapPage, wrapBrowser, locate, find, click }}
|
|
382
382
|
*/
|
|
383
383
|
function createElementus(userConfig = {}) {
|
|
@@ -1997,6 +1997,36 @@ function createElementus(userConfig = {}) {
|
|
|
1997
1997
|
return { tag: record.tag || '', text: (record.text || '').trim(), role: record.role || null }
|
|
1998
1998
|
}
|
|
1999
1999
|
|
|
2000
|
+
// Implicit ARIA roles for the common interactive tags (used when the element has no explicit
|
|
2001
|
+
// role attribute). `input` is deliberately omitted — its role depends on its type.
|
|
2002
|
+
const ROLE_BY_TAG = { a: 'link', button: 'button', select: 'combobox', textarea: 'textbox' }
|
|
2003
|
+
// Escape a value for a single-quoted string literal in the suggested locator.
|
|
2004
|
+
function _escSingle(s) {
|
|
2005
|
+
return String(s).replace(/\\/g, '\\\\').replace(/'/g, "\\'")
|
|
2006
|
+
}
|
|
2007
|
+
// Collapse whitespace and cap length so the suggested locator stays one readable line.
|
|
2008
|
+
function _suggestName(s) {
|
|
2009
|
+
return String(s || '').replace(/\s+/g, ' ').trim().slice(0, 80)
|
|
2010
|
+
}
|
|
2011
|
+
// Build a ready-to-use, framework-native, properly-escaped suggested replacement locator from
|
|
2012
|
+
// the resolved element. Consumers should surface event.suggestion verbatim — do NOT hand-roll it
|
|
2013
|
+
// (raw text interpolation re-introduces quote-escaping and role-mapping bugs).
|
|
2014
|
+
function _suggestLocator(framework, resolved) {
|
|
2015
|
+
if (!resolved) return undefined
|
|
2016
|
+
const tag = resolved.tag || ''
|
|
2017
|
+
const name = _suggestName(resolved.text)
|
|
2018
|
+
const role = resolved.role || ROLE_BY_TAG[tag] || null
|
|
2019
|
+
if (framework === 'playwright') {
|
|
2020
|
+
if (role && name) return `getByRole('${_escSingle(role)}', { name: '${_escSingle(name)}' })`
|
|
2021
|
+
if (name) return `getByText('${_escSingle(name)}')`
|
|
2022
|
+
return `locator('${_escSingle(tag || '*')}')`
|
|
2023
|
+
}
|
|
2024
|
+
if (framework === 'appium') {
|
|
2025
|
+
return name ? `$('~${_escSingle(name)}')` : `$('${_escSingle(tag || '*')}')`
|
|
2026
|
+
}
|
|
2027
|
+
return name ? `$('aria/${_escSingle(name)}')` : `$('${_escSingle(tag || '*')}')`
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2000
2030
|
// Fire the user's onHeal(event) after a real heal — the original selector failed and the AI
|
|
2001
2031
|
// pipeline resolved a replacement. Best-effort and fully isolated: a throwing or rejecting
|
|
2002
2032
|
// callback must never break a test. No-op with zero overhead when onHeal is not configured.
|
|
@@ -2008,7 +2038,10 @@ function createElementus(userConfig = {}) {
|
|
|
2008
2038
|
const framework = _isNative(ctx) ? 'appium' : _isPlaywright(ctx) ? 'playwright' : 'wdio'
|
|
2009
2039
|
try {
|
|
2010
2040
|
const event = { description, selector: selectorKey, method, framework }
|
|
2011
|
-
if (resolved)
|
|
2041
|
+
if (resolved) {
|
|
2042
|
+
event.resolved = resolved
|
|
2043
|
+
event.suggestion = _suggestLocator(framework, resolved)
|
|
2044
|
+
}
|
|
2012
2045
|
const r = cb(event)
|
|
2013
2046
|
if (r && typeof r.then === 'function') r.then(undefined, () => {})
|
|
2014
2047
|
} catch {}
|
package/index.d.ts
CHANGED
|
@@ -54,11 +54,15 @@ export interface HealEvent {
|
|
|
54
54
|
method: string
|
|
55
55
|
/** Which driver the heal happened on. */
|
|
56
56
|
framework: 'playwright' | 'wdio' | 'appium'
|
|
57
|
+
/** The element elementus resolved to (when known — absent on coordinate-only vision heals). */
|
|
58
|
+
resolved?: { tag: string; text: string; role: string | null }
|
|
57
59
|
/**
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
+
* Ready-to-use, framework-native, properly-escaped suggested replacement locator for the failed
|
|
61
|
+
* selector (e.g. `getByRole('link', { name: 'Save' })` on Playwright, `$('aria/Save')` on WDIO).
|
|
62
|
+
* Surface it verbatim — don't hand-build one from `resolved` (that re-introduces quote-escaping
|
|
63
|
+
* and role-mapping bugs). Absent on coordinate-only vision heals.
|
|
60
64
|
*/
|
|
61
|
-
|
|
65
|
+
suggestion?: string
|
|
62
66
|
}
|
|
63
67
|
|
|
64
68
|
/**
|
package/package.json
CHANGED