testchimp-runner-core 0.0.40 → 0.0.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/execution-service.d.ts.map +1 -1
- package/dist/execution-service.js +1 -3
- package/dist/execution-service.js.map +1 -1
- package/dist/index.d.ts +7 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/orchestrator/decision-parser.d.ts.map +1 -1
- package/dist/orchestrator/decision-parser.js +16 -0
- package/dist/orchestrator/decision-parser.js.map +1 -1
- package/dist/orchestrator/index.d.ts +3 -1
- package/dist/orchestrator/index.d.ts.map +1 -1
- package/dist/orchestrator/index.js +8 -1
- package/dist/orchestrator/index.js.map +1 -1
- package/dist/orchestrator/orchestrator-agent.d.ts +10 -4
- package/dist/orchestrator/orchestrator-agent.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator-agent.js +347 -93
- package/dist/orchestrator/orchestrator-agent.js.map +1 -1
- package/dist/orchestrator/orchestrator-prompts.d.ts.map +1 -1
- package/dist/orchestrator/orchestrator-prompts.js +355 -415
- package/dist/orchestrator/orchestrator-prompts.js.map +1 -1
- package/dist/orchestrator/page-loading-utils.d.ts +15 -0
- package/dist/orchestrator/page-loading-utils.d.ts.map +1 -0
- package/dist/orchestrator/page-loading-utils.js +115 -0
- package/dist/orchestrator/page-loading-utils.js.map +1 -0
- package/dist/orchestrator/page-som-handler.d.ts +2 -1
- package/dist/orchestrator/page-som-handler.d.ts.map +1 -1
- package/dist/orchestrator/page-som-handler.js +250 -33
- package/dist/orchestrator/page-som-handler.js.map +1 -1
- package/dist/orchestrator/site-learnings-utils.d.ts +31 -0
- package/dist/orchestrator/site-learnings-utils.d.ts.map +1 -0
- package/dist/orchestrator/site-learnings-utils.js +175 -0
- package/dist/orchestrator/site-learnings-utils.js.map +1 -0
- package/dist/orchestrator/som-types.d.ts +2 -0
- package/dist/orchestrator/som-types.d.ts.map +1 -1
- package/dist/orchestrator/som-types.js.map +1 -1
- package/dist/orchestrator/tools/take-screenshot.d.ts.map +1 -1
- package/dist/orchestrator/tools/take-screenshot.js +10 -1
- package/dist/orchestrator/tools/take-screenshot.js.map +1 -1
- package/dist/orchestrator/types.d.ts +54 -9
- package/dist/orchestrator/types.d.ts.map +1 -1
- package/dist/orchestrator/types.js.map +1 -1
- package/dist/progress-reporter.d.ts +23 -2
- package/dist/progress-reporter.d.ts.map +1 -1
- package/dist/progress-reporter.js.map +1 -1
- package/dist/scenario-service.d.ts +3 -3
- package/dist/scenario-service.d.ts.map +1 -1
- package/dist/scenario-service.js +6 -5
- package/dist/scenario-service.js.map +1 -1
- package/dist/scenario-worker-class.d.ts +7 -3
- package/dist/scenario-worker-class.d.ts.map +1 -1
- package/dist/scenario-worker-class.js +62 -9
- package/dist/scenario-worker-class.js.map +1 -1
- package/dist/types.d.ts +4 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
- package/dist/testing/agent-tester.d.ts +0 -35
- package/dist/testing/agent-tester.d.ts.map +0 -1
- package/dist/testing/agent-tester.js +0 -84
- package/dist/testing/agent-tester.js.map +0 -1
- package/dist/testing/ref-translator-tester.d.ts +0 -44
- package/dist/testing/ref-translator-tester.d.ts.map +0 -1
- package/dist/testing/ref-translator-tester.js +0 -104
- package/dist/testing/ref-translator-tester.js.map +0 -1
- package/dist/utils/hierarchical-selector.d.ts +0 -47
- package/dist/utils/hierarchical-selector.d.ts.map +0 -1
- package/dist/utils/hierarchical-selector.js +0 -212
- package/dist/utils/hierarchical-selector.js.map +0 -1
- package/dist/utils/ref-attacher.d.ts +0 -21
- package/dist/utils/ref-attacher.d.ts.map +0 -1
- package/dist/utils/ref-attacher.js +0 -149
- package/dist/utils/ref-attacher.js.map +0 -1
- package/dist/utils/ref-translator.d.ts +0 -49
- package/dist/utils/ref-translator.d.ts.map +0 -1
- package/dist/utils/ref-translator.js +0 -276
- package/dist/utils/ref-translator.js.map +0 -1
|
@@ -47,31 +47,116 @@ class PageSoMHandler {
|
|
|
47
47
|
/**
|
|
48
48
|
* Update SoM markers - extract interactive elements and draw overlay
|
|
49
49
|
* Clears coordinate markers from previous iteration (agent can view via previous_screenshot tool)
|
|
50
|
+
* @param includeOffscreen - If true, marks ALL elements including those below viewport (for full-page screenshots)
|
|
50
51
|
*/
|
|
51
|
-
async updateSom() {
|
|
52
|
-
this.logger?.(
|
|
52
|
+
async updateSom(includeOffscreen = false) {
|
|
53
|
+
this.logger?.(`[PageSoMHandler] Updating SoM markers${includeOffscreen ? ' (including offscreen elements)' : ''}...`, 'log');
|
|
53
54
|
// Clear any coordinate markers from previous iteration
|
|
54
55
|
await this.removeCoordinateMarker();
|
|
55
56
|
// Extract interactive elements and assign SoM IDs
|
|
56
|
-
const elements = await this.page.evaluate(() => {
|
|
57
|
+
const elements = await this.page.evaluate((includeOffscreen) => {
|
|
57
58
|
const doc = document;
|
|
58
59
|
const elements = [];
|
|
59
60
|
let idCounter = 1;
|
|
60
|
-
//
|
|
61
|
+
// Helper: Find all shadow roots in the document (recursive, but only called once)
|
|
62
|
+
function getAllShadowRoots(root = document) {
|
|
63
|
+
const shadowRoots = [];
|
|
64
|
+
// Use TreeWalker for efficient DOM traversal (faster than querySelectorAll('*'))
|
|
65
|
+
const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT, null);
|
|
66
|
+
let node = walker.currentNode;
|
|
67
|
+
while (node) {
|
|
68
|
+
if (node.shadowRoot) {
|
|
69
|
+
try {
|
|
70
|
+
const shadowRoot = node.shadowRoot;
|
|
71
|
+
shadowRoots.push(shadowRoot);
|
|
72
|
+
// Recursively find shadow roots inside this shadow root
|
|
73
|
+
const nestedShadowRoots = getAllShadowRoots(shadowRoot);
|
|
74
|
+
shadowRoots.push(...nestedShadowRoots);
|
|
75
|
+
}
|
|
76
|
+
catch (e) {
|
|
77
|
+
// Closed shadow root - skip it
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
node = walker.nextNode();
|
|
81
|
+
}
|
|
82
|
+
return shadowRoots;
|
|
83
|
+
}
|
|
84
|
+
// Helper: Query selector in document + all shadow roots
|
|
85
|
+
// Much more efficient than recursive approach - finds shadow roots once, then queries each
|
|
86
|
+
function querySelectorAllDeep(selector) {
|
|
87
|
+
const results = [];
|
|
88
|
+
// Query main document
|
|
89
|
+
doc.querySelectorAll(selector).forEach((el) => results.push(el));
|
|
90
|
+
// Query each shadow root
|
|
91
|
+
shadowRoots.forEach(shadowRoot => {
|
|
92
|
+
try {
|
|
93
|
+
shadowRoot.querySelectorAll(selector).forEach((el) => results.push(el));
|
|
94
|
+
}
|
|
95
|
+
catch (e) {
|
|
96
|
+
// Selector might not be valid in shadow context - skip
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
// Find all shadow roots upfront (only once, not per selector!)
|
|
102
|
+
const shadowRoots = getAllShadowRoots(doc);
|
|
103
|
+
// Query all interactive elements (including shadow DOM)
|
|
61
104
|
const interactiveSelectors = [
|
|
62
105
|
'button', 'input', 'textarea', 'select', 'a[href]',
|
|
63
106
|
'[role="button"]', '[role="link"]', '[role="textbox"]',
|
|
64
107
|
'[role="checkbox"]', '[role="radio"]', '[role="combobox"]',
|
|
65
108
|
'[role="menu"]', '[role="menuitem"]', '[role="option"]',
|
|
66
|
-
'[
|
|
109
|
+
'[onclick]', '[type="submit"]',
|
|
67
110
|
'[role="tab"]', '[role="switch"]', '[role="spinbutton"]'
|
|
68
111
|
];
|
|
69
112
|
const allInteractive = new Set();
|
|
70
113
|
interactiveSelectors.forEach(selector => {
|
|
71
|
-
|
|
114
|
+
querySelectorAllDeep(selector).forEach((el) => allInteractive.add(el));
|
|
115
|
+
});
|
|
116
|
+
// Special handling for containers with ARIA attributes that might not be directly clickable
|
|
117
|
+
// Pattern 1: [aria-haspopup] - dropdowns, menus, dialogs, date pickers
|
|
118
|
+
// Pattern 2: [aria-expanded] - accordions, collapses, expandable sections
|
|
119
|
+
// Pattern 3: [role="combobox"] - custom selects without aria-haspopup
|
|
120
|
+
const ariaContainerSelectors = ['[aria-haspopup]', '[aria-expanded]', '[role="combobox"]'];
|
|
121
|
+
ariaContainerSelectors.forEach(selector => {
|
|
122
|
+
querySelectorAllDeep(selector).forEach((container) => {
|
|
123
|
+
// Skip if already marked by other selectors (e.g., button with aria-expanded)
|
|
124
|
+
if (allInteractive.has(container))
|
|
125
|
+
return;
|
|
126
|
+
const styles = window.getComputedStyle(container);
|
|
127
|
+
const isContainerClickable = styles.cursor === 'pointer' ||
|
|
128
|
+
container.onclick ||
|
|
129
|
+
container.getAttribute('onclick') ||
|
|
130
|
+
container.tagName === 'BUTTON' ||
|
|
131
|
+
container.tagName === 'A';
|
|
132
|
+
if (!isContainerClickable) {
|
|
133
|
+
// Check for clickable children (buttons, icons, inputs)
|
|
134
|
+
const clickableChild = Array.from(container.children).find((child) => {
|
|
135
|
+
const childStyles = window.getComputedStyle(child);
|
|
136
|
+
return childStyles.cursor === 'pointer' ||
|
|
137
|
+
child.onclick ||
|
|
138
|
+
child.getAttribute('onclick') ||
|
|
139
|
+
child.tagName === 'BUTTON' ||
|
|
140
|
+
child.tagName === 'A' ||
|
|
141
|
+
child.tagName === 'INPUT';
|
|
142
|
+
});
|
|
143
|
+
if (clickableChild) {
|
|
144
|
+
// Mark the child, not the container
|
|
145
|
+
allInteractive.add(clickableChild);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
// No clickable child found, mark the container as fallback
|
|
149
|
+
allInteractive.add(container);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
// Container is clickable, mark it
|
|
154
|
+
allInteractive.add(container);
|
|
155
|
+
}
|
|
156
|
+
});
|
|
72
157
|
});
|
|
73
158
|
// Also detect styled-as-interactive elements
|
|
74
|
-
|
|
159
|
+
querySelectorAllDeep('div, span, p, li, td').forEach((el) => {
|
|
75
160
|
const styles = window.getComputedStyle(el);
|
|
76
161
|
const hasClickHandler = el.onclick || el.getAttribute('onclick') ||
|
|
77
162
|
el.hasAttribute('data-action') || el.hasAttribute('data-click');
|
|
@@ -79,6 +164,22 @@ class PageSoMHandler {
|
|
|
79
164
|
allInteractive.add(el);
|
|
80
165
|
}
|
|
81
166
|
});
|
|
167
|
+
// Special handling for labels wrapping hidden inputs (custom checkbox/radio patterns)
|
|
168
|
+
// If a label wraps a hidden input and has visible content, mark the label, not the input
|
|
169
|
+
querySelectorAllDeep('label').forEach((label) => {
|
|
170
|
+
const input = label.querySelector('input[type="checkbox"], input[type="radio"]');
|
|
171
|
+
if (input) {
|
|
172
|
+
const inputStyles = window.getComputedStyle(input);
|
|
173
|
+
const isInputHidden = inputStyles.display === 'none' ||
|
|
174
|
+
inputStyles.visibility === 'hidden' ||
|
|
175
|
+
parseFloat(inputStyles.opacity) === 0 ||
|
|
176
|
+
(input.getBoundingClientRect().width === 0);
|
|
177
|
+
if (isInputHidden) {
|
|
178
|
+
// Hidden input with styled label - mark the label instead
|
|
179
|
+
allInteractive.add(label);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
});
|
|
82
183
|
// Remove elements that are descendants of "true" interactive elements
|
|
83
184
|
// (e.g., <span> inside <button> should not get separate marker)
|
|
84
185
|
// But keep elements inside generic containers (div, section, etc.)
|
|
@@ -109,8 +210,18 @@ class PageSoMHandler {
|
|
|
109
210
|
return;
|
|
110
211
|
// Skip hidden elements (display, visibility, opacity checks)
|
|
111
212
|
const styles = window.getComputedStyle(el);
|
|
112
|
-
|
|
113
|
-
parseFloat(styles.opacity) === 0)
|
|
213
|
+
const isHidden = styles.display === 'none' ||
|
|
214
|
+
(styles.visibility === 'hidden' && parseFloat(styles.opacity) === 0);
|
|
215
|
+
// Special case: Check for visible pseudo-elements (::before, ::after)
|
|
216
|
+
// Some sites hide the main element but show content via pseudo-elements
|
|
217
|
+
let hasVisiblePseudo = false;
|
|
218
|
+
if (styles.visibility === 'hidden' || parseFloat(styles.opacity) === 0) {
|
|
219
|
+
const before = window.getComputedStyle(el, '::before');
|
|
220
|
+
const after = window.getComputedStyle(el, '::after');
|
|
221
|
+
hasVisiblePseudo = (before.content !== 'none' && before.visibility === 'visible' && before.display !== 'none') ||
|
|
222
|
+
(after.content !== 'none' && after.visibility === 'visible' && after.display !== 'none');
|
|
223
|
+
}
|
|
224
|
+
if (isHidden && !hasVisiblePseudo)
|
|
114
225
|
return;
|
|
115
226
|
// Skip disabled elements (they can't be interacted with)
|
|
116
227
|
const isDisabled = el.disabled ||
|
|
@@ -133,17 +244,30 @@ class PageSoMHandler {
|
|
|
133
244
|
{ x: rect.left + inset, y: rect.bottom - inset }, // bottom-left
|
|
134
245
|
{ x: rect.right - inset, y: rect.bottom - inset } // bottom-right
|
|
135
246
|
];
|
|
136
|
-
//
|
|
137
|
-
// (
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
247
|
+
// Occlusion detection - only for elements in viewport
|
|
248
|
+
// For offscreen elements (below fold), skip this check since elementFromPoint doesn't work
|
|
249
|
+
const isInViewport = rect.top < window.innerHeight && rect.bottom > 0 &&
|
|
250
|
+
rect.left < window.innerWidth && rect.right > 0;
|
|
251
|
+
if (!includeOffscreen && isInViewport) {
|
|
252
|
+
// Element is in viewport - check if it's occluded
|
|
253
|
+
let visiblePoints = 0;
|
|
254
|
+
for (const point of testPoints) {
|
|
255
|
+
const topEl = document.elementFromPoint(point.x, point.y);
|
|
256
|
+
if (topEl && (topEl === el || el.contains(topEl) || topEl.contains(el))) {
|
|
257
|
+
visiblePoints++;
|
|
258
|
+
}
|
|
143
259
|
}
|
|
260
|
+
// Skip fully occluded elements (no visible points)
|
|
261
|
+
if (visiblePoints < 1) {
|
|
262
|
+
return;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
else if (includeOffscreen && !isInViewport) {
|
|
266
|
+
// Element is offscreen - include it without occlusion check
|
|
267
|
+
// (elementFromPoint doesn't work for offscreen elements)
|
|
144
268
|
}
|
|
145
|
-
|
|
146
|
-
|
|
269
|
+
else if (!includeOffscreen && !isInViewport) {
|
|
270
|
+
// Viewport-only mode and element not in viewport - skip it
|
|
147
271
|
return;
|
|
148
272
|
}
|
|
149
273
|
// Assign tc-som-id attribute
|
|
@@ -151,12 +275,56 @@ class PageSoMHandler {
|
|
|
151
275
|
el.setAttribute('tc-som-id', somId);
|
|
152
276
|
// Capture element details
|
|
153
277
|
const parent = el.parentElement;
|
|
278
|
+
// Extract text - if main element is hidden, try to get text from pseudo-elements
|
|
279
|
+
let displayText = el.textContent?.trim().substring(0, 50) || '';
|
|
280
|
+
if (hasVisiblePseudo && (!displayText || styles.visibility === 'hidden')) {
|
|
281
|
+
const before = window.getComputedStyle(el, '::before');
|
|
282
|
+
const after = window.getComputedStyle(el, '::after');
|
|
283
|
+
if (before.content && before.content !== 'none') {
|
|
284
|
+
// Remove surrounding quotes from content
|
|
285
|
+
displayText = before.content.replace(/^["']|["']$/g, '');
|
|
286
|
+
}
|
|
287
|
+
else if (after.content && after.content !== 'none') {
|
|
288
|
+
displayText = after.content.replace(/^["']|["']$/g, '');
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
// For images, the accessible name comes from alt attribute
|
|
292
|
+
// For other elements, use aria-label if present
|
|
293
|
+
let accessibleName = el.getAttribute('aria-label') || '';
|
|
294
|
+
if (el.tagName.toLowerCase() === 'img' && !accessibleName) {
|
|
295
|
+
accessibleName = el.getAttribute('alt') || '';
|
|
296
|
+
}
|
|
297
|
+
// Detect associated <label> element for inputs/textareas/selects
|
|
298
|
+
// This enables getByLabel() selector generation
|
|
299
|
+
let labelText = '';
|
|
300
|
+
const tagLower = el.tagName.toLowerCase();
|
|
301
|
+
if (['input', 'textarea', 'select'].includes(tagLower)) {
|
|
302
|
+
// Method 1: Label with for="id"
|
|
303
|
+
if (el.id) {
|
|
304
|
+
const label = doc.querySelector(`label[for="${el.id}"]`);
|
|
305
|
+
if (label) {
|
|
306
|
+
labelText = label.textContent?.trim() || '';
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
// Method 2: Label wrapping the input
|
|
310
|
+
if (!labelText) {
|
|
311
|
+
let parent = el.parentElement;
|
|
312
|
+
while (parent && parent !== doc.body) {
|
|
313
|
+
if (parent.tagName.toLowerCase() === 'label') {
|
|
314
|
+
labelText = parent.textContent?.trim() || '';
|
|
315
|
+
break;
|
|
316
|
+
}
|
|
317
|
+
parent = parent.parentElement;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
154
321
|
elements.push({
|
|
155
322
|
somId,
|
|
156
323
|
tag: el.tagName.toLowerCase(),
|
|
157
324
|
role: el.getAttribute('role') || el.tagName.toLowerCase(),
|
|
158
|
-
text:
|
|
159
|
-
ariaLabel:
|
|
325
|
+
text: displayText,
|
|
326
|
+
ariaLabel: accessibleName,
|
|
327
|
+
labelText: labelText, // Associated <label> text for getByLabel()
|
|
160
328
|
placeholder: el.placeholder || '',
|
|
161
329
|
name: el.getAttribute('name') || '',
|
|
162
330
|
type: el.type || '',
|
|
@@ -168,6 +336,7 @@ class PageSoMHandler {
|
|
|
168
336
|
width: Math.round(rect.width),
|
|
169
337
|
height: Math.round(rect.height)
|
|
170
338
|
},
|
|
339
|
+
hasVisiblePseudoElement: hasVisiblePseudo,
|
|
171
340
|
parent: parent ? {
|
|
172
341
|
tag: parent.tagName.toLowerCase(),
|
|
173
342
|
role: parent.getAttribute('role') || '',
|
|
@@ -177,7 +346,7 @@ class PageSoMHandler {
|
|
|
177
346
|
});
|
|
178
347
|
});
|
|
179
348
|
return elements;
|
|
180
|
-
});
|
|
349
|
+
}, includeOffscreen);
|
|
181
350
|
// Store in somMap
|
|
182
351
|
this.somMap.clear();
|
|
183
352
|
elements.forEach(el => this.somMap.set(el.somId, el));
|
|
@@ -457,11 +626,20 @@ class PageSoMHandler {
|
|
|
457
626
|
await this.setupMutationObserver();
|
|
458
627
|
}
|
|
459
628
|
// Try semantic selectors first (unless useSomIdBasedCommands=true)
|
|
629
|
+
// For pseudo-element buttons, generateSemanticSelectors will use CSS selectors instead of getByRole/getByText
|
|
460
630
|
if (!useSomIdBasedCommands) {
|
|
461
631
|
const selectors = this.generateSemanticSelectors(element);
|
|
462
632
|
for (let i = 0; i < Math.min(selectors.length, maxAttempts); i++) {
|
|
463
633
|
const selector = selectors[i];
|
|
464
|
-
|
|
634
|
+
// Force click/hover for pseudo-element buttons (they have visibility:hidden)
|
|
635
|
+
const modifiedCommand = element?.hasVisiblePseudoElement &&
|
|
636
|
+
(command.action === som_types_1.InteractionAction.CLICK ||
|
|
637
|
+
command.action === som_types_1.InteractionAction.DOUBLE_CLICK ||
|
|
638
|
+
command.action === som_types_1.InteractionAction.RIGHT_CLICK ||
|
|
639
|
+
command.action === som_types_1.InteractionAction.HOVER)
|
|
640
|
+
? { ...command, force: true }
|
|
641
|
+
: command;
|
|
642
|
+
const result = await this.tryExecuteAction(selector, modifiedCommand, element);
|
|
465
643
|
if (result.status === som_types_1.CommandRunStatus.SUCCESS) {
|
|
466
644
|
// Wait for mutations if hover/focus
|
|
467
645
|
if (command.action === som_types_1.InteractionAction.HOVER || command.action === som_types_1.InteractionAction.FOCUS) {
|
|
@@ -482,7 +660,15 @@ class PageSoMHandler {
|
|
|
482
660
|
type: 'locator',
|
|
483
661
|
value: `[tc-som-id="${command.elementRef}"]`
|
|
484
662
|
};
|
|
485
|
-
|
|
663
|
+
// Force click/interactions for elements with pseudo-elements (they may have visibility:hidden)
|
|
664
|
+
const modifiedCommand = element?.hasVisiblePseudoElement &&
|
|
665
|
+
(command.action === som_types_1.InteractionAction.CLICK ||
|
|
666
|
+
command.action === som_types_1.InteractionAction.DOUBLE_CLICK ||
|
|
667
|
+
command.action === som_types_1.InteractionAction.RIGHT_CLICK ||
|
|
668
|
+
command.action === som_types_1.InteractionAction.HOVER)
|
|
669
|
+
? { ...command, force: true }
|
|
670
|
+
: command;
|
|
671
|
+
const result = await this.tryExecuteAction(somIdSelector, modifiedCommand, element);
|
|
486
672
|
if (result.status === som_types_1.CommandRunStatus.SUCCESS) {
|
|
487
673
|
await this.disconnectMutationObserver();
|
|
488
674
|
return result;
|
|
@@ -716,12 +902,18 @@ class PageSoMHandler {
|
|
|
716
902
|
// Priority 1: getByTestId (would need to capture in updateSom)
|
|
717
903
|
// Skipped for now - can add data-testid capture later
|
|
718
904
|
// Priority 2: Stable ID
|
|
719
|
-
|
|
905
|
+
// Filter unstable IDs:
|
|
906
|
+
// - Contains colons (React/MUI: :r123:, :r1r: - need CSS escaping which is fragile)
|
|
907
|
+
// - Auto-generated prefixes (rc_, __)
|
|
908
|
+
// - Dynamic markers (contains «)
|
|
909
|
+
if (element.id && !element.id.includes(':') && !element.id.match(/^(rc_|__)/) && !element.id.includes('«')) {
|
|
720
910
|
selectors.push({ type: 'id', value: element.id });
|
|
721
911
|
}
|
|
722
|
-
// Priority 3: getByLabel (
|
|
723
|
-
|
|
724
|
-
|
|
912
|
+
// Priority 3: getByLabel (ONLY when there's an actual <label> element)
|
|
913
|
+
// getByLabel doesn't work with aria-label/alt - it needs <label for="id">
|
|
914
|
+
// Now properly detected: we check for associated <label> during updateSom()
|
|
915
|
+
if (element.labelText) {
|
|
916
|
+
selectors.push({ type: 'label', value: element.labelText });
|
|
725
917
|
}
|
|
726
918
|
// Priority 4: input[name] (backend contract - very stable)
|
|
727
919
|
if (element.name && ['input', 'textarea', 'select'].includes(element.tag)) {
|
|
@@ -731,15 +923,36 @@ class PageSoMHandler {
|
|
|
731
923
|
if (element.placeholder) {
|
|
732
924
|
selectors.push({ type: 'placeholder', value: element.placeholder });
|
|
733
925
|
}
|
|
734
|
-
// Priority 6:
|
|
735
|
-
|
|
736
|
-
|
|
926
|
+
// Priority 6: CSS selector for pseudo-element buttons (BEFORE role/text)
|
|
927
|
+
// For buttons with visibility:hidden + ::before/::after, getByRole/getByText won't work
|
|
928
|
+
if (element.hasVisiblePseudoElement && element.tag === 'button') {
|
|
929
|
+
// Try button[type="submit"] first (most common for forms)
|
|
930
|
+
if (element.type === 'submit') {
|
|
931
|
+
selectors.push({ type: 'locator', value: 'button[type="submit"]' });
|
|
932
|
+
}
|
|
933
|
+
// Try class-based selector if available
|
|
934
|
+
if (element.className) {
|
|
935
|
+
const firstClass = element.className.split(' ')[0];
|
|
936
|
+
if (firstClass && !firstClass.match(/^(css-|MuiButton-|ant-|btn-)/)) {
|
|
937
|
+
selectors.push({ type: 'locator', value: `button.${firstClass}` });
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
// Priority 7: getByRole (broader, can match multiple elements)
|
|
942
|
+
// Skip for pseudo-element buttons (they have visibility:hidden → not in a11y tree)
|
|
943
|
+
// For images/buttons with accessible names, prefer ariaLabel over text
|
|
944
|
+
if (!element.hasVisiblePseudoElement && element.role) {
|
|
945
|
+
const accessibleName = element.ariaLabel || element.text;
|
|
946
|
+
if (accessibleName) {
|
|
947
|
+
selectors.push({ type: 'role', value: element.role, roleOptions: { name: accessibleName } });
|
|
948
|
+
}
|
|
737
949
|
}
|
|
738
|
-
// Priority
|
|
739
|
-
|
|
950
|
+
// Priority 8: getByText (last semantic option)
|
|
951
|
+
// Skip for pseudo-element buttons (getByText will match the heading instead!)
|
|
952
|
+
if (!element.hasVisiblePseudoElement && element.text) {
|
|
740
953
|
selectors.push({ type: 'text', value: element.text });
|
|
741
954
|
}
|
|
742
|
-
// Priority
|
|
955
|
+
// Priority 9: Parent-scoped locator (generic fallback)
|
|
743
956
|
if (element.parent?.className) {
|
|
744
957
|
const parentClass = element.parent.className.split(' ')[0];
|
|
745
958
|
if (parentClass) {
|
|
@@ -1194,6 +1407,10 @@ class PageSoMHandler {
|
|
|
1194
1407
|
await this.page.mouse.up();
|
|
1195
1408
|
playwrightCommand = `await page.mouse.move(${pixelX}, ${pixelY}); await page.mouse.down(); await page.mouse.move(${toPixelX}, ${toPixelY}); await page.mouse.up()`;
|
|
1196
1409
|
break;
|
|
1410
|
+
case som_types_1.InteractionAction.PRESS:
|
|
1411
|
+
case som_types_1.InteractionAction.PRESS_SEQUENTIALLY:
|
|
1412
|
+
// Keyboard press is page-level - coordinates don't make sense
|
|
1413
|
+
throw new Error(`PRESS action cannot use coordinates. To scroll: use SCROLL action with scrollDirection/scrollAmount. To press keys: use PRESS without coord.`);
|
|
1197
1414
|
default:
|
|
1198
1415
|
throw new Error(`Coordinate-based execution not supported for action: ${action}`);
|
|
1199
1416
|
}
|