@holoscript/std 2.1.0 → 6.0.3

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 (159) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/LICENSE +21 -0
  3. package/README.md +197 -0
  4. package/dist/__tests__/EconomicPrimitives.test.d.ts +2 -0
  5. package/dist/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
  6. package/dist/__tests__/EconomicTraits.test.d.ts +2 -0
  7. package/dist/__tests__/EconomicTraits.test.d.ts.map +1 -0
  8. package/dist/__tests__/SimulationLabPrimitives.test.d.ts +2 -0
  9. package/dist/__tests__/SimulationLabPrimitives.test.d.ts.map +1 -0
  10. package/dist/__tests__/collections.test.d.ts +2 -0
  11. package/dist/__tests__/collections.test.d.ts.map +1 -0
  12. package/dist/__tests__/events.test.d.ts +2 -0
  13. package/dist/__tests__/events.test.d.ts.map +1 -0
  14. package/dist/__tests__/index.test.d.ts +2 -0
  15. package/dist/__tests__/index.test.d.ts.map +1 -0
  16. package/dist/__tests__/materials.test.d.ts +2 -0
  17. package/dist/__tests__/materials.test.d.ts.map +1 -0
  18. package/dist/__tests__/math.test.d.ts +2 -0
  19. package/dist/__tests__/math.test.d.ts.map +1 -0
  20. package/dist/__tests__/physics.test.d.ts +2 -0
  21. package/dist/__tests__/physics.test.d.ts.map +1 -0
  22. package/dist/__tests__/spatial.test.d.ts +2 -0
  23. package/dist/__tests__/spatial.test.d.ts.map +1 -0
  24. package/dist/__tests__/standard-library-fundamentals.test.d.ts +2 -0
  25. package/dist/__tests__/standard-library-fundamentals.test.d.ts.map +1 -0
  26. package/dist/__tests__/string-and-time-utilities.test.d.ts +2 -0
  27. package/dist/__tests__/string-and-time-utilities.test.d.ts.map +1 -0
  28. package/dist/__tests__/string-decoupled.test.d.ts +2 -0
  29. package/dist/__tests__/string-decoupled.test.d.ts.map +1 -0
  30. package/dist/__tests__/string.test.d.ts +2 -0
  31. package/dist/__tests__/string.test.d.ts.map +1 -0
  32. package/dist/__tests__/time.test.d.ts +2 -0
  33. package/dist/__tests__/time.test.d.ts.map +1 -0
  34. package/dist/chunk-7HVUYGPS.js +679 -0
  35. package/dist/chunk-7HVUYGPS.js.map +1 -0
  36. package/dist/chunk-P4O3WNG2.js +287 -0
  37. package/dist/chunk-P4O3WNG2.js.map +1 -0
  38. package/dist/chunk-PFX2JM4X.js +683 -0
  39. package/dist/chunk-PFX2JM4X.js.map +1 -0
  40. package/dist/chunk-W2Q3LUCM.js +348 -0
  41. package/dist/chunk-W2Q3LUCM.js.map +1 -0
  42. package/dist/chunk-WS6W35YC.js +443 -0
  43. package/dist/chunk-WS6W35YC.js.map +1 -0
  44. package/dist/chunk-ZXVZAQCZ.js +470 -0
  45. package/dist/chunk-ZXVZAQCZ.js.map +1 -0
  46. package/dist/collections.cjs +715 -0
  47. package/dist/collections.cjs.map +1 -0
  48. package/dist/collections.d.ts.map +1 -1
  49. package/dist/collections.js +16 -719
  50. package/dist/collections.js.map +1 -1
  51. package/dist/events.d.ts +32 -0
  52. package/dist/events.d.ts.map +1 -0
  53. package/dist/index.cjs +4101 -0
  54. package/dist/index.cjs.map +1 -0
  55. package/dist/index.d.ts +21 -9
  56. package/dist/index.d.ts.map +1 -1
  57. package/dist/index.js +1270 -157
  58. package/dist/index.js.map +1 -1
  59. package/dist/materials.d.ts +50 -0
  60. package/dist/materials.d.ts.map +1 -0
  61. package/dist/math.cjs +591 -0
  62. package/dist/math.cjs.map +1 -0
  63. package/dist/math.d.ts.map +1 -1
  64. package/dist/math.js +51 -537
  65. package/dist/math.js.map +1 -1
  66. package/dist/physics.d.ts +59 -0
  67. package/dist/physics.d.ts.map +1 -0
  68. package/dist/spatial.d.ts +82 -0
  69. package/dist/spatial.d.ts.map +1 -0
  70. package/dist/string.basic.test.d.ts +5 -0
  71. package/dist/string.basic.test.d.ts.map +1 -0
  72. package/dist/string.cjs +422 -0
  73. package/dist/string.cjs.map +1 -0
  74. package/dist/string.d.ts +5 -1
  75. package/dist/string.d.ts.map +1 -1
  76. package/dist/string.js +106 -442
  77. package/dist/string.js.map +1 -1
  78. package/dist/string.test.d.ts +5 -0
  79. package/dist/string.test.d.ts.map +1 -0
  80. package/dist/time.cjs +512 -0
  81. package/dist/time.cjs.map +1 -0
  82. package/dist/time.d.ts.map +1 -1
  83. package/dist/time.js +41 -528
  84. package/dist/time.js.map +1 -1
  85. package/dist/traits/ARTraits.d.ts +14 -0
  86. package/dist/traits/ARTraits.d.ts.map +1 -0
  87. package/dist/traits/EconomicPrimitives.cjs +335 -0
  88. package/dist/traits/EconomicPrimitives.cjs.map +1 -0
  89. package/dist/traits/EconomicPrimitives.d.ts +345 -0
  90. package/dist/traits/EconomicPrimitives.d.ts.map +1 -0
  91. package/dist/traits/EconomicPrimitives.js +55 -0
  92. package/dist/traits/EconomicPrimitives.js.map +1 -0
  93. package/dist/traits/EconomicTraits.cjs +471 -0
  94. package/dist/traits/EconomicTraits.cjs.map +1 -0
  95. package/dist/traits/EconomicTraits.d.ts +71 -0
  96. package/dist/traits/EconomicTraits.d.ts.map +1 -0
  97. package/dist/traits/EconomicTraits.js +17 -0
  98. package/dist/traits/EconomicTraits.js.map +1 -0
  99. package/dist/traits/IoTTraits.d.ts +14 -0
  100. package/dist/traits/IoTTraits.d.ts.map +1 -0
  101. package/dist/traits/SimulationLabPrimitives.d.ts +187 -0
  102. package/dist/traits/SimulationLabPrimitives.d.ts.map +1 -0
  103. package/dist/traits/SimulationLabTraits.d.ts +42 -0
  104. package/dist/traits/SimulationLabTraits.d.ts.map +1 -0
  105. package/dist/traits/VRRTraits.d.ts +150 -0
  106. package/dist/traits/VRRTraits.d.ts.map +1 -0
  107. package/dist/traits/__tests__/ARTraits.test.d.ts +2 -0
  108. package/dist/traits/__tests__/ARTraits.test.d.ts.map +1 -0
  109. package/dist/traits/__tests__/EconomicPrimitives.test.d.ts +2 -0
  110. package/dist/traits/__tests__/EconomicPrimitives.test.d.ts.map +1 -0
  111. package/dist/traits/__tests__/EconomicTraits.test.d.ts +2 -0
  112. package/dist/traits/__tests__/EconomicTraits.test.d.ts.map +1 -0
  113. package/dist/types.d.ts +9 -0
  114. package/dist/types.d.ts.map +1 -1
  115. package/error-utf8.log +6 -0
  116. package/error.log +0 -0
  117. package/package.json +42 -19
  118. package/src/__tests__/EconomicPrimitives.test.ts +690 -0
  119. package/src/__tests__/EconomicTraits.test.ts +425 -0
  120. package/src/__tests__/SimulationLabPrimitives.test.ts +415 -0
  121. package/src/__tests__/collections.test.ts +182 -0
  122. package/src/__tests__/events.test.ts +135 -0
  123. package/src/__tests__/index.test.ts +14 -0
  124. package/src/__tests__/materials.test.ts +84 -0
  125. package/src/__tests__/math.test.ts +246 -0
  126. package/src/__tests__/physics.test.ts +83 -0
  127. package/src/__tests__/spatial.test.ts +333 -0
  128. package/src/__tests__/standard-library-fundamentals.test.ts +1301 -0
  129. package/src/__tests__/string-and-time-utilities.test.ts +1070 -0
  130. package/src/__tests__/string-decoupled.test.ts +14 -0
  131. package/src/__tests__/string.test.ts +164 -0
  132. package/src/__tests__/time.test.ts +110 -0
  133. package/src/collections.ts +8 -2
  134. package/src/events.ts +88 -0
  135. package/src/index.ts +161 -11
  136. package/src/materials.ts +109 -0
  137. package/src/math.ts +30 -14
  138. package/src/physics.ts +141 -0
  139. package/src/spatial.ts +320 -0
  140. package/src/string.basic.test.ts +14 -0
  141. package/src/string.test.ts +335 -0
  142. package/src/string.ts +45 -13
  143. package/src/time.ts +10 -11
  144. package/src/traits/ARTraits.ts +103 -0
  145. package/src/traits/EconomicPrimitives.ts +755 -0
  146. package/src/traits/EconomicTraits.ts +552 -0
  147. package/src/traits/IoTTraits.ts +102 -0
  148. package/src/traits/SimulationLabPrimitives.ts +650 -0
  149. package/src/traits/SimulationLabTraits.ts +191 -0
  150. package/src/traits/VRRTraits.ts +326 -0
  151. package/src/traits/__tests__/ARTraits.test.ts +14 -0
  152. package/src/traits/__tests__/EconomicPrimitives.test.ts +14 -0
  153. package/src/traits/__tests__/EconomicTraits.test.ts +14 -0
  154. package/src/types.ts +47 -12
  155. package/std_error.log +5 -0
  156. package/tsup.config.ts +17 -0
  157. package/vitest.config.ts +10 -0
  158. package/dist/types.js +0 -198
  159. package/dist/types.js.map +0 -1
@@ -0,0 +1,1301 @@
1
+ /**
2
+ * @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
+ });