simframe 0.8.0 → 0.9.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/src/screenmap.js CHANGED
@@ -12,11 +12,12 @@ import * as control from './control.js';
12
12
  import * as fingerprint from './fingerprint.js';
13
13
  import * as input from './input.js';
14
14
  import * as ocr from './ocr.js';
15
+ import * as matching from './matching.js';
15
16
  import * as regions from './regions.js';
16
17
  import { informative } from './refs.js';
17
18
  import * as store from './store.js';
18
19
 
19
- const MAP_VERSION = 7; // footprintless elements and containers no longer enter identity
20
+ const MAP_VERSION = 8; // an OCR reading contained in a labelled ax element merges into it
20
21
 
21
22
  function mapDir(udid) {
22
23
  return path.join(store.deviceDir(udid), 'screens');
@@ -232,22 +233,43 @@ export async function build(udid, {
232
233
  const point = { x: w.centerX, y: w.centerY };
233
234
  // If an accessibility element already covers this text, it is the same
234
235
  // control: keep the element and record the visible text as an alias.
235
- // Containing text is not the same as being that control. A tab bar
236
- // encloses all five tab labels but is not any of them, so only merge
237
- // when the element is close to the text's own size.
236
+ //
237
+ // Two rules, and the second one cost 16 escalations and half of
238
+ // HPI_accuracy. The first is a size test: an element close to the
239
+ // text's own size, containing it, is that text. It exists to stop a
240
+ // tab bar from swallowing all five of its tab labels — containing text
241
+ // is not the same as being that control.
242
+ //
243
+ // But a full-width list row is 19× the area of the words printed in
244
+ // it, so the size test could never fire for the shape it matters most
245
+ // on: every Contacts and Settings row arrived as an ax element AND as
246
+ // an OCR text box, both scoring 1.00 for the same query, and `tap
247
+ // "Kate Bell"` refused as ambiguous on all five runs of the
248
+ // instrumented flow suite. The second rule is the one the size test
249
+ // was standing in for: near-total containment AND the same text. A tab
250
+ // bar contains "Assets" but is not labelled "Assets", so it is still
251
+ // refused; a row labelled "Kate Bell" containing OCR's "Kate Bell" is
252
+ // one element that two sensors saw.
238
253
  const textArea = Math.max(1, w.width * w.height);
254
+ const box = { x: w.x, y: w.y, width: w.width, height: w.height };
255
+ const eligible = (t) =>
256
+ matching.isAxTarget(t) &&
257
+ t.frame &&
258
+ !/^(Group|Application|ScrollView|Table|Collection)$/i.test(t.type || '');
239
259
  const covering = targets
240
- .filter(
241
- (t) =>
242
- t.source === 'ax' &&
243
- t.frame &&
244
- inside(point, t.frame) &&
245
- !/^(Group|Application|ScrollView|Table|Collection)$/i.test(t.type || '') &&
246
- area(t.frame) <= textArea * 8,
247
- )
248
- .sort((a, b) => area(a.frame) - area(b.frame))[0];
260
+ .filter((t) => eligible(t) && inside(point, t.frame) && area(t.frame) <= textArea * 8)
261
+ .sort((a, b) => area(a.frame) - area(b.frame))[0]
262
+ ?? targets
263
+ .filter((t) => eligible(t) && matching.sameElementSeenTwice(t, { ...w, frame: box, label: w.text }))
264
+ .sort((a, b) => area(a.frame) - area(b.frame))[0];
249
265
  if (covering) {
250
266
  covering.aliases = [...(covering.aliases || []), w.text];
267
+ // Keep the ax role and frame — it is the hit target — and record that
268
+ // both sensors saw it. Anything asking "is this the tree's element?"
269
+ // must ask matching.isAxTarget, not `=== 'ax'`.
270
+ if (!String(covering.source ?? '').includes('ocr')) {
271
+ covering.source = `${covering.source ?? 'ax'}|ocr`;
272
+ }
251
273
  continue;
252
274
  }
253
275
  targets.push({
@@ -323,7 +345,7 @@ export function rank(entry, query) {
323
345
  ? exact
324
346
  : entry.targets.filter((t) => names(t).some((n) => n.includes(q)));
325
347
  return pool
326
- .map((t) => ({ target: t, score: (isInteractive(t) ? 2 : 0) + (t.source === 'ax' ? 1 : 0) }))
348
+ .map((t) => ({ target: t, score: (isInteractive(t) ? 2 : 0) + (matching.isAxTarget(t) ? 1 : 0) }))
327
349
  .sort((a, b) => b.score - a.score)
328
350
  .map((r) => r.target);
329
351
  }
package/src/view.js CHANGED
@@ -14,6 +14,7 @@
14
14
  import * as api from './index.js';
15
15
  import * as graph from './graph.js';
16
16
  import { writeRefs } from './refs.js';
17
+ import * as matching from './matching.js';
17
18
 
18
19
  /** Reading order. Chrome frames the screen, so it reads first and last. */
19
20
  const REGION_ORDER = ['nav-bar', 'content', 'tab-bar', 'keyboard', 'status-bar'];
@@ -78,7 +79,7 @@ function isHost(t) {
78
79
  // An accessibility element the app gave a label to is a unit the app itself
79
80
  // considers one thing — a dashboard tile reading "WOs past ETA, 1910" is one
80
81
  // tap target whose parts OCR happens to read separately.
81
- return t.source === 'ax' && Boolean(t.label);
82
+ return matching.isAxTarget(t) && Boolean(t.label);
82
83
  }
83
84
 
84
85
  /**
@@ -152,7 +153,7 @@ function dropContainers(targets, screen) {
152
153
  * an ellipsis menu, a chevron it decided was a period. Nothing can be tapped by
153
154
  * that name, so listing it is pure cost.
154
155
  */
155
- const isNoise = (t) => t.source === 'ocr' && !alnum(t.label);
156
+ const isNoise = (t) => !matching.isAxTarget(t) && t.source === 'ocr' && !alnum(t.label);
156
157
 
157
158
  const trim = (text) => {
158
159
  const one = String(text ?? '').replace(/\s+/g, ' ').trim();
@@ -203,7 +204,7 @@ export function rowsFor(entry, { screen, filter, interactive, all = false, limit
203
204
 
204
205
  function renderRow(r) {
205
206
  const name = [
206
- trim(r.label) || (r.source === 'ax' ? '(unlabelled)' : '(no text)'),
207
+ trim(r.label) || (matching.isAxTarget(r) ? '(unlabelled)' : '(no text)'),
207
208
  aliasNote(r),
208
209
  ].filter(Boolean).join(' ');
209
210
  const state = [