@holoscript/std 2.1.0 → 3.1.1

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 (85) hide show
  1. package/README.md +197 -0
  2. package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
  3. package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
  4. package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
  5. package/dist/__tests__/Sprint33.test.d.ts.map +1 -0
  6. package/dist/__tests__/Sprint56.test.d.ts.map +1 -0
  7. package/dist/__tests__/collections.test.d.ts.map +1 -0
  8. package/dist/__tests__/events.test.d.ts.map +1 -0
  9. package/dist/__tests__/materials.test.d.ts.map +1 -0
  10. package/dist/__tests__/math.test.d.ts.map +1 -0
  11. package/dist/__tests__/physics.test.d.ts.map +1 -0
  12. package/dist/__tests__/spatial.test.d.ts.map +1 -0
  13. package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
  14. package/dist/__tests__/string.test.d.ts.map +1 -0
  15. package/dist/__tests__/time.test.d.ts.map +1 -0
  16. package/dist/collections.d.ts.map +1 -1
  17. package/dist/events.d.ts.map +1 -0
  18. package/dist/index.d.ts.map +1 -1
  19. package/dist/index.js +42 -9
  20. package/dist/materials.d.ts.map +1 -0
  21. package/dist/math.d.ts.map +1 -1
  22. package/dist/physics.d.ts.map +1 -0
  23. package/dist/spatial.d.ts.map +1 -0
  24. package/dist/string.d.ts.map +1 -1
  25. package/dist/time.d.ts.map +1 -1
  26. package/dist/traits/ARTraits.d.ts.map +1 -0
  27. package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
  28. package/dist/traits/EconomicTraits.d.ts.map +1 -0
  29. package/dist/traits/IoTTraits.d.ts.map +1 -0
  30. package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
  31. package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
  32. package/dist/traits/VRRTraits.d.ts.map +1 -0
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +17 -6
  35. package/src/__tests__/EconomicPrimitives.test.ts +690 -0
  36. package/src/__tests__/EconomicTraits.test.ts +425 -0
  37. package/src/__tests__/SimulationLabPrimitives.test.ts +415 -0
  38. package/src/__tests__/Sprint33.test.ts +1301 -0
  39. package/src/__tests__/Sprint56.test.ts +1070 -0
  40. package/src/__tests__/collections.test.ts +182 -0
  41. package/src/__tests__/events.test.ts +135 -0
  42. package/src/__tests__/materials.test.ts +84 -0
  43. package/src/__tests__/math.test.ts +246 -0
  44. package/src/__tests__/physics.test.ts +83 -0
  45. package/src/__tests__/spatial.test.ts +333 -0
  46. package/src/__tests__/string-decoupled.test.ts +16 -0
  47. package/src/__tests__/string.test.ts +164 -0
  48. package/src/__tests__/time.test.ts +110 -0
  49. package/src/collections.ts +8 -2
  50. package/src/events.ts +88 -0
  51. package/src/index.ts +161 -11
  52. package/src/materials.ts +109 -0
  53. package/src/math.ts +30 -14
  54. package/src/physics.ts +141 -0
  55. package/src/spatial.ts +320 -0
  56. package/src/string.basic.test.ts +14 -0
  57. package/src/string.test.ts +335 -0
  58. package/src/string.ts +19 -2
  59. package/src/time.ts +9 -10
  60. package/src/traits/ARTraits.ts +103 -0
  61. package/src/traits/EconomicPrimitives.ts +755 -0
  62. package/src/traits/EconomicTraits.ts +552 -0
  63. package/src/traits/IoTTraits.ts +102 -0
  64. package/src/traits/SimulationLabPrimitives.ts +650 -0
  65. package/src/traits/SimulationLabTraits.ts +191 -0
  66. package/src/traits/VRRTraits.ts +326 -0
  67. package/src/types.ts +47 -12
  68. package/vitest.config.ts +10 -0
  69. package/dist/collections.d.ts +0 -177
  70. package/dist/collections.js +0 -720
  71. package/dist/collections.js.map +0 -1
  72. package/dist/index.d.ts +0 -89
  73. package/dist/index.js.map +0 -1
  74. package/dist/math.d.ts +0 -162
  75. package/dist/math.js +0 -539
  76. package/dist/math.js.map +0 -1
  77. package/dist/string.d.ts +0 -208
  78. package/dist/string.js +0 -443
  79. package/dist/string.js.map +0 -1
  80. package/dist/time.d.ts +0 -215
  81. package/dist/time.js +0 -530
  82. package/dist/time.js.map +0 -1
  83. package/dist/types.d.ts +0 -193
  84. package/dist/types.js +0 -198
  85. package/dist/types.js.map +0 -1
