touchpress 0.1.3 → 0.2.1
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 +2 -2
- package/dist/core/index.mjs +1 -1
- package/dist/index.d.mts +2 -2
- package/dist/index.mjs +1 -1
- package/dist/{preflight-DJH10uBo.d.mts → preflight-CqFFdaAv.d.mts} +87 -1
- package/dist/{preflight-BHYloK0j.mjs → preflight-D1aux7ic.mjs} +177 -26
- package/package.json +1 -1
package/dist/core/index.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $ as
|
|
1
|
+
import { $ as DeviceSelection, A as ResolvedOptions, B as renderTitle, C as DeviceSession, Ct as parseScreen, D as sessionName, E as openSession, F as ActionRecord, G as formatFailure, H as ProbeOptions, I as ActionSink, J as Binding, K as probe, L as EvidenceFile, M as TouchpressOptions, N as deviceNameForSlot, O as DeviceChoice, P as parseDeviceOptions, Q as DeviceInfo, R as StepOptions, S as createDevice, St as ScreenNode, T as SessionState, Tt as resolve, U as ProbeResult, V as silentSink, W as ProbeTarget, X as DeviceDriver, Y as CaptureOptions, Z as DeviceFailure, _ as Keyboard, _t as Platform, a as ExpectedValue, at as CheckName, b as TextOptions, bt as Resolution, c as ScrollSearch, ct as evaluate, d as directionToward, dt as Role, et as OpenRequest, f as ActionOptions, ft as TextMatch, g as FilterOptions, gt as PinnedRef, h as FillOptions, ht as textMatch, i as ErrorInfo, it as Check, j as TOUCHPRESS_DEFAULTS, k as ReadyQuery, l as ScrollTrail, lt as Filter, m as Device, mt as normalizeText, n as PreflightReport, nt as Settled, o as TouchpressError, ot as Verdict, p as BackOptions, pt as describeQuery, q as BackMode, r as preflight, rt as Tree, s as ScrollDevice, st as describeCheck, t as PreflightDevice, tt as ScrollDirection, u as createScrollSearch, ut as Query, v as Locator, vt as RawSnapshot, w as OpenSessionInput, wt as renderScreen, x as TypeOptions, xt as Screen, y as RoleOptions, yt as Rect, z as Typed } from "../preflight-CqFFdaAv.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
|
|
@@ -70,4 +70,4 @@ declare function toPixelBox(rect: {
|
|
|
70
70
|
/** Moves a box into the coordinates of a crop taken at `origin`. */
|
|
71
71
|
declare function relativeTo(box: PixelBox, origin: PixelBox): PixelBox;
|
|
72
72
|
//#endregion
|
|
73
|
-
export { type ActionOptions, type ActionRecord, type ActionSink, type Binding, type CaptureOptions, type Check, type CheckName, type CompareOptions, type Comparison, type Device, type DeviceChoice, type DeviceDriver, type DeviceFailure, type DeviceInfo, type DeviceSelection, type DeviceSession, type ErrorInfo, type EvidenceFile, type ExpectedValue, type FillOptions, type Filter, type FilterOptions, type Locator, type OpenRequest, type OpenSessionInput, type PinnedRef, type PixelBox, type Platform, type PreflightDevice, type PreflightReport, type ProbeOptions, type ProbeResult, type ProbeTarget, type Query, type RawSnapshot, type ReadyQuery, type Rect, type Resolution, type ResolvedOptions, type Role, type RoleOptions, type Screen, type ScreenNode, type ScrollDevice, type ScrollDirection, type ScrollSearch, type ScrollTrail, type SessionState, type Settled, type Size, type StepOptions, TOUCHPRESS_DEFAULTS, type TextMatch, type TextOptions, TouchpressError, type TouchpressOptions, type Tree, type Typed, type Verdict, 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 };
|
|
73
|
+
export { type ActionOptions, type ActionRecord, type ActionSink, type BackMode, type BackOptions, type Binding, type CaptureOptions, type Check, type CheckName, type CompareOptions, type Comparison, type Device, type DeviceChoice, type DeviceDriver, type DeviceFailure, type DeviceInfo, type DeviceSelection, type DeviceSession, type ErrorInfo, type EvidenceFile, type ExpectedValue, type FillOptions, type Filter, type FilterOptions, type Keyboard, type Locator, type OpenRequest, type OpenSessionInput, type PinnedRef, type PixelBox, type Platform, type PreflightDevice, type PreflightReport, type ProbeOptions, type ProbeResult, type ProbeTarget, type Query, type RawSnapshot, type ReadyQuery, type Rect, type Resolution, type ResolvedOptions, type Role, type RoleOptions, type Screen, type ScreenNode, type ScrollDevice, type ScrollDirection, type ScrollSearch, type ScrollTrail, type SessionState, type Settled, type Size, type StepOptions, TOUCHPRESS_DEFAULTS, type TextMatch, type TextOptions, TouchpressError, type TouchpressOptions, type Tree, type TypeOptions, type Typed, type Verdict, 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/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-D1aux7ic.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 {
|
|
1
|
+
import { C as DeviceSession, M as TouchpressOptions$1, St as ScreenNode, _ as Keyboard, _t as Platform, a as ExpectedValue, dt as Role, ft as TextMatch, g as FilterOptions, i as ErrorInfo, k as ReadyQuery, lt as Filter, m as Device$1, n as PreflightReport, o as TouchpressError, p as BackOptions, r as preflight, t as PreflightDevice, ut as Query, v as Locator, x as TypeOptions, xt as Screen, yt as Rect } from "./preflight-CqFFdaAv.mjs";
|
|
2
2
|
import { ExpectMatcherState } from "@playwright/test";
|
|
3
3
|
//#region src/ai/options.d.ts
|
|
4
4
|
/**
|
|
@@ -143,4 +143,4 @@ declare const expect: import("@playwright/test").Expect<{
|
|
|
143
143
|
type TouchpressOptions = TouchpressOptions$1 & AiOptions;
|
|
144
144
|
type Device = Device$1 & AiDevice;
|
|
145
145
|
//#endregion
|
|
146
|
-
export { type ActOptions, type AiDevice, type AiModel, type AiOptions, Device, type ErrorInfo, type ExpectedValue, type ExtractOptions, type ExtractSchema, type Filter, type FilterOptions, type Locator, type Platform, type PreflightDevice, type PreflightReport, type Query, type ReadyQuery, type Rect, type Role, type Screen, type ScreenNode, type ScreenshotOptions, type TextMatch, TouchpressError, TouchpressOptions, expect, preflight, setupTest, test };
|
|
146
|
+
export { type ActOptions, type AiDevice, type AiModel, type AiOptions, type BackOptions, Device, type ErrorInfo, type ExpectedValue, type ExtractOptions, type ExtractSchema, type Filter, type FilterOptions, type Keyboard, type Locator, type Platform, type PreflightDevice, type PreflightReport, type Query, type ReadyQuery, type Rect, type Role, type Screen, type ScreenNode, type ScreenshotOptions, type TextMatch, TouchpressError, TouchpressOptions, type TypeOptions, expect, preflight, setupTest, test };
|
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-D1aux7ic.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";
|
|
@@ -233,6 +233,11 @@ declare function describeCheck(check: Check): string;
|
|
|
233
233
|
//#endregion
|
|
234
234
|
//#region src/core/driver.d.ts
|
|
235
235
|
type ScrollDirection = 'up' | 'down' | 'left' | 'right';
|
|
236
|
+
/**
|
|
237
|
+
* `in-app` is the navigation control the app draws. `system` is the platform
|
|
238
|
+
* gesture or key, which iOS does not have.
|
|
239
|
+
*/
|
|
240
|
+
type BackMode = 'in-app' | 'system';
|
|
236
241
|
/**
|
|
237
242
|
* A session binds on its first command, so this rides on `open` and on every
|
|
238
243
|
* command after it. A call sent without selection lands on whichever device the
|
|
@@ -262,6 +267,8 @@ type Binding = {
|
|
|
262
267
|
readonly deviceLabel: string;
|
|
263
268
|
readonly appId: string;
|
|
264
269
|
readonly stateDir: string | null;
|
|
270
|
+
/** What lets a driver reach the device with a tool of its own. Null when the open response named none. */
|
|
271
|
+
readonly udid: string | null;
|
|
265
272
|
};
|
|
266
273
|
type Settled = {
|
|
267
274
|
readonly settled: boolean;
|
|
@@ -342,6 +349,24 @@ type DeviceDriver = {
|
|
|
342
349
|
tap(ref: PinnedRef, options: SettleOptions): Promise<Settled>;
|
|
343
350
|
longPress(ref: PinnedRef, durationMs: number, options: SettleOptions): Promise<Settled>;
|
|
344
351
|
fill(ref: PinnedRef, text: string, options: SettleOptions): Promise<Settled>;
|
|
352
|
+
/** Takes no target, because the platform decides what goes back. */
|
|
353
|
+
back(mode: BackMode, options: SettleOptions): Promise<Settled>;
|
|
354
|
+
/**
|
|
355
|
+
* Types into whatever holds focus. The one mutation with no `PinnedRef`,
|
|
356
|
+
* because a field the app focused for itself may carry nothing to select on.
|
|
357
|
+
*/
|
|
358
|
+
type(text: string, options: SettleOptions): Promise<Settled>;
|
|
359
|
+
/**
|
|
360
|
+
* Discards the app's stored state. Whether that reaches every store the app
|
|
361
|
+
* writes to is the driver's problem, not the core's.
|
|
362
|
+
*/
|
|
363
|
+
clearAppState(app: string): Promise<void>;
|
|
364
|
+
/**
|
|
365
|
+
* Resets the device's keychain, which on an iOS simulator is shared by every
|
|
366
|
+
* app on it. Kept apart from `clearAppState` for that reason. A no-op on a
|
|
367
|
+
* platform whose app data clear already covers the app's secure store.
|
|
368
|
+
*/
|
|
369
|
+
resetKeychain(): Promise<void>;
|
|
345
370
|
/** Returns nothing because the driver's scroll response carries no settle observation. */
|
|
346
371
|
scroll(direction: ScrollDirection, options: SettleOptions): Promise<void>;
|
|
347
372
|
dismissDevOverlay(): Promise<void>;
|
|
@@ -456,6 +481,14 @@ type ActionRecord = {
|
|
|
456
481
|
} | {
|
|
457
482
|
readonly kind: 'relaunch';
|
|
458
483
|
readonly app: string;
|
|
484
|
+
} | {
|
|
485
|
+
readonly kind: 'back';
|
|
486
|
+
readonly mode: BackMode;
|
|
487
|
+
} | {
|
|
488
|
+
readonly kind: 'clear-state';
|
|
489
|
+
readonly app: string;
|
|
490
|
+
} | {
|
|
491
|
+
readonly kind: 'clear-keychain';
|
|
459
492
|
} | {
|
|
460
493
|
readonly kind: 'dismiss-overlay';
|
|
461
494
|
} | {
|
|
@@ -635,6 +668,10 @@ type SessionDevice = {
|
|
|
635
668
|
tap(ref: PinnedRef, budgetMs: number): Promise<Settled>;
|
|
636
669
|
longPress(ref: PinnedRef, durationMs: number, budgetMs: number): Promise<Settled>;
|
|
637
670
|
fill(ref: PinnedRef, text: string, budgetMs: number): Promise<Settled>;
|
|
671
|
+
back(mode: BackMode, budgetMs: number): Promise<Settled>;
|
|
672
|
+
type(text: string, budgetMs: number): Promise<Settled>;
|
|
673
|
+
clearAppState(app: string): Promise<void>;
|
|
674
|
+
resetKeychain(): Promise<void>;
|
|
638
675
|
scroll(direction: ScrollDirection, budgetMs: number): Promise<void>;
|
|
639
676
|
};
|
|
640
677
|
type DeviceSession = {
|
|
@@ -649,6 +686,18 @@ type DeviceSession = {
|
|
|
649
686
|
screenshot(path: string): Promise<string>;
|
|
650
687
|
/** Relaunches the app and re-runs the ready gate. Reported as one step. */
|
|
651
688
|
relaunch(sink: ActionSink): Promise<void>;
|
|
689
|
+
/**
|
|
690
|
+
* Discards the app's stored state, then relaunches and re-runs the ready
|
|
691
|
+
* gate, because an app holding a cleared session in memory has not been
|
|
692
|
+
* cleared. Reported as one step with the relaunch nested under it.
|
|
693
|
+
*/
|
|
694
|
+
clearState(sink: ActionSink): Promise<void>;
|
|
695
|
+
/**
|
|
696
|
+
* Resets the simulator's keychain, which every app on it shares. Separate
|
|
697
|
+
* from `clearState` so an app-scoped clear never wipes another app's store.
|
|
698
|
+
* Reported as one step, with a note on Android where there is nothing to reset.
|
|
699
|
+
*/
|
|
700
|
+
clearKeychain(sink: ActionSink): Promise<void>;
|
|
652
701
|
dismissDevOverlay(): Promise<void>;
|
|
653
702
|
awaitReady(deadline: number): Promise<void>;
|
|
654
703
|
/** Idempotent. Never shuts the simulator down, and reaches `closed` even when the driver call fails. */
|
|
@@ -704,6 +753,25 @@ type FilterOptions = {
|
|
|
704
753
|
type FillOptions = ActionOptions & {
|
|
705
754
|
secret?: boolean;
|
|
706
755
|
};
|
|
756
|
+
type BackOptions = ActionOptions & {
|
|
757
|
+
mode?: BackMode;
|
|
758
|
+
};
|
|
759
|
+
/** `secret` reports a character count instead of the text, the way `fill`'s does. */
|
|
760
|
+
type TypeOptions = ActionOptions & {
|
|
761
|
+
secret?: boolean;
|
|
762
|
+
};
|
|
763
|
+
/**
|
|
764
|
+
* Playwright's `page.keyboard`, cut down to what a device keyboard can do with
|
|
765
|
+
* no target. Everything else touchpress offers needs a locator.
|
|
766
|
+
*/
|
|
767
|
+
type Keyboard = {
|
|
768
|
+
/**
|
|
769
|
+
* Types into whatever holds focus, for a field with nothing to select on,
|
|
770
|
+
* such as a one-time-code input the app focused for itself. Nothing is read
|
|
771
|
+
* back, because there is no target to read.
|
|
772
|
+
*/
|
|
773
|
+
type(text: string, options?: TypeOptions): Promise<void>;
|
|
774
|
+
};
|
|
707
775
|
type Device = {
|
|
708
776
|
/** Matches a node's accessibility name or its value. */
|
|
709
777
|
getByText(text: string | RegExp, options?: TextOptions): Locator;
|
|
@@ -712,6 +780,20 @@ type Device = {
|
|
|
712
780
|
getByTestId(testId: string): Locator;
|
|
713
781
|
locator(query: Query): Locator;
|
|
714
782
|
scroll(direction: ScrollDirection): Promise<void>;
|
|
783
|
+
/**
|
|
784
|
+
* Goes back. Defaults to the platform gesture on Android and to the app's own
|
|
785
|
+
* navigation control on iOS, which has no system back to press.
|
|
786
|
+
*/
|
|
787
|
+
goBack(options?: BackOptions): Promise<void>;
|
|
788
|
+
readonly keyboard: Keyboard;
|
|
789
|
+
/** Discards the app's stored state, then relaunches and waits for the ready gate again. */
|
|
790
|
+
clearState(): Promise<void>;
|
|
791
|
+
/**
|
|
792
|
+
* Resets the simulator's keychain, which is shared by every app on it, so
|
|
793
|
+
* it is not part of `clearState`. A no-op on Android, where clearing state
|
|
794
|
+
* already removes the app's keystore entries.
|
|
795
|
+
*/
|
|
796
|
+
clearKeychain(): Promise<void>;
|
|
715
797
|
/** Relaunches the app and waits for the ready gate again. */
|
|
716
798
|
relaunch(): Promise<void>;
|
|
717
799
|
/** Never automatic, because hiding the overlay would suppress a warning a test might want to see. */
|
|
@@ -850,6 +932,10 @@ type ErrorInfo = {
|
|
|
850
932
|
readonly kind: 'untappable';
|
|
851
933
|
readonly locator: string;
|
|
852
934
|
readonly screen: string;
|
|
935
|
+
} |
|
|
936
|
+
/** Carries no field, because the rejected text is the one thing this error may not hold. */
|
|
937
|
+
{
|
|
938
|
+
readonly kind: 'type-rejected';
|
|
853
939
|
} | {
|
|
854
940
|
readonly kind: 'driver';
|
|
855
941
|
readonly command: string;
|
|
@@ -899,4 +985,4 @@ type PreflightReport = {
|
|
|
899
985
|
*/
|
|
900
986
|
declare function preflight(options: Partial<TouchpressOptions>, driver?: DeviceDriver): Promise<PreflightReport>;
|
|
901
987
|
//#endregion
|
|
902
|
-
export {
|
|
988
|
+
export { DeviceSelection as $, ResolvedOptions as A, renderTitle as B, DeviceSession as C, parseScreen as Ct, sessionName as D, openSession as E, ActionRecord as F, formatFailure as G, ProbeOptions as H, ActionSink as I, Binding as J, probe as K, EvidenceFile as L, TouchpressOptions as M, deviceNameForSlot as N, DeviceChoice as O, parseDeviceOptions as P, DeviceInfo as Q, StepOptions as R, createDevice as S, ScreenNode as St, SessionState as T, resolve as Tt, ProbeResult as U, silentSink as V, ProbeTarget as W, DeviceDriver as X, CaptureOptions as Y, DeviceFailure as Z, Keyboard as _, Platform as _t, ExpectedValue as a, CheckName as at, TextOptions as b, Resolution as bt, ScrollSearch as c, evaluate as ct, directionToward as d, Role as dt, OpenRequest as et, ActionOptions as f, TextMatch as ft, FilterOptions as g, PinnedRef as gt, FillOptions as h, textMatch as ht, ErrorInfo as i, Check as it, TOUCHPRESS_DEFAULTS as j, ReadyQuery as k, ScrollTrail as l, Filter as lt, Device as m, normalizeText as mt, PreflightReport as n, Settled as nt, TouchpressError as o, Verdict as ot, BackOptions as p, describeQuery as pt, BackMode as q, preflight as r, Tree as rt, ScrollDevice as s, describeCheck as st, PreflightDevice as t, ScrollDirection as tt, createScrollSearch as u, Query as ut, Locator as v, RawSnapshot as vt, OpenSessionInput as w, renderScreen as wt, TypeOptions as x, Screen as xt, RoleOptions as y, Rect as yt, Typed as z };
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { promisify } from "node:util";
|
|
1
3
|
import { createAgentDeviceClient, normalizeAgentDeviceError } from "agent-device";
|
|
2
4
|
import pixelmatch from "pixelmatch";
|
|
3
5
|
import { PNG } from "pngjs";
|
|
@@ -49,11 +51,11 @@ function formatError(info) {
|
|
|
49
51
|
info.screen
|
|
50
52
|
].join("\n");
|
|
51
53
|
case "fill-unconfirmed": return [
|
|
52
|
-
`fill left the field holding something else after ${String(info.attempts)} attempts in ${String(info.timeoutMs)}ms.`,
|
|
54
|
+
info.actual === null ? `fill lost the field after ${String(info.attempts)} attempts in ${String(info.timeoutMs)}ms.` : `fill left the field holding something else after ${String(info.attempts)} attempts in ${String(info.timeoutMs)}ms.`,
|
|
53
55
|
``,
|
|
54
56
|
`Locator: ${info.locator}`,
|
|
55
57
|
`Expected value: ${describeValue(info.expected)}${info.expected.kind === "masked" ? " (a secure field reports a mask, not its contents)" : ""}`,
|
|
56
|
-
`Actual value: ${info.actual === null ? "the
|
|
58
|
+
`Actual value: ${info.actual === null ? "unknown, the field no longer resolved after the write" : describeValue(info.actual)}`,
|
|
57
59
|
``,
|
|
58
60
|
`Screen:`,
|
|
59
61
|
info.screen
|
|
@@ -69,6 +71,13 @@ function formatError(info) {
|
|
|
69
71
|
`Screen:`,
|
|
70
72
|
info.screen
|
|
71
73
|
].join("\n");
|
|
74
|
+
case "type-rejected": return [
|
|
75
|
+
"The driver cannot type text whose first word looks like a node reference.",
|
|
76
|
+
"It reads a leading @ followed by a name with a digit in it, or by ref, node,",
|
|
77
|
+
"element or el, as a ref to resolve rather than as characters to send.",
|
|
78
|
+
"",
|
|
79
|
+
"Type it through the field instead, with fill on a locator for that field."
|
|
80
|
+
].join("\n");
|
|
72
81
|
case "driver": return `${info.command} failed: ${describeFailure(info.failure)}`;
|
|
73
82
|
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");
|
|
74
83
|
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");
|
|
@@ -742,6 +751,9 @@ function renderTitle(record) {
|
|
|
742
751
|
case "scroll": return `scroll ${record.direction}`;
|
|
743
752
|
case "scroll-into-view": return `scrollIntoView ${describeQuery(record.query)}`;
|
|
744
753
|
case "relaunch": return `relaunch ${record.app}`;
|
|
754
|
+
case "back": return `back (${record.mode})`;
|
|
755
|
+
case "clear-state": return `clear state of ${record.app}`;
|
|
756
|
+
case "clear-keychain": return "clear keychain";
|
|
745
757
|
case "dismiss-overlay": return "dismiss the React Native dev overlay";
|
|
746
758
|
case "screenshot": return `screenshot ${record.path}`;
|
|
747
759
|
case "act": return `act "${truncate(record.instruction, PROMPT_LIMIT)}"`;
|
|
@@ -868,6 +880,10 @@ function createSession(driver, options, name, binding) {
|
|
|
868
880
|
tap: (ref, budgetMs) => driver.tap(ref, settle(budgetMs)),
|
|
869
881
|
longPress: (ref, durationMs, budgetMs) => driver.longPress(ref, durationMs, settle(budgetMs)),
|
|
870
882
|
fill: (ref, text, budgetMs) => driver.fill(ref, text, settle(budgetMs)),
|
|
883
|
+
back: (mode, budgetMs) => driver.back(mode, settle(budgetMs)),
|
|
884
|
+
type: (text, budgetMs) => driver.type(text, settle(budgetMs)),
|
|
885
|
+
clearAppState: (app) => driver.clearAppState(app),
|
|
886
|
+
resetKeychain: () => driver.resetKeychain(),
|
|
871
887
|
scroll: (direction, budgetMs) => driver.scroll(direction, settle(budgetMs))
|
|
872
888
|
};
|
|
873
889
|
function run(body) {
|
|
@@ -902,15 +918,8 @@ function createSession(driver, options, name, binding) {
|
|
|
902
918
|
screen: renderScreen(screen)
|
|
903
919
|
});
|
|
904
920
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
options,
|
|
908
|
-
state: () => state,
|
|
909
|
-
failure: () => state.phase === "broken" ? state.failure : null,
|
|
910
|
-
run,
|
|
911
|
-
screen: () => run((one) => one.capture()),
|
|
912
|
-
screenshot: (path) => queue.enqueue(() => driver.screenshot(path)),
|
|
913
|
-
relaunch: (sink) => sink.step(renderTitle({
|
|
921
|
+
function relaunch(sink) {
|
|
922
|
+
return sink.step(renderTitle({
|
|
914
923
|
kind: "relaunch",
|
|
915
924
|
app: options.app
|
|
916
925
|
}), async () => {
|
|
@@ -921,6 +930,27 @@ function createSession(driver, options, name, binding) {
|
|
|
921
930
|
}));
|
|
922
931
|
if (options.dismissDevOverlay) await run(() => driver.dismissDevOverlay());
|
|
923
932
|
await awaitReady(Date.now() + options.launchTimeout);
|
|
933
|
+
});
|
|
934
|
+
}
|
|
935
|
+
return {
|
|
936
|
+
name,
|
|
937
|
+
options,
|
|
938
|
+
state: () => state,
|
|
939
|
+
failure: () => state.phase === "broken" ? state.failure : null,
|
|
940
|
+
run,
|
|
941
|
+
screen: () => run((one) => one.capture()),
|
|
942
|
+
screenshot: (path) => queue.enqueue(() => driver.screenshot(path)),
|
|
943
|
+
relaunch,
|
|
944
|
+
clearState: (sink) => sink.step(renderTitle({
|
|
945
|
+
kind: "clear-state",
|
|
946
|
+
app: options.app
|
|
947
|
+
}), async () => {
|
|
948
|
+
await run((one) => one.clearAppState(options.app));
|
|
949
|
+
await relaunch(sink);
|
|
950
|
+
}),
|
|
951
|
+
clearKeychain: (sink) => sink.step(renderTitle({ kind: "clear-keychain" }), async () => {
|
|
952
|
+
if (options.platform === "android") sink.note("keychain", "nothing to reset on Android, clearing state covers the keystore");
|
|
953
|
+
await run((one) => one.resetKeychain());
|
|
924
954
|
}),
|
|
925
955
|
dismissDevOverlay: () => run(() => driver.dismissDevOverlay()),
|
|
926
956
|
awaitReady,
|
|
@@ -1232,6 +1262,25 @@ function createDevice(session, sink) {
|
|
|
1232
1262
|
}), async () => {
|
|
1233
1263
|
await session.run((device) => device.scroll(direction, session.options.actionTimeout));
|
|
1234
1264
|
}),
|
|
1265
|
+
goBack: (options) => {
|
|
1266
|
+
const mode = options?.mode ?? (session.options.platform === "android" ? "system" : "in-app");
|
|
1267
|
+
return sink.step(renderTitle({
|
|
1268
|
+
kind: "back",
|
|
1269
|
+
mode
|
|
1270
|
+
}), async () => {
|
|
1271
|
+
const budget = options?.timeout ?? session.options.actionTimeout;
|
|
1272
|
+
await session.run((device) => device.back(mode, budget));
|
|
1273
|
+
});
|
|
1274
|
+
},
|
|
1275
|
+
keyboard: { type: (text, options) => sink.step(renderTitle({
|
|
1276
|
+
kind: "typed",
|
|
1277
|
+
typed: typedText(text, options)
|
|
1278
|
+
}), async () => {
|
|
1279
|
+
const budget = options?.timeout ?? session.options.actionTimeout;
|
|
1280
|
+
await session.run((device) => device.type(text, budget));
|
|
1281
|
+
}) },
|
|
1282
|
+
clearState: () => session.clearState(sink),
|
|
1283
|
+
clearKeychain: () => session.clearKeychain(sink),
|
|
1235
1284
|
relaunch: () => session.relaunch(sink),
|
|
1236
1285
|
dismissDevOverlay: () => sink.step(renderTitle({ kind: "dismiss-overlay" }), () => session.dismissDevOverlay()),
|
|
1237
1286
|
screen: () => session.screen(),
|
|
@@ -1339,6 +1388,7 @@ function perform(session, sink, record, options, dispatch, write) {
|
|
|
1339
1388
|
let retriedStaleRef = false;
|
|
1340
1389
|
let attempts = 0;
|
|
1341
1390
|
let target = record.query;
|
|
1391
|
+
let written = null;
|
|
1342
1392
|
let lastActual = null;
|
|
1343
1393
|
let screen = await device.capture();
|
|
1344
1394
|
const unconfirmed = (expected) => new TouchpressError({
|
|
@@ -1377,6 +1427,7 @@ function perform(session, sink, record, options, dispatch, write) {
|
|
|
1377
1427
|
typed: typedOf(resolution.node.role, write)
|
|
1378
1428
|
}), () => Promise.resolve(), { box: true });
|
|
1379
1429
|
target = identityOf(screen, resolution.node);
|
|
1430
|
+
written = confirmation;
|
|
1380
1431
|
screen = await device.capture();
|
|
1381
1432
|
lastActual = valueAt(screen, target);
|
|
1382
1433
|
if (lastActual !== null && holds(confirmation, lastActual)) {
|
|
@@ -1393,13 +1444,16 @@ function perform(session, sink, record, options, dispatch, write) {
|
|
|
1393
1444
|
continue;
|
|
1394
1445
|
}
|
|
1395
1446
|
const remaining = deadline - Date.now();
|
|
1396
|
-
if (remaining <= 0)
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1447
|
+
if (remaining <= 0) {
|
|
1448
|
+
if (written !== null) throw unconfirmed(expectedOf(written));
|
|
1449
|
+
throw new TouchpressError({
|
|
1450
|
+
kind: "not-found",
|
|
1451
|
+
locator,
|
|
1452
|
+
timeoutMs: timeout,
|
|
1453
|
+
screen: renderScreen(screen),
|
|
1454
|
+
scrolled: search.trail()
|
|
1455
|
+
});
|
|
1456
|
+
}
|
|
1403
1457
|
if (attempts === 0 && await search.step(screen, target, remaining)) {
|
|
1404
1458
|
screen = await device.capture();
|
|
1405
1459
|
continue;
|
|
@@ -1542,6 +1596,16 @@ function expectedOf(confirmation) {
|
|
|
1542
1596
|
default: throw new Error(`unhandled confirmation ${JSON.stringify(confirmation)}`);
|
|
1543
1597
|
}
|
|
1544
1598
|
}
|
|
1599
|
+
/** A blind type has no node to ask about secrecy, so only the caller can say. */
|
|
1600
|
+
function typedText(text, options) {
|
|
1601
|
+
return options?.secret === true ? {
|
|
1602
|
+
kind: "hidden",
|
|
1603
|
+
length: text.length
|
|
1604
|
+
} : {
|
|
1605
|
+
kind: "text",
|
|
1606
|
+
value: text
|
|
1607
|
+
};
|
|
1608
|
+
}
|
|
1545
1609
|
/** What the report says was typed, verbatim, which the normalized confirmation no longer holds. */
|
|
1546
1610
|
function typedOf(role, write) {
|
|
1547
1611
|
if (role === "secure-text-field" || write.secret) return {
|
|
@@ -1571,18 +1635,29 @@ function valueAt(screen, target) {
|
|
|
1571
1635
|
/**
|
|
1572
1636
|
* How the written node is found again on the next snapshot.
|
|
1573
1637
|
*
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
1576
|
-
*
|
|
1577
|
-
*
|
|
1638
|
+
* A testId rarely names one node. SwiftUI puts one identifier on a field, its
|
|
1639
|
+
* placeholder, and the button under it, and the driver copies an ancestor's
|
|
1640
|
+
* identifier onto every inheriting descendant. The role narrows those to the
|
|
1641
|
+
* field, and failing that the field's position among nodes of its role stands
|
|
1642
|
+
* in. Never the raw tree index: the software keyboard inserts its windows at the
|
|
1643
|
+
* top of the tree, which shifts every index but adds no text field.
|
|
1578
1644
|
*/
|
|
1579
1645
|
function identityOf(screen, node) {
|
|
1580
1646
|
if (node.testId !== null) {
|
|
1581
|
-
const byTestId = {
|
|
1647
|
+
const byTestId = {
|
|
1648
|
+
role: node.role,
|
|
1649
|
+
testId: textMatch(node.testId, true)
|
|
1650
|
+
};
|
|
1582
1651
|
const resolution = resolve(screen, byTestId);
|
|
1583
1652
|
if (resolution.outcome === "one" && resolution.node === node) return byTestId;
|
|
1584
1653
|
}
|
|
1585
|
-
|
|
1654
|
+
const byRole = { role: node.role };
|
|
1655
|
+
const index = matchesOf(screen, byRole).indexOf(node);
|
|
1656
|
+
if (index === -1) throw new Error(`${node.ref} does not resolve under its own role`);
|
|
1657
|
+
return {
|
|
1658
|
+
...byRole,
|
|
1659
|
+
index
|
|
1660
|
+
};
|
|
1586
1661
|
}
|
|
1587
1662
|
//#endregion
|
|
1588
1663
|
//#region src/core/evidence.ts
|
|
@@ -1621,18 +1696,23 @@ function messageOf(error) {
|
|
|
1621
1696
|
function createClient() {
|
|
1622
1697
|
return createAgentDeviceClient({ responseLevel: "full" });
|
|
1623
1698
|
}
|
|
1699
|
+
const execFileAsync = promisify(execFile);
|
|
1700
|
+
const spawnCommand = async (file, args) => {
|
|
1701
|
+
await execFileAsync(file, [...args]);
|
|
1702
|
+
};
|
|
1624
1703
|
/**
|
|
1625
1704
|
* The only file that imports `agent-device`. Two jobs: translate domain requests
|
|
1626
1705
|
* into client calls carrying the session and device selection, and translate the
|
|
1627
1706
|
* driver's error codes into `DeviceFailure`. Snapshot parsing is not one of
|
|
1628
1707
|
* them, because role normalization and label rules live in `core/screen.ts`.
|
|
1629
1708
|
*/
|
|
1630
|
-
function createAgentDeviceDriver(client, session, selection) {
|
|
1709
|
+
function createAgentDeviceDriver(client, session, selection, runCommand = spawnCommand) {
|
|
1631
1710
|
const where = {
|
|
1632
1711
|
session,
|
|
1633
1712
|
platform: selection.platform,
|
|
1634
1713
|
...selection.name === null ? {} : { device: selection.name }
|
|
1635
1714
|
};
|
|
1715
|
+
let udid = null;
|
|
1636
1716
|
async function run(command, body) {
|
|
1637
1717
|
try {
|
|
1638
1718
|
return await body();
|
|
@@ -1659,12 +1739,14 @@ function createAgentDeviceDriver(client, session, selection) {
|
|
|
1659
1739
|
relaunch: request.relaunch,
|
|
1660
1740
|
...request.url === null ? {} : { url: request.url }
|
|
1661
1741
|
});
|
|
1742
|
+
udid = result.identifiers.udid ?? result.identifiers.deviceId ?? null;
|
|
1662
1743
|
return {
|
|
1663
1744
|
session: result.session,
|
|
1664
1745
|
platform: selection.platform,
|
|
1665
1746
|
deviceLabel: result.device?.name ?? result.identifiers.deviceName ?? selection.name ?? selection.platform,
|
|
1666
1747
|
appId: result.appBundleId ?? result.appId ?? request.app,
|
|
1667
|
-
stateDir: result.sessionStateDir ?? null
|
|
1748
|
+
stateDir: result.sessionStateDir ?? null,
|
|
1749
|
+
udid
|
|
1668
1750
|
};
|
|
1669
1751
|
}),
|
|
1670
1752
|
capture: (options) => run("snapshot", () => client.capture.snapshot({
|
|
@@ -1694,6 +1776,56 @@ function createAgentDeviceDriver(client, session, selection) {
|
|
|
1694
1776
|
text,
|
|
1695
1777
|
...settle(options)
|
|
1696
1778
|
}))),
|
|
1779
|
+
back: (mode, options) => run("back", async () => toSettled(await client.command.back({
|
|
1780
|
+
...where,
|
|
1781
|
+
mode,
|
|
1782
|
+
...settle(options)
|
|
1783
|
+
}))),
|
|
1784
|
+
/**
|
|
1785
|
+
* Scrubbed rather than run through `run`, because agent-device echoes the
|
|
1786
|
+
* text it rejected and a password has no business in a report.
|
|
1787
|
+
*/
|
|
1788
|
+
type: async (text, _options) => {
|
|
1789
|
+
if (looksLikeRef(text)) throw new TouchpressError({ kind: "type-rejected" });
|
|
1790
|
+
try {
|
|
1791
|
+
await client.interactions.type({
|
|
1792
|
+
...where,
|
|
1793
|
+
text
|
|
1794
|
+
});
|
|
1795
|
+
return {
|
|
1796
|
+
settled: false,
|
|
1797
|
+
waitedMs: 0
|
|
1798
|
+
};
|
|
1799
|
+
} catch (error) {
|
|
1800
|
+
throw new TouchpressError({
|
|
1801
|
+
kind: "driver",
|
|
1802
|
+
command: "type",
|
|
1803
|
+
failure: withoutText(classifyError(error), text)
|
|
1804
|
+
});
|
|
1805
|
+
}
|
|
1806
|
+
},
|
|
1807
|
+
clearAppState: (app) => run("clearAppState", async () => {
|
|
1808
|
+
await client.settings.update({
|
|
1809
|
+
...where,
|
|
1810
|
+
setting: "clear-app-state",
|
|
1811
|
+
state: "clear",
|
|
1812
|
+
app
|
|
1813
|
+
});
|
|
1814
|
+
}),
|
|
1815
|
+
/**
|
|
1816
|
+
* agent-device has no keychain command, and the simulator keychain is where
|
|
1817
|
+
* clerk-ios and expo-secure-store keep a session. Lives in the driver
|
|
1818
|
+
* because it needs the udid and a process, neither of which the core has.
|
|
1819
|
+
*/
|
|
1820
|
+
resetKeychain: () => run("resetKeychain", async () => {
|
|
1821
|
+
if (selection.platform !== "ios") return;
|
|
1822
|
+
await runCommand("xcrun", [
|
|
1823
|
+
"simctl",
|
|
1824
|
+
"keychain",
|
|
1825
|
+
udid ?? "booted",
|
|
1826
|
+
"reset"
|
|
1827
|
+
]);
|
|
1828
|
+
}),
|
|
1697
1829
|
scroll: (direction, options) => run("scroll", async () => {
|
|
1698
1830
|
await client.interactions.scroll({
|
|
1699
1831
|
...where,
|
|
@@ -1734,6 +1866,25 @@ function readNormalized(error) {
|
|
|
1734
1866
|
return normalizeAgentDeviceError(error);
|
|
1735
1867
|
}
|
|
1736
1868
|
/**
|
|
1869
|
+
* agent-device 0.20.10's own check, copied rather than approximated so the two
|
|
1870
|
+
* agree on what it will refuse. `@e12` and `@ref-x` are refs, while `@word` and
|
|
1871
|
+
* `@ home` are text.
|
|
1872
|
+
*/
|
|
1873
|
+
function looksLikeRef(text) {
|
|
1874
|
+
const word = text.trim().split(/\s+/, 1)[0];
|
|
1875
|
+
if (word === void 0 || !word.startsWith("@") || word.length < 3) return false;
|
|
1876
|
+
const rest = word.slice(1);
|
|
1877
|
+
return /^[A-Za-z_-]*\d[\w-]*$/i.test(rest) || /^(?:ref|node|element|el)[\w-]*$/i.test(rest);
|
|
1878
|
+
}
|
|
1879
|
+
/** Every failure kind carries a `detail`, and the driver puts what it was given into it. */
|
|
1880
|
+
function withoutText(failure, text) {
|
|
1881
|
+
if (text === "") return failure;
|
|
1882
|
+
return {
|
|
1883
|
+
...failure,
|
|
1884
|
+
detail: failure.detail.split(text).join("<typed text>")
|
|
1885
|
+
};
|
|
1886
|
+
}
|
|
1887
|
+
/**
|
|
1737
1888
|
* Timeouts and transport faults both arrive as `COMMAND_FAILED`, so the message
|
|
1738
1889
|
* and `details.reason` separate them. That matches against upstream text, and it
|
|
1739
1890
|
* is confined to this function for exactly that reason.
|