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,74 +0,0 @@
|
|
|
1
|
-
import type { Interaction, ServerEvent } from './types';
|
|
2
|
-
|
|
3
|
-
export class InteractionRecorder {
|
|
4
|
-
private history: Interaction[] = [];
|
|
5
|
-
private interactionId = 0;
|
|
6
|
-
private listeners: Set<(event: ServerEvent) => void> = new Set();
|
|
7
|
-
|
|
8
|
-
shouldRecord(method: string, path: string): boolean {
|
|
9
|
-
if (method === 'POST') {
|
|
10
|
-
return true;
|
|
11
|
-
}
|
|
12
|
-
return false;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
isActionEndpoint(method: string, path: string): boolean {
|
|
16
|
-
const actionPatterns = [
|
|
17
|
-
/\/element\/[^/]+\/click$/,
|
|
18
|
-
/\/element\/[^/]+\/value$/,
|
|
19
|
-
/\/element\/[^/]+\/clear$/,
|
|
20
|
-
/\/element$/,
|
|
21
|
-
/\/elements$/,
|
|
22
|
-
/\/touch\/perform$/,
|
|
23
|
-
/\/actions$/,
|
|
24
|
-
/\/back$/,
|
|
25
|
-
/\/forward$/,
|
|
26
|
-
/\/refresh$/,
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
if (method === 'POST' || method === 'DELETE') {
|
|
30
|
-
return actionPatterns.some(pattern => pattern.test(path));
|
|
31
|
-
}
|
|
32
|
-
return false;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
recordInteraction(interaction: Omit<Interaction, 'id' | 'timestamp'>): Interaction {
|
|
36
|
-
const fullInteraction: Interaction = {
|
|
37
|
-
id: ++this.interactionId,
|
|
38
|
-
timestamp: new Date().toISOString(),
|
|
39
|
-
...interaction,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
this.history.push(fullInteraction);
|
|
43
|
-
this.emit({ type: 'interaction', data: fullInteraction });
|
|
44
|
-
|
|
45
|
-
return fullInteraction;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
updateInteraction(id: number, updates: Partial<Interaction>): void {
|
|
49
|
-
const interaction = this.history.find(i => i.id === id);
|
|
50
|
-
if (interaction) {
|
|
51
|
-
Object.assign(interaction, updates);
|
|
52
|
-
this.emit({ type: 'interaction', data: interaction });
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
getHistory(): Interaction[] {
|
|
57
|
-
return this.history;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
clearHistory(): void {
|
|
61
|
-
this.history = [];
|
|
62
|
-
this.interactionId = 0;
|
|
63
|
-
this.emit({ type: 'clear', data: null });
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
on(listener: (event: ServerEvent) => void): () => void {
|
|
67
|
-
this.listeners.add(listener);
|
|
68
|
-
return () => this.listeners.delete(listener);
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
private emit(event: ServerEvent): void {
|
|
72
|
-
this.listeners.forEach(listener => listener(event));
|
|
73
|
-
}
|
|
74
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import { Request, Response, NextFunction } from 'express';
|
|
2
|
-
import { createProxyMiddleware, fixRequestBody } from 'http-proxy-middleware';
|
|
3
|
-
import type { Interaction } from './types';
|
|
4
|
-
import { AppiumClient } from './appium-client';
|
|
5
|
-
import { InteractionRecorder } from './interaction-recorder';
|
|
6
|
-
|
|
7
|
-
export function createSessionMiddleware(
|
|
8
|
-
recorder: InteractionRecorder,
|
|
9
|
-
appiumClient: AppiumClient,
|
|
10
|
-
) {
|
|
11
|
-
return async (req: Request, res: Response, next: NextFunction) => {
|
|
12
|
-
const { sessionId } = req.params;
|
|
13
|
-
|
|
14
|
-
// Skip ignored endpoints
|
|
15
|
-
if (!recorder.shouldRecord(req.method, req.originalUrl)) {
|
|
16
|
-
return next();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const isAction = recorder.isActionEndpoint(req.method, req.path);
|
|
20
|
-
|
|
21
|
-
// Create interaction record
|
|
22
|
-
const interactionData: Omit<Interaction, 'id' | 'timestamp'> = {
|
|
23
|
-
method: req.method,
|
|
24
|
-
path: req.originalUrl,
|
|
25
|
-
body: req.body && Object.keys(req.body).length > 0 ? req.body : undefined,
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
// Extract element info for find operations
|
|
29
|
-
if (req.body?.using && req.body?.value) {
|
|
30
|
-
interactionData.elementInfo = {
|
|
31
|
-
using: req.body.using,
|
|
32
|
-
value: req.body.value,
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const interaction = recorder.recordInteraction(interactionData);
|
|
37
|
-
|
|
38
|
-
console.log(`[${interaction.id}] ${req.method} ${req.originalUrl}`);
|
|
39
|
-
if (interaction.body) {
|
|
40
|
-
console.log('Body:', JSON.stringify(interaction.body, null, 2));
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
// For actions, capture state after the action completes
|
|
44
|
-
if (isAction) {
|
|
45
|
-
res.on('finish', async () => {
|
|
46
|
-
const state = await appiumClient.captureState(sessionId);
|
|
47
|
-
recorder.updateInteraction(interaction.id, {
|
|
48
|
-
screenshot: state.screenshot,
|
|
49
|
-
source: state.source,
|
|
50
|
-
});
|
|
51
|
-
console.log(`[${interaction.id}] State captured (screenshot + source)`);
|
|
52
|
-
});
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
next();
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function createAppiumProxy(appiumUrl: string) {
|
|
60
|
-
return createProxyMiddleware({
|
|
61
|
-
target: appiumUrl,
|
|
62
|
-
changeOrigin: true,
|
|
63
|
-
ws: true,
|
|
64
|
-
on: {
|
|
65
|
-
proxyReq: fixRequestBody,
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
}
|
package/src/server/routes.ts
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import express, { Router } from 'express';
|
|
2
|
-
import fs from 'fs';
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import type { InteractionRecorder } from './interaction-recorder';
|
|
6
|
-
|
|
7
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
8
|
-
const __dirname = path.dirname(__filename);
|
|
9
|
-
|
|
10
|
-
// Resolve UI dist path: works from both source (src/server/) and bundle (dist/)
|
|
11
|
-
function resolveUiDir(): string {
|
|
12
|
-
const candidates = [
|
|
13
|
-
path.join(__dirname, '../../dist/ui'), // dev: running from src/server/
|
|
14
|
-
path.join(__dirname, 'ui'), // prod: running from dist/
|
|
15
|
-
];
|
|
16
|
-
return candidates.find(p => fs.existsSync(p)) || candidates[0];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
const uiDir = resolveUiDir();
|
|
20
|
-
|
|
21
|
-
export function createRoutes(recorder: InteractionRecorder) {
|
|
22
|
-
const router = Router();
|
|
23
|
-
|
|
24
|
-
// Serve the UI
|
|
25
|
-
router.get('/_recorder', (_req, res) => {
|
|
26
|
-
res.sendFile(path.join(uiDir, 'index.html'));
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
// Serve static assets using express.static for better performance and caching
|
|
30
|
-
router.use('/_recorder/assets', express.static(path.join(uiDir, 'assets')));
|
|
31
|
-
|
|
32
|
-
// API: Get history
|
|
33
|
-
router.get('/_recorder/api/history', (_req, res) => {
|
|
34
|
-
res.json(recorder.getHistory());
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
// API: Clear history
|
|
38
|
-
router.delete('/_recorder/api/history', (_req, res) => {
|
|
39
|
-
recorder.clearHistory();
|
|
40
|
-
res.json({ ok: true });
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
// API: Server-Sent Events for real-time updates
|
|
44
|
-
router.get('/_recorder/api/stream', (req, res) => {
|
|
45
|
-
res.setHeader('Content-Type', 'text/event-stream');
|
|
46
|
-
res.setHeader('Cache-Control', 'no-cache');
|
|
47
|
-
res.setHeader('Connection', 'keep-alive');
|
|
48
|
-
|
|
49
|
-
// Send initial history
|
|
50
|
-
res.write(`data: ${JSON.stringify({ type: 'init', data: recorder.getHistory() })}\n\n`);
|
|
51
|
-
|
|
52
|
-
// Listen for new interactions
|
|
53
|
-
const unsubscribe = recorder.on((event) => {
|
|
54
|
-
res.write(`data: ${JSON.stringify(event)}\n\n`);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
// Clean up on close
|
|
58
|
-
req.on('close', () => {
|
|
59
|
-
unsubscribe();
|
|
60
|
-
});
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
return router;
|
|
64
|
-
}
|
package/src/server/server.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import express from 'express';
|
|
2
|
-
import type { RecorderOptions } from './types';
|
|
3
|
-
import { AppiumClient } from './appium-client';
|
|
4
|
-
import { InteractionRecorder } from './interaction-recorder';
|
|
5
|
-
import { createSessionMiddleware, createAppiumProxy } from './proxy-middleware';
|
|
6
|
-
import { createRoutes } from './routes';
|
|
7
|
-
|
|
8
|
-
export function createServer(options: RecorderOptions = {}) {
|
|
9
|
-
const appiumUrl = options.appiumUrl ?? 'http://127.0.0.1:4723';
|
|
10
|
-
|
|
11
|
-
const app = express();
|
|
12
|
-
const appiumClient = new AppiumClient(appiumUrl);
|
|
13
|
-
const recorder = new InteractionRecorder();
|
|
14
|
-
|
|
15
|
-
// Parse JSON bodies
|
|
16
|
-
app.use(express.json({ type: ['application/json', 'application/*+json'], limit: '10mb' }));
|
|
17
|
-
app.use(express.urlencoded({ extended: true, limit: '1mb' }));
|
|
18
|
-
|
|
19
|
-
// Register routes
|
|
20
|
-
app.use(createRoutes(recorder));
|
|
21
|
-
|
|
22
|
-
// Intercept session requests
|
|
23
|
-
app.use('/session/:sessionId', createSessionMiddleware(recorder, appiumClient));
|
|
24
|
-
|
|
25
|
-
// Proxy everything to Appium
|
|
26
|
-
app.use(createAppiumProxy(appiumUrl));
|
|
27
|
-
|
|
28
|
-
return { app, recorder };
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export function startServer(options: RecorderOptions = {}) {
|
|
32
|
-
const host = options.host ?? '127.0.0.1';
|
|
33
|
-
const port = options.port ?? 4724;
|
|
34
|
-
const appiumUrl = options.appiumUrl ?? 'http://127.0.0.1:4723';
|
|
35
|
-
|
|
36
|
-
const { app } = createServer(options);
|
|
37
|
-
|
|
38
|
-
return app.listen(port, host, () => {
|
|
39
|
-
console.log(`\n✅ Session Recorder started:`);
|
|
40
|
-
console.log(` Proxy: http://${host}:${port} → ${appiumUrl}`);
|
|
41
|
-
console.log(` Viewer: http://${host}:${port}/_recorder\n`);
|
|
42
|
-
});
|
|
43
|
-
}
|
package/src/server/types.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export type RecorderOptions = {
|
|
2
|
-
appiumUrl?: string;
|
|
3
|
-
host?: string;
|
|
4
|
-
port?: number;
|
|
5
|
-
};
|
|
6
|
-
|
|
7
|
-
export type ElementInfo = {
|
|
8
|
-
using: string;
|
|
9
|
-
value: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export type Interaction = {
|
|
13
|
-
id: number;
|
|
14
|
-
timestamp: string;
|
|
15
|
-
method: string;
|
|
16
|
-
path: string;
|
|
17
|
-
body?: any;
|
|
18
|
-
screenshot?: string; // base64
|
|
19
|
-
source?: string; // XML
|
|
20
|
-
elementInfo?: ElementInfo;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export type AppiumResponse = {
|
|
24
|
-
value: any;
|
|
25
|
-
sessionId?: string;
|
|
26
|
-
status?: number;
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
export type ServerEventType = 'interaction' | 'clear';
|
|
30
|
-
|
|
31
|
-
export type ServerEvent = {
|
|
32
|
-
type: ServerEventType;
|
|
33
|
-
data: Interaction | null;
|
|
34
|
-
};
|