testchimp-runner-core 0.0.33 → 0.0.35

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 (152) hide show
  1. package/dist/execution-service.d.ts +1 -4
  2. package/dist/execution-service.d.ts.map +1 -1
  3. package/dist/execution-service.js +155 -468
  4. package/dist/execution-service.js.map +1 -1
  5. package/dist/index.d.ts +3 -1
  6. package/dist/index.d.ts.map +1 -1
  7. package/dist/index.js +11 -1
  8. package/dist/index.js.map +1 -1
  9. package/dist/llm-facade.d.ts.map +1 -1
  10. package/dist/llm-facade.js +7 -7
  11. package/dist/llm-facade.js.map +1 -1
  12. package/dist/llm-provider.d.ts +9 -0
  13. package/dist/llm-provider.d.ts.map +1 -1
  14. package/dist/model-constants.d.ts +16 -5
  15. package/dist/model-constants.d.ts.map +1 -1
  16. package/dist/model-constants.js +17 -6
  17. package/dist/model-constants.js.map +1 -1
  18. package/dist/orchestrator/decision-parser.d.ts +18 -0
  19. package/dist/orchestrator/decision-parser.d.ts.map +1 -0
  20. package/dist/orchestrator/decision-parser.js +127 -0
  21. package/dist/orchestrator/decision-parser.js.map +1 -0
  22. package/dist/orchestrator/index.d.ts +4 -2
  23. package/dist/orchestrator/index.d.ts.map +1 -1
  24. package/dist/orchestrator/index.js +15 -2
  25. package/dist/orchestrator/index.js.map +1 -1
  26. package/dist/orchestrator/orchestrator-agent.d.ts +17 -22
  27. package/dist/orchestrator/orchestrator-agent.d.ts.map +1 -1
  28. package/dist/orchestrator/orchestrator-agent.js +708 -577
  29. package/dist/orchestrator/orchestrator-agent.js.map +1 -1
  30. package/dist/orchestrator/orchestrator-prompts.d.ts +32 -0
  31. package/dist/orchestrator/orchestrator-prompts.d.ts.map +1 -0
  32. package/dist/orchestrator/orchestrator-prompts.js +737 -0
  33. package/dist/orchestrator/orchestrator-prompts.js.map +1 -0
  34. package/dist/orchestrator/page-som-handler.d.ts +106 -0
  35. package/dist/orchestrator/page-som-handler.d.ts.map +1 -0
  36. package/dist/orchestrator/page-som-handler.js +1353 -0
  37. package/dist/orchestrator/page-som-handler.js.map +1 -0
  38. package/dist/orchestrator/som-types.d.ts +149 -0
  39. package/dist/orchestrator/som-types.d.ts.map +1 -0
  40. package/dist/orchestrator/som-types.js +87 -0
  41. package/dist/orchestrator/som-types.js.map +1 -0
  42. package/dist/orchestrator/tool-registry.d.ts +2 -0
  43. package/dist/orchestrator/tool-registry.d.ts.map +1 -1
  44. package/dist/orchestrator/tool-registry.js.map +1 -1
  45. package/dist/orchestrator/tools/index.d.ts +5 -1
  46. package/dist/orchestrator/tools/index.d.ts.map +1 -1
  47. package/dist/orchestrator/tools/index.js +9 -2
  48. package/dist/orchestrator/tools/index.js.map +1 -1
  49. package/dist/orchestrator/tools/refresh-som-markers.d.ts +12 -0
  50. package/dist/orchestrator/tools/refresh-som-markers.d.ts.map +1 -0
  51. package/dist/orchestrator/tools/refresh-som-markers.js +64 -0
  52. package/dist/orchestrator/tools/refresh-som-markers.js.map +1 -0
  53. package/dist/orchestrator/tools/verify-action-result.d.ts +17 -0
  54. package/dist/orchestrator/tools/verify-action-result.d.ts.map +1 -0
  55. package/dist/orchestrator/tools/verify-action-result.js +140 -0
  56. package/dist/orchestrator/tools/verify-action-result.js.map +1 -0
  57. package/dist/orchestrator/tools/view-previous-screenshot.d.ts +15 -0
  58. package/dist/orchestrator/tools/view-previous-screenshot.d.ts.map +1 -0
  59. package/dist/orchestrator/tools/view-previous-screenshot.js +92 -0
  60. package/dist/orchestrator/tools/view-previous-screenshot.js.map +1 -0
  61. package/dist/orchestrator/types.d.ts +49 -1
  62. package/dist/orchestrator/types.d.ts.map +1 -1
  63. package/dist/orchestrator/types.js +11 -1
  64. package/dist/orchestrator/types.js.map +1 -1
  65. package/dist/prompts.d.ts.map +1 -1
  66. package/dist/prompts.js +40 -34
  67. package/dist/prompts.js.map +1 -1
  68. package/dist/scenario-service.d.ts +5 -0
  69. package/dist/scenario-service.d.ts.map +1 -1
  70. package/dist/scenario-service.js +17 -0
  71. package/dist/scenario-service.js.map +1 -1
  72. package/dist/scenario-worker-class.d.ts +4 -0
  73. package/dist/scenario-worker-class.d.ts.map +1 -1
  74. package/dist/scenario-worker-class.js +21 -3
  75. package/dist/scenario-worker-class.js.map +1 -1
  76. package/dist/testing/agent-tester.d.ts +35 -0
  77. package/dist/testing/agent-tester.d.ts.map +1 -0
  78. package/dist/testing/agent-tester.js +84 -0
  79. package/dist/testing/agent-tester.js.map +1 -0
  80. package/dist/testing/ref-translator-tester.d.ts +44 -0
  81. package/dist/testing/ref-translator-tester.d.ts.map +1 -0
  82. package/dist/testing/ref-translator-tester.js +104 -0
  83. package/dist/testing/ref-translator-tester.js.map +1 -0
  84. package/dist/utils/coordinate-converter.d.ts +32 -0
  85. package/dist/utils/coordinate-converter.d.ts.map +1 -0
  86. package/dist/utils/coordinate-converter.js +130 -0
  87. package/dist/utils/coordinate-converter.js.map +1 -0
  88. package/dist/utils/hierarchical-selector.d.ts +47 -0
  89. package/dist/utils/hierarchical-selector.d.ts.map +1 -0
  90. package/dist/utils/hierarchical-selector.js +212 -0
  91. package/dist/utils/hierarchical-selector.js.map +1 -0
  92. package/dist/utils/page-info-retry.d.ts +14 -0
  93. package/dist/utils/page-info-retry.d.ts.map +1 -0
  94. package/dist/utils/page-info-retry.js +60 -0
  95. package/dist/utils/page-info-retry.js.map +1 -0
  96. package/dist/utils/page-info-utils.d.ts +1 -0
  97. package/dist/utils/page-info-utils.d.ts.map +1 -1
  98. package/dist/utils/page-info-utils.js +46 -18
  99. package/dist/utils/page-info-utils.js.map +1 -1
  100. package/dist/utils/ref-attacher.d.ts +21 -0
  101. package/dist/utils/ref-attacher.d.ts.map +1 -0
  102. package/dist/utils/ref-attacher.js +149 -0
  103. package/dist/utils/ref-attacher.js.map +1 -0
  104. package/dist/utils/ref-translator.d.ts +49 -0
  105. package/dist/utils/ref-translator.d.ts.map +1 -0
  106. package/dist/utils/ref-translator.js +276 -0
  107. package/dist/utils/ref-translator.js.map +1 -0
  108. package/package.json +1 -1
  109. package/plandocs/BEFORE_AFTER_VERIFICATION.md +148 -0
  110. package/plandocs/COORDINATE_MODE_DIAGNOSIS.md +144 -0
  111. package/plandocs/IMPLEMENTATION_STATUS.md +108 -0
  112. package/plandocs/PHASE_1_COMPLETE.md +165 -0
  113. package/plandocs/PHASE_1_SUMMARY.md +184 -0
  114. package/plandocs/PROMPT_OPTIMIZATION_ANALYSIS.md +120 -0
  115. package/plandocs/PROMPT_SANITY_CHECK.md +120 -0
  116. package/plandocs/SESSION_SUMMARY_v0.0.33.md +151 -0
  117. package/plandocs/TROUBLESHOOTING_SESSION.md +72 -0
  118. package/plandocs/VISUAL_AGENT_EVOLUTION_PLAN.md +396 -0
  119. package/plandocs/WHATS_NEW_v0.0.33.md +183 -0
  120. package/plandocs/exploratory-mode-support-v2.plan.md +953 -0
  121. package/plandocs/exploratory-mode-support.plan.md +928 -0
  122. package/plandocs/journey-id-tracking-addendum.md +227 -0
  123. package/src/execution-service.ts +179 -596
  124. package/src/index.ts +10 -0
  125. package/src/llm-facade.ts +8 -8
  126. package/src/llm-provider.ts +11 -1
  127. package/src/model-constants.ts +17 -5
  128. package/src/orchestrator/decision-parser.ts +139 -0
  129. package/src/orchestrator/index.ts +27 -2
  130. package/src/orchestrator/orchestrator-agent.ts +868 -623
  131. package/src/orchestrator/orchestrator-prompts.ts +786 -0
  132. package/src/orchestrator/page-som-handler.ts +1565 -0
  133. package/src/orchestrator/som-types.ts +188 -0
  134. package/src/orchestrator/tool-registry.ts +2 -0
  135. package/src/orchestrator/tools/index.ts +5 -1
  136. package/src/orchestrator/tools/refresh-som-markers.ts +69 -0
  137. package/src/orchestrator/tools/verify-action-result.ts +159 -0
  138. package/src/orchestrator/tools/view-previous-screenshot.ts +103 -0
  139. package/src/orchestrator/types.ts +95 -4
  140. package/src/prompts.ts +40 -34
  141. package/src/scenario-service.ts +20 -0
  142. package/src/scenario-worker-class.ts +30 -4
  143. package/src/utils/coordinate-converter.ts +162 -0
  144. package/src/utils/page-info-retry.ts +65 -0
  145. package/src/utils/page-info-utils.ts +53 -18
  146. package/testchimp-runner-core-0.0.35.tgz +0 -0
  147. /package/{CREDIT_CALLBACK_ARCHITECTURE.md → plandocs/CREDIT_CALLBACK_ARCHITECTURE.md} +0 -0
  148. /package/{INTEGRATION_COMPLETE.md → plandocs/INTEGRATION_COMPLETE.md} +0 -0
  149. /package/{VISION_DIAGNOSTICS_IMPROVEMENTS.md → plandocs/VISION_DIAGNOSTICS_IMPROVEMENTS.md} +0 -0
  150. /package/{RELEASE_0.0.26.md → releasenotes/RELEASE_0.0.26.md} +0 -0
  151. /package/{RELEASE_0.0.27.md → releasenotes/RELEASE_0.0.27.md} +0 -0
  152. /package/{RELEASE_0.0.28.md → releasenotes/RELEASE_0.0.28.md} +0 -0
