@avasapp/agent-bridge 0.2.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 +220 -20
- 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-LBYZ5JHF.js → chunk-AJD3XTNT.js} +329 -46
- package/dist/cli.js +848 -98
- package/dist/client/index.cjs +311 -47
- package/dist/client/index.d.ts +64 -3
- package/dist/client/index.js +5 -3
- package/dist/expo/index.d.cts +1 -1
- package/dist/network/index.cjs +434 -66
- package/dist/network/index.d.cts +53 -3
- package/dist/noop/index.cjs +12 -0
- package/dist/noop/network.cjs +7 -1
- 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 +1067 -111
- package/dist/runtime/index.d.cts +51 -5
- package/dist/types-BmpykP1P.d.cts +137 -0
- package/entries/react-native-webview.cjs +9 -0
- package/package.json +18 -6
- package/skills/agent-bridge/SKILL.md +226 -8
- package/dist/types-C6DUUHnB.d.cts +0 -76
package/dist/runtime/index.cjs
CHANGED
|
@@ -22,13 +22,15 @@ var runtime_exports = {};
|
|
|
22
22
|
__export(runtime_exports, {
|
|
23
23
|
RUNTIME_MARKER: () => RUNTIME_MARKER,
|
|
24
24
|
cdpTransport: () => cdpTransport,
|
|
25
|
+
createGate: () => createGate,
|
|
26
|
+
onRestore: () => onRestore,
|
|
25
27
|
settle: () => settle,
|
|
26
28
|
startAgentBridge: () => startAgentBridge,
|
|
27
29
|
useAgentBridge: () => useAgentBridge
|
|
28
30
|
});
|
|
29
31
|
module.exports = __toCommonJS(runtime_exports);
|
|
30
32
|
var import_react = require("react");
|
|
31
|
-
var
|
|
33
|
+
var import_react_native3 = require("react-native");
|
|
32
34
|
|
|
33
35
|
// src/shared/protocol.ts
|
|
34
36
|
var PROTOCOL_VERSION = 1;
|
|
@@ -42,6 +44,73 @@ function toAsciiJson(value) {
|
|
|
42
44
|
);
|
|
43
45
|
}
|
|
44
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
|
+
|
|
45
114
|
// src/runtime/tools/bridge.ts
|
|
46
115
|
function bridgeTools(listTools) {
|
|
47
116
|
return {
|
|
@@ -56,6 +125,42 @@ function bridgeTools(listTools) {
|
|
|
56
125
|
};
|
|
57
126
|
}
|
|
58
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
|
+
|
|
59
164
|
// src/runtime/tools/logs.ts
|
|
60
165
|
function logTools(logs) {
|
|
61
166
|
return {
|
|
@@ -73,14 +178,35 @@ function logTools(logs) {
|
|
|
73
178
|
// src/runtime/tools/restore.ts
|
|
74
179
|
var SUFFIX = ".restore";
|
|
75
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
|
+
}
|
|
76
197
|
function restoreTools(getTools) {
|
|
77
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
|
+
},
|
|
78
204
|
[SELF]: {
|
|
79
|
-
description: "Undo what the agent changed: runs every *.restore tool in name order and returns each result or error.",
|
|
205
|
+
description: "Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.",
|
|
80
206
|
run: async () => {
|
|
81
207
|
const tools = getTools();
|
|
82
208
|
const results = {};
|
|
83
|
-
const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort();
|
|
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));
|
|
84
210
|
for (const name of names) {
|
|
85
211
|
const definition = tools[name];
|
|
86
212
|
const run = typeof definition === "function" ? definition : definition.run;
|
|
@@ -98,6 +224,160 @@ function restoreTools(getTools) {
|
|
|
98
224
|
};
|
|
99
225
|
}
|
|
100
226
|
|
|
227
|
+
// src/runtime/options-schema.ts
|
|
228
|
+
var import_json_schema = require("@cfworker/json-schema");
|
|
229
|
+
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
230
|
+
var CASCADE = /* @__PURE__ */ new Set([
|
|
231
|
+
"properties",
|
|
232
|
+
"patternProperties",
|
|
233
|
+
"additionalProperties",
|
|
234
|
+
"unevaluatedProperties",
|
|
235
|
+
"items",
|
|
236
|
+
"prefixItems",
|
|
237
|
+
"unevaluatedItems",
|
|
238
|
+
"allOf",
|
|
239
|
+
"if",
|
|
240
|
+
"then",
|
|
241
|
+
"else",
|
|
242
|
+
"dependentSchemas",
|
|
243
|
+
"$ref",
|
|
244
|
+
"false"
|
|
245
|
+
]);
|
|
246
|
+
var path = (instanceLocation) => instanceLocation === "#" ? "options" : `options${instanceLocation.slice(1)}`;
|
|
247
|
+
function unknownKeys(schema, value, at, out) {
|
|
248
|
+
if (!isObject(schema)) return;
|
|
249
|
+
if (Array.isArray(value)) {
|
|
250
|
+
if (isObject(schema.items))
|
|
251
|
+
value.forEach((item, i) => unknownKeys(schema.items, item, `${at}/${i}`, out));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
if (!isObject(value) || !isObject(schema.properties)) return;
|
|
255
|
+
const props = schema.properties;
|
|
256
|
+
const open = schema.additionalProperties !== void 0 && schema.additionalProperties !== false || schema.patternProperties !== void 0 || // Composed schemas split their properties across branches; leave them to the validator.
|
|
257
|
+
["allOf", "anyOf", "oneOf", "$ref"].some((k) => schema[k] !== void 0);
|
|
258
|
+
for (const [key, child] of Object.entries(value)) {
|
|
259
|
+
if (key in props) unknownKeys(props[key], child, `${at}/${key}`, out);
|
|
260
|
+
else if (!open)
|
|
261
|
+
out.push(`${path(at)}: unknown option "${key}". Known: ${Object.keys(props).join(", ") || "none"}`);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
var validators = /* @__PURE__ */ new WeakMap();
|
|
265
|
+
function checkOptions(schema, options) {
|
|
266
|
+
let validator = validators.get(schema);
|
|
267
|
+
if (!validator) {
|
|
268
|
+
validator = new import_json_schema.Validator(schema, "2020-12", false);
|
|
269
|
+
validators.set(schema, validator);
|
|
270
|
+
}
|
|
271
|
+
const value = options === void 0 ? {} : options;
|
|
272
|
+
const problems = [];
|
|
273
|
+
unknownKeys(schema, value, "#", problems);
|
|
274
|
+
const result = validator.validate(value);
|
|
275
|
+
if (!result.valid) {
|
|
276
|
+
const leaves = result.errors.filter(
|
|
277
|
+
(e) => !CASCADE.has(e.keyword) && !/\/(anyOf|oneOf)\/\d/.test(e.keywordLocation)
|
|
278
|
+
);
|
|
279
|
+
const shown = leaves.length || problems.length ? leaves : result.errors;
|
|
280
|
+
for (const e of shown) problems.push(`${path(e.instanceLocation)}: ${e.error}`);
|
|
281
|
+
}
|
|
282
|
+
return [...new Set(problems)];
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// src/runtime/tools/scenario.ts
|
|
286
|
+
var active = /* @__PURE__ */ new Map();
|
|
287
|
+
var message = (error) => error instanceof Error ? error.message : String(error);
|
|
288
|
+
async function runUndos2(entry) {
|
|
289
|
+
const errors = [];
|
|
290
|
+
for (const undo2 of entry.undos.splice(0).reverse()) {
|
|
291
|
+
try {
|
|
292
|
+
await undo2();
|
|
293
|
+
} catch (error) {
|
|
294
|
+
errors.push(message(error));
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
return errors;
|
|
298
|
+
}
|
|
299
|
+
async function undo(name) {
|
|
300
|
+
const entry = active.get(name);
|
|
301
|
+
if (!entry) return [];
|
|
302
|
+
active.delete(name);
|
|
303
|
+
return (await runUndos2(entry)).map((e) => `${name}: ${e}`);
|
|
304
|
+
}
|
|
305
|
+
function scenarioTools(getScenarios, getTools) {
|
|
306
|
+
const find = (name) => {
|
|
307
|
+
const scenarios = getScenarios();
|
|
308
|
+
const scenario = scenarios[name];
|
|
309
|
+
if (!scenario)
|
|
310
|
+
throw new Error(
|
|
311
|
+
`Unknown scenario "${name}". Known: ${Object.keys(scenarios).join(", ") || "none"}`
|
|
312
|
+
);
|
|
313
|
+
return scenario;
|
|
314
|
+
};
|
|
315
|
+
const call = async (tool, ...args) => {
|
|
316
|
+
const definition = getTools()[tool];
|
|
317
|
+
if (!definition) throw new Error(`Unknown tool "${tool}"`);
|
|
318
|
+
const run = typeof definition === "function" ? definition : definition.run;
|
|
319
|
+
return run(...args);
|
|
320
|
+
};
|
|
321
|
+
return {
|
|
322
|
+
"scenario.list": {
|
|
323
|
+
description: "Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.",
|
|
324
|
+
run: () => Object.entries(getScenarios()).map(([name, s]) => ({
|
|
325
|
+
name,
|
|
326
|
+
description: s.description,
|
|
327
|
+
options: s.options,
|
|
328
|
+
active: active.has(name)
|
|
329
|
+
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
330
|
+
},
|
|
331
|
+
"scenario.apply": {
|
|
332
|
+
description: "Apply a scenario: [name, options?]. Options must fit its schema (scenario.list). Returns what it returns. Applying an active one undoes it first. bridge.restore undoes it.",
|
|
333
|
+
run: async (name, options) => {
|
|
334
|
+
const scenario = find(name);
|
|
335
|
+
if (scenario.options) {
|
|
336
|
+
const problems = checkOptions(scenario.options, options);
|
|
337
|
+
if (problems.length)
|
|
338
|
+
throw new Error(
|
|
339
|
+
`Scenario "${name}" got bad options, nothing was applied:
|
|
340
|
+
- ${problems.join("\n- ")}
|
|
341
|
+
scenario.list shows its options schema.`
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
const previous = await undo(name);
|
|
345
|
+
if (previous.length)
|
|
346
|
+
throw new Error(`Undoing the active "${name}" failed: ${previous.join("; ")}`);
|
|
347
|
+
const entry = { options, undos: [] };
|
|
348
|
+
active.set(name, entry);
|
|
349
|
+
const context = {
|
|
350
|
+
options,
|
|
351
|
+
call,
|
|
352
|
+
onUndo: (fn) => {
|
|
353
|
+
if (typeof fn !== "function")
|
|
354
|
+
throw new Error("onUndo takes a function");
|
|
355
|
+
entry.undos.push(fn);
|
|
356
|
+
}
|
|
357
|
+
};
|
|
358
|
+
try {
|
|
359
|
+
return await scenario.apply(context);
|
|
360
|
+
} catch (error) {
|
|
361
|
+
const errors = await undo(name);
|
|
362
|
+
const extra = errors.length ? ` (undo: ${errors.join("; ")})` : "";
|
|
363
|
+
throw new Error(`Scenario "${name}" failed: ${message(error)}${extra}`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
},
|
|
367
|
+
"scenario.restore": {
|
|
368
|
+
pending: () => active.size > 0,
|
|
369
|
+
description: "Undo every active scenario, newest first. bridge.restore runs this after every other restorer.",
|
|
370
|
+
run: async () => {
|
|
371
|
+
const names = [...active.keys()].reverse();
|
|
372
|
+
const errors = [];
|
|
373
|
+
for (const name of names) errors.push(...await undo(name));
|
|
374
|
+
if (errors.length) throw new Error(errors.join("; "));
|
|
375
|
+
return names;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
101
381
|
// src/runtime/tools/screen.ts
|
|
102
382
|
var import_react_native = require("react-native");
|
|
103
383
|
|
|
@@ -141,40 +421,101 @@ function textOf(fiber) {
|
|
|
141
421
|
}
|
|
142
422
|
return null;
|
|
143
423
|
}
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
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];
|
|
149
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;
|
|
150
450
|
return true;
|
|
151
451
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
if (found && (options.exact ? found.text === text : found.text.includes(text))) {
|
|
159
|
-
const rect = found.host ? measureHost(found.host) : null;
|
|
160
|
-
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;
|
|
161
|
-
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;
|
|
162
458
|
}
|
|
163
|
-
|
|
164
|
-
|
|
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);
|
|
165
495
|
}
|
|
496
|
+
let base = screens.length - 1;
|
|
497
|
+
while (base > 0 && seeThrough(screens[base])) base--;
|
|
166
498
|
return {
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
matches
|
|
499
|
+
covered: screens.slice(0, Math.max(0, base)),
|
|
500
|
+
presented: screens.slice(1).filter(coversWindow)
|
|
170
501
|
};
|
|
171
502
|
}
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
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"]);
|
|
175
515
|
var isFn = (v) => typeof v === "function";
|
|
176
|
-
var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && ("value" in p || "defaultValue" in p || "placeholder" in p);
|
|
177
|
-
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;
|
|
178
519
|
var ICON_GLYPHS = /[\uE000-\uF8FF]/g;
|
|
179
520
|
function firstHost(fiber) {
|
|
180
521
|
if (fiber.tag === HOST_COMPONENT) return fiber;
|
|
@@ -195,22 +536,77 @@ function absorb(rec, fiber, p) {
|
|
|
195
536
|
if (e[key] === void 0 && typeof v === "string") e[key] = v;
|
|
196
537
|
};
|
|
197
538
|
fill("testID", p.testID);
|
|
198
|
-
fill("label", p
|
|
539
|
+
fill("label", labelOf(p));
|
|
199
540
|
fill("role", p.accessibilityRole ?? p.role);
|
|
200
541
|
fill("placeholder", p.placeholder);
|
|
201
|
-
|
|
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)) {
|
|
202
549
|
const v = p.value ?? p.defaultValue;
|
|
203
|
-
if (v != null) e.value = String(v);
|
|
550
|
+
if (v != null && typeof v !== "object" && !isFn(v)) e.value = String(v);
|
|
204
551
|
}
|
|
205
552
|
if (rec.maxLength === void 0 && typeof p.maxLength === "number")
|
|
206
553
|
rec.maxLength = p.maxLength;
|
|
207
554
|
if (p.editable === false || p.readOnly === true) e.editable = false;
|
|
208
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");
|
|
209
565
|
if (p.disabled === true || state?.disabled === true || p["aria-disabled"] === true)
|
|
210
566
|
e.disabled = true;
|
|
211
567
|
}
|
|
212
|
-
|
|
213
|
-
|
|
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;
|
|
214
610
|
}
|
|
215
611
|
var round = (r) => r && {
|
|
216
612
|
x: Math.round(r.x),
|
|
@@ -234,10 +630,15 @@ function absorbChain(rec, fiber, host) {
|
|
|
234
630
|
if (f === host) break;
|
|
235
631
|
}
|
|
236
632
|
}
|
|
237
|
-
function
|
|
633
|
+
function collectScreen(roots, window) {
|
|
238
634
|
const byHost = /* @__PURE__ */ new Map();
|
|
239
635
|
const order = [];
|
|
240
|
-
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) => {
|
|
241
642
|
let rec = byHost.get(host);
|
|
242
643
|
if (rec) return rec;
|
|
243
644
|
rec = {
|
|
@@ -247,6 +648,10 @@ function collectElements(roots, window) {
|
|
|
247
648
|
host,
|
|
248
649
|
press: null,
|
|
249
650
|
input: null,
|
|
651
|
+
hidden,
|
|
652
|
+
scroller,
|
|
653
|
+
modal,
|
|
654
|
+
overlay,
|
|
250
655
|
parent,
|
|
251
656
|
parts: []
|
|
252
657
|
};
|
|
@@ -259,49 +664,118 @@ function collectElements(roots, window) {
|
|
|
259
664
|
for (let r = rec; r; r = r.parent) if (r.press) return r;
|
|
260
665
|
return null;
|
|
261
666
|
};
|
|
262
|
-
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
|
+
}));
|
|
263
685
|
while (stack.length) {
|
|
264
|
-
const
|
|
265
|
-
|
|
686
|
+
const item = stack.pop();
|
|
687
|
+
const { fiber, rec: parentRec } = item;
|
|
688
|
+
if (fiber.sibling) stack.push({ ...item, fiber: fiber.sibling });
|
|
266
689
|
const p = propsOf(fiber);
|
|
267
|
-
|
|
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
|
+
}
|
|
268
718
|
let rec = parentRec;
|
|
269
719
|
if (p && interesting(p)) {
|
|
270
720
|
const host = firstHost(fiber);
|
|
271
|
-
const
|
|
272
|
-
|
|
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) {
|
|
273
727
|
byHost.set(host, same);
|
|
274
728
|
same.host = host;
|
|
275
729
|
absorbChain(same, fiber, host);
|
|
276
730
|
rec = same;
|
|
277
731
|
} else if (host) {
|
|
278
|
-
rec = recFor(fiber, host, parentRec);
|
|
732
|
+
rec = recFor(fiber, host, parentRec, hidden, scroller, modal, overlay);
|
|
279
733
|
absorb(rec, fiber, p);
|
|
280
734
|
}
|
|
281
735
|
}
|
|
282
736
|
const found = textOf(fiber);
|
|
283
737
|
if (found?.host) {
|
|
284
|
-
const
|
|
738
|
+
const host = textHost(found.host);
|
|
739
|
+
const owner = buttonOf(rec) ?? recFor(host, host, rec, hidden, scroller, modal, overlay);
|
|
285
740
|
const last = owner.parts[owner.parts.length - 1];
|
|
286
|
-
if (last?.host ===
|
|
287
|
-
else owner.parts.push({ host
|
|
741
|
+
if (last?.host === host) last.text += found.text;
|
|
742
|
+
else owner.parts.push({ host, text: found.text });
|
|
288
743
|
}
|
|
289
|
-
if (fiber.child) stack.push({ fiber: fiber.child, rec });
|
|
744
|
+
if (fiber.child) stack.push({ fiber: fiber.child, rec, hidden, scroller, modal, overlay });
|
|
290
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));
|
|
291
750
|
for (const rec of order) {
|
|
292
751
|
const e = rec.element;
|
|
752
|
+
rec.hidden || (rec.hidden = covers2(rec));
|
|
293
753
|
const text = rec.parts.map((part) => part.text.replace(ICON_GLYPHS, "").replace(/\s+/g, " ").trim()).filter(Boolean).join(" ");
|
|
294
754
|
if (text) e.text = text;
|
|
295
|
-
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";
|
|
296
756
|
if (e.kind === "input" && e.editable === void 0) e.editable = true;
|
|
297
757
|
const rect = measureHost(rec.host);
|
|
298
758
|
e.rect = round(rect);
|
|
299
|
-
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);
|
|
300
762
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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
|
+
};
|
|
304
777
|
}
|
|
778
|
+
var focused = (found) => found.filter((f) => !f.hidden);
|
|
305
779
|
function pressFiberOf(found) {
|
|
306
780
|
if (found.press) return found.press;
|
|
307
781
|
for (let f = found.fiber.return; f; f = f.return) {
|
|
@@ -312,6 +786,26 @@ function pressFiberOf(found) {
|
|
|
312
786
|
}
|
|
313
787
|
|
|
314
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;
|
|
315
809
|
function tiers(target) {
|
|
316
810
|
if (typeof target === "string") {
|
|
317
811
|
return [
|
|
@@ -322,59 +816,77 @@ function tiers(target) {
|
|
|
322
816
|
(e) => !!e.text?.includes(target)
|
|
323
817
|
];
|
|
324
818
|
}
|
|
325
|
-
const { testID, label, placeholder, text } = target;
|
|
326
|
-
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);
|
|
327
821
|
if (text === void 0) return [fields];
|
|
328
822
|
return [
|
|
329
823
|
(e) => fields(e) && e.text === text,
|
|
330
824
|
(e) => fields(e) && !!e.text?.includes(text)
|
|
331
825
|
];
|
|
332
826
|
}
|
|
827
|
+
var labelOnly = ({ element: e, press, input }) => e.kind === "view" && !e.testID && !e.text && !e.placeholder && !press && !input;
|
|
333
828
|
function matchTarget(found, target) {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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
|
+
}
|
|
337
836
|
}
|
|
338
837
|
return [];
|
|
339
838
|
}
|
|
839
|
+
var isAt = (target) => typeof target === "object" && target.at !== void 0;
|
|
340
840
|
var indexOf = (target) => typeof target === "object" ? target.index : void 0;
|
|
341
841
|
var clip = (s) => s.length > 40 ? `${s.slice(0, 39)}\u2026` : s;
|
|
342
842
|
function describe(e) {
|
|
343
|
-
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));
|
|
344
846
|
if (e.testID) bits.push(`#${e.testID}`);
|
|
345
847
|
if (e.text) bits.push(JSON.stringify(clip(e.text)));
|
|
346
848
|
if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(clip(e.label))}`);
|
|
347
849
|
if (e.placeholder) bits.push(`placeholder=${JSON.stringify(clip(e.placeholder))}`);
|
|
348
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");
|
|
349
854
|
if (e.disabled) bits.push("disabled");
|
|
350
855
|
return bits.join(" ");
|
|
351
856
|
}
|
|
352
857
|
var showTarget = (target) => JSON.stringify(target);
|
|
353
858
|
function onScreenSummary(found, max = 15) {
|
|
354
|
-
const on = found.filter((f) => f.onScreen);
|
|
859
|
+
const on = found.filter((f) => f.onScreen && !f.hidden);
|
|
355
860
|
if (!on.length) return "nothing";
|
|
356
861
|
const shown = on.slice(0, max).map((f) => describe(f.element));
|
|
357
862
|
if (on.length > max) shown.push(`+${on.length - max} more`);
|
|
358
863
|
return shown.join("; ");
|
|
359
864
|
}
|
|
360
|
-
function resolveTarget(found, target, prefer) {
|
|
865
|
+
function resolveTarget(found, target, prefer, options) {
|
|
866
|
+
const reachable = found.filter((f) => !f.hidden);
|
|
361
867
|
let matches = matchTarget(
|
|
362
|
-
|
|
868
|
+
reachable.filter((f) => f.onScreen),
|
|
363
869
|
target
|
|
364
870
|
);
|
|
871
|
+
if (!matches.length && options?.offscreen) matches = matchTarget(reachable, target);
|
|
365
872
|
if (!matches.length) {
|
|
366
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
|
+
);
|
|
367
878
|
if (hidden)
|
|
368
879
|
throw new Error(
|
|
369
|
-
`${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`
|
|
370
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" : "";
|
|
371
883
|
throw new Error(
|
|
372
|
-
`Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}`
|
|
884
|
+
`Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}${unrendered}`
|
|
373
885
|
);
|
|
374
886
|
}
|
|
375
887
|
const preferred = prefer ? matches.filter(prefer) : [];
|
|
376
888
|
if (preferred.length) matches = preferred;
|
|
377
|
-
const index = indexOf(target);
|
|
889
|
+
const index = options?.index ?? indexOf(target) ?? (isAt(target) ? 0 : void 0);
|
|
378
890
|
if (index !== void 0) {
|
|
379
891
|
const pick = matches[index];
|
|
380
892
|
if (!pick)
|
|
@@ -385,8 +897,9 @@ function resolveTarget(found, target, prefer) {
|
|
|
385
897
|
}
|
|
386
898
|
if (matches.length > 1) {
|
|
387
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)`;
|
|
388
901
|
throw new Error(
|
|
389
|
-
`${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}`
|
|
390
903
|
);
|
|
391
904
|
}
|
|
392
905
|
return matches[0];
|
|
@@ -429,11 +942,13 @@ function fillInput(found, text, options = {}) {
|
|
|
429
942
|
return value;
|
|
430
943
|
}
|
|
431
944
|
var center = (rect) => rect ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } : { x: 0, y: 0 };
|
|
432
|
-
function pressElement(found) {
|
|
945
|
+
function pressElement(found, options = {}) {
|
|
433
946
|
const fiber = pressFiberOf(found);
|
|
434
947
|
if (!fiber) throw new Error(`${describe(found.element)} has no onPress`);
|
|
435
|
-
if (found.element.disabled || propsOf(fiber)?.disabled === true)
|
|
436
|
-
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
|
+
);
|
|
437
952
|
const { x, y } = center(found.element.rect);
|
|
438
953
|
const e = () => event({
|
|
439
954
|
locationX: found.element.rect ? found.element.rect.width / 2 : 0,
|
|
@@ -447,6 +962,236 @@ function pressElement(found) {
|
|
|
447
962
|
handler(fiber, "onPressOut")?.(e());
|
|
448
963
|
}
|
|
449
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
|
+
|
|
450
1195
|
// src/runtime/screen/settle.ts
|
|
451
1196
|
var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
|
|
452
1197
|
var UPDATE_LANES = (1 << 22) - 1;
|
|
@@ -518,6 +1263,11 @@ async function settle(options = {}) {
|
|
|
518
1263
|
|
|
519
1264
|
// src/runtime/screen/wait-for.ts
|
|
520
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
|
+
}
|
|
521
1271
|
function waitForTarget(collect, target, options = {}) {
|
|
522
1272
|
const timeoutMs = options.timeoutMs ?? 5e3;
|
|
523
1273
|
const t0 = performance.now();
|
|
@@ -536,7 +1286,7 @@ function waitForTarget(collect, target, options = {}) {
|
|
|
536
1286
|
try {
|
|
537
1287
|
const found = collect();
|
|
538
1288
|
const matches = matchTarget(
|
|
539
|
-
found.filter((f) => f.onScreen),
|
|
1289
|
+
found.filter((f) => f.onScreen && !f.hidden),
|
|
540
1290
|
target
|
|
541
1291
|
);
|
|
542
1292
|
const hit = matches[indexOf(target) ?? 0];
|
|
@@ -548,7 +1298,7 @@ function waitForTarget(collect, target, options = {}) {
|
|
|
548
1298
|
const what = options.gone ? "to disappear" : "to appear";
|
|
549
1299
|
reject(
|
|
550
1300
|
new Error(
|
|
551
|
-
`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)}`
|
|
552
1302
|
)
|
|
553
1303
|
);
|
|
554
1304
|
}
|
|
@@ -569,41 +1319,184 @@ function waitForTarget(collect, target, options = {}) {
|
|
|
569
1319
|
|
|
570
1320
|
// src/runtime/screen/index.ts
|
|
571
1321
|
function createScreen(env) {
|
|
572
|
-
const
|
|
573
|
-
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) => {
|
|
574
1337
|
const node = found.host.stateNode;
|
|
575
1338
|
const again = collect().find(
|
|
576
1339
|
(f) => f.host === found.host || node != null && f.host.stateNode === node
|
|
577
1340
|
);
|
|
578
|
-
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;
|
|
579
1381
|
};
|
|
580
1382
|
return {
|
|
581
1383
|
snapshot(options = {}) {
|
|
582
1384
|
const found = collect();
|
|
583
|
-
if (
|
|
584
|
-
return {
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
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 };
|
|
588
1397
|
},
|
|
1398
|
+
findText: (text, options) => findText(collect(), text, options),
|
|
589
1399
|
async fill(target, text, options = {}) {
|
|
590
|
-
const found =
|
|
1400
|
+
const found = await resolve(target, (f) => !!f.input, {
|
|
1401
|
+
index: options.index,
|
|
1402
|
+
afterText: true,
|
|
1403
|
+
scroll: options.scroll
|
|
1404
|
+
});
|
|
591
1405
|
const filled = fillInput(found, String(text), options);
|
|
592
1406
|
await settle();
|
|
593
|
-
return { filled, element:
|
|
1407
|
+
return { filled, element: reread(found).element };
|
|
594
1408
|
},
|
|
595
|
-
async press(target) {
|
|
596
|
-
const found =
|
|
597
|
-
pressElement(found);
|
|
1409
|
+
async press(target, options = {}) {
|
|
1410
|
+
const found = await resolve(target, (f) => !!f.press, options);
|
|
1411
|
+
pressElement(found, options);
|
|
598
1412
|
await settle();
|
|
599
1413
|
return found.element;
|
|
600
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
|
+
},
|
|
601
1493
|
waitFor: (target, options) => waitForTarget(collect, target, options)
|
|
602
1494
|
};
|
|
603
1495
|
}
|
|
604
1496
|
|
|
605
1497
|
// src/runtime/tools/screen.ts
|
|
606
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.";
|
|
607
1500
|
function screenTools() {
|
|
608
1501
|
const screen = createScreen({
|
|
609
1502
|
roots: () => fiberRoots(devToolsHook()),
|
|
@@ -611,40 +1504,49 @@ function screenTools() {
|
|
|
611
1504
|
});
|
|
612
1505
|
return {
|
|
613
1506
|
"screen.findText": {
|
|
614
|
-
description: "Find
|
|
615
|
-
run: (text, options) =>
|
|
616
|
-
fiberRoots(devToolsHook()),
|
|
617
|
-
text,
|
|
618
|
-
import_react_native.Dimensions.get("window"),
|
|
619
|
-
options
|
|
620
|
-
)
|
|
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)
|
|
621
1509
|
},
|
|
622
1510
|
"screen.snapshot": {
|
|
623
|
-
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.",
|
|
624
1512
|
run: (options) => screen.snapshot(options)
|
|
625
1513
|
},
|
|
626
1514
|
"screen.fill": {
|
|
627
|
-
description:
|
|
1515
|
+
description: `Type into an input, then wait for the render. ${TARGET_HELP} {submit:true} also submits.`,
|
|
628
1516
|
run: (target, text, options) => screen.fill(target, text, options)
|
|
629
1517
|
},
|
|
630
1518
|
"screen.press": {
|
|
631
|
-
description:
|
|
632
|
-
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)
|
|
633
1529
|
},
|
|
634
1530
|
"screen.waitFor": {
|
|
635
|
-
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.",
|
|
636
1532
|
run: (target, options) => screen.waitFor(target, options)
|
|
637
1533
|
}
|
|
638
1534
|
};
|
|
639
1535
|
}
|
|
640
1536
|
|
|
641
1537
|
// src/runtime/builtin-tools.ts
|
|
642
|
-
function builtinTools(listTools, getTools, logs) {
|
|
1538
|
+
function builtinTools(listTools, getTools, logs, getScenarios) {
|
|
643
1539
|
return {
|
|
644
1540
|
...bridgeTools(listTools),
|
|
1541
|
+
...appTools(
|
|
1542
|
+
selectReload(globalThis, () => import_react_native2.DevSettings.reload())
|
|
1543
|
+
),
|
|
645
1544
|
...logTools(logs),
|
|
646
1545
|
...restoreTools(getTools),
|
|
647
|
-
...screenTools()
|
|
1546
|
+
...screenTools(),
|
|
1547
|
+
...captureTools(loadViewShot),
|
|
1548
|
+
// Only when the app defines scenarios, so `tools` doesn't list empty ones.
|
|
1549
|
+
...getScenarios && scenarioTools(getScenarios, getTools)
|
|
648
1550
|
};
|
|
649
1551
|
}
|
|
650
1552
|
|
|
@@ -673,6 +1575,41 @@ function cdpTransport() {
|
|
|
673
1575
|
};
|
|
674
1576
|
}
|
|
675
1577
|
|
|
1578
|
+
// src/runtime/gate.ts
|
|
1579
|
+
function createGate(name) {
|
|
1580
|
+
let holds = 0;
|
|
1581
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
1582
|
+
const notify = () => {
|
|
1583
|
+
for (const fn of [...listeners]) fn(holds > 0);
|
|
1584
|
+
};
|
|
1585
|
+
return {
|
|
1586
|
+
name,
|
|
1587
|
+
get closed() {
|
|
1588
|
+
return holds > 0;
|
|
1589
|
+
},
|
|
1590
|
+
close: () => {
|
|
1591
|
+
let released = false;
|
|
1592
|
+
holds += 1;
|
|
1593
|
+
if (holds === 1) notify();
|
|
1594
|
+
return () => {
|
|
1595
|
+
if (released) return;
|
|
1596
|
+
released = true;
|
|
1597
|
+
holds -= 1;
|
|
1598
|
+
if (holds === 0) notify();
|
|
1599
|
+
};
|
|
1600
|
+
},
|
|
1601
|
+
subscribe: (fn) => {
|
|
1602
|
+
listeners.add(fn);
|
|
1603
|
+
return () => {
|
|
1604
|
+
listeners.delete(fn);
|
|
1605
|
+
};
|
|
1606
|
+
}
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
// src/runtime/load-id.ts
|
|
1611
|
+
var loadId = Math.random().toString(36).slice(2, 10);
|
|
1612
|
+
|
|
676
1613
|
// src/runtime/logs.ts
|
|
677
1614
|
var CAPACITY = 200;
|
|
678
1615
|
var STACK_LINES = 10;
|
|
@@ -735,9 +1672,9 @@ function formatArgs(args) {
|
|
|
735
1672
|
}
|
|
736
1673
|
parts.push(...rest.map(describe2));
|
|
737
1674
|
const error = args.find((a) => a instanceof Error);
|
|
738
|
-
const
|
|
1675
|
+
const message2 = parts.join(" ");
|
|
739
1676
|
return {
|
|
740
|
-
message:
|
|
1677
|
+
message: message2.length > MESSAGE_CHARS ? `${message2.slice(0, MESSAGE_CHARS)}...` : message2,
|
|
741
1678
|
...error?.stack ? { stack: trimStack(error.stack) } : {}
|
|
742
1679
|
};
|
|
743
1680
|
}
|
|
@@ -756,9 +1693,9 @@ function describe2(value) {
|
|
|
756
1693
|
var trimStack = (stack) => stack.split("\n").slice(0, STACK_LINES).join("\n");
|
|
757
1694
|
function installLogHooks(capture, env) {
|
|
758
1695
|
let quiet = 0;
|
|
759
|
-
let
|
|
1696
|
+
let active2 = true;
|
|
760
1697
|
const record = (level, args) => {
|
|
761
|
-
if (!
|
|
1698
|
+
if (!active2 || quiet) return;
|
|
762
1699
|
quiet++;
|
|
763
1700
|
try {
|
|
764
1701
|
capture.record(level, args);
|
|
@@ -801,7 +1738,7 @@ function installLogHooks(capture, env) {
|
|
|
801
1738
|
env.addEventListener?.("unhandledrejection", onRejection);
|
|
802
1739
|
}
|
|
803
1740
|
return () => {
|
|
804
|
-
|
|
1741
|
+
active2 = false;
|
|
805
1742
|
if (console.error === wrappers.error) console.error = originals.error;
|
|
806
1743
|
if (console.warn === wrappers.warn) console.warn = originals.warn;
|
|
807
1744
|
if (errorUtils && originalHandler && errorUtils.getGlobalHandler() === handler2)
|
|
@@ -854,7 +1791,7 @@ function toJson(value) {
|
|
|
854
1791
|
function unwrap(definition) {
|
|
855
1792
|
return typeof definition === "function" ? { run: definition } : definition;
|
|
856
1793
|
}
|
|
857
|
-
function createRegistry(getTools, logs) {
|
|
1794
|
+
function createRegistry(getTools, logs, loadId2) {
|
|
858
1795
|
const list = () => Object.entries(getTools()).map(([name, definition]) => ({
|
|
859
1796
|
name,
|
|
860
1797
|
description: unwrap(definition).description
|
|
@@ -863,7 +1800,12 @@ function createRegistry(getTools, logs) {
|
|
|
863
1800
|
const end = logs?.begin(call.tool);
|
|
864
1801
|
const result = await run(call, from).finally(end);
|
|
865
1802
|
const errors = logs?.takeErrors();
|
|
866
|
-
|
|
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 } : {};
|
|
867
1809
|
}
|
|
868
1810
|
async function run(call, from) {
|
|
869
1811
|
const t0 = performance.now();
|
|
@@ -874,11 +1816,17 @@ function createRegistry(getTools, logs) {
|
|
|
874
1816
|
const known = list().map((t) => t.name).join(", ");
|
|
875
1817
|
throw new Error(`Unknown tool "${call.tool}". Known: ${known}`);
|
|
876
1818
|
}
|
|
877
|
-
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);
|
|
878
1826
|
return { id: call.id, from, ok: true, value: toJson(value), ms: ms() };
|
|
879
1827
|
} catch (error) {
|
|
880
|
-
const
|
|
881
|
-
return { id: call.id, from, ok: false, error:
|
|
1828
|
+
const message2 = error instanceof Error ? error.message : String(error);
|
|
1829
|
+
return { id: call.id, from, ok: false, error: message2, ms: ms() };
|
|
882
1830
|
}
|
|
883
1831
|
}
|
|
884
1832
|
return { list, dispatch };
|
|
@@ -889,17 +1837,20 @@ var randomId = () => Math.random().toString(36).slice(2, 10);
|
|
|
889
1837
|
function startAgentBridge(options = {}) {
|
|
890
1838
|
const deviceId = randomId();
|
|
891
1839
|
const userTools = () => typeof options.tools === "function" ? options.tools() : options.tools ?? {};
|
|
1840
|
+
const { scenarios } = options;
|
|
1841
|
+
const getScenarios = scenarios ? () => typeof scenarios === "function" ? scenarios() : scenarios : void 0;
|
|
892
1842
|
const logs = startLogCapture();
|
|
893
1843
|
const allTools = () => ({
|
|
894
|
-
...builtinTools(() => registry.list(), allTools, logs.capture),
|
|
1844
|
+
...builtinTools(() => registry.list(), allTools, logs.capture, getScenarios),
|
|
895
1845
|
...userTools()
|
|
896
1846
|
});
|
|
897
|
-
const registry = createRegistry(allTools, logs.capture);
|
|
1847
|
+
const registry = createRegistry(allTools, logs.capture, loadId);
|
|
898
1848
|
const info = () => ({
|
|
899
1849
|
deviceId,
|
|
900
|
-
name: options.name ??
|
|
901
|
-
platform:
|
|
1850
|
+
name: options.name ?? import_react_native3.Platform.OS,
|
|
1851
|
+
platform: import_react_native3.Platform.OS,
|
|
902
1852
|
protocol: PROTOCOL_VERSION,
|
|
1853
|
+
loadId,
|
|
903
1854
|
tools: registry.list()
|
|
904
1855
|
});
|
|
905
1856
|
const context = {
|
|
@@ -927,7 +1878,12 @@ function useAgentBridge(options = {}) {
|
|
|
927
1878
|
tools: () => {
|
|
928
1879
|
const { tools } = latest.current;
|
|
929
1880
|
return typeof tools === "function" ? tools() : tools ?? {};
|
|
930
|
-
}
|
|
1881
|
+
},
|
|
1882
|
+
// Decided at mount: scenario.* tools are there or not for the bridge's life.
|
|
1883
|
+
scenarios: latest.current.scenarios ? () => {
|
|
1884
|
+
const { scenarios } = latest.current;
|
|
1885
|
+
return typeof scenarios === "function" ? scenarios() : scenarios ?? {};
|
|
1886
|
+
} : void 0
|
|
931
1887
|
});
|
|
932
1888
|
}, []);
|
|
933
1889
|
}
|