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,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
|
-
|