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/ui/tsconfig.json
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2020",
|
|
4
|
-
"module": "ESNext",
|
|
5
|
-
"lib": [
|
|
6
|
-
"ES2020",
|
|
7
|
-
"DOM",
|
|
8
|
-
"DOM.Iterable"
|
|
9
|
-
],
|
|
10
|
-
"jsx": "preserve",
|
|
11
|
-
"jsxImportSource": "solid-js",
|
|
12
|
-
"moduleResolution": "bundler",
|
|
13
|
-
"resolveJsonModule": true,
|
|
14
|
-
"allowImportingTsExtensions": true,
|
|
15
|
-
"strict": true,
|
|
16
|
-
"noUnusedLocals": true,
|
|
17
|
-
"noUnusedParameters": true,
|
|
18
|
-
"noFallthroughCasesInSwitch": true,
|
|
19
|
-
"skipLibCheck": true,
|
|
20
|
-
"paths": {
|
|
21
|
-
"@/*": [
|
|
22
|
-
"./src/*"
|
|
23
|
-
]
|
|
24
|
-
}
|
|
25
|
-
},
|
|
26
|
-
"include": [
|
|
27
|
-
"src"
|
|
28
|
-
],
|
|
29
|
-
"references": [
|
|
30
|
-
{
|
|
31
|
-
"path": "./tsconfig.node.json"
|
|
32
|
-
}
|
|
33
|
-
]
|
|
34
|
-
}
|
package/src/ui/vite.config.ts
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
import { defineConfig } from 'vite';
|
|
2
|
-
import solid from 'vite-plugin-solid';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
|
|
5
|
-
export default defineConfig({
|
|
6
|
-
plugins: [solid()],
|
|
7
|
-
root: '.',
|
|
8
|
-
base: '/_recorder/',
|
|
9
|
-
build: {
|
|
10
|
-
outDir: '../../dist/ui',
|
|
11
|
-
emptyOutDir: true,
|
|
12
|
-
},
|
|
13
|
-
server: {
|
|
14
|
-
port: 3000,
|
|
15
|
-
},
|
|
16
|
-
resolve: {
|
|
17
|
-
alias: {
|
|
18
|
-
'@': path.resolve(__dirname, './src'),
|
|
19
|
-
},
|
|
20
|
-
},
|
|
21
|
-
});
|
|
22
|
-
|
|
@@ -1,397 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { parseArgs, parseCliInput, validatePort, validateUrl, validateHost } from '../../src/cli/arg-parser';
|
|
3
|
-
|
|
4
|
-
describe('parseArgs', () => {
|
|
5
|
-
// Helper to simulate argv (node, script, ...args)
|
|
6
|
-
const createArgv = (...args: string[]) => ['node', 'script.js', ...args];
|
|
7
|
-
|
|
8
|
-
describe('help flag', () => {
|
|
9
|
-
it('should parse --help flag', () => {
|
|
10
|
-
const result = parseArgs(createArgv('--help'));
|
|
11
|
-
|
|
12
|
-
expect(result.success).toBe(true);
|
|
13
|
-
if (result.success) {
|
|
14
|
-
expect(result.args.help).toBe(true);
|
|
15
|
-
}
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should parse -h flag', () => {
|
|
19
|
-
const result = parseArgs(createArgv('-h'));
|
|
20
|
-
|
|
21
|
-
expect(result.success).toBe(true);
|
|
22
|
-
if (result.success) {
|
|
23
|
-
expect(result.args.help).toBe(true);
|
|
24
|
-
}
|
|
25
|
-
});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
describe('version flag', () => {
|
|
29
|
-
it('should parse --version flag', () => {
|
|
30
|
-
const result = parseArgs(createArgv('--version'));
|
|
31
|
-
|
|
32
|
-
expect(result.success).toBe(true);
|
|
33
|
-
if (result.success) {
|
|
34
|
-
expect(result.args.version).toBe(true);
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('should parse -v flag', () => {
|
|
39
|
-
const result = parseArgs(createArgv('-v'));
|
|
40
|
-
|
|
41
|
-
expect(result.success).toBe(true);
|
|
42
|
-
if (result.success) {
|
|
43
|
-
expect(result.args.version).toBe(true);
|
|
44
|
-
}
|
|
45
|
-
});
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
describe('port option', () => {
|
|
49
|
-
it('should parse --port option', () => {
|
|
50
|
-
const result = parseArgs(createArgv('--port', '8080'));
|
|
51
|
-
|
|
52
|
-
expect(result.success).toBe(true);
|
|
53
|
-
if (result.success) {
|
|
54
|
-
expect(result.args.port).toBe(8080);
|
|
55
|
-
}
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('should parse -p option', () => {
|
|
59
|
-
const result = parseArgs(createArgv('-p', '4724'));
|
|
60
|
-
|
|
61
|
-
expect(result.success).toBe(true);
|
|
62
|
-
if (result.success) {
|
|
63
|
-
expect(result.args.port).toBe(4724);
|
|
64
|
-
}
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('should return error when --port has no value', () => {
|
|
68
|
-
const result = parseArgs(createArgv('--port'));
|
|
69
|
-
|
|
70
|
-
expect(result.success).toBe(false);
|
|
71
|
-
if (!result.success) {
|
|
72
|
-
expect(result.error).toBe('--port requires a value');
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('should return error when --port is not a number', () => {
|
|
77
|
-
const result = parseArgs(createArgv('--port', 'abc'));
|
|
78
|
-
|
|
79
|
-
expect(result.success).toBe(false);
|
|
80
|
-
if (!result.success) {
|
|
81
|
-
expect(result.error).toBe('--port must be a number');
|
|
82
|
-
}
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should handle port at end of args', () => {
|
|
86
|
-
const result = parseArgs(createArgv('--host', '0.0.0.0', '--port', '3000'));
|
|
87
|
-
|
|
88
|
-
expect(result.success).toBe(true);
|
|
89
|
-
if (result.success) {
|
|
90
|
-
expect(result.args.port).toBe(3000);
|
|
91
|
-
}
|
|
92
|
-
});
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
describe('appium-url option', () => {
|
|
96
|
-
it('should parse --appium-url option', () => {
|
|
97
|
-
const result = parseArgs(createArgv('--appium-url', 'http://localhost:4723'));
|
|
98
|
-
|
|
99
|
-
expect(result.success).toBe(true);
|
|
100
|
-
if (result.success) {
|
|
101
|
-
expect(result.args.appiumUrl).toBe('http://localhost:4723');
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('should parse -u option', () => {
|
|
106
|
-
const result = parseArgs(createArgv('-u', 'http://192.168.1.100:4723'));
|
|
107
|
-
|
|
108
|
-
expect(result.success).toBe(true);
|
|
109
|
-
if (result.success) {
|
|
110
|
-
expect(result.args.appiumUrl).toBe('http://192.168.1.100:4723');
|
|
111
|
-
}
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should return error when --appium-url has no value', () => {
|
|
115
|
-
const result = parseArgs(createArgv('--appium-url'));
|
|
116
|
-
|
|
117
|
-
expect(result.success).toBe(false);
|
|
118
|
-
if (!result.success) {
|
|
119
|
-
expect(result.error).toBe('--appium-url requires a value');
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
describe('host option', () => {
|
|
125
|
-
it('should parse --host option', () => {
|
|
126
|
-
const result = parseArgs(createArgv('--host', '0.0.0.0'));
|
|
127
|
-
|
|
128
|
-
expect(result.success).toBe(true);
|
|
129
|
-
if (result.success) {
|
|
130
|
-
expect(result.args.host).toBe('0.0.0.0');
|
|
131
|
-
}
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('should return error when --host has no value', () => {
|
|
135
|
-
const result = parseArgs(createArgv('--host'));
|
|
136
|
-
|
|
137
|
-
expect(result.success).toBe(false);
|
|
138
|
-
if (!result.success) {
|
|
139
|
-
expect(result.error).toBe('--host requires a value');
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
describe('multiple options', () => {
|
|
145
|
-
it('should parse multiple options together', () => {
|
|
146
|
-
const result = parseArgs(createArgv(
|
|
147
|
-
'--port', '8080',
|
|
148
|
-
'--appium-url', 'http://localhost:4723',
|
|
149
|
-
'--host', '0.0.0.0'
|
|
150
|
-
));
|
|
151
|
-
|
|
152
|
-
expect(result.success).toBe(true);
|
|
153
|
-
if (result.success) {
|
|
154
|
-
expect(result.args.port).toBe(8080);
|
|
155
|
-
expect(result.args.appiumUrl).toBe('http://localhost:4723');
|
|
156
|
-
expect(result.args.host).toBe('0.0.0.0');
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
it('should parse mixed flags and options', () => {
|
|
161
|
-
const result = parseArgs(createArgv(
|
|
162
|
-
'-p', '3000',
|
|
163
|
-
'-u', 'http://test:4723'
|
|
164
|
-
));
|
|
165
|
-
|
|
166
|
-
expect(result.success).toBe(true);
|
|
167
|
-
if (result.success) {
|
|
168
|
-
expect(result.args.port).toBe(3000);
|
|
169
|
-
expect(result.args.appiumUrl).toBe('http://test:4723');
|
|
170
|
-
}
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
|
|
174
|
-
describe('empty args', () => {
|
|
175
|
-
it('should return empty args for no arguments', () => {
|
|
176
|
-
const result = parseArgs(createArgv());
|
|
177
|
-
|
|
178
|
-
expect(result.success).toBe(true);
|
|
179
|
-
if (result.success) {
|
|
180
|
-
expect(result.args).toEqual({});
|
|
181
|
-
}
|
|
182
|
-
});
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
describe('unknown args', () => {
|
|
186
|
-
it('should ignore unknown arguments', () => {
|
|
187
|
-
const result = parseArgs(createArgv('--unknown', 'value', '--port', '8080'));
|
|
188
|
-
|
|
189
|
-
expect(result.success).toBe(true);
|
|
190
|
-
if (result.success) {
|
|
191
|
-
expect(result.args.port).toBe(8080);
|
|
192
|
-
expect(result.args).not.toHaveProperty('unknown');
|
|
193
|
-
}
|
|
194
|
-
});
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
describe('edge cases', () => {
|
|
198
|
-
it('should handle negative port number', () => {
|
|
199
|
-
const result = parseArgs(createArgv('--port', '-1'));
|
|
200
|
-
|
|
201
|
-
expect(result.success).toBe(true);
|
|
202
|
-
if (result.success) {
|
|
203
|
-
expect(result.args.port).toBe(-1);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
it('should handle zero port', () => {
|
|
208
|
-
const result = parseArgs(createArgv('--port', '0'));
|
|
209
|
-
|
|
210
|
-
expect(result.success).toBe(true);
|
|
211
|
-
if (result.success) {
|
|
212
|
-
expect(result.args.port).toBe(0);
|
|
213
|
-
}
|
|
214
|
-
});
|
|
215
|
-
|
|
216
|
-
it('should handle floating point port (truncates)', () => {
|
|
217
|
-
const result = parseArgs(createArgv('--port', '8080.5'));
|
|
218
|
-
|
|
219
|
-
expect(result.success).toBe(true);
|
|
220
|
-
if (result.success) {
|
|
221
|
-
expect(result.args.port).toBe(8080.5);
|
|
222
|
-
}
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
it('should handle URL with special characters', () => {
|
|
226
|
-
const result = parseArgs(createArgv('--appium-url', 'http://user:pass@localhost:4723/path?query=value'));
|
|
227
|
-
|
|
228
|
-
expect(result.success).toBe(true);
|
|
229
|
-
if (result.success) {
|
|
230
|
-
expect(result.args.appiumUrl).toBe('http://user:pass@localhost:4723/path?query=value');
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
});
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
describe('validatePort', () => {
|
|
237
|
-
it('should return undefined for valid port', () => {
|
|
238
|
-
expect(validatePort('8080')).toBeUndefined();
|
|
239
|
-
});
|
|
240
|
-
|
|
241
|
-
it('should return undefined for empty string', () => {
|
|
242
|
-
expect(validatePort('')).toBeUndefined();
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('should return undefined for whitespace only', () => {
|
|
246
|
-
expect(validatePort(' ')).toBeUndefined();
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
it('should return error for non-numeric string', () => {
|
|
250
|
-
expect(validatePort('abc')).toBe('Please enter a valid port number (1-65535)');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('should return error for port below 1', () => {
|
|
254
|
-
expect(validatePort('0')).toBe('Please enter a valid port number (1-65535)');
|
|
255
|
-
expect(validatePort('-1')).toBe('Please enter a valid port number (1-65535)');
|
|
256
|
-
});
|
|
257
|
-
|
|
258
|
-
it('should return error for port above 65535', () => {
|
|
259
|
-
expect(validatePort('65536')).toBe('Please enter a valid port number (1-65535)');
|
|
260
|
-
expect(validatePort('99999')).toBe('Please enter a valid port number (1-65535)');
|
|
261
|
-
});
|
|
262
|
-
|
|
263
|
-
it('should accept valid port range', () => {
|
|
264
|
-
expect(validatePort('1')).toBeUndefined();
|
|
265
|
-
expect(validatePort('80')).toBeUndefined();
|
|
266
|
-
expect(validatePort('443')).toBeUndefined();
|
|
267
|
-
expect(validatePort('8080')).toBeUndefined();
|
|
268
|
-
expect(validatePort('65535')).toBeUndefined();
|
|
269
|
-
});
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
describe('validateUrl', () => {
|
|
273
|
-
it('should return undefined for valid URL', () => {
|
|
274
|
-
expect(validateUrl('http://localhost:4723')).toBeUndefined();
|
|
275
|
-
});
|
|
276
|
-
|
|
277
|
-
it('should return undefined for empty string', () => {
|
|
278
|
-
expect(validateUrl('')).toBeUndefined();
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
it('should return undefined for whitespace only', () => {
|
|
282
|
-
expect(validateUrl(' ')).toBeUndefined();
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
it('should return error for invalid URL', () => {
|
|
286
|
-
expect(validateUrl('not-a-url')).toBe('Please enter a valid URL');
|
|
287
|
-
});
|
|
288
|
-
|
|
289
|
-
it('should return error for completely invalid URL', () => {
|
|
290
|
-
// Note: URL constructor in Node.js treats 'localhost:4723' as valid
|
|
291
|
-
// (with 'localhost' as protocol), so we test truly invalid URLs
|
|
292
|
-
expect(validateUrl('://invalid')).toBe('Please enter a valid URL');
|
|
293
|
-
});
|
|
294
|
-
|
|
295
|
-
it('should accept various valid URL formats', () => {
|
|
296
|
-
expect(validateUrl('http://localhost')).toBeUndefined();
|
|
297
|
-
expect(validateUrl('https://example.com')).toBeUndefined();
|
|
298
|
-
expect(validateUrl('http://192.168.1.100:4723')).toBeUndefined();
|
|
299
|
-
expect(validateUrl('http://user:pass@host:8080/path')).toBeUndefined();
|
|
300
|
-
});
|
|
301
|
-
});
|
|
302
|
-
|
|
303
|
-
describe('validateHost', () => {
|
|
304
|
-
it('should return undefined for valid host', () => {
|
|
305
|
-
expect(validateHost('localhost')).toBeUndefined();
|
|
306
|
-
});
|
|
307
|
-
|
|
308
|
-
it('should return undefined for empty string', () => {
|
|
309
|
-
expect(validateHost('')).toBeUndefined();
|
|
310
|
-
});
|
|
311
|
-
|
|
312
|
-
it('should return undefined for whitespace only', () => {
|
|
313
|
-
expect(validateHost(' ')).toBeUndefined();
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('should accept IP addresses', () => {
|
|
317
|
-
expect(validateHost('127.0.0.1')).toBeUndefined();
|
|
318
|
-
expect(validateHost('0.0.0.0')).toBeUndefined();
|
|
319
|
-
expect(validateHost('192.168.1.100')).toBeUndefined();
|
|
320
|
-
});
|
|
321
|
-
|
|
322
|
-
it('should accept hostnames', () => {
|
|
323
|
-
expect(validateHost('my-host')).toBeUndefined();
|
|
324
|
-
expect(validateHost('server.local')).toBeUndefined();
|
|
325
|
-
});
|
|
326
|
-
});
|
|
327
|
-
|
|
328
|
-
describe('parseCliInput', () => {
|
|
329
|
-
const createArgv = (...args: string[]) => ['node', 'script.js', ...args];
|
|
330
|
-
|
|
331
|
-
it('keeps no-subcommand invocation in legacy mode', () => {
|
|
332
|
-
const result = parseCliInput(createArgv());
|
|
333
|
-
|
|
334
|
-
expect(result.success).toBe(true);
|
|
335
|
-
if (result.success) {
|
|
336
|
-
expect(result.value.mode).toBe('legacy');
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
|
|
340
|
-
it('routes proxy start command mode', () => {
|
|
341
|
-
const result = parseCliInput(createArgv('proxy', 'start', '--port', '4724'));
|
|
342
|
-
|
|
343
|
-
expect(result.success).toBe(true);
|
|
344
|
-
if (result.success) {
|
|
345
|
-
expect(result.value.mode).toBe('command');
|
|
346
|
-
expect(result.value.route?.group).toBe('proxy');
|
|
347
|
-
expect(result.value.route?.command).toBe('start');
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
|
|
351
|
-
it('supports global output flags', () => {
|
|
352
|
-
const result = parseCliInput(createArgv(
|
|
353
|
-
'session',
|
|
354
|
-
'delete',
|
|
355
|
-
'--session-id',
|
|
356
|
-
'abc',
|
|
357
|
-
'--appium-url',
|
|
358
|
-
'http://127.0.0.1:4723',
|
|
359
|
-
'--pretty',
|
|
360
|
-
'--output',
|
|
361
|
-
'tmp/out.json',
|
|
362
|
-
));
|
|
363
|
-
|
|
364
|
-
expect(result.success).toBe(true);
|
|
365
|
-
if (result.success) {
|
|
366
|
-
expect(result.value.global.pretty).toBe(true);
|
|
367
|
-
expect(result.value.global.output).toBe('tmp/out.json');
|
|
368
|
-
}
|
|
369
|
-
});
|
|
370
|
-
|
|
371
|
-
it('rejects --pretty in legacy mode', () => {
|
|
372
|
-
const result = parseCliInput(createArgv('--pretty', '--port', '4724'));
|
|
373
|
-
|
|
374
|
-
expect(result.success).toBe(false);
|
|
375
|
-
if (!result.success) {
|
|
376
|
-
expect(result.error).toBe('--pretty and --output are only supported with <group> <command> mode');
|
|
377
|
-
}
|
|
378
|
-
});
|
|
379
|
-
|
|
380
|
-
it('rejects --output in legacy mode', () => {
|
|
381
|
-
const result = parseCliInput(createArgv('--output', 'out.json', '--appium-url', 'http://127.0.0.1:4723'));
|
|
382
|
-
|
|
383
|
-
expect(result.success).toBe(false);
|
|
384
|
-
if (!result.success) {
|
|
385
|
-
expect(result.error).toBe('--pretty and --output are only supported with <group> <command> mode');
|
|
386
|
-
}
|
|
387
|
-
});
|
|
388
|
-
|
|
389
|
-
it('rejects mixed --help with command-mode global flags', () => {
|
|
390
|
-
const result = parseCliInput(createArgv('--help', '--pretty'));
|
|
391
|
-
|
|
392
|
-
expect(result.success).toBe(false);
|
|
393
|
-
if (!result.success) {
|
|
394
|
-
expect(result.error).toBe('--pretty and --output are only supported with <group> <command> mode');
|
|
395
|
-
}
|
|
396
|
-
});
|
|
397
|
-
});
|
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { runDriveBack, runDriveScroll, runDriveSwipe, runDriveTap, runDriveType } from '../../src/cli/commands/drive';
|
|
3
|
-
|
|
4
|
-
function jsonResponse(body: unknown, status = 200): Response {
|
|
5
|
-
return new Response(JSON.stringify(body), {
|
|
6
|
-
status,
|
|
7
|
-
headers: { 'Content-Type': 'application/json' },
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
describe('drive commands', () => {
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
vi.restoreAllMocks();
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('taps element by selector', async () => {
|
|
17
|
-
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
|
18
|
-
.mockResolvedValueOnce(jsonResponse({ value: { 'element-6066-11e4-a52e-4f735466cecf': 'element-1' } }))
|
|
19
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }));
|
|
20
|
-
|
|
21
|
-
const result = await runDriveTap([
|
|
22
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
23
|
-
'--session-id', 'session-1',
|
|
24
|
-
'--using', 'accessibility id',
|
|
25
|
-
'--value', 'Login',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
expect(result.command).toBe('drive.tap');
|
|
29
|
-
expect(fetchMock).toHaveBeenNthCalledWith(
|
|
30
|
-
2,
|
|
31
|
-
'http://127.0.0.1:4723/session/session-1/element/element-1/click',
|
|
32
|
-
expect.objectContaining({ method: 'POST' }),
|
|
33
|
-
);
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('types text with clear-first', async () => {
|
|
37
|
-
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
|
38
|
-
.mockResolvedValueOnce(jsonResponse({ value: { 'element-6066-11e4-a52e-4f735466cecf': 'element-1' } }))
|
|
39
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }))
|
|
40
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }));
|
|
41
|
-
|
|
42
|
-
const result = await runDriveType([
|
|
43
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
44
|
-
'--session-id', 'session-1',
|
|
45
|
-
'--using', 'id',
|
|
46
|
-
'--value', 'com.example:id/input',
|
|
47
|
-
'--text', 'user@example.com',
|
|
48
|
-
'--clear-first',
|
|
49
|
-
]);
|
|
50
|
-
|
|
51
|
-
expect(result.command).toBe('drive.type');
|
|
52
|
-
expect((result.result as any).clearFirst).toBe(true);
|
|
53
|
-
expect(fetchMock.mock.calls[1][0]).toContain('/clear');
|
|
54
|
-
expect(fetchMock.mock.calls[2][0]).toContain('/value');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('navigates back', async () => {
|
|
58
|
-
const fetchMock = vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(jsonResponse({ value: null }));
|
|
59
|
-
|
|
60
|
-
await runDriveBack([
|
|
61
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
62
|
-
'--session-id', 'session-1',
|
|
63
|
-
]);
|
|
64
|
-
|
|
65
|
-
expect(fetchMock).toHaveBeenCalledWith(
|
|
66
|
-
'http://127.0.0.1:4723/session/session-1/back',
|
|
67
|
-
expect.objectContaining({ method: 'POST' }),
|
|
68
|
-
);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('performs swipe gesture', async () => {
|
|
72
|
-
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
|
73
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }))
|
|
74
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }));
|
|
75
|
-
|
|
76
|
-
await runDriveSwipe([
|
|
77
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
78
|
-
'--session-id', 'session-1',
|
|
79
|
-
'--from', '100,700',
|
|
80
|
-
'--to', '100,200',
|
|
81
|
-
'--duration-ms', '350',
|
|
82
|
-
]);
|
|
83
|
-
|
|
84
|
-
expect(fetchMock.mock.calls[0][0]).toContain('/actions');
|
|
85
|
-
expect(fetchMock.mock.calls[1][0]).toContain('/actions');
|
|
86
|
-
expect((fetchMock.mock.calls[1][1] as RequestInit).method).toBe('DELETE');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('scrolls down with correct W3C actions payload', async () => {
|
|
90
|
-
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
|
91
|
-
.mockResolvedValueOnce(jsonResponse({ value: { width: 1000, height: 2000 } }))
|
|
92
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }))
|
|
93
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }));
|
|
94
|
-
|
|
95
|
-
const result = await runDriveScroll([
|
|
96
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
97
|
-
'--session-id', 'session-1',
|
|
98
|
-
'--direction', 'down',
|
|
99
|
-
]);
|
|
100
|
-
|
|
101
|
-
expect(result.command).toBe('drive.scroll');
|
|
102
|
-
expect((result.result as any).direction).toBe('down');
|
|
103
|
-
|
|
104
|
-
expect(fetchMock.mock.calls[0][0]).toContain('/session/session-1/window/rect');
|
|
105
|
-
|
|
106
|
-
const actionsCall = fetchMock.mock.calls[1];
|
|
107
|
-
expect(actionsCall[0]).toContain('/session/session-1/actions');
|
|
108
|
-
const body = JSON.parse((actionsCall[1] as RequestInit).body as string);
|
|
109
|
-
const pointerActions = body.actions[0].actions;
|
|
110
|
-
expect(pointerActions[0]).toEqual({ type: 'pointerMove', duration: 0, x: 500, y: 1600 });
|
|
111
|
-
expect(pointerActions[3]).toEqual({ type: 'pointerMove', duration: 300, x: 500, y: 400 });
|
|
112
|
-
|
|
113
|
-
expect(fetchMock.mock.calls[2][0]).toContain('/actions');
|
|
114
|
-
expect((fetchMock.mock.calls[2][1] as RequestInit).method).toBe('DELETE');
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('falls back to /window/size when /window/rect fails', async () => {
|
|
118
|
-
const fetchMock = vi.spyOn(globalThis, 'fetch')
|
|
119
|
-
.mockResolvedValueOnce(jsonResponse({ value: { error: 'unknown command', message: 'not supported' } }, 404))
|
|
120
|
-
.mockResolvedValueOnce(jsonResponse({ value: { width: 1200, height: 1800 } }))
|
|
121
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }))
|
|
122
|
-
.mockResolvedValueOnce(jsonResponse({ value: null }));
|
|
123
|
-
|
|
124
|
-
await runDriveScroll([
|
|
125
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
126
|
-
'--session-id', 'session-1',
|
|
127
|
-
'--direction', 'left',
|
|
128
|
-
]);
|
|
129
|
-
|
|
130
|
-
expect(fetchMock.mock.calls[0][0]).toContain('/session/session-1/window/rect');
|
|
131
|
-
expect(fetchMock.mock.calls[1][0]).toContain('/session/session-1/window/size');
|
|
132
|
-
|
|
133
|
-
const actionsCall = fetchMock.mock.calls[2];
|
|
134
|
-
expect(actionsCall[0]).toContain('/session/session-1/actions');
|
|
135
|
-
const body = JSON.parse((actionsCall[1] as RequestInit).body as string);
|
|
136
|
-
const pointerActions = body.actions[0].actions;
|
|
137
|
-
expect(pointerActions[0]).toEqual({ type: 'pointerMove', duration: 0, x: 960, y: 900 });
|
|
138
|
-
expect(pointerActions[3]).toEqual({ type: 'pointerMove', duration: 300, x: 240, y: 900 });
|
|
139
|
-
|
|
140
|
-
expect(fetchMock.mock.calls[3][0]).toContain('/actions');
|
|
141
|
-
expect((fetchMock.mock.calls[3][1] as RequestInit).method).toBe('DELETE');
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it('rejects invalid scroll direction', async () => {
|
|
145
|
-
await expect(runDriveScroll([
|
|
146
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
147
|
-
'--session-id', 'session-1',
|
|
148
|
-
'--direction', 'diagonal',
|
|
149
|
-
])).rejects.toThrow('--direction must be one of: up, down, left, right');
|
|
150
|
-
});
|
|
151
|
-
});
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { beforeEach, describe, expect, it, vi } from 'vitest';
|
|
2
|
-
import { parseSource } from '../../src/core/xml/parse-source';
|
|
3
|
-
import { runSelectorsBest } from '../../src/cli/commands/selectors';
|
|
4
|
-
|
|
5
|
-
function jsonResponse(body: unknown, status = 200): Response {
|
|
6
|
-
return new Response(JSON.stringify(body), {
|
|
7
|
-
status,
|
|
8
|
-
headers: { 'Content-Type': 'application/json' },
|
|
9
|
-
});
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
describe('selectors best command', () => {
|
|
13
|
-
beforeEach(() => {
|
|
14
|
-
vi.restoreAllMocks();
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
it('returns ranked selectors for a chosen element', async () => {
|
|
18
|
-
const source = `
|
|
19
|
-
<AppiumAUT type="XCUIElementTypeApplication">
|
|
20
|
-
<XCUIElementTypeWindow type="XCUIElementTypeWindow">
|
|
21
|
-
<XCUIElementTypeButton type="XCUIElementTypeButton" name="loginBtn" label="Log In" enabled="true" visible="true" clickable="true" />
|
|
22
|
-
</XCUIElementTypeWindow>
|
|
23
|
-
</AppiumAUT>
|
|
24
|
-
`;
|
|
25
|
-
|
|
26
|
-
const parsed = parseSource(source);
|
|
27
|
-
const target = parsed.elements.find(element => element.type === 'XCUIElementTypeButton');
|
|
28
|
-
expect(target).toBeDefined();
|
|
29
|
-
|
|
30
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValueOnce(jsonResponse({ value: source }));
|
|
31
|
-
|
|
32
|
-
const result = await runSelectorsBest([
|
|
33
|
-
'--appium-url', 'http://127.0.0.1:4723',
|
|
34
|
-
'--session-id', 'session-1',
|
|
35
|
-
'--element-ref', target!.elementRef,
|
|
36
|
-
]);
|
|
37
|
-
|
|
38
|
-
expect(result.command).toBe('selectors.best');
|
|
39
|
-
expect((result.result as any).topSelectors.length).toBeGreaterThan(0);
|
|
40
|
-
expect((result.result as any).topSelectors[0].reasons).toContain('UNIQUE_MATCH');
|
|
41
|
-
});
|
|
42
|
-
});
|