@nbakka/mcp-appium 2.0.97 → 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
@@ -888,31 +888,73 @@ tool(
888
888
  if (!session) return res.status(404).json({ error: 'Session not found' });
889
889
 
890
890
  const { type, id } = req.body;
891
- if (!['new', 'modify'].includes(type) || !id) {
891
+ if (!['new', 'modify', 'remove'].includes(type) || !id) {
892
892
  return res.status(400).json({ error: 'Invalid delete request' });
893
893
  }
894
894
  try {
895
- session.testCases[type] = session.testCases[type].filter(tc => tc.id !== id);
896
- approvalSessions.set(sessionId, session);
897
- return res.json({ status: 'deleted', testCases: session.testCases });
898
- } 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);
899
904
  return res.status(500).json({ error: 'Failed to delete test case' });
900
905
  }
901
906
  });
902
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
+
903
939
  app.post(`/approve/${sessionId}`, (req, res) => {
904
940
  const session = approvalSessions.get(sessionId);
905
941
  if (!session) return res.status(404).json({ error: 'Session not found' });
906
942
 
907
- // Use atomic update with locking
908
- session.locked = true;
909
- session.testCases = req.body;
910
- session.finalTestCases = req.body;
911
- session.status = 'approved';
912
- session.approvedAt = Date.now();
913
- approvalSessions.set(sessionId, session);
943
+ try {
944
+ // Use atomic update with locking
945
+ session.locked = true;
946
+ session.testCases = req.body;
947
+ session.finalTestCases = JSON.parse(JSON.stringify(req.body)); // Deep copy
948
+ session.status = 'approved';
949
+ session.approvedAt = Date.now();
950
+ approvalSessions.set(sessionId, session);
914
951
 
915
- res.json({ status: 'approved', message: 'Test cases approved successfully!' });
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' });
957
+ }
916
958
  });
917
959
 
918
960
  app.post(`/cancel/${sessionId}`, (_req, res) => {
@@ -921,6 +963,7 @@ tool(
921
963
  session.status = 'cancelled';
922
964
  session.cancelledAt = Date.now();
923
965
  approvalSessions.set(sessionId, session);
966
+ console.log(`Session ${sessionId} cancelled at ${new Date().toISOString()}`);
924
967
  }
925
968
  res.json({ status: 'cancelled', message: 'Review cancelled' });
926
969
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nbakka/mcp-appium",
3
- "version": "2.0.97",
3
+ "version": "2.0.98",
4
4
  "description": "Appium MCP",
5
5
  "engines": {
6
6
  "node": ">=18"