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,670 @@
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 Master Test Runner</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', sans-serif;
16
+ background: #0f172a;
17
+ color: #e2e8f0;
18
+ padding: 24px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1400px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ .header {
27
+ margin-bottom: 32px;
28
+ }
29
+
30
+ .title {
31
+ font-size: 32px;
32
+ font-weight: 700;
33
+ margin-bottom: 8px;
34
+ color: #38bdf8;
35
+ }
36
+
37
+ .subtitle {
38
+ font-size: 14px;
39
+ color: #94a3b8;
40
+ }
41
+
42
+ .controls {
43
+ display: flex;
44
+ gap: 12px;
45
+ margin-bottom: 24px;
46
+ }
47
+
48
+ button {
49
+ padding: 10px 16px;
50
+ background: #0ea5e9;
51
+ color: white;
52
+ border: none;
53
+ border-radius: 6px;
54
+ cursor: pointer;
55
+ font-size: 13px;
56
+ font-weight: 500;
57
+ transition: background 0.2s;
58
+ }
59
+
60
+ button:hover {
61
+ background: #0284c7;
62
+ }
63
+
64
+ button.secondary {
65
+ background: #475569;
66
+ }
67
+
68
+ button.secondary:hover {
69
+ background: #64748b;
70
+ }
71
+
72
+ button:disabled {
73
+ opacity: 0.5;
74
+ cursor: not-allowed;
75
+ }
76
+
77
+ .progress-section {
78
+ background: #1a202c;
79
+ border: 1px solid #2d3748;
80
+ border-radius: 8px;
81
+ padding: 24px;
82
+ margin-bottom: 24px;
83
+ }
84
+
85
+ .progress-header {
86
+ display: flex;
87
+ justify-content: space-between;
88
+ align-items: center;
89
+ margin-bottom: 16px;
90
+ }
91
+
92
+ .progress-label {
93
+ font-size: 14px;
94
+ font-weight: 600;
95
+ color: #cbd5e1;
96
+ }
97
+
98
+ .progress-time {
99
+ font-size: 13px;
100
+ color: #94a3b8;
101
+ }
102
+
103
+ .progress-bar {
104
+ width: 100%;
105
+ height: 8px;
106
+ background: #0f172a;
107
+ border-radius: 4px;
108
+ overflow: hidden;
109
+ margin-bottom: 16px;
110
+ }
111
+
112
+ .progress-fill {
113
+ height: 100%;
114
+ background: linear-gradient(90deg, #10b981, #38bdf8, #f59e0b);
115
+ width: 0%;
116
+ transition: width 0.3s;
117
+ }
118
+
119
+ .progress-stats {
120
+ display: grid;
121
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
122
+ gap: 12px;
123
+ }
124
+
125
+ .stat-card {
126
+ padding: 12px;
127
+ background: #0f172a;
128
+ border-radius: 6px;
129
+ border-left: 3px solid #64748b;
130
+ }
131
+
132
+ .stat-card.pass {
133
+ border-left-color: #10b981;
134
+ }
135
+
136
+ .stat-card.fail {
137
+ border-left-color: #ef4444;
138
+ }
139
+
140
+ .stat-card.skip {
141
+ border-left-color: #f59e0b;
142
+ }
143
+
144
+ .stat-label {
145
+ font-size: 11px;
146
+ color: #94a3b8;
147
+ margin-bottom: 4px;
148
+ }
149
+
150
+ .stat-value {
151
+ font-size: 24px;
152
+ font-weight: 700;
153
+ color: #e2e8f0;
154
+ }
155
+
156
+ .test-suite-grid {
157
+ display: grid;
158
+ grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
159
+ gap: 16px;
160
+ margin-bottom: 24px;
161
+ }
162
+
163
+ .test-suite-card {
164
+ background: #1a202c;
165
+ border: 1px solid #2d3748;
166
+ border-radius: 8px;
167
+ padding: 16px;
168
+ cursor: pointer;
169
+ transition: all 0.2s;
170
+ }
171
+
172
+ .test-suite-card:hover {
173
+ border-color: #475569;
174
+ background: #1e293b;
175
+ }
176
+
177
+ .test-suite-card.running {
178
+ border-color: #0284c7;
179
+ background: #1e293b;
180
+ }
181
+
182
+ .test-suite-card.completed {
183
+ border-color: #10b981;
184
+ }
185
+
186
+ .test-suite-card.failed {
187
+ border-color: #ef4444;
188
+ }
189
+
190
+ .suite-name {
191
+ font-size: 16px;
192
+ font-weight: 600;
193
+ color: #38bdf8;
194
+ margin-bottom: 12px;
195
+ }
196
+
197
+ .suite-stats {
198
+ display: grid;
199
+ grid-template-columns: 1fr 1fr 1fr;
200
+ gap: 8px;
201
+ font-size: 12px;
202
+ }
203
+
204
+ .suite-stat {
205
+ padding: 8px;
206
+ background: #0f172a;
207
+ border-radius: 4px;
208
+ border-left: 2px solid #64748b;
209
+ }
210
+
211
+ .suite-stat.pass {
212
+ border-left-color: #10b981;
213
+ }
214
+
215
+ .suite-stat.fail {
216
+ border-left-color: #ef4444;
217
+ }
218
+
219
+ .suite-stat.skip {
220
+ border-left-color: #f59e0b;
221
+ }
222
+
223
+ .suite-stat-label {
224
+ font-size: 10px;
225
+ color: #94a3b8;
226
+ margin-bottom: 2px;
227
+ }
228
+
229
+ .suite-stat-value {
230
+ font-size: 14px;
231
+ font-weight: 700;
232
+ }
233
+
234
+ .suite-progress {
235
+ margin-top: 12px;
236
+ height: 4px;
237
+ background: #0f172a;
238
+ border-radius: 2px;
239
+ overflow: hidden;
240
+ }
241
+
242
+ .suite-progress-fill {
243
+ height: 100%;
244
+ background: linear-gradient(90deg, #10b981, #0284c7);
245
+ width: 0%;
246
+ transition: width 0.3s;
247
+ }
248
+
249
+ .results-section {
250
+ background: #1a202c;
251
+ border: 1px solid #2d3748;
252
+ border-radius: 8px;
253
+ padding: 24px;
254
+ }
255
+
256
+ .results-title {
257
+ font-size: 18px;
258
+ font-weight: 600;
259
+ margin-bottom: 16px;
260
+ color: #cbd5e1;
261
+ }
262
+
263
+ .export-controls {
264
+ display: flex;
265
+ gap: 8px;
266
+ }
267
+
268
+ .results-empty {
269
+ text-align: center;
270
+ color: #64748b;
271
+ padding: 32px;
272
+ }
273
+
274
+ .footer {
275
+ text-align: center;
276
+ color: #64748b;
277
+ font-size: 12px;
278
+ margin-top: 32px;
279
+ }
280
+
281
+ .status-badge {
282
+ display: inline-block;
283
+ padding: 4px 8px;
284
+ border-radius: 4px;
285
+ font-size: 11px;
286
+ font-weight: 600;
287
+ margin-left: 8px;
288
+ }
289
+
290
+ .status-badge.running {
291
+ background: #0284c7;
292
+ color: white;
293
+ }
294
+
295
+ .status-badge.completed {
296
+ background: #10b981;
297
+ color: white;
298
+ }
299
+
300
+ .status-badge.failed {
301
+ background: #ef4444;
302
+ color: white;
303
+ }
304
+
305
+ @keyframes pulse {
306
+ 0%, 100% {
307
+ opacity: 1;
308
+ }
309
+ 50% {
310
+ opacity: 0.6;
311
+ }
312
+ }
313
+
314
+ .running {
315
+ animation: pulse 1.5s ease-in-out infinite;
316
+ }
317
+ </style>
318
+ </head>
319
+ <body>
320
+ <div class="container">
321
+ <div class="header">
322
+ <div class="title">cycleCAD Master Test Runner</div>
323
+ <div class="subtitle">Comprehensive test suite for all features — sketch, solid modeling, assembly, CAM, and simulation</div>
324
+ </div>
325
+
326
+ <div class="controls">
327
+ <button onclick="runAllSuites()">Run All Tests</button>
328
+ <button class="secondary" onclick="clearResults()">Clear Results</button>
329
+ <button class="secondary" onclick="exportResults('json')">Export JSON</button>
330
+ <button class="secondary" onclick="exportResults('html')">Export HTML</button>
331
+ </div>
332
+
333
+ <div class="progress-section">
334
+ <div class="progress-header">
335
+ <div class="progress-label">Overall Progress</div>
336
+ <div class="progress-time" id="elapsedTime">0s</div>
337
+ </div>
338
+ <div class="progress-bar">
339
+ <div class="progress-fill" id="overallProgress"></div>
340
+ </div>
341
+ <div class="progress-stats">
342
+ <div class="stat-card pass">
343
+ <div class="stat-label">PASSED</div>
344
+ <div class="stat-value" id="totalPassed">0</div>
345
+ </div>
346
+ <div class="stat-card fail">
347
+ <div class="stat-label">FAILED</div>
348
+ <div class="stat-value" id="totalFailed">0</div>
349
+ </div>
350
+ <div class="stat-card skip">
351
+ <div class="stat-label">SKIPPED</div>
352
+ <div class="stat-value" id="totalSkipped">0</div>
353
+ </div>
354
+ </div>
355
+ </div>
356
+
357
+ <div class="test-suite-grid" id="suiteGrid"></div>
358
+
359
+ <div class="results-section">
360
+ <div class="results-title">Test Results</div>
361
+ <div class="export-controls">
362
+ <button onclick="exportResults('json')">Export JSON</button>
363
+ <button class="secondary" onclick="exportResults('html')">Export HTML Report</button>
364
+ </div>
365
+ <div id="resultsContainer" class="results-empty">No test results yet</div>
366
+ </div>
367
+
368
+ <div class="footer">
369
+ Running cycleCAD test suites — comprehensive feature validation
370
+ </div>
371
+ </div>
372
+
373
+ <script>
374
+ const suites = [
375
+ {
376
+ name: 'Sketch Features',
377
+ path: 'fusion-sketch-tests.html',
378
+ description: 'All sketch tools, constraints, and dimensions',
379
+ },
380
+ {
381
+ name: 'Solid Modeling',
382
+ path: 'fusion-solid-tests.html',
383
+ description: 'Basic and advanced 3D operations',
384
+ },
385
+ {
386
+ name: 'Assembly',
387
+ path: 'fusion-assembly-tests.html',
388
+ description: 'Joints, components, and assemblies',
389
+ },
390
+ {
391
+ name: 'CAM & Toolpath',
392
+ path: 'fusion-cam-tests.html',
393
+ description: 'Setup, operations, and G-code',
394
+ },
395
+ {
396
+ name: 'Simulation',
397
+ path: 'fusion-simulation-tests.html',
398
+ description: 'Stress, thermal, modal analysis',
399
+ },
400
+ ];
401
+
402
+ let results = {
403
+ suites: {},
404
+ startTime: null,
405
+ endTime: null,
406
+ };
407
+
408
+ let elapsedSeconds = 0;
409
+ let elapsedTimer = null;
410
+
411
+ function initUI() {
412
+ const grid = document.getElementById('suiteGrid');
413
+ suites.forEach((suite, idx) => {
414
+ const card = document.createElement('div');
415
+ card.id = `suite-${idx}`;
416
+ card.className = 'test-suite-card';
417
+ card.innerHTML = `
418
+ <div class="suite-name">${suite.name}</div>
419
+ <div style="font-size: 12px; color: #94a3b8; margin-bottom: 12px;">${suite.description}</div>
420
+ <div class="suite-stats">
421
+ <div class="suite-stat pass">
422
+ <div class="suite-stat-label">PASS</div>
423
+ <div class="suite-stat-value" id="stat-${idx}-pass">-</div>
424
+ </div>
425
+ <div class="suite-stat fail">
426
+ <div class="suite-stat-label">FAIL</div>
427
+ <div class="suite-stat-value" id="stat-${idx}-fail">-</div>
428
+ </div>
429
+ <div class="suite-stat skip">
430
+ <div class="suite-stat-label">SKIP</div>
431
+ <div class="suite-stat-value" id="stat-${idx}-skip">-</div>
432
+ </div>
433
+ </div>
434
+ <div class="suite-progress">
435
+ <div class="suite-progress-fill" id="progress-${idx}" style="width: 0%"></div>
436
+ </div>
437
+ `;
438
+ grid.appendChild(card);
439
+ });
440
+ }
441
+
442
+ async function runAllSuites() {
443
+ results = { suites: {}, startTime: Date.now(), endTime: null };
444
+ elapsedSeconds = 0;
445
+ clearInterval(elapsedTimer);
446
+ elapsedTimer = setInterval(() => {
447
+ elapsedSeconds++;
448
+ updateElapsedTime();
449
+ }, 1000);
450
+
451
+ for (let i = 0; i < suites.length; i++) {
452
+ await runSuite(i);
453
+ await delay(500);
454
+ }
455
+
456
+ results.endTime = Date.now();
457
+ clearInterval(elapsedTimer);
458
+ updateOverallStats();
459
+ displayResults();
460
+ }
461
+
462
+ async function runSuite(idx) {
463
+ const suite = suites[idx];
464
+ const card = document.getElementById(`suite-${idx}`);
465
+ card.classList.add('running');
466
+
467
+ results.suites[suite.name] = {
468
+ pass: 0,
469
+ fail: 0,
470
+ skip: 0,
471
+ tests: [],
472
+ };
473
+
474
+ return new Promise(resolve => {
475
+ const iframe = document.createElement('iframe');
476
+ iframe.style.display = 'none';
477
+ iframe.src = suite.path;
478
+ iframe.onload = () => {
479
+ setTimeout(() => {
480
+ try {
481
+ const iframeDoc = iframe.contentDocument || iframe.contentWindow.document;
482
+ const passCount = iframeDoc.getElementById('passCount')?.textContent || '0';
483
+ const failCount = iframeDoc.getElementById('failCount')?.textContent || '0';
484
+ const skipCount = iframeDoc.getElementById('skipCount')?.textContent || '0';
485
+
486
+ results.suites[suite.name].pass = parseInt(passCount);
487
+ results.suites[suite.name].fail = parseInt(failCount);
488
+ results.suites[suite.name].skip = parseInt(skipCount);
489
+
490
+ updateSuiteStats(idx);
491
+ card.classList.remove('running');
492
+ card.classList.add('completed');
493
+
494
+ document.body.removeChild(iframe);
495
+ resolve();
496
+ } catch (e) {
497
+ console.error('Error reading iframe results:', e);
498
+ card.classList.remove('running');
499
+ card.classList.add('failed');
500
+ resolve();
501
+ }
502
+ }, 100);
503
+ };
504
+ document.body.appendChild(iframe);
505
+ });
506
+ }
507
+
508
+ function updateSuiteStats(idx) {
509
+ const suite = suites[idx];
510
+ const suiteData = results.suites[suite.name];
511
+
512
+ document.getElementById(`stat-${idx}-pass`).textContent = suiteData.pass;
513
+ document.getElementById(`stat-${idx}-fail`).textContent = suiteData.fail;
514
+ document.getElementById(`stat-${idx}-skip`).textContent = suiteData.skip;
515
+
516
+ const total = suiteData.pass + suiteData.fail + suiteData.skip;
517
+ const percent = total > 0 ? (suiteData.pass / total) * 100 : 0;
518
+ document.getElementById(`progress-${idx}`).style.width = percent + '%';
519
+
520
+ updateOverallStats();
521
+ }
522
+
523
+ function updateOverallStats() {
524
+ let totalPass = 0, totalFail = 0, totalSkip = 0;
525
+
526
+ for (const suite in results.suites) {
527
+ totalPass += results.suites[suite].pass;
528
+ totalFail += results.suites[suite].fail;
529
+ totalSkip += results.suites[suite].skip;
530
+ }
531
+
532
+ document.getElementById('totalPassed').textContent = totalPass;
533
+ document.getElementById('totalFailed').textContent = totalFail;
534
+ document.getElementById('totalSkipped').textContent = totalSkip;
535
+
536
+ const total = totalPass + totalFail + totalSkip;
537
+ const percent = total > 0 ? (totalPass / total) * 100 : 0;
538
+ document.getElementById('overallProgress').style.width = percent + '%';
539
+ }
540
+
541
+ function updateElapsedTime() {
542
+ const mins = Math.floor(elapsedSeconds / 60);
543
+ const secs = elapsedSeconds % 60;
544
+ document.getElementById('elapsedTime').textContent = `${mins}m ${secs}s`;
545
+ }
546
+
547
+ function displayResults() {
548
+ const container = document.getElementById('resultsContainer');
549
+ let html = '<table style="width: 100%; border-collapse: collapse;">';
550
+ html += '<thead><tr style="background: #0f172a; border-bottom: 1px solid #2d3748;">';
551
+ html += '<th style="text-align: left; padding: 12px; color: #cbd5e1;">Suite</th>';
552
+ html += '<th style="text-align: center; padding: 12px; color: #10b981;">Passed</th>';
553
+ html += '<th style="text-align: center; padding: 12px; color: #ef4444;">Failed</th>';
554
+ html += '<th style="text-align: center; padding: 12px; color: #f59e0b;">Skipped</th>';
555
+ html += '</tr></thead><tbody>';
556
+
557
+ for (const suite in results.suites) {
558
+ const data = results.suites[suite];
559
+ html += `<tr style="border-bottom: 1px solid #2d3748;">
560
+ <td style="padding: 12px; color: #cbd5e1;">${suite}</td>
561
+ <td style="text-align: center; color: #10b981;">${data.pass}</td>
562
+ <td style="text-align: center; color: #ef4444;">${data.fail}</td>
563
+ <td style="text-align: center; color: #f59e0b;">${data.skip}</td>
564
+ </tr>`;
565
+ }
566
+
567
+ html += '</tbody></table>';
568
+ container.innerHTML = html;
569
+ }
570
+
571
+ function exportResults(format) {
572
+ if (format === 'json') {
573
+ const json = JSON.stringify(results, null, 2);
574
+ downloadFile(json, 'cyclecad-test-results.json', 'application/json');
575
+ } else if (format === 'html') {
576
+ const html = generateHTMLReport();
577
+ downloadFile(html, 'cyclecad-test-results.html', 'text/html');
578
+ }
579
+ }
580
+
581
+ function generateHTMLReport() {
582
+ const total = Object.values(results.suites).reduce((acc, s) => acc + s.pass + s.fail + s.skip, 0);
583
+ const passed = Object.values(results.suites).reduce((acc, s) => acc + s.pass, 0);
584
+
585
+ let html = `<!DOCTYPE html>
586
+ <html>
587
+ <head>
588
+ <title>cycleCAD Test Report</title>
589
+ <style>
590
+ body { font-family: sans-serif; background: #f5f5f5; padding: 24px; }
591
+ .header { background: #0f172a; color: white; padding: 24px; border-radius: 8px; margin-bottom: 24px; }
592
+ .title { font-size: 28px; font-weight: bold; margin-bottom: 8px; }
593
+ .subtitle { color: #cbd5e1; font-size: 14px; }
594
+ table { width: 100%; border-collapse: collapse; background: white; }
595
+ th, td { padding: 12px; text-align: left; border-bottom: 1px solid #e5e7eb; }
596
+ th { background: #f3f4f6; font-weight: 600; }
597
+ .pass { color: #10b981; }
598
+ .fail { color: #ef4444; }
599
+ </style>
600
+ </head>
601
+ <body>
602
+ <div class="header">
603
+ <div class="title">cycleCAD Test Report</div>
604
+ <div class="subtitle">Generated: ${new Date().toLocaleString()}</div>
605
+ </div>
606
+
607
+ <h2>Summary</h2>
608
+ <p>Total Tests: ${total} | <span class="pass">Passed: ${passed}</span> | <span class="fail">Failed: ${total - passed}</span></p>
609
+
610
+ <h2>Results by Suite</h2>
611
+ <table>
612
+ <thead>
613
+ <tr>
614
+ <th>Suite</th>
615
+ <th class="pass">Passed</th>
616
+ <th class="fail">Failed</th>
617
+ <th>Skipped</th>
618
+ </tr>
619
+ </thead>
620
+ <tbody>`;
621
+
622
+ for (const suite in results.suites) {
623
+ const data = results.suites[suite];
624
+ html += `<tr>
625
+ <td>${suite}</td>
626
+ <td class="pass">${data.pass}</td>
627
+ <td class="fail">${data.fail}</td>
628
+ <td>${data.skip}</td>
629
+ </tr>`;
630
+ }
631
+
632
+ html += `</tbody>
633
+ </table>
634
+ </body>
635
+ </html>`;
636
+
637
+ return html;
638
+ }
639
+
640
+ function downloadFile(content, filename, type) {
641
+ const blob = new Blob([content], { type });
642
+ const url = URL.createObjectURL(blob);
643
+ const a = document.createElement('a');
644
+ a.href = url;
645
+ a.download = filename;
646
+ a.click();
647
+ URL.revokeObjectURL(url);
648
+ }
649
+
650
+ function clearResults() {
651
+ results = { suites: {}, startTime: null, endTime: null };
652
+ suites.forEach((_, idx) => {
653
+ document.getElementById(`stat-${idx}-pass`).textContent = '-';
654
+ document.getElementById(`stat-${idx}-fail`).textContent = '-';
655
+ document.getElementById(`stat-${idx}-skip`).textContent = '-';
656
+ document.getElementById(`progress-${idx}`).style.width = '0%';
657
+ document.getElementById(`suite-${idx}`).classList.remove('running', 'completed', 'failed');
658
+ });
659
+ updateOverallStats();
660
+ document.getElementById('resultsContainer').innerHTML = '<div class="results-empty">No test results yet</div>';
661
+ }
662
+
663
+ function delay(ms) {
664
+ return new Promise(resolve => setTimeout(resolve, ms));
665
+ }
666
+
667
+ window.addEventListener('DOMContentLoaded', initUI);
668
+ </script>
669
+ </body>
670
+ </html>