@reldens/utils 0.54.0 → 0.55.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CLAUDE.md CHANGED
@@ -29,8 +29,18 @@ npm test
29
29
  **Shortcuts** (`lib/shortcuts.js`):
30
30
  - Provides utility methods for object manipulation, property access, and validation
31
31
  - Imported as `sc` throughout the Reldens codebase
32
- - Key methods: `get`, `hasOwn`, `isObject`, `isArray`, `deepMerge`, `deepClone`, etc.
33
32
  - Used instead of direct property access or Object.prototype methods
33
+ - Type checks: `isObject`, `isArray`, `isFunction`, `isObjectFunction`, `isString`, `isNumber`, `isInt`, `isFloat`, `isBoolean`, `isSymbol`, `isPromise`, `isTrue`
34
+ - Ownership/existence: `hasOwn`, `get`, `getByPath`, `getByPriority`, `length`
35
+ - Array utilities: `inArray`, `isNotEmptyArray`, `removeFromArray`, `randomValueFromArray`, `arraySort`, `sortObjectKeysBy`, `convertObjectsArrayToObjectByKeys`, `chunk`, `flatten`, `unique`
36
+ - Object utilities: `deepMergeProperties`, `propsAssign`, `pickProps`, `omitProps`, `hasDangerousKeys`
37
+ - Search: `fetchByProperty`, `fetchAllByProperty`, `fetchByPropertyOnObject`, `fetchAllByPropertyOnObject`
38
+ - JSON: `toJson`, `parseJson`, `deepJsonClone`, `toJsonString`
39
+ - String utilities: `startsWith`, `contains`, `cleanMessage`, `slugify`, `sanitize`, `sanitizeUrl`, `camelCase`, `capitalizedCamelCase`, `kebabCase`, `capitalize`, `truncate`, `splitToArray`
40
+ - Number utilities: `roundToPrecision`, `randomInteger`, `randomChars`, `randomCharsWithSymbols`, `randomString`, `clamp`, `parseNumber`, `isValidInteger`
41
+ - Date/time: `getCurrentDate`, `getDateForFileName`, `formatDate`, `getTime`
42
+ - Validation: `isValidUrl`, `isValidIsoCode`, `isSecurePath`, `validateInput`
43
+ - Functional: `debounce`, `throttle`, `serializeFormData`
34
44
 
35
45
  **EventsManager** (`lib/events-manager.js`):
36
46
  - Singleton event system used across Reldens
package/lib/shortcuts.js CHANGED
@@ -229,9 +229,9 @@ class Shortcuts
229
229
  return JSON.parse(this.toJsonString(obj));
230
230
  }
231
231
 
232
- toJsonString(obj)
232
+ toJsonString(obj, ...args)
233
233
  {
234
- return JSON.stringify(obj);
234
+ return JSON.stringify(obj, ...args);
235
235
  }
236
236
 
237
237
  get(obj, prop, defaultReturn)
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@reldens/utils",
3
3
  "scope": "@reldens",
4
- "version": "0.54.0",
4
+ "version": "0.55.0",
5
5
  "description": "Reldens - Utils",
6
6
  "author": "Damian A. Pastorini",
7
7
  "license": "MIT",
@@ -57,7 +57,8 @@ class TestEventsManager
57
57
  console.error('Test method failed:', methodName, error.message);
58
58
  }
59
59
  }
60
- this.printSummary();
60
+ let failed = this.printSummary();
61
+ return {total: this.testCount, passed: this.passedCount, failed};
61
62
  }
62
63
 
63
64
  testConstructorCreatesInstance()
@@ -410,14 +411,14 @@ class TestEventsManager
410
411
  username: 'test',
411
412
  password: 'secret123',
412
413
  authToken: 'token123',
413
- apiKey: 'key123',
414
+ secretKey: 'key123',
414
415
  safe: 'data'
415
416
  };
416
417
  let result = emitter.filterSensitiveData(obj);
417
418
  this.assert('test' === result.username, 'Should keep safe fields');
418
419
  this.assert('[FILTERED]' === result.password, 'Should filter password');
419
420
  this.assert('[FILTERED]' === result.authToken, 'Should filter token');
420
- this.assert('[FILTERED]' === result.apiKey, 'Should filter key');
421
+ this.assert('[FILTERED]' === result.secretKey, 'Should filter key');
421
422
  this.assert('data' === result.safe, 'Should keep safe data');
422
423
  });
423
424
  }
@@ -450,14 +451,14 @@ class TestEventsManager
450
451
  password: 'secret'
451
452
  },
452
453
  config: {
453
- apiKey: 'key123',
454
+ secretKey: 'key123',
454
455
  safe: 'value'
455
456
  }
456
457
  };
457
458
  let result = emitter.filterSensitiveData(obj);
458
459
  this.assert('test' === result.user.name, 'Should keep nested safe fields');
459
460
  this.assert('[FILTERED]' === result.user.password, 'Should filter nested password');
460
- this.assert('[FILTERED]' === result.config.apiKey, 'Should filter nested key');
461
+ this.assert('[FILTERED]' === result.config.secretKey, 'Should filter nested key');
461
462
  this.assert('value' === result.config.safe, 'Should keep nested safe value');
462
463
  });
463
464
  }
