playwright-genie 1.0.0 → 1.0.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/README.md +404 -347
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,435 +1,492 @@
|
|
|
1
|
-
# playwright-
|
|
1
|
+
# playwright-genie
|
|
2
2
|
|
|
3
|
-
>
|
|
3
|
+
> Find and interact with Playwright elements using natural language — powered by any LLM
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/playwright-genie)
|
|
6
6
|
[](https://opensource.org/licenses/MIT)
|
|
7
7
|
|
|
8
|
-
**playwright-
|
|
8
|
+
**playwright-genie** lets you write Playwright tests in plain English. No more hunting for selectors — just describe the element and the genie finds it.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Features
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
12
|
+
- **Natural Language** — describe elements in plain English, no selectors needed
|
|
13
|
+
- **40+ Playwright Actions** — click, fill, check, hover, drag, wait, screenshot and more
|
|
14
|
+
- **Any LLM Provider** — OpenAI, Claude, Ollama, Azure, or any OpenAI-compatible API
|
|
15
|
+
- **Smart Caching** — persistent disk cache + in-memory cache to minimize LLM calls
|
|
16
|
+
- **Action-Aware** — `fill('username')` targets the input, not the label
|
|
17
|
+
- **Auto-Retry** — stale cached locators are automatically re-resolved
|
|
18
|
+
- **TypeScript Support** — full type definitions included
|
|
19
|
+
- **Single Page Object** — one `createSmartLocator(page)` works across all navigations
|
|
19
20
|
|
|
20
|
-
##
|
|
21
|
+
## Installation
|
|
21
22
|
|
|
22
23
|
```bash
|
|
23
|
-
npm install playwright-
|
|
24
|
+
npm install playwright-genie
|
|
24
25
|
```
|
|
25
26
|
|
|
26
27
|
### Prerequisites
|
|
27
28
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
4. **Abacus.AI API Key** - Set the `ABACUS_API_KEY` environment variable
|
|
29
|
+
- **Node.js** >= 18
|
|
30
|
+
- **Playwright** >= 1.40
|
|
31
|
+
- An **LLM API key** (OpenAI, Anthropic, or any OpenAI-compatible provider)
|
|
32
32
|
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
33
|
+
## Setup
|
|
34
|
+
|
|
35
|
+
Create a `.env` file in your project root:
|
|
36
|
+
|
|
37
|
+
```env
|
|
38
|
+
# Option 1: OpenAI
|
|
39
|
+
LLM_API_KEY=sk-your-openai-key
|
|
40
|
+
LLM_MODEL=gpt-4o-mini
|
|
41
|
+
|
|
42
|
+
# Option 2: Anthropic (via OpenAI-compatible proxy)
|
|
43
|
+
LLM_API_KEY=your-anthropic-key
|
|
44
|
+
LLM_BASE_URL=https://your-proxy.com/v1
|
|
45
|
+
LLM_MODEL=claude-sonnet-4-20250514
|
|
46
|
+
|
|
47
|
+
# Option 3: Ollama (local, free)
|
|
48
|
+
LLM_API_KEY=ollama
|
|
49
|
+
LLM_BASE_URL=http://localhost:11434/v1
|
|
50
|
+
LLM_MODEL=llama3
|
|
51
|
+
|
|
52
|
+
# Option 4: Azure OpenAI
|
|
53
|
+
LLM_API_KEY=your-azure-key
|
|
54
|
+
LLM_BASE_URL=https://your-resource.openai.azure.com/openai/deployments/your-deployment
|
|
55
|
+
LLM_MODEL=gpt-4o-mini
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
Also supports `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or `ROUTELLM_API_KEY` as fallbacks.
|
|
59
|
+
|
|
60
|
+
## Quick Start
|
|
61
|
+
|
|
62
|
+
### With Playwright Test
|
|
63
|
+
|
|
64
|
+
```js
|
|
65
|
+
import { test } from '@playwright/test';
|
|
66
|
+
import { createSmartLocator } from 'playwright-genie';
|
|
67
|
+
|
|
68
|
+
test('login flow', async ({ page }) => {
|
|
69
|
+
const smart = createSmartLocator(page);
|
|
70
|
+
|
|
71
|
+
await page.goto('https://myapp.com/login');
|
|
72
|
+
await smart.fill('username', 'admin');
|
|
73
|
+
await smart.fill('password', 'secret123');
|
|
74
|
+
await smart.click('login button');
|
|
75
|
+
await smart.waitForVisible('welcome heading');
|
|
76
|
+
});
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Standalone Script
|
|
80
|
+
|
|
81
|
+
```js
|
|
82
|
+
import { chromium } from 'playwright';
|
|
83
|
+
import { createSmartLocator } from 'playwright-genie';
|
|
84
|
+
|
|
85
|
+
const browser = await chromium.launch();
|
|
86
|
+
const page = await browser.newPage();
|
|
87
|
+
const smart = createSmartLocator(page);
|
|
88
|
+
|
|
89
|
+
await page.goto('https://myapp.com');
|
|
90
|
+
await smart.click('sign in link');
|
|
91
|
+
await smart.fill('email field', 'user@example.com');
|
|
92
|
+
await smart.fill('password field', 'secret');
|
|
93
|
+
await smart.click('submit button');
|
|
94
|
+
|
|
95
|
+
await browser.close();
|
|
36
96
|
```
|
|
37
97
|
|
|
38
|
-
##
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
### `createSmartLocator(page, options?)`
|
|
101
|
+
|
|
102
|
+
Creates a smart locator instance bound to a Playwright page. Works across navigations — no need to recreate it.
|
|
103
|
+
|
|
104
|
+
```js
|
|
105
|
+
const smart = createSmartLocator(page, { verbose: true });
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
**Options:**
|
|
109
|
+
|
|
110
|
+
| Option | Type | Default | Description |
|
|
111
|
+
|--------|------|---------|-------------|
|
|
112
|
+
| `verbose` | `boolean` | `false` | Log resolved locators to console |
|
|
113
|
+
| `debug` | `boolean` | `false` | Enable detailed debug logging |
|
|
114
|
+
| `model` | `string` | env var | Override LLM model |
|
|
115
|
+
| `temperature` | `number` | `0` | LLM temperature |
|
|
116
|
+
| `maxTokens` | `number` | `1024` | Max response tokens |
|
|
39
117
|
|
|
40
|
-
|
|
41
|
-
const { chromium } = require('playwright');
|
|
42
|
-
const { findLocator, createNLPHelper } = require('playwright-nlp-locator');
|
|
118
|
+
---
|
|
43
119
|
|
|
44
|
-
|
|
45
|
-
const browser = await chromium.launch();
|
|
46
|
-
const page = await browser.newPage();
|
|
47
|
-
await page.goto('https://example.com');
|
|
120
|
+
### Interaction Actions
|
|
48
121
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.log(result.confidence); // e.g., 0.95
|
|
122
|
+
```js
|
|
123
|
+
await smart.click('login button');
|
|
124
|
+
await smart.click('submit', { force: true });
|
|
53
125
|
|
|
54
|
-
|
|
55
|
-
const nlp = createNLPHelper(page);
|
|
56
|
-
await nlp.click('Submit button');
|
|
57
|
-
await nlp.type('Email input field', 'user@example.com');
|
|
58
|
-
await nlp.type('Password field', 'secret123');
|
|
126
|
+
await smart.dblclick('editable cell');
|
|
59
127
|
|
|
60
|
-
|
|
61
|
-
}
|
|
128
|
+
await smart.fill('username', 'Admin');
|
|
129
|
+
await smart.fill('email field', 'a@b.com', { timeout: 5000 });
|
|
62
130
|
|
|
63
|
-
|
|
131
|
+
await smart.type('search box', 'hello');
|
|
132
|
+
|
|
133
|
+
await smart.pressSequentially('otp input', '123456', { delay: 100 });
|
|
134
|
+
|
|
135
|
+
await smart.press('search box', 'Enter');
|
|
136
|
+
|
|
137
|
+
await smart.clear('email field');
|
|
138
|
+
|
|
139
|
+
await smart.hover('profile menu');
|
|
140
|
+
|
|
141
|
+
await smart.focus('first input');
|
|
142
|
+
|
|
143
|
+
await smart.tap('mobile menu icon');
|
|
144
|
+
|
|
145
|
+
await smart.select('country dropdown', 'India');
|
|
146
|
+
|
|
147
|
+
await smart.selectText('paragraph content');
|
|
64
148
|
```
|
|
65
149
|
|
|
66
|
-
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
### Checkbox & Radio
|
|
153
|
+
|
|
154
|
+
```js
|
|
155
|
+
await smart.check('remember me checkbox');
|
|
156
|
+
|
|
157
|
+
await smart.uncheck('newsletter opt-in');
|
|
158
|
+
|
|
159
|
+
await smart.setChecked('terms checkbox', true);
|
|
160
|
+
```
|
|
67
161
|
|
|
68
|
-
|
|
162
|
+
---
|
|
69
163
|
|
|
70
|
-
|
|
164
|
+
### File Upload
|
|
71
165
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
166
|
+
```js
|
|
167
|
+
await smart.setInputFiles('file upload', '/path/to/file.pdf');
|
|
168
|
+
await smart.setInputFiles('avatar input', ['/img1.png', '/img2.png']);
|
|
169
|
+
```
|
|
76
170
|
|
|
77
|
-
|
|
171
|
+
---
|
|
78
172
|
|
|
79
|
-
|
|
80
|
-
const result = await findLocator(page, 'email input field');
|
|
173
|
+
### Drag & Drop
|
|
81
174
|
|
|
82
|
-
|
|
83
|
-
{
|
|
84
|
-
found: true,
|
|
85
|
-
locator: "page.getByPlaceholder('Enter your email')",
|
|
86
|
-
locatorType: 'placeholder',
|
|
87
|
-
confidence: 0.92,
|
|
88
|
-
explanation: 'Input field with placeholder text indicating email entry',
|
|
89
|
-
alternatives: [
|
|
90
|
-
{ locator: "page.locator('#email')", confidence: 0.85 }
|
|
91
|
-
],
|
|
92
|
-
elementIndex: 3
|
|
93
|
-
}
|
|
175
|
+
```js
|
|
176
|
+
const { source, target } = await smart.dragTo('card item', 'drop zone');
|
|
94
177
|
```
|
|
95
178
|
|
|
96
|
-
|
|
179
|
+
---
|
|
180
|
+
|
|
181
|
+
### Wait Actions
|
|
182
|
+
|
|
183
|
+
```js
|
|
184
|
+
await smart.waitForVisible('success toast');
|
|
185
|
+
await smart.waitForVisible('modal', 10000);
|
|
186
|
+
|
|
187
|
+
await smart.waitForHidden('loading spinner');
|
|
97
188
|
|
|
98
|
-
|
|
189
|
+
await smart.waitForAttached('dynamic table');
|
|
99
190
|
|
|
100
|
-
|
|
101
|
-
const { locator, result } = await getLocator(page, 'Submit button');
|
|
191
|
+
await smart.waitForDetached('old modal');
|
|
102
192
|
|
|
103
|
-
|
|
104
|
-
await locator.click();
|
|
105
|
-
await locator.fill('text');
|
|
106
|
-
const text = await locator.textContent();
|
|
193
|
+
await smart.waitFor('element', { state: 'visible', timeout: 5000 });
|
|
107
194
|
```
|
|
108
195
|
|
|
109
|
-
|
|
196
|
+
---
|
|
197
|
+
|
|
198
|
+
### State Queries
|
|
199
|
+
|
|
200
|
+
```js
|
|
201
|
+
const visible = await smart.isVisible('error message');
|
|
202
|
+
const hidden = await smart.isHidden('loading spinner');
|
|
203
|
+
const enabled = await smart.isEnabled('submit button');
|
|
204
|
+
const disabled = await smart.isDisabled('locked field');
|
|
205
|
+
const checked = await smart.isChecked('terms checkbox');
|
|
206
|
+
const editable = await smart.isEditable('readonly field');
|
|
207
|
+
const found = await smart.exists('optional element');
|
|
208
|
+
```
|
|
110
209
|
|
|
111
|
-
|
|
210
|
+
---
|
|
112
211
|
|
|
113
|
-
|
|
114
|
-
const matches = await findAllLocators(page, 'navigation links');
|
|
212
|
+
### Content Retrieval
|
|
115
213
|
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
214
|
+
```js
|
|
215
|
+
const text = await smart.getText('welcome heading');
|
|
216
|
+
const inner = await smart.getInnerText('article body');
|
|
217
|
+
const html = await smart.getInnerHTML('rich content area');
|
|
218
|
+
const value = await smart.getInputValue('email field');
|
|
219
|
+
const attr = await smart.getAttribute('link', 'href');
|
|
220
|
+
const box = await smart.getBoundingBox('hero image');
|
|
221
|
+
const num = await smart.count('list items');
|
|
121
222
|
```
|
|
122
223
|
|
|
123
|
-
|
|
224
|
+
---
|
|
124
225
|
|
|
125
|
-
|
|
226
|
+
### Scroll & Visual
|
|
126
227
|
|
|
127
|
-
```
|
|
128
|
-
|
|
228
|
+
```js
|
|
229
|
+
await smart.scrollIntoView('footer section');
|
|
129
230
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
await
|
|
133
|
-
await nlp.select('Country dropdown', 'USA');
|
|
134
|
-
await nlp.hover('Profile menu');
|
|
135
|
-
await nlp.waitFor('Loading spinner to disappear');
|
|
136
|
-
const text = await nlp.getText('Welcome message');
|
|
137
|
-
const exists = await nlp.exists('Error message');
|
|
231
|
+
const buffer = await smart.screenshot('chart area', { path: 'chart.png' });
|
|
232
|
+
|
|
233
|
+
await smart.highlight('target element');
|
|
138
234
|
```
|
|
139
235
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### `smart.locate()` — Resolve Once, Act Many Times
|
|
239
|
+
|
|
240
|
+
When you need multiple actions on the same element, use `locate()` to resolve the locator once:
|
|
241
|
+
|
|
242
|
+
```js
|
|
243
|
+
const el = await smart.locate('username');
|
|
244
|
+
await el.clear();
|
|
245
|
+
await el.fill('NewAdmin');
|
|
246
|
+
await el.press('Tab');
|
|
247
|
+
console.log(await el.inputValue());
|
|
248
|
+
console.log(await el.isEnabled());
|
|
249
|
+
|
|
250
|
+
// Access the raw Playwright locator
|
|
251
|
+
const loc = el.rawLocator;
|
|
252
|
+
|
|
253
|
+
// SmartAction has 40+ methods matching Playwright's Locator API
|
|
254
|
+
await el.click();
|
|
255
|
+
await el.hover();
|
|
256
|
+
await el.screenshot({ path: 'element.png' });
|
|
257
|
+
await el.waitForVisible();
|
|
258
|
+
await el.evaluate((node) => node.style.border = '2px solid red');
|
|
161
259
|
```
|
|
162
260
|
|
|
163
|
-
|
|
261
|
+
---
|
|
164
262
|
|
|
165
|
-
###
|
|
263
|
+
### `smart.prefetch()` — Batch Resolve
|
|
166
264
|
|
|
167
|
-
|
|
168
|
-
// Be specific about the element type
|
|
169
|
-
await nlp.click('Login button');
|
|
170
|
-
await nlp.type('Email input field', 'user@example.com');
|
|
171
|
-
await nlp.click('Submit form button');
|
|
265
|
+
Pre-resolve multiple locators in a single LLM call to save time and cost:
|
|
172
266
|
|
|
173
|
-
|
|
174
|
-
await
|
|
175
|
-
await nlp.type('Search box in the header', 'shoes');
|
|
267
|
+
```js
|
|
268
|
+
await smart.prefetch('username', 'password', 'login button');
|
|
176
269
|
|
|
177
|
-
//
|
|
178
|
-
await
|
|
179
|
-
await
|
|
180
|
-
await
|
|
270
|
+
// These now hit the cache — no LLM calls
|
|
271
|
+
await smart.fill('username', 'Admin');
|
|
272
|
+
await smart.fill('password', 'secret');
|
|
273
|
+
await smart.click('login button');
|
|
181
274
|
```
|
|
182
275
|
|
|
183
|
-
|
|
276
|
+
---
|
|
184
277
|
|
|
185
|
-
|
|
186
|
-
// Too vague - could match multiple elements
|
|
187
|
-
await nlp.click('button');
|
|
188
|
-
await nlp.type('input', 'text');
|
|
278
|
+
### Cache Management
|
|
189
279
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
280
|
+
```js
|
|
281
|
+
smart.clearCache(); // clear in-memory cache only
|
|
282
|
+
smart.clearAllCaches(); // clear both memory + disk (.locator-cache.json)
|
|
193
283
|
```
|
|
194
284
|
|
|
195
|
-
##
|
|
285
|
+
## Caching
|
|
196
286
|
|
|
197
|
-
|
|
287
|
+
**playwright-genie** uses a two-level cache to minimize LLM calls:
|
|
198
288
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
const { createNLPHelper } = require('playwright-nlp-locator');
|
|
289
|
+
1. **Memory cache** — instant lookups within the same test run
|
|
290
|
+
2. **Disk cache** (`.locator-cache.json`) — persists across runs
|
|
202
291
|
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
292
|
+
Cache keys are scoped by **URL pathname + action + query**, so `fill('username')` on `/login` won't collide with `click('username')` on `/dashboard`.
|
|
293
|
+
|
|
294
|
+
If a cached locator becomes stale (element no longer exists), it's automatically invalidated and re-resolved via LLM.
|
|
295
|
+
|
|
296
|
+
Set `LOCATOR_CACHE_FILE` env var to customize the cache file path.
|
|
297
|
+
|
|
298
|
+
## LLM Provider Configuration
|
|
299
|
+
|
|
300
|
+
| Provider | `LLM_API_KEY` | `LLM_BASE_URL` | `LLM_MODEL` |
|
|
301
|
+
|----------|---------------|-----------------|-------------|
|
|
302
|
+
| OpenAI | `sk-...` | *(default)* | `gpt-4o-mini` |
|
|
303
|
+
| Anthropic | `sk-ant-...` | proxy URL | `claude-sonnet-4-20250514` |
|
|
304
|
+
| Ollama | `ollama` | `http://localhost:11434/v1` | `llama3` |
|
|
305
|
+
| Azure OpenAI | Azure key | deployment URL | `gpt-4o-mini` |
|
|
306
|
+
| RouteLLM | key | proxy URL | model name |
|
|
307
|
+
|
|
308
|
+
## Complete Examples
|
|
309
|
+
|
|
310
|
+
### Login Flow
|
|
311
|
+
|
|
312
|
+
```js
|
|
313
|
+
import { test, expect } from '@playwright/test';
|
|
314
|
+
import { createSmartLocator } from 'playwright-genie';
|
|
315
|
+
|
|
316
|
+
test('complete login flow', async ({ page }) => {
|
|
317
|
+
const smart = createSmartLocator(page);
|
|
207
318
|
await page.goto('https://myapp.com/login');
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
await
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
// Check "Remember me" if it exists
|
|
216
|
-
if (await nlp.exists('Remember me checkbox')) {
|
|
217
|
-
await nlp.click('Remember me checkbox');
|
|
319
|
+
|
|
320
|
+
await smart.fill('username', 'admin');
|
|
321
|
+
await smart.fill('password', 'secret123');
|
|
322
|
+
|
|
323
|
+
if (await smart.exists('remember me checkbox')) {
|
|
324
|
+
await smart.check('remember me checkbox');
|
|
218
325
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
await
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
console.log('Login successful!');
|
|
227
|
-
|
|
228
|
-
await browser.close();
|
|
229
|
-
}
|
|
326
|
+
|
|
327
|
+
await smart.click('sign in button');
|
|
328
|
+
await smart.waitForVisible('dashboard heading');
|
|
329
|
+
|
|
330
|
+
const welcome = await smart.getText('welcome message');
|
|
331
|
+
expect(welcome).toContain('admin');
|
|
332
|
+
});
|
|
230
333
|
```
|
|
231
334
|
|
|
232
|
-
###
|
|
335
|
+
### E-commerce Flow
|
|
233
336
|
|
|
234
|
-
```
|
|
235
|
-
async
|
|
236
|
-
const
|
|
237
|
-
const page = await browser.newPage();
|
|
238
|
-
const nlp = createNLPHelper(page);
|
|
239
|
-
|
|
337
|
+
```js
|
|
338
|
+
test('shopping flow', async ({ page }) => {
|
|
339
|
+
const smart = createSmartLocator(page);
|
|
240
340
|
await page.goto('https://shop.example.com');
|
|
241
|
-
|
|
242
|
-
// Search for a product
|
|
243
|
-
await nlp.type('Search bar', 'wireless headphones');
|
|
244
|
-
await nlp.click('Search button');
|
|
245
|
-
|
|
246
|
-
// Filter results
|
|
247
|
-
await nlp.click('Price filter dropdown');
|
|
248
|
-
await nlp.click('Under $100 option');
|
|
249
|
-
|
|
250
|
-
// Select a product
|
|
251
|
-
await nlp.click('First product in the list');
|
|
252
|
-
|
|
253
|
-
// Add to cart
|
|
254
|
-
await nlp.select('Size selector', 'Medium');
|
|
255
|
-
await nlp.click('Add to cart button');
|
|
256
|
-
|
|
257
|
-
// Proceed to checkout
|
|
258
|
-
await nlp.click('Shopping cart icon');
|
|
259
|
-
await nlp.click('Proceed to checkout button');
|
|
260
|
-
|
|
261
|
-
// Fill shipping info
|
|
262
|
-
await nlp.type('First name field', 'John');
|
|
263
|
-
await nlp.type('Last name field', 'Doe');
|
|
264
|
-
await nlp.type('Address line 1', '123 Main St');
|
|
265
|
-
await nlp.type('City input', 'New York');
|
|
266
|
-
await nlp.select('State dropdown', 'NY');
|
|
267
|
-
await nlp.type('Zip code', '10001');
|
|
268
|
-
|
|
269
|
-
await nlp.click('Continue to payment button');
|
|
270
|
-
|
|
271
|
-
await browser.close();
|
|
272
|
-
}
|
|
273
|
-
```
|
|
274
341
|
|
|
275
|
-
|
|
342
|
+
await smart.fill('search bar', 'wireless headphones');
|
|
343
|
+
await smart.press('search bar', 'Enter');
|
|
344
|
+
await smart.waitForVisible('product list');
|
|
276
345
|
|
|
277
|
-
|
|
278
|
-
|
|
346
|
+
await smart.click('first product card');
|
|
347
|
+
await smart.select('size dropdown', 'Medium');
|
|
348
|
+
await smart.click('add to cart button');
|
|
279
349
|
|
|
280
|
-
|
|
281
|
-
const
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
await page.goto('https://myapp.com/signup');
|
|
285
|
-
|
|
286
|
-
// Submit empty form to trigger validation
|
|
287
|
-
const { locator: submitBtn } = await getLocator(page, 'Submit button');
|
|
288
|
-
await submitBtn.click();
|
|
289
|
-
|
|
290
|
-
// Check for validation errors
|
|
291
|
-
const emailError = await findLocator(page, 'Email validation error message');
|
|
292
|
-
if (emailError.found && emailError.confidence > 0.7) {
|
|
293
|
-
console.log('Email validation working:', emailError.locator);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
const passwordError = await findLocator(page, 'Password error message');
|
|
297
|
-
if (passwordError.found) {
|
|
298
|
-
const { locator } = await getLocator(page, 'Password error');
|
|
299
|
-
const errorText = await locator.textContent();
|
|
300
|
-
console.log('Password error:', errorText);
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
await browser.close();
|
|
304
|
-
}
|
|
350
|
+
await smart.waitForVisible('cart badge');
|
|
351
|
+
const count = await smart.getText('cart badge');
|
|
352
|
+
expect(count).toBe('1');
|
|
353
|
+
});
|
|
305
354
|
```
|
|
306
355
|
|
|
307
|
-
###
|
|
356
|
+
### Dynamic Content & Modals
|
|
308
357
|
|
|
309
|
-
```
|
|
310
|
-
async
|
|
311
|
-
const
|
|
312
|
-
const page = await browser.newPage();
|
|
313
|
-
const nlp = createNLPHelper(page);
|
|
314
|
-
|
|
358
|
+
```js
|
|
359
|
+
test('handle dynamic content', async ({ page }) => {
|
|
360
|
+
const smart = createSmartLocator(page);
|
|
315
361
|
await page.goto('https://app.example.com');
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
// Handle modals
|
|
321
|
-
if (await nlp.exists('Cookie consent popup')) {
|
|
322
|
-
await nlp.click('Accept cookies button');
|
|
323
|
-
}
|
|
324
|
-
|
|
325
|
-
if (await nlp.exists('Newsletter subscription modal')) {
|
|
326
|
-
await nlp.click('Close modal button');
|
|
362
|
+
|
|
363
|
+
if (await smart.exists('cookie consent popup')) {
|
|
364
|
+
await smart.click('accept cookies button');
|
|
365
|
+
await smart.waitForHidden('cookie consent popup');
|
|
327
366
|
}
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
await
|
|
331
|
-
await
|
|
332
|
-
await
|
|
333
|
-
|
|
334
|
-
await browser.close();
|
|
335
|
-
}
|
|
367
|
+
|
|
368
|
+
await smart.scrollIntoView('footer section');
|
|
369
|
+
await smart.waitForVisible('load more button');
|
|
370
|
+
await smart.click('load more button');
|
|
371
|
+
await smart.waitForHidden('loading spinner');
|
|
372
|
+
});
|
|
336
373
|
```
|
|
337
374
|
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
const { findLocator, getLocator } = require('playwright-nlp-locator');
|
|
356
|
-
|
|
357
|
-
// Method 1: Check result
|
|
358
|
-
const result = await findLocator(page, 'Missing element');
|
|
359
|
-
if (!result.found) {
|
|
360
|
-
console.log('Element not found:', result.error);
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
if (result.confidence < 0.5) {
|
|
364
|
-
console.log('Low confidence match - may be incorrect');
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
// Method 2: Try-catch with getLocator
|
|
368
|
-
try {
|
|
369
|
-
const { locator } = await getLocator(page, 'Element description');
|
|
370
|
-
await locator.click();
|
|
371
|
-
} catch (error) {
|
|
372
|
-
console.error('Failed to find element:', error.message);
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
// Method 3: Fallback locators
|
|
376
|
-
const result = await findLocator(page, 'Submit button');
|
|
377
|
-
if (result.confidence < 0.7 && result.alternatives?.length > 0) {
|
|
378
|
-
console.log('Using alternative locator:', result.alternatives[0].locator);
|
|
379
|
-
}
|
|
375
|
+
### Form Validation
|
|
376
|
+
|
|
377
|
+
```js
|
|
378
|
+
test('form validation', async ({ page }) => {
|
|
379
|
+
const smart = createSmartLocator(page);
|
|
380
|
+
await page.goto('https://myapp.com/signup');
|
|
381
|
+
|
|
382
|
+
await smart.click('submit button');
|
|
383
|
+
await smart.waitForVisible('email error message');
|
|
384
|
+
|
|
385
|
+
const error = await smart.getText('email error message');
|
|
386
|
+
expect(error).toContain('required');
|
|
387
|
+
|
|
388
|
+
await smart.fill('email field', 'user@example.com');
|
|
389
|
+
const enabled = await smart.isEnabled('submit button');
|
|
390
|
+
expect(enabled).toBe(true);
|
|
391
|
+
});
|
|
380
392
|
```
|
|
381
393
|
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
} from 'playwright-nlp-locator';
|
|
401
|
-
|
|
402
|
-
async function typedExample(): Promise<void> {
|
|
403
|
-
const browser = await chromium.launch();
|
|
404
|
-
const page: Page = await browser.newPage();
|
|
405
|
-
|
|
406
|
-
const config: NLPLocatorConfig = {
|
|
407
|
-
confidenceThreshold: 0.7,
|
|
408
|
-
maxElements: 50
|
|
409
|
-
};
|
|
410
|
-
|
|
411
|
-
const result: FindLocatorResult = await findLocator(page, 'Login button', config);
|
|
412
|
-
|
|
413
|
-
if (result.found && result.confidence >= 0.7) {
|
|
414
|
-
console.log(`Found: ${result.locator}`);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
const nlp: NLPHelper = createNLPHelper(page, config);
|
|
418
|
-
await nlp.click('Submit button');
|
|
419
|
-
|
|
420
|
-
await browser.close();
|
|
421
|
-
}
|
|
394
|
+
### Drag & Drop
|
|
395
|
+
|
|
396
|
+
```js
|
|
397
|
+
test('kanban board', async ({ page }) => {
|
|
398
|
+
const smart = createSmartLocator(page);
|
|
399
|
+
await page.goto('https://app.example.com/board');
|
|
400
|
+
|
|
401
|
+
await smart.dragTo('first task card', 'done column');
|
|
402
|
+
await smart.waitForVisible('task moved toast');
|
|
403
|
+
});
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
## Best Practices
|
|
407
|
+
|
|
408
|
+
**Be specific about element type:**
|
|
409
|
+
```js
|
|
410
|
+
await smart.click('login button'); // good
|
|
411
|
+
await smart.click('button'); // too vague
|
|
422
412
|
```
|
|
423
413
|
|
|
424
|
-
|
|
414
|
+
**Include context when needed:**
|
|
415
|
+
```js
|
|
416
|
+
await smart.click('delete button in first row');
|
|
417
|
+
await smart.fill('search box in header', 'shoes');
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
**Use action-appropriate descriptions:**
|
|
421
|
+
```js
|
|
422
|
+
await smart.fill('username', 'Admin'); // finds the input
|
|
423
|
+
await smart.click('username label'); // finds the label
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
**Reuse the same instance across navigations:**
|
|
427
|
+
```js
|
|
428
|
+
const smart = createSmartLocator(page);
|
|
429
|
+
await page.goto('/login');
|
|
430
|
+
await smart.fill('username', 'Admin');
|
|
431
|
+
await smart.click('login button');
|
|
432
|
+
// navigated to /dashboard — same smart object works
|
|
433
|
+
await smart.click('settings tab');
|
|
434
|
+
```
|
|
435
|
+
|
|
436
|
+
**Use locate() for multiple actions on same element:**
|
|
437
|
+
```js
|
|
438
|
+
const el = await smart.locate('search box');
|
|
439
|
+
await el.fill('query');
|
|
440
|
+
await el.press('Enter');
|
|
441
|
+
// 1 LLM call instead of 2
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
## TypeScript
|
|
445
|
+
|
|
446
|
+
```ts
|
|
447
|
+
import { test } from '@playwright/test';
|
|
448
|
+
import { createSmartLocator, SmartAction, SmartLocator } from 'playwright-genie';
|
|
449
|
+
|
|
450
|
+
test('typed example', async ({ page }) => {
|
|
451
|
+
const smart: SmartLocator = createSmartLocator(page);
|
|
452
|
+
|
|
453
|
+
const el: SmartAction = await smart.locate('username');
|
|
454
|
+
await el.fill('Admin');
|
|
455
|
+
|
|
456
|
+
const visible: boolean = await smart.isVisible('dashboard');
|
|
457
|
+
const text: string | null = await smart.getText('heading');
|
|
458
|
+
});
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
## Debug Mode
|
|
462
|
+
|
|
463
|
+
```bash
|
|
464
|
+
LLM_LOCATOR_DEBUG=true npx playwright test
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
This logs:
|
|
468
|
+
- LLM queries and responses
|
|
469
|
+
- Cache hits/misses (memory and disk)
|
|
470
|
+
- Page structure payload sizes
|
|
471
|
+
- Stale cache invalidations
|
|
472
|
+
|
|
473
|
+
## Security Notes
|
|
474
|
+
|
|
475
|
+
- The library sends the page's accessibility tree to your configured LLM API
|
|
476
|
+
- Sensitive data visible in the DOM may be sent to the API
|
|
477
|
+
- Use environment variables for API keys — never hardcode them
|
|
478
|
+
- For sensitive environments, use a local LLM (e.g., Ollama)
|
|
479
|
+
|
|
480
|
+
## Contributing
|
|
425
481
|
|
|
426
482
|
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
427
483
|
|
|
428
|
-
##
|
|
484
|
+
## License
|
|
429
485
|
|
|
430
|
-
MIT License
|
|
486
|
+
MIT License — see the [LICENSE](LICENSE) file for details.
|
|
431
487
|
|
|
432
|
-
##
|
|
488
|
+
## Acknowledgments
|
|
433
489
|
|
|
434
|
-
- [Playwright](https://playwright.dev/)
|
|
435
|
-
- [
|
|
490
|
+
- [Playwright](https://playwright.dev/) — browser automation framework
|
|
491
|
+
- [OpenAI](https://openai.com/) — LLM API support
|
|
492
|
+
- [Claude Code](https://docs.anthropic.com/en/docs/claude-code) — AI-powered code generation and architecture design
|