agent-device 0.4.2 → 0.5.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/README.md +55 -11
- package/dist/src/50.js +1 -0
- package/dist/src/bin.js +31 -30
- package/dist/src/daemon.js +17 -20
- package/package.json +7 -9
- package/skills/agent-device/SKILL.md +48 -6
- package/skills/agent-device/references/batching.md +79 -0
- package/skills/agent-device/references/permissions.md +3 -15
- package/skills/agent-device/references/snapshot-refs.md +1 -4
- package/dist/bin/axsnapshot +0 -0
- package/dist/src/797.js +0 -1
- package/ios-runner/AXSnapshot/Package.swift +0 -18
- package/ios-runner/AXSnapshot/Sources/AXSnapshot/main.swift +0 -444
- package/src/__tests__/cli-close.test.ts +0 -155
- package/src/__tests__/cli-help.test.ts +0 -102
- package/src/bin.ts +0 -3
- package/src/cli.ts +0 -305
- package/src/core/__tests__/capabilities.test.ts +0 -75
- package/src/core/__tests__/dispatch-open.test.ts +0 -25
- package/src/core/__tests__/open-target.test.ts +0 -55
- package/src/core/capabilities.ts +0 -57
- package/src/core/dispatch.ts +0 -382
- package/src/core/open-target.ts +0 -27
- package/src/daemon/__tests__/device-ready.test.ts +0 -52
- package/src/daemon/__tests__/is-predicates.test.ts +0 -68
- package/src/daemon/__tests__/selectors.test.ts +0 -261
- package/src/daemon/__tests__/session-routing.test.ts +0 -108
- package/src/daemon/__tests__/session-selector.test.ts +0 -64
- package/src/daemon/__tests__/session-store.test.ts +0 -142
- package/src/daemon/__tests__/snapshot-processing.test.ts +0 -47
- package/src/daemon/action-utils.ts +0 -29
- package/src/daemon/context.ts +0 -48
- package/src/daemon/device-ready.ts +0 -155
- package/src/daemon/handlers/__tests__/find.test.ts +0 -99
- package/src/daemon/handlers/__tests__/interaction.test.ts +0 -22
- package/src/daemon/handlers/__tests__/replay-heal.test.ts +0 -509
- package/src/daemon/handlers/__tests__/session-reinstall.test.ts +0 -219
- package/src/daemon/handlers/__tests__/session.test.ts +0 -820
- package/src/daemon/handlers/__tests__/snapshot-handler.test.ts +0 -92
- package/src/daemon/handlers/__tests__/snapshot.test.ts +0 -128
- package/src/daemon/handlers/find.ts +0 -324
- package/src/daemon/handlers/interaction.ts +0 -550
- package/src/daemon/handlers/parse-utils.ts +0 -8
- package/src/daemon/handlers/record-trace.ts +0 -154
- package/src/daemon/handlers/session.ts +0 -1137
- package/src/daemon/handlers/snapshot.ts +0 -439
- package/src/daemon/is-predicates.ts +0 -46
- package/src/daemon/selectors.ts +0 -540
- package/src/daemon/session-routing.ts +0 -22
- package/src/daemon/session-selector.ts +0 -39
- package/src/daemon/session-store.ts +0 -296
- package/src/daemon/snapshot-processing.ts +0 -131
- package/src/daemon/types.ts +0 -56
- package/src/daemon-client.ts +0 -272
- package/src/daemon.ts +0 -295
- package/src/platforms/__tests__/boot-diagnostics.test.ts +0 -59
- package/src/platforms/android/__tests__/index.test.ts +0 -274
- package/src/platforms/android/devices.ts +0 -196
- package/src/platforms/android/index.ts +0 -784
- package/src/platforms/android/ui-hierarchy.ts +0 -312
- package/src/platforms/boot-diagnostics.ts +0 -128
- package/src/platforms/ios/__tests__/index.test.ts +0 -312
- package/src/platforms/ios/__tests__/runner-client.test.ts +0 -155
- package/src/platforms/ios/apps.ts +0 -358
- package/src/platforms/ios/ax-snapshot.ts +0 -207
- package/src/platforms/ios/config.ts +0 -28
- package/src/platforms/ios/devicectl.ts +0 -134
- package/src/platforms/ios/devices.ts +0 -100
- package/src/platforms/ios/index.ts +0 -20
- package/src/platforms/ios/runner-client.ts +0 -994
- package/src/platforms/ios/simulator.ts +0 -164
- package/src/utils/__tests__/args.test.ts +0 -239
- package/src/utils/__tests__/daemon-client.test.ts +0 -95
- package/src/utils/__tests__/exec.test.ts +0 -16
- package/src/utils/__tests__/finders.test.ts +0 -34
- package/src/utils/__tests__/keyed-lock.test.ts +0 -55
- package/src/utils/__tests__/process-identity.test.ts +0 -33
- package/src/utils/__tests__/retry.test.ts +0 -44
- package/src/utils/args.ts +0 -239
- package/src/utils/command-schema.ts +0 -622
- package/src/utils/device.ts +0 -84
- package/src/utils/errors.ts +0 -35
- package/src/utils/exec.ts +0 -339
- package/src/utils/finders.ts +0 -101
- package/src/utils/interactive.ts +0 -4
- package/src/utils/interactors.ts +0 -173
- package/src/utils/keyed-lock.ts +0 -14
- package/src/utils/output.ts +0 -204
- package/src/utils/process-identity.ts +0 -100
- package/src/utils/retry.ts +0 -180
- package/src/utils/snapshot.ts +0 -64
- package/src/utils/timeouts.ts +0 -9
- package/src/utils/version.ts +0 -26
|
@@ -1,439 +0,0 @@
|
|
|
1
|
-
import { dispatchCommand, resolveTargetDevice } from '../../core/dispatch.ts';
|
|
2
|
-
import { isCommandSupportedOnDevice } from '../../core/capabilities.ts';
|
|
3
|
-
import { runIosRunnerCommand, stopIosRunnerSession } from '../../platforms/ios/runner-client.ts';
|
|
4
|
-
import { snapshotAndroid } from '../../platforms/android/index.ts';
|
|
5
|
-
import {
|
|
6
|
-
attachRefs,
|
|
7
|
-
findNodeByRef,
|
|
8
|
-
normalizeRef,
|
|
9
|
-
type RawSnapshotNode,
|
|
10
|
-
type SnapshotState,
|
|
11
|
-
} from '../../utils/snapshot.ts';
|
|
12
|
-
import type { DaemonRequest, DaemonResponse, SessionState } from '../types.ts';
|
|
13
|
-
import { SessionStore } from '../session-store.ts';
|
|
14
|
-
import { contextFromFlags } from '../context.ts';
|
|
15
|
-
import { ensureDeviceReady } from '../device-ready.ts';
|
|
16
|
-
import { findNodeByLabel, pruneGroupNodes, resolveRefLabel } from '../snapshot-processing.ts';
|
|
17
|
-
import { findSelectorChainMatch, splitSelectorFromArgs, tryParseSelectorChain, type SelectorChain } from '../selectors.ts';
|
|
18
|
-
import { parseTimeout, POLL_INTERVAL_MS, DEFAULT_TIMEOUT_MS } from './parse-utils.ts';
|
|
19
|
-
|
|
20
|
-
export async function handleSnapshotCommands(params: {
|
|
21
|
-
req: DaemonRequest;
|
|
22
|
-
sessionName: string;
|
|
23
|
-
logPath: string;
|
|
24
|
-
sessionStore: SessionStore;
|
|
25
|
-
}): Promise<DaemonResponse | null> {
|
|
26
|
-
const { req, sessionName, logPath, sessionStore } = params;
|
|
27
|
-
const command = req.command;
|
|
28
|
-
|
|
29
|
-
if (command === 'snapshot') {
|
|
30
|
-
const { session, device } = await resolveSessionDevice(sessionStore, sessionName, req.flags);
|
|
31
|
-
if (!isCommandSupportedOnDevice('snapshot', device)) {
|
|
32
|
-
return {
|
|
33
|
-
ok: false,
|
|
34
|
-
error: {
|
|
35
|
-
code: 'UNSUPPORTED_OPERATION',
|
|
36
|
-
message: 'snapshot is not supported on this device',
|
|
37
|
-
},
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
if (device.platform === 'ios' && device.kind === 'device' && req.flags?.snapshotBackend === 'ax') {
|
|
41
|
-
return {
|
|
42
|
-
ok: false,
|
|
43
|
-
error: {
|
|
44
|
-
code: 'UNSUPPORTED_OPERATION',
|
|
45
|
-
message: 'AX snapshot backend is not supported on iOS physical devices; use --backend xctest',
|
|
46
|
-
},
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
const appBundleId = session?.appBundleId;
|
|
50
|
-
let snapshotScope = req.flags?.snapshotScope;
|
|
51
|
-
if (snapshotScope && snapshotScope.trim().startsWith('@')) {
|
|
52
|
-
if (!session?.snapshot) {
|
|
53
|
-
return {
|
|
54
|
-
ok: false,
|
|
55
|
-
error: {
|
|
56
|
-
code: 'INVALID_ARGS',
|
|
57
|
-
message: 'Ref scope requires an existing snapshot in session.',
|
|
58
|
-
},
|
|
59
|
-
};
|
|
60
|
-
}
|
|
61
|
-
const ref = normalizeRef(snapshotScope.trim());
|
|
62
|
-
if (!ref) {
|
|
63
|
-
return {
|
|
64
|
-
ok: false,
|
|
65
|
-
error: { code: 'INVALID_ARGS', message: `Invalid ref scope: ${snapshotScope}` },
|
|
66
|
-
};
|
|
67
|
-
}
|
|
68
|
-
const node = findNodeByRef(session.snapshot.nodes, ref);
|
|
69
|
-
const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined;
|
|
70
|
-
if (!resolved) {
|
|
71
|
-
return {
|
|
72
|
-
ok: false,
|
|
73
|
-
error: {
|
|
74
|
-
code: 'COMMAND_FAILED',
|
|
75
|
-
message: `Ref ${snapshotScope} not found or has no label`,
|
|
76
|
-
},
|
|
77
|
-
};
|
|
78
|
-
}
|
|
79
|
-
snapshotScope = resolved;
|
|
80
|
-
}
|
|
81
|
-
return await withSessionlessRunnerCleanup(session, device, async () => {
|
|
82
|
-
const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
|
|
83
|
-
...contextFromFlags(
|
|
84
|
-
logPath,
|
|
85
|
-
{ ...req.flags, snapshotScope },
|
|
86
|
-
appBundleId,
|
|
87
|
-
session?.trace?.outPath,
|
|
88
|
-
),
|
|
89
|
-
})) as {
|
|
90
|
-
nodes?: RawSnapshotNode[];
|
|
91
|
-
truncated?: boolean;
|
|
92
|
-
backend?: 'ax' | 'xctest' | 'android';
|
|
93
|
-
};
|
|
94
|
-
const rawNodes = data?.nodes ?? [];
|
|
95
|
-
const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
|
|
96
|
-
const snapshot: SnapshotState = {
|
|
97
|
-
nodes,
|
|
98
|
-
truncated: data?.truncated,
|
|
99
|
-
createdAt: Date.now(),
|
|
100
|
-
backend: data?.backend,
|
|
101
|
-
};
|
|
102
|
-
const nextSession: SessionState = session
|
|
103
|
-
? { ...session, snapshot }
|
|
104
|
-
: { name: sessionName, device, createdAt: Date.now(), appBundleId, snapshot, actions: [] };
|
|
105
|
-
recordIfSession(sessionStore, nextSession, req, {
|
|
106
|
-
nodes: nodes.length,
|
|
107
|
-
truncated: data?.truncated ?? false,
|
|
108
|
-
});
|
|
109
|
-
sessionStore.set(sessionName, nextSession);
|
|
110
|
-
return {
|
|
111
|
-
ok: true,
|
|
112
|
-
data: {
|
|
113
|
-
nodes,
|
|
114
|
-
truncated: data?.truncated ?? false,
|
|
115
|
-
appName: nextSession.appBundleId
|
|
116
|
-
? (nextSession.appName ?? nextSession.appBundleId)
|
|
117
|
-
: undefined,
|
|
118
|
-
appBundleId: nextSession.appBundleId,
|
|
119
|
-
},
|
|
120
|
-
};
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
if (command === 'wait') {
|
|
125
|
-
const { session, device } = await resolveSessionDevice(sessionStore, sessionName, req.flags);
|
|
126
|
-
const args = req.positionals ?? [];
|
|
127
|
-
const parsed = parseWaitArgs(args);
|
|
128
|
-
if (!parsed) {
|
|
129
|
-
return {
|
|
130
|
-
ok: false,
|
|
131
|
-
error: { code: 'INVALID_ARGS', message: 'wait requires a duration or text' },
|
|
132
|
-
};
|
|
133
|
-
}
|
|
134
|
-
if (parsed.kind === 'sleep') {
|
|
135
|
-
await new Promise((resolve) => setTimeout(resolve, parsed.durationMs));
|
|
136
|
-
recordIfSession(sessionStore, session, req, { waitedMs: parsed.durationMs });
|
|
137
|
-
return { ok: true, data: { waitedMs: parsed.durationMs } };
|
|
138
|
-
}
|
|
139
|
-
if (!isCommandSupportedOnDevice('wait', device)) {
|
|
140
|
-
return {
|
|
141
|
-
ok: false,
|
|
142
|
-
error: { code: 'UNSUPPORTED_OPERATION', message: 'wait is not supported on this device' },
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
return await withSessionlessRunnerCleanup(session, device, async () => {
|
|
146
|
-
let text: string;
|
|
147
|
-
let timeoutMs: number | null;
|
|
148
|
-
if (parsed.kind === 'selector') {
|
|
149
|
-
const timeout = parsed.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
150
|
-
const start = Date.now();
|
|
151
|
-
while (Date.now() - start < timeout) {
|
|
152
|
-
const data = (await dispatchCommand(device, 'snapshot', [], req.flags?.out, {
|
|
153
|
-
...contextFromFlags(
|
|
154
|
-
logPath,
|
|
155
|
-
{
|
|
156
|
-
...req.flags,
|
|
157
|
-
snapshotInteractiveOnly: false,
|
|
158
|
-
snapshotCompact: false,
|
|
159
|
-
},
|
|
160
|
-
session?.appBundleId,
|
|
161
|
-
session?.trace?.outPath,
|
|
162
|
-
),
|
|
163
|
-
})) as {
|
|
164
|
-
nodes?: RawSnapshotNode[];
|
|
165
|
-
truncated?: boolean;
|
|
166
|
-
backend?: 'ax' | 'xctest' | 'android';
|
|
167
|
-
};
|
|
168
|
-
const rawNodes = data?.nodes ?? [];
|
|
169
|
-
const nodes = attachRefs(req.flags?.snapshotRaw ? rawNodes : pruneGroupNodes(rawNodes));
|
|
170
|
-
if (session) {
|
|
171
|
-
session.snapshot = {
|
|
172
|
-
nodes,
|
|
173
|
-
truncated: data?.truncated,
|
|
174
|
-
createdAt: Date.now(),
|
|
175
|
-
backend: data?.backend,
|
|
176
|
-
};
|
|
177
|
-
sessionStore.set(sessionName, session);
|
|
178
|
-
}
|
|
179
|
-
const match = findSelectorChainMatch(nodes, parsed.selector, { platform: device.platform });
|
|
180
|
-
if (match) {
|
|
181
|
-
recordIfSession(sessionStore, session, req, {
|
|
182
|
-
selector: match.selector.raw,
|
|
183
|
-
waitedMs: Date.now() - start,
|
|
184
|
-
});
|
|
185
|
-
return {
|
|
186
|
-
ok: true,
|
|
187
|
-
data: {
|
|
188
|
-
selector: match.selector.raw,
|
|
189
|
-
waitedMs: Date.now() - start,
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
194
|
-
}
|
|
195
|
-
return {
|
|
196
|
-
ok: false,
|
|
197
|
-
error: {
|
|
198
|
-
code: 'COMMAND_FAILED',
|
|
199
|
-
message: `wait timed out for selector: ${parsed.selectorExpression}`,
|
|
200
|
-
},
|
|
201
|
-
};
|
|
202
|
-
} else if (parsed.kind === 'ref') {
|
|
203
|
-
if (!session?.snapshot) {
|
|
204
|
-
return {
|
|
205
|
-
ok: false,
|
|
206
|
-
error: {
|
|
207
|
-
code: 'INVALID_ARGS',
|
|
208
|
-
message: 'Ref wait requires an existing snapshot in session.',
|
|
209
|
-
},
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
const ref = normalizeRef(parsed.rawRef);
|
|
213
|
-
if (!ref) {
|
|
214
|
-
return {
|
|
215
|
-
ok: false,
|
|
216
|
-
error: { code: 'INVALID_ARGS', message: `Invalid ref: ${parsed.rawRef}` },
|
|
217
|
-
};
|
|
218
|
-
}
|
|
219
|
-
const node = findNodeByRef(session.snapshot.nodes, ref);
|
|
220
|
-
const resolved = node ? resolveRefLabel(node, session.snapshot.nodes) : undefined;
|
|
221
|
-
if (!resolved) {
|
|
222
|
-
return {
|
|
223
|
-
ok: false,
|
|
224
|
-
error: {
|
|
225
|
-
code: 'COMMAND_FAILED',
|
|
226
|
-
message: `Ref ${parsed.rawRef} not found or has no label`,
|
|
227
|
-
},
|
|
228
|
-
};
|
|
229
|
-
}
|
|
230
|
-
text = resolved;
|
|
231
|
-
timeoutMs = parsed.timeoutMs;
|
|
232
|
-
} else {
|
|
233
|
-
text = parsed.text;
|
|
234
|
-
timeoutMs = parsed.timeoutMs;
|
|
235
|
-
}
|
|
236
|
-
if (!text) {
|
|
237
|
-
return { ok: false, error: { code: 'INVALID_ARGS', message: 'wait requires text' } };
|
|
238
|
-
}
|
|
239
|
-
const timeout = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
240
|
-
const start = Date.now();
|
|
241
|
-
while (Date.now() - start < timeout) {
|
|
242
|
-
if (device.platform === 'ios') {
|
|
243
|
-
const result = (await runIosRunnerCommand(
|
|
244
|
-
device,
|
|
245
|
-
{ command: 'findText', text, appBundleId: session?.appBundleId },
|
|
246
|
-
{ verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
|
|
247
|
-
)) as { found?: boolean };
|
|
248
|
-
if (result?.found) {
|
|
249
|
-
recordIfSession(sessionStore, session, req, { text, waitedMs: Date.now() - start });
|
|
250
|
-
return { ok: true, data: { text, waitedMs: Date.now() - start } };
|
|
251
|
-
}
|
|
252
|
-
} else if (device.platform === 'android') {
|
|
253
|
-
const androidResult = await snapshotAndroid(device, { scope: text });
|
|
254
|
-
if (findNodeByLabel(attachRefs(androidResult.nodes ?? []), text)) {
|
|
255
|
-
recordIfSession(sessionStore, session, req, { text, waitedMs: Date.now() - start });
|
|
256
|
-
return { ok: true, data: { text, waitedMs: Date.now() - start } };
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
260
|
-
}
|
|
261
|
-
return {
|
|
262
|
-
ok: false,
|
|
263
|
-
error: { code: 'COMMAND_FAILED', message: `wait timed out for text: ${text}` },
|
|
264
|
-
};
|
|
265
|
-
});
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
if (command === 'alert') {
|
|
269
|
-
const { session, device } = await resolveSessionDevice(sessionStore, sessionName, req.flags);
|
|
270
|
-
const action = (req.positionals?.[0] ?? 'get').toLowerCase();
|
|
271
|
-
if (!isCommandSupportedOnDevice('alert', device)) {
|
|
272
|
-
return {
|
|
273
|
-
ok: false,
|
|
274
|
-
error: {
|
|
275
|
-
code: 'UNSUPPORTED_OPERATION',
|
|
276
|
-
message: 'alert is only supported on iOS simulators',
|
|
277
|
-
},
|
|
278
|
-
};
|
|
279
|
-
}
|
|
280
|
-
return await withSessionlessRunnerCleanup(session, device, async () => {
|
|
281
|
-
if (action === 'wait') {
|
|
282
|
-
const timeout = parseTimeout(req.positionals?.[1]) ?? DEFAULT_TIMEOUT_MS;
|
|
283
|
-
const start = Date.now();
|
|
284
|
-
while (Date.now() - start < timeout) {
|
|
285
|
-
try {
|
|
286
|
-
const data = await runIosRunnerCommand(
|
|
287
|
-
device,
|
|
288
|
-
{ command: 'alert', action: 'get', appBundleId: session?.appBundleId },
|
|
289
|
-
{ verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
|
|
290
|
-
);
|
|
291
|
-
recordIfSession(sessionStore, session, req, data as Record<string, unknown>);
|
|
292
|
-
return { ok: true, data };
|
|
293
|
-
} catch {
|
|
294
|
-
// keep waiting
|
|
295
|
-
}
|
|
296
|
-
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
297
|
-
}
|
|
298
|
-
return { ok: false, error: { code: 'COMMAND_FAILED', message: 'alert wait timed out' } };
|
|
299
|
-
}
|
|
300
|
-
const data = await runIosRunnerCommand(
|
|
301
|
-
device,
|
|
302
|
-
{
|
|
303
|
-
command: 'alert',
|
|
304
|
-
action:
|
|
305
|
-
action === 'accept' || action === 'dismiss' ? (action as 'accept' | 'dismiss') : 'get',
|
|
306
|
-
appBundleId: session?.appBundleId,
|
|
307
|
-
},
|
|
308
|
-
{ verbose: req.flags?.verbose, logPath, traceLogPath: session?.trace?.outPath },
|
|
309
|
-
);
|
|
310
|
-
recordIfSession(sessionStore, session, req, data as Record<string, unknown>);
|
|
311
|
-
return { ok: true, data };
|
|
312
|
-
});
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
if (command === 'settings') {
|
|
316
|
-
const setting = req.positionals?.[0];
|
|
317
|
-
const state = req.positionals?.[1];
|
|
318
|
-
if (!setting || !state) {
|
|
319
|
-
return {
|
|
320
|
-
ok: false,
|
|
321
|
-
error: {
|
|
322
|
-
code: 'INVALID_ARGS',
|
|
323
|
-
message: 'settings requires <wifi|airplane|location> <on|off>',
|
|
324
|
-
},
|
|
325
|
-
};
|
|
326
|
-
}
|
|
327
|
-
const { session, device } = await resolveSessionDevice(sessionStore, sessionName, req.flags);
|
|
328
|
-
if (!isCommandSupportedOnDevice('settings', device)) {
|
|
329
|
-
return {
|
|
330
|
-
ok: false,
|
|
331
|
-
error: {
|
|
332
|
-
code: 'UNSUPPORTED_OPERATION',
|
|
333
|
-
message: 'settings is not supported on this device',
|
|
334
|
-
},
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
return await withSessionlessRunnerCleanup(session, device, async () => {
|
|
338
|
-
const appBundleId = session?.appBundleId;
|
|
339
|
-
const data = await dispatchCommand(
|
|
340
|
-
device,
|
|
341
|
-
'settings',
|
|
342
|
-
[setting, state, appBundleId ?? ''],
|
|
343
|
-
req.flags?.out,
|
|
344
|
-
{
|
|
345
|
-
...contextFromFlags(logPath, req.flags, appBundleId, session?.trace?.outPath),
|
|
346
|
-
},
|
|
347
|
-
);
|
|
348
|
-
recordIfSession(sessionStore, session, req, data ?? { setting, state });
|
|
349
|
-
return { ok: true, data: data ?? { setting, state } };
|
|
350
|
-
});
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
return null;
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
type WaitParsed =
|
|
357
|
-
| { kind: 'sleep'; durationMs: number }
|
|
358
|
-
| { kind: 'ref'; rawRef: string; timeoutMs: number | null }
|
|
359
|
-
| { kind: 'selector'; selector: SelectorChain; selectorExpression: string; timeoutMs: number | null }
|
|
360
|
-
| { kind: 'text'; text: string; timeoutMs: number | null };
|
|
361
|
-
|
|
362
|
-
export function parseWaitArgs(args: string[]): WaitParsed | null {
|
|
363
|
-
if (args.length === 0) return null;
|
|
364
|
-
|
|
365
|
-
const sleepMs = parseTimeout(args[0]);
|
|
366
|
-
if (sleepMs !== null) return { kind: 'sleep', durationMs: sleepMs };
|
|
367
|
-
|
|
368
|
-
if (args[0] === 'text') {
|
|
369
|
-
const timeoutMs = parseTimeout(args[args.length - 1]);
|
|
370
|
-
const text = timeoutMs !== null ? args.slice(1, -1).join(' ') : args.slice(1).join(' ');
|
|
371
|
-
return { kind: 'text', text: text.trim(), timeoutMs };
|
|
372
|
-
}
|
|
373
|
-
|
|
374
|
-
if (args[0].startsWith('@')) {
|
|
375
|
-
const timeoutMs = parseTimeout(args[args.length - 1]);
|
|
376
|
-
return { kind: 'ref', rawRef: args[0], timeoutMs };
|
|
377
|
-
}
|
|
378
|
-
|
|
379
|
-
const timeoutMs = parseTimeout(args[args.length - 1]);
|
|
380
|
-
const argsWithoutTimeout = timeoutMs !== null ? args.slice(0, -1) : args.slice();
|
|
381
|
-
const split = splitSelectorFromArgs(argsWithoutTimeout);
|
|
382
|
-
if (split && split.rest.length === 0) {
|
|
383
|
-
const selector = tryParseSelectorChain(split.selectorExpression);
|
|
384
|
-
if (selector) {
|
|
385
|
-
return {
|
|
386
|
-
kind: 'selector',
|
|
387
|
-
selector,
|
|
388
|
-
selectorExpression: split.selectorExpression,
|
|
389
|
-
timeoutMs,
|
|
390
|
-
};
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
|
|
394
|
-
const text = timeoutMs !== null ? args.slice(0, -1).join(' ') : args.join(' ');
|
|
395
|
-
return { kind: 'text', text: text.trim(), timeoutMs };
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
async function resolveSessionDevice(
|
|
399
|
-
sessionStore: SessionStore,
|
|
400
|
-
sessionName: string,
|
|
401
|
-
flags: DaemonRequest['flags'],
|
|
402
|
-
) {
|
|
403
|
-
const session = sessionStore.get(sessionName);
|
|
404
|
-
const device = session?.device ?? (await resolveTargetDevice(flags ?? {}));
|
|
405
|
-
if (!session) await ensureDeviceReady(device);
|
|
406
|
-
return { session, device };
|
|
407
|
-
}
|
|
408
|
-
|
|
409
|
-
async function withSessionlessRunnerCleanup<T>(
|
|
410
|
-
session: SessionState | undefined,
|
|
411
|
-
device: SessionState['device'],
|
|
412
|
-
task: () => Promise<T>,
|
|
413
|
-
): Promise<T> {
|
|
414
|
-
const shouldCleanupSessionlessIosRunner = !session && device.platform === 'ios';
|
|
415
|
-
try {
|
|
416
|
-
return await task();
|
|
417
|
-
} finally {
|
|
418
|
-
// Sessionless iOS commands intentionally stop the runner to avoid leaked xcodebuild processes.
|
|
419
|
-
// For multi-command flows, keep an active session via `open` so the runner can be reused.
|
|
420
|
-
if (shouldCleanupSessionlessIosRunner) {
|
|
421
|
-
await stopIosRunnerSession(device.id);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
function recordIfSession(
|
|
427
|
-
sessionStore: SessionStore,
|
|
428
|
-
session: SessionState | undefined,
|
|
429
|
-
req: DaemonRequest,
|
|
430
|
-
result: Record<string, unknown>,
|
|
431
|
-
): void {
|
|
432
|
-
if (!session) return;
|
|
433
|
-
sessionStore.recordAction(session, {
|
|
434
|
-
command: req.command,
|
|
435
|
-
positionals: req.positionals ?? [],
|
|
436
|
-
flags: req.flags ?? {},
|
|
437
|
-
result,
|
|
438
|
-
});
|
|
439
|
-
}
|
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import type { SnapshotState } from '../utils/snapshot.ts';
|
|
2
|
-
import { extractNodeText } from './snapshot-processing.ts';
|
|
3
|
-
import { isNodeEditable, isNodeVisible } from './selectors.ts';
|
|
4
|
-
|
|
5
|
-
export type IsPredicate = 'visible' | 'hidden' | 'exists' | 'editable' | 'selected' | 'text';
|
|
6
|
-
|
|
7
|
-
export function isSupportedPredicate(input: string): input is IsPredicate {
|
|
8
|
-
return ['visible', 'hidden', 'exists', 'editable', 'selected', 'text'].includes(input);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function evaluateIsPredicate(params: {
|
|
12
|
-
predicate: Exclude<IsPredicate, 'exists'>;
|
|
13
|
-
node: SnapshotState['nodes'][number];
|
|
14
|
-
expectedText?: string;
|
|
15
|
-
platform: 'ios' | 'android';
|
|
16
|
-
}): { pass: boolean; actualText: string; details: string } {
|
|
17
|
-
const { predicate, node, expectedText, platform } = params;
|
|
18
|
-
const actualText = extractNodeText(node);
|
|
19
|
-
let pass = false;
|
|
20
|
-
switch (predicate) {
|
|
21
|
-
case 'visible':
|
|
22
|
-
pass = isNodeVisible(node);
|
|
23
|
-
break;
|
|
24
|
-
case 'hidden':
|
|
25
|
-
pass = !isNodeVisible(node);
|
|
26
|
-
break;
|
|
27
|
-
case 'editable':
|
|
28
|
-
pass = isNodeEditable(node, platform);
|
|
29
|
-
break;
|
|
30
|
-
case 'selected':
|
|
31
|
-
pass = node.selected === true;
|
|
32
|
-
break;
|
|
33
|
-
case 'text':
|
|
34
|
-
pass = actualText === (expectedText ?? '');
|
|
35
|
-
break;
|
|
36
|
-
}
|
|
37
|
-
const details =
|
|
38
|
-
predicate === 'text'
|
|
39
|
-
? `expected="${expectedText ?? ''}" actual="${actualText}"`
|
|
40
|
-
: `actual=${JSON.stringify({
|
|
41
|
-
visible: isNodeVisible(node),
|
|
42
|
-
editable: isNodeEditable(node, platform),
|
|
43
|
-
selected: node.selected === true,
|
|
44
|
-
})}`;
|
|
45
|
-
return { pass, actualText, details };
|
|
46
|
-
}
|