@mobilewright/inspector 0.0.49 → 0.0.51
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/lib/locator-derivation.d.ts +8 -4
- package/dist/lib/locator-derivation.d.ts.map +1 -1
- package/dist/lib/locator-derivation.js +38 -32
- package/dist/lib/locator-derivation.js.map +1 -1
- package/dist/routes/inspect.d.ts.map +1 -1
- package/dist/routes/inspect.js +64 -19
- package/dist/routes/inspect.js.map +1 -1
- package/package.json +3 -3
- package/public/css/app.css +235 -0
- package/public/index.html +11 -0
- package/public/js/app.js +178 -0
|
@@ -15,16 +15,20 @@ export type Locator = {
|
|
|
15
15
|
value: string;
|
|
16
16
|
};
|
|
17
17
|
/**
|
|
18
|
-
* Derive
|
|
19
|
-
* Returns
|
|
18
|
+
* Derive all matching mobilewright locators for a single ViewNode.
|
|
19
|
+
* Returns array ordered by priority: testId > role > label > text.
|
|
20
|
+
* Empty array when no supported locator field is present.
|
|
20
21
|
*/
|
|
22
|
+
export declare function deriveLocators(node: ViewNode): Locator[];
|
|
23
|
+
/** Keep single-locator derive for backward compat — returns highest priority match or null. */
|
|
21
24
|
export declare function deriveLocator(node: ViewNode): Locator | null;
|
|
22
25
|
/**
|
|
23
|
-
* Flatten a ViewNode tree depth-first and annotate each node with its
|
|
24
|
-
* Nodes with no locatable field are included with
|
|
26
|
+
* Flatten a ViewNode tree depth-first and annotate each node with its locators.
|
|
27
|
+
* Nodes with no locatable field are included with locators: [].
|
|
25
28
|
*/
|
|
26
29
|
export declare function deriveElementList(roots: ViewNode[]): Array<{
|
|
27
30
|
node: ViewNode;
|
|
28
31
|
locator: Locator | null;
|
|
32
|
+
locators: Locator[];
|
|
29
33
|
}>;
|
|
30
34
|
//# sourceMappingURL=locator-derivation.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locator-derivation.d.ts","sourceRoot":"","sources":["../../src/lib/locator-derivation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;
|
|
1
|
+
{"version":3,"file":"locator-derivation.d.ts","sourceRoot":"","sources":["../../src/lib/locator-derivation.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,wBAAwB,CAAC;AAavD,gFAAgF;AAChF,MAAM,MAAM,OAAO,GACf;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAG,KAAK,EAAE,MAAM,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,SAAS,CAAA;CAAE,GAC3D;IAAE,IAAI,EAAE,OAAO,CAAC;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GACjC;IAAE,IAAI,EAAE,MAAM,CAAC;IAAG,KAAK,EAAE,MAAM,CAAA;CAAE,CAAC;AAyBtC;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,EAAE,CAkBxD;AAED,+FAA+F;AAC/F,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAE5D;AAED;;;GAGG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,QAAQ,EAAE,GAChB,KAAK,CAAC;IAAE,IAAI,EAAE,QAAQ,CAAC;IAAC,OAAO,EAAE,OAAO,GAAG,IAAI,CAAC;IAAC,QAAQ,EAAE,OAAO,EAAE,CAAA;CAAE,CAAC,CAazE"}
|
|
@@ -1,64 +1,70 @@
|
|
|
1
1
|
// Derives the best mobilewright locator for every node in a ViewNode tree.
|
|
2
|
-
// Priority: Test ID > Role > Label > Text. Mirrors @mobilewright/core query-engine.ts
|
|
3
|
-
//
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
list: ['table', 'collectionview', 'listview', 'recyclerview', 'scrollview', 'reactscrollview'],
|
|
14
|
-
listitem: ['cell', 'linearlayout', 'relativelayout'],
|
|
15
|
-
tab: ['tab', 'tabbar'],
|
|
16
|
-
link: ['link'],
|
|
17
|
-
header: ['navigationbar', 'toolbar', 'header'],
|
|
18
|
-
};
|
|
2
|
+
// Priority: Test ID > Role > Label > Text. Mirrors @mobilewright/core query-engine.ts,
|
|
3
|
+
// and reuses its ROLE_TYPE_MAP so role derivation cannot drift from role matching.
|
|
4
|
+
import { ROLE_TYPE_MAP, bareTypeName } from '@mobilewright/core';
|
|
5
|
+
// core declares ROLE_TYPE_MAP `as const`, so each value is a readonly tuple of
|
|
6
|
+
// string literals. Widen to plain strings once, here, so membership can be tested
|
|
7
|
+
// against an arbitrary node type.
|
|
8
|
+
const ROLE_TYPES = Object.entries(ROLE_TYPE_MAP);
|
|
9
|
+
// Types that ROLE_TYPE_MAP matches on purpose but that must not be *suggested*:
|
|
10
|
+
// iOS reports generic containers as "other", so deriving getByRole('listitem')
|
|
11
|
+
// from one would hand the user a locator matching much of the tree.
|
|
12
|
+
const TYPES_TOO_GENERIC_TO_SUGGEST = new Set(['other']);
|
|
19
13
|
/** Map node.type to a mobilewright role string. Returns null for unmapped types. */
|
|
20
14
|
function deriveRole(node) {
|
|
21
|
-
|
|
15
|
+
// Same normalization core applies before matching: strips the Android package
|
|
16
|
+
// path ("android.widget.EditText") and the iOS "XCUIElementType" prefix.
|
|
17
|
+
const type = bareTypeName(node.type ?? '');
|
|
22
18
|
if (type === 'reactviewgroup') {
|
|
23
19
|
const isClickable = node.raw?.['clickable'] === 'true' || node.raw?.['accessible'] === 'true';
|
|
24
20
|
return isClickable ? 'button' : null;
|
|
25
21
|
}
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
if (TYPES_TOO_GENERIC_TO_SUGGEST.has(type)) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
for (const [role, types] of ROLE_TYPES) {
|
|
26
|
+
if (types.includes(type)) {
|
|
28
27
|
return role;
|
|
28
|
+
}
|
|
29
29
|
}
|
|
30
30
|
return null;
|
|
31
31
|
}
|
|
32
32
|
/**
|
|
33
|
-
* Derive
|
|
34
|
-
* Returns
|
|
33
|
+
* Derive all matching mobilewright locators for a single ViewNode.
|
|
34
|
+
* Returns array ordered by priority: testId > role > label > text.
|
|
35
|
+
* Empty array when no supported locator field is present.
|
|
35
36
|
*/
|
|
36
|
-
export function
|
|
37
|
+
export function deriveLocators(node) {
|
|
38
|
+
const locators = [];
|
|
37
39
|
const testId = node.identifier || node.resourceId;
|
|
38
40
|
if (testId)
|
|
39
|
-
|
|
41
|
+
locators.push({ kind: 'testId', value: testId });
|
|
40
42
|
const role = deriveRole(node);
|
|
41
43
|
if (role) {
|
|
42
44
|
const name = node.label || node.text || undefined;
|
|
43
|
-
|
|
45
|
+
locators.push({ kind: 'role', value: role, name });
|
|
44
46
|
}
|
|
45
47
|
if (node.label)
|
|
46
|
-
|
|
48
|
+
locators.push({ kind: 'label', value: node.label });
|
|
47
49
|
const text = node.text ?? (node.value != null ? String(node.value) : undefined);
|
|
48
50
|
if (text)
|
|
49
|
-
|
|
50
|
-
return
|
|
51
|
+
locators.push({ kind: 'text', value: text });
|
|
52
|
+
return locators;
|
|
53
|
+
}
|
|
54
|
+
/** Keep single-locator derive for backward compat — returns highest priority match or null. */
|
|
55
|
+
export function deriveLocator(node) {
|
|
56
|
+
return deriveLocators(node)[0] ?? null;
|
|
51
57
|
}
|
|
52
58
|
/**
|
|
53
|
-
* Flatten a ViewNode tree depth-first and annotate each node with its
|
|
54
|
-
* Nodes with no locatable field are included with
|
|
59
|
+
* Flatten a ViewNode tree depth-first and annotate each node with its locators.
|
|
60
|
+
* Nodes with no locatable field are included with locators: [].
|
|
55
61
|
*/
|
|
56
62
|
export function deriveElementList(roots) {
|
|
57
63
|
const result = [];
|
|
58
|
-
/** Depth-first recursive walk, pushing each visited node to result. */
|
|
59
64
|
function walk(nodes) {
|
|
60
65
|
for (const node of nodes) {
|
|
61
|
-
|
|
66
|
+
const nodeLocators = deriveLocators(node);
|
|
67
|
+
result.push({ node, locator: nodeLocators[0] ?? null, locators: nodeLocators });
|
|
62
68
|
if (node.children?.length)
|
|
63
69
|
walk(node.children);
|
|
64
70
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"locator-derivation.js","sourceRoot":"","sources":["../../src/lib/locator-derivation.ts"],"names":[],"mappings":"AAAA,2EAA2E;AAC3E,uFAAuF;AACvF,
|
|
1
|
+
{"version":3,"file":"locator-derivation.js","sourceRoot":"","sources":["../../src/lib/locator-derivation.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/routes/inspect.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAMzD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,aAAa,
|
|
1
|
+
{"version":3,"file":"inspect.d.ts","sourceRoot":"","sources":["../../src/routes/inspect.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,aAAa,EAAE,MAAM,0BAA0B,CAAC;AAMzD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,aAAa,EAAE,aAAa,8CAqD/D"}
|
package/dist/routes/inspect.js
CHANGED
|
@@ -3,7 +3,7 @@ import { deriveElementList } from '../lib/locator-derivation.js';
|
|
|
3
3
|
import { logger } from '../lib/logger.js';
|
|
4
4
|
const MAX_RETRIES = 3;
|
|
5
5
|
const RETRY_DELAY_MS = 1500;
|
|
6
|
-
const
|
|
6
|
+
const OP_TIMEOUT_MS = 10_000;
|
|
7
7
|
/**
|
|
8
8
|
* Express router for the inspect endpoint.
|
|
9
9
|
* GET /api/inspect — returns screenshot + element list from the same device moment.
|
|
@@ -24,14 +24,24 @@ export function createInspectRouter(deviceManager) {
|
|
|
24
24
|
}
|
|
25
25
|
try {
|
|
26
26
|
const { screenshotBuffer, tree, size } = await attemptWithRetry(device);
|
|
27
|
-
const elements = deriveElementList(tree).map(({ node, locator }, index) => ({
|
|
27
|
+
const elements = deriveElementList(tree).map(({ node, locator, locators }, index) => ({
|
|
28
28
|
index,
|
|
29
29
|
type: node.type,
|
|
30
30
|
label: node.label ?? null,
|
|
31
31
|
text: node.text ?? null,
|
|
32
32
|
bounds: node.bounds,
|
|
33
33
|
isVisible: node.isVisible,
|
|
34
|
+
identifier: node.identifier ?? null,
|
|
35
|
+
resourceId: node.resourceId ?? null,
|
|
36
|
+
placeholder: node.placeholder ?? null,
|
|
37
|
+
value: node.value ?? null,
|
|
38
|
+
isEnabled: node.isEnabled ?? true,
|
|
39
|
+
isSelected: node.isSelected ?? null,
|
|
40
|
+
isFocused: node.isFocused ?? null,
|
|
41
|
+
isChecked: node.isChecked ?? null,
|
|
42
|
+
raw: node.raw ?? null,
|
|
34
43
|
locator,
|
|
44
|
+
locators,
|
|
35
45
|
}));
|
|
36
46
|
res.json({
|
|
37
47
|
screenshot: `data:image/png;base64,${screenshotBuffer.toString('base64')}`,
|
|
@@ -49,31 +59,66 @@ export function createInspectRouter(deviceManager) {
|
|
|
49
59
|
});
|
|
50
60
|
return router;
|
|
51
61
|
}
|
|
52
|
-
/**
|
|
62
|
+
/**
|
|
63
|
+
* Run screenshot + viewTree + screenSize, retrying up to MAX_RETRIES.
|
|
64
|
+
*
|
|
65
|
+
* Strategy: first attempt runs all 3 in parallel for speed. Retries
|
|
66
|
+
* run only the failed ops sequentially (1 at a time) so that abandoned
|
|
67
|
+
* RPCs from timed-out attempts never stack up more than 2 concurrent
|
|
68
|
+
* mobilecli worker slots per retry cycle.
|
|
69
|
+
*/
|
|
53
70
|
async function attemptWithRetry(device) {
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
71
|
+
const ops = [
|
|
72
|
+
{ key: 'screenshotBuffer', run: () => device.screen.screenshot() },
|
|
73
|
+
{ key: 'tree', run: () => device.screen.viewTree() },
|
|
74
|
+
{ key: 'size', run: () => device.screenSize() },
|
|
75
|
+
];
|
|
76
|
+
const results = {};
|
|
77
|
+
for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
|
|
78
|
+
const pending = ops.filter(op => !(op.key in results));
|
|
79
|
+
if (pending.length === 0)
|
|
80
|
+
break;
|
|
81
|
+
if (attempt === 0) {
|
|
82
|
+
const settled = await Promise.allSettled(pending.map(op => timeoutAfter(op.run(), OP_TIMEOUT_MS)));
|
|
83
|
+
for (let i = 0; i < settled.length; i++) {
|
|
84
|
+
const result = settled[i];
|
|
85
|
+
if (result.status === 'fulfilled')
|
|
86
|
+
results[pending[i].key] = result.value;
|
|
87
|
+
}
|
|
58
88
|
}
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
89
|
+
else {
|
|
90
|
+
for (const op of pending) {
|
|
91
|
+
try {
|
|
92
|
+
results[op.key] = await timeoutAfter(op.run(), OP_TIMEOUT_MS);
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
logger.warn(`Inspect ${op.key} attempt ${attempt + 1}/${MAX_RETRIES} failed: ${err.message}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
64
98
|
}
|
|
99
|
+
const stillPending = ops.filter(op => !(op.key in results));
|
|
100
|
+
if (stillPending.length > 0 && attempt < MAX_RETRIES - 1) {
|
|
101
|
+
await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const missing = ops.filter(op => !(op.key in results));
|
|
105
|
+
if (missing.length > 0) {
|
|
106
|
+
throw new Error(`Inspect failed: ${missing.map(o => o.key).join(', ')} did not complete`);
|
|
65
107
|
}
|
|
66
|
-
|
|
108
|
+
return {
|
|
109
|
+
screenshotBuffer: results.screenshotBuffer,
|
|
110
|
+
tree: results.tree,
|
|
111
|
+
size: results.size,
|
|
112
|
+
};
|
|
67
113
|
}
|
|
68
|
-
/**
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
function withTimeout(promise, ms, message) {
|
|
114
|
+
/** Reject a promise if it doesn't settle within ms. */
|
|
115
|
+
function timeoutAfter(promise, ms) {
|
|
116
|
+
if (ms <= 0)
|
|
117
|
+
return Promise.reject(new Error('Deadline exceeded'));
|
|
73
118
|
let timer;
|
|
74
119
|
return Promise.race([
|
|
75
120
|
promise,
|
|
76
|
-
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(
|
|
121
|
+
new Promise((_, reject) => { timer = setTimeout(() => reject(new Error(`Operation timed out after ${ms}ms`)), ms); }),
|
|
77
122
|
]).finally(() => clearTimeout(timer));
|
|
78
123
|
}
|
|
79
124
|
//# sourceMappingURL=inspect.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"inspect.js","sourceRoot":"","sources":["../../src/routes/inspect.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAGjC,OAAO,EAAE,iBAAiB,EAAE,MAAM,8BAA8B,CAAC;AACjE,OAAO,EAAE,MAAM,EAAE,MAAM,kBAAkB,CAAC;AAG1C,MAAM,WAAW,GAAG,CAAC,CAAC;AACtB,MAAM,cAAc,GAAG,IAAI,CAAC;AAC5B,MAAM,
|
|
1
|
+
{"version":3,"file":"inspect.js","sourceRoot":"","sources":["../../src/routes/inspect.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mobilewright/inspector",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.51",
|
|
4
4
|
"description": "Browser-based element inspector for mobilewright",
|
|
5
5
|
"homepage": "https://mobilewright.dev",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -31,8 +31,8 @@
|
|
|
31
31
|
"public"
|
|
32
32
|
],
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@mobilewright/core": "^0.0.
|
|
35
|
-
"@mobilewright/protocol": "^0.0.
|
|
34
|
+
"@mobilewright/core": "^0.0.51",
|
|
35
|
+
"@mobilewright/protocol": "^0.0.51",
|
|
36
36
|
"express": "^5.2.1",
|
|
37
37
|
"open": "^10.2.0"
|
|
38
38
|
},
|
package/public/css/app.css
CHANGED
|
@@ -451,3 +451,238 @@ main {
|
|
|
451
451
|
padding: 40px 20px;
|
|
452
452
|
text-align: center;
|
|
453
453
|
}
|
|
454
|
+
|
|
455
|
+
/* ---- Multi-locator row enhancements ---- */
|
|
456
|
+
|
|
457
|
+
.locator-more {
|
|
458
|
+
font-size: 9px;
|
|
459
|
+
color: var(--c-text-muted);
|
|
460
|
+
background: var(--c-surface);
|
|
461
|
+
border: 1px solid var(--c-border);
|
|
462
|
+
border-radius: 3px;
|
|
463
|
+
padding: 1px 5px;
|
|
464
|
+
flex-shrink: 0;
|
|
465
|
+
cursor: default;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
/* ---- Detail pane ---- */
|
|
469
|
+
|
|
470
|
+
#detail-pane {
|
|
471
|
+
flex: 0 0 320px;
|
|
472
|
+
overflow-y: auto;
|
|
473
|
+
background: var(--c-surface);
|
|
474
|
+
border-left: 1px solid var(--c-border);
|
|
475
|
+
display: flex;
|
|
476
|
+
flex-direction: column;
|
|
477
|
+
animation: slide-in 0.15s ease-out;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
@keyframes slide-in {
|
|
481
|
+
from { flex-basis: 0; opacity: 0; }
|
|
482
|
+
to { flex-basis: 320px; opacity: 1; }
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#detail-header {
|
|
486
|
+
display: flex;
|
|
487
|
+
align-items: center;
|
|
488
|
+
justify-content: space-between;
|
|
489
|
+
padding: 12px 14px;
|
|
490
|
+
border-bottom: 1px solid var(--c-border);
|
|
491
|
+
position: sticky;
|
|
492
|
+
top: 0;
|
|
493
|
+
background: var(--c-surface);
|
|
494
|
+
z-index: 1;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
#detail-type {
|
|
498
|
+
font-size: 14px;
|
|
499
|
+
font-weight: 600;
|
|
500
|
+
color: var(--c-text-strong);
|
|
501
|
+
text-transform: capitalize;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
#detail-close {
|
|
505
|
+
background: none;
|
|
506
|
+
border: none;
|
|
507
|
+
color: var(--c-text-muted);
|
|
508
|
+
font-size: 18px;
|
|
509
|
+
cursor: pointer;
|
|
510
|
+
padding: 0 4px;
|
|
511
|
+
line-height: 1;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
#detail-close:hover { color: var(--c-text); }
|
|
515
|
+
|
|
516
|
+
#detail-body {
|
|
517
|
+
padding: 12px 14px;
|
|
518
|
+
display: flex;
|
|
519
|
+
flex-direction: column;
|
|
520
|
+
gap: 16px;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
/* Locator strategies section */
|
|
524
|
+
#detail-locators {
|
|
525
|
+
display: flex;
|
|
526
|
+
flex-direction: column;
|
|
527
|
+
gap: 4px;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
.detail-section-label {
|
|
531
|
+
font-size: 10px;
|
|
532
|
+
font-weight: 600;
|
|
533
|
+
text-transform: uppercase;
|
|
534
|
+
letter-spacing: 0.05em;
|
|
535
|
+
color: var(--c-text-muted);
|
|
536
|
+
margin-bottom: 6px;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
.detail-locator-row {
|
|
540
|
+
display: flex;
|
|
541
|
+
align-items: center;
|
|
542
|
+
gap: 6px;
|
|
543
|
+
padding: 5px 8px;
|
|
544
|
+
background: var(--c-surface2);
|
|
545
|
+
border-radius: 4px;
|
|
546
|
+
border: 1px solid var(--c-border2);
|
|
547
|
+
font-family: 'SF Mono', Menlo, Monaco, monospace;
|
|
548
|
+
font-size: 11px;
|
|
549
|
+
cursor: pointer;
|
|
550
|
+
transition: border-color 0.1s;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
.detail-locator-row:hover {
|
|
554
|
+
border-color: var(--c-accent);
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.detail-locator-row .locator-badge {
|
|
558
|
+
font-size: 9px;
|
|
559
|
+
padding: 1px 5px;
|
|
560
|
+
min-width: auto;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
.detail-locator-row .locator-value {
|
|
564
|
+
font-size: 11px;
|
|
565
|
+
white-space: nowrap;
|
|
566
|
+
overflow: hidden;
|
|
567
|
+
text-overflow: ellipsis;
|
|
568
|
+
flex: 1;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
.detail-locator-row .copy-btn {
|
|
572
|
+
background: none;
|
|
573
|
+
border: none;
|
|
574
|
+
color: var(--c-text-dim);
|
|
575
|
+
cursor: pointer;
|
|
576
|
+
font-size: 11px;
|
|
577
|
+
padding: 1px 4px;
|
|
578
|
+
border-radius: 3px;
|
|
579
|
+
opacity: 0;
|
|
580
|
+
transition: opacity 0.1s;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
.detail-locator-row:hover .copy-btn {
|
|
584
|
+
opacity: 1;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
.detail-locator-row .copy-btn:hover {
|
|
588
|
+
color: var(--c-accent);
|
|
589
|
+
background: var(--c-accent-dim);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
.detail-locator-row .copy-btn.copied {
|
|
593
|
+
color: var(--c-selected);
|
|
594
|
+
opacity: 1;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
/* Properties table */
|
|
598
|
+
#detail-properties {
|
|
599
|
+
display: flex;
|
|
600
|
+
flex-direction: column;
|
|
601
|
+
gap: 1px;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
.detail-prop-row {
|
|
605
|
+
display: flex;
|
|
606
|
+
align-items: center;
|
|
607
|
+
padding: 4px 8px;
|
|
608
|
+
background: var(--c-surface2);
|
|
609
|
+
border-radius: 3px;
|
|
610
|
+
gap: 8px;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
.detail-prop-key {
|
|
614
|
+
font-size: 10px;
|
|
615
|
+
font-weight: 500;
|
|
616
|
+
color: var(--c-text-muted);
|
|
617
|
+
text-transform: capitalize;
|
|
618
|
+
flex-shrink: 0;
|
|
619
|
+
min-width: 70px;
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
.detail-prop-value {
|
|
623
|
+
font-size: 11px;
|
|
624
|
+
color: var(--c-text);
|
|
625
|
+
font-family: 'SF Mono', Menlo, Monaco, monospace;
|
|
626
|
+
word-break: break-all;
|
|
627
|
+
flex: 1;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
.detail-prop-value.boolean-true { color: var(--c-selected); }
|
|
631
|
+
.detail-prop-value.boolean-false { color: var(--c-status-error); }
|
|
632
|
+
|
|
633
|
+
/* Raw attributes */
|
|
634
|
+
#detail-raw {
|
|
635
|
+
margin-top: 4px;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.detail-raw-toggle {
|
|
639
|
+
background: none;
|
|
640
|
+
border: 1px solid var(--c-border);
|
|
641
|
+
border-radius: 4px;
|
|
642
|
+
color: var(--c-text-muted);
|
|
643
|
+
font-size: 10px;
|
|
644
|
+
text-transform: uppercase;
|
|
645
|
+
letter-spacing: 0.03em;
|
|
646
|
+
padding: 4px 10px;
|
|
647
|
+
cursor: pointer;
|
|
648
|
+
width: 100%;
|
|
649
|
+
text-align: left;
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
.detail-raw-toggle:hover {
|
|
653
|
+
background: var(--c-row-hover);
|
|
654
|
+
color: var(--c-text);
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
.detail-raw-content {
|
|
658
|
+
margin-top: 4px;
|
|
659
|
+
padding: 8px;
|
|
660
|
+
background: var(--c-surface2);
|
|
661
|
+
border-radius: 4px;
|
|
662
|
+
font-family: 'SF Mono', Menlo, Monaco, monospace;
|
|
663
|
+
font-size: 10px;
|
|
664
|
+
color: var(--c-text-muted);
|
|
665
|
+
white-space: pre-wrap;
|
|
666
|
+
word-break: break-all;
|
|
667
|
+
max-height: 200px;
|
|
668
|
+
overflow-y: auto;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
/* ---- Responsive ---- */
|
|
672
|
+
|
|
673
|
+
@media (max-width: 900px) {
|
|
674
|
+
main {
|
|
675
|
+
flex-direction: column;
|
|
676
|
+
}
|
|
677
|
+
#screenshot-pane {
|
|
678
|
+
max-width: 100%;
|
|
679
|
+
border-right: none;
|
|
680
|
+
border-bottom: 1px solid var(--c-border);
|
|
681
|
+
}
|
|
682
|
+
#detail-pane {
|
|
683
|
+
flex: 1;
|
|
684
|
+
width: 100%;
|
|
685
|
+
border-left: none;
|
|
686
|
+
border-top: 1px solid var(--c-border);
|
|
687
|
+
}
|
|
688
|
+
}
|
package/public/index.html
CHANGED
|
@@ -60,6 +60,17 @@
|
|
|
60
60
|
<section id="elements-pane">
|
|
61
61
|
<div id="elements-list"></div>
|
|
62
62
|
</section>
|
|
63
|
+
<section id="detail-pane" hidden>
|
|
64
|
+
<div id="detail-header">
|
|
65
|
+
<span id="detail-type"></span>
|
|
66
|
+
<button id="detail-close" aria-label="Close detail panel">×</button>
|
|
67
|
+
</div>
|
|
68
|
+
<div id="detail-body">
|
|
69
|
+
<div id="detail-locators"></div>
|
|
70
|
+
<div id="detail-properties"></div>
|
|
71
|
+
<div id="detail-raw" hidden></div>
|
|
72
|
+
</div>
|
|
73
|
+
</section>
|
|
63
74
|
</main>
|
|
64
75
|
|
|
65
76
|
<script src="/js/app.js" type="module"></script>
|
package/public/js/app.js
CHANGED
|
@@ -153,6 +153,171 @@ class ScreenshotPane {
|
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
+
// ---- DetailPane ----
|
|
157
|
+
// Shows element properties, all locator strategies, raw attributes.
|
|
158
|
+
|
|
159
|
+
class DetailPane {
|
|
160
|
+
#pane
|
|
161
|
+
#typeEl
|
|
162
|
+
#locatorsEl
|
|
163
|
+
#propsEl
|
|
164
|
+
#rawEl
|
|
165
|
+
#closeBtn
|
|
166
|
+
#onCloseCb = null
|
|
167
|
+
|
|
168
|
+
constructor() {
|
|
169
|
+
this.#pane = document.getElementById('detail-pane')
|
|
170
|
+
this.#typeEl = document.getElementById('detail-type')
|
|
171
|
+
this.#locatorsEl = document.getElementById('detail-locators')
|
|
172
|
+
this.#propsEl = document.getElementById('detail-properties')
|
|
173
|
+
this.#rawEl = document.getElementById('detail-raw')
|
|
174
|
+
this.#closeBtn = document.getElementById('detail-close')
|
|
175
|
+
this.#closeBtn.addEventListener('click', () => {
|
|
176
|
+
this.hide()
|
|
177
|
+
this.#onCloseCb?.()
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
onClose(cb) { this.#onCloseCb = cb }
|
|
182
|
+
|
|
183
|
+
show(el) {
|
|
184
|
+
if (!el) { this.hide(); return }
|
|
185
|
+
|
|
186
|
+
this.#typeEl.textContent = el.type ?? 'unknown'
|
|
187
|
+
|
|
188
|
+
// Locators
|
|
189
|
+
this.#locatorsEl.innerHTML = ''
|
|
190
|
+
if (el.locators?.length) {
|
|
191
|
+
const label = document.createElement('div')
|
|
192
|
+
label.className = 'detail-section-label'
|
|
193
|
+
label.textContent = 'Locator Strategies'
|
|
194
|
+
this.#locatorsEl.appendChild(label)
|
|
195
|
+
|
|
196
|
+
for (const loc of el.locators) {
|
|
197
|
+
const row = document.createElement('div')
|
|
198
|
+
row.className = 'detail-locator-row'
|
|
199
|
+
row.title = 'Click to copy'
|
|
200
|
+
|
|
201
|
+
const badge = document.createElement('span')
|
|
202
|
+
badge.className = `locator-badge badge-${loc.kind}`
|
|
203
|
+
badge.textContent = loc.kind
|
|
204
|
+
row.appendChild(badge)
|
|
205
|
+
|
|
206
|
+
const val = document.createElement('span')
|
|
207
|
+
val.className = 'locator-value'
|
|
208
|
+
val.textContent = locatorLabel(loc)
|
|
209
|
+
row.appendChild(val)
|
|
210
|
+
|
|
211
|
+
const copyBtn = document.createElement('button')
|
|
212
|
+
copyBtn.className = 'copy-btn'
|
|
213
|
+
copyBtn.textContent = '📋'
|
|
214
|
+
copyBtn.title = 'Copy locator'
|
|
215
|
+
copyBtn.addEventListener('click', e => {
|
|
216
|
+
e.stopPropagation()
|
|
217
|
+
navigator.clipboard.writeText(locatorLabel(loc)).then(() => {
|
|
218
|
+
copyBtn.textContent = '✓'
|
|
219
|
+
copyBtn.classList.add('copied')
|
|
220
|
+
setTimeout(() => { copyBtn.textContent = '📋'; copyBtn.classList.remove('copied') }, 1500)
|
|
221
|
+
}).catch(() => {})
|
|
222
|
+
})
|
|
223
|
+
row.appendChild(copyBtn)
|
|
224
|
+
|
|
225
|
+
row.addEventListener('click', () => {
|
|
226
|
+
navigator.clipboard.writeText(locatorLabel(loc)).catch(() => {})
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
this.#locatorsEl.appendChild(row)
|
|
230
|
+
}
|
|
231
|
+
} else {
|
|
232
|
+
const row = document.createElement('div')
|
|
233
|
+
row.className = 'detail-locator-row'
|
|
234
|
+
row.style.opacity = '0.5'
|
|
235
|
+
row.textContent = '(no locator available)'
|
|
236
|
+
this.#locatorsEl.appendChild(row)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
// Properties
|
|
240
|
+
this.#propsEl.innerHTML = ''
|
|
241
|
+
const propLabel = document.createElement('div')
|
|
242
|
+
propLabel.className = 'detail-section-label'
|
|
243
|
+
propLabel.textContent = 'Properties'
|
|
244
|
+
this.#propsEl.appendChild(propLabel)
|
|
245
|
+
|
|
246
|
+
const props = [
|
|
247
|
+
{ key: 'type', value: el.type },
|
|
248
|
+
{ key: 'label', value: el.label },
|
|
249
|
+
{ key: 'text', value: el.text },
|
|
250
|
+
{ key: 'identifier', value: el.identifier },
|
|
251
|
+
{ key: 'resourceId', value: el.resourceId },
|
|
252
|
+
{ key: 'placeholder', value: el.placeholder },
|
|
253
|
+
{ key: 'value', value: el.value },
|
|
254
|
+
{ key: 'bounds', value: el.bounds ? `${el.bounds.x},${el.bounds.y} ${el.bounds.width}×${el.bounds.height}` : null },
|
|
255
|
+
{ key: 'visible', value: el.isVisible },
|
|
256
|
+
{ key: 'enabled', value: el.isEnabled },
|
|
257
|
+
{ key: 'selected', value: el.isSelected },
|
|
258
|
+
{ key: 'focused', value: el.isFocused },
|
|
259
|
+
{ key: 'checked', value: el.isChecked },
|
|
260
|
+
]
|
|
261
|
+
|
|
262
|
+
for (const p of props) {
|
|
263
|
+
if (p.value === null || p.value === undefined) continue
|
|
264
|
+
const row = document.createElement('div')
|
|
265
|
+
row.className = 'detail-prop-row'
|
|
266
|
+
|
|
267
|
+
const key = document.createElement('span')
|
|
268
|
+
key.className = 'detail-prop-key'
|
|
269
|
+
key.textContent = p.key
|
|
270
|
+
row.appendChild(key)
|
|
271
|
+
|
|
272
|
+
const val = document.createElement('span')
|
|
273
|
+
val.className = 'detail-prop-value'
|
|
274
|
+
if (typeof p.value === 'boolean') {
|
|
275
|
+
val.className += p.value ? ' boolean-true' : ' boolean-false'
|
|
276
|
+
val.textContent = String(p.value)
|
|
277
|
+
} else {
|
|
278
|
+
val.textContent = String(p.value)
|
|
279
|
+
}
|
|
280
|
+
row.appendChild(val)
|
|
281
|
+
|
|
282
|
+
this.#propsEl.appendChild(row)
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Raw attributes (collapsible)
|
|
286
|
+
if (el.raw && typeof el.raw === 'object' && Object.keys(el.raw).length > 0) {
|
|
287
|
+
this.#rawEl.hidden = false
|
|
288
|
+
this.#rawEl.innerHTML = ''
|
|
289
|
+
|
|
290
|
+
const toggle = document.createElement('button')
|
|
291
|
+
toggle.className = 'detail-raw-toggle'
|
|
292
|
+
toggle.textContent = `▶ Raw Attributes (${Object.keys(el.raw).length})`
|
|
293
|
+
this.#rawEl.appendChild(toggle)
|
|
294
|
+
|
|
295
|
+
const content = document.createElement('div')
|
|
296
|
+
content.className = 'detail-raw-content'
|
|
297
|
+
content.hidden = true
|
|
298
|
+
content.textContent = JSON.stringify(el.raw, null, 2)
|
|
299
|
+
|
|
300
|
+
toggle.addEventListener('click', () => {
|
|
301
|
+
const isHidden = content.hidden
|
|
302
|
+
content.hidden = !isHidden
|
|
303
|
+
toggle.textContent = isHidden
|
|
304
|
+
? `▼ Raw Attributes (${Object.keys(el.raw).length})`
|
|
305
|
+
: `▶ Raw Attributes (${Object.keys(el.raw).length})`
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
this.#rawEl.appendChild(content)
|
|
309
|
+
} else {
|
|
310
|
+
this.#rawEl.hidden = true
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
this.#pane.hidden = false
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
hide() {
|
|
317
|
+
this.#pane.hidden = true
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
|
|
156
321
|
// ---- ElementsPane ----
|
|
157
322
|
// Owns the element list: rendering rows, selection state, visibility toggles.
|
|
158
323
|
|
|
@@ -245,6 +410,14 @@ class ElementsPane {
|
|
|
245
410
|
row.appendChild(warn)
|
|
246
411
|
}
|
|
247
412
|
|
|
413
|
+
if (el.locators && el.locators.length > 1) {
|
|
414
|
+
const more = document.createElement('span')
|
|
415
|
+
more.className = 'locator-more'
|
|
416
|
+
more.textContent = `+${el.locators.length - 1} more`
|
|
417
|
+
more.title = el.locators.slice(1).map(l => locatorLabel(l)).join('\n')
|
|
418
|
+
row.appendChild(more)
|
|
419
|
+
}
|
|
420
|
+
|
|
248
421
|
const type = document.createElement('span')
|
|
249
422
|
type.className = 'element-type'
|
|
250
423
|
type.textContent = el.type ?? ''
|
|
@@ -297,6 +470,7 @@ class Inspector {
|
|
|
297
470
|
|
|
298
471
|
#screenshotPane = new ScreenshotPane()
|
|
299
472
|
#elementsPane = new ElementsPane()
|
|
473
|
+
#detailPane = new DetailPane()
|
|
300
474
|
#deviceSelect = document.getElementById('device-select')
|
|
301
475
|
#refreshBtn = document.getElementById('refresh-btn')
|
|
302
476
|
#autoRefreshToggle = document.getElementById('auto-refresh-toggle')
|
|
@@ -307,6 +481,7 @@ class Inspector {
|
|
|
307
481
|
this.#screenshotPane.onElementClick(i => this.#selectElement(i))
|
|
308
482
|
this.#elementsPane.onElementClick(i => this.#selectElement(i))
|
|
309
483
|
this.#elementsPane.onToggleHidden(i => this.#toggleHidden(i))
|
|
484
|
+
this.#detailPane.onClose(() => { this.#state.selectedIndex = null; this.#screenshotPane.setSelectedIndex(null); this.#elementsPane.setSelectedIndex(null) })
|
|
310
485
|
|
|
311
486
|
this.#refreshBtn.addEventListener('click', () => this.refresh())
|
|
312
487
|
this.#deviceSelect.addEventListener('change', () => {
|
|
@@ -353,6 +528,7 @@ class Inspector {
|
|
|
353
528
|
const data = await res.json()
|
|
354
529
|
this.#state.elements = data.elements ?? []
|
|
355
530
|
this.#state.selectedIndex = null
|
|
531
|
+
this.#detailPane.hide()
|
|
356
532
|
this.#state.logicalWidth = data.screen?.width ?? 0
|
|
357
533
|
this.#state.logicalHeight = data.screen?.height ?? 0
|
|
358
534
|
|
|
@@ -485,6 +661,8 @@ class Inspector {
|
|
|
485
661
|
this.#state.selectedIndex = index
|
|
486
662
|
this.#screenshotPane.setSelectedIndex(index)
|
|
487
663
|
this.#elementsPane.setSelectedIndex(index)
|
|
664
|
+
const el = this.#state.elements.find(e => e.index === index)
|
|
665
|
+
this.#detailPane.show(el ?? null)
|
|
488
666
|
}
|
|
489
667
|
|
|
490
668
|
#toggleHidden(index) {
|