@nbakka/mcp-appium 3.0.26 → 4.0.2

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/lib/server.js CHANGED
@@ -16,7 +16,6 @@ const png_1 = require("./png");
16
16
  const image_utils_1 = require("./image-utils");
17
17
  const { google } = require('googleapis');
18
18
  const axios = require('axios');
19
- const OpenAI = require("openai");
20
19
  const express = require('express');
21
20
  // Fixed: support ESM default export of 'open'
22
21
  const openImport = require('open');
@@ -147,221 +146,6 @@ const createMcpServer = () => {
147
146
  const fs = require('fs').promises;
148
147
  const path = require('path');
149
148
 
150
- // tool(
151
- // "mobile_learn_current_app_context",
152
- // "Follow these instructions strictly to navigate through the app. This should be the first step after launching the app and before performing any tests. Google Sheet name to fetch locatorName and androidLocator from the specified sheet. NOTE: Only use the locator data from the Google Sheet at the end while generating test cases, don't use to navigate through screens",
153
- // {
154
- // sheetName: zod_1.z.string().describe("The name of the Google Sheet to fetch locator data from").default("PDP"),
155
- // },
156
- // async ({ sheetName }) => {
157
- // try {
158
- // // Read app context notes from file
159
- // const notesFilePath = path.join(__dirname, 'app_context.txt');
160
- // const fileContent = await fs.readFile(notesFilePath, 'utf-8');
161
- //
162
- // const notes = fileContent
163
- // .split('\n')
164
- // .map(line => line.trim())
165
- // .filter(line => line.length > 0);
166
- //
167
- // // Initialize response object
168
- // const context = { notes, locatorData: null };
169
- //
170
- // if (sheetName && sheetName.trim() !== '') {
171
- // // Load Google Sheets credentials with keyFile option (no manual parse)
172
- // const keyFile = path.join(os.homedir(), 'Desktop', 'secret.json');
173
- //
174
- //
175
- // const auth = new google.auth.GoogleAuth({
176
- // keyFile,
177
- // scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
178
- // });
179
- //
180
- // const authClient = await auth.getClient();
181
- // const sheets = google.sheets({ version: 'v4', auth: authClient });
182
- // const spreadsheetId = '1UapR81AxaztDUlPGDV-_EwHo2hWXkKCZXl8ALsvIyxA';
183
- //
184
- // const range = `${sheetName}!A1:Z1000`;
185
- // const res = await sheets.spreadsheets.values.get({ spreadsheetId, range });
186
- // const rows = res.data.values;
187
- //
188
- // if (!rows || rows.length === 0) {
189
- // return `Sheet "${sheetName}" is empty or does not exist. Notes loaded: ${JSON.stringify(notes)}`;
190
- // }
191
- //
192
- // // Map headers exactly (trimmed, case-sensitive)
193
- // const header = rows[0].map(h => h.toString().trim());
194
- //
195
- // const locatorNameIdx = header.indexOf('locatorName');
196
- // const androidLocatorIdx = header.indexOf('androidLocator');
197
- //
198
- // if (locatorNameIdx === -1 || androidLocatorIdx === -1) {
199
- // return `Required columns "locatorName" and/or "androidLocator" not found in sheet "${sheetName}". Notes loaded: ${JSON.stringify(notes)}`;
200
- // }
201
- //
202
- // const locatorData = rows.slice(1)
203
- // .filter(row => row[locatorNameIdx] && row[androidLocatorIdx])
204
- // .map(row => ({
205
- // locatorName: row[locatorNameIdx],
206
- // androidLocator: row[androidLocatorIdx],
207
- // }));
208
- //
209
- // context.locatorData = locatorData;
210
- // }
211
- // return `App context learned: ${JSON.stringify(context)}`;
212
- // } catch (error) {
213
- // return `Error reading app context notes or fetching locator data: ${error.message}`;
214
- // }
215
- // }
216
- // );
217
-
218
- // Tool 1: Fetch incomplete test case
219
- tool(
220
- "mobile_fetch_incomplete_testcase",
221
- "Fetches the first test case from a Google Sheet that has blank test steps but has a value in 'UT v/s Automation' column. Returns the test scenario and row number for later update.",
222
- {
223
- sheetName: zod_1.z.string().describe("The name of the Google Sheet tab to fetch test case data from").default("TestCases"),
224
- },
225
- async ({ sheetName }) => {
226
- try {
227
- // Load Google Sheets credentials
228
- const keyFile = path.join(os.homedir(), 'Desktop', 'secret.json');
229
-
230
- const auth = new google.auth.GoogleAuth({
231
- keyFile,
232
- scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly'],
233
- });
234
-
235
- const authClient = await auth.getClient();
236
- const sheets = google.sheets({ version: 'v4', auth: authClient });
237
- const spreadsheetId = '1jAilVUeQW99JUYj1KL4jovoxeWGUnsIcY_nMJR5H6dc';
238
-
239
- const range = `${sheetName}!A1:Z1000`;
240
- const res = await sheets.spreadsheets.values.get({ spreadsheetId, range });
241
- const rows = res.data.values;
242
-
243
- if (!rows || rows.length === 0) {
244
- return `Sheet "${sheetName}" is empty or does not exist.`;
245
- }
246
-
247
- // Map headers (trimmed, case-sensitive)
248
- const header = rows[0].map(h => h.toString().trim());
249
-
250
- // Find required column indices
251
- const testCasesIdx = header.indexOf('Testcases');
252
- const testStepsIdx = header.indexOf('Test Steps');
253
- const utVsAutomationIdx = header.indexOf('UT v/s Automation');
254
-
255
- if (testCasesIdx === -1 || testStepsIdx === -1 || utVsAutomationIdx === -1) {
256
- return `Required columns "Testcases", "Test Steps", and/or "UT v/s Automation" not found in sheet "${sheetName}". Available columns: ${header.join(', ')}`;
257
- }
258
-
259
- // Find first row with empty test steps and non-empty UT v/s Automation
260
- for (let i = 1; i < rows.length; i++) {
261
- const row = rows[i];
262
- const testSteps = row[testStepsIdx] ? row[testStepsIdx].toString().trim() : '';
263
- const utVsAutomation = row[utVsAutomationIdx] ? row[utVsAutomationIdx].toString().trim() : '';
264
- const testScenario = row[testCasesIdx] ? row[testCasesIdx].toString().trim() : '';
265
-
266
- // Check if test steps are empty, UT v/s Automation is not empty, and test scenario exists
267
- if (!testSteps && utVsAutomation && testScenario) {
268
- // Convert column index to letter (A, B, C, etc.)
269
- const columnLetter = String.fromCharCode(65 + testStepsIdx);
270
- const rowNumber = i + 1; // +1 because spreadsheet rows are 1-indexed
271
-
272
- return JSON.stringify({
273
- testScenario: testScenario,
274
- sheetName: sheetName,
275
- rowNumber: rowNumber,
276
- columnLetter: columnLetter,
277
- cellReference: `${columnLetter}${rowNumber}`
278
- });
279
- }
280
- }
281
-
282
- return `No test cases found with blank test steps and non-empty "UT v/s Automation" in sheet "${sheetName}".`;
283
-
284
- } catch (error) {
285
- return `Error fetching incomplete test case: ${error.message}`;
286
- }
287
- }
288
- );
289
-
290
- // Tool 2: Update Test Steps
291
- tool(
292
- "mobile_update_test_steps",
293
- "Updates the Test Steps cell in Google Sheet for a specific test case. Use the sheetName, rowNumber, and columnLetter obtained from mobile_fetch_incomplete_testcase tool.",
294
- {
295
- sheetName: zod_1.z.string().describe("The name of the Google Sheet tab"),
296
- rowNumber: zod_1.z.number().describe("The row number to update (from fetch tool)"),
297
- columnLetter: zod_1.z.string().describe("The column letter for Test Steps (from fetch tool)"),
298
- testSteps: zod_1.z.string().describe("The test steps content to write into the cell"),
299
- },
300
- async ({ sheetName, rowNumber, columnLetter, testSteps }) => {
301
- try {
302
- // Load Google Sheets credentials with write permissions
303
- const keyFile = path.join(os.homedir(), 'Desktop', 'secret.json');
304
-
305
- const auth = new google.auth.GoogleAuth({
306
- keyFile,
307
- scopes: ['https://www.googleapis.com/auth/spreadsheets'], // Full read/write access
308
- });
309
-
310
- const authClient = await auth.getClient();
311
- const sheets = google.sheets({ version: 'v4', auth: authClient });
312
- const spreadsheetId = '1jAilVUeQW99JUYj1KL4jovoxeWGUnsIcY_nMJR5H6dc';
313
-
314
- // Construct the cell reference (e.g., "TestCases!C5")
315
- const cellReference = `${sheetName}!${columnLetter}${rowNumber}`;
316
-
317
- // Update the cell
318
- const updateRes = await sheets.spreadsheets.values.update({
319
- spreadsheetId,
320
- range: cellReference,
321
- valueInputOption: 'RAW',
322
- resource: {
323
- values: [[testSteps]]
324
- }
325
- });
326
-
327
- if (updateRes.status === 200) {
328
- return `Successfully updated Test Steps at ${cellReference} with content: "${testSteps.substring(0, 100)}${testSteps.length > 100 ? '...' : ''}"`;
329
- } else {
330
- return `Failed to update Test Steps. Status: ${updateRes.status}`;
331
- }
332
-
333
- } catch (error) {
334
- return `Error updating test steps: ${error.message}`;
335
- }
336
- }
337
- );
338
-
339
- tool(
340
- "mobile_learn_teststeps_generation_guidelines",
341
- "Reads previously saved test steps generation guidelines and returns them so they can be used to validate or update test steps. This should be executed before generating manual test steps.",
342
- {},
343
- async () => {
344
- try {
345
- // Read test steps generation guidelines from Desktop
346
- const guidelinesFilePath = path.join(os.homedir(), 'Desktop', 'teststeps_generation_guidelines.txt');
347
- const fileContent = await fs.readFile(guidelinesFilePath, "utf-8");
348
-
349
- const guidelines = fileContent
350
- .split("\n")
351
- .map(line => line.trim())
352
- .filter(line => line.length > 0);
353
-
354
- if (guidelines.length === 0) {
355
- return "No test steps generation guidelines found.";
356
- }
357
-
358
- return `Test steps generation guidelines loaded: ${JSON.stringify({ guidelines })}`;
359
- } catch (error) {
360
- return `Error reading test steps generation guidelines: ${error.message}`;
361
- }
362
- }
363
- );
364
-
365
149
  tool(
366
150
  "mobile_learn_app_context_and_navigation_guidelines",
367
151
  "Reads previously saved app context and navigation guidelines and returns them so they can be used to navigate through the app before generating test steps.",
@@ -387,117 +171,10 @@ tool(
387
171
  }
388
172
  );
389
173
 
390
- // Tool 3: Update Test Scenario (Always appends with green color)
391
- tool(
392
- "mobile_update_test_scenario",
393
- "Updates the Test Scenario (Testcases column) in Google Sheet for outdated scenarios. Always keeps the existing scenario and adds the updated scenario in green color on a new line.",
394
- {
395
- sheetName: zod_1.z.string().describe("The name of the Google Sheet tab"),
396
- rowNumber: zod_1.z.number().describe("The row number to update"),
397
- columnLetter: zod_1.z.string().describe("The column letter for Testcases column"),
398
- updatedScenario: zod_1.z.string().describe("The updated test scenario content"),
399
- },
400
- async ({ sheetName, rowNumber, columnLetter, updatedScenario }) => {
401
- try {
402
- // Load Google Sheets credentials with write permissions
403
- const keyFile = path.join(os.homedir(), 'Desktop', 'secret.json');
404
-
405
- const auth = new google.auth.GoogleAuth({
406
- keyFile,
407
- scopes: ['https://www.googleapis.com/auth/spreadsheets'],
408
- });
409
-
410
- const authClient = await auth.getClient();
411
- const sheets = google.sheets({ version: 'v4', auth: authClient });
412
- const spreadsheetId = '1jAilVUeQW99JUYj1KL4jovoxeWGUnsIcY_nMJR5H6dc';
413
-
414
- const cellReference = `${sheetName}!${columnLetter}${rowNumber}`;
415
-
416
- // First, read the existing scenario
417
- const getRes = await sheets.spreadsheets.values.get({
418
- spreadsheetId,
419
- range: cellReference
420
- });
421
-
422
- const existingScenario = getRes.data.values && getRes.data.values[0] && getRes.data.values[0][0]
423
- ? getRes.data.values[0][0]
424
- : '';
425
-
426
- if (!existingScenario) {
427
- return `No existing scenario found at ${cellReference}. Cannot append to empty cell.`;
428
- }
429
-
430
- // Combine: existing scenario + newline + updated scenario
431
- const combinedContent = `${existingScenario}\n${updatedScenario}`;
432
-
433
- // Get sheet ID dynamically
434
- const sheetsMetadata = await sheets.spreadsheets.get({ spreadsheetId });
435
- const sheetId = sheetsMetadata.data.sheets.find(s => s.properties.title === sheetName)?.properties.sheetId || 0;
436
-
437
- // Calculate text positions
438
- const existingLength = existingScenario.length;
439
- const newTextStartIndex = existingLength + 1; // +1 for newline
440
-
441
- // Apply green color formatting to the new text
442
- const batchUpdateRes = await sheets.spreadsheets.batchUpdate({
443
- spreadsheetId,
444
- resource: {
445
- requests: [
446
- {
447
- repeatCell: {
448
- range: {
449
- sheetId: sheetId,
450
- startRowIndex: rowNumber - 1,
451
- endRowIndex: rowNumber,
452
- startColumnIndex: columnLetter.charCodeAt(0) - 65,
453
- endColumnIndex: columnLetter.charCodeAt(0) - 64
454
- },
455
- cell: {
456
- userEnteredValue: {
457
- stringValue: combinedContent
458
- },
459
- textFormatRuns: [
460
- // Keep existing text in default color
461
- {
462
- startIndex: 0,
463
- format: {}
464
- },
465
- // New text in green
466
- {
467
- startIndex: newTextStartIndex,
468
- format: {
469
- foregroundColor: {
470
- red: 0.0,
471
- green: 0.5,
472
- blue: 0.0
473
- }
474
- }
475
- }
476
- ]
477
- },
478
- fields: 'userEnteredValue,textFormatRuns'
479
- }
480
- }
481
- ]
482
- }
483
- });
484
-
485
- if (batchUpdateRes.status === 200) {
486
- return `Successfully appended updated scenario at ${cellReference}. New scenario added in green color: "${updatedScenario.substring(0, 100)}${updatedScenario.length > 100 ? '...' : ''}"`;
487
- } else {
488
- return `Failed to append scenario. Status: ${batchUpdateRes.status}`;
489
- }
490
-
491
- } catch (error) {
492
- return `Error updating test scenario: ${error.message}`;
493
- }
494
- }
495
- );
496
-
497
- tool("mobile_list_elements_on_screennn", "List elements on screen and their coordinates, with display text or accessibility label. Returns the complete XML structure to maintain hierarchy for XPath creation. Do not cache this result.", {}, async ({}) => {
174
+ tool("mobile_list_elements_on_screen", "List elements on screen in a simplified, flattened structure. Returns only useful attributes: text, class, id (resource-id), accessibilityId (content-desc), and bounds. Do not cache this result.", {}, async ({}) => {
498
175
  requireRobot();
499
- const xmlStructure = await robot.getXmlStructure();
500
- return `Complete XML structure of current screen: ${JSON.stringify(xmlStructure)}`;
176
+ const elements = await robot.getSimplifiedElements();
177
+ return `Screen elements (${elements.length} found): ${JSON.stringify(elements, null, 2)}`;
501
178
  });
502
179
 
503
180
  tool("mobile_press_button", "Press a button on device", {
@@ -517,12 +194,12 @@ tool(
517
194
  return `Opened URL: ${url}`;
518
195
  });
519
196
 
520
- tool("swipe_on_screen", "Swipe on the screen", {
521
- direction: zod_1.z.enum(["up", "down"]).describe("The direction to swipe, up direction means it will from bottom part of the screen to top part of screen, essentially moving the screen content up and opposite happens for down"),
197
+ tool("scroll_on_screen", "Scroll on the screen. IMPORTANT: Don't scroll continuously 2 times to avoid missing elements. After each scroll, call mobile_list_elements_on_screen tool to check what is currently visible on screen before scrolling again.", {
198
+ direction: zod_1.z.enum(["up", "down"]).describe("The scroll direction: 'up' scrolls up to reveal content above (swipe down gesture), 'down' scrolls down to reveal content below (swipe up gesture)"),
522
199
  }, async ({ direction }) => {
523
200
  requireRobot();
524
201
  await robot.swipe(direction);
525
- return `Swiped ${direction} on screen`;
202
+ return `Scrolled ${direction} on screen. Remember to call mobile_list_elements_on_screen to check visible elements and scroll again if needed.`;
526
203
  });
527
204
 
528
205
  tool("mobile_type_keys", "Type text into the focused element", {
@@ -553,1379 +230,1404 @@ tool(
553
230
 
554
231
  tool(
555
232
  "mobile_tap_by_text",
556
- "Tap an element on screen by its displayed text or accessibility label using ADB tap",
233
+ "Tap an element on screen by any attribute: text, accessibilityId (content-desc), or id (resource-id). Searches all attributes automatically.",
557
234
  {
558
- text: zod_1.z.string().describe("The exact text or label of the element to tap"),
235
+ value: zod_1.z.string().describe("The value to search for - can be text, accessibilityId, or id"),
559
236
  },
560
- async ({ text }) => {
561
- if (!text) throw new Error("Input text is required");
237
+ async ({ value }) => {
238
+ if (!value) throw new Error("Input value is required");
562
239
 
563
240
  requireRobot(); // ensure robot instance available
564
- const elements = await robot.getElementsOnScreen();
241
+ const elements = await robot.getSimplifiedElements();
565
242
 
566
- // Find element by exact match on text or label
243
+ // Find element by matching any attribute (text, accessibilityId, or id)
567
244
  const element = elements.find(
568
- el => el.text === text || el.label === text
245
+ el => el.text === value || el.accessibilityId === value || el.id === value
569
246
  );
570
247
 
571
- if (!element) throw new Error(`Element with text or label "${text}" not found`);
572
-
248
+ if (!element) throw new Error(`Element with value "${value}" not found in text, accessibilityId, or id`);
249
+
250
+ // Parse bounds to get coordinates
251
+ // Bounds format: "[x1,y1][x2,y2]"
252
+ if (!element.bounds) throw new Error("Element has no bounds information");
253
+
254
+ const boundsMatch = element.bounds.match(/\[(\d+),(\d+)\]\[(\d+),(\d+)\]/);
255
+ if (!boundsMatch) throw new Error("Invalid bounds format");
256
+
257
+ const x1 = parseInt(boundsMatch[1]);
258
+ const y1 = parseInt(boundsMatch[2]);
259
+ const x2 = parseInt(boundsMatch[3]);
260
+ const y2 = parseInt(boundsMatch[4]);
261
+
573
262
  // Calculate center coordinates
574
- const rect = element.rect;
575
- const x = Math.floor(rect.x + rect.width / 2);
576
- const y = Math.floor(rect.y + rect.height / 2);
263
+ const x = Math.floor((x1 + x2) / 2);
264
+ const y = Math.floor((y1 + y2) / 2);
265
+
266
+ // Execute tap using robot (works for both Android and iOS)
267
+ await robot.tap(x, y);
577
268
 
578
- // Execute adb tap
579
- const { execSync } = require("child_process");
580
- execSync(`adb shell input tap ${x} ${y}`);
269
+ // Determine which attribute matched
270
+ let matchedBy = '';
271
+ if (element.text === value) matchedBy = 'text';
272
+ else if (element.accessibilityId === value) matchedBy = 'accessibilityId';
273
+ else if (element.id === value) matchedBy = 'id';
581
274
 
582
- return `Tapped element with text/label "${text}" at (${x},${y})`;
275
+ return `Tapped element by ${matchedBy} "${value}" at (${x},${y})`;
583
276
  }
584
277
  );
585
278
 
586
- // tool(
587
- // "mobile_fetch_jira_ticket",
588
- // "Fetch JIRA ticket information including summary, description, and extract Figma links from description",
589
- // {
590
- // ticketId: zod_1.z.string().describe("The JIRA ticket ID (e.g., HDA-434)"),
591
- // },
592
- // async ({ ticketId }) => {
593
- // try {
594
- // // Read JIRA credentials from desktop/jira.json file
595
- // const jiraConfigPath = path.join(os.homedir(), 'Desktop', 'jira.json');
596
- //
597
- // let jiraConfig;
598
- // try {
599
- // const configContent = await fs.readFile(jiraConfigPath, 'utf-8');
600
- // jiraConfig = JSON.parse(configContent);
601
- // } catch (error) {
602
- // throw new Error(`Failed to read JIRA config from ${jiraConfigPath}: ${error.message}`);
603
- // }
604
- //
605
- // // Extract all required values from JSON file
606
- // const { api: jiraApiToken, baseUrl: jiraBaseUrl, email: jiraEmail } = jiraConfig;
607
- //
608
- // if (!jiraApiToken) {
609
- // throw new Error('JIRA API token not found in jira.json file. Please ensure the file contains "api" field.');
610
- // }
611
- //
612
- // if (!jiraBaseUrl) {
613
- // throw new Error('JIRA base URL not found in jira.json file. Please ensure the file contains "baseUrl" field.');
614
- // }
615
- //
616
- // if (!jiraEmail) {
617
- // throw new Error('JIRA email not found in jira.json file. Please ensure the file contains "email" field.');
618
- // }
619
- //
620
- // // Create Basic Auth token
621
- // const auth = Buffer.from(`${jiraEmail}:${jiraApiToken}`).toString('base64');
622
- //
623
- // // Fetch ticket from JIRA API
624
- // const response = await axios.get(
625
- // `${jiraBaseUrl}/rest/api/3/issue/${ticketId}`,
626
- // {
627
- // headers: {
628
- // 'Authorization': `Basic ${auth}`,
629
- // 'Accept': 'application/json',
630
- // 'Content-Type': 'application/json'
631
- // }
632
- // }
633
- // );
634
- //
635
- // const issue = response.data;
636
- //
637
- // // Extract summary and description
638
- // const summary = issue.fields.summary || 'No summary available';
639
- // const description = issue.fields.description ?
640
- // extractTextFromADF(issue.fields.description) :
641
- // 'No description available';
642
- //
643
- // // Extract Figma links directly from ADF structure
644
- // const figmaLinks = extractFigmaLinksFromADF(issue.fields.description);
645
- //
646
- // // Format response
647
- // const result = {
648
- // ticketId: ticketId,
649
- // summary: summary,
650
- // description: description,
651
- // figmaLinks: figmaLinks.length > 0 ? figmaLinks : ['No Figma links found']
652
- // };
653
- //
654
- // return `JIRA Ticket Information:
655
- // Ticket ID: ${result.ticketId}
656
- // Summary: ${result.summary}
657
- // Description: ${result.description}
658
- // Figma Links: ${result.figmaLinks.join(', ')}`;
659
- //
660
- // } catch (error) {
661
- // if (error.response && error.response.status === 404) {
662
- // return `Error: JIRA ticket ${ticketId} not found. Please check the ticket ID.`;
663
- // } else if (error.response && error.response.status === 401) {
664
- // return `Error: Authentication failed. Please check your JIRA credentials.`;
665
- // } else {
666
- // return `Error fetching JIRA ticket: ${error.message}`;
667
- // }
668
- // }
669
- // }
670
- // );
671
- //
672
- // // Helper function to extract text from Atlassian Document Format (ADF)
673
- // function extractTextFromADF(adfContent) {
674
- // if (!adfContent || typeof adfContent !== 'object') {
675
- // return String(adfContent || '');
676
- // }
677
- //
678
- // let text = '';
679
- //
680
- // function traverse(node) {
681
- // if (node.type === 'text') {
682
- // text += node.text || '';
683
- // } else if (node.content && Array.isArray(node.content)) {
684
- // node.content.forEach(traverse);
685
- // }
686
- //
687
- // // Add line breaks for paragraphs
688
- // if (node.type === 'paragraph') {
689
- // text += '\n';
690
- // }
691
- // }
692
- //
693
- // if (adfContent.content) {
694
- // adfContent.content.forEach(traverse);
695
- // }
696
- //
697
- // return text.trim();
698
- // }
699
- //
700
- // // Helper function to extract Figma links directly from ADF structure
701
- // function extractFigmaLinksFromADF(adfContent) {
702
- // if (!adfContent || typeof adfContent !== 'object') {
703
- // return [];
704
- // }
705
- //
706
- // const figmaLinks = [];
707
- //
708
- // function traverse(node) {
709
- // // Check for inlineCard nodes with Figma URLs
710
- // if (node.type === 'inlineCard' && node.attrs && node.attrs.url) {
711
- // const url = node.attrs.url;
712
- // if (url.includes('figma.com')) {
713
- // figmaLinks.push(url);
714
- // }
715
- // }
716
- //
717
- // // Check for link marks with Figma URLs
718
- // if (node.marks && Array.isArray(node.marks)) {
719
- // node.marks.forEach(mark => {
720
- // if (mark.type === 'link' && mark.attrs && mark.attrs.href) {
721
- // const href = mark.attrs.href;
722
- // if (href.includes('figma.com')) {
723
- // figmaLinks.push(href);
724
- // }
725
- // }
726
- // });
727
- // }
728
- //
729
- // // Traverse child content
730
- // if (node.content && Array.isArray(node.content)) {
731
- // node.content.forEach(traverse);
732
- // }
733
- // }
734
- //
735
- // if (adfContent.content) {
736
- // adfContent.content.forEach(traverse);
737
- // }
738
- //
739
- // // Remove duplicates and return
740
- // return [...new Set(figmaLinks)];
741
- // }
742
- //
743
- //// ----------------------
744
- //// Helper: Extract File ID & Node ID
745
- //// ----------------------
746
- //function extractFileAndNodeId(url) {
747
- // const patterns = [
748
- // /figma\.com\/file\/([a-zA-Z0-9]+)/,
749
- // /figma\.com\/design\/([a-zA-Z0-9]+)/,
750
- // /figma\.com\/proto\/([a-zA-Z0-9]+)/
751
- // ];
752
- //
753
- // let fileId = null;
754
- // for (const pattern of patterns) {
755
- // const match = url.match(pattern);
756
- // if (match) {
757
- // fileId = match[1];
758
- // break;
759
- // }
760
- // }
761
- //
762
- // // Extract node-id if present
763
- // const nodeMatch = url.match(/[?&]node-id=([^&]+)/);
764
- // let nodeId = null;
765
- // if (nodeMatch) {
766
- // // Replace dash with colon (Figma expects 13:5951 instead of 13-5951)
767
- // nodeId = decodeURIComponent(nodeMatch[1]).replace(/-/g, ":");
768
- // }
769
- //
770
- // return { fileId, nodeId };
771
- //}
772
- //
773
- //// ----------------------
774
- //// TOOL 1: Export Figma to PNG
775
- //// ----------------------
776
- //tool(
777
- // "mobile_export_figma_png",
778
- // "Export Figma file as PNG",
779
- // {
780
- // figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PNG")
781
- // },
782
- // async ({ figmaUrl }) => {
783
- // try {
784
- // // Load Figma token from Desktop/figma.json
785
- // const figmaConfigPath = path.join(os.homedir(), "Desktop", "figma.json");
786
- // const configContent = await fs.readFile(figmaConfigPath, "utf-8");
787
- // const { token: figmaToken } = JSON.parse(configContent);
788
- //
789
- // if (!figmaToken) throw new Error("Figma API token missing in figma.json");
790
- //
791
- // // Extract fileId and nodeId from URL
792
- // const { fileId, nodeId } = extractFileAndNodeId(figmaUrl);
793
- // if (!fileId) throw new Error("Invalid Figma URL - cannot extract fileId");
794
- //
795
- // let idsToExport = [];
796
- //
797
- // if (nodeId) {
798
- // // Use node-id directly from URL
799
- // idsToExport = [nodeId];
800
- // } else {
801
- // // Fallback: scan file to collect all top-level frames
802
- // const fileResponse = await axios.get(
803
- // `https://api.figma.com/v1/files/${fileId}`,
804
- // { headers: { "X-Figma-Token": figmaToken } }
805
- // );
806
- //
807
- // fileResponse.data.document.children?.forEach(page => {
808
- // page.children?.forEach(child => {
809
- // if (child.type === "FRAME") idsToExport.push(child.id);
810
- // });
811
- // });
812
- //
813
- // if (idsToExport.length === 0)
814
- // throw new Error("No frames found in Figma file");
815
- // }
816
- //
817
- // // Request PNG export with higher scale for better quality
818
- // const exportResponse = await axios.get(
819
- // `https://api.figma.com/v1/images/${fileId}`,
820
- // {
821
- // headers: { "X-Figma-Token": figmaToken },
822
- // params: {
823
- // ids: idsToExport.join(","),
824
- // format: "png",
825
- // scale: "2" // 2x scale for better quality
826
- // }
827
- // }
828
- // );
829
- //
830
- // const exportPath = path.join(os.homedir(), "Desktop", "figma");
831
- //
832
- // // Clear the folder before creating new PNGs
833
- // try {
834
- // // Check if folder exists
835
- // await fs.access(exportPath);
836
- // // If folder exists, remove all contents
837
- // const files = await fs.readdir(exportPath);
838
- // await Promise.all(
839
- // files.map(file => fs.unlink(path.join(exportPath, file)))
840
- // );
841
- // } catch (err) {
842
- // // Folder doesn't exist or is empty, no need to clear
843
- // if (err.code !== 'ENOENT') {
844
- // }
845
- // }
846
- //
847
- // // Ensure directory exists
848
- // await fs.mkdir(exportPath, { recursive: true });
849
- //
850
- // const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
851
- //
852
- // // Download all PNG images
853
- // const downloadPromises = Object.entries(exportResponse.data.images).map(
854
- // async ([nodeId, pngUrl], index) => {
855
- // if (!pngUrl) throw new Error(`No PNG export URL returned for node ${nodeId}`);
856
- //
857
- // const pngResponse = await axios.get(pngUrl, { responseType: "arraybuffer" });
858
- // const filename = idsToExport.length > 1
859
- // ? `figma-export-${timestamp}-${index + 1}.png`
860
- // : `figma-export-${timestamp}.png`;
861
- // const pngPath = path.join(exportPath, filename);
862
- //
863
- // await fs.writeFile(pngPath, pngResponse.data);
864
- // return pngPath;
865
- // }
866
- // );
867
- //
868
- // const savedPaths = await Promise.all(downloadPromises);
869
- //
870
- // return `✅ PNG Export Complete: ${savedPaths.length} file(s) saved to ${exportPath}`;
871
- // } catch (err) {
872
- // return `❌ Error exporting Figma PNG: ${err.message}`;
873
- // }
874
- // }
875
- //);
876
-
877
- //tool(
878
- // "fetch_testcases_from_tcms",
879
- // "Before calling these tool, folder name can be analysed by data fetched from jira ticket info. Before generating test cases for a jira ticket, always fetch existing test cases from TCMS tool for a specific folder",
880
- // {
881
- // projectKey: zod_1.z.string().describe("The project key Default: SCRUM"),
882
- // folderName: zod_1.z.string().describe("The folder name to filter test cases (e.g., PDP), folder name can to be fetched from jira ticket info")
883
- // },
884
- // async ({ projectKey, folderName }) => {
885
- // try {
886
- // // Load AIO token from Desktop/aio.json
887
- // const aioConfigPath = path.join(os.homedir(), "Desktop", "aio.json");
888
- // const configContent = await fs.readFile(aioConfigPath, "utf-8");
889
- // const { token } = JSON.parse(configContent);
890
- //
891
- // if (!token) throw new Error("AIO token missing in aio.json");
892
- //
893
- // // Make API request to TCMS
894
- // const response = await axios.get(
895
- // `https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
896
- // {
897
- // headers: {
898
- // "accept": "application/json;charset=utf-8",
899
- // "Authorization": `AioAuth ${token}`
900
- // }
901
- // }
902
- // );
903
- //
904
- // const testCases = response.data.items || [];
905
- //
906
- // // Filter test cases by folder name
907
- // const filteredTestCases = testCases.filter(testCase =>
908
- // testCase.folder && testCase.folder.name === folderName
909
- // );
910
- //
911
- // if (filteredTestCases.length === 0) {
912
- // return `No test cases found in folder: ${folderName}`;
913
- // }
914
- //
915
- // // Extract key, title, and folder name
916
- // const extractedTestCases = filteredTestCases.map(testCase => ({
917
- // key: testCase.key,
918
- // title: testCase.title,
919
- // folderName: testCase.folder.name
920
- // }));
921
- //
922
- // // Format as string response
923
- // const result = `✅ Found ${extractedTestCases.length} test cases in folder: ${folderName}\n\n` +
924
- // extractedTestCases.map(tc =>
925
- // `Key: ${tc.key}\nTitle: ${tc.title}\nFolder: ${tc.folderName}\n---`
926
- // ).join('\n');
927
- //
928
- // return result;
929
- //
930
- // } catch (err) {
931
- // if (err.response) {
932
- // return `❌ TCMS API Error: ${err.response.status} - ${err.response.data?.message || err.response.statusText}`;
933
- // }
934
- // return `❌ Error fetching test cases: ${err.message}`;
935
- // }
936
- // }
937
- //);
938
-
939
- //tool(
940
- // "generate_testcases_from_ticket_data",
941
- // "Generate manual test cases by analyzing PNG design with JIRA requirements",
942
- // {
943
- // jiraSummary: zod_1.z.string().describe("Jira issue summary"),
944
- // jiraDescription: zod_1.z.string().describe("Jira issue description"),
945
- // existingTestCases: zod_1.z.string().optional().describe("Existing test cases from TCMS")
946
- // },
947
- // async ({ jiraSummary, jiraDescription, existingTestCases }) => {
948
- // try {
949
- // // Clear the generated test cases file before starting
950
- // const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
951
- // await fs.writeFile(testCasesFilePath, ''); // Clear the file
952
- //
953
- // // Load OpenAI API key from Desktop/openai.json
954
- // const openaiConfigPath = path.join(os.homedir(), "Desktop", "openai.json");
955
- // const configContent = await fs.readFile(openaiConfigPath, "utf-8");
956
- // const { apiKey } = JSON.parse(configContent.trim());
957
- //
958
- // // Load test case generation guidelines
959
- // const guidelinesPath = path.join(__dirname, 'testcases-generation-context.txt');
960
- // const guidelines = await fs.readFile(guidelinesPath, "utf-8");
961
- //
962
- // const figmaDir = path.join(os.homedir(), "Desktop", "figma");
963
- // const files = await fs.readdir(figmaDir);
964
- // const pngFiles = files.filter(file => file.toLowerCase().endsWith('.png'));
965
- //
966
- // if (pngFiles.length === 0) throw new Error("No PNG files found in figma folder");
967
- //
968
- // // Get the latest PNG file
969
- // const latestPng = pngFiles.sort((a, b) => b.localeCompare(a))[0];
970
- // const pngPath = path.join(figmaDir, latestPng);
971
- //
972
- // const client = new OpenAI({ apiKey });
973
- //
974
- // // Convert PNG to base64 for vision API
975
- // const pngBuffer = await fs.readFile(pngPath);
976
- // const base64Image = pngBuffer.toString('base64');
977
- //
978
- // // Start OpenAI generation (this will run in background due to timeout)
979
- // client.chat.completions.create({
980
- // model: "gpt-5", // Use GPT-5 model for image analysis
981
- // messages: [{
982
- // role: "user",
983
- // content: [
984
- // {
985
- // type: "text",
986
- // text: `Generate manual test cases based on the following:
987
- //
988
- //JIRA Summary: ${jiraSummary}
989
- //
990
- //JIRA Description: ${jiraDescription}
991
- //
992
- //${existingTestCases ? `Existing Test Cases from TCMS:
993
- //${existingTestCases}
994
- //
995
- //Please consider these existing test cases and generate additional comprehensive test cases that complement them.` : ''}
996
- //
997
- //Test Case Generation Guidelines:
998
- //${guidelines}`
999
- // },
1000
- // {
1001
- // type: "image_url",
1002
- // image_url: {
1003
- // url: `data:image/png;base64,${base64Image}`,
1004
- // detail: "high"
1005
- // }
1006
- // }
1007
- // ]
1008
- // }],
1009
- // max_completion_tokens: 10000
1010
- // }).then(async (completion) => {
1011
- // // Save test cases to file when generation completes
1012
- // const testCases = completion.choices[0].message.content;
1013
- // await fs.writeFile(testCasesFilePath, testCases);
1014
- // }).catch(async (error) => {
1015
- // // Save error to file if generation fails
1016
- // await fs.writeFile(testCasesFilePath, `Error generating test cases: ${error.message}`);
1017
- // });
1018
- //
1019
- // return "✅ Test case generation started. Use 'check_testcases_status' tool to check if generation is complete. Try max 10 times";
1020
- // } catch (err) {
1021
- // return `❌ Error starting test case generation: ${err.message}`;
1022
- // }
1023
- // }
1024
- //);
1025
- //
1026
- //tool(
1027
- // "check_testcases_status",
1028
- // "Check if test cases have been generated and saved to file",
1029
- // {},
1030
- // async () => {
1031
- // try {
1032
- // // Wait for 20 seconds before checking
1033
- // await new Promise(resolve => setTimeout(resolve, 25000));
1034
- //
1035
- // const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
1036
- //
1037
- // // Check if file exists and has content
1038
- // try {
1039
- // const fileContent = await fs.readFile(testCasesFilePath, 'utf-8');
1040
- //
1041
- // if (fileContent.trim().length === 0) {
1042
- // return "❌ Test cases are still being generated. Please wait and try again.";
1043
- // }
1044
- //
1045
- // if (fileContent.startsWith('Error generating test cases:')) {
1046
- // return `❌ ${fileContent}`;
1047
- // }
1048
- //
1049
- // return `✅ Test cases generated successfully!\n\n${fileContent}`;
1050
- // } catch (fileError) {
1051
- // return "❌ Test cases file not found or still being created. Please wait and try again.";
1052
- // }
1053
- // } catch (err) {
1054
- // return `❌ Error checking test cases status: ${err.message}`;
1055
- // }
1056
- // }
1057
- //);
1058
- //
1059
- //// Fix 2: Updated review_testcases tool with proper JSON handling and open module usage
1060
- //tool(
1061
- // "review_testcases",
1062
- // "Open test cases in browser for manual approval.",
1063
- // {
1064
- // testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("test cases array generated by tool generate_testcases_from_ticket_data")
1065
- // },
1066
- // async ({ testCases }) => {
1067
- // try {
1068
- // const app = express();
1069
- // let port = 3001;
1070
- //
1071
- // // Find an available port
1072
- // const findAvailablePort = async (startPort) => {
1073
- // const net = require('net');
1074
- // return new Promise((resolve) => {
1075
- // const server = net.createServer();
1076
- // server.listen(startPort, () => {
1077
- // const port = server.address().port;
1078
- // server.close(() => resolve(port));
1079
- // });
1080
- // server.on('error', () => {
1081
- // resolve(findAvailablePort(startPort + 1));
1082
- // });
1083
- // });
1084
- // };
1085
- //
1086
- // port = await findAvailablePort(port);
1087
- //
1088
- // // Generate unique session ID
1089
- // const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
1090
- //
1091
- // // Store approval status
1092
- // let approvalStatus = 'pending';
1093
- // let finalTestCases = [];
1094
- //
1095
- // app.use(express.json({ limit: '10mb' }));
1096
- // app.use(express.urlencoded({ extended: true, limit: '10mb' }));
1097
- //
1098
- // // Process test cases - handle the specific format properly
1099
- // const processedTestCases = testCases.map((testCase, index) => {
1100
- // if (Array.isArray(testCase)) {
1101
- // const arrayLength = testCase.length;
1102
- //
1103
- // if (arrayLength === 4) {
1104
- // // Modify case: ["original title", "new description", "Modify", "SCRUM-TC-1"]
1105
- // return {
1106
- // originalTitle: testCase[0] || `Test Case ${index + 1}`,
1107
- // newDescription: testCase[1] || '',
1108
- // status: testCase[2] || 'Modify',
1109
- // testId: testCase[3] || '',
1110
- // index: index
1111
- // };
1112
- // } else if (arrayLength === 3) {
1113
- // // Remove case: ["title", "Remove", "SCRUM-TC-2"]
1114
- // return {
1115
- // title: testCase[0] || `Test Case ${index + 1}`,
1116
- // status: testCase[1] || 'Remove',
1117
- // testId: testCase[2] || '',
1118
- // index: index
1119
- // };
1120
- // } else if (arrayLength === 2) {
1121
- // // New case: ["title", "New"]
1122
- // return {
1123
- // title: testCase[0] || `Test Case ${index + 1}`,
1124
- // status: testCase[1] || 'New',
1125
- // index: index
1126
- // };
1127
- // } else {
1128
- // // Fallback for unexpected format
1129
- // return {
1130
- // title: testCase[0] || `Test Case ${index + 1}`,
1131
- // status: 'New',
1132
- // index: index
1133
- // };
1134
- // }
1135
- // } else {
1136
- // // Fallback for non-array format
1137
- // return {
1138
- // title: String(testCase) || `Test Case ${index + 1}`,
1139
- // status: 'New',
1140
- // index: index
1141
- // };
1142
- // }
1143
- // });
1144
- //
1145
- // // Helper function to get display text for test cases
1146
- // const getTestCaseDisplayText = (testCase) => {
1147
- // const status = testCase.status.toLowerCase();
1148
- //
1149
- // if (status === 'modify') {
1150
- // // For modify cases, show original → changed format
1151
- // return `Original: ${testCase.originalTitle}\nChanged to: ${testCase.newDescription}`;
1152
- // } else if (status === 'remove') {
1153
- // // For remove cases, show the title
1154
- // return testCase.title;
1155
- // } else {
1156
- // // For new cases, show the title
1157
- // return testCase.title;
1158
- // }
1159
- // };
1160
- //
1161
- // // Main review page with proper handling
1162
- // app.get('/', (req, res) => {
1163
- // try {
1164
- // const htmlContent = `
1165
- //<!DOCTYPE html>
1166
- //<html lang="en">
1167
- //<head>
1168
- // <meta charset="UTF-8">
1169
- // <meta name="viewport" content="width=device-width, initial-scale=1.0">
1170
- // <title>Test Cases Review & Approval</title>
1171
- // <style>
1172
- // * {
1173
- // margin: 0;
1174
- // padding: 0;
1175
- // box-sizing: border-box;
1176
- // }
1177
- //
1178
- // body {
1179
- // font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
1180
- // background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1181
- // min-height: 100vh;
1182
- // padding: 20px;
1183
- // }
1184
- //
1185
- // .container {
1186
- // max-width: 1200px;
1187
- // margin: 0 auto;
1188
- // background: white;
1189
- // border-radius: 15px;
1190
- // box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
1191
- // overflow: hidden;
1192
- // }
1193
- //
1194
- // .header {
1195
- // background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
1196
- // color: white;
1197
- // padding: 30px;
1198
- // text-align: center;
1199
- // }
1200
- //
1201
- // .header h1 {
1202
- // font-size: 2.5rem;
1203
- // margin-bottom: 10px;
1204
- // font-weight: 700;
1205
- // }
1206
- //
1207
- // .header p {
1208
- // font-size: 1.1rem;
1209
- // opacity: 0.9;
1210
- // }
1211
- //
1212
- // .stats {
1213
- // display: flex;
1214
- // justify-content: space-around;
1215
- // background: #f8f9fa;
1216
- // padding: 20px;
1217
- // border-bottom: 1px solid #e9ecef;
1218
- // }
1219
- //
1220
- // .stat-item {
1221
- // text-align: center;
1222
- // }
1223
- //
1224
- // .stat-number {
1225
- // font-size: 2rem;
1226
- // font-weight: bold;
1227
- // color: #495057;
1228
- // }
1229
- //
1230
- // .stat-label {
1231
- // color: #6c757d;
1232
- // font-size: 0.9rem;
1233
- // margin-top: 5px;
1234
- // }
1235
- //
1236
- // .controls {
1237
- // padding: 20px;
1238
- // background: #f8f9fa;
1239
- // display: flex;
1240
- // justify-content: space-between;
1241
- // align-items: center;
1242
- // flex-wrap: wrap;
1243
- // gap: 10px;
1244
- // }
1245
- //
1246
- // .btn {
1247
- // padding: 12px 24px;
1248
- // border: none;
1249
- // border-radius: 8px;
1250
- // font-weight: 600;
1251
- // cursor: pointer;
1252
- // transition: all 0.3s ease;
1253
- // text-decoration: none;
1254
- // display: inline-block;
1255
- // font-size: 14px;
1256
- // }
1257
- //
1258
- // .btn-primary {
1259
- // background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
1260
- // color: white;
1261
- // }
1262
- //
1263
- // .btn-primary:hover {
1264
- // transform: translateY(-2px);
1265
- // box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
1266
- // }
1267
- //
1268
- // .btn-secondary {
1269
- // background: #6c757d;
1270
- // color: white;
1271
- // }
1272
- //
1273
- // .btn-secondary:hover {
1274
- // background: #5a6268;
1275
- // }
1276
- //
1277
- // .btn-delete {
1278
- // background: #dc3545;
1279
- // color: white;
1280
- // padding: 8px 16px;
1281
- // font-size: 12px;
1282
- // }
1283
- //
1284
- // .btn-delete:hover {
1285
- // background: #c82333;
1286
- // }
1287
- //
1288
- // .btn-restore {
1289
- // background: #28a745;
1290
- // color: white;
1291
- // padding: 8px 16px;
1292
- // font-size: 12px;
1293
- // }
1294
- //
1295
- // .btn-restore:hover {
1296
- // background: #218838;
1297
- // }
1298
- //
1299
- // .test-cases {
1300
- // max-height: 70vh;
1301
- // overflow-y: auto;
1302
- // }
1303
- //
1304
- // .test-case {
1305
- // border-bottom: 1px solid #e9ecef;
1306
- // padding: 20px;
1307
- // transition: all 0.3s ease;
1308
- // }
1309
- //
1310
- // .test-case:hover {
1311
- // background: #f8f9fa;
1312
- // }
1313
- //
1314
- // .test-case.deleted {
1315
- // opacity: 0.5;
1316
- // background: #f8d7da;
1317
- // }
1318
- //
1319
- // .test-case-header {
1320
- // display: flex;
1321
- // justify-content: space-between;
1322
- // align-items: center;
1323
- // margin-bottom: 15px;
1324
- // }
1325
- //
1326
- // .test-case-meta {
1327
- // display: flex;
1328
- // gap: 15px;
1329
- // align-items: center;
1330
- // }
1331
- //
1332
- // .test-case-index {
1333
- // background: #007bff;
1334
- // color: white;
1335
- // padding: 4px 8px;
1336
- // border-radius: 4px;
1337
- // font-size: 12px;
1338
- // font-weight: bold;
1339
- // }
1340
- //
1341
- // .test-case-status {
1342
- // padding: 4px 12px;
1343
- // border-radius: 12px;
1344
- // font-size: 12px;
1345
- // font-weight: 600;
1346
- // }
1347
- //
1348
- // .status-new {
1349
- // background: #d4edda;
1350
- // color: #155724;
1351
- // }
1352
- //
1353
- // .status-modify {
1354
- // background: #fff3cd;
1355
- // color: #856404;
1356
- // }
1357
- //
1358
- // .status-remove {
1359
- // background: #f8d7da;
1360
- // color: #721c24;
1361
- // }
1362
- //
1363
- // .test-case textarea {
1364
- // width: 100%;
1365
- // min-height: 100px;
1366
- // padding: 15px;
1367
- // border: 2px solid #e9ecef;
1368
- // border-radius: 8px;
1369
- // font-family: inherit;
1370
- // font-size: 14px;
1371
- // line-height: 1.5;
1372
- // resize: vertical;
1373
- // transition: border-color 0.3s ease;
1374
- // }
1375
- //
1376
- // .test-case textarea:focus {
1377
- // outline: none;
1378
- // border-color: #007bff;
1379
- // box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
1380
- // }
1381
- //
1382
- // .notification {
1383
- // position: fixed;
1384
- // top: 20px;
1385
- // right: 20px;
1386
- // padding: 15px 25px;
1387
- // border-radius: 8px;
1388
- // color: white;
1389
- // font-weight: 600;
1390
- // display: none;
1391
- // z-index: 1000;
1392
- // box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1393
- // }
1394
- //
1395
- // @media (max-width: 768px) {
1396
- // .container {
1397
- // margin: 10px;
1398
- // border-radius: 10px;
1399
- // }
1400
- //
1401
- // .header h1 {
1402
- // font-size: 2rem;
1403
- // }
1404
- //
1405
- // .stats {
1406
- // flex-direction: column;
1407
- // gap: 15px;
1408
- // }
1409
- //
1410
- // .controls {
1411
- // flex-direction: column;
1412
- // gap: 15px;
1413
- // }
1414
- //
1415
- // .test-case-header {
1416
- // flex-direction: column;
1417
- // align-items: flex-start;
1418
- // gap: 10px;
1419
- // }
1420
- // }
1421
- // </style>
1422
- //</head>
1423
- //<body>
1424
- // <div class="notification" id="notification"></div>
1425
- //
1426
- // <div class="container">
1427
- // <div class="header">
1428
- // <h1>🔍 Test Cases Review & Approval</h1>
1429
- // <p>Review, edit, and approve your test cases. Make any necessary changes before final approval.</p>
1430
- // </div>
1431
- //
1432
- // <div class="stats">
1433
- // <div class="stat-item">
1434
- // <div class="stat-number" id="totalCount">${processedTestCases.length}</div>
1435
- // <div class="stat-label">Total Cases</div>
1436
- // </div>
1437
- // <div class="stat-item">
1438
- // <div class="stat-number" id="activeCount">${processedTestCases.length}</div>
1439
- // <div class="stat-label">Active Cases</div>
1440
- // </div>
1441
- // <div class="stat-item">
1442
- // <div class="stat-number" id="deletedCount">0</div>
1443
- // <div class="stat-label">Deleted Cases</div>
1444
- // </div>
1445
- // </div>
1446
- //
1447
- // <div class="controls">
1448
- // <div>
1449
- // <button class="btn btn-secondary" onclick="resetAll()">🔄 Reset All</button>
1450
- // </div>
1451
- // <div>
1452
- // <button class="btn btn-primary" onclick="approveTestCases()">✅ Approve Test Cases</button>
1453
- // </div>
1454
- // </div>
1455
- //
1456
- // <div class="test-cases">
1457
- // ${processedTestCases.map((testCase, index) => {
1458
- // const displayText = getTestCaseDisplayText(testCase);
1459
- // const statusLabel = testCase.status === 'Remove' ? 'Remove' : testCase.status;
1460
- //
1461
- // // Create proper label and test ID display for modify/remove cases
1462
- // let labelAndIdDisplay = '';
1463
- // if (testCase.status.toLowerCase() === 'modify' && testCase.testId) {
1464
- // labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #856404; font-size: 13px;">Modify - ${testCase.testId}</div>`;
1465
- // } else if (testCase.status.toLowerCase() === 'remove' && testCase.testId) {
1466
- // labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #721c24; font-size: 13px;">Remove - ${testCase.testId}</div>`;
1467
- // }
1468
- //
1469
- // return `
1470
- // <div class="test-case" data-index="${index}">
1471
- // <div class="test-case-header">
1472
- // <div class="test-case-meta">
1473
- // <span class="test-case-index">#${index + 1}</span>
1474
- // <span class="test-case-status status-${testCase.status.toLowerCase()}">${statusLabel}</span>
1475
- // </div>
1476
- // <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1477
- // </div>
1478
- // ${labelAndIdDisplay}
1479
- // <textarea data-index="${index}" placeholder="Enter test case details...">${displayText}</textarea>
1480
- // </div>
1481
- // `;
1482
- // }).join('')}
1483
- // </div>
1484
- // </div>
1485
- //
1486
- // <script>
1487
- // let testCases = ${JSON.stringify(processedTestCases).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')};
1488
- // let deletedIndices = new Set();
1489
- //
1490
- // function updateStats() {
1491
- // document.getElementById('activeCount').textContent = testCases.length - deletedIndices.size;
1492
- // document.getElementById('deletedCount').textContent = deletedIndices.size;
1493
- // }
1494
- //
1495
- // function toggleDelete(index) {
1496
- // const testCaseEl = document.querySelector(\`[data-index="\${index}"]\`);
1497
- // const btn = testCaseEl.querySelector('.btn-delete, .btn-restore');
1498
- //
1499
- // if (deletedIndices.has(index)) {
1500
- // // Restore
1501
- // deletedIndices.delete(index);
1502
- // testCaseEl.classList.remove('deleted');
1503
- // btn.textContent = 'Delete';
1504
- // btn.className = 'btn btn-delete';
1505
- // } else {
1506
- // // Delete
1507
- // deletedIndices.add(index);
1508
- // testCaseEl.classList.add('deleted');
1509
- // btn.textContent = 'Restore';
1510
- // btn.className = 'btn btn-restore';
1511
- // }
1512
- // updateStats();
1513
- // }
1514
- //
1515
- // function resetAll() {
1516
- // deletedIndices.clear();
1517
- // document.querySelectorAll('.test-case').forEach((el, index) => {
1518
- // el.classList.remove('deleted');
1519
- // const btn = el.querySelector('.btn-delete, .btn-restore');
1520
- // btn.textContent = 'Delete';
1521
- // btn.className = 'btn btn-delete';
1522
- //
1523
- // // Reset textarea value
1524
- // const textarea = el.querySelector('textarea');
1525
- // const originalTestCase = testCases[index];
1526
- //
1527
- // // Use proper display format based on status
1528
- // let resetValue = '';
1529
- // const status = originalTestCase.status.toLowerCase();
1530
- //
1531
- // if (status === 'modify') {
1532
- // resetValue = 'Original: ' + originalTestCase.originalTitle + '\\nChanged to: ' + originalTestCase.newDescription;
1533
- // } else if (status === 'remove') {
1534
- // resetValue = originalTestCase.title;
1535
- // } else {
1536
- // resetValue = originalTestCase.title;
1537
- // }
1538
- //
1539
- // textarea.value = resetValue;
1540
- // });
1541
- // updateStats();
1542
- // }
1543
- //
1544
- // function showNotification(message, type = 'success') {
1545
- // const notification = document.getElementById('notification');
1546
- // notification.textContent = message;
1547
- // notification.style.background = type === 'success' ? '#28a745' : '#dc3545';
1548
- // notification.style.display = 'block';
1549
- //
1550
- // setTimeout(() => {
1551
- // notification.style.display = 'none';
1552
- // }, 3000);
1553
- // }
1554
- //
1555
- // function approveTestCases() {
1556
- // try {
1557
- // // Collect updated test cases
1558
- // const updatedTestCases = [];
1559
- // document.querySelectorAll('.test-case').forEach((el, index) => {
1560
- // if (!deletedIndices.has(index)) {
1561
- // const textarea = el.querySelector('textarea');
1562
- // const originalTestCase = testCases[index];
1563
- //
1564
- // // Create the updated test case array based on the original format
1565
- // let updatedCase;
1566
- // const status = originalTestCase.status.toLowerCase();
1567
- //
1568
- // if (status === 'modify') {
1569
- // // For modify: [updatedContent, newDescription, "Modify", testId]
1570
- // updatedCase = [
1571
- // textarea.value.trim(),
1572
- // originalTestCase.newDescription,
1573
- // originalTestCase.status,
1574
- // originalTestCase.testId
1575
- // ];
1576
- // } else if (status === 'remove') {
1577
- // // For remove: [updatedContent, "Remove", testId]
1578
- // updatedCase = [
1579
- // textarea.value.trim(),
1580
- // originalTestCase.status,
1581
- // originalTestCase.testId
1582
- // ];
1583
- // } else {
1584
- // // For new: [updatedContent, "New"]
1585
- // updatedCase = [
1586
- // textarea.value.trim(),
1587
- // originalTestCase.status
1588
- // ];
1589
- // }
1590
- //
1591
- // updatedTestCases.push(updatedCase);
1592
- // }
1593
- // });
1594
- //
1595
- // // Send approval to server
1596
- // fetch('/approve', {
1597
- // method: 'POST',
1598
- // headers: {
1599
- // 'Content-Type': 'application/json',
1600
- // },
1601
- // body: JSON.stringify({
1602
- // sessionId: '${sessionId}',
1603
- // testCases: updatedTestCases
1604
- // })
1605
- // })
1606
- // .then(response => response.json())
1607
- // .then(data => {
1608
- // if (data.success) {
1609
- // showNotification('Test cases approved successfully!');
1610
- // setTimeout(() => {
1611
- // window.close();
1612
- // }, 2000);
1613
- // } else {
1614
- // showNotification('Error approving test cases', 'error');
1615
- // }
1616
- // })
1617
- // .catch(error => {
1618
- // showNotification('Error approving test cases', 'error');
1619
- // console.error('Error:', error);
1620
- // });
1621
- // } catch (error) {
1622
- // showNotification('Error processing test cases', 'error');
1623
- // console.error('Error:', error);
1624
- // }
1625
- // }
1626
- //
1627
- // // Update test cases when textarea changes
1628
- // document.addEventListener('input', function(e) {
1629
- // if (e.target.tagName === 'TEXTAREA') {
1630
- // const index = parseInt(e.target.getAttribute('data-index'));
1631
- // if (!isNaN(index) && testCases[index]) {
1632
- // // Update the title with the textarea content
1633
- // testCases[index].title = e.target.value.trim();
1634
- // }
1635
- // }
1636
- // });
1637
- // </script>
1638
- //</body>
1639
- //</html>
1640
- // `;
1641
- // res.send(htmlContent);
1642
- // } catch (error) {
1643
- // console.error('Error rendering page:', error);
1644
- // res.status(500).send('Error rendering page');
1645
- // }
1646
- // });
1647
- //
1648
- // // Approval endpoint with better error handling
1649
- // app.post('/approve', (req, res) => {
1650
- // try {
1651
- // const { testCases: approvedTestCases, sessionId: receivedSessionId } = req.body;
1652
- //
1653
- // if (receivedSessionId !== sessionId) {
1654
- // return res.status(400).json({ success: false, message: 'Invalid session ID' });
1655
- // }
1656
- //
1657
- // finalTestCases = approvedTestCases;
1658
- // approvalStatus = 'approved';
1659
- //
1660
- // // Save to global state for the check tool
1661
- // global.approvalSessions = global.approvalSessions || {};
1662
- // global.approvalSessions[sessionId] = {
1663
- // status: 'approved',
1664
- // testCases: finalTestCases,
1665
- // timestamp: Date.now()
1666
- // };
1667
- //
1668
- // res.json({ success: true, message: 'Test cases approved successfully' });
1669
- //
1670
- // // Close server after approval
1671
- // setTimeout(() => {
1672
- // if (server && server.listening) {
1673
- // server.close();
1674
- // }
1675
- // }, 3000);
1676
- // } catch (error) {
1677
- // console.error('Approval error:', error);
1678
- // res.status(500).json({ success: false, message: error.message });
1679
- // }
1680
- // });
1681
- //
1682
- // // Error handling middleware
1683
- // app.use((err, req, res, next) => {
1684
- // console.error('Express error:', err);
1685
- // res.status(500).json({ error: 'Internal server error' });
1686
- // });
1687
- //
1688
- // // 404 handler
1689
- // app.use((req, res) => {
1690
- // res.status(404).json({ error: 'Not found' });
1691
- // });
1692
- //
1693
- // // Start server with promise-based approach
1694
- // const server = await new Promise((resolve, reject) => {
1695
- // const srv = app.listen(port, (err) => {
1696
- // if (err) {
1697
- // reject(err);
1698
- // return;
1699
- // }
1700
- // console.log(`✅ Test case review session started. Session ID: ${sessionId}.`);
1701
- // console.log(`Server running at http://localhost:${port}`);
1702
- // console.log(`Browser should open automatically.`);
1703
- // resolve(srv);
1704
- // });
1705
- //
1706
- // srv.on('error', (error) => {
1707
- // reject(error);
1708
- // });
1709
- // });
1710
- //
1711
- // // Open browser with proper error handling
1712
- // let openAttemptFailed = false;
1713
- // try {
1714
- // await openBrowser(`http://localhost:${port}`);
1715
- // } catch (err) {
1716
- // openAttemptFailed = true;
1717
- // console.error('Failed to open browser automatically:', err.message);
1718
- // // Continue without opening browser - user can manually navigate to the URL
1719
- // }
1720
- //
1721
- // // Store session globally for status checking
1722
- // global.approvalSessions = global.approvalSessions || {};
1723
- // global.approvalSessions[sessionId] = {
1724
- // status: 'pending',
1725
- // testCases: processedTestCases,
1726
- // timestamp: Date.now(),
1727
- // server: server
1728
- // };
1729
- //
1730
- // return `✅ Test case review session started. Session ID: ${sessionId}.\nServer running at http://localhost:${port}\n${openAttemptFailed ? 'Please manually open the URL in your browser.' : 'Browser should open automatically.'}`;
1731
- //
1732
- // } catch (err) {
1733
- // console.error('Review tool error:', err);
1734
- // return `❌ Error starting test case review: ${err.message}`;
1735
- // }
1736
- // }
1737
- //);
1738
- //
1739
- //// Fix 3: Updated check_approval_status tool with better error handling
1740
- //tool(
1741
- // "check_approval_status",
1742
- // "Check the approval status of test cases review session (waits 25 seconds before checking)",
1743
- // {
1744
- // sessionId: zod_1.z.string().describe("Session ID from review_testcases")
1745
- // },
1746
- // async ({ sessionId }) => {
1747
- // try {
1748
- // // Wait for 25 seconds
1749
- // await new Promise(resolve => setTimeout(resolve, 25000));
1750
- //
1751
- // // Check global approval sessions
1752
- // if (!global.approvalSessions || !global.approvalSessions[sessionId]) {
1753
- // return "Session not found. Please ensure the review session is still active.";
1754
- // }
1755
- //
1756
- // const session = global.approvalSessions[sessionId];
1757
- //
1758
- // if (session.status === 'approved') {
1759
- // const result = {
1760
- // status: 'approved',
1761
- // testCases: session.testCases,
1762
- // approvedCount: session.testCases.length,
1763
- // sessionId: sessionId
1764
- // };
1765
- //
1766
- // // Format the approved test cases properly
1767
- // const formattedTestCases = result.testCases.map((tc, index) => {
1768
- // if (!Array.isArray(tc)) {
1769
- // return `${index + 1}. ${String(tc)} (New)`;
1770
- // }
1771
- //
1772
- // // Handle different array structures based on length and content
1773
- // let title, description, status, originalCase;
1774
- //
1775
- // if (tc.length === 4) {
1776
- // // Standard format: [title, description, status, originalCase]
1777
- // title = tc[0] || `Test Case ${index + 1}`;
1778
- // description = tc[1] || '';
1779
- // status = tc[2] || 'New';
1780
- // originalCase = tc[3] || '';
1781
- // } else if (tc.length === 3) {
1782
- // // Could be [title, status, originalCase] for remove cases
1783
- // title = tc[0] || `Test Case ${index + 1}`;
1784
- // if (tc[1] && tc[1].toLowerCase() === 'remove') {
1785
- // status = tc[1];
1786
- // originalCase = tc[2] || '';
1787
- // description = '';
1788
- // } else {
1789
- // // [title, description, status]
1790
- // description = tc[1] || '';
1791
- // status = tc[2] || 'New';
1792
- // originalCase = '';
1793
- // }
1794
- // } else {
1795
- // // Fallback
1796
- // title = tc[0] || `Test Case ${index + 1}`;
1797
- // description = tc[1] || '';
1798
- // status = tc[2] || 'New';
1799
- // originalCase = tc[3] || '';
1800
- // }
1801
- //
1802
- // const statusLower = status.toLowerCase();
1803
- //
1804
- // if (statusLower === 'modify') {
1805
- // // For modify cases: show "Original: ... Changed to: ..." format with proper test ID
1806
- // return `${index + 1}. Original: ${title}\n Changed to: ${description} (Modify) (${originalCase})`;
1807
- // } else if (statusLower === 'remove') {
1808
- // // For remove cases: show title with Remove label and reference
1809
- // return `${index + 1}. ${title} (Remove) (${originalCase})`;
1810
- // } else {
1811
- // // For new cases: just show title with New label
1812
- // return `${index + 1}. ${title} (New)`;
1813
- // }
1814
- // }).join('\n');
1815
- //
1816
- // // Clean up session after returning result
1817
- // delete global.approvalSessions[sessionId];
1818
- //
1819
- // return `✅ Test cases approved successfully!\n\nApproved ${result.approvedCount} test cases:\n\n${formattedTestCases}\n\nSession completed: ${sessionId}`;
1820
- // } else {
1821
- // return "⏳ Still waiting for approval. The review session is active but not yet approved. Please complete the review in the browser.";
1822
- // }
1823
- //
1824
- // } catch (err) {
1825
- // console.error('Check approval status error:', err);
1826
- // return `❌ Error checking approval status: ${err.message}`;
1827
- // }
1828
- // }
1829
- //);
1830
- //
1831
- //tool(
1832
- // "update_testcases_to_tcms",
1833
- // "Create new test cases in TCMS from approved test cases. Only processes test cases with 'New' status, ignores Modify and Remove cases since APIs are not available.",
1834
- // {
1835
- // testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("Array of test case arrays from approved test cases")
1836
- // },
1837
- // async ({ testCases }) => {
1838
- // try {
1839
- // // Load AIO token from Desktop/aio.json
1840
- // const aioConfigPath = path.join(os.homedir(), "Desktop", "aio.json");
1841
- // const configContent = await fs.readFile(aioConfigPath, "utf-8");
1842
- // const { token } = JSON.parse(configContent);
1843
- //
1844
- // if (!token) throw new Error("AIO token missing in aio.json");
1845
- //
1846
- // // Filter test cases to extract only "New" test cases
1847
- // const newTestCases = [];
1848
- //
1849
- // for (const testCase of testCases) {
1850
- // if (Array.isArray(testCase) && testCase.length >= 2) {
1851
- // // Check if the last element or second-to-last element is "New"
1852
- // const status = testCase.length === 2 ? testCase[1] : testCase[testCase.length - 2];
1853
- //
1854
- // if (status && status.toLowerCase() === 'new') {
1855
- // const title = testCase[0]; // First element is always the title
1856
- // if (title && title.trim().length > 0) {
1857
- // newTestCases.push(title.trim());
1858
- // }
1859
- // }
1860
- // }
1861
- // }
1862
- //
1863
- // if (newTestCases.length === 0) {
1864
- // return "No new test cases found to create in TCMS. Only test cases marked as '(New)' are processed.";
1865
- // }
1866
- //
1867
- // // Hard-coded values as requested
1868
- // const projectKey = "SCRUM";
1869
- // const folderId = 1;
1870
- // const ownerId = "712020:37085ff2-5a05-47eb-8977-50a485355755";
1871
- //
1872
- // // Create test cases in TCMS one by one
1873
- // for (let i = 0; i < newTestCases.length; i++) {
1874
- // const title = newTestCases[i];
1875
- //
1876
- // try {
1877
- // const requestBody = {
1878
- // title: title,
1879
- // ownedByID: ownerId,
1880
- // folder: {
1881
- // ID: folderId
1882
- // },
1883
- // status: {
1884
- // name: "Published",
1885
- // description: "The test is ready for execution",
1886
- // ID: 1
1887
- // }
1888
- // };
1889
- //
1890
- // (0, logger_1.trace)(`Creating test case ${i + 1}/${newTestCases.length}: ${title}`);
1891
- //
1892
- // const response = await axios.post(
1893
- // `https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
1894
- // requestBody,
1895
- // {
1896
- // headers: {
1897
- // "accept": "application/json;charset=utf-8",
1898
- // "Authorization": `AioAuth ${token}`,
1899
- // "Content-Type": "application/json"
1900
- // }
1901
- // }
1902
- // );
1903
- //
1904
- // if (response.status === 200 || response.status === 201) {
1905
- // const testCaseKey = response.data.key || `${projectKey}-TC-${response.data.ID}`;
1906
- // (0, logger_1.trace)(`Successfully created test case: ${testCaseKey} - ${title}`);
1907
- // }
1908
- //
1909
- // // Add a small delay between requests to avoid rate limiting
1910
- // await new Promise(resolve => setTimeout(resolve, 500));
1911
- //
1912
- // } catch (error) {
1913
- // (0, logger_1.trace)(`Failed to create test case: ${title} - ${error.message}`);
1914
- // throw new Error(`Failed to create test case "${title}": ${error.message}`);
1915
- // }
1916
- // }
1917
- //
1918
- // return "All test cases have been updated to TCMS";
1919
- //
1920
- // } catch (error) {
1921
- // console.error('TCMS update error:', error);
1922
- // if (error.response) {
1923
- // return `❌ TCMS API Error: ${error.response.status} - ${error.response.data?.message || error.response.statusText}`;
1924
- // }
1925
- // return `❌ Error updating test cases to TCMS: ${error.message}`;
1926
- // }
1927
- // }
1928
- //);
279
+ tool(
280
+ "mobile_fetch_jira_ticket",
281
+ "Fetch JIRA ticket information including summary, description, and extract Figma links from description",
282
+ {
283
+ ticketId: zod_1.z.string().describe("The JIRA ticket ID (e.g., HDA-434)"),
284
+ },
285
+ async ({ ticketId }) => {
286
+ try {
287
+ // Read JIRA credentials from desktop/Secrets/jira.json file
288
+ const jiraConfigPath = path.join(os.homedir(), 'Desktop', 'Secrets', 'jira.json');
289
+
290
+ let jiraConfig;
291
+ try {
292
+ const configContent = await fs.readFile(jiraConfigPath, 'utf-8');
293
+ jiraConfig = JSON.parse(configContent);
294
+ } catch (error) {
295
+ throw new Error(`Failed to read JIRA config from ${jiraConfigPath}: ${error.message}`);
296
+ }
297
+
298
+ // Extract all required values from JSON file
299
+ const { api: jiraApiToken, baseUrl: jiraBaseUrl, email: jiraEmail } = jiraConfig;
300
+
301
+ if (!jiraApiToken) {
302
+ throw new Error('JIRA API token not found in jira.json file. Please ensure the file contains "api" field.');
303
+ }
304
+
305
+ if (!jiraBaseUrl) {
306
+ throw new Error('JIRA base URL not found in jira.json file. Please ensure the file contains "baseUrl" field.');
307
+ }
308
+
309
+ if (!jiraEmail) {
310
+ throw new Error('JIRA email not found in jira.json file. Please ensure the file contains "email" field.');
311
+ }
312
+
313
+ // Create Basic Auth token
314
+ const auth = Buffer.from(`${jiraEmail}:${jiraApiToken}`).toString('base64');
315
+
316
+ // Fetch ticket from JIRA API
317
+ const response = await axios.get(
318
+ `${jiraBaseUrl}/rest/api/3/issue/${ticketId}`,
319
+ {
320
+ headers: {
321
+ 'Authorization': `Basic ${auth}`,
322
+ 'Accept': 'application/json',
323
+ 'Content-Type': 'application/json'
324
+ }
325
+ }
326
+ );
327
+
328
+ const issue = response.data;
329
+
330
+ // Extract summary and description
331
+ const summary = issue.fields.summary || 'No summary available';
332
+ const description = issue.fields.description ?
333
+ extractTextFromADF(issue.fields.description) :
334
+ 'No description available';
335
+
336
+ // Extract Figma links directly from ADF structure
337
+ const figmaLinks = extractFigmaLinksFromADF(issue.fields.description);
338
+
339
+ // Format response
340
+ const result = {
341
+ ticketId: ticketId,
342
+ summary: summary,
343
+ description: description,
344
+ figmaLinks: figmaLinks.length > 0 ? figmaLinks : ['No Figma links found']
345
+ };
346
+
347
+ return `JIRA Ticket Information:
348
+ Ticket ID: ${result.ticketId}
349
+ Summary: ${result.summary}
350
+ Description: ${result.description}
351
+ Figma Links: ${result.figmaLinks.join(', ')}`;
352
+
353
+ } catch (error) {
354
+ if (error.response && error.response.status === 404) {
355
+ return `Error: JIRA ticket ${ticketId} not found. Please check the ticket ID.`;
356
+ } else if (error.response && error.response.status === 401) {
357
+ return `Error: Authentication failed. Please check your JIRA credentials.`;
358
+ } else {
359
+ return `Error fetching JIRA ticket: ${error.message}`;
360
+ }
361
+ }
362
+ }
363
+ );
364
+
365
+ // Helper function to extract text from Atlassian Document Format (ADF)
366
+ function extractTextFromADF(adfContent) {
367
+ if (!adfContent || typeof adfContent !== 'object') {
368
+ return String(adfContent || '');
369
+ }
370
+
371
+ let text = '';
372
+
373
+ function traverse(node) {
374
+ if (node.type === 'text') {
375
+ text += node.text || '';
376
+ } else if (node.content && Array.isArray(node.content)) {
377
+ node.content.forEach(traverse);
378
+ }
379
+
380
+ // Add line breaks for paragraphs
381
+ if (node.type === 'paragraph') {
382
+ text += '\n';
383
+ }
384
+ }
385
+
386
+ if (adfContent.content) {
387
+ adfContent.content.forEach(traverse);
388
+ }
389
+
390
+ return text.trim();
391
+ }
392
+
393
+ // Helper function to extract Figma links directly from ADF structure
394
+ function extractFigmaLinksFromADF(adfContent) {
395
+ if (!adfContent || typeof adfContent !== 'object') {
396
+ return [];
397
+ }
398
+
399
+ const figmaLinks = [];
400
+
401
+ function traverse(node) {
402
+ // Check for inlineCard nodes with Figma URLs
403
+ if (node.type === 'inlineCard' && node.attrs && node.attrs.url) {
404
+ const url = node.attrs.url;
405
+ if (url.includes('figma.com')) {
406
+ figmaLinks.push(url);
407
+ }
408
+ }
409
+
410
+ // Check for link marks with Figma URLs
411
+ if (node.marks && Array.isArray(node.marks)) {
412
+ node.marks.forEach(mark => {
413
+ if (mark.type === 'link' && mark.attrs && mark.attrs.href) {
414
+ const href = mark.attrs.href;
415
+ if (href.includes('figma.com')) {
416
+ figmaLinks.push(href);
417
+ }
418
+ }
419
+ });
420
+ }
421
+
422
+ // Traverse child content
423
+ if (node.content && Array.isArray(node.content)) {
424
+ node.content.forEach(traverse);
425
+ }
426
+ }
427
+
428
+ if (adfContent.content) {
429
+ adfContent.content.forEach(traverse);
430
+ }
431
+
432
+ // Remove duplicates and return
433
+ return [...new Set(figmaLinks)];
434
+ }
435
+
436
+ // ----------------------
437
+ // Helper: Extract File ID & Node ID
438
+ // ----------------------
439
+ function extractFileAndNodeId(url) {
440
+ const patterns = [
441
+ /figma\.com\/file\/([a-zA-Z0-9]+)/,
442
+ /figma\.com\/design\/([a-zA-Z0-9]+)/,
443
+ /figma\.com\/proto\/([a-zA-Z0-9]+)/
444
+ ];
445
+
446
+ let fileId = null;
447
+ for (const pattern of patterns) {
448
+ const match = url.match(pattern);
449
+ if (match) {
450
+ fileId = match[1];
451
+ break;
452
+ }
453
+ }
454
+
455
+ // Extract node-id if present
456
+ const nodeMatch = url.match(/[?&]node-id=([^&]+)/);
457
+ let nodeId = null;
458
+ if (nodeMatch) {
459
+ // Replace dash with colon (Figma expects 13:5951 instead of 13-5951)
460
+ nodeId = decodeURIComponent(nodeMatch[1]).replace(/-/g, ":");
461
+ }
462
+
463
+ return { fileId, nodeId };
464
+ }
465
+
466
+ // ----------------------
467
+ // TOOL 1: Export Figma to PNG
468
+ // ----------------------
469
+ tool(
470
+ "mobile_export_figma_png",
471
+ "Export Figma file as PNG",
472
+ {
473
+ figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PNG")
474
+ },
475
+ async ({ figmaUrl }) => {
476
+ try {
477
+ // Load Figma token from Desktop/Secrets/figma.json
478
+ const figmaConfigPath = path.join(os.homedir(), "Desktop", "Secrets", "figma.json");
479
+ const configContent = await fs.readFile(figmaConfigPath, "utf-8");
480
+ const { token: figmaToken } = JSON.parse(configContent);
481
+
482
+ if (!figmaToken) throw new Error("Figma API token missing in figma.json");
483
+
484
+ // Extract fileId and nodeId from URL
485
+ const { fileId, nodeId } = extractFileAndNodeId(figmaUrl);
486
+ if (!fileId) throw new Error("Invalid Figma URL - cannot extract fileId");
487
+
488
+ let idsToExport = [];
489
+
490
+ if (nodeId) {
491
+ // Use node-id directly from URL
492
+ idsToExport = [nodeId];
493
+ } else {
494
+ // Fallback: scan file to collect all top-level frames
495
+ const fileResponse = await axios.get(
496
+ `https://api.figma.com/v1/files/${fileId}`,
497
+ { headers: { "X-Figma-Token": figmaToken } }
498
+ );
499
+
500
+ fileResponse.data.document.children?.forEach(page => {
501
+ page.children?.forEach(child => {
502
+ if (child.type === "FRAME") idsToExport.push(child.id);
503
+ });
504
+ });
505
+
506
+ if (idsToExport.length === 0)
507
+ throw new Error("No frames found in Figma file");
508
+ }
509
+
510
+ // Request PNG export with higher scale for better quality
511
+ const exportResponse = await axios.get(
512
+ `https://api.figma.com/v1/images/${fileId}`,
513
+ {
514
+ headers: { "X-Figma-Token": figmaToken },
515
+ params: {
516
+ ids: idsToExport.join(","),
517
+ format: "png",
518
+ scale: "2" // 2x scale for better quality
519
+ }
520
+ }
521
+ );
522
+
523
+ const exportPath = path.join(os.homedir(), "Desktop", "figma");
524
+
525
+ // Clear the folder before creating new PNGs
526
+ try {
527
+ // Check if folder exists
528
+ await fs.access(exportPath);
529
+ // If folder exists, remove all contents
530
+ const files = await fs.readdir(exportPath);
531
+ await Promise.all(
532
+ files.map(file => fs.unlink(path.join(exportPath, file)))
533
+ );
534
+ } catch (err) {
535
+ // Folder doesn't exist or is empty, no need to clear
536
+ if (err.code !== 'ENOENT') {
537
+ }
538
+ }
539
+
540
+ // Ensure directory exists
541
+ await fs.mkdir(exportPath, { recursive: true });
542
+
543
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
544
+
545
+ // Download all PNG images
546
+ const downloadPromises = Object.entries(exportResponse.data.images).map(
547
+ async ([nodeId, pngUrl], index) => {
548
+ if (!pngUrl) throw new Error(`No PNG export URL returned for node ${nodeId}`);
549
+
550
+ const pngResponse = await axios.get(pngUrl, { responseType: "arraybuffer" });
551
+ const filename = idsToExport.length > 1
552
+ ? `figma-export-${timestamp}-${index + 1}.png`
553
+ : `figma-export-${timestamp}.png`;
554
+ const pngPath = path.join(exportPath, filename);
555
+
556
+ await fs.writeFile(pngPath, pngResponse.data);
557
+ return pngPath;
558
+ }
559
+ );
560
+
561
+ const savedPaths = await Promise.all(downloadPromises);
562
+
563
+ return `✅ PNG Export Complete: ${savedPaths.length} file(s) saved to ${exportPath}`;
564
+ } catch (err) {
565
+ return `❌ Error exporting Figma PNG: ${err.message}`;
566
+ }
567
+ }
568
+ );
569
+
570
+ tool(
571
+ "fetch_testcases_from_tcms",
572
+ "Before calling these tool, folder name can be analysed by data fetched from jira ticket info. Before generating test cases for a jira ticket, always fetch existing test cases from TCMS tool for a specific folder",
573
+ {
574
+ projectKey: zod_1.z.string().describe("The project key Default: SCRUM"),
575
+ folderName: zod_1.z.string().describe("The folder name to filter test cases (e.g., PDP), folder name can to be fetched from jira ticket info")
576
+ },
577
+ async ({ projectKey, folderName }) => {
578
+ try {
579
+ // Load AIO token from Desktop/Secrets/aio.json
580
+ const aioConfigPath = path.join(os.homedir(), "Desktop", "Secrets", "aio.json");
581
+ const configContent = await fs.readFile(aioConfigPath, "utf-8");
582
+ const { token } = JSON.parse(configContent);
583
+
584
+ if (!token) throw new Error("AIO token missing in aio.json");
585
+
586
+ // Make API request to TCMS
587
+ const response = await axios.get(
588
+ `https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
589
+ {
590
+ headers: {
591
+ "accept": "application/json;charset=utf-8",
592
+ "Authorization": `AioAuth ${token}`
593
+ }
594
+ }
595
+ );
596
+
597
+ const testCases = response.data.items || [];
598
+
599
+ // Filter test cases by folder name
600
+ const filteredTestCases = testCases.filter(testCase =>
601
+ testCase.folder && testCase.folder.name === folderName
602
+ );
603
+
604
+ if (filteredTestCases.length === 0) {
605
+ return `No test cases found in folder: ${folderName}`;
606
+ }
607
+
608
+ // Extract key, title, and folder name
609
+ const extractedTestCases = filteredTestCases.map(testCase => ({
610
+ key: testCase.key,
611
+ title: testCase.title,
612
+ folderName: testCase.folder.name
613
+ }));
614
+
615
+ // Format as string response
616
+ const result = `✅ Found ${extractedTestCases.length} test cases in folder: ${folderName}\n\n` +
617
+ extractedTestCases.map(tc =>
618
+ `Key: ${tc.key}\nTitle: ${tc.title}\nFolder: ${tc.folderName}\n---`
619
+ ).join('\n');
620
+
621
+ return result;
622
+
623
+ } catch (err) {
624
+ if (err.response) {
625
+ return `❌ TCMS API Error: ${err.response.status} - ${err.response.data?.message || err.response.statusText}`;
626
+ }
627
+ return `❌ Error fetching test cases: ${err.message}`;
628
+ }
629
+ }
630
+ );
631
+
632
+ tool(
633
+ "generate_testcases_from_ticket_data",
634
+ "Generate manual test cases by analyzing PNG design with JIRA requirements",
635
+ {
636
+ jiraSummary: zod_1.z.string().describe("Jira issue summary"),
637
+ jiraDescription: zod_1.z.string().describe("Jira issue description"),
638
+ existingTestCases: zod_1.z.string().optional().describe("Existing test cases from TCMS")
639
+ },
640
+ async ({ jiraSummary, jiraDescription, existingTestCases }) => {
641
+ try {
642
+ // Clear the generated test cases file before starting
643
+ const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
644
+ await fs.writeFile(testCasesFilePath, ''); // Clear the file
645
+
646
+ // Load Google API key from Desktop/Secrets/google.json
647
+ const googleConfigPath = path.join(os.homedir(), "Desktop", "Secrets", "google.json");
648
+ const configContent = await fs.readFile(googleConfigPath, "utf-8");
649
+ const { apiKey } = JSON.parse(configContent.trim());
650
+
651
+ if (!apiKey) throw new Error("Google API key missing in google.json");
652
+
653
+ // Load test case generation guidelines
654
+ const guidelinesPath = path.join(__dirname, 'testcases-generation-context.txt');
655
+ const guidelines = await fs.readFile(guidelinesPath, "utf-8");
656
+
657
+ const figmaDir = path.join(os.homedir(), "Desktop", "figma");
658
+ const files = await fs.readdir(figmaDir);
659
+ const pngFiles = files.filter(file => file.toLowerCase().endsWith('.png'));
660
+
661
+ if (pngFiles.length === 0) throw new Error("No PNG files found in figma folder");
662
+
663
+ // Get the latest PNG file
664
+ const latestPng = pngFiles.sort((a, b) => b.localeCompare(a))[0];
665
+ const pngPath = path.join(figmaDir, latestPng);
666
+
667
+ // Convert PNG to base64 for vision API
668
+ const pngBuffer = await fs.readFile(pngPath);
669
+ const base64Image = pngBuffer.toString('base64');
670
+
671
+ // Construct the prompt
672
+ const promptText = `Generate manual test cases based on the following:
673
+
674
+ JIRA Summary: ${jiraSummary}
675
+
676
+ JIRA Description: ${jiraDescription}
677
+
678
+ ${existingTestCases ? `Existing Test Cases from TCMS:
679
+ ${existingTestCases}
680
+
681
+ Please consider these existing test cases and generate additional comprehensive test cases that complement them.` : ''}
682
+
683
+ Test Case Generation Guidelines:
684
+ ${guidelines}`;
685
+
686
+ // Start Gemini API generation (this will run in background due to timeout)
687
+ axios.post(
688
+ `https://generativelanguage.googleapis.com/v1/models/gemini-2.0-flash-exp:generateContent?key=${apiKey}`,
689
+ {
690
+ contents: [
691
+ {
692
+ parts: [
693
+ {
694
+ text: promptText
695
+ },
696
+ {
697
+ inline_data: {
698
+ mime_type: "image/png",
699
+ data: base64Image
700
+ }
701
+ }
702
+ ]
703
+ }
704
+ ]
705
+ },
706
+ {
707
+ headers: {
708
+ "Content-Type": "application/json"
709
+ }
710
+ }
711
+ ).then(async (response) => {
712
+ // Save test cases to file when generation completes
713
+ const testCases = response.data.candidates?.[0]?.content?.parts?.[0]?.text || 'No test cases generated';
714
+ await fs.writeFile(testCasesFilePath, testCases);
715
+ }).catch(async (error) => {
716
+ // Save error to file if generation fails
717
+ const errorMessage = error.response?.data?.error?.message || error.message;
718
+ await fs.writeFile(testCasesFilePath, `Error generating test cases: ${errorMessage}`);
719
+ });
720
+
721
+ return " Test case generation started. Use 'check_testcases_status' tool to check if generation is complete. Try max 10 times";
722
+ } catch (err) {
723
+ return `❌ Error starting test case generation: ${err.message}`;
724
+ }
725
+ }
726
+ );
727
+
728
+ tool(
729
+ "check_testcases_status",
730
+ "Check if test cases have been generated and saved to file",
731
+ {},
732
+ async () => {
733
+ try {
734
+ // Wait for 20 seconds before checking
735
+ await new Promise(resolve => setTimeout(resolve, 25000));
736
+
737
+ const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
738
+
739
+ // Check if file exists and has content
740
+ try {
741
+ const fileContent = await fs.readFile(testCasesFilePath, 'utf-8');
742
+
743
+ if (fileContent.trim().length === 0) {
744
+ return "❌ Test cases are still being generated. Please wait and try again.";
745
+ }
746
+
747
+ if (fileContent.startsWith('Error generating test cases:')) {
748
+ return `❌ ${fileContent}`;
749
+ }
750
+
751
+ return `✅ Test cases generated successfully!\n\n${fileContent}`;
752
+ } catch (fileError) {
753
+ return "❌ Test cases file not found or still being created. Please wait and try again.";
754
+ }
755
+ } catch (err) {
756
+ return `❌ Error checking test cases status: ${err.message}`;
757
+ }
758
+ }
759
+ );
760
+
761
+ // Fix 2: Updated review_testcases tool with proper JSON handling and open module usage
762
+ tool(
763
+ "review_testcases",
764
+ "Open test cases in browser for manual approval.",
765
+ {
766
+ testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("test cases array generated by tool generate_testcases_from_ticket_data")
767
+ },
768
+ async ({ testCases }) => {
769
+ try {
770
+ const app = express();
771
+ let port = 3001;
772
+
773
+ // Find an available port
774
+ const findAvailablePort = async (startPort) => {
775
+ const net = require('net');
776
+ return new Promise((resolve) => {
777
+ const server = net.createServer();
778
+ server.listen(startPort, () => {
779
+ const port = server.address().port;
780
+ server.close(() => resolve(port));
781
+ });
782
+ server.on('error', () => {
783
+ resolve(findAvailablePort(startPort + 1));
784
+ });
785
+ });
786
+ };
787
+
788
+ port = await findAvailablePort(port);
789
+
790
+ // Generate unique session ID
791
+ const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
792
+
793
+ // Store approval status
794
+ let approvalStatus = 'pending';
795
+ let finalTestCases = [];
796
+
797
+ app.use(express.json({ limit: '10mb' }));
798
+ app.use(express.urlencoded({ extended: true, limit: '10mb' }));
799
+
800
+ // Process test cases - handle the specific format properly
801
+ const processedTestCases = testCases.map((testCase, index) => {
802
+ if (Array.isArray(testCase)) {
803
+ const arrayLength = testCase.length;
804
+
805
+ if (arrayLength === 4) {
806
+ // Modify case: ["original title", "new description", "Modify", "SCRUM-TC-1"]
807
+ return {
808
+ originalTitle: testCase[0] || `Test Case ${index + 1}`,
809
+ newDescription: testCase[1] || '',
810
+ status: testCase[2] || 'Modify',
811
+ testId: testCase[3] || '',
812
+ index: index
813
+ };
814
+ } else if (arrayLength === 3) {
815
+ // Remove case: ["title", "Remove", "SCRUM-TC-2"]
816
+ return {
817
+ title: testCase[0] || `Test Case ${index + 1}`,
818
+ status: testCase[1] || 'Remove',
819
+ testId: testCase[2] || '',
820
+ index: index
821
+ };
822
+ } else if (arrayLength === 2) {
823
+ // New case: ["title", "New"]
824
+ return {
825
+ title: testCase[0] || `Test Case ${index + 1}`,
826
+ status: testCase[1] || 'New',
827
+ index: index
828
+ };
829
+ } else {
830
+ // Fallback for unexpected format
831
+ return {
832
+ title: testCase[0] || `Test Case ${index + 1}`,
833
+ status: 'New',
834
+ index: index
835
+ };
836
+ }
837
+ } else {
838
+ // Fallback for non-array format
839
+ return {
840
+ title: String(testCase) || `Test Case ${index + 1}`,
841
+ status: 'New',
842
+ index: index
843
+ };
844
+ }
845
+ });
846
+
847
+ // Helper function to get display text for test cases
848
+ const getTestCaseDisplayText = (testCase) => {
849
+ const status = testCase.status.toLowerCase();
850
+
851
+ if (status === 'modify') {
852
+ // For modify cases, show original → changed format
853
+ return `Original: ${testCase.originalTitle}\nChanged to: ${testCase.newDescription}`;
854
+ } else if (status === 'remove') {
855
+ // For remove cases, show the title
856
+ return testCase.title;
857
+ } else {
858
+ // For new cases, show the title
859
+ return testCase.title;
860
+ }
861
+ };
862
+
863
+ // Main review page with proper handling
864
+ app.get('/', (req, res) => {
865
+ try {
866
+ const htmlContent = `
867
+ <!DOCTYPE html>
868
+ <html lang="en">
869
+ <head>
870
+ <meta charset="UTF-8">
871
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
872
+ <title>Test Cases Review & Approval</title>
873
+ <style>
874
+ * {
875
+ margin: 0;
876
+ padding: 0;
877
+ box-sizing: border-box;
878
+ }
879
+
880
+ body {
881
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
882
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
883
+ min-height: 100vh;
884
+ padding: 20px;
885
+ }
886
+
887
+ .container {
888
+ max-width: 1200px;
889
+ margin: 0 auto;
890
+ background: white;
891
+ border-radius: 15px;
892
+ box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
893
+ overflow: hidden;
894
+ }
895
+
896
+ .header {
897
+ background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
898
+ color: white;
899
+ padding: 30px;
900
+ text-align: center;
901
+ }
902
+
903
+ .header h1 {
904
+ font-size: 2.5rem;
905
+ margin-bottom: 10px;
906
+ font-weight: 700;
907
+ }
908
+
909
+ .header p {
910
+ font-size: 1.1rem;
911
+ opacity: 0.9;
912
+ }
913
+
914
+ .stats {
915
+ display: flex;
916
+ justify-content: space-around;
917
+ background: #f8f9fa;
918
+ padding: 20px;
919
+ border-bottom: 1px solid #e9ecef;
920
+ }
921
+
922
+ .stat-item {
923
+ text-align: center;
924
+ }
925
+
926
+ .stat-number {
927
+ font-size: 2rem;
928
+ font-weight: bold;
929
+ color: #495057;
930
+ }
931
+
932
+ .stat-label {
933
+ color: #6c757d;
934
+ font-size: 0.9rem;
935
+ margin-top: 5px;
936
+ }
937
+
938
+ .controls {
939
+ padding: 20px;
940
+ background: #f8f9fa;
941
+ display: flex;
942
+ justify-content: space-between;
943
+ align-items: center;
944
+ flex-wrap: wrap;
945
+ gap: 10px;
946
+ }
947
+
948
+ .btn {
949
+ padding: 12px 24px;
950
+ border: none;
951
+ border-radius: 8px;
952
+ font-weight: 600;
953
+ cursor: pointer;
954
+ transition: all 0.3s ease;
955
+ text-decoration: none;
956
+ display: inline-block;
957
+ font-size: 14px;
958
+ }
959
+
960
+ .btn-primary {
961
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
962
+ color: white;
963
+ }
964
+
965
+ .btn-primary:hover {
966
+ transform: translateY(-2px);
967
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
968
+ }
969
+
970
+ .btn-secondary {
971
+ background: #6c757d;
972
+ color: white;
973
+ }
974
+
975
+ .btn-secondary:hover {
976
+ background: #5a6268;
977
+ }
978
+
979
+ .btn-delete {
980
+ background: #dc3545;
981
+ color: white;
982
+ padding: 8px 16px;
983
+ font-size: 12px;
984
+ }
985
+
986
+ .btn-delete:hover {
987
+ background: #c82333;
988
+ }
989
+
990
+ .btn-restore {
991
+ background: #28a745;
992
+ color: white;
993
+ padding: 8px 16px;
994
+ font-size: 12px;
995
+ }
996
+
997
+ .btn-restore:hover {
998
+ background: #218838;
999
+ }
1000
+
1001
+ .test-cases {
1002
+ max-height: 70vh;
1003
+ overflow-y: auto;
1004
+ }
1005
+
1006
+ .test-case {
1007
+ border-bottom: 1px solid #e9ecef;
1008
+ padding: 20px;
1009
+ transition: all 0.3s ease;
1010
+ }
1011
+
1012
+ .test-case:hover {
1013
+ background: #f8f9fa;
1014
+ }
1015
+
1016
+ .test-case.deleted {
1017
+ opacity: 0.5;
1018
+ background: #f8d7da;
1019
+ }
1020
+
1021
+ .test-case-header {
1022
+ display: flex;
1023
+ justify-content: space-between;
1024
+ align-items: center;
1025
+ margin-bottom: 15px;
1026
+ }
1027
+
1028
+ .test-case-meta {
1029
+ display: flex;
1030
+ gap: 15px;
1031
+ align-items: center;
1032
+ }
1033
+
1034
+ .test-case-index {
1035
+ background: #007bff;
1036
+ color: white;
1037
+ padding: 4px 8px;
1038
+ border-radius: 4px;
1039
+ font-size: 12px;
1040
+ font-weight: bold;
1041
+ }
1042
+
1043
+ .test-case-status {
1044
+ padding: 4px 12px;
1045
+ border-radius: 12px;
1046
+ font-size: 12px;
1047
+ font-weight: 600;
1048
+ }
1049
+
1050
+ .status-new {
1051
+ background: #d4edda;
1052
+ color: #155724;
1053
+ }
1054
+
1055
+ .status-modify {
1056
+ background: #fff3cd;
1057
+ color: #856404;
1058
+ }
1059
+
1060
+ .status-remove {
1061
+ background: #f8d7da;
1062
+ color: #721c24;
1063
+ }
1064
+
1065
+ .test-case textarea {
1066
+ width: 100%;
1067
+ min-height: 100px;
1068
+ padding: 15px;
1069
+ border: 2px solid #e9ecef;
1070
+ border-radius: 8px;
1071
+ font-family: inherit;
1072
+ font-size: 14px;
1073
+ line-height: 1.5;
1074
+ resize: vertical;
1075
+ transition: border-color 0.3s ease;
1076
+ }
1077
+
1078
+ .test-case textarea:focus {
1079
+ outline: none;
1080
+ border-color: #007bff;
1081
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
1082
+ }
1083
+
1084
+ .notification {
1085
+ position: fixed;
1086
+ top: 20px;
1087
+ right: 20px;
1088
+ padding: 15px 25px;
1089
+ border-radius: 8px;
1090
+ color: white;
1091
+ font-weight: 600;
1092
+ display: none;
1093
+ z-index: 1000;
1094
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1095
+ }
1096
+
1097
+ @media (max-width: 768px) {
1098
+ .container {
1099
+ margin: 10px;
1100
+ border-radius: 10px;
1101
+ }
1102
+
1103
+ .header h1 {
1104
+ font-size: 2rem;
1105
+ }
1106
+
1107
+ .stats {
1108
+ flex-direction: column;
1109
+ gap: 15px;
1110
+ }
1111
+
1112
+ .controls {
1113
+ flex-direction: column;
1114
+ gap: 15px;
1115
+ }
1116
+
1117
+ .test-case-header {
1118
+ flex-direction: column;
1119
+ align-items: flex-start;
1120
+ gap: 10px;
1121
+ }
1122
+ }
1123
+ </style>
1124
+ </head>
1125
+ <body>
1126
+ <div class="notification" id="notification"></div>
1127
+
1128
+ <div class="container">
1129
+ <div class="header">
1130
+ <h1>🔍 Test Cases Review & Approval</h1>
1131
+ <p>Review, edit, and approve your test cases. Make any necessary changes before final approval.</p>
1132
+ </div>
1133
+
1134
+ <div class="stats">
1135
+ <div class="stat-item">
1136
+ <div class="stat-number" id="totalCount">${processedTestCases.length}</div>
1137
+ <div class="stat-label">Total Cases</div>
1138
+ </div>
1139
+ <div class="stat-item">
1140
+ <div class="stat-number" id="activeCount">${processedTestCases.length}</div>
1141
+ <div class="stat-label">Active Cases</div>
1142
+ </div>
1143
+ <div class="stat-item">
1144
+ <div class="stat-number" id="deletedCount">0</div>
1145
+ <div class="stat-label">Deleted Cases</div>
1146
+ </div>
1147
+ </div>
1148
+
1149
+ <div class="controls">
1150
+ <div>
1151
+ <button class="btn btn-secondary" onclick="resetAll()">🔄 Reset All</button>
1152
+ </div>
1153
+ <div>
1154
+ <button class="btn btn-primary" onclick="approveTestCases()">✅ Approve Test Cases</button>
1155
+ </div>
1156
+ </div>
1157
+
1158
+ <div class="test-cases">
1159
+ ${processedTestCases.map((testCase, index) => {
1160
+ const displayText = getTestCaseDisplayText(testCase);
1161
+ const statusLabel = testCase.status === 'Remove' ? 'Remove' : testCase.status;
1162
+
1163
+ // Create proper label and test ID display for modify/remove cases
1164
+ let labelAndIdDisplay = '';
1165
+ if (testCase.status.toLowerCase() === 'modify' && testCase.testId) {
1166
+ labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #856404; font-size: 13px;">Modify - ${testCase.testId}</div>`;
1167
+ } else if (testCase.status.toLowerCase() === 'remove' && testCase.testId) {
1168
+ labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #721c24; font-size: 13px;">Remove - ${testCase.testId}</div>`;
1169
+ }
1170
+
1171
+ return `
1172
+ <div class="test-case" data-index="${index}">
1173
+ <div class="test-case-header">
1174
+ <div class="test-case-meta">
1175
+ <span class="test-case-index">#${index + 1}</span>
1176
+ <span class="test-case-status status-${testCase.status.toLowerCase()}">${statusLabel}</span>
1177
+ </div>
1178
+ <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1179
+ </div>
1180
+ ${labelAndIdDisplay}
1181
+ <textarea data-index="${index}" placeholder="Enter test case details...">${displayText}</textarea>
1182
+ </div>
1183
+ `;
1184
+ }).join('')}
1185
+ </div>
1186
+ </div>
1187
+
1188
+ <script>
1189
+ let testCases = ${JSON.stringify(processedTestCases).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')};
1190
+ let deletedIndices = new Set();
1191
+
1192
+ function updateStats() {
1193
+ document.getElementById('activeCount').textContent = testCases.length - deletedIndices.size;
1194
+ document.getElementById('deletedCount').textContent = deletedIndices.size;
1195
+ }
1196
+
1197
+ function toggleDelete(index) {
1198
+ const testCaseEl = document.querySelector(\`[data-index="\${index}"]\`);
1199
+ const btn = testCaseEl.querySelector('.btn-delete, .btn-restore');
1200
+
1201
+ if (deletedIndices.has(index)) {
1202
+ // Restore
1203
+ deletedIndices.delete(index);
1204
+ testCaseEl.classList.remove('deleted');
1205
+ btn.textContent = 'Delete';
1206
+ btn.className = 'btn btn-delete';
1207
+ } else {
1208
+ // Delete
1209
+ deletedIndices.add(index);
1210
+ testCaseEl.classList.add('deleted');
1211
+ btn.textContent = 'Restore';
1212
+ btn.className = 'btn btn-restore';
1213
+ }
1214
+ updateStats();
1215
+ }
1216
+
1217
+ function resetAll() {
1218
+ deletedIndices.clear();
1219
+ document.querySelectorAll('.test-case').forEach((el, index) => {
1220
+ el.classList.remove('deleted');
1221
+ const btn = el.querySelector('.btn-delete, .btn-restore');
1222
+ btn.textContent = 'Delete';
1223
+ btn.className = 'btn btn-delete';
1224
+
1225
+ // Reset textarea value
1226
+ const textarea = el.querySelector('textarea');
1227
+ const originalTestCase = testCases[index];
1228
+
1229
+ // Use proper display format based on status
1230
+ let resetValue = '';
1231
+ const status = originalTestCase.status.toLowerCase();
1232
+
1233
+ if (status === 'modify') {
1234
+ resetValue = 'Original: ' + originalTestCase.originalTitle + '\\nChanged to: ' + originalTestCase.newDescription;
1235
+ } else if (status === 'remove') {
1236
+ resetValue = originalTestCase.title;
1237
+ } else {
1238
+ resetValue = originalTestCase.title;
1239
+ }
1240
+
1241
+ textarea.value = resetValue;
1242
+ });
1243
+ updateStats();
1244
+ }
1245
+
1246
+ function showNotification(message, type = 'success') {
1247
+ const notification = document.getElementById('notification');
1248
+ notification.textContent = message;
1249
+ notification.style.background = type === 'success' ? '#28a745' : '#dc3545';
1250
+ notification.style.display = 'block';
1251
+
1252
+ setTimeout(() => {
1253
+ notification.style.display = 'none';
1254
+ }, 3000);
1255
+ }
1256
+
1257
+ function approveTestCases() {
1258
+ try {
1259
+ // Collect updated test cases
1260
+ const updatedTestCases = [];
1261
+ document.querySelectorAll('.test-case').forEach((el, index) => {
1262
+ if (!deletedIndices.has(index)) {
1263
+ const textarea = el.querySelector('textarea');
1264
+ const originalTestCase = testCases[index];
1265
+
1266
+ // Create the updated test case array based on the original format
1267
+ let updatedCase;
1268
+ const status = originalTestCase.status.toLowerCase();
1269
+
1270
+ if (status === 'modify') {
1271
+ // For modify: [updatedContent, newDescription, "Modify", testId]
1272
+ updatedCase = [
1273
+ textarea.value.trim(),
1274
+ originalTestCase.newDescription,
1275
+ originalTestCase.status,
1276
+ originalTestCase.testId
1277
+ ];
1278
+ } else if (status === 'remove') {
1279
+ // For remove: [updatedContent, "Remove", testId]
1280
+ updatedCase = [
1281
+ textarea.value.trim(),
1282
+ originalTestCase.status,
1283
+ originalTestCase.testId
1284
+ ];
1285
+ } else {
1286
+ // For new: [updatedContent, "New"]
1287
+ updatedCase = [
1288
+ textarea.value.trim(),
1289
+ originalTestCase.status
1290
+ ];
1291
+ }
1292
+
1293
+ updatedTestCases.push(updatedCase);
1294
+ }
1295
+ });
1296
+
1297
+ // Send approval to server
1298
+ fetch('/approve', {
1299
+ method: 'POST',
1300
+ headers: {
1301
+ 'Content-Type': 'application/json',
1302
+ },
1303
+ body: JSON.stringify({
1304
+ sessionId: '${sessionId}',
1305
+ testCases: updatedTestCases
1306
+ })
1307
+ })
1308
+ .then(response => response.json())
1309
+ .then(data => {
1310
+ if (data.success) {
1311
+ showNotification('Test cases approved successfully!');
1312
+ setTimeout(() => {
1313
+ window.close();
1314
+ }, 2000);
1315
+ } else {
1316
+ showNotification('Error approving test cases', 'error');
1317
+ }
1318
+ })
1319
+ .catch(error => {
1320
+ showNotification('Error approving test cases', 'error');
1321
+ console.error('Error:', error);
1322
+ });
1323
+ } catch (error) {
1324
+ showNotification('Error processing test cases', 'error');
1325
+ console.error('Error:', error);
1326
+ }
1327
+ }
1328
+
1329
+ // Update test cases when textarea changes
1330
+ document.addEventListener('input', function(e) {
1331
+ if (e.target.tagName === 'TEXTAREA') {
1332
+ const index = parseInt(e.target.getAttribute('data-index'));
1333
+ if (!isNaN(index) && testCases[index]) {
1334
+ // Update the title with the textarea content
1335
+ testCases[index].title = e.target.value.trim();
1336
+ }
1337
+ }
1338
+ });
1339
+ </script>
1340
+ </body>
1341
+ </html>
1342
+ `;
1343
+ res.send(htmlContent);
1344
+ } catch (error) {
1345
+ console.error('Error rendering page:', error);
1346
+ res.status(500).send('Error rendering page');
1347
+ }
1348
+ });
1349
+
1350
+ // Approval endpoint with better error handling
1351
+ app.post('/approve', (req, res) => {
1352
+ try {
1353
+ const { testCases: approvedTestCases, sessionId: receivedSessionId } = req.body;
1354
+
1355
+ if (receivedSessionId !== sessionId) {
1356
+ return res.status(400).json({ success: false, message: 'Invalid session ID' });
1357
+ }
1358
+
1359
+ finalTestCases = approvedTestCases;
1360
+ approvalStatus = 'approved';
1361
+
1362
+ // Save to global state for the check tool
1363
+ global.approvalSessions = global.approvalSessions || {};
1364
+ global.approvalSessions[sessionId] = {
1365
+ status: 'approved',
1366
+ testCases: finalTestCases,
1367
+ timestamp: Date.now()
1368
+ };
1369
+
1370
+ res.json({ success: true, message: 'Test cases approved successfully' });
1371
+
1372
+ // Close server after approval
1373
+ setTimeout(() => {
1374
+ if (server && server.listening) {
1375
+ server.close();
1376
+ }
1377
+ }, 3000);
1378
+ } catch (error) {
1379
+ console.error('Approval error:', error);
1380
+ res.status(500).json({ success: false, message: error.message });
1381
+ }
1382
+ });
1383
+
1384
+ // Error handling middleware
1385
+ app.use((err, req, res, next) => {
1386
+ console.error('Express error:', err);
1387
+ res.status(500).json({ error: 'Internal server error' });
1388
+ });
1389
+
1390
+ // 404 handler
1391
+ app.use((req, res) => {
1392
+ res.status(404).json({ error: 'Not found' });
1393
+ });
1394
+
1395
+ // Start server with promise-based approach
1396
+ const server = await new Promise((resolve, reject) => {
1397
+ const srv = app.listen(port, (err) => {
1398
+ if (err) {
1399
+ reject(err);
1400
+ return;
1401
+ }
1402
+ console.log(`✅ Test case review session started. Session ID: ${sessionId}.`);
1403
+ console.log(`Server running at http://localhost:${port}`);
1404
+ console.log(`Browser should open automatically.`);
1405
+ resolve(srv);
1406
+ });
1407
+
1408
+ srv.on('error', (error) => {
1409
+ reject(error);
1410
+ });
1411
+ });
1412
+
1413
+ // Open browser with proper error handling
1414
+ let openAttemptFailed = false;
1415
+ try {
1416
+ await openBrowser(`http://localhost:${port}`);
1417
+ } catch (err) {
1418
+ openAttemptFailed = true;
1419
+ console.error('Failed to open browser automatically:', err.message);
1420
+ // Continue without opening browser - user can manually navigate to the URL
1421
+ }
1422
+
1423
+ // Store session globally for status checking
1424
+ global.approvalSessions = global.approvalSessions || {};
1425
+ global.approvalSessions[sessionId] = {
1426
+ status: 'pending',
1427
+ testCases: processedTestCases,
1428
+ timestamp: Date.now(),
1429
+ server: server
1430
+ };
1431
+
1432
+ return `✅ Test case review session started. Session ID: ${sessionId}.\nServer running at http://localhost:${port}\n${openAttemptFailed ? 'Please manually open the URL in your browser.' : 'Browser should open automatically.'}`;
1433
+
1434
+ } catch (err) {
1435
+ console.error('Review tool error:', err);
1436
+ return `❌ Error starting test case review: ${err.message}`;
1437
+ }
1438
+ }
1439
+ );
1440
+
1441
+ // Fix 3: Updated check_approval_status tool with better error handling
1442
+ tool(
1443
+ "check_approval_status",
1444
+ "Check the approval status of test cases review session (waits 25 seconds before checking)",
1445
+ {
1446
+ sessionId: zod_1.z.string().describe("Session ID from review_testcases")
1447
+ },
1448
+ async ({ sessionId }) => {
1449
+ try {
1450
+ // Wait for 25 seconds
1451
+ await new Promise(resolve => setTimeout(resolve, 25000));
1452
+
1453
+ // Check global approval sessions
1454
+ if (!global.approvalSessions || !global.approvalSessions[sessionId]) {
1455
+ return "❌ Session not found. Please ensure the review session is still active.";
1456
+ }
1457
+
1458
+ const session = global.approvalSessions[sessionId];
1459
+
1460
+ if (session.status === 'approved') {
1461
+ const result = {
1462
+ status: 'approved',
1463
+ testCases: session.testCases,
1464
+ approvedCount: session.testCases.length,
1465
+ sessionId: sessionId
1466
+ };
1467
+
1468
+ // Format the approved test cases properly
1469
+ const formattedTestCases = result.testCases.map((tc, index) => {
1470
+ if (!Array.isArray(tc)) {
1471
+ return `${index + 1}. ${String(tc)} (New)`;
1472
+ }
1473
+
1474
+ // Handle different array structures based on length and content
1475
+ let title, description, status, originalCase;
1476
+
1477
+ if (tc.length === 4) {
1478
+ // Standard format: [title, description, status, originalCase]
1479
+ title = tc[0] || `Test Case ${index + 1}`;
1480
+ description = tc[1] || '';
1481
+ status = tc[2] || 'New';
1482
+ originalCase = tc[3] || '';
1483
+ } else if (tc.length === 3) {
1484
+ // Could be [title, status, originalCase] for remove cases
1485
+ title = tc[0] || `Test Case ${index + 1}`;
1486
+ if (tc[1] && tc[1].toLowerCase() === 'remove') {
1487
+ status = tc[1];
1488
+ originalCase = tc[2] || '';
1489
+ description = '';
1490
+ } else {
1491
+ // [title, description, status]
1492
+ description = tc[1] || '';
1493
+ status = tc[2] || 'New';
1494
+ originalCase = '';
1495
+ }
1496
+ } else {
1497
+ // Fallback
1498
+ title = tc[0] || `Test Case ${index + 1}`;
1499
+ description = tc[1] || '';
1500
+ status = tc[2] || 'New';
1501
+ originalCase = tc[3] || '';
1502
+ }
1503
+
1504
+ const statusLower = status.toLowerCase();
1505
+
1506
+ if (statusLower === 'modify') {
1507
+ // For modify cases: show "Original: ... Changed to: ..." format with proper test ID
1508
+ return `${index + 1}. Original: ${title}\n Changed to: ${description} (Modify) (${originalCase})`;
1509
+ } else if (statusLower === 'remove') {
1510
+ // For remove cases: show title with Remove label and reference
1511
+ return `${index + 1}. ${title} (Remove) (${originalCase})`;
1512
+ } else {
1513
+ // For new cases: just show title with New label
1514
+ return `${index + 1}. ${title} (New)`;
1515
+ }
1516
+ }).join('\n');
1517
+
1518
+ // Clean up session after returning result
1519
+ delete global.approvalSessions[sessionId];
1520
+
1521
+ return `✅ Test cases approved successfully!\n\nApproved ${result.approvedCount} test cases:\n\n${formattedTestCases}\n\nSession completed: ${sessionId}`;
1522
+ } else {
1523
+ return "⏳ Still waiting for approval. The review session is active but not yet approved. Please complete the review in the browser.";
1524
+ }
1525
+
1526
+ } catch (err) {
1527
+ console.error('Check approval status error:', err);
1528
+ return `❌ Error checking approval status: ${err.message}`;
1529
+ }
1530
+ }
1531
+ );
1532
+
1533
+ tool(
1534
+ "update_testcases_to_tcms",
1535
+ "Create new test cases in TCMS from approved test cases. Only processes test cases with 'New' status, ignores Modify and Remove cases since APIs are not available.",
1536
+ {
1537
+ testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("Array of test case arrays from approved test cases")
1538
+ },
1539
+ async ({ testCases }) => {
1540
+ try {
1541
+ // Load AIO token from Desktop/Secrets/aio.json
1542
+ const aioConfigPath = path.join(os.homedir(), "Desktop", "Secrets", "aio.json");
1543
+ const configContent = await fs.readFile(aioConfigPath, "utf-8");
1544
+ const { token } = JSON.parse(configContent);
1545
+
1546
+ if (!token) throw new Error("AIO token missing in aio.json");
1547
+
1548
+ // Filter test cases to extract only "New" test cases
1549
+ const newTestCases = [];
1550
+
1551
+ for (const testCase of testCases) {
1552
+ if (Array.isArray(testCase) && testCase.length >= 2) {
1553
+ // Check if the last element or second-to-last element is "New"
1554
+ const status = testCase.length === 2 ? testCase[1] : testCase[testCase.length - 2];
1555
+
1556
+ if (status && status.toLowerCase() === 'new') {
1557
+ const title = testCase[0]; // First element is always the title
1558
+ if (title && title.trim().length > 0) {
1559
+ newTestCases.push(title.trim());
1560
+ }
1561
+ }
1562
+ }
1563
+ }
1564
+
1565
+ if (newTestCases.length === 0) {
1566
+ return "No new test cases found to create in TCMS. Only test cases marked as '(New)' are processed.";
1567
+ }
1568
+
1569
+ // Hard-coded values as requested
1570
+ const projectKey = "SCRUM";
1571
+ const folderId = 1;
1572
+ const ownerId = "712020:37085ff2-5a05-47eb-8977-50a485355755";
1573
+
1574
+ // Create test cases in TCMS one by one
1575
+ for (let i = 0; i < newTestCases.length; i++) {
1576
+ const title = newTestCases[i];
1577
+
1578
+ try {
1579
+ const requestBody = {
1580
+ title: title,
1581
+ ownedByID: ownerId,
1582
+ folder: {
1583
+ ID: folderId
1584
+ },
1585
+ status: {
1586
+ name: "Published",
1587
+ description: "The test is ready for execution",
1588
+ ID: 1
1589
+ }
1590
+ };
1591
+
1592
+ (0, logger_1.trace)(`Creating test case ${i + 1}/${newTestCases.length}: ${title}`);
1593
+
1594
+ const response = await axios.post(
1595
+ `https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
1596
+ requestBody,
1597
+ {
1598
+ headers: {
1599
+ "accept": "application/json;charset=utf-8",
1600
+ "Authorization": `AioAuth ${token}`,
1601
+ "Content-Type": "application/json"
1602
+ }
1603
+ }
1604
+ );
1605
+
1606
+ if (response.status === 200 || response.status === 201) {
1607
+ const testCaseKey = response.data.key || `${projectKey}-TC-${response.data.ID}`;
1608
+ (0, logger_1.trace)(`Successfully created test case: ${testCaseKey} - ${title}`);
1609
+ }
1610
+
1611
+ // Add a small delay between requests to avoid rate limiting
1612
+ await new Promise(resolve => setTimeout(resolve, 500));
1613
+
1614
+ } catch (error) {
1615
+ (0, logger_1.trace)(`Failed to create test case: ${title} - ${error.message}`);
1616
+ throw new Error(`Failed to create test case "${title}": ${error.message}`);
1617
+ }
1618
+ }
1619
+
1620
+ return "All test cases have been updated to TCMS";
1621
+
1622
+ } catch (error) {
1623
+ console.error('TCMS update error:', error);
1624
+ if (error.response) {
1625
+ return `❌ TCMS API Error: ${error.response.status} - ${error.response.data?.message || error.response.statusText}`;
1626
+ }
1627
+ return `❌ Error updating test cases to TCMS: ${error.message}`;
1628
+ }
1629
+ }
1630
+ );
1929
1631
 
1930
1632
  return server;
1931
1633
  };