@wdio/nightwatch-devtools 1.2.0 → 1.4.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,462 @@ 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, testMetadata) {
2064
+ const events = [];
2065
+ let prevEndMs = 0;
2066
+ let callCounter = 0;
2067
+ let lastTestUid;
2068
+ let groupCallId;
2069
+ for (const cmd of commands) {
2070
+ const action = mapCommandToAction(cmd.command);
2071
+ if (!action) {
2072
+ continue;
2073
+ }
2074
+ if (cmd.testUid && cmd.testUid !== lastTestUid) {
2075
+ if (lastTestUid && groupCallId) {
2076
+ callCounter++;
2077
+ events.push({
2078
+ type: "after",
2079
+ callId: groupCallId,
2080
+ endTime: prevEndMs
2081
+ });
2082
+ }
2083
+ callCounter++;
2084
+ groupCallId = `call@${callCounter}`;
2085
+ const meta = testMetadata?.get(cmd.testUid);
2086
+ const groupName = meta?.title ?? cmd.testUid;
2087
+ events.push({
2088
+ type: "before",
2089
+ callId: groupCallId,
2090
+ startTime: Math.max(
2091
+ prevEndMs,
2092
+ (cmd.startTime ?? cmd.timestamp) - wallTime
2093
+ ),
2094
+ class: "Tracing",
2095
+ method: "tracingGroup",
2096
+ pageId,
2097
+ params: { name: groupName },
2098
+ title: groupName,
2099
+ apiName: "tracing.tracingGroup"
2100
+ });
2101
+ lastTestUid = cmd.testUid;
2102
+ }
2103
+ callCounter++;
2104
+ const callId = `call@${callCounter}`;
2105
+ const rawStartMs = (cmd.startTime ?? cmd.timestamp) - wallTime;
2106
+ const rawEndMs = cmd.timestamp - wallTime;
2107
+ const startMs = Math.max(prevEndMs, rawStartMs);
2108
+ const endMs = Math.max(startMs + 1, rawEndMs);
2109
+ const rawArgs = cmd.args;
2110
+ let params;
2111
+ const isValueMethod = FILL_METHODS.has(action.method);
2112
+ if (action.class === "Element" && isValueMethod && rawArgs.length >= 2) {
2113
+ params = { selector: rawArgs[0], value: rawArgs[1] };
2114
+ } else if (action.class === "Element" && isValueMethod && rawArgs.length === 1) {
2115
+ params = { value: rawArgs[0] };
2116
+ } else if (action.class === "Element" && rawArgs.length === 1 && typeof rawArgs[0] === "string") {
2117
+ params = { selector: rawArgs[0] };
2118
+ } else if (rawArgs.length === 1 && typeof rawArgs[0] === "string") {
2119
+ params = { url: rawArgs[0] };
2120
+ } else {
2121
+ params = Object.fromEntries(rawArgs.map((a, i) => [String(i), a]));
2122
+ }
2123
+ events.push({
2124
+ type: "before",
2125
+ callId,
2126
+ startTime: startMs,
2127
+ class: action.class,
2128
+ method: action.method,
2129
+ pageId,
2130
+ params,
2131
+ title: formatActionTitle(action, cmd.args, params),
2132
+ apiName: `${action.class.toLowerCase()}.${action.method}`,
2133
+ parentId: groupCallId
2134
+ });
2135
+ const afterEvent = {
2136
+ type: "after",
2137
+ callId,
2138
+ endTime: endMs
2139
+ };
2140
+ if (cmd.error) {
2141
+ const err = cmd.error;
2142
+ afterEvent.error = { message: err.message ?? String(cmd.error) };
2143
+ }
2144
+ events.push(afterEvent);
2145
+ prevEndMs = endMs;
2146
+ }
2147
+ if (lastTestUid && groupCallId) {
2148
+ callCounter++;
2149
+ events.push({
2150
+ type: "after",
2151
+ callId: groupCallId,
2152
+ endTime: prevEndMs
2153
+ });
2154
+ }
2155
+ return events;
2156
+ }
2157
+ function buildNetworkNdjson(requests, wallTime, pageId) {
2158
+ if (!requests.length) {
2159
+ return Buffer.alloc(0);
2160
+ }
2161
+ const lines = requests.map((r) => {
2162
+ const entry = networkRequestToHar(r);
2163
+ entry.snapshot = {
2164
+ ...entry.snapshot,
2165
+ // Monotonic offset so the viewer positions bars on the timeline.
2166
+ _monotonicTime: Math.max(0, r.timestamp - wallTime),
2167
+ // Browsing context ID so the viewer associates requests with the page.
2168
+ _frameref: pageId
2169
+ };
2170
+ return JSON.stringify(entry);
2171
+ });
2172
+ return Buffer.from(lines.join("\n"), "utf8");
2173
+ }
2174
+ function buildSnapshotResources(snapshots, pageId) {
2175
+ const out = [];
2176
+ for (const snap of snapshots) {
2177
+ const base = `${pageId}-${snap.timestamp}`;
2178
+ if (snap.screenshot) {
2179
+ out.push({
2180
+ resourceName: `${base}.jpeg`,
2181
+ data: Buffer.from(snap.screenshot, "base64")
2182
+ });
2183
+ }
2184
+ if (snap.elements && snap.elements.length) {
2185
+ out.push({
2186
+ resourceName: `${base}-elements.json`,
2187
+ data: Buffer.from(JSON.stringify(snap.elements), "utf8")
2188
+ });
2189
+ }
2190
+ if (snap.snapshotText) {
2191
+ out.push({
2192
+ resourceName: `${base}-snapshot.txt`,
2193
+ data: Buffer.from(snap.snapshotText, "utf8")
2194
+ });
2195
+ }
2196
+ }
2197
+ return out;
2198
+ }
2199
+ function buildScreencastFrames(snapshots, pageId, wallTime, viewport) {
2200
+ return snapshots.filter((s) => s.screenshot).map((s) => {
2201
+ const base = `${pageId}-${s.timestamp}`;
2202
+ const frame = {
2203
+ type: "screencast-frame",
2204
+ pageId,
2205
+ sha1: `${base}.jpeg`,
2206
+ width: viewport.width,
2207
+ height: viewport.height,
2208
+ timestamp: Math.max(0, s.timestamp - wallTime)
2209
+ };
2210
+ if (s.elements && s.elements.length) {
2211
+ frame.elements = `${base}-elements.json`;
2212
+ }
2213
+ if (s.snapshotText) {
2214
+ frame.snapshot = `${base}-snapshot.txt`;
2215
+ }
2216
+ return frame;
2217
+ });
2218
+ }
2219
+ function eventTime(e) {
2220
+ switch (e.type) {
2221
+ case "context-options":
2222
+ return -Infinity;
2223
+ case "before":
2224
+ return e.startTime;
2225
+ case "after":
2226
+ return e.endTime;
2227
+ case "screencast-frame":
2228
+ return e.timestamp;
2229
+ }
2230
+ }
2231
+ function eventOrder(e) {
2232
+ switch (e.type) {
2233
+ case "context-options":
2234
+ return 0;
2235
+ case "after":
2236
+ return 1;
2237
+ case "screencast-frame":
2238
+ return 2;
2239
+ case "before":
2240
+ return 3;
2241
+ }
2242
+ }
2243
+ function compareEvents(a, b) {
2244
+ const dt = eventTime(a) - eventTime(b);
2245
+ return dt !== 0 ? dt : eventOrder(a) - eventOrder(b);
2246
+ }
2247
+ function generateTranscript(commands, startWallTime, title) {
2248
+ const wallTimeISO = new Date(startWallTime).toISOString();
2249
+ const lines = [`# ${title ?? "Session"} \u2014 ${wallTimeISO}`, ""];
2250
+ const captured = [];
2251
+ for (const c of commands) {
2252
+ const action = mapCommandToAction(String(c.command));
2253
+ if (action) {
2254
+ captured.push({ entry: c, action });
2255
+ }
2256
+ }
2257
+ captured.forEach(({ entry, action }, idx) => {
2258
+ const label = formatActionTitle(action, entry.args);
2259
+ const rawArgs = entry.args;
2260
+ const parts = [`${idx + 1}. ${label}`];
2261
+ if (FILL_METHODS.has(action.method) && rawArgs) {
2262
+ const valueIdx = rawArgs.length >= 2 ? 1 : 0;
2263
+ if (rawArgs[valueIdx] !== void 0) {
2264
+ parts.push(`value="${String(rawArgs[valueIdx])}"`);
2265
+ }
2266
+ }
2267
+ if (entry.error) {
2268
+ const msg = typeof entry.error === "object" && "message" in entry.error ? entry.error.message : String(entry.error);
2269
+ parts.push(`ERROR: ${msg}`);
2270
+ }
2271
+ lines.push(parts.join(" "));
2272
+ });
2273
+ return lines.join("\n");
2274
+ }
2275
+ function buildTraceBundle(trace, opts = {}) {
2276
+ const firstCommandTs = trace.commands[0]?.timestamp;
2277
+ const wallTime = opts.wallTimeOverride ?? firstCommandTs ?? Date.now();
2278
+ const idPrefix = shortId(opts.sessionId);
2279
+ const contextId = `context@${idPrefix}`;
2280
+ const pageId = `page@${idPrefix}`;
2281
+ const viewport = trace.metadata.viewport ?? { width: 1280, height: 720 };
2282
+ const snapshots = trace.actionSnapshots ?? [];
2283
+ const ctxOptions = buildContextOptions(trace, contextId, wallTime);
2284
+ const events = [ctxOptions];
2285
+ const firstSnap = snapshots.find((s) => s.screenshot);
2286
+ if (firstSnap) {
2287
+ const base = `${pageId}-${firstSnap.timestamp}`;
2288
+ const initFrame = {
2289
+ type: "screencast-frame",
2290
+ pageId,
2291
+ sha1: `${base}.jpeg`,
2292
+ width: viewport.width,
2293
+ height: viewport.height,
2294
+ timestamp: 0
2295
+ };
2296
+ if (firstSnap.elements && firstSnap.elements.length) {
2297
+ initFrame.elements = `${base}-elements.json`;
2298
+ }
2299
+ if (firstSnap.snapshotText) {
2300
+ initFrame.snapshot = `${base}-snapshot.txt`;
2301
+ }
2302
+ events.push(initFrame);
2303
+ }
2304
+ events.push(
2305
+ ...buildScreencastFrames(
2306
+ firstSnap ? snapshots.filter((s) => s !== firstSnap) : snapshots,
2307
+ pageId,
2308
+ wallTime,
2309
+ viewport
2310
+ ),
2311
+ ...buildActionEvents(trace.commands, pageId, wallTime, opts.testMetadata)
2312
+ );
2313
+ events.sort(compareEvents);
2314
+ const ctxBName = ctxOptions.title;
2315
+ return {
2316
+ traceNdjson: events.map((e) => JSON.stringify(e)).join("\n") + "\n",
2317
+ networkNdjson: buildNetworkNdjson(trace.networkRequests, wallTime, pageId),
2318
+ transcriptMd: generateTranscript(trace.commands, wallTime, ctxBName),
2319
+ resources: buildSnapshotResources(snapshots, pageId)
2320
+ };
2321
+ }
2322
+ async function exportTraceZip(trace, opts = {}) {
2323
+ const bundle = buildTraceBundle(trace, opts);
2324
+ return buildTraceZip({
2325
+ traceNdjson: bundle.traceNdjson,
2326
+ networkNdjson: bundle.networkNdjson,
2327
+ resources: bundle.resources,
2328
+ transcriptMd: bundle.transcriptMd
2329
+ });
2330
+ }
2331
+ async function exportTraceDirectory(trace, targetDir, opts = {}) {
2332
+ const bundle = buildTraceBundle(trace, opts);
2333
+ await fs.mkdir(path.join(targetDir, "resources"), { recursive: true });
2334
+ await Promise.all([
2335
+ fs.writeFile(path.join(targetDir, "trace.trace"), bundle.traceNdjson),
2336
+ fs.writeFile(
2337
+ path.join(targetDir, "transcript.md"),
2338
+ bundle.transcriptMd,
2339
+ "utf8"
2340
+ ),
2341
+ bundle.networkNdjson.length ? fs.writeFile(
2342
+ path.join(targetDir, "trace.network"),
2343
+ bundle.networkNdjson
2344
+ ) : Promise.resolve(),
2345
+ ...bundle.resources.map(
2346
+ (r) => fs.writeFile(path.join(targetDir, "resources", r.resourceName), r.data)
2347
+ )
2348
+ ]);
2349
+ }
2350
+ async function writeTraceZip(capturer, opts) {
2351
+ const baseMetadata = capturer.metadata ?? {};
2352
+ const actionSnapshots = opts.actionSnapshots ?? synthesizeSnapshotsFromCommands(capturer.commandsLog);
2353
+ const traceLog = {
2354
+ mutations: capturer.mutations,
2355
+ logs: capturer.traceLogs,
2356
+ consoleLogs: capturer.consoleLogs,
2357
+ networkRequests: capturer.networkRequests,
2358
+ metadata: {
2359
+ ...baseMetadata,
2360
+ ...opts.capabilities ? { capabilities: opts.capabilities } : {}
2361
+ },
2362
+ commands: capturer.commandsLog,
2363
+ sources: Object.fromEntries(capturer.sources),
2364
+ ...actionSnapshots.length ? { actionSnapshots } : {}
2365
+ };
2366
+ await fs.mkdir(opts.outputDir, { recursive: true });
2367
+ const exportOpts = {
2368
+ sessionId: opts.sessionId,
2369
+ wallTimeOverride: capturer.startWallTime,
2370
+ testMetadata: opts.testMetadata
2371
+ };
2372
+ if (opts.format === "ndjson-directory") {
2373
+ const dir = path.join(opts.outputDir, `trace-${opts.sessionId}`);
2374
+ await fs.mkdir(dir, { recursive: true });
2375
+ await exportTraceDirectory(traceLog, dir, exportOpts);
2376
+ return dir;
2377
+ }
2378
+ const zip = await exportTraceZip(traceLog, exportOpts);
2379
+ const zipPath = path.join(opts.outputDir, `trace-${opts.sessionId}.zip`);
2380
+ await fs.writeFile(zipPath, zip);
2381
+ return zipPath;
2382
+ }
2383
+ function synthesizeSnapshotsFromCommands(commands) {
2384
+ return commands.filter((c) => c.screenshot && mapCommandToAction(c.command)).map((c) => ({
2385
+ timestamp: c.timestamp,
2386
+ command: c.command,
2387
+ screenshot: c.screenshot
2388
+ }));
2389
+ }
2390
+
47
2391
  // ../core/src/uid.ts
