touchpress 0.1.1 → 0.1.3
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/core/index.d.mts +1 -1
- package/dist/core/index.mjs +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{preflight-aUba9u1k.mjs → preflight-BHYloK0j.mjs} +125 -10
- package/dist/{preflight-Czhj9QFM.d.mts → preflight-DJH10uBo.d.mts} +17 -0
- package/package.json +1 -1
package/dist/core/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as Tree, A as deviceNameForSlot, B as ProbeResult, C as openSession, D as ResolvedOptions, E as ReadyQuery, F as StepOptions, G as CaptureOptions, H as formatFailure, I as Typed, J as DeviceInfo, K as DeviceDriver, L as renderTitle, M as ActionRecord, N as ActionSink, O as TOUCHPRESS_DEFAULTS, P as EvidenceFile, Q as Settled, R as silentSink, S as SessionState, T as DeviceChoice, U as probe, V as ProbeTarget, W as Binding, X as OpenRequest, Y as DeviceSelection, Z as ScrollDirection, _ as RoleOptions, _t as Screen, a as ExpectedValue, at as Filter, b as DeviceSession, bt as renderScreen, c as ScrollSearch, ct as TextMatch, d as directionToward, dt as textMatch, et as Check, f as ActionOptions, ft as PinnedRef, g as Locator, gt as Resolution, h as FilterOptions, ht as Rect, i as ErrorInfo, it as evaluate, j as parseDeviceOptions, k as TouchpressOptions, l as ScrollTrail, lt as describeQuery, m as FillOptions, mt as RawSnapshot, n as PreflightReport, nt as Verdict, o as TouchpressError, ot as Query, p as Device, pt as Platform, q as DeviceFailure, r as preflight, rt as describeCheck, s as ScrollDevice, st as Role, t as PreflightDevice, tt as CheckName, u as createScrollSearch, ut as normalizeText, v as TextOptions, vt as ScreenNode, w as sessionName, x as OpenSessionInput, xt as resolve, y as createDevice, yt as parseScreen, z as ProbeOptions } from "../preflight-
|
|
1
|
+
import { $ as Tree, A as deviceNameForSlot, B as ProbeResult, C as openSession, D as ResolvedOptions, E as ReadyQuery, F as StepOptions, G as CaptureOptions, H as formatFailure, I as Typed, J as DeviceInfo, K as DeviceDriver, L as renderTitle, M as ActionRecord, N as ActionSink, O as TOUCHPRESS_DEFAULTS, P as EvidenceFile, Q as Settled, R as silentSink, S as SessionState, T as DeviceChoice, U as probe, V as ProbeTarget, W as Binding, X as OpenRequest, Y as DeviceSelection, Z as ScrollDirection, _ as RoleOptions, _t as Screen, a as ExpectedValue, at as Filter, b as DeviceSession, bt as renderScreen, c as ScrollSearch, ct as TextMatch, d as directionToward, dt as textMatch, et as Check, f as ActionOptions, ft as PinnedRef, g as Locator, gt as Resolution, h as FilterOptions, ht as Rect, i as ErrorInfo, it as evaluate, j as parseDeviceOptions, k as TouchpressOptions, l as ScrollTrail, lt as describeQuery, m as FillOptions, mt as RawSnapshot, n as PreflightReport, nt as Verdict, o as TouchpressError, ot as Query, p as Device, pt as Platform, q as DeviceFailure, r as preflight, rt as describeCheck, s as ScrollDevice, st as Role, t as PreflightDevice, tt as CheckName, u as createScrollSearch, ut as normalizeText, v as TextOptions, vt as ScreenNode, w as sessionName, x as OpenSessionInput, xt as resolve, y as createDevice, yt as parseScreen, z as ProbeOptions } from "../preflight-DJH10uBo.mjs";
|
|
2
2
|
//#region src/core/evidence.d.ts
|
|
3
3
|
/**
|
|
4
4
|
* Never throws. A capture that fails records a note and returns, because masking
|
package/dist/core/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { A as normalizeText, C as parseScreen, D as deviceNameForSlot, E as TOUCHPRESS_DEFAULTS, M as TouchpressError, O as parseDeviceOptions, S as silentSink, T as resolve, a as sizeOf, d as createScrollSearch, f as directionToward, g as evaluate, h as describeCheck, i as relativeTo, j as textMatch, k as describeQuery, l as captureEvidence, m as probe, n as compareScreenshot, o as toPixelBox, p as formatFailure, r as cropScreenshot, t as preflight, u as createDevice, v as openSession, w as renderScreen, x as renderTitle, y as sessionName } from "../preflight-
|
|
1
|
+
import { A as normalizeText, C as parseScreen, D as deviceNameForSlot, E as TOUCHPRESS_DEFAULTS, M as TouchpressError, O as parseDeviceOptions, S as silentSink, T as resolve, a as sizeOf, d as createScrollSearch, f as directionToward, g as evaluate, h as describeCheck, i as relativeTo, j as textMatch, k as describeQuery, l as captureEvidence, m as probe, n as compareScreenshot, o as toPixelBox, p as formatFailure, r as cropScreenshot, t as preflight, u as createDevice, v as openSession, w as renderScreen, x as renderTitle, y as sessionName } from "../preflight-BHYloK0j.mjs";
|
|
2
2
|
export { TOUCHPRESS_DEFAULTS, TouchpressError, captureEvidence, compareScreenshot, createDevice, createScrollSearch, cropScreenshot, describeCheck, describeQuery, deviceNameForSlot, directionToward, evaluate, formatFailure, normalizeText, openSession, parseDeviceOptions, parseScreen, preflight, probe, relativeTo, renderScreen, renderTitle, resolve, sessionName, silentSink, sizeOf, textMatch, toPixelBox };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { E as ReadyQuery, _t as Screen, a as ExpectedValue, at as Filter, b as DeviceSession, ct as TextMatch, g as Locator, h as FilterOptions, ht as Rect, i as ErrorInfo, k as TouchpressOptions$1, n as PreflightReport, o as TouchpressError, ot as Query, p as Device$1, pt as Platform, r as preflight, st as Role, t as PreflightDevice, vt as ScreenNode } from "./preflight-
|
|
1
|
+
import { E as ReadyQuery, _t as Screen, a as ExpectedValue, at as Filter, b as DeviceSession, ct as TextMatch, g as Locator, h as FilterOptions, ht as Rect, i as ErrorInfo, k as TouchpressOptions$1, n as PreflightReport, o as TouchpressError, ot as Query, p as Device$1, pt as Platform, r as preflight, st as Role, t as PreflightDevice, vt as ScreenNode } from "./preflight-DJH10uBo.mjs";
|
|
2
2
|
import { ExpectMatcherState } from "@playwright/test";
|
|
3
3
|
//#region src/ai/options.d.ts
|
|
4
4
|
/**
|
package/dist/index.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { C as parseScreen, E as TOUCHPRESS_DEFAULTS, M as TouchpressError, O as parseDeviceOptions, S as silentSink, T as resolve, _ as createQueue, a as sizeOf, b as sleep, c as createClient, i as relativeTo, j as textMatch, l as captureEvidence, n as compareScreenshot, o as toPixelBox, r as cropScreenshot, s as createAgentDeviceDriver, t as preflight, u as createDevice, v as openSession, w as renderScreen, x as renderTitle } from "./preflight-
|
|
1
|
+
import { C as parseScreen, E as TOUCHPRESS_DEFAULTS, M as TouchpressError, O as parseDeviceOptions, S as silentSink, T as resolve, _ as createQueue, a as sizeOf, b as sleep, c as createClient, i as relativeTo, j as textMatch, l as captureEvidence, n as compareScreenshot, o as toPixelBox, r as cropScreenshot, s as createAgentDeviceDriver, t as preflight, u as createDevice, v as openSession, w as renderScreen, x as renderTitle } from "./preflight-BHYloK0j.mjs";
|
|
2
2
|
import { expect as expect$1, test as test$1 } from "@playwright/test";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { dirname } from "node:path";
|
|
@@ -58,6 +58,17 @@ function formatError(info) {
|
|
|
58
58
|
`Screen:`,
|
|
59
59
|
info.screen
|
|
60
60
|
].join("\n");
|
|
61
|
+
case "untappable": return [
|
|
62
|
+
`The matched node has no touch point of its own, and no enclosing control was found.`,
|
|
63
|
+
``,
|
|
64
|
+
`Locator: ${info.locator}`,
|
|
65
|
+
``,
|
|
66
|
+
`Name the control that receives the touch, with getByRole, or with`,
|
|
67
|
+
`locator({ role: 'button', where: (node) => ... }).`,
|
|
68
|
+
``,
|
|
69
|
+
`Screen:`,
|
|
70
|
+
info.screen
|
|
71
|
+
].join("\n");
|
|
61
72
|
case "driver": return `${info.command} failed: ${describeFailure(info.failure)}`;
|
|
62
73
|
case "ai-not-configured": return ["device.act and device.extract need a model.", "Set use.aiModel to a gateway model id, such as 'anthropic/claude-sonnet-5', or to a provider model instance."].join("\n");
|
|
63
74
|
case "ai-missing-peer": return ["device.act and device.extract need the optional peer dependency 'ai'.", "Install it with: pnpm add -D ai"].join("\n");
|
|
@@ -111,6 +122,7 @@ function describeFailure(failure) {
|
|
|
111
122
|
case "session-rebound": return `the session is already bound to ${failure.boundTo} (${failure.detail})`;
|
|
112
123
|
case "stale-ref": return `the screen changed before the action reached it (${failure.detail})`;
|
|
113
124
|
case "ambiguous": return `the driver matched more than one element (${failure.detail})`;
|
|
125
|
+
case "covered": return `the node owns no touch point of its own (${failure.detail})`;
|
|
114
126
|
case "timeout": return `the driver timed out (${failure.detail})`;
|
|
115
127
|
case "unknown": return `${failure.code}: ${failure.detail}${failure.logPath === null ? "" : `\nDiagnostics: ${failure.logPath}`}`;
|
|
116
128
|
default: throw new Error(`unhandled failure ${JSON.stringify(failure)}`);
|
|
@@ -419,7 +431,9 @@ const IOS_ROLES = {
|
|
|
419
431
|
* Native 0.86 sample app, except the trailing widget block, which this app never
|
|
420
432
|
* renders and stays a plausible guess. `android.view.ViewGroup` is what every
|
|
421
433
|
* React Native `View` reports, testID containers included, so it is stated here
|
|
422
|
-
* rather than left to the `other` fall-through in `roleOf`.
|
|
434
|
+
* rather than left to the `other` fall-through in `roleOf`. A password field is
|
|
435
|
+
* an `EditText` too. agent-device 0.20.10 parses the tree's `password` flag but
|
|
436
|
+
* does not emit it, so once it does, map it to `secure-text-field` here.
|
|
423
437
|
*/
|
|
424
438
|
const ANDROID_ROLES = {
|
|
425
439
|
"android.widget.Button": "button",
|
|
@@ -598,6 +612,56 @@ function matchedText(node, query) {
|
|
|
598
612
|
function absorbAncestors(matched, query) {
|
|
599
613
|
return matched.filter((candidate) => !matched.some((other) => other !== candidate && isDescendant(other, candidate) && matchedText(other, query) === matchedText(candidate, query)));
|
|
600
614
|
}
|
|
615
|
+
/** `tab-bar` is left out because it is a container of buttons rather than a control that takes the touch. */
|
|
616
|
+
const INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
617
|
+
"button",
|
|
618
|
+
"link",
|
|
619
|
+
"switch",
|
|
620
|
+
"slider",
|
|
621
|
+
"text-field",
|
|
622
|
+
"secure-text-field",
|
|
623
|
+
"cell"
|
|
624
|
+
]);
|
|
625
|
+
/**
|
|
626
|
+
* The control a tap on `node` would really land on, or null when the screen
|
|
627
|
+
* offers none.
|
|
628
|
+
*
|
|
629
|
+
* A React Native pressable puts the accessible name on one node and the touch
|
|
630
|
+
* handler on another. On Android the two are siblings, so a locator that
|
|
631
|
+
* matched the label pins a ref that owns no touch point of its own. The button
|
|
632
|
+
* drawn under the label is the thing a finger hits, and its rect is what says
|
|
633
|
+
* so.
|
|
634
|
+
*
|
|
635
|
+
* A descendant wins over an enclosing node, because a control inside the
|
|
636
|
+
* matched node is what that node was labelling. Among several, the smallest
|
|
637
|
+
* wins, so a row of buttons inside one card does not hand back the card.
|
|
638
|
+
*/
|
|
639
|
+
function touchTargetFor(screen, node) {
|
|
640
|
+
const candidates = [];
|
|
641
|
+
for (const other of screen.nodes) {
|
|
642
|
+
if (other === node || !other.enabled || !INTERACTIVE_ROLES.has(other.role)) continue;
|
|
643
|
+
if (other.rect !== null) candidates.push({
|
|
644
|
+
node: other,
|
|
645
|
+
rect: other.rect
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
const inside = smallest(candidates.filter((candidate) => isDescendant(candidate.node, node)));
|
|
649
|
+
if (inside !== null) return inside;
|
|
650
|
+
const own = node.rect;
|
|
651
|
+
if (own === null) return null;
|
|
652
|
+
return smallest(candidates.filter((candidate) => encloses(candidate.rect, own)));
|
|
653
|
+
}
|
|
654
|
+
function encloses(outer, inner) {
|
|
655
|
+
return outer.x <= inner.x && outer.y <= inner.y && outer.x + outer.width >= inner.x + inner.width && outer.y + outer.height >= inner.y + inner.height;
|
|
656
|
+
}
|
|
657
|
+
function smallest(candidates) {
|
|
658
|
+
let best = null;
|
|
659
|
+
for (const candidate of candidates) if (best === null || areaOf(candidate.rect) < areaOf(best.rect)) best = candidate;
|
|
660
|
+
return best === null ? null : best.node;
|
|
661
|
+
}
|
|
662
|
+
function areaOf(rect) {
|
|
663
|
+
return rect.width * rect.height;
|
|
664
|
+
}
|
|
601
665
|
function isDescendant(node, ancestor) {
|
|
602
666
|
for (let walk = node.parent; walk !== null; walk = walk.parent) if (walk === ancestor) return true;
|
|
603
667
|
return false;
|
|
@@ -674,6 +738,7 @@ function renderTitle(record) {
|
|
|
674
738
|
case "long-press": return `longPress ${describeQuery(record.query)} for ${String(record.durationMs)}ms`;
|
|
675
739
|
case "fill": return `fill ${describeQuery(record.query)}`;
|
|
676
740
|
case "typed": return renderTyped(record.typed);
|
|
741
|
+
case "retarget": return `retarget to ${record.target}`;
|
|
677
742
|
case "scroll": return `scroll ${record.direction}`;
|
|
678
743
|
case "scroll-into-view": return `scrollIntoView ${describeQuery(record.query)}`;
|
|
679
744
|
case "relaunch": return `relaunch ${record.app}`;
|
|
@@ -1295,10 +1360,14 @@ function perform(session, sink, record, options, dispatch, write) {
|
|
|
1295
1360
|
try {
|
|
1296
1361
|
settled = await dispatch(device, pin(screen, resolution.node), deadline - Date.now());
|
|
1297
1362
|
} catch (error) {
|
|
1298
|
-
|
|
1299
|
-
retriedStaleRef
|
|
1300
|
-
|
|
1301
|
-
|
|
1363
|
+
const failure = failureOf(error);
|
|
1364
|
+
if (failure?.kind === "stale-ref" && !retriedStaleRef) {
|
|
1365
|
+
retriedStaleRef = true;
|
|
1366
|
+
screen = await device.capture();
|
|
1367
|
+
continue;
|
|
1368
|
+
}
|
|
1369
|
+
if (failure?.kind !== "covered" || record.kind === "fill") throw error;
|
|
1370
|
+
settled = await retarget(device, sink, dispatch, locator, screen, resolution.node, deadline - Date.now());
|
|
1302
1371
|
}
|
|
1303
1372
|
attempts += 1;
|
|
1304
1373
|
if (!settled.settled) sink.note("settle", `${renderTitle(record)} finished before the screen went quiet`);
|
|
@@ -1384,6 +1453,38 @@ function reportingScrolls(device, sink) {
|
|
|
1384
1453
|
}), () => device.scroll(direction, budgetMs))
|
|
1385
1454
|
};
|
|
1386
1455
|
}
|
|
1456
|
+
/**
|
|
1457
|
+
* A React Native pressable splits the accessible name off the touch handler, so
|
|
1458
|
+
* a locator naming what a user reads can pin a node the driver refuses. The
|
|
1459
|
+
* control covering that node is what the finger would have hit, so the action
|
|
1460
|
+
* goes there and the report names it, rather than asking the author to describe
|
|
1461
|
+
* a node they cannot see.
|
|
1462
|
+
*
|
|
1463
|
+
* One hop only. A second refusal means the retarget was the wrong reading of
|
|
1464
|
+
* the screen, and repeating it would only walk further from what was asked for.
|
|
1465
|
+
*/
|
|
1466
|
+
async function retarget(device, sink, dispatch, locator, screen, matched, budgetMs) {
|
|
1467
|
+
const target = touchTargetFor(screen, matched);
|
|
1468
|
+
if (target === null) throw untappable(locator, screen);
|
|
1469
|
+
return await sink.step(renderTitle({
|
|
1470
|
+
kind: "retarget",
|
|
1471
|
+
target: describeNode(target)
|
|
1472
|
+
}), async () => {
|
|
1473
|
+
try {
|
|
1474
|
+
return await dispatch(device, pin(screen, target), budgetMs);
|
|
1475
|
+
} catch (error) {
|
|
1476
|
+
if (failureOf(error)?.kind !== "covered") throw error;
|
|
1477
|
+
throw untappable(locator, screen);
|
|
1478
|
+
}
|
|
1479
|
+
}, { box: true });
|
|
1480
|
+
}
|
|
1481
|
+
function untappable(locator, screen) {
|
|
1482
|
+
return new TouchpressError({
|
|
1483
|
+
kind: "untappable",
|
|
1484
|
+
locator,
|
|
1485
|
+
screen: renderScreen(screen)
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1387
1488
|
function ambiguous(locator, nodes, screen) {
|
|
1388
1489
|
return new TouchpressError({
|
|
1389
1490
|
kind: "strict-mode",
|
|
@@ -1393,27 +1494,37 @@ function ambiguous(locator, nodes, screen) {
|
|
|
1393
1494
|
});
|
|
1394
1495
|
}
|
|
1395
1496
|
function confirmationOf(role, write) {
|
|
1497
|
+
const length = write.text.length;
|
|
1396
1498
|
if (role === "secure-text-field") return {
|
|
1397
1499
|
kind: "mask",
|
|
1398
|
-
length
|
|
1500
|
+
length
|
|
1399
1501
|
};
|
|
1400
1502
|
const value = normalizeText(write.text);
|
|
1401
1503
|
return write.secret ? {
|
|
1402
1504
|
kind: "secret",
|
|
1403
|
-
value
|
|
1505
|
+
value,
|
|
1506
|
+
length
|
|
1404
1507
|
} : {
|
|
1405
1508
|
kind: "open",
|
|
1406
|
-
value
|
|
1509
|
+
value,
|
|
1510
|
+
length
|
|
1407
1511
|
};
|
|
1408
1512
|
}
|
|
1409
1513
|
function holds(confirmation, actual) {
|
|
1410
1514
|
switch (confirmation.kind) {
|
|
1411
|
-
case "mask": return actual
|
|
1515
|
+
case "mask": return isMask(actual, confirmation.length);
|
|
1412
1516
|
case "secret":
|
|
1413
|
-
case "open": return actual === confirmation.value;
|
|
1517
|
+
case "open": return actual === confirmation.value || isMask(actual, confirmation.length);
|
|
1414
1518
|
default: throw new Error(`unhandled confirmation ${JSON.stringify(confirmation)}`);
|
|
1415
1519
|
}
|
|
1416
1520
|
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Requiring one repeated character stops a placeholder of the right length, which
|
|
1523
|
+
* is what an untouched password field reports, from passing as a landed write.
|
|
1524
|
+
*/
|
|
1525
|
+
function isMask(actual, length) {
|
|
1526
|
+
return actual.length === length && new Set(actual).size <= 1;
|
|
1527
|
+
}
|
|
1417
1528
|
function expectedOf(confirmation) {
|
|
1418
1529
|
switch (confirmation.kind) {
|
|
1419
1530
|
case "mask": return {
|
|
@@ -1669,6 +1780,10 @@ function classifyError(error) {
|
|
|
1669
1780
|
kind: "stale-ref",
|
|
1670
1781
|
detail: message
|
|
1671
1782
|
};
|
|
1783
|
+
if (details["reason"] === "covered_by_interactive_descendants") return {
|
|
1784
|
+
kind: "covered",
|
|
1785
|
+
detail: message
|
|
1786
|
+
};
|
|
1672
1787
|
if (/timed out|timeout/i.test(message)) return {
|
|
1673
1788
|
kind: "timeout",
|
|
1674
1789
|
detail: message
|
|
@@ -291,6 +291,14 @@ type DeviceFailure = {
|
|
|
291
291
|
} | {
|
|
292
292
|
readonly kind: 'ambiguous';
|
|
293
293
|
readonly detail: string;
|
|
294
|
+
} |
|
|
295
|
+
/**
|
|
296
|
+
* The pinned node owns no touch point outside the interactive nodes drawn
|
|
297
|
+
* over it, so the driver refused rather than guess which one was meant.
|
|
298
|
+
*/
|
|
299
|
+
{
|
|
300
|
+
readonly kind: 'covered';
|
|
301
|
+
readonly detail: string;
|
|
294
302
|
} | {
|
|
295
303
|
readonly kind: 'timeout';
|
|
296
304
|
readonly detail: string;
|
|
@@ -434,6 +442,11 @@ type ActionRecord = {
|
|
|
434
442
|
} | {
|
|
435
443
|
readonly kind: 'typed';
|
|
436
444
|
readonly typed: Typed;
|
|
445
|
+
} |
|
|
446
|
+
/** Where an action landed after the driver refused the node the locator matched. */
|
|
447
|
+
{
|
|
448
|
+
readonly kind: 'retarget';
|
|
449
|
+
readonly target: string;
|
|
437
450
|
} | {
|
|
438
451
|
readonly kind: 'scroll';
|
|
439
452
|
readonly direction: ScrollDirection;
|
|
@@ -833,6 +846,10 @@ type ErrorInfo = {
|
|
|
833
846
|
readonly attempts: number;
|
|
834
847
|
readonly timeoutMs: number;
|
|
835
848
|
readonly screen: string;
|
|
849
|
+
} | {
|
|
850
|
+
readonly kind: 'untappable';
|
|
851
|
+
readonly locator: string;
|
|
852
|
+
readonly screen: string;
|
|
836
853
|
} | {
|
|
837
854
|
readonly kind: 'driver';
|
|
838
855
|
readonly command: string;
|