cyclecad 3.2.1 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/CLAUDE.md +155 -1
  2. package/DOCKER-SETUP-VERIFICATION.md +399 -0
  3. package/DOCKER-TESTING.md +463 -0
  4. package/FUSION360_MODULES.md +478 -0
  5. package/FUSION_MODULES_README.md +352 -0
  6. package/INTEGRATION_SNIPPETS.md +608 -0
  7. package/KILLER-FEATURES-DELIVERY.md +469 -0
  8. package/MODULES_SUMMARY.txt +337 -0
  9. package/QUICK_REFERENCE.txt +298 -0
  10. package/README-DOCKER-TESTING.txt +438 -0
  11. package/app/index.html +23 -10
  12. package/app/js/fusion-help.json +1808 -0
  13. package/app/js/help-module-v3.js +1096 -0
  14. package/app/js/killer-features-help.json +395 -0
  15. package/app/js/killer-features.js +1508 -0
  16. package/app/js/modules/fusion-assembly.js +842 -0
  17. package/app/js/modules/fusion-cam.js +785 -0
  18. package/app/js/modules/fusion-data.js +814 -0
  19. package/app/js/modules/fusion-drawing.js +844 -0
  20. package/app/js/modules/fusion-inspection.js +756 -0
  21. package/app/js/modules/fusion-render.js +774 -0
  22. package/app/js/modules/fusion-simulation.js +986 -0
  23. package/app/js/modules/fusion-sketch.js +1044 -0
  24. package/app/js/modules/fusion-solid.js +1095 -0
  25. package/app/js/modules/fusion-surface.js +949 -0
  26. package/app/tests/FUSION_TEST_SUITE.md +266 -0
  27. package/app/tests/README.md +77 -0
  28. package/app/tests/TESTING-CHECKLIST.md +177 -0
  29. package/app/tests/TEST_SUITE_SUMMARY.txt +236 -0
  30. package/app/tests/brep-live-test.html +848 -0
  31. package/app/tests/docker-integration-test.html +811 -0
  32. package/app/tests/fusion-all-tests.html +670 -0
  33. package/app/tests/fusion-assembly-tests.html +461 -0
  34. package/app/tests/fusion-cam-tests.html +421 -0
  35. package/app/tests/fusion-simulation-tests.html +421 -0
  36. package/app/tests/fusion-sketch-tests.html +613 -0
  37. package/app/tests/fusion-solid-tests.html +529 -0
  38. package/app/tests/index.html +453 -0
  39. package/app/tests/killer-features-test.html +509 -0
  40. package/app/tests/run-tests.html +874 -0
  41. package/app/tests/step-import-live-test.html +1115 -0
  42. package/app/tests/test-agent-v3.html +93 -696
  43. package/architecture-dashboard.html +1970 -0
  44. package/docs/API-REFERENCE.md +1423 -0
  45. package/docs/BREP-LIVE-TEST-GUIDE.md +453 -0
  46. package/docs/DEVELOPER-GUIDE-v3.md +795 -0
  47. package/docs/DOCKER-QUICK-TEST.md +376 -0
  48. package/docs/FUSION-FEATURES-GUIDE.md +2513 -0
  49. package/docs/FUSION-TUTORIAL.md +1203 -0
  50. package/docs/INFRASTRUCTURE-GUIDE-INDEX.md +327 -0
  51. package/docs/KEYBOARD-SHORTCUTS.md +402 -0
  52. package/docs/KILLER-FEATURES-INTEGRATION.md +412 -0
  53. package/docs/KILLER-FEATURES-SUMMARY.md +424 -0
  54. package/docs/KILLER-FEATURES-TUTORIAL.md +784 -0
  55. package/docs/KILLER-FEATURES.md +562 -0
  56. package/docs/QUICK-REFERENCE.md +282 -0
  57. package/docs/README-v3-DOCS.md +274 -0
  58. package/docs/TUTORIAL-v3.md +1190 -0
  59. package/docs/architecture-dashboard.html +1970 -0
  60. package/docs/architecture-v3.html +1038 -0
  61. package/linkedin-post-v3.md +58 -0
  62. package/package.json +1 -1
  63. package/scripts/dev-setup.sh +338 -0
  64. package/scripts/docker-health-check.sh +159 -0
  65. package/scripts/integration-test.sh +311 -0
  66. package/scripts/test-docker.sh +515 -0
