simframe 0.12.2 → 0.14.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/README.md +138 -9
- package/native/simframed/Sources/SimframeCore/Motion.swift +33 -2
- package/native/supervise.swift +63 -4
- package/package.json +1 -1
- package/scripts/article-md.mjs +185 -0
- package/scripts/ci-device-guard.mjs +82 -0
- package/scripts/ci-integration-local.sh +33 -9
- package/scripts/ci-memory.mjs +117 -15
- package/scripts/eval-fingerprint.mjs +145 -3
- package/scripts/replay-rulings.mjs +60 -1
- package/src/actions.js +213 -13
- package/src/analyze.js +56 -0
- package/src/cli.js +205 -15
- package/src/fingerprint.js +10 -1
- package/src/graph.js +15 -2
- package/src/index.js +302 -16
- package/src/input.js +4 -0
- package/src/matching.js +17 -1
- package/src/mcp.js +148 -13
- package/src/metrics.js +49 -6
- package/src/navigate.js +4 -1
- package/src/ollama.js +37 -9
- package/src/platform/android.js +34 -0
- package/src/platform/index.js +7 -0
- package/src/platform/ios.js +128 -5
- package/src/platform/plist.js +156 -0
- package/src/refs.js +12 -1
- package/src/regions.js +54 -0
- package/src/screenmap.js +85 -6
- package/src/storage.js +201 -0
- package/src/store.js +53 -0
- package/src/supervisor.js +20 -2
- package/src/view.js +84 -9
package/src/view.js
CHANGED
|
@@ -247,8 +247,26 @@ export function actsInteractive(t) {
|
|
|
247
247
|
export function rowsFor(entry, { screen, filter, interactive, all = false, limit = DEFAULT_LIMIT } = {}) {
|
|
248
248
|
let kept = (entry?.targets ?? []).map((t) => ({ ...t })).filter((t) => {
|
|
249
249
|
if (!isNum(t.x) || !isNum(t.y)) return false;
|
|
250
|
-
// Off-screen elements are real in the tree and untappable in fact
|
|
251
|
-
|
|
250
|
+
// Off-screen elements are real in the tree and untappable in fact —
|
|
251
|
+
// *unless* enough of one is on screen to tap, which the centre test cannot
|
|
252
|
+
// see. A filter chip showing 29pt of itself at the right edge was dropped
|
|
253
|
+
// while OCR's reading of that same sliver, labelled "Flc", was kept and
|
|
254
|
+
// printed in its place. Item 121: say so or clamp. Both, here — the row is
|
|
255
|
+
// kept, its tap point is moved to the centre of the visible part, and it is
|
|
256
|
+
// marked so nobody reads a clamped coordinate as a whole element.
|
|
257
|
+
//
|
|
258
|
+
// Deliberately NOT also relaxing `locate`. A clipped element still answers
|
|
259
|
+
// "in the tree but not in view", so `scrollTo` keeps scrolling to it rather
|
|
260
|
+
// than declaring a sliver good enough — that conservatism was bought with a
|
|
261
|
+
// reported session lost to `"'Assigned to Me' is in view at 422,277
|
|
262
|
+
// already"`, and widening it here would buy the same bug back.
|
|
263
|
+
if (regions.offViewport(t, screen)) {
|
|
264
|
+
const clip = regions.clipping(t, screen);
|
|
265
|
+
if (!clip?.usable) return false;
|
|
266
|
+
t.clipped = true;
|
|
267
|
+
t.x = clip.point.x;
|
|
268
|
+
t.y = clip.point.y;
|
|
269
|
+
}
|
|
252
270
|
if (!all && !regions.offerable(t.region)) return false;
|
|
253
271
|
if (!all && isNoise(t)) return false;
|
|
254
272
|
return true;
|
|
@@ -380,11 +398,42 @@ function valueNote(r) {
|
|
|
380
398
|
return `= ${v}`;
|
|
381
399
|
}
|
|
382
400
|
|
|
401
|
+
/**
|
|
402
|
+
* What to call a row.
|
|
403
|
+
*
|
|
404
|
+
* A control with no accessibility label is not necessarily anonymous, and the
|
|
405
|
+
* order here is by how much the name can be trusted. Its own label first. Then
|
|
406
|
+
* its `testID`, which is a name a developer chose and which `tap` has always
|
|
407
|
+
* matched on. Then OCR's reading of it, which is how an icon-only control gets
|
|
408
|
+
* a name at all and which cannot contradict a label it does not have. Only when
|
|
409
|
+
* all three are missing is it really unaddressable, and that is the case item
|
|
410
|
+
* 122 is about: three field reports in a row spent their time on controls that
|
|
411
|
+
* existed, were tappable, and could be reached by no selector at all.
|
|
412
|
+
*/
|
|
413
|
+
export function displayName(r) {
|
|
414
|
+
return trim(r.label)
|
|
415
|
+
|| trim(r.identifier)
|
|
416
|
+
// Only an alias with a word in it. OCR reads the three dots of an overflow
|
|
417
|
+
// menu as `...`, and on the first live run of this the button was named
|
|
418
|
+
// `#1 button 364,84 ...` — which looks like a name, cannot be typed into a
|
|
419
|
+
// selector, and made the count below say there was nothing unnamed here.
|
|
420
|
+
// `isNoise` already refuses such text as a row of its own; it must not get
|
|
421
|
+
// in through the alias door either.
|
|
422
|
+
|| (r.aliases ?? []).find((a) => alnum(a))
|
|
423
|
+
|| (matching.isAxTarget(r) ? '(unlabelled)' : '(no text)');
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/** A row the map can print and no caller can name. */
|
|
427
|
+
export const unaddressable = (r) =>
|
|
428
|
+
matching.isAxTarget(r) && !trim(r.label) && !trim(r.identifier)
|
|
429
|
+
&& !(r.aliases ?? []).some((a) => alnum(a));
|
|
430
|
+
|
|
383
431
|
function renderRow(r) {
|
|
432
|
+
const shown = displayName(r);
|
|
384
433
|
const name = [
|
|
385
|
-
|
|
434
|
+
shown,
|
|
386
435
|
valueNote(r),
|
|
387
|
-
aliasNote(r),
|
|
436
|
+
aliasNote(r, shown),
|
|
388
437
|
].filter(Boolean).join(' ');
|
|
389
438
|
const state = [
|
|
390
439
|
r.enabled === false ? 'disabled' : null,
|
|
@@ -395,7 +444,9 @@ function renderRow(r) {
|
|
|
395
444
|
shortType(r.type).padEnd(9),
|
|
396
445
|
`${r.x},${r.y}`.padEnd(9),
|
|
397
446
|
state ? `${state} ` : '',
|
|
398
|
-
name,
|
|
447
|
+
// After the name, and appended rather than added as a column: an extra
|
|
448
|
+
// element in this join puts an extra space on *every* row, clipped or not.
|
|
449
|
+
r.clipped ? `${name} (partly off-screen — the coordinate is the middle of the visible part)` : name,
|
|
399
450
|
].join(' ');
|
|
400
451
|
}
|
|
401
452
|
|
|
@@ -406,11 +457,14 @@ function renderRow(r) {
|
|
|
406
457
|
* for, which is useful when they disagree and pure cost when they agree —
|
|
407
458
|
* "WELCOME ~ WELCOME" was a third of some rows.
|
|
408
459
|
*/
|
|
409
|
-
function aliasNote(r) {
|
|
460
|
+
function aliasNote(r, shown = r.label) {
|
|
410
461
|
const extra = (r.aliases ?? [])
|
|
411
462
|
.filter((a) => {
|
|
412
463
|
const t = alnum(a);
|
|
413
|
-
|
|
464
|
+
// Compared against the name actually printed, not against the label. An
|
|
465
|
+
// unlabelled control is named by its own alias now, and comparing against
|
|
466
|
+
// an absent label printed every one of them twice: `Sort ~ Sort`.
|
|
467
|
+
return t && !alnum(shown).includes(t);
|
|
414
468
|
})
|
|
415
469
|
.slice(0, 2);
|
|
416
470
|
return extra.length ? `~ ${trim(extra.join(' '))}` : null;
|
|
@@ -511,6 +565,25 @@ export async function screenMap(deviceQuery, {
|
|
|
511
565
|
+ ' you did not expect to see as belonging to the layer underneath'
|
|
512
566
|
: null;
|
|
513
567
|
|
|
568
|
+
// Controls that are on the screen and answer to no name — item 122.
|
|
569
|
+
//
|
|
570
|
+
// Counted from the rows about to be printed rather than from the map, so it
|
|
571
|
+
// is a statement about what the caller can see. The wording is the ask,
|
|
572
|
+
// near-verbatim from the report that made it: *"a line like 'N on-screen
|
|
573
|
+
// tappable views have no accessibility label — they cannot be addressed by
|
|
574
|
+
// selector' would push people toward instrumenting, which is the outcome
|
|
575
|
+
// everyone wants"*. It is also the honest answer to the second half of that
|
|
576
|
+
// item, which we cannot fix from here: a view UIKit was never told is
|
|
577
|
+
// accessible is invisible to the tree, so a screen whose controls are all
|
|
578
|
+
// undeclared shows this count as 0 and still needs a screenshot.
|
|
579
|
+
const anonymous = rows.filter(unaddressable).length;
|
|
580
|
+
const unnamed = anonymous
|
|
581
|
+
? `${anonymous} on-screen control(s) have no accessibility label — they are listed with their`
|
|
582
|
+
+ ' coordinates and can be tapped by point or by #ref, but not by name. If what you are'
|
|
583
|
+
+ ' looking for is not in the list either, the app has views that were never declared'
|
|
584
|
+
+ ' accessible and only a screenshot will find those.'
|
|
585
|
+
: null;
|
|
586
|
+
|
|
514
587
|
return {
|
|
515
588
|
device,
|
|
516
589
|
identity,
|
|
@@ -529,7 +602,8 @@ export async function screenMap(deviceQuery, {
|
|
|
529
602
|
staleExits,
|
|
530
603
|
cleared,
|
|
531
604
|
overlay,
|
|
532
|
-
|
|
605
|
+
unnamed,
|
|
606
|
+
text: render({ device, identity, rows, truncated, collapsed, screen, name, exits, exitList, staleExits, cleared, overlay, unnamed }),
|
|
533
607
|
};
|
|
534
608
|
}
|
|
535
609
|
|
|
@@ -712,7 +786,7 @@ export function ambiguousLabels(rows) {
|
|
|
712
786
|
return [...seen.values()].filter((n) => n > 1).length;
|
|
713
787
|
}
|
|
714
788
|
|
|
715
|
-
export function render({ device, identity, rows, truncated, collapsed, screen, name, exits, exitList, staleExits, verdictLine, ambiguities, cleared, overlay }) {
|
|
789
|
+
export function render({ device, identity, rows, truncated, collapsed, screen, name, exits, exitList, staleExits, verdictLine, ambiguities, cleared, overlay, unnamed }) {
|
|
716
790
|
const head = [
|
|
717
791
|
device?.name,
|
|
718
792
|
screen?.width ? `${screen.width}x${screen.height}pt` : null,
|
|
@@ -747,6 +821,7 @@ export function render({ device, identity, rows, truncated, collapsed, screen, n
|
|
|
747
821
|
// already believes, which is the one kind of news that must not be scrolled to.
|
|
748
822
|
if (cleared) lines.push(cleared);
|
|
749
823
|
if (overlay) lines.push(overlay);
|
|
824
|
+
if (unnamed) lines.push(unnamed);
|
|
750
825
|
const worked = exitsLine(exitList, { stale: staleExits });
|
|
751
826
|
if (worked) lines.push(worked);
|
|
752
827
|
|