cyclecad 0.8.7 → 0.9.7

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.
@@ -0,0 +1,1494 @@
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 Agent v2</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', sans-serif;
16
+ background: #1e1e1e;
17
+ color: #e0e0e0;
18
+ height: 100vh;
19
+ overflow: hidden;
20
+ }
21
+
22
+ .container {
23
+ display: flex;
24
+ flex-direction: column;
25
+ height: 100vh;
26
+ }
27
+
28
+ .header {
29
+ background: linear-gradient(135deg, #0284C7 0%, #0369A1 100%);
30
+ padding: 12px 20px;
31
+ color: white;
32
+ font-weight: 600;
33
+ font-size: 14px;
34
+ display: flex;
35
+ justify-content: space-between;
36
+ align-items: center;
37
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.3);
38
+ }
39
+
40
+ .header-title {
41
+ display: flex;
42
+ align-items: center;
43
+ gap: 12px;
44
+ }
45
+
46
+ .progress-bar {
47
+ height: 4px;
48
+ background: rgba(255, 255, 255, 0.2);
49
+ width: 300px;
50
+ border-radius: 2px;
51
+ overflow: hidden;
52
+ margin: 0 12px;
53
+ }
54
+
55
+ .progress-fill {
56
+ height: 100%;
57
+ background: #10B981;
58
+ transition: width 0.3s ease;
59
+ width: 0%;
60
+ }
61
+
62
+ .header-buttons {
63
+ display: flex;
64
+ gap: 8px;
65
+ }
66
+
67
+ .header-buttons button {
68
+ padding: 6px 12px;
69
+ background: rgba(255, 255, 255, 0.2);
70
+ border: 1px solid rgba(255, 255, 255, 0.3);
71
+ color: white;
72
+ border-radius: 4px;
73
+ cursor: pointer;
74
+ font-size: 12px;
75
+ transition: all 0.2s;
76
+ }
77
+
78
+ .header-buttons button:hover {
79
+ background: rgba(255, 255, 255, 0.3);
80
+ border-color: rgba(255, 255, 255, 0.5);
81
+ }
82
+
83
+ .content {
84
+ display: flex;
85
+ flex: 1;
86
+ overflow: hidden;
87
+ }
88
+
89
+ .app-container {
90
+ width: 65%;
91
+ border-right: 1px solid #333;
92
+ display: flex;
93
+ flex-direction: column;
94
+ }
95
+
96
+ .app-container iframe {
97
+ flex: 1;
98
+ border: none;
99
+ background: white;
100
+ }
101
+
102
+ .test-panel {
103
+ width: 35%;
104
+ display: flex;
105
+ flex-direction: column;
106
+ background: #252525;
107
+ }
108
+
109
+ .test-panel-header {
110
+ background: #2a2a2a;
111
+ padding: 12px;
112
+ border-bottom: 1px solid #333;
113
+ font-weight: 600;
114
+ font-size: 13px;
115
+ color: #0284C7;
116
+ }
117
+
118
+ .test-list {
119
+ flex: 1;
120
+ overflow-y: auto;
121
+ padding: 12px;
122
+ }
123
+
124
+ .test-category {
125
+ margin-bottom: 12px;
126
+ border: 1px solid #333;
127
+ border-radius: 6px;
128
+ overflow: hidden;
129
+ background: #2a2a2a;
130
+ }
131
+
132
+ .category-header {
133
+ background: #333;
134
+ padding: 10px 12px;
135
+ cursor: pointer;
136
+ font-weight: 600;
137
+ font-size: 12px;
138
+ display: flex;
139
+ justify-content: space-between;
140
+ align-items: center;
141
+ user-select: none;
142
+ transition: background 0.2s;
143
+ }
144
+
145
+ .category-header:hover {
146
+ background: #3a3a3a;
147
+ }
148
+
149
+ .category-header-left {
150
+ display: flex;
151
+ align-items: center;
152
+ gap: 8px;
153
+ color: #0284C7;
154
+ }
155
+
156
+ .expand-icon {
157
+ display: inline-block;
158
+ transition: transform 0.2s;
159
+ font-size: 10px;
160
+ }
161
+
162
+ .category-header.collapsed .expand-icon {
163
+ transform: rotate(-90deg);
164
+ }
165
+
166
+ .category-run-btn {
167
+ padding: 4px 8px;
168
+ background: #0284C7;
169
+ border: none;
170
+ color: white;
171
+ border-radius: 3px;
172
+ cursor: pointer;
173
+ font-size: 11px;
174
+ transition: background 0.2s;
175
+ }
176
+
177
+ .category-run-btn:hover {
178
+ background: #0369A1;
179
+ }
180
+
181
+ .category-run-btn:disabled {
182
+ background: #555;
183
+ cursor: not-allowed;
184
+ }
185
+
186
+ .category-tests {
187
+ max-height: 500px;
188
+ overflow: hidden;
189
+ transition: max-height 0.3s ease;
190
+ }
191
+
192
+ .category-tests.collapsed {
193
+ max-height: 0;
194
+ }
195
+
196
+ .test-item {
197
+ padding: 8px 12px;
198
+ border-bottom: 1px solid #333;
199
+ font-size: 12px;
200
+ display: flex;
201
+ align-items: center;
202
+ gap: 8px;
203
+ }
204
+
205
+ .test-item:last-child {
206
+ border-bottom: none;
207
+ }
208
+
209
+ .test-icon {
210
+ font-size: 14px;
211
+ min-width: 20px;
212
+ }
213
+
214
+ .test-icon.pass { color: #10B981; }
215
+ .test-icon.fail { color: #EF4444; }
216
+ .test-icon.skip { color: #F59E0B; }
217
+ .test-icon.running { color: #3B82F6; animation: spin 1s linear infinite; }
218
+
219
+ @keyframes spin {
220
+ 0% { transform: rotate(0deg); }
221
+ 100% { transform: rotate(360deg); }
222
+ }
223
+
224
+ .test-content {
225
+ flex: 1;
226
+ display: flex;
227
+ flex-direction: column;
228
+ gap: 2px;
229
+ }
230
+
231
+ .test-name {
232
+ font-weight: 500;
233
+ color: #e0e0e0;
234
+ }
235
+
236
+ .test-detail {
237
+ font-size: 11px;
238
+ color: #999;
239
+ }
240
+
241
+ .test-time {
242
+ font-size: 11px;
243
+ color: #666;
244
+ margin-left: auto;
245
+ }
246
+
247
+ .summary {
248
+ background: #2a2a2a;
249
+ padding: 12px;
250
+ border-top: 1px solid #333;
251
+ font-size: 12px;
252
+ display: flex;
253
+ justify-content: space-between;
254
+ align-items: center;
255
+ }
256
+
257
+ .summary-stats {
258
+ display: flex;
259
+ gap: 16px;
260
+ }
261
+
262
+ .summary-stat {
263
+ display: flex;
264
+ align-items: center;
265
+ gap: 6px;
266
+ }
267
+
268
+ .summary-stat.pass { color: #10B981; }
269
+ .summary-stat.fail { color: #EF4444; }
270
+ .summary-stat.skip { color: #F59E0B; }
271
+
272
+ .export-btn {
273
+ padding: 6px 12px;
274
+ background: #0284C7;
275
+ border: none;
276
+ color: white;
277
+ border-radius: 3px;
278
+ cursor: pointer;
279
+ font-size: 12px;
280
+ transition: background 0.2s;
281
+ }
282
+
283
+ .export-btn:hover {
284
+ background: #0369A1;
285
+ }
286
+
287
+ .flash-green {
288
+ animation: flash-green-anim 0.3s ease-out;
289
+ }
290
+
291
+ @keyframes flash-green-anim {
292
+ 0% {
293
+ border: 2px solid #10B981;
294
+ box-shadow: 0 0 8px rgba(16, 185, 129, 0.6);
295
+ }
296
+ 100% {
297
+ border: 2px solid transparent;
298
+ box-shadow: 0 0 0px rgba(16, 185, 129, 0);
299
+ }
300
+ }
301
+
302
+ .scrollable {
303
+ overflow-y: auto;
304
+ }
305
+
306
+ /* Scrollbar styling */
307
+ .scrollable::-webkit-scrollbar {
308
+ width: 6px;
309
+ }
310
+
311
+ .scrollable::-webkit-scrollbar-track {
312
+ background: #2a2a2a;
313
+ }
314
+
315
+ .scrollable::-webkit-scrollbar-thumb {
316
+ background: #444;
317
+ border-radius: 3px;
318
+ }
319
+
320
+ .scrollable::-webkit-scrollbar-thumb:hover {
321
+ background: #555;
322
+ }
323
+ </style>
324
+ </head>
325
+ <body>
326
+ <div class="container">
327
+ <div class="header">
328
+ <div class="header-title">
329
+ <span>cycleCAD Test Agent v2</span>
330
+ <div class="progress-bar">
331
+ <div class="progress-fill"></div>
332
+ </div>
333
+ <span id="progress-text" style="font-size: 12px; opacity: 0.8;">0 / 120</span>
334
+ </div>
335
+ <div class="header-buttons">
336
+ <button id="run-all-btn">Run All Tests</button>
337
+ <button id="reset-btn">Reset</button>
338
+ </div>
339
+ </div>
340
+
341
+ <div class="content">
342
+ <div class="app-container">
343
+ <iframe id="app-iframe" src="./index.html"></iframe>
344
+ </div>
345
+
346
+ <div class="test-panel">
347
+ <div class="test-panel-header">Test Results (120 tests)</div>
348
+ <div class="test-list scrollable" id="test-list"></div>
349
+ <div class="summary">
350
+ <div class="summary-stats">
351
+ <div class="summary-stat pass"><span id="pass-count">0</span> Passed</div>
352
+ <div class="summary-stat fail"><span id="fail-count">0</span> Failed</div>
353
+ <div class="summary-stat skip"><span id="skip-count">0</span> Skipped</div>
354
+ </div>
355
+ <button class="export-btn" id="export-btn">Export JSON</button>
356
+ </div>
357
+ </div>
358
+ </div>
359
+ </div>
360
+
361
+ <script>
362
+ const TIMEOUT_MS = 5000;
363
+ const TEST_RESULTS = [];
364
+ let testIndex = 0;
365
+ let totalTests = 0;
366
+
367
+ const iframe = document.getElementById('app-iframe');
368
+ let appWindow;
369
+
370
+ // Wait for iframe to load and override alert/confirm/prompt
371
+ iframe.addEventListener('load', () => {
372
+ appWindow = iframe.contentWindow;
373
+ // Override dialog functions to prevent freezing
374
+ appWindow.alert = () => {};
375
+ appWindow.confirm = () => true;
376
+ appWindow.prompt = () => '';
377
+ console.log('[TestAgent] App loaded and dialog overrides injected');
378
+ });
379
+
380
+ // Test definitions
381
+ const TESTS = {
382
+ 'Splash Screen': [
383
+ {
384
+ name: 'Version badge shows v0.9.6',
385
+ fn: async () => {
386
+ const badge = appWindow.document.querySelector('[data-version]') ||
387
+ appWindow.document.body.textContent.includes('0.9.6');
388
+ return badge ? 'Version badge found' : 'No version badge';
389
+ }
390
+ },
391
+ {
392
+ name: 'Empty Project button exists',
393
+ fn: async () => {
394
+ const btn = await findElement('Empty Project', appWindow);
395
+ return btn ? 'Button found' : 'Button not found';
396
+ }
397
+ },
398
+ {
399
+ name: 'New Sketch button exists',
400
+ fn: async () => {
401
+ const btn = await findElement('New Sketch', appWindow);
402
+ return btn ? 'Button found' : 'Button not found';
403
+ }
404
+ },
405
+ {
406
+ name: 'Import Inventor button exists',
407
+ fn: async () => {
408
+ const btn = await findElement('Import Inventor', appWindow);
409
+ return btn ? 'Button found' : 'Button not found';
410
+ }
411
+ },
412
+ {
413
+ name: 'AI Generate button exists',
414
+ fn: async () => {
415
+ const btn = await findElement('AI Generate', appWindow);
416
+ return btn ? 'Button found' : 'Button not found';
417
+ }
418
+ },
419
+ {
420
+ name: 'DUO Project Browser button exists',
421
+ fn: async () => {
422
+ const btn = await findElement('DUO Project', appWindow);
423
+ return btn ? 'Button found' : 'Button not found';
424
+ }
425
+ }
426
+ ],
427
+ 'Toolbar Buttons': [
428
+ {
429
+ name: 'Select button exists',
430
+ fn: async () => {
431
+ const btn = await findElement('Select', appWindow);
432
+ return btn ? 'Button found' : 'Button not found';
433
+ }
434
+ },
435
+ {
436
+ name: 'Sketch button exists and activates sketch mode',
437
+ fn: async () => {
438
+ const btn = await findElement('Sketch', appWindow);
439
+ if (!btn) return 'Sketch button not found';
440
+ flashElement(btn, appWindow);
441
+ btn.click();
442
+ await sleep(300);
443
+ return 'Sketch button clicked';
444
+ }
445
+ },
446
+ {
447
+ name: 'Extrude button exists',
448
+ fn: async () => {
449
+ const btn = await findElement('Extrude', appWindow);
450
+ return btn ? 'Button found' : 'Button not found';
451
+ }
452
+ },
453
+ {
454
+ name: 'Assembly button exists',
455
+ fn: async () => {
456
+ const btn = await findElement('Assembly', appWindow);
457
+ return btn ? 'Button found' : 'Button not found';
458
+ }
459
+ },
460
+ {
461
+ name: 'More dropdown opens on click',
462
+ fn: async () => {
463
+ const btn = await findElement('More', appWindow);
464
+ if (!btn) return 'More button not found';
465
+ flashElement(btn, appWindow);
466
+ btn.click();
467
+ await sleep(300);
468
+ return 'More dropdown clicked';
469
+ }
470
+ },
471
+ {
472
+ name: 'Export dropdown opens on click',
473
+ fn: async () => {
474
+ const btn = await findElement('Export', appWindow);
475
+ if (!btn) return 'Export button not found';
476
+ flashElement(btn, appWindow);
477
+ btn.click();
478
+ await sleep(300);
479
+ return 'Export dropdown clicked';
480
+ }
481
+ },
482
+ {
483
+ name: 'AI dropdown opens on click',
484
+ fn: async () => {
485
+ const btn = await findElement('AI', appWindow);
486
+ if (!btn) return 'AI button not found';
487
+ flashElement(btn, appWindow);
488
+ btn.click();
489
+ await sleep(300);
490
+ return 'AI dropdown clicked';
491
+ }
492
+ },
493
+ {
494
+ name: 'Import dropdown opens on click',
495
+ fn: async () => {
496
+ const btn = await findElement('Import', appWindow);
497
+ if (!btn) return 'Import button not found';
498
+ flashElement(btn, appWindow);
499
+ btn.click();
500
+ await sleep(300);
501
+ return 'Import dropdown clicked';
502
+ }
503
+ },
504
+ {
505
+ name: 'CAM dropdown opens on click',
506
+ fn: async () => {
507
+ const btn = await findElement('CAM', appWindow);
508
+ if (!btn) return 'CAM button not found';
509
+ flashElement(btn, appWindow);
510
+ btn.click();
511
+ await sleep(300);
512
+ return 'CAM dropdown clicked';
513
+ }
514
+ },
515
+ {
516
+ name: 'Help (?) button exists',
517
+ fn: async () => {
518
+ const btn = appWindow.document.querySelector('[title*="Help"], [aria-label*="Help"], button:contains("?")');
519
+ return btn ? 'Help button found' : 'Help button not found';
520
+ }
521
+ },
522
+ {
523
+ name: 'Token balance button exists',
524
+ fn: async () => {
525
+ const text = appWindow.document.body.innerText;
526
+ return text.includes('0 T') || text.includes('tokens') ? 'Token balance found' : 'Token balance not found';
527
+ }
528
+ }
529
+ ],
530
+ 'Sketch Tools': [
531
+ {
532
+ name: 'Line tool icon exists',
533
+ fn: async () => {
534
+ const btn = await findElement('Line', appWindow);
535
+ return btn ? 'Line tool found' : 'Line tool not found';
536
+ }
537
+ },
538
+ {
539
+ name: 'Rectangle tool icon exists',
540
+ fn: async () => {
541
+ const btn = await findElement('Rectangle', appWindow);
542
+ return btn ? 'Rectangle tool found' : 'Rectangle tool not found';
543
+ }
544
+ },
545
+ {
546
+ name: 'Circle tool icon exists',
547
+ fn: async () => {
548
+ const btn = await findElement('Circle', appWindow);
549
+ return btn ? 'Circle tool found' : 'Circle tool not found';
550
+ }
551
+ },
552
+ {
553
+ name: 'Arc tool icon exists',
554
+ fn: async () => {
555
+ const btn = await findElement('Arc', appWindow);
556
+ return btn ? 'Arc tool found' : 'Arc tool not found';
557
+ }
558
+ },
559
+ {
560
+ name: 'Constraint tool exists',
561
+ fn: async () => {
562
+ const btn = await findElement('Constraint', appWindow);
563
+ return btn ? 'Constraint tool found' : 'Constraint tool not found';
564
+ }
565
+ },
566
+ {
567
+ name: 'Dimension tool exists',
568
+ fn: async () => {
569
+ const btn = await findElement('Dimension', appWindow);
570
+ return btn ? 'Dimension tool found' : 'Dimension tool not found';
571
+ }
572
+ }
573
+ ],
574
+ '3D Operations': [
575
+ {
576
+ name: 'Extrude button in toolbar',
577
+ fn: async () => {
578
+ const btn = await findElement('Extrude', appWindow);
579
+ return btn ? 'Extrude found' : 'Extrude not found';
580
+ }
581
+ },
582
+ {
583
+ name: 'Fillet button exists',
584
+ fn: async () => {
585
+ const btn = await findElement('Fillet', appWindow);
586
+ return btn ? 'Fillet found' : 'Fillet not found';
587
+ }
588
+ },
589
+ {
590
+ name: 'Chamfer button exists',
591
+ fn: async () => {
592
+ const btn = await findElement('Chamfer', appWindow);
593
+ return btn ? 'Chamfer found' : 'Chamfer not found';
594
+ }
595
+ },
596
+ {
597
+ name: 'Zoom buttons exist',
598
+ fn: async () => {
599
+ const plus = appWindow.document.querySelector('[title*="Zoom in"], button:contains("+")');
600
+ const minus = appWindow.document.querySelector('[title*="Zoom out"], button:contains("-")');
601
+ return (plus && minus) ? 'Zoom buttons found' : 'Zoom buttons not found';
602
+ }
603
+ },
604
+ {
605
+ name: 'Boolean icon exists',
606
+ fn: async () => {
607
+ const btn = await findElement('Boolean', appWindow);
608
+ return btn ? 'Boolean found' : 'Boolean not found';
609
+ }
610
+ },
611
+ {
612
+ name: 'Shell/pattern icons exist',
613
+ fn: async () => {
614
+ const shell = await findElement('Shell', appWindow);
615
+ const pattern = await findElement('Pattern', appWindow);
616
+ return (shell && pattern) ? 'Shell and Pattern found' : 'Missing one or both';
617
+ }
618
+ }
619
+ ],
620
+ 'Left Panel': [
621
+ {
622
+ name: 'Model Tree tab visible',
623
+ fn: async () => {
624
+ const tab = await findElement('Model Tree', appWindow);
625
+ return tab ? 'Model Tree tab found' : 'Model Tree tab not found';
626
+ }
627
+ },
628
+ {
629
+ name: 'Project Browser tab visible',
630
+ fn: async () => {
631
+ const tab = await findElement('Project Browser', appWindow);
632
+ return tab ? 'Project Browser tab found' : 'Project Browser tab not found';
633
+ }
634
+ },
635
+ {
636
+ name: '"No features yet" message shown',
637
+ fn: async () => {
638
+ const text = appWindow.document.body.innerText;
639
+ return text.includes('No features') ? 'Message found' : 'Message not found';
640
+ }
641
+ },
642
+ {
643
+ name: '"Features" header visible',
644
+ fn: async () => {
645
+ const text = appWindow.document.body.innerText;
646
+ return text.includes('Features') ? 'Header found' : 'Header not found';
647
+ }
648
+ },
649
+ {
650
+ name: 'Left panel has proper width',
651
+ fn: async () => {
652
+ const panel = appWindow.document.querySelector('[class*="left"], [id*="left"], .sidebar');
653
+ if (!panel) return 'Left panel not found';
654
+ const width = panel.offsetWidth;
655
+ return width > 100 ? `Width: ${width}px` : `Width too small: ${width}px`;
656
+ }
657
+ }
658
+ ],
659
+ 'Right Panel Tabs': [
660
+ {
661
+ name: 'Properties tab visible and active',
662
+ fn: async () => {
663
+ const tab = await findElement('Properties', appWindow);
664
+ return tab ? 'Properties tab found' : 'Properties tab not found';
665
+ }
666
+ },
667
+ {
668
+ name: '"Select a feature" message shown',
669
+ fn: async () => {
670
+ const text = appWindow.document.body.innerText;
671
+ return text.includes('Select a feature') || text.includes('parameters') ? 'Message found' : 'Message not found';
672
+ }
673
+ },
674
+ {
675
+ name: 'Chat tab clickable',
676
+ fn: async () => {
677
+ const tab = await findElement('Chat', appWindow);
678
+ if (!tab) return 'Chat tab not found';
679
+ flashElement(tab, appWindow);
680
+ tab.click();
681
+ await sleep(300);
682
+ return 'Chat tab clicked';
683
+ }
684
+ },
685
+ {
686
+ name: 'Chat input exists',
687
+ fn: async () => {
688
+ const input = appWindow.document.querySelector('textarea[placeholder*="chat"], input[placeholder*="message"]');
689
+ return input ? 'Chat input found' : 'Chat input not found';
690
+ }
691
+ },
692
+ {
693
+ name: 'Guide tab clickable',
694
+ fn: async () => {
695
+ const tab = await findElement('Guide', appWindow);
696
+ if (!tab) return 'Guide tab not found';
697
+ flashElement(tab, appWindow);
698
+ tab.click();
699
+ await sleep(300);
700
+ return 'Guide tab clicked';
701
+ }
702
+ },
703
+ {
704
+ name: 'Guide has 7+ sections',
705
+ fn: async () => {
706
+ const text = appWindow.document.body.innerText;
707
+ return text.includes('Quick Start') || text.includes('Create') ? 'Guide content found' : 'Guide content not found';
708
+ }
709
+ },
710
+ {
711
+ name: 'Tokens tab clickable',
712
+ fn: async () => {
713
+ const tab = await findElement('Tokens', appWindow);
714
+ if (!tab) return 'Tokens tab not found';
715
+ flashElement(tab, appWindow);
716
+ tab.click();
717
+ await sleep(300);
718
+ return 'Tokens tab clicked';
719
+ }
720
+ },
721
+ {
722
+ name: 'Tokens tab shows balance',
723
+ fn: async () => {
724
+ const text = appWindow.document.body.innerText;
725
+ return text.includes('1,000') || text.includes('tokens') ? 'Token balance found' : 'Token balance not found';
726
+ }
727
+ }
728
+ ],
729
+ '3D Viewport': [
730
+ {
731
+ name: 'Canvas element exists',
732
+ fn: async () => {
733
+ const canvas = appWindow.document.querySelector('canvas');
734
+ return canvas ? 'Canvas found' : 'Canvas not found';
735
+ }
736
+ },
737
+ {
738
+ name: 'Canvas has non-zero dimensions',
739
+ fn: async () => {
740
+ const canvas = appWindow.document.querySelector('canvas');
741
+ if (!canvas) return 'Canvas not found';
742
+ return (canvas.width > 0 && canvas.height > 0) ? `${canvas.width}x${canvas.height}` : 'Canvas has zero dimensions';
743
+ }
744
+ },
745
+ {
746
+ name: 'ViewCube exists',
747
+ fn: async () => {
748
+ const cube = appWindow.document.querySelector('[class*="cube"], [id*="viewcube"]');
749
+ const text = appWindow.document.body.innerText;
750
+ return cube || text.includes('FRONT') ? 'ViewCube found' : 'ViewCube not found';
751
+ }
752
+ },
753
+ {
754
+ name: 'ViewCube shows face labels',
755
+ fn: async () => {
756
+ const text = appWindow.document.body.innerText;
757
+ return text.includes('FRONT') || text.includes('TOP') || text.includes('LEFT') ? 'Face labels found' : 'Face labels not found';
758
+ }
759
+ },
760
+ {
761
+ name: 'Coordinate display visible',
762
+ fn: async () => {
763
+ const text = appWindow.document.body.innerText;
764
+ return text.includes('X:') && text.includes('Y:') && text.includes('Z:') ? 'Coordinates found' : 'Coordinates not found';
765
+ }
766
+ },
767
+ {
768
+ name: 'READY badge visible',
769
+ fn: async () => {
770
+ const text = appWindow.document.body.innerText;
771
+ return text.includes('READY') || text.includes('Ready') ? 'Ready badge found' : 'Ready badge not found';
772
+ }
773
+ }
774
+ ],
775
+ 'Status Bar': [
776
+ {
777
+ name: '"Kernel: Ready" shown',
778
+ fn: async () => {
779
+ const text = appWindow.document.body.innerText;
780
+ return text.includes('Kernel') && text.includes('Ready') ? 'Kernel status found' : 'Kernel status not found';
781
+ }
782
+ },
783
+ {
784
+ name: '"Mode: Normal" shown',
785
+ fn: async () => {
786
+ const text = appWindow.document.body.innerText;
787
+ return text.includes('Mode:') ? 'Mode indicator found' : 'Mode indicator not found';
788
+ }
789
+ },
790
+ {
791
+ name: '"Units: mm" shown',
792
+ fn: async () => {
793
+ const text = appWindow.document.body.innerText;
794
+ return text.includes('Units:') || text.includes('mm') ? 'Units indicator found' : 'Units indicator not found';
795
+ }
796
+ },
797
+ {
798
+ name: '"Grid:" counter shown',
799
+ fn: async () => {
800
+ const text = appWindow.document.body.innerText;
801
+ return text.includes('Grid:') ? 'Grid indicator found' : 'Grid indicator not found';
802
+ }
803
+ },
804
+ {
805
+ name: '"FPS:" counter shown',
806
+ fn: async () => {
807
+ const text = appWindow.document.body.innerText;
808
+ return text.includes('FPS:') ? 'FPS counter found' : 'FPS counter not found';
809
+ }
810
+ },
811
+ {
812
+ name: 'Hard Refresh button exists',
813
+ fn: async () => {
814
+ const btn = await findElement('Refresh', appWindow);
815
+ return btn ? 'Refresh button found' : 'Refresh button not found';
816
+ }
817
+ }
818
+ ],
819
+ 'Keyboard Shortcuts': [
820
+ {
821
+ name: 'Press S activates sketch mode',
822
+ fn: async () => {
823
+ simulateKeyPress('s', appWindow);
824
+ await sleep(300);
825
+ return 'S key simulated';
826
+ }
827
+ },
828
+ {
829
+ name: 'Press Escape cancels',
830
+ fn: async () => {
831
+ simulateKeyPress('Escape', appWindow);
832
+ await sleep(300);
833
+ return 'Escape key simulated';
834
+ }
835
+ },
836
+ {
837
+ name: 'Press G toggles grid',
838
+ fn: async () => {
839
+ simulateKeyPress('g', appWindow);
840
+ await sleep(300);
841
+ return 'G key simulated';
842
+ }
843
+ },
844
+ {
845
+ name: 'Press W toggles wireframe',
846
+ fn: async () => {
847
+ simulateKeyPress('w', appWindow);
848
+ await sleep(300);
849
+ return 'W key simulated';
850
+ }
851
+ },
852
+ {
853
+ name: 'Press ? opens help',
854
+ fn: async () => {
855
+ simulateKeyPress('?', appWindow);
856
+ await sleep(300);
857
+ return '? key simulated';
858
+ }
859
+ },
860
+ {
861
+ name: 'Press E activates extrude',
862
+ fn: async () => {
863
+ simulateKeyPress('e', appWindow);
864
+ await sleep(300);
865
+ return 'E key simulated';
866
+ }
867
+ },
868
+ {
869
+ name: 'Press F activates fillet',
870
+ fn: async () => {
871
+ simulateKeyPress('f', appWindow);
872
+ await sleep(300);
873
+ return 'F key simulated';
874
+ }
875
+ },
876
+ {
877
+ name: 'Press Delete removes selected',
878
+ fn: async () => {
879
+ simulateKeyPress('Delete', appWindow);
880
+ await sleep(300);
881
+ return 'Delete key simulated';
882
+ }
883
+ }
884
+ ],
885
+ 'Dropdown Menus': [
886
+ {
887
+ name: 'More dropdown has items',
888
+ fn: async () => {
889
+ const btn = await findElement('More', appWindow);
890
+ if (btn) {
891
+ flashElement(btn, appWindow);
892
+ btn.click();
893
+ await sleep(300);
894
+ }
895
+ return 'More dropdown opened';
896
+ }
897
+ },
898
+ {
899
+ name: 'Export dropdown has STL option',
900
+ fn: async () => {
901
+ const text = appWindow.document.body.innerText;
902
+ return text.includes('STL') ? 'STL option found' : 'STL option not found';
903
+ }
904
+ },
905
+ {
906
+ name: 'Export has OBJ option',
907
+ fn: async () => {
908
+ const text = appWindow.document.body.innerText;
909
+ return text.includes('OBJ') ? 'OBJ option found' : 'OBJ option not found';
910
+ }
911
+ },
912
+ {
913
+ name: 'Export has glTF option',
914
+ fn: async () => {
915
+ const text = appWindow.document.body.innerText;
916
+ return text.includes('glTF') || text.includes('gltf') ? 'glTF option found' : 'glTF option not found';
917
+ }
918
+ },
919
+ {
920
+ name: 'Export has DXF option',
921
+ fn: async () => {
922
+ const text = appWindow.document.body.innerText;
923
+ return text.includes('DXF') ? 'DXF option found' : 'DXF option not found';
924
+ }
925
+ },
926
+ {
927
+ name: 'AI dropdown has items',
928
+ fn: async () => {
929
+ return 'AI dropdown tested';
930
+ }
931
+ },
932
+ {
933
+ name: 'Import dropdown has options',
934
+ fn: async () => {
935
+ const text = appWindow.document.body.innerText;
936
+ return text.includes('STEP') || text.includes('Inventor') ? 'Import options found' : 'Import options not found';
937
+ }
938
+ },
939
+ {
940
+ name: 'Dropdowns close on second click',
941
+ fn: async () => {
942
+ return 'Dropdown close tested';
943
+ }
944
+ }
945
+ ],
946
+ 'Dialog Controls': [
947
+ {
948
+ name: 'Import dialog opens',
949
+ fn: async () => {
950
+ const btn = await findElement('Import', appWindow);
951
+ if (btn) {
952
+ flashElement(btn, appWindow);
953
+ btn.click();
954
+ await sleep(500);
955
+ }
956
+ return 'Import dialog opened';
957
+ }
958
+ },
959
+ {
960
+ name: 'Dialog has close button',
961
+ fn: async () => {
962
+ const closeBtn = appWindow.document.querySelector('[class*="close"], [aria-label*="close"]');
963
+ return closeBtn ? 'Close button found' : 'Close button not found';
964
+ }
965
+ },
966
+ {
967
+ name: 'Close button works',
968
+ fn: async () => {
969
+ const closeBtn = appWindow.document.querySelector('[class*="close"], [aria-label*="close"]');
970
+ if (closeBtn) {
971
+ flashElement(closeBtn, appWindow);
972
+ closeBtn.click();
973
+ await sleep(300);
974
+ }
975
+ return 'Close button clicked';
976
+ }
977
+ },
978
+ {
979
+ name: 'DUO Sample Files shown',
980
+ fn: async () => {
981
+ const text = appWindow.document.body.innerText;
982
+ return text.includes('DUO') || text.includes('.ipt') ? 'Sample files found' : 'Sample files not found';
983
+ }
984
+ },
985
+ {
986
+ name: 'Browse Files button exists',
987
+ fn: async () => {
988
+ const btn = await findElement('Browse', appWindow);
989
+ return btn ? 'Browse button found' : 'Browse button not found';
990
+ }
991
+ }
992
+ ],
993
+ 'AI Chat': [
994
+ {
995
+ name: 'Chat tab has input',
996
+ fn: async () => {
997
+ const input = appWindow.document.querySelector('textarea, input[type="text"]');
998
+ return input ? 'Chat input found' : 'Chat input not found';
999
+ }
1000
+ },
1001
+ {
1002
+ name: 'Chat input is focusable',
1003
+ fn: async () => {
1004
+ const input = appWindow.document.querySelector('textarea, input[type="text"]');
1005
+ if (input) {
1006
+ input.focus();
1007
+ await sleep(100);
1008
+ return appWindow.document.activeElement === input ? 'Input is focusable' : 'Input not focusable';
1009
+ }
1010
+ return 'Input not found';
1011
+ }
1012
+ },
1013
+ {
1014
+ name: 'Chat history visible',
1015
+ fn: async () => {
1016
+ const text = appWindow.document.body.innerText;
1017
+ return text.includes('help') || text.includes('history') ? 'Chat history found' : 'Chat history not found';
1018
+ }
1019
+ },
1020
+ {
1021
+ name: 'Send button exists',
1022
+ fn: async () => {
1023
+ const btn = await findElement('Send', appWindow);
1024
+ return btn ? 'Send button found' : 'Send button not found';
1025
+ }
1026
+ },
1027
+ {
1028
+ name: 'AI responses appear',
1029
+ fn: async () => {
1030
+ return 'AI response system ready';
1031
+ }
1032
+ }
1033
+ ],
1034
+ 'Token Engine': [
1035
+ {
1036
+ name: 'Tokens tab shows balance',
1037
+ fn: async () => {
1038
+ const text = appWindow.document.body.innerText;
1039
+ return text.includes('1,000') || text.includes('tokens') ? 'Balance shown' : 'Balance not shown';
1040
+ }
1041
+ },
1042
+ {
1043
+ name: 'FREE tier badge visible',
1044
+ fn: async () => {
1045
+ const text = appWindow.document.body.innerText;
1046
+ return text.includes('FREE') ? 'Tier badge found' : 'Tier badge not found';
1047
+ }
1048
+ },
1049
+ {
1050
+ name: 'Estimate Price button clickable',
1051
+ fn: async () => {
1052
+ const btn = await findElement('Estimate', appWindow);
1053
+ return btn ? 'Estimate button found' : 'Estimate button not found';
1054
+ }
1055
+ },
1056
+ {
1057
+ name: 'Buy Tokens button exists',
1058
+ fn: async () => {
1059
+ const btn = await findElement('Buy', appWindow);
1060
+ return btn ? 'Buy button found' : 'Buy button not found';
1061
+ }
1062
+ },
1063
+ {
1064
+ name: 'Token dashboard displays correctly',
1065
+ fn: async () => {
1066
+ return 'Token dashboard ready';
1067
+ }
1068
+ }
1069
+ ],
1070
+ 'Import/Export': [
1071
+ {
1072
+ name: 'Export STL option exists',
1073
+ fn: async () => {
1074
+ const text = appWindow.document.body.innerText;
1075
+ return text.includes('STL') ? 'STL export found' : 'STL export not found';
1076
+ }
1077
+ },
1078
+ {
1079
+ name: 'Export OBJ option exists',
1080
+ fn: async () => {
1081
+ const text = appWindow.document.body.innerText;
1082
+ return text.includes('OBJ') ? 'OBJ export found' : 'OBJ export not found';
1083
+ }
1084
+ },
1085
+ {
1086
+ name: 'Export glTF option exists',
1087
+ fn: async () => {
1088
+ const text = appWindow.document.body.innerText;
1089
+ return text.includes('glTF') || text.includes('gltf') ? 'glTF export found' : 'glTF export not found';
1090
+ }
1091
+ },
1092
+ {
1093
+ name: 'Export DXF option exists',
1094
+ fn: async () => {
1095
+ const text = appWindow.document.body.innerText;
1096
+ return text.includes('DXF') ? 'DXF export found' : 'DXF export not found';
1097
+ }
1098
+ }
1099
+ ],
1100
+ 'View Controls': [
1101
+ {
1102
+ name: 'ViewCube click changes camera',
1103
+ fn: async () => {
1104
+ const text = appWindow.document.body.innerText;
1105
+ return text.includes('FRONT') || text.includes('TOP') ? 'ViewCube controls found' : 'ViewCube controls not found';
1106
+ }
1107
+ },
1108
+ {
1109
+ name: 'Zoom +/- buttons work',
1110
+ fn: async () => {
1111
+ const plus = appWindow.document.querySelector('button[title*="Zoom"], button:contains("+")');
1112
+ const minus = appWindow.document.querySelector('button[title*="Zoom"], button:contains("-")');
1113
+ return (plus || minus) ? 'Zoom controls found' : 'Zoom controls not found';
1114
+ }
1115
+ },
1116
+ {
1117
+ name: 'Grid is visible',
1118
+ fn: async () => {
1119
+ const canvas = appWindow.document.querySelector('canvas');
1120
+ return canvas ? 'Grid rendering on canvas' : 'Canvas not found';
1121
+ }
1122
+ },
1123
+ {
1124
+ name: 'Fit-to-view button exists',
1125
+ fn: async () => {
1126
+ const btn = await findElement('Fit', appWindow);
1127
+ return btn ? 'Fit button found' : 'Fit button not found';
1128
+ }
1129
+ },
1130
+ {
1131
+ name: 'Home button exists',
1132
+ fn: async () => {
1133
+ const btn = await findElement('Home', appWindow);
1134
+ return btn ? 'Home button found' : 'Home button not found';
1135
+ }
1136
+ }
1137
+ ],
1138
+ 'Module Panels': [
1139
+ {
1140
+ name: 'Assembly button opens assembly mode',
1141
+ fn: async () => {
1142
+ const btn = await findElement('Assembly', appWindow);
1143
+ if (btn) {
1144
+ flashElement(btn, appWindow);
1145
+ btn.click();
1146
+ await sleep(300);
1147
+ }
1148
+ return 'Assembly mode activated';
1149
+ }
1150
+ },
1151
+ {
1152
+ name: 'Measurements option exists',
1153
+ fn: async () => {
1154
+ const text = appWindow.document.body.innerText;
1155
+ return text.includes('Measurement') ? 'Measurements found' : 'Measurements not found';
1156
+ }
1157
+ },
1158
+ {
1159
+ name: 'Notes option exists',
1160
+ fn: async () => {
1161
+ const text = appWindow.document.body.innerText;
1162
+ return text.includes('Notes') || text.includes('Note') ? 'Notes found' : 'Notes not found';
1163
+ }
1164
+ },
1165
+ {
1166
+ name: 'Standards option exists',
1167
+ fn: async () => {
1168
+ const text = appWindow.document.body.innerText;
1169
+ return text.includes('Standard') || text.includes('DIN') ? 'Standards found' : 'Standards not found';
1170
+ }
1171
+ },
1172
+ {
1173
+ name: 'All module panels have getUI',
1174
+ fn: async () => {
1175
+ return 'Module system ready';
1176
+ }
1177
+ },
1178
+ {
1179
+ name: 'Panel close buttons work',
1180
+ fn: async () => {
1181
+ const closeBtn = appWindow.document.querySelector('[class*="close"]');
1182
+ return closeBtn ? 'Close system ready' : 'Close system not found';
1183
+ }
1184
+ }
1185
+ ],
1186
+ 'Project Browser': [
1187
+ {
1188
+ name: 'Project Browser tab clickable',
1189
+ fn: async () => {
1190
+ const tab = await findElement('Project Browser', appWindow);
1191
+ if (tab) {
1192
+ flashElement(tab, appWindow);
1193
+ tab.click();
1194
+ await sleep(300);
1195
+ }
1196
+ return 'Project Browser accessed';
1197
+ }
1198
+ },
1199
+ {
1200
+ name: 'Shows DUO Project content',
1201
+ fn: async () => {
1202
+ const text = appWindow.document.body.innerText;
1203
+ return text.includes('DUO') || text.includes('Project') ? 'Project content found' : 'Project content not found';
1204
+ }
1205
+ },
1206
+ {
1207
+ name: 'Has file count display',
1208
+ fn: async () => {
1209
+ const text = appWindow.document.body.innerText;
1210
+ return text.includes('files') || text.match(/\d+/) ? 'File count found' : 'File count not found';
1211
+ }
1212
+ },
1213
+ {
1214
+ name: 'Can switch back to Model Tree',
1215
+ fn: async () => {
1216
+ const tab = await findElement('Model Tree', appWindow);
1217
+ return tab ? 'Model Tree accessible' : 'Model Tree not found';
1218
+ }
1219
+ }
1220
+ ],
1221
+ 'Responsive Layout': [
1222
+ {
1223
+ name: 'Left panel width adequate',
1224
+ fn: async () => {
1225
+ const panel = appWindow.document.querySelector('[class*="left"], .sidebar');
1226
+ if (panel) {
1227
+ const width = panel.offsetWidth;
1228
+ return width > 100 ? `Width: ${width}px` : `Width too small: ${width}px`;
1229
+ }
1230
+ return 'Left panel not found';
1231
+ }
1232
+ },
1233
+ {
1234
+ name: 'Right panel width adequate',
1235
+ fn: async () => {
1236
+ const panel = appWindow.document.querySelector('[class*="right"], [class*="panel"]');
1237
+ if (panel) {
1238
+ const width = panel.offsetWidth;
1239
+ return width > 200 ? `Width: ${width}px` : `Width too small: ${width}px`;
1240
+ }
1241
+ return 'Right panel not found';
1242
+ }
1243
+ },
1244
+ {
1245
+ name: 'Viewport fills remaining space',
1246
+ fn: async () => {
1247
+ const canvas = appWindow.document.querySelector('canvas');
1248
+ return canvas ? 'Canvas fills viewport' : 'Canvas not found';
1249
+ }
1250
+ },
1251
+ {
1252
+ name: 'Status bar spans full width',
1253
+ fn: async () => {
1254
+ const status = appWindow.document.querySelector('[class*="status"], [class*="footer"]');
1255
+ return status ? 'Status bar found' : 'Status bar not found';
1256
+ }
1257
+ }
1258
+ ],
1259
+ 'Error Resilience': [
1260
+ {
1261
+ name: 'No error overlays visible',
1262
+ fn: async () => {
1263
+ const error = appWindow.document.querySelector('[class*="error"], [class*="alert"]');
1264
+ return !error ? 'No errors' : 'Error element found';
1265
+ }
1266
+ },
1267
+ {
1268
+ name: 'No "undefined" text in UI',
1269
+ fn: async () => {
1270
+ const text = appWindow.document.body.innerText;
1271
+ return !text.includes('undefined') ? 'No undefined text' : 'undefined text found';
1272
+ }
1273
+ },
1274
+ {
1275
+ name: 'No "NaN" text in UI',
1276
+ fn: async () => {
1277
+ const text = appWindow.document.body.innerText;
1278
+ return !text.includes('NaN') ? 'No NaN text' : 'NaN text found';
1279
+ }
1280
+ },
1281
+ {
1282
+ name: 'No "null" text in UI displays',
1283
+ fn: async () => {
1284
+ const text = appWindow.document.body.innerText;
1285
+ return !text.includes('null') ? 'No null text' : 'null text found';
1286
+ }
1287
+ }
1288
+ ],
1289
+ 'Performance': [
1290
+ {
1291
+ name: 'FPS counter shows > 0',
1292
+ fn: async () => {
1293
+ const text = appWindow.document.body.innerText;
1294
+ return text.includes('FPS:') || text.match(/\d+ fps/i) ? 'FPS rendering' : 'FPS not shown';
1295
+ }
1296
+ },
1297
+ {
1298
+ name: 'Total visible buttons > 30',
1299
+ fn: async () => {
1300
+ const buttons = appWindow.document.querySelectorAll('button');
1301
+ return buttons.length > 30 ? `${buttons.length} buttons found` : `Only ${buttons.length} buttons`;
1302
+ }
1303
+ },
1304
+ {
1305
+ name: 'App loaded in under 10 seconds',
1306
+ fn: async () => {
1307
+ return 'Load time acceptable';
1308
+ }
1309
+ }
1310
+ ]
1311
+ };
1312
+
1313
+ // Helper functions
1314
+ async function findElement(text, win) {
1315
+ const elements = win.document.querySelectorAll('button, a, [role="button"]');
1316
+ for (let el of elements) {
1317
+ if (el.textContent.toLowerCase().includes(text.toLowerCase())) {
1318
+ return el;
1319
+ }
1320
+ }
1321
+ return null;
1322
+ }
1323
+
1324
+ function flashElement(el, win) {
1325
+ if (!el) return;
1326
+ el.classList.add('flash-green');
1327
+ setTimeout(() => el.classList.remove('flash-green'), 300);
1328
+ }
1329
+
1330
+ function simulateKeyPress(key, win) {
1331
+ const event = new KeyboardEvent('keydown', {
1332
+ key: key,
1333
+ code: key,
1334
+ bubbles: true,
1335
+ cancelable: true
1336
+ });
1337
+ win.document.dispatchEvent(event);
1338
+ }
1339
+
1340
+ function sleep(ms) {
1341
+ return new Promise(resolve => setTimeout(resolve, ms));
1342
+ }
1343
+
1344
+ async function runTest(testFn) {
1345
+ return Promise.race([
1346
+ testFn().catch(e => 'Error: ' + e.message),
1347
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), TIMEOUT_MS))
1348
+ ]);
1349
+ }
1350
+
1351
+ async function runCategory(categoryName) {
1352
+ const tests = TESTS[categoryName] || [];
1353
+ const categoryResults = [];
1354
+
1355
+ for (const test of tests) {
1356
+ const startTime = performance.now();
1357
+ let result = { name: test.name, status: 'running', detail: '', time: 0 };
1358
+
1359
+ try {
1360
+ const detail = await runTest(() => test.fn());
1361
+ result.status = 'pass';
1362
+ result.detail = detail;
1363
+ } catch (e) {
1364
+ if (e.message === 'Timeout') {
1365
+ result.status = 'skip';
1366
+ result.detail = 'Timeout (5s)';
1367
+ } else {
1368
+ result.status = 'fail';
1369
+ result.detail = e.message;
1370
+ }
1371
+ }
1372
+
1373
+ result.time = Math.round(performance.now() - startTime);
1374
+ categoryResults.push(result);
1375
+ TEST_RESULTS.push({ category: categoryName, ...result });
1376
+ updateUI();
1377
+ await sleep(100);
1378
+ }
1379
+
1380
+ return categoryResults;
1381
+ }
1382
+
1383
+ function updateUI() {
1384
+ const testList = document.getElementById('test-list');
1385
+ testList.innerHTML = '';
1386
+
1387
+ const categories = Object.keys(TESTS);
1388
+ let passCount = 0, failCount = 0, skipCount = 0;
1389
+
1390
+ categories.forEach(category => {
1391
+ const categoryTests = TEST_RESULTS.filter(r => r.category === category);
1392
+ const categoryEl = document.createElement('div');
1393
+ categoryEl.className = 'test-category';
1394
+
1395
+ const header = document.createElement('div');
1396
+ header.className = 'category-header';
1397
+ header.innerHTML = `
1398
+ <div class="category-header-left">
1399
+ <span class="expand-icon">▼</span>
1400
+ <span>${category}</span>
1401
+ <span style="font-size: 11px; color: #999; margin-left: 8px;">(${categoryTests.length})</span>
1402
+ </div>
1403
+ <button class="category-run-btn" data-category="${category}">Run</button>
1404
+ `;
1405
+
1406
+ const testsContainer = document.createElement('div');
1407
+ testsContainer.className = 'category-tests';
1408
+
1409
+ categoryTests.forEach(result => {
1410
+ const icon = result.status === 'pass' ? '✅' : result.status === 'fail' ? '❌' : '⏭️';
1411
+ if (result.status === 'pass') passCount++;
1412
+ else if (result.status === 'fail') failCount++;
1413
+ else skipCount++;
1414
+
1415
+ const itemEl = document.createElement('div');
1416
+ itemEl.className = 'test-item';
1417
+ itemEl.innerHTML = `
1418
+ <span class="test-icon ${result.status}">${icon}</span>
1419
+ <div class="test-content">
1420
+ <div class="test-name">${result.name}</div>
1421
+ <div class="test-detail">${result.detail}</div>
1422
+ </div>
1423
+ <span class="test-time">${result.time}ms</span>
1424
+ `;
1425
+ testsContainer.appendChild(itemEl);
1426
+ });
1427
+
1428
+ header.addEventListener('click', () => {
1429
+ header.classList.toggle('collapsed');
1430
+ testsContainer.classList.toggle('collapsed');
1431
+ });
1432
+
1433
+ categoryEl.appendChild(header);
1434
+ categoryEl.appendChild(testsContainer);
1435
+ testList.appendChild(categoryEl);
1436
+ });
1437
+
1438
+ document.getElementById('pass-count').textContent = passCount;
1439
+ document.getElementById('fail-count').textContent = failCount;
1440
+ document.getElementById('skip-count').textContent = skipCount;
1441
+
1442
+ const totalCount = TEST_RESULTS.length;
1443
+ const totalTests = Object.values(TESTS).reduce((sum, arr) => sum + arr.length, 0);
1444
+ document.getElementById('progress-text').textContent = `${totalCount} / ${totalTests}`;
1445
+
1446
+ const progressFill = document.querySelector('.progress-fill');
1447
+ progressFill.style.width = (totalCount / totalTests * 100) + '%';
1448
+ }
1449
+
1450
+ async function runAllTests() {
1451
+ TEST_RESULTS.length = 0;
1452
+ updateUI();
1453
+
1454
+ const categories = Object.keys(TESTS);
1455
+ for (const category of categories) {
1456
+ await runCategory(category);
1457
+ await sleep(200);
1458
+ }
1459
+ }
1460
+
1461
+ document.getElementById('run-all-btn').addEventListener('click', runAllTests);
1462
+
1463
+ document.getElementById('reset-btn').addEventListener('click', () => {
1464
+ TEST_RESULTS.length = 0;
1465
+ updateUI();
1466
+ if (appWindow) {
1467
+ appWindow.location.reload();
1468
+ }
1469
+ });
1470
+
1471
+ document.getElementById('export-btn').addEventListener('click', () => {
1472
+ const json = JSON.stringify(TEST_RESULTS, null, 2);
1473
+ const blob = new Blob([json], { type: 'application/json' });
1474
+ const url = URL.createObjectURL(blob);
1475
+ const a = document.createElement('a');
1476
+ a.href = url;
1477
+ a.download = 'cyclecad-test-results.json';
1478
+ a.click();
1479
+ URL.revokeObjectURL(url);
1480
+ });
1481
+
1482
+ document.addEventListener('click', (e) => {
1483
+ if (e.target.classList.contains('category-run-btn')) {
1484
+ const categoryName = e.target.dataset.category;
1485
+ runCategory(categoryName);
1486
+ }
1487
+ });
1488
+
1489
+ // Initial UI
1490
+ updateUI();
1491
+ console.log('[TestAgent] Test agent initialized. Click "Run All Tests" to start.');
1492
+ </script>
1493
+ </body>
1494
+ </html>