elementus-ai 1.1.1 → 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 +49 -0
- package/README.md +149 -1
- package/elementus.js +44 -2
- package/index.d.ts +104 -0
- package/package.json +4 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes are documented here. This project adheres to [Semantic Versioning](https://semver.org).
|
|
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
|
+
|
|
33
|
+
## 1.2.0
|
|
34
|
+
|
|
35
|
+
### Added
|
|
36
|
+
|
|
37
|
+
- Bundled TypeScript type definitions (`index.d.ts`), exposed via the package `types` field — no `@types/...` package or `declare module` shim required.
|
|
38
|
+
- Exported types: `createElementus`, `ElementusOptions`, `Elementus`, `ElementusPage`, `AiLocatorOptions`.
|
|
39
|
+
- `ElementusPage` types `page.locator(selector, { ai })` by extending Playwright's own locator options, so the `{ ai }` hint type-checks while plain locators and native options keep working.
|
|
40
|
+
- `@playwright/test` types resolve as an optional peer — WDIO/Appium-only projects don't need Playwright installed.
|
|
41
|
+
- README: new **TypeScript** section (typed fixture + Page Object Model patterns); the One-Prompt Setup is now TypeScript-aware.
|
|
42
|
+
|
|
43
|
+
### Changed
|
|
44
|
+
|
|
45
|
+
- No runtime changes. `elementus.js` is untouched; this release is type definitions and documentation only.
|
|
46
|
+
|
|
47
|
+
## 1.1.1
|
|
48
|
+
|
|
49
|
+
- Previous release.
|
package/README.md
CHANGED
|
@@ -24,6 +24,7 @@ I just installed the npm package "elementus-ai" — a self-healing element resol
|
|
|
24
24
|
- Search for: playwright.config, wdio.conf, appium config files
|
|
25
25
|
- Check package.json for: @playwright/test, playwright, webdriverio, wdio, appium
|
|
26
26
|
- Read a few existing test files to understand the test structure
|
|
27
|
+
- Note whether the project is TypeScript (tsconfig.json or .ts test files) — this changes the fixture syntax (see step 3)
|
|
27
28
|
- If none found, tell me you can't detect a supported framework and stop
|
|
28
29
|
|
|
29
30
|
2. CHOOSE THE LLM PROVIDER
|
|
@@ -33,10 +34,24 @@ I just installed the npm package "elementus-ai" — a self-healing element resol
|
|
|
33
34
|
|
|
34
35
|
3. INTEGRATE BASED ON MY FRAMEWORK
|
|
35
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
|
+
|
|
36
44
|
For Playwright:
|
|
37
|
-
- Create or update a fixtures file that
|
|
45
|
+
- Create or update a fixtures file that overrides the page fixture with el.wrapPage(page)
|
|
38
46
|
- Make sure all tests import from the fixtures file instead of @playwright/test
|
|
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
|
|
39
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
|
|
40
55
|
|
|
41
56
|
For WebDriverIO:
|
|
42
57
|
- In wdio.conf.js before hook, wrap browser and override global $:
|
|
@@ -90,6 +105,8 @@ await p.locator('#submit-btn', { ai: 'Submit order button' }).click()
|
|
|
90
105
|
await p.locator('#stable-element').click()
|
|
91
106
|
```
|
|
92
107
|
|
|
108
|
+
> **Using TypeScript or ESM?** `import { createElementus } from 'elementus-ai'` — type definitions are bundled. See [TypeScript](#typescript) for the typed fixture pattern.
|
|
109
|
+
|
|
93
110
|
## LLM Provider Setup
|
|
94
111
|
|
|
95
112
|
### Option A: Local LLM via LM Studio (free, private)
|
|
@@ -193,6 +210,55 @@ await d.$('~emailField', { ai: 'Email input' }).setValue('test@test.com')
|
|
|
193
210
|
|
|
194
211
|
Works with Flutter, React Native, native Android/iOS — any Appium driver.
|
|
195
212
|
|
|
213
|
+
## TypeScript
|
|
214
|
+
|
|
215
|
+
Type definitions are bundled — there is no `@types/elementus-ai` package to install and no `declare module` shim to write. Because `@playwright/test` is an *optional* peer, WDIO/Appium-only projects can use the types without installing Playwright.
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import { createElementus, type ElementusPage } from 'elementus-ai'
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
**Typed Playwright fixture.** `wrapPage` changes the page's runtime value but not its static type, so override the `page` fixture's type with `ElementusPage` — then `{ ai }` is recognized and autocompleted (with docs) in your tests:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
// fixtures.ts
|
|
225
|
+
import { test as base, expect } from '@playwright/test'
|
|
226
|
+
import { createElementus, type ElementusPage } from 'elementus-ai'
|
|
227
|
+
|
|
228
|
+
const el = createElementus({ provider: 'gemini', geminiApiKey: process.env.GEMINI_API_KEY })
|
|
229
|
+
|
|
230
|
+
export const test = base.extend<{ page: ElementusPage }>({
|
|
231
|
+
page: async ({ page }, use) => {
|
|
232
|
+
await use(el.wrapPage(page))
|
|
233
|
+
},
|
|
234
|
+
})
|
|
235
|
+
export { expect }
|
|
236
|
+
|
|
237
|
+
// In tests — page is already wrapped and typed:
|
|
238
|
+
test('example', async ({ page }) => {
|
|
239
|
+
await page.locator('#btn', { ai: 'Submit button' }).click() // { ai } type-checks
|
|
240
|
+
await page.locator('#btn').click() // plain locator, zero overhead
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
> The override is for editor support — IntelliSense and inline docs for `{ ai }`. It heals at runtime either way, and because Playwright's `locator()` options are permissive, `{ ai }` compiles with or without the override; the override just surfaces it as a documented option.
|
|
245
|
+
|
|
246
|
+
**Page Object Model.** Type the page your objects receive as `ElementusPage`:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { type ElementusPage } from 'elementus-ai'
|
|
250
|
+
|
|
251
|
+
abstract class BasePage {
|
|
252
|
+
constructor(protected readonly page: ElementusPage) {}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
class LoginPage extends BasePage {
|
|
256
|
+
readonly submit = this.page.locator('#submit', { ai: 'Submit button' })
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
**Exported types:** `ElementusOptions`, `Elementus`, `ElementusPage`, `AiLocatorOptions`. `AiLocatorOptions` is Playwright's own `locator()` option type plus `ai?: string`, derived from the installed Playwright version so it never drifts.
|
|
261
|
+
|
|
196
262
|
## API Reference
|
|
197
263
|
|
|
198
264
|
### `el.wrapPage(page)`
|
|
@@ -267,9 +333,91 @@ createElementus({
|
|
|
267
333
|
|
|
268
334
|
// Custom stop words
|
|
269
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)
|
|
270
340
|
})
|
|
271
341
|
```
|
|
272
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
|
+
|
|
273
421
|
## Security Notes
|
|
274
422
|
|
|
275
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
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// Type definitions for elementus-ai
|
|
2
|
+
// Project: https://github.com/Morph93/elementus
|
|
3
|
+
//
|
|
4
|
+
// Self-healing element resolution for Playwright, WebdriverIO & Appium.
|
|
5
|
+
// These types describe the Playwright/core API. WebdriverIO's global `$`
|
|
6
|
+
// augmentation lives in the separate, opt-in `wdio.d.ts`.
|
|
7
|
+
//
|
|
8
|
+
// `@playwright/test` is an OPTIONAL peer dependency. The `@ts-ignore` below lets
|
|
9
|
+
// WDIO/Appium-only consumers (who have no Playwright installed) fall back to
|
|
10
|
+
// `any` for these types instead of failing module resolution.
|
|
11
|
+
// @ts-ignore -- optional peer dependency
|
|
12
|
+
import type { Page, Locator } from '@playwright/test'
|
|
13
|
+
|
|
14
|
+
export interface ElementusOptions {
|
|
15
|
+
/** LLM provider. @default 'lmstudio' */
|
|
16
|
+
provider?: 'lmstudio' | 'gemini'
|
|
17
|
+
/** LM Studio chat-completions endpoint. @default 'http://localhost:1234/v1/chat/completions' */
|
|
18
|
+
lmStudioUrl?: string
|
|
19
|
+
/** LM Studio model name. @default 'holo-3.1-9b' */
|
|
20
|
+
model?: string
|
|
21
|
+
/** Google Gemini API key (or set the GEMINI_API_KEY env var). @default null */
|
|
22
|
+
geminiApiKey?: string | null
|
|
23
|
+
/** Gemini model id. @default 'gemini-3.5-flash' */
|
|
24
|
+
geminiModel?: string
|
|
25
|
+
/** Max elements sent to the LLM for disambiguation. @default 20 */
|
|
26
|
+
maxCandidates?: number
|
|
27
|
+
/** Save debug screenshots to `debugDir`. @default false */
|
|
28
|
+
debug?: boolean
|
|
29
|
+
/** Directory for debug screenshots (required when `debug` is true). @default null */
|
|
30
|
+
debugDir?: string | null
|
|
31
|
+
/** Custom stop words to ignore in descriptions (replaces the defaults). @default null */
|
|
32
|
+
stopWords?: Set<string> | null
|
|
33
|
+
/** Max screenshot width (px) sent to the vision LLM. @default 1280 */
|
|
34
|
+
visionMaxWidth?: number
|
|
35
|
+
/** Opt-in fingerprint cache file, e.g. './elementus-cache.json'. @default null */
|
|
36
|
+
cacheFile?: string | null
|
|
37
|
+
/** Opt-in embedding model for semantic paraphrase matching. @default null */
|
|
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 }
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Playwright's own `locator()` options, plus the Elementus `ai` hint.
|
|
66
|
+
* Derived from the installed Playwright types so it never drifts.
|
|
67
|
+
*/
|
|
68
|
+
export type AiLocatorOptions = NonNullable<Parameters<Page['locator']>[1]> & {
|
|
69
|
+
/** Natural-language description; the self-healing fallback used when `selector` breaks. */
|
|
70
|
+
ai?: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* A Playwright Page whose `locator()` also accepts `{ ai }`. Locators created
|
|
75
|
+
* with an `ai` hint self-heal when the selector breaks; locators without it are
|
|
76
|
+
* returned unchanged (zero overhead).
|
|
77
|
+
*/
|
|
78
|
+
export type ElementusPage = Page & {
|
|
79
|
+
locator(selector: string, options?: AiLocatorOptions): Locator
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface Elementus {
|
|
83
|
+
/**
|
|
84
|
+
* Wrap a Playwright Page so `page.locator(selector, { ai })` self-heals.
|
|
85
|
+
* Call once per test, or in a fixture for the whole suite.
|
|
86
|
+
*/
|
|
87
|
+
wrapPage(page: Page): ElementusPage
|
|
88
|
+
/**
|
|
89
|
+
* Wrap a WebdriverIO/Appium browser so `$(selector, { ai })` self-heals.
|
|
90
|
+
* Returns the same object it was given (now AI-aware).
|
|
91
|
+
*/
|
|
92
|
+
wrapBrowser<T>(browser: T): T
|
|
93
|
+
/** Try `locator` first; fall back to AI resolution if it fails. */
|
|
94
|
+
locate(ctx: Page, locator: Locator, description: string): Promise<Locator>
|
|
95
|
+
/** Resolve an element from a natural-language description alone. */
|
|
96
|
+
find(ctx: Page, description: string): Promise<Locator>
|
|
97
|
+
/** Click with an optimized fallback (goto for links, JS click for buttons). */
|
|
98
|
+
click(ctx: Page, locator: Locator, description: string): Promise<void>
|
|
99
|
+
/** Low-level: wrap a single locator with AI fallback. Prefer wrapPage(). */
|
|
100
|
+
wrap(ctx: Page, locator: Locator, description: string): Locator
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Create an Elementus instance with the given configuration. */
|
|
104
|
+
export function createElementus(options?: ElementusOptions): Elementus
|
package/package.json
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "elementus-ai",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.0",
|
|
4
4
|
"description": "Self-healing element resolution for Playwright, WDIO & Appium. AI-powered fallback when selectors break.",
|
|
5
5
|
"main": "elementus.js",
|
|
6
|
+
"types": "index.d.ts",
|
|
6
7
|
"scripts": {
|
|
7
8
|
"test": "playwright test test/playwright.spec.js",
|
|
8
9
|
"test:smoke": "playwright test test/playwright.spec.js -g \"T01 |T02 |T09 |T17 |T23 \""
|
|
@@ -51,8 +52,10 @@
|
|
|
51
52
|
},
|
|
52
53
|
"files": [
|
|
53
54
|
"elementus.js",
|
|
55
|
+
"index.d.ts",
|
|
54
56
|
"wdio.d.ts",
|
|
55
57
|
"README.md",
|
|
58
|
+
"CHANGELOG.md",
|
|
56
59
|
"LICENSE"
|
|
57
60
|
]
|
|
58
61
|
}
|