@avasapp/agent-bridge 0.3.0 → 0.4.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 +149 -11
- package/dist/adapters/ably.cjs +22 -4
- package/dist/adapters/ably.d.cts +8 -1
- package/dist/adapters/expo-router.cjs +22 -1
- package/dist/adapters/expo-router.d.cts +5 -1
- package/dist/adapters/react-native-mmkv.cjs +1 -0
- package/dist/adapters/react-native-mmkv.d.cts +1 -1
- package/dist/adapters/react-native-webview.cjs +672 -0
- package/dist/adapters/react-native-webview.d.cts +79 -0
- package/dist/adapters/socket-io.cjs +3 -1
- package/dist/adapters/socket-io.d.cts +1 -1
- package/dist/adapters/tanstack-query.cjs +256 -17
- package/dist/adapters/tanstack-query.d.cts +1 -1
- package/dist/adapters/zustand.cjs +152 -12
- package/dist/adapters/zustand.d.cts +19 -3
- package/dist/{chunk-S2FYN2R5.js → chunk-AJD3XTNT.js} +223 -57
- package/dist/cli.js +817 -56
- package/dist/client/index.cjs +218 -56
- package/dist/client/index.d.ts +20 -2
- package/dist/client/index.js +1 -1
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +254 -51
- package/dist/network/index.d.cts +9 -2
- package/dist/noop/index.cjs +3 -0
- package/dist/noop/react-native-webview.cjs +31 -0
- package/dist/realtime/index.cjs +2 -1
- package/dist/realtime/index.d.cts +3 -1
- package/dist/runtime/index.cjs +860 -100
- package/dist/runtime/index.d.cts +31 -5
- package/dist/{types-6kKQsLC1.d.cts → types-BmpykP1P.d.cts} +23 -1
- package/entries/react-native-webview.cjs +9 -0
- package/package.json +17 -6
- package/skills/agent-bridge/SKILL.md +196 -5
package/dist/runtime/index.cjs
CHANGED
|
@@ -23,13 +23,14 @@ __export(runtime_exports, {
|
|
|
23
23
|
RUNTIME_MARKER: () => RUNTIME_MARKER,
|
|
24
24
|
cdpTransport: () => cdpTransport,
|
|
25
25
|
createGate: () => createGate,
|
|
26
|
+
onRestore: () => onRestore,
|
|
26
27
|
settle: () => settle,
|
|
27
28
|
startAgentBridge: () => startAgentBridge,
|
|
28
29
|
useAgentBridge: () => useAgentBridge
|
|
29
30
|
});
|
|
30
31
|
module.exports = __toCommonJS(runtime_exports);
|
|
31
32
|
var import_react = require("react");
|
|
32
|
-
var
|
|
33
|
+
var import_react_native3 = require("react-native");
|
|
33
34
|
|
|
34
35
|
// src/shared/protocol.ts
|
|
35
36
|
var PROTOCOL_VERSION = 1;
|
|
@@ -43,6 +44,73 @@ function toAsciiJson(value) {
|
|
|
43
44
|
);
|
|
44
45
|
}
|
|
45
46
|
|
|
47
|
+
// src/runtime/builtin-tools.ts
|
|
48
|
+
var import_react_native2 = require("react-native");
|
|
49
|
+
|
|
50
|
+
// src/runtime/undo.ts
|
|
51
|
+
var undos = [];
|
|
52
|
+
function onRestore(fn, label) {
|
|
53
|
+
if (typeof fn !== "function") throw new Error("onRestore takes a function");
|
|
54
|
+
undos.push({ fn, label });
|
|
55
|
+
}
|
|
56
|
+
function pendingUndos() {
|
|
57
|
+
return undos.map((u) => u.label ?? "undo").reverse();
|
|
58
|
+
}
|
|
59
|
+
async function runUndos() {
|
|
60
|
+
const batch = undos.splice(0).reverse();
|
|
61
|
+
const errors = [];
|
|
62
|
+
for (const { fn, label } of batch) {
|
|
63
|
+
try {
|
|
64
|
+
await fn();
|
|
65
|
+
} catch (error) {
|
|
66
|
+
const why = error instanceof Error ? error.message : String(error);
|
|
67
|
+
errors.push(label ? `${label}: ${why}` : why);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return { ran: batch.length, errors };
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// src/runtime/tools/app.ts
|
|
74
|
+
function selectReload(host, devSettingsReload) {
|
|
75
|
+
return () => {
|
|
76
|
+
const expo = host.expo;
|
|
77
|
+
if (typeof expo?.reloadAppAsync !== "function") return devSettingsReload();
|
|
78
|
+
try {
|
|
79
|
+
Promise.resolve(expo.reloadAppAsync("agent-bridge app.reload")).catch(
|
|
80
|
+
() => devSettingsReload()
|
|
81
|
+
);
|
|
82
|
+
} catch {
|
|
83
|
+
devSettingsReload();
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
function appTools(reload) {
|
|
88
|
+
return {
|
|
89
|
+
"app.reload": {
|
|
90
|
+
description: "Reload the app's JS (Expo's reloadAppAsync, else DevSettings.reload). Pending restores (store, query, mocks, pins) are lost. Returns before it reloads; a session waits for the new bridge and reports what was lost.",
|
|
91
|
+
run: () => {
|
|
92
|
+
setTimeout(reload, 50);
|
|
93
|
+
return { reloading: true };
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
"app.restore": {
|
|
97
|
+
maxArgs: 0,
|
|
98
|
+
// The labels, so bridge.pending shows what the app's own tools changed.
|
|
99
|
+
pending: () => {
|
|
100
|
+
const labels = pendingUndos();
|
|
101
|
+
return labels.length ? labels : false;
|
|
102
|
+
},
|
|
103
|
+
description: "Undo what the app's own tools changed: runs the undos they registered with onRestore, newest first, once. Returns how many ran.",
|
|
104
|
+
run: async () => {
|
|
105
|
+
const { ran, errors } = await runUndos();
|
|
106
|
+
if (errors.length)
|
|
107
|
+
throw new Error(`${errors.join("; ")} (${ran - errors.length} of ${ran} undone)`);
|
|
108
|
+
return { undone: ran };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
46
114
|
// src/runtime/tools/bridge.ts
|
|
47
115
|
function bridgeTools(listTools) {
|
|
48
116
|
return {
|
|
@@ -57,6 +125,42 @@ function bridgeTools(listTools) {
|
|
|
57
125
|
};
|
|
58
126
|
}
|
|
59
127
|
|
|
128
|
+
// src/runtime/tools/capture.ts
|
|
129
|
+
var MAX_BASE64_CHARS = 2e5;
|
|
130
|
+
var UNAVAILABLE = "screen.capture needs react-native-view-shot, which this app does not have. Take the screenshot with agent-device, `xcrun simctl io booted screenshot <file>` (iOS simulator) or `adb exec-out screencap -p > <file>` (Android) instead.";
|
|
131
|
+
function loadViewShot() {
|
|
132
|
+
try {
|
|
133
|
+
const mod = require("react-native-view-shot");
|
|
134
|
+
return typeof mod?.captureScreen === "function" ? mod : void 0;
|
|
135
|
+
} catch {
|
|
136
|
+
return void 0;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function captureTools(load) {
|
|
140
|
+
return {
|
|
141
|
+
"screen.capture": {
|
|
142
|
+
description: "Screenshot the app as a PNG. Returns {path} of a file on the device. `{base64:true}` returns {base64} instead when small enough (else {path}). Only works if the app already has react-native-view-shot; otherwise errors and you use agent-device, simctl or adb.",
|
|
143
|
+
maxArgs: 1,
|
|
144
|
+
run: async (options) => {
|
|
145
|
+
const viewShot = load();
|
|
146
|
+
if (!viewShot) throw new Error(UNAVAILABLE);
|
|
147
|
+
if (options?.base64) {
|
|
148
|
+
const base64 = await viewShot.captureScreen({
|
|
149
|
+
format: "png",
|
|
150
|
+
result: "base64"
|
|
151
|
+
});
|
|
152
|
+
if (base64.length <= MAX_BASE64_CHARS) return { base64 };
|
|
153
|
+
}
|
|
154
|
+
const path2 = await viewShot.captureScreen({
|
|
155
|
+
format: "png",
|
|
156
|
+
result: "tmpfile"
|
|
157
|
+
});
|
|
158
|
+
return { path: path2 };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
60
164
|
// src/runtime/tools/logs.ts
|
|
61
165
|
function logTools(logs) {
|
|
62
166
|
return {
|
|
@@ -75,8 +179,28 @@ function logTools(logs) {
|
|
|
75
179
|
var SUFFIX = ".restore";
|
|
76
180
|
var SELF = "bridge.restore";
|
|
77
181
|
var LAST = "scenario.restore";
|
|
182
|
+
function pendingByArea(tools, detail = false) {
|
|
183
|
+
const out = {};
|
|
184
|
+
for (const [name, d] of Object.entries(tools)) {
|
|
185
|
+
if (!name.endsWith(SUFFIX) || name === SELF || typeof d === "function")
|
|
186
|
+
continue;
|
|
187
|
+
const area3 = name.slice(0, -SUFFIX.length);
|
|
188
|
+
try {
|
|
189
|
+
const found = d.pending?.({ detail });
|
|
190
|
+
if (found) out[area3] = found;
|
|
191
|
+
} catch (error) {
|
|
192
|
+
out[area3] = `pending check failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
return out;
|
|
196
|
+
}
|
|
78
197
|
function restoreTools(getTools) {
|
|
79
198
|
return {
|
|
199
|
+
"bridge.pending": {
|
|
200
|
+
maxArgs: 0,
|
|
201
|
+
description: "What bridge.restore would undo right now, per area (store, query, net, app, ...): the value is true, or detail such as a store's snapshot against its current value. Empty when nothing is pending. Changes nothing.",
|
|
202
|
+
run: () => pendingByArea(getTools(), true)
|
|
203
|
+
},
|
|
80
204
|
[SELF]: {
|
|
81
205
|
description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
|
|
82
206
|
run: async () => {
|
|
@@ -161,7 +285,7 @@ function checkOptions(schema, options) {
|
|
|
161
285
|
// src/runtime/tools/scenario.ts
|
|
162
286
|
var active = /* @__PURE__ */ new Map();
|
|
163
287
|
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
164
|
-
async function
|
|
288
|
+
async function runUndos2(entry) {
|
|
165
289
|
const errors = [];
|
|
166
290
|
for (const undo2 of entry.undos.splice(0).reverse()) {
|
|
167
291
|
try {
|
|
@@ -176,7 +300,7 @@ async function undo(name) {
|
|
|
176
300
|
const entry = active.get(name);
|
|
177
301
|
if (!entry) return [];
|
|
178
302
|
active.delete(name);
|
|
179
|
-
return (await
|
|
303
|
+
return (await runUndos2(entry)).map((e) => `${name}: ${e}`);
|
|
180
304
|
}
|
|
181
305
|
function scenarioTools(getScenarios, getTools) {
|
|
182
306
|
const find = (name) => {
|
|
@@ -241,6 +365,7 @@ scenario.list shows its options schema.`
|
|
|
241
365
|
}
|
|
242
366
|
},
|
|
243
367
|
"scenario.restore": {
|
|
368
|
+
pending: () => active.size > 0,
|
|
244
369
|
description: "Undo every active scenario, newest first. bridge.restore runs this after every other restorer.",
|
|
245
370
|
run: async () => {
|
|
246
371
|
const names = [...active.keys()].reverse();
|
|
@@ -296,40 +421,101 @@ function textOf(fiber) {
|
|
|
296
421
|
}
|
|
297
422
|
return null;
|
|
298
423
|
}
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
424
|
+
|
|
425
|
+
// src/runtime/screen/webview-mark.ts
|
|
426
|
+
var MARK = /^\/\*agent-bridge:webview:([^*]*)\*\//;
|
|
427
|
+
function webViewNameOf(props) {
|
|
428
|
+
const script = props.injectedJavaScriptBeforeContentLoaded;
|
|
429
|
+
const found = typeof script === "string" ? MARK.exec(script) : null;
|
|
430
|
+
if (!found) return void 0;
|
|
431
|
+
try {
|
|
432
|
+
return decodeURIComponent(found[1]);
|
|
433
|
+
} catch {
|
|
434
|
+
return found[1];
|
|
304
435
|
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// src/runtime/screen/elements.ts
|
|
439
|
+
var propsOf = (fiber) => fiber.memoizedProps && typeof fiber.memoizedProps === "object" ? fiber.memoizedProps : null;
|
|
440
|
+
var nodeOf = (fiber) => {
|
|
441
|
+
const n = fiber.stateNode;
|
|
442
|
+
return n && typeof n === "object" ? n : null;
|
|
443
|
+
};
|
|
444
|
+
var SCROLL_METHODS = ["scrollTo", "scrollToOffset"];
|
|
445
|
+
function scrolls(fiber) {
|
|
446
|
+
const node = nodeOf(fiber);
|
|
447
|
+
if (!node || !SCROLL_METHODS.some((m) => isFn(node[m]))) return false;
|
|
448
|
+
if (typeof node.scrollHeight === "number" && typeof node.clientHeight === "number")
|
|
449
|
+
return node.scrollHeight > node.clientHeight + 1 || node.scrollWidth > node.clientWidth + 1;
|
|
305
450
|
return true;
|
|
306
451
|
}
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
if (found && (options.exact ? found.text === text : found.text.includes(text))) {
|
|
314
|
-
const rect = found.host ? measureHost(found.host) : null;
|
|
315
|
-
const onScreen = !!rect && inActiveScreen(fiber) && rect.width > 0 && rect.height > 0 && rect.x < window.width && rect.y < window.height && rect.x + rect.width > 0 && rect.y + rect.height > 0;
|
|
316
|
-
matches.push({ text: found.text, rect, onScreen });
|
|
452
|
+
var displayNone = (style) => displayOf(style) === "none";
|
|
453
|
+
function displayOf(style) {
|
|
454
|
+
if (Array.isArray(style)) {
|
|
455
|
+
for (let i = style.length - 1; i >= 0; i--) {
|
|
456
|
+
const d = displayOf(style[i]);
|
|
457
|
+
if (d !== void 0) return d;
|
|
317
458
|
}
|
|
318
|
-
|
|
319
|
-
|
|
459
|
+
return void 0;
|
|
460
|
+
}
|
|
461
|
+
return style && typeof style === "object" ? style.display : void 0;
|
|
462
|
+
}
|
|
463
|
+
var hiddenBy = (p) => p.activityState === 0 || displayNone(p.style) || p["aria-hidden"] === true || p.accessibilityElementsHidden === true || p.importantForAccessibility === "no-hide-descendants";
|
|
464
|
+
var MODAL_HOSTS = /* @__PURE__ */ new Set(["RCTModalHostView", "ModalHostView"]);
|
|
465
|
+
var OVERLAY_HOST = "RNSFullWindowOverlay";
|
|
466
|
+
var SCREEN_STACK = "RNSScreenStack";
|
|
467
|
+
var STACK_SCREENS = /* @__PURE__ */ new Set(["RNSScreen", "RNSModalScreen"]);
|
|
468
|
+
var SEE_THROUGH = /* @__PURE__ */ new Set(["transparentModal", "containedTransparentModal", "overFullScreen"]);
|
|
469
|
+
var undimmedSheet = (p) => {
|
|
470
|
+
const at = p.sheetLargestUndimmedDetentIndex;
|
|
471
|
+
return p.stackPresentation === "formSheet" && at !== void 0 && at !== null && at !== "none" && at !== -1;
|
|
472
|
+
};
|
|
473
|
+
function presentationOf(screen) {
|
|
474
|
+
const p = propsOf(screen);
|
|
475
|
+
if (typeof p?.stackPresentation === "string") return p.stackPresentation;
|
|
476
|
+
return screen.type === "RNSModalScreen" ? "modal" : null;
|
|
477
|
+
}
|
|
478
|
+
var seeThrough = (screen) => {
|
|
479
|
+
const presentation = presentationOf(screen);
|
|
480
|
+
return presentation !== null && SEE_THROUGH.has(presentation) || undimmedSheet(propsOf(screen) ?? {});
|
|
481
|
+
};
|
|
482
|
+
var coversWindow = (screen) => {
|
|
483
|
+
const presentation = presentationOf(screen);
|
|
484
|
+
return presentation !== null && presentation !== "push" && !undimmedSheet(propsOf(screen) ?? {});
|
|
485
|
+
};
|
|
486
|
+
function stackScreens(stack) {
|
|
487
|
+
const screens = [];
|
|
488
|
+
const todo = stack.child ? [stack.child] : [];
|
|
489
|
+
while (todo.length) {
|
|
490
|
+
const f = todo.pop();
|
|
491
|
+
if (f.sibling) todo.push(f.sibling);
|
|
492
|
+
if (f.tag === HOST_COMPONENT && STACK_SCREENS.has(f.type)) {
|
|
493
|
+
if (propsOf(f)?.activityState !== 0) screens.push(f);
|
|
494
|
+
} else if (f.child) todo.push(f.child);
|
|
320
495
|
}
|
|
496
|
+
let base = screens.length - 1;
|
|
497
|
+
while (base > 0 && seeThrough(screens[base])) base--;
|
|
321
498
|
return {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
matches
|
|
499
|
+
covered: screens.slice(0, Math.max(0, base)),
|
|
500
|
+
presented: screens.slice(1).filter(coversWindow)
|
|
325
501
|
};
|
|
326
502
|
}
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
503
|
+
function hostAbove(host) {
|
|
504
|
+
let up = host.return;
|
|
505
|
+
while (up && up.tag !== HOST_COMPONENT) up = up.return;
|
|
506
|
+
return up ?? host;
|
|
507
|
+
}
|
|
508
|
+
var REFRESH_HOST = /refresh/i;
|
|
509
|
+
var refreshTwin = (rec, host) => rec && REFRESH_HOST.test(String(rec.host.type)) && hostAbove(host) === rec.host ? rec : null;
|
|
510
|
+
var inside = (modal, top) => {
|
|
511
|
+
for (let m = modal; m; m = m.up) if (m === top) return true;
|
|
512
|
+
return false;
|
|
513
|
+
};
|
|
514
|
+
var SWITCH_HOSTS = /* @__PURE__ */ new Set(["RCTSwitch", "AndroidSwitch"]);
|
|
330
515
|
var isFn = (v) => typeof v === "function";
|
|
331
|
-
var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && ("value" in p || "defaultValue" in p || "placeholder" in p);
|
|
332
|
-
var
|
|
516
|
+
var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && typeof p.value !== "boolean" && ("value" in p || "defaultValue" in p || "placeholder" in p);
|
|
517
|
+
var labelOf = (p) => p.accessibilityLabel ?? p["aria-label"];
|
|
518
|
+
var interesting = (p) => isFn(p.onPress) || isFn(p.onValueChange) || isInputProps(p) || typeof p.testID === "string" || typeof labelOf(p) === "string" || webViewNameOf(p) !== void 0;
|
|
333
519
|
var ICON_GLYPHS = /[\uE000-\uF8FF]/g;
|
|
334
520
|
function firstHost(fiber) {
|
|
335
521
|
if (fiber.tag === HOST_COMPONENT) return fiber;
|
|
@@ -350,22 +536,77 @@ function absorb(rec, fiber, p) {
|
|
|
350
536
|
if (e[key] === void 0 && typeof v === "string") e[key] = v;
|
|
351
537
|
};
|
|
352
538
|
fill("testID", p.testID);
|
|
353
|
-
fill("label", p
|
|
539
|
+
fill("label", labelOf(p));
|
|
354
540
|
fill("role", p.accessibilityRole ?? p.role);
|
|
355
541
|
fill("placeholder", p.placeholder);
|
|
356
|
-
|
|
542
|
+
const webview = webViewNameOf(p);
|
|
543
|
+
if (e.webview === void 0 && webview !== void 0) e.webview = webview;
|
|
544
|
+
const isSwitch = SWITCH_HOSTS.has(fiber.type) || (p.accessibilityRole ?? p.role ?? e.role) === "switch";
|
|
545
|
+
if (isSwitch && typeof p.value === "boolean") {
|
|
546
|
+
if (e.checked === void 0) e.checked = p.value;
|
|
547
|
+
if (e.role === void 0) e.role = "switch";
|
|
548
|
+
} else if (e.value === void 0 && !(typeof p.value === "boolean" && isFn(p.onValueChange)) && ("value" in p || "defaultValue" in p)) {
|
|
357
549
|
const v = p.value ?? p.defaultValue;
|
|
358
|
-
if (v != null) e.value = String(v);
|
|
550
|
+
if (v != null && typeof v !== "object" && !isFn(v)) e.value = String(v);
|
|
359
551
|
}
|
|
360
552
|
if (rec.maxLength === void 0 && typeof p.maxLength === "number")
|
|
361
553
|
rec.maxLength = p.maxLength;
|
|
362
554
|
if (p.editable === false || p.readOnly === true) e.editable = false;
|
|
363
555
|
const state = p.accessibilityState;
|
|
556
|
+
const checked = state?.checked ?? p["aria-checked"];
|
|
557
|
+
if (e.checked === void 0 && (typeof checked === "boolean" || checked === "mixed"))
|
|
558
|
+
e.checked = checked;
|
|
559
|
+
const flag = (key) => {
|
|
560
|
+
const v = state?.[key] ?? p[`aria-${key}`];
|
|
561
|
+
if (e[key] === void 0 && typeof v === "boolean") e[key] = v;
|
|
562
|
+
};
|
|
563
|
+
flag("selected");
|
|
564
|
+
flag("expanded");
|
|
364
565
|
if (p.disabled === true || state?.disabled === true || p["aria-disabled"] === true)
|
|
365
566
|
e.disabled = true;
|
|
366
567
|
}
|
|
367
|
-
|
|
368
|
-
|
|
568
|
+
var intersect = (a, b) => {
|
|
569
|
+
const x = Math.max(a.x, b.x);
|
|
570
|
+
const y = Math.max(a.y, b.y);
|
|
571
|
+
const right = Math.min(a.x + a.width, b.x + b.width);
|
|
572
|
+
const bottom = Math.min(a.y + a.height, b.y + b.height);
|
|
573
|
+
return right > x && bottom > y ? { x, y, width: right - x, height: bottom - y } : null;
|
|
574
|
+
};
|
|
575
|
+
var usable = (r) => !!r && r.width > 0 && r.height > 0;
|
|
576
|
+
var within = (host, ancestor) => {
|
|
577
|
+
for (let f = host; f; f = f.return) if (f === ancestor) return true;
|
|
578
|
+
return false;
|
|
579
|
+
};
|
|
580
|
+
function visibleArea(base, host, bars, window) {
|
|
581
|
+
let area3 = base;
|
|
582
|
+
const clip2 = (r) => {
|
|
583
|
+
area3 = area3 && intersect(area3, r);
|
|
584
|
+
};
|
|
585
|
+
for (const bar of bars) {
|
|
586
|
+
if (within(host, bar)) continue;
|
|
587
|
+
const r = measureHost(bar);
|
|
588
|
+
if (!usable(r) || r.width < window.width * 0.9) continue;
|
|
589
|
+
clip2(
|
|
590
|
+
r.y + r.height / 2 > window.height / 2 ? { x: -1e9, y: -1e9, width: 2e9, height: 1e9 + r.y } : { x: -1e9, y: r.y + r.height, width: 2e9, height: 1e9 }
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
return area3;
|
|
594
|
+
}
|
|
595
|
+
var viewportOf = (s, view, window) => usable(view) ? visibleArea(view, s.host, s.modal ? [] : s.bars, window) : null;
|
|
596
|
+
function isOnScreen(rec, rect, window, bars) {
|
|
597
|
+
if (!usable(rect)) return false;
|
|
598
|
+
const shown = intersect(rect, { x: 0, y: 0, ...window });
|
|
599
|
+
if (!shown) return false;
|
|
600
|
+
const cx = shown.x + shown.width / 2;
|
|
601
|
+
const cy = shown.y + shown.height / 2;
|
|
602
|
+
const holds = (r) => !!r && cx >= r.x && cx <= r.x + r.width && cy >= r.y && cy <= r.y + r.height;
|
|
603
|
+
if (!holds(visibleArea(shown, rec.host, rec.modal ? [] : bars, window))) return false;
|
|
604
|
+
for (let s = rec.scroller; s; s = s.parent) {
|
|
605
|
+
if (s.host === rec.host || s.modal !== rec.modal) continue;
|
|
606
|
+
const view = measureHost(s.host);
|
|
607
|
+
if (usable(view) && !holds(viewportOf(s, view, window))) return false;
|
|
608
|
+
}
|
|
609
|
+
return true;
|
|
369
610
|
}
|
|
370
611
|
var round = (r) => r && {
|
|
371
612
|
x: Math.round(r.x),
|
|
@@ -389,10 +630,15 @@ function absorbChain(rec, fiber, host) {
|
|
|
389
630
|
if (f === host) break;
|
|
390
631
|
}
|
|
391
632
|
}
|
|
392
|
-
function
|
|
633
|
+
function collectScreen(roots, window) {
|
|
393
634
|
const byHost = /* @__PURE__ */ new Map();
|
|
394
635
|
const order = [];
|
|
395
|
-
const
|
|
636
|
+
const scrollByHost = /* @__PURE__ */ new Map();
|
|
637
|
+
const covered = /* @__PURE__ */ new Set();
|
|
638
|
+
const presented = /* @__PURE__ */ new Set();
|
|
639
|
+
const bars = [];
|
|
640
|
+
let topModal = null;
|
|
641
|
+
const recFor = (fiber, host, parent, hidden, scroller, modal, overlay) => {
|
|
396
642
|
let rec = byHost.get(host);
|
|
397
643
|
if (rec) return rec;
|
|
398
644
|
rec = {
|
|
@@ -402,6 +648,10 @@ function collectElements(roots, window) {
|
|
|
402
648
|
host,
|
|
403
649
|
press: null,
|
|
404
650
|
input: null,
|
|
651
|
+
hidden,
|
|
652
|
+
scroller,
|
|
653
|
+
modal,
|
|
654
|
+
overlay,
|
|
405
655
|
parent,
|
|
406
656
|
parts: []
|
|
407
657
|
};
|
|
@@ -414,49 +664,118 @@ function collectElements(roots, window) {
|
|
|
414
664
|
for (let r = rec; r; r = r.parent) if (r.press) return r;
|
|
415
665
|
return null;
|
|
416
666
|
};
|
|
417
|
-
const
|
|
667
|
+
const textHost = (host) => {
|
|
668
|
+
let h = host;
|
|
669
|
+
for (; ; ) {
|
|
670
|
+
let up = h.return;
|
|
671
|
+
while (up && up.tag !== HOST_COMPONENT) up = up.return;
|
|
672
|
+
if (!up) return h;
|
|
673
|
+
if (h.type !== "RCTVirtualText") return h;
|
|
674
|
+
h = up;
|
|
675
|
+
}
|
|
676
|
+
};
|
|
677
|
+
const stack = roots.map((fiber) => ({
|
|
678
|
+
fiber,
|
|
679
|
+
rec: null,
|
|
680
|
+
hidden: false,
|
|
681
|
+
scroller: null,
|
|
682
|
+
modal: null,
|
|
683
|
+
overlay: false
|
|
684
|
+
}));
|
|
418
685
|
while (stack.length) {
|
|
419
|
-
const
|
|
420
|
-
|
|
686
|
+
const item = stack.pop();
|
|
687
|
+
const { fiber, rec: parentRec } = item;
|
|
688
|
+
if (fiber.sibling) stack.push({ ...item, fiber: fiber.sibling });
|
|
421
689
|
const p = propsOf(fiber);
|
|
422
|
-
|
|
690
|
+
let { hidden, scroller, modal, overlay } = item;
|
|
691
|
+
if (fiber.tag === HOST_COMPONENT) {
|
|
692
|
+
if (p && hiddenBy(p)) hidden = true;
|
|
693
|
+
if (covered.has(fiber)) hidden = true;
|
|
694
|
+
if (fiber.type === SCREEN_STACK) {
|
|
695
|
+
const screens = stackScreens(fiber);
|
|
696
|
+
for (const s of screens.covered) covered.add(s);
|
|
697
|
+
for (const s of screens.presented) presented.add(s);
|
|
698
|
+
}
|
|
699
|
+
if (fiber.type === OVERLAY_HOST) overlay = true;
|
|
700
|
+
if (p && !hidden && (p.accessibilityRole ?? p.role) === "tablist") bars.push(fiber);
|
|
701
|
+
if (!hidden && (MODAL_HOSTS.has(fiber.type) || presented.has(fiber))) {
|
|
702
|
+
modal = { host: fiber, up: modal };
|
|
703
|
+
topModal = modal;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
if (scrolls(fiber)) {
|
|
707
|
+
const host = firstHost(fiber);
|
|
708
|
+
if (host) {
|
|
709
|
+
let found2 = scrollByHost.get(host);
|
|
710
|
+
if (!found2) {
|
|
711
|
+
found2 = { host, fibers: [], hidden, parent: scroller, modal, overlay, bars };
|
|
712
|
+
scrollByHost.set(host, found2);
|
|
713
|
+
}
|
|
714
|
+
found2.fibers.push(fiber);
|
|
715
|
+
scroller = found2;
|
|
716
|
+
}
|
|
717
|
+
}
|
|
423
718
|
let rec = parentRec;
|
|
424
719
|
if (p && interesting(p)) {
|
|
425
720
|
const host = firstHost(fiber);
|
|
426
|
-
const
|
|
427
|
-
|
|
721
|
+
const twin = host && !byHost.has(host) ? refreshTwin(parentRec, host) : null;
|
|
722
|
+
const same = host && !twin && !byHost.has(host) ? sameControl(parentRec, p) : null;
|
|
723
|
+
if (host && twin) {
|
|
724
|
+
byHost.set(host, twin);
|
|
725
|
+
rec = twin;
|
|
726
|
+
} else if (host && same) {
|
|
428
727
|
byHost.set(host, same);
|
|
429
728
|
same.host = host;
|
|
430
729
|
absorbChain(same, fiber, host);
|
|
431
730
|
rec = same;
|
|
432
731
|
} else if (host) {
|
|
433
|
-
rec = recFor(fiber, host, parentRec);
|
|
732
|
+
rec = recFor(fiber, host, parentRec, hidden, scroller, modal, overlay);
|
|
434
733
|
absorb(rec, fiber, p);
|
|
435
734
|
}
|
|
436
735
|
}
|
|
437
736
|
const found = textOf(fiber);
|
|
438
737
|
if (found?.host) {
|
|
439
|
-
const
|
|
738
|
+
const host = textHost(found.host);
|
|
739
|
+
const owner = buttonOf(rec) ?? recFor(host, host, rec, hidden, scroller, modal, overlay);
|
|
440
740
|
const last = owner.parts[owner.parts.length - 1];
|
|
441
|
-
if (last?.host ===
|
|
442
|
-
else owner.parts.push({ host
|
|
741
|
+
if (last?.host === host) last.text += found.text;
|
|
742
|
+
else owner.parts.push({ host, text: found.text });
|
|
443
743
|
}
|
|
444
|
-
if (fiber.child) stack.push({ fiber: fiber.child, rec });
|
|
744
|
+
if (fiber.child) stack.push({ fiber: fiber.child, rec, hidden, scroller, modal, overlay });
|
|
445
745
|
}
|
|
746
|
+
const top = topModal;
|
|
747
|
+
const covers2 = (x) => !!top && !x.overlay && !inside(x.modal, top);
|
|
748
|
+
for (const scrollable of scrollByHost.values())
|
|
749
|
+
scrollable.hidden || (scrollable.hidden = covers2(scrollable));
|
|
446
750
|
for (const rec of order) {
|
|
447
751
|
const e = rec.element;
|
|
752
|
+
rec.hidden || (rec.hidden = covers2(rec));
|
|
448
753
|
const text = rec.parts.map((part) => part.text.replace(ICON_GLYPHS, "").replace(/\s+/g, " ").trim()).filter(Boolean).join(" ");
|
|
449
754
|
if (text) e.text = text;
|
|
450
|
-
e.kind = rec.input ? "input" : rec.press ? "button" : rec.parts.length ? "text" : "view";
|
|
755
|
+
e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press ? "button" : rec.parts.length ? "text" : "view";
|
|
451
756
|
if (e.kind === "input" && e.editable === void 0) e.editable = true;
|
|
452
757
|
const rect = measureHost(rec.host);
|
|
453
758
|
e.rect = round(rect);
|
|
454
|
-
rec.
|
|
759
|
+
const own = scrollByHost.get(rec.host) ?? scrollByHost.get(hostAbove(rec.host));
|
|
760
|
+
if (own) rec.scroller = own;
|
|
761
|
+
rec.onScreen = isOnScreen(rec, rect, window, bars);
|
|
455
762
|
}
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
763
|
+
const named = /* @__PURE__ */ new Set();
|
|
764
|
+
const single = (rec) => {
|
|
765
|
+
const name = rec.element.webview;
|
|
766
|
+
if (name === void 0) return true;
|
|
767
|
+
if (named.has(name)) return false;
|
|
768
|
+
named.add(name);
|
|
769
|
+
return true;
|
|
770
|
+
};
|
|
771
|
+
return {
|
|
772
|
+
found: order.filter(
|
|
773
|
+
(rec) => (rec.element.kind !== "text" || rec.element.text !== void 0) && single(rec)
|
|
774
|
+
),
|
|
775
|
+
scrollables: [...scrollByHost.values()]
|
|
776
|
+
};
|
|
459
777
|
}
|
|
778
|
+
var focused = (found) => found.filter((f) => !f.hidden);
|
|
460
779
|
function pressFiberOf(found) {
|
|
461
780
|
if (found.press) return found.press;
|
|
462
781
|
for (let f = found.fiber.return; f; f = f.return) {
|
|
@@ -467,6 +786,26 @@ function pressFiberOf(found) {
|
|
|
467
786
|
}
|
|
468
787
|
|
|
469
788
|
// src/runtime/screen/targets.ts
|
|
789
|
+
var TARGET_KEYS = ["testID", "label", "placeholder", "text", "at", "index"];
|
|
790
|
+
function checkTarget(target) {
|
|
791
|
+
if (typeof target === "string") return;
|
|
792
|
+
if (target === null || typeof target !== "object" || Array.isArray(target))
|
|
793
|
+
throw new Error(
|
|
794
|
+
`A target is a string or an object with ${TARGET_KEYS.join(", ")}; got ${JSON.stringify(target)}`
|
|
795
|
+
);
|
|
796
|
+
const unknown = Object.keys(target).filter((k) => !TARGET_KEYS.includes(k));
|
|
797
|
+
if (unknown.length)
|
|
798
|
+
throw new Error(
|
|
799
|
+
`Unknown target key ${unknown.map((k) => JSON.stringify(k)).join(", ")}. Valid keys: ${TARGET_KEYS.join(", ")}`
|
|
800
|
+
);
|
|
801
|
+
const { at, index } = target;
|
|
802
|
+
if (at !== void 0 && !(Array.isArray(at) && at.length === 2 && at.every(Number.isFinite)))
|
|
803
|
+
throw new Error(`Target "at" must be [x, y] in window points; got ${JSON.stringify(at)}`);
|
|
804
|
+
if (index !== void 0 && !Number.isInteger(index))
|
|
805
|
+
throw new Error(`Target "index" must be an integer; got ${JSON.stringify(index)}`);
|
|
806
|
+
}
|
|
807
|
+
var covers = (e, [x, y]) => !!e.rect && x >= e.rect.x && x <= e.rect.x + e.rect.width && y >= e.rect.y && y <= e.rect.y + e.rect.height;
|
|
808
|
+
var area = (f) => f.element.rect ? f.element.rect.width * f.element.rect.height : 0;
|
|
470
809
|
function tiers(target) {
|
|
471
810
|
if (typeof target === "string") {
|
|
472
811
|
return [
|
|
@@ -477,59 +816,77 @@ function tiers(target) {
|
|
|
477
816
|
(e) => !!e.text?.includes(target)
|
|
478
817
|
];
|
|
479
818
|
}
|
|
480
|
-
const { testID, label, placeholder, text } = target;
|
|
481
|
-
const fields = (e) => (testID === void 0 || e.testID === testID) && (label === void 0 || e.label === label) && (placeholder === void 0 || e.placeholder === placeholder);
|
|
819
|
+
const { testID, label, placeholder, text, at } = target;
|
|
820
|
+
const fields = (e) => (testID === void 0 || e.testID === testID) && (at === void 0 || covers(e, at)) && (label === void 0 || e.label === label) && (placeholder === void 0 || e.placeholder === placeholder);
|
|
482
821
|
if (text === void 0) return [fields];
|
|
483
822
|
return [
|
|
484
823
|
(e) => fields(e) && e.text === text,
|
|
485
824
|
(e) => fields(e) && !!e.text?.includes(text)
|
|
486
825
|
];
|
|
487
826
|
}
|
|
827
|
+
var labelOnly = ({ element: e, press, input }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input;
|
|
488
828
|
function matchTarget(found, target) {
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
829
|
+
checkTarget(target);
|
|
830
|
+
for (const pool of [found.filter((f) => !labelOnly(f)), found.filter(labelOnly)]) {
|
|
831
|
+
for (const test of tiers(target)) {
|
|
832
|
+
const matches = pool.filter((f) => test(f.element));
|
|
833
|
+
if (matches.length)
|
|
834
|
+
return isAt(target) ? matches.sort((a, b) => area(a) - area(b)) : matches;
|
|
835
|
+
}
|
|
492
836
|
}
|
|
493
837
|
return [];
|
|
494
838
|
}
|
|
839
|
+
var isAt = (target) => typeof target === "object" && target.at !== void 0;
|
|
495
840
|
var indexOf = (target) => typeof target === "object" ? target.index : void 0;
|
|
496
841
|
var clip = (s) => s.length > 40 ? `${s.slice(0, 39)}\u2026` : s;
|
|
497
842
|
function describe(e) {
|
|
498
|
-
const
|
|
843
|
+
const stated = e.checked !== void 0 || e.selected !== void 0 || e.expanded !== void 0;
|
|
844
|
+
const bits = [stated && e.role ? e.role : e.kind];
|
|
845
|
+
if (e.webview !== void 0) bits.push(JSON.stringify(e.webview));
|
|
499
846
|
if (e.testID) bits.push(`#${e.testID}`);
|
|
500
847
|
if (e.text) bits.push(JSON.stringify(clip(e.text)));
|
|
501
848
|
if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(clip(e.label))}`);
|
|
502
849
|
if (e.placeholder) bits.push(`placeholder=${JSON.stringify(clip(e.placeholder))}`);
|
|
503
850
|
if (e.value !== void 0) bits.push(`value=${JSON.stringify(clip(e.value))}`);
|
|
851
|
+
if (e.checked !== void 0) bits.push(e.checked === true ? "checked" : e.checked === false ? "unchecked" : "mixed");
|
|
852
|
+
if (e.selected !== void 0) bits.push(e.selected ? "selected" : "unselected");
|
|
853
|
+
if (e.expanded !== void 0) bits.push(e.expanded ? "expanded" : "collapsed");
|
|
504
854
|
if (e.disabled) bits.push("disabled");
|
|
505
855
|
return bits.join(" ");
|
|
506
856
|
}
|
|
507
857
|
var showTarget = (target) => JSON.stringify(target);
|
|
508
858
|
function onScreenSummary(found, max = 15) {
|
|
509
|
-
const on = found.filter((f) => f.onScreen);
|
|
859
|
+
const on = found.filter((f) => f.onScreen && !f.hidden);
|
|
510
860
|
if (!on.length) return "nothing";
|
|
511
861
|
const shown = on.slice(0, max).map((f) => describe(f.element));
|
|
512
862
|
if (on.length > max) shown.push(`+${on.length - max} more`);
|
|
513
863
|
return shown.join("; ");
|
|
514
864
|
}
|
|
515
|
-
function resolveTarget(found, target, prefer) {
|
|
865
|
+
function resolveTarget(found, target, prefer, options) {
|
|
866
|
+
const reachable = found.filter((f) => !f.hidden);
|
|
516
867
|
let matches = matchTarget(
|
|
517
|
-
|
|
868
|
+
reachable.filter((f) => f.onScreen),
|
|
518
869
|
target
|
|
519
870
|
);
|
|
871
|
+
if (!matches.length && options?.offscreen) matches = matchTarget(reachable, target);
|
|
520
872
|
if (!matches.length) {
|
|
521
873
|
const hidden = matchTarget(found, target)[0];
|
|
874
|
+
if (hidden?.hidden)
|
|
875
|
+
throw new Error(
|
|
876
|
+
`${showTarget(target)} matches ${describe(hidden.element)}, which is on a screen that is not in front (an unfocused tab or stack screen, or behind a modal). screen.snapshot {all:true} lists it`
|
|
877
|
+
);
|
|
522
878
|
if (hidden)
|
|
523
879
|
throw new Error(
|
|
524
|
-
`${showTarget(target)} matches ${describe(hidden.element)}, which is not on screen`
|
|
880
|
+
`${showTarget(target)} matches ${describe(hidden.element)}, which is not on screen. Pass {scroll:true} to scroll it into view, or use screen.scroll`
|
|
525
881
|
);
|
|
882
|
+
const unrendered = options?.offscreen ? ". A list renders only the rows near its window: for a row it has not rendered yet, screen.scroll {toEnd:true} or {by: points} first" : "";
|
|
526
883
|
throw new Error(
|
|
527
|
-
`Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}`
|
|
884
|
+
`Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}${unrendered}`
|
|
528
885
|
);
|
|
529
886
|
}
|
|
530
887
|
const preferred = prefer ? matches.filter(prefer) : [];
|
|
531
888
|
if (preferred.length) matches = preferred;
|
|
532
|
-
const index = indexOf(target);
|
|
889
|
+
const index = options?.index ?? indexOf(target) ?? (isAt(target) ? 0 : void 0);
|
|
533
890
|
if (index !== void 0) {
|
|
534
891
|
const pick = matches[index];
|
|
535
892
|
if (!pick)
|
|
@@ -540,8 +897,9 @@ function resolveTarget(found, target, prefer) {
|
|
|
540
897
|
}
|
|
541
898
|
if (matches.length > 1) {
|
|
542
899
|
const list = matches.slice(0, 10).map((f, i) => `${i}: ${describe(f.element)}`).join("; ");
|
|
900
|
+
const example = typeof target === "string" ? `${JSON.stringify(options?.afterText ? [target, "<text>", { index: 1 }] : [target, { index: 1 }])} (index as the last argument)` : `${JSON.stringify({ ...target, index: 1 })} (index inside the target)`;
|
|
543
901
|
throw new Error(
|
|
544
|
-
`${showTarget(target)} matches ${matches.length} elements;
|
|
902
|
+
`${showTarget(target)} matches ${matches.length} elements; pick one with an index, e.g. ${example}, or use a narrower target. ${list}`
|
|
545
903
|
);
|
|
546
904
|
}
|
|
547
905
|
return matches[0];
|
|
@@ -584,11 +942,13 @@ function fillInput(found, text, options = {}) {
|
|
|
584
942
|
return value;
|
|
585
943
|
}
|
|
586
944
|
var center = (rect) => rect ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } : { x: 0, y: 0 };
|
|
587
|
-
function pressElement(found) {
|
|
945
|
+
function pressElement(found, options = {}) {
|
|
588
946
|
const fiber = pressFiberOf(found);
|
|
589
947
|
if (!fiber) throw new Error(`${describe(found.element)} has no onPress`);
|
|
590
|
-
if (found.element.disabled || propsOf(fiber)?.disabled === true)
|
|
591
|
-
throw new Error(
|
|
948
|
+
if (!options.force && (found.element.disabled || propsOf(fiber)?.disabled === true))
|
|
949
|
+
throw new Error(
|
|
950
|
+
`${describe(found.element)} is disabled; pass {"force":true} as the last argument to press anyway`
|
|
951
|
+
);
|
|
592
952
|
const { x, y } = center(found.element.rect);
|
|
593
953
|
const e = () => event({
|
|
594
954
|
locationX: found.element.rect ? found.element.rect.width / 2 : 0,
|
|
@@ -602,6 +962,236 @@ function pressElement(found) {
|
|
|
602
962
|
handler(fiber, "onPressOut")?.(e());
|
|
603
963
|
}
|
|
604
964
|
|
|
965
|
+
// src/runtime/screen/find-text.ts
|
|
966
|
+
var normalize = (s) => s.toLowerCase().replace(/\s+/g, " ").trim();
|
|
967
|
+
function nearMisses(found, text, max = 5) {
|
|
968
|
+
const want = normalize(text);
|
|
969
|
+
if (!want) return [];
|
|
970
|
+
const close = (s) => {
|
|
971
|
+
const have = s && normalize(s);
|
|
972
|
+
return !!have && (have.includes(want) || have.length >= 3 && want.includes(have));
|
|
973
|
+
};
|
|
974
|
+
return found.filter((f) => !f.hidden).filter((f) => close(f.element.text) || close(f.element.label)).slice(0, max).map((f) => `${describe(f.element)}${f.onScreen ? "" : " (off screen)"}`);
|
|
975
|
+
}
|
|
976
|
+
function findText(found, text, options = {}) {
|
|
977
|
+
const test = (s) => s !== void 0 && (options.exact ? s === text : s.includes(text));
|
|
978
|
+
const matches = [];
|
|
979
|
+
let hidden = 0;
|
|
980
|
+
for (const f of found) {
|
|
981
|
+
const { element } = f;
|
|
982
|
+
const field = test(element.text) ? "text" : options.labels !== false && test(element.label) ? "label" : null;
|
|
983
|
+
if (!field) continue;
|
|
984
|
+
if (f.hidden) {
|
|
985
|
+
hidden += 1;
|
|
986
|
+
continue;
|
|
987
|
+
}
|
|
988
|
+
matches.push({
|
|
989
|
+
text: field === "text" ? element.text : element.label,
|
|
990
|
+
field,
|
|
991
|
+
rect: element.rect,
|
|
992
|
+
onScreen: f.onScreen
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
const result = {
|
|
996
|
+
found: matches.length,
|
|
997
|
+
onScreen: matches.filter((m) => m.onScreen).length,
|
|
998
|
+
matches
|
|
999
|
+
};
|
|
1000
|
+
if (hidden) result.hidden = hidden;
|
|
1001
|
+
if (!matches.length) result.nearMisses = nearMisses(found, text);
|
|
1002
|
+
return result;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
// src/runtime/screen/scroll.ts
|
|
1006
|
+
var COMMANDS = ["to", "toEnd", "toStart", "by"];
|
|
1007
|
+
function parseScroll(arg) {
|
|
1008
|
+
if (arg === null || typeof arg !== "object" || Array.isArray(arg) || !COMMANDS.some((k) => k in arg))
|
|
1009
|
+
return { kind: "to", target: arg };
|
|
1010
|
+
const keys = Object.keys(arg);
|
|
1011
|
+
if (keys.length !== 1)
|
|
1012
|
+
throw new Error(
|
|
1013
|
+
`screen.scroll takes one of {to: target}, {toEnd:true}, {toStart:true} or {by: points}; got ${JSON.stringify(arg)}`
|
|
1014
|
+
);
|
|
1015
|
+
const [key] = keys;
|
|
1016
|
+
const value = arg[key];
|
|
1017
|
+
if (key === "to") return { kind: "to", target: value };
|
|
1018
|
+
if (key === "by") {
|
|
1019
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1020
|
+
throw new Error(`{by} is a number of points (negative scrolls back); got ${JSON.stringify(value)}`);
|
|
1021
|
+
return { kind: "by", amount: value };
|
|
1022
|
+
}
|
|
1023
|
+
if (value !== true) throw new Error(`{${key}} must be true; got ${JSON.stringify(value)}`);
|
|
1024
|
+
return { kind: key };
|
|
1025
|
+
}
|
|
1026
|
+
var nodeOf2 = (fiber) => {
|
|
1027
|
+
const n = fiber.stateNode;
|
|
1028
|
+
return n && typeof n === "object" ? n : null;
|
|
1029
|
+
};
|
|
1030
|
+
var method = (fiber, name) => {
|
|
1031
|
+
const node = nodeOf2(fiber);
|
|
1032
|
+
const fn = node?.[name];
|
|
1033
|
+
return typeof fn === "function" ? fn.bind(node) : null;
|
|
1034
|
+
};
|
|
1035
|
+
var isHorizontal = (s) => s.fibers.some((f) => propsOf(f)?.horizontal === true);
|
|
1036
|
+
function scrollNode(s) {
|
|
1037
|
+
if (!REFRESH_HOST.test(String(s.host.type))) return s.host;
|
|
1038
|
+
const todo = s.host.child ? [s.host.child] : [];
|
|
1039
|
+
while (todo.length) {
|
|
1040
|
+
const f = todo.pop();
|
|
1041
|
+
if (f.sibling) todo.push(f.sibling);
|
|
1042
|
+
if (f.tag === 5) return f;
|
|
1043
|
+
if (f.child) todo.push(f.child);
|
|
1044
|
+
}
|
|
1045
|
+
return s.host;
|
|
1046
|
+
}
|
|
1047
|
+
function contentHost(node) {
|
|
1048
|
+
const todo = node.child ? [node.child] : [];
|
|
1049
|
+
while (todo.length) {
|
|
1050
|
+
const f = todo.pop();
|
|
1051
|
+
if (f.sibling) todo.push(f.sibling);
|
|
1052
|
+
if (f.tag === 5) {
|
|
1053
|
+
if (!REFRESH_HOST.test(String(f.type))) return f;
|
|
1054
|
+
} else if (f.child) todo.push(f.child);
|
|
1055
|
+
}
|
|
1056
|
+
return null;
|
|
1057
|
+
}
|
|
1058
|
+
function isInverted(s, horizontal) {
|
|
1059
|
+
const axis = horizontal ? "scaleX" : "scaleY";
|
|
1060
|
+
return s.fibers.some((f) => {
|
|
1061
|
+
const p = propsOf(f);
|
|
1062
|
+
if (p?.inverted === true) return true;
|
|
1063
|
+
const style = [].concat(p?.style ?? []).flat(Infinity);
|
|
1064
|
+
return style.some((st) => {
|
|
1065
|
+
const t = st?.transform;
|
|
1066
|
+
return Array.isArray(t) && t.some(
|
|
1067
|
+
(x) => x && typeof x === "object" && (x[axis] === -1 || x.scale === -1)
|
|
1068
|
+
);
|
|
1069
|
+
});
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
function metricsOf(s) {
|
|
1073
|
+
const horizontal = isHorizontal(s);
|
|
1074
|
+
const host = scrollNode(s);
|
|
1075
|
+
const node = nodeOf2(host);
|
|
1076
|
+
const view = measureHost(host);
|
|
1077
|
+
if (node && typeof node.scrollTop === "number" && typeof node.scrollHeight === "number") {
|
|
1078
|
+
const size = horizontal ? [node.scrollLeft, node.scrollWidth, node.clientWidth] : [node.scrollTop, node.scrollHeight, node.clientHeight];
|
|
1079
|
+
const [offset, total, shown2] = size;
|
|
1080
|
+
return {
|
|
1081
|
+
offset,
|
|
1082
|
+
max: Math.max(0, total - shown2),
|
|
1083
|
+
view,
|
|
1084
|
+
inverted: false
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
const content = contentHost(host);
|
|
1088
|
+
const box = content && measureHost(content);
|
|
1089
|
+
if (!view || !box) return null;
|
|
1090
|
+
const max = Math.max(0, horizontal ? box.width - view.width : box.height - view.height);
|
|
1091
|
+
const shown = horizontal ? view.x - box.x : view.y - box.y;
|
|
1092
|
+
const inverted = isInverted(s, horizontal);
|
|
1093
|
+
return { offset: inverted ? max - shown : shown, max, view, inverted };
|
|
1094
|
+
}
|
|
1095
|
+
var round2 = (n) => Math.round(n * 100) / 100;
|
|
1096
|
+
function scrollToOffset(s, offset) {
|
|
1097
|
+
const horizontal = isHorizontal(s);
|
|
1098
|
+
const x = horizontal ? offset : 0;
|
|
1099
|
+
const y = horizontal ? 0 : offset;
|
|
1100
|
+
const target = { x, y, left: x, top: y, animated: false, behavior: "instant" };
|
|
1101
|
+
for (const f of [...s.fibers].reverse()) {
|
|
1102
|
+
const scrollTo = method(f, "scrollTo");
|
|
1103
|
+
if (scrollTo) return void scrollTo(target);
|
|
1104
|
+
}
|
|
1105
|
+
for (const f of s.fibers) {
|
|
1106
|
+
const toOffset = method(f, "scrollToOffset");
|
|
1107
|
+
if (toOffset) return void toOffset({ offset, animated: false });
|
|
1108
|
+
}
|
|
1109
|
+
throw new Error("This scrollable has no scrollTo or scrollToOffset");
|
|
1110
|
+
}
|
|
1111
|
+
function scrollToEnd(s) {
|
|
1112
|
+
for (const f of s.fibers) {
|
|
1113
|
+
const toEnd = method(f, "scrollToEnd");
|
|
1114
|
+
if (toEnd) return void toEnd({ animated: false });
|
|
1115
|
+
}
|
|
1116
|
+
const max = metricsOf(s)?.max;
|
|
1117
|
+
if (max == null) throw new Error("This scrollable has no scrollToEnd and its size is unknown");
|
|
1118
|
+
scrollToOffset(s, max);
|
|
1119
|
+
}
|
|
1120
|
+
var scrollEnabled = (s) => !s.fibers.some((f) => propsOf(f)?.scrollEnabled === false);
|
|
1121
|
+
function offsetFor(s, found, window) {
|
|
1122
|
+
const rect = found.element.rect;
|
|
1123
|
+
const m = metricsOf(s);
|
|
1124
|
+
if (!rect || !m?.view) return null;
|
|
1125
|
+
const view = viewportOf(s, m.view, window) ?? m.view;
|
|
1126
|
+
const [start, size, viewStart, viewSize] = isHorizontal(s) ? [rect.x, rect.width, view.x, view.width] : [rect.y, rect.height, view.y, view.height];
|
|
1127
|
+
if (start >= viewStart && start + size <= viewStart + viewSize) return null;
|
|
1128
|
+
const delta = size >= viewSize ? start - viewStart : start + size / 2 - (viewStart + viewSize / 2);
|
|
1129
|
+
const next = Math.max(0, Math.min(m.max ?? Infinity, m.offset + (m.inverted ? -delta : delta)));
|
|
1130
|
+
return Math.abs(next - m.offset) < 1 ? null : round2(next);
|
|
1131
|
+
}
|
|
1132
|
+
function scrollerFor(found, window) {
|
|
1133
|
+
for (let s = found.scroller; s; s = s.parent) {
|
|
1134
|
+
if (s.hidden || s.host === found.host || !scrollEnabled(s)) continue;
|
|
1135
|
+
const offset = offsetFor(s, found, window);
|
|
1136
|
+
if (offset !== null) return { scrollable: s, offset };
|
|
1137
|
+
}
|
|
1138
|
+
return null;
|
|
1139
|
+
}
|
|
1140
|
+
var wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
1141
|
+
async function awaitScroll(s, before, want) {
|
|
1142
|
+
const near = (x, to) => !!x && to !== void 0 && Math.abs(x.offset - to) < 1;
|
|
1143
|
+
let m = metricsOf(s);
|
|
1144
|
+
if (!m || near(m, want)) return m;
|
|
1145
|
+
let idle = 0;
|
|
1146
|
+
for (let waited = 0; waited < 300; waited += 16) {
|
|
1147
|
+
await wait(16);
|
|
1148
|
+
const next = metricsOf(s);
|
|
1149
|
+
if (!next || near(next, want)) return next;
|
|
1150
|
+
idle = Math.abs(next.offset - m.offset) < 1 ? idle + 1 : 0;
|
|
1151
|
+
m = next;
|
|
1152
|
+
if (idle >= 2 && !near(m, before)) break;
|
|
1153
|
+
}
|
|
1154
|
+
return m;
|
|
1155
|
+
}
|
|
1156
|
+
function refreshHandler(s) {
|
|
1157
|
+
for (const f of s.fibers) {
|
|
1158
|
+
const p = propsOf(f);
|
|
1159
|
+
const control = p?.refreshControl?.props;
|
|
1160
|
+
if (typeof control?.onRefresh === "function") return control.onRefresh;
|
|
1161
|
+
if (typeof p?.onRefresh === "function") return p.onRefresh;
|
|
1162
|
+
}
|
|
1163
|
+
return null;
|
|
1164
|
+
}
|
|
1165
|
+
function draggable(s) {
|
|
1166
|
+
for (let x = s; x; x = x.parent) if (!x.hidden && scrollEnabled(x)) return x;
|
|
1167
|
+
return null;
|
|
1168
|
+
}
|
|
1169
|
+
function layoutOf(scrollables, window) {
|
|
1170
|
+
let ready = true;
|
|
1171
|
+
const parts = [];
|
|
1172
|
+
for (const s of scrollables) {
|
|
1173
|
+
if (s.hidden) continue;
|
|
1174
|
+
const view = measureHost(s.host);
|
|
1175
|
+
if (!view) continue;
|
|
1176
|
+
const m = metricsOf(s);
|
|
1177
|
+
if (view.width <= 0 || view.height <= 0 || !m?.view) ready = false;
|
|
1178
|
+
const limit = viewportOf(s, view, window);
|
|
1179
|
+
parts.push(JSON.stringify([view, m && [m.max, m.offset], limit]));
|
|
1180
|
+
}
|
|
1181
|
+
return { ready, sig: parts.join("|") };
|
|
1182
|
+
}
|
|
1183
|
+
var area2 = (r) => r ? r.width * r.height : 0;
|
|
1184
|
+
var onWindow = (r, w) => !!r && r.width > 0 && r.height > 0 && r.x < w.width && r.y < w.height && r.x + r.width > 0 && r.y + r.height > 0;
|
|
1185
|
+
function mainScrollable(scrollables, window, need) {
|
|
1186
|
+
const candidates = scrollables.filter((s) => !s.hidden && (need === "refresh" ? refreshHandler(s) : scrollEnabled(s))).map((s) => ({ s, rect: measureHost(s.host) })).filter(({ rect }) => onWindow(rect, window)).map(({ s, rect }) => ({ s, rect: viewportOf(s, rect, window) })).filter(({ rect }) => !!rect).sort((a, b) => area2(b.rect) - area2(a.rect));
|
|
1187
|
+
const [best] = candidates;
|
|
1188
|
+
if (!best)
|
|
1189
|
+
throw new Error(
|
|
1190
|
+
need === "refresh" ? "No scrollable on screen has a RefreshControl (or an onRefresh prop)" : "No scrollable on screen"
|
|
1191
|
+
);
|
|
1192
|
+
return best.s;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
605
1195
|
// src/runtime/screen/settle.ts
|
|
606
1196
|
var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
|
|
607
1197
|
var UPDATE_LANES = (1 << 22) - 1;
|
|
@@ -673,6 +1263,11 @@ async function settle(options = {}) {
|
|
|
673
1263
|
|
|
674
1264
|
// src/runtime/screen/wait-for.ts
|
|
675
1265
|
var POLL_MS = 50;
|
|
1266
|
+
function nearMissNote(found, target) {
|
|
1267
|
+
const text = typeof target === "string" ? target : target.text;
|
|
1268
|
+
const near = text ? nearMisses(found, text) : [];
|
|
1269
|
+
return near.length ? `. Near misses: ${near.join("; ")}` : "";
|
|
1270
|
+
}
|
|
676
1271
|
function waitForTarget(collect, target, options = {}) {
|
|
677
1272
|
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
678
1273
|
const t0 = performance.now();
|
|
@@ -691,7 +1286,7 @@ function waitForTarget(collect, target, options = {}) {
|
|
|
691
1286
|
try {
|
|
692
1287
|
const found = collect();
|
|
693
1288
|
const matches = matchTarget(
|
|
694
|
-
found.filter((f) => f.onScreen),
|
|
1289
|
+
found.filter((f) => f.onScreen && !f.hidden),
|
|
695
1290
|
target
|
|
696
1291
|
);
|
|
697
1292
|
const hit = matches[indexOf(target) ?? 0];
|
|
@@ -703,7 +1298,7 @@ function waitForTarget(collect, target, options = {}) {
|
|
|
703
1298
|
const what = options.gone ? "to disappear" : "to appear";
|
|
704
1299
|
reject(
|
|
705
1300
|
new Error(
|
|
706
|
-
`Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}`
|
|
1301
|
+
`Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}${nearMissNote(found, target)}`
|
|
707
1302
|
)
|
|
708
1303
|
);
|
|
709
1304
|
}
|
|
@@ -724,41 +1319,184 @@ function waitForTarget(collect, target, options = {}) {
|
|
|
724
1319
|
|
|
725
1320
|
// src/runtime/screen/index.ts
|
|
726
1321
|
function createScreen(env) {
|
|
727
|
-
const
|
|
728
|
-
const
|
|
1322
|
+
const collectAll = () => collectScreen(env.roots(), env.window());
|
|
1323
|
+
const collect = () => collectAll().found;
|
|
1324
|
+
const collectSettled = async () => {
|
|
1325
|
+
let prev = null;
|
|
1326
|
+
let same = 0;
|
|
1327
|
+
for (let waited = 0; ; waited += 16) {
|
|
1328
|
+
const all = collectAll();
|
|
1329
|
+
const { ready, sig } = layoutOf(all.scrollables, env.window());
|
|
1330
|
+
same = sig === prev ? same + 1 : 0;
|
|
1331
|
+
if (waited >= 320 || (ready ? same >= 3 : same >= 6)) return all;
|
|
1332
|
+
prev = sig;
|
|
1333
|
+
await wait(16);
|
|
1334
|
+
}
|
|
1335
|
+
};
|
|
1336
|
+
const reread = (found) => {
|
|
729
1337
|
const node = found.host.stateNode;
|
|
730
1338
|
const again = collect().find(
|
|
731
1339
|
(f) => f.host === found.host || node != null && f.host.stateNode === node
|
|
732
1340
|
);
|
|
733
|
-
return
|
|
1341
|
+
return again ?? found;
|
|
1342
|
+
};
|
|
1343
|
+
const scrollableOf = (found, what) => {
|
|
1344
|
+
if (!found.scroller)
|
|
1345
|
+
throw new Error(`${describe(found.element)} ${what}`);
|
|
1346
|
+
return found.scroller;
|
|
1347
|
+
};
|
|
1348
|
+
const scrolled = async (scrollable, act) => {
|
|
1349
|
+
const before = metricsOf(scrollable);
|
|
1350
|
+
const want = act();
|
|
1351
|
+
await settle();
|
|
1352
|
+
return awaitScroll(scrollable, before?.offset, want ?? before?.max ?? void 0);
|
|
1353
|
+
};
|
|
1354
|
+
const bringIntoView = async (found) => {
|
|
1355
|
+
let current = found;
|
|
1356
|
+
let metrics = null;
|
|
1357
|
+
for (let i = 0; i < 4; i++) {
|
|
1358
|
+
const plan = scrollerFor(current, env.window());
|
|
1359
|
+
if (!plan) break;
|
|
1360
|
+
metrics = await scrolled(plan.scrollable, () => {
|
|
1361
|
+
scrollToOffset(plan.scrollable, plan.offset);
|
|
1362
|
+
return plan.offset;
|
|
1363
|
+
});
|
|
1364
|
+
current = reread(current);
|
|
1365
|
+
}
|
|
1366
|
+
if (!metrics && current.scroller) metrics = metricsOf(current.scroller);
|
|
1367
|
+
return { found: current, metrics };
|
|
1368
|
+
};
|
|
1369
|
+
const resolve = async (target, prefer, options) => {
|
|
1370
|
+
let found = resolveTarget(collect(), target, prefer, {
|
|
1371
|
+
...options,
|
|
1372
|
+
offscreen: options.scroll
|
|
1373
|
+
});
|
|
1374
|
+
if (found.onScreen) return found;
|
|
1375
|
+
found = (await bringIntoView(found)).found;
|
|
1376
|
+
if (!found.onScreen)
|
|
1377
|
+
throw new Error(
|
|
1378
|
+
`${describe(found.element)} is not on screen and no scrollable around it can bring it into view`
|
|
1379
|
+
);
|
|
1380
|
+
return found;
|
|
734
1381
|
};
|
|
735
1382
|
return {
|
|
736
1383
|
snapshot(options = {}) {
|
|
737
1384
|
const found = collect();
|
|
738
|
-
if (
|
|
739
|
-
return {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
1385
|
+
if (options.all)
|
|
1386
|
+
return {
|
|
1387
|
+
elements: found.map((f) => ({
|
|
1388
|
+
...f.element,
|
|
1389
|
+
onScreen: f.onScreen,
|
|
1390
|
+
...f.hidden && { hidden: true }
|
|
1391
|
+
}))
|
|
1392
|
+
};
|
|
1393
|
+
const shown = focused(found);
|
|
1394
|
+
const elements = shown.filter((f) => f.onScreen).map((f) => f.element);
|
|
1395
|
+
const hidden = found.length - shown.length;
|
|
1396
|
+
return hidden ? { elements, hidden } : { elements };
|
|
743
1397
|
},
|
|
1398
|
+
findText: (text, options) => findText(collect(), text, options),
|
|
744
1399
|
async fill(target, text, options = {}) {
|
|
745
|
-
const found =
|
|
1400
|
+
const found = await resolve(target, (f) => !!f.input, {
|
|
1401
|
+
index: options.index,
|
|
1402
|
+
afterText: true,
|
|
1403
|
+
scroll: options.scroll
|
|
1404
|
+
});
|
|
746
1405
|
const filled = fillInput(found, String(text), options);
|
|
747
1406
|
await settle();
|
|
748
|
-
return { filled, element:
|
|
1407
|
+
return { filled, element: reread(found).element };
|
|
749
1408
|
},
|
|
750
|
-
async press(target) {
|
|
751
|
-
const found =
|
|
752
|
-
pressElement(found);
|
|
1409
|
+
async press(target, options = {}) {
|
|
1410
|
+
const found = await resolve(target, (f) => !!f.press, options);
|
|
1411
|
+
pressElement(found, options);
|
|
753
1412
|
await settle();
|
|
754
1413
|
return found.element;
|
|
755
1414
|
},
|
|
1415
|
+
/**
|
|
1416
|
+
* Scrolls the nearest scrollable of a target (or `within`, or the main one
|
|
1417
|
+
* on screen) to the target, to either end, or by a distance in points.
|
|
1418
|
+
*/
|
|
1419
|
+
async scroll(arg, options = {}) {
|
|
1420
|
+
const command = parseScroll(arg);
|
|
1421
|
+
const { found: all, scrollables } = await collectSettled();
|
|
1422
|
+
const named = options.within ? scrollableOf(
|
|
1423
|
+
resolveTarget(all, options.within, void 0, { offscreen: true }),
|
|
1424
|
+
"is not a scrollable or inside one"
|
|
1425
|
+
) : null;
|
|
1426
|
+
const within2 = named && draggable(named);
|
|
1427
|
+
if (named && !within2)
|
|
1428
|
+
throw new Error(
|
|
1429
|
+
`${describe(resolveTarget(all, options.within, void 0, { offscreen: true }).element)} can't be scrolled (scrollEnabled is false, or it is on a hidden screen) and no scrollable around it can`
|
|
1430
|
+
);
|
|
1431
|
+
const target = command.kind === "to" ? resolveTarget(all, command.target, void 0, {
|
|
1432
|
+
index: options.index,
|
|
1433
|
+
offscreen: true
|
|
1434
|
+
}) : null;
|
|
1435
|
+
let metrics;
|
|
1436
|
+
let again = null;
|
|
1437
|
+
if (target && !within2) {
|
|
1438
|
+
scrollableOf(target, "is not inside a scrollable");
|
|
1439
|
+
({ found: again, metrics } = await bringIntoView(target));
|
|
1440
|
+
} else if (target && within2) {
|
|
1441
|
+
const offset = offsetFor(within2, target, env.window());
|
|
1442
|
+
metrics = offset === null ? metricsOf(within2) : await scrolled(within2, () => (scrollToOffset(within2, offset), offset));
|
|
1443
|
+
again = reread(target);
|
|
1444
|
+
} else {
|
|
1445
|
+
const scrollable = within2 ?? mainScrollable(scrollables, env.window(), "scroll");
|
|
1446
|
+
metrics = await scrolled(scrollable, () => {
|
|
1447
|
+
if (command.kind === "toEnd") {
|
|
1448
|
+
scrollToEnd(scrollable);
|
|
1449
|
+
return metricsOf(scrollable)?.max ?? void 0;
|
|
1450
|
+
}
|
|
1451
|
+
if (command.kind === "toStart") {
|
|
1452
|
+
scrollToOffset(scrollable, 0);
|
|
1453
|
+
return 0;
|
|
1454
|
+
}
|
|
1455
|
+
const now2 = metricsOf(scrollable);
|
|
1456
|
+
if (!now2)
|
|
1457
|
+
throw new Error("Can not read the scroll position, so can not scroll by a distance");
|
|
1458
|
+
const next = Math.max(
|
|
1459
|
+
0,
|
|
1460
|
+
Math.min(now2.max ?? Infinity, now2.offset + command.amount)
|
|
1461
|
+
);
|
|
1462
|
+
scrollToOffset(scrollable, next);
|
|
1463
|
+
return next;
|
|
1464
|
+
});
|
|
1465
|
+
}
|
|
1466
|
+
const result = {};
|
|
1467
|
+
if (metrics) {
|
|
1468
|
+
result.offset = Math.round(metrics.offset * 100) / 100;
|
|
1469
|
+
if (metrics.max != null) result.max = Math.round(metrics.max * 100) / 100;
|
|
1470
|
+
}
|
|
1471
|
+
if (again) {
|
|
1472
|
+
result.element = again.element;
|
|
1473
|
+
result.onScreen = again.onScreen;
|
|
1474
|
+
}
|
|
1475
|
+
return result;
|
|
1476
|
+
},
|
|
1477
|
+
/** Calls the `onRefresh` of a scrollable's RefreshControl, as a pull would. */
|
|
1478
|
+
async refresh(target, options = {}) {
|
|
1479
|
+
const { found: all, scrollables } = collectAll();
|
|
1480
|
+
const scrollable = target ? scrollableOf(
|
|
1481
|
+
resolveTarget(all, target, void 0, {
|
|
1482
|
+
index: options.index,
|
|
1483
|
+
offscreen: true
|
|
1484
|
+
}),
|
|
1485
|
+
"is not a scrollable or inside one"
|
|
1486
|
+
) : mainScrollable(scrollables, env.window(), "refresh");
|
|
1487
|
+
const onRefresh = refreshHandler(scrollable);
|
|
1488
|
+
if (!onRefresh) throw new Error("That scrollable has no RefreshControl (or onRefresh prop)");
|
|
1489
|
+
onRefresh();
|
|
1490
|
+
await settle();
|
|
1491
|
+
return { refreshed: true };
|
|
1492
|
+
},
|
|
756
1493
|
waitFor: (target, options) => waitForTarget(collect, target, options)
|
|
757
1494
|
};
|
|
758
1495
|
}
|
|
759
1496
|
|
|
760
1497
|
// src/runtime/tools/screen.ts
|
|
761
1498
|
var devToolsHook = () => globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1499
|
+
var TARGET_HELP = "target is a string (testID, label, placeholder or text) or an object that must match every key it sets: {testID, label, placeholder, text, at:[x,y], index}. Unknown keys are errors. {at:[x,y]} hits the smallest element covering that point, for icon-only buttons with no label. Several matches: add an index, either in the target or as a trailing {index:n} argument.";
|
|
762
1500
|
function screenTools() {
|
|
763
1501
|
const screen = createScreen({
|
|
764
1502
|
roots: () => fiberRoots(devToolsHook()),
|
|
@@ -766,28 +1504,31 @@ function screenTools() {
|
|
|
766
1504
|
});
|
|
767
1505
|
return {
|
|
768
1506
|
"screen.findText": {
|
|
769
|
-
description: "Find
|
|
770
|
-
run: (text, options) =>
|
|
771
|
-
fiberRoots(devToolsHook()),
|
|
772
|
-
text,
|
|
773
|
-
import_react_native.Dimensions.get("window"),
|
|
774
|
-
options
|
|
775
|
-
)
|
|
1507
|
+
description: "Find text on screen (substring, or exact with {exact:true}) and whether each match is on screen. Searches the same joined text screen.snapshot shows (nested Text concatenated) and accessibility labels; {labels:false} searches text only. Each match says which field matched; a miss lists near misses. Ignores screens the user can't reach (unfocused tabs and stack screens, under a modal); `hidden` counts matches there.",
|
|
1508
|
+
run: (text, options) => screen.findText(text, options)
|
|
776
1509
|
},
|
|
777
1510
|
"screen.snapshot": {
|
|
778
|
-
description: "Buttons, inputs, text and testID views on screen, with rects. {all:true}
|
|
1511
|
+
description: "Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch's value is checked). Only the focused screen: unfocused tabs and stack screens, content under an open modal, display:none and accessibility-hidden subtrees are left out, and `hidden` counts the elements skipped. {all:true} lists everything, marking off-screen (onScreen:false) and hidden (hidden:true) ones. A WebView registered with the react-native-webview adapter shows as one `webview` element with its name: use webview.* inside it.",
|
|
779
1512
|
run: (options) => screen.snapshot(options)
|
|
780
1513
|
},
|
|
781
1514
|
"screen.fill": {
|
|
782
|
-
description:
|
|
1515
|
+
description: `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits.`,
|
|
783
1516
|
run: (target, text, options) => screen.fill(target, text, options)
|
|
784
1517
|
},
|
|
785
1518
|
"screen.press": {
|
|
786
|
-
description:
|
|
787
|
-
run: (target) => screen.press(target)
|
|
1519
|
+
description: `Press a button, then wait for the render. ${TARGET_HELP} Disabled elements are refused; {force:true} presses anyway, to see what the app does on tap. {scroll:true} scrolls an off-screen target into view first.`,
|
|
1520
|
+
run: (target, options) => screen.press(target, options)
|
|
1521
|
+
},
|
|
1522
|
+
"screen.scroll": {
|
|
1523
|
+
description: `Scroll the nearest ScrollView/FlatList/FlashList, then wait for the render. Takes a target (or {to: target}) to bring into the middle of its scrollable, {toEnd:true}, {toStart:true}, or {by: points} (negative scrolls back); the last argument may be {within: target, index} to name the scrollable (a target inside it, or its testID). Without a target it scrolls the largest scrollable on screen. Only rendered elements can be targeted: rows a FlatList has not rendered yet need {toEnd:true} or {by} first. Returns the offset and, for a target, whether it is now on screen. ${TARGET_HELP}`,
|
|
1524
|
+
run: (arg, options) => screen.scroll(arg, options)
|
|
1525
|
+
},
|
|
1526
|
+
"screen.refresh": {
|
|
1527
|
+
description: "Pull to refresh: call the onRefresh of the RefreshControl of the scrollable holding a target (or the largest one with a RefreshControl on screen), then wait for the render. It does not wait for the data: follow with screen.waitFor.",
|
|
1528
|
+
run: (target, options) => screen.refresh(target, options)
|
|
788
1529
|
},
|
|
789
1530
|
"screen.waitFor": {
|
|
790
|
-
description: "Wait until a target is on screen, or gone with {gone:true}. Default timeout 5000 ms.",
|
|
1531
|
+
description: "Wait until a target is on screen, or gone with {gone:true}. Matches testID, label, placeholder, then the same joined text screen.snapshot shows. Default timeout 5000 ms; a timeout lists near misses.",
|
|
791
1532
|
run: (target, options) => screen.waitFor(target, options)
|
|
792
1533
|
}
|
|
793
1534
|
};
|
|
@@ -797,9 +1538,13 @@ function screenTools() {
|
|
|
797
1538
|
function builtinTools(listTools, getTools, logs, getScenarios) {
|
|
798
1539
|
return {
|
|
799
1540
|
...bridgeTools(listTools),
|
|
1541
|
+
...appTools(
|
|
1542
|
+
selectReload(globalThis, () => import_react_native2.DevSettings.reload())
|
|
1543
|
+
),
|
|
800
1544
|
...logTools(logs),
|
|
801
1545
|
...restoreTools(getTools),
|
|
802
1546
|
...screenTools(),
|
|
1547
|
+
...captureTools(loadViewShot),
|
|
803
1548
|
// Only when the app defines scenarios, so `tools` doesn't list empty ones.
|
|
804
1549
|
...getScenarios && scenarioTools(getScenarios, getTools)
|
|
805
1550
|
};
|
|
@@ -862,6 +1607,9 @@ function createGate(name) {
|
|
|
862
1607
|
};
|
|
863
1608
|
}
|
|
864
1609
|
|
|
1610
|
+
// src/runtime/load-id.ts
|
|
1611
|
+
var loadId = Math.random().toString(36).slice(2, 10);
|
|
1612
|
+
|
|
865
1613
|
// src/runtime/logs.ts
|
|
866
1614
|
var CAPACITY = 200;
|
|
867
1615
|
var STACK_LINES = 10;
|
|
@@ -1043,7 +1791,7 @@ function toJson(value) {
|
|
|
1043
1791
|
function unwrap(definition) {
|
|
1044
1792
|
return typeof definition === "function" ? { run: definition } : definition;
|
|
1045
1793
|
}
|
|
1046
|
-
function createRegistry(getTools, logs) {
|
|
1794
|
+
function createRegistry(getTools, logs, loadId2) {
|
|
1047
1795
|
const list = () => Object.entries(getTools()).map(([name, definition]) => ({
|
|
1048
1796
|
name,
|
|
1049
1797
|
description: unwrap(definition).description
|
|
@@ -1052,7 +1800,12 @@ function createRegistry(getTools, logs) {
|
|
|
1052
1800
|
const end = logs?.begin(call.tool);
|
|
1053
1801
|
const result = await run(call, from).finally(end);
|
|
1054
1802
|
const errors = logs?.takeErrors();
|
|
1055
|
-
|
|
1803
|
+
const stamped = loadId2 ? { ...result, loadId: loadId2, ...pending() } : result;
|
|
1804
|
+
return errors?.length ? { ...stamped, logs: errors } : stamped;
|
|
1805
|
+
}
|
|
1806
|
+
function pending() {
|
|
1807
|
+
const areas = Object.keys(pendingByArea(getTools()));
|
|
1808
|
+
return areas.length ? { pending: areas } : {};
|
|
1056
1809
|
}
|
|
1057
1810
|
async function run(call, from) {
|
|
1058
1811
|
const t0 = performance.now();
|
|
@@ -1063,7 +1816,13 @@ function createRegistry(getTools, logs) {
|
|
|
1063
1816
|
const known = list().map((t) => t.name).join(", ");
|
|
1064
1817
|
throw new Error(`Unknown tool "${call.tool}". Known: ${known}`);
|
|
1065
1818
|
}
|
|
1066
|
-
const
|
|
1819
|
+
const { run: tool, maxArgs } = unwrap(definition);
|
|
1820
|
+
const args = call.args ?? [];
|
|
1821
|
+
if (maxArgs !== void 0 && args.length > maxArgs)
|
|
1822
|
+
throw new Error(
|
|
1823
|
+
`${call.tool} takes at most ${maxArgs} argument${maxArgs === 1 ? "" : "s"}, got ${args.length}. A JSON array is spread into the arguments; wrap it once more to pass an array as one argument.`
|
|
1824
|
+
);
|
|
1825
|
+
const value = await tool(...args);
|
|
1067
1826
|
return { id: call.id, from, ok: true, value: toJson(value), ms: ms() };
|
|
1068
1827
|
} catch (error) {
|
|
1069
1828
|
const message2 = error instanceof Error ? error.message : String(error);
|
|
@@ -1085,12 +1844,13 @@ function startAgentBridge(options = {}) {
|
|
|
1085
1844
|
...builtinTools(() => registry.list(), allTools, logs.capture, getScenarios),
|
|
1086
1845
|
...userTools()
|
|
1087
1846
|
});
|
|
1088
|
-
const registry = createRegistry(allTools, logs.capture);
|
|
1847
|
+
const registry = createRegistry(allTools, logs.capture, loadId);
|
|
1089
1848
|
const info = () => ({
|
|
1090
1849
|
deviceId,
|
|
1091
|
-
name: options.name ??
|
|
1092
|
-
platform:
|
|
1850
|
+
name: options.name ?? import_react_native3.Platform.OS,
|
|
1851
|
+
platform: import_react_native3.Platform.OS,
|
|
1093
1852
|
protocol: PROTOCOL_VERSION,
|
|
1853
|
+
loadId,
|
|
1094
1854
|
tools: registry.list()
|
|
1095
1855
|
});
|
|
1096
1856
|
const context = {
|