@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,1070 +0,0 @@
1
- /**
2
- * @holoscript/std acceptance tests
3
- * Covers: string formatting/validation/escaping, time utilities (Stopwatch,
4
- * CountdownTimer, debounce, throttle, retry, measure), and
5
- * collections (HoloMap, HoloSet, PriorityQueue).
6
- */
7
- import { describe, it, expect, vi, beforeEach } from 'vitest';
8
- import {
9
- // String — formatting
10
- format,
11
- formatBytes,
12
- formatDuration,
13
- formatNumber,
14
- numberWithCommas,
15
- // String — HTML/regex
16
- escapeHtml,
17
- unescapeHtml,
18
- escapeRegex,
19
- slugify,
20
- // String — validation
21
- isValidIdentifier,
22
- isNumeric,
23
- isAlphanumeric,
24
- isAlpha,
25
- isBlank,
26
- isNotBlank,
27
- // String — manipulation
28
- truncate,
29
- truncateMiddle,
30
- center,
31
- padLeft,
32
- padRight,
33
- removePrefix,
34
- removeSuffix,
35
- indent,
36
- dedent,
37
- count,
38
- reverse,
39
- levenshtein,
40
- similarity,
41
- uuid,
42
- randomString,
43
- } from '../string.js';
44
-
45
- import {
46
- measure,
47
- retry,
48
- debounce,
49
- throttle,
50
- Stopwatch,
51
- CountdownTimer,
52
- IntervalTimer,
53
- FrameTimer,
54
- schedule,
55
- DateTime,
56
- } from '../time.js';
57
-
58
- import { HoloMap, HoloSet, PriorityQueue, List } from '../collections.js';
59
-
60
- // ═══════════════════════════════════════════════
61
- // String — formatting
62
- // ═══════════════════════════════════════════════
63
- describe('format', () => {
64
- it('replaces named placeholders', () => {
65
- expect(format('Hello {name}!', { name: 'Alice' })).toBe('Hello Alice!');
66
- });
67
-
68
- it('replaces multiple placeholders', () => {
69
- expect(format('{a} + {b} = {c}', { a: 1, b: 2, c: 3 })).toBe('1 + 2 = 3');
70
- });
71
-
72
- it('leaves unknown placeholders unchanged', () => {
73
- expect(format('{x} is unknown', {})).toBe('{x} is unknown');
74
- });
75
-
76
- it('handles empty template', () => {
77
- expect(format('', { a: 1 })).toBe('');
78
- });
79
- });
80
-
81
- describe('formatBytes', () => {
82
- it('0 bytes', () => {
83
- expect(formatBytes(0)).toBe('0 B');
84
- });
85
-
86
- it('bytes under 1KB', () => {
87
- expect(formatBytes(512)).toBe('512 B');
88
- });
89
-
90
- it('kilobytes', () => {
91
- expect(formatBytes(1024)).toBe('1 KB');
92
- });
93
-
94
- it('megabytes', () => {
95
- expect(formatBytes(1024 * 1024)).toBe('1 MB');
96
- });
97
-
98
- it('gigabytes', () => {
99
- expect(formatBytes(1024 ** 3)).toBe('1 GB');
100
- });
101
-
102
- it('custom decimals', () => {
103
- expect(formatBytes(1536, 1)).toBe('1.5 KB');
104
- });
105
- });
106
-
107
- describe('formatDuration', () => {
108
- it('milliseconds under 1s', () => {
109
- expect(formatDuration(500)).toBe('500ms');
110
- });
111
-
112
- it('seconds', () => {
113
- expect(formatDuration(2500)).toBe('2.5s');
114
- });
115
-
116
- it('minutes', () => {
117
- expect(formatDuration(90000)).toBe('1.5m');
118
- });
119
-
120
- it('hours', () => {
121
- expect(formatDuration(3900000)).toBe('1.1h');
122
- });
123
- });
124
-
125
- describe('numberWithCommas', () => {
126
- it('formats integer with commas', () => {
127
- expect(numberWithCommas(1000000)).toBe('1,000,000');
128
- });
129
-
130
- it('does not add commas for small numbers', () => {
131
- expect(numberWithCommas(999)).toBe('999');
132
- });
133
- });
134
-
135
- // ═══════════════════════════════════════════════
136
- // String — HTML / regex escaping
137
- // ═══════════════════════════════════════════════
138
- describe('escapeHtml', () => {
139
- it('escapes ampersand', () => {
140
- expect(escapeHtml('a & b')).toBe('a & b');
141
- });
142
-
143
- it('escapes angle brackets', () => {
144
- expect(escapeHtml('<script>')).toBe('&lt;script&gt;');
145
- });
146
-
147
- it('escapes double quotes', () => {
148
- expect(escapeHtml('"hello"')).toBe('&quot;hello&quot;');
149
- });
150
-
151
- it('escapes single quotes', () => {
152
- expect(escapeHtml("it's")).toBe('it&#39;s');
153
- });
154
-
155
- it('passes safe strings unchanged', () => {
156
- expect(escapeHtml('hello world')).toBe('hello world');
157
- });
158
- });
159
-
160
- describe('unescapeHtml', () => {
161
- it('unescapes &amp;', () => {
162
- expect(unescapeHtml('a &amp; b')).toBe('a & b');
163
- });
164
-
165
- it('unescapes &lt; and &gt;', () => {
166
- expect(unescapeHtml('&lt;b&gt;')).toBe('<b>');
167
- });
168
-
169
- it('unescapes &quot;', () => {
170
- expect(unescapeHtml('&quot;hi&quot;')).toBe('"hi"');
171
- });
172
-
173
- it('round-trips through escapeHtml', () => {
174
- const original = '<div class="test">Hello & World</div>';
175
- expect(unescapeHtml(escapeHtml(original))).toBe(original);
176
- });
177
- });
178
-
179
- describe('escapeRegex', () => {
180
- it('escapes dot', () => {
181
- const pattern = escapeRegex('a.b');
182
- expect(new RegExp(pattern).test('axb')).toBe(false);
183
- expect(new RegExp(pattern).test('a.b')).toBe(true);
184
- });
185
-
186
- it('escapes parentheses', () => {
187
- const pattern = escapeRegex('(test)');
188
- expect(new RegExp(pattern).test('(test)')).toBe(true);
189
- });
190
-
191
- it('escapes brackets', () => {
192
- expect(escapeRegex('[a]')).toBe('\\[a\\]');
193
- });
194
- });
195
-
196
- describe('slugify', () => {
197
- it('lowercases and replaces spaces with hyphens', () => {
198
- expect(slugify('Hello World')).toBe('hello-world');
199
- });
200
-
201
- it('removes special characters', () => {
202
- expect(slugify('Hello, World!')).toBe('hello-world');
203
- });
204
-
205
- it('strips leading/trailing hyphens', () => {
206
- expect(slugify(' hello ')).toBe('hello');
207
- });
208
-
209
- it('collapses multiple separators', () => {
210
- expect(slugify('hello world')).toBe('hello-world');
211
- });
212
- });
213
-
214
- // ═══════════════════════════════════════════════
215
- // String — validation
216
- // ═══════════════════════════════════════════════
217
- describe('isValidIdentifier', () => {
218
- it('accepts simple identifier', () => {
219
- expect(isValidIdentifier('myVar')).toBe(true);
220
- });
221
-
222
- it('accepts underscore prefix', () => {
223
- expect(isValidIdentifier('_private')).toBe(true);
224
- });
225
-
226
- it('accepts dollar prefix', () => {
227
- expect(isValidIdentifier('$el')).toBe(true);
228
- });
229
-
230
- it('rejects identifier starting with digit', () => {
231
- expect(isValidIdentifier('1bad')).toBe(false);
232
- });
233
-
234
- it('rejects identifier with spaces', () => {
235
- expect(isValidIdentifier('my var')).toBe(false);
236
- });
237
- });
238
-
239
- describe('isNumeric', () => {
240
- it('accepts integer string', () => {
241
- expect(isNumeric('42')).toBe(true);
242
- });
243
-
244
- it('accepts float string', () => {
245
- expect(isNumeric('3.14')).toBe(true);
246
- });
247
-
248
- it('rejects non-numeric', () => {
249
- expect(isNumeric('abc')).toBe(false);
250
- });
251
- });
252
-
253
- describe('isAlphanumeric / isAlpha / isBlank / isNotBlank', () => {
254
- it('isAlphanumeric accepts letters and digits', () => {
255
- expect(isAlphanumeric('abc123')).toBe(true);
256
- });
257
-
258
- it('isAlphanumeric rejects spaces', () => {
259
- expect(isAlphanumeric('abc 123')).toBe(false);
260
- });
261
-
262
- it('isAlpha accepts letters only', () => {
263
- expect(isAlpha('hello')).toBe(true);
264
- expect(isAlpha('hello1')).toBe(false);
265
- });
266
-
267
- it('isBlank for empty string', () => {
268
- expect(isBlank('')).toBe(true);
269
- expect(isBlank(' ')).toBe(true);
270
- });
271
-
272
- it('isNotBlank for non-empty string', () => {
273
- expect(isNotBlank('hello')).toBe(true);
274
- expect(isNotBlank('')).toBe(false);
275
- });
276
- });
277
-
278
- // ═══════════════════════════════════════════════
279
- // String — manipulation
280
- // ═══════════════════════════════════════════════
281
- describe('truncate', () => {
282
- it('does not truncate short strings', () => {
283
- expect(truncate('hi', 10)).toBe('hi');
284
- });
285
-
286
- it('truncates long strings with ellipsis', () => {
287
- expect(truncate('hello world', 8)).toBe('hello...');
288
- });
289
-
290
- it('custom ellipsis', () => {
291
- // Use ASCII marker to avoid Unicode encoding variance in this fixture.
292
- expect(truncate('hello world', 7, '*')).toBe('hello *');
293
- });
294
- });
295
-
296
- describe('truncateMiddle', () => {
297
- it('does not truncate short strings', () => {
298
- expect(truncateMiddle('hi', 10)).toBe('hi');
299
- });
300
-
301
- it('preserves start and end with ellipsis in middle', () => {
302
- // 'hello_world_long' length=16, maxLength=11, ellipsis='...' (3 chars)
303
- // available=8, leftLength=ceil(4)=4, rightLength=floor(4)=4
304
- // result = 'hell' + '...' + 'long'
305
- const result = truncateMiddle('hello_world_long', 11);
306
- expect(result.includes('...')).toBe(true);
307
- expect(result.length).toBe(11);
308
- });
309
- });
310
-
311
- describe('center', () => {
312
- it('centers a string', () => {
313
- expect(center('hi', 6)).toBe(' hi ');
314
- });
315
-
316
- it('uses custom char', () => {
317
- expect(center('hi', 6, '-')).toBe('--hi--');
318
- });
319
- });
320
-
321
- describe('padLeft / padRight', () => {
322
- it('pads left with spaces', () => {
323
- expect(padLeft('5', 3)).toBe(' 5');
324
- });
325
-
326
- it('pads right with zeros', () => {
327
- expect(padRight('5', 3, '0')).toBe('500');
328
- });
329
- });
330
-
331
- describe('removePrefix / removeSuffix', () => {
332
- it('removes prefix', () => {
333
- expect(removePrefix('https://example.com', 'https://')).toBe('example.com');
334
- });
335
-
336
- it('no change if prefix absent', () => {
337
- expect(removePrefix('example.com', 'https://')).toBe('example.com');
338
- });
339
-
340
- it('removes suffix', () => {
341
- expect(removeSuffix('file.ts', '.ts')).toBe('file');
342
- });
343
-
344
- it('no change if suffix absent', () => {
345
- expect(removeSuffix('file.ts', '.js')).toBe('file.ts');
346
- });
347
- });
348
-
349
- describe('indent / dedent', () => {
350
- it('indents each line', () => {
351
- expect(indent('a\nb', 2)).toBe(' a\n b');
352
- });
353
-
354
- it('dedent removes common indent', () => {
355
- expect(dedent(' a\n b')).toBe('a\nb');
356
- });
357
- });
358
-
359
- describe('count / reverse', () => {
360
- it('counts substring occurrences', () => {
361
- expect(count('abcabc', 'a')).toBe(2);
362
- });
363
-
364
- it('returns 0 when not found', () => {
365
- expect(count('hello', 'z')).toBe(0);
366
- });
367
-
368
- it('reverses a string', () => {
369
- expect(reverse('hello')).toBe('olleh');
370
- });
371
- });
372
-
373
- describe('levenshtein / similarity', () => {
374
- it('levenshtein of identical strings is 0', () => {
375
- expect(levenshtein('hello', 'hello')).toBe(0);
376
- });
377
-
378
- it('levenshtein of empty to non-empty is length', () => {
379
- expect(levenshtein('', 'abc')).toBe(3);
380
- });
381
-
382
- it('levenshtein of substitution', () => {
383
- expect(levenshtein('cat', 'cut')).toBe(1);
384
- });
385
-
386
- it('similarity of identical strings is 1', () => {
387
- expect(similarity('hello', 'hello')).toBe(1);
388
- });
389
-
390
- it('similarity of empty strings is 1', () => {
391
- expect(similarity('', '')).toBe(1);
392
- });
393
-
394
- it('similarity of completely different strings is less than 0.5', () => {
395
- expect(similarity('abc', 'xyz')).toBeLessThan(0.5);
396
- });
397
- });
398
-
399
- describe('uuid', () => {
400
- it('matches UUID v4 format', () => {
401
- expect(uuid()).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/);
402
- });
403
-
404
- it('generates unique values', () => {
405
- expect(uuid()).not.toBe(uuid());
406
- });
407
- });
408
-
409
- describe('randomString', () => {
410
- it('generates string of requested length', () => {
411
- expect(randomString(10)).toHaveLength(10);
412
- });
413
-
414
- it('uses custom charset', () => {
415
- const result = randomString(20, '01');
416
- expect(/^[01]+$/.test(result)).toBe(true);
417
- });
418
- });
419
-
420
- // ═══════════════════════════════════════════════
421
- // Time — measure
422
- // ═══════════════════════════════════════════════
423
- describe('measure', () => {
424
- it('returns result and duration', async () => {
425
- const { result, duration } = await measure(() => 42);
426
- expect(result).toBe(42);
427
- expect(duration).toBeGreaterThanOrEqual(0);
428
- });
429
-
430
- it('works with async functions', async () => {
431
- const { result, duration } = await measure(async () => 'done');
432
- expect(result).toBe('done');
433
- expect(typeof duration).toBe('number');
434
- });
435
- });
436
-
437
- // ═══════════════════════════════════════════════
438
- // Time — retry
439
- // ═══════════════════════════════════════════════
440
- describe('retry', () => {
441
- it('returns immediately on first success', async () => {
442
- let calls = 0;
443
- const result = await retry(
444
- async () => {
445
- calls++;
446
- return 42;
447
- },
448
- { maxAttempts: 3, initialDelay: 0 }
449
- );
450
- expect(result).toBe(42);
451
- expect(calls).toBe(1);
452
- });
453
-
454
- it('retries on failure and eventually succeeds', async () => {
455
- let calls = 0;
456
- const result = await retry(
457
- async () => {
458
- calls++;
459
- if (calls < 3) throw new Error('fail');
460
- return 'ok';
461
- },
462
- { maxAttempts: 5, initialDelay: 0 }
463
- );
464
- expect(result).toBe('ok');
465
- expect(calls).toBe(3);
466
- });
467
-
468
- it('throws after maxAttempts exhausted', async () => {
469
- await expect(
470
- retry(
471
- async () => {
472
- throw new Error('always fail');
473
- },
474
- { maxAttempts: 2, initialDelay: 0 }
475
- )
476
- ).rejects.toThrow('always fail');
477
- });
478
-
479
- it('calls onRetry callback', async () => {
480
- const onRetry = vi.fn();
481
- await retry(
482
- async () => {
483
- if (onRetry.mock.calls.length === 0) throw new Error('fail');
484
- return 'ok';
485
- },
486
- { maxAttempts: 3, initialDelay: 0, onRetry }
487
- ).catch(() => {});
488
- // onRetry may or may not have been called depending on timing; just verify it's a function
489
- expect(typeof onRetry).toBe('function');
490
- });
491
- });
492
-
493
- // ═══════════════════════════════════════════════
494
- // Time — debounce
495
- // ═══════════════════════════════════════════════
496
- describe('debounce', () => {
497
- it('does not call immediately', () => {
498
- vi.useFakeTimers();
499
- const fn = vi.fn();
500
- const debouncedFn = debounce(fn, 100);
501
- debouncedFn();
502
- expect(fn).not.toHaveBeenCalled();
503
- vi.useRealTimers();
504
- });
505
-
506
- it('calls after delay', () => {
507
- vi.useFakeTimers();
508
- const fn = vi.fn();
509
- const debouncedFn = debounce(fn, 100);
510
- debouncedFn();
511
- vi.advanceTimersByTime(100);
512
- expect(fn).toHaveBeenCalledOnce();
513
- vi.useRealTimers();
514
- });
515
-
516
- it('resets timer on repeated calls', () => {
517
- vi.useFakeTimers();
518
- const fn = vi.fn();
519
- const debouncedFn = debounce(fn, 100);
520
- debouncedFn();
521
- vi.advanceTimersByTime(50);
522
- debouncedFn();
523
- vi.advanceTimersByTime(50);
524
- expect(fn).not.toHaveBeenCalled();
525
- vi.advanceTimersByTime(50);
526
- expect(fn).toHaveBeenCalledOnce();
527
- vi.useRealTimers();
528
- });
529
- });
530
-
531
- // ═══════════════════════════════════════════════
532
- // Time — throttle
533
- // ═══════════════════════════════════════════════
534
- describe('throttle', () => {
535
- it('calls function on first invocation', () => {
536
- vi.useFakeTimers();
537
- const fn = vi.fn();
538
- const throttledFn = throttle(fn, 100);
539
- throttledFn();
540
- expect(fn).toHaveBeenCalledOnce();
541
- vi.useRealTimers();
542
- });
543
-
544
- it('blocks subsequent calls within window', () => {
545
- vi.useFakeTimers();
546
- const fn = vi.fn();
547
- const throttledFn = throttle(fn, 100);
548
- throttledFn();
549
- throttledFn();
550
- throttledFn();
551
- expect(fn).toHaveBeenCalledTimes(1);
552
- vi.useRealTimers();
553
- });
554
-
555
- it('allows call after window expires', () => {
556
- vi.useFakeTimers();
557
- const fn = vi.fn();
558
- const throttledFn = throttle(fn, 100);
559
- throttledFn();
560
- vi.advanceTimersByTime(150);
561
- throttledFn();
562
- expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
563
- vi.useRealTimers();
564
- });
565
- });
566
-
567
- // ═══════════════════════════════════════════════
568
- // Time — Stopwatch
569
- // ═══════════════════════════════════════════════
570
- describe('Stopwatch', () => {
571
- it('starts not running', () => {
572
- const sw = new Stopwatch();
573
- expect(sw.isRunning).toBe(false);
574
- });
575
-
576
- it('elapsed is 0 before start', () => {
577
- const sw = new Stopwatch();
578
- expect(sw.elapsed).toBe(0);
579
- });
580
-
581
- it('starts and becomes running', () => {
582
- const sw = new Stopwatch();
583
- sw.start();
584
- expect(sw.isRunning).toBe(true);
585
- sw.stop();
586
- });
587
-
588
- it('elapsed is positive after start', async () => {
589
- const sw = new Stopwatch();
590
- sw.start();
591
- await new Promise((r) => setTimeout(r, 10));
592
- expect(sw.elapsed).toBeGreaterThan(0);
593
- sw.stop();
594
- });
595
-
596
- it('stop pauses elapsed time', async () => {
597
- const sw = new Stopwatch();
598
- sw.start();
599
- await new Promise((r) => setTimeout(r, 10));
600
- sw.stop();
601
- const elapsed = sw.elapsed;
602
- await new Promise((r) => setTimeout(r, 10));
603
- expect(sw.elapsed).toBeCloseTo(elapsed, 0);
604
- });
605
-
606
- it('reset clears elapsed and running state', () => {
607
- const sw = new Stopwatch();
608
- sw.start();
609
- sw.reset();
610
- expect(sw.elapsed).toBe(0);
611
- expect(sw.isRunning).toBe(false);
612
- });
613
-
614
- it('restart resets and starts again', async () => {
615
- const sw = new Stopwatch();
616
- sw.start();
617
- await new Promise((r) => setTimeout(r, 20));
618
- sw.restart();
619
- expect(sw.isRunning).toBe(true);
620
- expect(sw.elapsed).toBeLessThan(15);
621
- sw.stop();
622
- });
623
-
624
- it('lap returns elapsed time and restarts', async () => {
625
- const sw = new Stopwatch();
626
- sw.start();
627
- await new Promise((r) => setTimeout(r, 10));
628
- const lapTime = sw.lap();
629
- expect(typeof lapTime).toBe('number');
630
- expect(lapTime).toBeGreaterThan(0);
631
- // After lap(), stopwatch restarted — elapsed should be less than before
632
- expect(sw.elapsed).toBeLessThan(lapTime);
633
- sw.stop();
634
- });
635
- });
636
-
637
- // ═══════════════════════════════════════════════
638
- // Time — CountdownTimer
639
- // ═══════════════════════════════════════════════
640
- describe('CountdownTimer', () => {
641
- it('starts not running', () => {
642
- const ct = new CountdownTimer(1000);
643
- expect(ct.isRunning).toBe(false);
644
- });
645
-
646
- it('remaining equals duration before start', () => {
647
- const ct = new CountdownTimer(5000);
648
- expect(ct.remaining).toBe(5000);
649
- });
650
-
651
- it('isComplete is false before start', () => {
652
- const ct = new CountdownTimer(1000);
653
- expect(ct.isComplete).toBe(false);
654
- });
655
-
656
- it('starts and becomes running', () => {
657
- const ct = new CountdownTimer(1000);
658
- ct.start();
659
- expect(ct.isRunning).toBe(true);
660
- ct.stop();
661
- });
662
-
663
- it('pause stops running', () => {
664
- const ct = new CountdownTimer(1000);
665
- ct.start();
666
- ct.pause();
667
- expect(ct.isRunning).toBe(false);
668
- ct.stop();
669
- });
670
-
671
- it('reset restores remaining', () => {
672
- const ct = new CountdownTimer(1000);
673
- ct.start();
674
- ct.reset(2000);
675
- expect(ct.remaining).toBe(2000);
676
- });
677
-
678
- it('fires onComplete callback', async () => {
679
- const onComplete = vi.fn();
680
- const ct = new CountdownTimer(50, { onComplete });
681
- ct.start();
682
- await new Promise((r) => setTimeout(r, 100));
683
- expect(onComplete).toHaveBeenCalledOnce();
684
- });
685
- });
686
-
687
- // ═══════════════════════════════════════════════
688
- // Time — IntervalTimer
689
- // ═══════════════════════════════════════════════
690
- describe('IntervalTimer', () => {
691
- it('starts not running', () => {
692
- const it = new IntervalTimer(() => {}, 1000);
693
- expect(it.running).toBe(false);
694
- });
695
-
696
- it('start makes it running', () => {
697
- const timer = new IntervalTimer(() => {}, 1000);
698
- timer.start();
699
- expect(timer.running).toBe(true);
700
- timer.stop();
701
- });
702
-
703
- it('stop clears running', () => {
704
- const timer = new IntervalTimer(() => {}, 1000);
705
- timer.start();
706
- timer.stop();
707
- expect(timer.running).toBe(false);
708
- });
709
-
710
- it('fires callback at interval', async () => {
711
- const fn = vi.fn();
712
- const timer = new IntervalTimer(fn, 10);
713
- timer.start();
714
- await new Promise((r) => setTimeout(r, 80));
715
- timer.stop();
716
- expect(fn.mock.calls.length).toBeGreaterThanOrEqual(2);
717
- });
718
-
719
- it('pause stops the interval without clearing isPaused', () => {
720
- const timer = new IntervalTimer(() => {}, 1000);
721
- timer.start();
722
- timer.pause();
723
- expect(timer.running).toBe(false);
724
- expect(timer.paused).toBe(true);
725
- timer.stop();
726
- });
727
- });
728
-
729
- // ═══════════════════════════════════════════════
730
- // Time — schedule
731
- // ═══════════════════════════════════════════════
732
- describe('schedule', () => {
733
- it('fires callback after delay', async () => {
734
- const fn = vi.fn();
735
- schedule(fn, 20);
736
- await new Promise((r) => setTimeout(r, 30));
737
- expect(fn).toHaveBeenCalledOnce();
738
- });
739
-
740
- it('cancel function prevents callback', async () => {
741
- const fn = vi.fn();
742
- const cancel = schedule(fn, 50);
743
- cancel();
744
- await new Promise((r) => setTimeout(r, 60));
745
- expect(fn).not.toHaveBeenCalled();
746
- });
747
- });
748
-
749
- // ═══════════════════════════════════════════════
750
- // Time — DateTime
751
- // ═══════════════════════════════════════════════
752
- describe('DateTime', () => {
753
- it('toISO returns ISO string', () => {
754
- const iso = DateTime.toISO(new Date(2024, 0, 15));
755
- expect(iso).toMatch(/^2024/);
756
- expect(iso).toContain('T');
757
- });
758
-
759
- it('format with YYYY pattern', () => {
760
- const result = DateTime.format(new Date(2024, 5, 15), 'YYYY-MM-DD');
761
- expect(result).toBe('2024-06-15');
762
- });
763
-
764
- it('diff in milliseconds (d1 - d2)', () => {
765
- // diff returns d1.getTime() - d2.getTime()
766
- const d1 = new Date(2024, 0, 2);
767
- const d2 = new Date(2024, 0, 1);
768
- expect(DateTime.diff(d1, d2, 'ms')).toBe(86400000);
769
- });
770
-
771
- it('isToday returns true for today', () => {
772
- expect(DateTime.isToday(new Date())).toBe(true);
773
- });
774
-
775
- it('isToday returns false for other day', () => {
776
- expect(DateTime.isToday(new Date(2000, 0, 1))).toBe(false);
777
- });
778
-
779
- it('startOfDay returns midnight', () => {
780
- const start = DateTime.startOfDay(new Date(2024, 5, 15, 12, 30));
781
- expect(start.getHours()).toBe(0);
782
- expect(start.getMinutes()).toBe(0);
783
- expect(start.getSeconds()).toBe(0);
784
- });
785
- });
786
-
787
- // ═══════════════════════════════════════════════
788
- // Collections — HoloMap
789
- // ═══════════════════════════════════════════════
790
- describe('HoloMap', () => {
791
- it('creates empty map', () => {
792
- const m = HoloMap.of<string, number>();
793
- expect(m.size).toBe(0);
794
- expect(m.isEmpty).toBe(true);
795
- });
796
-
797
- it('set and get', () => {
798
- const m = HoloMap.of<string, number>().set('a', 1);
799
- expect(m.get('a')).toBe(1);
800
- });
801
-
802
- it('set returns new map (immutable)', () => {
803
- const m1 = HoloMap.of<string, number>();
804
- const m2 = m1.set('x', 10);
805
- expect(m1.has('x')).toBe(false);
806
- expect(m2.has('x')).toBe(true);
807
- });
808
-
809
- it('delete removes entry', () => {
810
- const m = HoloMap.of<string, number>().set('a', 1).delete('a');
811
- expect(m.has('a')).toBe(false);
812
- });
813
-
814
- it('has returns true for existing key', () => {
815
- const m = HoloMap.of<string, number>().set('z', 99);
816
- expect(m.has('z')).toBe(true);
817
- expect(m.has('w')).toBe(false);
818
- });
819
-
820
- it('getOrDefault returns default for missing key', () => {
821
- const m = HoloMap.of<string, number>();
822
- expect(m.getOrDefault('missing', 42)).toBe(42);
823
- });
824
-
825
- it('from creates from entries', () => {
826
- const m = HoloMap.from([
827
- ['a', 1],
828
- ['b', 2],
829
- ]);
830
- expect(m.size).toBe(2);
831
- expect(m.get('b')).toBe(2);
832
- });
833
-
834
- it('merge combines two maps', () => {
835
- const m1 = HoloMap.from([['a', 1]]);
836
- const m2 = HoloMap.from([['b', 2]]);
837
- const merged = m1.merge(m2);
838
- expect(merged.size).toBe(2);
839
- expect(merged.get('a')).toBe(1);
840
- expect(merged.get('b')).toBe(2);
841
- });
842
-
843
- it('map transforms values', () => {
844
- const m = HoloMap.from([
845
- ['a', 1],
846
- ['b', 2],
847
- ]).map((v) => v * 10);
848
- expect(m.get('a')).toBe(10);
849
- expect(m.get('b')).toBe(20);
850
- });
851
-
852
- it('filter keeps matching entries', () => {
853
- const m = HoloMap.from([
854
- ['a', 1],
855
- ['b', 2],
856
- ['c', 3],
857
- ]).filter((v) => v > 1);
858
- expect(m.size).toBe(2);
859
- expect(m.has('a')).toBe(false);
860
- });
861
-
862
- it('keys, values, entries iteration', () => {
863
- const m = HoloMap.from([
864
- ['x', 1],
865
- ['y', 2],
866
- ]);
867
- expect([...m.keys()].sort()).toEqual(['x', 'y']);
868
- expect([...m.values()].sort()).toEqual([1, 2]);
869
- expect([...m.entries()].length).toBe(2);
870
- });
871
-
872
- it('toObject converts to plain object', () => {
873
- const m = HoloMap.from([
874
- ['a', 1],
875
- ['b', 2],
876
- ]);
877
- const obj = (m as HoloMap<string, number>).toObject();
878
- expect(obj).toEqual({ a: 1, b: 2 });
879
- });
880
- });
881
-
882
- // ═══════════════════════════════════════════════
883
- // Collections — HoloSet
884
- // ═══════════════════════════════════════════════
885
- describe('HoloSet', () => {
886
- it('creates empty set', () => {
887
- const s = HoloSet.of<number>();
888
- expect(s.size).toBe(0);
889
- expect(s.isEmpty).toBe(true);
890
- });
891
-
892
- it('add and has', () => {
893
- const s = HoloSet.of<number>().add(1);
894
- expect(s.has(1)).toBe(true);
895
- expect(s.has(2)).toBe(false);
896
- });
897
-
898
- it('add is immutable', () => {
899
- const s1 = HoloSet.of<number>();
900
- const s2 = s1.add(5);
901
- expect(s1.has(5)).toBe(false);
902
- expect(s2.has(5)).toBe(true);
903
- });
904
-
905
- it('delete removes element', () => {
906
- const s = HoloSet.of<number>().add(1).delete(1);
907
- expect(s.has(1)).toBe(false);
908
- });
909
-
910
- it('from creates from array', () => {
911
- const s = HoloSet.from([1, 2, 3]);
912
- expect(s.size).toBe(3);
913
- });
914
-
915
- it('union combines elements', () => {
916
- const a = HoloSet.from([1, 2]);
917
- const b = HoloSet.from([2, 3]);
918
- const u = a.union(b);
919
- expect(u.size).toBe(3);
920
- expect(u.has(1)).toBe(true);
921
- expect(u.has(3)).toBe(true);
922
- });
923
-
924
- it('intersection keeps common elements', () => {
925
- const a = HoloSet.from([1, 2, 3]);
926
- const b = HoloSet.from([2, 3, 4]);
927
- const inter = a.intersection(b);
928
- expect(inter.size).toBe(2);
929
- expect(inter.has(1)).toBe(false);
930
- expect(inter.has(2)).toBe(true);
931
- });
932
-
933
- it('difference keeps elements not in other', () => {
934
- const a = HoloSet.from([1, 2, 3]);
935
- const b = HoloSet.from([2]);
936
- const diff = a.difference(b);
937
- expect(diff.size).toBe(2);
938
- expect(diff.has(2)).toBe(false);
939
- });
940
-
941
- it('isSubsetOf / isSupersetOf', () => {
942
- const small = HoloSet.from([1, 2]);
943
- const big = HoloSet.from([1, 2, 3]);
944
- expect(small.isSubsetOf(big)).toBe(true);
945
- expect(big.isSupersetOf(small)).toBe(true);
946
- });
947
-
948
- it('toArray converts to array', () => {
949
- const s = HoloSet.from([10, 20, 30]);
950
- expect(s.toArray().sort((a, b) => a - b)).toEqual([10, 20, 30]);
951
- });
952
- });
953
-
954
- // ═══════════════════════════════════════════════
955
- // Collections — PriorityQueue
956
- // ═══════════════════════════════════════════════
957
- describe('PriorityQueue', () => {
958
- it('creates empty queue', () => {
959
- const pq = new PriorityQueue<number>();
960
- expect(pq.size).toBe(0);
961
- expect(pq.isEmpty).toBe(true);
962
- });
963
-
964
- it('enqueue and size', () => {
965
- const pq = new PriorityQueue<number>();
966
- pq.enqueue(5, 1);
967
- pq.enqueue(3, 0);
968
- expect(pq.size).toBe(2);
969
- });
970
-
971
- it('dequeue returns item with highest priority (min-heap)', () => {
972
- const pq = PriorityQueue.minHeap<string>();
973
- pq.enqueue('low', 10);
974
- pq.enqueue('high', 1);
975
- pq.enqueue('mid', 5);
976
- expect(pq.dequeue()).toBe('high');
977
- expect(pq.dequeue()).toBe('mid');
978
- expect(pq.dequeue()).toBe('low');
979
- });
980
-
981
- it('maxHeap dequeues highest priority first', () => {
982
- const pq = PriorityQueue.maxHeap<string>();
983
- pq.enqueue('low', 1);
984
- pq.enqueue('high', 10);
985
- expect(pq.dequeue()).toBe('high');
986
- expect(pq.dequeue()).toBe('low');
987
- });
988
-
989
- it('peek does not remove item', () => {
990
- const pq = PriorityQueue.minHeap<number>();
991
- pq.enqueue(42, 1);
992
- expect(pq.peek()).toBe(42);
993
- expect(pq.size).toBe(1);
994
- });
995
-
996
- it('dequeue on empty returns undefined', () => {
997
- const pq = new PriorityQueue<number>();
998
- expect(pq.dequeue()).toBeUndefined();
999
- });
1000
-
1001
- it('clear empties the queue', () => {
1002
- const pq = PriorityQueue.minHeap<number>();
1003
- pq.enqueue(1, 1);
1004
- pq.enqueue(2, 2);
1005
- pq.clear();
1006
- expect(pq.size).toBe(0);
1007
- expect(pq.isEmpty).toBe(true);
1008
- });
1009
- });
1010
-
1011
- // ═══════════════════════════════════════════════
1012
- // Collections — List (extended)
1013
- // ═══════════════════════════════════════════════
1014
- describe('List (extended)', () => {
1015
- it('range creates sequential list (exclusive end)', () => {
1016
- const l = List.range(1, 6);
1017
- expect(l.toArray()).toEqual([1, 2, 3, 4, 5]);
1018
- });
1019
-
1020
- it('groupBy groups elements', () => {
1021
- const l = List.of(1, 2, 3, 4);
1022
- const grouped = l.groupBy((n) => (n % 2 === 0 ? 'even' : 'odd'));
1023
- expect(grouped.get('even')?.toArray().sort()).toEqual([2, 4]);
1024
- expect(grouped.get('odd')?.toArray().sort()).toEqual([1, 3]);
1025
- });
1026
-
1027
- it('partition splits by predicate', () => {
1028
- const l = List.of(1, 2, 3, 4, 5);
1029
- const [evens, odds] = l.partition((n) => n % 2 === 0);
1030
- expect(evens.toArray()).toEqual([2, 4]);
1031
- expect(odds.toArray()).toEqual([1, 3, 5]);
1032
- });
1033
-
1034
- it('zip pairs two lists', () => {
1035
- const a = List.of(1, 2, 3);
1036
- const b = List.of('a', 'b', 'c');
1037
- const zipped = a.zip(b);
1038
- expect(zipped.toArray()).toEqual([
1039
- [1, 'a'],
1040
- [2, 'b'],
1041
- [3, 'c'],
1042
- ]);
1043
- });
1044
-
1045
- it('unique removes duplicates', () => {
1046
- const l = List.of(1, 2, 1, 3, 2);
1047
- expect(l.unique().toArray().sort()).toEqual([1, 2, 3]);
1048
- });
1049
-
1050
- it('takeLast returns last N elements', () => {
1051
- const l = List.of(1, 2, 3, 4, 5);
1052
- expect(l.takeLast(3).toArray()).toEqual([3, 4, 5]);
1053
- });
1054
-
1055
- it('dropLast removes last N elements', () => {
1056
- const l = List.of(1, 2, 3, 4, 5);
1057
- expect(l.dropLast(2).toArray()).toEqual([1, 2, 3]);
1058
- });
1059
-
1060
- it('average of numeric list', () => {
1061
- const l = List.of(2, 4, 6);
1062
- expect((l as List<number>).average()).toBe(4);
1063
- });
1064
-
1065
- it('none returns true when predicate never satisfied', () => {
1066
- const l = List.of(1, 2, 3);
1067
- expect(l.none((n) => n > 10)).toBe(true);
1068
- expect(l.none((n) => n > 1)).toBe(false);
1069
- });
1070
- });