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
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import { readFile } from 'node:fs/promises';
|
|
2
|
-
import { AppiumCommandClient } from '../../core/appium/client';
|
|
3
|
-
import { ensureNoUnexpectedFlags, expectOptionalString, expectStringFlag, parseFlags } from '../arg-parser';
|
|
4
|
-
import type { CommandExecutionResult } from './types';
|
|
5
|
-
|
|
6
|
-
export async function runSessionCreate(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', 'caps-file', 'caps-json']);
|
|
12
|
-
|
|
13
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
14
|
-
const capsFile = expectOptionalString(parsed.flags['caps-file']);
|
|
15
|
-
const capsJson = expectOptionalString(parsed.flags['caps-json']);
|
|
16
|
-
|
|
17
|
-
if ((capsFile && capsJson) || (!capsFile && !capsJson)) {
|
|
18
|
-
throw new Error('Provide exactly one of --caps-file or --caps-json');
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
let capabilities: Record<string, unknown>;
|
|
22
|
-
|
|
23
|
-
if (capsFile) {
|
|
24
|
-
const raw = await readFile(capsFile, 'utf8');
|
|
25
|
-
capabilities = JSON.parse(raw);
|
|
26
|
-
} else {
|
|
27
|
-
capabilities = JSON.parse(capsJson!);
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
31
|
-
const session = await client.createSession(capabilities);
|
|
32
|
-
|
|
33
|
-
return {
|
|
34
|
-
command: 'session.create',
|
|
35
|
-
result: {
|
|
36
|
-
appiumUrl,
|
|
37
|
-
sessionId: session.sessionId,
|
|
38
|
-
value: session.value,
|
|
39
|
-
},
|
|
40
|
-
};
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export async function runSessionDelete(args: string[]): Promise<CommandExecutionResult> {
|
|
44
|
-
const parsed = parseFlags(args);
|
|
45
|
-
if (!parsed.success) throw new Error(parsed.error);
|
|
46
|
-
if (parsed.positionals.length > 0) throw new Error(`Unexpected arguments: ${parsed.positionals.join(', ')}`);
|
|
47
|
-
|
|
48
|
-
ensureNoUnexpectedFlags(parsed.flags, ['appium-url', 'session-id']);
|
|
49
|
-
|
|
50
|
-
const appiumUrl = expectStringFlag('appium-url', parsed.flags['appium-url']);
|
|
51
|
-
const sessionId = expectStringFlag('session-id', parsed.flags['session-id']);
|
|
52
|
-
|
|
53
|
-
const client = new AppiumCommandClient(appiumUrl);
|
|
54
|
-
await client.deleteSession(sessionId);
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
command: 'session.delete',
|
|
58
|
-
result: {
|
|
59
|
-
appiumUrl,
|
|
60
|
-
sessionId,
|
|
61
|
-
deleted: true,
|
|
62
|
-
},
|
|
63
|
-
};
|
|
64
|
-
}
|
package/src/cli/index.ts
DELETED
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
import * as p from '@clack/prompts';
|
|
2
|
-
import { runPrompts } from './prompts';
|
|
3
|
-
import { startServer } from '../server';
|
|
4
|
-
import { parseArgs, parseCliInput } from './arg-parser';
|
|
5
|
-
import { dispatchCommand } from './commands';
|
|
6
|
-
import { emitResponse, errorResponse, successResponse } from './response';
|
|
7
|
-
import { AppiumCommandError } from '../core/appium/client';
|
|
8
|
-
import type { RecorderOptions } from '../server';
|
|
9
|
-
|
|
10
|
-
function showHelp() {
|
|
11
|
-
console.log(`
|
|
12
|
-
🎬 Appium Session Recorder
|
|
13
|
-
|
|
14
|
-
USAGE:
|
|
15
|
-
bun run cli [legacy-options]
|
|
16
|
-
bun run cli <group> <command> [flags]
|
|
17
|
-
|
|
18
|
-
LEGACY OPTIONS:
|
|
19
|
-
-p, --port <number> Proxy server port (default: 4724)
|
|
20
|
-
-u, --appium-url <url> Appium server URL (default: http://127.0.0.1:4723)
|
|
21
|
-
--host <host> Proxy server host (default: 127.0.0.1)
|
|
22
|
-
-h, --help Show this help message
|
|
23
|
-
-v, --version Show version
|
|
24
|
-
|
|
25
|
-
GLOBAL COMMAND FLAGS:
|
|
26
|
-
--pretty Pretty-print JSON output
|
|
27
|
-
--output <path> Write command JSON output to file
|
|
28
|
-
(supported only with <group> <command> mode)
|
|
29
|
-
|
|
30
|
-
COMMAND GROUPS:
|
|
31
|
-
proxy start Start proxy server (JSON-first output)
|
|
32
|
-
session create Create Appium session
|
|
33
|
-
session delete Delete Appium session
|
|
34
|
-
screen snapshot Capture screenshot/source and parsed metadata
|
|
35
|
-
screen elements List parsed elements
|
|
36
|
-
selectors best Return top ranked selectors for an element
|
|
37
|
-
drive tap Tap element by selector
|
|
38
|
-
drive type Type text into element by selector
|
|
39
|
-
drive back Navigate back
|
|
40
|
-
drive swipe Perform swipe gesture
|
|
41
|
-
drive scroll Scroll in a direction (up/down/left/right)
|
|
42
|
-
`);
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
function showVersion() {
|
|
46
|
-
console.log('Appium Session Recorder v3.0.0');
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function runLegacyCLI(argv: string[]): Promise<void> {
|
|
50
|
-
const result = parseArgs(argv);
|
|
51
|
-
|
|
52
|
-
if (!result.success) {
|
|
53
|
-
console.error(`Error: ${result.error}`);
|
|
54
|
-
process.exit(1);
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
const args = result.args;
|
|
58
|
-
|
|
59
|
-
if (args.help) {
|
|
60
|
-
showHelp();
|
|
61
|
-
process.exit(0);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
if (args.version) {
|
|
65
|
-
showVersion();
|
|
66
|
-
process.exit(0);
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
// Determine if we need to run interactive prompts
|
|
70
|
-
const hasRequiredArgs = args.port !== undefined || args.appiumUrl !== undefined;
|
|
71
|
-
|
|
72
|
-
let promptConfig: Partial<RecorderOptions> = {
|
|
73
|
-
port: args.port,
|
|
74
|
-
appiumUrl: args.appiumUrl,
|
|
75
|
-
host: args.host,
|
|
76
|
-
};
|
|
77
|
-
|
|
78
|
-
if (!hasRequiredArgs) {
|
|
79
|
-
// Run interactive prompts
|
|
80
|
-
promptConfig = await runPrompts();
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
// Display startup banner
|
|
84
|
-
console.log('');
|
|
85
|
-
p.intro('🎬 Starting Appium Session Recorder');
|
|
86
|
-
|
|
87
|
-
const s = p.spinner();
|
|
88
|
-
s.start('Initializing server...');
|
|
89
|
-
|
|
90
|
-
try {
|
|
91
|
-
// Start server
|
|
92
|
-
startServer(promptConfig);
|
|
93
|
-
s.stop('✅ Server initialized');
|
|
94
|
-
|
|
95
|
-
console.log('');
|
|
96
|
-
console.log(`📊 Configuration:`);
|
|
97
|
-
console.log(` Port: ${promptConfig.port}`);
|
|
98
|
-
console.log(` Appium URL: ${promptConfig.appiumUrl}`);
|
|
99
|
-
console.log(` Host: ${promptConfig.host}`);
|
|
100
|
-
|
|
101
|
-
p.outro('🚀 Server is running! Press Ctrl+C to stop.');
|
|
102
|
-
} catch (error) {
|
|
103
|
-
s.stop('❌ Failed to start server');
|
|
104
|
-
console.error(error);
|
|
105
|
-
process.exit(1);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
// Handle graceful shutdown
|
|
109
|
-
process.on('SIGINT', () => {
|
|
110
|
-
console.log('\n');
|
|
111
|
-
p.outro('👋 Shutting down...');
|
|
112
|
-
process.exit(0);
|
|
113
|
-
});
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
export async function runCLI(): Promise<void> {
|
|
117
|
-
const parsedInput = parseCliInput(process.argv);
|
|
118
|
-
|
|
119
|
-
if (!parsedInput.success) {
|
|
120
|
-
const response = errorResponse('cli.parse', 'CLI_PARSE_ERROR', parsedInput.error);
|
|
121
|
-
await emitResponse(response, { pretty: true });
|
|
122
|
-
process.exit(1);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const cliInput = parsedInput.value;
|
|
126
|
-
|
|
127
|
-
if (cliInput.mode === 'legacy') {
|
|
128
|
-
await runLegacyCLI(cliInput.legacyArgv ?? process.argv);
|
|
129
|
-
return;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const route = cliInput.route!;
|
|
133
|
-
const commandName = `${route.group}.${route.command}`;
|
|
134
|
-
|
|
135
|
-
try {
|
|
136
|
-
const execution = await dispatchCommand(route.group, route.command, route.args);
|
|
137
|
-
const response = successResponse(execution.command, execution.result);
|
|
138
|
-
await emitResponse(response, cliInput.global);
|
|
139
|
-
} catch (error) {
|
|
140
|
-
if (error instanceof AppiumCommandError) {
|
|
141
|
-
const response = errorResponse(commandName, error.code, error.message, {
|
|
142
|
-
status: error.status,
|
|
143
|
-
details: error.details,
|
|
144
|
-
});
|
|
145
|
-
await emitResponse(response, cliInput.global);
|
|
146
|
-
process.exit(1);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
const response = errorResponse(
|
|
151
|
-
commandName,
|
|
152
|
-
'COMMAND_EXECUTION_ERROR',
|
|
153
|
-
error instanceof Error ? error.message : 'Unknown error',
|
|
154
|
-
);
|
|
155
|
-
await emitResponse(response, cliInput.global);
|
|
156
|
-
process.exit(1);
|
|
157
|
-
}
|
|
158
|
-
}
|
package/src/cli/prompts.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import * as p from '@clack/prompts';
|
|
2
|
-
import type {RecorderOptions} from '../server/types';
|
|
3
|
-
|
|
4
|
-
export async function runPrompts(): Promise<Partial<RecorderOptions>> {
|
|
5
|
-
console.clear();
|
|
6
|
-
|
|
7
|
-
p.intro('🎬 Appium Session Recorder');
|
|
8
|
-
|
|
9
|
-
const defaults = {
|
|
10
|
-
port: '4724',
|
|
11
|
-
host: '127.0.0.1',
|
|
12
|
-
appiumUrl: 'http://127.0.0.1:4723',
|
|
13
|
-
} as const;
|
|
14
|
-
|
|
15
|
-
const answers = await p.group(
|
|
16
|
-
{
|
|
17
|
-
port: () => p.text({
|
|
18
|
-
message: `Proxy port (default: ${defaults.port}):`,
|
|
19
|
-
placeholder: defaults.port,
|
|
20
|
-
defaultValue: defaults.port,
|
|
21
|
-
validate: (value) => {
|
|
22
|
-
if (!value || value.trim().length === 0) return;
|
|
23
|
-
const num = Number(value);
|
|
24
|
-
if (isNaN(num) || num < 1 || num > 65535) {
|
|
25
|
-
return 'Please enter a valid port number (1-65535)';
|
|
26
|
-
}
|
|
27
|
-
},
|
|
28
|
-
}),
|
|
29
|
-
host: () => p.text({
|
|
30
|
-
message: `Proxy host (default: ${defaults.host}):`,
|
|
31
|
-
placeholder: defaults.host,
|
|
32
|
-
defaultValue: defaults.host,
|
|
33
|
-
validate: (value) => {
|
|
34
|
-
if (!value || value.trim().length === 0) return undefined;
|
|
35
|
-
},
|
|
36
|
-
}),
|
|
37
|
-
appiumUrl: () => p.text({
|
|
38
|
-
message: `Appium server URL (default: ${defaults.appiumUrl}):`,
|
|
39
|
-
placeholder: defaults.appiumUrl,
|
|
40
|
-
defaultValue: defaults.appiumUrl,
|
|
41
|
-
validate: (value) => {
|
|
42
|
-
if (!value || value.trim().length === 0) return;
|
|
43
|
-
try {
|
|
44
|
-
new URL(value);
|
|
45
|
-
} catch {
|
|
46
|
-
return 'Please enter a valid URL';
|
|
47
|
-
}
|
|
48
|
-
},
|
|
49
|
-
}),
|
|
50
|
-
},
|
|
51
|
-
{
|
|
52
|
-
onCancel: () => {
|
|
53
|
-
p.cancel('Operation cancelled.');
|
|
54
|
-
process.exit(0);
|
|
55
|
-
},
|
|
56
|
-
}
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
return {
|
|
60
|
-
port: Number((answers.port as string) || defaults.port),
|
|
61
|
-
host: ((answers.host as string) || defaults.host).trim(),
|
|
62
|
-
appiumUrl: ((answers.appiumUrl as string) || defaults.appiumUrl).trim(),
|
|
63
|
-
};
|
|
64
|
-
}
|
package/src/cli/response.ts
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import type { CommandResponse } from '../core/types';
|
|
4
|
-
|
|
5
|
-
export type CliOutputOptions = {
|
|
6
|
-
pretty: boolean;
|
|
7
|
-
output?: string;
|
|
8
|
-
};
|
|
9
|
-
|
|
10
|
-
export function successResponse<T>(command: string, result: T): CommandResponse<T> {
|
|
11
|
-
return {
|
|
12
|
-
ok: true,
|
|
13
|
-
command,
|
|
14
|
-
timestamp: new Date().toISOString(),
|
|
15
|
-
result,
|
|
16
|
-
};
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function errorResponse(command: string, code: string, message: string, details?: unknown): CommandResponse {
|
|
20
|
-
return {
|
|
21
|
-
ok: false,
|
|
22
|
-
command,
|
|
23
|
-
timestamp: new Date().toISOString(),
|
|
24
|
-
error: {
|
|
25
|
-
code,
|
|
26
|
-
message,
|
|
27
|
-
details,
|
|
28
|
-
},
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function emitResponse(response: CommandResponse, options: CliOutputOptions): Promise<void> {
|
|
33
|
-
const output = options.pretty
|
|
34
|
-
? `${JSON.stringify(response, null, 2)}\n`
|
|
35
|
-
: `${JSON.stringify(response)}\n`;
|
|
36
|
-
|
|
37
|
-
process.stdout.write(output);
|
|
38
|
-
|
|
39
|
-
if (options.output) {
|
|
40
|
-
const outPath = path.resolve(options.output);
|
|
41
|
-
await mkdir(path.dirname(outPath), { recursive: true });
|
|
42
|
-
await writeFile(outPath, output, 'utf8');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
import type { Point } from '../types';
|
|
2
|
-
|
|
3
|
-
const W3C_ELEMENT_KEY = 'element-6066-11e4-a52e-4f735466cecf';
|
|
4
|
-
|
|
5
|
-
export class AppiumCommandError extends Error {
|
|
6
|
-
constructor(
|
|
7
|
-
message: string,
|
|
8
|
-
public readonly code: string,
|
|
9
|
-
public readonly status: number,
|
|
10
|
-
public readonly details?: unknown,
|
|
11
|
-
) {
|
|
12
|
-
super(message);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
export class AppiumCommandClient {
|
|
17
|
-
constructor(private readonly appiumUrl: string) {}
|
|
18
|
-
|
|
19
|
-
private parseWindowSize(value: unknown): { width: number; height: number } | null {
|
|
20
|
-
if (!value || typeof value !== 'object') {
|
|
21
|
-
return null;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
const candidate = value as Record<string, unknown>;
|
|
25
|
-
const width = Number(candidate.width);
|
|
26
|
-
const height = Number(candidate.height);
|
|
27
|
-
|
|
28
|
-
if (!Number.isFinite(width) || width <= 0 || !Number.isFinite(height) || height <= 0) {
|
|
29
|
-
return null;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
return { width, height };
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
private async tryGetWindowSize(sessionId: string, endpoint: '/window/rect' | '/window/size'): Promise<{ width: number; height: number } | null> {
|
|
36
|
-
try {
|
|
37
|
-
const result = await this.request<unknown>('GET', `/session/${sessionId}${endpoint}`);
|
|
38
|
-
return this.parseWindowSize(result);
|
|
39
|
-
} catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
private async getWindowSize(sessionId: string): Promise<{ width: number; height: number }> {
|
|
45
|
-
const rect = await this.tryGetWindowSize(sessionId, '/window/rect');
|
|
46
|
-
if (rect) {
|
|
47
|
-
return rect;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
const size = await this.tryGetWindowSize(sessionId, '/window/size');
|
|
51
|
-
if (size) {
|
|
52
|
-
return size;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
throw new AppiumCommandError(
|
|
56
|
-
'Unable to determine window size for scroll gesture',
|
|
57
|
-
'WINDOW_SIZE_UNAVAILABLE',
|
|
58
|
-
500,
|
|
59
|
-
);
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
private async request<T>(method: string, path: string, body?: unknown): Promise<T> {
|
|
63
|
-
const response = await fetch(`${this.appiumUrl}${path}`, {
|
|
64
|
-
method,
|
|
65
|
-
headers: {
|
|
66
|
-
'Content-Type': 'application/json',
|
|
67
|
-
},
|
|
68
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
let data: any = null;
|
|
72
|
-
try {
|
|
73
|
-
data = await response.json();
|
|
74
|
-
} catch {
|
|
75
|
-
data = null;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
if (!response.ok) {
|
|
79
|
-
const errorMessage = data?.value?.message || data?.message || `Appium request failed (${response.status})`;
|
|
80
|
-
const errorCode = data?.value?.error || 'APPIUM_REQUEST_FAILED';
|
|
81
|
-
throw new AppiumCommandError(errorMessage, errorCode, response.status, data);
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (data && typeof data === 'object' && 'value' in data) {
|
|
85
|
-
return data.value as T;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
return data as T;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
async createSession(caps: Record<string, unknown>): Promise<{ sessionId: string; value: unknown }> {
|
|
92
|
-
const payload = 'capabilities' in caps
|
|
93
|
-
? caps
|
|
94
|
-
: {
|
|
95
|
-
capabilities: {
|
|
96
|
-
alwaysMatch: caps,
|
|
97
|
-
firstMatch: [{}],
|
|
98
|
-
},
|
|
99
|
-
};
|
|
100
|
-
|
|
101
|
-
const response = await fetch(`${this.appiumUrl}/session`, {
|
|
102
|
-
method: 'POST',
|
|
103
|
-
headers: {
|
|
104
|
-
'Content-Type': 'application/json',
|
|
105
|
-
},
|
|
106
|
-
body: JSON.stringify(payload),
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
const data: any = await response.json();
|
|
110
|
-
|
|
111
|
-
if (!response.ok) {
|
|
112
|
-
const errorMessage = data?.value?.message || data?.message || `Failed to create session (${response.status})`;
|
|
113
|
-
const errorCode = data?.value?.error || 'APPIUM_SESSION_CREATE_FAILED';
|
|
114
|
-
throw new AppiumCommandError(errorMessage, errorCode, response.status, data);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
const sessionId = data?.sessionId || data?.value?.sessionId;
|
|
118
|
-
if (!sessionId) {
|
|
119
|
-
throw new AppiumCommandError('Appium did not return a sessionId', 'APPIUM_SESSION_ID_MISSING', 500, data);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
return {
|
|
123
|
-
sessionId,
|
|
124
|
-
value: data?.value,
|
|
125
|
-
};
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
async deleteSession(sessionId: string): Promise<void> {
|
|
129
|
-
await this.request('DELETE', `/session/${sessionId}`);
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async getSource(sessionId: string): Promise<string> {
|
|
133
|
-
return await this.request<string>('GET', `/session/${sessionId}/source`);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async getScreenshot(sessionId: string): Promise<string> {
|
|
137
|
-
return await this.request<string>('GET', `/session/${sessionId}/screenshot`);
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async captureState(sessionId: string): Promise<{ source: string; screenshot: string }> {
|
|
141
|
-
const [source, screenshot] = await Promise.all([
|
|
142
|
-
this.getSource(sessionId),
|
|
143
|
-
this.getScreenshot(sessionId),
|
|
144
|
-
]);
|
|
145
|
-
|
|
146
|
-
return {
|
|
147
|
-
source,
|
|
148
|
-
screenshot,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
async findElement(sessionId: string, using: string, value: string): Promise<string> {
|
|
153
|
-
const result = await this.request<Record<string, string>>('POST', `/session/${sessionId}/element`, {
|
|
154
|
-
using,
|
|
155
|
-
value,
|
|
156
|
-
});
|
|
157
|
-
|
|
158
|
-
return result[W3C_ELEMENT_KEY] || result.ELEMENT;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
async tap(sessionId: string, using: string, value: string): Promise<void> {
|
|
162
|
-
const elementId = await this.findElement(sessionId, using, value);
|
|
163
|
-
if (!elementId) {
|
|
164
|
-
throw new AppiumCommandError('Element not found for tap command', 'ELEMENT_NOT_FOUND', 404, {
|
|
165
|
-
using,
|
|
166
|
-
value,
|
|
167
|
-
});
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
await this.request('POST', `/session/${sessionId}/element/${elementId}/click`, {});
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
async clear(sessionId: string, using: string, value: string): Promise<void> {
|
|
174
|
-
const elementId = await this.findElement(sessionId, using, value);
|
|
175
|
-
if (!elementId) {
|
|
176
|
-
throw new AppiumCommandError('Element not found for clear command', 'ELEMENT_NOT_FOUND', 404, {
|
|
177
|
-
using,
|
|
178
|
-
value,
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
await this.request('POST', `/session/${sessionId}/element/${elementId}/clear`, {});
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
async type(sessionId: string, using: string, value: string, text: string, clearFirst: boolean): Promise<void> {
|
|
185
|
-
const elementId = await this.findElement(sessionId, using, value);
|
|
186
|
-
if (!elementId) {
|
|
187
|
-
throw new AppiumCommandError('Element not found for type command', 'ELEMENT_NOT_FOUND', 404, {
|
|
188
|
-
using,
|
|
189
|
-
value,
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
if (clearFirst) {
|
|
194
|
-
await this.request('POST', `/session/${sessionId}/element/${elementId}/clear`, {});
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
await this.request('POST', `/session/${sessionId}/element/${elementId}/value`, {
|
|
198
|
-
text,
|
|
199
|
-
value: [...text],
|
|
200
|
-
});
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
async back(sessionId: string): Promise<void> {
|
|
204
|
-
await this.request('POST', `/session/${sessionId}/back`, {});
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
async swipe(sessionId: string, from: Point, to: Point, durationMs: number): Promise<void> {
|
|
208
|
-
await this.request('POST', `/session/${sessionId}/actions`, {
|
|
209
|
-
actions: [
|
|
210
|
-
{
|
|
211
|
-
type: 'pointer',
|
|
212
|
-
id: 'finger1',
|
|
213
|
-
parameters: { pointerType: 'touch' },
|
|
214
|
-
actions: [
|
|
215
|
-
{ type: 'pointerMove', duration: 0, x: from.x, y: from.y },
|
|
216
|
-
{ type: 'pointerDown', button: 0 },
|
|
217
|
-
{ type: 'pause', duration: Math.max(50, durationMs) },
|
|
218
|
-
{ type: 'pointerMove', duration: Math.max(50, durationMs), x: to.x, y: to.y },
|
|
219
|
-
{ type: 'pointerUp', button: 0 },
|
|
220
|
-
],
|
|
221
|
-
},
|
|
222
|
-
],
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
await this.request('DELETE', `/session/${sessionId}/actions`);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
async scroll(sessionId: string, direction: 'up' | 'down' | 'left' | 'right', durationMs = 300): Promise<void> {
|
|
229
|
-
const { width, height } = await this.getWindowSize(sessionId);
|
|
230
|
-
|
|
231
|
-
const centerX = Math.round(width * 0.5);
|
|
232
|
-
const centerY = Math.round(height * 0.5);
|
|
233
|
-
const startY = Math.round(height * 0.8);
|
|
234
|
-
const endY = Math.round(height * 0.2);
|
|
235
|
-
const startX = Math.round(width * 0.8);
|
|
236
|
-
const endX = Math.round(width * 0.2);
|
|
237
|
-
|
|
238
|
-
const coords: Record<'up' | 'down' | 'left' | 'right', { from: Point; to: Point }> = {
|
|
239
|
-
down: { from: { x: centerX, y: startY }, to: { x: centerX, y: endY } },
|
|
240
|
-
up: { from: { x: centerX, y: endY }, to: { x: centerX, y: startY } },
|
|
241
|
-
left: { from: { x: startX, y: centerY }, to: { x: endX, y: centerY } },
|
|
242
|
-
right: { from: { x: endX, y: centerY }, to: { x: startX, y: centerY } },
|
|
243
|
-
};
|
|
244
|
-
|
|
245
|
-
const { from, to } = coords[direction];
|
|
246
|
-
await this.swipe(sessionId, from, to, durationMs);
|
|
247
|
-
}
|
|
248
|
-
}
|