react-native-tapline 1.3.1 → 1.5.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/dist/actions.d.ts +66 -1
- package/dist/actions.js +483 -51
- package/dist/actions.js.map +1 -1
- package/dist/checks.d.ts +43 -1
- package/dist/checks.js +17 -5
- package/dist/checks.js.map +1 -1
- package/dist/client.js +11 -1
- package/dist/client.js.map +1 -1
- package/dist/closest.d.ts +30 -0
- package/dist/closest.js +101 -0
- package/dist/closest.js.map +1 -0
- package/dist/commands.d.ts +9 -0
- package/dist/commands.js +135 -2
- 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/index.d.ts +5 -1
- package/dist/index.js +10 -1
- package/dist/index.js.map +1 -1
- package/dist/inspect.d.ts +7 -1
- package/dist/inspect.js +16 -3
- package/dist/inspect.js.map +1 -1
- package/dist/locale.d.ts +67 -0
- package/dist/locale.js +224 -0
- package/dist/locale.js.map +1 -0
- package/dist/logs.d.ts +2 -0
- package/dist/logs.js +7 -0
- package/dist/logs.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 +6 -1
- package/dist/methods.js.map +1 -1
- package/dist/network.d.ts +20 -2
- package/dist/network.js +750 -3
- package/dist/network.js.map +1 -1
- package/dist/types.d.ts +8 -2
- 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.js +30 -3
- package/dist/wait.js.map +1 -1
- package/package.json +1 -1
package/dist/actions.js
CHANGED
|
@@ -15,10 +15,12 @@ exports.longPress = longPress;
|
|
|
15
15
|
exports.setKeyboardControl = setKeyboardControl;
|
|
16
16
|
exports.typeText = typeText;
|
|
17
17
|
exports.scroll = scroll;
|
|
18
|
+
exports.measureScrollState = measureScrollState;
|
|
18
19
|
exports.toggle = toggle;
|
|
19
20
|
exports.openURL = openURL;
|
|
20
21
|
exports.inspect = inspect;
|
|
21
22
|
exports.audit = audit;
|
|
23
|
+
exports.uxCheck = uxCheck;
|
|
22
24
|
exports.getLogs = getLogs;
|
|
23
25
|
exports.reload = reload;
|
|
24
26
|
exports.screenshotAvailable = screenshotAvailable;
|
|
@@ -29,7 +31,10 @@ const keyboard_1 = require("./keyboard");
|
|
|
29
31
|
const native_1 = require("./native");
|
|
30
32
|
const inspect_1 = require("./inspect");
|
|
31
33
|
const source_1 = require("./source");
|
|
34
|
+
const closest_1 = require("./closest");
|
|
35
|
+
const glassclip_1 = require("./glassclip");
|
|
32
36
|
const checks_1 = require("./checks");
|
|
37
|
+
const uxcheck_1 = require("./uxcheck");
|
|
33
38
|
const alerts_1 = require("./alerts");
|
|
34
39
|
const fiber_1 = require("./fiber");
|
|
35
40
|
const logs_1 = require("./logs");
|
|
@@ -306,6 +311,44 @@ function notFoundMessage(target) {
|
|
|
306
311
|
return `nothing on screen at (${target.x}, ${target.y}); look, then pick another target.`;
|
|
307
312
|
}
|
|
308
313
|
}
|
|
314
|
+
/** Where an off-screen frame lies from the screen: `above`, `below`, `left` or `right`; null when any of it is on screen. */
|
|
315
|
+
function sideOffScreen(frame, screen) {
|
|
316
|
+
if (visiblePart(frame, screen) !== null)
|
|
317
|
+
return null;
|
|
318
|
+
if (frame.y + frame.height <= 0)
|
|
319
|
+
return 'above';
|
|
320
|
+
if (frame.y >= screen.height)
|
|
321
|
+
return 'below';
|
|
322
|
+
if (frame.x + frame.width <= 0)
|
|
323
|
+
return 'left';
|
|
324
|
+
return 'right';
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* A `not_found`, with up to 3 closest matches when there are any (AGENT-TOOLS 1.12 §16.2):
|
|
328
|
+
* `Closest: e12 button "Save draft" id=save-draft, e30 text "Saved" (off screen, below).` An element
|
|
329
|
+
* the list doesn't show gets its ref here, so the agent can use it (refs count on from the highest given).
|
|
330
|
+
*/
|
|
331
|
+
async function notFoundError(snapshot, target) {
|
|
332
|
+
const base = notFoundMessage(target);
|
|
333
|
+
if (target.kind !== 'testID' && target.kind !== 'label' && target.kind !== 'text')
|
|
334
|
+
return new types_1.TaplineError('not_found', base);
|
|
335
|
+
const pool = inFrontLayer(snapshot.entries.filter((entry) => entry.info.interesting && !entry.tab));
|
|
336
|
+
const picks = (0, closest_1.closestMatches)(target.kind, target.value, pool.map((entry) => ({ testID: entry.info.testID, label: entry.info.label, text: entry.info.text }))).map((index) => pool[index]);
|
|
337
|
+
const lines = [];
|
|
338
|
+
for (const entry of picks) {
|
|
339
|
+
if (!entry)
|
|
340
|
+
continue;
|
|
341
|
+
const { info } = entry;
|
|
342
|
+
const shown = (0, inspect_1.cutText)(info.text !== '' ? info.text : (info.label ?? ''), 0, inspect_1.MAX_TEXT);
|
|
343
|
+
const frame = await (0, inspect_1.measureFiber)(entry.node.fiber);
|
|
344
|
+
const side = frame ? sideOffScreen(frame, snapshot.screen) : null;
|
|
345
|
+
const line = [(0, inspect_1.refFor)(entry.node.fiber), (0, inspect_1.roleName)(info), shown !== '' ? `"${shown}"` : '', info.testID ? `id=${info.testID}` : '']
|
|
346
|
+
.filter((part) => part !== '')
|
|
347
|
+
.join(' ');
|
|
348
|
+
lines.push(side ? `${line} (off screen, ${side})` : line);
|
|
349
|
+
}
|
|
350
|
+
return new types_1.TaplineError('not_found', lines.length > 0 ? `${base} Closest: ${lines.join(', ')}.` : base);
|
|
351
|
+
}
|
|
309
352
|
/**
|
|
310
353
|
* What a `label` target matches: the element's own accessibility label, else its text. iOS forms
|
|
311
354
|
* the label of an element that has none from the text inside it, and Tapwright reads that label, so
|
|
@@ -448,7 +491,7 @@ function inFrontLayer(matches) {
|
|
|
448
491
|
return inAlert.length > 0 ? inAlert : matches;
|
|
449
492
|
}
|
|
450
493
|
/** Resolve a protocol target to exactly one element, or throw not_found / ambiguous. */
|
|
451
|
-
async function resolveTarget(raw, hint) {
|
|
494
|
+
async function resolveTarget(raw, hint, glassOnly = false) {
|
|
452
495
|
const target = parseTarget(raw);
|
|
453
496
|
const snapshot = (0, inspect_1.takeSnapshot)();
|
|
454
497
|
if (target.kind === 'ref') {
|
|
@@ -492,7 +535,14 @@ async function resolveTarget(raw, hint) {
|
|
|
492
535
|
}
|
|
493
536
|
return { snapshot, entry: hit };
|
|
494
537
|
}
|
|
495
|
-
|
|
538
|
+
let matches = inFrontLayer(matchEntries(snapshot, target));
|
|
539
|
+
// A journey in a user's eyes (§17.1): visible text matches only what shows on the glass, not what a scroll view
|
|
540
|
+
// clips or the keyboard covers (1.5 review, finding 11).
|
|
541
|
+
if (glassOnly && target.kind === 'text' && matches.length > 0) {
|
|
542
|
+
const scrollers = await (0, glassclip_1.measureScrollers)(snapshot);
|
|
543
|
+
const frames = await Promise.all(matches.map((entry) => (0, inspect_1.measureFiber)(entry.node.fiber)));
|
|
544
|
+
matches = matches.filter((entry, i) => (0, glassclip_1.visiblePartOnGlass)(snapshot, entry, frames[i] ?? null, scrollers) !== null);
|
|
545
|
+
}
|
|
496
546
|
if (matches.length === 0)
|
|
497
547
|
await refuseBehindSheet(snapshot, target);
|
|
498
548
|
if (target.index !== undefined) {
|
|
@@ -504,7 +554,7 @@ async function resolveTarget(raw, hint) {
|
|
|
504
554
|
}
|
|
505
555
|
const [only, ...rest] = matches;
|
|
506
556
|
if (!only)
|
|
507
|
-
throw
|
|
557
|
+
throw await notFoundError(snapshot, target);
|
|
508
558
|
if (rest.length > 0) {
|
|
509
559
|
const shown = matches.slice(0, 5);
|
|
510
560
|
throw new types_1.TaplineError('ambiguous', `${matches.length} elements match ${describeTarget(target)}.`, shown.map((entry) => (0, inspect_1.refFor)(entry.node.fiber)), await candidateElements(shown));
|
|
@@ -896,7 +946,8 @@ function focusInput(entry, frame) {
|
|
|
896
946
|
}
|
|
897
947
|
async function pressLike(kind, rawParams) {
|
|
898
948
|
const params = paramsOf(rawParams);
|
|
899
|
-
const
|
|
949
|
+
const afterMs = kind === 'press' ? againAfter(params.again) : undefined;
|
|
950
|
+
const { snapshot, entry } = await resolveTarget(params.target, kind, params.noScroll === true);
|
|
900
951
|
if (entry.tab) {
|
|
901
952
|
if (kind === 'longPress')
|
|
902
953
|
throw notActionable('no_action', `${describe(entry)} has no long-press action`);
|
|
@@ -911,25 +962,46 @@ async function pressLike(kind, rawParams) {
|
|
|
911
962
|
const { route: _route, ...outcome } = await back({});
|
|
912
963
|
return { element: (0, inspect_1.toElement)(entry, null), ...outcome };
|
|
913
964
|
}
|
|
914
|
-
const actor = findActionable(snapshot, entry, kind, false);
|
|
915
965
|
const handlerName = kind === 'press' ? 'onPress' : 'onLongPress';
|
|
916
966
|
const actionName = kind === 'press' ? 'tap' : 'long-press';
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
967
|
+
const plan = (snap, picked) => {
|
|
968
|
+
const found = findActionable(snap, picked, kind, false);
|
|
969
|
+
if (!found) {
|
|
970
|
+
if (picked.info.disabled)
|
|
971
|
+
throw notActionable('disabled', `${describe(picked)} is disabled; ${ENABLE_FIRST}.`);
|
|
972
|
+
throw notActionable('no_action', `${describe(picked)} has no ${actionName} action`);
|
|
973
|
+
}
|
|
974
|
+
if (found.info.disabled || picked.info.disabled) {
|
|
975
|
+
throw notActionable('disabled', `${describe(found)} is disabled; ${ENABLE_FIRST}.`);
|
|
976
|
+
}
|
|
977
|
+
// A finger on an input focuses it (AGENT-TOOLS v1.3): an input never answers no_action to a tap.
|
|
978
|
+
const isInput = kind === 'press' && found.info.actions.includes('type');
|
|
979
|
+
const own = isInput ? null : (0, inspect_1.findHandler)(found.node, handlerName);
|
|
980
|
+
if (!own && !isInput) {
|
|
981
|
+
throw notActionable('no_action', `${describe(found)} has no ${actionName} action`);
|
|
982
|
+
}
|
|
983
|
+
return { actor: found, handler: own, focusing: isInput };
|
|
984
|
+
};
|
|
985
|
+
let planned = plan(snapshot, entry);
|
|
986
|
+
// Off screen, in the tree: scroll it into view first (AGENT-TOOLS 1.12 §16.2). The policy ran before this call.
|
|
987
|
+
let reach = {};
|
|
988
|
+
let current = snapshot;
|
|
989
|
+
if (params.force !== true) {
|
|
990
|
+
// What doesn't depend on where the element is comes first: an open alert is answered before anything scrolls.
|
|
991
|
+
assertNoBlockingAlert(planned.actor);
|
|
992
|
+
reach = await scrollIntoReach(snapshot, planned.actor, params.dryRun === true, params.noScroll === true);
|
|
993
|
+
if (reach.scrolled) {
|
|
994
|
+
const again = await reresolve(planned.actor, kind, params.target);
|
|
995
|
+
current = again.snapshot;
|
|
996
|
+
planned = plan(again.snapshot, again.entry);
|
|
997
|
+
await assertRefoundOnScreen(current, planned.actor, reach);
|
|
998
|
+
}
|
|
930
999
|
}
|
|
1000
|
+
const { actor, handler, focusing } = planned;
|
|
931
1001
|
const { element, frame } = await elementFor(actor);
|
|
932
|
-
|
|
1002
|
+
if (reach.scrolls)
|
|
1003
|
+
return { element, dryRun: true, scrolls: true, ...(reach.point ? { point: reach.point } : {}), settledMs: 0 };
|
|
1004
|
+
const forced = await checkedAfterScroll(params.force === true, reach, () => assertReachable(current, actor, frame));
|
|
933
1005
|
// A dry run (AGENT-TOOLS 1.8 §11.4): every check ran; nothing is touched.
|
|
934
1006
|
if (params.dryRun === true)
|
|
935
1007
|
return { element, dryRun: true, ...pointOf(frame), ...forced, settledMs: 0 };
|
|
@@ -944,7 +1016,61 @@ async function pressLike(kind, rawParams) {
|
|
|
944
1016
|
if (own)
|
|
945
1017
|
callHandler('onPress', own, [pressEvent(actor.node.fiber, frame)]);
|
|
946
1018
|
}
|
|
947
|
-
|
|
1019
|
+
const second = afterMs === undefined ? undefined : await pressAgain(actor, params, afterMs);
|
|
1020
|
+
return {
|
|
1021
|
+
element,
|
|
1022
|
+
...(reach.scrolled ? { scrolled: true } : {}),
|
|
1023
|
+
...keyboardNote(closedKeyboard),
|
|
1024
|
+
...forced,
|
|
1025
|
+
...(second !== undefined ? { second } : {}),
|
|
1026
|
+
...(await settleOutcome()),
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
/** `again`: a second tap this many milliseconds after the first (a stress double tap, AGENT-TOOLS 1.12 §17.4). */
|
|
1030
|
+
function againAfter(raw) {
|
|
1031
|
+
if (raw === undefined)
|
|
1032
|
+
return undefined;
|
|
1033
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw) || raw < 20 || raw > 1000) {
|
|
1034
|
+
throw badParams('again must be milliseconds from 20 to 1000');
|
|
1035
|
+
}
|
|
1036
|
+
return Math.round(raw);
|
|
1037
|
+
}
|
|
1038
|
+
/**
|
|
1039
|
+
* The second tap of a double tap, made `afterMs` after the first on whatever is there by then, as a finger would
|
|
1040
|
+
* land: the app may have disabled or removed the control (that is the guard against a double submit working).
|
|
1041
|
+
* It goes through the same checks as a tap; a refusal is the answer's `second`, never an error.
|
|
1042
|
+
*/
|
|
1043
|
+
async function pressAgain(first, params, afterMs) {
|
|
1044
|
+
const started = Date.now();
|
|
1045
|
+
await new Promise((resolve) => setTimeout(resolve, afterMs));
|
|
1046
|
+
try {
|
|
1047
|
+
const again = await reresolve(first, 'press', params.target, false);
|
|
1048
|
+
const found = findActionable(again.snapshot, again.entry, 'press', false);
|
|
1049
|
+
if (!found || found.info.disabled || again.entry.info.disabled) {
|
|
1050
|
+
return { done: false, reason: found ? 'disabled' : 'no_action', afterMs: Date.now() - started };
|
|
1051
|
+
}
|
|
1052
|
+
// The second tap is refused as `changed` when the id, label or text is no longer what the first tap found: a
|
|
1053
|
+
// control that renames itself ("Added 1") and a list row that was recycled are both another element now.
|
|
1054
|
+
if (!sameTarget(first, again.entry))
|
|
1055
|
+
return { done: false, reason: 'changed', afterMs: Date.now() - started };
|
|
1056
|
+
const handler = (0, inspect_1.findHandler)(found.node, 'onPress');
|
|
1057
|
+
if (!handler)
|
|
1058
|
+
return { done: false, reason: 'no_action', afterMs: Date.now() - started };
|
|
1059
|
+
const { frame } = await elementFor(found);
|
|
1060
|
+
if (params.force !== true)
|
|
1061
|
+
await assertReachable(again.snapshot, found, frame);
|
|
1062
|
+
const at = Date.now() - started;
|
|
1063
|
+
callHandler('onPress', handler, [pressEvent(found.node.fiber, frame)]);
|
|
1064
|
+
return { done: true, afterMs: at };
|
|
1065
|
+
}
|
|
1066
|
+
catch (error) {
|
|
1067
|
+
const reason = error instanceof types_1.TaplineError
|
|
1068
|
+
? error.code === 'not_actionable'
|
|
1069
|
+
? (error.message.split(':')[0] ?? error.code)
|
|
1070
|
+
: error.code
|
|
1071
|
+
: 'internal';
|
|
1072
|
+
return { done: false, reason, afterMs: Date.now() - started };
|
|
1073
|
+
}
|
|
948
1074
|
}
|
|
949
1075
|
/** Where a finger would land: the middle of the element (points, rounded). */
|
|
950
1076
|
function pointOf(frame) {
|
|
@@ -1069,9 +1195,9 @@ async function moveFocusAboveKeyboard(entry) {
|
|
|
1069
1195
|
function keyboardNote(closed) {
|
|
1070
1196
|
return closed ? { closedKeyboard: true } : {};
|
|
1071
1197
|
}
|
|
1072
|
-
async function resolveInput(snapshot, rawTarget) {
|
|
1198
|
+
async function resolveInput(snapshot, rawTarget, glassOnly = false) {
|
|
1073
1199
|
if (rawTarget !== undefined && rawTarget !== null) {
|
|
1074
|
-
const { snapshot: snap, entry } = await resolveTarget(rawTarget, 'type');
|
|
1200
|
+
const { snapshot: snap, entry } = await resolveTarget(rawTarget, 'type', glassOnly);
|
|
1075
1201
|
const input = findActionable(snap, entry, 'type', true);
|
|
1076
1202
|
if (!input)
|
|
1077
1203
|
throw notActionable('no_action', `${describe(entry)} is not an input`);
|
|
@@ -1118,13 +1244,44 @@ async function typeText(rawParams) {
|
|
|
1118
1244
|
throw badParams('text is required (or pass submit: true)');
|
|
1119
1245
|
// Deleting from the end edits what is there, so it never replaces the whole text.
|
|
1120
1246
|
const replace = params.replace !== false && !deleting;
|
|
1121
|
-
|
|
1247
|
+
let { snapshot, entry } = await resolveInput(null, params.target, params.noScroll === true);
|
|
1122
1248
|
if (entry.info.disabled)
|
|
1123
1249
|
throw notActionable('disabled', `${describe(entry)} is not editable; ${ENABLE_FIRST}.`);
|
|
1250
|
+
// Off screen, in the tree: scroll it into view first (AGENT-TOOLS 1.12 §16.2; the policy ran before this
|
|
1251
|
+
// call). The input that already has focus is typed into without touching it, so it is not scrolled to.
|
|
1252
|
+
let reach = {};
|
|
1253
|
+
if (params.force !== true)
|
|
1254
|
+
assertNoBlockingAlert(entry);
|
|
1255
|
+
if (params.force !== true && !ownsInstance(entry, keyboard.focusedInput())) {
|
|
1256
|
+
reach = await scrollIntoReach(snapshot, entry, params.dryRun === true, params.noScroll === true);
|
|
1257
|
+
if (reach.scrolled) {
|
|
1258
|
+
let again;
|
|
1259
|
+
try {
|
|
1260
|
+
again = await resolveInput(null, { ref: (0, inspect_1.refFor)(entry.node.fiber) });
|
|
1261
|
+
}
|
|
1262
|
+
catch (error) {
|
|
1263
|
+
// a list unmounted the row while scrolling: find it again by the original target
|
|
1264
|
+
const refind = error instanceof types_1.TaplineError && error.code === 'not_found' ? refindTarget(entry, params.target) : null;
|
|
1265
|
+
if (refind === null)
|
|
1266
|
+
throw error;
|
|
1267
|
+
again = await resolveInput(null, refind);
|
|
1268
|
+
}
|
|
1269
|
+
assertSameTarget(entry, again.entry);
|
|
1270
|
+
snapshot = again.snapshot;
|
|
1271
|
+
entry = again.entry;
|
|
1272
|
+
if (entry.info.disabled)
|
|
1273
|
+
throw notActionable('disabled', `${describe(entry)} is not editable; ${ENABLE_FIRST}.`);
|
|
1274
|
+
await assertRefoundOnScreen(snapshot, entry, reach);
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1277
|
+
if (reach.scrolls) {
|
|
1278
|
+
const { element } = await elementFor(entry);
|
|
1279
|
+
return { element, value: (0, inspect_1.displayText)((0, inspect_1.currentInputValue)(entry.node)), dryRun: true, scrolls: true, ...(reach.point ? { point: reach.point } : {}), settledMs: 0 };
|
|
1280
|
+
}
|
|
1124
1281
|
// Typing is a tap that focuses the input, then keys: a person couldn't do it where they couldn't tap,
|
|
1125
1282
|
// so the checks are a tap's, with the same reasons and the same `force`. The input that already has
|
|
1126
1283
|
// focus is the exception: the keyboard is up for it and a person types without touching it.
|
|
1127
|
-
const forced = await
|
|
1284
|
+
const forced = await checkedAfterScroll(params.force === true, reach, async () => {
|
|
1128
1285
|
if (ownsInstance(entry, keyboard.focusedInput())) {
|
|
1129
1286
|
// An alert takes the keyboard away even from the focused input: a person answers it first.
|
|
1130
1287
|
assertNoBlockingAlert(entry);
|
|
@@ -1180,6 +1337,7 @@ async function typeText(rawParams) {
|
|
|
1180
1337
|
element: { ...rest, ...(next === '' && placeholder !== undefined ? { placeholder } : {}), value: shown },
|
|
1181
1338
|
value: shown,
|
|
1182
1339
|
...(secure ? { secure: true } : {}),
|
|
1340
|
+
...(reach.scrolled ? { scrolled: true } : {}),
|
|
1183
1341
|
...keyboardNote(closedKeyboard),
|
|
1184
1342
|
...forced,
|
|
1185
1343
|
...outcome,
|
|
@@ -1268,6 +1426,18 @@ async function scrollToTarget(params) {
|
|
|
1268
1426
|
throw badParams('to: "target" needs an element (ref, id, label or text), not a point');
|
|
1269
1427
|
}
|
|
1270
1428
|
const { snapshot, entry: wanted } = await resolveTarget(raw);
|
|
1429
|
+
const scrollers = scrollersOf(snapshot, wanted);
|
|
1430
|
+
if (params.dryRun === true) {
|
|
1431
|
+
const first = scrollers[0];
|
|
1432
|
+
const target = (await elementFor(wanted)).element;
|
|
1433
|
+
if (!first)
|
|
1434
|
+
return { element: target, target, offset: { x: 0, y: 0 }, moved: false, dryRun: true, settledMs: 0 };
|
|
1435
|
+
return { element: (await elementFor(first)).element, target, offset: { x: 0, y: 0 }, moved: false, dryRun: true, settledMs: 0 };
|
|
1436
|
+
}
|
|
1437
|
+
return scrollEntryIntoView(snapshot, wanted, scrollers);
|
|
1438
|
+
}
|
|
1439
|
+
/** The scroll views that hold an element, innermost first. */
|
|
1440
|
+
function scrollersOf(snapshot, wanted) {
|
|
1271
1441
|
const scrollers = [];
|
|
1272
1442
|
for (let p = wanted.node.parent; p >= 0;) {
|
|
1273
1443
|
const candidate = snapshot.entries[p];
|
|
@@ -1278,26 +1448,22 @@ async function scrollToTarget(params) {
|
|
|
1278
1448
|
break;
|
|
1279
1449
|
p = node.parent;
|
|
1280
1450
|
}
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1451
|
+
return scrollers;
|
|
1452
|
+
}
|
|
1453
|
+
/**
|
|
1454
|
+
* How far the element is from clear inside a scroll view, per axis. The area is the part of the
|
|
1455
|
+
* scroll view that is on screen between the bars. A scroll view that is itself out of view (a
|
|
1456
|
+
* list further down the page) has no such part: there the element only has to be inside the
|
|
1457
|
+
* scroll view, and the outer one brings both onto the screen. Without a scroll view the area is
|
|
1458
|
+
* the screen between the bars.
|
|
1459
|
+
*/
|
|
1460
|
+
function shiftsFor(snapshot, wanted) {
|
|
1288
1461
|
const { width, height } = snapshot.screen;
|
|
1289
1462
|
const navBar = (0, inspect_1.nativeNavBar)(snapshot);
|
|
1290
1463
|
const tabBar = (0, inspect_1.nativeTabBar)(snapshot);
|
|
1291
1464
|
const clearTop = navBar && !(0, inspect_1.inNavBarHeader)(navBar, wanted.node.fiber) ? navBar.bottom : 0;
|
|
1292
1465
|
const clearBottom = tabBar ? tabBar.top : height;
|
|
1293
|
-
|
|
1294
|
-
* How far the element is from clear inside a scroll view, per axis. The area is the part of the
|
|
1295
|
-
* scroll view that is on screen between the bars. A scroll view that is itself out of view (a
|
|
1296
|
-
* list further down the page) has no such part: there the element only has to be inside the
|
|
1297
|
-
* scroll view, and the outer one brings both onto the screen. Without a scroll view the area is
|
|
1298
|
-
* the screen between the bars.
|
|
1299
|
-
*/
|
|
1300
|
-
const shiftsIn = (frame, area) => {
|
|
1466
|
+
return (frame, area) => {
|
|
1301
1467
|
let left = Math.max(area?.x ?? 0, 0);
|
|
1302
1468
|
let right = Math.min(area ? area.x + area.width : width, width);
|
|
1303
1469
|
let top = Math.max(area?.y ?? 0, clearTop);
|
|
@@ -1310,6 +1476,10 @@ async function scrollToTarget(params) {
|
|
|
1310
1476
|
}
|
|
1311
1477
|
return { x: axisShift(frame.x, frame.width, left, right), y: axisShift(frame.y, frame.height, top, bottom) };
|
|
1312
1478
|
};
|
|
1479
|
+
}
|
|
1480
|
+
/** The scrolling half of `scroll` with `to: "target"`: scrolls the given scroll views until `wanted` is clear. */
|
|
1481
|
+
async function scrollEntryIntoView(snapshot, wanted, scrollers) {
|
|
1482
|
+
const shiftsIn = shiftsFor(snapshot, wanted);
|
|
1313
1483
|
let settledMs = 0;
|
|
1314
1484
|
let settleCapMs;
|
|
1315
1485
|
let scrolled = null;
|
|
@@ -1410,6 +1580,184 @@ async function scrollToTarget(params) {
|
|
|
1410
1580
|
const { element } = await elementFor(scrolled.entry);
|
|
1411
1581
|
return { element, target, offset: scrolled.offset, moved: true, reached, ...settle };
|
|
1412
1582
|
}
|
|
1583
|
+
/** The part of a frame that is on the screen, or null when none of it is. */
|
|
1584
|
+
function visiblePart(frame, screen) {
|
|
1585
|
+
const left = Math.max(frame.x, 0);
|
|
1586
|
+
const top = Math.max(frame.y, 0);
|
|
1587
|
+
const right = Math.min(frame.x + frame.width, screen.width);
|
|
1588
|
+
const bottom = Math.min(frame.y + frame.height, screen.height);
|
|
1589
|
+
return right <= left || bottom <= top ? null : { x: left, y: top, width: right - left, height: bottom - top };
|
|
1590
|
+
}
|
|
1591
|
+
/**
|
|
1592
|
+
* An action on an element that is in the tree but entirely off screen scrolls it into view first, clear
|
|
1593
|
+
* of the bars (§16.2). The caller has run the policy check already. The touch checks (covered, behind an
|
|
1594
|
+
* alert, under a bar) run afterwards, as for any element. Refused as `off_screen`, with the reason, when
|
|
1595
|
+
* no scroll can bring it on screen. `dry` predicts the point after the scroll and scrolls nothing.
|
|
1596
|
+
*/
|
|
1597
|
+
async function scrollIntoReach(snapshot, entry, dry, noScroll = false) {
|
|
1598
|
+
const frame = await (0, inspect_1.measureFiber)(entry.node.fiber);
|
|
1599
|
+
if (frame === null)
|
|
1600
|
+
return {};
|
|
1601
|
+
// A journey in a user's eyes (`noScroll`, §17.1): nothing scrolls by itself. An element wholly off screen is refused.
|
|
1602
|
+
if (noScroll) {
|
|
1603
|
+
// On the glass, not just on screen: a scroll view clips it, the keyboard covers it (1.5 review, finding 11).
|
|
1604
|
+
if ((0, glassclip_1.visiblePartOnGlass)(snapshot, entry, frame, await (0, glassclip_1.measureScrollers)(snapshot)) !== null)
|
|
1605
|
+
return {};
|
|
1606
|
+
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.`);
|
|
1607
|
+
}
|
|
1608
|
+
// Partly on screen (over the edge, or under a bar): scrolled clear too, with one attempt; if that
|
|
1609
|
+
// can't be done the action goes on as before and the touch checks decide. Never refused for this.
|
|
1610
|
+
const partial = visiblePart(frame, snapshot.screen) !== null;
|
|
1611
|
+
if (partial && !isNotClear(snapshot, entry, frame))
|
|
1612
|
+
return {};
|
|
1613
|
+
const { width, height } = snapshot.screen;
|
|
1614
|
+
const name = describe(entry);
|
|
1615
|
+
const where = `at ${Math.round(frame.x)},${Math.round(frame.y)}; the screen is ${width}x${height}`;
|
|
1616
|
+
const scrollers = scrollersOf(snapshot, entry);
|
|
1617
|
+
if (scrollers.length === 0 && partial)
|
|
1618
|
+
return {};
|
|
1619
|
+
if (scrollers.length === 0) {
|
|
1620
|
+
throw notActionable('off_screen', `${name} is off screen (${where}). It isn't inside a scroll view, so no scroll can bring it on screen; pass force: true to call its handler anyway.`);
|
|
1621
|
+
}
|
|
1622
|
+
if (dry) {
|
|
1623
|
+
const shiftsIn = shiftsFor(snapshot, entry);
|
|
1624
|
+
let predicted = frame;
|
|
1625
|
+
for (const scroller of scrollers) {
|
|
1626
|
+
const [area, state] = await Promise.all([(0, inspect_1.measureFiber)(scroller.node.fiber), measureScrollState(scroller)]);
|
|
1627
|
+
if (!area)
|
|
1628
|
+
continue;
|
|
1629
|
+
const horizontal = (0, inspect_1.findProp)(scroller.node, 'horizontal') === true;
|
|
1630
|
+
const shifts = shiftsIn(predicted, area);
|
|
1631
|
+
const wanted = horizontal ? shifts.x : shifts.y;
|
|
1632
|
+
if (Math.abs(wanted) < 1)
|
|
1633
|
+
continue;
|
|
1634
|
+
let delta = wanted;
|
|
1635
|
+
if (state) {
|
|
1636
|
+
const current = horizontal ? state.offset.x : state.offset.y;
|
|
1637
|
+
const max = horizontal ? state.max.x : state.max.y;
|
|
1638
|
+
delta = Math.max(Math.min(0, current), Math.min(current + wanted, Math.max(max, current))) - current;
|
|
1639
|
+
}
|
|
1640
|
+
predicted = horizontal ? { ...predicted, x: predicted.x - delta } : { ...predicted, y: predicted.y - delta };
|
|
1641
|
+
}
|
|
1642
|
+
if (partial) {
|
|
1643
|
+
return predicted.x === frame.x && predicted.y === frame.y ? {} : { scrolls: true, ...pointOf(visiblePart(predicted, snapshot.screen)) };
|
|
1644
|
+
}
|
|
1645
|
+
const seen = visiblePart(predicted, snapshot.screen);
|
|
1646
|
+
if (seen === null) {
|
|
1647
|
+
throw notActionable('off_screen', `${name} is off screen (${where}), and its scroll view can't bring it on screen: it is as far as it goes; pass force: true to call its handler anyway.`);
|
|
1648
|
+
}
|
|
1649
|
+
return { scrolls: true, ...pointOf(seen) };
|
|
1650
|
+
}
|
|
1651
|
+
let outcome;
|
|
1652
|
+
try {
|
|
1653
|
+
outcome = await scrollEntryIntoView(snapshot, entry, scrollers);
|
|
1654
|
+
}
|
|
1655
|
+
catch (error) {
|
|
1656
|
+
if (partial && error instanceof types_1.TaplineError)
|
|
1657
|
+
return {};
|
|
1658
|
+
throw error;
|
|
1659
|
+
}
|
|
1660
|
+
const after = await (0, inspect_1.measureFiber)(entry.node.fiber);
|
|
1661
|
+
if (partial) {
|
|
1662
|
+
if (!outcome.moved)
|
|
1663
|
+
return {};
|
|
1664
|
+
return after === null ? { scrolled: true, unverified: true } : { scrolled: true };
|
|
1665
|
+
}
|
|
1666
|
+
// A list may have unmounted the row on the way: the caller finds it again, and checks it is on screen.
|
|
1667
|
+
if (after === null && outcome.moved)
|
|
1668
|
+
return { scrolled: true, unverified: true };
|
|
1669
|
+
if (after === null || visiblePart(after, snapshot.screen) === null) {
|
|
1670
|
+
throw notActionable('off_screen', `${name} is off screen (${where}), and scrolling its scroll view${outcome.moved ? '' : ' (already as far as it goes)'} didn't bring it on screen; pass force: true to call its handler anyway.`);
|
|
1671
|
+
}
|
|
1672
|
+
return outcome.moved ? { scrolled: true } : {};
|
|
1673
|
+
}
|
|
1674
|
+
/** Whether a partly visible element is off the screen edge or under a bar, and would fit clear of them if scrolled. */
|
|
1675
|
+
function isNotClear(snapshot, entry, frame) {
|
|
1676
|
+
const navBar = (0, inspect_1.nativeNavBar)(snapshot);
|
|
1677
|
+
const tabBar = (0, inspect_1.nativeTabBar)(snapshot);
|
|
1678
|
+
const clearTop = navBar && !(0, inspect_1.inNavBarHeader)(navBar, entry.node.fiber) ? navBar.bottom : 0;
|
|
1679
|
+
const clearBottom = tabBar ? tabBar.top : snapshot.screen.height;
|
|
1680
|
+
// Taller or wider than the room there is: no scroll makes it clear, so it is left as it is.
|
|
1681
|
+
if (frame.height > clearBottom - clearTop || frame.width > snapshot.screen.width)
|
|
1682
|
+
return false;
|
|
1683
|
+
const shift = shiftsFor(snapshot, entry)(frame, null);
|
|
1684
|
+
return Math.abs(shift.x) >= 1 || Math.abs(shift.y) >= 1;
|
|
1685
|
+
}
|
|
1686
|
+
/** A target that finds an element again by what it shows: the app's own target, else its id, label or text. */
|
|
1687
|
+
function refindTarget(entry, original) {
|
|
1688
|
+
if ((0, types_1.isRecord)(original) && original.ref === undefined && original.x === undefined && original.y === undefined)
|
|
1689
|
+
return original;
|
|
1690
|
+
const { testID, label, text } = entry.info;
|
|
1691
|
+
if (testID)
|
|
1692
|
+
return { testID };
|
|
1693
|
+
if (label)
|
|
1694
|
+
return { label };
|
|
1695
|
+
if (text !== '')
|
|
1696
|
+
return { text };
|
|
1697
|
+
return null;
|
|
1698
|
+
}
|
|
1699
|
+
/**
|
|
1700
|
+
* The same element after a scroll, from a fresh snapshot (the list may have laid out other rows on the
|
|
1701
|
+
* way). When a list unmounted the row, the ref is gone: it is found again by the original target.
|
|
1702
|
+
*/
|
|
1703
|
+
async function reresolve(entry, hint, original, checkSame = true) {
|
|
1704
|
+
let found;
|
|
1705
|
+
try {
|
|
1706
|
+
found = await resolveTarget({ ref: (0, inspect_1.refFor)(entry.node.fiber) }, hint);
|
|
1707
|
+
}
|
|
1708
|
+
catch (error) {
|
|
1709
|
+
if (!(error instanceof types_1.TaplineError) || error.code !== 'not_found')
|
|
1710
|
+
throw error;
|
|
1711
|
+
const again = refindTarget(entry, original);
|
|
1712
|
+
if (again === null)
|
|
1713
|
+
throw error;
|
|
1714
|
+
found = await resolveTarget(again, hint);
|
|
1715
|
+
}
|
|
1716
|
+
if (checkSame)
|
|
1717
|
+
assertSameTarget(entry, found.entry);
|
|
1718
|
+
return found;
|
|
1719
|
+
}
|
|
1720
|
+
/** The same element by id, label and text, each on its own (a row can keep its id and label while its text changes). */
|
|
1721
|
+
function sameTarget(before, after) {
|
|
1722
|
+
return before.info.testID === after.info.testID && before.info.label === after.info.label && before.info.text === after.info.text;
|
|
1723
|
+
}
|
|
1724
|
+
/**
|
|
1725
|
+
* After a scroll the element found again must still be the one the safety check saw: a list that recycles its rows
|
|
1726
|
+
* can hand the same component another row ("Continue" becomes "Delete account"). A changed id, label or text is
|
|
1727
|
+
* refused before anything is touched (Codex review of 1.4.0).
|
|
1728
|
+
*/
|
|
1729
|
+
function assertSameTarget(before, after) {
|
|
1730
|
+
if (sameTarget(before, after))
|
|
1731
|
+
return;
|
|
1732
|
+
const was = { id: before.info.testID, name: labelOf(before.info) };
|
|
1733
|
+
const now = { id: after.info.testID, name: labelOf(after.info) };
|
|
1734
|
+
if (was.name === now.name) {
|
|
1735
|
+
was.name = before.info.text;
|
|
1736
|
+
now.name = after.info.text;
|
|
1737
|
+
}
|
|
1738
|
+
throw new types_1.TaplineError('not_found', `the element changed while the list scrolled (it was ${JSON.stringify(was.name || was.id || "?")}, now ${JSON.stringify(now.name || now.id || "?")}); nothing was touched: look, then try again`);
|
|
1739
|
+
}
|
|
1740
|
+
/** After an element was found again because it could not be measured: it must now be on screen. */
|
|
1741
|
+
async function assertRefoundOnScreen(snapshot, entry, reach) {
|
|
1742
|
+
if (!reach.unverified)
|
|
1743
|
+
return;
|
|
1744
|
+
const frame = await (0, inspect_1.measureFiber)(entry.node.fiber);
|
|
1745
|
+
if (frame === null || visiblePart(frame, snapshot.screen) === null) {
|
|
1746
|
+
throw notActionable('off_screen', `${describe(entry)} is off screen, and scrolling its scroll view didn't bring it on screen; pass force: true to call its handler anyway.`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
/** `checkedOrForced`, and a refusal that comes after Tapline scrolled to the element says so. */
|
|
1750
|
+
async function checkedAfterScroll(force, reach, check) {
|
|
1751
|
+
try {
|
|
1752
|
+
return await checkedOrForced(force, check);
|
|
1753
|
+
}
|
|
1754
|
+
catch (error) {
|
|
1755
|
+
if (reach.scrolled && error instanceof types_1.TaplineError && error.code === 'not_actionable') {
|
|
1756
|
+
throw new types_1.TaplineError(error.code, `${error.message} (after scrolling to it)`, error.refs, error.candidates);
|
|
1757
|
+
}
|
|
1758
|
+
throw error;
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1413
1761
|
async function scroll(rawParams) {
|
|
1414
1762
|
const params = paramsOf(rawParams);
|
|
1415
1763
|
const to = params.to;
|
|
@@ -1615,18 +1963,40 @@ async function toggle(rawParams) {
|
|
|
1615
1963
|
const params = paramsOf(rawParams);
|
|
1616
1964
|
if (params.value !== undefined && typeof params.value !== 'boolean')
|
|
1617
1965
|
throw badParams('value must be a boolean');
|
|
1618
|
-
const
|
|
1619
|
-
const
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1966
|
+
const first = await resolveTarget(params.target, 'toggle', params.noScroll === true);
|
|
1967
|
+
const plan = (snap, picked) => {
|
|
1968
|
+
const found = findActionable(snap, picked, 'toggle', true);
|
|
1969
|
+
if (!found)
|
|
1970
|
+
throw notActionable('no_action', `${describe(picked)} is not a switch`);
|
|
1971
|
+
if (found.info.disabled)
|
|
1972
|
+
throw notActionable('disabled', `${describe(found)} is disabled; ${ENABLE_FIRST}.`);
|
|
1973
|
+
const own = (0, inspect_1.findHandler)(found.node, 'onValueChange');
|
|
1974
|
+
if (!own)
|
|
1975
|
+
throw notActionable('no_action', `${describe(found)} is not a switch`);
|
|
1976
|
+
return { control: found, handler: own };
|
|
1977
|
+
};
|
|
1978
|
+
let snapshot = first.snapshot;
|
|
1979
|
+
let planned = plan(snapshot, first.entry);
|
|
1980
|
+
// Off screen, in the tree: scroll it into view first (AGENT-TOOLS 1.12 §16.2). The policy ran before this call.
|
|
1981
|
+
let reach = {};
|
|
1982
|
+
if (params.force !== true) {
|
|
1983
|
+
assertNoBlockingAlert(planned.control);
|
|
1984
|
+
reach = await scrollIntoReach(snapshot, planned.control, params.dryRun === true, params.noScroll === true);
|
|
1985
|
+
if (reach.scrolled) {
|
|
1986
|
+
const again = await reresolve(planned.control, 'toggle', params.target);
|
|
1987
|
+
snapshot = again.snapshot;
|
|
1988
|
+
planned = plan(snapshot, again.entry);
|
|
1989
|
+
await assertRefoundOnScreen(snapshot, planned.control, reach);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
const { control, handler } = planned;
|
|
1627
1993
|
const current = control.info.checked === true;
|
|
1628
1994
|
const next = typeof params.value === 'boolean' ? params.value : !current;
|
|
1629
|
-
|
|
1995
|
+
if (reach.scrolls) {
|
|
1996
|
+
const { element } = await elementFor(control);
|
|
1997
|
+
return { element, value: next, dryRun: true, scrolls: true, ...(reach.point ? { point: reach.point } : {}), settledMs: 0 };
|
|
1998
|
+
}
|
|
1999
|
+
const forced = await checkedAfterScroll(params.force === true, reach, async () => assertReachable(snapshot, control, await (0, inspect_1.measureFiber)(control.node.fiber)));
|
|
1630
2000
|
if (params.dryRun === true) {
|
|
1631
2001
|
const { element, frame } = await elementFor(control);
|
|
1632
2002
|
return { element, value: next, dryRun: true, ...pointOf(frame), ...forced, settledMs: 0 };
|
|
@@ -1634,13 +2004,13 @@ async function toggle(rawParams) {
|
|
|
1634
2004
|
if (typeof params.value === 'boolean' && control.info.checked === params.value) {
|
|
1635
2005
|
// Already there: a person wouldn't touch it, so the handler isn't called and the keyboard stays.
|
|
1636
2006
|
const { element } = await elementFor(control);
|
|
1637
|
-
return { element, value: params.value, unchanged: true, settledMs: 0 };
|
|
2007
|
+
return { element, value: params.value, unchanged: true, ...(reach.scrolled ? { scrolled: true } : {}), settledMs: 0 };
|
|
1638
2008
|
}
|
|
1639
2009
|
const closedKeyboard = closeKeyboardFor(control);
|
|
1640
2010
|
callHandler('onValueChange', handler, [next]);
|
|
1641
2011
|
const outcome = await settleOutcome();
|
|
1642
2012
|
const { element } = await elementFor(control);
|
|
1643
|
-
return { element: { ...element, checked: next }, value: next, ...keyboardNote(closedKeyboard), ...forced, ...outcome };
|
|
2013
|
+
return { element: { ...element, checked: next }, value: next, ...(reach.scrolled ? { scrolled: true } : {}), ...keyboardNote(closedKeyboard), ...forced, ...outcome };
|
|
1644
2014
|
}
|
|
1645
2015
|
/* -------------------------------------------------------------------------- */
|
|
1646
2016
|
/* openURL / logs / reload / screenshot */
|
|
@@ -1742,6 +2112,68 @@ async function audit(rawParams) {
|
|
|
1742
2112
|
}
|
|
1743
2113
|
return { issues, checked: [...types] };
|
|
1744
2114
|
}
|
|
2115
|
+
/** `ux_check {wayBack?, earlier?}` (AGENT-TOOLS 1.12 §17.3): the screen against the usability basics. */
|
|
2116
|
+
async function uxCheck(rawParams) {
|
|
2117
|
+
const params = paramsOf(rawParams);
|
|
2118
|
+
if (params.wayBack !== undefined && typeof params.wayBack !== 'boolean')
|
|
2119
|
+
throw badParams('wayBack must be true or false');
|
|
2120
|
+
const earlierRaw = params.earlier;
|
|
2121
|
+
let earlier;
|
|
2122
|
+
if (earlierRaw !== undefined && earlierRaw !== null) {
|
|
2123
|
+
if (!Array.isArray(earlierRaw))
|
|
2124
|
+
throw badParams('earlier must be a list of {label, id?}');
|
|
2125
|
+
earlier = [];
|
|
2126
|
+
for (const item of earlierRaw) {
|
|
2127
|
+
if (!(0, types_1.isRecord)(item) || typeof item.label !== 'string')
|
|
2128
|
+
throw badParams('earlier must be a list of {label, id?}');
|
|
2129
|
+
earlier.push({ label: item.label, ...(typeof item.id === 'string' && item.id !== '' ? { id: item.id } : {}) });
|
|
2130
|
+
}
|
|
2131
|
+
}
|
|
2132
|
+
if (params.glass !== undefined && typeof params.glass !== 'boolean')
|
|
2133
|
+
throw badParams('glass must be true or false');
|
|
2134
|
+
const result = await (0, uxcheck_1.uxScreen)({
|
|
2135
|
+
...(params.glass === true ? { glass: true } : {}),
|
|
2136
|
+
...(params.wayBack === true ? { wayBack: true } : {}),
|
|
2137
|
+
...(earlier !== undefined ? { earlier } : {}),
|
|
2138
|
+
});
|
|
2139
|
+
let sources = [];
|
|
2140
|
+
if (typeof __DEV__ !== 'undefined' && __DEV__) {
|
|
2141
|
+
const fibers = result.findings.flatMap((f) => (f.entry !== null ? [f.entry.node.fiber] : []));
|
|
2142
|
+
try {
|
|
2143
|
+
sources = fibers.length > 0 ? await (0, source_1.sourcesOf)(fibers) : [];
|
|
2144
|
+
}
|
|
2145
|
+
catch {
|
|
2146
|
+
sources = []; // Metro unreachable: the report still stands, without lines
|
|
2147
|
+
}
|
|
2148
|
+
}
|
|
2149
|
+
let next = 0;
|
|
2150
|
+
const issues = [];
|
|
2151
|
+
for (const finding of result.findings) {
|
|
2152
|
+
const source = finding.entry !== null ? sources[next++] : undefined;
|
|
2153
|
+
let element = null;
|
|
2154
|
+
if (finding.entry !== null) {
|
|
2155
|
+
try {
|
|
2156
|
+
element = (await elementFor(finding.entry, finding.frame)).element;
|
|
2157
|
+
}
|
|
2158
|
+
catch {
|
|
2159
|
+
element = null;
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
const where = element === null && finding.entry !== null
|
|
2163
|
+
? whereOf({ type: 'description', entry: finding.entry, frame: finding.frame, problem: '', fix: '' })
|
|
2164
|
+
: null;
|
|
2165
|
+
const location = source?.location ?? null;
|
|
2166
|
+
issues.push({
|
|
2167
|
+
kind: finding.kind,
|
|
2168
|
+
element,
|
|
2169
|
+
...(where !== null ? { where } : {}),
|
|
2170
|
+
problem: finding.problem,
|
|
2171
|
+
basis: finding.basis,
|
|
2172
|
+
...(location !== null ? { at: { file: location.file, line: location.line, direct: location.direct } } : {}),
|
|
2173
|
+
});
|
|
2174
|
+
}
|
|
2175
|
+
return { issues, checked: result.checked, choices: result.choices, actions: result.actions };
|
|
2176
|
+
}
|
|
1745
2177
|
async function getLogs(rawParams) {
|
|
1746
2178
|
const params = paramsOf(rawParams);
|
|
1747
2179
|
if (params.since !== undefined && typeof params.since !== 'number')
|