smart-playwright-nlp 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/README.md +46 -0
- package/dist/index.d.mts +18 -0
- package/dist/index.d.ts +18 -0
- package/dist/index.js +309 -0
- package/dist/index.mjs +274 -0
- package/package.json +42 -0
package/README.md
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# smart-playwright-nlp 🚀
|
|
2
|
+
|
|
3
|
+
An AI-assisted natural language and text-based locator wrapper for **Playwright**. Author end-to-end tests using intuitive, human-readable instructions backed by a robust, multi-tier locator fallback engine.
|
|
4
|
+
|
|
5
|
+
[](https://www.npmjs.com/package/smart-playwright-nlp)
|
|
6
|
+
[](https://opensource.org/licenses/MIT)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 🌟 Key Features
|
|
11
|
+
|
|
12
|
+
* 📝 **Natural Language Parsing:** Powered by lightweight deterministic tokenization (`compromise` NLP), eliminating heavy API latency.
|
|
13
|
+
* 🛡️ **Multi-Tier Smart Locators:** Cascades through accessibility roles, data attributes, placeholders, labels, and text-proximity rules to avoid test flakiness.
|
|
14
|
+
* 📦 **Dual Module Export:** Ships fully compiled with clean **ESM** (`.mjs`) and **CommonJS** (`.js`) paths with baked-in TypeScript declaration maps (`.d.ts`).
|
|
15
|
+
* ⚡ **Zero-Config Playwright Integration:** Drop it straight into your current standard `@playwright/test` runner configuration hooks.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## 💾 Installation
|
|
20
|
+
|
|
21
|
+
Ensure you have `@playwright/test` installed in your host project repository:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install smart-playwright-nlp
|
|
25
|
+
|
|
26
|
+
Usage
|
|
27
|
+
|
|
28
|
+
import { test } from '@playwright/test';
|
|
29
|
+
import { SmartWrapper } from 'smart-playwright-nlp';
|
|
30
|
+
|
|
31
|
+
test('End-to-End Core E-Commerce Purchase Flow', async ({ page }) => {
|
|
32
|
+
const ai = new SmartWrapper(page);
|
|
33
|
+
|
|
34
|
+
// 1. Navigation Commands
|
|
35
|
+
await ai.execute("Navigate to '[https://example.com/login](https://example.com/login)'");
|
|
36
|
+
|
|
37
|
+
// 2. Interactive Form Actions (First quotes = value, Second quotes = target)
|
|
38
|
+
await ai.execute("Type 'test_admin' into 'Username'");
|
|
39
|
+
await ai.execute("Type 'SecurePassword123' into 'Password'");
|
|
40
|
+
await ai.execute("Click the 'Sign In' button");
|
|
41
|
+
|
|
42
|
+
// 3. Smart Web-First Assertions
|
|
43
|
+
await ai.execute("Verify that the text 'Welcome Back' is visible");
|
|
44
|
+
await ai.execute("Verify that url contains '/dashboard'");
|
|
45
|
+
});
|
|
46
|
+
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
declare class SmartWrapper {
|
|
4
|
+
private page;
|
|
5
|
+
constructor(page: Page);
|
|
6
|
+
/**
|
|
7
|
+
* Main entry point to process any natural language string
|
|
8
|
+
*/
|
|
9
|
+
execute(instruction: string): Promise<void>;
|
|
10
|
+
private isNavigation;
|
|
11
|
+
private handleNavigation;
|
|
12
|
+
private handleAction;
|
|
13
|
+
private isAssertion;
|
|
14
|
+
private handleAssertion;
|
|
15
|
+
private resolveSmartLocator;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { SmartWrapper };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Page } from '@playwright/test';
|
|
2
|
+
|
|
3
|
+
declare class SmartWrapper {
|
|
4
|
+
private page;
|
|
5
|
+
constructor(page: Page);
|
|
6
|
+
/**
|
|
7
|
+
* Main entry point to process any natural language string
|
|
8
|
+
*/
|
|
9
|
+
execute(instruction: string): Promise<void>;
|
|
10
|
+
private isNavigation;
|
|
11
|
+
private handleNavigation;
|
|
12
|
+
private handleAction;
|
|
13
|
+
private isAssertion;
|
|
14
|
+
private handleAssertion;
|
|
15
|
+
private resolveSmartLocator;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { SmartWrapper };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __create = Object.create;
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
5
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
6
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
7
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
8
|
+
var __export = (target, all) => {
|
|
9
|
+
for (var name in all)
|
|
10
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
11
|
+
};
|
|
12
|
+
var __copyProps = (to, from, except, desc) => {
|
|
13
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
14
|
+
for (let key of __getOwnPropNames(from))
|
|
15
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
16
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
17
|
+
}
|
|
18
|
+
return to;
|
|
19
|
+
};
|
|
20
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
21
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
22
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
23
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
24
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
25
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
26
|
+
mod
|
|
27
|
+
));
|
|
28
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
29
|
+
|
|
30
|
+
// src/index.ts
|
|
31
|
+
var index_exports = {};
|
|
32
|
+
__export(index_exports, {
|
|
33
|
+
SmartWrapper: () => SmartWrapper
|
|
34
|
+
});
|
|
35
|
+
module.exports = __toCommonJS(index_exports);
|
|
36
|
+
var import_test = require("@playwright/test");
|
|
37
|
+
|
|
38
|
+
// src/parser/intentParser.ts
|
|
39
|
+
var import_compromise = __toESM(require("compromise"));
|
|
40
|
+
function parseInstruction(text) {
|
|
41
|
+
const normalized = text.trim();
|
|
42
|
+
const doc = (0, import_compromise.default)(normalized.toLowerCase());
|
|
43
|
+
const quotes = normalized.match(/['"‘“](.*?)['"”’]/g)?.map((q) => q.replace(/['"‘“]/g, "")) || [];
|
|
44
|
+
if (doc.has("navigate to") || doc.has("goto") || doc.has("open")) {
|
|
45
|
+
return { action: "goto", target: quotes[0] || normalized.replace(/navigate to|goto|open/gi, "").trim() };
|
|
46
|
+
}
|
|
47
|
+
if (doc.has("reload") || doc.has("refresh")) {
|
|
48
|
+
return { action: "reload", target: "" };
|
|
49
|
+
}
|
|
50
|
+
if (doc.has("go back")) {
|
|
51
|
+
return { action: "back", target: "" };
|
|
52
|
+
}
|
|
53
|
+
if (doc.has("go forward")) {
|
|
54
|
+
return { action: "forward", target: "" };
|
|
55
|
+
}
|
|
56
|
+
if (doc.has("wait for network") || doc.has("wait until idle")) {
|
|
57
|
+
return { action: "wait_network", target: "" };
|
|
58
|
+
}
|
|
59
|
+
if (doc.has("verify") || doc.has("expect") || doc.has("assert") || doc.has("should be")) {
|
|
60
|
+
if (doc.has("url")) {
|
|
61
|
+
return { action: "assert_url", target: "url", value: quotes[0] };
|
|
62
|
+
}
|
|
63
|
+
if (doc.has("visible") || doc.has("see")) {
|
|
64
|
+
return { action: "assert_visible", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
65
|
+
}
|
|
66
|
+
if (doc.has("hidden") || doc.has("disappear")) {
|
|
67
|
+
return { action: "assert_hidden", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
68
|
+
}
|
|
69
|
+
if (doc.has("enabled")) {
|
|
70
|
+
return { action: "assert_enabled", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
71
|
+
}
|
|
72
|
+
if (doc.has("checked")) {
|
|
73
|
+
return { action: "assert_checked", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
74
|
+
}
|
|
75
|
+
if (doc.has("value")) {
|
|
76
|
+
return { action: "assert_value", target: quotes[1] || "", value: quotes[0] };
|
|
77
|
+
}
|
|
78
|
+
return { action: "assert_text", target: quotes[1] || normalized, value: quotes[0] };
|
|
79
|
+
}
|
|
80
|
+
if (doc.has("double click") || doc.has("dblclick")) {
|
|
81
|
+
return { action: "dblclick", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
82
|
+
}
|
|
83
|
+
if (doc.has("click") || doc.has("tap") || doc.has("press")) {
|
|
84
|
+
if (doc.has("key") || doc.has("enter key") || doc.has("escape")) {
|
|
85
|
+
return { action: "press_key", target: quotes[1] || "body", value: quotes[0] || "Enter" };
|
|
86
|
+
}
|
|
87
|
+
return { action: "click", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
88
|
+
}
|
|
89
|
+
if (doc.has("hover") || doc.has("mouse over")) {
|
|
90
|
+
return { action: "hover", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
91
|
+
}
|
|
92
|
+
if (doc.has("clear") || doc.has("wipe")) {
|
|
93
|
+
return { action: "clear", target: quotes[0] || normalized, role: "textbox" };
|
|
94
|
+
}
|
|
95
|
+
if (doc.has("uncheck")) {
|
|
96
|
+
return { action: "uncheck", target: quotes[0] || normalized, role: "checkbox" };
|
|
97
|
+
}
|
|
98
|
+
if (doc.has("check")) {
|
|
99
|
+
return { action: "check", target: quotes[0] || normalized, role: "checkbox" };
|
|
100
|
+
}
|
|
101
|
+
if (doc.has("type") || doc.has("fill") || doc.has("enter")) {
|
|
102
|
+
return {
|
|
103
|
+
action: "fill",
|
|
104
|
+
value: quotes[0],
|
|
105
|
+
// What to type (First quoted string)
|
|
106
|
+
target: quotes[1] || normalized,
|
|
107
|
+
// Where to type it (Second quoted string)
|
|
108
|
+
role: "textbox"
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (doc.has("select") || doc.has("choose")) {
|
|
112
|
+
return {
|
|
113
|
+
action: "select",
|
|
114
|
+
value: quotes[0],
|
|
115
|
+
// Option text/value
|
|
116
|
+
target: quotes[1] || normalized,
|
|
117
|
+
// Dropdown target element
|
|
118
|
+
role: "combobox"
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
if (doc.has("upload") || doc.has("attach file")) {
|
|
122
|
+
return {
|
|
123
|
+
action: "upload",
|
|
124
|
+
value: quotes[0],
|
|
125
|
+
// System file path reference location
|
|
126
|
+
target: quotes[1] || normalized
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return { action: "unknown", target: normalized };
|
|
130
|
+
}
|
|
131
|
+
function inferRole(text) {
|
|
132
|
+
const lower = text.toLowerCase();
|
|
133
|
+
if (lower.includes("button")) return "button";
|
|
134
|
+
if (lower.includes("input") || lower.includes("field") || lower.includes("box")) return "textbox";
|
|
135
|
+
if (lower.includes("checkbox")) return "checkbox";
|
|
136
|
+
if (lower.includes("dropdown") || lower.includes("select")) return "combobox";
|
|
137
|
+
if (lower.includes("link")) return "link";
|
|
138
|
+
if (lower.includes("heading")) return "heading";
|
|
139
|
+
return void 0;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// src/index.ts
|
|
143
|
+
var SmartWrapper = class {
|
|
144
|
+
page;
|
|
145
|
+
constructor(page) {
|
|
146
|
+
this.page = page;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* Main entry point to process any natural language string
|
|
150
|
+
*/
|
|
151
|
+
async execute(instruction) {
|
|
152
|
+
const parsed = parseInstruction(instruction);
|
|
153
|
+
if (parsed.action === "unknown") {
|
|
154
|
+
throw new Error(`Unable to cleanly parse step intent: "${instruction}"`);
|
|
155
|
+
}
|
|
156
|
+
if (this.isNavigation(parsed.action)) {
|
|
157
|
+
await this.handleNavigation(parsed);
|
|
158
|
+
} else if (this.isAssertion(parsed.action)) {
|
|
159
|
+
await this.handleAssertion(parsed);
|
|
160
|
+
} else {
|
|
161
|
+
await this.handleAction(parsed);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
// =========================================================================
|
|
165
|
+
// 1. Navigation & Context Command Processor
|
|
166
|
+
// =========================================================================
|
|
167
|
+
isNavigation(action) {
|
|
168
|
+
return ["goto", "reload", "back", "forward", "wait_network"].includes(action);
|
|
169
|
+
}
|
|
170
|
+
async handleNavigation(command) {
|
|
171
|
+
switch (command.action) {
|
|
172
|
+
case "goto":
|
|
173
|
+
await this.page.goto(command.target);
|
|
174
|
+
break;
|
|
175
|
+
case "reload":
|
|
176
|
+
await this.page.reload();
|
|
177
|
+
break;
|
|
178
|
+
case "back":
|
|
179
|
+
await this.page.goBack();
|
|
180
|
+
break;
|
|
181
|
+
case "forward":
|
|
182
|
+
await this.page.goForward();
|
|
183
|
+
break;
|
|
184
|
+
case "wait_network":
|
|
185
|
+
await this.page.waitForLoadState("networkidle");
|
|
186
|
+
break;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
// =========================================================================
|
|
190
|
+
// 2. Interaction Actions Processor
|
|
191
|
+
// =========================================================================
|
|
192
|
+
async handleAction(command) {
|
|
193
|
+
const element = await this.resolveSmartLocator(command.target, command.role);
|
|
194
|
+
switch (command.action) {
|
|
195
|
+
case "click":
|
|
196
|
+
await element.click();
|
|
197
|
+
break;
|
|
198
|
+
case "dblclick":
|
|
199
|
+
await element.dblclick();
|
|
200
|
+
break;
|
|
201
|
+
case "hover":
|
|
202
|
+
await element.hover();
|
|
203
|
+
break;
|
|
204
|
+
case "clear":
|
|
205
|
+
await element.clear();
|
|
206
|
+
break;
|
|
207
|
+
case "check":
|
|
208
|
+
await element.check();
|
|
209
|
+
break;
|
|
210
|
+
case "uncheck":
|
|
211
|
+
await element.uncheck();
|
|
212
|
+
break;
|
|
213
|
+
case "type":
|
|
214
|
+
case "fill":
|
|
215
|
+
if (command.value === void 0) throw new Error(`Type action requires a trailing value mapping rule for target "${command.target}"`);
|
|
216
|
+
await element.fill(command.value);
|
|
217
|
+
break;
|
|
218
|
+
case "press_key":
|
|
219
|
+
if (command.value === void 0) throw new Error(`Press key requires a mapped trigger shortcut key`);
|
|
220
|
+
await element.press(command.value);
|
|
221
|
+
break;
|
|
222
|
+
case "select":
|
|
223
|
+
if (command.value === void 0) throw new Error(`Select target options require a target value option flag`);
|
|
224
|
+
await element.selectOption(command.value);
|
|
225
|
+
break;
|
|
226
|
+
case "upload":
|
|
227
|
+
if (command.value === void 0) throw new Error(`Upload operations require valid absolute file references`);
|
|
228
|
+
await element.setInputFiles(command.value.split(","));
|
|
229
|
+
break;
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// =========================================================================
|
|
233
|
+
// 3. Web-First Assertions Processor
|
|
234
|
+
// =========================================================================
|
|
235
|
+
isAssertion(action) {
|
|
236
|
+
return ["assert_visible", "assert_hidden", "assert_enabled", "assert_checked", "assert_text", "assert_value", "assert_url"].includes(action);
|
|
237
|
+
}
|
|
238
|
+
async handleAssertion(command) {
|
|
239
|
+
if (command.action === "assert_url") {
|
|
240
|
+
if (!command.value) throw new Error("URL confirmation assertions require a baseline comparison string reference");
|
|
241
|
+
await (0, import_test.expect)(this.page).toHaveURL(command.value);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const element = await this.resolveSmartLocator(command.target, command.role);
|
|
245
|
+
switch (command.action) {
|
|
246
|
+
case "assert_visible":
|
|
247
|
+
await (0, import_test.expect)(element).toBeVisible();
|
|
248
|
+
break;
|
|
249
|
+
case "assert_hidden":
|
|
250
|
+
await (0, import_test.expect)(element).toBeHidden();
|
|
251
|
+
break;
|
|
252
|
+
case "assert_enabled":
|
|
253
|
+
await (0, import_test.expect)(element).toBeEnabled();
|
|
254
|
+
break;
|
|
255
|
+
case "assert_checked":
|
|
256
|
+
await (0, import_test.expect)(element).toBeChecked();
|
|
257
|
+
break;
|
|
258
|
+
case "assert_text":
|
|
259
|
+
if (!command.value) throw new Error("Text verification requires validation targets");
|
|
260
|
+
await (0, import_test.expect)(element).toContainText(command.value);
|
|
261
|
+
break;
|
|
262
|
+
case "assert_value":
|
|
263
|
+
if (!command.value) throw new Error("Value validation matches require target data strings");
|
|
264
|
+
await (0, import_test.expect)(element).toHaveValue(command.value);
|
|
265
|
+
break;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
// =========================================================================
|
|
269
|
+
// 4. Hierarchical Locator Engine (Fallback System)
|
|
270
|
+
// =========================================================================
|
|
271
|
+
async resolveSmartLocator(target, explicitRole) {
|
|
272
|
+
if (explicitRole) {
|
|
273
|
+
try {
|
|
274
|
+
return this.page.getByRole(explicitRole, { name: target, exact: false }).first();
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
const testIdLocator = this.page.getByTestId(target);
|
|
280
|
+
if (await testIdLocator.count() > 0) return testIdLocator.first();
|
|
281
|
+
} catch {
|
|
282
|
+
}
|
|
283
|
+
try {
|
|
284
|
+
const placeholderLocator = this.page.getByPlaceholder(target, { exact: false });
|
|
285
|
+
if (await placeholderLocator.count() > 0) return placeholderLocator.first();
|
|
286
|
+
} catch {
|
|
287
|
+
}
|
|
288
|
+
try {
|
|
289
|
+
const labelLocator = this.page.getByLabel(target, { exact: false });
|
|
290
|
+
if (await labelLocator.count() > 0) return labelLocator.first();
|
|
291
|
+
} catch {
|
|
292
|
+
}
|
|
293
|
+
try {
|
|
294
|
+
const textLocator = this.page.getByText(target, { exact: false });
|
|
295
|
+
if (await textLocator.count() > 0) return textLocator.first();
|
|
296
|
+
} catch {
|
|
297
|
+
}
|
|
298
|
+
try {
|
|
299
|
+
const nearInputLocator = this.page.locator(`input:near(:text("${target}"))`);
|
|
300
|
+
if (await nearInputLocator.count() > 0) return nearInputLocator.first();
|
|
301
|
+
} catch {
|
|
302
|
+
}
|
|
303
|
+
return this.page.locator(target).first();
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
307
|
+
0 && (module.exports = {
|
|
308
|
+
SmartWrapper
|
|
309
|
+
});
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
// src/index.ts
|
|
2
|
+
import { expect } from "@playwright/test";
|
|
3
|
+
|
|
4
|
+
// src/parser/intentParser.ts
|
|
5
|
+
import nlp from "compromise";
|
|
6
|
+
function parseInstruction(text) {
|
|
7
|
+
const normalized = text.trim();
|
|
8
|
+
const doc = nlp(normalized.toLowerCase());
|
|
9
|
+
const quotes = normalized.match(/['"‘“](.*?)['"”’]/g)?.map((q) => q.replace(/['"‘“]/g, "")) || [];
|
|
10
|
+
if (doc.has("navigate to") || doc.has("goto") || doc.has("open")) {
|
|
11
|
+
return { action: "goto", target: quotes[0] || normalized.replace(/navigate to|goto|open/gi, "").trim() };
|
|
12
|
+
}
|
|
13
|
+
if (doc.has("reload") || doc.has("refresh")) {
|
|
14
|
+
return { action: "reload", target: "" };
|
|
15
|
+
}
|
|
16
|
+
if (doc.has("go back")) {
|
|
17
|
+
return { action: "back", target: "" };
|
|
18
|
+
}
|
|
19
|
+
if (doc.has("go forward")) {
|
|
20
|
+
return { action: "forward", target: "" };
|
|
21
|
+
}
|
|
22
|
+
if (doc.has("wait for network") || doc.has("wait until idle")) {
|
|
23
|
+
return { action: "wait_network", target: "" };
|
|
24
|
+
}
|
|
25
|
+
if (doc.has("verify") || doc.has("expect") || doc.has("assert") || doc.has("should be")) {
|
|
26
|
+
if (doc.has("url")) {
|
|
27
|
+
return { action: "assert_url", target: "url", value: quotes[0] };
|
|
28
|
+
}
|
|
29
|
+
if (doc.has("visible") || doc.has("see")) {
|
|
30
|
+
return { action: "assert_visible", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
31
|
+
}
|
|
32
|
+
if (doc.has("hidden") || doc.has("disappear")) {
|
|
33
|
+
return { action: "assert_hidden", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
34
|
+
}
|
|
35
|
+
if (doc.has("enabled")) {
|
|
36
|
+
return { action: "assert_enabled", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
37
|
+
}
|
|
38
|
+
if (doc.has("checked")) {
|
|
39
|
+
return { action: "assert_checked", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
40
|
+
}
|
|
41
|
+
if (doc.has("value")) {
|
|
42
|
+
return { action: "assert_value", target: quotes[1] || "", value: quotes[0] };
|
|
43
|
+
}
|
|
44
|
+
return { action: "assert_text", target: quotes[1] || normalized, value: quotes[0] };
|
|
45
|
+
}
|
|
46
|
+
if (doc.has("double click") || doc.has("dblclick")) {
|
|
47
|
+
return { action: "dblclick", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
48
|
+
}
|
|
49
|
+
if (doc.has("click") || doc.has("tap") || doc.has("press")) {
|
|
50
|
+
if (doc.has("key") || doc.has("enter key") || doc.has("escape")) {
|
|
51
|
+
return { action: "press_key", target: quotes[1] || "body", value: quotes[0] || "Enter" };
|
|
52
|
+
}
|
|
53
|
+
return { action: "click", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
54
|
+
}
|
|
55
|
+
if (doc.has("hover") || doc.has("mouse over")) {
|
|
56
|
+
return { action: "hover", target: quotes[0] || normalized, role: inferRole(normalized) };
|
|
57
|
+
}
|
|
58
|
+
if (doc.has("clear") || doc.has("wipe")) {
|
|
59
|
+
return { action: "clear", target: quotes[0] || normalized, role: "textbox" };
|
|
60
|
+
}
|
|
61
|
+
if (doc.has("uncheck")) {
|
|
62
|
+
return { action: "uncheck", target: quotes[0] || normalized, role: "checkbox" };
|
|
63
|
+
}
|
|
64
|
+
if (doc.has("check")) {
|
|
65
|
+
return { action: "check", target: quotes[0] || normalized, role: "checkbox" };
|
|
66
|
+
}
|
|
67
|
+
if (doc.has("type") || doc.has("fill") || doc.has("enter")) {
|
|
68
|
+
return {
|
|
69
|
+
action: "fill",
|
|
70
|
+
value: quotes[0],
|
|
71
|
+
// What to type (First quoted string)
|
|
72
|
+
target: quotes[1] || normalized,
|
|
73
|
+
// Where to type it (Second quoted string)
|
|
74
|
+
role: "textbox"
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (doc.has("select") || doc.has("choose")) {
|
|
78
|
+
return {
|
|
79
|
+
action: "select",
|
|
80
|
+
value: quotes[0],
|
|
81
|
+
// Option text/value
|
|
82
|
+
target: quotes[1] || normalized,
|
|
83
|
+
// Dropdown target element
|
|
84
|
+
role: "combobox"
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
if (doc.has("upload") || doc.has("attach file")) {
|
|
88
|
+
return {
|
|
89
|
+
action: "upload",
|
|
90
|
+
value: quotes[0],
|
|
91
|
+
// System file path reference location
|
|
92
|
+
target: quotes[1] || normalized
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
return { action: "unknown", target: normalized };
|
|
96
|
+
}
|
|
97
|
+
function inferRole(text) {
|
|
98
|
+
const lower = text.toLowerCase();
|
|
99
|
+
if (lower.includes("button")) return "button";
|
|
100
|
+
if (lower.includes("input") || lower.includes("field") || lower.includes("box")) return "textbox";
|
|
101
|
+
if (lower.includes("checkbox")) return "checkbox";
|
|
102
|
+
if (lower.includes("dropdown") || lower.includes("select")) return "combobox";
|
|
103
|
+
if (lower.includes("link")) return "link";
|
|
104
|
+
if (lower.includes("heading")) return "heading";
|
|
105
|
+
return void 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// src/index.ts
|
|
109
|
+
var SmartWrapper = class {
|
|
110
|
+
page;
|
|
111
|
+
constructor(page) {
|
|
112
|
+
this.page = page;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Main entry point to process any natural language string
|
|
116
|
+
*/
|
|
117
|
+
async execute(instruction) {
|
|
118
|
+
const parsed = parseInstruction(instruction);
|
|
119
|
+
if (parsed.action === "unknown") {
|
|
120
|
+
throw new Error(`Unable to cleanly parse step intent: "${instruction}"`);
|
|
121
|
+
}
|
|
122
|
+
if (this.isNavigation(parsed.action)) {
|
|
123
|
+
await this.handleNavigation(parsed);
|
|
124
|
+
} else if (this.isAssertion(parsed.action)) {
|
|
125
|
+
await this.handleAssertion(parsed);
|
|
126
|
+
} else {
|
|
127
|
+
await this.handleAction(parsed);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// =========================================================================
|
|
131
|
+
// 1. Navigation & Context Command Processor
|
|
132
|
+
// =========================================================================
|
|
133
|
+
isNavigation(action) {
|
|
134
|
+
return ["goto", "reload", "back", "forward", "wait_network"].includes(action);
|
|
135
|
+
}
|
|
136
|
+
async handleNavigation(command) {
|
|
137
|
+
switch (command.action) {
|
|
138
|
+
case "goto":
|
|
139
|
+
await this.page.goto(command.target);
|
|
140
|
+
break;
|
|
141
|
+
case "reload":
|
|
142
|
+
await this.page.reload();
|
|
143
|
+
break;
|
|
144
|
+
case "back":
|
|
145
|
+
await this.page.goBack();
|
|
146
|
+
break;
|
|
147
|
+
case "forward":
|
|
148
|
+
await this.page.goForward();
|
|
149
|
+
break;
|
|
150
|
+
case "wait_network":
|
|
151
|
+
await this.page.waitForLoadState("networkidle");
|
|
152
|
+
break;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
// =========================================================================
|
|
156
|
+
// 2. Interaction Actions Processor
|
|
157
|
+
// =========================================================================
|
|
158
|
+
async handleAction(command) {
|
|
159
|
+
const element = await this.resolveSmartLocator(command.target, command.role);
|
|
160
|
+
switch (command.action) {
|
|
161
|
+
case "click":
|
|
162
|
+
await element.click();
|
|
163
|
+
break;
|
|
164
|
+
case "dblclick":
|
|
165
|
+
await element.dblclick();
|
|
166
|
+
break;
|
|
167
|
+
case "hover":
|
|
168
|
+
await element.hover();
|
|
169
|
+
break;
|
|
170
|
+
case "clear":
|
|
171
|
+
await element.clear();
|
|
172
|
+
break;
|
|
173
|
+
case "check":
|
|
174
|
+
await element.check();
|
|
175
|
+
break;
|
|
176
|
+
case "uncheck":
|
|
177
|
+
await element.uncheck();
|
|
178
|
+
break;
|
|
179
|
+
case "type":
|
|
180
|
+
case "fill":
|
|
181
|
+
if (command.value === void 0) throw new Error(`Type action requires a trailing value mapping rule for target "${command.target}"`);
|
|
182
|
+
await element.fill(command.value);
|
|
183
|
+
break;
|
|
184
|
+
case "press_key":
|
|
185
|
+
if (command.value === void 0) throw new Error(`Press key requires a mapped trigger shortcut key`);
|
|
186
|
+
await element.press(command.value);
|
|
187
|
+
break;
|
|
188
|
+
case "select":
|
|
189
|
+
if (command.value === void 0) throw new Error(`Select target options require a target value option flag`);
|
|
190
|
+
await element.selectOption(command.value);
|
|
191
|
+
break;
|
|
192
|
+
case "upload":
|
|
193
|
+
if (command.value === void 0) throw new Error(`Upload operations require valid absolute file references`);
|
|
194
|
+
await element.setInputFiles(command.value.split(","));
|
|
195
|
+
break;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
// =========================================================================
|
|
199
|
+
// 3. Web-First Assertions Processor
|
|
200
|
+
// =========================================================================
|
|
201
|
+
isAssertion(action) {
|
|
202
|
+
return ["assert_visible", "assert_hidden", "assert_enabled", "assert_checked", "assert_text", "assert_value", "assert_url"].includes(action);
|
|
203
|
+
}
|
|
204
|
+
async handleAssertion(command) {
|
|
205
|
+
if (command.action === "assert_url") {
|
|
206
|
+
if (!command.value) throw new Error("URL confirmation assertions require a baseline comparison string reference");
|
|
207
|
+
await expect(this.page).toHaveURL(command.value);
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
const element = await this.resolveSmartLocator(command.target, command.role);
|
|
211
|
+
switch (command.action) {
|
|
212
|
+
case "assert_visible":
|
|
213
|
+
await expect(element).toBeVisible();
|
|
214
|
+
break;
|
|
215
|
+
case "assert_hidden":
|
|
216
|
+
await expect(element).toBeHidden();
|
|
217
|
+
break;
|
|
218
|
+
case "assert_enabled":
|
|
219
|
+
await expect(element).toBeEnabled();
|
|
220
|
+
break;
|
|
221
|
+
case "assert_checked":
|
|
222
|
+
await expect(element).toBeChecked();
|
|
223
|
+
break;
|
|
224
|
+
case "assert_text":
|
|
225
|
+
if (!command.value) throw new Error("Text verification requires validation targets");
|
|
226
|
+
await expect(element).toContainText(command.value);
|
|
227
|
+
break;
|
|
228
|
+
case "assert_value":
|
|
229
|
+
if (!command.value) throw new Error("Value validation matches require target data strings");
|
|
230
|
+
await expect(element).toHaveValue(command.value);
|
|
231
|
+
break;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
// =========================================================================
|
|
235
|
+
// 4. Hierarchical Locator Engine (Fallback System)
|
|
236
|
+
// =========================================================================
|
|
237
|
+
async resolveSmartLocator(target, explicitRole) {
|
|
238
|
+
if (explicitRole) {
|
|
239
|
+
try {
|
|
240
|
+
return this.page.getByRole(explicitRole, { name: target, exact: false }).first();
|
|
241
|
+
} catch {
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
try {
|
|
245
|
+
const testIdLocator = this.page.getByTestId(target);
|
|
246
|
+
if (await testIdLocator.count() > 0) return testIdLocator.first();
|
|
247
|
+
} catch {
|
|
248
|
+
}
|
|
249
|
+
try {
|
|
250
|
+
const placeholderLocator = this.page.getByPlaceholder(target, { exact: false });
|
|
251
|
+
if (await placeholderLocator.count() > 0) return placeholderLocator.first();
|
|
252
|
+
} catch {
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
const labelLocator = this.page.getByLabel(target, { exact: false });
|
|
256
|
+
if (await labelLocator.count() > 0) return labelLocator.first();
|
|
257
|
+
} catch {
|
|
258
|
+
}
|
|
259
|
+
try {
|
|
260
|
+
const textLocator = this.page.getByText(target, { exact: false });
|
|
261
|
+
if (await textLocator.count() > 0) return textLocator.first();
|
|
262
|
+
} catch {
|
|
263
|
+
}
|
|
264
|
+
try {
|
|
265
|
+
const nearInputLocator = this.page.locator(`input:near(:text("${target}"))`);
|
|
266
|
+
if (await nearInputLocator.count() > 0) return nearInputLocator.first();
|
|
267
|
+
} catch {
|
|
268
|
+
}
|
|
269
|
+
return this.page.locator(target).first();
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
export {
|
|
273
|
+
SmartWrapper
|
|
274
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "smart-playwright-nlp",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Natural language and text-based locator wrapper for Playwright",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"module": "./dist/index.mjs",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.mjs",
|
|
11
|
+
"require": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"scripts": {
|
|
18
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --clean",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
21
|
+
},
|
|
22
|
+
"peerDependencies": {
|
|
23
|
+
"@playwright/test": ">=1.0.0"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"compromise": "^14.15.1"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@playwright/test": "^1.61.0",
|
|
30
|
+
"tsup": "^8.5.1",
|
|
31
|
+
"typescript": "^6.0.3"
|
|
32
|
+
},
|
|
33
|
+
"keywords": [
|
|
34
|
+
"playwright",
|
|
35
|
+
"nlp",
|
|
36
|
+
"qa",
|
|
37
|
+
"test-automation",
|
|
38
|
+
"smart-locators"
|
|
39
|
+
],
|
|
40
|
+
"author": "",
|
|
41
|
+
"license": "MIT"
|
|
42
|
+
}
|