@dyyz1993/agent-browser 0.13.2 → 0.24.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 +108 -0
- package/bin/agent-browser-darwin-arm64 +0 -0
- package/bin/agent-browser-darwin-x64 +0 -0
- package/bin/agent-browser-linux-arm64 +0 -0
- package/bin/agent-browser-linux-x64 +0 -0
- package/bin/agent-browser-win32-x64.exe +0 -0
- package/dist/__tests__/e2e/utils/test-helpers.d.ts +1 -0
- package/dist/__tests__/e2e/utils/test-helpers.d.ts.map +1 -1
- package/dist/__tests__/e2e/utils/test-helpers.js +14 -1
- package/dist/__tests__/e2e/utils/test-helpers.js.map +1 -1
- package/dist/__tests__/utils/free-port.d.ts +2 -0
- package/dist/__tests__/utils/free-port.d.ts.map +1 -0
- package/dist/__tests__/utils/free-port.js +18 -0
- package/dist/__tests__/utils/free-port.js.map +1 -0
- package/dist/__tests__/utils/parseCli.d.ts.map +1 -1
- package/dist/__tests__/utils/parseCli.js +83 -9
- package/dist/__tests__/utils/parseCli.js.map +1 -1
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +298 -9
- package/dist/actions.js.map +1 -1
- package/dist/browser.d.ts +11 -1
- package/dist/browser.d.ts.map +1 -1
- package/dist/browser.js +75 -19
- package/dist/browser.js.map +1 -1
- package/dist/cli/commands.d.ts.map +1 -1
- package/dist/cli/commands.js +172 -15
- package/dist/cli/commands.js.map +1 -1
- package/dist/cli/connection.d.ts +13 -0
- package/dist/cli/connection.d.ts.map +1 -1
- package/dist/cli/connection.js +137 -48
- package/dist/cli/connection.js.map +1 -1
- package/dist/cli/flags.d.ts.map +1 -1
- package/dist/cli/flags.js +0 -1
- package/dist/cli/flags.js.map +1 -1
- package/dist/cli/help.d.ts.map +1 -1
- package/dist/cli/help.js +63 -22
- package/dist/cli/help.js.map +1 -1
- package/dist/cli/output.d.ts.map +1 -1
- package/dist/cli/output.js +0 -32
- package/dist/cli/output.js.map +1 -1
- package/dist/cli.js +20 -2
- package/dist/cli.js.map +1 -1
- package/dist/daemon.d.ts +1 -0
- package/dist/daemon.d.ts.map +1 -1
- package/dist/daemon.js +291 -264
- package/dist/daemon.js.map +1 -1
- package/dist/diff.d.ts.map +1 -1
- package/dist/diff.js +1 -1
- package/dist/diff.js.map +1 -1
- package/dist/flow/exporters/cypress.d.ts +9 -0
- package/dist/flow/exporters/cypress.d.ts.map +1 -0
- package/dist/flow/exporters/cypress.js +256 -0
- package/dist/flow/exporters/cypress.js.map +1 -0
- package/dist/flow/exporters/index.d.ts +6 -0
- package/dist/flow/exporters/index.d.ts.map +1 -0
- package/dist/flow/exporters/index.js +5 -0
- package/dist/flow/exporters/index.js.map +1 -0
- package/dist/flow/exporters/playwright.d.ts +20 -0
- package/dist/flow/exporters/playwright.d.ts.map +1 -0
- package/dist/flow/exporters/playwright.js +175 -0
- package/dist/flow/exporters/playwright.js.map +1 -0
- package/dist/flow/exporters/python.d.ts +20 -0
- package/dist/flow/exporters/python.d.ts.map +1 -0
- package/dist/flow/exporters/python.js +163 -0
- package/dist/flow/exporters/python.js.map +1 -0
- package/dist/flow/exporters/selenium.d.ts +9 -0
- package/dist/flow/exporters/selenium.d.ts.map +1 -0
- package/dist/flow/exporters/selenium.js +298 -0
- package/dist/flow/exporters/selenium.js.map +1 -0
- package/dist/flow/exporters/types.d.ts +13 -0
- package/dist/flow/exporters/types.d.ts.map +1 -0
- package/dist/flow/exporters/types.js +2 -0
- package/dist/flow/exporters/types.js.map +1 -0
- package/dist/flow/flow-executor.d.ts +57 -0
- package/dist/flow/flow-executor.d.ts.map +1 -0
- package/dist/flow/flow-executor.js +1263 -0
- package/dist/flow/flow-executor.js.map +1 -0
- package/dist/flow/index.d.ts +15 -0
- package/dist/flow/index.d.ts.map +1 -0
- package/dist/flow/index.js +10 -0
- package/dist/flow/index.js.map +1 -0
- package/dist/flow/output.d.ts +11 -0
- package/dist/flow/output.d.ts.map +1 -0
- package/dist/flow/output.js +84 -0
- package/dist/flow/output.js.map +1 -0
- package/dist/flow/plugin-system.d.ts +48 -0
- package/dist/flow/plugin-system.d.ts.map +1 -0
- package/dist/flow/plugin-system.js +132 -0
- package/dist/flow/plugin-system.js.map +1 -0
- package/dist/flow/plugins/file-output-plugin.d.ts +8 -0
- package/dist/flow/plugins/file-output-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/file-output-plugin.js +31 -0
- package/dist/flow/plugins/file-output-plugin.js.map +1 -0
- package/dist/flow/plugins/index.d.ts +4 -0
- package/dist/flow/plugins/index.d.ts.map +1 -0
- package/dist/flow/plugins/index.js +4 -0
- package/dist/flow/plugins/index.js.map +1 -0
- package/dist/flow/plugins/logging-plugin.d.ts +7 -0
- package/dist/flow/plugins/logging-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/logging-plugin.js +40 -0
- package/dist/flow/plugins/logging-plugin.js.map +1 -0
- package/dist/flow/plugins/webhook-plugin.d.ts +7 -0
- package/dist/flow/plugins/webhook-plugin.d.ts.map +1 -0
- package/dist/flow/plugins/webhook-plugin.js +24 -0
- package/dist/flow/plugins/webhook-plugin.js.map +1 -0
- package/dist/flow/presets/index.d.ts +10 -0
- package/dist/flow/presets/index.d.ts.map +1 -0
- package/dist/flow/presets/index.js +29 -0
- package/dist/flow/presets/index.js.map +1 -0
- package/dist/flow/recorder-to-flow.d.ts +70 -0
- package/dist/flow/recorder-to-flow.d.ts.map +1 -0
- package/dist/flow/recorder-to-flow.js +392 -0
- package/dist/flow/recorder-to-flow.js.map +1 -0
- package/dist/flow/site-manager.d.ts +24 -0
- package/dist/flow/site-manager.d.ts.map +1 -0
- package/dist/flow/site-manager.js +125 -0
- package/dist/flow/site-manager.js.map +1 -0
- package/dist/flow/types.d.ts +196 -0
- package/dist/flow/types.d.ts.map +1 -0
- package/dist/flow/types.js +2 -0
- package/dist/flow/types.js.map +1 -0
- package/dist/flow/yaml-parser.d.ts +15 -0
- package/dist/flow/yaml-parser.d.ts.map +1 -0
- package/dist/flow/yaml-parser.js +216 -0
- package/dist/flow/yaml-parser.js.map +1 -0
- package/dist/human-mouse.d.ts.map +1 -1
- package/dist/protocol.d.ts.map +1 -1
- package/dist/protocol.js +15 -11
- package/dist/protocol.js.map +1 -1
- package/dist/rc-config.d.ts.map +1 -1
- package/dist/rc-config.js +1 -2
- package/dist/rc-config.js.map +1 -1
- package/dist/recorder/inject.js +730 -332
- package/dist/snapshot-store.d.ts +83 -0
- package/dist/snapshot-store.d.ts.map +1 -0
- package/dist/snapshot-store.js +112 -0
- package/dist/snapshot-store.js.map +1 -0
- package/dist/snapshot.d.ts +6 -7
- package/dist/snapshot.d.ts.map +1 -1
- package/dist/snapshot.js +471 -17
- package/dist/snapshot.js.map +1 -1
- package/dist/stream-server-standalone.d.ts.map +1 -1
- package/dist/stream-server-standalone.js.map +1 -1
- package/dist/stream-server.d.ts.map +1 -1
- package/dist/stream-server.js +38 -13
- package/dist/stream-server.js.map +1 -1
- package/dist/test-live.js +5 -5
- package/dist/test-live.js.map +1 -1
- package/dist/types.d.ts +13 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/viewer-script.d.ts.map +1 -1
- package/dist/viewer-script.js +12 -6
- package/dist/viewer-script.js.map +1 -1
- package/package.json +18 -5
- package/skills/agent-browser/SKILL.md +151 -3
- package/dist/ios-actions.d.ts +0 -11
- package/dist/ios-actions.d.ts.map +0 -1
- package/dist/ios-actions.js +0 -228
- package/dist/ios-actions.js.map +0 -1
- package/dist/ios-manager.d.ts +0 -266
- package/dist/ios-manager.d.ts.map +0 -1
- package/dist/ios-manager.js +0 -1076
- package/dist/ios-manager.js.map +0 -1
package/dist/snapshot.js
CHANGED
|
@@ -311,6 +311,381 @@ async function suggestSelectors(page) {
|
|
|
311
311
|
}
|
|
312
312
|
return selectors;
|
|
313
313
|
}
|
|
314
|
+
export async function generateStableSelectors(page, refs) {
|
|
315
|
+
const result = {};
|
|
316
|
+
for (const [ref, data] of Object.entries(refs)) {
|
|
317
|
+
if (data.role === 'clickable' || data.role === 'focusable') {
|
|
318
|
+
if (data.selector && !data.selector.startsWith('getByRole')) {
|
|
319
|
+
result[ref] = { cssSelector: data.selector, xpath: '' };
|
|
320
|
+
}
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
let locator;
|
|
325
|
+
if (data.name) {
|
|
326
|
+
locator = page.getByRole(data.role, {
|
|
327
|
+
name: data.name,
|
|
328
|
+
exact: true,
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
else {
|
|
332
|
+
locator = page.getByRole(data.role);
|
|
333
|
+
}
|
|
334
|
+
if (data.nth !== undefined) {
|
|
335
|
+
locator = locator.nth(data.nth);
|
|
336
|
+
}
|
|
337
|
+
const elementCount = await locator.count();
|
|
338
|
+
if (elementCount === 0)
|
|
339
|
+
continue;
|
|
340
|
+
const selectorData = await locator
|
|
341
|
+
.evaluate((el) => {
|
|
342
|
+
const UTILITY_CLASS_PATTERNS = [
|
|
343
|
+
/^_/,
|
|
344
|
+
/^css-/,
|
|
345
|
+
/^[a-z]{1,2}$/,
|
|
346
|
+
/^(active|disabled|hidden|visible|selected|hover|focus|current|open|closed)$/i,
|
|
347
|
+
/^(text-|font-|bg-|p-|m-|w-|h-|flex|grid|border|rounded|shadow|opacity|z-)/,
|
|
348
|
+
/^(sm:|md:|lg:|xl:|2xl:)/,
|
|
349
|
+
];
|
|
350
|
+
const SEMANTIC_ATTRS = [
|
|
351
|
+
'data-testid',
|
|
352
|
+
'data-test',
|
|
353
|
+
'data-cy',
|
|
354
|
+
'name',
|
|
355
|
+
'aria-label',
|
|
356
|
+
'aria-labelledby',
|
|
357
|
+
'role',
|
|
358
|
+
'type',
|
|
359
|
+
'placeholder',
|
|
360
|
+
'title',
|
|
361
|
+
'alt',
|
|
362
|
+
];
|
|
363
|
+
function isHighEntropyClassName(className) {
|
|
364
|
+
if (!className || className.length < 4 || className.length > 15)
|
|
365
|
+
return false;
|
|
366
|
+
if (/^[a-zA-Z]+_[a-zA-Z]+_{2}[a-zA-Z0-9]+$/.test(className))
|
|
367
|
+
return true;
|
|
368
|
+
if (/^sc-[a-zA-Z0-9]+$/.test(className))
|
|
369
|
+
return true;
|
|
370
|
+
const hasUpper = /[A-Z]/.test(className);
|
|
371
|
+
const hasLower = /[a-z]/.test(className);
|
|
372
|
+
const hasDigit = /[0-9]/.test(className);
|
|
373
|
+
const hasSeparator = /[-_]/.test(className);
|
|
374
|
+
if (hasSeparator)
|
|
375
|
+
return false;
|
|
376
|
+
if (hasUpper && hasLower && hasDigit)
|
|
377
|
+
return true;
|
|
378
|
+
if (/^[A-Z][a-z0-9]+[A-Z]/.test(className) && className.length <= 12)
|
|
379
|
+
return true;
|
|
380
|
+
if (/^[a-z]/.test(className) && /[a-z][A-Z][a-z][A-Z]/.test(className))
|
|
381
|
+
return true;
|
|
382
|
+
return false;
|
|
383
|
+
}
|
|
384
|
+
function isUniqueSelector(selector) {
|
|
385
|
+
try {
|
|
386
|
+
return document.querySelectorAll(selector).length === 1;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return false;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function filterUsefulClasses(element) {
|
|
393
|
+
const htmlEl = element;
|
|
394
|
+
if (!htmlEl.className || typeof htmlEl.className !== 'string')
|
|
395
|
+
return [];
|
|
396
|
+
return htmlEl.className
|
|
397
|
+
.trim()
|
|
398
|
+
.split(/\s+/)
|
|
399
|
+
.filter((c) => {
|
|
400
|
+
if (!c)
|
|
401
|
+
return false;
|
|
402
|
+
if (UTILITY_CLASS_PATTERNS.some((p) => p.test(c)))
|
|
403
|
+
return false;
|
|
404
|
+
if (isHighEntropyClassName(c))
|
|
405
|
+
return false;
|
|
406
|
+
return true;
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
function tryIdSelector(element) {
|
|
410
|
+
const htmlEl = element;
|
|
411
|
+
if (htmlEl.id) {
|
|
412
|
+
const sel = '#' + CSS.escape(htmlEl.id);
|
|
413
|
+
if (isUniqueSelector(sel))
|
|
414
|
+
return sel;
|
|
415
|
+
}
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
function getMultiAttributeSelector(element) {
|
|
419
|
+
const tag = element.tagName.toLowerCase();
|
|
420
|
+
const attrs = [];
|
|
421
|
+
for (const attr of SEMANTIC_ATTRS) {
|
|
422
|
+
const value = element.getAttribute(attr);
|
|
423
|
+
if (value)
|
|
424
|
+
attrs.push({ attr, value });
|
|
425
|
+
}
|
|
426
|
+
if (attrs.length === 0)
|
|
427
|
+
return null;
|
|
428
|
+
for (const { attr, value } of attrs) {
|
|
429
|
+
const sel = tag + '[' + attr + '="' + CSS.escape(value) + '"]';
|
|
430
|
+
if (isUniqueSelector(sel))
|
|
431
|
+
return sel;
|
|
432
|
+
}
|
|
433
|
+
if (attrs.length >= 2) {
|
|
434
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
435
|
+
for (let j = i + 1; j < attrs.length; j++) {
|
|
436
|
+
const sel = tag +
|
|
437
|
+
'[' +
|
|
438
|
+
attrs[i].attr +
|
|
439
|
+
'="' +
|
|
440
|
+
CSS.escape(attrs[i].value) +
|
|
441
|
+
'"]' +
|
|
442
|
+
'[' +
|
|
443
|
+
attrs[j].attr +
|
|
444
|
+
'="' +
|
|
445
|
+
CSS.escape(attrs[j].value) +
|
|
446
|
+
'"]';
|
|
447
|
+
if (isUniqueSelector(sel))
|
|
448
|
+
return sel;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
function getAttributeClassComboSelector(element) {
|
|
455
|
+
const tag = element.tagName.toLowerCase();
|
|
456
|
+
const classes = filterUsefulClasses(element);
|
|
457
|
+
if (classes.length === 0)
|
|
458
|
+
return null;
|
|
459
|
+
classes.sort((a, b) => b.length - a.length);
|
|
460
|
+
const bestClass = classes[0];
|
|
461
|
+
for (const attr of SEMANTIC_ATTRS) {
|
|
462
|
+
const value = element.getAttribute(attr);
|
|
463
|
+
if (value) {
|
|
464
|
+
const sel = tag + '.' + CSS.escape(bestClass) + '[' + attr + '="' + CSS.escape(value) + '"]';
|
|
465
|
+
if (isUniqueSelector(sel))
|
|
466
|
+
return sel;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
return null;
|
|
470
|
+
}
|
|
471
|
+
function getBestClassSelector(element) {
|
|
472
|
+
const classes = filterUsefulClasses(element);
|
|
473
|
+
if (classes.length === 0)
|
|
474
|
+
return null;
|
|
475
|
+
classes.sort((a, b) => b.length - a.length);
|
|
476
|
+
const tag = element.tagName.toLowerCase();
|
|
477
|
+
for (const cls of classes) {
|
|
478
|
+
const sel = tag + '.' + CSS.escape(cls);
|
|
479
|
+
if (isUniqueSelector(sel))
|
|
480
|
+
return sel;
|
|
481
|
+
}
|
|
482
|
+
for (let i = 2; i <= Math.min(3, classes.length); i++) {
|
|
483
|
+
const sel = tag +
|
|
484
|
+
'.' +
|
|
485
|
+
classes
|
|
486
|
+
.slice(0, i)
|
|
487
|
+
.map((c) => CSS.escape(c))
|
|
488
|
+
.join('.');
|
|
489
|
+
if (isUniqueSelector(sel))
|
|
490
|
+
return sel;
|
|
491
|
+
}
|
|
492
|
+
return null;
|
|
493
|
+
}
|
|
494
|
+
function getFeatureSelector(element) {
|
|
495
|
+
if (!element || element === document.body)
|
|
496
|
+
return null;
|
|
497
|
+
const htmlEl = element;
|
|
498
|
+
if (htmlEl.id)
|
|
499
|
+
return '#' + CSS.escape(htmlEl.id);
|
|
500
|
+
for (const attr of ['data-testid', 'data-test', 'name', 'role', 'aria-label']) {
|
|
501
|
+
const value = element.getAttribute(attr);
|
|
502
|
+
if (value)
|
|
503
|
+
return element.tagName.toLowerCase() + '[' + attr + '="' + CSS.escape(value) + '"]';
|
|
504
|
+
}
|
|
505
|
+
const classes = filterUsefulClasses(element);
|
|
506
|
+
if (classes.length > 0) {
|
|
507
|
+
classes.sort((a, b) => b.length - a.length);
|
|
508
|
+
const sel = element.tagName.toLowerCase() + '.' + CSS.escape(classes[0]);
|
|
509
|
+
if (isUniqueSelector(sel))
|
|
510
|
+
return sel;
|
|
511
|
+
}
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
function getBaseSelector(element) {
|
|
515
|
+
let sel = element.tagName.toLowerCase();
|
|
516
|
+
const classes = filterUsefulClasses(element);
|
|
517
|
+
if (classes.length > 0) {
|
|
518
|
+
classes.sort((a, b) => b.length - a.length);
|
|
519
|
+
sel +=
|
|
520
|
+
'.' +
|
|
521
|
+
classes
|
|
522
|
+
.slice(0, 2)
|
|
523
|
+
.map((c) => CSS.escape(c))
|
|
524
|
+
.join('.');
|
|
525
|
+
}
|
|
526
|
+
return sel;
|
|
527
|
+
}
|
|
528
|
+
function makeUniqueWithNth(element, baseSelector) {
|
|
529
|
+
const parent = element.parentElement;
|
|
530
|
+
if (!parent)
|
|
531
|
+
return baseSelector;
|
|
532
|
+
const siblings = Array.from(parent.children);
|
|
533
|
+
const sameTagSiblings = siblings.filter((s) => s.tagName === element.tagName);
|
|
534
|
+
if (sameTagSiblings.length === 1)
|
|
535
|
+
return baseSelector;
|
|
536
|
+
const index = siblings.indexOf(element) + 1;
|
|
537
|
+
return baseSelector + ':nth-child(' + index + ')';
|
|
538
|
+
}
|
|
539
|
+
function getSiblingBasedSelector(element) {
|
|
540
|
+
let prevSibling = element.previousElementSibling;
|
|
541
|
+
let attempts = 0;
|
|
542
|
+
while (prevSibling && attempts < 3) {
|
|
543
|
+
const siblingSelector = getFeatureSelector(prevSibling);
|
|
544
|
+
if (siblingSelector && isUniqueSelector(siblingSelector)) {
|
|
545
|
+
const elementSelector = getBaseSelector(element);
|
|
546
|
+
const combined = siblingSelector + ' + ' + elementSelector;
|
|
547
|
+
if (isUniqueSelector(combined))
|
|
548
|
+
return combined;
|
|
549
|
+
}
|
|
550
|
+
prevSibling = prevSibling.previousElementSibling;
|
|
551
|
+
attempts++;
|
|
552
|
+
}
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
function buildComposedSelector(element) {
|
|
556
|
+
const selfSelector = getBestClassSelector(element);
|
|
557
|
+
if (selfSelector && isUniqueSelector(selfSelector))
|
|
558
|
+
return selfSelector;
|
|
559
|
+
const parts = [];
|
|
560
|
+
let current = element;
|
|
561
|
+
let depth = 0;
|
|
562
|
+
const maxDepth = 3;
|
|
563
|
+
while (current && current !== document.body && depth < maxDepth) {
|
|
564
|
+
const featureSelector = getFeatureSelector(current);
|
|
565
|
+
if (featureSelector) {
|
|
566
|
+
parts.unshift(featureSelector);
|
|
567
|
+
const elementSelector = depth === 0 ? getBaseSelector(element) : getBaseSelector(current);
|
|
568
|
+
const fullSelector = parts.join(' > ') + (depth > 0 ? '' : ' > ' + elementSelector);
|
|
569
|
+
if (isUniqueSelector(fullSelector))
|
|
570
|
+
return fullSelector;
|
|
571
|
+
}
|
|
572
|
+
else {
|
|
573
|
+
const baseSelector = getBaseSelector(current);
|
|
574
|
+
const selector = makeUniqueWithNth(current, baseSelector);
|
|
575
|
+
parts.unshift(selector);
|
|
576
|
+
const fullSelector = parts.join(' > ');
|
|
577
|
+
if (isUniqueSelector(fullSelector))
|
|
578
|
+
return fullSelector;
|
|
579
|
+
}
|
|
580
|
+
current = current.parentElement;
|
|
581
|
+
depth++;
|
|
582
|
+
}
|
|
583
|
+
return parts.length > 0 ? parts.join(' > ') : null;
|
|
584
|
+
}
|
|
585
|
+
function tryNthChild(element) {
|
|
586
|
+
const baseSelector = getBaseSelector(element);
|
|
587
|
+
const uniqueSelector = makeUniqueWithNth(element, baseSelector);
|
|
588
|
+
try {
|
|
589
|
+
if (document.querySelectorAll(uniqueSelector).length === 1)
|
|
590
|
+
return uniqueSelector;
|
|
591
|
+
}
|
|
592
|
+
catch (_e) {
|
|
593
|
+
// Intentionally ignored: invalid CSS selector in tryNthChild
|
|
594
|
+
}
|
|
595
|
+
return null;
|
|
596
|
+
}
|
|
597
|
+
function buildUniquePath(element) {
|
|
598
|
+
const parts = [];
|
|
599
|
+
let current = element;
|
|
600
|
+
let depth = 0;
|
|
601
|
+
while (current && current !== document.body && depth < 5) {
|
|
602
|
+
const baseSelector = getBaseSelector(current);
|
|
603
|
+
const selector = makeUniqueWithNth(current, baseSelector);
|
|
604
|
+
parts.unshift(selector);
|
|
605
|
+
const fullSelector = parts.join(' > ');
|
|
606
|
+
if (isUniqueSelector(fullSelector))
|
|
607
|
+
return fullSelector;
|
|
608
|
+
current = current.parentElement;
|
|
609
|
+
depth++;
|
|
610
|
+
}
|
|
611
|
+
return parts.length > 0 ? parts.join(' > ') : null;
|
|
612
|
+
}
|
|
613
|
+
function generateXPath(element) {
|
|
614
|
+
const htmlEl = element;
|
|
615
|
+
if (htmlEl.id)
|
|
616
|
+
return '//*[@id="' + htmlEl.id + '"]';
|
|
617
|
+
const testId = element.getAttribute('data-testid');
|
|
618
|
+
if (testId)
|
|
619
|
+
return '//*[@data-testid="' + testId + '"]';
|
|
620
|
+
const nameAttr = element.getAttribute('name');
|
|
621
|
+
if (nameAttr)
|
|
622
|
+
return '//' + element.tagName.toLowerCase() + '[@name="' + nameAttr + '"]';
|
|
623
|
+
const parts = [];
|
|
624
|
+
let current = element;
|
|
625
|
+
let depth = 0;
|
|
626
|
+
while (current && depth < 5) {
|
|
627
|
+
const curHtml = current;
|
|
628
|
+
if (curHtml.id) {
|
|
629
|
+
parts.unshift('//*[@id="' + curHtml.id + '"]');
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
const testId = current.getAttribute('data-testid');
|
|
633
|
+
if (testId) {
|
|
634
|
+
parts.unshift('//*[@data-testid="' + testId + '"]');
|
|
635
|
+
break;
|
|
636
|
+
}
|
|
637
|
+
const tagName = current.tagName.toLowerCase();
|
|
638
|
+
const parent = current.parentElement;
|
|
639
|
+
if (parent) {
|
|
640
|
+
const siblings = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
|
|
641
|
+
const index = siblings.indexOf(current) + 1;
|
|
642
|
+
parts.unshift(tagName + '[' + index + ']');
|
|
643
|
+
}
|
|
644
|
+
else {
|
|
645
|
+
parts.unshift(tagName);
|
|
646
|
+
}
|
|
647
|
+
current = current.parentElement;
|
|
648
|
+
depth++;
|
|
649
|
+
}
|
|
650
|
+
if (parts.length > 0 && !parts[0].startsWith('//'))
|
|
651
|
+
parts.unshift('//');
|
|
652
|
+
return parts.join('/');
|
|
653
|
+
}
|
|
654
|
+
let cssSelector = null;
|
|
655
|
+
cssSelector = tryIdSelector(el);
|
|
656
|
+
if (!cssSelector)
|
|
657
|
+
cssSelector = getMultiAttributeSelector(el);
|
|
658
|
+
if (!cssSelector)
|
|
659
|
+
cssSelector = getAttributeClassComboSelector(el);
|
|
660
|
+
if (!cssSelector)
|
|
661
|
+
cssSelector = getBestClassSelector(el);
|
|
662
|
+
if (!cssSelector)
|
|
663
|
+
cssSelector = getSiblingBasedSelector(el);
|
|
664
|
+
if (!cssSelector)
|
|
665
|
+
cssSelector = buildComposedSelector(el);
|
|
666
|
+
if (!cssSelector)
|
|
667
|
+
cssSelector = tryNthChild(el);
|
|
668
|
+
if (!cssSelector)
|
|
669
|
+
cssSelector = buildUniquePath(el);
|
|
670
|
+
if (!cssSelector)
|
|
671
|
+
cssSelector = el.tagName.toLowerCase();
|
|
672
|
+
const xpath = generateXPath(el);
|
|
673
|
+
return { cssSelector, xpath };
|
|
674
|
+
})
|
|
675
|
+
.catch(() => null);
|
|
676
|
+
if (selectorData) {
|
|
677
|
+
result[ref] = {
|
|
678
|
+
cssSelector: selectorData.cssSelector,
|
|
679
|
+
xpath: selectorData.xpath,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
catch (_e) {
|
|
684
|
+
// Intentionally ignored: element ref generation failed for this element
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
return result;
|
|
688
|
+
}
|
|
314
689
|
/**
|
|
315
690
|
* Get enhanced snapshot with refs and optional filtering
|
|
316
691
|
*/
|
|
@@ -378,7 +753,83 @@ export async function getEnhancedSnapshot(page, options = {}) {
|
|
|
378
753
|
if (options.path || options.attrs) {
|
|
379
754
|
await enrichRefsWithPathsAndAttrs(page, refs, options);
|
|
380
755
|
}
|
|
381
|
-
|
|
756
|
+
let finalTree = enhancedTree;
|
|
757
|
+
if (options.selectors && Object.keys(refs).length > 0) {
|
|
758
|
+
const selectorMap = await buildCompactSelectors(page, refs, options);
|
|
759
|
+
if (selectorMap) {
|
|
760
|
+
finalTree += '\n## Selectors\n' + selectorMap;
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
return { tree: finalTree, refs };
|
|
764
|
+
}
|
|
765
|
+
async function buildCompactSelectors(page, refs, options) {
|
|
766
|
+
const entries = Object.entries(refs);
|
|
767
|
+
const parts = [];
|
|
768
|
+
const includeAll = options?.all ?? false;
|
|
769
|
+
for (const [ref, data] of entries) {
|
|
770
|
+
if (data.role === 'clickable' || data.role === 'focusable')
|
|
771
|
+
continue;
|
|
772
|
+
try {
|
|
773
|
+
let locator;
|
|
774
|
+
if (data.name) {
|
|
775
|
+
locator = page.getByRole(data.role, {
|
|
776
|
+
name: data.name,
|
|
777
|
+
exact: true,
|
|
778
|
+
});
|
|
779
|
+
}
|
|
780
|
+
else {
|
|
781
|
+
locator = page.getByRole(data.role);
|
|
782
|
+
}
|
|
783
|
+
if (data.nth !== undefined)
|
|
784
|
+
locator = locator.nth(data.nth);
|
|
785
|
+
if (!includeAll) {
|
|
786
|
+
const isReallyVisible = await locator
|
|
787
|
+
.evaluate((el) => {
|
|
788
|
+
const style = getComputedStyle(el);
|
|
789
|
+
const rect = el.getBoundingClientRect();
|
|
790
|
+
return !(style.display === 'none' ||
|
|
791
|
+
style.visibility === 'hidden' ||
|
|
792
|
+
parseFloat(style.opacity) === 0 ||
|
|
793
|
+
(rect.width === 0 && rect.height === 0) ||
|
|
794
|
+
rect.x + rect.width < 0 ||
|
|
795
|
+
rect.y + rect.height < 0);
|
|
796
|
+
})
|
|
797
|
+
.catch(() => false);
|
|
798
|
+
if (!isReallyVisible)
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
const attrs = await locator
|
|
802
|
+
.evaluate((el) => {
|
|
803
|
+
const htmlEl = el;
|
|
804
|
+
const r = {};
|
|
805
|
+
if (htmlEl.dataset.testid)
|
|
806
|
+
r['testid'] = `[data-testid="${htmlEl.dataset.testid}"]`;
|
|
807
|
+
if (htmlEl.id && !htmlEl.id.match(/^[:]/))
|
|
808
|
+
r['id'] = '#' + CSS.escape(htmlEl.id);
|
|
809
|
+
const nameAttr = htmlEl.getAttribute('name');
|
|
810
|
+
if (nameAttr)
|
|
811
|
+
r['name'] = `${htmlEl.tagName.toLowerCase()}[name="${nameAttr}"]`;
|
|
812
|
+
return r;
|
|
813
|
+
})
|
|
814
|
+
.catch(() => null);
|
|
815
|
+
if (!attrs)
|
|
816
|
+
continue;
|
|
817
|
+
let bestSelector = '';
|
|
818
|
+
if (attrs.testid)
|
|
819
|
+
bestSelector = attrs.testid;
|
|
820
|
+
else if (attrs.id)
|
|
821
|
+
bestSelector = attrs.id;
|
|
822
|
+
else if (attrs.name)
|
|
823
|
+
bestSelector = attrs.name;
|
|
824
|
+
if (bestSelector) {
|
|
825
|
+
parts.push(`${ref}: ${bestSelector}`);
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
catch {
|
|
829
|
+
// skip
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
return parts.join(' | ');
|
|
382
833
|
}
|
|
383
834
|
async function enrichRefsWithPathsAndAttrs(page, refs, options) {
|
|
384
835
|
if (Object.keys(refs).length === 0) {
|
|
@@ -663,9 +1114,10 @@ async function enrichRefsWithPathsAndAttrs(page, refs, options) {
|
|
|
663
1114
|
const injectScript = `
|
|
664
1115
|
window.__AGENT_BROWSER_REFS__ = ${JSON.stringify(refs)};
|
|
665
1116
|
`;
|
|
666
|
-
|
|
1117
|
+
if ('evaluate' in page) {
|
|
1118
|
+
await page.evaluate(injectScript);
|
|
1119
|
+
}
|
|
667
1120
|
// Evaluate the function in the browser context
|
|
668
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
669
1121
|
const elementData = await page.evaluate(() => {
|
|
670
1122
|
const STYLE_CLASS_PATTERNS = [
|
|
671
1123
|
/^(flex|grid|block|inline|hidden)$/,
|
|
@@ -692,7 +1144,6 @@ async function enrichRefsWithPathsAndAttrs(page, refs, options) {
|
|
|
692
1144
|
'aside',
|
|
693
1145
|
'form',
|
|
694
1146
|
]);
|
|
695
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
696
1147
|
function getSemanticClass(element) {
|
|
697
1148
|
const className = element.getAttribute('class');
|
|
698
1149
|
if (!className)
|
|
@@ -705,7 +1156,6 @@ async function enrichRefsWithPathsAndAttrs(page, refs, options) {
|
|
|
705
1156
|
const selectedClasses = classes.slice(0, 2);
|
|
706
1157
|
return selectedClasses.map((cls) => 'contains(@class, "' + cls + '")').join(' and ');
|
|
707
1158
|
}
|
|
708
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
709
1159
|
function getElementIndex(element) {
|
|
710
1160
|
const parent = element.parentElement;
|
|
711
1161
|
if (!parent)
|
|
@@ -870,7 +1320,6 @@ async function enrichRefsWithPathsAndAttrs(page, refs, options) {
|
|
|
870
1320
|
return null;
|
|
871
1321
|
}
|
|
872
1322
|
const results = {};
|
|
873
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
874
1323
|
const refEntries = Object.entries(window.__AGENT_BROWSER_REFS__ || {});
|
|
875
1324
|
for (const [ref, data] of refEntries) {
|
|
876
1325
|
const targetRole = data.role;
|
|
@@ -941,13 +1390,14 @@ async function enrichRefsWithPathsAndAttrs(page, refs, options) {
|
|
|
941
1390
|
return;
|
|
942
1391
|
}
|
|
943
1392
|
for (const [ref, data] of Object.entries(elementData)) {
|
|
1393
|
+
const typedData = data;
|
|
944
1394
|
if (refs[ref] && data) {
|
|
945
1395
|
if (options.path) {
|
|
946
|
-
refs[ref].xpath =
|
|
947
|
-
refs[ref].cssPath =
|
|
1396
|
+
refs[ref].xpath = typedData.xpath;
|
|
1397
|
+
refs[ref].cssPath = typedData.cssPath;
|
|
948
1398
|
}
|
|
949
1399
|
if (options.attrs) {
|
|
950
|
-
refs[ref].attributes =
|
|
1400
|
+
refs[ref].attributes = typedData.attributes;
|
|
951
1401
|
}
|
|
952
1402
|
}
|
|
953
1403
|
}
|
|
@@ -1232,8 +1682,9 @@ export function getSemanticClass(element) {
|
|
|
1232
1682
|
return selectedClasses.map((cls) => `contains(@class, "${cls}")`).join(' and ');
|
|
1233
1683
|
}
|
|
1234
1684
|
export function generateXPath(element, maxDepth = 5) {
|
|
1235
|
-
|
|
1236
|
-
|
|
1685
|
+
const elemId = element.id;
|
|
1686
|
+
if (elemId) {
|
|
1687
|
+
return `//*[@id="${elemId}"]`;
|
|
1237
1688
|
}
|
|
1238
1689
|
const testId = element.getAttribute('data-testid');
|
|
1239
1690
|
if (testId) {
|
|
@@ -1254,8 +1705,9 @@ function buildRelativeXPath(element, maxDepth) {
|
|
|
1254
1705
|
let current = element;
|
|
1255
1706
|
let depth = 0;
|
|
1256
1707
|
while (current && depth < maxDepth) {
|
|
1257
|
-
|
|
1258
|
-
|
|
1708
|
+
const currentId = current.id;
|
|
1709
|
+
if (currentId) {
|
|
1710
|
+
path.unshift(`//*[@id="${currentId}"]`);
|
|
1259
1711
|
break;
|
|
1260
1712
|
}
|
|
1261
1713
|
const testId = current.getAttribute('data-testid');
|
|
@@ -1287,8 +1739,9 @@ function getElementIndex(element) {
|
|
|
1287
1739
|
return siblings.indexOf(element) + 1;
|
|
1288
1740
|
}
|
|
1289
1741
|
export function generateCSSPath(element, maxDepth = 5) {
|
|
1290
|
-
|
|
1291
|
-
|
|
1742
|
+
const elemId = element.id;
|
|
1743
|
+
if (elemId) {
|
|
1744
|
+
return `#${elemId}`;
|
|
1292
1745
|
}
|
|
1293
1746
|
const testId = element.getAttribute('data-testid');
|
|
1294
1747
|
if (testId) {
|
|
@@ -1298,8 +1751,9 @@ export function generateCSSPath(element, maxDepth = 5) {
|
|
|
1298
1751
|
let current = element;
|
|
1299
1752
|
let depth = 0;
|
|
1300
1753
|
while (current && depth < maxDepth) {
|
|
1301
|
-
|
|
1302
|
-
|
|
1754
|
+
const currentId = current.id;
|
|
1755
|
+
if (currentId) {
|
|
1756
|
+
path.unshift(`#${currentId}`);
|
|
1303
1757
|
break;
|
|
1304
1758
|
}
|
|
1305
1759
|
const testId = current.getAttribute('data-testid');
|