playwright-genie 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/LICENSE +21 -0
- package/README.md +435 -0
- package/dist/index.cjs +1185 -0
- package/dist/index.cjs.map +7 -0
- package/dist/index.d.ts +253 -0
- package/dist/index.js +1138 -0
- package/dist/index.js.map +7 -0
- package/package.json +70 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,1185 @@
|
|
|
1
|
+
var __create = Object.create;
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
+
var __export = (target, all) => {
|
|
8
|
+
for (var name in all)
|
|
9
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
10
|
+
};
|
|
11
|
+
var __copyProps = (to, from, except, desc) => {
|
|
12
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
13
|
+
for (let key of __getOwnPropNames(from))
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
15
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
16
|
+
}
|
|
17
|
+
return to;
|
|
18
|
+
};
|
|
19
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
20
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
21
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
22
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
23
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
24
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
25
|
+
mod
|
|
26
|
+
));
|
|
27
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
|
+
|
|
29
|
+
// src/entry.js
|
|
30
|
+
var entry_exports = {};
|
|
31
|
+
__export(entry_exports, {
|
|
32
|
+
SmartAction: () => SmartAction,
|
|
33
|
+
chatCompletion: () => chatCompletion,
|
|
34
|
+
clearAllCaches: () => clearAllCaches,
|
|
35
|
+
clearCache: () => clearCache,
|
|
36
|
+
createSmartLocator: () => createSmartLocator,
|
|
37
|
+
findAllMatches: () => findAllMatches,
|
|
38
|
+
findLocator: () => findLocator,
|
|
39
|
+
getConfig: () => getConfig,
|
|
40
|
+
getLocator: () => getLocator,
|
|
41
|
+
getPageStructure: () => getPageStructure,
|
|
42
|
+
resolveLocator: () => resolveLocator,
|
|
43
|
+
resolveLocatorsBatch: () => resolveLocatorsBatch
|
|
44
|
+
});
|
|
45
|
+
module.exports = __toCommonJS(entry_exports);
|
|
46
|
+
|
|
47
|
+
// src/llm/client.js
|
|
48
|
+
var import_config = require("dotenv/config");
|
|
49
|
+
var import_openai = __toESM(require("openai"), 1);
|
|
50
|
+
var DEBUG = process.env.LLM_LOCATOR_DEBUG === "true";
|
|
51
|
+
var LLM_API_KEY = process.env.LLM_API_KEY || process.env.ROUTELLM_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
52
|
+
var LLM_BASE_URL = process.env.LLM_BASE_URL || process.env.ROUTELLM_BASE_URL || "https://api.openai.com/v1";
|
|
53
|
+
var LLM_MODEL = process.env.LLM_MODEL || process.env.ROUTELLM_MODEL || "gpt-4o-mini";
|
|
54
|
+
if (!LLM_API_KEY) {
|
|
55
|
+
throw new Error(
|
|
56
|
+
"No LLM API key found. Set one of: LLM_API_KEY, ROUTELLM_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY"
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
var client = new import_openai.default({
|
|
60
|
+
baseURL: LLM_BASE_URL,
|
|
61
|
+
apiKey: LLM_API_KEY
|
|
62
|
+
});
|
|
63
|
+
async function chatCompletion(messages, options = {}) {
|
|
64
|
+
const {
|
|
65
|
+
model = LLM_MODEL,
|
|
66
|
+
temperature = 0,
|
|
67
|
+
maxTokens = 1024
|
|
68
|
+
} = options;
|
|
69
|
+
if (DEBUG) {
|
|
70
|
+
console.log(`[LLM Client] model=${model} base=${LLM_BASE_URL} msgs=${messages.length}`);
|
|
71
|
+
}
|
|
72
|
+
const completion = await client.chat.completions.create({
|
|
73
|
+
model,
|
|
74
|
+
temperature,
|
|
75
|
+
max_tokens: maxTokens,
|
|
76
|
+
messages
|
|
77
|
+
});
|
|
78
|
+
return completion.choices[0].message.content.trim();
|
|
79
|
+
}
|
|
80
|
+
function getConfig() {
|
|
81
|
+
return {
|
|
82
|
+
apiKey: LLM_API_KEY ? "***" + LLM_API_KEY.slice(-4) : null,
|
|
83
|
+
baseUrl: LLM_BASE_URL,
|
|
84
|
+
model: LLM_MODEL
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// src/llm/prompt.js
|
|
89
|
+
var SYSTEM_PROMPT = `You are an expert at generating Playwright locators from page structure.
|
|
90
|
+
You receive a (possibly trimmed) accessibility tree in YAML and special DOM elements.
|
|
91
|
+
|
|
92
|
+
CRITICAL RULES:
|
|
93
|
+
1. ONLY use exact text/names that EXIST in the provided structure. NEVER invent or echo the user's query as locator text.
|
|
94
|
+
2. The user's query is a DESCRIPTION of the element, NOT the element's text. You must FIND the matching element in the tree.
|
|
95
|
+
Example: Query "Admin Tab" \u2192 find the link/tab named "Admin" in the tree \u2192 getByRole('link', { name: /Admin/i })
|
|
96
|
+
Example: Query "login button" \u2192 find button named "Login" in the tree \u2192 getByRole('button', { name: /Login/i })
|
|
97
|
+
3. YAML format: "- link \\"Sign in\\"" means a link element with accessible name "Sign in"
|
|
98
|
+
"- textbox \\"Username\\"" means a textbox with accessible name "Username"
|
|
99
|
+
4. Prefer getByRole with name regex: { name: /text/i } for robustness.
|
|
100
|
+
5. Words like "tab", "button", "link", "field", "textbox", "input" in the query describe the ELEMENT TYPE, not the text content.
|
|
101
|
+
|
|
102
|
+
ACTION-AWARE RULES:
|
|
103
|
+
- If action is "fill", "type", "clear", "press": The target MUST be an editable element (textbox, searchbox, combobox, input). Use getByRole('textbox', ...) or getByPlaceholder or getByLabel. NEVER target a label or static text.
|
|
104
|
+
- If action is "click", "dblclick", "tap", "focus", "hover": Target the interactive element (link, button, tab, menuitem, etc).
|
|
105
|
+
- If action is "check", "uncheck": Target a checkbox or radio element.
|
|
106
|
+
- If action is "select": Target a combobox or listbox element.
|
|
107
|
+
- If action is "getText" or null: Target any matching element.
|
|
108
|
+
|
|
109
|
+
PRIORITY: getByTestId > getByRole > getByLabel > getByPlaceholder > getByAltText > getByTitle > getByText > locator(CSS)
|
|
110
|
+
For iframes: page.frameLocator('sel').getByRole(...)
|
|
111
|
+
For positional: .first() / .nth(n) / .last()
|
|
112
|
+
|
|
113
|
+
Return ONLY valid JSON.`;
|
|
114
|
+
function buildMessages(payloadStr, query, action) {
|
|
115
|
+
const actionHint = action ? `
|
|
116
|
+
Action: "${action}" (the locator MUST target an element appropriate for this action)` : "";
|
|
117
|
+
return [
|
|
118
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
119
|
+
{
|
|
120
|
+
role: "user",
|
|
121
|
+
content: `Page:
|
|
122
|
+
${payloadStr}
|
|
123
|
+
|
|
124
|
+
Find element described as: "${query}"${actionHint}
|
|
125
|
+
|
|
126
|
+
IMPORTANT: Match the query to an ACTUAL element in the tree above. Do NOT use the query text as locator text.
|
|
127
|
+
|
|
128
|
+
JSON: { "strategy":"\u2026", "locatorString":"page.getBy\u2026()", "isInFrame":false, "frameSelector":null, "confidence":0.95, "reasoning":"\u2026" }`
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
function buildBatchMessages(payloadStr, queries) {
|
|
133
|
+
const queriesList = queries.map((q, i) => `${i + 1}. "${q}"`).join("\n");
|
|
134
|
+
return [
|
|
135
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
136
|
+
{
|
|
137
|
+
role: "user",
|
|
138
|
+
content: `Page:
|
|
139
|
+
${payloadStr}
|
|
140
|
+
|
|
141
|
+
Find locators for ALL (match each query to ACTUAL elements in the tree, do NOT echo query text):
|
|
142
|
+
${queriesList}
|
|
143
|
+
|
|
144
|
+
Return JSON array:
|
|
145
|
+
[{ "query":"\u2026", "strategy":"\u2026", "locatorString":"page.getBy\u2026()", "isInFrame":false, "frameSelector":null, "confidence":0.95, "reasoning":"\u2026" }]`
|
|
146
|
+
}
|
|
147
|
+
];
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// src/llm/parser.js
|
|
151
|
+
function parseJsonResponse(raw) {
|
|
152
|
+
let text = raw.trim();
|
|
153
|
+
if (text.startsWith("```")) {
|
|
154
|
+
text = text.replace(/```json?\n?/g, "").replace(/```\n?$/g, "");
|
|
155
|
+
}
|
|
156
|
+
return JSON.parse(text);
|
|
157
|
+
}
|
|
158
|
+
function createPlaywrightLocator(page, locatorString, isInFrame, frameSelector) {
|
|
159
|
+
let code = locatorString.replace(/^page\./, "");
|
|
160
|
+
let context = page;
|
|
161
|
+
if (isInFrame && frameSelector) {
|
|
162
|
+
context = page.frameLocator(frameSelector);
|
|
163
|
+
code = code.replace(/^frameLocator\([^)]+\)\./, "");
|
|
164
|
+
}
|
|
165
|
+
return evalLocator(context, code);
|
|
166
|
+
}
|
|
167
|
+
function parseInlineOptions(str) {
|
|
168
|
+
if (!str) return {};
|
|
169
|
+
const opts = {};
|
|
170
|
+
const nameRegex = str.match(/name:\s*\/([^/]+)\/([gimsuy]*)/);
|
|
171
|
+
const nameStr = str.match(/name:\s*'([^']+)'/);
|
|
172
|
+
const exact = str.match(/exact:\s*(true|false)/);
|
|
173
|
+
if (nameRegex) opts.name = new RegExp(nameRegex[1], nameRegex[2] || void 0);
|
|
174
|
+
else if (nameStr) opts.name = nameStr[1];
|
|
175
|
+
if (exact) opts.exact = exact[1] === "true";
|
|
176
|
+
return opts;
|
|
177
|
+
}
|
|
178
|
+
function parseTextArg(quoted, regexBody, regexFlags) {
|
|
179
|
+
if (quoted) return quoted;
|
|
180
|
+
if (regexBody) return new RegExp(regexBody, regexFlags || void 0);
|
|
181
|
+
return null;
|
|
182
|
+
}
|
|
183
|
+
function parseFilterOptions(str) {
|
|
184
|
+
if (!str) return {};
|
|
185
|
+
const opts = {};
|
|
186
|
+
const hasText = str.match(/hasText:\s*(?:'([^']+)'|\/([^/]+)\/([gimsuy]*))/);
|
|
187
|
+
const hasNotText = str.match(/hasNotText:\s*(?:'([^']+)'|\/([^/]+)\/([gimsuy]*))/);
|
|
188
|
+
if (hasText) opts.hasText = hasText[1] || new RegExp(hasText[2], hasText[3] || void 0);
|
|
189
|
+
if (hasNotText) opts.hasNotText = hasNotText[1] || new RegExp(hasNotText[2], hasNotText[3] || void 0);
|
|
190
|
+
return opts;
|
|
191
|
+
}
|
|
192
|
+
function evalLocator(context, code) {
|
|
193
|
+
const patterns = [
|
|
194
|
+
{
|
|
195
|
+
regex: /getByRole\('([^']+)'(?:,\s*({[^}]+}))?\)(.*)/,
|
|
196
|
+
handler: (m) => {
|
|
197
|
+
const opts = parseInlineOptions(m[2]);
|
|
198
|
+
return applyChain(context.getByRole(m[1], opts), m[3]);
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
{
|
|
202
|
+
regex: /getByTestId\('([^']+)'\)(.*)/,
|
|
203
|
+
handler: (m) => applyChain(context.getByTestId(m[1]), m[2])
|
|
204
|
+
},
|
|
205
|
+
{
|
|
206
|
+
regex: /getByLabel\((?:'([^']+)'|\/([^/]+)\/([gimsuy]*))(?:,\s*({[^}]+}))?\)(.*)/,
|
|
207
|
+
handler: (m) => {
|
|
208
|
+
const t = parseTextArg(m[1], m[2], m[3]);
|
|
209
|
+
const opts = parseInlineOptions(m[4]);
|
|
210
|
+
return applyChain(context.getByLabel(t, opts), m[5]);
|
|
211
|
+
}
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
regex: /getByPlaceholder\((?:'([^']+)'|\/([^/]+)\/([gimsuy]*))(?:,\s*({[^}]+}))?\)(.*)/,
|
|
215
|
+
handler: (m) => {
|
|
216
|
+
const t = parseTextArg(m[1], m[2], m[3]);
|
|
217
|
+
const opts = parseInlineOptions(m[4]);
|
|
218
|
+
return applyChain(context.getByPlaceholder(t, opts), m[5]);
|
|
219
|
+
}
|
|
220
|
+
},
|
|
221
|
+
{
|
|
222
|
+
regex: /getByText\((?:'([^']+)'|\/([^/]+)\/([gimsuy]*))(?:,\s*({[^}]+}))?\)(.*)/,
|
|
223
|
+
handler: (m) => {
|
|
224
|
+
const t = parseTextArg(m[1], m[2], m[3]);
|
|
225
|
+
const opts = parseInlineOptions(m[4]);
|
|
226
|
+
return applyChain(context.getByText(t, opts), m[5]);
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
{
|
|
230
|
+
regex: /getByAltText\((?:'([^']+)'|\/([^/]+)\/([gimsuy]*))(?:,\s*({[^}]+}))?\)(.*)/,
|
|
231
|
+
handler: (m) => {
|
|
232
|
+
const t = parseTextArg(m[1], m[2], m[3]);
|
|
233
|
+
const opts = parseInlineOptions(m[4]);
|
|
234
|
+
return applyChain(context.getByAltText(t, opts), m[5]);
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
{
|
|
238
|
+
regex: /getByTitle\((?:'([^']+)'|\/([^/]+)\/([gimsuy]*))(?:,\s*({[^}]+}))?\)(.*)/,
|
|
239
|
+
handler: (m) => {
|
|
240
|
+
const t = parseTextArg(m[1], m[2], m[3]);
|
|
241
|
+
const opts = parseInlineOptions(m[4]);
|
|
242
|
+
return applyChain(context.getByTitle(t, opts), m[5]);
|
|
243
|
+
}
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
regex: /locator\('([^']+)'\)(.*)/,
|
|
247
|
+
handler: (m) => applyChain(context.locator(m[1]), m[2])
|
|
248
|
+
}
|
|
249
|
+
];
|
|
250
|
+
for (const p of patterns) {
|
|
251
|
+
const match = code.match(p.regex);
|
|
252
|
+
if (match) return p.handler(match);
|
|
253
|
+
}
|
|
254
|
+
throw new Error(`Unsupported locator pattern: ${code}`);
|
|
255
|
+
}
|
|
256
|
+
function applyChain(loc, chain) {
|
|
257
|
+
if (!chain?.trim()) return loc;
|
|
258
|
+
if (chain.includes(".first()")) {
|
|
259
|
+
loc = loc.first();
|
|
260
|
+
chain = chain.replace(".first()", "");
|
|
261
|
+
}
|
|
262
|
+
if (chain.includes(".last()")) {
|
|
263
|
+
loc = loc.last();
|
|
264
|
+
chain = chain.replace(".last()", "");
|
|
265
|
+
}
|
|
266
|
+
const n = chain.match(/\.nth\((\d+)\)/);
|
|
267
|
+
if (n) loc = loc.nth(parseInt(n[1]));
|
|
268
|
+
const f = chain.match(/\.filter\(({[^}]+})\)/);
|
|
269
|
+
if (f) loc = loc.filter(parseFilterOptions(f[1]));
|
|
270
|
+
return loc;
|
|
271
|
+
}
|
|
272
|
+
async function validateLocator(locator, timeoutMs = 3e3) {
|
|
273
|
+
try {
|
|
274
|
+
await locator.waitFor({ state: "attached", timeout: timeoutMs });
|
|
275
|
+
return true;
|
|
276
|
+
} catch {
|
|
277
|
+
return false;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/cache/disk-cache.js
|
|
282
|
+
var import_fs = __toESM(require("fs"), 1);
|
|
283
|
+
var import_path = __toESM(require("path"), 1);
|
|
284
|
+
var DEBUG2 = process.env.LLM_LOCATOR_DEBUG === "true";
|
|
285
|
+
var CACHE_FILE = process.env.LOCATOR_CACHE_FILE || ".locator-cache.json";
|
|
286
|
+
var diskCache = null;
|
|
287
|
+
function loadDiskCache() {
|
|
288
|
+
if (diskCache !== null) return diskCache;
|
|
289
|
+
const filePath = import_path.default.resolve(CACHE_FILE);
|
|
290
|
+
try {
|
|
291
|
+
if (import_fs.default.existsSync(filePath)) {
|
|
292
|
+
diskCache = JSON.parse(import_fs.default.readFileSync(filePath, "utf-8"));
|
|
293
|
+
if (DEBUG2) console.log(`[Disk Cache] Loaded ${Object.keys(diskCache).length} entries from ${CACHE_FILE}`);
|
|
294
|
+
} else {
|
|
295
|
+
diskCache = {};
|
|
296
|
+
}
|
|
297
|
+
} catch (e) {
|
|
298
|
+
console.warn(`[Disk Cache] Failed to read ${CACHE_FILE}:`, e.message);
|
|
299
|
+
diskCache = {};
|
|
300
|
+
}
|
|
301
|
+
return diskCache;
|
|
302
|
+
}
|
|
303
|
+
function saveDiskCache() {
|
|
304
|
+
const filePath = import_path.default.resolve(CACHE_FILE);
|
|
305
|
+
try {
|
|
306
|
+
import_fs.default.writeFileSync(filePath, JSON.stringify(diskCache, null, 2), "utf-8");
|
|
307
|
+
} catch (e) {
|
|
308
|
+
console.warn(`[Disk Cache] Failed to write ${CACHE_FILE}:`, e.message);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
function makeCacheKey(urlOrPage, query, action) {
|
|
312
|
+
let urlPath;
|
|
313
|
+
if (typeof urlOrPage === "string") {
|
|
314
|
+
try {
|
|
315
|
+
urlPath = new URL(urlOrPage).pathname;
|
|
316
|
+
} catch {
|
|
317
|
+
urlPath = urlOrPage;
|
|
318
|
+
}
|
|
319
|
+
} else {
|
|
320
|
+
try {
|
|
321
|
+
urlPath = new URL(urlOrPage.url()).pathname;
|
|
322
|
+
} catch {
|
|
323
|
+
urlPath = urlOrPage.url();
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return `${urlPath}::${(action || "any").toLowerCase()}::${query.toLowerCase().trim()}`;
|
|
327
|
+
}
|
|
328
|
+
function getDiskEntry(url, query, action) {
|
|
329
|
+
const cache = loadDiskCache();
|
|
330
|
+
return cache[makeCacheKey(url, query, action)] || null;
|
|
331
|
+
}
|
|
332
|
+
function setDiskEntry(url, query, action, entry) {
|
|
333
|
+
const cache = loadDiskCache();
|
|
334
|
+
cache[makeCacheKey(url, query, action)] = {
|
|
335
|
+
locatorString: entry.locatorString,
|
|
336
|
+
strategy: entry.strategy,
|
|
337
|
+
isInFrame: entry.isInFrame || false,
|
|
338
|
+
frameSelector: entry.frameSelector || null,
|
|
339
|
+
confidence: entry.confidence,
|
|
340
|
+
reasoning: entry.reasoning,
|
|
341
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
342
|
+
hitCount: 0
|
|
343
|
+
};
|
|
344
|
+
saveDiskCache();
|
|
345
|
+
}
|
|
346
|
+
function invalidateDiskEntry(url, query, action) {
|
|
347
|
+
const cache = loadDiskCache();
|
|
348
|
+
const key = makeCacheKey(url, query, action);
|
|
349
|
+
if (cache[key]) {
|
|
350
|
+
delete cache[key];
|
|
351
|
+
saveDiskCache();
|
|
352
|
+
return true;
|
|
353
|
+
}
|
|
354
|
+
return false;
|
|
355
|
+
}
|
|
356
|
+
function bumpHitCount(url, query, action) {
|
|
357
|
+
const cache = loadDiskCache();
|
|
358
|
+
const key = makeCacheKey(url, query, action);
|
|
359
|
+
if (cache[key]) {
|
|
360
|
+
cache[key].hitCount = (cache[key].hitCount || 0) + 1;
|
|
361
|
+
cache[key].lastUsed = (/* @__PURE__ */ new Date()).toISOString();
|
|
362
|
+
saveDiskCache();
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function clearDiskCache() {
|
|
366
|
+
diskCache = {};
|
|
367
|
+
saveDiskCache();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// src/cache/memory-cache.js
|
|
371
|
+
var memCache = /* @__PURE__ */ new Map();
|
|
372
|
+
var structureCache = { url: null, timestamp: 0, data: null };
|
|
373
|
+
var STRUCTURE_TTL = 8e3;
|
|
374
|
+
function getMemKey(url, query, action) {
|
|
375
|
+
return `${url}::${(action || "any").toLowerCase()}::${query.toLowerCase().trim()}`;
|
|
376
|
+
}
|
|
377
|
+
function getMemEntry(url, query, action) {
|
|
378
|
+
return memCache.get(getMemKey(url, query, action)) || null;
|
|
379
|
+
}
|
|
380
|
+
function setMemEntry(url, query, action, entry) {
|
|
381
|
+
memCache.set(getMemKey(url, query, action), entry);
|
|
382
|
+
}
|
|
383
|
+
function deleteMemEntry(url, query, action) {
|
|
384
|
+
memCache.delete(getMemKey(url, query, action));
|
|
385
|
+
}
|
|
386
|
+
function getStructureCache() {
|
|
387
|
+
return structureCache;
|
|
388
|
+
}
|
|
389
|
+
function setStructureCache(url, data) {
|
|
390
|
+
structureCache = { url, timestamp: Date.now(), data };
|
|
391
|
+
}
|
|
392
|
+
function isStructureCacheValid(url) {
|
|
393
|
+
return structureCache.url === url && Date.now() - structureCache.timestamp < STRUCTURE_TTL && structureCache.data;
|
|
394
|
+
}
|
|
395
|
+
function clearStructureCache() {
|
|
396
|
+
structureCache = { url: null, timestamp: 0, data: null };
|
|
397
|
+
}
|
|
398
|
+
function clearMemoryCache() {
|
|
399
|
+
memCache.clear();
|
|
400
|
+
clearStructureCache();
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// src/page-structure.js
|
|
404
|
+
var DEBUG3 = process.env.LLM_LOCATOR_DEBUG === "true";
|
|
405
|
+
async function getPageStructure(page, forceRefresh = false) {
|
|
406
|
+
const url = page.url();
|
|
407
|
+
if (!forceRefresh && isStructureCacheValid(url)) {
|
|
408
|
+
if (DEBUG3) console.log("[Page Structure] Using cached");
|
|
409
|
+
return getStructureCache().data;
|
|
410
|
+
}
|
|
411
|
+
const structure = { mainFrame: await getFrameStructure(page), frames: [] };
|
|
412
|
+
for (const frame of page.frames()) {
|
|
413
|
+
if (frame === page.mainFrame()) continue;
|
|
414
|
+
try {
|
|
415
|
+
const frameElement = await frame.frameElement();
|
|
416
|
+
const frameInfo = await page.evaluate((el) => ({
|
|
417
|
+
id: el.id || null,
|
|
418
|
+
name: el.name || null,
|
|
419
|
+
selector: el.id ? `#${el.id}` : el.name ? `iframe[name="${el.name}"]` : el.title ? `iframe[title="${el.title}"]` : "iframe"
|
|
420
|
+
}), frameElement);
|
|
421
|
+
structure.frames.push({ ...frameInfo, content: await getFrameStructure(frame) });
|
|
422
|
+
} catch (e) {
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
setStructureCache(url, structure);
|
|
426
|
+
return structure;
|
|
427
|
+
}
|
|
428
|
+
async function getFrameStructure(pageOrFrame) {
|
|
429
|
+
let ariaTree = null;
|
|
430
|
+
let specialElements = [];
|
|
431
|
+
const [ariaResult, domResult] = await Promise.allSettled([
|
|
432
|
+
pageOrFrame.locator("body").ariaSnapshot({ timeout: 5e3 }),
|
|
433
|
+
pageOrFrame.evaluate(() => {
|
|
434
|
+
const els = [];
|
|
435
|
+
for (const el of document.querySelectorAll(
|
|
436
|
+
"[data-testid], [data-test-id], [data-test], [data-qa], [data-cy], [placeholder], [alt], [aria-label]"
|
|
437
|
+
)) {
|
|
438
|
+
const e = {
|
|
439
|
+
tag: el.tagName.toLowerCase(),
|
|
440
|
+
testId: el.getAttribute("data-testid") || el.getAttribute("data-test-id") || el.getAttribute("data-test") || el.getAttribute("data-qa") || el.getAttribute("data-cy") || null,
|
|
441
|
+
placeholder: el.getAttribute("placeholder") || null,
|
|
442
|
+
alt: el.getAttribute("alt") || null,
|
|
443
|
+
ariaLabel: el.getAttribute("aria-label") || null
|
|
444
|
+
};
|
|
445
|
+
if (e.testId || e.placeholder || e.alt || e.ariaLabel) els.push(e);
|
|
446
|
+
}
|
|
447
|
+
return els;
|
|
448
|
+
})
|
|
449
|
+
]);
|
|
450
|
+
if (ariaResult.status === "fulfilled") ariaTree = ariaResult.value;
|
|
451
|
+
else {
|
|
452
|
+
try {
|
|
453
|
+
if (typeof pageOrFrame.mainFrame === "function") {
|
|
454
|
+
const snap = await pageOrFrame.accessibility.snapshot();
|
|
455
|
+
if (snap) ariaTree = JSON.stringify(simplifyAriaTree(snap));
|
|
456
|
+
}
|
|
457
|
+
} catch (e) {
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
if (domResult.status === "fulfilled") specialElements = domResult.value;
|
|
461
|
+
return { ariaTree, specialElements };
|
|
462
|
+
}
|
|
463
|
+
function simplifyAriaTree(node, depth = 0) {
|
|
464
|
+
if (!node || depth > 8) return null;
|
|
465
|
+
const s = {};
|
|
466
|
+
if (node.role) s.role = node.role;
|
|
467
|
+
if (node.name) s.name = node.name;
|
|
468
|
+
if (node.children?.length) {
|
|
469
|
+
s.children = node.children.map((c) => simplifyAriaTree(c, depth + 1)).filter(Boolean);
|
|
470
|
+
}
|
|
471
|
+
return s;
|
|
472
|
+
}
|
|
473
|
+
function trimSnapshotForQuery(ariaYaml, query, maxLines = 150) {
|
|
474
|
+
if (!ariaYaml || typeof ariaYaml !== "string") return ariaYaml;
|
|
475
|
+
const lines = ariaYaml.split("\n");
|
|
476
|
+
if (lines.length <= maxLines) return ariaYaml;
|
|
477
|
+
const queryTokens = query.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter(Boolean);
|
|
478
|
+
const scoredLines = lines.map((line, idx) => {
|
|
479
|
+
const lower = line.toLowerCase();
|
|
480
|
+
let score = 0;
|
|
481
|
+
for (const token of queryTokens) {
|
|
482
|
+
if (lower.includes(token)) score += 10;
|
|
483
|
+
}
|
|
484
|
+
if (/button|link|textbox|searchbox|combobox|checkbox|tab\b/.test(lower)) score += 3;
|
|
485
|
+
if (/heading|navigation|banner|main|form/.test(lower)) score += 2;
|
|
486
|
+
return { line, idx, score };
|
|
487
|
+
});
|
|
488
|
+
const relevant = /* @__PURE__ */ new Set();
|
|
489
|
+
for (const { idx, score } of scoredLines) {
|
|
490
|
+
if (score > 0) {
|
|
491
|
+
const contextStart = Math.max(0, idx - 3);
|
|
492
|
+
const contextEnd = Math.min(lines.length - 1, idx + 2);
|
|
493
|
+
for (let i = contextStart; i <= contextEnd; i++) relevant.add(i);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
for (let i = 0; i < Math.min(15, lines.length); i++) relevant.add(i);
|
|
497
|
+
if (relevant.size < maxLines) {
|
|
498
|
+
const interactive = scoredLines.filter((s) => s.score >= 3 && !relevant.has(s.idx)).sort((a, b) => b.score - a.score);
|
|
499
|
+
for (const { idx } of interactive) {
|
|
500
|
+
if (relevant.size >= maxLines) break;
|
|
501
|
+
relevant.add(idx);
|
|
502
|
+
if (idx > 0) relevant.add(idx - 1);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
const sorted = [...relevant].sort((a, b) => a - b);
|
|
506
|
+
const result = [];
|
|
507
|
+
let lastIdx = -1;
|
|
508
|
+
for (const idx of sorted) {
|
|
509
|
+
if (lastIdx !== -1 && idx - lastIdx > 1) result.push(" ...");
|
|
510
|
+
result.push(lines[idx]);
|
|
511
|
+
lastIdx = idx;
|
|
512
|
+
}
|
|
513
|
+
return result.join("\n");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
// src/resolver.js
|
|
517
|
+
var DEBUG4 = process.env.LLM_LOCATOR_DEBUG === "true";
|
|
518
|
+
function buildPayload(pageStructure, query) {
|
|
519
|
+
const trimmedAria = trimSnapshotForQuery(pageStructure.mainFrame.ariaTree, query);
|
|
520
|
+
const payload = {
|
|
521
|
+
ariaTree: trimmedAria,
|
|
522
|
+
specialElements: pageStructure.mainFrame.specialElements,
|
|
523
|
+
frames: pageStructure.frames.length > 0 ? pageStructure.frames.map((f) => ({
|
|
524
|
+
selector: f.selector,
|
|
525
|
+
ariaTree: trimSnapshotForQuery(f.content?.ariaTree, query, 50),
|
|
526
|
+
specialElements: f.content?.specialElements || []
|
|
527
|
+
})) : void 0
|
|
528
|
+
};
|
|
529
|
+
let payloadStr = JSON.stringify(payload, null, 2);
|
|
530
|
+
if (payloadStr.length > 15e3) payloadStr = payloadStr.substring(0, 15e3) + "\n...(truncated)";
|
|
531
|
+
return payloadStr;
|
|
532
|
+
}
|
|
533
|
+
async function queryLLM(pageStructure, query, options = {}) {
|
|
534
|
+
const { action = null } = options;
|
|
535
|
+
const payloadStr = buildPayload(pageStructure, query);
|
|
536
|
+
if (DEBUG4) console.log("[Resolver] Payload size:", payloadStr.length, "chars");
|
|
537
|
+
const messages = buildMessages(payloadStr, query, action);
|
|
538
|
+
const raw = await chatCompletion(messages, options);
|
|
539
|
+
return parseJsonResponse(raw);
|
|
540
|
+
}
|
|
541
|
+
async function resolveLocator(page, query, options = {}) {
|
|
542
|
+
const shouldDebug = DEBUG4 || options.debug;
|
|
543
|
+
const action = options.action || null;
|
|
544
|
+
const url = page.url();
|
|
545
|
+
const memHit = getMemEntry(url, query, action);
|
|
546
|
+
if (memHit) {
|
|
547
|
+
if (shouldDebug) console.log(`[Memory Cache Hit] "${query}" \u2192 ${memHit.locatorString}`);
|
|
548
|
+
return memHit;
|
|
549
|
+
}
|
|
550
|
+
const diskHit = getDiskEntry(url, query, action);
|
|
551
|
+
if (diskHit) {
|
|
552
|
+
if (shouldDebug) console.log(`[Disk Cache Hit] "${query}" \u2192 ${diskHit.locatorString} (hits: ${diskHit.hitCount || 0})`);
|
|
553
|
+
const result = {
|
|
554
|
+
found: true,
|
|
555
|
+
strategy: diskHit.strategy,
|
|
556
|
+
locatorString: diskHit.locatorString,
|
|
557
|
+
isInFrame: diskHit.isInFrame || false,
|
|
558
|
+
frameSelector: diskHit.frameSelector || null,
|
|
559
|
+
confidence: diskHit.confidence,
|
|
560
|
+
reasoning: diskHit.reasoning,
|
|
561
|
+
source: "disk"
|
|
562
|
+
};
|
|
563
|
+
setMemEntry(url, query, action, result);
|
|
564
|
+
return result;
|
|
565
|
+
}
|
|
566
|
+
if (shouldDebug) console.log(`[LLM] Querying: "${query}" (action: ${action || "any"})`);
|
|
567
|
+
const t0 = Date.now();
|
|
568
|
+
const pageStructure = await getPageStructure(page);
|
|
569
|
+
try {
|
|
570
|
+
const parsed = await queryLLM(pageStructure, query, options);
|
|
571
|
+
const result = {
|
|
572
|
+
found: parsed.confidence > 0.5,
|
|
573
|
+
strategy: parsed.strategy,
|
|
574
|
+
locatorString: parsed.locatorString,
|
|
575
|
+
isInFrame: parsed.isInFrame || false,
|
|
576
|
+
frameSelector: parsed.frameSelector || null,
|
|
577
|
+
confidence: parsed.confidence,
|
|
578
|
+
reasoning: parsed.reasoning,
|
|
579
|
+
fallbackLocators: parsed.fallbackLocators || [],
|
|
580
|
+
source: "llm"
|
|
581
|
+
};
|
|
582
|
+
if (shouldDebug) console.log(`[LLM] "${query}" \u2192 ${result.locatorString} (${Date.now() - t0}ms)`);
|
|
583
|
+
if (result.found) {
|
|
584
|
+
setMemEntry(url, query, action, result);
|
|
585
|
+
setDiskEntry(url, query, action, result);
|
|
586
|
+
}
|
|
587
|
+
return result;
|
|
588
|
+
} catch (error) {
|
|
589
|
+
console.error("LLM locator error:", error.message);
|
|
590
|
+
return { found: false, confidence: 0, error: error.message, source: "llm" };
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
async function getLocator(page, query, options = {}) {
|
|
594
|
+
const shouldDebug = DEBUG4 || options.debug;
|
|
595
|
+
const action = options.action || null;
|
|
596
|
+
const url = page.url();
|
|
597
|
+
const result = await resolveLocator(page, query, options);
|
|
598
|
+
if (!result.found) throw new Error(`Could not find element: ${query}. ${result.error || ""}`);
|
|
599
|
+
const locator = createPlaywrightLocator(page, result.locatorString, result.isInFrame, result.frameSelector);
|
|
600
|
+
if (result.source === "disk") {
|
|
601
|
+
const valid = await validateLocator(locator);
|
|
602
|
+
if (valid) {
|
|
603
|
+
bumpHitCount(url, query, action);
|
|
604
|
+
return { locator, result };
|
|
605
|
+
}
|
|
606
|
+
if (shouldDebug) console.log(`[Stale Cache] "${query}" \u2192 ${result.locatorString} no longer valid, re-querying LLM...`);
|
|
607
|
+
invalidateDiskEntry(url, query, action);
|
|
608
|
+
deleteMemEntry(url, query, action);
|
|
609
|
+
clearStructureCache();
|
|
610
|
+
const freshResult = await resolveLocator(page, query, options);
|
|
611
|
+
if (!freshResult.found) throw new Error(`Could not find element after retry: ${query}. ${freshResult.error || ""}`);
|
|
612
|
+
const freshLocator = createPlaywrightLocator(page, freshResult.locatorString, freshResult.isInFrame, freshResult.frameSelector);
|
|
613
|
+
return { locator: freshLocator, result: freshResult };
|
|
614
|
+
}
|
|
615
|
+
return { locator, result };
|
|
616
|
+
}
|
|
617
|
+
async function resolveLocatorsBatch(page, queries, options = {}) {
|
|
618
|
+
const shouldDebug = DEBUG4 || options.debug;
|
|
619
|
+
const url = page.url();
|
|
620
|
+
const results = /* @__PURE__ */ new Map();
|
|
621
|
+
const uncached = [];
|
|
622
|
+
for (const q of queries) {
|
|
623
|
+
const memHit = getMemEntry(url, q, null);
|
|
624
|
+
if (memHit) {
|
|
625
|
+
results.set(q, memHit);
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
const diskHit = getDiskEntry(url, q, null);
|
|
629
|
+
if (diskHit) {
|
|
630
|
+
const r = {
|
|
631
|
+
found: true,
|
|
632
|
+
strategy: diskHit.strategy,
|
|
633
|
+
locatorString: diskHit.locatorString,
|
|
634
|
+
isInFrame: diskHit.isInFrame || false,
|
|
635
|
+
frameSelector: diskHit.frameSelector || null,
|
|
636
|
+
confidence: diskHit.confidence,
|
|
637
|
+
reasoning: diskHit.reasoning,
|
|
638
|
+
source: "disk"
|
|
639
|
+
};
|
|
640
|
+
setMemEntry(url, q, null, r);
|
|
641
|
+
results.set(q, r);
|
|
642
|
+
} else {
|
|
643
|
+
uncached.push(q);
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
if (uncached.length === 0) return results;
|
|
647
|
+
if (shouldDebug) console.log(`[Batch] Resolving ${uncached.length} queries in 1 call`);
|
|
648
|
+
const t0 = Date.now();
|
|
649
|
+
const pageStructure = await getPageStructure(page);
|
|
650
|
+
let payloadStr = buildPayload(pageStructure, uncached.join(" "));
|
|
651
|
+
if (payloadStr.length > 2e4) payloadStr = payloadStr.substring(0, 2e4) + "\n...(truncated)";
|
|
652
|
+
try {
|
|
653
|
+
const messages = buildBatchMessages(payloadStr, uncached);
|
|
654
|
+
const raw = await chatCompletion(messages, options);
|
|
655
|
+
const parsed = parseJsonResponse(raw);
|
|
656
|
+
if (shouldDebug) console.log(`[Batch] ${parsed.length} results in ${Date.now() - t0}ms`);
|
|
657
|
+
for (let i = 0; i < uncached.length && i < parsed.length; i++) {
|
|
658
|
+
const p = parsed[i];
|
|
659
|
+
const result = {
|
|
660
|
+
found: p.confidence > 0.5,
|
|
661
|
+
strategy: p.strategy,
|
|
662
|
+
locatorString: p.locatorString,
|
|
663
|
+
isInFrame: p.isInFrame || false,
|
|
664
|
+
frameSelector: p.frameSelector || null,
|
|
665
|
+
confidence: p.confidence,
|
|
666
|
+
reasoning: p.reasoning,
|
|
667
|
+
source: "llm"
|
|
668
|
+
};
|
|
669
|
+
results.set(uncached[i], result);
|
|
670
|
+
if (result.found) {
|
|
671
|
+
setMemEntry(url, uncached[i], null, result);
|
|
672
|
+
setDiskEntry(url, uncached[i], null, result);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} catch (error) {
|
|
676
|
+
console.error("Batch LLM error:", error.message);
|
|
677
|
+
for (const q of uncached) {
|
|
678
|
+
if (!results.has(q)) results.set(q, { found: false, confidence: 0, error: error.message });
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return results;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// src/actions.js
|
|
685
|
+
var DEBUG5 = process.env.LLM_LOCATOR_DEBUG === "true";
|
|
686
|
+
var SmartAction = class _SmartAction {
|
|
687
|
+
#locator;
|
|
688
|
+
#result;
|
|
689
|
+
#query;
|
|
690
|
+
constructor(locator, result, query) {
|
|
691
|
+
this.#locator = locator;
|
|
692
|
+
this.#result = result;
|
|
693
|
+
this.#query = query;
|
|
694
|
+
}
|
|
695
|
+
get rawLocator() {
|
|
696
|
+
return this.#locator;
|
|
697
|
+
}
|
|
698
|
+
get result() {
|
|
699
|
+
return this.#result;
|
|
700
|
+
}
|
|
701
|
+
get query() {
|
|
702
|
+
return this.#query;
|
|
703
|
+
}
|
|
704
|
+
async click(options) {
|
|
705
|
+
return this.#locator.click(options);
|
|
706
|
+
}
|
|
707
|
+
async dblclick(options) {
|
|
708
|
+
return this.#locator.dblclick(options);
|
|
709
|
+
}
|
|
710
|
+
async tap(options) {
|
|
711
|
+
return this.#locator.tap(options);
|
|
712
|
+
}
|
|
713
|
+
async hover(options) {
|
|
714
|
+
return this.#locator.hover(options);
|
|
715
|
+
}
|
|
716
|
+
async focus() {
|
|
717
|
+
return this.#locator.focus();
|
|
718
|
+
}
|
|
719
|
+
async blur() {
|
|
720
|
+
return this.#locator.blur();
|
|
721
|
+
}
|
|
722
|
+
async fill(value, options) {
|
|
723
|
+
return this.#locator.fill(value, options);
|
|
724
|
+
}
|
|
725
|
+
async type(text, options) {
|
|
726
|
+
return this.#locator.type(text, options);
|
|
727
|
+
}
|
|
728
|
+
async press(key, options) {
|
|
729
|
+
return this.#locator.press(key, options);
|
|
730
|
+
}
|
|
731
|
+
async pressSequentially(text, options) {
|
|
732
|
+
return this.#locator.pressSequentially(text, options);
|
|
733
|
+
}
|
|
734
|
+
async clear(options) {
|
|
735
|
+
return this.#locator.clear(options);
|
|
736
|
+
}
|
|
737
|
+
async setInputFiles(files, options) {
|
|
738
|
+
return this.#locator.setInputFiles(files, options);
|
|
739
|
+
}
|
|
740
|
+
async selectOption(values, options) {
|
|
741
|
+
return this.#locator.selectOption(values, options);
|
|
742
|
+
}
|
|
743
|
+
async selectText(options) {
|
|
744
|
+
return this.#locator.selectText(options);
|
|
745
|
+
}
|
|
746
|
+
async check(options) {
|
|
747
|
+
return this.#locator.check(options);
|
|
748
|
+
}
|
|
749
|
+
async uncheck(options) {
|
|
750
|
+
return this.#locator.uncheck(options);
|
|
751
|
+
}
|
|
752
|
+
async setChecked(checked, options) {
|
|
753
|
+
return this.#locator.setChecked(checked, options);
|
|
754
|
+
}
|
|
755
|
+
async scrollIntoViewIfNeeded(options) {
|
|
756
|
+
return this.#locator.scrollIntoViewIfNeeded(options);
|
|
757
|
+
}
|
|
758
|
+
async screenshot(options) {
|
|
759
|
+
return this.#locator.screenshot(options);
|
|
760
|
+
}
|
|
761
|
+
async dragTo(target, options) {
|
|
762
|
+
const dest = target instanceof _SmartAction ? target.rawLocator : target;
|
|
763
|
+
return this.#locator.dragTo(dest, options);
|
|
764
|
+
}
|
|
765
|
+
async waitFor(options) {
|
|
766
|
+
return this.#locator.waitFor(options);
|
|
767
|
+
}
|
|
768
|
+
async waitForVisible(timeout = 3e4) {
|
|
769
|
+
return this.#locator.waitFor({ state: "visible", timeout });
|
|
770
|
+
}
|
|
771
|
+
async waitForHidden(timeout = 3e4) {
|
|
772
|
+
return this.#locator.waitFor({ state: "hidden", timeout });
|
|
773
|
+
}
|
|
774
|
+
async waitForAttached(timeout = 3e4) {
|
|
775
|
+
return this.#locator.waitFor({ state: "attached", timeout });
|
|
776
|
+
}
|
|
777
|
+
async waitForDetached(timeout = 3e4) {
|
|
778
|
+
return this.#locator.waitFor({ state: "detached", timeout });
|
|
779
|
+
}
|
|
780
|
+
async isVisible() {
|
|
781
|
+
return this.#locator.isVisible();
|
|
782
|
+
}
|
|
783
|
+
async isHidden() {
|
|
784
|
+
return this.#locator.isHidden();
|
|
785
|
+
}
|
|
786
|
+
async isEnabled() {
|
|
787
|
+
return this.#locator.isEnabled();
|
|
788
|
+
}
|
|
789
|
+
async isDisabled() {
|
|
790
|
+
return this.#locator.isDisabled();
|
|
791
|
+
}
|
|
792
|
+
async isChecked() {
|
|
793
|
+
return this.#locator.isChecked();
|
|
794
|
+
}
|
|
795
|
+
async isEditable() {
|
|
796
|
+
return this.#locator.isEditable();
|
|
797
|
+
}
|
|
798
|
+
async textContent() {
|
|
799
|
+
return this.#locator.textContent();
|
|
800
|
+
}
|
|
801
|
+
async innerText() {
|
|
802
|
+
return this.#locator.innerText();
|
|
803
|
+
}
|
|
804
|
+
async innerHTML() {
|
|
805
|
+
return this.#locator.innerHTML();
|
|
806
|
+
}
|
|
807
|
+
async inputValue(options) {
|
|
808
|
+
return this.#locator.inputValue(options);
|
|
809
|
+
}
|
|
810
|
+
async getAttribute(name) {
|
|
811
|
+
return this.#locator.getAttribute(name);
|
|
812
|
+
}
|
|
813
|
+
async boundingBox() {
|
|
814
|
+
return this.#locator.boundingBox();
|
|
815
|
+
}
|
|
816
|
+
async count() {
|
|
817
|
+
return this.#locator.count();
|
|
818
|
+
}
|
|
819
|
+
async allTextContents() {
|
|
820
|
+
return this.#locator.allTextContents();
|
|
821
|
+
}
|
|
822
|
+
async allInnerTexts() {
|
|
823
|
+
return this.#locator.allInnerTexts();
|
|
824
|
+
}
|
|
825
|
+
async all() {
|
|
826
|
+
return this.#locator.all();
|
|
827
|
+
}
|
|
828
|
+
async evaluate(pageFunction, arg) {
|
|
829
|
+
return this.#locator.evaluate(pageFunction, arg);
|
|
830
|
+
}
|
|
831
|
+
async evaluateAll(pageFunction, arg) {
|
|
832
|
+
return this.#locator.evaluateAll(pageFunction, arg);
|
|
833
|
+
}
|
|
834
|
+
async evaluateHandle(pageFunction, arg) {
|
|
835
|
+
return this.#locator.evaluateHandle(pageFunction, arg);
|
|
836
|
+
}
|
|
837
|
+
first() {
|
|
838
|
+
return this.#locator.first();
|
|
839
|
+
}
|
|
840
|
+
last() {
|
|
841
|
+
return this.#locator.last();
|
|
842
|
+
}
|
|
843
|
+
nth(index) {
|
|
844
|
+
return this.#locator.nth(index);
|
|
845
|
+
}
|
|
846
|
+
filter(options) {
|
|
847
|
+
return this.#locator.filter(options);
|
|
848
|
+
}
|
|
849
|
+
getByRole(role, options) {
|
|
850
|
+
return this.#locator.getByRole(role, options);
|
|
851
|
+
}
|
|
852
|
+
getByText(text, options) {
|
|
853
|
+
return this.#locator.getByText(text, options);
|
|
854
|
+
}
|
|
855
|
+
getByLabel(text, options) {
|
|
856
|
+
return this.#locator.getByLabel(text, options);
|
|
857
|
+
}
|
|
858
|
+
getByPlaceholder(text, options) {
|
|
859
|
+
return this.#locator.getByPlaceholder(text, options);
|
|
860
|
+
}
|
|
861
|
+
getByTestId(testId) {
|
|
862
|
+
return this.#locator.getByTestId(testId);
|
|
863
|
+
}
|
|
864
|
+
locator(selectorOrLocator, options) {
|
|
865
|
+
return this.#locator.locator(selectorOrLocator, options);
|
|
866
|
+
}
|
|
867
|
+
async highlight() {
|
|
868
|
+
return this.#locator.highlight();
|
|
869
|
+
}
|
|
870
|
+
async exists() {
|
|
871
|
+
return await this.#locator.count() > 0;
|
|
872
|
+
}
|
|
873
|
+
async getValue() {
|
|
874
|
+
try {
|
|
875
|
+
return await this.#locator.inputValue();
|
|
876
|
+
} catch {
|
|
877
|
+
return await this.#locator.textContent();
|
|
878
|
+
}
|
|
879
|
+
}
|
|
880
|
+
async getClasses() {
|
|
881
|
+
const cls = await this.#locator.getAttribute("class");
|
|
882
|
+
return cls ? cls.split(/\s+/).filter(Boolean) : [];
|
|
883
|
+
}
|
|
884
|
+
async hasClass(className) {
|
|
885
|
+
return (await this.getClasses()).includes(className);
|
|
886
|
+
}
|
|
887
|
+
async getCssProperty(property) {
|
|
888
|
+
return this.#locator.evaluate(
|
|
889
|
+
(el, prop) => window.getComputedStyle(el).getPropertyValue(prop),
|
|
890
|
+
property
|
|
891
|
+
);
|
|
892
|
+
}
|
|
893
|
+
toString() {
|
|
894
|
+
return `SmartAction("${this.#query}" \u2192 ${this.#result?.locatorString || "unknown"})`;
|
|
895
|
+
}
|
|
896
|
+
};
|
|
897
|
+
|
|
898
|
+
// src/smart-locator.js
|
|
899
|
+
function clearCache() {
|
|
900
|
+
clearMemoryCache();
|
|
901
|
+
}
|
|
902
|
+
function clearAllCaches() {
|
|
903
|
+
clearMemoryCache();
|
|
904
|
+
clearDiskCache();
|
|
905
|
+
}
|
|
906
|
+
function createSmartLocator(page, options = {}) {
|
|
907
|
+
const { verbose = false } = options;
|
|
908
|
+
async function resolve(query, action) {
|
|
909
|
+
const { locator, result } = await getLocator(page, query, { ...options, action });
|
|
910
|
+
if (verbose) console.log(`[Smart Locator] ${action || "locate"}("${query}") \u2192 ${result.locatorString} (${result.source})`);
|
|
911
|
+
return new SmartAction(locator, result, query);
|
|
912
|
+
}
|
|
913
|
+
return {
|
|
914
|
+
async prefetch(...queries) {
|
|
915
|
+
const t0 = Date.now();
|
|
916
|
+
const results = await resolveLocatorsBatch(page, queries, options);
|
|
917
|
+
if (verbose) console.log(`[Smart Locator] Prefetched ${results.size} locators in ${Date.now() - t0}ms`);
|
|
918
|
+
return results;
|
|
919
|
+
},
|
|
920
|
+
async locate(query, action) {
|
|
921
|
+
return resolve(query, action || null);
|
|
922
|
+
},
|
|
923
|
+
async click(query, clickOptions) {
|
|
924
|
+
const el = await resolve(query, "click");
|
|
925
|
+
await el.click(clickOptions);
|
|
926
|
+
return el;
|
|
927
|
+
},
|
|
928
|
+
async dblclick(query, clickOptions) {
|
|
929
|
+
const el = await resolve(query, "click");
|
|
930
|
+
await el.dblclick(clickOptions);
|
|
931
|
+
return el;
|
|
932
|
+
},
|
|
933
|
+
async fill(query, value, fillOptions) {
|
|
934
|
+
const el = await resolve(query, "fill");
|
|
935
|
+
await el.fill(value, fillOptions);
|
|
936
|
+
return el;
|
|
937
|
+
},
|
|
938
|
+
async type(query, text, typeOptions) {
|
|
939
|
+
const el = await resolve(query, "fill");
|
|
940
|
+
await el.type(text, typeOptions);
|
|
941
|
+
return el;
|
|
942
|
+
},
|
|
943
|
+
async pressSequentially(query, text, pressOptions) {
|
|
944
|
+
const el = await resolve(query, "fill");
|
|
945
|
+
await el.pressSequentially(text, pressOptions);
|
|
946
|
+
return el;
|
|
947
|
+
},
|
|
948
|
+
async clear(query) {
|
|
949
|
+
const el = await resolve(query, "fill");
|
|
950
|
+
await el.clear();
|
|
951
|
+
return el;
|
|
952
|
+
},
|
|
953
|
+
async press(query, key, pressOptions) {
|
|
954
|
+
const el = await resolve(query, "fill");
|
|
955
|
+
await el.press(key, pressOptions);
|
|
956
|
+
return el;
|
|
957
|
+
},
|
|
958
|
+
async check(query, checkOptions) {
|
|
959
|
+
const el = await resolve(query, "check");
|
|
960
|
+
await el.check(checkOptions);
|
|
961
|
+
return el;
|
|
962
|
+
},
|
|
963
|
+
async uncheck(query, uncheckOptions) {
|
|
964
|
+
const el = await resolve(query, "uncheck");
|
|
965
|
+
await el.uncheck(uncheckOptions);
|
|
966
|
+
return el;
|
|
967
|
+
},
|
|
968
|
+
async setChecked(query, checked, checkOptions) {
|
|
969
|
+
const el = await resolve(query, checked ? "check" : "uncheck");
|
|
970
|
+
await el.setChecked(checked, checkOptions);
|
|
971
|
+
return el;
|
|
972
|
+
},
|
|
973
|
+
async select(query, values, selectOptions) {
|
|
974
|
+
const el = await resolve(query, "select");
|
|
975
|
+
await el.selectOption(values, selectOptions);
|
|
976
|
+
return el;
|
|
977
|
+
},
|
|
978
|
+
async hover(query, hoverOptions) {
|
|
979
|
+
const el = await resolve(query, "hover");
|
|
980
|
+
await el.hover(hoverOptions);
|
|
981
|
+
return el;
|
|
982
|
+
},
|
|
983
|
+
async focus(query) {
|
|
984
|
+
const el = await resolve(query, "click");
|
|
985
|
+
await el.focus();
|
|
986
|
+
return el;
|
|
987
|
+
},
|
|
988
|
+
async tap(query, tapOptions) {
|
|
989
|
+
const el = await resolve(query, "click");
|
|
990
|
+
await el.tap(tapOptions);
|
|
991
|
+
return el;
|
|
992
|
+
},
|
|
993
|
+
async setInputFiles(query, files, fileOptions) {
|
|
994
|
+
const el = await resolve(query, "fill");
|
|
995
|
+
await el.setInputFiles(files, fileOptions);
|
|
996
|
+
return el;
|
|
997
|
+
},
|
|
998
|
+
async dragTo(srcQuery, destQuery, dragOptions) {
|
|
999
|
+
const src = await resolve(srcQuery, "click");
|
|
1000
|
+
const dest = await resolve(destQuery, "click");
|
|
1001
|
+
await src.dragTo(dest, dragOptions);
|
|
1002
|
+
return { source: src, target: dest };
|
|
1003
|
+
},
|
|
1004
|
+
async selectText(query) {
|
|
1005
|
+
const el = await resolve(query, "click");
|
|
1006
|
+
await el.selectText();
|
|
1007
|
+
return el;
|
|
1008
|
+
},
|
|
1009
|
+
async scrollIntoView(query) {
|
|
1010
|
+
const el = await resolve(query, null);
|
|
1011
|
+
await el.scrollIntoViewIfNeeded();
|
|
1012
|
+
return el;
|
|
1013
|
+
},
|
|
1014
|
+
async screenshot(query, screenshotOptions) {
|
|
1015
|
+
const el = await resolve(query, null);
|
|
1016
|
+
return el.screenshot(screenshotOptions);
|
|
1017
|
+
},
|
|
1018
|
+
async highlight(query) {
|
|
1019
|
+
const el = await resolve(query, null);
|
|
1020
|
+
await el.highlight();
|
|
1021
|
+
return el;
|
|
1022
|
+
},
|
|
1023
|
+
async waitFor(query, waitOptions) {
|
|
1024
|
+
const el = await resolve(query, null);
|
|
1025
|
+
await el.waitFor(waitOptions);
|
|
1026
|
+
return el;
|
|
1027
|
+
},
|
|
1028
|
+
async waitForVisible(query, timeout) {
|
|
1029
|
+
const el = await resolve(query, null);
|
|
1030
|
+
await el.waitForVisible(timeout);
|
|
1031
|
+
return el;
|
|
1032
|
+
},
|
|
1033
|
+
async waitForHidden(query, timeout) {
|
|
1034
|
+
const el = await resolve(query, null);
|
|
1035
|
+
await el.waitForHidden(timeout);
|
|
1036
|
+
return el;
|
|
1037
|
+
},
|
|
1038
|
+
async waitForAttached(query, timeout) {
|
|
1039
|
+
const el = await resolve(query, null);
|
|
1040
|
+
await el.waitForAttached(timeout);
|
|
1041
|
+
return el;
|
|
1042
|
+
},
|
|
1043
|
+
async waitForDetached(query, timeout) {
|
|
1044
|
+
const el = await resolve(query, null);
|
|
1045
|
+
await el.waitForDetached(timeout);
|
|
1046
|
+
return el;
|
|
1047
|
+
},
|
|
1048
|
+
async isVisible(query) {
|
|
1049
|
+
try {
|
|
1050
|
+
const el = await resolve(query, null);
|
|
1051
|
+
return await el.isVisible();
|
|
1052
|
+
} catch {
|
|
1053
|
+
return false;
|
|
1054
|
+
}
|
|
1055
|
+
},
|
|
1056
|
+
async isHidden(query) {
|
|
1057
|
+
try {
|
|
1058
|
+
const el = await resolve(query, null);
|
|
1059
|
+
return await el.isHidden();
|
|
1060
|
+
} catch {
|
|
1061
|
+
return true;
|
|
1062
|
+
}
|
|
1063
|
+
},
|
|
1064
|
+
async isEnabled(query) {
|
|
1065
|
+
try {
|
|
1066
|
+
const el = await resolve(query, null);
|
|
1067
|
+
return await el.isEnabled();
|
|
1068
|
+
} catch {
|
|
1069
|
+
return false;
|
|
1070
|
+
}
|
|
1071
|
+
},
|
|
1072
|
+
async isDisabled(query) {
|
|
1073
|
+
try {
|
|
1074
|
+
const el = await resolve(query, null);
|
|
1075
|
+
return await el.isDisabled();
|
|
1076
|
+
} catch {
|
|
1077
|
+
return true;
|
|
1078
|
+
}
|
|
1079
|
+
},
|
|
1080
|
+
async isChecked(query) {
|
|
1081
|
+
try {
|
|
1082
|
+
const el = await resolve(query, null);
|
|
1083
|
+
return await el.isChecked();
|
|
1084
|
+
} catch {
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
},
|
|
1088
|
+
async isEditable(query) {
|
|
1089
|
+
try {
|
|
1090
|
+
const el = await resolve(query, null);
|
|
1091
|
+
return await el.isEditable();
|
|
1092
|
+
} catch {
|
|
1093
|
+
return false;
|
|
1094
|
+
}
|
|
1095
|
+
},
|
|
1096
|
+
async exists(query) {
|
|
1097
|
+
try {
|
|
1098
|
+
const el = await resolve(query, null);
|
|
1099
|
+
return await el.exists();
|
|
1100
|
+
} catch {
|
|
1101
|
+
return false;
|
|
1102
|
+
}
|
|
1103
|
+
},
|
|
1104
|
+
async getText(query) {
|
|
1105
|
+
const el = await resolve(query, "getText");
|
|
1106
|
+
return el.textContent();
|
|
1107
|
+
},
|
|
1108
|
+
async getInnerText(query) {
|
|
1109
|
+
const el = await resolve(query, "getText");
|
|
1110
|
+
return el.innerText();
|
|
1111
|
+
},
|
|
1112
|
+
async getInnerHTML(query) {
|
|
1113
|
+
const el = await resolve(query, "getText");
|
|
1114
|
+
return el.innerHTML();
|
|
1115
|
+
},
|
|
1116
|
+
async getInputValue(query) {
|
|
1117
|
+
const el = await resolve(query, "fill");
|
|
1118
|
+
return el.inputValue();
|
|
1119
|
+
},
|
|
1120
|
+
async getAttribute(query, attr) {
|
|
1121
|
+
const el = await resolve(query, null);
|
|
1122
|
+
return el.getAttribute(attr);
|
|
1123
|
+
},
|
|
1124
|
+
async getBoundingBox(query) {
|
|
1125
|
+
const el = await resolve(query, null);
|
|
1126
|
+
return el.boundingBox();
|
|
1127
|
+
},
|
|
1128
|
+
async count(query) {
|
|
1129
|
+
const el = await resolve(query, null);
|
|
1130
|
+
return el.count();
|
|
1131
|
+
},
|
|
1132
|
+
clearCache() {
|
|
1133
|
+
clearCache();
|
|
1134
|
+
},
|
|
1135
|
+
clearAllCaches() {
|
|
1136
|
+
clearAllCaches();
|
|
1137
|
+
}
|
|
1138
|
+
};
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
// src/llm-locator.js
|
|
1142
|
+
async function findLocator(page, query, options = {}) {
|
|
1143
|
+
try {
|
|
1144
|
+
const { result } = await getLocator(page, query, options);
|
|
1145
|
+
return {
|
|
1146
|
+
found: true,
|
|
1147
|
+
strategy: result.strategy,
|
|
1148
|
+
locatorString: result.locatorString,
|
|
1149
|
+
locatorType: result.strategy,
|
|
1150
|
+
confidence: result.confidence,
|
|
1151
|
+
fullLocatorString: result.locatorString,
|
|
1152
|
+
reasoning: result.reasoning,
|
|
1153
|
+
isInFrame: result.isInFrame,
|
|
1154
|
+
frameSelector: result.frameSelector,
|
|
1155
|
+
fallbackLocators: result.fallbackLocators || [],
|
|
1156
|
+
element: { strategy: result.strategy }
|
|
1157
|
+
};
|
|
1158
|
+
} catch (e) {
|
|
1159
|
+
return { found: false, locatorString: null, confidence: 0, error: e.message };
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
async function findAllMatches(page, query, options = {}) {
|
|
1163
|
+
try {
|
|
1164
|
+
const { result } = await getLocator(page, query, options);
|
|
1165
|
+
return result.found ? [result] : [];
|
|
1166
|
+
} catch {
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
}
|
|
1170
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
1171
|
+
0 && (module.exports = {
|
|
1172
|
+
SmartAction,
|
|
1173
|
+
chatCompletion,
|
|
1174
|
+
clearAllCaches,
|
|
1175
|
+
clearCache,
|
|
1176
|
+
createSmartLocator,
|
|
1177
|
+
findAllMatches,
|
|
1178
|
+
findLocator,
|
|
1179
|
+
getConfig,
|
|
1180
|
+
getLocator,
|
|
1181
|
+
getPageStructure,
|
|
1182
|
+
resolveLocator,
|
|
1183
|
+
resolveLocatorsBatch
|
|
1184
|
+
});
|
|
1185
|
+
//# sourceMappingURL=index.cjs.map
|