agent-device 0.2.4 → 0.2.5
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/README.md +41 -4
- package/dist/src/bin.js +26 -21
- package/dist/src/daemon.js +9 -8
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunner.xcodeproj/project.pbxproj +2 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +15 -0
- package/package.json +3 -2
- package/skills/agent-device/SKILL.md +22 -6
- package/skills/agent-device/references/session-management.md +9 -0
- package/skills/agent-device/references/snapshot-refs.md +18 -5
- package/skills/agent-device/references/video-recording.md +2 -2
- package/src/cli.ts +6 -0
- package/src/core/__tests__/capabilities.test.ts +67 -0
- package/src/core/capabilities.ts +49 -0
- package/src/core/dispatch.ts +29 -118
- package/src/daemon/__tests__/is-predicates.test.ts +68 -0
- package/src/daemon/__tests__/selectors.test.ts +128 -0
- package/src/daemon/__tests__/session-routing.test.ts +108 -0
- package/src/daemon/__tests__/session-selector.test.ts +64 -0
- package/src/daemon/__tests__/session-store.test.ts +95 -0
- package/src/daemon/__tests__/snapshot-processing.test.ts +47 -0
- package/src/daemon/action-utils.ts +29 -0
- package/src/daemon/app-state.ts +66 -0
- package/src/daemon/context.ts +36 -0
- package/src/daemon/device-ready.ts +13 -0
- package/src/daemon/handlers/__tests__/find.test.ts +99 -0
- package/src/daemon/handlers/__tests__/replay-heal.test.ts +364 -0
- package/src/daemon/handlers/__tests__/snapshot.test.ts +128 -0
- package/src/daemon/handlers/find.ts +304 -0
- package/src/daemon/handlers/interaction.ts +510 -0
- package/src/daemon/handlers/parse-utils.ts +8 -0
- package/src/daemon/handlers/record-trace.ts +154 -0
- package/src/daemon/handlers/session.ts +732 -0
- package/src/daemon/handlers/snapshot.ts +396 -0
- package/src/daemon/is-predicates.ts +46 -0
- package/src/daemon/selectors.ts +423 -0
- package/src/daemon/session-routing.ts +22 -0
- package/src/daemon/session-selector.ts +39 -0
- package/src/daemon/session-store.ts +275 -0
- package/src/daemon/snapshot-processing.ts +127 -0
- package/src/daemon/types.ts +55 -0
- package/src/daemon.ts +66 -1592
- package/src/platforms/ios/index.ts +0 -62
- package/src/platforms/ios/runner-client.ts +2 -0
- package/src/utils/args.ts +19 -10
- package/src/utils/interactors.ts +102 -16
- package/src/utils/snapshot.ts +1 -0
|
@@ -83,68 +83,6 @@ export async function closeIosApp(device: DeviceInfo, app: string): Promise<void
|
|
|
83
83
|
]);
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
export async function pressIos(device: DeviceInfo, x: number, y: number): Promise<void> {
|
|
87
|
-
ensureSimulator(device, 'press');
|
|
88
|
-
throw new AppError(
|
|
89
|
-
'UNSUPPORTED_OPERATION',
|
|
90
|
-
'simctl io tap is not available; use the XCTest runner for input',
|
|
91
|
-
);
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
export async function longPressIos(
|
|
95
|
-
device: DeviceInfo,
|
|
96
|
-
x: number,
|
|
97
|
-
y: number,
|
|
98
|
-
durationMs = 800,
|
|
99
|
-
): Promise<void> {
|
|
100
|
-
ensureSimulator(device, 'long-press');
|
|
101
|
-
throw new AppError(
|
|
102
|
-
'UNSUPPORTED_OPERATION',
|
|
103
|
-
'long-press is not supported on iOS simulators without XCTest runner support',
|
|
104
|
-
);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
export async function focusIos(device: DeviceInfo, x: number, y: number): Promise<void> {
|
|
108
|
-
await pressIos(device, x, y);
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
export async function typeIos(device: DeviceInfo, text: string): Promise<void> {
|
|
112
|
-
ensureSimulator(device, 'type');
|
|
113
|
-
throw new AppError(
|
|
114
|
-
'UNSUPPORTED_OPERATION',
|
|
115
|
-
'simctl io keyboard is not available; use the XCTest runner for input',
|
|
116
|
-
);
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
export async function fillIos(
|
|
120
|
-
device: DeviceInfo,
|
|
121
|
-
x: number,
|
|
122
|
-
y: number,
|
|
123
|
-
text: string,
|
|
124
|
-
): Promise<void> {
|
|
125
|
-
await focusIos(device, x, y);
|
|
126
|
-
await typeIos(device, text);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
export async function scrollIos(
|
|
130
|
-
device: DeviceInfo,
|
|
131
|
-
direction: string,
|
|
132
|
-
amount = 0.6,
|
|
133
|
-
): Promise<void> {
|
|
134
|
-
ensureSimulator(device, 'scroll');
|
|
135
|
-
throw new AppError(
|
|
136
|
-
'UNSUPPORTED_OPERATION',
|
|
137
|
-
'simctl io swipe is not available; use the XCTest runner for input',
|
|
138
|
-
);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
export async function scrollIntoViewIos(text: string): Promise<void> {
|
|
142
|
-
throw new AppError(
|
|
143
|
-
'UNSUPPORTED_OPERATION',
|
|
144
|
-
`scrollintoview is not supported on iOS without UI automation (${text})`,
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
86
|
export async function screenshotIos(device: DeviceInfo, outPath: string): Promise<void> {
|
|
149
87
|
if (device.kind === 'simulator') {
|
|
150
88
|
await ensureBootedSimulator(device);
|
|
@@ -11,6 +11,7 @@ import net from 'node:net';
|
|
|
11
11
|
export type RunnerCommand = {
|
|
12
12
|
command:
|
|
13
13
|
| 'tap'
|
|
14
|
+
| 'longPress'
|
|
14
15
|
| 'type'
|
|
15
16
|
| 'swipe'
|
|
16
17
|
| 'findText'
|
|
@@ -27,6 +28,7 @@ export type RunnerCommand = {
|
|
|
27
28
|
action?: 'get' | 'accept' | 'dismiss';
|
|
28
29
|
x?: number;
|
|
29
30
|
y?: number;
|
|
31
|
+
durationMs?: number;
|
|
30
32
|
direction?: 'up' | 'down' | 'left' | 'right';
|
|
31
33
|
scale?: number;
|
|
32
34
|
interactiveOnly?: boolean;
|
package/src/utils/args.ts
CHANGED
|
@@ -21,8 +21,9 @@ export type ParsedArgs = {
|
|
|
21
21
|
appsFilter?: 'launchable' | 'user-installed' | 'all';
|
|
22
22
|
appsMetadata?: boolean;
|
|
23
23
|
activity?: string;
|
|
24
|
+
saveScript?: boolean;
|
|
24
25
|
noRecord?: boolean;
|
|
25
|
-
|
|
26
|
+
replayUpdate?: boolean;
|
|
26
27
|
help: boolean;
|
|
27
28
|
};
|
|
28
29
|
};
|
|
@@ -61,8 +62,12 @@ export function parseArgs(argv: string[]): ParsedArgs {
|
|
|
61
62
|
flags.noRecord = true;
|
|
62
63
|
continue;
|
|
63
64
|
}
|
|
64
|
-
if (arg === '--
|
|
65
|
-
flags.
|
|
65
|
+
if (arg === '--save-script') {
|
|
66
|
+
flags.saveScript = true;
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
if (arg === '--update' || arg === '-u') {
|
|
70
|
+
flags.replayUpdate = true;
|
|
66
71
|
continue;
|
|
67
72
|
}
|
|
68
73
|
if (arg === '--user-installed') {
|
|
@@ -180,17 +185,19 @@ Commands:
|
|
|
180
185
|
back Navigate back (where supported)
|
|
181
186
|
home Go to home screen (where supported)
|
|
182
187
|
app-switcher Open app switcher (where supported)
|
|
183
|
-
wait <ms>|text <text>|@ref [timeoutMs]
|
|
188
|
+
wait <ms>|text <text>|@ref|<selector> [timeoutMs]
|
|
189
|
+
Wait for duration, text, ref, or selector to appear
|
|
184
190
|
alert [get|accept|dismiss|wait] [timeout] Inspect or handle alert (iOS simulator)
|
|
185
|
-
click <@ref>
|
|
186
|
-
get text <@ref>
|
|
187
|
-
get attrs <@ref>
|
|
188
|
-
replay <path>
|
|
191
|
+
click <@ref|selector> Click element by snapshot ref or selector
|
|
192
|
+
get text <@ref|selector> Return element text by ref or selector
|
|
193
|
+
get attrs <@ref|selector> Return element attributes by ref or selector
|
|
194
|
+
replay <path> [--update|-u] Replay a recorded session
|
|
189
195
|
press <x> <y> Tap at coordinates
|
|
190
196
|
long-press <x> <y> [durationMs] Long press (where supported)
|
|
191
197
|
focus <x> <y> Focus input at coordinates
|
|
192
198
|
type <text> Type text in focused field
|
|
193
|
-
fill <x> <y> <text> | fill <@ref> <text>
|
|
199
|
+
fill <x> <y> <text> | fill <@ref|selector> <text>
|
|
200
|
+
Tap then type
|
|
194
201
|
scroll <direction> [amount] Scroll in direction (0-1 amount)
|
|
195
202
|
scrollintoview <text> Scroll until text appears (Android only)
|
|
196
203
|
screenshot [path] Capture screenshot
|
|
@@ -204,6 +211,7 @@ Commands:
|
|
|
204
211
|
find value <value> <action> [value] Find by value
|
|
205
212
|
find role <role> <action> [value] Find by role/type
|
|
206
213
|
find id <id> <action> [value] Find by identifier/resource-id
|
|
214
|
+
is <predicate> <selector> [value] Assert UI state (visible|hidden|exists|editable|selected|text)
|
|
207
215
|
settings <wifi|airplane|location> <on|off> Toggle OS settings (simulators)
|
|
208
216
|
session list List active sessions
|
|
209
217
|
|
|
@@ -216,8 +224,9 @@ Flags:
|
|
|
216
224
|
--session <name> Named session
|
|
217
225
|
--verbose Stream daemon/runner logs
|
|
218
226
|
--json JSON output
|
|
227
|
+
--save-script Save session script (.ad) on close
|
|
219
228
|
--no-record Do not record this action
|
|
220
|
-
--
|
|
229
|
+
--update, -u Replay: update selectors and rewrite replay file in place
|
|
221
230
|
--user-installed Apps: list user-installed packages (Android only)
|
|
222
231
|
--all Apps: list all packages (Android only)
|
|
223
232
|
`;
|
package/src/utils/interactors.ts
CHANGED
|
@@ -15,17 +15,18 @@ import {
|
|
|
15
15
|
} from '../platforms/android/index.ts';
|
|
16
16
|
import {
|
|
17
17
|
closeIosApp,
|
|
18
|
-
fillIos,
|
|
19
|
-
focusIos,
|
|
20
|
-
longPressIos,
|
|
21
18
|
openIosApp,
|
|
22
19
|
openIosDevice,
|
|
23
|
-
pressIos,
|
|
24
|
-
scrollIos,
|
|
25
|
-
scrollIntoViewIos,
|
|
26
20
|
screenshotIos,
|
|
27
|
-
typeIos,
|
|
28
21
|
} from '../platforms/ios/index.ts';
|
|
22
|
+
import { runIosRunnerCommand } from '../platforms/ios/runner-client.ts';
|
|
23
|
+
|
|
24
|
+
export type RunnerContext = {
|
|
25
|
+
appBundleId?: string;
|
|
26
|
+
verbose?: boolean;
|
|
27
|
+
logPath?: string;
|
|
28
|
+
traceLogPath?: string;
|
|
29
|
+
};
|
|
29
30
|
|
|
30
31
|
export type Interactor = {
|
|
31
32
|
open(app: string, options?: { activity?: string }): Promise<void>;
|
|
@@ -37,11 +38,11 @@ export type Interactor = {
|
|
|
37
38
|
type(text: string): Promise<void>;
|
|
38
39
|
fill(x: number, y: number, text: string): Promise<void>;
|
|
39
40
|
scroll(direction: string, amount?: number): Promise<void>;
|
|
40
|
-
scrollIntoView(text: string): Promise<void>;
|
|
41
|
+
scrollIntoView(text: string): Promise<{ attempts?: number } | void>;
|
|
41
42
|
screenshot(outPath: string): Promise<void>;
|
|
42
43
|
};
|
|
43
44
|
|
|
44
|
-
export function getInteractor(device: DeviceInfo): Interactor {
|
|
45
|
+
export function getInteractor(device: DeviceInfo, runnerContext: RunnerContext): Interactor {
|
|
45
46
|
switch (device.platform) {
|
|
46
47
|
case 'android':
|
|
47
48
|
return {
|
|
@@ -62,16 +63,101 @@ export function getInteractor(device: DeviceInfo): Interactor {
|
|
|
62
63
|
open: (app) => openIosApp(device, app),
|
|
63
64
|
openDevice: () => openIosDevice(device),
|
|
64
65
|
close: (app) => closeIosApp(device, app),
|
|
65
|
-
tap: (x, y) => pressIos(device, x, y),
|
|
66
|
-
longPress: (x, y, durationMs) => longPressIos(device, x, y, durationMs),
|
|
67
|
-
focus: (x, y) => focusIos(device, x, y),
|
|
68
|
-
type: (text) => typeIos(device, text),
|
|
69
|
-
fill: (x, y, text) => fillIos(device, x, y, text),
|
|
70
|
-
scroll: (direction, amount) => scrollIos(device, direction, amount),
|
|
71
|
-
scrollIntoView: (text) => scrollIntoViewIos(text),
|
|
72
66
|
screenshot: (outPath) => screenshotIos(device, outPath),
|
|
67
|
+
...iosRunnerOverrides(device, runnerContext),
|
|
73
68
|
};
|
|
74
69
|
default:
|
|
75
70
|
throw new AppError('UNSUPPORTED_PLATFORM', `Unsupported platform: ${device.platform}`);
|
|
76
71
|
}
|
|
77
72
|
}
|
|
73
|
+
|
|
74
|
+
type IoRunnerOverrides = Pick<Interactor, 'tap' | 'longPress' | 'focus' | 'type' | 'fill' | 'scroll' | 'scrollIntoView'>;
|
|
75
|
+
|
|
76
|
+
function iosRunnerOverrides(device: DeviceInfo, ctx: RunnerContext): IoRunnerOverrides {
|
|
77
|
+
const runnerOpts = { verbose: ctx.verbose, logPath: ctx.logPath, traceLogPath: ctx.traceLogPath };
|
|
78
|
+
|
|
79
|
+
return {
|
|
80
|
+
tap: async (x, y) => {
|
|
81
|
+
await runIosRunnerCommand(
|
|
82
|
+
device,
|
|
83
|
+
{ command: 'tap', x, y, appBundleId: ctx.appBundleId },
|
|
84
|
+
runnerOpts,
|
|
85
|
+
);
|
|
86
|
+
},
|
|
87
|
+
longPress: async (x, y, durationMs) => {
|
|
88
|
+
await runIosRunnerCommand(
|
|
89
|
+
device,
|
|
90
|
+
{ command: 'longPress', x, y, durationMs, appBundleId: ctx.appBundleId },
|
|
91
|
+
runnerOpts,
|
|
92
|
+
);
|
|
93
|
+
},
|
|
94
|
+
focus: async (x, y) => {
|
|
95
|
+
await runIosRunnerCommand(
|
|
96
|
+
device,
|
|
97
|
+
{ command: 'tap', x, y, appBundleId: ctx.appBundleId },
|
|
98
|
+
runnerOpts,
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
type: async (text) => {
|
|
102
|
+
await runIosRunnerCommand(
|
|
103
|
+
device,
|
|
104
|
+
{ command: 'type', text, appBundleId: ctx.appBundleId },
|
|
105
|
+
runnerOpts,
|
|
106
|
+
);
|
|
107
|
+
},
|
|
108
|
+
fill: async (x, y, text) => {
|
|
109
|
+
await runIosRunnerCommand(
|
|
110
|
+
device,
|
|
111
|
+
{ command: 'tap', x, y, appBundleId: ctx.appBundleId },
|
|
112
|
+
runnerOpts,
|
|
113
|
+
);
|
|
114
|
+
await runIosRunnerCommand(
|
|
115
|
+
device,
|
|
116
|
+
{ command: 'type', text, clearFirst: true, appBundleId: ctx.appBundleId },
|
|
117
|
+
runnerOpts,
|
|
118
|
+
);
|
|
119
|
+
},
|
|
120
|
+
scroll: async (direction, _amount) => {
|
|
121
|
+
if (!['up', 'down', 'left', 'right'].includes(direction)) {
|
|
122
|
+
throw new AppError('INVALID_ARGS', `Unknown direction: ${direction}`);
|
|
123
|
+
}
|
|
124
|
+
const inverted = invertScrollDirection(direction as 'up' | 'down' | 'left' | 'right');
|
|
125
|
+
await runIosRunnerCommand(
|
|
126
|
+
device,
|
|
127
|
+
{ command: 'swipe', direction: inverted, appBundleId: ctx.appBundleId },
|
|
128
|
+
runnerOpts,
|
|
129
|
+
);
|
|
130
|
+
},
|
|
131
|
+
scrollIntoView: async (text) => {
|
|
132
|
+
const maxAttempts = 8;
|
|
133
|
+
for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
|
|
134
|
+
const found = (await runIosRunnerCommand(
|
|
135
|
+
device,
|
|
136
|
+
{ command: 'findText', text, appBundleId: ctx.appBundleId },
|
|
137
|
+
runnerOpts,
|
|
138
|
+
)) as { found?: boolean };
|
|
139
|
+
if (found?.found) return { attempts: attempt + 1 };
|
|
140
|
+
await runIosRunnerCommand(
|
|
141
|
+
device,
|
|
142
|
+
{ command: 'swipe', direction: 'up', appBundleId: ctx.appBundleId },
|
|
143
|
+
runnerOpts,
|
|
144
|
+
);
|
|
145
|
+
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
146
|
+
}
|
|
147
|
+
throw new AppError('COMMAND_FAILED', `scrollintoview could not find text: ${text}`);
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function invertScrollDirection(direction: 'up' | 'down' | 'left' | 'right'): 'up' | 'down' | 'left' | 'right' {
|
|
153
|
+
switch (direction) {
|
|
154
|
+
case 'up':
|
|
155
|
+
return 'down';
|
|
156
|
+
case 'down':
|
|
157
|
+
return 'up';
|
|
158
|
+
case 'left':
|
|
159
|
+
return 'right';
|
|
160
|
+
case 'right':
|
|
161
|
+
return 'left';
|
|
162
|
+
}
|
|
163
|
+
}
|