cyclecad 3.6.0 → 3.8.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.
@@ -0,0 +1,1362 @@
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 Killer Features Visual Test</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
+ display: flex;
19
+ height: 100vh;
20
+ gap: 1px;
21
+ }
22
+
23
+ .app-container {
24
+ flex: 0 0 60%;
25
+ background: #1a202c;
26
+ border: 1px solid #2d3748;
27
+ position: relative;
28
+ }
29
+
30
+ iframe {
31
+ width: 100%;
32
+ height: 100%;
33
+ border: none;
34
+ }
35
+
36
+ .test-panel {
37
+ flex: 0 0 40%;
38
+ display: flex;
39
+ flex-direction: column;
40
+ background: #1a202c;
41
+ border-left: 1px solid #2d3748;
42
+ overflow: hidden;
43
+ }
44
+
45
+ .test-header {
46
+ padding: 16px;
47
+ border-bottom: 1px solid #2d3748;
48
+ background: #0f172a;
49
+ max-height: 180px;
50
+ overflow-y: auto;
51
+ }
52
+
53
+ .test-title {
54
+ font-size: 16px;
55
+ font-weight: 600;
56
+ margin-bottom: 10px;
57
+ color: #38bdf8;
58
+ }
59
+
60
+ .test-stats {
61
+ display: grid;
62
+ grid-template-columns: 1fr 1fr 1fr 1fr 1fr;
63
+ gap: 6px;
64
+ font-size: 11px;
65
+ margin-bottom: 10px;
66
+ }
67
+
68
+ .stat-box {
69
+ padding: 8px;
70
+ background: #1e293b;
71
+ border-radius: 4px;
72
+ border-left: 2px solid #64748b;
73
+ }
74
+
75
+ .stat-box.pass {
76
+ border-left-color: #10b981;
77
+ }
78
+
79
+ .stat-box.fail {
80
+ border-left-color: #ef4444;
81
+ }
82
+
83
+ .stat-box.skip {
84
+ border-left-color: #f59e0b;
85
+ }
86
+
87
+ .stat-box.error {
88
+ border-left-color: #ec4899;
89
+ }
90
+
91
+ .stat-label {
92
+ font-size: 10px;
93
+ color: #94a3b8;
94
+ margin-bottom: 2px;
95
+ }
96
+
97
+ .stat-value {
98
+ font-size: 16px;
99
+ font-weight: 600;
100
+ }
101
+
102
+ .test-controls {
103
+ display: flex;
104
+ gap: 6px;
105
+ padding: 10px;
106
+ border-bottom: 1px solid #2d3748;
107
+ flex-wrap: wrap;
108
+ background: #0f172a;
109
+ }
110
+
111
+ .button {
112
+ padding: 6px 12px;
113
+ background: #0284c7;
114
+ color: white;
115
+ border: none;
116
+ border-radius: 4px;
117
+ font-size: 11px;
118
+ font-weight: 500;
119
+ cursor: pointer;
120
+ transition: background 0.2s;
121
+ }
122
+
123
+ .button:hover {
124
+ background: #0369a1;
125
+ }
126
+
127
+ .button:disabled {
128
+ background: #64748b;
129
+ cursor: not-allowed;
130
+ opacity: 0.5;
131
+ }
132
+
133
+ .button.secondary {
134
+ background: #475569;
135
+ }
136
+
137
+ .button.secondary:hover {
138
+ background: #64748b;
139
+ }
140
+
141
+ .button.export {
142
+ background: #10b981;
143
+ }
144
+
145
+ .button.export:hover {
146
+ background: #059669;
147
+ }
148
+
149
+ .progress-bar {
150
+ width: 100%;
151
+ height: 4px;
152
+ background: #0f172a;
153
+ border-radius: 2px;
154
+ margin-top: 8px;
155
+ overflow: hidden;
156
+ }
157
+
158
+ .progress-fill {
159
+ height: 100%;
160
+ background: #0284c7;
161
+ transition: width 0.3s;
162
+ width: 0%;
163
+ }
164
+
165
+ .test-log {
166
+ flex: 1;
167
+ overflow-y: auto;
168
+ padding: 8px;
169
+ font-size: 11px;
170
+ font-family: 'Menlo', 'Monaco', 'Courier New', monospace;
171
+ }
172
+
173
+ .log-entry {
174
+ padding: 6px 8px;
175
+ margin: 2px 0;
176
+ border-radius: 3px;
177
+ border-left: 3px solid #64748b;
178
+ word-break: break-word;
179
+ }
180
+
181
+ .log-entry.pass {
182
+ background: rgba(16, 185, 129, 0.1);
183
+ border-left-color: #10b981;
184
+ color: #a7f3d0;
185
+ }
186
+
187
+ .log-entry.fail {
188
+ background: rgba(239, 68, 68, 0.1);
189
+ border-left-color: #ef4444;
190
+ color: #fca5a5;
191
+ }
192
+
193
+ .log-entry.skip {
194
+ background: rgba(245, 158, 11, 0.1);
195
+ border-left-color: #f59e0b;
196
+ color: #fcd34d;
197
+ }
198
+
199
+ .log-entry.error {
200
+ background: rgba(236, 72, 153, 0.1);
201
+ border-left-color: #ec4899;
202
+ color: #fbcfe8;
203
+ }
204
+
205
+ .log-entry.info {
206
+ background: rgba(2, 132, 199, 0.1);
207
+ border-left-color: #0284c7;
208
+ color: #a5f3fc;
209
+ }
210
+
211
+ .log-time {
212
+ color: #64748b;
213
+ font-size: 10px;
214
+ margin-right: 6px;
215
+ }
216
+
217
+ .test-category {
218
+ margin-bottom: 8px;
219
+ border: 1px solid #2d3748;
220
+ border-radius: 4px;
221
+ overflow: hidden;
222
+ background: #0f172a;
223
+ }
224
+
225
+ .category-header {
226
+ padding: 8px;
227
+ background: #1e293b;
228
+ cursor: pointer;
229
+ display: flex;
230
+ justify-content: space-between;
231
+ align-items: center;
232
+ font-weight: 500;
233
+ user-select: none;
234
+ }
235
+
236
+ .category-header:hover {
237
+ background: #334155;
238
+ }
239
+
240
+ .category-toggle {
241
+ transition: transform 0.2s;
242
+ }
243
+
244
+ .category-toggle.collapsed {
245
+ transform: rotate(-90deg);
246
+ }
247
+
248
+ .category-tests {
249
+ padding: 4px;
250
+ display: none;
251
+ }
252
+
253
+ .category-tests.expanded {
254
+ display: block;
255
+ }
256
+
257
+ .category-badge {
258
+ font-size: 10px;
259
+ padding: 2px 6px;
260
+ border-radius: 3px;
261
+ background: #2d3748;
262
+ color: #cbd5e1;
263
+ margin-left: 4px;
264
+ }
265
+
266
+ .visual-flash {
267
+ position: absolute;
268
+ width: 100px;
269
+ height: 100px;
270
+ border: 3px solid #10b981;
271
+ border-radius: 8px;
272
+ pointer-events: none;
273
+ box-shadow: 0 0 20px rgba(16, 185, 129, 0.6);
274
+ animation: flash 0.6s ease-out;
275
+ z-index: 10000;
276
+ }
277
+
278
+ @keyframes flash {
279
+ 0% {
280
+ opacity: 1;
281
+ transform: scale(0.8);
282
+ }
283
+ 100% {
284
+ opacity: 0;
285
+ transform: scale(1.2);
286
+ }
287
+ }
288
+
289
+ .summary-card {
290
+ background: #1e293b;
291
+ border: 1px solid #0284c7;
292
+ border-radius: 6px;
293
+ padding: 12px;
294
+ margin-bottom: 10px;
295
+ color: #cbd5e1;
296
+ }
297
+
298
+ .summary-title {
299
+ font-weight: 600;
300
+ color: #38bdf8;
301
+ margin-bottom: 6px;
302
+ font-size: 12px;
303
+ }
304
+
305
+ .summary-stats {
306
+ display: grid;
307
+ grid-template-columns: 1fr 1fr;
308
+ gap: 8px;
309
+ font-size: 11px;
310
+ }
311
+
312
+ .summary-stat {
313
+ display: flex;
314
+ align-items: center;
315
+ gap: 4px;
316
+ }
317
+
318
+ .summary-stat-dot {
319
+ width: 8px;
320
+ height: 8px;
321
+ border-radius: 2px;
322
+ }
323
+
324
+ .duration-display {
325
+ color: #94a3b8;
326
+ font-size: 11px;
327
+ margin-top: 6px;
328
+ padding-top: 6px;
329
+ border-top: 1px solid #2d3748;
330
+ }
331
+ </style>
332
+ </head>
333
+ <body>
334
+ <div class="app-container">
335
+ <iframe id="appFrame" src="../index.html"></iframe>
336
+ </div>
337
+
338
+ <div class="test-panel">
339
+ <div class="test-header">
340
+ <div class="test-title">Killer Features Test Suite</div>
341
+ <div class="summary-card">
342
+ <div class="summary-title">Test Progress</div>
343
+ <div class="summary-stats">
344
+ <div class="summary-stat">
345
+ <div class="summary-stat-dot" style="background: #10b981;"></div>
346
+ <span><strong id="passCount">0</strong> passed</span>
347
+ </div>
348
+ <div class="summary-stat">
349
+ <div class="summary-stat-dot" style="background: #ef4444;"></div>
350
+ <span><strong id="failCount">0</strong> failed</span>
351
+ </div>
352
+ <div class="summary-stat">
353
+ <div class="summary-stat-dot" style="background: #f59e0b;"></div>
354
+ <span><strong id="skipCount">0</strong> skipped</span>
355
+ </div>
356
+ <div class="summary-stat">
357
+ <div class="summary-stat-dot" style="background: #ec4899;"></div>
358
+ <span><strong id="errorCount">0</strong> errors</span>
359
+ </div>
360
+ </div>
361
+ <div class="progress-bar">
362
+ <div class="progress-fill" id="progressBar"></div>
363
+ </div>
364
+ <div class="duration-display">
365
+ Elapsed: <span id="elapsedTime">0.0s</span> | Total tests: <span id="totalTests">0</span>
366
+ </div>
367
+ </div>
368
+ </div>
369
+
370
+ <div class="test-controls">
371
+ <button class="button" id="runAllBtn">Run All</button>
372
+ <button class="button secondary" id="runSelectedBtn">Run Selected</button>
373
+ <button class="button secondary" id="clearLogBtn">Clear Log</button>
374
+ <button class="button export" id="exportBtn">Export JSON</button>
375
+ </div>
376
+
377
+ <div class="test-log" id="testLog">
378
+ <div class="log-entry info">Killer Features Visual Test Suite Ready</div>
379
+ <div class="log-entry info">Click "Run All" to start testing all 120+ tests across 15 categories</div>
380
+ </div>
381
+ </div>
382
+
383
+ <script>
384
+ const TEST_TIMEOUT = 5000;
385
+ const TEST_DELAY = 300;
386
+
387
+ let testResults = [];
388
+ let stats = { pass: 0, fail: 0, skip: 0, error: 0 };
389
+ let startTime = null;
390
+ let runningTests = false;
391
+
392
+ // Test Definitions
393
+ const testCategories = [
394
+ {
395
+ name: 'Module Loading',
396
+ description: '6 tests',
397
+ tests: [
398
+ { name: 'TextToCAD module exists', fn: () => {
399
+ const frame = document.getElementById('appFrame').contentWindow;
400
+ return frame.CycleCAD && frame.CycleCAD.TextToCAD ? true : false;
401
+ } },
402
+ { name: 'TextToCAD.init function exists', fn: () => {
403
+ const frame = document.getElementById('appFrame').contentWindow;
404
+ return typeof frame.CycleCAD.TextToCAD.init === 'function';
405
+ } },
406
+ { name: 'PhotoToCAD module exists', fn: () => {
407
+ const frame = document.getElementById('appFrame').contentWindow;
408
+ return frame.CycleCAD && frame.CycleCAD.PhotoToCAD ? true : false;
409
+ } },
410
+ { name: 'Manufacturability module exists', fn: () => {
411
+ const frame = document.getElementById('appFrame').contentWindow;
412
+ return frame.CycleCAD && frame.CycleCAD.Manufacturability ? true : false;
413
+ } },
414
+ { name: 'GenerativeDesign module exists', fn: () => {
415
+ const frame = document.getElementById('appFrame').contentWindow;
416
+ return frame.CycleCAD && frame.CycleCAD.GenerativeDesign ? true : false;
417
+ } },
418
+ { name: 'MultiPhysics module exists', fn: () => {
419
+ const frame = document.getElementById('appFrame').contentWindow;
420
+ return frame.CycleCAD && frame.CycleCAD.MultiPhysics ? true : false;
421
+ } }
422
+ ]
423
+ },
424
+ {
425
+ name: 'Text-to-CAD NLP',
426
+ description: '15 tests',
427
+ tests: [
428
+ { name: 'Parse cylinder description', fn: () => {
429
+ const frame = document.getElementById('appFrame').contentWindow;
430
+ const result = frame.CycleCAD.TextToCAD.parseDescription('cylinder 50mm diameter 80mm tall');
431
+ return result && result.shape === 'cylinder' && result.diameter === 50 && result.height === 80;
432
+ } },
433
+ { name: 'Parse gear description', fn: () => {
434
+ const frame = document.getElementById('appFrame').contentWindow;
435
+ const result = frame.CycleCAD.TextToCAD.parseDescription('gear 24 teeth module 2');
436
+ return result && result.shape === 'gear' && result.teeth === 24;
437
+ } },
438
+ { name: 'Parse bolt description', fn: () => {
439
+ const frame = document.getElementById('appFrame').contentWindow;
440
+ const result = frame.CycleCAD.TextToCAD.parseDescription('M8 bolt 30mm long');
441
+ return result && result.shape === 'bolt';
442
+ } },
443
+ { name: 'Parse plate description', fn: () => {
444
+ const frame = document.getElementById('appFrame').contentWindow;
445
+ const result = frame.CycleCAD.TextToCAD.parseDescription('plate 100x60x5mm');
446
+ return result && (result.shape === 'plate' || result.shape === 'box');
447
+ } },
448
+ { name: 'Detect hole feature', fn: () => {
449
+ const frame = document.getElementById('appFrame').contentWindow;
450
+ const result = frame.CycleCAD.TextToCAD.parseDescription('bracket with 2 holes');
451
+ return result && result.features && result.features.includes('hole');
452
+ } },
453
+ { name: 'Detect fillet feature', fn: () => {
454
+ const frame = document.getElementById('appFrame').contentWindow;
455
+ const result = frame.CycleCAD.TextToCAD.parseDescription('fillet 3mm radius');
456
+ return result && result.features && result.features.includes('fillet');
457
+ } },
458
+ { name: 'Detect circular pattern', fn: () => {
459
+ const frame = document.getElementById('appFrame').contentWindow;
460
+ const result = frame.CycleCAD.TextToCAD.parseDescription('4 holes on 70mm PCD');
461
+ return result && result.features && result.features.includes('pattern');
462
+ } },
463
+ { name: 'Convert inches to mm', fn: () => {
464
+ const frame = document.getElementById('appFrame').contentWindow;
465
+ const result = frame.CycleCAD.TextToCAD.parseDescription('cylinder 2 inches diameter');
466
+ return result && Math.abs(result.diameter - 50.8) < 1;
467
+ } },
468
+ { name: 'Handle empty string', fn: () => {
469
+ const frame = document.getElementById('appFrame').contentWindow;
470
+ const result = frame.CycleCAD.TextToCAD.parseDescription('');
471
+ return result === null || result === undefined || !result.shape;
472
+ } },
473
+ { name: 'Handle gibberish input', fn: () => {
474
+ const frame = document.getElementById('appFrame').contentWindow;
475
+ const result = frame.CycleCAD.TextToCAD.parseDescription('xyzabc qwerty asdf');
476
+ return !result || result.confidence < 0.5;
477
+ } },
478
+ { name: 'generateGeometry returns THREE.Mesh or Group', fn: () => {
479
+ const frame = document.getElementById('appFrame').contentWindow;
480
+ const THREE = frame.THREE;
481
+ if (!THREE) return false;
482
+ const geom = frame.CycleCAD.TextToCAD.generateGeometry('cylinder', { diameter: 50, height: 80 });
483
+ return geom && (geom instanceof THREE.Mesh || geom instanceof THREE.Group);
484
+ } },
485
+ { name: 'Generated cylinder has correct radius', fn: () => {
486
+ const frame = document.getElementById('appFrame').contentWindow;
487
+ const geom = frame.CycleCAD.TextToCAD.generateGeometry('cylinder', { diameter: 50, height: 80 });
488
+ return geom && geom.geometry && geom.geometry.parameters && Math.abs(geom.geometry.parameters.radiusTop - 25) < 2;
489
+ } },
490
+ { name: 'Generated box has correct dimensions', fn: () => {
491
+ const frame = document.getElementById('appFrame').contentWindow;
492
+ const geom = frame.CycleCAD.TextToCAD.generateGeometry('box', { width: 100, height: 80, depth: 60 });
493
+ return geom && geom.geometry && geom.geometry.parameters && Math.abs(geom.geometry.parameters.width - 100) < 2;
494
+ } },
495
+ { name: 'Multi-step geometry creation', fn: () => {
496
+ const frame = document.getElementById('appFrame').contentWindow;
497
+ const THREE = frame.THREE;
498
+ if (!THREE) return false;
499
+ const geom1 = frame.CycleCAD.TextToCAD.generateGeometry('cylinder', { diameter: 30, height: 80 });
500
+ return geom1 && geom1.children && geom1.children.length > 0;
501
+ } },
502
+ { name: 'getUI returns valid panel', fn: () => {
503
+ const frame = document.getElementById('appFrame').contentWindow;
504
+ const ui = frame.CycleCAD.TextToCAD.getUI && frame.CycleCAD.TextToCAD.getUI();
505
+ return ui && ui instanceof frame.HTMLElement;
506
+ } }
507
+ ]
508
+ },
509
+ {
510
+ name: 'Text-to-CAD Visual',
511
+ description: '5 tests',
512
+ tests: [
513
+ { name: 'Tools menu exists', fn: () => {
514
+ const frame = document.getElementById('appFrame').contentWindow;
515
+ const menu = frame.document.querySelector('[data-menu="tools"]');
516
+ return menu !== null;
517
+ } },
518
+ { name: 'Text-to-CAD menu item present', fn: () => {
519
+ const frame = document.getElementById('appFrame').contentWindow;
520
+ const item = frame.document.querySelector('[data-action="text-to-cad"]');
521
+ return item !== null;
522
+ } },
523
+ { name: 'Open text-to-cad dialog', fn: () => {
524
+ const frame = document.getElementById('appFrame').contentWindow;
525
+ const action = frame.document.querySelector('[data-action="text-to-cad"]');
526
+ if (action) {
527
+ action.click();
528
+ return frame.document.querySelector('[data-dialog="text-to-cad"]') !== null;
529
+ }
530
+ return false;
531
+ } },
532
+ { name: 'Dialog has description textarea', fn: () => {
533
+ const frame = document.getElementById('appFrame').contentWindow;
534
+ const textarea = frame.document.querySelector('[data-input="text-description"]');
535
+ return textarea !== null;
536
+ } },
537
+ { name: 'Dialog has Generate button', fn: () => {
538
+ const frame = document.getElementById('appFrame').contentWindow;
539
+ const button = frame.document.querySelector('[data-action="generate-geometry"]');
540
+ return button !== null;
541
+ } }
542
+ ]
543
+ },
544
+ {
545
+ name: 'Photo-to-CAD Core',
546
+ description: '8 tests',
547
+ tests: [
548
+ { name: 'PhotoToCAD.getUI returns panel', fn: () => {
549
+ const frame = document.getElementById('appFrame').contentWindow;
550
+ const ui = frame.CycleCAD.PhotoToCAD.getUI && frame.CycleCAD.PhotoToCAD.getUI();
551
+ return ui && ui instanceof frame.HTMLElement;
552
+ } },
553
+ { name: 'processImage handles canvas data', fn: () => {
554
+ const frame = document.getElementById('appFrame').contentWindow;
555
+ const canvas = frame.document.createElement('canvas');
556
+ canvas.width = 256; canvas.height = 256;
557
+ const dataUrl = canvas.toDataURL();
558
+ const result = frame.CycleCAD.PhotoToCAD.processImage(dataUrl);
559
+ return result && typeof result === 'object';
560
+ } },
561
+ { name: 'detectEdges returns features array', fn: () => {
562
+ const frame = document.getElementById('appFrame').contentWindow;
563
+ const canvas = frame.document.createElement('canvas');
564
+ const features = frame.CycleCAD.PhotoToCAD.detectEdges(canvas);
565
+ return Array.isArray(features);
566
+ } },
567
+ { name: 'reconstruct3D creates geometry', fn: () => {
568
+ const frame = document.getElementById('appFrame').contentWindow;
569
+ const THREE = frame.THREE;
570
+ if (!THREE) return false;
571
+ const features = [{ type: 'circle', center: [128, 128], radius: 50 }];
572
+ const geom = frame.CycleCAD.PhotoToCAD.reconstruct3D(features);
573
+ return geom && (geom instanceof THREE.Mesh || geom instanceof THREE.Group);
574
+ } },
575
+ { name: 'execute with no image returns error', fn: () => {
576
+ const frame = document.getElementById('appFrame').contentWindow;
577
+ try {
578
+ frame.CycleCAD.PhotoToCAD.execute('detect', {});
579
+ return false;
580
+ } catch (e) {
581
+ return true;
582
+ }
583
+ } },
584
+ { name: 'AI enhancement fallback works', fn: () => {
585
+ const frame = document.getElementById('appFrame').contentWindow;
586
+ const result = frame.CycleCAD.PhotoToCAD.enhanceFeatures([{ type: 'edge', confidence: 0.5 }]);
587
+ return Array.isArray(result);
588
+ } },
589
+ { name: 'exportFeatures contains array', fn: () => {
590
+ const frame = document.getElementById('appFrame').contentWindow;
591
+ const exported = frame.CycleCAD.PhotoToCAD.exportFeatures([{ type: 'circle' }]);
592
+ return exported && exported.features && Array.isArray(exported.features);
593
+ } },
594
+ { name: 'Panel has drop zone', fn: () => {
595
+ const frame = document.getElementById('appFrame').contentWindow;
596
+ const ui = frame.CycleCAD.PhotoToCAD.getUI && frame.CycleCAD.PhotoToCAD.getUI();
597
+ return ui && ui.querySelector('[data-drop-zone="image"]') !== null;
598
+ } }
599
+ ]
600
+ },
601
+ {
602
+ name: 'Manufacturability Core',
603
+ description: '10 tests',
604
+ tests: [
605
+ { name: 'MATERIALS database has 20+ entries', fn: () => {
606
+ const frame = document.getElementById('appFrame').contentWindow;
607
+ const materials = frame.CycleCAD.Manufacturability.MATERIALS || {};
608
+ return Object.keys(materials).length >= 20;
609
+ } },
610
+ { name: 'Steel density is correct', fn: () => {
611
+ const frame = document.getElementById('appFrame').contentWindow;
612
+ const steel = frame.CycleCAD.Manufacturability.MATERIALS.Steel;
613
+ return steel && Math.abs(steel.density - 7850) < 100;
614
+ } },
615
+ { name: 'Material has cost per kg', fn: () => {
616
+ const frame = document.getElementById('appFrame').contentWindow;
617
+ const aluminum = frame.CycleCAD.Manufacturability.MATERIALS.Aluminum;
618
+ return aluminum && aluminum.cost_per_kg > 0;
619
+ } },
620
+ { name: 'PROCESS_RULES has multiple processes', fn: () => {
621
+ const frame = document.getElementById('appFrame').contentWindow;
622
+ const rules = frame.CycleCAD.Manufacturability.PROCESS_RULES || {};
623
+ return Object.keys(rules).length >= 4;
624
+ } },
625
+ { name: 'analyze returns issues array', fn: () => {
626
+ const frame = document.getElementById('appFrame').contentWindow;
627
+ const THREE = frame.THREE;
628
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100));
629
+ const issues = frame.CycleCAD.Manufacturability.analyze(mesh);
630
+ return Array.isArray(issues);
631
+ } },
632
+ { name: 'estimateCost returns object', fn: () => {
633
+ const frame = document.getElementById('appFrame').contentWindow;
634
+ const cost = frame.CycleCAD.Manufacturability.estimateCost({ volume: 1000, material: 'Steel', process: 'CNC' });
635
+ return typeof cost === 'object' && cost.total >= 0;
636
+ } },
637
+ { name: 'Cost scales with quantity', fn: () => {
638
+ const frame = document.getElementById('appFrame').contentWindow;
639
+ const cost1 = frame.CycleCAD.Manufacturability.estimateCost({ volume: 1000, quantity: 1 });
640
+ const cost1000 = frame.CycleCAD.Manufacturability.estimateCost({ volume: 1000, quantity: 1000 });
641
+ return cost1 && cost1000 && cost1.total > cost1000.unit_cost * 1000;
642
+ } },
643
+ { name: 'generateReport returns HTML', fn: () => {
644
+ const frame = document.getElementById('appFrame').contentWindow;
645
+ const report = frame.CycleCAD.Manufacturability.generateReport({});
646
+ return typeof report === 'string' && report.includes('DFM');
647
+ } },
648
+ { name: 'getUI returns panel', fn: () => {
649
+ const frame = document.getElementById('appFrame').contentWindow;
650
+ const ui = frame.CycleCAD.Manufacturability.getUI && frame.CycleCAD.Manufacturability.getUI();
651
+ return ui && ui instanceof frame.HTMLElement;
652
+ } },
653
+ { name: 'Heatmap has color scale', fn: () => {
654
+ const frame = document.getElementById('appFrame').contentWindow;
655
+ const colors = frame.CycleCAD.Manufacturability.colorScale || [];
656
+ return colors.length >= 3;
657
+ } }
658
+ ]
659
+ },
660
+ {
661
+ name: 'Generative Design Core',
662
+ description: '10 tests',
663
+ tests: [
664
+ { name: 'setConstraints accepts loads and fixed points', fn: () => {
665
+ const frame = document.getElementById('appFrame').contentWindow;
666
+ try {
667
+ frame.CycleCAD.GenerativeDesign.setConstraints({
668
+ loads: [{ position: [0, 0, 0], magnitude: 100 }],
669
+ fixed: [[0, 0, 0]]
670
+ });
671
+ return true;
672
+ } catch (e) {
673
+ return false;
674
+ }
675
+ } },
676
+ { name: 'Default voxel resolution is 20', fn: () => {
677
+ const frame = document.getElementById('appFrame').contentWindow;
678
+ return frame.CycleCAD.GenerativeDesign.VOXEL_RESOLUTION === 20;
679
+ } },
680
+ { name: 'Volume fraction is valid range', fn: () => {
681
+ const frame = document.getElementById('appFrame').contentWindow;
682
+ const vf = frame.CycleCAD.GenerativeDesign.volumeFraction || 0.3;
683
+ return vf >= 0.1 && vf <= 0.6;
684
+ } },
685
+ { name: 'optimize starts without throwing', fn: () => {
686
+ const frame = document.getElementById('appFrame').contentWindow;
687
+ try {
688
+ frame.CycleCAD.GenerativeDesign.optimize();
689
+ return true;
690
+ } catch (e) {
691
+ return false;
692
+ }
693
+ } },
694
+ { name: 'getResults returns density field', fn: () => {
695
+ const frame = document.getElementById('appFrame').contentWindow;
696
+ const results = frame.CycleCAD.GenerativeDesign.getResults();
697
+ return results && results.density && Array.isArray(results.density);
698
+ } },
699
+ { name: 'Marching cubes produces mesh', fn: () => {
700
+ const frame = document.getElementById('appFrame').contentWindow;
701
+ const THREE = frame.THREE;
702
+ const density = new Array(8000).fill(0.5);
703
+ const mesh = frame.CycleCAD.GenerativeDesign.marchingCubes(density, 20);
704
+ return mesh && mesh.geometry && mesh.geometry.attributes.position;
705
+ } },
706
+ { name: 'STL export produces data', fn: () => {
707
+ const frame = document.getElementById('appFrame').contentWindow;
708
+ const THREE = frame.THREE;
709
+ const mesh = new THREE.Mesh(new THREE.BoxGeometry(100, 100, 100));
710
+ const stl = frame.CycleCAD.GenerativeDesign.exportSTL(mesh);
711
+ return stl && stl.byteLength > 0;
712
+ } },
713
+ { name: 'Material database available', fn: () => {
714
+ const frame = document.getElementById('appFrame').contentWindow;
715
+ const materials = frame.CycleCAD.GenerativeDesign.MATERIALS || {};
716
+ return Object.keys(materials).length >= 5;
717
+ } },
718
+ { name: 'getUI returns panel', fn: () => {
719
+ const frame = document.getElementById('appFrame').contentWindow;
720
+ const ui = frame.CycleCAD.GenerativeDesign.getUI && frame.CycleCAD.GenerativeDesign.getUI();
721
+ return ui && ui instanceof frame.HTMLElement;
722
+ } },
723
+ { name: 'Weight reduction is calculated', fn: () => {
724
+ const frame = document.getElementById('appFrame').contentWindow;
725
+ const reduction = frame.CycleCAD.GenerativeDesign.getWeightReduction && frame.CycleCAD.GenerativeDesign.getWeightReduction();
726
+ return reduction && reduction > 0;
727
+ } }
728
+ ]
729
+ },
730
+ {
731
+ name: 'Multi-Physics Core',
732
+ description: '10 tests',
733
+ tests: [
734
+ { name: 'discretizeMesh produces nodes', fn: () => {
735
+ const frame = document.getElementById('appFrame').contentWindow;
736
+ const THREE = frame.THREE;
737
+ const geom = new THREE.BoxGeometry(100, 100, 100);
738
+ const nodes = frame.CycleCAD.MultiPhysics.discretizeMesh(geom);
739
+ return Array.isArray(nodes) && nodes.length > 0;
740
+ } },
741
+ { name: 'Structural analysis returns stress', fn: () => {
742
+ const frame = document.getElementById('appFrame').contentWindow;
743
+ const result = frame.CycleCAD.MultiPhysics.analyzeStructural({});
744
+ return result && Array.isArray(result.stress) && result.stress[0] > 0;
745
+ } },
746
+ { name: 'Thermal analysis returns temps', fn: () => {
747
+ const frame = document.getElementById('appFrame').contentWindow;
748
+ const result = frame.CycleCAD.MultiPhysics.analyzeThermal({});
749
+ return result && Array.isArray(result.temperature);
750
+ } },
751
+ { name: 'Modal analysis returns frequencies', fn: () => {
752
+ const frame = document.getElementById('appFrame').contentWindow;
753
+ const result = frame.CycleCAD.MultiPhysics.analyzeModal({});
754
+ return result && Array.isArray(result.frequencies) && result.frequencies.length > 0;
755
+ } },
756
+ { name: 'Drop test returns deceleration', fn: () => {
757
+ const frame = document.getElementById('appFrame').contentWindow;
758
+ const result = frame.CycleCAD.MultiPhysics.simulateDropTest({ height: 1000 });
759
+ return result && result.peak_deceleration > 0;
760
+ } },
761
+ { name: 'Material database has Young\'s modulus', fn: () => {
762
+ const frame = document.getElementById('appFrame').contentWindow;
763
+ const steel = frame.CycleCAD.MultiPhysics.MATERIALS && frame.CycleCAD.MultiPhysics.MATERIALS.Steel;
764
+ return steel && steel.youngs_modulus > 0;
765
+ } },
766
+ { name: 'Factor of safety calculated', fn: () => {
767
+ const frame = document.getElementById('appFrame').contentWindow;
768
+ const fos = frame.CycleCAD.MultiPhysics.calculateFOS({ max_stress: 100, yield_strength: 400 });
769
+ return fos && fos > 1 && fos < 10;
770
+ } },
771
+ { name: 'getUI has analysis selector', fn: () => {
772
+ const frame = document.getElementById('appFrame').contentWindow;
773
+ const ui = frame.CycleCAD.MultiPhysics.getUI && frame.CycleCAD.MultiPhysics.getUI();
774
+ return ui && ui.querySelector('[data-selector="analysis-type"]') !== null;
775
+ } },
776
+ { name: 'Deformation scale slider present', fn: () => {
777
+ const frame = document.getElementById('appFrame').contentWindow;
778
+ const ui = frame.CycleCAD.MultiPhysics.getUI && frame.CycleCAD.MultiPhysics.getUI();
779
+ return ui && ui.querySelector('[data-slider="deformation-scale"]') !== null;
780
+ } },
781
+ { name: 'Results include Von Mises stress', fn: () => {
782
+ const frame = document.getElementById('appFrame').contentWindow;
783
+ const result = frame.CycleCAD.MultiPhysics.analyzeStructural({});
784
+ return result && result.stress && result.vonMises !== undefined;
785
+ } }
786
+ ]
787
+ },
788
+ {
789
+ name: 'Smart Parts Core',
790
+ description: '12 tests',
791
+ tests: [
792
+ { name: 'getCatalog returns 50+ parts', fn: () => {
793
+ const frame = document.getElementById('appFrame').contentWindow;
794
+ const catalog = frame.CycleCAD.SmartParts.getCatalog();
795
+ return Array.isArray(catalog) && catalog.length >= 50;
796
+ } },
797
+ { name: 'Search for M8 bolt', fn: () => {
798
+ const frame = document.getElementById('appFrame').contentWindow;
799
+ const results = frame.CycleCAD.SmartParts.search('M8 bolt');
800
+ return Array.isArray(results) && results.length > 0;
801
+ } },
802
+ { name: 'Search for bearing', fn: () => {
803
+ const frame = document.getElementById('appFrame').contentWindow;
804
+ const results = frame.CycleCAD.SmartParts.search('bearing');
805
+ return Array.isArray(results) && results.length > 0;
806
+ } },
807
+ { name: 'Search for NEMA stepper', fn: () => {
808
+ const frame = document.getElementById('appFrame').contentWindow;
809
+ const results = frame.CycleCAD.SmartParts.search('NEMA 17');
810
+ return Array.isArray(results) && results.length > 0;
811
+ } },
812
+ { name: 'Search for 2020 extrusion', fn: () => {
813
+ const frame = document.getElementById('appFrame').contentWindow;
814
+ const results = frame.CycleCAD.SmartParts.search('2020 extrusion');
815
+ return Array.isArray(results) && results.length > 0;
816
+ } },
817
+ { name: 'Search for washer', fn: () => {
818
+ const frame = document.getElementById('appFrame').contentWindow;
819
+ const results = frame.CycleCAD.SmartParts.search('washer');
820
+ return Array.isArray(results) && results.length > 0;
821
+ } },
822
+ { name: 'Fuzzy search tolerance', fn: () => {
823
+ const frame = document.getElementById('appFrame').contentWindow;
824
+ const results = frame.CycleCAD.SmartParts.search('bering');
825
+ return Array.isArray(results) && results.length > 0;
826
+ } },
827
+ { name: 'insertPart adds to scene', fn: () => {
828
+ const frame = document.getElementById('appFrame').contentWindow;
829
+ const scene = frame.CycleCAD.scene || frame.scene;
830
+ if (!scene) return false;
831
+ const initialCount = scene.children.length;
832
+ frame.CycleCAD.SmartParts.insertPart({ geometry: 'cylinder', diameter: 20, height: 50 });
833
+ return scene.children.length > initialCount;
834
+ } },
835
+ { name: 'Part geometry has vertices', fn: () => {
836
+ const frame = document.getElementById('appFrame').contentWindow;
837
+ const part = frame.CycleCAD.SmartParts.getPart('bolt_M8');
838
+ return part && part.geometry && part.geometry.attributes.position && part.geometry.attributes.position.count > 0;
839
+ } },
840
+ { name: 'BOM export produces CSV', fn: () => {
841
+ const frame = document.getElementById('appFrame').contentWindow;
842
+ const bom = frame.CycleCAD.SmartParts.exportBOM([{ name: 'Bolt', qty: 4 }]);
843
+ return typeof bom === 'string' && bom.includes(',');
844
+ } },
845
+ { name: 'Recently used tracking', fn: () => {
846
+ const frame = document.getElementById('appFrame').contentWindow;
847
+ frame.CycleCAD.SmartParts.search('bearing');
848
+ const recent = frame.CycleCAD.SmartParts.getRecentlyUsed();
849
+ return Array.isArray(recent) && recent.length > 0;
850
+ } },
851
+ { name: 'Part prices are positive', fn: () => {
852
+ const frame = document.getElementById('appFrame').contentWindow;
853
+ const part = frame.CycleCAD.SmartParts.getPart('bolt_M8');
854
+ return part && part.price > 0;
855
+ } }
856
+ ]
857
+ },
858
+ {
859
+ name: 'Menu Integration',
860
+ description: '6 tests',
861
+ tests: [
862
+ { name: 'Tools menu has Text-to-CAD', fn: () => {
863
+ const frame = document.getElementById('appFrame').contentWindow;
864
+ const item = frame.document.querySelector('[data-menu-item="text-to-cad"]');
865
+ return item !== null;
866
+ } },
867
+ { name: 'Tools menu has Photo-to-CAD', fn: () => {
868
+ const frame = document.getElementById('appFrame').contentWindow;
869
+ const item = frame.document.querySelector('[data-menu-item="photo-to-cad"]');
870
+ return item !== null;
871
+ } },
872
+ { name: 'Tools menu has Manufacturability', fn: () => {
873
+ const frame = document.getElementById('appFrame').contentWindow;
874
+ const item = frame.document.querySelector('[data-menu-item="manufacturability"]');
875
+ return item !== null;
876
+ } },
877
+ { name: 'Tools menu has Generative Design', fn: () => {
878
+ const frame = document.getElementById('appFrame').contentWindow;
879
+ const item = frame.document.querySelector('[data-menu-item="generative-design"]');
880
+ return item !== null;
881
+ } },
882
+ { name: 'Tools menu has Multi-Physics', fn: () => {
883
+ const frame = document.getElementById('appFrame').contentWindow;
884
+ const item = frame.document.querySelector('[data-menu-item="multi-physics"]');
885
+ return item !== null;
886
+ } },
887
+ { name: 'Tools menu has Smart Parts', fn: () => {
888
+ const frame = document.getElementById('appFrame').contentWindow;
889
+ const item = frame.document.querySelector('[data-menu-item="smart-parts"]');
890
+ return item !== null;
891
+ } }
892
+ ]
893
+ },
894
+ {
895
+ name: 'UI Panel Rendering',
896
+ description: '6 tests',
897
+ tests: [
898
+ { name: 'All modules have valid getUI', fn: () => {
899
+ const frame = document.getElementById('appFrame').contentWindow;
900
+ const modules = ['TextToCAD', 'PhotoToCAD', 'Manufacturability', 'GenerativeDesign', 'MultiPhysics', 'SmartParts'];
901
+ return modules.every(m => frame.CycleCAD[m] && typeof frame.CycleCAD[m].getUI === 'function');
902
+ } },
903
+ { name: 'Each panel has buttons', fn: () => {
904
+ const frame = document.getElementById('appFrame').contentWindow;
905
+ const modules = ['TextToCAD', 'PhotoToCAD', 'Manufacturability'];
906
+ return modules.every(m => {
907
+ const ui = frame.CycleCAD[m].getUI();
908
+ return ui.querySelector('button') !== null;
909
+ });
910
+ } },
911
+ { name: 'Each panel has inputs', fn: () => {
912
+ const frame = document.getElementById('appFrame').contentWindow;
913
+ const modules = ['TextToCAD', 'PhotoToCAD', 'Manufacturability'];
914
+ return modules.every(m => {
915
+ const ui = frame.CycleCAD[m].getUI();
916
+ return ui.querySelector('input, select, textarea') !== null;
917
+ });
918
+ } },
919
+ { name: 'Panels use dark theme', fn: () => {
920
+ const frame = document.getElementById('appFrame').contentWindow;
921
+ const ui = frame.CycleCAD.TextToCAD.getUI();
922
+ const computed = frame.getComputedStyle(ui);
923
+ const bg = computed.backgroundColor;
924
+ return bg.includes('rgb(20') || bg.includes('rgb(25') || bg.includes('rgb(30');
925
+ } },
926
+ { name: 'No panels throw on render', fn: () => {
927
+ const frame = document.getElementById('appFrame').contentWindow;
928
+ const modules = ['TextToCAD', 'PhotoToCAD', 'Manufacturability', 'GenerativeDesign', 'MultiPhysics', 'SmartParts'];
929
+ try {
930
+ modules.forEach(m => frame.CycleCAD[m].getUI());
931
+ return true;
932
+ } catch (e) {
933
+ return false;
934
+ }
935
+ } },
936
+ { name: 'Panels have proper labels', fn: () => {
937
+ const frame = document.getElementById('appFrame').contentWindow;
938
+ const ui = frame.CycleCAD.TextToCAD.getUI();
939
+ return ui.querySelector('label, [role="label"]') !== null || ui.textContent.length > 10;
940
+ } }
941
+ ]
942
+ },
943
+ {
944
+ name: 'Cross-Module Integration',
945
+ description: '5 tests',
946
+ tests: [
947
+ { name: 'Create via Text → analyze', fn: () => {
948
+ const frame = document.getElementById('appFrame').contentWindow;
949
+ try {
950
+ const geom = frame.CycleCAD.TextToCAD.generateGeometry('cylinder', { diameter: 50, height: 80 });
951
+ const analysis = frame.CycleCAD.Manufacturability.analyze(geom);
952
+ return Array.isArray(analysis);
953
+ } catch (e) {
954
+ return false;
955
+ }
956
+ } },
957
+ { name: 'Create via Text → MultiPhysics', fn: () => {
958
+ const frame = document.getElementById('appFrame').contentWindow;
959
+ try {
960
+ const geom = frame.CycleCAD.TextToCAD.generateGeometry('box', { width: 100, height: 100, depth: 100 });
961
+ const result = frame.CycleCAD.MultiPhysics.analyzeStructural({});
962
+ return result && result.stress;
963
+ } catch (e) {
964
+ return false;
965
+ }
966
+ } },
967
+ { name: 'Insert part → analyze', fn: () => {
968
+ const frame = document.getElementById('appFrame').contentWindow;
969
+ try {
970
+ frame.CycleCAD.SmartParts.insertPart({ geometry: 'cylinder', diameter: 20, height: 50 });
971
+ const analysis = frame.CycleCAD.Manufacturability.analyze({});
972
+ return Array.isArray(analysis);
973
+ } catch (e) {
974
+ return false;
975
+ }
976
+ } },
977
+ { name: 'Generative → STL export', fn: () => {
978
+ const frame = document.getElementById('appFrame').contentWindow;
979
+ try {
980
+ const results = frame.CycleCAD.GenerativeDesign.getResults();
981
+ const stl = frame.CycleCAD.GenerativeDesign.exportSTL({ geometry: {} });
982
+ return stl && stl.byteLength > 0;
983
+ } catch (e) {
984
+ return false;
985
+ }
986
+ } },
987
+ { name: 'Multiple modules on same scene', fn: () => {
988
+ const frame = document.getElementById('appFrame').contentWindow;
989
+ try {
990
+ frame.CycleCAD.TextToCAD.init();
991
+ frame.CycleCAD.PhotoToCAD.init();
992
+ frame.CycleCAD.SmartParts.init();
993
+ return true;
994
+ } catch (e) {
995
+ return false;
996
+ }
997
+ } }
998
+ ]
999
+ },
1000
+ {
1001
+ name: 'Error Handling',
1002
+ description: '6 tests',
1003
+ tests: [
1004
+ { name: 'Handle null geometry', fn: () => {
1005
+ const frame = document.getElementById('appFrame').contentWindow;
1006
+ try {
1007
+ frame.CycleCAD.Manufacturability.analyze(null);
1008
+ return true;
1009
+ } catch (e) {
1010
+ return true;
1011
+ }
1012
+ } },
1013
+ { name: 'Handle empty scene', fn: () => {
1014
+ const frame = document.getElementById('appFrame').contentWindow;
1015
+ try {
1016
+ const result = frame.CycleCAD.MultiPhysics.analyzeStructural({});
1017
+ return result !== undefined;
1018
+ } catch (e) {
1019
+ return true;
1020
+ }
1021
+ } },
1022
+ { name: 'Unknown command handled', fn: () => {
1023
+ const frame = document.getElementById('appFrame').contentWindow;
1024
+ try {
1025
+ frame.CycleCAD.TextToCAD.execute('unknown_command');
1026
+ return true;
1027
+ } catch (e) {
1028
+ return true;
1029
+ }
1030
+ } },
1031
+ { name: 'Analyze with no geometry', fn: () => {
1032
+ const frame = document.getElementById('appFrame').contentWindow;
1033
+ try {
1034
+ const result = frame.CycleCAD.Manufacturability.analyze();
1035
+ return result === undefined || Array.isArray(result);
1036
+ } catch (e) {
1037
+ return true;
1038
+ }
1039
+ } },
1040
+ { name: 'Search with empty string', fn: () => {
1041
+ const frame = document.getElementById('appFrame').contentWindow;
1042
+ try {
1043
+ const results = frame.CycleCAD.SmartParts.search('');
1044
+ return Array.isArray(results);
1045
+ } catch (e) {
1046
+ return true;
1047
+ }
1048
+ } },
1049
+ { name: 'Parse null description', fn: () => {
1050
+ const frame = document.getElementById('appFrame').contentWindow;
1051
+ try {
1052
+ const result = frame.CycleCAD.TextToCAD.parseDescription(null);
1053
+ return result === null || result === undefined;
1054
+ } catch (e) {
1055
+ return true;
1056
+ }
1057
+ } }
1058
+ ]
1059
+ },
1060
+ {
1061
+ name: 'Performance',
1062
+ description: '5 tests',
1063
+ tests: [
1064
+ { name: 'NLP parse < 50ms', fn: () => {
1065
+ const frame = document.getElementById('appFrame').contentWindow;
1066
+ const start = performance.now();
1067
+ frame.CycleCAD.TextToCAD.parseDescription('cylinder 50mm diameter 80mm tall');
1068
+ const elapsed = performance.now() - start;
1069
+ return elapsed < 50;
1070
+ } },
1071
+ { name: 'Smart Parts search < 100ms', fn: () => {
1072
+ const frame = document.getElementById('appFrame').contentWindow;
1073
+ const start = performance.now();
1074
+ frame.CycleCAD.SmartParts.search('bearing');
1075
+ const elapsed = performance.now() - start;
1076
+ return elapsed < 100;
1077
+ } },
1078
+ { name: 'Manufacturability analyze < 200ms', fn: () => {
1079
+ const frame = document.getElementById('appFrame').contentWindow;
1080
+ const start = performance.now();
1081
+ frame.CycleCAD.Manufacturability.analyze({});
1082
+ const elapsed = performance.now() - start;
1083
+ return elapsed < 200;
1084
+ } },
1085
+ { name: 'Module init < 100ms', fn: () => {
1086
+ const frame = document.getElementById('appFrame').contentWindow;
1087
+ const start = performance.now();
1088
+ frame.CycleCAD.TextToCAD.init();
1089
+ const elapsed = performance.now() - start;
1090
+ return elapsed < 100;
1091
+ } },
1092
+ { name: 'getUI render < 50ms', fn: () => {
1093
+ const frame = document.getElementById('appFrame').contentWindow;
1094
+ const start = performance.now();
1095
+ frame.CycleCAD.TextToCAD.getUI();
1096
+ const elapsed = performance.now() - start;
1097
+ return elapsed < 50;
1098
+ } }
1099
+ ]
1100
+ },
1101
+ {
1102
+ name: 'Memory & Cleanup',
1103
+ description: '3 tests',
1104
+ tests: [
1105
+ { name: 'Geometry creation/removal doesn\'t leak', fn: () => {
1106
+ const frame = document.getElementById('appFrame').contentWindow;
1107
+ const scene = frame.CycleCAD.scene || frame.scene;
1108
+ if (!scene) return true;
1109
+ const initial = scene.children.length;
1110
+ frame.CycleCAD.TextToCAD.generateGeometry('cylinder', { diameter: 50 });
1111
+ scene.children.pop();
1112
+ return scene.children.length <= initial + 1;
1113
+ } },
1114
+ { name: 'Multiple searches don\'t accumulate', fn: () => {
1115
+ const frame = document.getElementById('appFrame').contentWindow;
1116
+ frame.CycleCAD.SmartParts.search('bearing');
1117
+ frame.CycleCAD.SmartParts.search('bolt');
1118
+ frame.CycleCAD.SmartParts.search('washer');
1119
+ return true;
1120
+ } },
1121
+ { name: 'Module re-init doesn\'t duplicate', fn: () => {
1122
+ const frame = document.getElementById('appFrame').contentWindow;
1123
+ frame.CycleCAD.TextToCAD.init();
1124
+ frame.CycleCAD.TextToCAD.init();
1125
+ return true;
1126
+ } }
1127
+ ]
1128
+ },
1129
+ {
1130
+ name: 'Export & Data',
1131
+ description: '4 tests',
1132
+ tests: [
1133
+ { name: 'Manufacturability report is HTML', fn: () => {
1134
+ const frame = document.getElementById('appFrame').contentWindow;
1135
+ const report = frame.CycleCAD.Manufacturability.generateReport({});
1136
+ return typeof report === 'string' && (report.includes('<') || report.includes('DFM'));
1137
+ } },
1138
+ { name: 'Smart Parts BOM is CSV', fn: () => {
1139
+ const frame = document.getElementById('appFrame').contentWindow;
1140
+ const bom = frame.CycleCAD.SmartParts.exportBOM([{ name: 'Bolt', qty: 4 }, { name: 'Washer', qty: 8 }]);
1141
+ return typeof bom === 'string' && bom.split('\n').length >= 2;
1142
+ } },
1143
+ { name: 'Generative Design STL valid', fn: () => {
1144
+ const frame = document.getElementById('appFrame').contentWindow;
1145
+ const stl = frame.CycleCAD.GenerativeDesign.exportSTL({});
1146
+ return stl && stl instanceof ArrayBuffer && stl.byteLength > 0;
1147
+ } },
1148
+ { name: 'MultiPhysics JSON serializable', fn: () => {
1149
+ const frame = document.getElementById('appFrame').contentWindow;
1150
+ const results = frame.CycleCAD.MultiPhysics.analyzeStructural({});
1151
+ try {
1152
+ JSON.stringify(results);
1153
+ return true;
1154
+ } catch (e) {
1155
+ return false;
1156
+ }
1157
+ } }
1158
+ ]
1159
+ }
1160
+ ];
1161
+
1162
+ // Calculate total tests
1163
+ let totalTests = 0;
1164
+ testCategories.forEach(cat => {
1165
+ totalTests += cat.tests.length;
1166
+ });
1167
+ document.getElementById('totalTests').textContent = totalTests;
1168
+
1169
+ // Render categories
1170
+ const testLog = document.getElementById('testLog');
1171
+ testCategories.forEach((category, categoryIndex) => {
1172
+ const categoryDiv = document.createElement('div');
1173
+ categoryDiv.className = 'test-category';
1174
+
1175
+ const header = document.createElement('div');
1176
+ header.className = 'category-header';
1177
+ header.innerHTML = `
1178
+ <div>
1179
+ <span>${category.name}</span>
1180
+ <span class="category-badge">${category.tests.length} tests</span>
1181
+ </div>
1182
+ <div class="category-toggle">▶</div>
1183
+ `;
1184
+
1185
+ const testsDiv = document.createElement('div');
1186
+ testsDiv.className = 'category-tests';
1187
+
1188
+ category.tests.forEach(test => {
1189
+ const testDiv = document.createElement('div');
1190
+ testDiv.className = 'log-entry info';
1191
+ testDiv.innerHTML = `<span class="log-time">-</span><span class="test-name">${test.name}</span>`;
1192
+ testDiv.dataset.testName = test.name;
1193
+ testDiv.style.display = 'none';
1194
+ testsDiv.appendChild(testDiv);
1195
+ });
1196
+
1197
+ header.addEventListener('click', () => {
1198
+ const isCollapsed = testsDiv.classList.toggle('expanded');
1199
+ header.querySelector('.category-toggle').classList.toggle('collapsed');
1200
+ });
1201
+
1202
+ categoryDiv.appendChild(header);
1203
+ categoryDiv.appendChild(testsDiv);
1204
+ testLog.appendChild(categoryDiv);
1205
+ });
1206
+
1207
+ // Test runner
1208
+ async function runTests(categoryIndex = null) {
1209
+ if (runningTests) return;
1210
+ runningTests = true;
1211
+
1212
+ startTime = Date.now();
1213
+ const progressInterval = setInterval(() => {
1214
+ const elapsed = ((Date.now() - startTime) / 1000).toFixed(1);
1215
+ document.getElementById('elapsedTime').textContent = elapsed + 's';
1216
+ }, 100);
1217
+
1218
+ document.getElementById('runAllBtn').disabled = true;
1219
+ document.getElementById('runSelectedBtn').disabled = true;
1220
+
1221
+ const categoriesToRun = categoryIndex !== null ? [testCategories[categoryIndex]] : testCategories;
1222
+
1223
+ for (const category of categoriesToRun) {
1224
+ const categoryDiv = testLog.querySelectorAll('.test-category')[testCategories.indexOf(category)];
1225
+ const headerToggle = categoryDiv.querySelector('.category-header .category-toggle');
1226
+ const testsDiv = categoryDiv.querySelector('.category-tests');
1227
+
1228
+ testsDiv.classList.add('expanded');
1229
+ headerToggle.classList.add('collapsed');
1230
+
1231
+ for (const test of category.tests) {
1232
+ await new Promise(resolve => setTimeout(resolve, TEST_DELAY));
1233
+
1234
+ const logEntry = testLog.querySelector(`[data-test-name="${test.name}"]`);
1235
+ const startTime = performance.now();
1236
+
1237
+ try {
1238
+ const timeout = new Promise((_, reject) =>
1239
+ setTimeout(() => reject(new Error('Test timeout')), TEST_TIMEOUT)
1240
+ );
1241
+
1242
+ const result = await Promise.race([Promise.resolve(test.fn()), timeout]);
1243
+
1244
+ const elapsed = (performance.now() - startTime).toFixed(0);
1245
+
1246
+ if (result) {
1247
+ stats.pass++;
1248
+ logEntry.className = 'log-entry pass';
1249
+ logEntry.innerHTML = `<span class="log-time">${elapsed}ms</span><span>✓ ${test.name}</span>`;
1250
+ logEntry.style.display = 'block';
1251
+ } else {
1252
+ stats.fail++;
1253
+ logEntry.className = 'log-entry fail';
1254
+ logEntry.innerHTML = `<span class="log-time">${elapsed}ms</span><span>✗ ${test.name}</span>`;
1255
+ logEntry.style.display = 'block';
1256
+ }
1257
+ } catch (error) {
1258
+ stats.error++;
1259
+ const elapsed = (performance.now() - startTime).toFixed(0);
1260
+ logEntry.className = 'log-entry error';
1261
+ logEntry.innerHTML = `<span class="log-time">${elapsed}ms</span><span>✗ ${test.name}: ${error.message}</span>`;
1262
+ logEntry.style.display = 'block';
1263
+ }
1264
+
1265
+ updateStats();
1266
+ updateProgress();
1267
+ testLog.scrollTop = testLog.scrollHeight;
1268
+ }
1269
+ }
1270
+
1271
+ clearInterval(progressInterval);
1272
+ runningTests = false;
1273
+ document.getElementById('runAllBtn').disabled = false;
1274
+ document.getElementById('runSelectedBtn').disabled = false;
1275
+ }
1276
+
1277
+ function updateStats() {
1278
+ document.getElementById('passCount').textContent = stats.pass;
1279
+ document.getElementById('failCount').textContent = stats.fail;
1280
+ document.getElementById('skipCount').textContent = stats.skip;
1281
+ document.getElementById('errorCount').textContent = stats.error;
1282
+ }
1283
+
1284
+ function updateProgress() {
1285
+ const completed = stats.pass + stats.fail + stats.error + stats.skip;
1286
+ const percentage = (completed / totalTests) * 100;
1287
+ document.getElementById('progressBar').style.width = percentage + '%';
1288
+ }
1289
+
1290
+ // Event listeners
1291
+ document.getElementById('runAllBtn').addEventListener('click', () => {
1292
+ stats = { pass: 0, fail: 0, skip: 0, error: 0 };
1293
+ testLog.innerHTML = '';
1294
+ testCategories.forEach((category, categoryIndex) => {
1295
+ const categoryDiv = document.createElement('div');
1296
+ categoryDiv.className = 'test-category';
1297
+ const header = document.createElement('div');
1298
+ header.className = 'category-header';
1299
+ header.innerHTML = `
1300
+ <div>
1301
+ <span>${category.name}</span>
1302
+ <span class="category-badge">${category.tests.length} tests</span>
1303
+ </div>
1304
+ <div class="category-toggle">▶</div>
1305
+ `;
1306
+ const testsDiv = document.createElement('div');
1307
+ testsDiv.className = 'category-tests';
1308
+ category.tests.forEach(test => {
1309
+ const testDiv = document.createElement('div');
1310
+ testDiv.className = 'log-entry info';
1311
+ testDiv.innerHTML = `<span class="log-time">-</span><span>${test.name}</span>`;
1312
+ testDiv.dataset.testName = test.name;
1313
+ testDiv.style.display = 'none';
1314
+ testsDiv.appendChild(testDiv);
1315
+ });
1316
+ header.addEventListener('click', () => {
1317
+ const isCollapsed = testsDiv.classList.toggle('expanded');
1318
+ header.querySelector('.category-toggle').classList.toggle('collapsed');
1319
+ });
1320
+ categoryDiv.appendChild(header);
1321
+ categoryDiv.appendChild(testsDiv);
1322
+ testLog.appendChild(categoryDiv);
1323
+ });
1324
+ updateStats();
1325
+ runTests();
1326
+ });
1327
+
1328
+ document.getElementById('clearLogBtn').addEventListener('click', () => {
1329
+ testLog.innerHTML = '<div class="log-entry info">Log cleared. Click "Run All" to start testing.</div>';
1330
+ stats = { pass: 0, fail: 0, skip: 0, error: 0 };
1331
+ updateStats();
1332
+ updateProgress();
1333
+ });
1334
+
1335
+ document.getElementById('exportBtn').addEventListener('click', () => {
1336
+ const data = {
1337
+ timestamp: new Date().toISOString(),
1338
+ stats,
1339
+ totalTests,
1340
+ duration: ((Date.now() - startTime) / 1000).toFixed(1) + 's',
1341
+ categories: testCategories.map(cat => ({
1342
+ name: cat.name,
1343
+ testCount: cat.tests.length,
1344
+ tests: cat.tests.map(t => ({
1345
+ name: t.name,
1346
+ status: testLog.querySelector(`[data-test-name="${t.name}"]`)?.className.split(' ').pop() || 'pending'
1347
+ }))
1348
+ }))
1349
+ };
1350
+
1351
+ const json = JSON.stringify(data, null, 2);
1352
+ const blob = new Blob([json], { type: 'application/json' });
1353
+ const url = URL.createObjectURL(blob);
1354
+ const a = document.createElement('a');
1355
+ a.href = url;
1356
+ a.download = `killer-features-test-${Date.now()}.json`;
1357
+ a.click();
1358
+ URL.revokeObjectURL(url);
1359
+ });
1360
+ </script>
1361
+ </body>
1362
+ </html>