@avasapp/agent-bridge 0.5.0 → 0.6.1
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 +20 -6
- 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.d.cts +1 -1
- package/dist/adapters/zustand.cjs +193 -22
- package/dist/adapters/zustand.d.cts +11 -15
- package/dist/{chunk-3QIG7A5W.js → chunk-Y6QFU5ON.js} +233 -26
- package/dist/cli.js +1383 -255
- package/dist/client/index.cjs +233 -27
- 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 +822 -140
- package/dist/runtime/index.d.cts +2 -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 +32 -1
- package/skills/agent-bridge/SKILL.md +39 -8
- package/skills/agent-bridge/references/device.md +21 -0
- package/skills/agent-bridge/references/flows.md +13 -3
- package/skills/agent-bridge/references/network.md +19 -1
- package/skills/agent-bridge/references/realtime.md +1 -1
- package/skills/agent-bridge/references/restore.md +21 -3
- package/skills/agent-bridge/references/screen.md +8 -2
- package/skills/agent-bridge/references/state.md +21 -1
package/dist/runtime/index.cjs
CHANGED
|
@@ -70,6 +70,121 @@ async function runUndos() {
|
|
|
70
70
|
return { ran: batch.length, errors };
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
+
// src/runtime/tools/descriptions.ts
|
|
74
|
+
var INTERNAL_TOOLS = ["bridge.adopt"];
|
|
75
|
+
var UNLISTED_WITHOUT_APP = [...INTERNAL_TOOLS, "bridge.ping"];
|
|
76
|
+
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.";
|
|
77
|
+
var TOOL_DESCRIPTIONS = {
|
|
78
|
+
"app.reload": `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.`,
|
|
79
|
+
"app.restore": `Undo what the app's own tools changed: runs the undos they registered with onRestore, newest first, once. Returns how many ran.`,
|
|
80
|
+
"bridge.adopt": `Internal: a session hands back its journal after a reload.`,
|
|
81
|
+
"bridge.logs": `Errors and warnings the app logged, newest last. Options: {level: "error"|"warn"|"all", limit, clear}.`,
|
|
82
|
+
"bridge.pending": `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.`,
|
|
83
|
+
"bridge.ping": `Round-trip check. Returns the app clock.`,
|
|
84
|
+
"bridge.restore": `Undo what the agent changed: runs every *.restore tool in name order, scenario.restore last, and returns each result or error.`,
|
|
85
|
+
"bridge.tools": `Every tool this app exposes.`,
|
|
86
|
+
"scenario.apply": `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.`,
|
|
87
|
+
"scenario.list": `Setups the app defines, e.g. a signed-in user: name, description, options (a JSON Schema), active.`,
|
|
88
|
+
"scenario.restore": `Undo every active scenario, newest first. bridge.restore runs this after every other restorer.`,
|
|
89
|
+
"screen.capture": `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.`,
|
|
90
|
+
"screen.fill": `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.`,
|
|
91
|
+
"screen.findText": `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.`,
|
|
92
|
+
"screen.press": `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.`,
|
|
93
|
+
"screen.refresh": `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.`,
|
|
94
|
+
"screen.scroll": `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}`,
|
|
95
|
+
"screen.setValue": `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}`,
|
|
96
|
+
"screen.snapshot": `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).`,
|
|
97
|
+
"screen.waitFor": `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.`
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
// src/runtime/tools/restore.ts
|
|
101
|
+
var SUFFIX = ".restore";
|
|
102
|
+
var SELF = "bridge.restore";
|
|
103
|
+
var LAST = "scenario.restore";
|
|
104
|
+
function pendingByArea(tools, detail = false) {
|
|
105
|
+
const out = {};
|
|
106
|
+
for (const [name, d] of Object.entries(tools)) {
|
|
107
|
+
if (!name.endsWith(SUFFIX) || name === SELF || typeof d === "function")
|
|
108
|
+
continue;
|
|
109
|
+
const area3 = name.slice(0, -SUFFIX.length);
|
|
110
|
+
try {
|
|
111
|
+
const found = d.pending?.({ detail });
|
|
112
|
+
if (found) out[area3] = found;
|
|
113
|
+
} catch (error) {
|
|
114
|
+
out[area3] = `pending check failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return out;
|
|
118
|
+
}
|
|
119
|
+
function journalByArea(tools) {
|
|
120
|
+
const out = {};
|
|
121
|
+
for (const [name, d] of restorers(tools)) {
|
|
122
|
+
try {
|
|
123
|
+
const data = d.journal?.();
|
|
124
|
+
if (data !== void 0) out[name] = data;
|
|
125
|
+
} catch {
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return out;
|
|
129
|
+
}
|
|
130
|
+
function journalRev(tools) {
|
|
131
|
+
let rev = 0;
|
|
132
|
+
for (const [, d] of restorers(tools)) rev += d.journalRev?.() ?? 0;
|
|
133
|
+
return rev;
|
|
134
|
+
}
|
|
135
|
+
function restorers(tools) {
|
|
136
|
+
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");
|
|
137
|
+
}
|
|
138
|
+
function restoreTools(getTools) {
|
|
139
|
+
return {
|
|
140
|
+
"bridge.pending": {
|
|
141
|
+
maxArgs: 0,
|
|
142
|
+
description: TOOL_DESCRIPTIONS["bridge.pending"],
|
|
143
|
+
run: () => pendingByArea(getTools(), true)
|
|
144
|
+
},
|
|
145
|
+
"bridge.adopt": {
|
|
146
|
+
maxArgs: 1,
|
|
147
|
+
description: TOOL_DESCRIPTIONS["bridge.adopt"],
|
|
148
|
+
run: async (data) => {
|
|
149
|
+
const tools = getTools();
|
|
150
|
+
const out = {};
|
|
151
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) throw new Error("bridge.adopt takes an object");
|
|
152
|
+
for (const [area3, part] of Object.entries(data)) {
|
|
153
|
+
const d = tools[`${area3}${SUFFIX}`];
|
|
154
|
+
if (typeof d === "function" || !d?.adopt) out[area3] = { adopted: [], already: [], changed: [], retry: part, error: "this area has no adopt" };
|
|
155
|
+
else
|
|
156
|
+
try {
|
|
157
|
+
out[area3] = await d.adopt(part);
|
|
158
|
+
} catch (error) {
|
|
159
|
+
out[area3] = { adopted: [], already: [], changed: [], retry: part, error: error instanceof Error ? error.message : String(error) };
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return out;
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
[SELF]: {
|
|
166
|
+
description: TOOL_DESCRIPTIONS["bridge.restore"],
|
|
167
|
+
run: async () => {
|
|
168
|
+
const tools = getTools();
|
|
169
|
+
const results = {};
|
|
170
|
+
const names = Object.keys(tools).filter((name) => name.endsWith(SUFFIX) && name !== SELF).sort((a, b) => Number(a === LAST) - Number(b === LAST) || a.localeCompare(b));
|
|
171
|
+
for (const name of names) {
|
|
172
|
+
const definition = tools[name];
|
|
173
|
+
const run = typeof definition === "function" ? definition : definition.run;
|
|
174
|
+
try {
|
|
175
|
+
results[name] = await run();
|
|
176
|
+
} catch (error) {
|
|
177
|
+
results[name] = {
|
|
178
|
+
error: error instanceof Error ? error.message : String(error)
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return results;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
|
|
73
188
|
// src/runtime/tools/app.ts
|
|
74
189
|
function selectReload(host, devSettingsReload) {
|
|
75
190
|
return () => {
|
|
@@ -84,13 +199,14 @@ function selectReload(host, devSettingsReload) {
|
|
|
84
199
|
}
|
|
85
200
|
};
|
|
86
201
|
}
|
|
87
|
-
function appTools(reload) {
|
|
202
|
+
function appTools(reload, getTools) {
|
|
88
203
|
return {
|
|
89
204
|
"app.reload": {
|
|
90
|
-
description: "
|
|
205
|
+
description: TOOL_DESCRIPTIONS["app.reload"],
|
|
91
206
|
run: () => {
|
|
92
207
|
setTimeout(reload, 50);
|
|
93
|
-
|
|
208
|
+
const lost = getTools ? Object.keys(pendingByArea(getTools())) : [];
|
|
209
|
+
return lost.length ? { reloading: true, lost: `pending restores are lost: ${lost.join(", ")}` } : { reloading: true };
|
|
94
210
|
}
|
|
95
211
|
},
|
|
96
212
|
"app.restore": {
|
|
@@ -100,7 +216,7 @@ function appTools(reload) {
|
|
|
100
216
|
const labels = pendingUndos();
|
|
101
217
|
return labels.length ? labels : false;
|
|
102
218
|
},
|
|
103
|
-
description: "
|
|
219
|
+
description: TOOL_DESCRIPTIONS["app.restore"],
|
|
104
220
|
run: async () => {
|
|
105
221
|
const { ran, errors } = await runUndos();
|
|
106
222
|
if (errors.length)
|
|
@@ -115,11 +231,11 @@ function appTools(reload) {
|
|
|
115
231
|
function bridgeTools(listTools) {
|
|
116
232
|
return {
|
|
117
233
|
"bridge.ping": {
|
|
118
|
-
description: "
|
|
234
|
+
description: TOOL_DESCRIPTIONS["bridge.ping"],
|
|
119
235
|
run: () => ({ pong: true, at: Date.now() })
|
|
120
236
|
},
|
|
121
237
|
"bridge.tools": {
|
|
122
|
-
description: "
|
|
238
|
+
description: TOOL_DESCRIPTIONS["bridge.tools"],
|
|
123
239
|
run: listTools
|
|
124
240
|
}
|
|
125
241
|
};
|
|
@@ -139,7 +255,7 @@ function loadViewShot() {
|
|
|
139
255
|
function captureTools(load) {
|
|
140
256
|
return {
|
|
141
257
|
"screen.capture": {
|
|
142
|
-
description: "
|
|
258
|
+
description: TOOL_DESCRIPTIONS["screen.capture"],
|
|
143
259
|
maxArgs: 1,
|
|
144
260
|
run: async (options) => {
|
|
145
261
|
const viewShot = load();
|
|
@@ -165,7 +281,7 @@ function captureTools(load) {
|
|
|
165
281
|
function logTools(logs) {
|
|
166
282
|
return {
|
|
167
283
|
"bridge.logs": {
|
|
168
|
-
description:
|
|
284
|
+
description: TOOL_DESCRIPTIONS["bridge.logs"],
|
|
169
285
|
run: (options = {}) => {
|
|
170
286
|
const entries = logs.read(options);
|
|
171
287
|
if (options.clear) logs.clear();
|
|
@@ -175,55 +291,6 @@ function logTools(logs) {
|
|
|
175
291
|
};
|
|
176
292
|
}
|
|
177
293
|
|
|
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
294
|
// src/runtime/options-schema.ts
|
|
228
295
|
var import_json_schema = require("@cfworker/json-schema");
|
|
229
296
|
var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
@@ -320,7 +387,7 @@ function scenarioTools(getScenarios, getTools) {
|
|
|
320
387
|
};
|
|
321
388
|
return {
|
|
322
389
|
"scenario.list": {
|
|
323
|
-
description: "
|
|
390
|
+
description: TOOL_DESCRIPTIONS["scenario.list"],
|
|
324
391
|
run: () => Object.entries(getScenarios()).map(([name, s]) => ({
|
|
325
392
|
name,
|
|
326
393
|
description: s.description,
|
|
@@ -329,7 +396,7 @@ function scenarioTools(getScenarios, getTools) {
|
|
|
329
396
|
})).sort((a, b) => a.name.localeCompare(b.name))
|
|
330
397
|
},
|
|
331
398
|
"scenario.apply": {
|
|
332
|
-
description: "
|
|
399
|
+
description: TOOL_DESCRIPTIONS["scenario.apply"],
|
|
333
400
|
run: async (name, options) => {
|
|
334
401
|
const scenario = find(name);
|
|
335
402
|
if (scenario.options) {
|
|
@@ -366,7 +433,7 @@ scenario.list shows its options schema.`
|
|
|
366
433
|
},
|
|
367
434
|
"scenario.restore": {
|
|
368
435
|
pending: () => active.size > 0,
|
|
369
|
-
description: "
|
|
436
|
+
description: TOOL_DESCRIPTIONS["scenario.restore"],
|
|
370
437
|
run: async () => {
|
|
371
438
|
const names = [...active.keys()].reverse();
|
|
372
439
|
const errors = [];
|
|
@@ -465,6 +532,9 @@ var MODAL_HOSTS = /* @__PURE__ */ new Set(["RCTModalHostView", "ModalHostView"])
|
|
|
465
532
|
var OVERLAY_HOST = "RNSFullWindowOverlay";
|
|
466
533
|
var SCREEN_STACK = "RNSScreenStack";
|
|
467
534
|
var STACK_SCREENS = /* @__PURE__ */ new Set(["RNSScreen", "RNSModalScreen"]);
|
|
535
|
+
var TABS_HOSTS = /* @__PURE__ */ new Set(["RNSTabsHostIOS", "RNSTabsHostAndroid"]);
|
|
536
|
+
var OLD_TAB_SCREENS = /* @__PURE__ */ new Set(["RNSBottomTabsScreen", "RNSTabsScreen"]);
|
|
537
|
+
var TAB_SCREENS = /* @__PURE__ */ new Set(["RNSTabsScreenIOS", "RNSTabsScreenAndroid"]);
|
|
468
538
|
var SEE_THROUGH = /* @__PURE__ */ new Set(["transparentModal", "containedTransparentModal", "overFullScreen"]);
|
|
469
539
|
var undimmedSheet = (p) => {
|
|
470
540
|
const at = p.sheetLargestUndimmedDetentIndex;
|
|
@@ -500,6 +570,23 @@ function stackScreens(stack) {
|
|
|
500
570
|
presented: screens.slice(1).filter(coversWindow)
|
|
501
571
|
};
|
|
502
572
|
}
|
|
573
|
+
function unselectedTabs(tabs) {
|
|
574
|
+
const selected = propsOf(tabs)?.navStateRequest?.selectedScreenKey;
|
|
575
|
+
const out = [];
|
|
576
|
+
let matches = 0;
|
|
577
|
+
if (typeof selected !== "string") return out;
|
|
578
|
+
const todo = tabs.child ? [tabs.child] : [];
|
|
579
|
+
while (todo.length) {
|
|
580
|
+
const f = todo.pop();
|
|
581
|
+
if (f.sibling) todo.push(f.sibling);
|
|
582
|
+
if (f.tag === HOST_COMPONENT && TAB_SCREENS.has(f.type)) {
|
|
583
|
+
if (propsOf(f)?.screenKey === selected) matches++;
|
|
584
|
+
else out.push(f);
|
|
585
|
+
} else if (f.child) todo.push(f.child);
|
|
586
|
+
}
|
|
587
|
+
return matches === 1 ? out : [];
|
|
588
|
+
}
|
|
589
|
+
var hasDom = (recs) => recs.some((r) => !!r.host.stateNode && "getBoundingClientRect" in r.host.stateNode);
|
|
503
590
|
function hostAbove(host) {
|
|
504
591
|
let up = host.return;
|
|
505
592
|
while (up && up.tag !== HOST_COMPONENT) up = up.return;
|
|
@@ -513,6 +600,12 @@ var inside = (modal, top) => {
|
|
|
513
600
|
};
|
|
514
601
|
var SWITCH_HOSTS = /* @__PURE__ */ new Set(["RCTSwitch", "AndroidSwitch"]);
|
|
515
602
|
var isFn = (v) => typeof v === "function";
|
|
603
|
+
function isSwitchFiber(fiber) {
|
|
604
|
+
const p = propsOf(fiber);
|
|
605
|
+
if ((p?.accessibilityRole ?? p?.role) === "switch") return true;
|
|
606
|
+
const own = firstHost(fiber);
|
|
607
|
+
return !!own && SWITCH_HOSTS.has(String(own.type));
|
|
608
|
+
}
|
|
516
609
|
var isInputProps = (p) => isFn(p.onChangeText) || isFn(p.onChange) && typeof p.value !== "boolean" && ("value" in p || "defaultValue" in p || "placeholder" in p);
|
|
517
610
|
var labelOf = (p) => p.accessibilityLabel ?? p["aria-label"];
|
|
518
611
|
function actionNamesOf(p) {
|
|
@@ -520,7 +613,37 @@ function actionNamesOf(p) {
|
|
|
520
613
|
return p.accessibilityActions.map((a) => a?.name).filter((name) => typeof name === "string");
|
|
521
614
|
}
|
|
522
615
|
var activates = (rec) => !!rec.actions && !!rec.element.actions?.includes("activate");
|
|
523
|
-
var
|
|
616
|
+
var TEXT_HOSTS = /^RCT(Virtual)?Text$/;
|
|
617
|
+
var changesValue = (p) => isFn(p.onValueChange) || isFn(p.onSlidingComplete) || Array.isArray(p.values) && isFn(p.onChange);
|
|
618
|
+
var isControl = (p) => isFn(p.onPress) || changesValue(p) || activatesProps(p);
|
|
619
|
+
var activatesProps = (p) => !!actionNamesOf(p)?.includes("activate");
|
|
620
|
+
function wrapsControls(rec) {
|
|
621
|
+
const skip = /* @__PURE__ */ new Set();
|
|
622
|
+
const markChain = (from) => {
|
|
623
|
+
for (let f = from; f; f = f.child) {
|
|
624
|
+
skip.add(f);
|
|
625
|
+
if (f.tag === HOST_COMPONENT) break;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
markChain(rec.fiber);
|
|
629
|
+
let controls = 0;
|
|
630
|
+
const stack = rec.fiber.child ? [rec.fiber.child] : [];
|
|
631
|
+
while (stack.length) {
|
|
632
|
+
const f = stack.pop();
|
|
633
|
+
if (f.sibling) stack.push(f.sibling);
|
|
634
|
+
if (f.child) stack.push(f.child);
|
|
635
|
+
const p = propsOf(f);
|
|
636
|
+
if (!p || skip.has(f)) continue;
|
|
637
|
+
if (isInputProps(p)) return true;
|
|
638
|
+
if (!isControl(p)) continue;
|
|
639
|
+
const own = firstHost(f);
|
|
640
|
+
if (isFn(p.onPress) && !isFn(p.onValueChange) && own && TEXT_HOSTS.test(String(own.type))) continue;
|
|
641
|
+
markChain(f);
|
|
642
|
+
if (++controls >= 2) return true;
|
|
643
|
+
}
|
|
644
|
+
return false;
|
|
645
|
+
}
|
|
646
|
+
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;
|
|
524
647
|
var ICON_GLYPHS = /[\uE000-\uF8FF]/g;
|
|
525
648
|
function firstHost(fiber) {
|
|
526
649
|
if (fiber.tag === HOST_COMPONENT) return fiber;
|
|
@@ -537,6 +660,7 @@ function absorb(rec, fiber, p) {
|
|
|
537
660
|
const e = rec.element;
|
|
538
661
|
if (!rec.input && isInputProps(p)) rec.input = fiber;
|
|
539
662
|
if (!rec.press && isFn(p.onPress)) rec.press = fiber;
|
|
663
|
+
if (!rec.change && changesValue(p)) rec.change = fiber;
|
|
540
664
|
const actions = actionNamesOf(p);
|
|
541
665
|
if (!rec.actions && actions) {
|
|
542
666
|
rec.actions = fiber;
|
|
@@ -589,14 +713,14 @@ var within = (host, ancestor) => {
|
|
|
589
713
|
};
|
|
590
714
|
function visibleArea(base, host, bars, window) {
|
|
591
715
|
let area3 = base;
|
|
592
|
-
const
|
|
716
|
+
const clip = (r) => {
|
|
593
717
|
area3 = area3 && intersect(area3, r);
|
|
594
718
|
};
|
|
595
719
|
for (const bar of bars) {
|
|
596
720
|
if (within(host, bar)) continue;
|
|
597
721
|
const r = measureHost(bar);
|
|
598
722
|
if (!usable(r) || r.width < window.width * 0.9) continue;
|
|
599
|
-
|
|
723
|
+
clip(
|
|
600
724
|
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 }
|
|
601
725
|
);
|
|
602
726
|
}
|
|
@@ -658,6 +782,7 @@ function collectScreen(roots, window) {
|
|
|
658
782
|
host,
|
|
659
783
|
press: null,
|
|
660
784
|
input: null,
|
|
785
|
+
change: null,
|
|
661
786
|
actions: null,
|
|
662
787
|
hidden,
|
|
663
788
|
scroller,
|
|
@@ -671,8 +796,14 @@ function collectScreen(roots, window) {
|
|
|
671
796
|
order.push(rec);
|
|
672
797
|
return rec;
|
|
673
798
|
};
|
|
799
|
+
const wrapper = /* @__PURE__ */ new Map();
|
|
674
800
|
const buttonOf = (rec) => {
|
|
675
|
-
for (let r = rec; r; r = r.parent)
|
|
801
|
+
for (let r = rec; r; r = r.parent)
|
|
802
|
+
if (r.press || activates(r)) {
|
|
803
|
+
let wraps = wrapper.get(r);
|
|
804
|
+
if (wraps === void 0) wrapper.set(r, wraps = wrapsControls(r));
|
|
805
|
+
return wraps ? null : r;
|
|
806
|
+
}
|
|
676
807
|
return null;
|
|
677
808
|
};
|
|
678
809
|
const textHost = (host) => {
|
|
@@ -707,6 +838,8 @@ function collectScreen(roots, window) {
|
|
|
707
838
|
for (const s of screens.covered) covered.add(s);
|
|
708
839
|
for (const s of screens.presented) presented.add(s);
|
|
709
840
|
}
|
|
841
|
+
if (OLD_TAB_SCREENS.has(fiber.type) && p?.isFocused === false) hidden = true;
|
|
842
|
+
if (TABS_HOSTS.has(fiber.type)) for (const s of unselectedTabs(fiber)) covered.add(s);
|
|
710
843
|
if (fiber.type === OVERLAY_HOST) overlay = true;
|
|
711
844
|
if (p && !hidden && (p.accessibilityRole ?? p.role) === "tablist") bars.push(fiber);
|
|
712
845
|
if (!hidden && (MODAL_HOSTS.has(fiber.type) || presented.has(fiber))) {
|
|
@@ -758,6 +891,7 @@ function collectScreen(roots, window) {
|
|
|
758
891
|
const covers2 = (x) => !!top && !x.overlay && !inside(x.modal, top);
|
|
759
892
|
for (const scrollable of scrollByHost.values())
|
|
760
893
|
scrollable.hidden || (scrollable.hidden = covers2(scrollable));
|
|
894
|
+
let measured = false;
|
|
761
895
|
for (const rec of order) {
|
|
762
896
|
const e = rec.element;
|
|
763
897
|
rec.hidden || (rec.hidden = covers2(rec));
|
|
@@ -766,11 +900,13 @@ function collectScreen(roots, window) {
|
|
|
766
900
|
e.kind = e.webview !== void 0 ? "webview" : rec.input ? "input" : rec.press || activates(rec) ? "button" : rec.parts.length ? "text" : "view";
|
|
767
901
|
if (e.kind === "input" && e.editable === void 0) e.editable = true;
|
|
768
902
|
const rect = measureHost(rec.host);
|
|
903
|
+
if (rect) measured = true;
|
|
769
904
|
e.rect = round(rect);
|
|
770
905
|
const own = scrollByHost.get(rec.host) ?? scrollByHost.get(hostAbove(rec.host));
|
|
771
906
|
if (own) rec.scroller = own;
|
|
772
907
|
rec.onScreen = isOnScreen(rec, rect, window, bars);
|
|
773
908
|
}
|
|
909
|
+
const unmeasured = order.length > 0 && !measured && !hasDom(order) && !globalThis.nativeFabricUIManager;
|
|
774
910
|
const named = /* @__PURE__ */ new Set();
|
|
775
911
|
const single = (rec) => {
|
|
776
912
|
const name = rec.element.webview;
|
|
@@ -783,18 +919,22 @@ function collectScreen(roots, window) {
|
|
|
783
919
|
found: order.filter(
|
|
784
920
|
(rec) => (rec.element.kind !== "text" || rec.element.text !== void 0) && single(rec)
|
|
785
921
|
),
|
|
786
|
-
scrollables: [...scrollByHost.values()]
|
|
922
|
+
scrollables: [...scrollByHost.values()],
|
|
923
|
+
unmeasured
|
|
787
924
|
};
|
|
788
925
|
}
|
|
926
|
+
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.";
|
|
789
927
|
var focused = (found) => found.filter((f) => !f.hidden);
|
|
790
928
|
function pressFiberOf(found) {
|
|
791
|
-
if (found.press) return { fiber: found.press, via: "onPress" };
|
|
792
|
-
if (activates(found))
|
|
929
|
+
if (found.press) return { fiber: found.press, via: "onPress", ancestor: false };
|
|
930
|
+
if (activates(found))
|
|
931
|
+
return { fiber: found.actions, via: "accessibilityAction", ancestor: false };
|
|
793
932
|
for (let f = found.fiber.return; f; f = f.return) {
|
|
794
933
|
const p = propsOf(f);
|
|
795
934
|
if (!p) continue;
|
|
796
|
-
if (isFn(p.onPress)) return { fiber: f, via: "onPress" };
|
|
797
|
-
if (actionNamesOf(p)?.includes("activate"))
|
|
935
|
+
if (isFn(p.onPress)) return { fiber: f, via: "onPress", ancestor: true };
|
|
936
|
+
if (actionNamesOf(p)?.includes("activate"))
|
|
937
|
+
return { fiber: f, via: "accessibilityAction", ancestor: true };
|
|
798
938
|
}
|
|
799
939
|
return null;
|
|
800
940
|
}
|
|
@@ -860,15 +1000,16 @@ function matchTarget(found, target) {
|
|
|
860
1000
|
}
|
|
861
1001
|
var isAt = (target) => typeof target === "object" && target.at !== void 0;
|
|
862
1002
|
var indexOf = (target) => typeof target === "object" ? target.index : void 0;
|
|
863
|
-
|
|
864
|
-
|
|
1003
|
+
function describe(e, max = 40, wholeIds = false) {
|
|
1004
|
+
const clip = (s) => s.length > max ? `${s.slice(0, max - 1)}\u2026` : s;
|
|
1005
|
+
const id = (s) => wholeIds ? s : clip(s);
|
|
865
1006
|
const stated = e.checked !== void 0 || e.selected !== void 0 || e.expanded !== void 0;
|
|
866
1007
|
const bits = [stated && e.role ? e.role : e.kind];
|
|
867
1008
|
if (e.webview !== void 0) bits.push(JSON.stringify(e.webview));
|
|
868
1009
|
if (e.testID) bits.push(`#${e.testID}`);
|
|
869
1010
|
if (e.text) bits.push(JSON.stringify(clip(e.text)));
|
|
870
|
-
if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(
|
|
871
|
-
if (e.placeholder) bits.push(`placeholder=${JSON.stringify(
|
|
1011
|
+
if (e.label && e.label !== e.text) bits.push(`label=${JSON.stringify(id(e.label))}`);
|
|
1012
|
+
if (e.placeholder) bits.push(`placeholder=${JSON.stringify(id(e.placeholder))}`);
|
|
872
1013
|
if (e.value !== void 0) bits.push(`value=${JSON.stringify(clip(e.value))}`);
|
|
873
1014
|
if (e.checked !== void 0) bits.push(e.checked === true ? "checked" : e.checked === false ? "unchecked" : "mixed");
|
|
874
1015
|
if (e.selected !== void 0) bits.push(e.selected ? "selected" : "unselected");
|
|
@@ -890,7 +1031,8 @@ function resolveTarget(found, target, prefer, options) {
|
|
|
890
1031
|
reachable.filter((f) => f.onScreen),
|
|
891
1032
|
target
|
|
892
1033
|
);
|
|
893
|
-
|
|
1034
|
+
const hasIndex = (options?.index ?? indexOf(target)) !== void 0;
|
|
1035
|
+
if (options?.offscreen && (!matches.length || hasIndex)) matches = matchTarget(reachable, target);
|
|
894
1036
|
if (!matches.length) {
|
|
895
1037
|
const hidden = matchTarget(found, target)[0];
|
|
896
1038
|
if (hidden?.hidden)
|
|
@@ -901,7 +1043,7 @@ function resolveTarget(found, target, prefer, options) {
|
|
|
901
1043
|
throw new Error(
|
|
902
1044
|
`${showTarget(target)} matches ${describe(hidden.element)}, which is not on screen. Pass {scroll:true} to scroll it into view, or use screen.scroll`
|
|
903
1045
|
);
|
|
904
|
-
const unrendered = options?.offscreen ? ". A list renders only the rows near its window: for a row it has not rendered yet
|
|
1046
|
+
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";
|
|
905
1047
|
throw new Error(
|
|
906
1048
|
`Nothing on screen matches ${showTarget(target)}. On screen: ${onScreenSummary(found)}${unrendered}`
|
|
907
1049
|
);
|
|
@@ -918,10 +1060,10 @@ function resolveTarget(found, target, prefer, options) {
|
|
|
918
1060
|
return pick;
|
|
919
1061
|
}
|
|
920
1062
|
if (matches.length > 1) {
|
|
921
|
-
const
|
|
1063
|
+
const list2 = matches.slice(0, 10).map((f, i) => `${i}: ${describe(f.element)}`).join("; ");
|
|
922
1064
|
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)`;
|
|
923
1065
|
throw new Error(
|
|
924
|
-
`${showTarget(target)} matches ${matches.length} elements; pick one with an index, e.g. ${example}, or use a narrower target. ${
|
|
1066
|
+
`${showTarget(target)} matches ${matches.length} elements; pick one with an index, e.g. ${example}, or use a narrower target. ${list2}`
|
|
925
1067
|
);
|
|
926
1068
|
}
|
|
927
1069
|
return matches[0];
|
|
@@ -946,22 +1088,64 @@ var event = (nativeEvent, target) => ({
|
|
|
946
1088
|
persist: noop
|
|
947
1089
|
});
|
|
948
1090
|
var eventCount = 0;
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1091
|
+
var has = (v) => v !== void 0 && v !== null;
|
|
1092
|
+
var isUncontrolled = (found) => !!found.input && !has(propsOf(found.input)?.value) && !has(propsOf(found.host)?.value);
|
|
1093
|
+
function setNativeText(found, text) {
|
|
1094
|
+
for (const fiber of [found.host, found.input]) {
|
|
1095
|
+
const node = fiber?.stateNode;
|
|
1096
|
+
for (const target of [node, node?.canonical?.publicInstance]) {
|
|
1097
|
+
if (target && typeof target.setNativeProps === "function") {
|
|
1098
|
+
target.setNativeProps({ text });
|
|
1099
|
+
return true;
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
const dom = found.host.stateNode;
|
|
1104
|
+
if (dom && typeof dom === "object" && "value" in dom) {
|
|
1105
|
+
dom.value = text;
|
|
1106
|
+
return true;
|
|
1107
|
+
}
|
|
1108
|
+
return false;
|
|
1109
|
+
}
|
|
1110
|
+
var keysOf = (text) => {
|
|
1111
|
+
const Segmenter = Intl.Segmenter;
|
|
1112
|
+
return Segmenter ? Array.from(new Segmenter(void 0, { granularity: "grapheme" }).segment(text), (x) => x.segment) : Array.from(text);
|
|
1113
|
+
};
|
|
1114
|
+
async function fillInput(found, text, options = {}) {
|
|
1115
|
+
if (!found.input) throw new Error(`${describe(found.element)} is not a text input`);
|
|
952
1116
|
if (found.element.editable === false)
|
|
953
1117
|
throw new Error(`${describe(found.element)} is not editable`);
|
|
954
1118
|
const { maxLength } = found;
|
|
955
1119
|
const value = maxLength === void 0 ? text : text.slice(0, Math.max(0, maxLength));
|
|
1120
|
+
const uncontrolled = isUncontrolled(found);
|
|
1121
|
+
let native = uncontrolled ? false : null;
|
|
1122
|
+
let current = found;
|
|
1123
|
+
const change = (next) => {
|
|
1124
|
+
const target2 = { value: next };
|
|
1125
|
+
if (uncontrolled && setNativeText(current, next)) native = true;
|
|
1126
|
+
const input2 = current.input;
|
|
1127
|
+
handler(input2, "onChangeText")?.(next);
|
|
1128
|
+
eventCount += 1;
|
|
1129
|
+
handler(input2, "onChange")?.(event({ text: next, eventCount }, target2));
|
|
1130
|
+
};
|
|
956
1131
|
const target = { value };
|
|
957
|
-
handler(input, "onFocus")?.(event({ text: value }, target));
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
1132
|
+
handler(found.input, "onFocus")?.(event({ text: value }, target));
|
|
1133
|
+
if (options.type && value) {
|
|
1134
|
+
const keys = keysOf(value);
|
|
1135
|
+
let typed = "";
|
|
1136
|
+
for (const key of keys) {
|
|
1137
|
+
typed += key;
|
|
1138
|
+
handler(current.input, "onKeyPress")?.(event({ key: key === "\n" ? "Enter" : key }));
|
|
1139
|
+
change(typed);
|
|
1140
|
+
if (options.between) await options.between();
|
|
1141
|
+
if (options.again) current = options.again(current);
|
|
1142
|
+
}
|
|
1143
|
+
} else change(value);
|
|
1144
|
+
const input = current.input;
|
|
1145
|
+
if (options.submit) handler(input, "onSubmitEditing")?.(event({ text: value }, target));
|
|
1146
|
+
handler(input, "onEndEditing")?.(event({ text: value }, target));
|
|
963
1147
|
handler(input, "onBlur")?.(event({ text: value }, target));
|
|
964
|
-
return value;
|
|
1148
|
+
return { filled: value, native };
|
|
965
1149
|
}
|
|
966
1150
|
var center = (rect) => rect ? { x: rect.x + rect.width / 2, y: rect.y + rect.height / 2 } : { x: 0, y: 0 };
|
|
967
1151
|
var disabledProps = (fiber) => {
|
|
@@ -969,43 +1153,204 @@ var disabledProps = (fiber) => {
|
|
|
969
1153
|
const state = p?.accessibilityState;
|
|
970
1154
|
return p?.disabled === true || state?.disabled === true || p?.["aria-disabled"] === true;
|
|
971
1155
|
};
|
|
1156
|
+
function refuseDisabled(found, fiber, force) {
|
|
1157
|
+
if (!force && (found.element.disabled || disabledProps(fiber)))
|
|
1158
|
+
throw new Error(
|
|
1159
|
+
`${describe(found.element)} is disabled; pass {"force":true} as the last argument to act on it anyway`
|
|
1160
|
+
);
|
|
1161
|
+
}
|
|
972
1162
|
function pressTargetOf(found, action) {
|
|
973
1163
|
if (action === void 0) return pressFiberOf(found);
|
|
974
1164
|
const fiber = actionFiberOf(found, action);
|
|
975
|
-
return fiber && { fiber, via: "accessibilityAction" };
|
|
1165
|
+
return fiber && { fiber, via: "accessibilityAction", ancestor: fiber !== found.actions };
|
|
1166
|
+
}
|
|
1167
|
+
var isFn2 = (v) => typeof v === "function";
|
|
1168
|
+
var isToggle = (found) => !found.press && !!found.change && isFn2(propsOf(found.change)?.onValueChange) && isSwitchFiber(found.change);
|
|
1169
|
+
var HOLD = ["onPress", "onPressIn", "onLongPress"];
|
|
1170
|
+
function longPressFiberOf(found) {
|
|
1171
|
+
let handlesPress = false;
|
|
1172
|
+
for (let f = found.fiber; f; f = f.child) {
|
|
1173
|
+
const p = propsOf(f);
|
|
1174
|
+
if (isFn2(p?.onLongPress)) return { fiber: f, ancestor: false };
|
|
1175
|
+
if (p && HOLD.some((k) => isFn2(p[k]))) handlesPress = true;
|
|
1176
|
+
if (f === found.host) break;
|
|
1177
|
+
}
|
|
1178
|
+
if (handlesPress) return null;
|
|
1179
|
+
for (let f = found.fiber.return; f; f = f.return) {
|
|
1180
|
+
const p = propsOf(f);
|
|
1181
|
+
if (isFn2(p?.onLongPress)) return { fiber: f, ancestor: true };
|
|
1182
|
+
if (p && HOLD.some((k) => isFn2(p[k]))) return null;
|
|
1183
|
+
}
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
var TAP_GESTURES = /* @__PURE__ */ new Set(["TapGestureHandler", "LongPressGestureHandler"]);
|
|
1187
|
+
function isTapGesture(g) {
|
|
1188
|
+
if (!g || typeof g !== "object") return false;
|
|
1189
|
+
const x = g;
|
|
1190
|
+
if (typeof x.handlerName === "string" && TAP_GESTURES.has(x.handlerName)) return true;
|
|
1191
|
+
if (typeof x.type === "string" && TAP_GESTURES.has(x.type)) return true;
|
|
1192
|
+
return Array.isArray(x.gestures) && x.gestures.some(isTapGesture);
|
|
1193
|
+
}
|
|
1194
|
+
function tapDetectorBetween(found, until) {
|
|
1195
|
+
for (let f = found.fiber; f; f = f.child) {
|
|
1196
|
+
if (isTapGesture(propsOf(f)?.gesture)) return true;
|
|
1197
|
+
if (f === found.host) break;
|
|
1198
|
+
}
|
|
1199
|
+
for (let f = found.fiber.return; f && f !== until; f = f.return) {
|
|
1200
|
+
const p = propsOf(f);
|
|
1201
|
+
if (isTapGesture(p?.gesture)) return true;
|
|
1202
|
+
if (!until && p && HOLD.some((k) => isFn2(p[k]))) break;
|
|
1203
|
+
}
|
|
1204
|
+
return false;
|
|
976
1205
|
}
|
|
1206
|
+
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)";
|
|
1207
|
+
var gestureNote = (found) => tapDetectorBetween(found, null) ? `; ${GESTURE_NOTE}` : "";
|
|
977
1208
|
function pressElement(found, options = {}) {
|
|
978
1209
|
const { action } = options;
|
|
1210
|
+
if (options.long) return longPress(found, options);
|
|
1211
|
+
if (action === void 0 && isToggle(found)) {
|
|
1212
|
+
refuseDisabled(found, found.change, options.force);
|
|
1213
|
+
const now2 = propsOf(found.change)?.value;
|
|
1214
|
+
handler(found.change, "onValueChange")?.(now2 !== true);
|
|
1215
|
+
return "onValueChange";
|
|
1216
|
+
}
|
|
979
1217
|
const target = pressTargetOf(found, action);
|
|
980
1218
|
if (!target) {
|
|
981
1219
|
const names = found.element.actions ?? [];
|
|
982
|
-
const
|
|
1220
|
+
const has2 = names.length ? `; it has ${names.map((a) => JSON.stringify(a)).join(", ")}` : "";
|
|
983
1221
|
throw new Error(
|
|
984
|
-
action === void 0 ? `${describe(found.element)} has no onPress or "activate" accessibility action` : `${describe(found.element)} has no accessibility action ${JSON.stringify(action)}${
|
|
1222
|
+
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}`
|
|
985
1223
|
);
|
|
986
1224
|
}
|
|
987
|
-
const { fiber, via } = target;
|
|
988
|
-
if (
|
|
1225
|
+
const { fiber, via, ancestor } = target;
|
|
1226
|
+
if (ancestor && action === void 0 && tapDetectorBetween(found, fiber))
|
|
989
1227
|
throw new Error(
|
|
990
|
-
`${describe(found.element)}
|
|
1228
|
+
`${describe(found.element)} has no onPress of its own, and ${GESTURE_NOTE}; pressing the ancestor that has one would do something else`
|
|
991
1229
|
);
|
|
1230
|
+
const from = ancestor ? "ancestor:" : "";
|
|
1231
|
+
refuseDisabled(found, fiber, options.force);
|
|
992
1232
|
if (via === "accessibilityAction") {
|
|
993
1233
|
const actionName = action ?? "activate";
|
|
994
1234
|
handler(fiber, "onAccessibilityAction")?.(event({ actionName }));
|
|
995
|
-
return
|
|
1235
|
+
return `${from}accessibilityAction:${actionName}`;
|
|
996
1236
|
}
|
|
1237
|
+
const e = () => pressEvent(found);
|
|
1238
|
+
handler(fiber, "onPressIn")?.(e());
|
|
1239
|
+
handler(fiber, "onPress")?.(e());
|
|
1240
|
+
handler(fiber, "onPressOut")?.(e());
|
|
1241
|
+
return ancestor ? "ancestor" : "onPress";
|
|
1242
|
+
}
|
|
1243
|
+
var pressEvent = (found) => {
|
|
997
1244
|
const { x, y } = center(found.element.rect);
|
|
998
|
-
|
|
1245
|
+
return event({
|
|
999
1246
|
locationX: found.element.rect ? found.element.rect.width / 2 : 0,
|
|
1000
1247
|
locationY: found.element.rect ? found.element.rect.height / 2 : 0,
|
|
1001
1248
|
pageX: x,
|
|
1002
1249
|
pageY: y,
|
|
1003
1250
|
timestamp: Date.now()
|
|
1004
1251
|
});
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1252
|
+
};
|
|
1253
|
+
function longPress(found, options) {
|
|
1254
|
+
if (options.action !== void 0)
|
|
1255
|
+
throw new Error("{long:true} and {action} cannot be combined: long presses call onLongPress, actions send an accessibility action");
|
|
1256
|
+
const target = longPressFiberOf(found);
|
|
1257
|
+
if (!target)
|
|
1258
|
+
throw new Error(`${describe(found.element)} has no onLongPress${gestureNote(found)}`);
|
|
1259
|
+
refuseDisabled(found, target.fiber, options.force);
|
|
1260
|
+
handler(target.fiber, "onPressIn")?.(pressEvent(found));
|
|
1261
|
+
handler(target.fiber, "onLongPress")?.(pressEvent(found));
|
|
1262
|
+
handler(target.fiber, "onPressOut")?.(pressEvent(found));
|
|
1263
|
+
return target.ancestor ? "ancestor:onLongPress" : "onLongPress";
|
|
1264
|
+
}
|
|
1265
|
+
function itemsOfChildren(children, out = []) {
|
|
1266
|
+
if (Array.isArray(children)) for (const c of children) itemsOfChildren(c, out);
|
|
1267
|
+
else if (children && typeof children === "object") {
|
|
1268
|
+
const p = children.props;
|
|
1269
|
+
if (p && has(p.value) && typeof p.label === "string") out.push({ value: p.value, label: p.label });
|
|
1270
|
+
else if (p) itemsOfChildren(p.children, out);
|
|
1271
|
+
}
|
|
1272
|
+
return out;
|
|
1273
|
+
}
|
|
1274
|
+
function pickerItems(fiber) {
|
|
1275
|
+
const items = itemsOfChildren(propsOf(fiber)?.children);
|
|
1276
|
+
if (items.length) return items;
|
|
1277
|
+
const stack = fiber.child ? [fiber.child] : [];
|
|
1278
|
+
while (stack.length) {
|
|
1279
|
+
const f = stack.pop();
|
|
1280
|
+
if (f.sibling) stack.push(f.sibling);
|
|
1281
|
+
if (f.child) stack.push(f.child);
|
|
1282
|
+
const p = propsOf(f);
|
|
1283
|
+
if (p && has(p.value) && typeof p.label === "string") items.push({ value: p.value, label: p.label });
|
|
1284
|
+
}
|
|
1285
|
+
return items;
|
|
1286
|
+
}
|
|
1287
|
+
function isSlider(fiber) {
|
|
1288
|
+
const p = propsOf(fiber);
|
|
1289
|
+
if (["onSlidingStart", "onSlidingComplete", "minimumValue", "maximumValue", "step"].some((k) => k in p))
|
|
1290
|
+
return true;
|
|
1291
|
+
const own = firstHost(fiber);
|
|
1292
|
+
return !!own && /Slider/.test(String(own.type));
|
|
1293
|
+
}
|
|
1294
|
+
function valueOf(found) {
|
|
1295
|
+
const p = found.change && propsOf(found.change);
|
|
1296
|
+
if (!p) return void 0;
|
|
1297
|
+
if (has(p.value)) return p.value;
|
|
1298
|
+
if (has(p.selectedValue)) return p.selectedValue;
|
|
1299
|
+
if (Array.isArray(p.values) && typeof p.selectedIndex === "number") return p.values[p.selectedIndex];
|
|
1300
|
+
return void 0;
|
|
1301
|
+
}
|
|
1302
|
+
var list = (xs) => xs.map((x) => JSON.stringify(x)).join(", ");
|
|
1303
|
+
function setControlValue(found, value, options = {}) {
|
|
1304
|
+
const fiber = found.change;
|
|
1305
|
+
const what = describe(found.element);
|
|
1306
|
+
if (!fiber)
|
|
1307
|
+
throw new Error(`${what} has no onValueChange (a Switch, Slider, Picker or SegmentedControl has one)`);
|
|
1308
|
+
refuseDisabled(found, fiber, options.force);
|
|
1309
|
+
const p = propsOf(fiber);
|
|
1310
|
+
const call = (name, ...args) => handler(fiber, name)?.(...args);
|
|
1311
|
+
if (isSwitchFiber(fiber)) {
|
|
1312
|
+
if (typeof value !== "boolean") throw new Error(`${what} is a switch: pass true or false; got ${JSON.stringify(value)}`);
|
|
1313
|
+
return void call("onValueChange", value);
|
|
1314
|
+
}
|
|
1315
|
+
if (Array.isArray(p.values)) {
|
|
1316
|
+
if (!p.values.includes(value))
|
|
1317
|
+
throw new Error(`${what} takes one of ${list(p.values)}; got ${JSON.stringify(value)}`);
|
|
1318
|
+
const selectedSegmentIndex = p.values.indexOf(value);
|
|
1319
|
+
call("onChange", event({ value, selectedSegmentIndex }));
|
|
1320
|
+
return void call("onValueChange", value);
|
|
1321
|
+
}
|
|
1322
|
+
const items = pickerItems(fiber);
|
|
1323
|
+
if (items.length || "selectedValue" in p) {
|
|
1324
|
+
const index = items.findIndex((i) => i.value === value);
|
|
1325
|
+
if (!items.length) throw new Error(`${what} is a picker but no Picker.Item children could be read`);
|
|
1326
|
+
if (index < 0)
|
|
1327
|
+
throw new Error(`${what} takes one of ${list(items.map((i) => i.value))}; got ${JSON.stringify(value)}`);
|
|
1328
|
+
return void call("onValueChange", value, index);
|
|
1329
|
+
}
|
|
1330
|
+
const current = valueOf(found);
|
|
1331
|
+
if (isSlider(fiber) || typeof current === "number") {
|
|
1332
|
+
if (typeof value !== "number" || !Number.isFinite(value))
|
|
1333
|
+
throw new Error(`${what} takes a number; got ${JSON.stringify(value)}`);
|
|
1334
|
+
const slider = isSlider(fiber);
|
|
1335
|
+
const min = typeof p.minimumValue === "number" ? p.minimumValue : slider ? 0 : void 0;
|
|
1336
|
+
const max = typeof p.maximumValue === "number" ? p.maximumValue : slider ? 1 : void 0;
|
|
1337
|
+
if (min !== void 0 && value < min || max !== void 0 && value > max)
|
|
1338
|
+
throw new Error(`${what} takes a number between ${min ?? "-\u221E"} and ${max ?? "\u221E"}; got ${value}`);
|
|
1339
|
+
const step = p.step;
|
|
1340
|
+
if (typeof step === "number" && step > 0) {
|
|
1341
|
+
const n = (value - (min ?? 0)) / step;
|
|
1342
|
+
if (Math.abs(n - Math.round(n)) > 1e-6) {
|
|
1343
|
+
const lo = (min ?? 0) + Math.floor(n) * step;
|
|
1344
|
+
throw new Error(`${what} moves in steps of ${step}; got ${value} (nearest: ${lo}, ${lo + step})`);
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
call("onSlidingStart", value);
|
|
1348
|
+
call("onValueChange", value);
|
|
1349
|
+
return void call("onSlidingComplete", value);
|
|
1350
|
+
}
|
|
1351
|
+
if (typeof current === "string" && typeof value !== "string")
|
|
1352
|
+
throw new Error(`${what} takes a string; got ${JSON.stringify(value)}`);
|
|
1353
|
+
call("onValueChange", value);
|
|
1009
1354
|
}
|
|
1010
1355
|
|
|
1011
1356
|
// src/runtime/screen/find-text.ts
|
|
@@ -1237,6 +1582,63 @@ function mainScrollable(scrollables, window, need) {
|
|
|
1237
1582
|
);
|
|
1238
1583
|
return best.s;
|
|
1239
1584
|
}
|
|
1585
|
+
var isVirtualized = (s) => s.fibers.some((f) => typeof propsOf(f)?.renderItem === "function");
|
|
1586
|
+
function pagingList(scrollables, window) {
|
|
1587
|
+
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);
|
|
1588
|
+
const list2 = all.find((s) => !isHorizontal(s)) ?? all[0] ?? null;
|
|
1589
|
+
return { list: list2, skipped: all.filter((s) => s !== list2) };
|
|
1590
|
+
}
|
|
1591
|
+
async function awaitRows(read, has2, maxMs = 250) {
|
|
1592
|
+
const sig = () => {
|
|
1593
|
+
const { metrics, rows } = read();
|
|
1594
|
+
return { metrics, rows, key: `${rows}|${metrics?.max}|${metrics?.offset}` };
|
|
1595
|
+
};
|
|
1596
|
+
let cur = sig();
|
|
1597
|
+
let still = 0;
|
|
1598
|
+
for (let waited = 0; waited < maxMs; waited += 16) {
|
|
1599
|
+
if (has2()) break;
|
|
1600
|
+
await wait(16);
|
|
1601
|
+
const next = sig();
|
|
1602
|
+
still = next.key === cur.key ? still + 1 : 0;
|
|
1603
|
+
cur = next;
|
|
1604
|
+
if (cur.rows > 0 && still >= 4) break;
|
|
1605
|
+
}
|
|
1606
|
+
return cur.metrics;
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
// src/runtime/screen/snapshot-text.ts
|
|
1610
|
+
var ELEMENT_KINDS = ["button", "input", "text", "view", "webview"];
|
|
1611
|
+
var center2 = (r) => ({ x: r.x + r.width / 2, y: r.y + r.height / 2 });
|
|
1612
|
+
function underScope(found, scope) {
|
|
1613
|
+
for (let f = found.fiber; f; f = f.return) if (f === scope.fiber) return true;
|
|
1614
|
+
return false;
|
|
1615
|
+
}
|
|
1616
|
+
function snapshotText(lines, hidden, empty = "(nothing on screen)") {
|
|
1617
|
+
const base = lines.map(({ element: e }) => {
|
|
1618
|
+
const bits = [describe(e, 80, true)];
|
|
1619
|
+
if (e.editable === false) bits.push("readonly");
|
|
1620
|
+
if (e.actions?.length) bits.push(`actions=${e.actions.join(",")}`);
|
|
1621
|
+
if (e.onScreen === false) bits.push("offscreen");
|
|
1622
|
+
if (e.hidden) bits.push("hidden");
|
|
1623
|
+
return bits.join(" ");
|
|
1624
|
+
});
|
|
1625
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1626
|
+
for (const l of base) seen.set(l, (seen.get(l) ?? 0) + 1);
|
|
1627
|
+
const out = base.map((l, i) => {
|
|
1628
|
+
const { element: e, index } = lines[i];
|
|
1629
|
+
const repeats = (seen.get(l) ?? 0) > 1;
|
|
1630
|
+
const named = !!(e.testID || e.text || e.label || e.placeholder);
|
|
1631
|
+
const bits = [l];
|
|
1632
|
+
if (repeats && index !== void 0) bits.push(`index=${index}`);
|
|
1633
|
+
if (e.rect && (repeats && index === void 0 || !named)) {
|
|
1634
|
+
const c = center2(e.rect);
|
|
1635
|
+
bits.push(`@${Math.round(c.x)},${Math.round(c.y)}`);
|
|
1636
|
+
}
|
|
1637
|
+
return bits.join(" ");
|
|
1638
|
+
});
|
|
1639
|
+
if (hidden) out.push(`(${hidden} hidden: on screens not in front; {all:true} lists them)`);
|
|
1640
|
+
return lines.length || hidden ? out.join("\n") : empty;
|
|
1641
|
+
}
|
|
1240
1642
|
|
|
1241
1643
|
// src/runtime/screen/settle.ts
|
|
1242
1644
|
var STATE = /* @__PURE__ */ Symbol.for("@avasapp/agent-bridge/commits");
|
|
@@ -1309,6 +1711,7 @@ async function settle(options = {}) {
|
|
|
1309
1711
|
|
|
1310
1712
|
// src/runtime/screen/wait-for.ts
|
|
1311
1713
|
var POLL_MS = 50;
|
|
1714
|
+
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}";
|
|
1312
1715
|
function nearMissNote(found, target) {
|
|
1313
1716
|
const text = typeof target === "string" ? target : target.text;
|
|
1314
1717
|
const near = text ? nearMisses(found, text) : [];
|
|
@@ -1359,7 +1762,7 @@ function waitForTarget(collect, target, options = {}, reveal) {
|
|
|
1359
1762
|
const what = options.gone ? "to disappear" : "to appear";
|
|
1360
1763
|
reject(
|
|
1361
1764
|
new Error(
|
|
1362
|
-
`Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}${nearMissNote(found, target)}`
|
|
1765
|
+
`Timed out after ${timeoutMs} ms waiting for ${showTarget(target)} ${what}. On screen: ${onScreenSummary(found)}${nearMissNote(found, target)}${options.scroll && !options.gone ? `. ${NO_PAGING}` : ""}`
|
|
1363
1766
|
)
|
|
1364
1767
|
);
|
|
1365
1768
|
}
|
|
@@ -1379,8 +1782,25 @@ function waitForTarget(collect, target, options = {}, reveal) {
|
|
|
1379
1782
|
}
|
|
1380
1783
|
|
|
1381
1784
|
// src/runtime/screen/index.ts
|
|
1785
|
+
function snapshotKinds(kinds) {
|
|
1786
|
+
if (kinds === void 0) return [];
|
|
1787
|
+
const list2 = typeof kinds === "string" ? [kinds] : kinds;
|
|
1788
|
+
if (!Array.isArray(list2))
|
|
1789
|
+
throw new Error(`screen.snapshot kinds is a list like ["button","input"], not ${JSON.stringify(kinds)}`);
|
|
1790
|
+
for (const k of list2)
|
|
1791
|
+
if (!ELEMENT_KINDS.includes(k))
|
|
1792
|
+
throw new Error(
|
|
1793
|
+
`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")`
|
|
1794
|
+
);
|
|
1795
|
+
return list2;
|
|
1796
|
+
}
|
|
1382
1797
|
function createScreen(env) {
|
|
1383
|
-
const
|
|
1798
|
+
const collectLoose = () => collectScreen(env.roots(), env.window());
|
|
1799
|
+
const collectAll = () => {
|
|
1800
|
+
const all = collectLoose();
|
|
1801
|
+
if (all.unmeasured) throw new Error(UNMEASURED_ERROR);
|
|
1802
|
+
return all;
|
|
1803
|
+
};
|
|
1384
1804
|
const collect = () => collectAll().found;
|
|
1385
1805
|
const collectSettled = async () => {
|
|
1386
1806
|
let prev = null;
|
|
@@ -1435,39 +1855,258 @@ function createScreen(env) {
|
|
|
1435
1855
|
);
|
|
1436
1856
|
return shown;
|
|
1437
1857
|
};
|
|
1858
|
+
const PAGE_LIMIT = 40;
|
|
1859
|
+
const PAGE_MS = 1e4;
|
|
1860
|
+
const pagingBudget = (scroll) => {
|
|
1861
|
+
if (scroll === null || typeof scroll !== "object") return PAGE_MS;
|
|
1862
|
+
const bad = Object.keys(scroll).filter((k) => k !== "timeoutMs");
|
|
1863
|
+
if (bad.length)
|
|
1864
|
+
throw new Error(`Unknown scroll option ${bad.map((k) => JSON.stringify(k)).join(", ")}. Valid: timeoutMs`);
|
|
1865
|
+
const ms = scroll.timeoutMs;
|
|
1866
|
+
if (ms === void 0) return PAGE_MS;
|
|
1867
|
+
if (typeof ms !== "number" || !Number.isFinite(ms) || ms <= 0)
|
|
1868
|
+
throw new Error(`scroll.timeoutMs is a positive number of milliseconds; got ${JSON.stringify(ms)}`);
|
|
1869
|
+
return ms;
|
|
1870
|
+
};
|
|
1871
|
+
const scrollableAgain = (host) => {
|
|
1872
|
+
const node = host.stateNode;
|
|
1873
|
+
return collectAll().scrollables.find(
|
|
1874
|
+
(s) => s.host === host || node != null && s.host.stateNode === node
|
|
1875
|
+
);
|
|
1876
|
+
};
|
|
1877
|
+
const rendered = (target, index) => () => matchTarget(collect().filter((f) => !f.hidden), target).length > (indexOf(target) ?? index ?? 0);
|
|
1878
|
+
const nameOf = (host) => {
|
|
1879
|
+
const el = collectAll().found.find((f) => f.host === host || f.host.stateNode === host.stateNode);
|
|
1880
|
+
return el ? describe(el.element) : "the list";
|
|
1881
|
+
};
|
|
1882
|
+
const pageList = async (first, skipped, has2, budgetMs) => {
|
|
1883
|
+
const { host } = first;
|
|
1884
|
+
const name = nameOf(host);
|
|
1885
|
+
const first0 = metricsOf(first);
|
|
1886
|
+
if (!first0) return { found: false, note: `${name} has no readable scroll position` };
|
|
1887
|
+
const start = first0;
|
|
1888
|
+
const size = Math.max(1, ((isHorizontal(first) ? start.view?.width : start.view?.height) ?? 400) * 0.9);
|
|
1889
|
+
const deadline = performance.now() + budgetMs;
|
|
1890
|
+
let pages = 0;
|
|
1891
|
+
let lowest = start.offset;
|
|
1892
|
+
let highest = start.offset;
|
|
1893
|
+
let max = start.max;
|
|
1894
|
+
let stop = "both ends";
|
|
1895
|
+
const horizontal = isHorizontal(first);
|
|
1896
|
+
const node = host.stateNode;
|
|
1897
|
+
let span = null;
|
|
1898
|
+
const readRows = () => {
|
|
1899
|
+
const now2 = scrollableAgain(host);
|
|
1900
|
+
const metrics = now2 ? metricsOf(now2) : null;
|
|
1901
|
+
let rows = 0;
|
|
1902
|
+
let top = Infinity;
|
|
1903
|
+
let bottom = -Infinity;
|
|
1904
|
+
for (const f of collectAll().found) {
|
|
1905
|
+
let inList = false;
|
|
1906
|
+
for (let x = f.scroller; x; x = x.parent)
|
|
1907
|
+
if (x.host === host || node != null && x.host.stateNode === node) inList = true;
|
|
1908
|
+
if (!inList) continue;
|
|
1909
|
+
if (f.onScreen) rows++;
|
|
1910
|
+
const box = metrics?.view && !metrics.inverted ? measureHost(f.host) : null;
|
|
1911
|
+
if (!metrics?.view || !box) continue;
|
|
1912
|
+
const at = (horizontal ? box.x - metrics.view.x : box.y - metrics.view.y) + metrics.offset;
|
|
1913
|
+
top = Math.min(top, at);
|
|
1914
|
+
bottom = Math.max(bottom, at + (horizontal ? box.width : box.height));
|
|
1915
|
+
}
|
|
1916
|
+
span = top <= bottom ? { top, bottom } : null;
|
|
1917
|
+
return { metrics, rows };
|
|
1918
|
+
};
|
|
1919
|
+
const go = async (offset) => {
|
|
1920
|
+
const now2 = scrollableAgain(host);
|
|
1921
|
+
if (!now2) return false;
|
|
1922
|
+
const before = metricsOf(now2);
|
|
1923
|
+
scrollToOffset(now2, offset);
|
|
1924
|
+
await awaitScroll(now2, before?.offset, offset);
|
|
1925
|
+
pages++;
|
|
1926
|
+
if (has2()) return true;
|
|
1927
|
+
const m = await awaitRows(readRows, has2);
|
|
1928
|
+
if (m) {
|
|
1929
|
+
lowest = Math.min(lowest, m.offset);
|
|
1930
|
+
highest = Math.max(highest, m.offset);
|
|
1931
|
+
max = m.max;
|
|
1932
|
+
}
|
|
1933
|
+
return has2();
|
|
1934
|
+
};
|
|
1935
|
+
const sweep = async (dir, until, limit) => {
|
|
1936
|
+
const from = pages;
|
|
1937
|
+
while (true) {
|
|
1938
|
+
if (pages - from >= limit || performance.now() >= until) {
|
|
1939
|
+
stop = pages >= PAGE_LIMIT ? `the ${PAGE_LIMIT}-page limit` : `the ${budgetMs} ms budget`;
|
|
1940
|
+
return false;
|
|
1941
|
+
}
|
|
1942
|
+
const m = readRows().metrics;
|
|
1943
|
+
if (!m) return false;
|
|
1944
|
+
const viewSize = size / 0.9;
|
|
1945
|
+
const reach = span;
|
|
1946
|
+
const want = dir === 1 ? Math.max(m.offset + size, reach ? reach.bottom : -Infinity) : Math.min(m.offset - size, reach ? reach.top - viewSize : Infinity);
|
|
1947
|
+
const next = dir === 1 ? Math.min(m.max ?? Infinity, want) : Math.max(0, want);
|
|
1948
|
+
if (Math.abs(next - m.offset) < 1) {
|
|
1949
|
+
const grown = await awaitRows(readRows, has2, 400);
|
|
1950
|
+
if (has2()) return true;
|
|
1951
|
+
if (dir === 1 && grown && (grown.max ?? 0) - m.offset >= 1) continue;
|
|
1952
|
+
return false;
|
|
1953
|
+
}
|
|
1954
|
+
if (await go(next)) return true;
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
let found = false;
|
|
1958
|
+
try {
|
|
1959
|
+
const downFirst = start.max == null || start.max - start.offset <= start.offset;
|
|
1960
|
+
const order = downFirst ? [1, -1] : [-1, 1];
|
|
1961
|
+
const now2 = performance.now();
|
|
1962
|
+
for (const [i, dir] of order.entries()) {
|
|
1963
|
+
const otherRoom = dir === -1 ? start.max == null || start.max - start.offset >= 1 : start.offset >= 1;
|
|
1964
|
+
const half = i === 0 && otherRoom;
|
|
1965
|
+
const share = half ? now2 + budgetMs / 2 : deadline;
|
|
1966
|
+
const limit = half ? PAGE_LIMIT / 2 : PAGE_LIMIT - pages;
|
|
1967
|
+
if (dir === -1 && start.offset < 1) continue;
|
|
1968
|
+
if (i === 1 && pages) await go(start.offset);
|
|
1969
|
+
if (await sweep(dir, share, limit)) return found = true, done();
|
|
1970
|
+
}
|
|
1971
|
+
} finally {
|
|
1972
|
+
if (!found) {
|
|
1973
|
+
const now2 = scrollableAgain(host);
|
|
1974
|
+
if (now2) {
|
|
1975
|
+
await scrolled(now2, () => (scrollToOffset(now2, start.offset), start.offset));
|
|
1976
|
+
await settle();
|
|
1977
|
+
}
|
|
1978
|
+
}
|
|
1979
|
+
}
|
|
1980
|
+
return done();
|
|
1981
|
+
function done() {
|
|
1982
|
+
const where = `up to offset ${Math.round(lowest)} and down to offset ${Math.round(highest)} of ${max == null ? "?" : Math.round(max)}`;
|
|
1983
|
+
const left = skipped.length ? `. Not paged: ${skipped.map((s) => nameOf(s.host)).join(", ")}` : "";
|
|
1984
|
+
return {
|
|
1985
|
+
found,
|
|
1986
|
+
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}`
|
|
1987
|
+
};
|
|
1988
|
+
}
|
|
1989
|
+
};
|
|
1990
|
+
const pageFor = async (has2, budgetMs) => {
|
|
1991
|
+
const { list: list2, skipped } = pagingList(collectAll().scrollables, env.window());
|
|
1992
|
+
if (!list2)
|
|
1993
|
+
return { found: false, note: "{scroll:true} found no FlatList, SectionList or FlashList on screen to page" };
|
|
1994
|
+
return pageList(list2, skipped, has2, budgetMs);
|
|
1995
|
+
};
|
|
1438
1996
|
const resolve = async (target, prefer, options) => {
|
|
1439
|
-
const
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1997
|
+
const pick = () => resolveTarget(collect(), target, prefer, { ...options, offscreen: !!options.scroll });
|
|
1998
|
+
let note = "";
|
|
1999
|
+
if (options.scroll && typeof target === "object" && target.at !== void 0)
|
|
2000
|
+
throw new Error(
|
|
2001
|
+
"{at:[x,y]} is a point on the screen and can't be scrolled to; drop {scroll:true} or target by text or testID"
|
|
2002
|
+
);
|
|
2003
|
+
const has2 = rendered(target, options.index);
|
|
2004
|
+
if (options.scroll && !has2()) {
|
|
2005
|
+
const paged = await pageFor(has2, pagingBudget(options.scroll));
|
|
2006
|
+
note = paged.note;
|
|
2007
|
+
if (!paged.found) {
|
|
2008
|
+
try {
|
|
2009
|
+
return await settleOn(pick());
|
|
2010
|
+
} catch (error) {
|
|
2011
|
+
throw note ? new Error(`${error.message}. ${note}`) : error;
|
|
2012
|
+
}
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
2015
|
+
return settleOn(pick());
|
|
2016
|
+
};
|
|
2017
|
+
const settleOn = async (found) => found.onScreen ? found : reveal(found);
|
|
2018
|
+
const build = (options) => {
|
|
2019
|
+
const { all, format, within: within2 } = options;
|
|
2020
|
+
if (format !== void 0 && format !== "json" && format !== "text")
|
|
2021
|
+
throw new Error(`screen.snapshot format is "json" or "text", not ${JSON.stringify(format)}`);
|
|
2022
|
+
const kinds = snapshotKinds(options.kinds);
|
|
2023
|
+
const found = all ? collectLoose().found : collect();
|
|
2024
|
+
let pool;
|
|
2025
|
+
let hidden;
|
|
2026
|
+
if (all) {
|
|
2027
|
+
pool = found;
|
|
2028
|
+
} else {
|
|
2029
|
+
const shown = focused(found);
|
|
2030
|
+
pool = shown.filter((f) => f.onScreen);
|
|
2031
|
+
hidden = found.length - shown.length || void 0;
|
|
2032
|
+
}
|
|
2033
|
+
let items = pool.map((f) => ({
|
|
2034
|
+
found: f,
|
|
2035
|
+
element: all ? { ...f.element, onScreen: f.onScreen, ...f.hidden && { hidden: true } } : f.element
|
|
2036
|
+
}));
|
|
2037
|
+
let scoped = "";
|
|
2038
|
+
if (within2 !== void 0) {
|
|
2039
|
+
const scope = scopeOf(found, within2, !!all);
|
|
2040
|
+
if (!scope.element.rect)
|
|
2041
|
+
throw new Error(`${describe(scope.element)} has no position, so it can't scope a snapshot`);
|
|
2042
|
+
scoped = describe(scope.element);
|
|
2043
|
+
items = items.filter((i) => i.found !== scope && underScope(i.found, scope));
|
|
2044
|
+
}
|
|
2045
|
+
if (kinds.length) items = items.filter((i) => kinds.includes(i.element.kind));
|
|
2046
|
+
const empty = kinds.length || scoped ? `(no ${kinds.length ? kinds.join("/") : ""} elements${scoped ? ` inside ${scoped}` : ""})`.replace("(no ", "(no ") : void 0;
|
|
2047
|
+
return {
|
|
2048
|
+
hidden,
|
|
2049
|
+
empty,
|
|
2050
|
+
items: items.map((i) => ({ element: i.element, index: matchIndex(pool, i.found) }))
|
|
2051
|
+
};
|
|
2052
|
+
};
|
|
2053
|
+
const matchIndex = (pool, f) => {
|
|
2054
|
+
const e = f.element;
|
|
2055
|
+
const name = e.testID ?? e.label ?? e.placeholder ?? e.text;
|
|
2056
|
+
if (!name) return void 0;
|
|
2057
|
+
const at = matchTarget(pool, name).indexOf(f);
|
|
2058
|
+
return at < 0 ? void 0 : at;
|
|
2059
|
+
};
|
|
2060
|
+
const scopeOf = (found, within2, all) => {
|
|
2061
|
+
const ranged = indexOf(within2) !== void 0;
|
|
2062
|
+
if (!ranged) {
|
|
2063
|
+
const matches = matchTarget(found.filter((f) => !f.hidden && (all || f.onScreen)), within2);
|
|
2064
|
+
if (matches.length > 1) {
|
|
2065
|
+
const first = matches[0].element;
|
|
2066
|
+
const fix = typeof within2 === "string" ? {
|
|
2067
|
+
[first.testID === within2 ? "testID" : first.label === within2 ? "label" : first.placeholder === within2 ? "placeholder" : "text"]: within2,
|
|
2068
|
+
index: 1
|
|
2069
|
+
} : { ...within2, index: 1 };
|
|
2070
|
+
throw new Error(
|
|
2071
|
+
`within ${showTarget(within2)} matches ${matches.length} elements; pick one with ${JSON.stringify({ within: fix })}`
|
|
2072
|
+
);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
return resolveTarget(found, within2, void 0, { offscreen: all });
|
|
1444
2076
|
};
|
|
1445
2077
|
return {
|
|
1446
2078
|
snapshot(options = {}) {
|
|
1447
|
-
const
|
|
1448
|
-
|
|
1449
|
-
return {
|
|
1450
|
-
elements: found.map((f) => ({
|
|
1451
|
-
...f.element,
|
|
1452
|
-
onScreen: f.onScreen,
|
|
1453
|
-
...f.hidden && { hidden: true }
|
|
1454
|
-
}))
|
|
1455
|
-
};
|
|
1456
|
-
const shown = focused(found);
|
|
1457
|
-
const elements = shown.filter((f) => f.onScreen).map((f) => f.element);
|
|
1458
|
-
const hidden = found.length - shown.length;
|
|
2079
|
+
const { items, hidden } = build(options);
|
|
2080
|
+
const elements = items.map((i) => i.element);
|
|
1459
2081
|
return hidden ? { elements, hidden } : { elements };
|
|
1460
2082
|
},
|
|
1461
|
-
|
|
2083
|
+
/** `snapshot` as one line per element. */
|
|
2084
|
+
snapshotText(options = {}) {
|
|
2085
|
+
const { items, hidden, empty } = build(options);
|
|
2086
|
+
return snapshotText(items, hidden, empty);
|
|
2087
|
+
},
|
|
2088
|
+
findText: (text, options) => findText(collectLoose().found, text, options),
|
|
1462
2089
|
async fill(target, text, options = {}) {
|
|
1463
2090
|
const found = await resolve(target, (f) => !!f.input, {
|
|
1464
2091
|
index: options.index,
|
|
1465
2092
|
afterText: true,
|
|
1466
2093
|
scroll: options.scroll
|
|
1467
2094
|
});
|
|
1468
|
-
const filled = fillInput(found, String(text),
|
|
2095
|
+
const { filled, native } = await fillInput(found, String(text), {
|
|
2096
|
+
...options,
|
|
2097
|
+
between: async () => void await settle(),
|
|
2098
|
+
again: reread
|
|
2099
|
+
});
|
|
1469
2100
|
await settle();
|
|
1470
|
-
|
|
2101
|
+
const element = reread(found).element;
|
|
2102
|
+
if (native === true) return { filled, element: { ...element, value: filled } };
|
|
2103
|
+
if (native === false)
|
|
2104
|
+
return {
|
|
2105
|
+
filled,
|
|
2106
|
+
element,
|
|
2107
|
+
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"
|
|
2108
|
+
};
|
|
2109
|
+
return { filled, element };
|
|
1471
2110
|
},
|
|
1472
2111
|
async press(target, options = {}) {
|
|
1473
2112
|
const { action } = options;
|
|
@@ -1475,13 +2114,30 @@ function createScreen(env) {
|
|
|
1475
2114
|
throw new Error(`"action" must be an accessibility action name; got ${JSON.stringify(action)}`);
|
|
1476
2115
|
const found = await resolve(
|
|
1477
2116
|
target,
|
|
1478
|
-
(f) => action === void 0 ? !!f.press || f.element.kind === "button" : !!f.element.actions?.includes(action),
|
|
2117
|
+
(f) => options.long ? !!longPressFiberOf(f) : action === void 0 ? !!f.press || f.element.kind === "button" || isToggle(f) : !!f.element.actions?.includes(action),
|
|
1479
2118
|
options
|
|
1480
2119
|
);
|
|
1481
2120
|
const via = pressElement(found, options);
|
|
1482
2121
|
await settle();
|
|
1483
2122
|
return via === "onPress" ? found.element : { ...found.element, via };
|
|
1484
2123
|
},
|
|
2124
|
+
/** Sets a Switch, Slider, Picker or SegmentedControl through its onValueChange, and reads the value back. */
|
|
2125
|
+
async setValue(target, value, options = {}) {
|
|
2126
|
+
const found = await resolve(target, (f) => !!f.change, options);
|
|
2127
|
+
const before = valueOf(found);
|
|
2128
|
+
setControlValue(found, value, options);
|
|
2129
|
+
await settle();
|
|
2130
|
+
const again = reread(found);
|
|
2131
|
+
const now2 = valueOf(again);
|
|
2132
|
+
const result = { value: now2, element: again.element };
|
|
2133
|
+
const same = typeof now2 === "number" && typeof value === "number" ? Math.abs(now2 - value) < 1e-6 : now2 === value;
|
|
2134
|
+
if (before !== void 0 && !same && !(found.change && isSlider(found.change)))
|
|
2135
|
+
return {
|
|
2136
|
+
...result,
|
|
2137
|
+
warning: `the control still shows ${JSON.stringify(now2)}; the app did not apply ${JSON.stringify(value)}`
|
|
2138
|
+
};
|
|
2139
|
+
return result;
|
|
2140
|
+
},
|
|
1485
2141
|
/**
|
|
1486
2142
|
* Scrolls the nearest scrollable of a target (or `within`, or the main one
|
|
1487
2143
|
* on screen) to the target, to either end, or by a distance in points.
|
|
@@ -1566,7 +2222,6 @@ function createScreen(env) {
|
|
|
1566
2222
|
|
|
1567
2223
|
// src/runtime/tools/screen.ts
|
|
1568
2224
|
var devToolsHook = () => globalThis.__REACT_DEVTOOLS_GLOBAL_HOOK__;
|
|
1569
|
-
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.";
|
|
1570
2225
|
function screenTools() {
|
|
1571
2226
|
const screen = createScreen({
|
|
1572
2227
|
roots: () => fiberRoots(devToolsHook()),
|
|
@@ -1574,31 +2229,35 @@ function screenTools() {
|
|
|
1574
2229
|
});
|
|
1575
2230
|
return {
|
|
1576
2231
|
"screen.findText": {
|
|
1577
|
-
description: "
|
|
2232
|
+
description: TOOL_DESCRIPTIONS["screen.findText"],
|
|
1578
2233
|
run: (text, options) => screen.findText(text, options)
|
|
1579
2234
|
},
|
|
1580
2235
|
"screen.snapshot": {
|
|
1581
|
-
description:
|
|
1582
|
-
run: (options) => screen.snapshot(options)
|
|
2236
|
+
description: TOOL_DESCRIPTIONS["screen.snapshot"],
|
|
2237
|
+
run: (options) => options?.format === "text" ? screen.snapshotText(options) : screen.snapshot(options)
|
|
1583
2238
|
},
|
|
1584
2239
|
"screen.fill": {
|
|
1585
|
-
description:
|
|
2240
|
+
description: TOOL_DESCRIPTIONS["screen.fill"],
|
|
1586
2241
|
run: (target, text, options) => screen.fill(target, text, options)
|
|
1587
2242
|
},
|
|
1588
2243
|
"screen.press": {
|
|
1589
|
-
description:
|
|
2244
|
+
description: TOOL_DESCRIPTIONS["screen.press"],
|
|
1590
2245
|
run: (target, options) => screen.press(target, options)
|
|
1591
2246
|
},
|
|
2247
|
+
"screen.setValue": {
|
|
2248
|
+
description: TOOL_DESCRIPTIONS["screen.setValue"],
|
|
2249
|
+
run: (target, value, options) => screen.setValue(target, value, options)
|
|
2250
|
+
},
|
|
1592
2251
|
"screen.scroll": {
|
|
1593
|
-
description:
|
|
2252
|
+
description: TOOL_DESCRIPTIONS["screen.scroll"],
|
|
1594
2253
|
run: (arg, options) => screen.scroll(arg, options)
|
|
1595
2254
|
},
|
|
1596
2255
|
"screen.refresh": {
|
|
1597
|
-
description: "
|
|
2256
|
+
description: TOOL_DESCRIPTIONS["screen.refresh"],
|
|
1598
2257
|
run: (target, options) => screen.refresh(target, options)
|
|
1599
2258
|
},
|
|
1600
2259
|
"screen.waitFor": {
|
|
1601
|
-
description: "
|
|
2260
|
+
description: TOOL_DESCRIPTIONS["screen.waitFor"],
|
|
1602
2261
|
run: (target, options) => screen.waitFor(target, options)
|
|
1603
2262
|
}
|
|
1604
2263
|
};
|
|
@@ -1609,7 +2268,8 @@ function builtinTools(listTools, getTools, logs, getScenarios) {
|
|
|
1609
2268
|
return {
|
|
1610
2269
|
...bridgeTools(listTools),
|
|
1611
2270
|
...appTools(
|
|
1612
|
-
selectReload(globalThis, () => import_react_native2.DevSettings.reload())
|
|
2271
|
+
selectReload(globalThis, () => import_react_native2.DevSettings.reload()),
|
|
2272
|
+
getTools
|
|
1613
2273
|
),
|
|
1614
2274
|
...logTools(logs),
|
|
1615
2275
|
...restoreTools(getTools),
|
|
@@ -1858,32 +2518,54 @@ function toJson(value) {
|
|
|
1858
2518
|
}
|
|
1859
2519
|
|
|
1860
2520
|
// src/runtime/registry.ts
|
|
2521
|
+
var loadSlot = {};
|
|
2522
|
+
function ownsKey(slot, key) {
|
|
2523
|
+
if (typeof key !== "string" || key === "") return false;
|
|
2524
|
+
slot.key ?? (slot.key = key);
|
|
2525
|
+
return slot.key === key;
|
|
2526
|
+
}
|
|
1861
2527
|
function unwrap(definition) {
|
|
1862
2528
|
return typeof definition === "function" ? { run: definition } : definition;
|
|
1863
2529
|
}
|
|
1864
|
-
function createRegistry(getTools, logs, loadId2) {
|
|
1865
|
-
const
|
|
2530
|
+
function createRegistry(getTools, logs, loadId2, keySlot = loadSlot) {
|
|
2531
|
+
const list2 = () => Object.entries(getTools()).filter(([name]) => !INTERNAL_TOOLS.includes(name)).map(([name, definition]) => ({
|
|
1866
2532
|
name,
|
|
1867
2533
|
description: unwrap(definition).description
|
|
1868
2534
|
})).sort((a, b) => a.name.localeCompare(b.name));
|
|
1869
2535
|
async function dispatch(call, from) {
|
|
1870
|
-
const
|
|
1871
|
-
const
|
|
1872
|
-
const
|
|
1873
|
-
const
|
|
2536
|
+
const quiet = call.tool === "bridge.ping" || call.tool === "bridge.pending";
|
|
2537
|
+
const trusted = ownsKey(keySlot, call.journalKey);
|
|
2538
|
+
const end = quiet ? void 0 : logs?.begin(call.tool);
|
|
2539
|
+
const result = INTERNAL_TOOLS.includes(call.tool) && !trusted ? await refused(call, from) : await run(call, from).finally(end);
|
|
2540
|
+
const errors = quiet ? void 0 : logs?.takeErrors();
|
|
2541
|
+
const stamped = loadId2 ? { ...result, loadId: loadId2, ...pending(), ...journal(call, trusted) } : result;
|
|
1874
2542
|
return errors?.length ? { ...stamped, logs: errors } : stamped;
|
|
1875
2543
|
}
|
|
1876
2544
|
function pending() {
|
|
1877
2545
|
const areas = Object.keys(pendingByArea(getTools()));
|
|
1878
2546
|
return areas.length ? { pending: areas } : {};
|
|
1879
2547
|
}
|
|
2548
|
+
function journal(call, trusted) {
|
|
2549
|
+
const rev = journalRev(getTools());
|
|
2550
|
+
const out = rev ? { journal: rev } : {};
|
|
2551
|
+
if (trusted && call.journalAck !== void 0 && call.journalAck !== `${loadId2}:${rev}`)
|
|
2552
|
+
out.journalData = journalByArea(getTools());
|
|
2553
|
+
return out;
|
|
2554
|
+
}
|
|
2555
|
+
const refused = (call, from) => ({
|
|
2556
|
+
id: call.id,
|
|
2557
|
+
from,
|
|
2558
|
+
ok: false,
|
|
2559
|
+
error: `${call.tool} is for a session's own use`,
|
|
2560
|
+
ms: 0
|
|
2561
|
+
});
|
|
1880
2562
|
async function run(call, from) {
|
|
1881
2563
|
const t0 = performance.now();
|
|
1882
2564
|
const ms = () => Math.round((performance.now() - t0) * 100) / 100;
|
|
1883
2565
|
try {
|
|
1884
2566
|
const definition = getTools()[call.tool];
|
|
1885
2567
|
if (!definition) {
|
|
1886
|
-
const known =
|
|
2568
|
+
const known = list2().map((t) => t.name).join(", ");
|
|
1887
2569
|
throw new Error(`Unknown tool "${call.tool}". Known: ${known}`);
|
|
1888
2570
|
}
|
|
1889
2571
|
const { run: tool, maxArgs } = unwrap(definition);
|
|
@@ -1899,7 +2581,7 @@ function createRegistry(getTools, logs, loadId2) {
|
|
|
1899
2581
|
return { id: call.id, from, ok: false, error: message2, ms: ms() };
|
|
1900
2582
|
}
|
|
1901
2583
|
}
|
|
1902
|
-
return { list, dispatch };
|
|
2584
|
+
return { list: list2, dispatch };
|
|
1903
2585
|
}
|
|
1904
2586
|
|
|
1905
2587
|
// src/runtime/index.ts
|