@wdio/mcp 2.0.0 → 2.2.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 +15 -6
- package/lib/server.js +611 -302
- package/lib/server.js.map +1 -1
- package/lib/snapshot.d.ts +123 -0
- package/lib/snapshot.js +1178 -0
- package/lib/snapshot.js.map +1 -0
- package/package.json +12 -1
package/lib/server.js
CHANGED
|
@@ -7,10 +7,13 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
7
7
|
// src/tools/browser.tool.ts
|
|
8
8
|
import { remote } from "webdriverio";
|
|
9
9
|
import { z } from "zod";
|
|
10
|
+
var supportedBrowsers = ["chrome", "firefox", "edge", "safari"];
|
|
11
|
+
var browserSchema = z.enum(supportedBrowsers).default("chrome");
|
|
10
12
|
var startBrowserToolDefinition = {
|
|
11
13
|
name: "start_browser",
|
|
12
|
-
description: "starts a browser session and sets it to the current state",
|
|
14
|
+
description: "starts a browser session (Chrome, Firefox, Edge, Safari) and sets it to the current state",
|
|
13
15
|
inputSchema: {
|
|
16
|
+
browser: browserSchema.describe("Browser to launch: chrome, firefox, edge, safari (default: chrome)"),
|
|
14
17
|
headless: z.boolean().optional(),
|
|
15
18
|
windowWidth: z.number().min(400).max(3840).optional().default(1920),
|
|
16
19
|
windowHeight: z.number().min(400).max(2160).optional().default(1080),
|
|
@@ -38,12 +41,22 @@ var getBrowser = () => {
|
|
|
38
41
|
};
|
|
39
42
|
getBrowser.__state = state;
|
|
40
43
|
var startBrowserTool = async ({
|
|
44
|
+
browser = "chrome",
|
|
41
45
|
headless = false,
|
|
42
46
|
windowWidth = 1920,
|
|
43
47
|
windowHeight = 1080,
|
|
44
48
|
navigationUrl
|
|
45
49
|
}) => {
|
|
46
|
-
const
|
|
50
|
+
const browserDisplayNames = {
|
|
51
|
+
chrome: "Chrome",
|
|
52
|
+
firefox: "Firefox",
|
|
53
|
+
edge: "Edge",
|
|
54
|
+
safari: "Safari"
|
|
55
|
+
};
|
|
56
|
+
const selectedBrowser = browser;
|
|
57
|
+
const headlessSupported = selectedBrowser !== "safari";
|
|
58
|
+
const effectiveHeadless = headless && headlessSupported;
|
|
59
|
+
const chromiumArgs = [
|
|
47
60
|
`--window-size=${windowWidth},${windowHeight}`,
|
|
48
61
|
"--no-sandbox",
|
|
49
62
|
"--disable-search-engine-choice-screen",
|
|
@@ -54,37 +67,66 @@ var startBrowserTool = async ({
|
|
|
54
67
|
"--disable-web-security",
|
|
55
68
|
"--allow-running-insecure-content"
|
|
56
69
|
];
|
|
57
|
-
if (
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
}
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
+
if (effectiveHeadless) {
|
|
71
|
+
chromiumArgs.push("--headless=new");
|
|
72
|
+
chromiumArgs.push("--disable-gpu");
|
|
73
|
+
chromiumArgs.push("--disable-dev-shm-usage");
|
|
74
|
+
}
|
|
75
|
+
const firefoxArgs = [];
|
|
76
|
+
if (effectiveHeadless && selectedBrowser === "firefox") {
|
|
77
|
+
firefoxArgs.push("-headless");
|
|
78
|
+
}
|
|
79
|
+
const capabilities = {
|
|
80
|
+
acceptInsecureCerts: true
|
|
81
|
+
};
|
|
82
|
+
switch (selectedBrowser) {
|
|
83
|
+
case "chrome":
|
|
84
|
+
capabilities.browserName = "chrome";
|
|
85
|
+
capabilities["goog:chromeOptions"] = { args: chromiumArgs };
|
|
86
|
+
break;
|
|
87
|
+
case "edge":
|
|
88
|
+
capabilities.browserName = "msedge";
|
|
89
|
+
capabilities["ms:edgeOptions"] = { args: chromiumArgs };
|
|
90
|
+
break;
|
|
91
|
+
case "firefox":
|
|
92
|
+
capabilities.browserName = "firefox";
|
|
93
|
+
if (firefoxArgs.length > 0) {
|
|
94
|
+
capabilities["moz:firefoxOptions"] = { args: firefoxArgs };
|
|
95
|
+
}
|
|
96
|
+
break;
|
|
97
|
+
case "safari":
|
|
98
|
+
capabilities.browserName = "safari";
|
|
99
|
+
break;
|
|
100
|
+
}
|
|
101
|
+
const wdioBrowser = await remote({
|
|
102
|
+
capabilities
|
|
70
103
|
});
|
|
71
|
-
const { sessionId } =
|
|
72
|
-
state.browsers.set(sessionId,
|
|
104
|
+
const { sessionId } = wdioBrowser;
|
|
105
|
+
state.browsers.set(sessionId, wdioBrowser);
|
|
73
106
|
state.currentSession = sessionId;
|
|
74
107
|
state.sessionMetadata.set(sessionId, {
|
|
75
108
|
type: "browser",
|
|
76
|
-
capabilities:
|
|
109
|
+
capabilities: wdioBrowser.capabilities,
|
|
77
110
|
isAttached: false
|
|
78
111
|
});
|
|
112
|
+
let sizeNote = "";
|
|
113
|
+
try {
|
|
114
|
+
await wdioBrowser.setWindowSize(windowWidth, windowHeight);
|
|
115
|
+
} catch (e) {
|
|
116
|
+
sizeNote = `
|
|
117
|
+
Note: Unable to set window size (${windowWidth}x${windowHeight}). ${e}`;
|
|
118
|
+
}
|
|
79
119
|
if (navigationUrl) {
|
|
80
|
-
await
|
|
120
|
+
await wdioBrowser.url(navigationUrl);
|
|
81
121
|
}
|
|
82
|
-
const modeText =
|
|
122
|
+
const modeText = effectiveHeadless ? "headless" : "headed";
|
|
123
|
+
const browserText = browserDisplayNames[selectedBrowser];
|
|
83
124
|
const urlText = navigationUrl ? ` and navigated to ${navigationUrl}` : "";
|
|
125
|
+
const headlessNote = headless && !headlessSupported ? "\nNote: Safari does not support headless mode. Started in headed mode." : "";
|
|
84
126
|
return {
|
|
85
127
|
content: [{
|
|
86
128
|
type: "text",
|
|
87
|
-
text:
|
|
129
|
+
text: `${browserText} browser started in ${modeText} mode with sessionId: ${sessionId} (${windowWidth}x${windowHeight})${urlText}${headlessNote}${sizeNote}`
|
|
88
130
|
}]
|
|
89
131
|
};
|
|
90
132
|
};
|
|
@@ -572,130 +614,32 @@ var elementsScript = (elementType = "interactable") => (function() {
|
|
|
572
614
|
const isInViewport = rect.top >= 0 && rect.left >= 0 && rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && rect.right <= (window.innerWidth || document.documentElement.clientWidth);
|
|
573
615
|
const info = {
|
|
574
616
|
tagName: el.tagName.toLowerCase(),
|
|
617
|
+
type: el.getAttribute("type") || "",
|
|
618
|
+
id: el.id || "",
|
|
619
|
+
className: (typeof el.className === "string" ? el.className : "") || "",
|
|
620
|
+
textContent: el.textContent?.trim() || "",
|
|
621
|
+
value: inputEl.value || "",
|
|
622
|
+
placeholder: inputEl.placeholder || "",
|
|
623
|
+
href: el.getAttribute("href") || "",
|
|
624
|
+
ariaLabel: el.getAttribute("aria-label") || "",
|
|
625
|
+
role: el.getAttribute("role") || "",
|
|
626
|
+
src: el.getAttribute("src") || "",
|
|
627
|
+
alt: el.getAttribute("alt") || "",
|
|
575
628
|
cssSelector: getCssSelector(el),
|
|
576
629
|
isInViewport
|
|
577
630
|
};
|
|
578
|
-
const type = el.getAttribute("type");
|
|
579
|
-
if (type) info.type = type;
|
|
580
|
-
const id = el.id;
|
|
581
|
-
if (id) info.id = id;
|
|
582
|
-
const className = el.className;
|
|
583
|
-
if (className && typeof className === "string") info.className = className;
|
|
584
|
-
const textContent = el.textContent?.trim();
|
|
585
|
-
if (textContent) info.textContent = textContent;
|
|
586
|
-
const value = inputEl.value;
|
|
587
|
-
if (value) info.value = value;
|
|
588
|
-
const placeholder = inputEl.placeholder;
|
|
589
|
-
if (placeholder) info.placeholder = placeholder;
|
|
590
|
-
const href = el.getAttribute("href");
|
|
591
|
-
if (href) info.href = href;
|
|
592
|
-
const ariaLabel = el.getAttribute("aria-label");
|
|
593
|
-
if (ariaLabel) info.ariaLabel = ariaLabel;
|
|
594
|
-
const role = el.getAttribute("role");
|
|
595
|
-
if (role) info.role = role;
|
|
596
|
-
const src = el.getAttribute("src");
|
|
597
|
-
if (src) info.src = src;
|
|
598
|
-
const alt = el.getAttribute("alt");
|
|
599
|
-
if (alt) info.alt = alt;
|
|
600
|
-
if (elementType === "visual" || elementType === "all") {
|
|
601
|
-
const bgImage = window.getComputedStyle(el).backgroundImage;
|
|
602
|
-
if (bgImage && bgImage !== "none") info.backgroundImage = bgImage;
|
|
603
|
-
}
|
|
604
631
|
return info;
|
|
605
632
|
});
|
|
606
633
|
return elementInfos;
|
|
607
634
|
}
|
|
608
635
|
return getElements();
|
|
609
636
|
})();
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
import { DOMParser } from "@xmldom/xmldom";
|
|
614
|
-
function childNodesOf(node) {
|
|
615
|
-
const children = [];
|
|
616
|
-
if (node.childNodes) {
|
|
617
|
-
for (let i = 0; i < node.childNodes.length; i++) {
|
|
618
|
-
const child = node.childNodes.item(i);
|
|
619
|
-
if (child?.nodeType === 1) {
|
|
620
|
-
children.push(child);
|
|
621
|
-
}
|
|
622
|
-
}
|
|
623
|
-
}
|
|
624
|
-
return children;
|
|
625
|
-
}
|
|
626
|
-
function translateRecursively(domNode, parentPath = "", index = null) {
|
|
627
|
-
const attributes = {};
|
|
628
|
-
const element = domNode;
|
|
629
|
-
if (element.attributes) {
|
|
630
|
-
for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) {
|
|
631
|
-
const attr = element.attributes.item(attrIdx);
|
|
632
|
-
if (attr) {
|
|
633
|
-
attributes[attr.name] = attr.value.replace(/(\n)/gm, "\\n");
|
|
634
|
-
}
|
|
635
|
-
}
|
|
636
|
-
}
|
|
637
|
-
const path = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
|
|
638
|
-
return {
|
|
639
|
-
children: childNodesOf(domNode).map(
|
|
640
|
-
(childNode, childIndex) => translateRecursively(childNode, path, childIndex)
|
|
641
|
-
),
|
|
642
|
-
tagName: domNode.nodeName,
|
|
643
|
-
attributes,
|
|
644
|
-
path
|
|
645
|
-
};
|
|
646
|
-
}
|
|
647
|
-
function xmlToJSON(sourceXML) {
|
|
648
|
-
try {
|
|
649
|
-
const parser = new DOMParser();
|
|
650
|
-
const sourceDoc = parser.parseFromString(sourceXML, "text/xml");
|
|
651
|
-
const parseErrors = sourceDoc.getElementsByTagName("parsererror");
|
|
652
|
-
if (parseErrors.length > 0) {
|
|
653
|
-
console.error("[xmlToJSON] XML parsing error:", parseErrors[0].textContent);
|
|
654
|
-
return null;
|
|
655
|
-
}
|
|
656
|
-
const children = childNodesOf(sourceDoc);
|
|
657
|
-
const firstChild = children[0] || (sourceDoc.documentElement ? childNodesOf(sourceDoc.documentElement)[0] : null);
|
|
658
|
-
return firstChild ? translateRecursively(firstChild) : { children: [], tagName: "", attributes: {}, path: "" };
|
|
659
|
-
} catch (e) {
|
|
660
|
-
console.error("[xmlToJSON] Failed to parse XML:", e);
|
|
661
|
-
return null;
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
function parseAndroidBounds(bounds) {
|
|
665
|
-
const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
666
|
-
if (!match) {
|
|
667
|
-
return { x: 0, y: 0, width: 0, height: 0 };
|
|
668
|
-
}
|
|
669
|
-
const x1 = parseInt(match[1], 10);
|
|
670
|
-
const y1 = parseInt(match[2], 10);
|
|
671
|
-
const x2 = parseInt(match[3], 10);
|
|
672
|
-
const y2 = parseInt(match[4], 10);
|
|
673
|
-
return {
|
|
674
|
-
x: x1,
|
|
675
|
-
y: y1,
|
|
676
|
-
width: x2 - x1,
|
|
677
|
-
height: y2 - y1
|
|
678
|
-
};
|
|
679
|
-
}
|
|
680
|
-
function parseIOSBounds(attributes) {
|
|
681
|
-
return {
|
|
682
|
-
x: parseInt(attributes.x || "0", 10),
|
|
683
|
-
y: parseInt(attributes.y || "0", 10),
|
|
684
|
-
width: parseInt(attributes.width || "0", 10),
|
|
685
|
-
height: parseInt(attributes.height || "0", 10)
|
|
686
|
-
};
|
|
687
|
-
}
|
|
688
|
-
function countAttributeOccurrences(sourceXML, attribute, value) {
|
|
689
|
-
const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
690
|
-
const pattern = new RegExp(`${attribute}=["']${escapedValue}["']`, "g");
|
|
691
|
-
const matches = sourceXML.match(pattern);
|
|
692
|
-
return matches ? matches.length : 0;
|
|
693
|
-
}
|
|
694
|
-
function isAttributeUnique(sourceXML, attribute, value) {
|
|
695
|
-
return countAttributeOccurrences(sourceXML, attribute, value) === 1;
|
|
637
|
+
async function getBrowserInteractableElements(browser, options = {}) {
|
|
638
|
+
const { elementType = "interactable" } = options;
|
|
639
|
+
return browser.execute(elementsScript, elementType);
|
|
696
640
|
}
|
|
697
641
|
|
|
698
|
-
// src/locators/
|
|
642
|
+
// src/locators/constants.ts
|
|
699
643
|
var ANDROID_INTERACTABLE_TAGS = [
|
|
700
644
|
// Input elements
|
|
701
645
|
"android.widget.EditText",
|
|
@@ -730,43 +674,6 @@ var ANDROID_INTERACTABLE_TAGS = [
|
|
|
730
674
|
// List/grid items
|
|
731
675
|
"android.widget.AdapterView"
|
|
732
676
|
];
|
|
733
|
-
var IOS_INTERACTABLE_TAGS = [
|
|
734
|
-
// Input elements
|
|
735
|
-
"XCUIElementTypeTextField",
|
|
736
|
-
"XCUIElementTypeSecureTextField",
|
|
737
|
-
"XCUIElementTypeTextView",
|
|
738
|
-
"XCUIElementTypeSearchField",
|
|
739
|
-
// Button-like elements
|
|
740
|
-
"XCUIElementTypeButton",
|
|
741
|
-
"XCUIElementTypeLink",
|
|
742
|
-
// Text elements (often tappable)
|
|
743
|
-
"XCUIElementTypeStaticText",
|
|
744
|
-
// Image elements
|
|
745
|
-
"XCUIElementTypeImage",
|
|
746
|
-
"XCUIElementTypeIcon",
|
|
747
|
-
// Selection elements
|
|
748
|
-
"XCUIElementTypeSwitch",
|
|
749
|
-
"XCUIElementTypeSlider",
|
|
750
|
-
"XCUIElementTypeStepper",
|
|
751
|
-
"XCUIElementTypeSegmentedControl",
|
|
752
|
-
"XCUIElementTypePicker",
|
|
753
|
-
"XCUIElementTypePickerWheel",
|
|
754
|
-
"XCUIElementTypeDatePicker",
|
|
755
|
-
"XCUIElementTypePageIndicator",
|
|
756
|
-
// Table/list items
|
|
757
|
-
"XCUIElementTypeCell",
|
|
758
|
-
"XCUIElementTypeMenuItem",
|
|
759
|
-
"XCUIElementTypeMenuBarItem",
|
|
760
|
-
// Toggle elements
|
|
761
|
-
"XCUIElementTypeCheckBox",
|
|
762
|
-
"XCUIElementTypeRadioButton",
|
|
763
|
-
"XCUIElementTypeToggle",
|
|
764
|
-
// Other interactive
|
|
765
|
-
"XCUIElementTypeKey",
|
|
766
|
-
"XCUIElementTypeKeyboard",
|
|
767
|
-
"XCUIElementTypeAlert",
|
|
768
|
-
"XCUIElementTypeSheet"
|
|
769
|
-
];
|
|
770
677
|
var ANDROID_LAYOUT_CONTAINERS = [
|
|
771
678
|
// Core ViewGroup classes
|
|
772
679
|
"android.view.ViewGroup",
|
|
@@ -805,6 +712,43 @@ var ANDROID_LAYOUT_CONTAINERS = [
|
|
|
805
712
|
"com.android.internal.policy.DecorView",
|
|
806
713
|
"android.widget.DecorView"
|
|
807
714
|
];
|
|
715
|
+
var IOS_INTERACTABLE_TAGS = [
|
|
716
|
+
// Input elements
|
|
717
|
+
"XCUIElementTypeTextField",
|
|
718
|
+
"XCUIElementTypeSecureTextField",
|
|
719
|
+
"XCUIElementTypeTextView",
|
|
720
|
+
"XCUIElementTypeSearchField",
|
|
721
|
+
// Button-like elements
|
|
722
|
+
"XCUIElementTypeButton",
|
|
723
|
+
"XCUIElementTypeLink",
|
|
724
|
+
// Text elements (often tappable)
|
|
725
|
+
"XCUIElementTypeStaticText",
|
|
726
|
+
// Image elements
|
|
727
|
+
"XCUIElementTypeImage",
|
|
728
|
+
"XCUIElementTypeIcon",
|
|
729
|
+
// Selection elements
|
|
730
|
+
"XCUIElementTypeSwitch",
|
|
731
|
+
"XCUIElementTypeSlider",
|
|
732
|
+
"XCUIElementTypeStepper",
|
|
733
|
+
"XCUIElementTypeSegmentedControl",
|
|
734
|
+
"XCUIElementTypePicker",
|
|
735
|
+
"XCUIElementTypePickerWheel",
|
|
736
|
+
"XCUIElementTypeDatePicker",
|
|
737
|
+
"XCUIElementTypePageIndicator",
|
|
738
|
+
// Table/list items
|
|
739
|
+
"XCUIElementTypeCell",
|
|
740
|
+
"XCUIElementTypeMenuItem",
|
|
741
|
+
"XCUIElementTypeMenuBarItem",
|
|
742
|
+
// Toggle elements
|
|
743
|
+
"XCUIElementTypeCheckBox",
|
|
744
|
+
"XCUIElementTypeRadioButton",
|
|
745
|
+
"XCUIElementTypeToggle",
|
|
746
|
+
// Other interactive
|
|
747
|
+
"XCUIElementTypeKey",
|
|
748
|
+
"XCUIElementTypeKeyboard",
|
|
749
|
+
"XCUIElementTypeAlert",
|
|
750
|
+
"XCUIElementTypeSheet"
|
|
751
|
+
];
|
|
808
752
|
var IOS_LAYOUT_CONTAINERS = [
|
|
809
753
|
// Generic containers
|
|
810
754
|
"XCUIElementTypeOther",
|
|
@@ -837,6 +781,188 @@ var IOS_LAYOUT_CONTAINERS = [
|
|
|
837
781
|
// Application root
|
|
838
782
|
"XCUIElementTypeApplication"
|
|
839
783
|
];
|
|
784
|
+
|
|
785
|
+
// src/locators/xml-parsing.ts
|
|
786
|
+
import { DOMParser } from "@xmldom/xmldom";
|
|
787
|
+
import xpath from "xpath";
|
|
788
|
+
function childNodesOf(node) {
|
|
789
|
+
const children = [];
|
|
790
|
+
if (node.childNodes) {
|
|
791
|
+
for (let i = 0; i < node.childNodes.length; i++) {
|
|
792
|
+
const child = node.childNodes.item(i);
|
|
793
|
+
if (child?.nodeType === 1) {
|
|
794
|
+
children.push(child);
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
return children;
|
|
799
|
+
}
|
|
800
|
+
function translateRecursively(domNode, parentPath = "", index = null) {
|
|
801
|
+
const attributes = {};
|
|
802
|
+
const element = domNode;
|
|
803
|
+
if (element.attributes) {
|
|
804
|
+
for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) {
|
|
805
|
+
const attr = element.attributes.item(attrIdx);
|
|
806
|
+
if (attr) {
|
|
807
|
+
attributes[attr.name] = attr.value.replace(/(\n)/gm, "\\n");
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
const path = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
|
|
812
|
+
return {
|
|
813
|
+
children: childNodesOf(domNode).map(
|
|
814
|
+
(childNode, childIndex) => translateRecursively(childNode, path, childIndex)
|
|
815
|
+
),
|
|
816
|
+
tagName: domNode.nodeName,
|
|
817
|
+
attributes,
|
|
818
|
+
path
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
function isSameElement(node1, node2) {
|
|
822
|
+
if (node1.nodeType !== 1 || node2.nodeType !== 1) return false;
|
|
823
|
+
const el1 = node1;
|
|
824
|
+
const el2 = node2;
|
|
825
|
+
if (el1.nodeName !== el2.nodeName) return false;
|
|
826
|
+
const bounds1 = el1.getAttribute("bounds");
|
|
827
|
+
const bounds2 = el2.getAttribute("bounds");
|
|
828
|
+
if (bounds1 && bounds2) {
|
|
829
|
+
return bounds1 === bounds2;
|
|
830
|
+
}
|
|
831
|
+
const x1 = el1.getAttribute("x");
|
|
832
|
+
const y1 = el1.getAttribute("y");
|
|
833
|
+
const x2 = el2.getAttribute("x");
|
|
834
|
+
const y2 = el2.getAttribute("y");
|
|
835
|
+
if (x1 && y1 && x2 && y2) {
|
|
836
|
+
return x1 === x2 && y1 === y2 && el1.getAttribute("width") === el2.getAttribute("width") && el1.getAttribute("height") === el2.getAttribute("height");
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
function xmlToJSON(sourceXML) {
|
|
841
|
+
try {
|
|
842
|
+
const parser = new DOMParser();
|
|
843
|
+
const sourceDoc = parser.parseFromString(sourceXML, "text/xml");
|
|
844
|
+
const parseErrors = sourceDoc.getElementsByTagName("parsererror");
|
|
845
|
+
if (parseErrors.length > 0) {
|
|
846
|
+
console.error("[xmlToJSON] XML parsing error:", parseErrors[0].textContent);
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
const children = childNodesOf(sourceDoc);
|
|
850
|
+
const firstChild = children[0] || (sourceDoc.documentElement ? childNodesOf(sourceDoc.documentElement)[0] : null);
|
|
851
|
+
return firstChild ? translateRecursively(firstChild) : { children: [], tagName: "", attributes: {}, path: "" };
|
|
852
|
+
} catch (e) {
|
|
853
|
+
console.error("[xmlToJSON] Failed to parse XML:", e);
|
|
854
|
+
return null;
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
function xmlToDOM(sourceXML) {
|
|
858
|
+
try {
|
|
859
|
+
const parser = new DOMParser();
|
|
860
|
+
const doc = parser.parseFromString(sourceXML, "text/xml");
|
|
861
|
+
const parseErrors = doc.getElementsByTagName("parsererror");
|
|
862
|
+
if (parseErrors.length > 0) {
|
|
863
|
+
console.error("[xmlToDOM] XML parsing error:", parseErrors[0].textContent);
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
return doc;
|
|
867
|
+
} catch (e) {
|
|
868
|
+
console.error("[xmlToDOM] Failed to parse XML:", e);
|
|
869
|
+
return null;
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
function evaluateXPath(doc, xpathExpr) {
|
|
873
|
+
try {
|
|
874
|
+
const nodes = xpath.select(xpathExpr, doc);
|
|
875
|
+
if (Array.isArray(nodes)) {
|
|
876
|
+
return nodes;
|
|
877
|
+
}
|
|
878
|
+
return [];
|
|
879
|
+
} catch (e) {
|
|
880
|
+
console.error(`[evaluateXPath] Failed to evaluate "${xpathExpr}":`, e);
|
|
881
|
+
return [];
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
function checkXPathUniqueness(doc, xpathExpr, targetNode) {
|
|
885
|
+
try {
|
|
886
|
+
const nodes = evaluateXPath(doc, xpathExpr);
|
|
887
|
+
const totalMatches = nodes.length;
|
|
888
|
+
if (totalMatches === 0) {
|
|
889
|
+
return { isUnique: false };
|
|
890
|
+
}
|
|
891
|
+
if (totalMatches === 1) {
|
|
892
|
+
return { isUnique: true };
|
|
893
|
+
}
|
|
894
|
+
if (targetNode) {
|
|
895
|
+
for (let i = 0; i < nodes.length; i++) {
|
|
896
|
+
if (nodes[i].isSameNode(targetNode) || isSameElement(nodes[i], targetNode)) {
|
|
897
|
+
return {
|
|
898
|
+
isUnique: false,
|
|
899
|
+
index: i + 1,
|
|
900
|
+
// 1-based index for XPath
|
|
901
|
+
totalMatches
|
|
902
|
+
};
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
return { isUnique: false, totalMatches };
|
|
907
|
+
} catch (e) {
|
|
908
|
+
console.error(`[checkXPathUniqueness] Error checking "${xpathExpr}":`, e);
|
|
909
|
+
return { isUnique: false };
|
|
910
|
+
}
|
|
911
|
+
}
|
|
912
|
+
function findDOMNodeByPath(doc, path) {
|
|
913
|
+
if (!path) return doc.documentElement;
|
|
914
|
+
const indices = path.split(".").map(Number);
|
|
915
|
+
let current = doc.documentElement;
|
|
916
|
+
for (const index of indices) {
|
|
917
|
+
if (!current) return null;
|
|
918
|
+
const children = [];
|
|
919
|
+
if (current.childNodes) {
|
|
920
|
+
for (let i = 0; i < current.childNodes.length; i++) {
|
|
921
|
+
const child = current.childNodes.item(i);
|
|
922
|
+
if (child?.nodeType === 1) {
|
|
923
|
+
children.push(child);
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
current = children[index] || null;
|
|
928
|
+
}
|
|
929
|
+
return current;
|
|
930
|
+
}
|
|
931
|
+
function parseAndroidBounds(bounds) {
|
|
932
|
+
const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
933
|
+
if (!match) {
|
|
934
|
+
return { x: 0, y: 0, width: 0, height: 0 };
|
|
935
|
+
}
|
|
936
|
+
const x1 = parseInt(match[1], 10);
|
|
937
|
+
const y1 = parseInt(match[2], 10);
|
|
938
|
+
const x2 = parseInt(match[3], 10);
|
|
939
|
+
const y2 = parseInt(match[4], 10);
|
|
940
|
+
return {
|
|
941
|
+
x: x1,
|
|
942
|
+
y: y1,
|
|
943
|
+
width: x2 - x1,
|
|
944
|
+
height: y2 - y1
|
|
945
|
+
};
|
|
946
|
+
}
|
|
947
|
+
function parseIOSBounds(attributes) {
|
|
948
|
+
return {
|
|
949
|
+
x: parseInt(attributes.x || "0", 10),
|
|
950
|
+
y: parseInt(attributes.y || "0", 10),
|
|
951
|
+
width: parseInt(attributes.width || "0", 10),
|
|
952
|
+
height: parseInt(attributes.height || "0", 10)
|
|
953
|
+
};
|
|
954
|
+
}
|
|
955
|
+
function countAttributeOccurrences(sourceXML, attribute, value) {
|
|
956
|
+
const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
957
|
+
const pattern = new RegExp(`${attribute}=["']${escapedValue}["']`, "g");
|
|
958
|
+
const matches = sourceXML.match(pattern);
|
|
959
|
+
return matches ? matches.length : 0;
|
|
960
|
+
}
|
|
961
|
+
function isAttributeUnique(sourceXML, attribute, value) {
|
|
962
|
+
return countAttributeOccurrences(sourceXML, attribute, value) === 1;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
// src/locators/element-filter.ts
|
|
840
966
|
function matchesTagList(tagName, tagList) {
|
|
841
967
|
if (tagList.includes(tagName)) {
|
|
842
968
|
return true;
|
|
@@ -917,7 +1043,6 @@ function shouldIncludeElement(element, filters, isNative, automationName) {
|
|
|
917
1043
|
const {
|
|
918
1044
|
includeTagNames = [],
|
|
919
1045
|
excludeTagNames = ["hierarchy"],
|
|
920
|
-
// Always exclude root hierarchy node
|
|
921
1046
|
requireAttributes = [],
|
|
922
1047
|
minAttributeCount = 0,
|
|
923
1048
|
fetchableOnly = false,
|
|
@@ -964,38 +1089,144 @@ function isValidValue(value) {
|
|
|
964
1089
|
function escapeText(text) {
|
|
965
1090
|
return text.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
966
1091
|
}
|
|
967
|
-
function
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
if (
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
1092
|
+
function escapeXPathValue(value) {
|
|
1093
|
+
if (!value.includes("'")) {
|
|
1094
|
+
return `'${value}'`;
|
|
1095
|
+
}
|
|
1096
|
+
if (!value.includes('"')) {
|
|
1097
|
+
return `"${value}"`;
|
|
1098
|
+
}
|
|
1099
|
+
const parts = [];
|
|
1100
|
+
let current = "";
|
|
1101
|
+
for (const char of value) {
|
|
1102
|
+
if (char === "'") {
|
|
1103
|
+
if (current) parts.push(`'${current}'`);
|
|
1104
|
+
parts.push(`"'"`);
|
|
1105
|
+
current = "";
|
|
1106
|
+
} else {
|
|
1107
|
+
current += char;
|
|
975
1108
|
}
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1109
|
+
}
|
|
1110
|
+
if (current) parts.push(`'${current}'`);
|
|
1111
|
+
return `concat(${parts.join(",")})`;
|
|
1112
|
+
}
|
|
1113
|
+
function generateIndexedXPath(baseXPath, index) {
|
|
1114
|
+
return `(${baseXPath})[${index}]`;
|
|
1115
|
+
}
|
|
1116
|
+
function generateIndexedUiAutomator(baseSelector, index) {
|
|
1117
|
+
return `${baseSelector}.instance(${index - 1})`;
|
|
1118
|
+
}
|
|
1119
|
+
function checkUniqueness(ctx, xpath2, targetNode) {
|
|
1120
|
+
if (ctx.parsedDOM) {
|
|
1121
|
+
return checkXPathUniqueness(ctx.parsedDOM, xpath2, targetNode);
|
|
1122
|
+
}
|
|
1123
|
+
const match = xpath2.match(/\/\/\*\[@([^=]+)="([^"]+)"\]/);
|
|
1124
|
+
if (match) {
|
|
1125
|
+
const [, attr, value] = match;
|
|
1126
|
+
return { isUnique: isAttributeUnique(ctx.sourceXML, attr, value) };
|
|
1127
|
+
}
|
|
1128
|
+
return { isUnique: false };
|
|
1129
|
+
}
|
|
1130
|
+
function getSiblingIndex(element) {
|
|
1131
|
+
const parent = element.parentNode;
|
|
1132
|
+
if (!parent) return 1;
|
|
1133
|
+
const tagName = element.nodeName;
|
|
1134
|
+
let index = 0;
|
|
1135
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
1136
|
+
const child = parent.childNodes.item(i);
|
|
1137
|
+
if (child?.nodeType === 1 && child.nodeName === tagName) {
|
|
1138
|
+
index++;
|
|
1139
|
+
if (child === element) return index;
|
|
1140
|
+
}
|
|
1141
|
+
}
|
|
1142
|
+
return 1;
|
|
1143
|
+
}
|
|
1144
|
+
function countSiblings(element) {
|
|
1145
|
+
const parent = element.parentNode;
|
|
1146
|
+
if (!parent) return 1;
|
|
1147
|
+
const tagName = element.nodeName;
|
|
1148
|
+
let count = 0;
|
|
1149
|
+
for (let i = 0; i < parent.childNodes.length; i++) {
|
|
1150
|
+
const child = parent.childNodes.item(i);
|
|
1151
|
+
if (child?.nodeType === 1 && child.nodeName === tagName) {
|
|
1152
|
+
count++;
|
|
1153
|
+
}
|
|
1154
|
+
}
|
|
1155
|
+
return count;
|
|
1156
|
+
}
|
|
1157
|
+
function findUniqueAttribute(element, ctx) {
|
|
1158
|
+
const attrs = ctx.isAndroid ? ["resource-id", "content-desc", "text"] : ["name", "label", "value"];
|
|
1159
|
+
for (const attr of attrs) {
|
|
1160
|
+
const value = element.getAttribute(attr);
|
|
1161
|
+
if (value && value.trim()) {
|
|
1162
|
+
const xpath2 = `//*[@${attr}=${escapeXPathValue(value)}]`;
|
|
1163
|
+
const result = ctx.parsedDOM ? checkXPathUniqueness(ctx.parsedDOM, xpath2) : { isUnique: isAttributeUnique(ctx.sourceXML, attr, value) };
|
|
1164
|
+
if (result.isUnique) {
|
|
1165
|
+
return `@${attr}=${escapeXPathValue(value)}`;
|
|
1166
|
+
}
|
|
979
1167
|
}
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
1168
|
+
}
|
|
1169
|
+
return null;
|
|
1170
|
+
}
|
|
1171
|
+
function buildHierarchicalXPath(ctx, element, maxDepth = 3) {
|
|
1172
|
+
if (!ctx.parsedDOM) return null;
|
|
1173
|
+
const pathParts = [];
|
|
1174
|
+
let current = element;
|
|
1175
|
+
let depth = 0;
|
|
1176
|
+
while (current && depth < maxDepth) {
|
|
1177
|
+
const tagName = current.nodeName;
|
|
1178
|
+
const uniqueAttr = findUniqueAttribute(current, ctx);
|
|
1179
|
+
if (uniqueAttr) {
|
|
1180
|
+
pathParts.unshift(`//${tagName}[${uniqueAttr}]`);
|
|
1181
|
+
break;
|
|
1182
|
+
} else {
|
|
1183
|
+
const siblingIndex = getSiblingIndex(current);
|
|
1184
|
+
const siblingCount = countSiblings(current);
|
|
1185
|
+
if (siblingCount > 1) {
|
|
1186
|
+
pathParts.unshift(`${tagName}[${siblingIndex}]`);
|
|
1187
|
+
} else {
|
|
1188
|
+
pathParts.unshift(tagName);
|
|
1189
|
+
}
|
|
983
1190
|
}
|
|
1191
|
+
const parent = current.parentNode;
|
|
1192
|
+
current = parent && parent.nodeType === 1 ? parent : null;
|
|
1193
|
+
depth++;
|
|
1194
|
+
}
|
|
1195
|
+
if (pathParts.length === 0) return null;
|
|
1196
|
+
let result = pathParts[0];
|
|
1197
|
+
for (let i = 1; i < pathParts.length; i++) {
|
|
1198
|
+
result += "/" + pathParts[i];
|
|
1199
|
+
}
|
|
1200
|
+
if (!result.startsWith("//")) {
|
|
1201
|
+
result = "//" + result;
|
|
1202
|
+
}
|
|
1203
|
+
return result;
|
|
1204
|
+
}
|
|
1205
|
+
function addXPathLocator(results, xpath2, ctx, targetNode) {
|
|
1206
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1207
|
+
if (uniqueness.isUnique) {
|
|
1208
|
+
results.push(["xpath", xpath2]);
|
|
1209
|
+
} else if (uniqueness.index) {
|
|
1210
|
+
results.push(["xpath", generateIndexedXPath(xpath2, uniqueness.index)]);
|
|
984
1211
|
} else {
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
if (isValidValue(label) && label !== name && isAttributeUnique(sourceXML, "label", label)) {
|
|
991
|
-
results.push(["predicate-string", `-ios predicate string:label == "${escapeText(label)}"`]);
|
|
992
|
-
}
|
|
993
|
-
const value = attrs.value;
|
|
994
|
-
if (isValidValue(value) && isAttributeUnique(sourceXML, "value", value)) {
|
|
995
|
-
results.push(["predicate-string", `-ios predicate string:value == "${escapeText(value)}"`]);
|
|
1212
|
+
if (targetNode && ctx.parsedDOM) {
|
|
1213
|
+
const hierarchical = buildHierarchicalXPath(ctx, targetNode);
|
|
1214
|
+
if (hierarchical) {
|
|
1215
|
+
results.push(["xpath", hierarchical]);
|
|
1216
|
+
}
|
|
996
1217
|
}
|
|
1218
|
+
results.push(["xpath", xpath2]);
|
|
997
1219
|
}
|
|
998
|
-
|
|
1220
|
+
}
|
|
1221
|
+
function isInUiAutomatorScope(element, doc) {
|
|
1222
|
+
if (!doc) return true;
|
|
1223
|
+
const hierarchyNodes = evaluateXPath(doc, "/hierarchy/*");
|
|
1224
|
+
if (hierarchyNodes.length === 0) return true;
|
|
1225
|
+
const lastIndex = hierarchyNodes.length;
|
|
1226
|
+
const pathParts = element.path.split(".");
|
|
1227
|
+
if (pathParts.length === 0 || pathParts[0] === "") return true;
|
|
1228
|
+
const firstIndex = parseInt(pathParts[0], 10);
|
|
1229
|
+
return firstIndex === lastIndex - 1;
|
|
999
1230
|
}
|
|
1000
1231
|
function buildUiAutomatorSelector(element) {
|
|
1001
1232
|
const attrs = element.attributes;
|
|
@@ -1078,19 +1309,86 @@ function buildXPath(element, sourceXML, isAndroid) {
|
|
|
1078
1309
|
}
|
|
1079
1310
|
return `//${tagName}[${conditions.join(" and ")}]`;
|
|
1080
1311
|
}
|
|
1081
|
-
function
|
|
1312
|
+
function getSimpleSuggestedLocators(element, ctx, automationName, targetNode) {
|
|
1313
|
+
const results = [];
|
|
1314
|
+
const isAndroid = automationName.toLowerCase().includes("uiautomator");
|
|
1315
|
+
const attrs = element.attributes;
|
|
1316
|
+
const inUiAutomatorScope = isAndroid ? isInUiAutomatorScope(element, ctx.parsedDOM) : true;
|
|
1317
|
+
if (isAndroid) {
|
|
1318
|
+
const resourceId = attrs["resource-id"];
|
|
1319
|
+
if (isValidValue(resourceId)) {
|
|
1320
|
+
const xpath2 = `//*[@resource-id="${resourceId}"]`;
|
|
1321
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1322
|
+
if (uniqueness.isUnique && inUiAutomatorScope) {
|
|
1323
|
+
results.push(["id", `android=new UiSelector().resourceId("${resourceId}")`]);
|
|
1324
|
+
} else if (uniqueness.index && inUiAutomatorScope) {
|
|
1325
|
+
const base = `android=new UiSelector().resourceId("${resourceId}")`;
|
|
1326
|
+
results.push(["id", generateIndexedUiAutomator(base, uniqueness.index)]);
|
|
1327
|
+
}
|
|
1328
|
+
}
|
|
1329
|
+
const contentDesc = attrs["content-desc"];
|
|
1330
|
+
if (isValidValue(contentDesc)) {
|
|
1331
|
+
const xpath2 = `//*[@content-desc="${contentDesc}"]`;
|
|
1332
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1333
|
+
if (uniqueness.isUnique) {
|
|
1334
|
+
results.push(["accessibility-id", `~${contentDesc}`]);
|
|
1335
|
+
}
|
|
1336
|
+
}
|
|
1337
|
+
const text = attrs.text;
|
|
1338
|
+
if (isValidValue(text) && text.length < 100) {
|
|
1339
|
+
const xpath2 = `//*[@text="${escapeText(text)}"]`;
|
|
1340
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1341
|
+
if (uniqueness.isUnique && inUiAutomatorScope) {
|
|
1342
|
+
results.push(["text", `android=new UiSelector().text("${escapeText(text)}")`]);
|
|
1343
|
+
} else if (uniqueness.index && inUiAutomatorScope) {
|
|
1344
|
+
const base = `android=new UiSelector().text("${escapeText(text)}")`;
|
|
1345
|
+
results.push(["text", generateIndexedUiAutomator(base, uniqueness.index)]);
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
} else {
|
|
1349
|
+
const name = attrs.name;
|
|
1350
|
+
if (isValidValue(name)) {
|
|
1351
|
+
const xpath2 = `//*[@name="${name}"]`;
|
|
1352
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1353
|
+
if (uniqueness.isUnique) {
|
|
1354
|
+
results.push(["accessibility-id", `~${name}`]);
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
const label = attrs.label;
|
|
1358
|
+
if (isValidValue(label) && label !== attrs.name) {
|
|
1359
|
+
const xpath2 = `//*[@label="${escapeText(label)}"]`;
|
|
1360
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1361
|
+
if (uniqueness.isUnique) {
|
|
1362
|
+
results.push(["predicate-string", `-ios predicate string:label == "${escapeText(label)}"`]);
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
const value = attrs.value;
|
|
1366
|
+
if (isValidValue(value)) {
|
|
1367
|
+
const xpath2 = `//*[@value="${escapeText(value)}"]`;
|
|
1368
|
+
const uniqueness = checkUniqueness(ctx, xpath2, targetNode);
|
|
1369
|
+
if (uniqueness.isUnique) {
|
|
1370
|
+
results.push(["predicate-string", `-ios predicate string:value == "${escapeText(value)}"`]);
|
|
1371
|
+
}
|
|
1372
|
+
}
|
|
1373
|
+
}
|
|
1374
|
+
return results;
|
|
1375
|
+
}
|
|
1376
|
+
function getComplexSuggestedLocators(element, ctx, automationName, targetNode) {
|
|
1082
1377
|
const results = [];
|
|
1083
1378
|
const isAndroid = automationName.toLowerCase().includes("uiautomator");
|
|
1379
|
+
const inUiAutomatorScope = isAndroid ? isInUiAutomatorScope(element, ctx.parsedDOM) : true;
|
|
1084
1380
|
if (isAndroid) {
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1381
|
+
if (inUiAutomatorScope) {
|
|
1382
|
+
const uiAutomator = buildUiAutomatorSelector(element);
|
|
1383
|
+
if (uiAutomator) {
|
|
1384
|
+
results.push(["uiautomator", uiAutomator]);
|
|
1385
|
+
}
|
|
1088
1386
|
}
|
|
1089
|
-
const
|
|
1090
|
-
if (
|
|
1091
|
-
results
|
|
1387
|
+
const xpath2 = buildXPath(element, ctx.sourceXML, true);
|
|
1388
|
+
if (xpath2) {
|
|
1389
|
+
addXPathLocator(results, xpath2, ctx, targetNode);
|
|
1092
1390
|
}
|
|
1093
|
-
if (isValidValue(element.attributes.class)) {
|
|
1391
|
+
if (inUiAutomatorScope && isValidValue(element.attributes.class)) {
|
|
1094
1392
|
results.push([
|
|
1095
1393
|
"class-name",
|
|
1096
1394
|
`android=new UiSelector().className("${element.attributes.class}")`
|
|
@@ -1105,9 +1403,9 @@ function getComplexSuggestedLocators(element, sourceXML, isNative, automationNam
|
|
|
1105
1403
|
if (classChain) {
|
|
1106
1404
|
results.push(["class-chain", classChain]);
|
|
1107
1405
|
}
|
|
1108
|
-
const
|
|
1109
|
-
if (
|
|
1110
|
-
results
|
|
1406
|
+
const xpath2 = buildXPath(element, ctx.sourceXML, false);
|
|
1407
|
+
if (xpath2) {
|
|
1408
|
+
addXPathLocator(results, xpath2, ctx, targetNode);
|
|
1111
1409
|
}
|
|
1112
1410
|
const type = element.tagName;
|
|
1113
1411
|
if (type.startsWith("XCUIElementType")) {
|
|
@@ -1116,9 +1414,14 @@ function getComplexSuggestedLocators(element, sourceXML, isNative, automationNam
|
|
|
1116
1414
|
}
|
|
1117
1415
|
return results;
|
|
1118
1416
|
}
|
|
1119
|
-
function getSuggestedLocators(element, sourceXML,
|
|
1120
|
-
const
|
|
1121
|
-
|
|
1417
|
+
function getSuggestedLocators(element, sourceXML, automationName, ctx, targetNode) {
|
|
1418
|
+
const locatorCtx = ctx ?? {
|
|
1419
|
+
sourceXML,
|
|
1420
|
+
parsedDOM: null,
|
|
1421
|
+
isAndroid: automationName.toLowerCase().includes("uiautomator")
|
|
1422
|
+
};
|
|
1423
|
+
const simpleLocators = getSimpleSuggestedLocators(element, locatorCtx, automationName, targetNode);
|
|
1424
|
+
const complexLocators = getComplexSuggestedLocators(element, locatorCtx, automationName, targetNode);
|
|
1122
1425
|
const seen = /* @__PURE__ */ new Set();
|
|
1123
1426
|
const results = [];
|
|
1124
1427
|
for (const locator of [...simpleLocators, ...complexLocators]) {
|
|
@@ -1139,7 +1442,7 @@ function locatorsToObject(locators) {
|
|
|
1139
1442
|
return result;
|
|
1140
1443
|
}
|
|
1141
1444
|
|
|
1142
|
-
// src/locators/
|
|
1445
|
+
// src/locators/index.ts
|
|
1143
1446
|
function parseBounds(element, platform) {
|
|
1144
1447
|
return platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
|
|
1145
1448
|
}
|
|
@@ -1175,7 +1478,14 @@ function shouldProcess(element, ctx) {
|
|
|
1175
1478
|
function processElement(element, ctx) {
|
|
1176
1479
|
if (!shouldProcess(element, ctx)) return;
|
|
1177
1480
|
try {
|
|
1178
|
-
const
|
|
1481
|
+
const targetNode = ctx.parsedDOM ? findDOMNodeByPath(ctx.parsedDOM, element.path) : void 0;
|
|
1482
|
+
const locators = getSuggestedLocators(
|
|
1483
|
+
element,
|
|
1484
|
+
ctx.sourceXML,
|
|
1485
|
+
ctx.automationName,
|
|
1486
|
+
{ sourceXML: ctx.sourceXML, parsedDOM: ctx.parsedDOM, isAndroid: ctx.platform === "android" },
|
|
1487
|
+
targetNode || void 0
|
|
1488
|
+
);
|
|
1179
1489
|
if (locators.length === 0) return;
|
|
1180
1490
|
const transformed = transformElement(element, locators, ctx);
|
|
1181
1491
|
if (Object.keys(transformed.locators).length === 0) return;
|
|
@@ -1197,6 +1507,7 @@ function generateAllElementLocators(sourceXML, options) {
|
|
|
1197
1507
|
console.error("[generateAllElementLocators] Failed to parse page source XML");
|
|
1198
1508
|
return [];
|
|
1199
1509
|
}
|
|
1510
|
+
const parsedDOM = xmlToDOM(sourceXML);
|
|
1200
1511
|
const ctx = {
|
|
1201
1512
|
sourceXML,
|
|
1202
1513
|
platform: options.platform,
|
|
@@ -1204,7 +1515,8 @@ function generateAllElementLocators(sourceXML, options) {
|
|
|
1204
1515
|
isNative: options.isNative ?? true,
|
|
1205
1516
|
viewportSize: options.viewportSize ?? { width: 9999, height: 9999 },
|
|
1206
1517
|
filters: options.filters ?? {},
|
|
1207
|
-
results: []
|
|
1518
|
+
results: [],
|
|
1519
|
+
parsedDOM
|
|
1208
1520
|
};
|
|
1209
1521
|
traverseTree(sourceJSON, ctx);
|
|
1210
1522
|
return ctx.results;
|
|
@@ -1246,27 +1558,18 @@ function selectBestLocators(locators) {
|
|
|
1246
1558
|
}
|
|
1247
1559
|
function toMobileElementInfo(element, includeBounds) {
|
|
1248
1560
|
const selectedLocators = selectBestLocators(element.locators);
|
|
1561
|
+
const accessId = element.accessibilityId || element.contentDesc;
|
|
1249
1562
|
const info = {
|
|
1250
1563
|
selector: selectedLocators[0] || "",
|
|
1251
1564
|
tagName: element.tagName,
|
|
1252
|
-
isInViewport: element.isInViewport
|
|
1565
|
+
isInViewport: element.isInViewport,
|
|
1566
|
+
text: element.text || "",
|
|
1567
|
+
resourceId: element.resourceId || "",
|
|
1568
|
+
accessibilityId: accessId || "",
|
|
1569
|
+
isEnabled: element.enabled !== false,
|
|
1570
|
+
altSelector: selectedLocators[1] || ""
|
|
1571
|
+
// Single alternative (flattened for tabular)
|
|
1253
1572
|
};
|
|
1254
|
-
if (element.text) {
|
|
1255
|
-
info.text = element.text;
|
|
1256
|
-
}
|
|
1257
|
-
if (element.resourceId) {
|
|
1258
|
-
info.resourceId = element.resourceId;
|
|
1259
|
-
}
|
|
1260
|
-
const accessId = element.accessibilityId || element.contentDesc;
|
|
1261
|
-
if (accessId) {
|
|
1262
|
-
info.accessibilityId = accessId;
|
|
1263
|
-
}
|
|
1264
|
-
if (!element.enabled) {
|
|
1265
|
-
info.isEnabled = false;
|
|
1266
|
-
}
|
|
1267
|
-
if (selectedLocators.length > 1) {
|
|
1268
|
-
info.alternativeSelectors = selectedLocators.slice(1);
|
|
1269
|
-
}
|
|
1270
1573
|
if (includeBounds) {
|
|
1271
1574
|
info.bounds = element.bounds;
|
|
1272
1575
|
}
|
|
@@ -1299,18 +1602,6 @@ async function getMobileVisibleElements(browser, platform, options = {}) {
|
|
|
1299
1602
|
// src/tools/get-visible-elements.tool.ts
|
|
1300
1603
|
import { encode } from "@toon-format/toon";
|
|
1301
1604
|
import { z as z7 } from "zod";
|
|
1302
|
-
|
|
1303
|
-
// src/utils/strip-undefined.ts
|
|
1304
|
-
function stripUndefined(obj) {
|
|
1305
|
-
return Object.fromEntries(
|
|
1306
|
-
Object.entries(obj).filter(([_, v]) => v !== void 0 && v !== null && v !== "")
|
|
1307
|
-
);
|
|
1308
|
-
}
|
|
1309
|
-
function stripUndefinedFromArray(arr) {
|
|
1310
|
-
return arr.map(stripUndefined);
|
|
1311
|
-
}
|
|
1312
|
-
|
|
1313
|
-
// src/tools/get-visible-elements.tool.ts
|
|
1314
1605
|
var getVisibleElementsToolDefinition = {
|
|
1315
1606
|
name: "get_visible_elements",
|
|
1316
1607
|
description: 'get a list of visible (in viewport & displayed) interactable elements on the page (buttons, links, inputs). Use elementType="visual" for images/SVGs. Must prefer this to take_screenshot for interactions',
|
|
@@ -1347,8 +1638,7 @@ var getVisibleElementsTool = async (args) => {
|
|
|
1347
1638
|
const platform = browser.isAndroid ? "android" : "ios";
|
|
1348
1639
|
elements = await getMobileVisibleElements(browser, platform, { includeContainers, includeBounds });
|
|
1349
1640
|
} else {
|
|
1350
|
-
|
|
1351
|
-
elements = stripUndefinedFromArray(raw);
|
|
1641
|
+
elements = await getBrowserInteractableElements(browser, { elementType });
|
|
1352
1642
|
}
|
|
1353
1643
|
if (inViewportOnly) {
|
|
1354
1644
|
elements = elements.filter((el) => el.isInViewport !== false);
|
|
@@ -1366,8 +1656,9 @@ var getVisibleElementsTool = async (args) => {
|
|
|
1366
1656
|
hasMore: offset + elements.length < total,
|
|
1367
1657
|
elements
|
|
1368
1658
|
};
|
|
1659
|
+
const toon = encode(result).replace(/,""/g, ",").replace(/"",/g, ",");
|
|
1369
1660
|
return {
|
|
1370
|
-
content: [{ type: "text", text:
|
|
1661
|
+
content: [{ type: "text", text: toon }]
|
|
1371
1662
|
};
|
|
1372
1663
|
} catch (e) {
|
|
1373
1664
|
return {
|
|
@@ -1538,6 +1829,62 @@ var deleteCookiesTool = async ({ name }) => {
|
|
|
1538
1829
|
}
|
|
1539
1830
|
};
|
|
1540
1831
|
|
|
1832
|
+
// src/scripts/get-browser-accessibility-tree.ts
|
|
1833
|
+
function flattenAccessibilityTree(node, result = []) {
|
|
1834
|
+
if (!node) return result;
|
|
1835
|
+
if (node.role !== "WebArea" || node.name) {
|
|
1836
|
+
const entry = {
|
|
1837
|
+
role: node.role || "",
|
|
1838
|
+
name: node.name || "",
|
|
1839
|
+
value: node.value ?? "",
|
|
1840
|
+
description: node.description || "",
|
|
1841
|
+
disabled: node.disabled ? "true" : "",
|
|
1842
|
+
focused: node.focused ? "true" : "",
|
|
1843
|
+
selected: node.selected ? "true" : "",
|
|
1844
|
+
checked: node.checked === true ? "true" : node.checked === false ? "false" : node.checked === "mixed" ? "mixed" : "",
|
|
1845
|
+
expanded: node.expanded === true ? "true" : node.expanded === false ? "false" : "",
|
|
1846
|
+
pressed: node.pressed === true ? "true" : node.pressed === false ? "false" : node.pressed === "mixed" ? "mixed" : "",
|
|
1847
|
+
readonly: node.readonly ? "true" : "",
|
|
1848
|
+
required: node.required ? "true" : "",
|
|
1849
|
+
level: node.level ?? "",
|
|
1850
|
+
valuemin: node.valuemin ?? "",
|
|
1851
|
+
valuemax: node.valuemax ?? "",
|
|
1852
|
+
autocomplete: node.autocomplete || "",
|
|
1853
|
+
haspopup: node.haspopup || "",
|
|
1854
|
+
invalid: node.invalid ? "true" : "",
|
|
1855
|
+
modal: node.modal ? "true" : "",
|
|
1856
|
+
multiline: node.multiline ? "true" : "",
|
|
1857
|
+
multiselectable: node.multiselectable ? "true" : "",
|
|
1858
|
+
orientation: node.orientation || "",
|
|
1859
|
+
keyshortcuts: node.keyshortcuts || "",
|
|
1860
|
+
roledescription: node.roledescription || "",
|
|
1861
|
+
valuetext: node.valuetext || ""
|
|
1862
|
+
};
|
|
1863
|
+
result.push(entry);
|
|
1864
|
+
}
|
|
1865
|
+
if (node.children && Array.isArray(node.children)) {
|
|
1866
|
+
for (const child of node.children) {
|
|
1867
|
+
flattenAccessibilityTree(child, result);
|
|
1868
|
+
}
|
|
1869
|
+
}
|
|
1870
|
+
return result;
|
|
1871
|
+
}
|
|
1872
|
+
async function getBrowserAccessibilityTree(browser) {
|
|
1873
|
+
const puppeteer = await browser.getPuppeteer();
|
|
1874
|
+
const pages = await puppeteer.pages();
|
|
1875
|
+
if (pages.length === 0) {
|
|
1876
|
+
return [];
|
|
1877
|
+
}
|
|
1878
|
+
const page = pages[0];
|
|
1879
|
+
const snapshot = await page.accessibility.snapshot({
|
|
1880
|
+
interestingOnly: true
|
|
1881
|
+
});
|
|
1882
|
+
if (!snapshot) {
|
|
1883
|
+
return [];
|
|
1884
|
+
}
|
|
1885
|
+
return flattenAccessibilityTree(snapshot);
|
|
1886
|
+
}
|
|
1887
|
+
|
|
1541
1888
|
// src/tools/get-accessibility-tree.tool.ts
|
|
1542
1889
|
import { encode as encode2 } from "@toon-format/toon";
|
|
1543
1890
|
import { z as z10 } from "zod";
|
|
@@ -1551,44 +1898,6 @@ var getAccessibilityToolDefinition = {
|
|
|
1551
1898
|
namedOnly: z10.boolean().optional().describe("Only return nodes with a name/label. Default: true. Filters out anonymous containers.")
|
|
1552
1899
|
}
|
|
1553
1900
|
};
|
|
1554
|
-
function flattenAccessibilityTree(node, result = []) {
|
|
1555
|
-
if (!node) return result;
|
|
1556
|
-
if (node.role !== "WebArea" || node.name) {
|
|
1557
|
-
const entry = {};
|
|
1558
|
-
if (node.role) entry.role = node.role;
|
|
1559
|
-
if (node.name) entry.name = node.name;
|
|
1560
|
-
if (node.value !== void 0 && node.value !== "") entry.value = node.value;
|
|
1561
|
-
if (node.description) entry.description = node.description;
|
|
1562
|
-
if (node.keyshortcuts) entry.keyshortcuts = node.keyshortcuts;
|
|
1563
|
-
if (node.roledescription) entry.roledescription = node.roledescription;
|
|
1564
|
-
if (node.valuetext) entry.valuetext = node.valuetext;
|
|
1565
|
-
if (node.disabled) entry.disabled = node.disabled;
|
|
1566
|
-
if (node.expanded !== void 0) entry.expanded = node.expanded;
|
|
1567
|
-
if (node.focused) entry.focused = node.focused;
|
|
1568
|
-
if (node.modal) entry.modal = node.modal;
|
|
1569
|
-
if (node.multiline) entry.multiline = node.multiline;
|
|
1570
|
-
if (node.multiselectable) entry.multiselectable = node.multiselectable;
|
|
1571
|
-
if (node.readonly) entry.readonly = node.readonly;
|
|
1572
|
-
if (node.required) entry.required = node.required;
|
|
1573
|
-
if (node.selected) entry.selected = node.selected;
|
|
1574
|
-
if (node.checked !== void 0) entry.checked = node.checked;
|
|
1575
|
-
if (node.pressed !== void 0) entry.pressed = node.pressed;
|
|
1576
|
-
if (node.level !== void 0) entry.level = node.level;
|
|
1577
|
-
if (node.valuemin !== void 0) entry.valuemin = node.valuemin;
|
|
1578
|
-
if (node.valuemax !== void 0) entry.valuemax = node.valuemax;
|
|
1579
|
-
if (node.autocomplete) entry.autocomplete = node.autocomplete;
|
|
1580
|
-
if (node.haspopup) entry.haspopup = node.haspopup;
|
|
1581
|
-
if (node.invalid) entry.invalid = node.invalid;
|
|
1582
|
-
if (node.orientation) entry.orientation = node.orientation;
|
|
1583
|
-
result.push(entry);
|
|
1584
|
-
}
|
|
1585
|
-
if (node.children && Array.isArray(node.children)) {
|
|
1586
|
-
for (const child of node.children) {
|
|
1587
|
-
flattenAccessibilityTree(child, result);
|
|
1588
|
-
}
|
|
1589
|
-
}
|
|
1590
|
-
return result;
|
|
1591
|
-
}
|
|
1592
1901
|
var getAccessibilityTreeTool = async (args) => {
|
|
1593
1902
|
try {
|
|
1594
1903
|
const browser = getBrowser();
|
|
@@ -1601,24 +1910,12 @@ var getAccessibilityTreeTool = async (args) => {
|
|
|
1601
1910
|
};
|
|
1602
1911
|
}
|
|
1603
1912
|
const { limit = 100, offset = 0, roles, namedOnly = true } = args || {};
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
if (pages.length === 0) {
|
|
1607
|
-
return {
|
|
1608
|
-
content: [{ type: "text", text: "No active pages found" }]
|
|
1609
|
-
};
|
|
1610
|
-
}
|
|
1611
|
-
const page = pages[0];
|
|
1612
|
-
const snapshot = await page.accessibility.snapshot({
|
|
1613
|
-
interestingOnly: true
|
|
1614
|
-
// Filter to only interesting/semantic nodes
|
|
1615
|
-
});
|
|
1616
|
-
if (!snapshot) {
|
|
1913
|
+
let nodes = await getBrowserAccessibilityTree(browser);
|
|
1914
|
+
if (nodes.length === 0) {
|
|
1617
1915
|
return {
|
|
1618
1916
|
content: [{ type: "text", text: "No accessibility tree available" }]
|
|
1619
1917
|
};
|
|
1620
1918
|
}
|
|
1621
|
-
let nodes = flattenAccessibilityTree(snapshot);
|
|
1622
1919
|
if (namedOnly) {
|
|
1623
1920
|
nodes = nodes.filter((n) => n.name && n.name.trim() !== "");
|
|
1624
1921
|
}
|
|
@@ -1639,8 +1936,9 @@ var getAccessibilityTreeTool = async (args) => {
|
|
|
1639
1936
|
hasMore: offset + nodes.length < total,
|
|
1640
1937
|
nodes
|
|
1641
1938
|
};
|
|
1939
|
+
const toon = encode2(result).replace(/,""/g, ",").replace(/"",/g, ",");
|
|
1642
1940
|
return {
|
|
1643
|
-
content: [{ type: "text", text:
|
|
1941
|
+
content: [{ type: "text", text: toon }]
|
|
1644
1942
|
};
|
|
1645
1943
|
} catch (e) {
|
|
1646
1944
|
return {
|
|
@@ -2049,11 +2347,21 @@ var package_default = {
|
|
|
2049
2347
|
type: "git",
|
|
2050
2348
|
url: "git://github.com/webdriverio/mcp.git"
|
|
2051
2349
|
},
|
|
2052
|
-
version: "1.
|
|
2350
|
+
version: "2.1.0",
|
|
2053
2351
|
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
2054
2352
|
main: "./lib/server.js",
|
|
2055
2353
|
module: "./lib/server.js",
|
|
2056
2354
|
types: "./lib/server.d.ts",
|
|
2355
|
+
exports: {
|
|
2356
|
+
".": {
|
|
2357
|
+
import: "./lib/server.js",
|
|
2358
|
+
types: "./lib/server.d.ts"
|
|
2359
|
+
},
|
|
2360
|
+
"./snapshot": {
|
|
2361
|
+
import: "./lib/snapshot.js",
|
|
2362
|
+
types: "./lib/snapshot.d.ts"
|
|
2363
|
+
}
|
|
2364
|
+
},
|
|
2057
2365
|
bin: {
|
|
2058
2366
|
"wdio-mcp": "lib/server.js"
|
|
2059
2367
|
},
|
|
@@ -2077,6 +2385,7 @@ var package_default = {
|
|
|
2077
2385
|
},
|
|
2078
2386
|
dependencies: {
|
|
2079
2387
|
"@modelcontextprotocol/sdk": "1.25",
|
|
2388
|
+
xpath: "^0.0.34",
|
|
2080
2389
|
"@toon-format/toon": "^2.1.0",
|
|
2081
2390
|
"@wdio/protocols": "^9.16.2",
|
|
2082
2391
|
"@xmldom/xmldom": "^0.8.11",
|
|
@@ -2114,7 +2423,7 @@ var server = new McpServer({
|
|
|
2114
2423
|
description: package_default.description,
|
|
2115
2424
|
websiteUrl: "https://github.com/webdriverio/mcp"
|
|
2116
2425
|
}, {
|
|
2117
|
-
instructions: "MCP server for browser and mobile app automation using WebDriverIO. Supports Chrome browser control
|
|
2426
|
+
instructions: "MCP server for browser and mobile app automation using WebDriverIO. Supports Chrome, Firefox, Edge, and Safari browser control plus iOS/Android native app testing via Appium.",
|
|
2118
2427
|
capabilities: {
|
|
2119
2428
|
tools: {}
|
|
2120
2429
|
}
|