appium-session-recorder 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (73) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +362 -0
  3. package/bun.lock +731 -0
  4. package/package.json +62 -0
  5. package/skills/appium-cli-selector-navigator/SKILL.md +349 -0
  6. package/src/cli/arg-parser.ts +311 -0
  7. package/src/cli/commands/drive.ts +147 -0
  8. package/src/cli/commands/index.ts +54 -0
  9. package/src/cli/commands/proxy.ts +41 -0
  10. package/src/cli/commands/screen.ts +73 -0
  11. package/src/cli/commands/selectors.ts +42 -0
  12. package/src/cli/commands/session.ts +64 -0
  13. package/src/cli/commands/types.ts +11 -0
  14. package/src/cli/index.ts +158 -0
  15. package/src/cli/prompts.ts +64 -0
  16. package/src/cli/response.ts +44 -0
  17. package/src/core/appium/client.ts +248 -0
  18. package/src/core/index.ts +5 -0
  19. package/src/core/selectors/generate-candidates.ts +155 -0
  20. package/src/core/selectors/score-candidates.ts +184 -0
  21. package/src/core/types.ts +79 -0
  22. package/src/core/xml/parse-source.ts +197 -0
  23. package/src/index.ts +7 -0
  24. package/src/server/appium-client.ts +24 -0
  25. package/src/server/index.ts +6 -0
  26. package/src/server/interaction-recorder.ts +74 -0
  27. package/src/server/proxy-middleware.ts +68 -0
  28. package/src/server/routes.ts +53 -0
  29. package/src/server/server.ts +43 -0
  30. package/src/server/types.ts +34 -0
  31. package/src/ui/bun.lock +311 -0
  32. package/src/ui/index.html +16 -0
  33. package/src/ui/package.json +20 -0
  34. package/src/ui/src/App.css +12 -0
  35. package/src/ui/src/App.tsx +41 -0
  36. package/src/ui/src/components/ActionCarousel.css +128 -0
  37. package/src/ui/src/components/ActionCarousel.tsx +92 -0
  38. package/src/ui/src/components/Inspector.css +314 -0
  39. package/src/ui/src/components/Inspector.tsx +265 -0
  40. package/src/ui/src/components/InteractionCard.css +159 -0
  41. package/src/ui/src/components/InteractionCard.tsx +60 -0
  42. package/src/ui/src/components/MainInspector.css +304 -0
  43. package/src/ui/src/components/MainInspector.tsx +304 -0
  44. package/src/ui/src/components/Stats.css +27 -0
  45. package/src/ui/src/components/Timeline.css +31 -0
  46. package/src/ui/src/components/Timeline.tsx +37 -0
  47. package/src/ui/src/hooks/useInteractions.ts +73 -0
  48. package/src/ui/src/index.tsx +11 -0
  49. package/src/ui/src/services/api.ts +41 -0
  50. package/src/ui/src/styles/tokens.css +126 -0
  51. package/src/ui/src/types.ts +34 -0
  52. package/src/ui/src/utils/__tests__/locators.test.ts +304 -0
  53. package/src/ui/src/utils/__tests__/xml-parser.test.ts +326 -0
  54. package/src/ui/src/utils/locators.ts +14 -0
  55. package/src/ui/src/utils/xml-parser.ts +45 -0
  56. package/src/ui/tsconfig.json +34 -0
  57. package/src/ui/tsconfig.node.json +11 -0
  58. package/src/ui/vite.config.ts +22 -0
  59. package/tests/cli/arg-parser.test.ts +397 -0
  60. package/tests/cli/drive-commands.test.ts +151 -0
  61. package/tests/cli/selectors-best.test.ts +42 -0
  62. package/tests/cli/session-commands.test.ts +53 -0
  63. package/tests/core/selector-candidates.test.ts +83 -0
  64. package/tests/core/selector-scoring.test.ts +75 -0
  65. package/tests/core/xml-parser.test.ts +56 -0
  66. package/tests/server/appium-client.test.ts +229 -0
  67. package/tests/server/interaction-recorder.test.ts +377 -0
  68. package/tests/server/proxy-middleware.test.ts +343 -0
  69. package/tests/server/routes.test.ts +305 -0
  70. package/tsconfig.json +26 -0
  71. package/vitest.config.ts +16 -0
  72. package/vitest.ui.config.ts +15 -0
  73. package/workflow.gif +0 -0
