cyclecad 3.0.0 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
  2. package/BILLING-INDEX.md +293 -0
  3. package/BILLING-INTEGRATION-GUIDE.md +414 -0
  4. package/COLLABORATION-INDEX.md +440 -0
  5. package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
  6. package/DOCKER-BUILD-MANIFEST.txt +483 -0
  7. package/DOCKER-FILES-REFERENCE.md +440 -0
  8. package/DOCKER-INFRASTRUCTURE.md +475 -0
  9. package/DOCKER-README.md +435 -0
  10. package/Dockerfile +33 -55
  11. package/PWA-FILES-CREATED.txt +350 -0
  12. package/QUICK-START-TESTING.md +126 -0
  13. package/STEP-IMPORT-QUICKSTART.md +347 -0
  14. package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
  15. package/app/css/mobile.css +1074 -0
  16. package/app/icons/generate-icons.js +203 -0
  17. package/app/js/billing-ui.js +990 -0
  18. package/app/js/brep-kernel.js +933 -981
  19. package/app/js/collab-client.js +750 -0
  20. package/app/js/mobile-nav.js +623 -0
  21. package/app/js/mobile-toolbar.js +476 -0
  22. package/app/js/modules/billing-module.js +724 -0
  23. package/app/js/modules/step-module-enhanced.js +938 -0
  24. package/app/js/offline-manager.js +705 -0
  25. package/app/js/responsive-init.js +360 -0
  26. package/app/js/touch-handler.js +429 -0
  27. package/app/manifest.json +211 -0
  28. package/app/offline.html +508 -0
  29. package/app/sw.js +571 -0
  30. package/app/tests/billing-tests.html +779 -0
  31. package/app/tests/brep-tests.html +980 -0
  32. package/app/tests/collab-tests.html +743 -0
  33. package/app/tests/mobile-tests.html +1299 -0
  34. package/app/tests/pwa-tests.html +1134 -0
  35. package/app/tests/step-tests.html +1042 -0
  36. package/app/tests/test-agent-v3.html +719 -0
  37. package/docker-compose.yml +225 -0
  38. package/docs/BILLING-HELP.json +260 -0
  39. package/docs/BILLING-README.md +639 -0
  40. package/docs/BILLING-TUTORIAL.md +736 -0
  41. package/docs/BREP-HELP.json +326 -0
  42. package/docs/BREP-TUTORIAL.md +802 -0
  43. package/docs/COLLABORATION-HELP.json +228 -0
  44. package/docs/COLLABORATION-TUTORIAL.md +818 -0
  45. package/docs/DOCKER-HELP.json +224 -0
  46. package/docs/DOCKER-TUTORIAL.md +974 -0
  47. package/docs/MOBILE-HELP.json +243 -0
  48. package/docs/MOBILE-RESPONSIVE-README.md +378 -0
  49. package/docs/MOBILE-TUTORIAL.md +747 -0
  50. package/docs/PWA-HELP.json +228 -0
  51. package/docs/PWA-README.md +662 -0
  52. package/docs/PWA-TUTORIAL.md +757 -0
  53. package/docs/STEP-HELP.json +481 -0
  54. package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
  55. package/docs/TESTING-GUIDE.md +528 -0
  56. package/docs/TESTING-HELP.json +182 -0
  57. package/fusion-vs-cyclecad.html +1771 -0
  58. package/nginx.conf +237 -0
  59. package/package.json +1 -1
  60. package/server/Dockerfile.converter +51 -0
  61. package/server/Dockerfile.signaling +28 -0
  62. package/server/billing-server.js +487 -0
  63. package/server/converter-enhanced.py +528 -0
  64. package/server/requirements-converter.txt +29 -0
  65. package/server/signaling-server.js +801 -0
  66. package/tests/docker-tests.sh +389 -0