@@ -0,0 +1,874 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>cycleCAD Test Runner</title>
7
+ <style>
8
+ * { margin: 0; padding: 0; box-sizing: border-box; }
9
+
10
+ body {
11
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
12
+ background: #1e1e1e;
13
+ color: #e0e0e0;
14
+ line-height: 1.6;
15
+ }
16
+
17
+ .header {
18
+ background: #2d2d30;
19
+ border-bottom: 1px solid #3e3e42;
20
+ padding: 20px;
21
+ position: sticky;
22
+ top: 0;
23
+ z-index: 100;
24
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
25
+ }
26
+
27
+ .header h1 {
28
+ font-size: 24px;
29
+ font-weight: 600;
30
+ margin-bottom: 12px;
31
+ }
32
+
33
+ .header-subtitle {
34
+ font-size: 13px;
35
+ color: #999;
36
+ }
37
+
38
+ .container {
39
+ display: grid;
40
+ grid-template-columns: 250px 1fr 350px;
41
+ gap: 0;
42
+ height: calc(100vh - 140px);
43
+ overflow: hidden;
44
+ }
45
+
46
+ .sidebar {
47
+ background: #252526;
48
+ border-right: 1px solid #3e3e42;
49
+ overflow-y: auto;
50
+ padding: 12px;
51
+ }
52
+
53
+ .test-group {
54
+ margin-bottom: 20px;
55
+ }
56
+
57
+ .test-group-title {
58
+ font-size: 11px;
59
+ text-transform: uppercase;
60
+ letter-spacing: 0.5px;
61
+ color: #999;
62
+ margin-bottom: 8px;
63
+ font-weight: 600;
64
+ padding: 8px 12px;
65
+ }
66
+
67
+ .test-item {
68
+ padding: 10px 12px;
69
+ margin-bottom: 4px;
70
+ background: #2d2d30;
71
+ border: 1px solid #3e3e42;
72
+ border-radius: 4px;
73
+ cursor: pointer;
74
+ transition: all 0.2s;
75
+ font-size: 12px;
76
+ border-left: 3px solid transparent;
77
+ }
78
+
79
+ .test-item:hover {
80
+ background: #323236;
81
+ border-color: #007acc;
82
+ border-left-color: #007acc;
83
+ }
84
+
85
+ .test-item.active {
86
+ background: #1e3a5f;
87
+ border-color: #007acc;
88
+ border-left-color: #0e639c;
89
+ color: #fff;
90
+ font-weight: 600;
91
+ }
92
+
93
+ .test-item.success {
94
+ border-left-color: #89d185;
95
+ }
96
+
97
+ .test-item.error {
98
+ border-left-color: #d13438;
99
+ }
100
+
101
+ .test-item.pending {
102
+ border-left-color: #dcdcaa;
103
+ }
104
+
105
+ .test-item-icon {
106
+ margin-right: 6px;
107
+ font-size: 10px;
108
+ }
109
+
110
+ .main-area {
111
+ display: flex;
112
+ flex-direction: column;
113
+ background: #1e1e1e;
114
+ overflow: hidden;
115
+ }
116
+
117
+ .toolbar {
118
+ background: #252526;
119
+ border-bottom: 1px solid #3e3e42;
120
+ padding: 12px 16px;
121
+ display: flex;
122
+ gap: 8px;
123
+ align-items: center;
124
+ flex-wrap: wrap;
125
+ flex-shrink: 0;
126
+ }
127
+
128
+ button {
129
+ padding: 8px 12px;
130
+ background: #0e639c;
131
+ color: white;
132
+ border: none;
133
+ border-radius: 4px;
134
+ cursor: pointer;
135
+ font-size: 13px;
136
+ font-weight: 500;
137
+ transition: background 0.2s;
138
+ }
139
+
140
+ button:hover:not(:disabled) {
141
+ background: #1177bb;
142
+ }
143
+
144
+ button:disabled {
145
+ background: #3e3e42;
146
+ color: #999;
147
+ cursor: not-allowed;
148
+ }
149
+
150
+ .btn-danger {
151
+ background: #d13438;
152
+ }
153
+
154
+ .btn-danger:hover:not(:disabled) {
155
+ background: #e81123;
156
+ }
157
+
158
+ .btn-success {
159
+ background: #107c10;
160
+ }
161
+
162
+ .btn-success:hover:not(:disabled) {
163
+ background: #13a538;
164
+ }
165
+
166
+ .content {
167
+ flex: 1;
168
+ overflow: hidden;
169
+ display: flex;
170
+ flex-direction: column;
171
+ }
172
+
173
+ .iframe-container {
174
+ flex: 1;
175
+ overflow: hidden;
176
+ }
177
+
178
+ iframe {
179
+ width: 100%;
180
+ height: 100%;
181
+ border: none;
182
+ background: white;
183
+ }
184
+
185
+ .status-panel {
186
+ background: #252526;
187
+ border-left: 1px solid #3e3e42;
188
+ padding: 16px;
189
+ width: 350px;
190
+ overflow-y: auto;
191
+ display: flex;
192
+ flex-direction: column;
193
+ gap: 16px;
194
+ }
195
+
196
+ .status-card {
197
+ background: #2d2d30;
198
+ padding: 12px;
199
+ border-radius: 4px;
200
+ border-left: 3px solid #007acc;
201
+ }
202
+
203
+ .status-card h3 {
204
+ font-size: 12px;
205
+ text-transform: uppercase;
206
+ letter-spacing: 0.5px;
207
+ color: #999;
208
+ margin-bottom: 8px;
209
+ font-weight: 600;
210
+ }
211
+
212
+ .status-value {
213
+ font-size: 20px;
214
+ font-weight: bold;
215
+ color: #e0e0e0;
216
+ font-family: 'Courier New', monospace;
217
+ margin-bottom: 4px;
218
+ }
219
+
220
+ .status-subtext {
221
+ font-size: 11px;
222
+ color: #999;
223
+ }
224
+
225
+ .status-card.success {
226
+ border-left-color: #89d185;
227
+ }
228
+
229
+ .status-card.error {
230
+ border-left-color: #d13438;
231
+ }
232
+
233
+ .status-card.warning {
234
+ border-left-color: #dcdcaa;
235
+ }
236
+
237
+ .status-card.info {
238
+ border-left-color: #4fc1ff;
239
+ }
240
+
241
+ .progress-bar {
242
+ width: 100%;
243
+ height: 6px;
244
+ background: #3e3e42;
245
+ border-radius: 3px;
246
+ overflow: hidden;
247
+ margin-top: 8px;
248
+ }
249
+
250
+ .progress-fill {
251
+ height: 100%;
252
+ background: #007acc;
253
+ width: 0%;
254
+ transition: width 0.3s;
255
+ }
256
+
257
+ .summary-table {
258
+ width: 100%;
259
+ font-size: 11px;
260
+ margin-top: 8px;
261
+ }
262
+
263
+ .summary-table tr {
264
+ border-bottom: 1px solid #3e3e42;
265
+ }
266
+
267
+ .summary-table td {
268
+ padding: 4px;
269
+ text-align: right;
270
+ }
271
+
272
+ .summary-table td:first-child {
273
+ text-align: left;
274
+ color: #999;
275
+ }
276
+
277
+ .badge {
278
+ display: inline-block;
279
+ padding: 2px 6px;
280
+ border-radius: 3px;
281
+ font-size: 10px;
282
+ font-weight: 600;
283
+ text-transform: uppercase;
284
+ letter-spacing: 0.5px;
285
+ }
286
+
287
+ .badge-success {
288
+ background: #89d185;
289
+ color: #000;
290
+ }
291
+
292
+ .badge-error {
293
+ background: #d13438;
294
+ color: white;
295
+ }
296
+
297
+ .badge-pending {
298
+ background: #3e3e42;
299
+ color: #999;
300
+ }
301
+
302
+ .dashboard {
303
+ padding: 16px;
304
+ display: grid;
305
+ grid-template-columns: 1fr 1fr;
306
+ gap: 16px;
307
+ overflow-y: auto;
308
+ }
309
+
310
+ .dashboard-card {
311
+ background: #2d2d30;
312
+ padding: 16px;
313
+ border-radius: 6px;
314
+ border: 1px solid #3e3e42;
315
+ }
316
+
317
+ .dashboard-card h3 {
318
+ font-size: 13px;
319
+ font-weight: 600;
320
+ margin-bottom: 12px;
321
+ color: #e0e0e0;
322
+ }
323
+
324
+ .dashboard-item {
325
+ display: flex;
326
+ justify-content: space-between;
327
+ padding: 8px 0;
328
+ font-size: 12px;
329
+ border-bottom: 1px solid #3e3e42;
330
+ }
331
+
332
+ .dashboard-item:last-child {
333
+ border-bottom: none;
334
+ }
335
+
336
+ .dashboard-label {
337
+ color: #999;
338
+ }
339
+
340
+ .dashboard-value {
341
+ color: #e0e0e0;
342
+ font-weight: 600;
343
+ }
344
+
345
+ ::-webkit-scrollbar {
346
+ width: 10px;
347
+ height: 10px;
348
+ }
349
+
350
+ ::-webkit-scrollbar-track {
351
+ background: #1e1e1e;
352
+ }
353
+
354
+ ::-webkit-scrollbar-thumb {
355
+ background: #3e3e42;
356
+ border-radius: 5px;
357
+ }
358
+
359
+ ::-webkit-scrollbar-thumb:hover {
360
+ background: #4e4e54;
361
+ }
362
+
363
+ .hidden {
364
+ display: none !important;
365
+ }
366
+
367
+ .no-test-selected {
368
+ display: flex;
369
+ align-items: center;
370
+ justify-content: center;
371
+ flex-direction: column;
372
+ gap: 16px;
373
+ color: #999;
374
+ }
375
+
376
+ .no-test-selected-icon {
377
+ font-size: 48px;
378
+ opacity: 0.5;
379
+ }
380
+
381
+ .footer {
382
+ background: #2d2d30;
383
+ border-top: 1px solid #3e3e42;
384
+ padding: 12px 16px;
385
+ font-size: 11px;
386
+ color: #999;
387
+ }
388
+ </style>
389
+ </head>
390
+ <body>
391
+ <div class="header">
392
+ <h1>🧪 cycleCAD Test Runner</h1>
393
+ <div class="header-subtitle">Run individual test suites or all tests at once</div>
394
+ </div>
395
+
396
+ <div class="container">
397
+ <div class="sidebar" id="sidebar"></div>
398
+
399
+ <div class="main-area">
400
+ <div class="toolbar">
401
+ <button id="runAllBtn" onclick="runAllTests()">▶ Run All Tests</button>
402
+ <button id="runSelectedBtn" onclick="runSelected()" disabled>▶ Run Selected</button>
403
+ <button id="stopBtn" onclick="stopTests()" disabled class="btn-danger">⏹ Stop</button>
404
+ <button id="exportBtn" onclick="exportResults()" disabled class="btn-success">↓ Export Report</button>
405
+ <span id="runStatus" style="margin-left: auto; font-size: 12px; color: #999;">Ready</span>
406
+ </div>
407
+
408
+ <div class="content">
409
+ <div class="iframe-container" id="iframeContainer">
410
+ <div class="no-test-selected">
411
+ <div class="no-test-selected-icon">📋</div>
412
+ <div>Select a test suite from the left panel to begin</div>
413
+ <div style="font-size: 11px;">Or click "Run All Tests" to execute all suites automatically</div>
414
+ </div>
415
+ </div>
416
+ </div>
417
+ </div>
418
+
419
+ <div class="status-panel">
420
+ <div class="status-card info">
421
+ <h3>Overall Progress</h3>
422
+ <div class="progress-bar">
423
+ <div class="progress-fill" id="overallProgressFill"></div>
424
+ </div>
425
+ <table class="summary-table">
426
+ <tr>
427
+ <td>Running:</td>
428
+ <td id="countRunning">0</td>
429
+ </tr>
430
+ <tr>
431
+ <td>Passed:</td>
432
+ <td><span class="badge badge-success" id="countPassed">0</span></td>
433
+ </tr>
434
+ <tr>
435
+ <td>Failed:</td>
436
+ <td><span class="badge badge-error" id="countFailed">0</span></td>
437
+ </tr>
438
+ <tr>
439
+ <td>Pending:</td>
440
+ <td><span class="badge badge-pending" id="countPending">8</span></td>
441
+ </tr>
442
+ </table>
443
+ </div>
444
+
445
+ <div class="status-card info">
446
+ <h3>Current Test</h3>
447
+ <div id="currentTest" style="font-size: 12px; color: #999;">None</div>
448
+ </div>
449
+
450
+ <div class="status-card info">
451
+ <h3>Execution Time</h3>
452
+ <div class="status-value" id="executionTime">0s</div>
453
+ <div class="status-subtext" id="executionDetails">Waiting...</div>
454
+ </div>
455
+
456
+ <div class="status-card info">
457
+ <h3>Quick Actions</h3>
458
+ <div style="display: flex; gap: 4px; flex-direction: column;">
459
+ <button onclick="downloadResults()" style="width: 100%; padding: 6px;">Download JSON</button>
460
+ <button onclick="downloadHTML()" style="width: 100%; padding: 6px;">Download HTML</button>
461
+ <button onclick="clearResults()" style="width: 100%; padding: 6px;">Clear Results</button>
462
+ </div>
463
+ </div>
464
+ </div>
465
+ </div>
466
+
467
+ <div class="footer">
468
+ cycleCAD Test Runner v1.0 — Testing infrastructure for all modules and features
469
+ </div>
470
+
471
+ <script>
472
+ // ========== TEST SUITE DEFINITIONS ==========
473
+ const testSuites = [
474
+ {
475
+ id: 'step-import',
476
+ name: 'STEP Import Live Test',
477
+ file: 'step-import-live-test.html',
478
+ category: 'Import/Export',
479
+ description: 'Test STEP file parsing with 3 strategies',
480
+ icon: '📥'
481
+ },
482
+ {
483
+ id: 'brep',
484
+ name: 'B-Rep Operations',
485
+ file: 'brep-tests.html',
486
+ category: 'Geometry Engine',
487
+ description: 'Boolean operations and solid geometry',
488
+ icon: '🔷'
489
+ },
490
+ {
491
+ id: 'brep-live',
492
+ name: 'B-Rep Live Tests',
493
+ file: 'brep-live-test.html',
494
+ category: 'Geometry Engine',
495
+ description: 'Interactive B-Rep testing',
496
+ icon: '🔷'
497
+ },
498
+ {
499
+ id: 'collab',
500
+ name: 'Collaboration Tests',
501
+ file: 'collab-tests.html',
502
+ category: 'Features',
503
+ description: 'Multi-user editing and sync',
504
+ icon: '🤝'
505
+ },
506
+ {
507
+ id: 'billing',
508
+ name: 'Billing & Token Tests',
509
+ file: 'billing-tests.html',
510
+ category: 'Features',
511
+ description: '$CYCLE token engine and billing',
512
+ icon: '💰'
513
+ },
514
+ {
515
+ id: 'pwa',
516
+ name: 'PWA Tests',
517
+ file: 'pwa-tests.html',
518
+ category: 'Infrastructure',
519
+ description: 'Progressive Web App functionality',
520
+ icon: '📱'
521
+ },
522
+ {
523
+ id: 'mobile',
524
+ name: 'Mobile Tests',
525
+ file: 'mobile-tests.html',
526
+ category: 'Mobile',
527
+ description: 'Mobile viewer and touch controls',
528
+ icon: '📲'
529
+ },
530
+ {
531
+ id: 'ui-agent',
532
+ name: 'UI Test Agent v3',
533
+ file: 'test-agent-v3.html',
534
+ category: 'UI Automation',
535
+ description: 'Automated UI testing with live visualization',
536
+ icon: '🤖'
537
+ }
538
+ ];
539
+
540
+ // ========== STATE ==========
541
+ let currentTest = null;
542
+ let isRunning = false;
543
+ let runStartTime = 0;
544
+ let results = {};
545
+ let completedTests = [];
546
+
547
+ // ========== INITIALIZATION ==========
548
+ function initUI() {
549
+ const sidebar = document.getElementById('sidebar');
550
+ const categories = {};
551
+
552
+ // Group tests by category
553
+ testSuites.forEach(test => {
554
+ if (!categories[test.category]) {
555
+ categories[test.category] = [];
556
+ }
557
+ categories[test.category].push(test);
558
+ });
559
+
560
+ // Render categories and tests
561
+ Object.entries(categories).forEach(([cat, tests]) => {
562
+ const group = document.createElement('div');
563
+ group.className = 'test-group';
564
+
565
+ const title = document.createElement('div');
566
+ title.className = 'test-group-title';
567
+ title.textContent = cat;
568
+ group.appendChild(title);
569
+
570
+ tests.forEach(test => {
571
+ const item = document.createElement('div');
572
+ item.className = 'test-item pending';
573
+ item.id = 'test-' + test.id;
574
+ item.innerHTML = `<span class="test-item-icon">${test.icon}</span>${test.name}`;
575
+ item.title = test.description;
576
+
577
+ item.addEventListener('click', () => selectTest(test));
578
+ group.appendChild(item);
579
+ });
580
+
581
+ sidebar.appendChild(group);
582
+ });
583
+ }
584
+
585
+ // ========== TEST SELECTION ==========
586
+ function selectTest(test) {
587
+ // Deselect previous
588
+ if (currentTest) {
589
+ document.getElementById('test-' + currentTest.id).classList.remove('active');
590
+ }
591
+
592
+ currentTest = test;
593
+ document.getElementById('test-' + test.id).classList.add('active');
594
+ loadTest(test);
595
+
596
+ document.getElementById('runSelectedBtn').disabled = false;
597
+ document.getElementById('currentTest').textContent = test.name;
598
+ }
599
+
600
+ function loadTest(test) {
601
+ const container = document.getElementById('iframeContainer');
602
+ container.innerHTML = `<iframe src="${test.file}"></iframe>`;
603
+ }
604
+
605
+ // ========== TEST EXECUTION ==========
606
+ async function runAllTests() {
607
+ isRunning = true;
608
+ runStartTime = Date.now();
609
+ completedTests = [];
610
+ results = {};
611
+
612
+ document.getElementById('runAllBtn').disabled = true;
613
+ document.getElementById('stopBtn').disabled = false;
614
+ document.getElementById('runSelectedBtn').disabled = true;
615
+ document.getElementById('runStatus').textContent = 'Running all tests...';
616
+
617
+ for (const test of testSuites) {
618
+ if (!isRunning) break;
619
+
620
+ await runTest(test);
621
+ await new Promise(resolve => setTimeout(resolve, 500));
622
+ }
623
+
624
+ finishRun();
625
+ }
626
+
627
+ async function runSelected() {
628
+ if (!currentTest) return;
629
+
630
+ isRunning = true;
631
+ runStartTime = Date.now();
632
+ completedTests = [];
633
+ results = {};
634
+
635
+ document.getElementById('runAllBtn').disabled = true;
636
+ document.getElementById('stopBtn').disabled = false;
637
+ document.getElementById('runSelectedBtn').disabled = true;
638
+ document.getElementById('runStatus').textContent = `Running: ${currentTest.name}`;
639
+
640
+ await runTest(currentTest);
641
+ finishRun();
642
+ }
643
+
644
+ async function runTest(test) {
645
+ selectTest(test);
646
+ document.getElementById('countRunning').textContent = completedTests.length + 1;
647
+
648
+ // Wait for iframe to load
649
+ await new Promise(resolve => {
650
+ const iframe = document.querySelector('iframe');
651
+ if (iframe.contentWindow.runTests) {
652
+ iframe.contentWindow.runTests().then(result => {
653
+ results[test.id] = result;
654
+ completedTests.push(test.id);
655
+
656
+ // Update test item status
657
+ const item = document.getElementById('test-' + test.id);
658
+ if (result.success) {
659
+ item.classList.remove('pending', 'error');
660
+ item.classList.add('success');
661
+ } else {
662
+ item.classList.remove('pending', 'success');
663
+ item.classList.add('error');
664
+ }
665
+
666
+ resolve();
667
+ }).catch(err => {
668
+ results[test.id] = { success: false, error: err.message };
669
+ completedTests.push(test.id);
670
+
671
+ const item = document.getElementById('test-' + test.id);
672
+ item.classList.remove('pending', 'success');
673
+ item.classList.add('error');
674
+
675
+ resolve();
676
+ });
677
+ } else {
678
+ setTimeout(() => {
679
+ results[test.id] = { success: false, error: 'Test runner not available' };
680
+ completedTests.push(test.id);
681
+ resolve();
682
+ }, 2000);
683
+ }
684
+ });
685
+
686
+ updateProgress();
687
+ }
688
+
689
+ function stopTests() {
690
+ isRunning = false;
691
+ document.getElementById('stopBtn').disabled = true;
692
+ document.getElementById('runStatus').textContent = 'Stopped';
693
+ finishRun();
694
+ }
695
+
696
+ function finishRun() {
697
+ isRunning = false;
698
+ const elapsed = Math.round((Date.now() - runStartTime) / 1000);
699
+
700
+ document.getElementById('runAllBtn').disabled = false;
701
+ document.getElementById('stopBtn').disabled = true;
702
+ document.getElementById('runSelectedBtn').disabled = currentTest ? false : true;
703
+ document.getElementById('exportBtn').disabled = false;
704
+ document.getElementById('executionTime').textContent = elapsed + 's';
705
+ document.getElementById('executionDetails').textContent = `${completedTests.length} of ${testSuites.length} completed`;
706
+ document.getElementById('runStatus').textContent = 'Complete';
707
+
708
+ updateProgress();
709
+ }
710
+
711
+ function updateProgress() {
712
+ const total = testSuites.length;
713
+ const passed = Object.values(results).filter(r => r.success).length;
714
+ const failed = Object.values(results).filter(r => !r.success).length;
715
+ const pending = total - completedTests.length;
716
+
717
+ const percent = total > 0 ? Math.round((completedTests.length / total) * 100) : 0;
718
+
719
+ document.getElementById('overallProgressFill').style.width = percent + '%';
720
+ document.getElementById('countPassed').textContent = passed;
721
+ document.getElementById('countFailed').textContent = failed;
722
+ document.getElementById('countPending').textContent = pending;
723
+ }
724
+
725
+ // ========== EXPORT ==========
726
+ function exportResults() {
727
+ const json = JSON.stringify(results, null, 2);
728
+ const html = generateHTMLReport();
729
+
730
+ // Show both in a new window
731
+ const win = window.open('', '', 'width=800,height=600');
732
+ win.document.write(`
733
+ <!DOCTYPE html>
734
+ <html>
735
+ <head>
736
+ <title>Test Results Report</title>
737
+ <style>
738
+ body { font-family: monospace; background: #1e1e1e; color: #e0e0e0; padding: 20px; }
739
+ h1 { margin-bottom: 20px; }
740
+ .json { background: #252526; padding: 16px; border-radius: 4px; overflow-x: auto; }
741
+ .json code { color: #4fc1ff; }
742
+ .summary { background: #252526; padding: 16px; margin-top: 20px; border-radius: 4px; }
743
+ .passed { color: #89d185; }
744
+ .failed { color: #d13438; }
745
+ </style>
746
+ </head>
747
+ <body>
748
+ <h1>Test Results Report</h1>
749
+ <div class="summary">
750
+ <div>Total Tests: ${testSuites.length}</div>
751
+ <div class="passed">Passed: ${Object.values(results).filter(r => r.success).length}</div>
752
+ <div class="failed">Failed: ${Object.values(results).filter(r => !r.success).length}</div>
753
+ <div>Completed At: ${new Date().toISOString()}</div>
754
+ </div>
755
+ <h2 style="margin-top: 30px;">JSON Results</h2>
756
+ <pre class="json"><code>${json}</code></pre>
757
+ </body>
758
+ </html>
759
+ `);
760
+ win.document.close();
761
+ }
762
+
763
+ function generateHTMLReport() {
764
+ const passed = Object.values(results).filter(r => r.success).length;
765
+ const failed = Object.values(results).filter(r => !r.success).length;
766
+
767
+ return `
768
+ <!DOCTYPE html>
769
+ <html>
770
+ <head>
771
+ <title>cycleCAD Test Report</title>
772
+ <style>
773
+ body { font-family: 'Segoe UI', sans-serif; background: #f5f5f5; padding: 40px; }
774
+ .header { background: white; padding: 20px; border-radius: 4px; margin-bottom: 20px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
775
+ .summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 16px; margin-bottom: 20px; }
776
+ .summary-card { background: white; padding: 16px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); text-align: center; }
777
+ .summary-card .value { font-size: 32px; font-weight: bold; }
778
+ .summary-card .label { font-size: 12px; color: #666; text-transform: uppercase; margin-top: 8px; }
779
+ .passed { color: #107c10; }
780
+ .failed { color: #e81123; }
781
+ .results { background: white; padding: 20px; border-radius: 4px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
782
+ .result-item { padding: 12px; border-bottom: 1px solid #eee; }
783
+ .result-item:last-child { border-bottom: none; }
784
+ .result-status { display: inline-block; padding: 4px 8px; border-radius: 3px; font-size: 11px; font-weight: bold; margin-right: 8px; }
785
+ .result-pass { background: #d4edda; color: #155724; }
786
+ .result-fail { background: #f8d7da; color: #721c24; }
787
+ </style>
788
+ </head>
789
+ <body>
790
+ <div class="header">
791
+ <h1>cycleCAD Test Results Report</h1>
792
+ <p>Generated: ${new Date().toLocaleString()}</p>
793
+ </div>
794
+
795
+ <div class="summary">
796
+ <div class="summary-card">
797
+ <div class="value">${testSuites.length}</div>
798
+ <div class="label">Total Tests</div>
799
+ </div>
800
+ <div class="summary-card">
801
+ <div class="value passed">${passed}</div>
802
+ <div class="label">Passed</div>
803
+ </div>
804
+ <div class="summary-card">
805
+ <div class="value failed">${failed}</div>
806
+ <div class="label">Failed</div>
807
+ </div>
808
+ </div>
809
+
810
+ <div class="results">
811
+ <h2 style="margin-bottom: 16px;">Test Results</h2>
812
+ ${testSuites.map(test => {
813
+ const result = results[test.id];
814
+ const passed = result && result.success;
815
+ return `
816
+ <div class="result-item">
817
+ <span class="result-status ${passed ? 'result-pass' : 'result-fail'}">
818
+ ${passed ? '✓ PASS' : '✗ FAIL'}
819
+ </span>
820
+ <strong>${test.name}</strong>
821
+ ${result && result.message ? `<br><small style="color: #666;">${result.message}</small>` : ''}
822
+ </div>
823
+ `;
824
+ }).join('')}
825
+ </div>
826
+ </body>
827
+ </html>
828
+ `;
829
+ }
830
+
831
+ function downloadResults() {
832
+ const json = JSON.stringify(results, null, 2);
833
+ const blob = new Blob([json], { type: 'application/json' });
834
+ const url = URL.createObjectURL(blob);
835
+ const a = document.createElement('a');
836
+ a.href = url;
837
+ a.download = `test-results-${Date.now()}.json`;
838
+ a.click();
839
+ URL.revokeObjectURL(url);
840
+ }
841
+
842
+ function downloadHTML() {
843
+ const html = generateHTMLReport();
844
+ const blob = new Blob([html], { type: 'text/html' });
845
+ const url = URL.createObjectURL(blob);
846
+ const a = document.createElement('a');
847
+ a.href = url;
848
+ a.download = `test-report-${Date.now()}.html`;
849
+ a.click();
850
+ URL.revokeObjectURL(url);
851
+ }
852
+
853
+ function clearResults() {
854
+ results = {};
855
+ completedTests = [];
856
+ testSuites.forEach(test => {
857
+ const item = document.getElementById('test-' + test.id);
858
+ item.classList.remove('success', 'error');
859
+ item.classList.add('pending');
860
+ });
861
+ updateProgress();
862
+ document.getElementById('currentTest').textContent = 'None';
863
+ document.getElementById('executionTime').textContent = '0s';
864
+ document.getElementById('executionDetails').textContent = 'Waiting...';
865
+ }
866
+
867
+ // ========== STARTUP ==========
868
+ window.addEventListener('load', () => {
869
+ initUI();
870
+ updateProgress();
871
+ });
872
+ </script>
873
+ </body>
874
+ </html>