devlens-mcp 0.3.0 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (162) hide show
  1. package/README.md +10 -21
  2. package/docs/setup-guide.md +11 -31
  3. package/package.json +1 -1
  4. package/.claude/settings.json +0 -12
  5. package/.claude/settings.local.json +0 -17
  6. package/bin/cli.ts +0 -22
  7. package/bin/register.ts +0 -96
  8. package/dist/src/config/devlens-config.d.ts +0 -92
  9. package/dist/src/config/devlens-config.d.ts.map +0 -1
  10. package/dist/src/config/devlens-config.js +0 -70
  11. package/dist/src/config/devlens-config.js.map +0 -1
  12. package/dist/src/index.d.ts +0 -35
  13. package/dist/src/index.d.ts.map +0 -1
  14. package/dist/src/index.js +0 -8
  15. package/dist/src/index.js.map +0 -1
  16. package/dist/src/metro/cdp-client.d.ts +0 -48
  17. package/dist/src/metro/cdp-client.d.ts.map +0 -1
  18. package/dist/src/metro/cdp-client.js +0 -127
  19. package/dist/src/metro/cdp-client.js.map +0 -1
  20. package/dist/src/metro/log-collector.d.ts +0 -30
  21. package/dist/src/metro/log-collector.d.ts.map +0 -1
  22. package/dist/src/metro/log-collector.js +0 -114
  23. package/dist/src/metro/log-collector.js.map +0 -1
  24. package/dist/src/metro/metro-bridge.d.ts +0 -56
  25. package/dist/src/metro/metro-bridge.d.ts.map +0 -1
  26. package/dist/src/metro/metro-bridge.js +0 -255
  27. package/dist/src/metro/metro-bridge.js.map +0 -1
  28. package/dist/src/metro/network-inspector.d.ts +0 -34
  29. package/dist/src/metro/network-inspector.d.ts.map +0 -1
  30. package/dist/src/metro/network-inspector.js +0 -100
  31. package/dist/src/metro/network-inspector.js.map +0 -1
  32. package/dist/src/platform/android/adb.d.ts +0 -50
  33. package/dist/src/platform/android/adb.d.ts.map +0 -1
  34. package/dist/src/platform/android/adb.js +0 -137
  35. package/dist/src/platform/android/adb.js.map +0 -1
  36. package/dist/src/platform/android/android-device.d.ts +0 -21
  37. package/dist/src/platform/android/android-device.d.ts.map +0 -1
  38. package/dist/src/platform/android/android-device.js +0 -94
  39. package/dist/src/platform/android/android-device.js.map +0 -1
  40. package/dist/src/platform/android/ui-automator.d.ts +0 -17
  41. package/dist/src/platform/android/ui-automator.d.ts.map +0 -1
  42. package/dist/src/platform/android/ui-automator.js +0 -126
  43. package/dist/src/platform/android/ui-automator.js.map +0 -1
  44. package/dist/src/platform/device-manager.d.ts +0 -28
  45. package/dist/src/platform/device-manager.d.ts.map +0 -1
  46. package/dist/src/platform/device-manager.js +0 -185
  47. package/dist/src/platform/device-manager.js.map +0 -1
  48. package/dist/src/platform/device.d.ts +0 -86
  49. package/dist/src/platform/device.d.ts.map +0 -1
  50. package/dist/src/platform/device.js +0 -7
  51. package/dist/src/platform/device.js.map +0 -1
  52. package/dist/src/platform/ios/accessibility.d.ts +0 -17
  53. package/dist/src/platform/ios/accessibility.d.ts.map +0 -1
  54. package/dist/src/platform/ios/accessibility.js +0 -159
  55. package/dist/src/platform/ios/accessibility.js.map +0 -1
  56. package/dist/src/platform/ios/ios-device.d.ts +0 -22
  57. package/dist/src/platform/ios/ios-device.d.ts.map +0 -1
  58. package/dist/src/platform/ios/ios-device.js +0 -97
  59. package/dist/src/platform/ios/ios-device.js.map +0 -1
  60. package/dist/src/platform/ios/simctl.d.ts +0 -54
  61. package/dist/src/platform/ios/simctl.d.ts.map +0 -1
  62. package/dist/src/platform/ios/simctl.js +0 -192
  63. package/dist/src/platform/ios/simctl.js.map +0 -1
  64. package/dist/src/server.d.ts +0 -3
  65. package/dist/src/server.d.ts.map +0 -1
  66. package/dist/src/server.js +0 -176
  67. package/dist/src/server.js.map +0 -1
  68. package/dist/src/snapshot/formatter.d.ts +0 -18
  69. package/dist/src/snapshot/formatter.d.ts.map +0 -1
  70. package/dist/src/snapshot/formatter.js +0 -86
  71. package/dist/src/snapshot/formatter.js.map +0 -1
  72. package/dist/src/snapshot/ref-registry.d.ts +0 -67
  73. package/dist/src/snapshot/ref-registry.d.ts.map +0 -1
  74. package/dist/src/snapshot/ref-registry.js +0 -169
  75. package/dist/src/snapshot/ref-registry.js.map +0 -1
  76. package/dist/src/snapshot/snapshot-differ.d.ts +0 -57
  77. package/dist/src/snapshot/snapshot-differ.d.ts.map +0 -1
  78. package/dist/src/snapshot/snapshot-differ.js +0 -153
  79. package/dist/src/snapshot/snapshot-differ.js.map +0 -1
  80. package/dist/src/tools/app-tools.d.ts +0 -71
  81. package/dist/src/tools/app-tools.d.ts.map +0 -1
  82. package/dist/src/tools/app-tools.js +0 -97
  83. package/dist/src/tools/app-tools.js.map +0 -1
  84. package/dist/src/tools/device-tools.d.ts +0 -53
  85. package/dist/src/tools/device-tools.d.ts.map +0 -1
  86. package/dist/src/tools/device-tools.js +0 -86
  87. package/dist/src/tools/device-tools.js.map +0 -1
  88. package/dist/src/tools/ds-tools.d.ts +0 -65
  89. package/dist/src/tools/ds-tools.d.ts.map +0 -1
  90. package/dist/src/tools/ds-tools.js +0 -314
  91. package/dist/src/tools/ds-tools.js.map +0 -1
  92. package/dist/src/tools/interaction-tools.d.ts +0 -248
  93. package/dist/src/tools/interaction-tools.d.ts.map +0 -1
  94. package/dist/src/tools/interaction-tools.js +0 -391
  95. package/dist/src/tools/interaction-tools.js.map +0 -1
  96. package/dist/src/tools/metro-tools.d.ts +0 -115
  97. package/dist/src/tools/metro-tools.d.ts.map +0 -1
  98. package/dist/src/tools/metro-tools.js +0 -270
  99. package/dist/src/tools/metro-tools.js.map +0 -1
  100. package/dist/src/tools/navigation-tools.d.ts +0 -36
  101. package/dist/src/tools/navigation-tools.d.ts.map +0 -1
  102. package/dist/src/tools/navigation-tools.js +0 -60
  103. package/dist/src/tools/navigation-tools.js.map +0 -1
  104. package/dist/src/tools/screenshot-tools.d.ts +0 -298
  105. package/dist/src/tools/screenshot-tools.d.ts.map +0 -1
  106. package/dist/src/tools/screenshot-tools.js +0 -565
  107. package/dist/src/tools/screenshot-tools.js.map +0 -1
  108. package/dist/src/tools/snapshot-tools.d.ts +0 -161
  109. package/dist/src/tools/snapshot-tools.d.ts.map +0 -1
  110. package/dist/src/tools/snapshot-tools.js +0 -479
  111. package/dist/src/tools/snapshot-tools.js.map +0 -1
  112. package/dist/src/utils/image-preprocess.d.ts +0 -49
  113. package/dist/src/utils/image-preprocess.d.ts.map +0 -1
  114. package/dist/src/utils/image-preprocess.js +0 -322
  115. package/dist/src/utils/image-preprocess.js.map +0 -1
  116. package/dist/src/utils/retry.d.ts +0 -21
  117. package/dist/src/utils/retry.d.ts.map +0 -1
  118. package/dist/src/utils/retry.js +0 -33
  119. package/dist/src/utils/retry.js.map +0 -1
  120. package/dist/src/visual/comparator.d.ts +0 -51
  121. package/dist/src/visual/comparator.d.ts.map +0 -1
  122. package/dist/src/visual/comparator.js +0 -119
  123. package/dist/src/visual/comparator.js.map +0 -1
  124. package/dist/src/visual/layout-analyzer.d.ts +0 -64
  125. package/dist/src/visual/layout-analyzer.d.ts.map +0 -1
  126. package/dist/src/visual/layout-analyzer.js +0 -198
  127. package/dist/src/visual/layout-analyzer.js.map +0 -1
  128. package/dist/src/visual/screenshot.d.ts +0 -17
  129. package/dist/src/visual/screenshot.d.ts.map +0 -1
  130. package/dist/src/visual/screenshot.js +0 -39
  131. package/dist/src/visual/screenshot.js.map +0 -1
  132. package/src/config/devlens-config.ts +0 -76
  133. package/src/index.ts +0 -5
  134. package/src/metro/cdp-client.ts +0 -160
  135. package/src/metro/log-collector.ts +0 -137
  136. package/src/metro/metro-bridge.ts +0 -307
  137. package/src/metro/network-inspector.ts +0 -134
  138. package/src/platform/android/adb.ts +0 -200
  139. package/src/platform/android/android-device.ts +0 -116
  140. package/src/platform/android/ui-automator.ts +0 -141
  141. package/src/platform/device-manager.ts +0 -229
  142. package/src/platform/device.ts +0 -110
  143. package/src/platform/ios/accessibility.ts +0 -189
  144. package/src/platform/ios/ios-device.ts +0 -116
  145. package/src/platform/ios/simctl.ts +0 -244
  146. package/src/server.ts +0 -228
  147. package/src/snapshot/formatter.ts +0 -102
  148. package/src/snapshot/ref-registry.ts +0 -230
  149. package/src/snapshot/snapshot-differ.ts +0 -220
  150. package/src/tools/app-tools.ts +0 -111
  151. package/src/tools/device-tools.ts +0 -96
  152. package/src/tools/ds-tools.ts +0 -395
  153. package/src/tools/interaction-tools.ts +0 -467
  154. package/src/tools/metro-tools.ts +0 -320
  155. package/src/tools/navigation-tools.ts +0 -71
  156. package/src/tools/screenshot-tools.ts +0 -698
  157. package/src/tools/snapshot-tools.ts +0 -585
  158. package/src/utils/image-preprocess.ts +0 -430
  159. package/src/utils/retry.ts +0 -51
  160. package/src/visual/comparator.ts +0 -191
  161. package/src/visual/layout-analyzer.ts +0 -283
  162. package/src/visual/screenshot.ts +0 -49
