@dyyz1993/agent-browser 0.27.4 → 0.29.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.
Files changed (184) hide show
  1. package/dist/actions/index.d.ts.map +1 -1
  2. package/dist/actions/index.js +160 -296
  3. package/dist/actions/index.js.map +1 -1
  4. package/dist/actions/interaction.d.ts.map +1 -1
  5. package/dist/actions/interaction.js +3 -2
  6. package/dist/actions/interaction.js.map +1 -1
  7. package/dist/actions/search.d.ts.map +1 -1
  8. package/dist/actions/search.js +15 -5
  9. package/dist/actions/search.js.map +1 -1
  10. package/dist/actions/utils.js +1 -1
  11. package/dist/actions/utils.js.map +1 -1
  12. package/dist/browser/browser-manager.d.ts.map +1 -1
  13. package/dist/browser/browser-manager.js +1 -0
  14. package/dist/browser/browser-manager.js.map +1 -1
  15. package/dist/cli/commands/index.d.ts +5 -0
  16. package/dist/cli/commands/index.d.ts.map +1 -0
  17. package/dist/cli/commands/index.js +217 -0
  18. package/dist/cli/commands/index.js.map +1 -0
  19. package/dist/cli/commands/interact.d.ts +7 -0
  20. package/dist/cli/commands/interact.d.ts.map +1 -0
  21. package/dist/cli/commands/interact.js +371 -0
  22. package/dist/cli/commands/interact.js.map +1 -0
  23. package/dist/cli/commands/navigate.d.ts +4 -0
  24. package/dist/cli/commands/navigate.d.ts.map +1 -0
  25. package/dist/cli/commands/navigate.js +46 -0
  26. package/dist/cli/commands/navigate.js.map +1 -0
  27. package/dist/cli/commands/network.d.ts +3 -0
  28. package/dist/cli/commands/network.d.ts.map +1 -0
  29. package/dist/cli/commands/network.js +292 -0
  30. package/dist/cli/commands/network.js.map +1 -0
  31. package/dist/cli/commands/plugin.d.ts +3 -0
  32. package/dist/cli/commands/plugin.d.ts.map +1 -0
  33. package/dist/cli/commands/plugin.js +84 -0
  34. package/dist/cli/commands/plugin.js.map +1 -0
  35. package/dist/cli/commands/query.d.ts +7 -0
  36. package/dist/cli/commands/query.d.ts.map +1 -0
  37. package/dist/cli/commands/query.js +333 -0
  38. package/dist/cli/commands/query.js.map +1 -0
  39. package/dist/cli/commands/session.d.ts +3 -0
  40. package/dist/cli/commands/session.d.ts.map +1 -0
  41. package/dist/cli/commands/session.js +369 -0
  42. package/dist/cli/commands/session.js.map +1 -0
  43. package/dist/cli/commands/shared.d.ts +24 -0
  44. package/dist/cli/commands/shared.d.ts.map +1 -0
  45. package/dist/cli/commands/shared.js +113 -0
  46. package/dist/cli/commands/shared.js.map +1 -0
  47. package/dist/cli/commands.d.ts +1 -7
  48. package/dist/cli/commands.d.ts.map +1 -1
  49. package/dist/cli/commands.js +1 -1684
  50. package/dist/cli/commands.js.map +1 -1
  51. package/dist/cli/help.d.ts.map +1 -1
  52. package/dist/cli/help.js +1 -24
  53. package/dist/cli/help.js.map +1 -1
  54. package/dist/daemon.d.ts.map +1 -1
  55. package/dist/daemon.js +31 -1
  56. package/dist/daemon.js.map +1 -1
  57. package/dist/openapi.d.ts.map +1 -1
  58. package/dist/openapi.js +2 -1
  59. package/dist/openapi.js.map +1 -1
  60. package/dist/plugins/context.d.ts.map +1 -1
  61. package/dist/plugins/context.js +23 -6
  62. package/dist/plugins/context.js.map +1 -1
  63. package/dist/plugins/registry.d.ts.map +1 -1
  64. package/dist/plugins/registry.js +4 -1
  65. package/dist/plugins/registry.js.map +1 -1
  66. package/dist/plugins/types.d.ts +35 -4
  67. package/dist/plugins/types.d.ts.map +1 -1
  68. package/dist/snapshot/constants.d.ts +6 -0
  69. package/dist/snapshot/constants.d.ts.map +1 -0
  70. package/dist/snapshot/constants.js +77 -0
  71. package/dist/snapshot/constants.js.map +1 -0
  72. package/dist/snapshot/dom-scripts.d.ts +12 -0
  73. package/dist/snapshot/dom-scripts.d.ts.map +1 -0
  74. package/dist/snapshot/dom-scripts.js +438 -0
  75. package/dist/snapshot/dom-scripts.js.map +1 -0
  76. package/dist/snapshot/format.d.ts +13 -0
  77. package/dist/snapshot/format.d.ts.map +1 -0
  78. package/dist/snapshot/format.js +175 -0
  79. package/dist/snapshot/format.js.map +1 -0
  80. package/dist/snapshot/index.d.ts +6 -0
  81. package/dist/snapshot/index.d.ts.map +1 -0
  82. package/dist/snapshot/index.js +5 -0
  83. package/dist/snapshot/index.js.map +1 -0
  84. package/dist/snapshot/refs.d.ts +3 -0
  85. package/dist/snapshot/refs.d.ts.map +1 -0
  86. package/dist/snapshot/refs.js +8 -0
  87. package/dist/snapshot/refs.js.map +1 -0
  88. package/dist/snapshot/selectors.d.ts +17 -0
  89. package/dist/snapshot/selectors.d.ts.map +1 -0
  90. package/dist/snapshot/selectors.js +619 -0
  91. package/dist/snapshot/selectors.js.map +1 -0
  92. package/dist/snapshot/snapshot.d.ts +12 -0
  93. package/dist/snapshot/snapshot.d.ts.map +1 -0
  94. package/dist/snapshot/snapshot.js +104 -0
  95. package/dist/snapshot/snapshot.js.map +1 -0
  96. package/dist/snapshot/types.d.ts +27 -0
  97. package/dist/snapshot/types.d.ts.map +1 -0
  98. package/dist/snapshot/types.js +2 -0
  99. package/dist/snapshot/types.js.map +1 -0
  100. package/dist/snapshot.d.ts +1 -79
  101. package/dist/snapshot.d.ts.map +1 -1
  102. package/dist/snapshot.js +1 -1800
  103. package/dist/snapshot.js.map +1 -1
  104. package/dist/ssr-detection.d.ts +9 -0
  105. package/dist/ssr-detection.d.ts.map +1 -0
  106. package/dist/ssr-detection.js +119 -0
  107. package/dist/ssr-detection.js.map +1 -0
  108. package/dist/stream/client-state.d.ts +13 -0
  109. package/dist/stream/client-state.d.ts.map +1 -0
  110. package/dist/stream/client-state.js +2 -0
  111. package/dist/stream/client-state.js.map +1 -0
  112. package/dist/stream/element-utils.d.ts +8 -0
  113. package/dist/stream/element-utils.d.ts.map +1 -0
  114. package/dist/stream/element-utils.js +25 -0
  115. package/dist/stream/element-utils.js.map +1 -0
  116. package/dist/stream/frame-processor.d.ts +63 -0
  117. package/dist/stream/frame-processor.d.ts.map +1 -0
  118. package/dist/stream/frame-processor.js +178 -0
  119. package/dist/stream/frame-processor.js.map +1 -0
  120. package/dist/stream/index.d.ts +10 -0
  121. package/dist/stream/index.d.ts.map +1 -0
  122. package/dist/stream/index.js +5 -0
  123. package/dist/stream/index.js.map +1 -0
  124. package/dist/stream/input-handler.d.ts +10 -0
  125. package/dist/stream/input-handler.d.ts.map +1 -0
  126. package/dist/stream/input-handler.js +81 -0
  127. package/dist/stream/input-handler.js.map +1 -0
  128. package/dist/stream/messages.d.ts +144 -0
  129. package/dist/stream/messages.d.ts.map +1 -0
  130. package/dist/stream/messages.js +46 -0
  131. package/dist/stream/messages.js.map +1 -0
  132. package/dist/stream-server-standalone.d.ts +0 -3
  133. package/dist/stream-server-standalone.d.ts.map +1 -1
  134. package/dist/stream-server-standalone.js +223 -101
  135. package/dist/stream-server-standalone.js.map +1 -1
  136. package/dist/stream-server.d.ts +8 -212
  137. package/dist/stream-server.d.ts.map +1 -1
  138. package/dist/stream-server.js +35 -389
  139. package/dist/stream-server.js.map +1 -1
  140. package/dist/types/base.d.ts +11 -0
  141. package/dist/types/base.d.ts.map +1 -0
  142. package/dist/types/base.js +2 -0
  143. package/dist/types/base.js.map +1 -0
  144. package/dist/types/browser.d.ts +26 -0
  145. package/dist/types/browser.d.ts.map +1 -0
  146. package/dist/types/browser.js +2 -0
  147. package/dist/types/browser.js.map +1 -0
  148. package/dist/types/commands.d.ts +763 -0
  149. package/dist/types/commands.d.ts.map +1 -0
  150. package/dist/types/commands.js +2 -0
  151. package/dist/types/commands.js.map +1 -0
  152. package/dist/types/crawl.d.ts +89 -0
  153. package/dist/types/crawl.d.ts.map +1 -0
  154. package/dist/types/crawl.js +2 -0
  155. package/dist/types/crawl.js.map +1 -0
  156. package/dist/types/index.d.ts +9 -0
  157. package/dist/types/index.d.ts.map +1 -0
  158. package/dist/types/index.js +9 -0
  159. package/dist/types/index.js.map +1 -0
  160. package/dist/types/interact.d.ts +61 -0
  161. package/dist/types/interact.d.ts.map +1 -0
  162. package/dist/types/interact.js +2 -0
  163. package/dist/types/interact.js.map +1 -0
  164. package/dist/types/plugins.d.ts +39 -0
  165. package/dist/types/plugins.d.ts.map +1 -0
  166. package/dist/types/plugins.js +2 -0
  167. package/dist/types/plugins.js.map +1 -0
  168. package/dist/types/responses.d.ts +140 -0
  169. package/dist/types/responses.d.ts.map +1 -0
  170. package/dist/types/responses.js +4 -0
  171. package/dist/types/responses.js.map +1 -0
  172. package/dist/types/utils.d.ts +12 -0
  173. package/dist/types/utils.d.ts.map +1 -0
  174. package/dist/types/utils.js +2 -0
  175. package/dist/types/utils.js.map +1 -0
  176. package/dist/types.d.ts +1 -1121
  177. package/dist/types.d.ts.map +1 -1
  178. package/dist/types.js +1 -3
  179. package/dist/types.js.map +1 -1
  180. package/dist/version.d.ts +2 -0
  181. package/dist/version.d.ts.map +1 -0
  182. package/dist/version.js +31 -0
  183. package/dist/version.js.map +1 -0
  184. package/package.json +1 -1