@@ -0,0 +1,1353 @@
1
+ "use strict";
2
+ /**
3
+ * PageSoMHandler - Set-of-Marks Handler for Visual Element Identification
4
+ * Manages SoM markers, canvas overlay, and semantic command execution
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.PageSoMHandler = void 0;
8
+ // @ts-nocheck - DOM APIs used in page.evaluate() browser context
9
+ const test_1 = require("@playwright/test");
10
+ const som_types_1 = require("./som-types");
11
+ class PageSoMHandler {
12
+ constructor(page, logger) {
13
+ this.canvasInjected = false;
14
+ this.mutationsList = [];
15
+ this.mutationObserver = null; // MutationObserver only exists in browser context
16
+ this.page = page;
17
+ this.somMap = new Map();
18
+ this.logger = logger;
19
+ }
20
+ setPage(page) {
21
+ this.page = page;
22
+ this.somMap.clear();
23
+ this.canvasInjected = false;
24
+ this.disconnectMutationObserver();
25
+ }
26
+ async disconnectMutationObserver() {
27
+ if (this.mutationObserver || this.page) {
28
+ try {
29
+ // Clean up in page context
30
+ await this.page.evaluate(() => {
31
+ if (window.__tcSomMutationObserver) {
32
+ window.__tcSomMutationObserver.disconnect();
33
+ delete window.__tcSomMutationObserver;
34
+ }
35
+ if (window.__tcSomMutations) {
36
+ delete window.__tcSomMutations;
37
+ }
38
+ });
39
+ }
40
+ catch (error) {
41
+ // Page may be closed or navigated away
42
+ this.logger?.(`[PageSoMHandler] Failed to disconnect mutation observer: ${error}`, 'warn');
43
+ }
44
+ this.mutationObserver = null;
45
+ }
46
+ }
47
+ /**
48
+ * Update SoM markers - extract interactive elements and draw overlay
49
+ * Note: Coordinate markers from previous iteration are NOT removed here
50
+ * They persist through screenshots so agent can see where coords landed
51
+ */
52
+ async updateSom() {
53
+ this.logger?.('[PageSoMHandler] Updating SoM markers...', 'log');
54
+ // Extract interactive elements and assign SoM IDs
55
+ const elements = await this.page.evaluate(() => {
56
+ const doc = document;
57
+ const elements = [];
58
+ let idCounter = 1;
59
+ // Query all interactive elements
60
+ const interactiveSelectors = [
61
+ 'button', 'input', 'textarea', 'select', 'a[href]',
62
+ '[role="button"]', '[role="link"]', '[role="textbox"]',
63
+ '[role="checkbox"]', '[role="radio"]', '[role="combobox"]',
64
+ '[role="menu"]', '[role="menuitem"]', '[role="option"]',
65
+ '[aria-haspopup]', '[onclick]', '[type="submit"]',
66
+ '[role="tab"]', '[role="switch"]', '[role="spinbutton"]'
67
+ ];
68
+ const allInteractive = new Set();
69
+ interactiveSelectors.forEach(selector => {
70
+ doc.querySelectorAll(selector).forEach((el) => allInteractive.add(el));
71
+ });
72
+ // Also detect styled-as-interactive elements
73
+ doc.querySelectorAll('div, span, p, li, td').forEach((el) => {
74
+ const styles = window.getComputedStyle(el);
75
+ const hasClickHandler = el.onclick || el.getAttribute('onclick') ||
76
+ el.hasAttribute('data-action') || el.hasAttribute('data-click');
77
+ if (styles.cursor === 'pointer' || hasClickHandler || el.tabIndex >= 0) {
78
+ allInteractive.add(el);
79
+ }
80
+ });
81
+ // Remove elements that are descendants of "true" interactive elements
82
+ // (e.g., <span> inside <button> should not get separate marker)
83
+ // But keep elements inside generic containers (div, section, etc.)
84
+ const trueInteractiveTags = new Set([
85
+ 'BUTTON', 'A', 'INPUT', 'TEXTAREA', 'SELECT', 'LABEL'
86
+ ]);
87
+ const topLevelInteractive = new Set();
88
+ allInteractive.forEach((el) => {
89
+ let hasInteractiveAncestor = false;
90
+ let parent = el.parentElement;
91
+ // Check if any ancestor is a "true" interactive element
92
+ while (parent) {
93
+ if (allInteractive.has(parent) && trueInteractiveTags.has(parent.tagName)) {
94
+ hasInteractiveAncestor = true;
95
+ break;
96
+ }
97
+ parent = parent.parentElement;
98
+ }
99
+ if (!hasInteractiveAncestor) {
100
+ topLevelInteractive.add(el);
101
+ }
102
+ });
103
+ // Filter to visible, non-occluded, enabled elements
104
+ topLevelInteractive.forEach((el) => {
105
+ const rect = el.getBoundingClientRect();
106
+ // Skip invisible elements (zero size)
107
+ if (rect.width === 0 || rect.height === 0)
108
+ return;
109
+ // Skip hidden elements (display, visibility, opacity checks)
110
+ const styles = window.getComputedStyle(el);
111
+ if (styles.display === 'none' || styles.visibility === 'hidden' ||
112
+ parseFloat(styles.opacity) === 0)
113
+ return;
114
+ // Skip disabled elements (they can't be interacted with)
115
+ const isDisabled = el.disabled ||
116
+ el.hasAttribute('disabled') ||
117
+ el.getAttribute('aria-disabled') === 'true' ||
118
+ el.getAttribute('data-disabled') === 'true' ||
119
+ el.classList?.contains('disabled');
120
+ if (isDisabled)
121
+ return;
122
+ // Z-index occlusion detection: Check if element is fully blocked
123
+ // Sample 5 points: center + 4 corners (slightly inset)
124
+ const centerX = rect.left + rect.width / 2;
125
+ const centerY = rect.top + rect.height / 2;
126
+ // Use smaller inset for small elements (min 1px, max 10% of dimension)
127
+ const inset = Math.max(1, Math.min(rect.width, rect.height) * 0.1);
128
+ const testPoints = [
129
+ { x: centerX, y: centerY }, // center
130
+ { x: rect.left + inset, y: rect.top + inset }, // top-left
131
+ { x: rect.right - inset, y: rect.top + inset }, // top-right
132
+ { x: rect.left + inset, y: rect.bottom - inset }, // bottom-left
133
+ { x: rect.right - inset, y: rect.bottom - inset } // bottom-right
134
+ ];
135
+ // Element is visible if at least 1 test point hits it or a descendant
136
+ // (relaxed from 2 to handle small clickable icons better)
137
+ let visiblePoints = 0;
138
+ for (const point of testPoints) {
139
+ const topEl = document.elementFromPoint(point.x, point.y);
140
+ if (topEl && (topEl === el || el.contains(topEl) || topEl.contains(el))) {
141
+ visiblePoints++;
142
+ }
143
+ }
144
+ // Skip fully occluded elements (no visible points)
145
+ if (visiblePoints < 1) {
146
+ return;
147
+ }
148
+ // Assign tc-som-id attribute
149
+ const somId = String(idCounter++);
150
+ el.setAttribute('tc-som-id', somId);
151
+ // Capture element details
152
+ const parent = el.parentElement;
153
+ elements.push({
154
+ somId,
155
+ tag: el.tagName.toLowerCase(),
156
+ role: el.getAttribute('role') || el.tagName.toLowerCase(),
157
+ text: el.textContent?.trim().substring(0, 50) || '',
158
+ ariaLabel: el.getAttribute('aria-label') || '',
159
+ placeholder: el.placeholder || '',
160
+ name: el.getAttribute('name') || '',
161
+ type: el.type || '',
162
+ id: el.id || '',
163
+ className: el.className || '',
164
+ bbox: {
165
+ x: Math.round(rect.x),
166
+ y: Math.round(rect.y),
167
+ width: Math.round(rect.width),
168
+ height: Math.round(rect.height)
169
+ },
170
+ parent: parent ? {
171
+ tag: parent.tagName.toLowerCase(),
172
+ role: parent.getAttribute('role') || '',
173
+ className: parent.className || '',
174
+ text: parent.textContent?.trim().substring(0, 30) || ''
175
+ } : undefined
176
+ });
177
+ });
178
+ return elements;
179
+ });
180
+ // Store in somMap
181
+ this.somMap.clear();
182
+ elements.forEach(el => this.somMap.set(el.somId, el));
183
+ // Inject/update canvas overlay with bounding boxes
184
+ await this.drawSomOverlay(elements);
185
+ this.logger?.(`[PageSoMHandler] Mapped ${elements.length} interactive elements`, 'log');
186
+ return elements.length;
187
+ }
188
+ /**
189
+ * Draw canvas overlay with bounding boxes and ID labels
190
+ */
191
+ async drawSomOverlay(elements) {
192
+ await this.page.evaluate((els) => {
193
+ const doc = document;
194
+ // Create or get canvas
195
+ let canvas = doc.getElementById('tc-som-canvas');
196
+ if (!canvas) {
197
+ canvas = doc.createElement('canvas');
198
+ canvas.id = 'tc-som-canvas';
199
+ canvas.style.cssText = `
200
+ position: fixed;
201
+ top: 0;
202
+ left: 0;
203
+ width: 100%;
204
+ height: 100%;
205
+ z-index: 2147483647;
206
+ pointer-events: none;
207
+ display: none;
208
+ `;
209
+ doc.body.appendChild(canvas);
210
+ }
211
+ // Set canvas dimensions to viewport size
212
+ canvas.width = window.innerWidth;
213
+ canvas.height = window.innerHeight;
214
+ const ctx = canvas.getContext('2d');
215
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
216
+ // Make canvas visible for debugging (markers shown on live page)
217
+ canvas.style.display = 'block';
218
+ // 20 distinct colors with high contrast for readability
219
+ // Each color chosen to ensure the ID label is clearly visible
220
+ const COLOR_PALETTE = [
221
+ { stroke: '#CC0000', fill: '#FF4444', text: 'white' }, // Bright Red
222
+ { stroke: '#00AA00', fill: '#66FF66', text: 'black' }, // Bright Green
223
+ { stroke: '#0066CC', fill: '#3399FF', text: 'white' }, // Bright Blue
224
+ { stroke: '#FFAA00', fill: '#FFDD66', text: 'black' }, // Bright Orange
225
+ { stroke: '#CC00CC', fill: '#FF66FF', text: 'black' }, // Bright Magenta
226
+ { stroke: '#00AAAA', fill: '#66FFFF', text: 'black' }, // Bright Cyan
227
+ { stroke: '#FF6600', fill: '#FFAA66', text: 'black' }, // Orange-Red
228
+ { stroke: '#6600CC', fill: '#9966FF', text: 'white' }, // Purple
229
+ { stroke: '#00CC66', fill: '#66FFAA', text: 'black' }, // Sea Green
230
+ { stroke: '#FF0066', fill: '#FF66AA', text: 'white' }, // Hot Pink
231
+ { stroke: '#66CC00', fill: '#AAFF66', text: 'black' }, // Lime Green
232
+ { stroke: '#CC6600', fill: '#FFAA44', text: 'black' }, // Dark Orange
233
+ { stroke: '#0099FF', fill: '#66CCFF', text: 'black' }, // Sky Blue
234
+ { stroke: '#FF9999', fill: '#FFDDDD', text: 'black' }, // Light Coral
235
+ { stroke: '#AA5500', fill: '#FF8833', text: 'white' }, // Dark Orange/Brown
236
+ { stroke: '#5555AA', fill: '#8888FF', text: 'white' }, // Slate Blue
237
+ { stroke: '#AA0044', fill: '#FF4488', text: 'white' }, // Raspberry
238
+ { stroke: '#00AA88', fill: '#44FFCC', text: 'black' }, // Turquoise
239
+ { stroke: '#AA44AA', fill: '#DD88DD', text: 'black' }, // Orchid
240
+ { stroke: '#AAAA00', fill: '#FFFF66', text: 'black' } // Yellow-Green
241
+ ];
242
+ // Draw bounding boxes and labels
243
+ els.forEach((el, index) => {
244
+ const { bbox, somId } = el;
245
+ // Assign color based on element index (cycles through 20 colors)
246
+ const colors = COLOR_PALETTE[index % COLOR_PALETTE.length];
247
+ // Draw bounding box
248
+ ctx.strokeStyle = colors.stroke;
249
+ ctx.lineWidth = 3;
250
+ ctx.strokeRect(bbox.x, bbox.y, bbox.width, bbox.height);
251
+ // Draw ID label at TOP-RIGHT corner, OUTSIDE above the bounding box
252
+ // This prevents obscuring element content (text, icons, etc.)
253
+ const labelText = somId;
254
+ ctx.font = 'bold 14px Arial';
255
+ const textMetrics = ctx.measureText(labelText);
256
+ const textWidth = textMetrics.width;
257
+ const textHeight = 14;
258
+ const padding = 3;
259
+ const labelBoxWidth = textWidth + padding * 2;
260
+ const labelBoxHeight = textHeight + padding;
261
+ // Position label at top-right corner, ABOVE the bounding box
262
+ // Label is right-aligned with bbox, bottom of label touches top of bbox
263
+ const labelX = bbox.x + bbox.width - labelBoxWidth;
264
+ let labelY = bbox.y - labelBoxHeight;
265
+ // If no room above, place below the bounding box instead
266
+ if (labelY < 0) {
267
+ labelY = bbox.y + bbox.height;
268
+ }
269
+ // Draw label background
270
+ ctx.fillStyle = colors.fill;
271
+ ctx.fillRect(labelX, labelY, labelBoxWidth, labelBoxHeight);
272
+ // Draw label text
273
+ ctx.fillStyle = colors.text;
274
+ ctx.textBaseline = 'top';
275
+ ctx.fillText(labelText, labelX + padding, labelY + padding / 2);
276
+ ctx.textBaseline = 'alphabetic'; // Reset
277
+ });
278
+ }, elements);
279
+ this.canvasInjected = true;
280
+ }
281
+ /**
282
+ * Get formatted SoM element map for agent context
283
+ * Returns concise, truncated element details to help disambiguate similar elements
284
+ */
285
+ getSomElementMap() {
286
+ if (this.somMap.size === 0) {
287
+ return 'No SoM elements mapped yet.';
288
+ }
289
+ const lines = [];
290
+ // Sort by somId (numeric)
291
+ const sortedEntries = Array.from(this.somMap.entries()).sort((a, b) => {
292
+ return parseInt(a[0]) - parseInt(b[0]);
293
+ });
294
+ for (const [somId, element] of sortedEntries) {
295
+ const parts = [];
296
+ // Tag (always present)
297
+ parts.push(element.tag);
298
+ // Text (truncate to 40 chars)
299
+ if (element.text && element.text.trim()) {
300
+ const truncated = element.text.trim().substring(0, 40);
301
+ parts.push(`"${truncated}${element.text.length > 40 ? '...' : ''}"`);
302
+ }
303
+ // Build attributes string
304
+ const attrs = [];
305
+ if (element.ariaLabel) {
306
+ attrs.push(`aria: "${element.ariaLabel.substring(0, 30)}"`);
307
+ }
308
+ if (element.placeholder) {
309
+ attrs.push(`placeholder: "${element.placeholder.substring(0, 30)}"`);
310
+ }
311
+ if (element.type && element.type !== 'text') {
312
+ attrs.push(`type: "${element.type}"`);
313
+ }
314
+ if (element.role && element.role !== 'generic') {
315
+ attrs.push(`role: "${element.role}"`);
316
+ }
317
+ if (element.name) {
318
+ attrs.push(`name: "${element.name.substring(0, 20)}"`);
319
+ }
320
+ // Combine: [1]: button "Submit" (aria: "submit-form", type: "submit")
321
+ const attrStr = attrs.length > 0 ? ` (${attrs.join(', ')})` : '';
322
+ lines.push(`[${somId}]: ${parts.join(' ')}${attrStr}`);
323
+ }
324
+ return lines.join('\n');
325
+ }
326
+ /**
327
+ * Get screenshot with optional SoM markers
328
+ */
329
+ async getScreenshot(includeSomMarkers, fullPage = false, quality = 60) {
330
+ // Show/hide canvas overlay
331
+ await this.page.evaluate((show) => {
332
+ const canvas = document.getElementById('tc-som-canvas');
333
+ if (canvas)
334
+ canvas.style.display = show ? 'block' : 'none';
335
+ }, includeSomMarkers);
336
+ // Capture screenshot
337
+ const buffer = await this.page.screenshot({
338
+ fullPage,
339
+ type: 'jpeg',
340
+ quality
341
+ });
342
+ // Keep markers visible for debugging - don't hide them
343
+ // Only hide if specifically requested (includeSomMarkers = false)
344
+ if (!includeSomMarkers) {
345
+ await this.page.evaluate(() => {
346
+ const canvas = document.getElementById('tc-som-canvas');
347
+ if (canvas)
348
+ canvas.style.display = 'none';
349
+ });
350
+ }
351
+ return `data:image/jpeg;base64,${buffer.toString('base64')}`;
352
+ }
353
+ /**
354
+ * Run command with semantic selector generation and coordinate fallback
355
+ */
356
+ async runCommand(command, useSomIdBasedCommands = false) {
357
+ const failedAttempts = [];
358
+ const maxAttempts = 7; // Error budget per command
359
+ // Handle navigation actions (no element/coord required)
360
+ if (command.action === som_types_1.InteractionAction.NAVIGATE ||
361
+ command.action === som_types_1.InteractionAction.GO_BACK ||
362
+ command.action === som_types_1.InteractionAction.GO_FORWARD ||
363
+ command.action === som_types_1.InteractionAction.RELOAD) {
364
+ this.logger?.(`[PageSoMHandler] Executing navigation action: ${command.action}`, 'log');
365
+ try {
366
+ let playwrightCommand = '';
367
+ switch (command.action) {
368
+ case som_types_1.InteractionAction.NAVIGATE:
369
+ if (!command.value) {
370
+ throw new Error('NAVIGATE action requires URL in value field');
371
+ }
372
+ await this.page.goto(command.value, { waitUntil: 'networkidle', timeout: 30000 });
373
+ playwrightCommand = `await page.goto('${command.value}')`;
374
+ break;
375
+ case som_types_1.InteractionAction.GO_BACK:
376
+ await this.page.goBack({ waitUntil: 'networkidle', timeout: 30000 });
377
+ playwrightCommand = 'await page.goBack()';
378
+ break;
379
+ case som_types_1.InteractionAction.GO_FORWARD:
380
+ await this.page.goForward({ waitUntil: 'networkidle', timeout: 30000 });
381
+ playwrightCommand = 'await page.goForward()';
382
+ break;
383
+ case som_types_1.InteractionAction.RELOAD:
384
+ await this.page.reload({ waitUntil: 'networkidle', timeout: 30000 });
385
+ playwrightCommand = 'await page.reload()';
386
+ break;
387
+ }
388
+ return {
389
+ failedAttempts: [],
390
+ successAttempt: {
391
+ command: playwrightCommand,
392
+ status: som_types_1.CommandRunStatus.SUCCESS
393
+ },
394
+ status: som_types_1.CommandRunStatus.SUCCESS
395
+ };
396
+ }
397
+ catch (error) {
398
+ return {
399
+ failedAttempts: [{
400
+ command: `Navigation action: ${command.action}`,
401
+ status: som_types_1.CommandRunStatus.FAILURE,
402
+ error: error.message
403
+ }],
404
+ error: error.message,
405
+ status: som_types_1.CommandRunStatus.FAILURE
406
+ };
407
+ }
408
+ }
409
+ // Handle direct percentage-based coordinate commands
410
+ if (command.coord && !command.elementRef) {
411
+ this.logger?.(`[PageSoMHandler] Executing ${command.action} at percentage coords (${command.coord.x}%, ${command.coord.y}%)`, 'log');
412
+ try {
413
+ const result = await this.executePercentageCoordinateAction(command);
414
+ return {
415
+ failedAttempts: [],
416
+ successAttempt: {
417
+ command: `Coordinate action: ${command.action} at (${command.coord.x}%, ${command.coord.y}%)`,
418
+ status: som_types_1.CommandRunStatus.SUCCESS
419
+ },
420
+ status: som_types_1.CommandRunStatus.SUCCESS
421
+ };
422
+ }
423
+ catch (error) {
424
+ return {
425
+ failedAttempts: [{
426
+ command: `Coordinate action: ${command.action} at (${command.coord.x}%, ${command.coord.y}%)`,
427
+ status: som_types_1.CommandRunStatus.FAILURE,
428
+ error: error.message
429
+ }],
430
+ error: error.message,
431
+ status: som_types_1.CommandRunStatus.FAILURE
432
+ };
433
+ }
434
+ }
435
+ // Lookup element in SoM map
436
+ if (!command.elementRef) {
437
+ return {
438
+ failedAttempts: [],
439
+ error: 'Command must have either elementRef or coord specified',
440
+ status: som_types_1.CommandRunStatus.FAILURE
441
+ };
442
+ }
443
+ const element = this.somMap.get(command.elementRef);
444
+ if (!element) {
445
+ return {
446
+ failedAttempts: [],
447
+ error: `Element with SoM ID "${command.elementRef}" not found in map`,
448
+ status: som_types_1.CommandRunStatus.FAILURE
449
+ };
450
+ }
451
+ this.logger?.(`[PageSoMHandler] Executing ${command.action} on element ${command.elementRef}`, 'log');
452
+ // Clear mutations list before command execution
453
+ this.mutationsList = [];
454
+ // Setup mutation observer for hover/focus actions
455
+ if (command.action === som_types_1.InteractionAction.HOVER || command.action === som_types_1.InteractionAction.FOCUS) {
456
+ await this.setupMutationObserver();
457
+ }
458
+ // Try semantic selectors first (unless useSomIdBasedCommands=true)
459
+ if (!useSomIdBasedCommands) {
460
+ const selectors = this.generateSemanticSelectors(element);
461
+ for (let i = 0; i < Math.min(selectors.length, maxAttempts); i++) {
462
+ const selector = selectors[i];
463
+ const result = await this.tryExecuteAction(selector, command, element);
464
+ if (result.status === som_types_1.CommandRunStatus.SUCCESS) {
465
+ // Wait for mutations if hover/focus
466
+ if (command.action === som_types_1.InteractionAction.HOVER || command.action === som_types_1.InteractionAction.FOCUS) {
467
+ await this.page.waitForTimeout(500); // Wait for mutations to settle
468
+ result.mutations = await this.filterRelevantMutations();
469
+ }
470
+ await this.disconnectMutationObserver();
471
+ return result;
472
+ }
473
+ if (result.failedAttempts.length > 0) {
474
+ failedAttempts.push(result.failedAttempts[0]);
475
+ }
476
+ }
477
+ }
478
+ else {
479
+ // Use tc-som-id attribute directly
480
+ const somIdSelector = {
481
+ type: 'locator',
482
+ value: `[tc-som-id="${command.elementRef}"]`
483
+ };
484
+ const result = await this.tryExecuteAction(somIdSelector, command, element);
485
+ if (result.status === som_types_1.CommandRunStatus.SUCCESS) {
486
+ await this.disconnectMutationObserver();
487
+ return result;
488
+ }
489
+ if (result.failedAttempts.length > 0) {
490
+ failedAttempts.push(result.failedAttempts[0]);
491
+ }
492
+ }
493
+ // Fallback to coordinates (more replayable)
494
+ this.logger?.(`[PageSoMHandler] Semantic selectors exhausted (${failedAttempts.length} attempts), falling back to coordinates`, 'warn');
495
+ const coordResult = await this.executeCoordinateAction(command, element);
496
+ await this.disconnectMutationObserver();
497
+ // Add failed attempts to coordinate result
498
+ if (coordResult.status === som_types_1.CommandRunStatus.SUCCESS) {
499
+ coordResult.failedAttempts = failedAttempts;
500
+ }
501
+ return coordResult;
502
+ }
503
+ /**
504
+ * Execute verification command and generate Playwright expect assertion
505
+ * Uses semantic selectors (reuses generateSemanticSelectors) for script portability
506
+ */
507
+ async executeVerification(verification) {
508
+ try {
509
+ let locatorStr = '';
510
+ let locator;
511
+ let element;
512
+ // Build locator using semantic selectors (like runCommand does)
513
+ if (verification.elementRef) {
514
+ element = this.somMap.get(verification.elementRef);
515
+ if (!element) {
516
+ return {
517
+ success: false,
518
+ playwrightCommand: '',
519
+ error: `Element with SoM ID ${verification.elementRef} not found`
520
+ };
521
+ }
522
+ // Generate semantic selectors (reuse existing logic)
523
+ const semanticSelectors = this.generateSemanticSelectors(element);
524
+ if (semanticSelectors.length === 0) {
525
+ return {
526
+ success: false,
527
+ playwrightCommand: '',
528
+ error: `No semantic selectors available for element ${verification.elementRef}`
529
+ };
530
+ }
531
+ // Try selectors in priority order to find working one
532
+ let workingSelector = null;
533
+ for (const typedSelector of semanticSelectors) {
534
+ try {
535
+ const testLocator = this.buildLocatorFromTypedSelector(typedSelector);
536
+ await testLocator.first().waitFor({ state: 'attached', timeout: 1000 });
537
+ workingSelector = typedSelector;
538
+ break;
539
+ }
540
+ catch {
541
+ continue;
542
+ }
543
+ }
544
+ if (!workingSelector) {
545
+ // Fall back to first selector even if not confirmed working
546
+ workingSelector = semanticSelectors[0];
547
+ this.logger?.(`[PageSoMHandler] No confirmed working selector, using first: ${this.formatSelector(workingSelector)}`, 'warn');
548
+ }
549
+ locator = this.buildLocatorFromTypedSelector(workingSelector);
550
+ locatorStr = this.formatSelector(workingSelector);
551
+ }
552
+ else if (verification.selector) {
553
+ // Direct CSS selector (for count verifications on non-SoM elements)
554
+ locatorStr = `page.locator('${verification.selector}')`;
555
+ locator = this.page.locator(verification.selector);
556
+ }
557
+ else {
558
+ return {
559
+ success: false,
560
+ playwrightCommand: '',
561
+ error: 'Either elementRef or selector required for verification'
562
+ };
563
+ }
564
+ let expectCommand = '';
565
+ let verificationPassed = true;
566
+ // Generate Playwright command string FIRST (before executing, so we have it even if assertion fails)
567
+ switch (verification.verificationType) {
568
+ case som_types_1.VerificationType.TEXT_CONTAINS:
569
+ expectCommand = `await expect(${locatorStr}).toContainText('${verification.expected}')`;
570
+ break;
571
+ case som_types_1.VerificationType.TEXT_EQUALS:
572
+ expectCommand = `await expect(${locatorStr}).toHaveText('${verification.expected}')`;
573
+ break;
574
+ case som_types_1.VerificationType.VALUE_EQUALS:
575
+ expectCommand = `await expect(${locatorStr}).toHaveValue('${verification.expected}')`;
576
+ break;
577
+ case som_types_1.VerificationType.VALUE_EMPTY:
578
+ expectCommand = `await expect(${locatorStr}).toHaveValue('')`;
579
+ break;
580
+ case som_types_1.VerificationType.IS_VISIBLE:
581
+ expectCommand = `await expect(${locatorStr}).toBeVisible()`;
582
+ break;
583
+ case som_types_1.VerificationType.IS_HIDDEN:
584
+ expectCommand = `await expect(${locatorStr}).toBeHidden()`;
585
+ break;
586
+ case som_types_1.VerificationType.IS_ENABLED:
587
+ expectCommand = `await expect(${locatorStr}).toBeEnabled()`;
588
+ break;
589
+ case som_types_1.VerificationType.IS_DISABLED:
590
+ expectCommand = `await expect(${locatorStr}).toBeDisabled()`;
591
+ break;
592
+ case som_types_1.VerificationType.IS_CHECKED:
593
+ expectCommand = `await expect(${locatorStr}).toBeChecked()`;
594
+ break;
595
+ case som_types_1.VerificationType.IS_UNCHECKED:
596
+ expectCommand = `await expect(${locatorStr}).not.toBeChecked()`;
597
+ break;
598
+ case som_types_1.VerificationType.COUNT_EQUALS:
599
+ expectCommand = `await expect(${locatorStr}).toHaveCount(${verification.expected})`;
600
+ break;
601
+ case som_types_1.VerificationType.COUNT_GREATER_THAN:
602
+ expectCommand = `expect(await ${locatorStr}.count()).toBeGreaterThan(${verification.expected})`;
603
+ break;
604
+ case som_types_1.VerificationType.COUNT_LESS_THAN:
605
+ expectCommand = `expect(await ${locatorStr}.count()).toBeLessThan(${verification.expected})`;
606
+ break;
607
+ case som_types_1.VerificationType.HAS_CLASS:
608
+ expectCommand = `await expect(${locatorStr}).toHaveClass(/${verification.expected}/)`;
609
+ break;
610
+ case som_types_1.VerificationType.HAS_ATTRIBUTE:
611
+ // Format: "attr:value" or just "attr" (use colon to avoid = in values)
612
+ const parts = verification.expected.split(':', 2);
613
+ const attr = parts[0];
614
+ const value = parts[1];
615
+ if (value) {
616
+ expectCommand = `await expect(${locatorStr}).toHaveAttribute('${attr}', '${value}')`;
617
+ }
618
+ else {
619
+ expectCommand = `await expect(${locatorStr}).toHaveAttribute('${attr}')`;
620
+ }
621
+ break;
622
+ default:
623
+ return {
624
+ success: false,
625
+ playwrightCommand: '',
626
+ error: `Unknown verification type: ${verification.verificationType}`
627
+ };
628
+ }
629
+ // Now execute the assertion (may fail, but we already have the command string)
630
+ try {
631
+ switch (verification.verificationType) {
632
+ case som_types_1.VerificationType.TEXT_CONTAINS:
633
+ await (0, test_1.expect)(locator).toContainText(verification.expected, { timeout: 5000 });
634
+ break;
635
+ case som_types_1.VerificationType.TEXT_EQUALS:
636
+ await (0, test_1.expect)(locator).toHaveText(verification.expected, { timeout: 5000 });
637
+ break;
638
+ case som_types_1.VerificationType.VALUE_EQUALS:
639
+ await (0, test_1.expect)(locator).toHaveValue(verification.expected, { timeout: 5000 });
640
+ break;
641
+ case som_types_1.VerificationType.VALUE_EMPTY:
642
+ await (0, test_1.expect)(locator).toHaveValue('', { timeout: 5000 });
643
+ break;
644
+ case som_types_1.VerificationType.IS_VISIBLE:
645
+ await (0, test_1.expect)(locator).toBeVisible({ timeout: 5000 });
646
+ break;
647
+ case som_types_1.VerificationType.IS_HIDDEN:
648
+ await (0, test_1.expect)(locator).toBeHidden({ timeout: 5000 });
649
+ break;
650
+ case som_types_1.VerificationType.IS_ENABLED:
651
+ await (0, test_1.expect)(locator).toBeEnabled({ timeout: 5000 });
652
+ break;
653
+ case som_types_1.VerificationType.IS_DISABLED:
654
+ await (0, test_1.expect)(locator).toBeDisabled({ timeout: 5000 });
655
+ break;
656
+ case som_types_1.VerificationType.IS_CHECKED:
657
+ await (0, test_1.expect)(locator).toBeChecked({ timeout: 5000 });
658
+ break;
659
+ case som_types_1.VerificationType.IS_UNCHECKED:
660
+ await (0, test_1.expect)(locator).not.toBeChecked({ timeout: 5000 });
661
+ break;
662
+ case som_types_1.VerificationType.COUNT_EQUALS:
663
+ await (0, test_1.expect)(locator).toHaveCount(verification.expected, { timeout: 5000 });
664
+ break;
665
+ case som_types_1.VerificationType.COUNT_GREATER_THAN:
666
+ const countGT = await locator.count();
667
+ (0, test_1.expect)(countGT).toBeGreaterThan(verification.expected);
668
+ break;
669
+ case som_types_1.VerificationType.COUNT_LESS_THAN:
670
+ const countLT = await locator.count();
671
+ (0, test_1.expect)(countLT).toBeLessThan(verification.expected);
672
+ break;
673
+ case som_types_1.VerificationType.HAS_CLASS:
674
+ await (0, test_1.expect)(locator).toHaveClass(new RegExp(verification.expected), { timeout: 5000 });
675
+ break;
676
+ case som_types_1.VerificationType.HAS_ATTRIBUTE:
677
+ const parts = verification.expected.split(':', 2);
678
+ const attr = parts[0];
679
+ const value = parts[1];
680
+ if (value) {
681
+ await (0, test_1.expect)(locator).toHaveAttribute(attr, value, { timeout: 5000 });
682
+ }
683
+ else {
684
+ await (0, test_1.expect)(locator).toHaveAttribute(attr, { timeout: 5000 });
685
+ }
686
+ break;
687
+ }
688
+ }
689
+ catch (assertionError) {
690
+ // Assertion failed but we still want the command in the script
691
+ verificationPassed = false;
692
+ this.logger?.(`[PageSoMHandler] Verification assertion failed (non-fatal): ${assertionError.message}`, 'warn');
693
+ }
694
+ // Always return the command (even if assertion failed) - scripts need the expect
695
+ return {
696
+ success: verificationPassed,
697
+ playwrightCommand: expectCommand,
698
+ error: verificationPassed ? undefined : 'Assertion failed during generation'
699
+ };
700
+ }
701
+ catch (error) {
702
+ // Could not build command at all (selector not found, etc.)
703
+ return {
704
+ success: false,
705
+ playwrightCommand: '',
706
+ error: error.message
707
+ };
708
+ }
709
+ }
710
+ /**
711
+ * Generate typed semantic selectors from element details (no string parsing needed)
712
+ */
713
+ generateSemanticSelectors(element) {
714
+ const selectors = [];
715
+ // Priority 1: getByTestId (would need to capture in updateSom)
716
+ // Skipped for now - can add data-testid capture later
717
+ // Priority 2: Stable ID
718
+ if (element.id && !element.id.match(/^(rc_|:r[0-9]+:|__)/) && !element.id.includes('«')) {
719
+ selectors.push({ type: 'id', value: element.id });
720
+ }
721
+ // Priority 3: getByLabel
722
+ if (element.ariaLabel) {
723
+ selectors.push({ type: 'label', value: element.ariaLabel });
724
+ }
725
+ // Priority 4: getByRole
726
+ if (element.role && element.text) {
727
+ selectors.push({ type: 'role', value: element.role, roleOptions: { name: element.text } });
728
+ }
729
+ // Priority 5: getByPlaceholder
730
+ if (element.placeholder) {
731
+ selectors.push({ type: 'placeholder', value: element.placeholder });
732
+ }
733
+ // Priority 6: input[name]
734
+ if (element.name && ['input', 'textarea', 'select'].includes(element.tag)) {
735
+ selectors.push({ type: 'name', value: element.name });
736
+ }
737
+ // Priority 7: getByText
738
+ if (element.text) {
739
+ selectors.push({ type: 'text', value: element.text });
740
+ }
741
+ // Priority 8: Parent-scoped locator (generic Playwright selector)
742
+ if (element.parent?.className) {
743
+ const parentClass = element.parent.className.split(' ')[0];
744
+ if (parentClass) {
745
+ selectors.push({ type: 'locator', value: `.${parentClass} ${element.tag}` });
746
+ }
747
+ }
748
+ return selectors;
749
+ }
750
+ /**
751
+ * Try executing action with a typed selector (no string parsing)
752
+ */
753
+ async tryExecuteAction(typedSelector, command, element) {
754
+ // Format selector description for both success and error paths
755
+ const selectorDesc = this.formatSelector(typedSelector);
756
+ try {
757
+ // Build locator with optional parent chaining
758
+ const locator = this.buildLocatorFromTypedSelector(typedSelector);
759
+ // Execute action based on type
760
+ const playwrightCommand = await this.executeActionOnLocator(locator, command, selectorDesc);
761
+ return {
762
+ failedAttempts: [],
763
+ successAttempt: {
764
+ command: playwrightCommand,
765
+ status: som_types_1.CommandRunStatus.SUCCESS
766
+ },
767
+ status: som_types_1.CommandRunStatus.SUCCESS
768
+ };
769
+ }
770
+ catch (error) {
771
+ this.logger?.(`[PageSoMHandler] Selector "${selectorDesc}" failed: ${error.message}`, 'warn');
772
+ // Apply error-specific heuristics
773
+ return await this.applyHeuristicsAndRetry(typedSelector, command, element, error);
774
+ }
775
+ }
776
+ /**
777
+ * Apply intelligent heuristics based on error type and retry
778
+ */
779
+ async applyHeuristicsAndRetry(typedSelector, command, element, error) {
780
+ const errorMsg = error.message || String(error);
781
+ const selectorDesc = this.formatSelector(typedSelector);
782
+ // Heuristic 1: Strict mode violation → Try parent scoping
783
+ if (errorMsg.includes('strict mode violation')) {
784
+ if (typedSelector.type === 'locator' && element.parent?.className) {
785
+ this.logger?.(`[PageSoMHandler] Heuristic: Strict mode → trying parent scoping`, 'log');
786
+ return await this.tryRefinedSelector(typedSelector, command, element, error);
787
+ }
788
+ // Could add nth() selector here based on bbox position
789
+ }
790
+ // Heuristic 2: Timeout → Quick retry with scroll (once only, don't get stuck)
791
+ if (errorMsg.includes('Timeout') && !errorMsg.includes('not enabled')) {
792
+ this.logger?.(`[PageSoMHandler] Heuristic: Timeout → quick scroll + retry (once)`, 'log');
793
+ try {
794
+ const locator = this.buildLocatorFromTypedSelector(typedSelector);
795
+ // Try scroll (if element exists but not in viewport)
796
+ await locator.scrollIntoViewIfNeeded({ timeout: 1000 }).catch(() => { });
797
+ await this.page.waitForTimeout(500); // Reduced wait
798
+ const playwrightCommand = await this.executeActionOnLocator(locator, command, selectorDesc);
799
+ return {
800
+ failedAttempts: [{
801
+ command: selectorDesc,
802
+ status: som_types_1.CommandRunStatus.FAILURE,
803
+ error: 'Timeout (first attempt)'
804
+ }],
805
+ successAttempt: {
806
+ command: `${playwrightCommand} (after scroll)`,
807
+ status: som_types_1.CommandRunStatus.SUCCESS
808
+ },
809
+ status: som_types_1.CommandRunStatus.SUCCESS
810
+ };
811
+ }
812
+ catch (retryError) {
813
+ // Element likely doesn't exist or selector is wrong - move on to next selector
814
+ this.logger?.(`[PageSoMHandler] Scroll retry failed, moving to next selector`, 'log');
815
+ }
816
+ }
817
+ // Heuristic 3: Not enabled/not actionable → Try force flag
818
+ if (errorMsg.includes('not enabled') || errorMsg.includes('not editable') || errorMsg.includes('not actionable')) {
819
+ this.logger?.(`[PageSoMHandler] Heuristic: Not actionable → trying force flag`, 'log');
820
+ try {
821
+ const locator = this.buildLocatorFromTypedSelector(typedSelector);
822
+ const modifiedCommand = { ...command, force: true };
823
+ const playwrightCommand = await this.executeActionOnLocator(locator, modifiedCommand, selectorDesc);
824
+ return {
825
+ failedAttempts: [{
826
+ command: selectorDesc,
827
+ status: som_types_1.CommandRunStatus.FAILURE,
828
+ error: errorMsg
829
+ }],
830
+ successAttempt: {
831
+ command: `${playwrightCommand} (with force)`,
832
+ status: som_types_1.CommandRunStatus.SUCCESS
833
+ },
834
+ status: som_types_1.CommandRunStatus.SUCCESS
835
+ };
836
+ }
837
+ catch (forceError) {
838
+ // Force didn't work
839
+ }
840
+ }
841
+ // Heuristic 4: Element moving/detached → Wait for stability
842
+ if (errorMsg.includes('detached') || errorMsg.includes('moving')) {
843
+ this.logger?.(`[PageSoMHandler] Heuristic: Element unstable → waiting for stability`, 'log');
844
+ try {
845
+ await this.page.waitForLoadState('domcontentloaded');
846
+ await this.page.waitForTimeout(1000);
847
+ const locator = this.buildLocatorFromTypedSelector(typedSelector);
848
+ const playwrightCommand = await this.executeActionOnLocator(locator, command, selectorDesc);
849
+ return {
850
+ failedAttempts: [{
851
+ command: selectorDesc,
852
+ status: som_types_1.CommandRunStatus.FAILURE,
853
+ error: errorMsg
854
+ }],
855
+ successAttempt: {
856
+ command: `${playwrightCommand} (after stability wait)`,
857
+ status: som_types_1.CommandRunStatus.SUCCESS
858
+ },
859
+ status: som_types_1.CommandRunStatus.SUCCESS
860
+ };
861
+ }
862
+ catch (stabilityError) {
863
+ // Stability wait didn't help
864
+ }
865
+ }
866
+ // No heuristics worked - return failure
867
+ return {
868
+ failedAttempts: [{
869
+ command: selectorDesc,
870
+ status: som_types_1.CommandRunStatus.FAILURE,
871
+ error: errorMsg
872
+ }],
873
+ status: som_types_1.CommandRunStatus.FAILURE
874
+ };
875
+ }
876
+ /**
877
+ * Build Playwright locator from typed selector (supports chaining)
878
+ */
879
+ buildLocatorFromTypedSelector(typedSelector) {
880
+ // Get base locator (page or parent)
881
+ const base = typedSelector.parent
882
+ ? this.buildLocatorFromTypedSelector(typedSelector.parent)
883
+ : this.page;
884
+ // Build locator from base
885
+ switch (typedSelector.type) {
886
+ case 'id':
887
+ return base.locator(`#${typedSelector.value}`);
888
+ case 'testId':
889
+ return base.getByTestId(typedSelector.value);
890
+ case 'label':
891
+ return base.getByLabel(typedSelector.value);
892
+ case 'role':
893
+ return base.getByRole(typedSelector.value, typedSelector.roleOptions);
894
+ case 'placeholder':
895
+ return base.getByPlaceholder(typedSelector.value);
896
+ case 'text':
897
+ return base.getByText(typedSelector.value);
898
+ case 'title':
899
+ return base.getByTitle(typedSelector.value);
900
+ case 'altText':
901
+ return base.getByAltText(typedSelector.value);
902
+ case 'name':
903
+ return base.locator(`[name="${typedSelector.value}"]`);
904
+ case 'locator':
905
+ // Generic locator - supports CSS, text=, has-text=, chaining, etc.
906
+ return base.locator(typedSelector.value);
907
+ default:
908
+ throw new Error(`Unknown selector type: ${typedSelector.type}`);
909
+ }
910
+ }
911
+ /**
912
+ * Format typed selector for logging (supports chaining)
913
+ */
914
+ formatSelector(sel) {
915
+ let formatted;
916
+ switch (sel.type) {
917
+ case 'id':
918
+ formatted = `#${sel.value}`;
919
+ break;
920
+ case 'testId':
921
+ formatted = `getByTestId('${sel.value}')`;
922
+ break;
923
+ case 'label':
924
+ formatted = `getByLabel('${sel.value}')`;
925
+ break;
926
+ case 'role':
927
+ formatted = `getByRole('${sel.value}', {name: '${sel.roleOptions?.name}'})`;
928
+ break;
929
+ case 'placeholder':
930
+ formatted = `getByPlaceholder('${sel.value}')`;
931
+ break;
932
+ case 'text':
933
+ formatted = `getByText('${sel.value}')`;
934
+ break;
935
+ case 'title':
936
+ formatted = `getByTitle('${sel.value}')`;
937
+ break;
938
+ case 'altText':
939
+ formatted = `getByAltText('${sel.value}')`;
940
+ break;
941
+ case 'name':
942
+ formatted = `[name="${sel.value}"]`;
943
+ break;
944
+ case 'locator':
945
+ formatted = sel.value;
946
+ break;
947
+ default: formatted = String(sel.value);
948
+ }
949
+ // Add parent chain if exists
950
+ if (sel.parent) {
951
+ return `${this.formatSelector(sel.parent)}.locator(${formatted})`;
952
+ }
953
+ return formatted;
954
+ }
955
+ /**
956
+ * Execute action on locator based on command type
957
+ */
958
+ async executeActionOnLocator(locator, command, selectorDesc) {
959
+ const { action, value, force, scrollAmount } = command;
960
+ const selector = selectorDesc || 'locator';
961
+ switch (action) {
962
+ case som_types_1.InteractionAction.CLICK:
963
+ await locator.click({ force });
964
+ return `await ${selector}.click()`;
965
+ case som_types_1.InteractionAction.DOUBLE_CLICK:
966
+ await locator.dblclick({ force });
967
+ return `await ${selector}.dblclick()`;
968
+ case som_types_1.InteractionAction.RIGHT_CLICK:
969
+ await locator.click({ button: 'right', force });
970
+ return `await ${selector}.click({ button: 'right' })`;
971
+ case som_types_1.InteractionAction.FILL:
972
+ await locator.fill(value || '', { force });
973
+ return `await ${selector}.fill('${value}')`;
974
+ case som_types_1.InteractionAction.TYPE:
975
+ await locator.pressSequentially(value || '', { delay: command.delay || 50 });
976
+ return `await ${selector}.pressSequentially('${value}')`;
977
+ case som_types_1.InteractionAction.CLEAR:
978
+ await locator.clear({ force });
979
+ return `await ${selector}.clear()`;
980
+ case som_types_1.InteractionAction.PRESS:
981
+ await locator.press(value || 'Enter');
982
+ return `await ${selector}.press('${value}')`;
983
+ case som_types_1.InteractionAction.SELECT:
984
+ await locator.selectOption(value || '');
985
+ return `await ${selector}.selectOption('${value}')`;
986
+ case som_types_1.InteractionAction.CHECK:
987
+ await locator.check({ force });
988
+ return `await ${selector}.check()`;
989
+ case som_types_1.InteractionAction.UNCHECK:
990
+ await locator.uncheck({ force });
991
+ return `await ${selector}.uncheck()`;
992
+ case som_types_1.InteractionAction.HOVER:
993
+ await locator.hover({ force });
994
+ return `await ${selector}.hover()`;
995
+ case som_types_1.InteractionAction.FOCUS:
996
+ await locator.focus();
997
+ return `await ${selector}.focus()`;
998
+ case som_types_1.InteractionAction.BLUR:
999
+ await locator.blur();
1000
+ return `await ${selector}.blur()`;
1001
+ case som_types_1.InteractionAction.SCROLL_INTO_VIEW:
1002
+ await locator.scrollIntoViewIfNeeded();
1003
+ return `await ${selector}.scrollIntoViewIfNeeded()`;
1004
+ case som_types_1.InteractionAction.SCROLL:
1005
+ if (scrollAmount) {
1006
+ await locator.evaluate((el, amount) => {
1007
+ el.scrollBy(0, amount);
1008
+ }, scrollAmount);
1009
+ return `await ${selector}.evaluate(el => el.scrollBy(0, ${scrollAmount}))`;
1010
+ }
1011
+ break;
1012
+ case som_types_1.InteractionAction.DRAG:
1013
+ if (command.toCoord) {
1014
+ await locator.dragTo(this.page.locator('body'), {
1015
+ targetPosition: { x: command.toCoord.x, y: command.toCoord.y }
1016
+ });
1017
+ return `await ${selector}.dragTo(target)`;
1018
+ }
1019
+ break;
1020
+ default:
1021
+ throw new Error(`Unsupported action: ${action}`);
1022
+ }
1023
+ return `await ${selector}.${action}()`;
1024
+ }
1025
+ /**
1026
+ * Execute coordinate-based action as fallback
1027
+ */
1028
+ /**
1029
+ * Draw a visual marker at coordinate position (for debugging in headed mode)
1030
+ * Marker persists through screenshots so agent can see where coords landed
1031
+ */
1032
+ async drawCoordinateMarker(pixelX, pixelY) {
1033
+ await this.page.evaluate(({ x, y }) => {
1034
+ // Remove any existing coordinate marker and labels
1035
+ const existing = document.getElementById('tc-coord-marker');
1036
+ if (existing)
1037
+ existing.remove();
1038
+ const existingLabels = Array.from(document.querySelectorAll('div')).filter(el => el.textContent === 'clicked' && el.style.position === 'fixed');
1039
+ existingLabels.forEach(label => label.remove());
1040
+ // Create marker element
1041
+ const marker = document.createElement('div');
1042
+ marker.id = 'tc-coord-marker';
1043
+ marker.style.cssText = `
1044
+ position: fixed;
1045
+ left: ${x}px;
1046
+ top: ${y}px;
1047
+ width: 24px;
1048
+ height: 24px;
1049
+ margin-left: -12px;
1050
+ margin-top: -12px;
1051
+ border-radius: 50%;
1052
+ background: rgba(255, 0, 255, 0.8);
1053
+ border: 3px solid rgba(255, 255, 0, 0.95);
1054
+ z-index: 2147483647;
1055
+ pointer-events: none;
1056
+ box-shadow: 0 0 10px rgba(255, 0, 255, 0.6);
1057
+ animation: tc-pulse 0.5s ease-out;
1058
+ `;
1059
+ // Add label showing this was a coord click
1060
+ const label = document.createElement('div');
1061
+ label.style.cssText = `
1062
+ position: fixed;
1063
+ left: ${x + 15}px;
1064
+ top: ${y - 20}px;
1065
+ background: rgba(255, 0, 255, 0.9);
1066
+ color: white;
1067
+ padding: 2px 6px;
1068
+ border-radius: 3px;
1069
+ font-size: 11px;
1070
+ font-weight: bold;
1071
+ font-family: Arial;
1072
+ z-index: 2147483647;
1073
+ pointer-events: none;
1074
+ white-space: nowrap;
1075
+ `;
1076
+ label.textContent = 'clicked';
1077
+ // Add pulsing animation
1078
+ const style = document.createElement('style');
1079
+ style.textContent = `
1080
+ @keyframes tc-pulse {
1081
+ 0% { transform: scale(0); opacity: 0; }
1082
+ 50% { transform: scale(1.5); opacity: 1; }
1083
+ 100% { transform: scale(1); opacity: 1; }
1084
+ }
1085
+ `;
1086
+ document.head.appendChild(style);
1087
+ document.body.appendChild(marker);
1088
+ document.body.appendChild(label);
1089
+ }, { x: pixelX, y: pixelY });
1090
+ // Brief pause for animation to complete, then marker stays visible
1091
+ await this.page.waitForTimeout(300);
1092
+ }
1093
+ /**
1094
+ * Remove coordinate marker from page (used when cleaning up or replacing)
1095
+ */
1096
+ async removeCoordinateMarker() {
1097
+ await this.page.evaluate(() => {
1098
+ // Remove marker circle
1099
+ const marker = document.getElementById('tc-coord-marker');
1100
+ if (marker)
1101
+ marker.remove();
1102
+ // Remove all "clicked" labels (in case there are multiple)
1103
+ const labels = Array.from(document.querySelectorAll('div')).filter(el => el.textContent === 'clicked' && el.style.position === 'fixed' && el.style.background?.includes('255, 0, 255'));
1104
+ labels.forEach(label => label.remove());
1105
+ });
1106
+ }
1107
+ /**
1108
+ * Execute action using percentage-based coordinates
1109
+ */
1110
+ async executePercentageCoordinateAction(command) {
1111
+ if (!command.coord) {
1112
+ throw new Error('Coordinate is required for percentage-based commands');
1113
+ }
1114
+ // Get viewport dimensions
1115
+ const viewport = this.page.viewportSize();
1116
+ if (!viewport) {
1117
+ throw new Error('Could not determine viewport size');
1118
+ }
1119
+ // Convert percentage to pixels
1120
+ const pixelX = Math.round((command.coord.x / 100) * viewport.width);
1121
+ const pixelY = Math.round((command.coord.y / 100) * viewport.height);
1122
+ this.logger?.(`[PageSoMHandler] Using percentage coords: (${command.coord.x}%, ${command.coord.y}%) -> pixels (${pixelX}, ${pixelY})`, 'log');
1123
+ // Draw visual marker at target position (for headed mode debugging)
1124
+ await this.drawCoordinateMarker(pixelX, pixelY);
1125
+ const { action, value } = command;
1126
+ let playwrightCommand = '';
1127
+ try {
1128
+ switch (action) {
1129
+ case som_types_1.InteractionAction.CLICK:
1130
+ case som_types_1.InteractionAction.DOUBLE_CLICK:
1131
+ case som_types_1.InteractionAction.RIGHT_CLICK:
1132
+ const clickCount = action === som_types_1.InteractionAction.DOUBLE_CLICK ? 2 : 1;
1133
+ const button = action === som_types_1.InteractionAction.RIGHT_CLICK ? 'right' :
1134
+ command.button || 'left';
1135
+ await this.page.mouse.click(pixelX, pixelY, {
1136
+ button,
1137
+ clickCount,
1138
+ delay: command.delay
1139
+ });
1140
+ playwrightCommand = `await page.mouse.click(${pixelX}, ${pixelY}${button !== 'left' ? `, { button: '${button}' }` : ''})`;
1141
+ break;
1142
+ case som_types_1.InteractionAction.FILL:
1143
+ case som_types_1.InteractionAction.TYPE:
1144
+ // Click to focus, then type
1145
+ await this.page.mouse.click(pixelX, pixelY);
1146
+ await this.page.waitForTimeout(100); // Brief wait for focus
1147
+ if (action === som_types_1.InteractionAction.FILL) {
1148
+ await this.page.keyboard.type(value || '');
1149
+ }
1150
+ else {
1151
+ await this.page.keyboard.type(value || '', { delay: command.delay || 50 });
1152
+ }
1153
+ playwrightCommand = `await page.mouse.click(${pixelX}, ${pixelY}); await page.keyboard.type('${value}')`;
1154
+ break;
1155
+ case som_types_1.InteractionAction.HOVER:
1156
+ await this.page.mouse.move(pixelX, pixelY);
1157
+ playwrightCommand = `await page.mouse.move(${pixelX}, ${pixelY})`;
1158
+ break;
1159
+ case som_types_1.InteractionAction.DRAG:
1160
+ if (!command.toCoord) {
1161
+ throw new Error('toCoord is required for drag action');
1162
+ }
1163
+ const toPixelX = Math.round((command.toCoord.x / 100) * viewport.width);
1164
+ const toPixelY = Math.round((command.toCoord.y / 100) * viewport.height);
1165
+ await this.page.mouse.move(pixelX, pixelY);
1166
+ await this.page.mouse.down();
1167
+ await this.page.mouse.move(toPixelX, toPixelY);
1168
+ await this.page.mouse.up();
1169
+ playwrightCommand = `await page.mouse.move(${pixelX}, ${pixelY}); await page.mouse.down(); await page.mouse.move(${toPixelX}, ${toPixelY}); await page.mouse.up()`;
1170
+ break;
1171
+ default:
1172
+ throw new Error(`Coordinate-based execution not supported for action: ${action}`);
1173
+ }
1174
+ return {
1175
+ failedAttempts: [],
1176
+ successAttempt: {
1177
+ command: playwrightCommand,
1178
+ status: som_types_1.CommandRunStatus.SUCCESS
1179
+ },
1180
+ status: som_types_1.CommandRunStatus.SUCCESS
1181
+ };
1182
+ }
1183
+ catch (error) {
1184
+ throw new Error(`Failed to execute percentage coordinate action: ${error.message}`);
1185
+ }
1186
+ }
1187
+ async executeCoordinateAction(command, element) {
1188
+ try {
1189
+ // Calculate center of bounding box
1190
+ const centerX = element.bbox.x + element.bbox.width / 2;
1191
+ const centerY = element.bbox.y + element.bbox.height / 2;
1192
+ this.logger?.(`[PageSoMHandler] Using coordinates (${centerX}, ${centerY})`, 'log');
1193
+ const { action, value } = command;
1194
+ let playwrightCommand = '';
1195
+ switch (action) {
1196
+ case som_types_1.InteractionAction.CLICK:
1197
+ case som_types_1.InteractionAction.DOUBLE_CLICK:
1198
+ case som_types_1.InteractionAction.RIGHT_CLICK:
1199
+ const clickCount = action === som_types_1.InteractionAction.DOUBLE_CLICK ? 2 : 1;
1200
+ const button = action === som_types_1.InteractionAction.RIGHT_CLICK ? 'right' : 'left';
1201
+ await this.page.mouse.click(centerX, centerY, { button, clickCount });
1202
+ playwrightCommand = `await page.mouse.click(${centerX}, ${centerY}${button !== 'left' ? `, { button: '${button}' }` : ''})`;
1203
+ break;
1204
+ case som_types_1.InteractionAction.FILL:
1205
+ case som_types_1.InteractionAction.TYPE:
1206
+ // Click first, then type
1207
+ await this.page.mouse.click(centerX, centerY);
1208
+ if (action === som_types_1.InteractionAction.FILL) {
1209
+ await this.page.keyboard.type(value || '');
1210
+ }
1211
+ else {
1212
+ await this.page.keyboard.type(value || '', { delay: command.delay || 50 });
1213
+ }
1214
+ playwrightCommand = `await page.mouse.click(${centerX}, ${centerY}); await page.keyboard.type('${value}')`;
1215
+ break;
1216
+ case som_types_1.InteractionAction.HOVER:
1217
+ await this.page.mouse.move(centerX, centerY);
1218
+ playwrightCommand = `await page.mouse.move(${centerX}, ${centerY})`;
1219
+ break;
1220
+ default:
1221
+ throw new Error(`Coordinate fallback not supported for action: ${action}`);
1222
+ }
1223
+ return {
1224
+ failedAttempts: [],
1225
+ successAttempt: {
1226
+ command: playwrightCommand,
1227
+ status: som_types_1.CommandRunStatus.SUCCESS
1228
+ },
1229
+ status: som_types_1.CommandRunStatus.SUCCESS
1230
+ };
1231
+ }
1232
+ catch (error) {
1233
+ return {
1234
+ failedAttempts: [{
1235
+ command: `coordinate-based ${command.action}`,
1236
+ status: som_types_1.CommandRunStatus.FAILURE,
1237
+ error: error.message
1238
+ }],
1239
+ error: `Coordinate fallback failed: ${error.message}`,
1240
+ status: som_types_1.CommandRunStatus.FAILURE
1241
+ };
1242
+ }
1243
+ }
1244
+ /**
1245
+ * Setup mutation observer to track DOM changes
1246
+ */
1247
+ async setupMutationObserver() {
1248
+ await this.page.evaluate(() => {
1249
+ const mutations = [];
1250
+ const observer = new MutationObserver((mutationsList) => {
1251
+ for (const mutation of mutationsList) {
1252
+ if (mutation.type === 'childList') {
1253
+ mutation.addedNodes.forEach((node) => {
1254
+ if (node.nodeType === 1) { // Element node
1255
+ const el = node;
1256
+ const styles = window.getComputedStyle(el);
1257
+ // Only track visible additions
1258
+ if (styles.display !== 'none' && styles.visibility !== 'hidden') {
1259
+ mutations.push({
1260
+ type: 'added',
1261
+ elementDescription: `${el.tagName.toLowerCase()}${el.className ? '.' + el.className.split(' ')[0] : ''}`,
1262
+ timestamp: Date.now()
1263
+ });
1264
+ }
1265
+ }
1266
+ });
1267
+ }
1268
+ }
1269
+ });
1270
+ observer.observe(document.body, {
1271
+ childList: true,
1272
+ subtree: true,
1273
+ attributes: true
1274
+ });
1275
+ window.__tcSomMutationObserver = observer;
1276
+ window.__tcSomMutations = mutations;
1277
+ });
1278
+ }
1279
+ /**
1280
+ * Filter relevant mutations (exclude mass updates)
1281
+ */
1282
+ async filterRelevantMutations() {
1283
+ // Get mutations from page
1284
+ const mutations = await this.page.evaluate(() => {
1285
+ return window.__tcSomMutations || [];
1286
+ });
1287
+ // Filter: If too many mutations (>10), likely page rebuild - don't report
1288
+ if (mutations.length > 10) {
1289
+ this.logger?.(`[PageSoMHandler] ${mutations.length} mutations detected - too many, filtering out`, 'warn');
1290
+ return [];
1291
+ }
1292
+ // Return relevant mutations (tooltips, notices, message boxes)
1293
+ return mutations;
1294
+ }
1295
+ /**
1296
+ * Escape selector text
1297
+ */
1298
+ escapeSelector(text) {
1299
+ return text.replace(/'/g, "\\'").trim().substring(0, 50);
1300
+ }
1301
+ /**
1302
+ * Try refined selector with parent scoping
1303
+ */
1304
+ async tryRefinedSelector(typedSelector, command, element, originalError) {
1305
+ // Try parent scoping for generic locators (but avoid if already scoped)
1306
+ if (typedSelector.type === 'locator' && element.parent?.className) {
1307
+ const parentClass = element.parent.className.split(' ')[0];
1308
+ // Guard: Don't add parent if selector already includes it (prevent infinite recursion)
1309
+ if (parentClass && !typedSelector.value.includes(parentClass)) {
1310
+ const refinedSelector = {
1311
+ type: 'locator',
1312
+ value: `.${parentClass} ${typedSelector.value}`
1313
+ };
1314
+ const selectorDesc = this.formatSelector(refinedSelector);
1315
+ this.logger?.(`[PageSoMHandler] Trying refined selector with parent: ${selectorDesc}`, 'log');
1316
+ // Important: Don't call tryExecuteAction (would recurse), execute directly
1317
+ try {
1318
+ const locator = this.buildLocatorFromTypedSelector(refinedSelector);
1319
+ const refinedDesc = this.formatSelector(refinedSelector);
1320
+ const playwrightCommand = await this.executeActionOnLocator(locator, command, refinedDesc);
1321
+ return {
1322
+ failedAttempts: [{
1323
+ command: this.formatSelector(typedSelector),
1324
+ status: som_types_1.CommandRunStatus.FAILURE,
1325
+ error: originalError.message
1326
+ }],
1327
+ successAttempt: {
1328
+ command: playwrightCommand,
1329
+ status: som_types_1.CommandRunStatus.SUCCESS
1330
+ },
1331
+ status: som_types_1.CommandRunStatus.SUCCESS
1332
+ };
1333
+ }
1334
+ catch (refinedError) {
1335
+ // Parent scoping didn't help, fall through to failure
1336
+ this.logger?.(`[PageSoMHandler] Refined selector also failed: ${refinedError.message}`, 'warn');
1337
+ }
1338
+ }
1339
+ }
1340
+ // Could implement nth() based on bbox position here
1341
+ const selectorDesc = this.formatSelector(typedSelector);
1342
+ return {
1343
+ failedAttempts: [{
1344
+ command: selectorDesc,
1345
+ status: som_types_1.CommandRunStatus.FAILURE,
1346
+ error: originalError.message
1347
+ }],
1348
+ status: som_types_1.CommandRunStatus.FAILURE
1349
+ };
1350
+ }
1351
+ }
1352
+ exports.PageSoMHandler = PageSoMHandler;
1353
+ //# sourceMappingURL=page-som-handler.js.map