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 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. 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
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
- // 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
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 (when known), for suggesting a replacement. Best-effort; throwing never breaks a test.
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) event.resolved = 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
- * 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
+ * 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
- resolved?: { tag: string; text: string; role: string | null }
65
+ suggestion?: string
62
66
  }
63
67
 
64
68
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elementus-ai",
3
- "version": "1.4.0",
3
+ "version": "1.5.0",
4
4
  "description": "Self-healing element resolution for Playwright, WDIO & Appium. AI-powered fallback when selectors break.",
5
5
  "main": "elementus.js",
6
6
  "types": "index.d.ts",