playwright-healing-locators 1.0.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/.github/workflows/playwright.yml +27 -0
- package/README.md +177 -0
- package/package.json +48 -0
- package/playwright-healing-locators-1.0.0.tgz +0 -0
- package/playwright.config.js +102 -0
- package/src/core/autoHeal.js +111 -0
- package/src/core/healer.js +70 -0
- package/src/core/logger.js +21 -0
- package/src/core/strategies.js +64 -0
- package/src/index.js +16 -0
- package/src/modes/auto.js +98 -0
- package/src/modes/regular.js +44 -0
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
name: Playwright Tests
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [ main, master ]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [ main, master ]
|
|
7
|
+
jobs:
|
|
8
|
+
test:
|
|
9
|
+
timeout-minutes: 60
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
steps:
|
|
12
|
+
- uses: actions/checkout@v5
|
|
13
|
+
- uses: actions/setup-node@v5
|
|
14
|
+
with:
|
|
15
|
+
node-version: lts/*
|
|
16
|
+
- name: Install dependencies
|
|
17
|
+
run: npm ci
|
|
18
|
+
- name: Install Playwright Browsers
|
|
19
|
+
run: npx playwright install --with-deps
|
|
20
|
+
- name: Run Playwright tests
|
|
21
|
+
run: npx playwright test
|
|
22
|
+
- uses: actions/upload-artifact@v5
|
|
23
|
+
if: ${{ !cancelled() }}
|
|
24
|
+
with:
|
|
25
|
+
name: playwright-report
|
|
26
|
+
path: playwright-report/
|
|
27
|
+
retention-days: 30
|
package/README.md
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
# 🎭 Playwright Healing Locators
|
|
2
|
+
|
|
3
|
+
A robust, easy-to-use helper library for Playwright that improves test stability by automatically resolving broken selectors.
|
|
4
|
+
|
|
5
|
+
When web UI structure changes are frequent (class renaming, attribute updates, dynamic ids), normal selectors break tests. This package helps by:
|
|
6
|
+
|
|
7
|
+
- ✅ Trying a second locator when primary fails (`regularHeal`)
|
|
8
|
+
- 🤖 Detecting similar elements based on selector keywords (`autoHeal`)
|
|
9
|
+
- 🎯 Supporting common locator methods (CSS, XPath, text, ARIA, role)
|
|
10
|
+
- 👀 Only continuing once a visible element is found, reducing false positives
|
|
11
|
+
|
|
12
|
+
## 🔍 The Problem & Solution
|
|
13
|
+
|
|
14
|
+
### ⚠️ Common Problem: Brittle selectors break tests
|
|
15
|
+
- UI changes happen all the time (CSS classes change, DOM structure shifts, IDs get regenerated, attributes are refactored).
|
|
16
|
+
- Traditional Playwright selectors (`page.locator('#x')`, `getByRole...`) fail immediately if the exact path is gone.
|
|
17
|
+
- Teams spend time constantly updating tests, creating flakiness and maintenance debt.
|
|
18
|
+
|
|
19
|
+
### 💡 Why This Package is Needed
|
|
20
|
+
- It reduces **fast failure** when the first selector is invalid.
|
|
21
|
+
- It avoids **unnecessary rework** by attempting recovery first.
|
|
22
|
+
- It gives a **safe second chance** to selectors without manual intervention.
|
|
23
|
+
|
|
24
|
+
### 🎉 What We Improved After Using This Package
|
|
25
|
+
- ✨ **Resilient tests**: a bad primary selector can change to fallback instead of failing test.
|
|
26
|
+
- 🔧 **Less maintenance**: one failing selector does not require immediate test patching.
|
|
27
|
+
- 🛡️ **Better coverage**: both normal (`regularHeal`) and self-healing (`autoHeal`) paths are protected.
|
|
28
|
+
- 🔐 **Higher confidence**: tests now verify actual element exists before action (`waitFor visible`).
|
|
29
|
+
- 📊 **Faster debugging**: healing logs show which attempt worked.
|
|
30
|
+
|
|
31
|
+
## ⭐ Features
|
|
32
|
+
|
|
33
|
+
- `regularHeal`: manual fallback locators (primary + fallback array)
|
|
34
|
+
- `autoHeal`: automatic healing by keyword matching and candidate scoring
|
|
35
|
+
- Supports CSS, XPath, text, ARIA label, and role-based selectors
|
|
36
|
+
- Built-in retry with visibility wait
|
|
37
|
+
|
|
38
|
+
## 📦 Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
npm install playwright-healing-locators
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## 🚀 Usage
|
|
45
|
+
|
|
46
|
+
```js
|
|
47
|
+
import { test, expect } from '@playwright/test';
|
|
48
|
+
import { regularHeal, autoHeal } from 'playwright-healing-locators';
|
|
49
|
+
|
|
50
|
+
test('Regular Healing with fallback', async ({ page }) => {
|
|
51
|
+
await page.goto('https://practicetestautomation.com/practice-test-login/');
|
|
52
|
+
|
|
53
|
+
await regularHeal.fill(page, {
|
|
54
|
+
primary: '#wrong-username',
|
|
55
|
+
fallbacks: [
|
|
56
|
+
{ type: 'role', value: 'textbox', name: 'Username' }
|
|
57
|
+
]
|
|
58
|
+
}, 'student');
|
|
59
|
+
|
|
60
|
+
await regularHeal.fill(page, {
|
|
61
|
+
primary: '#wrong-password',
|
|
62
|
+
fallbacks: [
|
|
63
|
+
{ type: 'role', value: 'textbox', name: 'Password' }
|
|
64
|
+
]
|
|
65
|
+
}, 'Password123');
|
|
66
|
+
|
|
67
|
+
await regularHeal.click(page, {
|
|
68
|
+
primary: '#wrong-submit',
|
|
69
|
+
fallbacks: [
|
|
70
|
+
{ type: 'role', value: 'button', name: 'Submit' }
|
|
71
|
+
]
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
await expect(page.getByText('Logged In Successfully')).toBeVisible();
|
|
75
|
+
});
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
```js
|
|
79
|
+
test('Auto Healing with keyword-based candidate search', async ({ page }) => {
|
|
80
|
+
await page.goto('https://practicetestautomation.com/practice-test-login/');
|
|
81
|
+
|
|
82
|
+
await autoHeal.fill(page, {
|
|
83
|
+
primary: '#username-field-invalid'
|
|
84
|
+
}, 'student');
|
|
85
|
+
|
|
86
|
+
await autoHeal.fill(page, {
|
|
87
|
+
primary: '#password-field-invalid'
|
|
88
|
+
}, 'Password123');
|
|
89
|
+
|
|
90
|
+
await autoHeal.click(page, {
|
|
91
|
+
primary: '#submit-btn-invalid'
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
await expect(page.getByText('Logged In Successfully')).toBeVisible();
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## 📚 API
|
|
99
|
+
|
|
100
|
+
### ✍️ `regularHeal.fill(page, options, value)`
|
|
101
|
+
- `page`: Playwright page
|
|
102
|
+
- `options.primary`: CSS/XPath primary selector
|
|
103
|
+
- `options.fallbacks`: Array of fallback { type, value, name? }
|
|
104
|
+
- `value`: text to fill
|
|
105
|
+
|
|
106
|
+
### 🖱️ `regularHeal.click(page, options)`
|
|
107
|
+
- `page`: Playwright page
|
|
108
|
+
- `options.primary`: CSS/XPath primary selector
|
|
109
|
+
- `options.fallbacks`: Array of fallback { type, value, name? }
|
|
110
|
+
|
|
111
|
+
### ✍️ `autoHeal.fill(page, options, value)`
|
|
112
|
+
- `page`: Playwright page
|
|
113
|
+
- `options.primary`: primary selector to auto-heal from
|
|
114
|
+
- `value`: text to fill
|
|
115
|
+
|
|
116
|
+
### 🖱️ `autoHeal.click(page, options)`
|
|
117
|
+
- `page`: Playwright page
|
|
118
|
+
- `options.primary`: primary selector to auto-heal from
|
|
119
|
+
|
|
120
|
+
## 🤝 Contribution
|
|
121
|
+
|
|
122
|
+
1. 🍴 Fork repository
|
|
123
|
+
2. 🌿 Create feature branch
|
|
124
|
+
3. ✅ Add tests in `tests/*.spec.ts`
|
|
125
|
+
4. 📤 Submit PR
|
|
126
|
+
|
|
127
|
+
## ⚠️ Limitations & Known Issues
|
|
128
|
+
|
|
129
|
+
While this package improves test resilience, be aware of these considerations:
|
|
130
|
+
|
|
131
|
+
### ⏱️ Performance Impact
|
|
132
|
+
- `autoHeal` searches the DOM for candidate elements, which may be slower on large/complex pages
|
|
133
|
+
- Multiple fallback attempts in `regularHeal` add execution time
|
|
134
|
+
- Consider using `regularHeal` for known stable fallbacks vs `autoHeal` for unpredictable UIs
|
|
135
|
+
|
|
136
|
+
### 🎯 Accuracy Risks
|
|
137
|
+
- `autoHeal` keyword matching might select incorrect elements if multiple similar elements exist
|
|
138
|
+
- Always verify healing results in test reports and adjust primary selectors when possible
|
|
139
|
+
- False positives possible if element attributes match but context differs
|
|
140
|
+
|
|
141
|
+
### 🔄 Maintenance Overhead
|
|
142
|
+
- `regularHeal` requires manual fallback definitions that need updates when UI changes
|
|
143
|
+
- Auto-healing might mask underlying selector problems that should be fixed
|
|
144
|
+
- Regular review of healing logs recommended to identify patterns
|
|
145
|
+
|
|
146
|
+
### 🐛 Debugging Challenges
|
|
147
|
+
- When healing fails completely, error messages may not pinpoint the exact issue
|
|
148
|
+
- Healing logs help but require interpretation
|
|
149
|
+
- Complex DOM structures may confuse keyword extraction
|
|
150
|
+
|
|
151
|
+
### 🌐 Browser Compatibility
|
|
152
|
+
- Tested on Chromium, Firefox, and WebKit as configured
|
|
153
|
+
- XPath support may vary slightly between browsers
|
|
154
|
+
- Mobile testing not yet validated
|
|
155
|
+
|
|
156
|
+
### 📦 Dependencies
|
|
157
|
+
- Requires Playwright `^1.59.1` (may work with newer but not guaranteed)
|
|
158
|
+
- ES modules required (not CommonJS compatible)
|
|
159
|
+
|
|
160
|
+
## 💻 Best Practices
|
|
161
|
+
|
|
162
|
+
- Use `regularHeal` for predictable, stable fallbacks
|
|
163
|
+
- Use `autoHeal` for dynamic content or when exact selectors are unreliable
|
|
164
|
+
- Monitor healing logs to identify frequently failing selectors
|
|
165
|
+
- Combine with good locator strategies (prefer roles/ARIA over fragile CSS)
|
|
166
|
+
- Don't rely solely on healing - fix underlying UI issues when possible
|
|
167
|
+
|
|
168
|
+
## 📝 Notes
|
|
169
|
+
|
|
170
|
+
- Use a stable version of Playwright `^1.59.1`.
|
|
171
|
+
- Ensure `playwright.config.js` uses `reporter: 'html'` if you need HTML test reports.
|
|
172
|
+
|
|
173
|
+
## 📄 License
|
|
174
|
+
|
|
175
|
+
This project is released under the **ISC License** - a permissive open-source license suitable for commercial and open-source projects.
|
|
176
|
+
|
|
177
|
+
See the [LICENSE](LICENSE) file for details.
|
package/package.json
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "playwright-healing-locators",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "A robust helper library for Playwright that improves test stability by automatically resolving broken selectors through fallback strategies and auto-healing.",
|
|
5
|
+
"main": "src/index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"test": "npx playwright test",
|
|
9
|
+
"test:headed": "npx playwright test --headed",
|
|
10
|
+
"report": "npx playwright show-report"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"playwright",
|
|
14
|
+
"testing",
|
|
15
|
+
"automation",
|
|
16
|
+
"selectors",
|
|
17
|
+
"healing",
|
|
18
|
+
"resilient",
|
|
19
|
+
"fallback",
|
|
20
|
+
"auto-heal",
|
|
21
|
+
"test-stability",
|
|
22
|
+
"e2e-testing"
|
|
23
|
+
],
|
|
24
|
+
"author": "Iniyavan",
|
|
25
|
+
"license": "ISC",
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "https://github.com/iniyavans/playwright-healing-locators.git"
|
|
29
|
+
},
|
|
30
|
+
"homepage": "https://github.com/iniyavans/playwright-healing-locators#readme",
|
|
31
|
+
"bugs": {
|
|
32
|
+
"url": "https://github.com/iniyavans/playwright-healing-locators/issues"
|
|
33
|
+
},
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=16.0.0"
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"@playwright/test": "^1.59.0"
|
|
39
|
+
},
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"playwright": "^1.59.1"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@playwright/test": "^1.59.1",
|
|
45
|
+
"@types/node": "^25.5.0",
|
|
46
|
+
"playwright": "^1.59.1"
|
|
47
|
+
}
|
|
48
|
+
}
|
|
Binary file
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
// TypeScript type checking for this config file
|
|
2
|
+
// @ts-check
|
|
3
|
+
|
|
4
|
+
// Import Playwright's configuration helper and device presets
|
|
5
|
+
import { defineConfig, devices } from '@playwright/test';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Optional: Read environment variables from a .env file.
|
|
9
|
+
* Uncomment the lines below if you want to use environment variables.
|
|
10
|
+
* https://github.com/motdotla/dotenv
|
|
11
|
+
*/
|
|
12
|
+
// import dotenv from 'dotenv';
|
|
13
|
+
// import path from 'path';
|
|
14
|
+
// dotenv.config({ path: path.resolve(__dirname, '.env') });
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Playwright test configuration
|
|
18
|
+
* This file configures how Playwright runs your tests.
|
|
19
|
+
* @see https://playwright.dev/docs/test-configuration
|
|
20
|
+
*/
|
|
21
|
+
export default defineConfig({
|
|
22
|
+
// Directory where test files are located
|
|
23
|
+
testDir: './tests',
|
|
24
|
+
|
|
25
|
+
/* Run tests in files in parallel for faster execution */
|
|
26
|
+
fullyParallel: true,
|
|
27
|
+
|
|
28
|
+
/* Fail the build on CI if you accidentally left test.only in the source code.
|
|
29
|
+
This prevents focused tests from being committed to CI. */
|
|
30
|
+
forbidOnly: !!process.env.CI,
|
|
31
|
+
|
|
32
|
+
/* Retry failed tests: only retry on CI, not locally */
|
|
33
|
+
retries: process.env.CI ? 2 : 0,
|
|
34
|
+
|
|
35
|
+
/* Limit workers on CI to avoid resource conflicts.
|
|
36
|
+
Locally, use undefined to let Playwright decide. */
|
|
37
|
+
workers: process.env.CI ? 1 : undefined,
|
|
38
|
+
|
|
39
|
+
/* Reporter to use: 'html' generates a visual report */
|
|
40
|
+
reporter: 'html',
|
|
41
|
+
|
|
42
|
+
/* Shared settings for all test projects */
|
|
43
|
+
use: {
|
|
44
|
+
/* Base URL to use in actions like `await page.goto('')`.
|
|
45
|
+
Uncomment and set if all tests use the same domain. */
|
|
46
|
+
// baseURL: 'http://localhost:3000',
|
|
47
|
+
|
|
48
|
+
/* Collect trace when retrying the failed test.
|
|
49
|
+
Traces help debug failures. See https://playwright.dev/docs/trace-viewer */
|
|
50
|
+
trace: 'on-first-retry',
|
|
51
|
+
},
|
|
52
|
+
|
|
53
|
+
/* Configure projects for different browsers */
|
|
54
|
+
projects: [
|
|
55
|
+
{
|
|
56
|
+
// Test configuration for Google Chrome desktop browser
|
|
57
|
+
name: 'chromium',
|
|
58
|
+
use: { ...devices['Desktop Chrome'] },
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
// {
|
|
62
|
+
// // Test configuration for Mozilla Firefox desktop browser
|
|
63
|
+
// name: 'firefox',
|
|
64
|
+
// use: { ...devices['Desktop Firefox'] },
|
|
65
|
+
// },
|
|
66
|
+
|
|
67
|
+
// {
|
|
68
|
+
// // Test configuration for Apple Safari desktop browser
|
|
69
|
+
// name: 'webkit',
|
|
70
|
+
// use: { ...devices['Desktop Safari'] },
|
|
71
|
+
// },
|
|
72
|
+
|
|
73
|
+
/* Test against mobile viewports. */
|
|
74
|
+
// {
|
|
75
|
+
// name: 'Mobile Chrome',
|
|
76
|
+
// use: { ...devices['Pixel 5'] },
|
|
77
|
+
// },
|
|
78
|
+
// {
|
|
79
|
+
// name: 'Mobile Safari',
|
|
80
|
+
// use: { ...devices['iPhone 12'] },
|
|
81
|
+
// },
|
|
82
|
+
|
|
83
|
+
/* Test against branded browsers. */
|
|
84
|
+
// {
|
|
85
|
+
// name: 'Microsoft Edge',
|
|
86
|
+
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
|
|
87
|
+
// },
|
|
88
|
+
// {
|
|
89
|
+
// name: 'Google Chrome',
|
|
90
|
+
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
|
|
91
|
+
// },
|
|
92
|
+
],
|
|
93
|
+
|
|
94
|
+
/* Run your local dev server before starting the tests.
|
|
95
|
+
Uncomment and configure if you need to start a local server for testing. */
|
|
96
|
+
// webServer: {
|
|
97
|
+
// command: 'npm run start', // Command to start the server
|
|
98
|
+
// url: 'http://localhost:3000', // URL to wait for before running tests
|
|
99
|
+
// reuseExistingServer: !process.env.CI, // Reuse server if already running (not on CI)
|
|
100
|
+
// },
|
|
101
|
+
});
|
|
102
|
+
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Healing Utilities Module
|
|
3
|
+
* This module contains functions for automatically finding alternative elements
|
|
4
|
+
* when a selector fails. It extracts keywords from selectors and searches the DOM
|
|
5
|
+
* for similar elements that might be the intended target.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts meaningful keywords from a selector string for auto-healing.
|
|
10
|
+
* Supports both CSS and XPath selectors by parsing them differently.
|
|
11
|
+
* @param {string} selector - The CSS or XPath selector to analyze
|
|
12
|
+
* @returns {string[]} Array of lowercase keywords extracted from the selector
|
|
13
|
+
*/
|
|
14
|
+
function extractKeywords(selector) {
|
|
15
|
+
// Check if this is an XPath selector (starts with / or //)
|
|
16
|
+
if (selector.startsWith('/') || selector.startsWith('//')) {
|
|
17
|
+
// XPath parsing logic
|
|
18
|
+
const keywords = [];
|
|
19
|
+
|
|
20
|
+
// Extract tag names like 'input', 'button' from //tagName
|
|
21
|
+
const tagMatch = selector.match(/\/\/(\w+)/);
|
|
22
|
+
if (tagMatch) keywords.push(tagMatch[1]);
|
|
23
|
+
|
|
24
|
+
// Extract attribute values like @id='username' -> 'username'
|
|
25
|
+
const attrMatches = selector.match(/@[\w-]+='([^']+)'/g);
|
|
26
|
+
if (attrMatches) {
|
|
27
|
+
attrMatches.forEach(match => {
|
|
28
|
+
const value = match.match(/'([^']+)'/)?.[1];
|
|
29
|
+
if (value) keywords.push(value);
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Extract text content like text()='Submit' -> 'Submit'
|
|
34
|
+
const textMatch = selector.match(/text\(\)='([^']+)'/);
|
|
35
|
+
if (textMatch) keywords.push(textMatch[1]);
|
|
36
|
+
|
|
37
|
+
// Convert all keywords to lowercase for case-insensitive matching
|
|
38
|
+
return keywords.map(word => word.toLowerCase());
|
|
39
|
+
} else {
|
|
40
|
+
// CSS parsing logic
|
|
41
|
+
return selector
|
|
42
|
+
.replace(/[#._-]/g, ' ') // Replace special chars with spaces
|
|
43
|
+
.split(' ') // Split into words
|
|
44
|
+
.filter(Boolean) // Remove empty strings
|
|
45
|
+
.map(word => word.toLowerCase()); // Convert to lowercase
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Finds candidate elements in the DOM that match the extracted keywords.
|
|
51
|
+
* Searches for elements likely to be interactive based on the intended action.
|
|
52
|
+
* @param {Page} page - The Playwright page object
|
|
53
|
+
* @param {string[]} keywords - Keywords extracted from the failed selector
|
|
54
|
+
* @param {string} [action='click'] - The intended action ('click' or 'fill') to optimize search
|
|
55
|
+
* @returns {Locator[]} Array of candidate element locators, sorted by relevance
|
|
56
|
+
*/
|
|
57
|
+
async function findCandidates(page, keywords, action = 'click') {
|
|
58
|
+
let selector;
|
|
59
|
+
|
|
60
|
+
// Choose different element types based on the intended action
|
|
61
|
+
if (action === 'fill') {
|
|
62
|
+
// For filling, look for input elements that can receive text
|
|
63
|
+
selector = 'input, textarea, select';
|
|
64
|
+
} else {
|
|
65
|
+
// For clicking, look for clickable elements
|
|
66
|
+
selector = 'button, a, input[type="button"], input[type="submit"]';
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Get all matching elements on the page
|
|
70
|
+
const elements = await page.locator(selector).all();
|
|
71
|
+
|
|
72
|
+
const matches = [];
|
|
73
|
+
|
|
74
|
+
// Evaluate each element to see how well it matches our keywords
|
|
75
|
+
for (const el of elements) {
|
|
76
|
+
// Get the visible text content (if any)
|
|
77
|
+
const text = (await el.innerText().catch(() => '')).toLowerCase();
|
|
78
|
+
// Get the aria-label attribute (if any)
|
|
79
|
+
const aria = (await el.getAttribute('aria-label') || '').toLowerCase();
|
|
80
|
+
// Get the name attribute (common for form inputs)
|
|
81
|
+
const name = (await el.getAttribute('name') || '').toLowerCase();
|
|
82
|
+
// Get the id attribute
|
|
83
|
+
const id = (await el.getAttribute('id') || '').toLowerCase();
|
|
84
|
+
// Get the placeholder attribute (for inputs)
|
|
85
|
+
const placeholder = (await el.getAttribute('placeholder') || '').toLowerCase();
|
|
86
|
+
|
|
87
|
+
let score = 0; // Relevance score for this element
|
|
88
|
+
|
|
89
|
+
// Check each keyword against various element properties
|
|
90
|
+
for (const key of keywords) {
|
|
91
|
+
if (text.includes(key)) score += 2; // Text content is most important
|
|
92
|
+
if (aria.includes(key)) score += 1; // ARIA label is helpful
|
|
93
|
+
if (name.includes(key)) score += 1; // Name attribute
|
|
94
|
+
if (id.includes(key)) score += 1; // ID attribute
|
|
95
|
+
if (placeholder.includes(key)) score += 1; // Placeholder text
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// If the element has any matching keywords, add it to candidates
|
|
99
|
+
if (score > 0) {
|
|
100
|
+
matches.push({ el, score });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Sort candidates by score (highest first) and return just the elements
|
|
105
|
+
matches.sort((a, b) => b.score - a.score);
|
|
106
|
+
|
|
107
|
+
return matches.map(m => m.el);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Export the functions for use in other modules
|
|
111
|
+
export { extractKeywords, findCandidates };
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core Healing Logic Module
|
|
3
|
+
* This module contains the fundamental healing algorithms used by both regular and auto modes.
|
|
4
|
+
* It handles the process of trying multiple locators until one works.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Import functions for resolving locators and detecting selector types
|
|
8
|
+
import { resolveLocator, detectSelectorType } from './strategies.js';
|
|
9
|
+
// Import logging utility
|
|
10
|
+
import { log } from './logger.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Core healing function that tries multiple locators in sequence.
|
|
14
|
+
* @param {Page} page - The Playwright page object
|
|
15
|
+
* @param {Object} options - Healing configuration
|
|
16
|
+
* @param {string} options.primary - The primary selector to try first
|
|
17
|
+
* @param {Array} options.fallbacks - Array of fallback selector configurations
|
|
18
|
+
* @param {number} [options.timeout=2000] - Timeout in ms for each locator attempt
|
|
19
|
+
* @param {boolean} [options.log=true] - Whether to enable logging
|
|
20
|
+
* @returns {Locator} The first working locator found
|
|
21
|
+
* @throws {Error} If no locators work
|
|
22
|
+
*/
|
|
23
|
+
async function regularHeal(page, options) {
|
|
24
|
+
// Extract configuration options with default values
|
|
25
|
+
const {
|
|
26
|
+
primary, // The main selector to try
|
|
27
|
+
fallbacks = [], // Additional selectors to try if primary fails
|
|
28
|
+
timeout = 2000, // How long to wait for each selector
|
|
29
|
+
log: enableLog = true // Whether to show progress logs
|
|
30
|
+
} = options;
|
|
31
|
+
|
|
32
|
+
// Create an array of all locator attempts, starting with the primary
|
|
33
|
+
// Automatically detect if primary is CSS or XPath
|
|
34
|
+
const attempts = [
|
|
35
|
+
{ type: detectSelectorType(primary), value: primary },
|
|
36
|
+
...fallbacks // Add all fallback locators
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
// Try each locator in order until one works
|
|
40
|
+
for (let attempt of attempts) {
|
|
41
|
+
try {
|
|
42
|
+
// Log which locator we're trying
|
|
43
|
+
log(`Trying: ${JSON.stringify(attempt)}`, enableLog);
|
|
44
|
+
|
|
45
|
+
// Convert the locator configuration to a Playwright locator
|
|
46
|
+
const locator = resolveLocator(page, attempt);
|
|
47
|
+
|
|
48
|
+
// Wait for the element to be visible (this will throw if not found/visible)
|
|
49
|
+
await locator.waitFor({
|
|
50
|
+
state: 'visible',
|
|
51
|
+
timeout
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// Success! Log and return the working locator
|
|
55
|
+
log(`✅ Success`, enableLog);
|
|
56
|
+
|
|
57
|
+
return locator;
|
|
58
|
+
|
|
59
|
+
} catch (err) {
|
|
60
|
+
// This locator failed, log the failure and try the next one
|
|
61
|
+
log(`❌ Failed`, enableLog);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// All locators failed, throw an error with details
|
|
66
|
+
throw new Error('Regular mode failed: All locators failed');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Export the main healing function
|
|
70
|
+
export { regularHeal };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Logging Utility Module
|
|
3
|
+
* Provides a simple logging function for the healing process.
|
|
4
|
+
* All log messages are prefixed with '[Healing]' for easy identification.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Logs a message to the console if logging is enabled.
|
|
9
|
+
* @param {string} message - The message to log
|
|
10
|
+
* @param {boolean} [enabled=true] - Whether to actually log the message
|
|
11
|
+
*/
|
|
12
|
+
function log(message, enabled = true) {
|
|
13
|
+
// Only log if logging is enabled (default is true)
|
|
14
|
+
if (enabled) {
|
|
15
|
+
// Prefix all messages with '[Healing]' for easy filtering
|
|
16
|
+
console.log(`[Healing] ${message}`);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Export the logging function
|
|
21
|
+
export { log };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Locator Strategies Module
|
|
3
|
+
* This module handles different types of element selectors and converts them
|
|
4
|
+
* to Playwright locators. It supports CSS, XPath, text, ARIA labels, and roles.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Detects the type of selector string (CSS or XPath).
|
|
9
|
+
* @param {string} selector - The selector string to analyze
|
|
10
|
+
* @returns {string} 'xpath' if it starts with '/' or '//', otherwise 'css'
|
|
11
|
+
*/
|
|
12
|
+
function detectSelectorType(selector) {
|
|
13
|
+
// XPath selectors always start with '/' or '//'
|
|
14
|
+
if (selector.startsWith('/') || selector.startsWith('//')) {
|
|
15
|
+
return 'xpath';
|
|
16
|
+
}
|
|
17
|
+
// Default to CSS for all other selectors
|
|
18
|
+
// Could be extended to detect other types like data-testid, etc.
|
|
19
|
+
return 'css';
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Converts a strategy configuration to a Playwright locator.
|
|
24
|
+
* @param {Page} page - The Playwright page object
|
|
25
|
+
* @param {Object} strategy - The locator strategy configuration
|
|
26
|
+
* @param {string} strategy.type - The type of locator ('css', 'xpath', 'text', 'aria', 'role')
|
|
27
|
+
* @param {string} strategy.value - The selector value
|
|
28
|
+
* @param {string} [strategy.name] - For role strategies, the accessible name
|
|
29
|
+
* @returns {Locator} A Playwright locator object
|
|
30
|
+
* @throws {Error} If the strategy type is not supported
|
|
31
|
+
*/
|
|
32
|
+
function resolveLocator(page, strategy) {
|
|
33
|
+
// Use a switch statement to handle different locator types
|
|
34
|
+
switch (strategy.type) {
|
|
35
|
+
case 'css':
|
|
36
|
+
// Standard CSS selector like '#id', '.class', 'input[type="text"]'
|
|
37
|
+
return page.locator(strategy.value);
|
|
38
|
+
|
|
39
|
+
case 'xpath':
|
|
40
|
+
// XPath selector like '//input[@id="username"]'
|
|
41
|
+
return page.locator(strategy.value);
|
|
42
|
+
|
|
43
|
+
case 'text':
|
|
44
|
+
// Find element by its visible text content
|
|
45
|
+
return page.getByText(strategy.value);
|
|
46
|
+
|
|
47
|
+
case 'aria':
|
|
48
|
+
// Find element by its aria-label attribute
|
|
49
|
+
return page.getByLabel(strategy.value);
|
|
50
|
+
|
|
51
|
+
case 'role':
|
|
52
|
+
// Find element by ARIA role and accessible name
|
|
53
|
+
return page.getByRole(strategy.value, {
|
|
54
|
+
name: strategy.name // The accessible name for the role
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
default:
|
|
58
|
+
// Unknown strategy type, throw an error with details
|
|
59
|
+
throw new Error(`Unsupported strategy: ${JSON.stringify(strategy)}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Export the functions for use in other modules
|
|
64
|
+
export { resolveLocator, detectSelectorType };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Main entry point for the Playwright Healing Locators library.
|
|
3
|
+
* This library provides self-healing locator strategies for Playwright tests,
|
|
4
|
+
* allowing tests to automatically find alternative selectors when the primary one fails.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Import the regular healing mode module, which provides fill and click methods with fallback support
|
|
8
|
+
import * as regular from './modes/regular.js';
|
|
9
|
+
|
|
10
|
+
// Import the auto-healing mode module, which provides intelligent self-healing without manual fallbacks
|
|
11
|
+
import * as auto from './modes/auto.js';
|
|
12
|
+
|
|
13
|
+
// Export the healing modules with descriptive names
|
|
14
|
+
// regularHeal: Use this for tests where you want to specify fallback locators manually
|
|
15
|
+
// autoHeal: Use this for tests where you want automatic healing based on element analysis
|
|
16
|
+
export { regular as regularHeal, auto as autoHeal };
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-Healing Mode Module
|
|
3
|
+
* This module provides intelligent, automatic healing for Playwright locators.
|
|
4
|
+
* When a selector fails, it analyzes the selector to extract keywords and finds
|
|
5
|
+
* similar elements on the page that might be the intended target.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
// Import the core healing function for basic locator resolution
|
|
9
|
+
import { regularHeal } from '../core/healer.js';
|
|
10
|
+
// Import functions for keyword extraction and candidate finding
|
|
11
|
+
import { extractKeywords, findCandidates } from '../core/autoHeal.js';
|
|
12
|
+
// Import logging utility for debugging and progress tracking
|
|
13
|
+
import { log } from '../core/logger.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Core auto-healing logic that tries to find a working locator automatically.
|
|
17
|
+
* @param {Page} page - The Playwright page object
|
|
18
|
+
* @param {Object} options - Configuration object
|
|
19
|
+
* @param {string} options.primary - The failed selector to analyze
|
|
20
|
+
* @param {number} [options.timeout=2000] - Timeout for element waiting
|
|
21
|
+
* @param {boolean} [options.log=true] - Whether to log healing process
|
|
22
|
+
* @param {string} [action='click'] - The intended action ('click' or 'fill') to optimize candidate search
|
|
23
|
+
* @returns {Locator} A working Playwright locator
|
|
24
|
+
* @throws {Error} If no suitable element can be found
|
|
25
|
+
*/
|
|
26
|
+
async function autoHealLocator(page, options, action = 'click') {
|
|
27
|
+
try {
|
|
28
|
+
// First, try the primary locator without any fallbacks
|
|
29
|
+
return await regularHeal(page, { ...options, fallbacks: [] });
|
|
30
|
+
|
|
31
|
+
} catch (err) {
|
|
32
|
+
// Primary locator failed, start auto-healing process
|
|
33
|
+
log('🤖 Auto-healing started...', options.log);
|
|
34
|
+
|
|
35
|
+
// Extract meaningful keywords from the failed selector
|
|
36
|
+
const keywords = extractKeywords(options.primary);
|
|
37
|
+
log(`Keywords: ${keywords.join(', ')}`, options.log);
|
|
38
|
+
|
|
39
|
+
// Find potential matching elements based on the keywords
|
|
40
|
+
const candidates = await findCandidates(page, keywords, action);
|
|
41
|
+
|
|
42
|
+
// Try the top candidates (limited to 4 attempts to avoid too many tries)
|
|
43
|
+
for (let i = 0; i < candidates.length; i++) {
|
|
44
|
+
if (i > 3) break; // Limit attempts to prevent excessive waiting
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
const el = candidates[i];
|
|
48
|
+
|
|
49
|
+
// Wait for the candidate element to be visible
|
|
50
|
+
await el.waitFor({
|
|
51
|
+
state: 'visible',
|
|
52
|
+
timeout: 2000
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
// Success! Log and return the working element
|
|
56
|
+
log(`✅ Auto-healed using candidate ${i + 1}`, options.log);
|
|
57
|
+
|
|
58
|
+
return el;
|
|
59
|
+
|
|
60
|
+
} catch (e) {
|
|
61
|
+
// This candidate didn't work, try the next one
|
|
62
|
+
log(`❌ Candidate ${i + 1} failed`, options.log);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// All candidates failed, throw an error
|
|
67
|
+
throw new Error('Auto-healing failed: No suitable element found');
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Performs a click action using auto-healing strategy.
|
|
73
|
+
* @param {Page} page - The Playwright page object
|
|
74
|
+
* @param {Object} options - Configuration object with primary selector
|
|
75
|
+
*/
|
|
76
|
+
async function click(page, options) {
|
|
77
|
+
const locator = await autoHealLocator(page, options, 'click');
|
|
78
|
+
await locator.click();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Performs a fill action using auto-healing strategy.
|
|
83
|
+
* @param {Page} page - The Playwright page object
|
|
84
|
+
* @param {Object} options - Configuration object with primary selector
|
|
85
|
+
* @param {string} options.primary - The selector to try (will auto-heal if it fails)
|
|
86
|
+
* @param {number} [options.timeout=2000] - Timeout for element waiting
|
|
87
|
+
* @param {boolean} [options.log=true] - Whether to log the healing process
|
|
88
|
+
* @param {string} value - The text value to fill into the input field
|
|
89
|
+
*/
|
|
90
|
+
async function fill(page, options, value) {
|
|
91
|
+
// Use the auto-healing locator function with 'fill' action for optimization
|
|
92
|
+
const locator = await autoHealLocator(page, options, 'fill');
|
|
93
|
+
// Fill the input field with the provided value
|
|
94
|
+
await locator.fill(value);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Export the functions for use in other modules
|
|
98
|
+
export { click, fill };
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regular Healing Mode Module
|
|
3
|
+
* This module provides manual fallback-based healing for Playwright locators.
|
|
4
|
+
* When a primary selector fails, it tries a series of fallback selectors in order.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
// Import the core healing function that handles the logic of trying selectors
|
|
8
|
+
import { regularHeal } from '../core/healer.js';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Performs a click action using regular healing strategy.
|
|
12
|
+
* @param {Page} page - The Playwright page object
|
|
13
|
+
* @param {Object} options - Configuration object containing primary selector and fallbacks
|
|
14
|
+
* @param {string} options.primary - The main CSS or XPath selector to try first
|
|
15
|
+
* @param {Array} options.fallbacks - Array of fallback selector objects with type and value
|
|
16
|
+
* @param {number} [options.timeout=2000] - Timeout in milliseconds for each selector attempt
|
|
17
|
+
* @param {boolean} [options.log=true] - Whether to log healing attempts to console
|
|
18
|
+
*/
|
|
19
|
+
async function click(page, options) {
|
|
20
|
+
// Call the core healing function to get a working locator
|
|
21
|
+
const locator = await regularHeal(page, options);
|
|
22
|
+
// Perform the click action on the found locator
|
|
23
|
+
await locator.click();
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Performs a fill action using regular healing strategy.
|
|
28
|
+
* @param {Page} page - The Playwright page object
|
|
29
|
+
* @param {Object} options - Configuration object containing primary selector and fallbacks
|
|
30
|
+
* @param {string} options.primary - The main CSS or XPath selector to try first
|
|
31
|
+
* @param {Array} options.fallbacks - Array of fallback selector objects with type and value
|
|
32
|
+
* @param {number} [options.timeout=2000] - Timeout in milliseconds for each selector attempt
|
|
33
|
+
* @param {boolean} [options.log=true] - Whether to log healing attempts to console
|
|
34
|
+
* @param {string} value - The text value to fill into the input field
|
|
35
|
+
*/
|
|
36
|
+
async function fill(page, options, value) {
|
|
37
|
+
// Call the core healing function to get a working locator
|
|
38
|
+
const locator = await regularHeal(page, options);
|
|
39
|
+
// Fill the input field with the provided value
|
|
40
|
+
await locator.fill(value);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Export the functions so they can be used by other modules
|
|
44
|
+
export { click, fill };
|