@wdio/nightwatch-devtools 1.4.0 → 1.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +167 -104
- package/dist/index.js.map +1 -1
- package/package.json +7 -3
package/dist/index.js
CHANGED
|
@@ -17,7 +17,72 @@ import {
|
|
|
17
17
|
// src/index.ts
|
|
18
18
|
import { fileURLToPath } from "url";
|
|
19
19
|
|
|
20
|
-
// ../
|
|
20
|
+
// ../shared/src/files.ts
|
|
21
|
+
var SPEC_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/i;
|
|
22
|
+
|
|
23
|
+
// ../shared/src/routes.ts
|
|
24
|
+
var WS_PATHS = {
|
|
25
|
+
/** Adapter session upgrade endpoint. One socket per running adapter. */
|
|
26
|
+
worker: "/worker",
|
|
27
|
+
/** App/UI client upgrade endpoint. Multiple browser tabs may connect. */
|
|
28
|
+
client: "/client"
|
|
29
|
+
};
|
|
30
|
+
var WS_SCOPE = {
|
|
31
|
+
/** Backend → worker: a dashboard client has subscribed. Wakes up the
|
|
32
|
+
* adapter's `await UI ready` gate before tests start. */
|
|
33
|
+
clientConnected: "clientConnected",
|
|
34
|
+
/** Backend → worker: the last dashboard tab closed. Triggers the
|
|
35
|
+
* interactive shutdown flow (close WS, exit, pkill the dashboard). */
|
|
36
|
+
clientDisconnected: "clientDisconnected",
|
|
37
|
+
/** Worker → backend → clients (or app-local): wipe the visualization data
|
|
38
|
+
* for a specific test/suite uid (or the whole tree). */
|
|
39
|
+
clearExecutionData: "clearExecutionData",
|
|
40
|
+
/** Worker → backend: clear-by-test-uid request (drives clearExecutionData). */
|
|
41
|
+
clearCommands: "clearCommands",
|
|
42
|
+
/** Backend → clients: signal that the active run was stopped by the UI. */
|
|
43
|
+
testStopped: "testStopped",
|
|
44
|
+
/** Worker → backend → clients: swap an earlier captured command for an
|
|
45
|
+
* updated entry (used when a retry coalesces commands). */
|
|
46
|
+
replaceCommand: "replaceCommand",
|
|
47
|
+
/** Worker → backend: register the config file path so reruns can spawn
|
|
48
|
+
* with the same config. */
|
|
49
|
+
config: "config"
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// ../shared/src/runner.ts
|
|
53
|
+
var REUSE_ENV = {
|
|
54
|
+
REUSE: "DEVTOOLS_APP_REUSE",
|
|
55
|
+
HOST: "DEVTOOLS_APP_HOST",
|
|
56
|
+
PORT: "DEVTOOLS_APP_PORT",
|
|
57
|
+
RERUN_LABEL: "DEVTOOLS_RERUN_LABEL",
|
|
58
|
+
RERUN_ENTRY_TYPE: "DEVTOOLS_RERUN_ENTRY_TYPE"
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
// ../shared/src/timing.ts
|
|
62
|
+
var TIMING_BASE = {
|
|
63
|
+
/** ms to let the dashboard render between rapid state updates. */
|
|
64
|
+
UI_RENDER_DELAY: 150,
|
|
65
|
+
/** ms to wait after a test starts before flushing initial state. */
|
|
66
|
+
TEST_START_DELAY: 100,
|
|
67
|
+
/** ms gap between consecutive suite-finalize broadcasts. */
|
|
68
|
+
SUITE_COMPLETE_DELAY: 200,
|
|
69
|
+
/** ms allowed for the browser-close handshake before forcing teardown. */
|
|
70
|
+
BROWSER_CLOSE_WAIT: 2e3,
|
|
71
|
+
/** ms before first worker WS connection attempt — lets backend bind. */
|
|
72
|
+
INITIAL_CONNECTION_WAIT: 500,
|
|
73
|
+
/** ms between browser-window-alive polls. */
|
|
74
|
+
BROWSER_POLL_INTERVAL: 1e3
|
|
75
|
+
};
|
|
76
|
+
var DEFAULTS_BASE = {
|
|
77
|
+
/** Synthetic capability ID — present on every CommandLog / TestStats. */
|
|
78
|
+
CID: "0-0",
|
|
79
|
+
/** TestStats.retries default — adapters that surface retry counts override. */
|
|
80
|
+
RETRIES: 0,
|
|
81
|
+
/** _duration placeholder for not-yet-finalized stats. */
|
|
82
|
+
DURATION: 0
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// ../shared/src/trace-actions.ts
|
|
21
86
|
var ACTION_MAP = {
|
|
22
87
|
// WDIO browser-level
|
|
23
88
|
url: { class: "Page", method: "navigate" },
|
|
@@ -48,6 +113,25 @@ var ACTION_MAP = {
|
|
|
48
113
|
switchToFrame: { class: "Frame", method: "goto" },
|
|
49
114
|
touchAction: { class: "Element", method: "tap" }
|
|
50
115
|
};
|
|
116
|
+
|
|
117
|
+
// ../shared/src/types.ts
|
|
118
|
+
var TEST_STATE = {
|
|
119
|
+
PENDING: "pending",
|
|
120
|
+
RUNNING: "running",
|
|
121
|
+
PASSED: "passed",
|
|
122
|
+
FAILED: "failed",
|
|
123
|
+
SKIPPED: "skipped"
|
|
124
|
+
};
|
|
125
|
+
var SCREENCAST_DEFAULTS = {
|
|
126
|
+
enabled: false,
|
|
127
|
+
captureFormat: "jpeg",
|
|
128
|
+
quality: 70,
|
|
129
|
+
maxWidth: 1280,
|
|
130
|
+
maxHeight: 720,
|
|
131
|
+
pollIntervalMs: 200
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// ../core/src/action-mapping.ts
|
|
51
135
|
function mapCommandToAction(command) {
|
|
52
136
|
return ACTION_MAP[command] ?? null;
|
|
53
137
|
}
|
|
@@ -470,10 +554,45 @@ function isSameElement(node1, node2) {
|
|
|
470
554
|
}
|
|
471
555
|
return false;
|
|
472
556
|
}
|
|
557
|
+
function sanitizePageSource(sourceXML) {
|
|
558
|
+
let sanitized = sourceXML.replace(
|
|
559
|
+
/&(?!(?:amp|lt|gt|quot|apos|#\d+|#x[0-9a-fA-F]+);)/g,
|
|
560
|
+
"&"
|
|
561
|
+
);
|
|
562
|
+
const VOID_ELEMENTS = [
|
|
563
|
+
"link",
|
|
564
|
+
"meta",
|
|
565
|
+
"br",
|
|
566
|
+
"hr",
|
|
567
|
+
"img",
|
|
568
|
+
"input",
|
|
569
|
+
"source",
|
|
570
|
+
"area",
|
|
571
|
+
"base",
|
|
572
|
+
"col",
|
|
573
|
+
"embed",
|
|
574
|
+
"track",
|
|
575
|
+
"wbr"
|
|
576
|
+
];
|
|
577
|
+
for (const tag of VOID_ELEMENTS) {
|
|
578
|
+
sanitized = sanitized.replace(
|
|
579
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- tag from hardcoded list
|
|
580
|
+
new RegExp(`<${tag}(\\s[^>]*?[^/])>`, "gi"),
|
|
581
|
+
`<${tag}$1/>`
|
|
582
|
+
);
|
|
583
|
+
sanitized = sanitized.replace(
|
|
584
|
+
// eslint-disable-next-line security/detect-non-literal-regexp -- tag from hardcoded list
|
|
585
|
+
new RegExp(`<${tag}>`, "gi"),
|
|
586
|
+
`<${tag}/>`
|
|
587
|
+
);
|
|
588
|
+
}
|
|
589
|
+
return sanitized;
|
|
590
|
+
}
|
|
473
591
|
function xmlToJSON(sourceXML) {
|
|
474
592
|
try {
|
|
593
|
+
const sanitized = sanitizePageSource(sourceXML);
|
|
475
594
|
const parser = new DOMParser();
|
|
476
|
-
const sourceDoc = parser.parseFromString(
|
|
595
|
+
const sourceDoc = parser.parseFromString(sanitized, "text/xml");
|
|
477
596
|
const parseErrors = sourceDoc.getElementsByTagName("parsererror");
|
|
478
597
|
if (parseErrors.length > 0) {
|
|
479
598
|
console.error(
|
|
@@ -492,8 +611,9 @@ function xmlToJSON(sourceXML) {
|
|
|
492
611
|
}
|
|
493
612
|
function xmlToDOM(sourceXML) {
|
|
494
613
|
try {
|
|
614
|
+
const sanitized = sanitizePageSource(sourceXML);
|
|
495
615
|
const parser = new DOMParser();
|
|
496
|
-
const doc = parser.parseFromString(
|
|
616
|
+
const doc = parser.parseFromString(sanitized, "text/xml");
|
|
497
617
|
const parseErrors = doc.getElementsByTagName("parsererror");
|
|
498
618
|
if (parseErrors.length > 0) {
|
|
499
619
|
console.error("[xmlToDOM] XML parsing error:", parseErrors[0].textContent);
|
|
@@ -750,7 +870,7 @@ function isValidValue(value) {
|
|
|
750
870
|
return value !== void 0 && value !== null && value !== "null" && value.trim() !== "";
|
|
751
871
|
}
|
|
752
872
|
function escapeText(text) {
|
|
753
|
-
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
873
|
+
return text.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/\n/g, "\\n").replace(/\r/g, "\\r").replace(/\t/g, "\\t");
|
|
754
874
|
}
|
|
755
875
|
function escapeXPathValue(value) {
|
|
756
876
|
if (!value.includes("'")) {
|
|
@@ -1206,21 +1326,8 @@ function serializeWebSnapshot(nodes, context, options = {}) {
|
|
|
1206
1326
|
}
|
|
1207
1327
|
const indent = " ".repeat(node.depth + 1);
|
|
1208
1328
|
const isInteractive = INTERACTIVE_ROLES.has(node.role);
|
|
1209
|
-
if (
|
|
1210
|
-
|
|
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
|
-
}
|
|
1329
|
+
if (isStatictextEchoedByParent(nodes, i)) {
|
|
1330
|
+
continue;
|
|
1224
1331
|
}
|
|
1225
1332
|
const roleLabel = node.role === "heading" && node.level ? `heading[${node.level}]` : node.role;
|
|
1226
1333
|
if (isInteractive) {
|
|
@@ -1245,6 +1352,22 @@ function serializeWebSnapshot(nodes, context, options = {}) {
|
|
|
1245
1352
|
}
|
|
1246
1353
|
return lines.join("\n");
|
|
1247
1354
|
}
|
|
1355
|
+
function isStatictextEchoedByParent(nodes, index) {
|
|
1356
|
+
const node = nodes[index];
|
|
1357
|
+
if (node.role !== "statictext" || !node.name) {
|
|
1358
|
+
return false;
|
|
1359
|
+
}
|
|
1360
|
+
for (let j = index - 1; j >= 0; j--) {
|
|
1361
|
+
if (nodes[j].depth < node.depth) {
|
|
1362
|
+
const parent = nodes[j];
|
|
1363
|
+
if (INTERACTIVE_ROLES.has(parent.role) && parent.name && parent.name.includes(node.name)) {
|
|
1364
|
+
return true;
|
|
1365
|
+
}
|
|
1366
|
+
break;
|
|
1367
|
+
}
|
|
1368
|
+
}
|
|
1369
|
+
return false;
|
|
1370
|
+
}
|
|
1248
1371
|
function simplifyTag(tagName) {
|
|
1249
1372
|
const dot = tagName.lastIndexOf(".");
|
|
1250
1373
|
if (dot !== -1) {
|
|
@@ -1308,6 +1431,20 @@ function classifyMobileRole(tagName, platform) {
|
|
|
1308
1431
|
}
|
|
1309
1432
|
return IOS_ROLE_MAP[tagName] || simplifyTag(tagName);
|
|
1310
1433
|
}
|
|
1434
|
+
function getFirstChildText(element) {
|
|
1435
|
+
const queue = [...element.children || []];
|
|
1436
|
+
while (queue.length > 0) {
|
|
1437
|
+
const el = queue.shift();
|
|
1438
|
+
const text = el.attributes?.text?.trim();
|
|
1439
|
+
if (text) {
|
|
1440
|
+
return text;
|
|
1441
|
+
}
|
|
1442
|
+
if (el.children) {
|
|
1443
|
+
queue.push(...el.children);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1446
|
+
return void 0;
|
|
1447
|
+
}
|
|
1311
1448
|
function getBestAndroidLocator(attrs) {
|
|
1312
1449
|
if (attrs._selector) {
|
|
1313
1450
|
return attrs._selector;
|
|
@@ -1393,7 +1530,7 @@ function isMobileInViewport(element, platform, viewport) {
|
|
|
1393
1530
|
function collectMobileNodes(element, platform, depth, nodes, walkOpts) {
|
|
1394
1531
|
const attrs = element.attributes;
|
|
1395
1532
|
const role = classifyMobileRole(element.tagName, platform);
|
|
1396
|
-
|
|
1533
|
+
let name = getMobileNodeIdentity(attrs, platform);
|
|
1397
1534
|
const explicit = isExplicitlyInteractive(attrs, platform);
|
|
1398
1535
|
const interactive = isMobileInteractive(element, platform);
|
|
1399
1536
|
const inViewport = isMobileInViewport(element, platform, walkOpts.viewport);
|
|
@@ -1412,7 +1549,8 @@ function collectMobileNodes(element, platform, depth, nodes, walkOpts) {
|
|
|
1412
1549
|
depth,
|
|
1413
1550
|
isInteractive: false,
|
|
1414
1551
|
isExplicitInteractive: false,
|
|
1415
|
-
isInViewport: false
|
|
1552
|
+
isInViewport: false,
|
|
1553
|
+
tagName: element.tagName
|
|
1416
1554
|
});
|
|
1417
1555
|
return;
|
|
1418
1556
|
}
|
|
@@ -1437,6 +1575,12 @@ function collectMobileNodes(element, platform, depth, nodes, walkOpts) {
|
|
|
1437
1575
|
if (!locator) {
|
|
1438
1576
|
locator = (platform === "android" ? getBestAndroidLocator(attrs) : getBestIOSLocator(attrs)) ?? "";
|
|
1439
1577
|
}
|
|
1578
|
+
if (!name && platform === "android" && (locator.startsWith("class:") || locator.startsWith('android=new UiSelector().className("'))) {
|
|
1579
|
+
const childText = getFirstChildText(element);
|
|
1580
|
+
if (childText && childText.length < 100) {
|
|
1581
|
+
name = childText;
|
|
1582
|
+
}
|
|
1583
|
+
}
|
|
1440
1584
|
}
|
|
1441
1585
|
nodes.push({
|
|
1442
1586
|
role,
|
|
@@ -1445,7 +1589,8 @@ function collectMobileNodes(element, platform, depth, nodes, walkOpts) {
|
|
|
1445
1589
|
depth,
|
|
1446
1590
|
isInteractive: interactive,
|
|
1447
1591
|
isExplicitInteractive: explicit,
|
|
1448
|
-
isInViewport: inViewport
|
|
1592
|
+
isInViewport: inViewport,
|
|
1593
|
+
tagName: element.tagName
|
|
1449
1594
|
});
|
|
1450
1595
|
for (const child of element.children || []) {
|
|
1451
1596
|
collectMobileNodes(child, platform, depth + 1, nodes, walkOpts);
|
|
@@ -2695,88 +2840,6 @@ var RetryTracker = class {
|
|
|
2695
2840
|
}
|
|
2696
2841
|
};
|
|
2697
2842
|
|
|
2698
|
-
// ../shared/src/files.ts
|
|
2699
|
-
var SPEC_FILE_RE = /\.(?:test|spec)\.[cm]?[jt]sx?$/i;
|
|
2700
|
-
|
|
2701
|
-
// ../shared/src/routes.ts
|
|
2702
|
-
var WS_PATHS = {
|
|
2703
|
-
/** Adapter session upgrade endpoint. One socket per running adapter. */
|
|
2704
|
-
worker: "/worker",
|
|
2705
|
-
/** App/UI client upgrade endpoint. Multiple browser tabs may connect. */
|
|
2706
|
-
client: "/client"
|
|
2707
|
-
};
|
|
2708
|
-
var WS_SCOPE = {
|
|
2709
|
-
/** Backend → worker: a dashboard client has subscribed. Wakes up the
|
|
2710
|
-
* adapter's `await UI ready` gate before tests start. */
|
|
2711
|
-
clientConnected: "clientConnected",
|
|
2712
|
-
/** Backend → worker: the last dashboard tab closed. Triggers the
|
|
2713
|
-
* interactive shutdown flow (close WS, exit, pkill the dashboard). */
|
|
2714
|
-
clientDisconnected: "clientDisconnected",
|
|
2715
|
-
/** Worker → backend → clients (or app-local): wipe the visualization data
|
|
2716
|
-
* for a specific test/suite uid (or the whole tree). */
|
|
2717
|
-
clearExecutionData: "clearExecutionData",
|
|
2718
|
-
/** Worker → backend: clear-by-test-uid request (drives clearExecutionData). */
|
|
2719
|
-
clearCommands: "clearCommands",
|
|
2720
|
-
/** Backend → clients: signal that the active run was stopped by the UI. */
|
|
2721
|
-
testStopped: "testStopped",
|
|
2722
|
-
/** Worker → backend → clients: swap an earlier captured command for an
|
|
2723
|
-
* updated entry (used when a retry coalesces commands). */
|
|
2724
|
-
replaceCommand: "replaceCommand",
|
|
2725
|
-
/** Worker → backend: register the config file path so reruns can spawn
|
|
2726
|
-
* with the same config. */
|
|
2727
|
-
config: "config"
|
|
2728
|
-
};
|
|
2729
|
-
|
|
2730
|
-
// ../shared/src/runner.ts
|
|
2731
|
-
var REUSE_ENV = {
|
|
2732
|
-
REUSE: "DEVTOOLS_APP_REUSE",
|
|
2733
|
-
HOST: "DEVTOOLS_APP_HOST",
|
|
2734
|
-
PORT: "DEVTOOLS_APP_PORT",
|
|
2735
|
-
RERUN_LABEL: "DEVTOOLS_RERUN_LABEL",
|
|
2736
|
-
RERUN_ENTRY_TYPE: "DEVTOOLS_RERUN_ENTRY_TYPE"
|
|
2737
|
-
};
|
|
2738
|
-
|
|
2739
|
-
// ../shared/src/timing.ts
|
|
2740
|
-
var TIMING_BASE = {
|
|
2741
|
-
/** ms to let the dashboard render between rapid state updates. */
|
|
2742
|
-
UI_RENDER_DELAY: 150,
|
|
2743
|
-
/** ms to wait after a test starts before flushing initial state. */
|
|
2744
|
-
TEST_START_DELAY: 100,
|
|
2745
|
-
/** ms gap between consecutive suite-finalize broadcasts. */
|
|
2746
|
-
SUITE_COMPLETE_DELAY: 200,
|
|
2747
|
-
/** ms allowed for the browser-close handshake before forcing teardown. */
|
|
2748
|
-
BROWSER_CLOSE_WAIT: 2e3,
|
|
2749
|
-
/** ms before first worker WS connection attempt — lets backend bind. */
|
|
2750
|
-
INITIAL_CONNECTION_WAIT: 500,
|
|
2751
|
-
/** ms between browser-window-alive polls. */
|
|
2752
|
-
BROWSER_POLL_INTERVAL: 1e3
|
|
2753
|
-
};
|
|
2754
|
-
var DEFAULTS_BASE = {
|
|
2755
|
-
/** Synthetic capability ID — present on every CommandLog / TestStats. */
|
|
2756
|
-
CID: "0-0",
|
|
2757
|
-
/** TestStats.retries default — adapters that surface retry counts override. */
|
|
2758
|
-
RETRIES: 0,
|
|
2759
|
-
/** _duration placeholder for not-yet-finalized stats. */
|
|
2760
|
-
DURATION: 0
|
|
2761
|
-
};
|
|
2762
|
-
|
|
2763
|
-
// ../shared/src/types.ts
|
|
2764
|
-
var TEST_STATE = {
|
|
2765
|
-
PENDING: "pending",
|
|
2766
|
-
RUNNING: "running",
|
|
2767
|
-
PASSED: "passed",
|
|
2768
|
-
FAILED: "failed",
|
|
2769
|
-
SKIPPED: "skipped"
|
|
2770
|
-
};
|
|
2771
|
-
var SCREENCAST_DEFAULTS = {
|
|
2772
|
-
enabled: false,
|
|
2773
|
-
captureFormat: "jpeg",
|
|
2774
|
-
quality: 70,
|
|
2775
|
-
maxWidth: 1280,
|
|
2776
|
-
maxHeight: 720,
|
|
2777
|
-
pollIntervalMs: 200
|
|
2778
|
-
};
|
|
2779
|
-
|
|
2780
2843
|
// ../core/src/screencast.ts
|
|
2781
2844
|
var ScreencastRecorderBase = class {
|
|
2782
2845
|
buffer = [];
|