@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/android.js +62 -5
- package/lib/ios.js +4 -0
- package/lib/iphone-simulator.js +4 -0
- package/lib/server.js +1390 -1688
- package/lib/webdriver-agent.js +59 -2
- package/package.json +6 -6
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
|
-
|
|
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
|
|
500
|
-
return `
|
|
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("
|
|
521
|
-
direction: zod_1.z.enum(["up", "down"]).describe("The direction
|
|
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 `
|
|
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
|
|
233
|
+
"Tap an element on screen by any attribute: text, accessibilityId (content-desc), or id (resource-id). Searches all attributes automatically.",
|
|
557
234
|
{
|
|
558
|
-
|
|
235
|
+
value: zod_1.z.string().describe("The value to search for - can be text, accessibilityId, or id"),
|
|
559
236
|
},
|
|
560
|
-
async ({
|
|
561
|
-
if (!
|
|
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.
|
|
241
|
+
const elements = await robot.getSimplifiedElements();
|
|
565
242
|
|
|
566
|
-
// Find element by
|
|
243
|
+
// Find element by matching any attribute (text, accessibilityId, or id)
|
|
567
244
|
const element = elements.find(
|
|
568
|
-
el => el.text ===
|
|
245
|
+
el => el.text === value || el.accessibilityId === value || el.id === value
|
|
569
246
|
);
|
|
570
247
|
|
|
571
|
-
if (!element) throw new Error(`Element with
|
|
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
|
|
575
|
-
const
|
|
576
|
-
|
|
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
|
-
//
|
|
579
|
-
|
|
580
|
-
|
|
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
|
|
275
|
+
return `Tapped element by ${matchedBy} "${value}" at (${x},${y})`;
|
|
583
276
|
}
|
|
584
277
|
);
|
|
585
278
|
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
//
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
//
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
//
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
//
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
//
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
//
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
//
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
//
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
//
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
//
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
//
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
//
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
//
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
//
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
//
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
//
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
//
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
//
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
//
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
//
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
//
|
|
833
|
-
|
|
834
|
-
//
|
|
835
|
-
|
|
836
|
-
//
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
//
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
//
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
//
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
//
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
//
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
//
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
//
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
//
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
//
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
//
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
//
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
//
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
//
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
//
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
997
|
-
|
|
998
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
//
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
//
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
//
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
//
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
//
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
//
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
//
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
//
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
//
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
//
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
//
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
//
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
//
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
//
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
//
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
//
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
//
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
//
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1196
|
-
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
|
|
1218
|
-
|
|
1219
|
-
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1255
|
-
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
//
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
//
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
//
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
//
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
//
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
//
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
//
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
//
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
//
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
//
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
//
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
//
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
//
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
//
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
//
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
//
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
//
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
//
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
//
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
//
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
//
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
//
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
//
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
//
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
//
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
//
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
//
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
//
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
//
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
//
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
//
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1814
|
-
//
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
//
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
//
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
|
|
1825
|
-
//
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
1834
|
-
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
//
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
//
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
//
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
//
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
//
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
//
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
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
|
};
|