appium-session-recorder 0.0.2 → 0.0.3
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 +32422 -0
- package/dist/ui/assets/index-CUcJNRfB.css +1 -0
- package/dist/ui/assets/index-Cl_X3tPj.js +4 -0
- package/{src → dist}/ui/index.html +2 -1
- package/package.json +10 -3
- 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,304 +0,0 @@
|
|
|
1
|
-
import { type Component, createSignal, Show, For, createEffect, createRenderEffect, onCleanup } from 'solid-js';
|
|
2
|
-
import type { Interaction } from '../types';
|
|
3
|
-
import { parseXmlSource } from '../utils/xml-parser';
|
|
4
|
-
import { generateLocators } from '../utils/locators';
|
|
5
|
-
import type { ParsedElement, Locator } from '../types';
|
|
6
|
-
import './MainInspector.css';
|
|
7
|
-
|
|
8
|
-
type MainInspectorProps = {
|
|
9
|
-
interaction: Interaction | null;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
export const MainInspector: Component<MainInspectorProps> = (props) => {
|
|
13
|
-
const [selectedElement, setSelectedElement] = createSignal<ParsedElement | null>(null);
|
|
14
|
-
const [queryStrategy, setQueryStrategy] = createSignal('accessibility id');
|
|
15
|
-
const [queryValue, setQueryValue] = createSignal('');
|
|
16
|
-
const [foundElements, setFoundElements] = createSignal<ParsedElement[]>([]);
|
|
17
|
-
const [copiedText, setCopiedText] = createSignal<string | null>(null);
|
|
18
|
-
const [queryError, setQueryError] = createSignal<string | null>(null);
|
|
19
|
-
const [xmlPreRef, setXmlPreRef] = createSignal<HTMLPreElement | undefined>(undefined);
|
|
20
|
-
|
|
21
|
-
// Reset state when interaction changes
|
|
22
|
-
createEffect(() => {
|
|
23
|
-
if (props.interaction) {
|
|
24
|
-
setSelectedElement(null);
|
|
25
|
-
setQueryValue('');
|
|
26
|
-
setFoundElements([]);
|
|
27
|
-
}
|
|
28
|
-
});
|
|
29
|
-
|
|
30
|
-
const parsedElements = () => {
|
|
31
|
-
if (!props.interaction?.source) return [];
|
|
32
|
-
return parseXmlSource(props.interaction.source);
|
|
33
|
-
};
|
|
34
|
-
|
|
35
|
-
const runQuery = () => {
|
|
36
|
-
const strategy = queryStrategy();
|
|
37
|
-
const value = queryValue().trim();
|
|
38
|
-
|
|
39
|
-
setQueryError(null);
|
|
40
|
-
|
|
41
|
-
if (!value) return;
|
|
42
|
-
|
|
43
|
-
const elements = parsedElements();
|
|
44
|
-
let found: ParsedElement[] = [];
|
|
45
|
-
|
|
46
|
-
switch (strategy) {
|
|
47
|
-
case 'accessibility id':
|
|
48
|
-
found = elements.filter(el => el.name === value || el.label === value);
|
|
49
|
-
break;
|
|
50
|
-
case 'class name':
|
|
51
|
-
found = elements.filter(el => el.type === value);
|
|
52
|
-
break;
|
|
53
|
-
case 'xpath':
|
|
54
|
-
if (props.interaction?.source) {
|
|
55
|
-
try {
|
|
56
|
-
const parser = new DOMParser();
|
|
57
|
-
const doc = parser.parseFromString(props.interaction.source, 'text/xml');
|
|
58
|
-
const result = doc.evaluate(value, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
|
|
59
|
-
const matchedNodes: Element[] = [];
|
|
60
|
-
for (let i = 0; i < result.snapshotLength; i++) {
|
|
61
|
-
const node = result.snapshotItem(i);
|
|
62
|
-
if (node && node.nodeType === 1) {
|
|
63
|
-
matchedNodes.push(node as Element);
|
|
64
|
-
}
|
|
65
|
-
}
|
|
66
|
-
found = elements.filter(el => matchedNodes.some(node => el.node.isEqualNode(node)));
|
|
67
|
-
} catch (e) {
|
|
68
|
-
console.error('Invalid XPath expression:', e);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
break;
|
|
72
|
-
case '-ios predicate string':
|
|
73
|
-
found = elements.filter(el => {
|
|
74
|
-
const predicateLower = value.toLowerCase();
|
|
75
|
-
if (predicateLower.includes('name')) {
|
|
76
|
-
const match = value.match(/name\s*(==|CONTAINS)\s*['"](.*)['"]/i);
|
|
77
|
-
if (match) {
|
|
78
|
-
return match[1] === '==' ? el.name === match[2] : el.name.includes(match[2]);
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
if (predicateLower.includes('label')) {
|
|
82
|
-
const match = value.match(/label\s*(==|CONTAINS)\s*['"](.*)['"]/i);
|
|
83
|
-
if (match) {
|
|
84
|
-
return match[1] === '==' ? el.label === match[2] : el.label.includes(match[2]);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
if (predicateLower.includes('type')) {
|
|
88
|
-
const match = value.match(/type\s*(==|CONTAINS)\s*['"](.*)['"]/i);
|
|
89
|
-
if (match) {
|
|
90
|
-
return match[1] === '==' ? el.type === match[2] : el.type.includes(match[2]);
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
return false;
|
|
94
|
-
});
|
|
95
|
-
break;
|
|
96
|
-
case '-ios class chain':
|
|
97
|
-
const classChainMatch = value.match(/\*\*\/(\w+)(?:\[`(.+?)`\])?/);
|
|
98
|
-
if (classChainMatch) {
|
|
99
|
-
const targetType = classChainMatch[1];
|
|
100
|
-
const predicate = classChainMatch[2];
|
|
101
|
-
found = elements.filter(el => {
|
|
102
|
-
if (el.type !== targetType) return false;
|
|
103
|
-
if (!predicate) return true;
|
|
104
|
-
const nameMatch = predicate.match(/name\s*==\s*['"](.*)['"]/i);
|
|
105
|
-
if (nameMatch) return el.name === nameMatch[1];
|
|
106
|
-
const labelMatch = predicate.match(/label\s*==\s*['"](.*)['"]/i);
|
|
107
|
-
if (labelMatch) return el.label === labelMatch[1];
|
|
108
|
-
return true;
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
break;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
setFoundElements(found);
|
|
115
|
-
if (found.length > 0) {
|
|
116
|
-
setSelectedElement(found[0]);
|
|
117
|
-
setQueryError(null);
|
|
118
|
-
} else {
|
|
119
|
-
setSelectedElement(null);
|
|
120
|
-
setQueryError(`No elements found for ${strategy}: "${value}"`);
|
|
121
|
-
}
|
|
122
|
-
};
|
|
123
|
-
|
|
124
|
-
const copyText = (text: string) => {
|
|
125
|
-
navigator.clipboard.writeText(text);
|
|
126
|
-
setCopiedText(text);
|
|
127
|
-
setTimeout(() => setCopiedText(null), 2000);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
const locators = (): Locator[] => {
|
|
131
|
-
const el = selectedElement();
|
|
132
|
-
return el ? generateLocators(el) : [];
|
|
133
|
-
};
|
|
134
|
-
|
|
135
|
-
const formatXml = (xml: string) => {
|
|
136
|
-
// Simple XML formatting for better readability
|
|
137
|
-
let formatted = '';
|
|
138
|
-
let indent = 0;
|
|
139
|
-
const lines = xml.replace(/></g, '>\n<').split('\n');
|
|
140
|
-
|
|
141
|
-
for (const line of lines) {
|
|
142
|
-
const trimmed = line.trim();
|
|
143
|
-
if (!trimmed) continue;
|
|
144
|
-
|
|
145
|
-
if (trimmed.startsWith('</')) {
|
|
146
|
-
indent = Math.max(0, indent - 1);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
formatted += ' '.repeat(indent) + trimmed + '\n';
|
|
150
|
-
|
|
151
|
-
if (trimmed.startsWith('<') && !trimmed.startsWith('</') && !trimmed.endsWith('/>') && !trimmed.includes('</')) {
|
|
152
|
-
indent++;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return formatted;
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
// Defense-in-depth: render XML as textContent (never HTML)
|
|
160
|
-
createRenderEffect(() => {
|
|
161
|
-
const el = xmlPreRef();
|
|
162
|
-
if (!el) return;
|
|
163
|
-
el.textContent = formatXml(props.interaction?.source || '');
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
return (
|
|
167
|
-
<div class="main-inspector">
|
|
168
|
-
<Show
|
|
169
|
-
when={props.interaction}
|
|
170
|
-
fallback={
|
|
171
|
-
<div class="inspector-empty">
|
|
172
|
-
<div class="inspector-empty-content">
|
|
173
|
-
<span class="inspector-empty-icon">🔍</span>
|
|
174
|
-
<span class="inspector-empty-text">Select an action to inspect</span>
|
|
175
|
-
</div>
|
|
176
|
-
</div>
|
|
177
|
-
}
|
|
178
|
-
>
|
|
179
|
-
{/* Query Tester Section */}
|
|
180
|
-
<div class="query-section">
|
|
181
|
-
<h3 class="section-title">Query Tester</h3>
|
|
182
|
-
<div class="query-row">
|
|
183
|
-
<select
|
|
184
|
-
value={queryStrategy()}
|
|
185
|
-
onChange={(e) => setQueryStrategy(e.currentTarget.value)}
|
|
186
|
-
class="query-select"
|
|
187
|
-
>
|
|
188
|
-
<option value="accessibility id">accessibility id</option>
|
|
189
|
-
<option value="xpath">xpath</option>
|
|
190
|
-
<option value="class name">class name</option>
|
|
191
|
-
<option value="-ios predicate string">-ios predicate string</option>
|
|
192
|
-
<option value="-ios class chain">-ios class chain</option>
|
|
193
|
-
</select>
|
|
194
|
-
<input
|
|
195
|
-
type="text"
|
|
196
|
-
value={queryValue()}
|
|
197
|
-
onInput={(e) => setQueryValue(e.currentTarget.value)}
|
|
198
|
-
onKeyPress={(e) => e.key === 'Enter' && runQuery()}
|
|
199
|
-
placeholder="Enter locator value..."
|
|
200
|
-
class="query-input"
|
|
201
|
-
/>
|
|
202
|
-
<button onClick={runQuery} class="query-btn">
|
|
203
|
-
Find
|
|
204
|
-
</button>
|
|
205
|
-
</div>
|
|
206
|
-
|
|
207
|
-
<Show when={foundElements().length > 0}>
|
|
208
|
-
<div class="query-result success">
|
|
209
|
-
Found {foundElements().length} element(s)
|
|
210
|
-
</div>
|
|
211
|
-
</Show>
|
|
212
|
-
|
|
213
|
-
<Show when={queryError()}>
|
|
214
|
-
<div class="query-result error">
|
|
215
|
-
<span class="error-icon">⚠️</span>
|
|
216
|
-
<span>{queryError()}</span>
|
|
217
|
-
<button class="error-dismiss" onClick={() => setQueryError(null)}>✕</button>
|
|
218
|
-
</div>
|
|
219
|
-
</Show>
|
|
220
|
-
|
|
221
|
-
{/* Element Details */}
|
|
222
|
-
<Show when={selectedElement()}>
|
|
223
|
-
<div class="element-panel">
|
|
224
|
-
<div class="element-details">
|
|
225
|
-
<div class="element-attr">
|
|
226
|
-
<span class="attr-name">Type:</span>
|
|
227
|
-
<span class="attr-value">{selectedElement()!.type}</span>
|
|
228
|
-
</div>
|
|
229
|
-
<Show when={selectedElement()!.name}>
|
|
230
|
-
<div class="element-attr">
|
|
231
|
-
<span class="attr-name">Name:</span>
|
|
232
|
-
<span class="attr-value">{selectedElement()!.name}</span>
|
|
233
|
-
</div>
|
|
234
|
-
</Show>
|
|
235
|
-
<Show when={selectedElement()!.label}>
|
|
236
|
-
<div class="element-attr">
|
|
237
|
-
<span class="attr-name">Label:</span>
|
|
238
|
-
<span class="attr-value">{selectedElement()!.label}</span>
|
|
239
|
-
</div>
|
|
240
|
-
</Show>
|
|
241
|
-
<div class="element-attr">
|
|
242
|
-
<span class="attr-name">Bounds:</span>
|
|
243
|
-
<span class="attr-value">
|
|
244
|
-
x={selectedElement()!.x}, y={selectedElement()!.y},
|
|
245
|
-
w={selectedElement()!.width}, h={selectedElement()!.height}
|
|
246
|
-
</span>
|
|
247
|
-
</div>
|
|
248
|
-
</div>
|
|
249
|
-
|
|
250
|
-
<div class="locators-section">
|
|
251
|
-
<h4>Locators (click to copy)</h4>
|
|
252
|
-
<div class="locators-list">
|
|
253
|
-
<For each={locators()}>
|
|
254
|
-
{(locator) => (
|
|
255
|
-
<div
|
|
256
|
-
class="locator-row"
|
|
257
|
-
classList={{ copied: copiedText() === locator.value }}
|
|
258
|
-
onClick={() => copyText(locator.value)}
|
|
259
|
-
>
|
|
260
|
-
<span class="locator-strategy">{locator.strategy}</span>
|
|
261
|
-
<span class="locator-value">{locator.value}</span>
|
|
262
|
-
<Show when={copiedText() === locator.value}>
|
|
263
|
-
<span class="copied-badge">Copied!</span>
|
|
264
|
-
</Show>
|
|
265
|
-
</div>
|
|
266
|
-
)}
|
|
267
|
-
</For>
|
|
268
|
-
</div>
|
|
269
|
-
</div>
|
|
270
|
-
</div>
|
|
271
|
-
</Show>
|
|
272
|
-
</div>
|
|
273
|
-
|
|
274
|
-
{/* Content Area: Screenshot Left, XML Right */}
|
|
275
|
-
<div class="content-area">
|
|
276
|
-
{/* Screenshot Section */}
|
|
277
|
-
<div class="screenshot-section">
|
|
278
|
-
<Show when={props.interaction!.screenshot}>
|
|
279
|
-
<img
|
|
280
|
-
src={`data:image/png;base64,${props.interaction!.screenshot}`}
|
|
281
|
-
alt="Screenshot"
|
|
282
|
-
class="screenshot-image"
|
|
283
|
-
/>
|
|
284
|
-
</Show>
|
|
285
|
-
</div>
|
|
286
|
-
|
|
287
|
-
{/* XML Source Section */}
|
|
288
|
-
<div class="xml-section">
|
|
289
|
-
<h3 class="section-title">XML Source</h3>
|
|
290
|
-
<pre
|
|
291
|
-
ref={(el) => {
|
|
292
|
-
setXmlPreRef(el);
|
|
293
|
-
onCleanup(() => {
|
|
294
|
-
setXmlPreRef(undefined);
|
|
295
|
-
});
|
|
296
|
-
}}
|
|
297
|
-
class="xml-source"
|
|
298
|
-
/>
|
|
299
|
-
</div>
|
|
300
|
-
</div>
|
|
301
|
-
</Show>
|
|
302
|
-
</div>
|
|
303
|
-
);
|
|
304
|
-
};
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
.stats {
|
|
2
|
-
display: flex;
|
|
3
|
-
gap: var(--spacing-6);
|
|
4
|
-
padding: var(--spacing-5);
|
|
5
|
-
background: var(--color-bg-secondary);
|
|
6
|
-
border-radius: var(--radius-xl);
|
|
7
|
-
margin-bottom: var(--spacing-6);
|
|
8
|
-
box-shadow: var(--shadow-sm);
|
|
9
|
-
border: 1px solid var(--color-border);
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
.stat {
|
|
13
|
-
text-align: center;
|
|
14
|
-
flex: 1;
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
.stat-value {
|
|
18
|
-
font-size: var(--font-size-3xl);
|
|
19
|
-
font-weight: var(--font-weight-bold);
|
|
20
|
-
color: var(--color-accent-primary);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
.stat-label {
|
|
24
|
-
font-size: var(--font-size-sm);
|
|
25
|
-
color: var(--color-text-tertiary);
|
|
26
|
-
margin-top: var(--spacing-2);
|
|
27
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
.timeline {
|
|
2
|
-
display: flex;
|
|
3
|
-
flex-direction: column;
|
|
4
|
-
gap: var(--spacing-4);
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
.empty-state {
|
|
8
|
-
text-align: center;
|
|
9
|
-
padding: var(--spacing-12) var(--spacing-6);
|
|
10
|
-
background: var(--color-bg-secondary);
|
|
11
|
-
border-radius: var(--radius-2xl);
|
|
12
|
-
border: 1px dashed var(--color-border);
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
.empty-icon {
|
|
16
|
-
font-size: 3rem;
|
|
17
|
-
margin-bottom: var(--spacing-4);
|
|
18
|
-
opacity: 0.4;
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
.empty-title {
|
|
22
|
-
font-size: var(--font-size-lg);
|
|
23
|
-
font-weight: var(--font-weight-semibold);
|
|
24
|
-
color: var(--color-text-primary);
|
|
25
|
-
margin-bottom: var(--spacing-2);
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
.empty-text {
|
|
29
|
-
color: var(--color-text-tertiary);
|
|
30
|
-
font-size: var(--font-size-sm);
|
|
31
|
-
}
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
import { type Component, For, Show } from 'solid-js';
|
|
2
|
-
import type { Interaction } from '../types';
|
|
3
|
-
import { InteractionCard } from './InteractionCard';
|
|
4
|
-
import './Timeline.css';
|
|
5
|
-
|
|
6
|
-
type TimelineProps = {
|
|
7
|
-
interactions: Interaction[];
|
|
8
|
-
onInspect?: (interaction: Interaction) => void;
|
|
9
|
-
};
|
|
10
|
-
|
|
11
|
-
export const Timeline: Component<TimelineProps> = (props) => {
|
|
12
|
-
return (
|
|
13
|
-
<div class="timeline">
|
|
14
|
-
<Show
|
|
15
|
-
when={props.interactions.length > 0}
|
|
16
|
-
fallback={
|
|
17
|
-
<div class="empty-state">
|
|
18
|
-
<div class="empty-icon">📱</div>
|
|
19
|
-
<div class="empty-title">No interactions recorded yet</div>
|
|
20
|
-
<div class="empty-text">
|
|
21
|
-
Connect Appium Inspector to port 4724 and start interacting
|
|
22
|
-
</div>
|
|
23
|
-
</div>
|
|
24
|
-
}
|
|
25
|
-
>
|
|
26
|
-
<For each={props.interactions}>
|
|
27
|
-
{(interaction) => (
|
|
28
|
-
<InteractionCard
|
|
29
|
-
interaction={interaction}
|
|
30
|
-
onInspect={() => props.onInspect?.(interaction)}
|
|
31
|
-
/>
|
|
32
|
-
)}
|
|
33
|
-
</For>
|
|
34
|
-
</Show>
|
|
35
|
-
</div>
|
|
36
|
-
);
|
|
37
|
-
};
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { createSignal, onCleanup, createEffect } from 'solid-js';
|
|
2
|
-
import type { Interaction } from '../types';
|
|
3
|
-
import { api } from '../services/api';
|
|
4
|
-
|
|
5
|
-
export function useInteractions() {
|
|
6
|
-
const [interactions, setInteractions] = createSignal<Interaction[]>([]);
|
|
7
|
-
const [loading, setLoading] = createSignal(true);
|
|
8
|
-
|
|
9
|
-
// Load initial history
|
|
10
|
-
async function loadHistory() {
|
|
11
|
-
setLoading(true);
|
|
12
|
-
try {
|
|
13
|
-
const history = await api.getHistory();
|
|
14
|
-
setInteractions(history);
|
|
15
|
-
} catch (error) {
|
|
16
|
-
console.error('Failed to load history:', error);
|
|
17
|
-
} finally {
|
|
18
|
-
setLoading(false);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
// Connect to SSE stream
|
|
23
|
-
createEffect(() => {
|
|
24
|
-
const unsubscribe = api.connectToStream((event) => {
|
|
25
|
-
if (event.type === 'init') {
|
|
26
|
-
setInteractions(event.data);
|
|
27
|
-
setLoading(false);
|
|
28
|
-
} else if (event.type === 'interaction') {
|
|
29
|
-
setInteractions(prev => {
|
|
30
|
-
const existing = prev.findIndex(i => i.id === event.data.id);
|
|
31
|
-
if (existing >= 0) {
|
|
32
|
-
// Update existing
|
|
33
|
-
const updated = [...prev];
|
|
34
|
-
updated[existing] = event.data;
|
|
35
|
-
return updated;
|
|
36
|
-
} else {
|
|
37
|
-
// Add new
|
|
38
|
-
return [...prev, event.data];
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
} else if (event.type === 'clear') {
|
|
42
|
-
setInteractions([]);
|
|
43
|
-
}
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
onCleanup(unsubscribe);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
// Note: loadHistory() is not called here because the SSE stream
|
|
50
|
-
// already sends the complete initial history via the 'init' event
|
|
51
|
-
|
|
52
|
-
async function clearHistory() {
|
|
53
|
-
await api.clearHistory();
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
async function refresh() {
|
|
57
|
-
await loadHistory();
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
const actions = interactions().filter(i => i.screenshot);
|
|
61
|
-
const stats = () => ({
|
|
62
|
-
total: interactions().length,
|
|
63
|
-
actions: actions.length,
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
return {
|
|
67
|
-
interactions,
|
|
68
|
-
loading,
|
|
69
|
-
stats,
|
|
70
|
-
clearHistory,
|
|
71
|
-
refresh,
|
|
72
|
-
};
|
|
73
|
-
}
|
package/src/ui/src/index.tsx
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import type { Interaction } from '../types';
|
|
2
|
-
|
|
3
|
-
class ApiClient {
|
|
4
|
-
async getHistory(): Promise<Interaction[]> {
|
|
5
|
-
const response = await fetch('/_recorder/api/history');
|
|
6
|
-
|
|
7
|
-
if(!response.ok) {
|
|
8
|
-
throw new Error(`Failed to fetch history: ${response.statusText}`);
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
return response.json();
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async clearHistory(): Promise<void> {
|
|
15
|
-
const response = await fetch('/_recorder/api/clear', { method: 'POST' });
|
|
16
|
-
if(!response.ok) {
|
|
17
|
-
throw new Error(`Failed to clear history: ${response.statusText}`);
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
connectToStream(onEvent: (event: any) => void): () => void {
|
|
22
|
-
const eventSource = new EventSource('/_recorder/api/stream');
|
|
23
|
-
|
|
24
|
-
eventSource.onmessage = (event) => {
|
|
25
|
-
try {
|
|
26
|
-
const data = JSON.parse(event.data);
|
|
27
|
-
onEvent(data);
|
|
28
|
-
} catch (error) {
|
|
29
|
-
console.error('Failed to parse SSE event:', error);
|
|
30
|
-
}
|
|
31
|
-
};
|
|
32
|
-
|
|
33
|
-
eventSource.onerror = (error) => {
|
|
34
|
-
console.error('SSE connection error:', error);
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
return () => eventSource.close();
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export const api = new ApiClient();
|
|
@@ -1,126 +0,0 @@
|
|
|
1
|
-
:root {
|
|
2
|
-
/* Colors - Claude-inspired warm light theme */
|
|
3
|
-
--color-bg-primary: #F9F7F3;
|
|
4
|
-
--color-bg-secondary: #FFFFFF;
|
|
5
|
-
--color-bg-tertiary: #F0EDE6;
|
|
6
|
-
--color-bg-elevated: #FFFFFF;
|
|
7
|
-
|
|
8
|
-
--color-accent-primary: #C4682A;
|
|
9
|
-
--color-accent-secondary: #B05C26;
|
|
10
|
-
--color-accent-success: #2D7D4F;
|
|
11
|
-
--color-accent-warning: #B8862E;
|
|
12
|
-
--color-accent-error: #C4453A;
|
|
13
|
-
|
|
14
|
-
--color-text-primary: #1B1B18;
|
|
15
|
-
--color-text-secondary: #6B6B64;
|
|
16
|
-
--color-text-tertiary: #9B9B94;
|
|
17
|
-
|
|
18
|
-
--color-border: #E3DFD7;
|
|
19
|
-
--color-border-hover: #C4682A;
|
|
20
|
-
|
|
21
|
-
/* Typography */
|
|
22
|
-
--font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
|
23
|
-
--font-mono: 'SF Mono', 'Menlo', 'Monaco', 'Consolas', monospace;
|
|
24
|
-
--font-size-xs: 0.75rem;
|
|
25
|
-
--font-size-sm: 0.8125rem;
|
|
26
|
-
--font-size-base: 0.9375rem;
|
|
27
|
-
--font-size-lg: 1.0625rem;
|
|
28
|
-
--font-size-xl: 1.25rem;
|
|
29
|
-
--font-size-2xl: 1.5rem;
|
|
30
|
-
--font-size-3xl: 2rem;
|
|
31
|
-
|
|
32
|
-
--font-weight-normal: 400;
|
|
33
|
-
--font-weight-medium: 500;
|
|
34
|
-
--font-weight-semibold: 600;
|
|
35
|
-
--font-weight-bold: 700;
|
|
36
|
-
|
|
37
|
-
/* Spacing */
|
|
38
|
-
--spacing-1: 0.25rem;
|
|
39
|
-
--spacing-2: 0.5rem;
|
|
40
|
-
--spacing-3: 0.75rem;
|
|
41
|
-
--spacing-4: 1rem;
|
|
42
|
-
--spacing-5: 1.25rem;
|
|
43
|
-
--spacing-6: 1.5rem;
|
|
44
|
-
--spacing-8: 2rem;
|
|
45
|
-
--spacing-10: 2.5rem;
|
|
46
|
-
--spacing-12: 3rem;
|
|
47
|
-
|
|
48
|
-
/* Border radius */
|
|
49
|
-
--radius-sm: 0.375rem;
|
|
50
|
-
--radius-md: 0.5rem;
|
|
51
|
-
--radius-lg: 0.75rem;
|
|
52
|
-
--radius-xl: 1rem;
|
|
53
|
-
--radius-2xl: 1.25rem;
|
|
54
|
-
--radius-full: 9999px;
|
|
55
|
-
|
|
56
|
-
/* Shadows - soft and subtle */
|
|
57
|
-
--shadow-xs: 0 1px 2px rgba(0, 0, 0, 0.03);
|
|
58
|
-
--shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 2px rgba(0, 0, 0, 0.03);
|
|
59
|
-
--shadow-md: 0 4px 8px -2px rgba(0, 0, 0, 0.06), 0 2px 4px -2px rgba(0, 0, 0, 0.04);
|
|
60
|
-
--shadow-lg: 0 12px 28px -6px rgba(0, 0, 0, 0.08), 0 4px 12px -4px rgba(0, 0, 0, 0.04);
|
|
61
|
-
|
|
62
|
-
/* Transitions */
|
|
63
|
-
--transition-fast: 150ms ease;
|
|
64
|
-
--transition-base: 200ms ease;
|
|
65
|
-
--transition-slow: 300ms ease;
|
|
66
|
-
|
|
67
|
-
/* Z-index */
|
|
68
|
-
--z-dropdown: 1000;
|
|
69
|
-
--z-sticky: 1020;
|
|
70
|
-
--z-modal: 1050;
|
|
71
|
-
--z-tooltip: 1080;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
* {
|
|
75
|
-
box-sizing: border-box;
|
|
76
|
-
margin: 0;
|
|
77
|
-
padding: 0;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
body {
|
|
81
|
-
font-family: var(--font-family);
|
|
82
|
-
background: var(--color-bg-primary);
|
|
83
|
-
color: var(--color-text-primary);
|
|
84
|
-
line-height: 1.6;
|
|
85
|
-
-webkit-font-smoothing: antialiased;
|
|
86
|
-
-moz-osx-font-smoothing: grayscale;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
#root {
|
|
90
|
-
min-height: 100vh;
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/* Scrollbar styling */
|
|
94
|
-
::-webkit-scrollbar {
|
|
95
|
-
width: 6px;
|
|
96
|
-
height: 6px;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
::-webkit-scrollbar-track {
|
|
100
|
-
background: transparent;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
::-webkit-scrollbar-thumb {
|
|
104
|
-
background: var(--color-border);
|
|
105
|
-
border-radius: var(--radius-full);
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
::-webkit-scrollbar-thumb:hover {
|
|
109
|
-
background: var(--color-text-tertiary);
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
/* Utility classes */
|
|
113
|
-
.text-xs { font-size: var(--font-size-xs); }
|
|
114
|
-
.text-sm { font-size: var(--font-size-sm); }
|
|
115
|
-
.text-base { font-size: var(--font-size-base); }
|
|
116
|
-
.text-lg { font-size: var(--font-size-lg); }
|
|
117
|
-
.text-xl { font-size: var(--font-size-xl); }
|
|
118
|
-
.text-2xl { font-size: var(--font-size-2xl); }
|
|
119
|
-
.text-3xl { font-size: var(--font-size-3xl); }
|
|
120
|
-
|
|
121
|
-
.font-normal { font-weight: var(--font-weight-normal); }
|
|
122
|
-
.font-medium { font-weight: var(--font-weight-medium); }
|
|
123
|
-
.font-semibold { font-weight: var(--font-weight-semibold); }
|
|
124
|
-
.font-bold { font-weight: var(--font-weight-bold); }
|
|
125
|
-
|
|
126
|
-
.transition { transition: all var(--transition-base); }
|
package/src/ui/src/types.ts
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
export type Interaction = {
|
|
2
|
-
id: number;
|
|
3
|
-
timestamp: string;
|
|
4
|
-
method: string;
|
|
5
|
-
path: string;
|
|
6
|
-
body?: any;
|
|
7
|
-
screenshot?: string;
|
|
8
|
-
source?: string;
|
|
9
|
-
elementInfo?: {
|
|
10
|
-
using: string;
|
|
11
|
-
value: string;
|
|
12
|
-
};
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type ParsedElement = {
|
|
16
|
-
type: string;
|
|
17
|
-
name: string;
|
|
18
|
-
label: string;
|
|
19
|
-
value: string;
|
|
20
|
-
enabled: boolean;
|
|
21
|
-
visible: boolean;
|
|
22
|
-
accessible: boolean;
|
|
23
|
-
x: number;
|
|
24
|
-
y: number;
|
|
25
|
-
width: number;
|
|
26
|
-
height: number;
|
|
27
|
-
xpath: string;
|
|
28
|
-
node: Element;
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
export type Locator = {
|
|
32
|
-
strategy: string;
|
|
33
|
-
value: string;
|
|
34
|
-
};
|