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