execbro 2.1.2 → 2.3.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 +4 -3
- package/build/__tests__/integration/text-input.test.d.ts +2 -0
- package/build/__tests__/integration/text-input.test.d.ts.map +1 -0
- package/build/__tests__/integration/text-input.test.js +181 -0
- package/build/__tests__/integration/text-input.test.js.map +1 -0
- package/build/__tests__/unit/focusedInputTools.test.js +19 -6
- package/build/__tests__/unit/focusedInputTools.test.js.map +1 -1
- package/build/__tests__/unit/inputTarget.test.d.ts +2 -0
- package/build/__tests__/unit/inputTarget.test.d.ts.map +1 -0
- package/build/__tests__/unit/inputTarget.test.js +144 -0
- package/build/__tests__/unit/inputTarget.test.js.map +1 -0
- package/build/__tests__/unit/inputTargetTools.test.d.ts +2 -0
- package/build/__tests__/unit/inputTargetTools.test.d.ts.map +1 -0
- package/build/__tests__/unit/inputTargetTools.test.js +46 -0
- package/build/__tests__/unit/inputTargetTools.test.js.map +1 -0
- package/build/__tests__/unit/inputTextTool.test.d.ts +2 -0
- package/build/__tests__/unit/inputTextTool.test.d.ts.map +1 -0
- package/build/__tests__/unit/inputTextTool.test.js +120 -0
- package/build/__tests__/unit/inputTextTool.test.js.map +1 -0
- package/build/__tests__/unit/keyboardMetrics.test.d.ts +2 -0
- package/build/__tests__/unit/keyboardMetrics.test.d.ts.map +1 -0
- package/build/__tests__/unit/keyboardMetrics.test.js +58 -0
- package/build/__tests__/unit/keyboardMetrics.test.js.map +1 -0
- package/build/__tests__/unit/keyboardRaise.test.d.ts +2 -0
- package/build/__tests__/unit/keyboardRaise.test.d.ts.map +1 -0
- package/build/__tests__/unit/keyboardRaise.test.js +74 -0
- package/build/__tests__/unit/keyboardRaise.test.js.map +1 -0
- package/build/__tests__/unit/nativeInputValue.test.d.ts +2 -0
- package/build/__tests__/unit/nativeInputValue.test.d.ts.map +1 -0
- package/build/__tests__/unit/nativeInputValue.test.js +67 -0
- package/build/__tests__/unit/nativeInputValue.test.js.map +1 -0
- package/build/__tests__/unit/screenSpace.test.d.ts +2 -0
- package/build/__tests__/unit/screenSpace.test.d.ts.map +1 -0
- package/build/__tests__/unit/screenSpace.test.js +58 -0
- package/build/__tests__/unit/screenSpace.test.js.map +1 -0
- package/build/__tests__/unit/screenStateInputValue.test.d.ts +2 -0
- package/build/__tests__/unit/screenStateInputValue.test.d.ts.map +1 -0
- package/build/__tests__/unit/screenStateInputValue.test.js +49 -0
- package/build/__tests__/unit/screenStateInputValue.test.js.map +1 -0
- package/build/__tests__/unit/screenStateKeyboard.test.d.ts +2 -0
- package/build/__tests__/unit/screenStateKeyboard.test.d.ts.map +1 -0
- package/build/__tests__/unit/screenStateKeyboard.test.js +99 -0
- package/build/__tests__/unit/screenStateKeyboard.test.js.map +1 -0
- package/build/__tests__/unit/textEntry.test.d.ts +2 -0
- package/build/__tests__/unit/textEntry.test.d.ts.map +1 -0
- package/build/__tests__/unit/textEntry.test.js +291 -0
- package/build/__tests__/unit/textEntry.test.js.map +1 -0
- package/build/__tests__/unit/visibility.test.js +21 -0
- package/build/__tests__/unit/visibility.test.js.map +1 -1
- package/build/core/focusedInputTools.d.ts.map +1 -1
- package/build/core/focusedInputTools.js +13 -35
- package/build/core/focusedInputTools.js.map +1 -1
- package/build/core/guides.d.ts.map +1 -1
- package/build/core/guides.js +18 -4
- package/build/core/guides.js.map +1 -1
- package/build/core/index.d.ts +1 -1
- package/build/core/index.d.ts.map +1 -1
- package/build/core/index.js +1 -1
- package/build/core/index.js.map +1 -1
- package/build/core/injected/visibility.d.ts +14 -1
- package/build/core/injected/visibility.d.ts.map +1 -1
- package/build/core/injected/visibility.js +28 -4
- package/build/core/injected/visibility.js.map +1 -1
- package/build/core/injectedFilters.d.ts +31 -0
- package/build/core/injectedFilters.d.ts.map +1 -0
- package/build/core/injectedFilters.js +31 -0
- package/build/core/injectedFilters.js.map +1 -0
- package/build/core/inputTarget.d.ts +85 -0
- package/build/core/inputTarget.d.ts.map +1 -0
- package/build/core/inputTarget.js +364 -0
- package/build/core/inputTarget.js.map +1 -0
- package/build/core/inputTargetTools.d.ts +12 -0
- package/build/core/inputTargetTools.d.ts.map +1 -0
- package/build/core/inputTargetTools.js +26 -0
- package/build/core/inputTargetTools.js.map +1 -0
- package/build/core/inspector.d.ts +6 -0
- package/build/core/inspector.d.ts.map +1 -1
- package/build/core/inspector.js +200 -36
- package/build/core/inspector.js.map +1 -1
- package/build/core/ios.d.ts.map +1 -1
- package/build/core/ios.js +7 -1
- package/build/core/ios.js.map +1 -1
- package/build/core/keyboardMetrics.d.ts +11 -0
- package/build/core/keyboardMetrics.d.ts.map +1 -0
- package/build/core/keyboardMetrics.js +40 -0
- package/build/core/keyboardMetrics.js.map +1 -0
- package/build/core/keyboardRaise.d.ts +22 -0
- package/build/core/keyboardRaise.d.ts.map +1 -0
- package/build/core/keyboardRaise.js +74 -0
- package/build/core/keyboardRaise.js.map +1 -0
- package/build/core/nativeInputValue.d.ts +48 -0
- package/build/core/nativeInputValue.d.ts.map +1 -0
- package/build/core/nativeInputValue.js +113 -0
- package/build/core/nativeInputValue.js.map +1 -0
- package/build/core/pressables.d.ts +0 -32
- package/build/core/pressables.d.ts.map +1 -1
- package/build/core/pressables.js +5 -1061
- package/build/core/pressables.js.map +1 -1
- package/build/core/screenLayout.d.ts +3 -0
- package/build/core/screenLayout.d.ts.map +1 -1
- package/build/core/screenLayout.js +32 -0
- package/build/core/screenLayout.js.map +1 -1
- package/build/core/screenSpace.d.ts +75 -0
- package/build/core/screenSpace.d.ts.map +1 -0
- package/build/core/screenSpace.js +84 -0
- package/build/core/screenSpace.js.map +1 -0
- package/build/core/screenSpaceDevice.d.ts +15 -0
- package/build/core/screenSpaceDevice.d.ts.map +1 -0
- package/build/core/screenSpaceDevice.js +24 -0
- package/build/core/screenSpaceDevice.js.map +1 -0
- package/build/core/screenState.d.ts +39 -0
- package/build/core/screenState.d.ts.map +1 -1
- package/build/core/screenState.js +200 -16
- package/build/core/screenState.js.map +1 -1
- package/build/core/textEntry.d.ts +75 -0
- package/build/core/textEntry.d.ts.map +1 -0
- package/build/core/textEntry.js +252 -0
- package/build/core/textEntry.js.map +1 -0
- package/build/tools/componentTools.d.ts.map +1 -1
- package/build/tools/componentTools.js +63 -188
- package/build/tools/componentTools.js.map +1 -1
- package/build/tools/interactionTools.d.ts +13 -0
- package/build/tools/interactionTools.d.ts.map +1 -1
- package/build/tools/interactionTools.js +112 -2
- package/build/tools/interactionTools.js.map +1 -1
- package/build/tools/screenshotTools.d.ts.map +1 -1
- package/build/tools/screenshotTools.js +37 -72
- package/build/tools/screenshotTools.js.map +1 -1
- package/package.json +1 -1
- package/tools.json +10 -4
- package/build/__tests__/unit/focusedInput.test.d.ts +0 -2
- package/build/__tests__/unit/focusedInput.test.d.ts.map +0 -1
- package/build/__tests__/unit/focusedInput.test.js +0 -58
- package/build/__tests__/unit/focusedInput.test.js.map +0 -1
- package/build/core/focusedInput.d.ts +0 -27
- package/build/core/focusedInput.d.ts.map +0 -1
- package/build/core/focusedInput.js +0 -110
- package/build/core/focusedInput.js.map +0 -1
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { resolveWrittenField } from "./nativeInputValue.js";
|
|
2
|
+
/**
|
|
3
|
+
* Names the likely cause of a mismatch. The field's own keyboard settings
|
|
4
|
+
* transform text on the way in, and "landed differently" alone sends the
|
|
5
|
+
* reader hunting for a bug in the write when the write was fine.
|
|
6
|
+
*/
|
|
7
|
+
export function diagnoseMismatch(sent, landed) {
|
|
8
|
+
if (landed === null)
|
|
9
|
+
return "";
|
|
10
|
+
if (landed !== sent && landed.toLowerCase() === sent.toLowerCase()) {
|
|
11
|
+
return " — only the capitalisation differs, which is the field's keyboard transforming input" +
|
|
12
|
+
" (RN TextInput defaults autoCapitalize to 'sentences'); set autoCapitalize=\"none\" to type verbatim";
|
|
13
|
+
}
|
|
14
|
+
if (landed.replace(/\s+/g, "") === sent.replace(/\s+/g, "")) {
|
|
15
|
+
return " — only whitespace differs, which is usually iOS smart punctuation or autocorrect spacing";
|
|
16
|
+
}
|
|
17
|
+
if (landed.length === sent.length && [...landed].sort().join("") === [...sent].sort().join("")) {
|
|
18
|
+
return " — the same characters arrived in a different order, which is the HID keystroke race;" +
|
|
19
|
+
" the retry already ran, so this one did not settle";
|
|
20
|
+
}
|
|
21
|
+
return "";
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Whether the HID driver can express this text at all.
|
|
25
|
+
*
|
|
26
|
+
* AXe types US-keyboard HID keycodes: A-Z, a-z, 0-9, space and the ASCII
|
|
27
|
+
* symbols. Nothing outside that map has a keycode to send — which rules out
|
|
28
|
+
* Cyrillic and CJK, and equally Spanish accents (á, ñ, ü), a case that is easy
|
|
29
|
+
* to miss because the text otherwise looks Latin. `adb shell input text` is no
|
|
30
|
+
* better: non-ASCII throws a NullPointerException inside InputShellCommand.
|
|
31
|
+
*/
|
|
32
|
+
export function isHidTypeable(text) {
|
|
33
|
+
// Printable ASCII plus tab/newline. Deliberately excludes the C1 range and
|
|
34
|
+
// anything above 0x7E.
|
|
35
|
+
return /^[\x20-\x7E\t\n]*$/.test(text);
|
|
36
|
+
}
|
|
37
|
+
function queryOf(a) {
|
|
38
|
+
const q = {};
|
|
39
|
+
if (a.testID != null)
|
|
40
|
+
q.testID = a.testID;
|
|
41
|
+
if (a.component != null)
|
|
42
|
+
q.component = a.component;
|
|
43
|
+
if (a.textMatch != null)
|
|
44
|
+
q.textMatch = a.textMatch;
|
|
45
|
+
if (a.index != null)
|
|
46
|
+
q.index = a.index;
|
|
47
|
+
return Object.keys(q).length > 0 ? q : undefined;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Resolve -> focus -> write -> read back -> retry once -> raise keyboard.
|
|
51
|
+
*
|
|
52
|
+
* The read-back comparison is EXACT. The corruption reproduced on device is a
|
|
53
|
+
* reorder (CASEB -> CSEBA), which every loose check — length, non-empty,
|
|
54
|
+
* prefix — passes. A success from this function means the field holds the
|
|
55
|
+
* requested string, or it says it could not confirm that.
|
|
56
|
+
*/
|
|
57
|
+
export async function enterText(args, deps) {
|
|
58
|
+
const q = queryOf(args);
|
|
59
|
+
// 1. Resolve. A miss is a hard failure — this is precisely where the old
|
|
60
|
+
// tools typed into the void and reported success.
|
|
61
|
+
const target = await deps.runOp({ kind: "find" }, q, args.device);
|
|
62
|
+
if (!target.found) {
|
|
63
|
+
return {
|
|
64
|
+
success: false,
|
|
65
|
+
error: target.reason,
|
|
66
|
+
...(target.ambiguous && { ambiguous: true }),
|
|
67
|
+
...(target.candidates && { candidates: target.candidates }),
|
|
68
|
+
...(target.totalInputs !== undefined && { totalInputs: target.totalInputs })
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
// 2. Focus it ourselves if needed. A tap reporting success does not
|
|
72
|
+
// guarantee React focus — reproduced on device.
|
|
73
|
+
if (!target.focused) {
|
|
74
|
+
const focused = await deps.runOp({ kind: "focus" }, q, args.device);
|
|
75
|
+
if (!focused.found)
|
|
76
|
+
return { success: false, error: focused.reason };
|
|
77
|
+
if (!focused.ok)
|
|
78
|
+
return { success: false, error: focused.via ?? "could not focus the input" };
|
|
79
|
+
}
|
|
80
|
+
// An uncontrolled field's current text is not in props, so `target.value` is
|
|
81
|
+
// always null for one. Taking that as "" made append silently behave as
|
|
82
|
+
// replace, and the retry then CLEARED the field to make the wrong answer
|
|
83
|
+
// verify clean. The accessibility tree is the only place the real previous
|
|
84
|
+
// text exists, so it has to be read before `desired` is computed.
|
|
85
|
+
let nativeBefore = [];
|
|
86
|
+
const usesNativeReadBack = !target.controlled && deps.readNativeFields !== undefined;
|
|
87
|
+
let previous = target.value ?? "";
|
|
88
|
+
let previousKnown = target.controlled;
|
|
89
|
+
if (usesNativeReadBack) {
|
|
90
|
+
nativeBefore = (await deps.readNativeFields()).fields;
|
|
91
|
+
const current = resolveWrittenField(nativeBefore, nativeBefore, target.testID);
|
|
92
|
+
if (current && current.text !== null) {
|
|
93
|
+
previous = current.text;
|
|
94
|
+
previousKnown = true;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const desired = args.replace ? args.text : previous + args.text;
|
|
98
|
+
// Three paths, chosen by what the field can actually honour:
|
|
99
|
+
//
|
|
100
|
+
// controlled -> onChangeText. The value prop mirrors the text,
|
|
101
|
+
// so this both writes and stays verifiable.
|
|
102
|
+
// uncontrolled + handler, ASCII -> real keystrokes. The most faithful
|
|
103
|
+
// simulation, and the retry now catches its
|
|
104
|
+
// keystroke race, so the racy path is only taken
|
|
105
|
+
// where it can actually succeed.
|
|
106
|
+
// uncontrolled + handler, non-ASCII -> setNativeProps AND a direct
|
|
107
|
+
// onChangeText call. HID cannot express these
|
|
108
|
+
// characters at all, so faithfulness is not on
|
|
109
|
+
// offer; firing the handler is what keeps the
|
|
110
|
+
// app from missing text the field displays.
|
|
111
|
+
// uncontrolled, no handler -> setNativeProps. Nothing to fire.
|
|
112
|
+
// `fromEmpty` says whether the field was just cleared. It matters only for
|
|
113
|
+
// HID, which appends at the caret rather than setting a value: typing the
|
|
114
|
+
// delta into a populated field is right, but typing it into a cleared one
|
|
115
|
+
// would lose whatever came before.
|
|
116
|
+
const write = async (fromEmpty) => {
|
|
117
|
+
if (target.controlled) {
|
|
118
|
+
const r = await deps.runOp({ kind: "setValue", value: desired }, q, args.device);
|
|
119
|
+
return { path: "react", ok: r.found && r.ok, error: r.found ? r.via : r.reason };
|
|
120
|
+
}
|
|
121
|
+
if (target.hasOnChangeText && isHidTypeable(args.text)) {
|
|
122
|
+
const r = await deps.typeHid(fromEmpty ? desired : args.text);
|
|
123
|
+
return { path: "hid", ok: r.success, error: r.error };
|
|
124
|
+
}
|
|
125
|
+
const r = await deps.runOp({ kind: "setNative", value: desired }, q, args.device);
|
|
126
|
+
return { path: "native", ok: r.found && r.ok, error: r.found ? r.via : r.reason };
|
|
127
|
+
};
|
|
128
|
+
// `undefined` from readBack means "could not determine" and must surface as
|
|
129
|
+
// unverified — never as a mismatch, which is what comparing against an
|
|
130
|
+
// always-null read produced before.
|
|
131
|
+
const readBack = async () => {
|
|
132
|
+
if (target.controlled) {
|
|
133
|
+
const r = await deps.runOp({ kind: "read" }, q, args.device);
|
|
134
|
+
return r.found ? r.value : null;
|
|
135
|
+
}
|
|
136
|
+
if (!usesNativeReadBack)
|
|
137
|
+
return undefined;
|
|
138
|
+
// A native write updates the view asynchronously, and the accessibility
|
|
139
|
+
// dump is a separate process — reading immediately captures the state
|
|
140
|
+
// from before the write. Poll until the text appears, then stop. Going
|
|
141
|
+
// through MCP hid this because the call latency happened to cover it.
|
|
142
|
+
const wait = deps.delay ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
143
|
+
let last;
|
|
144
|
+
for (const ms of [0, 250, 500]) {
|
|
145
|
+
if (ms > 0)
|
|
146
|
+
await wait(ms);
|
|
147
|
+
const after = await deps.readNativeFields();
|
|
148
|
+
if (after.error) {
|
|
149
|
+
last = undefined;
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
const hit = resolveWrittenField(nativeBefore, after.fields, target.testID);
|
|
153
|
+
last = hit ? hit.text : undefined;
|
|
154
|
+
if (last === desired)
|
|
155
|
+
return last;
|
|
156
|
+
}
|
|
157
|
+
return last;
|
|
158
|
+
};
|
|
159
|
+
// HID appends at the caret, so a replace must clear first or it concatenates.
|
|
160
|
+
const needsClearFirst = args.replace === true && !target.controlled && target.hasOnChangeText &&
|
|
161
|
+
isHidTypeable(args.text) && previous.length > 0;
|
|
162
|
+
if (needsClearFirst)
|
|
163
|
+
await deps.runOp({ kind: "clear" }, q, args.device);
|
|
164
|
+
const first = await write(needsClearFirst);
|
|
165
|
+
if (!first.ok) {
|
|
166
|
+
// A failed replace already cleared the field. Put it back rather than
|
|
167
|
+
// leaving the user's data destroyed with no mention of it.
|
|
168
|
+
if (args.replace && target.hasOnChangeText && previous.length > 0) {
|
|
169
|
+
const restored = await deps.runOp({ kind: "setValue", value: previous }, q, args.device);
|
|
170
|
+
return {
|
|
171
|
+
success: false,
|
|
172
|
+
path: first.path,
|
|
173
|
+
error: `${first.error ?? "write failed"} (previous value ${restored.found && restored.ok ? "restored" : "COULD NOT be restored"})`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
return { success: false, path: first.path, error: first.error ?? "write failed" };
|
|
177
|
+
}
|
|
178
|
+
let landed = await readBack();
|
|
179
|
+
// Could not read the field at all — say so instead of guessing either way.
|
|
180
|
+
if (landed === undefined) {
|
|
181
|
+
const keyboard = await deps.raise();
|
|
182
|
+
return {
|
|
183
|
+
success: true,
|
|
184
|
+
path: first.path,
|
|
185
|
+
verified: false,
|
|
186
|
+
keyboard,
|
|
187
|
+
error: target.controlled
|
|
188
|
+
? "the value could not be read back"
|
|
189
|
+
: "this field is uncontrolled and could not be located in the accessibility tree" +
|
|
190
|
+
(target.testID ? "" : " (it has no testID, and no single field's text changed)")
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
let retried = false;
|
|
194
|
+
if (landed !== desired) {
|
|
195
|
+
retried = true;
|
|
196
|
+
// Clear only for HID, which appends at the caret. The other paths set
|
|
197
|
+
// the whole value, so a clear is redundant — and actively harmful:
|
|
198
|
+
// publicInstance.clear() after HID typing races the setNativeProps that
|
|
199
|
+
// follows and wipes it, which made every non-ASCII retry land empty.
|
|
200
|
+
const clearedForRetry = first.path === "hid";
|
|
201
|
+
if (clearedForRetry) {
|
|
202
|
+
await deps.runOp({ kind: "clear" }, q, args.device);
|
|
203
|
+
}
|
|
204
|
+
else {
|
|
205
|
+
// A native write immediately after another input operation is
|
|
206
|
+
// sometimes swallowed — the value simply does not change. Give the
|
|
207
|
+
// previous operation a beat to settle before rewriting, rather than
|
|
208
|
+
// repeating the same call into the same contention.
|
|
209
|
+
const wait = deps.delay ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
210
|
+
await wait(300);
|
|
211
|
+
}
|
|
212
|
+
const second = await write(clearedForRetry);
|
|
213
|
+
if (!second.ok) {
|
|
214
|
+
return { success: false, path: second.path, retried, error: second.error ?? "rewrite failed" };
|
|
215
|
+
}
|
|
216
|
+
landed = await readBack();
|
|
217
|
+
}
|
|
218
|
+
if (landed !== desired) {
|
|
219
|
+
// A read that failed only on the retry is still "unconfirmed", not a
|
|
220
|
+
// mismatch — we have no evidence about what the field holds.
|
|
221
|
+
if (landed === undefined) {
|
|
222
|
+
const keyboard = await deps.raise();
|
|
223
|
+
return {
|
|
224
|
+
success: true, path: first.path, verified: false, retried, keyboard,
|
|
225
|
+
error: "the rewrite could not be read back, so the result is unconfirmed"
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
return {
|
|
229
|
+
success: false,
|
|
230
|
+
path: first.path,
|
|
231
|
+
verified: false,
|
|
232
|
+
retried,
|
|
233
|
+
sent: desired,
|
|
234
|
+
landed,
|
|
235
|
+
error: `text landed differently than it was sent${diagnoseMismatch(desired, landed)}` +
|
|
236
|
+
(!previousKnown && !args.replace
|
|
237
|
+
? " — note the field's prior text could not be read, so the appended result was predicted from an empty starting point; pass replace:true to write an exact value"
|
|
238
|
+
: "")
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// Last, and never fatal.
|
|
242
|
+
const keyboard = await deps.raise();
|
|
243
|
+
return {
|
|
244
|
+
success: true,
|
|
245
|
+
value: landed,
|
|
246
|
+
path: first.path,
|
|
247
|
+
verified: true,
|
|
248
|
+
...(retried && { retried }),
|
|
249
|
+
keyboard
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
//# sourceMappingURL=textEntry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"textEntry.js","sourceRoot":"","sources":["../../src/core/textEntry.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"componentTools.d.ts","sourceRoot":"","sources":["../../src/tools/componentTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"componentTools.d.ts","sourceRoot":"","sources":["../../src/tools/componentTools.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAsDzE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CAqkB9D"}
|
|
@@ -1,9 +1,28 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { registerToolWithTelemetry } from "../core/register.js";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { getComponentTree, getScreenLayout, getScreenState, formatScreenStateSummary, inspectComponent, findComponents, harvestStacksAtPoint, enrichWithSource, inspectAtPoint, measureComponent, metroMissingHintIfAbsent, hasMetro, getConnectedAppByDevice, getFirstConnectedApp, } from "../core/index.js";
|
|
4
|
+
import { screenStateToScreenSpace, toScreenSpaceY } from "../core/screenSpace.js";
|
|
5
|
+
import { resolveScreenSpaceMetrics } from "../core/screenSpaceDevice.js";
|
|
6
|
+
import { readKeyboardState } from "../core/keyboardMetrics.js";
|
|
5
7
|
import { primaryInteractionBanner } from "../core/toolHelpers.js";
|
|
6
8
|
import { DEVICE_ARG_DESC } from "./_deviceArg.js";
|
|
9
|
+
/**
|
|
10
|
+
* Top inset for whichever device a layout tool is about to answer for.
|
|
11
|
+
*
|
|
12
|
+
* A zero inset is the safe default: {@link toScreenSpaceY} treats it as "leave coordinates
|
|
13
|
+
* alone", so an unresolvable device degrades to the previous raw-fiber behaviour rather
|
|
14
|
+
* than to a wrongly shifted one.
|
|
15
|
+
*/
|
|
16
|
+
async function resolveScreenSpaceMetricsFor(device) {
|
|
17
|
+
const app = (device ? getConnectedAppByDevice(device) : getFirstConnectedApp()) ?? getFirstConnectedApp();
|
|
18
|
+
if (!app)
|
|
19
|
+
return { platform: "ios", topInset: 0 };
|
|
20
|
+
return resolveScreenSpaceMetrics({
|
|
21
|
+
platform: app.platform,
|
|
22
|
+
udid: app.simulatorUdid,
|
|
23
|
+
deviceId: app.adbSerial
|
|
24
|
+
});
|
|
25
|
+
}
|
|
7
26
|
function collectMetaNotes(r) {
|
|
8
27
|
const out = [];
|
|
9
28
|
if (r._meta?.reconnected) {
|
|
@@ -17,7 +36,7 @@ function collectMetaNotes(r) {
|
|
|
17
36
|
export function registerComponentTools(server) {
|
|
18
37
|
// Tool: Get full screen layout (all components with layout styles)
|
|
19
38
|
registerToolWithTelemetry(server, "get_screen_layout", {
|
|
20
|
-
description: "Get a screen map showing visible components as an indented tree with actual screen positions. Uses measureInWindow for real coordinates and filters out off-screen components. Returns meaningful component names with text content and frame data (x,y width x height). Coordinates are
|
|
39
|
+
description: "Get a screen map showing visible components as an indented tree with actual screen positions. Uses measureInWindow for real coordinates and filters out off-screen components. Returns meaningful component names with text content and frame data (x,y width x height). Coordinates are screen space — the same space screenshots, get_screen_state and tap() use, so pass them through unchanged. Use extended=true to include layout styles (padding, margin, flex, backgroundColor, etc.)." +
|
|
21
40
|
primaryInteractionBanner() + "\n" +
|
|
22
41
|
"PURPOSE: Quickest textual map of what is actually on screen right now — component names, positions, and text — so you can plan taps and inspections without guessing.\n" +
|
|
23
42
|
"WHEN TO USE: First step whenever the user asks \"what's on screen\", \"why is X covering Y\", or before tapping a visually ambiguous element.\n" +
|
|
@@ -52,7 +71,13 @@ export function registerComponentTools(server) {
|
|
|
52
71
|
};
|
|
53
72
|
}
|
|
54
73
|
const effectiveTimeoutMs = timeoutMs ?? (extended ? 15000 : 5000);
|
|
55
|
-
const result = await getScreenLayout({
|
|
74
|
+
const result = await getScreenLayout({
|
|
75
|
+
extended,
|
|
76
|
+
summary,
|
|
77
|
+
device,
|
|
78
|
+
timeoutMs: effectiveTimeoutMs,
|
|
79
|
+
screenSpace: await resolveScreenSpaceMetricsFor(device)
|
|
80
|
+
});
|
|
56
81
|
const metaNotes = collectMetaNotes(result);
|
|
57
82
|
if (!result.success) {
|
|
58
83
|
const errText = metaNotes.length > 0 ? `Error: ${result.error}\n\n${metaNotes.join("\n")}` : `Error: ${result.error}`;
|
|
@@ -139,129 +164,13 @@ export function registerComponentTools(server) {
|
|
|
139
164
|
: `React Component Tree:\n\n${result.result}`;
|
|
140
165
|
return { content: [{ type: "text", text: body }] };
|
|
141
166
|
});
|
|
142
|
-
// Tool: Get all pressable elements on screen
|
|
143
|
-
registerToolWithTelemetry(server, "get_pressable_elements", {
|
|
144
|
-
description: "Prefer get_screen_state after navigation (route + overlays + pressables in one call).\n\n" +
|
|
145
|
-
"Find all pressable (onPress) and input (TextInput) elements currently visible on screen. Returns component names, ready-to-tap center coordinates in SCREENSHOT PIXELS (same space as ios_screenshot/android_screenshot — pass directly to tap(x, y)), text labels, testID, accessibilityLabel, and a spatial nearbyText hint for icon-only buttons. Each element includes hasLabel (true if it contains text) and isInput (true for TextInput fields).\n" +
|
|
146
|
-
"HELPER — call before `tap` when you need to enumerate candidate elements before committing to a target; not a replacement for tap itself.\n" +
|
|
147
|
-
"PURPOSE: Produce a ready-to-tap inventory of every interactive element on screen with screenshot-pixel coordinates that tap(x, y) accepts directly.\n" +
|
|
148
|
-
"WHEN TO USE: Before tapping icon-only buttons, when text-based tap keeps failing, or to enumerate what the user can actually interact with.\n" +
|
|
149
|
-
"WORKFLOW: ios_screenshot / android_screenshot -> get_pressable_elements -> tap(testID=\"...\") or tap(x, y) using the center coordinates.\n" +
|
|
150
|
-
"LIMITATIONS: Visible-only (off-screen pressables are excluded). Requires a live React connection. Coordinates are in screenshot pixels — the tap tool converts to points internally.\n" +
|
|
151
|
-
"GOOD: get_pressable_elements()\n" +
|
|
152
|
-
"BAD: Calling when tap(testID=\"...\") already works — testID matching is faster and more stable.\n",
|
|
153
|
-
inputSchema: {
|
|
154
|
-
device: z.string().optional().describe(DEVICE_ARG_DESC)
|
|
155
|
-
}
|
|
156
|
-
}, async ({ device }) => {
|
|
157
|
-
const result = await getPressableElements({ device });
|
|
158
|
-
if (!result.success) {
|
|
159
|
-
return {
|
|
160
|
-
content: [
|
|
161
|
-
{
|
|
162
|
-
type: "text",
|
|
163
|
-
text: `Error: ${result.error}`
|
|
164
|
-
}
|
|
165
|
-
],
|
|
166
|
-
isError: true
|
|
167
|
-
};
|
|
168
|
-
}
|
|
169
|
-
const elements = result.parsedElements;
|
|
170
|
-
if (!elements || elements.length === 0) {
|
|
171
|
-
return {
|
|
172
|
-
content: [{ type: "text", text: result.result || "No pressable elements found." }]
|
|
173
|
-
};
|
|
174
|
-
}
|
|
175
|
-
// Resolve target app so we can pick the right simulator / android device
|
|
176
|
-
const targetApp = device ? getConnectedAppByDevice(device) : getFirstConnectedApp();
|
|
177
|
-
if (!targetApp) {
|
|
178
|
-
// No connected app — return raw (points) output; better than nothing
|
|
179
|
-
return {
|
|
180
|
-
content: [{ type: "text", text: result.result || "No pressable elements found." }]
|
|
181
|
-
};
|
|
182
|
-
}
|
|
183
|
-
// Capture a lightweight screenshot to learn scaleFactor (downscale) and dimensions.
|
|
184
|
-
// Conversion: screenshot_px = native_coord * devicePixelRatio / screenshotScale
|
|
185
|
-
let lines = [];
|
|
186
|
-
try {
|
|
187
|
-
if (targetApp.platform === "ios") {
|
|
188
|
-
const udid = targetApp.simulatorUdid;
|
|
189
|
-
const shot = await iosScreenshot(undefined, udid);
|
|
190
|
-
const screenshotScale = shot.scaleFactor || 1;
|
|
191
|
-
const devicePixelRatio = (shot.originalWidth && shot.originalHeight
|
|
192
|
-
? inferIOSDevicePixelRatio(shot.originalWidth, shot.originalHeight)
|
|
193
|
-
: null) ?? (await getDevicePixelRatio(udid)) ?? 3;
|
|
194
|
-
// Fallback to 59pt (iPhone typical) when the UI driver preflight can't
|
|
195
|
-
// resolve the true inset — matches ios_screenshot's default. Without this
|
|
196
|
-
// shift, react-native-screens modal-presented screens report y relative
|
|
197
|
-
// to content origin and taps land in the status bar instead of the button.
|
|
198
|
-
const safeAreaTop = (await getIOSSafeAreaTop(udid).catch(() => 0)) || 59;
|
|
199
|
-
// Keep the app's lastScreenshot metadata in sync so tap(x, y) uses the
|
|
200
|
-
// same scaleFactor when converting our pixel coords back to points.
|
|
201
|
-
if (shot.originalWidth && shot.originalHeight) {
|
|
202
|
-
targetApp.lastScreenshot = {
|
|
203
|
-
originalWidth: shot.originalWidth,
|
|
204
|
-
originalHeight: shot.originalHeight,
|
|
205
|
-
scaleFactor: screenshotScale
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
lines = formatPressablesInPixels(elements, {
|
|
209
|
-
platform: "ios",
|
|
210
|
-
devicePixelRatio,
|
|
211
|
-
screenshotScale,
|
|
212
|
-
safeAreaTop
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
else {
|
|
216
|
-
// Metro's `deviceName` is the device model (e.g. "sdk_gphone16k_arm64"),
|
|
217
|
-
// not the adb serial (e.g. "emulator-5554"), so passing it as -s makes
|
|
218
|
-
// adb miss the device and androidScreenshot/androidGetDensity silently
|
|
219
|
-
// return defaults (scale=1, density=160). That leaves coords in raw
|
|
220
|
-
// device pixels — off by ~1.2× from the screenshot/JPEG space the tool
|
|
221
|
-
// description promises. Pass undefined to let adb auto-pick (matches
|
|
222
|
-
// how android_screenshot works when called without deviceId). Multi-
|
|
223
|
-
// Android-device support tracks separately under the multi-device
|
|
224
|
-
// refactor; this path is a single-Android-device fix.
|
|
225
|
-
const shot = await androidScreenshot(undefined, undefined);
|
|
226
|
-
const screenshotScale = shot.scaleFactor || 1;
|
|
227
|
-
const density = await androidGetDensity(undefined).catch(() => ({ density: 160 }));
|
|
228
|
-
const devicePixelRatio = (density.density || 160) / 160;
|
|
229
|
-
if (shot.originalWidth && shot.originalHeight) {
|
|
230
|
-
targetApp.lastScreenshot = {
|
|
231
|
-
originalWidth: shot.originalWidth,
|
|
232
|
-
originalHeight: shot.originalHeight,
|
|
233
|
-
scaleFactor: screenshotScale
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
lines = formatPressablesInPixels(elements, {
|
|
237
|
-
platform: "android",
|
|
238
|
-
devicePixelRatio,
|
|
239
|
-
screenshotScale,
|
|
240
|
-
safeAreaTop: 0
|
|
241
|
-
});
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
catch {
|
|
245
|
-
// Fallback to points if screenshot/metadata unavailable
|
|
246
|
-
return {
|
|
247
|
-
content: [{ type: "text", text: result.result || "No pressable elements found." }]
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
const iconCount = elements.filter((e) => !e.hasLabel).length;
|
|
251
|
-
const labeledCount = elements.length - iconCount;
|
|
252
|
-
const summary = `Found ${elements.length} pressable elements (${iconCount} icon-only, ${labeledCount} with text labels)`;
|
|
253
|
-
const text = [summary, "", ...lines].join("\n");
|
|
254
|
-
return {
|
|
255
|
-
content: [{ type: "text", text }]
|
|
256
|
-
};
|
|
257
|
-
});
|
|
258
167
|
// Tool: Get current screen state — route, overlays, pressables (post-navigation checkpoint)
|
|
259
168
|
registerToolWithTelemetry(server, "get_screen_state", {
|
|
260
169
|
description: "Screenshot-free snapshot of the current screen: active route + params, blocking overlays (sheets, modals, alerts), and every on-screen element merged top-to-bottom within reachability groups. Call after any tap or navigation to orient before the next action. " +
|
|
261
170
|
"Each line carries an (x, y) center + frame bounds (so anything is a tap(x, y) target), typed by a leading marker: 🔘 pressable (with component JSX tag, label, testID, onPress hint), 📝 text, 🖼 image (with src/alt). " +
|
|
262
171
|
"Elements covered by an open overlay are grouped under 🚫 Blocked — visible for context, but taps will NOT reach them until the overlay closes. Long text truncates to 80 chars (fullText=true for full strings); pressablesOnly=true returns just the lean tappable list.\n\n" +
|
|
263
172
|
"WHEN TO USE: After every tap/swipe that may navigate, and to read screen content (prices, labels, which image loaded) without a screenshot+OCR round-trip.\n" +
|
|
264
|
-
"LIMITATIONS: route is null without React Navigation / Expo Router. Requires a live Metro connection
|
|
173
|
+
"LIMITATIONS: route is null without React Navigation / Expo Router. Requires a live Metro connection.\n" +
|
|
265
174
|
"SOURCE: this lists what is on screen, not where it lives in code — for the file:line that renders an element, call inspect_at_point(x, y).\n" +
|
|
266
175
|
"SEE ALSO: get_screen_layout for the full hierarchical component tree (deep inspection) — this gives a flat, tap-ready content list instead.",
|
|
267
176
|
inputSchema: {
|
|
@@ -277,7 +186,13 @@ export function registerComponentTools(server) {
|
|
|
277
186
|
isError: true
|
|
278
187
|
};
|
|
279
188
|
}
|
|
280
|
-
|
|
189
|
+
// In parallel: the keyboard read is a second CDP round trip and has
|
|
190
|
+
// no reason to queue behind the fiber walk. It degrades to
|
|
191
|
+
// { visible: false, error } on failure, so it cannot fail this call.
|
|
192
|
+
const [result, keyboard] = await Promise.all([
|
|
193
|
+
getScreenState({ device }),
|
|
194
|
+
readKeyboardState(device)
|
|
195
|
+
]);
|
|
281
196
|
const metaNotes = collectMetaNotes(result);
|
|
282
197
|
if (!result.success) {
|
|
283
198
|
const errText = metaNotes.length > 0
|
|
@@ -288,69 +203,18 @@ export function registerComponentTools(server) {
|
|
|
288
203
|
isError: true
|
|
289
204
|
};
|
|
290
205
|
}
|
|
291
|
-
|
|
292
|
-
|
|
206
|
+
// Normalise to screen space so a coordinate read here can be handed straight to
|
|
207
|
+
// tap()/inspect_at_point/a screenshot. Raw measureInWindow is not screen-relative
|
|
208
|
+
// (Android excludes the status bar; iOS modals measure from their own container),
|
|
209
|
+
// which is why point-space output used to disagree with screenshot pixels.
|
|
210
|
+
const rawSs = result.screenState;
|
|
211
|
+
const ss = rawSs ? screenStateToScreenSpace(rawSs, await resolveScreenSpaceMetricsFor(device)) : rawSs;
|
|
212
|
+
const summary = ss ? formatScreenStateSummary(ss, undefined, { pressablesOnly, fullText, keyboard }) : (result.result ?? "{}");
|
|
293
213
|
const body = metaNotes.length > 0
|
|
294
214
|
? `${summary}\n\n${metaNotes.join("\n")}`
|
|
295
215
|
: summary;
|
|
296
216
|
return { content: [{ type: "text", text: body }] };
|
|
297
217
|
});
|
|
298
|
-
function formatPressablesInPixels(elements, opts) {
|
|
299
|
-
const { platform, devicePixelRatio, screenshotScale, safeAreaTop } = opts;
|
|
300
|
-
const out = [];
|
|
301
|
-
for (let i = 0; i < elements.length; i++) {
|
|
302
|
-
const el = elements[i];
|
|
303
|
-
// react-native-screens modal presentations report y relative to content origin;
|
|
304
|
-
// shift into the window frame when the element's center sits inside the safe-area
|
|
305
|
-
// band. The shift MUST be applied to center and frame atomically — independent
|
|
306
|
-
// checks (cy<inset, fy<inset) drift apart on iPad-style insets where a header's
|
|
307
|
-
// center is just below the inset but its frame top is just above, producing
|
|
308
|
-
// center/frame.y values that don't satisfy center = frame.y + frame.h/2.
|
|
309
|
-
let cy = el.center.y;
|
|
310
|
-
let fy = el.frame.y;
|
|
311
|
-
if (platform === "ios" && safeAreaTop > 0 && cy < safeAreaTop) {
|
|
312
|
-
cy += safeAreaTop;
|
|
313
|
-
fy += safeAreaTop;
|
|
314
|
-
}
|
|
315
|
-
// iOS: fiber returns points → convert points × DPR / screenshotScale = JPEG px.
|
|
316
|
-
// Android: getPressableElements reconciles fiber DP against uiautomator device-pixel
|
|
317
|
-
// bounds (executor.ts, 2026-05-17). After reconciliation, coords are already in
|
|
318
|
-
// device pixels — only the JPEG downscale needs to be applied. Multiplying by DPR
|
|
319
|
-
// here would re-inflate them by ~density/160 (~2.6× on a 420dpi device), reproducing
|
|
320
|
-
// the original out-of-bounds bug.
|
|
321
|
-
const toPx = (v) => platform === "android"
|
|
322
|
-
? Math.round(v / screenshotScale)
|
|
323
|
-
: Math.round((v * devicePixelRatio) / screenshotScale);
|
|
324
|
-
const cx = toPx(el.center.x);
|
|
325
|
-
const cyPx = toPx(cy);
|
|
326
|
-
const fx = toPx(el.frame.x);
|
|
327
|
-
const fyPx = toPx(fy);
|
|
328
|
-
const fw = toPx(el.frame.width);
|
|
329
|
-
const fh = toPx(el.frame.height);
|
|
330
|
-
const num = i + 1;
|
|
331
|
-
const iconHint = !el.hasLabel ? iconLabel(el.component, el.icon) : null;
|
|
332
|
-
const label = el.hasLabel
|
|
333
|
-
? `"${el.text}"`
|
|
334
|
-
: iconHint
|
|
335
|
-
? `(${iconHint})`
|
|
336
|
-
: el.intent
|
|
337
|
-
? `(${el.intent} icon)`
|
|
338
|
-
: "(icon/image)";
|
|
339
|
-
const ids = [];
|
|
340
|
-
if (el.testID)
|
|
341
|
-
ids.push(`testID="${el.testID}"`);
|
|
342
|
-
if (el.accessibilityLabel)
|
|
343
|
-
ids.push(`a11y="${el.accessibilityLabel}"`);
|
|
344
|
-
const idStr = ids.length > 0 ? ` [${ids.join(", ")}]` : "";
|
|
345
|
-
const inputStr = el.isInput ? " (input)" : "";
|
|
346
|
-
const wrapperStr = el.isWrapper ? " [wrapper — skip unless dismissing keyboard]" : "";
|
|
347
|
-
const nearPart = el.nearbyText ? ` near "${el.nearbyText}"` : "";
|
|
348
|
-
out.push(`${num}. ${el.component} ${label}${nearPart} — center:(${cx},${cyPx}) frame:(${fx},${fyPx} ${fw}x${fh})${idStr}${inputStr}${wrapperStr}`);
|
|
349
|
-
if (el.path)
|
|
350
|
-
out.push(` path: ${el.path}`);
|
|
351
|
-
}
|
|
352
|
-
return out;
|
|
353
|
-
}
|
|
354
218
|
// Tool: Inspect a specific component by name
|
|
355
219
|
registerToolWithTelemetry(server, "inspect_component", {
|
|
356
220
|
description: "Inspect a specific React component by name. **DRILL-DOWN TOOL**: Use after get_screen_layout or find_components to identify which component to inspect. Returns props, style, state (hooks), and optionally children tree. Use childrenDepth to control how deep nested children go." +
|
|
@@ -492,16 +356,16 @@ export function registerComponentTools(server) {
|
|
|
492
356
|
description: "Inspect layout AND props at (x, y). Returns FRAME PER ANCESTOR (position/size in dp for every ancestor that hit-tested the point) + the innermost component's PROPS (handlers as [Function], refs, custom props like onPress/data/testID). Pure JS hit-test via fiber + measureInWindow — no overlay toggled, zero visual side effect. Works on Paper and Fabric.\n" +
|
|
493
357
|
"PURPOSE: Layout/props diagnosis — \"where is each ancestor positioned, and what props does the touched component expose?\"\n" +
|
|
494
358
|
"WHEN TO USE: A button is clipped, hit area is wrong, animated frame is unexpected — or you need handler/ref/non-style props. Also preferred for tight loops (no overlay flicker).\n" +
|
|
495
|
-
"WORKFLOW: screenshot
|
|
496
|
-
"LIMITATIONS:
|
|
359
|
+
"WORKFLOW: screenshot or get_screen_state → take the coordinate as-is → inspect_at_point(x, y).\n" +
|
|
360
|
+
"LIMITATIONS: Style is the node's own style object, not the merged cascade. `frame` is the element's own box; `hitFrame` (when present) is the innermost node actually under the point.\n" +
|
|
497
361
|
"SOURCE: also returns `source: {file, line, column}` for the component at the point, plus the owner chain as `Source ancestors` (set source=false to skip in tight loops).\n",
|
|
498
362
|
inputSchema: {
|
|
499
363
|
x: z
|
|
500
364
|
.number()
|
|
501
|
-
.describe("X coordinate in
|
|
365
|
+
.describe("X coordinate in screen space — take it straight from a screenshot, get_screen_state or get_screen_layout. No conversion."),
|
|
502
366
|
y: z
|
|
503
367
|
.number()
|
|
504
|
-
.describe("Y coordinate in
|
|
368
|
+
.describe("Y coordinate in screen space — take it straight from a screenshot, get_screen_state or get_screen_layout. No conversion."),
|
|
505
369
|
includeProps: z
|
|
506
370
|
.boolean()
|
|
507
371
|
.optional()
|
|
@@ -520,7 +384,12 @@ export function registerComponentTools(server) {
|
|
|
520
384
|
.describe("Resolve the component's source file and line via Metro symbolication. Default true. Set false to skip in tight loops.")
|
|
521
385
|
}
|
|
522
386
|
}, async ({ x, y, includeProps, includeFrame, device, source = true }) => {
|
|
523
|
-
const result = await inspectAtPoint(x, y, {
|
|
387
|
+
const result = await inspectAtPoint(x, y, {
|
|
388
|
+
includeProps,
|
|
389
|
+
includeFrame,
|
|
390
|
+
device,
|
|
391
|
+
screenSpace: await resolveScreenSpaceMetricsFor(device)
|
|
392
|
+
});
|
|
524
393
|
if (!result.success) {
|
|
525
394
|
return {
|
|
526
395
|
content: [
|
|
@@ -588,7 +457,7 @@ export function registerComponentTools(server) {
|
|
|
588
457
|
});
|
|
589
458
|
// Tool: Measure on-screen geometry of a named component
|
|
590
459
|
registerToolWithTelemetry(server, "measure", {
|
|
591
|
-
description: "Get on-screen geometry {x, y, width, height} for a named React component instance. Calls measureInWindow on the matched fiber (or its nearest host descendant for composite components). Coordinates are
|
|
460
|
+
description: "Get on-screen geometry {x, y, width, height} for a named React component instance. Calls measureInWindow on the matched fiber (or its nearest host descendant for composite components). Coordinates are screen space, the same space as get_screen_layout, get_screen_state, inspect_at_point and tap().\n" +
|
|
592
461
|
"PURPOSE: One-shot, name-based component measurement — avoids hand-rolling fiber walks and Promise-wrapping measureInWindow callbacks in execute_in_app.\n" +
|
|
593
462
|
"WHEN TO USE: You already know the component's display name (from get_screen_layout or find_components) and just need its current bounds — e.g. to verify a layout change, compute a tap target, or compare against design specs.\n" +
|
|
594
463
|
"WORKFLOW: find_components(pattern=\"...\") -> measure(componentName=\"...\", index=N) -> tap(x, y) at the center, or inspect_at_point at the center to verify identity.\n" +
|
|
@@ -626,10 +495,16 @@ export function registerComponentTools(server) {
|
|
|
626
495
|
_errorContext: result.outcome
|
|
627
496
|
};
|
|
628
497
|
}
|
|
498
|
+
// measure returns a single leaf box, so the band rule applies directly — there is
|
|
499
|
+
// no root container here to exempt. Without this the tool's own promise of "the
|
|
500
|
+
// same space as get_screen_layout and inspect_at_point" would be false: off by the
|
|
501
|
+
// status bar on every Android screen, and by the safe area inside an iOS modal.
|
|
502
|
+
const measureMetrics = await resolveScreenSpaceMetricsFor(device);
|
|
503
|
+
const measuredY = toScreenSpaceY(result.y, measureMetrics);
|
|
629
504
|
const lines = [
|
|
630
505
|
`Component: ${result.name}`,
|
|
631
|
-
`Frame: (${result.x.toFixed(1)}, ${
|
|
632
|
-
`Center: (${(result.x + result.width / 2).toFixed(1)}, ${(
|
|
506
|
+
`Frame: (${result.x.toFixed(1)}, ${measuredY.toFixed(1)}) ${result.width.toFixed(1)}x${result.height.toFixed(1)}`,
|
|
507
|
+
`Center: (${(result.x + result.width / 2).toFixed(1)}, ${(measuredY + result.height / 2).toFixed(1)})`,
|
|
633
508
|
];
|
|
634
509
|
if (typeof result.nativeTag === "number") {
|
|
635
510
|
lines.push(`nativeTag: ${result.nativeTag}`);
|