@wdio/nightwatch-devtools 1.2.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/{bidi-OH4ZFDN5.js → bidi-FIHBBGT4.js} +17 -3
- package/dist/bidi-FIHBBGT4.js.map +1 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +2749 -133
- package/dist/index.js.map +1 -1
- package/package.json +9 -6
- package/dist/bidi-OH4ZFDN5.js.map +0 -1
package/dist/index.js
CHANGED
|
@@ -17,6 +17,1894 @@ import {
|
|
|
17
17
|
// src/index.ts
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
|
|
20
|
+
// ../core/src/action-mapping.ts
|
|
21
|
+
var ACTION_MAP = {
|
|
22
|
+
// WDIO browser-level
|
|
23
|
+
url: { class: "Page", method: "navigate" },
|
|
24
|
+
navigateTo: { class: "Page", method: "navigate" },
|
|
25
|
+
back: { class: "Page", method: "goBack" },
|
|
26
|
+
forward: { class: "Page", method: "goForward" },
|
|
27
|
+
refresh: { class: "Page", method: "reload" },
|
|
28
|
+
newWindow: { class: "Page", method: "goto" },
|
|
29
|
+
// Selenium WebDriver navigation (driver.get, driver.navigate().to/back/forward/refresh)
|
|
30
|
+
get: { class: "Page", method: "navigate" },
|
|
31
|
+
to: { class: "Page", method: "navigate" },
|
|
32
|
+
// WDIO element-level
|
|
33
|
+
click: { class: "Element", method: "click" },
|
|
34
|
+
doubleClick: { class: "Element", method: "dblclick" },
|
|
35
|
+
setValue: { class: "Element", method: "fill" },
|
|
36
|
+
selectByVisibleText: { class: "Element", method: "selectOption" },
|
|
37
|
+
moveTo: { class: "Element", method: "hover" },
|
|
38
|
+
scrollIntoView: { class: "Element", method: "scrollIntoViewIfNeeded" },
|
|
39
|
+
dragAndDrop: { class: "Element", method: "dragTo" },
|
|
40
|
+
// Selenium WebElement actions
|
|
41
|
+
sendKeys: { class: "Element", method: "fill" },
|
|
42
|
+
clear: { class: "Element", method: "clear" },
|
|
43
|
+
submit: { class: "Element", method: "submit" },
|
|
44
|
+
// Cross-runner
|
|
45
|
+
keys: { class: "Keyboard", method: "press" },
|
|
46
|
+
execute: { class: "Page", method: "evaluate" },
|
|
47
|
+
executeAsync: { class: "Page", method: "evaluate" },
|
|
48
|
+
switchToFrame: { class: "Frame", method: "goto" },
|
|
49
|
+
touchAction: { class: "Element", method: "tap" }
|
|
50
|
+
};
|
|
51
|
+
function mapCommandToAction(command) {
|
|
52
|
+
return ACTION_MAP[command] ?? null;
|
|
53
|
+
}
|
|
54
|
+
function formatActionTitle(action, args, params) {
|
|
55
|
+
const firstArg = args[0] ?? params?.selector;
|
|
56
|
+
if (firstArg === void 0) {
|
|
57
|
+
return `${action.class}.${action.method}()`;
|
|
58
|
+
}
|
|
59
|
+
const label = typeof firstArg === "object" ? JSON.stringify(firstArg) : String(firstArg);
|
|
60
|
+
return `${action.class}.${action.method}("${label}")`;
|
|
61
|
+
}
|
|
62
|
+
var FILL_METHODS = /* @__PURE__ */ new Set(["fill", "selectOption"]);
|
|
63
|
+
|
|
64
|
+
// ../core/src/element-scripts.ts
|
|
65
|
+
function accessibilityTreeScript(inViewportOnly) {
|
|
66
|
+
return `(function () {
|
|
67
|
+
var INPUT_TYPE_ROLES = {
|
|
68
|
+
text: 'textbox', search: 'searchbox', email: 'textbox', url: 'textbox',
|
|
69
|
+
tel: 'textbox', password: 'textbox', number: 'spinbutton',
|
|
70
|
+
checkbox: 'checkbox', radio: 'radio', range: 'slider',
|
|
71
|
+
submit: 'button', reset: 'button', image: 'button', file: 'button', color: 'button'
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
var CONTAINER_ROLES = new Set([
|
|
75
|
+
'navigation', 'banner', 'contentinfo', 'complementary', 'main',
|
|
76
|
+
'form', 'region', 'group', 'list', 'listitem', 'table', 'row', 'rowgroup', 'generic'
|
|
77
|
+
])
|
|
78
|
+
|
|
79
|
+
function getRole(el) {
|
|
80
|
+
var explicit = el.getAttribute('role')
|
|
81
|
+
if (explicit) { return explicit.split(' ')[0] }
|
|
82
|
+
var tag = el.tagName.toLowerCase()
|
|
83
|
+
switch (tag) {
|
|
84
|
+
case 'button': return 'button'
|
|
85
|
+
case 'a': return el.hasAttribute('href') ? 'link' : null
|
|
86
|
+
case 'input': {
|
|
87
|
+
var type = (el.getAttribute('type') || 'text').toLowerCase()
|
|
88
|
+
if (type === 'hidden') { return null }
|
|
89
|
+
return INPUT_TYPE_ROLES[type] || 'textbox'
|
|
90
|
+
}
|
|
91
|
+
case 'select': return 'combobox'
|
|
92
|
+
case 'textarea': return 'textbox'
|
|
93
|
+
case 'h1': case 'h2': case 'h3': case 'h4': case 'h5': case 'h6': return 'heading'
|
|
94
|
+
case 'img': return 'img'
|
|
95
|
+
case 'nav': return 'navigation'
|
|
96
|
+
case 'main': return 'main'
|
|
97
|
+
case 'header': return !el.closest('article,aside,main,nav,section') ? 'banner' : null
|
|
98
|
+
case 'footer': return !el.closest('article,aside,main,nav,section') ? 'contentinfo' : null
|
|
99
|
+
case 'aside': return 'complementary'
|
|
100
|
+
case 'dialog': return 'dialog'
|
|
101
|
+
case 'form': return 'form'
|
|
102
|
+
case 'section': return el.hasAttribute('aria-label') || el.hasAttribute('aria-labelledby') ? 'region' : null
|
|
103
|
+
case 'summary': return 'button'
|
|
104
|
+
case 'details': return 'group'
|
|
105
|
+
case 'progress': return 'progressbar'
|
|
106
|
+
case 'meter': return 'meter'
|
|
107
|
+
case 'ul': case 'ol': return 'list'
|
|
108
|
+
case 'li': return 'listitem'
|
|
109
|
+
case 'table': return 'table'
|
|
110
|
+
}
|
|
111
|
+
if (el.contentEditable === 'true') { return 'textbox' }
|
|
112
|
+
if (el.hasAttribute('tabindex') && parseInt(el.getAttribute('tabindex') || '-1', 10) >= 0) { return 'generic' }
|
|
113
|
+
if (getDirectText(el)) { return 'statictext' }
|
|
114
|
+
return null
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function getAccessibleName(el, role) {
|
|
118
|
+
var ariaLabel = el.getAttribute('aria-label')
|
|
119
|
+
if (ariaLabel) { return ariaLabel.trim() }
|
|
120
|
+
var labelledBy = el.getAttribute('aria-labelledby')
|
|
121
|
+
if (labelledBy) {
|
|
122
|
+
var texts = labelledBy.split(/\\s+/).map(function(id) { return (document.getElementById(id)?.textContent || '').trim() }).filter(Boolean)
|
|
123
|
+
if (texts.length > 0) { return texts.join(' ').slice(0, 200) }
|
|
124
|
+
}
|
|
125
|
+
var tag = el.tagName.toLowerCase()
|
|
126
|
+
if (tag === 'img' || (tag === 'input' && el.getAttribute('type') === 'image')) {
|
|
127
|
+
var alt = el.getAttribute('alt')
|
|
128
|
+
if (alt !== null) { return alt.trim() }
|
|
129
|
+
}
|
|
130
|
+
if (['input', 'select', 'textarea'].indexOf(tag) !== -1) {
|
|
131
|
+
var id = el.getAttribute('id')
|
|
132
|
+
if (id) {
|
|
133
|
+
var label = document.querySelector('label[for="' + CSS.escape(id) + '"]')
|
|
134
|
+
if (label) { return (label.textContent || '').trim() }
|
|
135
|
+
}
|
|
136
|
+
var parentLabel = el.closest('label')
|
|
137
|
+
if (parentLabel) {
|
|
138
|
+
var clone = parentLabel.cloneNode(true)
|
|
139
|
+
clone.querySelectorAll('input,select,textarea').forEach(function(n) { n.remove() })
|
|
140
|
+
var lt = (clone.textContent || '').trim()
|
|
141
|
+
if (lt) { return lt }
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
var ph = el.getAttribute('placeholder')
|
|
145
|
+
if (ph) { return ph.trim() }
|
|
146
|
+
var title = el.getAttribute('title')
|
|
147
|
+
if (title) { return title.trim() }
|
|
148
|
+
var childImg = el.querySelector('img')
|
|
149
|
+
if (childImg) {
|
|
150
|
+
var imgAlt = childImg.getAttribute('alt')
|
|
151
|
+
if (imgAlt) { return imgAlt.trim() }
|
|
152
|
+
}
|
|
153
|
+
if (role && CONTAINER_ROLES.has(role)) { return '' }
|
|
154
|
+
return ((el.textContent || '').trim().replace(/\\s+/g, ' ') || '').slice(0, 200)
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function getSelector(element) {
|
|
158
|
+
var tag = element.tagName.toLowerCase()
|
|
159
|
+
var text = (element.textContent || '').trim().replace(/\\s+/g, ' ')
|
|
160
|
+
if (text && text.length > 0 && text.length <= 120) {
|
|
161
|
+
var sameTagElements = document.querySelectorAll(tag)
|
|
162
|
+
var matchCount = 0
|
|
163
|
+
sameTagElements.forEach(function(el) { if (el.textContent.includes(text)) { matchCount++ } })
|
|
164
|
+
if (matchCount === 1) { return tag + '*=' + text }
|
|
165
|
+
}
|
|
166
|
+
var ariaLabel = element.getAttribute('aria-label')
|
|
167
|
+
if (ariaLabel && ariaLabel.length <= 200) {
|
|
168
|
+
var sel = '[aria-label="' + CSS.escape(ariaLabel) + '"]'
|
|
169
|
+
if (document.querySelectorAll(sel).length === 1) { return sel }
|
|
170
|
+
}
|
|
171
|
+
var testId = element.getAttribute('data-testid')
|
|
172
|
+
if (testId) {
|
|
173
|
+
var testSel = '[data-testid="' + CSS.escape(testId) + '"]'
|
|
174
|
+
if (document.querySelectorAll(testSel).length === 1) { return testSel }
|
|
175
|
+
}
|
|
176
|
+
if (element.id) { return '#' + CSS.escape(element.id) }
|
|
177
|
+
var nameAttr = element.getAttribute('name')
|
|
178
|
+
if (nameAttr) {
|
|
179
|
+
var nameSel = tag + '[name="' + CSS.escape(nameAttr) + '"]'
|
|
180
|
+
if (document.querySelectorAll(nameSel).length === 1) { return nameSel }
|
|
181
|
+
}
|
|
182
|
+
if (element.className && typeof element.className === 'string') {
|
|
183
|
+
var classes = element.className.trim().split(/\\s+/).filter(Boolean)
|
|
184
|
+
for (var i = 0; i < classes.length; i++) {
|
|
185
|
+
var clsSel = tag + '.' + CSS.escape(classes[i])
|
|
186
|
+
if (document.querySelectorAll(clsSel).length === 1) { return clsSel }
|
|
187
|
+
}
|
|
188
|
+
if (classes.length >= 2) {
|
|
189
|
+
var twoClsSel = tag + classes.slice(0, 2).map(function(c) { return '.' + CSS.escape(c) }).join('')
|
|
190
|
+
if (document.querySelectorAll(twoClsSel).length === 1) { return twoClsSel }
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
var current = element
|
|
194
|
+
var path = []
|
|
195
|
+
while (current && current !== document.documentElement) {
|
|
196
|
+
var seg = current.tagName.toLowerCase()
|
|
197
|
+
if (current.id) { path.unshift('#' + CSS.escape(current.id)); break }
|
|
198
|
+
var parent = current.parentElement
|
|
199
|
+
if (parent) {
|
|
200
|
+
var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName })
|
|
201
|
+
if (siblings.length > 1) { seg += ':nth-of-type(' + (siblings.indexOf(current) + 1) + ')' }
|
|
202
|
+
}
|
|
203
|
+
path.unshift(seg)
|
|
204
|
+
current = current.parentElement
|
|
205
|
+
if (path.length >= 4) { break }
|
|
206
|
+
}
|
|
207
|
+
return path.join(' > ')
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function getDirectText(el) {
|
|
211
|
+
var text = ''
|
|
212
|
+
for (var i = 0; i < el.childNodes.length; i++) {
|
|
213
|
+
if (el.childNodes[i].nodeType === 3) { text += el.childNodes[i].textContent }
|
|
214
|
+
}
|
|
215
|
+
return text.trim().replace(/\\s+/g, ' ')
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function isVisible(el) {
|
|
219
|
+
if (typeof el.checkVisibility === 'function') {
|
|
220
|
+
return el.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true })
|
|
221
|
+
}
|
|
222
|
+
var style = window.getComputedStyle(el)
|
|
223
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && el.offsetWidth > 0 && el.offsetHeight > 0
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function isInViewport(el) {
|
|
227
|
+
var rect = el.getBoundingClientRect()
|
|
228
|
+
return rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function getLevel(el) {
|
|
232
|
+
var m = el.tagName.toLowerCase().match(/^h([1-6])$/)
|
|
233
|
+
if (m) { return parseInt(m[1], 10) }
|
|
234
|
+
var ariaLevel = el.getAttribute('aria-level')
|
|
235
|
+
if (ariaLevel) { return parseInt(ariaLevel, 10) }
|
|
236
|
+
return undefined
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getState(el) {
|
|
240
|
+
var inputEl = el
|
|
241
|
+
var isCheckable = ['input', 'menuitemcheckbox', 'menuitemradio'].indexOf(el.tagName.toLowerCase()) !== -1 || ['checkbox', 'radio', 'switch'].indexOf(el.getAttribute('role') || '') !== -1
|
|
242
|
+
return {
|
|
243
|
+
disabled: el.getAttribute('aria-disabled') === 'true' || inputEl.disabled ? 'true' : '',
|
|
244
|
+
checked: isCheckable && inputEl.checked ? 'true' : el.getAttribute('aria-checked') || '',
|
|
245
|
+
expanded: el.getAttribute('aria-expanded') || '',
|
|
246
|
+
selected: el.getAttribute('aria-selected') || '',
|
|
247
|
+
pressed: el.getAttribute('aria-pressed') || '',
|
|
248
|
+
required: inputEl.required || el.getAttribute('aria-required') === 'true' ? 'true' : '',
|
|
249
|
+
readonly: inputEl.readOnly || el.getAttribute('aria-readonly') === 'true' ? 'true' : ''
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
var result = []
|
|
254
|
+
|
|
255
|
+
function walk(el, depth) {
|
|
256
|
+
if (depth > 200) { return }
|
|
257
|
+
if (!isVisible(el)) { return }
|
|
258
|
+
var role = getRole(el)
|
|
259
|
+
var inViewport = isInViewport(el)
|
|
260
|
+
if (!role) {
|
|
261
|
+
for (var i = 0; i < el.children.length; i++) { walk(el.children[i], depth + 1) }
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
if (${inViewportOnly} && !inViewport) {
|
|
265
|
+
for (var i = 0; i < el.children.length; i++) { walk(el.children[i], depth + 1) }
|
|
266
|
+
return
|
|
267
|
+
}
|
|
268
|
+
var name = getAccessibleName(el, role)
|
|
269
|
+
var selector = getSelector(el)
|
|
270
|
+
var node = { role: role, name: name, selector: selector, depth: depth, level: getLevel(el) ?? '', isInViewport: inViewport }
|
|
271
|
+
var state = getState(el)
|
|
272
|
+
for (var k in state) { node[k] = state[k] }
|
|
273
|
+
result.push(node)
|
|
274
|
+
for (var i = 0; i < el.children.length; i++) { walk(el.children[i], depth + 1) }
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
for (var i = 0; i < document.body.children.length; i++) { walk(document.body.children[i], 0) }
|
|
278
|
+
return result
|
|
279
|
+
})()`;
|
|
280
|
+
}
|
|
281
|
+
function elementsScript(includeBounds, inViewportOnly) {
|
|
282
|
+
return `(function () {
|
|
283
|
+
var interactableSelectors = [
|
|
284
|
+
'a[href]', 'button', 'input:not([type="hidden"])', 'select', 'textarea',
|
|
285
|
+
'[role="button"]', '[role="link"]', '[role="checkbox"]', '[role="radio"]',
|
|
286
|
+
'[role="tab"]', '[role="menuitem"]', '[role="combobox"]', '[role="option"]',
|
|
287
|
+
'[role="switch"]', '[role="slider"]', '[role="textbox"]', '[role="searchbox"]',
|
|
288
|
+
'[role="spinbutton"]', '[contenteditable="true"]', '[tabindex]:not([tabindex="-1"])'
|
|
289
|
+
].join(',')
|
|
290
|
+
|
|
291
|
+
function isVisible(element) {
|
|
292
|
+
if (typeof element.checkVisibility === 'function') {
|
|
293
|
+
return element.checkVisibility({ opacityProperty: true, visibilityProperty: true, contentVisibilityAuto: true })
|
|
294
|
+
}
|
|
295
|
+
var style = window.getComputedStyle(element)
|
|
296
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0' && element.offsetWidth > 0 && element.offsetHeight > 0
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function getAccessibleName(el) {
|
|
300
|
+
var ariaLabel = el.getAttribute('aria-label')
|
|
301
|
+
if (ariaLabel) { return ariaLabel.trim() }
|
|
302
|
+
var labelledBy = el.getAttribute('aria-labelledby')
|
|
303
|
+
if (labelledBy) {
|
|
304
|
+
var texts = labelledBy.split(/\\s+/).map(function(id) { return (document.getElementById(id)?.textContent || '').trim() }).filter(Boolean)
|
|
305
|
+
if (texts.length > 0) { return texts.join(' ').slice(0, 200) }
|
|
306
|
+
}
|
|
307
|
+
var tag = el.tagName.toLowerCase()
|
|
308
|
+
if (tag === 'img' || (tag === 'input' && el.getAttribute('type') === 'image')) {
|
|
309
|
+
var alt = el.getAttribute('alt')
|
|
310
|
+
if (alt !== null) { return alt.trim() }
|
|
311
|
+
}
|
|
312
|
+
if (['input', 'select', 'textarea'].indexOf(tag) !== -1) {
|
|
313
|
+
var id = el.getAttribute('id')
|
|
314
|
+
if (id) {
|
|
315
|
+
var label = document.querySelector('label[for="' + CSS.escape(id) + '"]')
|
|
316
|
+
if (label) { return (label.textContent || '').trim() }
|
|
317
|
+
}
|
|
318
|
+
var parentLabel = el.closest('label')
|
|
319
|
+
if (parentLabel) {
|
|
320
|
+
var clone = parentLabel.cloneNode(true)
|
|
321
|
+
clone.querySelectorAll('input,select,textarea').forEach(function(n) { n.remove() })
|
|
322
|
+
var lt = (clone.textContent || '').trim()
|
|
323
|
+
if (lt) { return lt }
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
var ph = el.getAttribute('placeholder')
|
|
327
|
+
if (ph) { return ph.trim() }
|
|
328
|
+
var title = el.getAttribute('title')
|
|
329
|
+
if (title) { return title.trim() }
|
|
330
|
+
return ((el.textContent || '').trim().replace(/\\s+/g, ' ') || '').slice(0, 200)
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function getSelector(element) {
|
|
334
|
+
var tag = element.tagName.toLowerCase()
|
|
335
|
+
var text = (element.textContent || '').trim().replace(/\\s+/g, ' ')
|
|
336
|
+
if (text && text.length > 0 && text.length <= 120) {
|
|
337
|
+
var sameTagElements = document.querySelectorAll(tag)
|
|
338
|
+
var matchCount = 0
|
|
339
|
+
sameTagElements.forEach(function(el) { if (el.textContent.includes(text)) { matchCount++ } })
|
|
340
|
+
if (matchCount === 1) { return tag + '*=' + text }
|
|
341
|
+
}
|
|
342
|
+
var ariaLabel = element.getAttribute('aria-label')
|
|
343
|
+
if (ariaLabel && ariaLabel.length <= 200) {
|
|
344
|
+
var sel = '[aria-label="' + CSS.escape(ariaLabel) + '"]'
|
|
345
|
+
if (document.querySelectorAll(sel).length === 1) { return sel }
|
|
346
|
+
}
|
|
347
|
+
var testId = element.getAttribute('data-testid')
|
|
348
|
+
if (testId) {
|
|
349
|
+
var testSel = '[data-testid="' + CSS.escape(testId) + '"]'
|
|
350
|
+
if (document.querySelectorAll(testSel).length === 1) { return testSel }
|
|
351
|
+
}
|
|
352
|
+
if (element.id) { return '#' + CSS.escape(element.id) }
|
|
353
|
+
var nameAttr = element.getAttribute('name')
|
|
354
|
+
if (nameAttr) {
|
|
355
|
+
var nameSel = tag + '[name="' + CSS.escape(nameAttr) + '"]'
|
|
356
|
+
if (document.querySelectorAll(nameSel).length === 1) { return nameSel }
|
|
357
|
+
}
|
|
358
|
+
if (element.className && typeof element.className === 'string') {
|
|
359
|
+
var classes = element.className.trim().split(/\\s+/).filter(Boolean)
|
|
360
|
+
for (var i = 0; i < classes.length; i++) {
|
|
361
|
+
var clsSel = tag + '.' + CSS.escape(classes[i])
|
|
362
|
+
if (document.querySelectorAll(clsSel).length === 1) { return clsSel }
|
|
363
|
+
}
|
|
364
|
+
if (classes.length >= 2) {
|
|
365
|
+
var twoClsSel = tag + classes.slice(0, 2).map(function(c) { return '.' + CSS.escape(c) }).join('')
|
|
366
|
+
if (document.querySelectorAll(twoClsSel).length === 1) { return twoClsSel }
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
var current = element
|
|
370
|
+
var path = []
|
|
371
|
+
while (current && current !== document.documentElement) {
|
|
372
|
+
var seg = current.tagName.toLowerCase()
|
|
373
|
+
if (current.id) { path.unshift('#' + CSS.escape(current.id)); break }
|
|
374
|
+
var parent = current.parentElement
|
|
375
|
+
if (parent) {
|
|
376
|
+
var siblings = Array.from(parent.children).filter(function(c) { return c.tagName === current.tagName })
|
|
377
|
+
if (siblings.length > 1) { seg += ':nth-of-type(' + (siblings.indexOf(current) + 1) + ')' }
|
|
378
|
+
}
|
|
379
|
+
path.unshift(seg)
|
|
380
|
+
current = current.parentElement
|
|
381
|
+
if (path.length >= 4) { break }
|
|
382
|
+
}
|
|
383
|
+
return path.join(' > ')
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
var elements = []
|
|
387
|
+
var seen = new Set()
|
|
388
|
+
|
|
389
|
+
document.querySelectorAll(interactableSelectors).forEach(function(el) {
|
|
390
|
+
if (seen.has(el)) { return }
|
|
391
|
+
seen.add(el)
|
|
392
|
+
var htmlEl = el
|
|
393
|
+
if (!isVisible(htmlEl)) { return }
|
|
394
|
+
var inputEl = htmlEl
|
|
395
|
+
var rect = htmlEl.getBoundingClientRect()
|
|
396
|
+
var isInVp = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth)
|
|
397
|
+
if (${inViewportOnly} && !isInVp) { return }
|
|
398
|
+
var entry = {
|
|
399
|
+
tagName: htmlEl.tagName.toLowerCase(),
|
|
400
|
+
name: getAccessibleName(htmlEl),
|
|
401
|
+
type: htmlEl.getAttribute('type') || '',
|
|
402
|
+
value: inputEl.value || '',
|
|
403
|
+
href: htmlEl.getAttribute('href') || '',
|
|
404
|
+
selector: getSelector(htmlEl),
|
|
405
|
+
isInViewport: isInVp
|
|
406
|
+
}
|
|
407
|
+
${includeBounds ? "entry.boundingBox = { x: rect.x + window.scrollX, y: rect.y + window.scrollY, width: rect.width, height: rect.height }" : ""}
|
|
408
|
+
elements.push(entry)
|
|
409
|
+
})
|
|
410
|
+
return elements
|
|
411
|
+
})()`;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
// ../core/src/locators/xml-parsing.ts
|
|
415
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
416
|
+
import xpath from "xpath";
|
|
417
|
+
function childNodesOf(node) {
|
|
418
|
+
const children = [];
|
|
419
|
+
if (node.childNodes) {
|
|
420
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
421
|
+
const child = node.childNodes.item(i);
|
|
422
|
+
if (child?.nodeType === 1) {
|
|
423
|
+
children.push(child);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
return children;
|
|
428
|
+
}
|
|
429
|
+
function translateRecursively(domNode, parentPath = "", index = null) {
|
|
430
|
+
const attributes = {};
|
|
431
|
+
const element = domNode;
|
|
432
|
+
if (element.attributes) {
|
|
433
|
+
for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) {
|
|
434
|
+
const attr = element.attributes.item(attrIdx);
|
|
435
|
+
if (attr) {
|
|
436
|
+
attributes[attr.name] = attr.value.replace(/(\n)/gm, "\\n");
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const path10 = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
|
|
441
|
+
return {
|
|
442
|
+
children: childNodesOf(domNode).map(
|
|
443
|
+
(childNode, childIndex) => translateRecursively(childNode, path10, childIndex)
|
|
444
|
+
),
|
|
445
|
+
tagName: domNode.nodeName,
|
|
446
|
+
attributes,
|
|
447
|
+
path: path10
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
function isSameElement(node1, node2) {
|
|
451
|
+
if (node1.nodeType !== 1 || node2.nodeType !== 1) {
|
|
452
|
+
return false;
|
|
453
|
+
}
|
|
454
|
+
const el1 = node1;
|
|
455
|
+
const el2 = node2;
|
|
456
|
+
if (el1.nodeName !== el2.nodeName) {
|
|
457
|
+
return false;
|
|
458
|
+
}
|
|
459
|
+
const bounds1 = el1.getAttribute("bounds");
|
|
460
|
+
const bounds2 = el2.getAttribute("bounds");
|
|
461
|
+
if (bounds1 && bounds2) {
|
|
462
|
+
return bounds1 === bounds2;
|
|
463
|
+
}
|
|
464
|
+
const x1 = el1.getAttribute("x");
|
|
465
|
+
const y1 = el1.getAttribute("y");
|
|
466
|
+
const x2 = el2.getAttribute("x");
|
|
467
|
+
const y2 = el2.getAttribute("y");
|
|
468
|
+
if (x1 && y1 && x2 && y2) {
|
|
469
|
+
return x1 === x2 && y1 === y2 && el1.getAttribute("width") === el2.getAttribute("width") && el1.getAttribute("height") === el2.getAttribute("height");
|
|
470
|
+
}
|
|
471
|
+
return false;
|
|
472
|
+
}
|
|
473
|
+
function xmlToJSON(sourceXML) {
|
|
474
|
+
try {
|
|
475
|
+
const parser = new DOMParser();
|
|
476
|
+
const sourceDoc = parser.parseFromString(sourceXML, "text/xml");
|
|
477
|
+
const parseErrors = sourceDoc.getElementsByTagName("parsererror");
|
|
478
|
+
if (parseErrors.length > 0) {
|
|
479
|
+
console.error(
|
|
480
|
+
"[xmlToJSON] XML parsing error:",
|
|
481
|
+
parseErrors[0].textContent
|
|
482
|
+
);
|
|
483
|
+
return null;
|
|
484
|
+
}
|
|
485
|
+
const children = childNodesOf(sourceDoc);
|
|
486
|
+
const firstChild = children[0] || (sourceDoc.documentElement ? childNodesOf(sourceDoc.documentElement)[0] : null);
|
|
487
|
+
return firstChild ? translateRecursively(firstChild) : { children: [], tagName: "", attributes: {}, path: "" };
|
|
488
|
+
} catch (e) {
|
|
489
|
+
console.error("[xmlToJSON] Failed to parse XML:", e);
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
function xmlToDOM(sourceXML) {
|
|
494
|
+
try {
|
|
495
|
+
const parser = new DOMParser();
|
|
496
|
+
const doc = parser.parseFromString(sourceXML, "text/xml");
|
|
497
|
+
const parseErrors = doc.getElementsByTagName("parsererror");
|
|
498
|
+
if (parseErrors.length > 0) {
|
|
499
|
+
console.error("[xmlToDOM] XML parsing error:", parseErrors[0].textContent);
|
|
500
|
+
return null;
|
|
501
|
+
}
|
|
502
|
+
return doc;
|
|
503
|
+
} catch (e) {
|
|
504
|
+
console.error("[xmlToDOM] Failed to parse XML:", e);
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
function evaluateXPath(doc, xpathExpr) {
|
|
509
|
+
try {
|
|
510
|
+
const nodes = xpath.select(xpathExpr, doc);
|
|
511
|
+
if (Array.isArray(nodes)) {
|
|
512
|
+
return nodes;
|
|
513
|
+
}
|
|
514
|
+
return [];
|
|
515
|
+
} catch (e) {
|
|
516
|
+
console.error(`[evaluateXPath] Failed to evaluate "${xpathExpr}":`, e);
|
|
517
|
+
return [];
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
function checkXPathUniqueness(doc, xpathExpr, targetNode) {
|
|
521
|
+
try {
|
|
522
|
+
const nodes = evaluateXPath(doc, xpathExpr);
|
|
523
|
+
const totalMatches = nodes.length;
|
|
524
|
+
if (totalMatches === 0) {
|
|
525
|
+
return { isUnique: false };
|
|
526
|
+
}
|
|
527
|
+
if (totalMatches === 1) {
|
|
528
|
+
return { isUnique: true };
|
|
529
|
+
}
|
|
530
|
+
if (targetNode) {
|
|
531
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
532
|
+
if (nodes[i] === targetNode || isSameElement(nodes[i], targetNode)) {
|
|
533
|
+
return {
|
|
534
|
+
isUnique: false,
|
|
535
|
+
index: i + 1,
|
|
536
|
+
// 1-based index for XPath
|
|
537
|
+
totalMatches
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
return { isUnique: false, totalMatches };
|
|
543
|
+
} catch (e) {
|
|
544
|
+
console.error(`[checkXPathUniqueness] Error checking "${xpathExpr}":`, e);
|
|
545
|
+
return { isUnique: false };
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
function findDOMNodeByPath(doc, path10) {
|
|
549
|
+
if (!path10) {
|
|
550
|
+
return doc.documentElement;
|
|
551
|
+
}
|
|
552
|
+
const indices = path10.split(".").map(Number);
|
|
553
|
+
let current = doc.documentElement;
|
|
554
|
+
for (const index of indices) {
|
|
555
|
+
if (!current) {
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
const children = [];
|
|
559
|
+
if (current.childNodes) {
|
|
560
|
+
for (let i = 0; i < current.childNodes.length; i++) {
|
|
561
|
+
const child = current.childNodes.item(i);
|
|
562
|
+
if (child?.nodeType === 1) {
|
|
563
|
+
children.push(child);
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
current = children[index] || null;
|
|
568
|
+
}
|
|
569
|
+
return current;
|
|
570
|
+
}
|
|
571
|
+
function parseAndroidBounds(bounds) {
|
|
572
|
+
const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
573
|
+
if (!match) {
|
|
574
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
575
|
+
}
|
|
576
|
+
const x1 = parseInt(match[1], 10);
|
|
577
|
+
const y1 = parseInt(match[2], 10);
|
|
578
|
+
const x2 = parseInt(match[3], 10);
|
|
579
|
+
const y2 = parseInt(match[4], 10);
|
|
580
|
+
return {
|
|
581
|
+
x: x1,
|
|
582
|
+
y: y1,
|
|
583
|
+
width: x2 - x1,
|
|
584
|
+
height: y2 - y1
|
|
585
|
+
};
|
|
586
|
+
}
|
|
587
|
+
function parseIOSBounds(attributes) {
|
|
588
|
+
return {
|
|
589
|
+
x: parseInt(attributes.x || "0", 10),
|
|
590
|
+
y: parseInt(attributes.y || "0", 10),
|
|
591
|
+
width: parseInt(attributes.width || "0", 10),
|
|
592
|
+
height: parseInt(attributes.height || "0", 10)
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
function countAttributeOccurrences(sourceXML, attribute, value) {
|
|
596
|
+
const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
597
|
+
const pattern = new RegExp(`${attribute}=["']${escapedValue}["']`, "g");
|
|
598
|
+
const matches = sourceXML.match(pattern);
|
|
599
|
+
return matches ? matches.length : 0;
|
|
600
|
+
}
|
|
601
|
+
function isAttributeUnique(sourceXML, attribute, value) {
|
|
602
|
+
return countAttributeOccurrences(sourceXML, attribute, value) === 1;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
// ../core/src/locators/constants.ts
|
|
606
|
+
var ANDROID_INTERACTABLE_TAGS = [
|
|
607
|
+
// Input elements
|
|
608
|
+
"android.widget.EditText",
|
|
609
|
+
"android.widget.AutoCompleteTextView",
|
|
610
|
+
"android.widget.MultiAutoCompleteTextView",
|
|
611
|
+
"android.widget.SearchView",
|
|
612
|
+
// Button-like elements
|
|
613
|
+
"android.widget.Button",
|
|
614
|
+
"android.widget.ImageButton",
|
|
615
|
+
"android.widget.ToggleButton",
|
|
616
|
+
"android.widget.CompoundButton",
|
|
617
|
+
"android.widget.RadioButton",
|
|
618
|
+
"android.widget.CheckBox",
|
|
619
|
+
"android.widget.Switch",
|
|
620
|
+
"android.widget.FloatingActionButton",
|
|
621
|
+
"com.google.android.material.button.MaterialButton",
|
|
622
|
+
"com.google.android.material.floatingactionbutton.FloatingActionButton",
|
|
623
|
+
// Text elements (often tappable)
|
|
624
|
+
"android.widget.TextView",
|
|
625
|
+
"android.widget.CheckedTextView",
|
|
626
|
+
// Image elements (often tappable)
|
|
627
|
+
"android.widget.ImageView",
|
|
628
|
+
"android.widget.QuickContactBadge",
|
|
629
|
+
// Selection elements
|
|
630
|
+
"android.widget.Spinner",
|
|
631
|
+
"android.widget.SeekBar",
|
|
632
|
+
"android.widget.RatingBar",
|
|
633
|
+
"android.widget.ProgressBar",
|
|
634
|
+
"android.widget.DatePicker",
|
|
635
|
+
"android.widget.TimePicker",
|
|
636
|
+
"android.widget.NumberPicker",
|
|
637
|
+
// List/grid items
|
|
638
|
+
"android.widget.AdapterView"
|
|
639
|
+
];
|
|
640
|
+
var ANDROID_LAYOUT_CONTAINERS = [
|
|
641
|
+
// Core ViewGroup classes
|
|
642
|
+
"android.view.ViewGroup",
|
|
643
|
+
"android.view.View",
|
|
644
|
+
"android.widget.FrameLayout",
|
|
645
|
+
"android.widget.LinearLayout",
|
|
646
|
+
"android.widget.RelativeLayout",
|
|
647
|
+
"android.widget.GridLayout",
|
|
648
|
+
"android.widget.TableLayout",
|
|
649
|
+
"android.widget.TableRow",
|
|
650
|
+
"android.widget.AbsoluteLayout",
|
|
651
|
+
// AndroidX layout classes
|
|
652
|
+
"androidx.constraintlayout.widget.ConstraintLayout",
|
|
653
|
+
"androidx.coordinatorlayout.widget.CoordinatorLayout",
|
|
654
|
+
"androidx.appcompat.widget.LinearLayoutCompat",
|
|
655
|
+
"androidx.cardview.widget.CardView",
|
|
656
|
+
"androidx.appcompat.widget.ContentFrameLayout",
|
|
657
|
+
"androidx.appcompat.widget.FitWindowsFrameLayout",
|
|
658
|
+
// Scrolling containers
|
|
659
|
+
"android.widget.ScrollView",
|
|
660
|
+
"android.widget.HorizontalScrollView",
|
|
661
|
+
"android.widget.NestedScrollView",
|
|
662
|
+
"androidx.core.widget.NestedScrollView",
|
|
663
|
+
"androidx.recyclerview.widget.RecyclerView",
|
|
664
|
+
"android.widget.ListView",
|
|
665
|
+
"android.widget.GridView",
|
|
666
|
+
"android.widget.AbsListView",
|
|
667
|
+
// App chrome / system elements
|
|
668
|
+
"android.widget.ActionBarContainer",
|
|
669
|
+
"android.widget.ActionBarOverlayLayout",
|
|
670
|
+
"android.view.ViewStub",
|
|
671
|
+
"androidx.appcompat.widget.ActionBarContainer",
|
|
672
|
+
"androidx.appcompat.widget.ActionBarContextView",
|
|
673
|
+
"androidx.appcompat.widget.ActionBarOverlayLayout",
|
|
674
|
+
// Decor views
|
|
675
|
+
"com.android.internal.policy.DecorView",
|
|
676
|
+
"android.widget.DecorView"
|
|
677
|
+
];
|
|
678
|
+
var IOS_INTERACTABLE_TAGS = [
|
|
679
|
+
// Input elements
|
|
680
|
+
"XCUIElementTypeTextField",
|
|
681
|
+
"XCUIElementTypeSecureTextField",
|
|
682
|
+
"XCUIElementTypeTextView",
|
|
683
|
+
"XCUIElementTypeSearchField",
|
|
684
|
+
// Button-like elements
|
|
685
|
+
"XCUIElementTypeButton",
|
|
686
|
+
"XCUIElementTypeLink",
|
|
687
|
+
// Text elements (often tappable)
|
|
688
|
+
"XCUIElementTypeStaticText",
|
|
689
|
+
// Image elements
|
|
690
|
+
"XCUIElementTypeImage",
|
|
691
|
+
"XCUIElementTypeIcon",
|
|
692
|
+
// Selection elements
|
|
693
|
+
"XCUIElementTypeSwitch",
|
|
694
|
+
"XCUIElementTypeSlider",
|
|
695
|
+
"XCUIElementTypeStepper",
|
|
696
|
+
"XCUIElementTypeSegmentedControl",
|
|
697
|
+
"XCUIElementTypePicker",
|
|
698
|
+
"XCUIElementTypePickerWheel",
|
|
699
|
+
"XCUIElementTypeDatePicker",
|
|
700
|
+
"XCUIElementTypePageIndicator",
|
|
701
|
+
// Table/list items
|
|
702
|
+
"XCUIElementTypeCell",
|
|
703
|
+
"XCUIElementTypeMenuItem",
|
|
704
|
+
"XCUIElementTypeMenuBarItem",
|
|
705
|
+
// Toggle elements
|
|
706
|
+
"XCUIElementTypeCheckBox",
|
|
707
|
+
"XCUIElementTypeRadioButton",
|
|
708
|
+
"XCUIElementTypeToggle",
|
|
709
|
+
// Other interactive
|
|
710
|
+
"XCUIElementTypeKey",
|
|
711
|
+
"XCUIElementTypeKeyboard",
|
|
712
|
+
"XCUIElementTypeAlert",
|
|
713
|
+
"XCUIElementTypeSheet"
|
|
714
|
+
];
|
|
715
|
+
var IOS_LAYOUT_CONTAINERS = [
|
|
716
|
+
// Generic containers
|
|
717
|
+
"XCUIElementTypeOther",
|
|
718
|
+
"XCUIElementTypeGroup",
|
|
719
|
+
"XCUIElementTypeLayoutItem",
|
|
720
|
+
// Scroll containers
|
|
721
|
+
"XCUIElementTypeScrollView",
|
|
722
|
+
"XCUIElementTypeTable",
|
|
723
|
+
"XCUIElementTypeCollectionView",
|
|
724
|
+
"XCUIElementTypeScrollBar",
|
|
725
|
+
// Navigation chrome
|
|
726
|
+
"XCUIElementTypeNavigationBar",
|
|
727
|
+
"XCUIElementTypeTabBar",
|
|
728
|
+
"XCUIElementTypeToolbar",
|
|
729
|
+
"XCUIElementTypeStatusBar",
|
|
730
|
+
"XCUIElementTypeMenuBar",
|
|
731
|
+
// Windows and views
|
|
732
|
+
"XCUIElementTypeWindow",
|
|
733
|
+
"XCUIElementTypeSheet",
|
|
734
|
+
"XCUIElementTypeDrawer",
|
|
735
|
+
"XCUIElementTypeDialog",
|
|
736
|
+
"XCUIElementTypePopover",
|
|
737
|
+
"XCUIElementTypePopUpButton",
|
|
738
|
+
// Outline elements
|
|
739
|
+
"XCUIElementTypeOutline",
|
|
740
|
+
"XCUIElementTypeOutlineRow",
|
|
741
|
+
"XCUIElementTypeBrowser",
|
|
742
|
+
"XCUIElementTypeSplitGroup",
|
|
743
|
+
"XCUIElementTypeSplitter",
|
|
744
|
+
// Application root
|
|
745
|
+
"XCUIElementTypeApplication"
|
|
746
|
+
];
|
|
747
|
+
|
|
748
|
+
// ../core/src/locators/locator-generation.ts
|
|
749
|
+
function isValidValue(value) {
|
|
750
|
+
return value !== void 0 && value !== null && value !== "null" && value.trim() !== "";
|
|
751
|
+
}
|
|
752
|
+
function escapeText(text) {
|
|
753
|
+
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
754
|
+
}
|
|
755
|
+
function escapeXPathValue(value) {
|
|
756
|
+
if (!value.includes("'")) {
|
|
757
|
+
return `'${value}'`;
|
|
758
|
+
}
|
|
759
|
+
if (!value.includes('"')) {
|
|
760
|
+
return `"${value}"`;
|
|
761
|
+
}
|
|
762
|
+
const parts = [];
|
|
763
|
+
let current = "";
|
|
764
|
+
for (const char of value) {
|
|
765
|
+
if (char === "'") {
|
|
766
|
+
if (current) {
|
|
767
|
+
parts.push(`'${current}'`);
|
|
768
|
+
}
|
|
769
|
+
parts.push(`"'"`);
|
|
770
|
+
current = "";
|
|
771
|
+
} else {
|
|
772
|
+
current += char;
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (current) {
|
|
776
|
+
parts.push(`'${current}'`);
|
|
777
|
+
}
|
|
778
|
+
return `concat(${parts.join(",")})`;
|
|
779
|
+
}
|
|
780
|
+
function generateIndexedXPath(baseXPath, index) {
|
|
781
|
+
return `(${baseXPath})[${index}]`;
|
|
782
|
+
}
|
|
783
|
+
function generateIndexedUiAutomator(baseSelector, index) {
|
|
784
|
+
return `${baseSelector}.instance(${index - 1})`;
|
|
785
|
+
}
|
|
786
|
+
function checkUniqueness(ctx, xpath2, targetNode) {
|
|
787
|
+
if (ctx.parsedDOM) {
|
|
788
|
+
return checkXPathUniqueness(ctx.parsedDOM, xpath2, targetNode);
|
|
789
|
+
}
|
|
790
|
+
const match = xpath2.match(/\/\/\*\[@([^=]{1,200})="([^"]{1,2000})"\]/);
|
|
791
|
+
if (match) {
|
|
792
|
+
const [, attr, value] = match;
|
|
793
|
+
return { isUnique: isAttributeUnique(ctx.sourceXML, attr, value) };
|
|
794
|
+
}
|
|
795
|
+
return { isUnique: false };
|
|
796
|
+
}
|
|
797
|
+
function getSiblingIndex(element) {
|
|
798
|
+
const parent = element.parentNode;
|
|
799
|
+
if (!parent) {
|
|
800
|
+
return 1;
|
|
801
|
+
}
|
|
802
|
+
const tagName = element.nodeName;
|
|
803
|
+
let index = 0;
|
|
804
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
805
|
+
const child = parent.childNodes.item(i);
|
|
806
|
+
if (child?.nodeType === 1 && child.nodeName === tagName) {
|
|
807
|
+
index++;
|
|
808
|
+
if (child === element) {
|
|
809
|
+
return index;
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
return 1;
|
|
814
|
+
}
|
|
815
|
+
function countSiblings(element) {
|
|
816
|
+
const parent = element.parentNode;
|
|
817
|
+
if (!parent) {
|
|
818
|
+
return 1;
|
|
819
|
+
}
|
|
820
|
+
const tagName = element.nodeName;
|
|
821
|
+
let count = 0;
|
|
822
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
823
|
+
const child = parent.childNodes.item(i);
|
|
824
|
+
if (child?.nodeType === 1 && child.nodeName === tagName) {
|
|
825
|
+
count++;
|
|
826
|
+
}
|
|
827
|
+
}
|
|
828
|
+
return count;
|
|
829
|
+
}
|
|
830
|
+
function findUniqueAttribute(element, ctx) {
|
|
831
|
+
const attrs = ctx.isAndroid ? ["resource-id", "content-desc", "text"] : ["name", "label", "value"];
|
|
832
|
+
for (const attr of attrs) {
|
|
833
|
+
const value = element.getAttribute(attr);
|
|
834
|
+
if (value && value.trim()) {
|
|
835
|
+
const xpath2 = `//*[@${attr}=${escapeXPathValue(value)}]`;
|
|
836
|
+
const result = ctx.parsedDOM ? checkXPathUniqueness(ctx.parsedDOM, xpath2) : { isUnique: isAttributeUnique(ctx.sourceXML, attr, value) };
|
|
837
|
+
if (result.isUnique) {
|
|
838
|
+
return `@${attr}=${escapeXPathValue(value)}`;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
return null;
|
|
843
|
+
}
|
|
844
|
+
function buildHierarchicalXPath(ctx, element, maxDepth = 3) {
|
|
845
|
+
if (!ctx.parsedDOM) {
|
|
846
|
+
return null;
|
|
847
|
+
}
|
|
848
|
+
const pathParts = [];
|
|
849
|
+
let current = element;
|
|
850
|
+
let depth = 0;
|
|
851
|
+
while (current && depth < maxDepth) {
|
|
852
|
+
const tagName = current.nodeName;
|
|
853
|
+
const uniqueAttr = findUniqueAttribute(current, ctx);
|
|
854
|
+
if (uniqueAttr) {
|
|
855
|
+
pathParts.unshift(`//${tagName}[${uniqueAttr}]`);
|
|
856
|
+
break;
|
|
857
|
+
} else {
|
|
858
|
+
const siblingIndex = getSiblingIndex(current);
|
|
859
|
+
const siblingCount = countSiblings(current);
|
|
860
|
+
if (siblingCount > 1) {
|
|
861
|
+
pathParts.unshift(`${tagName}[${siblingIndex}]`);
|
|
862
|
+
} else {
|
|
863
|
+
pathParts.unshift(tagName);
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
const parent = current.parentNode;
|
|
867
|
+
current = parent && parent.nodeType === 1 ? parent : null;
|
|
868
|
+
depth++;
|
|
869
|
+
}
|
|
870
|
+
if (pathParts.length === 0) {
|
|
871
|
+
return null;
|
|
872
|
+
}
|
|
873
|
+
let result = pathParts[0];
|
|
874
|
+
for (let i = 1; i < pathParts.length; i++) {
|
|
875
|
+
result += "/" + pathParts[i];
|
|
876
|
+
}
|
|
877
|
+
if (!result.startsWith("//")) {
|
|
878
|
+
result = "//" + result;
|
|
879
|
+
}
|
|
880
|
+
return result;
|
|
881
|
+
}
|
|
882
|
+
function addXPathLocator(results, xpath2, ctx, targetNode) {
|
|
883
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
884
|
+
if (uniqueness.isUnique) {
|
|
885
|
+
results.push(["xpath", xpath2]);
|
|
886
|
+
} else if (uniqueness.index) {
|
|
887
|
+
results.push(["xpath", generateIndexedXPath(xpath2, uniqueness.index)]);
|
|
888
|
+
} else {
|
|
889
|
+
if (targetNode && ctx.parsedDOM) {
|
|
890
|
+
const hierarchical = buildHierarchicalXPath(
|
|
891
|
+
ctx,
|
|
892
|
+
targetNode
|
|
893
|
+
);
|
|
894
|
+
if (hierarchical) {
|
|
895
|
+
results.push(["xpath", hierarchical]);
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
results.push(["xpath", xpath2]);
|
|
899
|
+
}
|
|
900
|
+
}
|
|
901
|
+
function isInUiAutomatorScope(element, doc) {
|
|
902
|
+
if (!doc) {
|
|
903
|
+
return true;
|
|
904
|
+
}
|
|
905
|
+
const hierarchyNodes = evaluateXPath(doc, "/hierarchy/*");
|
|
906
|
+
if (hierarchyNodes.length === 0) {
|
|
907
|
+
return true;
|
|
908
|
+
}
|
|
909
|
+
const lastIndex = hierarchyNodes.length;
|
|
910
|
+
const pathParts = element.path.split(".");
|
|
911
|
+
if (pathParts.length === 0 || pathParts[0] === "") {
|
|
912
|
+
return true;
|
|
913
|
+
}
|
|
914
|
+
const firstIndex = parseInt(pathParts[0], 10);
|
|
915
|
+
return firstIndex === lastIndex - 1;
|
|
916
|
+
}
|
|
917
|
+
function buildUiAutomatorSelector(element) {
|
|
918
|
+
const attrs = element.attributes;
|
|
919
|
+
const parts = [];
|
|
920
|
+
if (isValidValue(attrs["resource-id"])) {
|
|
921
|
+
parts.push(`resourceId("${attrs["resource-id"]}")`);
|
|
922
|
+
}
|
|
923
|
+
if (isValidValue(attrs.text) && attrs.text.length < 100) {
|
|
924
|
+
parts.push(`text("${escapeText(attrs.text)}")`);
|
|
925
|
+
}
|
|
926
|
+
if (isValidValue(attrs["content-desc"])) {
|
|
927
|
+
parts.push(`description("${attrs["content-desc"]}")`);
|
|
928
|
+
}
|
|
929
|
+
if (isValidValue(attrs.class)) {
|
|
930
|
+
parts.push(`className("${attrs.class}")`);
|
|
931
|
+
}
|
|
932
|
+
if (parts.length === 0) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
return `android=new UiSelector().${parts.join(".")}`;
|
|
936
|
+
}
|
|
937
|
+
function buildPredicateString(element) {
|
|
938
|
+
const attrs = element.attributes;
|
|
939
|
+
const conditions = [];
|
|
940
|
+
if (isValidValue(attrs.name)) {
|
|
941
|
+
conditions.push(`name == "${escapeText(attrs.name)}"`);
|
|
942
|
+
}
|
|
943
|
+
if (isValidValue(attrs.label)) {
|
|
944
|
+
conditions.push(`label == "${escapeText(attrs.label)}"`);
|
|
945
|
+
}
|
|
946
|
+
if (isValidValue(attrs.value)) {
|
|
947
|
+
conditions.push(`value == "${escapeText(attrs.value)}"`);
|
|
948
|
+
}
|
|
949
|
+
if (attrs.visible === "true") {
|
|
950
|
+
conditions.push("visible == 1");
|
|
951
|
+
}
|
|
952
|
+
if (attrs.enabled === "true") {
|
|
953
|
+
conditions.push("enabled == 1");
|
|
954
|
+
}
|
|
955
|
+
if (conditions.length === 0) {
|
|
956
|
+
return null;
|
|
957
|
+
}
|
|
958
|
+
return `-ios predicate string:${conditions.join(" AND ")}`;
|
|
959
|
+
}
|
|
960
|
+
function buildClassChain(element) {
|
|
961
|
+
const attrs = element.attributes;
|
|
962
|
+
const tagName = element.tagName;
|
|
963
|
+
if (!tagName.startsWith("XCUI")) {
|
|
964
|
+
return null;
|
|
965
|
+
}
|
|
966
|
+
let selector = `**/${tagName}`;
|
|
967
|
+
if (isValidValue(attrs.label)) {
|
|
968
|
+
selector += `[\`label == "${escapeText(attrs.label)}"\`]`;
|
|
969
|
+
} else if (isValidValue(attrs.name)) {
|
|
970
|
+
selector += `[\`name == "${escapeText(attrs.name)}"\`]`;
|
|
971
|
+
}
|
|
972
|
+
return `-ios class chain:${selector}`;
|
|
973
|
+
}
|
|
974
|
+
function buildXPath(element, _sourceXML, isAndroid) {
|
|
975
|
+
const attrs = element.attributes;
|
|
976
|
+
const tagName = element.tagName;
|
|
977
|
+
const conditions = [];
|
|
978
|
+
if (isAndroid) {
|
|
979
|
+
if (isValidValue(attrs["resource-id"])) {
|
|
980
|
+
conditions.push(`@resource-id="${attrs["resource-id"]}"`);
|
|
981
|
+
}
|
|
982
|
+
if (isValidValue(attrs["content-desc"])) {
|
|
983
|
+
conditions.push(`@content-desc="${attrs["content-desc"]}"`);
|
|
984
|
+
}
|
|
985
|
+
if (isValidValue(attrs.text) && attrs.text.length < 100) {
|
|
986
|
+
conditions.push(`@text="${escapeText(attrs.text)}"`);
|
|
987
|
+
}
|
|
988
|
+
} else {
|
|
989
|
+
if (isValidValue(attrs.name)) {
|
|
990
|
+
conditions.push(`@name="${attrs.name}"`);
|
|
991
|
+
}
|
|
992
|
+
if (isValidValue(attrs.label)) {
|
|
993
|
+
conditions.push(`@label="${attrs.label}"`);
|
|
994
|
+
}
|
|
995
|
+
if (isValidValue(attrs.value)) {
|
|
996
|
+
conditions.push(`@value="${attrs.value}"`);
|
|
997
|
+
}
|
|
998
|
+
}
|
|
999
|
+
if (conditions.length === 0) {
|
|
1000
|
+
return `//${tagName}`;
|
|
1001
|
+
}
|
|
1002
|
+
return `//${tagName}[${conditions.join(" and ")}]`;
|
|
1003
|
+
}
|
|
1004
|
+
function getSimpleSuggestedLocators(element, ctx, automationName, inUiAutomatorScope, targetNode) {
|
|
1005
|
+
const results = [];
|
|
1006
|
+
const isAndroid = automationName.toLowerCase().includes("uiautomator");
|
|
1007
|
+
const attrs = element.attributes;
|
|
1008
|
+
if (isAndroid) {
|
|
1009
|
+
const resourceId = attrs["resource-id"];
|
|
1010
|
+
if (isValidValue(resourceId)) {
|
|
1011
|
+
const xpath2 = `//*[@resource-id="${resourceId}"]`;
|
|
1012
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1013
|
+
if (uniqueness.isUnique && inUiAutomatorScope) {
|
|
1014
|
+
results.push([
|
|
1015
|
+
"id",
|
|
1016
|
+
`android=new UiSelector().resourceId("${resourceId}")`
|
|
1017
|
+
]);
|
|
1018
|
+
} else if (uniqueness.index && inUiAutomatorScope) {
|
|
1019
|
+
const base = `android=new UiSelector().resourceId("${resourceId}")`;
|
|
1020
|
+
results.push(["id", generateIndexedUiAutomator(base, uniqueness.index)]);
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
const contentDesc = attrs["content-desc"];
|
|
1024
|
+
if (isValidValue(contentDesc)) {
|
|
1025
|
+
const xpath2 = `//*[@content-desc="${contentDesc}"]`;
|
|
1026
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1027
|
+
if (uniqueness.isUnique) {
|
|
1028
|
+
results.push(["accessibility-id", `~${contentDesc}`]);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
const text = attrs.text;
|
|
1032
|
+
if (isValidValue(text) && text.length < 100) {
|
|
1033
|
+
const xpath2 = `//*[@text="${escapeText(text)}"]`;
|
|
1034
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1035
|
+
if (uniqueness.isUnique && inUiAutomatorScope) {
|
|
1036
|
+
results.push([
|
|
1037
|
+
"text",
|
|
1038
|
+
`android=new UiSelector().text("${escapeText(text)}")`
|
|
1039
|
+
]);
|
|
1040
|
+
} else if (uniqueness.index && inUiAutomatorScope) {
|
|
1041
|
+
const base = `android=new UiSelector().text("${escapeText(text)}")`;
|
|
1042
|
+
results.push([
|
|
1043
|
+
"text",
|
|
1044
|
+
generateIndexedUiAutomator(base, uniqueness.index)
|
|
1045
|
+
]);
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
} else {
|
|
1049
|
+
const name = attrs.name;
|
|
1050
|
+
if (isValidValue(name)) {
|
|
1051
|
+
const xpath2 = `//*[@name="${name}"]`;
|
|
1052
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1053
|
+
if (uniqueness.isUnique) {
|
|
1054
|
+
results.push(["accessibility-id", `~${name}`]);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
const label = attrs.label;
|
|
1058
|
+
if (isValidValue(label) && label !== attrs.name) {
|
|
1059
|
+
const xpath2 = `//*[@label="${escapeText(label)}"]`;
|
|
1060
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1061
|
+
if (uniqueness.isUnique) {
|
|
1062
|
+
results.push([
|
|
1063
|
+
"predicate-string",
|
|
1064
|
+
`-ios predicate string:label == "${escapeText(label)}"`
|
|
1065
|
+
]);
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
const value = attrs.value;
|
|
1069
|
+
if (isValidValue(value)) {
|
|
1070
|
+
const xpath2 = `//*[@value="${escapeText(value)}"]`;
|
|
1071
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1072
|
+
if (uniqueness.isUnique) {
|
|
1073
|
+
results.push([
|
|
1074
|
+
"predicate-string",
|
|
1075
|
+
`-ios predicate string:value == "${escapeText(value)}"`
|
|
1076
|
+
]);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
}
|
|
1080
|
+
return results;
|
|
1081
|
+
}
|
|
1082
|
+
function getComplexSuggestedLocators(element, ctx, automationName, inUiAutomatorScope, targetNode) {
|
|
1083
|
+
const results = [];
|
|
1084
|
+
const isAndroid = automationName.toLowerCase().includes("uiautomator");
|
|
1085
|
+
if (isAndroid) {
|
|
1086
|
+
if (inUiAutomatorScope) {
|
|
1087
|
+
const uiAutomator = buildUiAutomatorSelector(element);
|
|
1088
|
+
if (uiAutomator) {
|
|
1089
|
+
results.push(["uiautomator", uiAutomator]);
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
const xpath2 = buildXPath(element, ctx.sourceXML, true);
|
|
1093
|
+
if (xpath2) {
|
|
1094
|
+
addXPathLocator(results, xpath2, ctx, targetNode);
|
|
1095
|
+
}
|
|
1096
|
+
if (inUiAutomatorScope && isValidValue(element.attributes.class)) {
|
|
1097
|
+
results.push([
|
|
1098
|
+
"class-name",
|
|
1099
|
+
`android=new UiSelector().className("${element.attributes.class}")`
|
|
1100
|
+
]);
|
|
1101
|
+
}
|
|
1102
|
+
} else {
|
|
1103
|
+
const predicate = buildPredicateString(element);
|
|
1104
|
+
if (predicate) {
|
|
1105
|
+
results.push(["predicate-string", predicate]);
|
|
1106
|
+
}
|
|
1107
|
+
const classChain = buildClassChain(element);
|
|
1108
|
+
if (classChain) {
|
|
1109
|
+
results.push(["class-chain", classChain]);
|
|
1110
|
+
}
|
|
1111
|
+
const xpath2 = buildXPath(element, ctx.sourceXML, false);
|
|
1112
|
+
if (xpath2) {
|
|
1113
|
+
addXPathLocator(results, xpath2, ctx, targetNode);
|
|
1114
|
+
}
|
|
1115
|
+
const type = element.tagName;
|
|
1116
|
+
if (type.startsWith("XCUIElementType")) {
|
|
1117
|
+
results.push(["class-name", `-ios class chain:**/${type}`]);
|
|
1118
|
+
}
|
|
1119
|
+
}
|
|
1120
|
+
return results;
|
|
1121
|
+
}
|
|
1122
|
+
function getSuggestedLocators(element, sourceXML, automationName, ctx, targetNode) {
|
|
1123
|
+
const locatorCtx = ctx ?? {
|
|
1124
|
+
sourceXML,
|
|
1125
|
+
parsedDOM: null,
|
|
1126
|
+
isAndroid: automationName.toLowerCase().includes("uiautomator")
|
|
1127
|
+
};
|
|
1128
|
+
const inUiAutomatorScope = locatorCtx.isAndroid ? isInUiAutomatorScope(element, locatorCtx.parsedDOM) : true;
|
|
1129
|
+
const simpleLocators = getSimpleSuggestedLocators(
|
|
1130
|
+
element,
|
|
1131
|
+
locatorCtx,
|
|
1132
|
+
automationName,
|
|
1133
|
+
inUiAutomatorScope,
|
|
1134
|
+
targetNode
|
|
1135
|
+
);
|
|
1136
|
+
const complexLocators = getComplexSuggestedLocators(
|
|
1137
|
+
element,
|
|
1138
|
+
locatorCtx,
|
|
1139
|
+
automationName,
|
|
1140
|
+
inUiAutomatorScope,
|
|
1141
|
+
targetNode
|
|
1142
|
+
);
|
|
1143
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1144
|
+
const results = [];
|
|
1145
|
+
for (const locator of [...simpleLocators, ...complexLocators]) {
|
|
1146
|
+
if (!seen.has(locator[1])) {
|
|
1147
|
+
seen.add(locator[1]);
|
|
1148
|
+
results.push(locator);
|
|
1149
|
+
}
|
|
1150
|
+
}
|
|
1151
|
+
return results;
|
|
1152
|
+
}
|
|
1153
|
+
function locatorsToObject(locators) {
|
|
1154
|
+
const result = {};
|
|
1155
|
+
for (const [strategy, value] of locators) {
|
|
1156
|
+
if (!result[strategy]) {
|
|
1157
|
+
result[strategy] = value;
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
return result;
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
// ../core/src/element-snapshot.ts
|
|
1164
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
1165
|
+
"button",
|
|
1166
|
+
"link",
|
|
1167
|
+
"textbox",
|
|
1168
|
+
"checkbox",
|
|
1169
|
+
"radio",
|
|
1170
|
+
"combobox",
|
|
1171
|
+
"slider",
|
|
1172
|
+
"searchbox",
|
|
1173
|
+
"spinbutton",
|
|
1174
|
+
"switch",
|
|
1175
|
+
"tab",
|
|
1176
|
+
"menuitem",
|
|
1177
|
+
"option"
|
|
1178
|
+
]);
|
|
1179
|
+
function inferPurpose(nodes, index) {
|
|
1180
|
+
const myDepth = nodes[index].depth;
|
|
1181
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
1182
|
+
if (nodes[i].depth <= myDepth && nodes[i].name) {
|
|
1183
|
+
if (nodes[i].depth === myDepth && INTERACTIVE_ROLES.has(nodes[i].role)) {
|
|
1184
|
+
continue;
|
|
1185
|
+
}
|
|
1186
|
+
return nodes[i].name;
|
|
1187
|
+
}
|
|
1188
|
+
}
|
|
1189
|
+
return void 0;
|
|
1190
|
+
}
|
|
1191
|
+
function serializeWebSnapshot(nodes, context, options = {}) {
|
|
1192
|
+
const { inViewportOnly = true } = options;
|
|
1193
|
+
let header = "[Page";
|
|
1194
|
+
if (context?.title) {
|
|
1195
|
+
header += `: ${context.title}`;
|
|
1196
|
+
}
|
|
1197
|
+
if (context?.url) {
|
|
1198
|
+
header += ` \u2014 ${context.url}`;
|
|
1199
|
+
}
|
|
1200
|
+
header += "]";
|
|
1201
|
+
const lines = [header];
|
|
1202
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1203
|
+
const node = nodes[i];
|
|
1204
|
+
if (inViewportOnly && node.isInViewport === false) {
|
|
1205
|
+
continue;
|
|
1206
|
+
}
|
|
1207
|
+
const indent = " ".repeat(node.depth + 1);
|
|
1208
|
+
const isInteractive = INTERACTIVE_ROLES.has(node.role);
|
|
1209
|
+
if (node.role === "statictext" && node.name) {
|
|
1210
|
+
let echoedByParent = false;
|
|
1211
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
1212
|
+
if (nodes[j].depth < node.depth) {
|
|
1213
|
+
const parentRole = nodes[j].role;
|
|
1214
|
+
const parentName = nodes[j].name;
|
|
1215
|
+
if (INTERACTIVE_ROLES.has(parentRole) && parentName && parentName.includes(node.name)) {
|
|
1216
|
+
echoedByParent = true;
|
|
1217
|
+
}
|
|
1218
|
+
break;
|
|
1219
|
+
}
|
|
1220
|
+
}
|
|
1221
|
+
if (echoedByParent) {
|
|
1222
|
+
continue;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const roleLabel = node.role === "heading" && node.level ? `heading[${node.level}]` : node.role;
|
|
1226
|
+
if (isInteractive) {
|
|
1227
|
+
if (!node.selector) {
|
|
1228
|
+
continue;
|
|
1229
|
+
}
|
|
1230
|
+
const purpose = inferPurpose(nodes, i);
|
|
1231
|
+
if (node.name) {
|
|
1232
|
+
lines.push(
|
|
1233
|
+
purpose ? `${indent}${roleLabel} "${node.name}" \u2208 "${purpose}" \u2192 ${node.selector}` : `${indent}${roleLabel} "${node.name}" \u2192 ${node.selector}`
|
|
1234
|
+
);
|
|
1235
|
+
} else if (purpose) {
|
|
1236
|
+
lines.push(`${indent}${roleLabel} \u2208 "${purpose}" \u2192 ${node.selector}`);
|
|
1237
|
+
} else {
|
|
1238
|
+
lines.push(`${indent}${roleLabel} \u2192 ${node.selector}`);
|
|
1239
|
+
}
|
|
1240
|
+
} else {
|
|
1241
|
+
lines.push(
|
|
1242
|
+
node.name ? `${indent}${roleLabel} "${node.name}"` : `${indent}${roleLabel}`
|
|
1243
|
+
);
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
return lines.join("\n");
|
|
1247
|
+
}
|
|
1248
|
+
function simplifyTag(tagName) {
|
|
1249
|
+
const dot = tagName.lastIndexOf(".");
|
|
1250
|
+
if (dot !== -1) {
|
|
1251
|
+
return tagName.slice(dot + 1);
|
|
1252
|
+
}
|
|
1253
|
+
return tagName.replace(/^XCUIElementType/, "");
|
|
1254
|
+
}
|
|
1255
|
+
var ANDROID_ROLE_MAP = {
|
|
1256
|
+
"android.widget.Button": "button",
|
|
1257
|
+
"android.widget.ImageButton": "button",
|
|
1258
|
+
"android.widget.ToggleButton": "button",
|
|
1259
|
+
"android.widget.FloatingActionButton": "button",
|
|
1260
|
+
"com.google.android.material.button.MaterialButton": "button",
|
|
1261
|
+
"com.google.android.material.floatingactionbutton.FloatingActionButton": "button",
|
|
1262
|
+
"android.widget.EditText": "textbox",
|
|
1263
|
+
"android.widget.AutoCompleteTextView": "textbox",
|
|
1264
|
+
"android.widget.MultiAutoCompleteTextView": "textbox",
|
|
1265
|
+
"android.widget.SearchView": "searchbox",
|
|
1266
|
+
"android.widget.ImageView": "img",
|
|
1267
|
+
"android.widget.QuickContactBadge": "img",
|
|
1268
|
+
"android.widget.CheckBox": "checkbox",
|
|
1269
|
+
"android.widget.RadioButton": "radio",
|
|
1270
|
+
"android.widget.Switch": "switch",
|
|
1271
|
+
"android.widget.Spinner": "combobox",
|
|
1272
|
+
"android.widget.SeekBar": "slider",
|
|
1273
|
+
"android.widget.RatingBar": "slider",
|
|
1274
|
+
"android.widget.ProgressBar": "progressbar",
|
|
1275
|
+
"android.widget.TextView": "statictext",
|
|
1276
|
+
"android.widget.CheckedTextView": "statictext",
|
|
1277
|
+
"android.widget.RecyclerView": "list",
|
|
1278
|
+
"android.widget.ListView": "list",
|
|
1279
|
+
"android.widget.GridView": "list",
|
|
1280
|
+
"android.webkit.WebView": "webview"
|
|
1281
|
+
};
|
|
1282
|
+
var IOS_ROLE_MAP = {
|
|
1283
|
+
XCUIElementTypeButton: "button",
|
|
1284
|
+
XCUIElementTypeLink: "link",
|
|
1285
|
+
XCUIElementTypeTextField: "textbox",
|
|
1286
|
+
XCUIElementTypeSecureTextField: "textbox",
|
|
1287
|
+
XCUIElementTypeTextView: "textbox",
|
|
1288
|
+
XCUIElementTypeSearchField: "searchbox",
|
|
1289
|
+
XCUIElementTypeImage: "img",
|
|
1290
|
+
XCUIElementTypeIcon: "img",
|
|
1291
|
+
XCUIElementTypeSwitch: "switch",
|
|
1292
|
+
XCUIElementTypeSlider: "slider",
|
|
1293
|
+
XCUIElementTypeStepper: "slider",
|
|
1294
|
+
XCUIElementTypeCheckBox: "checkbox",
|
|
1295
|
+
XCUIElementTypeRadioButton: "radio",
|
|
1296
|
+
XCUIElementTypePicker: "combobox",
|
|
1297
|
+
XCUIElementTypePickerWheel: "combobox",
|
|
1298
|
+
XCUIElementTypeDatePicker: "combobox",
|
|
1299
|
+
XCUIElementTypeSegmentedControl: "combobox",
|
|
1300
|
+
XCUIElementTypeStaticText: "statictext",
|
|
1301
|
+
XCUIElementTypeCell: "listitem",
|
|
1302
|
+
XCUIElementTypeTable: "list",
|
|
1303
|
+
XCUIElementTypeCollectionView: "list"
|
|
1304
|
+
};
|
|
1305
|
+
function classifyMobileRole(tagName, platform) {
|
|
1306
|
+
if (platform === "android") {
|
|
1307
|
+
return ANDROID_ROLE_MAP[tagName] || simplifyTag(tagName);
|
|
1308
|
+
}
|
|
1309
|
+
return IOS_ROLE_MAP[tagName] || simplifyTag(tagName);
|
|
1310
|
+
}
|
|
1311
|
+
function getBestAndroidLocator(attrs) {
|
|
1312
|
+
if (attrs._selector) {
|
|
1313
|
+
return attrs._selector;
|
|
1314
|
+
}
|
|
1315
|
+
if (attrs["content-desc"]) {
|
|
1316
|
+
return `~${attrs["content-desc"]}`;
|
|
1317
|
+
}
|
|
1318
|
+
if (attrs["resource-id"]) {
|
|
1319
|
+
return `id:${attrs["resource-id"]}`;
|
|
1320
|
+
}
|
|
1321
|
+
if (attrs.text) {
|
|
1322
|
+
return `~${attrs.text}`;
|
|
1323
|
+
}
|
|
1324
|
+
if (attrs.class) {
|
|
1325
|
+
return `class:${simplifyTag(attrs.class)}`;
|
|
1326
|
+
}
|
|
1327
|
+
return void 0;
|
|
1328
|
+
}
|
|
1329
|
+
function getBestIOSLocator(attrs) {
|
|
1330
|
+
if (attrs._selector) {
|
|
1331
|
+
return attrs._selector;
|
|
1332
|
+
}
|
|
1333
|
+
if (attrs.name) {
|
|
1334
|
+
return `~${attrs.name}`;
|
|
1335
|
+
}
|
|
1336
|
+
if (attrs.label) {
|
|
1337
|
+
return `~${attrs.label}`;
|
|
1338
|
+
}
|
|
1339
|
+
if (attrs.value) {
|
|
1340
|
+
return `~${attrs.value}`;
|
|
1341
|
+
}
|
|
1342
|
+
if (attrs.type) {
|
|
1343
|
+
return `class:${simplifyTag(attrs.type)}`;
|
|
1344
|
+
}
|
|
1345
|
+
return void 0;
|
|
1346
|
+
}
|
|
1347
|
+
function getMobileNodeIdentity(attrs, platform) {
|
|
1348
|
+
if (platform === "android") {
|
|
1349
|
+
const contentDesc = attrs["content-desc"];
|
|
1350
|
+
if (contentDesc) {
|
|
1351
|
+
return contentDesc;
|
|
1352
|
+
}
|
|
1353
|
+
if (attrs.text) {
|
|
1354
|
+
return attrs.text;
|
|
1355
|
+
}
|
|
1356
|
+
const rid = attrs["resource-id"];
|
|
1357
|
+
if (rid) {
|
|
1358
|
+
const slash = rid.lastIndexOf("/");
|
|
1359
|
+
return slash !== -1 ? rid.slice(slash + 1) : rid;
|
|
1360
|
+
}
|
|
1361
|
+
return "";
|
|
1362
|
+
}
|
|
1363
|
+
return attrs.name || attrs.label || attrs.value || attrs.text || "";
|
|
1364
|
+
}
|
|
1365
|
+
var ANDROID_INTERACTABLE_SET = new Set(ANDROID_INTERACTABLE_TAGS);
|
|
1366
|
+
var IOS_INTERACTABLE_SET = new Set(IOS_INTERACTABLE_TAGS);
|
|
1367
|
+
function isExplicitlyInteractive(attrs, platform) {
|
|
1368
|
+
if (platform === "android") {
|
|
1369
|
+
return attrs.clickable === "true" || attrs.focusable === "true" || attrs.checkable === "true" || attrs["long-clickable"] === "true";
|
|
1370
|
+
}
|
|
1371
|
+
return attrs.accessible === "true";
|
|
1372
|
+
}
|
|
1373
|
+
function isMobileInteractive(element, platform) {
|
|
1374
|
+
const attrs = element.attributes;
|
|
1375
|
+
if (platform === "android") {
|
|
1376
|
+
if (ANDROID_INTERACTABLE_SET.has(element.tagName)) {
|
|
1377
|
+
return true;
|
|
1378
|
+
}
|
|
1379
|
+
return attrs.clickable === "true" || attrs["long-clickable"] === "true" || attrs.focusable === "true" || attrs.checkable === "true";
|
|
1380
|
+
}
|
|
1381
|
+
if (IOS_INTERACTABLE_SET.has(element.tagName)) {
|
|
1382
|
+
return true;
|
|
1383
|
+
}
|
|
1384
|
+
return attrs.accessible === "true";
|
|
1385
|
+
}
|
|
1386
|
+
function isMobileInViewport(element, platform, viewport) {
|
|
1387
|
+
const bounds = platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
|
|
1388
|
+
if (bounds.width === 0 && bounds.height === 0) {
|
|
1389
|
+
return true;
|
|
1390
|
+
}
|
|
1391
|
+
return bounds.x >= 0 && bounds.y >= 0 && bounds.width > 0 && bounds.height > 0 && bounds.x + bounds.width <= viewport.width && bounds.y + bounds.height <= viewport.height;
|
|
1392
|
+
}
|
|
1393
|
+
function collectMobileNodes(element, platform, depth, nodes, walkOpts) {
|
|
1394
|
+
const attrs = element.attributes;
|
|
1395
|
+
const role = classifyMobileRole(element.tagName, platform);
|
|
1396
|
+
const name = getMobileNodeIdentity(attrs, platform);
|
|
1397
|
+
const explicit = isExplicitlyInteractive(attrs, platform);
|
|
1398
|
+
const interactive = isMobileInteractive(element, platform);
|
|
1399
|
+
const inViewport = isMobileInViewport(element, platform, walkOpts.viewport);
|
|
1400
|
+
if (walkOpts.inViewportOnly) {
|
|
1401
|
+
if (interactive && !inViewport) {
|
|
1402
|
+
for (const child of element.children || []) {
|
|
1403
|
+
collectMobileNodes(child, platform, depth + 1, nodes, walkOpts);
|
|
1404
|
+
}
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
if (!interactive && !inViewport) {
|
|
1408
|
+
nodes.push({
|
|
1409
|
+
role: "generic",
|
|
1410
|
+
name: name ? `${role} "${name}"` : role,
|
|
1411
|
+
selector: "",
|
|
1412
|
+
depth,
|
|
1413
|
+
isInteractive: false,
|
|
1414
|
+
isExplicitInteractive: false,
|
|
1415
|
+
isInViewport: false
|
|
1416
|
+
});
|
|
1417
|
+
return;
|
|
1418
|
+
}
|
|
1419
|
+
}
|
|
1420
|
+
let locator = "";
|
|
1421
|
+
if (interactive) {
|
|
1422
|
+
if (walkOpts.sourceXML && walkOpts.automationName) {
|
|
1423
|
+
const suggested = getSuggestedLocators(
|
|
1424
|
+
element,
|
|
1425
|
+
walkOpts.sourceXML,
|
|
1426
|
+
walkOpts.automationName,
|
|
1427
|
+
{
|
|
1428
|
+
sourceXML: walkOpts.sourceXML,
|
|
1429
|
+
parsedDOM: null,
|
|
1430
|
+
isAndroid: platform === "android"
|
|
1431
|
+
}
|
|
1432
|
+
);
|
|
1433
|
+
if (suggested.length > 0) {
|
|
1434
|
+
locator = suggested[0][1];
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
if (!locator) {
|
|
1438
|
+
locator = (platform === "android" ? getBestAndroidLocator(attrs) : getBestIOSLocator(attrs)) ?? "";
|
|
1439
|
+
}
|
|
1440
|
+
}
|
|
1441
|
+
nodes.push({
|
|
1442
|
+
role,
|
|
1443
|
+
name,
|
|
1444
|
+
selector: locator,
|
|
1445
|
+
depth,
|
|
1446
|
+
isInteractive: interactive,
|
|
1447
|
+
isExplicitInteractive: explicit,
|
|
1448
|
+
isInViewport: inViewport
|
|
1449
|
+
});
|
|
1450
|
+
for (const child of element.children || []) {
|
|
1451
|
+
collectMobileNodes(child, platform, depth + 1, nodes, walkOpts);
|
|
1452
|
+
}
|
|
1453
|
+
}
|
|
1454
|
+
var MOBILE_STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
1455
|
+
"img",
|
|
1456
|
+
"heading",
|
|
1457
|
+
"list",
|
|
1458
|
+
"listitem",
|
|
1459
|
+
"webview",
|
|
1460
|
+
"progressbar",
|
|
1461
|
+
"slider",
|
|
1462
|
+
"switch",
|
|
1463
|
+
"generic"
|
|
1464
|
+
]);
|
|
1465
|
+
function mobileInferPurpose(nodes, index) {
|
|
1466
|
+
const myDepth = nodes[index].depth;
|
|
1467
|
+
for (let i = index - 1; i >= 0; i--) {
|
|
1468
|
+
if (nodes[i].depth <= myDepth && nodes[i].name) {
|
|
1469
|
+
if (nodes[i].depth === myDepth && !MOBILE_STRUCTURAL_ROLES.has(nodes[i].role)) {
|
|
1470
|
+
continue;
|
|
1471
|
+
}
|
|
1472
|
+
return nodes[i].name;
|
|
1473
|
+
}
|
|
1474
|
+
}
|
|
1475
|
+
return void 0;
|
|
1476
|
+
}
|
|
1477
|
+
function suppressTagOnlyChildren(nodes) {
|
|
1478
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1479
|
+
const node = nodes[i];
|
|
1480
|
+
if (!node.isInteractive || node.isExplicitInteractive) {
|
|
1481
|
+
continue;
|
|
1482
|
+
}
|
|
1483
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
1484
|
+
if (nodes[j].depth < node.depth) {
|
|
1485
|
+
if (nodes[j].isExplicitInteractive) {
|
|
1486
|
+
node.isInteractive = false;
|
|
1487
|
+
break;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
var NOISY_ROLES = /* @__PURE__ */ new Set([
|
|
1494
|
+
"FrameLayout",
|
|
1495
|
+
"LinearLayout",
|
|
1496
|
+
"ViewGroup",
|
|
1497
|
+
"RelativeLayout",
|
|
1498
|
+
"View",
|
|
1499
|
+
"CardView",
|
|
1500
|
+
"ConstraintLayout",
|
|
1501
|
+
"ScrollView"
|
|
1502
|
+
]);
|
|
1503
|
+
function countSelectors(nodes) {
|
|
1504
|
+
const counts = /* @__PURE__ */ new Map();
|
|
1505
|
+
for (const node of nodes) {
|
|
1506
|
+
if (node.selector) {
|
|
1507
|
+
counts.set(node.selector, (counts.get(node.selector) ?? 0) + 1);
|
|
1508
|
+
}
|
|
1509
|
+
}
|
|
1510
|
+
return counts;
|
|
1511
|
+
}
|
|
1512
|
+
function renderMobileNodes(nodes) {
|
|
1513
|
+
const lines = [];
|
|
1514
|
+
const selectorCounts = countSelectors(nodes);
|
|
1515
|
+
const selectorIndex = /* @__PURE__ */ new Map();
|
|
1516
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
1517
|
+
const node = nodes[i];
|
|
1518
|
+
const indent = " ".repeat(node.depth + 1);
|
|
1519
|
+
if (NOISY_ROLES.has(node.role) && !node.name && node.depth > 1 && !node.isInteractive) {
|
|
1520
|
+
continue;
|
|
1521
|
+
}
|
|
1522
|
+
if (node.isInViewport === false && !node.isInteractive) {
|
|
1523
|
+
lines.push(`${indent}\u22EF ${node.name} (off-screen)`);
|
|
1524
|
+
continue;
|
|
1525
|
+
}
|
|
1526
|
+
if (node.role === "statictext" && node.name) {
|
|
1527
|
+
let echoedByParent = false;
|
|
1528
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
1529
|
+
if (nodes[j].depth < node.depth) {
|
|
1530
|
+
if (nodes[j].isInteractive && nodes[j].name && nodes[j].name.includes(node.name)) {
|
|
1531
|
+
echoedByParent = true;
|
|
1532
|
+
}
|
|
1533
|
+
break;
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
if (echoedByParent) {
|
|
1537
|
+
continue;
|
|
1538
|
+
}
|
|
1539
|
+
}
|
|
1540
|
+
if (node.isInteractive && node.selector) {
|
|
1541
|
+
let selector = node.selector;
|
|
1542
|
+
const total = selectorCounts.get(selector) ?? 1;
|
|
1543
|
+
if (total > 1) {
|
|
1544
|
+
const idx = selectorIndex.get(selector) ?? 0;
|
|
1545
|
+
selectorIndex.set(selector, idx + 1);
|
|
1546
|
+
selector = `${selector}.instance(${idx})`;
|
|
1547
|
+
}
|
|
1548
|
+
const purpose = mobileInferPurpose(nodes, i);
|
|
1549
|
+
if (node.name) {
|
|
1550
|
+
lines.push(
|
|
1551
|
+
purpose ? `${indent}${node.role} "${node.name}" \u2208 "${purpose}" \u2192 ${selector}` : `${indent}${node.role} "${node.name}" \u2192 ${selector}`
|
|
1552
|
+
);
|
|
1553
|
+
} else if (purpose) {
|
|
1554
|
+
lines.push(`${indent}${node.role} \u2208 "${purpose}" \u2192 ${selector}`);
|
|
1555
|
+
} else {
|
|
1556
|
+
lines.push(`${indent}${node.role} \u2192 ${selector}`);
|
|
1557
|
+
}
|
|
1558
|
+
} else {
|
|
1559
|
+
lines.push(
|
|
1560
|
+
node.name ? `${indent}${node.role} "${node.name}"` : `${indent}${node.role}`
|
|
1561
|
+
);
|
|
1562
|
+
}
|
|
1563
|
+
}
|
|
1564
|
+
return lines;
|
|
1565
|
+
}
|
|
1566
|
+
function serializeMobileSnapshot(root, context, options = {}) {
|
|
1567
|
+
const { platform, deviceName, viewport, sourceXML } = context;
|
|
1568
|
+
const { inViewportOnly = true } = options;
|
|
1569
|
+
const effectiveXML = sourceXML || root.attributes._sourceXML;
|
|
1570
|
+
const effectiveViewport = viewport ?? { width: 9999, height: 9999 };
|
|
1571
|
+
const automationName = platform === "android" ? "uiautomator2" : "xcuitest";
|
|
1572
|
+
let header = `[${platform}`;
|
|
1573
|
+
if (deviceName) {
|
|
1574
|
+
header += ` \u2014 ${deviceName}`;
|
|
1575
|
+
}
|
|
1576
|
+
if (viewport) {
|
|
1577
|
+
header += ` (${viewport.width}\xD7${viewport.height})`;
|
|
1578
|
+
}
|
|
1579
|
+
header += "]";
|
|
1580
|
+
const nodes = [];
|
|
1581
|
+
collectMobileNodes(root, platform, 0, nodes, {
|
|
1582
|
+
inViewportOnly,
|
|
1583
|
+
viewport: effectiveViewport,
|
|
1584
|
+
sourceXML: effectiveXML,
|
|
1585
|
+
automationName: effectiveXML ? automationName : void 0
|
|
1586
|
+
});
|
|
1587
|
+
suppressTagOnlyChildren(nodes);
|
|
1588
|
+
const lines = renderMobileNodes(nodes);
|
|
1589
|
+
return [header, ...lines].join("\n");
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
// ../core/src/locators/element-filter.ts
|
|
1593
|
+
function matchesTagList(tagName, tagList) {
|
|
1594
|
+
if (tagList.includes(tagName)) {
|
|
1595
|
+
return true;
|
|
1596
|
+
}
|
|
1597
|
+
for (const tag of tagList) {
|
|
1598
|
+
if (tagName.endsWith(tag) || tagName.includes(tag)) {
|
|
1599
|
+
return true;
|
|
1600
|
+
}
|
|
1601
|
+
}
|
|
1602
|
+
return false;
|
|
1603
|
+
}
|
|
1604
|
+
function matchesTagFilters(element, includeTagNames, excludeTagNames) {
|
|
1605
|
+
if (includeTagNames.length > 0 && !matchesTagList(element.tagName, includeTagNames)) {
|
|
1606
|
+
return false;
|
|
1607
|
+
}
|
|
1608
|
+
if (matchesTagList(element.tagName, excludeTagNames)) {
|
|
1609
|
+
return false;
|
|
1610
|
+
}
|
|
1611
|
+
return true;
|
|
1612
|
+
}
|
|
1613
|
+
function matchesAttributeFilters(element, requireAttributes, minAttributeCount) {
|
|
1614
|
+
if (requireAttributes.length > 0) {
|
|
1615
|
+
const hasRequiredAttr = requireAttributes.some(
|
|
1616
|
+
(attr) => element.attributes?.[attr]
|
|
1617
|
+
);
|
|
1618
|
+
if (!hasRequiredAttr) {
|
|
1619
|
+
return false;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
if (element.attributes && minAttributeCount > 0) {
|
|
1623
|
+
const attrCount = Object.values(element.attributes).filter(
|
|
1624
|
+
(v) => v !== void 0 && v !== null && v !== ""
|
|
1625
|
+
).length;
|
|
1626
|
+
if (attrCount < minAttributeCount) {
|
|
1627
|
+
return false;
|
|
1628
|
+
}
|
|
1629
|
+
}
|
|
1630
|
+
return true;
|
|
1631
|
+
}
|
|
1632
|
+
function isInteractableElement(element, _isNative, automationName) {
|
|
1633
|
+
const isAndroid = automationName.toLowerCase().includes("uiautomator");
|
|
1634
|
+
const interactableTags = isAndroid ? ANDROID_INTERACTABLE_TAGS : IOS_INTERACTABLE_TAGS;
|
|
1635
|
+
if (matchesTagList(element.tagName, interactableTags)) {
|
|
1636
|
+
return true;
|
|
1637
|
+
}
|
|
1638
|
+
if (isAndroid) {
|
|
1639
|
+
if (element.attributes?.clickable === "true" || element.attributes?.focusable === "true" || element.attributes?.checkable === "true" || element.attributes?.["long-clickable"] === "true") {
|
|
1640
|
+
return true;
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
if (!isAndroid) {
|
|
1644
|
+
if (element.attributes?.accessible === "true") {
|
|
1645
|
+
return true;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1648
|
+
return false;
|
|
1649
|
+
}
|
|
1650
|
+
function isLayoutContainer(element, platform) {
|
|
1651
|
+
const containerList = platform === "android" ? ANDROID_LAYOUT_CONTAINERS : IOS_LAYOUT_CONTAINERS;
|
|
1652
|
+
return matchesTagList(element.tagName, containerList);
|
|
1653
|
+
}
|
|
1654
|
+
function hasMeaningfulContent(element, platform) {
|
|
1655
|
+
const attrs = element.attributes;
|
|
1656
|
+
if (attrs.text && attrs.text.trim() !== "" && attrs.text !== "null") {
|
|
1657
|
+
return true;
|
|
1658
|
+
}
|
|
1659
|
+
if (platform === "android") {
|
|
1660
|
+
if (attrs["content-desc"] && attrs["content-desc"].trim() !== "" && attrs["content-desc"] !== "null") {
|
|
1661
|
+
return true;
|
|
1662
|
+
}
|
|
1663
|
+
} else {
|
|
1664
|
+
if (attrs.label && attrs.label.trim() !== "" && attrs.label !== "null") {
|
|
1665
|
+
return true;
|
|
1666
|
+
}
|
|
1667
|
+
if (attrs.name && attrs.name.trim() !== "" && attrs.name !== "null") {
|
|
1668
|
+
return true;
|
|
1669
|
+
}
|
|
1670
|
+
}
|
|
1671
|
+
return false;
|
|
1672
|
+
}
|
|
1673
|
+
function shouldIncludeElement(element, filters, isNative, automationName) {
|
|
1674
|
+
const {
|
|
1675
|
+
includeTagNames = [],
|
|
1676
|
+
excludeTagNames = ["hierarchy"],
|
|
1677
|
+
requireAttributes = [],
|
|
1678
|
+
minAttributeCount = 0,
|
|
1679
|
+
fetchableOnly = false,
|
|
1680
|
+
clickableOnly = false,
|
|
1681
|
+
visibleOnly = true
|
|
1682
|
+
} = filters;
|
|
1683
|
+
if (!matchesTagFilters(element, includeTagNames, excludeTagNames)) {
|
|
1684
|
+
if (element.attributes?.clickable !== "true") {
|
|
1685
|
+
return false;
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
if (!matchesAttributeFilters(element, requireAttributes, minAttributeCount)) {
|
|
1689
|
+
return false;
|
|
1690
|
+
}
|
|
1691
|
+
if (clickableOnly && element.attributes?.clickable !== "true") {
|
|
1692
|
+
return false;
|
|
1693
|
+
}
|
|
1694
|
+
if (visibleOnly) {
|
|
1695
|
+
const isAndroid = automationName.toLowerCase().includes("uiautomator");
|
|
1696
|
+
if (isAndroid && element.attributes?.displayed === "false") {
|
|
1697
|
+
return false;
|
|
1698
|
+
}
|
|
1699
|
+
if (!isAndroid && element.attributes?.visible === "false") {
|
|
1700
|
+
return false;
|
|
1701
|
+
}
|
|
1702
|
+
}
|
|
1703
|
+
if (fetchableOnly && !isInteractableElement(element, isNative, automationName)) {
|
|
1704
|
+
return false;
|
|
1705
|
+
}
|
|
1706
|
+
return true;
|
|
1707
|
+
}
|
|
1708
|
+
function getDefaultFilters(platform, includeContainers = false) {
|
|
1709
|
+
const layoutContainers = platform === "android" ? ANDROID_LAYOUT_CONTAINERS : IOS_LAYOUT_CONTAINERS;
|
|
1710
|
+
return {
|
|
1711
|
+
excludeTagNames: includeContainers ? ["hierarchy"] : ["hierarchy", ...layoutContainers],
|
|
1712
|
+
fetchableOnly: !includeContainers,
|
|
1713
|
+
visibleOnly: true,
|
|
1714
|
+
clickableOnly: false
|
|
1715
|
+
};
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
// ../core/src/locators/index.ts
|
|
1719
|
+
function parseBounds(element, platform) {
|
|
1720
|
+
return platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
|
|
1721
|
+
}
|
|
1722
|
+
function isWithinViewport(bounds, viewport) {
|
|
1723
|
+
return bounds.x >= 0 && bounds.y >= 0 && bounds.width > 0 && bounds.height > 0 && bounds.x + bounds.width <= viewport.width && bounds.y + bounds.height <= viewport.height;
|
|
1724
|
+
}
|
|
1725
|
+
function transformElement(element, locators, ctx) {
|
|
1726
|
+
const attrs = element.attributes;
|
|
1727
|
+
const bounds = parseBounds(element, ctx.platform);
|
|
1728
|
+
return {
|
|
1729
|
+
tagName: element.tagName,
|
|
1730
|
+
locators: locatorsToObject(locators),
|
|
1731
|
+
text: attrs.text || attrs.label || "",
|
|
1732
|
+
contentDesc: attrs["content-desc"] || "",
|
|
1733
|
+
resourceId: attrs["resource-id"] || "",
|
|
1734
|
+
accessibilityId: attrs.name || attrs["content-desc"] || "",
|
|
1735
|
+
label: attrs.label || "",
|
|
1736
|
+
value: attrs.value || "",
|
|
1737
|
+
className: attrs.class || element.tagName,
|
|
1738
|
+
clickable: attrs.clickable === "true" || attrs.accessible === "true" || attrs["long-clickable"] === "true",
|
|
1739
|
+
enabled: attrs.enabled !== "false",
|
|
1740
|
+
displayed: ctx.platform === "android" ? attrs.displayed !== "false" : attrs.visible !== "false",
|
|
1741
|
+
bounds,
|
|
1742
|
+
isInViewport: isWithinViewport(bounds, ctx.viewportSize)
|
|
1743
|
+
};
|
|
1744
|
+
}
|
|
1745
|
+
function shouldProcess(element, ctx) {
|
|
1746
|
+
if (shouldIncludeElement(element, ctx.filters, ctx.isNative, ctx.automationName)) {
|
|
1747
|
+
return true;
|
|
1748
|
+
}
|
|
1749
|
+
return isLayoutContainer(element, ctx.platform) && hasMeaningfulContent(element, ctx.platform);
|
|
1750
|
+
}
|
|
1751
|
+
function processElement(element, ctx) {
|
|
1752
|
+
if (!shouldProcess(element, ctx)) {
|
|
1753
|
+
return;
|
|
1754
|
+
}
|
|
1755
|
+
if (ctx.inViewportOnly) {
|
|
1756
|
+
const b = parseBounds(element, ctx.platform);
|
|
1757
|
+
if (!isWithinViewport(b, ctx.viewportSize)) {
|
|
1758
|
+
return;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
try {
|
|
1762
|
+
const targetNode = ctx.parsedDOM ? findDOMNodeByPath(ctx.parsedDOM, element.path) : void 0;
|
|
1763
|
+
const locators = getSuggestedLocators(
|
|
1764
|
+
element,
|
|
1765
|
+
ctx.sourceXML,
|
|
1766
|
+
ctx.automationName,
|
|
1767
|
+
{
|
|
1768
|
+
sourceXML: ctx.sourceXML,
|
|
1769
|
+
parsedDOM: ctx.parsedDOM,
|
|
1770
|
+
isAndroid: ctx.platform === "android"
|
|
1771
|
+
},
|
|
1772
|
+
targetNode || void 0
|
|
1773
|
+
);
|
|
1774
|
+
if (locators.length === 0) {
|
|
1775
|
+
return;
|
|
1776
|
+
}
|
|
1777
|
+
element.attributes._selector = locators[0][1];
|
|
1778
|
+
const transformed = transformElement(element, locators, ctx);
|
|
1779
|
+
if (Object.keys(transformed.locators).length === 0) {
|
|
1780
|
+
return;
|
|
1781
|
+
}
|
|
1782
|
+
ctx.results.push(transformed);
|
|
1783
|
+
} catch (error) {
|
|
1784
|
+
console.error(`[processElement] Error at path ${element.path}:`, error);
|
|
1785
|
+
}
|
|
1786
|
+
}
|
|
1787
|
+
function traverseTree(element, ctx) {
|
|
1788
|
+
if (!element) {
|
|
1789
|
+
return;
|
|
1790
|
+
}
|
|
1791
|
+
processElement(element, ctx);
|
|
1792
|
+
for (const child of element.children || []) {
|
|
1793
|
+
traverseTree(child, ctx);
|
|
1794
|
+
}
|
|
1795
|
+
}
|
|
1796
|
+
function generateAllElementLocators(sourceXML, options) {
|
|
1797
|
+
const sourceJSON = xmlToJSON(sourceXML);
|
|
1798
|
+
if (!sourceJSON) {
|
|
1799
|
+
console.error(
|
|
1800
|
+
"[generateAllElementLocators] Failed to parse page source XML"
|
|
1801
|
+
);
|
|
1802
|
+
return [];
|
|
1803
|
+
}
|
|
1804
|
+
const parsedDOM = xmlToDOM(sourceXML);
|
|
1805
|
+
const ctx = {
|
|
1806
|
+
sourceXML,
|
|
1807
|
+
platform: options.platform,
|
|
1808
|
+
automationName: options.platform === "android" ? "uiautomator2" : "xcuitest",
|
|
1809
|
+
isNative: options.isNative ?? true,
|
|
1810
|
+
viewportSize: options.viewportSize ?? { width: 9999, height: 9999 },
|
|
1811
|
+
filters: options.filters ?? {},
|
|
1812
|
+
inViewportOnly: options.inViewportOnly ?? true,
|
|
1813
|
+
results: [],
|
|
1814
|
+
parsedDOM
|
|
1815
|
+
};
|
|
1816
|
+
traverseTree(sourceJSON, ctx);
|
|
1817
|
+
return ctx.results;
|
|
1818
|
+
}
|
|
1819
|
+
|
|
1820
|
+
// ../core/src/with-timeout.ts
|
|
1821
|
+
function withTimeout(promise, ms, fallback) {
|
|
1822
|
+
let timer;
|
|
1823
|
+
const timeout = new Promise((resolve2) => {
|
|
1824
|
+
timer = setTimeout(() => resolve2(fallback), ms);
|
|
1825
|
+
});
|
|
1826
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
1827
|
+
if (timer) {
|
|
1828
|
+
clearTimeout(timer);
|
|
1829
|
+
}
|
|
1830
|
+
});
|
|
1831
|
+
}
|
|
1832
|
+
var SNAPSHOT_PROBE_TIMEOUT_MS = 2500;
|
|
1833
|
+
|
|
1834
|
+
// ../core/src/action-snapshot.ts
|
|
1835
|
+
async function runWith(runScript, scriptSrc, fallback) {
|
|
1836
|
+
if (!runScript) {
|
|
1837
|
+
return fallback;
|
|
1838
|
+
}
|
|
1839
|
+
return withTimeout(
|
|
1840
|
+
runScript(scriptSrc).then((r) => r),
|
|
1841
|
+
SNAPSHOT_PROBE_TIMEOUT_MS,
|
|
1842
|
+
fallback
|
|
1843
|
+
).catch(() => fallback);
|
|
1844
|
+
}
|
|
1845
|
+
async function captureActionSnapshot(input) {
|
|
1846
|
+
try {
|
|
1847
|
+
const timestamp = Date.now();
|
|
1848
|
+
const isNativeMobile = !input.runScript && !!input.getPageSource;
|
|
1849
|
+
const [shot, url, title, pageSource, tree, elements] = await Promise.all([
|
|
1850
|
+
input.takeScreenshot?.().catch(() => null),
|
|
1851
|
+
input.getUrl?.().catch(() => void 0),
|
|
1852
|
+
input.getTitle?.().catch(() => void 0),
|
|
1853
|
+
isNativeMobile ? input.getPageSource?.().catch(() => void 0) : void 0,
|
|
1854
|
+
runWith(
|
|
1855
|
+
input.runScript,
|
|
1856
|
+
accessibilityTreeScript(true),
|
|
1857
|
+
[]
|
|
1858
|
+
),
|
|
1859
|
+
runWith(
|
|
1860
|
+
input.runScript,
|
|
1861
|
+
elementsScript(false, true),
|
|
1862
|
+
[]
|
|
1863
|
+
)
|
|
1864
|
+
]);
|
|
1865
|
+
let snapshotText;
|
|
1866
|
+
let finalElements = elements;
|
|
1867
|
+
if (isNativeMobile && pageSource) {
|
|
1868
|
+
const platform = input.platform ?? "android";
|
|
1869
|
+
const jsonTree = xmlToJSON(pageSource);
|
|
1870
|
+
if (jsonTree) {
|
|
1871
|
+
jsonTree.attributes._sourceXML = pageSource;
|
|
1872
|
+
snapshotText = serializeMobileSnapshot(jsonTree, {
|
|
1873
|
+
platform,
|
|
1874
|
+
sourceXML: pageSource
|
|
1875
|
+
});
|
|
1876
|
+
} else {
|
|
1877
|
+
snapshotText = `[${platform}]`;
|
|
1878
|
+
}
|
|
1879
|
+
try {
|
|
1880
|
+
const viewport = { width: 9999, height: 9999 };
|
|
1881
|
+
const filters = getDefaultFilters(platform, false);
|
|
1882
|
+
const locators = generateAllElementLocators(pageSource, {
|
|
1883
|
+
platform,
|
|
1884
|
+
viewportSize: viewport,
|
|
1885
|
+
filters,
|
|
1886
|
+
inViewportOnly: false
|
|
1887
|
+
});
|
|
1888
|
+
finalElements = locators;
|
|
1889
|
+
} catch {
|
|
1890
|
+
}
|
|
1891
|
+
} else {
|
|
1892
|
+
snapshotText = serializeWebSnapshot(tree, { url, title });
|
|
1893
|
+
}
|
|
1894
|
+
return {
|
|
1895
|
+
timestamp,
|
|
1896
|
+
command: input.command,
|
|
1897
|
+
url,
|
|
1898
|
+
title,
|
|
1899
|
+
screenshot: shot ?? void 0,
|
|
1900
|
+
elements: finalElements,
|
|
1901
|
+
snapshotText
|
|
1902
|
+
};
|
|
1903
|
+
} catch {
|
|
1904
|
+
return null;
|
|
1905
|
+
}
|
|
1906
|
+
}
|
|
1907
|
+
|
|
20
1908
|
// ../core/src/stack.ts
|
|
21
1909
|
import { parse as parseStackTrace } from "stacktrace-parser";
|
|
22
1910
|
function isUserCodeFrame(frame) {
|
|
@@ -44,6 +1932,462 @@ function getCallSourceFromStack() {
|
|
|
44
1932
|
return { filePath, callSource: `${filePath}:${frame.lineNumber ?? 0}` };
|
|
45
1933
|
}
|
|
46
1934
|
|
|
1935
|
+
// ../core/src/trace-exporter.ts
|
|
1936
|
+
import fs from "fs/promises";
|
|
1937
|
+
import path from "path";
|
|
1938
|
+
|
|
1939
|
+
// ../core/src/trace-har.ts
|
|
1940
|
+
function toHeaderArray(h) {
|
|
1941
|
+
if (!h) {
|
|
1942
|
+
return [];
|
|
1943
|
+
}
|
|
1944
|
+
return Object.entries(h).map(([name, value]) => ({ name, value }));
|
|
1945
|
+
}
|
|
1946
|
+
function toQueryString(url) {
|
|
1947
|
+
try {
|
|
1948
|
+
const u = new URL(url);
|
|
1949
|
+
const out = [];
|
|
1950
|
+
u.searchParams.forEach((value, name) => out.push({ name, value }));
|
|
1951
|
+
return out;
|
|
1952
|
+
} catch {
|
|
1953
|
+
return [];
|
|
1954
|
+
}
|
|
1955
|
+
}
|
|
1956
|
+
function networkRequestToHar(entry) {
|
|
1957
|
+
const startedDateTime = new Date(entry.timestamp).toISOString();
|
|
1958
|
+
const duration = entry.time ?? (entry.endTime ?? entry.startTime) - entry.startTime;
|
|
1959
|
+
const status = entry.response?.status ?? entry.status ?? 0;
|
|
1960
|
+
const mimeType = entry.response?.mimeType ?? "";
|
|
1961
|
+
const responseHeaders = entry.response?.headers ?? entry.responseHeaders;
|
|
1962
|
+
return {
|
|
1963
|
+
type: "resource-snapshot",
|
|
1964
|
+
snapshot: {
|
|
1965
|
+
startedDateTime,
|
|
1966
|
+
time: Math.max(0, duration),
|
|
1967
|
+
request: {
|
|
1968
|
+
method: entry.method,
|
|
1969
|
+
url: entry.url,
|
|
1970
|
+
httpVersion: "HTTP/1.1",
|
|
1971
|
+
cookies: [],
|
|
1972
|
+
headers: toHeaderArray(entry.requestHeaders ?? entry.headers),
|
|
1973
|
+
queryString: toQueryString(entry.url),
|
|
1974
|
+
headersSize: -1,
|
|
1975
|
+
bodySize: entry.requestBody ? entry.requestBody.length : -1
|
|
1976
|
+
},
|
|
1977
|
+
response: {
|
|
1978
|
+
status,
|
|
1979
|
+
statusText: entry.statusText ?? "",
|
|
1980
|
+
httpVersion: "HTTP/1.1",
|
|
1981
|
+
cookies: [],
|
|
1982
|
+
headers: toHeaderArray(responseHeaders),
|
|
1983
|
+
content: { size: entry.size ?? 0, mimeType },
|
|
1984
|
+
redirectURL: "",
|
|
1985
|
+
headersSize: -1,
|
|
1986
|
+
bodySize: entry.size ?? -1
|
|
1987
|
+
},
|
|
1988
|
+
cache: {},
|
|
1989
|
+
timings: {
|
|
1990
|
+
send: 0,
|
|
1991
|
+
wait: Math.max(0, duration),
|
|
1992
|
+
receive: 0
|
|
1993
|
+
}
|
|
1994
|
+
}
|
|
1995
|
+
};
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
// ../core/src/trace-zip-writer.ts
|
|
1999
|
+
import yazl from "yazl";
|
|
2000
|
+
function buildTraceZip(inputs) {
|
|
2001
|
+
return new Promise((resolve2, reject) => {
|
|
2002
|
+
const zipFile = new yazl.ZipFile();
|
|
2003
|
+
zipFile.addBuffer(Buffer.from(inputs.traceNdjson, "utf8"), "trace.trace");
|
|
2004
|
+
zipFile.addBuffer(inputs.networkNdjson, "trace.network");
|
|
2005
|
+
if (inputs.transcriptMd) {
|
|
2006
|
+
zipFile.addBuffer(
|
|
2007
|
+
Buffer.from(inputs.transcriptMd, "utf8"),
|
|
2008
|
+
"transcript.md"
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
for (const resource of inputs.resources) {
|
|
2012
|
+
zipFile.addBuffer(resource.data, `resources/${resource.resourceName}`);
|
|
2013
|
+
}
|
|
2014
|
+
zipFile.end();
|
|
2015
|
+
const chunks = [];
|
|
2016
|
+
zipFile.outputStream.on("data", (chunk) => chunks.push(chunk));
|
|
2017
|
+
zipFile.outputStream.on("end", () => resolve2(Buffer.concat(chunks)));
|
|
2018
|
+
zipFile.outputStream.on("error", reject);
|
|
2019
|
+
});
|
|
2020
|
+
}
|
|
2021
|
+
|
|
2022
|
+
// ../core/src/trace-exporter.ts
|
|
2023
|
+
var TRACE_VERSION = 8;
|
|
2024
|
+
var LIBRARY_NAME = "@wdio/devtools-core";
|
|
2025
|
+
var LIBRARY_VERSION = "1.0.0";
|
|
2026
|
+
function shortId(sessionId) {
|
|
2027
|
+
return (sessionId ?? Math.random().toString(36).slice(2, 10)).slice(0, 8);
|
|
2028
|
+
}
|
|
2029
|
+
function resolveContextNaming(caps) {
|
|
2030
|
+
const platformName = typeof caps?.platformName === "string" ? caps.platformName.toLowerCase() : void 0;
|
|
2031
|
+
const deviceName = typeof caps?.["appium:deviceName"] === "string" ? caps["appium:deviceName"] : void 0;
|
|
2032
|
+
if (platformName === "android" || platformName === "ios") {
|
|
2033
|
+
return {
|
|
2034
|
+
browserName: "chromium",
|
|
2035
|
+
title: deviceName ? `${platformName} \u2014 ${deviceName}` : platformName
|
|
2036
|
+
};
|
|
2037
|
+
}
|
|
2038
|
+
const browserName = typeof caps?.browserName === "string" ? caps.browserName : "chromium";
|
|
2039
|
+
return { browserName, title: browserName };
|
|
2040
|
+
}
|
|
2041
|
+
function buildContextOptions(trace, contextId, wallTime) {
|
|
2042
|
+
const caps = trace.metadata.capabilities;
|
|
2043
|
+
const { browserName, title } = resolveContextNaming(caps);
|
|
2044
|
+
const viewport = trace.metadata.viewport ?? { width: 1280, height: 720 };
|
|
2045
|
+
return {
|
|
2046
|
+
version: TRACE_VERSION,
|
|
2047
|
+
type: "context-options",
|
|
2048
|
+
origin: "library",
|
|
2049
|
+
libraryName: LIBRARY_NAME,
|
|
2050
|
+
libraryVersion: LIBRARY_VERSION,
|
|
2051
|
+
browserName,
|
|
2052
|
+
platform: process.platform === "darwin" ? "darwin" : process.platform === "win32" ? "windows" : "linux",
|
|
2053
|
+
wallTime,
|
|
2054
|
+
monotonicTime: 0,
|
|
2055
|
+
sdkLanguage: "javascript",
|
|
2056
|
+
title,
|
|
2057
|
+
contextId,
|
|
2058
|
+
options: {
|
|
2059
|
+
viewport: { width: viewport.width, height: viewport.height }
|
|
2060
|
+
}
|
|
2061
|
+
};
|
|
2062
|
+
}
|
|
2063
|
+
function buildActionEvents(commands, pageId, wallTime, testMetadata) {
|
|
2064
|
+
const events = [];
|
|
2065
|
+
let prevEndMs = 0;
|
|
2066
|
+
let callCounter = 0;
|
|
2067
|
+
let lastTestUid;
|
|
2068
|
+
let groupCallId;
|
|
2069
|
+
for (const cmd of commands) {
|
|
2070
|
+
const action = mapCommandToAction(cmd.command);
|
|
2071
|
+
if (!action) {
|
|
2072
|
+
continue;
|
|
2073
|
+
}
|
|
2074
|
+
if (cmd.testUid && cmd.testUid !== lastTestUid) {
|
|
2075
|
+
if (lastTestUid && groupCallId) {
|
|
2076
|
+
callCounter++;
|
|
2077
|
+
events.push({
|
|
2078
|
+
type: "after",
|
|
2079
|
+
callId: groupCallId,
|
|
2080
|
+
endTime: prevEndMs
|
|
2081
|
+
});
|
|
2082
|
+
}
|
|
2083
|
+
callCounter++;
|
|
2084
|
+
groupCallId = `call@${callCounter}`;
|
|
2085
|
+
const meta = testMetadata?.get(cmd.testUid);
|
|
2086
|
+
const groupName = meta?.title ?? cmd.testUid;
|
|
2087
|
+
events.push({
|
|
2088
|
+
type: "before",
|
|
2089
|
+
callId: groupCallId,
|
|
2090
|
+
startTime: Math.max(
|
|
2091
|
+
prevEndMs,
|
|
2092
|
+
(cmd.startTime ?? cmd.timestamp) - wallTime
|
|
2093
|
+
),
|
|
2094
|
+
class: "Tracing",
|
|
2095
|
+
method: "tracingGroup",
|
|
2096
|
+
pageId,
|
|
2097
|
+
params: { name: groupName },
|
|
2098
|
+
title: groupName,
|
|
2099
|
+
apiName: "tracing.tracingGroup"
|
|
2100
|
+
});
|
|
2101
|
+
lastTestUid = cmd.testUid;
|
|
2102
|
+
}
|
|
2103
|
+
callCounter++;
|
|
2104
|
+
const callId = `call@${callCounter}`;
|
|
2105
|
+
const rawStartMs = (cmd.startTime ?? cmd.timestamp) - wallTime;
|
|
2106
|
+
const rawEndMs = cmd.timestamp - wallTime;
|
|
2107
|
+
const startMs = Math.max(prevEndMs, rawStartMs);
|
|
2108
|
+
const endMs = Math.max(startMs + 1, rawEndMs);
|
|
2109
|
+
const rawArgs = cmd.args;
|
|
2110
|
+
let params;
|
|
2111
|
+
const isValueMethod = FILL_METHODS.has(action.method);
|
|
2112
|
+
if (action.class === "Element" && isValueMethod && rawArgs.length >= 2) {
|
|
2113
|
+
params = { selector: rawArgs[0], value: rawArgs[1] };
|
|
2114
|
+
} else if (action.class === "Element" && isValueMethod && rawArgs.length === 1) {
|
|
2115
|
+
params = { value: rawArgs[0] };
|
|
2116
|
+
} else if (action.class === "Element" && rawArgs.length === 1 && typeof rawArgs[0] === "string") {
|
|
2117
|
+
params = { selector: rawArgs[0] };
|
|
2118
|
+
} else if (rawArgs.length === 1 && typeof rawArgs[0] === "string") {
|
|
2119
|
+
params = { url: rawArgs[0] };
|
|
2120
|
+
} else {
|
|
2121
|
+
params = Object.fromEntries(rawArgs.map((a, i) => [String(i), a]));
|
|
2122
|
+
}
|
|
2123
|
+
events.push({
|
|
2124
|
+
type: "before",
|
|
2125
|
+
callId,
|
|
2126
|
+
startTime: startMs,
|
|
2127
|
+
class: action.class,
|
|
2128
|
+
method: action.method,
|
|
2129
|
+
pageId,
|
|
2130
|
+
params,
|
|
2131
|
+
title: formatActionTitle(action, cmd.args, params),
|
|
2132
|
+
apiName: `${action.class.toLowerCase()}.${action.method}`,
|
|
2133
|
+
parentId: groupCallId
|
|
2134
|
+
});
|
|
2135
|
+
const afterEvent = {
|
|
2136
|
+
type: "after",
|
|
2137
|
+
callId,
|
|
2138
|
+
endTime: endMs
|
|
2139
|
+
};
|
|
2140
|
+
if (cmd.error) {
|
|
2141
|
+
const err = cmd.error;
|
|
2142
|
+
afterEvent.error = { message: err.message ?? String(cmd.error) };
|
|
2143
|
+
}
|
|
2144
|
+
events.push(afterEvent);
|
|
2145
|
+
prevEndMs = endMs;
|
|
2146
|
+
}
|
|
2147
|
+
if (lastTestUid && groupCallId) {
|
|
2148
|
+
callCounter++;
|
|
2149
|
+
events.push({
|
|
2150
|
+
type: "after",
|
|
2151
|
+
callId: groupCallId,
|
|
2152
|
+
endTime: prevEndMs
|
|
2153
|
+
});
|
|
2154
|
+
}
|
|
2155
|
+
return events;
|
|
2156
|
+
}
|
|
2157
|
+
function buildNetworkNdjson(requests, wallTime, pageId) {
|
|
2158
|
+
if (!requests.length) {
|
|
2159
|
+
return Buffer.alloc(0);
|
|
2160
|
+
}
|
|
2161
|
+
const lines = requests.map((r) => {
|
|
2162
|
+
const entry = networkRequestToHar(r);
|
|
2163
|
+
entry.snapshot = {
|
|
2164
|
+
...entry.snapshot,
|
|
2165
|
+
// Monotonic offset so the viewer positions bars on the timeline.
|
|
2166
|
+
_monotonicTime: Math.max(0, r.timestamp - wallTime),
|
|
2167
|
+
// Browsing context ID so the viewer associates requests with the page.
|
|
2168
|
+
_frameref: pageId
|
|
2169
|
+
};
|
|
2170
|
+
return JSON.stringify(entry);
|
|
2171
|
+
});
|
|
2172
|
+
return Buffer.from(lines.join("\n"), "utf8");
|
|
2173
|
+
}
|
|
2174
|
+
function buildSnapshotResources(snapshots, pageId) {
|
|
2175
|
+
const out = [];
|
|
2176
|
+
for (const snap of snapshots) {
|
|
2177
|
+
const base = `${pageId}-${snap.timestamp}`;
|
|
2178
|
+
if (snap.screenshot) {
|
|
2179
|
+
out.push({
|
|
2180
|
+
resourceName: `${base}.jpeg`,
|
|
2181
|
+
data: Buffer.from(snap.screenshot, "base64")
|
|
2182
|
+
});
|
|
2183
|
+
}
|
|
2184
|
+
if (snap.elements && snap.elements.length) {
|
|
2185
|
+
out.push({
|
|
2186
|
+
resourceName: `${base}-elements.json`,
|
|
2187
|
+
data: Buffer.from(JSON.stringify(snap.elements), "utf8")
|
|
2188
|
+
});
|
|
2189
|
+
}
|
|
2190
|
+
if (snap.snapshotText) {
|
|
2191
|
+
out.push({
|
|
2192
|
+
resourceName: `${base}-snapshot.txt`,
|
|
2193
|
+
data: Buffer.from(snap.snapshotText, "utf8")
|
|
2194
|
+
});
|
|
2195
|
+
}
|
|
2196
|
+
}
|
|
2197
|
+
return out;
|
|
2198
|
+
}
|
|
2199
|
+
function buildScreencastFrames(snapshots, pageId, wallTime, viewport) {
|
|
2200
|
+
return snapshots.filter((s) => s.screenshot).map((s) => {
|
|
2201
|
+
const base = `${pageId}-${s.timestamp}`;
|
|
2202
|
+
const frame = {
|
|
2203
|
+
type: "screencast-frame",
|
|
2204
|
+
pageId,
|
|
2205
|
+
sha1: `${base}.jpeg`,
|
|
2206
|
+
width: viewport.width,
|
|
2207
|
+
height: viewport.height,
|
|
2208
|
+
timestamp: Math.max(0, s.timestamp - wallTime)
|
|
2209
|
+
};
|
|
2210
|
+
if (s.elements && s.elements.length) {
|
|
2211
|
+
frame.elements = `${base}-elements.json`;
|
|
2212
|
+
}
|
|
2213
|
+
if (s.snapshotText) {
|
|
2214
|
+
frame.snapshot = `${base}-snapshot.txt`;
|
|
2215
|
+
}
|
|
2216
|
+
return frame;
|
|
2217
|
+
});
|
|
2218
|
+
}
|
|
2219
|
+
function eventTime(e) {
|
|
2220
|
+
switch (e.type) {
|
|
2221
|
+
case "context-options":
|
|
2222
|
+
return -Infinity;
|
|
2223
|
+
case "before":
|
|
2224
|
+
return e.startTime;
|
|
2225
|
+
case "after":
|
|
2226
|
+
return e.endTime;
|
|
2227
|
+
case "screencast-frame":
|
|
2228
|
+
return e.timestamp;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
function eventOrder(e) {
|
|
2232
|
+
switch (e.type) {
|
|
2233
|
+
case "context-options":
|
|
2234
|
+
return 0;
|
|
2235
|
+
case "after":
|
|
2236
|
+
return 1;
|
|
2237
|
+
case "screencast-frame":
|
|
2238
|
+
return 2;
|
|
2239
|
+
case "before":
|
|
2240
|
+
return 3;
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
function compareEvents(a, b) {
|
|
2244
|
+
const dt = eventTime(a) - eventTime(b);
|
|
2245
|
+
return dt !== 0 ? dt : eventOrder(a) - eventOrder(b);
|
|
2246
|
+
}
|
|
2247
|
+
function generateTranscript(commands, startWallTime, title) {
|
|
2248
|
+
const wallTimeISO = new Date(startWallTime).toISOString();
|
|
2249
|
+
const lines = [`# ${title ?? "Session"} \u2014 ${wallTimeISO}`, ""];
|
|
2250
|
+
const captured = [];
|
|
2251
|
+
for (const c of commands) {
|
|
2252
|
+
const action = mapCommandToAction(String(c.command));
|
|
2253
|
+
if (action) {
|
|
2254
|
+
captured.push({ entry: c, action });
|
|
2255
|
+
}
|
|
2256
|
+
}
|
|
2257
|
+
captured.forEach(({ entry, action }, idx) => {
|
|
2258
|
+
const label = formatActionTitle(action, entry.args);
|
|
2259
|
+
const rawArgs = entry.args;
|
|
2260
|
+
const parts = [`${idx + 1}. ${label}`];
|
|
2261
|
+
if (FILL_METHODS.has(action.method) && rawArgs) {
|
|
2262
|
+
const valueIdx = rawArgs.length >= 2 ? 1 : 0;
|
|
2263
|
+
if (rawArgs[valueIdx] !== void 0) {
|
|
2264
|
+
parts.push(`value="${String(rawArgs[valueIdx])}"`);
|
|
2265
|
+
}
|
|
2266
|
+
}
|
|
2267
|
+
if (entry.error) {
|
|
2268
|
+
const msg = typeof entry.error === "object" && "message" in entry.error ? entry.error.message : String(entry.error);
|
|
2269
|
+
parts.push(`ERROR: ${msg}`);
|
|
2270
|
+
}
|
|
2271
|
+
lines.push(parts.join(" "));
|
|
2272
|
+
});
|
|
2273
|
+
return lines.join("\n");
|
|
2274
|
+
}
|
|
2275
|
+
function buildTraceBundle(trace, opts = {}) {
|
|
2276
|
+
const firstCommandTs = trace.commands[0]?.timestamp;
|
|
2277
|
+
const wallTime = opts.wallTimeOverride ?? firstCommandTs ?? Date.now();
|
|
2278
|
+
const idPrefix = shortId(opts.sessionId);
|
|
2279
|
+
const contextId = `context@${idPrefix}`;
|
|
2280
|
+
const pageId = `page@${idPrefix}`;
|
|
2281
|
+
const viewport = trace.metadata.viewport ?? { width: 1280, height: 720 };
|
|
2282
|
+
const snapshots = trace.actionSnapshots ?? [];
|
|
2283
|
+
const ctxOptions = buildContextOptions(trace, contextId, wallTime);
|
|
2284
|
+
const events = [ctxOptions];
|
|
2285
|
+
const firstSnap = snapshots.find((s) => s.screenshot);
|
|
2286
|
+
if (firstSnap) {
|
|
2287
|
+
const base = `${pageId}-${firstSnap.timestamp}`;
|
|
2288
|
+
const initFrame = {
|
|
2289
|
+
type: "screencast-frame",
|
|
2290
|
+
pageId,
|
|
2291
|
+
sha1: `${base}.jpeg`,
|
|
2292
|
+
width: viewport.width,
|
|
2293
|
+
height: viewport.height,
|
|
2294
|
+
timestamp: 0
|
|
2295
|
+
};
|
|
2296
|
+
if (firstSnap.elements && firstSnap.elements.length) {
|
|
2297
|
+
initFrame.elements = `${base}-elements.json`;
|
|
2298
|
+
}
|
|
2299
|
+
if (firstSnap.snapshotText) {
|
|
2300
|
+
initFrame.snapshot = `${base}-snapshot.txt`;
|
|
2301
|
+
}
|
|
2302
|
+
events.push(initFrame);
|
|
2303
|
+
}
|
|
2304
|
+
events.push(
|
|
2305
|
+
...buildScreencastFrames(
|
|
2306
|
+
firstSnap ? snapshots.filter((s) => s !== firstSnap) : snapshots,
|
|
2307
|
+
pageId,
|
|
2308
|
+
wallTime,
|
|
2309
|
+
viewport
|
|
2310
|
+
),
|
|
2311
|
+
...buildActionEvents(trace.commands, pageId, wallTime, opts.testMetadata)
|
|
2312
|
+
);
|
|
2313
|
+
events.sort(compareEvents);
|
|
2314
|
+
const ctxBName = ctxOptions.title;
|
|
2315
|
+
return {
|
|
2316
|
+
traceNdjson: events.map((e) => JSON.stringify(e)).join("\n") + "\n",
|
|
2317
|
+
networkNdjson: buildNetworkNdjson(trace.networkRequests, wallTime, pageId),
|
|
2318
|
+
transcriptMd: generateTranscript(trace.commands, wallTime, ctxBName),
|
|
2319
|
+
resources: buildSnapshotResources(snapshots, pageId)
|
|
2320
|
+
};
|
|
2321
|
+
}
|
|
2322
|
+
async function exportTraceZip(trace, opts = {}) {
|
|
2323
|
+
const bundle = buildTraceBundle(trace, opts);
|
|
2324
|
+
return buildTraceZip({
|
|
2325
|
+
traceNdjson: bundle.traceNdjson,
|
|
2326
|
+
networkNdjson: bundle.networkNdjson,
|
|
2327
|
+
resources: bundle.resources,
|
|
2328
|
+
transcriptMd: bundle.transcriptMd
|
|
2329
|
+
});
|
|
2330
|
+
}
|
|
2331
|
+
async function exportTraceDirectory(trace, targetDir, opts = {}) {
|
|
2332
|
+
const bundle = buildTraceBundle(trace, opts);
|
|
2333
|
+
await fs.mkdir(path.join(targetDir, "resources"), { recursive: true });
|
|
2334
|
+
await Promise.all([
|
|
2335
|
+
fs.writeFile(path.join(targetDir, "trace.trace"), bundle.traceNdjson),
|
|
2336
|
+
fs.writeFile(
|
|
2337
|
+
path.join(targetDir, "transcript.md"),
|
|
2338
|
+
bundle.transcriptMd,
|
|
2339
|
+
"utf8"
|
|
2340
|
+
),
|
|
2341
|
+
bundle.networkNdjson.length ? fs.writeFile(
|
|
2342
|
+
path.join(targetDir, "trace.network"),
|
|
2343
|
+
bundle.networkNdjson
|
|
2344
|
+
) : Promise.resolve(),
|
|
2345
|
+
...bundle.resources.map(
|
|
2346
|
+
(r) => fs.writeFile(path.join(targetDir, "resources", r.resourceName), r.data)
|
|
2347
|
+
)
|
|
2348
|
+
]);
|
|
2349
|
+
}
|
|
2350
|
+
async function writeTraceZip(capturer, opts) {
|
|
2351
|
+
const baseMetadata = capturer.metadata ?? {};
|
|
2352
|
+
const actionSnapshots = opts.actionSnapshots ?? synthesizeSnapshotsFromCommands(capturer.commandsLog);
|
|
2353
|
+
const traceLog = {
|
|
2354
|
+
mutations: capturer.mutations,
|
|
2355
|
+
logs: capturer.traceLogs,
|
|
2356
|
+
consoleLogs: capturer.consoleLogs,
|
|
2357
|
+
networkRequests: capturer.networkRequests,
|
|
2358
|
+
metadata: {
|
|
2359
|
+
...baseMetadata,
|
|
2360
|
+
...opts.capabilities ? { capabilities: opts.capabilities } : {}
|
|
2361
|
+
},
|
|
2362
|
+
commands: capturer.commandsLog,
|
|
2363
|
+
sources: Object.fromEntries(capturer.sources),
|
|
2364
|
+
...actionSnapshots.length ? { actionSnapshots } : {}
|
|
2365
|
+
};
|
|
2366
|
+
await fs.mkdir(opts.outputDir, { recursive: true });
|
|
2367
|
+
const exportOpts = {
|
|
2368
|
+
sessionId: opts.sessionId,
|
|
2369
|
+
wallTimeOverride: capturer.startWallTime,
|
|
2370
|
+
testMetadata: opts.testMetadata
|
|
2371
|
+
};
|
|
2372
|
+
if (opts.format === "ndjson-directory") {
|
|
2373
|
+
const dir = path.join(opts.outputDir, `trace-${opts.sessionId}`);
|
|
2374
|
+
await fs.mkdir(dir, { recursive: true });
|
|
2375
|
+
await exportTraceDirectory(traceLog, dir, exportOpts);
|
|
2376
|
+
return dir;
|
|
2377
|
+
}
|
|
2378
|
+
const zip = await exportTraceZip(traceLog, exportOpts);
|
|
2379
|
+
const zipPath = path.join(opts.outputDir, `trace-${opts.sessionId}.zip`);
|
|
2380
|
+
await fs.writeFile(zipPath, zip);
|
|
2381
|
+
return zipPath;
|
|
2382
|
+
}
|
|
2383
|
+
function synthesizeSnapshotsFromCommands(commands) {
|
|
2384
|
+
return commands.filter((c) => c.screenshot && mapCommandToAction(c.command)).map((c) => ({
|
|
2385
|
+
timestamp: c.timestamp,
|
|
2386
|
+
command: c.command,
|
|
2387
|
+
screenshot: c.screenshot
|
|
2388
|
+
}));
|
|
2389
|
+
}
|
|
2390
|
+
|
|
47
2391
|
// ../core/src/uid.ts
|
|
48
2392
|
function deterministicUid(...parts) {
|
|
49
2393
|
const hash = parts.join("::").split("").reduce((acc, char) => (acc << 5) - acc + char.charCodeAt(0) | 0, 0);
|
|
@@ -62,13 +2406,13 @@ function resetSignatureCounters() {
|
|
|
62
2406
|
}
|
|
63
2407
|
|
|
64
2408
|
// ../core/src/finalize-screencast.ts
|
|
65
|
-
import
|
|
2409
|
+
import fs3 from "fs";
|
|
66
2410
|
import os2 from "os";
|
|
67
|
-
import
|
|
2411
|
+
import path3 from "path";
|
|
68
2412
|
|
|
69
2413
|
// ../core/src/video-encoder.ts
|
|
70
|
-
import
|
|
71
|
-
import
|
|
2414
|
+
import fs2 from "fs/promises";
|
|
2415
|
+
import path2 from "path";
|
|
72
2416
|
import os from "os";
|
|
73
2417
|
import { createRequire } from "module";
|
|
74
2418
|
var require2 = createRequire(import.meta.url);
|
|
@@ -85,20 +2429,20 @@ async function writeFramesAndManifest(frames, tmpDir, ext) {
|
|
|
85
2429
|
const manifestLines = ["ffconcat version 1.0"];
|
|
86
2430
|
for (let i = 0; i < frames.length; i++) {
|
|
87
2431
|
const frameName = `frame-${String(i).padStart(6, "0")}.${ext}`;
|
|
88
|
-
const framePath =
|
|
89
|
-
await
|
|
2432
|
+
const framePath = path2.join(tmpDir, frameName);
|
|
2433
|
+
await fs2.writeFile(framePath, Buffer.from(frames[i].data, "base64"));
|
|
90
2434
|
const nextTs = frames[i + 1]?.timestamp ?? frames[i].timestamp + 100;
|
|
91
2435
|
const durationSecs = Math.max((nextTs - frames[i].timestamp) / 1e3, 0.01);
|
|
92
2436
|
manifestLines.push(`file '${framePath}'`);
|
|
93
2437
|
manifestLines.push(`duration ${durationSecs.toFixed(6)}`);
|
|
94
2438
|
}
|
|
95
|
-
const lastFramePath =
|
|
2439
|
+
const lastFramePath = path2.join(
|
|
96
2440
|
tmpDir,
|
|
97
2441
|
`frame-${String(frames.length - 1).padStart(6, "0")}.${ext}`
|
|
98
2442
|
);
|
|
99
2443
|
manifestLines.push(`file '${lastFramePath}'`);
|
|
100
|
-
const manifestPath =
|
|
101
|
-
await
|
|
2444
|
+
const manifestPath = path2.join(tmpDir, "manifest.txt");
|
|
2445
|
+
await fs2.writeFile(manifestPath, manifestLines.join("\n"));
|
|
102
2446
|
return manifestPath;
|
|
103
2447
|
}
|
|
104
2448
|
function classifyFfmpegError(err) {
|
|
@@ -121,6 +2465,15 @@ function runFfmpeg(ffmpeg, manifestPath, outputPath) {
|
|
|
121
2465
|
"cfr",
|
|
122
2466
|
"-r",
|
|
123
2467
|
"10",
|
|
2468
|
+
// Force a keyframe every frame. VP8 can only seek to keyframes, and
|
|
2469
|
+
// libvpx's default GOP places a single keyframe on near-static
|
|
2470
|
+
// screencast content — so scrubber/marker seeks snap back to the
|
|
2471
|
+
// start. At 10fps over short recordings, all-intra keeps seeking
|
|
2472
|
+
// frame-accurate at an acceptable size.
|
|
2473
|
+
"-g",
|
|
2474
|
+
"1",
|
|
2475
|
+
"-keyint_min",
|
|
2476
|
+
"1",
|
|
124
2477
|
"-auto-alt-ref",
|
|
125
2478
|
"0",
|
|
126
2479
|
"-disposition:v",
|
|
@@ -134,14 +2487,14 @@ async function encodeToVideo(frames, outputPath, options = {}) {
|
|
|
134
2487
|
}
|
|
135
2488
|
const ffmpeg = loadFfmpeg();
|
|
136
2489
|
const ext = options.captureFormat === "png" ? "png" : "jpg";
|
|
137
|
-
const tmpDir = await
|
|
138
|
-
|
|
2490
|
+
const tmpDir = await fs2.mkdtemp(
|
|
2491
|
+
path2.join(os.tmpdir(), "devtools-screencast-")
|
|
139
2492
|
);
|
|
140
2493
|
try {
|
|
141
2494
|
const manifestPath = await writeFramesAndManifest(frames, tmpDir, ext);
|
|
142
2495
|
await runFfmpeg(ffmpeg, manifestPath, outputPath);
|
|
143
2496
|
} finally {
|
|
144
|
-
await
|
|
2497
|
+
await fs2.rm(tmpDir, { recursive: true, force: true }).catch(() => {
|
|
145
2498
|
});
|
|
146
2499
|
}
|
|
147
2500
|
}
|
|
@@ -170,11 +2523,11 @@ async function finalizeScreencast({
|
|
|
170
2523
|
}
|
|
171
2524
|
const fileName = `${filenamePrefix}-${sessionId}.webm`;
|
|
172
2525
|
const candidate = outputDir || process.cwd();
|
|
173
|
-
let videoPath =
|
|
2526
|
+
let videoPath = path3.join(candidate, fileName);
|
|
174
2527
|
try {
|
|
175
|
-
|
|
2528
|
+
fs3.accessSync(candidate, fs3.constants.W_OK);
|
|
176
2529
|
} catch {
|
|
177
|
-
videoPath =
|
|
2530
|
+
videoPath = path3.join(os2.tmpdir(), fileName);
|
|
178
2531
|
}
|
|
179
2532
|
try {
|
|
180
2533
|
await encodeToVideo(frames, videoPath, { captureFormat });
|
|
@@ -184,7 +2537,8 @@ async function finalizeScreencast({
|
|
|
184
2537
|
videoPath,
|
|
185
2538
|
videoFile: fileName,
|
|
186
2539
|
frameCount: frames.length,
|
|
187
|
-
duration: recorder.duration
|
|
2540
|
+
duration: recorder.duration,
|
|
2541
|
+
startTime: frames[0]?.timestamp
|
|
188
2542
|
});
|
|
189
2543
|
} catch (err) {
|
|
190
2544
|
log12("warn", `Screencast encode failed: ${errorMessage(err)}`);
|
|
@@ -192,12 +2546,12 @@ async function finalizeScreencast({
|
|
|
192
2546
|
}
|
|
193
2547
|
|
|
194
2548
|
// ../core/src/output-dir.ts
|
|
195
|
-
import
|
|
196
|
-
import
|
|
197
|
-
var NODE_MODULES_SEGMENT = `${
|
|
2549
|
+
import fs4 from "fs";
|
|
2550
|
+
import path4 from "path";
|
|
2551
|
+
var NODE_MODULES_SEGMENT = `${path4.sep}node_modules${path4.sep}`;
|
|
198
2552
|
function isWritable(dir) {
|
|
199
2553
|
try {
|
|
200
|
-
|
|
2554
|
+
fs4.accessSync(dir, fs4.constants.W_OK);
|
|
201
2555
|
return true;
|
|
202
2556
|
} catch {
|
|
203
2557
|
return false;
|
|
@@ -210,10 +2564,10 @@ function resolveAdapterOutputDir(input = {}) {
|
|
|
210
2564
|
}
|
|
211
2565
|
const candidates = [];
|
|
212
2566
|
if (input.testFilePath) {
|
|
213
|
-
candidates.push(
|
|
2567
|
+
candidates.push(path4.dirname(input.testFilePath));
|
|
214
2568
|
}
|
|
215
2569
|
if (input.configPath) {
|
|
216
|
-
candidates.push(
|
|
2570
|
+
candidates.push(path4.dirname(input.configPath));
|
|
217
2571
|
}
|
|
218
2572
|
candidates.push(fallback);
|
|
219
2573
|
for (const dir of candidates) {
|
|
@@ -582,15 +2936,15 @@ var ScreencastRecorderBase = class {
|
|
|
582
2936
|
};
|
|
583
2937
|
|
|
584
2938
|
// ../core/src/script-loader.ts
|
|
585
|
-
import
|
|
586
|
-
import
|
|
2939
|
+
import fs5 from "fs/promises";
|
|
2940
|
+
import path5 from "path";
|
|
587
2941
|
import { createRequire as createRequire2 } from "module";
|
|
588
2942
|
var require3 = createRequire2(import.meta.url);
|
|
589
2943
|
async function loadInjectableScript() {
|
|
590
2944
|
const scriptPath = require3.resolve("@wdio/devtools-script");
|
|
591
|
-
const scriptDir =
|
|
592
|
-
const preloadScriptPath =
|
|
593
|
-
const scriptContent = await
|
|
2945
|
+
const scriptDir = path5.dirname(scriptPath);
|
|
2946
|
+
const preloadScriptPath = path5.join(scriptDir, "script.js");
|
|
2947
|
+
const scriptContent = await fs5.readFile(preloadScriptPath, "utf-8");
|
|
594
2948
|
return `(async function() { ${scriptContent} })()`;
|
|
595
2949
|
}
|
|
596
2950
|
async function pollUntilReady(check, opts = {}) {
|
|
@@ -606,7 +2960,7 @@ async function pollUntilReady(check, opts = {}) {
|
|
|
606
2960
|
}
|
|
607
2961
|
|
|
608
2962
|
// ../core/src/session-capturer.ts
|
|
609
|
-
import
|
|
2963
|
+
import fs6 from "fs/promises";
|
|
610
2964
|
import { WebSocket } from "ws";
|
|
611
2965
|
var SessionCapturerBase = class {
|
|
612
2966
|
// ── State (mostly private; subclasses access shared ws via `this.ws`) ────
|
|
@@ -648,9 +3002,12 @@ var SessionCapturerBase = class {
|
|
|
648
3002
|
metadata;
|
|
649
3003
|
// ── Construction ────────────────────────────────────────────────────────
|
|
650
3004
|
constructor(opts = {}) {
|
|
651
|
-
const { hostname, port } = opts;
|
|
3005
|
+
const { hostname, port, reconnect } = opts;
|
|
652
3006
|
if (hostname && port) {
|
|
653
|
-
|
|
3007
|
+
const query = reconnect ? "?reconnect=1" : "";
|
|
3008
|
+
this.ws = new WebSocket(
|
|
3009
|
+
`ws://${hostname}:${port}${WS_PATHS.worker}${query}`
|
|
3010
|
+
);
|
|
654
3011
|
this.ws.on("open", () => {
|
|
655
3012
|
this.#hasConnected = true;
|
|
656
3013
|
this.onWsOpen();
|
|
@@ -756,7 +3113,7 @@ var SessionCapturerBase = class {
|
|
|
756
3113
|
return;
|
|
757
3114
|
}
|
|
758
3115
|
try {
|
|
759
|
-
const source = (await
|
|
3116
|
+
const source = (await fs6.readFile(filePath, "utf-8")).toString();
|
|
760
3117
|
this.sources.set(filePath, source);
|
|
761
3118
|
this.sendUpstream("sources", { [filePath]: source });
|
|
762
3119
|
} catch (err) {
|
|
@@ -938,17 +3295,17 @@ var SessionCapturerBase = class {
|
|
|
938
3295
|
}
|
|
939
3296
|
// ── Hooks (subclasses override) ─────────────────────────────────────────
|
|
940
3297
|
/**
|
|
941
|
-
*
|
|
942
|
-
*
|
|
943
|
-
*
|
|
944
|
-
* the multi-argument structure for the UI.
|
|
3298
|
+
* Build a ConsoleLog entry from the captured line, push it into the local
|
|
3299
|
+
* `consoleLogs` array (so it ends up in any future trace export), and
|
|
3300
|
+
* broadcast it live via the `consoleLogs` WS scope.
|
|
945
3301
|
*
|
|
946
|
-
*
|
|
947
|
-
*
|
|
948
|
-
*
|
|
3302
|
+
* Args is passed as an array (matching the original console.* call shape:
|
|
3303
|
+
* `console.log('a', 'b')` → `args = ['a', 'b']`) so the multi-argument
|
|
3304
|
+
* structure is preserved for the UI.
|
|
949
3305
|
*/
|
|
950
3306
|
onLine(type, args, source) {
|
|
951
3307
|
const entry = createConsoleLogEntry(type, args, source);
|
|
3308
|
+
this.consoleLogs.push(entry);
|
|
952
3309
|
this.sendUpstream("consoleLogs", [entry]);
|
|
953
3310
|
}
|
|
954
3311
|
/**
|
|
@@ -982,6 +3339,100 @@ function safeStringify(value) {
|
|
|
982
3339
|
}
|
|
983
3340
|
}
|
|
984
3341
|
|
|
3342
|
+
// ../core/src/spec-trace-helpers.ts
|
|
3343
|
+
function sanitizeSpecName(specFile) {
|
|
3344
|
+
return specFile.replace(/^.*[/\\]/, "").replace(/\.[^.]+$/, "").replace(/[^a-zA-Z0-9_-]/g, "_").replace(/^_+|_+$/g, "") || "unknown-spec";
|
|
3345
|
+
}
|
|
3346
|
+
function buildSpecSessionId(specFile, sessionId) {
|
|
3347
|
+
const base = sanitizeSpecName(specFile);
|
|
3348
|
+
const hash = deterministicUid(specFile).split("-").pop().slice(0, 8);
|
|
3349
|
+
return `${base}-${hash}-${sessionId.slice(0, 8)}`;
|
|
3350
|
+
}
|
|
3351
|
+
function buildSpecCapturer(capturer, range, nextRange) {
|
|
3352
|
+
const end = nextRange ? {
|
|
3353
|
+
commands: nextRange.commandStartIdx,
|
|
3354
|
+
console: nextRange.consoleStartIdx,
|
|
3355
|
+
network: nextRange.networkStartIdx,
|
|
3356
|
+
mutations: nextRange.mutationStartIdx,
|
|
3357
|
+
traceLogs: nextRange.traceLogStartIdx
|
|
3358
|
+
} : void 0;
|
|
3359
|
+
return {
|
|
3360
|
+
mutations: capturer.mutations.slice(range.mutationStartIdx, end?.mutations),
|
|
3361
|
+
traceLogs: capturer.traceLogs.slice(range.traceLogStartIdx, end?.traceLogs),
|
|
3362
|
+
consoleLogs: capturer.consoleLogs.slice(
|
|
3363
|
+
range.consoleStartIdx,
|
|
3364
|
+
end?.console
|
|
3365
|
+
),
|
|
3366
|
+
networkRequests: capturer.networkRequests.slice(
|
|
3367
|
+
range.networkStartIdx,
|
|
3368
|
+
end?.network
|
|
3369
|
+
),
|
|
3370
|
+
commandsLog: capturer.commandsLog.slice(
|
|
3371
|
+
range.commandStartIdx,
|
|
3372
|
+
end?.commands
|
|
3373
|
+
),
|
|
3374
|
+
sources: new Map(capturer.sources),
|
|
3375
|
+
metadata: capturer.metadata,
|
|
3376
|
+
startWallTime: capturer.startWallTime
|
|
3377
|
+
};
|
|
3378
|
+
}
|
|
3379
|
+
function filterTestMetadataBySpec(allMetadata, specFile) {
|
|
3380
|
+
const filtered = /* @__PURE__ */ new Map();
|
|
3381
|
+
for (const [uid, meta] of allMetadata) {
|
|
3382
|
+
if (meta.specFile === specFile) {
|
|
3383
|
+
filtered.set(uid, meta);
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
return filtered;
|
|
3387
|
+
}
|
|
3388
|
+
function recordSpecBoundary(ctx, specFile, traceGranularity) {
|
|
3389
|
+
if (traceGranularity !== "spec") {
|
|
3390
|
+
return null;
|
|
3391
|
+
}
|
|
3392
|
+
const lastRange = ctx.specRanges[ctx.specRanges.length - 1];
|
|
3393
|
+
if (lastRange && lastRange.specFile === specFile) {
|
|
3394
|
+
return null;
|
|
3395
|
+
}
|
|
3396
|
+
const prevRange = lastRange && !ctx.flushedSpecs.has(lastRange.specFile) ? lastRange : null;
|
|
3397
|
+
ctx.specRanges.push({
|
|
3398
|
+
specFile,
|
|
3399
|
+
commandStartIdx: ctx.capturer.commandsLog.length,
|
|
3400
|
+
consoleStartIdx: ctx.capturer.consoleLogs.length,
|
|
3401
|
+
networkStartIdx: ctx.capturer.networkRequests.length,
|
|
3402
|
+
mutationStartIdx: ctx.capturer.mutations.length,
|
|
3403
|
+
traceLogStartIdx: ctx.capturer.traceLogs.length,
|
|
3404
|
+
snapshotCount: ctx.actionSnapshots.length
|
|
3405
|
+
});
|
|
3406
|
+
return prevRange;
|
|
3407
|
+
}
|
|
3408
|
+
async function writeSpecTrace(input) {
|
|
3409
|
+
const specCapturer = buildSpecCapturer(
|
|
3410
|
+
input.capturer,
|
|
3411
|
+
input.range,
|
|
3412
|
+
input.nextRange
|
|
3413
|
+
);
|
|
3414
|
+
const specSnapshots = input.actionSnapshots.slice(
|
|
3415
|
+
input.range.snapshotCount,
|
|
3416
|
+
input.nextRange?.snapshotCount ?? input.actionSnapshots.length
|
|
3417
|
+
);
|
|
3418
|
+
const specSessionId = buildSpecSessionId(
|
|
3419
|
+
input.range.specFile,
|
|
3420
|
+
input.sessionId
|
|
3421
|
+
);
|
|
3422
|
+
const testMetadata = filterTestMetadataBySpec(
|
|
3423
|
+
input.testMetadata,
|
|
3424
|
+
input.range.specFile
|
|
3425
|
+
);
|
|
3426
|
+
return writeTraceZip(specCapturer, {
|
|
3427
|
+
outputDir: input.outputDir,
|
|
3428
|
+
sessionId: specSessionId,
|
|
3429
|
+
capabilities: input.capabilities,
|
|
3430
|
+
actionSnapshots: specSnapshots.length > 0 ? specSnapshots : void 0,
|
|
3431
|
+
format: input.format,
|
|
3432
|
+
testMetadata
|
|
3433
|
+
});
|
|
3434
|
+
}
|
|
3435
|
+
|
|
985
3436
|
// ../core/src/suite-helpers.ts
|
|
986
3437
|
function createSuiteStats(init) {
|
|
987
3438
|
return {
|
|
@@ -1063,14 +3514,14 @@ function stampTestEnd(test, end = /* @__PURE__ */ new Date()) {
|
|
|
1063
3514
|
}
|
|
1064
3515
|
|
|
1065
3516
|
// ../core/src/test-discovery.ts
|
|
1066
|
-
import
|
|
3517
|
+
import fs7 from "fs";
|
|
1067
3518
|
function findTestDefinitions(filePath, opts = {}) {
|
|
1068
|
-
if (!
|
|
3519
|
+
if (!fs7.existsSync(filePath)) {
|
|
1069
3520
|
return [];
|
|
1070
3521
|
}
|
|
1071
3522
|
let source;
|
|
1072
3523
|
try {
|
|
1073
|
-
source =
|
|
3524
|
+
source = fs7.readFileSync(filePath, "utf-8");
|
|
1074
3525
|
} catch {
|
|
1075
3526
|
return [];
|
|
1076
3527
|
}
|
|
@@ -1165,11 +3616,12 @@ var TestReporterBase = class {
|
|
|
1165
3616
|
};
|
|
1166
3617
|
|
|
1167
3618
|
// src/index.ts
|
|
3619
|
+
import { stop as stopBackend } from "@wdio/devtools-backend";
|
|
1168
3620
|
import logger11 from "@wdio/logger";
|
|
1169
3621
|
|
|
1170
3622
|
// src/run-lifecycle.ts
|
|
1171
|
-
import * as
|
|
1172
|
-
import * as
|
|
3623
|
+
import * as fs9 from "fs";
|
|
3624
|
+
import * as path7 from "path";
|
|
1173
3625
|
import * as os3 from "os";
|
|
1174
3626
|
import logger from "@wdio/logger";
|
|
1175
3627
|
import { remote } from "webdriverio";
|
|
@@ -1229,8 +3681,8 @@ var CONFIG_FILENAMES = [
|
|
|
1229
3681
|
var PLUGIN_GLOBAL_KEY = "__nightwatchDevtoolsPlugin";
|
|
1230
3682
|
|
|
1231
3683
|
// src/helpers/utils.ts
|
|
1232
|
-
import * as
|
|
1233
|
-
import * as
|
|
3684
|
+
import * as fs8 from "fs";
|
|
3685
|
+
import * as path6 from "path";
|
|
1234
3686
|
import { parse as parseStackTrace2 } from "stacktrace-parser";
|
|
1235
3687
|
function determineTestState(testcase) {
|
|
1236
3688
|
if (testcase.passed === 0 && testcase.failed === 0) {
|
|
@@ -1287,7 +3739,7 @@ function findTestFileFromStack() {
|
|
|
1287
3739
|
return void 0;
|
|
1288
3740
|
}
|
|
1289
3741
|
const filePath = normalizeFilePath(frame.file);
|
|
1290
|
-
return
|
|
3742
|
+
return fs8.existsSync(filePath) ? filePath : void 0;
|
|
1291
3743
|
}
|
|
1292
3744
|
function extractTestMetadata(filePath) {
|
|
1293
3745
|
const defs = findTestDefinitions(filePath, {
|
|
@@ -1415,21 +3867,21 @@ function resolveNightwatchConfig() {
|
|
|
1415
3867
|
);
|
|
1416
3868
|
if (configFlagIdx !== -1 && process.argv[configFlagIdx + 1]) {
|
|
1417
3869
|
const argvConfig = process.argv[configFlagIdx + 1];
|
|
1418
|
-
const resolved =
|
|
1419
|
-
if (
|
|
3870
|
+
const resolved = path6.isAbsolute(argvConfig) ? argvConfig : path6.resolve(process.cwd(), argvConfig);
|
|
3871
|
+
if (fs8.existsSync(resolved)) {
|
|
1420
3872
|
return resolved;
|
|
1421
3873
|
}
|
|
1422
3874
|
}
|
|
1423
3875
|
let dir = process.cwd();
|
|
1424
|
-
const root =
|
|
3876
|
+
const root = path6.parse(dir).root;
|
|
1425
3877
|
while (dir && dir !== root) {
|
|
1426
3878
|
for (const file of CONFIG_FILENAMES) {
|
|
1427
|
-
const candidate =
|
|
1428
|
-
if (
|
|
3879
|
+
const candidate = path6.join(dir, file);
|
|
3880
|
+
if (fs8.existsSync(candidate)) {
|
|
1429
3881
|
return candidate;
|
|
1430
3882
|
}
|
|
1431
3883
|
}
|
|
1432
|
-
const parent =
|
|
3884
|
+
const parent = path6.dirname(dir);
|
|
1433
3885
|
if (parent === dir) {
|
|
1434
3886
|
break;
|
|
1435
3887
|
}
|
|
@@ -1474,20 +3926,25 @@ async function runPluginBefore(ctx) {
|
|
|
1474
3926
|
return;
|
|
1475
3927
|
}
|
|
1476
3928
|
try {
|
|
1477
|
-
ctx.options.
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
(
|
|
1490
|
-
|
|
3929
|
+
if (ctx.options.mode === "trace") {
|
|
3930
|
+
log.info("Trace mode \u2014 skipping backend port-bind and UI window");
|
|
3931
|
+
} else {
|
|
3932
|
+
ctx.options.port = await findFreePort(
|
|
3933
|
+
ctx.options.port,
|
|
3934
|
+
ctx.options.hostname
|
|
3935
|
+
);
|
|
3936
|
+
log.info("\u{1F680} Starting DevTools backend...");
|
|
3937
|
+
const { port } = await start(ctx.options);
|
|
3938
|
+
ctx.options.port = port;
|
|
3939
|
+
const url = `http://${ctx.options.hostname}:${ctx.options.port}`;
|
|
3940
|
+
log.info(`\u2713 Backend started on port ${ctx.options.port}`);
|
|
3941
|
+
log.info(` DevTools UI: ${url}`);
|
|
3942
|
+
await ctx.openDevtoolsBrowserAt(url);
|
|
3943
|
+
await new Promise(
|
|
3944
|
+
(resolve2) => setTimeout(resolve2, TIMING.UI_CONNECTION_WAIT)
|
|
3945
|
+
);
|
|
3946
|
+
}
|
|
3947
|
+
;
|
|
1491
3948
|
globalThis[PLUGIN_GLOBAL_KEY] = ctx.plugin;
|
|
1492
3949
|
} catch (err) {
|
|
1493
3950
|
log.error(`Failed to start backend: ${errorMessage(err)}`);
|
|
@@ -1496,12 +3953,12 @@ async function runPluginBefore(ctx) {
|
|
|
1496
3953
|
}
|
|
1497
3954
|
async function openDevtoolsBrowser(ctx, url) {
|
|
1498
3955
|
try {
|
|
1499
|
-
ctx.userDataDir =
|
|
3956
|
+
ctx.userDataDir = path7.join(
|
|
1500
3957
|
os3.tmpdir(),
|
|
1501
3958
|
`nightwatch-devtools-${ctx.options.port}-${Date.now()}`
|
|
1502
3959
|
);
|
|
1503
|
-
if (!
|
|
1504
|
-
|
|
3960
|
+
if (!fs9.existsSync(ctx.userDataDir)) {
|
|
3961
|
+
fs9.mkdirSync(ctx.userDataDir, { recursive: true });
|
|
1505
3962
|
}
|
|
1506
3963
|
ctx.devtoolsBrowser = await remote({
|
|
1507
3964
|
logLevel: "info",
|
|
@@ -1515,6 +3972,13 @@ async function openDevtoolsBrowser(ctx, url) {
|
|
|
1515
3972
|
"--no-first-run",
|
|
1516
3973
|
"--no-default-browser-check"
|
|
1517
3974
|
]
|
|
3975
|
+
},
|
|
3976
|
+
// Dashboard uses the Puppeteer-based 'devtools' protocol; drop the
|
|
3977
|
+
// "controlled by automated test software" infobar by ignoring
|
|
3978
|
+
// Puppeteer's --enable-automation default. (Runtime-valid capability
|
|
3979
|
+
// not present in the type.)
|
|
3980
|
+
"wdio:devtoolsOptions": {
|
|
3981
|
+
ignoreDefaultArgs: ["--enable-automation"]
|
|
1518
3982
|
}
|
|
1519
3983
|
}
|
|
1520
3984
|
});
|
|
@@ -1726,43 +4190,43 @@ function buildCucumberScenarioSuite(input) {
|
|
|
1726
4190
|
}
|
|
1727
4191
|
|
|
1728
4192
|
// src/helpers/featureFileScan.ts
|
|
1729
|
-
import
|
|
1730
|
-
import
|
|
4193
|
+
import fs10 from "fs";
|
|
4194
|
+
import path8 from "path";
|
|
1731
4195
|
function scanFeatureFile(featureUri) {
|
|
1732
|
-
const featureAbsPath =
|
|
4196
|
+
const featureAbsPath = path8.resolve(process.cwd(), featureUri);
|
|
1733
4197
|
const result = {
|
|
1734
|
-
featureName:
|
|
4198
|
+
featureName: path8.basename(featureUri, ".feature"),
|
|
1735
4199
|
featureContent: "",
|
|
1736
4200
|
featureAbsPath,
|
|
1737
4201
|
stepDefFiles: [],
|
|
1738
4202
|
capturedPaths: []
|
|
1739
4203
|
};
|
|
1740
|
-
if (featureUri === "unknown.feature" || !
|
|
4204
|
+
if (featureUri === "unknown.feature" || !fs10.existsSync(featureAbsPath)) {
|
|
1741
4205
|
return result;
|
|
1742
4206
|
}
|
|
1743
|
-
result.featureContent =
|
|
4207
|
+
result.featureContent = fs10.readFileSync(featureAbsPath, "utf-8");
|
|
1744
4208
|
const match = result.featureContent.match(/^\s*Feature:\s*(.+)/m);
|
|
1745
4209
|
if (match) {
|
|
1746
4210
|
result.featureName = match[1].trim();
|
|
1747
4211
|
}
|
|
1748
4212
|
result.capturedPaths.push(featureAbsPath);
|
|
1749
|
-
const featureDir =
|
|
4213
|
+
const featureDir = path8.dirname(featureAbsPath);
|
|
1750
4214
|
const stepDirCandidates = ["step_definitions", "steps", "support"];
|
|
1751
4215
|
for (const candidate of stepDirCandidates) {
|
|
1752
|
-
const stepDir =
|
|
1753
|
-
if (!
|
|
4216
|
+
const stepDir = path8.join(featureDir, candidate);
|
|
4217
|
+
if (!fs10.existsSync(stepDir) || !fs10.statSync(stepDir).isDirectory()) {
|
|
1754
4218
|
continue;
|
|
1755
4219
|
}
|
|
1756
|
-
for (const entry of
|
|
4220
|
+
for (const entry of fs10.readdirSync(stepDir)) {
|
|
1757
4221
|
if (!/\.(js|ts|mjs|cjs)$/.test(entry)) {
|
|
1758
4222
|
continue;
|
|
1759
4223
|
}
|
|
1760
|
-
const stepFilePath =
|
|
4224
|
+
const stepFilePath = path8.join(stepDir, entry);
|
|
1761
4225
|
result.capturedPaths.push(stepFilePath);
|
|
1762
4226
|
try {
|
|
1763
4227
|
result.stepDefFiles.push({
|
|
1764
4228
|
filePath: stepFilePath,
|
|
1765
|
-
content:
|
|
4229
|
+
content: fs10.readFileSync(stepFilePath, "utf-8")
|
|
1766
4230
|
});
|
|
1767
4231
|
} catch {
|
|
1768
4232
|
}
|
|
@@ -1929,8 +4393,8 @@ async function cucumberAfter(ctx, browser, result, pickle) {
|
|
|
1929
4393
|
import logger5 from "@wdio/logger";
|
|
1930
4394
|
|
|
1931
4395
|
// src/helpers/specFileResolver.ts
|
|
1932
|
-
import
|
|
1933
|
-
import
|
|
4396
|
+
import fs11 from "fs";
|
|
4397
|
+
import path9 from "path";
|
|
1934
4398
|
function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
|
|
1935
4399
|
let fullPath = findTestFileFromStack() || null;
|
|
1936
4400
|
if (!fullPath && cachedPath && cachedPath.includes(testFile)) {
|
|
@@ -1946,27 +4410,27 @@ function resolveSpecFilePath(testFile, modulePath, srcFolders, cachedPath) {
|
|
|
1946
4410
|
const normalized = (modulePath || "").replace(/\\/g, "/");
|
|
1947
4411
|
const srcFolderPaths = srcFolders.flatMap(
|
|
1948
4412
|
(sf) => normalized ? [
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
4413
|
+
path9.resolve(workspaceRoot, sf, normalized + ".js"),
|
|
4414
|
+
path9.resolve(workspaceRoot, sf, normalized + ".ts"),
|
|
4415
|
+
path9.resolve(workspaceRoot, sf, normalized + ".cjs"),
|
|
4416
|
+
path9.resolve(workspaceRoot, sf, normalized)
|
|
1953
4417
|
] : []
|
|
1954
4418
|
);
|
|
1955
4419
|
const possiblePaths = [
|
|
1956
4420
|
...srcFolderPaths,
|
|
1957
4421
|
// Treat module path as relative to cwd (works when src_folders isn't nested)
|
|
1958
4422
|
...normalized ? [
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
4423
|
+
path9.resolve(workspaceRoot, normalized + ".js"),
|
|
4424
|
+
path9.resolve(workspaceRoot, normalized + ".ts"),
|
|
4425
|
+
path9.resolve(workspaceRoot, normalized + ".cjs"),
|
|
4426
|
+
path9.resolve(workspaceRoot, normalized)
|
|
1963
4427
|
] : [],
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
4428
|
+
path9.resolve(workspaceRoot, "tests", testFile + ".js"),
|
|
4429
|
+
path9.resolve(workspaceRoot, "test", testFile + ".js"),
|
|
4430
|
+
path9.resolve(workspaceRoot, testFile + ".js")
|
|
1967
4431
|
];
|
|
1968
4432
|
for (const candidate of possiblePaths) {
|
|
1969
|
-
if (
|
|
4433
|
+
if (fs11.existsSync(candidate)) {
|
|
1970
4434
|
return candidate;
|
|
1971
4435
|
}
|
|
1972
4436
|
}
|
|
@@ -2182,6 +4646,18 @@ import logger10 from "@wdio/logger";
|
|
|
2182
4646
|
import http from "http";
|
|
2183
4647
|
import logger6 from "@wdio/logger";
|
|
2184
4648
|
|
|
4649
|
+
// src/action-snapshot.ts
|
|
4650
|
+
function captureActionSnapshot2(browser, command, takeScreenshot) {
|
|
4651
|
+
const b = browser;
|
|
4652
|
+
return captureActionSnapshot({
|
|
4653
|
+
command,
|
|
4654
|
+
runScript: (src) => browser.execute(`return (${src})`),
|
|
4655
|
+
takeScreenshot,
|
|
4656
|
+
getUrl: () => b.getCurrentUrl ? b.getCurrentUrl().catch(() => void 0) : Promise.resolve(void 0),
|
|
4657
|
+
getTitle: () => b.getTitle ? b.getTitle().catch(() => void 0) : Promise.resolve(void 0)
|
|
4658
|
+
});
|
|
4659
|
+
}
|
|
4660
|
+
|
|
2185
4661
|
// src/helpers/perfLogs.ts
|
|
2186
4662
|
function applyResponseReceived(p, response) {
|
|
2187
4663
|
const responseHeaders = {};
|
|
@@ -2194,18 +4670,22 @@ function applyResponseReceived(p, response) {
|
|
|
2194
4670
|
p.mimeType = response.mimeType;
|
|
2195
4671
|
p.type = getRequestType(p.url, response.mimeType);
|
|
2196
4672
|
}
|
|
4673
|
+
function eventTimeMs(params, entry) {
|
|
4674
|
+
return typeof params.timestamp === "number" ? Math.round(params.timestamp * 1e3) : entry.timestamp;
|
|
4675
|
+
}
|
|
2197
4676
|
function startPendingRequest(params, entry, pending) {
|
|
2198
4677
|
const { requestId, request: req, timestamp } = params;
|
|
2199
4678
|
if (!req || typeof timestamp !== "number") {
|
|
2200
4679
|
return;
|
|
2201
4680
|
}
|
|
4681
|
+
const startTime = eventTimeMs(params, entry);
|
|
2202
4682
|
pending.set(requestId, {
|
|
2203
4683
|
id: `${entry.timestamp}-${requestId}`,
|
|
2204
4684
|
url: req.url,
|
|
2205
4685
|
method: req.method,
|
|
2206
4686
|
requestHeaders: req.headers ?? {},
|
|
2207
|
-
timestamp:
|
|
2208
|
-
startTime
|
|
4687
|
+
timestamp: startTime,
|
|
4688
|
+
startTime
|
|
2209
4689
|
});
|
|
2210
4690
|
}
|
|
2211
4691
|
function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
@@ -2224,8 +4704,8 @@ function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
|
2224
4704
|
const p = pending.get(params.requestId);
|
|
2225
4705
|
if (p && p.status !== void 0) {
|
|
2226
4706
|
p.size = params.encodedDataLength;
|
|
2227
|
-
p.endTime = entry
|
|
2228
|
-
p.time =
|
|
4707
|
+
p.endTime = eventTimeMs(params, entry);
|
|
4708
|
+
p.time = Math.max(0, p.endTime - p.startTime);
|
|
2229
4709
|
completed.push({ ...p });
|
|
2230
4710
|
pending.delete(params.requestId);
|
|
2231
4711
|
}
|
|
@@ -2235,8 +4715,8 @@ function handlePerfLogEvent(method, params, entry, pending, completed) {
|
|
|
2235
4715
|
const p = pending.get(params.requestId);
|
|
2236
4716
|
if (p) {
|
|
2237
4717
|
p.error = params.errorText;
|
|
2238
|
-
p.endTime = entry
|
|
2239
|
-
p.time =
|
|
4718
|
+
p.endTime = eventTimeMs(params, entry);
|
|
4719
|
+
p.time = Math.max(0, p.endTime - p.startTime);
|
|
2240
4720
|
completed.push({ ...p });
|
|
2241
4721
|
pending.delete(params.requestId);
|
|
2242
4722
|
}
|
|
@@ -2310,7 +4790,7 @@ function unwrapDriverValue(result) {
|
|
|
2310
4790
|
return result;
|
|
2311
4791
|
}
|
|
2312
4792
|
var getProp = (obj, key) => obj && typeof obj === "object" ? obj[key] : void 0;
|
|
2313
|
-
var getPath = (obj, ...
|
|
4793
|
+
var getPath = (obj, ...path10) => path10.reduce((acc, k) => getProp(acc, k), obj);
|
|
2314
4794
|
var firstDefined = (obj, ...keys) => {
|
|
2315
4795
|
if (!obj || typeof obj !== "object") {
|
|
2316
4796
|
return void 0;
|
|
@@ -2347,6 +4827,10 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
2347
4827
|
// True once BiDi inspectors are attached — the per-command perf-log network
|
|
2348
4828
|
// capture path skips when set, so we don't double-emit network requests.
|
|
2349
4829
|
bidiActive = false;
|
|
4830
|
+
// Populated by captureCommand when mode === 'trace' (set by the plugin).
|
|
4831
|
+
traceMode = "live";
|
|
4832
|
+
actionSnapshots = [];
|
|
4833
|
+
snapshotCaptures = [];
|
|
2350
4834
|
constructor(devtoolsOptions = {}, browser) {
|
|
2351
4835
|
super(devtoolsOptions);
|
|
2352
4836
|
this.#browser = browser;
|
|
@@ -2362,15 +4846,6 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
2362
4846
|
onWsClose() {
|
|
2363
4847
|
log6.info("Worker WebSocket disconnected");
|
|
2364
4848
|
}
|
|
2365
|
-
/**
|
|
2366
|
-
* Push every captured line into the local `consoleLogs` array so it ends up
|
|
2367
|
-
* in any future trace export, in addition to the live WS broadcast.
|
|
2368
|
-
*/
|
|
2369
|
-
onLine(type, args, source) {
|
|
2370
|
-
const entry = createConsoleLogEntry(type, args, source);
|
|
2371
|
-
this.consoleLogs.push(entry);
|
|
2372
|
-
this.sendUpstream("consoleLogs", [entry]);
|
|
2373
|
-
}
|
|
2374
4849
|
async captureCommand(command, args, result, error, testUid, callSource, timestamp) {
|
|
2375
4850
|
const serializedError = serializeError(error);
|
|
2376
4851
|
const commandId = this.commandCounter++;
|
|
@@ -2393,6 +4868,20 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
2393
4868
|
log6.warn(`Failed to capture performance data: ${errorMessage(err)}`);
|
|
2394
4869
|
});
|
|
2395
4870
|
}
|
|
4871
|
+
if (this.traceMode === "trace" && !error && this.#browser && mapCommandToAction(command)) {
|
|
4872
|
+
const browser = this.#browser;
|
|
4873
|
+
this.snapshotCaptures.push(
|
|
4874
|
+
captureActionSnapshot2(
|
|
4875
|
+
browser,
|
|
4876
|
+
command,
|
|
4877
|
+
() => this.takeScreenshotViaHttp(browser)
|
|
4878
|
+
).then((snap) => {
|
|
4879
|
+
if (snap) {
|
|
4880
|
+
this.actionSnapshots.push(snap);
|
|
4881
|
+
}
|
|
4882
|
+
})
|
|
4883
|
+
);
|
|
4884
|
+
}
|
|
2396
4885
|
return true;
|
|
2397
4886
|
}
|
|
2398
4887
|
async #capturePerformanceData(commandLogEntry, args) {
|
|
@@ -2589,13 +5078,6 @@ var SessionCapturer = class extends SessionCapturerBase {
|
|
|
2589
5078
|
async captureTrace(browser) {
|
|
2590
5079
|
await this.captureNetworkFromPerformanceLogs(browser);
|
|
2591
5080
|
try {
|
|
2592
|
-
const checkResult = await browser.execute(
|
|
2593
|
-
'return typeof window.wdioTraceCollector !== "undefined"'
|
|
2594
|
-
);
|
|
2595
|
-
const collectorExists = unwrapDriverValue(checkResult) === true;
|
|
2596
|
-
if (!collectorExists) {
|
|
2597
|
-
return;
|
|
2598
|
-
}
|
|
2599
5081
|
const result = await browser.execute(`
|
|
2600
5082
|
if (typeof window.wdioTraceCollector === 'undefined') {
|
|
2601
5083
|
return null;
|
|
@@ -3043,8 +5525,8 @@ var BrowserProxy = class {
|
|
|
3043
5525
|
getCurrentTestFullPath() {
|
|
3044
5526
|
return this.currentTestFullPath;
|
|
3045
5527
|
}
|
|
3046
|
-
setCurrentTestFullPath(
|
|
3047
|
-
this.currentTestFullPath =
|
|
5528
|
+
setCurrentTestFullPath(path10) {
|
|
5529
|
+
this.currentTestFullPath = path10;
|
|
3048
5530
|
}
|
|
3049
5531
|
/**
|
|
3050
5532
|
* Wrap browser navigation methods (url / navigate / navigateTo) to inject
|
|
@@ -3397,7 +5879,7 @@ function broadcastSessionMetadata(ctx, browser) {
|
|
|
3397
5879
|
const sf = opts.src_folders;
|
|
3398
5880
|
ctx.srcFolders = Array.isArray(sf) ? sf : sf ? [sf] : [];
|
|
3399
5881
|
}
|
|
3400
|
-
|
|
5882
|
+
const metadata = {
|
|
3401
5883
|
type: "testrunner" /* Testrunner */,
|
|
3402
5884
|
capabilities,
|
|
3403
5885
|
desiredCapabilities,
|
|
@@ -3406,7 +5888,9 @@ function broadcastSessionMetadata(ctx, browser) {
|
|
|
3406
5888
|
host: opts.webdriver?.host,
|
|
3407
5889
|
options: ctx.buildMetadataOptions(),
|
|
3408
5890
|
url: ""
|
|
3409
|
-
}
|
|
5891
|
+
};
|
|
5892
|
+
ctx.sessionCapturer.metadata = metadata;
|
|
5893
|
+
ctx.sessionCapturer.sendUpstream("metadata", metadata);
|
|
3410
5894
|
const browserName = capabilities.browserName || desiredCapabilities.browserName || "unknown";
|
|
3411
5895
|
const browserVersion = capabilities.browserVersion || capabilities.version || "";
|
|
3412
5896
|
log10.info(
|
|
@@ -3429,7 +5913,7 @@ async function tryAttachBidi(ctx, browser) {
|
|
|
3429
5913
|
log10.warn("bidi:true set but browser.driver unavailable \u2014 skipping");
|
|
3430
5914
|
return;
|
|
3431
5915
|
}
|
|
3432
|
-
const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-
|
|
5916
|
+
const { attachBidiHandlers, buildBidiSinks } = await import("./bidi-FIHBBGT4.js");
|
|
3433
5917
|
const ok = await attachBidiHandlers(
|
|
3434
5918
|
driver,
|
|
3435
5919
|
buildBidiSinks(ctx.sessionCapturer)
|
|
@@ -3465,12 +5949,21 @@ async function ensureSessionInitialized(ctx, browser, finalizeCurrentScreencast2
|
|
|
3465
5949
|
(resolve2) => setTimeout(resolve2, TIMING.INITIAL_CONNECTION_WAIT)
|
|
3466
5950
|
);
|
|
3467
5951
|
ctx.sessionCapturer = new SessionCapturer(
|
|
3468
|
-
|
|
5952
|
+
ctx.mode === "trace" ? {} : {
|
|
5953
|
+
port: ctx.port,
|
|
5954
|
+
hostname: ctx.hostname,
|
|
5955
|
+
// A session change reopens the WS mid-run — tell the backend to keep
|
|
5956
|
+
// the accumulated run state so earlier tests' commands survive.
|
|
5957
|
+
reconnect: Boolean(isSessionChange)
|
|
5958
|
+
},
|
|
3469
5959
|
browser
|
|
3470
5960
|
);
|
|
3471
|
-
|
|
3472
|
-
if (
|
|
3473
|
-
|
|
5961
|
+
ctx.sessionCapturer.traceMode = ctx.mode;
|
|
5962
|
+
if (ctx.mode !== "trace") {
|
|
5963
|
+
const connected = await ctx.sessionCapturer.waitForConnection(3e3);
|
|
5964
|
+
if (!connected) {
|
|
5965
|
+
log10.error("\u274C Worker WebSocket failed to connect!");
|
|
5966
|
+
}
|
|
3474
5967
|
}
|
|
3475
5968
|
if (!ctx.testReporter) {
|
|
3476
5969
|
initReporterChain(ctx);
|
|
@@ -3523,6 +6016,10 @@ var NightwatchDevToolsPlugin = class {
|
|
|
3523
6016
|
#skipCount = 0;
|
|
3524
6017
|
#configPath;
|
|
3525
6018
|
#srcFolders = [];
|
|
6019
|
+
/** Index ranges into the session capturer's flat arrays, one per spec file. */
|
|
6020
|
+
#specRanges = [];
|
|
6021
|
+
/** Set of spec files already flushed to disk. */
|
|
6022
|
+
#flushedSpecs = /* @__PURE__ */ new Set();
|
|
3526
6023
|
#getRerunLabel() {
|
|
3527
6024
|
return process.env[REUSE_ENV.RERUN_ENTRY_TYPE] === "test" ? process.env[REUSE_ENV.RERUN_LABEL]?.trim() : void 0;
|
|
3528
6025
|
}
|
|
@@ -3532,16 +6029,22 @@ var NightwatchDevToolsPlugin = class {
|
|
|
3532
6029
|
#bidiEnabled = false;
|
|
3533
6030
|
#bidiAttachAttempted = false;
|
|
3534
6031
|
constructor(options = {}) {
|
|
6032
|
+
const mode = options.mode ?? "live";
|
|
6033
|
+
const ignore = mode === "trace" && options.screencast?.enabled === true;
|
|
6034
|
+
if (ignore) {
|
|
6035
|
+
log11.warn("trace mode: ignoring screencast option (live-mode feature)");
|
|
6036
|
+
}
|
|
6037
|
+
const screencast = ignore ? {} : options.screencast ?? {};
|
|
3535
6038
|
this.options = {
|
|
3536
6039
|
port: options.port ?? 3e3,
|
|
3537
6040
|
hostname: options.hostname ?? "localhost",
|
|
3538
|
-
screencast
|
|
3539
|
-
bidi: options.bidi ?? false
|
|
3540
|
-
|
|
3541
|
-
|
|
3542
|
-
|
|
3543
|
-
...options.screencast ?? {}
|
|
6041
|
+
screencast,
|
|
6042
|
+
bidi: options.bidi ?? false,
|
|
6043
|
+
mode,
|
|
6044
|
+
traceFormat: options.traceFormat ?? "zip",
|
|
6045
|
+
traceGranularity: options.traceGranularity ?? "session"
|
|
3544
6046
|
};
|
|
6047
|
+
this.#screencastOptions = { ...SCREENCAST_DEFAULTS, ...screencast };
|
|
3545
6048
|
this.#bidiEnabled = options.bidi === true;
|
|
3546
6049
|
}
|
|
3547
6050
|
// Single internals "bag" — structurally satisfies all 4 lifecycle ctx
|
|
@@ -3565,6 +6068,9 @@ var NightwatchDevToolsPlugin = class {
|
|
|
3565
6068
|
get port() {
|
|
3566
6069
|
return self.options.port;
|
|
3567
6070
|
},
|
|
6071
|
+
get mode() {
|
|
6072
|
+
return self.options.mode;
|
|
6073
|
+
},
|
|
3568
6074
|
get screencastOptions() {
|
|
3569
6075
|
return self.#screencastOptions;
|
|
3570
6076
|
},
|
|
@@ -3794,6 +6300,25 @@ var NightwatchDevToolsPlugin = class {
|
|
|
3794
6300
|
this.sessionCapturer.captureSource(fullPath).catch(() => {
|
|
3795
6301
|
});
|
|
3796
6302
|
}
|
|
6303
|
+
if (fullPath) {
|
|
6304
|
+
const prevRange = recordSpecBoundary(
|
|
6305
|
+
{
|
|
6306
|
+
specRanges: this.#specRanges,
|
|
6307
|
+
flushedSpecs: this.#flushedSpecs,
|
|
6308
|
+
capturer: this.sessionCapturer,
|
|
6309
|
+
actionSnapshots: this.sessionCapturer.actionSnapshots
|
|
6310
|
+
},
|
|
6311
|
+
fullPath,
|
|
6312
|
+
this.options.traceGranularity
|
|
6313
|
+
);
|
|
6314
|
+
if (prevRange) {
|
|
6315
|
+
void this.#flushSpecTrace(prevRange).catch(
|
|
6316
|
+
(err) => log11.warn(
|
|
6317
|
+
`Failed to flush trace for spec "${prevRange.specFile}": ${errorMessage(err)}`
|
|
6318
|
+
)
|
|
6319
|
+
);
|
|
6320
|
+
}
|
|
6321
|
+
}
|
|
3797
6322
|
await this.#closePreviousRunningTest(currentSuite, testFile, currentTest);
|
|
3798
6323
|
const processedTests = this.testManager.getProcessedTests(testFile);
|
|
3799
6324
|
const currentTestName = this.#pickCurrentTestName(
|
|
@@ -3827,6 +6352,12 @@ var NightwatchDevToolsPlugin = class {
|
|
|
3827
6352
|
try {
|
|
3828
6353
|
await this.#finalizeAllSuites(browser);
|
|
3829
6354
|
this.#logRunSummary();
|
|
6355
|
+
if (this.options.mode === "trace") {
|
|
6356
|
+
await this.#writeTraceZipIfNeeded();
|
|
6357
|
+
await this.sessionCapturer?.closeWebSocket();
|
|
6358
|
+
await stopBackend();
|
|
6359
|
+
return;
|
|
6360
|
+
}
|
|
3830
6361
|
if (!this.#devtoolsBrowser) {
|
|
3831
6362
|
this.testReporter?.updateSuites();
|
|
3832
6363
|
log11.info("\u267B Rerun complete \u2014 flushing WebSocket...");
|
|
@@ -3845,6 +6376,91 @@ var NightwatchDevToolsPlugin = class {
|
|
|
3845
6376
|
#logRunSummary() {
|
|
3846
6377
|
logRunSummary(this.#getInternals());
|
|
3847
6378
|
}
|
|
6379
|
+
async #flushSpecTrace(range, nextRange) {
|
|
6380
|
+
if (this.#flushedSpecs.has(range.specFile)) {
|
|
6381
|
+
return void 0;
|
|
6382
|
+
}
|
|
6383
|
+
this.#flushedSpecs.add(range.specFile);
|
|
6384
|
+
const sessionId = this.sessionCapturer.metadata?.sessionId;
|
|
6385
|
+
if (!sessionId) {
|
|
6386
|
+
return void 0;
|
|
6387
|
+
}
|
|
6388
|
+
const allMetadata = /* @__PURE__ */ new Map();
|
|
6389
|
+
if (this.suiteManager) {
|
|
6390
|
+
for (const suite of this.suiteManager.getAllSuites().values()) {
|
|
6391
|
+
for (const entry of suite.tests) {
|
|
6392
|
+
if (typeof entry === "string") {
|
|
6393
|
+
continue;
|
|
6394
|
+
}
|
|
6395
|
+
allMetadata.set(entry.uid, {
|
|
6396
|
+
title: entry.fullTitle,
|
|
6397
|
+
specFile: entry.file
|
|
6398
|
+
});
|
|
6399
|
+
}
|
|
6400
|
+
}
|
|
6401
|
+
}
|
|
6402
|
+
const tracePath = await writeSpecTrace({
|
|
6403
|
+
range,
|
|
6404
|
+
nextRange,
|
|
6405
|
+
capturer: this.sessionCapturer,
|
|
6406
|
+
actionSnapshots: this.sessionCapturer.actionSnapshots,
|
|
6407
|
+
sessionId,
|
|
6408
|
+
outputDir: resolveAdapterOutputDir({ configPath: this.#configPath }),
|
|
6409
|
+
format: this.options.traceFormat,
|
|
6410
|
+
testMetadata: allMetadata
|
|
6411
|
+
});
|
|
6412
|
+
log11.info(`Trace for spec "${range.specFile}" saved to ${tracePath}`);
|
|
6413
|
+
return tracePath;
|
|
6414
|
+
}
|
|
6415
|
+
async #writeTraceZipIfNeeded() {
|
|
6416
|
+
if (this.options.mode !== "trace" || !this.sessionCapturer) {
|
|
6417
|
+
return;
|
|
6418
|
+
}
|
|
6419
|
+
const sessionId = this.sessionCapturer.metadata?.sessionId;
|
|
6420
|
+
if (!sessionId) {
|
|
6421
|
+
return;
|
|
6422
|
+
}
|
|
6423
|
+
try {
|
|
6424
|
+
if (this.sessionCapturer.snapshotCaptures.length) {
|
|
6425
|
+
await Promise.allSettled(this.sessionCapturer.snapshotCaptures);
|
|
6426
|
+
}
|
|
6427
|
+
if (this.options.traceGranularity === "spec") {
|
|
6428
|
+
for (const range of this.#specRanges) {
|
|
6429
|
+
if (!this.#flushedSpecs.has(range.specFile)) {
|
|
6430
|
+
await this.#flushSpecTrace(range);
|
|
6431
|
+
}
|
|
6432
|
+
}
|
|
6433
|
+
return;
|
|
6434
|
+
}
|
|
6435
|
+
const testMetadata = /* @__PURE__ */ new Map();
|
|
6436
|
+
if (this.suiteManager) {
|
|
6437
|
+
for (const suite of this.suiteManager.getAllSuites().values()) {
|
|
6438
|
+
for (const entry of suite.tests) {
|
|
6439
|
+
if (typeof entry === "string") {
|
|
6440
|
+
continue;
|
|
6441
|
+
}
|
|
6442
|
+
testMetadata.set(entry.uid, {
|
|
6443
|
+
title: entry.fullTitle,
|
|
6444
|
+
specFile: entry.file
|
|
6445
|
+
});
|
|
6446
|
+
}
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
const snapshots = this.sessionCapturer.actionSnapshots;
|
|
6450
|
+
const tracePath = await writeTraceZip(this.sessionCapturer, {
|
|
6451
|
+
outputDir: resolveAdapterOutputDir({
|
|
6452
|
+
configPath: this.#configPath
|
|
6453
|
+
}),
|
|
6454
|
+
sessionId,
|
|
6455
|
+
actionSnapshots: snapshots.length ? snapshots : void 0,
|
|
6456
|
+
format: this.options.traceFormat,
|
|
6457
|
+
testMetadata
|
|
6458
|
+
});
|
|
6459
|
+
log11.info(`Trace saved to ${tracePath}`);
|
|
6460
|
+
} catch (err) {
|
|
6461
|
+
log11.warn(`trace write failed: ${errorMessage(err)}`);
|
|
6462
|
+
}
|
|
6463
|
+
}
|
|
3848
6464
|
async #waitForDevtoolsBrowserClose() {
|
|
3849
6465
|
await waitForDevtoolsBrowserClose(this.#getInternals());
|
|
3850
6466
|
}
|