touchpress 0.1.2 → 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-CWeJYtS7.mjs → preflight-BHYloK0j.mjs} +107 -4
- 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)}`);
|
|
@@ -600,6 +612,56 @@ function matchedText(node, query) {
|
|
|
600
612
|
function absorbAncestors(matched, query) {
|
|
601
613
|
return matched.filter((candidate) => !matched.some((other) => other !== candidate && isDescendant(other, candidate) && matchedText(other, query) === matchedText(candidate, query)));
|
|
602
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
|
+
}
|
|
603
665
|
function isDescendant(node, ancestor) {
|
|
604
666
|
for (let walk = node.parent; walk !== null; walk = walk.parent) if (walk === ancestor) return true;
|
|
605
667
|
return false;
|
|
@@ -676,6 +738,7 @@ function renderTitle(record) {
|
|
|
676
738
|
case "long-press": return `longPress ${describeQuery(record.query)} for ${String(record.durationMs)}ms`;
|
|
677
739
|
case "fill": return `fill ${describeQuery(record.query)}`;
|
|
678
740
|
case "typed": return renderTyped(record.typed);
|
|
741
|
+
case "retarget": return `retarget to ${record.target}`;
|
|
679
742
|
case "scroll": return `scroll ${record.direction}`;
|
|
680
743
|
case "scroll-into-view": return `scrollIntoView ${describeQuery(record.query)}`;
|
|
681
744
|
case "relaunch": return `relaunch ${record.app}`;
|
|
@@ -1297,10 +1360,14 @@ function perform(session, sink, record, options, dispatch, write) {
|
|
|
1297
1360
|
try {
|
|
1298
1361
|
settled = await dispatch(device, pin(screen, resolution.node), deadline - Date.now());
|
|
1299
1362
|
} catch (error) {
|
|
1300
|
-
|
|
1301
|
-
retriedStaleRef
|
|
1302
|
-
|
|
1303
|
-
|
|
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());
|
|
1304
1371
|
}
|
|
1305
1372
|
attempts += 1;
|
|
1306
1373
|
if (!settled.settled) sink.note("settle", `${renderTitle(record)} finished before the screen went quiet`);
|
|
@@ -1386,6 +1453,38 @@ function reportingScrolls(device, sink) {
|
|
|
1386
1453
|
}), () => device.scroll(direction, budgetMs))
|
|
1387
1454
|
};
|
|
1388
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
|
+
}
|
|
1389
1488
|
function ambiguous(locator, nodes, screen) {
|
|
1390
1489
|
return new TouchpressError({
|
|
1391
1490
|
kind: "strict-mode",
|
|
@@ -1681,6 +1780,10 @@ function classifyError(error) {
|
|
|
1681
1780
|
kind: "stale-ref",
|
|
1682
1781
|
detail: message
|
|
1683
1782
|
};
|
|
1783
|
+
if (details["reason"] === "covered_by_interactive_descendants") return {
|
|
1784
|
+
kind: "covered",
|
|
1785
|
+
detail: message
|
|
1786
|
+
};
|
|
1684
1787
|
if (/timed out|timeout/i.test(message)) return {
|
|
1685
1788
|
kind: "timeout",
|
|
1686
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;
|