@wdio/nightwatch-devtools 1.2.0 → 1.3.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/dist/index.js CHANGED
@@ -17,6 +17,1894 @@ import {
17
17
  // src/index.ts
18
18
  import { fileURLToPath } from "url";
19
19
 
20
+ // ../core/src/action-mapping.ts
21
+ var ACTION_MAP = {
22
+ // WDIO browser-level
23
+ url: { class: "Page", method: "navigate" },
24
+ navigateTo: { class: "Page", method: "navigate" },
25
+ back: { class: "Page", method: "goBack" },
26
+ forward: { class: "Page", method: "goForward" },
27
+ refresh: { class: "Page", method: "reload" },
28
+ newWindow: { class: "Page", method: "goto" },
29
+ // Selenium WebDriver navigation (driver.get, driver.navigate().to/back/forward/refresh)
30
+ get: { class: "Page", method: "navigate" },
31
+ to: { class: "Page", method: "navigate" },
32
+ // WDIO element-level
33
+ click: { class: "Element", method: "click" },
34
+ doubleClick: { class: "Element", method: "dblclick" },
35
+ setValue: { class: "Element", method: "fill" },
36
+ selectByVisibleText: { class: "Element", method: "selectOption" },
37
+ moveTo: { class: "Element", method: "hover" },
38
+ scrollIntoView: { class: "Element", method: "scrollIntoViewIfNeeded" },
39
+ dragAndDrop: { class: "Element", method: "dragTo" },
40
+ // Selenium WebElement actions
41
+ sendKeys: { class: "Element", method: "fill" },
42
+ clear: { class: "Element", method: "clear" },
43
+ submit: { class: "Element", method: "submit" },
44
+ // Cross-runner
45
+ keys: { class: "Keyboard", method: "press" },
46
+ execute: { class: "Page", method: "evaluate" },
47
+ executeAsync: { class: "Page", method: "evaluate" },
48
+ switchToFrame: { class: "Frame", method: "goto" },
49
+ touchAction: { class: "Element", method: "tap" }
50
+ };
51
+ function mapCommandToAction(command) {
52
+ return ACTION_MAP[command] ?? null;
53
+ }
54
+ function formatActionTitle(action, args, params) {
55
+ const firstArg = args[0] ?? params?.selector;
56
+ if (firstArg === void 0) {
57
+ return `${action.class}.${action.method}()`;
58
+ }
59
+ const label = typeof firstArg === "object" ? JSON.stringify(firstArg) : String(firstArg);
60
+ return `${action.class}.${action.method}("${label}")`;
61
+ }
62
+ var FILL_METHODS = /* @__PURE__ */ new Set(["fill", "selectOption"]);
63
+
64
+ // ../core/src/element-scripts.ts
65
+ function accessibilityTreeScript(inViewportOnly) {
66
+ return `(function () {
67
+ var INPUT_TYPE_ROLES = {
68
+ text: 'textbox', search: 'searchbox', email: 'textbox', url: 'textbox',
69
+ tel: 'textbox', password: 'textbox', number: 'spinbutton',
70
+ checkbox: 'checkbox', radio: 'radio', range: 'slider',
71
+ submit: 'button', reset: 'button', image: 'button', file: 'button', color: 'button'
72
+ }
73
+
74
+ var CONTAINER_ROLES = new Set([
75
+ 'navigation', 'banner', 'contentinfo', 'complementary', 'main',
76
+ 'form', 'region', 'group', 'list', 'listitem', 'table', 'row', 'rowgroup', 'generic'
77
+ ])
78
+
79
+ function getRole(el) {
80
+ var explicit = el.getAttribute('role')
81
+ if (explicit) { return explicit.split(' ')[0] }
82
+ var tag = el.tagName.toLowerCase()
83
+ switch (tag) {
84
+ case 'button': return 'button'
85
+ case 'a': return el.hasAttribute('href') ? 'link' : null
86
+ case 'input': {
87
+ var type = (el.getAttribute('type') || 'text').toLowerCase()
88
+ if (type === 'hidden') { return null }
89
+ return INPUT_TYPE_ROLES[type] || 'textbox'
90
+ }
91
+ case 'select': return 'combobox'
92
+ case 'textarea': return 'textbox'
93
+ case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': return 'heading'
94
+ case 'img': return 'img'
95
+ case 'nav': return 'navigation'
96
+ case 'main': return 'main'
97
+ case 'header': return !el.closest('article,aside,main,nav,section') ? 'banner' : null
98
+ case 'footer': return !el.closest('article,aside,main,nav,section') ? 'contentinfo' : null
99
+ case 'aside': return 'complementary'
100
+ case 'dialog': return 'dialog'
101
+ case 'form': return 'form'
102
+ case 'section': return el.hasAttribute('aria-label') || el.hasAttribute('aria-labelledby') ? 'region' : null
103
+ case 'summary': return 'button'
104
+ case 'details': return 'group'
105
+ case 'progress': return 'progressbar'
106
+ case 'meter': return 'meter'
107
+ case 'ul': case 'ol': return 'list'
108
+ case 'li': return 'listitem'
109
+ case 'table': return 'table'
110
+ }
111
+ if (el.contentEditable === 'true') { return 'textbox' }
112
+ if (el.hasAttribute('tabindex') && parseInt(el.getAttribute('tabindex') || '-1', 10) >= 0) { return 'generic' }
113
+ if (getDirectText(el)) { return 'statictext' }
114
+ return null
115
+ }
116
+
117
+ function getAccessibleName(el, role) {
118
+ var ariaLabel = el.getAttribute('aria-label')
119
+ if (ariaLabel) { return ariaLabel.trim() }
120
+ var labelledBy = el.getAttribute('aria-labelledby')
121
+ if (labelledBy) {
122
+ var texts = labelledBy.split(/\\s+/).map(function(id) { return (document.getElementById(id)?.textContent || '').trim() }).filter(Boolean)
123
+ if (texts.length > 0) { return texts.join(' ').slice(0, 200) }
124
+ }
125
+ var tag = el.tagName.toLowerCase()
126
+ if (tag === 'img' || (tag === 'input' && el.getAttribute('type') === 'image')) {
127
+ var alt = el.getAttribute('alt')
128
+ if (alt !== null) { return alt.trim() }
129
+ }
130
+ if (['input', 'select', 'textarea'].indexOf(tag) !== -1) {
131
+ var id = el.getAttribute('id')
132
+ if (id) {
133
+ var label = document.querySelector('label[for="' + CSS.escape(id) + '"]')
134
+ if (label) { return (label.textContent || '').trim() }
135
+ }
136
+ var parentLabel = el.closest('label')
137
+ if (parentLabel) {
138
+ var clone = parentLabel.cloneNode(true)
139
+ clone.querySelectorAll('input,select,textarea').forEach(function(n) { n.remove() })
140
+ var lt = (clone.textContent || '').trim()
141
+ if (lt) { return lt }
142
+ }
143
+ }
144
+ var ph = el.getAttribute('placeholder')
145
+ if (ph) { return ph.trim() }
146
+ var title = el.getAttribute('title')
147
+ if (title) { return title.trim() }
148
+ var childImg = el.querySelector('img')
149
+ if (childImg) {
150
+ var imgAlt = childImg.getAttribute('alt')
151
+ if (imgAlt) { return imgAlt.trim() }
152
+ }
153
+ if (role && CONTAINER_ROLES.has(role)) { return '' }
154
+ return ((el.textContent || '').trim().replace(/\\s+/g, ' ') || '').slice(0, 200)
155
+ }
156
+
157
+ function getSelector(element) {
158
+ var tag = element.tagName.toLowerCase()
159
+ var text = (element.textContent || '').trim().replace(/\\s+/g, ' ')
160
+ if (text && text.length > 0 && text.length <= 120) {
161
+ var sameTagElements = document.querySelectorAll(tag)
162
+ var matchCount = 0
163
+ sameTagElements.forEach(function(el) { if (el.textContent.includes(text)) { matchCount++ } })
164
+ if (matchCount === 1) { return tag + '*=' + text }
165
+ }
166
+ var ariaLabel = element.getAttribute('aria-label')
167
+ if (ariaLabel && ariaLabel.length <= 200) {
168
+ var sel = '[aria-label="' + CSS.escape(ariaLabel) + '"]'
169
+ if (document.querySelectorAll(sel).length === 1) { return sel }
170
+ }
171
+ var testId = element.getAttribute('data-testid')
172
+ if (testId) {
173
+ var testSel = '[data-testid="' + CSS.escape(testId) + '"]'
174
+ if (document.querySelectorAll(testSel).length === 1) { return testSel }
175
+ }
176
+ if (element.id) { return '#' + CSS.escape(element.id) }
177
+ var nameAttr = element.getAttribute('name')
178
+ if (nameAttr) {
179
+ var nameSel = tag + '[name="' + CSS.escape(nameAttr) + '"]'
180
+ if (document.querySelectorAll(nameSel).length === 1) { return nameSel }
181
+ }
182
+ if (element.className && typeof element.className === 'string') {
183
+ var classes = element.className.trim().split(/\\s+/).filter(Boolean)
184
+ for (var i = 0; i < classes.length; i++) {
185
+ var clsSel = tag + '.' + CSS.escape(classes[i])
186
+ if (document.querySelectorAll(clsSel).length === 1) { return clsSel }
187
+ }
188
+ if (classes.length >= 2) {
189
+ var twoClsSel = tag + classes.slice(0, 2).map(function(c) { return '.' + CSS.escape(c) }).join('')
190
+ if (document.querySelectorAll(twoClsSel).length === 1) { return twoClsSel }
191
+ }
192
+ }
193
+ var current = element
194
+ var path = []
195
+ while (current && current !== document.documentElement) {
196
+ var seg = current.tagName.toLowerCase()
197
+ if (current.id) { path.unshift('#' + CSS.escape(current.id)); break }
198
+ var parent = current.parentElement
199
+ if (parent) {
200
+ var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName })
201
+ if (siblings.length > 1) { seg += ':nth-of-type(' + (siblings.indexOf(current) + 1) + ')' }
202
+ }
203
+ path.unshift(seg)
204
+ current = current.parentElement
205
+ if (path.length >= 4) { break }
206
+ }
207
+ return path.join(' > ')
208
+ }
209
+
210
+ function getDirectText(el) {
211
+ var text = ''
212
+ for (var i = 0; i < el.childNodes.length; i++) {
213
+ if (el.childNodes[i].nodeType === 3) { text += el.childNodes[i].textContent }
214
+ }
215
+ return text.trim().replace(/\\s+/g, ' ')
216
+ }
217
+
218
+ function isVisible(el) {
219
+ if (typeof el.checkVisibility === 'function') {
220
+ return el.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true })
221
+ }
222
+ var style = window.getComputedStyle(el)
223
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && el.offsetWidth > 0 && el.offsetHeight > 0
224
+ }
225
+
226
+ function isInViewport(el) {
227
+ var rect = el.getBoundingClientRect()
228
+ return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth)
229
+ }
230
+
231
+ function getLevel(el) {
232
+ var m = el.tagName.toLowerCase().match(/^h([1-6])$/)
233
+ if (m) { return parseInt(m[1], 10) }
234
+ var ariaLevel = el.getAttribute('aria-level')
235
+ if (ariaLevel) { return parseInt(ariaLevel, 10) }
236
+ return undefined
237
+ }
238
+
239
+ function getState(el) {
240
+ var inputEl = el
241
+ var isCheckable = ['input', 'menuitemcheckbox', 'menuitemradio'].indexOf(el.tagName.toLowerCase()) !== -1 || ['checkbox', 'radio', 'switch'].indexOf(el.getAttribute('role') || '') !== -1
242
+ return {
243
+ disabled: el.getAttribute('aria-disabled') === 'true' || inputEl.disabled ? 'true' : '',
244
+ checked: isCheckable && inputEl.checked ? 'true' : el.getAttribute('aria-checked') || '',
245
+ expanded: el.getAttribute('aria-expanded') || '',
246
+ selected: el.getAttribute('aria-selected') || '',
247
+ pressed: el.getAttribute('aria-pressed') || '',
248
+ required: inputEl.required || el.getAttribute('aria-required') === 'true' ? 'true' : '',
249
+ readonly: inputEl.readOnly || el.getAttribute('aria-readonly') === 'true' ? 'true' : ''
250
+ }
251
+ }
252
+
253
+ var result = []
254
+
255
+ function walk(el, depth) {
256
+ if (depth > 200) { return }
257
+ if (!isVisible(el)) { return }
258
+ var role = getRole(el)
259
+ var inViewport = isInViewport(el)
260
+ if (!role) {
261
+ for (var i = 0; i < el.children.length; i++) { walk(el.children[i], depth + 1) }
262
+ return
263
+ }
264
+ if (${inViewportOnly} && !inViewport) {
265
+ for (var i = 0; i < el.children.length; i++) { walk(el.children[i], depth + 1) }
266
+ return
267
+ }
268
+ var name = getAccessibleName(el, role)
269
+ var selector = getSelector(el)
270
+ var node = { role: role, name: name, selector: selector, depth: depth, level: getLevel(el) ?? '', isInViewport: inViewport }
271
+ var state = getState(el)
272
+ for (var k in state) { node[k] = state[k] }
273
+ result.push(node)
274
+ for (var i = 0; i < el.children.length; i++) { walk(el.children[i], depth + 1) }
275
+ }
276
+
277
+ for (var i = 0; i < document.body.children.length; i++) { walk(document.body.children[i], 0) }
278
+ return result
279
+ })()`;
280
+ }
281
+ function elementsScript(includeBounds, inViewportOnly) {
282
+ return `(function () {
283
+ var interactableSelectors = [
284
+ 'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
285
+ '[role="button"]', '[role="link"]', '[role="checkbox"]', '[role="radio"]',
286
+ '[role="tab"]', '[role="menuitem"]', '[role="combobox"]', '[role="option"]',
287
+ '[role="switch"]', '[role="slider"]', '[role="textbox"]', '[role="searchbox"]',
288
+ '[role="spinbutton"]', '[contenteditable="true"]', '[tabindex]:not([tabindex="-1"])'
289
+ ].join(',')
290
+
291
+ function isVisible(element) {
292
+ if (typeof element.checkVisibility === 'function') {
293
+ return element.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true })
294
+ }
295
+ var style = window.getComputedStyle(element)
296
+ return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && element.offsetWidth > 0 && element.offsetHeight > 0
297
+ }
298
+
299
+ function getAccessibleName(el) {
300
+ var ariaLabel = el.getAttribute('aria-label')
301
+ if (ariaLabel) { return ariaLabel.trim() }
302
+ var labelledBy = el.getAttribute('aria-labelledby')
303
+ if (labelledBy) {
304
+ var texts = labelledBy.split(/\\s+/).map(function(id) { return (document.getElementById(id)?.textContent || '').trim() }).filter(Boolean)
305
+ if (texts.length > 0) { return texts.join(' ').slice(0, 200) }
306
+ }
307
+ var tag = el.tagName.toLowerCase()
308
+ if (tag === 'img' || (tag === 'input' && el.getAttribute('type') === 'image')) {
309
+ var alt = el.getAttribute('alt')
310
+ if (alt !== null) { return alt.trim() }
311
+ }
312
+ if (['input', 'select', 'textarea'].indexOf(tag) !== -1) {
313
+ var id = el.getAttribute('id')
314
+ if (id) {
315
+ var label = document.querySelector('label[for="' + CSS.escape(id) + '"]')
316
+ if (label) { return (label.textContent || '').trim() }
317
+ }
318
+ var parentLabel = el.closest('label')
319
+ if (parentLabel) {
320
+ var clone = parentLabel.cloneNode(true)
321
+ clone.querySelectorAll('input,select,textarea').forEach(function(n) { n.remove() })
322
+ var lt = (clone.textContent || '').trim()
323
+ if (lt) { return lt }
324
+ }
325
+ }
326
+ var ph = el.getAttribute('placeholder')
327
+ if (ph) { return ph.trim() }
328
+ var title = el.getAttribute('title')
329
+ if (title) { return title.trim() }
330
+ return ((el.textContent || '').trim().replace(/\\s+/g, ' ') || '').slice(0, 200)
331
+ }
332
+
333
+ function getSelector(element) {
334
+ var tag = element.tagName.toLowerCase()
335
+ var text = (element.textContent || '').trim().replace(/\\s+/g, ' ')
336
+ if (text && text.length > 0 && text.length <= 120) {
337
+ var sameTagElements = document.querySelectorAll(tag)
338
+ var matchCount = 0
339
+ sameTagElements.forEach(function(el) { if (el.textContent.includes(text)) { matchCount++ } })
340
+ if (matchCount === 1) { return tag + '*=' + text }
341
+ }
342
+ var ariaLabel = element.getAttribute('aria-label')
343
+ if (ariaLabel && ariaLabel.length <= 200) {
344
+ var sel = '[aria-label="' + CSS.escape(ariaLabel) + '"]'
345
+ if (document.querySelectorAll(sel).length === 1) { return sel }
346
+ }
347
+ var testId = element.getAttribute('data-testid')
348
+ if (testId) {
349
+ var testSel = '[data-testid="' + CSS.escape(testId) + '"]'
350
+ if (document.querySelectorAll(testSel).length === 1) { return testSel }
351
+ }
352
+ if (element.id) { return '#' + CSS.escape(element.id) }
353
+ var nameAttr = element.getAttribute('name')
354
+ if (nameAttr) {
355
+ var nameSel = tag + '[name="' + CSS.escape(nameAttr) + '"]'
356
+ if (document.querySelectorAll(nameSel).length === 1) { return nameSel }
357
+ }
358
+ if (element.className && typeof element.className === 'string') {
359
+ var classes = element.className.trim().split(/\\s+/).filter(Boolean)
360
+ for (var i = 0; i < classes.length; i++) {
361
+ var clsSel = tag + '.' + CSS.escape(classes[i])
362
+ if (document.querySelectorAll(clsSel).length === 1) { return clsSel }
363
+ }
364
+ if (classes.length >= 2) {
365
+ var twoClsSel = tag + classes.slice(0, 2).map(function(c) { return '.' + CSS.escape(c) }).join('')
366
+ if (document.querySelectorAll(twoClsSel).length === 1) { return twoClsSel }
367
+ }
368
+ }
369
+ var current = element
370
+ var path = []
371
+ while (current && current !== document.documentElement) {
372
+ var seg = current.tagName.toLowerCase()
373
+ if (current.id) { path.unshift('#' + CSS.escape(current.id)); break }
374
+ var parent = current.parentElement
375
+ if (parent) {
376
+ var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName })
377
+ if (siblings.length > 1) { seg += ':nth-of-type(' + (siblings.indexOf(current) + 1) + ')' }
378
+ }
379
+ path.unshift(seg)
380
+ current = current.parentElement
381
+ if (path.length >= 4) { break }
382
+ }
383
+ return path.join(' > ')
384
+ }
385
+
386
+ var elements = []
387
+ var seen = new Set()
388
+
389
+ document.querySelectorAll(interactableSelectors).forEach(function(el) {
390
+ if (seen.has(el)) { return }
391
+ seen.add(el)
392
+ var htmlEl = el
393
+ if (!isVisible(htmlEl)) { return }
394
+ var inputEl = htmlEl
395
+ var rect = htmlEl.getBoundingClientRect()
396
+ var isInVp = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth)
397
+ if (${inViewportOnly} && !isInVp) { return }
398
+ var entry = {
399
+ tagName: htmlEl.tagName.toLowerCase(),
400
+ name: getAccessibleName(htmlEl),
401
+ type: htmlEl.getAttribute('type') || '',
402
+ value: inputEl.value || '',
403
+ href: htmlEl.getAttribute('href') || '',
404
+ selector: getSelector(htmlEl),
405
+ isInViewport: isInVp
406
+ }
407
+ ${includeBounds ? "entry.boundingBox = { x: rect.x + window.scrollX, y: rect.y + window.scrollY, width: rect.width, height: rect.height }" : ""}
408
+ elements.push(entry)
409
+ })
410
+ return elements
411
+ })()`;
412
+ }
413
+
414
+ // ../core/src/locators/xml-parsing.ts
415
+ import { DOMParser } from "@xmldom/xmldom";
416
+ import xpath from "xpath";
417
+ function childNodesOf(node) {
418
+ const children = [];
419
+ if (node.childNodes) {
420
+ for (let i = 0; i < node.childNodes.length; i++) {
421
+ const child = node.childNodes.item(i);
422
+ if (child?.nodeType === 1) {
423
+ children.push(child);
424
+ }
425
+ }
426
+ }
427
+ return children;
428
+ }
429
+ function translateRecursively(domNode, parentPath = "", index = null) {
430
+ const attributes = {};
431
+ const element = domNode;
432
+ if (element.attributes) {
433
+ for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) {
434
+ const attr = element.attributes.item(attrIdx);
435
+ if (attr) {
436
+ attributes[attr.name] = attr.value.replace(/(\n)/gm, "\\n");
437
+ }
438
+ }
439
+ }
440
+ const path10 = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
441
+ return {
442
+ children: childNodesOf(domNode).map(
443
+ (childNode, childIndex) => translateRecursively(childNode, path10, childIndex)
444
+ ),
445
+ tagName: domNode.nodeName,
446
+ attributes,
447
+ path: path10
448
+ };
449
+ }
450
+ function isSameElement(node1, node2) {
451
+ if (node1.nodeType !== 1 || node2.nodeType !== 1) {
452
+ return false;
453
+ }
454
+ const el1 = node1;
455
+ const el2 = node2;
456
+ if (el1.nodeName !== el2.nodeName) {
457
+ return false;
458
+ }
459
+ const bounds1 = el1.getAttribute("bounds");
460
+ const bounds2 = el2.getAttribute("bounds");
461
+ if (bounds1 && bounds2) {
462
+ return bounds1 === bounds2;
463
+ }
464
+ const x1 = el1.getAttribute("x");
465
+ const y1 = el1.getAttribute("y");
466
+ const x2 = el2.getAttribute("x");
467
+ const y2 = el2.getAttribute("y");
468
+ if (x1 && y1 && x2 && y2) {
469
+ return x1 === x2 && y1 === y2 && el1.getAttribute("width") === el2.getAttribute("width") && el1.getAttribute("height") === el2.getAttribute("height");
470
+ }
471
+ return false;
472
+ }
473
+ function xmlToJSON(sourceXML) {
474
+ try {
475
+ const parser = new DOMParser();
476
+ const sourceDoc = parser.parseFromString(sourceXML, "text/xml");
477
+ const parseErrors = sourceDoc.getElementsByTagName("parsererror");
478
+ if (parseErrors.length > 0) {
479
+ console.error(
480
+ "[xmlToJSON] XML parsing error:",
481
+ parseErrors[0].textContent
482
+ );
483
+ return null;
484
+ }
485
+ const children = childNodesOf(sourceDoc);
486
+ const firstChild = children[0] || (sourceDoc.documentElement ? childNodesOf(sourceDoc.documentElement)[0] : null);
487
+ return firstChild ? translateRecursively(firstChild) : { children: [], tagName: "", attributes: {}, path: "" };
488
+ } catch (e) {
489
+ console.error("[xmlToJSON] Failed to parse XML:", e);
490
+ return null;
491
+ }
492
+ }
493
+ function xmlToDOM(sourceXML) {
494
+ try {
495
+ const parser = new DOMParser();
496
+ const doc = parser.parseFromString(sourceXML, "text/xml");
497
+ const parseErrors = doc.getElementsByTagName("parsererror");
498
+ if (parseErrors.length > 0) {
499
+ console.error("[xmlToDOM] XML parsing error:", parseErrors[0].textContent);
500
+ return null;
501
+ }
502
+ return doc;
503
+ } catch (e) {
504
+ console.error("[xmlToDOM] Failed to parse XML:", e);
505
+ return null;
506
+ }
507
+ }
508
+ function evaluateXPath(doc, xpathExpr) {
509
+ try {
510
+ const nodes = xpath.select(xpathExpr, doc);
511
+ if (Array.isArray(nodes)) {
512
+ return nodes;
513
+ }
514
+ return [];
515
+ } catch (e) {
516
+ console.error(`[evaluateXPath] Failed to evaluate "${xpathExpr}":`, e);
517
+ return [];
518
+ }
519
+ }
520
+ function checkXPathUniqueness(doc, xpathExpr, targetNode) {
521
+ try {
522
+ const nodes = evaluateXPath(doc, xpathExpr);
523
+ const totalMatches = nodes.length;
524
+ if (totalMatches === 0) {
525
+ return { isUnique: false };
526
+ }
527
+ if (totalMatches === 1) {
528
+ return { isUnique: true };
529
+ }
530
+ if (targetNode) {
531
+ for (let i = 0; i < nodes.length; i++) {
532
+ if (nodes[i] === targetNode || isSameElement(nodes[i], targetNode)) {
533
+ return {
534
+ isUnique: false,
535
+ index: i + 1,
536
+ // 1-based index for XPath
537
+ totalMatches
538
+ };
539
+ }
540
+ }
541
+ }
542
+ return { isUnique: false, totalMatches };
543
+ } catch (e) {
544
+ console.error(`[checkXPathUniqueness] Error checking "${xpathExpr}":`, e);
545
+ return { isUnique: false };
546
+ }
547
+ }
548
+ function findDOMNodeByPath(doc, path10) {
549
+ if (!path10) {
550
+ return doc.documentElement;
551
+ }
552
+ const indices = path10.split(".").map(Number);
553
+ let current = doc.documentElement;
554
+ for (const index of indices) {
555
+ if (!current) {
556
+ return null;
557
+ }
558
+ const children = [];
559
+ if (current.childNodes) {
560
+ for (let i = 0; i < current.childNodes.length; i++) {
561
+ const child = current.childNodes.item(i);
562
+ if (child?.nodeType === 1) {
563
+ children.push(child);
564
+ }
565
+ }
566
+ }
567
+ current = children[index] || null;
568
+ }
569
+ return current;
570
+ }
571
+ function parseAndroidBounds(bounds) {
572
+ const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
573
+ if (!match) {
574
+ return { x: 0, y: 0, width: 0, height: 0 };
575
+ }
576
+ const x1 = parseInt(match[1], 10);
577
+ const y1 = parseInt(match[2], 10);
578
+ const x2 = parseInt(match[3], 10);
579
+ const y2 = parseInt(match[4], 10);
580
+ return {
581
+ x: x1,
582
+ y: y1,
583
+ width: x2 - x1,
584
+ height: y2 - y1
585
+ };
586
+ }
587
+ function parseIOSBounds(attributes) {
588
+ return {
589
+ x: parseInt(attributes.x || "0", 10),
590
+ y: parseInt(attributes.y || "0", 10),
591
+ width: parseInt(attributes.width || "0", 10),
592
+ height: parseInt(attributes.height || "0", 10)
593
+ };
594
+ }
595
+ function countAttributeOccurrences(sourceXML, attribute, value) {
596
+ const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
597
+ const pattern = new RegExp(`${attribute}=["']${escapedValue}["']`, "g");
598
+ const matches = sourceXML.match(pattern);
599
+ return matches ? matches.length : 0;
600
+ }
601
+ function isAttributeUnique(sourceXML, attribute, value) {
602
+ return countAttributeOccurrences(sourceXML, attribute, value) === 1;
603
+ }
604
+
605
+ // ../core/src/locators/constants.ts
606
+ var ANDROID_INTERACTABLE_TAGS = [
607
+ // Input elements
608
+ "android.widget.EditText",
609
+ "android.widget.AutoCompleteTextView",
610
+ "android.widget.MultiAutoCompleteTextView",
611
+ "android.widget.SearchView",
612
+ // Button-like elements
613
+ "android.widget.Button",
614
+ "android.widget.ImageButton",
615
+ "android.widget.ToggleButton",
616
+ "android.widget.CompoundButton",
617
+ "android.widget.RadioButton",
618
+ "android.widget.CheckBox",
619
+ "android.widget.Switch",
620
+ "android.widget.FloatingActionButton",
621
+ "com.google.android.material.button.MaterialButton",
622
+ "com.google.android.material.floatingactionbutton.FloatingActionButton",
623
+ // Text elements (often tappable)
624
+ "android.widget.TextView",
625
+ "android.widget.CheckedTextView",
626
+ // Image elements (often tappable)
627
+ "android.widget.ImageView",
628
+ "android.widget.QuickContactBadge",
629
+ // Selection elements
630
+ "android.widget.Spinner",
631
+ "android.widget.SeekBar",
632
+ "android.widget.RatingBar",
633
+ "android.widget.ProgressBar",
634
+ "android.widget.DatePicker",
635
+ "android.widget.TimePicker",
636
+ "android.widget.NumberPicker",
637
+ // List/grid items
638
+ "android.widget.AdapterView"
639
+ ];
640
+ var ANDROID_LAYOUT_CONTAINERS = [
641
+ // Core ViewGroup classes
642
+ "android.view.ViewGroup",
643
+ "android.view.View",
644
+ "android.widget.FrameLayout",
645
+ "android.widget.LinearLayout",
646
+ "android.widget.RelativeLayout",
647
+ "android.widget.GridLayout",
648
+ "android.widget.TableLayout",
649
+ "android.widget.TableRow",
650
+ "android.widget.AbsoluteLayout",
651
+ // AndroidX layout classes
652
+ "androidx.constraintlayout.widget.ConstraintLayout",
653
+ "androidx.coordinatorlayout.widget.CoordinatorLayout",
654
+ "androidx.appcompat.widget.LinearLayoutCompat",
655
+ "androidx.cardview.widget.CardView",
656
+ "androidx.appcompat.widget.ContentFrameLayout",
657
+ "androidx.appcompat.widget.FitWindowsFrameLayout",
658
+ // Scrolling containers
659
+ "android.widget.ScrollView",
660
+ "android.widget.HorizontalScrollView",
661
+ "android.widget.NestedScrollView",
662
+ "androidx.core.widget.NestedScrollView",
663
+ "androidx.recyclerview.widget.RecyclerView",
664
+ "android.widget.ListView",
665
+ "android.widget.GridView",
666
+ "android.widget.AbsListView",
667
+ // App chrome / system elements
668
+ "android.widget.ActionBarContainer",
669
+ "android.widget.ActionBarOverlayLayout",
670
+ "android.view.ViewStub",
671
+ "androidx.appcompat.widget.ActionBarContainer",
672
+ "androidx.appcompat.widget.ActionBarContextView",
673
+ "androidx.appcompat.widget.ActionBarOverlayLayout",
674
+ // Decor views
675
+ "com.android.internal.policy.DecorView",
676
+ "android.widget.DecorView"
677
+ ];
678
+ var IOS_INTERACTABLE_TAGS = [
679
+ // Input elements
680
+ "XCUIElementTypeTextField",
681
+ "XCUIElementTypeSecureTextField",
682
+ "XCUIElementTypeTextView",
683
+ "XCUIElementTypeSearchField",
684
+ // Button-like elements
685
+ "XCUIElementTypeButton",
686
+ "XCUIElementTypeLink",
687
+ // Text elements (often tappable)
688
+ "XCUIElementTypeStaticText",
689
+ // Image elements
690
+ "XCUIElementTypeImage",
691
+ "XCUIElementTypeIcon",
692
+ // Selection elements
693
+ "XCUIElementTypeSwitch",
694
+ "XCUIElementTypeSlider",
695
+ "XCUIElementTypeStepper",
696
+ "XCUIElementTypeSegmentedControl",
697
+ "XCUIElementTypePicker",
698
+ "XCUIElementTypePickerWheel",
699
+ "XCUIElementTypeDatePicker",
700
+ "XCUIElementTypePageIndicator",
701
+ // Table/list items
702
+ "XCUIElementTypeCell",
703
+ "XCUIElementTypeMenuItem",
704
+ "XCUIElementTypeMenuBarItem",
705
+ // Toggle elements
706
+ "XCUIElementTypeCheckBox",
707
+ "XCUIElementTypeRadioButton",
708
+ "XCUIElementTypeToggle",
709
+ // Other interactive
710
+ "XCUIElementTypeKey",
711
+ "XCUIElementTypeKeyboard",
712
+ "XCUIElementTypeAlert",
713
+ "XCUIElementTypeSheet"
714
+ ];
715
+ var IOS_LAYOUT_CONTAINERS = [
716
+ // Generic containers
717
+ "XCUIElementTypeOther",
718
+ "XCUIElementTypeGroup",
719
+ "XCUIElementTypeLayoutItem",
720
+ // Scroll containers
721
+ "XCUIElementTypeScrollView",
722
+ "XCUIElementTypeTable",
723
+ "XCUIElementTypeCollectionView",
724
+ "XCUIElementTypeScrollBar",
725
+ // Navigation chrome
726
+ "XCUIElementTypeNavigationBar",
727
+ "XCUIElementTypeTabBar",
728
+ "XCUIElementTypeToolbar",
729
+ "XCUIElementTypeStatusBar",
730
+ "XCUIElementTypeMenuBar",
731
+ // Windows and views
732
+ "XCUIElementTypeWindow",
733
+ "XCUIElementTypeSheet",
734
+ "XCUIElementTypeDrawer",
735
+ "XCUIElementTypeDialog",
736
+ "XCUIElementTypePopover",
737
+ "XCUIElementTypePopUpButton",
738
+ // Outline elements
739
+ "XCUIElementTypeOutline",
740
+ "XCUIElementTypeOutlineRow",
741
+ "XCUIElementTypeBrowser",
742
+ "XCUIElementTypeSplitGroup",
743
+ "XCUIElementTypeSplitter",
744
+ // Application root
745
+ "XCUIElementTypeApplication"
746
+ ];
747
+
748
+ // ../core/src/locators/locator-generation.ts
749
+ function isValidValue(value) {
750
+ return value !== void 0 && value !== null && value !== "null" && value.trim() !== "";
751
+ }
752
+ function escapeText(text) {
753
+ return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
754
+ }
755
+ function escapeXPathValue(value) {
756
+ if (!value.includes("'")) {
757
+ return `'${value}'`;
758
+ }
759
+ if (!value.includes('"')) {
760
+ return `"${value}"`;
761
+ }
762
+ const parts = [];
763
+ let current = "";
764
+ for (const char of value) {
765
+ if (char === "'") {
766
+ if (current) {
767
+ parts.push(`'${current}'`);
768
+ }
769
+ parts.push(`"'"`);
770
+ current = "";
771
+ } else {
772
+ current += char;
773
+ }
774
+ }
775
+ if (current) {
776
+ parts.push(`'${current}'`);
777
+ }
778
+ return `concat(${parts.join(",")})`;
779
+ }
780
+ function generateIndexedXPath(baseXPath, index) {
781
+ return `(${baseXPath})[${index}]`;
782
+ }
783
+ function generateIndexedUiAutomator(baseSelector, index) {
784
+ return `${baseSelector}.instance(${index - 1})`;
785
+ }
786
+ function checkUniqueness(ctx, xpath2, targetNode) {
787
+ if (ctx.parsedDOM) {
788
+ return checkXPathUniqueness(ctx.parsedDOM, xpath2, targetNode);
789
+ }
790
+ const match = xpath2.match(/\/\/\*\[@([^=]{1,200})="([^"]{1,2000})"\]/);
791
+ if (match) {
792
+ const [, attr, value] = match;
793
+ return { isUnique: isAttributeUnique(ctx.sourceXML, attr, value) };
794
+ }
795
+ return { isUnique: false };
796
+ }
797
+ function getSiblingIndex(element) {
798
+ const parent = element.parentNode;
799
+ if (!parent) {
800
+ return 1;
801
+ }
802
+ const tagName = element.nodeName;
803
+ let index = 0;
804
+ for (let i = 0; i < parent.childNodes.length; i++) {
805
+ const child = parent.childNodes.item(i);
806
+ if (child?.nodeType === 1 && child.nodeName === tagName) {
807
+ index++;
808
+ if (child === element) {
809
+ return index;
810
+ }
811
+ }
812
+ }
813
+ return 1;
814
+ }
815
+ function countSiblings(element) {
816
+ const parent = element.parentNode;
817
+ if (!parent) {
818
+ return 1;
819
+ }
820
+ const tagName = element.nodeName;
821
+ let count = 0;
822
+ for (let i = 0; i < parent.childNodes.length; i++) {
823
+ const child = parent.childNodes.item(i);
824
+ if (child?.nodeType === 1 && child.nodeName === tagName) {
825
+ count++;
826
+ }
827
+ }
828
+ return count;
829
+ }
830
+ function findUniqueAttribute(element, ctx) {
831
+ const attrs = ctx.isAndroid ? ["resource-id", "content-desc", "text"] : ["name", "label", "value"];
832
+ for (const attr of attrs) {
833
+ const value = element.getAttribute(attr);
834
+ if (value && value.trim()) {
835
+ const xpath2 = `//*[@${attr}=${escapeXPathValue(value)}]`;
836
+ const result = ctx.parsedDOM ? checkXPathUniqueness(ctx.parsedDOM, xpath2) : { isUnique: isAttributeUnique(ctx.sourceXML, attr, value) };
837
+ if (result.isUnique) {
838
+ return `@${attr}=${escapeXPathValue(value)}`;
839
+ }
840
+ }
841
+ }
842
+ return null;
843
+ }
844
+ function buildHierarchicalXPath(ctx, element, maxDepth = 3) {
845
+ if (!ctx.parsedDOM) {
846
+ return null;
847
+ }
848
+ const pathParts = [];
849
+ let current = element;
850
+ let depth = 0;
851
+ while (current && depth < maxDepth) {
852
+ const tagName = current.nodeName;
853
+ const uniqueAttr = findUniqueAttribute(current, ctx);
854
+ if (uniqueAttr) {
855
+ pathParts.unshift(`//${tagName}[${uniqueAttr}]`);
856
+ break;
857
+ } else {
858
+ const siblingIndex = getSiblingIndex(current);
859
+ const siblingCount = countSiblings(current);
860
+ if (siblingCount > 1) {
861
+ pathParts.unshift(`${tagName}[${siblingIndex}]`);
862
+ } else {
863
+ pathParts.unshift(tagName);
864
+ }
865
+ }
866
+ const parent = current.parentNode;
867
+ current = parent && parent.nodeType === 1 ? parent : null;
868
+ depth++;
869
+ }
870
+ if (pathParts.length === 0) {
871
+ return null;
872
+ }
873
+ let result = pathParts[0];
874
+ for (let i = 1; i < pathParts.length; i++) {
875
+ result += "/" + pathParts[i];
876
+ }
877
+ if (!result.startsWith("//")) {
878
+ result = "//" + result;
879
+ }
880
+ return result;
881
+ }
882
+ function addXPathLocator(results, xpath2, ctx, targetNode) {
883
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
884
+ if (uniqueness.isUnique) {
885
+ results.push(["xpath", xpath2]);
886
+ } else if (uniqueness.index) {
887
+ results.push(["xpath", generateIndexedXPath(xpath2, uniqueness.index)]);
888
+ } else {
889
+ if (targetNode && ctx.parsedDOM) {
890
+ const hierarchical = buildHierarchicalXPath(
891
+ ctx,
892
+ targetNode
893
+ );
894
+ if (hierarchical) {
895
+ results.push(["xpath", hierarchical]);
896
+ }
897
+ }
898
+ results.push(["xpath", xpath2]);
899
+ }
900
+ }
901
+ function isInUiAutomatorScope(element, doc) {
902
+ if (!doc) {
903
+ return true;
904
+ }
905
+ const hierarchyNodes = evaluateXPath(doc, "/hierarchy/*");
906
+ if (hierarchyNodes.length === 0) {
907
+ return true;
908
+ }
909
+ const lastIndex = hierarchyNodes.length;
910
+ const pathParts = element.path.split(".");
911
+ if (pathParts.length === 0 || pathParts[0] === "") {
912
+ return true;
913
+ }
914
+ const firstIndex = parseInt(pathParts[0], 10);
915
+ return firstIndex === lastIndex - 1;
916
+ }
917
+ function buildUiAutomatorSelector(element) {
918
+ const attrs = element.attributes;
919
+ const parts = [];
920
+ if (isValidValue(attrs["resource-id"])) {
921
+ parts.push(`resourceId("${attrs["resource-id"]}")`);
922
+ }
923
+ if (isValidValue(attrs.text) && attrs.text.length < 100) {
924
+ parts.push(`text("${escapeText(attrs.text)}")`);
925
+ }
926
+ if (isValidValue(attrs["content-desc"])) {
927
+ parts.push(`description("${attrs["content-desc"]}")`);
928
+ }
929
+ if (isValidValue(attrs.class)) {
930
+ parts.push(`className("${attrs.class}")`);
931
+ }
932
+ if (parts.length === 0) {
933
+ return null;
934
+ }
935
+ return `android=new UiSelector().${parts.join(".")}`;
936
+ }
937
+ function buildPredicateString(element) {
938
+ const attrs = element.attributes;
939
+ const conditions = [];
940
+ if (isValidValue(attrs.name)) {
941
+ conditions.push(`name == "${escapeText(attrs.name)}"`);
942
+ }
943
+ if (isValidValue(attrs.label)) {
944
+ conditions.push(`label == "${escapeText(attrs.label)}"`);
945
+ }
946
+ if (isValidValue(attrs.value)) {
947
+ conditions.push(`value == "${escapeText(attrs.value)}"`);
948
+ }
949
+ if (attrs.visible === "true") {
950
+ conditions.push("visible == 1");
951
+ }
952
+ if (attrs.enabled === "true") {
953
+ conditions.push("enabled == 1");
954
+ }
955
+ if (conditions.length === 0) {
956
+ return null;
957
+ }
958
+ return `-ios predicate string:${conditions.join(" AND ")}`;
959
+ }
960
+ function buildClassChain(element) {
961
+ const attrs = element.attributes;
962
+ const tagName = element.tagName;
963
+ if (!tagName.startsWith("XCUI")) {
964
+ return null;
965
+ }
966
+ let selector = `**/${tagName}`;
967
+ if (isValidValue(attrs.label)) {
968
+ selector += `[\`label == "${escapeText(attrs.label)}"\`]`;
969
+ } else if (isValidValue(attrs.name)) {
970
+ selector += `[\`name == "${escapeText(attrs.name)}"\`]`;
971
+ }
972
+ return `-ios class chain:${selector}`;
973
+ }
974
+ function buildXPath(element, _sourceXML, isAndroid) {
975
+ const attrs = element.attributes;
976
+ const tagName = element.tagName;
977
+ const conditions = [];
978
+ if (isAndroid) {
979
+ if (isValidValue(attrs["resource-id"])) {
980
+ conditions.push(`@resource-id="${attrs["resource-id"]}"`);
981
+ }
982
+ if (isValidValue(attrs["content-desc"])) {
983
+ conditions.push(`@content-desc="${attrs["content-desc"]}"`);
984
+ }
985
+ if (isValidValue(attrs.text) && attrs.text.length < 100) {
986
+ conditions.push(`@text="${escapeText(attrs.text)}"`);
987
+ }
988
+ } else {
989
+ if (isValidValue(attrs.name)) {
990
+ conditions.push(`@name="${attrs.name}"`);
991
+ }
992
+ if (isValidValue(attrs.label)) {
993
+ conditions.push(`@label="${attrs.label}"`);
994
+ }
995
+ if (isValidValue(attrs.value)) {
996
+ conditions.push(`@value="${attrs.value}"`);
997
+ }
998
+ }
999
+ if (conditions.length === 0) {
1000
+ return `//${tagName}`;
1001
+ }
1002
+ return `//${tagName}[${conditions.join(" and ")}]`;
1003
+ }
1004
+ function getSimpleSuggestedLocators(element, ctx, automationName, inUiAutomatorScope, targetNode) {
1005
+ const results = [];
1006
+ const isAndroid = automationName.toLowerCase().includes("uiautomator");
1007
+ const attrs = element.attributes;
1008
+ if (isAndroid) {
1009
+ const resourceId = attrs["resource-id"];
1010
+ if (isValidValue(resourceId)) {
1011
+ const xpath2 = `//*[@resource-id="${resourceId}"]`;
1012
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1013
+ if (uniqueness.isUnique && inUiAutomatorScope) {
1014
+ results.push([
1015
+ "id",
1016
+ `android=new UiSelector().resourceId("${resourceId}")`
1017
+ ]);
1018
+ } else if (uniqueness.index && inUiAutomatorScope) {
1019
+ const base = `android=new UiSelector().resourceId("${resourceId}")`;
1020
+ results.push(["id", generateIndexedUiAutomator(base, uniqueness.index)]);
1021
+ }
1022
+ }
1023
+ const contentDesc = attrs["content-desc"];
1024
+ if (isValidValue(contentDesc)) {
1025
+ const xpath2 = `//*[@content-desc="${contentDesc}"]`;
1026
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1027
+ if (uniqueness.isUnique) {
1028
+ results.push(["accessibility-id", `~${contentDesc}`]);
1029
+ }
1030
+ }
1031
+ const text = attrs.text;
1032
+ if (isValidValue(text) && text.length < 100) {
1033
+ const xpath2 = `//*[@text="${escapeText(text)}"]`;
1034
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1035
+ if (uniqueness.isUnique && inUiAutomatorScope) {
1036
+ results.push([
1037
+ "text",
1038
+ `android=new UiSelector().text("${escapeText(text)}")`
1039
+ ]);
1040
+ } else if (uniqueness.index && inUiAutomatorScope) {
1041
+ const base = `android=new UiSelector().text("${escapeText(text)}")`;
1042
+ results.push([
1043
+ "text",
1044
+ generateIndexedUiAutomator(base, uniqueness.index)
1045
+ ]);
1046
+ }
1047
+ }
1048
+ } else {
1049
+ const name = attrs.name;
1050
+ if (isValidValue(name)) {
1051
+ const xpath2 = `//*[@name="${name}"]`;
1052
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1053
+ if (uniqueness.isUnique) {
1054
+ results.push(["accessibility-id", `~${name}`]);
1055
+ }
1056
+ }
1057
+ const label = attrs.label;
1058
+ if (isValidValue(label) && label !== attrs.name) {
1059
+ const xpath2 = `//*[@label="${escapeText(label)}"]`;
1060
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1061
+ if (uniqueness.isUnique) {
1062
+ results.push([
1063
+ "predicate-string",
1064
+ `-ios predicate string:label == "${escapeText(label)}"`
1065
+ ]);
1066
+ }
1067
+ }
1068
+ const value = attrs.value;
1069
+ if (isValidValue(value)) {
1070
+ const xpath2 = `//*[@value="${escapeText(value)}"]`;
1071
+ const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
1072
+ if (uniqueness.isUnique) {
1073
+ results.push([
1074
+ "predicate-string",
1075
+ `-ios predicate string:value == "${escapeText(value)}"`
1076
+ ]);
1077
+ }
1078
+ }
1079
+ }
1080
+ return results;
1081
+ }
1082
+ function getComplexSuggestedLocators(element, ctx, automationName, inUiAutomatorScope, targetNode) {
1083
+ const results = [];
1084
+ const isAndroid = automationName.toLowerCase().includes("uiautomator");
1085
+ if (isAndroid) {
1086
+ if (inUiAutomatorScope) {
1087
+ const uiAutomator = buildUiAutomatorSelector(element);
1088
+ if (uiAutomator) {
1089
+ results.push(["uiautomator", uiAutomator]);
1090
+ }
1091
+ }
1092
+ const xpath2 = buildXPath(element, ctx.sourceXML, true);
1093
+ if (xpath2) {
1094
+ addXPathLocator(results, xpath2, ctx, targetNode);
1095
+ }
1096
+ if (inUiAutomatorScope && isValidValue(element.attributes.class)) {
1097
+ results.push([
1098
+ "class-name",
1099
+ `android=new UiSelector().className("${element.attributes.class}")`
1100
+ ]);
1101
+ }
1102
+ } else {
1103
+ const predicate = buildPredicateString(element);
1104
+ if (predicate) {
1105
+ results.push(["predicate-string", predicate]);
1106
+ }
1107
+ const classChain = buildClassChain(element);
1108
+ if (classChain) {
1109
+ results.push(["class-chain", classChain]);
1110
+ }
1111
+ const xpath2 = buildXPath(element, ctx.sourceXML, false);
1112
+ if (xpath2) {
1113
+ addXPathLocator(results, xpath2, ctx, targetNode);
1114
+ }
1115
+ const type = element.tagName;
1116
+ if (type.startsWith("XCUIElementType")) {
1117
+ results.push(["class-name", `-ios class chain:**/${type}`]);
1118
+ }
1119
+ }
1120
+ return results;
1121
+ }
1122
+ function getSuggestedLocators(element, sourceXML, automationName, ctx, targetNode) {
1123
+ const locatorCtx = ctx ?? {
1124
+ sourceXML,
1125
+ parsedDOM: null,
1126
+ isAndroid: automationName.toLowerCase().includes("uiautomator")
1127
+ };
1128
+ const inUiAutomatorScope = locatorCtx.isAndroid ? isInUiAutomatorScope(element, locatorCtx.parsedDOM) : true;
1129
+ const simpleLocators = getSimpleSuggestedLocators(
1130
+ element,
1131
+ locatorCtx,
1132
+ automationName,
1133
+ inUiAutomatorScope,
1134
+ targetNode
1135
+ );
1136
+ const complexLocators = getComplexSuggestedLocators(
1137
+ element,
1138
+ locatorCtx,
1139
+ automationName,
1140
+ inUiAutomatorScope,
1141
+ targetNode
1142
+ );
1143
+ const seen = /* @__PURE__ */ new Set();
1144
+ const results = [];
1145
+ for (const locator of [...simpleLocators, ...complexLocators]) {
1146
+ if (!seen.has(locator[1])) {
1147
+ seen.add(locator[1]);
1148
+ results.push(locator);
1149
+ }
1150
+ }
1151
+ return results;
1152
+ }
1153
+ function locatorsToObject(locators) {
1154
+ const result = {};
1155
+ for (const [strategy, value] of locators) {
1156
+ if (!result[strategy]) {
1157
+ result[strategy] = value;
1158
+ }
1159
+ }
1160
+ return result;
1161
+ }
1162
+
1163
+ // ../core/src/element-snapshot.ts
1164
+ var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
1165
+ "button",
1166
+ "link",
1167
+ "textbox",
1168
+ "checkbox",
1169
+ "radio",
1170
+ "combobox",
1171
+ "slider",
1172
+ "searchbox",
1173
+ "spinbutton",
1174
+ "switch",
1175
+ "tab",
1176
+ "menuitem",
1177
+ "option"
1178
+ ]);
1179
+ function inferPurpose(nodes, index) {
1180
+ const myDepth = nodes[index].depth;
1181
+ for (let i = index - 1; i >= 0; i--) {
1182
+ if (nodes[i].depth <= myDepth && nodes[i].name) {
1183
+ if (nodes[i].depth === myDepth && INTERACTIVE_ROLES.has(nodes[i].role)) {
1184
+ continue;
1185
+ }
1186
+ return nodes[i].name;
1187
+ }
1188
+ }
1189
+ return void 0;
1190
+ }
1191
+ function serializeWebSnapshot(nodes, context, options = {}) {
1192
+ const { inViewportOnly = true } = options;
1193
+ let header = "[Page";
1194
+ if (context?.title) {
1195
+ header += `: ${context.title}`;
1196
+ }
1197
+ if (context?.url) {
1198
+ header += ` \u2014 ${context.url}`;
1199
+ }
1200
+ header += "]";
1201
+ const lines = [header];
1202
+ for (let i = 0; i < nodes.length; i++) {
1203
+ const node = nodes[i];
1204
+ if (inViewportOnly && node.isInViewport === false) {
1205
+ continue;
1206
+ }
1207
+ const indent = " ".repeat(node.depth + 1);
1208
+ const isInteractive = INTERACTIVE_ROLES.has(node.role);
1209
+ if (node.role === "statictext" && node.name) {
1210
+ let echoedByParent = false;
1211
+ for (let j = i - 1; j >= 0; j--) {
1212
+ if (nodes[j].depth < node.depth) {
1213
+ const parentRole = nodes[j].role;
1214
+ const parentName = nodes[j].name;
1215
+ if (INTERACTIVE_ROLES.has(parentRole) && parentName && parentName.includes(node.name)) {
1216
+ echoedByParent = true;
1217
+ }
1218
+ break;
1219
+ }
1220
+ }
1221
+ if (echoedByParent) {
1222
+ continue;
1223
+ }
1224
+ }
1225
+ const roleLabel = node.role === "heading" && node.level ? `heading[${node.level}]` : node.role;
1226
+ if (isInteractive) {
1227
+ if (!node.selector) {
1228
+ continue;
1229
+ }
1230
+ const purpose = inferPurpose(nodes, i);
1231
+ if (node.name) {
1232
+ lines.push(
1233
+ purpose ? `${indent}${roleLabel} "${node.name}" \u2208 "${purpose}" \u2192 ${node.selector}` : `${indent}${roleLabel} "${node.name}" \u2192 ${node.selector}`
1234
+ );
1235
+ } else if (purpose) {
1236
+ lines.push(`${indent}${roleLabel} \u2208 "${purpose}" \u2192 ${node.selector}`);
1237
+ } else {
1238
+ lines.push(`${indent}${roleLabel} \u2192 ${node.selector}`);
1239
+ }
1240
+ } else {
1241
+ lines.push(
1242
+ node.name ? `${indent}${roleLabel} "${node.name}"` : `${indent}${roleLabel}`
1243
+ );
1244
+ }
1245
+ }
1246
+ return lines.join("\n");
1247
+ }
1248
+ function simplifyTag(tagName) {
1249
+ const dot = tagName.lastIndexOf(".");
1250
+ if (dot !== -1) {
1251
+ return tagName.slice(dot + 1);
1252
+ }
1253
+ return tagName.replace(/^XCUIElementType/, "");
1254
+ }
1255
+ var ANDROID_ROLE_MAP = {
1256
+ "android.widget.Button": "button",
1257
+ "android.widget.ImageButton": "button",
1258
+ "android.widget.ToggleButton": "button",
1259
+ "android.widget.FloatingActionButton": "button",
1260
+ "com.google.android.material.button.MaterialButton": "button",
1261
+ "com.google.android.material.floatingactionbutton.FloatingActionButton": "button",
1262
+ "android.widget.EditText": "textbox",
1263
+ "android.widget.AutoCompleteTextView": "textbox",
1264
+ "android.widget.MultiAutoCompleteTextView": "textbox",
1265
+ "android.widget.SearchView": "searchbox",
1266
+ "android.widget.ImageView": "img",
1267
+ "android.widget.QuickContactBadge": "img",
1268
+ "android.widget.CheckBox": "checkbox",
1269
+ "android.widget.RadioButton": "radio",
1270
+ "android.widget.Switch": "switch",
1271
+ "android.widget.Spinner": "combobox",
1272
+ "android.widget.SeekBar": "slider",
1273
+ "android.widget.RatingBar": "slider",
1274
+ "android.widget.ProgressBar": "progressbar",
1275
+ "android.widget.TextView": "statictext",
1276
+ "android.widget.CheckedTextView": "statictext",
1277
+ "android.widget.RecyclerView": "list",
1278
+ "android.widget.ListView": "list",
1279
+ "android.widget.GridView": "list",
1280
+ "android.webkit.WebView": "webview"
1281
+ };
1282
+ var IOS_ROLE_MAP = {
1283
+ XCUIElementTypeButton: "button",
1284
+ XCUIElementTypeLink: "link",
1285
+ XCUIElementTypeTextField: "textbox",
1286
+ XCUIElementTypeSecureTextField: "textbox",
1287
+ XCUIElementTypeTextView: "textbox",
1288
+ XCUIElementTypeSearchField: "searchbox",
1289
+ XCUIElementTypeImage: "img",
1290
+ XCUIElementTypeIcon: "img",
1291
+ XCUIElementTypeSwitch: "switch",
1292
+ XCUIElementTypeSlider: "slider",
1293
+ XCUIElementTypeStepper: "slider",
1294
+ XCUIElementTypeCheckBox: "checkbox",
1295
+ XCUIElementTypeRadioButton: "radio",
1296
+ XCUIElementTypePicker: "combobox",
1297
+ XCUIElementTypePickerWheel: "combobox",
1298
+ XCUIElementTypeDatePicker: "combobox",
1299
+ XCUIElementTypeSegmentedControl: "combobox",
1300
+ XCUIElementTypeStaticText: "statictext",
1301
+ XCUIElementTypeCell: "listitem",
1302
+ XCUIElementTypeTable: "list",
1303
+ XCUIElementTypeCollectionView: "list"
1304
+ };
1305
+ function classifyMobileRole(tagName, platform) {
1306
+ if (platform === "android") {
1307
+ return ANDROID_ROLE_MAP[tagName] || simplifyTag(tagName);
1308
+ }
1309
+ return IOS_ROLE_MAP[tagName] || simplifyTag(tagName);
1310
+ }
1311
+ function getBestAndroidLocator(attrs) {
1312
+ if (attrs._selector) {
1313
+ return attrs._selector;
1314
+ }
1315
+ if (attrs["content-desc"]) {
1316
+ return `~${attrs["content-desc"]}`;
1317
+ }
1318
+ if (attrs["resource-id"]) {
1319
+ return `id:${attrs["resource-id"]}`;
1320
+ }
1321
+ if (attrs.text) {
1322
+ return `~${attrs.text}`;
1323
+ }
1324
+ if (attrs.class) {
1325
+ return `class:${simplifyTag(attrs.class)}`;
1326
+ }
1327
+ return void 0;
1328
+ }
1329
+ function getBestIOSLocator(attrs) {
1330
+ if (attrs._selector) {
1331
+ return attrs._selector;
1332
+ }
1333
+ if (attrs.name) {
1334
+ return `~${attrs.name}`;
1335
+ }
1336
+ if (attrs.label) {
1337
+ return `~${attrs.label}`;
1338
+ }
1339
+ if (attrs.value) {
1340
+ return `~${attrs.value}`;
1341
+ }
1342
+ if (attrs.type) {
1343
+ return `class:${simplifyTag(attrs.type)}`;
1344
+ }
1345
+ return void 0;
1346
+ }
1347
+ function getMobileNodeIdentity(attrs, platform) {
1348
+ if (platform === "android") {
1349
+ const contentDesc = attrs["content-desc"];
1350
+ if (contentDesc) {
1351
+ return contentDesc;
1352
+ }
1353
+ if (attrs.text) {
1354
+ return attrs.text;
1355
+ }
1356
+ const rid = attrs["resource-id"];
1357
+ if (rid) {
1358
+ const slash = rid.lastIndexOf("/");
1359
+ return slash !== -1 ? rid.slice(slash + 1) : rid;
1360
+ }
1361
+ return "";
1362
+ }
1363
+ return attrs.name || attrs.label || attrs.value || attrs.text || "";
1364
+ }
1365
+ var ANDROID_INTERACTABLE_SET = new Set(ANDROID_INTERACTABLE_TAGS);
1366
+ var IOS_INTERACTABLE_SET = new Set(IOS_INTERACTABLE_TAGS);
1367
+ function isExplicitlyInteractive(attrs, platform) {
1368
+ if (platform === "android") {
1369
+ return attrs.clickable === "true" || attrs.focusable === "true" || attrs.checkable === "true" || attrs["long-clickable"] === "true";
1370
+ }
1371
+ return attrs.accessible === "true";
1372
+ }
1373
+ function isMobileInteractive(element, platform) {
1374
+ const attrs = element.attributes;
1375
+ if (platform === "android") {
1376
+ if (ANDROID_INTERACTABLE_SET.has(element.tagName)) {
1377
+ return true;
1378
+ }
1379
+ return attrs.clickable === "true" || attrs["long-clickable"] === "true" || attrs.focusable === "true" || attrs.checkable === "true";
1380
+ }
1381
+ if (IOS_INTERACTABLE_SET.has(element.tagName)) {
1382
+ return true;
1383
+ }
1384
+ return attrs.accessible === "true";
1385
+ }
1386
+ function isMobileInViewport(element, platform, viewport) {
1387
+ const bounds = platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
1388
+ if (bounds.width === 0 && bounds.height === 0) {
1389
+ return true;
1390
+ }
1391
+ return bounds.x >= 0 && bounds.y >= 0 && bounds.width > 0 && bounds.height > 0 && bounds.x + bounds.width <= viewport.width && bounds.y + bounds.height <= viewport.height;
1392
+ }
1393
+ function collectMobileNodes(element, platform, depth, nodes, walkOpts) {
1394
+ const attrs = element.attributes;
1395
+ const role = classifyMobileRole(element.tagName, platform);
1396
+ const name = getMobileNodeIdentity(attrs, platform);
1397
+ const explicit = isExplicitlyInteractive(attrs, platform);
1398
+ const interactive = isMobileInteractive(element, platform);
1399
+ const inViewport = isMobileInViewport(element, platform, walkOpts.viewport);
1400
+ if (walkOpts.inViewportOnly) {
1401
+ if (interactive && !inViewport) {
1402
+ for (const child of element.children || []) {
1403
+ collectMobileNodes(child, platform, depth + 1, nodes, walkOpts);
1404
+ }
1405
+ return;
1406
+ }
1407
+ if (!interactive && !inViewport) {
1408
+ nodes.push({
1409
+ role: "generic",
1410
+ name: name ? `${role} "${name}"` : role,
1411
+ selector: "",
1412
+ depth,
1413
+ isInteractive: false,
1414
+ isExplicitInteractive: false,
1415
+ isInViewport: false
1416
+ });
1417
+ return;
1418
+ }
1419
+ }
1420
+ let locator = "";
1421
+ if (interactive) {
1422
+ if (walkOpts.sourceXML && walkOpts.automationName) {
1423
+ const suggested = getSuggestedLocators(
1424
+ element,
1425
+ walkOpts.sourceXML,
1426
+ walkOpts.automationName,
1427
+ {
1428
+ sourceXML: walkOpts.sourceXML,
1429
+ parsedDOM: null,
1430
+ isAndroid: platform === "android"
1431
+ }
1432
+ );
1433
+ if (suggested.length > 0) {
1434
+ locator = suggested[0][1];
1435
+ }
1436
+ }
1437
+ if (!locator) {
1438
+ locator = (platform === "android" ? getBestAndroidLocator(attrs) : getBestIOSLocator(attrs)) ?? "";
1439
+ }
1440
+ }
1441
+ nodes.push({
1442
+ role,
1443
+ name,
1444
+ selector: locator,
1445
+ depth,
1446
+ isInteractive: interactive,
1447
+ isExplicitInteractive: explicit,
1448
+ isInViewport: inViewport
1449
+ });
1450
+ for (const child of element.children || []) {
1451
+ collectMobileNodes(child, platform, depth + 1, nodes, walkOpts);
1452
+ }
1453
+ }
1454
+ var MOBILE_STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
1455
+ "img",
1456
+ "heading",
1457
+ "list",
1458
+ "listitem",
1459
+ "webview",
1460
+ "progressbar",
1461
+ "slider",
1462
+ "switch",
1463
+ "generic"
1464
+ ]);
1465
+ function mobileInferPurpose(nodes, index) {
1466
+ const myDepth = nodes[index].depth;
1467
+ for (let i = index - 1; i >= 0; i--) {
1468
+ if (nodes[i].depth <= myDepth && nodes[i].name) {
1469
+ if (nodes[i].depth === myDepth && !MOBILE_STRUCTURAL_ROLES.has(nodes[i].role)) {
1470
+ continue;
1471
+ }
1472
+ return nodes[i].name;
1473
+ }
1474
+ }
1475
+ return void 0;
1476
+ }
1477
+ function suppressTagOnlyChildren(nodes) {
1478
+ for (let i = 0; i < nodes.length; i++) {
1479
+ const node = nodes[i];
1480
+ if (!node.isInteractive || node.isExplicitInteractive) {
1481
+ continue;
1482
+ }
1483
+ for (let j = i - 1; j >= 0; j--) {
1484
+ if (nodes[j].depth < node.depth) {
1485
+ if (nodes[j].isExplicitInteractive) {
1486
+ node.isInteractive = false;
1487
+ break;
1488
+ }
1489
+ }
1490
+ }
1491
+ }
1492
+ }
1493
+ var NOISY_ROLES = /* @__PURE__ */ new Set([
1494
+ "FrameLayout",
1495
+ "LinearLayout",
1496
+ "ViewGroup",
1497
+ "RelativeLayout",
1498
+ "View",
1499
+ "CardView",
1500
+ "ConstraintLayout",
1501
+ "ScrollView"
1502
+ ]);
1503
+ function countSelectors(nodes) {
1504
+ const counts = /* @__PURE__ */ new Map();
1505
+ for (const node of nodes) {
1506
+ if (node.selector) {
1507
+ counts.set(node.selector, (counts.get(node.selector) ?? 0) + 1);
1508
+ }
1509
+ }
1510
+ return counts;
1511
+ }
1512
+ function renderMobileNodes(nodes) {
1513
+ const lines = [];
1514
+ const selectorCounts = countSelectors(nodes);
1515
+ const selectorIndex = /* @__PURE__ */ new Map();
1516
+ for (let i = 0; i < nodes.length; i++) {
1517
+ const node = nodes[i];
1518
+ const indent = " ".repeat(node.depth + 1);
1519
+ if (NOISY_ROLES.has(node.role) && !node.name && node.depth > 1 && !node.isInteractive) {
1520
+ continue;
1521
+ }
1522
+ if (node.isInViewport === false && !node.isInteractive) {
1523
+ lines.push(`${indent}\u22EF ${node.name} (off-screen)`);
1524
+ continue;
1525
+ }
1526
+ if (node.role === "statictext" && node.name) {
1527
+ let echoedByParent = false;
1528
+ for (let j = i - 1; j >= 0; j--) {
1529
+ if (nodes[j].depth < node.depth) {
1530
+ if (nodes[j].isInteractive && nodes[j].name && nodes[j].name.includes(node.name)) {
1531
+ echoedByParent = true;
1532
+ }
1533
+ break;
1534
+ }
1535
+ }
1536
+ if (echoedByParent) {
1537
+ continue;
1538
+ }
1539
+ }
1540
+ if (node.isInteractive && node.selector) {
1541
+ let selector = node.selector;
1542
+ const total = selectorCounts.get(selector) ?? 1;
1543
+ if (total > 1) {
1544
+ const idx = selectorIndex.get(selector) ?? 0;
1545
+ selectorIndex.set(selector, idx + 1);
1546
+ selector = `${selector}.instance(${idx})`;
1547
+ }
1548
+ const purpose = mobileInferPurpose(nodes, i);
1549
+ if (node.name) {
1550
+ lines.push(
1551
+ purpose ? `${indent}${node.role} "${node.name}" \u2208 "${purpose}" \u2192 ${selector}` : `${indent}${node.role} "${node.name}" \u2192 ${selector}`
1552
+ );
1553
+ } else if (purpose) {
1554
+ lines.push(`${indent}${node.role} \u2208 "${purpose}" \u2192 ${selector}`);
1555
+ } else {
1556
+ lines.push(`${indent}${node.role} \u2192 ${selector}`);
1557
+ }
1558
+ } else {
1559
+ lines.push(
1560
+ node.name ? `${indent}${node.role} "${node.name}"` : `${indent}${node.role}`
1561
+ );
1562
+ }
1563
+ }
1564
+ return lines;
1565
+ }
1566
+ function serializeMobileSnapshot(root, context, options = {}) {
1567
+ const { platform, deviceName, viewport, sourceXML } = context;
1568
+ const { inViewportOnly = true } = options;
1569
+ const effectiveXML = sourceXML || root.attributes._sourceXML;
1570
+ const effectiveViewport = viewport ?? { width: 9999, height: 9999 };
1571
+ const automationName = platform === "android" ? "uiautomator2" : "xcuitest";
1572
+ let header = `[${platform}`;
1573
+ if (deviceName) {
1574
+ header += ` \u2014 ${deviceName}`;
1575
+ }
1576
+ if (viewport) {
1577
+ header += ` (${viewport.width}\xD7${viewport.height})`;
1578
+ }
1579
+ header += "]";
1580
+ const nodes = [];
1581
+ collectMobileNodes(root, platform, 0, nodes, {
1582
+ inViewportOnly,
1583
+ viewport: effectiveViewport,
1584
+ sourceXML: effectiveXML,
1585
+ automationName: effectiveXML ? automationName : void 0
1586
+ });
1587
+ suppressTagOnlyChildren(nodes);
1588
+ const lines = renderMobileNodes(nodes);
1589
+ return [header, ...lines].join("\n");
1590
+ }
1591
+
1592
+ // ../core/src/locators/element-filter.ts
1593
+ function matchesTagList(tagName, tagList) {
1594
+ if (tagList.includes(tagName)) {
1595
+ return true;
1596
+ }
1597
+ for (const tag of tagList) {
1598
+ if (tagName.endsWith(tag) || tagName.includes(tag)) {
1599
+ return true;
1600
+ }
1601
+ }
1602
+ return false;
1603
+ }
1604
+ function matchesTagFilters(element, includeTagNames, excludeTagNames) {
1605
+ if (includeTagNames.length > 0 && !matchesTagList(element.tagName, includeTagNames)) {
1606
+ return false;
1607
+ }
1608
+ if (matchesTagList(element.tagName, excludeTagNames)) {
1609
+ return false;
1610
+ }
1611
+ return true;
1612
+ }
1613
+ function matchesAttributeFilters(element, requireAttributes, minAttributeCount) {
1614
+ if (requireAttributes.length > 0) {
1615
+ const hasRequiredAttr = requireAttributes.some(
1616
+ (attr) => element.attributes?.[attr]
1617
+ );
1618
+ if (!hasRequiredAttr) {
1619
+ return false;
1620
+ }
1621
+ }
1622
+ if (element.attributes && minAttributeCount > 0) {
1623
+ const attrCount = Object.values(element.attributes).filter(
1624
+ (v) => v !== void 0 && v !== null && v !== ""
1625
+ ).length;
1626
+ if (attrCount < minAttributeCount) {
1627
+ return false;
1628
+ }
1629
+ }
1630
+ return true;
1631
+ }
1632
+ function isInteractableElement(element, _isNative, automationName) {
1633
+ const isAndroid = automationName.toLowerCase().includes("uiautomator");
1634
+ const interactableTags = isAndroid ? ANDROID_INTERACTABLE_TAGS : IOS_INTERACTABLE_TAGS;
1635
+ if (matchesTagList(element.tagName, interactableTags)) {
1636
+ return true;
1637
+ }
1638
+ if (isAndroid) {
1639
+ if (element.attributes?.clickable === "true" || element.attributes?.focusable === "true" || element.attributes?.checkable === "true" || element.attributes?.["long-clickable"] === "true") {
1640
+ return true;
1641
+ }
1642
+ }
1643
+ if (!isAndroid) {
1644
+ if (element.attributes?.accessible === "true") {
1645
+ return true;
1646
+ }
1647
+ }
1648
+ return false;
1649
+ }
1650
+ function isLayoutContainer(element, platform) {
1651
+ const containerList = platform === "android" ? ANDROID_LAYOUT_CONTAINERS : IOS_LAYOUT_CONTAINERS;
1652
+ return matchesTagList(element.tagName, containerList);
1653
+ }
1654
+ function hasMeaningfulContent(element, platform) {
1655
+ const attrs = element.attributes;
1656
+ if (attrs.text && attrs.text.trim() !== "" && attrs.text !== "null") {
1657
+ return true;
1658
+ }
1659
+ if (platform === "android") {
1660
+ if (attrs["content-desc"] && attrs["content-desc"].trim() !== "" && attrs["content-desc"] !== "null") {
1661
+ return true;
1662
+ }
1663
+ } else {
1664
+ if (attrs.label && attrs.label.trim() !== "" && attrs.label !== "null") {
1665
+ return true;
1666
+ }
1667
+ if (attrs.name && attrs.name.trim() !== "" && attrs.name !== "null") {
1668
+ return true;
1669
+ }
1670
+ }
1671
+ return false;
1672
+ }
1673
+ function shouldIncludeElement(element, filters, isNative, automationName) {
1674
+ const {
1675
+ includeTagNames = [],
1676
+ excludeTagNames = ["hierarchy"],
1677
+ requireAttributes = [],
1678
+ minAttributeCount = 0,
1679
+ fetchableOnly = false,
1680
+ clickableOnly = false,
1681
+ visibleOnly = true
1682
+ } = filters;
1683
+ if (!matchesTagFilters(element, includeTagNames, excludeTagNames)) {
1684
+ if (element.attributes?.clickable !== "true") {
1685
+ return false;
1686
+ }
1687
+ }
1688
+ if (!matchesAttributeFilters(element, requireAttributes, minAttributeCount)) {
1689
+ return false;
1690
+ }
1691
+ if (clickableOnly && element.attributes?.clickable !== "true") {
1692
+ return false;
1693
+ }
1694
+ if (visibleOnly) {
1695
+ const isAndroid = automationName.toLowerCase().includes("uiautomator");
1696
+ if (isAndroid && element.attributes?.displayed === "false") {
1697
+ return false;
1698
+ }
1699
+ if (!isAndroid && element.attributes?.visible === "false") {
1700
+ return false;
1701
+ }
1702
+ }
1703
+ if (fetchableOnly && !isInteractableElement(element, isNative, automationName)) {
1704
+ return false;
1705
+ }
1706
+ return true;
1707
+ }
1708
+ function getDefaultFilters(platform, includeContainers = false) {
1709
+ const layoutContainers = platform === "android" ? ANDROID_LAYOUT_CONTAINERS : IOS_LAYOUT_CONTAINERS;
1710
+ return {
1711
+ excludeTagNames: includeContainers ? ["hierarchy"] : ["hierarchy", ...layoutContainers],
1712
+ fetchableOnly: !includeContainers,
1713
+ visibleOnly: true,
1714
+ clickableOnly: false
1715
+ };
1716
+ }
1717
+
1718
+ // ../core/src/locators/index.ts
1719
+ function parseBounds(element, platform) {
1720
+ return platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
1721
+ }
1722
+ function isWithinViewport(bounds, viewport) {
1723
+ return bounds.x >= 0 && bounds.y >= 0 && bounds.width > 0 && bounds.height > 0 && bounds.x + bounds.width <= viewport.width && bounds.y + bounds.height <= viewport.height;
1724
+ }
1725
+ function transformElement(element, locators, ctx) {
1726
+ const attrs = element.attributes;
1727
+ const bounds = parseBounds(element, ctx.platform);
1728
+ return {
1729
+ tagName: element.tagName,
1730
+ locators: locatorsToObject(locators),
1731
+ text: attrs.text || attrs.label || "",
1732
+ contentDesc: attrs["content-desc"] || "",
1733
+ resourceId: attrs["resource-id"] || "",
1734
+ accessibilityId: attrs.name || attrs["content-desc"] || "",
1735
+ label: attrs.label || "",
1736
+ value: attrs.value || "",
1737
+ className: attrs.class || element.tagName,
1738
+ clickable: attrs.clickable === "true" || attrs.accessible === "true" || attrs["long-clickable"] === "true",
1739
+ enabled: attrs.enabled !== "false",
1740
+ displayed: ctx.platform === "android" ? attrs.displayed !== "false" : attrs.visible !== "false",
1741
+ bounds,
1742
+ isInViewport: isWithinViewport(bounds, ctx.viewportSize)
1743
+ };
1744
+ }
1745
+ function shouldProcess(element, ctx) {
1746
+ if (shouldIncludeElement(element, ctx.filters, ctx.isNative, ctx.automationName)) {
1747
+ return true;
1748
+ }
1749
+ return isLayoutContainer(element, ctx.platform) && hasMeaningfulContent(element, ctx.platform);
1750
+ }
1751
+ function processElement(element, ctx) {
1752
+ if (!shouldProcess(element, ctx)) {
1753
+ return;
1754
+ }
1755
+ if (ctx.inViewportOnly) {
1756
+ const b = parseBounds(element, ctx.platform);
1757
+ if (!isWithinViewport(b, ctx.viewportSize)) {
1758
+ return;
1759
+ }
1760
+ }
1761
+ try {
1762
+ const targetNode = ctx.parsedDOM ? findDOMNodeByPath(ctx.parsedDOM, element.path) : void 0;
1763
+ const locators = getSuggestedLocators(
1764
+ element,
1765
+ ctx.sourceXML,
1766
+ ctx.automationName,
1767
+ {
1768
+ sourceXML: ctx.sourceXML,
1769
+ parsedDOM: ctx.parsedDOM,
1770
+ isAndroid: ctx.platform === "android"
1771
+ },
1772
+ targetNode || void 0
1773
+ );
1774
+ if (locators.length === 0) {
1775
+ return;
1776
+ }
1777
+ element.attributes._selector = locators[0][1];
1778
+ const transformed = transformElement(element, locators, ctx);
1779
+ if (Object.keys(transformed.locators).length === 0) {
1780
+ return;
1781
+ }
1782
+ ctx.results.push(transformed);
1783
+ } catch (error) {
1784
+ console.error(`[processElement] Error at path ${element.path}:`, error);
1785
+ }
1786
+ }
1787
+ function traverseTree(element, ctx) {
1788
+ if (!element) {
1789
+ return;
1790
+ }
1791
+ processElement(element, ctx);
1792
+ for (const child of element.children || []) {
1793
+ traverseTree(child, ctx);
1794
+ }
1795
+ }
1796
+ function generateAllElementLocators(sourceXML, options) {
1797
+ const sourceJSON = xmlToJSON(sourceXML);
1798
+ if (!sourceJSON) {
1799
+ console.error(
1800
+ "[generateAllElementLocators] Failed to parse page source XML"
1801
+ );
1802
+ return [];
1803
+ }
1804
+ const parsedDOM = xmlToDOM(sourceXML);
1805
+ const ctx = {
1806
+ sourceXML,
1807
+ platform: options.platform,
1808
+ automationName: options.platform === "android" ? "uiautomator2" : "xcuitest",
1809
+ isNative: options.isNative ?? true,
1810
+ viewportSize: options.viewportSize ?? { width: 9999, height: 9999 },
1811
+ filters: options.filters ?? {},
1812
+ inViewportOnly: options.inViewportOnly ?? true,
1813
+ results: [],
1814
+ parsedDOM
1815
+ };
1816
+ traverseTree(sourceJSON, ctx);
1817
+ return ctx.results;
1818
+ }
1819
+
1820
+ // ../core/src/with-timeout.ts
1821
+ function withTimeout(promise, ms, fallback) {
1822
+ let timer;
1823
+ const timeout = new Promise((resolve2) => {
1824
+ timer = setTimeout(() => resolve2(fallback), ms);
1825
+ });
1826
+ return Promise.race([promise, timeout]).finally(() => {
1827
+ if (timer) {
1828
+ clearTimeout(timer);
1829
+ }
1830
+ });
1831
+ }
1832
+ var SNAPSHOT_PROBE_TIMEOUT_MS = 2500;
1833
+
1834
+ // ../core/src/action-snapshot.ts
1835
+ async function runWith(runScript, scriptSrc, fallback) {
1836
+ if (!runScript) {
1837
+ return fallback;
1838
+ }
1839
+ return withTimeout(
1840
+ runScript(scriptSrc).then((r) => r),
1841
+ SNAPSHOT_PROBE_TIMEOUT_MS,
1842
+ fallback
1843
+ ).catch(() => fallback);
1844
+ }
1845
+ async function captureActionSnapshot(input) {
1846
+ try {
1847
+ const timestamp = Date.now();
1848
+ const isNativeMobile = !input.runScript && !!input.getPageSource;
1849
+ const [shot, url, title, pageSource, tree, elements] = await Promise.all([
1850
+ input.takeScreenshot?.().catch(() => null),
1851
+ input.getUrl?.().catch(() => void 0),
1852
+ input.getTitle?.().catch(() => void 0),
1853
+ isNativeMobile ? input.getPageSource?.().catch(() => void 0) : void 0,
1854
+ runWith(
1855
+ input.runScript,
1856
+ accessibilityTreeScript(true),
1857
+ []
1858
+ ),
1859
+ runWith(
1860
+ input.runScript,
1861
+ elementsScript(false, true),
1862
+ []
1863
+ )
1864
+ ]);
1865
+ let snapshotText;
1866
+ let finalElements = elements;
1867
+ if (isNativeMobile && pageSource) {
1868
+ const platform = input.platform ?? "android";
1869
+ const jsonTree = xmlToJSON(pageSource);
1870
+ if (jsonTree) {
1871
+ jsonTree.attributes._sourceXML = pageSource;
1872
+ snapshotText = serializeMobileSnapshot(jsonTree, {
1873
+ platform,
1874
+ sourceXML: pageSource
1875
+ });
1876
+ } else {
1877
+ snapshotText = `[${platform}]`;
1878
+ }
1879
+ try {
1880
+ const viewport = { width: 9999, height: 9999 };
1881
+ const filters = getDefaultFilters(platform, false);
1882
+ const locators = generateAllElementLocators(pageSource, {
1883
+ platform,
1884
+ viewportSize: viewport,
1885
+ filters,
1886
+ inViewportOnly: false
1887
+ });
1888
+ finalElements = locators;
1889
+ } catch {
1890
+ }
1891
+ } else {
1892
+ snapshotText = serializeWebSnapshot(tree, { url, title });
1893
+ }
1894
+ return {
1895
+ timestamp,
1896
+ command: input.command,
1897
+ url,
1898
+ title,
1899
+ screenshot: shot ?? void 0,
1900
+ elements: finalElements,
1901
+ snapshotText
1902
+ };
1903
+ } catch {
1904
+ return null;
1905
+ }
1906
+ }
1907
+
20
1908
  // ../core/src/stack.ts
