@phone-use/sdk 0.1.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.
@@ -0,0 +1,188 @@
1
+ import { n as BaseDeviceBackend, t as ALL_CAPABILITIES } from "./device-BzPnHvQy.mjs";
2
+ //#region src/testing.ts
3
+ /**
4
+ * The device-free test double the VPS test tier stands on (docs/20 item 2
5
+ * verification protocol). Scripts a sequence of snapshot "screens"; records
6
+ * every call; lets tests simulate screen transitions per action. Zero deps,
7
+ * Node-only. Public via the `@phone-use/sdk/testing` subpath so harness tests
8
+ * and third-party backend authors share it.
9
+ */
10
+ var FakeBackend = class extends BaseDeviceBackend {
11
+ /** Every backend call made, in order. */
12
+ calls = [];
13
+ screens;
14
+ current = 0;
15
+ onAction;
16
+ failWith;
17
+ constructor(opts) {
18
+ super(opts.name ?? "fake", opts.capabilities ?? ALL_CAPABILITIES);
19
+ if (!opts.screens.length) throw new Error("FakeBackend needs at least one screen");
20
+ this.screens = opts.screens;
21
+ this.onAction = opts.onAction;
22
+ this.failWith = opts.failWith;
23
+ }
24
+ /** Jump to screen `i` (throws when out of range). */
25
+ setScreen(i) {
26
+ if (i < 0 || i >= this.screens.length) throw new Error(`no screen ${i}`);
27
+ this.current = i;
28
+ }
29
+ /** Index of the screen currently being served. */
30
+ get screenIndex() {
31
+ return this.current;
32
+ }
33
+ record(method, ...args) {
34
+ const call = {
35
+ method,
36
+ args
37
+ };
38
+ this.calls.push(call);
39
+ const err = this.failWith?.(call);
40
+ if (err !== void 0) throw err;
41
+ const next = this.onAction?.(call, this.current);
42
+ if (next !== void 0) this.setScreen(next);
43
+ return call;
44
+ }
45
+ snapshot(opts) {
46
+ this.record("snapshot", opts);
47
+ return Promise.resolve(this.screens[this.current]);
48
+ }
49
+ screenshot(opts) {
50
+ this.record("screenshot", opts);
51
+ return Promise.resolve({ path: opts.path });
52
+ }
53
+ press(target) {
54
+ this.record("press", target);
55
+ return Promise.resolve();
56
+ }
57
+ longPress(ref, durationMs) {
58
+ this.record("longPress", ref, durationMs);
59
+ return Promise.resolve();
60
+ }
61
+ fill(ref, text) {
62
+ this.record("fill", ref, text);
63
+ return Promise.resolve();
64
+ }
65
+ typeText(text) {
66
+ this.record("typeText", text);
67
+ return Promise.resolve();
68
+ }
69
+ pressKey(key) {
70
+ this.record("pressKey", key);
71
+ return Promise.resolve();
72
+ }
73
+ scroll(direction) {
74
+ this.record("scroll", direction);
75
+ return Promise.resolve();
76
+ }
77
+ pan(x, y, dx, dy, durationMs) {
78
+ this.record("pan", x, y, dx, dy, durationMs);
79
+ return Promise.resolve();
80
+ }
81
+ waitForText(text, timeoutMs) {
82
+ this.record("waitForText", text, timeoutMs);
83
+ return Promise.resolve();
84
+ }
85
+ systemAlert(action) {
86
+ this.record("systemAlert", action);
87
+ return Promise.resolve({ alert: null });
88
+ }
89
+ home() {
90
+ this.record("home");
91
+ return Promise.resolve();
92
+ }
93
+ back() {
94
+ this.record("back");
95
+ return Promise.resolve();
96
+ }
97
+ openApp(opts) {
98
+ this.record("openApp", opts);
99
+ const s = this.screens[this.current];
100
+ return Promise.resolve({
101
+ appName: s.appName,
102
+ appBundleId: s.appBundleId
103
+ });
104
+ }
105
+ listApps() {
106
+ this.record("listApps");
107
+ return Promise.resolve([]);
108
+ }
109
+ closeSession() {
110
+ this.record("closeSession");
111
+ return Promise.resolve();
112
+ }
113
+ };
114
+ const DEFAULT_RECT = {
115
+ x: 0,
116
+ y: 100,
117
+ width: 390,
118
+ height: 44
119
+ };
120
+ /** Fixture sugar: an element with sane defaults (Button role, on-screen rect). */
121
+ function el(partial) {
122
+ return {
123
+ type: "Button",
124
+ label: "",
125
+ rect: { ...DEFAULT_RECT },
126
+ ...partial
127
+ };
128
+ }
129
+ /** Fixture sugar: a screen with an Application root so viewport detection works. */
130
+ function screen(nodes, app = {}) {
131
+ return {
132
+ nodes: [{
133
+ type: "Application",
134
+ rect: {
135
+ x: 0,
136
+ y: 0,
137
+ width: 390,
138
+ height: 844
139
+ }
140
+ }, ...nodes],
141
+ appName: app.appName,
142
+ appBundleId: app.appBundleId
143
+ };
144
+ }
145
+ /**
146
+ * The DI'd process runner for lifecycle tests (docs/19: dependency-inject the
147
+ * spawn/exec runner). Takes an ordered script of steps; each call consumes the
148
+ * next step, optionally asserting the argv shape, and resolves/rejects in the
149
+ * execFile-error shape defaultExecRunner produces.
150
+ */
151
+ function scriptedExecRunner(steps) {
152
+ const queue = [...steps];
153
+ const calls = [];
154
+ const runner = (async (file, args) => {
155
+ calls.push({
156
+ file,
157
+ args: [...args]
158
+ });
159
+ const step = queue.shift();
160
+ if (!step) throw new Error(`scriptedExecRunner: unexpected call ${file} ${args.join(" ")}`);
161
+ if (step.expectArgs && !step.expectArgs(file, args)) throw new Error(`scriptedExecRunner: argv mismatch for ${file} ${args.join(" ")}`);
162
+ if (step.code !== void 0 && step.code !== 0) {
163
+ const err = /* @__PURE__ */ new Error(`Command failed: ${file} ${args.join(" ")}\n${step.stderr ?? ""}`);
164
+ err.code = step.code;
165
+ err.stdout = step.stdout ?? "";
166
+ err.stderr = step.stderr ?? "";
167
+ err.killed = step.killed ?? false;
168
+ throw err;
169
+ }
170
+ if (step.killed) {
171
+ const err = /* @__PURE__ */ new Error(`Command killed: ${file}`);
172
+ err.killed = true;
173
+ err.signal = "SIGTERM";
174
+ throw err;
175
+ }
176
+ return {
177
+ stdout: step.stdout ?? "",
178
+ stderr: step.stderr ?? ""
179
+ };
180
+ });
181
+ runner.calls = calls;
182
+ runner.remaining = () => queue.length;
183
+ return runner;
184
+ }
185
+ //#endregion
186
+ export { FakeBackend, el, screen, scriptedExecRunner };
187
+
188
+ //# sourceMappingURL=testing.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"testing.mjs","names":[],"sources":["../src/testing.ts"],"sourcesContent":["import { BaseDeviceBackend } from './backend.ts';\nimport type {\n AlertAction,\n BackendAlertResult,\n Capability,\n OpenAppResult,\n PressTarget,\n Rect,\n ScrollDirection,\n Snapshot,\n SnapshotNode,\n} from './device.ts';\nimport { ALL_CAPABILITIES } from './device.ts';\n\n/** One scripted screen in a {@link FakeBackend} sequence (alias of Snapshot). */\nexport type FakeScreen = Snapshot;\n/** A recorded backend call: method name plus the arguments it received. */\nexport type FakeCall = { method: string; args: unknown[] };\n\n/**\n * The device-free test double the VPS test tier stands on (docs/20 item 2\n * verification protocol). Scripts a sequence of snapshot \"screens\"; records\n * every call; lets tests simulate screen transitions per action. Zero deps,\n * Node-only. Public via the `@phone-use/sdk/testing` subpath so harness tests\n * and third-party backend authors share it.\n */\nexport class FakeBackend extends BaseDeviceBackend {\n /** Every backend call made, in order. */\n readonly calls: FakeCall[] = [];\n private screens: FakeScreen[];\n private current = 0;\n private readonly onAction: ((call: FakeCall, current: number) => number | undefined) | undefined;\n private readonly failWith: ((call: FakeCall) => unknown) | undefined;\n\n constructor(opts: {\n screens: FakeScreen[];\n name?: string;\n capabilities?: Iterable<Capability>;\n /** Return a new screen index to simulate a transition caused by the call. */\n onAction?: (call: FakeCall, current: number) => number | undefined;\n /** Throw for matching calls — return the error to throw, undefined to pass. */\n failWith?: (call: FakeCall) => unknown;\n }) {\n super(opts.name ?? 'fake', opts.capabilities ?? ALL_CAPABILITIES);\n if (!opts.screens.length) throw new Error('FakeBackend needs at least one screen');\n this.screens = opts.screens;\n this.onAction = opts.onAction;\n this.failWith = opts.failWith;\n }\n\n /** Jump to screen `i` (throws when out of range). */\n setScreen(i: number): void {\n if (i < 0 || i >= this.screens.length) throw new Error(`no screen ${i}`);\n this.current = i;\n }\n\n /** Index of the screen currently being served. */\n get screenIndex(): number {\n return this.current;\n }\n\n private record(method: string, ...args: unknown[]): FakeCall {\n const call: FakeCall = { method, args };\n this.calls.push(call);\n const err = this.failWith?.(call);\n if (err !== undefined) throw err;\n const next = this.onAction?.(call, this.current);\n if (next !== undefined) this.setScreen(next);\n return call;\n }\n\n override snapshot(opts?: {\n interactiveOnly?: boolean | undefined;\n depth?: number | undefined;\n }): Promise<Snapshot> {\n this.record('snapshot', opts);\n return Promise.resolve(this.screens[this.current]!);\n }\n\n override screenshot(opts: { path: string; overlayRefs?: boolean | undefined }): Promise<{ path: string }> {\n this.record('screenshot', opts);\n return Promise.resolve({ path: opts.path }); // records; writes nothing\n }\n\n override press(target: PressTarget): Promise<void> {\n this.record('press', target);\n return Promise.resolve();\n }\n\n override longPress(ref: string, durationMs?: number): Promise<void> {\n this.record('longPress', ref, durationMs);\n return Promise.resolve();\n }\n\n override fill(ref: string, text: string): Promise<void> {\n this.record('fill', ref, text);\n return Promise.resolve();\n }\n\n override typeText(text: string): Promise<void> {\n this.record('typeText', text);\n return Promise.resolve();\n }\n\n override pressKey(key: 'return'): Promise<void> {\n this.record('pressKey', key);\n return Promise.resolve();\n }\n\n override scroll(direction: ScrollDirection): Promise<void> {\n this.record('scroll', direction);\n return Promise.resolve();\n }\n\n override pan(x: number, y: number, dx: number, dy: number, durationMs?: number): Promise<void> {\n this.record('pan', x, y, dx, dy, durationMs);\n return Promise.resolve();\n }\n\n override waitForText(text: string, timeoutMs?: number): Promise<void> {\n this.record('waitForText', text, timeoutMs);\n return Promise.resolve();\n }\n\n override systemAlert(action: AlertAction): Promise<BackendAlertResult> {\n this.record('systemAlert', action);\n return Promise.resolve({ alert: null });\n }\n\n override home(): Promise<void> {\n this.record('home');\n return Promise.resolve();\n }\n\n override back(): Promise<void> {\n this.record('back');\n return Promise.resolve();\n }\n\n override openApp(opts: {\n app?: string | undefined;\n url?: string | undefined;\n relaunch?: boolean | undefined;\n }): Promise<OpenAppResult> {\n this.record('openApp', opts);\n const s = this.screens[this.current]!;\n return Promise.resolve({ appName: s.appName, appBundleId: s.appBundleId });\n }\n\n override listApps(): Promise<string[]> {\n this.record('listApps');\n return Promise.resolve([]);\n }\n\n override closeSession(): Promise<void> {\n this.record('closeSession');\n return Promise.resolve();\n }\n}\n\nconst DEFAULT_RECT: Rect = { x: 0, y: 100, width: 390, height: 44 };\n\n/** Fixture sugar: an element with sane defaults (Button role, on-screen rect). */\nexport function el(partial: Partial<SnapshotNode> & { ref: string }): SnapshotNode {\n return { type: 'Button', label: '', rect: { ...DEFAULT_RECT }, ...partial };\n}\n\n/** Fixture sugar: a screen with an Application root so viewport detection works. */\nexport function screen(\n nodes: SnapshotNode[],\n app: { appName?: string | undefined; appBundleId?: string | undefined } = {},\n): FakeScreen {\n const root: SnapshotNode = { type: 'Application', rect: { x: 0, y: 0, width: 390, height: 844 } };\n return { nodes: [root, ...nodes], appName: app.appName, appBundleId: app.appBundleId };\n}\n\nexport type { ExecOptions, ExecResult, ExecRunner } from './exec.ts';\n\n/** One step in a {@link scriptedExecRunner} script — what the next exec call gets. */\nexport type ScriptedExecStep = {\n /** Optional argv guard — the step throws if the call doesn't match. */\n expectArgs?: ((file: string, args: string[]) => boolean) | undefined;\n stdout?: string | undefined;\n stderr?: string | undefined;\n /** Nonzero → the call rejects with an execFile-shaped error. */\n code?: number | undefined;\n /** Simulate a runner timeout kill. */\n killed?: boolean | undefined;\n};\n\n/** The runner {@link scriptedExecRunner} returns: an ExecRunner plus call inspection. */\nexport type ScriptedExecRunner = import('./exec.ts').ExecRunner & {\n /** Every exec call made, in order. */\n calls: { file: string; args: string[] }[];\n /** Steps not yet consumed (assert 0 at test end for full-sequence checks). */\n remaining(): number;\n};\n\n/**\n * The DI'd process runner for lifecycle tests (docs/19: dependency-inject the\n * spawn/exec runner). Takes an ordered script of steps; each call consumes the\n * next step, optionally asserting the argv shape, and resolves/rejects in the\n * execFile-error shape defaultExecRunner produces.\n */\nexport function scriptedExecRunner(steps: ScriptedExecStep[]): ScriptedExecRunner {\n const queue = [...steps];\n const calls: { file: string; args: string[] }[] = [];\n const runner = (async (file: string, args: string[]) => {\n calls.push({ file, args: [...args] });\n const step = queue.shift();\n if (!step) throw new Error(`scriptedExecRunner: unexpected call ${file} ${args.join(' ')}`);\n if (step.expectArgs && !step.expectArgs(file, args)) {\n throw new Error(`scriptedExecRunner: argv mismatch for ${file} ${args.join(' ')}`);\n }\n if (step.code !== undefined && step.code !== 0) {\n const err = new Error(`Command failed: ${file} ${args.join(' ')}\\n${step.stderr ?? ''}`) as Error & {\n code: number;\n stdout: string;\n stderr: string;\n killed: boolean;\n };\n err.code = step.code;\n err.stdout = step.stdout ?? '';\n err.stderr = step.stderr ?? '';\n err.killed = step.killed ?? false;\n throw err;\n }\n if (step.killed) {\n const err = new Error(`Command killed: ${file}`) as Error & { killed: boolean; signal: string };\n err.killed = true;\n err.signal = 'SIGTERM';\n throw err;\n }\n return { stdout: step.stdout ?? '', stderr: step.stderr ?? '' };\n }) as ScriptedExecRunner;\n runner.calls = calls;\n runner.remaining = () => queue.length;\n return runner;\n}\n"],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@phone-use/sdk",
3
+ "version": "0.1.0",
4
+ "description": "Typed mobile-device SDK — Device, backends, errors, actions",
5
+ "type": "module",
6
+ "license": "Apache-2.0",
7
+ "sideEffects": false,
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/Rajmeet/phone-use.git",
11
+ "directory": "packages/sdk"
12
+ },
13
+ "publishConfig": {
14
+ "access": "public",
15
+ "registry": "https://registry.npmjs.org"
16
+ },
17
+ "engines": {
18
+ "node": ">=22"
19
+ },
20
+ "exports": {
21
+ ".": {
22
+ "types": "./dist/index.d.mts",
23
+ "bun": "./src/index.ts",
24
+ "import": "./dist/index.mjs"
25
+ },
26
+ "./testing": {
27
+ "types": "./dist/testing.d.mts",
28
+ "bun": "./src/testing.ts",
29
+ "import": "./dist/testing.mjs"
30
+ },
31
+ "./package.json": "./package.json"
32
+ },
33
+ "files": [
34
+ "dist",
35
+ "src"
36
+ ],
37
+ "scripts": {
38
+ "build": "tsdown",
39
+ "typecheck": "tsc --noEmit"
40
+ },
41
+ "dependencies": {
42
+ "agent-device": "0.19.3"
43
+ }
44
+ }