@tontoko/fast-playwright-mcp 0.1.0 → 0.1.2

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.
@@ -0,0 +1,541 @@
1
+ import { createRequire } from "node:module";
2
+ var __create = Object.create;
3
+ var __getProtoOf = Object.getPrototypeOf;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __toESM = (mod, isNodeMode, target) => {
8
+ target = mod != null ? __create(__getProtoOf(mod)) : {};
9
+ const to = isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target;
10
+ for (let key of __getOwnPropNames(mod))
11
+ if (!__hasOwnProp.call(to, key))
12
+ __defProp(to, key, {
13
+ get: () => mod[key],
14
+ enumerable: true
15
+ });
16
+ return to;
17
+ };
18
+ var __require = /* @__PURE__ */ createRequire(import.meta.url);
19
+
20
+ // src/services/selector-resolver.ts
21
+ import {
22
+ isCSSSelector,
23
+ isRefSelector,
24
+ isRoleSelector,
25
+ isTextSelector,
26
+ ResolutionStrategy,
27
+ SelectorConfidence,
28
+ validateElementSelector
29
+ } from "../types/selectors.js";
30
+ import { tabDebug } from "../utils/log.js";
31
+ var GENERIC_TAG_SELECTOR = /^[a-z]+$/;
32
+
33
+ class SelectorResolver {
34
+ page;
35
+ defaultTimeoutMs = 3000;
36
+ constructor(page) {
37
+ this.page = page;
38
+ }
39
+ async resolveSelectors(selectors, options = {}) {
40
+ const startTime = Date.now();
41
+ const {
42
+ timeoutMs = this.defaultTimeoutMs,
43
+ continueOnError = true,
44
+ executionStrategy = "hybrid"
45
+ } = options;
46
+ tabDebug(`Resolving ${selectors.length} selectors using ${executionStrategy} strategy`);
47
+ const validatedSelectors = this._validateSelectors(selectors);
48
+ const categorized = this._categorizeSelectors(validatedSelectors);
49
+ try {
50
+ switch (executionStrategy) {
51
+ case "parallel":
52
+ return await this._resolveParallel(categorized, timeoutMs, continueOnError);
53
+ case "sequential":
54
+ return await this._resolveSequential(categorized, timeoutMs, continueOnError);
55
+ default:
56
+ return await this._resolveHybrid(categorized, timeoutMs, continueOnError);
57
+ }
58
+ } catch (error) {
59
+ tabDebug(`Selector resolution failed after ${Date.now() - startTime}ms:`, error);
60
+ throw error;
61
+ }
62
+ }
63
+ async resolveSingleSelector(selector, options = {}) {
64
+ const { timeoutMs = this.defaultTimeoutMs } = options;
65
+ const startTime = Date.now();
66
+ try {
67
+ validateElementSelector(selector);
68
+ const result = await this._resolveSelectorWithTimeout(selector, timeoutMs);
69
+ if (result.locator && !result.error) {
70
+ const metadata = await this._extractElementMetadata(result.locator);
71
+ const suggestions = this._generateSuggestions(selector, metadata);
72
+ return {
73
+ ...result,
74
+ metadata,
75
+ suggestions
76
+ };
77
+ }
78
+ return result;
79
+ } catch (error) {
80
+ const errorMessage = error instanceof Error ? error.message : String(error);
81
+ tabDebug(`Single selector resolution failed: ${errorMessage}`);
82
+ return {
83
+ locator: this.page.locator("not-found"),
84
+ selector,
85
+ confidence: 0,
86
+ strategy: this._getStrategyForSelector(selector),
87
+ resolutionTimeMs: Date.now() - startTime,
88
+ error: errorMessage
89
+ };
90
+ }
91
+ }
92
+ _validateSelectors(selectors) {
93
+ return selectors.map((selector, index) => {
94
+ try {
95
+ return validateElementSelector(selector);
96
+ } catch (error) {
97
+ throw new Error(`Invalid selector at index ${index}: ${error instanceof Error ? error.message : String(error)}`);
98
+ }
99
+ });
100
+ }
101
+ _categorizeSelectors(selectors) {
102
+ const categorized = {
103
+ refs: [],
104
+ css: [],
105
+ roles: [],
106
+ text: []
107
+ };
108
+ for (const selector of selectors) {
109
+ if (isRefSelector(selector)) {
110
+ categorized.refs.push(selector);
111
+ } else if (isCSSSelector(selector)) {
112
+ categorized.css.push(selector);
113
+ } else if (isRoleSelector(selector)) {
114
+ categorized.roles.push(selector);
115
+ } else if (isTextSelector(selector)) {
116
+ categorized.text.push(selector);
117
+ }
118
+ }
119
+ return categorized;
120
+ }
121
+ async _resolveParallel(categorized, timeoutMs, continueOnError) {
122
+ const allSelectors = [
123
+ ...categorized.refs,
124
+ ...categorized.css,
125
+ ...categorized.roles,
126
+ ...categorized.text
127
+ ];
128
+ const promises = allSelectors.map(async (selector) => {
129
+ try {
130
+ return await this._resolveSelectorWithTimeout(selector, timeoutMs);
131
+ } catch (error) {
132
+ if (!continueOnError) {
133
+ throw error;
134
+ }
135
+ return this._createErrorResult(selector, error);
136
+ }
137
+ });
138
+ return await Promise.all(promises);
139
+ }
140
+ async _resolveSequential(categorized, timeoutMs, continueOnError) {
141
+ const allSelectors = [
142
+ ...categorized.refs,
143
+ ...categorized.css,
144
+ ...categorized.roles,
145
+ ...categorized.text
146
+ ];
147
+ const promises = allSelectors.map(async (selector, index) => {
148
+ await new Promise((resolve) => setTimeout(resolve, index * 10));
149
+ try {
150
+ return await this._resolveSelectorWithTimeout(selector, timeoutMs);
151
+ } catch (error) {
152
+ if (!continueOnError) {
153
+ throw error;
154
+ }
155
+ return this._createErrorResult(selector, error);
156
+ }
157
+ });
158
+ return await Promise.all(promises);
159
+ }
160
+ async _resolveHybrid(categorized, timeoutMs, continueOnError) {
161
+ const results = [];
162
+ if (categorized.refs.length > 0) {
163
+ const refPromises = categorized.refs.map(async (selector) => {
164
+ try {
165
+ return await this._resolveSelectorWithTimeout(selector, timeoutMs);
166
+ } catch (error) {
167
+ if (!continueOnError) {
168
+ throw error;
169
+ }
170
+ return this._createErrorResult(selector, error);
171
+ }
172
+ });
173
+ const refResults = await Promise.all(refPromises);
174
+ results.push(...refResults);
175
+ }
176
+ const otherSelectors = [
177
+ ...categorized.css,
178
+ ...categorized.roles,
179
+ ...categorized.text
180
+ ];
181
+ const otherPromises = otherSelectors.map(async (selector, index) => {
182
+ await new Promise((resolve) => setTimeout(resolve, index * 10));
183
+ try {
184
+ return await this._resolveSelectorWithTimeout(selector, timeoutMs);
185
+ } catch (error) {
186
+ if (!continueOnError) {
187
+ throw error;
188
+ }
189
+ return this._createErrorResult(selector, error);
190
+ }
191
+ });
192
+ const otherResults = await Promise.all(otherPromises);
193
+ results.push(...otherResults);
194
+ return results;
195
+ }
196
+ async _resolveSelectorWithTimeout(selector, timeoutMs) {
197
+ const startTime = Date.now();
198
+ const timeoutPromise = new Promise((_, reject) => {
199
+ setTimeout(() => {
200
+ reject(new Error(`Selector resolution timed out after ${timeoutMs}ms`));
201
+ }, timeoutMs);
202
+ });
203
+ try {
204
+ const result = await Promise.race([
205
+ this._resolveSingleSelectorInternal(selector),
206
+ timeoutPromise
207
+ ]);
208
+ return {
209
+ ...result,
210
+ resolutionTimeMs: Date.now() - startTime
211
+ };
212
+ } catch (error) {
213
+ return this._createErrorResult(selector, error, Date.now() - startTime);
214
+ }
215
+ }
216
+ async _resolveSingleSelectorInternal(selector) {
217
+ if (isRefSelector(selector)) {
218
+ return await this._resolveRefSelector(selector);
219
+ }
220
+ if (isRoleSelector(selector)) {
221
+ return await this._resolveRoleSelector(selector);
222
+ }
223
+ if (isCSSSelector(selector)) {
224
+ return await this._resolveCSSSelector(selector);
225
+ }
226
+ if (isTextSelector(selector)) {
227
+ return await this._resolveTextSelector(selector);
228
+ }
229
+ throw new Error("Unknown selector type");
230
+ }
231
+ async _resolveRefSelector(selector) {
232
+ try {
233
+ const locator = this.page.locator(`aria-ref=${selector.ref}`);
234
+ const count = await locator.count();
235
+ if (count === 0) {
236
+ return {
237
+ locator: this.page.locator("not-found"),
238
+ selector,
239
+ confidence: 0,
240
+ strategy: ResolutionStrategy.REF,
241
+ error: `Ref ${selector.ref} not found in current page state`
242
+ };
243
+ }
244
+ return {
245
+ locator,
246
+ selector,
247
+ confidence: SelectorConfidence.HIGH,
248
+ strategy: ResolutionStrategy.REF
249
+ };
250
+ } catch (error) {
251
+ throw new Error(`Failed to resolve ref selector: ${error instanceof Error ? error.message : String(error)}`);
252
+ }
253
+ }
254
+ async _resolveRoleSelector(selector) {
255
+ try {
256
+ let locator = this.page.getByRole(selector.role);
257
+ let confidence = SelectorConfidence.MEDIUM;
258
+ if (selector.text) {
259
+ locator = locator.filter({ hasText: selector.text });
260
+ confidence = SelectorConfidence.HIGH;
261
+ }
262
+ const count = await locator.count();
263
+ if (count === 0) {
264
+ const alternatives = await this._findRoleAlternatives(selector.role, selector.text);
265
+ return {
266
+ locator: this.page.locator("not-found"),
267
+ selector,
268
+ confidence: 0,
269
+ strategy: ResolutionStrategy.ROLE_SEQUENTIAL,
270
+ alternatives,
271
+ error: selector.text ? `No elements found with role "${selector.role}" and text "${selector.text}"` : `No elements found with role "${selector.role}"`
272
+ };
273
+ }
274
+ if (count > 1 && !selector.text) {
275
+ const candidates = await this._getMatchCandidates(locator);
276
+ const candidateDescriptions = candidates.map((c, i) => {
277
+ const attrs = Object.entries(c.attributes).filter(([key]) => ["id", "name", "data-testid", "aria-label"].includes(key)).map(([key, value]) => `${key}="${value}"`).join(" ");
278
+ const prefix = attrs ? `[${attrs}] ` : "";
279
+ const truncatedText = c.text.length > 50 ? `${c.text.substring(0, 50)}...` : c.text;
280
+ return ` ${i + 1}) ${prefix}text: "${truncatedText}"`;
281
+ }).join(`
282
+ `);
283
+ return {
284
+ locator: this.page.locator("not-found"),
285
+ selector,
286
+ confidence: 0,
287
+ strategy: ResolutionStrategy.ROLE_SEQUENTIAL,
288
+ error: `Multiple elements (${count}) found with role "${selector.role}". Please be more specific:
289
+ ${candidateDescriptions}
290
+ Consider adding text filter or using a more specific selector.`
291
+ };
292
+ }
293
+ return {
294
+ locator,
295
+ selector,
296
+ confidence,
297
+ strategy: ResolutionStrategy.ROLE_SEQUENTIAL
298
+ };
299
+ } catch (error) {
300
+ throw new Error(`Failed to resolve role selector: ${error instanceof Error ? error.message : String(error)}`);
301
+ }
302
+ }
303
+ async _resolveCSSSelector(selector) {
304
+ try {
305
+ const locator = this.page.locator(selector.css);
306
+ const count = await locator.count();
307
+ if (count === 0) {
308
+ return {
309
+ locator: this.page.locator("not-found"),
310
+ selector,
311
+ confidence: 0,
312
+ strategy: ResolutionStrategy.CSS_PARALLEL,
313
+ error: `No elements found matching CSS selector "${selector.css}"`
314
+ };
315
+ }
316
+ let confidence = SelectorConfidence.MEDIUM;
317
+ if (selector.css.startsWith("#")) {
318
+ confidence = SelectorConfidence.HIGH;
319
+ } else if (count > 1 && GENERIC_TAG_SELECTOR.test(selector.css)) {
320
+ const candidates = await this._getMatchCandidates(locator);
321
+ const candidateDescriptions = candidates.map((c, i) => {
322
+ const attrs = Object.entries(c.attributes).filter(([key]) => ["id", "class", "name", "data-testid"].includes(key)).map(([key, value]) => `${key}="${value}"`).join(" ");
323
+ const prefix = attrs ? `[${attrs}] ` : "";
324
+ const truncatedText = c.text.length > 50 ? `${c.text.substring(0, 50)}...` : c.text;
325
+ return ` ${i + 1}) ${prefix}text: "${truncatedText}"`;
326
+ }).join(`
327
+ `);
328
+ return {
329
+ locator: this.page.locator("not-found"),
330
+ selector,
331
+ confidence: 0,
332
+ strategy: ResolutionStrategy.CSS_PARALLEL,
333
+ error: `Multiple elements (${count}) found with CSS selector "${selector.css}". Please be more specific:
334
+ ${candidateDescriptions}
335
+ Consider using a more specific selector like ID or adding :nth-child().`
336
+ };
337
+ }
338
+ return {
339
+ locator,
340
+ selector,
341
+ confidence,
342
+ strategy: ResolutionStrategy.CSS_PARALLEL
343
+ };
344
+ } catch (error) {
345
+ throw new Error(`Failed to resolve CSS selector: ${error instanceof Error ? error.message : String(error)}`);
346
+ }
347
+ }
348
+ async _resolveTextSelector(selector) {
349
+ try {
350
+ let locator = this.page.getByText(selector.text);
351
+ if (selector.tag) {
352
+ locator = this.page.locator(selector.tag).filter({ hasText: selector.text });
353
+ }
354
+ const count = await locator.count();
355
+ if (count === 0) {
356
+ const alternatives = await this._findTextAlternatives(selector.text);
357
+ return {
358
+ locator: this.page.locator("not-found"),
359
+ selector,
360
+ confidence: 0,
361
+ strategy: ResolutionStrategy.TEXT_FALLBACK,
362
+ alternatives,
363
+ error: selector.tag ? `No elements found with text "${selector.text}" in ${selector.tag} tags` : `No elements found with text "${selector.text}"`
364
+ };
365
+ }
366
+ let confidence = SelectorConfidence.MEDIUM;
367
+ if (selector.tag) {
368
+ confidence = SelectorConfidence.HIGH;
369
+ } else if (count > 3) {
370
+ const candidates = await this._getMatchCandidates(locator);
371
+ const candidateDescriptions = candidates.map((c, i) => {
372
+ const attrs = Object.entries(c.attributes).filter(([key]) => ["id", "class", "role", "data-testid"].includes(key)).map(([key, value]) => `${key}="${value}"`).join(" ");
373
+ const tagName = c.attributes.tagName || "element";
374
+ const attrString = attrs ? ` ${attrs}` : "";
375
+ const truncatedText = c.text.length > 50 ? `${c.text.substring(0, 50)}...` : c.text;
376
+ return ` ${i + 1}) <${tagName}${attrString}> text: "${truncatedText}"`;
377
+ }).join(`
378
+ `);
379
+ return {
380
+ locator: this.page.locator("not-found"),
381
+ selector,
382
+ confidence: 0,
383
+ strategy: ResolutionStrategy.TEXT_FALLBACK,
384
+ error: `Multiple elements (${count}) found with text "${selector.text}". Please be more specific:
385
+ ${candidateDescriptions}
386
+ Consider using CSS selector or role with text filter.`
387
+ };
388
+ }
389
+ return {
390
+ locator,
391
+ selector,
392
+ confidence,
393
+ strategy: ResolutionStrategy.TEXT_FALLBACK
394
+ };
395
+ } catch (error) {
396
+ throw new Error(`Failed to resolve text selector: ${error instanceof Error ? error.message : String(error)}`);
397
+ }
398
+ }
399
+ async _getMatchCandidates(locator, limit = 5) {
400
+ const count = await locator.count();
401
+ const maxCount = Math.min(count, limit);
402
+ const promises = [];
403
+ for (let i = 0;i < maxCount; i++) {
404
+ const element = locator.nth(i);
405
+ promises.push(Promise.all([
406
+ element.textContent(),
407
+ element.evaluate((el) => {
408
+ const attrs = {};
409
+ for (const attr of el.attributes) {
410
+ attrs[attr.name] = attr.value;
411
+ }
412
+ return attrs;
413
+ })
414
+ ]).then(([text, attributes]) => ({
415
+ index: i,
416
+ text: text?.trim() || "",
417
+ attributes
418
+ })).catch(() => null));
419
+ }
420
+ const results = await Promise.all(promises);
421
+ return results.filter((result) => result !== null);
422
+ }
423
+ async _findRoleAlternatives(role, text) {
424
+ const alternatives = [];
425
+ const commonRoleAlternatives = {
426
+ button: ["link", "menuitem"],
427
+ link: ["button", "menuitem"],
428
+ textbox: ["searchbox", "combobox"],
429
+ checkbox: ["radio", "switch"]
430
+ };
431
+ const roleAlts = commonRoleAlternatives[role] || [];
432
+ const rolePromises = roleAlts.map(async (altRole) => {
433
+ try {
434
+ const locator = text ? this.page.getByRole(altRole).filter({ hasText: text }) : this.page.getByRole(altRole);
435
+ const count = await locator.count();
436
+ if (count > 0) {
437
+ return { role: altRole, text };
438
+ }
439
+ return null;
440
+ } catch {
441
+ return null;
442
+ }
443
+ });
444
+ const results = await Promise.all(rolePromises);
445
+ const validAlternatives = results.filter((alt) => alt !== null);
446
+ alternatives.push(...validAlternatives);
447
+ return alternatives.slice(0, 3);
448
+ }
449
+ async _findTextAlternatives(text) {
450
+ const alternatives = [];
451
+ const partialText = text.substring(0, Math.floor(text.length / 2));
452
+ if (partialText.length > 3) {
453
+ try {
454
+ const count = await this.page.getByText(partialText, { exact: false }).count();
455
+ if (count > 0) {
456
+ alternatives.push({ text: partialText });
457
+ }
458
+ } catch {}
459
+ }
460
+ return alternatives.slice(0, 2);
461
+ }
462
+ async _extractElementMetadata(locator) {
463
+ try {
464
+ const element = locator.first();
465
+ const [
466
+ tagName,
467
+ attributes,
468
+ textContent,
469
+ boundingBox,
470
+ isVisible,
471
+ isEnabled
472
+ ] = await Promise.all([
473
+ element.evaluate((el) => el.tagName.toLowerCase()),
474
+ element.evaluate((el) => {
475
+ const attrs = {};
476
+ for (const attr of el.attributes) {
477
+ attrs[attr.name] = attr.value;
478
+ }
479
+ return attrs;
480
+ }),
481
+ element.textContent() || "",
482
+ element.boundingBox().catch(() => null),
483
+ element.isVisible().catch(() => false),
484
+ element.isEnabled().catch(() => false)
485
+ ]);
486
+ return {
487
+ tagName,
488
+ attributes,
489
+ textContent: textContent || "",
490
+ boundingBox: boundingBox || undefined,
491
+ isVisible,
492
+ isEnabled
493
+ };
494
+ } catch (error) {
495
+ tabDebug("Failed to extract element metadata:", error);
496
+ return;
497
+ }
498
+ }
499
+ _generateSuggestions(selector, metadata) {
500
+ const suggestions = [];
501
+ if (!metadata) {
502
+ return suggestions;
503
+ }
504
+ if (metadata.attributes["data-testid"] && !isCSSSelector(selector)) {
505
+ suggestions.push(`Consider using CSS selector with data-testid: [data-testid="${metadata.attributes["data-testid"]}"]`);
506
+ }
507
+ if (metadata.attributes.id && !isCSSSelector(selector)) {
508
+ suggestions.push(`Consider using CSS selector with ID: #${metadata.attributes.id}`);
509
+ }
510
+ if (metadata.attributes.role && !isRoleSelector(selector)) {
511
+ suggestions.push(`Consider using role selector: role="${metadata.attributes.role}"`);
512
+ }
513
+ return suggestions;
514
+ }
515
+ _createErrorResult(selector, error, resolutionTimeMs = 0) {
516
+ const errorMessage = error instanceof Error ? error.message : String(error);
517
+ return {
518
+ locator: this.page.locator("not-found"),
519
+ selector,
520
+ confidence: 0,
521
+ strategy: this._getStrategyForSelector(selector),
522
+ resolutionTimeMs,
523
+ error: errorMessage
524
+ };
525
+ }
526
+ _getStrategyForSelector(selector) {
527
+ if (isRefSelector(selector)) {
528
+ return ResolutionStrategy.REF;
529
+ }
530
+ if (isCSSSelector(selector)) {
531
+ return ResolutionStrategy.CSS_PARALLEL;
532
+ }
533
+ if (isRoleSelector(selector)) {
534
+ return ResolutionStrategy.ROLE_SEQUENTIAL;
535
+ }
536
+ return ResolutionStrategy.TEXT_FALLBACK;
537
+ }
538
+ }
539
+ export {
540
+ SelectorResolver
541
+ };
package/lib/tab.js CHANGED
@@ -21,10 +21,10 @@ var __require = /* @__PURE__ */ createRequire(import.meta.url);
21
21
  import { EventEmitter } from "node:events";
