@nbakka/mcp-appium 3.0.5 → 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 +198 -232
  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
@@ -844,7 +861,7 @@ tool(
844
861
  // Main review page with proper handling
845
862
  app.get('/', (req, res) => {
846
863
  try {
847
- res.send(`
864
+ const htmlContent = `
848
865
  <!DOCTYPE html>
849
866
  <html lang="en">
850
867
  <head>
@@ -912,189 +929,154 @@ tool(
912
929
 
913
930
  .stat-label {
914
931
  color: #6c757d;
932
+ font-size: 0.9rem;
915
933
  margin-top: 5px;
916
934
  }
917
935
 
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 {
936
+ .controls {
937
+ padding: 20px;
946
938
  background: #f8f9fa;
947
- padding: 15px 20px;
948
- border-bottom: 1px solid #e9ecef;
949
939
  display: flex;
950
940
  justify-content: space-between;
951
941
  align-items: center;
942
+ flex-wrap: wrap;
943
+ gap: 10px;
952
944
  }
953
945
 
954
- .test-case-id {
946
+ .btn {
947
+ padding: 12px 24px;
948
+ border: none;
949
+ border-radius: 8px;
955
950
  font-weight: 600;
956
- color: #495057;
951
+ cursor: pointer;
952
+ transition: all 0.3s ease;
953
+ text-decoration: none;
954
+ display: inline-block;
955
+ font-size: 14px;
957
956
  }
958
957
 
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;
958
+ .btn-primary {
959
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
960
+ color: white;
965
961
  }
966
962
 
967
- .status-new {
968
- background: #d4edda;
969
- color: #155724;
963
+ .btn-primary:hover {
964
+ transform: translateY(-2px);
965
+ box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
970
966
  }
971
967
 
972
- .status-modify {
973
- background: #fff3cd;
974
- color: #856404;
968
+ .btn-secondary {
969
+ background: #6c757d;
970
+ color: white;
975
971
  }
976
972
 
977
- .status-remove {
978
- background: #f8d7da;
979
- color: #721c24;
973
+ .btn-secondary:hover {
974
+ background: #5a6268;
980
975
  }
981
976
 
982
- .test-case-content {
983
- padding: 20px;
977
+ .btn-delete {
978
+ background: #dc3545;
979
+ color: white;
980
+ padding: 8px 16px;
981
+ font-size: 12px;
984
982
  }
985
983
 
986
- .test-case-title {
987
- margin-bottom: 15px;
984
+ .btn-delete:hover {
985
+ background: #c82333;
988
986
  }
989
987
 
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;
988
+ .btn-restore {
989
+ background: #28a745;
990
+ color: white;
991
+ padding: 8px 16px;
992
+ font-size: 12px;
1000
993
  }
1001
994
 
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);
995
+ .btn-restore:hover {
996
+ background: #218838;
1006
997
  }
1007
998
 
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;
999
+ .test-cases {
1000
+ max-height: 70vh;
1001
+ overflow-y: auto;
1015
1002
  }
1016
1003
 
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;
1004
+ .test-case {
1005
+ border-bottom: 1px solid #e9ecef;
1006
+ padding: 20px;
1007
+ transition: all 0.3s ease;
1024
1008
  }
1025
1009
 
1026
- .test-case-actions {
1027
- display: flex;
1028
- gap: 10px;
1010
+ .test-case:hover {
1011
+ background: #f8f9fa;
1029
1012
  }
1030
1013
 
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;
1014
+ .test-case.deleted {
1015
+ opacity: 0.5;
1016
+ background: #f8d7da;
1039
1017
  }
1040
1018
 
1041
- .btn-delete {
1042
- background: #dc3545;
1043
- color: white;
1019
+ .test-case-header {
1020
+ display: flex;
1021
+ justify-content: space-between;
1022
+ align-items: center;
1023
+ margin-bottom: 15px;
1044
1024
  }
1045
1025
 
1046
- .btn-delete:hover {
1047
- background: #c82333;
1026
+ .test-case-meta {
1027
+ display: flex;
1028
+ gap: 15px;
1029
+ align-items: center;
1048
1030
  }
1049
1031
 
1050
- .btn-restore {
1051
- background: #28a745;
1032
+ .test-case-index {
1033
+ background: #007bff;
1052
1034
  color: white;
1035
+ padding: 4px 8px;
1036
+ border-radius: 4px;
1037
+ font-size: 12px;
1038
+ font-weight: bold;
1053
1039
  }
1054
1040
 
1055
- .btn-restore:hover {
1056
- background: #218838;
1041
+ .test-case-status {
1042
+ padding: 4px 12px;
1043
+ border-radius: 12px;
1044
+ font-size: 12px;
1045
+ font-weight: 600;
1057
1046
  }
1058
1047
 
1059
- .footer {
1060
- background: #f8f9fa;
1061
- padding: 30px;
1062
- text-align: center;
1063
- border-top: 1px solid #e9ecef;
1048
+ .status-new {
1049
+ background: #d4edda;
1050
+ color: #155724;
1064
1051
  }
1065
1052
 
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;
1053
+ .status-modify {
1054
+ background: #fff3cd;
1055
+ color: #856404;
1076
1056
  }
1077
1057
 
1078
- .btn-primary:hover {
1079
- transform: translateY(-2px);
1080
- box-shadow: 0 10px 30px rgba(79, 172, 254, 0.4);
1058
+ .status-remove {
1059
+ background: #f8d7da;
1060
+ color: #721c24;
1081
1061
  }
1082
1062
 
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;
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;
1093
1074
  }
1094
1075
 
1095
- .btn-secondary:hover {
1096
- background: #5a6268;
1097
- 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);
1098
1080
  }
1099
1081
 
1100
1082
  .notification {
@@ -1102,104 +1084,94 @@ tool(
1102
1084
  top: 20px;
1103
1085
  right: 20px;
1104
1086
  padding: 15px 25px;
1105
- background: #28a745;
1106
- color: white;
1107
1087
  border-radius: 8px;
1108
- box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
1088
+ color: white;
1089
+ font-weight: 600;
1109
1090
  display: none;
1110
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
+ }
1111
1120
  }
1112
1121
  </style>
1113
1122
  </head>
1114
1123
  <body>
1124
+ <div class="notification" id="notification"></div>
1125
+
1115
1126
  <div class="container">
1116
1127
  <div class="header">
1117
- <h1>Test Cases Review</h1>
1118
- <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>
1119
1130
  </div>
1120
1131
 
1121
1132
  <div class="stats">
1122
1133
  <div class="stat-item">
1123
1134
  <div class="stat-number" id="totalCount">${processedTestCases.length}</div>
1124
- <div class="stat-label">Total Test Cases</div>
1135
+ <div class="stat-label">Total Cases</div>
1125
1136
  </div>
1126
1137
  <div class="stat-item">
1127
1138
  <div class="stat-number" id="activeCount">${processedTestCases.length}</div>
1128
- <div class="stat-label">Active</div>
1139
+ <div class="stat-label">Active Cases</div>
1129
1140
  </div>
1130
1141
  <div class="stat-item">
1131
1142
  <div class="stat-number" id="deletedCount">0</div>
1132
- <div class="stat-label">Deleted</div>
1143
+ <div class="stat-label">Deleted Cases</div>
1133
1144
  </div>
1134
1145
  </div>
1135
1146
 
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 `
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) => `
1162
1158
  <div class="test-case" data-index="${index}">
1163
1159
  <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>
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>` : ''}
1185
1164
  </div>
1165
+ <button class="btn btn-delete" onclick="toggleDelete(${index})">Delete</button>
1186
1166
  </div>
1167
+ <textarea data-index="${index}" placeholder="Enter test case details...">${testCase.title}${testCase.description ? '\n\nSteps:\n' + testCase.description : ''}</textarea>
1187
1168
  </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>
1169
+ `).join('')}
1196
1170
  </div>
1197
1171
  </div>
1198
1172
 
1199
- <div class="notification" id="notification"></div>
1200
-
1201
1173
  <script>
1202
- let testCases = ${JSON.stringify(processedTestCases)};
1174
+ let testCases = ${JSON.stringify(processedTestCases).replace(/</g, '\\u003c').replace(/>/g, '\\u003e')};
1203
1175
  let deletedIndices = new Set();
1204
1176
 
1205
1177
  function updateStats() {
@@ -1238,7 +1210,7 @@ tool(
1238
1210
  // Reset textarea value
1239
1211
  const textarea = el.querySelector('textarea');
1240
1212
  const originalTestCase = testCases[index];
1241
- const resetValue = originalTestCase.title + (originalTestCase.description ? '\n\nSteps:\n' + originalTestCase.description : '');
1213
+ const resetValue = originalTestCase.title + (originalTestCase.description ? '\\n\\nSteps:\\n' + originalTestCase.description : '');
1242
1214
  textarea.value = resetValue;
1243
1215
  });
1244
1216
  updateStats();
@@ -1285,7 +1257,7 @@ tool(
1285
1257
  body: JSON.stringify({
1286
1258
  sessionId: '${sessionId}',
1287
1259
  testCases: updatedTestCases
1288
- })
1260
+ })
1289
1261
  })
1290
1262
  .then(response => response.json())
1291
1263
  .then(data => {
@@ -1313,8 +1285,8 @@ tool(
1313
1285
  if (e.target.tagName === 'TEXTAREA') {
1314
1286
  const index = parseInt(e.target.getAttribute('data-index'));
1315
1287
  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');
1288
+ testCases[index].title = e.target.value.split('\\n\\nSteps:\\n')[0];
1289
+ const stepsIndex = e.target.value.indexOf('\\n\\nSteps:\\n');
1318
1290
  if (stepsIndex !== -1) {
1319
1291
  testCases[index].description = e.target.value.substring(stepsIndex + 9);
1320
1292
  }
@@ -1324,7 +1296,8 @@ tool(
1324
1296
  </script>
1325
1297
  </body>
1326
1298
  </html>
1327
- `);
1299
+ `;
1300
+ res.send(htmlContent);
1328
1301
  } catch (error) {
1329
1302
  console.error('Error rendering page:', error);
1330
1303
  res.status(500).send('Error rendering page');
@@ -1355,7 +1328,9 @@ tool(
1355
1328
 
1356
1329
  // Close server after approval
1357
1330
  setTimeout(() => {
1358
- server.close();
1331
+ if (server && server.listening) {
1332
+ server.close();
1333
+ }
1359
1334
  }, 3000);
1360
1335
  } catch (error) {
1361
1336
  console.error('Approval error:', error);
@@ -1374,32 +1349,22 @@ tool(
1374
1349
  res.status(404).json({ error: 'Not found' });
1375
1350
  });
1376
1351
 
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
- });
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
+ });
1386
1364
 
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;
1365
+ srv.on('error', (error) => {
1366
+ reject(error);
1367
+ });
1403
1368
  });
1404
1369
 
1405
1370
  // Open browser with proper error handling
@@ -1417,7 +1382,8 @@ tool(
1417
1382
  global.approvalSessions[sessionId] = {
1418
1383
  status: 'pending',
1419
1384
  testCases: processedTestCases,
1420
- timestamp: Date.now()
1385
+ timestamp: Date.now(),
1386
+ server: server
1421
1387
  };
1422
1388
 
1423
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.5",
3
+ "version": "3.0.6",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"