react-native-tapline 1.4.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.
- package/TaplineNative.podspec +3 -1
- package/dist/actions.d.ts +75 -2
- package/dist/actions.js +333 -25
- package/dist/actions.js.map +1 -1
- package/dist/alerts.d.ts +2 -0
- package/dist/alerts.js +5 -0
- package/dist/alerts.js.map +1 -1
- package/dist/anim.d.ts +25 -0
- package/dist/anim.js +311 -0
- package/dist/anim.js.map +1 -0
- package/dist/appinfo.d.ts +12 -0
- package/dist/appinfo.js +78 -0
- package/dist/appinfo.js.map +1 -0
- package/dist/checks.d.ts +43 -1
- package/dist/checks.js +17 -5
- package/dist/checks.js.map +1 -1
- package/dist/client.d.ts +2 -0
- package/dist/client.js +28 -2
- package/dist/client.js.map +1 -1
- package/dist/commands.js +28 -0
- package/dist/commands.js.map +1 -1
- package/dist/glass.d.ts +3 -0
- package/dist/glass.js +81 -0
- package/dist/glass.js.map +1 -0
- package/dist/glassclip.d.ts +18 -0
- package/dist/glassclip.js +69 -0
- package/dist/glassclip.js.map +1 -0
- package/dist/guard.d.ts +17 -0
- package/dist/guard.js +44 -0
- package/dist/guard.js.map +1 -0
- package/dist/idle.d.ts +43 -0
- package/dist/idle.js +379 -0
- package/dist/idle.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +18 -1
- package/dist/inspect.js +48 -8
- package/dist/inspect.js.map +1 -1
- package/dist/loading.d.ts +17 -0
- package/dist/loading.js +96 -0
- package/dist/loading.js.map +1 -0
- package/dist/locale.js +1 -1
- package/dist/locale.js.map +1 -1
- package/dist/media.d.ts +71 -0
- package/dist/media.js +516 -0
- package/dist/media.js.map +1 -0
- package/dist/methods.js +14 -1
- package/dist/methods.js.map +1 -1
- package/dist/move.d.ts +40 -0
- package/dist/move.js +125 -0
- package/dist/move.js.map +1 -0
- package/dist/native.d.ts +5 -0
- package/dist/native.js.map +1 -1
- package/dist/network.d.ts +4 -0
- package/dist/network.js +49 -1
- package/dist/network.js.map +1 -1
- package/dist/place.d.ts +23 -0
- package/dist/place.js +66 -0
- package/dist/place.js.map +1 -0
- package/dist/placekey.d.ts +29 -0
- package/dist/placekey.js +134 -0
- package/dist/placekey.js.map +1 -0
- package/dist/routetree.d.ts +27 -0
- package/dist/routetree.js +216 -0
- package/dist/routetree.js.map +1 -0
- package/dist/types.d.ts +28 -2
- package/dist/types.js +5 -1
- package/dist/types.js.map +1 -1
- package/dist/uxcheck.d.ts +57 -0
- package/dist/uxcheck.js +222 -0
- package/dist/uxcheck.js.map +1 -0
- package/dist/wait.d.ts +5 -0
- package/dist/wait.js +34 -3
- package/dist/wait.js.map +1 -1
- package/ios/TaplineNativeModule.swift +61 -0
- package/package.json +1 -1
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;
|
|
@@ -15,10 +16,12 @@ exports.longPress = longPress;
|
|
|
15
16
|
exports.setKeyboardControl = setKeyboardControl;
|
|
16
17
|
exports.typeText = typeText;
|
|
17
18
|
exports.scroll = scroll;
|
|
19
|
+
exports.measureScrollState = measureScrollState;
|
|
18
20
|
exports.toggle = toggle;
|
|
19
21
|
exports.openURL = openURL;
|
|
20
22
|
exports.inspect = inspect;
|
|
21
23
|
exports.audit = audit;
|
|
24
|
+
exports.uxCheck = uxCheck;
|
|
22
25
|
exports.getLogs = getLogs;
|
|
23
26
|
exports.reload = reload;
|
|
24
27
|
exports.screenshotAvailable = screenshotAvailable;
|
|
@@ -26,15 +29,21 @@ exports.screenshot = screenshot;
|
|
|
26
29
|
exports.swipe = swipe;
|
|
27
30
|
const react_native_1 = require("react-native");
|
|
28
31
|
const keyboard_1 = require("./keyboard");
|
|
32
|
+
const guard_1 = require("./guard");
|
|
33
|
+
const network_1 = require("./network");
|
|
34
|
+
const loading_1 = require("./loading");
|
|
29
35
|
const native_1 = require("./native");
|
|
30
36
|
const inspect_1 = require("./inspect");
|
|
31
37
|
const source_1 = require("./source");
|
|
32
38
|
const closest_1 = require("./closest");
|
|
39
|
+
const glassclip_1 = require("./glassclip");
|
|
33
40
|
const checks_1 = require("./checks");
|
|
41
|
+
const uxcheck_1 = require("./uxcheck");
|
|
34
42
|
const alerts_1 = require("./alerts");
|
|
35
43
|
const fiber_1 = require("./fiber");
|
|
36
44
|
const logs_1 = require("./logs");
|
|
37
45
|
const register_1 = require("./register");
|
|
46
|
+
const idle_1 = require("./idle");
|
|
38
47
|
const settle_1 = require("./settle");
|
|
39
48
|
const types_1 = require("./types");
|
|
40
49
|
var inspect_2 = require("./inspect");
|
|
@@ -126,14 +135,90 @@ async function sheetProbe() {
|
|
|
126
135
|
}
|
|
127
136
|
return null;
|
|
128
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
|
+
}
|
|
129
186
|
async function settleNow() {
|
|
130
187
|
const started = Date.now();
|
|
131
188
|
let capped = false;
|
|
132
189
|
const onCap = () => {
|
|
133
190
|
capped = true;
|
|
134
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
|
+
}
|
|
135
219
|
await (0, settle_1.waitUntilStill)({
|
|
136
220
|
...settings,
|
|
221
|
+
capMs: settings.capMs - knowSpentMs,
|
|
137
222
|
onCommit: (listener) => (0, register_1.onCommit)(() => listener()),
|
|
138
223
|
now: Date.now,
|
|
139
224
|
onCap,
|
|
@@ -168,6 +253,7 @@ async function settleNow() {
|
|
|
168
253
|
}
|
|
169
254
|
}
|
|
170
255
|
const settledMs = Math.max(0, Math.min(settings.capMs, Date.now() - started));
|
|
256
|
+
requestMark = (0, network_1.startedRequests)();
|
|
171
257
|
return capped && settings.capMs > 0 ? { settledMs, settleCapMs: settings.capMs } : { settledMs };
|
|
172
258
|
}
|
|
173
259
|
function badParams(message) {
|
|
@@ -203,6 +289,22 @@ function callHandler(name, handler, args) {
|
|
|
203
289
|
});
|
|
204
290
|
}
|
|
205
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
|
+
}
|
|
206
308
|
function parseTarget(raw) {
|
|
207
309
|
if (!(0, types_1.isRecord)(raw))
|
|
208
310
|
throw badParams('target must be an object such as {"testID":"login"}');
|
|
@@ -486,8 +588,38 @@ function inFrontLayer(matches) {
|
|
|
486
588
|
const inAlert = matches.filter((entry) => inStandInAlert(entry.node.fiber));
|
|
487
589
|
return inAlert.length > 0 ? inAlert : matches;
|
|
488
590
|
}
|
|
489
|
-
/**
|
|
490
|
-
|
|
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) {
|
|
491
623
|
const target = parseTarget(raw);
|
|
492
624
|
const snapshot = (0, inspect_1.takeSnapshot)();
|
|
493
625
|
if (target.kind === 'ref') {
|
|
@@ -531,7 +663,14 @@ async function resolveTarget(raw, hint) {
|
|
|
531
663
|
}
|
|
532
664
|
return { snapshot, entry: hit };
|
|
533
665
|
}
|
|
534
|
-
|
|
666
|
+
let matches = inFrontLayer(matchEntries(snapshot, target));
|
|
667
|
+
// A journey in a user's eyes (§17.1): visible text matches only what shows on the glass, not what a scroll view
|
|
668
|
+
// clips or the keyboard covers (1.5 review, finding 11).
|
|
669
|
+
if (glassOnly && target.kind === 'text' && matches.length > 0) {
|
|
670
|
+
const scrollers = await (0, glassclip_1.measureScrollers)(snapshot);
|
|
671
|
+
const frames = await Promise.all(matches.map((entry) => (0, inspect_1.measureFiber)(entry.node.fiber)));
|
|
672
|
+
matches = matches.filter((entry, i) => (0, glassclip_1.visiblePartOnGlass)(snapshot, entry, frames[i] ?? null, scrollers) !== null);
|
|
673
|
+
}
|
|
535
674
|
if (matches.length === 0)
|
|
536
675
|
await refuseBehindSheet(snapshot, target);
|
|
537
676
|
if (target.index !== undefined) {
|
|
@@ -724,7 +863,8 @@ async function assertReachable(snapshot, actor, frame) {
|
|
|
724
863
|
const name = describe(actor);
|
|
725
864
|
if (right <= left || bottom <= top) {
|
|
726
865
|
throw notActionable('off_screen', `${name} is off screen (at ${Math.round(frame.x)},${Math.round(frame.y)}; the screen is ${width}x${height}). ` +
|
|
727
|
-
|
|
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.');
|
|
728
868
|
}
|
|
729
869
|
const bar = (0, inspect_1.nativeTabBar)(snapshot);
|
|
730
870
|
const navBar = (0, inspect_1.nativeNavBar)(snapshot);
|
|
@@ -933,20 +1073,28 @@ function focusInput(entry, frame) {
|
|
|
933
1073
|
callHandler(key, handler, [pressEvent(entry.node.fiber, frame)]);
|
|
934
1074
|
}
|
|
935
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
|
+
}
|
|
936
1080
|
async function pressLike(kind, rawParams) {
|
|
937
1081
|
const params = paramsOf(rawParams);
|
|
938
|
-
const
|
|
1082
|
+
const afterMs = kind === 'press' ? againAfter(params.again) : undefined;
|
|
1083
|
+
const guard = recipientGuardOf(params);
|
|
1084
|
+
const { snapshot, entry } = await resolveTarget(params.target, kind, params.noScroll === true, params.dryRun !== true);
|
|
939
1085
|
if (entry.tab) {
|
|
940
1086
|
if (kind === 'longPress')
|
|
941
1087
|
throw notActionable('no_action', `${describe(entry)} has no long-press action`);
|
|
942
1088
|
if (params.dryRun === true)
|
|
943
1089
|
return { element: (0, inspect_1.toElement)(entry, null), dryRun: true, settledMs: 0 };
|
|
1090
|
+
(0, guard_1.assertRecipient)(guard, entry);
|
|
944
1091
|
selectTab(snapshot, entry.tab);
|
|
945
1092
|
return { element: (0, inspect_1.toElement)(entry, null), ...keyboardNote(closeKeyboardFor(entry)), ...(await settleOutcome()) };
|
|
946
1093
|
}
|
|
947
1094
|
if (entry.navBack) {
|
|
948
1095
|
if (params.dryRun === true)
|
|
949
1096
|
return { element: (0, inspect_1.toElement)(entry, null), dryRun: true, settledMs: 0 };
|
|
1097
|
+
(0, guard_1.assertRecipient)(guard, entry);
|
|
950
1098
|
const { route: _route, ...outcome } = await back({});
|
|
951
1099
|
return { element: (0, inspect_1.toElement)(entry, null), ...outcome };
|
|
952
1100
|
}
|
|
@@ -971,21 +1119,24 @@ async function pressLike(kind, rawParams) {
|
|
|
971
1119
|
return { actor: found, handler: own, focusing: isInput };
|
|
972
1120
|
};
|
|
973
1121
|
let planned = plan(snapshot, entry);
|
|
1122
|
+
// The element the agent named; a scroll may find it again.
|
|
1123
|
+
let touched = entry;
|
|
974
1124
|
// Off screen, in the tree: scroll it into view first (AGENT-TOOLS 1.12 §16.2). The policy ran before this call.
|
|
975
1125
|
let reach = {};
|
|
976
1126
|
let current = snapshot;
|
|
977
1127
|
if (params.force !== true) {
|
|
978
1128
|
// What doesn't depend on where the element is comes first: an open alert is answered before anything scrolls.
|
|
979
1129
|
assertNoBlockingAlert(planned.actor);
|
|
980
|
-
reach = await scrollIntoReach(snapshot, planned.actor, params.dryRun === true);
|
|
1130
|
+
reach = await scrollIntoReach(snapshot, planned.actor, params.dryRun === true, params.noScroll === true);
|
|
981
1131
|
if (reach.scrolled) {
|
|
982
1132
|
const again = await reresolve(planned.actor, kind, params.target);
|
|
983
1133
|
current = again.snapshot;
|
|
1134
|
+
touched = again.entry;
|
|
984
1135
|
planned = plan(again.snapshot, again.entry);
|
|
985
1136
|
await assertRefoundOnScreen(current, planned.actor, reach);
|
|
986
1137
|
}
|
|
987
1138
|
}
|
|
988
|
-
const { actor, handler
|
|
1139
|
+
const { actor, handler } = planned;
|
|
989
1140
|
const { element, frame } = await elementFor(actor);
|
|
990
1141
|
if (reach.scrolls)
|
|
991
1142
|
return { element, dryRun: true, scrolls: true, ...(reach.point ? { point: reach.point } : {}), settledMs: 0 };
|
|
@@ -993,18 +1144,88 @@ async function pressLike(kind, rawParams) {
|
|
|
993
1144
|
// A dry run (AGENT-TOOLS 1.8 §11.4): every check ran; nothing is touched.
|
|
994
1145
|
if (params.dryRun === true)
|
|
995
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);
|
|
996
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;
|
|
997
1154
|
if (handler)
|
|
998
|
-
|
|
1155
|
+
threw = callHandlerCaught(handlerName, handler, [pressEvent(actor.node.fiber, frame)]);
|
|
999
1156
|
else {
|
|
1000
1157
|
// A finger focuses the input and also fires the input's own onPress (not an ancestor's: the
|
|
1001
1158
|
// input takes the touch).
|
|
1002
1159
|
focusInput(actor, frame);
|
|
1003
1160
|
const own = appHandler(actor, 'onPress');
|
|
1004
|
-
if (own)
|
|
1005
|
-
|
|
1161
|
+
if (own) {
|
|
1162
|
+
handled = true;
|
|
1163
|
+
threw = callHandlerCaught('onPress', own, [pressEvent(actor.node.fiber, frame)]);
|
|
1164
|
+
}
|
|
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 } : {}) };
|
|
1170
|
+
const second = afterMs === undefined ? undefined : await pressAgain(actor, params, afterMs);
|
|
1171
|
+
const settled = await settleOutcome();
|
|
1172
|
+
return {
|
|
1173
|
+
...(facts !== undefined ? { handler: facts } : {}),
|
|
1174
|
+
startedRequests: Math.max(0, (0, network_1.startedRequests)() - requestsBefore),
|
|
1175
|
+
element,
|
|
1176
|
+
...(reach.scrolled ? { scrolled: true } : {}),
|
|
1177
|
+
...keyboardNote(closedKeyboard),
|
|
1178
|
+
...forced,
|
|
1179
|
+
...(second !== undefined ? { second } : {}),
|
|
1180
|
+
...settled,
|
|
1181
|
+
};
|
|
1182
|
+
}
|
|
1183
|
+
/** `again`: a second tap this many milliseconds after the first (a stress double tap, AGENT-TOOLS 1.12 §17.4). */
|
|
1184
|
+
function againAfter(raw) {
|
|
1185
|
+
if (raw === undefined)
|
|
1186
|
+
return undefined;
|
|
1187
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 20 || raw > 1000) {
|
|
1188
|
+
throw badParams('again must be milliseconds from 20 to 1000');
|
|
1189
|
+
}
|
|
1190
|
+
return Math.round(raw);
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* The second tap of a double tap, made `afterMs` after the first on whatever is there by then, as a finger would
|
|
1194
|
+
* land: the app may have disabled or removed the control (that is the guard against a double submit working).
|
|
1195
|
+
* It goes through the same checks as a tap; a refusal is the answer's `second`, never an error.
|
|
1196
|
+
*/
|
|
1197
|
+
async function pressAgain(first, params, afterMs) {
|
|
1198
|
+
const started = Date.now();
|
|
1199
|
+
await new Promise((resolve) => setTimeout(resolve, afterMs));
|
|
1200
|
+
try {
|
|
1201
|
+
const again = await reresolve(first, 'press', params.target, false);
|
|
1202
|
+
const found = findActionable(again.snapshot, again.entry, 'press', false);
|
|
1203
|
+
if (!found || found.info.disabled || again.entry.info.disabled) {
|
|
1204
|
+
return { done: false, reason: found ? 'disabled' : 'no_action', afterMs: Date.now() - started };
|
|
1205
|
+
}
|
|
1206
|
+
// The second tap is refused as `changed` when the id, label or text is no longer what the first tap found: a
|
|
1207
|
+
// control that renames itself ("Added 1") and a list row that was recycled are both another element now.
|
|
1208
|
+
if (!sameTarget(first, again.entry))
|
|
1209
|
+
return { done: false, reason: 'changed', afterMs: Date.now() - started };
|
|
1210
|
+
const handler = (0, inspect_1.findHandler)(found.node, 'onPress');
|
|
1211
|
+
if (!handler)
|
|
1212
|
+
return { done: false, reason: 'no_action', afterMs: Date.now() - started };
|
|
1213
|
+
const { frame } = await elementFor(found);
|
|
1214
|
+
if (params.force !== true)
|
|
1215
|
+
await assertReachable(again.snapshot, found, frame);
|
|
1216
|
+
const at = Date.now() - started;
|
|
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 };
|
|
1220
|
+
}
|
|
1221
|
+
catch (error) {
|
|
1222
|
+
const reason = error instanceof types_1.TaplineError
|
|
1223
|
+
? error.code === 'not_actionable'
|
|
1224
|
+
? (error.message.split(':')[0] ?? error.code)
|
|
1225
|
+
: error.code
|
|
1226
|
+
: 'internal';
|
|
1227
|
+
return { done: false, reason, afterMs: Date.now() - started };
|
|
1006
1228
|
}
|
|
1007
|
-
return { element, ...(reach.scrolled ? { scrolled: true } : {}), ...keyboardNote(closedKeyboard), ...forced, ...(await settleOutcome()) };
|
|
1008
1229
|
}
|
|
1009
1230
|
/** Where a finger would land: the middle of the element (points, rounded). */
|
|
1010
1231
|
function pointOf(frame) {
|
|
@@ -1129,9 +1350,9 @@ async function moveFocusAboveKeyboard(entry) {
|
|
|
1129
1350
|
function keyboardNote(closed) {
|
|
1130
1351
|
return closed ? { closedKeyboard: true } : {};
|
|
1131
1352
|
}
|
|
1132
|
-
async function resolveInput(snapshot, rawTarget) {
|
|
1353
|
+
async function resolveInput(snapshot, rawTarget, glassOnly = false, loadingWait = true) {
|
|
1133
1354
|
if (rawTarget !== undefined && rawTarget !== null) {
|
|
1134
|
-
const { snapshot: snap, entry } = await resolveTarget(rawTarget, 'type');
|
|
1355
|
+
const { snapshot: snap, entry } = await resolveTarget(rawTarget, 'type', glassOnly, loadingWait);
|
|
1135
1356
|
const input = findActionable(snap, entry, 'type', true);
|
|
1136
1357
|
if (!input)
|
|
1137
1358
|
throw notActionable('no_action', `${describe(entry)} is not an input`);
|
|
@@ -1164,6 +1385,12 @@ function setNativeText(entry, text) {
|
|
|
1164
1385
|
}
|
|
1165
1386
|
}
|
|
1166
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
|
+
}
|
|
1167
1394
|
async function typeText(rawParams) {
|
|
1168
1395
|
const params = paramsOf(rawParams);
|
|
1169
1396
|
const submit = params.submit === true;
|
|
@@ -1178,7 +1405,13 @@ async function typeText(rawParams) {
|
|
|
1178
1405
|
throw badParams('text is required (or pass submit: true)');
|
|
1179
1406
|
// Deleting from the end edits what is there, so it never replaces the whole text.
|
|
1180
1407
|
const replace = params.replace !== false && !deleting;
|
|
1181
|
-
|
|
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);
|
|
1182
1415
|
if (entry.info.disabled)
|
|
1183
1416
|
throw notActionable('disabled', `${describe(entry)} is not editable; ${ENABLE_FIRST}.`);
|
|
1184
1417
|
// Off screen, in the tree: scroll it into view first (AGENT-TOOLS 1.12 §16.2; the policy ran before this
|
|
@@ -1187,7 +1420,7 @@ async function typeText(rawParams) {
|
|
|
1187
1420
|
if (params.force !== true)
|
|
1188
1421
|
assertNoBlockingAlert(entry);
|
|
1189
1422
|
if (params.force !== true && !ownsInstance(entry, keyboard.focusedInput())) {
|
|
1190
|
-
reach = await scrollIntoReach(snapshot, entry, params.dryRun === true);
|
|
1423
|
+
reach = await scrollIntoReach(snapshot, entry, params.dryRun === true, params.noScroll === true);
|
|
1191
1424
|
if (reach.scrolled) {
|
|
1192
1425
|
let again;
|
|
1193
1426
|
try {
|
|
@@ -1201,6 +1434,8 @@ async function typeText(rawParams) {
|
|
|
1201
1434
|
again = await resolveInput(null, refind);
|
|
1202
1435
|
}
|
|
1203
1436
|
assertSameTarget(entry, again.entry);
|
|
1437
|
+
// A recycled row may hold another kind of field under the same names.
|
|
1438
|
+
assertFieldKind(again.entry, kind);
|
|
1204
1439
|
snapshot = again.snapshot;
|
|
1205
1440
|
entry = again.entry;
|
|
1206
1441
|
if (entry.info.disabled)
|
|
@@ -1528,10 +1763,17 @@ function visiblePart(frame, screen) {
|
|
|
1528
1763
|
* alert, under a bar) run afterwards, as for any element. Refused as `off_screen`, with the reason, when
|
|
1529
1764
|
* no scroll can bring it on screen. `dry` predicts the point after the scroll and scrolls nothing.
|
|
1530
1765
|
*/
|
|
1531
|
-
async function scrollIntoReach(snapshot, entry, dry) {
|
|
1766
|
+
async function scrollIntoReach(snapshot, entry, dry, noScroll = false) {
|
|
1532
1767
|
const frame = await (0, inspect_1.measureFiber)(entry.node.fiber);
|
|
1533
1768
|
if (frame === null)
|
|
1534
1769
|
return {};
|
|
1770
|
+
// A journey in a user's eyes (`noScroll`, §17.1): nothing scrolls by itself. An element wholly off screen is refused.
|
|
1771
|
+
if (noScroll) {
|
|
1772
|
+
// On the glass, not just on screen: a scroll view clips it, the keyboard covers it (1.5 review, finding 11).
|
|
1773
|
+
if ((0, glassclip_1.visiblePartOnGlass)(snapshot, entry, frame, await (0, glassclip_1.measureScrollers)(snapshot)) !== null)
|
|
1774
|
+
return {};
|
|
1775
|
+
throw notActionable('off_screen', `${describe(entry)} is off screen; in a journey nothing scrolls by itself: scroll the way a person would, then try again.`);
|
|
1776
|
+
}
|
|
1535
1777
|
// Partly on screen (over the edge, or under a bar): scrolled clear too, with one attempt; if that
|
|
1536
1778
|
// can't be done the action goes on as before and the touch checks decide. Never refused for this.
|
|
1537
1779
|
const partial = visiblePart(frame, snapshot.screen) !== null;
|
|
@@ -1627,7 +1869,7 @@ function refindTarget(entry, original) {
|
|
|
1627
1869
|
* The same element after a scroll, from a fresh snapshot (the list may have laid out other rows on the
|
|
1628
1870
|
* way). When a list unmounted the row, the ref is gone: it is found again by the original target.
|
|
1629
1871
|
*/
|
|
1630
|
-
async function reresolve(entry, hint, original) {
|
|
1872
|
+
async function reresolve(entry, hint, original, checkSame = true) {
|
|
1631
1873
|
let found;
|
|
1632
1874
|
try {
|
|
1633
1875
|
found = await resolveTarget({ ref: (0, inspect_1.refFor)(entry.node.fiber) }, hint);
|
|
@@ -1640,21 +1882,24 @@ async function reresolve(entry, hint, original) {
|
|
|
1640
1882
|
throw error;
|
|
1641
1883
|
found = await resolveTarget(again, hint);
|
|
1642
1884
|
}
|
|
1643
|
-
|
|
1885
|
+
if (checkSame)
|
|
1886
|
+
assertSameTarget(entry, found.entry);
|
|
1644
1887
|
return found;
|
|
1645
1888
|
}
|
|
1889
|
+
/** The same element by id, label and text, each on its own (a row can keep its id and label while its text changes). */
|
|
1890
|
+
function sameTarget(before, after) {
|
|
1891
|
+
return before.info.testID === after.info.testID && before.info.label === after.info.label && before.info.text === after.info.text;
|
|
1892
|
+
}
|
|
1646
1893
|
/**
|
|
1647
1894
|
* After a scroll the element found again must still be the one the safety check saw: a list that recycles its rows
|
|
1648
1895
|
* can hand the same component another row ("Continue" becomes "Delete account"). A changed id, label or text is
|
|
1649
1896
|
* refused before anything is touched (Codex review of 1.4.0).
|
|
1650
1897
|
*/
|
|
1651
1898
|
function assertSameTarget(before, after) {
|
|
1652
|
-
|
|
1899
|
+
if (sameTarget(before, after))
|
|
1900
|
+
return;
|
|
1653
1901
|
const was = { id: before.info.testID, name: labelOf(before.info) };
|
|
1654
1902
|
const now = { id: after.info.testID, name: labelOf(after.info) };
|
|
1655
|
-
const same = was.id === now.id && before.info.label === after.info.label && before.info.text === after.info.text;
|
|
1656
|
-
if (same)
|
|
1657
|
-
return;
|
|
1658
1903
|
if (was.name === now.name) {
|
|
1659
1904
|
was.name = before.info.text;
|
|
1660
1905
|
now.name = after.info.text;
|
|
@@ -1709,7 +1954,7 @@ async function scroll(rawParams) {
|
|
|
1709
1954
|
let entry;
|
|
1710
1955
|
let frame = null;
|
|
1711
1956
|
if (params.target !== undefined && params.target !== null) {
|
|
1712
|
-
const resolved = await resolveTarget(params.target, 'scroll');
|
|
1957
|
+
const resolved = await resolveTarget(params.target, 'scroll', undefined, params.dryRun !== true);
|
|
1713
1958
|
const scroller = findActionable(resolved.snapshot, resolved.entry, 'scroll', true);
|
|
1714
1959
|
if (!scroller)
|
|
1715
1960
|
throw notActionable('no_action', `${describe(resolved.entry)} is not inside a scroll view`);
|
|
@@ -1887,7 +2132,7 @@ async function toggle(rawParams) {
|
|
|
1887
2132
|
const params = paramsOf(rawParams);
|
|
1888
2133
|
if (params.value !== undefined && typeof params.value !== 'boolean')
|
|
1889
2134
|
throw badParams('value must be a boolean');
|
|
1890
|
-
const first = await resolveTarget(params.target, 'toggle');
|
|
2135
|
+
const first = await resolveTarget(params.target, 'toggle', params.noScroll === true, params.dryRun !== true);
|
|
1891
2136
|
const plan = (snap, picked) => {
|
|
1892
2137
|
const found = findActionable(snap, picked, 'toggle', true);
|
|
1893
2138
|
if (!found)
|
|
@@ -1905,7 +2150,7 @@ async function toggle(rawParams) {
|
|
|
1905
2150
|
let reach = {};
|
|
1906
2151
|
if (params.force !== true) {
|
|
1907
2152
|
assertNoBlockingAlert(planned.control);
|
|
1908
|
-
reach = await scrollIntoReach(snapshot, planned.control, params.dryRun === true);
|
|
2153
|
+
reach = await scrollIntoReach(snapshot, planned.control, params.dryRun === true, params.noScroll === true);
|
|
1909
2154
|
if (reach.scrolled) {
|
|
1910
2155
|
const again = await reresolve(planned.control, 'toggle', params.target);
|
|
1911
2156
|
snapshot = again.snapshot;
|
|
@@ -1930,6 +2175,7 @@ async function toggle(rawParams) {
|
|
|
1930
2175
|
const { element } = await elementFor(control);
|
|
1931
2176
|
return { element, value: params.value, unchanged: true, ...(reach.scrolled ? { scrolled: true } : {}), settledMs: 0 };
|
|
1932
2177
|
}
|
|
2178
|
+
(0, guard_1.assertRecipient)(recipientGuardOf(params), control);
|
|
1933
2179
|
const closedKeyboard = closeKeyboardFor(control);
|
|
1934
2180
|
callHandler('onValueChange', handler, [next]);
|
|
1935
2181
|
const outcome = await settleOutcome();
|
|
@@ -2036,6 +2282,68 @@ async function audit(rawParams) {
|
|
|
2036
2282
|
}
|
|
2037
2283
|
return { issues, checked: [...types] };
|
|
2038
2284
|
}
|
|
2285
|
+
/** `ux_check {wayBack?, earlier?}` (AGENT-TOOLS 1.12 §17.3): the screen against the usability basics. */
|
|
2286
|
+
async function uxCheck(rawParams) {
|
|
2287
|
+
const params = paramsOf(rawParams);
|
|
2288
|
+
if (params.wayBack !== undefined && typeof params.wayBack !== 'boolean')
|
|
2289
|
+
throw badParams('wayBack must be true or false');
|
|
2290
|
+
const earlierRaw = params.earlier;
|
|
2291
|
+
let earlier;
|
|
2292
|
+
if (earlierRaw !== undefined && earlierRaw !== null) {
|
|
2293
|
+
if (!Array.isArray(earlierRaw))
|
|
2294
|
+
throw badParams('earlier must be a list of {label, id?}');
|
|
2295
|
+
earlier = [];
|
|
2296
|
+
for (const item of earlierRaw) {
|
|
2297
|
+
if (!(0, types_1.isRecord)(item) || typeof item.label !== 'string')
|
|
2298
|
+
throw badParams('earlier must be a list of {label, id?}');
|
|
2299
|
+
earlier.push({ label: item.label, ...(typeof item.id === 'string' && item.id !== '' ? { id: item.id } : {}) });
|
|
2300
|
+
}
|
|
2301
|
+
}
|
|
2302
|
+
if (params.glass !== undefined && typeof params.glass !== 'boolean')
|
|
2303
|
+
throw badParams('glass must be true or false');
|
|
2304
|
+
const result = await (0, uxcheck_1.uxScreen)({
|
|
2305
|
+
...(params.glass === true ? { glass: true } : {}),
|
|
2306
|
+
...(params.wayBack === true ? { wayBack: true } : {}),
|
|
2307
|
+
...(earlier !== undefined ? { earlier } : {}),
|
|
2308
|
+
});
|
|
2309
|
+
let sources = [];
|
|
2310
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
2311
|
+
const fibers = result.findings.flatMap((f) => (f.entry !== null ? [f.entry.node.fiber] : []));
|
|
2312
|
+
try {
|
|
2313
|
+
sources = fibers.length > 0 ? await (0, source_1.sourcesOf)(fibers) : [];
|
|
2314
|
+
}
|
|
2315
|
+
catch {
|
|
2316
|
+
sources = []; // Metro unreachable: the report still stands, without lines
|
|
2317
|
+
}
|
|
2318
|
+
}
|
|
2319
|
+
let next = 0;
|
|
2320
|
+
const issues = [];
|
|
2321
|
+
for (const finding of result.findings) {
|
|
2322
|
+
const source = finding.entry !== null ? sources[next++] : undefined;
|
|
2323
|
+
let element = null;
|
|
2324
|
+
if (finding.entry !== null) {
|
|
2325
|
+
try {
|
|
2326
|
+
element = (await elementFor(finding.entry, finding.frame)).element;
|
|
2327
|
+
}
|
|
2328
|
+
catch {
|
|
2329
|
+
element = null;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
const where = element === null && finding.entry !== null
|
|
2333
|
+
? whereOf({ type: 'description', entry: finding.entry, frame: finding.frame, problem: '', fix: '' })
|
|
2334
|
+
: null;
|
|
2335
|
+
const location = source?.location ?? null;
|
|
2336
|
+
issues.push({
|
|
2337
|
+
kind: finding.kind,
|
|
2338
|
+
element,
|
|
2339
|
+
...(where !== null ? { where } : {}),
|
|
2340
|
+
problem: finding.problem,
|
|
2341
|
+
basis: finding.basis,
|
|
2342
|
+
...(location !== null ? { at: { file: location.file, line: location.line, direct: location.direct } } : {}),
|
|
2343
|
+
});
|
|
2344
|
+
}
|
|
2345
|
+
return { issues, checked: result.checked, choices: result.choices, actions: result.actions };
|
|
2346
|
+
}
|
|
2039
2347
|
async function getLogs(rawParams) {
|
|
2040
2348
|
const params = paramsOf(rawParams);
|
|
2041
2349
|
if (params.since !== undefined && typeof params.since !== 'number')
|