elementus-ai 1.5.0 → 1.5.1

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,24 @@
2
2
 
3
3
  All notable changes are documented here. This project adheres to [Semantic Versioning](https://semver.org).
4
4
 
5
+ ## 1.5.1
6
+
7
+ ### Fixed
8
+
9
+ - Suggestion/`resolved` quality for **form controls**. A healed `<input>`/`<select>`/`<textarea>` now
10
+ reports its implicit ARIA **role** (`<input type="submit">` → `button`, text inputs → `textbox`,
11
+ checkbox/radio/range/number → their roles) and a proper **accessible name** (a button-like input's
12
+ `value`, e.g. `Login`, instead of its `name` attribute). So the heal suggests
13
+ `getByRole('button', { name: 'Login' })` instead of the previous `getByText('login-button')` — which
14
+ would never match a form control. `_suggestLocator` also never falls back to `getByText` for a form
15
+ control (it uses a tag locator when no role/name is available).
16
+
17
+ ### Changed
18
+
19
+ - `textOf` (element label derivation, used for both matching and suggestions) now prefers a button-like
20
+ input's `value` over its `name`/`title`/`alt` attributes; a text input's current value stays a last
21
+ resort. No public API change — the `resolved`/`suggestion` shapes are unchanged, just more accurate.
22
+
5
23
  ## 1.5.0
6
24
 
7
25
  ### Added
package/elementus.js CHANGED
@@ -800,10 +800,19 @@ function createElementus(userConfig = {}) {
800
800
  function textOf(el) {
801
801
  const t = el.textContent.trim().replace(/\s+/g, ' ')
802
802
  if (t) return t
803
- for (const attr of ['aria-label', 'placeholder', 'name', 'title', 'alt']) {
803
+ for (const attr of ['aria-label', 'placeholder']) {
804
804
  const v = el.getAttribute(attr)
805
805
  if (v && v.trim()) return v.trim().replace(/\s+/g, ' ')
806
806
  }
807
+ // Button-like inputs: `value` is the visible label, e.g. <input type="submit" value="Login">
808
+ if (el.tagName === 'INPUT' && /^(submit|button|reset)$/i.test(el.type) && el.value) {
809
+ return String(el.value).trim().replace(/\s+/g, ' ')
810
+ }
811
+ for (const attr of ['name', 'title', 'alt']) {
812
+ const v = el.getAttribute(attr)
813
+ if (v && v.trim()) return v.trim().replace(/\s+/g, ' ')
814
+ }
815
+ // Text inputs/textarea: current value last (it's user-entered content, not a label)
807
816
  if ((el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') && el.type !== 'password' && el.value) {
808
817
  return String(el.value).trim().replace(/\s+/g, ' ')
809
818
  }
@@ -824,6 +833,7 @@ function createElementus(userConfig = {}) {
824
833
  text,
825
834
  tag: el.tagName.toLowerCase(),
826
835
  role: el.getAttribute('role') || null,
836
+ type: el.type || '',
827
837
  href: el.getAttribute('href') || null,
828
838
  docX: Math.round(rect.left + window.scrollX + rect.width / 2),
829
839
  docY: Math.round(rect.top + window.scrollY + rect.height / 2),
@@ -1013,10 +1023,19 @@ function createElementus(userConfig = {}) {
1013
1023
  function textOf(el) {
1014
1024
  const t = el.textContent.trim().replace(/\s+/g, ' ')
1015
1025
  if (t) return t
1016
- for (const attr of ['aria-label', 'placeholder', 'name', 'title', 'alt']) {
1026
+ for (const attr of ['aria-label', 'placeholder']) {
1027
+ const v = el.getAttribute(attr)
1028
+ if (v && v.trim()) return v.trim().replace(/\s+/g, ' ')
1029
+ }
1030
+ // Button-like inputs: `value` is the visible label, e.g. <input type="submit" value="Login">
1031
+ if (el.tagName === 'INPUT' && /^(submit|button|reset)$/i.test(el.type) && el.value) {
1032
+ return String(el.value).trim().replace(/\s+/g, ' ')
1033
+ }
1034
+ for (const attr of ['name', 'title', 'alt']) {
1017
1035
  const v = el.getAttribute(attr)
1018
1036
  if (v && v.trim()) return v.trim().replace(/\s+/g, ' ')
1019
1037
  }
1038
+ // Text inputs/textarea: current value last (it's user-entered content, not a label)
1020
1039
  if ((el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') && el.type !== 'password' && el.value) {
1021
1040
  return String(el.value).trim().replace(/\s+/g, ' ')
1022
1041
  }
@@ -1035,6 +1054,7 @@ function createElementus(userConfig = {}) {
1035
1054
  classes: typeof el.className === 'string' ? el.className.trim() : '',
1036
1055
  name: el.getAttribute('name') || '',
1037
1056
  role: el.getAttribute('role') || '',
1057
+ type: el.type || '',
1038
1058
  href: el.getAttribute('href') || '',
1039
1059
  text: textOf(el),
1040
1060
  neighborText: el.parentElement
@@ -1341,10 +1361,19 @@ function createElementus(userConfig = {}) {
1341
1361
  function textOf(el) {
1342
1362
  const t = el.textContent.trim().replace(/\s+/g, ' ')
1343
1363
  if (t) return t
1344
- for (const attr of ['aria-label', 'placeholder', 'name', 'title', 'alt']) {
1364
+ for (const attr of ['aria-label', 'placeholder']) {
1345
1365
  const v = el.getAttribute(attr)
1346
1366
  if (v && v.trim()) return v.trim().replace(/\s+/g, ' ')
1347
1367
  }
1368
+ // Button-like inputs: `value` is the visible label, e.g. <input type="submit" value="Login">
1369
+ if (el.tagName === 'INPUT' && /^(submit|button|reset)$/i.test(el.type) && el.value) {
1370
+ return String(el.value).trim().replace(/\s+/g, ' ')
1371
+ }
1372
+ for (const attr of ['name', 'title', 'alt']) {
1373
+ const v = el.getAttribute(attr)
1374
+ if (v && v.trim()) return v.trim().replace(/\s+/g, ' ')
1375
+ }
1376
+ // Text inputs/textarea: current value last (it's user-entered content, not a label)
1348
1377
  if ((el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') && el.type !== 'password' && el.value) {
1349
1378
  return String(el.value).trim().replace(/\s+/g, ' ')
1350
1379
  }
@@ -1356,6 +1385,7 @@ function createElementus(userConfig = {}) {
1356
1385
  return {
1357
1386
  uid: existing || uid,
1358
1387
  tag: el.tagName.toLowerCase(),
1388
+ type: el.type || '',
1359
1389
  text: textOf(el),
1360
1390
  href: el.getAttribute('href') || null,
1361
1391
  docX: Math.round(rect.left + window.scrollX + rect.width / 2),
@@ -1364,7 +1394,7 @@ function createElementus(userConfig = {}) {
1364
1394
  }, uid, { timeout: 5000 })
1365
1395
  console.log(`[Resolve] Aria grounded <${record.tag}> "${record.text}" via ref=${ref}`)
1366
1396
  const locator = await _makeLocator(ctx, `[data-elementus="${record.uid}"]`)
1367
- return { tag: record.tag, text: record.text, href: record.href, docX: record.docX, docY: record.docY, _locator: locator, _uid: record.uid }
1397
+ return { tag: record.tag, type: record.type, text: record.text, href: record.href, docX: record.docX, docY: record.docY, _locator: locator, _uid: record.uid }
1368
1398
  } catch (err) {
1369
1399
  console.log(`[Resolve] Aria ref resolution failed (${err.message}) — falling through`)
1370
1400
  return null
@@ -1693,10 +1723,19 @@ function createElementus(userConfig = {}) {
1693
1723
  function textOf(el) {
1694
1724
  const t = el.textContent.trim().replace(/\s+/g, ' ')
1695
1725
  if (t) return t
1696
- for (const attr of ['aria-label', 'placeholder', 'name', 'title', 'alt']) {
1726
+ for (const attr of ['aria-label', 'placeholder']) {
1697
1727
  const v = el.getAttribute(attr)
1698
1728
  if (v && v.trim()) return v.trim().replace(/\s+/g, ' ')
1699
1729
  }
1730
+ // Button-like inputs: `value` is the visible label, e.g. <input type="submit" value="Login">
1731
+ if (el.tagName === 'INPUT' && /^(submit|button|reset)$/i.test(el.type) && el.value) {
1732
+ return String(el.value).trim().replace(/\s+/g, ' ')
1733
+ }
1734
+ for (const attr of ['name', 'title', 'alt']) {
1735
+ const v = el.getAttribute(attr)
1736
+ if (v && v.trim()) return v.trim().replace(/\s+/g, ' ')
1737
+ }
1738
+ // Text inputs/textarea: current value last (it's user-entered content, not a label)
1700
1739
  if ((el.tagName === 'INPUT' || el.tagName === 'TEXTAREA') && el.type !== 'password' && el.value) {
1701
1740
  return String(el.value).trim().replace(/\s+/g, ' ')
1702
1741
  }
@@ -1992,9 +2031,30 @@ function createElementus(userConfig = {}) {
1992
2031
  // Facts about the element the last heal resolved to (tag/text/role) — set during resolution,
1993
2032
  // read once by _notifyHeal. Framework-agnostic; consumers format a suggested replacement locator.
1994
2033
  let _lastResolved = null
2034
+ // Implicit ARIA role for native interactive elements with no explicit `role` attribute, derived
2035
+ // from the tag (and an input's `type`). Lets the heal suggest a correct getByRole(...) — e.g. a
2036
+ // submit input becomes role 'button', not a dead getByText(...).
2037
+ function _implicitRole(tag, type, href) {
2038
+ if (tag === 'a') return href ? 'link' : null
2039
+ if (tag === 'button') return 'button'
2040
+ if (tag === 'select') return 'combobox'
2041
+ if (tag === 'textarea') return 'textbox'
2042
+ if (tag === 'input') {
2043
+ const t = (type || '').toLowerCase()
2044
+ if (t === 'submit' || t === 'button' || t === 'reset' || t === 'image') return 'button'
2045
+ if (t === 'checkbox') return 'checkbox'
2046
+ if (t === 'radio') return 'radio'
2047
+ if (t === 'range') return 'slider'
2048
+ if (t === 'number') return 'spinbutton'
2049
+ if (t === 'hidden') return null
2050
+ return 'textbox' // text/email/search/tel/url/password/date/…
2051
+ }
2052
+ return null
2053
+ }
1995
2054
  function _resolvedFacts(record) {
1996
2055
  if (!record) return null
1997
- return { tag: record.tag || '', text: (record.text || '').trim(), role: record.role || null }
2056
+ const tag = (record.tag || '').toLowerCase()
2057
+ return { tag, text: (record.text || '').trim(), role: record.role || _implicitRole(tag, record.type, record.href) }
1998
2058
  }
1999
2059
 
2000
2060
  // Implicit ARIA roles for the common interactive tags (used when the element has no explicit
@@ -2018,7 +2078,10 @@ function createElementus(userConfig = {}) {
2018
2078
  const role = resolved.role || ROLE_BY_TAG[tag] || null
2019
2079
  if (framework === 'playwright') {
2020
2080
  if (role && name) return `getByRole('${_escSingle(role)}', { name: '${_escSingle(name)}' })`
2021
- if (name) return `getByText('${_escSingle(name)}')`
2081
+ // getByText matches visible text content — never right for a form control (its label lives in
2082
+ // value/label/placeholder, not text), so fall back to a tag locator instead of a dead getByText.
2083
+ const isFormControl = tag === 'input' || tag === 'select' || tag === 'textarea'
2084
+ if (name && !isFormControl) return `getByText('${_escSingle(name)}')`
2022
2085
  return `locator('${_escSingle(tag || '*')}')`
2023
2086
  }
2024
2087
  if (framework === 'appium') {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "elementus-ai",
3
- "version": "1.5.0",
3
+ "version": "1.5.1",
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",