package/dist/snapshot.js CHANGED
@@ -1,1801 +1,2 @@
1
- /**
2
- * Enhanced snapshot with element refs for deterministic element selection.
3
- *
4
- * This module generates accessibility snapshots with embedded refs that can be
5
- * used to click/fill/interact with elements without re-querying the DOM.
6
- *
7
- * Example output:
8
- * - heading "Example Domain" [ref=e1] [level=1]
9
- * - paragraph: Some text content
10
- * - button "Submit" [ref=e2]
11
- * - textbox "Email" [ref=e3]
12
- *
13
- * Usage:
14
- * agent-browser snapshot # Full snapshot
15
- * agent-browser snapshot -i # Interactive elements only
16
- * agent-browser snapshot --depth 3 # Limit depth
17
- * agent-browser click @e2 # Click element by ref
18
- */
19
- // Counter for generating refs
20
- let refCounter = 0;
21
- /**
22
- * Reset ref counter (call at start of each snapshot)
23
- */
24
- export function resetRefs() {
25
- refCounter = 0;
26
- }
27
- /**
28
- * Generate next ref ID
29
- */
30
- function nextRef() {
31
- return `e${++refCounter}`;
32
- }
33
- /**
34
- * Roles that are interactive and should get refs
35
- */
36
- const INTERACTIVE_ROLES = new Set([
37
- 'button',
38
- 'link',
39
- 'textbox',
40
- 'checkbox',
41
- 'radio',
42
- 'combobox',
43
- 'listbox',
44
- 'menuitem',
45
- 'menuitemcheckbox',
46
- 'menuitemradio',
47
- 'option',
48
- 'searchbox',
49
- 'slider',
50
- 'spinbutton',
51
- 'switch',
52
- 'tab',
53
- 'treeitem',
54
- ]);
55
- /**
56
- * Roles that provide structure/context (get refs for text extraction)
57
- */
58
- const CONTENT_ROLES = new Set([
59
- 'heading',
60
- 'cell',
61
- 'gridcell',
62
- 'columnheader',
63
- 'rowheader',
64
- 'listitem',
65
- 'article',
66
- 'region',
67
- 'main',
68
- 'navigation',
69
- ]);
70
- /**
71
- * Roles that are purely structural (can be filtered in compact mode)
72
- */
73
- const STRUCTURAL_ROLES = new Set([
74
- 'generic',
75
- 'group',
76
- 'list',
77
- 'table',
78
- 'row',
79
- 'rowgroup',
80
- 'grid',
81
- 'treegrid',
82
- 'menu',
83
- 'menubar',
84
- 'toolbar',
85
- 'tablist',
86
- 'tree',
87
- 'directory',
88
- 'document',
89
- 'application',
90
- 'presentation',
91
- 'none',
92
- ]);
93
- /**
94
- * Build a selector string for storing in ref map
95
- */
96
- function buildSelector(role, name) {
97
- if (name) {
98
- const escapedName = name.replace(/"/g, '\\"');
99
- return `getByRole('${role}', { name: "${escapedName}", exact: true })`;
100
- }
101
- return `getByRole('${role}')`;
102
- }
103
- /**
104
- * Query the page for clickable elements that might not have proper ARIA roles.
105
- * This finds elements with cursor: pointer or onclick handlers.
106
- */
107
- async function findCursorInteractiveElements(page, selector) {
108
- const rootSelector = selector || 'body';
109
- // Use a string function body to avoid TypeScript transpilation issues
110
- const scriptBody = `(rootSel) => {
111
- const results = [];
112
-
113
- // Elements that already have interactive ARIA roles - skip these
114
- const interactiveRoles = new Set([
115
- 'button', 'link', 'textbox', 'checkbox', 'radio', 'combobox', 'listbox',
116
- 'menuitem', 'menuitemcheckbox', 'menuitemradio', 'option', 'searchbox',
117
- 'slider', 'spinbutton', 'switch', 'tab', 'treeitem'
118
- ]);
119
-
120
- // Tags that are already interactive by default
121
- const interactiveTags = new Set([
122
- 'a', 'button', 'input', 'select', 'textarea', 'details', 'summary'
123
- ]);
124
-
125
- const root = document.querySelector(rootSel) || document.body;
126
- const allElements = root.querySelectorAll('*');
127
-
128
- // Build a unique selector for an element
129
- const buildSelector = (el) => {
130
- const testId = el.getAttribute('data-testid');
131
- if (testId) return '[data-testid="' + testId + '"]';
132
- if (el.id) return '#' + CSS.escape(el.id);
133
-
134
- const path = [];
135
- let current = el;
136
- while (current && current !== document.body) {
137
- let sel = current.tagName.toLowerCase();
138
- const classes = Array.from(current.classList).filter(c => c.trim());
139
- if (classes.length > 0) sel += '.' + CSS.escape(classes[0]);
140
-
141
- const parent = current.parentElement;
142
- if (parent) {
143
- const siblings = Array.from(parent.children);
144
- const matching = siblings.filter(s => {
145
- if (s.tagName !== current.tagName) return false;
146
- if (classes.length > 0 && !s.classList.contains(classes[0])) return false;
147
- return true;
148
- });
149
- if (matching.length > 1) {
150
- const idx = matching.indexOf(current) + 1;
151
- sel += ':nth-of-type(' + idx + ')';
152
- }
153
- }
154
- path.unshift(sel);
155
- current = current.parentElement;
156
- if (path.length >= 3) break;
157
- }
158
- return path.join(' > ');
159
- };
160
-
161
- // Heuristic patterns for clickable element class names
162
- // Use substring matching (contains) for more flexible detection
163
- const clickableClassPatterns = [
164
- /tab/i, // matches: tab, tabs, tab-item, creator-tab, tablist
165
- /btn/i, // matches: btn, button, btn-primary, my-btn
166
- /button/i, // matches: button, button-group, my-button
167
- /clickable/i, // matches: clickable, is-clickable, clickable-area
168
- /action/i, // matches: action, actions, action-btn, user-action
169
- /menuitem/i, // matches: menuitem, menu-item, my-menuitem
170
- /navitem/i, // matches: navitem, nav-item, navbar-item
171
- /listitem/i, // matches: listitem, list-item, my-listitem
172
- /card/i, // matches: card, card-item, profile-card
173
- /toggle/i, // matches: toggle, toggle-btn, dark-toggle
174
- /switch/i, // matches: switch, switch-btn, toggle-switch
175
- /dropdown/i, // matches: dropdown, dropdown-menu, my-dropdown
176
- /modal/i, // matches: modal, modal-trigger, modal-open
177
- /popup/i, // matches: popup, popup-trigger, show-popup
178
- /close/i, // matches: close, close-btn, modal-close
179
- /dismiss/i, // matches: dismiss, dismiss-btn
180
- /expand/i, // matches: expand, expand-btn, expandable
181
- /collapse/i, // matches: collapse, collapse-btn, collapsible
182
- ];
183
-
184
- // Check if element looks clickable based on class name
185
- const hasClickableClassName = (el) => {
186
- const className = el.getAttribute('class') || '';
187
- // Split by whitespace and check each individual class
188
- const classes = className.split(/\s+/);
189
- return classes.some(cls => clickableClassPatterns.some(p => p.test(cls)));
190
- };
191
-
192
- // Check for Vue/React event handlers (data attributes or special patterns)
193
- const hasFrameworkEventHandler = (el) => {
194
- // Vue: elements with @click often have data-v-* attributes and are in components
195
- // Check for common Vue/React patterns
196
- const attrs = el.attributes;
197
- for (let i = 0; i < attrs.length; i++) {
198
- const name = attrs[i].name;
199
- // Vue compiled @click handlers
200
- if (name.startsWith('data-v-')) return true;
201
- // React synthetic events sometimes leave traces
202
- if (name.startsWith('data-reactid')) return true;
203
- }
204
- return false;
205
- };
206
-
207
- for (const el of allElements) {
208
- const tagName = el.tagName.toLowerCase();
209
- if (interactiveTags.has(tagName)) continue;
210
-
211
- const role = el.getAttribute('role');
212
- if (role && interactiveRoles.has(role.toLowerCase())) continue;
213
-
214
- const computedStyle = getComputedStyle(el);
215
- const hasCursorPointer = computedStyle.cursor === 'pointer';
216
- const hasOnClick = el.hasAttribute('onclick') || el.onclick !== null;
217
- const tabIndex = el.getAttribute('tabindex');
218
- const hasTabIndex = tabIndex !== null && tabIndex !== '-1';
219
-
220
- // New heuristic checks
221
- const looksClickable = hasClickableClassName(el);
222
- const hasFrameworkEvent = hasFrameworkEventHandler(el);
223
-
224
- // Skip if no indication of interactivity
225
- // Only include elements that look explicitly clickable (not just have framework events)
226
- const isClickable = hasCursorPointer || hasOnClick || hasTabIndex || looksClickable;
227
- if (!isClickable) continue;
228
-
229
- // Get direct text content only (exclude child elements' text)
230
- // This helps avoid duplicate entries from parent containers
231
- let text = '';
232
- for (const node of el.childNodes) {
233
- if (node.nodeType === Node.TEXT_NODE) {
234
- text += node.textContent || '';
235
- }
236
- }
237
- text = text.trim();
238
-
239
- // If no direct text, check if element has only one child element (like a span)
240
- // This handles cases like <div class="tab"><span>Tab Text</span></div>
241
- if (!text && el.children.length === 1) {
242
- const child = el.children[0];
243
- // Check if child is a simple inline element with only text
244
- if (child.tagName === 'SPAN' || child.tagName === 'A' || child.tagName === 'LABEL') {
245
- text = (child.textContent || '').trim();
246
- }
247
- }
248
-
249
- // Skip if no text
250
- if (!text) continue;
251
-
252
- // Skip if text is too long (likely a container with many children)
253
- if (text.length > 30) continue;
254
-
255
- const rect = el.getBoundingClientRect();
256
- if (rect.width === 0 || rect.height === 0) continue;
257
-
258
- // Determine if this is truly clickable or just has framework events
259
- const trulyClickable = hasCursorPointer || hasOnClick || hasTabIndex || looksClickable;
260
-
261
- results.push({
262
- selector: buildSelector(el),
263
- text,
264
- tagName,
265
- hasOnClick,
266
- hasCursorPointer: trulyClickable,
267
- hasTabIndex
268
- });
269
- }
270
- return results;
271
- }`;
272
- const fn = new Function('return ' + scriptBody)();
273
- return page.evaluate(fn, rootSelector);
274
- }
275
- /**
276
- * Suggest common selectors for the current page
277
- */
278
- async function suggestSelectors(page) {
279
- const selectors = [];
280
- try {
281
- const commonSelectors = [
282
- 'body',
283
- 'main',
284
- '#main',
285
- '#content',
286
- '.content',
287
- 'article',
288
- 'form',
289
- '#app',
290
- '.app',
291
- ];
292
- for (const selector of commonSelectors) {
293
- try {
294
- const locator = page.locator(selector);
295
- const count = await locator.count();
296
- if (count > 0) {
297
- selectors.push(selector);
298
- }
299
- }
300
- catch {
301
- // Ignore errors
302
- }
303
- }
304
- if (selectors.length === 0) {
305
- selectors.push('body');
306
- }
307
- }
308
- catch {
309
- selectors.push('body');
310
- }
311
- return selectors;
312
- }
313
- export async function generateStableSelectors(page, refs) {
314
- const result = {};
315
- for (const [ref, data] of Object.entries(refs)) {
316
- if (data.role === 'clickable' || data.role === 'focusable') {
317
- if (data.selector && !data.selector.startsWith('getByRole')) {
318
- result[ref] = { cssSelector: data.selector, xpath: '' };
319
- }
320
- continue;
321
- }
322
- try {
323
- let locator;
324
- if (data.name) {
325
- locator = page.getByRole(data.role, {
326
- name: data.name,
327
- exact: true,
328
- });
329
- }
330
- else {
331
- locator = page.getByRole(data.role);
332
- }
333
- if (data.nth !== undefined) {
334
- locator = locator.nth(data.nth);
335
- }
336
- const elementCount = await locator.count();
337
- if (elementCount === 0)
338
- continue;
339
- const selectorData = await locator
340
- .evaluate((el) => {
341
- const UTILITY_CLASS_PATTERNS = [
342
- /^_/,
343
- /^css-/,
344
- /^[a-z]{1,2}$/,
345
- /^(active|disabled|hidden|visible|selected|hover|focus|current|open|closed)$/i,
346
- /^(text-|font-|bg-|p-|m-|w-|h-|flex|grid|border|rounded|shadow|opacity|z-)/,
347
- /^(sm:|md:|lg:|xl:|2xl:)/,
348
- ];
349
- const SEMANTIC_ATTRS = [
350
- 'data-testid',
351
- 'data-test',
352
- 'data-cy',
353
- 'name',
354
- 'aria-label',
355
- 'aria-labelledby',
356
- 'role',
357
- 'type',
358
- 'placeholder',
359
- 'title',
360
- 'alt',
361
- ];
362
- function isHighEntropyClassName(className) {
363
- if (!className || className.length < 4 || className.length > 15)
364
- return false;
365
- if (/^[a-zA-Z]+_[a-zA-Z]+_{2}[a-zA-Z0-9]+$/.test(className))
366
- return true;
367
- if (/^sc-[a-zA-Z0-9]+$/.test(className))
368
- return true;
369
- const hasUpper = /[A-Z]/.test(className);
370
- const hasLower = /[a-z]/.test(className);
371
- const hasDigit = /[0-9]/.test(className);
372
- const hasSeparator = /[-_]/.test(className);
373
- if (hasSeparator)
374
- return false;
375
- if (hasUpper && hasLower && hasDigit)
376
- return true;
377
- if (/^[A-Z][a-z0-9]+[A-Z]/.test(className) && className.length <= 12)
378
- return true;
379
- if (/^[a-z]/.test(className) && /[a-z][A-Z][a-z][A-Z]/.test(className))
380
- return true;
381
- return false;
382
- }
383
- function isUniqueSelector(selector) {
384
- try {
385
- return document.querySelectorAll(selector).length === 1;
386
- }
387
- catch {
388
- return false;
389
- }
390
- }
391
- function filterUsefulClasses(element) {
392
- const htmlEl = element;
393
- if (!htmlEl.className || typeof htmlEl.className !== 'string')
394
- return [];
395
- return htmlEl.className
396
- .trim()
397
- .split(/\s+/)
398
- .filter((c) => {
399
- if (!c)
400
- return false;
401
- if (UTILITY_CLASS_PATTERNS.some((p) => p.test(c)))
402
- return false;
403
- if (isHighEntropyClassName(c))
404
- return false;
405
- return true;
406
- });
407
- }
408
- function tryIdSelector(element) {
409
- const htmlEl = element;
410
- if (htmlEl.id) {
411
- const sel = '#' + CSS.escape(htmlEl.id);
412
- if (isUniqueSelector(sel))
413
- return sel;
414
- }
415
- return null;
416
- }
417
- function getMultiAttributeSelector(element) {
418
- const tag = element.tagName.toLowerCase();
419
- const attrs = [];
420
- for (const attr of SEMANTIC_ATTRS) {
421
- const value = element.getAttribute(attr);
422
- if (value)
423
- attrs.push({ attr, value });
424
- }
425
- if (attrs.length === 0)
426
- return null;
427
- for (const { attr, value } of attrs) {
428
- const sel = tag + '[' + attr + '="' + CSS.escape(value) + '"]';
429
- if (isUniqueSelector(sel))
430
- return sel;
431
- }
432
- if (attrs.length >= 2) {
433
- for (let i = 0; i < attrs.length; i++) {
434
- for (let j = i + 1; j < attrs.length; j++) {
435
- const sel = tag +
436
- '[' +
437
- attrs[i].attr +
438
- '="' +
439
- CSS.escape(attrs[i].value) +
440
- '"]' +
441
- '[' +
442
- attrs[j].attr +
443
- '="' +
444
- CSS.escape(attrs[j].value) +
445
- '"]';
446
- if (isUniqueSelector(sel))
447
- return sel;
448
- }
449
- }
450
- }
451
- return null;
452
- }
453
- function getAttributeClassComboSelector(element) {
454
- const tag = element.tagName.toLowerCase();
455
- const classes = filterUsefulClasses(element);
456
- if (classes.length === 0)
457
- return null;
458
- classes.sort((a, b) => b.length - a.length);
459
- const bestClass = classes[0];
460
- for (const attr of SEMANTIC_ATTRS) {
461
- const value = element.getAttribute(attr);
462
- if (value) {
463
- const sel = tag + '.' + CSS.escape(bestClass) + '[' + attr + '="' + CSS.escape(value) + '"]';
464
- if (isUniqueSelector(sel))
465
- return sel;
466
- }
467
- }
468
- return null;
469
- }
470
- function getBestClassSelector(element) {
471
- const classes = filterUsefulClasses(element);
472
- if (classes.length === 0)
473
- return null;
474
- classes.sort((a, b) => b.length - a.length);
475
- const tag = element.tagName.toLowerCase();
476
- for (const cls of classes) {
477
- const sel = tag + '.' + CSS.escape(cls);
478
- if (isUniqueSelector(sel))
479
- return sel;
480
- }
481
- for (let i = 2; i <= Math.min(3, classes.length); i++) {
482
- const sel = tag +
483
- '.' +
484
- classes
485
- .slice(0, i)
486
- .map((c) => CSS.escape(c))
487
- .join('.');
488
- if (isUniqueSelector(sel))
489
- return sel;
490
- }
491
- return null;
492
- }
493
- function getFeatureSelector(element) {
494
- if (!element || element === document.body)
495
- return null;
496
- const htmlEl = element;
497
- if (htmlEl.id)
498
- return '#' + CSS.escape(htmlEl.id);
499
- for (const attr of ['data-testid', 'data-test', 'name', 'role', 'aria-label']) {
500
- const value = element.getAttribute(attr);
501
- if (value)
502
- return element.tagName.toLowerCase() + '[' + attr + '="' + CSS.escape(value) + '"]';
503
- }
504
- const classes = filterUsefulClasses(element);
505
- if (classes.length > 0) {
506
- classes.sort((a, b) => b.length - a.length);
507
- const sel = element.tagName.toLowerCase() + '.' + CSS.escape(classes[0]);
508
- if (isUniqueSelector(sel))
509
- return sel;
510
- }
511
- return null;
512
- }
513
- function getBaseSelector(element) {
514
- let sel = element.tagName.toLowerCase();
515
- const classes = filterUsefulClasses(element);
516
- if (classes.length > 0) {
517
- classes.sort((a, b) => b.length - a.length);
518
- sel +=
519
- '.' +
520
- classes
521
- .slice(0, 2)
522
- .map((c) => CSS.escape(c))
523
- .join('.');
524
- }
525
- return sel;
526
- }
527
- function makeUniqueWithNth(element, baseSelector) {
528
- const parent = element.parentElement;
529
- if (!parent)
530
- return baseSelector;
531
- const siblings = Array.from(parent.children);
532
- const sameTagSiblings = siblings.filter((s) => s.tagName === element.tagName);
533
- if (sameTagSiblings.length === 1)
534
- return baseSelector;
535
- const index = siblings.indexOf(element) + 1;
536
- return baseSelector + ':nth-child(' + index + ')';
537
- }
538
- function getSiblingBasedSelector(element) {
539
- let prevSibling = element.previousElementSibling;
540
- let attempts = 0;
541
- while (prevSibling && attempts < 3) {
542
- const siblingSelector = getFeatureSelector(prevSibling);
543
- if (siblingSelector && isUniqueSelector(siblingSelector)) {
544
- const elementSelector = getBaseSelector(element);
545
- const combined = siblingSelector + ' + ' + elementSelector;
546
- if (isUniqueSelector(combined))
547
- return combined;
548
- }
549
- prevSibling = prevSibling.previousElementSibling;
550
- attempts++;
551
- }
552
- return null;
553
- }
554
- function buildComposedSelector(element) {
555
- const selfSelector = getBestClassSelector(element);
556
- if (selfSelector && isUniqueSelector(selfSelector))
557
- return selfSelector;
558
- const parts = [];
559
- let current = element;
560
- let depth = 0;
561
- const maxDepth = 3;
562
- while (current && current !== document.body && depth < maxDepth) {
563
- const featureSelector = getFeatureSelector(current);
564
- if (featureSelector) {
565
- parts.unshift(featureSelector);
566
- const elementSelector = depth === 0 ? getBaseSelector(element) : getBaseSelector(current);
567
- const fullSelector = parts.join(' > ') + (depth > 0 ? '' : ' > ' + elementSelector);
568
- if (isUniqueSelector(fullSelector))
569
- return fullSelector;
570
- }
571
- else {
572
- const baseSelector = getBaseSelector(current);
573
- const selector = makeUniqueWithNth(current, baseSelector);
574
- parts.unshift(selector);
575
- const fullSelector = parts.join(' > ');
576
- if (isUniqueSelector(fullSelector))
577
- return fullSelector;
578
- }
579
- current = current.parentElement;
580
- depth++;
581
- }
582
- return parts.length > 0 ? parts.join(' > ') : null;
583
- }
584
- function tryNthChild(element) {
585
- const baseSelector = getBaseSelector(element);
586
- const uniqueSelector = makeUniqueWithNth(element, baseSelector);
587
- try {
588
- if (document.querySelectorAll(uniqueSelector).length === 1)
589
- return uniqueSelector;
590
- }
591
- catch {
592
- // Intentionally ignored: invalid CSS selector in tryNthChild
593
- }
594
- return null;
595
- }
596
- function buildUniquePath(element) {
597
- const parts = [];
598
- let current = element;
599
- let depth = 0;
600
- while (current && current !== document.body && depth < 5) {
601
- const baseSelector = getBaseSelector(current);
602
- const selector = makeUniqueWithNth(current, baseSelector);
603
- parts.unshift(selector);
604
- const fullSelector = parts.join(' > ');
605
- if (isUniqueSelector(fullSelector))
606
- return fullSelector;
607
- current = current.parentElement;
608
- depth++;
609
- }
610
- return parts.length > 0 ? parts.join(' > ') : null;
611
- }
612
- function generateXPath(element) {
613
- const htmlEl = element;
614
- if (htmlEl.id)
615
- return '//*[@id="' + htmlEl.id + '"]';
616
- const testId = element.getAttribute('data-testid');
617
- if (testId)
618
- return '//*[@data-testid="' + testId + '"]';
619
- const nameAttr = element.getAttribute('name');
620
- if (nameAttr)
621
- return '//' + element.tagName.toLowerCase() + '[@name="' + nameAttr + '"]';
622
- const parts = [];
623
- let current = element;
624
- let depth = 0;
625
- while (current && depth < 5) {
626
- const curHtml = current;
627
- if (curHtml.id) {
628
- parts.unshift('//*[@id="' + curHtml.id + '"]');
629
- break;
630
- }
631
- const testId = current.getAttribute('data-testid');
632
- if (testId) {
633
- parts.unshift('//*[@data-testid="' + testId + '"]');
634
- break;
635
- }
636
- const tagName = current.tagName.toLowerCase();
637
- const parent = current.parentElement;
638
- if (parent) {
639
- const siblings = Array.from(parent.children).filter((c) => c.tagName === current.tagName);
640
- const index = siblings.indexOf(current) + 1;
641
- parts.unshift(tagName + '[' + index + ']');
642
- }
643
- else {
644
- parts.unshift(tagName);
645
- }
646
- current = current.parentElement;
647
- depth++;
648
- }
649
- if (parts.length > 0 && !parts[0].startsWith('//'))
650
- parts.unshift('//');
651
- return parts.join('/');
652
- }
653
- let cssSelector = null;
654
- cssSelector = tryIdSelector(el);
655
- if (!cssSelector)
656
- cssSelector = getMultiAttributeSelector(el);
657
- if (!cssSelector)
658
- cssSelector = getAttributeClassComboSelector(el);
659
- if (!cssSelector)
660
- cssSelector = getBestClassSelector(el);
661
- if (!cssSelector)
662
- cssSelector = getSiblingBasedSelector(el);
663
- if (!cssSelector)
664
- cssSelector = buildComposedSelector(el);
665
- if (!cssSelector)
666
- cssSelector = tryNthChild(el);
667
- if (!cssSelector)
668
- cssSelector = buildUniquePath(el);
669
- if (!cssSelector)
670
- cssSelector = el.tagName.toLowerCase();
671
- const xpath = generateXPath(el);
672
- return { cssSelector, xpath };
673
- })
674
- .catch(() => null);
675
- if (selectorData) {
676
- result[ref] = {
677
- cssSelector: selectorData.cssSelector,
678
- xpath: selectorData.xpath,
679
- };
680
- }
681
- }
682
- catch {
683
- // Intentionally ignored: element ref generation failed for this element
684
- }
685
- }
686
- return result;
687
- }
688
- /**
689
- * Get enhanced snapshot with refs and optional filtering
690
- */
691
- export async function getEnhancedSnapshot(page, options = {}) {
692
- if ((options.path || options.attrs) && !options.selector) {
693
- throw new Error('由于内容可能过大,请使用 selector 参数限定范围');
694
- }
695
- resetRefs();
696
- const refs = {};
697
- const locator = options.selector ? page.locator(options.selector) : page.locator(':root');
698
- let ariaTree;
699
- try {
700
- ariaTree = await locator.ariaSnapshot({ timeout: 2000 });
701
- }
702
- catch (error) {
703
- const errorMessage = error instanceof Error ? error.message : String(error);
704
- if (errorMessage.includes('Timeout') && options.selector) {
705
- const suggestedSelectors = await suggestSelectors(page);
706
- return {
707
- tree: `(no elements found for selector: ${options.selector})\n\nSuggested selectors: ${suggestedSelectors.join(', ')}`,
708
- refs: {},
709
- };
710
- }
711
- throw error;
712
- }
713
- if (!ariaTree) {
714
- return {
715
- tree: '(empty)',
716
- refs: {},
717
- };
718
- }
719
- const enhancedTree = processAriaTree(ariaTree, refs, options);
720
- if (options.cursor) {
721
- const cursorElements = await findCursorInteractiveElements(page, options.selector);
722
- const existingTexts = new Set(Object.values(refs).map((r) => r.name?.toLowerCase()));
723
- const additionalLines = [];
724
- for (const el of cursorElements) {
725
- if (existingTexts.has(el.text.toLowerCase()))
726
- continue;
727
- const ref = nextRef();
728
- const role = el.hasCursorPointer ? 'clickable' : el.hasOnClick ? 'clickable' : 'focusable';
729
- refs[ref] = {
730
- selector: el.selector,
731
- role: role,
732
- name: el.text,
733
- };
734
- const hints = [];
735
- if (el.hasCursorPointer)
736
- hints.push('cursor:pointer');
737
- if (el.hasOnClick)
738
- hints.push('onclick');
739
- if (el.hasTabIndex)
740
- hints.push('tabindex');
741
- additionalLines.push(`- ${role} "${el.text}" [ref=${ref}] [${hints.join(', ')}]`);
742
- }
743
- if (additionalLines.length > 0) {
744
- const separator = enhancedTree === '(no interactive elements)' ? '' : '\n# Cursor-interactive elements:\n';
745
- const base = enhancedTree === '(no interactive elements)' ? '' : enhancedTree;
746
- return {
747
- tree: base + separator + additionalLines.join('\n'),
748
- refs,
749
- };
750
- }
751
- }
752
- if (options.path || options.attrs) {
753
- await enrichRefsWithPathsAndAttrs(page, refs, options);
754
- }
755
- let finalTree = enhancedTree;
756
- if (options.selectors && Object.keys(refs).length > 0) {
757
- const selectorMap = await buildCompactSelectors(page, refs, options);
758
- if (selectorMap) {
759
- finalTree += '\n## Selectors\n' + selectorMap;
760
- }
761
- }
762
- return { tree: finalTree, refs };
763
- }
764
- async function buildCompactSelectors(page, refs, options) {
765
- const entries = Object.entries(refs);
766
- const parts = [];
767
- const includeAll = options?.all ?? false;
768
- for (const [ref, data] of entries) {
769
- if (data.role === 'clickable' || data.role === 'focusable')
770
- continue;
771
- try {
772
- let locator;
773
- if (data.name) {
774
- locator = page.getByRole(data.role, {
775
- name: data.name,
776
- exact: true,
777
- });
778
- }
779
- else {
780
- locator = page.getByRole(data.role);
781
- }
782
- if (data.nth !== undefined)
783
- locator = locator.nth(data.nth);
784
- if (!includeAll) {
785
- const isReallyVisible = await locator
786
- .evaluate((el) => {
787
- const style = getComputedStyle(el);
788
- const rect = el.getBoundingClientRect();
789
- return !(style.display === 'none' ||
790
- style.visibility === 'hidden' ||
791
- parseFloat(style.opacity) === 0 ||
792
- (rect.width === 0 && rect.height === 0) ||
793
- rect.x + rect.width < 0 ||
794
- rect.y + rect.height < 0);
795
- })
796
- .catch(() => false);
797
- if (!isReallyVisible)
798
- continue;
799
- }
800
- const attrs = await locator
801
- .evaluate((el) => {
802
- const htmlEl = el;
803
- const r = {};
804
- if (htmlEl.dataset.testid)
805
- r['testid'] = `[data-testid="${htmlEl.dataset.testid}"]`;
806
- if (htmlEl.id && !htmlEl.id.match(/^[:]/))
807
- r['id'] = '#' + CSS.escape(htmlEl.id);
808
- const nameAttr = htmlEl.getAttribute('name');
809
- if (nameAttr)
810
- r['name'] = `${htmlEl.tagName.toLowerCase()}[name="${nameAttr}"]`;
811
- return r;
812
- })
813
- .catch(() => null);
814
- if (!attrs)
815
- continue;
816
- let bestSelector = '';
817
- if (attrs.testid)
818
- bestSelector = attrs.testid;
819
- else if (attrs.id)
820
- bestSelector = attrs.id;
821
- else if (attrs.name)
822
- bestSelector = attrs.name;
823
- if (bestSelector) {
824
- parts.push(`${ref}: ${bestSelector}`);
825
- }
826
- }
827
- catch {
828
- // skip
829
- }
830
- }
831
- return parts.join(' | ');
832
- }
833
- async function enrichRefsWithPathsAndAttrs(page, refs, options) {
834
- if (Object.keys(refs).length === 0) {
835
- return;
836
- }
837
- void `
838
- () => {
839
- const STYLE_CLASS_PATTERNS = [
840
- /^(flex|grid|block|inline|hidden)$/,
841
- /^(mt|mb|ml|mr|mx|my|pt|pb|pl|pr|px|py)-?\\d*$/,
842
- /^(w|h|min|max)-/,
843
- /^(text|font|bg|border)-/,
844
- /^(rounded|shadow|opacity)-/,
845
- /^(hover|focus|active):/,
846
- /^(items|justify|gap|space)-/,
847
- /^(p|m)-\\d*$/,
848
- /^transition/,
849
- /^duration/,
850
- /^ease/,
851
- /^transform/,
852
- /^scale|rotate|translate/,
853
- ];
854
-
855
- const SEMANTIC_TAGS = new Set([
856
- 'main', 'nav', 'header', 'footer', 'article', 'section', 'aside', 'form'
857
- ]);
858
-
859
- function getSemanticClass(element) {
860
- const className = element.getAttribute('class');
861
- if (!className) return null;
862
- const classes = className.split(/\\s+/).filter(cls => {
863
- return !STYLE_CLASS_PATTERNS.some(p => p.test(cls));
864
- });
865
- if (classes.length === 0) return null;
866
- const selectedClasses = classes.slice(0, 2);
867
- return selectedClasses.map(cls => 'contains(@class, "' + cls + '")').join(' and ');
868
- }
869
-
870
- function getElementIndex(element) {
871
- const parent = element.parentElement;
872
- if (!parent) return 1;
873
- const siblings = Array.from(parent.children).filter(
874
- child => child.tagName === element.tagName
875
- );
876
- return siblings.indexOf(element) + 1;
877
- }
878
-
879
- function buildRelativeXPath(element, maxDepth) {
880
- const path = [];
881
- let current = element;
882
- let depth = 0;
883
- while (current && depth < maxDepth) {
884
- if (current.id) {
885
- path.unshift('//*[@id="' + current.id + '"]');
886
- break;
887
- }
888
- const testId = current.getAttribute('data-testid');
889
- if (testId) {
890
- path.unshift('//*[@data-testid="' + testId + '"]');
891
- break;
892
- }
893
- const tagName = current.tagName.toLowerCase();
894
- if (SEMANTIC_TAGS.has(tagName)) {
895
- const index = getElementIndex(current);
896
- path.unshift('//' + tagName + '[' + index + ']');
897
- break;
898
- }
899
- const index = getElementIndex(current);
900
- path.unshift(tagName + '[' + index + ']');
901
- current = current.parentElement;
902
- depth++;
903
- }
904
- if (path.length > 0 && !path[0].startsWith('//')) {
905
- path.unshift('//');
906
- }
907
- return path.join('/');
908
- }
909
-
910
- function generateXPath(element, maxDepth) {
911
- if (element.id) return '//*[@id="' + element.id + '"]';
912
- const testId = element.getAttribute('data-testid');
913
- if (testId) return '//*[@data-testid="' + testId + '"]';
914
- const dataId = element.getAttribute('data-id');
915
- if (dataId) return '//*[@data-id="' + dataId + '"]';
916
- const semanticClass = getSemanticClass(element);
917
- if (semanticClass) return '//' + element.tagName.toLowerCase() + '[' + semanticClass + ']';
918
- return buildRelativeXPath(element, maxDepth);
919
- }
920
-
921
- function buildElementSelector(element) {
922
- const tagName = element.tagName.toLowerCase();
923
- const className = element.getAttribute('class');
924
- if (className) {
925
- const classes = className.split(/\\s+/).filter(cls => {
926
- return !STYLE_CLASS_PATTERNS.some(p => p.test(cls));
927
- });
928
- if (classes.length > 0) {
929
- return tagName + '.' + classes.slice(0, 2).join('.');
930
- }
931
- }
932
- const parent = element.parentElement;
933
- if (parent) {
934
- const index = Array.from(parent.children).indexOf(element) + 1;
935
- return tagName + ':nth-child(' + index + ')';
936
- }
937
- return tagName;
938
- }
939
-
940
- function generateCSSPath(element, maxDepth) {
941
- if (element.id) return '#' + element.id;
942
- const testId = element.getAttribute('data-testid');
943
- if (testId) return '[data-testid="' + testId + '"]';
944
- const path = [];
945
- let current = element;
946
- let depth = 0;
947
- while (current && depth < maxDepth) {
948
- if (current.id) {
949
- path.unshift('#' + current.id);
950
- break;
951
- }
952
- const testId = current.getAttribute('data-testid');
953
- if (testId) {
954
- path.unshift('[data-testid="' + testId + '"]');
955
- break;
956
- }
957
- const tagName = current.tagName.toLowerCase();
958
- if (SEMANTIC_TAGS.has(tagName)) {
959
- path.unshift(tagName);
960
- break;
961
- }
962
- const selector = buildElementSelector(current);
963
- path.unshift(selector);
964
- current = current.parentElement;
965
- depth++;
966
- }
967
- return path.join(' > ');
968
- }
969
-
970
- function collectAttributes(element) {
971
- const attrs = {};
972
- for (let i = 0; i < element.attributes.length; i++) {
973
- const attr = element.attributes[i];
974
- attrs[attr.name] = attr.value;
975
- }
976
- return attrs;
977
- }
978
-
979
- // Map role to implicit tag names
980
- const roleToTag = {
981
- 'heading': ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
982
- 'link': ['a'],
983
- 'button': ['button', 'input[type="button"]', 'input[type="submit"]'],
984
- 'textbox': ['input[type="text"]', 'input:not([type])', 'textarea'],
985
- 'checkbox': ['input[type="checkbox"]'],
986
- 'radio': ['input[type="radio"]'],
987
- 'listitem': ['li'],
988
- 'list': ['ul', 'ol'],
989
- 'navigation': ['nav'],
990
- 'main': ['main'],
991
- 'article': ['article'],
992
- 'section': ['section'],
993
- 'form': ['form'],
994
- };
995
-
996
- function getImplicitRole(element) {
997
- const tag = element.tagName.toLowerCase();
998
- const type = element.getAttribute('type');
999
-
1000
- // Check explicit role first
1001
- const explicitRole = element.getAttribute('role');
1002
- if (explicitRole) return explicitRole.toLowerCase();
1003
-
1004
- // Check implicit roles
1005
- if (tag === 'a' && element.hasAttribute('href')) return 'link';
1006
- if (tag === 'button') return 'button';
1007
- if (tag === 'input') {
1008
- if (type === 'button' || type === 'submit' || type === 'reset') return 'button';
1009
- if (type === 'checkbox') return 'checkbox';
1010
- if (type === 'radio') return 'radio';
1011
- if (type === 'file') return 'button'; // file input is rendered as button
1012
- if (type === 'range') return 'slider';
1013
- if (type === 'number') return 'spinbutton';
1014
- if (type === 'date' || type === 'datetime-local' || type === 'month' || type === 'week' || type === 'time') return 'textbox';
1015
- return 'textbox';
1016
- }
1017
- if (tag === 'textarea') return 'textbox';
1018
- if (tag === 'select') return 'combobox';
1019
- if (tag === 'img' && element.hasAttribute('alt')) return 'img';
1020
- if (/^h[1-6]$/.test(tag)) return 'heading';
1021
- if (tag === 'nav') return 'navigation';
1022
- if (tag === 'main') return 'main';
1023
- if (tag === 'article') return 'article';
1024
- if (tag === 'section') return 'section';
1025
- if (tag === 'form') return 'form';
1026
- if (tag === 'ul' || tag === 'ol') return 'list';
1027
- if (tag === 'li') return 'listitem';
1028
-
1029
- return null;
1030
- }
1031
-
1032
- const results = {};
1033
- const refEntries = Object.entries(window.__AGENT_BROWSER_REFS__ || {});
1034
-
1035
- for (const [ref, data] of refEntries) {
1036
- const targetRole = data.role;
1037
- const targetName = data.name;
1038
- const nth = data.nth;
1039
-
1040
- // Find matching elements
1041
- let elements = [];
1042
- const allElements = document.querySelectorAll('*');
1043
-
1044
- for (const el of allElements) {
1045
- const elRole = getImplicitRole(el);
1046
- if (elRole !== targetRole && targetRole !== 'clickable' && targetRole !== 'focusable') continue;
1047
-
1048
- // Get accessible name
1049
- let elName = el.getAttribute('aria-label') ||
1050
- el.getAttribute('title') ||
1051
- el.getAttribute('alt') || '';
1052
-
1053
- if (!elName) {
1054
- // For heading, use text content
1055
- if (targetRole === 'heading') {
1056
- elName = el.textContent?.trim() || '';
1057
- }
1058
- // For link, use text content
1059
- else if (targetRole === 'link' || el.tagName.toLowerCase() === 'a') {
1060
- elName = el.textContent?.trim() || '';
1061
- // Also check img alt inside link
1062
- if (!elName) {
1063
- const img = el.querySelector('img[alt]');
1064
- if (img) elName = img.getAttribute('alt') || '';
1065
- }
1066
- }
1067
- // For button, use text or value
1068
- else if (targetRole === 'button') {
1069
- elName = el.textContent?.trim() || el.getAttribute('value') || '';
1070
- // Special case for file input - Playwright shows "Choose File" but element has no name
1071
- if (!elName && el.tagName.toLowerCase() === 'input' && el.getAttribute('type') === 'file') {
1072
- elName = 'choose file'; // Match Playwright's localized name
1073
- }
1074
- }
1075
- // For textbox, use label or placeholder
1076
- else if (targetRole === 'textbox') {
1077
- elName = el.getAttribute('placeholder') || '';
1078
- const label = el.labels?.[0]?.textContent?.trim();
1079
- if (label) elName = label;
1080
- }
1081
- else {
1082
- elName = el.textContent?.trim().slice(0, 100) || '';
1083
- }
1084
- }
1085
-
1086
- // Match name
1087
- if (targetName) {
1088
- const normalizedElName = elName.toLowerCase().trim();
1089
- const normalizedTargetName = targetName.toLowerCase().trim();
1090
- if (normalizedElName !== normalizedTargetName && !normalizedElName.includes(normalizedTargetName)) {
1091
- continue;
1092
- }
1093
- }
1094
-
1095
- elements.push(el);
1096
- }
1097
-
1098
- if (elements.length > 0) {
1099
- const element = nth !== undefined ? elements[nth] : elements[0];
1100
- if (element) {
1101
- results[ref] = {
1102
- xpath: generateXPath(element, 5),
1103
- cssPath: generateCSSPath(element, 5),
1104
- attributes: collectAttributes(element),
1105
- };
1106
- }
1107
- }
1108
- }
1109
-
1110
- return results;
1111
- }
1112
- `;
1113
- const injectScript = `
1114
- window.__AGENT_BROWSER_REFS__ = ${JSON.stringify(refs)};
1115
- `;
1116
- if ('evaluate' in page) {
1117
- await page.evaluate(injectScript);
1118
- }
1119
- // Evaluate the function in the browser context
1120
- const elementData = await page.evaluate(() => {
1121
- const STYLE_CLASS_PATTERNS = [
1122
- /^(flex|grid|block|inline|hidden)$/,
1123
- /^(mt|mb|ml|mr|mx|my|pt|pb|pl|pr|px|py)-?\d*$/,
1124
- /^(w|h|min|max)-/,
1125
- /^(text|font|bg|border)-/,
1126
- /^(rounded|shadow|opacity)-/,
1127
- /^(hover|focus|active):/,
1128
- /^(items|justify|gap|space)-/,
1129
- /^(p|m)-\d*$/,
1130
- /^transition/,
1131
- /^duration/,
1132
- /^ease/,
1133
- /^transform/,
1134
- /^scale|rotate|translate/,
1135
- ];
1136
- const SEMANTIC_TAGS = new Set([
1137
- 'main',
1138
- 'nav',
1139
- 'header',
1140
- 'footer',
1141
- 'article',
1142
- 'section',
1143
- 'aside',
1144
- 'form',
1145
- ]);
1146
- function getSemanticClass(element) {
1147
- const className = element.getAttribute('class');
1148
- if (!className)
1149
- return null;
1150
- const classes = className.split(/\s+/).filter((cls) => {
1151
- return !STYLE_CLASS_PATTERNS.some((p) => p.test(cls));
1152
- });
1153
- if (classes.length === 0)
1154
- return null;
1155
- const selectedClasses = classes.slice(0, 2);
1156
- return selectedClasses.map((cls) => 'contains(@class, "' + cls + '")').join(' and ');
1157
- }
1158
- function getElementIndex(element) {
1159
- const parent = element.parentElement;
1160
- if (!parent)
1161
- return 1;
1162
- const siblings = Array.from(parent.children).filter((child) => child.tagName === element.tagName);
1163
- return siblings.indexOf(element) + 1;
1164
- }
1165
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1166
- function buildRelativeXPath(element, maxDepth) {
1167
- const path = [];
1168
- let current = element;
1169
- let depth = 0;
1170
- while (current && depth < maxDepth) {
1171
- if (current.id) {
1172
- path.unshift('//*[@id="' + current.id + '"]');
1173
- break;
1174
- }
1175
- const testId = current.getAttribute('data-testid');
1176
- if (testId) {
1177
- path.unshift('//*[@data-testid="' + testId + '"]');
1178
- break;
1179
- }
1180
- const tagName = current.tagName.toLowerCase();
1181
- if (SEMANTIC_TAGS.has(tagName)) {
1182
- const index = getElementIndex(current);
1183
- path.unshift('//' + tagName + '[' + index + ']');
1184
- break;
1185
- }
1186
- const index = getElementIndex(current);
1187
- path.unshift(tagName + '[' + index + ']');
1188
- current = current.parentElement;
1189
- depth++;
1190
- }
1191
- if (path.length > 0 && !path[0].startsWith('//')) {
1192
- path.unshift('//');
1193
- }
1194
- return path.join('/');
1195
- }
1196
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1197
- function generateXPath(element, maxDepth) {
1198
- if (element.id)
1199
- return '//*[@id="' + element.id + '"]';
1200
- const testId = element.getAttribute('data-testid');
1201
- if (testId)
1202
- return '//*[@data-testid="' + testId + '"]';
1203
- const dataId = element.getAttribute('data-id');
1204
- if (dataId)
1205
- return '//*[@data-id="' + dataId + '"]';
1206
- const semanticClass = getSemanticClass(element);
1207
- if (semanticClass)
1208
- return '//' + element.tagName.toLowerCase() + '[' + semanticClass + ']';
1209
- return buildRelativeXPath(element, maxDepth);
1210
- }
1211
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1212
- function buildElementSelector(element) {
1213
- const tagName = element.tagName.toLowerCase();
1214
- const className = element.getAttribute('class');
1215
- if (className) {
1216
- const classes = className.split(/\s+/).filter((cls) => {
1217
- return !STYLE_CLASS_PATTERNS.some((p) => p.test(cls));
1218
- });
1219
- if (classes.length > 0) {
1220
- return tagName + '.' + classes.slice(0, 2).join('.');
1221
- }
1222
- }
1223
- const parent = element.parentElement;
1224
- if (parent) {
1225
- const index = Array.from(parent.children).indexOf(element) + 1;
1226
- return tagName + ':nth-child(' + index + ')';
1227
- }
1228
- return tagName;
1229
- }
1230
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1231
- function generateCSSPath(element, maxDepth) {
1232
- if (element.id)
1233
- return '#' + element.id;
1234
- const testId = element.getAttribute('data-testid');
1235
- if (testId)
1236
- return '[data-testid="' + testId + '"]';
1237
- const path = [];
1238
- let current = element;
1239
- let depth = 0;
1240
- while (current && depth < maxDepth) {
1241
- if (current.id) {
1242
- path.unshift('#' + current.id);
1243
- break;
1244
- }
1245
- const testId = current.getAttribute('data-testid');
1246
- if (testId) {
1247
- path.unshift('[data-testid="' + testId + '"]');
1248
- break;
1249
- }
1250
- const tagName = current.tagName.toLowerCase();
1251
- if (SEMANTIC_TAGS.has(tagName)) {
1252
- path.unshift(tagName);
1253
- break;
1254
- }
1255
- const selector = buildElementSelector(current);
1256
- path.unshift(selector);
1257
- current = current.parentElement;
1258
- depth++;
1259
- }
1260
- return path.join(' > ');
1261
- }
1262
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1263
- function collectAttributes(element) {
1264
- const attrs = {};
1265
- for (let i = 0; i < element.attributes.length; i++) {
1266
- const attr = element.attributes[i];
1267
- attrs[attr.name] = attr.value;
1268
- }
1269
- return attrs;
1270
- }
1271
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
1272
- function getImplicitRole(element) {
1273
- const tag = element.tagName.toLowerCase();
1274
- const type = element.getAttribute('type');
1275
- const explicitRole = element.getAttribute('role');
1276
- if (explicitRole)
1277
- return explicitRole.toLowerCase();
1278
- if (tag === 'a' && element.hasAttribute('href'))
1279
- return 'link';
1280
- if (tag === 'button')
1281
- return 'button';
1282
- if (tag === 'input') {
1283
- if (type === 'button' || type === 'submit' || type === 'reset')
1284
- return 'button';
1285
- if (type === 'checkbox')
1286
- return 'checkbox';
1287
- if (type === 'radio')
1288
- return 'radio';
1289
- if (type === 'file')
1290
- return 'button'; // file input is rendered as button
1291
- if (type === 'range')
1292
- return 'slider';
1293
- if (type === 'number')
1294
- return 'spinbutton';
1295
- return 'textbox';
1296
- }
1297
- if (tag === 'textarea')
1298
- return 'textbox';
1299
- if (tag === 'select')
1300
- return 'combobox';
1301
- if (tag === 'img' && element.hasAttribute('alt'))
1302
- return 'img';
1303
- if (/^h[1-6]$/.test(tag))
1304
- return 'heading';
1305
- if (tag === 'nav')
1306
- return 'navigation';
1307
- if (tag === 'main')
1308
- return 'main';
1309
- if (tag === 'article')
1310
- return 'article';
1311
- if (tag === 'section')
1312
- return 'section';
1313
- if (tag === 'form')
1314
- return 'form';
1315
- if (tag === 'ul' || tag === 'ol')
1316
- return 'list';
1317
- if (tag === 'li')
1318
- return 'listitem';
1319
- return null;
1320
- }
1321
- const results = {};
1322
- const refEntries = Object.entries(window.__AGENT_BROWSER_REFS__ || {});
1323
- for (const [ref, data] of refEntries) {
1324
- const targetRole = data.role;
1325
- const targetName = data.name;
1326
- const nth = data.nth;
1327
- const elements = [];
1328
- const allElements = Array.from(document.querySelectorAll('*'));
1329
- for (const el of allElements) {
1330
- const elRole = getImplicitRole(el);
1331
- if (elRole !== targetRole && targetRole !== 'clickable' && targetRole !== 'focusable')
1332
- continue;
1333
- let elName = el.getAttribute('aria-label') || el.getAttribute('title') || el.getAttribute('alt') || '';
1334
- if (!elName) {
1335
- if (targetRole === 'heading') {
1336
- elName = el.textContent?.trim() || '';
1337
- }
1338
- else if (targetRole === 'link' || el.tagName.toLowerCase() === 'a') {
1339
- elName = el.textContent?.trim() || '';
1340
- if (!elName) {
1341
- const img = el.querySelector('img[alt]');
1342
- if (img)
1343
- elName = img.getAttribute('alt') || '';
1344
- }
1345
- }
1346
- else if (targetRole === 'button') {
1347
- elName = el.textContent?.trim() || el.getAttribute('value') || '';
1348
- // Special case for file input - Playwright shows "Choose File" but element has no name
1349
- if (!elName &&
1350
- el.tagName.toLowerCase() === 'input' &&
1351
- el.getAttribute('type') === 'file') {
1352
- elName = 'choose file'; // Match Playwright's localized name
1353
- }
1354
- }
1355
- else if (targetRole === 'textbox') {
1356
- elName = el.getAttribute('placeholder') || '';
1357
- const label = el.labels?.[0]?.textContent?.trim();
1358
- if (label)
1359
- elName = label;
1360
- }
1361
- else {
1362
- elName = el.textContent?.trim().slice(0, 100) || '';
1363
- }
1364
- }
1365
- if (targetName) {
1366
- const normalizedElName = elName.toLowerCase().trim();
1367
- const normalizedTargetName = targetName.toLowerCase().trim();
1368
- if (normalizedElName !== normalizedTargetName &&
1369
- !normalizedElName.includes(normalizedTargetName)) {
1370
- continue;
1371
- }
1372
- }
1373
- elements.push(el);
1374
- }
1375
- if (elements.length > 0) {
1376
- const element = nth !== undefined ? elements[nth] : elements[0];
1377
- if (element) {
1378
- results[ref] = {
1379
- xpath: generateXPath(element, 5),
1380
- cssPath: generateCSSPath(element, 5),
1381
- attributes: collectAttributes(element),
1382
- };
1383
- }
1384
- }
1385
- }
1386
- return results;
1387
- });
1388
- if (!elementData || typeof elementData !== 'object') {
1389
- return;
1390
- }
1391
- for (const [ref, data] of Object.entries(elementData)) {
1392
- const typedData = data;
1393
- if (refs[ref] && data) {
1394
- if (options.path) {
1395
- refs[ref].xpath = typedData.xpath;
1396
- refs[ref].cssPath = typedData.cssPath;
1397
- }
1398
- if (options.attrs) {
1399
- refs[ref].attributes = typedData.attributes;
1400
- }
1401
- }
1402
- }
1403
- }
1404
- function createRoleNameTracker() {
1405
- const counts = new Map();
1406
- const refsByKey = new Map();
1407
- return {
1408
- counts,
1409
- refsByKey,
1410
- getKey(role, name) {
1411
- return `${role}:${name ?? ''}`;
1412
- },
1413
- getNextIndex(role, name) {
1414
- const key = this.getKey(role, name);
1415
- const current = counts.get(key) ?? 0;
1416
- counts.set(key, current + 1);
1417
- return current;
1418
- },
1419
- trackRef(role, name, ref) {
1420
- const key = this.getKey(role, name);
1421
- const refs = refsByKey.get(key) ?? [];
1422
- refs.push(ref);
1423
- refsByKey.set(key, refs);
1424
- },
1425
- getDuplicateKeys() {
1426
- const duplicates = new Set();
1427
- for (const [key, refs] of refsByKey) {
1428
- if (refs.length > 1) {
1429
- duplicates.add(key);
1430
- }
1431
- }
1432
- return duplicates;
1433
- },
1434
- };
1435
- }
1436
- /**
1437
- * Process ARIA snapshot: add refs and apply filters
1438
- */
1439
- function processAriaTree(ariaTree, refs, options) {
1440
- const lines = ariaTree.split('\n');
1441
- const result = [];
1442
- const tracker = createRoleNameTracker();
1443
- // For interactive-only mode, we collect just interactive elements
1444
- if (options.interactive) {
1445
- for (const line of lines) {
1446
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
1447
- if (!match)
1448
- continue;
1449
- const [, , role, name, suffix] = match;
1450
- const roleLower = role.toLowerCase();
1451
- if (INTERACTIVE_ROLES.has(roleLower)) {
1452
- const ref = nextRef();
1453
- const nth = tracker.getNextIndex(roleLower, name);
1454
- tracker.trackRef(roleLower, name, ref);
1455
- refs[ref] = {
1456
- selector: buildSelector(roleLower, name),
1457
- role: roleLower,
1458
- name,
1459
- nth, // Always store nth, we'll use it for duplicates
1460
- };
1461
- let enhanced = `- ${role}`;
1462
- if (name)
1463
- enhanced += ` "${name}"`;
1464
- enhanced += ` [ref=${ref}]`;
1465
- // Only show nth in output if it's > 0 (for readability)
1466
- if (nth > 0)
1467
- enhanced += ` [nth=${nth}]`;
1468
- if (suffix && suffix.includes('['))
1469
- enhanced += suffix;
1470
- result.push(enhanced);
1471
- }
1472
- }
1473
- // Post-process: remove nth from refs that don't have duplicates
1474
- removeNthFromNonDuplicates(refs, tracker);
1475
- return result.join('\n') || '(no interactive elements)';
1476
- }
1477
- // Normal processing with depth/compact filters
1478
- for (const line of lines) {
1479
- const processed = processLine(line, refs, options, tracker);
1480
- if (processed !== null) {
1481
- result.push(processed);
1482
- }
1483
- }
1484
- // Post-process: remove nth from refs that don't have duplicates
1485
- removeNthFromNonDuplicates(refs, tracker);
1486
- // If compact mode, remove empty structural elements
1487
- if (options.compact) {
1488
- return compactTree(result.join('\n'));
1489
- }
1490
- return result.join('\n');
1491
- }
1492
- /**
1493
- * Remove nth from refs that ended up not having duplicates
1494
- * This keeps single-element locators simple (no unnecessary .nth(0))
1495
- */
1496
- function removeNthFromNonDuplicates(refs, tracker) {
1497
- const duplicateKeys = tracker.getDuplicateKeys();
1498
- for (const [ref, data] of Object.entries(refs)) {
1499
- const key = tracker.getKey(data.role, data.name);
1500
- if (!duplicateKeys.has(key)) {
1501
- // Not a duplicate, remove nth to keep locator simple
1502
- delete refs[ref].nth;
1503
- }
1504
- }
1505
- }
1506
- /**
1507
- * Get indentation level (number of spaces / 2)
1508
- */
1509
- function getIndentLevel(line) {
1510
- const match = line.match(/^(\s*)/);
1511
- return match ? Math.floor(match[1].length / 2) : 0;
1512
- }
1513
- /**
1514
- * Process a single line: add ref if needed, filter if requested
1515
- */
1516
- function processLine(line, refs, options, tracker) {
1517
- const depth = getIndentLevel(line);
1518
- // Check max depth
1519
- if (options.maxDepth !== undefined && depth > options.maxDepth) {
1520
- return null;
1521
- }
1522
- // Match lines like:
1523
- // - button "Submit"
1524
- // - heading "Title" [level=1]
1525
- // - link "Click me":
1526
- const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
1527
- if (!match) {
1528
- // Metadata lines (like /url:) or text content
1529
- if (options.interactive) {
1530
- // In interactive mode, only keep metadata under interactive elements
1531
- return null;
1532
- }
1533
- return line;
1534
- }
1535
- const [, prefix, role, name, suffix] = match;
1536
- const roleLower = role.toLowerCase();
1537
- // Skip metadata lines (like /url:)
1538
- if (role.startsWith('/')) {
1539
- return line;
1540
- }
1541
- const isInteractive = INTERACTIVE_ROLES.has(roleLower);
1542
- const isContent = CONTENT_ROLES.has(roleLower);
1543
- const isStructural = STRUCTURAL_ROLES.has(roleLower);
1544
- // In interactive-only mode, filter non-interactive elements
1545
- if (options.interactive && !isInteractive) {
1546
- return null;
1547
- }
1548
- // In compact mode, skip unnamed structural elements
1549
- if (options.compact && isStructural && !name) {
1550
- return null;
1551
- }
1552
- // Add ref for interactive or named content elements
1553
- const shouldHaveRef = isInteractive || (isContent && name);
1554
- if (shouldHaveRef) {
1555
- const ref = nextRef();
1556
- const nth = tracker.getNextIndex(roleLower, name);
1557
- tracker.trackRef(roleLower, name, ref);
1558
- refs[ref] = {
1559
- selector: buildSelector(roleLower, name),
1560
- role: roleLower,
1561
- name,
1562
- nth, // Always store nth, we'll clean up non-duplicates later
1563
- };
1564
- // Build enhanced line with ref
1565
- let enhanced = `${prefix}${role}`;
1566
- if (name)
1567
- enhanced += ` "${name}"`;
1568
- enhanced += ` [ref=${ref}]`;
1569
- // Only show nth in output if it's > 0 (for readability)
1570
- if (nth > 0)
1571
- enhanced += ` [nth=${nth}]`;
1572
- if (suffix)
1573
- enhanced += suffix;
1574
- return enhanced;
1575
- }
1576
- return line;
1577
- }
1578
- /**
1579
- * Remove empty structural branches in compact mode
1580
- */
1581
- function compactTree(tree) {
1582
- const lines = tree.split('\n');
1583
- const result = [];
1584
- // Simple pass: keep lines that have content or refs
1585
- for (let i = 0; i < lines.length; i++) {
1586
- const line = lines[i];
1587
- // Always keep lines with refs
1588
- if (line.includes('[ref=')) {
1589
- result.push(line);
1590
- continue;
1591
- }
1592
- // Keep lines with text content (after :)
1593
- if (line.includes(':') && !line.endsWith(':')) {
1594
- result.push(line);
1595
- continue;
1596
- }
1597
- // Check if this structural element has children with refs
1598
- const currentIndent = getIndentLevel(line);
1599
- let hasRelevantChildren = false;
1600
- for (let j = i + 1; j < lines.length; j++) {
1601
- const childIndent = getIndentLevel(lines[j]);
1602
- if (childIndent <= currentIndent)
1603
- break;
1604
- if (lines[j].includes('[ref=')) {
1605
- hasRelevantChildren = true;
1606
- break;
1607
- }
1608
- }
1609
- if (hasRelevantChildren) {
1610
- result.push(line);
1611
- }
1612
- }
1613
- return result.join('\n');
1614
- }
1615
- /**
1616
- * Parse a ref from command argument (e.g., "@e1" -> "e1")
1617
- */
1618
- export function parseRef(arg) {
1619
- if (arg.startsWith('@')) {
1620
- return arg.slice(1);
1621
- }
1622
- if (arg.startsWith('[ref=') && arg.endsWith(']')) {
1623
- return arg.slice(5, -1);
1624
- }
1625
- if (arg.startsWith('ref=')) {
1626
- return arg.slice(4);
1627
- }
1628
- if (/^e\d+$/.test(arg)) {
1629
- return arg;
1630
- }
1631
- return null;
1632
- }
1633
- /**
1634
- * Get snapshot statistics
1635
- */
1636
- export function getSnapshotStats(tree, refs) {
1637
- const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
1638
- return {
1639
- lines: tree.split('\n').length,
1640
- chars: tree.length,
1641
- tokens: Math.ceil(tree.length / 4),
1642
- refs: Object.keys(refs).length,
1643
- interactive,
1644
- };
1645
- }
1646
- const STYLE_CLASS_PATTERNS = [
1647
- /^(flex|grid|block|inline|hidden)$/,
1648
- /^(mt|mb|ml|mr|mx|my|pt|pb|pl|pr|px|py)-?\d*$/,
1649
- /^(w|h|min|max)-/,
1650
- /^(text|font|bg|border)-/,
1651
- /^(rounded|shadow|opacity)-/,
1652
- /^(hover|focus|active):/,
1653
- /^(items|justify|gap|space)-/,
1654
- /^(p|m)-\d*$/,
1655
- /^transition/,
1656
- /^duration/,
1657
- /^ease/,
1658
- /^transform/,
1659
- /^scale|rotate|translate/,
1660
- ];
1661
- const SEMANTIC_TAGS = new Set([
1662
- 'main',
1663
- 'nav',
1664
- 'header',
1665
- 'footer',
1666
- 'article',
1667
- 'section',
1668
- 'aside',
1669
- 'form',
1670
- ]);
1671
- export function getSemanticClass(element) {
1672
- const className = element.getAttribute('class');
1673
- if (!className)
1674
- return null;
1675
- const classes = className.split(/\s+/).filter((cls) => {
1676
- return !STYLE_CLASS_PATTERNS.some((p) => p.test(cls));
1677
- });
1678
- if (classes.length === 0)
1679
- return null;
1680
- const selectedClasses = classes.slice(0, 2);
1681
- return selectedClasses.map((cls) => `contains(@class, "${cls}")`).join(' and ');
1682
- }
1683
- export function generateXPath(element, maxDepth = 5) {
1684
- const elemId = element.id;
1685
- if (elemId) {
1686
- return `//*[@id="${elemId}"]`;
1687
- }
1688
- const testId = element.getAttribute('data-testid');
1689
- if (testId) {
1690
- return `//*[@data-testid="${testId}"]`;
1691
- }
1692
- const dataId = element.getAttribute('data-id');
1693
- if (dataId) {
1694
- return `//*[@data-id="${dataId}"]`;
1695
- }
1696
- const semanticClass = getSemanticClass(element);
1697
- if (semanticClass) {
1698
- return `//${element.tagName.toLowerCase()}[${semanticClass}]`;
1699
- }
1700
- return buildRelativeXPath(element, maxDepth);
1701
- }
1702
- function buildRelativeXPath(element, maxDepth) {
1703
- const path = [];
1704
- let current = element;
1705
- let depth = 0;
1706
- while (current && depth < maxDepth) {
1707
- const currentId = current.id;
1708
- if (currentId) {
1709
- path.unshift(`//*[@id="${currentId}"]`);
1710
- break;
1711
- }
1712
- const testId = current.getAttribute('data-testid');
1713
- if (testId) {
1714
- path.unshift(`//*[@data-testid="${testId}"]`);
1715
- break;
1716
- }
1717
- const tagName = current.tagName.toLowerCase();
1718
- if (SEMANTIC_TAGS.has(tagName)) {
1719
- const index = getElementIndex(current);
1720
- path.unshift(`//${tagName}[${index}]`);
1721
- break;
1722
- }
1723
- const index = getElementIndex(current);
1724
- path.unshift(`${tagName}[${index}]`);
1725
- current = current.parentElement;
1726
- depth++;
1727
- }
1728
- if (path.length > 0 && !path[0].startsWith('//')) {
1729
- path.unshift('//');
1730
- }
1731
- return path.join('/');
1732
- }
1733
- function getElementIndex(element) {
1734
- const parent = element.parentElement;
1735
- if (!parent)
1736
- return 1;
1737
- const siblings = Array.from(parent.children).filter((child) => child.tagName === element.tagName);
1738
- return siblings.indexOf(element) + 1;
1739
- }
1740
- export function generateCSSPath(element, maxDepth = 5) {
1741
- const elemId = element.id;
1742
- if (elemId) {
1743
- return `#${elemId}`;
1744
- }
1745
- const testId = element.getAttribute('data-testid');
1746
- if (testId) {
1747
- return `[data-testid="${testId}"]`;
1748
- }
1749
- const path = [];
1750
- let current = element;
1751
- let depth = 0;
1752
- while (current && depth < maxDepth) {
1753
- const currentId = current.id;
1754
- if (currentId) {
1755
- path.unshift(`#${currentId}`);
1756
- break;
1757
- }
1758
- const testId = current.getAttribute('data-testid');
1759
- if (testId) {
1760
- path.unshift(`[data-testid="${testId}"]`);
1761
- break;
1762
- }
1763
- const tagName = current.tagName.toLowerCase();
1764
- if (SEMANTIC_TAGS.has(tagName)) {
1765
- path.unshift(tagName);
1766
- break;
1767
- }
1768
- const selector = buildElementSelector(current);
1769
- path.unshift(selector);
1770
- current = current.parentElement;
1771
- depth++;
1772
- }
1773
- return path.join(' > ');
1774
- }
1775
- function buildElementSelector(element) {
1776
- const tagName = element.tagName.toLowerCase();
1777
- const className = element.getAttribute('class');
1778
- if (className) {
1779
- const classes = className.split(/\s+/).filter((cls) => {
1780
- return !STYLE_CLASS_PATTERNS.some((p) => p.test(cls));
1781
- });
1782
- if (classes.length > 0) {
1783
- return `${tagName}.${classes.slice(0, 2).join('.')}`;
1784
- }
1785
- }
1786
- const parent = element.parentElement;
1787
- if (parent) {
1788
- const index = Array.from(parent.children).indexOf(element) + 1;
1789
- return `${tagName}:nth-child(${index})`;
1790
- }
1791
- return tagName;
1792
- }
1793
- export function collectAttributes(element) {
1794
- const attrs = {};
1795
- for (let i = 0; i < element.attributes.length; i++) {
1796
- const attr = element.attributes[i];
1797
- attrs[attr.name] = attr.value;
1798
- }
1799
- return attrs;
1800
- }
1
+ export * from './snapshot/index.js';
1801
2
  //# sourceMappingURL=snapshot.js.map