@avasapp/agent-bridge 0.4.2 → 0.6.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 +36 -14
- package/dist/adapters/ably.cjs +79 -3
- package/dist/adapters/ably.d.cts +2 -1
- package/dist/adapters/async-storage.cjs +158 -0
- package/dist/adapters/async-storage.d.cts +14 -0
- package/dist/adapters/device.cjs +1110 -0
- package/dist/adapters/device.d.cts +49 -0
- package/dist/adapters/expo-router.cjs +112 -6
- package/dist/adapters/expo-router.d.cts +1 -1
- package/dist/adapters/jotai.cjs +266 -0
- package/dist/adapters/jotai.d.cts +19 -0
- package/dist/adapters/react-native-mmkv.cjs +80 -0
- package/dist/adapters/react-native-mmkv.d.cts +3 -1
- package/dist/adapters/react-native-webview.d.cts +1 -1
- package/dist/adapters/react-navigation.cjs +293 -0
- package/dist/adapters/react-navigation.d.cts +55 -0
- package/dist/adapters/redux.cjs +322 -0
- package/dist/adapters/redux.d.cts +28 -0
- package/dist/adapters/secure-store.cjs +169 -0
- package/dist/adapters/secure-store.d.cts +28 -0
- package/dist/adapters/socket-io.cjs +77 -3
- package/dist/adapters/socket-io.d.cts +2 -1
- package/dist/adapters/tanstack-query.cjs +3 -0
- package/dist/adapters/tanstack-query.d.cts +13 -4
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-TA2OLPPT.js → chunk-Y6QFU5ON.js} +477 -165
- package/dist/cli.js +2370 -103
- package/dist/client/index.cjs +441 -136
- package/dist/client/index.d.ts +67 -4
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +320 -73
- package/dist/network/index.d.cts +18 -3
- package/dist/noop/async-storage.cjs +26 -0
- package/dist/noop/device.cjs +26 -0
- package/dist/noop/jotai.cjs +26 -0
- package/dist/noop/react-navigation.cjs +26 -0
- package/dist/noop/realtime.cjs +6 -1
- package/dist/noop/redux.cjs +28 -0
- package/dist/noop/secure-store.cjs +26 -0
- package/dist/realtime/index.cjs +71 -4
- package/dist/realtime/index.d.cts +21 -2
- package/dist/runtime/index.cjs +838 -128
- package/dist/runtime/index.d.cts +4 -2
- package/dist/store-common-j0cHHvBr.d.cts +23 -0
- package/dist/{types-BmpykP1P.d.cts → types-qlwdcGjt.d.cts} +22 -0
- package/entries/async-storage.cjs +9 -0
- package/entries/device.cjs +9 -0
- package/entries/jotai.cjs +9 -0
- package/entries/react-navigation.cjs +9 -0
- package/entries/redux.cjs +9 -0
- package/entries/secure-store.cjs +9 -0
- package/package.json +34 -2
- package/skills/agent-bridge/SKILL.md +70 -22
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +19 -8
- package/skills/agent-bridge/references/large-data.md +6 -6
- package/skills/agent-bridge/references/network.md +25 -7
- package/skills/agent-bridge/references/realtime.md +5 -5
- package/skills/agent-bridge/references/restore.md +25 -7
- package/skills/agent-bridge/references/screen.md +25 -11
- package/skills/agent-bridge/references/state.md +31 -11
- package/skills/agent-bridge/references/webview.md +9 -9
- package/skills/agent-bridge/references/writing-scenarios.md +3 -3
package/dist/runtime/index.cjs
CHANGED
|
@@ -70,6 +70,95 @@ async function runUndos() {
|
|
|
70
70
|
return { ran: batch.length, errors };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// src/runtime/tools/restore.ts
|
|
74
|
+
var SUFFIX = ".restore";
|
|
75
|
+
var SELF = "bridge.restore";
|
|
76
|
+
var LAST = "scenario.restore";
|
|
77
|
+
function pendingByArea(tools, detail = false) {
|
|
78
|
+
const out = {};
|
|
79
|
+
for (const [name, d] of Object.entries(tools)) {
|
|
80
|
+
if (!name.endsWith(SUFFIX) || name === SELF || typeof d === "function")
|
|
81
|
+
continue;
|
|
82
|
+
const area3 = name.slice(0, -SUFFIX.length);
|
|
83
|
+
try {
|
|
84
|
+
const found = d.pending?.({ detail });
|
|
85
|
+
if (found) out[area3] = found;
|
|
86
|
+
} catch (error) {
|
|
87
|
+
out[area3] = `pending check failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return out;
|
|
91
|
+
}
|
|
92
|
+
function journalByArea(tools) {
|
|
93
|
+
const out = {};
|
|
94
|
+
for (const [name, d] of restorers(tools)) {
|
|
95
|
+
try {
|
|
96
|
+
const data = d.journal?.();
|
|
97
|
+
if (data !== void 0) out[name] = data;
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
function journalRev(tools) {
|
|
104
|
+
let rev = 0;
|
|
105
|
+
for (const [, d] of restorers(tools)) rev += d.journalRev?.() ?? 0;
|
|
106
|
+
return rev;
|
|
107
|
+
}
|
|
108
|
+
function restorers(tools) {
|
|
109
|
+
return Object.entries(tools).filter(([name, d]) => name.endsWith(SUFFIX) && name !== SELF && typeof d !== "function").map(([name, d]) => [name.slice(0, -SUFFIX.length), d]).filter((e) => typeof e[1] !== "function");
|
|
110
|
+
}
|
|
111
|
+
var INTERNAL_TOOLS = ["bridge.adopt"];
|
|
112
|
+
function restoreTools(getTools) {
|
|
113
|
+
return {
|
|
114
|
+
"bridge.pending": {
|
|
115
|
+
maxArgs: 0,
|
|
116
|
+
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.",
|
|
117
|
+
run: () => pendingByArea(getTools(), true)
|
|
118
|
+
},
|
|
119
|
+
"bridge.adopt": {
|
|
120
|
+
maxArgs: 1,
|
|
121
|
+
description: "Internal: a session hands back its journal after a reload.",
|
|
122
|
+
run: async (data) => {
|
|
123
|
+
const tools = getTools();
|
|
124
|
+
const out = {};
|
|
125
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("bridge.adopt takes an object");
|
|
126
|
+
for (const [area3, part] of Object.entries(data)) {
|
|
127
|
+
const d = tools[`${area3}${SUFFIX}`];
|
|
128
|
+
if (typeof d === "function" || !d?.adopt) out[area3] = { adopted: [], already: [], changed: [], retry: part, error: "this area has no adopt" };
|
|
129
|
+
else
|
|
130
|
+
try {
|
|
131
|
+
out[area3] = await d.adopt(part);
|
|
132
|
+
} catch (error) {
|
|
133
|
+
out[area3] = { adopted: [], already: [], changed: [], retry: part, error: error instanceof Error ? error.message : String(error) };
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return out;
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
[SELF]: {
|
|
140
|
+
description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
|
|
141
|
+
run: async () => {
|
|
142
|
+
const tools = getTools();
|
|
143
|
+
const results = {};
|
|
144
|
+
const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort((a, b) => Number(a === LAST) - Number(b === LAST) || a.localeCompare(b));
|
|
145
|
+
for (const name of names) {
|
|
146
|
+
const definition = tools[name];
|
|
147
|
+
const run = typeof definition === "function" ? definition : definition.run;
|
|
148
|
+
try {
|
|
149
|
+
results[name] = await run();
|
|
150
|
+
} catch (error) {
|
|
151
|
+
results[name] = {
|
|
152
|
+
error: error instanceof Error ? error.message : String(error)
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return results;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
73
162
|
// src/runtime/tools/app.ts
|
|
74
163
|
function selectReload(host, devSettingsReload) {
|
|
75
164
|
return () => {
|
|
@@ -84,13 +173,14 @@ function selectReload(host, devSettingsReload) {
|
|
|
84
173
|
}
|
|
85
174
|
};
|
|
86
175
|
}
|
|
87
|
-
function appTools(reload) {
|
|
176
|
+
function appTools(reload, getTools) {
|
|
88
177
|
return {
|
|
89
178
|
"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.",
|
|
179
|
+
description: "Reload the app's JS (Expo's reloadAppAsync, else DevSettings.reload). Pending restores (store, query, mocks, pins, storage) are lost, and the reply names them: restore first. Returns before it reloads; a session waits for the new bridge and reports what was lost.",
|
|
91
180
|
run: () => {
|
|
92
181
|
setTimeout(reload, 50);
|
|
93
|
-
|
|
182
|
+
const lost = getTools ? Object.keys(pendingByArea(getTools())) : [];
|
|
183
|
+
return lost.length ? { reloading: true, lost: `pending restores are lost: ${lost.join(", ")}` } : { reloading: true };
|
|
94
184
|
}
|
|
95
185
|
},
|
|
96
186
|
"app.restore": {
|
|
@@ -175,55 +265,6 @@ function logTools(logs) {
|
|
|
175
265
|
};
|
|
176
266
|
}
|
|
177
267
|
|
|
178
|
-
// src/runtime/tools/restore.ts
|
|
179
|
-
var SUFFIX = ".restore";
|
|
180
|
-
var SELF = "bridge.restore";
|
|
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
|
-
}
|
|
197
|
-
function restoreTools(getTools) {
|
|
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
|
-
},
|
|
204
|
-
[SELF]: {
|
|
205
|
-
description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
|
|
206
|
-
run: async () => {
|
|
207
|
-
const tools = getTools();
|
|
208
|
-
const results = {};
|
|
209
|
-
const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort((a, b) => Number(a === LAST) - Number(b === LAST) || a.localeCompare(b));
|
|
210
|
-
for (const name of names) {
|
|
211
|
-
const definition = tools[name];
|
|
212
|
-
const run = typeof definition === "function" ? definition : definition.run;
|
|
213
|
-
try {
|
|
214
|
-
results[name] = await run();
|
|
215
|
-
} catch (error) {
|
|
216
|
-
results[name] = {
|
|
217
|
-
error: error instanceof Error ? error.message : String(error)
|
|
218
|
-
};
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
return results;
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
268
|
// src/runtime/options-schema.ts
|
|
228
269
|
var import_json_schema = require("@cfworker/json-schema");
|
|
229
270
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -465,6 +506,9 @@ var MODAL_HOSTS = /* @__PURE__ */ new Set(["RCTModalHostView", "ModalHostView"])
|
|
|
465
506
|
var OVERLAY_HOST = "RNSFullWindowOverlay";
|
|
466
507
|
var SCREEN_STACK = "RNSScreenStack";
|
|
467
508
|
var STACK_SCREENS = /* @__PURE__ */ new Set(["RNSScreen", "RNSModalScreen"]);
|
|
509
|
+
var TABS_HOSTS = /* @__PURE__ */ new Set(["RNSTabsHostIOS", "RNSTabsHostAndroid"]);
|
|
510
|
+
var OLD_TAB_SCREENS = /* @__PURE__ */ new Set(["RNSBottomTabsScreen", "RNSTabsScreen"]);
|
|
511
|
+
var TAB_SCREENS = /* @__PURE__ */ new Set(["RNSTabsScreenIOS", "RNSTabsScreenAndroid"]);
|
|
468
512
|
var SEE_THROUGH = /* @__PURE__ */ new Set(["transparentModal", "containedTransparentModal", "overFullScreen"]);
|
|
469
513
|
var undimmedSheet = (p) => {
|
|
470
514
|
const at = p.sheetLargestUndimmedDetentIndex;
|
|
@@ -500,6 +544,23 @@ function stackScreens(stack) {
|
|
|
500
544
|
presented: screens.slice(1).filter(coversWindow)
|
|
501
545
|
};
|
|
502
546
|
}
|
|
547
|
+
function unselectedTabs(tabs) {
|
|
548
|
+
const selected = propsOf(tabs)?.navStateRequest?.selectedScreenKey;
|
|
549
|
+
const out = [];
|
|
550
|
+
let matches = 0;
|
|
551
|
+
if (typeof selected !== "string") return out;
|
|
552
|
+
const todo = tabs.child ? [tabs.child] : [];
|
|
553
|
+
while (todo.length) {
|
|
554
|
+
const f = todo.pop();
|
|
555
|
+
if (f.sibling) todo.push(f.sibling);
|
|
556
|
+
if (f.tag === HOST_COMPONENT && TAB_SCREENS.has(f.type)) {
|
|
557
|
+
if (propsOf(f)?.screenKey === selected) matches++;
|
|
558
|
+
else out.push(f);
|
|
559
|
+
} else if (f.child) todo.push(f.child);
|
|
560
|
+
}
|
|
561
|
+
return matches === 1 ? out : [];
|
|
562
|
+
}
|
|
563
|
+
var hasDom = (recs) => recs.some((r) => !!r.host.stateNode && "getBoundingClientRect" in r.host.stateNode);
|
|
503
564
|
function hostAbove(host) {
|
|
504
565
|
let up = host.return;
|
|
505
566
|
while (up && up.tag !== HOST_COMPONENT) up = up.return;
|
|
@@ -513,9 +574,50 @@ var inside = (modal, top) => {
|
|
|
513
574
|
};
|
|
514
575
|
var SWITCH_HOSTS = /* @__PURE__ */ new Set(["RCTSwitch", "AndroidSwitch"]);
|
|
515
576
|
var isFn = (v) => typeof v === "function";
|
|
577
|
+
function isSwitchFiber(fiber) {
|
|
578
|
+
const p = propsOf(fiber);
|
|
579
|
+
if ((p?.accessibilityRole ?? p?.role) === "switch") return true;
|
|
580
|
+
const own = firstHost(fiber);
|
|
581
|
+
return !!own && SWITCH_HOSTS.has(String(own.type));
|
|
582
|
+
}
|
|
516
583
|
var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && typeof p.value !== "boolean" && ("value" in p || "defaultValue" in p || "placeholder" in p);
|
|
517
584
|
var labelOf = (p) => p.accessibilityLabel ?? p["aria-label"];
|
|
518
|
-
|
|
585
|
+
function actionNamesOf(p) {
|
|
586
|
+
if (!isFn(p.onAccessibilityAction) || !Array.isArray(p.accessibilityActions)) return null;
|
|
587
|
+
return p.accessibilityActions.map((a) => a?.name).filter((name) => typeof name === "string");
|
|
588
|
+
}
|
|
589
|
+
var activates = (rec) => !!rec.actions && !!rec.element.actions?.includes("activate");
|
|
590
|
+
var TEXT_HOSTS = /^RCT(Virtual)?Text$/;
|
|
591
|
+
var changesValue = (p) => isFn(p.onValueChange) || isFn(p.onSlidingComplete) || Array.isArray(p.values) && isFn(p.onChange);
|
|
592
|
+
var isControl = (p) => isFn(p.onPress) || changesValue(p) || activatesProps(p);
|
|
593
|
+
var activatesProps = (p) => !!actionNamesOf(p)?.includes("activate");
|
|
594
|
+
function wrapsControls(rec) {
|
|
595
|
+
const skip = /* @__PURE__ */ new Set();
|
|
596
|
+
const markChain = (from) => {
|
|
597
|
+
for (let f = from; f; f = f.child) {
|
|
598
|
+
skip.add(f);
|
|
599
|
+
if (f.tag === HOST_COMPONENT) break;
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
markChain(rec.fiber);
|
|
603
|
+
let controls = 0;
|
|
604
|
+
const stack = rec.fiber.child ? [rec.fiber.child] : [];
|
|
605
|
+
while (stack.length) {
|
|
606
|
+
const f = stack.pop();
|
|
607
|
+
if (f.sibling) stack.push(f.sibling);
|
|
608
|
+
if (f.child) stack.push(f.child);
|
|
609
|
+
const p = propsOf(f);
|
|
610
|
+
if (!p || skip.has(f)) continue;
|
|
611
|
+
if (isInputProps(p)) return true;
|
|
612
|
+
if (!isControl(p)) continue;
|
|
613
|
+
const own = firstHost(f);
|
|
614
|
+
if (isFn(p.onPress) && !isFn(p.onValueChange) && own && TEXT_HOSTS.test(String(own.type))) continue;
|
|
615
|
+
markChain(f);
|
|
616
|
+
if (++controls >= 2) return true;
|
|
617
|
+
}
|
|
618
|
+
return false;
|
|
619
|
+
}
|
|
620
|
+
var interesting = (p) => isFn(p.onPress) || actionNamesOf(p) !== null || changesValue(p) || isInputProps(p) || typeof p.testID === "string" || typeof labelOf(p) === "string" || webViewNameOf(p) !== void 0;
|
|
519
621
|
var ICON_GLYPHS = /[\uE000-\uF8FF]/g;
|
|
520
622
|
function firstHost(fiber) {
|
|
521
623
|
if (fiber.tag === HOST_COMPONENT) return fiber;
|
|
@@ -532,6 +634,12 @@ function absorb(rec, fiber, p) {
|
|
|
532
634
|
const e = rec.element;
|
|
533
635
|
if (!rec.input && isInputProps(p)) rec.input = fiber;
|
|
534
636
|
if (!rec.press && isFn(p.onPress)) rec.press = fiber;
|
|
637
|
+
if (!rec.change && changesValue(p)) rec.change = fiber;
|
|
638
|
+
const actions = actionNamesOf(p);
|
|
639
|
+
if (!rec.actions && actions) {
|
|
640
|
+
rec.actions = fiber;
|
|
641
|
+
if (actions.length) e.actions = actions;
|
|
642
|
+
}
|
|
535
643
|
const fill = (key, v) => {
|
|
536
644
|
if (e[key] === void 0 && typeof v === "string") e[key] = v;
|
|
537
645
|
};
|
|
@@ -579,14 +687,14 @@ var within = (host, ancestor) => {
|
|
|
579
687
|
};
|
|
580
688
|
function visibleArea(base, host, bars, window) {
|
|
581
689
|
let area3 = base;
|
|
582
|
-
const
|
|
690
|
+
const clip = (r) => {
|
|
583
691
|
area3 = area3 && intersect(area3, r);
|
|
584
692
|
};
|
|
585
693
|
for (const bar of bars) {
|
|
586
694
|
if (within(host, bar)) continue;
|
|
587
695
|
const r = measureHost(bar);
|
|
588
696
|
if (!usable(r) || r.width < window.width * 0.9) continue;
|
|
589
|
-
|
|
697
|
+
clip(
|
|
590
698
|
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
699
|
);
|
|
592
700
|
}
|
|
@@ -617,8 +725,8 @@ var round = (r) => r && {
|
|
|
617
725
|
var handles = (control, p, name) => !!control && isFn(p[name]) && propsOf(control)?.[name] === p[name];
|
|
618
726
|
function sameControl(rec, p) {
|
|
619
727
|
for (let r = rec; r; r = r.parent) {
|
|
620
|
-
if (!r.press && !r.input) continue;
|
|
621
|
-
const same = handles(r.press, p, "onPress") || handles(r.input, p, "onChangeText") || handles(r.input, p, "onChange");
|
|
728
|
+
if (!r.press && !r.input && !r.actions) continue;
|
|
729
|
+
const same = handles(r.press, p, "onPress") || handles(r.actions, p, "onAccessibilityAction") || handles(r.input, p, "onChangeText") || handles(r.input, p, "onChange");
|
|
622
730
|
return same ? r : null;
|
|
623
731
|
}
|
|
624
732
|
return null;
|
|
@@ -648,6 +756,8 @@ function collectScreen(roots, window) {
|
|
|
648
756
|
host,
|
|
649
757
|
press: null,
|
|
650
758
|
input: null,
|
|
759
|
+
change: null,
|
|
760
|
+
actions: null,
|
|
651
761
|
hidden,
|
|
652
762
|
scroller,
|
|
653
763
|
modal,
|
|
@@ -660,8 +770,14 @@ function collectScreen(roots, window) {
|
|
|
660
770
|
order.push(rec);
|
|
661
771
|
return rec;
|
|
662
772
|
};
|
|
773
|
+
const wrapper = /* @__PURE__ */ new Map();
|
|
663
774
|
const buttonOf = (rec) => {
|
|
664
|
-
for (let r = rec; r; r = r.parent)
|
|
775
|
+
for (let r = rec; r; r = r.parent)
|
|
776
|
+
if (r.press || activates(r)) {
|
|
777
|
+
let wraps = wrapper.get(r);
|
|
778
|
+
if (wraps === void 0) wrapper.set(r, wraps = wrapsControls(r));
|
|
779
|
+
return wraps ? null : r;
|
|
780
|
+
}
|
|
665
781
|
return null;
|
|
666
782
|
};
|
|
667
783
|
const textHost = (host) => {
|
|
@@ -696,6 +812,8 @@ function collectScreen(roots, window) {
|
|
|
696
812
|
for (const s of screens.covered) covered.add(s);
|
|
697
813
|
for (const s of screens.presented) presented.add(s);
|
|
698
814
|
}
|
|
815
|
+
if (OLD_TAB_SCREENS.has(fiber.type) && p?.isFocused === false) hidden = true;
|
|
816
|
+
if (TABS_HOSTS.has(fiber.type)) for (const s of unselectedTabs(fiber)) covered.add(s);
|
|
699
817
|
if (fiber.type === OVERLAY_HOST) overlay = true;
|
|
700
818
|
if (p && !hidden && (p.accessibilityRole ?? p.role) === "tablist") bars.push(fiber);
|
|
701
819
|
if (!hidden && (MODAL_HOSTS.has(fiber.type) || presented.has(fiber))) {
|
|
@@ -747,19 +865,22 @@ function collectScreen(roots, window) {
|
|
|
747
865
|
const covers2 = (x) => !!top && !x.overlay && !inside(x.modal, top);
|
|
748
866
|
for (const scrollable of scrollByHost.values())
|
|
749
867
|
scrollable.hidden || (scrollable.hidden = covers2(scrollable));
|
|
868
|
+
let measured = false;
|
|
750
869
|
for (const rec of order) {
|
|
751
870
|
const e = rec.element;
|
|
752
871
|
rec.hidden || (rec.hidden = covers2(rec));
|
|
753
872
|
const text = rec.parts.map((part) => part.text.replace(ICON_GLYPHS, "").replace(/\s+/g, " ").trim()).filter(Boolean).join(" ");
|
|
754
873
|
if (text) e.text = text;
|
|
755
|
-
e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press ? "button" : rec.parts.length ? "text" : "view";
|
|
874
|
+
e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press || activates(rec) ? "button" : rec.parts.length ? "text" : "view";
|
|
756
875
|
if (e.kind === "input" && e.editable === void 0) e.editable = true;
|
|
757
876
|
const rect = measureHost(rec.host);
|
|
877
|
+
if (rect) measured = true;
|
|
758
878
|
e.rect = round(rect);
|
|
759
879
|
const own = scrollByHost.get(rec.host) ?? scrollByHost.get(hostAbove(rec.host));
|
|
760
880
|
if (own) rec.scroller = own;
|
|
761
881
|
rec.onScreen = isOnScreen(rec, rect, window, bars);
|
|
762
882
|
}
|
|
883
|
+
const unmeasured = order.length > 0 && !measured && !hasDom(order) && !globalThis.nativeFabricUIManager;
|
|
763
884
|
const named = /* @__PURE__ */ new Set();
|
|
764
885
|
const single = (rec) => {
|
|
765
886
|
const name = rec.element.webview;
|
|
@@ -772,15 +893,30 @@ function collectScreen(roots, window) {
|
|
|
772
893
|
found: order.filter(
|
|
773
894
|
(rec) => (rec.element.kind !== "text" || rec.element.text !== void 0) && single(rec)
|
|
774
895
|
),
|
|
775
|
-
scrollables: [...scrollByHost.values()]
|
|
896
|
+
scrollables: [...scrollByHost.values()],
|
|
897
|
+
unmeasured
|
|
776
898
|
};
|
|
777
899
|
}
|
|
900
|
+
var UNMEASURED_ERROR = "No Fabric UIManager found (React Native's old architecture, or a test renderer), so elements can't be measured. agent-bridge screen tools need the New Architecture.";
|
|
778
901
|
var focused = (found) => found.filter((f) => !f.hidden);
|
|
779
902
|
function pressFiberOf(found) {
|
|
780
|
-
if (found.press) return found.press;
|
|
903
|
+
if (found.press) return { fiber: found.press, via: "onPress", ancestor: false };
|
|
904
|
+
if (activates(found))
|
|
905
|
+
return { fiber: found.actions, via: "accessibilityAction", ancestor: false };
|
|
906
|
+
for (let f = found.fiber.return; f; f = f.return) {
|
|
907
|
+
const p = propsOf(f);
|
|
908
|
+
if (!p) continue;
|
|
909
|
+
if (isFn(p.onPress)) return { fiber: f, via: "onPress", ancestor: true };
|
|
910
|
+
if (actionNamesOf(p)?.includes("activate"))
|
|
911
|
+
return { fiber: f, via: "accessibilityAction", ancestor: true };
|
|
912
|
+
}
|
|
913
|
+
return null;
|
|
914
|
+
}
|
|
915
|
+
function actionFiberOf(found, name) {
|
|
916
|
+
if (found.actions && found.element.actions?.includes(name)) return found.actions;
|
|
781
917
|
for (let f = found.fiber.return; f; f = f.return) {
|
|
782
918
|
const p = propsOf(f);
|
|
783
|
-
if (p &&
|
|
919
|
+
if (p && actionNamesOf(p)?.includes(name)) return f;
|
|
784
920
|
}
|
|
785
921
|
return null;
|
|
786
922
|
}
|
|
@@ -824,7 +960,7 @@ function tiers(target) {
|
|
|
824
960
|
(e) => fields(e) && !!e.text?.includes(text)
|
|
825
961
|
];
|
|
826
962
|
}
|
|
827
|
-
var labelOnly = ({ element: e, press, input }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input;
|
|
963
|
+
var labelOnly = ({ element: e, press, input, actions }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input && !actions;
|
|
828
964
|
function matchTarget(found, target) {
|
|
829
965
|
checkTarget(target);
|
|
830
966
|
for (const pool of [found.filter((f) => !labelOnly(f)), found.filter(labelOnly)]) {
|
|
@@ -838,15 +974,16 @@ function matchTarget(found, target) {
|
|
|
838
974
|
}
|
|
839
975
|
var isAt = (target) => typeof target === "object" && target.at !== void 0;
|
|
840
976
|
var indexOf = (target) => typeof target === "object" ? target.index : void 0;
|
|
841
|
-
|
|
842
|
-
|
|
977
|
+
function describe(e, max = 40, wholeIds = false) {
|
|
978
|
+
const clip = (s) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
979
|
+
const id = (s) => wholeIds ? s : clip(s);
|
|
843
980
|
const stated = e.checked !== void 0 || e.selected !== void 0 || e.expanded !== void 0;
|
|
844
981
|
const bits = [stated && e.role ? e.role : e.kind];
|
|
845
982
|
if (e.webview !== void 0) bits.push(JSON.stringify(e.webview));
|
|
846
983
|
if (e.testID) bits.push(`#${e.testID}`);
|
|
847
984
|
if (e.text) bits.push(JSON.stringify(clip(e.text)));
|
|
848
|
-
if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(
|
|
849
|
-
if (e.placeholder) bits.push(`placeholder=${JSON.stringify(
|
|
985
|
+
if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(id(e.label))}`);
|
|
986
|
+
if (e.placeholder) bits.push(`placeholder=${JSON.stringify(id(e.placeholder))}`);
|
|
850
987
|
if (e.value !== void 0) bits.push(`value=${JSON.stringify(clip(e.value))}`);
|
|
851
988
|
if (e.checked !== void 0) bits.push(e.checked === true ? "checked" : e.checked === false ? "unchecked" : "mixed");
|
|
852
989
|
if (e.selected !== void 0) bits.push(e.selected ? "selected" : "unselected");
|
|
@@ -868,7 +1005,8 @@ function resolveTarget(found, target, prefer, options) {
|
|
|
868
1005
|
reachable.filter((f) => f.onScreen),
|
|
869
1006
|
target
|
|
870
1007
|
);
|
|
871
|
-
|
|
1008
|
+
const hasIndex = (options?.index ?? indexOf(target)) !== void 0;
|
|
1009
|
+
if (options?.offscreen && (!matches.length || hasIndex)) matches = matchTarget(reachable, target);
|
|
872
1010
|
if (!matches.length) {
|
|
873
1011
|
const hidden = matchTarget(found, target)[0];
|
|
874
1012
|
if (hidden?.hidden)
|
|
@@ -879,7 +1017,7 @@ function resolveTarget(found, target, prefer, options) {
|
|
|
879
1017
|
throw new Error(
|
|
880
1018
|
`${showTarget(target)} matches ${describe(hidden.element)}, which is not on screen. Pass {scroll:true} to scroll it into view, or use screen.scroll`
|
|
881
1019
|
);
|
|
882
|
-
const unrendered = options?.offscreen ? ". A list renders only the rows near its window: for a row it has not rendered yet
|
|
1020
|
+
const unrendered = options?.offscreen ? "" : ". A list renders only the rows near its window: pass {scroll:true} to page it for a row it has not rendered yet";
|
|
883
1021
|
throw new Error(
|
|
884
1022
|
`Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}${unrendered}`
|
|
885
1023
|
);
|
|
@@ -896,10 +1034,10 @@ function resolveTarget(found, target, prefer, options) {
|
|
|
896
1034
|
return pick;
|
|
897
1035
|
}
|
|
898
1036
|
if (matches.length > 1) {
|
|
899
|
-
const
|
|
1037
|
+
const list2 = matches.slice(0, 10).map((f, i) => `${i}: ${describe(f.element)}`).join("; ");
|
|
900
1038
|
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)`;
|
|
901
1039
|
throw new Error(
|
|
902
|
-
`${showTarget(target)} matches ${matches.length} elements; pick one with an index, e.g. ${example}, or use a narrower target. ${
|
|
1040
|
+
`${showTarget(target)} matches ${matches.length} elements; pick one with an index, e.g. ${example}, or use a narrower target. ${list2}`
|
|
903
1041
|
);
|
|
904
1042
|
}
|
|
905
1043
|
return matches[0];
|
|
@@ -924,42 +1062,269 @@ var event = (nativeEvent, target) => ({
|
|
|
924
1062
|
persist: noop
|
|
925
1063
|
});
|
|
926
1064
|
var eventCount = 0;
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
1065
|
+
var has = (v) => v !== void 0 && v !== null;
|
|
1066
|
+
var isUncontrolled = (found) => !!found.input && !has(propsOf(found.input)?.value) && !has(propsOf(found.host)?.value);
|
|
1067
|
+
function setNativeText(found, text) {
|
|
1068
|
+
for (const fiber of [found.host, found.input]) {
|
|
1069
|
+
const node = fiber?.stateNode;
|
|
1070
|
+
for (const target of [node, node?.canonical?.publicInstance]) {
|
|
1071
|
+
if (target && typeof target.setNativeProps === "function") {
|
|
1072
|
+
target.setNativeProps({ text });
|
|
1073
|
+
return true;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
const dom = found.host.stateNode;
|
|
1078
|
+
if (dom && typeof dom === "object" && "value" in dom) {
|
|
1079
|
+
dom.value = text;
|
|
1080
|
+
return true;
|
|
1081
|
+
}
|
|
1082
|
+
return false;
|
|
1083
|
+
}
|
|
1084
|
+
var keysOf = (text) => {
|
|
1085
|
+
const Segmenter = Intl.Segmenter;
|
|
1086
|
+
return Segmenter ? Array.from(new Segmenter(void 0, { granularity: "grapheme" }).segment(text), (x) => x.segment) : Array.from(text);
|
|
1087
|
+
};
|
|
1088
|
+
async function fillInput(found, text, options = {}) {
|
|
1089
|
+
if (!found.input) throw new Error(`${describe(found.element)} is not a text input`);
|
|
930
1090
|
if (found.element.editable === false)
|
|
931
1091
|
throw new Error(`${describe(found.element)} is not editable`);
|
|
932
1092
|
const { maxLength } = found;
|
|
933
1093
|
const value = maxLength === void 0 ? text : text.slice(0, Math.max(0, maxLength));
|
|
1094
|
+
const uncontrolled = isUncontrolled(found);
|
|
1095
|
+
let native = uncontrolled ? false : null;
|
|
1096
|
+
let current = found;
|
|
1097
|
+
const change = (next) => {
|
|
1098
|
+
const target2 = { value: next };
|
|
1099
|
+
if (uncontrolled && setNativeText(current, next)) native = true;
|
|
1100
|
+
const input2 = current.input;
|
|
1101
|
+
handler(input2, "onChangeText")?.(next);
|
|
1102
|
+
eventCount += 1;
|
|
1103
|
+
handler(input2, "onChange")?.(event({ text: next, eventCount }, target2));
|
|
1104
|
+
};
|
|
934
1105
|
const target = { value };
|
|
935
|
-
handler(input, "onFocus")?.(event({ text: value }, target));
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
1106
|
+
handler(found.input, "onFocus")?.(event({ text: value }, target));
|
|
1107
|
+
if (options.type && value) {
|
|
1108
|
+
const keys = keysOf(value);
|
|
1109
|
+
let typed = "";
|
|
1110
|
+
for (const key of keys) {
|
|
1111
|
+
typed += key;
|
|
1112
|
+
handler(current.input, "onKeyPress")?.(event({ key: key === "\n" ? "Enter" : key }));
|
|
1113
|
+
change(typed);
|
|
1114
|
+
if (options.between) await options.between();
|
|
1115
|
+
if (options.again) current = options.again(current);
|
|
1116
|
+
}
|
|
1117
|
+
} else change(value);
|
|
1118
|
+
const input = current.input;
|
|
1119
|
+
if (options.submit) handler(input, "onSubmitEditing")?.(event({ text: value }, target));
|
|
1120
|
+
handler(input, "onEndEditing")?.(event({ text: value }, target));
|
|
941
1121
|
handler(input, "onBlur")?.(event({ text: value }, target));
|
|
942
|
-
return value;
|
|
1122
|
+
return { filled: value, native };
|
|
943
1123
|
}
|
|
944
1124
|
var center = (rect) => rect ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } : { x: 0, y: 0 };
|
|
1125
|
+
var disabledProps = (fiber) => {
|
|
1126
|
+
const p = propsOf(fiber);
|
|
1127
|
+
const state = p?.accessibilityState;
|
|
1128
|
+
return p?.disabled === true || state?.disabled === true || p?.["aria-disabled"] === true;
|
|
1129
|
+
};
|
|
1130
|
+
function refuseDisabled(found, fiber, force) {
|
|
1131
|
+
if (!force && (found.element.disabled || disabledProps(fiber)))
|
|
1132
|
+
throw new Error(
|
|
1133
|
+
`${describe(found.element)} is disabled; pass {"force":true} as the last argument to act on it anyway`
|
|
1134
|
+
);
|
|
1135
|
+
}
|
|
1136
|
+
function pressTargetOf(found, action) {
|
|
1137
|
+
if (action === void 0) return pressFiberOf(found);
|
|
1138
|
+
const fiber = actionFiberOf(found, action);
|
|
1139
|
+
return fiber && { fiber, via: "accessibilityAction", ancestor: fiber !== found.actions };
|
|
1140
|
+
}
|
|
1141
|
+
var isFn2 = (v) => typeof v === "function";
|
|
1142
|
+
var isToggle = (found) => !found.press && !!found.change && isFn2(propsOf(found.change)?.onValueChange) && isSwitchFiber(found.change);
|
|
1143
|
+
var HOLD = ["onPress", "onPressIn", "onLongPress"];
|
|
1144
|
+
function longPressFiberOf(found) {
|
|
1145
|
+
let handlesPress = false;
|
|
1146
|
+
for (let f = found.fiber; f; f = f.child) {
|
|
1147
|
+
const p = propsOf(f);
|
|
1148
|
+
if (isFn2(p?.onLongPress)) return { fiber: f, ancestor: false };
|
|
1149
|
+
if (p && HOLD.some((k) => isFn2(p[k]))) handlesPress = true;
|
|
1150
|
+
if (f === found.host) break;
|
|
1151
|
+
}
|
|
1152
|
+
if (handlesPress) return null;
|
|
1153
|
+
for (let f = found.fiber.return; f; f = f.return) {
|
|
1154
|
+
const p = propsOf(f);
|
|
1155
|
+
if (isFn2(p?.onLongPress)) return { fiber: f, ancestor: true };
|
|
1156
|
+
if (p && HOLD.some((k) => isFn2(p[k]))) return null;
|
|
1157
|
+
}
|
|
1158
|
+
return null;
|
|
1159
|
+
}
|
|
1160
|
+
var TAP_GESTURES = /* @__PURE__ */ new Set(["TapGestureHandler", "LongPressGestureHandler"]);
|
|
1161
|
+
function isTapGesture(g) {
|
|
1162
|
+
if (!g || typeof g !== "object") return false;
|
|
1163
|
+
const x = g;
|
|
1164
|
+
if (typeof x.handlerName === "string" && TAP_GESTURES.has(x.handlerName)) return true;
|
|
1165
|
+
if (typeof x.type === "string" && TAP_GESTURES.has(x.type)) return true;
|
|
1166
|
+
return Array.isArray(x.gestures) && x.gestures.some(isTapGesture);
|
|
1167
|
+
}
|
|
1168
|
+
function tapDetectorBetween(found, until) {
|
|
1169
|
+
for (let f = found.fiber; f; f = f.child) {
|
|
1170
|
+
if (isTapGesture(propsOf(f)?.gesture)) return true;
|
|
1171
|
+
if (f === found.host) break;
|
|
1172
|
+
}
|
|
1173
|
+
for (let f = found.fiber.return; f && f !== until; f = f.return) {
|
|
1174
|
+
const p = propsOf(f);
|
|
1175
|
+
if (isTapGesture(p?.gesture)) return true;
|
|
1176
|
+
if (!until && p && HOLD.some((k) => isFn2(p[k]))) break;
|
|
1177
|
+
}
|
|
1178
|
+
return false;
|
|
1179
|
+
}
|
|
1180
|
+
var GESTURE_NOTE = "it is inside a react-native-gesture-handler GestureDetector, whose callbacks run on the UI thread and cannot be fired from here, so use a real tap (agent-device)";
|
|
1181
|
+
var gestureNote = (found) => tapDetectorBetween(found, null) ? `; ${GESTURE_NOTE}` : "";
|
|
945
1182
|
function pressElement(found, options = {}) {
|
|
946
|
-
const
|
|
947
|
-
if (
|
|
948
|
-
if (
|
|
1183
|
+
const { action } = options;
|
|
1184
|
+
if (options.long) return longPress(found, options);
|
|
1185
|
+
if (action === void 0 && isToggle(found)) {
|
|
1186
|
+
refuseDisabled(found, found.change, options.force);
|
|
1187
|
+
const now2 = propsOf(found.change)?.value;
|
|
1188
|
+
handler(found.change, "onValueChange")?.(now2 !== true);
|
|
1189
|
+
return "onValueChange";
|
|
1190
|
+
}
|
|
1191
|
+
const target = pressTargetOf(found, action);
|
|
1192
|
+
if (!target) {
|
|
1193
|
+
const names = found.element.actions ?? [];
|
|
1194
|
+
const has2 = names.length ? `; it has ${names.map((a) => JSON.stringify(a)).join(", ")}` : "";
|
|
1195
|
+
throw new Error(
|
|
1196
|
+
action === void 0 ? `${describe(found.element)} has no onPress or "activate" accessibility action${gestureNote(found)}` : `${describe(found.element)} has no accessibility action ${JSON.stringify(action)}${has2}`
|
|
1197
|
+
);
|
|
1198
|
+
}
|
|
1199
|
+
const { fiber, via, ancestor } = target;
|
|
1200
|
+
if (ancestor && action === void 0 && tapDetectorBetween(found, fiber))
|
|
949
1201
|
throw new Error(
|
|
950
|
-
`${describe(found.element)}
|
|
1202
|
+
`${describe(found.element)} has no onPress of its own, and ${GESTURE_NOTE}; pressing the ancestor that has one would do something else`
|
|
951
1203
|
);
|
|
1204
|
+
const from = ancestor ? "ancestor:" : "";
|
|
1205
|
+
refuseDisabled(found, fiber, options.force);
|
|
1206
|
+
if (via === "accessibilityAction") {
|
|
1207
|
+
const actionName = action ?? "activate";
|
|
1208
|
+
handler(fiber, "onAccessibilityAction")?.(event({ actionName }));
|
|
1209
|
+
return `${from}accessibilityAction:${actionName}`;
|
|
1210
|
+
}
|
|
1211
|
+
const e = () => pressEvent(found);
|
|
1212
|
+
handler(fiber, "onPressIn")?.(e());
|
|
1213
|
+
handler(fiber, "onPress")?.(e());
|
|
1214
|
+
handler(fiber, "onPressOut")?.(e());
|
|
1215
|
+
return ancestor ? "ancestor" : "onPress";
|
|
1216
|
+
}
|
|
1217
|
+
var pressEvent = (found) => {
|
|
952
1218
|
const { x, y } = center(found.element.rect);
|
|
953
|
-
|
|
1219
|
+
return event({
|
|
954
1220
|
locationX: found.element.rect ? found.element.rect.width / 2 : 0,
|
|
955
1221
|
locationY: found.element.rect ? found.element.rect.height / 2 : 0,
|
|
956
1222
|
pageX: x,
|
|
957
1223
|
pageY: y,
|
|
958
1224
|
timestamp: Date.now()
|
|
959
1225
|
});
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1226
|
+
};
|
|
1227
|
+
function longPress(found, options) {
|
|
1228
|
+
if (options.action !== void 0)
|
|
1229
|
+
throw new Error("{long:true} and {action} cannot be combined: long presses call onLongPress, actions send an accessibility action");
|
|
1230
|
+
const target = longPressFiberOf(found);
|
|
1231
|
+
if (!target)
|
|
1232
|
+
throw new Error(`${describe(found.element)} has no onLongPress${gestureNote(found)}`);
|
|
1233
|
+
refuseDisabled(found, target.fiber, options.force);
|
|
1234
|
+
handler(target.fiber, "onPressIn")?.(pressEvent(found));
|
|
1235
|
+
handler(target.fiber, "onLongPress")?.(pressEvent(found));
|
|
1236
|
+
handler(target.fiber, "onPressOut")?.(pressEvent(found));
|
|
1237
|
+
return target.ancestor ? "ancestor:onLongPress" : "onLongPress";
|
|
1238
|
+
}
|
|
1239
|
+
function itemsOfChildren(children, out = []) {
|
|
1240
|
+
if (Array.isArray(children)) for (const c of children) itemsOfChildren(c, out);
|
|
1241
|
+
else if (children && typeof children === "object") {
|
|
1242
|
+
const p = children.props;
|
|
1243
|
+
if (p && has(p.value) && typeof p.label === "string") out.push({ value: p.value, label: p.label });
|
|
1244
|
+
else if (p) itemsOfChildren(p.children, out);
|
|
1245
|
+
}
|
|
1246
|
+
return out;
|
|
1247
|
+
}
|
|
1248
|
+
function pickerItems(fiber) {
|
|
1249
|
+
const items = itemsOfChildren(propsOf(fiber)?.children);
|
|
1250
|
+
if (items.length) return items;
|
|
1251
|
+
const stack = fiber.child ? [fiber.child] : [];
|
|
1252
|
+
while (stack.length) {
|
|
1253
|
+
const f = stack.pop();
|
|
1254
|
+
if (f.sibling) stack.push(f.sibling);
|
|
1255
|
+
if (f.child) stack.push(f.child);
|
|
1256
|
+
const p = propsOf(f);
|
|
1257
|
+
if (p && has(p.value) && typeof p.label === "string") items.push({ value: p.value, label: p.label });
|
|
1258
|
+
}
|
|
1259
|
+
return items;
|
|
1260
|
+
}
|
|
1261
|
+
function isSlider(fiber) {
|
|
1262
|
+
const p = propsOf(fiber);
|
|
1263
|
+
if (["onSlidingStart", "onSlidingComplete", "minimumValue", "maximumValue", "step"].some((k) => k in p))
|
|
1264
|
+
return true;
|
|
1265
|
+
const own = firstHost(fiber);
|
|
1266
|
+
return !!own && /Slider/.test(String(own.type));
|
|
1267
|
+
}
|
|
1268
|
+
function valueOf(found) {
|
|
1269
|
+
const p = found.change && propsOf(found.change);
|
|
1270
|
+
if (!p) return void 0;
|
|
1271
|
+
if (has(p.value)) return p.value;
|
|
1272
|
+
if (has(p.selectedValue)) return p.selectedValue;
|
|
1273
|
+
if (Array.isArray(p.values) && typeof p.selectedIndex === "number") return p.values[p.selectedIndex];
|
|
1274
|
+
return void 0;
|
|
1275
|
+
}
|
|
1276
|
+
var list = (xs) => xs.map((x) => JSON.stringify(x)).join(", ");
|
|
1277
|
+
function setControlValue(found, value, options = {}) {
|
|
1278
|
+
const fiber = found.change;
|
|
1279
|
+
const what = describe(found.element);
|
|
1280
|
+
if (!fiber)
|
|
1281
|
+
throw new Error(`${what} has no onValueChange (a Switch, Slider, Picker or SegmentedControl has one)`);
|
|
1282
|
+
refuseDisabled(found, fiber, options.force);
|
|
1283
|
+
const p = propsOf(fiber);
|
|
1284
|
+
const call = (name, ...args) => handler(fiber, name)?.(...args);
|
|
1285
|
+
if (isSwitchFiber(fiber)) {
|
|
1286
|
+
if (typeof value !== "boolean") throw new Error(`${what} is a switch: pass true or false; got ${JSON.stringify(value)}`);
|
|
1287
|
+
return void call("onValueChange", value);
|
|
1288
|
+
}
|
|
1289
|
+
if (Array.isArray(p.values)) {
|
|
1290
|
+
if (!p.values.includes(value))
|
|
1291
|
+
throw new Error(`${what} takes one of ${list(p.values)}; got ${JSON.stringify(value)}`);
|
|
1292
|
+
const selectedSegmentIndex = p.values.indexOf(value);
|
|
1293
|
+
call("onChange", event({ value, selectedSegmentIndex }));
|
|
1294
|
+
return void call("onValueChange", value);
|
|
1295
|
+
}
|
|
1296
|
+
const items = pickerItems(fiber);
|
|
1297
|
+
if (items.length || "selectedValue" in p) {
|
|
1298
|
+
const index = items.findIndex((i) => i.value === value);
|
|
1299
|
+
if (!items.length) throw new Error(`${what} is a picker but no Picker.Item children could be read`);
|
|
1300
|
+
if (index < 0)
|
|
1301
|
+
throw new Error(`${what} takes one of ${list(items.map((i) => i.value))}; got ${JSON.stringify(value)}`);
|
|
1302
|
+
return void call("onValueChange", value, index);
|
|
1303
|
+
}
|
|
1304
|
+
const current = valueOf(found);
|
|
1305
|
+
if (isSlider(fiber) || typeof current === "number") {
|
|
1306
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1307
|
+
throw new Error(`${what} takes a number; got ${JSON.stringify(value)}`);
|
|
1308
|
+
const slider = isSlider(fiber);
|
|
1309
|
+
const min = typeof p.minimumValue === "number" ? p.minimumValue : slider ? 0 : void 0;
|
|
1310
|
+
const max = typeof p.maximumValue === "number" ? p.maximumValue : slider ? 1 : void 0;
|
|
1311
|
+
if (min !== void 0 && value < min || max !== void 0 && value > max)
|
|
1312
|
+
throw new Error(`${what} takes a number between ${min ?? "-\u221E"} and ${max ?? "\u221E"}; got ${value}`);
|
|
1313
|
+
const step = p.step;
|
|
1314
|
+
if (typeof step === "number" && step > 0) {
|
|
1315
|
+
const n = (value - (min ?? 0)) / step;
|
|
1316
|
+
if (Math.abs(n - Math.round(n)) > 1e-6) {
|
|
1317
|
+
const lo = (min ?? 0) + Math.floor(n) * step;
|
|
1318
|
+
throw new Error(`${what} moves in steps of ${step}; got ${value} (nearest: ${lo}, ${lo + step})`);
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
call("onSlidingStart", value);
|
|
1322
|
+
call("onValueChange", value);
|
|
1323
|
+
return void call("onSlidingComplete", value);
|
|
1324
|
+
}
|
|
1325
|
+
if (typeof current === "string" && typeof value !== "string")
|
|
1326
|
+
throw new Error(`${what} takes a string; got ${JSON.stringify(value)}`);
|
|
1327
|
+
call("onValueChange", value);
|
|
963
1328
|
}
|
|
964
1329
|
|
|
965
1330
|
// src/runtime/screen/find-text.ts
|
|
@@ -1191,6 +1556,63 @@ function mainScrollable(scrollables, window, need) {
|
|
|
1191
1556
|
);
|
|
1192
1557
|
return best.s;
|
|
1193
1558
|
}
|
|
1559
|
+
var isVirtualized = (s) => s.fibers.some((f) => typeof propsOf(f)?.renderItem === "function");
|
|
1560
|
+
function pagingList(scrollables, window) {
|
|
1561
|
+
const all = scrollables.filter((s) => !s.hidden && scrollEnabled(s) && isVirtualized(s)).map((s) => ({ s, rect: measureHost(s.host) })).filter(({ rect }) => onWindow(rect, window)).sort((a, b) => area2(b.rect) - area2(a.rect)).map(({ s }) => s);
|
|
1562
|
+
const list2 = all.find((s) => !isHorizontal(s)) ?? all[0] ?? null;
|
|
1563
|
+
return { list: list2, skipped: all.filter((s) => s !== list2) };
|
|
1564
|
+
}
|
|
1565
|
+
async function awaitRows(read, has2, maxMs = 250) {
|
|
1566
|
+
const sig = () => {
|
|
1567
|
+
const { metrics, rows } = read();
|
|
1568
|
+
return { metrics, rows, key: `${rows}|${metrics?.max}|${metrics?.offset}` };
|
|
1569
|
+
};
|
|
1570
|
+
let cur = sig();
|
|
1571
|
+
let still = 0;
|
|
1572
|
+
for (let waited = 0; waited < maxMs; waited += 16) {
|
|
1573
|
+
if (has2()) break;
|
|
1574
|
+
await wait(16);
|
|
1575
|
+
const next = sig();
|
|
1576
|
+
still = next.key === cur.key ? still + 1 : 0;
|
|
1577
|
+
cur = next;
|
|
1578
|
+
if (cur.rows > 0 && still >= 4) break;
|
|
1579
|
+
}
|
|
1580
|
+
return cur.metrics;
|
|
1581
|
+
}
|
|
1582
|
+
|
|
1583
|
+
// src/runtime/screen/snapshot-text.ts
|
|
1584
|
+
var ELEMENT_KINDS = ["button", "input", "text", "view", "webview"];
|
|
1585
|
+
var center2 = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
1586
|
+
function underScope(found, scope) {
|
|
1587
|
+
for (let f = found.fiber; f; f = f.return) if (f === scope.fiber) return true;
|
|
1588
|
+
return false;
|
|
1589
|
+
}
|
|
1590
|
+
function snapshotText(lines, hidden, empty = "(nothing on screen)") {
|
|
1591
|
+
const base = lines.map(({ element: e }) => {
|
|
1592
|
+
const bits = [describe(e, 80, true)];
|
|
1593
|
+
if (e.editable === false) bits.push("readonly");
|
|
1594
|
+
if (e.actions?.length) bits.push(`actions=${e.actions.join(",")}`);
|
|
1595
|
+
if (e.onScreen === false) bits.push("offscreen");
|
|
1596
|
+
if (e.hidden) bits.push("hidden");
|
|
1597
|
+
return bits.join(" ");
|
|
1598
|
+
});
|
|
1599
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1600
|
+
for (const l of base) seen.set(l, (seen.get(l) ?? 0) + 1);
|
|
1601
|
+
const out = base.map((l, i) => {
|
|
1602
|
+
const { element: e, index } = lines[i];
|
|
1603
|
+
const repeats = (seen.get(l) ?? 0) > 1;
|
|
1604
|
+
const named = !!(e.testID || e.text || e.label || e.placeholder);
|
|
1605
|
+
const bits = [l];
|
|
1606
|
+
if (repeats && index !== void 0) bits.push(`index=${index}`);
|
|
1607
|
+
if (e.rect && (repeats && index === void 0 || !named)) {
|
|
1608
|
+
const c = center2(e.rect);
|
|
1609
|
+
bits.push(`@${Math.round(c.x)},${Math.round(c.y)}`);
|
|
1610
|
+
}
|
|
1611
|
+
return bits.join(" ");
|
|
1612
|
+
});
|
|
1613
|
+
if (hidden) out.push(`(${hidden} hidden: on screens not in front; {all:true} lists them)`);
|
|
1614
|
+
return lines.length || hidden ? out.join("\n") : empty;
|
|
1615
|
+
}
|
|
1194
1616
|
|
|
1195
1617
|
// src/runtime/screen/settle.ts
|
|
1196
1618
|
var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
|
|
@@ -1263,6 +1685,7 @@ async function settle(options = {}) {
|
|
|
1263
1685
|
|
|
1264
1686
|
// src/runtime/screen/wait-for.ts
|
|
1265
1687
|
var POLL_MS = 50;
|
|
1688
|
+
var NO_PAGING = "waitFor does not page lists for rows they have not rendered; wait for the list, then use press, fill or setValue with {scroll:true}";
|
|
1266
1689
|
function nearMissNote(found, target) {
|
|
1267
1690
|
const text = typeof target === "string" ? target : target.text;
|
|
1268
1691
|
const near = text ? nearMisses(found, text) : [];
|
|
@@ -1313,7 +1736,7 @@ function waitForTarget(collect, target, options = {}, reveal) {
|
|
|
1313
1736
|
const what = options.gone ? "to disappear" : "to appear";
|
|
1314
1737
|
reject(
|
|
1315
1738
|
new Error(
|
|
1316
|
-
`Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}${nearMissNote(found, target)}`
|
|
1739
|
+
`Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}${nearMissNote(found, target)}${options.scroll && !options.gone ? `. ${NO_PAGING}` : ""}`
|
|
1317
1740
|
)
|
|
1318
1741
|
);
|
|
1319
1742
|
}
|
|
@@ -1333,8 +1756,25 @@ function waitForTarget(collect, target, options = {}, reveal) {
|
|
|
1333
1756
|
}
|
|
1334
1757
|
|
|
1335
1758
|
// src/runtime/screen/index.ts
|
|
1759
|
+
function snapshotKinds(kinds) {
|
|
1760
|
+
if (kinds === void 0) return [];
|
|
1761
|
+
const list2 = typeof kinds === "string" ? [kinds] : kinds;
|
|
1762
|
+
if (!Array.isArray(list2))
|
|
1763
|
+
throw new Error(`screen.snapshot kinds is a list like ["button","input"], not ${JSON.stringify(kinds)}`);
|
|
1764
|
+
for (const k of list2)
|
|
1765
|
+
if (!ELEMENT_KINDS.includes(k))
|
|
1766
|
+
throw new Error(
|
|
1767
|
+
`screen.snapshot kinds are ${ELEMENT_KINDS.join(", ")}; ${JSON.stringify(k)} is not one (a line that starts with switch, checkbox, tab or another role is a button: ask for "button")`
|
|
1768
|
+
);
|
|
1769
|
+
return list2;
|
|
1770
|
+
}
|
|
1336
1771
|
function createScreen(env) {
|
|
1337
|
-
const
|
|
1772
|
+
const collectLoose = () => collectScreen(env.roots(), env.window());
|
|
1773
|
+
const collectAll = () => {
|
|
1774
|
+
const all = collectLoose();
|
|
1775
|
+
if (all.unmeasured) throw new Error(UNMEASURED_ERROR);
|
|
1776
|
+
return all;
|
|
1777
|
+
};
|
|
1338
1778
|
const collect = () => collectAll().found;
|
|
1339
1779
|
const collectSettled = async () => {
|
|
1340
1780
|
let prev = null;
|
|
@@ -1389,45 +1829,288 @@ function createScreen(env) {
|
|
|
1389
1829
|
);
|
|
1390
1830
|
return shown;
|
|
1391
1831
|
};
|
|
1832
|
+
const PAGE_LIMIT = 40;
|
|
1833
|
+
const PAGE_MS = 1e4;
|
|
1834
|
+
const pagingBudget = (scroll) => {
|
|
1835
|
+
if (scroll === null || typeof scroll !== "object") return PAGE_MS;
|
|
1836
|
+
const bad = Object.keys(scroll).filter((k) => k !== "timeoutMs");
|
|
1837
|
+
if (bad.length)
|
|
1838
|
+
throw new Error(`Unknown scroll option ${bad.map((k) => JSON.stringify(k)).join(", ")}. Valid: timeoutMs`);
|
|
1839
|
+
const ms = scroll.timeoutMs;
|
|
1840
|
+
if (ms === void 0) return PAGE_MS;
|
|
1841
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0)
|
|
1842
|
+
throw new Error(`scroll.timeoutMs is a positive number of milliseconds; got ${JSON.stringify(ms)}`);
|
|
1843
|
+
return ms;
|
|
1844
|
+
};
|
|
1845
|
+
const scrollableAgain = (host) => {
|
|
1846
|
+
const node = host.stateNode;
|
|
1847
|
+
return collectAll().scrollables.find(
|
|
1848
|
+
(s) => s.host === host || node != null && s.host.stateNode === node
|
|
1849
|
+
);
|
|
1850
|
+
};
|
|
1851
|
+
const rendered = (target, index) => () => matchTarget(collect().filter((f) => !f.hidden), target).length > (indexOf(target) ?? index ?? 0);
|
|
1852
|
+
const nameOf = (host) => {
|
|
1853
|
+
const el = collectAll().found.find((f) => f.host === host || f.host.stateNode === host.stateNode);
|
|
1854
|
+
return el ? describe(el.element) : "the list";
|
|
1855
|
+
};
|
|
1856
|
+
const pageList = async (first, skipped, has2, budgetMs) => {
|
|
1857
|
+
const { host } = first;
|
|
1858
|
+
const name = nameOf(host);
|
|
1859
|
+
const first0 = metricsOf(first);
|
|
1860
|
+
if (!first0) return { found: false, note: `${name} has no readable scroll position` };
|
|
1861
|
+
const start = first0;
|
|
1862
|
+
const size = Math.max(1, ((isHorizontal(first) ? start.view?.width : start.view?.height) ?? 400) * 0.9);
|
|
1863
|
+
const deadline = performance.now() + budgetMs;
|
|
1864
|
+
let pages = 0;
|
|
1865
|
+
let lowest = start.offset;
|
|
1866
|
+
let highest = start.offset;
|
|
1867
|
+
let max = start.max;
|
|
1868
|
+
let stop = "both ends";
|
|
1869
|
+
const horizontal = isHorizontal(first);
|
|
1870
|
+
const node = host.stateNode;
|
|
1871
|
+
let span = null;
|
|
1872
|
+
const readRows = () => {
|
|
1873
|
+
const now2 = scrollableAgain(host);
|
|
1874
|
+
const metrics = now2 ? metricsOf(now2) : null;
|
|
1875
|
+
let rows = 0;
|
|
1876
|
+
let top = Infinity;
|
|
1877
|
+
let bottom = -Infinity;
|
|
1878
|
+
for (const f of collectAll().found) {
|
|
1879
|
+
let inList = false;
|
|
1880
|
+
for (let x = f.scroller; x; x = x.parent)
|
|
1881
|
+
if (x.host === host || node != null && x.host.stateNode === node) inList = true;
|
|
1882
|
+
if (!inList) continue;
|
|
1883
|
+
if (f.onScreen) rows++;
|
|
1884
|
+
const box = metrics?.view && !metrics.inverted ? measureHost(f.host) : null;
|
|
1885
|
+
if (!metrics?.view || !box) continue;
|
|
1886
|
+
const at = (horizontal ? box.x - metrics.view.x : box.y - metrics.view.y) + metrics.offset;
|
|
1887
|
+
top = Math.min(top, at);
|
|
1888
|
+
bottom = Math.max(bottom, at + (horizontal ? box.width : box.height));
|
|
1889
|
+
}
|
|
1890
|
+
span = top <= bottom ? { top, bottom } : null;
|
|
1891
|
+
return { metrics, rows };
|
|
1892
|
+
};
|
|
1893
|
+
const go = async (offset) => {
|
|
1894
|
+
const now2 = scrollableAgain(host);
|
|
1895
|
+
if (!now2) return false;
|
|
1896
|
+
const before = metricsOf(now2);
|
|
1897
|
+
scrollToOffset(now2, offset);
|
|
1898
|
+
await awaitScroll(now2, before?.offset, offset);
|
|
1899
|
+
pages++;
|
|
1900
|
+
if (has2()) return true;
|
|
1901
|
+
const m = await awaitRows(readRows, has2);
|
|
1902
|
+
if (m) {
|
|
1903
|
+
lowest = Math.min(lowest, m.offset);
|
|
1904
|
+
highest = Math.max(highest, m.offset);
|
|
1905
|
+
max = m.max;
|
|
1906
|
+
}
|
|
1907
|
+
return has2();
|
|
1908
|
+
};
|
|
1909
|
+
const sweep = async (dir, until, limit) => {
|
|
1910
|
+
const from = pages;
|
|
1911
|
+
while (true) {
|
|
1912
|
+
if (pages - from >= limit || performance.now() >= until) {
|
|
1913
|
+
stop = pages >= PAGE_LIMIT ? `the ${PAGE_LIMIT}-page limit` : `the ${budgetMs} ms budget`;
|
|
1914
|
+
return false;
|
|
1915
|
+
}
|
|
1916
|
+
const m = readRows().metrics;
|
|
1917
|
+
if (!m) return false;
|
|
1918
|
+
const viewSize = size / 0.9;
|
|
1919
|
+
const reach = span;
|
|
1920
|
+
const want = dir === 1 ? Math.max(m.offset + size, reach ? reach.bottom : -Infinity) : Math.min(m.offset - size, reach ? reach.top - viewSize : Infinity);
|
|
1921
|
+
const next = dir === 1 ? Math.min(m.max ?? Infinity, want) : Math.max(0, want);
|
|
1922
|
+
if (Math.abs(next - m.offset) < 1) {
|
|
1923
|
+
const grown = await awaitRows(readRows, has2, 400);
|
|
1924
|
+
if (has2()) return true;
|
|
1925
|
+
if (dir === 1 && grown && (grown.max ?? 0) - m.offset >= 1) continue;
|
|
1926
|
+
return false;
|
|
1927
|
+
}
|
|
1928
|
+
if (await go(next)) return true;
|
|
1929
|
+
}
|
|
1930
|
+
};
|
|
1931
|
+
let found = false;
|
|
1932
|
+
try {
|
|
1933
|
+
const downFirst = start.max == null || start.max - start.offset <= start.offset;
|
|
1934
|
+
const order = downFirst ? [1, -1] : [-1, 1];
|
|
1935
|
+
const now2 = performance.now();
|
|
1936
|
+
for (const [i, dir] of order.entries()) {
|
|
1937
|
+
const otherRoom = dir === -1 ? start.max == null || start.max - start.offset >= 1 : start.offset >= 1;
|
|
1938
|
+
const half = i === 0 && otherRoom;
|
|
1939
|
+
const share = half ? now2 + budgetMs / 2 : deadline;
|
|
1940
|
+
const limit = half ? PAGE_LIMIT / 2 : PAGE_LIMIT - pages;
|
|
1941
|
+
if (dir === -1 && start.offset < 1) continue;
|
|
1942
|
+
if (i === 1 && pages) await go(start.offset);
|
|
1943
|
+
if (await sweep(dir, share, limit)) return found = true, done();
|
|
1944
|
+
}
|
|
1945
|
+
} finally {
|
|
1946
|
+
if (!found) {
|
|
1947
|
+
const now2 = scrollableAgain(host);
|
|
1948
|
+
if (now2) {
|
|
1949
|
+
await scrolled(now2, () => (scrollToOffset(now2, start.offset), start.offset));
|
|
1950
|
+
await settle();
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return done();
|
|
1955
|
+
function done() {
|
|
1956
|
+
const where = `up to offset ${Math.round(lowest)} and down to offset ${Math.round(highest)} of ${max == null ? "?" : Math.round(max)}`;
|
|
1957
|
+
const left = skipped.length ? `. Not paged: ${skipped.map((s) => nameOf(s.host)).join(", ")}` : "";
|
|
1958
|
+
return {
|
|
1959
|
+
found,
|
|
1960
|
+
note: found ? "" : `{scroll:true} paged ${name} ${pages} page(s), ${where}, without rendering a match, then scrolled back to offset ${Math.round(start.offset)}. Stopped at: ${stop}${left}`
|
|
1961
|
+
};
|
|
1962
|
+
}
|
|
1963
|
+
};
|
|
1964
|
+
const pageFor = async (has2, budgetMs) => {
|
|
1965
|
+
const { list: list2, skipped } = pagingList(collectAll().scrollables, env.window());
|
|
1966
|
+
if (!list2)
|
|
1967
|
+
return { found: false, note: "{scroll:true} found no FlatList, SectionList or FlashList on screen to page" };
|
|
1968
|
+
return pageList(list2, skipped, has2, budgetMs);
|
|
1969
|
+
};
|
|
1392
1970
|
const resolve = async (target, prefer, options) => {
|
|
1393
|
-
const
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1971
|
+
const pick = () => resolveTarget(collect(), target, prefer, { ...options, offscreen: !!options.scroll });
|
|
1972
|
+
let note = "";
|
|
1973
|
+
if (options.scroll && typeof target === "object" && target.at !== void 0)
|
|
1974
|
+
throw new Error(
|
|
1975
|
+
"{at:[x,y]} is a point on the screen and can't be scrolled to; drop {scroll:true} or target by text or testID"
|
|
1976
|
+
);
|
|
1977
|
+
const has2 = rendered(target, options.index);
|
|
1978
|
+
if (options.scroll && !has2()) {
|
|
1979
|
+
const paged = await pageFor(has2, pagingBudget(options.scroll));
|
|
1980
|
+
note = paged.note;
|
|
1981
|
+
if (!paged.found) {
|
|
1982
|
+
try {
|
|
1983
|
+
return await settleOn(pick());
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
throw note ? new Error(`${error.message}. ${note}`) : error;
|
|
1986
|
+
}
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
return settleOn(pick());
|
|
1990
|
+
};
|
|
1991
|
+
const settleOn = async (found) => found.onScreen ? found : reveal(found);
|
|
1992
|
+
const build = (options) => {
|
|
1993
|
+
const { all, format, within: within2 } = options;
|
|
1994
|
+
if (format !== void 0 && format !== "json" && format !== "text")
|
|
1995
|
+
throw new Error(`screen.snapshot format is "json" or "text", not ${JSON.stringify(format)}`);
|
|
1996
|
+
const kinds = snapshotKinds(options.kinds);
|
|
1997
|
+
const found = all ? collectLoose().found : collect();
|
|
1998
|
+
let pool;
|
|
1999
|
+
let hidden;
|
|
2000
|
+
if (all) {
|
|
2001
|
+
pool = found;
|
|
2002
|
+
} else {
|
|
2003
|
+
const shown = focused(found);
|
|
2004
|
+
pool = shown.filter((f) => f.onScreen);
|
|
2005
|
+
hidden = found.length - shown.length || void 0;
|
|
2006
|
+
}
|
|
2007
|
+
let items = pool.map((f) => ({
|
|
2008
|
+
found: f,
|
|
2009
|
+
element: all ? { ...f.element, onScreen: f.onScreen, ...f.hidden && { hidden: true } } : f.element
|
|
2010
|
+
}));
|
|
2011
|
+
let scoped = "";
|
|
2012
|
+
if (within2 !== void 0) {
|
|
2013
|
+
const scope = scopeOf(found, within2, !!all);
|
|
2014
|
+
if (!scope.element.rect)
|
|
2015
|
+
throw new Error(`${describe(scope.element)} has no position, so it can't scope a snapshot`);
|
|
2016
|
+
scoped = describe(scope.element);
|
|
2017
|
+
items = items.filter((i) => i.found !== scope && underScope(i.found, scope));
|
|
2018
|
+
}
|
|
2019
|
+
if (kinds.length) items = items.filter((i) => kinds.includes(i.element.kind));
|
|
2020
|
+
const empty = kinds.length || scoped ? `(no ${kinds.length ? kinds.join("/") : ""} elements${scoped ? ` inside ${scoped}` : ""})`.replace("(no ", "(no ") : void 0;
|
|
2021
|
+
return {
|
|
2022
|
+
hidden,
|
|
2023
|
+
empty,
|
|
2024
|
+
items: items.map((i) => ({ element: i.element, index: matchIndex(pool, i.found) }))
|
|
2025
|
+
};
|
|
2026
|
+
};
|
|
2027
|
+
const matchIndex = (pool, f) => {
|
|
2028
|
+
const e = f.element;
|
|
2029
|
+
const name = e.testID ?? e.label ?? e.placeholder ?? e.text;
|
|
2030
|
+
if (!name) return void 0;
|
|
2031
|
+
const at = matchTarget(pool, name).indexOf(f);
|
|
2032
|
+
return at < 0 ? void 0 : at;
|
|
2033
|
+
};
|
|
2034
|
+
const scopeOf = (found, within2, all) => {
|
|
2035
|
+
const ranged = indexOf(within2) !== void 0;
|
|
2036
|
+
if (!ranged) {
|
|
2037
|
+
const matches = matchTarget(found.filter((f) => !f.hidden && (all || f.onScreen)), within2);
|
|
2038
|
+
if (matches.length > 1) {
|
|
2039
|
+
const first = matches[0].element;
|
|
2040
|
+
const fix = typeof within2 === "string" ? {
|
|
2041
|
+
[first.testID === within2 ? "testID" : first.label === within2 ? "label" : first.placeholder === within2 ? "placeholder" : "text"]: within2,
|
|
2042
|
+
index: 1
|
|
2043
|
+
} : { ...within2, index: 1 };
|
|
2044
|
+
throw new Error(
|
|
2045
|
+
`within ${showTarget(within2)} matches ${matches.length} elements; pick one with ${JSON.stringify({ within: fix })}`
|
|
2046
|
+
);
|
|
2047
|
+
}
|
|
2048
|
+
}
|
|
2049
|
+
return resolveTarget(found, within2, void 0, { offscreen: all });
|
|
1398
2050
|
};
|
|
1399
2051
|
return {
|
|
1400
2052
|
snapshot(options = {}) {
|
|
1401
|
-
const
|
|
1402
|
-
|
|
1403
|
-
return {
|
|
1404
|
-
elements: found.map((f) => ({
|
|
1405
|
-
...f.element,
|
|
1406
|
-
onScreen: f.onScreen,
|
|
1407
|
-
...f.hidden && { hidden: true }
|
|
1408
|
-
}))
|
|
1409
|
-
};
|
|
1410
|
-
const shown = focused(found);
|
|
1411
|
-
const elements = shown.filter((f) => f.onScreen).map((f) => f.element);
|
|
1412
|
-
const hidden = found.length - shown.length;
|
|
2053
|
+
const { items, hidden } = build(options);
|
|
2054
|
+
const elements = items.map((i) => i.element);
|
|
1413
2055
|
return hidden ? { elements, hidden } : { elements };
|
|
1414
2056
|
},
|
|
1415
|
-
|
|
2057
|
+
/** `snapshot` as one line per element. */
|
|
2058
|
+
snapshotText(options = {}) {
|
|
2059
|
+
const { items, hidden, empty } = build(options);
|
|
2060
|
+
return snapshotText(items, hidden, empty);
|
|
2061
|
+
},
|
|
2062
|
+
findText: (text, options) => findText(collectLoose().found, text, options),
|
|
1416
2063
|
async fill(target, text, options = {}) {
|
|
1417
2064
|
const found = await resolve(target, (f) => !!f.input, {
|
|
1418
2065
|
index: options.index,
|
|
1419
2066
|
afterText: true,
|
|
1420
2067
|
scroll: options.scroll
|
|
1421
2068
|
});
|
|
1422
|
-
const filled = fillInput(found, String(text),
|
|
2069
|
+
const { filled, native } = await fillInput(found, String(text), {
|
|
2070
|
+
...options,
|
|
2071
|
+
between: async () => void await settle(),
|
|
2072
|
+
again: reread
|
|
2073
|
+
});
|
|
1423
2074
|
await settle();
|
|
1424
|
-
|
|
2075
|
+
const element = reread(found).element;
|
|
2076
|
+
if (native === true) return { filled, element: { ...element, value: filled } };
|
|
2077
|
+
if (native === false)
|
|
2078
|
+
return {
|
|
2079
|
+
filled,
|
|
2080
|
+
element,
|
|
2081
|
+
warning: "the input has no value prop and its native text could not be set from here, so the field may not show the text"
|
|
2082
|
+
};
|
|
2083
|
+
return { filled, element };
|
|
1425
2084
|
},
|
|
1426
2085
|
async press(target, options = {}) {
|
|
1427
|
-
const
|
|
1428
|
-
|
|
2086
|
+
const { action } = options;
|
|
2087
|
+
if (action !== void 0 && typeof action !== "string")
|
|
2088
|
+
throw new Error(`"action" must be an accessibility action name; got ${JSON.stringify(action)}`);
|
|
2089
|
+
const found = await resolve(
|
|
2090
|
+
target,
|
|
2091
|
+
(f) => options.long ? !!longPressFiberOf(f) : action === void 0 ? !!f.press || f.element.kind === "button" || isToggle(f) : !!f.element.actions?.includes(action),
|
|
2092
|
+
options
|
|
2093
|
+
);
|
|
2094
|
+
const via = pressElement(found, options);
|
|
2095
|
+
await settle();
|
|
2096
|
+
return via === "onPress" ? found.element : { ...found.element, via };
|
|
2097
|
+
},
|
|
2098
|
+
/** Sets a Switch, Slider, Picker or SegmentedControl through its onValueChange, and reads the value back. */
|
|
2099
|
+
async setValue(target, value, options = {}) {
|
|
2100
|
+
const found = await resolve(target, (f) => !!f.change, options);
|
|
2101
|
+
const before = valueOf(found);
|
|
2102
|
+
setControlValue(found, value, options);
|
|
1429
2103
|
await settle();
|
|
1430
|
-
|
|
2104
|
+
const again = reread(found);
|
|
2105
|
+
const now2 = valueOf(again);
|
|
2106
|
+
const result = { value: now2, element: again.element };
|
|
2107
|
+
const same = typeof now2 === "number" && typeof value === "number" ? Math.abs(now2 - value) < 1e-6 : now2 === value;
|
|
2108
|
+
if (before !== void 0 && !same && !(found.change && isSlider(found.change)))
|
|
2109
|
+
return {
|
|
2110
|
+
...result,
|
|
2111
|
+
warning: `the control still shows ${JSON.stringify(now2)}; the app did not apply ${JSON.stringify(value)}`
|
|
2112
|
+
};
|
|
2113
|
+
return result;
|
|
1431
2114
|
},
|
|
1432
2115
|
/**
|
|
1433
2116
|
* Scrolls the nearest scrollable of a target (or `within`, or the main one
|
|
@@ -1525,17 +2208,21 @@ function screenTools() {
|
|
|
1525
2208
|
run: (text, options) => screen.findText(text, options)
|
|
1526
2209
|
},
|
|
1527
2210
|
"screen.snapshot": {
|
|
1528
|
-
description:
|
|
1529
|
-
run: (options) => screen.snapshot(options)
|
|
2211
|
+
description: 'Buttons, inputs, text, and testID or labelled views on screen, with rects and checked/selected/expanded state (a Switch\'s value is checked). `actions` lists the accessibility actions an element handles; one with "activate" and no onPress (a swipe-to-confirm) is a button screen.press can press. 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. Options: {format:"json"|"text"} (the runtime default is JSON with rects; the CLI and the MCP server return the text form unless you pass {format:"json"}). Text is one line per element: kind (or role: switch, checkbox, tab), #testID, text, label, value, state, then `readonly`, `actions=\u2026`, `offscreen`, `hidden`. Lines that repeat get `index=N` (the {index} of the target that names them); a line with no testID, text, label or placeholder gets `@x,y`, the point for {at:[x,y]}. JSON has rects, onScreen:false and hidden:true. {kinds:["button","input"]} keeps only those kinds (kinds are button, input, text, view, webview; a switch or tab is a button); {within: target} keeps only elements under that target in the tree (a target as for screen.press; with several matches pass {testID, index} inside it).',
|
|
2212
|
+
run: (options) => options?.format === "text" ? screen.snapshotText(options) : screen.snapshot(options)
|
|
1530
2213
|
},
|
|
1531
2214
|
"screen.fill": {
|
|
1532
|
-
description: `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits.`,
|
|
2215
|
+
description: `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits. "" clears it. {type:true} types one character at a time, waiting for a render after each (for debounced or per-character inputs; "\\n" is the Enter key). An input with no value prop gets its native text set; if that cannot be done the reply has a warning.`,
|
|
1533
2216
|
run: (target, text, options) => screen.fill(target, text, options)
|
|
1534
2217
|
},
|
|
1535
2218
|
"screen.press": {
|
|
1536
|
-
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.`,
|
|
2219
|
+
description: `Press a button, then wait for the render. ${TARGET_HELP} Text with no handler of its own presses the nearest ancestor that has one, and the reply says via:"ancestor". 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. With no onPress it sends the "activate" accessibility action, as a screen reader's double tap does (swipe-to-confirm buttons, sliders), and the reply says via:"accessibilityAction:activate". {action:"increment"} (or any name in the element's snapshot \`actions\`) sends that action instead. A Switch (native switch host or role switch, with onValueChange) is toggled with onValueChange(!value), not the row around it; reply via:"onValueChange". {long:true} long presses: onPressIn, onLongPress, onPressOut (never onPress); an error when there is no onLongPress, on the element or an ancestor. {scroll:true} also pages the largest vertical FlatList/SectionList/FlashList for a row it has not rendered yet (and scrolls back if it finds none; onEndReached may fire). A tap inside a react-native-gesture-handler GestureDetector cannot be pressed from here (its callbacks run on the UI thread): the error says so.`,
|
|
1537
2220
|
run: (target, options) => screen.press(target, options)
|
|
1538
2221
|
},
|
|
2222
|
+
"screen.setValue": {
|
|
2223
|
+
description: `Set a Switch, Slider, Picker or SegmentedControl: calls its onValueChange (and a Slider's onSlidingComplete) with the value, waits for the render and reads the value back. A Switch takes true or false, a Slider a number within minimumValue and maximumValue (0 and 1 by default) and on its step (onSlidingStart, onValueChange, onSlidingComplete), a Picker one of its Picker.Item values, a SegmentedControl one of its values (a wrong type or value is an error that lists the options). The reply has \`value\` as the app now shows it, and a warning when that is not what was set (a controlled value the app refused). Disabled controls are refused; {force:true} sets anyway. {scroll:true} scrolls or pages the target into view first (pages the largest vertical list; onEndReached may fire). ${TARGET_HELP}`,
|
|
2224
|
+
run: (target, value, options) => screen.setValue(target, value, options)
|
|
2225
|
+
},
|
|
1539
2226
|
"screen.scroll": {
|
|
1540
2227
|
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}`,
|
|
1541
2228
|
run: (arg, options) => screen.scroll(arg, options)
|
|
@@ -1545,7 +2232,7 @@ function screenTools() {
|
|
|
1545
2232
|
run: (target, options) => screen.refresh(target, options)
|
|
1546
2233
|
},
|
|
1547
2234
|
"screen.waitFor": {
|
|
1548
|
-
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. {scroll:true} also accepts a match rendered off screen and scrolls it into view (not with gone). Default timeout 5000 ms; a timeout lists near misses.",
|
|
2235
|
+
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. {scroll:true} also accepts a match rendered off screen and scrolls it into view, (it does not page lists for rows they have not rendered: wait for the list, then press/fill/setValue with {scroll:true}; not with gone). Default timeout 5000 ms; a timeout lists near misses.",
|
|
1549
2236
|
run: (target, options) => screen.waitFor(target, options)
|
|
1550
2237
|
}
|
|
1551
2238
|
};
|
|
@@ -1556,7 +2243,8 @@ function builtinTools(listTools, getTools, logs, getScenarios) {
|
|
|
1556
2243
|
return {
|
|
1557
2244
|
...bridgeTools(listTools),
|
|
1558
2245
|
...appTools(
|
|
1559
|
-
selectReload(globalThis, () => import_react_native2.DevSettings.reload())
|
|
2246
|
+
selectReload(globalThis, () => import_react_native2.DevSettings.reload()),
|
|
2247
|
+
getTools
|
|
1560
2248
|
),
|
|
1561
2249
|
...logTools(logs),
|
|
1562
2250
|
...restoreTools(getTools),
|
|
@@ -1805,32 +2493,54 @@ function toJson(value) {
|
|
|
1805
2493
|
}
|
|
1806
2494
|
|
|
1807
2495
|
// src/runtime/registry.ts
|
|
2496
|
+
var loadSlot = {};
|
|
2497
|
+
function ownsKey(slot, key) {
|
|
2498
|
+
if (typeof key !== "string" || key === "") return false;
|
|
2499
|
+
slot.key ?? (slot.key = key);
|
|
2500
|
+
return slot.key === key;
|
|
2501
|
+
}
|
|
1808
2502
|
function unwrap(definition) {
|
|
1809
2503
|
return typeof definition === "function" ? { run: definition } : definition;
|
|
1810
2504
|
}
|
|
1811
|
-
function createRegistry(getTools, logs, loadId2) {
|
|
1812
|
-
const
|
|
2505
|
+
function createRegistry(getTools, logs, loadId2, keySlot = loadSlot) {
|
|
2506
|
+
const list2 = () => Object.entries(getTools()).filter(([name]) => !INTERNAL_TOOLS.includes(name)).map(([name, definition]) => ({
|
|
1813
2507
|
name,
|
|
1814
2508
|
description: unwrap(definition).description
|
|
1815
2509
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
1816
2510
|
async function dispatch(call, from) {
|
|
1817
|
-
const
|
|
1818
|
-
const
|
|
1819
|
-
const
|
|
1820
|
-
const
|
|
2511
|
+
const quiet = call.tool === "bridge.ping" || call.tool === "bridge.pending";
|
|
2512
|
+
const trusted = ownsKey(keySlot, call.journalKey);
|
|
2513
|
+
const end = quiet ? void 0 : logs?.begin(call.tool);
|
|
2514
|
+
const result = INTERNAL_TOOLS.includes(call.tool) && !trusted ? await refused(call, from) : await run(call, from).finally(end);
|
|
2515
|
+
const errors = quiet ? void 0 : logs?.takeErrors();
|
|
2516
|
+
const stamped = loadId2 ? { ...result, loadId: loadId2, ...pending(), ...journal(call, trusted) } : result;
|
|
1821
2517
|
return errors?.length ? { ...stamped, logs: errors } : stamped;
|
|
1822
2518
|
}
|
|
1823
2519
|
function pending() {
|
|
1824
2520
|
const areas = Object.keys(pendingByArea(getTools()));
|
|
1825
2521
|
return areas.length ? { pending: areas } : {};
|
|
1826
2522
|
}
|
|
2523
|
+
function journal(call, trusted) {
|
|
2524
|
+
const rev = journalRev(getTools());
|
|
2525
|
+
const out = rev ? { journal: rev } : {};
|
|
2526
|
+
if (trusted && call.journalAck !== void 0 && call.journalAck !== `${loadId2}:${rev}`)
|
|
2527
|
+
out.journalData = journalByArea(getTools());
|
|
2528
|
+
return out;
|
|
2529
|
+
}
|
|
2530
|
+
const refused = (call, from) => ({
|
|
2531
|
+
id: call.id,
|
|
2532
|
+
from,
|
|
2533
|
+
ok: false,
|
|
2534
|
+
error: `${call.tool} is for a session's own use`,
|
|
2535
|
+
ms: 0
|
|
2536
|
+
});
|
|
1827
2537
|
async function run(call, from) {
|
|
1828
2538
|
const t0 = performance.now();
|
|
1829
2539
|
const ms = () => Math.round((performance.now() - t0) * 100) / 100;
|
|
1830
2540
|
try {
|
|
1831
2541
|
const definition = getTools()[call.tool];
|
|
1832
2542
|
if (!definition) {
|
|
1833
|
-
const known =
|
|
2543
|
+
const known = list2().map((t) => t.name).join(", ");
|
|
1834
2544
|
throw new Error(`Unknown tool "${call.tool}". Known: ${known}`);
|
|
1835
2545
|
}
|
|
1836
2546
|
const { run: tool, maxArgs } = unwrap(definition);
|
|
@@ -1846,7 +2556,7 @@ function createRegistry(getTools, logs, loadId2) {
|
|
|
1846
2556
|
return { id: call.id, from, ok: false, error: message2, ms: ms() };
|
|
1847
2557
|
}
|
|
1848
2558
|
}
|
|
1849
|
-
return { list, dispatch };
|
|
2559
|
+
return { list: list2, dispatch };
|
|
1850
2560
|
}
|
|
1851
2561
|
|
|
1852
2562
|
// src/runtime/index.ts
|