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