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.
Files changed (73) hide show
  1. package/dist/index.js +32422 -0
  2. package/dist/ui/assets/index-CUcJNRfB.css +1 -0
  3. package/dist/ui/assets/index-Cl_X3tPj.js +4 -0
  4. package/{src → dist}/ui/index.html +2 -1
  5. package/package.json +10 -3
  6. package/bun.lock +0 -731
  7. package/src/cli/arg-parser.ts +0 -311
  8. package/src/cli/commands/drive.ts +0 -147
  9. package/src/cli/commands/index.ts +0 -54
  10. package/src/cli/commands/proxy.ts +0 -41
  11. package/src/cli/commands/screen.ts +0 -73
  12. package/src/cli/commands/selectors.ts +0 -42
  13. package/src/cli/commands/session.ts +0 -64
  14. package/src/cli/commands/types.ts +0 -11
  15. package/src/cli/index.ts +0 -158
  16. package/src/cli/prompts.ts +0 -64
  17. package/src/cli/response.ts +0 -44
  18. package/src/core/appium/client.ts +0 -248
  19. package/src/core/index.ts +0 -5
  20. package/src/core/selectors/generate-candidates.ts +0 -155
  21. package/src/core/selectors/score-candidates.ts +0 -184
  22. package/src/core/types.ts +0 -79
  23. package/src/core/xml/parse-source.ts +0 -197
  24. package/src/index.ts +0 -7
  25. package/src/server/appium-client.ts +0 -24
  26. package/src/server/index.ts +0 -6
  27. package/src/server/interaction-recorder.ts +0 -74
  28. package/src/server/proxy-middleware.ts +0 -68
  29. package/src/server/routes.ts +0 -64
  30. package/src/server/server.ts +0 -43
  31. package/src/server/types.ts +0 -34
  32. package/src/ui/bun.lock +0 -311
  33. package/src/ui/package.json +0 -20
  34. package/src/ui/src/App.css +0 -12
  35. package/src/ui/src/App.tsx +0 -41
  36. package/src/ui/src/components/ActionCarousel.css +0 -128
  37. package/src/ui/src/components/ActionCarousel.tsx +0 -92
  38. package/src/ui/src/components/Inspector.css +0 -314
  39. package/src/ui/src/components/Inspector.tsx +0 -265
  40. package/src/ui/src/components/InteractionCard.css +0 -159
  41. package/src/ui/src/components/InteractionCard.tsx +0 -60
  42. package/src/ui/src/components/MainInspector.css +0 -304
  43. package/src/ui/src/components/MainInspector.tsx +0 -304
  44. package/src/ui/src/components/Stats.css +0 -27
  45. package/src/ui/src/components/Timeline.css +0 -31
  46. package/src/ui/src/components/Timeline.tsx +0 -37
  47. package/src/ui/src/hooks/useInteractions.ts +0 -73
  48. package/src/ui/src/index.tsx +0 -11
  49. package/src/ui/src/services/api.ts +0 -41
  50. package/src/ui/src/styles/tokens.css +0 -126
  51. package/src/ui/src/types.ts +0 -34
  52. package/src/ui/src/utils/__tests__/locators.test.ts +0 -304
  53. package/src/ui/src/utils/__tests__/xml-parser.test.ts +0 -326
  54. package/src/ui/src/utils/locators.ts +0 -14
  55. package/src/ui/src/utils/xml-parser.ts +0 -45
  56. package/src/ui/tsconfig.json +0 -34
  57. package/src/ui/tsconfig.node.json +0 -11
  58. package/src/ui/vite.config.ts +0 -22
  59. package/tests/cli/arg-parser.test.ts +0 -397
  60. package/tests/cli/drive-commands.test.ts +0 -151
  61. package/tests/cli/selectors-best.test.ts +0 -42
  62. package/tests/cli/session-commands.test.ts +0 -53
  63. package/tests/core/selector-candidates.test.ts +0 -83
  64. package/tests/core/selector-scoring.test.ts +0 -75
  65. package/tests/core/xml-parser.test.ts +0 -56
  66. package/tests/server/appium-client.test.ts +0 -229
  67. package/tests/server/interaction-recorder.test.ts +0 -377
  68. package/tests/server/proxy-middleware.test.ts +0 -343
  69. package/tests/server/routes.test.ts +0 -305
  70. package/tsconfig.json +0 -26
  71. package/vitest.config.ts +0 -16
  72. package/vitest.ui.config.ts +0 -15
  73. package/workflow.gif +0 -0
