@reldens/utils
Version:
1,108 lines (1,028 loc) • 38.7 kB
JavaScript
/**
*
* Reldens - TestShortcuts
*
*/
const sc = require('../lib/shortcuts');
class TestShortcuts
{
constructor()
{
this.testResults = [];
this.testCount = 0;
this.passedCount = 0;
this.currentTestMethod = '';
}
test(name, testFn)
{
this.testCount++;
try{
testFn();
let logMessage = '✓ PASS: '+this.currentTestMethod+' - '+name;
console.log(logMessage);
this.passedCount++;
this.testResults.push({name, status: 'PASS'});
} catch(error){
let logMessage = '✗ FAIL: '+this.currentTestMethod+' - '+name+' - '+error.message;
console.log(logMessage);
this.testResults.push({name, status: 'FAIL', error: error.message});
}
}
assert(condition, message)
{
if(!condition){
throw new Error(message || 'Assertion failed');
}
}
async runAllTests()
{
console.log('Running tests for Shortcuts...\n');
let methodNames = Object.getOwnPropertyNames(Object.getPrototypeOf(this));
let testMethods = methodNames.filter(name =>
name.startsWith('test') &&
'function' === typeof this[name] &&
name !== 'test'
);
for(let methodName of testMethods){
this.currentTestMethod = methodName;
try{
await this[methodName]();
} catch(error){
console.error('Test method failed:', methodName, error.message);
}
}
let failed = this.printSummary();
return {total: this.testCount, passed: this.passedCount, failed};
}
printSummary()
{
console.log('\n'+'='.repeat(60));
console.log('SHORTCUTS TEST SUMMARY');
console.log('='.repeat(60));
let failedTests = this.testResults.filter(r => 'FAIL' === r.status);
console.log('Total: '+this.testCount+' | Passed: '+this.passedCount+' | Failed: '+failedTests.length);
if(0 < failedTests.length){
console.log('\nFailed tests:');
for(let failed of failedTests){
console.log(' ✗ '+failed.name+': '+failed.error);
}
}
return failedTests.length;
}
testHasOwn()
{
this.test('returns true for existing prop', () => {
this.assert(sc.hasOwn({a: 1}, 'a'));
});
this.test('returns false for missing prop', () => {
this.assert(!sc.hasOwn({a: 1}, 'b'));
});
this.test('returns false for undefined value', () => {
this.assert(!sc.hasOwn({a: undefined}, 'a'));
});
this.test('accepts array of props - all present', () => {
this.assert(sc.hasOwn({a: 1, b: 2}, ['a', 'b']));
});
this.test('accepts array of props - one missing', () => {
this.assert(!sc.hasOwn({a: 1}, ['a', 'b']));
});
this.test('returns false for null obj', () => {
this.assert(!sc.hasOwn(null, 'a'));
});
}
testIsTrue()
{
this.test('returns true for truthy prop', () => {
this.assert(sc.isTrue({a: 1}, 'a'));
});
this.test('returns false for falsy prop', () => {
this.assert(!sc.isTrue({a: 0}, 'a'));
});
this.test('strict mode - true for boolean true', () => {
this.assert(sc.isTrue({a: true}, 'a', true));
});
this.test('strict mode - false for truthy non-boolean', () => {
this.assert(!sc.isTrue({a: 1}, 'a', true));
});
this.test('returns false for missing prop', () => {
this.assert(!sc.isTrue({}, 'a'));
});
}
testIsObject()
{
this.test('returns true for plain object', () => {
this.assert(sc.isObject({a: 1}));
});
this.test('returns false for array', () => {
this.assert(!sc.isObject([1, 2]));
});
this.test('returns false for null', () => {
this.assert(!sc.isObject(null));
});
this.test('returns false for string', () => {
this.assert(!sc.isObject('str'));
});
this.test('returns false for number', () => {
this.assert(!sc.isObject(42));
});
}
testIsArray()
{
this.test('returns true for array', () => {
this.assert(sc.isArray([1, 2, 3]));
});
this.test('returns false for object', () => {
this.assert(!sc.isArray({a: 1}));
});
this.test('returns true for empty array', () => {
this.assert(sc.isArray([]));
});
this.test('returns false for string', () => {
this.assert(!sc.isArray('abc'));
});
}
testInArray()
{
this.test('returns true when value present', () => {
this.assert(sc.inArray(2, [1, 2, 3]));
});
this.test('returns false when value absent', () => {
this.assert(!sc.inArray(5, [1, 2, 3]));
});
this.test('returns false for non-array', () => {
this.assert(!sc.inArray(1, 'not an array'));
});
}
testIsNotEmptyArray()
{
this.test('returns true for non-empty array', () => {
this.assert(sc.isNotEmptyArray([1]));
});
this.test('returns false for empty array', () => {
this.assert(!sc.isNotEmptyArray([]));
});
this.test('returns false for non-array', () => {
this.assert(!sc.isNotEmptyArray('abc'));
});
}
testIsFunction()
{
this.test('returns true for function', () => {
this.assert(sc.isFunction(() => {}));
});
this.test('returns false for non-function', () => {
this.assert(!sc.isFunction(42));
});
this.test('returns false for null', () => {
this.assert(!sc.isFunction(null));
});
}
testIsObjectFunction()
{
this.test('returns true when property is a function', () => {
this.assert(sc.isObjectFunction({fn: () => {}}, 'fn'));
});
this.test('returns false when property is not a function', () => {
this.assert(!sc.isObjectFunction({a: 1}, 'a'));
});
this.test('returns false for non-object', () => {
this.assert(!sc.isObjectFunction(null, 'fn'));
});
}
testIsSymbol()
{
this.test('returns true for symbol', () => {
this.assert(sc.isSymbol(Symbol('x')));
});
this.test('returns false for string', () => {
this.assert(!sc.isSymbol('x'));
});
}
testIsString()
{
this.test('returns true for string', () => {
this.assert(sc.isString('hello'));
});
this.test('returns false for number', () => {
this.assert(!sc.isString(42));
});
this.test('returns true for empty string', () => {
this.assert(sc.isString(''));
});
}
testIsNumber()
{
this.test('returns true for number', () => {
this.assert(sc.isNumber(42));
});
this.test('returns true for float', () => {
this.assert(sc.isNumber(3.14));
});
this.test('returns false for string', () => {
this.assert(!sc.isNumber('42'));
});
}
testIsInt()
{
this.test('returns true for integer', () => {
this.assert(sc.isInt(5));
});
this.test('returns false for float', () => {
this.assert(!sc.isInt(5.5));
});
this.test('returns false for string', () => {
this.assert(!sc.isInt('5'));
});
}
testIsFloat()
{
this.test('returns true for float', () => {
this.assert(sc.isFloat(3.14));
});
this.test('returns false for integer', () => {
this.assert(!sc.isFloat(3));
});
this.test('returns false for string', () => {
this.assert(!sc.isFloat('3.14'));
});
}
testIsBoolean()
{
this.test('returns true for true', () => {
this.assert(sc.isBoolean(true));
});
this.test('returns true for false', () => {
this.assert(sc.isBoolean(false));
});
this.test('returns false for 1', () => {
this.assert(!sc.isBoolean(1));
});
}
testIsPromise()
{
this.test('returns true for promise', () => {
this.assert(sc.isPromise(Promise.resolve()));
});
this.test('returns true for thenable', () => {
this.assert(sc.isPromise({then: () => {}}));
});
this.test('returns false for non-promise', () => {
this.assert(!sc.isPromise(42));
});
}
testHasDangerousKeys()
{
this.test('detects __proto__ key', () => {
this.assert(sc.hasDangerousKeys(null, '__proto__'));
});
this.test('detects constructor key', () => {
this.assert(sc.hasDangerousKeys(null, 'constructor'));
});
this.test('detects prototype key', () => {
this.assert(sc.hasDangerousKeys(null, 'prototype'));
});
this.test('returns false for safe key', () => {
this.assert(!sc.hasDangerousKeys(null, 'safeKey'));
});
this.test('detects dangerous key in object', () => {
let obj = Object.create(null);
obj['__proto__'] = {};
this.assert(sc.hasDangerousKeys(obj));
});
this.test('returns false for safe object', () => {
this.assert(!sc.hasDangerousKeys({a: 1}));
});
}
testDeepMergeProperties()
{
this.test('merges flat objects', () => {
let target = {a: 1};
sc.deepMergeProperties(target, {b: 2});
this.assert(1 === target.a && 2 === target.b);
});
this.test('merges nested objects', () => {
let target = {a: {x: 1}};
sc.deepMergeProperties(target, {a: {y: 2}});
this.assert(1 === target.a.x && 2 === target.a.y);
});
this.test('overwrites scalar values', () => {
let target = {a: 1};
sc.deepMergeProperties(target, {a: 2});
this.assert(2 === target.a);
});
this.test('returns false for non-objects', () => {
this.assert(false === sc.deepMergeProperties(null, {a: 1}));
});
this.test('skips dangerous keys', () => {
let target = {a: 1};
let source = Object.create(null);
source['__proto__'] = {evil: true};
source['b'] = 2;
sc.deepMergeProperties(target, source);
this.assert(undefined === target['evil'] && 2 === target.b);
});
}
testLength()
{
this.test('returns key count', () => {
this.assert(2 === sc.length({a: 1, b: 2}));
});
this.test('returns 0 for empty object', () => {
this.assert(0 === sc.length({}));
});
this.test('returns 0 for null', () => {
this.assert(0 === sc.length(null));
});
}
testStartsWith()
{
this.test('returns true when string starts with prefix', () => {
this.assert(sc.startsWith('hello world', 'hello'));
});
this.test('returns false when it does not', () => {
this.assert(!sc.startsWith('hello world', 'world'));
});
this.test('returns false for non-string', () => {
this.assert(!sc.startsWith(42, '4'));
});
}
testContains()
{
this.test('returns true when needle is found', () => {
this.assert(sc.contains('hello world', 'world'));
});
this.test('returns false when not found', () => {
this.assert(!sc.contains('hello', 'xyz'));
});
this.test('returns false for non-string', () => {
this.assert(!sc.contains(42, '4'));
});
}
testConvertObjectsArrayToObjectByKeys()
{
this.test('converts array to keyed object', () => {
let result = sc.convertObjectsArrayToObjectByKeys([{id: 1, name: 'a'}, {id: 2, name: 'b'}], 'id');
this.assert(result[1].name === 'a' && result[2].name === 'b');
});
this.test('returns empty object for empty array', () => {
let result = sc.convertObjectsArrayToObjectByKeys([], 'id');
this.assert(0 === Object.keys(result).length);
});
this.test('returns empty object for non-array', () => {
let result = sc.convertObjectsArrayToObjectByKeys(null, 'id');
this.assert(0 === Object.keys(result).length);
});
}
testSortObjectKeysBy()
{
this.test('sorts keys by field ascending', () => {
let obj = {b: {order: 2}, a: {order: 1}};
let sorted = sc.sortObjectKeysBy(obj, 'order');
this.assert('a' === sorted[0] && 'b' === sorted[1]);
});
}
testArraySort()
{
this.test('sorts asc by field', () => {
let arr = [{n: 3}, {n: 1}, {n: 2}];
let sorted = sc.arraySort(arr, 'n');
this.assert(1 === sorted[0].n && 3 === sorted[2].n);
});
this.test('sorts desc by field', () => {
let arr = [{n: 1}, {n: 3}, {n: 2}];
let sorted = sc.arraySort(arr, 'n', 'desc');
this.assert(3 === sorted[0].n && 1 === sorted[2].n);
});
this.test('returns collection unchanged if no sortField', () => {
let arr = [3, 1, 2];
this.assert(arr === sc.arraySort(arr, null));
});
}
testPropsAssign()
{
this.test('assigns listed props from source to target', () => {
let from = {a: 1, b: 2, c: 3};
let to = {};
sc.propsAssign(from, to, ['a', 'c']);
this.assert(1 === to.a && 3 === to.c && undefined === to.b);
});
this.test('returns to unchanged for non-array props', () => {
let to = {};
sc.propsAssign({a: 1}, to, null);
this.assert(0 === Object.keys(to).length);
});
}
testToJson()
{
this.test('parses valid JSON string', () => {
let result = sc.toJson('{"a":1}');
this.assert(1 === result.a);
});
this.test('returns default for invalid JSON', () => {
this.assert(false === sc.toJson('invalid'));
});
this.test('returns custom default for invalid JSON', () => {
this.assert(null === sc.toJson('invalid', null));
});
}
testParseJson()
{
this.test('parses valid JSON', () => {
let result = sc.parseJson('{"x":2}');
this.assert(2 === result.x);
});
this.test('returns false for invalid JSON', () => {
this.assert(false === sc.parseJson('{bad}'));
});
this.test('returns false for JSON with dangerous keys', () => {
this.assert(false === sc.parseJson('{"__proto__":{"evil":true}}'));
});
}
testDeepJsonClone()
{
this.test('clones object deeply', () => {
let original = {a: {b: 1}};
let clone = sc.deepJsonClone(original);
clone.a.b = 99;
this.assert(1 === original.a.b);
});
}
testToJsonString()
{
this.test('stringifies object', () => {
this.assert('{"a":1}' === sc.toJsonString({a: 1}));
});
this.test('accepts replacer and space args', () => {
let result = sc.toJsonString({a: 1}, null, 2);
this.assert(-1 !== result.indexOf('\n'));
});
}
testGet()
{
this.test('returns value for existing prop', () => {
this.assert(42 === sc.get({x: 42}, 'x'));
});
this.test('returns defaultReturn for missing prop', () => {
this.assert('default' === sc.get({}, 'x', 'default'));
});
this.test('returns undefined default when not specified', () => {
this.assert(undefined === sc.get({}, 'x'));
});
}
testGetByPath()
{
this.test('gets nested value by path', () => {
this.assert(5 === sc.getByPath({a: {b: 5}}, ['a', 'b']));
});
this.test('returns default for missing path', () => {
this.assert('def' === sc.getByPath({a: {}}, ['a', 'b'], 'def'));
});
this.test('returns default for non-object', () => {
this.assert('def' === sc.getByPath(null, ['a'], 'def'));
});
this.test('returns default for non-array path', () => {
this.assert('def' === sc.getByPath({a: 1}, 'a', 'def'));
});
}
testGetByPriority()
{
this.test('returns first found prop value', () => {
this.assert(2 === sc.getByPriority({b: 2}, ['a', 'b']));
});
this.test('returns false if none found', () => {
this.assert(false === sc.getByPriority({}, ['a', 'b']));
});
this.test('returns false for non-array propsArray', () => {
this.assert(false === sc.getByPriority({a: 1}, null));
});
}
testFetchByProperty()
{
this.test('returns first matching item', () => {
let arr = [{id: 1}, {id: 2}];
this.assert(2 === sc.fetchByProperty(arr, 'id', 2).id);
});
this.test('returns false when not found', () => {
this.assert(false === sc.fetchByProperty([{id: 1}], 'id', 99));
});
this.test('returns false for non-array', () => {
this.assert(false === sc.fetchByProperty(null, 'id', 1));
});
this.test('returns false for empty array', () => {
this.assert(false === sc.fetchByProperty([], 'id', 1));
});
}
testFetchAllByProperty()
{
this.test('returns all matching items', () => {
let arr = [{t: 'a'}, {t: 'b'}, {t: 'a'}];
let result = sc.fetchAllByProperty(arr, 't', 'a');
this.assert(2 === result.length);
});
this.test('returns empty array when none match', () => {
this.assert(0 === sc.fetchAllByProperty([{t: 'a'}], 't', 'z').length);
});
this.test('returns empty array for non-array', () => {
this.assert(0 === sc.fetchAllByProperty(null, 't', 'a').length);
});
}
testFetchByPropertyOnObject()
{
this.test('returns matching object value', () => {
let obj = {x: {type: 'a'}, y: {type: 'b'}};
this.assert('a' === sc.fetchByPropertyOnObject(obj, 'type', 'a').type);
});
this.test('returns false when not found', () => {
this.assert(false === sc.fetchByPropertyOnObject({x: {type: 'a'}}, 'type', 'z'));
});
this.test('returns false for null', () => {
this.assert(false === sc.fetchByPropertyOnObject(null, 'type', 'a'));
});
}
testFetchAllByPropertyOnObject()
{
this.test('returns all matching values', () => {
let obj = {x: {t: 'a'}, y: {t: 'b'}, z: {t: 'a'}};
this.assert(2 === sc.fetchAllByPropertyOnObject(obj, 't', 'a').length);
});
this.test('returns empty array for null', () => {
this.assert(0 === sc.fetchAllByPropertyOnObject(null, 't', 'a').length);
});
}
testRemoveFromArray()
{
this.test('removes specified values', () => {
let result = sc.removeFromArray([1, 2, 3, 4], [2, 4]);
this.assert(2 === result.length && 1 === result[0] && 3 === result[1]);
});
this.test('returns original if none match', () => {
let result = sc.removeFromArray([1, 2], [5]);
this.assert(2 === result.length);
});
}
testGetCurrentDate()
{
this.test('returns date string in Y-m-d H:i:s format', () => {
let result = sc.getCurrentDate();
this.assert('string' === typeof result && 19 === result.length);
});
}
testGetDateForFileName()
{
this.test('returns date string safe for filenames', () => {
let result = sc.getDateForFileName();
this.assert('string' === typeof result && !result.includes(':'));
});
}
testFormatDate()
{
this.test('formats date with default format', () => {
let date = new Date(2024, 0, 15, 10, 5, 3);
let result = sc.formatDate(date);
this.assert('2024-01-15 10:05:03' === result);
});
this.test('returns input unchanged for non-Date', () => {
this.assert('not-a-date' === sc.formatDate('not-a-date'));
});
this.test('supports custom format', () => {
let date = new Date(2024, 5, 1, 0, 0, 0);
this.assert('2024' === sc.formatDate(date, 'Y'));
});
}
testGetTime()
{
this.test('returns a number', () => {
this.assert('number' === typeof sc.getTime());
});
this.test('returns current timestamp approx', () => {
let before = Date.now();
let t = sc.getTime();
let after = Date.now();
this.assert(t >= before && t <= after);
});
}
testRoundToPrecision()
{
this.test('rounds to 4 decimal places by default', () => {
this.assert(3.1416 === sc.roundToPrecision(Math.PI));
});
this.test('rounds to custom precision', () => {
this.assert(3.14 === sc.roundToPrecision(Math.PI, 2));
});
}
testRandomValueFromArray()
{
this.test('returns a value from the array', () => {
let arr = [1, 2, 3];
this.assert(sc.inArray(sc.randomValueFromArray(arr), arr));
});
this.test('returns null for empty array', () => {
this.assert(null === sc.randomValueFromArray([]));
});
this.test('returns null for non-array', () => {
this.assert(null === sc.randomValueFromArray(null));
});
}
testRandomInteger()
{
this.test('returns integer within range', () => {
let result = sc.randomInteger(1, 10);
this.assert(result >= 1 && result <= 10 && Number.isInteger(result));
});
}
testRandomChars()
{
this.test('returns string of given length', () => {
let result = sc.randomChars(8);
this.assert('string' === typeof result && 8 === result.length);
});
this.test('returns empty string for length <= 0', () => {
this.assert('' === sc.randomChars(0));
});
}
testRandomCharsWithSymbols()
{
this.test('returns string of given length', () => {
let result = sc.randomCharsWithSymbols(10);
this.assert('string' === typeof result && 10 === result.length);
});
this.test('returns empty string for length <= 0', () => {
this.assert('' === sc.randomCharsWithSymbols(0));
});
}
testCleanMessage()
{
this.test('removes backslashes', () => {
this.assert('helloworld' === sc.cleanMessage('hello\\world', 0));
});
this.test('replaces newlines with space', () => {
this.assert('hello world' === sc.cleanMessage('hello\nworld', 0));
});
this.test('truncates to characterLimit', () => {
this.assert('hel' === sc.cleanMessage('hello', 3));
});
this.test('returns empty string for falsy input', () => {
this.assert('' === sc.cleanMessage(null, 0));
});
}
testSlugify()
{
this.test('converts to lowercase slug', () => {
this.assert('hello-world' === sc.slugify('Hello World'));
});
this.test('replaces & with and', () => {
this.assert('bread-and-butter' === sc.slugify('Bread & Butter'));
});
this.test('removes special chars', () => {
this.assert('test-123' === sc.slugify('test!@#123'));
});
this.test('returns empty string for falsy input', () => {
this.assert('' === sc.slugify(''));
});
}
testIsValidIsoCode()
{
this.test('returns true for 2-letter code', () => {
this.assert(sc.isValidIsoCode('en'));
});
this.test('returns true for uppercase', () => {
this.assert(sc.isValidIsoCode('EN'));
});
this.test('returns false for 3-letter code', () => {
this.assert(!sc.isValidIsoCode('eng'));
});
this.test('returns false for numeric', () => {
this.assert(!sc.isValidIsoCode('12'));
});
}
testSanitize()
{
this.test('escapes &', () => {
this.assert(-1 !== sc.sanitize('a&b').indexOf('&'));
});
this.test('escapes <', () => {
this.assert(-1 !== sc.sanitize('<script>').indexOf('<'));
});
this.test('escapes >', () => {
this.assert(-1 !== sc.sanitize('<>').indexOf('>'));
});
this.test('returns empty string for falsy input', () => {
this.assert('' === sc.sanitize(null));
});
}
testSanitizeUrl()
{
this.test('returns valid http URL unchanged', () => {
let url = 'http://example.com/path';
this.assert(url === sc.sanitizeUrl(url));
});
this.test('returns valid https URL unchanged', () => {
let url = 'https://example.com';
this.assert(url === sc.sanitizeUrl(url));
});
this.test('returns empty string for javascript: URL', () => {
this.assert('' === sc.sanitizeUrl('javascript:alert(1)'));
});
this.test('returns empty string for non-string', () => {
this.assert('' === sc.sanitizeUrl(null));
});
this.test('returns empty string for too-long URL', () => {
this.assert('' === sc.sanitizeUrl('https://example.com/' + 'a'.repeat(2050)));
});
}
testCamelCase()
{
this.test('converts snake_case to camelCase', () => {
this.assert('helloWorld' === sc.camelCase('hello_world'));
});
this.test('converts space-separated to camelCase', () => {
this.assert('helloWorld' === sc.camelCase('hello world'));
});
this.test('returns input unchanged for non-string', () => {
this.assert(42 === sc.camelCase(42));
});
}
testCapitalizedCamelCase()
{
this.test('converts to CapitalizedCamelCase', () => {
this.assert('HelloWorld' === sc.capitalizedCamelCase('hello_world'));
});
this.test('returns input unchanged for non-string', () => {
this.assert(42 === sc.capitalizedCamelCase(42));
});
}
testKebabCase()
{
this.test('converts underscores to hyphens', () => {
this.assert('hello-world' === sc.kebabCase('hello_world'));
});
this.test('converts spaces to hyphens', () => {
this.assert('hello-world' === sc.kebabCase('hello world'));
});
}
testCapitalize()
{
this.test('capitalizes first letter and lowercases rest', () => {
this.assert('Hello' === sc.capitalize('hELLO'));
});
this.test('returns input unchanged for non-string', () => {
this.assert(42 === sc.capitalize(42));
});
this.test('returns empty string unchanged', () => {
this.assert('' === sc.capitalize(''));
});
}
testChunk()
{
this.test('splits array into chunks', () => {
let result = sc.chunk([1, 2, 3, 4, 5], 2);
this.assert(3 === result.length && 2 === result[0].length && 1 === result[2].length);
});
this.test('returns empty array for non-array', () => {
this.assert(0 === sc.chunk(null, 2).length);
});
this.test('returns empty array for size <= 0', () => {
this.assert(0 === sc.chunk([1, 2], 0).length);
});
}
testFlatten()
{
this.test('flattens one level by default', () => {
let result = sc.flatten([[1, 2], [3, 4]]);
this.assert(4 === result.length && 1 === result[0]);
});
this.test('flattens to specified depth', () => {
let result = sc.flatten([[[1]], [[2]]], 2);
this.assert(2 === result.length);
});
this.test('returns empty array for non-array', () => {
this.assert(0 === sc.flatten(null).length);
});
}
testUnique()
{
this.test('removes duplicates', () => {
let result = sc.unique([1, 2, 2, 3, 1]);
this.assert(3 === result.length);
});
this.test('returns empty array for non-array', () => {
this.assert(0 === sc.unique(null).length);
});
}
testClamp()
{
this.test('clamps value within range', () => {
this.assert(5 === sc.clamp(10, 0, 5));
});
this.test('returns min when below', () => {
this.assert(0 === sc.clamp(-5, 0, 10));
});
this.test('returns value when within range', () => {
this.assert(5 === sc.clamp(5, 0, 10));
});
this.test('returns value unchanged for non-number', () => {
this.assert('x' === sc.clamp('x', 0, 10));
});
}
testTruncate()
{
this.test('truncates with default suffix', () => {
this.assert('hel...' === sc.truncate('hello world', 3));
});
this.test('returns original if within length', () => {
this.assert('hi' === sc.truncate('hi', 10));
});
this.test('uses custom suffix', () => {
this.assert('hel~' === sc.truncate('hello', 3, '~'));
});
this.test('returns input unchanged for non-string', () => {
this.assert(42 === sc.truncate(42, 3));
});
}
testPickProps()
{
this.test('picks specified props', () => {
let result = sc.pickProps({a: 1, b: 2, c: 3}, ['a', 'c']);
this.assert(1 === result.a && 3 === result.c && undefined === result.b);
});
this.test('returns empty object for non-object', () => {
this.assert(0 === Object.keys(sc.pickProps(null, ['a'])).length);
});
this.test('skips dangerous keys', () => {
let result = sc.pickProps({a: 1, constructor: 'hacked'}, ['constructor', 'a']);
this.assert('hacked' !== result.constructor && 1 === result.a);
});
}
testOmitProps()
{
this.test('omits specified props', () => {
let result = sc.omitProps({a: 1, b: 2, c: 3}, ['b']);
this.assert(undefined === result.b && 1 === result.a && 3 === result.c);
});
this.test('returns obj unchanged for non-array props', () => {
let obj = {a: 1};
this.assert(obj === sc.omitProps(obj, null));
});
}
testDebounce()
{
this.test('returns a function', () => {
this.assert('function' === typeof sc.debounce(() => {}, 100));
});
this.test('returns original func for invalid wait', () => {
let fn = () => {};
this.assert(fn === sc.debounce(fn, 'invalid'));
});
}
testThrottle()
{
this.test('returns a function', () => {
this.assert('function' === typeof sc.throttle(() => {}, 100));
});
this.test('returns original func for invalid limit', () => {
let fn = () => {};
this.assert(fn === sc.throttle(fn, 'invalid'));
});
}
testIsValidUrl()
{
this.test('returns true for valid http URL', () => {
this.assert(sc.isValidUrl('http://example.com'));
});
this.test('returns true for valid https URL', () => {
this.assert(sc.isValidUrl('https://example.com/path?q=1'));
});
this.test('returns false for non-URL string', () => {
this.assert(!sc.isValidUrl('not a url'));
});
this.test('returns false for non-string', () => {
this.assert(!sc.isValidUrl(42));
});
this.test('returns false for too-long string', () => {
this.assert(!sc.isValidUrl('https://x.com/' + 'a'.repeat(2050)));
});
}
testIsValidInteger()
{
this.test('returns true for integer', () => {
this.assert(sc.isValidInteger(5));
});
this.test('returns false for float', () => {
this.assert(!sc.isValidInteger(5.5));
});
this.test('validates min boundary', () => {
this.assert(!sc.isValidInteger(3, 5));
});
this.test('validates max boundary', () => {
this.assert(!sc.isValidInteger(15, 0, 10));
});
this.test('returns true within range', () => {
this.assert(sc.isValidInteger(5, 0, 10));
});
}
testParseNumber()
{
this.test('parses integer string', () => {
this.assert(42 === sc.parseNumber('42'));
});
this.test('parses float string', () => {
this.assert(3.14 === sc.parseNumber('3.14'));
});
this.test('returns null for non-numeric', () => {
this.assert(null === sc.parseNumber('abc'));
});
this.test('returns null for empty string', () => {
this.assert(null === sc.parseNumber(''));
});
this.test('returns null for null', () => {
this.assert(null === sc.parseNumber(null));
});
}
testSplitToArray()
{
this.test('splits comma-separated string', () => {
let result = sc.splitToArray('a, b, c');
this.assert(3 === result.length && 'a' === result[0]);
});
this.test('uses custom separator', () => {
let result = sc.splitToArray('a|b|c', '|');
this.assert(3 === result.length);
});
this.test('returns null for empty string', () => {
this.assert(null === sc.splitToArray(''));
});
this.test('returns null for non-string', () => {
this.assert(null === sc.splitToArray(null));
});
this.test('filters empty segments', () => {
let result = sc.splitToArray('a,,b', ',');
this.assert(2 === result.length);
});
}
testIsSecurePath()
{
this.test('returns true for normal path', () => {
this.assert(sc.isSecurePath('uploads/file.txt'));
});
this.test('returns false for path traversal', () => {
this.assert(!sc.isSecurePath('../etc/passwd'));
});
this.test('returns false for /etc/ path', () => {
this.assert(!sc.isSecurePath('/etc/passwd'));
});
this.test('returns false for non-string', () => {
this.assert(!sc.isSecurePath(null));
});
this.test('returns false for too-long path', () => {
this.assert(!sc.isSecurePath('a'.repeat(2049)));
});
}
testValidateInput()
{
this.test('validates email', () => {
this.assert(sc.validateInput('test@example.com', 'email'));
});
this.test('rejects invalid email', () => {
this.assert(!sc.validateInput('not-an-email', 'email'));
});
this.test('validates username', () => {
this.assert(sc.validateInput('user_123', 'username'));
});
this.test('validates alphanumeric', () => {
this.assert(sc.validateInput('abc123', 'alphanumeric'));
});
this.test('validates numeric', () => {
this.assert(sc.validateInput('12345', 'numeric'));
});
this.test('validates hexColor', () => {
this.assert(sc.validateInput('#FF5733', 'hexColor'));
});
this.test('validates ipv4', () => {
this.assert(sc.validateInput('192.168.1.1', 'ipv4'));
});
this.test('returns false for unknown type', () => {
this.assert(!sc.validateInput('anything', 'unknownType'));
});
this.test('returns false for non-string input', () => {
this.assert(!sc.validateInput(42, 'numeric'));
});
}
testSerializeFormData()
{
this.test('serializes single values', () => {
let formData = [['name', 'Alice'], ['age', '30']];
let result = sc.serializeFormData(formData);
this.assert('Alice' === result.name && '30' === result.age);
});
this.test('serializes repeated keys into array', () => {
let formData = [['tag', 'a'], ['tag', 'b']];
let result = sc.serializeFormData(formData);
this.assert(sc.isArray(result.tag) && 2 === result.tag.length);
});
this.test('returns empty object for empty formData', () => {
let result = sc.serializeFormData([]);
this.assert(0 === Object.keys(result).length);
});
}
}
module.exports = { TestShortcuts };