@@ -0,0 +1,1042 @@
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 STEP Import Test Suite</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', sans-serif;
16
+ background: #1e1e1e;
17
+ color: #e0e0e0;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1200px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ h1 {
27
+ margin-bottom: 20px;
28
+ font-size: 24px;
29
+ color: #fff;
30
+ }
31
+
32
+ .test-panel {
33
+ display: grid;
34
+ grid-template-columns: 1fr 1fr;
35
+ gap: 20px;
36
+ margin-bottom: 20px;
37
+ }
38
+
39
+ .controls {
40
+ background: #252526;
41
+ padding: 16px;
42
+ border-radius: 8px;
43
+ border: 1px solid #3e3e42;
44
+ }
45
+
46
+ .controls h2 {
47
+ font-size: 14px;
48
+ margin-bottom: 12px;
49
+ color: #fff;
50
+ }
51
+
52
+ .button-group {
53
+ display: flex;
54
+ gap: 8px;
55
+ margin-bottom: 12px;
56
+ }
57
+
58
+ button {
59
+ flex: 1;
60
+ padding: 8px 12px;
61
+ background: #0284c7;
62
+ color: #fff;
63
+ border: none;
64
+ border-radius: 4px;
65
+ cursor: pointer;
66
+ font-size: 12px;
67
+ font-weight: 500;
68
+ transition: background 0.2s;
69
+ }
70
+
71
+ button:hover {
72
+ background: #0369a1;
73
+ }
74
+
75
+ button:disabled {
76
+ background: #666;
77
+ cursor: not-allowed;
78
+ }
79
+
80
+ .test-categories {
81
+ display: grid;
82
+ grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
83
+ gap: 8px;
84
+ margin-bottom: 12px;
85
+ }
86
+
87
+ .category-btn {
88
+ padding: 8px;
89
+ background: #2d2d30;
90
+ color: #e0e0e0;
91
+ border: 1px solid #3e3e42;
92
+ border-radius: 4px;
93
+ cursor: pointer;
94
+ font-size: 11px;
95
+ transition: all 0.2s;
96
+ }
97
+
98
+ .category-btn:hover {
99
+ background: #3e3e42;
100
+ border-color: #0284c7;
101
+ }
102
+
103
+ .category-btn.active {
104
+ background: #0284c7;
105
+ border-color: #0284c7;
106
+ color: #fff;
107
+ }
108
+
109
+ .stats {
110
+ display: grid;
111
+ grid-template-columns: repeat(4, 1fr);
112
+ gap: 8px;
113
+ margin-bottom: 12px;
114
+ }
115
+
116
+ .stat {
117
+ padding: 8px;
118
+ background: #2d2d30;
119
+ border-radius: 4px;
120
+ text-align: center;
121
+ font-size: 11px;
122
+ }
123
+
124
+ .stat-label {
125
+ color: #999;
126
+ font-size: 10px;
127
+ }
128
+
129
+ .stat-value {
130
+ font-size: 16px;
131
+ font-weight: 600;
132
+ color: #fff;
133
+ margin-top: 4px;
134
+ }
135
+
136
+ .stat.passed .stat-value {
137
+ color: #10b981;
138
+ }
139
+
140
+ .stat.failed .stat-value {
141
+ color: #ef4444;
142
+ }
143
+
144
+ .stat.skipped .stat-value {
145
+ color: #f59e0b;
146
+ }
147
+
148
+ .progress-bar {
149
+ height: 4px;
150
+ background: #3e3e42;
151
+ border-radius: 2px;
152
+ overflow: hidden;
153
+ margin-bottom: 12px;
154
+ }
155
+
156
+ .progress-fill {
157
+ height: 100%;
158
+ background: #10b981;
159
+ width: 0%;
160
+ transition: width 0.3s;
161
+ }
162
+
163
+ .test-list {
164
+ background: #252526;
165
+ padding: 16px;
166
+ border-radius: 8px;
167
+ border: 1px solid #3e3e42;
168
+ max-height: 600px;
169
+ overflow-y: auto;
170
+ }
171
+
172
+ .test-list h2 {
173
+ font-size: 14px;
174
+ margin-bottom: 12px;
175
+ color: #fff;
176
+ }
177
+
178
+ .test-item {
179
+ padding: 8px;
180
+ margin-bottom: 6px;
181
+ background: #1e1e1e;
182
+ border-left: 3px solid #666;
183
+ border-radius: 2px;
184
+ font-size: 12px;
185
+ display: flex;
186
+ align-items: center;
187
+ gap: 8px;
188
+ }
189
+
190
+ .test-item.passed {
191
+ border-left-color: #10b981;
192
+ }
193
+
194
+ .test-item.failed {
195
+ border-left-color: #ef4444;
196
+ }
197
+
198
+ .test-item.skipped {
199
+ border-left-color: #f59e0b;
200
+ opacity: 0.7;
201
+ }
202
+
203
+ .test-item.running {
204
+ border-left-color: #0284c7;
205
+ animation: pulse 1s infinite;
206
+ }
207
+
208
+ @keyframes pulse {
209
+ 0%, 100% { opacity: 1; }
210
+ 50% { opacity: 0.7; }
211
+ }
212
+
213
+ .test-icon {
214
+ font-weight: bold;
215
+ font-size: 14px;
216
+ min-width: 20px;
217
+ }
218
+
219
+ .test-item.passed .test-icon { color: #10b981; }
220
+ .test-item.failed .test-icon { color: #ef4444; }
221
+ .test-item.skipped .test-icon { color: #f59e0b; }
222
+ .test-item.running .test-icon { color: #0284c7; }
223
+
224
+ .test-name {
225
+ flex: 1;
226
+ }
227
+
228
+ .test-time {
229
+ color: #666;
230
+ font-size: 10px;
231
+ min-width: 40px;
232
+ text-align: right;
233
+ }
234
+
235
+ .console {
236
+ background: #1e1e1e;
237
+ padding: 12px;
238
+ border-radius: 4px;
239
+ border: 1px solid #3e3e42;
240
+ font-family: 'Courier New', monospace;
241
+ font-size: 11px;
242
+ max-height: 300px;
243
+ overflow-y: auto;
244
+ margin-top: 12px;
245
+ }
246
+
247
+ .console-line {
248
+ margin-bottom: 2px;
249
+ padding: 2px 4px;
250
+ }
251
+
252
+ .console-line.info { color: #0284c7; }
253
+ .console-line.success { color: #10b981; }
254
+ .console-line.error { color: #ef4444; }
255
+ .console-line.warning { color: #f59e0b; }
256
+
257
+ .export-btn {
258
+ background: #10b981;
259
+ }
260
+
261
+ .export-btn:hover {
262
+ background: #059669;
263
+ }
264
+
265
+ .file-input-wrapper {
266
+ position: relative;
267
+ overflow: hidden;
268
+ display: inline-block;
269
+ width: 100%;
270
+ }
271
+
272
+ .file-input-wrapper input[type="file"] {
273
+ position: absolute;
274
+ left: -9999px;
275
+ }
276
+
277
+ .file-input-btn {
278
+ display: block;
279
+ width: 100%;
280
+ padding: 8px;
281
+ background: #0284c7;
282
+ color: #fff;
283
+ text-align: center;
284
+ cursor: pointer;
285
+ border-radius: 4px;
286
+ font-size: 12px;
287
+ font-weight: 500;
288
+ }
289
+
290
+ .file-input-btn:hover {
291
+ background: #0369a1;
292
+ }
293
+ </style>
294
+ </head>
295
+ <body>
296
+ <div class="container">
297
+ <h1>cycleCAD STEP Import Test Suite v2.0</h1>
298
+
299
+ <div class="test-panel">
300
+ <!-- Left: Controls -->
301
+ <div class="controls">
302
+ <h2>Test Controls</h2>
303
+
304
+ <div class="button-group">
305
+ <button id="runAllBtn">▶ Run All Tests</button>
306
+ <button id="stopBtn" disabled>⏹ Stop</button>
307
+ </div>
308
+
309
+ <h3 style="font-size: 12px; color: #999; margin: 12px 0 8px 0;">Categories</h3>
310
+ <div class="test-categories">
311
+ <button class="category-btn active" data-category="file-picker">File Picker</button>
312
+ <button class="category-btn" data-category="drag-drop">Drag & Drop</button>
313
+ <button class="category-btn" data-category="url-import">URL Import</button>
314
+ <button class="category-btn" data-category="file-validation">File Validation</button>
315
+ <button class="category-btn" data-category="size-routing">Size Routing</button>
316
+ <button class="category-btn" data-category="worker">Worker</button>
317
+ <button class="category-btn" data-category="server">Server</button>
318
+ <button class="category-btn" data-category="cache">Cache</button>
319
+ <button class="category-btn" data-category="progress">Progress</button>
320
+ <button class="category-btn" data-category="cancel">Cancel</button>
321
+ <button class="category-btn" data-category="geometry">Geometry</button>
322
+ <button class="category-btn" data-category="color">Color</button>
323
+ <button class="category-btn" data-category="error-handling">Errors</button>
324
+ <button class="category-btn" data-category="metadata">Metadata</button>
325
+ </div>
326
+
327
+ <h3 style="font-size: 12px; color: #999; margin: 12px 0 8px 0;">File Upload (for manual testing)</h3>
328
+ <div class="file-input-wrapper">
329
+ <input type="file" id="stepFileInput" accept=".step,.stp" />
330
+ <label for="stepFileInput" class="file-input-btn">Select STEP File</label>
331
+ </div>
332
+
333
+ <div class="stats" style="margin-top: 12px;">
334
+ <div class="stat passed">
335
+ <div class="stat-label">Passed</div>
336
+ <div class="stat-value" id="passedCount">0</div>
337
+ </div>
338
+ <div class="stat failed">
339
+ <div class="stat-label">Failed</div>
340
+ <div class="stat-value" id="failedCount">0</div>
341
+ </div>
342
+ <div class="stat skipped">
343
+ <div class="stat-label">Skipped</div>
344
+ <div class="stat-value" id="skippedCount">0</div>
345
+ </div>
346
+ <div class="stat">
347
+ <div class="stat-label">Total</div>
348
+ <div class="stat-value" id="totalCount">0</div>
349
+ </div>
350
+ </div>
351
+
352
+ <div class="progress-bar">
353
+ <div class="progress-fill" id="progressFill"></div>
354
+ </div>
355
+
356
+ <div style="display: flex; gap: 8px; margin-bottom: 8px;">
357
+ <button id="exportJsonBtn" class="export-btn" style="flex: 1;">📊 Export JSON</button>
358
+ <button id="exportHtmlBtn" class="export-btn" style="flex: 1;">📄 Export HTML</button>
359
+ </div>
360
+
361
+ <div style="font-size: 10px; color: #666; line-height: 1.4;">
362
+ <p><strong>Instructions:</strong></p>
363
+ <p>1. Click "Run All Tests" to execute test suite</p>
364
+ <p>2. Tests run automatically with 5s timeout each</p>
365
+ <p>3. Upload STEP file to test real import</p>
366
+ <p>4. Export results as JSON or HTML report</p>
367
+ </div>
368
+ </div>
369
+
370
+ <!-- Right: Test Results -->
371
+ <div class="test-list">
372
+ <h2>Test Results</h2>
373
+ <div id="testResults"></div>
374
+ </div>
375
+ </div>
376
+
377
+ <!-- Console -->
378
+ <div class="console" id="console"></div>
379
+ </div>
380
+
381
+ <script>
382
+ // ===== TEST SUITE STATE =====
383
+ const testSuite = {
384
+ isRunning: false,
385
+ results: [],
386
+ stats: { passed: 0, failed: 0, skipped: 0 },
387
+ tests: [
388
+ // File Picker Tests
389
+ {
390
+ category: 'file-picker',
391
+ name: 'File picker dialog opens',
392
+ async test() {
393
+ const input = document.createElement('input');
394
+ input.type = 'file';
395
+ input.accept = '.step,.stp';
396
+ return input !== null;
397
+ }
398
+ },
399
+ {
400
+ category: 'file-picker',
401
+ name: 'File picker accepts .step files',
402
+ async test() {
403
+ const input = document.createElement('input');
404
+ input.type = 'file';
405
+ input.accept = '.step,.stp';
406
+ return input.accept.includes('.step');
407
+ }
408
+ },
409
+ {
410
+ category: 'file-picker',
411
+ name: 'File picker accepts .stp files',
412
+ async test() {
413
+ const input = document.createElement('input');
414
+ input.type = 'file';
415
+ input.accept = '.step,.stp';
416
+ return input.accept.includes('.stp');
417
+ }
418
+ },
419
+
420
+ // Drag & Drop Tests
421
+ {
422
+ category: 'drag-drop',
423
+ name: 'Drag and drop listener can be attached',
424
+ async test() {
425
+ const div = document.createElement('div');
426
+ let dragEnterCalled = false;
427
+ div.addEventListener('dragenter', () => { dragEnterCalled = true; });
428
+ const event = new DragEvent('dragenter');
429
+ div.dispatchEvent(event);
430
+ return dragEnterCalled;
431
+ }
432
+ },
433
+ {
434
+ category: 'drag-drop',
435
+ name: 'Drop event can be intercepted',
436
+ async test() {
437
+ const div = document.createElement('div');
438
+ let dropCalled = false;
439
+ div.addEventListener('drop', (e) => {
440
+ e.preventDefault();
441
+ dropCalled = true;
442
+ });
443
+ const event = new DragEvent('drop');
444
+ div.dispatchEvent(event);
445
+ return dropCalled;
446
+ }
447
+ },
448
+
449
+ // URL Import Tests
450
+ {
451
+ category: 'url-import',
452
+ name: 'Fetch API available',
453
+ async test() {
454
+ return typeof fetch === 'function';
455
+ }
456
+ },
457
+ {
458
+ category: 'url-import',
459
+ name: 'Can construct download URL',
460
+ async test() {
461
+ const url = new URL('https://example.com/model.step');
462
+ return url.href === 'https://example.com/model.step';
463
+ }
464
+ },
465
+
466
+ // File Validation Tests
467
+ {
468
+ category: 'file-validation',
469
+ name: 'File size validation (small file)',
470
+ async test() {
471
+ const blob = new Blob(['test'], { type: 'application/octet-stream' });
472
+ const file = new File([blob], 'test.step');
473
+ return file.size > 0;
474
+ }
475
+ },
476
+ {
477
+ category: 'file-validation',
478
+ name: 'File name extraction',
479
+ async test() {
480
+ const file = new File([], 'model.step');
481
+ return file.name === 'model.step';
482
+ }
483
+ },
484
+ {
485
+ category: 'file-validation',
486
+ name: 'File type validation',
487
+ async test() {
488
+ const file = new File([], 'model.step', { type: 'application/octet-stream' });
489
+ return file instanceof File;
490
+ }
491
+ },
492
+
493
+ // Size Routing Tests
494
+ {
495
+ category: 'size-routing',
496
+ name: 'Size category: small (<30MB)',
497
+ async test() {
498
+ const size = 10 * 1024 * 1024; // 10MB
499
+ const isSmall = size < 30 * 1024 * 1024;
500
+ return isSmall;
501
+ }
502
+ },
503
+ {
504
+ category: 'size-routing',
505
+ name: 'Size category: medium (30-50MB)',
506
+ async test() {
507
+ const size = 40 * 1024 * 1024; // 40MB
508
+ const isMedium = size >= 30 * 1024 * 1024 && size < 50 * 1024 * 1024;
509
+ return isMedium;
510
+ }
511
+ },
512
+ {
513
+ category: 'size-routing',
514
+ name: 'Size category: large (≥50MB)',
515
+ async test() {
516
+ const size = 80 * 1024 * 1024; // 80MB
517
+ const isLarge = size >= 50 * 1024 * 1024;
518
+ return isLarge;
519
+ }
520
+ },
521
+ {
522
+ category: 'size-routing',
523
+ name: 'Deflection selection (small)',
524
+ async test() {
525
+ const size = 10 * 1024 * 1024;
526
+ const deflection = size < 30 * 1024 * 1024 ? 0.01 : 0.05;
527
+ return deflection === 0.01;
528
+ }
529
+ },
530
+ {
531
+ category: 'size-routing',
532
+ name: 'Deflection selection (large)',
533
+ async test() {
534
+ const size = 80 * 1024 * 1024;
535
+ const deflection = size >= 50 * 1024 * 1024 ? 0.05 : 0.01;
536
+ return deflection === 0.05;
537
+ }
538
+ },
539
+
540
+ // Worker Tests
541
+ {
542
+ category: 'worker',
543
+ name: 'Worker creation',
544
+ async test() {
545
+ const blob = new Blob(['console.log("test");'], { type: 'application/javascript' });
546
+ const url = URL.createObjectURL(blob);
547
+ try {
548
+ const worker = new Worker(url);
549
+ worker.terminate();
550
+ return true;
551
+ } catch (e) {
552
+ return false;
553
+ }
554
+ }
555
+ },
556
+ {
557
+ category: 'worker',
558
+ name: 'Worker message passing',
559
+ async test() {
560
+ const workerCode = `self.onmessage = (e) => { postMessage({reply: e.data.msg}); };`;
561
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
562
+ const url = URL.createObjectURL(blob);
563
+ return new Promise((resolve) => {
564
+ const worker = new Worker(url);
565
+ worker.onmessage = (e) => {
566
+ worker.terminate();
567
+ resolve(e.data.reply === 'hello');
568
+ };
569
+ worker.postMessage({ msg: 'hello' });
570
+ setTimeout(() => {
571
+ worker.terminate();
572
+ resolve(false);
573
+ }, 1000);
574
+ });
575
+ }
576
+ },
577
+ {
578
+ category: 'worker',
579
+ name: 'Worker heartbeat mechanism',
580
+ async test() {
581
+ const workerCode = `
582
+ let interval = setInterval(() => {
583
+ postMessage({ type: 'heartbeat' });
584
+ }, 1000);
585
+ self.onmessage = () => { clearInterval(interval); };
586
+ `;
587
+ const blob = new Blob([workerCode], { type: 'application/javascript' });
588
+ const url = URL.createObjectURL(blob);
589
+ return new Promise((resolve) => {
590
+ const worker = new Worker(url);
591
+ let heartbeatCount = 0;
592
+ worker.onmessage = (e) => {
593
+ if (e.data.type === 'heartbeat') heartbeatCount++;
594
+ };
595
+ setTimeout(() => {
596
+ worker.terminate();
597
+ resolve(heartbeatCount > 0);
598
+ }, 3000);
599
+ });
600
+ }
601
+ },
602
+
603
+ // Server Tests
604
+ {
605
+ category: 'server',
606
+ name: 'Health endpoint construction',
607
+ async test() {
608
+ const baseURL = 'http://localhost:8787/convert';
609
+ const healthURL = baseURL + '/../health';
610
+ return healthURL.includes('health');
611
+ }
612
+ },
613
+ {
614
+ category: 'server',
615
+ name: 'FormData construction',
616
+ async test() {
617
+ const formData = new FormData();
618
+ const blob = new Blob(['test'], { type: 'application/octet-stream' });
619
+ formData.append('file', blob, 'test.step');
620
+ return formData.has('file');
621
+ }
622
+ },
623
+ {
624
+ category: 'server',
625
+ name: 'Server URL configuration',
626
+ async test() {
627
+ const url = 'http://custom.server:9999/convert';
628
+ localStorage.setItem('ev_converter_url', url);
629
+ const retrieved = localStorage.getItem('ev_converter_url');
630
+ localStorage.removeItem('ev_converter_url');
631
+ return retrieved === url;
632
+ }
633
+ },
634
+
635
+ // Cache Tests
636
+ {
637
+ category: 'cache',
638
+ name: 'IndexedDB database opening',
639
+ async test() {
640
+ return new Promise((resolve) => {
641
+ const request = indexedDB.open('cyclecad-test', 1);
642
+ request.onsuccess = () => {
643
+ const db = request.result;
644
+ db.close();
645
+ resolve(true);
646
+ };
647
+ request.onerror = () => resolve(false);
648
+ });
649
+ }
650
+ },
651
+ {
652
+ category: 'cache',
653
+ name: 'Cache key generation (SHA-256)',
654
+ async test() {
655
+ const data = new Uint8Array([1, 2, 3, 4, 5]);
656
+ const hashBuffer = await crypto.subtle.digest('SHA-256', data);
657
+ const hashArray = Array.from(new Uint8Array(hashBuffer));
658
+ return hashArray.length === 32;
659
+ }
660
+ },
661
+ {
662
+ category: 'cache',
663
+ name: 'LocalStorage persistence',
664
+ async test() {
665
+ localStorage.setItem('ev_step_test', 'value123');
666
+ const value = localStorage.getItem('ev_step_test');
667
+ localStorage.removeItem('ev_step_test');
668
+ return value === 'value123';
669
+ }
670
+ },
671
+
672
+ // Progress Tests
673
+ {
674
+ category: 'progress',
675
+ name: 'Progress bar rendering',
676
+ async test() {
677
+ const bar = document.createElement('div');
678
+ bar.style.width = '50%';
679
+ return bar.style.width === '50%';
680
+ }
681
+ },
682
+ {
683
+ category: 'progress',
684
+ name: 'Progress percentage calculation',
685
+ async test() {
686
+ const current = 25;
687
+ const total = 100;
688
+ const percent = Math.round((current / total) * 100);
689
+ return percent === 25;
690
+ }
691
+ },
692
+ {
693
+ category: 'progress',
694
+ name: 'Elapsed time tracking',
695
+ async test() {
696
+ const startTime = Date.now();
697
+ await new Promise(r => setTimeout(r, 100));
698
+ const elapsed = Date.now() - startTime;
699
+ return elapsed >= 100;
700
+ }
701
+ },
702
+
703
+ // Cancel Tests
704
+ {
705
+ category: 'cancel',
706
+ name: 'AbortController creation',
707
+ async test() {
708
+ const controller = new AbortController();
709
+ return controller instanceof AbortController;
710
+ }
711
+ },
712
+ {
713
+ category: 'cancel',
714
+ name: 'Signal abort works',
715
+ async test() {
716
+ const controller = new AbortController();
717
+ controller.abort();
718
+ return controller.signal.aborted === true;
719
+ }
720
+ },
721
+
722
+ // Geometry Tests
723
+ {
724
+ category: 'geometry',
725
+ name: 'BufferGeometry creation',
726
+ async test() {
727
+ // Mock THREE.js
728
+ const mockGeometry = {
729
+ setAttribute: () => {},
730
+ setIndex: () => {},
731
+ computeVertexNormals: () => {}
732
+ };
733
+ return mockGeometry !== null;
734
+ }
735
+ },
736
+ {
737
+ category: 'geometry',
738
+ name: 'Mesh position attribute',
739
+ async test() {
740
+ const positions = new Float32Array([0, 0, 0, 1, 0, 0, 0, 1, 0]);
741
+ return positions.length === 9;
742
+ }
743
+ },
744
+
745
+ // Color Tests
746
+ {
747
+ category: 'color',
748
+ name: 'Color attribute parsing',
749
+ async test() {
750
+ const colors = new Uint8Array([255, 0, 0, 0, 255, 0, 0, 0, 255]);
751
+ return colors.length === 9;
752
+ }
753
+ },
754
+ {
755
+ category: 'color',
756
+ name: 'Hex color conversion',
757
+ async test() {
758
+ const hex = 0xcccccc;
759
+ const r = (hex >> 16) & 255;
760
+ const g = (hex >> 8) & 255;
761
+ const b = hex & 255;
762
+ return r === 204 && g === 204 && b === 204;
763
+ }
764
+ },
765
+
766
+ // Error Handling Tests
767
+ {
768
+ category: 'error-handling',
769
+ name: 'Timeout error creation',
770
+ async test() {
771
+ const error = new Error('WASM timeout (90s)');
772
+ return error.message.includes('timeout');
773
+ }
774
+ },
775
+ {
776
+ category: 'error-handling',
777
+ name: 'Server error handling',
778
+ async test() {
779
+ const error = new Error('Server error: 500');
780
+ return error.message.includes('Server');
781
+ }
782
+ },
783
+ {
784
+ category: 'error-handling',
785
+ name: 'Recovery suggestion logic',
786
+ async test() {
787
+ const error = 'WASM memory exceeded';
788
+ const suggestion = error.includes('memory') ? 'Use server converter' : 'Try again';
789
+ return suggestion === 'Use server converter';
790
+ }
791
+ },
792
+
793
+ // Metadata Tests
794
+ {
795
+ category: 'metadata',
796
+ name: 'Metadata structure',
797
+ async test() {
798
+ const metadata = { partCount: 47, filename: 'model.step' };
799
+ return metadata.partCount > 0 && metadata.filename.includes('.step');
800
+ }
801
+ },
802
+ {
803
+ category: 'metadata',
804
+ name: 'Part count estimation',
805
+ async test() {
806
+ const stepText = 'PART(\'Part1\')PART(\'Part2\')';
807
+ const partCount = (stepText.match(/PART\(/g) || []).length;
808
+ return partCount === 2;
809
+ }
810
+ }
811
+ ]
812
+ };
813
+
814
+ // ===== UTILITY FUNCTIONS =====
815
+ function log(message, type = 'info') {
816
+ const console = document.getElementById('console');
817
+ const line = document.createElement('div');
818
+ line.className = `console-line ${type}`;
819
+ line.textContent = `[${new Date().toLocaleTimeString()}] ${message}`;
820
+ console.appendChild(line);
821
+ console.scrollTop = console.scrollHeight;
822
+ }
823
+
824
+ function updateStats() {
825
+ document.getElementById('passedCount').textContent = testSuite.stats.passed;
826
+ document.getElementById('failedCount').textContent = testSuite.stats.failed;
827
+ document.getElementById('skippedCount').textContent = testSuite.stats.skipped;
828
+ document.getElementById('totalCount').textContent = testSuite.results.length;
829
+
830
+ const total = testSuite.results.length;
831
+ const progress = total > 0 ? (testSuite.stats.passed / total) * 100 : 0;
832
+ document.getElementById('progressFill').style.width = progress + '%';
833
+ }
834
+
835
+ function renderResults() {
836
+ const resultsDiv = document.getElementById('testResults');
837
+ resultsDiv.innerHTML = '';
838
+
839
+ testSuite.results.forEach(result => {
840
+ const item = document.createElement('div');
841
+ item.className = `test-item ${result.status}`;
842
+ item.innerHTML = `
843
+ <span class="test-icon">${result.status === 'passed' ? '✓' : result.status === 'failed' ? '✕' : '○'}</span>
844
+ <span class="test-name">${result.name}</span>
845
+ <span class="test-time">${result.duration}ms</span>
846
+ `;
847
+ resultsDiv.appendChild(item);
848
+ });
849
+ }
850
+
851
+ async function runTest(testDef) {
852
+ const start = performance.now();
853
+ try {
854
+ const result = await Promise.race([
855
+ testDef.test(),
856
+ new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), 5000))
857
+ ]);
858
+ const duration = performance.now() - start;
859
+ return { status: result ? 'passed' : 'failed', duration: Math.round(duration) };
860
+ } catch (e) {
861
+ const duration = performance.now() - start;
862
+ return { status: 'failed', duration: Math.round(duration), error: e.message };
863
+ }
864
+ }
865
+
866
+ async function runAllTests() {
867
+ testSuite.isRunning = true;
868
+ testSuite.results = [];
869
+ testSuite.stats = { passed: 0, failed: 0, skipped: 0 };
870
+
871
+ document.getElementById('runAllBtn').disabled = true;
872
+ document.getElementById('stopBtn').disabled = false;
873
+ document.getElementById('console').innerHTML = '';
874
+
875
+ log('Starting test suite...', 'info');
876
+ log(`Total tests: ${testSuite.tests.length}`, 'info');
877
+
878
+ for (const testDef of testSuite.tests) {
879
+ if (!testSuite.isRunning) break;
880
+
881
+ log(`Running: ${testDef.name}...`, 'info');
882
+ const result = await runTest(testDef);
883
+
884
+ testSuite.results.push({
885
+ name: testDef.name,
886
+ category: testDef.category,
887
+ ...result
888
+ });
889
+
890
+ if (result.status === 'passed') {
891
+ testSuite.stats.passed++;
892
+ log(`✓ ${testDef.name}`, 'success');
893
+ } else {
894
+ testSuite.stats.failed++;
895
+ log(`✕ ${testDef.name}: ${result.error || 'assertion failed'}`, 'error');
896
+ }
897
+
898
+ updateStats();
899
+ renderResults();
900
+ await new Promise(r => setTimeout(r, 100));
901
+ }
902
+
903
+ testSuite.isRunning = false;
904
+ document.getElementById('runAllBtn').disabled = false;
905
+ document.getElementById('stopBtn').disabled = true;
906
+
907
+ const passRate = testSuite.stats.passed / testSuite.results.length * 100;
908
+ log(`Test suite complete: ${passRate.toFixed(1)}% passed`, 'success');
909
+ }
910
+
911
+ // ===== EVENT LISTENERS =====
912
+ document.getElementById('runAllBtn').addEventListener('click', runAllTests);
913
+
914
+ document.getElementById('stopBtn').addEventListener('click', () => {
915
+ testSuite.isRunning = false;
916
+ log('Tests stopped by user', 'warning');
917
+ });
918
+
919
+ document.querySelectorAll('.category-btn').forEach(btn => {
920
+ btn.addEventListener('click', (e) => {
921
+ document.querySelectorAll('.category-btn').forEach(b => b.classList.remove('active'));
922
+ e.target.classList.add('active');
923
+ const category = e.target.dataset.category;
924
+ const filtered = testSuite.tests.filter(t => t.category === category);
925
+ log(`Filtered to ${category}: ${filtered.length} tests`, 'info');
926
+ });
927
+ });
928
+
929
+ document.getElementById('exportJsonBtn').addEventListener('click', () => {
930
+ const json = JSON.stringify(testSuite.results, null, 2);
931
+ const blob = new Blob([json], { type: 'application/json' });
932
+ const url = URL.createObjectURL(blob);
933
+ const a = document.createElement('a');
934
+ a.href = url;
935
+ a.download = `step-tests-${Date.now()}.json`;
936
+ a.click();
937
+ URL.revokeObjectURL(url);
938
+ });
939
+
940
+ document.getElementById('exportHtmlBtn').addEventListener('click', () => {
941
+ const html = `
942
+ <!DOCTYPE html>
943
+ <html>
944
+ <head>
945
+ <title>STEP Import Test Report</title>
946
+ <style>
947
+ body { font-family: Arial; background: #f5f5f5; padding: 20px; }
948
+ .report { background: white; padding: 20px; border-radius: 8px; }
949
+ h1 { color: #333; }
950
+ .summary { display: grid; grid-template-columns: repeat(3, 1fr); gap: 20px; margin: 20px 0; }
951
+ .summary-card { padding: 20px; border-radius: 4px; text-align: center; }
952
+ .summary-card.passed { background: #e8f5e9; }
953
+ .summary-card.failed { background: #ffebee; }
954
+ .summary-card.skipped { background: #fff3e0; }
955
+ .summary-card h3 { margin: 0; font-size: 24px; }
956
+ table { width: 100%; border-collapse: collapse; margin-top: 20px; }
957
+ th { background: #f5f5f5; padding: 10px; text-align: left; }
958
+ td { padding: 10px; border-bottom: 1px solid #eee; }
959
+ tr:hover { background: #f9f9f9; }
960
+ .passed { color: #4caf50; }
961
+ .failed { color: #f44336; }
962
+ .skipped { color: #ff9800; }
963
+ </style>
964
+ </head>
965
+ <body>
966
+ <div class="report">
967
+ <h1>cycleCAD STEP Import Test Report</h1>
968
+ <p>Generated: ${new Date().toLocaleString()}</p>
969
+
970
+ <div class="summary">
971
+ <div class="summary-card passed">
972
+ <h3>${testSuite.stats.passed}</h3>
973
+ <p>Passed</p>
974
+ </div>
975
+ <div class="summary-card failed">
976
+ <h3>${testSuite.stats.failed}</h3>
977
+ <p>Failed</p>
978
+ </div>
979
+ <div class="summary-card skipped">
980
+ <h3>${testSuite.stats.skipped}</h3>
981
+ <p>Skipped</p>
982
+ </div>
983
+ </div>
984
+
985
+ <table>
986
+ <thead>
987
+ <tr>
988
+ <th>Test Name</th>
989
+ <th>Category</th>
990
+ <th>Status</th>
991
+ <th>Duration (ms)</th>
992
+ </tr>
993
+ </thead>
994
+ <tbody>
995
+ ${testSuite.results.map(r => `
996
+ <tr>
997
+ <td>${r.name}</td>
998
+ <td>${r.category}</td>
999
+ <td class="${r.status}">${r.status}</td>
1000
+ <td>${r.duration}</td>
1001
+ </tr>
1002
+ `).join('')}
1003
+ </tbody>
1004
+ </table>
1005
+ </div>
1006
+ </body>
1007
+ </html>
1008
+ `;
1009
+
1010
+ const blob = new Blob([html], { type: 'text/html' });
1011
+ const url = URL.createObjectURL(blob);
1012
+ const a = document.createElement('a');
1013
+ a.href = url;
1014
+ a.download = `step-tests-${Date.now()}.html`;
1015
+ a.click();
1016
+ URL.revokeObjectURL(url);
1017
+ });
1018
+
1019
+ document.getElementById('stepFileInput').addEventListener('change', async (e) => {
1020
+ const file = e.target.files[0];
1021
+ if (file) {
1022
+ log(`Selected file: ${file.name} (${(file.size / 1024 / 1024).toFixed(2)}MB)`, 'info');
1023
+ const metadata = await getMetadata(file);
1024
+ log(`File metadata: ${JSON.stringify(metadata)}`, 'info');
1025
+ }
1026
+ });
1027
+
1028
+ async function getMetadata(file) {
1029
+ return {
1030
+ name: file.name,
1031
+ size: file.size,
1032
+ type: file.type,
1033
+ modified: new Date(file.lastModified).toLocaleString()
1034
+ };
1035
+ }
1036
+
1037
+ // Initialize
1038
+ updateStats();
1039
+ log('Test suite ready. Click "Run All Tests" to start.', 'info');
1040
+ </script>
1041
+ </body>
1042
+ </html>