@@ -1,92 +0,0 @@
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
- };
@@ -1,314 +0,0 @@
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
- }
@@ -1,265 +0,0 @@
1
- import { type Component, createSignal, Show, For } from 'solid-js';
2
- import { Dialog } from '@kobalte/core/dialog';
3
- import type { Interaction } from '../types';
4
- import { parseXmlSource } from '../utils/xml-parser';
5
- import { generateLocators } from '../utils/locators';
6
- import type { ParsedElement, Locator } from '../types';
7
- import './Inspector.css';
8
-
9
- type InspectorProps = {
10
- interaction: Interaction | null;
11
- open: boolean;
12
- onClose: () => void;
13
- };
14
-
15
- export const Inspector: Component<InspectorProps> = (props) => {
16
- const [selectedElement, setSelectedElement] = createSignal<ParsedElement | null>(null);
17
- const [queryStrategy, setQueryStrategy] = createSignal('accessibility id');
18
- const [queryValue, setQueryValue] = createSignal('');
19
- const [foundElements, setFoundElements] = createSignal<ParsedElement[]>([]);
20
- const [showSource, setShowSource] = createSignal(false);
21
-
22
- const parsedElements = () => {
23
- if (!props.interaction?.source) return [];
24
- return parseXmlSource(props.interaction.source);
25
- };
26
-
27
- const runQuery = () => {
28
- const strategy = queryStrategy();
29
- const value = queryValue().trim();
30
-
31
- if (!value) return;
32
-
33
- const elements = parsedElements();
34
- let found: ParsedElement[] = [];
35
-
36
- switch (strategy) {
37
- case 'accessibility id':
38
- found = elements.filter(el => el.name === value || el.label === value);
39
- break;
40
- case 'class name':
41
- found = elements.filter(el => el.type === value);
42
- break;
43
- case 'xpath':
44
- // Properly evaluate XPath against the XML source
45
- if (props.interaction?.source) {
46
- try {
47
- const parser = new DOMParser();
48
- const doc = parser.parseFromString(props.interaction.source, 'text/xml');
49
- const result = doc.evaluate(value, doc, null, XPathResult.ORDERED_NODE_SNAPSHOT_TYPE, null);
50
- const matchedNodes: Element[] = [];
51
- for (let i = 0; i < result.snapshotLength; i++) {
52
- const node = result.snapshotItem(i);
53
- if (node && node.nodeType === 1) {
54
- matchedNodes.push(node as Element);
55
- }
56
- }
57
- // Match found nodes back to parsed elements
58
- found = elements.filter(el => matchedNodes.some(node => el.node.isEqualNode(node)));
59
- } catch (e) {
60
- console.error('Invalid XPath expression:', e);
61
- }
62
- }
63
- break;
64
- case '-ios predicate string':
65
- // iOS predicate string matching (simplified attribute matching)
66
- found = elements.filter(el => {
67
- // Parse simple predicates like "name == 'value'" or "label CONTAINS 'text'"
68
- const predicateLower = value.toLowerCase();
69
- if (predicateLower.includes('name')) {
70
- const match = value.match(/name\s*(==|CONTAINS)\s*['"](.*)['"]/i);
71
- if (match) {
72
- return match[1] === '=='
73
- ? el.name === match[2]
74
- : el.name.includes(match[2]);
75
- }
76
- }
77
- if (predicateLower.includes('label')) {
78
- const match = value.match(/label\s*(==|CONTAINS)\s*['"](.*)['"]/i);
79
- if (match) {
80
- return match[1] === '=='
81
- ? el.label === match[2]
82
- : el.label.includes(match[2]);
83
- }
84
- }
85
- if (predicateLower.includes('type')) {
86
- const match = value.match(/type\s*(==|CONTAINS)\s*['"](.*)['"]/i);
87
- if (match) {
88
- return match[1] === '=='
89
- ? el.type === match[2]
90
- : el.type.includes(match[2]);
91
- }
92
- }
93
- return false;
94
- });
95
- break;
96
- case '-ios class chain':
97
- // iOS class chain matching (simplified type/index matching)
98
- // Format: **/XCUIElementTypeButton[`name == "buttonName"`]
99
- const classChainMatch = value.match(/\*\*\/(\w+)(?:\[`(.+?)`\])?/);
100
- if (classChainMatch) {
101
- const targetType = classChainMatch[1];
102
- const predicate = classChainMatch[2];
103
- found = elements.filter(el => {
104
- if (el.type !== targetType) return false;
105
- if (!predicate) return true;
106
- // Simple predicate matching within class chain
107
- const nameMatch = predicate.match(/name\s*==\s*['"](.*)['"]/i);
108
- if (nameMatch) return el.name === nameMatch[1];
109
- const labelMatch = predicate.match(/label\s*==\s*['"](.*)['"]/i);
110
- if (labelMatch) return el.label === labelMatch[1];
111
- return true;
112
- });
113
- }
114
- break;
115
- }
116
-
117
- setFoundElements(found);
118
- if (found.length > 0) {
119
- setSelectedElement(found[0]);
120
- }
121
- };
122
-
123
- const copyText = (text: string) => {
124
- navigator.clipboard.writeText(text);
125
- };
126
-
127
- const locators = (): Locator[] => {
128
- const el = selectedElement();
129
- return el ? generateLocators(el) : [];
130
- };
131
-
132
- const resetState = () => {
133
- setSelectedElement(null);
134
- setQueryStrategy('accessibility id');
135
- setQueryValue('');
136
- setFoundElements([]);
137
- setShowSource(false);
138
- };
139
-
140
- const handleClose = () => {
141
- resetState();
142
- props.onClose();
143
- };
144
-
145
- return (
146
- <Dialog open={props.open} onOpenChange={(open) => !open && handleClose()}>
147
- <Dialog.Portal>
148
- <Dialog.Overlay class="inspector-overlay" />
149
- <Dialog.Content class="inspector-modal">
150
- <Dialog.CloseButton class="inspector-close">✕</Dialog.CloseButton>
151
-
152
- <div class="inspector-panel">
153
- <div class="inspector-left">
154
- <Show when={props.interaction?.screenshot}>
155
- <img
156
- src={`data:image/png;base64,${props.interaction!.screenshot}`}
157
- alt="Screenshot"
158
- class="inspector-screenshot"
159
- />
160
- </Show>
161
- </div>
162
-
163
- <div class="inspector-right">
164
- <div class="inspector-section">
165
- <h3>Query Tester</h3>
166
- <div class="query-tester">
167
- <div class="query-row">
168
- <select
169
- value={queryStrategy()}
170
- onChange={(e) => setQueryStrategy(e.currentTarget.value)}
171
- class="query-select"
172
- >
173
- <option value="accessibility id">accessibility id</option>
174
- <option value="xpath">xpath</option>
175
- <option value="class name">class name</option>
176
- <option value="-ios predicate string">-ios predicate string</option>
177
- <option value="-ios class chain">-ios class chain</option>
178
- </select>
179
- <input
180
- type="text"
181
- value={queryValue()}
182
- onInput={(e) => setQueryValue(e.currentTarget.value)}
183
- onKeyPress={(e) => e.key === 'Enter' && runQuery()}
184
- placeholder="Enter locator value..."
185
- class="query-input"
186
- />
187
- <button onClick={runQuery} class="query-btn">
188
- Find
189
- </button>
190
- </div>
191
-
192
- <Show when={foundElements().length > 0}>
193
- <div class="query-result success">
194
- Found {foundElements().length} element(s)
195
- </div>
196
- </Show>
197
- </div>
198
- </div>
199
- <Show when={selectedElement()}>
200
- <div class="inspector-section">
201
- <h3>Element Details</h3>
202
- <div class="element-details">
203
- <div class="element-attr">
204
- <span class="attr-name">Type:</span>
205
- <span class="attr-value">{selectedElement()!.type}</span>
206
- </div>
207
- <Show when={selectedElement()!.name}>
208
- <div class="element-attr">
209
- <span class="attr-name">Name:</span>
210
- <span class="attr-value">{selectedElement()!.name}</span>
211
- </div>
212
- </Show>
213
- <Show when={selectedElement()!.label}>
214
- <div class="element-attr">
215
- <span class="attr-name">Label:</span>
216
- <span class="attr-value">{selectedElement()!.label}</span>
217
- </div>
218
- </Show>
219
- <div class="element-attr">
220
- <span class="attr-name">Bounds:</span>
221
- <span class="attr-value">
222
- x={selectedElement()!.x}, y={selectedElement()!.y},
223
- w={selectedElement()!.width}, h={selectedElement()!.height}
224
- </span>
225
- </div>
226
- </div>
227
- </div>
228
-
229
- <div class="inspector-section">
230
- <h3>Locators (click to copy)</h3>
231
- <div class="locators-list">
232
- <For each={locators()}>
233
- {(locator) => (
234
- <div class="locator-row" onClick={() => copyText(locator.value)}>
235
- <span class="locator-strategy">{locator.strategy}</span>
236
- <span class="locator-value">{locator.value}</span>
237
- </div>
238
- )}
239
- </For>
240
- </div>
241
- </div>
242
- </Show>
243
-
244
- {/* XML Source Toggle */}
245
- <Show when={props.interaction?.source}>
246
- <div class="inspector-section">
247
- <button
248
- class="source-toggle-btn"
249
- onClick={() => setShowSource(!showSource())}
250
- >
251
- {showSource() ? 'Hide' : 'Show'} XML Source
252
- </button>
253
- <Show when={showSource()}>
254
- <pre class="xml-source">{props.interaction!.source}</pre>
255
- </Show>
256
- </div>
257
- </Show>
258
- </div>
259
- </div>
260
- </Dialog.Content>
261
- </Dialog.Portal>
262
- </Dialog>
263
- );
264
- };
265
-