chrometools-mcp 3.3.8 → 3.4.0
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 +51 -0
- package/README.md +159 -24
- package/SPEC-pom-integration.md +227 -0
- package/SPEC-swagger-api-tools.md +3101 -0
- package/index.js +591 -209
- package/package.json +2 -1
- package/pom/apom-tree-converter.js +5 -26
- package/recorder/page-object-generator.js +45 -1
- package/server/tool-definitions.js +54 -5
- package/server/tool-schemas.js +29 -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 +159 -24
- 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/utils/post-click-diagnostics.js +14 -4
- package/nul +0 -0
package/index.js
CHANGED
|
@@ -72,6 +72,12 @@ import {PlaywrightPythonGenerator} from './utils/code-generators/playwright-pyth
|
|
|
72
72
|
import {SeleniumPythonGenerator} from './utils/code-generators/selenium-python.js';
|
|
73
73
|
import {SeleniumJavaGenerator} from './utils/code-generators/selenium-java.js';
|
|
74
74
|
import {FileAppender} from './utils/code-generators/file-appender.js';
|
|
75
|
+
import {parsePomFile} from './utils/code-generators/pom-integrator.js';
|
|
76
|
+
|
|
77
|
+
// Import OpenAPI / Swagger tools
|
|
78
|
+
import {OpenAPIParser} from './utils/openapi/parser.js';
|
|
79
|
+
import {ApiModelsTypeScriptGenerator} from './utils/api-generators/api-models-typescript.js';
|
|
80
|
+
import {ApiModelsPythonGenerator} from './utils/api-generators/api-models-python.js';
|
|
75
81
|
// Import Figma tools
|
|
76
82
|
import {
|
|
77
83
|
collectAllText,
|
|
@@ -343,6 +349,13 @@ async function executeToolInternal(name, args) {
|
|
|
343
349
|
|
|
344
350
|
let hintsText = '\n\n** AI HINTS **';
|
|
345
351
|
hintsText += `\nPage type: ${hints.pageType}`;
|
|
352
|
+
if (hints.heading) {
|
|
353
|
+
hintsText += `\nPage heading: "${hints.heading}"`;
|
|
354
|
+
}
|
|
355
|
+
if (hints.authRedirect) {
|
|
356
|
+
hintsText += `\n⚠️ AUTH REDIRECT: Page redirected to login (intended: ${hints.authRedirect.returnUrl || 'unknown'})`;
|
|
357
|
+
hintsText += `\n → Session/cookies not established. Login first, then retry navigation.`;
|
|
358
|
+
}
|
|
346
359
|
if (hints.availableActions.length > 0) {
|
|
347
360
|
hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
|
|
348
361
|
}
|
|
@@ -475,23 +488,74 @@ async function executeToolInternal(name, args) {
|
|
|
475
488
|
// ALWAYS scroll to element first to ensure it's in viewport
|
|
476
489
|
await element.evaluate(el => el.scrollIntoView({ behavior: 'instant', block: 'center' }));
|
|
477
490
|
|
|
478
|
-
// Click with
|
|
491
|
+
// Click with adaptive fallback strategy:
|
|
492
|
+
//
|
|
493
|
+
// Pre-check: elementFromPoint() determines if element is the topmost at its center.
|
|
494
|
+
// This decides the click path — Puppeteer's click() doesn't always throw on interception.
|
|
495
|
+
//
|
|
496
|
+
// Path A (element covered by another, e.g. small button under <a routerLink>):
|
|
497
|
+
// → JS element.click() — DOM dispatch bypasses coordinate hit-testing
|
|
498
|
+
//
|
|
499
|
+
// Path B (element is topmost — normal case):
|
|
500
|
+
// Tier 1: Puppeteer native click (trusted CDP events)
|
|
501
|
+
// Tier 2: page.mouse.click at coordinates (trusted CDP, no interception check)
|
|
502
|
+
// Tier 3: JS element.click() (untrusted, last resort)
|
|
503
|
+
// Trusted CDP events (Tier 1 & 2) are critical for Angular/Zone.js apps where
|
|
504
|
+
// untrusted .click() triggers change detection mid-dispatch, destroying *ngFor elements.
|
|
479
505
|
const clickWithTimeout = async (timeoutMs = 5000) => {
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
506
|
+
const withTimeout = (promise) => Promise.race([
|
|
507
|
+
promise,
|
|
508
|
+
new Promise((_, reject) => setTimeout(() => reject(new Error('click timeout')), timeoutMs))
|
|
509
|
+
]);
|
|
510
|
+
|
|
511
|
+
// Pre-check: is another element covering our target at its center coordinates?
|
|
512
|
+
const intercepted = await element.evaluate(el => {
|
|
513
|
+
const rect = el.getBoundingClientRect();
|
|
514
|
+
if (rect.width === 0 || rect.height === 0) return false;
|
|
515
|
+
const cx = rect.left + rect.width / 2;
|
|
516
|
+
const cy = rect.top + rect.height / 2;
|
|
517
|
+
const topEl = document.elementFromPoint(cx, cy);
|
|
518
|
+
// topEl is our element or a child of it — not intercepted
|
|
519
|
+
return topEl !== el && !el.contains(topEl);
|
|
490
520
|
});
|
|
521
|
+
|
|
522
|
+
if (intercepted) {
|
|
523
|
+
// Path A: Element is covered (e.g., small button under <a routerLink>)
|
|
524
|
+
// Coordinate clicks would hit the covering element — use DOM dispatch
|
|
525
|
+
await element.evaluate(el => el.click());
|
|
526
|
+
return;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Path B: Element is topmost — use trusted CDP events
|
|
530
|
+
// Tier 1: Puppeteer native click
|
|
531
|
+
try {
|
|
532
|
+
await withTimeout(element.click());
|
|
533
|
+
return;
|
|
534
|
+
} catch (e) { /* fall through to Tier 2 */ }
|
|
535
|
+
|
|
536
|
+
// Tier 2: CDP coordinate click — trusted events, bypasses Puppeteer's own checks
|
|
537
|
+
try {
|
|
538
|
+
const box = await element.boundingBox();
|
|
539
|
+
if (box) {
|
|
540
|
+
await withTimeout(page.mouse.click(box.x + box.width / 2, box.y + box.height / 2));
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
} catch (e) { /* fall through to Tier 3 */ }
|
|
544
|
+
|
|
545
|
+
// Tier 3: JS click — untrusted, last resort
|
|
546
|
+
await element.evaluate(el => el.click());
|
|
491
547
|
};
|
|
492
548
|
|
|
493
549
|
await clickWithTimeout();
|
|
494
550
|
|
|
551
|
+
// Check if element was detached from DOM during click (Angular *ngFor + Zone.js pattern)
|
|
552
|
+
let elementDetached = false;
|
|
553
|
+
try {
|
|
554
|
+
elementDetached = await element.evaluate(el => !el.parentNode);
|
|
555
|
+
} catch (e) {
|
|
556
|
+
// Element handle may be invalid if page navigated — not a detachment issue
|
|
557
|
+
}
|
|
558
|
+
|
|
495
559
|
// NEW POST-CLICK PATTERN:
|
|
496
560
|
// 1. Run post-click diagnostics (waits for network requests within 200ms, max 10s timeout)
|
|
497
561
|
let diagnostics;
|
|
@@ -521,10 +585,48 @@ async function executeToolInternal(name, args) {
|
|
|
521
585
|
|
|
522
586
|
// 3. Format output with hints and diagnostics
|
|
523
587
|
let hintsText = '\n\n** AI HINTS **';
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
588
|
+
|
|
589
|
+
// Modal: show title, body text, and actions
|
|
590
|
+
if (hints.modalOpened && hints.newElements.some(e => e.type === 'modal')) {
|
|
591
|
+
const modal = hints.newElements.find(e => e.type === 'modal');
|
|
592
|
+
let modalText = 'Modal opened';
|
|
593
|
+
if (modal.title) modalText += `: "${modal.title}"`;
|
|
594
|
+
if (modal.text) modalText += `\n ${modal.text}`;
|
|
595
|
+
if (modal.actions?.length) modalText += `\n Actions: [${modal.actions.join('] [')}]`;
|
|
596
|
+
hintsText += '\n' + modalText;
|
|
597
|
+
} else if (hints.modalOpened) {
|
|
598
|
+
hintsText += '\nModal opened - interact with it or close';
|
|
527
599
|
}
|
|
600
|
+
|
|
601
|
+
// Overlay: show items
|
|
602
|
+
const overlay = hints.newElements.find(e => e.type === 'dropdown' || e.type === 'menu');
|
|
603
|
+
if (overlay?.items?.length) {
|
|
604
|
+
const label = overlay.type === 'menu' ? 'Menu' : 'Dropdown';
|
|
605
|
+
hintsText += `\n${label} with ${overlay.totalCount} options: ${overlay.items.join(', ')}`;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// Other new elements (alerts, etc.)
|
|
609
|
+
const otherElements = hints.newElements.filter(e => e.type !== 'modal' && e.type !== 'dropdown' && e.type !== 'menu');
|
|
610
|
+
if (otherElements.length > 0) {
|
|
611
|
+
hintsText += `\nNew elements appeared: ${otherElements.map(e => e.text ? `${e.type}: ${e.text}` : e.type).join(', ')}`;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Auth redirect after click (session expired, protected route)
|
|
615
|
+
if (hints.authRedirect) {
|
|
616
|
+
hintsText += '\n⚠️ AUTH REDIRECT: Landed on login page. Session may have expired.';
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// Element detached during click (Angular *ngFor + Zone.js)
|
|
620
|
+
if (elementDetached) {
|
|
621
|
+
hintsText += '\n⚠️ ELEMENT DETACHED: Element was removed from DOM during click — handler did NOT fire.';
|
|
622
|
+
hintsText += '\n Cause: Angular Zone.js triggers change detection mid-click, *ngFor recreates elements.';
|
|
623
|
+
hintsText += '\n App fix: add trackBy to *ngFor, or cache array reference instead of returning new one.';
|
|
624
|
+
hintsText += '\n Workaround via executeScript:';
|
|
625
|
+
hintsText += "\n 1. Find component: ng.getComponent(document.querySelector('component-tag'))";
|
|
626
|
+
hintsText += "\n 2. Explore API: Object.keys(comp).filter(k => k.includes('Event'))";
|
|
627
|
+
hintsText += '\n 3. Emit: comp.someChangeEvent.emit(selectedOption)';
|
|
628
|
+
}
|
|
629
|
+
|
|
528
630
|
if (hints.suggestedNext.length > 0) {
|
|
529
631
|
hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
|
|
530
632
|
}
|
|
@@ -1245,6 +1347,7 @@ async function executeToolInternal(name, args) {
|
|
|
1245
1347
|
|
|
1246
1348
|
const distance = validatedArgs.distance || 100;
|
|
1247
1349
|
const duration = validatedArgs.duration || 500;
|
|
1350
|
+
const mode = validatedArgs.mode || 'native';
|
|
1248
1351
|
|
|
1249
1352
|
// Calculate drag deltas based on direction
|
|
1250
1353
|
let deltaX = 0;
|
|
@@ -1281,62 +1384,166 @@ async function executeToolInternal(name, args) {
|
|
|
1281
1384
|
break;
|
|
1282
1385
|
}
|
|
1283
1386
|
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
const
|
|
1287
|
-
|
|
1288
|
-
|
|
1387
|
+
if (mode === 'synthetic') {
|
|
1388
|
+
// Synthetic mode: dispatch DOM events for better JS library compatibility
|
|
1389
|
+
const result = await page.evaluate((selector, deltaX, deltaY, duration) => {
|
|
1390
|
+
const element = document.querySelector(selector);
|
|
1391
|
+
if (!element) {
|
|
1392
|
+
return { success: false, error: `Element not found: ${selector}` };
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
const rect = element.getBoundingClientRect();
|
|
1396
|
+
const startX = rect.left + rect.width / 2;
|
|
1397
|
+
const startY = rect.top + rect.height / 2;
|
|
1398
|
+
const endX = startX + deltaX;
|
|
1399
|
+
const endY = startY + deltaY;
|
|
1400
|
+
|
|
1401
|
+
// Helper to create mouse/pointer event
|
|
1402
|
+
const createEvent = (type, clientX, clientY, buttons = 0) => {
|
|
1403
|
+
// Try PointerEvent first (modern browsers)
|
|
1404
|
+
if (typeof PointerEvent !== 'undefined') {
|
|
1405
|
+
return new PointerEvent(type, {
|
|
1406
|
+
bubbles: true,
|
|
1407
|
+
cancelable: true,
|
|
1408
|
+
view: window,
|
|
1409
|
+
clientX,
|
|
1410
|
+
clientY,
|
|
1411
|
+
screenX: clientX,
|
|
1412
|
+
screenY: clientY,
|
|
1413
|
+
buttons,
|
|
1414
|
+
button: 0,
|
|
1415
|
+
pointerId: 1,
|
|
1416
|
+
pointerType: 'mouse',
|
|
1417
|
+
isPrimary: true
|
|
1418
|
+
});
|
|
1419
|
+
}
|
|
1420
|
+
// Fallback to MouseEvent
|
|
1421
|
+
return new MouseEvent(type, {
|
|
1422
|
+
bubbles: true,
|
|
1423
|
+
cancelable: true,
|
|
1424
|
+
view: window,
|
|
1425
|
+
clientX,
|
|
1426
|
+
clientY,
|
|
1427
|
+
screenX: clientX,
|
|
1428
|
+
screenY: clientY,
|
|
1429
|
+
buttons,
|
|
1430
|
+
button: 0
|
|
1431
|
+
});
|
|
1432
|
+
};
|
|
1433
|
+
|
|
1434
|
+
// Dispatch mousedown/pointerdown
|
|
1435
|
+
element.dispatchEvent(createEvent('pointerdown', startX, startY, 1));
|
|
1436
|
+
element.dispatchEvent(createEvent('mousedown', startX, startY, 1));
|
|
1437
|
+
|
|
1438
|
+
// Dispatch intermediate mousemove/pointermove events
|
|
1439
|
+
const steps = Math.max(10, Math.floor(duration / 20));
|
|
1440
|
+
const stepDelay = duration / steps;
|
|
1441
|
+
|
|
1442
|
+
return new Promise((resolve) => {
|
|
1443
|
+
let currentStep = 0;
|
|
1444
|
+
|
|
1445
|
+
const moveInterval = setInterval(() => {
|
|
1446
|
+
currentStep++;
|
|
1447
|
+
const progress = currentStep / steps;
|
|
1448
|
+
const currentX = startX + (deltaX * progress);
|
|
1449
|
+
const currentY = startY + (deltaY * progress);
|
|
1450
|
+
|
|
1451
|
+
element.dispatchEvent(createEvent('pointermove', currentX, currentY, 1));
|
|
1452
|
+
element.dispatchEvent(createEvent('mousemove', currentX, currentY, 1));
|
|
1453
|
+
|
|
1454
|
+
if (currentStep >= steps) {
|
|
1455
|
+
clearInterval(moveInterval);
|
|
1456
|
+
|
|
1457
|
+
// Dispatch mouseup/pointerup
|
|
1458
|
+
element.dispatchEvent(createEvent('pointerup', endX, endY, 0));
|
|
1459
|
+
element.dispatchEvent(createEvent('mouseup', endX, endY, 0));
|
|
1460
|
+
element.dispatchEvent(createEvent('click', endX, endY, 0));
|
|
1461
|
+
|
|
1462
|
+
resolve({
|
|
1463
|
+
success: true,
|
|
1464
|
+
startX: Math.round(startX),
|
|
1465
|
+
startY: Math.round(startY),
|
|
1466
|
+
endX: Math.round(endX),
|
|
1467
|
+
endY: Math.round(endY),
|
|
1468
|
+
mode: 'synthetic'
|
|
1469
|
+
});
|
|
1470
|
+
}
|
|
1471
|
+
}, stepDelay);
|
|
1472
|
+
});
|
|
1473
|
+
}, validatedArgs.selector, deltaX, deltaY, duration);
|
|
1474
|
+
|
|
1475
|
+
if (!result.success) {
|
|
1476
|
+
throw new Error(result.error);
|
|
1289
1477
|
}
|
|
1290
1478
|
|
|
1291
|
-
const rect = element.getBoundingClientRect();
|
|
1292
1479
|
return {
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1480
|
+
content: [{
|
|
1481
|
+
type: "text",
|
|
1482
|
+
text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px (${result.mode} mode):\n` +
|
|
1483
|
+
` Start position: (${result.startX}, ${result.startY})\n` +
|
|
1484
|
+
` End position: (${result.endX}, ${result.endY})\n` +
|
|
1485
|
+
` Delta: (${deltaX}px, ${deltaY}px)\n` +
|
|
1486
|
+
` Duration: ${duration}ms\n` +
|
|
1487
|
+
` Events: pointerdown → ${Math.floor(duration / 20)} × pointermove → pointerup`
|
|
1488
|
+
}],
|
|
1298
1489
|
};
|
|
1299
|
-
}
|
|
1490
|
+
} else {
|
|
1491
|
+
// Native mode: use Puppeteer mouse API (default, faster)
|
|
1492
|
+
const elementInfo = await page.evaluate((selector) => {
|
|
1493
|
+
const element = document.querySelector(selector);
|
|
1494
|
+
if (!element) {
|
|
1495
|
+
return { success: false, error: `Element not found: ${selector}` };
|
|
1496
|
+
}
|
|
1300
1497
|
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1498
|
+
const rect = element.getBoundingClientRect();
|
|
1499
|
+
return {
|
|
1500
|
+
success: true,
|
|
1501
|
+
centerX: rect.left + rect.width / 2,
|
|
1502
|
+
centerY: rect.top + rect.height / 2,
|
|
1503
|
+
width: rect.width,
|
|
1504
|
+
height: rect.height
|
|
1505
|
+
};
|
|
1506
|
+
}, validatedArgs.selector);
|
|
1507
|
+
|
|
1508
|
+
if (!elementInfo.success) {
|
|
1509
|
+
throw new Error(elementInfo.error);
|
|
1510
|
+
}
|
|
1304
1511
|
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
const endY = startY + deltaY;
|
|
1512
|
+
const startX = elementInfo.centerX;
|
|
1513
|
+
const startY = elementInfo.centerY;
|
|
1514
|
+
const endX = startX + deltaX;
|
|
1515
|
+
const endY = startY + deltaY;
|
|
1310
1516
|
|
|
1311
|
-
|
|
1312
|
-
|
|
1517
|
+
// Move to start position
|
|
1518
|
+
await page.mouse.move(startX, startY);
|
|
1313
1519
|
|
|
1314
|
-
|
|
1315
|
-
|
|
1520
|
+
// Press mouse button (start drag)
|
|
1521
|
+
await page.mouse.down();
|
|
1316
1522
|
|
|
1317
|
-
|
|
1318
|
-
|
|
1523
|
+
// Wait a bit to ensure drag is registered
|
|
1524
|
+
await new Promise(resolve => setTimeout(resolve, 50));
|
|
1319
1525
|
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1526
|
+
// Move mouse to end position (drag)
|
|
1527
|
+
const steps = Math.max(10, Math.floor(duration / 20)); // Smooth movement
|
|
1528
|
+
await page.mouse.move(endX, endY, { steps });
|
|
1323
1529
|
|
|
1324
|
-
|
|
1325
|
-
|
|
1530
|
+
// Wait for duration
|
|
1531
|
+
await new Promise(resolve => setTimeout(resolve, Math.max(0, duration - steps * 20)));
|
|
1326
1532
|
|
|
1327
|
-
|
|
1328
|
-
|
|
1533
|
+
// Release mouse button (end drag)
|
|
1534
|
+
await page.mouse.up();
|
|
1329
1535
|
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1536
|
+
return {
|
|
1537
|
+
content: [{
|
|
1538
|
+
type: "text",
|
|
1539
|
+
text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px (native mode):\n` +
|
|
1540
|
+
` Start position: (${Math.round(startX)}, ${Math.round(startY)})\n` +
|
|
1541
|
+
` End position: (${Math.round(endX)}, ${Math.round(endY)})\n` +
|
|
1542
|
+
` Delta: (${deltaX}px, ${deltaY}px)\n` +
|
|
1543
|
+
` Duration: ${duration}ms`
|
|
1544
|
+
}],
|
|
1545
|
+
};
|
|
1546
|
+
}
|
|
1340
1547
|
}
|
|
1341
1548
|
|
|
1342
1549
|
if (name === "scrollHorizontal") {
|
|
@@ -1515,6 +1722,13 @@ async function executeToolInternal(name, args) {
|
|
|
1515
1722
|
|
|
1516
1723
|
let hintsText = '\n\n** AI HINTS **';
|
|
1517
1724
|
hintsText += `\nPage type: ${hints.pageType}`;
|
|
1725
|
+
if (hints.heading) {
|
|
1726
|
+
hintsText += `\nPage heading: "${hints.heading}"`;
|
|
1727
|
+
}
|
|
1728
|
+
if (hints.authRedirect) {
|
|
1729
|
+
hintsText += `\n⚠️ AUTH REDIRECT: Page redirected to login (intended: ${hints.authRedirect.returnUrl || 'unknown'})`;
|
|
1730
|
+
hintsText += `\n → Session/cookies not established. Login first, then retry navigation.`;
|
|
1731
|
+
}
|
|
1518
1732
|
if (hints.availableActions.length > 0) {
|
|
1519
1733
|
hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
|
|
1520
1734
|
}
|
|
@@ -2105,9 +2319,12 @@ Start coding now.`;
|
|
|
2105
2319
|
const maxResults = validatedArgs.maxResults || 5;
|
|
2106
2320
|
|
|
2107
2321
|
// Execute smart search in page context
|
|
2108
|
-
const results = await page.evaluate((description, maxResults, utilsCode) => {
|
|
2322
|
+
const results = await page.evaluate((description, maxResults, utilsCode, selectorResolverCode) => {
|
|
2109
2323
|
// Inject utilities into page context
|
|
2110
2324
|
eval(utilsCode);
|
|
2325
|
+
if (typeof registerElement === 'undefined') {
|
|
2326
|
+
eval(selectorResolverCode);
|
|
2327
|
+
}
|
|
2111
2328
|
|
|
2112
2329
|
// Determine element type from description
|
|
2113
2330
|
const elementType = determineElementType(description);
|
|
@@ -2163,18 +2380,29 @@ Start coding now.`;
|
|
|
2163
2380
|
});
|
|
2164
2381
|
|
|
2165
2382
|
// Filter and sort
|
|
2166
|
-
|
|
2383
|
+
const filtered = analyzed
|
|
2167
2384
|
.filter(r => r.score > 5) // Minimum threshold
|
|
2168
2385
|
.sort((a, b) => b.score - a.score)
|
|
2169
2386
|
.slice(0, maxResults);
|
|
2170
2387
|
|
|
2171
|
-
|
|
2388
|
+
// Register found elements in APOM registry and assign IDs
|
|
2389
|
+
filtered.forEach((result, idx) => {
|
|
2390
|
+
const apomId = `smart_${result.type}_${idx}`;
|
|
2391
|
+
result.id = apomId;
|
|
2392
|
+
if (typeof registerElement === 'function') {
|
|
2393
|
+
registerElement(apomId, result.selector, { source: 'smartFindElement' });
|
|
2394
|
+
}
|
|
2395
|
+
});
|
|
2396
|
+
|
|
2397
|
+
return filtered;
|
|
2398
|
+
|
|
2399
|
+
}, validatedArgs.description, maxResults, elementFinderUtils, selectorResolver);
|
|
2172
2400
|
|
|
2173
2401
|
const hints = {
|
|
2174
2402
|
totalCandidates: results.length,
|
|
2175
2403
|
bestMatch: results[0] || null,
|
|
2176
2404
|
suggestion: results.length > 0
|
|
2177
|
-
? `Use selector: ${results[0].selector}`
|
|
2405
|
+
? `Use id: "${results[0].id}" or selector: ${results[0].selector}`
|
|
2178
2406
|
: 'No good matches found. Try a different description.',
|
|
2179
2407
|
};
|
|
2180
2408
|
|
|
@@ -2536,13 +2764,18 @@ Start coding now.`;
|
|
|
2536
2764
|
const validatedArgs = schemas.FindElementsByTextSchema.parse(args);
|
|
2537
2765
|
const page = await getLastOpenPage();
|
|
2538
2766
|
|
|
2539
|
-
const elements = await page.evaluate((text, exact, caseSensitive, utilsCode) => {
|
|
2767
|
+
const elements = await page.evaluate((text, exact, caseSensitive, utilsCode, selectorResolverCode) => {
|
|
2540
2768
|
eval(utilsCode);
|
|
2769
|
+
if (typeof registerElement === 'undefined') {
|
|
2770
|
+
eval(selectorResolverCode);
|
|
2771
|
+
}
|
|
2541
2772
|
|
|
2542
2773
|
const results = [];
|
|
2774
|
+
const MAX_RESULTS = 40; // Collect up to 40 to allow visible/hidden sorting, cap expensive selector generation
|
|
2543
2775
|
const searchText = caseSensitive ? text : text.toLowerCase();
|
|
2544
2776
|
|
|
2545
2777
|
document.querySelectorAll('*').forEach(el => {
|
|
2778
|
+
if (results.length >= MAX_RESULTS) return; // Stop expensive selector generation after enough results
|
|
2546
2779
|
// Skip script, style, etc
|
|
2547
2780
|
if (['SCRIPT', 'STYLE', 'NOSCRIPT', 'BR', 'HR'].includes(el.tagName)) return;
|
|
2548
2781
|
|
|
@@ -2564,9 +2797,16 @@ Start coding now.`;
|
|
|
2564
2797
|
: compareText.includes(searchText);
|
|
2565
2798
|
|
|
2566
2799
|
if (matches) {
|
|
2800
|
+
const selector = getUniqueSelectorInPage(el);
|
|
2801
|
+
const type = el.tagName.toLowerCase();
|
|
2802
|
+
const apomId = `text_${type}_${results.length}`;
|
|
2803
|
+
if (typeof registerElement === 'function') {
|
|
2804
|
+
registerElement(apomId, selector, { source: 'findElementsByText' });
|
|
2805
|
+
}
|
|
2567
2806
|
results.push({
|
|
2568
|
-
|
|
2569
|
-
|
|
2807
|
+
id: apomId,
|
|
2808
|
+
selector,
|
|
2809
|
+
type,
|
|
2570
2810
|
text: elementText.substring(0, 100), // Only first 100 chars for preview
|
|
2571
2811
|
visible: el.offsetParent !== null, // Add visibility check
|
|
2572
2812
|
});
|
|
@@ -2574,7 +2814,7 @@ Start coding now.`;
|
|
|
2574
2814
|
});
|
|
2575
2815
|
|
|
2576
2816
|
return results;
|
|
2577
|
-
}, validatedArgs.text, validatedArgs.exact || false, validatedArgs.caseSensitive || false, elementFinderUtils);
|
|
2817
|
+
}, validatedArgs.text, validatedArgs.exact || false, validatedArgs.caseSensitive || false, elementFinderUtils, selectorResolver);
|
|
2578
2818
|
|
|
2579
2819
|
// Prioritize visible elements and limit results to prevent token overflow
|
|
2580
2820
|
const visibleElements = elements.filter(el => el.visible);
|
|
@@ -3089,13 +3329,102 @@ Start coding now.`;
|
|
|
3089
3329
|
};
|
|
3090
3330
|
}
|
|
3091
3331
|
|
|
3332
|
+
// Resolve pageObjectMode (backward compat: generatePageObject: true -> 'generate')
|
|
3333
|
+
const pageObjectMode = args.pageObjectMode || (args.generatePageObject ? 'generate' : 'none');
|
|
3334
|
+
|
|
3092
3335
|
// Select generator based on language
|
|
3093
|
-
let generator;
|
|
3094
3336
|
const options = {
|
|
3095
3337
|
cleanSelectors: args.cleanSelectors !== false, // default true
|
|
3096
3338
|
includeComments: args.includeComments !== false, // default true
|
|
3097
3339
|
};
|
|
3098
3340
|
|
|
3341
|
+
// Resolve POM elements for integrated modes
|
|
3342
|
+
let pomElements = null;
|
|
3343
|
+
let pomClassName = null;
|
|
3344
|
+
let pomImportPath = null;
|
|
3345
|
+
let pageObjectData = null;
|
|
3346
|
+
|
|
3347
|
+
if (pageObjectMode === 'generate-integrated' || pageObjectMode === 'generate') {
|
|
3348
|
+
try {
|
|
3349
|
+
const entryUrl = scenario.metadata?.entryUrl;
|
|
3350
|
+
if (entryUrl) {
|
|
3351
|
+
let page;
|
|
3352
|
+
try {
|
|
3353
|
+
page = await getLastOpenPage();
|
|
3354
|
+
const currentUrl = page.url();
|
|
3355
|
+
if (currentUrl !== entryUrl) {
|
|
3356
|
+
await page.goto(entryUrl, { waitUntil: 'networkidle2' });
|
|
3357
|
+
}
|
|
3358
|
+
} catch (error) {
|
|
3359
|
+
page = await getOrCreatePage(entryUrl);
|
|
3360
|
+
}
|
|
3361
|
+
|
|
3362
|
+
const pageObjectOptions = {
|
|
3363
|
+
className: args.pageObjectClassName || null,
|
|
3364
|
+
framework: args.language,
|
|
3365
|
+
includeComments: args.includeComments !== false,
|
|
3366
|
+
groupElements: true
|
|
3367
|
+
};
|
|
3368
|
+
|
|
3369
|
+
const pageObjectResult = await generatePageObject(page, pageObjectOptions);
|
|
3370
|
+
if (pageObjectResult.success) {
|
|
3371
|
+
const extension = args.language.includes('typescript') ? '.ts' :
|
|
3372
|
+
args.language.includes('java') ? '.java' : '.py';
|
|
3373
|
+
pageObjectData = {
|
|
3374
|
+
code: pageObjectResult.code,
|
|
3375
|
+
className: pageObjectResult.className,
|
|
3376
|
+
suggestedFileName: `${pageObjectResult.className}${extension}`,
|
|
3377
|
+
elementCount: pageObjectResult.elementCount
|
|
3378
|
+
};
|
|
3379
|
+
|
|
3380
|
+
if (pageObjectMode === 'generate-integrated') {
|
|
3381
|
+
pomElements = pageObjectResult.elements;
|
|
3382
|
+
pomClassName = pageObjectResult.className;
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
} catch (error) {
|
|
3387
|
+
// Page Object generation failed, continue without it
|
|
3388
|
+
}
|
|
3389
|
+
} else if (pageObjectMode === 'use-existing') {
|
|
3390
|
+
if (!args.pageObjectFile) {
|
|
3391
|
+
return {
|
|
3392
|
+
content: [{
|
|
3393
|
+
type: 'text',
|
|
3394
|
+
text: JSON.stringify({
|
|
3395
|
+
error: "pageObjectFile is required for 'use-existing' mode"
|
|
3396
|
+
}, null, 2)
|
|
3397
|
+
}],
|
|
3398
|
+
isError: true
|
|
3399
|
+
};
|
|
3400
|
+
}
|
|
3401
|
+
|
|
3402
|
+
try {
|
|
3403
|
+
const pomContent = FileAppender.readFile(args.pageObjectFile);
|
|
3404
|
+
const parsed = parsePomFile(pomContent, args.language);
|
|
3405
|
+
pomElements = parsed.elements;
|
|
3406
|
+
pomClassName = parsed.className;
|
|
3407
|
+
} catch (error) {
|
|
3408
|
+
return {
|
|
3409
|
+
content: [{
|
|
3410
|
+
type: 'text',
|
|
3411
|
+
text: JSON.stringify({
|
|
3412
|
+
error: `Failed to parse POM file: ${error.message}`
|
|
3413
|
+
}, null, 2)
|
|
3414
|
+
}],
|
|
3415
|
+
isError: true
|
|
3416
|
+
};
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
3419
|
+
|
|
3420
|
+
// Add POM options to generator
|
|
3421
|
+
if (pomElements && pomClassName) {
|
|
3422
|
+
options.pomElements = pomElements;
|
|
3423
|
+
options.pomClassName = pomClassName;
|
|
3424
|
+
options.pomImportPath = pomImportPath;
|
|
3425
|
+
}
|
|
3426
|
+
|
|
3427
|
+
let generator;
|
|
3099
3428
|
switch (args.language) {
|
|
3100
3429
|
case 'playwright-typescript':
|
|
3101
3430
|
generator = new PlaywrightTypeScriptGenerator(options);
|
|
@@ -3134,59 +3463,21 @@ Start coding now.`;
|
|
|
3134
3463
|
referenceTestName: args.referenceTestName
|
|
3135
3464
|
};
|
|
3136
3465
|
|
|
3137
|
-
// Generate Page Object if requested
|
|
3138
|
-
let pageObjectData = null;
|
|
3139
|
-
if (args.generatePageObject) {
|
|
3140
|
-
try {
|
|
3141
|
-
const entryUrl = scenario.metadata?.entryUrl;
|
|
3142
|
-
if (entryUrl) {
|
|
3143
|
-
let page;
|
|
3144
|
-
try {
|
|
3145
|
-
page = await getLastOpenPage();
|
|
3146
|
-
const currentUrl = page.url();
|
|
3147
|
-
if (currentUrl !== entryUrl) {
|
|
3148
|
-
await page.goto(entryUrl, { waitUntil: 'networkidle2' });
|
|
3149
|
-
}
|
|
3150
|
-
} catch (error) {
|
|
3151
|
-
page = await getOrCreatePage(entryUrl);
|
|
3152
|
-
}
|
|
3153
|
-
|
|
3154
|
-
const pageObjectOptions = {
|
|
3155
|
-
className: args.pageObjectClassName || null,
|
|
3156
|
-
framework: args.language,
|
|
3157
|
-
includeComments: args.includeComments !== false,
|
|
3158
|
-
groupElements: true
|
|
3159
|
-
};
|
|
3160
|
-
|
|
3161
|
-
const pageObjectResult = await generatePageObject(page, pageObjectOptions);
|
|
3162
|
-
if (pageObjectResult.success) {
|
|
3163
|
-
// Suggest filename based on className
|
|
3164
|
-
const extension = args.language.includes('typescript') ? '.ts' :
|
|
3165
|
-
args.language.includes('java') ? '.java' : '.py';
|
|
3166
|
-
pageObjectData = {
|
|
3167
|
-
code: pageObjectResult.code,
|
|
3168
|
-
className: pageObjectResult.className,
|
|
3169
|
-
suggestedFileName: `${pageObjectResult.className}${extension}`,
|
|
3170
|
-
elementCount: pageObjectResult.elementCount
|
|
3171
|
-
};
|
|
3172
|
-
}
|
|
3173
|
-
}
|
|
3174
|
-
} catch (error) {
|
|
3175
|
-
// Page Object generation failed, continue without it
|
|
3176
|
-
}
|
|
3177
|
-
}
|
|
3178
|
-
|
|
3179
3466
|
// Return JSON with instructions for Claude Code to append the test
|
|
3180
3467
|
const result = {
|
|
3181
3468
|
action: 'append_test',
|
|
3182
3469
|
targetFile: args.targetFile,
|
|
3183
|
-
testCode: testOnly,
|
|
3470
|
+
testCode: testOnly,
|
|
3184
3471
|
testName: args.testName || scenario.metadata?.name,
|
|
3185
3472
|
insertPosition: appendOptions.insertPosition,
|
|
3186
3473
|
referenceTestName: appendOptions.referenceTestName,
|
|
3187
3474
|
instruction: `Read file '${args.targetFile}', append the testCode at position '${appendOptions.insertPosition}', then write the file back.`
|
|
3188
3475
|
};
|
|
3189
3476
|
|
|
3477
|
+
if (pomClassName) {
|
|
3478
|
+
result.pomIntegration = { className: pomClassName, mode: pageObjectMode };
|
|
3479
|
+
}
|
|
3480
|
+
|
|
3190
3481
|
if (pageObjectData) {
|
|
3191
3482
|
result.pageObject = pageObjectData;
|
|
3192
3483
|
result.instruction += ` Also create a Page Object file '${pageObjectData.suggestedFileName}' with the provided pageObject.code.`;
|
|
@@ -3228,86 +3519,48 @@ Start coding now.`;
|
|
|
3228
3519
|
};
|
|
3229
3520
|
}
|
|
3230
3521
|
|
|
3231
|
-
//
|
|
3232
|
-
|
|
3522
|
+
// Resolve pageObjectMode (backward compat: generatePageObject: true -> 'generate')
|
|
3523
|
+
const pageObjectMode = args.pageObjectMode || (args.generatePageObject ? 'generate' : 'none');
|
|
3524
|
+
|
|
3233
3525
|
const options = {
|
|
3234
3526
|
cleanSelectors: args.cleanSelectors !== false, // default true
|
|
3235
3527
|
includeComments: args.includeComments !== false, // default true
|
|
3236
3528
|
};
|
|
3237
3529
|
|
|
3238
|
-
|
|
3239
|
-
|
|
3240
|
-
|
|
3241
|
-
|
|
3242
|
-
|
|
3243
|
-
|
|
3244
|
-
|
|
3245
|
-
|
|
3246
|
-
generator = new SeleniumPythonGenerator(options);
|
|
3247
|
-
break;
|
|
3248
|
-
case 'selenium-java':
|
|
3249
|
-
generator = new SeleniumJavaGenerator(options);
|
|
3250
|
-
break;
|
|
3251
|
-
default:
|
|
3530
|
+
// Resolve POM elements for integrated modes
|
|
3531
|
+
let pomElements = null;
|
|
3532
|
+
let pomClassName = null;
|
|
3533
|
+
let pageObjectData = null;
|
|
3534
|
+
const entryUrl = scenario.metadata?.entryUrl;
|
|
3535
|
+
|
|
3536
|
+
if (pageObjectMode === 'generate-integrated' || pageObjectMode === 'generate') {
|
|
3537
|
+
if (!entryUrl) {
|
|
3252
3538
|
return {
|
|
3253
3539
|
content: [{
|
|
3254
3540
|
type: 'text',
|
|
3255
3541
|
text: JSON.stringify({
|
|
3256
|
-
error:
|
|
3542
|
+
error: 'Cannot generate Page Object: scenario has no entryUrl in metadata'
|
|
3257
3543
|
}, null, 2)
|
|
3258
3544
|
}],
|
|
3259
3545
|
isError: true
|
|
3260
3546
|
};
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
// Generate test code with full imports
|
|
3264
|
-
const testCode = generator.generate(scenario, options);
|
|
3265
|
-
|
|
3266
|
-
// Generate suggested filename
|
|
3267
|
-
const testName = scenario.metadata?.name || 'test';
|
|
3268
|
-
const extension = args.language.includes('typescript') ? '.spec.ts' :
|
|
3269
|
-
args.language.includes('java') ? 'Test.java' :
|
|
3270
|
-
args.language.includes('python') ? '_test.py' : '.test.js';
|
|
3271
|
-
const suggestedFileName = args.language.includes('java')
|
|
3272
|
-
? testName.charAt(0).toUpperCase() + testName.slice(1) + 'Test.java'
|
|
3273
|
-
: testName.replace(/\s+/g, '_').toLowerCase() + extension;
|
|
3547
|
+
}
|
|
3274
3548
|
|
|
3275
|
-
// If generatePageObject is requested, also generate Page Object class
|
|
3276
|
-
if (args.generatePageObject) {
|
|
3277
3549
|
try {
|
|
3278
|
-
// Get page - need to open at scenario's entry URL
|
|
3279
3550
|
let page;
|
|
3280
|
-
const entryUrl = scenario.metadata?.entryUrl;
|
|
3281
|
-
|
|
3282
|
-
if (!entryUrl) {
|
|
3283
|
-
return {
|
|
3284
|
-
content: [{
|
|
3285
|
-
type: 'text',
|
|
3286
|
-
text: JSON.stringify({
|
|
3287
|
-
error: 'Cannot generate Page Object: scenario has no entryUrl in metadata'
|
|
3288
|
-
}, null, 2)
|
|
3289
|
-
}],
|
|
3290
|
-
isError: true
|
|
3291
|
-
};
|
|
3292
|
-
}
|
|
3293
|
-
|
|
3294
|
-
// Try to get existing page or open new one
|
|
3295
3551
|
try {
|
|
3296
3552
|
page = await getLastOpenPage();
|
|
3297
|
-
// Navigate to entry URL if current page is different
|
|
3298
3553
|
const currentUrl = page.url();
|
|
3299
3554
|
if (currentUrl !== entryUrl) {
|
|
3300
3555
|
await page.goto(entryUrl, { waitUntil: 'networkidle2' });
|
|
3301
3556
|
}
|
|
3302
3557
|
} catch (error) {
|
|
3303
|
-
// No page open, create new one
|
|
3304
3558
|
page = await getOrCreatePage(entryUrl);
|
|
3305
3559
|
}
|
|
3306
3560
|
|
|
3307
|
-
// Generate Page Object
|
|
3308
3561
|
const pageObjectOptions = {
|
|
3309
3562
|
className: args.pageObjectClassName || null,
|
|
3310
|
-
framework: args.language,
|
|
3563
|
+
framework: args.language,
|
|
3311
3564
|
includeComments: args.includeComments !== false,
|
|
3312
3565
|
groupElements: true
|
|
3313
3566
|
};
|
|
@@ -3315,71 +3568,119 @@ Start coding now.`;
|
|
|
3315
3568
|
const pageObjectResult = await generatePageObject(page, pageObjectOptions);
|
|
3316
3569
|
|
|
3317
3570
|
if (pageObjectResult.success) {
|
|
3318
|
-
// Suggest Page Object filename
|
|
3319
3571
|
const poExtension = args.language.includes('typescript') ? '.ts' :
|
|
3320
3572
|
args.language.includes('java') ? '.java' : '.py';
|
|
3321
|
-
|
|
3322
|
-
|
|
3323
|
-
|
|
3324
|
-
|
|
3325
|
-
|
|
3326
|
-
type: 'text',
|
|
3327
|
-
text: JSON.stringify({
|
|
3328
|
-
action: 'create_new_file',
|
|
3329
|
-
suggestedFileName: suggestedFileName,
|
|
3330
|
-
testCode: testCode,
|
|
3331
|
-
pageObject: {
|
|
3332
|
-
code: pageObjectResult.code,
|
|
3333
|
-
className: pageObjectResult.className,
|
|
3334
|
-
suggestedFileName: pageObjectFileName,
|
|
3335
|
-
elementCount: pageObjectResult.elementCount
|
|
3336
|
-
},
|
|
3337
|
-
instruction: `Create a new test file '${suggestedFileName}' with the testCode. Also create a Page Object file '${pageObjectFileName}' with the pageObject.code.`
|
|
3338
|
-
}, null, 2)
|
|
3339
|
-
}]
|
|
3340
|
-
};
|
|
3341
|
-
} else {
|
|
3342
|
-
// Page Object generation failed, return test code only with warning
|
|
3343
|
-
return {
|
|
3344
|
-
content: [{
|
|
3345
|
-
type: 'text',
|
|
3346
|
-
text: JSON.stringify({
|
|
3347
|
-
action: 'create_new_file',
|
|
3348
|
-
suggestedFileName: suggestedFileName,
|
|
3349
|
-
testCode: testCode,
|
|
3350
|
-
warning: 'Page Object generation failed: ' + (pageObjectResult.error || 'Unknown error'),
|
|
3351
|
-
instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
|
|
3352
|
-
}, null, 2)
|
|
3353
|
-
}]
|
|
3573
|
+
pageObjectData = {
|
|
3574
|
+
code: pageObjectResult.code,
|
|
3575
|
+
className: pageObjectResult.className,
|
|
3576
|
+
suggestedFileName: `${pageObjectResult.className}${poExtension}`,
|
|
3577
|
+
elementCount: pageObjectResult.elementCount
|
|
3354
3578
|
};
|
|
3579
|
+
|
|
3580
|
+
if (pageObjectMode === 'generate-integrated') {
|
|
3581
|
+
pomElements = pageObjectResult.elements;
|
|
3582
|
+
pomClassName = pageObjectResult.className;
|
|
3583
|
+
}
|
|
3355
3584
|
}
|
|
3356
3585
|
} catch (error) {
|
|
3357
|
-
// Page Object generation failed,
|
|
3586
|
+
// Page Object generation failed, continue without it
|
|
3587
|
+
}
|
|
3588
|
+
} else if (pageObjectMode === 'use-existing') {
|
|
3589
|
+
if (!args.pageObjectFile) {
|
|
3358
3590
|
return {
|
|
3359
3591
|
content: [{
|
|
3360
3592
|
type: 'text',
|
|
3361
3593
|
text: JSON.stringify({
|
|
3362
|
-
|
|
3363
|
-
suggestedFileName: suggestedFileName,
|
|
3364
|
-
testCode: testCode,
|
|
3365
|
-
warning: 'Page Object generation error: ' + error.message,
|
|
3366
|
-
instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
|
|
3594
|
+
error: "pageObjectFile is required for 'use-existing' mode"
|
|
3367
3595
|
}, null, 2)
|
|
3368
|
-
}]
|
|
3596
|
+
}],
|
|
3597
|
+
isError: true
|
|
3598
|
+
};
|
|
3599
|
+
}
|
|
3600
|
+
|
|
3601
|
+
try {
|
|
3602
|
+
const pomContent = FileAppender.readFile(args.pageObjectFile);
|
|
3603
|
+
const parsed = parsePomFile(pomContent, args.language);
|
|
3604
|
+
pomElements = parsed.elements;
|
|
3605
|
+
pomClassName = parsed.className;
|
|
3606
|
+
} catch (error) {
|
|
3607
|
+
return {
|
|
3608
|
+
content: [{
|
|
3609
|
+
type: 'text',
|
|
3610
|
+
text: JSON.stringify({
|
|
3611
|
+
error: `Failed to parse POM file: ${error.message}`
|
|
3612
|
+
}, null, 2)
|
|
3613
|
+
}],
|
|
3614
|
+
isError: true
|
|
3369
3615
|
};
|
|
3370
3616
|
}
|
|
3371
3617
|
}
|
|
3372
3618
|
|
|
3373
|
-
//
|
|
3619
|
+
// Add POM options to generator
|
|
3620
|
+
if (pomElements && pomClassName) {
|
|
3621
|
+
options.pomElements = pomElements;
|
|
3622
|
+
options.pomClassName = pomClassName;
|
|
3623
|
+
}
|
|
3624
|
+
|
|
3625
|
+
let generator;
|
|
3626
|
+
switch (args.language) {
|
|
3627
|
+
case 'playwright-typescript':
|
|
3628
|
+
generator = new PlaywrightTypeScriptGenerator(options);
|
|
3629
|
+
break;
|
|
3630
|
+
case 'playwright-python':
|
|
3631
|
+
generator = new PlaywrightPythonGenerator(options);
|
|
3632
|
+
break;
|
|
3633
|
+
case 'selenium-python':
|
|
3634
|
+
generator = new SeleniumPythonGenerator(options);
|
|
3635
|
+
break;
|
|
3636
|
+
case 'selenium-java':
|
|
3637
|
+
generator = new SeleniumJavaGenerator(options);
|
|
3638
|
+
break;
|
|
3639
|
+
default:
|
|
3640
|
+
return {
|
|
3641
|
+
content: [{
|
|
3642
|
+
type: 'text',
|
|
3643
|
+
text: JSON.stringify({
|
|
3644
|
+
error: `Unknown language: ${args.language}. Supported: playwright-typescript, playwright-python, selenium-python, selenium-java`
|
|
3645
|
+
}, null, 2)
|
|
3646
|
+
}],
|
|
3647
|
+
isError: true
|
|
3648
|
+
};
|
|
3649
|
+
}
|
|
3650
|
+
|
|
3651
|
+
// Generate test code with full imports
|
|
3652
|
+
const testCode = generator.generate(scenario, options);
|
|
3653
|
+
|
|
3654
|
+
// Generate suggested filename
|
|
3655
|
+
const testName = scenario.metadata?.name || 'test';
|
|
3656
|
+
const extension = args.language.includes('typescript') ? '.spec.ts' :
|
|
3657
|
+
args.language.includes('java') ? 'Test.java' :
|
|
3658
|
+
args.language.includes('python') ? '_test.py' : '.test.js';
|
|
3659
|
+
const suggestedFileName = args.language.includes('java')
|
|
3660
|
+
? testName.charAt(0).toUpperCase() + testName.slice(1) + 'Test.java'
|
|
3661
|
+
: testName.replace(/\s+/g, '_').toLowerCase() + extension;
|
|
3662
|
+
|
|
3663
|
+
// Build result
|
|
3664
|
+
const result = {
|
|
3665
|
+
action: 'create_new_file',
|
|
3666
|
+
suggestedFileName: suggestedFileName,
|
|
3667
|
+
testCode: testCode,
|
|
3668
|
+
instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
|
|
3669
|
+
};
|
|
3670
|
+
|
|
3671
|
+
if (pomClassName) {
|
|
3672
|
+
result.pomIntegration = { className: pomClassName, mode: pageObjectMode };
|
|
3673
|
+
}
|
|
3674
|
+
|
|
3675
|
+
if (pageObjectData) {
|
|
3676
|
+
result.pageObject = pageObjectData;
|
|
3677
|
+
result.instruction = `Create a new test file '${suggestedFileName}' with the testCode. Also create a Page Object file '${pageObjectData.suggestedFileName}' with the pageObject.code.`;
|
|
3678
|
+
}
|
|
3679
|
+
|
|
3374
3680
|
return {
|
|
3375
3681
|
content: [{
|
|
3376
3682
|
type: 'text',
|
|
3377
|
-
text: JSON.stringify(
|
|
3378
|
-
action: 'create_new_file',
|
|
3379
|
-
suggestedFileName: suggestedFileName,
|
|
3380
|
-
testCode: testCode,
|
|
3381
|
-
instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
|
|
3382
|
-
}, null, 2)
|
|
3683
|
+
text: JSON.stringify(result, null, 2)
|
|
3383
3684
|
}]
|
|
3384
3685
|
};
|
|
3385
3686
|
}
|
|
@@ -3541,6 +3842,87 @@ Start coding now.`;
|
|
|
3541
3842
|
};
|
|
3542
3843
|
}
|
|
3543
3844
|
|
|
3845
|
+
// ========== API / Swagger Tools ==========
|
|
3846
|
+
|
|
3847
|
+
if (name === "loadSwagger") {
|
|
3848
|
+
const validatedArgs = schemas.LoadSwaggerSchema.parse(args);
|
|
3849
|
+
const parser = await OpenAPIParser.load(validatedArgs.source, validatedArgs.format || 'auto');
|
|
3850
|
+
const summary = parser.getSummary();
|
|
3851
|
+
|
|
3852
|
+
return {
|
|
3853
|
+
content: [{
|
|
3854
|
+
type: 'text',
|
|
3855
|
+
text: JSON.stringify({
|
|
3856
|
+
success: true,
|
|
3857
|
+
...summary,
|
|
3858
|
+
instruction: 'Use generateApiModels to generate typed models from these schemas.'
|
|
3859
|
+
}, null, 2)
|
|
3860
|
+
}]
|
|
3861
|
+
};
|
|
3862
|
+
}
|
|
3863
|
+
|
|
3864
|
+
if (name === "generateApiModels") {
|
|
3865
|
+
const validatedArgs = schemas.GenerateApiModelsSchema.parse(args);
|
|
3866
|
+
const parser = await OpenAPIParser.load(validatedArgs.source, validatedArgs.format || 'auto');
|
|
3867
|
+
let schemasObj = parser.getSchemas();
|
|
3868
|
+
|
|
3869
|
+
// Filter schemas if specified
|
|
3870
|
+
if (validatedArgs.schemas && validatedArgs.schemas.length > 0) {
|
|
3871
|
+
const filtered = {};
|
|
3872
|
+
for (const schemaName of validatedArgs.schemas) {
|
|
3873
|
+
if (schemasObj[schemaName]) filtered[schemaName] = schemasObj[schemaName];
|
|
3874
|
+
}
|
|
3875
|
+
schemasObj = filtered;
|
|
3876
|
+
}
|
|
3877
|
+
|
|
3878
|
+
const metadata = {
|
|
3879
|
+
title: parser.spec.info?.title || '',
|
|
3880
|
+
source: validatedArgs.source,
|
|
3881
|
+
version: parser.version
|
|
3882
|
+
};
|
|
3883
|
+
|
|
3884
|
+
let code, suggestedFileName;
|
|
3885
|
+
|
|
3886
|
+
if (validatedArgs.language === 'typescript') {
|
|
3887
|
+
const generator = new ApiModelsTypeScriptGenerator(schemasObj, {
|
|
3888
|
+
style: validatedArgs.style || 'interface',
|
|
3889
|
+
includeEnums: validatedArgs.includeEnums !== false,
|
|
3890
|
+
includeValidation: validatedArgs.includeValidation || false,
|
|
3891
|
+
});
|
|
3892
|
+
code = generator.generate(metadata);
|
|
3893
|
+
const titleSlug = (metadata.title || 'api').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
|
|
3894
|
+
suggestedFileName = `${titleSlug}.models.ts`;
|
|
3895
|
+
} else {
|
|
3896
|
+
const generator = new ApiModelsPythonGenerator(schemasObj, {
|
|
3897
|
+
style: validatedArgs.pythonStyle || 'dataclass',
|
|
3898
|
+
includeEnums: validatedArgs.includeEnums !== false,
|
|
3899
|
+
includeValidation: validatedArgs.includeValidation || false,
|
|
3900
|
+
});
|
|
3901
|
+
code = generator.generate(metadata);
|
|
3902
|
+
const titleSlug = (metadata.title || 'api').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
|
|
3903
|
+
suggestedFileName = `${titleSlug}_models.py`;
|
|
3904
|
+
}
|
|
3905
|
+
|
|
3906
|
+
const schemaCount = Object.keys(schemasObj).length;
|
|
3907
|
+
const enumCount = Object.values(schemasObj).filter(s => s.enum && s.type === 'string').length;
|
|
3908
|
+
|
|
3909
|
+
return {
|
|
3910
|
+
content: [{
|
|
3911
|
+
type: 'text',
|
|
3912
|
+
text: JSON.stringify({
|
|
3913
|
+
action: 'create_new_file',
|
|
3914
|
+
suggestedFileName,
|
|
3915
|
+
code,
|
|
3916
|
+
schemaCount,
|
|
3917
|
+
enumCount,
|
|
3918
|
+
language: validatedArgs.language,
|
|
3919
|
+
source: validatedArgs.source,
|
|
3920
|
+
instruction: `Create file '${suggestedFileName}' with the generated code.`
|
|
3921
|
+
}, null, 2)
|
|
3922
|
+
}]
|
|
3923
|
+
};
|
|
3924
|
+
}
|
|
3925
|
+
|
|
3544
3926
|
return {
|
|
3545
3927
|
content: [
|
|
3546
3928
|
{
|