@nbakka/mcp-appium 2.0.89 → 2.0.90

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.
@@ -3,26 +3,175 @@
3
3
  <head>
4
4
  <meta charset="UTF-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>Test Case Review</title>
6
+ <title>Test Case Review & Approval</title>
7
7
  <style>
8
- body { font-family: Arial, sans-serif; margin: 20px; background-color: #f5f5f5; }
9
- .container { max-width: 1200px; margin: 0 auto; background: white; padding: 20px; border-radius: 8px; }
10
- .section { margin-bottom: 30px; padding: 20px; border: 1px solid #ddd; border-radius: 5px; }
11
- .new { background-color: #e7f5e7; border-color: #4caf50; }
12
- .modify { background-color: #fff3cd; border-color: #ffc107; }
13
- .remove { background-color: #f8d7da; border-color: #dc3545; }
14
- .test-case { margin: 10px 0; padding: 10px; background: white; border-radius: 3px; }
15
- button { padding: 10px 20px; margin: 10px; border: none; border-radius: 5px; cursor: pointer; }
16
- .approve { background-color: #4caf50; color: white; }
17
- .cancel { background-color: #dc3545; color: white; }
18
- h2 { margin-top: 0; }
19
- .original { color: #666; text-decoration: line-through; }
20
- .modified { color: #000; font-weight: bold; }
8
+ body {
9
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
+ margin: 0;
11
+ padding: 20px;
12
+ background-color: #f5f5f5;
13
+ line-height: 1.6;
14
+ }
15
+ .container {
16
+ max-width: 1200px;
17
+ margin: 0 auto;
18
+ background: white;
19
+ padding: 30px;
20
+ border-radius: 12px;
21
+ box-shadow: 0 4px 6px rgba(0,0,0,0.1);
22
+ }
23
+ .section {
24
+ margin-bottom: 30px;
25
+ padding: 25px;
26
+ border: 2px solid #ddd;
27
+ border-radius: 8px;
28
+ position: relative;
29
+ }
30
+ .section h2 {
31
+ margin-top: 0;
32
+ margin-bottom: 20px;
33
+ padding-bottom: 10px;
34
+ border-bottom: 2px solid;
35
+ }
36
+ .new {
37
+ background-color: #e8f5e8;
38
+ border-color: #4caf50;
39
+ }
40
+ .new h2 { border-bottom-color: #4caf50; color: #2e7d32; }
41
+ .modify {
42
+ background-color: #fff8e1;
43
+ border-color: #ff9800;
44
+ }
45
+ .modify h2 { border-bottom-color: #ff9800; color: #f57c00; }
46
+ .remove {
47
+ background-color: #ffebee;
48
+ border-color: #f44336;
49
+ }
50
+ .remove h2 { border-bottom-color: #f44336; color: #c62828; }
51
+ .test-case {
52
+ margin: 15px 0;
53
+ padding: 20px;
54
+ background: white;
55
+ border-radius: 6px;
56
+ border: 1px solid #e0e0e0;
57
+ box-shadow: 0 2px 4px rgba(0,0,0,0.05);
58
+ }
59
+ .test-case-header {
60
+ display: flex;
61
+ justify-content: space-between;
62
+ align-items: center;
63
+ margin-bottom: 15px;
64
+ }
65
+ .test-case-id {
66
+ font-weight: bold;
67
+ color: #666;
68
+ font-size: 0.9em;
69
+ }
70
+ .edit-btn, .save-btn, .cancel-btn-inline {
71
+ padding: 6px 12px;
72
+ border: none;
73
+ border-radius: 4px;
74
+ cursor: pointer;
75
+ font-size: 0.85em;
76
+ margin-left: 5px;
77
+ }
78
+ .edit-btn {
79
+ background-color: #2196f3;
80
+ color: white;
81
+ }
82
+ .save-btn {
83
+ background-color: #4caf50;
84
+ color: white;
85
+ }
86
+ .cancel-btn-inline {
87
+ background-color: #757575;
88
+ color: white;
89
+ }
90
+ .original-text {
91
+ color: #999;
92
+ text-decoration: line-through;
93
+ font-style: italic;
94
+ margin-bottom: 10px;
95
+ }
96
+ .modified-text {
97
+ color: #000;
98
+ font-weight: 500;
99
+ }
100
+ .editable-area {
101
+ width: 100%;
102
+ min-height: 60px;
103
+ padding: 10px;
104
+ border: 2px solid #ddd;
105
+ border-radius: 4px;
106
+ font-family: inherit;
107
+ font-size: 14px;
108
+ resize: vertical;
109
+ }
110
+ .editable-area:focus {
111
+ outline: none;
112
+ border-color: #2196f3;
113
+ }
114
+ .button-container {
115
+ text-align: center;
116
+ margin-top: 40px;
117
+ padding-top: 30px;
118
+ border-top: 2px solid #eee;
119
+ }
120
+ .main-button {
121
+ padding: 15px 30px;
122
+ margin: 0 15px;
123
+ border: none;
124
+ border-radius: 8px;
125
+ cursor: pointer;
126
+ font-size: 16px;
127
+ font-weight: 600;
128
+ text-transform: uppercase;
129
+ letter-spacing: 0.5px;
130
+ transition: all 0.3s ease;
131
+ }
132
+ .approve {
133
+ background-color: #4caf50;
134
+ color: white;
135
+ }
136
+ .approve:hover {
137
+ background-color: #45a049;
138
+ transform: translateY(-2px);
139
+ }
140
+ .cancel {
141
+ background-color: #f44336;
142
+ color: white;
143
+ }
144
+ .cancel:hover {
145
+ background-color: #da190b;
146
+ transform: translateY(-2px);
147
+ }
148
+ .status {
149
+ margin-top: 20px;
150
+ padding: 15px;
151
+ border-radius: 6px;
152
+ text-align: center;
153
+ font-weight: 500;
154
+ }
155
+ .status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
156
+ .status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
157
+ .status.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
158
+ #loading {
159
+ text-align: center;
160
+ padding: 50px;
161
+ font-size: 18px;
162
+ color: #666;
163
+ }
164
+ .empty-section {
165
+ text-align: center;
166
+ color: #999;
167
+ font-style: italic;
168
+ padding: 30px;
169
+ }
21
170
  </style>
22
171
  </head>
23
172
  <body>
24
173
  <div class="container">
25
- <h1>Test Case Review</h1>
174
+ <h1>Test Case Review & Approval</h1>
26
175
  <div id="loading">Loading test cases...</div>
27
176
  <div id="content" style="display: none;">
28
177
  <div id="new-section" class="section new">
@@ -37,9 +186,10 @@
37
186
  <h2>Test Cases to Remove</h2>
38
187
  <div id="remove-cases"></div>
39
188
  </div>
40
- <div style="text-align: center; margin-top: 30px;">
41
- <button class="approve" onclick="approve()">Approve All</button>
42
- <button class="cancel" onclick="cancel()">Cancel</button>
189
+ <div class="button-container">
190
+ <button class="main-button approve" onclick="approveTestCases()">✓ Approve Test Cases</button>
191
+ <button class="main-button cancel" onclick="cancelReview()">✗ Cancel Review</button>
192
+ <div id="status" class="status" style="display: none;"></div>
43
193
  </div>
44
194
  </div>
45
195
  </div>
@@ -47,10 +197,14 @@
47
197
  <script>
48
198
  let sessionId;
49
199
  let testCases;
200
+ let editingStates = {};
50
201
 
51
202
  async function loadTestCases() {
52
203
  try {
53
204
  const response = await fetch('/api/testcases');
205
+ if (!response.ok) {
206
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
207
+ }
54
208
  const data = await response.json();
55
209
  sessionId = data.sessionId;
56
210
  testCases = data.testCases;
@@ -60,71 +214,186 @@
60
214
 
61
215
  renderTestCases();
62
216
  } catch (error) {
63
- document.getElementById('loading').innerHTML = 'Error loading test cases: ' + error.message;
217
+ document.getElementById('loading').innerHTML = `<div class="status error">Error loading test cases: ${error.message}</div>`;
64
218
  }
65
219
  }
66
220
 
67
221
  function renderTestCases() {
68
- // Render new test cases
69
- const newCases = document.getElementById('new-cases');
222
+ renderNewCases();
223
+ renderModifyCases();
224
+ renderRemoveCases();
225
+ }
226
+
227
+ function renderNewCases() {
228
+ const container = document.getElementById('new-cases');
70
229
  if (testCases.new && testCases.new.length > 0) {
71
- newCases.innerHTML = testCases.new.map(tc =>
72
- `<div class="test-case"><strong>ID:</strong> ${tc.id}<br><strong>Description:</strong> ${tc.description}</div>`
73
- ).join('');
230
+ container.innerHTML = testCases.new.map((tc, index) => `
231
+ <div class="test-case">
232
+ <div class="test-case-header">
233
+ <span class="test-case-id">ID: ${tc.id}</span>
234
+ <button class="edit-btn" onclick="toggleEdit('new', ${index})">Edit</button>
235
+ </div>
236
+ <div id="new-${index}-display" style="display: block;">
237
+ <strong>Description:</strong> ${tc.description}
238
+ </div>
239
+ <div id="new-${index}-edit" style="display: none;">
240
+ <label><strong>Description:</strong></label>
241
+ <textarea class="editable-area" id="new-${index}-desc">${tc.description}</textarea>
242
+ <div style="margin-top: 10px;">
243
+ <button class="save-btn" onclick="saveEdit('new', ${index})">Save</button>
244
+ <button class="cancel-btn-inline" onclick="cancelEdit('new', ${index})">Cancel</button>
245
+ </div>
246
+ </div>
247
+ </div>
248
+ `).join('');
74
249
  } else {
75
- newCases.innerHTML = '<p>No new test cases</p>';
250
+ container.innerHTML = '<div class="empty-section">No new test cases</div>';
76
251
  }
252
+ }
77
253
 
78
- // Render modified test cases
79
- const modifyCases = document.getElementById('modify-cases');
254
+ function renderModifyCases() {
255
+ const container = document.getElementById('modify-cases');
80
256
  if (testCases.modify && testCases.modify.length > 0) {
81
- modifyCases.innerHTML = testCases.modify.map(tc =>
82
- `<div class="test-case">
83
- <strong>ID:</strong> ${tc.id}<br>
84
- <strong>Original:</strong> <span class="original">${tc.original}</span><br>
85
- <strong>Modified:</strong> <span class="modified">${tc.modified}</span>
86
- </div>`
87
- ).join('');
257
+ container.innerHTML = testCases.modify.map((tc, index) => `
258
+ <div class="test-case">
259
+ <div class="test-case-header">
260
+ <span class="test-case-id">ID: ${tc.id}</span>
261
+ <button class="edit-btn" onclick="toggleEdit('modify', ${index})">Edit</button>
262
+ </div>
263
+ <div id="modify-${index}-display" style="display: block;">
264
+ <div class="original-text"><strong>Original:</strong> ${tc.original}</div>
265
+ <div class="modified-text"><strong>Modified:</strong> ${tc.modified}</div>
266
+ </div>
267
+ <div id="modify-${index}-edit" style="display: none;">
268
+ <label><strong>Original:</strong></label>
269
+ <textarea class="editable-area" id="modify-${index}-orig" readonly style="background-color: #f5f5f5;">${tc.original}</textarea>
270
+ <label style="margin-top: 10px; display: block;"><strong>Modified:</strong></label>
271
+ <textarea class="editable-area" id="modify-${index}-mod">${tc.modified}</textarea>
272
+ <div style="margin-top: 10px;">
273
+ <button class="save-btn" onclick="saveEdit('modify', ${index})">Save</button>
274
+ <button class="cancel-btn-inline" onclick="cancelEdit('modify', ${index})">Cancel</button>
275
+ </div>
276
+ </div>
277
+ </div>
278
+ `).join('');
88
279
  } else {
89
- modifyCases.innerHTML = '<p>No modified test cases</p>';
280
+ container.innerHTML = '<div class="empty-section">No modified test cases</div>';
90
281
  }
282
+ }
91
283
 
92
- // Render remove test cases
93
- const removeCases = document.getElementById('remove-cases');
284
+ function renderRemoveCases() {
285
+ const container = document.getElementById('remove-cases');
94
286
  if (testCases.remove && testCases.remove.length > 0) {
95
- removeCases.innerHTML = testCases.remove.map(tc =>
96
- `<div class="test-case"><strong>ID:</strong> ${tc.id}<br><strong>Description:</strong> ${tc.description}</div>`
97
- ).join('');
287
+ container.innerHTML = testCases.remove.map((tc, index) => `
288
+ <div class="test-case">
289
+ <div class="test-case-header">
290
+ <span class="test-case-id">ID: ${tc.id}</span>
291
+ </div>
292
+ <div>
293
+ <strong>Description:</strong> ${tc.description}
294
+ <div style="margin-top: 10px; color: #c62828; font-weight: 500;">
295
+ ⚠️ This test case will be removed
296
+ </div>
297
+ </div>
298
+ </div>
299
+ `).join('');
98
300
  } else {
99
- removeCases.innerHTML = '<p>No test cases to remove</p>';
301
+ container.innerHTML = '<div class="empty-section">No test cases to remove</div>';
100
302
  }
101
303
  }
102
304
 
103
- async function approve() {
305
+ function toggleEdit(type, index) {
306
+ const displayDiv = document.getElementById(`${type}-${index}-display`);
307
+ const editDiv = document.getElementById(`${type}-${index}-edit`);
308
+
309
+ if (displayDiv.style.display === 'block') {
310
+ displayDiv.style.display = 'none';
311
+ editDiv.style.display = 'block';
312
+ editingStates[`${type}-${index}`] = true;
313
+ }
314
+ }
315
+
316
+ function saveEdit(type, index) {
317
+ if (type === 'new') {
318
+ const newDesc = document.getElementById(`new-${index}-desc`).value.trim();
319
+ if (newDesc) {
320
+ testCases.new[index].description = newDesc;
321
+ }
322
+ } else if (type === 'modify') {
323
+ const newMod = document.getElementById(`modify-${index}-mod`).value.trim();
324
+ if (newMod) {
325
+ testCases.modify[index].modified = newMod;
326
+ }
327
+ }
328
+
329
+ cancelEdit(type, index);
330
+ renderTestCases();
331
+ showStatus('Changes saved successfully!', 'success');
332
+ }
333
+
334
+ function cancelEdit(type, index) {
335
+ const displayDiv = document.getElementById(`${type}-${index}-display`);
336
+ const editDiv = document.getElementById(`${type}-${index}-edit`);
337
+
338
+ displayDiv.style.display = 'block';
339
+ editDiv.style.display = 'none';
340
+ delete editingStates[`${type}-${index}`];
341
+ }
342
+
343
+ function showStatus(message, type) {
344
+ const statusDiv = document.getElementById('status');
345
+ statusDiv.textContent = message;
346
+ statusDiv.className = `status ${type}`;
347
+ statusDiv.style.display = 'block';
348
+
349
+ setTimeout(() => {
350
+ statusDiv.style.display = 'none';
351
+ }, 3000);
352
+ }
353
+
354
+ async function approveTestCases() {
104
355
  try {
356
+ showStatus('Processing approval...', 'info');
105
357
  const response = await fetch(`/approve/${sessionId}`, {
106
358
  method: 'POST',
107
359
  headers: { 'Content-Type': 'application/json' },
108
360
  body: JSON.stringify(testCases)
109
361
  });
362
+
363
+ if (!response.ok) {
364
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
365
+ }
366
+
110
367
  const result = await response.json();
111
- alert(result.message);
112
- window.close();
368
+ showStatus(result.message || 'Test cases approved successfully!', 'success');
369
+
370
+ setTimeout(() => {
371
+ window.close();
372
+ }, 2000);
113
373
  } catch (error) {
114
- alert('Error approving test cases: ' + error.message);
374
+ showStatus(`Error approving test cases: ${error.message}`, 'error');
115
375
  }
116
376
  }
117
377
 
118
- async function cancel() {
378
+ async function cancelReview() {
119
379
  try {
380
+ showStatus('Cancelling review...', 'info');
120
381
  const response = await fetch(`/cancel/${sessionId}`, {
121
382
  method: 'POST'
122
383
  });
384
+
385
+ if (!response.ok) {
386
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
387
+ }
388
+
123
389
  const result = await response.json();
124
- alert(result.message);
125
- window.close();
390
+ showStatus(result.message || 'Review cancelled', 'info');
391
+
392
+ setTimeout(() => {
393
+ window.close();
394
+ }, 2000);
126
395
  } catch (error) {
127
- alert('Error cancelling review: ' + error.message);
396
+ showStatus(`Error cancelling review: ${error.message}`, 'error');
128
397
  }
129
398
  }
130
399
 
package/lib/server.js CHANGED
@@ -809,48 +809,44 @@ tool(
809
809
  const path = require('path');
810
810
  const sessionId = Date.now().toString();
811
811
 
812
- // Parse test cases into categories
813
- const parsedTestCases = {
814
- new: [],
815
- modify: [],
816
- remove: []
817
- };
818
-
819
- testCases.forEach((tc, index) => {
820
- if (!tc || !Array.isArray(tc) || tc.length < 2) {
821
- console.log(`Skipping invalid test case at index ${index}:`, tc);
822
- return;
823
- }
812
+ // Add the improved parseTestCases function here
813
+ function parseTestCases(testCasesArray) {
814
+ const parsed = {
815
+ new: [],
816
+ modify: [],
817
+ remove: []
818
+ };
824
819
 
825
- const lastElement = tc[tc.length - 1];
826
- const secondLastElement = tc.length > 1 ? tc[tc.length - 2] : null;
827
-
828
- if (lastElement?.toLowerCase() === 'new') {
829
- // Format: [description, "New"]
830
- parsedTestCases.new.push({
831
- description: tc[0],
832
- id: `NEW-${index + 1}`
833
- });
834
- } else if (secondLastElement?.toLowerCase() === 'modify' && tc.length === 4) {
835
- // Format: [originalDescription, newDescription, "Modify", testCaseId]
836
- parsedTestCases.modify.push({
837
- id: tc[3],
838
- original: tc[0],
839
- modified: tc[1]
840
- });
841
- } else if (secondLastElement?.toLowerCase() === 'remove' && tc.length === 3) {
842
- // Format: [description, "Remove", testCaseId]
843
- parsedTestCases.remove.push({
844
- id: tc[2],
845
- description: tc[0]
846
- });
847
- } else {
848
- console.log(`Unrecognized test case format at index ${index}:`, tc);
849
- }
850
- });
820
+ testCasesArray.forEach(tc => {
821
+ if (tc.length === 4 && tc[2] === 'Modify') {
822
+ // Modify: [original, modified, "Modify", tcmsId]
823
+ parsed.modify.push({
824
+ id: tc[3],
825
+ original: tc[0],
826
+ modified: tc[1]
827
+ });
828
+ } else if (tc.length === 3 && tc[1] === 'Remove') {
829
+ // Remove: [description, "Remove", tcmsId]
830
+ parsed.remove.push({
831
+ id: tc[2],
832
+ description: tc[0]
833
+ });
834
+ } else if (tc.length === 2 && tc[1] === 'New') {
835
+ // New: [description, "New"]
836
+ parsed.new.push({
837
+ description: tc[0],
838
+ id: `NEW-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
839
+ });
840
+ }
841
+ });
851
842
 
852
- console.log('Parsed test cases:', JSON.stringify(parsedTestCases, null, 2));
843
+ return parsed;
844
+ }
853
845
 
846
+ // Use the improved parsing function
847
+ const parsedTestCases = parseTestCases(testCases);
848
+
849
+ // Remove the old parsing logic and console.log statements that cause JSON errors
854
850
  // Initialize session state
855
851
  approvalSessions.set(sessionId, {
856
852
  status: 'pending',
@@ -901,12 +897,12 @@ tool(
901
897
  });
902
898
 
903
899
  const server = app.listen(port, async () => {
904
- console.log(`Test case review server started on http://localhost:${port}`);
900
+ // Remove console.log to prevent JSON parsing errors
905
901
  try {
906
902
  const { default: open } = await import('open');
907
903
  await open(`http://localhost:${port}`);
908
904
  } catch (openError) {
909
- console.log('Failed to open browser automatically:', openError.message);
905
+ // Silent fail for browser opening
910
906
  }
911
907
  });
912
908
 
@@ -925,23 +921,21 @@ tool(
925
921
  }
926
922
  }, 300000);
927
923
 
928
- const response = {
924
+ // Return clean JSON response
925
+ return JSON.stringify({
929
926
  status: "review_started",
930
927
  sessionId: sessionId,
931
928
  message: "Test case review interface opened in browser. Use check_approval_status tool to poll for approval.",
932
929
  testCasesCount: testCases.length,
933
930
  browserUrl: `http://localhost:${port}`,
934
931
  instructions: "Poll every 25 seconds using check_approval_status tool until approved or timeout (5 minutes)"
935
- };
936
-
937
- return JSON.stringify(response);
932
+ });
938
933
 
939
934
  } catch (err) {
940
- const errorResponse = {
935
+ return JSON.stringify({
941
936
  status: "error",
942
937
  message: `Error setting up review interface: ${err.message}`
943
- };
944
- return JSON.stringify(errorResponse);
938
+ });
945
939
  }
946
940
  }
947
941
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.89",
3
+ "version": "2.0.90",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"