@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 +11 -1
- package/lib/shortcuts.js +2 -2
- package/package.json +1 -1
- package/tests/events-manager-test.js +16 -19
- package/tests/run.js +15 -2
- package/tests/shortcuts-test.js +1107 -0
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
|
@@ -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
|
-
|
|
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.
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
1088
|
-
console.log('
|
|
1089
|
-
console.log('
|
|
1090
|
-
console.log('
|
|
1091
|
-
console.log('Passed:'
|
|
1092
|
-
|
|
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('
|
|
1099
|
+
console.log(' ✗ '+result.name+': '+result.error);
|
|
1099
1100
|
}
|
|
1100
1101
|
}
|
|
1101
1102
|
}
|
|
1102
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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('&'));
|
|
758
|
+
});
|
|
759
|
+
this.test('escapes <', () => {
|
|
760
|
+
this.assert(-1 !== sc.sanitize('<script>').indexOf('<'));
|
|
761
|
+
});
|
|
762
|
+
this.test('escapes >', () => {
|
|
763
|
+
this.assert(-1 !== sc.sanitize('<>').indexOf('>'));
|
|
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 };
|