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/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 timeout to prevent hanging on navigation
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 clickPromise = element.click().catch(() => {
481
- // If Puppeteer click fails, fallback to JS click
482
- return element.evaluate(el => el.click());
483
- });
484
- const timeoutPromise = new Promise((_, reject) =>
485
- setTimeout(() => reject(new Error('click timeout')), timeoutMs)
486
- );
487
- return Promise.race([clickPromise, timeoutPromise]).catch(() => {
488
- // If click times out, try JS click as last resort
489
- return element.evaluate(el => el.click());
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
- if (hints.modalOpened) hintsText += '\nModal opened - interact with it or close';
525
- if (hints.newElements.length > 0) {
526
- hintsText += `\nNew elements appeared: ${hints.newElements.map(e => e.type).join(', ')}`;
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
- // Get element center position for drag start
1285
- const elementInfo = await page.evaluate((selector) => {
1286
- const element = document.querySelector(selector);
1287
- if (!element) {
1288
- return { success: false, error: `Element not found: ${selector}` };
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
- success: true,
1294
- centerX: rect.left + rect.width / 2,
1295
- centerY: rect.top + rect.height / 2,
1296
- width: rect.width,
1297
- height: rect.height
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
- }, validatedArgs.selector);
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
- if (!elementInfo.success) {
1302
- throw new Error(elementInfo.error);
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
- // Perform drag: mousedown → mousemove → mouseup
1306
- const startX = elementInfo.centerX;
1307
- const startY = elementInfo.centerY;
1308
- const endX = startX + deltaX;
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
- // Move to start position
1312
- await page.mouse.move(startX, startY);
1517
+ // Move to start position
1518
+ await page.mouse.move(startX, startY);
1313
1519
 
1314
- // Press mouse button (start drag)
1315
- await page.mouse.down();
1520
+ // Press mouse button (start drag)
1521
+ await page.mouse.down();
1316
1522
 
1317
- // Wait a bit to ensure drag is registered
1318
- await new Promise(resolve => setTimeout(resolve, 50));
1523
+ // Wait a bit to ensure drag is registered
1524
+ await new Promise(resolve => setTimeout(resolve, 50));
1319
1525
 
1320
- // Move mouse to end position (drag)
1321
- const steps = Math.max(10, Math.floor(duration / 20)); // Smooth movement
1322
- await page.mouse.move(endX, endY, { steps });
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
- // Wait for duration
1325
- await new Promise(resolve => setTimeout(resolve, Math.max(0, duration - steps * 20)));
1530
+ // Wait for duration
1531
+ await new Promise(resolve => setTimeout(resolve, Math.max(0, duration - steps * 20)));
1326
1532
 
1327
- // Release mouse button (end drag)
1328
- await page.mouse.up();
1533
+ // Release mouse button (end drag)
1534
+ await page.mouse.up();
1329
1535
 
1330
- return {
1331
- content: [{
1332
- type: "text",
1333
- text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px:\n` +
1334
- ` Start position: (${Math.round(startX)}, ${Math.round(startY)})\n` +
1335
- ` End position: (${Math.round(endX)}, ${Math.round(endY)})\n` +
1336
- ` Delta: (${deltaX}px, ${deltaY}px)\n` +
1337
- ` Duration: ${duration}ms`
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
- return analyzed
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
- }, validatedArgs.description, maxResults, elementFinderUtils);
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
- selector: getUniqueSelectorInPage(el),
2569
- type: el.tagName.toLowerCase(),
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, // Only test code, no imports
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
- // Select generator based on language
3232
- let generator;
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
- switch (args.language) {
3239
- case 'playwright-typescript':
3240
- generator = new PlaywrightTypeScriptGenerator(options);
3241
- break;
3242
- case 'playwright-python':
3243
- generator = new PlaywrightPythonGenerator(options);
3244
- break;
3245
- case 'selenium-python':
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: `Unknown language: ${args.language}. Supported: playwright-typescript, playwright-python, selenium-python, selenium-java`
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, // Use same framework as test
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
- const pageObjectFileName = `${pageObjectResult.className}${poExtension}`;
3322
-
3323
- // Return both test code and Page Object code
3324
- return {
3325
- content: [{
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, return test code only with error
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
- action: 'create_new_file',
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
- // Default: return test code only
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
  {