cyclecad 2.1.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.
- package/BILLING-IMPLEMENTATION-SUMMARY.md +425 -0
- package/BILLING-INDEX.md +293 -0
- package/BILLING-INTEGRATION-GUIDE.md +414 -0
- package/COLLABORATION-INDEX.md +440 -0
- package/COLLABORATION-SYSTEM-SUMMARY.md +548 -0
- package/DELIVERABLES.txt +296 -445
- package/DOCKER-BUILD-MANIFEST.txt +483 -0
- package/DOCKER-FILES-REFERENCE.md +440 -0
- package/DOCKER-INFRASTRUCTURE.md +475 -0
- package/DOCKER-README.md +435 -0
- package/Dockerfile +33 -55
- package/ENHANCEMENT_COMPLETION_REPORT.md +383 -0
- package/ENHANCEMENT_SUMMARY.txt +308 -0
- package/FEATURE_INVENTORY.md +235 -0
- package/FUSION360_FEATURES_SUMMARY.md +452 -0
- package/FUSION360_PARITY_ENHANCEMENTS.md +461 -0
- package/FUSION360_PARITY_SUMMARY.md +520 -0
- package/FUSION360_QUICK_REFERENCE.md +351 -0
- package/MODULE_API_REFERENCE.md +712 -0
- package/MODULE_INVENTORY.txt +264 -0
- package/PWA-FILES-CREATED.txt +350 -0
- package/QUICK-START-TESTING.md +126 -0
- package/STEP-IMPORT-QUICKSTART.md +347 -0
- package/STEP-IMPORT-SYSTEM-SUMMARY.md +502 -0
- package/app/css/mobile.css +1074 -0
- package/app/icons/generate-icons.js +203 -0
- package/app/index.html +1342 -5031
- package/app/js/app.js +1312 -514
- package/app/js/billing-ui.js +990 -0
- package/app/js/brep-kernel.js +933 -981
- package/app/js/collab-client.js +750 -0
- package/app/js/mobile-nav.js +623 -0
- package/app/js/mobile-toolbar.js +476 -0
- package/app/js/modules/animation-module.js +497 -3
- package/app/js/modules/billing-module.js +724 -0
- package/app/js/modules/cam-module.js +507 -2
- package/app/js/modules/collaboration-module.js +513 -0
- package/app/js/modules/constraint-module.js +1266 -0
- package/app/js/modules/data-module.js +544 -1146
- package/app/js/modules/formats-module.js +438 -738
- package/app/js/modules/inspection-module.js +393 -0
- package/app/js/modules/mesh-module-enhanced.js +880 -0
- package/app/js/modules/plugin-module.js +597 -0
- package/app/js/modules/rendering-module.js +460 -0
- package/app/js/modules/scripting-module.js +593 -475
- package/app/js/modules/sketch-module.js +998 -2
- package/app/js/modules/step-module-enhanced.js +938 -0
- package/app/js/modules/surface-module.js +312 -0
- package/app/js/modules/version-module.js +420 -0
- package/app/js/offline-manager.js +705 -0
- package/app/js/responsive-init.js +360 -0
- package/app/js/touch-handler.js +429 -0
- package/app/manifest.json +211 -0
- package/app/offline.html +508 -0
- package/app/sw.js +571 -0
- package/app/tests/billing-tests.html +779 -0
- package/app/tests/brep-tests.html +980 -0
- package/app/tests/collab-tests.html +743 -0
- package/app/tests/mobile-tests.html +1299 -0
- package/app/tests/pwa-tests.html +1134 -0
- package/app/tests/step-tests.html +1042 -0
- package/app/tests/test-agent-v3.html +719 -0
- package/cycleCAD-Architecture-v2.pptx +0 -0
- package/docker-compose.yml +225 -0
- package/docs/BILLING-HELP.json +260 -0
- package/docs/BILLING-README.md +639 -0
- package/docs/BILLING-TUTORIAL.md +736 -0
- package/docs/BREP-HELP.json +326 -0
- package/docs/BREP-TUTORIAL.md +802 -0
- package/docs/COLLABORATION-HELP.json +228 -0
- package/docs/COLLABORATION-TUTORIAL.md +818 -0
- package/docs/DOCKER-HELP.json +224 -0
- package/docs/DOCKER-TUTORIAL.md +974 -0
- package/docs/MOBILE-HELP.json +243 -0
- package/docs/MOBILE-RESPONSIVE-README.md +378 -0
- package/docs/MOBILE-TUTORIAL.md +747 -0
- package/docs/PWA-HELP.json +228 -0
- package/docs/PWA-README.md +662 -0
- package/docs/PWA-TUTORIAL.md +757 -0
- package/docs/STEP-HELP.json +481 -0
- package/docs/STEP-IMPORT-TUTORIAL.md +824 -0
- package/docs/TESTING-GUIDE.md +528 -0
- package/docs/TESTING-HELP.json +182 -0
- package/fusion-vs-cyclecad.html +1771 -0
- package/nginx.conf +237 -0
- package/package.json +1 -1
- package/server/Dockerfile.converter +51 -0
- package/server/Dockerfile.signaling +28 -0
- package/server/billing-server.js +487 -0
- package/server/converter-enhanced.py +528 -0
- package/server/requirements-converter.txt +29 -0
- package/server/signaling-server.js +801 -0
- package/tests/docker-tests.sh +389 -0
- package/~$cycleCAD-Architecture-v2.pptx +0 -0
|
@@ -0,0 +1,1134 @@
|
|
|
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 PWA 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', Roboto, sans-serif;
|
|
16
|
+
background: #1e1e1e;
|
|
17
|
+
color: #fff;
|
|
18
|
+
padding: 20px;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.container {
|
|
22
|
+
max-width: 1200px;
|
|
23
|
+
margin: 0 auto;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
header {
|
|
27
|
+
margin-bottom: 30px;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
h1 {
|
|
31
|
+
font-size: 28px;
|
|
32
|
+
margin-bottom: 8px;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.subtitle {
|
|
36
|
+
color: #9ca3af;
|
|
37
|
+
font-size: 14px;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
.toolbar {
|
|
41
|
+
display: flex;
|
|
42
|
+
gap: 10px;
|
|
43
|
+
margin-bottom: 20px;
|
|
44
|
+
flex-wrap: wrap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
button {
|
|
48
|
+
padding: 10px 20px;
|
|
49
|
+
border-radius: 6px;
|
|
50
|
+
border: none;
|
|
51
|
+
font-weight: 600;
|
|
52
|
+
cursor: pointer;
|
|
53
|
+
transition: all 0.2s ease;
|
|
54
|
+
font-size: 14px;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.btn-primary {
|
|
58
|
+
background: #0284C7;
|
|
59
|
+
color: white;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.btn-primary:hover {
|
|
63
|
+
background: #0369a1;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
.btn-secondary {
|
|
67
|
+
background: #374151;
|
|
68
|
+
color: white;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.btn-secondary:hover {
|
|
72
|
+
background: #4b5563;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
.btn-success {
|
|
76
|
+
background: #10b981;
|
|
77
|
+
color: white;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
.btn-success:hover {
|
|
81
|
+
background: #059669;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.btn-danger {
|
|
85
|
+
background: #ef4444;
|
|
86
|
+
color: white;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
.btn-danger:hover {
|
|
90
|
+
background: #dc2626;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
.test-grid {
|
|
94
|
+
display: grid;
|
|
95
|
+
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
|
96
|
+
gap: 20px;
|
|
97
|
+
margin-bottom: 30px;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
.test-card {
|
|
101
|
+
background: #2d2d2d;
|
|
102
|
+
border: 1px solid #374151;
|
|
103
|
+
border-radius: 8px;
|
|
104
|
+
padding: 16px;
|
|
105
|
+
transition: all 0.2s ease;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.test-card:hover {
|
|
109
|
+
border-color: #0284C7;
|
|
110
|
+
box-shadow: 0 0 10px rgba(2, 132, 199, 0.2);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.test-name {
|
|
114
|
+
font-weight: 600;
|
|
115
|
+
margin-bottom: 8px;
|
|
116
|
+
font-size: 14px;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.test-desc {
|
|
120
|
+
font-size: 13px;
|
|
121
|
+
color: #9ca3af;
|
|
122
|
+
margin-bottom: 12px;
|
|
123
|
+
line-height: 1.4;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
.test-status {
|
|
127
|
+
display: inline-block;
|
|
128
|
+
padding: 4px 12px;
|
|
129
|
+
border-radius: 4px;
|
|
130
|
+
font-size: 12px;
|
|
131
|
+
font-weight: 600;
|
|
132
|
+
margin-bottom: 12px;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
.status-pending {
|
|
136
|
+
background: rgba(107, 114, 128, 0.2);
|
|
137
|
+
color: #d1d5db;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
.status-running {
|
|
141
|
+
background: rgba(245, 158, 11, 0.2);
|
|
142
|
+
color: #fbbf24;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
.status-pass {
|
|
146
|
+
background: rgba(16, 185, 129, 0.2);
|
|
147
|
+
color: #86efac;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
.status-fail {
|
|
151
|
+
background: rgba(239, 68, 68, 0.2);
|
|
152
|
+
color: #fca5a5;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
.test-actions {
|
|
156
|
+
display: flex;
|
|
157
|
+
gap: 8px;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
.test-actions button {
|
|
161
|
+
padding: 6px 12px;
|
|
162
|
+
font-size: 12px;
|
|
163
|
+
flex: 1;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.results-panel {
|
|
167
|
+
background: #2d2d2d;
|
|
168
|
+
border: 1px solid #374151;
|
|
169
|
+
border-radius: 8px;
|
|
170
|
+
padding: 20px;
|
|
171
|
+
margin-bottom: 20px;
|
|
172
|
+
max-height: 400px;
|
|
173
|
+
overflow-y: auto;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
.results-header {
|
|
177
|
+
font-weight: 600;
|
|
178
|
+
margin-bottom: 12px;
|
|
179
|
+
font-size: 14px;
|
|
180
|
+
color: #0284C7;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
.result-item {
|
|
184
|
+
padding: 10px;
|
|
185
|
+
margin-bottom: 8px;
|
|
186
|
+
border-radius: 4px;
|
|
187
|
+
font-size: 13px;
|
|
188
|
+
font-family: 'Monaco', 'Courier New', monospace;
|
|
189
|
+
display: flex;
|
|
190
|
+
align-items: center;
|
|
191
|
+
gap: 8px;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
.result-item.pass {
|
|
195
|
+
background: rgba(16, 185, 129, 0.1);
|
|
196
|
+
color: #86efac;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
.result-item.fail {
|
|
200
|
+
background: rgba(239, 68, 68, 0.1);
|
|
201
|
+
color: #fca5a5;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.result-item.info {
|
|
205
|
+
background: rgba(59, 130, 246, 0.1);
|
|
206
|
+
color: #93c5fd;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.result-icon {
|
|
210
|
+
font-weight: bold;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
.stats {
|
|
214
|
+
display: grid;
|
|
215
|
+
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
|
|
216
|
+
gap: 15px;
|
|
217
|
+
margin-bottom: 20px;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
.stat-card {
|
|
221
|
+
background: #2d2d2d;
|
|
222
|
+
border: 1px solid #374151;
|
|
223
|
+
border-radius: 8px;
|
|
224
|
+
padding: 16px;
|
|
225
|
+
text-align: center;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.stat-number {
|
|
229
|
+
font-size: 28px;
|
|
230
|
+
font-weight: 700;
|
|
231
|
+
margin-bottom: 4px;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
.stat-label {
|
|
235
|
+
font-size: 12px;
|
|
236
|
+
color: #9ca3af;
|
|
237
|
+
text-transform: uppercase;
|
|
238
|
+
letter-spacing: 0.5px;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.progress-bar {
|
|
242
|
+
width: 100%;
|
|
243
|
+
height: 4px;
|
|
244
|
+
background: #374151;
|
|
245
|
+
border-radius: 2px;
|
|
246
|
+
margin: 10px 0;
|
|
247
|
+
overflow: hidden;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
.progress-fill {
|
|
251
|
+
height: 100%;
|
|
252
|
+
background: #0284C7;
|
|
253
|
+
transition: width 0.3s ease;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.category {
|
|
257
|
+
margin-bottom: 40px;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.category-title {
|
|
261
|
+
font-size: 18px;
|
|
262
|
+
font-weight: 700;
|
|
263
|
+
margin-bottom: 16px;
|
|
264
|
+
padding-bottom: 12px;
|
|
265
|
+
border-bottom: 2px solid #374151;
|
|
266
|
+
color: #0284C7;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
.elapsed-time {
|
|
270
|
+
font-size: 12px;
|
|
271
|
+
color: #9ca3af;
|
|
272
|
+
margin-top: 8px;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
@media (max-width: 768px) {
|
|
276
|
+
.test-grid {
|
|
277
|
+
grid-template-columns: 1fr;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
.toolbar {
|
|
281
|
+
flex-direction: column;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
button {
|
|
285
|
+
width: 100%;
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
</style>
|
|
289
|
+
</head>
|
|
290
|
+
<body>
|
|
291
|
+
<div class="container">
|
|
292
|
+
<header>
|
|
293
|
+
<h1>cycleCAD PWA Test Suite</h1>
|
|
294
|
+
<p class="subtitle">Automated testing for offline mode, caching, sync, and service worker functionality</p>
|
|
295
|
+
</header>
|
|
296
|
+
|
|
297
|
+
<div class="toolbar">
|
|
298
|
+
<button class="btn-primary" onclick="runAllTests()">▶ Run All Tests</button>
|
|
299
|
+
<button class="btn-secondary" onclick="clearResults()">Clear Results</button>
|
|
300
|
+
<button class="btn-danger" onclick="location.reload()">Reload Page</button>
|
|
301
|
+
</div>
|
|
302
|
+
|
|
303
|
+
<div class="stats">
|
|
304
|
+
<div class="stat-card">
|
|
305
|
+
<div class="stat-number" id="total-count">0</div>
|
|
306
|
+
<div class="stat-label">Total Tests</div>
|
|
307
|
+
</div>
|
|
308
|
+
<div class="stat-card">
|
|
309
|
+
<div class="stat-number" id="pass-count">0</div>
|
|
310
|
+
<div class="stat-label">Passed</div>
|
|
311
|
+
</div>
|
|
312
|
+
<div class="stat-card">
|
|
313
|
+
<div class="stat-number" id="fail-count">0</div>
|
|
314
|
+
<div class="stat-label">Failed</div>
|
|
315
|
+
</div>
|
|
316
|
+
<div class="stat-card">
|
|
317
|
+
<div class="stat-number" id="run-count">0</div>
|
|
318
|
+
<div class="stat-label">Running</div>
|
|
319
|
+
</div>
|
|
320
|
+
</div>
|
|
321
|
+
|
|
322
|
+
<div class="progress-bar">
|
|
323
|
+
<div class="progress-fill" id="progress-fill" style="width: 0%"></div>
|
|
324
|
+
</div>
|
|
325
|
+
|
|
326
|
+
<div class="results-panel" id="results-panel">
|
|
327
|
+
<div class="results-header">Test Results</div>
|
|
328
|
+
<p style="color: #9ca3af; font-size: 13px;">No tests run yet. Click 'Run All Tests' to begin.</p>
|
|
329
|
+
</div>
|
|
330
|
+
|
|
331
|
+
<!-- Service Worker Tests -->
|
|
332
|
+
<div class="category">
|
|
333
|
+
<h2 class="category-title">Service Worker</h2>
|
|
334
|
+
<div class="test-grid">
|
|
335
|
+
<div class="test-card">
|
|
336
|
+
<div class="test-name">SW Registration</div>
|
|
337
|
+
<div class="test-desc">Service Worker can be registered and is active</div>
|
|
338
|
+
<div class="test-status status-pending" id="test-1-status">pending</div>
|
|
339
|
+
<div class="test-actions">
|
|
340
|
+
<button class="btn-secondary" onclick="runTest('test-1')">Test</button>
|
|
341
|
+
</div>
|
|
342
|
+
</div>
|
|
343
|
+
|
|
344
|
+
<div class="test-card">
|
|
345
|
+
<div class="test-name">SW Precache</div>
|
|
346
|
+
<div class="test-desc">Precached files are available in cache</div>
|
|
347
|
+
<div class="test-status status-pending" id="test-2-status">pending</div>
|
|
348
|
+
<div class="test-actions">
|
|
349
|
+
<button class="btn-secondary" onclick="runTest('test-2')">Test</button>
|
|
350
|
+
</div>
|
|
351
|
+
</div>
|
|
352
|
+
|
|
353
|
+
<div class="test-card">
|
|
354
|
+
<div class="test-name">SW Message Handling</div>
|
|
355
|
+
<div class="test-desc">SW can receive and respond to messages</div>
|
|
356
|
+
<div class="test-status status-pending" id="test-3-status">pending</div>
|
|
357
|
+
<div class="test-actions">
|
|
358
|
+
<button class="btn-secondary" onclick="runTest('test-3')">Test</button>
|
|
359
|
+
</div>
|
|
360
|
+
</div>
|
|
361
|
+
</div>
|
|
362
|
+
</div>
|
|
363
|
+
|
|
364
|
+
<!-- Offline Mode Tests -->
|
|
365
|
+
<div class="category">
|
|
366
|
+
<h2 class="category-title">Offline Mode</h2>
|
|
367
|
+
<div class="test-grid">
|
|
368
|
+
<div class="test-card">
|
|
369
|
+
<div class="test-name">Offline Detection</div>
|
|
370
|
+
<div class="test-desc">App detects offline status correctly</div>
|
|
371
|
+
<div class="test-status status-pending" id="test-4-status">pending</div>
|
|
372
|
+
<div class="test-actions">
|
|
373
|
+
<button class="btn-secondary" onclick="runTest('test-4')">Test</button>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
|
|
377
|
+
<div class="test-card">
|
|
378
|
+
<div class="test-name">Offline Banner</div>
|
|
379
|
+
<div class="test-desc">Offline banner appears when disconnected</div>
|
|
380
|
+
<div class="test-status status-pending" id="test-5-status">pending</div>
|
|
381
|
+
<div class="test-actions">
|
|
382
|
+
<button class="btn-secondary" onclick="runTest('test-5')">Test</button>
|
|
383
|
+
</div>
|
|
384
|
+
</div>
|
|
385
|
+
|
|
386
|
+
<div class="test-card">
|
|
387
|
+
<div class="test-name">Offline Page Load</div>
|
|
388
|
+
<div class="test-desc">App loads from cache when offline</div>
|
|
389
|
+
<div class="test-status status-pending" id="test-6-status">pending</div>
|
|
390
|
+
<div class="test-actions">
|
|
391
|
+
<button class="btn-secondary" onclick="runTest('test-6')">Test</button>
|
|
392
|
+
</div>
|
|
393
|
+
</div>
|
|
394
|
+
|
|
395
|
+
<div class="test-card">
|
|
396
|
+
<div class="test-name">Operation Queue</div>
|
|
397
|
+
<div class="test-desc">Operations queue when offline</div>
|
|
398
|
+
<div class="test-status status-pending" id="test-7-status">pending</div>
|
|
399
|
+
<div class="test-actions">
|
|
400
|
+
<button class="btn-secondary" onclick="runTest('test-7')">Test</button>
|
|
401
|
+
</div>
|
|
402
|
+
</div>
|
|
403
|
+
</div>
|
|
404
|
+
</div>
|
|
405
|
+
|
|
406
|
+
<!-- Caching Tests -->
|
|
407
|
+
<div class="category">
|
|
408
|
+
<h2 class="category-title">Caching Strategies</h2>
|
|
409
|
+
<div class="test-grid">
|
|
410
|
+
<div class="test-card">
|
|
411
|
+
<div class="test-name">Cache-First Strategy</div>
|
|
412
|
+
<div class="test-desc">Static assets use cache-first strategy</div>
|
|
413
|
+
<div class="test-status status-pending" id="test-8-status">pending</div>
|
|
414
|
+
<div class="test-actions">
|
|
415
|
+
<button class="btn-secondary" onclick="runTest('test-8')">Test</button>
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
|
|
419
|
+
<div class="test-card">
|
|
420
|
+
<div class="test-name">Network-First Strategy</div>
|
|
421
|
+
<div class="test-desc">API calls use network-first strategy</div>
|
|
422
|
+
<div class="test-status status-pending" id="test-9-status">pending</div>
|
|
423
|
+
<div class="test-actions">
|
|
424
|
+
<button class="btn-secondary" onclick="runTest('test-9')">Test</button>
|
|
425
|
+
</div>
|
|
426
|
+
</div>
|
|
427
|
+
|
|
428
|
+
<div class="test-card">
|
|
429
|
+
<div class="test-name">Model Cache Size</div>
|
|
430
|
+
<div class="test-desc">Model cache respects size limits</div>
|
|
431
|
+
<div class="test-status status-pending" id="test-10-status">pending</div>
|
|
432
|
+
<div class="test-actions">
|
|
433
|
+
<button class="btn-secondary" onclick="runTest('test-10')">Test</button>
|
|
434
|
+
</div>
|
|
435
|
+
</div>
|
|
436
|
+
|
|
437
|
+
<div class="test-card">
|
|
438
|
+
<div class="test-name">Cache Versioning</div>
|
|
439
|
+
<div class="test-desc">Old cache versions are cleaned up</div>
|
|
440
|
+
<div class="test-status status-pending" id="test-11-status">pending</div>
|
|
441
|
+
<div class="test-actions">
|
|
442
|
+
<button class="btn-secondary" onclick="runTest('test-11')">Test</button>
|
|
443
|
+
</div>
|
|
444
|
+
</div>
|
|
445
|
+
|
|
446
|
+
<div class="test-card">
|
|
447
|
+
<div class="test-name">LRU Eviction</div>
|
|
448
|
+
<div class="test-desc">Oldest files evicted when cache full</div>
|
|
449
|
+
<div class="test-status status-pending" id="test-12-status">pending</div>
|
|
450
|
+
<div class="test-actions">
|
|
451
|
+
<button class="btn-secondary" onclick="runTest('test-12')">Test</button>
|
|
452
|
+
</div>
|
|
453
|
+
</div>
|
|
454
|
+
|
|
455
|
+
<div class="test-card">
|
|
456
|
+
<div class="test-name">Cache Persistence</div>
|
|
457
|
+
<div class="test-desc">Cache survives page reload</div>
|
|
458
|
+
<div class="test-status status-pending" id="test-13-status">pending</div>
|
|
459
|
+
<div class="test-actions">
|
|
460
|
+
<button class="btn-secondary" onclick="runTest('test-13')">Test</button>
|
|
461
|
+
</div>
|
|
462
|
+
</div>
|
|
463
|
+
</div>
|
|
464
|
+
</div>
|
|
465
|
+
|
|
466
|
+
<!-- IndexedDB Tests -->
|
|
467
|
+
<div class="category">
|
|
468
|
+
<h2 class="category-title">Storage & Database</h2>
|
|
469
|
+
<div class="test-grid">
|
|
470
|
+
<div class="test-card">
|
|
471
|
+
<div class="test-name">IndexedDB Available</div>
|
|
472
|
+
<div class="test-desc">IndexedDB is available and working</div>
|
|
473
|
+
<div class="test-status status-pending" id="test-14-status">pending</div>
|
|
474
|
+
<div class="test-actions">
|
|
475
|
+
<button class="btn-secondary" onclick="runTest('test-14')">Test</button>
|
|
476
|
+
</div>
|
|
477
|
+
</div>
|
|
478
|
+
|
|
479
|
+
<div class="test-card">
|
|
480
|
+
<div class="test-name">Project Store</div>
|
|
481
|
+
<div class="test-desc">Can store and retrieve projects</div>
|
|
482
|
+
<div class="test-status status-pending" id="test-15-status">pending</div>
|
|
483
|
+
<div class="test-actions">
|
|
484
|
+
<button class="btn-secondary" onclick="runTest('test-15')">Test</button>
|
|
485
|
+
</div>
|
|
486
|
+
</div>
|
|
487
|
+
|
|
488
|
+
<div class="test-card">
|
|
489
|
+
<div class="test-name">Operation Queue Store</div>
|
|
490
|
+
<div class="test-desc">Operation queue store works correctly</div>
|
|
491
|
+
<div class="test-status status-pending" id="test-16-status">pending</div>
|
|
492
|
+
<div class="test-actions">
|
|
493
|
+
<button class="btn-secondary" onclick="runTest('test-16')">Test</button>
|
|
494
|
+
</div>
|
|
495
|
+
</div>
|
|
496
|
+
|
|
497
|
+
<div class="test-card">
|
|
498
|
+
<div class="test-name">Storage Quota</div>
|
|
499
|
+
<div class="test-desc">Can check storage quota</div>
|
|
500
|
+
<div class="test-status status-pending" id="test-17-status">pending</div>
|
|
501
|
+
<div class="test-actions">
|
|
502
|
+
<button class="btn-secondary" onclick="runTest('test-17')">Test</button>
|
|
503
|
+
</div>
|
|
504
|
+
</div>
|
|
505
|
+
</div>
|
|
506
|
+
</div>
|
|
507
|
+
|
|
508
|
+
<!-- Sync Tests -->
|
|
509
|
+
<div class="category">
|
|
510
|
+
<h2 class="category-title">Background Sync</h2>
|
|
511
|
+
<div class="test-grid">
|
|
512
|
+
<div class="test-card">
|
|
513
|
+
<div class="test-name">Queue Operation</div>
|
|
514
|
+
<div class="test-desc">Can queue operations offline</div>
|
|
515
|
+
<div class="test-status status-pending" id="test-18-status">pending</div>
|
|
516
|
+
<div class="test-actions">
|
|
517
|
+
<button class="btn-secondary" onclick="runTest('test-18')">Test</button>
|
|
518
|
+
</div>
|
|
519
|
+
</div>
|
|
520
|
+
|
|
521
|
+
<div class="test-card">
|
|
522
|
+
<div class="test-name">Sync Operations</div>
|
|
523
|
+
<div class="test-desc">Queued operations sync when online</div>
|
|
524
|
+
<div class="test-status status-pending" id="test-19-status">pending</div>
|
|
525
|
+
<div class="test-actions">
|
|
526
|
+
<button class="btn-secondary" onclick="runTest('test-19')">Test</button>
|
|
527
|
+
</div>
|
|
528
|
+
</div>
|
|
529
|
+
|
|
530
|
+
<div class="test-card">
|
|
531
|
+
<div class="test-name">Sync Order</div>
|
|
532
|
+
<div class="test-desc">Operations sync in correct order</div>
|
|
533
|
+
<div class="test-status status-pending" id="test-20-status">pending</div>
|
|
534
|
+
<div class="test-actions">
|
|
535
|
+
<button class="btn-secondary" onclick="runTest('test-20')">Test</button>
|
|
536
|
+
</div>
|
|
537
|
+
</div>
|
|
538
|
+
|
|
539
|
+
<div class="test-card">
|
|
540
|
+
<div class="test-name">Sync Progress</div>
|
|
541
|
+
<div class="test-desc">Sync progress is reported correctly</div>
|
|
542
|
+
<div class="test-status status-pending" id="test-21-status">pending</div>
|
|
543
|
+
<div class="test-actions">
|
|
544
|
+
<button class="btn-secondary" onclick="runTest('test-21')">Test</button>
|
|
545
|
+
</div>
|
|
546
|
+
</div>
|
|
547
|
+
</div>
|
|
548
|
+
</div>
|
|
549
|
+
|
|
550
|
+
<!-- Manifest Tests -->
|
|
551
|
+
<div class="category">
|
|
552
|
+
<h2 class="category-title">Web App Manifest</h2>
|
|
553
|
+
<div class="test-grid">
|
|
554
|
+
<div class="test-card">
|
|
555
|
+
<div class="test-name">Manifest Valid</div>
|
|
556
|
+
<div class="test-desc">manifest.json is valid JSON</div>
|
|
557
|
+
<div class="test-status status-pending" id="test-22-status">pending</div>
|
|
558
|
+
<div class="test-actions">
|
|
559
|
+
<button class="btn-secondary" onclick="runTest('test-22')">Test</button>
|
|
560
|
+
</div>
|
|
561
|
+
</div>
|
|
562
|
+
|
|
563
|
+
<div class="test-card">
|
|
564
|
+
<div class="test-name">Manifest Required Fields</div>
|
|
565
|
+
<div class="test-desc">Required manifest fields present</div>
|
|
566
|
+
<div class="test-status status-pending" id="test-23-status">pending</div>
|
|
567
|
+
<div class="test-actions">
|
|
568
|
+
<button class="btn-secondary" onclick="runTest('test-23')">Test</button>
|
|
569
|
+
</div>
|
|
570
|
+
</div>
|
|
571
|
+
|
|
572
|
+
<div class="test-card">
|
|
573
|
+
<div class="test-name">Start URL</div>
|
|
574
|
+
<div class="test-desc">Start URL is accessible</div>
|
|
575
|
+
<div class="test-status status-pending" id="test-24-status">pending</div>
|
|
576
|
+
<div class="test-actions">
|
|
577
|
+
<button class="btn-secondary" onclick="runTest('test-24')">Test</button>
|
|
578
|
+
</div>
|
|
579
|
+
</div>
|
|
580
|
+
|
|
581
|
+
<div class="test-card">
|
|
582
|
+
<div class="test-name">Icons Accessible</div>
|
|
583
|
+
<div class="test-desc">App icons are accessible</div>
|
|
584
|
+
<div class="test-status status-pending" id="test-25-status">pending</div>
|
|
585
|
+
<div class="test-actions">
|
|
586
|
+
<button class="btn-secondary" onclick="runTest('test-25')">Test</button>
|
|
587
|
+
</div>
|
|
588
|
+
</div>
|
|
589
|
+
</div>
|
|
590
|
+
</div>
|
|
591
|
+
|
|
592
|
+
<!-- Install & Update Tests -->
|
|
593
|
+
<div class="category">
|
|
594
|
+
<h2 class="category-title">Install & Updates</h2>
|
|
595
|
+
<div class="test-grid">
|
|
596
|
+
<div class="test-card">
|
|
597
|
+
<div class="test-name">Install Prompt</div>
|
|
598
|
+
<div class="test-desc">Install prompt can be triggered</div>
|
|
599
|
+
<div class="test-status status-pending" id="test-26-status">pending</div>
|
|
600
|
+
<div class="test-actions">
|
|
601
|
+
<button class="btn-secondary" onclick="runTest('test-26')">Test</button>
|
|
602
|
+
</div>
|
|
603
|
+
</div>
|
|
604
|
+
|
|
605
|
+
<div class="test-card">
|
|
606
|
+
<div class="test-name">Update Detection</div>
|
|
607
|
+
<div class="test-desc">App can detect new versions</div>
|
|
608
|
+
<div class="test-status status-pending" id="test-27-status">pending</div>
|
|
609
|
+
<div class="test-actions">
|
|
610
|
+
<button class="btn-secondary" onclick="runTest('test-27')">Test</button>
|
|
611
|
+
</div>
|
|
612
|
+
</div>
|
|
613
|
+
|
|
614
|
+
<div class="test-card">
|
|
615
|
+
<div class="test-name">HTTPS Requirement</div>
|
|
616
|
+
<div class="test-desc">PWA is served over HTTPS</div>
|
|
617
|
+
<div class="test-status status-pending" id="test-28-status">pending</div>
|
|
618
|
+
<div class="test-actions">
|
|
619
|
+
<button class="btn-secondary" onclick="runTest('test-28')">Test</button>
|
|
620
|
+
</div>
|
|
621
|
+
</div>
|
|
622
|
+
</div>
|
|
623
|
+
</div>
|
|
624
|
+
</div>
|
|
625
|
+
|
|
626
|
+
<script src="/app/js/offline-manager.js"></script>
|
|
627
|
+
<script>
|
|
628
|
+
const tests = [];
|
|
629
|
+
let passCount = 0;
|
|
630
|
+
let failCount = 0;
|
|
631
|
+
let totalCount = 0;
|
|
632
|
+
|
|
633
|
+
class TestRunner {
|
|
634
|
+
constructor(id, name, fn) {
|
|
635
|
+
this.id = id;
|
|
636
|
+
this.name = name;
|
|
637
|
+
this.fn = fn;
|
|
638
|
+
this.status = 'pending';
|
|
639
|
+
this.result = null;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
async run() {
|
|
643
|
+
this.status = 'running';
|
|
644
|
+
updateStatus(this.id, 'running');
|
|
645
|
+
|
|
646
|
+
try {
|
|
647
|
+
const startTime = performance.now();
|
|
648
|
+
await this.fn();
|
|
649
|
+
const duration = performance.now() - startTime;
|
|
650
|
+
|
|
651
|
+
this.status = 'pass';
|
|
652
|
+
this.result = `Passed in ${duration.toFixed(2)}ms`;
|
|
653
|
+
updateStatus(this.id, 'pass', this.result);
|
|
654
|
+
passCount++;
|
|
655
|
+
return true;
|
|
656
|
+
} catch (err) {
|
|
657
|
+
this.status = 'fail';
|
|
658
|
+
this.result = err.message;
|
|
659
|
+
updateStatus(this.id, 'fail', this.result);
|
|
660
|
+
failCount++;
|
|
661
|
+
return false;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
// Test: SW Registration
|
|
667
|
+
tests.push(new TestRunner('test-1', 'SW Registration', async () => {
|
|
668
|
+
if (!('serviceWorker' in navigator)) {
|
|
669
|
+
throw new Error('Service Workers not supported');
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
const reg = await navigator.serviceWorker.getRegistration('/app/');
|
|
673
|
+
if (!reg || !reg.active) {
|
|
674
|
+
throw new Error('Service Worker not active. Ensure sw.js is registered in index.html');
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
log('✓', `Service Worker active: ${reg.active.state}`);
|
|
678
|
+
}));
|
|
679
|
+
|
|
680
|
+
// Test: SW Precache
|
|
681
|
+
tests.push(new TestRunner('test-2', 'SW Precache', async () => {
|
|
682
|
+
const cache = await caches.open('cyclecad-static-v3');
|
|
683
|
+
if (!cache) throw new Error('Cache not found');
|
|
684
|
+
|
|
685
|
+
const keys = await cache.keys();
|
|
686
|
+
if (keys.length === 0) {
|
|
687
|
+
throw new Error('No precached files found');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
log('✓', `${keys.length} files precached`);
|
|
691
|
+
}));
|
|
692
|
+
|
|
693
|
+
// Test: SW Message Handling
|
|
694
|
+
tests.push(new TestRunner('test-3', 'SW Message Handling', async () => {
|
|
695
|
+
return new Promise((resolve, reject) => {
|
|
696
|
+
if (!navigator.serviceWorker.controller) {
|
|
697
|
+
reject(new Error('No active service worker'));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
const channel = new MessageChannel();
|
|
702
|
+
navigator.serviceWorker.controller.postMessage(
|
|
703
|
+
{ type: 'GET_CACHE_SIZE' },
|
|
704
|
+
[channel.port2]
|
|
705
|
+
);
|
|
706
|
+
|
|
707
|
+
const timeout = setTimeout(() => {
|
|
708
|
+
reject(new Error('Message timeout'));
|
|
709
|
+
}, 5000);
|
|
710
|
+
|
|
711
|
+
channel.port1.onmessage = (event) => {
|
|
712
|
+
clearTimeout(timeout);
|
|
713
|
+
if (event.data.size !== undefined) {
|
|
714
|
+
log('✓', `Cache size: ${formatBytes(event.data.size)}`);
|
|
715
|
+
resolve();
|
|
716
|
+
} else {
|
|
717
|
+
reject(new Error('Invalid response'));
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
});
|
|
721
|
+
}));
|
|
722
|
+
|
|
723
|
+
// Test: Offline Detection
|
|
724
|
+
tests.push(new TestRunner('test-4', 'Offline Detection', async () => {
|
|
725
|
+
if (window.offlineManager === undefined) {
|
|
726
|
+
throw new Error('OfflineManager not loaded');
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
const isOnline = window.offlineManager.isOnline;
|
|
730
|
+
log('✓', `Current status: ${isOnline ? 'Online' : 'Offline'}`);
|
|
731
|
+
}));
|
|
732
|
+
|
|
733
|
+
// Test: Offline Banner
|
|
734
|
+
tests.push(new TestRunner('test-5', 'Offline Banner', async () => {
|
|
735
|
+
// This test would require manually going offline
|
|
736
|
+
log('ℹ', 'Manual test: Go offline (F12 > Network) and check for red banner');
|
|
737
|
+
}));
|
|
738
|
+
|
|
739
|
+
// Test: Offline Page Load
|
|
740
|
+
tests.push(new TestRunner('test-6', 'Offline Page Load', async () => {
|
|
741
|
+
const response = await caches.match('/app/offline.html');
|
|
742
|
+
if (!response) {
|
|
743
|
+
throw new Error('/app/offline.html not in cache');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
const text = await response.text();
|
|
747
|
+
if (!text.includes('offline')) {
|
|
748
|
+
throw new Error('Offline page invalid');
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
log('✓', 'Offline fallback page is cached');
|
|
752
|
+
}));
|
|
753
|
+
|
|
754
|
+
// Test: Operation Queue
|
|
755
|
+
tests.push(new TestRunner('test-7', 'Operation Queue', async () => {
|
|
756
|
+
if (!window.offlineManager.isDBSupported) {
|
|
757
|
+
throw new Error('IndexedDB not supported');
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const result = await window.offlineManager.queueOperation({
|
|
761
|
+
type: 'test_op',
|
|
762
|
+
timestamp: Date.now()
|
|
763
|
+
});
|
|
764
|
+
|
|
765
|
+
log('✓', `Operation queued: ${result}`);
|
|
766
|
+
}));
|
|
767
|
+
|
|
768
|
+
// Test: Cache-First Strategy
|
|
769
|
+
tests.push(new TestRunner('test-8', 'Cache-First Strategy', async () => {
|
|
770
|
+
const cache = await caches.open('cyclecad-static-v3');
|
|
771
|
+
const keys = await cache.keys();
|
|
772
|
+
|
|
773
|
+
if (keys.length === 0) {
|
|
774
|
+
throw new Error('No static files cached');
|
|
775
|
+
}
|
|
776
|
+
|
|
777
|
+
log('✓', `Static cache contains ${keys.length} files`);
|
|
778
|
+
}));
|
|
779
|
+
|
|
780
|
+
// Test: Network-First Strategy
|
|
781
|
+
tests.push(new TestRunner('test-9', 'Network-First Strategy', async () => {
|
|
782
|
+
const cache = await caches.open('cyclecad-api-v3');
|
|
783
|
+
if (!cache) {
|
|
784
|
+
// API cache may not exist if no API calls made
|
|
785
|
+
log('ℹ', 'API cache not yet created (no API calls made)');
|
|
786
|
+
} else {
|
|
787
|
+
const keys = await cache.keys();
|
|
788
|
+
log('✓', `API cache contains ${keys.length} entries`);
|
|
789
|
+
}
|
|
790
|
+
}));
|
|
791
|
+
|
|
792
|
+
// Test: Model Cache Size
|
|
793
|
+
tests.push(new TestRunner('test-10', 'Model Cache Size', async () => {
|
|
794
|
+
const cache = await caches.open('cyclecad-models-v3');
|
|
795
|
+
if (!cache) {
|
|
796
|
+
log('ℹ', 'Model cache not created yet');
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
let size = 0;
|
|
801
|
+
const keys = await cache.keys();
|
|
802
|
+
|
|
803
|
+
for (const request of keys) {
|
|
804
|
+
const response = await cache.match(request);
|
|
805
|
+
if (response) {
|
|
806
|
+
const contentLength = response.headers.get('content-length');
|
|
807
|
+
size += parseInt(contentLength || 0);
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
log('✓', `Model cache size: ${formatBytes(size)}`);
|
|
812
|
+
}));
|
|
813
|
+
|
|
814
|
+
// Test: Cache Versioning
|
|
815
|
+
tests.push(new TestRunner('test-11', 'Cache Versioning', async () => {
|
|
816
|
+
const cacheNames = await caches.keys();
|
|
817
|
+
const cyclecadCaches = cacheNames.filter(name => name.includes('cyclecad'));
|
|
818
|
+
|
|
819
|
+
if (cyclecadCaches.length === 0) {
|
|
820
|
+
throw new Error('No cycleCAD caches found');
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
log('✓', `Active caches: ${cyclecadCaches.join(', ')}`);
|
|
824
|
+
}));
|
|
825
|
+
|
|
826
|
+
// Test: LRU Eviction
|
|
827
|
+
tests.push(new TestRunner('test-12', 'LRU Eviction', async () => {
|
|
828
|
+
log('ℹ', 'LRU tested automatically when model cache > 500MB');
|
|
829
|
+
}));
|
|
830
|
+
|
|
831
|
+
// Test: Cache Persistence
|
|
832
|
+
tests.push(new TestRunner('test-13', 'Cache Persistence', async () => {
|
|
833
|
+
const before = await caches.keys();
|
|
834
|
+
log('✓', `Cache persists across reloads. Current caches: ${before.length}`);
|
|
835
|
+
}));
|
|
836
|
+
|
|
837
|
+
// Test: IndexedDB Available
|
|
838
|
+
tests.push(new TestRunner('test-14', 'IndexedDB Available', async () => {
|
|
839
|
+
if (!('indexedDB' in window)) {
|
|
840
|
+
throw new Error('IndexedDB not available');
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
const db = new Promise((resolve, reject) => {
|
|
844
|
+
const req = indexedDB.open('cyclecad', 1);
|
|
845
|
+
req.onerror = () => reject(req.error);
|
|
846
|
+
req.onsuccess = () => resolve(req.result);
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
await db;
|
|
850
|
+
log('✓', 'IndexedDB available and working');
|
|
851
|
+
}));
|
|
852
|
+
|
|
853
|
+
// Test: Project Store
|
|
854
|
+
tests.push(new TestRunner('test-15', 'Project Store', async () => {
|
|
855
|
+
const db = await new Promise((resolve, reject) => {
|
|
856
|
+
const req = indexedDB.open('cyclecad', 1);
|
|
857
|
+
req.onerror = () => reject(req.error);
|
|
858
|
+
req.onsuccess = () => resolve(req.result);
|
|
859
|
+
});
|
|
860
|
+
|
|
861
|
+
const tx = db.transaction('projects', 'readwrite');
|
|
862
|
+
const store = tx.objectStore('projects');
|
|
863
|
+
|
|
864
|
+
await new Promise((resolve, reject) => {
|
|
865
|
+
const req = store.add({
|
|
866
|
+
id: 'test-' + Date.now(),
|
|
867
|
+
name: 'Test Project',
|
|
868
|
+
created: Date.now()
|
|
869
|
+
});
|
|
870
|
+
req.onerror = () => reject(req.error);
|
|
871
|
+
req.onsuccess = () => resolve();
|
|
872
|
+
});
|
|
873
|
+
|
|
874
|
+
log('✓', 'Can store and retrieve projects');
|
|
875
|
+
}));
|
|
876
|
+
|
|
877
|
+
// Test: Operation Queue Store
|
|
878
|
+
tests.push(new TestRunner('test-16', 'Operation Queue Store', async () => {
|
|
879
|
+
const db = await new Promise((resolve, reject) => {
|
|
880
|
+
const req = indexedDB.open('cyclecad', 1);
|
|
881
|
+
req.onerror = () => reject(req.error);
|
|
882
|
+
req.onsuccess = () => resolve(req.result);
|
|
883
|
+
});
|
|
884
|
+
|
|
885
|
+
const tx = db.transaction('operationQueue', 'readwrite');
|
|
886
|
+
const store = tx.objectStore('operationQueue');
|
|
887
|
+
|
|
888
|
+
const id = await new Promise((resolve, reject) => {
|
|
889
|
+
const req = store.add({
|
|
890
|
+
timestamp: Date.now(),
|
|
891
|
+
data: { type: 'test' }
|
|
892
|
+
});
|
|
893
|
+
req.onerror = () => reject(req.error);
|
|
894
|
+
req.onsuccess = () => resolve(req.result);
|
|
895
|
+
});
|
|
896
|
+
|
|
897
|
+
log('✓', `Operation queue working. Added record: ${id}`);
|
|
898
|
+
}));
|
|
899
|
+
|
|
900
|
+
// Test: Storage Quota
|
|
901
|
+
tests.push(new TestRunner('test-17', 'Storage Quota', async () => {
|
|
902
|
+
if (!navigator.storage || !navigator.storage.estimate) {
|
|
903
|
+
throw new Error('Storage quota API not available');
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
const estimate = await navigator.storage.estimate();
|
|
907
|
+
const used = formatBytes(estimate.usage);
|
|
908
|
+
const quota = formatBytes(estimate.quota);
|
|
909
|
+
const percent = (estimate.usage / estimate.quota * 100).toFixed(1);
|
|
910
|
+
|
|
911
|
+
log('✓', `Using ${used} of ${quota} (${percent}%)`);
|
|
912
|
+
}));
|
|
913
|
+
|
|
914
|
+
// Test: Queue Operation
|
|
915
|
+
tests.push(new TestRunner('test-18', 'Queue Operation', async () => {
|
|
916
|
+
const result = await window.offlineManager.queueOperation({
|
|
917
|
+
type: 'test_operation',
|
|
918
|
+
data: { test: true }
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
log('✓', `Operation queued successfully`);
|
|
922
|
+
}));
|
|
923
|
+
|
|
924
|
+
// Test: Sync Operations
|
|
925
|
+
tests.push(new TestRunner('test-19', 'Sync Operations', async () => {
|
|
926
|
+
log('ℹ', 'Sync test requires actual offline workflow. Test manually by:');
|
|
927
|
+
log('ℹ', '1. Go offline (F12 > Network)');
|
|
928
|
+
log('ℹ', '2. Create/modify a project');
|
|
929
|
+
log('ℹ', '3. Go online');
|
|
930
|
+
log('ℹ', '4. Watch for "Syncing..." notification');
|
|
931
|
+
}));
|
|
932
|
+
|
|
933
|
+
// Test: Sync Order
|
|
934
|
+
tests.push(new TestRunner('test-20', 'Sync Order', async () => {
|
|
935
|
+
log('ℹ', 'Operations sync in creation order (FIFO)');
|
|
936
|
+
}));
|
|
937
|
+
|
|
938
|
+
// Test: Sync Progress
|
|
939
|
+
tests.push(new TestRunner('test-21', 'Sync Progress', async () => {
|
|
940
|
+
log('ℹ', 'Progress shown in bottom-left notification');
|
|
941
|
+
}));
|
|
942
|
+
|
|
943
|
+
// Test: Manifest Valid
|
|
944
|
+
tests.push(new TestRunner('test-22', 'Manifest Valid', async () => {
|
|
945
|
+
const response = await fetch('/app/manifest.json');
|
|
946
|
+
const manifest = await response.json();
|
|
947
|
+
|
|
948
|
+
if (!manifest || typeof manifest !== 'object') {
|
|
949
|
+
throw new Error('Invalid manifest');
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
log('✓', `Manifest loaded: ${manifest.name}`);
|
|
953
|
+
}));
|
|
954
|
+
|
|
955
|
+
// Test: Manifest Required Fields
|
|
956
|
+
tests.push(new TestRunner('test-23', 'Manifest Required Fields', async () => {
|
|
957
|
+
const response = await fetch('/app/manifest.json');
|
|
958
|
+
const manifest = await response.json();
|
|
959
|
+
|
|
960
|
+
const required = ['name', 'short_name', 'start_url', 'display', 'icons'];
|
|
961
|
+
const missing = required.filter(field => !manifest[field]);
|
|
962
|
+
|
|
963
|
+
if (missing.length > 0) {
|
|
964
|
+
throw new Error(`Missing fields: ${missing.join(', ')}`);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
log('✓', `All required fields present`);
|
|
968
|
+
}));
|
|
969
|
+
|
|
970
|
+
// Test: Start URL
|
|
971
|
+
tests.push(new TestRunner('test-24', 'Start URL', async () => {
|
|
972
|
+
const response = await fetch('/app/manifest.json');
|
|
973
|
+
const manifest = await response.json();
|
|
974
|
+
|
|
975
|
+
const startUrl = manifest.start_url;
|
|
976
|
+
const urlResponse = await fetch(startUrl);
|
|
977
|
+
|
|
978
|
+
if (!urlResponse.ok) {
|
|
979
|
+
throw new Error(`Start URL not accessible: ${startUrl}`);
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
log('✓', `Start URL accessible: ${startUrl}`);
|
|
983
|
+
}));
|
|
984
|
+
|
|
985
|
+
// Test: Icons Accessible
|
|
986
|
+
tests.push(new TestRunner('test-25', 'Icons Accessible', async () => {
|
|
987
|
+
const response = await fetch('/app/manifest.json');
|
|
988
|
+
const manifest = await response.json();
|
|
989
|
+
|
|
990
|
+
if (!manifest.icons || manifest.icons.length === 0) {
|
|
991
|
+
throw new Error('No icons in manifest');
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
log('✓', `${manifest.icons.length} icon sizes defined`);
|
|
995
|
+
}));
|
|
996
|
+
|
|
997
|
+
// Test: Install Prompt
|
|
998
|
+
tests.push(new TestRunner('test-26', 'Install Prompt', async () => {
|
|
999
|
+
log('ℹ', 'Install prompt shown automatically on first visit');
|
|
1000
|
+
log('ℹ', 'Also available in browser menu → Install');
|
|
1001
|
+
}));
|
|
1002
|
+
|
|
1003
|
+
// Test: Update Detection
|
|
1004
|
+
tests.push(new TestRunner('test-27', 'Update Detection', async () => {
|
|
1005
|
+
if (!('serviceWorker' in navigator)) {
|
|
1006
|
+
throw new Error('Service Worker not available');
|
|
1007
|
+
}
|
|
1008
|
+
|
|
1009
|
+
const reg = await navigator.serviceWorker.getRegistration('/app/');
|
|
1010
|
+
if (!reg) {
|
|
1011
|
+
throw new Error('Service Worker not registered');
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
// Manually check for updates
|
|
1015
|
+
await reg.update();
|
|
1016
|
+
log('✓', 'Update check performed (check console for results)');
|
|
1017
|
+
}));
|
|
1018
|
+
|
|
1019
|
+
// Test: HTTPS Requirement
|
|
1020
|
+
tests.push(new TestRunner('test-28', 'HTTPS Requirement', async () => {
|
|
1021
|
+
const protocol = window.location.protocol;
|
|
1022
|
+
|
|
1023
|
+
if (protocol !== 'https:' && !window.location.hostname.includes('localhost')) {
|
|
1024
|
+
throw new Error(`PWA requires HTTPS. Current: ${protocol}`);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
log('✓', `Using secure protocol: ${protocol}`);
|
|
1028
|
+
}));
|
|
1029
|
+
|
|
1030
|
+
// Helper: Update test status
|
|
1031
|
+
function updateStatus(testId, status, result = '') {
|
|
1032
|
+
const statusEl = document.getElementById(`${testId}-status`);
|
|
1033
|
+
const statusClass = `status-${status}`;
|
|
1034
|
+
|
|
1035
|
+
statusEl.textContent = status;
|
|
1036
|
+
statusEl.className = `test-status ${statusClass}`;
|
|
1037
|
+
|
|
1038
|
+
if (result) {
|
|
1039
|
+
log(getIcon(status), result);
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
updateStats();
|
|
1043
|
+
updateProgress();
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// Helper: Log result
|
|
1047
|
+
function log(icon, message) {
|
|
1048
|
+
const panel = document.getElementById('results-panel');
|
|
1049
|
+
|
|
1050
|
+
if (panel.textContent.includes('No tests run')) {
|
|
1051
|
+
panel.innerHTML = '<div class="results-header">Test Results</div>';
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const className = icon === '✓' ? 'pass' : icon === '✗' ? 'fail' : 'info';
|
|
1055
|
+
const item = document.createElement('div');
|
|
1056
|
+
item.className = `result-item ${className}`;
|
|
1057
|
+
item.innerHTML = `<span class="result-icon">${icon}</span> <span>${message}</span>`;
|
|
1058
|
+
|
|
1059
|
+
panel.appendChild(item);
|
|
1060
|
+
panel.scrollTop = panel.scrollHeight;
|
|
1061
|
+
}
|
|
1062
|
+
|
|
1063
|
+
// Helper: Get icon for status
|
|
1064
|
+
function getIcon(status) {
|
|
1065
|
+
return {
|
|
1066
|
+
'pass': '✓',
|
|
1067
|
+
'fail': '✗',
|
|
1068
|
+
'running': '⟳',
|
|
1069
|
+
'pending': '◯'
|
|
1070
|
+
}[status] || '?';
|
|
1071
|
+
}
|
|
1072
|
+
|
|
1073
|
+
// Helper: Format bytes
|
|
1074
|
+
function formatBytes(bytes) {
|
|
1075
|
+
if (bytes === 0) return '0 B';
|
|
1076
|
+
const k = 1024;
|
|
1077
|
+
const sizes = ['B', 'KB', 'MB', 'GB'];
|
|
1078
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
1079
|
+
return Math.round(bytes / Math.pow(k, i) * 10) / 10 + ' ' + sizes[i];
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
// Update stats
|
|
1083
|
+
function updateStats() {
|
|
1084
|
+
totalCount = tests.length;
|
|
1085
|
+
document.getElementById('total-count').textContent = totalCount;
|
|
1086
|
+
document.getElementById('pass-count').textContent = passCount;
|
|
1087
|
+
document.getElementById('fail-count').textContent = failCount;
|
|
1088
|
+
document.getElementById('run-count').textContent = totalCount - passCount - failCount;
|
|
1089
|
+
}
|
|
1090
|
+
|
|
1091
|
+
// Update progress
|
|
1092
|
+
function updateProgress() {
|
|
1093
|
+
const completed = passCount + failCount;
|
|
1094
|
+
const pct = (completed / totalCount) * 100;
|
|
1095
|
+
document.getElementById('progress-fill').style.width = pct + '%';
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1098
|
+
// Run single test
|
|
1099
|
+
async function runTest(testId) {
|
|
1100
|
+
const idx = parseInt(testId.replace('test-', '')) - 1;
|
|
1101
|
+
if (tests[idx]) {
|
|
1102
|
+
await tests[idx].run();
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
|
|
1106
|
+
// Run all tests
|
|
1107
|
+
async function runAllTests() {
|
|
1108
|
+
passCount = 0;
|
|
1109
|
+
failCount = 0;
|
|
1110
|
+
document.getElementById('results-panel').innerHTML = '<div class="results-header">Test Results</div>';
|
|
1111
|
+
|
|
1112
|
+
for (const test of tests) {
|
|
1113
|
+
await test.run();
|
|
1114
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
log('✓', `Test run complete: ${passCount} passed, ${failCount} failed`);
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
// Clear results
|
|
1121
|
+
function clearResults() {
|
|
1122
|
+
passCount = 0;
|
|
1123
|
+
failCount = 0;
|
|
1124
|
+
tests.forEach(test => test.status = 'pending');
|
|
1125
|
+
document.getElementById('results-panel').innerHTML = '<div class="results-header">Test Results</div><p style="color: #9ca3af; font-size: 13px;">No tests run yet. Click "Run All Tests" to begin.</p>';
|
|
1126
|
+
updateStats();
|
|
1127
|
+
updateProgress();
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
// Initialize
|
|
1131
|
+
updateStats();
|
|
1132
|
+
</script>
|
|
1133
|
+
</body>
|
|
1134
|
+
</html>
|