react-native-tapline 1.5.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. package/TaplineNative.podspec +3 -1
  2. package/dist/actions.d.ts +22 -2
  3. package/dist/actions.js +186 -16
  4. package/dist/actions.js.map +1 -1
  5. package/dist/alerts.d.ts +2 -0
  6. package/dist/alerts.js +5 -0
  7. package/dist/alerts.js.map +1 -1
  8. package/dist/anim.d.ts +25 -0
  9. package/dist/anim.js +311 -0
  10. package/dist/anim.js.map +1 -0
  11. package/dist/appinfo.d.ts +12 -0
  12. package/dist/appinfo.js +78 -0
  13. package/dist/appinfo.js.map +1 -0
  14. package/dist/client.d.ts +2 -0
  15. package/dist/client.js +25 -2
  16. package/dist/client.js.map +1 -1
  17. package/dist/commands.js +28 -0
  18. package/dist/commands.js.map +1 -1
  19. package/dist/guard.d.ts +17 -0
  20. package/dist/guard.js +44 -0
  21. package/dist/guard.js.map +1 -0
  22. package/dist/idle.d.ts +43 -0
  23. package/dist/idle.js +379 -0
  24. package/dist/idle.js.map +1 -0
  25. package/dist/index.d.ts +6 -0
  26. package/dist/index.js +3 -0
  27. package/dist/index.js.map +1 -1
  28. package/dist/inspect.d.ts +11 -0
  29. package/dist/inspect.js +36 -7
  30. package/dist/inspect.js.map +1 -1
  31. package/dist/loading.d.ts +17 -0
  32. package/dist/loading.js +96 -0
  33. package/dist/loading.js.map +1 -0
  34. package/dist/locale.js +1 -1
  35. package/dist/locale.js.map +1 -1
  36. package/dist/methods.js +8 -0
  37. package/dist/methods.js.map +1 -1
  38. package/dist/move.d.ts +40 -0
  39. package/dist/move.js +125 -0
  40. package/dist/move.js.map +1 -0
  41. package/dist/native.d.ts +5 -0
  42. package/dist/native.js.map +1 -1
  43. package/dist/network.d.ts +4 -0
  44. package/dist/network.js +29 -0
  45. package/dist/network.js.map +1 -1
  46. package/dist/place.d.ts +23 -0
  47. package/dist/place.js +66 -0
  48. package/dist/place.js.map +1 -0
  49. package/dist/placekey.d.ts +29 -0
  50. package/dist/placekey.js +134 -0
  51. package/dist/placekey.js.map +1 -0
  52. package/dist/routetree.d.ts +27 -0
  53. package/dist/routetree.js +216 -0
  54. package/dist/routetree.js.map +1 -0
  55. package/dist/types.d.ts +23 -2
  56. package/dist/types.js +5 -1
  57. package/dist/types.js.map +1 -1
  58. package/dist/wait.d.ts +5 -0
  59. package/dist/wait.js +4 -0
  60. package/dist/wait.js.map +1 -1
  61. package/ios/TaplineNativeModule.swift +61 -0
  62. package/package.json +1 -1
@@ -5,7 +5,9 @@ Pod::Spec.new do |s|
5
5
  s.license = { :type => 'MIT' }
6
6
  s.author = 'Tapline'
7
7
  s.homepage = 'https://github.com/tapline/tapline'
8
- s.platforms = { :ios => '16.4' }
8
+ # 15.1 is the floor of Expo SDK 55 and React Native 0.83. CocoaPods skips a pod that needs a newer iOS than the
9
+ # app, silently, so a higher number here left SDK 55 apps without the module (found by the native CI job).
10
+ s.platforms = { :ios => '15.1' }
9
11
  s.source = { :git => '' }
10
12
  s.static_framework = true
11
13
  s.dependency 'ExpoModulesCore'