@@ -0,0 +1,1301 @@
1
+ /**
2
+ * Sprint 33 — @holoscript/std acceptance tests
3
+ * Covers: types, collections, math, string, time
4
+ */
5
+ import { describe, it, expect, vi, beforeEach } from 'vitest';
6
+
7
+ // ─────────────────────────────────────────────
8
+ // types.ts
9
+ // ─────────────────────────────────────────────
10
+ import {
11
+ vec2,
12
+ vec3,
13
+ vec4,
14
+ quat,
15
+ transform,
16
+ rgb,
17
+ rgba,
18
+ hsl,
19
+ aabb,
20
+ ray,
21
+ vec3ToArray,
22
+ arrayToVec3,
23
+ quatToArray,
24
+ arrayToQuat,
25
+ parseColor,
26
+ colorToHex,
27
+ } from '../types.js';
28
+
29
+ // ─────────────────────────────────────────────
30
+ // collections.ts
31
+ // ─────────────────────────────────────────────
32
+ import { List, HoloMap, HoloSet, SpatialGrid, PriorityQueue } from '../collections.js';
33
+
34
+ // ─────────────────────────────────────────────
35
+ // math.ts
36
+ // ─────────────────────────────────────────────
37
+ import {
38
+ PI,
39
+ TAU,
40
+ HALF_PI,
41
+ DEG_TO_RAD,
42
+ RAD_TO_DEG,
43
+ EPSILON,
44
+ clamp,
45
+ lerp,
46
+ inverseLerp,
47
+ remap,
48
+ smoothstep,
49
+ degToRad,
50
+ radToDeg,
51
+ mod,
52
+ fract,
53
+ sign,
54
+ step,
55
+ vec2Math,
56
+ vec3Math,
57
+ quatMath,
58
+ aabbMath,
59
+ noise,
60
+ random,
61
+ } from '../math.js';
62
+
63
+ // ─────────────────────────────────────────────
64
+ // string.ts
65
+ // ─────────────────────────────────────────────
66
+ import {
67
+ capitalize,
68
+ titleCase,
69
+ camelCase,
70
+ pascalCase,
71
+ snakeCase,
72
+ kebabCase,
73
+ constantCase,
74
+ padLeft,
75
+ padRight,
76
+ center,
77
+ truncate,
78
+ truncateMiddle,
79
+ isBlank,
80
+ isNotBlank,
81
+ containsIgnoreCase,
82
+ startsWithIgnoreCase,
83
+ endsWithIgnoreCase,
84
+ isValidIdentifier,
85
+ isNumeric,
86
+ isAlphanumeric,
87
+ isAlpha,
88
+ repeat,
89
+ reverse,
90
+ count,
91
+ removeWhitespace,
92
+ collapseWhitespace,
93
+ removePrefix,
94
+ removeSuffix,
95
+ wrap,
96
+ unwrap,
97
+ lines,
98
+ words,
99
+ chars,
100
+ format,
101
+ formatBytes,
102
+ formatDuration,
103
+ escapeHtml,
104
+ unescapeHtml,
105
+ escapeRegex,
106
+ slugify,
107
+ levenshtein,
108
+ similarity,
109
+ indent,
110
+ dedent,
111
+ wordWrap,
112
+ randomString,
113
+ uuid,
114
+ } from '../string.js';
115
+
116
+ // ─────────────────────────────────────────────
117
+ // time.ts
118
+ // ─────────────────────────────────────────────
119
+ import {
120
+ now,
121
+ sleep,
122
+ measure,
123
+ debounce,
124
+ throttle,
125
+ Stopwatch,
126
+ FrameTimer,
127
+ DateTime,
128
+ } from '../time.js';
129
+
130
+ // ═══════════════════════════════════════════════
131
+ // types.ts
132
+ // ═══════════════════════════════════════════════
133
+ describe('types — factory functions', () => {
134
+ it('vec2 defaults to (0,0)', () => {
135
+ const v = vec2();
136
+ expect(v.x).toBe(0);
137
+ expect(v.y).toBe(0);
138
+ });
139
+
140
+ it('vec2 with values', () => {
141
+ const v = vec2(3, 4);
142
+ expect(v.x).toBe(3);
143
+ expect(v.y).toBe(4);
144
+ });
145
+
146
+ it('vec3 defaults to (0,0,0)', () => {
147
+ const v = vec3();
148
+ expect(v.x).toBe(0);
149
+ expect(v.y).toBe(0);
150
+ expect(v.z).toBe(0);
151
+ });
152
+
153
+ it('vec3 with values', () => {
154
+ const v = vec3(1, 2, 3);
155
+ expect(v.x).toBe(1);
156
+ expect(v.y).toBe(2);
157
+ expect(v.z).toBe(3);
158
+ });
159
+
160
+ it('vec4 with values', () => {
161
+ const v = vec4(1, 2, 3, 4);
162
+ expect(v.x).toBe(1);
163
+ expect(v.w).toBe(4);
164
+ });
165
+
166
+ it('quat defaults to identity', () => {
167
+ const q = quat();
168
+ expect(q.w).toBe(1);
169
+ expect(q.x).toBe(0);
170
+ expect(q.y).toBe(0);
171
+ expect(q.z).toBe(0);
172
+ });
173
+
174
+ it('transform has position, rotation, scale', () => {
175
+ const t = transform();
176
+ expect(t.position).toBeDefined();
177
+ expect(t.rotation).toBeDefined();
178
+ expect(t.scale).toBeDefined();
179
+ });
180
+
181
+ it('rgb creates color', () => {
182
+ const c = rgb(255, 0, 0);
183
+ expect(c.r).toBe(255);
184
+ expect(c.g).toBe(0);
185
+ expect(c.b).toBe(0);
186
+ });
187
+
188
+ it('rgba creates color with alpha', () => {
189
+ const c = rgba(0, 255, 0, 0.5);
190
+ expect(c.a).toBe(0.5);
191
+ });
192
+
193
+ it('aabb creates bounding box', () => {
194
+ const box = aabb(vec3(0, 0, 0), vec3(1, 1, 1));
195
+ expect(box.min).toBeDefined();
196
+ expect(box.max).toBeDefined();
197
+ });
198
+
199
+ it('ray creates ray with origin and direction', () => {
200
+ const r = ray(vec3(0, 0, 0), vec3(0, 1, 0));
201
+ expect(r.origin).toBeDefined();
202
+ expect(r.direction).toBeDefined();
203
+ });
204
+ });
205
+
206
+ describe('types — conversion utilities', () => {
207
+ it('vec3ToArray / arrayToVec3 round-trip', () => {
208
+ const v = vec3(1, 2, 3);
209
+ const arr = vec3ToArray(v);
210
+ expect(arr).toEqual([1, 2, 3]);
211
+ const v2 = arrayToVec3(arr);
212
+ expect(v2).toEqual({ x: 1, y: 2, z: 3 });
213
+ });
214
+
215
+ it('quatToArray / arrayToQuat round-trip', () => {
216
+ const q = quat(0, 0, 0, 1);
217
+ const arr = quatToArray(q);
218
+ expect(arr.length).toBe(4);
219
+ const q2 = arrayToQuat(arr);
220
+ expect(q2.w).toBeCloseTo(1);
221
+ });
222
+
223
+ it('parseColor handles hex #RRGGBB', () => {
224
+ const c = parseColor('#ff0000');
225
+ expect(c).not.toBeNull();
226
+ });
227
+
228
+ it('parseColor handles named color', () => {
229
+ const c = parseColor('red');
230
+ expect(c).not.toBeNull();
231
+ });
232
+
233
+ it('colorToHex produces # string', () => {
234
+ const hex = colorToHex(rgb(255, 0, 0));
235
+ expect(hex).toMatch(/^#/);
236
+ });
237
+ });
238
+
239
+ // ═══════════════════════════════════════════════
240
+ // collections — List
241
+ // ═══════════════════════════════════════════════
242
+ describe('List', () => {
243
+ it('creates empty list', () => {
244
+ const l = new List<number>();
245
+ expect(l.length).toBe(0);
246
+ expect(l.isEmpty).toBe(true);
247
+ });
248
+
249
+ it('creates from array', () => {
250
+ const l = new List([1, 2, 3]);
251
+ expect(l.length).toBe(3);
252
+ });
253
+
254
+ it('List.of()', () => {
255
+ const l = List.of(10, 20, 30);
256
+ expect(l.length).toBe(3);
257
+ expect(l.get(0)).toBe(10);
258
+ });
259
+
260
+ it('List.from()', () => {
261
+ const l = List.from([5, 6, 7]);
262
+ expect(l.get(2)).toBe(7);
263
+ });
264
+
265
+ it('List.range()', () => {
266
+ const l = List.range(0, 5, 1);
267
+ expect(l.length).toBe(5);
268
+ expect(l.get(4)).toBe(4);
269
+ });
270
+
271
+ it('map transforms elements', () => {
272
+ const l = List.of(1, 2, 3).map((x) => x * 2);
273
+ expect(l.toArray()).toEqual([2, 4, 6]);
274
+ });
275
+
276
+ it('filter keeps matching elements', () => {
277
+ const l = List.of(1, 2, 3, 4).filter((x) => x % 2 === 0);
278
+ expect(l.toArray()).toEqual([2, 4]);
279
+ });
280
+
281
+ it('reduce accumulates', () => {
282
+ const sum = List.of(1, 2, 3, 4).reduce((acc, x) => acc + x, 0);
283
+ expect(sum).toBe(10);
284
+ });
285
+
286
+ it('find returns matching element', () => {
287
+ const v = List.of(1, 2, 3).find((x) => x > 1);
288
+ expect(v).toBe(2);
289
+ });
290
+
291
+ it('some / every', () => {
292
+ const l = List.of(2, 4, 6);
293
+ expect(l.some((x) => x > 5)).toBe(true);
294
+ expect(l.every((x) => x % 2 === 0)).toBe(true);
295
+ });
296
+
297
+ it('concat combines lists', () => {
298
+ const a = List.of(1, 2);
299
+ const b = List.of(3, 4);
300
+ expect(a.concat(b).length).toBe(4);
301
+ });
302
+
303
+ it('append / prepend', () => {
304
+ const l = List.of(2, 3).prepend(1).append(4);
305
+ expect(l.toArray()).toEqual([1, 2, 3, 4]);
306
+ });
307
+
308
+ it('reverse', () => {
309
+ expect(List.of(1, 2, 3).reverse().toArray()).toEqual([3, 2, 1]);
310
+ });
311
+
312
+ it('sort', () => {
313
+ expect(
314
+ List.of(3, 1, 2)
315
+ .sort((a, b) => a - b)
316
+ .toArray()
317
+ ).toEqual([1, 2, 3]);
318
+ });
319
+
320
+ it('unique removes duplicates', () => {
321
+ expect(List.of(1, 2, 2, 3).unique().toArray()).toEqual([1, 2, 3]);
322
+ });
323
+
324
+ it('groupBy groups by key', () => {
325
+ const map = List.of('a', 'bb', 'c', 'dd').groupBy((s) => s.length);
326
+ expect(map.has(1)).toBe(true);
327
+ expect(map.has(2)).toBe(true);
328
+ });
329
+
330
+ it('partition splits into two', () => {
331
+ const [evens, odds] = List.of(1, 2, 3, 4).partition((x) => x % 2 === 0);
332
+ expect(evens.toArray()).toEqual([2, 4]);
333
+ expect(odds.toArray()).toEqual([1, 3]);
334
+ });
335
+
336
+ it('zip combines two lists', () => {
337
+ const z = List.of(1, 2, 3).zip(List.of('a', 'b', 'c'));
338
+ expect(z.get(0)).toEqual([1, 'a']);
339
+ });
340
+
341
+ it('flatten flattens one level', () => {
342
+ const l = List.of(List.of(1, 2), List.of(3, 4)).flatten();
343
+ expect(l.toArray()).toEqual([1, 2, 3, 4]);
344
+ });
345
+
346
+ it('sum / min / max / average', () => {
347
+ const nums = List.of(1, 2, 3, 4);
348
+ expect(nums.sum()).toBe(10);
349
+ expect(nums.min()).toBe(1);
350
+ expect(nums.max()).toBe(4);
351
+ expect(nums.average()).toBe(2.5);
352
+ });
353
+
354
+ it('take / drop', () => {
355
+ const l = List.of(1, 2, 3, 4, 5);
356
+ expect(l.take(3).toArray()).toEqual([1, 2, 3]);
357
+ expect(l.drop(3).toArray()).toEqual([4, 5]);
358
+ });
359
+
360
+ it('toSet converts to HoloSet', () => {
361
+ const s = List.of(1, 2, 3).toSet();
362
+ expect(s.has(2)).toBe(true);
363
+ });
364
+ });
365
+
366
+ // ═══════════════════════════════════════════════
367
+ // collections — HoloMap
368
+ // ═══════════════════════════════════════════════
369
+ describe('HoloMap', () => {
370
+ it('creates empty map', () => {
371
+ const m = new HoloMap<string, number>();
372
+ expect(m.size).toBe(0);
373
+ });
374
+
375
+ it('HoloMap.of()', () => {
376
+ const m = HoloMap.of(['a', 1], ['b', 2]);
377
+ expect(m.size).toBe(2);
378
+ expect(m.get('a')).toBe(1);
379
+ });
380
+
381
+ it('HoloMap.from()', () => {
382
+ const m = HoloMap.from([
383
+ ['x', 10],
384
+ ['y', 20],
385
+ ]);
386
+ expect(m.get('y')).toBe(20);
387
+ });
388
+
389
+ it('HoloMap.fromObject()', () => {
390
+ const m = HoloMap.fromObject({ a: 1, b: 2 });
391
+ expect(m.get('a')).toBe(1);
392
+ });
393
+
394
+ it('set returns new map with entry', () => {
395
+ const m1 = HoloMap.of<string, number>(['a', 1]);
396
+ const m2 = m1.set('b', 2);
397
+ expect(m2.get('b')).toBe(2);
398
+ expect(m1.has('b')).toBe(false); // immutable
399
+ });
400
+
401
+ it('delete removes entry', () => {
402
+ const m = HoloMap.of(['a', 1], ['b', 2]).delete('a');
403
+ expect(m.has('a')).toBe(false);
404
+ expect(m.size).toBe(1);
405
+ });
406
+
407
+ it('merge combines maps', () => {
408
+ const a = HoloMap.of(['a', 1]);
409
+ const b = HoloMap.of(['b', 2]);
410
+ const c = a.merge(b);
411
+ expect(c.size).toBe(2);
412
+ });
413
+
414
+ it('keys / values / entries', () => {
415
+ const m = HoloMap.of(['a', 1], ['b', 2]);
416
+ expect(m.keys().toArray()).toContain('a');
417
+ expect(m.values().toArray()).toContain(1);
418
+ expect(m.entries().length).toBe(2);
419
+ });
420
+
421
+ it('map transforms values', () => {
422
+ const m = HoloMap.of(['a', 1], ['b', 2]).map((v) => v * 10);
423
+ expect(m.get('a')).toBe(10);
424
+ });
425
+
426
+ it('filter keeps matching entries', () => {
427
+ const m = HoloMap.of(['a', 1], ['b', 20]).filter((v) => v > 5);
428
+ expect(m.has('b')).toBe(true);
429
+ expect(m.has('a')).toBe(false);
430
+ });
431
+
432
+ it('toObject converts to plain object', () => {
433
+ const obj = HoloMap.of(['x', 1]).toObject();
434
+ expect(obj['x']).toBe(1);
435
+ });
436
+ });
437
+
438
+ // ═══════════════════════════════════════════════
439
+ // collections — HoloSet
440
+ // ═══════════════════════════════════════════════
441
+ describe('HoloSet', () => {
442
+ it('creates empty set', () => {
443
+ const s = new HoloSet<number>();
444
+ expect(s.size).toBe(0);
445
+ });
446
+
447
+ it('HoloSet.of()', () => {
448
+ const s = HoloSet.of(1, 2, 3);
449
+ expect(s.size).toBe(3);
450
+ expect(s.has(2)).toBe(true);
451
+ });
452
+
453
+ it('HoloSet.from()', () => {
454
+ const s = HoloSet.from([4, 5, 6]);
455
+ expect(s.has(5)).toBe(true);
456
+ });
457
+
458
+ it('add returns new set', () => {
459
+ const s1 = HoloSet.of(1, 2);
460
+ const s2 = s1.add(3);
461
+ expect(s2.has(3)).toBe(true);
462
+ expect(s1.has(3)).toBe(false); // immutable
463
+ });
464
+
465
+ it('delete removes element', () => {
466
+ const s = HoloSet.of(1, 2, 3).delete(2);
467
+ expect(s.has(2)).toBe(false);
468
+ });
469
+
470
+ it('union', () => {
471
+ const a = HoloSet.of(1, 2);
472
+ const b = HoloSet.of(2, 3);
473
+ expect(a.union(b).size).toBe(3);
474
+ });
475
+
476
+ it('intersection', () => {
477
+ const a = HoloSet.of(1, 2, 3);
478
+ const b = HoloSet.of(2, 3, 4);
479
+ const i = a.intersection(b);
480
+ expect(i.toArray().sort()).toEqual([2, 3]);
481
+ });
482
+
483
+ it('difference', () => {
484
+ const a = HoloSet.of(1, 2, 3);
485
+ const b = HoloSet.of(2, 3);
486
+ expect(a.difference(b).toArray()).toEqual([1]);
487
+ });
488
+
489
+ it('symmetricDifference', () => {
490
+ const a = HoloSet.of(1, 2, 3);
491
+ const b = HoloSet.of(2, 3, 4);
492
+ const sd = a.symmetricDifference(b);
493
+ expect(sd.has(1)).toBe(true);
494
+ expect(sd.has(4)).toBe(true);
495
+ expect(sd.has(2)).toBe(false);
496
+ });
497
+
498
+ it('isSubsetOf / isSupersetOf', () => {
499
+ const small = HoloSet.of(1, 2);
500
+ const big = HoloSet.of(1, 2, 3);
501
+ expect(small.isSubsetOf(big)).toBe(true);
502
+ expect(big.isSupersetOf(small)).toBe(true);
503
+ });
504
+
505
+ it('isDisjointFrom', () => {
506
+ const a = HoloSet.of(1, 2);
507
+ const b = HoloSet.of(3, 4);
508
+ expect(a.isDisjointFrom(b)).toBe(true);
509
+ });
510
+
511
+ it('toArray / toList / toNativeSet', () => {
512
+ const s = HoloSet.of(1, 2, 3);
513
+ expect(s.toArray().length).toBe(3);
514
+ expect(s.toList().length).toBe(3);
515
+ expect(s.toNativeSet().size).toBe(3);
516
+ });
517
+ });
518
+
519
+ // ═══════════════════════════════════════════════
520
+ // collections — PriorityQueue
521
+ // ═══════════════════════════════════════════════
522
+ describe('PriorityQueue', () => {
523
+ it('minHeap dequeues lowest priority first', () => {
524
+ const pq = PriorityQueue.minHeap<string>();
525
+ pq.enqueue('low', 10);
526
+ pq.enqueue('high', 1);
527
+ pq.enqueue('mid', 5);
528
+ expect(pq.dequeue()).toBe('high');
529
+ expect(pq.dequeue()).toBe('mid');
530
+ expect(pq.dequeue()).toBe('low');
531
+ });
532
+
533
+ it('maxHeap dequeues highest priority first', () => {
534
+ const pq = PriorityQueue.maxHeap<string>();
535
+ pq.enqueue('a', 1);
536
+ pq.enqueue('b', 10);
537
+ pq.enqueue('c', 5);
538
+ expect(pq.dequeue()).toBe('b');
539
+ });
540
+
541
+ it('peek returns top without removing', () => {
542
+ const pq = PriorityQueue.minHeap<number>();
543
+ pq.enqueue(42, 1);
544
+ expect(pq.peek()).toBe(42);
545
+ expect(pq.size).toBe(1);
546
+ });
547
+
548
+ it('isEmpty / size', () => {
549
+ const pq = PriorityQueue.minHeap<number>();
550
+ expect(pq.isEmpty).toBe(true);
551
+ pq.enqueue(1, 1);
552
+ expect(pq.size).toBe(1);
553
+ expect(pq.isEmpty).toBe(false);
554
+ });
555
+
556
+ it('clear empties queue', () => {
557
+ const pq = PriorityQueue.minHeap<number>();
558
+ pq.enqueue(1, 1);
559
+ pq.clear();
560
+ expect(pq.isEmpty).toBe(true);
561
+ });
562
+ });
563
+
564
+ // ═══════════════════════════════════════════════
565
+ // collections — SpatialGrid
566
+ // ═══════════════════════════════════════════════
567
+ describe('SpatialGrid', () => {
568
+ it('insert and query by radius', () => {
569
+ const grid = new SpatialGrid<string>(1.0);
570
+ grid.insert({ x: 0, y: 0, z: 0 }, 'origin');
571
+ grid.insert({ x: 10, y: 0, z: 0 }, 'far');
572
+ const results = grid.query({ x: 0, y: 0, z: 0 }, 1.5);
573
+ expect(results).toContain('origin');
574
+ expect(results).not.toContain('far');
575
+ });
576
+
577
+ it('remove deletes item', () => {
578
+ const grid = new SpatialGrid<string>(1.0);
579
+ grid.insert({ x: 0, y: 0, z: 0 }, 'item');
580
+ grid.remove({ x: 0, y: 0, z: 0 }, 'item');
581
+ const results = grid.query({ x: 0, y: 0, z: 0 }, 1.5);
582
+ expect(results).not.toContain('item');
583
+ });
584
+
585
+ it('queryBox returns items in AABB', () => {
586
+ const grid = new SpatialGrid<string>(1.0);
587
+ grid.insert({ x: 0.5, y: 0.5, z: 0.5 }, 'inside');
588
+ grid.insert({ x: 5, y: 5, z: 5 }, 'outside');
589
+ const results = grid.queryBox({ x: 0, y: 0, z: 0 }, { x: 1, y: 1, z: 1 });
590
+ expect(results).toContain('inside');
591
+ expect(results).not.toContain('outside');
592
+ });
593
+ });
594
+
595
+ // ═══════════════════════════════════════════════
596
+ // math — constants
597
+ // ═══════════════════════════════════════════════
598
+ describe('math constants', () => {
599
+ it('PI is ~3.14159', () => expect(PI).toBeCloseTo(3.14159, 4));
600
+ it('TAU is 2*PI', () => expect(TAU).toBeCloseTo(PI * 2, 10));
601
+ it('HALF_PI is PI/2', () => expect(HALF_PI).toBeCloseTo(PI / 2, 10));
602
+ it('DEG_TO_RAD * 180 = PI', () => expect(DEG_TO_RAD * 180).toBeCloseTo(PI, 10));
603
+ it('RAD_TO_DEG * PI = 180', () => expect(RAD_TO_DEG * PI).toBeCloseTo(180, 10));
604
+ it('EPSILON is small', () => expect(EPSILON).toBeGreaterThan(0));
605
+ });
606
+
607
+ // ═══════════════════════════════════════════════
608
+ // math — basic functions
609
+ // ═══════════════════════════════════════════════
610
+ describe('math — basic functions', () => {
611
+ it('clamp', () => {
612
+ expect(clamp(5, 0, 10)).toBe(5);
613
+ expect(clamp(-5, 0, 10)).toBe(0);
614
+ expect(clamp(15, 0, 10)).toBe(10);
615
+ });
616
+
617
+ it('lerp', () => {
618
+ expect(lerp(0, 10, 0.5)).toBe(5);
619
+ expect(lerp(0, 10, 0)).toBe(0);
620
+ expect(lerp(0, 10, 1)).toBe(10);
621
+ });
622
+
623
+ it('inverseLerp', () => {
624
+ expect(inverseLerp(0, 10, 5)).toBeCloseTo(0.5);
625
+ expect(inverseLerp(0, 10, 0)).toBe(0);
626
+ expect(inverseLerp(0, 10, 10)).toBe(1);
627
+ });
628
+
629
+ it('remap', () => {
630
+ const result = remap(5, 0, 10, 0, 100);
631
+ expect(result).toBeCloseTo(50);
632
+ });
633
+
634
+ it('smoothstep', () => {
635
+ expect(smoothstep(0, 1, 0)).toBeCloseTo(0);
636
+ expect(smoothstep(0, 1, 1)).toBeCloseTo(1);
637
+ expect(smoothstep(0, 1, 0.5)).toBeCloseTo(0.5);
638
+ });
639
+
640
+ it('degToRad / radToDeg', () => {
641
+ expect(degToRad(180)).toBeCloseTo(PI);
642
+ expect(radToDeg(PI)).toBeCloseTo(180);
643
+ });
644
+
645
+ it('mod handles negative values correctly', () => {
646
+ expect(mod(7, 3)).toBe(1);
647
+ expect(mod(-1, 3)).toBeGreaterThanOrEqual(0);
648
+ });
649
+
650
+ it('fract returns fractional part', () => {
651
+ expect(fract(3.7)).toBeCloseTo(0.7);
652
+ expect(fract(3.0)).toBeCloseTo(0.0);
653
+ });
654
+
655
+ it('sign', () => {
656
+ expect(sign(5)).toBe(1);
657
+ expect(sign(-5)).toBe(-1);
658
+ expect(sign(0)).toBe(0);
659
+ });
660
+
661
+ it('step', () => {
662
+ expect(step(0.5, 0.3)).toBe(0);
663
+ expect(step(0.5, 0.7)).toBe(1);
664
+ });
665
+ });
666
+
667
+ // ═══════════════════════════════════════════════
668
+ // math — vec2Math
669
+ // ═══════════════════════════════════════════════
670
+ describe('vec2Math', () => {
671
+ it('add', () => {
672
+ const r = vec2Math.add({ x: 1, y: 2 }, { x: 3, y: 4 });
673
+ expect(r).toEqual({ x: 4, y: 6 });
674
+ });
675
+
676
+ it('sub', () => {
677
+ const r = vec2Math.sub({ x: 5, y: 5 }, { x: 2, y: 3 });
678
+ expect(r).toEqual({ x: 3, y: 2 });
679
+ });
680
+
681
+ it('mul (scalar)', () => {
682
+ const r = vec2Math.mul({ x: 2, y: 3 }, 2);
683
+ expect(r).toEqual({ x: 4, y: 6 });
684
+ });
685
+
686
+ it('dot', () => {
687
+ expect(vec2Math.dot({ x: 1, y: 0 }, { x: 0, y: 1 })).toBe(0);
688
+ expect(vec2Math.dot({ x: 1, y: 0 }, { x: 1, y: 0 })).toBe(1);
689
+ });
690
+
691
+ it('length', () => {
692
+ expect(vec2Math.length({ x: 3, y: 4 })).toBeCloseTo(5);
693
+ });
694
+
695
+ it('normalize produces unit vector', () => {
696
+ const n = vec2Math.normalize({ x: 3, y: 4 });
697
+ expect(vec2Math.length(n)).toBeCloseTo(1);
698
+ });
699
+
700
+ it('distance', () => {
701
+ expect(vec2Math.distance({ x: 0, y: 0 }, { x: 3, y: 4 })).toBeCloseTo(5);
702
+ });
703
+
704
+ it('lerp', () => {
705
+ const r = vec2Math.lerp({ x: 0, y: 0 }, { x: 10, y: 10 }, 0.5);
706
+ expect(r).toEqual({ x: 5, y: 5 });
707
+ });
708
+
709
+ it('angle', () => {
710
+ const a = vec2Math.angle({ x: 1, y: 0 });
711
+ expect(a).toBeCloseTo(0);
712
+ });
713
+ });
714
+
715
+ // ═══════════════════════════════════════════════
716
+ // math — vec3Math
717
+ // ═══════════════════════════════════════════════
718
+ describe('vec3Math', () => {
719
+ it('zero / one factory functions', () => {
720
+ expect(vec3Math.zero()).toEqual({ x: 0, y: 0, z: 0 });
721
+ expect(vec3Math.one()).toEqual({ x: 1, y: 1, z: 1 });
722
+ });
723
+
724
+ it('up / down / forward / back factory functions', () => {
725
+ expect(vec3Math.up().y).toBe(1);
726
+ expect(vec3Math.down().y).toBe(-1);
727
+ expect(vec3Math.forward().z).not.toBeUndefined();
728
+ expect(vec3Math.back().z).not.toBeUndefined();
729
+ });
730
+
731
+ it('add', () => {
732
+ const r = vec3Math.add({ x: 1, y: 2, z: 3 }, { x: 4, y: 5, z: 6 });
733
+ expect(r).toEqual({ x: 5, y: 7, z: 9 });
734
+ });
735
+
736
+ it('sub', () => {
737
+ const r = vec3Math.sub({ x: 5, y: 5, z: 5 }, { x: 2, y: 3, z: 1 });
738
+ expect(r).toEqual({ x: 3, y: 2, z: 4 });
739
+ });
740
+
741
+ it('dot', () => {
742
+ expect(vec3Math.dot({ x: 1, y: 0, z: 0 }, { x: 0, y: 1, z: 0 })).toBe(0);
743
+ });
744
+
745
+ it('cross', () => {
746
+ const c = vec3Math.cross({ x: 1, y: 0, z: 0 }, { x: 0, y: 1, z: 0 });
747
+ expect(c).toEqual({ x: 0, y: 0, z: 1 });
748
+ });
749
+
750
+ it('length', () => {
751
+ expect(vec3Math.length({ x: 1, y: 2, z: 2 })).toBeCloseTo(3);
752
+ });
753
+
754
+ it('normalize', () => {
755
+ const n = vec3Math.normalize({ x: 0, y: 0, z: 5 });
756
+ expect(vec3Math.length(n)).toBeCloseTo(1);
757
+ });
758
+
759
+ it('distance', () => {
760
+ expect(vec3Math.distance({ x: 0, y: 0, z: 0 }, { x: 1, y: 2, z: 2 })).toBeCloseTo(3);
761
+ });
762
+
763
+ it('lerp', () => {
764
+ const r = vec3Math.lerp({ x: 0, y: 0, z: 0 }, { x: 2, y: 4, z: 6 }, 0.5);
765
+ expect(r).toEqual({ x: 1, y: 2, z: 3 });
766
+ });
767
+
768
+ it('negate', () => {
769
+ const n = vec3Math.negate({ x: 1, y: -2, z: 3 });
770
+ expect(n).toEqual({ x: -1, y: 2, z: -3 });
771
+ });
772
+
773
+ it('equals', () => {
774
+ expect(vec3Math.equals({ x: 1, y: 2, z: 3 }, { x: 1, y: 2, z: 3 })).toBe(true);
775
+ expect(vec3Math.equals({ x: 1, y: 2, z: 3 }, { x: 1, y: 2, z: 4 })).toBe(false);
776
+ });
777
+ });
778
+
779
+ // ═══════════════════════════════════════════════
780
+ // math — quatMath
781
+ // ═══════════════════════════════════════════════
782
+ describe('quatMath', () => {
783
+ it('identity quaternion', () => {
784
+ const q = quatMath.identity();
785
+ expect(q.w).toBeCloseTo(1);
786
+ expect(q.x).toBeCloseTo(0);
787
+ expect(q.y).toBeCloseTo(0);
788
+ expect(q.z).toBeCloseTo(0);
789
+ });
790
+
791
+ it('fromAxisAngle', () => {
792
+ const q = quatMath.fromAxisAngle({ x: 0, y: 1, z: 0 }, 0);
793
+ expect(q.w).toBeCloseTo(1);
794
+ });
795
+
796
+ it('multiply identity stays identity', () => {
797
+ const id = quatMath.identity();
798
+ const q = quatMath.multiply(id, id);
799
+ expect(q.w).toBeCloseTo(1);
800
+ });
801
+
802
+ it('normalize', () => {
803
+ const q = { x: 0, y: 0, z: 0, w: 2 };
804
+ const n = quatMath.normalize(q);
805
+ expect(n.w).toBeCloseTo(1);
806
+ });
807
+
808
+ it('conjugate', () => {
809
+ const q = { x: 1, y: 0, z: 0, w: 0 };
810
+ const c = quatMath.conjugate(q);
811
+ expect(c.x).toBe(-1);
812
+ });
813
+
814
+ it('toEuler / fromEuler round-trip', () => {
815
+ const euler = { x: 0, y: 90, z: 0 };
816
+ const q = quatMath.fromEuler(euler);
817
+ const e2 = quatMath.toEuler(q);
818
+ expect(e2.y).toBeCloseTo(90, 0);
819
+ });
820
+
821
+ it('rotateVec3', () => {
822
+ const id = quatMath.identity();
823
+ const v = { x: 1, y: 0, z: 0 };
824
+ const r = quatMath.rotateVec3(id, v);
825
+ expect(r.x).toBeCloseTo(1);
826
+ expect(r.y).toBeCloseTo(0);
827
+ });
828
+ });
829
+
830
+ // ═══════════════════════════════════════════════
831
+ // math — aabbMath
832
+ // ═══════════════════════════════════════════════
833
+ describe('aabbMath', () => {
834
+ const box = { min: { x: 0, y: 0, z: 0 }, max: { x: 2, y: 2, z: 2 } };
835
+
836
+ it('center', () => {
837
+ const c = aabbMath.center(box);
838
+ expect(c).toEqual({ x: 1, y: 1, z: 1 });
839
+ });
840
+
841
+ it('size', () => {
842
+ const s = aabbMath.size(box);
843
+ expect(s).toEqual({ x: 2, y: 2, z: 2 });
844
+ });
845
+
846
+ it('contains point inside', () => {
847
+ expect(aabbMath.contains(box, { x: 1, y: 1, z: 1 })).toBe(true);
848
+ });
849
+
850
+ it('contains point outside', () => {
851
+ expect(aabbMath.contains(box, { x: 5, y: 5, z: 5 })).toBe(false);
852
+ });
853
+
854
+ it('intersects overlapping boxes', () => {
855
+ const b2 = { min: { x: 1, y: 1, z: 1 }, max: { x: 3, y: 3, z: 3 } };
856
+ expect(aabbMath.intersects(box, b2)).toBe(true);
857
+ });
858
+
859
+ it('intersects non-overlapping boxes', () => {
860
+ const b3 = { min: { x: 5, y: 5, z: 5 }, max: { x: 8, y: 8, z: 8 } };
861
+ expect(aabbMath.intersects(box, b3)).toBe(false);
862
+ });
863
+
864
+ it('expand to include point', () => {
865
+ const expanded = aabbMath.expand(box, { x: 5, y: 5, z: 5 });
866
+ expect(expanded.max.x).toBe(5);
867
+ expect(expanded.min.x).toBe(0);
868
+ });
869
+
870
+ it('merge', () => {
871
+ const b2 = { min: { x: 1, y: 1, z: 1 }, max: { x: 5, y: 5, z: 5 } };
872
+ const merged = aabbMath.merge(box, b2);
873
+ expect(merged.max.x).toBe(5);
874
+ expect(merged.min.x).toBe(0);
875
+ });
876
+ });
877
+
878
+ // ═══════════════════════════════════════════════
879
+ // math — noise
880
+ // ═══════════════════════════════════════════════
881
+ describe('noise', () => {
882
+ it('perlin2d returns value in range', () => {
883
+ const v = noise.perlin2d(0.5, 0.5);
884
+ expect(typeof v).toBe('number');
885
+ expect(v).toBeGreaterThanOrEqual(-1);
886
+ expect(v).toBeLessThanOrEqual(1);
887
+ });
888
+
889
+ it('perlin3d returns value in range', () => {
890
+ const v = noise.perlin3d(0.5, 0.5, 0.5);
891
+ expect(v).toBeGreaterThanOrEqual(-1);
892
+ expect(v).toBeLessThanOrEqual(1);
893
+ });
894
+
895
+ it('fbm returns numeric value', () => {
896
+ const v = noise.fbm(0.5, 0.5, 4);
897
+ expect(typeof v).toBe('number');
898
+ });
899
+
900
+ it('simplex returns value', () => {
901
+ const v = noise.simplex(0.3, 0.7);
902
+ expect(typeof v).toBe('number');
903
+ });
904
+
905
+ it('worley returns value in range', () => {
906
+ const v = noise.worley(1, 2, 3);
907
+ expect(v).toBeGreaterThanOrEqual(0);
908
+ });
909
+
910
+ it('same inputs produce same output (deterministic)', () => {
911
+ expect(noise.perlin2d(1.23, 4.56)).toBe(noise.perlin2d(1.23, 4.56));
912
+ });
913
+ });
914
+
915
+ // ═══════════════════════════════════════════════
916
+ // math — random
917
+ // ═══════════════════════════════════════════════
918
+ describe('random', () => {
919
+ it('float returns [0,1)', () => {
920
+ for (let i = 0; i < 20; i++) {
921
+ const v = random.float();
922
+ expect(v).toBeGreaterThanOrEqual(0);
923
+ expect(v).toBeLessThan(1);
924
+ }
925
+ });
926
+
927
+ it('range returns value in [min,max)', () => {
928
+ for (let i = 0; i < 20; i++) {
929
+ const v = random.range(5, 10);
930
+ expect(v).toBeGreaterThanOrEqual(5);
931
+ expect(v).toBeLessThan(10);
932
+ }
933
+ });
934
+
935
+ it('int returns integer in [min,max]', () => {
936
+ for (let i = 0; i < 20; i++) {
937
+ const v = random.int(1, 6);
938
+ expect(Number.isInteger(v)).toBe(true);
939
+ expect(v).toBeGreaterThanOrEqual(1);
940
+ expect(v).toBeLessThanOrEqual(6);
941
+ }
942
+ });
943
+
944
+ it('bool returns boolean', () => {
945
+ expect(typeof random.bool()).toBe('boolean');
946
+ });
947
+
948
+ it('pick returns element from array', () => {
949
+ const arr = [10, 20, 30];
950
+ const v = random.pick(arr);
951
+ expect(arr).toContain(v);
952
+ });
953
+
954
+ it('shuffle returns array of same length', () => {
955
+ const arr = [1, 2, 3, 4, 5];
956
+ const s = random.shuffle([...arr]);
957
+ expect(s.length).toBe(arr.length);
958
+ });
959
+
960
+ it('seeded RNG is deterministic', () => {
961
+ const rng1 = random.seeded(42);
962
+ const rng2 = random.seeded(42);
963
+ expect(rng1()).toBe(rng2());
964
+ });
965
+
966
+ it('insideUnitSphere returns point within sphere', () => {
967
+ const p = random.insideUnitSphere();
968
+ const len = Math.sqrt(p.x * p.x + p.y * p.y + p.z * p.z);
969
+ expect(len).toBeLessThanOrEqual(1 + EPSILON);
970
+ });
971
+
972
+ it('insideUnitCircle returns 2D point within circle', () => {
973
+ const p = random.insideUnitCircle();
974
+ const len = Math.sqrt(p.x * p.x + p.y * p.y);
975
+ expect(len).toBeLessThanOrEqual(1 + EPSILON);
976
+ });
977
+ });
978
+
979
+ // ═══════════════════════════════════════════════
980
+ // string utilities
981
+ // ═══════════════════════════════════════════════
982
+ describe('string — case conversion', () => {
983
+ it('capitalize', () => expect(capitalize('hello world')).toBe('Hello world'));
984
+ it('titleCase', () => expect(titleCase('hello world')).toBe('Hello World'));
985
+ it('camelCase', () => expect(camelCase('hello world')).toBe('helloWorld'));
986
+ it('pascalCase', () => expect(pascalCase('hello world')).toBe('HelloWorld'));
987
+ it('snakeCase', () => expect(snakeCase('helloWorld')).toBe('hello_world'));
988
+ it('kebabCase', () => expect(kebabCase('helloWorld')).toBe('hello-world'));
989
+ it('constantCase', () => expect(constantCase('helloWorld')).toBe('HELLO_WORLD'));
990
+ });
991
+
992
+ describe('string — padding and truncation', () => {
993
+ it('padLeft', () => expect(padLeft('hi', 5)).toBe(' hi'));
994
+ it('padRight', () => expect(padRight('hi', 5)).toBe('hi '));
995
+ it('center', () => {
996
+ const s = center('hi', 6);
997
+ expect(s.length).toBe(6);
998
+ expect(s).toContain('hi');
999
+ });
1000
+ it('truncate', () => {
1001
+ const t = truncate('hello world', 8);
1002
+ expect(t.length).toBeLessThanOrEqual(8);
1003
+ });
1004
+ it('truncateMiddle', () => {
1005
+ const t = truncateMiddle('hello world', 7);
1006
+ expect(t.length).toBeLessThanOrEqual(7);
1007
+ });
1008
+ });
1009
+
1010
+ describe('string — checks', () => {
1011
+ it('isBlank / isNotBlank', () => {
1012
+ expect(isBlank('')).toBe(true);
1013
+ expect(isBlank(' ')).toBe(true);
1014
+ expect(isNotBlank('a')).toBe(true);
1015
+ });
1016
+
1017
+ it('containsIgnoreCase', () => {
1018
+ expect(containsIgnoreCase('Hello World', 'world')).toBe(true);
1019
+ expect(containsIgnoreCase('Hello World', 'xyz')).toBe(false);
1020
+ });
1021
+
1022
+ it('startsWithIgnoreCase', () => {
1023
+ expect(startsWithIgnoreCase('Hello', 'hello')).toBe(true);
1024
+ });
1025
+
1026
+ it('endsWithIgnoreCase', () => {
1027
+ expect(endsWithIgnoreCase('World', 'WORLD')).toBe(true);
1028
+ });
1029
+
1030
+ it('isValidIdentifier', () => {
1031
+ expect(isValidIdentifier('myVar')).toBe(true);
1032
+ expect(isValidIdentifier('123bad')).toBe(false);
1033
+ });
1034
+
1035
+ it('isNumeric', () => {
1036
+ expect(isNumeric('123')).toBe(true);
1037
+ expect(isNumeric('12.5')).toBe(true);
1038
+ expect(isNumeric('abc')).toBe(false);
1039
+ });
1040
+
1041
+ it('isAlpha', () => {
1042
+ expect(isAlpha('abc')).toBe(true);
1043
+ expect(isAlpha('abc123')).toBe(false);
1044
+ });
1045
+
1046
+ it('isAlphanumeric', () => {
1047
+ expect(isAlphanumeric('abc123')).toBe(true);
1048
+ expect(isAlphanumeric('abc!')).toBe(false);
1049
+ });
1050
+ });
1051
+
1052
+ describe('string — manipulation', () => {
1053
+ it('repeat', () => expect(repeat('ab', 3)).toBe('ababab'));
1054
+ it('reverse', () => expect(reverse('hello')).toBe('olleh'));
1055
+ it('count occurrences', () => expect(count('hello world hello', 'hello')).toBe(2));
1056
+ it('removeWhitespace', () => expect(removeWhitespace('h e l l o')).toBe('hello'));
1057
+ it('collapseWhitespace', () => expect(collapseWhitespace('a b c')).toBe('a b c'));
1058
+ it('removePrefix', () => expect(removePrefix('foobar', 'foo')).toBe('bar'));
1059
+ it('removeSuffix', () => expect(removeSuffix('foobar', 'bar')).toBe('foo'));
1060
+ it('wrap', () => expect(wrap('hello', '"')).toBe('"hello"'));
1061
+ it('unwrap', () => expect(unwrap('"hello"', '"')).toBe('hello'));
1062
+ });
1063
+
1064
+ describe('string — splitting', () => {
1065
+ it('lines splits by newline', () => {
1066
+ const l = lines('a\nb\nc');
1067
+ expect(l).toEqual(['a', 'b', 'c']);
1068
+ });
1069
+ it('words splits by whitespace', () => {
1070
+ const w = words('hello world foo');
1071
+ expect(w).toEqual(['hello', 'world', 'foo']);
1072
+ });
1073
+ it('chars splits to characters', () => {
1074
+ expect(chars('abc')).toEqual(['a', 'b', 'c']);
1075
+ });
1076
+ });
1077
+
1078
+ describe('string — formatting', () => {
1079
+ it('format substitutes named placeholders', () => {
1080
+ const result = format('Hello {name}, you are {age}!', { name: 'World', age: 42 });
1081
+ expect(result).toContain('World');
1082
+ expect(result).toContain('42');
1083
+ });
1084
+
1085
+ it('formatBytes', () => {
1086
+ const s = formatBytes(1024);
1087
+ expect(s).toMatch(/KB|KiB/i);
1088
+ });
1089
+
1090
+ it('formatDuration', () => {
1091
+ const s = formatDuration(3661000);
1092
+ expect(s).toMatch(/\d/);
1093
+ });
1094
+ });
1095
+
1096
+ describe('string — escaping', () => {
1097
+ it('escapeHtml / unescapeHtml', () => {
1098
+ const escaped = escapeHtml('<b>hello</b>');
1099
+ expect(escaped).toContain('&lt;');
1100
+ expect(unescapeHtml(escaped)).toBe('<b>hello</b>');
1101
+ });
1102
+
1103
+ it('escapeRegex', () => {
1104
+ const s = escapeRegex('a.b*c');
1105
+ expect(s).toContain('\\.');
1106
+ expect(s).toContain('\\*');
1107
+ });
1108
+
1109
+ it('slugify', () => {
1110
+ expect(slugify('Hello World!')).toBe('hello-world');
1111
+ });
1112
+ });
1113
+
1114
+ describe('string — similarity', () => {
1115
+ it('levenshtein', () => {
1116
+ expect(levenshtein('kitten', 'sitting')).toBeGreaterThan(0);
1117
+ expect(levenshtein('abc', 'abc')).toBe(0);
1118
+ });
1119
+
1120
+ it('similarity returns 1 for identical strings', () => {
1121
+ expect(similarity('hello', 'hello')).toBe(1);
1122
+ });
1123
+
1124
+ it('similarity returns value in [0,1]', () => {
1125
+ const s = similarity('hello', 'world');
1126
+ expect(s).toBeGreaterThanOrEqual(0);
1127
+ expect(s).toBeLessThanOrEqual(1);
1128
+ });
1129
+ });
1130
+
1131
+ describe('string — indentation', () => {
1132
+ it('indent adds prefix', () => {
1133
+ const s = indent('a\nb', 2);
1134
+ expect(s).toContain(' a');
1135
+ expect(s).toContain(' b');
1136
+ });
1137
+
1138
+ it('dedent removes common indentation', () => {
1139
+ const s = dedent(' a\n b\n c');
1140
+ expect(s).not.toMatch(/^ /m);
1141
+ });
1142
+
1143
+ it('wordWrap wraps long lines', () => {
1144
+ const s = wordWrap('hello world foo bar baz', 10);
1145
+ const lineArr = s.split('\n');
1146
+ for (const line of lineArr) {
1147
+ expect(line.length).toBeLessThanOrEqual(10);
1148
+ }
1149
+ });
1150
+ });
1151
+
1152
+ describe('string — generation', () => {
1153
+ it('randomString generates string of given length', () => {
1154
+ const s = randomString(10);
1155
+ expect(s.length).toBe(10);
1156
+ expect(typeof s).toBe('string');
1157
+ });
1158
+
1159
+ it('uuid returns valid UUID format', () => {
1160
+ const id = uuid();
1161
+ expect(id).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i);
1162
+ });
1163
+ });
1164
+
1165
+ // ═══════════════════════════════════════════════
1166
+ // time utilities
1167
+ // ═══════════════════════════════════════════════
1168
+ describe('now', () => {
1169
+ it('returns number', () => {
1170
+ expect(typeof now()).toBe('number');
1171
+ });
1172
+
1173
+ it('increases over time', async () => {
1174
+ const t1 = now();
1175
+ await sleep(10);
1176
+ const t2 = now();
1177
+ expect(t2).toBeGreaterThan(t1);
1178
+ });
1179
+ });
1180
+
1181
+ describe('measure', () => {
1182
+ it('returns duration and result', async () => {
1183
+ const result = await measure(async () => {
1184
+ await sleep(5);
1185
+ return 42;
1186
+ });
1187
+ expect(result.duration).toBeGreaterThanOrEqual(0);
1188
+ expect(result.result).toBe(42);
1189
+ });
1190
+ });
1191
+
1192
+ describe('debounce', () => {
1193
+ it('delays function call', async () => {
1194
+ vi.useFakeTimers();
1195
+ const fn = vi.fn();
1196
+ const debounced = debounce(fn, 100);
1197
+ debounced();
1198
+ debounced();
1199
+ debounced();
1200
+ expect(fn).not.toHaveBeenCalled();
1201
+ await vi.advanceTimersByTimeAsync(150);
1202
+ expect(fn).toHaveBeenCalledTimes(1);
1203
+ vi.useRealTimers();
1204
+ });
1205
+ });
1206
+
1207
+ describe('throttle', () => {
1208
+ it('limits call frequency', async () => {
1209
+ vi.useFakeTimers();
1210
+ const fn = vi.fn();
1211
+ const throttled = throttle(fn, 100);
1212
+ throttled();
1213
+ throttled();
1214
+ throttled();
1215
+ expect(fn).toHaveBeenCalledTimes(1);
1216
+ vi.useRealTimers();
1217
+ });
1218
+ });
1219
+
1220
+ describe('Stopwatch', () => {
1221
+ it('starts and stops', async () => {
1222
+ const sw = new Stopwatch();
1223
+ sw.start();
1224
+ await sleep(20);
1225
+ sw.stop();
1226
+ expect(sw.elapsed).toBeGreaterThan(0);
1227
+ });
1228
+
1229
+ it('reset clears elapsed', () => {
1230
+ const sw = new Stopwatch();
1231
+ sw.start();
1232
+ sw.stop();
1233
+ sw.reset();
1234
+ expect(sw.elapsed).toBe(0);
1235
+ });
1236
+
1237
+ it('lap returns elapsed and resets timer', async () => {
1238
+ const sw = new Stopwatch();
1239
+ sw.start();
1240
+ await sleep(10);
1241
+ const lapTime = sw.lap();
1242
+ expect(lapTime).toBeGreaterThan(0);
1243
+ // after lap(), stopwatch restarted
1244
+ expect(sw.isRunning).toBe(true);
1245
+ });
1246
+
1247
+ it('restart resets and starts again', async () => {
1248
+ const sw = new Stopwatch();
1249
+ sw.start();
1250
+ await sleep(10);
1251
+ sw.restart();
1252
+ const e = sw.elapsed;
1253
+ expect(e).toBeLessThan(50);
1254
+ });
1255
+ });
1256
+
1257
+ describe('FrameTimer', () => {
1258
+ it('tracks delta via update()', async () => {
1259
+ const ft = new FrameTimer();
1260
+ ft.update();
1261
+ await sleep(16);
1262
+ ft.update();
1263
+ expect(ft.delta).toBeGreaterThan(0);
1264
+ expect(ft.frames).toBeGreaterThan(0);
1265
+ });
1266
+ });
1267
+
1268
+ describe('DateTime', () => {
1269
+ it('toISO returns ISO string', () => {
1270
+ const date = new Date(2024, 0, 15);
1271
+ const iso = DateTime.toISO(date);
1272
+ expect(iso).toMatch(/2024-01-15/);
1273
+ });
1274
+
1275
+ it('format with pattern', () => {
1276
+ const date = new Date(2024, 0, 15, 12, 30, 45);
1277
+ const s = DateTime.format(date, 'YYYY-MM-DD');
1278
+ expect(s).toMatch(/2024-\d{2}-\d{2}/);
1279
+ });
1280
+
1281
+ it('diff returns millisecond difference', () => {
1282
+ const d1 = new Date(2024, 0, 1);
1283
+ const d2 = new Date(2024, 0, 2);
1284
+ const diff = DateTime.diff(d1, d2);
1285
+ expect(Math.abs(diff)).toBe(86400000);
1286
+ });
1287
+
1288
+ it('add adds duration', () => {
1289
+ const d = new Date(2024, 0, 1);
1290
+ const next = DateTime.add(d, 1, 'd');
1291
+ expect(next.getDate()).toBe(2);
1292
+ });
1293
+
1294
+ it('startOfDay / endOfDay', () => {
1295
+ const d = new Date(2024, 0, 15, 12, 30);
1296
+ const start = DateTime.startOfDay(d);
1297
+ const end = DateTime.endOfDay(d);
1298
+ expect(start.getHours()).toBe(0);
1299
+ expect(end.getHours()).toBe(23);
1300
+ });
1301
+ });