@astur-mobile/cli 0.1.0-beta.0
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/assets/brand/Astur_A_balck.png +0 -0
- package/assets/brand/Astur_A_white.png +0 -0
- package/assets/brand/astur-logo-dark.png +0 -0
- package/assets/brand/astur-logo-light.png +0 -0
- package/assets/brand/astur-mark-light.png +0 -0
- package/assets/brand/astur-mark-transparent.png +0 -0
- package/dist/index.d.ts +52 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +917 -0
- package/dist/index.js.map +1 -0
- package/dist/inspectorServer.d.ts +194 -0
- package/dist/inspectorServer.d.ts.map +1 -0
- package/dist/inspectorServer.js +3487 -0
- package/dist/inspectorServer.js.map +1 -0
- package/dist/inspectorUi.d.ts +17 -0
- package/dist/inspectorUi.d.ts.map +1 -0
- package/dist/inspectorUi.js +1181 -0
- package/dist/inspectorUi.js.map +1 -0
- package/dist/scaffold.d.ts +67 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +470 -0
- package/dist/scaffold.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,1181 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
import { constants } from 'node:fs';
|
|
3
|
+
import { access } from 'node:fs/promises';
|
|
4
|
+
import { mkdtemp, readFile, writeFile } from 'node:fs/promises';
|
|
5
|
+
import { tmpdir } from 'node:os';
|
|
6
|
+
import { join, resolve } from 'node:path';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
export async function launchInspectorUi(model) {
|
|
9
|
+
const payload = await buildPayload(model);
|
|
10
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'astur-inspector-'));
|
|
11
|
+
const filePath = join(tempDir, 'index.html');
|
|
12
|
+
const html = renderInspectorHtml(payload);
|
|
13
|
+
await writeFile(filePath, html, 'utf8');
|
|
14
|
+
const opened = openExternal(filePath);
|
|
15
|
+
return {
|
|
16
|
+
filePath,
|
|
17
|
+
opened
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function buildPayload(model) {
|
|
21
|
+
const nodes = flattenTree(model.tree);
|
|
22
|
+
const initialSelection = pickInitialSelection(nodes);
|
|
23
|
+
return {
|
|
24
|
+
generatedAt: new Date().toISOString(),
|
|
25
|
+
device: {
|
|
26
|
+
id: model.device.id,
|
|
27
|
+
name: model.device.name,
|
|
28
|
+
platform: model.device.platform,
|
|
29
|
+
kind: model.device.kind,
|
|
30
|
+
state: model.device.state
|
|
31
|
+
},
|
|
32
|
+
app: {
|
|
33
|
+
launched: model.launched,
|
|
34
|
+
warning: model.launchWarning
|
|
35
|
+
},
|
|
36
|
+
tree: {
|
|
37
|
+
nodes: model.treeNodeCount,
|
|
38
|
+
visibleNodes: model.visibleNodeCount
|
|
39
|
+
},
|
|
40
|
+
nodes,
|
|
41
|
+
suggestions: model.suggestions.map((suggestion) => ({
|
|
42
|
+
code: suggestion.code,
|
|
43
|
+
score: suggestion.score
|
|
44
|
+
})),
|
|
45
|
+
initialSelectionUid: initialSelection?.uid,
|
|
46
|
+
viewport: estimateViewport(nodes),
|
|
47
|
+
screenshotDataUri: model.screenshotDataUri,
|
|
48
|
+
logoDataUri: await readAsturLogoDataUri()
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
function flattenTree(root) {
|
|
52
|
+
const nodes = [];
|
|
53
|
+
const visit = (node, depth, uid, parentUid) => {
|
|
54
|
+
nodes.push({
|
|
55
|
+
uid,
|
|
56
|
+
parentUid,
|
|
57
|
+
depth,
|
|
58
|
+
title: node.label ?? node.text ?? node.id ?? node.type,
|
|
59
|
+
type: node.type,
|
|
60
|
+
id: node.id,
|
|
61
|
+
label: node.label,
|
|
62
|
+
text: node.text,
|
|
63
|
+
value: node.value,
|
|
64
|
+
visible: node.visible,
|
|
65
|
+
enabled: node.enabled,
|
|
66
|
+
bounds: node.bounds
|
|
67
|
+
});
|
|
68
|
+
for (const [index, child] of node.children.entries()) {
|
|
69
|
+
visit(child, depth + 1, `${uid}.${index}`, uid);
|
|
70
|
+
}
|
|
71
|
+
};
|
|
72
|
+
visit(root, 0, '0');
|
|
73
|
+
return nodes;
|
|
74
|
+
}
|
|
75
|
+
function pickInitialSelection(nodes) {
|
|
76
|
+
return nodes.find((node) => {
|
|
77
|
+
return node.visible
|
|
78
|
+
&& node.enabled
|
|
79
|
+
&& !node.type.endsWith('.root')
|
|
80
|
+
&& Boolean(node.id || node.label || node.text);
|
|
81
|
+
}) ?? nodes[0];
|
|
82
|
+
}
|
|
83
|
+
function estimateViewport(nodes) {
|
|
84
|
+
const visible = nodes.filter((node) => node.visible && node.bounds.width > 0 && node.bounds.height > 0);
|
|
85
|
+
const right = Math.max(0, ...visible.map((node) => node.bounds.x + node.bounds.width));
|
|
86
|
+
const bottom = Math.max(0, ...visible.map((node) => node.bounds.y + node.bounds.height));
|
|
87
|
+
return {
|
|
88
|
+
width: Math.max(1, right),
|
|
89
|
+
height: Math.max(1, bottom)
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
async function readAsturLogoDataUri() {
|
|
93
|
+
const candidates = [
|
|
94
|
+
fileURLToPath(new URL('../assets/brand/astur-logo-dark.png', import.meta.url)),
|
|
95
|
+
fileURLToPath(new URL('../assets/brand/astur-logo-light.png', import.meta.url)),
|
|
96
|
+
resolve(process.cwd(), 'packages/cli/assets/brand/astur-logo-dark.png'),
|
|
97
|
+
resolve(process.cwd(), 'packages/cli/assets/brand/astur-logo-light.png')
|
|
98
|
+
];
|
|
99
|
+
for (const candidate of candidates) {
|
|
100
|
+
if (!(await exists(candidate))) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
const file = await readFile(candidate);
|
|
104
|
+
return `data:image/png;base64,${file.toString('base64')}`;
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
async function exists(path) {
|
|
109
|
+
try {
|
|
110
|
+
await access(path, constants.F_OK);
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function openExternal(target) {
|
|
118
|
+
try {
|
|
119
|
+
if (process.platform === 'darwin') {
|
|
120
|
+
spawn('open', [target], { detached: true, stdio: 'ignore' }).unref();
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
if (process.platform === 'win32') {
|
|
124
|
+
spawn('cmd', ['/c', 'start', '', target], { detached: true, stdio: 'ignore' }).unref();
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
spawn('xdg-open', [target], { detached: true, stdio: 'ignore' }).unref();
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function renderInspectorHtml(payload) {
|
|
135
|
+
const payloadJson = JSON.stringify(payload).replace(/</g, '\\u003c');
|
|
136
|
+
return `<!doctype html>
|
|
137
|
+
<html lang="en">
|
|
138
|
+
<head>
|
|
139
|
+
<meta charset="utf-8" />
|
|
140
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
141
|
+
<title>Astur Inspector</title>
|
|
142
|
+
<style>
|
|
143
|
+
:root {
|
|
144
|
+
--bg: #040914;
|
|
145
|
+
--bg-soft: #0b162b;
|
|
146
|
+
--panel: rgba(12, 27, 49, 0.84);
|
|
147
|
+
--panel-strong: rgba(15, 34, 62, 0.96);
|
|
148
|
+
--line: rgba(104, 180, 255, 0.26);
|
|
149
|
+
--text: #e9f2ff;
|
|
150
|
+
--muted: #91a7cc;
|
|
151
|
+
--accent: #38bdf8;
|
|
152
|
+
--accent-strong: #0ea5e9;
|
|
153
|
+
--good: #22c55e;
|
|
154
|
+
--warn: #fb923c;
|
|
155
|
+
--danger: #ef4444;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
* {
|
|
159
|
+
box-sizing: border-box;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
body {
|
|
163
|
+
margin: 0;
|
|
164
|
+
min-height: 100vh;
|
|
165
|
+
color: var(--text);
|
|
166
|
+
background:
|
|
167
|
+
radial-gradient(1100px 600px at 20% -10%, rgba(37, 99, 235, 0.24), transparent 70%),
|
|
168
|
+
radial-gradient(900px 500px at 95% 5%, rgba(14, 165, 233, 0.18), transparent 70%),
|
|
169
|
+
linear-gradient(150deg, #050b18, #030814 54%, #071326);
|
|
170
|
+
font-family: 'Space Grotesk', 'Avenir Next', 'Segoe UI Variable', 'Segoe UI', sans-serif;
|
|
171
|
+
padding: 14px;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
.toolbar {
|
|
175
|
+
height: 62px;
|
|
176
|
+
border: 1px solid var(--line);
|
|
177
|
+
border-radius: 14px;
|
|
178
|
+
background: linear-gradient(180deg, rgba(11, 24, 43, 0.9), rgba(8, 19, 36, 0.9));
|
|
179
|
+
display: flex;
|
|
180
|
+
align-items: center;
|
|
181
|
+
justify-content: space-between;
|
|
182
|
+
padding: 0 14px;
|
|
183
|
+
backdrop-filter: blur(8px);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
.brand {
|
|
187
|
+
display: flex;
|
|
188
|
+
align-items: center;
|
|
189
|
+
gap: 10px;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.brand-logo {
|
|
193
|
+
width: 96px;
|
|
194
|
+
height: 34px;
|
|
195
|
+
border-radius: 0;
|
|
196
|
+
object-fit: contain;
|
|
197
|
+
background: transparent;
|
|
198
|
+
border: 0;
|
|
199
|
+
padding: 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
.brand-fallback {
|
|
203
|
+
width: 32px;
|
|
204
|
+
height: 32px;
|
|
205
|
+
border-radius: 10px;
|
|
206
|
+
display: grid;
|
|
207
|
+
place-items: center;
|
|
208
|
+
background: linear-gradient(160deg, #0ea5e9, #1d4ed8);
|
|
209
|
+
font-weight: 700;
|
|
210
|
+
color: white;
|
|
211
|
+
border: 1px solid rgba(255, 255, 255, 0.35);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
.brand-title {
|
|
215
|
+
font-size: 15px;
|
|
216
|
+
font-weight: 700;
|
|
217
|
+
letter-spacing: 0.3px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.toolbar-right {
|
|
221
|
+
display: flex;
|
|
222
|
+
align-items: center;
|
|
223
|
+
gap: 10px;
|
|
224
|
+
color: var(--muted);
|
|
225
|
+
font-size: 13px;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.badge {
|
|
229
|
+
border-radius: 999px;
|
|
230
|
+
border: 1px solid var(--line);
|
|
231
|
+
padding: 6px 10px;
|
|
232
|
+
background: rgba(15, 32, 56, 0.7);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.badge.live {
|
|
236
|
+
color: #a7f3d0;
|
|
237
|
+
border-color: rgba(16, 185, 129, 0.36);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
.layout {
|
|
241
|
+
margin-top: 12px;
|
|
242
|
+
display: grid;
|
|
243
|
+
grid-template-columns: 280px 420px 320px 1fr;
|
|
244
|
+
gap: 12px;
|
|
245
|
+
min-height: calc(100vh - 100px);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.panel {
|
|
249
|
+
border: 1px solid var(--line);
|
|
250
|
+
border-radius: 14px;
|
|
251
|
+
background: linear-gradient(180deg, var(--panel-strong), var(--panel));
|
|
252
|
+
backdrop-filter: blur(8px);
|
|
253
|
+
overflow: hidden;
|
|
254
|
+
min-height: 0;
|
|
255
|
+
display: flex;
|
|
256
|
+
flex-direction: column;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.panel-header {
|
|
260
|
+
padding: 12px 14px;
|
|
261
|
+
border-bottom: 1px solid rgba(133, 187, 255, 0.18);
|
|
262
|
+
font-size: 12px;
|
|
263
|
+
text-transform: uppercase;
|
|
264
|
+
letter-spacing: 0.8px;
|
|
265
|
+
color: #b7cbeb;
|
|
266
|
+
display: flex;
|
|
267
|
+
align-items: center;
|
|
268
|
+
justify-content: space-between;
|
|
269
|
+
gap: 8px;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
.panel-body {
|
|
273
|
+
padding: 12px;
|
|
274
|
+
overflow: auto;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
.nav-strip {
|
|
278
|
+
display: grid;
|
|
279
|
+
grid-template-columns: 42px 1fr;
|
|
280
|
+
height: 100%;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
.nav-icons {
|
|
284
|
+
border-right: 1px solid rgba(133, 187, 255, 0.16);
|
|
285
|
+
padding: 12px 8px;
|
|
286
|
+
display: grid;
|
|
287
|
+
gap: 10px;
|
|
288
|
+
align-content: start;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.icon-btn {
|
|
292
|
+
height: 34px;
|
|
293
|
+
border-radius: 10px;
|
|
294
|
+
border: 1px solid rgba(133, 187, 255, 0.24);
|
|
295
|
+
display: grid;
|
|
296
|
+
place-items: center;
|
|
297
|
+
color: #cae0ff;
|
|
298
|
+
font-size: 11px;
|
|
299
|
+
background: rgba(17, 36, 64, 0.8);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
.icon-btn.active {
|
|
303
|
+
background: linear-gradient(160deg, rgba(29, 78, 216, 0.75), rgba(14, 165, 233, 0.35));
|
|
304
|
+
border-color: rgba(125, 211, 252, 0.62);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
.inspector-main {
|
|
308
|
+
display: flex;
|
|
309
|
+
flex-direction: column;
|
|
310
|
+
min-height: 0;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
.hint {
|
|
314
|
+
color: var(--muted);
|
|
315
|
+
font-size: 13px;
|
|
316
|
+
line-height: 1.5;
|
|
317
|
+
margin-bottom: 14px;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
.best-card {
|
|
321
|
+
border: 1px solid rgba(56, 189, 248, 0.35);
|
|
322
|
+
border-radius: 12px;
|
|
323
|
+
background: linear-gradient(180deg, rgba(18, 45, 78, 0.74), rgba(14, 31, 57, 0.74));
|
|
324
|
+
padding: 10px;
|
|
325
|
+
margin-bottom: 10px;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
.best-card .label {
|
|
329
|
+
font-size: 11px;
|
|
330
|
+
color: #93c5fd;
|
|
331
|
+
text-transform: uppercase;
|
|
332
|
+
letter-spacing: 0.8px;
|
|
333
|
+
margin-bottom: 6px;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
.mono {
|
|
337
|
+
font-family: 'IBM Plex Mono', 'JetBrains Mono', 'SFMono-Regular', Menlo, monospace;
|
|
338
|
+
font-size: 12px;
|
|
339
|
+
color: #dbeafe;
|
|
340
|
+
word-break: break-word;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.score {
|
|
344
|
+
margin-top: 8px;
|
|
345
|
+
display: inline-flex;
|
|
346
|
+
align-items: center;
|
|
347
|
+
gap: 6px;
|
|
348
|
+
border: 1px solid rgba(34, 197, 94, 0.4);
|
|
349
|
+
color: #bbf7d0;
|
|
350
|
+
border-radius: 999px;
|
|
351
|
+
padding: 2px 8px;
|
|
352
|
+
font-size: 11px;
|
|
353
|
+
background: rgba(22, 101, 52, 0.22);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
.alternative-list {
|
|
357
|
+
display: grid;
|
|
358
|
+
gap: 8px;
|
|
359
|
+
margin-bottom: 14px;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
.alternative-item {
|
|
363
|
+
border: 1px solid rgba(133, 187, 255, 0.24);
|
|
364
|
+
border-radius: 10px;
|
|
365
|
+
padding: 8px;
|
|
366
|
+
background: rgba(15, 34, 59, 0.7);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
.details-grid {
|
|
370
|
+
border: 1px solid rgba(133, 187, 255, 0.2);
|
|
371
|
+
border-radius: 10px;
|
|
372
|
+
overflow: hidden;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
.detail-row {
|
|
376
|
+
display: grid;
|
|
377
|
+
grid-template-columns: 112px 1fr;
|
|
378
|
+
border-bottom: 1px solid rgba(133, 187, 255, 0.15);
|
|
379
|
+
font-size: 12px;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
.detail-row:last-child {
|
|
383
|
+
border-bottom: none;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
.detail-row > div {
|
|
387
|
+
padding: 7px 9px;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
.detail-key {
|
|
391
|
+
color: #a0b9de;
|
|
392
|
+
background: rgba(13, 27, 47, 0.8);
|
|
393
|
+
border-right: 1px solid rgba(133, 187, 255, 0.15);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
.mirror-panel {
|
|
397
|
+
justify-content: space-between;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
.mirror-wrap {
|
|
401
|
+
height: 100%;
|
|
402
|
+
display: grid;
|
|
403
|
+
place-items: center;
|
|
404
|
+
padding: 16px;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
.phone-shell {
|
|
408
|
+
width: min(100%, 340px);
|
|
409
|
+
height: min(100%, 720px);
|
|
410
|
+
border-radius: 34px;
|
|
411
|
+
border: 1px solid rgba(133, 187, 255, 0.24);
|
|
412
|
+
padding: 10px;
|
|
413
|
+
background: linear-gradient(180deg, rgba(11, 23, 41, 0.96), rgba(5, 13, 24, 0.96));
|
|
414
|
+
position: relative;
|
|
415
|
+
box-shadow: 0 18px 70px rgba(2, 8, 23, 0.8);
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
.phone-notch {
|
|
419
|
+
position: absolute;
|
|
420
|
+
top: 8px;
|
|
421
|
+
left: 50%;
|
|
422
|
+
width: 92px;
|
|
423
|
+
height: 16px;
|
|
424
|
+
transform: translateX(-50%);
|
|
425
|
+
border-radius: 999px;
|
|
426
|
+
background: rgba(3, 10, 20, 0.95);
|
|
427
|
+
border: 1px solid rgba(141, 201, 255, 0.2);
|
|
428
|
+
z-index: 3;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
.mirror-stage {
|
|
432
|
+
width: 100%;
|
|
433
|
+
height: 100%;
|
|
434
|
+
border-radius: 24px;
|
|
435
|
+
overflow: hidden;
|
|
436
|
+
border: 1px solid rgba(133, 187, 255, 0.18);
|
|
437
|
+
background: radial-gradient(circle at 50% 8%, rgba(29, 78, 216, 0.4), rgba(3, 10, 22, 0.95) 70%);
|
|
438
|
+
position: relative;
|
|
439
|
+
display: grid;
|
|
440
|
+
place-items: center;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
.mirror-image {
|
|
444
|
+
max-width: 100%;
|
|
445
|
+
max-height: 100%;
|
|
446
|
+
object-fit: contain;
|
|
447
|
+
display: none;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
.mirror-empty {
|
|
451
|
+
text-align: center;
|
|
452
|
+
max-width: 250px;
|
|
453
|
+
color: #c6d9f5;
|
|
454
|
+
font-size: 13px;
|
|
455
|
+
line-height: 1.6;
|
|
456
|
+
padding: 12px;
|
|
457
|
+
border-radius: 12px;
|
|
458
|
+
background: rgba(8, 21, 40, 0.62);
|
|
459
|
+
border: 1px solid rgba(133, 187, 255, 0.2);
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
.highlight {
|
|
463
|
+
position: absolute;
|
|
464
|
+
border: 2px solid rgba(125, 211, 252, 0.96);
|
|
465
|
+
border-radius: 10px;
|
|
466
|
+
box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2), 0 0 30px rgba(56, 189, 248, 0.45);
|
|
467
|
+
pointer-events: none;
|
|
468
|
+
display: none;
|
|
469
|
+
animation: pulse 1.6s ease-in-out infinite;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
@keyframes pulse {
|
|
473
|
+
0% { box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2), 0 0 22px rgba(56, 189, 248, 0.4); }
|
|
474
|
+
50% { box-shadow: 0 0 0 4px rgba(56, 189, 248, 0.26), 0 0 30px rgba(56, 189, 248, 0.62); }
|
|
475
|
+
100% { box-shadow: 0 0 0 3px rgba(56, 189, 248, 0.2), 0 0 22px rgba(56, 189, 248, 0.4); }
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
.tree-search {
|
|
479
|
+
width: 100%;
|
|
480
|
+
border-radius: 9px;
|
|
481
|
+
border: 1px solid rgba(133, 187, 255, 0.26);
|
|
482
|
+
background: rgba(7, 18, 35, 0.9);
|
|
483
|
+
color: var(--text);
|
|
484
|
+
padding: 9px 10px;
|
|
485
|
+
margin-bottom: 10px;
|
|
486
|
+
font-size: 13px;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
.tree-list {
|
|
490
|
+
display: grid;
|
|
491
|
+
gap: 4px;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.tree-item {
|
|
495
|
+
border: 1px solid transparent;
|
|
496
|
+
border-radius: 8px;
|
|
497
|
+
padding: 6px 8px;
|
|
498
|
+
cursor: pointer;
|
|
499
|
+
color: #d5e5ff;
|
|
500
|
+
font-size: 12px;
|
|
501
|
+
background: rgba(10, 22, 40, 0.5);
|
|
502
|
+
transition: 120ms ease;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
.tree-item:hover {
|
|
506
|
+
border-color: rgba(125, 211, 252, 0.34);
|
|
507
|
+
background: rgba(17, 38, 66, 0.85);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
.tree-item.active {
|
|
511
|
+
border-color: rgba(125, 211, 252, 0.68);
|
|
512
|
+
background: linear-gradient(150deg, rgba(37, 99, 235, 0.5), rgba(14, 165, 233, 0.38));
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
.code-head {
|
|
516
|
+
display: flex;
|
|
517
|
+
align-items: center;
|
|
518
|
+
gap: 6px;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
.pill-btn {
|
|
522
|
+
border: 1px solid rgba(133, 187, 255, 0.24);
|
|
523
|
+
background: rgba(13, 29, 50, 0.8);
|
|
524
|
+
color: #dbeafe;
|
|
525
|
+
padding: 6px 9px;
|
|
526
|
+
border-radius: 999px;
|
|
527
|
+
font-size: 12px;
|
|
528
|
+
cursor: pointer;
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
.pill-btn.active {
|
|
532
|
+
border-color: rgba(125, 211, 252, 0.6);
|
|
533
|
+
background: rgba(14, 165, 233, 0.2);
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
.code-block {
|
|
537
|
+
border: 1px solid rgba(133, 187, 255, 0.22);
|
|
538
|
+
border-radius: 10px;
|
|
539
|
+
background: rgba(4, 11, 22, 0.92);
|
|
540
|
+
color: #d8ebff;
|
|
541
|
+
padding: 12px;
|
|
542
|
+
min-height: 220px;
|
|
543
|
+
max-height: 380px;
|
|
544
|
+
overflow: auto;
|
|
545
|
+
white-space: pre;
|
|
546
|
+
line-height: 1.6;
|
|
547
|
+
font-size: 12px;
|
|
548
|
+
margin-bottom: 10px;
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
.action-row {
|
|
552
|
+
display: flex;
|
|
553
|
+
gap: 8px;
|
|
554
|
+
flex-wrap: wrap;
|
|
555
|
+
margin-bottom: 10px;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
.recording-table {
|
|
559
|
+
width: 100%;
|
|
560
|
+
border-collapse: collapse;
|
|
561
|
+
font-size: 12px;
|
|
562
|
+
border: 1px solid rgba(133, 187, 255, 0.2);
|
|
563
|
+
border-radius: 10px;
|
|
564
|
+
overflow: hidden;
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
.recording-table th,
|
|
568
|
+
.recording-table td {
|
|
569
|
+
border-bottom: 1px solid rgba(133, 187, 255, 0.16);
|
|
570
|
+
padding: 7px 8px;
|
|
571
|
+
text-align: left;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
.recording-table th {
|
|
575
|
+
color: #9fbae1;
|
|
576
|
+
background: rgba(13, 26, 46, 0.9);
|
|
577
|
+
font-weight: 600;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
.recording-table tr:last-child td {
|
|
581
|
+
border-bottom: none;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
.status {
|
|
585
|
+
color: #a5c3ea;
|
|
586
|
+
font-size: 12px;
|
|
587
|
+
margin-top: 8px;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
@media (max-width: 1680px) {
|
|
591
|
+
.layout {
|
|
592
|
+
grid-template-columns: 260px 1fr 320px;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
.tree-panel {
|
|
596
|
+
grid-column: 1 / 3;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
@media (max-width: 1180px) {
|
|
601
|
+
body {
|
|
602
|
+
padding: 10px;
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
.layout {
|
|
606
|
+
grid-template-columns: 1fr;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
.tree-panel {
|
|
610
|
+
grid-column: auto;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
</style>
|
|
614
|
+
</head>
|
|
615
|
+
<body>
|
|
616
|
+
<header class="toolbar">
|
|
617
|
+
<div class="brand">
|
|
618
|
+
${payload.logoDataUri
|
|
619
|
+
? '<img class="brand-logo" src="' + payload.logoDataUri + '" alt="Astur logo" />'
|
|
620
|
+
: '<div class="brand-fallback">A</div>'}
|
|
621
|
+
<div>
|
|
622
|
+
<div class="brand-title">Astur Inspector</div>
|
|
623
|
+
<div style="font-size: 11px; color: var(--muted);">Playwright-style native codegen UI</div>
|
|
624
|
+
</div>
|
|
625
|
+
</div>
|
|
626
|
+
<div class="toolbar-right">
|
|
627
|
+
<div class="badge">${payload.device.name} (${payload.device.platform})</div>
|
|
628
|
+
<div class="badge live">Live tree: ${payload.tree.visibleNodes}/${payload.tree.nodes}</div>
|
|
629
|
+
<div class="badge" id="recordBadge">Recording: ON</div>
|
|
630
|
+
</div>
|
|
631
|
+
</header>
|
|
632
|
+
|
|
633
|
+
<main class="layout">
|
|
634
|
+
<section class="panel">
|
|
635
|
+
<div class="nav-strip">
|
|
636
|
+
<div class="nav-icons">
|
|
637
|
+
<div class="icon-btn active">INS</div>
|
|
638
|
+
<div class="icon-btn">REC</div>
|
|
639
|
+
<div class="icon-btn">SES</div>
|
|
640
|
+
<div class="icon-btn">DEV</div>
|
|
641
|
+
<div class="icon-btn">SET</div>
|
|
642
|
+
</div>
|
|
643
|
+
<div class="inspector-main">
|
|
644
|
+
<div class="panel-header">Inspector</div>
|
|
645
|
+
<div class="panel-body">
|
|
646
|
+
<div class="hint">
|
|
647
|
+
Tap on the mirror or pick a node in the tree. Locators and generated
|
|
648
|
+
Astur code stay in sync with the same runtime selector semantics.
|
|
649
|
+
</div>
|
|
650
|
+
|
|
651
|
+
<div class="best-card">
|
|
652
|
+
<div class="label">Automatic Best Locator</div>
|
|
653
|
+
<div id="bestLocator" class="mono"></div>
|
|
654
|
+
<div id="bestScore" class="score"></div>
|
|
655
|
+
</div>
|
|
656
|
+
|
|
657
|
+
<div style="font-size: 11px; color: #96b7df; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 7px;">Alternatives</div>
|
|
658
|
+
<div id="alternativeList" class="alternative-list"></div>
|
|
659
|
+
|
|
660
|
+
<div style="font-size: 11px; color: #96b7df; text-transform: uppercase; letter-spacing: 0.8px; margin-bottom: 7px;">Element Details</div>
|
|
661
|
+
<div id="detailsGrid" class="details-grid"></div>
|
|
662
|
+
</div>
|
|
663
|
+
</div>
|
|
664
|
+
</div>
|
|
665
|
+
</section>
|
|
666
|
+
|
|
667
|
+
<section class="panel mirror-panel">
|
|
668
|
+
<div class="panel-header">
|
|
669
|
+
<span>Device Mirror</span>
|
|
670
|
+
<span style="color: var(--muted); font-size: 11px;">${payload.device.id}</span>
|
|
671
|
+
</div>
|
|
672
|
+
<div class="mirror-wrap">
|
|
673
|
+
<div class="phone-shell">
|
|
674
|
+
<div class="phone-notch"></div>
|
|
675
|
+
<div class="mirror-stage" id="mirrorStage">
|
|
676
|
+
<img id="mirrorImage" class="mirror-image" alt="Device screenshot" />
|
|
677
|
+
<div id="mirrorEmpty" class="mirror-empty">
|
|
678
|
+
No screenshot was captured for this session.<br />
|
|
679
|
+
Use a running app target to show the live frame.
|
|
680
|
+
</div>
|
|
681
|
+
<div id="highlightBox" class="highlight"></div>
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
</div>
|
|
685
|
+
</section>
|
|
686
|
+
|
|
687
|
+
<section class="panel tree-panel">
|
|
688
|
+
<div class="panel-header">
|
|
689
|
+
<span>UI Tree</span>
|
|
690
|
+
<span style="font-size: 11px; color: var(--muted);">${payload.generatedAt}</span>
|
|
691
|
+
</div>
|
|
692
|
+
<div class="panel-body">
|
|
693
|
+
<input id="treeSearch" class="tree-search" placeholder="Search element text, id, or type" />
|
|
694
|
+
<div id="treeList" class="tree-list"></div>
|
|
695
|
+
</div>
|
|
696
|
+
</section>
|
|
697
|
+
|
|
698
|
+
<section class="panel code-panel">
|
|
699
|
+
<div class="panel-header">
|
|
700
|
+
<div class="code-head">
|
|
701
|
+
<button class="pill-btn active" data-lang="typescript">TypeScript</button>
|
|
702
|
+
<button class="pill-btn" data-lang="javascript">JavaScript</button>
|
|
703
|
+
<button class="pill-btn" data-lang="python">Python</button>
|
|
704
|
+
</div>
|
|
705
|
+
<button class="pill-btn" id="exportCodeBtn">Export Code</button>
|
|
706
|
+
</div>
|
|
707
|
+
<div class="panel-body">
|
|
708
|
+
<pre id="codeBlock" class="code-block"></pre>
|
|
709
|
+
|
|
710
|
+
<div class="action-row">
|
|
711
|
+
<button class="pill-btn" id="recordToggleBtn">Pause Recording</button>
|
|
712
|
+
<button class="pill-btn" id="addTapBtn">Add Tap</button>
|
|
713
|
+
<button class="pill-btn" id="addFillBtn">Add Fill</button>
|
|
714
|
+
<button class="pill-btn" id="addExpectBtn">Add Expect Visible</button>
|
|
715
|
+
<button class="pill-btn" id="clearStepsBtn">Clear</button>
|
|
716
|
+
</div>
|
|
717
|
+
|
|
718
|
+
<table class="recording-table">
|
|
719
|
+
<thead>
|
|
720
|
+
<tr>
|
|
721
|
+
<th style="width: 42px;">#</th>
|
|
722
|
+
<th style="width: 90px;">Action</th>
|
|
723
|
+
<th>Locator</th>
|
|
724
|
+
</tr>
|
|
725
|
+
</thead>
|
|
726
|
+
<tbody id="stepsBody"></tbody>
|
|
727
|
+
</table>
|
|
728
|
+
<div id="statusText" class="status"></div>
|
|
729
|
+
</div>
|
|
730
|
+
</section>
|
|
731
|
+
</main>
|
|
732
|
+
|
|
733
|
+
<script id="astur-bootstrap" type="application/json">${payloadJson}</script>
|
|
734
|
+
<script>
|
|
735
|
+
(() => {
|
|
736
|
+
const payload = JSON.parse(document.getElementById('astur-bootstrap').textContent || '{}');
|
|
737
|
+
const state = {
|
|
738
|
+
language: 'typescript',
|
|
739
|
+
selectedUid: payload.initialSelectionUid,
|
|
740
|
+
steps: [],
|
|
741
|
+
recording: true
|
|
742
|
+
};
|
|
743
|
+
|
|
744
|
+
const treeSearch = document.getElementById('treeSearch');
|
|
745
|
+
const treeList = document.getElementById('treeList');
|
|
746
|
+
const bestLocator = document.getElementById('bestLocator');
|
|
747
|
+
const bestScore = document.getElementById('bestScore');
|
|
748
|
+
const alternativeList = document.getElementById('alternativeList');
|
|
749
|
+
const detailsGrid = document.getElementById('detailsGrid');
|
|
750
|
+
const codeBlock = document.getElementById('codeBlock');
|
|
751
|
+
const stepsBody = document.getElementById('stepsBody');
|
|
752
|
+
const statusText = document.getElementById('statusText');
|
|
753
|
+
const mirrorImage = document.getElementById('mirrorImage');
|
|
754
|
+
const mirrorEmpty = document.getElementById('mirrorEmpty');
|
|
755
|
+
const highlightBox = document.getElementById('highlightBox');
|
|
756
|
+
const mirrorStage = document.getElementById('mirrorStage');
|
|
757
|
+
const recordToggleBtn = document.getElementById('recordToggleBtn');
|
|
758
|
+
const recordBadge = document.getElementById('recordBadge');
|
|
759
|
+
|
|
760
|
+
if (payload.screenshotDataUri) {
|
|
761
|
+
mirrorImage.src = payload.screenshotDataUri;
|
|
762
|
+
mirrorImage.style.display = 'block';
|
|
763
|
+
mirrorEmpty.style.display = 'none';
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
mirrorImage.addEventListener('load', () => {
|
|
767
|
+
renderSelection();
|
|
768
|
+
});
|
|
769
|
+
|
|
770
|
+
for (const button of document.querySelectorAll('[data-lang]')) {
|
|
771
|
+
button.addEventListener('click', () => {
|
|
772
|
+
state.language = button.getAttribute('data-lang');
|
|
773
|
+
for (const candidate of document.querySelectorAll('[data-lang]')) {
|
|
774
|
+
candidate.classList.toggle('active', candidate === button);
|
|
775
|
+
}
|
|
776
|
+
renderCode();
|
|
777
|
+
});
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
recordToggleBtn.addEventListener('click', () => {
|
|
781
|
+
state.recording = !state.recording;
|
|
782
|
+
recordToggleBtn.textContent = state.recording ? 'Pause Recording' : 'Resume Recording';
|
|
783
|
+
recordBadge.textContent = state.recording ? 'Recording: ON' : 'Recording: OFF';
|
|
784
|
+
});
|
|
785
|
+
|
|
786
|
+
document.getElementById('addTapBtn').addEventListener('click', () => {
|
|
787
|
+
addStep('Tap');
|
|
788
|
+
});
|
|
789
|
+
|
|
790
|
+
document.getElementById('addFillBtn').addEventListener('click', () => {
|
|
791
|
+
const value = prompt('Fill value', 'qa@example.com');
|
|
792
|
+
if (value === null) {
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
addStep('Fill', value);
|
|
796
|
+
});
|
|
797
|
+
|
|
798
|
+
document.getElementById('addExpectBtn').addEventListener('click', () => {
|
|
799
|
+
addStep('Expect');
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
document.getElementById('clearStepsBtn').addEventListener('click', () => {
|
|
803
|
+
state.steps = [];
|
|
804
|
+
renderCode();
|
|
805
|
+
renderSteps();
|
|
806
|
+
});
|
|
807
|
+
|
|
808
|
+
document.getElementById('exportCodeBtn').addEventListener('click', async () => {
|
|
809
|
+
const code = codeBlock.textContent || '';
|
|
810
|
+
const copied = await copyText(code);
|
|
811
|
+
statusText.textContent = copied
|
|
812
|
+
? 'Code copied to clipboard.'
|
|
813
|
+
: 'Copy failed in this browser; use manual copy from the code panel.';
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
treeSearch.addEventListener('input', () => {
|
|
817
|
+
renderTree();
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
if (!state.selectedUid && payload.nodes.length > 0) {
|
|
821
|
+
state.selectedUid = payload.nodes[0].uid;
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
renderTree();
|
|
825
|
+
renderSelection();
|
|
826
|
+
renderCode();
|
|
827
|
+
renderSteps();
|
|
828
|
+
|
|
829
|
+
function currentNode() {
|
|
830
|
+
return payload.nodes.find((node) => node.uid === state.selectedUid);
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
function inferRole(node) {
|
|
834
|
+
const type = String(node.type || '').toLowerCase();
|
|
835
|
+
if (type.includes('button')) return 'button';
|
|
836
|
+
if (type.includes('checkbox')) return 'checkbox';
|
|
837
|
+
if (type.includes('switch')) return 'switch';
|
|
838
|
+
if (type.includes('radio')) return 'radio';
|
|
839
|
+
if (type.includes('edittext') || type.includes('textfield') || type.includes('textinput')) return 'textbox';
|
|
840
|
+
if (type.includes('text')) return 'text';
|
|
841
|
+
return undefined;
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function escapeSingle(value) {
|
|
845
|
+
return String(value).replaceAll('\\\\', '\\\\\\\\').replaceAll("'", "\\\\'");
|
|
846
|
+
}
|
|
847
|
+
|
|
848
|
+
function scoreTag(score) {
|
|
849
|
+
return Math.round(Number(score || 0) * 100);
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
function locatorCandidates(node) {
|
|
853
|
+
if (!node) {
|
|
854
|
+
return [];
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
const candidates = [];
|
|
858
|
+
const role = inferRole(node);
|
|
859
|
+
const name = node.label || node.text || node.value;
|
|
860
|
+
|
|
861
|
+
if (node.id) {
|
|
862
|
+
candidates.push({ code: "device.getByTestId('" + escapeSingle(node.id) + "')", score: 0.99 });
|
|
863
|
+
candidates.push({ code: "device.getById('" + escapeSingle(node.id) + "')", score: 0.96 });
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
if (role && name) {
|
|
867
|
+
candidates.push({
|
|
868
|
+
code: "device.getByRole('" + escapeSingle(role) + "', { name: '" + escapeSingle(name) + "' })",
|
|
869
|
+
score: 0.92
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (node.label) {
|
|
874
|
+
candidates.push({ code: "device.getByLabel('" + escapeSingle(node.label) + "')", score: 0.89 });
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
if (node.text) {
|
|
878
|
+
candidates.push({ code: "device.getByText('" + escapeSingle(node.text) + "')", score: 0.84 });
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
if (node.type) {
|
|
882
|
+
candidates.push({ code: "device.getByType('" + escapeSingle(node.type) + "')", score: 0.58 });
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
const unique = [];
|
|
886
|
+
const seen = new Set();
|
|
887
|
+
for (const candidate of candidates) {
|
|
888
|
+
if (!seen.has(candidate.code)) {
|
|
889
|
+
unique.push(candidate);
|
|
890
|
+
seen.add(candidate.code);
|
|
891
|
+
}
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
return unique;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
function selectionCandidates(node) {
|
|
898
|
+
if (!node) {
|
|
899
|
+
return [];
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
if (node.uid === payload.initialSelectionUid && Array.isArray(payload.suggestions) && payload.suggestions.length > 0) {
|
|
903
|
+
return payload.suggestions;
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
return locatorCandidates(node);
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
function renderTree() {
|
|
910
|
+
const query = String(treeSearch.value || '').trim().toLowerCase();
|
|
911
|
+
const rows = payload.nodes.filter((node) => {
|
|
912
|
+
if (!query) {
|
|
913
|
+
return true;
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
return [node.title, node.id, node.label, node.text, node.type]
|
|
917
|
+
.filter(Boolean)
|
|
918
|
+
.some((value) => String(value).toLowerCase().includes(query));
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
treeList.innerHTML = '';
|
|
922
|
+
for (const node of rows) {
|
|
923
|
+
const item = document.createElement('button');
|
|
924
|
+
item.type = 'button';
|
|
925
|
+
item.className = 'tree-item' + (node.uid === state.selectedUid ? ' active' : '');
|
|
926
|
+
item.style.paddingLeft = (8 + node.depth * 13) + 'px';
|
|
927
|
+
item.textContent = node.title;
|
|
928
|
+
item.title = node.type;
|
|
929
|
+
item.addEventListener('click', () => {
|
|
930
|
+
state.selectedUid = node.uid;
|
|
931
|
+
renderTree();
|
|
932
|
+
renderSelection();
|
|
933
|
+
renderCode();
|
|
934
|
+
});
|
|
935
|
+
treeList.appendChild(item);
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
function renderSelection() {
|
|
940
|
+
const node = currentNode();
|
|
941
|
+
const candidates = selectionCandidates(node);
|
|
942
|
+
const best = candidates[0];
|
|
943
|
+
|
|
944
|
+
bestLocator.textContent = best ? best.code : 'No locator available for selected node';
|
|
945
|
+
bestScore.textContent = best ? 'Score ' + scoreTag(best.score) : 'Score 0';
|
|
946
|
+
|
|
947
|
+
alternativeList.innerHTML = '';
|
|
948
|
+
for (const candidate of candidates.slice(1, 5)) {
|
|
949
|
+
const row = document.createElement('div');
|
|
950
|
+
row.className = 'alternative-item';
|
|
951
|
+
row.innerHTML = '<div class="mono">' + sanitize(candidate.code) + '</div>'
|
|
952
|
+
+ '<div style="margin-top:6px;color:#9ab8de;font-size:11px;">Score ' + scoreTag(candidate.score) + '</div>';
|
|
953
|
+
alternativeList.appendChild(row);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
if (alternativeList.children.length === 0) {
|
|
957
|
+
const empty = document.createElement('div');
|
|
958
|
+
empty.className = 'alternative-item';
|
|
959
|
+
empty.textContent = 'No alternative locators for this node yet.';
|
|
960
|
+
alternativeList.appendChild(empty);
|
|
961
|
+
}
|
|
962
|
+
|
|
963
|
+
detailsGrid.innerHTML = '';
|
|
964
|
+
const rows = [
|
|
965
|
+
['Type', node?.type ?? '-'],
|
|
966
|
+
['Text', node?.text ?? '-'],
|
|
967
|
+
['Label', node?.label ?? '-'],
|
|
968
|
+
['Resource id', node?.id ?? '-'],
|
|
969
|
+
['Enabled', String(node?.enabled ?? false)],
|
|
970
|
+
['Visible', String(node?.visible ?? false)],
|
|
971
|
+
['Bounds', node ? formatBounds(node.bounds) : '-']
|
|
972
|
+
];
|
|
973
|
+
|
|
974
|
+
for (const [key, value] of rows) {
|
|
975
|
+
const row = document.createElement('div');
|
|
976
|
+
row.className = 'detail-row';
|
|
977
|
+
row.innerHTML = '<div class="detail-key">' + sanitize(key) + '</div><div>' + sanitize(value) + '</div>';
|
|
978
|
+
detailsGrid.appendChild(row);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
updateHighlight(node);
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
function formatBounds(bounds) {
|
|
985
|
+
if (!bounds) {
|
|
986
|
+
return '-';
|
|
987
|
+
}
|
|
988
|
+
|
|
989
|
+
const right = bounds.x + bounds.width;
|
|
990
|
+
const bottom = bounds.y + bounds.height;
|
|
991
|
+
return '[' + bounds.x + ',' + bounds.y + '][' + right + ',' + bottom + ']';
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
function updateHighlight(node) {
|
|
995
|
+
if (!node || !payload.viewport || !payload.screenshotDataUri) {
|
|
996
|
+
highlightBox.style.display = 'none';
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
const viewportWidth = Number(payload.viewport.width || 1);
|
|
1001
|
+
const viewportHeight = Number(payload.viewport.height || 1);
|
|
1002
|
+
if (viewportWidth <= 0 || viewportHeight <= 0) {
|
|
1003
|
+
highlightBox.style.display = 'none';
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
const imageWidth = mirrorImage.clientWidth;
|
|
1008
|
+
const imageHeight = mirrorImage.clientHeight;
|
|
1009
|
+
if (!imageWidth || !imageHeight) {
|
|
1010
|
+
highlightBox.style.display = 'none';
|
|
1011
|
+
return;
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
const stageWidth = mirrorStage.clientWidth;
|
|
1015
|
+
const stageHeight = mirrorStage.clientHeight;
|
|
1016
|
+
const offsetX = (stageWidth - imageWidth) / 2;
|
|
1017
|
+
const offsetY = (stageHeight - imageHeight) / 2;
|
|
1018
|
+
|
|
1019
|
+
const scaleX = imageWidth / viewportWidth;
|
|
1020
|
+
const scaleY = imageHeight / viewportHeight;
|
|
1021
|
+
|
|
1022
|
+
highlightBox.style.display = 'block';
|
|
1023
|
+
highlightBox.style.left = (offsetX + node.bounds.x * scaleX) + 'px';
|
|
1024
|
+
highlightBox.style.top = (offsetY + node.bounds.y * scaleY) + 'px';
|
|
1025
|
+
highlightBox.style.width = Math.max(2, node.bounds.width * scaleX) + 'px';
|
|
1026
|
+
highlightBox.style.height = Math.max(2, node.bounds.height * scaleY) + 'px';
|
|
1027
|
+
}
|
|
1028
|
+
|
|
1029
|
+
function addStep(action, value) {
|
|
1030
|
+
if (!state.recording) {
|
|
1031
|
+
statusText.textContent = 'Recording is paused.';
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
const node = currentNode();
|
|
1036
|
+
const candidate = selectionCandidates(node)[0];
|
|
1037
|
+
if (!candidate) {
|
|
1038
|
+
statusText.textContent = 'No locator available for this node.';
|
|
1039
|
+
return;
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
state.steps.push({
|
|
1043
|
+
action,
|
|
1044
|
+
locator: candidate.code,
|
|
1045
|
+
value: value || undefined
|
|
1046
|
+
});
|
|
1047
|
+
|
|
1048
|
+
renderSteps();
|
|
1049
|
+
renderCode();
|
|
1050
|
+
statusText.textContent = action + ' step added.';
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
function renderSteps() {
|
|
1054
|
+
stepsBody.innerHTML = '';
|
|
1055
|
+
|
|
1056
|
+
if (state.steps.length === 0) {
|
|
1057
|
+
const row = document.createElement('tr');
|
|
1058
|
+
row.innerHTML = '<td colspan="3" style="color:#97afd2;">No recorded steps yet. Select a node and add actions.</td>';
|
|
1059
|
+
stepsBody.appendChild(row);
|
|
1060
|
+
return;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
for (const [index, step] of state.steps.entries()) {
|
|
1064
|
+
const row = document.createElement('tr');
|
|
1065
|
+
row.innerHTML = '<td>' + (index + 1) + '</td>'
|
|
1066
|
+
+ '<td>' + sanitize(step.action) + '</td>'
|
|
1067
|
+
+ '<td class="mono">' + sanitize(step.locator) + '</td>';
|
|
1068
|
+
stepsBody.appendChild(row);
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1072
|
+
function renderCode() {
|
|
1073
|
+
const lines = [];
|
|
1074
|
+
|
|
1075
|
+
if (state.language === 'python') {
|
|
1076
|
+
lines.push('from astur_test import expect, test');
|
|
1077
|
+
lines.push('');
|
|
1078
|
+
lines.push('def test_recorded_flow(device):');
|
|
1079
|
+
if (state.steps.length === 0) {
|
|
1080
|
+
const node = currentNode();
|
|
1081
|
+
const candidate = selectionCandidates(node)[0];
|
|
1082
|
+
if (candidate) {
|
|
1083
|
+
lines.push(' await ' + toPythonAction('Tap', candidate.code));
|
|
1084
|
+
} else {
|
|
1085
|
+
lines.push(' pass');
|
|
1086
|
+
}
|
|
1087
|
+
} else {
|
|
1088
|
+
for (const step of state.steps) {
|
|
1089
|
+
lines.push(' await ' + toPythonAction(step.action, step.locator, step.value));
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
} else {
|
|
1093
|
+
lines.push("import { expect, test } from '@astur-mobile/test';");
|
|
1094
|
+
lines.push('');
|
|
1095
|
+
lines.push("test('recorded flow', async ({ device }) => {");
|
|
1096
|
+
|
|
1097
|
+
if (state.steps.length === 0) {
|
|
1098
|
+
const node = currentNode();
|
|
1099
|
+
const candidate = selectionCandidates(node)[0];
|
|
1100
|
+
if (candidate) {
|
|
1101
|
+
lines.push(' await ' + toJsAction('Tap', candidate.code) + ';');
|
|
1102
|
+
} else {
|
|
1103
|
+
lines.push(' // Select an element and record an action.');
|
|
1104
|
+
}
|
|
1105
|
+
} else {
|
|
1106
|
+
for (const step of state.steps) {
|
|
1107
|
+
lines.push(' await ' + toJsAction(step.action, step.locator, step.value) + ';');
|
|
1108
|
+
}
|
|
1109
|
+
}
|
|
1110
|
+
|
|
1111
|
+
lines.push('});');
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
codeBlock.textContent = lines.join('\n');
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
function toJsAction(action, locator, value) {
|
|
1118
|
+
if (action === 'Fill') {
|
|
1119
|
+
return locator + '.fill(' + JSON.stringify(value || '') + ')';
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
if (action === 'Expect') {
|
|
1123
|
+
return 'expect(' + locator + ').toBeVisible()';
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
return locator + '.tap()';
|
|
1127
|
+
}
|
|
1128
|
+
|
|
1129
|
+
function toPythonAction(action, locator, value) {
|
|
1130
|
+
if (action === 'Fill') {
|
|
1131
|
+
return locator + '.fill(' + JSON.stringify(value || '') + ')';
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
if (action === 'Expect') {
|
|
1135
|
+
return 'expect(' + locator + ').to_be_visible()';
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
return locator + '.tap()';
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
async function copyText(value) {
|
|
1142
|
+
if (navigator.clipboard && window.isSecureContext) {
|
|
1143
|
+
try {
|
|
1144
|
+
await navigator.clipboard.writeText(value);
|
|
1145
|
+
return true;
|
|
1146
|
+
} catch {
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
const area = document.createElement('textarea');
|
|
1152
|
+
area.value = value;
|
|
1153
|
+
area.style.position = 'fixed';
|
|
1154
|
+
area.style.left = '-1000px';
|
|
1155
|
+
document.body.appendChild(area);
|
|
1156
|
+
area.focus();
|
|
1157
|
+
area.select();
|
|
1158
|
+
|
|
1159
|
+
try {
|
|
1160
|
+
return document.execCommand('copy');
|
|
1161
|
+
} catch {
|
|
1162
|
+
return false;
|
|
1163
|
+
} finally {
|
|
1164
|
+
document.body.removeChild(area);
|
|
1165
|
+
}
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
function sanitize(value) {
|
|
1169
|
+
return String(value)
|
|
1170
|
+
.replaceAll('&', '&')
|
|
1171
|
+
.replaceAll('<', '<')
|
|
1172
|
+
.replaceAll('>', '>')
|
|
1173
|
+
.replaceAll('"', '"')
|
|
1174
|
+
.replaceAll("'", ''');
|
|
1175
|
+
}
|
|
1176
|
+
})();
|
|
1177
|
+
</script>
|
|
1178
|
+
</body>
|
|
1179
|
+
</html>`;
|
|
1180
|
+
}
|
|
1181
|
+
//# sourceMappingURL=inspectorUi.js.map
|