package/dist/actions.d.ts CHANGED
@@ -22,6 +22,8 @@ export interface SettleOutcome {
22
22
  export declare function settle(): Promise<number>;
23
23
  /** Like `settle`, and says when the wait ended at the cap: spread it into an action's result. */
24
24
  export declare function settleOutcome(): Promise<SettleOutcome>;
25
+ /** An action is starting: only requests begun from now on count as its own. */
26
+ export declare function beginAction(): void;
25
27
  type ParsedTarget = {
26
28
  kind: 'ref';
27
29
  ref: string;
@@ -60,8 +62,12 @@ export declare function labelOf(info: {
60
62
  }): string;
61
63
  /** What the caller means to do; a point target then picks only an element that does it. */
62
64
  type ActionHint = 'press' | 'longPress' | 'toggle' | 'scroll' | 'type';
63
- /** Resolve a protocol target to exactly one element, or throw not_found / ambiguous. */
64
- export declare function resolveTarget(raw: unknown, hint?: ActionHint, glassOnly?: boolean): Promise<Resolved>;
65
+ /**
66
+ * Resolve a protocol target to exactly one element, or throw not_found / ambiguous. A target that isn't there while the screen
67
+ * is loading (a request in flight, a spinner) is waited for up to the settle cap before `not_found` is answered (AGENT-TOOLS
68
+ * 1.13 §18.7); the answer after the wait is made from the screen as it is then, so its closest matches are unchanged.
69
+ */
70
+ export declare function resolveTarget(raw: unknown, hint?: ActionHint, glassOnly?: boolean, loadingWait?: boolean): Promise<Resolved>;
65
71
  /** The nearest listed element at or above a host fiber, for naming what covers a target. */
66
72
  export declare function describeHit(snapshot: Snapshot, hit: Fiber, actor?: Entry): string;
67
73
  /** `back`: what the back button or gesture does, on the focused screen; with a sheet open, closes the sheet. */
@@ -74,6 +80,7 @@ export declare function back(rawParams: unknown): Promise<{
74
80
  export type PressSecond = {
75
81
  done: true;
76
82
  afterMs: number;
83
+ threw?: string;
77
84
  } | {
78
85
  done: false;
79
86
  reason: string;
@@ -98,7 +105,20 @@ export type PressResult = {
98
105
  };
99
106
  /** With `again`: the second tap of a double tap. */
100
107
  second?: PressSecond;
108
+ /** Whose handler took the touch, and its message if it threw (AGENT-TOOLS 1.13 §3). Absent on a dry run. */
109
+ handler?: HandlerFacts;
110
+ /** Requests that began during the action. Absent on a dry run. */
111
+ startedRequests?: number;
101
112
  } & SettleOutcome;
113
+ /** An own handler (`threw`: its message), or an ancestor's (`element`: that ancestor) that took the touch. */
114
+ export type HandlerFacts = {
115
+ kind: 'own';
116
+ threw?: string;
117
+ } | {
118
+ kind: 'parent';
119
+ element: TaplineElement;
120
+ threw?: string;
121
+ };
102
122
  export declare function press(params: unknown): Promise<PressResult>;
103
123
  export declare function longPress(params: unknown): Promise<PressResult>;
104
124
  /** What the SDK asks of the keyboard and the focused input; replaceable so tests need no native side. */
package/dist/actions.js CHANGED
@@ -5,6 +5,7 @@ exports.configureActions = configureActions;
5
5
  exports.onActed = onActed;
6
6
  exports.settle = settle;
7
7
  exports.settleOutcome = settleOutcome;
8
+ exports.beginAction = beginAction;
8
9
  exports.parseTarget = parseTarget;
9
10
  exports.labelOf = labelOf;
10
11
  exports.resolveTarget = resolveTarget;
@@ -28,6 +29,9 @@ exports.screenshot = screenshot;
28
29
  exports.swipe = swipe;
29
30
  const react_native_1 = require("react-native");
30
31
  const keyboard_1 = require("./keyboard");
32
+ const guard_1 = require("./guard");
33
+ const network_1 = require("./network");
34
+ const loading_1 = require("./loading");
31
35
  const native_1 = require("./native");
32
36
  const inspect_1 = require("./inspect");
33
37
  const source_1 = require("./source");
@@ -39,6 +43,7 @@ const alerts_1 = require("./alerts");
39
43
  const fiber_1 = require("./fiber");
40
44
  const logs_1 = require("./logs");
41
45
  const register_1 = require("./register");
46
+ const idle_1 = require("./idle");
42
47
  const settle_1 = require("./settle");
43
48
  const types_1 = require("./types");
44
49
  var inspect_2 = require("./inspect");
@@ -130,14 +135,90 @@ async function sheetProbe() {
130
135
  }
131
136
  return null;
132
137
  }
138
+ /**
139
+ * Where `startedRequests()` stood when the action began (or when the last settle ended): a request begun after
140
+ * it is one the action started (F1).
141
+ */
142
+ let requestMark = 0;
143
+ /** An action is starting: only requests begun from now on count as its own. */
144
+ function beginAction() {
145
+ requestMark = (0, network_1.startedRequests)();
146
+ }
147
+ const FRAME_MS = 16;
148
+ /** Whether an open or closing sheet hides the app's motion from the SDK. */
149
+ function sheetPresent() {
150
+ return (0, inspect_1.openModalHosts)().length > 0 || (0, inspect_1.closingModalHosts)().length > 0;
151
+ }
152
+ /** The elements whose positions say the screen has stopped: the acting ones and the outermost. */
153
+ function watchedFibers() {
154
+ try {
155
+ const entries = (0, inspect_1.takeSnapshot)().entries;
156
+ const picked = new Map();
157
+ for (const entry of [...entries.filter((e) => e.info.actions.length > 0).slice(0, 6), ...entries.slice(0, 6)]) {
158
+ picked.set(entry.node.index, entry.node.fiber);
159
+ }
160
+ return [...picked.values()];
161
+ }
162
+ catch {
163
+ return [];
164
+ }
165
+ }
166
+ function sameFrame(a, b) {
167
+ if (a === null || b === null)
168
+ return a === b;
169
+ return Math.abs(a.x - b.x) <= 1 && Math.abs(a.y - b.y) <= 1 && Math.abs(a.width - b.width) <= 1 && Math.abs(a.height - b.height) <= 1;
170
+ }
171
+ /**
172
+ * A native push, a Lottie or a gesture-handler sheet moves without a commit or an Animated call, so after the
173
+ * idle hold the visible positions are read twice, a frame apart. Says which element moved, and whether React
174
+ * committed in between.
175
+ */
176
+ async function positionsMoved() {
177
+ const before = (0, register_1.commitRevision)();
178
+ const fibers = watchedFibers();
179
+ const read = () => Promise.all(fibers.map((fiber) => (0, inspect_1.measureFiber)(fiber).catch(() => null)));
180
+ const first = await read();
181
+ await new Promise((resolve) => setTimeout(resolve, FRAME_MS));
182
+ const second = await read();
183
+ const at = first.findIndex((frame, i) => !sameFrame(frame, second[i] ?? null));
184
+ return { moving: at >= 0 ? (fibers[at] ?? null) : null, committed: (0, register_1.commitRevision)() !== before };
185
+ }
133
186
  async function settleNow() {
134
187
  const started = Date.now();
135
188
  let capped = false;
136
189
  const onCap = () => {
137
190
  capped = true;
138
191
  };
192
+ // Know-don't-watch (off unless `knowIdle`): answer when every signal the SDK can see is idle and still.
193
+ // An open sheet is out of its sight, so that keeps today's window and position poll.
194
+ let knowSpentMs = 0;
195
+ if ((0, idle_1.isKnowIdle)() && settings.capMs > 0 && !sheetPresent()) {
196
+ const known = await (0, idle_1.waitUntilIdle)({ frames: 2, capMs: settings.capMs, requestMark });
197
+ let moving = null;
198
+ let steady = !known.usedFallback && !known.capped;
199
+ if (steady && !sheetPresent()) {
200
+ const check = await positionsMoved();
201
+ moving = check.moving;
202
+ steady = moving === null && !check.committed;
203
+ }
204
+ if (!known.usedFallback && !sheetPresent() && (steady || known.capped)) {
205
+ requestMark = (0, network_1.startedRequests)();
206
+ const settledMs = Math.max(0, Math.min(settings.capMs, Date.now() - started));
207
+ return known.capped ? { settledMs, settleCapMs: settings.capMs } : { settledMs };
208
+ }
209
+ if (known.capped)
210
+ onCap();
211
+ knowSpentMs = Math.max(0, Date.now() - started);
212
+ // Something moved without a commit: today's poll waits for it to stop.
213
+ const left = settings.capMs - knowSpentMs;
214
+ if (moving && left > 0) {
215
+ await (0, settle_1.waitUntilSteady)(() => (0, inspect_1.measureFiber)(moving), { pollMs: SHEET_POLL_MS, capMs: left, now: Date.now, onCap });
216
+ knowSpentMs = Math.max(0, Date.now() - started);
217
+ }
218
+ }
139
219
  await (0, settle_1.waitUntilStill)({
140
220
  ...settings,
221
+ capMs: settings.capMs - knowSpentMs,
141
222
  onCommit: (listener) => (0, register_1.onCommit)(() => listener()),
142
223
  now: Date.now,
143
224
  onCap,
@@ -172,6 +253,7 @@ async function settleNow() {
172
253
  }
173
254
  }
174
255
  const settledMs = Math.max(0, Math.min(settings.capMs, Date.now() - started));
256
+ requestMark = (0, network_1.startedRequests)();
175
257
  return capped && settings.capMs > 0 ? { settledMs, settleCapMs: settings.capMs } : { settledMs };
176
258
  }
177
259
  function badParams(message) {
@@ -207,6 +289,22 @@ function callHandler(name, handler, args) {
207
289
  });
208
290
  }
209
291
  }
292
+ /**
293
+ * Calls a press handler as `callHandler` does, but a throw is not an error of the action: a person's tap
294
+ * on a button whose handler throws still happened. The message goes to the logs and to the answer's facts
295
+ * (AGENT-TOOLS 1.13 §3). Returns the message, or undefined when the handler didn't throw.
296
+ */
297
+ function callHandlerCaught(name, handler, args) {
298
+ try {
299
+ callHandler(name, handler, args);
300
+ }
301
+ catch (error) {
302
+ const message = error instanceof types_1.TaplineError ? error.message.replace(/^[^:]*threw: /, '') : String(error);
303
+ console.error(`[tapline] ${name} threw: ${message}`);
304
+ return message.slice(0, 500);
305
+ }
306
+ return undefined;
307
+ }
210
308
  function parseTarget(raw) {
211
309
  if (!(0, types_1.isRecord)(raw))
212
310
  throw badParams('target must be an object such as {"testID":"login"}');
@@ -490,8 +588,38 @@ function inFrontLayer(matches) {
490
588
  const inAlert = matches.filter((entry) => inStandInAlert(entry.node.fiber));
491
589
  return inAlert.length > 0 ? inAlert : matches;
492
590
  }
493
- /** Resolve a protocol target to exactly one element, or throw not_found / ambiguous. */
494
- async function resolveTarget(raw, hint, glassOnly = false) {
591
+ /**
592
+ * Resolve a protocol target to exactly one element, or throw not_found / ambiguous. A target that isn't there while the screen
593
+ * is loading (a request in flight, a spinner) is waited for up to the settle cap before `not_found` is answered (AGENT-TOOLS
594
+ * 1.13 §18.7); the answer after the wait is made from the screen as it is then, so its closest matches are unchanged.
595
+ */
596
+ async function resolveTarget(raw, hint, glassOnly = false, loadingWait = true) {
597
+ try {
598
+ return await resolveNow(raw, hint, glassOnly);
599
+ }
600
+ catch (error) {
601
+ if (!(error instanceof types_1.TaplineError) || error.code !== 'not_found' || !loadingWait || !isNamedTarget(raw) || settings.capMs <= 0)
602
+ throw error;
603
+ await (0, loading_1.waitWhileLoading)({
604
+ requestMark,
605
+ capMs: settings.capMs,
606
+ // Anything but `not_found` (ambiguous, covered) means the target has come.
607
+ isThere: () => resolveNow(raw, hint, glassOnly).then(() => true, (again) => !(again instanceof types_1.TaplineError && again.code === 'not_found')),
608
+ });
609
+ return resolveNow(raw, hint, glassOnly);
610
+ }
611
+ }
612
+ /** A target by id, label or text: the kinds a screen can still be loading. A ref or a point is never waited for. */
613
+ function isNamedTarget(raw) {
614
+ try {
615
+ const kind = parseTarget(raw).kind;
616
+ return kind === 'testID' || kind === 'label' || kind === 'text';
617
+ }
618
+ catch {
619
+ return false;
620
+ }
621
+ }
622
+ async function resolveNow(raw, hint, glassOnly = false) {
495
623
  const target = parseTarget(raw);
496
624
  const snapshot = (0, inspect_1.takeSnapshot)();
497
625
  if (target.kind === 'ref') {
@@ -735,7 +863,8 @@ async function assertReachable(snapshot, actor, frame) {
735
863
  const name = describe(actor);
736
864
  if (right <= left || bottom <= top) {
737
865
  throw notActionable('off_screen', `${name} is off screen (at ${Math.round(frame.x)},${Math.round(frame.y)}; the screen is ${width}x${height}). ` +
738
- `Scroll to it first ${SCROLL_TO_TARGET_HINT}, or pass force: true to call its handler anyway.`);
866
+ 'It lies in a part of the screen that nothing scrolls (behind a fixed bar, or outside every list); look at the screen, then target something else or report it. ' +
867
+ 'Pass force: true to call its handler anyway.');
739
868
  }
740
869
  const bar = (0, inspect_1.nativeTabBar)(snapshot);
741
870
  const navBar = (0, inspect_1.nativeNavBar)(snapshot);
@@ -944,21 +1073,28 @@ function focusInput(entry, frame) {
944
1073
  callHandler(key, handler, [pressEvent(entry.node.fiber, frame)]);
945
1074
  }
946
1075
  }
1076
+ /** The guard a `move` hands to the touch it makes, never part of a plain call. */
1077
+ function recipientGuardOf(params) {
1078
+ return params.recipientGuard === undefined ? undefined : (0, guard_1.parseRecipientGuard)(params.recipientGuard);
1079
+ }
947
1080
  async function pressLike(kind, rawParams) {
948
1081
  const params = paramsOf(rawParams);
949
1082
  const afterMs = kind === 'press' ? againAfter(params.again) : undefined;
950
- const { snapshot, entry } = await resolveTarget(params.target, kind, params.noScroll === true);
1083
+ const guard = recipientGuardOf(params);
1084
+ const { snapshot, entry } = await resolveTarget(params.target, kind, params.noScroll === true, params.dryRun !== true);
951
1085
  if (entry.tab) {
952
1086
  if (kind === 'longPress')
953
1087
  throw notActionable('no_action', `${describe(entry)} has no long-press action`);
954
1088
  if (params.dryRun === true)
955
1089
  return { element: (0, inspect_1.toElement)(entry, null), dryRun: true, settledMs: 0 };
1090
+ (0, guard_1.assertRecipient)(guard, entry);
956
1091
  selectTab(snapshot, entry.tab);
957
1092
  return { element: (0, inspect_1.toElement)(entry, null), ...keyboardNote(closeKeyboardFor(entry)), ...(await settleOutcome()) };
958
1093
  }
959
1094
  if (entry.navBack) {
960
1095
  if (params.dryRun === true)
961
1096
  return { element: (0, inspect_1.toElement)(entry, null), dryRun: true, settledMs: 0 };
1097
+ (0, guard_1.assertRecipient)(guard, entry);
962
1098
  const { route: _route, ...outcome } = await back({});
963
1099
  return { element: (0, inspect_1.toElement)(entry, null), ...outcome };
964
1100
  }
@@ -983,6 +1119,8 @@ async function pressLike(kind, rawParams) {
983
1119
  return { actor: found, handler: own, focusing: isInput };
984
1120
  };
985
1121
  let planned = plan(snapshot, entry);
1122
+ // The element the agent named; a scroll may find it again.
1123
+ let touched = entry;
986
1124
  // Off screen, in the tree: scroll it into view first (AGENT-TOOLS 1.12 §16.2). The policy ran before this call.
987
1125
  let reach = {};
988
1126
  let current = snapshot;
@@ -993,11 +1131,12 @@ async function pressLike(kind, rawParams) {
993
1131
  if (reach.scrolled) {
994
1132
  const again = await reresolve(planned.actor, kind, params.target);
995
1133
  current = again.snapshot;
1134
+ touched = again.entry;
996
1135
  planned = plan(again.snapshot, again.entry);
997
1136
  await assertRefoundOnScreen(current, planned.actor, reach);
998
1137
  }
999
1138
  }
1000
- const { actor, handler, focusing } = planned;
1139
+ const { actor, handler } = planned;
1001
1140
  const { element, frame } = await elementFor(actor);
1002
1141
  if (reach.scrolls)
1003
1142
  return { element, dryRun: true, scrolls: true, ...(reach.point ? { point: reach.point } : {}), settledMs: 0 };
@@ -1005,25 +1144,40 @@ async function pressLike(kind, rawParams) {
1005
1144
  // A dry run (AGENT-TOOLS 1.8 §11.4): every check ran; nothing is touched.
1006
1145
  if (params.dryRun === true)
1007
1146
  return { element, dryRun: true, ...pointOf(frame), ...forced, settledMs: 0 };
1147
+ // The policy checked this very element: refuse when the one that takes the touch is another now (§18.4).
1148
+ (0, guard_1.assertRecipient)(guard, actor);
1008
1149
  const closedKeyboard = closeKeyboardFor(actor);
1150
+ const requestsBefore = (0, network_1.startedRequests)();
1151
+ let threw;
1152
+ // An input with no onPress of its own was only focused: no handler ran, so none is reported.
1153
+ let handled = handler !== null;
1009
1154
  if (handler)
1010
- callHandler(handlerName, handler, [pressEvent(actor.node.fiber, frame)]);
1155
+ threw = callHandlerCaught(handlerName, handler, [pressEvent(actor.node.fiber, frame)]);
1011
1156
  else {
1012
1157
  // A finger focuses the input and also fires the input's own onPress (not an ancestor's: the
1013
1158
  // input takes the touch).
1014
1159
  focusInput(actor, frame);
1015
1160
  const own = appHandler(actor, 'onPress');
1016
- if (own)
1017
- callHandler('onPress', own, [pressEvent(actor.node.fiber, frame)]);
1161
+ if (own) {
1162
+ handled = true;
1163
+ threw = callHandlerCaught('onPress', own, [pressEvent(actor.node.fiber, frame)]);
1164
+ }
1018
1165
  }
1166
+ // Whose handler it was: the touched element's own, or an ancestor's that took the touch (§3).
1167
+ const facts = !handled ? undefined : (0, fiber_1.sameFiber)(actor.node.fiber, touched.node.fiber)
1168
+ ? { kind: 'own', ...(threw !== undefined ? { threw } : {}) }
1169
+ : { kind: 'parent', element, ...(threw !== undefined ? { threw } : {}) };
1019
1170
  const second = afterMs === undefined ? undefined : await pressAgain(actor, params, afterMs);
1171
+ const settled = await settleOutcome();
1020
1172
  return {
1173
+ ...(facts !== undefined ? { handler: facts } : {}),
1174
+ startedRequests: Math.max(0, (0, network_1.startedRequests)() - requestsBefore),
1021
1175
  element,
1022
1176
  ...(reach.scrolled ? { scrolled: true } : {}),
1023
1177
  ...keyboardNote(closedKeyboard),
1024
1178
  ...forced,
1025
1179
  ...(second !== undefined ? { second } : {}),
1026
- ...(await settleOutcome()),
1180
+ ...settled,
1027
1181
  };
1028
1182
  }
1029
1183
  /** `again`: a second tap this many milliseconds after the first (a stress double tap, AGENT-TOOLS 1.12 §17.4). */
@@ -1060,8 +1214,9 @@ async function pressAgain(first, params, afterMs) {
1060
1214
  if (params.force !== true)
1061
1215
  await assertReachable(again.snapshot, found, frame);
1062
1216
  const at = Date.now() - started;
1063
- callHandler('onPress', handler, [pressEvent(found.node.fiber, frame)]);
1064
- return { done: true, afterMs: at };
1217
+ // A handler that throws on the second tap was reached: that is the app's failure, not a tap that didn't land.
1218
+ const threw = callHandlerCaught('onPress', handler, [pressEvent(found.node.fiber, frame)]);
1219
+ return threw === undefined ? { done: true, afterMs: at } : { done: true, afterMs: at, threw };
1065
1220
  }
1066
1221
  catch (error) {
1067
1222
  const reason = error instanceof types_1.TaplineError
@@ -1195,9 +1350,9 @@ async function moveFocusAboveKeyboard(entry) {
1195
1350
  function keyboardNote(closed) {
1196
1351
  return closed ? { closedKeyboard: true } : {};
1197
1352
  }
1198
- async function resolveInput(snapshot, rawTarget, glassOnly = false) {
1353
+ async function resolveInput(snapshot, rawTarget, glassOnly = false, loadingWait = true) {
1199
1354
  if (rawTarget !== undefined && rawTarget !== null) {
1200
- const { snapshot: snap, entry } = await resolveTarget(rawTarget, 'type', glassOnly);
1355
+ const { snapshot: snap, entry } = await resolveTarget(rawTarget, 'type', glassOnly, loadingWait);
1201
1356
  const input = findActionable(snap, entry, 'type', true);
1202
1357
  if (!input)
1203
1358
  throw notActionable('no_action', `${describe(entry)} is not an input`);
@@ -1230,6 +1385,12 @@ function setNativeText(entry, text) {
1230
1385
  }
1231
1386
  }
1232
1387
  }
1388
+ function assertFieldKind(entry, kind) {
1389
+ const isSecure = (0, inspect_1.isSecureInput)(entry.node);
1390
+ if (kind !== undefined && (kind === 'secure') !== isSecure) {
1391
+ throw new types_1.TaplineError('bad_params', `${describe(entry)} is ${isSecure ? 'a secure field' : 'not a secure field'}, not a ${kind} field`, undefined, undefined, { secure: isSecure });
1392
+ }
1393
+ }
1233
1394
  async function typeText(rawParams) {
1234
1395
  const params = paramsOf(rawParams);
1235
1396
  const submit = params.submit === true;
@@ -1244,7 +1405,13 @@ async function typeText(rawParams) {
1244
1405
  throw badParams('text is required (or pass submit: true)');
1245
1406
  // Deleting from the end edits what is there, so it never replaces the whole text.
1246
1407
  const replace = params.replace !== false && !deleting;
1247
- let { snapshot, entry } = await resolveInput(null, params.target, params.noScroll === true);
1408
+ const kind = params.kind;
1409
+ if (kind !== undefined && kind !== 'secure' && kind !== 'plain')
1410
+ throw badParams('kind is "secure" or "plain"');
1411
+ let { snapshot, entry } = await resolveInput(null, params.target, params.noScroll === true, params.dryRun !== true);
1412
+ // A test account's value goes only into the kind of field it is for (AGENT-TOOLS 1.13 §18.6). The refusal
1413
+ // comes before anything is typed or scrolled, and says only whether the field is secure, never its text.
1414
+ assertFieldKind(entry, kind);
1248
1415
  if (entry.info.disabled)
1249
1416
  throw notActionable('disabled', `${describe(entry)} is not editable; ${ENABLE_FIRST}.`);
1250
1417
  // Off screen, in the tree: scroll it into view first (AGENT-TOOLS 1.12 §16.2; the policy ran before this
@@ -1267,6 +1434,8 @@ async function typeText(rawParams) {
1267
1434
  again = await resolveInput(null, refind);
1268
1435
  }
1269
1436
  assertSameTarget(entry, again.entry);
1437
+ // A recycled row may hold another kind of field under the same names.
1438
+ assertFieldKind(again.entry, kind);
1270
1439
  snapshot = again.snapshot;
1271
1440
  entry = again.entry;
1272
1441
  if (entry.info.disabled)
@@ -1785,7 +1954,7 @@ async function scroll(rawParams) {
1785
1954
  let entry;
1786
1955
  let frame = null;
1787
1956
  if (params.target !== undefined && params.target !== null) {
1788
- const resolved = await resolveTarget(params.target, 'scroll');
1957
+ const resolved = await resolveTarget(params.target, 'scroll', undefined, params.dryRun !== true);
1789
1958
  const scroller = findActionable(resolved.snapshot, resolved.entry, 'scroll', true);
1790
1959
  if (!scroller)
1791
1960
  throw notActionable('no_action', `${describe(resolved.entry)} is not inside a scroll view`);
@@ -1963,7 +2132,7 @@ async function toggle(rawParams) {
1963
2132
  const params = paramsOf(rawParams);
1964
2133
  if (params.value !== undefined && typeof params.value !== 'boolean')
1965
2134
  throw badParams('value must be a boolean');
1966
- const first = await resolveTarget(params.target, 'toggle', params.noScroll === true);
2135
+ const first = await resolveTarget(params.target, 'toggle', params.noScroll === true, params.dryRun !== true);
1967
2136
  const plan = (snap, picked) => {
1968
2137
  const found = findActionable(snap, picked, 'toggle', true);
1969
2138
  if (!found)
@@ -2006,6 +2175,7 @@ async function toggle(rawParams) {
2006
2175
  const { element } = await elementFor(control);
2007
2176
  return { element, value: params.value, unchanged: true, ...(reach.scrolled ? { scrolled: true } : {}), settledMs: 0 };
2008
2177
  }
2178
+ (0, guard_1.assertRecipient)(recipientGuardOf(params), control);
2009
2179
  const closedKeyboard = closeKeyboardFor(control);
2010
2180
  callHandler('onValueChange', handler, [next]);
2011
2181
  const outcome = await settleOutcome();