playwright-genie 1.0.0 → 2.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 +618 -356
- package/dist/index.cjs +757 -199
- package/dist/index.cjs.map +4 -4
- package/dist/index.js +745 -180
- package/dist/index.js.map +4 -4
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,107 +1,23 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
var
|
|
7
|
-
var
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
"No LLM API key found. Set one of: LLM_API_KEY, ROUTELLM_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY"
|
|
11
|
-
);
|
|
12
|
-
}
|
|
13
|
-
var client = new OpenAI({
|
|
14
|
-
baseURL: LLM_BASE_URL,
|
|
15
|
-
apiKey: LLM_API_KEY
|
|
16
|
-
});
|
|
17
|
-
async function chatCompletion(messages, options = {}) {
|
|
18
|
-
const {
|
|
19
|
-
model = LLM_MODEL,
|
|
20
|
-
temperature = 0,
|
|
21
|
-
maxTokens = 1024
|
|
22
|
-
} = options;
|
|
23
|
-
if (DEBUG) {
|
|
24
|
-
console.log(`[LLM Client] model=${model} base=${LLM_BASE_URL} msgs=${messages.length}`);
|
|
25
|
-
}
|
|
26
|
-
const completion = await client.chat.completions.create({
|
|
27
|
-
model,
|
|
28
|
-
temperature,
|
|
29
|
-
max_tokens: maxTokens,
|
|
30
|
-
messages
|
|
31
|
-
});
|
|
32
|
-
return completion.choices[0].message.content.trim();
|
|
33
|
-
}
|
|
34
|
-
function getConfig() {
|
|
35
|
-
return {
|
|
36
|
-
apiKey: LLM_API_KEY ? "***" + LLM_API_KEY.slice(-4) : null,
|
|
37
|
-
baseUrl: LLM_BASE_URL,
|
|
38
|
-
model: LLM_MODEL
|
|
39
|
-
};
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// src/llm/prompt.js
|
|
43
|
-
var SYSTEM_PROMPT = `You are an expert at generating Playwright locators from page structure.
|
|
44
|
-
You receive a (possibly trimmed) accessibility tree in YAML and special DOM elements.
|
|
45
|
-
|
|
46
|
-
CRITICAL RULES:
|
|
47
|
-
1. ONLY use exact text/names that EXIST in the provided structure. NEVER invent or echo the user's query as locator text.
|
|
48
|
-
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.
|
|
49
|
-
Example: Query "Admin Tab" \u2192 find the link/tab named "Admin" in the tree \u2192 getByRole('link', { name: /Admin/i })
|
|
50
|
-
Example: Query "login button" \u2192 find button named "Login" in the tree \u2192 getByRole('button', { name: /Login/i })
|
|
51
|
-
3. YAML format: "- link \\"Sign in\\"" means a link element with accessible name "Sign in"
|
|
52
|
-
"- textbox \\"Username\\"" means a textbox with accessible name "Username"
|
|
53
|
-
4. Prefer getByRole with name regex: { name: /text/i } for robustness.
|
|
54
|
-
5. Words like "tab", "button", "link", "field", "textbox", "input" in the query describe the ELEMENT TYPE, not the text content.
|
|
55
|
-
|
|
56
|
-
ACTION-AWARE RULES:
|
|
57
|
-
- 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.
|
|
58
|
-
- If action is "click", "dblclick", "tap", "focus", "hover": Target the interactive element (link, button, tab, menuitem, etc).
|
|
59
|
-
- If action is "check", "uncheck": Target a checkbox or radio element.
|
|
60
|
-
- If action is "select": Target a combobox or listbox element.
|
|
61
|
-
- If action is "getText" or null: Target any matching element.
|
|
62
|
-
|
|
63
|
-
PRIORITY: getByTestId > getByRole > getByLabel > getByPlaceholder > getByAltText > getByTitle > getByText > locator(CSS)
|
|
64
|
-
For iframes: page.frameLocator('sel').getByRole(...)
|
|
65
|
-
For positional: .first() / .nth(n) / .last()
|
|
66
|
-
|
|
67
|
-
Return ONLY valid JSON.`;
|
|
68
|
-
function buildMessages(payloadStr, query, action) {
|
|
69
|
-
const actionHint = action ? `
|
|
70
|
-
Action: "${action}" (the locator MUST target an element appropriate for this action)` : "";
|
|
71
|
-
return [
|
|
72
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
73
|
-
{
|
|
74
|
-
role: "user",
|
|
75
|
-
content: `Page:
|
|
76
|
-
${payloadStr}
|
|
77
|
-
|
|
78
|
-
Find element described as: "${query}"${actionHint}
|
|
79
|
-
|
|
80
|
-
IMPORTANT: Match the query to an ACTUAL element in the tree above. Do NOT use the query text as locator text.
|
|
81
|
-
|
|
82
|
-
JSON: { "strategy":"\u2026", "locatorString":"page.getBy\u2026()", "isInFrame":false, "frameSelector":null, "confidence":0.95, "reasoning":"\u2026" }`
|
|
83
|
-
}
|
|
84
|
-
];
|
|
85
|
-
}
|
|
86
|
-
function buildBatchMessages(payloadStr, queries) {
|
|
87
|
-
const queriesList = queries.map((q, i) => `${i + 1}. "${q}"`).join("\n");
|
|
88
|
-
return [
|
|
89
|
-
{ role: "system", content: SYSTEM_PROMPT },
|
|
90
|
-
{
|
|
91
|
-
role: "user",
|
|
92
|
-
content: `Page:
|
|
93
|
-
${payloadStr}
|
|
94
|
-
|
|
95
|
-
Find locators for ALL (match each query to ACTUAL elements in the tree, do NOT echo query text):
|
|
96
|
-
${queriesList}
|
|
97
|
-
|
|
98
|
-
Return JSON array:
|
|
99
|
-
[{ "query":"\u2026", "strategy":"\u2026", "locatorString":"page.getBy\u2026()", "isInFrame":false, "frameSelector":null, "confidence":0.95, "reasoning":"\u2026" }]`
|
|
100
|
-
}
|
|
101
|
-
];
|
|
102
|
-
}
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
3
|
+
var __esm = (fn, res) => function __init() {
|
|
4
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
5
|
+
};
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
103
10
|
|
|
104
11
|
// src/llm/parser.js
|
|
12
|
+
var parser_exports = {};
|
|
13
|
+
__export(parser_exports, {
|
|
14
|
+
applyChain: () => applyChain,
|
|
15
|
+
createPlaywrightLocator: () => createPlaywrightLocator,
|
|
16
|
+
evalLocator: () => evalLocator,
|
|
17
|
+
parseJsonResponse: () => parseJsonResponse,
|
|
18
|
+
validateLocator: () => validateLocator,
|
|
19
|
+
validateWithFallback: () => validateWithFallback
|
|
20
|
+
});
|
|
105
21
|
function parseJsonResponse(raw) {
|
|
106
22
|
let text = raw.trim();
|
|
107
23
|
if (text.startsWith("```")) {
|
|
@@ -121,17 +37,39 @@ function createPlaywrightLocator(page, locatorString, isInFrame, frameSelector)
|
|
|
121
37
|
function parseInlineOptions(str) {
|
|
122
38
|
if (!str) return {};
|
|
123
39
|
const opts = {};
|
|
124
|
-
|
|
125
|
-
const
|
|
40
|
+
let nameMatch = null;
|
|
41
|
+
const nameIdx = str.indexOf("name:");
|
|
42
|
+
if (nameIdx !== -1) {
|
|
43
|
+
const after = str.substring(nameIdx + 5).trim();
|
|
44
|
+
if (after.startsWith("/")) {
|
|
45
|
+
const lastSlash = after.lastIndexOf("/");
|
|
46
|
+
if (lastSlash > 0) {
|
|
47
|
+
const body = after.substring(1, lastSlash);
|
|
48
|
+
const flags = after.substring(lastSlash + 1).match(/^[gimsuy]*/)[0];
|
|
49
|
+
try {
|
|
50
|
+
opts.name = new RegExp(body, flags || void 0);
|
|
51
|
+
} catch {
|
|
52
|
+
opts.name = body;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else if (after.startsWith("'")) {
|
|
56
|
+
const end = after.indexOf("'", 1);
|
|
57
|
+
if (end > 0) opts.name = after.substring(1, end);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
126
60
|
const exact = str.match(/exact:\s*(true|false)/);
|
|
127
|
-
if (nameRegex) opts.name = new RegExp(nameRegex[1], nameRegex[2] || void 0);
|
|
128
|
-
else if (nameStr) opts.name = nameStr[1];
|
|
129
61
|
if (exact) opts.exact = exact[1] === "true";
|
|
130
62
|
return opts;
|
|
131
63
|
}
|
|
132
64
|
function parseTextArg(quoted, regexBody, regexFlags) {
|
|
133
65
|
if (quoted) return quoted;
|
|
134
|
-
if (regexBody)
|
|
66
|
+
if (regexBody) {
|
|
67
|
+
try {
|
|
68
|
+
return new RegExp(regexBody, regexFlags || void 0);
|
|
69
|
+
} catch {
|
|
70
|
+
return regexBody;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
135
73
|
return null;
|
|
136
74
|
}
|
|
137
75
|
function parseFilterOptions(str) {
|
|
@@ -146,7 +84,7 @@ function parseFilterOptions(str) {
|
|
|
146
84
|
function evalLocator(context, code) {
|
|
147
85
|
const patterns = [
|
|
148
86
|
{
|
|
149
|
-
regex: /getByRole\('([^']+)'(?:,\s*({
|
|
87
|
+
regex: /getByRole\('([^']+)'(?:,\s*(\{.*\}))?\)(.*)/,
|
|
150
88
|
handler: (m) => {
|
|
151
89
|
const opts = parseInlineOptions(m[2]);
|
|
152
90
|
return applyChain(context.getByRole(m[1], opts), m[3]);
|
|
@@ -196,9 +134,21 @@ function evalLocator(context, code) {
|
|
|
196
134
|
return applyChain(context.getByTitle(t, opts), m[5]);
|
|
197
135
|
}
|
|
198
136
|
},
|
|
137
|
+
{
|
|
138
|
+
regex: /locator\('xpath=([^']+)'\)(.*)/,
|
|
139
|
+
handler: (m) => applyChain(context.locator(`xpath=${m[1]}`), m[2])
|
|
140
|
+
},
|
|
141
|
+
{
|
|
142
|
+
regex: /locator\("xpath=([^"]+)"\)(.*)/,
|
|
143
|
+
handler: (m) => applyChain(context.locator(`xpath=${m[1]}`), m[2])
|
|
144
|
+
},
|
|
199
145
|
{
|
|
200
146
|
regex: /locator\('([^']+)'\)(.*)/,
|
|
201
147
|
handler: (m) => applyChain(context.locator(m[1]), m[2])
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
regex: /locator\("([^"]+)"\)(.*)/,
|
|
151
|
+
handler: (m) => applyChain(context.locator(m[1]), m[2])
|
|
202
152
|
}
|
|
203
153
|
];
|
|
204
154
|
for (const p of patterns) {
|
|
@@ -231,6 +181,243 @@ async function validateLocator(locator, timeoutMs = 3e3) {
|
|
|
231
181
|
return false;
|
|
232
182
|
}
|
|
233
183
|
}
|
|
184
|
+
async function validateWithFallback(page, primary, fallbacks, isInFrame, frameSelector) {
|
|
185
|
+
const primaryLocator = createPlaywrightLocator(page, primary, isInFrame, frameSelector);
|
|
186
|
+
const valid = await validateLocator(primaryLocator);
|
|
187
|
+
if (valid) return { locator: primaryLocator, locatorString: primary };
|
|
188
|
+
if (fallbacks && fallbacks.length > 0) {
|
|
189
|
+
for (const fb of fallbacks) {
|
|
190
|
+
try {
|
|
191
|
+
const fbLocator = createPlaywrightLocator(page, fb, isInFrame, frameSelector);
|
|
192
|
+
const fbValid = await validateLocator(fbLocator, 2e3);
|
|
193
|
+
if (fbValid) return { locator: fbLocator, locatorString: fb };
|
|
194
|
+
} catch {
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
var init_parser = __esm({
|
|
201
|
+
"src/llm/parser.js"() {
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// src/llm/client.js
|
|
206
|
+
import "dotenv/config";
|
|
207
|
+
import OpenAI from "openai";
|
|
208
|
+
var DEBUG = process.env.LLM_LOCATOR_DEBUG === "true";
|
|
209
|
+
var LLM_API_KEY = process.env.LLM_API_KEY || process.env.ROUTELLM_API_KEY || process.env.OPENAI_API_KEY || process.env.ANTHROPIC_API_KEY;
|
|
210
|
+
var LLM_BASE_URL = process.env.LLM_BASE_URL || process.env.ROUTELLM_BASE_URL || "https://api.openai.com/v1";
|
|
211
|
+
var LLM_MODEL = process.env.LLM_MODEL || process.env.ROUTELLM_MODEL || "gpt-4o-mini";
|
|
212
|
+
if (!LLM_API_KEY) {
|
|
213
|
+
throw new Error(
|
|
214
|
+
"No LLM API key found. Set one of: LLM_API_KEY, ROUTELLM_API_KEY, OPENAI_API_KEY, or ANTHROPIC_API_KEY"
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
var client = new OpenAI({
|
|
218
|
+
baseURL: LLM_BASE_URL,
|
|
219
|
+
apiKey: LLM_API_KEY
|
|
220
|
+
});
|
|
221
|
+
async function chatCompletion(messages, options = {}) {
|
|
222
|
+
const {
|
|
223
|
+
model = LLM_MODEL,
|
|
224
|
+
temperature = 0,
|
|
225
|
+
maxTokens = 256
|
|
226
|
+
} = options;
|
|
227
|
+
if (DEBUG) {
|
|
228
|
+
console.log(`[LLM Client] model=${model} base=${LLM_BASE_URL} msgs=${messages.length}`);
|
|
229
|
+
}
|
|
230
|
+
const params = {
|
|
231
|
+
model,
|
|
232
|
+
temperature,
|
|
233
|
+
max_tokens: maxTokens,
|
|
234
|
+
messages
|
|
235
|
+
};
|
|
236
|
+
if (model.includes("gpt-") || model.includes("o1") || model.includes("o3") || model.includes("o4")) {
|
|
237
|
+
params.response_format = { type: "json_object" };
|
|
238
|
+
}
|
|
239
|
+
const completion = await client.chat.completions.create(params);
|
|
240
|
+
return completion.choices[0].message.content.trim();
|
|
241
|
+
}
|
|
242
|
+
function getConfig() {
|
|
243
|
+
return {
|
|
244
|
+
apiKey: LLM_API_KEY ? "***" + LLM_API_KEY.slice(-4) : null,
|
|
245
|
+
baseUrl: LLM_BASE_URL,
|
|
246
|
+
model: LLM_MODEL
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// src/llm/prompt.js
|
|
251
|
+
var SYSTEM_PROMPT = `You are an expert at generating Playwright locators from page structure.
|
|
252
|
+
You receive a (possibly trimmed) accessibility tree in YAML, special DOM elements, and interactive DOM elements.
|
|
253
|
+
|
|
254
|
+
CRITICAL RULES:
|
|
255
|
+
1. ONLY use exact text/names that EXIST in the provided structure. NEVER invent or echo the user's query as locator text.
|
|
256
|
+
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.
|
|
257
|
+
Example: Query "Admin Tab" \u2192 find the link/tab named "Admin" in the tree \u2192 getByRole('link', { name: /Admin/i })
|
|
258
|
+
Example: Query "login button" \u2192 find button named "Login" in the tree \u2192 getByRole('button', { name: /Login/i })
|
|
259
|
+
3. YAML format: "- link \\"Sign in\\"" means a link element with accessible name "Sign in"
|
|
260
|
+
"- textbox \\"Username\\"" means a textbox with accessible name "Username"
|
|
261
|
+
4. Prefer getByRole with name regex: { name: /text/i } for robustness.
|
|
262
|
+
5. Words like "tab", "button", "link", "field", "textbox", "input" in the query describe the ELEMENT TYPE, not the text content.
|
|
263
|
+
6. NEVER invent accessible names. If an element appears in the aria tree WITHOUT a quoted name
|
|
264
|
+
(e.g., "textbox", "button", "checkbox", "combobox", "link" with no "Name" after it),
|
|
265
|
+
it has NO accessible name. Do NOT guess a name from nearby text, labels, cells, or headings.
|
|
266
|
+
Instead, find that element in interactiveElements by matching tag/type/position, then use its
|
|
267
|
+
id, htmlName, testId, or selector to build a CSS locator:
|
|
268
|
+
- page.locator('#escapedId')
|
|
269
|
+
- page.locator('[name="htmlName"]')
|
|
270
|
+
- page.locator('[data-test="value"]')
|
|
271
|
+
- page.locator('select.className')
|
|
272
|
+
Only use getByRole('role', { name: /text/i }) when the aria tree EXPLICITLY shows a quoted name
|
|
273
|
+
like: button "Submit", link "Home", textbox "Email".
|
|
274
|
+
|
|
275
|
+
INTERACTIVE ELEMENTS:
|
|
276
|
+
- The interactiveElements array contains DOM metadata for ALL interactive elements including unnamed ones.
|
|
277
|
+
- Each entry has: tag, type, role, id, classes, name, testId, text, options (for selects), selector, parentContext.
|
|
278
|
+
- When an element has NO accessible name in the aria tree, use interactiveElements to find it by id, testId, classes, or parent context.
|
|
279
|
+
- For <select> elements: use the "selector" or "id" field to build page.locator('#id') or page.getByTestId('...').
|
|
280
|
+
- For unnamed comboboxes: look at interactiveElements for a select/combobox with matching options, then use its id/testId/classes.
|
|
281
|
+
|
|
282
|
+
ACTION-AWARE RULES:
|
|
283
|
+
- 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.
|
|
284
|
+
- If action is "click", "dblclick", "tap", "focus", "hover": Target the interactive element (link, button, tab, menuitem, etc).
|
|
285
|
+
- If action is "check", "uncheck": Target a checkbox or radio element.
|
|
286
|
+
- If action is "select": Target a combobox, listbox, or <select> element. If the element is a native <select>, prefer page.locator('#id') or page.locator('select.className').
|
|
287
|
+
- If action is "getText" or null: Target any matching element.
|
|
288
|
+
|
|
289
|
+
LOCATOR PRIORITY: getByTestId > getByRole > getByLabel > getByPlaceholder > getByAltText > getByTitle > getByText > locator(CSS)
|
|
290
|
+
FALLBACK: When no accessible name exists, use locator('CSS selector') with id, class, or data attribute from interactiveElements.
|
|
291
|
+
- When using test IDs, use the EXACT attribute from the page data.
|
|
292
|
+
- If data shows [data-test="x"], use page.locator('[data-test="x"]') NOT getByTestId('x').
|
|
293
|
+
- Only use getByTestId() if the attribute is literally "data-testid".
|
|
294
|
+
For iframes: page.frameLocator('sel').getByRole(...)
|
|
295
|
+
For positional: .first() / .nth(n) / .last()
|
|
296
|
+
|
|
297
|
+
Return ONLY valid JSON.`;
|
|
298
|
+
var DOM_SYSTEM_PROMPT = `You are an expert at generating Playwright locators from raw DOM structure.
|
|
299
|
+
You receive a DOM tree with element metadata (tag, id, classes, xpath, css selectors, text, attributes).
|
|
300
|
+
There is NO accessibility tree available \u2014 you must build locators purely from DOM data.
|
|
301
|
+
|
|
302
|
+
CRITICAL RULES:
|
|
303
|
+
1. The user's query is a DESCRIPTION of the element, NOT the element's text. Match the description to DOM nodes.
|
|
304
|
+
2. ONLY use data that EXISTS in the provided DOM structure. NEVER invent IDs, classes, or text.
|
|
305
|
+
3. Each DOM node may have: tag, id, classes, role, testId ({attr, value}), ariaLabel, title, name, placeholder, text, href, xpath, css, label, type, value, disabled, checked, depth, interactive, heading, landmark.
|
|
306
|
+
|
|
307
|
+
LOCATOR STRATEGY (in priority order):
|
|
308
|
+
1. Test ID: If testId exists, use page.locator('[<attr>="<value>"]') with the EXACT attribute name.
|
|
309
|
+
Only use page.getByTestId('x') when attr is literally "data-testid".
|
|
310
|
+
2. ID: page.locator('#<id>') \u2014 most stable, prefer when available.
|
|
311
|
+
3. Role + accessible name: page.getByRole('<role>', { name: /<text>/i }) \u2014 when role and ariaLabel/label/text exist.
|
|
312
|
+
4. Label: page.getByLabel(/<text>/i) \u2014 for form fields with associated labels.
|
|
313
|
+
5. Placeholder: page.getByPlaceholder(/<text>/i) \u2014 for inputs with placeholder text.
|
|
314
|
+
6. Name attribute: page.locator('[name="<name>"]') \u2014 for form elements.
|
|
315
|
+
7. CSS selector: page.locator('<css>') \u2014 use the "css" field from DOM data when available.
|
|
316
|
+
8. XPath: page.locator('xpath=<xpath>') \u2014 use as last resort, take from "xpath" field.
|
|
317
|
+
9. Compound CSS: Build from tag + class + structural position when no unique identifier exists.
|
|
318
|
+
Example: page.locator('div.container button.submit')
|
|
319
|
+
Example: page.locator('form#login input[type="email"]')
|
|
320
|
+
|
|
321
|
+
COMBINING SELECTORS FOR UNIQUENESS:
|
|
322
|
+
- If a single attribute isn't unique, combine: page.locator('form#login input[name="email"]')
|
|
323
|
+
- Use parent context from "depth" and surrounding nodes to disambiguate.
|
|
324
|
+
- Use .first() / .nth(n) / .last() when multiple matches exist.
|
|
325
|
+
- Use .filter({ hasText: /text/i }) to narrow results.
|
|
326
|
+
|
|
327
|
+
ACTION-AWARE RULES:
|
|
328
|
+
- If action is "fill", "type", "clear", "press": Target MUST be input, textarea, select, or [contenteditable]. NEVER target labels, divs, or spans.
|
|
329
|
+
- If action is "click": Target the clickable element (button, a, [role="button"], etc).
|
|
330
|
+
- If action is "check", "uncheck": Target checkbox or radio input.
|
|
331
|
+
- If action is "select": Target the <select> element or [role="combobox"]/[role="listbox"].
|
|
332
|
+
|
|
333
|
+
FALLBACK LOCATORS:
|
|
334
|
+
- Always provide 1-2 fallback locators in case the primary doesn't match.
|
|
335
|
+
- Use different strategies for fallbacks (e.g., primary=CSS, fallback=XPath).
|
|
336
|
+
|
|
337
|
+
For iframes: page.frameLocator('sel').locator(...)
|
|
338
|
+
Return ONLY valid JSON.`;
|
|
339
|
+
function buildMessages(pageStructure, query, action) {
|
|
340
|
+
const actionNote = action ? `
|
|
341
|
+
ACTION: The user wants to "${action}" this element.` : "";
|
|
342
|
+
return [
|
|
343
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
344
|
+
{
|
|
345
|
+
role: "user",
|
|
346
|
+
content: `Page structure:
|
|
347
|
+
${pageStructure}
|
|
348
|
+
|
|
349
|
+
Find element: "${query}"${actionNote}
|
|
350
|
+
|
|
351
|
+
IMPORTANT: Find the element that matches the description "${query}". Do NOT use "${query}" as the locator text - find the ACTUAL element name/text from the page structure above.
|
|
352
|
+
|
|
353
|
+
Return JSON: {"locatorString":"page.getBy...(...)", "confidence": 0.0-1.0, "explanation":"...", "fallbackLocators":["page.locator(...)"]}`
|
|
354
|
+
}
|
|
355
|
+
];
|
|
356
|
+
}
|
|
357
|
+
function buildDomMessages(pageStructure, query, action) {
|
|
358
|
+
const actionNote = action ? `
|
|
359
|
+
ACTION: The user wants to "${action}" this element.` : "";
|
|
360
|
+
return [
|
|
361
|
+
{ role: "system", content: DOM_SYSTEM_PROMPT },
|
|
362
|
+
{
|
|
363
|
+
role: "user",
|
|
364
|
+
content: `DOM structure:
|
|
365
|
+
${pageStructure}
|
|
366
|
+
|
|
367
|
+
Find element: "${query}"${actionNote}
|
|
368
|
+
|
|
369
|
+
INSTRUCTIONS:
|
|
370
|
+
- Search the domTree and interactiveElements for the element matching "${query}".
|
|
371
|
+
- Use the element's id, testId, css, xpath, name, classes, role, text, label, or placeholder to build a Playwright locator.
|
|
372
|
+
- Prefer stable selectors (id, testId, name) over positional ones (xpath, nth).
|
|
373
|
+
- Provide fallback locators using alternative strategies.
|
|
374
|
+
|
|
375
|
+
Return JSON: {"locatorString":"page.locator(...)", "confidence": 0.0-1.0, "explanation":"...", "fallbackLocators":["page.locator('xpath=...')"]}`
|
|
376
|
+
}
|
|
377
|
+
];
|
|
378
|
+
}
|
|
379
|
+
function buildBatchMessages(pageStructure, queries) {
|
|
380
|
+
const queryList = queries.map((q, i) => `${i + 1}. "${q.query || q}" (action: ${q.action || "none"})`).join("\n");
|
|
381
|
+
return [
|
|
382
|
+
{ role: "system", content: SYSTEM_PROMPT },
|
|
383
|
+
{
|
|
384
|
+
role: "user",
|
|
385
|
+
content: `Page structure:
|
|
386
|
+
${pageStructure}
|
|
387
|
+
|
|
388
|
+
Find ALL of these elements:
|
|
389
|
+
${queryList}
|
|
390
|
+
|
|
391
|
+
IMPORTANT: For each query, find the ACTUAL element name/text from the page structure. Do NOT echo the query as locator text.
|
|
392
|
+
|
|
393
|
+
Return JSON array: [{"query":"...","locatorString":"page.getBy...(...)", "confidence": 0.0-1.0, "explanation":"...", "fallbackLocators":["..."]}]`
|
|
394
|
+
}
|
|
395
|
+
];
|
|
396
|
+
}
|
|
397
|
+
function buildDomBatchMessages(pageStructure, queries) {
|
|
398
|
+
const queryList = queries.map((q, i) => `${i + 1}. "${q.query || q}" (action: ${q.action || "none"})`).join("\n");
|
|
399
|
+
return [
|
|
400
|
+
{ role: "system", content: DOM_SYSTEM_PROMPT },
|
|
401
|
+
{
|
|
402
|
+
role: "user",
|
|
403
|
+
content: `DOM structure:
|
|
404
|
+
${pageStructure}
|
|
405
|
+
|
|
406
|
+
Find ALL of these elements:
|
|
407
|
+
${queryList}
|
|
408
|
+
|
|
409
|
+
INSTRUCTIONS:
|
|
410
|
+
- Search the domTree and interactiveElements for each element.
|
|
411
|
+
- Use id, testId, css, xpath, name, classes, role, text, label, or placeholder to build Playwright locators.
|
|
412
|
+
- Prefer stable selectors over positional ones.
|
|
413
|
+
|
|
414
|
+
Return JSON array: [{"query":"...","locatorString":"page.locator(...)", "confidence": 0.0-1.0, "explanation":"...", "fallbackLocators":["..."]}]`
|
|
415
|
+
}
|
|
416
|
+
];
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// src/resolver.js
|
|
420
|
+
init_parser();
|
|
234
421
|
|
|
235
422
|
// src/cache/disk-cache.js
|
|
236
423
|
import fs from "fs";
|
|
@@ -356,6 +543,8 @@ function clearMemoryCache() {
|
|
|
356
543
|
|
|
357
544
|
// src/page-structure.js
|
|
358
545
|
var DEBUG3 = process.env.LLM_LOCATOR_DEBUG === "true";
|
|
546
|
+
var MAX_DOM_NODES = 300;
|
|
547
|
+
var MAX_INTERACTIVE = 100;
|
|
359
548
|
async function getPageStructure(page, forceRefresh = false) {
|
|
360
549
|
const url = page.url();
|
|
361
550
|
if (!forceRefresh && isStructureCacheValid(url)) {
|
|
@@ -382,7 +571,9 @@ async function getPageStructure(page, forceRefresh = false) {
|
|
|
382
571
|
async function getFrameStructure(pageOrFrame) {
|
|
383
572
|
let ariaTree = null;
|
|
384
573
|
let specialElements = [];
|
|
385
|
-
|
|
574
|
+
let interactiveElements = [];
|
|
575
|
+
let domTree = [];
|
|
576
|
+
const [ariaResult, domResult, interactiveResult, domTreeResult] = await Promise.allSettled([
|
|
386
577
|
pageOrFrame.locator("body").ariaSnapshot({ timeout: 5e3 }),
|
|
387
578
|
pageOrFrame.evaluate(() => {
|
|
388
579
|
const els = [];
|
|
@@ -399,7 +590,220 @@ async function getFrameStructure(pageOrFrame) {
|
|
|
399
590
|
if (e.testId || e.placeholder || e.alt || e.ariaLabel) els.push(e);
|
|
400
591
|
}
|
|
401
592
|
return els;
|
|
402
|
-
})
|
|
593
|
+
}),
|
|
594
|
+
pageOrFrame.evaluate((maxInteractive) => {
|
|
595
|
+
const els = [];
|
|
596
|
+
const selectors = 'select, input, textarea, button, a, [role="combobox"], [role="listbox"], [role="menu"], [role="tab"], [role="checkbox"], [role="radio"], [role="slider"], [role="switch"], [contenteditable="true"]';
|
|
597
|
+
for (const el of document.querySelectorAll(selectors)) {
|
|
598
|
+
if (els.length >= maxInteractive) break;
|
|
599
|
+
const ariaLabel = el.getAttribute("aria-label") || el.getAttribute("placeholder") || null;
|
|
600
|
+
const htmlName = el.getAttribute("name") || null;
|
|
601
|
+
const id = el.id || null;
|
|
602
|
+
const classes = el.className ? el.className.toString().split(/\s+/).filter((c) => c && c.length < 40).slice(0, 3).join(" ") : null;
|
|
603
|
+
const tag = el.tagName.toLowerCase();
|
|
604
|
+
const type = el.getAttribute("type") || null;
|
|
605
|
+
const role = el.getAttribute("role") || null;
|
|
606
|
+
const testIdAttr = el.getAttribute("data-testid") ? "data-testid" : el.getAttribute("data-test") ? "data-test" : el.getAttribute("data-test-id") ? "data-test-id" : el.getAttribute("data-qa") ? "data-qa" : el.getAttribute("data-cy") ? "data-cy" : null;
|
|
607
|
+
const testId = testIdAttr ? el.getAttribute(testIdAttr) : null;
|
|
608
|
+
const text = el.textContent?.trim().substring(0, 50) || null;
|
|
609
|
+
const labelEl = id ? document.querySelector(`label[for="${id}"]`) : null;
|
|
610
|
+
const labelText = labelEl ? labelEl.textContent.trim().substring(0, 50) : null;
|
|
611
|
+
let options = null;
|
|
612
|
+
if (tag === "select") {
|
|
613
|
+
options = [...el.options].map((o) => ({ value: o.value, text: o.text, selected: o.selected })).slice(0, 10);
|
|
614
|
+
}
|
|
615
|
+
const parent = el.closest("[class], [id], [data-testid], [data-test], [data-cy], [data-qa]");
|
|
616
|
+
let parentContext = null;
|
|
617
|
+
if (parent && parent !== el) {
|
|
618
|
+
const parentTestAttr = parent.getAttribute("data-testid") ? "data-testid" : parent.getAttribute("data-test") ? "data-test" : parent.getAttribute("data-qa") ? "data-qa" : parent.getAttribute("data-cy") ? "data-cy" : null;
|
|
619
|
+
parentContext = parent.id ? `#${parent.id}` : parentTestAttr ? `[${parentTestAttr}="${parent.getAttribute(parentTestAttr)}"]` : parent.className ? `.${parent.className.toString().split(/\s+/)[0]}` : null;
|
|
620
|
+
}
|
|
621
|
+
const selector = testId ? `[${testIdAttr}="${testId}"]` : id ? `[id="${id}"]` : htmlName ? `[name="${htmlName}"]` : classes ? `.${classes.split(" ")[0]}` : null;
|
|
622
|
+
els.push({
|
|
623
|
+
tag,
|
|
624
|
+
type,
|
|
625
|
+
role,
|
|
626
|
+
id,
|
|
627
|
+
classes,
|
|
628
|
+
ariaLabel,
|
|
629
|
+
htmlName,
|
|
630
|
+
testId,
|
|
631
|
+
labelText,
|
|
632
|
+
text: tag === "select" ? null : text,
|
|
633
|
+
options,
|
|
634
|
+
selector,
|
|
635
|
+
parentContext
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
return els;
|
|
639
|
+
}, MAX_INTERACTIVE),
|
|
640
|
+
pageOrFrame.evaluate((maxNodes) => {
|
|
641
|
+
function getXPath(el) {
|
|
642
|
+
if (el.id) return `//*[@id="${el.id}"]`;
|
|
643
|
+
const parts = [];
|
|
644
|
+
let current = el;
|
|
645
|
+
while (current && current.nodeType === 1) {
|
|
646
|
+
let idx = 0;
|
|
647
|
+
let sibling = current.previousSibling;
|
|
648
|
+
while (sibling) {
|
|
649
|
+
if (sibling.nodeType === 1 && sibling.tagName === current.tagName) idx++;
|
|
650
|
+
sibling = sibling.previousSibling;
|
|
651
|
+
}
|
|
652
|
+
const tag = current.tagName.toLowerCase();
|
|
653
|
+
parts.unshift(idx > 0 || current.nextElementSibling?.tagName === current.tagName ? `${tag}[${idx + 1}]` : tag);
|
|
654
|
+
current = current.parentElement;
|
|
655
|
+
}
|
|
656
|
+
return "/" + parts.join("/");
|
|
657
|
+
}
|
|
658
|
+
function getUniqueSelector(el) {
|
|
659
|
+
if (el.id) return `#${CSS.escape(el.id)}`;
|
|
660
|
+
const testAttrs = ["data-testid", "data-test-id", "data-test", "data-qa", "data-cy"];
|
|
661
|
+
for (const attr of testAttrs) {
|
|
662
|
+
const val = el.getAttribute(attr);
|
|
663
|
+
if (val) return `[${attr}="${CSS.escape(val)}"]`;
|
|
664
|
+
}
|
|
665
|
+
const tag = el.tagName.toLowerCase();
|
|
666
|
+
const name = el.getAttribute("name");
|
|
667
|
+
if (name) return `${tag}[name="${CSS.escape(name)}"]`;
|
|
668
|
+
const cls = el.className?.toString().trim();
|
|
669
|
+
if (cls) {
|
|
670
|
+
const sel = tag + "." + cls.split(/\s+/).map((c) => CSS.escape(c)).join(".");
|
|
671
|
+
if (document.querySelectorAll(sel).length === 1) return sel;
|
|
672
|
+
}
|
|
673
|
+
return null;
|
|
674
|
+
}
|
|
675
|
+
function isVisible(el) {
|
|
676
|
+
if (!el.offsetParent && el.tagName !== "BODY" && el.tagName !== "HTML" && getComputedStyle(el).position !== "fixed" && getComputedStyle(el).position !== "sticky") return false;
|
|
677
|
+
const style = getComputedStyle(el);
|
|
678
|
+
return style.display !== "none" && style.visibility !== "hidden" && parseFloat(style.opacity) > 0;
|
|
679
|
+
}
|
|
680
|
+
const interactiveTags = /* @__PURE__ */ new Set([
|
|
681
|
+
"a",
|
|
682
|
+
"button",
|
|
683
|
+
"input",
|
|
684
|
+
"textarea",
|
|
685
|
+
"select",
|
|
686
|
+
"details",
|
|
687
|
+
"summary",
|
|
688
|
+
"label"
|
|
689
|
+
]);
|
|
690
|
+
const interactiveRoles = /* @__PURE__ */ new Set([
|
|
691
|
+
"button",
|
|
692
|
+
"link",
|
|
693
|
+
"tab",
|
|
694
|
+
"menuitem",
|
|
695
|
+
"menuitemcheckbox",
|
|
696
|
+
"menuitemradio",
|
|
697
|
+
"option",
|
|
698
|
+
"checkbox",
|
|
699
|
+
"radio",
|
|
700
|
+
"switch",
|
|
701
|
+
"slider",
|
|
702
|
+
"spinbutton",
|
|
703
|
+
"combobox",
|
|
704
|
+
"listbox",
|
|
705
|
+
"searchbox",
|
|
706
|
+
"textbox",
|
|
707
|
+
"treeitem"
|
|
708
|
+
]);
|
|
709
|
+
const landmarkRoles = /* @__PURE__ */ new Set([
|
|
710
|
+
"banner",
|
|
711
|
+
"navigation",
|
|
712
|
+
"main",
|
|
713
|
+
"complementary",
|
|
714
|
+
"contentinfo",
|
|
715
|
+
"form",
|
|
716
|
+
"region",
|
|
717
|
+
"search",
|
|
718
|
+
"dialog",
|
|
719
|
+
"alertdialog",
|
|
720
|
+
"tablist",
|
|
721
|
+
"toolbar",
|
|
722
|
+
"menu",
|
|
723
|
+
"menubar",
|
|
724
|
+
"tree",
|
|
725
|
+
"grid",
|
|
726
|
+
"table",
|
|
727
|
+
"list"
|
|
728
|
+
]);
|
|
729
|
+
const structuralSel = "h1, h2, h3, h4, h5, h6, nav, main, header, footer, section, form, table, thead, tbody, tr, th, td, ul, ol, li, dl, dt, dd, fieldset, legend, [role]";
|
|
730
|
+
const nodes = [];
|
|
731
|
+
const allEls = document.querySelectorAll("body *");
|
|
732
|
+
for (const el of allEls) {
|
|
733
|
+
if (nodes.length >= maxNodes) break;
|
|
734
|
+
const tag = el.tagName.toLowerCase();
|
|
735
|
+
if (["script", "style", "noscript", "br", "hr", "wbr", "meta", "link"].includes(tag)) continue;
|
|
736
|
+
if (!isVisible(el)) continue;
|
|
737
|
+
const role = el.getAttribute("role") || null;
|
|
738
|
+
const isInteractive = interactiveTags.has(tag) || role && interactiveRoles.has(role) || el.getAttribute("contenteditable") === "true" || el.getAttribute("tabindex") !== null || el.getAttribute("onclick") !== null;
|
|
739
|
+
const isLandmark = landmarkRoles.has(role) || ["nav", "main", "header", "footer", "section", "form", "table"].includes(tag);
|
|
740
|
+
const isHeading = /^h[1-6]$/.test(tag);
|
|
741
|
+
const isStructural = el.matches(structuralSel);
|
|
742
|
+
if (!isInteractive && !isLandmark && !isHeading && !isStructural) continue;
|
|
743
|
+
const id = el.id || null;
|
|
744
|
+
const classes = el.className ? el.className.toString().split(/\s+/).filter((c) => c && c.length < 50).slice(0, 5).join(" ") : null;
|
|
745
|
+
const text = (el.textContent?.trim() || "").substring(0, 80) || null;
|
|
746
|
+
const directText = (() => {
|
|
747
|
+
let t = "";
|
|
748
|
+
for (const child of el.childNodes) {
|
|
749
|
+
if (child.nodeType === 3) t += child.textContent;
|
|
750
|
+
}
|
|
751
|
+
return t.trim().substring(0, 80) || null;
|
|
752
|
+
})();
|
|
753
|
+
const ariaLabel = el.getAttribute("aria-label") || null;
|
|
754
|
+
const title = el.getAttribute("title") || null;
|
|
755
|
+
const type = el.getAttribute("type") || null;
|
|
756
|
+
const href = tag === "a" ? (el.getAttribute("href") || "").substring(0, 100) : null;
|
|
757
|
+
const placeholder = el.getAttribute("placeholder") || null;
|
|
758
|
+
const htmlName = el.getAttribute("name") || null;
|
|
759
|
+
const value = tag === "input" || tag === "textarea" ? (el.value || "").substring(0, 50) || null : null;
|
|
760
|
+
const disabled = el.disabled || el.getAttribute("aria-disabled") === "true" || null;
|
|
761
|
+
const checked = el.checked !== void 0 ? el.checked : null;
|
|
762
|
+
const testIdAttr = el.getAttribute("data-testid") ? "data-testid" : el.getAttribute("data-test") ? "data-test" : el.getAttribute("data-test-id") ? "data-test-id" : el.getAttribute("data-qa") ? "data-qa" : el.getAttribute("data-cy") ? "data-cy" : null;
|
|
763
|
+
const testId = testIdAttr ? { attr: testIdAttr, value: el.getAttribute(testIdAttr) } : null;
|
|
764
|
+
const xpath = getXPath(el);
|
|
765
|
+
const cssSelector = getUniqueSelector(el);
|
|
766
|
+
const depth = (() => {
|
|
767
|
+
let d = 0;
|
|
768
|
+
let p = el.parentElement;
|
|
769
|
+
while (p && p !== document.body) {
|
|
770
|
+
d++;
|
|
771
|
+
p = p.parentElement;
|
|
772
|
+
}
|
|
773
|
+
return d;
|
|
774
|
+
})();
|
|
775
|
+
const node = { tag, depth };
|
|
776
|
+
if (role) node.role = role;
|
|
777
|
+
if (id) node.id = id;
|
|
778
|
+
if (classes) node.classes = classes;
|
|
779
|
+
if (testId) node.testId = testId;
|
|
780
|
+
if (ariaLabel) node.ariaLabel = ariaLabel;
|
|
781
|
+
if (title) node.title = title;
|
|
782
|
+
if (type) node.type = type;
|
|
783
|
+
if (htmlName) node.name = htmlName;
|
|
784
|
+
if (placeholder) node.placeholder = placeholder;
|
|
785
|
+
if (href) node.href = href;
|
|
786
|
+
if (directText) node.text = directText;
|
|
787
|
+
else if (text && text.length <= 80) node.text = text;
|
|
788
|
+
if (value) node.value = value;
|
|
789
|
+
if (disabled) node.disabled = true;
|
|
790
|
+
if (checked !== null && checked !== void 0) node.checked = checked;
|
|
791
|
+
if (isInteractive) node.interactive = true;
|
|
792
|
+
if (isHeading) node.heading = true;
|
|
793
|
+
if (isLandmark) node.landmark = true;
|
|
794
|
+
node.xpath = xpath;
|
|
795
|
+
if (cssSelector) node.css = cssSelector;
|
|
796
|
+
const labelEl = id ? document.querySelector(`label[for="${id}"]`) : null;
|
|
797
|
+
if (labelEl) node.label = labelEl.textContent.trim().substring(0, 50);
|
|
798
|
+
let selectOpts = null;
|
|
799
|
+
if (tag === "select") {
|
|
800
|
+
selectOpts = [...el.options].map((o) => ({ value: o.value, text: o.text, selected: o.selected })).slice(0, 15);
|
|
801
|
+
if (selectOpts.length) node.options = selectOpts;
|
|
802
|
+
}
|
|
803
|
+
nodes.push(node);
|
|
804
|
+
}
|
|
805
|
+
return nodes;
|
|
806
|
+
}, MAX_DOM_NODES)
|
|
403
807
|
]);
|
|
404
808
|
if (ariaResult.status === "fulfilled") ariaTree = ariaResult.value;
|
|
405
809
|
else {
|
|
@@ -412,7 +816,19 @@ async function getFrameStructure(pageOrFrame) {
|
|
|
412
816
|
}
|
|
413
817
|
}
|
|
414
818
|
if (domResult.status === "fulfilled") specialElements = domResult.value;
|
|
415
|
-
|
|
819
|
+
if (interactiveResult.status === "fulfilled") interactiveElements = interactiveResult.value;
|
|
820
|
+
if (domTreeResult.status === "fulfilled") domTree = domTreeResult.value;
|
|
821
|
+
const ariaQuality = evaluateAriaQuality(ariaTree, interactiveElements.length);
|
|
822
|
+
return { ariaTree, specialElements, interactiveElements, domTree, ariaQuality };
|
|
823
|
+
}
|
|
824
|
+
function evaluateAriaQuality(ariaTree, interactiveCount) {
|
|
825
|
+
if (!ariaTree || typeof ariaTree !== "string") return "none";
|
|
826
|
+
const lines = ariaTree.split("\n").filter((l) => l.trim());
|
|
827
|
+
const namedInteractive = (ariaTree.match(/(button|link|textbox|combobox|checkbox|tab|radio|switch)\s+"[^"]+"/g) || []).length;
|
|
828
|
+
if (lines.length < 5) return "none";
|
|
829
|
+
if (namedInteractive < 3 && interactiveCount > 5) return "sparse";
|
|
830
|
+
if (lines.length < 20 && interactiveCount > 10) return "sparse";
|
|
831
|
+
return "good";
|
|
416
832
|
}
|
|
417
833
|
function simplifyAriaTree(node, depth = 0) {
|
|
418
834
|
if (!node || depth > 8) return null;
|
|
@@ -466,30 +882,123 @@ function trimSnapshotForQuery(ariaYaml, query, maxLines = 150) {
|
|
|
466
882
|
}
|
|
467
883
|
return result.join("\n");
|
|
468
884
|
}
|
|
885
|
+
function filterDomTreeForQuery(domTree, query, maxNodes = 80) {
|
|
886
|
+
if (!domTree || !domTree.length) return domTree;
|
|
887
|
+
if (domTree.length <= maxNodes) return domTree;
|
|
888
|
+
const queryTokens = query.toLowerCase().replace(/[^a-z0-9\s]/g, "").split(/\s+/).filter(Boolean);
|
|
889
|
+
const scored = domTree.map((node, idx) => {
|
|
890
|
+
let score = 0;
|
|
891
|
+
if (node.interactive) score += 5;
|
|
892
|
+
if (node.heading) score += 3;
|
|
893
|
+
if (node.landmark) score += 2;
|
|
894
|
+
const searchable = [node.text, node.ariaLabel, node.label, node.placeholder, node.title, node.id, node.name, node.testId?.value].filter(Boolean).join(" ").toLowerCase();
|
|
895
|
+
for (const token of queryTokens) {
|
|
896
|
+
if (searchable.includes(token)) score += 15;
|
|
897
|
+
}
|
|
898
|
+
return { node, idx, score };
|
|
899
|
+
});
|
|
900
|
+
scored.sort((a, b) => b.score - a.score);
|
|
901
|
+
return scored.slice(0, maxNodes).sort((a, b) => a.idx - b.idx).map((s) => s.node);
|
|
902
|
+
}
|
|
469
903
|
|
|
470
904
|
// src/resolver.js
|
|
471
905
|
var DEBUG4 = process.env.LLM_LOCATOR_DEBUG === "true";
|
|
906
|
+
var PAYLOAD_LIMITS = {
|
|
907
|
+
good: 15e3,
|
|
908
|
+
sparse: 25e3,
|
|
909
|
+
none: 3e4
|
|
910
|
+
};
|
|
472
911
|
function buildPayload(pageStructure, query) {
|
|
912
|
+
const quality = pageStructure.mainFrame.ariaQuality || "good";
|
|
913
|
+
const maxPayload = PAYLOAD_LIMITS[quality] || PAYLOAD_LIMITS.good;
|
|
914
|
+
if (quality === "none") {
|
|
915
|
+
return buildDomOnlyPayload(pageStructure, query, maxPayload);
|
|
916
|
+
}
|
|
917
|
+
if (quality === "sparse") {
|
|
918
|
+
return buildHybridPayload(pageStructure, query, maxPayload);
|
|
919
|
+
}
|
|
920
|
+
return buildAriaPayload(pageStructure, query, maxPayload);
|
|
921
|
+
}
|
|
922
|
+
function buildAriaPayload(pageStructure, query, maxPayload) {
|
|
473
923
|
const trimmedAria = trimSnapshotForQuery(pageStructure.mainFrame.ariaTree, query);
|
|
924
|
+
const specialElements = pageStructure.mainFrame.specialElements;
|
|
925
|
+
const interactiveElements = pageStructure.mainFrame.interactiveElements || [];
|
|
474
926
|
const payload = {
|
|
927
|
+
mode: "aria",
|
|
475
928
|
ariaTree: trimmedAria,
|
|
476
|
-
specialElements:
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
ariaTree: trimSnapshotForQuery(f.content?.ariaTree, query, 50),
|
|
480
|
-
specialElements: f.content?.specialElements || []
|
|
481
|
-
})) : void 0
|
|
929
|
+
specialElements: specialElements.length > 30 ? specialElements.slice(0, 30) : specialElements,
|
|
930
|
+
interactiveElements: interactiveElements.length > 50 ? interactiveElements.slice(0, 50) : interactiveElements,
|
|
931
|
+
frames: buildFramesPayload(pageStructure, query)
|
|
482
932
|
};
|
|
483
|
-
let payloadStr = JSON.stringify(payload
|
|
484
|
-
if (payloadStr.length >
|
|
485
|
-
return payloadStr;
|
|
933
|
+
let payloadStr = JSON.stringify(payload);
|
|
934
|
+
if (payloadStr.length > maxPayload) payloadStr = payloadStr.substring(0, maxPayload) + "...(truncated)";
|
|
935
|
+
return { payloadStr, mode: "aria" };
|
|
936
|
+
}
|
|
937
|
+
function buildHybridPayload(pageStructure, query, maxPayload) {
|
|
938
|
+
const trimmedAria = trimSnapshotForQuery(pageStructure.mainFrame.ariaTree, query);
|
|
939
|
+
const interactiveElements = pageStructure.mainFrame.interactiveElements || [];
|
|
940
|
+
const domTree = filterDomTreeForQuery(pageStructure.mainFrame.domTree || [], query, 120);
|
|
941
|
+
const payload = {
|
|
942
|
+
mode: "hybrid",
|
|
943
|
+
ariaTree: trimmedAria,
|
|
944
|
+
interactiveElements: interactiveElements.slice(0, 60),
|
|
945
|
+
domTree,
|
|
946
|
+
frames: buildFramesPayload(pageStructure, query)
|
|
947
|
+
};
|
|
948
|
+
let payloadStr = JSON.stringify(payload);
|
|
949
|
+
if (payloadStr.length > maxPayload) {
|
|
950
|
+
payload.domTree = filterDomTreeForQuery(pageStructure.mainFrame.domTree || [], query, 60);
|
|
951
|
+
payloadStr = JSON.stringify(payload);
|
|
952
|
+
}
|
|
953
|
+
if (payloadStr.length > maxPayload) payloadStr = payloadStr.substring(0, maxPayload) + "...(truncated)";
|
|
954
|
+
return { payloadStr, mode: "hybrid" };
|
|
955
|
+
}
|
|
956
|
+
function buildDomOnlyPayload(pageStructure, query, maxPayload) {
|
|
957
|
+
const interactiveElements = pageStructure.mainFrame.interactiveElements || [];
|
|
958
|
+
const domTree = filterDomTreeForQuery(pageStructure.mainFrame.domTree || [], query, 150);
|
|
959
|
+
const specialElements = pageStructure.mainFrame.specialElements || [];
|
|
960
|
+
const payload = {
|
|
961
|
+
mode: "dom",
|
|
962
|
+
domTree,
|
|
963
|
+
specialElements: specialElements.slice(0, 30),
|
|
964
|
+
interactiveElements: interactiveElements.slice(0, 80),
|
|
965
|
+
frames: buildFramesPayload(pageStructure, query)
|
|
966
|
+
};
|
|
967
|
+
let payloadStr = JSON.stringify(payload);
|
|
968
|
+
if (payloadStr.length > maxPayload) {
|
|
969
|
+
payload.domTree = filterDomTreeForQuery(pageStructure.mainFrame.domTree || [], query, 80);
|
|
970
|
+
payload.interactiveElements = interactiveElements.slice(0, 50);
|
|
971
|
+
payloadStr = JSON.stringify(payload);
|
|
972
|
+
}
|
|
973
|
+
if (payloadStr.length > maxPayload) payloadStr = payloadStr.substring(0, maxPayload) + "...(truncated)";
|
|
974
|
+
return { payloadStr, mode: "dom" };
|
|
975
|
+
}
|
|
976
|
+
function buildFramesPayload(pageStructure, query) {
|
|
977
|
+
if (!pageStructure.frames || pageStructure.frames.length === 0) return void 0;
|
|
978
|
+
return pageStructure.frames.map((f) => {
|
|
979
|
+
const frame = { selector: f.selector };
|
|
980
|
+
if (f.content?.ariaTree) {
|
|
981
|
+
frame.ariaTree = trimSnapshotForQuery(f.content.ariaTree, query, 50);
|
|
982
|
+
}
|
|
983
|
+
if (f.content?.specialElements) {
|
|
984
|
+
frame.specialElements = (f.content.specialElements || []).slice(0, 10);
|
|
985
|
+
}
|
|
986
|
+
if (f.content?.interactiveElements) {
|
|
987
|
+
frame.interactiveElements = (f.content.interactiveElements || []).slice(0, 20);
|
|
988
|
+
}
|
|
989
|
+
if (f.content?.domTree) {
|
|
990
|
+
frame.domTree = filterDomTreeForQuery(f.content.domTree || [], query, 30);
|
|
991
|
+
}
|
|
992
|
+
return frame;
|
|
993
|
+
});
|
|
486
994
|
}
|
|
487
995
|
async function queryLLM(pageStructure, query, options = {}) {
|
|
488
996
|
const { action = null } = options;
|
|
489
|
-
const payloadStr = buildPayload(pageStructure, query);
|
|
490
|
-
if (DEBUG4) console.log(
|
|
491
|
-
const messages = buildMessages(payloadStr, query, action);
|
|
492
|
-
const
|
|
997
|
+
const { payloadStr, mode } = buildPayload(pageStructure, query);
|
|
998
|
+
if (DEBUG4) console.log(`[Resolver] mode=${mode} payload=${payloadStr.length} chars`);
|
|
999
|
+
const messages = mode === "dom" ? buildDomMessages(payloadStr, query, action) : mode === "hybrid" ? buildDomMessages(payloadStr, query, action) : buildMessages(payloadStr, query, action);
|
|
1000
|
+
const maxTokens = mode === "aria" ? 256 : 512;
|
|
1001
|
+
const raw = await chatCompletion(messages, { ...options, maxTokens });
|
|
493
1002
|
return parseJsonResponse(raw);
|
|
494
1003
|
}
|
|
495
1004
|
async function resolveLocator(page, query, options = {}) {
|
|
@@ -499,7 +1008,7 @@ async function resolveLocator(page, query, options = {}) {
|
|
|
499
1008
|
const memHit = getMemEntry(url, query, action);
|
|
500
1009
|
if (memHit) {
|
|
501
1010
|
if (shouldDebug) console.log(`[Memory Cache Hit] "${query}" \u2192 ${memHit.locatorString}`);
|
|
502
|
-
return memHit;
|
|
1011
|
+
return { ...memHit, source: "memory" };
|
|
503
1012
|
}
|
|
504
1013
|
const diskHit = getDiskEntry(url, query, action);
|
|
505
1014
|
if (diskHit) {
|
|
@@ -557,6 +1066,17 @@ async function getLocator(page, query, options = {}) {
|
|
|
557
1066
|
bumpHitCount(url, query, action);
|
|
558
1067
|
return { locator, result };
|
|
559
1068
|
}
|
|
1069
|
+
if (result.fallbackLocators && result.fallbackLocators.length > 0) {
|
|
1070
|
+
if (shouldDebug) console.log(`[Fallback] Primary "${result.locatorString}" not found, trying ${result.fallbackLocators.length} fallbacks...`);
|
|
1071
|
+
const fbResult = await validateWithFallback(page, result.locatorString, result.fallbackLocators, result.isInFrame, result.frameSelector);
|
|
1072
|
+
if (fbResult && fbResult.locatorString !== result.locatorString) {
|
|
1073
|
+
if (shouldDebug) console.log(`[Fallback] Using fallback: ${fbResult.locatorString}`);
|
|
1074
|
+
const updatedResult = { ...result, locatorString: fbResult.locatorString, strategy: "fallback" };
|
|
1075
|
+
setMemEntry(url, query, action, updatedResult);
|
|
1076
|
+
setDiskEntry(url, query, action, updatedResult);
|
|
1077
|
+
return { locator: fbResult.locator, result: updatedResult };
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
560
1080
|
if (shouldDebug) console.log(`[Stale Cache] "${query}" \u2192 ${result.locatorString} no longer valid, re-querying LLM...`);
|
|
561
1081
|
invalidateDiskEntry(url, query, action);
|
|
562
1082
|
deleteMemEntry(url, query, action);
|
|
@@ -566,6 +1086,20 @@ async function getLocator(page, query, options = {}) {
|
|
|
566
1086
|
const freshLocator = createPlaywrightLocator(page, freshResult.locatorString, freshResult.isInFrame, freshResult.frameSelector);
|
|
567
1087
|
return { locator: freshLocator, result: freshResult };
|
|
568
1088
|
}
|
|
1089
|
+
if (result.source === "llm" && result.fallbackLocators && result.fallbackLocators.length > 0) {
|
|
1090
|
+
const valid = await validateLocator(locator, 2e3);
|
|
1091
|
+
if (!valid) {
|
|
1092
|
+
if (shouldDebug) console.log(`[Fallback] Primary "${result.locatorString}" not found, trying ${result.fallbackLocators.length} fallbacks...`);
|
|
1093
|
+
const fbResult = await validateWithFallback(page, result.locatorString, result.fallbackLocators, result.isInFrame, result.frameSelector);
|
|
1094
|
+
if (fbResult && fbResult.locatorString !== result.locatorString) {
|
|
1095
|
+
if (shouldDebug) console.log(`[Fallback] Using fallback: ${fbResult.locatorString}`);
|
|
1096
|
+
const updatedResult = { ...result, locatorString: fbResult.locatorString, strategy: "fallback" };
|
|
1097
|
+
setMemEntry(url, query, action, updatedResult);
|
|
1098
|
+
setDiskEntry(url, query, action, updatedResult);
|
|
1099
|
+
return { locator: fbResult.locator, result: updatedResult };
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
569
1103
|
return { locator, result };
|
|
570
1104
|
}
|
|
571
1105
|
async function resolveLocatorsBatch(page, queries, options = {}) {
|
|
@@ -601,11 +1135,11 @@ async function resolveLocatorsBatch(page, queries, options = {}) {
|
|
|
601
1135
|
if (shouldDebug) console.log(`[Batch] Resolving ${uncached.length} queries in 1 call`);
|
|
602
1136
|
const t0 = Date.now();
|
|
603
1137
|
const pageStructure = await getPageStructure(page);
|
|
604
|
-
|
|
605
|
-
if (payloadStr.length > 2e4) payloadStr = payloadStr.substring(0, 2e4) + "\n...(truncated)";
|
|
1138
|
+
const { payloadStr, mode } = buildPayload(pageStructure, uncached.join(" "));
|
|
606
1139
|
try {
|
|
607
|
-
const messages = buildBatchMessages(payloadStr, uncached);
|
|
608
|
-
const
|
|
1140
|
+
const messages = mode === "dom" || mode === "hybrid" ? buildDomBatchMessages(payloadStr, uncached) : buildBatchMessages(payloadStr, uncached);
|
|
1141
|
+
const maxTokens = mode === "aria" ? 256 : 512;
|
|
1142
|
+
const raw = await chatCompletion(messages, { ...options, maxTokens });
|
|
609
1143
|
const parsed = parseJsonResponse(raw);
|
|
610
1144
|
if (shouldDebug) console.log(`[Batch] ${parsed.length} results in ${Date.now() - t0}ms`);
|
|
611
1145
|
for (let i = 0; i < uncached.length && i < parsed.length; i++) {
|
|
@@ -655,66 +1189,84 @@ var SmartAction = class _SmartAction {
|
|
|
655
1189
|
get query() {
|
|
656
1190
|
return this.#query;
|
|
657
1191
|
}
|
|
1192
|
+
async #safeAction(action, ...args) {
|
|
1193
|
+
try {
|
|
1194
|
+
return await this.#locator[action](...args);
|
|
1195
|
+
} catch (err) {
|
|
1196
|
+
if (err.message && err.message.includes("strict mode violation")) {
|
|
1197
|
+
if (DEBUG5) console.log(`[SmartAction] Strict mode on "${this.#query}", retrying with .first()`);
|
|
1198
|
+
return await this.#locator.first()[action](...args);
|
|
1199
|
+
}
|
|
1200
|
+
throw err;
|
|
1201
|
+
}
|
|
1202
|
+
}
|
|
658
1203
|
async click(options) {
|
|
659
|
-
return this.#
|
|
1204
|
+
return this.#safeAction("click", options);
|
|
660
1205
|
}
|
|
661
1206
|
async dblclick(options) {
|
|
662
|
-
return this.#
|
|
1207
|
+
return this.#safeAction("dblclick", options);
|
|
663
1208
|
}
|
|
664
1209
|
async tap(options) {
|
|
665
|
-
return this.#
|
|
1210
|
+
return this.#safeAction("tap", options);
|
|
666
1211
|
}
|
|
667
1212
|
async hover(options) {
|
|
668
|
-
return this.#
|
|
1213
|
+
return this.#safeAction("hover", options);
|
|
669
1214
|
}
|
|
670
1215
|
async focus() {
|
|
671
|
-
return this.#
|
|
1216
|
+
return this.#safeAction("focus");
|
|
672
1217
|
}
|
|
673
1218
|
async blur() {
|
|
674
|
-
return this.#
|
|
1219
|
+
return this.#safeAction("blur");
|
|
675
1220
|
}
|
|
676
1221
|
async fill(value, options) {
|
|
677
|
-
return this.#
|
|
1222
|
+
return this.#safeAction("fill", value, options);
|
|
678
1223
|
}
|
|
679
1224
|
async type(text, options) {
|
|
680
|
-
return this.#
|
|
1225
|
+
return this.#safeAction("type", text, options);
|
|
681
1226
|
}
|
|
682
1227
|
async press(key, options) {
|
|
683
|
-
return this.#
|
|
1228
|
+
return this.#safeAction("press", key, options);
|
|
684
1229
|
}
|
|
685
1230
|
async pressSequentially(text, options) {
|
|
686
|
-
return this.#
|
|
1231
|
+
return this.#safeAction("pressSequentially", text, options);
|
|
687
1232
|
}
|
|
688
1233
|
async clear(options) {
|
|
689
|
-
return this.#
|
|
1234
|
+
return this.#safeAction("clear", options);
|
|
690
1235
|
}
|
|
691
1236
|
async setInputFiles(files, options) {
|
|
692
|
-
return this.#
|
|
1237
|
+
return this.#safeAction("setInputFiles", files, options);
|
|
693
1238
|
}
|
|
694
1239
|
async selectOption(values, options) {
|
|
695
|
-
return this.#
|
|
1240
|
+
return this.#safeAction("selectOption", values, options);
|
|
696
1241
|
}
|
|
697
1242
|
async selectText(options) {
|
|
698
|
-
return this.#
|
|
1243
|
+
return this.#safeAction("selectText", options);
|
|
699
1244
|
}
|
|
700
1245
|
async check(options) {
|
|
701
|
-
return this.#
|
|
1246
|
+
return this.#safeAction("check", options);
|
|
702
1247
|
}
|
|
703
1248
|
async uncheck(options) {
|
|
704
|
-
return this.#
|
|
1249
|
+
return this.#safeAction("uncheck", options);
|
|
705
1250
|
}
|
|
706
1251
|
async setChecked(checked, options) {
|
|
707
|
-
return this.#
|
|
1252
|
+
return this.#safeAction("setChecked", checked, options);
|
|
708
1253
|
}
|
|
709
1254
|
async scrollIntoViewIfNeeded(options) {
|
|
710
|
-
return this.#
|
|
1255
|
+
return this.#safeAction("scrollIntoViewIfNeeded", options);
|
|
711
1256
|
}
|
|
712
1257
|
async screenshot(options) {
|
|
713
|
-
return this.#
|
|
1258
|
+
return this.#safeAction("screenshot", options);
|
|
714
1259
|
}
|
|
715
1260
|
async dragTo(target, options) {
|
|
716
1261
|
const dest = target instanceof _SmartAction ? target.rawLocator : target;
|
|
717
|
-
|
|
1262
|
+
try {
|
|
1263
|
+
return await this.#locator.dragTo(dest, options);
|
|
1264
|
+
} catch (err) {
|
|
1265
|
+
if (err.message && err.message.includes("strict mode violation")) {
|
|
1266
|
+
return await this.#locator.first().dragTo(dest, options);
|
|
1267
|
+
}
|
|
1268
|
+
throw err;
|
|
1269
|
+
}
|
|
718
1270
|
}
|
|
719
1271
|
async waitFor(options) {
|
|
720
1272
|
return this.#locator.waitFor(options);
|
|
@@ -750,22 +1302,22 @@ var SmartAction = class _SmartAction {
|
|
|
750
1302
|
return this.#locator.isEditable();
|
|
751
1303
|
}
|
|
752
1304
|
async textContent() {
|
|
753
|
-
return this.#
|
|
1305
|
+
return this.#safeAction("textContent");
|
|
754
1306
|
}
|
|
755
1307
|
async innerText() {
|
|
756
|
-
return this.#
|
|
1308
|
+
return this.#safeAction("innerText");
|
|
757
1309
|
}
|
|
758
1310
|
async innerHTML() {
|
|
759
|
-
return this.#
|
|
1311
|
+
return this.#safeAction("innerHTML");
|
|
760
1312
|
}
|
|
761
1313
|
async inputValue(options) {
|
|
762
|
-
return this.#
|
|
1314
|
+
return this.#safeAction("inputValue", options);
|
|
763
1315
|
}
|
|
764
1316
|
async getAttribute(name) {
|
|
765
|
-
return this.#
|
|
1317
|
+
return this.#safeAction("getAttribute", name);
|
|
766
1318
|
}
|
|
767
1319
|
async boundingBox() {
|
|
768
|
-
return this.#
|
|
1320
|
+
return this.#safeAction("boundingBox");
|
|
769
1321
|
}
|
|
770
1322
|
async count() {
|
|
771
1323
|
return this.#locator.count();
|
|
@@ -780,13 +1332,13 @@ var SmartAction = class _SmartAction {
|
|
|
780
1332
|
return this.#locator.all();
|
|
781
1333
|
}
|
|
782
1334
|
async evaluate(pageFunction, arg) {
|
|
783
|
-
return this.#
|
|
1335
|
+
return this.#safeAction("evaluate", pageFunction, arg);
|
|
784
1336
|
}
|
|
785
1337
|
async evaluateAll(pageFunction, arg) {
|
|
786
1338
|
return this.#locator.evaluateAll(pageFunction, arg);
|
|
787
1339
|
}
|
|
788
1340
|
async evaluateHandle(pageFunction, arg) {
|
|
789
|
-
return this.#
|
|
1341
|
+
return this.#safeAction("evaluateHandle", pageFunction, arg);
|
|
790
1342
|
}
|
|
791
1343
|
first() {
|
|
792
1344
|
return this.#locator.first();
|
|
@@ -832,14 +1384,15 @@ var SmartAction = class _SmartAction {
|
|
|
832
1384
|
}
|
|
833
1385
|
}
|
|
834
1386
|
async getClasses() {
|
|
835
|
-
const cls = await this
|
|
1387
|
+
const cls = await this.getAttribute("class");
|
|
836
1388
|
return cls ? cls.split(/\s+/).filter(Boolean) : [];
|
|
837
1389
|
}
|
|
838
1390
|
async hasClass(className) {
|
|
839
1391
|
return (await this.getClasses()).includes(className);
|
|
840
1392
|
}
|
|
841
1393
|
async getCssProperty(property) {
|
|
842
|
-
return this.#
|
|
1394
|
+
return this.#safeAction(
|
|
1395
|
+
"evaluate",
|
|
843
1396
|
(el, prop) => window.getComputedStyle(el).getPropertyValue(prop),
|
|
844
1397
|
property
|
|
845
1398
|
);
|
|
@@ -858,12 +1411,21 @@ function clearAllCaches() {
|
|
|
858
1411
|
clearDiskCache();
|
|
859
1412
|
}
|
|
860
1413
|
function createSmartLocator(page, options = {}) {
|
|
861
|
-
const { verbose = false } = options;
|
|
1414
|
+
const { verbose = false, actionTimeout = 1e4 } = options;
|
|
862
1415
|
async function resolve(query, action) {
|
|
863
1416
|
const { locator, result } = await getLocator(page, query, { ...options, action });
|
|
864
1417
|
if (verbose) console.log(`[Smart Locator] ${action || "locate"}("${query}") \u2192 ${result.locatorString} (${result.source})`);
|
|
865
1418
|
return new SmartAction(locator, result, query);
|
|
866
1419
|
}
|
|
1420
|
+
function withTimeout(promise, ms, label) {
|
|
1421
|
+
if (!ms) return promise;
|
|
1422
|
+
return Promise.race([
|
|
1423
|
+
promise,
|
|
1424
|
+
new Promise(
|
|
1425
|
+
(_, reject) => setTimeout(() => reject(new Error(`Timeout: "${label}" exceeded ${ms}ms`)), ms)
|
|
1426
|
+
)
|
|
1427
|
+
]);
|
|
1428
|
+
}
|
|
867
1429
|
return {
|
|
868
1430
|
async prefetch(...queries) {
|
|
869
1431
|
const t0 = Date.now();
|
|
@@ -876,93 +1438,93 @@ function createSmartLocator(page, options = {}) {
|
|
|
876
1438
|
},
|
|
877
1439
|
async click(query, clickOptions) {
|
|
878
1440
|
const el = await resolve(query, "click");
|
|
879
|
-
await el.click(clickOptions);
|
|
1441
|
+
await withTimeout(el.click(clickOptions), actionTimeout, `click("${query}")`);
|
|
880
1442
|
return el;
|
|
881
1443
|
},
|
|
882
1444
|
async dblclick(query, clickOptions) {
|
|
883
1445
|
const el = await resolve(query, "click");
|
|
884
|
-
await el.dblclick(clickOptions);
|
|
1446
|
+
await withTimeout(el.dblclick(clickOptions), actionTimeout, `dblclick("${query}")`);
|
|
885
1447
|
return el;
|
|
886
1448
|
},
|
|
887
1449
|
async fill(query, value, fillOptions) {
|
|
888
1450
|
const el = await resolve(query, "fill");
|
|
889
|
-
await el.fill(value, fillOptions);
|
|
1451
|
+
await withTimeout(el.fill(value, fillOptions), actionTimeout, `fill("${query}")`);
|
|
890
1452
|
return el;
|
|
891
1453
|
},
|
|
892
1454
|
async type(query, text, typeOptions) {
|
|
893
1455
|
const el = await resolve(query, "fill");
|
|
894
|
-
await el.type(text, typeOptions);
|
|
1456
|
+
await withTimeout(el.type(text, typeOptions), actionTimeout, `type("${query}")`);
|
|
895
1457
|
return el;
|
|
896
1458
|
},
|
|
897
1459
|
async pressSequentially(query, text, pressOptions) {
|
|
898
1460
|
const el = await resolve(query, "fill");
|
|
899
|
-
await el.pressSequentially(text, pressOptions);
|
|
1461
|
+
await withTimeout(el.pressSequentially(text, pressOptions), actionTimeout, `pressSequentially("${query}")`);
|
|
900
1462
|
return el;
|
|
901
1463
|
},
|
|
902
1464
|
async clear(query) {
|
|
903
1465
|
const el = await resolve(query, "fill");
|
|
904
|
-
await el.clear();
|
|
1466
|
+
await withTimeout(el.clear(), actionTimeout, `clear("${query}")`);
|
|
905
1467
|
return el;
|
|
906
1468
|
},
|
|
907
1469
|
async press(query, key, pressOptions) {
|
|
908
1470
|
const el = await resolve(query, "fill");
|
|
909
|
-
await el.press(key, pressOptions);
|
|
1471
|
+
await withTimeout(el.press(key, pressOptions), actionTimeout, `press("${query}")`);
|
|
910
1472
|
return el;
|
|
911
1473
|
},
|
|
912
1474
|
async check(query, checkOptions) {
|
|
913
1475
|
const el = await resolve(query, "check");
|
|
914
|
-
await el.check(checkOptions);
|
|
1476
|
+
await withTimeout(el.check(checkOptions), actionTimeout, `check("${query}")`);
|
|
915
1477
|
return el;
|
|
916
1478
|
},
|
|
917
1479
|
async uncheck(query, uncheckOptions) {
|
|
918
1480
|
const el = await resolve(query, "uncheck");
|
|
919
|
-
await el.uncheck(uncheckOptions);
|
|
1481
|
+
await withTimeout(el.uncheck(uncheckOptions), actionTimeout, `uncheck("${query}")`);
|
|
920
1482
|
return el;
|
|
921
1483
|
},
|
|
922
1484
|
async setChecked(query, checked, checkOptions) {
|
|
923
1485
|
const el = await resolve(query, checked ? "check" : "uncheck");
|
|
924
|
-
await el.setChecked(checked, checkOptions);
|
|
1486
|
+
await withTimeout(el.setChecked(checked, checkOptions), actionTimeout, `setChecked("${query}")`);
|
|
925
1487
|
return el;
|
|
926
1488
|
},
|
|
927
1489
|
async select(query, values, selectOptions) {
|
|
928
1490
|
const el = await resolve(query, "select");
|
|
929
|
-
await el.selectOption(values, selectOptions);
|
|
1491
|
+
await withTimeout(el.selectOption(values, selectOptions), actionTimeout, `select("${query}")`);
|
|
930
1492
|
return el;
|
|
931
1493
|
},
|
|
932
1494
|
async hover(query, hoverOptions) {
|
|
933
1495
|
const el = await resolve(query, "hover");
|
|
934
|
-
await el.hover(hoverOptions);
|
|
1496
|
+
await withTimeout(el.hover(hoverOptions), actionTimeout, `hover("${query}")`);
|
|
935
1497
|
return el;
|
|
936
1498
|
},
|
|
937
1499
|
async focus(query) {
|
|
938
1500
|
const el = await resolve(query, "click");
|
|
939
|
-
await el.focus();
|
|
1501
|
+
await withTimeout(el.focus(), actionTimeout, `focus("${query}")`);
|
|
940
1502
|
return el;
|
|
941
1503
|
},
|
|
942
1504
|
async tap(query, tapOptions) {
|
|
943
1505
|
const el = await resolve(query, "click");
|
|
944
|
-
await el.tap(tapOptions);
|
|
1506
|
+
await withTimeout(el.tap(tapOptions), actionTimeout, `tap("${query}")`);
|
|
945
1507
|
return el;
|
|
946
1508
|
},
|
|
947
1509
|
async setInputFiles(query, files, fileOptions) {
|
|
948
1510
|
const el = await resolve(query, "fill");
|
|
949
|
-
await el.setInputFiles(files, fileOptions);
|
|
1511
|
+
await withTimeout(el.setInputFiles(files, fileOptions), actionTimeout, `setInputFiles("${query}")`);
|
|
950
1512
|
return el;
|
|
951
1513
|
},
|
|
952
1514
|
async dragTo(srcQuery, destQuery, dragOptions) {
|
|
953
1515
|
const src = await resolve(srcQuery, "click");
|
|
954
1516
|
const dest = await resolve(destQuery, "click");
|
|
955
|
-
await src.dragTo(dest, dragOptions);
|
|
1517
|
+
await withTimeout(src.dragTo(dest, dragOptions), actionTimeout, `dragTo("${srcQuery}")`);
|
|
956
1518
|
return { source: src, target: dest };
|
|
957
1519
|
},
|
|
958
1520
|
async selectText(query) {
|
|
959
1521
|
const el = await resolve(query, "click");
|
|
960
|
-
await el.selectText();
|
|
1522
|
+
await withTimeout(el.selectText(), actionTimeout, `selectText("${query}")`);
|
|
961
1523
|
return el;
|
|
962
1524
|
},
|
|
963
1525
|
async scrollIntoView(query) {
|
|
964
1526
|
const el = await resolve(query, null);
|
|
965
|
-
await el.scrollIntoViewIfNeeded();
|
|
1527
|
+
await withTimeout(el.scrollIntoViewIfNeeded(), actionTimeout, `scrollIntoView("${query}")`);
|
|
966
1528
|
return el;
|
|
967
1529
|
},
|
|
968
1530
|
async screenshot(query, screenshotOptions) {
|
|
@@ -1049,8 +1611,11 @@ function createSmartLocator(page, options = {}) {
|
|
|
1049
1611
|
},
|
|
1050
1612
|
async exists(query) {
|
|
1051
1613
|
try {
|
|
1052
|
-
const
|
|
1053
|
-
|
|
1614
|
+
const result = await resolveLocator(page, query, { ...options, action: null });
|
|
1615
|
+
if (!result.found || result.confidence < 0.7) return false;
|
|
1616
|
+
const { createPlaywrightLocator: createPlaywrightLocator2 } = await Promise.resolve().then(() => (init_parser(), parser_exports));
|
|
1617
|
+
const locator = createPlaywrightLocator2(page, result.locatorString, result.isInFrame, result.frameSelector);
|
|
1618
|
+
return await locator.count() > 0;
|
|
1054
1619
|
} catch {
|
|
1055
1620
|
return false;
|
|
1056
1621
|
}
|