chrometools-mcp 3.3.8 → 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/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,9 @@ 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
+ }
346
355
  if (hints.availableActions.length > 0) {
347
356
  hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
348
357
  }
@@ -521,10 +530,32 @@ async function executeToolInternal(name, args) {
521
530
 
522
531
  // 3. Format output with hints and diagnostics
523
532
  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(', ')}`;
533
+
534
+ // Modal: show title, body text, and actions
535
+ if (hints.modalOpened && hints.newElements.some(e => e.type === 'modal')) {
536
+ const modal = hints.newElements.find(e => e.type === 'modal');
537
+ let modalText = 'Modal opened';
538
+ if (modal.title) modalText += `: "${modal.title}"`;
539
+ if (modal.text) modalText += `\n ${modal.text}`;
540
+ if (modal.actions?.length) modalText += `\n Actions: [${modal.actions.join('] [')}]`;
541
+ hintsText += '\n' + modalText;
542
+ } else if (hints.modalOpened) {
543
+ hintsText += '\nModal opened - interact with it or close';
544
+ }
545
+
546
+ // Overlay: show items
547
+ const overlay = hints.newElements.find(e => e.type === 'dropdown' || e.type === 'menu');
548
+ if (overlay?.items?.length) {
549
+ const label = overlay.type === 'menu' ? 'Menu' : 'Dropdown';
550
+ hintsText += `\n${label} with ${overlay.totalCount} options: ${overlay.items.join(', ')}`;
527
551
  }
552
+
553
+ // Other new elements (alerts, etc.)
554
+ const otherElements = hints.newElements.filter(e => e.type !== 'modal' && e.type !== 'dropdown' && e.type !== 'menu');
555
+ if (otherElements.length > 0) {
556
+ hintsText += `\nNew elements appeared: ${otherElements.map(e => e.text ? `${e.type}: ${e.text}` : e.type).join(', ')}`;
557
+ }
558
+
528
559
  if (hints.suggestedNext.length > 0) {
529
560
  hintsText += `\nSuggested next: ${hints.suggestedNext.join('; ')}`;
530
561
  }
@@ -1245,6 +1276,7 @@ async function executeToolInternal(name, args) {
1245
1276
 
1246
1277
  const distance = validatedArgs.distance || 100;
1247
1278
  const duration = validatedArgs.duration || 500;
1279
+ const mode = validatedArgs.mode || 'native';
1248
1280
 
1249
1281
  // Calculate drag deltas based on direction
1250
1282
  let deltaX = 0;
@@ -1281,62 +1313,166 @@ async function executeToolInternal(name, args) {
1281
1313
  break;
1282
1314
  }
1283
1315
 
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}` };
1316
+ if (mode === 'synthetic') {
1317
+ // Synthetic mode: dispatch DOM events for better JS library compatibility
1318
+ const result = await page.evaluate((selector, deltaX, deltaY, duration) => {
1319
+ const element = document.querySelector(selector);
1320
+ if (!element) {
1321
+ return { success: false, error: `Element not found: ${selector}` };
1322
+ }
1323
+
1324
+ const rect = element.getBoundingClientRect();
1325
+ const startX = rect.left + rect.width / 2;
1326
+ const startY = rect.top + rect.height / 2;
1327
+ const endX = startX + deltaX;
1328
+ const endY = startY + deltaY;
1329
+
1330
+ // Helper to create mouse/pointer event
1331
+ const createEvent = (type, clientX, clientY, buttons = 0) => {
1332
+ // Try PointerEvent first (modern browsers)
1333
+ if (typeof PointerEvent !== 'undefined') {
1334
+ return new PointerEvent(type, {
1335
+ bubbles: true,
1336
+ cancelable: true,
1337
+ view: window,
1338
+ clientX,
1339
+ clientY,
1340
+ screenX: clientX,
1341
+ screenY: clientY,
1342
+ buttons,
1343
+ button: 0,
1344
+ pointerId: 1,
1345
+ pointerType: 'mouse',
1346
+ isPrimary: true
1347
+ });
1348
+ }
1349
+ // Fallback to MouseEvent
1350
+ return new MouseEvent(type, {
1351
+ bubbles: true,
1352
+ cancelable: true,
1353
+ view: window,
1354
+ clientX,
1355
+ clientY,
1356
+ screenX: clientX,
1357
+ screenY: clientY,
1358
+ buttons,
1359
+ button: 0
1360
+ });
1361
+ };
1362
+
1363
+ // Dispatch mousedown/pointerdown
1364
+ element.dispatchEvent(createEvent('pointerdown', startX, startY, 1));
1365
+ element.dispatchEvent(createEvent('mousedown', startX, startY, 1));
1366
+
1367
+ // Dispatch intermediate mousemove/pointermove events
1368
+ const steps = Math.max(10, Math.floor(duration / 20));
1369
+ const stepDelay = duration / steps;
1370
+
1371
+ return new Promise((resolve) => {
1372
+ let currentStep = 0;
1373
+
1374
+ const moveInterval = setInterval(() => {
1375
+ currentStep++;
1376
+ const progress = currentStep / steps;
1377
+ const currentX = startX + (deltaX * progress);
1378
+ const currentY = startY + (deltaY * progress);
1379
+
1380
+ element.dispatchEvent(createEvent('pointermove', currentX, currentY, 1));
1381
+ element.dispatchEvent(createEvent('mousemove', currentX, currentY, 1));
1382
+
1383
+ if (currentStep >= steps) {
1384
+ clearInterval(moveInterval);
1385
+
1386
+ // Dispatch mouseup/pointerup
1387
+ element.dispatchEvent(createEvent('pointerup', endX, endY, 0));
1388
+ element.dispatchEvent(createEvent('mouseup', endX, endY, 0));
1389
+ element.dispatchEvent(createEvent('click', endX, endY, 0));
1390
+
1391
+ resolve({
1392
+ success: true,
1393
+ startX: Math.round(startX),
1394
+ startY: Math.round(startY),
1395
+ endX: Math.round(endX),
1396
+ endY: Math.round(endY),
1397
+ mode: 'synthetic'
1398
+ });
1399
+ }
1400
+ }, stepDelay);
1401
+ });
1402
+ }, validatedArgs.selector, deltaX, deltaY, duration);
1403
+
1404
+ if (!result.success) {
1405
+ throw new Error(result.error);
1289
1406
  }
1290
1407
 
1291
- const rect = element.getBoundingClientRect();
1292
1408
  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
1409
+ content: [{
1410
+ type: "text",
1411
+ text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px (${result.mode} mode):\n` +
1412
+ ` Start position: (${result.startX}, ${result.startY})\n` +
1413
+ ` End position: (${result.endX}, ${result.endY})\n` +
1414
+ ` Delta: (${deltaX}px, ${deltaY}px)\n` +
1415
+ ` Duration: ${duration}ms\n` +
1416
+ ` Events: pointerdown → ${Math.floor(duration / 20)} × pointermove → pointerup`
1417
+ }],
1298
1418
  };
1299
- }, validatedArgs.selector);
1419
+ } else {
1420
+ // Native mode: use Puppeteer mouse API (default, faster)
1421
+ const elementInfo = await page.evaluate((selector) => {
1422
+ const element = document.querySelector(selector);
1423
+ if (!element) {
1424
+ return { success: false, error: `Element not found: ${selector}` };
1425
+ }
1300
1426
 
1301
- if (!elementInfo.success) {
1302
- throw new Error(elementInfo.error);
1303
- }
1427
+ const rect = element.getBoundingClientRect();
1428
+ return {
1429
+ success: true,
1430
+ centerX: rect.left + rect.width / 2,
1431
+ centerY: rect.top + rect.height / 2,
1432
+ width: rect.width,
1433
+ height: rect.height
1434
+ };
1435
+ }, validatedArgs.selector);
1304
1436
 
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;
1437
+ if (!elementInfo.success) {
1438
+ throw new Error(elementInfo.error);
1439
+ }
1310
1440
 
1311
- // Move to start position
1312
- await page.mouse.move(startX, startY);
1441
+ const startX = elementInfo.centerX;
1442
+ const startY = elementInfo.centerY;
1443
+ const endX = startX + deltaX;
1444
+ const endY = startY + deltaY;
1313
1445
 
1314
- // Press mouse button (start drag)
1315
- await page.mouse.down();
1446
+ // Move to start position
1447
+ await page.mouse.move(startX, startY);
1316
1448
 
1317
- // Wait a bit to ensure drag is registered
1318
- await new Promise(resolve => setTimeout(resolve, 50));
1449
+ // Press mouse button (start drag)
1450
+ await page.mouse.down();
1319
1451
 
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 });
1452
+ // Wait a bit to ensure drag is registered
1453
+ await new Promise(resolve => setTimeout(resolve, 50));
1323
1454
 
1324
- // Wait for duration
1325
- await new Promise(resolve => setTimeout(resolve, Math.max(0, duration - steps * 20)));
1455
+ // Move mouse to end position (drag)
1456
+ const steps = Math.max(10, Math.floor(duration / 20)); // Smooth movement
1457
+ await page.mouse.move(endX, endY, { steps });
1326
1458
 
1327
- // Release mouse button (end drag)
1328
- await page.mouse.up();
1459
+ // Wait for duration
1460
+ await new Promise(resolve => setTimeout(resolve, Math.max(0, duration - steps * 20)));
1329
1461
 
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
- };
1462
+ // Release mouse button (end drag)
1463
+ await page.mouse.up();
1464
+
1465
+ return {
1466
+ content: [{
1467
+ type: "text",
1468
+ text: `Dragged ${validatedArgs.selector} ${validatedArgs.direction} by ${distance}px (native mode):\n` +
1469
+ ` Start position: (${Math.round(startX)}, ${Math.round(startY)})\n` +
1470
+ ` End position: (${Math.round(endX)}, ${Math.round(endY)})\n` +
1471
+ ` Delta: (${deltaX}px, ${deltaY}px)\n` +
1472
+ ` Duration: ${duration}ms`
1473
+ }],
1474
+ };
1475
+ }
1340
1476
  }
1341
1477
 
1342
1478
  if (name === "scrollHorizontal") {
@@ -1515,6 +1651,9 @@ async function executeToolInternal(name, args) {
1515
1651
 
1516
1652
  let hintsText = '\n\n** AI HINTS **';
1517
1653
  hintsText += `\nPage type: ${hints.pageType}`;
1654
+ if (hints.heading) {
1655
+ hintsText += `\nPage heading: "${hints.heading}"`;
1656
+ }
1518
1657
  if (hints.availableActions.length > 0) {
1519
1658
  hintsText += `\nAvailable actions: ${hints.availableActions.join(', ')}`;
1520
1659
  }
@@ -2105,9 +2244,12 @@ Start coding now.`;
2105
2244
  const maxResults = validatedArgs.maxResults || 5;
2106
2245
 
2107
2246
  // Execute smart search in page context
2108
- const results = await page.evaluate((description, maxResults, utilsCode) => {
2247
+ const results = await page.evaluate((description, maxResults, utilsCode, selectorResolverCode) => {
2109
2248
  // Inject utilities into page context
2110
2249
  eval(utilsCode);
2250
+ if (typeof registerElement === 'undefined') {
2251
+ eval(selectorResolverCode);
2252
+ }
2111
2253
 
2112
2254
  // Determine element type from description
2113
2255
  const elementType = determineElementType(description);
@@ -2163,18 +2305,29 @@ Start coding now.`;
2163
2305
  });
2164
2306
 
2165
2307
  // Filter and sort
2166
- return analyzed
2308
+ const filtered = analyzed
2167
2309
  .filter(r => r.score > 5) // Minimum threshold
2168
2310
  .sort((a, b) => b.score - a.score)
2169
2311
  .slice(0, maxResults);
2170
2312
 
2171
- }, validatedArgs.description, maxResults, elementFinderUtils);
2313
+ // Register found elements in APOM registry and assign IDs
2314
+ filtered.forEach((result, idx) => {
2315
+ const apomId = `smart_${result.type}_${idx}`;
2316
+ result.id = apomId;
2317
+ if (typeof registerElement === 'function') {
2318
+ registerElement(apomId, result.selector, { source: 'smartFindElement' });
2319
+ }
2320
+ });
2321
+
2322
+ return filtered;
2323
+
2324
+ }, validatedArgs.description, maxResults, elementFinderUtils, selectorResolver);
2172
2325
 
2173
2326
  const hints = {
2174
2327
  totalCandidates: results.length,
2175
2328
  bestMatch: results[0] || null,
2176
2329
  suggestion: results.length > 0
2177
- ? `Use selector: ${results[0].selector}`
2330
+ ? `Use id: "${results[0].id}" or selector: ${results[0].selector}`
2178
2331
  : 'No good matches found. Try a different description.',
2179
2332
  };
2180
2333
 
@@ -2536,8 +2689,11 @@ Start coding now.`;
2536
2689
  const validatedArgs = schemas.FindElementsByTextSchema.parse(args);
2537
2690
  const page = await getLastOpenPage();
2538
2691
 
2539
- const elements = await page.evaluate((text, exact, caseSensitive, utilsCode) => {
2692
+ const elements = await page.evaluate((text, exact, caseSensitive, utilsCode, selectorResolverCode) => {
2540
2693
  eval(utilsCode);
2694
+ if (typeof registerElement === 'undefined') {
2695
+ eval(selectorResolverCode);
2696
+ }
2541
2697
 
2542
2698
  const results = [];
2543
2699
  const searchText = caseSensitive ? text : text.toLowerCase();
@@ -2564,9 +2720,16 @@ Start coding now.`;
2564
2720
  : compareText.includes(searchText);
2565
2721
 
2566
2722
  if (matches) {
2723
+ const selector = getUniqueSelectorInPage(el);
2724
+ const type = el.tagName.toLowerCase();
2725
+ const apomId = `text_${type}_${results.length}`;
2726
+ if (typeof registerElement === 'function') {
2727
+ registerElement(apomId, selector, { source: 'findElementsByText' });
2728
+ }
2567
2729
  results.push({
2568
- selector: getUniqueSelectorInPage(el),
2569
- type: el.tagName.toLowerCase(),
2730
+ id: apomId,
2731
+ selector,
2732
+ type,
2570
2733
  text: elementText.substring(0, 100), // Only first 100 chars for preview
2571
2734
  visible: el.offsetParent !== null, // Add visibility check
2572
2735
  });
@@ -2574,7 +2737,7 @@ Start coding now.`;
2574
2737
  });
2575
2738
 
2576
2739
  return results;
2577
- }, validatedArgs.text, validatedArgs.exact || false, validatedArgs.caseSensitive || false, elementFinderUtils);
2740
+ }, validatedArgs.text, validatedArgs.exact || false, validatedArgs.caseSensitive || false, elementFinderUtils, selectorResolver);
2578
2741
 
2579
2742
  // Prioritize visible elements and limit results to prevent token overflow
2580
2743
  const visibleElements = elements.filter(el => el.visible);
@@ -3089,13 +3252,102 @@ Start coding now.`;
3089
3252
  };
3090
3253
  }
3091
3254
 
3255
+ // Resolve pageObjectMode (backward compat: generatePageObject: true -> 'generate')
3256
+ const pageObjectMode = args.pageObjectMode || (args.generatePageObject ? 'generate' : 'none');
3257
+
3092
3258
  // Select generator based on language
3093
- let generator;
3094
3259
  const options = {
3095
3260
  cleanSelectors: args.cleanSelectors !== false, // default true
3096
3261
  includeComments: args.includeComments !== false, // default true
3097
3262
  };
3098
3263
 
3264
+ // Resolve POM elements for integrated modes
3265
+ let pomElements = null;
3266
+ let pomClassName = null;
3267
+ let pomImportPath = null;
3268
+ let pageObjectData = null;
3269
+
3270
+ if (pageObjectMode === 'generate-integrated' || pageObjectMode === 'generate') {
3271
+ try {
3272
+ const entryUrl = scenario.metadata?.entryUrl;
3273
+ if (entryUrl) {
3274
+ let page;
3275
+ try {
3276
+ page = await getLastOpenPage();
3277
+ const currentUrl = page.url();
3278
+ if (currentUrl !== entryUrl) {
3279
+ await page.goto(entryUrl, { waitUntil: 'networkidle2' });
3280
+ }
3281
+ } catch (error) {
3282
+ page = await getOrCreatePage(entryUrl);
3283
+ }
3284
+
3285
+ const pageObjectOptions = {
3286
+ className: args.pageObjectClassName || null,
3287
+ framework: args.language,
3288
+ includeComments: args.includeComments !== false,
3289
+ groupElements: true
3290
+ };
3291
+
3292
+ const pageObjectResult = await generatePageObject(page, pageObjectOptions);
3293
+ if (pageObjectResult.success) {
3294
+ const extension = args.language.includes('typescript') ? '.ts' :
3295
+ args.language.includes('java') ? '.java' : '.py';
3296
+ pageObjectData = {
3297
+ code: pageObjectResult.code,
3298
+ className: pageObjectResult.className,
3299
+ suggestedFileName: `${pageObjectResult.className}${extension}`,
3300
+ elementCount: pageObjectResult.elementCount
3301
+ };
3302
+
3303
+ if (pageObjectMode === 'generate-integrated') {
3304
+ pomElements = pageObjectResult.elements;
3305
+ pomClassName = pageObjectResult.className;
3306
+ }
3307
+ }
3308
+ }
3309
+ } catch (error) {
3310
+ // Page Object generation failed, continue without it
3311
+ }
3312
+ } else if (pageObjectMode === 'use-existing') {
3313
+ if (!args.pageObjectFile) {
3314
+ return {
3315
+ content: [{
3316
+ type: 'text',
3317
+ text: JSON.stringify({
3318
+ error: "pageObjectFile is required for 'use-existing' mode"
3319
+ }, null, 2)
3320
+ }],
3321
+ isError: true
3322
+ };
3323
+ }
3324
+
3325
+ try {
3326
+ const pomContent = FileAppender.readFile(args.pageObjectFile);
3327
+ const parsed = parsePomFile(pomContent, args.language);
3328
+ pomElements = parsed.elements;
3329
+ pomClassName = parsed.className;
3330
+ } catch (error) {
3331
+ return {
3332
+ content: [{
3333
+ type: 'text',
3334
+ text: JSON.stringify({
3335
+ error: `Failed to parse POM file: ${error.message}`
3336
+ }, null, 2)
3337
+ }],
3338
+ isError: true
3339
+ };
3340
+ }
3341
+ }
3342
+
3343
+ // Add POM options to generator
3344
+ if (pomElements && pomClassName) {
3345
+ options.pomElements = pomElements;
3346
+ options.pomClassName = pomClassName;
3347
+ options.pomImportPath = pomImportPath;
3348
+ }
3349
+
3350
+ let generator;
3099
3351
  switch (args.language) {
3100
3352
  case 'playwright-typescript':
3101
3353
  generator = new PlaywrightTypeScriptGenerator(options);
@@ -3134,59 +3386,21 @@ Start coding now.`;
3134
3386
  referenceTestName: args.referenceTestName
3135
3387
  };
3136
3388
 
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
3389
  // Return JSON with instructions for Claude Code to append the test
3180
3390
  const result = {
3181
3391
  action: 'append_test',
3182
3392
  targetFile: args.targetFile,
3183
- testCode: testOnly, // Only test code, no imports
3393
+ testCode: testOnly,
3184
3394
  testName: args.testName || scenario.metadata?.name,
3185
3395
  insertPosition: appendOptions.insertPosition,
3186
3396
  referenceTestName: appendOptions.referenceTestName,
3187
3397
  instruction: `Read file '${args.targetFile}', append the testCode at position '${appendOptions.insertPosition}', then write the file back.`
3188
3398
  };
3189
3399
 
3400
+ if (pomClassName) {
3401
+ result.pomIntegration = { className: pomClassName, mode: pageObjectMode };
3402
+ }
3403
+
3190
3404
  if (pageObjectData) {
3191
3405
  result.pageObject = pageObjectData;
3192
3406
  result.instruction += ` Also create a Page Object file '${pageObjectData.suggestedFileName}' with the provided pageObject.code.`;
@@ -3228,86 +3442,48 @@ Start coding now.`;
3228
3442
  };
3229
3443
  }
3230
3444
 
3231
- // Select generator based on language
3232
- let generator;
3445
+ // Resolve pageObjectMode (backward compat: generatePageObject: true -> 'generate')
3446
+ const pageObjectMode = args.pageObjectMode || (args.generatePageObject ? 'generate' : 'none');
3447
+
3233
3448
  const options = {
3234
3449
  cleanSelectors: args.cleanSelectors !== false, // default true
3235
3450
  includeComments: args.includeComments !== false, // default true
3236
3451
  };
3237
3452
 
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:
3453
+ // Resolve POM elements for integrated modes
3454
+ let pomElements = null;
3455
+ let pomClassName = null;
3456
+ let pageObjectData = null;
3457
+ const entryUrl = scenario.metadata?.entryUrl;
3458
+
3459
+ if (pageObjectMode === 'generate-integrated' || pageObjectMode === 'generate') {
3460
+ if (!entryUrl) {
3252
3461
  return {
3253
3462
  content: [{
3254
3463
  type: 'text',
3255
3464
  text: JSON.stringify({
3256
- error: `Unknown language: ${args.language}. Supported: playwright-typescript, playwright-python, selenium-python, selenium-java`
3465
+ error: 'Cannot generate Page Object: scenario has no entryUrl in metadata'
3257
3466
  }, null, 2)
3258
3467
  }],
3259
3468
  isError: true
3260
3469
  };
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;
3470
+ }
3274
3471
 
3275
- // If generatePageObject is requested, also generate Page Object class
3276
- if (args.generatePageObject) {
3277
3472
  try {
3278
- // Get page - need to open at scenario's entry URL
3279
3473
  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
3474
  try {
3296
3475
  page = await getLastOpenPage();
3297
- // Navigate to entry URL if current page is different
3298
3476
  const currentUrl = page.url();
3299
3477
  if (currentUrl !== entryUrl) {
3300
3478
  await page.goto(entryUrl, { waitUntil: 'networkidle2' });
3301
3479
  }
3302
3480
  } catch (error) {
3303
- // No page open, create new one
3304
3481
  page = await getOrCreatePage(entryUrl);
3305
3482
  }
3306
3483
 
3307
- // Generate Page Object
3308
3484
  const pageObjectOptions = {
3309
3485
  className: args.pageObjectClassName || null,
3310
- framework: args.language, // Use same framework as test
3486
+ framework: args.language,
3311
3487
  includeComments: args.includeComments !== false,
3312
3488
  groupElements: true
3313
3489
  };
@@ -3315,71 +3491,119 @@ Start coding now.`;
3315
3491
  const pageObjectResult = await generatePageObject(page, pageObjectOptions);
3316
3492
 
3317
3493
  if (pageObjectResult.success) {
3318
- // Suggest Page Object filename
3319
3494
  const poExtension = args.language.includes('typescript') ? '.ts' :
3320
3495
  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
- }]
3496
+ pageObjectData = {
3497
+ code: pageObjectResult.code,
3498
+ className: pageObjectResult.className,
3499
+ suggestedFileName: `${pageObjectResult.className}${poExtension}`,
3500
+ elementCount: pageObjectResult.elementCount
3354
3501
  };
3502
+
3503
+ if (pageObjectMode === 'generate-integrated') {
3504
+ pomElements = pageObjectResult.elements;
3505
+ pomClassName = pageObjectResult.className;
3506
+ }
3355
3507
  }
3356
3508
  } catch (error) {
3357
- // Page Object generation failed, return test code only with error
3509
+ // Page Object generation failed, continue without it
3510
+ }
3511
+ } else if (pageObjectMode === 'use-existing') {
3512
+ if (!args.pageObjectFile) {
3358
3513
  return {
3359
3514
  content: [{
3360
3515
  type: 'text',
3361
3516
  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.`
3517
+ error: "pageObjectFile is required for 'use-existing' mode"
3367
3518
  }, null, 2)
3368
- }]
3519
+ }],
3520
+ isError: true
3521
+ };
3522
+ }
3523
+
3524
+ try {
3525
+ const pomContent = FileAppender.readFile(args.pageObjectFile);
3526
+ const parsed = parsePomFile(pomContent, args.language);
3527
+ pomElements = parsed.elements;
3528
+ pomClassName = parsed.className;
3529
+ } catch (error) {
3530
+ return {
3531
+ content: [{
3532
+ type: 'text',
3533
+ text: JSON.stringify({
3534
+ error: `Failed to parse POM file: ${error.message}`
3535
+ }, null, 2)
3536
+ }],
3537
+ isError: true
3369
3538
  };
3370
3539
  }
3371
3540
  }
3372
3541
 
3373
- // Default: return test code only
3542
+ // Add POM options to generator
3543
+ if (pomElements && pomClassName) {
3544
+ options.pomElements = pomElements;
3545
+ options.pomClassName = pomClassName;
3546
+ }
3547
+
3548
+ let generator;
3549
+ switch (args.language) {
3550
+ case 'playwright-typescript':
3551
+ generator = new PlaywrightTypeScriptGenerator(options);
3552
+ break;
3553
+ case 'playwright-python':
3554
+ generator = new PlaywrightPythonGenerator(options);
3555
+ break;
3556
+ case 'selenium-python':
3557
+ generator = new SeleniumPythonGenerator(options);
3558
+ break;
3559
+ case 'selenium-java':
3560
+ generator = new SeleniumJavaGenerator(options);
3561
+ break;
3562
+ default:
3563
+ return {
3564
+ content: [{
3565
+ type: 'text',
3566
+ text: JSON.stringify({
3567
+ error: `Unknown language: ${args.language}. Supported: playwright-typescript, playwright-python, selenium-python, selenium-java`
3568
+ }, null, 2)
3569
+ }],
3570
+ isError: true
3571
+ };
3572
+ }
3573
+
3574
+ // Generate test code with full imports
3575
+ const testCode = generator.generate(scenario, options);
3576
+
3577
+ // Generate suggested filename
3578
+ const testName = scenario.metadata?.name || 'test';
3579
+ const extension = args.language.includes('typescript') ? '.spec.ts' :
3580
+ args.language.includes('java') ? 'Test.java' :
3581
+ args.language.includes('python') ? '_test.py' : '.test.js';
3582
+ const suggestedFileName = args.language.includes('java')
3583
+ ? testName.charAt(0).toUpperCase() + testName.slice(1) + 'Test.java'
3584
+ : testName.replace(/\s+/g, '_').toLowerCase() + extension;
3585
+
3586
+ // Build result
3587
+ const result = {
3588
+ action: 'create_new_file',
3589
+ suggestedFileName: suggestedFileName,
3590
+ testCode: testCode,
3591
+ instruction: `Create a new test file '${suggestedFileName}' with the testCode.`
3592
+ };
3593
+
3594
+ if (pomClassName) {
3595
+ result.pomIntegration = { className: pomClassName, mode: pageObjectMode };
3596
+ }
3597
+
3598
+ if (pageObjectData) {
3599
+ result.pageObject = pageObjectData;
3600
+ result.instruction = `Create a new test file '${suggestedFileName}' with the testCode. Also create a Page Object file '${pageObjectData.suggestedFileName}' with the pageObject.code.`;
3601
+ }
3602
+
3374
3603
  return {
3375
3604
  content: [{
3376
3605
  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)
3606
+ text: JSON.stringify(result, null, 2)
3383
3607
  }]
3384
3608
  };
3385
3609
  }
@@ -3541,6 +3765,87 @@ Start coding now.`;
3541
3765
  };
3542
3766
  }
3543
3767
 
3768
+ // ========== API / Swagger Tools ==========
3769
+
3770
+ if (name === "loadSwagger") {
3771
+ const validatedArgs = schemas.LoadSwaggerSchema.parse(args);
3772
+ const parser = await OpenAPIParser.load(validatedArgs.source, validatedArgs.format || 'auto');
3773
+ const summary = parser.getSummary();
3774
+
3775
+ return {
3776
+ content: [{
3777
+ type: 'text',
3778
+ text: JSON.stringify({
3779
+ success: true,
3780
+ ...summary,
3781
+ instruction: 'Use generateApiModels to generate typed models from these schemas.'
3782
+ }, null, 2)
3783
+ }]
3784
+ };
3785
+ }
3786
+
3787
+ if (name === "generateApiModels") {
3788
+ const validatedArgs = schemas.GenerateApiModelsSchema.parse(args);
3789
+ const parser = await OpenAPIParser.load(validatedArgs.source, validatedArgs.format || 'auto');
3790
+ let schemasObj = parser.getSchemas();
3791
+
3792
+ // Filter schemas if specified
3793
+ if (validatedArgs.schemas && validatedArgs.schemas.length > 0) {
3794
+ const filtered = {};
3795
+ for (const schemaName of validatedArgs.schemas) {
3796
+ if (schemasObj[schemaName]) filtered[schemaName] = schemasObj[schemaName];
3797
+ }
3798
+ schemasObj = filtered;
3799
+ }
3800
+
3801
+ const metadata = {
3802
+ title: parser.spec.info?.title || '',
3803
+ source: validatedArgs.source,
3804
+ version: parser.version
3805
+ };
3806
+
3807
+ let code, suggestedFileName;
3808
+
3809
+ if (validatedArgs.language === 'typescript') {
3810
+ const generator = new ApiModelsTypeScriptGenerator(schemasObj, {
3811
+ style: validatedArgs.style || 'interface',
3812
+ includeEnums: validatedArgs.includeEnums !== false,
3813
+ includeValidation: validatedArgs.includeValidation || false,
3814
+ });
3815
+ code = generator.generate(metadata);
3816
+ const titleSlug = (metadata.title || 'api').toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
3817
+ suggestedFileName = `${titleSlug}.models.ts`;
3818
+ } else {
3819
+ const generator = new ApiModelsPythonGenerator(schemasObj, {
3820
+ style: validatedArgs.pythonStyle || 'dataclass',
3821
+ includeEnums: validatedArgs.includeEnums !== false,
3822
+ includeValidation: validatedArgs.includeValidation || false,
3823
+ });
3824
+ code = generator.generate(metadata);
3825
+ const titleSlug = (metadata.title || 'api').toLowerCase().replace(/[^a-z0-9]+/g, '_').replace(/^_|_$/g, '');
3826
+ suggestedFileName = `${titleSlug}_models.py`;
3827
+ }
3828
+
3829
+ const schemaCount = Object.keys(schemasObj).length;
3830
+ const enumCount = Object.values(schemasObj).filter(s => s.enum && s.type === 'string').length;
3831
+
3832
+ return {
3833
+ content: [{
3834
+ type: 'text',
3835
+ text: JSON.stringify({
3836
+ action: 'create_new_file',
3837
+ suggestedFileName,
3838
+ code,
3839
+ schemaCount,
3840
+ enumCount,
3841
+ language: validatedArgs.language,
3842
+ source: validatedArgs.source,
3843
+ instruction: `Create file '${suggestedFileName}' with the generated code.`
3844
+ }, null, 2)
3845
+ }]
3846
+ };
3847
+ }
3848
+
3544
3849
  return {
3545
3850
  content: [
3546
3851
  {