48
2392
  function deterministicUid(...parts) {
49
2393
  const hash = parts.join("::").split("").reduce((acc, char) => (acc << 5) - acc + char.charCodeAt(0) | 0, 0);
@@ -62,13 +2406,13 @@ function resetSignatureCounters() {
62
2406
  }
63
2407
 
64
2408
  // ../core/src/finalize-screencast.ts
65
- import fs2 from "fs";
2409
+ import fs3 from "fs";
66
2410
  import os2 from "os";
67
- import path2 from "path";
2411
+ import path3 from "path";
68
2412
 
69
2413
  // ../core/src/video-encoder.ts
70
- import fs from "fs/promises";
71
- import path from "path";
2414
+ import fs2 from "fs/promises";
2415
+ import path2 from "path";
72
2416
  import os from "os";
73
2417
  import { createRequire } from "module";
74
2418
  var require2 = createRequire(import.meta.url);
@@ -85,20 +2429,20 @@ async function writeFramesAndManifest(frames, tmpDir, ext) {
85
2429
  const manifestLines = ["ffconcat version 1.0"];
86
2430
  for (let i = 0; i < frames.length; i++) {
87
2431
  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"));
2432
+ const framePath = path2.join(tmpDir, frameName);
2433
+ await fs2.writeFile(framePath, Buffer.from(frames[i].data, "base64"));
90
2434
  const nextTs = frames[i + 1]?.timestamp ?? frames[i].timestamp + 100;
91
2435
  const durationSecs = Math.max((nextTs - frames[i].timestamp) / 1e3, 0.01);
92
2436
  manifestLines.push(`file '${framePath}'`);
93
2437
  manifestLines.push(`duration ${durationSecs.toFixed(6)}`);
94
2438
  }
95
- const lastFramePath = path.join(
2439
+ const lastFramePath = path2.join(
96
2440
  tmpDir,
97
2441
  `frame-${String(frames.length - 1).padStart(6, "0")}.${ext}`
98
2442
  );
99
2443
  manifestLines.push(`file '${lastFramePath}'`);
100
- const manifestPath = path.join(tmpDir, "manifest.txt");
101
- await fs.writeFile(manifestPath, manifestLines.join("\n"));
2444
+ const manifestPath = path2.join(tmpDir, "manifest.txt");
2445
+ await fs2.writeFile(manifestPath, manifestLines.join("\n"));
102
2446
  return manifestPath;
103
2447
  }
104
2448
  function classifyFfmpegError(err) {
@@ -121,6 +2465,15 @@ function runFfmpeg(ffmpeg, manifestPath, outputPath) {
121
2465
  "cfr",
122
2466
  "-r",
123
2467
  "10",
2468
+ // Force a keyframe every frame. VP8 can only seek to keyframes, and
2469
+ // libvpx's default GOP places a single keyframe on near-static
2470
+ // screencast content — so scrubber/marker seeks snap back to the
2471
+ // start. At 10fps over short recordings, all-intra keeps seeking
2472
+ // frame-accurate at an acceptable size.
2473
+ "-g",
2474
+ "1",
2475
+ "-keyint_min",
2476
+ "1",
124
2477
  "-auto-alt-ref",
125
2478
  "0",
126
2479
  "-disposition:v",
@@ -134,14 +2487,14 @@ async function encodeToVideo(frames, outputPath, options = {}) {
134
2487
  }
135
2488
  const ffmpeg = loadFfmpeg();
136
2489
  const ext = options.captureFormat === "png" ? "png" : "jpg";
137
- const tmpDir = await fs.mkdtemp(
138
- path.join(os.tmpdir(), "devtools-screencast-")
2490
+ const tmpDir = await fs2.mkdtemp(
2491
+ path2.join(os.tmpdir(), "devtools-screencast-")
139
2492
  );
140
2493
  try {
141
2494
  const manifestPath = await writeFramesAndManifest(frames, tmpDir, ext);
142
2495
  await runFfmpeg(ffmpeg, manifestPath, outputPath);
143
2496
  } finally {
144
- await fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {
2497
+ await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
145
2498
  });
146
2499
  }
147
2500
  }
@@ -170,11 +2523,11 @@ async function finalizeScreencast({
170
2523
  }
171
2524
  const fileName = `${filenamePrefix}-${sessionId}.webm`;
172
2525
  const candidate = outputDir || process.cwd();
173
- let videoPath = path2.join(candidate, fileName);
2526
+ let videoPath = path3.join(candidate, fileName);
174
2527
  try {
175
- fs2.accessSync(candidate, fs2.constants.W_OK);
2528
+ fs3.accessSync(candidate, fs3.constants.W_OK);
176
2529
  } catch {
177
- videoPath = path2.join(os2.tmpdir(), fileName);
2530
+ videoPath = path3.join(os2.tmpdir(), fileName);
178
2531
  }
179
2532
  try {
180
2533
  await encodeToVideo(frames, videoPath, { captureFormat });
@@ -184,7 +2537,8 @@ async function finalizeScreencast({
184
2537
  videoPath,
185
2538
  videoFile: fileName,
186
2539
  frameCount: frames.length,
187
- duration: recorder.duration
2540
+ duration: recorder.duration,
2541
+ startTime: frames[0]?.timestamp
188
2542
  });
189
2543
  } catch (err) {
190
2544
  log12("warn", `Screencast encode failed: ${errorMessage(err)}`);
@@ -192,12 +2546,12 @@ async function finalizeScreencast({
192
2546
  }
193
2547
 
194
2548
  // ../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}`;
2549
+ import fs4 from "fs";
2550
+ import path4 from "path";
2551
+ var NODE_MODULES_SEGMENT = `${path4.sep}node_modules${path4.sep}`;
198
2552
  function isWritable(dir) {
199
2553
  try {
200
- fs3.accessSync(dir, fs3.constants.W_OK);
2554
+ fs4.accessSync(dir, fs4.constants.W_OK);
201
2555
  return true;
202
2556
  } catch {
203
2557
  return false;
@@ -210,10 +2564,10 @@ function resolveAdapterOutputDir(input = {}) {
210
2564
  }
211
2565
  const candidates = [];
212
2566
  if (input.testFilePath) {
213
- candidates.push(path3.dirname(input.testFilePath));
2567
+ candidates.push(path4.dirname(input.testFilePath));
214
2568
  }
215
2569
  if (input.configPath) {
216
- candidates.push(path3.dirname(input.configPath));
2570
+ candidates.push(path4.dirname(input.configPath));
217
2571
  }
218
2572
  candidates.push(fallback);
219
2573
  for (const dir of candidates) {
@@ -582,15 +2936,15 @@ var ScreencastRecorderBase = class {
582
2936
  };
583
2937
 
584
2938
  // ../core/src/script-loader.ts
585
- import fs4 from "fs/promises";
586
- import path4 from "path";
2939
+ import fs5 from "fs/promises";
2940
+ import path5 from "path";
587
2941
  import { createRequire as createRequire2 } from "module";
588
2942
  var require3 = createRequire2(import.meta.url);
589
2943
  async function loadInjectableScript() {
590
2944
  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");
2945
+ const scriptDir = path5.dirname(scriptPath);
2946
+ const preloadScriptPath = path5.join(scriptDir, "script.js");
2947
+ const scriptContent = await fs5.readFile(preloadScriptPath, "utf-8");
594
2948
  return `(async function() { ${scriptContent} })()`;
595
2949
  }
596
2950
  async function pollUntilReady(check, opts = {}) {
@@ -606,7 +2960,7 @@ async function pollUntilReady(check, opts = {}) {
606
2960
  }
607
2961
 
608
2962
  // ../core/src/session-capturer.ts
609
- import fs5 from "fs/promises";
2963
+ import fs6 from "fs/promises";
610
2964
  import { WebSocket } from "ws";
611
2965
  var SessionCapturerBase = class {
612
2966
  // ── State (mostly private; subclasses access shared ws via `this.ws`) ────
@@ -648,9 +3002,12 @@ var SessionCapturerBase = class {
648
3002
  metadata;
649
3003
  // ── Construction ────────────────────────────────────────────────────────
650
3004
  constructor(opts = {}) {
651
- const { hostname, port } = opts;
3005
+ const { hostname, port, reconnect } = opts;
652
3006
  if (hostname && port) {
653
- this.ws = new WebSocket(`ws://${hostname}:${port}${WS_PATHS.worker}`);
3007
+ const query = reconnect ? "?reconnect=1" : "";
3008
+ this.ws = new WebSocket(
3009
+ `ws://${hostname}:${port}${WS_PATHS.worker}${query}`
3010
+ );
654
3011
  this.ws.on("open", () => {
655
3012
  this.#hasConnected = true;
656
3013
  this.onWsOpen();
@@ -756,7 +3113,7 @@ var SessionCapturerBase = class {
756
3113
  return;
757
3114
  }
758
3115
  try {
759
- const source = (await fs5.readFile(filePath, "utf-8")).toString();
3116
+ const source = (await fs6.readFile(filePath, "utf-8")).toString();
760
3117
  this.sources.set(filePath, source);
761
3118
  this.sendUpstream("sources", { [filePath]: source });
762
3119
  } catch (err) {
@@ -938,17 +3295,17 @@ var SessionCapturerBase = class {
938
3295
  }
939
3296
  // ── Hooks (subclasses override) ─────────────────────────────────────────
940
3297
  /**
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.
3298
+ * Build a ConsoleLog entry from the captured line, push it into the local
3299
+ * `consoleLogs` array (so it ends up in any future trace export), and
3300
+ * broadcast it live via the `consoleLogs` WS scope.
945
3301
  *
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.
3302
+ * Args is passed as an array (matching the original console.* call shape:
3303
+ * `console.log('a', 'b')` → `args = ['a', 'b']`) so the multi-argument
3304
+ * structure is preserved for the UI.
949
3305
  */
950
3306
  onLine(type, args, source) {
951
3307
  const entry = createConsoleLogEntry(type, args, source);
3308
+ this.consoleLogs.push(entry);
952
3309
  this.sendUpstream("consoleLogs", [entry]);
953
3310
  }
954
3311
  /**
@@ -982,6 +3339,100 @@ function safeStringify(value) {
982
3339
  }
983
3340
  }
984
3341
 
3342
+ // ../core/src/spec-trace-helpers.ts
3343
+ function sanitizeSpecName(specFile) {
3344
+ return specFile.replace(/^.*[/\\]/, "").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || "unknown-spec";
3345
+ }
3346
+ function buildSpecSessionId(specFile, sessionId) {
3347
+ const base = sanitizeSpecName(specFile);
3348
+ const hash = deterministicUid(specFile).split("-").pop().slice(0, 8);
3349
+ return `${base}-${hash}-${sessionId.slice(0, 8)}`;
3350
+ }
3351
+ function buildSpecCapturer(capturer, range, nextRange) {
3352
+ const end = nextRange ? {
3353
+ commands: nextRange.commandStartIdx,
3354
+ console: nextRange.consoleStartIdx,
3355
+ network: nextRange.networkStartIdx,
3356
+ mutations: nextRange.mutationStartIdx,
3357
+ traceLogs: nextRange.traceLogStartIdx
3358
+ } : void 0;
3359
+ return {
3360
+ mutations: capturer.mutations.slice(range.mutationStartIdx, end?.mutations),
3361
+ traceLogs: capturer.traceLogs.slice(range.traceLogStartIdx, end?.traceLogs),
3362
+ consoleLogs: capturer.consoleLogs.slice(
3363
+ range.consoleStartIdx,
3364
+ end?.console
3365
+ ),
3366
+ networkRequests: capturer.networkRequests.slice(
3367
+ range.networkStartIdx,
3368
+ end?.network
3369
+ ),
3370
+ commandsLog: capturer.commandsLog.slice(
3371
+ range.commandStartIdx,
3372
+ end?.commands
3373
+ ),
3374
+ sources: new Map(capturer.sources),
3375
+ metadata: capturer.metadata,
3376
+ startWallTime: capturer.startWallTime
3377
+ };
3378
+ }
3379
+ function filterTestMetadataBySpec(allMetadata, specFile) {
3380
+ const filtered = /* @__PURE__ */ new Map();
3381
+ for (const [uid, meta] of allMetadata) {
3382
+ if (meta.specFile === specFile) {
3383
+ filtered.set(uid, meta);
3384
+ }
3385
+ }
3386
+ return filtered;
3387
+ }
3388
+ function recordSpecBoundary(ctx, specFile, traceGranularity) {
3389
+ if (traceGranularity !== "spec") {
3390
+ return null;
3391
+ }
3392
+ const lastRange = ctx.specRanges[ctx.specRanges.length - 1];
3393
+ if (lastRange && lastRange.specFile === specFile) {
3394
+ return null;
3395
+ }
3396
+ const prevRange = lastRange && !ctx.flushedSpecs.has(lastRange.specFile) ? lastRange : null;
3397
+ ctx.specRanges.push({
3398
+ specFile,
3399
+ commandStartIdx: ctx.capturer.commandsLog.length,
3400
+ consoleStartIdx: ctx.capturer.consoleLogs.length,
3401
+ networkStartIdx: ctx.capturer.networkRequests.length,
3402
+ mutationStartIdx: ctx.capturer.mutations.length,
3403
+ traceLogStartIdx: ctx.capturer.traceLogs.length,
3404
+ snapshotCount: ctx.actionSnapshots.length
3405
+ });
3406
+ return prevRange;
3407
+ }
3408
+ async function writeSpecTrace(input) {
3409
+ const specCapturer = buildSpecCapturer(
3410
+ input.capturer,
3411
+ input.range,
3412
+ input.nextRange
3413
+ );
3414
+ const specSnapshots = input.actionSnapshots.slice(
3415
+ input.range.snapshotCount,
3416
+ input.nextRange?.snapshotCount ?? input.actionSnapshots.length
3417
+ );
3418
+ const specSessionId = buildSpecSessionId(
3419
+ input.range.specFile,
3420
+ input.sessionId
3421
+ );
3422
+ const testMetadata = filterTestMetadataBySpec(
3423
+ input.testMetadata,
3424
+ input.range.specFile
3425
+ );
3426
+ return writeTraceZip(specCapturer, {
3427
+ outputDir: input.outputDir,
3428
+ sessionId: specSessionId,
3429
+ capabilities: input.capabilities,
3430
+ actionSnapshots: specSnapshots.length > 0 ? specSnapshots : void 0,
3431
+ format: input.format,
3432
+ testMetadata
3433
+ });
3434
+ }
3435
+
985
3436
  // ../core/src/suite-helpers.ts
986
3437
  function createSuiteStats(init) {
987
3438
  return {
@@ -1063,14 +3514,14 @@ function stampTestEnd(test, end = /* @__PURE__ */ new Date()) {
1063
3514
  }
1064
3515
 
1065
3516
  // ../core/src/test-discovery.ts
1066
- import fs6 from "fs";
3517
+ import fs7 from "fs";
1067
3518
  function findTestDefinitions(filePath, opts = {}) {
1068
- if (!fs6.existsSync(filePath)) {
3519
+ if (!fs7.existsSync(filePath)) {
1069
3520
  return [];
1070
3521
  }
1071
3522
  let source;
1072
3523
  try {
1073
- source = fs6.readFileSync(filePath, "utf-8");
3524
+ source = fs7.readFileSync(filePath, "utf-8");
1074
3525
  } catch {
1075
3526
  return [];
1076
3527
  }
@@ -1165,11 +3616,12 @@ var TestReporterBase = class {
1165
3616
  };
1166
3617
 
1167
3618
  // src/index.ts
3619
+ import { stop as stopBackend } from "@wdio/devtools-backend";
1168
3620
  import logger11 from "@wdio/logger";
1169
3621
 
1170
3622
  // src/run-lifecycle.ts
1171
- import * as fs8 from "fs";
1172
- import * as path6 from "path";
3623
+ import * as fs9 from "fs";
3624
+ import * as path7 from "path";
1173
3625
  import * as os3 from "os";
1174
3626
  import logger from "@wdio/logger";
1175
3627
  import { remote } from "webdriverio";
@@ -1229,8 +3681,8 @@ var CONFIG_FILENAMES = [
1229
3681
  var PLUGIN_GLOBAL_KEY = "__nightwatchDevtoolsPlugin";
1230
3682
 
1231
3683
  // src/helpers/utils.ts
1232
- import * as fs7 from "fs";
1233
- import * as path5 from "path";
3684
+ import * as fs8 from "fs";
3685
+ import * as path6 from "path";
1234
3686
  import { parse as parseStackTrace2 } from "stacktrace-parser";
1235
3687
  function determineTestState(testcase) {
1236
3688
  if (testcase.passed === 0 && testcase.failed === 0) {
@@ -1287,7 +3739,7 @@ function findTestFileFromStack() {
1287
3739
  return void 0;
1288
3740
  }
1289
3741
  const filePath = normalizeFilePath(frame.file);
1290
- return fs7.existsSync(filePath) ? filePath : void 0;
3742
+ return fs8.existsSync(filePath) ? filePath : void 0;
1291
3743
  }
1292
3744
  function extractTestMetadata(filePath) {
1293
3745
  const defs = findTestDefinitions(filePath, {
@@ -1415,21 +3867,21 @@ function resolveNightwatchConfig() {
1415
3867
  );
1416
3868
  if (configFlagIdx !== -1 && process.argv[configFlagIdx + 1]) {
1417
3869
  const argvConfig = process.argv[configFlagIdx + 1];
1418
- const resolved = path5.isAbsolute(argvConfig) ? argvConfig : path5.resolve(process.cwd(), argvConfig);
1419
- if (fs7.existsSync(resolved)) {
3870
+ const resolved = path6.isAbsolute(argvConfig) ? argvConfig : path6.resolve(process.cwd(), argvConfig);
3871
+ if (fs8.existsSync(resolved)) {
1420
3872
  return resolved;
1421
3873
  }
1422
3874
  }
1423
3875
  let dir = process.cwd();
1424
- const root = path5.parse(dir).root;
3876
+ const root = path6.parse(dir).root;
1425
3877
  while (dir && dir !== root) {
1426
3878
  for (const file of CONFIG_FILENAMES) {
1427
- const candidate = path5.join(dir, file);
1428
- if (fs7.existsSync(candidate)) {
3879
+ const candidate = path6.join(dir, file);
3880
+ if (fs8.existsSync(candidate)) {
1429
3881
  return candidate;
1430
3882
  }
1431
3883
  }
1432
- const parent = path5.dirname(dir);
3884
+ const parent = path6.dirname(dir);
1433
3885
  if (parent === dir) {
1434
3886
  break;
1435
3887
  }
@@ -1474,20 +3926,25 @@ async function runPluginBefore(ctx) {
1474
3926
  return;
1475
3927
  }
1476
3928
  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
- );
3929
+ if (ctx.options.mode === "trace") {
3930
+ log.info("Trace mode \u2014 skipping backend port-bind and UI window");
3931
+ } else {
3932
+ ctx.options.port = await findFreePort(
3933
+ ctx.options.port,
3934
+ ctx.options.hostname
3935
+ );
3936
+ log.info("\u{1F680} Starting DevTools backend...");
3937
+ const { port } = await start(ctx.options);
3938
+ ctx.options.port = port;
3939
+ const url = `http://${ctx.options.hostname}:${ctx.options.port}`;
3940
+ log.info(`\u2713 Backend started on port ${ctx.options.port}`);
3941
+ log.info(` DevTools UI: ${url}`);
3942
+ await ctx.openDevtoolsBrowserAt(url);
3943
+ await new Promise(
3944
+ (resolve2) => setTimeout(resolve2, TIMING.UI_CONNECTION_WAIT)
3945
+ );
3946
+ }
3947
+ ;
1491
3948
  globalThis[PLUGIN_GLOBAL_KEY] = ctx.plugin;
1492
3949
  } catch (err) {
1493
3950
  log.error(`Failed to start backend: ${errorMessage(err)}`);
@@ -1496,12 +3953,12 @@ async function runPluginBefore(ctx) {
1496
3953
  }
1497
3954
  async function openDevtoolsBrowser(ctx, url) {
1498
3955
  try {
1499
- ctx.userDataDir = path6.join(
3956
+ ctx.userDataDir = path7.join(
1500
3957
  os3.tmpdir(),
1501
3958
  `nightwatch-devtools-${ctx.options.port}-${Date.now()}`
1502
3959
  );
1503
- if (!fs8.existsSync(ctx.userDataDir)) {
1504
- fs8.mkdirSync(ctx.userDataDir, { recursive: true });
3960
+ if (!fs9.existsSync(ctx.userDataDir)) {
3961
+ fs9.mkdirSync(ctx.userDataDir, { recursive: true });
1505
3962
  }
1506
3963
  ctx.devtoolsBrowser = await remote({
1507
3964
  logLevel: "info",
@@ -1515,6 +3972,13 @@ async function openDevtoolsBrowser(ctx, url) {
1515
3972
  "--no-first-run",
1516
3973
  "--no-default-browser-check"
1517
3974
  ]
3975
+ },
3976
+ // Dashboard uses the Puppeteer-based 'devtools' protocol; drop the
3977
+ // "controlled by automated test software" infobar by ignoring
3978
+ // Puppeteer's --enable-automation default. (Runtime-valid capability
3979
+ // not present in the type.)
3980
+ "wdio:devtoolsOptions": {
3981
+ ignoreDefaultArgs: ["--enable-automation"]
1518
3982
  }
1519
3983
  }
1520
3984
  });
@@ -1726,43 +4190,43 @@ function buildCucumberScenarioSuite(input) {
1726
4190
  }
1727
4191
 
1728
4192
  // src/helpers/featureFileScan.ts
1729
- import fs9 from "fs";
1730
- import path7 from "path";
4193
+ import fs10 from "fs";
4194
+ import path8 from "path";
1731
4195
  function scanFeatureFile(featureUri) {
1732
- const featureAbsPath = path7.resolve(process.cwd(), featureUri);
4196
+ const featureAbsPath = path8.resolve(process.cwd(), featureUri);
1733
4197
  const result = {
1734
- featureName: path7.basename(featureUri, ".feature"),
4198
+ featureName: path8.basename(featureUri, ".feature"),
1735
4199
  featureContent: "",
1736
4200
  featureAbsPath,
1737
4201
  stepDefFiles: [],
1738
4202
  capturedPaths: []
1739
4203
  };
1740
- if (featureUri === "unknown.feature" || !fs9.existsSync(featureAbsPath)) {
4204
+ if (featureUri === "unknown.feature" || !fs10.existsSync(featureAbsPath)) {
1741
4205
  return result;
1742
4206
  }
1743
- result.featureContent = fs9.readFileSync(featureAbsPath, "utf-8");
4207
+ result.featureContent = fs10.readFileSync(featureAbsPath, "utf-8");
1744
4208
  const match = result.featureContent.match(/^\s*Feature:\s*(.+)/m);
1745
4209
  if (match) {
1746
4210
  result.featureName = match[1].trim();
1747
4211
  }
1748
4212
  result.capturedPaths.push(featureAbsPath);
1749
- const featureDir = path7.dirname(featureAbsPath);
4213
+ const featureDir = path8.dirname(featureAbsPath);
1750
4214
  const stepDirCandidates = ["step_definitions", "steps", "support"];
1751
4215
  for (const candidate of stepDirCandidates) {
1752
- const stepDir = path7.join(featureDir, candidate);
1753
- if (!fs9.existsSync(stepDir) || !fs9.statSync(stepDir).isDirectory()) {
4216
+ const stepDir = path8.join(featureDir, candidate);
4217
+ if (!fs10.existsSync(stepDir) || !fs10.statSync(stepDir).isDirectory()) {
1754
4218
  continue;
1755
4219
  }
1756
- for (const entry of fs9.readdirSync(stepDir)) {
4220
+ for (const entry of fs10.readdirSync(stepDir)) {
1757
4221
  if (!/\.(js|ts|mjs|cjs)$/.test(entry)) {
1758
4222
  continue;
1759
4223
  }
1760
- const stepFilePath = path7.join(stepDir, entry);
4224
+ const stepFilePath = path8.join(stepDir, entry);
1761
4225
  result.capturedPaths.push(stepFilePath);
1762
4226
  try {
1763
4227
  result.stepDefFiles.push({
1764
4228
  filePath: stepFilePath,
1765
- content: fs9.readFileSync(stepFilePath, "utf-8")
4229
+ content: fs10.readFileSync(stepFilePath, "utf-8")
1766
4230
  });
1767
4231
  } catch {
1768
4232
  }
@@ -1929,8 +4393,8 @@ async function cucumberAfter(ctx, browser, result, pickle) {
1929
4393
  import logger5 from "@wdio/logger";
1930
4394
 
1931
4395
  // src/helpers/specFileResolver.ts
1932
- import fs10 from "fs";
1933
- import path8 from "path";
4396
+ import fs11 from "fs";
4397
+ import path9 from "path";
1934
4398
  function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
1935
4399
  let fullPath = findTestFileFromStack() || null;
1936
4400
  if (!fullPath && cachedPath && cachedPath.includes(testFile)) {
@@ -1946,27 +4410,27 @@ function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
1946
4410
  const normalized = (modulePath || "").replace(/\\/g, "/");
1947
4411
  const srcFolderPaths = srcFolders.flatMap(
1948
4412
  (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)
4413
+ path9.resolve(workspaceRoot, sf, normalized + ".js"),
4414
+ path9.resolve(workspaceRoot, sf, normalized + ".ts"),
4415
+ path9.resolve(workspaceRoot, sf, normalized + ".cjs"),
4416
+ path9.resolve(workspaceRoot, sf, normalized)
1953
4417
  ] : []
1954
4418
  );
1955
4419
  const possiblePaths = [
1956
4420
  ...srcFolderPaths,
1957
4421
  // Treat module path as relative to cwd (works when src_folders isn't nested)
1958
4422
  ...normalized ? [
1959
- path8.resolve(workspaceRoot, normalized + ".js"),
1960
- path8.resolve(workspaceRoot, normalized + ".ts"),
1961
- path8.resolve(workspaceRoot, normalized + ".cjs"),
1962
- path8.resolve(workspaceRoot, normalized)
4423
+ path9.resolve(workspaceRoot, normalized + ".js"),
4424
+ path9.resolve(workspaceRoot, normalized + ".ts"),
4425
+ path9.resolve(workspaceRoot, normalized + ".cjs"),
4426
+ path9.resolve(workspaceRoot, normalized)
1963
4427
  ] : [],
1964
- path8.resolve(workspaceRoot, "tests", testFile + ".js"),
1965
- path8.resolve(workspaceRoot, "test", testFile + ".js"),
1966
- path8.resolve(workspaceRoot, testFile + ".js")
4428
+ path9.resolve(workspaceRoot, "tests", testFile + ".js"),
4429
+ path9.resolve(workspaceRoot, "test", testFile + ".js"),
4430
+ path9.resolve(workspaceRoot, testFile + ".js")
1967
4431
  ];
1968
4432
  for (const candidate of possiblePaths) {
1969
- if (fs10.existsSync(candidate)) {
4433
+ if (fs11.existsSync(candidate)) {
1970
4434
  return candidate;
1971
4435
  }
1972
4436
  }
@@ -2182,6 +4646,18 @@ import logger10 from "@wdio/logger";
2182
4646
  import http from "http";
2183
4647
  import logger6 from "@wdio/logger";
2184
4648
 
4649
+ // src/action-snapshot.ts
4650
+ function captureActionSnapshot2(browser, command, takeScreenshot) {
4651
+ const b = browser;
4652
+ return captureActionSnapshot({
4653
+ command,
4654
+ runScript: (src) => browser.execute(`return (${src})`),
4655
+ takeScreenshot,
4656
+ getUrl: () => b.getCurrentUrl ? b.getCurrentUrl().catch(() => void 0) : Promise.resolve(void 0),
4657
+ getTitle: () => b.getTitle ? b.getTitle().catch(() => void 0) : Promise.resolve(void 0)
4658
+ });
4659
+ }
4660
+
2185
4661
  // src/helpers/perfLogs.ts
2186
4662
  function applyResponseReceived(p, response) {
2187
4663
  const responseHeaders = {};
@@ -2194,18 +4670,22 @@ function applyResponseReceived(p, response) {
2194
4670
  p.mimeType = response.mimeType;
2195
4671
  p.type = getRequestType(p.url, response.mimeType);
2196
4672
  }
4673
+ function eventTimeMs(params, entry) {
4674
+ return typeof params.timestamp === "number" ? Math.round(params.timestamp * 1e3) : entry.timestamp;
4675
+ }
2197
4676
  function startPendingRequest(params, entry, pending) {
2198
4677
  const { requestId, request: req, timestamp } = params;
2199
4678
  if (!req || typeof timestamp !== "number") {
2200
4679
  return;
2201
4680
  }
4681
+ const startTime = eventTimeMs(params, entry);
2202
4682
  pending.set(requestId, {
2203
4683
  id: `${entry.timestamp}-${requestId}`,
2204
4684
  url: req.url,
2205
4685
  method: req.method,
2206
4686
  requestHeaders: req.headers ?? {},
2207
- timestamp: Math.round(timestamp * 1e3),
2208
- startTime: entry.timestamp
4687
+ timestamp: startTime,
4688
+ startTime
2209
4689
  });
2210
4690
  }
2211
4691
  function handlePerfLogEvent(method, params, entry, pending, completed) {
@@ -2224,8 +4704,8 @@ function handlePerfLogEvent(method, params, entry, pending, completed) {
2224
4704
  const p = pending.get(params.requestId);
2225
4705
  if (p && p.status !== void 0) {
2226
4706
  p.size = params.encodedDataLength;
2227
- p.endTime = entry.timestamp;
2228
- p.time = entry.timestamp - p.startTime;
4707
+ p.endTime = eventTimeMs(params, entry);
4708
+ p.time = Math.max(0, p.endTime - p.startTime);
2229
4709
  completed.push({ ...p });
2230
4710
  pending.delete(params.requestId);
2231
4711
  }
@@ -2235,8 +4715,8 @@ function handlePerfLogEvent(method, params, entry, pending, completed) {
2235
4715
  const p = pending.get(params.requestId);
2236
4716
  if (p) {
2237
4717
  p.error = params.errorText;
2238
- p.endTime = entry.timestamp;
2239
- p.time = entry.timestamp - p.startTime;
4718
+ p.endTime = eventTimeMs(params, entry);
4719
+ p.time = Math.max(0, p.endTime - p.startTime);
2240
4720
  completed.push({ ...p });
2241
4721
  pending.delete(params.requestId);
2242
4722
  }
@@ -2310,7 +4790,7 @@ function unwrapDriverValue(result) {
2310
4790
  return result;
2311
4791
  }
2312
4792
  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);
4793
+ var getPath = (obj, ...path10) => path10.reduce((acc, k) => getProp(acc, k), obj);
2314
4794
  var firstDefined = (obj, ...keys) => {
2315
4795
  if (!obj || typeof obj !== "object") {
2316
4796
  return void 0;
@@ -2347,6 +4827,10 @@ var SessionCapturer = class extends SessionCapturerBase {
2347
4827
  // True once BiDi inspectors are attached — the per-command perf-log network
2348
4828
  // capture path skips when set, so we don't double-emit network requests.
2349
4829
  bidiActive = false;
4830
+ // Populated by captureCommand when mode === 'trace' (set by the plugin).
4831
+ traceMode = "live";
4832
+ actionSnapshots = [];
4833
+ snapshotCaptures = [];
2350
4834
  constructor(devtoolsOptions = {}, browser) {
2351
4835
  super(devtoolsOptions);
2352
4836
  this.#browser = browser;
@@ -2362,15 +4846,6 @@ var SessionCapturer = class extends SessionCapturerBase {
2362
4846
  onWsClose() {
2363
4847
  log6.info("Worker WebSocket disconnected");
2364
4848
  }
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
4849
  async captureCommand(command, args, result, error, testUid, callSource, timestamp) {
2375
4850
  const serializedError = serializeError(error);
2376
4851
  const commandId = this.commandCounter++;
@@ -2393,6 +4868,20 @@ var SessionCapturer = class extends SessionCapturerBase {
2393
4868
  log6.warn(`Failed to capture performance data: ${errorMessage(err)}`);
2394
4869
  });
2395
4870
  }
4871
+ if (this.traceMode === "trace" && !error && this.#browser && mapCommandToAction(command)) {
4872
+ const browser = this.#browser;
4873
+ this.snapshotCaptures.push(
4874
+ captureActionSnapshot2(
4875
+ browser,
4876
+ command,
4877
+ () => this.takeScreenshotViaHttp(browser)
4878
+ ).then((snap) => {
4879
+ if (snap) {
4880
+ this.actionSnapshots.push(snap);
4881
+ }
4882
+ })
4883
+ );
4884
+ }
2396
4885
  return true;
2397
4886
  }
2398
4887
  async #capturePerformanceData(commandLogEntry, args) {
@@ -2589,13 +5078,6 @@ var SessionCapturer = class extends SessionCapturerBase {
2589
5078
  async captureTrace(browser) {
2590
5079
  await this.captureNetworkFromPerformanceLogs(browser);
2591
5080
  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
5081
  const result = await browser.execute(`
2600
5082
  if (typeof window.wdioTraceCollector === 'undefined') {
2601
5083
  return null;
@@ -3043,8 +5525,8 @@ var BrowserProxy = class {
3043
5525
  getCurrentTestFullPath() {
3044
5526
  return this.currentTestFullPath;
3045
5527
  }
3046
- setCurrentTestFullPath(path9) {
3047
- this.currentTestFullPath = path9;
5528
+ setCurrentTestFullPath(path10) {
5529
+ this.currentTestFullPath = path10;
3048
5530
  }
3049
5531
  /**
3050
5532
  * Wrap browser navigation methods (url / navigate / navigateTo) to inject
@@ -3397,7 +5879,7 @@ function broadcastSessionMetadata(ctx, browser) {
3397
5879
  const sf = opts.src_folders;
3398
5880
  ctx.srcFolders = Array.isArray(sf) ? sf : sf ? [sf] : [];
3399
5881
  }
3400
- ctx.sessionCapturer.sendUpstream("metadata", {
5882
+ const metadata = {
3401
5883
  type: "testrunner" /* Testrunner */,
3402
5884
  capabilities,
3403
5885
  desiredCapabilities,
@@ -3406,7 +5888,9 @@ function broadcastSessionMetadata(ctx, browser) {
3406
5888
  host: opts.webdriver?.host,
3407
5889
  options: ctx.buildMetadataOptions(),
3408
5890
  url: ""
3409
- });
5891
+ };
5892
+ ctx.sessionCapturer.metadata = metadata;
5893
+ ctx.sessionCapturer.sendUpstream("metadata", metadata);
3410
5894
  const browserName = capabilities.browserName || desiredCapabilities.browserName || "unknown";
3411
5895
  const browserVersion = capabilities.browserVersion || capabilities.version || "";
3412
5896
  log10.info(
@@ -3429,7 +5913,7 @@ async function tryAttachBidi(ctx, browser) {
3429
5913
  log10.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
3430
5914
  return;
3431
5915
  }
3432
- const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-OH4ZFDN5.js");
5916
+ const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-FIHBBGT4.js");
3433
5917
  const ok = await attachBidiHandlers(
3434
5918
  driver,
3435
5919
  buildBidiSinks(ctx.sessionCapturer)
@@ -3465,12 +5949,21 @@ async function ensureSessionInitialized(ctx, browser, finalizeCurrentScreencast2
3465
5949
  (resolve2) => setTimeout(resolve2, TIMING.INITIAL_CONNECTION_WAIT)
3466
5950
  );
3467
5951
  ctx.sessionCapturer = new SessionCapturer(
3468
- { port: ctx.port, hostname: ctx.hostname },
5952
+ ctx.mode === "trace" ? {} : {
5953
+ port: ctx.port,
5954
+ hostname: ctx.hostname,
5955
+ // A session change reopens the WS mid-run — tell the backend to keep
5956
+ // the accumulated run state so earlier tests' commands survive.
5957
+ reconnect: Boolean(isSessionChange)
5958
+ },
3469
5959
  browser
3470
5960
  );
3471
- const connected = await ctx.sessionCapturer.waitForConnection(3e3);
3472
- if (!connected) {
3473
- log10.error("\u274C Worker WebSocket failed to connect!");
5961
+ ctx.sessionCapturer.traceMode = ctx.mode;
5962
+ if (ctx.mode !== "trace") {
5963
+ const connected = await ctx.sessionCapturer.waitForConnection(3e3);
5964
+ if (!connected) {
5965
+ log10.error("\u274C Worker WebSocket failed to connect!");
5966
+ }
3474
5967
  }
3475
5968
  if (!ctx.testReporter) {
3476
5969
  initReporterChain(ctx);
@@ -3523,6 +6016,10 @@ var NightwatchDevToolsPlugin = class {
3523
6016
  #skipCount = 0;
3524
6017
  #configPath;
3525
6018
  #srcFolders = [];
6019
+ /** Index ranges into the session capturer's flat arrays, one per spec file. */
6020
+ #specRanges = [];
6021
+ /** Set of spec files already flushed to disk. */
6022
+ #flushedSpecs = /* @__PURE__ */ new Set();
3526
6023
  #getRerunLabel() {
3527
6024
  return process.env[REUSE_ENV.RERUN_ENTRY_TYPE] === "test" ? process.env[REUSE_ENV.RERUN_LABEL]?.trim() : void 0;
3528
6025
  }
@@ -3532,16 +6029,22 @@ var NightwatchDevToolsPlugin = class {
3532
6029
  #bidiEnabled = false;
3533
6030
  #bidiAttachAttempted = false;
3534
6031
  constructor(options = {}) {
6032
+ const mode = options.mode ?? "live";
6033
+ const ignore = mode === "trace" && options.screencast?.enabled === true;
6034
+ if (ignore) {
6035
+ log11.warn("trace mode: ignoring screencast option (live-mode feature)");
6036
+ }
6037
+ const screencast = ignore ? {} : options.screencast ?? {};
3535
6038
  this.options = {
3536
6039
  port: options.port ?? 3e3,
3537
6040
  hostname: options.hostname ?? "localhost",
3538
- screencast: options.screencast ?? {},
3539
- bidi: options.bidi ?? false
3540
- };
3541
- this.#screencastOptions = {
3542
- ...SCREENCAST_DEFAULTS,
3543
- ...options.screencast ?? {}
6041
+ screencast,
6042
+ bidi: options.bidi ?? false,
6043
+ mode,
6044
+ traceFormat: options.traceFormat ?? "zip",
6045
+ traceGranularity: options.traceGranularity ?? "session"
3544
6046
  };
6047
+ this.#screencastOptions = { ...SCREENCAST_DEFAULTS, ...screencast };
3545
6048
  this.#bidiEnabled = options.bidi === true;
3546
6049
  }
3547
6050
  // Single internals "bag" — structurally satisfies all 4 lifecycle ctx
@@ -3565,6 +6068,9 @@ var NightwatchDevToolsPlugin = class {
3565
6068
  get port() {
3566
6069
  return self.options.port;
3567
6070
  },
6071
+ get mode() {
6072
+ return self.options.mode;
6073
+ },
3568
6074
  get screencastOptions() {
3569
6075
  return self.#screencastOptions;
3570
6076
  },
@@ -3794,6 +6300,25 @@ var NightwatchDevToolsPlugin = class {
3794
6300
  this.sessionCapturer.captureSource(fullPath).catch(() => {
3795
6301
  });
3796
6302
  }
6303
+ if (fullPath) {
6304
+ const prevRange = recordSpecBoundary(
6305
+ {
6306
+ specRanges: this.#specRanges,
6307
+ flushedSpecs: this.#flushedSpecs,
6308
+ capturer: this.sessionCapturer,
6309
+ actionSnapshots: this.sessionCapturer.actionSnapshots
6310
+ },
6311
+ fullPath,
6312
+ this.options.traceGranularity
6313
+ );
6314
+ if (prevRange) {
6315
+ void this.#flushSpecTrace(prevRange).catch(
6316
+ (err) => log11.warn(
6317
+ `Failed to flush trace for spec "${prevRange.specFile}": ${errorMessage(err)}`
6318
+ )
6319
+ );
6320
+ }
6321
+ }
3797
6322
  await this.#closePreviousRunningTest(currentSuite, testFile, currentTest);
3798
6323
  const processedTests = this.testManager.getProcessedTests(testFile);
3799
6324
  const currentTestName = this.#pickCurrentTestName(
@@ -3827,6 +6352,12 @@ var NightwatchDevToolsPlugin = class {
3827
6352
  try {
3828
6353
  await this.#finalizeAllSuites(browser);
3829
6354
  this.#logRunSummary();
6355
+ if (this.options.mode === "trace") {
6356
+ await this.#writeTraceZipIfNeeded();
6357
+ await this.sessionCapturer?.closeWebSocket();
6358
+ await stopBackend();
6359
+ return;
6360
+ }
3830
6361
  if (!this.#devtoolsBrowser) {
3831
6362
  this.testReporter?.updateSuites();
3832
6363
  log11.info("\u267B Rerun complete \u2014 flushing WebSocket...");
@@ -3845,6 +6376,91 @@ var NightwatchDevToolsPlugin = class {
3845
6376
  #logRunSummary() {
3846
6377
  logRunSummary(this.#getInternals());
3847
6378
  }
6379
+ async #flushSpecTrace(range, nextRange) {
6380
+ if (this.#flushedSpecs.has(range.specFile)) {
6381
+ return void 0;
6382
+ }
6383
+ this.#flushedSpecs.add(range.specFile);
6384
+ const sessionId = this.sessionCapturer.metadata?.sessionId;
6385
+ if (!sessionId) {
6386
+ return void 0;
6387
+ }
6388
+ const allMetadata = /* @__PURE__ */ new Map();
6389
+ if (this.suiteManager) {
6390
+ for (const suite of this.suiteManager.getAllSuites().values()) {
6391
+ for (const entry of suite.tests) {
6392
+ if (typeof entry === "string") {
6393
+ continue;
6394
+ }
6395
+ allMetadata.set(entry.uid, {
6396
+ title: entry.fullTitle,
6397
+ specFile: entry.file
6398
+ });
6399
+ }
6400
+ }
6401
+ }
6402
+ const tracePath = await writeSpecTrace({
6403
+ range,
6404
+ nextRange,
6405
+ capturer: this.sessionCapturer,
6406
+ actionSnapshots: this.sessionCapturer.actionSnapshots,
6407
+ sessionId,
6408
+ outputDir: resolveAdapterOutputDir({ configPath: this.#configPath }),
6409
+ format: this.options.traceFormat,
6410
+ testMetadata: allMetadata
6411
+ });
6412
+ log11.info(`Trace for spec "${range.specFile}" saved to ${tracePath}`);
6413
+ return tracePath;
6414
+ }
6415
+ async #writeTraceZipIfNeeded() {
6416
+ if (this.options.mode !== "trace" || !this.sessionCapturer) {
6417
+ return;
6418
+ }
6419
+ const sessionId = this.sessionCapturer.metadata?.sessionId;
6420
+ if (!sessionId) {
6421
+ return;
6422
+ }
6423
+ try {
6424
+ if (this.sessionCapturer.snapshotCaptures.length) {
6425
+ await Promise.allSettled(this.sessionCapturer.snapshotCaptures);
6426
+ }
6427
+ if (this.options.traceGranularity === "spec") {
6428
+ for (const range of this.#specRanges) {
6429
+ if (!this.#flushedSpecs.has(range.specFile)) {
6430
+ await this.#flushSpecTrace(range);
6431
+ }
6432
+ }
6433
+ return;
6434
+ }
6435
+ const testMetadata = /* @__PURE__ */ new Map();
6436
+ if (this.suiteManager) {
6437
+ for (const suite of this.suiteManager.getAllSuites().values()) {
6438
+ for (const entry of suite.tests) {
6439
+ if (typeof entry === "string") {
6440
+ continue;
6441
+ }
6442
+ testMetadata.set(entry.uid, {
6443
+ title: entry.fullTitle,
6444
+ specFile: entry.file
6445
+ });
6446
+ }
6447
+ }
6448
+ }
6449
+ const snapshots = this.sessionCapturer.actionSnapshots;
6450
+ const tracePath = await writeTraceZip(this.sessionCapturer, {
6451
+ outputDir: resolveAdapterOutputDir({
6452
+ configPath: this.#configPath
6453
+ }),
6454
+ sessionId,
6455
+ actionSnapshots: snapshots.length ? snapshots : void 0,
6456
+ format: this.options.traceFormat,
6457
+ testMetadata
6458
+ });
6459
+ log11.info(`Trace saved to ${tracePath}`);
6460
+ } catch (err) {
6461
+ log11.warn(`trace write failed: ${errorMessage(err)}`);
6462
+ }
6463
+ }
3848
6464
  async #waitForDevtoolsBrowserClose() {
3849
6465
  await waitForDevtoolsBrowserClose(this.#getInternals());
3850
6466
  }