@hypothesi/tauri-mcp-server 0.11.2 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/dist/config.d.ts +14 -0
- package/dist/config.js +26 -0
- package/dist/driver/app-discovery.d.ts +4 -2
- package/dist/driver/app-discovery.js +23 -3
- package/dist/driver/element-picker.d.ts +1 -1
- package/dist/driver/plugin-client.d.ts +23 -1
- package/dist/driver/plugin-client.js +127 -2
- package/dist/driver/plugin-commands.d.ts +8 -4
- package/dist/driver/plugin-commands.js +71 -16
- package/dist/driver/protocol.d.ts +7 -0
- package/dist/driver/scripts/find-element.js +129 -16
- package/dist/driver/scripts/focus.js +31 -4
- package/dist/driver/scripts/html2canvas-loader.js +20 -2
- package/dist/driver/scripts/index.d.ts +2 -4
- package/dist/driver/scripts/index.js +40 -11
- package/dist/driver/scripts/interact.js +66 -5
- package/dist/driver/scripts/resolve-ref.js +101 -50
- package/dist/driver/session-manager.d.ts +39 -0
- package/dist/driver/session-manager.js +133 -13
- package/dist/driver/webview-executor.d.ts +20 -8
- package/dist/driver/webview-executor.js +159 -58
- package/dist/driver/webview-interactions.d.ts +57 -42
- package/dist/driver/webview-interactions.js +62 -40
- package/dist/manager/mobile.js +3 -2
- package/dist/monitor/logs.d.ts +14 -2
- package/dist/monitor/logs.js +37 -5
- package/dist/tools-registry.js +35 -33
- package/dist/version.d.ts +5 -0
- package/dist/version.js +16 -0
- package/package.json +1 -1
- package/dist/driver/scripts/get-styles.js +0 -47
- package/dist/driver/scripts/keyboard.js +0 -76
|
@@ -1,26 +1,139 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Find
|
|
2
|
+
* Find elements using various selector strategies and return structured data
|
|
3
3
|
*
|
|
4
4
|
* @param {Object} params
|
|
5
5
|
* @param {string} params.selector - Element selector, ref ID (e.g., "ref=e3"), or text
|
|
6
6
|
* @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
|
|
7
|
+
* @param {number|null} params.nth - Zero-based index to pick one of several matching elements
|
|
8
|
+
* @param {number} params.limit - Maximum number of matches to return
|
|
9
|
+
* @param {string[]} params.properties - Computed CSS properties to include
|
|
10
|
+
* @param {boolean} params.visibleOnly - Return only rendered elements
|
|
11
|
+
* @param {boolean} params.includeHtml - Include outerHTML for each match
|
|
7
12
|
*/
|
|
8
13
|
(function(params) {
|
|
9
|
-
const { selector, strategy } = params;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
14
|
+
const { selector, strategy, nth, limit, properties, visibleOnly, includeHtml } = params;
|
|
15
|
+
|
|
16
|
+
function getAccessibleName(element) {
|
|
17
|
+
try {
|
|
18
|
+
if (window.ariaApi && typeof window.ariaApi.getName === 'function') {
|
|
19
|
+
return window.ariaApi.getName(element) || null;
|
|
20
|
+
}
|
|
21
|
+
} catch (e) {
|
|
22
|
+
// Ignore aria-api errors
|
|
23
|
+
}
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function getAttributes(element) {
|
|
28
|
+
const attrs = {
|
|
29
|
+
id: element.id || null,
|
|
30
|
+
class: element.className || null,
|
|
31
|
+
role: element.getAttribute('role') || null,
|
|
32
|
+
'aria-label': element.getAttribute('aria-label') || null,
|
|
33
|
+
'data-testid': element.getAttribute('data-testid') || null,
|
|
34
|
+
href: element.getAttribute('href') || null,
|
|
35
|
+
type: element.getAttribute('type') || null,
|
|
36
|
+
disabled: element.hasAttribute('disabled') ? '' : null,
|
|
37
|
+
};
|
|
38
|
+
// Remove null values
|
|
39
|
+
const result = {};
|
|
40
|
+
for (const key in attrs) {
|
|
41
|
+
if (attrs[key] !== null) {
|
|
42
|
+
result[key] = attrs[key];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return result;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function getStyles(element) {
|
|
49
|
+
if (!properties || properties.length === 0) return null;
|
|
50
|
+
const computed = window.getComputedStyle(element);
|
|
51
|
+
const result = {};
|
|
52
|
+
for (let i = 0; i < properties.length; i++) {
|
|
53
|
+
const prop = properties[i];
|
|
54
|
+
result[prop] = computed.getPropertyValue(prop);
|
|
55
|
+
}
|
|
56
|
+
return result;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isInteractive(element) {
|
|
60
|
+
return element.matches && element.matches(window.__MCP__.INTERACTIVE_SELECTOR);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Get all candidates first
|
|
64
|
+
var allMatches = window.__MCP__.resolveAll(selector, strategy);
|
|
65
|
+
|
|
66
|
+
// Filter by visibility if requested
|
|
67
|
+
var matches = visibleOnly
|
|
68
|
+
? allMatches.filter(function(el) { return window.__MCP__.isRendered(el); })
|
|
69
|
+
: allMatches;
|
|
70
|
+
|
|
71
|
+
const totalMatches = matches.length;
|
|
72
|
+
|
|
73
|
+
// Apply nth if specified
|
|
74
|
+
if (typeof nth === 'number') {
|
|
75
|
+
if (!matches[nth]) {
|
|
76
|
+
return JSON.stringify({
|
|
77
|
+
selector: selector,
|
|
78
|
+
strategy: strategy,
|
|
79
|
+
totalMatches: totalMatches,
|
|
80
|
+
returned: 0,
|
|
81
|
+
truncated: false,
|
|
82
|
+
matches: [],
|
|
83
|
+
error: 'nth=' + nth + ' is out of range, ' + totalMatches + ' matches for ' + selector
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
matches = [matches[nth]];
|
|
23
87
|
}
|
|
24
88
|
|
|
25
|
-
|
|
89
|
+
// Apply limit
|
|
90
|
+
const truncated = matches.length > limit;
|
|
91
|
+
if (truncated) {
|
|
92
|
+
matches = matches.slice(0, limit);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const returned = matches.length;
|
|
96
|
+
|
|
97
|
+
const results = matches.map(function(element, index) {
|
|
98
|
+
const rect = element.getBoundingClientRect();
|
|
99
|
+
const ref = window.__MCP__.reverseRefs && window.__MCP__.reverseRefs.has(element)
|
|
100
|
+
? Array.from(window.__MCP__.reverseRefs.entries()).find(([k, v]) => v === element)[0]
|
|
101
|
+
: null;
|
|
102
|
+
|
|
103
|
+
const match = {
|
|
104
|
+
nth: typeof nth === 'number' ? nth : (index + (typeof nth !== 'number' ? 0 : 0)),
|
|
105
|
+
ref: ref,
|
|
106
|
+
tag: element.tagName.toLowerCase(),
|
|
107
|
+
text: (element.textContent || '').trim(),
|
|
108
|
+
accessibleName: getAccessibleName(element),
|
|
109
|
+
attributes: getAttributes(element),
|
|
110
|
+
rect: {
|
|
111
|
+
x: Math.round(rect.left),
|
|
112
|
+
y: Math.round(rect.top),
|
|
113
|
+
width: Math.round(rect.width),
|
|
114
|
+
height: Math.round(rect.height),
|
|
115
|
+
},
|
|
116
|
+
visible: window.__MCP__.isRendered(element),
|
|
117
|
+
interactive: isInteractive(element),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const styles = getStyles(element);
|
|
121
|
+
if (styles) match.styles = styles;
|
|
122
|
+
|
|
123
|
+
if (includeHtml) {
|
|
124
|
+
const html = element.outerHTML;
|
|
125
|
+
match.html = html.length > 2000 ? html.substring(0, 2000) + '…' : html;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return match;
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
return JSON.stringify({
|
|
132
|
+
selector: selector,
|
|
133
|
+
strategy: strategy,
|
|
134
|
+
totalMatches: totalMatches,
|
|
135
|
+
returned: returned,
|
|
136
|
+
truncated: truncated,
|
|
137
|
+
matches: results,
|
|
138
|
+
});
|
|
26
139
|
})
|
|
@@ -4,15 +4,42 @@
|
|
|
4
4
|
* @param {Object} params
|
|
5
5
|
* @param {string} params.selector - CSS selector, XPath, text, or ref ID (e.g., "ref=e3") for element to focus
|
|
6
6
|
* @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
|
|
7
|
+
* @param {number|null} params.nth - Zero-based index to pick one of several matching elements
|
|
7
8
|
*/
|
|
8
9
|
(function(params) {
|
|
9
|
-
const { selector, strategy } = params;
|
|
10
|
+
const { selector, strategy, nth } = params;
|
|
10
11
|
|
|
11
12
|
function resolveElement(selectorOrRef) {
|
|
12
13
|
if (!selectorOrRef) return null;
|
|
13
|
-
var
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
var matches = window.__MCP__.resolveAll(selectorOrRef, strategy);
|
|
15
|
+
|
|
16
|
+
if (matches.length === 0) throw new Error('Element not found: ' + selectorOrRef);
|
|
17
|
+
if (typeof nth === 'number') {
|
|
18
|
+
if (!matches[nth]) {
|
|
19
|
+
throw new Error('nth=' + nth + ' is out of range, ' + matches.length + ' matches for ' + selectorOrRef);
|
|
20
|
+
}
|
|
21
|
+
return matches[nth];
|
|
22
|
+
}
|
|
23
|
+
if (matches.length > 1 && strategy === 'text') {
|
|
24
|
+
throw new Error(describeAmbiguity(selectorOrRef, matches));
|
|
25
|
+
}
|
|
26
|
+
return matches[0];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function describeAmbiguity(selectorOrRef, matches) {
|
|
30
|
+
var lines = ['Ambiguous text selector "' + selectorOrRef + '" matched ' + matches.length + ' visible elements. Pass nth, or use a CSS selector or ref.'];
|
|
31
|
+
for (var i = 0; i < Math.min(matches.length, 5); i++) {
|
|
32
|
+
var el = matches[i];
|
|
33
|
+
var rect = el.getBoundingClientRect();
|
|
34
|
+
var text = (el.textContent || '').trim().substring(0, 50);
|
|
35
|
+
var tag = el.tagName.toLowerCase();
|
|
36
|
+
var className = el.className || '';
|
|
37
|
+
lines.push(' nth=' + i + ' ' + tag + (className ? '.' + className.split(' ')[0] : '') + ' "' + text + '" rect ' + Math.round(rect.left) + ',' + Math.round(rect.top) + ' ' + Math.round(rect.width) + 'x' + Math.round(rect.height));
|
|
38
|
+
}
|
|
39
|
+
if (matches.length > 5) {
|
|
40
|
+
lines.push(' ... and ' + (matches.length - 5) + ' more');
|
|
41
|
+
}
|
|
42
|
+
return lines.join('\n');
|
|
16
43
|
}
|
|
17
44
|
|
|
18
45
|
const element = resolveElement(selector);
|
|
@@ -58,14 +58,32 @@ export function buildScreenshotCaptureScript(format, quality) {
|
|
|
58
58
|
return `
|
|
59
59
|
${HTML2CANVAS_RESOLVER_SCRIPT}
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// Render the document, but crop the output to the visible viewport. Without
|
|
62
|
+
// explicit bounds html2canvas allocates a canvas for the full scrollable
|
|
63
|
+
// document, which is both inconsistent with the native implementations and
|
|
64
|
+
// can time out on long pages.
|
|
62
65
|
const element = document.documentElement;
|
|
63
66
|
if (!element) {
|
|
64
67
|
throw new Error('document.documentElement is null');
|
|
65
68
|
}
|
|
66
69
|
|
|
70
|
+
const viewportWidth = Math.max(element.clientWidth, window.innerWidth || 0);
|
|
71
|
+
const viewportHeight = Math.max(element.clientHeight, window.innerHeight || 0);
|
|
72
|
+
const viewportX = window.scrollX || window.pageXOffset || 0;
|
|
73
|
+
const viewportY = window.scrollY || window.pageYOffset || 0;
|
|
74
|
+
|
|
67
75
|
// Capture the webview
|
|
68
|
-
const canvas = await html2canvasFn(element,
|
|
76
|
+
const canvas = await html2canvasFn(element, {
|
|
77
|
+
...${HTML2CANVAS_OPTIONS_SCRIPT},
|
|
78
|
+
x: viewportX,
|
|
79
|
+
y: viewportY,
|
|
80
|
+
width: viewportWidth,
|
|
81
|
+
height: viewportHeight,
|
|
82
|
+
windowWidth: viewportWidth,
|
|
83
|
+
windowHeight: viewportHeight,
|
|
84
|
+
scrollX: viewportX,
|
|
85
|
+
scrollY: viewportY,
|
|
86
|
+
});
|
|
69
87
|
if (!canvas) {
|
|
70
88
|
throw new Error('html2canvas returned null canvas');
|
|
71
89
|
}
|
|
@@ -8,9 +8,7 @@ export declare const SCRIPTS: {
|
|
|
8
8
|
readonly resolveRef: string;
|
|
9
9
|
readonly interact: string;
|
|
10
10
|
readonly swipe: string;
|
|
11
|
-
readonly keyboard: string;
|
|
12
11
|
readonly waitFor: string;
|
|
13
|
-
readonly getStyles: string;
|
|
14
12
|
readonly focus: string;
|
|
15
13
|
readonly findElement: string;
|
|
16
14
|
readonly domSnapshot: string;
|
|
@@ -28,9 +26,9 @@ export declare function getResolveRefSource(): string;
|
|
|
28
26
|
*/
|
|
29
27
|
export declare function buildScript(script: string, params: Record<string, unknown>): string;
|
|
30
28
|
/**
|
|
31
|
-
* Build a script for typing text
|
|
29
|
+
* Build a script for typing text
|
|
32
30
|
*/
|
|
33
|
-
export declare function buildTypeScript(selector: string, text: string, strategy?: string): string;
|
|
31
|
+
export declare function buildTypeScript(selector: string, text: string, strategy?: string, nth?: number | null): string;
|
|
34
32
|
/**
|
|
35
33
|
* Build a script for key events (press, down, up)
|
|
36
34
|
*/
|
|
@@ -16,9 +16,7 @@ export const SCRIPTS = {
|
|
|
16
16
|
resolveRef: loadScript('resolve-ref'),
|
|
17
17
|
interact: loadScript('interact'),
|
|
18
18
|
swipe: loadScript('swipe'),
|
|
19
|
-
keyboard: loadScript('keyboard'),
|
|
20
19
|
waitFor: loadScript('wait-for'),
|
|
21
|
-
getStyles: loadScript('get-styles'),
|
|
22
20
|
focus: loadScript('focus'),
|
|
23
21
|
findElement: loadScript('find-element'),
|
|
24
22
|
domSnapshot: loadScript('dom-snapshot'),
|
|
@@ -40,21 +38,52 @@ export function buildScript(script, params) {
|
|
|
40
38
|
return `(${script})(${JSON.stringify(params)})`;
|
|
41
39
|
}
|
|
42
40
|
/**
|
|
43
|
-
* Build a script for typing text
|
|
41
|
+
* Build a script for typing text
|
|
44
42
|
*/
|
|
45
|
-
export function buildTypeScript(selector, text, strategy) {
|
|
46
|
-
const escapedText = text.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
47
|
-
const escapedSelector = selector.replace(/\\/g, '\\\\').replace(/'/g, "\\'");
|
|
43
|
+
export function buildTypeScript(selector, text, strategy, nth) {
|
|
48
44
|
const strat = strategy || 'css';
|
|
45
|
+
const nthValue = nth ?? null;
|
|
49
46
|
return `
|
|
50
47
|
(function() {
|
|
51
|
-
const selector =
|
|
52
|
-
const strategy =
|
|
53
|
-
const text =
|
|
48
|
+
const selector = ${JSON.stringify(selector)};
|
|
49
|
+
const strategy = ${JSON.stringify(strat)};
|
|
50
|
+
const text = ${JSON.stringify(text)};
|
|
51
|
+
const nth = ${JSON.stringify(nthValue)};
|
|
54
52
|
|
|
55
|
-
|
|
56
|
-
|
|
53
|
+
function resolveElement(selectorOrRef) {
|
|
54
|
+
if (!selectorOrRef) return null;
|
|
55
|
+
var matches = window.__MCP__.resolveAll(selectorOrRef, strategy);
|
|
57
56
|
|
|
57
|
+
if (matches.length === 0) throw new Error('Element not found: ' + selectorOrRef);
|
|
58
|
+
if (typeof nth === 'number') {
|
|
59
|
+
if (!matches[nth]) {
|
|
60
|
+
throw new Error('nth=' + nth + ' is out of range, ' + matches.length + ' matches for ' + selectorOrRef);
|
|
61
|
+
}
|
|
62
|
+
return matches[nth];
|
|
63
|
+
}
|
|
64
|
+
if (matches.length > 1 && strategy === 'text') {
|
|
65
|
+
throw new Error(describeAmbiguity(selectorOrRef, matches));
|
|
66
|
+
}
|
|
67
|
+
return matches[0];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function describeAmbiguity(selectorOrRef, matches) {
|
|
71
|
+
var lines = ['Ambiguous text selector "' + selectorOrRef + '" matched ' + matches.length + ' visible elements. Pass nth, or use a CSS selector or ref.'];
|
|
72
|
+
for (var i = 0; i < Math.min(matches.length, 5); i++) {
|
|
73
|
+
var el = matches[i];
|
|
74
|
+
var rect = el.getBoundingClientRect();
|
|
75
|
+
var text = (el.textContent || '').trim().substring(0, 50);
|
|
76
|
+
var tag = el.tagName.toLowerCase();
|
|
77
|
+
var className = el.className || '';
|
|
78
|
+
lines.push(' nth=' + i + ' ' + tag + (className ? '.' + className.split(' ')[0] : '') + ' "' + text + '" rect ' + Math.round(rect.left) + ',' + Math.round(rect.top) + ' ' + Math.round(rect.width) + 'x' + Math.round(rect.height));
|
|
79
|
+
}
|
|
80
|
+
if (matches.length > 5) {
|
|
81
|
+
lines.push(' ... and ' + (matches.length - 5) + ' more');
|
|
82
|
+
}
|
|
83
|
+
return lines.join('\\n');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
var element = resolveElement(selector);
|
|
58
87
|
element.focus();
|
|
59
88
|
|
|
60
89
|
// Use native prototype setter to bypass React's value tracker
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Webview interaction script - handles click, double-click, long-press, and
|
|
2
|
+
* Webview interaction script - handles click, double-click, long-press, scroll, swipe, hover, and right-click actions
|
|
3
3
|
* This script is injected into the webview and executed with parameters.
|
|
4
4
|
*
|
|
5
5
|
* @param {Object} params
|
|
6
6
|
* @param {string} params.action - The action to perform
|
|
7
7
|
* @param {string|null} params.selector - CSS selector, XPath, text, or ref ID (e.g., "ref=e3") for the element
|
|
8
8
|
* @param {string} params.strategy - Selector strategy: 'css', 'xpath', or 'text'
|
|
9
|
+
* @param {number|null} params.nth - Zero-based index to pick one of several matching elements
|
|
9
10
|
* @param {number|null} params.x - X coordinate
|
|
10
11
|
* @param {number|null} params.y - Y coordinate
|
|
11
12
|
* @param {number} params.duration - Duration for long-press
|
|
@@ -13,13 +14,39 @@
|
|
|
13
14
|
* @param {number} params.scrollY - Vertical scroll amount
|
|
14
15
|
*/
|
|
15
16
|
(function(params) {
|
|
16
|
-
const { action, selector, strategy, x, y, duration, scrollX, scrollY } = params;
|
|
17
|
+
const { action, selector, strategy, nth, x, y, duration, scrollX, scrollY } = params;
|
|
17
18
|
|
|
18
19
|
function resolveElement(selectorOrRef) {
|
|
19
20
|
if (!selectorOrRef) return null;
|
|
20
|
-
var
|
|
21
|
-
|
|
22
|
-
|
|
21
|
+
var matches = window.__MCP__.resolveAll(selectorOrRef, strategy);
|
|
22
|
+
|
|
23
|
+
if (matches.length === 0) throw new Error('Element not found: ' + selectorOrRef);
|
|
24
|
+
if (typeof nth === 'number') {
|
|
25
|
+
if (!matches[nth]) {
|
|
26
|
+
throw new Error('nth=' + nth + ' is out of range, ' + matches.length + ' matches for ' + selectorOrRef);
|
|
27
|
+
}
|
|
28
|
+
return matches[nth];
|
|
29
|
+
}
|
|
30
|
+
if (matches.length > 1 && strategy === 'text') {
|
|
31
|
+
throw new Error(describeAmbiguity(selectorOrRef, matches));
|
|
32
|
+
}
|
|
33
|
+
return matches[0];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function describeAmbiguity(selectorOrRef, matches) {
|
|
37
|
+
var lines = ['Ambiguous text selector "' + selectorOrRef + '" matched ' + matches.length + ' visible elements. Pass nth, or use a CSS selector or ref.'];
|
|
38
|
+
for (var i = 0; i < Math.min(matches.length, 5); i++) {
|
|
39
|
+
var el = matches[i];
|
|
40
|
+
var rect = el.getBoundingClientRect();
|
|
41
|
+
var text = (el.textContent || '').trim().substring(0, 50);
|
|
42
|
+
var tag = el.tagName.toLowerCase();
|
|
43
|
+
var className = el.className || '';
|
|
44
|
+
lines.push(' nth=' + i + ' ' + tag + (className ? '.' + className.split(' ')[0] : '') + ' "' + text + '" rect ' + Math.round(rect.left) + ',' + Math.round(rect.top) + ' ' + Math.round(rect.width) + 'x' + Math.round(rect.height));
|
|
45
|
+
}
|
|
46
|
+
if (matches.length > 5) {
|
|
47
|
+
lines.push(' ... and ' + (matches.length - 5) + ' more');
|
|
48
|
+
}
|
|
49
|
+
return lines.join('\n');
|
|
23
50
|
}
|
|
24
51
|
|
|
25
52
|
function matchHint() {
|
|
@@ -62,6 +89,19 @@
|
|
|
62
89
|
clientY: targetY,
|
|
63
90
|
};
|
|
64
91
|
|
|
92
|
+
const pointerOptions = {
|
|
93
|
+
...eventOptions,
|
|
94
|
+
pointerId: 1,
|
|
95
|
+
pointerType: 'mouse',
|
|
96
|
+
isPrimary: true,
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
const rightButtonOptions = {
|
|
100
|
+
...eventOptions,
|
|
101
|
+
button: 2,
|
|
102
|
+
buttons: 2,
|
|
103
|
+
};
|
|
104
|
+
|
|
65
105
|
if (action === 'click') {
|
|
66
106
|
if (element) {
|
|
67
107
|
element.dispatchEvent(new MouseEvent('mousedown', eventOptions));
|
|
@@ -94,6 +134,27 @@
|
|
|
94
134
|
return `Long-pressed at (${targetX}, ${targetY}) for ${duration}ms` + matchHint();
|
|
95
135
|
}
|
|
96
136
|
|
|
137
|
+
if (action === 'hover') {
|
|
138
|
+
if (element) {
|
|
139
|
+
element.dispatchEvent(new PointerEvent('pointerover', pointerOptions));
|
|
140
|
+
element.dispatchEvent(new PointerEvent('pointerenter', pointerOptions));
|
|
141
|
+
element.dispatchEvent(new MouseEvent('mouseover', eventOptions));
|
|
142
|
+
element.dispatchEvent(new MouseEvent('mouseenter', eventOptions));
|
|
143
|
+
element.dispatchEvent(new PointerEvent('pointermove', pointerOptions));
|
|
144
|
+
element.dispatchEvent(new MouseEvent('mousemove', eventOptions));
|
|
145
|
+
}
|
|
146
|
+
return `Hovered at (${targetX}, ${targetY})` + matchHint();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (action === 'right-click') {
|
|
150
|
+
if (element) {
|
|
151
|
+
element.dispatchEvent(new MouseEvent('mousedown', rightButtonOptions));
|
|
152
|
+
element.dispatchEvent(new MouseEvent('mouseup', rightButtonOptions));
|
|
153
|
+
element.dispatchEvent(new MouseEvent('contextmenu', rightButtonOptions));
|
|
154
|
+
}
|
|
155
|
+
return `Right-clicked at (${targetX}, ${targetY})` + matchHint();
|
|
156
|
+
}
|
|
157
|
+
|
|
97
158
|
if (action === 'scroll') {
|
|
98
159
|
const scrollTarget = element || window;
|
|
99
160
|
if (scrollX !== 0 || scrollY !== 0) {
|
|
@@ -15,6 +15,84 @@
|
|
|
15
15
|
|
|
16
16
|
var REF_PATTERN = /^\[?(?:ref=)?(e\d+)\]?$/;
|
|
17
17
|
|
|
18
|
+
var NON_RENDERED_TAGS = {
|
|
19
|
+
SCRIPT: 1, STYLE: 1, TITLE: 1, HEAD: 1, META: 1,
|
|
20
|
+
LINK: 1, TEMPLATE: 1, NOSCRIPT: 1, BASE: 1,
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
var INTERACTIVE_SELECTOR = 'a,button,input,select,textarea,summary,' +
|
|
24
|
+
'[role=button],[role=link],[role=tab],[role=menuitem],[role=option],[tabindex]';
|
|
25
|
+
|
|
26
|
+
function isRendered(element) {
|
|
27
|
+
if (!element || element.nodeType !== 1) return false;
|
|
28
|
+
if (NON_RENDERED_TAGS[element.tagName]) return false;
|
|
29
|
+
if (element.hidden) return false;
|
|
30
|
+
if (element.getClientRects().length === 0) return false;
|
|
31
|
+
try {
|
|
32
|
+
var style = window.getComputedStyle(element);
|
|
33
|
+
if (style.display === 'none') return false;
|
|
34
|
+
if (style.visibility === 'hidden' || style.visibility === 'collapse') return false;
|
|
35
|
+
} catch (e) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return true;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function hasMatchingDescendant(element, needle) {
|
|
42
|
+
var descendants = element.querySelectorAll('*');
|
|
43
|
+
for (var i = 0; i < descendants.length; i++) {
|
|
44
|
+
var el = descendants[i];
|
|
45
|
+
if (!isRendered(el)) continue;
|
|
46
|
+
var content = (el.textContent || '').trim();
|
|
47
|
+
if (content.indexOf(needle) !== -1) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function preferInteractive(el) {
|
|
53
|
+
if (el.matches(INTERACTIVE_SELECTOR)) return el;
|
|
54
|
+
var host = el.closest(INTERACTIVE_SELECTOR);
|
|
55
|
+
return host && isRendered(host) ? host : el;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function dedupe(arr) {
|
|
59
|
+
var seen = new Set();
|
|
60
|
+
var result = [];
|
|
61
|
+
for (var i = 0; i < arr.length; i++) {
|
|
62
|
+
var el = arr[i];
|
|
63
|
+
if (!seen.has(el)) {
|
|
64
|
+
seen.add(el);
|
|
65
|
+
result.push(el);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return result;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function textCandidates(needle) {
|
|
72
|
+
var all = document.body ? document.body.querySelectorAll('*') : [],
|
|
73
|
+
exact = [], partial = [];
|
|
74
|
+
|
|
75
|
+
for (var i = 0; i < all.length; i++) {
|
|
76
|
+
var el = all[i],
|
|
77
|
+
content = (el.textContent || '').trim(),
|
|
78
|
+
attributeText = [
|
|
79
|
+
el.getAttribute('placeholder'),
|
|
80
|
+
el.getAttribute('aria-label'),
|
|
81
|
+
el.getAttribute('title'),
|
|
82
|
+
].filter(Boolean),
|
|
83
|
+
exactAttribute = attributeText.indexOf(needle) !== -1,
|
|
84
|
+
partialAttribute = attributeText.some(function(value) { return value.indexOf(needle) !== -1; }),
|
|
85
|
+
textMatch = content.indexOf(needle) !== -1;
|
|
86
|
+
|
|
87
|
+
if (!textMatch && !partialAttribute) continue;
|
|
88
|
+
if (!isRendered(el)) continue;
|
|
89
|
+
if (textMatch && el.querySelector('*') && hasMatchingDescendant(el, needle)) continue;
|
|
90
|
+
(content === needle || exactAttribute ? exact : partial).push(preferInteractive(el));
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return dedupe(exact.length > 0 ? exact : partial);
|
|
94
|
+
}
|
|
95
|
+
|
|
18
96
|
function xpathForText(text) {
|
|
19
97
|
// Escape single quotes for XPath by splitting on ' and using concat()
|
|
20
98
|
if (text.indexOf("'") === -1) {
|
|
@@ -47,39 +125,26 @@
|
|
|
47
125
|
}
|
|
48
126
|
|
|
49
127
|
if (strategy === 'text') {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
var result = document.evaluate(xpath, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
53
|
-
if (result.singleNodeValue) return result.singleNodeValue;
|
|
54
|
-
|
|
55
|
-
// Fallback: search placeholder, aria-label, and title attributes
|
|
56
|
-
var attrSelectors = [
|
|
57
|
-
'[placeholder*="' + selectorOrRef.replace(/"/g, '\\"') + '"]',
|
|
58
|
-
'[aria-label*="' + selectorOrRef.replace(/"/g, '\\"') + '"]',
|
|
59
|
-
'[title*="' + selectorOrRef.replace(/"/g, '\\"') + '"]',
|
|
60
|
-
];
|
|
61
|
-
for (var i = 0; i < attrSelectors.length; i++) {
|
|
62
|
-
var el = document.querySelector(attrSelectors[i]);
|
|
63
|
-
if (el) return el;
|
|
64
|
-
}
|
|
65
|
-
return null;
|
|
128
|
+
var candidates = textCandidates(selectorOrRef);
|
|
129
|
+
return candidates[0] || null;
|
|
66
130
|
}
|
|
67
131
|
|
|
68
132
|
if (strategy === 'xpath') {
|
|
69
133
|
var result = document.evaluate(selectorOrRef, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null);
|
|
70
|
-
|
|
134
|
+
var node = result.singleNodeValue;
|
|
135
|
+
return (node && isRendered(node)) ? node : null;
|
|
71
136
|
}
|
|
72
137
|
|
|
73
138
|
// Default: CSS selector
|
|
74
139
|
return document.querySelector(selectorOrRef);
|
|
75
140
|
};
|
|
76
141
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Resolve all matching elements as an Array.
|
|
144
|
+
* @param {string} selector - Selector, XPath, or text
|
|
145
|
+
* @param {string} [strategy] - 'css' (default), 'xpath', or 'text'
|
|
146
|
+
* @returns {Element[]}
|
|
147
|
+
*/
|
|
83
148
|
window.__MCP__.resolveAll = function(selector, strategy) {
|
|
84
149
|
if (!selector) return [];
|
|
85
150
|
|
|
@@ -91,33 +156,15 @@
|
|
|
91
156
|
}
|
|
92
157
|
|
|
93
158
|
if (strategy === 'text') {
|
|
94
|
-
|
|
95
|
-
var xpath = xpathForText(selector);
|
|
96
|
-
var snapshot = document.evaluate(xpath, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
97
|
-
var results = [];
|
|
98
|
-
for (var i = 0; i < snapshot.snapshotLength; i++) {
|
|
99
|
-
results.push(snapshot.snapshotItem(i));
|
|
100
|
-
}
|
|
101
|
-
if (results.length > 0) return results;
|
|
102
|
-
|
|
103
|
-
// Fallback: search placeholder, aria-label, and title attributes
|
|
104
|
-
var attrSelectors = [
|
|
105
|
-
'[placeholder*="' + selector.replace(/"/g, '\\"') + '"]',
|
|
106
|
-
'[aria-label*="' + selector.replace(/"/g, '\\"') + '"]',
|
|
107
|
-
'[title*="' + selector.replace(/"/g, '\\"') + '"]',
|
|
108
|
-
];
|
|
109
|
-
for (var i = 0; i < attrSelectors.length; i++) {
|
|
110
|
-
var found = Array.from(document.querySelectorAll(attrSelectors[i]));
|
|
111
|
-
if (found.length > 0) return results.concat(found);
|
|
112
|
-
}
|
|
113
|
-
return results;
|
|
159
|
+
return textCandidates(selector);
|
|
114
160
|
}
|
|
115
161
|
|
|
116
162
|
if (strategy === 'xpath') {
|
|
117
163
|
var snapshot = document.evaluate(selector, document, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
118
164
|
var results = [];
|
|
119
165
|
for (var i = 0; i < snapshot.snapshotLength; i++) {
|
|
120
|
-
|
|
166
|
+
var node = snapshot.snapshotItem(i);
|
|
167
|
+
if (node && isRendered(node)) results.push(node);
|
|
121
168
|
}
|
|
122
169
|
return results;
|
|
123
170
|
}
|
|
@@ -126,13 +173,17 @@
|
|
|
126
173
|
return Array.from(document.querySelectorAll(selector));
|
|
127
174
|
};
|
|
128
175
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
176
|
+
/**
|
|
177
|
+
* Count all matching elements.
|
|
178
|
+
* @param {string} selector - Selector, XPath, or text
|
|
179
|
+
* @param {string} [strategy] - 'css' (default), 'xpath', or 'text'
|
|
180
|
+
* @returns {number}
|
|
181
|
+
*/
|
|
135
182
|
window.__MCP__.countAll = function(selector, strategy) {
|
|
136
183
|
return window.__MCP__.resolveAll(selector, strategy).length;
|
|
137
184
|
};
|
|
185
|
+
|
|
186
|
+
// Export for reuse by other scripts
|
|
187
|
+
window.__MCP__.isRendered = isRendered;
|
|
188
|
+
window.__MCP__.INTERACTIVE_SELECTOR = INTERACTIVE_SELECTOR;
|
|
138
189
|
})();
|