@nbakka/mcp-appium 3.0.4 → 3.0.6

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.
Files changed (2) hide show
  1. package/lib/server.js +258 -212
  2. package/package.json +1 -1
package/lib/server.js CHANGED
@@ -806,33 +806,62 @@ tool(
806
806
  async ({ testCases }) => {
807
807
  try {
808
808
  const app = express();
809
- const port = 3001;
809
+ let port = 3001;
810
+
811
+ // Find an available port
812
+ const findAvailablePort = async (startPort) => {
813
+ const net = require('net');
814
+ return new Promise((resolve) => {
815
+ const server = net.createServer();
816
+ server.listen(startPort, () => {
817
+ const port = server.address().port;
818
+ server.close(() => resolve(port));
819
+ });
820
+ server.on('error', () => {
821
+ resolve(findAvailablePort(startPort + 1));
822
+ });
823
+ });
824
+ };
825
+
826
+ port = await findAvailablePort(port);
810
827
 
811
828
  // Generate unique session ID
812
829
  const sessionId = `session_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
813
830
 
814
831
  // Store approval status
815
832
  let approvalStatus = 'pending';
816
- let finalTestCases = JSON.parse(JSON.stringify(testCases)); // Deep copy
833
+ let finalTestCases = [];
817
834
 
818
- app.use(express.json({ limit: '10mb' })); // Increase JSON limit
835
+ app.use(express.json({ limit: '10mb' }));
819
836
  app.use(express.urlencoded({ extended: true, limit: '10mb' }));
820
837
 
821
- // Main review page with fixed JSON handling
822
- app.get('/', (req, res) => {
823
- // Safely stringify testCases to avoid JSON issues
824
- const safeTestCases = testCases.map((testCase, index) => {
825
- // Ensure each test case is an array with at least 3 elements
826
- const safeCase = Array.isArray(testCase) ? testCase : [testCase];
827
- return [
828
- String(safeCase[0] || `Test Case ${index + 1}`), // Title
829
- String(safeCase[1] || ''), // Description/Steps
830
- String(safeCase[2] || 'New'), // Status
831
- String(safeCase[3] || '') // Original case (for modifications)
832
- ];
833
- });
838
+ // Process test cases - handle the specific format properly
839
+ const processedTestCases = testCases.map((testCase, index) => {
840
+ // Each testCase is an array like ["title", "description", "status", "originalCase"]
841
+ if (Array.isArray(testCase)) {
842
+ return {
843
+ title: testCase[0] || `Test Case ${index + 1}`,
844
+ description: testCase[1] || '',
845
+ status: testCase[2] || 'New',
846
+ originalCase: testCase[3] || '',
847
+ index: index
848
+ };
849
+ } else {
850
+ // Fallback for unexpected format
851
+ return {
852
+ title: String(testCase) || `Test Case ${index + 1}`,
853
+ description: '',
854
+ status: 'New',
855
+ originalCase: '',
856
+ index: index
857
+ };
858
+ }
859
+ });
834
860
 
835
- res.send(`
861
+ // Main review page with proper handling
862
+ app.get('/', (req, res) => {
863
+ try {
864
+ const htmlContent = `
836
865
  <!DOCTYPE html>
837
866
  <html lang="en">
838
867
  <head>
@@ -900,179 +929,154 @@ tool(
900
929
 
901
930
  .stat-label {
902
931
  color: #6c757d;
932
+ font-size: 0.9rem;
903
933
  margin-top: 5px;
904
934
  }
905
935
 
906
- .content {
907
- padding: 30px;
908
- max-height: 70vh;
909
- overflow-y: auto;
910
- }
911
-
912
- .test-case {
913
- background: #fff;
914
- border: 2px solid #e9ecef;
915
- border-radius: 12px;
916
- margin-bottom: 20px;
917
- overflow: hidden;
918
- transition: all 0.3s ease;
919
- position: relative;
920
- }
921
-
922
- .test-case:hover {
923
- border-color: #4facfe;
924
- box-shadow: 0 8px 25px rgba(79, 172, 254, 0.15);
925
- }
926
-
927
- .test-case.deleted {
928
- opacity: 0.5;
929
- background: #f8f9fa;
930
- border-color: #dc3545;
931
- }
932
-
933
- .test-case-header {
936
+ .controls {
937
+ padding: 20px;
934
938
  background: #f8f9fa;
935
- padding: 15px 20px;
936
- border-bottom: 1px solid #e9ecef;
937
939
  display: flex;
938
940
  justify-content: space-between;
939
941
  align-items: center;
942
+ flex-wrap: wrap;
943
+ gap: 10px;
940
944
  }
941
945
 
942
- .test-case-id {
946
+ .btn {
947
+ padding: 12px 24px;
948
+ border: none;
949
+ border-radius: 8px;
943
950
  font-weight: 600;
944
- color: #495057;
951
+ cursor: pointer;
952
+ transition: all 0.3s ease;
953
+ text-decoration: none;
954
+ display: inline-block;
955
+ font-size: 14px;
945
956
  }
946
957
 
947
- .test-case-status {
948
- padding: 5px 12px;
949
- border-radius: 20px;
950
- font-size: 0.85rem;
951
- font-weight: 500;
952
- text-transform: uppercase;
958
+ .btn-primary {
959
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
960
+ color: white;
953
961
  }
954
962
 
955
- .status-new {
956
- background: #d4edda;
957
- color: #155724;
963
+ .btn-primary:hover {
964
+ transform: translateY(-2px);
965
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
958
966
  }
959
967
 
960
- .status-modify {
961
- background: #fff3cd;
962
- color: #856404;
968
+ .btn-secondary {
969
+ background: #6c757d;
970
+ color: white;
963
971
  }
964
972
 
965
- .status-remove {
966
- background: #f8d7da;
967
- color: #721c24;
973
+ .btn-secondary:hover {
974
+ background: #5a6268;
968
975
  }
969
976
 
970
- .test-case-content {
971
- padding: 20px;
977
+ .btn-delete {
978
+ background: #dc3545;
979
+ color: white;
980
+ padding: 8px 16px;
981
+ font-size: 12px;
972
982
  }
973
983
 
974
- .test-case-title {
975
- margin-bottom: 15px;
984
+ .btn-delete:hover {
985
+ background: #c82333;
976
986
  }
977
987
 
978
- .test-case-title textarea {
979
- width: 100%;
980
- border: 1px solid #ced4da;
981
- border-radius: 6px;
982
- padding: 10px;
983
- font-size: 1rem;
984
- transition: border-color 0.3s ease;
985
- min-height: 100px;
986
- resize: vertical;
987
- font-family: inherit;
988
+ .btn-restore {
989
+ background: #28a745;
990
+ color: white;
991
+ padding: 8px 16px;
992
+ font-size: 12px;
988
993
  }
989
994
 
990
- .test-case-title textarea:focus {
991
- outline: none;
992
- border-color: #4facfe;
993
- box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.1);
995
+ .btn-restore:hover {
996
+ background: #218838;
994
997
  }
995
998
 
996
- .original-case, .remove-case {
997
- background: #f8f9fa;
998
- padding: 10px;
999
- border-radius: 6px;
1000
- margin-bottom: 15px;
1001
- font-size: 0.9rem;
999
+ .test-cases {
1000
+ max-height: 70vh;
1001
+ overflow-y: auto;
1002
1002
  }
1003
1003
 
1004
- .test-case-actions {
1005
- display: flex;
1006
- gap: 10px;
1004
+ .test-case {
1005
+ border-bottom: 1px solid #e9ecef;
1006
+ padding: 20px;
1007
+ transition: all 0.3s ease;
1007
1008
  }
1008
1009
 
1009
- .btn {
1010
- padding: 8px 16px;
1011
- border: none;
1012
- border-radius: 6px;
1013
- cursor: pointer;
1014
- font-size: 0.9rem;
1015
- transition: all 0.3s ease;
1016
- font-weight: 500;
1010
+ .test-case:hover {
1011
+ background: #f8f9fa;
1017
1012
  }
1018
1013
 
1019
- .btn-delete {
1020
- background: #dc3545;
1021
- color: white;
1014
+ .test-case.deleted {
1015
+ opacity: 0.5;
1016
+ background: #f8d7da;
1022
1017
  }
1023
1018
 
1024
- .btn-delete:hover {
1025
- background: #c82333;
1019
+ .test-case-header {
1020
+ display: flex;
1021
+ justify-content: space-between;
1022
+ align-items: center;
1023
+ margin-bottom: 15px;
1026
1024
  }
1027
1025
 
1028
- .btn-restore {
1029
- background: #28a745;
1026
+ .test-case-meta {
1027
+ display: flex;
1028
+ gap: 15px;
1029
+ align-items: center;
1030
+ }
1031
+
1032
+ .test-case-index {
1033
+ background: #007bff;
1030
1034
  color: white;
1035
+ padding: 4px 8px;
1036
+ border-radius: 4px;
1037
+ font-size: 12px;
1038
+ font-weight: bold;
1031
1039
  }
1032
1040
 
1033
- .btn-restore:hover {
1034
- background: #218838;
1041
+ .test-case-status {
1042
+ padding: 4px 12px;
1043
+ border-radius: 12px;
1044
+ font-size: 12px;
1045
+ font-weight: 600;
1035
1046
  }
1036
1047
 
1037
- .footer {
1038
- background: #f8f9fa;
1039
- padding: 30px;
1040
- text-align: center;
1041
- border-top: 1px solid #e9ecef;
1048
+ .status-new {
1049
+ background: #d4edda;
1050
+ color: #155724;
1042
1051
  }
1043
1052
 
1044
- .btn-primary {
1045
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
1046
- color: white;
1047
- padding: 15px 40px;
1048
- font-size: 1.1rem;
1049
- border: none;
1050
- border-radius: 50px;
1051
- cursor: pointer;
1052
- transition: all 0.3s ease;
1053
- margin: 0 10px;
1053
+ .status-modify {
1054
+ background: #fff3cd;
1055
+ color: #856404;
1054
1056
  }
1055
1057
 
1056
- .btn-primary:hover {
1057
- transform: translateY(-2px);
1058
- box-shadow: 0 10px 30px rgba(79, 172, 254, 0.4);
1058
+ .status-remove {
1059
+ background: #f8d7da;
1060
+ color: #721c24;
1059
1061
  }
1060
1062
 
1061
- .btn-secondary {
1062
- background: #6c757d;
1063
- color: white;
1064
- padding: 15px 40px;
1065
- font-size: 1.1rem;
1066
- border: none;
1067
- border-radius: 50px;
1068
- cursor: pointer;
1069
- transition: all 0.3s ease;
1070
- margin: 0 10px;
1063
+ .test-case textarea {
1064
+ width: 100%;
1065
+ min-height: 100px;
1066
+ padding: 15px;
1067
+ border: 2px solid #e9ecef;
1068
+ border-radius: 8px;
1069
+ font-family: inherit;
1070
+ font-size: 14px;
1071
+ line-height: 1.5;
1072
+ resize: vertical;
1073
+ transition: border-color 0.3s ease;
1071
1074
  }
1072
1075
 
1073
- .btn-secondary:hover {
1074
- background: #5a6268;
1075
- transform: translateY(-2px);
1076
+ .test-case textarea:focus {
1077
+ outline: none;
1078
+ border-color: #007bff;
1079
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
1076
1080
  }
1077
1081
 
1078
1082
  .notification {
@@ -1080,81 +1084,94 @@ tool(
1080
1084
  top: 20px;
1081
1085
  right: 20px;
1082
1086
  padding: 15px 25px;
1083
- background: #28a745;
1084
- color: white;
1085
1087
  border-radius: 8px;
1086
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1088
+ color: white;
1089
+ font-weight: 600;
1087
1090
  display: none;
1088
1091
  z-index: 1000;
1092
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1093
+ }
1094
+
1095
+ @media (max-width: 768px) {
1096
+ .container {
1097
+ margin: 10px;
1098
+ border-radius: 10px;
1099
+ }
1100
+
1101
+ .header h1 {
1102
+ font-size: 2rem;
1103
+ }
1104
+
1105
+ .stats {
1106
+ flex-direction: column;
1107
+ gap: 15px;
1108
+ }
1109
+
1110
+ .controls {
1111
+ flex-direction: column;
1112
+ gap: 15px;
1113
+ }
1114
+
1115
+ .test-case-header {
1116
+ flex-direction: column;
1117
+ align-items: flex-start;
1118
+ gap: 10px;
1119
+ }
1089
1120
  }
1090
1121
  </style>
1091
1122
  </head>
1092
1123
  <body>
1124
+ <div class="notification" id="notification"></div>
1125
+
1093
1126
  <div class="container">
1094
1127
  <div class="header">
1095
- <h1>Test Cases Review</h1>
1096
- <p>Review, edit, and approve test cases for JIRA ticket</p>
1128
+ <h1>🔍 Test Cases Review & Approval</h1>
1129
+ <p>Review, edit, and approve your test cases. Make any necessary changes before final approval.</p>
1097
1130
  </div>
1098
1131
 
1099
1132
  <div class="stats">
1100
1133
  <div class="stat-item">
1101
- <div class="stat-number" id="totalCount">${safeTestCases.length}</div>
1102
- <div class="stat-label">Total Test Cases</div>
1134
+ <div class="stat-number" id="totalCount">${processedTestCases.length}</div>
1135
+ <div class="stat-label">Total Cases</div>
1103
1136
  </div>
1104
1137
  <div class="stat-item">
1105
- <div class="stat-number" id="activeCount">${safeTestCases.length}</div>
1106
- <div class="stat-label">Active</div>
1138
+ <div class="stat-number" id="activeCount">${processedTestCases.length}</div>
1139
+ <div class="stat-label">Active Cases</div>
1107
1140
  </div>
1108
1141
  <div class="stat-item">
1109
1142
  <div class="stat-number" id="deletedCount">0</div>
1110
- <div class="stat-label">Deleted</div>
1143
+ <div class="stat-label">Deleted Cases</div>
1111
1144
  </div>
1112
1145
  </div>
1113
1146
 
1114
- <div class="content">
1115
- <div id="testCases">
1116
- ${safeTestCases.map((testCase, index) => {
1117
- const escapedContent = testCase[0].replace(/"/g, '&quot;').replace(/'/g, '&#39;').replace(/\n/g, '\\n');
1118
- return `
1147
+ <div class="controls">
1148
+ <div>
1149
+ <button class="btn btn-secondary" onclick="resetAll()">🔄 Reset All</button>
1150
+ </div>
1151
+ <div>
1152
+ <button class="btn btn-primary" onclick="approveTestCases()">✅ Approve Test Cases</button>
1153
+ </div>
1154
+ </div>
1155
+
1156
+ <div class="test-cases">
1157
+ ${processedTestCases.map((testCase, index) => `
1119
1158
  <div class="test-case" data-index="${index}">
1120
1159
  <div class="test-case-header">
1121
- <span class="test-case-id">Test Case #${index + 1}</span>
1122
- <span class="test-case-status status-${testCase[2].toLowerCase()}">${testCase[2]}</span>
1123
- </div>
1124
- <div class="test-case-content">
1125
- <div class="test-case-title">
1126
- <textarea data-index="${index}" placeholder="Enter test case title and steps...">${escapedContent}</textarea>
1127
- </div>
1128
- ${testCase[2] === 'Modify' && testCase[3] ? `
1129
- <div class="original-case">
1130
- <small><strong>Original:</strong> ${testCase[3]}</small>
1131
- </div>
1132
- ` : ''}
1133
- ${testCase[2] === 'Remove' ? `
1134
- <div class="remove-case">
1135
- <small><strong>Marked for Removal</strong></small>
1136
- </div>
1137
- ` : ''}
1138
- <div class="test-case-actions">
1139
- <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1160
+ <div class="test-case-meta">
1161
+ <span class="test-case-index">#${index + 1}</span>
1162
+ <span class="test-case-status status-${testCase.status.toLowerCase()}">${testCase.status}</span>
1163
+ ${testCase.originalCase ? `<span style="font-size: 12px; color: #6c757d;">Ref: ${testCase.originalCase}</span>` : ''}
1140
1164
  </div>
1165
+ <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1141
1166
  </div>
1167
+ <textarea data-index="${index}" placeholder="Enter test case details...">${testCase.title}${testCase.description ? '\n\nSteps:\n' + testCase.description : ''}</textarea>
1142
1168
  </div>
1143
- `;
1144
- }).join('')}
1145
- </div>
1146
- </div>
1147
-
1148
- <div class="footer">
1149
- <button class="btn-primary" onclick="approveTestCases()">Approve Test Cases</button>
1150
- <button class="btn-secondary" onclick="resetAll()">Reset All</button>
1169
+ `).join('')}
1151
1170
  </div>
1152
1171
  </div>
1153
1172
 
1154
- <div class="notification" id="notification"></div>
1155
-
1156
1173
  <script>
1157
- let testCases = ${JSON.stringify(safeTestCases)};
1174
+ let testCases = ${JSON.stringify(processedTestCases).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')};
1158
1175
  let deletedIndices = new Set();
1159
1176
 
1160
1177
  function updateStats() {
@@ -1192,7 +1209,9 @@ tool(
1192
1209
 
1193
1210
  // Reset textarea value
1194
1211
  const textarea = el.querySelector('textarea');
1195
- textarea.value = testCases[index][0];
1212
+ const originalTestCase = testCases[index];
1213
+ const resetValue = originalTestCase.title + (originalTestCase.description ? '\\n\\nSteps:\\n' + originalTestCase.description : '');
1214
+ textarea.value = resetValue;
1196
1215
  });
1197
1216
  updateStats();
1198
1217
  }
@@ -1215,9 +1234,17 @@ tool(
1215
1234
  document.querySelectorAll('.test-case').forEach((el, index) => {
1216
1235
  if (!deletedIndices.has(index)) {
1217
1236
  const textarea = el.querySelector('textarea');
1218
- const originalTestCase = [...testCases[index]];
1219
- originalTestCase[0] = textarea.value;
1220
- updatedTestCases.push(originalTestCase);
1237
+ const originalTestCase = testCases[index];
1238
+
1239
+ // Create the updated test case array in the original format
1240
+ const updatedCase = [
1241
+ textarea.value.trim(), // Updated title/content
1242
+ originalTestCase.description, // Keep original description
1243
+ originalTestCase.status, // Keep original status
1244
+ originalTestCase.originalCase // Keep original case reference
1245
+ ];
1246
+
1247
+ updatedTestCases.push(updatedCase);
1221
1248
  }
1222
1249
  });
1223
1250
 
@@ -1230,7 +1257,7 @@ tool(
1230
1257
  body: JSON.stringify({
1231
1258
  sessionId: '${sessionId}',
1232
1259
  testCases: updatedTestCases
1233
- })
1260
+ })
1234
1261
  })
1235
1262
  .then(response => response.json())
1236
1263
  .then(data => {
@@ -1258,14 +1285,23 @@ tool(
1258
1285
  if (e.target.tagName === 'TEXTAREA') {
1259
1286
  const index = parseInt(e.target.getAttribute('data-index'));
1260
1287
  if (!isNaN(index) && testCases[index]) {
1261
- testCases[index][0] = e.target.value;
1288
+ testCases[index].title = e.target.value.split('\\n\\nSteps:\\n')[0];
1289
+ const stepsIndex = e.target.value.indexOf('\\n\\nSteps:\\n');
1290
+ if (stepsIndex !== -1) {
1291
+ testCases[index].description = e.target.value.substring(stepsIndex + 9);
1292
+ }
1262
1293
  }
1263
1294
  }
1264
1295
  });
1265
1296
  </script>
1266
1297
  </body>
1267
1298
  </html>
1268
- `);
1299
+ `;
1300
+ res.send(htmlContent);
1301
+ } catch (error) {
1302
+ console.error('Error rendering page:', error);
1303
+ res.status(500).send('Error rendering page');
1304
+ }
1269
1305
  });
1270
1306
 
1271
1307
  // Approval endpoint with better error handling
@@ -1292,7 +1328,9 @@ tool(
1292
1328
 
1293
1329
  // Close server after approval
1294
1330
  setTimeout(() => {
1295
- server.close();
1331
+ if (server && server.listening) {
1332
+ server.close();
1333
+ }
1296
1334
  }, 3000);
1297
1335
  } catch (error) {
1298
1336
  console.error('Approval error:', error);
@@ -1300,26 +1338,33 @@ tool(
1300
1338
  }
1301
1339
  });
1302
1340
 
1303
- // Start server
1304
- const server = app.listen(port, () => {
1305
- console.log(`Test case review server running at http://localhost:${port}`);
1341
+ // Error handling middleware
1342
+ app.use((err, req, res, next) => {
1343
+ console.error('Express error:', err);
1344
+ res.status(500).json({ error: 'Internal server error' });
1306
1345
  });
1307
1346
 
1308
- // Handle server errors
1309
- server.on('error', (error) => {
1310
- if (error.code === 'EADDRINUSE') {
1311
- // Try a different port
1312
- const newPort = port + Math.floor(Math.random() * 100);
1313
- return app.listen(newPort, () => {
1314
- console.log(`Test case review server running at http://localhost:${newPort}`);
1315
- try {
1316
- openBrowser(`http://localhost:${newPort}`).catch(err => console.error('Failed to open browser:', err));
1317
- } catch (e) {
1318
- console.error('Failed to open browser automatically:', e.message);
1319
- }
1320
- });
1321
- }
1322
- throw error;
1347
+ // 404 handler
1348
+ app.use((req, res) => {
1349
+ res.status(404).json({ error: 'Not found' });
1350
+ });
1351
+
1352
+ // Start server with promise-based approach
1353
+ const server = await new Promise((resolve, reject) => {
1354
+ const srv = app.listen(port, (err) => {
1355
+ if (err) {
1356
+ reject(err);
1357
+ return;
1358
+ }
1359
+ console.log(`✅ Test case review session started. Session ID: ${sessionId}.`);
1360
+ console.log(`Server running at http://localhost:${port}`);
1361
+ console.log(`Browser should open automatically.`);
1362
+ resolve(srv);
1363
+ });
1364
+
1365
+ srv.on('error', (error) => {
1366
+ reject(error);
1367
+ });
1323
1368
  });
1324
1369
 
1325
1370
  // Open browser with proper error handling
@@ -1336,8 +1381,9 @@ tool(
1336
1381
  global.approvalSessions = global.approvalSessions || {};
1337
1382
  global.approvalSessions[sessionId] = {
1338
1383
  status: 'pending',
1339
- testCases: testCases,
1340
- timestamp: Date.now()
1384
+ testCases: processedTestCases,
1385
+ timestamp: Date.now(),
1386
+ server: server
1341
1387
  };
1342
1388
 
1343
1389
  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.'}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "3.0.4",
3
+ "version": "3.0.6",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"