@nbakka/mcp-appium 3.0.5 → 3.0.7

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 +234 -235
  2. package/package.json +1 -1
package/lib/server.js CHANGED
@@ -806,16 +806,33 @@ 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
838
  // Process test cases - handle the specific format properly
@@ -841,10 +858,26 @@ tool(
841
858
  }
842
859
  });
843
860
 
861
+ // Helper function to get display text for test cases
862
+ const getTestCaseDisplayText = (testCase) => {
863
+ const status = testCase.status.toLowerCase();
864
+
865
+ if (status === 'modify') {
866
+ // For modify cases, show original → changed format
867
+ return `${testCase.title}\n\nOriginal: ${testCase.originalCase || 'Not specified'}\nChanged to: ${testCase.description || 'Not specified'}`;
868
+ } else if (status === 'remove') {
869
+ // For remove cases, just show the title
870
+ return testCase.title;
871
+ } else {
872
+ // For new cases, just show the title
873
+ return testCase.title;
874
+ }
875
+ };
876
+
844
877
  // Main review page with proper handling
845
878
  app.get('/', (req, res) => {
846
879
  try {
847
- res.send(`
880
+ const htmlContent = `
848
881
  <!DOCTYPE html>
849
882
  <html lang="en">
850
883
  <head>
@@ -912,189 +945,154 @@ tool(
912
945
 
913
946
  .stat-label {
914
947
  color: #6c757d;
948
+ font-size: 0.9rem;
915
949
  margin-top: 5px;
916
950
  }
917
951
 
918
- .content {
919
- padding: 30px;
920
- max-height: 70vh;
921
- overflow-y: auto;
922
- }
923
-
924
- .test-case {
925
- background: #fff;
926
- border: 2px solid #e9ecef;
927
- border-radius: 12px;
928
- margin-bottom: 20px;
929
- overflow: hidden;
930
- transition: all 0.3s ease;
931
- position: relative;
932
- }
933
-
934
- .test-case:hover {
935
- border-color: #4facfe;
936
- box-shadow: 0 8px 25px rgba(79, 172, 254, 0.15);
937
- }
938
-
939
- .test-case.deleted {
940
- opacity: 0.5;
941
- background: #f8f9fa;
942
- border-color: #dc3545;
943
- }
944
-
945
- .test-case-header {
952
+ .controls {
953
+ padding: 20px;
946
954
  background: #f8f9fa;
947
- padding: 15px 20px;
948
- border-bottom: 1px solid #e9ecef;
949
955
  display: flex;
950
956
  justify-content: space-between;
951
957
  align-items: center;
958
+ flex-wrap: wrap;
959
+ gap: 10px;
952
960
  }
953
961
 
954
- .test-case-id {
962
+ .btn {
963
+ padding: 12px 24px;
964
+ border: none;
965
+ border-radius: 8px;
955
966
  font-weight: 600;
956
- color: #495057;
967
+ cursor: pointer;
968
+ transition: all 0.3s ease;
969
+ text-decoration: none;
970
+ display: inline-block;
971
+ font-size: 14px;
957
972
  }
958
973
 
959
- .test-case-status {
960
- padding: 5px 12px;
961
- border-radius: 20px;
962
- font-size: 0.85rem;
963
- font-weight: 500;
964
- text-transform: uppercase;
974
+ .btn-primary {
975
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
976
+ color: white;
965
977
  }
966
978
 
967
- .status-new {
968
- background: #d4edda;
969
- color: #155724;
979
+ .btn-primary:hover {
980
+ transform: translateY(-2px);
981
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
970
982
  }
971
983
 
972
- .status-modify {
973
- background: #fff3cd;
974
- color: #856404;
984
+ .btn-secondary {
985
+ background: #6c757d;
986
+ color: white;
975
987
  }
976
988
 
977
- .status-remove {
978
- background: #f8d7da;
979
- color: #721c24;
989
+ .btn-secondary:hover {
990
+ background: #5a6268;
980
991
  }
981
992
 
982
- .test-case-content {
983
- padding: 20px;
993
+ .btn-delete {
994
+ background: #dc3545;
995
+ color: white;
996
+ padding: 8px 16px;
997
+ font-size: 12px;
984
998
  }
985
999
 
986
- .test-case-title {
987
- margin-bottom: 15px;
1000
+ .btn-delete:hover {
1001
+ background: #c82333;
988
1002
  }
989
1003
 
990
- .test-case-title textarea {
991
- width: 100%;
992
- border: 1px solid #ced4da;
993
- border-radius: 6px;
994
- padding: 10px;
995
- font-size: 1rem;
996
- transition: border-color 0.3s ease;
997
- min-height: 120px;
998
- resize: vertical;
999
- font-family: inherit;
1004
+ .btn-restore {
1005
+ background: #28a745;
1006
+ color: white;
1007
+ padding: 8px 16px;
1008
+ font-size: 12px;
1000
1009
  }
1001
1010
 
1002
- .test-case-title textarea:focus {
1003
- outline: none;
1004
- border-color: #4facfe;
1005
- box-shadow: 0 0 0 3px rgba(79, 172, 254, 0.1);
1011
+ .btn-restore:hover {
1012
+ background: #218838;
1006
1013
  }
1007
1014
 
1008
- .original-case {
1009
- background: #fff3cd;
1010
- border: 1px solid #ffeaa7;
1011
- padding: 10px;
1012
- border-radius: 6px;
1013
- margin-bottom: 15px;
1014
- font-size: 0.9rem;
1015
+ .test-cases {
1016
+ max-height: 70vh;
1017
+ overflow-y: auto;
1015
1018
  }
1016
1019
 
1017
- .remove-case {
1018
- background: #f8d7da;
1019
- border: 1px solid #f5c6cb;
1020
- padding: 10px;
1021
- border-radius: 6px;
1022
- margin-bottom: 15px;
1023
- font-size: 0.9rem;
1020
+ .test-case {
1021
+ border-bottom: 1px solid #e9ecef;
1022
+ padding: 20px;
1023
+ transition: all 0.3s ease;
1024
1024
  }
1025
1025
 
1026
- .test-case-actions {
1027
- display: flex;
1028
- gap: 10px;
1026
+ .test-case:hover {
1027
+ background: #f8f9fa;
1029
1028
  }
1030
1029
 
1031
- .btn {
1032
- padding: 8px 16px;
1033
- border: none;
1034
- border-radius: 6px;
1035
- cursor: pointer;
1036
- font-size: 0.9rem;
1037
- transition: all 0.3s ease;
1038
- font-weight: 500;
1030
+ .test-case.deleted {
1031
+ opacity: 0.5;
1032
+ background: #f8d7da;
1039
1033
  }
1040
1034
 
1041
- .btn-delete {
1042
- background: #dc3545;
1043
- color: white;
1035
+ .test-case-header {
1036
+ display: flex;
1037
+ justify-content: space-between;
1038
+ align-items: center;
1039
+ margin-bottom: 15px;
1044
1040
  }
1045
1041
 
1046
- .btn-delete:hover {
1047
- background: #c82333;
1042
+ .test-case-meta {
1043
+ display: flex;
1044
+ gap: 15px;
1045
+ align-items: center;
1048
1046
  }
1049
1047
 
1050
- .btn-restore {
1051
- background: #28a745;
1048
+ .test-case-index {
1049
+ background: #007bff;
1052
1050
  color: white;
1051
+ padding: 4px 8px;
1052
+ border-radius: 4px;
1053
+ font-size: 12px;
1054
+ font-weight: bold;
1053
1055
  }
1054
1056
 
1055
- .btn-restore:hover {
1056
- background: #218838;
1057
+ .test-case-status {
1058
+ padding: 4px 12px;
1059
+ border-radius: 12px;
1060
+ font-size: 12px;
1061
+ font-weight: 600;
1057
1062
  }
1058
1063
 
1059
- .footer {
1060
- background: #f8f9fa;
1061
- padding: 30px;
1062
- text-align: center;
1063
- border-top: 1px solid #e9ecef;
1064
+ .status-new {
1065
+ background: #d4edda;
1066
+ color: #155724;
1064
1067
  }
1065
1068
 
1066
- .btn-primary {
1067
- background: linear-gradient(135deg, #4facfe 0%, #00f2fe 100%);
1068
- color: white;
1069
- padding: 15px 40px;
1070
- font-size: 1.1rem;
1071
- border: none;
1072
- border-radius: 50px;
1073
- cursor: pointer;
1074
- transition: all 0.3s ease;
1075
- margin: 0 10px;
1069
+ .status-modify {
1070
+ background: #fff3cd;
1071
+ color: #856404;
1076
1072
  }
1077
1073
 
1078
- .btn-primary:hover {
1079
- transform: translateY(-2px);
1080
- box-shadow: 0 10px 30px rgba(79, 172, 254, 0.4);
1074
+ .status-remove {
1075
+ background: #f8d7da;
1076
+ color: #721c24;
1081
1077
  }
1082
1078
 
1083
- .btn-secondary {
1084
- background: #6c757d;
1085
- color: white;
1086
- padding: 15px 40px;
1087
- font-size: 1.1rem;
1088
- border: none;
1089
- border-radius: 50px;
1090
- cursor: pointer;
1091
- transition: all 0.3s ease;
1092
- margin: 0 10px;
1079
+ .test-case textarea {
1080
+ width: 100%;
1081
+ min-height: 100px;
1082
+ padding: 15px;
1083
+ border: 2px solid #e9ecef;
1084
+ border-radius: 8px;
1085
+ font-family: inherit;
1086
+ font-size: 14px;
1087
+ line-height: 1.5;
1088
+ resize: vertical;
1089
+ transition: border-color 0.3s ease;
1093
1090
  }
1094
1091
 
1095
- .btn-secondary:hover {
1096
- background: #5a6268;
1097
- transform: translateY(-2px);
1092
+ .test-case textarea:focus {
1093
+ outline: none;
1094
+ border-color: #007bff;
1095
+ box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.1);
1098
1096
  }
1099
1097
 
1100
1098
  .notification {
@@ -1102,104 +1100,104 @@ tool(
1102
1100
  top: 20px;
1103
1101
  right: 20px;
1104
1102
  padding: 15px 25px;
1105
- background: #28a745;
1106
- color: white;
1107
1103
  border-radius: 8px;
1108
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1104
+ color: white;
1105
+ font-weight: 600;
1109
1106
  display: none;
1110
1107
  z-index: 1000;
1108
+ box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1109
+ }
1110
+
1111
+ @media (max-width: 768px) {
1112
+ .container {
1113
+ margin: 10px;
1114
+ border-radius: 10px;
1115
+ }
1116
+
1117
+ .header h1 {
1118
+ font-size: 2rem;
1119
+ }
1120
+
1121
+ .stats {
1122
+ flex-direction: column;
1123
+ gap: 15px;
1124
+ }
1125
+
1126
+ .controls {
1127
+ flex-direction: column;
1128
+ gap: 15px;
1129
+ }
1130
+
1131
+ .test-case-header {
1132
+ flex-direction: column;
1133
+ align-items: flex-start;
1134
+ gap: 10px;
1135
+ }
1111
1136
  }
1112
1137
  </style>
1113
1138
  </head>
1114
1139
  <body>
1140
+ <div class="notification" id="notification"></div>
1141
+
1115
1142
  <div class="container">
1116
1143
  <div class="header">
1117
- <h1>Test Cases Review</h1>
1118
- <p>Review, edit, and approve test cases for JIRA ticket</p>
1144
+ <h1>🔍 Test Cases Review & Approval</h1>
1145
+ <p>Review, edit, and approve your test cases. Make any necessary changes before final approval.</p>
1119
1146
  </div>
1120
1147
 
1121
1148
  <div class="stats">
1122
1149
  <div class="stat-item">
1123
1150
  <div class="stat-number" id="totalCount">${processedTestCases.length}</div>
1124
- <div class="stat-label">Total Test Cases</div>
1151
+ <div class="stat-label">Total Cases</div>
1125
1152
  </div>
1126
1153
  <div class="stat-item">
1127
1154
  <div class="stat-number" id="activeCount">${processedTestCases.length}</div>
1128
- <div class="stat-label">Active</div>
1155
+ <div class="stat-label">Active Cases</div>
1129
1156
  </div>
1130
1157
  <div class="stat-item">
1131
1158
  <div class="stat-number" id="deletedCount">0</div>
1132
- <div class="stat-label">Deleted</div>
1159
+ <div class="stat-label">Deleted Cases</div>
1160
+ </div>
1161
+ </div>
1162
+
1163
+ <div class="controls">
1164
+ <div>
1165
+ <button class="btn btn-secondary" onclick="resetAll()">🔄 Reset All</button>
1166
+ </div>
1167
+ <div>
1168
+ <button class="btn btn-primary" onclick="approveTestCases()">✅ Approve Test Cases</button>
1133
1169
  </div>
1134
1170
  </div>
1135
1171
 
1136
- <div class="content">
1137
- <div id="testCases">
1138
- ${processedTestCases.map((testCase, index) => {
1139
- // Safely escape HTML characters
1140
- const safeTitle = testCase.title
1141
- .replace(/&/g, '&amp;')
1142
- .replace(/</g, '&lt;')
1143
- .replace(/>/g, '&gt;')
1144
- .replace(/"/g, '&quot;')
1145
- .replace(/'/g, '&#39;');
1146
-
1147
- const safeDescription = testCase.description
1148
- .replace(/&/g, '&amp;')
1149
- .replace(/</g, '&lt;')
1150
- .replace(/>/g, '&gt;')
1151
- .replace(/"/g, '&quot;')
1152
- .replace(/'/g, '&#39;');
1153
-
1154
- const safeOriginal = testCase.originalCase
1155
- .replace(/&/g, '&amp;')
1156
- .replace(/</g, '&lt;')
1157
- .replace(/>/g, '&gt;')
1158
- .replace(/"/g, '&quot;')
1159
- .replace(/'/g, '&#39;');
1160
-
1161
- return `
1172
+ <div class="test-cases">
1173
+ ${processedTestCases.map((testCase, index) => {
1174
+ const displayText = getTestCaseDisplayText(testCase);
1175
+ const statusLabel = testCase.status === 'Remove' ? 'Remove' : testCase.status;
1176
+ const refText = testCase.originalCase && testCase.status.toLowerCase() === 'remove'
1177
+ ? `Ref: ${testCase.originalCase}`
1178
+ : testCase.originalCase && testCase.status.toLowerCase() !== 'modify'
1179
+ ? `Ref: ${testCase.originalCase}`
1180
+ : '';
1181
+
1182
+ return `
1162
1183
  <div class="test-case" data-index="${index}">
1163
1184
  <div class="test-case-header">
1164
- <span class="test-case-id">Test Case #${index + 1}</span>
1165
- <span class="test-case-status status-${testCase.status.toLowerCase()}">${testCase.status}</span>
1166
- </div>
1167
- <div class="test-case-content">
1168
- <div class="test-case-title">
1169
- <textarea data-index="${index}" placeholder="Enter test case title and steps...">${safeTitle}${safeDescription ? '\n\nSteps:\n' + safeDescription : ''}</textarea>
1170
- </div>
1171
- ${testCase.status === 'Modify' && testCase.originalCase ? `
1172
- <div class="original-case">
1173
- <strong>Original Test Case:</strong><br>
1174
- ${safeOriginal}
1175
- </div>
1176
- ` : ''}
1177
- ${testCase.status === 'Remove' ? `
1178
- <div class="remove-case">
1179
- <strong>⚠️ Marked for Removal</strong><br>
1180
- This test case is scheduled to be removed. You can restore it using the button below.
1181
- </div>
1182
- ` : ''}
1183
- <div class="test-case-actions">
1184
- <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1185
+ <div class="test-case-meta">
1186
+ <span class="test-case-index">#${index + 1}</span>
1187
+ <span class="test-case-status status-${testCase.status.toLowerCase()}">${statusLabel}</span>
1188
+ ${refText ? `<span style="font-size: 12px; color: #6c757d;">${refText}</span>` : ''}
1185
1189
  </div>
1190
+ <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1186
1191
  </div>
1192
+ <textarea data-index="${index}" placeholder="Enter test case details...">${displayText}</textarea>
1187
1193
  </div>
1188
- `;
1189
- }).join('')}
1190
- </div>
1191
- </div>
1192
-
1193
- <div class="footer">
1194
- <button class="btn-primary" onclick="approveTestCases()">Approve Test Cases</button>
1195
- <button class="btn-secondary" onclick="resetAll()">Reset All</button>
1194
+ `;
1195
+ }).join('')}
1196
1196
  </div>
1197
1197
  </div>
1198
1198
 
1199
- <div class="notification" id="notification"></div>
1200
-
1201
1199
  <script>
1202
- let testCases = ${JSON.stringify(processedTestCases)};
1200
+ let testCases = ${JSON.stringify(processedTestCases).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')};
1203
1201
  let deletedIndices = new Set();
1204
1202
 
1205
1203
  function updateStats() {
@@ -1238,7 +1236,17 @@ tool(
1238
1236
  // Reset textarea value
1239
1237
  const textarea = el.querySelector('textarea');
1240
1238
  const originalTestCase = testCases[index];
1241
- const resetValue = originalTestCase.title + (originalTestCase.description ? '\n\nSteps:\n' + originalTestCase.description : '');
1239
+
1240
+ // Use proper display format based on status
1241
+ let resetValue = '';
1242
+ const status = originalTestCase.status.toLowerCase();
1243
+
1244
+ if (status === 'modify') {
1245
+ resetValue = originalTestCase.title + '\\n\\nOriginal: ' + (originalTestCase.originalCase || 'Not specified') + '\\nChanged to: ' + (originalTestCase.description || 'Not specified');
1246
+ } else {
1247
+ resetValue = originalTestCase.title;
1248
+ }
1249
+
1242
1250
  textarea.value = resetValue;
1243
1251
  });
1244
1252
  updateStats();
@@ -1285,7 +1293,7 @@ tool(
1285
1293
  body: JSON.stringify({
1286
1294
  sessionId: '${sessionId}',
1287
1295
  testCases: updatedTestCases
1288
- })
1296
+ })
1289
1297
  })
1290
1298
  .then(response => response.json())
1291
1299
  .then(data => {
@@ -1313,18 +1321,16 @@ tool(
1313
1321
  if (e.target.tagName === 'TEXTAREA') {
1314
1322
  const index = parseInt(e.target.getAttribute('data-index'));
1315
1323
  if (!isNaN(index) && testCases[index]) {
1316
- testCases[index].title = e.target.value.split('\n\nSteps:\n')[0];
1317
- const stepsIndex = e.target.value.indexOf('\n\nSteps:\n');
1318
- if (stepsIndex !== -1) {
1319
- testCases[index].description = e.target.value.substring(stepsIndex + 9);
1320
- }
1324
+ // Update the title with the textarea content
1325
+ testCases[index].title = e.target.value.trim();
1321
1326
  }
1322
1327
  }
1323
1328
  });
1324
1329
  </script>
1325
1330
  </body>
1326
1331
  </html>
1327
- `);
1332
+ `;
1333
+ res.send(htmlContent);
1328
1334
  } catch (error) {
1329
1335
  console.error('Error rendering page:', error);
1330
1336
  res.status(500).send('Error rendering page');
@@ -1355,7 +1361,9 @@ tool(
1355
1361
 
1356
1362
  // Close server after approval
1357
1363
  setTimeout(() => {
1358
- server.close();
1364
+ if (server && server.listening) {
1365
+ server.close();
1366
+ }
1359
1367
  }, 3000);
1360
1368
  } catch (error) {
1361
1369
  console.error('Approval error:', error);
@@ -1374,32 +1382,22 @@ tool(
1374
1382
  res.status(404).json({ error: 'Not found' });
1375
1383
  });
1376
1384
 
1377
- // Start server with better error handling
1378
- const server = app.listen(port, (err) => {
1379
- if (err) {
1380
- console.error('Failed to start server:', err);
1381
- return;
1382
- }
1383
- console.log(`✅ Test case review session started. Session ID: ${sessionId}.`);
1384
- console.log(`Server running at http://localhost:${port}`);
1385
- });
1385
+ // Start server with promise-based approach
1386
+ const server = await new Promise((resolve, reject) => {
1387
+ const srv = app.listen(port, (err) => {
1388
+ if (err) {
1389
+ reject(err);
1390
+ return;
1391
+ }
1392
+ console.log(`✅ Test case review session started. Session ID: ${sessionId}.`);
1393
+ console.log(`Server running at http://localhost:${port}`);
1394
+ console.log(`Browser should open automatically.`);
1395
+ resolve(srv);
1396
+ });
1386
1397
 
1387
- // Handle server errors
1388
- server.on('error', (error) => {
1389
- if (error.code === 'EADDRINUSE') {
1390
- // Try a different port
1391
- const newPort = port + Math.floor(Math.random() * 100);
1392
- const newServer = app.listen(newPort, () => {
1393
- console.log(`Test case review server running at http://localhost:${newPort}`);
1394
- try {
1395
- openBrowser(`http://localhost:${newPort}`).catch(err => console.error('Failed to open browser:', err));
1396
- } catch (e) {
1397
- console.error('Failed to open browser automatically:', e.message);
1398
- }
1399
- });
1400
- return newServer;
1401
- }
1402
- throw error;
1398
+ srv.on('error', (error) => {
1399
+ reject(error);
1400
+ });
1403
1401
  });
1404
1402
 
1405
1403
  // Open browser with proper error handling
@@ -1417,7 +1415,8 @@ tool(
1417
1415
  global.approvalSessions[sessionId] = {
1418
1416
  status: 'pending',
1419
1417
  testCases: processedTestCases,
1420
- timestamp: Date.now()
1418
+ timestamp: Date.now(),
1419
+ server: server
1421
1420
  };
1422
1421
 
1423
1422
  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.5",
3
+ "version": "3.0.7",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"