@buoy-gg/highlight-updates 7.0.34 → 7.0.36
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/lib/commonjs/highlight-updates/HighlightUpdatesOverlay.js +1 -1
- package/lib/commonjs/highlight-updates/components/HighlightFilterView.js +6 -6
- package/lib/commonjs/highlight-updates/components/IdentifierBadge.js +2 -2
- package/lib/commonjs/highlight-updates/components/RenderCauseBadge.js +7 -7
- package/lib/commonjs/highlight-updates/components/RenderDetailView.js +6 -6
- package/lib/commonjs/highlight-updates/components/RenderHistoryViewer.js +8 -8
- package/lib/commonjs/highlight-updates/components/RenderListItem.js +2 -2
- package/lib/commonjs/highlight-updates/components/RendersCopySettingsView.js +1 -1
- package/lib/commonjs/highlight-updates/components/StatsDisplay.js +1 -1
- package/lib/commonjs/highlight-updates/utils/screenElements.js +272 -70
- package/lib/commonjs/highlight-updates/utils/tapMatch.js +26 -1
- package/lib/commonjs/sync/highlightUpdatesSyncAdapter.js +1 -1
- package/lib/module/highlight-updates/HighlightUpdatesOverlay.js +2 -2
- package/lib/module/highlight-updates/components/HighlightFilterView.js +7 -7
- package/lib/module/highlight-updates/components/IdentifierBadge.js +3 -3
- package/lib/module/highlight-updates/components/RenderCauseBadge.js +8 -8
- package/lib/module/highlight-updates/components/RenderDetailView.js +7 -7
- package/lib/module/highlight-updates/components/RenderHistoryViewer.js +9 -9
- package/lib/module/highlight-updates/components/RenderListItem.js +3 -3
- package/lib/module/highlight-updates/components/RendersCopySettingsView.js +2 -2
- package/lib/module/highlight-updates/components/StatsDisplay.js +2 -2
- package/lib/module/highlight-updates/utils/screenElements.js +272 -70
- package/lib/module/highlight-updates/utils/tapMatch.js +25 -1
- package/lib/module/sync/highlightUpdatesSyncAdapter.js +1 -1
- package/lib/typescript/highlight-updates/components/HighlightFilterView.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/components/IdentifierBadge.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/components/RenderDetailView.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/components/RenderHistoryViewer.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/components/RendersCopySettingsView.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/utils/screenElements.d.ts +52 -1
- package/lib/typescript/highlight-updates/utils/screenElements.d.ts.map +1 -1
- package/lib/typescript/highlight-updates/utils/tapMatch.d.ts +11 -0
- package/lib/typescript/highlight-updates/utils/tapMatch.d.ts.map +1 -1
- package/lib/typescript/sync/highlightUpdatesSyncAdapter.d.ts +1 -1
- package/lib/typescript/sync/highlightUpdatesSyncAdapter.d.ts.map +1 -1
- package/package.json +7 -7
|
@@ -40,6 +40,69 @@ function getReactDevToolsHook() {
|
|
|
40
40
|
return global.__REACT_DEVTOOLS_GLOBAL_HOOK__ ?? null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* React commits observed since this counter was installed.
|
|
45
|
+
*
|
|
46
|
+
* `tapElement` reads it either side of firing a handler to say whether
|
|
47
|
+
* ANYTHING re-rendered afterwards. Without that a tap that matched a button
|
|
48
|
+
* on the screen BENEATH the current one (a stack keeps it mounted) came back
|
|
49
|
+
* `tapped:true` — the handler really did run — and an agent then built its
|
|
50
|
+
* next three turns on a sign-in that never happened. Zero commits inside the
|
|
51
|
+
* settle window is the honest signal: the handler ran and the app did nothing
|
|
52
|
+
* visible with it.
|
|
53
|
+
*
|
|
54
|
+
* Installed once and chained, the same way CommitProfiler wraps the hook;
|
|
55
|
+
* counting is the only work it does on the commit path.
|
|
56
|
+
*/
|
|
57
|
+
let commitCount = 0;
|
|
58
|
+
let commitCounterInstalled = false;
|
|
59
|
+
function ensureCommitCounter() {
|
|
60
|
+
if (commitCounterInstalled) return;
|
|
61
|
+
const hook = getReactDevToolsHook();
|
|
62
|
+
if (!hook) return;
|
|
63
|
+
const original = hook.onCommitFiberRoot;
|
|
64
|
+
hook.onCommitFiberRoot = function (...args) {
|
|
65
|
+
commitCount++;
|
|
66
|
+
return typeof original === "function" ? original.apply(hook, args) : undefined;
|
|
67
|
+
};
|
|
68
|
+
commitCounterInstalled = true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** How long a tap waits to see the app react before reporting. */
|
|
72
|
+
const TAP_SETTLE_MS = 250;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Per-walk memo of "is this fiber under Buoy's own overlay". Every candidate
|
|
76
|
+
* asks, and they share ancestors, so the answer is cached along the path
|
|
77
|
+
* walked rather than recomputed 400 levels deep per row.
|
|
78
|
+
*/
|
|
79
|
+
function makeChromeCheck() {
|
|
80
|
+
const memo = new Map();
|
|
81
|
+
return fiber => {
|
|
82
|
+
const path = [];
|
|
83
|
+
let cur = fiber;
|
|
84
|
+
let depth = 0;
|
|
85
|
+
let inChrome = false;
|
|
86
|
+
while (cur && depth < 400) {
|
|
87
|
+
const known = memo.get(cur);
|
|
88
|
+
if (known !== undefined) {
|
|
89
|
+
inChrome = known;
|
|
90
|
+
break;
|
|
91
|
+
}
|
|
92
|
+
if (getProps(cur)?.__buoyDevToolsRoot) {
|
|
93
|
+
inChrome = true;
|
|
94
|
+
memo.set(cur, true);
|
|
95
|
+
break;
|
|
96
|
+
}
|
|
97
|
+
path.push(cur);
|
|
98
|
+
cur = cur.return;
|
|
99
|
+
depth++;
|
|
100
|
+
}
|
|
101
|
+
for (const f of path) memo.set(f, inChrome);
|
|
102
|
+
return inChrome;
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
43
106
|
/**
|
|
44
107
|
* Collect fiber roots across ALL renderers. A single renderer id is not safe
|
|
45
108
|
* to assume: secondary reconcilers (react-native-skia, react-native-svg, …)
|
|
@@ -258,13 +321,48 @@ function getOwningComponentName(fiber) {
|
|
|
258
321
|
function getProps(fiber) {
|
|
259
322
|
return fiber?.memoizedProps ?? fiber?.pendingProps ?? null;
|
|
260
323
|
}
|
|
324
|
+
|
|
325
|
+
/**
|
|
326
|
+
* The text of a `children` value, INCLUDING one assembled from several pieces.
|
|
327
|
+
*
|
|
328
|
+
* `<Text>{value} {label}</Text>` compiles to `children: ["1,450", " ", "Points"]`
|
|
329
|
+
* and `Check Back Soon{"\n"}For Available Offers` to three strings. This used
|
|
330
|
+
* to accept a lone string or number and nothing else, so EVERY composed label
|
|
331
|
+
* in an app was invisible to describeScreen, tapElement and waitFor — while
|
|
332
|
+
* native accessibility, which concatenates the same children, could read them.
|
|
333
|
+
* Measured on a live QA run: five waits failed against an empty state that was
|
|
334
|
+
* on screen the whole time, and the agent then reported the app was missing
|
|
335
|
+
* one; a points balance could not be found by its own number.
|
|
336
|
+
*
|
|
337
|
+
* Nested React ELEMENTS are not walked here — `collectDescendantText` owns
|
|
338
|
+
* that, with its stop-at-a-nested-button rule — so this stays a leaf join, and
|
|
339
|
+
* adjacent pieces are concatenated exactly as React renders them.
|
|
340
|
+
*/
|
|
341
|
+
function flattenText(value, depth = 0) {
|
|
342
|
+
if (typeof value === "string") return value;
|
|
343
|
+
if (typeof value === "number" && Number.isFinite(value)) return String(value);
|
|
344
|
+
if (Array.isArray(value) && depth < 4) {
|
|
345
|
+
const parts = [];
|
|
346
|
+
for (const child of value) {
|
|
347
|
+
const part = flattenText(child, depth + 1);
|
|
348
|
+
if (part) parts.push(part);
|
|
349
|
+
}
|
|
350
|
+
return parts.length > 0 ? parts.join("") : undefined;
|
|
351
|
+
}
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/** What a person reads: newlines and runs of spaces collapse to one space. */
|
|
356
|
+
function normalizeVisibleText(value) {
|
|
357
|
+
return value.replace(/\s+/g, " ").trim();
|
|
358
|
+
}
|
|
261
359
|
function getVisibleText(props) {
|
|
262
360
|
if (!props) return undefined;
|
|
263
361
|
const pick = v => {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
return undefined;
|
|
362
|
+
const flat = flattenText(v);
|
|
363
|
+
if (flat === undefined) return undefined;
|
|
364
|
+
const text = normalizeVisibleText(flat);
|
|
365
|
+
return text ? text.slice(0, 80) : undefined;
|
|
268
366
|
};
|
|
269
367
|
return pick(props.children) ?? pick(props.title) ?? pick(props.label) ?? pick(props.placeholder) ?? undefined;
|
|
270
368
|
}
|
|
@@ -378,7 +476,19 @@ function collectCandidates() {
|
|
|
378
476
|
const roots = getAllFiberRoots();
|
|
379
477
|
if (roots.length === 0) return [];
|
|
380
478
|
const out = [];
|
|
381
|
-
|
|
479
|
+
/**
|
|
480
|
+
* De-dup indexes. Two fibers resolving to the same host node collapse to one
|
|
481
|
+
* row — a Pressable and its inner View, but ALSO the same native view reached
|
|
482
|
+
* twice by the walk, which produces two different `instance` objects for one
|
|
483
|
+
* `nativeTag`. Measured on a live QA run: a 141-row screen dump held 70
|
|
484
|
+
* distinct elements, every one listed twice, and the agent told the user
|
|
485
|
+
* their layout had duplicate elements. Keyed both ways so either collision
|
|
486
|
+
* collapses, and as Maps because the old `out.find(…)` was a linear scan per
|
|
487
|
+
* candidate on a list that can run to hundreds.
|
|
488
|
+
*/
|
|
489
|
+
const byInstance = new Map();
|
|
490
|
+
const byTag = new Map();
|
|
491
|
+
const inBuoyChrome = makeChromeCheck();
|
|
382
492
|
for (const root of roots) {
|
|
383
493
|
traverseFiberTree(root.current, fiber => {
|
|
384
494
|
const props = getProps(fiber);
|
|
@@ -436,25 +546,25 @@ function collectCandidates() {
|
|
|
436
546
|
text = collectDescendantText(fiber, 6);
|
|
437
547
|
}
|
|
438
548
|
const instance = host?.instance ?? null;
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
if (
|
|
442
|
-
|
|
443
|
-
if (existing) {
|
|
444
|
-
|
|
445
|
-
existing.press.longPress = press.longPress;
|
|
446
|
-
}
|
|
447
|
-
if (!existing.valueControl && valueControl) existing.valueControl = valueControl;
|
|
448
|
-
if (!existing.textControl && textControl) existing.textControl = textControl;
|
|
449
|
-
if (!existing.testID && testID) existing.testID = testID;
|
|
450
|
-
if (!existing.label && label) existing.label = label;
|
|
451
|
-
if (!existing.text && text) existing.text = text;
|
|
452
|
-
return;
|
|
549
|
+
const nativeTag = host?.nativeTag ?? null;
|
|
550
|
+
const existing = (instance ? byInstance.get(instance) : undefined) ?? (nativeTag != null ? byTag.get(nativeTag) : undefined);
|
|
551
|
+
if (existing) {
|
|
552
|
+
// Keep the more interactive / better-labelled of the two.
|
|
553
|
+
if (!existing.press && press) existing.press = press;else if (existing.press && press?.longPress && !existing.press.longPress) {
|
|
554
|
+
existing.press.longPress = press.longPress;
|
|
453
555
|
}
|
|
556
|
+
if (!existing.valueControl && valueControl) existing.valueControl = valueControl;
|
|
557
|
+
if (!existing.textControl && textControl) existing.textControl = textControl;
|
|
558
|
+
if (!existing.testID && testID) existing.testID = testID;
|
|
559
|
+
if (!existing.label && label) existing.label = label;
|
|
560
|
+
if (!existing.text && text) existing.text = text;
|
|
561
|
+
// Index the alias too, so a third sighting through either key finds it.
|
|
562
|
+
if (instance && !byInstance.has(instance)) byInstance.set(instance, existing);
|
|
563
|
+
if (nativeTag != null && !byTag.has(nativeTag)) byTag.set(nativeTag, existing);
|
|
564
|
+
return;
|
|
454
565
|
}
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
nativeTag: host?.nativeTag ?? null,
|
|
566
|
+
const candidate = {
|
|
567
|
+
nativeTag,
|
|
458
568
|
name,
|
|
459
569
|
role,
|
|
460
570
|
testID,
|
|
@@ -466,12 +576,48 @@ function collectCandidates() {
|
|
|
466
576
|
press,
|
|
467
577
|
valueControl,
|
|
468
578
|
textControl,
|
|
469
|
-
rect: null
|
|
470
|
-
|
|
579
|
+
rect: null,
|
|
580
|
+
buoy: inBuoyChrome(fiber)
|
|
581
|
+
};
|
|
582
|
+
out.push(candidate);
|
|
583
|
+
if (instance) byInstance.set(instance, candidate);
|
|
584
|
+
if (nativeTag != null) byTag.set(nativeTag, candidate);
|
|
471
585
|
});
|
|
472
586
|
}
|
|
473
587
|
return out;
|
|
474
588
|
}
|
|
589
|
+
|
|
590
|
+
/**
|
|
591
|
+
* Whether the caller named THIS element, rather than described one like it.
|
|
592
|
+
* An exact nativeTag or testID is a deliberate choice, and is honoured even
|
|
593
|
+
* inside Buoy's own chrome — that is how a test or an editor agent drives a
|
|
594
|
+
* Buoy tool on purpose. A fuzzy `query` is not.
|
|
595
|
+
*/
|
|
596
|
+
function explicitlyTargets(c, params) {
|
|
597
|
+
if (params.nativeTag != null) return c.nativeTag === params.nativeTag;
|
|
598
|
+
if (params.testID && c.testID) return c.testID.toLowerCase() === params.testID.toLowerCase();
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* The app under test, not the tool looking at it.
|
|
604
|
+
*
|
|
605
|
+
* Buoy runs INSIDE the app, so its own sheet is in the same fiber tree as the
|
|
606
|
+
* screens it inspects — and Ask Buoy's activity log echoes whatever the model
|
|
607
|
+
* is searching for back as visible text. Left in, that echo was the best
|
|
608
|
+
* match for the next fuzzy tap or wait, so every text-driven action landed on
|
|
609
|
+
* Buoy's own panel and nothing in the app moved. The agent diagnosed it
|
|
610
|
+
* itself, four chats in ("every text-based tap and wait I've done this
|
|
611
|
+
* session has been landing on Buoy's panel"), and still could not escape it.
|
|
612
|
+
*
|
|
613
|
+
* `includeBuoy` lists the chrome for callers that want it; an exact id gets
|
|
614
|
+
* through regardless (see `explicitlyTargets`).
|
|
615
|
+
*/
|
|
616
|
+
function appCandidates(params) {
|
|
617
|
+
const all = collectCandidates();
|
|
618
|
+
if (params.includeBuoy) return all;
|
|
619
|
+
return all.filter(c => !c.buoy || explicitlyTargets(c, params));
|
|
620
|
+
}
|
|
475
621
|
async function measureCandidates(candidates) {
|
|
476
622
|
await Promise.all(candidates.map(async c => {
|
|
477
623
|
if (c.instance) c.rect = await measureInstance(c.instance);
|
|
@@ -480,8 +626,10 @@ async function measureCandidates(candidates) {
|
|
|
480
626
|
|
|
481
627
|
// --- describeScreen -------------------------------------------------------
|
|
482
628
|
|
|
483
|
-
export async function describeScreen() {
|
|
484
|
-
const
|
|
629
|
+
export async function describeScreen(params = {}) {
|
|
630
|
+
const all = collectCandidates();
|
|
631
|
+
const candidates = params.includeBuoy ? all : all.filter(c => !c.buoy);
|
|
632
|
+
const hiddenBuoy = all.length - candidates.length;
|
|
485
633
|
await measureCandidates(candidates);
|
|
486
634
|
const win = Dimensions.get("window");
|
|
487
635
|
const screenW = Math.round(win.width) || 1;
|
|
@@ -543,7 +691,10 @@ export async function describeScreen() {
|
|
|
543
691
|
height: screenH
|
|
544
692
|
},
|
|
545
693
|
count: elements.length,
|
|
546
|
-
elements
|
|
694
|
+
elements,
|
|
695
|
+
...(hiddenBuoy > 0 ? {
|
|
696
|
+
hiddenBuoy
|
|
697
|
+
} : {})
|
|
547
698
|
};
|
|
548
699
|
}
|
|
549
700
|
function round2(n) {
|
|
@@ -599,7 +750,7 @@ function makeSyntheticPressEvent(rect) {
|
|
|
599
750
|
};
|
|
600
751
|
}
|
|
601
752
|
export async function tapElement(params) {
|
|
602
|
-
const candidates =
|
|
753
|
+
const candidates = appCandidates(params);
|
|
603
754
|
if (candidates.length === 0) {
|
|
604
755
|
return {
|
|
605
756
|
tapped: false,
|
|
@@ -648,6 +799,21 @@ export async function tapElement(params) {
|
|
|
648
799
|
// sit below 15 text-score peers.
|
|
649
800
|
const target = top.find(r => isInteractive(r.c)) ?? ranked.find(r => isInteractive(r.c)) ?? top[0];
|
|
650
801
|
const c = target.c;
|
|
802
|
+
|
|
803
|
+
// Everything else that matched just as well. A fuzzy "SIGN IN" on a stack
|
|
804
|
+
// that still has the home screen mounted underneath has two equal answers,
|
|
805
|
+
// and the caller has to know that before trusting whichever one fired.
|
|
806
|
+
const sameControl = (a, b) => a.press && a.press.handler === b.press?.handler || a.valueControl && a.valueControl.handler === b.valueControl?.handler || a.textControl && a.textControl.handler === b.textControl?.handler;
|
|
807
|
+
const alsoMatched = top.filter(r => r !== target && isInteractive(r.c) && r.score === target.score)
|
|
808
|
+
// The label Text inside the tapped button matches its own button's text
|
|
809
|
+
// and inherits its onPress: same control, not a rival.
|
|
810
|
+
.filter(r => !sameControl(r.c, c)).filter((r, i, arr) => arr.findIndex(o => sameControl(o.c, r.c)) === i).slice(0, 3).map(r => ({
|
|
811
|
+
nativeTag: r.c.nativeTag,
|
|
812
|
+
name: r.c.name,
|
|
813
|
+
testID: r.c.testID,
|
|
814
|
+
text: r.c.text,
|
|
815
|
+
tap: tapPoint(r.c.rect, screenW, screenH)
|
|
816
|
+
}));
|
|
651
817
|
if (!isInteractive(c)) {
|
|
652
818
|
return {
|
|
653
819
|
tapped: false,
|
|
@@ -685,6 +851,28 @@ export async function tapElement(params) {
|
|
|
685
851
|
label: c.label,
|
|
686
852
|
text: c.text
|
|
687
853
|
};
|
|
854
|
+
|
|
855
|
+
// Fire, then watch. The handler is the app's own; what it did — or did
|
|
856
|
+
// not do — shows up as commits (or none) in the settle window.
|
|
857
|
+
ensureCommitCounter();
|
|
858
|
+
// Read BEFORE the handler runs: a synchronous setState commits inside it.
|
|
859
|
+
const c0 = commitCount;
|
|
860
|
+
const t0 = Date.now();
|
|
861
|
+
const fired = async matched => {
|
|
862
|
+
await new Promise(resolve => setTimeout(resolve, TAP_SETTLE_MS));
|
|
863
|
+
return {
|
|
864
|
+
tapped: true,
|
|
865
|
+
scrolled,
|
|
866
|
+
matched,
|
|
867
|
+
effect: {
|
|
868
|
+
commits: commitCount - c0,
|
|
869
|
+
observedMs: Date.now() - t0
|
|
870
|
+
},
|
|
871
|
+
...(alsoMatched.length ? {
|
|
872
|
+
alsoMatched
|
|
873
|
+
} : {})
|
|
874
|
+
};
|
|
875
|
+
};
|
|
688
876
|
try {
|
|
689
877
|
if (c.press) {
|
|
690
878
|
if (params.longPress) {
|
|
@@ -701,24 +889,16 @@ export async function tapElement(params) {
|
|
|
701
889
|
};
|
|
702
890
|
}
|
|
703
891
|
lp(makeSyntheticPressEvent(c.rect));
|
|
704
|
-
return {
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
...base,
|
|
709
|
-
via: "onLongPress"
|
|
710
|
-
}
|
|
711
|
-
};
|
|
892
|
+
return await fired({
|
|
893
|
+
...base,
|
|
894
|
+
via: "onLongPress"
|
|
895
|
+
});
|
|
712
896
|
}
|
|
713
897
|
c.press.handler(makeSyntheticPressEvent(c.rect));
|
|
714
|
-
return {
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
...base,
|
|
719
|
-
via: c.press.kind
|
|
720
|
-
}
|
|
721
|
-
};
|
|
898
|
+
return await fired({
|
|
899
|
+
...base,
|
|
900
|
+
via: c.press.kind
|
|
901
|
+
});
|
|
722
902
|
}
|
|
723
903
|
if (c.valueControl) {
|
|
724
904
|
const vc = c.valueControl;
|
|
@@ -736,28 +916,20 @@ export async function tapElement(params) {
|
|
|
736
916
|
}
|
|
737
917
|
}
|
|
738
918
|
vc.handler(next);
|
|
739
|
-
return {
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
via: "onValueChange",
|
|
745
|
-
value: next
|
|
746
|
-
}
|
|
747
|
-
};
|
|
919
|
+
return await fired({
|
|
920
|
+
...base,
|
|
921
|
+
via: "onValueChange",
|
|
922
|
+
value: next
|
|
923
|
+
});
|
|
748
924
|
}
|
|
749
925
|
// textControl
|
|
750
926
|
const next = params.text ?? (typeof params.value === "string" ? params.value : "");
|
|
751
927
|
c.textControl.handler(next);
|
|
752
|
-
return {
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
via: "onChangeText",
|
|
758
|
-
value: next
|
|
759
|
-
}
|
|
760
|
-
};
|
|
928
|
+
return await fired({
|
|
929
|
+
...base,
|
|
930
|
+
via: "onChangeText",
|
|
931
|
+
value: next
|
|
932
|
+
});
|
|
761
933
|
} catch (e) {
|
|
762
934
|
return {
|
|
763
935
|
tapped: false,
|
|
@@ -806,7 +978,9 @@ const WAIT_DEFAULT_POLL_MS = 150;
|
|
|
806
978
|
* waiting for.
|
|
807
979
|
*/
|
|
808
980
|
export async function waitFor(params) {
|
|
809
|
-
|
|
981
|
+
// Coerced, not optional-chained: a model sends `query: 1450` often enough,
|
|
982
|
+
// and `.trim()` on a number throws before the wait ever starts.
|
|
983
|
+
const hasTarget = params.nativeTag != null || !!String(params.testID ?? "").trim() || !!String(params.query ?? "").trim();
|
|
810
984
|
if (!hasTarget) {
|
|
811
985
|
return {
|
|
812
986
|
ok: false,
|
|
@@ -822,7 +996,10 @@ export async function waitFor(params) {
|
|
|
822
996
|
let polls = 0;
|
|
823
997
|
for (;;) {
|
|
824
998
|
polls += 1;
|
|
825
|
-
const
|
|
999
|
+
const {
|
|
1000
|
+
hit,
|
|
1001
|
+
closest
|
|
1002
|
+
} = await findOnScreen(params);
|
|
826
1003
|
if (wantGone ? !hit : !!hit) {
|
|
827
1004
|
return {
|
|
828
1005
|
ok: true,
|
|
@@ -832,12 +1009,18 @@ export async function waitFor(params) {
|
|
|
832
1009
|
};
|
|
833
1010
|
}
|
|
834
1011
|
if (Date.now() - started >= timeoutMs) {
|
|
1012
|
+
const nearMisses = closest?.length ? closest : undefined;
|
|
1013
|
+
// Say which of the two failures this is. "Nothing is called that" and
|
|
1014
|
+
// "it exists but is not visible" need opposite next moves, and a caller
|
|
1015
|
+
// that cannot tell them apart tries the same wait with new words.
|
|
1016
|
+
const why = wantGone ? `Still on screen after ${timeoutMs}ms.` : nearMisses ? `Not on screen after ${timeoutMs}ms — ${nearMisses.length === 1 ? "one element" : `${nearMisses.length} elements`} matched but ${nearMisses.some(n => n.onScreen) ? "not well enough" : "none is visible"}. See \`closest\`; do not retry with different words, call describeScreen.` : `Not on screen after ${timeoutMs}ms, and nothing matched those words at all. Call describeScreen to see what is there.`;
|
|
835
1017
|
return {
|
|
836
1018
|
ok: false,
|
|
837
1019
|
waitedMs: Date.now() - started,
|
|
838
1020
|
polls,
|
|
839
|
-
reason:
|
|
840
|
-
matched: hit ?? undefined
|
|
1021
|
+
reason: why,
|
|
1022
|
+
matched: hit ?? undefined,
|
|
1023
|
+
closest: nearMisses
|
|
841
1024
|
};
|
|
842
1025
|
}
|
|
843
1026
|
await new Promise(resolve => setTimeout(resolve, pollMs));
|
|
@@ -846,22 +1029,26 @@ export async function waitFor(params) {
|
|
|
846
1029
|
|
|
847
1030
|
/** One scan: rank by identity, then confirm the best match is really visible. */
|
|
848
1031
|
async function findOnScreen(params) {
|
|
849
|
-
const ranked =
|
|
1032
|
+
const ranked = appCandidates(params).map(c => ({
|
|
850
1033
|
c,
|
|
851
1034
|
score: matchScore(c, params)
|
|
852
1035
|
})).filter(r => r.score > 0).sort((a, b) => b.score - a.score)
|
|
853
1036
|
// Only the plausible few get measured; a broad query can match dozens.
|
|
854
1037
|
.slice(0, 8);
|
|
855
|
-
if (ranked.length === 0) return
|
|
1038
|
+
if (ranked.length === 0) return {
|
|
1039
|
+
hit: null,
|
|
1040
|
+
closest: undefined
|
|
1041
|
+
};
|
|
856
1042
|
await measureCandidates(ranked.map(r => r.c));
|
|
857
1043
|
const win = Dimensions.get("window");
|
|
858
1044
|
const screenW = Math.round(win.width) || 1;
|
|
859
1045
|
const screenH = Math.round(win.height) || 1;
|
|
1046
|
+
let hit = null;
|
|
860
1047
|
for (const {
|
|
861
1048
|
c
|
|
862
1049
|
} of ranked) {
|
|
863
1050
|
if (!isRectCenterOnScreen(c.rect, screenW, screenH)) continue;
|
|
864
|
-
|
|
1051
|
+
hit = {
|
|
865
1052
|
nativeTag: c.nativeTag,
|
|
866
1053
|
name: c.name,
|
|
867
1054
|
testID: c.testID,
|
|
@@ -869,8 +1056,23 @@ async function findOnScreen(params) {
|
|
|
869
1056
|
text: c.text,
|
|
870
1057
|
interactive: !!(c.press || c.valueControl || c.textControl)
|
|
871
1058
|
};
|
|
1059
|
+
break;
|
|
872
1060
|
}
|
|
873
|
-
|
|
1061
|
+
|
|
1062
|
+
// Built from the same measured ranking, so a failed wait can say what it
|
|
1063
|
+
// nearly matched and whether that thing was visible.
|
|
1064
|
+
const closest = ranked.slice(0, 3).map(({
|
|
1065
|
+
c
|
|
1066
|
+
}) => ({
|
|
1067
|
+
name: c.name,
|
|
1068
|
+
testID: c.testID,
|
|
1069
|
+
text: c.text,
|
|
1070
|
+
onScreen: isRectCenterOnScreen(c.rect, screenW, screenH)
|
|
1071
|
+
}));
|
|
1072
|
+
return {
|
|
1073
|
+
hit,
|
|
1074
|
+
closest
|
|
1075
|
+
};
|
|
874
1076
|
}
|
|
875
1077
|
|
|
876
1078
|
// --- resolveTouchTarget ---------------------------------------------------
|
|
@@ -27,15 +27,39 @@ export function isRectCenterOnScreen(rect, screenW, screenH) {
|
|
|
27
27
|
const slackY = screenH * 0.1;
|
|
28
28
|
return cx >= -slackX && cx <= screenW + slackX && cy >= -slackY && cy <= screenH + slackY;
|
|
29
29
|
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* The comparable form of a piece of text.
|
|
33
|
+
*
|
|
34
|
+
* "1,450", `1450` and "1,450 Points" are one request as far as a person is
|
|
35
|
+
* concerned. Measured on a live QA run: waits for BOTH "1,450" and 1450 failed
|
|
36
|
+
* against a balance that was on screen, because the thousands separator made
|
|
37
|
+
* every comparison miss. Lowercased, whitespace collapsed, digit-group
|
|
38
|
+
* separators dropped. A number arrives here as a number often enough that the
|
|
39
|
+
* coercion is not optional — `.trim()` on one is a crash.
|
|
40
|
+
*/
|
|
41
|
+
export function looseText(value) {
|
|
42
|
+
return String(value ?? "").toLowerCase().replace(/\s+/g, " ").replace(/(\d)[,\u00a0\u202f](?=\d{3}\b)/g, "$1").trim();
|
|
43
|
+
}
|
|
30
44
|
export function scoreTapMatch(c, params) {
|
|
31
45
|
if (params.nativeTag != null && c.nativeTag === params.nativeTag) return 1000;
|
|
32
|
-
const q = (params.query ?? params.testID ?? "").trim().toLowerCase();
|
|
46
|
+
const q = String(params.query ?? params.testID ?? "").trim().toLowerCase();
|
|
33
47
|
if (!q) return 0;
|
|
34
48
|
const fields = [c.testID, c.label, c.text, c.name].filter(Boolean).map(s => s.toLowerCase());
|
|
35
49
|
let best = 0;
|
|
36
50
|
for (const f of fields) {
|
|
37
51
|
if (f === q) best = Math.max(best, 100);else if (f.startsWith(q)) best = Math.max(best, 50);else if (f.includes(q)) best = Math.max(best, 25);
|
|
38
52
|
}
|
|
53
|
+
// Same tiers on the loose form, one notch lower so an exact hit still wins
|
|
54
|
+
// when both match. Skipped when the loose query is empty, or `includes("")`
|
|
55
|
+
// would score every candidate on the screen.
|
|
56
|
+
const looseQuery = looseText(q);
|
|
57
|
+
if (looseQuery) {
|
|
58
|
+
for (const f of fields) {
|
|
59
|
+
const loose = looseText(f);
|
|
60
|
+
if (loose === looseQuery) best = Math.max(best, 90);else if (loose.startsWith(looseQuery)) best = Math.max(best, 45);else if (loose.includes(looseQuery)) best = Math.max(best, 22);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
39
63
|
if (params.testID && c.testID && c.testID.toLowerCase() === params.testID.toLowerCase()) {
|
|
40
64
|
best = Math.max(best, 200);
|
|
41
65
|
}
|
|
@@ -200,7 +200,7 @@ export const highlightUpdatesSyncAdapter = {
|
|
|
200
200
|
// Walk the live fiber tree and return the current screen's meaningful /
|
|
201
201
|
// interactive elements, each with normalized tap coordinates. Powers the
|
|
202
202
|
// MCP `describe_screen` reader — no screenshot, no silent-tracking needed.
|
|
203
|
-
describeScreen:
|
|
203
|
+
describeScreen: params => describeScreen(params ?? {}),
|
|
204
204
|
// Tap an element purely in JS: resolve it (by nativeTag / testID / query),
|
|
205
205
|
// find the nearest onPress up its fiber chain, and invoke it with a
|
|
206
206
|
// synthetic press event. Powers the MCP `tap_element` action.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"HighlightFilterView.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/HighlightFilterView.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAgC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"HighlightFilterView.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/HighlightFilterView.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAgC,MAAM,OAAO,CAAC;AAyBrD,OAAO,KAAK,EACV,YAAY,EAGZ,qBAAqB,EAGtB,MAAM,wBAAwB,CAAC;AAOhC,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AAG1D,UAAU,wBAAwB;IAChC,OAAO,EAAE,YAAY,CAAC;IACtB,cAAc,EAAE,CAAC,OAAO,EAAE,OAAO,CAAC,YAAY,CAAC,KAAK,IAAI,CAAC;IACzD,QAAQ,EAAE,qBAAqB,CAAC;IAChC,gBAAgB,EAAE,CAAC,QAAQ,EAAE,OAAO,CAAC,qBAAqB,CAAC,KAAK,IAAI,CAAC;IACrE,cAAc,EAAE;QACd,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,OAAO,EAAE,MAAM,EAAE,CAAC;QAClB,SAAS,EAAE,MAAM,EAAE,CAAC;QACpB,cAAc,EAAE,MAAM,EAAE,CAAC;QACzB,mBAAmB,EAAE,MAAM,EAAE,CAAC;KAC/B,CAAC;IACF,SAAS,EAAE,aAAa,CAAC;CAC1B;AAwRD,wBAAgB,mBAAmB,CAAC,EAClC,OAAO,EACP,cAAc,EACd,QAAQ,EACR,gBAAgB,EAChB,cAAc,EACd,SAAS,GACV,EAAE,wBAAwB,qBAm8B1B;AAibD,eAAe,mBAAmB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"IdentifierBadge.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/IdentifierBadge.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,GAAG,
|
|
1
|
+
{"version":3,"file":"IdentifierBadge.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/IdentifierBadge.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,KAAK,MAAM,OAAO,CAAC;AAE1B,OAAO,EAAE,GAAG,EAAoE,MAAM,oBAAoB,CAAC;AAG3G,MAAM,MAAM,cAAc,GACtB,UAAU,GACV,QAAQ,GACR,UAAU,GACV,WAAW,GACX,oBAAoB,GACpB,WAAW,GACX,KAAK,CAAC;AAGV,eAAO,MAAM,iBAAiB,EAAE,MAAM,CACpC,cAAc,EACd;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,OAAO,GAAG,CAAA;CAAE,CA4CvE,CAAC;AAEF,UAAU,oBAAoB;IAC5B,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,wCAAwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wCAAwC;IACxC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,sBAAsB;IACtB,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,yBAAyB;IACzB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,wBAAgB,eAAe,CAAC,EAC9B,IAAI,EACJ,KAAK,EACL,OAAe,EACf,SAAiB,EACjB,SAAiB,EACjB,UAAkB,EAClB,QAAgB,EAChB,MAAM,GACP,EAAE,oBAAoB,qBAkEtB;AAGD,UAAU,kBAAkB;IAC1B,IAAI,EAAE,cAAc,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,wBAAgB,aAAa,CAAC,EAC5B,IAAI,EACJ,KAAK,EACL,UAAkB,EAClB,QAAe,GAChB,EAAE,kBAAkB,qBA4BpB;AAuGD,eAAe,eAAe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RenderDetailView.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/RenderDetailView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAyC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"RenderDetailView.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/RenderDetailView.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAyC,MAAM,OAAO,CAAC;AAc9D,OAAO,KAAK,EAAE,aAAa,EAAc,aAAa,EAAE,MAAM,wBAAwB,CAAC;AAIvF,UAAU,qBAAqB;IAC7B,MAAM,EAAE,aAAa,CAAC;IACtB,iEAAiE;IACjE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,yDAAyD;IACzD,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wDAAwD;IACxD,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,4DAA4D;IAC5D,WAAW,CAAC,EAAE,CAAC,OAAO,EAAE,aAAa,EAAE,IAAI,EAAE,SAAS,GAAG,SAAS,KAAK,IAAI,CAAC;IAC5E,8DAA8D;IAC9D,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAID,wBAAgB,gBAAgB,CAAC,EAC/B,MAAM,EACN,qBAA6B,EAC7B,kBAAkB,EAAE,aAAa,EACjC,kBAAkB,EAAE,gBAAgB,EACpC,WAAW,EACX,KAAa,GACd,EAAE,qBAAqB,qBA4IvB;AAqqBD,eAAe,gBAAgB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RenderHistoryViewer.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/RenderHistoryViewer.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAyC,MAAM,OAAO,CAAC;
|
|
1
|
+
{"version":3,"file":"RenderHistoryViewer.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/RenderHistoryViewer.tsx"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,KAAyC,MAAM,OAAO,CAAC;AAkB9D,OAAO,EAAiB,KAAK,aAAa,EAAoB,MAAM,wBAAwB,CAAC;AAG7F,UAAU,wBAAwB;IAChC,MAAM,EAAE,aAAa,CAAC;IACtB,uDAAuD;IACvD,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,wCAAwC;IACxC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,wCAAwC;IACxC,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,gEAAgE;IAChE,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAqBD;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,qBAA6B,EAC7B,kBAAkB,EAAE,aAAa,EACjC,kBAAkB,EAAE,gBAAgB,EACpC,KAAa,GACd,EAAE,wBAAwB,qBAiN1B;AAyRD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,EAClC,MAAM,EACN,kBAAsB,EACtB,kBAA6B,EAC7B,KAAa,GACd,EAAE;IACD,MAAM,EAAE,aAAa,CAAC;IACtB,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,kBAAkB,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC7C,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB,4BA6CA;AA+YD,eAAe,mBAAmB,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"RendersCopySettingsView.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/RendersCopySettingsView.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"RendersCopySettingsView.d.ts","sourceRoot":"","sources":["../../../../src/highlight-updates/components/RendersCopySettingsView.tsx"],"names":[],"mappings":"AAAA;;;;;GAKG;AAuBH,OAAO,KAAK,EAAE,aAAa,EAAmB,MAAM,wBAAwB,CAAC;AAmB7E,UAAU,4BAA4B;IACpC,OAAO,EAAE,aAAa,EAAE,CAAC;CAC1B;AA+ED,wBAAgB,uBAAuB,CAAC,EACtC,OAAO,GACR,EAAE,4BAA4B,+BAuY9B"}
|
|
@@ -49,6 +49,14 @@ export interface ScreenElement {
|
|
|
49
49
|
height: number;
|
|
50
50
|
};
|
|
51
51
|
}
|
|
52
|
+
export interface DescribeScreenParams {
|
|
53
|
+
/**
|
|
54
|
+
* Also list Buoy's own overlay — the dial, tool sheets, Ask Buoy's chat.
|
|
55
|
+
* Off by default: the screen being described is the app's, and Buoy's
|
|
56
|
+
* chrome echoing the caller's own words back is how taps went astray.
|
|
57
|
+
*/
|
|
58
|
+
includeBuoy?: boolean;
|
|
59
|
+
}
|
|
52
60
|
export interface DescribeScreenResult {
|
|
53
61
|
screen: {
|
|
54
62
|
width: number;
|
|
@@ -56,11 +64,15 @@ export interface DescribeScreenResult {
|
|
|
56
64
|
};
|
|
57
65
|
count: number;
|
|
58
66
|
elements: ScreenElement[];
|
|
67
|
+
/** Elements under Buoy's own overlay that were left out (pass includeBuoy to see them). */
|
|
68
|
+
hiddenBuoy?: number;
|
|
59
69
|
}
|
|
60
70
|
export interface TapElementParams {
|
|
61
71
|
nativeTag?: number;
|
|
62
72
|
testID?: string;
|
|
63
73
|
query?: string;
|
|
74
|
+
/** Let a fuzzy `query` match Buoy's own overlay. Exact nativeTag/testID always may. */
|
|
75
|
+
includeBuoy?: boolean;
|
|
64
76
|
/** Scroll the target into view first if it's off-screen. Default true. */
|
|
65
77
|
scrollIntoView?: boolean;
|
|
66
78
|
/** Invoke the element's onLongPress instead of onPress. Fails if it has none. */
|
|
@@ -88,6 +100,26 @@ export interface TapElementResult {
|
|
|
88
100
|
/** New value set, for toggle/slider/text controls. */
|
|
89
101
|
value?: number | boolean | string;
|
|
90
102
|
};
|
|
103
|
+
/**
|
|
104
|
+
* What the app did after the handler ran. `commits` is the number of React
|
|
105
|
+
* commits inside the settle window; 0 means the handler fired and nothing
|
|
106
|
+
* re-rendered — an occluded button, a no-op press, a guard that bailed.
|
|
107
|
+
*/
|
|
108
|
+
effect?: {
|
|
109
|
+
commits: number;
|
|
110
|
+
observedMs: number;
|
|
111
|
+
};
|
|
112
|
+
/** Other interactive elements that matched equally well. Non-empty means the choice was a coin toss. */
|
|
113
|
+
alsoMatched?: Array<{
|
|
114
|
+
nativeTag: number | null;
|
|
115
|
+
name: string;
|
|
116
|
+
testID?: string;
|
|
117
|
+
text?: string;
|
|
118
|
+
tap?: {
|
|
119
|
+
x: number;
|
|
120
|
+
y: number;
|
|
121
|
+
};
|
|
122
|
+
}>;
|
|
91
123
|
candidates?: Array<{
|
|
92
124
|
nativeTag: number | null;
|
|
93
125
|
name: string;
|
|
@@ -95,12 +127,14 @@ export interface TapElementResult {
|
|
|
95
127
|
text?: string;
|
|
96
128
|
}>;
|
|
97
129
|
}
|
|
98
|
-
export declare function describeScreen(): Promise<DescribeScreenResult>;
|
|
130
|
+
export declare function describeScreen(params?: DescribeScreenParams): Promise<DescribeScreenResult>;
|
|
99
131
|
export declare function tapElement(params: TapElementParams): Promise<TapElementResult>;
|
|
100
132
|
export interface WaitForParams {
|
|
101
133
|
nativeTag?: number;
|
|
102
134
|
testID?: string;
|
|
103
135
|
query?: string;
|
|
136
|
+
/** Let a fuzzy `query` match Buoy's own overlay. Exact nativeTag/testID always may. */
|
|
137
|
+
includeBuoy?: boolean;
|
|
104
138
|
/** Wait for the element to be GONE instead of present. Default false. */
|
|
105
139
|
gone?: boolean;
|
|
106
140
|
/** Give up after this long. Default 5000ms, capped at 30000. */
|
|
@@ -122,6 +156,23 @@ export interface WaitForResult {
|
|
|
122
156
|
text?: string;
|
|
123
157
|
interactive: boolean;
|
|
124
158
|
};
|
|
159
|
+
/**
|
|
160
|
+
* What DID rank for this query on the last scan, when the wait failed.
|
|
161
|
+
*
|
|
162
|
+
* A failed wait used to say only "not on screen" — so a caller could not tell
|
|
163
|
+
* "nothing is called that" from "it is there but scrolled off", and the
|
|
164
|
+
* cheapest next move looked like another wait with different words. Measured
|
|
165
|
+
* on a live QA run: five consecutive waits, 99 seconds, against text that was
|
|
166
|
+
* on screen the whole time. Naming the near misses ends that loop in one
|
|
167
|
+
* result instead of five calls.
|
|
168
|
+
*/
|
|
169
|
+
closest?: {
|
|
170
|
+
name: string;
|
|
171
|
+
testID?: string;
|
|
172
|
+
text?: string;
|
|
173
|
+
/** False means it matched but its measured centre is outside the window. */
|
|
174
|
+
onScreen: boolean;
|
|
175
|
+
}[];
|
|
125
176
|
}
|
|
126
177
|
/**
|
|
127
178
|
* Block until an element appears (or disappears), then report how long it took.
|