@@ -530,6 +531,7 @@ class TestEventsManager
530
531
  {
531
532
  this.test('emit uses sanitized arguments', async () => {
532
533
  let emitter = new EventsManager();
534
+ emitter.enableSensitiveDataFiltering = true;
533
535
  let receivedArgs = null;
534
536
 
535
537
  emitter.on('sanitize-test', (...args) => {
@@ -549,6 +551,7 @@ class TestEventsManager
549
551
  {
550
552
  this.test('emitSync uses sanitized arguments', () => {
551
553
  let emitter = new EventsManager();
554
+ emitter.enableSensitiveDataFiltering = true;
552
555
  let receivedArgs = null;
553
556
 
554
557
  emitter.on('sanitize-sync-test', (...args) => {
@@ -1084,26 +1087,20 @@ class TestEventsManager
1084
1087
 
1085
1088
  printSummary()
1086
1089
  {
1087
- console.log('\n'+'='.repeat(50));
1088
- console.log('TEST SUMMARY');
1089
- console.log('='.repeat(50));
1090
- console.log('Total tests:', this.testCount);
1091
- console.log('Passed:', this.passedCount);
1092
- console.log('Failed:', this.testCount - this.passedCount);
1093
- console.log('Success rate:', Math.round((this.passedCount / this.testCount) * 100)+'%');
1094
- if(this.testCount - this.passedCount > 0){
1090
+ let failedCount = this.testCount - this.passedCount;
1091
+ console.log('\n'+'='.repeat(60));
1092
+ console.log('EVENTS MANAGER TEST SUMMARY');
1093
+ console.log('='.repeat(60));
1094
+ console.log('Total: '+this.testCount+' | Passed: '+this.passedCount+' | Failed: '+failedCount);
1095
+ if(0 < failedCount){
1095
1096
  console.log('\nFailed tests:');
1096
1097
  for(let result of this.testResults){
1097
1098
  if('FAIL' === result.status){
1098
- console.log('-', result.name, ':', result.error);
1099
+ console.log(''+result.name+': '+result.error);
1099
1100
  }
1100
1101
  }
1101
1102
  }
1102
- console.log('\n'+'='.repeat(60));
1103
- if(this.testCount - this.passedCount === 0){
1104
- console.log('All tests completed successfully!');
1105
- }
1106
- console.log('='.repeat(60));
1103
+ return failedCount;
1107
1104
  }
1108
1105
 
1109
1106
  }
package/tests/run.js CHANGED
@@ -4,9 +4,22 @@ async function runTests(){
4
4
  console.log('='.repeat(60));
5
5
  console.log('TESTING IMPLEMENTATION');
6
6
  console.log('='.repeat(60));
7
+ let suites = [];
8
+ const { TestShortcuts } = require('./shortcuts-test.js');
9
+ suites.push(await (new TestShortcuts()).runAllTests());
7
10
  const { TestEventsManager } = require('./events-manager-test.js');
8
- let testInstance = new TestEventsManager();
9
- await testInstance.runAllTests();
11
+ suites.push(await (new TestEventsManager()).runAllTests());
12
+ let totalTests = suites.reduce((sum, s) => sum + s.total, 0);
13
+ let totalPassed = suites.reduce((sum, s) => sum + s.passed, 0);
14
+ let totalFailed = suites.reduce((sum, s) => sum + s.failed, 0);
15
+ console.log('\n'+'='.repeat(60));
16
+ console.log('FINAL TOTAL');
17
+ console.log('='.repeat(60));
18
+ console.log('Total: '+totalTests+' | Passed: '+totalPassed+' | Failed: '+totalFailed);
19
+ console.log('='.repeat(60));
20
+ if(0 < totalFailed){
21
+ process.exit(1);
22
+ }
10
23
  }
11
24
 
12
25
  runTests().catch(error => {
@@ -0,0 +1,1107 @@
1
+ /**
2
+ *
3
+ * Reldens - TestShortcuts
4
+ *
5
+ */
6
+
7
+ const sc = require('../lib/shortcuts');
8
+
9
+ class TestShortcuts
10
+ {
11
+
12
+ constructor()
13
+ {
14
+ this.testResults = [];
15
+ this.testCount = 0;
16
+ this.passedCount = 0;
17
+ this.currentTestMethod = '';
18
+ }
19
+
20
+ test(name, testFn)
21
+ {
22
+ this.testCount++;
23
+ try{
24
+ testFn();
25
+ let logMessage = '✓ PASS: '+this.currentTestMethod+' - '+name;
26
+ console.log(logMessage);
27
+ this.passedCount++;
28
+ this.testResults.push({name, status: 'PASS'});
29
+ } catch(error){
30
+ let logMessage = '✗ FAIL: '+this.currentTestMethod+' - '+name+' - '+error.message;
31
+ console.log(logMessage);
32
+ this.testResults.push({name, status: 'FAIL', error: error.message});
33
+ }
34
+ }
35
+
36
+ assert(condition, message)
37
+ {
38
+ if(!condition){
39
+ throw new Error(message || 'Assertion failed');
40
+ }
41
+ }
42
+
43
+ async runAllTests()
44
+ {
45
+ console.log('Running tests for Shortcuts...\n');
46
+ let methodNames = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
47
+ let testMethods = methodNames.filter(name =>
48
+ name.startsWith('test') &&
49
+ 'function' === typeof this[name] &&
50
+ name !== 'test'
51
+ );
52
+ for(let methodName of testMethods){
53
+ this.currentTestMethod = methodName;
54
+ try{
55
+ await this[methodName]();
56
+ } catch(error){
57
+ console.error('Test method failed:', methodName, error.message);
58
+ }
59
+ }
60
+ let failed = this.printSummary();
61
+ return {total: this.testCount, passed: this.passedCount, failed};
62
+ }
63
+
64
+ printSummary()
65
+ {
66
+ console.log('\n'+'='.repeat(60));
67
+ console.log('SHORTCUTS TEST SUMMARY');
68
+ console.log('='.repeat(60));
69
+ let failedTests = this.testResults.filter(r => 'FAIL' === r.status);
70
+ console.log('Total: '+this.testCount+' | Passed: '+this.passedCount+' | Failed: '+failedTests.length);
71
+ if(0 < failedTests.length){
72
+ console.log('\nFailed tests:');
73
+ for(let failed of failedTests){
74
+ console.log(' ✗ '+failed.name+': '+failed.error);
75
+ }
76
+ }
77
+ return failedTests.length;
78
+ }
79
+
80
+ testHasOwn()
81
+ {
82
+ this.test('returns true for existing prop', () => {
83
+ this.assert(sc.hasOwn({a: 1}, 'a'));
84
+ });
85
+ this.test('returns false for missing prop', () => {
86
+ this.assert(!sc.hasOwn({a: 1}, 'b'));
87
+ });
88
+ this.test('returns false for undefined value', () => {
89
+ this.assert(!sc.hasOwn({a: undefined}, 'a'));
90
+ });
91
+ this.test('accepts array of props - all present', () => {
92
+ this.assert(sc.hasOwn({a: 1, b: 2}, ['a', 'b']));
93
+ });
94
+ this.test('accepts array of props - one missing', () => {
95
+ this.assert(!sc.hasOwn({a: 1}, ['a', 'b']));
96
+ });
97
+ this.test('returns false for null obj', () => {
98
+ this.assert(!sc.hasOwn(null, 'a'));
99
+ });
100
+ }
101
+
102
+ testIsTrue()
103
+ {
104
+ this.test('returns true for truthy prop', () => {
105
+ this.assert(sc.isTrue({a: 1}, 'a'));
106
+ });
107
+ this.test('returns false for falsy prop', () => {
108
+ this.assert(!sc.isTrue({a: 0}, 'a'));
109
+ });
110
+ this.test('strict mode - true for boolean true', () => {
111
+ this.assert(sc.isTrue({a: true}, 'a', true));
112
+ });
113
+ this.test('strict mode - false for truthy non-boolean', () => {
114
+ this.assert(!sc.isTrue({a: 1}, 'a', true));
115
+ });
116
+ this.test('returns false for missing prop', () => {
117
+ this.assert(!sc.isTrue({}, 'a'));
118
+ });
119
+ }
120
+
121
+ testIsObject()
122
+ {
123
+ this.test('returns true for plain object', () => {
124
+ this.assert(sc.isObject({a: 1}));
125
+ });
126
+ this.test('returns false for array', () => {
127
+ this.assert(!sc.isObject([1, 2]));
128
+ });
129
+ this.test('returns false for null', () => {
130
+ this.assert(!sc.isObject(null));
131
+ });
132
+ this.test('returns false for string', () => {
133
+ this.assert(!sc.isObject('str'));
134
+ });
135
+ this.test('returns false for number', () => {
136
+ this.assert(!sc.isObject(42));
137
+ });
138
+ }
139
+
140
+ testIsArray()
141
+ {
142
+ this.test('returns true for array', () => {
143
+ this.assert(sc.isArray([1, 2, 3]));
144
+ });
145
+ this.test('returns false for object', () => {
146
+ this.assert(!sc.isArray({a: 1}));
147
+ });
148
+ this.test('returns true for empty array', () => {
149
+ this.assert(sc.isArray([]));
150
+ });
151
+ this.test('returns false for string', () => {
152
+ this.assert(!sc.isArray('abc'));
153
+ });
154
+ }
155
+
156
+ testInArray()
157
+ {
158
+ this.test('returns true when value present', () => {
159
+ this.assert(sc.inArray(2, [1, 2, 3]));
160
+ });
161
+ this.test('returns false when value absent', () => {
162
+ this.assert(!sc.inArray(5, [1, 2, 3]));
163
+ });
164
+ this.test('returns false for non-array', () => {
165
+ this.assert(!sc.inArray(1, 'not an array'));
166
+ });
167
+ }
168
+
169
+ testIsNotEmptyArray()
170
+ {
171
+ this.test('returns true for non-empty array', () => {
172
+ this.assert(sc.isNotEmptyArray([1]));
173
+ });
174
+ this.test('returns false for empty array', () => {
175
+ this.assert(!sc.isNotEmptyArray([]));
176
+ });
177
+ this.test('returns false for non-array', () => {
178
+ this.assert(!sc.isNotEmptyArray('abc'));
179
+ });
180
+ }
181
+
182
+ testIsFunction()
183
+ {
184
+ this.test('returns true for function', () => {
185
+ this.assert(sc.isFunction(() => {}));
186
+ });
187
+ this.test('returns false for non-function', () => {
188
+ this.assert(!sc.isFunction(42));
189
+ });
190
+ this.test('returns false for null', () => {
191
+ this.assert(!sc.isFunction(null));
192
+ });
193
+ }
194
+
195
+ testIsObjectFunction()
196
+ {
197
+ this.test('returns true when property is a function', () => {
198
+ this.assert(sc.isObjectFunction({fn: () => {}}, 'fn'));
199
+ });
200
+ this.test('returns false when property is not a function', () => {
201
+ this.assert(!sc.isObjectFunction({a: 1}, 'a'));
202
+ });
203
+ this.test('returns false for non-object', () => {
204
+ this.assert(!sc.isObjectFunction(null, 'fn'));
205
+ });
206
+ }
207
+
208
+ testIsSymbol()
209
+ {
210
+ this.test('returns true for symbol', () => {
211
+ this.assert(sc.isSymbol(Symbol('x')));
212
+ });
213
+ this.test('returns false for string', () => {
214
+ this.assert(!sc.isSymbol('x'));
215
+ });
216
+ }
217
+
218
+ testIsString()
219
+ {
220
+ this.test('returns true for string', () => {
221
+ this.assert(sc.isString('hello'));
222
+ });
223
+ this.test('returns false for number', () => {
224
+ this.assert(!sc.isString(42));
225
+ });
226
+ this.test('returns true for empty string', () => {
227
+ this.assert(sc.isString(''));
228
+ });
229
+ }
230
+
231
+ testIsNumber()
232
+ {
233
+ this.test('returns true for number', () => {
234
+ this.assert(sc.isNumber(42));
235
+ });
236
+ this.test('returns true for float', () => {
237
+ this.assert(sc.isNumber(3.14));
238
+ });
239
+ this.test('returns false for string', () => {
240
+ this.assert(!sc.isNumber('42'));
241
+ });
242
+ }
243
+
244
+ testIsInt()
245
+ {
246
+ this.test('returns true for integer', () => {
247
+ this.assert(sc.isInt(5));
248
+ });
249
+ this.test('returns false for float', () => {
250
+ this.assert(!sc.isInt(5.5));
251
+ });
252
+ this.test('returns false for string', () => {
253
+ this.assert(!sc.isInt('5'));
254
+ });
255
+ }
256
+
257
+ testIsFloat()
258
+ {
259
+ this.test('returns true for float', () => {
260
+ this.assert(sc.isFloat(3.14));
261
+ });
262
+ this.test('returns false for integer', () => {
263
+ this.assert(!sc.isFloat(3));
264
+ });
265
+ this.test('returns false for string', () => {
266
+ this.assert(!sc.isFloat('3.14'));
267
+ });
268
+ }
269
+
270
+ testIsBoolean()
271
+ {
272
+ this.test('returns true for true', () => {
273
+ this.assert(sc.isBoolean(true));
274
+ });
275
+ this.test('returns true for false', () => {
276
+ this.assert(sc.isBoolean(false));
277
+ });
278
+ this.test('returns false for 1', () => {
279
+ this.assert(!sc.isBoolean(1));
280
+ });
281
+ }
282
+
283
+ testIsPromise()
284
+ {
285
+ this.test('returns true for promise', () => {
286
+ this.assert(sc.isPromise(Promise.resolve()));
287
+ });
288
+ this.test('returns true for thenable', () => {
289
+ this.assert(sc.isPromise({then: () => {}}));
290
+ });
291
+ this.test('returns false for non-promise', () => {
292
+ this.assert(!sc.isPromise(42));
293
+ });
294
+ }
295
+
296
+ testHasDangerousKeys()
297
+ {
298
+ this.test('detects __proto__ key', () => {
299
+ this.assert(sc.hasDangerousKeys(null, '__proto__'));
300
+ });
301
+ this.test('detects constructor key', () => {
302
+ this.assert(sc.hasDangerousKeys(null, 'constructor'));
303
+ });
304
+ this.test('detects prototype key', () => {
305
+ this.assert(sc.hasDangerousKeys(null, 'prototype'));
306
+ });
307
+ this.test('returns false for safe key', () => {
308
+ this.assert(!sc.hasDangerousKeys(null, 'safeKey'));
309
+ });
310
+ this.test('detects dangerous key in object', () => {
311
+ let obj = Object.create(null);
312
+ obj['__proto__'] = {};
313
+ this.assert(sc.hasDangerousKeys(obj));
314
+ });
315
+ this.test('returns false for safe object', () => {
316
+ this.assert(!sc.hasDangerousKeys({a: 1}));
317
+ });
318
+ }
319
+
320
+ testDeepMergeProperties()
321
+ {
322
+ this.test('merges flat objects', () => {
323
+ let target = {a: 1};
324
+ sc.deepMergeProperties(target, {b: 2});
325
+ this.assert(1 === target.a && 2 === target.b);
326
+ });
327
+ this.test('merges nested objects', () => {
328
+ let target = {a: {x: 1}};
329
+ sc.deepMergeProperties(target, {a: {y: 2}});
330
+ this.assert(1 === target.a.x && 2 === target.a.y);
331
+ });
332
+ this.test('overwrites scalar values', () => {
333
+ let target = {a: 1};
334
+ sc.deepMergeProperties(target, {a: 2});
335
+ this.assert(2 === target.a);
336
+ });
337
+ this.test('returns false for non-objects', () => {
338
+ this.assert(false === sc.deepMergeProperties(null, {a: 1}));
339
+ });
340
+ this.test('skips dangerous keys', () => {
341
+ let target = {a: 1};
342
+ let source = Object.create(null);
343
+ source['__proto__'] = {evil: true};
344
+ source['b'] = 2;
345
+ sc.deepMergeProperties(target, source);
346
+ this.assert(undefined === target['evil'] && 2 === target.b);
347
+ });
348
+ }
349
+
350
+ testLength()
351
+ {
352
+ this.test('returns key count', () => {
353
+ this.assert(2 === sc.length({a: 1, b: 2}));
354
+ });
355
+ this.test('returns 0 for empty object', () => {
356
+ this.assert(0 === sc.length({}));
357
+ });
358
+ this.test('returns 0 for null', () => {
359
+ this.assert(0 === sc.length(null));
360
+ });
361
+ }
362
+
363
+ testStartsWith()
364
+ {
365
+ this.test('returns true when string starts with prefix', () => {
366
+ this.assert(sc.startsWith('hello world', 'hello'));
367
+ });
368
+ this.test('returns false when it does not', () => {
369
+ this.assert(!sc.startsWith('hello world', 'world'));
370
+ });
371
+ this.test('returns false for non-string', () => {
372
+ this.assert(!sc.startsWith(42, '4'));
373
+ });
374
+ }
375
+
376
+ testContains()
377
+ {
378
+ this.test('returns true when needle is found', () => {
379
+ this.assert(sc.contains('hello world', 'world'));
380
+ });
381
+ this.test('returns false when not found', () => {
382
+ this.assert(!sc.contains('hello', 'xyz'));
383
+ });
384
+ this.test('returns false for non-string', () => {
385
+ this.assert(!sc.contains(42, '4'));
386
+ });
387
+ }
388
+
389
+ testConvertObjectsArrayToObjectByKeys()
390
+ {
391
+ this.test('converts array to keyed object', () => {
392
+ let result = sc.convertObjectsArrayToObjectByKeys([{id: 1, name: 'a'}, {id: 2, name: 'b'}], 'id');
393
+ this.assert(result[1].name === 'a' && result[2].name === 'b');
394
+ });
395
+ this.test('returns empty object for empty array', () => {
396
+ let result = sc.convertObjectsArrayToObjectByKeys([], 'id');
397
+ this.assert(0 === Object.keys(result).length);
398
+ });
399
+ this.test('returns empty object for non-array', () => {
400
+ let result = sc.convertObjectsArrayToObjectByKeys(null, 'id');
401
+ this.assert(0 === Object.keys(result).length);
402
+ });
403
+ }
404
+
405
+ testSortObjectKeysBy()
406
+ {
407
+ this.test('sorts keys by field ascending', () => {
408
+ let obj = {b: {order: 2}, a: {order: 1}};
409
+ let sorted = sc.sortObjectKeysBy(obj, 'order');
410
+ this.assert('a' === sorted[0] && 'b' === sorted[1]);
411
+ });
412
+ }
413
+
414
+ testArraySort()
415
+ {
416
+ this.test('sorts asc by field', () => {
417
+ let arr = [{n: 3}, {n: 1}, {n: 2}];
418
+ let sorted = sc.arraySort(arr, 'n');
419
+ this.assert(1 === sorted[0].n && 3 === sorted[2].n);
420
+ });
421
+ this.test('sorts desc by field', () => {
422
+ let arr = [{n: 1}, {n: 3}, {n: 2}];
423
+ let sorted = sc.arraySort(arr, 'n', 'desc');
424
+ this.assert(3 === sorted[0].n && 1 === sorted[2].n);
425
+ });
426
+ this.test('returns collection unchanged if no sortField', () => {
427
+ let arr = [3, 1, 2];
428
+ this.assert(arr === sc.arraySort(arr, null));
429
+ });
430
+ }
431
+
432
+ testPropsAssign()
433
+ {
434
+ this.test('assigns listed props from source to target', () => {
435
+ let from = {a: 1, b: 2, c: 3};
436
+ let to = {};
437
+ sc.propsAssign(from, to, ['a', 'c']);
438
+ this.assert(1 === to.a && 3 === to.c && undefined === to.b);
439
+ });
440
+ this.test('returns to unchanged for non-array props', () => {
441
+ let to = {};
442
+ sc.propsAssign({a: 1}, to, null);
443
+ this.assert(0 === Object.keys(to).length);
444
+ });
445
+ }
446
+
447
+ testToJson()
448
+ {
449
+ this.test('parses valid JSON string', () => {
450
+ let result = sc.toJson('{"a":1}');
451
+ this.assert(1 === result.a);
452
+ });
453
+ this.test('returns default for invalid JSON', () => {
454
+ this.assert(false === sc.toJson('invalid'));
455
+ });
456
+ this.test('returns custom default for invalid JSON', () => {
457
+ this.assert(null === sc.toJson('invalid', null));
458
+ });
459
+ }
460
+
461
+ testParseJson()
462
+ {
463
+ this.test('parses valid JSON', () => {
464
+ let result = sc.parseJson('{"x":2}');
465
+ this.assert(2 === result.x);
466
+ });
467
+ this.test('returns false for invalid JSON', () => {
468
+ this.assert(false === sc.parseJson('{bad}'));
469
+ });
470
+ this.test('returns false for JSON with dangerous keys', () => {
471
+ this.assert(false === sc.parseJson('{"__proto__":{"evil":true}}'));
472
+ });
473
+ }
474
+
475
+ testDeepJsonClone()
476
+ {
477
+ this.test('clones object deeply', () => {
478
+ let original = {a: {b: 1}};
479
+ let clone = sc.deepJsonClone(original);
480
+ clone.a.b = 99;
481
+ this.assert(1 === original.a.b);
482
+ });
483
+ }
484
+
485
+ testToJsonString()
486
+ {
487
+ this.test('stringifies object', () => {
488
+ this.assert('{"a":1}' === sc.toJsonString({a: 1}));
489
+ });
490
+ this.test('accepts replacer and space args', () => {
491
+ let result = sc.toJsonString({a: 1}, null, 2);
492
+ this.assert(-1 !== result.indexOf('\n'));
493
+ });
494
+ }
495
+
496
+ testGet()
497
+ {
498
+ this.test('returns value for existing prop', () => {
499
+ this.assert(42 === sc.get({x: 42}, 'x'));
500
+ });
501
+ this.test('returns defaultReturn for missing prop', () => {
502
+ this.assert('default' === sc.get({}, 'x', 'default'));
503
+ });
504
+ this.test('returns undefined default when not specified', () => {
505
+ this.assert(undefined === sc.get({}, 'x'));
506
+ });
507
+ }
508
+
509
+ testGetByPath()
510
+ {
511
+ this.test('gets nested value by path', () => {
512
+ this.assert(5 === sc.getByPath({a: {b: 5}}, ['a', 'b']));
513
+ });
514
+ this.test('returns default for missing path', () => {
515
+ this.assert('def' === sc.getByPath({a: {}}, ['a', 'b'], 'def'));
516
+ });
517
+ this.test('returns default for non-object', () => {
518
+ this.assert('def' === sc.getByPath(null, ['a'], 'def'));
519
+ });
520
+ this.test('returns default for non-array path', () => {
521
+ this.assert('def' === sc.getByPath({a: 1}, 'a', 'def'));
522
+ });
523
+ }
524
+
525
+ testGetByPriority()
526
+ {
527
+ this.test('returns first found prop value', () => {
528
+ this.assert(2 === sc.getByPriority({b: 2}, ['a', 'b']));
529
+ });
530
+ this.test('returns false if none found', () => {
531
+ this.assert(false === sc.getByPriority({}, ['a', 'b']));
532
+ });
533
+ this.test('returns false for non-array propsArray', () => {
534
+ this.assert(false === sc.getByPriority({a: 1}, null));
535
+ });
536
+ }
537
+
538
+ testFetchByProperty()
539
+ {
540
+ this.test('returns first matching item', () => {
541
+ let arr = [{id: 1}, {id: 2}];
542
+ this.assert(2 === sc.fetchByProperty(arr, 'id', 2).id);
543
+ });
544
+ this.test('returns false when not found', () => {
545
+ this.assert(false === sc.fetchByProperty([{id: 1}], 'id', 99));
546
+ });
547
+ this.test('returns false for non-array', () => {
548
+ this.assert(false === sc.fetchByProperty(null, 'id', 1));
549
+ });
550
+ this.test('returns false for empty array', () => {
551
+ this.assert(false === sc.fetchByProperty([], 'id', 1));
552
+ });
553
+ }
554
+
555
+ testFetchAllByProperty()
556
+ {
557
+ this.test('returns all matching items', () => {
558
+ let arr = [{t: 'a'}, {t: 'b'}, {t: 'a'}];
559
+ let result = sc.fetchAllByProperty(arr, 't', 'a');
560
+ this.assert(2 === result.length);
561
+ });
562
+ this.test('returns empty array when none match', () => {
563
+ this.assert(0 === sc.fetchAllByProperty([{t: 'a'}], 't', 'z').length);
564
+ });
565
+ this.test('returns empty array for non-array', () => {
566
+ this.assert(0 === sc.fetchAllByProperty(null, 't', 'a').length);
567
+ });
568
+ }
569
+
570
+ testFetchByPropertyOnObject()
571
+ {
572
+ this.test('returns matching object value', () => {
573
+ let obj = {x: {type: 'a'}, y: {type: 'b'}};
574
+ this.assert('a' === sc.fetchByPropertyOnObject(obj, 'type', 'a').type);
575
+ });
576
+ this.test('returns false when not found', () => {
577
+ this.assert(false === sc.fetchByPropertyOnObject({x: {type: 'a'}}, 'type', 'z'));
578
+ });
579
+ this.test('returns false for null', () => {
580
+ this.assert(false === sc.fetchByPropertyOnObject(null, 'type', 'a'));
581
+ });
582
+ }
583
+
584
+ testFetchAllByPropertyOnObject()
585
+ {
586
+ this.test('returns all matching values', () => {
587
+ let obj = {x: {t: 'a'}, y: {t: 'b'}, z: {t: 'a'}};
588
+ this.assert(2 === sc.fetchAllByPropertyOnObject(obj, 't', 'a').length);
589
+ });
590
+ this.test('returns empty array for null', () => {
591
+ this.assert(0 === sc.fetchAllByPropertyOnObject(null, 't', 'a').length);
592
+ });
593
+ }
594
+
595
+ testRemoveFromArray()
596
+ {
597
+ this.test('removes specified values', () => {
598
+ let result = sc.removeFromArray([1, 2, 3, 4], [2, 4]);
599
+ this.assert(2 === result.length && 1 === result[0] && 3 === result[1]);
600
+ });
601
+ this.test('returns original if none match', () => {
602
+ let result = sc.removeFromArray([1, 2], [5]);
603
+ this.assert(2 === result.length);
604
+ });
605
+ }
606
+
607
+ testGetCurrentDate()
608
+ {
609
+ this.test('returns date string in Y-m-d H:i:s format', () => {
610
+ let result = sc.getCurrentDate();
611
+ this.assert('string' === typeof result && 19 === result.length);
612
+ });
613
+ }
614
+
615
+ testGetDateForFileName()
616
+ {
617
+ this.test('returns date string safe for filenames', () => {
618
+ let result = sc.getDateForFileName();
619
+ this.assert('string' === typeof result && !result.includes(':'));
620
+ });
621
+ }
622
+
623
+ testFormatDate()
624
+ {
625
+ this.test('formats date with default format', () => {
626
+ let date = new Date(2024, 0, 15, 10, 5, 3);
627
+ let result = sc.formatDate(date);
628
+ this.assert('2024-01-15 10:05:03' === result);
629
+ });
630
+ this.test('returns input unchanged for non-Date', () => {
631
+ this.assert('not-a-date' === sc.formatDate('not-a-date'));
632
+ });
633
+ this.test('supports custom format', () => {
634
+ let date = new Date(2024, 5, 1, 0, 0, 0);
635
+ this.assert('2024' === sc.formatDate(date, 'Y'));
636
+ });
637
+ }
638
+
639
+ testGetTime()
640
+ {
641
+ this.test('returns a number', () => {
642
+ this.assert('number' === typeof sc.getTime());
643
+ });
644
+ this.test('returns current timestamp approx', () => {
645
+ let before = Date.now();
646
+ let t = sc.getTime();
647
+ let after = Date.now();
648
+ this.assert(t >= before && t <= after);
649
+ });
650
+ }
651
+
652
+ testRoundToPrecision()
653
+ {
654
+ this.test('rounds to 4 decimal places by default', () => {
655
+ this.assert(3.1416 === sc.roundToPrecision(Math.PI));
656
+ });
657
+ this.test('rounds to custom precision', () => {
658
+ this.assert(3.14 === sc.roundToPrecision(Math.PI, 2));
659
+ });
660
+ }
661
+
662
+ testRandomValueFromArray()
663
+ {
664
+ this.test('returns a value from the array', () => {
665
+ let arr = [1, 2, 3];
666
+ this.assert(sc.inArray(sc.randomValueFromArray(arr), arr));
667
+ });
668
+ this.test('returns null for empty array', () => {
669
+ this.assert(null === sc.randomValueFromArray([]));
670
+ });
671
+ this.test('returns null for non-array', () => {
672
+ this.assert(null === sc.randomValueFromArray(null));
673
+ });
674
+ }
675
+
676
+ testRandomInteger()
677
+ {
678
+ this.test('returns integer within range', () => {
679
+ let result = sc.randomInteger(1, 10);
680
+ this.assert(result >= 1 && result <= 10 && Number.isInteger(result));
681
+ });
682
+ }
683
+
684
+ testRandomChars()
685
+ {
686
+ this.test('returns string of given length', () => {
687
+ let result = sc.randomChars(8);
688
+ this.assert('string' === typeof result && 8 === result.length);
689
+ });
690
+ this.test('returns empty string for length <= 0', () => {
691
+ this.assert('' === sc.randomChars(0));
692
+ });
693
+ }
694
+
695
+ testRandomCharsWithSymbols()
696
+ {
697
+ this.test('returns string of given length', () => {
698
+ let result = sc.randomCharsWithSymbols(10);
699
+ this.assert('string' === typeof result && 10 === result.length);
700
+ });
701
+ this.test('returns empty string for length <= 0', () => {
702
+ this.assert('' === sc.randomCharsWithSymbols(0));
703
+ });
704
+ }
705
+
706
+ testCleanMessage()
707
+ {
708
+ this.test('removes backslashes', () => {
709
+ this.assert('helloworld' === sc.cleanMessage('hello\\world', 0));
710
+ });
711
+ this.test('replaces newlines with space', () => {
712
+ this.assert('hello world' === sc.cleanMessage('hello\nworld', 0));
713
+ });
714
+ this.test('truncates to characterLimit', () => {
715
+ this.assert('hel' === sc.cleanMessage('hello', 3));
716
+ });
717
+ this.test('returns empty string for falsy input', () => {
718
+ this.assert('' === sc.cleanMessage(null, 0));
719
+ });
720
+ }
721
+
722
+ testSlugify()
723
+ {
724
+ this.test('converts to lowercase slug', () => {
725
+ this.assert('hello-world' === sc.slugify('Hello World'));
726
+ });
727
+ this.test('replaces & with and', () => {
728
+ this.assert('bread-and-butter' === sc.slugify('Bread & Butter'));
729
+ });
730
+ this.test('removes special chars', () => {
731
+ this.assert('test-123' === sc.slugify('test!@#123'));
732
+ });
733
+ this.test('returns empty string for falsy input', () => {
734
+ this.assert('' === sc.slugify(''));
735
+ });
736
+ }
737
+
738
+ testIsValidIsoCode()
739
+ {
740
+ this.test('returns true for 2-letter code', () => {
741
+ this.assert(sc.isValidIsoCode('en'));
742
+ });
743
+ this.test('returns true for uppercase', () => {
744
+ this.assert(sc.isValidIsoCode('EN'));
745
+ });
746
+ this.test('returns false for 3-letter code', () => {
747
+ this.assert(!sc.isValidIsoCode('eng'));
748
+ });
749
+ this.test('returns false for numeric', () => {
750
+ this.assert(!sc.isValidIsoCode('12'));
751
+ });
752
+ }
753
+
754
+ testSanitize()
755
+ {
756
+ this.test('escapes &', () => {
757
+ this.assert(-1 !== sc.sanitize('a&b').indexOf('&amp;'));
758
+ });
759
+ this.test('escapes <', () => {
760
+ this.assert(-1 !== sc.sanitize('<script>').indexOf('&lt;'));
761
+ });
762
+ this.test('escapes >', () => {
763
+ this.assert(-1 !== sc.sanitize('<>').indexOf('&gt;'));
764
+ });
765
+ this.test('returns empty string for falsy input', () => {
766
+ this.assert('' === sc.sanitize(null));
767
+ });
768
+ }
769
+
770
+ testSanitizeUrl()
771
+ {
772
+ this.test('returns valid http URL unchanged', () => {
773
+ let url = 'http://example.com/path';
774
+ this.assert(url === sc.sanitizeUrl(url));
775
+ });
776
+ this.test('returns valid https URL unchanged', () => {
777
+ let url = 'https://example.com';
778
+ this.assert(url === sc.sanitizeUrl(url));
779
+ });
780
+ this.test('returns empty string for javascript: URL', () => {
781
+ this.assert('' === sc.sanitizeUrl('javascript:alert(1)'));
782
+ });
783
+ this.test('returns empty string for non-string', () => {
784
+ this.assert('' === sc.sanitizeUrl(null));
785
+ });
786
+ this.test('returns empty string for too-long URL', () => {
787
+ this.assert('' === sc.sanitizeUrl('https://example.com/' + 'a'.repeat(2050)));
788
+ });
789
+ }
790
+
791
+ testCamelCase()
792
+ {
793
+ this.test('converts snake_case to camelCase', () => {
794
+ this.assert('helloWorld' === sc.camelCase('hello_world'));
795
+ });
796
+ this.test('converts space-separated to camelCase', () => {
797
+ this.assert('helloWorld' === sc.camelCase('hello world'));
798
+ });
799
+ this.test('returns input unchanged for non-string', () => {
800
+ this.assert(42 === sc.camelCase(42));
801
+ });
802
+ }
803
+
804
+ testCapitalizedCamelCase()
805
+ {
806
+ this.test('converts to CapitalizedCamelCase', () => {
807
+ this.assert('HelloWorld' === sc.capitalizedCamelCase('hello_world'));
808
+ });
809
+ this.test('returns input unchanged for non-string', () => {
810
+ this.assert(42 === sc.capitalizedCamelCase(42));
811
+ });
812
+ }
813
+
814
+ testKebabCase()
815
+ {
816
+ this.test('converts underscores to hyphens', () => {
817
+ this.assert('hello-world' === sc.kebabCase('hello_world'));
818
+ });
819
+ this.test('converts spaces to hyphens', () => {
820
+ this.assert('hello-world' === sc.kebabCase('hello world'));
821
+ });
822
+ }
823
+
824
+ testCapitalize()
825
+ {
826
+ this.test('capitalizes first letter and lowercases rest', () => {
827
+ this.assert('Hello' === sc.capitalize('hELLO'));
828
+ });
829
+ this.test('returns input unchanged for non-string', () => {
830
+ this.assert(42 === sc.capitalize(42));
831
+ });
832
+ this.test('returns empty string unchanged', () => {
833
+ this.assert('' === sc.capitalize(''));
834
+ });
835
+ }
836
+
837
+ testChunk()
838
+ {
839
+ this.test('splits array into chunks', () => {
840
+ let result = sc.chunk([1, 2, 3, 4, 5], 2);
841
+ this.assert(3 === result.length && 2 === result[0].length && 1 === result[2].length);
842
+ });
843
+ this.test('returns empty array for non-array', () => {
844
+ this.assert(0 === sc.chunk(null, 2).length);
845
+ });
846
+ this.test('returns empty array for size <= 0', () => {
847
+ this.assert(0 === sc.chunk([1, 2], 0).length);
848
+ });
849
+ }
850
+
851
+ testFlatten()
852
+ {
853
+ this.test('flattens one level by default', () => {
854
+ let result = sc.flatten([[1, 2], [3, 4]]);
855
+ this.assert(4 === result.length && 1 === result[0]);
856
+ });
857
+ this.test('flattens to specified depth', () => {
858
+ let result = sc.flatten([[[1]], [[2]]], 2);
859
+ this.assert(2 === result.length);
860
+ });
861
+ this.test('returns empty array for non-array', () => {
862
+ this.assert(0 === sc.flatten(null).length);
863
+ });
864
+ }
865
+
866
+ testUnique()
867
+ {
868
+ this.test('removes duplicates', () => {
869
+ let result = sc.unique([1, 2, 2, 3, 1]);
870
+ this.assert(3 === result.length);
871
+ });
872
+ this.test('returns empty array for non-array', () => {
873
+ this.assert(0 === sc.unique(null).length);
874
+ });
875
+ }
876
+
877
+ testClamp()
878
+ {
879
+ this.test('clamps value within range', () => {
880
+ this.assert(5 === sc.clamp(10, 0, 5));
881
+ });
882
+ this.test('returns min when below', () => {
883
+ this.assert(0 === sc.clamp(-5, 0, 10));
884
+ });
885
+ this.test('returns value when within range', () => {
886
+ this.assert(5 === sc.clamp(5, 0, 10));
887
+ });
888
+ this.test('returns value unchanged for non-number', () => {
889
+ this.assert('x' === sc.clamp('x', 0, 10));
890
+ });
891
+ }
892
+
893
+ testTruncate()
894
+ {
895
+ this.test('truncates with default suffix', () => {
896
+ this.assert('hel...' === sc.truncate('hello world', 3));
897
+ });
898
+ this.test('returns original if within length', () => {
899
+ this.assert('hi' === sc.truncate('hi', 10));
900
+ });
901
+ this.test('uses custom suffix', () => {
902
+ this.assert('hel~' === sc.truncate('hello', 3, '~'));
903
+ });
904
+ this.test('returns input unchanged for non-string', () => {
905
+ this.assert(42 === sc.truncate(42, 3));
906
+ });
907
+ }
908
+
909
+ testPickProps()
910
+ {
911
+ this.test('picks specified props', () => {
912
+ let result = sc.pickProps({a: 1, b: 2, c: 3}, ['a', 'c']);
913
+ this.assert(1 === result.a && 3 === result.c && undefined === result.b);
914
+ });
915
+ this.test('returns empty object for non-object', () => {
916
+ this.assert(0 === Object.keys(sc.pickProps(null, ['a'])).length);
917
+ });
918
+ this.test('skips dangerous keys', () => {
919
+ let result = sc.pickProps({a: 1, constructor: 'hacked'}, ['constructor', 'a']);
920
+ this.assert('hacked' !== result.constructor && 1 === result.a);
921
+ });
922
+ }
923
+
924
+ testOmitProps()
925
+ {
926
+ this.test('omits specified props', () => {
927
+ let result = sc.omitProps({a: 1, b: 2, c: 3}, ['b']);
928
+ this.assert(undefined === result.b && 1 === result.a && 3 === result.c);
929
+ });
930
+ this.test('returns obj unchanged for non-array props', () => {
931
+ let obj = {a: 1};
932
+ this.assert(obj === sc.omitProps(obj, null));
933
+ });
934
+ }
935
+
936
+ testDebounce()
937
+ {
938
+ this.test('returns a function', () => {
939
+ this.assert('function' === typeof sc.debounce(() => {}, 100));
940
+ });
941
+ this.test('returns original func for invalid wait', () => {
942
+ let fn = () => {};
943
+ this.assert(fn === sc.debounce(fn, 'invalid'));
944
+ });
945
+ }
946
+
947
+ testThrottle()
948
+ {
949
+ this.test('returns a function', () => {
950
+ this.assert('function' === typeof sc.throttle(() => {}, 100));
951
+ });
952
+ this.test('returns original func for invalid limit', () => {
953
+ let fn = () => {};
954
+ this.assert(fn === sc.throttle(fn, 'invalid'));
955
+ });
956
+ }
957
+
958
+ testIsValidUrl()
959
+ {
960
+ this.test('returns true for valid http URL', () => {
961
+ this.assert(sc.isValidUrl('http://example.com'));
962
+ });
963
+ this.test('returns true for valid https URL', () => {
964
+ this.assert(sc.isValidUrl('https://example.com/path?q=1'));
965
+ });
966
+ this.test('returns false for non-URL string', () => {
967
+ this.assert(!sc.isValidUrl('not a url'));
968
+ });
969
+ this.test('returns false for non-string', () => {
970
+ this.assert(!sc.isValidUrl(42));
971
+ });
972
+ this.test('returns false for too-long string', () => {
973
+ this.assert(!sc.isValidUrl('https://x.com/' + 'a'.repeat(2050)));
974
+ });
975
+ }
976
+
977
+ testIsValidInteger()
978
+ {
979
+ this.test('returns true for integer', () => {
980
+ this.assert(sc.isValidInteger(5));
981
+ });
982
+ this.test('returns false for float', () => {
983
+ this.assert(!sc.isValidInteger(5.5));
984
+ });
985
+ this.test('validates min boundary', () => {
986
+ this.assert(!sc.isValidInteger(3, 5));
987
+ });
988
+ this.test('validates max boundary', () => {
989
+ this.assert(!sc.isValidInteger(15, 0, 10));
990
+ });
991
+ this.test('returns true within range', () => {
992
+ this.assert(sc.isValidInteger(5, 0, 10));
993
+ });
994
+ }
995
+
996
+ testParseNumber()
997
+ {
998
+ this.test('parses integer string', () => {
999
+ this.assert(42 === sc.parseNumber('42'));
1000
+ });
1001
+ this.test('parses float string', () => {
1002
+ this.assert(3.14 === sc.parseNumber('3.14'));
1003
+ });
1004
+ this.test('returns null for non-numeric', () => {
1005
+ this.assert(null === sc.parseNumber('abc'));
1006
+ });
1007
+ this.test('returns null for empty string', () => {
1008
+ this.assert(null === sc.parseNumber(''));
1009
+ });
1010
+ this.test('returns null for null', () => {
1011
+ this.assert(null === sc.parseNumber(null));
1012
+ });
1013
+ }
1014
+
1015
+ testSplitToArray()
1016
+ {
1017
+ this.test('splits comma-separated string', () => {
1018
+ let result = sc.splitToArray('a, b, c');
1019
+ this.assert(3 === result.length && 'a' === result[0]);
1020
+ });
1021
+ this.test('uses custom separator', () => {
1022
+ let result = sc.splitToArray('a|b|c', '|');
1023
+ this.assert(3 === result.length);
1024
+ });
1025
+ this.test('returns null for empty string', () => {
1026
+ this.assert(null === sc.splitToArray(''));
1027
+ });
1028
+ this.test('returns null for non-string', () => {
1029
+ this.assert(null === sc.splitToArray(null));
1030
+ });
1031
+ this.test('filters empty segments', () => {
1032
+ let result = sc.splitToArray('a,,b', ',');
1033
+ this.assert(2 === result.length);
1034
+ });
1035
+ }
1036
+
1037
+ testIsSecurePath()
1038
+ {
1039
+ this.test('returns true for normal path', () => {
1040
+ this.assert(sc.isSecurePath('uploads/file.txt'));
1041
+ });
1042
+ this.test('returns false for path traversal', () => {
1043
+ this.assert(!sc.isSecurePath('../etc/passwd'));
1044
+ });
1045
+ this.test('returns false for /etc/ path', () => {
1046
+ this.assert(!sc.isSecurePath('/etc/passwd'));
1047
+ });
1048
+ this.test('returns false for non-string', () => {
1049
+ this.assert(!sc.isSecurePath(null));
1050
+ });
1051
+ this.test('returns false for too-long path', () => {
1052
+ this.assert(!sc.isSecurePath('a'.repeat(2049)));
1053
+ });
1054
+ }
1055
+
1056
+ testValidateInput()
1057
+ {
1058
+ this.test('validates email', () => {
1059
+ this.assert(sc.validateInput('test@example.com', 'email'));
1060
+ });
1061
+ this.test('rejects invalid email', () => {
1062
+ this.assert(!sc.validateInput('not-an-email', 'email'));
1063
+ });
1064
+ this.test('validates username', () => {
1065
+ this.assert(sc.validateInput('user_123', 'username'));
1066
+ });
1067
+ this.test('validates alphanumeric', () => {
1068
+ this.assert(sc.validateInput('abc123', 'alphanumeric'));
1069
+ });
1070
+ this.test('validates numeric', () => {
1071
+ this.assert(sc.validateInput('12345', 'numeric'));
1072
+ });
1073
+ this.test('validates hexColor', () => {
1074
+ this.assert(sc.validateInput('#FF5733', 'hexColor'));
1075
+ });
1076
+ this.test('validates ipv4', () => {
1077
+ this.assert(sc.validateInput('192.168.1.1', 'ipv4'));
1078
+ });
1079
+ this.test('returns false for unknown type', () => {
1080
+ this.assert(!sc.validateInput('anything', 'unknownType'));
1081
+ });
1082
+ this.test('returns false for non-string input', () => {
1083
+ this.assert(!sc.validateInput(42, 'numeric'));
1084
+ });
1085
+ }
1086
+
1087
+ testSerializeFormData()
1088
+ {
1089
+ this.test('serializes single values', () => {
1090
+ let formData = [['name', 'Alice'], ['age', '30']];
1091
+ let result = sc.serializeFormData(formData);
1092
+ this.assert('Alice' === result.name && '30' === result.age);
1093
+ });
1094
+ this.test('serializes repeated keys into array', () => {
1095
+ let formData = [['tag', 'a'], ['tag', 'b']];
1096
+ let result = sc.serializeFormData(formData);
1097
+ this.assert(sc.isArray(result.tag) && 2 === result.tag.length);
1098
+ });
1099
+ this.test('returns empty object for empty formData', () => {
1100
+ let result = sc.serializeFormData([]);
1101
+ this.assert(0 === Object.keys(result).length);
1102
+ });
1103
+ }
1104
+
1105
+ }
1106
+
1107
+ module.exports = { TestShortcuts };