@nbakka/mcp-appium 3.0.24 → 3.0.25
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 +1368 -1343
- package/package.json +1 -1
package/lib/server.js
CHANGED
|
@@ -362,6 +362,31 @@ tool(
|
|
|
362
362
|
}
|
|
363
363
|
);
|
|
364
364
|
|
|
365
|
+
tool(
|
|
366
|
+
"mobile_learn_app_context_and_navigation_guidelines",
|
|
367
|
+
"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.",
|
|
368
|
+
{},
|
|
369
|
+
async () => {
|
|
370
|
+
try {
|
|
371
|
+
const guidelinesFilePath = path.join(os.homedir(), 'Desktop', 'app_context_and_navigation_guidelines.txt');
|
|
372
|
+
const fileContent = await fs.readFile(guidelinesFilePath, "utf-8");
|
|
373
|
+
|
|
374
|
+
const guidelines = fileContent
|
|
375
|
+
.split("\n")
|
|
376
|
+
.map(line => line.trim())
|
|
377
|
+
.filter(line => line.length > 0);
|
|
378
|
+
|
|
379
|
+
if (guidelines.length === 0) {
|
|
380
|
+
return "No app context and navigation guidelines found.";
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return `App context and navigation guidelines loaded: ${JSON.stringify({ guidelines })}`;
|
|
384
|
+
} catch (error) {
|
|
385
|
+
return `Error reading app context and navigation guidelines: ${error.message}`;
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
);
|
|
389
|
+
|
|
365
390
|
// Tool 3: Update Test Scenario (Always appends with green color)
|
|
366
391
|
tool(
|
|
367
392
|
"mobile_update_test_scenario",
|
|
@@ -558,1349 +583,1349 @@ tool(
|
|
|
558
583
|
}
|
|
559
584
|
);
|
|
560
585
|
|
|
561
|
-
tool(
|
|
562
|
-
"mobile_fetch_jira_ticket",
|
|
563
|
-
"Fetch JIRA ticket information including summary, description, and extract Figma links from description",
|
|
564
|
-
{
|
|
565
|
-
ticketId: zod_1.z.string().describe("The JIRA ticket ID (e.g., HDA-434)"),
|
|
566
|
-
},
|
|
567
|
-
async ({ ticketId }) => {
|
|
568
|
-
try {
|
|
569
|
-
// Read JIRA credentials from desktop/jira.json file
|
|
570
|
-
const jiraConfigPath = path.join(os.homedir(), 'Desktop', 'jira.json');
|
|
571
|
-
|
|
572
|
-
let jiraConfig;
|
|
573
|
-
try {
|
|
574
|
-
const configContent = await fs.readFile(jiraConfigPath, 'utf-8');
|
|
575
|
-
jiraConfig = JSON.parse(configContent);
|
|
576
|
-
} catch (error) {
|
|
577
|
-
throw new Error(`Failed to read JIRA config from ${jiraConfigPath}: ${error.message}`);
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
// Extract all required values from JSON file
|
|
581
|
-
const { api: jiraApiToken, baseUrl: jiraBaseUrl, email: jiraEmail } = jiraConfig;
|
|
582
|
-
|
|
583
|
-
if (!jiraApiToken) {
|
|
584
|
-
throw new Error('JIRA API token not found in jira.json file. Please ensure the file contains "api" field.');
|
|
585
|
-
}
|
|
586
|
-
|
|
587
|
-
if (!jiraBaseUrl) {
|
|
588
|
-
throw new Error('JIRA base URL not found in jira.json file. Please ensure the file contains "baseUrl" field.');
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
if (!jiraEmail) {
|
|
592
|
-
throw new Error('JIRA email not found in jira.json file. Please ensure the file contains "email" field.');
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// Create Basic Auth token
|
|
596
|
-
const auth = Buffer.from(`${jiraEmail}:${jiraApiToken}`).toString('base64');
|
|
597
|
-
|
|
598
|
-
// Fetch ticket from JIRA API
|
|
599
|
-
const response = await axios.get(
|
|
600
|
-
`${jiraBaseUrl}/rest/api/3/issue/${ticketId}`,
|
|
601
|
-
{
|
|
602
|
-
headers: {
|
|
603
|
-
'Authorization': `Basic ${auth}`,
|
|
604
|
-
'Accept': 'application/json',
|
|
605
|
-
'Content-Type': 'application/json'
|
|
606
|
-
}
|
|
607
|
-
}
|
|
608
|
-
);
|
|
609
|
-
|
|
610
|
-
const issue = response.data;
|
|
611
|
-
|
|
612
|
-
// Extract summary and description
|
|
613
|
-
const summary = issue.fields.summary || 'No summary available';
|
|
614
|
-
const description = issue.fields.description ?
|
|
615
|
-
extractTextFromADF(issue.fields.description) :
|
|
616
|
-
'No description available';
|
|
617
|
-
|
|
618
|
-
// Extract Figma links directly from ADF structure
|
|
619
|
-
const figmaLinks = extractFigmaLinksFromADF(issue.fields.description);
|
|
620
|
-
|
|
621
|
-
// Format response
|
|
622
|
-
const result = {
|
|
623
|
-
ticketId: ticketId,
|
|
624
|
-
summary: summary,
|
|
625
|
-
description: description,
|
|
626
|
-
figmaLinks: figmaLinks.length > 0 ? figmaLinks : ['No Figma links found']
|
|
627
|
-
};
|
|
628
|
-
|
|
629
|
-
return `JIRA Ticket Information:
|
|
630
|
-
Ticket ID: ${result.ticketId}
|
|
631
|
-
Summary: ${result.summary}
|
|
632
|
-
Description: ${result.description}
|
|
633
|
-
Figma Links: ${result.figmaLinks.join(', ')}`;
|
|
634
|
-
|
|
635
|
-
} catch (error) {
|
|
636
|
-
if (error.response && error.response.status === 404) {
|
|
637
|
-
return `Error: JIRA ticket ${ticketId} not found. Please check the ticket ID.`;
|
|
638
|
-
} else if (error.response && error.response.status === 401) {
|
|
639
|
-
return `Error: Authentication failed. Please check your JIRA credentials.`;
|
|
640
|
-
} else {
|
|
641
|
-
return `Error fetching JIRA ticket: ${error.message}`;
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
// Helper function to extract text from Atlassian Document Format (ADF)
|
|
648
|
-
function extractTextFromADF(adfContent) {
|
|
649
|
-
if (!adfContent || typeof adfContent !== 'object') {
|
|
650
|
-
return String(adfContent || '');
|
|
651
|
-
}
|
|
652
|
-
|
|
653
|
-
let text = '';
|
|
654
|
-
|
|
655
|
-
function traverse(node) {
|
|
656
|
-
if (node.type === 'text') {
|
|
657
|
-
text += node.text || '';
|
|
658
|
-
} else if (node.content && Array.isArray(node.content)) {
|
|
659
|
-
node.content.forEach(traverse);
|
|
660
|
-
}
|
|
661
|
-
|
|
662
|
-
// Add line breaks for paragraphs
|
|
663
|
-
if (node.type === 'paragraph') {
|
|
664
|
-
text += '\n';
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
|
|
668
|
-
if (adfContent.content) {
|
|
669
|
-
adfContent.content.forEach(traverse);
|
|
670
|
-
}
|
|
671
|
-
|
|
672
|
-
return text.trim();
|
|
673
|
-
}
|
|
674
|
-
|
|
675
|
-
// Helper function to extract Figma links directly from ADF structure
|
|
676
|
-
function extractFigmaLinksFromADF(adfContent) {
|
|
677
|
-
if (!adfContent || typeof adfContent !== 'object') {
|
|
678
|
-
return [];
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
const figmaLinks = [];
|
|
682
|
-
|
|
683
|
-
function traverse(node) {
|
|
684
|
-
// Check for inlineCard nodes with Figma URLs
|
|
685
|
-
if (node.type === 'inlineCard' && node.attrs && node.attrs.url) {
|
|
686
|
-
const url = node.attrs.url;
|
|
687
|
-
if (url.includes('figma.com')) {
|
|
688
|
-
figmaLinks.push(url);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
|
|
692
|
-
// Check for link marks with Figma URLs
|
|
693
|
-
if (node.marks && Array.isArray(node.marks)) {
|
|
694
|
-
node.marks.forEach(mark => {
|
|
695
|
-
if (mark.type === 'link' && mark.attrs && mark.attrs.href) {
|
|
696
|
-
const href = mark.attrs.href;
|
|
697
|
-
if (href.includes('figma.com')) {
|
|
698
|
-
figmaLinks.push(href);
|
|
699
|
-
}
|
|
700
|
-
}
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Traverse child content
|
|
705
|
-
if (node.content && Array.isArray(node.content)) {
|
|
706
|
-
node.content.forEach(traverse);
|
|
707
|
-
}
|
|
708
|
-
}
|
|
709
|
-
|
|
710
|
-
if (adfContent.content) {
|
|
711
|
-
adfContent.content.forEach(traverse);
|
|
712
|
-
}
|
|
713
|
-
|
|
714
|
-
// Remove duplicates and return
|
|
715
|
-
return [...new Set(figmaLinks)];
|
|
716
|
-
}
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
function extractFileAndNodeId(url) {
|
|
722
|
-
const patterns = [
|
|
723
|
-
/figma\.com\/file\/([a-zA-Z0-9]+)/,
|
|
724
|
-
/figma\.com\/design\/([a-zA-Z0-9]+)/,
|
|
725
|
-
/figma\.com\/proto\/([a-zA-Z0-9]+)/
|
|
726
|
-
];
|
|
727
|
-
|
|
728
|
-
let fileId = null;
|
|
729
|
-
for (const pattern of patterns) {
|
|
730
|
-
const match = url.match(pattern);
|
|
731
|
-
if (match) {
|
|
732
|
-
fileId = match[1];
|
|
733
|
-
break;
|
|
734
|
-
}
|
|
735
|
-
}
|
|
736
|
-
|
|
737
|
-
// Extract node-id if present
|
|
738
|
-
const nodeMatch = url.match(/[?&]node-id=([^&]+)/);
|
|
739
|
-
let nodeId = null;
|
|
740
|
-
if (nodeMatch) {
|
|
741
|
-
// Replace dash with colon (Figma expects 13:5951 instead of 13-5951)
|
|
742
|
-
nodeId = decodeURIComponent(nodeMatch[1]).replace(/-/g, ":");
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
return { fileId, nodeId };
|
|
746
|
-
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
tool(
|
|
752
|
-
"mobile_export_figma_png",
|
|
753
|
-
"Export Figma file as PNG",
|
|
754
|
-
{
|
|
755
|
-
figmaUrl: zod_1.z.string().describe("The Figma file URL to export as PNG")
|
|
756
|
-
},
|
|
757
|
-
async ({ figmaUrl }) => {
|
|
758
|
-
try {
|
|
759
|
-
// Load Figma token from Desktop/figma.json
|
|
760
|
-
const figmaConfigPath = path.join(os.homedir(), "Desktop", "figma.json");
|
|
761
|
-
const configContent = await fs.readFile(figmaConfigPath, "utf-8");
|
|
762
|
-
const { token: figmaToken } = JSON.parse(configContent);
|
|
763
|
-
|
|
764
|
-
if (!figmaToken) throw new Error("Figma API token missing in figma.json");
|
|
765
|
-
|
|
766
|
-
// Extract fileId and nodeId from URL
|
|
767
|
-
const { fileId, nodeId } = extractFileAndNodeId(figmaUrl);
|
|
768
|
-
if (!fileId) throw new Error("Invalid Figma URL - cannot extract fileId");
|
|
769
|
-
|
|
770
|
-
let idsToExport = [];
|
|
771
|
-
|
|
772
|
-
if (nodeId) {
|
|
773
|
-
// Use node-id directly from URL
|
|
774
|
-
idsToExport = [nodeId];
|
|
775
|
-
} else {
|
|
776
|
-
// Fallback: scan file to collect all top-level frames
|
|
777
|
-
const fileResponse = await axios.get(
|
|
778
|
-
`https://api.figma.com/v1/files/${fileId}`,
|
|
779
|
-
{ headers: { "X-Figma-Token": figmaToken } }
|
|
780
|
-
);
|
|
781
|
-
|
|
782
|
-
fileResponse.data.document.children?.forEach(page => {
|
|
783
|
-
page.children?.forEach(child => {
|
|
784
|
-
if (child.type === "FRAME") idsToExport.push(child.id);
|
|
785
|
-
});
|
|
786
|
-
});
|
|
787
|
-
|
|
788
|
-
if (idsToExport.length === 0)
|
|
789
|
-
throw new Error("No frames found in Figma file");
|
|
790
|
-
}
|
|
791
|
-
|
|
792
|
-
// Request PNG export with higher scale for better quality
|
|
793
|
-
const exportResponse = await axios.get(
|
|
794
|
-
`https://api.figma.com/v1/images/${fileId}`,
|
|
795
|
-
{
|
|
796
|
-
headers: { "X-Figma-Token": figmaToken },
|
|
797
|
-
params: {
|
|
798
|
-
ids: idsToExport.join(","),
|
|
799
|
-
format: "png",
|
|
800
|
-
scale: "2" // 2x scale for better quality
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
);
|
|
804
|
-
|
|
805
|
-
const exportPath = path.join(os.homedir(), "Desktop", "figma");
|
|
806
|
-
|
|
807
|
-
// Clear the folder before creating new PNGs
|
|
808
|
-
try {
|
|
809
|
-
// Check if folder exists
|
|
810
|
-
await fs.access(exportPath);
|
|
811
|
-
// If folder exists, remove all contents
|
|
812
|
-
const files = await fs.readdir(exportPath);
|
|
813
|
-
await Promise.all(
|
|
814
|
-
files.map(file => fs.unlink(path.join(exportPath, file)))
|
|
815
|
-
);
|
|
816
|
-
} catch (err) {
|
|
817
|
-
// Folder doesn't exist or is empty, no need to clear
|
|
818
|
-
if (err.code !== 'ENOENT') {
|
|
819
|
-
}
|
|
820
|
-
}
|
|
821
|
-
|
|
822
|
-
// Ensure directory exists
|
|
823
|
-
await fs.mkdir(exportPath, { recursive: true });
|
|
824
|
-
|
|
825
|
-
const timestamp = new Date().toISOString().replace(/[:.]/g, "-").slice(0, -5);
|
|
826
|
-
|
|
827
|
-
// Download all PNG images
|
|
828
|
-
const downloadPromises = Object.entries(exportResponse.data.images).map(
|
|
829
|
-
async ([nodeId, pngUrl], index) => {
|
|
830
|
-
if (!pngUrl) throw new Error(`No PNG export URL returned for node ${nodeId}`);
|
|
831
|
-
|
|
832
|
-
const pngResponse = await axios.get(pngUrl, { responseType: "arraybuffer" });
|
|
833
|
-
const filename = idsToExport.length > 1
|
|
834
|
-
? `figma-export-${timestamp}-${index + 1}.png`
|
|
835
|
-
: `figma-export-${timestamp}.png`;
|
|
836
|
-
const pngPath = path.join(exportPath, filename);
|
|
837
|
-
|
|
838
|
-
await fs.writeFile(pngPath, pngResponse.data);
|
|
839
|
-
return pngPath;
|
|
840
|
-
}
|
|
841
|
-
);
|
|
842
|
-
|
|
843
|
-
const savedPaths = await Promise.all(downloadPromises);
|
|
844
|
-
|
|
845
|
-
return `✅ PNG Export Complete: ${savedPaths.length} file(s) saved to ${exportPath}`;
|
|
846
|
-
} catch (err) {
|
|
847
|
-
return `❌ Error exporting Figma PNG: ${err.message}`;
|
|
848
|
-
}
|
|
849
|
-
}
|
|
850
|
-
);
|
|
851
|
-
|
|
852
|
-
tool(
|
|
853
|
-
"fetch_testcases_from_tcms",
|
|
854
|
-
"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",
|
|
855
|
-
{
|
|
856
|
-
projectKey: zod_1.z.string().describe("The project key Default: SCRUM"),
|
|
857
|
-
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")
|
|
858
|
-
},
|
|
859
|
-
async ({ projectKey, folderName }) => {
|
|
860
|
-
try {
|
|
861
|
-
// Load AIO token from Desktop/aio.json
|
|
862
|
-
const aioConfigPath = path.join(os.homedir(), "Desktop", "aio.json");
|
|
863
|
-
const configContent = await fs.readFile(aioConfigPath, "utf-8");
|
|
864
|
-
const { token } = JSON.parse(configContent);
|
|
865
|
-
|
|
866
|
-
if (!token) throw new Error("AIO token missing in aio.json");
|
|
867
|
-
|
|
868
|
-
// Make API request to TCMS
|
|
869
|
-
const response = await axios.get(
|
|
870
|
-
`https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
|
|
871
|
-
{
|
|
872
|
-
headers: {
|
|
873
|
-
"accept": "application/json;charset=utf-8",
|
|
874
|
-
"Authorization": `AioAuth ${token}`
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
);
|
|
878
|
-
|
|
879
|
-
const testCases = response.data.items || [];
|
|
880
|
-
|
|
881
|
-
// Filter test cases by folder name
|
|
882
|
-
const filteredTestCases = testCases.filter(testCase =>
|
|
883
|
-
testCase.folder && testCase.folder.name === folderName
|
|
884
|
-
);
|
|
885
|
-
|
|
886
|
-
if (filteredTestCases.length === 0) {
|
|
887
|
-
return `No test cases found in folder: ${folderName}`;
|
|
888
|
-
}
|
|
889
|
-
|
|
890
|
-
// Extract key, title, and folder name
|
|
891
|
-
const extractedTestCases = filteredTestCases.map(testCase => ({
|
|
892
|
-
key: testCase.key,
|
|
893
|
-
title: testCase.title,
|
|
894
|
-
folderName: testCase.folder.name
|
|
895
|
-
}));
|
|
896
|
-
|
|
897
|
-
// Format as string response
|
|
898
|
-
const result = `✅ Found ${extractedTestCases.length} test cases in folder: ${folderName}\n\n` +
|
|
899
|
-
extractedTestCases.map(tc =>
|
|
900
|
-
`Key: ${tc.key}\nTitle: ${tc.title}\nFolder: ${tc.folderName}\n---`
|
|
901
|
-
).join('\n');
|
|
902
|
-
|
|
903
|
-
return result;
|
|
904
|
-
|
|
905
|
-
} catch (err) {
|
|
906
|
-
if (err.response) {
|
|
907
|
-
return `❌ TCMS API Error: ${err.response.status} - ${err.response.data?.message || err.response.statusText}`;
|
|
908
|
-
}
|
|
909
|
-
return `❌ Error fetching test cases: ${err.message}`;
|
|
910
|
-
}
|
|
911
|
-
}
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
tool(
|
|
915
|
-
"generate_testcases_from_ticket_data",
|
|
916
|
-
"Generate manual test cases by analyzing PNG design with JIRA requirements",
|
|
917
|
-
{
|
|
918
|
-
jiraSummary: zod_1.z.string().describe("Jira issue summary"),
|
|
919
|
-
jiraDescription: zod_1.z.string().describe("Jira issue description"),
|
|
920
|
-
existingTestCases: zod_1.z.string().optional().describe("Existing test cases from TCMS")
|
|
921
|
-
},
|
|
922
|
-
async ({ jiraSummary, jiraDescription, existingTestCases }) => {
|
|
923
|
-
try {
|
|
924
|
-
// Clear the generated test cases file before starting
|
|
925
|
-
const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
|
|
926
|
-
await fs.writeFile(testCasesFilePath, ''); // Clear the file
|
|
927
|
-
|
|
928
|
-
// Load OpenAI API key from Desktop/openai.json
|
|
929
|
-
const openaiConfigPath = path.join(os.homedir(), "Desktop", "openai.json");
|
|
930
|
-
const configContent = await fs.readFile(openaiConfigPath, "utf-8");
|
|
931
|
-
const { apiKey } = JSON.parse(configContent.trim());
|
|
932
|
-
|
|
933
|
-
// Load test case generation guidelines
|
|
934
|
-
const guidelinesPath = path.join(__dirname, 'testcases-generation-context.txt');
|
|
935
|
-
const guidelines = await fs.readFile(guidelinesPath, "utf-8");
|
|
936
|
-
|
|
937
|
-
const figmaDir = path.join(os.homedir(), "Desktop", "figma");
|
|
938
|
-
const files = await fs.readdir(figmaDir);
|
|
939
|
-
const pngFiles = files.filter(file => file.toLowerCase().endsWith('.png'));
|
|
940
|
-
|
|
941
|
-
if (pngFiles.length === 0) throw new Error("No PNG files found in figma folder");
|
|
942
|
-
|
|
943
|
-
// Get the latest PNG file
|
|
944
|
-
const latestPng = pngFiles.sort((a, b) => b.localeCompare(a))[0];
|
|
945
|
-
const pngPath = path.join(figmaDir, latestPng);
|
|
946
|
-
|
|
947
|
-
const client = new OpenAI({ apiKey });
|
|
948
|
-
|
|
949
|
-
// Convert PNG to base64 for vision API
|
|
950
|
-
const pngBuffer = await fs.readFile(pngPath);
|
|
951
|
-
const base64Image = pngBuffer.toString('base64');
|
|
952
|
-
|
|
953
|
-
// Start OpenAI generation (this will run in background due to timeout)
|
|
954
|
-
client.chat.completions.create({
|
|
955
|
-
model: "gpt-5", // Use GPT-5 model for image analysis
|
|
956
|
-
messages: [{
|
|
957
|
-
role: "user",
|
|
958
|
-
content: [
|
|
959
|
-
{
|
|
960
|
-
type: "text",
|
|
961
|
-
text: `Generate manual test cases based on the following:
|
|
962
|
-
|
|
963
|
-
JIRA Summary: ${jiraSummary}
|
|
964
|
-
|
|
965
|
-
JIRA Description: ${jiraDescription}
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
Please consider these existing test cases and generate additional comprehensive test cases that complement them.` : ''}
|
|
971
|
-
|
|
972
|
-
Test Case Generation Guidelines:
|
|
973
|
-
|
|
974
|
-
},
|
|
975
|
-
{
|
|
976
|
-
type: "image_url",
|
|
977
|
-
image_url: {
|
|
978
|
-
url: `data:image/png;base64,${base64Image}`,
|
|
979
|
-
detail: "high"
|
|
980
|
-
}
|
|
981
|
-
}
|
|
982
|
-
]
|
|
983
|
-
}],
|
|
984
|
-
max_completion_tokens: 10000
|
|
985
|
-
}).then(async (completion) => {
|
|
986
|
-
// Save test cases to file when generation completes
|
|
987
|
-
const testCases = completion.choices[0].message.content;
|
|
988
|
-
await fs.writeFile(testCasesFilePath, testCases);
|
|
989
|
-
}).catch(async (error) => {
|
|
990
|
-
// Save error to file if generation fails
|
|
991
|
-
await fs.writeFile(testCasesFilePath, `Error generating test cases: ${error.message}`);
|
|
992
|
-
});
|
|
993
|
-
|
|
994
|
-
return "✅ Test case generation started. Use 'check_testcases_status' tool to check if generation is complete. Try max 10 times";
|
|
995
|
-
} catch (err) {
|
|
996
|
-
return `❌ Error starting test case generation: ${err.message}`;
|
|
997
|
-
}
|
|
998
|
-
}
|
|
999
|
-
);
|
|
1000
|
-
|
|
1001
|
-
tool(
|
|
1002
|
-
"check_testcases_status",
|
|
1003
|
-
"Check if test cases have been generated and saved to file",
|
|
1004
|
-
{},
|
|
1005
|
-
async () => {
|
|
1006
|
-
try {
|
|
1007
|
-
// Wait for 20 seconds before checking
|
|
1008
|
-
await new Promise(resolve => setTimeout(resolve, 25000));
|
|
1009
|
-
|
|
1010
|
-
const testCasesFilePath = path.join(__dirname, 'generated-testcases.txt');
|
|
1011
|
-
|
|
1012
|
-
// Check if file exists and has content
|
|
1013
|
-
try {
|
|
1014
|
-
const fileContent = await fs.readFile(testCasesFilePath, 'utf-8');
|
|
1015
|
-
|
|
1016
|
-
if (fileContent.trim().length === 0) {
|
|
1017
|
-
return "❌ Test cases are still being generated. Please wait and try again.";
|
|
1018
|
-
}
|
|
1019
|
-
|
|
1020
|
-
if (fileContent.startsWith('Error generating test cases:')) {
|
|
1021
|
-
return `❌ ${fileContent}`;
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
return `✅ Test cases generated successfully!\n\n${fileContent}`;
|
|
1025
|
-
} catch (fileError) {
|
|
1026
|
-
return "❌ Test cases file not found or still being created. Please wait and try again.";
|
|
1027
|
-
}
|
|
1028
|
-
} catch (err) {
|
|
1029
|
-
return `❌ Error checking test cases status: ${err.message}`;
|
|
1030
|
-
}
|
|
1031
|
-
}
|
|
1032
|
-
);
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
tool(
|
|
1036
|
-
"review_testcases",
|
|
1037
|
-
"Open test cases in browser for manual approval.",
|
|
1038
|
-
{
|
|
1039
|
-
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")
|
|
1040
|
-
},
|
|
1041
|
-
async ({ testCases }) => {
|
|
1042
|
-
try {
|
|
1043
|
-
const app = express();
|
|
1044
|
-
let port = 3001;
|
|
1045
|
-
|
|
1046
|
-
// Find an available port
|
|
1047
|
-
const findAvailablePort = async (startPort) => {
|
|
1048
|
-
const net = require('net');
|
|
1049
|
-
return new Promise((resolve) => {
|
|
1050
|
-
const server = net.createServer();
|
|
1051
|
-
server.listen(startPort, () => {
|
|
1052
|
-
const port = server.address().port;
|
|
1053
|
-
server.close(() => resolve(port));
|
|
1054
|
-
});
|
|
1055
|
-
server.on('error', () => {
|
|
1056
|
-
resolve(findAvailablePort(startPort + 1));
|
|
1057
|
-
});
|
|
1058
|
-
});
|
|
1059
|
-
};
|
|
1060
|
-
|
|
1061
|
-
port = await findAvailablePort(port);
|
|
1062
|
-
|
|
1063
|
-
// Generate unique session ID
|
|
1064
|
-
const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
|
1065
|
-
|
|
1066
|
-
// Store approval status
|
|
1067
|
-
let approvalStatus = 'pending';
|
|
1068
|
-
let finalTestCases = [];
|
|
1069
|
-
|
|
1070
|
-
app.use(express.json({ limit: '10mb' }));
|
|
1071
|
-
app.use(express.urlencoded({ extended: true, limit: '10mb' }));
|
|
1072
|
-
|
|
1073
|
-
// Process test cases - handle the specific format properly
|
|
1074
|
-
const processedTestCases = testCases.map((testCase, index) => {
|
|
1075
|
-
if (Array.isArray(testCase)) {
|
|
1076
|
-
const arrayLength = testCase.length;
|
|
1077
|
-
|
|
1078
|
-
if (arrayLength === 4) {
|
|
1079
|
-
// Modify case: ["original title", "new description", "Modify", "SCRUM-TC-1"]
|
|
1080
|
-
return {
|
|
1081
|
-
originalTitle: testCase[0] || `Test Case ${index + 1}`,
|
|
1082
|
-
newDescription: testCase[1] || '',
|
|
1083
|
-
status: testCase[2] || 'Modify',
|
|
1084
|
-
testId: testCase[3] || '',
|
|
1085
|
-
index: index
|
|
1086
|
-
};
|
|
1087
|
-
} else if (arrayLength === 3) {
|
|
1088
|
-
// Remove case: ["title", "Remove", "SCRUM-TC-2"]
|
|
1089
|
-
return {
|
|
1090
|
-
title: testCase[0] || `Test Case ${index + 1}`,
|
|
1091
|
-
status: testCase[1] || 'Remove',
|
|
1092
|
-
testId: testCase[2] || '',
|
|
1093
|
-
index: index
|
|
1094
|
-
};
|
|
1095
|
-
} else if (arrayLength === 2) {
|
|
1096
|
-
// New case: ["title", "New"]
|
|
1097
|
-
return {
|
|
1098
|
-
title: testCase[0] || `Test Case ${index + 1}`,
|
|
1099
|
-
status: testCase[1] || 'New',
|
|
1100
|
-
index: index
|
|
1101
|
-
};
|
|
1102
|
-
} else {
|
|
1103
|
-
// Fallback for unexpected format
|
|
1104
|
-
return {
|
|
1105
|
-
title: testCase[0] || `Test Case ${index + 1}`,
|
|
1106
|
-
status: 'New',
|
|
1107
|
-
index: index
|
|
1108
|
-
};
|
|
1109
|
-
}
|
|
1110
|
-
} else {
|
|
1111
|
-
// Fallback for non-array format
|
|
1112
|
-
return {
|
|
1113
|
-
title: String(testCase) || `Test Case ${index + 1}`,
|
|
1114
|
-
status: 'New',
|
|
1115
|
-
index: index
|
|
1116
|
-
};
|
|
1117
|
-
}
|
|
1118
|
-
});
|
|
1119
|
-
|
|
1120
|
-
// Helper function to get display text for test cases
|
|
1121
|
-
const getTestCaseDisplayText = (testCase) => {
|
|
1122
|
-
const status = testCase.status.toLowerCase();
|
|
1123
|
-
|
|
1124
|
-
if (status === 'modify') {
|
|
1125
|
-
// For modify cases, show original → changed format
|
|
1126
|
-
return `Original: ${testCase.originalTitle}\nChanged to: ${testCase.newDescription}`;
|
|
1127
|
-
} else if (status === 'remove') {
|
|
1128
|
-
// For remove cases, show the title
|
|
1129
|
-
return testCase.title;
|
|
1130
|
-
} else {
|
|
1131
|
-
// For new cases, show the title
|
|
1132
|
-
return testCase.title;
|
|
1133
|
-
}
|
|
1134
|
-
};
|
|
1135
|
-
|
|
1136
|
-
// Main review page with proper handling
|
|
1137
|
-
app.get('/', (req, res) => {
|
|
1138
|
-
try {
|
|
1139
|
-
const htmlContent = `
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
<meta charset="UTF-8">
|
|
1144
|
-
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
1145
|
-
<title>Test Cases Review & Approval</title>
|
|
1146
|
-
<style>
|
|
1147
|
-
* {
|
|
1148
|
-
margin: 0;
|
|
1149
|
-
padding: 0;
|
|
1150
|
-
box-sizing: border-box;
|
|
1151
|
-
}
|
|
1152
|
-
|
|
1153
|
-
body {
|
|
1154
|
-
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
|
1155
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1156
|
-
min-height: 100vh;
|
|
1157
|
-
padding: 20px;
|
|
1158
|
-
}
|
|
1159
|
-
|
|
1160
|
-
.container {
|
|
1161
|
-
max-width: 1200px;
|
|
1162
|
-
margin: 0 auto;
|
|
1163
|
-
background: white;
|
|
1164
|
-
border-radius: 15px;
|
|
1165
|
-
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.1);
|
|
1166
|
-
overflow: hidden;
|
|
1167
|
-
}
|
|
1168
|
-
|
|
1169
|
-
.header {
|
|
1170
|
-
background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
|
|
1171
|
-
color: white;
|
|
1172
|
-
padding: 30px;
|
|
1173
|
-
text-align: center;
|
|
1174
|
-
}
|
|
1175
|
-
|
|
1176
|
-
.header h1 {
|
|
1177
|
-
font-size: 2.5rem;
|
|
1178
|
-
margin-bottom: 10px;
|
|
1179
|
-
font-weight: 700;
|
|
1180
|
-
}
|
|
1181
|
-
|
|
1182
|
-
.header p {
|
|
1183
|
-
font-size: 1.1rem;
|
|
1184
|
-
opacity: 0.9;
|
|
1185
|
-
}
|
|
1186
|
-
|
|
1187
|
-
.stats {
|
|
1188
|
-
display: flex;
|
|
1189
|
-
justify-content: space-around;
|
|
1190
|
-
background: #f8f9fa;
|
|
1191
|
-
padding: 20px;
|
|
1192
|
-
border-bottom: 1px solid #e9ecef;
|
|
1193
|
-
}
|
|
1194
|
-
|
|
1195
|
-
.stat-item {
|
|
1196
|
-
text-align: center;
|
|
1197
|
-
}
|
|
1198
|
-
|
|
1199
|
-
.stat-number {
|
|
1200
|
-
font-size: 2rem;
|
|
1201
|
-
font-weight: bold;
|
|
1202
|
-
color: #495057;
|
|
1203
|
-
}
|
|
1204
|
-
|
|
1205
|
-
.stat-label {
|
|
1206
|
-
color: #6c757d;
|
|
1207
|
-
font-size: 0.9rem;
|
|
1208
|
-
margin-top: 5px;
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
.controls {
|
|
1212
|
-
padding: 20px;
|
|
1213
|
-
background: #f8f9fa;
|
|
1214
|
-
display: flex;
|
|
1215
|
-
justify-content: space-between;
|
|
1216
|
-
align-items: center;
|
|
1217
|
-
flex-wrap: wrap;
|
|
1218
|
-
gap: 10px;
|
|
1219
|
-
}
|
|
1220
|
-
|
|
1221
|
-
.btn {
|
|
1222
|
-
padding: 12px 24px;
|
|
1223
|
-
border: none;
|
|
1224
|
-
border-radius: 8px;
|
|
1225
|
-
font-weight: 600;
|
|
1226
|
-
cursor: pointer;
|
|
1227
|
-
transition: all 0.3s ease;
|
|
1228
|
-
text-decoration: none;
|
|
1229
|
-
display: inline-block;
|
|
1230
|
-
font-size: 14px;
|
|
1231
|
-
}
|
|
1232
|
-
|
|
1233
|
-
.btn-primary {
|
|
1234
|
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
1235
|
-
color: white;
|
|
1236
|
-
}
|
|
1237
|
-
|
|
1238
|
-
.btn-primary:hover {
|
|
1239
|
-
transform: translateY(-2px);
|
|
1240
|
-
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
.btn-secondary {
|
|
1244
|
-
background: #6c757d;
|
|
1245
|
-
color: white;
|
|
1246
|
-
}
|
|
1247
|
-
|
|
1248
|
-
.btn-secondary:hover {
|
|
1249
|
-
background: #5a6268;
|
|
1250
|
-
}
|
|
1251
|
-
|
|
1252
|
-
.btn-delete {
|
|
1253
|
-
background: #dc3545;
|
|
1254
|
-
color: white;
|
|
1255
|
-
padding: 8px 16px;
|
|
1256
|
-
font-size: 12px;
|
|
1257
|
-
}
|
|
1258
|
-
|
|
1259
|
-
.btn-delete:hover {
|
|
1260
|
-
background: #c82333;
|
|
1261
|
-
}
|
|
1262
|
-
|
|
1263
|
-
.btn-restore {
|
|
1264
|
-
background: #28a745;
|
|
1265
|
-
color: white;
|
|
1266
|
-
padding: 8px 16px;
|
|
1267
|
-
font-size: 12px;
|
|
1268
|
-
}
|
|
1269
|
-
|
|
1270
|
-
.btn-restore:hover {
|
|
1271
|
-
background: #218838;
|
|
1272
|
-
}
|
|
1273
|
-
|
|
1274
|
-
.test-cases {
|
|
1275
|
-
max-height: 70vh;
|
|
1276
|
-
overflow-y: auto;
|
|
1277
|
-
}
|
|
1278
|
-
|
|
1279
|
-
.test-case {
|
|
1280
|
-
border-bottom: 1px solid #e9ecef;
|
|
1281
|
-
padding: 20px;
|
|
1282
|
-
transition: all 0.3s ease;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
.test-case:hover {
|
|
1286
|
-
background: #f8f9fa;
|
|
1287
|
-
}
|
|
1288
|
-
|
|
1289
|
-
.test-case.deleted {
|
|
1290
|
-
opacity: 0.5;
|
|
1291
|
-
background: #f8d7da;
|
|
1292
|
-
}
|
|
1293
|
-
|
|
1294
|
-
.test-case-header {
|
|
1295
|
-
display: flex;
|
|
1296
|
-
justify-content: space-between;
|
|
1297
|
-
align-items: center;
|
|
1298
|
-
margin-bottom: 15px;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
.test-case-meta {
|
|
1302
|
-
display: flex;
|
|
1303
|
-
gap: 15px;
|
|
1304
|
-
align-items: center;
|
|
1305
|
-
}
|
|
1306
|
-
|
|
1307
|
-
.test-case-index {
|
|
1308
|
-
background: #007bff;
|
|
1309
|
-
color: white;
|
|
1310
|
-
padding: 4px 8px;
|
|
1311
|
-
border-radius: 4px;
|
|
1312
|
-
font-size: 12px;
|
|
1313
|
-
font-weight: bold;
|
|
1314
|
-
}
|
|
1315
|
-
|
|
1316
|
-
.test-case-status {
|
|
1317
|
-
padding: 4px 12px;
|
|
1318
|
-
border-radius: 12px;
|
|
1319
|
-
font-size: 12px;
|
|
1320
|
-
font-weight: 600;
|
|
1321
|
-
}
|
|
1322
|
-
|
|
1323
|
-
.status-new {
|
|
1324
|
-
background: #d4edda;
|
|
1325
|
-
color: #155724;
|
|
1326
|
-
}
|
|
1327
|
-
|
|
1328
|
-
.status-modify {
|
|
1329
|
-
background: #fff3cd;
|
|
1330
|
-
color: #856404;
|
|
1331
|
-
}
|
|
1332
|
-
|
|
1333
|
-
.status-remove {
|
|
1334
|
-
background: #f8d7da;
|
|
1335
|
-
color: #721c24;
|
|
1336
|
-
}
|
|
1337
|
-
|
|
1338
|
-
.test-case textarea {
|
|
1339
|
-
width: 100%;
|
|
1340
|
-
min-height: 100px;
|
|
1341
|
-
padding: 15px;
|
|
1342
|
-
border: 2px solid #e9ecef;
|
|
1343
|
-
border-radius: 8px;
|
|
1344
|
-
font-family: inherit;
|
|
1345
|
-
font-size: 14px;
|
|
1346
|
-
line-height: 1.5;
|
|
1347
|
-
resize: vertical;
|
|
1348
|
-
transition: border-color 0.3s ease;
|
|
1349
|
-
}
|
|
1350
|
-
|
|
1351
|
-
.test-case textarea:focus {
|
|
1352
|
-
outline: none;
|
|
1353
|
-
border-color: #007bff;
|
|
1354
|
-
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
.notification {
|
|
1358
|
-
position: fixed;
|
|
1359
|
-
top: 20px;
|
|
1360
|
-
right: 20px;
|
|
1361
|
-
padding: 15px 25px;
|
|
1362
|
-
border-radius: 8px;
|
|
1363
|
-
color: white;
|
|
1364
|
-
font-weight: 600;
|
|
1365
|
-
display: none;
|
|
1366
|
-
z-index: 1000;
|
|
1367
|
-
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
|
-
@media (max-width: 768px) {
|
|
1371
|
-
.container {
|
|
1372
|
-
margin: 10px;
|
|
1373
|
-
border-radius: 10px;
|
|
1374
|
-
}
|
|
1375
|
-
|
|
1376
|
-
.header h1 {
|
|
1377
|
-
font-size: 2rem;
|
|
1378
|
-
}
|
|
1379
|
-
|
|
1380
|
-
.stats {
|
|
1381
|
-
flex-direction: column;
|
|
1382
|
-
gap: 15px;
|
|
1383
|
-
}
|
|
1384
|
-
|
|
1385
|
-
.controls {
|
|
1386
|
-
flex-direction: column;
|
|
1387
|
-
gap: 15px;
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
.test-case-header {
|
|
1391
|
-
flex-direction: column;
|
|
1392
|
-
align-items: flex-start;
|
|
1393
|
-
gap: 10px;
|
|
1394
|
-
}
|
|
1395
|
-
}
|
|
1396
|
-
</style>
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
<div class="notification" id="notification"></div>
|
|
1400
|
-
|
|
1401
|
-
<div class="container">
|
|
1402
|
-
<div class="header">
|
|
1403
|
-
<h1>🔍 Test Cases Review & Approval</h1>
|
|
1404
|
-
<p>Review, edit, and approve your test cases. Make any necessary changes before final approval.</p>
|
|
1405
|
-
</div>
|
|
1406
|
-
|
|
1407
|
-
<div class="stats">
|
|
1408
|
-
<div class="stat-item">
|
|
1409
|
-
<div class="stat-number" id="totalCount">${processedTestCases.length}</div>
|
|
1410
|
-
<div class="stat-label">Total Cases</div>
|
|
1411
|
-
</div>
|
|
1412
|
-
<div class="stat-item">
|
|
1413
|
-
<div class="stat-number" id="activeCount">${processedTestCases.length}</div>
|
|
1414
|
-
<div class="stat-label">Active Cases</div>
|
|
1415
|
-
</div>
|
|
1416
|
-
<div class="stat-item">
|
|
1417
|
-
<div class="stat-number" id="deletedCount">0</div>
|
|
1418
|
-
<div class="stat-label">Deleted Cases</div>
|
|
1419
|
-
</div>
|
|
1420
|
-
</div>
|
|
1421
|
-
|
|
1422
|
-
<div class="controls">
|
|
1423
|
-
<div>
|
|
1424
|
-
<button class="btn btn-secondary" onclick="resetAll()">🔄 Reset All</button>
|
|
1425
|
-
</div>
|
|
1426
|
-
<div>
|
|
1427
|
-
<button class="btn btn-primary" onclick="approveTestCases()">✅ Approve Test Cases</button>
|
|
1428
|
-
</div>
|
|
1429
|
-
</div>
|
|
1430
|
-
|
|
1431
|
-
<div class="test-cases">
|
|
1432
|
-
${processedTestCases.map((testCase, index) => {
|
|
1433
|
-
const displayText = getTestCaseDisplayText(testCase);
|
|
1434
|
-
const statusLabel = testCase.status === 'Remove' ? 'Remove' : testCase.status;
|
|
1435
|
-
|
|
1436
|
-
// Create proper label and test ID display for modify/remove cases
|
|
1437
|
-
let labelAndIdDisplay = '';
|
|
1438
|
-
if (testCase.status.toLowerCase() === 'modify' && testCase.testId) {
|
|
1439
|
-
labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #856404; font-size: 13px;">Modify - ${testCase.testId}</div>`;
|
|
1440
|
-
} else if (testCase.status.toLowerCase() === 'remove' && testCase.testId) {
|
|
1441
|
-
labelAndIdDisplay = `<div style="margin-bottom: 8px; font-weight: 600; color: #721c24; font-size: 13px;">Remove - ${testCase.testId}</div>`;
|
|
1442
|
-
}
|
|
1443
|
-
|
|
1444
|
-
return `
|
|
1445
|
-
<div class="test-case" data-index="${index}">
|
|
1446
|
-
<div class="test-case-header">
|
|
1447
|
-
<div class="test-case-meta">
|
|
1448
|
-
<span class="test-case-index">#${index + 1}</span>
|
|
1449
|
-
<span class="test-case-status status-${testCase.status.toLowerCase()}">${statusLabel}</span>
|
|
1450
|
-
</div>
|
|
1451
|
-
<button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
|
|
1452
|
-
</div>
|
|
1453
|
-
${labelAndIdDisplay}
|
|
1454
|
-
<textarea data-index="${index}" placeholder="Enter test case details...">${displayText}</textarea>
|
|
1455
|
-
</div>
|
|
1456
|
-
`;
|
|
1457
|
-
}).join('')}
|
|
1458
|
-
</div>
|
|
1459
|
-
</div>
|
|
1460
|
-
|
|
1461
|
-
<script>
|
|
1462
|
-
let testCases = ${JSON.stringify(processedTestCases).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')};
|
|
1463
|
-
let deletedIndices = new Set();
|
|
1464
|
-
|
|
1465
|
-
function updateStats() {
|
|
1466
|
-
document.getElementById('activeCount').textContent = testCases.length - deletedIndices.size;
|
|
1467
|
-
document.getElementById('deletedCount').textContent = deletedIndices.size;
|
|
1468
|
-
}
|
|
1469
|
-
|
|
1470
|
-
function toggleDelete(index) {
|
|
1471
|
-
const testCaseEl = document.querySelector(\`[data-index="\${index}"]\`);
|
|
1472
|
-
const btn = testCaseEl.querySelector('.btn-delete, .btn-restore');
|
|
1473
|
-
|
|
1474
|
-
if (deletedIndices.has(index)) {
|
|
1475
|
-
// Restore
|
|
1476
|
-
deletedIndices.delete(index);
|
|
1477
|
-
testCaseEl.classList.remove('deleted');
|
|
1478
|
-
btn.textContent = 'Delete';
|
|
1479
|
-
btn.className = 'btn btn-delete';
|
|
1480
|
-
} else {
|
|
1481
|
-
// Delete
|
|
1482
|
-
deletedIndices.add(index);
|
|
1483
|
-
testCaseEl.classList.add('deleted');
|
|
1484
|
-
btn.textContent = 'Restore';
|
|
1485
|
-
btn.className = 'btn btn-restore';
|
|
1486
|
-
}
|
|
1487
|
-
updateStats();
|
|
1488
|
-
}
|
|
1489
|
-
|
|
1490
|
-
function resetAll() {
|
|
1491
|
-
deletedIndices.clear();
|
|
1492
|
-
document.querySelectorAll('.test-case').forEach((el, index) => {
|
|
1493
|
-
el.classList.remove('deleted');
|
|
1494
|
-
const btn = el.querySelector('.btn-delete, .btn-restore');
|
|
1495
|
-
btn.textContent = 'Delete';
|
|
1496
|
-
btn.className = 'btn btn-delete';
|
|
1497
|
-
|
|
1498
|
-
// Reset textarea value
|
|
1499
|
-
const textarea = el.querySelector('textarea');
|
|
1500
|
-
const originalTestCase = testCases[index];
|
|
1501
|
-
|
|
1502
|
-
// Use proper display format based on status
|
|
1503
|
-
let resetValue = '';
|
|
1504
|
-
const status = originalTestCase.status.toLowerCase();
|
|
1505
|
-
|
|
1506
|
-
if (status === 'modify') {
|
|
1507
|
-
resetValue = 'Original: ' + originalTestCase.originalTitle + '\\nChanged to: ' + originalTestCase.newDescription;
|
|
1508
|
-
} else if (status === 'remove') {
|
|
1509
|
-
resetValue = originalTestCase.title;
|
|
1510
|
-
} else {
|
|
1511
|
-
resetValue = originalTestCase.title;
|
|
1512
|
-
}
|
|
1513
|
-
|
|
1514
|
-
textarea.value = resetValue;
|
|
1515
|
-
});
|
|
1516
|
-
updateStats();
|
|
1517
|
-
}
|
|
1518
|
-
|
|
1519
|
-
function showNotification(message, type = 'success') {
|
|
1520
|
-
const notification = document.getElementById('notification');
|
|
1521
|
-
notification.textContent = message;
|
|
1522
|
-
notification.style.background = type === 'success' ? '#28a745' : '#dc3545';
|
|
1523
|
-
notification.style.display = 'block';
|
|
1524
|
-
|
|
1525
|
-
setTimeout(() => {
|
|
1526
|
-
notification.style.display = 'none';
|
|
1527
|
-
}, 3000);
|
|
1528
|
-
}
|
|
1529
|
-
|
|
1530
|
-
function approveTestCases() {
|
|
1531
|
-
try {
|
|
1532
|
-
// Collect updated test cases
|
|
1533
|
-
const updatedTestCases = [];
|
|
1534
|
-
document.querySelectorAll('.test-case').forEach((el, index) => {
|
|
1535
|
-
if (!deletedIndices.has(index)) {
|
|
1536
|
-
const textarea = el.querySelector('textarea');
|
|
1537
|
-
const originalTestCase = testCases[index];
|
|
1538
|
-
|
|
1539
|
-
// Create the updated test case array based on the original format
|
|
1540
|
-
let updatedCase;
|
|
1541
|
-
const status = originalTestCase.status.toLowerCase();
|
|
1542
|
-
|
|
1543
|
-
if (status === 'modify') {
|
|
1544
|
-
// For modify: [updatedContent, newDescription, "Modify", testId]
|
|
1545
|
-
updatedCase = [
|
|
1546
|
-
textarea.value.trim(),
|
|
1547
|
-
originalTestCase.newDescription,
|
|
1548
|
-
originalTestCase.status,
|
|
1549
|
-
originalTestCase.testId
|
|
1550
|
-
];
|
|
1551
|
-
} else if (status === 'remove') {
|
|
1552
|
-
// For remove: [updatedContent, "Remove", testId]
|
|
1553
|
-
updatedCase = [
|
|
1554
|
-
textarea.value.trim(),
|
|
1555
|
-
originalTestCase.status,
|
|
1556
|
-
originalTestCase.testId
|
|
1557
|
-
];
|
|
1558
|
-
} else {
|
|
1559
|
-
// For new: [updatedContent, "New"]
|
|
1560
|
-
updatedCase = [
|
|
1561
|
-
textarea.value.trim(),
|
|
1562
|
-
originalTestCase.status
|
|
1563
|
-
];
|
|
1564
|
-
}
|
|
1565
|
-
|
|
1566
|
-
updatedTestCases.push(updatedCase);
|
|
1567
|
-
}
|
|
1568
|
-
});
|
|
1569
|
-
|
|
1570
|
-
// Send approval to server
|
|
1571
|
-
fetch('/approve', {
|
|
1572
|
-
method: 'POST',
|
|
1573
|
-
headers: {
|
|
1574
|
-
'Content-Type': 'application/json',
|
|
1575
|
-
},
|
|
1576
|
-
body: JSON.stringify({
|
|
1577
|
-
sessionId: '${sessionId}',
|
|
1578
|
-
testCases: updatedTestCases
|
|
1579
|
-
})
|
|
1580
|
-
})
|
|
1581
|
-
.then(response => response.json())
|
|
1582
|
-
.then(data => {
|
|
1583
|
-
if (data.success) {
|
|
1584
|
-
showNotification('Test cases approved successfully!');
|
|
1585
|
-
setTimeout(() => {
|
|
1586
|
-
window.close();
|
|
1587
|
-
}, 2000);
|
|
1588
|
-
} else {
|
|
1589
|
-
showNotification('Error approving test cases', 'error');
|
|
1590
|
-
}
|
|
1591
|
-
})
|
|
1592
|
-
.catch(error => {
|
|
1593
|
-
showNotification('Error approving test cases', 'error');
|
|
1594
|
-
console.error('Error:', error);
|
|
1595
|
-
});
|
|
1596
|
-
} catch (error) {
|
|
1597
|
-
showNotification('Error processing test cases', 'error');
|
|
1598
|
-
console.error('Error:', error);
|
|
1599
|
-
}
|
|
1600
|
-
}
|
|
1601
|
-
|
|
1602
|
-
// Update test cases when textarea changes
|
|
1603
|
-
document.addEventListener('input', function(e) {
|
|
1604
|
-
if (e.target.tagName === 'TEXTAREA') {
|
|
1605
|
-
const index = parseInt(e.target.getAttribute('data-index'));
|
|
1606
|
-
if (!isNaN(index) && testCases[index]) {
|
|
1607
|
-
// Update the title with the textarea content
|
|
1608
|
-
testCases[index].title = e.target.value.trim();
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
});
|
|
1612
|
-
</script>
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
`;
|
|
1616
|
-
res.send(htmlContent);
|
|
1617
|
-
} catch (error) {
|
|
1618
|
-
console.error('Error rendering page:', error);
|
|
1619
|
-
res.status(500).send('Error rendering page');
|
|
1620
|
-
}
|
|
1621
|
-
});
|
|
1622
|
-
|
|
1623
|
-
// Approval endpoint with better error handling
|
|
1624
|
-
app.post('/approve', (req, res) => {
|
|
1625
|
-
try {
|
|
1626
|
-
const { testCases: approvedTestCases, sessionId: receivedSessionId } = req.body;
|
|
1627
|
-
|
|
1628
|
-
if (receivedSessionId !== sessionId) {
|
|
1629
|
-
return res.status(400).json({ success: false, message: 'Invalid session ID' });
|
|
1630
|
-
}
|
|
1631
|
-
|
|
1632
|
-
finalTestCases = approvedTestCases;
|
|
1633
|
-
approvalStatus = 'approved';
|
|
1634
|
-
|
|
1635
|
-
// Save to global state for the check tool
|
|
1636
|
-
global.approvalSessions = global.approvalSessions || {};
|
|
1637
|
-
global.approvalSessions[sessionId] = {
|
|
1638
|
-
status: 'approved',
|
|
1639
|
-
testCases: finalTestCases,
|
|
1640
|
-
timestamp: Date.now()
|
|
1641
|
-
};
|
|
1642
|
-
|
|
1643
|
-
res.json({ success: true, message: 'Test cases approved successfully' });
|
|
1644
|
-
|
|
1645
|
-
// Close server after approval
|
|
1646
|
-
setTimeout(() => {
|
|
1647
|
-
if (server && server.listening) {
|
|
1648
|
-
server.close();
|
|
1649
|
-
}
|
|
1650
|
-
}, 3000);
|
|
1651
|
-
} catch (error) {
|
|
1652
|
-
console.error('Approval error:', error);
|
|
1653
|
-
res.status(500).json({ success: false, message: error.message });
|
|
1654
|
-
}
|
|
1655
|
-
});
|
|
1656
|
-
|
|
1657
|
-
// Error handling middleware
|
|
1658
|
-
app.use((err, req, res, next) => {
|
|
1659
|
-
console.error('Express error:', err);
|
|
1660
|
-
res.status(500).json({ error: 'Internal server error' });
|
|
1661
|
-
});
|
|
1662
|
-
|
|
1663
|
-
// 404 handler
|
|
1664
|
-
app.use((req, res) => {
|
|
1665
|
-
res.status(404).json({ error: 'Not found' });
|
|
1666
|
-
});
|
|
1667
|
-
|
|
1668
|
-
// Start server with promise-based approach
|
|
1669
|
-
const server = await new Promise((resolve, reject) => {
|
|
1670
|
-
const srv = app.listen(port, (err) => {
|
|
1671
|
-
if (err) {
|
|
1672
|
-
reject(err);
|
|
1673
|
-
return;
|
|
1674
|
-
}
|
|
1675
|
-
console.log(`✅ Test case review session started. Session ID: ${sessionId}.`);
|
|
1676
|
-
console.log(`Server running at http://localhost:${port}`);
|
|
1677
|
-
console.log(`Browser should open automatically.`);
|
|
1678
|
-
resolve(srv);
|
|
1679
|
-
});
|
|
1680
|
-
|
|
1681
|
-
srv.on('error', (error) => {
|
|
1682
|
-
reject(error);
|
|
1683
|
-
});
|
|
1684
|
-
});
|
|
1685
|
-
|
|
1686
|
-
// Open browser with proper error handling
|
|
1687
|
-
let openAttemptFailed = false;
|
|
1688
|
-
try {
|
|
1689
|
-
await openBrowser(`http://localhost:${port}`);
|
|
1690
|
-
} catch (err) {
|
|
1691
|
-
openAttemptFailed = true;
|
|
1692
|
-
console.error('Failed to open browser automatically:', err.message);
|
|
1693
|
-
// Continue without opening browser - user can manually navigate to the URL
|
|
1694
|
-
}
|
|
1695
|
-
|
|
1696
|
-
// Store session globally for status checking
|
|
1697
|
-
global.approvalSessions = global.approvalSessions || {};
|
|
1698
|
-
global.approvalSessions[sessionId] = {
|
|
1699
|
-
status: 'pending',
|
|
1700
|
-
testCases: processedTestCases,
|
|
1701
|
-
timestamp: Date.now(),
|
|
1702
|
-
server: server
|
|
1703
|
-
};
|
|
1704
|
-
|
|
1705
|
-
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.'}`;
|
|
1706
|
-
|
|
1707
|
-
} catch (err) {
|
|
1708
|
-
console.error('Review tool error:', err);
|
|
1709
|
-
return `❌ Error starting test case review: ${err.message}`;
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
);
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
tool(
|
|
1716
|
-
"check_approval_status",
|
|
1717
|
-
"Check the approval status of test cases review session (waits 25 seconds before checking)",
|
|
1718
|
-
{
|
|
1719
|
-
sessionId: zod_1.z.string().describe("Session ID from review_testcases")
|
|
1720
|
-
},
|
|
1721
|
-
async ({ sessionId }) => {
|
|
1722
|
-
try {
|
|
1723
|
-
// Wait for 25 seconds
|
|
1724
|
-
await new Promise(resolve => setTimeout(resolve, 25000));
|
|
1725
|
-
|
|
1726
|
-
// Check global approval sessions
|
|
1727
|
-
if (!global.approvalSessions || !global.approvalSessions[sessionId]) {
|
|
1728
|
-
return "❌ Session not found. Please ensure the review session is still active.";
|
|
1729
|
-
}
|
|
1730
|
-
|
|
1731
|
-
const session = global.approvalSessions[sessionId];
|
|
1732
|
-
|
|
1733
|
-
if (session.status === 'approved') {
|
|
1734
|
-
const result = {
|
|
1735
|
-
status: 'approved',
|
|
1736
|
-
testCases: session.testCases,
|
|
1737
|
-
approvedCount: session.testCases.length,
|
|
1738
|
-
sessionId: sessionId
|
|
1739
|
-
};
|
|
1740
|
-
|
|
1741
|
-
// Format the approved test cases properly
|
|
1742
|
-
const formattedTestCases = result.testCases.map((tc, index) => {
|
|
1743
|
-
if (!Array.isArray(tc)) {
|
|
1744
|
-
return `${index + 1}. ${String(tc)} (New)`;
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
// Handle different array structures based on length and content
|
|
1748
|
-
let title, description, status, originalCase;
|
|
1749
|
-
|
|
1750
|
-
if (tc.length === 4) {
|
|
1751
|
-
// Standard format: [title, description, status, originalCase]
|
|
1752
|
-
title = tc[0] || `Test Case ${index + 1}`;
|
|
1753
|
-
description = tc[1] || '';
|
|
1754
|
-
status = tc[2] || 'New';
|
|
1755
|
-
originalCase = tc[3] || '';
|
|
1756
|
-
} else if (tc.length === 3) {
|
|
1757
|
-
// Could be [title, status, originalCase] for remove cases
|
|
1758
|
-
title = tc[0] || `Test Case ${index + 1}`;
|
|
1759
|
-
if (tc[1] && tc[1].toLowerCase() === 'remove') {
|
|
1760
|
-
status = tc[1];
|
|
1761
|
-
originalCase = tc[2] || '';
|
|
1762
|
-
description = '';
|
|
1763
|
-
} else {
|
|
1764
|
-
// [title, description, status]
|
|
1765
|
-
description = tc[1] || '';
|
|
1766
|
-
status = tc[2] || 'New';
|
|
1767
|
-
originalCase = '';
|
|
1768
|
-
}
|
|
1769
|
-
} else {
|
|
1770
|
-
// Fallback
|
|
1771
|
-
title = tc[0] || `Test Case ${index + 1}`;
|
|
1772
|
-
description = tc[1] || '';
|
|
1773
|
-
status = tc[2] || 'New';
|
|
1774
|
-
originalCase = tc[3] || '';
|
|
1775
|
-
}
|
|
1776
|
-
|
|
1777
|
-
const statusLower = status.toLowerCase();
|
|
1778
|
-
|
|
1779
|
-
if (statusLower === 'modify') {
|
|
1780
|
-
// For modify cases: show "Original: ... Changed to: ..." format with proper test ID
|
|
1781
|
-
return `${index + 1}. Original: ${title}\n Changed to: ${description} (Modify) (${originalCase})`;
|
|
1782
|
-
} else if (statusLower === 'remove') {
|
|
1783
|
-
// For remove cases: show title with Remove label and reference
|
|
1784
|
-
return `${index + 1}. ${title} (Remove) (${originalCase})`;
|
|
1785
|
-
} else {
|
|
1786
|
-
// For new cases: just show title with New label
|
|
1787
|
-
return `${index + 1}. ${title} (New)`;
|
|
1788
|
-
}
|
|
1789
|
-
}).join('\n');
|
|
1790
|
-
|
|
1791
|
-
// Clean up session after returning result
|
|
1792
|
-
delete global.approvalSessions[sessionId];
|
|
1793
|
-
|
|
1794
|
-
return `✅ Test cases approved successfully!\n\nApproved ${result.approvedCount} test cases:\n\n${formattedTestCases}\n\nSession completed: ${sessionId}`;
|
|
1795
|
-
} else {
|
|
1796
|
-
return "⏳ Still waiting for approval. The review session is active but not yet approved. Please complete the review in the browser.";
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
} catch (err) {
|
|
1800
|
-
console.error('Check approval status error:', err);
|
|
1801
|
-
return `❌ Error checking approval status: ${err.message}`;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
);
|
|
1805
|
-
|
|
1806
|
-
tool(
|
|
1807
|
-
"update_testcases_to_tcms",
|
|
1808
|
-
"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.",
|
|
1809
|
-
{
|
|
1810
|
-
testCases: zod_1.z.array(zod_1.z.array(zod_1.z.string())).describe("Array of test case arrays from approved test cases")
|
|
1811
|
-
},
|
|
1812
|
-
async ({ testCases }) => {
|
|
1813
|
-
try {
|
|
1814
|
-
// Load AIO token from Desktop/aio.json
|
|
1815
|
-
const aioConfigPath = path.join(os.homedir(), "Desktop", "aio.json");
|
|
1816
|
-
const configContent = await fs.readFile(aioConfigPath, "utf-8");
|
|
1817
|
-
const { token } = JSON.parse(configContent);
|
|
1818
|
-
|
|
1819
|
-
if (!token) throw new Error("AIO token missing in aio.json");
|
|
1820
|
-
|
|
1821
|
-
// Filter test cases to extract only "New" test cases
|
|
1822
|
-
const newTestCases = [];
|
|
1823
|
-
|
|
1824
|
-
for (const testCase of testCases) {
|
|
1825
|
-
if (Array.isArray(testCase) && testCase.length >= 2) {
|
|
1826
|
-
// Check if the last element or second-to-last element is "New"
|
|
1827
|
-
const status = testCase.length === 2 ? testCase[1] : testCase[testCase.length - 2];
|
|
1828
|
-
|
|
1829
|
-
if (status && status.toLowerCase() === 'new') {
|
|
1830
|
-
const title = testCase[0]; // First element is always the title
|
|
1831
|
-
if (title && title.trim().length > 0) {
|
|
1832
|
-
newTestCases.push(title.trim());
|
|
1833
|
-
}
|
|
1834
|
-
}
|
|
1835
|
-
}
|
|
1836
|
-
}
|
|
1837
|
-
|
|
1838
|
-
if (newTestCases.length === 0) {
|
|
1839
|
-
return "No new test cases found to create in TCMS. Only test cases marked as '(New)' are processed.";
|
|
1840
|
-
}
|
|
1841
|
-
|
|
1842
|
-
// Hard-coded values as requested
|
|
1843
|
-
const projectKey = "SCRUM";
|
|
1844
|
-
const folderId = 1;
|
|
1845
|
-
const ownerId = "712020:37085ff2-5a05-47eb-8977-50a485355755";
|
|
1846
|
-
|
|
1847
|
-
// Create test cases in TCMS one by one
|
|
1848
|
-
for (let i = 0; i < newTestCases.length; i++) {
|
|
1849
|
-
const title = newTestCases[i];
|
|
1850
|
-
|
|
1851
|
-
try {
|
|
1852
|
-
const requestBody = {
|
|
1853
|
-
title: title,
|
|
1854
|
-
ownedByID: ownerId,
|
|
1855
|
-
folder: {
|
|
1856
|
-
ID: folderId
|
|
1857
|
-
},
|
|
1858
|
-
status: {
|
|
1859
|
-
name: "Published",
|
|
1860
|
-
description: "The test is ready for execution",
|
|
1861
|
-
ID: 1
|
|
1862
|
-
}
|
|
1863
|
-
};
|
|
1864
|
-
|
|
1865
|
-
(0, logger_1.trace)(`Creating test case ${i + 1}/${newTestCases.length}: ${title}`);
|
|
1866
|
-
|
|
1867
|
-
const response = await axios.post(
|
|
1868
|
-
`https://tcms.aiojiraapps.com/aio-tcms/api/v1/project/${projectKey}/testcase`,
|
|
1869
|
-
requestBody,
|
|
1870
|
-
{
|
|
1871
|
-
headers: {
|
|
1872
|
-
"accept": "application/json;charset=utf-8",
|
|
1873
|
-
"Authorization": `AioAuth ${token}`,
|
|
1874
|
-
"Content-Type": "application/json"
|
|
1875
|
-
}
|
|
1876
|
-
}
|
|
1877
|
-
);
|
|
1878
|
-
|
|
1879
|
-
if (response.status === 200 || response.status === 201) {
|
|
1880
|
-
const testCaseKey = response.data.key || `${projectKey}-TC-${response.data.ID}`;
|
|
1881
|
-
(0, logger_1.trace)(`Successfully created test case: ${testCaseKey} - ${title}`);
|
|
1882
|
-
}
|
|
1883
|
-
|
|
1884
|
-
// Add a small delay between requests to avoid rate limiting
|
|
1885
|
-
await new Promise(resolve => setTimeout(resolve, 500));
|
|
1886
|
-
|
|
1887
|
-
} catch (error) {
|
|
1888
|
-
(0, logger_1.trace)(`Failed to create test case: ${title} - ${error.message}`);
|
|
1889
|
-
throw new Error(`Failed to create test case "${title}": ${error.message}`);
|
|
1890
|
-
}
|
|
1891
|
-
}
|
|
1892
|
-
|
|
1893
|
-
return "All test cases have been updated to TCMS";
|
|
1894
|
-
|
|
1895
|
-
} catch (error) {
|
|
1896
|
-
console.error('TCMS update error:', error);
|
|
1897
|
-
if (error.response) {
|
|
1898
|
-
return `❌ TCMS API Error: ${error.response.status} - ${error.response.data?.message || error.response.statusText}`;
|
|
1899
|
-
}
|
|
1900
|
-
return `❌ Error updating test cases to TCMS: ${error.message}`;
|
|
1901
|
-
}
|
|
1902
|
-
}
|
|
1903
|
-
);
|
|
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
|
+
//);
|
|
1904
1929
|
|
|
1905
1930
|
return server;
|
|
1906
1931
|
};
|