@@ -0,0 +1,16 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="description" content="Appium Session Recorder - Interactive session recording and element inspection" />
7
+ <title>Appium Session Recorder</title>
8
+ <link rel="preconnect" href="https://fonts.googleapis.com">
9
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
11
+ </head>
12
+ <body>
13
+ <div id="root"></div>
14
+ <script type="module" src="/src/index.tsx"></script>
15
+ </body>
16
+ </html>
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "appium-recorder-ui",
3
+ "version": "2.0.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "vite",
8
+ "build": "vite build",
9
+ "preview": "vite preview"
10
+ },
11
+ "dependencies": {
12
+ "@kobalte/core": "^0.13.11",
13
+ "solid-js": "^1.9.11"
14
+ },
15
+ "devDependencies": {
16
+ "typescript": "^5.9.3",
17
+ "vite": "^6.3.5",
18
+ "vite-plugin-solid": "^2.11.10"
19
+ }
20
+ }
@@ -0,0 +1,12 @@
1
+ .app {
2
+ height: 100vh;
3
+ display: flex;
4
+ flex-direction: column;
5
+ background: var(--color-bg-primary);
6
+ overflow: hidden;
7
+ }
8
+
9
+ .app-main {
10
+ flex: 1;
11
+ overflow: hidden;
12
+ }
@@ -0,0 +1,41 @@
1
+ import { type Component, createSignal, createMemo, createEffect } from 'solid-js';
2
+ import { useInteractions } from './hooks/useInteractions';
3
+ import { ActionCarousel } from './components/ActionCarousel';
4
+ import { MainInspector } from './components/MainInspector';
5
+ import './App.css';
6
+
7
+ const App: Component = () => {
8
+ const { interactions } = useInteractions();
9
+ const [currentIndex, setCurrentIndex] = createSignal(0);
10
+
11
+ // Filter to only actions (interactions with screenshots)
12
+ const actions = createMemo(() =>
13
+ interactions().filter(i => i.screenshot)
14
+ );
15
+
16
+ // Get the current action
17
+ const currentAction = createMemo(() => actions()[currentIndex()]);
18
+
19
+ // Auto-select the latest action when new ones are added
20
+ createEffect(() => {
21
+ const actionsCount = actions().length;
22
+ if (actionsCount > 0) {
23
+ setCurrentIndex(actionsCount - 1);
24
+ }
25
+ });
26
+
27
+ return (
28
+ <div class="app">
29
+ <ActionCarousel
30
+ interactions={interactions()}
31
+ currentIndex={currentIndex()}
32
+ onNavigate={setCurrentIndex}
33
+ />
34
+ <main class="app-main">
35
+ <MainInspector interaction={currentAction()} />
36
+ </main>
37
+ </div>
38
+ );
39
+ };
40
+
41
+ export default App;
@@ -0,0 +1,128 @@
1
+ .carousel {
2
+ display: flex;
3
+ align-items: center;
4
+ gap: var(--spacing-4);
5
+ padding: var(--spacing-3) var(--spacing-6);
6
+ background: var(--color-bg-secondary);
7
+ border-bottom: 1px solid var(--color-border);
8
+ }
9
+
10
+ .carousel-empty {
11
+ flex: 1;
12
+ display: flex;
13
+ align-items: center;
14
+ justify-content: center;
15
+ gap: var(--spacing-3);
16
+ color: var(--color-text-tertiary);
17
+ font-size: var(--font-size-sm);
18
+ padding: var(--spacing-2) 0;
19
+ }
20
+
21
+ .carousel-empty-icon {
22
+ font-size: var(--font-size-lg);
23
+ }
24
+
25
+ .carousel-btn {
26
+ background: var(--color-bg-secondary);
27
+ color: var(--color-text-secondary);
28
+ border: 1px solid var(--color-border);
29
+ padding: var(--spacing-2) var(--spacing-4);
30
+ border-radius: var(--radius-lg);
31
+ cursor: pointer;
32
+ font-weight: var(--font-weight-medium);
33
+ font-size: var(--font-size-sm);
34
+ transition: all var(--transition-fast);
35
+ white-space: nowrap;
36
+ }
37
+
38
+ .carousel-btn:hover:not(:disabled) {
39
+ background: var(--color-accent-primary);
40
+ color: white;
41
+ border-color: var(--color-accent-primary);
42
+ }
43
+
44
+ .carousel-btn:disabled {
45
+ opacity: 0.35;
46
+ cursor: not-allowed;
47
+ }
48
+
49
+ .carousel-info {
50
+ flex: 1;
51
+ display: flex;
52
+ flex-direction: column;
53
+ align-items: center;
54
+ gap: var(--spacing-1);
55
+ }
56
+
57
+ .carousel-counter {
58
+ font-size: var(--font-size-xs);
59
+ color: var(--color-text-tertiary);
60
+ font-weight: var(--font-weight-medium);
61
+ letter-spacing: 0.02em;
62
+ }
63
+
64
+ .carousel-details {
65
+ display: flex;
66
+ align-items: center;
67
+ gap: var(--spacing-3);
68
+ flex-wrap: wrap;
69
+ justify-content: center;
70
+ }
71
+
72
+ .carousel-id {
73
+ font-size: var(--font-size-xs);
74
+ color: var(--color-text-tertiary);
75
+ font-weight: var(--font-weight-medium);
76
+ }
77
+
78
+ .carousel-method {
79
+ font-size: var(--font-size-xs);
80
+ font-weight: var(--font-weight-semibold);
81
+ padding: 2px var(--spacing-2);
82
+ border-radius: var(--radius-sm);
83
+ text-transform: uppercase;
84
+ letter-spacing: 0.03em;
85
+ }
86
+
87
+ .carousel-method.POST {
88
+ background: #E8F5EE;
89
+ color: var(--color-accent-success);
90
+ }
91
+
92
+ .carousel-method.GET {
93
+ background: #FFF3E0;
94
+ color: var(--color-accent-warning);
95
+ }
96
+
97
+ .carousel-method.DELETE {
98
+ background: #FDECEB;
99
+ color: var(--color-accent-error);
100
+ }
101
+
102
+ .carousel-path {
103
+ font-size: var(--font-size-sm);
104
+ color: var(--color-text-secondary);
105
+ max-width: 400px;
106
+ overflow: hidden;
107
+ text-overflow: ellipsis;
108
+ white-space: nowrap;
109
+ }
110
+
111
+ .carousel-time {
112
+ font-size: var(--font-size-xs);
113
+ color: var(--color-text-tertiary);
114
+ }
115
+
116
+ .carousel-element {
117
+ font-size: var(--font-size-xs);
118
+ color: var(--color-text-secondary);
119
+ }
120
+
121
+ .carousel-element-using {
122
+ color: var(--color-accent-secondary);
123
+ font-weight: var(--font-weight-medium);
124
+ }
125
+
126
+ .carousel-element-value {
127
+ color: var(--color-accent-primary);
128
+ }
@@ -0,0 +1,92 @@
1
+ import { type Component, Show, createMemo } from 'solid-js';
2
+ import type { Interaction } from '../types';
3
+ import './ActionCarousel.css';
4
+
5
+ type ActionCarouselProps = {
6
+ interactions: Interaction[];
7
+ currentIndex: number;
8
+ onNavigate: (index: number) => void;
9
+ };
10
+
11
+ export const ActionCarousel: Component<ActionCarouselProps> = (props) => {
12
+ const actions = createMemo(() =>
13
+ props.interactions.filter(i => i.screenshot)
14
+ );
15
+
16
+ const currentAction = createMemo(() => actions()[props.currentIndex]);
17
+ const total = createMemo(() => actions().length);
18
+
19
+ const goToPrevious = () => {
20
+ if (props.currentIndex > 0) {
21
+ props.onNavigate(props.currentIndex - 1);
22
+ }
23
+ };
24
+
25
+ const goToNext = () => {
26
+ if (props.currentIndex < total() - 1) {
27
+ props.onNavigate(props.currentIndex + 1);
28
+ }
29
+ };
30
+
31
+ const formattedTime = () => {
32
+ const action = currentAction();
33
+ return action ? new Date(action.timestamp).toLocaleTimeString() : '';
34
+ };
35
+
36
+ return (
37
+ <div class="carousel">
38
+ <Show
39
+ when={total() > 0}
40
+ fallback={
41
+ <div class="carousel-empty">
42
+ <span class="carousel-empty-icon">📱</span>
43
+ <span>No actions recorded yet. Connect Appium Inspector to port 4724 and start interacting.</span>
44
+ </div>
45
+ }
46
+ >
47
+ <button
48
+ class="carousel-btn"
49
+ onClick={goToPrevious}
50
+ disabled={props.currentIndex === 0}
51
+ >
52
+ ← Previous
53
+ </button>
54
+
55
+ <div class="carousel-info">
56
+ <div class="carousel-counter">
57
+ Action {props.currentIndex + 1} of {total()}
58
+ </div>
59
+ <Show when={currentAction()}>
60
+ <div class="carousel-details">
61
+ <span class="carousel-id">#{currentAction()!.id}</span>
62
+ <span classList={{
63
+ 'carousel-method': true,
64
+ [currentAction()!.method]: true
65
+ }}>
66
+ {currentAction()!.method}
67
+ </span>
68
+ <span class="carousel-path">{currentAction()!.path}</span>
69
+ <span class="carousel-time">{formattedTime()}</span>
70
+ </div>
71
+ <Show when={currentAction()!.elementInfo}>
72
+ <div class="carousel-element">
73
+ <span class="carousel-element-using">{currentAction()!.elementInfo!.using}:</span>
74
+ {' "'}
75
+ <span class="carousel-element-value">{currentAction()!.elementInfo!.value}</span>
76
+ {'"'}
77
+ </div>
78
+ </Show>
79
+ </Show>
80
+ </div>
81
+
82
+ <button
83
+ class="carousel-btn"
84
+ onClick={goToNext}
85
+ disabled={props.currentIndex === total() - 1}
86
+ >
87
+ Next →
88
+ </button>
89
+ </Show>
90
+ </div>
91
+ );
92
+ };
@@ -0,0 +1,314 @@
1
+ .inspector-overlay {
2
+ position: fixed;
3
+ inset: 0;
4
+ background: rgba(0, 0, 0, 0.4);
5
+ backdrop-filter: blur(4px);
6
+ z-index: var(--z-modal);
7
+ animation: fadeIn var(--transition-base);
8
+ }
9
+
10
+ .inspector-modal {
11
+ position: fixed;
12
+ top: 50%;
13
+ left: 50%;
14
+ transform: translate(-50%, -50%);
15
+ background: var(--color-bg-primary);
16
+ border-radius: var(--radius-2xl);
17
+ padding: var(--spacing-6);
18
+ width: 90vw;
19
+ max-width: 1400px;
20
+ max-height: 90vh;
21
+ overflow: hidden;
22
+ z-index: var(--z-modal);
23
+ box-shadow: var(--shadow-lg);
24
+ border: 1px solid var(--color-border);
25
+ animation: slideIn var(--transition-base);
26
+ }
27
+
28
+ .inspector-close {
29
+ position: absolute;
30
+ top: var(--spacing-4);
31
+ right: var(--spacing-4);
32
+ background: var(--color-bg-tertiary);
33
+ border: 1px solid var(--color-border);
34
+ width: 36px;
35
+ height: 36px;
36
+ border-radius: var(--radius-full);
37
+ font-size: var(--font-size-base);
38
+ cursor: pointer;
39
+ color: var(--color-text-secondary);
40
+ transition: all var(--transition-fast);
41
+ z-index: 1;
42
+ display: flex;
43
+ align-items: center;
44
+ justify-content: center;
45
+ }
46
+
47
+ .inspector-close:hover {
48
+ background: var(--color-accent-error);
49
+ border-color: var(--color-accent-error);
50
+ color: white;
51
+ }
52
+
53
+ .inspector-panel {
54
+ display: flex;
55
+ gap: var(--spacing-6);
56
+ height: 100%;
57
+ }
58
+
59
+ .inspector-left {
60
+ flex-shrink: 0;
61
+ }
62
+
63
+ .inspector-screenshot {
64
+ max-height: 80vh;
65
+ border-radius: var(--radius-xl);
66
+ box-shadow: var(--shadow-md);
67
+ border: 1px solid var(--color-border);
68
+ }
69
+
70
+ .inspector-right {
71
+ flex: 1;
72
+ overflow-y: auto;
73
+ padding-right: var(--spacing-2);
74
+ max-height: calc(90vh - var(--spacing-12));
75
+ }
76
+
77
+ .inspector-section {
78
+ background: var(--color-bg-secondary);
79
+ border-radius: var(--radius-xl);
80
+ padding: var(--spacing-5);
81
+ margin-bottom: var(--spacing-3);
82
+ border: 1px solid var(--color-border);
83
+ }
84
+
85
+ .inspector-section h3 {
86
+ color: var(--color-text-primary);
87
+ font-size: var(--font-size-sm);
88
+ margin-bottom: var(--spacing-3);
89
+ font-weight: var(--font-weight-semibold);
90
+ }
91
+
92
+ .query-tester {
93
+ display: flex;
94
+ flex-direction: column;
95
+ gap: var(--spacing-3);
96
+ }
97
+
98
+ .query-row {
99
+ display: flex;
100
+ gap: var(--spacing-2);
101
+ }
102
+
103
+ .query-select,
104
+ .query-input {
105
+ background: var(--color-bg-primary);
106
+ color: var(--color-text-primary);
107
+ border: 1px solid var(--color-border);
108
+ padding: var(--spacing-2) var(--spacing-3);
109
+ border-radius: var(--radius-lg);
110
+ font-size: var(--font-size-sm);
111
+ font-family: var(--font-family);
112
+ transition: border-color var(--transition-fast);
113
+ }
114
+
115
+ .query-select:focus,
116
+ .query-input:focus {
117
+ outline: none;
118
+ border-color: var(--color-accent-primary);
119
+ }
120
+
121
+ .query-select {
122
+ min-width: 150px;
123
+ }
124
+
125
+ .query-input {
126
+ flex: 1;
127
+ }
128
+
129
+ .query-btn {
130
+ background: var(--color-accent-primary);
131
+ color: white;
132
+ border: none;
133
+ padding: var(--spacing-2) var(--spacing-5);
134
+ border-radius: var(--radius-lg);
135
+ cursor: pointer;
136
+ font-weight: var(--font-weight-medium);
137
+ font-size: var(--font-size-sm);
138
+ transition: all var(--transition-fast);
139
+ }
140
+
141
+ .query-btn:hover {
142
+ background: var(--color-accent-secondary);
143
+ }
144
+
145
+ .query-result {
146
+ padding: var(--spacing-2) var(--spacing-3);
147
+ border-radius: var(--radius-md);
148
+ font-size: var(--font-size-sm);
149
+ }
150
+
151
+ .query-result.success {
152
+ background: #E8F5EE;
153
+ color: var(--color-accent-success);
154
+ }
155
+
156
+ .element-details {
157
+ display: flex;
158
+ flex-direction: column;
159
+ gap: var(--spacing-2);
160
+ }
161
+
162
+ .element-attr {
163
+ display: flex;
164
+ gap: var(--spacing-4);
165
+ font-size: var(--font-size-sm);
166
+ }
167
+
168
+ .attr-name {
169
+ color: var(--color-text-tertiary);
170
+ min-width: 90px;
171
+ font-weight: var(--font-weight-medium);
172
+ }
173
+
174
+ .attr-value {
175
+ color: var(--color-text-primary);
176
+ }
177
+
178
+ .locators-list {
179
+ display: flex;
180
+ flex-direction: column;
181
+ gap: var(--spacing-1);
182
+ }
183
+
184
+ .locator-row {
185
+ display: flex;
186
+ gap: var(--spacing-3);
187
+ padding: var(--spacing-2) var(--spacing-3);
188
+ background: var(--color-bg-primary);
189
+ border-radius: var(--radius-md);
190
+ cursor: pointer;
191
+ transition: all var(--transition-fast);
192
+ border: 1px solid transparent;
193
+ }
194
+
195
+ .locator-row:hover {
196
+ background: var(--color-bg-tertiary);
197
+ border-color: var(--color-border);
198
+ }
199
+
200
+ .locator-strategy {
201
+ color: var(--color-accent-primary);
202
+ font-size: var(--font-size-xs);
203
+ min-width: 140px;
204
+ font-weight: var(--font-weight-medium);
205
+ font-family: var(--font-mono);
206
+ }
207
+
208
+ .locator-value {
209
+ flex: 1;
210
+ font-size: var(--font-size-xs);
211
+ color: var(--color-text-secondary);
212
+ word-break: break-all;
213
+ font-family: var(--font-mono);
214
+ }
215
+
216
+ @keyframes fadeIn {
217
+ from { opacity: 0; }
218
+ to { opacity: 1; }
219
+ }
220
+
221
+ @keyframes slideIn {
222
+ from {
223
+ opacity: 0;
224
+ transform: translate(-50%, -48%);
225
+ }
226
+ to {
227
+ opacity: 1;
228
+ transform: translate(-50%, -50%);
229
+ }
230
+ }
231
+
232
+ .element-item {
233
+ display: flex;
234
+ gap: var(--spacing-3);
235
+ padding: var(--spacing-2) var(--spacing-3);
236
+ background: var(--color-bg-primary);
237
+ border-radius: var(--radius-sm);
238
+ cursor: pointer;
239
+ transition: all var(--transition-fast);
240
+ font-size: var(--font-size-xs);
241
+ border: 1px solid transparent;
242
+ }
243
+
244
+ .element-item:hover {
245
+ background: var(--color-bg-tertiary);
246
+ border-color: var(--color-border);
247
+ }
248
+
249
+ .element-item.selected {
250
+ background: var(--color-accent-primary);
251
+ color: white;
252
+ border-color: var(--color-accent-primary);
253
+ }
254
+
255
+ .element-item.selected .element-type {
256
+ color: white;
257
+ }
258
+
259
+ .element-type {
260
+ color: var(--color-accent-primary);
261
+ font-weight: var(--font-weight-medium);
262
+ min-width: 100px;
263
+ }
264
+
265
+ .element-name {
266
+ color: var(--color-text-secondary);
267
+ overflow: hidden;
268
+ text-overflow: ellipsis;
269
+ white-space: nowrap;
270
+ }
271
+
272
+ .element-item.selected .element-name {
273
+ color: rgba(255, 255, 255, 0.85);
274
+ }
275
+
276
+ .elements-more {
277
+ padding: var(--spacing-2);
278
+ color: var(--color-text-tertiary);
279
+ font-size: var(--font-size-xs);
280
+ text-align: center;
281
+ }
282
+
283
+ /* XML Source */
284
+ .source-toggle-btn {
285
+ background: var(--color-bg-tertiary);
286
+ color: var(--color-text-secondary);
287
+ border: 1px solid var(--color-border);
288
+ padding: var(--spacing-2) var(--spacing-4);
289
+ border-radius: var(--radius-lg);
290
+ cursor: pointer;
291
+ font-size: var(--font-size-sm);
292
+ transition: all var(--transition-fast);
293
+ margin-bottom: var(--spacing-3);
294
+ }
295
+
296
+ .source-toggle-btn:hover {
297
+ background: var(--color-border);
298
+ color: var(--color-text-primary);
299
+ }
300
+
301
+ .xml-source {
302
+ background: var(--color-bg-primary);
303
+ padding: var(--spacing-4);
304
+ border-radius: var(--radius-lg);
305
+ font-size: var(--font-size-xs);
306
+ font-family: var(--font-mono);
307
+ max-height: 300px;
308
+ overflow: auto;
309
+ white-space: pre-wrap;
310
+ word-break: break-all;
311
+ color: var(--color-text-secondary);
312
+ border: 1px solid var(--color-border);
313
+ line-height: 1.7;
314
+ }