@mobilewright/inspector 0.0.48 → 0.0.50

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.
@@ -15,16 +15,20 @@ export type Locator = {
15
15
  value: string;
16
16
  };
17
17
  /**
18
- * Derive the best mobilewright locator for a single ViewNode.
19
- * Returns null when no supported locator field is present.
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 best locator.
24
- * Nodes with no locatable field are included with locator: null.
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;AAkBvD,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;AAiBtC;;;GAGG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,QAAQ,GAAG,OAAO,GAAG,IAAI,CAgB5D;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,CAAA;CAAE,CAAC,CAapD"}
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;AAkBvD,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;AAiBtC;;;;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"}
@@ -30,35 +30,41 @@ function deriveRole(node) {
30
30
  return null;
31
31
  }
32
32
  /**
33
- * Derive the best mobilewright locator for a single ViewNode.
34
- * Returns null when no supported locator field is present.
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 deriveLocator(node) {
37
+ export function deriveLocators(node) {
38
+ const locators = [];
37
39
  const testId = node.identifier || node.resourceId;
38
40
  if (testId)
39
- return { kind: 'testId', value: testId };
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
- return { kind: 'role', value: role, name };
45
+ locators.push({ kind: 'role', value: role, name });
44
46
  }
45
47
  if (node.label)
46
- return { kind: 'label', value: node.label };
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
- return { kind: 'text', value: text };
50
- return null;
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 best locator.
54
- * Nodes with no locatable field are included with locator: null.
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
- result.push({ node, locator: deriveLocator(node) });
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,sFAAsF;AAItF,uFAAuF;AACvF,MAAM,aAAa,GAA6B;IAC9C,MAAM,EAAI,CAAC,QAAQ,EAAE,aAAa,CAAC;IACnC,SAAS,EAAE,CAAC,WAAW,EAAE,iBAAiB,EAAE,UAAU,EAAE,aAAa,EAAE,eAAe,CAAC;IACvF,IAAI,EAAM,CAAC,YAAY,EAAE,UAAU,EAAE,MAAM,EAAE,eAAe,CAAC;IAC7D,KAAK,EAAK,CAAC,OAAO,EAAE,WAAW,EAAE,gBAAgB,CAAC;IAClD,MAAM,EAAI,CAAC,QAAQ,EAAE,QAAQ,CAAC;IAC9B,QAAQ,EAAE,CAAC,UAAU,CAAC;IACtB,MAAM,EAAI,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC/B,IAAI,EAAM,CAAC,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,cAAc,EAAE,YAAY,EAAE,iBAAiB,CAAC;IAClG,QAAQ,EAAE,CAAC,MAAM,EAAE,cAAc,EAAE,gBAAgB,CAAC;IACpD,GAAG,EAAO,CAAC,KAAK,EAAE,QAAQ,CAAC;IAC3B,IAAI,EAAM,CAAC,MAAM,CAAC;IAClB,MAAM,EAAI,CAAC,eAAe,EAAE,SAAS,EAAE,QAAQ,CAAC;CACjD,CAAC;AASF,oFAAoF;AACpF,SAAS,UAAU,CAAC,IAAc;IAChC,MAAM,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IAE7C,IAAI,IAAI,KAAK,gBAAgB,EAAE,CAAC;QAC9B,MAAM,WAAW,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,WAAW,CAAC,KAAK,MAAM,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,YAAY,CAAC,KAAK,MAAM,CAAC;QAC9F,OAAO,WAAW,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC;IACvC,CAAC;IAED,KAAK,MAAM,CAAC,IAAI,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,aAAa,CAAC,EAAE,CAAC;QAC1D,IAAI,KAAK,CAAC,QAAQ,CAAC,IAAI,CAAC;YAAE,OAAO,IAAI,CAAC;IACxC,CAAC;IACD,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,aAAa,CAAC,IAAc;IAC1C,MAAM,MAAM,GAAG,IAAI,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,CAAC;IAClD,IAAI,MAAM;QAAE,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,EAAE,CAAC;IAErD,MAAM,IAAI,GAAG,UAAU,CAAC,IAAI,CAAC,CAAC;IAC9B,IAAI,IAAI,EAAE,CAAC;QACT,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,IAAI,IAAI,SAAS,CAAC;QAClD,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;IAC7C,CAAC;IAED,IAAI,IAAI,CAAC,KAAK;QAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,CAAC;IAE5D,MAAM,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;IAChF,IAAI,IAAI;QAAE,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC;IAE/C,OAAO,IAAI,CAAC;AACd,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,iBAAiB,CAC/B,KAAiB;IAEjB,MAAM,MAAM,GAAuD,EAAE,CAAC;IAEtE,uEAAuE;IACvE,SAAS,IAAI,CAAC,KAAiB;QAC7B,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;YACzB,MAAM,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,aAAa,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;YACpD,IAAI,IAAI,CAAC,QAAQ,EAAE,MAAM;gBAAE,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACjD,CAAC;IACH,CAAC;IAED,IAAI,CAAC,KAAK,CAAC,CAAC;IACZ,OAAO,MAAM,CAAC;AAChB,CAAC"}
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,8CA2C/D"}
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"}
@@ -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 ATTEMPT_TIMEOUT_MS = 10_000;
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
- /** Run screenshot + viewTree + screenSize together, retrying up to MAX_RETRIES times on failure. */
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
- let lastErr;
55
- for (let i = 0; i < MAX_RETRIES; i++) {
56
- try {
57
- return await withTimeout(Promise.all([device.screen.screenshot(), device.screen.viewTree(), device.screenSize()]).then(([screenshotBuffer, tree, size]) => ({ screenshotBuffer, tree, size })), ATTEMPT_TIMEOUT_MS, `Device operation timed out after ${ATTEMPT_TIMEOUT_MS}ms`);
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
- catch (err) {
60
- lastErr = err;
61
- logger.warn(`Inspect attempt ${i + 1}/${MAX_RETRIES} failed: ${err.message}`);
62
- if (i < MAX_RETRIES - 1)
63
- await new Promise(r => setTimeout(r, RETRY_DELAY_MS));
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
- throw lastErr;
108
+ return {
109
+ screenshotBuffer: results.screenshotBuffer,
110
+ tree: results.tree,
111
+ size: results.size,
112
+ };
67
113
  }
68
- /**
69
- * Race promise against a ms deadline. Rejects with message on timeout.
70
- * Note: cannot cancel the underlying promise; it continues running until the driver times out.
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(message)), ms); }),
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":"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"}
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.48",
3
+ "version": "0.0.50",
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.48",
35
- "@mobilewright/protocol": "^0.0.48",
34
+ "@mobilewright/core": "^0.0.50",
35
+ "@mobilewright/protocol": "^0.0.50",
36
36
  "express": "^5.2.1",
37
37
  "open": "^10.2.0"
38
38
  },
@@ -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">&times;</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) {