21
1909
  import { parse as parseStackTrace } from "stacktrace-parser";
22
1910
  function isUserCodeFrame(frame) {
@@ -44,6 +1932,421 @@ function getCallSourceFromStack() {
44
1932
  return { filePath, callSource: `${filePath}:${frame.lineNumber ?? 0}` };
45
1933
  }
46
1934
 
1935
+ // ../core/src/trace-exporter.ts
1936
+ import fs from "fs/promises";
1937
+ import path from "path";
1938
+
1939
+ // ../core/src/trace-har.ts
1940
+ function toHeaderArray(h) {
1941
+ if (!h) {
1942
+ return [];
1943
+ }
1944
+ return Object.entries(h).map(([name, value]) => ({ name, value }));
1945
+ }
1946
+ function toQueryString(url) {
1947
+ try {
1948
+ const u = new URL(url);
1949
+ const out = [];
1950
+ u.searchParams.forEach((value, name) => out.push({ name, value }));
1951
+ return out;
1952
+ } catch {
1953
+ return [];
1954
+ }
1955
+ }
1956
+ function networkRequestToHar(entry) {
1957
+ const startedDateTime = new Date(entry.timestamp).toISOString();
1958
+ const duration = entry.time ?? (entry.endTime ?? entry.startTime) - entry.startTime;
1959
+ const status = entry.response?.status ?? entry.status ?? 0;
1960
+ const mimeType = entry.response?.mimeType ?? "";
1961
+ const responseHeaders = entry.response?.headers ?? entry.responseHeaders;
1962
+ return {
1963
+ type: "resource-snapshot",
1964
+ snapshot: {
1965
+ startedDateTime,
1966
+ time: Math.max(0, duration),
1967
+ request: {
1968
+ method: entry.method,
1969
+ url: entry.url,
1970
+ httpVersion: "HTTP/1.1",
1971
+ cookies: [],
1972
+ headers: toHeaderArray(entry.requestHeaders ?? entry.headers),
1973
+ queryString: toQueryString(entry.url),
1974
+ headersSize: -1,
1975
+ bodySize: entry.requestBody ? entry.requestBody.length : -1
1976
+ },
1977
+ response: {
1978
+ status,
1979
+ statusText: entry.statusText ?? "",
1980
+ httpVersion: "HTTP/1.1",
1981
+ cookies: [],
1982
+ headers: toHeaderArray(responseHeaders),
1983
+ content: { size: entry.size ?? 0, mimeType },
1984
+ redirectURL: "",
1985
+ headersSize: -1,
1986
+ bodySize: entry.size ?? -1
1987
+ },
1988
+ cache: {},
1989
+ timings: {
1990
+ send: 0,
1991
+ wait: Math.max(0, duration),
1992
+ receive: 0
1993
+ }
1994
+ }
1995
+ };
1996
+ }
1997
+
1998
+ // ../core/src/trace-zip-writer.ts
1999
+ import yazl from "yazl";
2000
+ function buildTraceZip(inputs) {
2001
+ return new Promise((resolve2, reject) => {
2002
+ const zipFile = new yazl.ZipFile();
2003
+ zipFile.addBuffer(Buffer.from(inputs.traceNdjson, "utf8"), "trace.trace");
2004
+ zipFile.addBuffer(inputs.networkNdjson, "trace.network");
2005
+ if (inputs.transcriptMd) {
2006
+ zipFile.addBuffer(
2007
+ Buffer.from(inputs.transcriptMd, "utf8"),
2008
+ "transcript.md"
2009
+ );
2010
+ }
2011
+ for (const resource of inputs.resources) {
2012
+ zipFile.addBuffer(resource.data, `resources/${resource.resourceName}`);
2013
+ }
2014
+ zipFile.end();
2015
+ const chunks = [];
2016
+ zipFile.outputStream.on("data", (chunk) => chunks.push(chunk));
2017
+ zipFile.outputStream.on("end", () => resolve2(Buffer.concat(chunks)));
2018
+ zipFile.outputStream.on("error", reject);
2019
+ });
2020
+ }
2021
+
2022
+ // ../core/src/trace-exporter.ts
2023
+ var TRACE_VERSION = 8;
2024
+ var LIBRARY_NAME = "@wdio/devtools-core";
2025
+ var LIBRARY_VERSION = "1.0.0";
2026
+ function shortId(sessionId) {
2027
+ return (sessionId ?? Math.random().toString(36).slice(2, 10)).slice(0, 8);
2028
+ }
2029
+ function resolveContextNaming(caps) {
2030
+ const platformName = typeof caps?.platformName === "string" ? caps.platformName.toLowerCase() : void 0;
2031
+ const deviceName = typeof caps?.["appium:deviceName"] === "string" ? caps["appium:deviceName"] : void 0;
2032
+ if (platformName === "android" || platformName === "ios") {
2033
+ return {
2034
+ browserName: "chromium",
2035
+ title: deviceName ? `${platformName} \u2014 ${deviceName}` : platformName
2036
+ };
2037
+ }
2038
+ const browserName = typeof caps?.browserName === "string" ? caps.browserName : "chromium";
2039
+ return { browserName, title: browserName };
2040
+ }
2041
+ function buildContextOptions(trace, contextId, wallTime) {
2042
+ const caps = trace.metadata.capabilities;
2043
+ const { browserName, title } = resolveContextNaming(caps);
2044
+ const viewport = trace.metadata.viewport ?? { width: 1280, height: 720 };
2045
+ return {
2046
+ version: TRACE_VERSION,
2047
+ type: "context-options",
2048
+ origin: "library",
2049
+ libraryName: LIBRARY_NAME,
2050
+ libraryVersion: LIBRARY_VERSION,
2051
+ browserName,
2052
+ platform: process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux",
2053
+ wallTime,
2054
+ monotonicTime: 0,
2055
+ sdkLanguage: "javascript",
2056
+ title,
2057
+ contextId,
2058
+ options: {
2059
+ viewport: { width: viewport.width, height: viewport.height }
2060
+ }
2061
+ };
2062
+ }
2063
+ function buildActionEvents(commands, pageId, wallTime) {
2064
+ const events = [];
2065
+ let prevEndMs = 0;
2066
+ let callCounter = 0;
2067
+ for (const cmd of commands) {
2068
+ const action = mapCommandToAction(cmd.command);
2069
+ if (!action) {
2070
+ continue;
2071
+ }
2072
+ callCounter++;
2073
+ const callId = `call@${callCounter}`;
2074
+ const rawStartMs = (cmd.startTime ?? cmd.timestamp) - wallTime;
2075
+ const rawEndMs = cmd.timestamp - wallTime;
2076
+ const startMs = Math.max(prevEndMs, rawStartMs);
2077
+ const endMs = Math.max(startMs + 1, rawEndMs);
2078
+ const rawArgs = cmd.args;
2079
+ let params;
2080
+ const isValueMethod = FILL_METHODS.has(action.method);
2081
+ if (action.class === "Element" && isValueMethod && rawArgs.length >= 2) {
2082
+ params = { selector: rawArgs[0], value: rawArgs[1] };
2083
+ } else if (action.class === "Element" && isValueMethod && rawArgs.length === 1) {
2084
+ params = { value: rawArgs[0] };
2085
+ } else if (action.class === "Element" && rawArgs.length === 1 && typeof rawArgs[0] === "string") {
2086
+ params = { selector: rawArgs[0] };
2087
+ } else if (rawArgs.length === 1 && typeof rawArgs[0] === "string") {
2088
+ params = { url: rawArgs[0] };
2089
+ } else {
2090
+ params = Object.fromEntries(rawArgs.map((a, i) => [String(i), a]));
2091
+ }
2092
+ events.push({
2093
+ type: "before",
2094
+ callId,
2095
+ startTime: startMs,
2096
+ class: action.class,
2097
+ method: action.method,
2098
+ pageId,
2099
+ params,
2100
+ title: formatActionTitle(action, cmd.args, params),
2101
+ apiName: `${action.class.toLowerCase()}.${action.method}`
2102
+ });
2103
+ const afterEvent = {
2104
+ type: "after",
2105
+ callId,
2106
+ endTime: endMs
2107
+ };
2108
+ if (cmd.error) {
2109
+ const err = cmd.error;
2110
+ afterEvent.error = { message: err.message ?? String(cmd.error) };
2111
+ }
2112
+ events.push(afterEvent);
2113
+ prevEndMs = endMs;
2114
+ }
2115
+ return events;
2116
+ }
2117
+ function buildNetworkNdjson(requests, wallTime, pageId) {
2118
+ if (!requests.length) {
2119
+ return Buffer.alloc(0);
2120
+ }
2121
+ const lines = requests.map((r) => {
2122
+ const entry = networkRequestToHar(r);
2123
+ entry.snapshot = {
2124
+ ...entry.snapshot,
2125
+ // Monotonic offset so the viewer positions bars on the timeline.
2126
+ _monotonicTime: Math.max(0, r.timestamp - wallTime),
2127
+ // Browsing context ID so the viewer associates requests with the page.
2128
+ _frameref: pageId
2129
+ };
2130
+ return JSON.stringify(entry);
2131
+ });
2132
+ return Buffer.from(lines.join("\n"), "utf8");
2133
+ }
2134
+ function buildSnapshotResources(snapshots, pageId) {
2135
+ const out = [];
2136
+ for (const snap of snapshots) {
2137
+ const base = `${pageId}-${snap.timestamp}`;
2138
+ if (snap.screenshot) {
2139
+ out.push({
2140
+ resourceName: `${base}.jpeg`,
2141
+ data: Buffer.from(snap.screenshot, "base64")
2142
+ });
2143
+ }
2144
+ if (snap.elements && snap.elements.length) {
2145
+ out.push({
2146
+ resourceName: `${base}-elements.json`,
2147
+ data: Buffer.from(JSON.stringify(snap.elements), "utf8")
2148
+ });
2149
+ }
2150
+ if (snap.snapshotText) {
2151
+ out.push({
2152
+ resourceName: `${base}-snapshot.txt`,
2153
+ data: Buffer.from(snap.snapshotText, "utf8")
2154
+ });
2155
+ }
2156
+ }
2157
+ return out;
2158
+ }
2159
+ function buildScreencastFrames(snapshots, pageId, wallTime, viewport) {
2160
+ return snapshots.filter((s) => s.screenshot).map((s) => {
2161
+ const base = `${pageId}-${s.timestamp}`;
2162
+ const frame = {
2163
+ type: "screencast-frame",
2164
+ pageId,
2165
+ sha1: `${base}.jpeg`,
2166
+ width: viewport.width,
2167
+ height: viewport.height,
2168
+ timestamp: Math.max(0, s.timestamp - wallTime)
2169
+ };
2170
+ if (s.elements && s.elements.length) {
2171
+ frame.elements = `${base}-elements.json`;
2172
+ }
2173
+ if (s.snapshotText) {
2174
+ frame.snapshot = `${base}-snapshot.txt`;
2175
+ }
2176
+ return frame;
2177
+ });
2178
+ }
2179
+ function eventTime(e) {
2180
+ switch (e.type) {
2181
+ case "context-options":
2182
+ return -Infinity;
2183
+ case "before":
2184
+ return e.startTime;
2185
+ case "after":
2186
+ return e.endTime;
2187
+ case "screencast-frame":
2188
+ return e.timestamp;
2189
+ }
2190
+ }
2191
+ function eventOrder(e) {
2192
+ switch (e.type) {
2193
+ case "context-options":
2194
+ return 0;
2195
+ case "after":
2196
+ return 1;
2197
+ case "screencast-frame":
2198
+ return 2;
2199
+ case "before":
2200
+ return 3;
2201
+ }
2202
+ }
2203
+ function compareEvents(a, b) {
2204
+ const dt = eventTime(a) - eventTime(b);
2205
+ return dt !== 0 ? dt : eventOrder(a) - eventOrder(b);
2206
+ }
2207
+ function generateTranscript(commands, startWallTime, title) {
2208
+ const wallTimeISO = new Date(startWallTime).toISOString();
2209
+ const lines = [`# ${title ?? "Session"} \u2014 ${wallTimeISO}`, ""];
2210
+ const captured = [];
2211
+ for (const c of commands) {
2212
+ const action = mapCommandToAction(String(c.command));
2213
+ if (action) {
2214
+ captured.push({ entry: c, action });
2215
+ }
2216
+ }
2217
+ captured.forEach(({ entry, action }, idx) => {
2218
+ const label = formatActionTitle(action, entry.args);
2219
+ const rawArgs = entry.args;
2220
+ const parts = [`${idx + 1}. ${label}`];
2221
+ if (FILL_METHODS.has(action.method) && rawArgs) {
2222
+ const valueIdx = rawArgs.length >= 2 ? 1 : 0;
2223
+ if (rawArgs[valueIdx] !== void 0) {
2224
+ parts.push(`value="${String(rawArgs[valueIdx])}"`);
2225
+ }
2226
+ }
2227
+ if (entry.error) {
2228
+ const msg = typeof entry.error === "object" && "message" in entry.error ? entry.error.message : String(entry.error);
2229
+ parts.push(`ERROR: ${msg}`);
2230
+ }
2231
+ lines.push(parts.join(" "));
2232
+ });
2233
+ return lines.join("\n");
2234
+ }
2235
+ function buildTraceBundle(trace, opts = {}) {
2236
+ const firstCommandTs = trace.commands[0]?.timestamp;
2237
+ const wallTime = opts.wallTimeOverride ?? firstCommandTs ?? Date.now();
2238
+ const idPrefix = shortId(opts.sessionId);
2239
+ const contextId = `context@${idPrefix}`;
2240
+ const pageId = `page@${idPrefix}`;
2241
+ const viewport = trace.metadata.viewport ?? { width: 1280, height: 720 };
2242
+ const snapshots = trace.actionSnapshots ?? [];
2243
+ const ctxOptions = buildContextOptions(trace, contextId, wallTime);
2244
+ const events = [ctxOptions];
2245
+ const firstSnap = snapshots.find((s) => s.screenshot);
2246
+ if (firstSnap) {
2247
+ const base = `${pageId}-${firstSnap.timestamp}`;
2248
+ const initFrame = {
2249
+ type: "screencast-frame",
2250
+ pageId,
2251
+ sha1: `${base}.jpeg`,
2252
+ width: viewport.width,
2253
+ height: viewport.height,
2254
+ timestamp: 0
2255
+ };
2256
+ if (firstSnap.elements && firstSnap.elements.length) {
2257
+ initFrame.elements = `${base}-elements.json`;
2258
+ }
2259
+ if (firstSnap.snapshotText) {
2260
+ initFrame.snapshot = `${base}-snapshot.txt`;
2261
+ }
2262
+ events.push(initFrame);
2263
+ }
2264
+ events.push(
2265
+ ...buildScreencastFrames(
2266
+ firstSnap ? snapshots.filter((s) => s !== firstSnap) : snapshots,
2267
+ pageId,
2268
+ wallTime,
2269
+ viewport
2270
+ ),
2271
+ ...buildActionEvents(trace.commands, pageId, wallTime)
2272
+ );
2273
+ events.sort(compareEvents);
2274
+ const ctxBName = ctxOptions.title;
2275
+ return {
2276
+ traceNdjson: events.map((e) => JSON.stringify(e)).join("\n") + "\n",
2277
+ networkNdjson: buildNetworkNdjson(trace.networkRequests, wallTime, pageId),
2278
+ transcriptMd: generateTranscript(trace.commands, wallTime, ctxBName),
2279
+ resources: buildSnapshotResources(snapshots, pageId)
2280
+ };
2281
+ }
2282
+ async function exportTraceZip(trace, opts = {}) {
2283
+ const bundle = buildTraceBundle(trace, opts);
2284
+ return buildTraceZip({
2285
+ traceNdjson: bundle.traceNdjson,
2286
+ networkNdjson: bundle.networkNdjson,
2287
+ resources: bundle.resources,
2288
+ transcriptMd: bundle.transcriptMd
2289
+ });
2290
+ }
2291
+ async function exportTraceDirectory(trace, targetDir, opts = {}) {
2292
+ const bundle = buildTraceBundle(trace, opts);
2293
+ await fs.mkdir(path.join(targetDir, "resources"), { recursive: true });
2294
+ await Promise.all([
2295
+ fs.writeFile(path.join(targetDir, "trace.trace"), bundle.traceNdjson),
2296
+ fs.writeFile(
2297
+ path.join(targetDir, "transcript.md"),
2298
+ bundle.transcriptMd,
2299
+ "utf8"
2300
+ ),
2301
+ bundle.networkNdjson.length ? fs.writeFile(
2302
+ path.join(targetDir, "trace.network"),
2303
+ bundle.networkNdjson
2304
+ ) : Promise.resolve(),
2305
+ ...bundle.resources.map(
2306
+ (r) => fs.writeFile(path.join(targetDir, "resources", r.resourceName), r.data)
2307
+ )
2308
+ ]);
2309
+ }
2310
+ async function writeTraceZip(capturer, opts) {
2311
+ const baseMetadata = capturer.metadata ?? {};
2312
+ const actionSnapshots = opts.actionSnapshots ?? synthesizeSnapshotsFromCommands(capturer.commandsLog);
2313
+ const traceLog = {
2314
+ mutations: capturer.mutations,
2315
+ logs: capturer.traceLogs,
2316
+ consoleLogs: capturer.consoleLogs,
2317
+ networkRequests: capturer.networkRequests,
2318
+ metadata: {
2319
+ ...baseMetadata,
2320
+ ...opts.capabilities ? { capabilities: opts.capabilities } : {}
2321
+ },
2322
+ commands: capturer.commandsLog,
2323
+ sources: Object.fromEntries(capturer.sources),
2324
+ ...actionSnapshots.length ? { actionSnapshots } : {}
2325
+ };
2326
+ await fs.mkdir(opts.outputDir, { recursive: true });
2327
+ const exportOpts = {
2328
+ sessionId: opts.sessionId,
2329
+ wallTimeOverride: capturer.startWallTime
2330
+ };
2331
+ if (opts.format === "ndjson-directory") {
2332
+ const dir = path.join(opts.outputDir, `trace-${opts.sessionId}`);
2333
+ await fs.mkdir(dir, { recursive: true });
2334
+ await exportTraceDirectory(traceLog, dir, exportOpts);
2335
+ return dir;
2336
+ }
2337
+ const zip = await exportTraceZip(traceLog, exportOpts);
2338
+ const zipPath = path.join(opts.outputDir, `trace-${opts.sessionId}.zip`);
2339
+ await fs.writeFile(zipPath, zip);
2340
+ return zipPath;
2341
+ }
2342
+ function synthesizeSnapshotsFromCommands(commands) {
2343
+ return commands.filter((c) => c.screenshot && mapCommandToAction(c.command)).map((c) => ({
2344
+ timestamp: c.timestamp,
2345
+ command: c.command,
2346
+ screenshot: c.screenshot
2347
+ }));
2348
+ }
2349
+
47
2350
  // ../core/src/uid.ts
48
2351
  function deterministicUid(...parts) {
49
2352
  const hash = parts.join("::").split("").reduce((acc, char) => (acc << 5) - acc + char.charCodeAt(0) | 0, 0);
@@ -62,13 +2365,13 @@ function resetSignatureCounters() {
62
2365
  }
63
2366
 
64
2367
  // ../core/src/finalize-screencast.ts
65
- import fs2 from "fs";
2368
+ import fs3 from "fs";
66
2369
  import os2 from "os";
67
- import path2 from "path";
2370
+ import path3 from "path";
68
2371
 
69
2372
  // ../core/src/video-encoder.ts
70
- import fs from "fs/promises";
71
- import path from "path";
2373
+ import fs2 from "fs/promises";
2374
+ import path2 from "path";
72
2375
  import os from "os";
73
2376
  import { createRequire } from "module";
74
2377
  var require2 = createRequire(import.meta.url);
@@ -85,20 +2388,20 @@ async function writeFramesAndManifest(frames, tmpDir, ext) {
85
2388
  const manifestLines = ["ffconcat version 1.0"];
86
2389
  for (let i = 0; i < frames.length; i++) {
87
2390
  const frameName = `frame-${String(i).padStart(6, "0")}.${ext}`;
88
- const framePath = path.join(tmpDir, frameName);
89
- await fs.writeFile(framePath, Buffer.from(frames[i].data, "base64"));
2391
+ const framePath = path2.join(tmpDir, frameName);
2392
+ await fs2.writeFile(framePath, Buffer.from(frames[i].data, "base64"));
90
2393
  const nextTs = frames[i + 1]?.timestamp ?? frames[i].timestamp + 100;
91
2394
  const durationSecs = Math.max((nextTs - frames[i].timestamp) / 1e3, 0.01);
92
2395
  manifestLines.push(`file '${framePath}'`);
93
2396
  manifestLines.push(`duration ${durationSecs.toFixed(6)}`);
94
2397
  }
95
- const lastFramePath = path.join(
2398
+ const lastFramePath = path2.join(
96
2399
  tmpDir,
97
2400
  `frame-${String(frames.length - 1).padStart(6, "0")}.${ext}`
98
2401
  );
99
2402
  manifestLines.push(`file '${lastFramePath}'`);
100
- const manifestPath = path.join(tmpDir, "manifest.txt");
101
- await fs.writeFile(manifestPath, manifestLines.join("\n"));
2403
+ const manifestPath = path2.join(tmpDir, "manifest.txt");
2404
+ await fs2.writeFile(manifestPath, manifestLines.join("\n"));
102
2405
  return manifestPath;
103
2406
  }
104
2407
  function classifyFfmpegError(err) {
@@ -134,14 +2437,14 @@ async function encodeToVideo(frames, outputPath, options = {}) {
134
2437
  }
135
2438
  const ffmpeg = loadFfmpeg();
136
2439
  const ext = options.captureFormat === "png" ? "png" : "jpg";
137
- const tmpDir = await fs.mkdtemp(
138
- path.join(os.tmpdir(), "devtools-screencast-")
2440
+ const tmpDir = await fs2.mkdtemp(
2441
+ path2.join(os.tmpdir(), "devtools-screencast-")
139
2442
  );
140
2443
  try {
141
2444
  const manifestPath = await writeFramesAndManifest(frames, tmpDir, ext);
142
2445
  await runFfmpeg(ffmpeg, manifestPath, outputPath);
143
2446
  } finally {
144
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {
2447
+ await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
145
2448
  });
146
2449
  }
147
2450
  }
@@ -170,11 +2473,11 @@ async function finalizeScreencast({
170
2473
  }
171
2474
  const fileName = `${filenamePrefix}-${sessionId}.webm`;
172
2475
  const candidate = outputDir || process.cwd();
173
- let videoPath = path2.join(candidate, fileName);
2476
+ let videoPath = path3.join(candidate, fileName);
174
2477
  try {
175
- fs2.accessSync(candidate, fs2.constants.W_OK);
2478
+ fs3.accessSync(candidate, fs3.constants.W_OK);
176
2479
  } catch {
177
- videoPath = path2.join(os2.tmpdir(), fileName);
2480
+ videoPath = path3.join(os2.tmpdir(), fileName);
178
2481
  }
179
2482
  try {
180
2483
  await encodeToVideo(frames, videoPath, { captureFormat });
@@ -192,12 +2495,12 @@ async function finalizeScreencast({
192
2495
  }
193
2496
 
194
2497
  // ../core/src/output-dir.ts
195
- import fs3 from "fs";
196
- import path3 from "path";
197
- var NODE_MODULES_SEGMENT = `${path3.sep}node_modules${path3.sep}`;
2498
+ import fs4 from "fs";
2499
+ import path4 from "path";
2500
+ var NODE_MODULES_SEGMENT = `${path4.sep}node_modules${path4.sep}`;
198
2501
  function isWritable(dir) {
199
2502
  try {
200
- fs3.accessSync(dir, fs3.constants.W_OK);
2503
+ fs4.accessSync(dir, fs4.constants.W_OK);
201
2504
  return true;
202
2505
  } catch {
203
2506
  return false;
@@ -210,10 +2513,10 @@ function resolveAdapterOutputDir(input = {}) {
210
2513
  }
211
2514
  const candidates = [];
212
2515
  if (input.testFilePath) {
213
- candidates.push(path3.dirname(input.testFilePath));
2516
+ candidates.push(path4.dirname(input.testFilePath));
214
2517
  }
215
2518
  if (input.configPath) {
216
- candidates.push(path3.dirname(input.configPath));
2519
+ candidates.push(path4.dirname(input.configPath));
217
2520
  }
218
2521
  candidates.push(fallback);
219
2522
  for (const dir of candidates) {
@@ -582,15 +2885,15 @@ var ScreencastRecorderBase = class {
582
2885
  };
583
2886
 
584
2887
  // ../core/src/script-loader.ts
585
- import fs4 from "fs/promises";
586
- import path4 from "path";
2888
+ import fs5 from "fs/promises";
2889
+ import path5 from "path";
587
2890
  import { createRequire as createRequire2 } from "module";
588
2891
  var require3 = createRequire2(import.meta.url);
589
2892
  async function loadInjectableScript() {
590
2893
  const scriptPath = require3.resolve("@wdio/devtools-script");
591
- const scriptDir = path4.dirname(scriptPath);
592
- const preloadScriptPath = path4.join(scriptDir, "script.js");
593
- const scriptContent = await fs4.readFile(preloadScriptPath, "utf-8");
2894
+ const scriptDir = path5.dirname(scriptPath);
2895
+ const preloadScriptPath = path5.join(scriptDir, "script.js");
2896
+ const scriptContent = await fs5.readFile(preloadScriptPath, "utf-8");
594
2897
  return `(async function() { ${scriptContent} })()`;
595
2898
  }
596
2899
  async function pollUntilReady(check, opts = {}) {
@@ -606,7 +2909,7 @@ async function pollUntilReady(check, opts = {}) {
606
2909
  }
607
2910
 
608
2911
  // ../core/src/session-capturer.ts
609
- import fs5 from "fs/promises";
2912
+ import fs6 from "fs/promises";
610
2913
  import { WebSocket } from "ws";
611
2914
  var SessionCapturerBase = class {
612
2915
  // ── State (mostly private; subclasses access shared ws via `this.ws`) ────
@@ -756,7 +3059,7 @@ var SessionCapturerBase = class {
756
3059
  return;
757
3060
  }
758
3061
  try {
759
- const source = (await fs5.readFile(filePath, "utf-8")).toString();
3062
+ const source = (await fs6.readFile(filePath, "utf-8")).toString();
760
3063
  this.sources.set(filePath, source);
761
3064
  this.sendUpstream("sources", { [filePath]: source });
762
3065
  } catch (err) {
@@ -938,17 +3241,17 @@ var SessionCapturerBase = class {
938
3241
  }
939
3242
  // ── Hooks (subclasses override) ─────────────────────────────────────────
940
3243
  /**
941
- * Default: forward a single ConsoleLog via the `consoleLogs` scope.
942
- * Args is passed as an array (matching the original console.* call shape:
943
- * `console.log('a', 'b')` → `args = ['a', 'b']`) so subclasses can preserve
944
- * the multi-argument structure for the UI.
3244
+ * Build a ConsoleLog entry from the captured line, push it into the local
3245
+ * `consoleLogs` array (so it ends up in any future trace export), and
3246
+ * broadcast it live via the `consoleLogs` WS scope.
945
3247
  *
946
- * Subclasses that need to maintain local capture state (for the rerun/
947
- * replay flow) should override to also push the entry into their own
948
- * array — see service's onLine override.
3248
+ * Args is passed as an array (matching the original console.* call shape:
3249
+ * `console.log('a', 'b')` → `args = ['a', 'b']`) so the multi-argument
3250
+ * structure is preserved for the UI.
949
3251
  */
950
3252
  onLine(type, args, source) {
951
3253
  const entry = createConsoleLogEntry(type, args, source);
3254
+ this.consoleLogs.push(entry);
952
3255
  this.sendUpstream("consoleLogs", [entry]);
953
3256
  }
954
3257
  /**
@@ -1063,14 +3366,14 @@ function stampTestEnd(test, end = /* @__PURE__ */ new Date()) {
1063
3366
  }
1064
3367
 
1065
3368
  // ../core/src/test-discovery.ts
1066
- import fs6 from "fs";
3369
+ import fs7 from "fs";
1067
3370
  function findTestDefinitions(filePath, opts = {}) {
1068
- if (!fs6.existsSync(filePath)) {
3371
+ if (!fs7.existsSync(filePath)) {
1069
3372
  return [];
1070
3373
  }
1071
3374
  let source;
1072
3375
  try {
1073
- source = fs6.readFileSync(filePath, "utf-8");
3376
+ source = fs7.readFileSync(filePath, "utf-8");
1074
3377
  } catch {
1075
3378
  return [];
1076
3379
  }
@@ -1165,11 +3468,12 @@ var TestReporterBase = class {
1165
3468
  };
1166
3469
 
1167
3470
  // src/index.ts
3471
+ import { stop as stopBackend } from "@wdio/devtools-backend";
1168
3472
  import logger11 from "@wdio/logger";
1169
3473
 
1170
3474
  // src/run-lifecycle.ts
1171
- import * as fs8 from "fs";
1172
- import * as path6 from "path";
3475
+ import * as fs9 from "fs";
3476
+ import * as path7 from "path";
1173
3477
  import * as os3 from "os";
1174
3478
  import logger from "@wdio/logger";
1175
3479
  import { remote } from "webdriverio";
@@ -1229,8 +3533,8 @@ var CONFIG_FILENAMES = [
1229
3533
  var PLUGIN_GLOBAL_KEY = "__nightwatchDevtoolsPlugin";
1230
3534
 
1231
3535
  // src/helpers/utils.ts
1232
- import * as fs7 from "fs";
1233
- import * as path5 from "path";
3536
+ import * as fs8 from "fs";
3537
+ import * as path6 from "path";
1234
3538
  import { parse as parseStackTrace2 } from "stacktrace-parser";
1235
3539
  function determineTestState(testcase) {
1236
3540
  if (testcase.passed === 0 && testcase.failed === 0) {
@@ -1287,7 +3591,7 @@ function findTestFileFromStack() {
1287
3591
  return void 0;
1288
3592
  }
1289
3593
  const filePath = normalizeFilePath(frame.file);
1290
- return fs7.existsSync(filePath) ? filePath : void 0;
3594
+ return fs8.existsSync(filePath) ? filePath : void 0;
1291
3595
  }
1292
3596
  function extractTestMetadata(filePath) {
1293
3597
  const defs = findTestDefinitions(filePath, {
@@ -1415,21 +3719,21 @@ function resolveNightwatchConfig() {
1415
3719
  );
1416
3720
  if (configFlagIdx !== -1 && process.argv[configFlagIdx + 1]) {
1417
3721
  const argvConfig = process.argv[configFlagIdx + 1];
1418
- const resolved = path5.isAbsolute(argvConfig) ? argvConfig : path5.resolve(process.cwd(), argvConfig);
1419
- if (fs7.existsSync(resolved)) {
3722
+ const resolved = path6.isAbsolute(argvConfig) ? argvConfig : path6.resolve(process.cwd(), argvConfig);
3723
+ if (fs8.existsSync(resolved)) {
1420
3724
  return resolved;
1421
3725
  }
1422
3726
  }
1423
3727
  let dir = process.cwd();
1424
- const root = path5.parse(dir).root;
3728
+ const root = path6.parse(dir).root;
1425
3729
  while (dir && dir !== root) {
1426
3730
  for (const file of CONFIG_FILENAMES) {
1427
- const candidate = path5.join(dir, file);
1428
- if (fs7.existsSync(candidate)) {
3731
+ const candidate = path6.join(dir, file);
3732
+ if (fs8.existsSync(candidate)) {
1429
3733
  return candidate;
1430
3734
  }
1431
3735
  }
1432
- const parent = path5.dirname(dir);
3736
+ const parent = path6.dirname(dir);
1433
3737
  if (parent === dir) {
1434
3738
  break;
1435
3739
  }
@@ -1474,20 +3778,25 @@ async function runPluginBefore(ctx) {
1474
3778
  return;
1475
3779
  }
1476
3780
  try {
1477
- ctx.options.port = await findFreePort(
1478
- ctx.options.port,
1479
- ctx.options.hostname
1480
- );
1481
- log.info("\u{1F680} Starting DevTools backend...");
1482
- const { port } = await start(ctx.options);
1483
- ctx.options.port = port;
1484
- const url = `http://${ctx.options.hostname}:${ctx.options.port}`;
1485
- log.info(`\u2713 Backend started on port ${ctx.options.port}`);
1486
- log.info(` DevTools UI: ${url}`);
1487
- await ctx.openDevtoolsBrowserAt(url);
1488
- await new Promise(
1489
- (resolve2) => setTimeout(resolve2, TIMING.UI_CONNECTION_WAIT)
1490
- );
3781
+ if (ctx.options.mode === "trace") {
3782
+ log.info("Trace mode \u2014 skipping backend port-bind and UI window");
3783
+ } else {
3784
+ ctx.options.port = await findFreePort(
3785
+ ctx.options.port,
3786
+ ctx.options.hostname
3787
+ );
3788
+ log.info("\u{1F680} Starting DevTools backend...");
3789
+ const { port } = await start(ctx.options);
3790
+ ctx.options.port = port;
3791
+ const url = `http://${ctx.options.hostname}:${ctx.options.port}`;
3792
+ log.info(`\u2713 Backend started on port ${ctx.options.port}`);
3793
+ log.info(` DevTools UI: ${url}`);
3794
+ await ctx.openDevtoolsBrowserAt(url);
3795
+ await new Promise(
3796
+ (resolve2) => setTimeout(resolve2, TIMING.UI_CONNECTION_WAIT)
3797
+ );
3798
+ }
3799
+ ;
1491
3800
  globalThis[PLUGIN_GLOBAL_KEY] = ctx.plugin;
1492
3801
  } catch (err) {
1493
3802
  log.error(`Failed to start backend: ${errorMessage(err)}`);
@@ -1496,12 +3805,12 @@ async function runPluginBefore(ctx) {
1496
3805
  }
1497
3806
  async function openDevtoolsBrowser(ctx, url) {
1498
3807
  try {
1499
- ctx.userDataDir = path6.join(
3808
+ ctx.userDataDir = path7.join(
1500
3809
  os3.tmpdir(),
1501
3810
  `nightwatch-devtools-${ctx.options.port}-${Date.now()}`
1502
3811
  );
1503
- if (!fs8.existsSync(ctx.userDataDir)) {
1504
- fs8.mkdirSync(ctx.userDataDir, { recursive: true });
3812
+ if (!fs9.existsSync(ctx.userDataDir)) {
3813
+ fs9.mkdirSync(ctx.userDataDir, { recursive: true });
1505
3814
  }
1506
3815
  ctx.devtoolsBrowser = await remote({
1507
3816
  logLevel: "info",
@@ -1726,43 +4035,43 @@ function buildCucumberScenarioSuite(input) {
1726
4035
  }
1727
4036
 
1728
4037
  // src/helpers/featureFileScan.ts
1729
- import fs9 from "fs";
1730
- import path7 from "path";
4038
+ import fs10 from "fs";
4039
+ import path8 from "path";
1731
4040
  function scanFeatureFile(featureUri) {
1732
- const featureAbsPath = path7.resolve(process.cwd(), featureUri);
4041
+ const featureAbsPath = path8.resolve(process.cwd(), featureUri);
1733
4042
  const result = {
1734
- featureName: path7.basename(featureUri, ".feature"),
4043
+ featureName: path8.basename(featureUri, ".feature"),
1735
4044
  featureContent: "",
1736
4045
  featureAbsPath,
1737
4046
  stepDefFiles: [],
1738
4047
  capturedPaths: []
1739
4048
  };
1740
- if (featureUri === "unknown.feature" || !fs9.existsSync(featureAbsPath)) {
4049
+ if (featureUri === "unknown.feature" || !fs10.existsSync(featureAbsPath)) {
1741
4050
  return result;
1742
4051
  }
1743
- result.featureContent = fs9.readFileSync(featureAbsPath, "utf-8");
4052
+ result.featureContent = fs10.readFileSync(featureAbsPath, "utf-8");
1744
4053
  const match = result.featureContent.match(/^\s*Feature:\s*(.+)/m);
1745
4054
  if (match) {
1746
4055
  result.featureName = match[1].trim();
1747
4056
  }
1748
4057
  result.capturedPaths.push(featureAbsPath);
1749
- const featureDir = path7.dirname(featureAbsPath);
4058
+ const featureDir = path8.dirname(featureAbsPath);
1750
4059
  const stepDirCandidates = ["step_definitions", "steps", "support"];
1751
4060
  for (const candidate of stepDirCandidates) {
1752
- const stepDir = path7.join(featureDir, candidate);
1753
- if (!fs9.existsSync(stepDir) || !fs9.statSync(stepDir).isDirectory()) {
4061
+ const stepDir = path8.join(featureDir, candidate);
4062
+ if (!fs10.existsSync(stepDir) || !fs10.statSync(stepDir).isDirectory()) {
1754
4063
  continue;
1755
4064
  }
1756
- for (const entry of fs9.readdirSync(stepDir)) {
4065
+ for (const entry of fs10.readdirSync(stepDir)) {
1757
4066
  if (!/\.(js|ts|mjs|cjs)$/.test(entry)) {
1758
4067
  continue;
1759
4068
  }
1760
- const stepFilePath = path7.join(stepDir, entry);
4069
+ const stepFilePath = path8.join(stepDir, entry);
1761
4070
  result.capturedPaths.push(stepFilePath);
1762
4071
  try {
1763
4072
  result.stepDefFiles.push({
1764
4073
  filePath: stepFilePath,
1765
- content: fs9.readFileSync(stepFilePath, "utf-8")
4074
+ content: fs10.readFileSync(stepFilePath, "utf-8")
1766
4075
  });
1767
4076
  } catch {
1768
4077
  }
@@ -1929,8 +4238,8 @@ async function cucumberAfter(ctx, browser, result, pickle) {
1929
4238
  import logger5 from "@wdio/logger";
1930
4239
 
1931
4240
  // src/helpers/specFileResolver.ts
1932
- import fs10 from "fs";
1933
- import path8 from "path";
4241
+ import fs11 from "fs";
4242
+ import path9 from "path";
1934
4243
  function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
1935
4244
  let fullPath = findTestFileFromStack() || null;
1936
4245
  if (!fullPath && cachedPath && cachedPath.includes(testFile)) {
@@ -1946,27 +4255,27 @@ function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
1946
4255
  const normalized = (modulePath || "").replace(/\\/g, "/");
1947
4256
  const srcFolderPaths = srcFolders.flatMap(
1948
4257
  (sf) => normalized ? [
1949
- path8.resolve(workspaceRoot, sf, normalized + ".js"),
1950
- path8.resolve(workspaceRoot, sf, normalized + ".ts"),
1951
- path8.resolve(workspaceRoot, sf, normalized + ".cjs"),
1952
- path8.resolve(workspaceRoot, sf, normalized)
4258
+ path9.resolve(workspaceRoot, sf, normalized + ".js"),
4259
+ path9.resolve(workspaceRoot, sf, normalized + ".ts"),
4260
+ path9.resolve(workspaceRoot, sf, normalized + ".cjs"),
4261
+ path9.resolve(workspaceRoot, sf, normalized)
1953
4262
  ] : []
1954
4263
  );
1955
4264
  const possiblePaths = [
1956
4265
  ...srcFolderPaths,
1957
4266
  // Treat module path as relative to cwd (works when src_folders isn't nested)
1958
4267
  ...normalized ? [
1959
- path8.resolve(workspaceRoot, normalized + ".js"),
1960
- path8.resolve(workspaceRoot, normalized + ".ts"),
1961
- path8.resolve(workspaceRoot, normalized + ".cjs"),
1962
- path8.resolve(workspaceRoot, normalized)
4268
+ path9.resolve(workspaceRoot, normalized + ".js"),
4269
+ path9.resolve(workspaceRoot, normalized + ".ts"),
4270
+ path9.resolve(workspaceRoot, normalized + ".cjs"),
4271
+ path9.resolve(workspaceRoot, normalized)
1963
4272
  ] : [],
1964
- path8.resolve(workspaceRoot, "tests", testFile + ".js"),
1965
- path8.resolve(workspaceRoot, "test", testFile + ".js"),
1966
- path8.resolve(workspaceRoot, testFile + ".js")
4273
+ path9.resolve(workspaceRoot, "tests", testFile + ".js"),
4274
+ path9.resolve(workspaceRoot, "test", testFile + ".js"),
4275
+ path9.resolve(workspaceRoot, testFile + ".js")
1967
4276
  ];
1968
4277
  for (const candidate of possiblePaths) {
1969
- if (fs10.existsSync(candidate)) {
4278
+ if (fs11.existsSync(candidate)) {
1970
4279
  return candidate;
1971
4280
  }
1972
4281
  }
@@ -2182,6 +4491,18 @@ import logger10 from "@wdio/logger";
2182
4491
  import http from "http";
2183
4492
  import logger6 from "@wdio/logger";
2184
4493
 
4494
+ // src/action-snapshot.ts
4495
+ function captureActionSnapshot2(browser, command, takeScreenshot) {
4496
+ const b = browser;
4497
+ return captureActionSnapshot({
4498
+ command,
4499
+ runScript: (src) => browser.execute(`return (${src})`),
4500
+ takeScreenshot,
4501
+ getUrl: () => b.getCurrentUrl ? b.getCurrentUrl().catch(() => void 0) : Promise.resolve(void 0),
4502
+ getTitle: () => b.getTitle ? b.getTitle().catch(() => void 0) : Promise.resolve(void 0)
4503
+ });
4504
+ }
4505
+
2185
4506
  // src/helpers/perfLogs.ts
2186
4507
  function applyResponseReceived(p, response) {
2187
4508
  const responseHeaders = {};
@@ -2310,7 +4631,7 @@ function unwrapDriverValue(result) {
2310
4631
  return result;
2311
4632
  }
2312
4633
  var getProp = (obj, key) => obj && typeof obj === "object" ? obj[key] : void 0;
2313
- var getPath = (obj, ...path9) => path9.reduce((acc, k) => getProp(acc, k), obj);
4634
+ var getPath = (obj, ...path10) => path10.reduce((acc, k) => getProp(acc, k), obj);
2314
4635
  var firstDefined = (obj, ...keys) => {
2315
4636
  if (!obj || typeof obj !== "object") {
2316
4637
  return void 0;
@@ -2347,6 +4668,10 @@ var SessionCapturer = class extends SessionCapturerBase {
2347
4668
  // True once BiDi inspectors are attached — the per-command perf-log network
2348
4669
  // capture path skips when set, so we don't double-emit network requests.
2349
4670
  bidiActive = false;
4671
+ // Populated by captureCommand when mode === 'trace' (set by the plugin).
4672
+ traceMode = "live";
4673
+ actionSnapshots = [];
4674
+ snapshotCaptures = [];
2350
4675
  constructor(devtoolsOptions = {}, browser) {
2351
4676
  super(devtoolsOptions);
2352
4677
  this.#browser = browser;
@@ -2362,15 +4687,6 @@ var SessionCapturer = class extends SessionCapturerBase {
2362
4687
  onWsClose() {
2363
4688
  log6.info("Worker WebSocket disconnected");
2364
4689
  }
2365
- /**
2366
- * Push every captured line into the local `consoleLogs` array so it ends up
2367
- * in any future trace export, in addition to the live WS broadcast.
2368
- */
2369
- onLine(type, args, source) {
2370
- const entry = createConsoleLogEntry(type, args, source);
2371
- this.consoleLogs.push(entry);
2372
- this.sendUpstream("consoleLogs", [entry]);
2373
- }
2374
4690
  async captureCommand(command, args, result, error, testUid, callSource, timestamp) {
2375
4691
  const serializedError = serializeError(error);
2376
4692
  const commandId = this.commandCounter++;
@@ -2393,6 +4709,20 @@ var SessionCapturer = class extends SessionCapturerBase {
2393
4709
  log6.warn(`Failed to capture performance data: ${errorMessage(err)}`);
2394
4710
  });
2395
4711
  }
4712
+ if (this.traceMode === "trace" && !error && this.#browser && mapCommandToAction(command)) {
4713
+ const browser = this.#browser;
4714
+ this.snapshotCaptures.push(
4715
+ captureActionSnapshot2(
4716
+ browser,
4717
+ command,
4718
+ () => this.takeScreenshotViaHttp(browser)
4719
+ ).then((snap) => {
4720
+ if (snap) {
4721
+ this.actionSnapshots.push(snap);
4722
+ }
4723
+ })
4724
+ );
4725
+ }
2396
4726
  return true;
2397
4727
  }
2398
4728
  async #capturePerformanceData(commandLogEntry, args) {
@@ -2589,13 +4919,6 @@ var SessionCapturer = class extends SessionCapturerBase {
2589
4919
  async captureTrace(browser) {
2590
4920
  await this.captureNetworkFromPerformanceLogs(browser);
2591
4921
  try {
2592
- const checkResult = await browser.execute(
2593
- 'return typeof window.wdioTraceCollector !== "undefined"'
2594
- );
2595
- const collectorExists = unwrapDriverValue(checkResult) === true;
2596
- if (!collectorExists) {
2597
- return;
2598
- }
2599
4922
  const result = await browser.execute(`
2600
4923
  if (typeof window.wdioTraceCollector === 'undefined') {
2601
4924
  return null;
@@ -3043,8 +5366,8 @@ var BrowserProxy = class {
3043
5366
  getCurrentTestFullPath() {
3044
5367
  return this.currentTestFullPath;
3045
5368
  }
3046
- setCurrentTestFullPath(path9) {
3047
- this.currentTestFullPath = path9;
5369
+ setCurrentTestFullPath(path10) {
5370
+ this.currentTestFullPath = path10;
3048
5371
  }
3049
5372
  /**
3050
5373
  * Wrap browser navigation methods (url / navigate / navigateTo) to inject
@@ -3397,7 +5720,7 @@ function broadcastSessionMetadata(ctx, browser) {
3397
5720
  const sf = opts.src_folders;
3398
5721
  ctx.srcFolders = Array.isArray(sf) ? sf : sf ? [sf] : [];
3399
5722
  }
3400
- ctx.sessionCapturer.sendUpstream("metadata", {
5723
+ const metadata = {
3401
5724
  type: "testrunner" /* Testrunner */,
3402
5725
  capabilities,
3403
5726
  desiredCapabilities,
@@ -3406,7 +5729,9 @@ function broadcastSessionMetadata(ctx, browser) {
3406
5729
  host: opts.webdriver?.host,
3407
5730
  options: ctx.buildMetadataOptions(),
3408
5731
  url: ""
3409
- });
5732
+ };
5733
+ ctx.sessionCapturer.metadata = metadata;
5734
+ ctx.sessionCapturer.sendUpstream("metadata", metadata);
3410
5735
  const browserName = capabilities.browserName || desiredCapabilities.browserName || "unknown";
3411
5736
  const browserVersion = capabilities.browserVersion || capabilities.version || "";
3412
5737
  log10.info(
@@ -3465,12 +5790,15 @@ async function ensureSessionInitialized(ctx, browser, finalizeCurrentScreencast2
3465
5790
  (resolve2) => setTimeout(resolve2, TIMING.INITIAL_CONNECTION_WAIT)
3466
5791
  );
3467
5792
  ctx.sessionCapturer = new SessionCapturer(
3468
- { port: ctx.port, hostname: ctx.hostname },
5793
+ ctx.mode === "trace" ? {} : { port: ctx.port, hostname: ctx.hostname },
3469
5794
  browser
3470
5795
  );
3471
- const connected = await ctx.sessionCapturer.waitForConnection(3e3);
3472
- if (!connected) {
3473
- log10.error("\u274C Worker WebSocket failed to connect!");
5796
+ ctx.sessionCapturer.traceMode = ctx.mode;
5797
+ if (ctx.mode !== "trace") {
5798
+ const connected = await ctx.sessionCapturer.waitForConnection(3e3);
5799
+ if (!connected) {
5800
+ log10.error("\u274C Worker WebSocket failed to connect!");
5801
+ }
3474
5802
  }
3475
5803
  if (!ctx.testReporter) {
3476
5804
  initReporterChain(ctx);
@@ -3532,16 +5860,21 @@ var NightwatchDevToolsPlugin = class {
3532
5860
  #bidiEnabled = false;
3533
5861
  #bidiAttachAttempted = false;
3534
5862
  constructor(options = {}) {
5863
+ const mode = options.mode ?? "live";
5864
+ const ignore = mode === "trace" && options.screencast?.enabled === true;
5865
+ if (ignore) {
5866
+ log11.warn("trace mode: ignoring screencast option (live-mode feature)");
5867
+ }
5868
+ const screencast = ignore ? {} : options.screencast ?? {};
3535
5869
  this.options = {
3536
5870
  port: options.port ?? 3e3,
3537
5871
  hostname: options.hostname ?? "localhost",
3538
- screencast: options.screencast ?? {},
3539
- bidi: options.bidi ?? false
3540
- };
3541
- this.#screencastOptions = {
3542
- ...SCREENCAST_DEFAULTS,
3543
- ...options.screencast ?? {}
5872
+ screencast,
5873
+ bidi: options.bidi ?? false,
5874
+ mode,
5875
+ traceFormat: options.traceFormat ?? "zip"
3544
5876
  };
5877
+ this.#screencastOptions = { ...SCREENCAST_DEFAULTS, ...screencast };
3545
5878
  this.#bidiEnabled = options.bidi === true;
3546
5879
  }
3547
5880
  // Single internals "bag" — structurally satisfies all 4 lifecycle ctx
@@ -3565,6 +5898,9 @@ var NightwatchDevToolsPlugin = class {
3565
5898
  get port() {
3566
5899
  return self.options.port;
3567
5900
  },
5901
+ get mode() {
5902
+ return self.options.mode;
5903
+ },
3568
5904
  get screencastOptions() {
3569
5905
  return self.#screencastOptions;
3570
5906
  },
@@ -3827,6 +6163,12 @@ var NightwatchDevToolsPlugin = class {
3827
6163
  try {
3828
6164
  await this.#finalizeAllSuites(browser);
3829
6165
  this.#logRunSummary();
6166
+ if (this.options.mode === "trace") {
6167
+ await this.#writeTraceZipIfNeeded();
6168
+ await this.sessionCapturer?.closeWebSocket();
6169
+ await stopBackend();
6170
+ return;
6171
+ }
3830
6172
  if (!this.#devtoolsBrowser) {
3831
6173
  this.testReporter?.updateSuites();
3832
6174
  log11.info("\u267B Rerun complete \u2014 flushing WebSocket...");
@@ -3845,6 +6187,32 @@ var NightwatchDevToolsPlugin = class {
3845
6187
  #logRunSummary() {
3846
6188
  logRunSummary(this.#getInternals());
3847
6189
  }
6190
+ async #writeTraceZipIfNeeded() {
6191
+ if (this.options.mode !== "trace" || !this.sessionCapturer) {
6192
+ return;
6193
+ }
6194
+ const sessionId = this.sessionCapturer.metadata?.sessionId;
6195
+ if (!sessionId) {
6196
+ return;
6197
+ }
6198
+ try {
6199
+ if (this.sessionCapturer.snapshotCaptures.length) {
6200
+ await Promise.allSettled(this.sessionCapturer.snapshotCaptures);
6201
+ }
6202
+ const snapshots = this.sessionCapturer.actionSnapshots;
6203
+ const tracePath = await writeTraceZip(this.sessionCapturer, {
6204
+ outputDir: resolveAdapterOutputDir({
6205
+ configPath: this.#configPath
6206
+ }),
6207
+ sessionId,
6208
+ actionSnapshots: snapshots.length ? snapshots : void 0,
6209
+ format: this.options.traceFormat
6210
+ });
6211
+ log11.info(`Trace saved to ${tracePath}`);
6212
+ } catch (err) {
6213
+ log11.warn(`trace write failed: ${errorMessage(err)}`);
6214
+ }
6215
+ }
3848
6216
  async #waitForDevtoolsBrowserClose() {
3849
6217
  await waitForDevtoolsBrowserClose(this.#getInternals());
3850
6218
  }