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 +18 -0
- package/elementus.js +70 -7
- package/package.json +1 -1
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'
|
|
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'
|
|
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'
|
|
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'
|
|
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
|
-
|
|
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
|
-
|
|
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