chrometools-mcp 3.3.6 → 3.3.9
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/CHANGELOG.md +87 -0
- package/README.md +129 -24
- package/SPEC-pom-integration.md +227 -0
- package/SPEC-swagger-api-tools.md +3101 -0
- package/browser/page-manager.js +83 -0
- package/index.js +623 -201
- package/package.json +2 -1
- package/pom/apom-tree-converter.js +287 -51
- package/recorder/page-object-generator.js +45 -1
- package/server/tool-definitions.js +57 -6
- package/server/tool-schemas.js +31 -0
- package/test-swagger-phase1.mjs +959 -0
- package/utils/api-generators/api-models-python.js +448 -0
- package/utils/api-generators/api-models-typescript.js +375 -0
- package/utils/code-generators/code-generator-base.js +111 -6
- package/utils/code-generators/playwright-python.js +74 -0
- package/utils/code-generators/playwright-typescript.js +69 -0
- package/utils/code-generators/pom-integrator.js +373 -0
- package/utils/code-generators/selenium-java.js +72 -0
- package/utils/code-generators/selenium-python.js +75 -0
- package/utils/hints-generator.js +114 -19
- package/utils/openapi/helpers.js +25 -0
- package/utils/openapi/parser.js +448 -0
- package/utils/openapi/ref-resolver.js +149 -0
- package/utils/openapi/type-mapper.js +174 -0
- package/nul +0 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "chrometools-mcp",
|
|
3
|
-
"version": "3.3.
|
|
3
|
+
"version": "3.3.9",
|
|
4
4
|
"description": "MCP (Model Context Protocol) server for Chrome automation using Puppeteer. Persistent browser sessions, UI framework detection (MUI, Ant Design, etc.), Page Object support, visual testing, Figma comparison. Works seamlessly in WSL, Linux, macOS, and Windows.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -48,6 +48,7 @@
|
|
|
48
48
|
"dependencies": {
|
|
49
49
|
"@modelcontextprotocol/sdk": "^1.20.2",
|
|
50
50
|
"jimp": "^0.22.12",
|
|
51
|
+
"js-yaml": "^4.1.1",
|
|
51
52
|
"pixelmatch": "^7.1.0",
|
|
52
53
|
"puppeteer": "^24.27.0",
|
|
53
54
|
"ws": "^8.18.0",
|
|
@@ -10,9 +10,10 @@
|
|
|
10
10
|
* Runs in browser context via page.evaluate()
|
|
11
11
|
*
|
|
12
12
|
* @param {boolean} interactiveOnly - Only include interactive elements and their parents
|
|
13
|
+
* @param {boolean} viewportOnly - Only include elements visible in current viewport
|
|
13
14
|
* @returns {Object} APOM tree structure
|
|
14
15
|
*/
|
|
15
|
-
function buildAPOMTree(interactiveOnly = true) {
|
|
16
|
+
function buildAPOMTree(interactiveOnly = true, viewportOnly = false) {
|
|
16
17
|
const pageId = `page_${btoa(window.location.href).replace(/[^a-zA-Z0-9]/g, '').substring(0, 20)}_${Date.now()}`;
|
|
17
18
|
|
|
18
19
|
const result = {
|
|
@@ -26,7 +27,14 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
26
27
|
interactiveCount: 0,
|
|
27
28
|
formCount: 0,
|
|
28
29
|
modalCount: 0,
|
|
29
|
-
maxDepth: 0
|
|
30
|
+
maxDepth: 0,
|
|
31
|
+
viewportOnly: viewportOnly || undefined,
|
|
32
|
+
viewport: viewportOnly ? {
|
|
33
|
+
width: window.innerWidth || document.documentElement.clientWidth,
|
|
34
|
+
height: window.innerHeight || document.documentElement.clientHeight,
|
|
35
|
+
scrollX: window.scrollX || window.pageXOffset,
|
|
36
|
+
scrollY: window.scrollY || window.pageYOffset
|
|
37
|
+
} : undefined
|
|
30
38
|
}
|
|
31
39
|
};
|
|
32
40
|
|
|
@@ -51,8 +59,122 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
51
59
|
// Collect radio and checkbox groups for easier agent access
|
|
52
60
|
result.groups = collectInputGroups(result.tree);
|
|
53
61
|
|
|
62
|
+
// Detect page alerts, warnings, errors, and restrictions
|
|
63
|
+
result.alerts = detectPageAlerts();
|
|
64
|
+
|
|
54
65
|
return result;
|
|
55
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Detect page alerts, warnings, errors, toasts, and restrictions
|
|
69
|
+
* Uses heuristics to find notification elements
|
|
70
|
+
*/
|
|
71
|
+
function detectPageAlerts() {
|
|
72
|
+
const alerts = [];
|
|
73
|
+
const seenTexts = new Set();
|
|
74
|
+
const MIN_TEXT_LENGTH = 10; // Ignore very short texts like "19", "OK"
|
|
75
|
+
|
|
76
|
+
// Helper to add alert avoiding duplicates and substrings
|
|
77
|
+
function addAlert(type, text, source) {
|
|
78
|
+
text = text?.trim();
|
|
79
|
+
if (!text || text.length < MIN_TEXT_LENGTH || text.length > 300) return;
|
|
80
|
+
|
|
81
|
+
// Normalize text
|
|
82
|
+
const normalizedText = text.substring(0, 200);
|
|
83
|
+
|
|
84
|
+
// Skip if already seen or is substring of existing
|
|
85
|
+
if (seenTexts.has(normalizedText)) return;
|
|
86
|
+
for (const seen of seenTexts) {
|
|
87
|
+
if (seen.includes(normalizedText) || normalizedText.includes(seen)) return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
seenTexts.add(normalizedText);
|
|
91
|
+
alerts.push({ type, text: normalizedText, source });
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// 1. Elements with role="alert" or aria-live (highest priority)
|
|
95
|
+
document.querySelectorAll('[role="alert"], [aria-live="assertive"]').forEach(el => {
|
|
96
|
+
const text = el.textContent?.trim();
|
|
97
|
+
addAlert('alert', text, 'aria');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
// 2. Classes containing restriction keywords (high priority - affects functionality)
|
|
101
|
+
const restrictionSelector = '[class*="deactivat"], [class*="disabled"], [class*="blocked"], [class*="restrict"], [class*="suspend"], [class*="inactive"]';
|
|
102
|
+
document.querySelectorAll(restrictionSelector).forEach(el => {
|
|
103
|
+
if (!isVisibleForAlert(el)) return;
|
|
104
|
+
const text = el.textContent?.trim();
|
|
105
|
+
addAlert('restriction', text, 'class');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// 3. Error classes
|
|
109
|
+
document.querySelectorAll('[class*="error"], [class*="danger"], [class*="invalid"]').forEach(el => {
|
|
110
|
+
if (!isVisibleForAlert(el)) return;
|
|
111
|
+
const text = el.textContent?.trim();
|
|
112
|
+
addAlert('error', text, 'class');
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
// 4. Warning classes
|
|
116
|
+
document.querySelectorAll('[class*="warning"], [class*="warn"], [class*="caution"]').forEach(el => {
|
|
117
|
+
if (!isVisibleForAlert(el)) return;
|
|
118
|
+
const text = el.textContent?.trim();
|
|
119
|
+
addAlert('warning', text, 'class');
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
// 5. Toast/Snackbar notifications (usually important messages)
|
|
123
|
+
document.querySelectorAll('[class*="toast"], [class*="snackbar"], [class*="alert-banner"]').forEach(el => {
|
|
124
|
+
if (!isVisibleForAlert(el)) return;
|
|
125
|
+
const text = el.textContent?.trim();
|
|
126
|
+
addAlert('notification', text, 'toast');
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
// 6. SVG icons with warning colors - only for restriction/deactivated contexts
|
|
130
|
+
const warningColors = ['#FF3B30', '#FAB32F', '#FFCC00', '#FF9500', '#F44336'];
|
|
131
|
+
document.querySelectorAll('svg').forEach(svg => {
|
|
132
|
+
const svgHtml = svg.outerHTML.toLowerCase();
|
|
133
|
+
const hasWarningColor = warningColors.some(c => svgHtml.includes(c.toLowerCase()));
|
|
134
|
+
if (!hasWarningColor) return;
|
|
135
|
+
|
|
136
|
+
// Only consider if parent has restriction-related class or text
|
|
137
|
+
let parent = svg.parentElement;
|
|
138
|
+
let attempts = 0;
|
|
139
|
+
while (parent && attempts < 3) {
|
|
140
|
+
const className = parent.className?.toLowerCase() || '';
|
|
141
|
+
const text = parent.textContent?.trim() || '';
|
|
142
|
+
|
|
143
|
+
// Check if context suggests restriction/warning
|
|
144
|
+
const isRestrictionContext =
|
|
145
|
+
/deactivat|disabled|blocked|restrict|suspend|приостановлен|заблокирован/.test(className + ' ' + text.toLowerCase());
|
|
146
|
+
|
|
147
|
+
if (isRestrictionContext && text.length >= MIN_TEXT_LENGTH) {
|
|
148
|
+
addAlert('restriction', text, 'icon-color');
|
|
149
|
+
break;
|
|
150
|
+
}
|
|
151
|
+
parent = parent.parentElement;
|
|
152
|
+
attempts++;
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
// 7. Elements with semantic key/name attributes
|
|
157
|
+
document.querySelectorAll('[key*="error"], [key*="warning"], [key*="deactivat"], [key*="block"]').forEach(el => {
|
|
158
|
+
const parent = el.closest('div, span, p');
|
|
159
|
+
if (parent) {
|
|
160
|
+
const text = parent.textContent?.trim();
|
|
161
|
+
addAlert('warning', text, 'semantic-attr');
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// Return only if there are meaningful alerts
|
|
166
|
+
return alerts.length > 0 ? alerts : undefined;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Check if element is visible (for alert detection)
|
|
171
|
+
*/
|
|
172
|
+
function isVisibleForAlert(el) {
|
|
173
|
+
if (el.offsetWidth === 0 || el.offsetHeight === 0) return false;
|
|
174
|
+
const style = window.getComputedStyle(el);
|
|
175
|
+
return style.display !== 'none' && style.visibility !== 'hidden' && style.opacity !== '0';
|
|
176
|
+
}
|
|
177
|
+
|
|
56
178
|
/**
|
|
57
179
|
* Collect radio and checkbox groups from the tree
|
|
58
180
|
*/
|
|
@@ -148,6 +270,32 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
148
270
|
return node;
|
|
149
271
|
}
|
|
150
272
|
|
|
273
|
+
/**
|
|
274
|
+
* Filter out null, undefined, and empty string values from object
|
|
275
|
+
* to reduce JSON output size
|
|
276
|
+
*/
|
|
277
|
+
function filterNullValues(obj) {
|
|
278
|
+
if (!obj || typeof obj !== 'object') return obj;
|
|
279
|
+
|
|
280
|
+
const result = {};
|
|
281
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
282
|
+
// Skip null, undefined, and empty strings
|
|
283
|
+
if (value === null || value === undefined || value === '') continue;
|
|
284
|
+
// Skip false for boolean fields (keep true)
|
|
285
|
+
if (value === false) continue;
|
|
286
|
+
// Recursively filter nested objects (but not arrays)
|
|
287
|
+
if (typeof value === 'object' && !Array.isArray(value)) {
|
|
288
|
+
const filtered = filterNullValues(value);
|
|
289
|
+
if (Object.keys(filtered).length > 0) {
|
|
290
|
+
result[key] = filtered;
|
|
291
|
+
}
|
|
292
|
+
} else {
|
|
293
|
+
result[key] = value;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
return result;
|
|
297
|
+
}
|
|
298
|
+
|
|
151
299
|
/**
|
|
152
300
|
* Check if cursor:pointer is explicitly set (not inherited)
|
|
153
301
|
*/
|
|
@@ -206,9 +354,9 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
206
354
|
// tabindex (except -1)
|
|
207
355
|
(el.hasAttribute('tabindex') && el.getAttribute('tabindex') !== '-1') ||
|
|
208
356
|
// contenteditable
|
|
209
|
-
el.getAttribute('contenteditable') === 'true'
|
|
210
|
-
//
|
|
211
|
-
|
|
357
|
+
el.getAttribute('contenteditable') === 'true' ||
|
|
358
|
+
// Click event listeners (Angular, React, Vue) via monkey-patched addEventListener tracker
|
|
359
|
+
hasExplicitClickBinding(el)
|
|
212
360
|
);
|
|
213
361
|
|
|
214
362
|
if (isInteractive && isVisible(el)) {
|
|
@@ -229,6 +377,23 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
229
377
|
interactiveElements.add(document.body);
|
|
230
378
|
}
|
|
231
379
|
|
|
380
|
+
/**
|
|
381
|
+
* Check if element is in viewport
|
|
382
|
+
*/
|
|
383
|
+
function isInViewport(el) {
|
|
384
|
+
const rect = el.getBoundingClientRect();
|
|
385
|
+
const viewportHeight = window.innerHeight || document.documentElement.clientHeight;
|
|
386
|
+
const viewportWidth = window.innerWidth || document.documentElement.clientWidth;
|
|
387
|
+
|
|
388
|
+
// Element is in viewport if any part of it is visible
|
|
389
|
+
return (
|
|
390
|
+
rect.bottom > 0 &&
|
|
391
|
+
rect.right > 0 &&
|
|
392
|
+
rect.top < viewportHeight &&
|
|
393
|
+
rect.left < viewportWidth
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
232
397
|
/**
|
|
233
398
|
* Check if element is visible
|
|
234
399
|
* More reliable check that works with position:fixed elements (Angular Material, etc.)
|
|
@@ -248,6 +413,11 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
248
413
|
// For body element, always consider visible if dimensions > 0
|
|
249
414
|
if (el === document.body) return true;
|
|
250
415
|
|
|
416
|
+
// Check viewport if viewportOnly mode is enabled
|
|
417
|
+
if (viewportOnly && !isInViewport(el)) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
|
|
251
421
|
// Additional check: element should be in viewport or have offsetParent
|
|
252
422
|
// This handles elements inside position:fixed containers (Angular Material)
|
|
253
423
|
return el.offsetParent !== null || style.position === 'fixed' || style.position === 'sticky';
|
|
@@ -297,19 +467,23 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
297
467
|
node = {
|
|
298
468
|
id,
|
|
299
469
|
tag: element.tagName.toLowerCase(),
|
|
300
|
-
position,
|
|
301
470
|
type: elementType.type,
|
|
302
471
|
children: []
|
|
303
472
|
};
|
|
304
473
|
|
|
474
|
+
// Only include position if not static (position is null for static)
|
|
475
|
+
if (position) {
|
|
476
|
+
node.position = position;
|
|
477
|
+
}
|
|
478
|
+
|
|
305
479
|
// Add selector only in includeAll mode
|
|
306
480
|
if (!interactiveOnly) {
|
|
307
481
|
node.selector = selector;
|
|
308
482
|
}
|
|
309
483
|
|
|
310
|
-
// Add metadata for interactive elements
|
|
484
|
+
// Add metadata for interactive elements, filtering out null/undefined values
|
|
311
485
|
if (elementType.metadata) {
|
|
312
|
-
node.metadata = elementType.metadata;
|
|
486
|
+
node.metadata = filterNullValues(elementType.metadata);
|
|
313
487
|
}
|
|
314
488
|
} else {
|
|
315
489
|
// Containers: compact format "tag_id": [children] when interactiveOnly
|
|
@@ -340,8 +514,8 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
340
514
|
if (elementType.type === 'form') {
|
|
341
515
|
result.metadata.formCount++;
|
|
342
516
|
}
|
|
343
|
-
if (position.type === 'fixed' || position.type === 'absolute') {
|
|
344
|
-
if (position.zIndex >= 100) {
|
|
517
|
+
if (position && (position.type === 'fixed' || position.type === 'absolute')) {
|
|
518
|
+
if (position.zIndex && position.zIndex >= 100) {
|
|
345
519
|
result.metadata.modalCount++;
|
|
346
520
|
}
|
|
347
521
|
}
|
|
@@ -365,6 +539,11 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
365
539
|
return compactNode;
|
|
366
540
|
}
|
|
367
541
|
|
|
542
|
+
// Remove empty children array to reduce output size
|
|
543
|
+
if (node.children && node.children.length === 0) {
|
|
544
|
+
delete node.children;
|
|
545
|
+
}
|
|
546
|
+
|
|
368
547
|
return node;
|
|
369
548
|
}
|
|
370
549
|
|
|
@@ -379,37 +558,30 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
379
558
|
|
|
380
559
|
/**
|
|
381
560
|
* Get positioning information
|
|
561
|
+
* Returns null for static position (default) to reduce output size
|
|
382
562
|
*/
|
|
383
563
|
function getPositionInfo(element) {
|
|
384
564
|
const style = window.getComputedStyle(element);
|
|
385
565
|
const position = style.position;
|
|
386
|
-
const zIndex = style.zIndex === 'auto' ? 'auto' : parseInt(style.zIndex, 10);
|
|
387
566
|
|
|
388
|
-
//
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
(position === 'absolute' && zIndex !== 'auto') ||
|
|
393
|
-
(position === 'relative' && zIndex !== 'auto') ||
|
|
394
|
-
parseFloat(style.opacity) < 1 ||
|
|
395
|
-
style.transform !== 'none' ||
|
|
396
|
-
style.filter !== 'none' ||
|
|
397
|
-
style.perspective !== 'none' ||
|
|
398
|
-
style.clipPath !== 'none' ||
|
|
399
|
-
style.mask !== 'none' ||
|
|
400
|
-
style.mixBlendMode !== 'normal' ||
|
|
401
|
-
style.isolation === 'isolate';
|
|
567
|
+
// Skip static position (default) - no need to include
|
|
568
|
+
if (position === 'static') {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
402
571
|
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
hasBackdrop: style.backgroundColor !== 'rgba(0, 0, 0, 0)' &&
|
|
409
|
-
(position === 'fixed' || position === 'absolute'),
|
|
410
|
-
isFullscreen: element.offsetWidth >= window.innerWidth * 0.9 &&
|
|
411
|
-
element.offsetHeight >= window.innerHeight * 0.9
|
|
572
|
+
const zIndex = style.zIndex === 'auto' ? 'auto' : parseInt(style.zIndex, 10);
|
|
573
|
+
|
|
574
|
+
// Only include non-default values
|
|
575
|
+
const result = {
|
|
576
|
+
type: position
|
|
412
577
|
};
|
|
578
|
+
|
|
579
|
+
// Only include zIndex if it's not 'auto'
|
|
580
|
+
if (zIndex !== 'auto') {
|
|
581
|
+
result.zIndex = zIndex;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
return result;
|
|
413
585
|
}
|
|
414
586
|
|
|
415
587
|
/**
|
|
@@ -426,24 +598,80 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
426
598
|
}
|
|
427
599
|
|
|
428
600
|
/**
|
|
429
|
-
* Check if element
|
|
601
|
+
* Check if tag is a custom element (Web Component / Framework component)
|
|
602
|
+
*/
|
|
603
|
+
function isCustomElement(tag) {
|
|
604
|
+
// Custom elements must contain a hyphen
|
|
605
|
+
return tag.includes('-');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Check if element has explicit click binding (framework attributes or addEventListener)
|
|
610
|
+
* Uses monkey-patched addEventListener tracker for framework detection (Angular, React, Vue)
|
|
430
611
|
*/
|
|
431
|
-
function
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
612
|
+
function hasExplicitClickBinding(element) {
|
|
613
|
+
// Check for framework-specific click bindings (attributes)
|
|
614
|
+
const attrs = element.attributes;
|
|
615
|
+
for (let i = 0; i < attrs.length; i++) {
|
|
616
|
+
const name = attrs[i].name.toLowerCase();
|
|
617
|
+
// Angular: (click), Angular.js: ng-click
|
|
618
|
+
// Vue: @click, v-on:click
|
|
619
|
+
// React: onClick (but this is a property, not attribute)
|
|
620
|
+
if (name === '(click)' || name === 'ng-click' ||
|
|
621
|
+
name === '@click' || name === 'v-on:click' ||
|
|
622
|
+
name.startsWith('on') && name.includes('click')) {
|
|
623
|
+
return true;
|
|
437
624
|
}
|
|
625
|
+
}
|
|
626
|
+
// Check onclick property
|
|
627
|
+
if (element.onclick) return true;
|
|
628
|
+
// Check onclick attribute
|
|
629
|
+
if (element.hasAttribute('onclick')) return true;
|
|
630
|
+
|
|
631
|
+
// Check for click listeners added via addEventListener (framework detection)
|
|
632
|
+
// This is injected by page-manager.js via evaluateOnNewDocument
|
|
633
|
+
if (typeof window.__hasClickListener === 'function' && window.__hasClickListener(element)) {
|
|
634
|
+
return true;
|
|
635
|
+
}
|
|
438
636
|
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
/**
|
|
641
|
+
* Find click handler for interactive elements (bubbling search)
|
|
642
|
+
* Searches UP from element to find ancestor with explicit click binding
|
|
643
|
+
* Returns "tag:id" format like "div:container_19" or null if not found
|
|
644
|
+
*/
|
|
645
|
+
function findClickHandler(element) {
|
|
646
|
+
const tag = element.tagName.toLowerCase();
|
|
647
|
+
|
|
648
|
+
// Check self first - native interactive elements handle their own clicks
|
|
649
|
+
if (hasExplicitClickBinding(element) ||
|
|
650
|
+
['button', 'a', 'input', 'select'].includes(tag) ||
|
|
651
|
+
element.getAttribute('role') === 'button') {
|
|
652
|
+
return null; // element handles its own click, no need for clickTarget
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Search UP (like event bubbling) - find ancestor with explicit click handler
|
|
656
|
+
let parent = element.parentElement;
|
|
657
|
+
let depth = 0;
|
|
658
|
+
const maxDepth = 5; // limit to prevent hanging
|
|
659
|
+
|
|
660
|
+
while (parent && depth < maxDepth) {
|
|
661
|
+
if (hasExplicitClickBinding(parent)) {
|
|
662
|
+
// Found real click handler
|
|
663
|
+
const parentId = elementIds.get(parent);
|
|
664
|
+
if (parentId) {
|
|
665
|
+
const parentTag = parent.tagName.toLowerCase();
|
|
666
|
+
return `${parentTag}:${parentId}`;
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
parent = parent.parentElement;
|
|
670
|
+
depth++;
|
|
446
671
|
}
|
|
672
|
+
|
|
673
|
+
// No click handler found - return null (no fallback!)
|
|
674
|
+
return null;
|
|
447
675
|
}
|
|
448
676
|
|
|
449
677
|
/**
|
|
@@ -483,8 +711,8 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
483
711
|
return { isInteractive: true, reason: 'cursor-pointer' };
|
|
484
712
|
}
|
|
485
713
|
|
|
486
|
-
// 6. Elements with click event listeners
|
|
487
|
-
if (
|
|
714
|
+
// 6. Elements with click event listeners (framework handlers: Angular, React, Vue)
|
|
715
|
+
if (hasExplicitClickBinding(element)) {
|
|
488
716
|
return { isInteractive: true, reason: 'event-listener' };
|
|
489
717
|
}
|
|
490
718
|
|
|
@@ -680,12 +908,20 @@ function buildAPOMTree(interactiveOnly = true) {
|
|
|
680
908
|
|
|
681
909
|
// Generic container - check for JavaScript interactivity
|
|
682
910
|
const interactivityCheck = checkInteractivity(element);
|
|
911
|
+
|
|
912
|
+
// For ALL interactive elements, search for click handler (bubbling)
|
|
913
|
+
// Returns "tag:id" format or null if not found
|
|
914
|
+
const clickTarget = interactivityCheck.isInteractive
|
|
915
|
+
? findClickHandler(element)
|
|
916
|
+
: null;
|
|
917
|
+
|
|
683
918
|
return {
|
|
684
919
|
type: 'container',
|
|
685
920
|
isInteractive: interactivityCheck.isInteractive,
|
|
686
921
|
metadata: interactivityCheck.isInteractive ? {
|
|
687
922
|
text: element.textContent?.trim().substring(0, 100) || '',
|
|
688
|
-
interactivityReason: interactivityCheck.reason
|
|
923
|
+
interactivityReason: interactivityCheck.reason,
|
|
924
|
+
clickTarget: clickTarget || undefined
|
|
689
925
|
} : null
|
|
690
926
|
};
|
|
691
927
|
}
|
|
@@ -33,6 +33,19 @@ export async function generatePageObject(page, options = {}) {
|
|
|
33
33
|
// Generate code based on framework
|
|
34
34
|
const code = await generateCode(finalClassName, elementGroups, pageAnalysis, framework, includeComments);
|
|
35
35
|
|
|
36
|
+
// Build structured elements metadata for POM integration
|
|
37
|
+
const allElements = Object.values(elementGroups).flat();
|
|
38
|
+
const uniqueElements = deduplicateElements(allElements);
|
|
39
|
+
const lang = framework.includes('python') ? 'python' : framework.includes('java') ? 'java' : 'typescript';
|
|
40
|
+
const elements = uniqueElements.map(el => ({
|
|
41
|
+
name: sanitizeIdentifier(el.name, lang),
|
|
42
|
+
selector: el.selector,
|
|
43
|
+
tag: el.tag,
|
|
44
|
+
type: el.type,
|
|
45
|
+
methodName: generateMethodName(el, framework),
|
|
46
|
+
methodType: getMethodType(el)
|
|
47
|
+
}));
|
|
48
|
+
|
|
36
49
|
return {
|
|
37
50
|
success: true,
|
|
38
51
|
className: finalClassName,
|
|
@@ -41,7 +54,8 @@ export async function generatePageObject(page, options = {}) {
|
|
|
41
54
|
elementCount: pageAnalysis.elements.length,
|
|
42
55
|
groups: Object.keys(elementGroups),
|
|
43
56
|
framework,
|
|
44
|
-
code
|
|
57
|
+
code,
|
|
58
|
+
elements
|
|
45
59
|
};
|
|
46
60
|
}
|
|
47
61
|
|
|
@@ -728,6 +742,36 @@ function generateSeleniumJavaActionMethods(lines, elements) {
|
|
|
728
742
|
});
|
|
729
743
|
}
|
|
730
744
|
|
|
745
|
+
/**
|
|
746
|
+
* Helper: Determine method type for element
|
|
747
|
+
* @param {Object} el - Element info
|
|
748
|
+
* @returns {string} - "fill" | "click" | "select"
|
|
749
|
+
*/
|
|
750
|
+
function getMethodType(el) {
|
|
751
|
+
if (el.tag === 'select') return 'select';
|
|
752
|
+
if (el.tag === 'input' || el.tag === 'textarea') return 'fill';
|
|
753
|
+
return 'click';
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Helper: Generate method name for element based on framework
|
|
758
|
+
* @param {Object} el - Element info
|
|
759
|
+
* @param {string} framework - Target framework
|
|
760
|
+
* @returns {string} - Method name (e.g., "fillUsername", "clickSubmit", "fill_username")
|
|
761
|
+
*/
|
|
762
|
+
function generateMethodName(el, framework) {
|
|
763
|
+
const methodType = getMethodType(el);
|
|
764
|
+
const isPython = framework.includes('python');
|
|
765
|
+
const lang = isPython ? 'python' : framework.includes('java') ? 'java' : 'typescript';
|
|
766
|
+
const name = sanitizeIdentifier(el.name, lang);
|
|
767
|
+
|
|
768
|
+
if (isPython) {
|
|
769
|
+
return `${methodType}_${name}`;
|
|
770
|
+
}
|
|
771
|
+
// TypeScript/Java: camelCase
|
|
772
|
+
return `${methodType}${capitalize(name)}`;
|
|
773
|
+
}
|
|
774
|
+
|
|
731
775
|
/**
|
|
732
776
|
* Helper: Capitalize first letter
|
|
733
777
|
*/
|
|
@@ -238,7 +238,7 @@ export const toolDefinitions = [
|
|
|
238
238
|
},
|
|
239
239
|
{
|
|
240
240
|
name: "drag",
|
|
241
|
-
description: "Drag element in any direction. For maps, charts, SVG, canvas, sliders. Use scrollHorizontal for scrollbars.",
|
|
241
|
+
description: "Drag element in any direction. For maps, charts, SVG, canvas, sliders. Use mode='synthetic' for JS libraries (frappe-gantt, jQuery UI). Use scrollHorizontal for scrollbars.",
|
|
242
242
|
inputSchema: {
|
|
243
243
|
type: "object",
|
|
244
244
|
properties: {
|
|
@@ -246,6 +246,7 @@ export const toolDefinitions = [
|
|
|
246
246
|
direction: { type: "string", enum: ["up", "down", "left", "right", "up-left", "up-right", "down-left", "down-right"], description: "Drag direction" },
|
|
247
247
|
distance: { type: "number", description: "Distance in pixels (default: 100)" },
|
|
248
248
|
duration: { type: "number", description: "Drag duration in ms (default: 500)" },
|
|
249
|
+
mode: { type: "string", enum: ["native", "synthetic"], description: "Drag mode: 'native' (default, faster) or 'synthetic' (better for JS libraries)" },
|
|
249
250
|
},
|
|
250
251
|
required: ["selector", "direction"],
|
|
251
252
|
},
|
|
@@ -477,7 +478,7 @@ export const toolDefinitions = [
|
|
|
477
478
|
},
|
|
478
479
|
{
|
|
479
480
|
name: "analyzePage",
|
|
480
|
-
description: "PRIMARY tool for reading page state. Returns APOM tree: {tree, metadata, groups}. Compact format (default): containers as \"tag_id\":[children] keys, interactive elements as {id, tag, type, position, metadata} without selectors. Use element IDs (e.g., button_45, input_20) with click/type tools. Selectors registered internally for resolution. Use refresh:true after clicks. Efficient: 8-10k tokens vs screenshot 15-25k.",
|
|
481
|
+
description: "PRIMARY tool for reading page state. Returns APOM tree: {tree, metadata, groups}. Compact format (default): containers as \"tag_id\":[children] keys, interactive elements as {id, tag, type, position, metadata} without selectors. Use element IDs (e.g., button_45, input_20) with click/type tools. Selectors registered internally for resolution. Use refresh:true after clicks. Efficient: 8-10k tokens vs screenshot 15-25k. Legend: clickTarget format is \"tag:id\" (e.g., \"kp-chats-list-item:container_58\") - use the id part for clicking. No clickTarget = element handles its own click.",
|
|
481
482
|
inputSchema: {
|
|
482
483
|
type: "object",
|
|
483
484
|
properties: {
|
|
@@ -486,6 +487,8 @@ export const toolDefinitions = [
|
|
|
486
487
|
useLegacyFormat: { type: "boolean", description: "Return legacy format instead of APOM (default: false - APOM is now default)" },
|
|
487
488
|
registerElements: { type: "boolean", description: "Auto-register elements in selector resolver (default: true)" },
|
|
488
489
|
groupBy: { type: "string", description: "Group elements: 'type' or 'flat' (default: 'type')", enum: ["type", "flat"] },
|
|
490
|
+
viewportOnly: { type: "boolean", description: "Only analyze elements in current viewport (default: false). Reduces output for long pages." },
|
|
491
|
+
diff: { type: "boolean", description: "Return only changes since last analysis: {added, removed, changed} (default: false)." },
|
|
489
492
|
},
|
|
490
493
|
},
|
|
491
494
|
},
|
|
@@ -683,7 +686,7 @@ export const toolDefinitions = [
|
|
|
683
686
|
},
|
|
684
687
|
{
|
|
685
688
|
name: "exportScenarioAsCode",
|
|
686
|
-
description: "Export scenario as test code for NEW file.
|
|
689
|
+
description: "Export scenario as test code for NEW file. Supports Page Object integration: 'generate-integrated' generates POM + test using it, 'use-existing' generates test using existing POM file. Use appendScenarioToFile for existing files.",
|
|
687
690
|
inputSchema: {
|
|
688
691
|
type: "object",
|
|
689
692
|
properties: {
|
|
@@ -706,19 +709,28 @@ export const toolDefinitions = [
|
|
|
706
709
|
},
|
|
707
710
|
generatePageObject: {
|
|
708
711
|
type: "boolean",
|
|
709
|
-
description: "Also generate Page Object class for the page (default: false)"
|
|
712
|
+
description: "Also generate Page Object class for the page (default: false). Legacy - use pageObjectMode instead."
|
|
710
713
|
},
|
|
711
714
|
pageObjectClassName: {
|
|
712
715
|
type: "string",
|
|
713
716
|
description: "Page Object class name (optional, auto-generated if not provided)"
|
|
714
717
|
},
|
|
718
|
+
pageObjectMode: {
|
|
719
|
+
type: "string",
|
|
720
|
+
enum: ["none", "generate", "generate-integrated", "use-existing"],
|
|
721
|
+
description: "POM integration: 'none' (default), 'generate' (separate POM), 'generate-integrated' (POM + test using it), 'use-existing' (test uses existing POM file)"
|
|
722
|
+
},
|
|
723
|
+
pageObjectFile: {
|
|
724
|
+
type: "string",
|
|
725
|
+
description: "Path to existing POM file (required for 'use-existing' mode)"
|
|
726
|
+
},
|
|
715
727
|
},
|
|
716
728
|
required: ["scenarioName", "language"],
|
|
717
729
|
},
|
|
718
730
|
},
|
|
719
731
|
{
|
|
720
732
|
name: "appendScenarioToFile",
|
|
721
|
-
description: "Append scenario as test code to EXISTING file.
|
|
733
|
+
description: "Append scenario as test code to EXISTING file. Supports Page Object integration: 'generate-integrated' generates POM + test using it, 'use-existing' generates test using existing POM file. Use exportScenarioAsCode for new files.",
|
|
722
734
|
inputSchema: {
|
|
723
735
|
type: "object",
|
|
724
736
|
properties: {
|
|
@@ -758,7 +770,16 @@ export const toolDefinitions = [
|
|
|
758
770
|
},
|
|
759
771
|
generatePageObject: {
|
|
760
772
|
type: "boolean",
|
|
761
|
-
description: "Also generate Page Object class for the page (default: false)"
|
|
773
|
+
description: "Also generate Page Object class for the page (default: false). Legacy - use pageObjectMode instead."
|
|
774
|
+
},
|
|
775
|
+
pageObjectMode: {
|
|
776
|
+
type: "string",
|
|
777
|
+
enum: ["none", "generate", "generate-integrated", "use-existing"],
|
|
778
|
+
description: "POM integration: 'none' (default), 'generate' (separate POM), 'generate-integrated' (POM + test using it), 'use-existing' (test uses existing POM file)"
|
|
779
|
+
},
|
|
780
|
+
pageObjectFile: {
|
|
781
|
+
type: "string",
|
|
782
|
+
description: "Path to existing POM file (required for 'use-existing' mode)"
|
|
762
783
|
},
|
|
763
784
|
pageObjectClassName: {
|
|
764
785
|
type: "string",
|
|
@@ -819,4 +840,34 @@ export const toolDefinitions = [
|
|
|
819
840
|
required: ["tab"],
|
|
820
841
|
},
|
|
821
842
|
},
|
|
843
|
+
{
|
|
844
|
+
name: "loadSwagger",
|
|
845
|
+
description: "Load and parse OpenAPI/Swagger spec from URL or local file. Returns structured summary: endpoints, schemas, auth types, base URL. Supports both OpenAPI 2.0 (Swagger) and 3.x, JSON and YAML formats. Use this first to understand an API before generating models or client code.",
|
|
846
|
+
inputSchema: {
|
|
847
|
+
type: "object",
|
|
848
|
+
properties: {
|
|
849
|
+
source: { type: "string", description: "URL (http/https) or local file path to swagger.json / openapi.yaml" },
|
|
850
|
+
format: { type: "string", enum: ["auto", "json", "yaml"], description: "Spec format. 'auto' (default) detects from extension/content" },
|
|
851
|
+
},
|
|
852
|
+
required: ["source"],
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: "generateApiModels",
|
|
857
|
+
description: "Generate typed data models from OpenAPI/Swagger spec. Creates TypeScript interfaces/types or Python dataclasses/pydantic/TypedDict from API schemas. Handles $ref resolution, enums, allOf/oneOf, nested objects. Use after loadSwagger to generate model files.",
|
|
858
|
+
inputSchema: {
|
|
859
|
+
type: "object",
|
|
860
|
+
properties: {
|
|
861
|
+
source: { type: "string", description: "URL or file path to OpenAPI spec" },
|
|
862
|
+
language: { type: "string", enum: ["typescript", "python"], description: "Target language for models" },
|
|
863
|
+
format: { type: "string", enum: ["auto", "json", "yaml"], description: "Spec format (default: auto)" },
|
|
864
|
+
style: { type: "string", enum: ["interface", "type"], description: "TypeScript only: 'interface' (default) or 'type' aliases" },
|
|
865
|
+
pythonStyle: { type: "string", enum: ["dataclass", "pydantic", "typeddict"], description: "Python only: 'dataclass' (default), 'pydantic', or 'typeddict'" },
|
|
866
|
+
includeEnums: { type: "boolean", description: "Generate enum types (default: true)" },
|
|
867
|
+
includeValidation: { type: "boolean", description: "Include validation constraints as comments (default: false)" },
|
|
868
|
+
schemas: { type: "array", items: { type: "string" }, description: "Generate only these schemas (default: all)" },
|
|
869
|
+
},
|
|
870
|
+
required: ["source", "language"],
|
|
871
|
+
},
|
|
872
|
+
},
|
|
822
873
|
];
|