appium-session-recorder 0.0.2 → 0.0.4
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/index.js +33319 -0
- package/dist/ui/assets/index-CnJwu_Mc.js +8 -0
- package/dist/ui/assets/index-VIFL67d5.css +1 -0
- package/{src → dist}/ui/index.html +2 -1
- package/package.json +20 -13
- package/bun.lock +0 -731
- package/src/cli/arg-parser.ts +0 -311
- package/src/cli/commands/drive.ts +0 -147
- package/src/cli/commands/index.ts +0 -54
- package/src/cli/commands/proxy.ts +0 -41
- package/src/cli/commands/screen.ts +0 -73
- package/src/cli/commands/selectors.ts +0 -42
- package/src/cli/commands/session.ts +0 -64
- package/src/cli/commands/types.ts +0 -11
- package/src/cli/index.ts +0 -158
- package/src/cli/prompts.ts +0 -64
- package/src/cli/response.ts +0 -44
- package/src/core/appium/client.ts +0 -248
- package/src/core/index.ts +0 -5
- package/src/core/selectors/generate-candidates.ts +0 -155
- package/src/core/selectors/score-candidates.ts +0 -184
- package/src/core/types.ts +0 -79
- package/src/core/xml/parse-source.ts +0 -197
- package/src/index.ts +0 -7
- package/src/server/appium-client.ts +0 -24
- package/src/server/index.ts +0 -6
- package/src/server/interaction-recorder.ts +0 -74
- package/src/server/proxy-middleware.ts +0 -68
- package/src/server/routes.ts +0 -64
- package/src/server/server.ts +0 -43
- package/src/server/types.ts +0 -34
- package/src/ui/bun.lock +0 -311
- package/src/ui/package.json +0 -20
- package/src/ui/src/App.css +0 -12
- package/src/ui/src/App.tsx +0 -41
- package/src/ui/src/components/ActionCarousel.css +0 -128
- package/src/ui/src/components/ActionCarousel.tsx +0 -92
- package/src/ui/src/components/Inspector.css +0 -314
- package/src/ui/src/components/Inspector.tsx +0 -265
- package/src/ui/src/components/InteractionCard.css +0 -159
- package/src/ui/src/components/InteractionCard.tsx +0 -60
- package/src/ui/src/components/MainInspector.css +0 -304
- package/src/ui/src/components/MainInspector.tsx +0 -304
- package/src/ui/src/components/Stats.css +0 -27
- package/src/ui/src/components/Timeline.css +0 -31
- package/src/ui/src/components/Timeline.tsx +0 -37
- package/src/ui/src/hooks/useInteractions.ts +0 -73
- package/src/ui/src/index.tsx +0 -11
- package/src/ui/src/services/api.ts +0 -41
- package/src/ui/src/styles/tokens.css +0 -126
- package/src/ui/src/types.ts +0 -34
- package/src/ui/src/utils/__tests__/locators.test.ts +0 -304
- package/src/ui/src/utils/__tests__/xml-parser.test.ts +0 -326
- package/src/ui/src/utils/locators.ts +0 -14
- package/src/ui/src/utils/xml-parser.ts +0 -45
- package/src/ui/tsconfig.json +0 -34
- package/src/ui/tsconfig.node.json +0 -11
- package/src/ui/vite.config.ts +0 -22
- package/tests/cli/arg-parser.test.ts +0 -397
- package/tests/cli/drive-commands.test.ts +0 -151
- package/tests/cli/selectors-best.test.ts +0 -42
- package/tests/cli/session-commands.test.ts +0 -53
- package/tests/core/selector-candidates.test.ts +0 -83
- package/tests/core/selector-scoring.test.ts +0 -75
- package/tests/core/xml-parser.test.ts +0 -56
- package/tests/server/appium-client.test.ts +0 -229
- package/tests/server/interaction-recorder.test.ts +0 -377
- package/tests/server/proxy-middleware.test.ts +0 -343
- package/tests/server/routes.test.ts +0 -305
- package/tsconfig.json +0 -26
- package/vitest.config.ts +0 -16
- package/vitest.ui.config.ts +0 -15
- package/workflow.gif +0 -0
package/src/cli/arg-parser.ts
DELETED
|
@@ -1,311 +0,0 @@
|
|
|
1
|
-
import type { RecorderOptions } from '../server/types';
|
|
2
|
-
|
|
3
|
-
export type ParsedArgs = Partial<RecorderOptions> & {
|
|
4
|
-
help?: boolean;
|
|
5
|
-
version?: boolean;
|
|
6
|
-
};
|
|
7
|
-
|
|
8
|
-
export type ParseArgsResult =
|
|
9
|
-
| { success: true; args: ParsedArgs }
|
|
10
|
-
| { success: false; error: string };
|
|
11
|
-
|
|
12
|
-
export function parseArgs(argv: string[]): ParseArgsResult {
|
|
13
|
-
const args = argv.slice(2);
|
|
14
|
-
const parsed: ParsedArgs = {};
|
|
15
|
-
|
|
16
|
-
for (let i = 0; i < args.length; i++) {
|
|
17
|
-
const arg = args[i];
|
|
18
|
-
|
|
19
|
-
if (arg === '--help' || arg === '-h') {
|
|
20
|
-
parsed.help = true;
|
|
21
|
-
} else if (arg === '--version' || arg === '-v') {
|
|
22
|
-
parsed.version = true;
|
|
23
|
-
} else if (arg === '--port' || arg === '-p') {
|
|
24
|
-
if (i + 1 >= args.length) {
|
|
25
|
-
return { success: false, error: '--port requires a value' };
|
|
26
|
-
}
|
|
27
|
-
parsed.port = Number(args[++i]);
|
|
28
|
-
if (isNaN(parsed.port)) {
|
|
29
|
-
return { success: false, error: '--port must be a number' };
|
|
30
|
-
}
|
|
31
|
-
} else if (arg === '--appium-url' || arg === '-u') {
|
|
32
|
-
if (i + 1 >= args.length) {
|
|
33
|
-
return { success: false, error: '--appium-url requires a value' };
|
|
34
|
-
}
|
|
35
|
-
parsed.appiumUrl = args[++i];
|
|
36
|
-
} else if (arg === '--host') {
|
|
37
|
-
if (i + 1 >= args.length) {
|
|
38
|
-
return { success: false, error: '--host requires a value' };
|
|
39
|
-
}
|
|
40
|
-
parsed.host = args[++i];
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return { success: true, args: parsed };
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function validatePort(value: string): string | undefined {
|
|
48
|
-
if (value.trim().length === 0) return undefined;
|
|
49
|
-
const num = Number(value);
|
|
50
|
-
if (isNaN(num) || num < 1 || num > 65535) {
|
|
51
|
-
return 'Please enter a valid port number (1-65535)';
|
|
52
|
-
}
|
|
53
|
-
return undefined;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
export function validateUrl(value: string): string | undefined {
|
|
57
|
-
if (value.trim().length === 0) return undefined;
|
|
58
|
-
try {
|
|
59
|
-
new URL(value);
|
|
60
|
-
return undefined;
|
|
61
|
-
} catch {
|
|
62
|
-
return 'Please enter a valid URL';
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function validateHost(value: string): string | undefined {
|
|
67
|
-
if (value.trim().length === 0) return undefined;
|
|
68
|
-
if (!value || value.trim().length === 0) {
|
|
69
|
-
return 'Please enter a valid host';
|
|
70
|
-
}
|
|
71
|
-
return undefined;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
export type GlobalCliOptions = {
|
|
75
|
-
pretty: boolean;
|
|
76
|
-
output?: string;
|
|
77
|
-
};
|
|
78
|
-
|
|
79
|
-
export type CommandRoute = {
|
|
80
|
-
group: 'proxy' | 'session' | 'screen' | 'selectors' | 'drive';
|
|
81
|
-
command: string;
|
|
82
|
-
args: string[];
|
|
83
|
-
};
|
|
84
|
-
|
|
85
|
-
export type ParsedCliInput = {
|
|
86
|
-
mode: 'legacy' | 'command';
|
|
87
|
-
global: GlobalCliOptions;
|
|
88
|
-
legacyArgv?: string[];
|
|
89
|
-
route?: CommandRoute;
|
|
90
|
-
help?: boolean;
|
|
91
|
-
version?: boolean;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
export type ParseCliInputResult =
|
|
95
|
-
| { success: true; value: ParsedCliInput }
|
|
96
|
-
| { success: false; error: string };
|
|
97
|
-
|
|
98
|
-
const commandSubcommands: Record<string, string[]> = {
|
|
99
|
-
proxy: ['start'],
|
|
100
|
-
session: ['create', 'delete'],
|
|
101
|
-
screen: ['snapshot', 'elements'],
|
|
102
|
-
selectors: ['best'],
|
|
103
|
-
drive: ['tap', 'type', 'back', 'swipe', 'scroll'],
|
|
104
|
-
};
|
|
105
|
-
|
|
106
|
-
const LEGACY_GLOBAL_FLAGS_ERROR = '--pretty and --output are only supported with <group> <command> mode';
|
|
107
|
-
|
|
108
|
-
function hasLegacyOnlyGlobalFlags(global: GlobalCliOptions): boolean {
|
|
109
|
-
return global.pretty || global.output !== undefined;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export function parseCliInput(argv: string[]): ParseCliInputResult {
|
|
113
|
-
const input = argv.slice(2);
|
|
114
|
-
const global: GlobalCliOptions = { pretty: false };
|
|
115
|
-
const remaining: string[] = [];
|
|
116
|
-
let wantsHelp = false;
|
|
117
|
-
let wantsVersion = false;
|
|
118
|
-
|
|
119
|
-
for (let i = 0; i < input.length; i++) {
|
|
120
|
-
const token = input[i];
|
|
121
|
-
|
|
122
|
-
if (token === '--pretty') {
|
|
123
|
-
global.pretty = true;
|
|
124
|
-
continue;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
if (token === '--output') {
|
|
128
|
-
const value = input[i + 1];
|
|
129
|
-
if (!value || value.startsWith('-')) {
|
|
130
|
-
return { success: false, error: '--output requires a file path value' };
|
|
131
|
-
}
|
|
132
|
-
global.output = value;
|
|
133
|
-
i++;
|
|
134
|
-
continue;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
if (token === '--help' || token === '-h') {
|
|
138
|
-
wantsHelp = true;
|
|
139
|
-
continue;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
if (token === '--version' || token === '-v') {
|
|
143
|
-
wantsVersion = true;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
remaining.push(token);
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
if (wantsHelp) {
|
|
151
|
-
if (hasLegacyOnlyGlobalFlags(global)) {
|
|
152
|
-
return { success: false, error: LEGACY_GLOBAL_FLAGS_ERROR };
|
|
153
|
-
}
|
|
154
|
-
return {
|
|
155
|
-
success: true,
|
|
156
|
-
value: {
|
|
157
|
-
mode: 'legacy',
|
|
158
|
-
help: true,
|
|
159
|
-
global,
|
|
160
|
-
legacyArgv: ['node', 'script', '--help'],
|
|
161
|
-
},
|
|
162
|
-
};
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
if (wantsVersion) {
|
|
166
|
-
if (hasLegacyOnlyGlobalFlags(global)) {
|
|
167
|
-
return { success: false, error: LEGACY_GLOBAL_FLAGS_ERROR };
|
|
168
|
-
}
|
|
169
|
-
return {
|
|
170
|
-
success: true,
|
|
171
|
-
value: {
|
|
172
|
-
mode: 'legacy',
|
|
173
|
-
version: true,
|
|
174
|
-
global,
|
|
175
|
-
legacyArgv: ['node', 'script', '--version'],
|
|
176
|
-
},
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
if (remaining.length === 0) {
|
|
181
|
-
if (hasLegacyOnlyGlobalFlags(global)) {
|
|
182
|
-
return { success: false, error: LEGACY_GLOBAL_FLAGS_ERROR };
|
|
183
|
-
}
|
|
184
|
-
return {
|
|
185
|
-
success: true,
|
|
186
|
-
value: {
|
|
187
|
-
mode: 'legacy',
|
|
188
|
-
global,
|
|
189
|
-
legacyArgv: ['node', 'script'],
|
|
190
|
-
},
|
|
191
|
-
};
|
|
192
|
-
}
|
|
193
|
-
|
|
194
|
-
const maybeGroup = remaining[0];
|
|
195
|
-
if (!(maybeGroup in commandSubcommands)) {
|
|
196
|
-
if (hasLegacyOnlyGlobalFlags(global)) {
|
|
197
|
-
return { success: false, error: LEGACY_GLOBAL_FLAGS_ERROR };
|
|
198
|
-
}
|
|
199
|
-
return {
|
|
200
|
-
success: true,
|
|
201
|
-
value: {
|
|
202
|
-
mode: 'legacy',
|
|
203
|
-
global,
|
|
204
|
-
legacyArgv: ['node', 'script', ...remaining],
|
|
205
|
-
},
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
const subcommand = remaining[1];
|
|
210
|
-
if (!subcommand) {
|
|
211
|
-
return { success: false, error: `Missing subcommand for '${maybeGroup}'` };
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
const allowed = commandSubcommands[maybeGroup];
|
|
215
|
-
if (!allowed.includes(subcommand)) {
|
|
216
|
-
return {
|
|
217
|
-
success: false,
|
|
218
|
-
error: `Unknown subcommand '${subcommand}' for '${maybeGroup}'. Allowed: ${allowed.join(', ')}`,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return {
|
|
223
|
-
success: true,
|
|
224
|
-
value: {
|
|
225
|
-
mode: 'command',
|
|
226
|
-
global,
|
|
227
|
-
route: {
|
|
228
|
-
group: maybeGroup as CommandRoute['group'],
|
|
229
|
-
command: subcommand,
|
|
230
|
-
args: remaining.slice(2),
|
|
231
|
-
},
|
|
232
|
-
},
|
|
233
|
-
};
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
export type ParseFlagsResult =
|
|
237
|
-
| { success: true; flags: Record<string, string | boolean>; positionals: string[] }
|
|
238
|
-
| { success: false; error: string };
|
|
239
|
-
|
|
240
|
-
export function parseFlags(args: string[]): ParseFlagsResult {
|
|
241
|
-
const flags: Record<string, string | boolean> = {};
|
|
242
|
-
const positionals: string[] = [];
|
|
243
|
-
|
|
244
|
-
for (let i = 0; i < args.length; i++) {
|
|
245
|
-
const token = args[i];
|
|
246
|
-
|
|
247
|
-
if (!token.startsWith('-')) {
|
|
248
|
-
positionals.push(token);
|
|
249
|
-
continue;
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
if (!token.startsWith('--')) {
|
|
253
|
-
return { success: false, error: `Short options are not supported in command mode: '${token}'` };
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
const equalsIndex = token.indexOf('=');
|
|
257
|
-
if (equalsIndex > -1) {
|
|
258
|
-
const key = token.slice(2, equalsIndex);
|
|
259
|
-
const value = token.slice(equalsIndex + 1);
|
|
260
|
-
if (!key) return { success: false, error: `Invalid flag '${token}'` };
|
|
261
|
-
flags[key] = value;
|
|
262
|
-
continue;
|
|
263
|
-
}
|
|
264
|
-
|
|
265
|
-
const key = token.slice(2);
|
|
266
|
-
if (!key) return { success: false, error: `Invalid flag '${token}'` };
|
|
267
|
-
|
|
268
|
-
const next = args[i + 1];
|
|
269
|
-
if (!next || next.startsWith('--')) {
|
|
270
|
-
flags[key] = true;
|
|
271
|
-
continue;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
flags[key] = next;
|
|
275
|
-
i++;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
return { success: true, flags, positionals };
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export function parseNumberFlag(name: string, value: string | boolean | undefined): number | undefined {
|
|
282
|
-
if (typeof value !== 'string') return undefined;
|
|
283
|
-
const parsed = Number(value);
|
|
284
|
-
if (!Number.isFinite(parsed)) {
|
|
285
|
-
throw new Error(`--${name} must be a number`);
|
|
286
|
-
}
|
|
287
|
-
return parsed;
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
export function expectStringFlag(name: string, value: string | boolean | undefined): string {
|
|
291
|
-
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
292
|
-
throw new Error(`--${name} is required`);
|
|
293
|
-
}
|
|
294
|
-
return value;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
export function expectOptionalString(value: string | boolean | undefined): string | undefined {
|
|
298
|
-
if (typeof value !== 'string') return undefined;
|
|
299
|
-
const trimmed = value.trim();
|
|
300
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
export function ensureNoUnexpectedFlags(
|
|
304
|
-
flags: Record<string, string | boolean>,
|
|
305
|
-
allowed: string[],
|
|
306
|
-
): void {
|
|
307
|
-
const unknown = Object.keys(flags).filter(key => !allowed.includes(key));
|
|
308
|
-
if (unknown.length > 0) {
|
|
309
|
-
throw new Error(`Unknown flags: ${unknown.map(flag => `--${flag}`).join(', ')}`);
|
|
310
|
-
}
|
|
311
|
-
}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
import { AppiumCommandClient } from '../../core/appium/client';
|
|
2
|
-
import { ensureNoUnexpectedFlags, expectStringFlag, parseFlags, parseNumberFlag } from '../arg-parser';
|
|
3
|
-
|
|
4
|
-
const SCROLL_DIRECTIONS = ['up', 'down', 'left', 'right'] as const;
|
|
5
|
-
type ScrollDirection = (typeof SCROLL_DIRECTIONS)[number];
|
|
6
|
-
import type { CommandExecutionResult } from './types';
|
|
7
|
-
|
|
8
|
-
function parsePoint(name: string, value: string): { x: number; y: number } {
|
|
9
|
-
const [xRaw, yRaw] = value.split(',');
|
|
10
|
-
const x = Number(xRaw);
|
|
11
|
-
const y = Number(yRaw);
|
|
12
|
-
|
|
13
|
-
if (!Number.isFinite(x) || !Number.isFinite(y)) {
|
|
14
|
-
throw new Error(`--${name} must be in format x,y`);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
return { x, y };
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export async function runDriveTap(args: string[]): Promise<CommandExecutionResult> {
|
|
21
|
-
const parsed = parseFlags(args);
|
|
22
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
23
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
24
|
-
|
|
25
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id', 'using', 'value']);
|
|
26
|
-
|
|
27
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
28
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
29
|
-
const using = expectStringFlag('using', parsed.flags.using);
|
|
30
|
-
const value = expectStringFlag('value', parsed.flags.value);
|
|
31
|
-
|
|
32
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
33
|
-
await client.tap(sessionId, using, value);
|
|
34
|
-
|
|
35
|
-
return {
|
|
36
|
-
command: 'drive.tap',
|
|
37
|
-
result: { sessionId, using, value, performed: true },
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export async function runDriveType(args: string[]): Promise<CommandExecutionResult> {
|
|
42
|
-
const parsed = parseFlags(args);
|
|
43
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
44
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
45
|
-
|
|
46
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id', 'using', 'value', 'text', 'clear-first']);
|
|
47
|
-
|
|
48
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
49
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
50
|
-
const using = expectStringFlag('using', parsed.flags.using);
|
|
51
|
-
const value = expectStringFlag('value', parsed.flags.value);
|
|
52
|
-
const text = expectStringFlag('text', parsed.flags.text);
|
|
53
|
-
const clearFirst = parsed.flags['clear-first'] === true;
|
|
54
|
-
|
|
55
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
56
|
-
await client.type(sessionId, using, value, text, clearFirst);
|
|
57
|
-
|
|
58
|
-
return {
|
|
59
|
-
command: 'drive.type',
|
|
60
|
-
result: { sessionId, using, value, textLength: text.length, clearFirst, performed: true },
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
export async function runDriveBack(args: string[]): Promise<CommandExecutionResult> {
|
|
65
|
-
const parsed = parseFlags(args);
|
|
66
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
67
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
68
|
-
|
|
69
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id']);
|
|
70
|
-
|
|
71
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
72
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
73
|
-
|
|
74
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
75
|
-
await client.back(sessionId);
|
|
76
|
-
|
|
77
|
-
return {
|
|
78
|
-
command: 'drive.back',
|
|
79
|
-
result: { sessionId, performed: true },
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
export async function runDriveSwipe(args: string[]): Promise<CommandExecutionResult> {
|
|
84
|
-
const parsed = parseFlags(args);
|
|
85
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
86
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
87
|
-
|
|
88
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id', 'from', 'to', 'duration-ms']);
|
|
89
|
-
|
|
90
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
91
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
92
|
-
const from = parsePoint('from', expectStringFlag('from', parsed.flags.from));
|
|
93
|
-
const to = parsePoint('to', expectStringFlag('to', parsed.flags.to));
|
|
94
|
-
const durationMs = parseNumberFlag('duration-ms', parsed.flags['duration-ms']) ?? 300;
|
|
95
|
-
|
|
96
|
-
if (!Number.isFinite(durationMs) || durationMs < 1) {
|
|
97
|
-
throw new Error('--duration-ms must be a positive number');
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
101
|
-
await client.swipe(sessionId, from, to, durationMs);
|
|
102
|
-
|
|
103
|
-
return {
|
|
104
|
-
command: 'drive.swipe',
|
|
105
|
-
result: {
|
|
106
|
-
sessionId,
|
|
107
|
-
from,
|
|
108
|
-
to,
|
|
109
|
-
durationMs,
|
|
110
|
-
performed: true,
|
|
111
|
-
},
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
export async function runDriveScroll(args: string[]): Promise<CommandExecutionResult> {
|
|
116
|
-
const parsed = parseFlags(args);
|
|
117
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
118
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
119
|
-
|
|
120
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id', 'direction', 'duration-ms']);
|
|
121
|
-
|
|
122
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
123
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
124
|
-
const direction = expectStringFlag('direction', parsed.flags.direction);
|
|
125
|
-
const durationMs = parseNumberFlag('duration-ms', parsed.flags['duration-ms']) ?? 300;
|
|
126
|
-
|
|
127
|
-
if (!SCROLL_DIRECTIONS.includes(direction as ScrollDirection)) {
|
|
128
|
-
throw new Error(`--direction must be one of: ${SCROLL_DIRECTIONS.join(', ')}`);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
if (!Number.isFinite(durationMs) || durationMs < 1) {
|
|
132
|
-
throw new Error('--duration-ms must be a positive number');
|
|
133
|
-
}
|
|
134
|
-
|
|
135
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
136
|
-
await client.scroll(sessionId, direction as ScrollDirection, durationMs);
|
|
137
|
-
|
|
138
|
-
return {
|
|
139
|
-
command: 'drive.scroll',
|
|
140
|
-
result: {
|
|
141
|
-
sessionId,
|
|
142
|
-
direction,
|
|
143
|
-
durationMs,
|
|
144
|
-
performed: true,
|
|
145
|
-
},
|
|
146
|
-
};
|
|
147
|
-
}
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { runDriveBack, runDriveScroll, runDriveSwipe, runDriveTap, runDriveType } from './drive';
|
|
2
|
-
import { runProxyStart } from './proxy';
|
|
3
|
-
import { runScreenElements, runScreenSnapshot } from './screen';
|
|
4
|
-
import { runSelectorsBest } from './selectors';
|
|
5
|
-
import { runSessionCreate, runSessionDelete } from './session';
|
|
6
|
-
import type { CommandExecutionResult } from './types';
|
|
7
|
-
|
|
8
|
-
export async function dispatchCommand(group: string, command: string, args: string[]): Promise<CommandExecutionResult> {
|
|
9
|
-
if (group === 'proxy' && command === 'start') {
|
|
10
|
-
return await runProxyStart(args);
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
if (group === 'session' && command === 'create') {
|
|
14
|
-
return await runSessionCreate(args);
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
if (group === 'session' && command === 'delete') {
|
|
18
|
-
return await runSessionDelete(args);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
if (group === 'screen' && command === 'snapshot') {
|
|
22
|
-
return await runScreenSnapshot(args);
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
if (group === 'screen' && command === 'elements') {
|
|
26
|
-
return await runScreenElements(args);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
if (group === 'selectors' && command === 'best') {
|
|
30
|
-
return await runSelectorsBest(args);
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (group === 'drive' && command === 'tap') {
|
|
34
|
-
return await runDriveTap(args);
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
if (group === 'drive' && command === 'type') {
|
|
38
|
-
return await runDriveType(args);
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
if (group === 'drive' && command === 'back') {
|
|
42
|
-
return await runDriveBack(args);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
if (group === 'drive' && command === 'swipe') {
|
|
46
|
-
return await runDriveSwipe(args);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (group === 'drive' && command === 'scroll') {
|
|
50
|
-
return await runDriveScroll(args);
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
throw new Error(`Unsupported command: ${group} ${command}`);
|
|
54
|
-
}
|
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { createServer } from '../../server/server';
|
|
2
|
-
import { ensureNoUnexpectedFlags, expectOptionalString, parseFlags, parseNumberFlag } from '../arg-parser';
|
|
3
|
-
import type { CommandExecutionResult } from './types';
|
|
4
|
-
|
|
5
|
-
export async function runProxyStart(args: string[]): Promise<CommandExecutionResult> {
|
|
6
|
-
const parsed = parseFlags(args);
|
|
7
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
8
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
9
|
-
|
|
10
|
-
ensureNoUnexpectedFlags(parsed.flags, ['host', 'port', 'appium-url']);
|
|
11
|
-
|
|
12
|
-
const host = expectOptionalString(parsed.flags.host) ?? '127.0.0.1';
|
|
13
|
-
const port = parseNumberFlag('port', parsed.flags.port) ?? 4724;
|
|
14
|
-
const appiumUrl = expectOptionalString(parsed.flags['appium-url']) ?? 'http://127.0.0.1:4723';
|
|
15
|
-
|
|
16
|
-
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
17
|
-
throw new Error('--port must be an integer between 1 and 65535');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
const { app } = createServer({ host, port, appiumUrl });
|
|
21
|
-
const server = app.listen(port, host);
|
|
22
|
-
|
|
23
|
-
process.on('SIGINT', () => {
|
|
24
|
-
server.close(() => process.exit(0));
|
|
25
|
-
});
|
|
26
|
-
process.on('SIGTERM', () => {
|
|
27
|
-
server.close(() => process.exit(0));
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
return {
|
|
31
|
-
command: 'proxy.start',
|
|
32
|
-
result: {
|
|
33
|
-
host,
|
|
34
|
-
port,
|
|
35
|
-
appiumUrl,
|
|
36
|
-
proxyUrl: `http://${host}:${port}`,
|
|
37
|
-
viewerUrl: `http://${host}:${port}/_recorder`,
|
|
38
|
-
running: true,
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { AppiumCommandClient } from '../../core/appium/client';
|
|
2
|
-
import { parseSource } from '../../core/xml/parse-source';
|
|
3
|
-
import { ensureNoUnexpectedFlags, expectStringFlag, parseFlags, parseNumberFlag } from '../arg-parser';
|
|
4
|
-
import type { CommandExecutionResult } from './types';
|
|
5
|
-
|
|
6
|
-
export async function runScreenSnapshot(args: string[]): Promise<CommandExecutionResult> {
|
|
7
|
-
const parsed = parseFlags(args);
|
|
8
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
9
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
10
|
-
|
|
11
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id']);
|
|
12
|
-
|
|
13
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
14
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
15
|
-
|
|
16
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
17
|
-
const { source, screenshot } = await client.captureState(sessionId);
|
|
18
|
-
const parsedSource = parseSource(source);
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
command: 'screen.snapshot',
|
|
22
|
-
result: {
|
|
23
|
-
sessionId,
|
|
24
|
-
platform: parsedSource.platform,
|
|
25
|
-
screenshot,
|
|
26
|
-
source,
|
|
27
|
-
metadata: {
|
|
28
|
-
elementCount: parsedSource.elements.length,
|
|
29
|
-
},
|
|
30
|
-
},
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export async function runScreenElements(args: string[]): Promise<CommandExecutionResult> {
|
|
35
|
-
const parsed = parseFlags(args);
|
|
36
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
37
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
38
|
-
|
|
39
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id', 'limit', 'only-actionable']);
|
|
40
|
-
|
|
41
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
42
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
43
|
-
const limit = parseNumberFlag('limit', parsed.flags.limit);
|
|
44
|
-
const onlyActionable = parsed.flags['only-actionable'] === true;
|
|
45
|
-
|
|
46
|
-
if (limit !== undefined && (!Number.isInteger(limit) || limit < 1)) {
|
|
47
|
-
throw new Error('--limit must be a positive integer');
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
51
|
-
const source = await client.getSource(sessionId);
|
|
52
|
-
const parsedSource = parseSource(source);
|
|
53
|
-
|
|
54
|
-
let elements = parsedSource.elements;
|
|
55
|
-
if (onlyActionable) {
|
|
56
|
-
elements = elements.filter(element => element.enabled && element.visible && (element.clickable || element.accessible));
|
|
57
|
-
}
|
|
58
|
-
if (limit !== undefined) {
|
|
59
|
-
elements = elements.slice(0, limit);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return {
|
|
63
|
-
command: 'screen.elements',
|
|
64
|
-
result: {
|
|
65
|
-
sessionId,
|
|
66
|
-
platform: parsedSource.platform,
|
|
67
|
-
total: parsedSource.elements.length,
|
|
68
|
-
returned: elements.length,
|
|
69
|
-
onlyActionable,
|
|
70
|
-
elements,
|
|
71
|
-
},
|
|
72
|
-
};
|
|
73
|
-
}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { AppiumCommandClient } from '../../core/appium/client';
|
|
2
|
-
import { generateSelectorCandidates } from '../../core/selectors/generate-candidates';
|
|
3
|
-
import { rankSelectorCandidates } from '../../core/selectors/score-candidates';
|
|
4
|
-
import { parseSource } from '../../core/xml/parse-source';
|
|
5
|
-
import { ensureNoUnexpectedFlags, expectStringFlag, parseFlags } from '../arg-parser';
|
|
6
|
-
import type { CommandExecutionResult } from './types';
|
|
7
|
-
|
|
8
|
-
export async function runSelectorsBest(args: string[]): Promise<CommandExecutionResult> {
|
|
9
|
-
const parsed = parseFlags(args);
|
|
10
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
11
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
12
|
-
|
|
13
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id', 'element-ref']);
|
|
14
|
-
|
|
15
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
16
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
17
|
-
const elementRef = expectStringFlag('element-ref', parsed.flags['element-ref']);
|
|
18
|
-
|
|
19
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
20
|
-
const source = await client.getSource(sessionId);
|
|
21
|
-
const parsedSource = parseSource(source);
|
|
22
|
-
|
|
23
|
-
const target = parsedSource.elements.find(element => element.elementRef === elementRef);
|
|
24
|
-
if (!target) {
|
|
25
|
-
throw new Error(`Element not found for --element-ref '${elementRef}'`);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
const candidates = generateSelectorCandidates(target);
|
|
29
|
-
const ranked = rankSelectorCandidates(target, parsedSource.elements, candidates);
|
|
30
|
-
|
|
31
|
-
return {
|
|
32
|
-
command: 'selectors.best',
|
|
33
|
-
result: {
|
|
34
|
-
sessionId,
|
|
35
|
-
platform: parsedSource.platform,
|
|
36
|
-
elementRef,
|
|
37
|
-
target,
|
|
38
|
-
topSelectors: ranked.slice(0, 5),
|
|
39
|
-
allSelectors: ranked,
|
|
40
|
-
},
|
|
41
|
-
};
|
|
42
|
-
}
|