@nbakka/mcp-appium 2.0.96 → 2.0.98

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.
@@ -5,371 +5,749 @@
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
6
  <title>Test Case Review & Approval</title>
7
7
  <style>
8
+ * {
9
+ box-sizing: border-box;
10
+ margin: 0;
11
+ padding: 0;
12
+ }
13
+
8
14
  body {
9
15
  font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
10
- margin: 0;
11
- padding: 20px;
12
- background-color: #f5f5f5;
16
+ background-color: #f8f9fa;
13
17
  line-height: 1.6;
18
+ color: #333;
14
19
  }
20
+
15
21
  .container {
16
- max-width: 1200px;
22
+ max-width: 1400px;
17
23
  margin: 0 auto;
24
+ padding: 20px;
25
+ }
26
+
27
+ .header {
28
+ text-align: center;
29
+ margin-bottom: 30px;
30
+ padding: 20px;
18
31
  background: white;
19
- padding: 30px;
20
- border-radius: 12px;
21
- box-shadow: 0 4px 6px rgba(0,0,0,0.1);
32
+ border-radius: 10px;
33
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
34
+ }
35
+
36
+ .header h1 {
37
+ color: #2c3e50;
38
+ margin-bottom: 10px;
22
39
  }
40
+
41
+ .session-info {
42
+ color: #7f8c8d;
43
+ font-size: 14px;
44
+ }
45
+
23
46
  .section {
24
47
  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 { background-color: #e8f5e8; border-color: #4caf50; }
37
- .new h2 { border-bottom-color: #4caf50; color: #2e7d32; }
38
- .modify { background-color: #fff8e1; border-color: #ff9800; }
39
- .modify h2 { border-bottom-color: #ff9800; color: #f57c00; }
40
- .remove { background-color: #ffebee; border-color: #f44336; }
41
- .remove h2 { border-bottom-color: #f44336; color: #c62828; }
42
- .test-case {
43
- margin: 15px 0;
44
- padding: 20px;
45
48
  background: white;
46
- border-radius: 6px;
47
- border: 1px solid #e0e0e0;
48
- box-shadow: 0 2px 4px rgba(0,0,0,0.05);
49
+ border-radius: 10px;
50
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
51
+ overflow: hidden;
52
+ }
53
+
54
+ .section-header {
55
+ padding: 20px;
56
+ font-weight: bold;
57
+ font-size: 18px;
58
+ border-bottom: 3px solid;
59
+ }
60
+
61
+ .new-section .section-header {
62
+ background-color: #d4edda;
63
+ color: #155724;
64
+ border-bottom-color: #28a745;
65
+ }
66
+
67
+ .modify-section .section-header {
68
+ background-color: #fff3cd;
69
+ color: #856404;
70
+ border-bottom-color: #ffc107;
71
+ }
72
+
73
+ .remove-section .section-header {
74
+ background-color: #f8d7da;
75
+ color: #721c24;
76
+ border-bottom-color: #dc3545;
77
+ }
78
+
79
+ .section-content {
80
+ padding: 20px;
81
+ }
82
+
83
+ .test-case {
84
+ border: 1px solid #e9ecef;
85
+ border-radius: 8px;
86
+ margin-bottom: 15px;
87
+ background: #f8f9fa;
88
+ transition: all 0.3s ease;
49
89
  }
90
+
91
+ .test-case:hover {
92
+ box-shadow: 0 4px 12px rgba(0,0,0,0.1);
93
+ }
94
+
50
95
  .test-case-header {
96
+ padding: 15px;
97
+ background: white;
98
+ border-bottom: 1px solid #e9ecef;
51
99
  display: flex;
52
100
  justify-content: space-between;
53
101
  align-items: center;
54
- margin-bottom: 15px;
102
+ border-radius: 8px 8px 0 0;
55
103
  }
104
+
56
105
  .test-case-id {
57
- font-weight: bold;
58
- color: #666;
59
- font-size: 0.9em;
106
+ font-family: 'Courier New', monospace;
107
+ background: #e9ecef;
108
+ padding: 4px 8px;
109
+ border-radius: 4px;
110
+ font-size: 12px;
111
+ color: #495057;
112
+ }
113
+
114
+ .test-case-actions {
115
+ display: flex;
116
+ gap: 8px;
60
117
  }
61
- .edit-btn, .save-btn, .cancel-btn-inline, .delete-btn {
62
- padding: 6px 12px;
118
+
119
+ .btn {
120
+ padding: 8px 16px;
63
121
  border: none;
64
- border-radius: 4px;
122
+ border-radius: 6px;
65
123
  cursor: pointer;
66
- font-size: 0.85em;
67
- margin-left: 5px;
68
- }
69
- .edit-btn { background-color: #2196f3; color: white; }
70
- .save-btn { background-color: #4caf50; color: white; }
71
- .cancel-btn-inline { background-color: #757575; color: white; }
72
- .delete-btn { background-color: #f44336; color: white; }
73
- .delete-btn:hover { background-color: #d32f2f; }
124
+ font-size: 13px;
125
+ font-weight: 500;
126
+ transition: all 0.2s ease;
127
+ text-transform: uppercase;
128
+ letter-spacing: 0.5px;
129
+ }
130
+
131
+ .btn:hover {
132
+ transform: translateY(-1px);
133
+ box-shadow: 0 2px 8px rgba(0,0,0,0.2);
134
+ }
135
+
136
+ .btn-edit {
137
+ background: #007bff;
138
+ color: white;
139
+ }
140
+
141
+ .btn-edit:hover {
142
+ background: #0056b3;
143
+ }
144
+
145
+ .btn-delete {
146
+ background: #dc3545;
147
+ color: white;
148
+ }
149
+
150
+ .btn-delete:hover {
151
+ background: #c82333;
152
+ }
153
+
154
+ .btn-save {
155
+ background: #28a745;
156
+ color: white;
157
+ }
158
+
159
+ .btn-save:hover {
160
+ background: #218838;
161
+ }
162
+
163
+ .btn-cancel {
164
+ background: #6c757d;
165
+ color: white;
166
+ }
167
+
168
+ .btn-cancel:hover {
169
+ background: #545b62;
170
+ }
171
+
172
+ .test-case-content {
173
+ padding: 15px;
174
+ }
175
+
74
176
  .original-text {
75
- color: #999;
177
+ background: #f8d7da;
178
+ border: 1px solid #f1aeb5;
179
+ border-radius: 4px;
180
+ padding: 10px;
181
+ margin-bottom: 10px;
76
182
  text-decoration: line-through;
183
+ color: #721c24;
77
184
  font-style: italic;
78
- margin-bottom: 10px;
79
185
  }
80
- .modified-text { color: #000; font-weight: 500; }
81
- .editable-area {
82
- width: 100%;
83
- min-height: 60px;
186
+
187
+ .modified-text {
188
+ background: #d1ecf1;
189
+ border: 1px solid #b8daff;
190
+ border-radius: 4px;
191
+ padding: 10px;
192
+ color: #0c5460;
193
+ font-weight: 500;
194
+ }
195
+
196
+ .description-text {
197
+ background: #d4edda;
198
+ border: 1px solid #c3e6cb;
199
+ border-radius: 4px;
84
200
  padding: 10px;
85
- border: 2px solid #ddd;
201
+ color: #155724;
202
+ }
203
+
204
+ .remove-text {
205
+ background: #f8d7da;
206
+ border: 1px solid #f1aeb5;
86
207
  border-radius: 4px;
208
+ padding: 10px;
209
+ color: #721c24;
210
+ }
211
+
212
+ .edit-form {
213
+ background: #e9ecef;
214
+ border-radius: 6px;
215
+ padding: 15px;
216
+ }
217
+
218
+ .form-group {
219
+ margin-bottom: 15px;
220
+ }
221
+
222
+ .form-label {
223
+ display: block;
224
+ margin-bottom: 5px;
225
+ font-weight: 600;
226
+ color: #495057;
227
+ }
228
+
229
+ .form-control {
230
+ width: 100%;
231
+ padding: 10px;
232
+ border: 2px solid #ced4da;
233
+ border-radius: 6px;
87
234
  font-family: inherit;
88
235
  font-size: 14px;
89
236
  resize: vertical;
237
+ min-height: 80px;
238
+ }
239
+
240
+ .form-control:focus {
241
+ outline: none;
242
+ border-color: #007bff;
243
+ box-shadow: 0 0 0 3px rgba(0,123,255,0.1);
244
+ }
245
+
246
+ .form-control[readonly] {
247
+ background-color: #f8f9fa;
248
+ cursor: not-allowed;
249
+ }
250
+
251
+ .empty-section {
252
+ text-align: center;
253
+ padding: 40px;
254
+ color: #6c757d;
255
+ font-style: italic;
90
256
  }
91
- .editable-area:focus { outline: none; border-color: #2196f3; }
92
- .button-container {
257
+
258
+ .approval-section {
259
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
260
+ color: white;
93
261
  text-align: center;
94
- margin-top: 40px;
95
- padding-top: 30px;
96
- border-top: 2px solid #eee;
262
+ padding: 30px;
263
+ border-radius: 10px;
264
+ margin-top: 30px;
265
+ }
266
+
267
+ .approval-buttons {
268
+ display: flex;
269
+ justify-content: center;
270
+ gap: 20px;
271
+ margin-top: 20px;
97
272
  }
98
- .main-button {
273
+
274
+ .btn-primary {
275
+ background: #28a745;
276
+ color: white;
99
277
  padding: 15px 30px;
100
- margin: 0 15px;
101
- border: none;
102
- border-radius: 8px;
103
- cursor: pointer;
104
278
  font-size: 16px;
105
- font-weight: 600;
106
- text-transform: uppercase;
107
- letter-spacing: 0.5px;
108
- transition: all 0.3s ease;
279
+ font-weight: bold;
280
+ }
281
+
282
+ .btn-primary:hover {
283
+ background: #218838;
284
+ }
285
+
286
+ .btn-danger {
287
+ background: #dc3545;
288
+ color: white;
289
+ padding: 15px 30px;
290
+ font-size: 16px;
291
+ font-weight: bold;
292
+ }
293
+
294
+ .btn-danger:hover {
295
+ background: #c82333;
109
296
  }
110
- .approve { background-color: #4caf50; color: white; }
111
- .approve:hover { background-color: #45a049; transform: translateY(-2px); }
112
- .cancel { background-color: #f44336; color: white; }
113
- .cancel:hover { background-color: #da190b; transform: translateY(-2px); }
114
- .status {
297
+
298
+ .status-message {
115
299
  margin-top: 20px;
116
300
  padding: 15px;
117
301
  border-radius: 6px;
118
- text-align: center;
119
302
  font-weight: 500;
303
+ text-align: center;
304
+ }
305
+
306
+ .status-success {
307
+ background: #d4edda;
308
+ color: #155724;
309
+ border: 1px solid #c3e6cb;
310
+ }
311
+
312
+ .status-error {
313
+ background: #f8d7da;
314
+ color: #721c24;
315
+ border: 1px solid #f1aeb5;
316
+ }
317
+
318
+ .status-info {
319
+ background: #d1ecf1;
320
+ color: #0c5460;
321
+ border: 1px solid #b8daff;
120
322
  }
121
- .status.success { background-color: #d4edda; color: #155724; border: 1px solid #c3e6cb; }
122
- .status.error { background-color: #f8d7da; color: #721c24; border: 1px solid #f5c6cb; }
123
- .status.info { background-color: #d1ecf1; color: #0c5460; border: 1px solid #bee5eb; }
124
- #loading {
323
+
324
+ .loading {
125
325
  text-align: center;
126
- padding: 50px;
326
+ padding: 60px;
127
327
  font-size: 18px;
128
- color: #666;
328
+ color: #6c757d;
129
329
  }
130
- .empty-section {
131
- text-align: center;
132
- color: #999;
133
- font-style: italic;
134
- padding: 30px;
330
+
331
+ .loading::after {
332
+ content: '...';
333
+ animation: dots 1.5s steps(5, end) infinite;
334
+ }
335
+
336
+ @keyframes dots {
337
+ 0%, 20% { color: rgba(0,0,0,0); text-shadow: .25em 0 0 rgba(0,0,0,0), .5em 0 0 rgba(0,0,0,0); }
338
+ 40% { color: #6c757d; text-shadow: .25em 0 0 rgba(0,0,0,0), .5em 0 0 rgba(0,0,0,0); }
339
+ 60% { text-shadow: .25em 0 0 #6c757d, .5em 0 0 rgba(0,0,0,0); }
340
+ 80%, 100% { text-shadow: .25em 0 0 #6c757d, .5em 0 0 #6c757d; }
341
+ }
342
+
343
+ .fade-in {
344
+ animation: fadeIn 0.5s ease-in;
345
+ }
346
+
347
+ @keyframes fadeIn {
348
+ from { opacity: 0; transform: translateY(20px); }
349
+ to { opacity: 1; transform: translateY(0); }
135
350
  }
136
351
  </style>
137
352
  </head>
138
353
  <body>
139
- <div class="container">
140
- <h1>Test Case Review & Approval</h1>
141
- <div id="loading">Loading test cases...</div>
142
- <div id="content" style="display: none;">
143
- <div id="new-section" class="section new">
144
- <h2>New Test Cases</h2>
145
- <div id="new-cases"></div>
146
- </div>
147
- <div id="modify-section" class="section modify">
148
- <h2>Modified Test Cases</h2>
149
- <div id="modify-cases"></div>
354
+ <div class="container">
355
+ <div class="header">
356
+ <h1>🧪 Test Case Review & Approval</h1>
357
+ <div class="session-info" id="sessionInfo">
358
+ Loading session information...
359
+ </div>
150
360
  </div>
151
- <div id="remove-section" class="section remove">
152
- <h2>Test Cases to Remove</h2>
153
- <div id="remove-cases"></div>
361
+
362
+ <div id="loading" class="loading">
363
+ Loading test cases
154
364
  </div>
155
- <div class="button-container">
156
- <button class="main-button approve" onclick="approveTestCases()">✓ Approve Test Cases</button>
157
- <button class="main-button cancel" onclick="cancelReview()">✗ Cancel Review</button>
158
- <div id="status" class="status" style="display: none;"></div>
365
+
366
+ <div id="content" style="display: none;">
367
+ <div id="newSection" class="section new-section">
368
+ <div class="section-header">
369
+ ✨ New Test Cases (<span id="newCount">0</span>)
370
+ </div>
371
+ <div class="section-content" id="newCases">
372
+ <div class="empty-section">No new test cases</div>
373
+ </div>
374
+ </div>
375
+
376
+ <div id="modifySection" class="section modify-section">
377
+ <div class="section-header">
378
+ 🔄 Modified Test Cases (<span id="modifyCount">0</span>)
379
+ </div>
380
+ <div class="section-content" id="modifyCases">
381
+ <div class="empty-section">No modified test cases</div>
382
+ </div>
383
+ </div>
384
+
385
+ <div id="removeSection" class="section remove-section">
386
+ <div class="section-header">
387
+ 🗑️ Test Cases to Remove (<span id="removeCount">0</span>)
388
+ </div>
389
+ <div class="section-content" id="removeCases">
390
+ <div class="empty-section">No test cases to remove</div>
391
+ </div>
392
+ </div>
393
+
394
+ <div class="approval-section">
395
+ <h2>🎯 Review Complete?</h2>
396
+ <p>Review all test cases above and make any necessary changes, then approve or cancel.</p>
397
+ <div class="approval-buttons">
398
+ <button class="btn btn-primary" onclick="approveTestCases()">
399
+ ✅ Approve All Test Cases
400
+ </button>
401
+ <button class="btn btn-danger" onclick="cancelReview()">
402
+ ❌ Cancel Review
403
+ </button>
404
+ </div>
405
+ <div id="statusMessage" class="status-message" style="display: none;"></div>
406
+ </div>
159
407
  </div>
160
408
  </div>
161
- </div>
162
-
163
- <script>
164
- let sessionId;
165
- let testCases = { new: [], modify: [], remove: [] };
166
- let editingStates = {};
167
-
168
- async function loadTestCases() {
169
- try {
170
- const response = await fetch('/api/testcases');
171
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
172
- const data = await response.json();
173
- sessionId = data.sessionId;
174
- testCases = data.testCases;
175
- document.getElementById('loading').style.display = 'none';
176
- document.getElementById('content').style.display = 'block';
177
- renderTestCases();
178
- } catch (error) {
179
- document.getElementById('loading').innerHTML = `<div class="status error">Error loading test cases: ${error.message}</div>`;
180
- }
181
- }
182
-
183
- function renderTestCases() {
184
- renderNewCases();
185
- renderModifyCases();
186
- renderRemoveCases();
187
- }
188
-
189
- function renderNewCases() {
190
- const container = document.getElementById('new-cases');
191
- if (testCases.new && testCases.new.length > 0) {
192
- container.innerHTML = testCases.new.map((tc, index) => `
193
- <div class="test-case" id="new-case-${index}">
409
+
410
+ <script>
411
+ // Global state
412
+ let sessionId = null;
413
+ let testCases = { new: [], modify: [], remove: [] };
414
+ let editingStates = new Set();
415
+
416
+ // Utility functions
417
+ function showStatus(message, type = 'info') {
418
+ const statusEl = document.getElementById('statusMessage');
419
+ statusEl.textContent = message;
420
+ statusEl.className = `status-message status-${type}`;
421
+ statusEl.style.display = 'block';
422
+
423
+ if (type === 'success') {
424
+ setTimeout(() => {
425
+ statusEl.style.display = 'none';
426
+ }, 3000);
427
+ }
428
+ }
429
+
430
+ function updateCounts() {
431
+ document.getElementById('newCount').textContent = testCases.new?.length || 0;
432
+ document.getElementById('modifyCount').textContent = testCases.modify?.length || 0;
433
+ document.getElementById('removeCount').textContent = testCases.remove?.length || 0;
434
+ }
435
+
436
+ // API functions
437
+ async function apiCall(url, options = {}) {
438
+ try {
439
+ const response = await fetch(url, {
440
+ headers: {
441
+ 'Content-Type': 'application/json',
442
+ ...options.headers
443
+ },
444
+ ...options
445
+ });
446
+
447
+ if (!response.ok) {
448
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
449
+ }
450
+
451
+ return await response.json();
452
+ } catch (error) {
453
+ console.error('API call failed:', error);
454
+ throw error;
455
+ }
456
+ }
457
+
458
+ // Load initial data
459
+ async function loadTestCases() {
460
+ try {
461
+ const data = await apiCall('/api/testcases');
462
+ sessionId = data.sessionId;
463
+ testCases = data.testCases || { new: [], modify: [], remove: [] };
464
+
465
+ document.getElementById('sessionInfo').textContent =
466
+ `Session ID: ${sessionId} | Total Cases: ${
467
+ (testCases.new?.length || 0) +
468
+ (testCases.modify?.length || 0) +
469
+ (testCases.remove?.length || 0)
470
+ }`;
471
+
472
+ document.getElementById('loading').style.display = 'none';
473
+ document.getElementById('content').style.display = 'block';
474
+ document.getElementById('content').classList.add('fade-in');
475
+
476
+ renderAllSections();
477
+ updateCounts();
478
+ } catch (error) {
479
+ document.getElementById('loading').innerHTML =
480
+ `<div class="status-message status-error">❌ Error loading test cases: ${error.message}</div>`;
481
+ }
482
+ }
483
+
484
+ // Render functions
485
+ function renderAllSections() {
486
+ renderNewCases();
487
+ renderModifyCases();
488
+ renderRemoveCases();
489
+ }
490
+
491
+ function renderNewCases() {
492
+ const container = document.getElementById('newCases');
493
+ const cases = testCases.new || [];
494
+
495
+ if (cases.length === 0) {
496
+ container.innerHTML = '<div class="empty-section">No new test cases</div>';
497
+ return;
498
+ }
499
+
500
+ container.innerHTML = cases.map((tc, index) => `
501
+ <div class="test-case" id="new-${index}">
194
502
  <div class="test-case-header">
195
- <span class="test-case-id">ID: ${tc.id}</span>
196
- <div>
197
- <button class="edit-btn" onclick="toggleEdit('new', ${index})">Edit</button>
198
- <button class="delete-btn" onclick="deleteTestCase('new', ${index})">Delete</button>
503
+ <span class="test-case-id">${tc.id}</span>
504
+ <div class="test-case-actions">
505
+ <button class="btn btn-edit" onclick="startEdit('new', ${index})">✏️ Edit</button>
506
+ <button class="btn btn-delete" onclick="deleteTestCase('new', '${tc.id}')">🗑️ Delete</button>
199
507
  </div>
200
508
  </div>
201
- <div id="new-${index}-display" style="display: block;">
202
- <strong>Description:</strong> ${tc.description}
203
- </div>
204
- <div id="new-${index}-edit" style="display: none;">
205
- <label><strong>Description:</strong></label>
206
- <textarea class="editable-area" id="new-${index}-desc">${tc.description}</textarea>
207
- <div style="margin-top: 10px;">
208
- <button class="save-btn" onclick="saveEdit('new', ${index})">Save</button>
209
- <button class="cancel-btn-inline" onclick="cancelEdit('new', ${index})">Cancel</button>
509
+ <div class="test-case-content">
510
+ <div id="new-${index}-display">
511
+ <div class="description-text">${escapeHtml(tc.description)}</div>
512
+ </div>
513
+ <div id="new-${index}-edit" style="display: none;">
514
+ <div class="edit-form">
515
+ <div class="form-group">
516
+ <label class="form-label">Description:</label>
517
+ <textarea class="form-control" id="new-${index}-desc">${escapeHtml(tc.description)}</textarea>
518
+ </div>
519
+ <div class="test-case-actions">
520
+ <button class="btn btn-save" onclick="saveEdit('new', ${index})">💾 Save</button>
521
+ <button class="btn btn-cancel" onclick="cancelEdit('new', ${index})">❌ Cancel</button>
522
+ </div>
523
+ </div>
210
524
  </div>
211
525
  </div>
212
526
  </div>
213
527
  `).join('');
214
- } else {
215
- container.innerHTML = '<div class="empty-section">No new test cases</div>';
216
528
  }
217
- }
218
529
 
219
- function renderModifyCases() {
220
- const container = document.getElementById('modify-cases');
221
- if (testCases.modify && testCases.modify.length > 0) {
222
- container.innerHTML = testCases.modify.map((tc, index) => `
223
- <div class="test-case" id="modify-case-${index}">
530
+ function renderModifyCases() {
531
+ const container = document.getElementById('modifyCases');
532
+ const cases = testCases.modify || [];
533
+
534
+ if (cases.length === 0) {
535
+ container.innerHTML = '<div class="empty-section">No modified test cases</div>';
536
+ return;
537
+ }
538
+
539
+ container.innerHTML = cases.map((tc, index) => `
540
+ <div class="test-case" id="modify-${index}">
224
541
  <div class="test-case-header">
225
- <span class="test-case-id">ID: ${tc.id}</span>
226
- <div>
227
- <button class="edit-btn" onclick="toggleEdit('modify', ${index})">Edit</button>
228
- <button class="delete-btn" onclick="deleteTestCase('modify', ${index})">Delete</button>
542
+ <span class="test-case-id">${tc.id}</span>
543
+ <div class="test-case-actions">
544
+ <button class="btn btn-edit" onclick="startEdit('modify', ${index})">✏️ Edit</button>
545
+ <button class="btn btn-delete" onclick="deleteTestCase('modify', '${tc.id}')">🗑️ Delete</button>
229
546
  </div>
230
547
  </div>
231
- <div id="modify-${index}-display" style="display: block;">
232
- <div class="original-text"><strong>Original:</strong> ${tc.original}</div>
233
- <div class="modified-text"><strong>Modified:</strong> ${tc.modified}</div>
234
- </div>
235
- <div id="modify-${index}-edit" style="display: none;">
236
- <label><strong>Original:</strong></label>
237
- <textarea class="editable-area" id="modify-${index}-orig" readonly style="background-color: #f5f5f5;">${tc.original}</textarea>
238
- <label style="margin-top: 10px; display: block;"><strong>Modified:</strong></label>
239
- <textarea class="editable-area" id="modify-${index}-mod">${tc.modified}</textarea>
240
- <div style="margin-top: 10px;">
241
- <button class="save-btn" onclick="saveEdit('modify', ${index})">Save</button>
242
- <button class="cancel-btn-inline" onclick="cancelEdit('modify', ${index})">Cancel</button>
548
+ <div class="test-case-content">
549
+ <div id="modify-${index}-display">
550
+ <div class="original-text">
551
+ <strong>Original:</strong><br>${escapeHtml(tc.original)}
552
+ </div>
553
+ <div class="modified-text">
554
+ <strong>Modified:</strong><br>${escapeHtml(tc.modified)}
555
+ </div>
556
+ </div>
557
+ <div id="modify-${index}-edit" style="display: none;">
558
+ <div class="edit-form">
559
+ <div class="form-group">
560
+ <label class="form-label">Original (Read-only):</label>
561
+ <textarea class="form-control" readonly>${escapeHtml(tc.original)}</textarea>
562
+ </div>
563
+ <div class="form-group">
564
+ <label class="form-label">Modified:</label>
565
+ <textarea class="form-control" id="modify-${index}-mod">${escapeHtml(tc.modified)}</textarea>
566
+ </div>
567
+ <div class="test-case-actions">
568
+ <button class="btn btn-save" onclick="saveEdit('modify', ${index})">💾 Save</button>
569
+ <button class="btn btn-cancel" onclick="cancelEdit('modify', ${index})">❌ Cancel</button>
570
+ </div>
571
+ </div>
243
572
  </div>
244
573
  </div>
245
574
  </div>
246
575
  `).join('');
247
- } else {
248
- container.innerHTML = '<div class="empty-section">No modified test cases</div>';
249
576
  }
250
- }
251
577
 
252
- function renderRemoveCases() {
253
- const container = document.getElementById('remove-cases');
254
- if (testCases.remove && testCases.remove.length > 0) {
255
- container.innerHTML = testCases.remove.map(tc => `
256
- <div class="test-case">
578
+ function renderRemoveCases() {
579
+ const container = document.getElementById('removeCases');
580
+ const cases = testCases.remove || [];
581
+
582
+ if (cases.length === 0) {
583
+ container.innerHTML = '<div class="empty-section">No test cases to remove</div>';
584
+ return;
585
+ }
586
+
587
+ container.innerHTML = cases.map((tc, index) => `
588
+ <div class="test-case" id="remove-${index}">
257
589
  <div class="test-case-header">
258
- <span class="test-case-id">ID: ${tc.id}</span>
590
+ <span class="test-case-id">${tc.id}</span>
591
+ <div class="test-case-actions">
592
+ <button class="btn btn-delete" onclick="deleteTestCase('remove', '${tc.id}')">🗑️ Remove</button>
593
+ </div>
259
594
  </div>
260
- <div>
261
- <strong>Description:</strong> ${tc.description}
262
- <div style="margin-top: 10px; color: #c62828; font-weight: 500;">
263
- ⚠️ This test case will be removed
595
+ <div class="test-case-content">
596
+ <div class="remove-text">
597
+ <strong>⚠️ This test case will be removed:</strong><br>
598
+ ${escapeHtml(tc.description)}
264
599
  </div>
265
600
  </div>
266
601
  </div>
267
602
  `).join('');
268
- } else {
269
- container.innerHTML = '<div class="empty-section">No test cases to remove</div>';
270
- }
271
- }
272
-
273
- function toggleEdit(type, index) {
274
- const displayDiv = document.getElementById(`${type}-${index}-display`);
275
- const editDiv = document.getElementById(`${type}-${index}-edit`);
276
- if (displayDiv && editDiv && displayDiv.style.display === 'block') {
277
- displayDiv.style.display = 'none';
278
- editDiv.style.display = 'block';
279
- editingStates[`${type}-${index}`] = true;
280
- }
281
- }
282
-
283
- function saveEdit(type, index) {
284
- if (type === 'new') {
285
- const newDesc = document.getElementById(`new-${index}-desc`).value.trim();
286
- if (newDesc) testCases.new[index].description = newDesc;
287
- } else if (type === 'modify') {
288
- const newMod = document.getElementById(`modify-${index}-mod`).value.trim();
289
- if (newMod) testCases.modify[index].modified = newMod;
290
- }
291
- cancelEdit(type, index);
292
- renderTestCases();
293
- showStatus('Changes saved successfully!', 'success');
294
- }
295
-
296
- function cancelEdit(type, index) {
297
- const displayDiv = document.getElementById(`${type}-${index}-display`);
298
- const editDiv = document.getElementById(`${type}-${index}-edit`);
299
- if (displayDiv && editDiv) {
300
- displayDiv.style.display = 'block';
301
- editDiv.style.display = 'none';
302
- delete editingStates[`${type}-${index}`];
303
- }
304
- }
305
-
306
- function showStatus(message, type) {
307
- const statusDiv = document.getElementById('status');
308
- statusDiv.textContent = message;
309
- statusDiv.className = `status ${type}`;
310
- statusDiv.style.display = 'block';
311
- setTimeout(() => { statusDiv.style.display = 'none'; }, 3000);
312
- }
313
-
314
- async function deleteTestCase(type, index) {
315
- if (!['new', 'modify'].includes(type)) return;
316
- const confirmation = confirm("Are you sure you want to delete this test case?");
317
- if (!confirmation) return;
318
- const item = testCases[type][index];
319
- if (!item) return;
320
- try {
321
- showStatus('Deleting test case...', 'info');
322
- const response = await fetch(`/delete/${sessionId}`, {
323
- method: 'POST',
324
- headers: { 'Content-Type': 'application/json' },
325
- body: JSON.stringify({ type, id: item.id })
326
- });
327
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
328
- const updated = await response.json();
329
- if (updated.testCases) {
330
- testCases = updated.testCases;
331
- } else {
332
- // fallback local update
333
- testCases[type] = testCases[type].filter(tc => tc.id !== item.id);
603
+ }
604
+
605
+ // Edit functions
606
+ function startEdit(type, index) {
607
+ const editKey = `${type}-${index}`;
608
+ if (editingStates.has(editKey)) return;
609
+
610
+ editingStates.add(editKey);
611
+ document.getElementById(`${type}-${index}-display`).style.display = 'none';
612
+ document.getElementById(`${type}-${index}-edit`).style.display = 'block';
613
+ }
614
+
615
+ function cancelEdit(type, index) {
616
+ const editKey = `${type}-${index}`;
617
+ editingStates.delete(editKey);
618
+ document.getElementById(`${type}-${index}-display`).style.display = 'block';
619
+ document.getElementById(`${type}-${index}-edit`).style.display = 'none';
620
+
621
+ // Reset form values
622
+ if (type === 'new') {
623
+ document.getElementById(`new-${index}-desc`).value = testCases.new[index].description;
624
+ } else if (type === 'modify') {
625
+ document.getElementById(`modify-${index}-mod`).value = testCases.modify[index].modified;
626
+ }
627
+ }
628
+
629
+ async function saveEdit(type, index) {
630
+ try {
631
+ const editKey = `${type}-${index}`;
632
+ let updatedData = {};
633
+
634
+ if (type === 'new') {
635
+ const newDesc = document.getElementById(`new-${index}-desc`).value.trim();
636
+ if (!newDesc) {
637
+ showStatus('Description cannot be empty', 'error');
638
+ return;
639
+ }
640
+ updatedData = { description: newDesc };
641
+ testCases.new[index].description = newDesc;
642
+ } else if (type === 'modify') {
643
+ const newMod = document.getElementById(`modify-${index}-mod`).value.trim();
644
+ if (!newMod) {
645
+ showStatus('Modified description cannot be empty', 'error');
646
+ return;
647
+ }
648
+ updatedData = { modified: newMod };
649
+ testCases.modify[index].modified = newMod;
650
+ }
651
+
652
+ // Send update to server
653
+ await apiCall(`/update/${sessionId}`, {
654
+ method: 'POST',
655
+ body: JSON.stringify({
656
+ type,
657
+ id: testCases[type][index].id,
658
+ data: updatedData
659
+ })
660
+ });
661
+
662
+ editingStates.delete(editKey);
663
+ document.getElementById(`${type}-${index}-display`).style.display = 'block';
664
+ document.getElementById(`${type}-${index}-edit`).style.display = 'none';
665
+
666
+ // Re-render the specific section
667
+ if (type === 'new') renderNewCases();
668
+ else if (type === 'modify') renderModifyCases();
669
+
670
+ showStatus('Changes saved successfully!', 'success');
671
+ } catch (error) {
672
+ showStatus(`Error saving changes: ${error.message}`, 'error');
673
+ }
674
+ }
675
+
676
+ // Delete function
677
+ async function deleteTestCase(type, id) {
678
+ if (!confirm(`Are you sure you want to delete this test case?`)) return;
679
+
680
+ try {
681
+ showStatus('Deleting test case...', 'info');
682
+
683
+ const result = await apiCall(`/delete/${sessionId}`, {
684
+ method: 'POST',
685
+ body: JSON.stringify({ type, id })
686
+ });
687
+
688
+ if (result.testCases) {
689
+ testCases = result.testCases;
690
+ renderAllSections();
691
+ updateCounts();
692
+ showStatus('Test case deleted successfully!', 'success');
693
+ }
694
+ } catch (error) {
695
+ showStatus(`Error deleting test case: ${error.message}`, 'error');
696
+ }
697
+ }
698
+
699
+ // Approval functions
700
+ async function approveTestCases() {
701
+ if (!confirm('Are you sure you want to approve all test cases? This action cannot be undone.')) return;
702
+
703
+ try {
704
+ showStatus('Processing approval...', 'info');
705
+
706
+ const result = await apiCall(`/approve/${sessionId}`, {
707
+ method: 'POST',
708
+ body: JSON.stringify(testCases)
709
+ });
710
+
711
+ showStatus('✅ Test cases approved successfully! You can close this window.', 'success');
712
+
713
+ // Disable all controls
714
+ document.querySelectorAll('button').forEach(btn => btn.disabled = true);
715
+
716
+ setTimeout(() => {
717
+ window.close();
718
+ }, 2000);
719
+ } catch (error) {
720
+ showStatus(`Error approving test cases: ${error.message}`, 'error');
721
+ }
722
+ }
723
+
724
+ async function cancelReview() {
725
+ if (!confirm('Are you sure you want to cancel this review?')) return;
726
+
727
+ try {
728
+ showStatus('Cancelling review...', 'info');
729
+
730
+ await apiCall(`/cancel/${sessionId}`, { method: 'POST' });
731
+
732
+ showStatus('Review cancelled. You can close this window.', 'info');
733
+
734
+ setTimeout(() => {
735
+ window.close();
736
+ }, 1500);
737
+ } catch (error) {
738
+ showStatus(`Error cancelling review: ${error.message}`, 'error');
334
739
  }
335
- renderTestCases();
336
- showStatus('Test case deleted successfully!', 'success');
337
- } catch (error) {
338
- showStatus(`Error deleting test case: ${error.message}`, 'error');
339
- }
340
- }
341
-
342
- async function approveTestCases() {
343
- try {
344
- showStatus('Processing approval...', 'info');
345
- const response = await fetch(`/approve/${sessionId}`, {
346
- method: 'POST',
347
- headers: { 'Content-Type': 'application/json' },
348
- body: JSON.stringify(testCases)
349
- });
350
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
351
- const result = await response.json();
352
- showStatus(result.message || 'Test cases approved successfully!', 'success');
353
- setTimeout(() => window.close(), 1200);
354
- } catch (error) {
355
- showStatus(`Error approving test cases: ${error.message}`, 'error');
356
- }
357
- }
358
-
359
- async function cancelReview() {
360
- try {
361
- showStatus('Cancelling review...', 'info');
362
- const response = await fetch(`/cancel/${sessionId}`, { method: 'POST' });
363
- if (!response.ok) throw new Error(`HTTP ${response.status}: ${response.statusText}`);
364
- const result = await response.json();
365
- showStatus(result.message || 'Review cancelled', 'info');
366
- setTimeout(() => window.close(), 1500);
367
- } catch (error) {
368
- showStatus(`Error cancelling review: ${error.message}`, 'error');
369
- }
370
- }
371
-
372
- window.onload = loadTestCases;
373
- </script>
740
+ }
741
+
742
+ // Utility function
743
+ function escapeHtml(text) {
744
+ const div = document.createElement('div');
745
+ div.textContent = text;
746
+ return div.innerHTML;
747
+ }
748
+
749
+ // Initialize when page loads
750
+ window.addEventListener('load', loadTestCases);
751
+ </script>
374
752
  </body>
375
753
  </html>
package/lib/server.js CHANGED
@@ -828,16 +828,47 @@ tool(
828
828
 
829
829
  const parsedTestCases = parseTestCases(testCases);
830
830
 
831
+ // Initialize session with proper locking mechanism
831
832
  approvalSessions.set(sessionId, {
832
833
  status: 'pending',
833
834
  testCases: parsedTestCases,
834
835
  originalTestCases: JSON.parse(JSON.stringify(parsedTestCases)),
835
836
  server: null,
836
- startTime: Date.now()
837
+ startTime: Date.now(),
838
+ locked: false
837
839
  });
838
840
 
839
841
  const app = express();
840
- const port = 3001;
842
+
843
+ // Try multiple ports if 3001 is busy
844
+ let port = 3001;
845
+ let server = null;
846
+
847
+ const tryStartServer = (portToTry) => {
848
+ return new Promise((resolve, reject) => {
849
+ const testServer = app.listen(portToTry, () => {
850
+ resolve({ server: testServer, port: portToTry });
851
+ }).on('error', (err) => {
852
+ if (err.code === 'EADDRINUSE') {
853
+ reject(err);
854
+ } else {
855
+ reject(err);
856
+ }
857
+ });
858
+ });
859
+ };
860
+
861
+ // Try ports 3001-3010
862
+ for (let i = 0; i < 10; i++) {
863
+ try {
864
+ const result = await tryStartServer(port + i);
865
+ server = result.server;
866
+ port = result.port;
867
+ break;
868
+ } catch (err) {
869
+ if (i === 9) throw new Error(`No available ports found (tried ${port}-${port + 9})`);
870
+ }
871
+ }
841
872
 
842
873
  app.use(express.json());
843
874
  app.use(express.static(path.join(__dirname, 'review-ui')));
@@ -857,50 +888,98 @@ tool(
857
888
  if (!session) return res.status(404).json({ error: 'Session not found' });
858
889
 
859
890
  const { type, id } = req.body;
860
- if (!['new', 'modify'].includes(type) || !id) {
891
+ if (!['new', 'modify', 'remove'].includes(type) || !id) {
861
892
  return res.status(400).json({ error: 'Invalid delete request' });
862
893
  }
863
894
  try {
864
- session.testCases[type] = session.testCases[type].filter(tc => tc.id !== id);
865
- approvalSessions.set(sessionId, session);
866
- return res.json({ status: 'deleted', testCases: session.testCases });
867
- } catch {
895
+ if (session.testCases[type] && Array.isArray(session.testCases[type])) {
896
+ session.testCases[type] = session.testCases[type].filter(tc => tc.id !== id);
897
+ approvalSessions.set(sessionId, session);
898
+ return res.json({ status: 'deleted', testCases: session.testCases });
899
+ } else {
900
+ return res.status(400).json({ error: `Invalid type: ${type}` });
901
+ }
902
+ } catch (error) {
903
+ console.error('Delete error:', error);
868
904
  return res.status(500).json({ error: 'Failed to delete test case' });
869
905
  }
870
906
  });
871
907
 
908
+ app.post(`/update/${sessionId}`, (req, res) => {
909
+ const session = approvalSessions.get(sessionId);
910
+ if (!session) return res.status(404).json({ error: 'Session not found' });
911
+
912
+ try {
913
+ const { type, id, data } = req.body;
914
+ if (!['new', 'modify'].includes(type) || !id || !data) {
915
+ return res.status(400).json({ error: 'Invalid update request' });
916
+ }
917
+
918
+ const items = session.testCases[type];
919
+ const index = items.findIndex(tc => tc.id === id);
920
+ if (index === -1) {
921
+ return res.status(404).json({ error: 'Test case not found' });
922
+ }
923
+
924
+ // Update the test case
925
+ if (type === 'new') {
926
+ items[index].description = data.description;
927
+ } else if (type === 'modify') {
928
+ items[index].modified = data.modified;
929
+ }
930
+
931
+ approvalSessions.set(sessionId, session);
932
+ return res.json({ status: 'updated', testCases: session.testCases });
933
+ } catch (error) {
934
+ console.error('Update error:', error);
935
+ return res.status(500).json({ error: 'Failed to update test case' });
936
+ }
937
+ });
938
+
872
939
  app.post(`/approve/${sessionId}`, (req, res) => {
873
940
  const session = approvalSessions.get(sessionId);
874
- if (session) {
941
+ if (!session) return res.status(404).json({ error: 'Session not found' });
942
+
943
+ try {
944
+ // Use atomic update with locking
945
+ session.locked = true;
875
946
  session.testCases = req.body;
876
- session.finalTestCases = req.body;
947
+ session.finalTestCases = JSON.parse(JSON.stringify(req.body)); // Deep copy
877
948
  session.status = 'approved';
949
+ session.approvedAt = Date.now();
878
950
  approvalSessions.set(sessionId, session);
951
+
952
+ console.log(`Session ${sessionId} approved at ${new Date().toISOString()}`);
953
+ res.json({ status: 'approved', message: 'Test cases approved successfully!' });
954
+ } catch (error) {
955
+ console.error('Approval error:', error);
956
+ res.status(500).json({ error: 'Failed to approve test cases' });
879
957
  }
880
- res.json({ status: 'approved', message: 'Test cases approved successfully!' });
881
958
  });
882
959
 
883
960
  app.post(`/cancel/${sessionId}`, (_req, res) => {
884
961
  const session = approvalSessions.get(sessionId);
885
962
  if (session) {
886
963
  session.status = 'cancelled';
964
+ session.cancelledAt = Date.now();
887
965
  approvalSessions.set(sessionId, session);
966
+ console.log(`Session ${sessionId} cancelled at ${new Date().toISOString()}`);
888
967
  }
889
968
  res.json({ status: 'cancelled', message: 'Review cancelled' });
890
969
  });
891
970
 
892
- const serverInstance = app.listen(port, async () => {
893
- try {
894
- const { default: open } = await import('open');
895
- await open(`http://localhost:${port}`);
896
- } catch { /* silent */ }
897
- });
898
-
971
+ // Update session with server reference
899
972
  const stored = approvalSessions.get(sessionId);
900
- stored.server = serverInstance;
973
+ stored.server = server;
901
974
  approvalSessions.set(sessionId, stored);
902
975
 
903
- // timeout safeguard
976
+ // Open browser
977
+ try {
978
+ const { default: open } = await import('open');
979
+ await open(`http://localhost:${port}`);
980
+ } catch { /* silent */ }
981
+
982
+ // Timeout safeguard - but don't override approved status
904
983
  setTimeout(() => {
905
984
  const s = approvalSessions.get(sessionId);
906
985
  if (s && s.status === 'pending') {
@@ -910,7 +989,6 @@ tool(
910
989
  }
911
990
  }, 300000);
912
991
 
913
- // Return a simple string instead of an object
914
992
  return `✅ Test case review interface opened successfully!
915
993
  Session ID: ${sessionId}
916
994
  Test Cases Count: ${testCases.length}
@@ -925,10 +1003,9 @@ Instructions: Use check_approval_status tool with session ID "${sessionId}" to p
925
1003
  }
926
1004
  );
927
1005
 
928
-
929
1006
  tool(
930
1007
  "check_approval_status",
931
- "Check the approval status of test cases review session (short-circuits if already approved)",
1008
+ "Check the approval status of test cases review session (waits 20 seconds before checking)",
932
1009
  { sessionId: zod_1.z.string().describe("Session ID from review_testcases") },
933
1010
  async ({ sessionId }) => {
934
1011
  const session = approvalSessions.get(sessionId);
@@ -937,6 +1014,7 @@ tool(
937
1014
  const now = Date.now();
938
1015
  const elapsed = Math.floor((now - session.startTime) / 1000);
939
1016
 
1017
+ // Check if already approved (short-circuit without waiting)
940
1018
  if (session.status === 'approved') {
941
1019
  const approvedTestCases = session.finalTestCases || session.testCases;
942
1020
  session.server?.close();
@@ -944,31 +1022,36 @@ tool(
944
1022
  return `✅ Test cases approved. Elapsed: ${elapsed}s\n${JSON.stringify(approvedTestCases, null, 2)}`;
945
1023
  }
946
1024
 
947
- await new Promise(r => setTimeout(r, 5000));
1025
+ // Wait 20 seconds before checking status
1026
+ await new Promise(r => setTimeout(r, 20000));
948
1027
 
1028
+ // Re-fetch session after wait
949
1029
  const refreshed = approvalSessions.get(sessionId);
950
- if (!refreshed) return `❌ Session expired`;
1030
+ if (!refreshed) return `❌ Session expired during wait`;
1031
+
1032
+ const newElapsed = Math.floor((Date.now() - refreshed.startTime) / 1000);
951
1033
 
952
1034
  if (refreshed.status === 'approved') {
953
1035
  const approvedTestCases = refreshed.finalTestCases || refreshed.testCases;
954
1036
  refreshed.server?.close();
955
1037
  approvalSessions.delete(sessionId);
956
- return `✅ Test cases approved. Elapsed: ${elapsed + 5}s\n${JSON.stringify(approvedTestCases, null, 2)}`;
1038
+ return `✅ Test cases approved. Elapsed: ${newElapsed}s\n${JSON.stringify(approvedTestCases, null, 2)}`;
957
1039
  } else if (refreshed.status === 'cancelled') {
958
1040
  refreshed.server?.close();
959
1041
  approvalSessions.delete(sessionId);
960
- return `❌ Review cancelled. Elapsed: ${elapsed + 5}s`;
961
- } else if (refreshed.status === 'timeout' || elapsed > 300) {
1042
+ return `❌ Review cancelled. Elapsed: ${newElapsed}s`;
1043
+ } else if (refreshed.status === 'timeout' || newElapsed > 300) {
962
1044
  refreshed.server?.close();
963
1045
  approvalSessions.delete(sessionId);
964
- return `⏰ Review session timed out. Elapsed: ${elapsed + 5}s`;
1046
+ return `⏰ Review session timed out. Elapsed: ${newElapsed}s`;
965
1047
  }
966
1048
 
967
- return `⏳ Still pending. Elapsed: ${elapsed + 5}s Remaining: ${Math.max(0, 300 - (elapsed + 5))}s`;
1049
+ return `⏳ Still pending. Elapsed: ${newElapsed}s Remaining: ${Math.max(0, 300 - newElapsed)}s`;
968
1050
  }
969
1051
  );
970
1052
 
971
1053
 
1054
+
972
1055
  return server;
973
1056
  };
974
1057
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.96",
3
+ "version": "2.0.98",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"