@wdio/mcp 2.1.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 +564 -223
- 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
|
};
|
|
@@ -592,95 +634,12 @@ var elementsScript = (elementType = "interactable") => (function() {
|
|
|
592
634
|
}
|
|
593
635
|
return getElements();
|
|
594
636
|
})();
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
import { DOMParser } from "@xmldom/xmldom";
|
|
599
|
-
function childNodesOf(node) {
|
|
600
|
-
const children = [];
|
|
601
|
-
if (node.childNodes) {
|
|
602
|
-
for (let i = 0; i < node.childNodes.length; i++) {
|
|
603
|
-
const child = node.childNodes.item(i);
|
|
604
|
-
if (child?.nodeType === 1) {
|
|
605
|
-
children.push(child);
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
}
|
|
609
|
-
return children;
|
|
610
|
-
}
|
|
611
|
-
function translateRecursively(domNode, parentPath = "", index = null) {
|
|
612
|
-
const attributes = {};
|
|
613
|
-
const element = domNode;
|
|
614
|
-
if (element.attributes) {
|
|
615
|
-
for (let attrIdx = 0; attrIdx < element.attributes.length; attrIdx++) {
|
|
616
|
-
const attr = element.attributes.item(attrIdx);
|
|
617
|
-
if (attr) {
|
|
618
|
-
attributes[attr.name] = attr.value.replace(/(\n)/gm, "\\n");
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
}
|
|
622
|
-
const path = index === null ? "" : `${parentPath ? parentPath + "." : ""}${index}`;
|
|
623
|
-
return {
|
|
624
|
-
children: childNodesOf(domNode).map(
|
|
625
|
-
(childNode, childIndex) => translateRecursively(childNode, path, childIndex)
|
|
626
|
-
),
|
|
627
|
-
tagName: domNode.nodeName,
|
|
628
|
-
attributes,
|
|
629
|
-
path
|
|
630
|
-
};
|
|
631
|
-
}
|
|
632
|
-
function xmlToJSON(sourceXML) {
|
|
633
|
-
try {
|
|
634
|
-
const parser = new DOMParser();
|
|
635
|
-
const sourceDoc = parser.parseFromString(sourceXML, "text/xml");
|
|
636
|
-
const parseErrors = sourceDoc.getElementsByTagName("parsererror");
|
|
637
|
-
if (parseErrors.length > 0) {
|
|
638
|
-
console.error("[xmlToJSON] XML parsing error:", parseErrors[0].textContent);
|
|
639
|
-
return null;
|
|
640
|
-
}
|
|
641
|
-
const children = childNodesOf(sourceDoc);
|
|
642
|
-
const firstChild = children[0] || (sourceDoc.documentElement ? childNodesOf(sourceDoc.documentElement)[0] : null);
|
|
643
|
-
return firstChild ? translateRecursively(firstChild) : { children: [], tagName: "", attributes: {}, path: "" };
|
|
644
|
-
} catch (e) {
|
|
645
|
-
console.error("[xmlToJSON] Failed to parse XML:", e);
|
|
646
|
-
return null;
|
|
647
|
-
}
|
|
648
|
-
}
|
|
649
|
-
function parseAndroidBounds(bounds) {
|
|
650
|
-
const match = bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
|
|
651
|
-
if (!match) {
|
|
652
|
-
return { x: 0, y: 0, width: 0, height: 0 };
|
|
653
|
-
}
|
|
654
|
-
const x1 = parseInt(match[1], 10);
|
|
655
|
-
const y1 = parseInt(match[2], 10);
|
|
656
|
-
const x2 = parseInt(match[3], 10);
|
|
657
|
-
const y2 = parseInt(match[4], 10);
|
|
658
|
-
return {
|
|
659
|
-
x: x1,
|
|
660
|
-
y: y1,
|
|
661
|
-
width: x2 - x1,
|
|
662
|
-
height: y2 - y1
|
|
663
|
-
};
|
|
664
|
-
}
|
|
665
|
-
function parseIOSBounds(attributes) {
|
|
666
|
-
return {
|
|
667
|
-
x: parseInt(attributes.x || "0", 10),
|
|
668
|
-
y: parseInt(attributes.y || "0", 10),
|
|
669
|
-
width: parseInt(attributes.width || "0", 10),
|
|
670
|
-
height: parseInt(attributes.height || "0", 10)
|
|
671
|
-
};
|
|
672
|
-
}
|
|
673
|
-
function countAttributeOccurrences(sourceXML, attribute, value) {
|
|
674
|
-
const escapedValue = value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
675
|
-
const pattern = new RegExp(`${attribute}=["']${escapedValue}["']`, "g");
|
|
676
|
-
const matches = sourceXML.match(pattern);
|
|
677
|
-
return matches ? matches.length : 0;
|
|
678
|
-
}
|
|
679
|
-
function isAttributeUnique(sourceXML, attribute, value) {
|
|
680
|
-
return countAttributeOccurrences(sourceXML, attribute, value) === 1;
|
|
637
|
+
async function getBrowserInteractableElements(browser, options = {}) {
|
|
638
|
+
const { elementType = "interactable" } = options;
|
|
639
|
+
return browser.execute(elementsScript, elementType);
|
|
681
640
|
}
|
|
682
641
|
|
|
683
|
-
// src/locators/
|
|
642
|
+
// src/locators/constants.ts
|
|
684
643
|
var ANDROID_INTERACTABLE_TAGS = [
|
|
685
644
|
// Input elements
|
|
686
645
|
"android.widget.EditText",
|
|
@@ -715,43 +674,6 @@ var ANDROID_INTERACTABLE_TAGS = [
|
|
|
715
674
|
// List/grid items
|
|
716
675
|
"android.widget.AdapterView"
|
|
717
676
|
];
|
|
718
|
-
var IOS_INTERACTABLE_TAGS = [
|
|
719
|
-
// Input elements
|
|
720
|
-
"XCUIElementTypeTextField",
|
|
721
|
-
"XCUIElementTypeSecureTextField",
|
|
722
|
-
"XCUIElementTypeTextView",
|
|
723
|
-
"XCUIElementTypeSearchField",
|
|
724
|
-
// Button-like elements
|
|
725
|
-
"XCUIElementTypeButton",
|
|
726
|
-
"XCUIElementTypeLink",
|
|
727
|
-
// Text elements (often tappable)
|
|
728
|
-
"XCUIElementTypeStaticText",
|
|
729
|
-
// Image elements
|
|
730
|
-
"XCUIElementTypeImage",
|
|
731
|
-
"XCUIElementTypeIcon",
|
|
732
|
-
// Selection elements
|
|
733
|
-
"XCUIElementTypeSwitch",
|
|
734
|
-
"XCUIElementTypeSlider",
|
|
735
|
-
"XCUIElementTypeStepper",
|
|
736
|
-
"XCUIElementTypeSegmentedControl",
|
|
737
|
-
"XCUIElementTypePicker",
|
|
738
|
-
"XCUIElementTypePickerWheel",
|
|
739
|
-
"XCUIElementTypeDatePicker",
|
|
740
|
-
"XCUIElementTypePageIndicator",
|
|
741
|
-
// Table/list items
|
|
742
|
-
"XCUIElementTypeCell",
|
|
743
|
-
"XCUIElementTypeMenuItem",
|
|
744
|
-
"XCUIElementTypeMenuBarItem",
|
|
745
|
-
// Toggle elements
|
|
746
|
-
"XCUIElementTypeCheckBox",
|
|
747
|
-
"XCUIElementTypeRadioButton",
|
|
748
|
-
"XCUIElementTypeToggle",
|
|
749
|
-
// Other interactive
|
|
750
|
-
"XCUIElementTypeKey",
|
|
751
|
-
"XCUIElementTypeKeyboard",
|
|
752
|
-
"XCUIElementTypeAlert",
|
|
753
|
-
"XCUIElementTypeSheet"
|
|
754
|
-
];
|
|
755
677
|
var ANDROID_LAYOUT_CONTAINERS = [
|
|
756
678
|
// Core ViewGroup classes
|
|
757
679
|
"android.view.ViewGroup",
|
|
@@ -790,6 +712,43 @@ var ANDROID_LAYOUT_CONTAINERS = [
|
|
|
790
712
|
"com.android.internal.policy.DecorView",
|
|
791
713
|
"android.widget.DecorView"
|
|
792
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
|
+
];
|
|
793
752
|
var IOS_LAYOUT_CONTAINERS = [
|
|
794
753
|
// Generic containers
|
|
795
754
|
"XCUIElementTypeOther",
|
|
@@ -822,6 +781,188 @@ var IOS_LAYOUT_CONTAINERS = [
|
|
|
822
781
|
// Application root
|
|
823
782
|
"XCUIElementTypeApplication"
|
|
824
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
|
|
825
966
|
function matchesTagList(tagName, tagList) {
|
|
826
967
|
if (tagList.includes(tagName)) {
|
|
827
968
|
return true;
|
|
@@ -902,7 +1043,6 @@ function shouldIncludeElement(element, filters, isNative, automationName) {
|
|
|
902
1043
|
const {
|
|
903
1044
|
includeTagNames = [],
|
|
904
1045
|
excludeTagNames = ["hierarchy"],
|
|
905
|
-
// Always exclude root hierarchy node
|
|
906
1046
|
requireAttributes = [],
|
|
907
1047
|
minAttributeCount = 0,
|
|
908
1048
|
fetchableOnly = false,
|
|
@@ -949,38 +1089,144 @@ function isValidValue(value) {
|
|
|
949
1089
|
function escapeText(text) {
|
|
950
1090
|
return text.replace(/"/g, '\\"').replace(/\n/g, "\\n");
|
|
951
1091
|
}
|
|
952
|
-
function
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
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;
|
|
960
1108
|
}
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
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
|
+
}
|
|
964
1167
|
}
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
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
|
+
}
|
|
968
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)]);
|
|
969
1211
|
} else {
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
if (isValidValue(label) && label !== name && isAttributeUnique(sourceXML, "label", label)) {
|
|
976
|
-
results.push(["predicate-string", `-ios predicate string:label == "${escapeText(label)}"`]);
|
|
977
|
-
}
|
|
978
|
-
const value = attrs.value;
|
|
979
|
-
if (isValidValue(value) && isAttributeUnique(sourceXML, "value", value)) {
|
|
980
|
-
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
|
+
}
|
|
981
1217
|
}
|
|
1218
|
+
results.push(["xpath", xpath2]);
|
|
982
1219
|
}
|
|
983
|
-
|
|
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;
|
|
984
1230
|
}
|
|
985
1231
|
function buildUiAutomatorSelector(element) {
|
|
986
1232
|
const attrs = element.attributes;
|
|
@@ -1063,19 +1309,86 @@ function buildXPath(element, sourceXML, isAndroid) {
|
|
|
1063
1309
|
}
|
|
1064
1310
|
return `//${tagName}[${conditions.join(" and ")}]`;
|
|
1065
1311
|
}
|
|
1066
|
-
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) {
|
|
1067
1377
|
const results = [];
|
|
1068
1378
|
const isAndroid = automationName.toLowerCase().includes("uiautomator");
|
|
1379
|
+
const inUiAutomatorScope = isAndroid ? isInUiAutomatorScope(element, ctx.parsedDOM) : true;
|
|
1069
1380
|
if (isAndroid) {
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1381
|
+
if (inUiAutomatorScope) {
|
|
1382
|
+
const uiAutomator = buildUiAutomatorSelector(element);
|
|
1383
|
+
if (uiAutomator) {
|
|
1384
|
+
results.push(["uiautomator", uiAutomator]);
|
|
1385
|
+
}
|
|
1073
1386
|
}
|
|
1074
|
-
const
|
|
1075
|
-
if (
|
|
1076
|
-
results
|
|
1387
|
+
const xpath2 = buildXPath(element, ctx.sourceXML, true);
|
|
1388
|
+
if (xpath2) {
|
|
1389
|
+
addXPathLocator(results, xpath2, ctx, targetNode);
|
|
1077
1390
|
}
|
|
1078
|
-
if (isValidValue(element.attributes.class)) {
|
|
1391
|
+
if (inUiAutomatorScope && isValidValue(element.attributes.class)) {
|
|
1079
1392
|
results.push([
|
|
1080
1393
|
"class-name",
|
|
1081
1394
|
`android=new UiSelector().className("${element.attributes.class}")`
|
|
@@ -1090,9 +1403,9 @@ function getComplexSuggestedLocators(element, sourceXML, isNative, automationNam
|
|
|
1090
1403
|
if (classChain) {
|
|
1091
1404
|
results.push(["class-chain", classChain]);
|
|
1092
1405
|
}
|
|
1093
|
-
const
|
|
1094
|
-
if (
|
|
1095
|
-
results
|
|
1406
|
+
const xpath2 = buildXPath(element, ctx.sourceXML, false);
|
|
1407
|
+
if (xpath2) {
|
|
1408
|
+
addXPathLocator(results, xpath2, ctx, targetNode);
|
|
1096
1409
|
}
|
|
1097
1410
|
const type = element.tagName;
|
|
1098
1411
|
if (type.startsWith("XCUIElementType")) {
|
|
@@ -1101,9 +1414,14 @@ function getComplexSuggestedLocators(element, sourceXML, isNative, automationNam
|
|
|
1101
1414
|
}
|
|
1102
1415
|
return results;
|
|
1103
1416
|
}
|
|
1104
|
-
function getSuggestedLocators(element, sourceXML,
|
|
1105
|
-
const
|
|
1106
|
-
|
|
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);
|
|
1107
1425
|
const seen = /* @__PURE__ */ new Set();
|
|
1108
1426
|
const results = [];
|
|
1109
1427
|
for (const locator of [...simpleLocators, ...complexLocators]) {
|
|
@@ -1124,7 +1442,7 @@ function locatorsToObject(locators) {
|
|
|
1124
1442
|
return result;
|
|
1125
1443
|
}
|
|
1126
1444
|
|
|
1127
|
-
// src/locators/
|
|
1445
|
+
// src/locators/index.ts
|
|
1128
1446
|
function parseBounds(element, platform) {
|
|
1129
1447
|
return platform === "android" ? parseAndroidBounds(element.attributes.bounds || "") : parseIOSBounds(element.attributes);
|
|
1130
1448
|
}
|
|
@@ -1160,7 +1478,14 @@ function shouldProcess(element, ctx) {
|
|
|
1160
1478
|
function processElement(element, ctx) {
|
|
1161
1479
|
if (!shouldProcess(element, ctx)) return;
|
|
1162
1480
|
try {
|
|
1163
|
-
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
|
+
);
|
|
1164
1489
|
if (locators.length === 0) return;
|
|
1165
1490
|
const transformed = transformElement(element, locators, ctx);
|
|
1166
1491
|
if (Object.keys(transformed.locators).length === 0) return;
|
|
@@ -1182,6 +1507,7 @@ function generateAllElementLocators(sourceXML, options) {
|
|
|
1182
1507
|
console.error("[generateAllElementLocators] Failed to parse page source XML");
|
|
1183
1508
|
return [];
|
|
1184
1509
|
}
|
|
1510
|
+
const parsedDOM = xmlToDOM(sourceXML);
|
|
1185
1511
|
const ctx = {
|
|
1186
1512
|
sourceXML,
|
|
1187
1513
|
platform: options.platform,
|
|
@@ -1189,7 +1515,8 @@ function generateAllElementLocators(sourceXML, options) {
|
|
|
1189
1515
|
isNative: options.isNative ?? true,
|
|
1190
1516
|
viewportSize: options.viewportSize ?? { width: 9999, height: 9999 },
|
|
1191
1517
|
filters: options.filters ?? {},
|
|
1192
|
-
results: []
|
|
1518
|
+
results: [],
|
|
1519
|
+
parsedDOM
|
|
1193
1520
|
};
|
|
1194
1521
|
traverseTree(sourceJSON, ctx);
|
|
1195
1522
|
return ctx.results;
|
|
@@ -1240,7 +1567,8 @@ function toMobileElementInfo(element, includeBounds) {
|
|
|
1240
1567
|
resourceId: element.resourceId || "",
|
|
1241
1568
|
accessibilityId: accessId || "",
|
|
1242
1569
|
isEnabled: element.enabled !== false,
|
|
1243
|
-
|
|
1570
|
+
altSelector: selectedLocators[1] || ""
|
|
1571
|
+
// Single alternative (flattened for tabular)
|
|
1244
1572
|
};
|
|
1245
1573
|
if (includeBounds) {
|
|
1246
1574
|
info.bounds = element.bounds;
|
|
@@ -1310,7 +1638,7 @@ var getVisibleElementsTool = async (args) => {
|
|
|
1310
1638
|
const platform = browser.isAndroid ? "android" : "ios";
|
|
1311
1639
|
elements = await getMobileVisibleElements(browser, platform, { includeContainers, includeBounds });
|
|
1312
1640
|
} else {
|
|
1313
|
-
elements = await browser
|
|
1641
|
+
elements = await getBrowserInteractableElements(browser, { elementType });
|
|
1314
1642
|
}
|
|
1315
1643
|
if (inViewportOnly) {
|
|
1316
1644
|
elements = elements.filter((el) => el.isInViewport !== false);
|
|
@@ -1501,29 +1829,15 @@ var deleteCookiesTool = async ({ name }) => {
|
|
|
1501
1829
|
}
|
|
1502
1830
|
};
|
|
1503
1831
|
|
|
1504
|
-
// src/
|
|
1505
|
-
import { encode as encode2 } from "@toon-format/toon";
|
|
1506
|
-
import { z as z10 } from "zod";
|
|
1507
|
-
var getAccessibilityToolDefinition = {
|
|
1508
|
-
name: "get_accessibility",
|
|
1509
|
-
description: "gets accessibility tree snapshot with semantic information about page elements (roles, names, states). Browser-only - use when get_visible_elements does not return expected elements.",
|
|
1510
|
-
inputSchema: {
|
|
1511
|
-
limit: z10.number().optional().describe("Maximum number of nodes to return. Default: 100. Use 0 for unlimited."),
|
|
1512
|
-
offset: z10.number().optional().describe("Number of nodes to skip (for pagination). Default: 0."),
|
|
1513
|
-
roles: z10.array(z10.string()).optional().describe('Filter to specific roles (e.g., ["button", "link", "textbox"]). Default: all roles.'),
|
|
1514
|
-
namedOnly: z10.boolean().optional().describe("Only return nodes with a name/label. Default: true. Filters out anonymous containers.")
|
|
1515
|
-
}
|
|
1516
|
-
};
|
|
1832
|
+
// src/scripts/get-browser-accessibility-tree.ts
|
|
1517
1833
|
function flattenAccessibilityTree(node, result = []) {
|
|
1518
1834
|
if (!node) return result;
|
|
1519
1835
|
if (node.role !== "WebArea" || node.name) {
|
|
1520
1836
|
const entry = {
|
|
1521
|
-
// Primary identifiers (most useful)
|
|
1522
1837
|
role: node.role || "",
|
|
1523
1838
|
name: node.name || "",
|
|
1524
1839
|
value: node.value ?? "",
|
|
1525
1840
|
description: node.description || "",
|
|
1526
|
-
// Boolean states (empty string = not applicable/false)
|
|
1527
1841
|
disabled: node.disabled ? "true" : "",
|
|
1528
1842
|
focused: node.focused ? "true" : "",
|
|
1529
1843
|
selected: node.selected ? "true" : "",
|
|
@@ -1532,7 +1846,6 @@ function flattenAccessibilityTree(node, result = []) {
|
|
|
1532
1846
|
pressed: node.pressed === true ? "true" : node.pressed === false ? "false" : node.pressed === "mixed" ? "mixed" : "",
|
|
1533
1847
|
readonly: node.readonly ? "true" : "",
|
|
1534
1848
|
required: node.required ? "true" : "",
|
|
1535
|
-
// Less common properties
|
|
1536
1849
|
level: node.level ?? "",
|
|
1537
1850
|
valuemin: node.valuemin ?? "",
|
|
1538
1851
|
valuemax: node.valuemax ?? "",
|
|
@@ -1556,6 +1869,35 @@ function flattenAccessibilityTree(node, result = []) {
|
|
|
1556
1869
|
}
|
|
1557
1870
|
return result;
|
|
1558
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
|
+
|
|
1888
|
+
// src/tools/get-accessibility-tree.tool.ts
|
|
1889
|
+
import { encode as encode2 } from "@toon-format/toon";
|
|
1890
|
+
import { z as z10 } from "zod";
|
|
1891
|
+
var getAccessibilityToolDefinition = {
|
|
1892
|
+
name: "get_accessibility",
|
|
1893
|
+
description: "gets accessibility tree snapshot with semantic information about page elements (roles, names, states). Browser-only - use when get_visible_elements does not return expected elements.",
|
|
1894
|
+
inputSchema: {
|
|
1895
|
+
limit: z10.number().optional().describe("Maximum number of nodes to return. Default: 100. Use 0 for unlimited."),
|
|
1896
|
+
offset: z10.number().optional().describe("Number of nodes to skip (for pagination). Default: 0."),
|
|
1897
|
+
roles: z10.array(z10.string()).optional().describe('Filter to specific roles (e.g., ["button", "link", "textbox"]). Default: all roles.'),
|
|
1898
|
+
namedOnly: z10.boolean().optional().describe("Only return nodes with a name/label. Default: true. Filters out anonymous containers.")
|
|
1899
|
+
}
|
|
1900
|
+
};
|
|
1559
1901
|
var getAccessibilityTreeTool = async (args) => {
|
|
1560
1902
|
try {
|
|
1561
1903
|
const browser = getBrowser();
|
|
@@ -1568,24 +1910,12 @@ var getAccessibilityTreeTool = async (args) => {
|
|
|
1568
1910
|
};
|
|
1569
1911
|
}
|
|
1570
1912
|
const { limit = 100, offset = 0, roles, namedOnly = true } = args || {};
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
if (pages.length === 0) {
|
|
1574
|
-
return {
|
|
1575
|
-
content: [{ type: "text", text: "No active pages found" }]
|
|
1576
|
-
};
|
|
1577
|
-
}
|
|
1578
|
-
const page = pages[0];
|
|
1579
|
-
const snapshot = await page.accessibility.snapshot({
|
|
1580
|
-
interestingOnly: true
|
|
1581
|
-
// Filter to only interesting/semantic nodes
|
|
1582
|
-
});
|
|
1583
|
-
if (!snapshot) {
|
|
1913
|
+
let nodes = await getBrowserAccessibilityTree(browser);
|
|
1914
|
+
if (nodes.length === 0) {
|
|
1584
1915
|
return {
|
|
1585
1916
|
content: [{ type: "text", text: "No accessibility tree available" }]
|
|
1586
1917
|
};
|
|
1587
1918
|
}
|
|
1588
|
-
let nodes = flattenAccessibilityTree(snapshot);
|
|
1589
1919
|
if (namedOnly) {
|
|
1590
1920
|
nodes = nodes.filter((n) => n.name && n.name.trim() !== "");
|
|
1591
1921
|
}
|
|
@@ -2017,11 +2347,21 @@ var package_default = {
|
|
|
2017
2347
|
type: "git",
|
|
2018
2348
|
url: "git://github.com/webdriverio/mcp.git"
|
|
2019
2349
|
},
|
|
2020
|
-
version: "2.
|
|
2350
|
+
version: "2.1.0",
|
|
2021
2351
|
description: "MCP server with WebdriverIO for browser and mobile app automation (iOS/Android via Appium)",
|
|
2022
2352
|
main: "./lib/server.js",
|
|
2023
2353
|
module: "./lib/server.js",
|
|
2024
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
|
+
},
|
|
2025
2365
|
bin: {
|
|
2026
2366
|
"wdio-mcp": "lib/server.js"
|
|
2027
2367
|
},
|
|
@@ -2045,6 +2385,7 @@ var package_default = {
|
|
|
2045
2385
|
},
|
|
2046
2386
|
dependencies: {
|
|
2047
2387
|
"@modelcontextprotocol/sdk": "1.25",
|
|
2388
|
+
xpath: "^0.0.34",
|
|
2048
2389
|
"@toon-format/toon": "^2.1.0",
|
|
2049
2390
|
"@wdio/protocols": "^9.16.2",
|
|
2050
2391
|
"@xmldom/xmldom": "^0.8.11",
|
|
@@ -2082,7 +2423,7 @@ var server = new McpServer({
|
|
|
2082
2423
|
description: package_default.description,
|
|
2083
2424
|
websiteUrl: "https://github.com/webdriverio/mcp"
|
|
2084
2425
|
}, {
|
|
2085
|
-
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.",
|
|
2086
2427
|
capabilities: {
|
|
2087
2428
|
tools: {}
|
|
2088
2429
|
}
|