package/src/server.ts DELETED
@@ -1,228 +0,0 @@
1
- import { Server } from "@modelcontextprotocol/sdk/server/index.js";
2
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import {
4
- CallToolRequestSchema,
5
- ListToolsRequestSchema,
6
- } from "@modelcontextprotocol/sdk/types.js";
7
- import { DeviceManager } from "./platform/device-manager.js";
8
- import { RefRegistry } from "./snapshot/ref-registry.js";
9
- import { loadDevLensConfig } from "./config/devlens-config.js";
10
- import { dsToolSchemas, createDsToolHandlers } from "./tools/ds-tools.js";
11
- import {
12
- deviceToolSchemas,
13
- createDeviceToolHandlers,
14
- } from "./tools/device-tools.js";
15
- import {
16
- snapshotToolSchemas,
17
- createSnapshotToolHandlers,
18
- } from "./tools/snapshot-tools.js";
19
- import {
20
- interactionToolSchemas,
21
- createInteractionToolHandlers,
22
- } from "./tools/interaction-tools.js";
23
- import {
24
- screenshotToolSchemas,
25
- createScreenshotToolHandlers,
26
- } from "./tools/screenshot-tools.js";
27
- import {
28
- appToolSchemas,
29
- createAppToolHandlers,
30
- } from "./tools/app-tools.js";
31
- import {
32
- metroToolSchemas,
33
- createMetroToolHandlers,
34
- } from "./tools/metro-tools.js";
35
- import {
36
- navigationToolSchemas,
37
- createNavigationToolHandlers,
38
- } from "./tools/navigation-tools.js";
39
-
40
- export async function startServer(): Promise<Server> {
41
- const server = new Server(
42
- {
43
- name: "devlens-mcp",
44
- version: "0.3.0",
45
- },
46
- {
47
- capabilities: {
48
- tools: {},
49
- },
50
- }
51
- );
52
-
53
- // Load config (from DEVLENS_CONFIG env var or ./devlens.config.json)
54
- const devlensConfig = await loadDevLensConfig();
55
-
56
- // Shared state
57
- const deviceManager = new DeviceManager();
58
- const refRegistry = new RefRegistry();
59
- const metroPort = parseInt(process.env.METRO_PORT || "8081", 10);
60
-
61
- // Create tool handlers
62
- const dsHandlers = createDsToolHandlers(devlensConfig.designSystem);
63
- const deviceHandlers = createDeviceToolHandlers(deviceManager);
64
- const { handlers: snapshotHandlers, resetPreviousSnapshot } =
65
- createSnapshotToolHandlers(deviceManager, refRegistry);
66
- const interactionHandlers = createInteractionToolHandlers(
67
- deviceManager,
68
- refRegistry,
69
- resetPreviousSnapshot
70
- );
71
- const screenshotHandlers = createScreenshotToolHandlers(
72
- deviceManager,
73
- refRegistry
74
- );
75
- const appHandlers = createAppToolHandlers(deviceManager);
76
- const metroHandlers = createMetroToolHandlers(metroPort, deviceManager);
77
- const navigationHandlers = createNavigationToolHandlers(
78
- deviceManager,
79
- refRegistry,
80
- resetPreviousSnapshot
81
- );
82
-
83
- // Merge all tool schemas
84
- const allSchemas: Record<string, { description: string; parameters: any }> = {
85
- ...dsToolSchemas,
86
- ...deviceToolSchemas,
87
- ...snapshotToolSchemas,
88
- ...interactionToolSchemas,
89
- ...screenshotToolSchemas,
90
- ...appToolSchemas,
91
- ...metroToolSchemas,
92
- ...navigationToolSchemas,
93
- };
94
-
95
- // Merge all handlers
96
- const allHandlers: Record<string, (params: any) => Promise<any>> = {
97
- ...dsHandlers,
98
- ...deviceHandlers,
99
- ...snapshotHandlers,
100
- ...interactionHandlers,
101
- ...screenshotHandlers,
102
- ...appHandlers,
103
- ...metroHandlers,
104
- ...navigationHandlers,
105
- };
106
-
107
- // Register tools/list handler
108
- server.setRequestHandler(ListToolsRequestSchema, async () => {
109
- return {
110
- tools: Object.entries(allSchemas).map(([name, schema]) => ({
111
- name,
112
- description: schema.description,
113
- inputSchema: zodToJsonSchema(schema.parameters),
114
- })),
115
- };
116
- });
117
-
118
- // Register tools/call handler
119
- server.setRequestHandler(CallToolRequestSchema, async (request) => {
120
- const { name, arguments: args } = request.params;
121
-
122
- const handler = allHandlers[name];
123
- if (!handler) {
124
- return {
125
- content: [
126
- {
127
- type: "text" as const,
128
- text: `Unknown tool: ${name}`,
129
- },
130
- ],
131
- isError: true,
132
- };
133
- }
134
-
135
- try {
136
- // Validate params with Zod schema
137
- const schema = allSchemas[name];
138
- const validatedArgs = schema.parameters.parse(args || {});
139
- return await handler(validatedArgs);
140
- } catch (error: any) {
141
- return {
142
- content: [
143
- {
144
- type: "text" as const,
145
- text: `Error: ${error.message || String(error)}`,
146
- },
147
- ],
148
- isError: true,
149
- };
150
- }
151
- });
152
-
153
- // Connect via stdio transport
154
- const transport = new StdioServerTransport();
155
- await server.connect(transport);
156
-
157
- return server;
158
- }
159
-
160
- /**
161
- * Convert a Zod schema to JSON Schema for MCP tool registration.
162
- * This is a simplified converter that handles the common cases.
163
- */
164
- function zodToJsonSchema(schema: any): Record<string, any> {
165
- // Use Zod's built-in JSON schema support if available
166
- if (schema._def) {
167
- return convertZodDef(schema._def);
168
- }
169
- return { type: "object", properties: {} };
170
- }
171
-
172
- function convertZodDef(def: any): Record<string, any> {
173
- switch (def.typeName) {
174
- case "ZodObject": {
175
- const properties: Record<string, any> = {};
176
- const required: string[] = [];
177
-
178
- for (const [key, value] of Object.entries(def.shape()) as Array<
179
- [string, any]
180
- >) {
181
- const propDef = value._def;
182
- if (propDef.typeName === "ZodOptional" || propDef.typeName === "ZodDefault") {
183
- properties[key] = convertZodDef(
184
- propDef.typeName === "ZodDefault" ? propDef.innerType._def : propDef.innerType._def
185
- );
186
- } else {
187
- properties[key] = convertZodDef(propDef);
188
- required.push(key);
189
- }
190
- // Add description from Zod describe()
191
- if (propDef.description) {
192
- properties[key].description = propDef.description;
193
- } else if (value.description) {
194
- properties[key].description = value.description;
195
- }
196
- }
197
-
198
- return {
199
- type: "object",
200
- properties,
201
- ...(required.length > 0 ? { required } : {}),
202
- };
203
- }
204
- case "ZodString":
205
- return { type: "string", ...(def.description ? { description: def.description } : {}) };
206
- case "ZodNumber":
207
- return { type: "number", ...(def.description ? { description: def.description } : {}) };
208
- case "ZodBoolean":
209
- return { type: "boolean", ...(def.description ? { description: def.description } : {}) };
210
- case "ZodEnum":
211
- return { type: "string", enum: def.values, ...(def.description ? { description: def.description } : {}) };
212
- case "ZodArray":
213
- return {
214
- type: "array",
215
- items: convertZodDef(def.type._def),
216
- ...(def.description ? { description: def.description } : {}),
217
- };
218
- case "ZodDefault":
219
- return {
220
- ...convertZodDef(def.innerType._def),
221
- default: def.defaultValue(),
222
- };
223
- case "ZodOptional":
224
- return convertZodDef(def.innerType._def);
225
- default:
226
- return {};
227
- }
228
- }
@@ -1,102 +0,0 @@
1
- import type { SnapshotNode } from "../platform/device.js";
2
-
3
- /**
4
- * Formats a SnapshotNode tree into a YAML-like text representation
5
- * that LLMs can easily parse and reason about.
6
- *
7
- * Output format (inspired by Playwright MCP):
8
- *
9
- * - FrameLayout
10
- * - LinearLayout
11
- * - TextView "Hello World" [ref=e1]
12
- * - Button "Submit" [ref=e2]
13
- * - EditText "Email" [ref=e3] [focused]
14
- * - RecyclerView [ref=e4]
15
- * - CardView
16
- * - TextView "Item 1" [ref=e5]
17
- */
18
- export function formatSnapshot(
19
- root: SnapshotNode,
20
- nodeToRef: Map<SnapshotNode, string>
21
- ): string {
22
- const lines: string[] = [];
23
- formatNode(root, nodeToRef, 0, lines);
24
- return lines.join("\n");
25
- }
26
-
27
- function formatNode(
28
- node: SnapshotNode,
29
- nodeToRef: Map<SnapshotNode, string>,
30
- depth: number,
31
- lines: string[]
32
- ): void {
33
- const indent = " ".repeat(depth);
34
- const parts: string[] = [];
35
-
36
- // Type
37
- parts.push(node.type);
38
-
39
- // Text content
40
- if (node.text) {
41
- parts.push(`"${truncate(node.text, 60)}"`);
42
- }
43
-
44
- // Accessibility label (if different from text)
45
- if (node.label && node.label !== node.text) {
46
- parts.push(`label="${truncate(node.label, 40)}"`);
47
- }
48
-
49
- // Annotations
50
- const annotations: string[] = [];
51
-
52
- // Ref
53
- const ref = nodeToRef.get(node);
54
- if (ref) {
55
- annotations.push(`ref=${ref}`);
56
- }
57
-
58
- // State annotations
59
- if (node.focused) annotations.push("focused");
60
- if (node.value) annotations.push(`value="${node.value}"`);
61
- if (node.enabled === false) annotations.push("disabled");
62
- if (node.resourceId) {
63
- const shortId = node.resourceId.includes("/")
64
- ? node.resourceId.split("/").pop()!
65
- : node.resourceId;
66
- annotations.push(`id="${shortId}"`);
67
- }
68
-
69
- if (annotations.length > 0) {
70
- parts.push(`[${annotations.join("] [")}]`);
71
- }
72
-
73
- // Build the line
74
- const line = `${indent}- ${parts.join(" ")}`;
75
-
76
- // Only include nodes that have refs, text, labels, or children with refs
77
- if (ref || node.text || node.label || hasRefInSubtree(node, nodeToRef)) {
78
- lines.push(line);
79
- }
80
-
81
- // Recurse into children
82
- for (const child of node.children) {
83
- formatNode(child, nodeToRef, depth + 1, lines);
84
- }
85
- }
86
-
87
- /** Check if any descendant has a ref assigned */
88
- function hasRefInSubtree(
89
- node: SnapshotNode,
90
- nodeToRef: Map<SnapshotNode, string>
91
- ): boolean {
92
- for (const child of node.children) {
93
- if (nodeToRef.has(child)) return true;
94
- if (hasRefInSubtree(child, nodeToRef)) return true;
95
- }
96
- return false;
97
- }
98
-
99
- function truncate(str: string, maxLen: number): string {
100
- if (str.length <= maxLen) return str;
101
- return str.slice(0, maxLen - 3) + "...";
102
- }
@@ -1,230 +0,0 @@
1
- import type { SnapshotNode, Bounds } from "../platform/device.js";
2
-
3
- /**
4
- * RefRegistry — the Playwright-style ref assignment system.
5
- *
6
- * Assigns monotonically increasing refs (e1, e2, ...) to every interactive
7
- * or meaningful node in the accessibility tree. Maintains a map from ref
8
- * to node info (bounds, center coords) so interaction tools can resolve
9
- * "tap ref=e5" to actual screen coordinates.
10
- */
11
-
12
- export interface RefEntry {
13
- ref: string;
14
- node: SnapshotNode;
15
- centerX: number;
16
- centerY: number;
17
- bounds: Bounds;
18
- }
19
-
20
- export interface ValidationIssue {
21
- type: "zero-size" | "invisible" | "empty-container";
22
- nodeType: string;
23
- text?: string;
24
- label?: string;
25
- resourceId?: string;
26
- bounds: Bounds;
27
- detail: string;
28
- }
29
-
30
- export interface ValidationReport {
31
- issues: ValidationIssue[];
32
- totalNodes: number;
33
- visibleNodes: number;
34
- zeroSizeNodes: number;
35
- interactiveNodes: number;
36
- }
37
-
38
- export class RefRegistry {
39
- private refMap: Map<string, RefEntry> = new Map();
40
- private counter: number = 0;
41
-
42
- /** Clear all refs and reset counter (call before each new snapshot) */
43
- reset(): void {
44
- this.refMap.clear();
45
- this.counter = 0;
46
- }
47
-
48
- /**
49
- * Walk the snapshot tree and assign refs to all interactive or content-bearing nodes.
50
- * Returns a new tree with refs attached (as annotations in the formatter output).
51
- */
52
- assignRefs(root: SnapshotNode): Map<SnapshotNode, string> {
53
- this.reset();
54
- const nodeToRef = new Map<SnapshotNode, string>();
55
- this.walkAndAssign(root, nodeToRef);
56
- return nodeToRef;
57
- }
58
-
59
- private walkAndAssign(
60
- node: SnapshotNode,
61
- nodeToRef: Map<SnapshotNode, string>
62
- ): void {
63
- if (this.shouldAssignRef(node)) {
64
- const ref = `e${++this.counter}`;
65
- const centerX = node.bounds.x + node.bounds.width / 2;
66
- const centerY = node.bounds.y + node.bounds.height / 2;
67
-
68
- this.refMap.set(ref, {
69
- ref,
70
- node,
71
- centerX,
72
- centerY,
73
- bounds: node.bounds,
74
- });
75
- nodeToRef.set(node, ref);
76
- }
77
-
78
- for (const child of node.children) {
79
- this.walkAndAssign(child, nodeToRef);
80
- }
81
- }
82
-
83
- /** Determine if a node should get a ref */
84
- private shouldAssignRef(node: SnapshotNode): boolean {
85
- // Skip invisible/zero-size elements
86
- if (node.bounds.width === 0 || node.bounds.height === 0) return false;
87
-
88
- // Always assign refs to interactive elements
89
- if (node.interactive) return true;
90
-
91
- // Assign refs to elements with meaningful text content
92
- if (node.text && node.text.trim().length > 0) return true;
93
-
94
- // Assign refs to elements with accessibility labels
95
- if (node.label && node.label.trim().length > 0) return true;
96
-
97
- // Skip purely structural containers
98
- return false;
99
- }
100
-
101
- /** Resolve a ref string to its entry (coordinates + node info) */
102
- resolve(ref: string): RefEntry | undefined {
103
- return this.refMap.get(ref);
104
- }
105
-
106
- /** Get all current ref entries */
107
- getAllRefs(): RefEntry[] {
108
- return Array.from(this.refMap.values());
109
- }
110
-
111
- /** Get the total number of assigned refs */
112
- get size(): number {
113
- return this.refMap.size;
114
- }
115
-
116
- /**
117
- * Validate the snapshot tree for visibility issues.
118
- * Flags zero-size elements that should be visible, explicitly invisible elements,
119
- * and empty containers that might indicate missing content.
120
- */
121
- validateTree(root: SnapshotNode): ValidationReport {
122
- const issues: ValidationIssue[] = [];
123
- let totalNodes = 0;
124
- let visibleNodes = 0;
125
- let interactiveNodes = 0;
126
- let zeroSizeNodes = 0;
127
-
128
- const walk = (node: SnapshotNode): void => {
129
- totalNodes++;
130
-
131
- const isZeroSize =
132
- node.bounds.width === 0 || node.bounds.height === 0;
133
-
134
- if (isZeroSize) {
135
- zeroSizeNodes++;
136
- // Only flag if the node has content that SHOULD be visible
137
- if (node.text || node.label || node.interactive) {
138
- issues.push({
139
- type: "zero-size",
140
- nodeType: node.type,
141
- text: node.text,
142
- label: node.label,
143
- resourceId: node.resourceId,
144
- bounds: node.bounds,
145
- detail:
146
- `${node.type}${node.text ? ` "${node.text}"` : ""}${node.label ? ` label="${node.label}"` : ""}` +
147
- ` has zero size (${node.bounds.width}x${node.bounds.height}) — renders as invisible`,
148
- });
149
- }
150
- } else {
151
- visibleNodes++;
152
- }
153
-
154
- if (node.visible === false) {
155
- issues.push({
156
- type: "invisible",
157
- nodeType: node.type,
158
- text: node.text,
159
- label: node.label,
160
- resourceId: node.resourceId,
161
- bounds: node.bounds,
162
- detail:
163
- `${node.type}${node.text ? ` "${node.text}"` : ""} has visible=false`,
164
- });
165
- }
166
-
167
- if (node.interactive) interactiveNodes++;
168
-
169
- // Empty container with a resource ID — likely an intentional component
170
- // that should have content (e.g., missing icon, empty image)
171
- if (
172
- node.children.length === 0 &&
173
- node.resourceId &&
174
- !node.text &&
175
- !node.label &&
176
- !isZeroSize
177
- ) {
178
- issues.push({
179
- type: "empty-container",
180
- nodeType: node.type,
181
- resourceId: node.resourceId,
182
- bounds: node.bounds,
183
- detail:
184
- `${node.type} id="${node.resourceId}" is an empty container (no children, no text) — may be a missing icon or placeholder`,
185
- });
186
- }
187
-
188
- for (const child of node.children) {
189
- walk(child);
190
- }
191
- };
192
-
193
- walk(root);
194
- return { issues, totalNodes, visibleNodes, zeroSizeNodes, interactiveNodes };
195
- }
196
-
197
- /**
198
- * Search the full snapshot tree (including zero-size nodes) for text/label matches.
199
- * Used by find_element to detect invisible elements that the refMap skips.
200
- */
201
- findInTree(
202
- root: SnapshotNode,
203
- query: { text?: string; label?: string }
204
- ): SnapshotNode[] {
205
- const matches: SnapshotNode[] = [];
206
- const walk = (node: SnapshotNode): void => {
207
- const isZeroSize =
208
- node.bounds.width === 0 || node.bounds.height === 0;
209
- if (isZeroSize) {
210
- let matched = false;
211
- if (query.text && node.text) {
212
- matched = node.text
213
- .toLowerCase()
214
- .includes(query.text.toLowerCase());
215
- }
216
- if (query.label && node.label) {
217
- matched =
218
- matched ||
219
- node.label.toLowerCase().includes(query.label.toLowerCase());
220
- }
221
- if (matched) matches.push(node);
222
- }
223
- for (const child of node.children) {
224
- walk(child);
225
- }
226
- };
227
- walk(root);
228
- return matches;
229
- }
230
- }