22
22
  import { TIMEOUTS } from "./config/constants.js";
23
23
  import { ManualPromise } from "./manual-promise.js";
24
+ import { SelectorResolver } from "./services/selector-resolver.js";
24
25
  import { callOnPageNoTrace, waitForCompletion } from "./tools/utils.js";
25
26
  import { logUnhandledError } from "./utils/log.js";
26
27
  import { snapshotDebug, tabDebug } from "./utils/log.js";
27
- var REF_VALUE_REGEX = /\[ref=([^\]]+)\]/;
28
28
  var TabEvents = {
29
29
  modalState: "modalState"
30
30
  };
@@ -41,6 +41,7 @@ class Tab extends EventEmitter {
41
41
  _downloads = [];
42
42
  _customRefMappings = new Map;
43
43
  _customRefCounter = 0;
44
+ _selectorResolver;
44
45
  _navigationState = {
45
46
  isNavigating: false,
46
47
  lastNavigationStart: 0
@@ -50,6 +51,7 @@ class Tab extends EventEmitter {
50
51
  this.context = context;
51
52
  this.page = page;
52
53
  this._onPageClose = onPageClose;
54
+ this._selectorResolver = new SelectorResolver(page);
53
55
  page.on("console", (event) => this._handleConsoleMessage(messageToConsoleMessage(event)));
54
56
  page.on("pageerror", (error) => this._handleConsoleMessage(pageErrorToConsoleMessage(error)));
55
57
  page.on("request", (request) => this._requests.set(request, null));
@@ -368,24 +370,55 @@ class Tab extends EventEmitter {
368
370
  }
369
371
  return `element_${this._customRefCounter}`;
370
372
  }
373
+ async resolveElementLocators(selectors, options) {
374
+ tabDebug(`Resolving ${selectors.length} element locators`);
375
+ try {
376
+ return await this._selectorResolver.resolveSelectors(selectors, options);
377
+ } catch (error) {
378
+ tabDebug("Failed to resolve element locators:", error);
379
+ throw error;
380
+ }
381
+ }
382
+ async resolveSingleElementLocator(selector, options) {
383
+ tabDebug("Resolving single element locator:", selector);
384
+ try {
385
+ return await this._selectorResolver.resolveSingleSelector(selector, options);
386
+ } catch (error) {
387
+ tabDebug("Failed to resolve single element locator:", error);
388
+ throw error;
389
+ }
390
+ }
371
391
  async refLocator(params) {
372
- return (await this.refLocators([params]))[0];
392
+ const { selector } = params;
393
+ tabDebug("Using selector system for element:", params.element);
394
+ const result = await this._selectorResolver.resolveSingleSelector(selector);
395
+ if (result.error || !result.locator) {
396
+ const errorMessage = `Failed to resolve selector for element "${params.element}": ${result.error || "Unknown error"}`;
397
+ const alternativesMessage = result.alternatives ? `. Alternatives: ${JSON.stringify(result.alternatives)}` : "";
398
+ throw new Error(errorMessage + alternativesMessage);
399
+ }
400
+ return result.locator.describe(params.element);
373
401
  }
374
402
  async refLocators(params) {
375
- const snapshot = await this.page._snapshotForAI();
376
- return params.map((param) => {
377
- if (this._customRefMappings.has(param.ref)) {
378
- const selector = this._customRefMappings.get(param.ref);
379
- if (selector) {
380
- return this.page.locator(selector).describe(param.element);
381
- }
382
- }
383
- if (!snapshot.includes(`[ref=${param.ref}]`)) {
384
- const availableRefs = this._getAvailableRefs(snapshot);
385
- throw new Error(`Ref ${param.ref} not found. Available refs: [${availableRefs.join(", ")}]. Element: ${param.element}. Consider capturing a new snapshot if the page has changed.`);
403
+ const selectors = params.map((p) => {
404
+ if (!p.selector) {
405
+ throw new Error(`Missing selector for element: ${p.element}`);
386
406
  }
387
- return this.page.locator(`aria-ref=${param.ref}`).describe(param.element);
407
+ return p.selector;
388
408
  });
409
+ const resolutionResults = await this._selectorResolver.resolveSelectors(selectors);
410
+ const results = [];
411
+ for (let i = 0;i < resolutionResults.length; i++) {
412
+ const result = resolutionResults[i];
413
+ const param = params[i];
414
+ if (result.error || !result.locator) {
415
+ const errorMessage = `Failed to resolve selector for element "${param.element}": ${result.error || "Unknown error"}`;
416
+ const alternativesMessage = result.alternatives ? `. Alternatives: ${JSON.stringify(result.alternatives)}` : "";
417
+ throw new Error(errorMessage + alternativesMessage);
418
+ }
419
+ results.push(result.locator.describe(param.element));
420
+ }
421
+ return results;
389
422
  }
390
423
  async waitForTimeout(time) {
391
424
  if (this._javaScriptBlocked()) {
@@ -396,16 +429,6 @@ class Tab extends EventEmitter {
396
429
  return page.evaluate((timeout) => new Promise((f) => setTimeout(f, timeout)), time);
397
430
  });
398
431
  }
399
- _getAvailableRefs(snapshot) {
400
- const refMatches = snapshot.match(/\[ref=([^\]]+)\]/g);
401
- if (!refMatches) {
402
- return [];
403
- }
404
- return refMatches.map((match) => {
405
- const refValue = REF_VALUE_REGEX.exec(match);
406
- return refValue ? refValue[1] : "";
407
- }).filter(Boolean);
408
- }
409
432
  }
410
433
  function messageToConsoleMessage(message) {
411
434
  return {
@@ -79,28 +79,12 @@ class BaseToolHandler {
79
79
  }
80
80
 
81
81
  class BaseElementToolHandler extends BaseToolHandler {
82
- async resolveElementLocator(tab, params) {
83
- if (params.ref && params.element) {
84
- return await tab.refLocator({
85
- ref: params.ref,
86
- element: params.element
87
- });
88
- }
89
- }
90
- validateElementParams(params) {
91
- if (params.ref && !params.element) {
92
- throw new Error("Element description is required when ref is provided");
93
- }
94
- if (params.element && !params.ref) {
95
- throw new Error("Element ref is required when element description is provided");
96
- }
82
+ resolveElementLocator(_tab, _params) {
83
+ return Promise.resolve(undefined);
97
84
  }
85
+ validateElementParams(_params) {}
98
86
  handleToolError(error, response, params) {
99
87
  super.handleToolError(error, response, params);
100
- if (params.element && params.ref) {
101
- response.addResult("Suggestion: Verify element selector is still valid");
102
- response.addResult("Suggestion: Check if element is visible and interactable");
103
- }
104
88
  }
105
89
  }
106
90
  function createToolWithBaseHandler(config, handlerClass) {
@@ -26,7 +26,7 @@ var batchExecuteTool = defineTool({
26
26
  schema: {
27
27
  name: "browser_batch_execute",
28
28
  title: "Batch Execute Browser Actions",
29
- description: `Execute multiple browser actions in sequence with optimized response handling.RECOMMENDED:Use this tool instead of individual actions when performing multiple operations to significantly reduce token usage and improve performance.BY DEFAULT use for:form filling(multiple type→click),multi-step navigation,any workflow with 2+ known steps.Saves 90% tokens vs individual calls.globalExpectation:{includeSnapshot:false,snapshotOptions:{selector:"#app"},diffOptions:{enabled:true}}.Per-step override:steps[].expectation.Example:[{tool:"browser_navigate",arguments:{url:"https://example.com"}},{tool:"browser_type",arguments:{element:"username",ref:"#user",text:"john"}},{tool:"browser_click",arguments:{element:"submit",ref:"#btn"}}].Tool names must match exactly(e.g.browser_navigate,browser_click,browser_type).`,
29
+ description: "Execute multiple browser actions in sequence. PREFER over individual tools for 2+ operations.",
30
30
  inputSchema: batchExecuteSchema,
31
31
  type: "destructive"
32
32
  },