Test cases and minor function fixes

This commit is contained in:
2024-12-10 22:55:30 +02:00
parent aba68a3c0a
commit a136af8e02
38 changed files with 3250 additions and 824 deletions

View File

@@ -5,34 +5,52 @@
* @param deep Enable deep comparison for nested objects/arrays
*/
export function objectCompare<T extends Record<string, unknown>>(
obj: T,
objToCompare: T,
deep = false
): boolean {
if (!obj || !objToCompare) return false;
return Object.keys(obj).length === Object.keys(objToCompare).length &&
Object.keys(obj).every((key) => {
if (!Object.prototype.hasOwnProperty.call(objToCompare, key)) return false;
const val1 = obj[key];
const val2 = objToCompare[key];
if (!deep) return val1 === val2;
if (Array.isArray(val1) && Array.isArray(val2)) {
return val1.length === val2.length &&
val1.every((item, i) =>
typeof item === 'object' && item !== null
? objectCompare(item as Record<string, unknown>, val2[i] as Record<string, unknown>, true)
: item === val2[i]
);
}
if (typeof val1 === 'object' && typeof val2 === 'object' && val1 !== null && val2 !== null) {
return objectCompare(val1 as Record<string, unknown>, val2 as Record<string, unknown>, true);
}
return val1 === val2;
});
}
obj: T | any,
objToCompare: T | any,
deep = false
): boolean {
if (!obj || !objToCompare) return false;
return (
Object.keys(obj).length === Object.keys(objToCompare).length &&
Object.keys(obj).every((key) => {
if (!Object.prototype.hasOwnProperty.call(objToCompare, key))
return false;
const val1 = obj[key];
const val2 = objToCompare[key];
if (!deep) return val1 === val2;
if (Array.isArray(val1) && Array.isArray(val2)) {
return (
val1.length === val2.length &&
val1.every((item, i) =>
typeof item === "object" && item !== null
? objectCompare(
item as Record<string, unknown>,
val2[i] as Record<string, unknown>,
true
)
: item === val2[i]
)
);
}
if (
typeof val1 === "object" &&
typeof val2 === "object" &&
val1 !== null &&
val2 !== null
) {
return objectCompare(
val1 as Record<string, unknown>,
val2 as Record<string, unknown>,
true
);
}
return val1 === val2;
})
);
}

133
src/object/comparte.test.ts Normal file
View File

@@ -0,0 +1,133 @@
import { describe, expect, test } from "vitest";
import { objectCompare } from "./compare";
describe("objectCompare", () => {
// Basic shallow comparison tests
test("should return true for identical simple objects in shallow mode", () => {
const obj1 = { a: 1, b: "test", c: true };
const obj2 = { a: 1, b: "test", c: true };
expect(objectCompare(obj1, obj2)).toBe(true);
});
test("should return false for different simple objects in shallow mode", () => {
const obj1 = { a: 1, b: "test" };
const obj2 = { a: 1, b: "different" };
expect(objectCompare(obj1, obj2)).toBe(false);
});
// Property order tests
test("should return true for objects with same properties in different order", () => {
const obj1 = { a: 1, b: 2, c: 3 };
const obj2 = { c: 3, a: 1, b: 2 };
expect(objectCompare(obj1, obj2)).toBe(true);
});
// Deep comparison tests
test("should compare nested objects when deep is true", () => {
const obj1 = { a: { b: 1, c: 2 }, d: 3 };
const obj2 = { a: { b: 1, c: 2 }, d: 3 };
expect(objectCompare(obj1, obj2, true)).toBe(true);
});
test("should detect differences in nested objects when deep is true", () => {
const obj1 = { a: { b: 1, c: 2 }, d: 3 };
const obj2 = { a: { b: 1, c: 3 }, d: 3 };
expect(objectCompare(obj1, obj2, true)).toBe(false);
});
// Array comparison tests
test("should compare arrays correctly in deep mode", () => {
const obj1 = { arr: [1, 2, { x: 1 }] };
const obj2 = { arr: [1, 2, { x: 1 }] };
expect(objectCompare(obj1, obj2, true)).toBe(true);
});
test("should detect array differences in deep mode", () => {
const obj1 = { arr: [1, 2, { x: 1 }] };
const obj2 = { arr: [1, 2, { x: 2 }] };
expect(objectCompare(obj1, obj2, true)).toBe(false);
});
// Edge cases
test("should handle null values", () => {
const obj1 = { a: null };
const obj2 = { a: null };
expect(objectCompare(obj1, obj2)).toBe(true);
});
test("should handle undefined values", () => {
const obj1 = { a: undefined };
const obj2 = { a: undefined };
expect(objectCompare(obj1, obj2)).toBe(true);
});
test("should return false when comparing with null", () => {
const obj1 = { a: 1 };
expect(objectCompare(obj1, null as any)).toBe(false);
expect(objectCompare(null as any, obj1)).toBe(false);
});
// Complex nested structure tests
test("should handle complex nested structures in deep mode", () => {
const obj1 = {
a: 1,
b: {
c: [1, 2, { d: 3 }],
e: { f: 4, g: [5, 6] },
},
};
const obj2 = {
a: 1,
b: {
c: [1, 2, { d: 3 }],
e: { f: 4, g: [5, 6] },
},
};
expect(objectCompare(obj1, obj2, true)).toBe(true);
});
test("should detect differences in complex nested structures", () => {
const obj1 = {
a: 1,
b: {
c: [1, 2, { d: 3 }],
e: { f: 4, g: [5, 6] },
},
};
const obj2 = {
a: 1,
b: {
c: [1, 2, { d: 3 }],
e: { f: 4, g: [5, 7] }, // Changed 6 to 7
},
};
expect(objectCompare(obj1, obj2, true)).toBe(false);
});
// Property existence tests
test("should handle objects with different number of properties", () => {
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, b: 2, c: 3 };
expect(objectCompare(obj1, obj2)).toBe(false);
});
test("should handle objects with same number of properties but different keys", () => {
const obj1 = { a: 1, b: 2 };
const obj2 = { a: 1, c: 2 };
expect(objectCompare(obj1, obj2)).toBe(false);
});
// Type comparison tests
test("should handle type coercion correctly", () => {
const obj1 = { a: 1 };
const obj2 = { a: "1" };
expect(objectCompare(obj1, obj2)).toBe(false);
});
test("should handle empty objects", () => {
const obj1 = {};
const obj2 = {};
expect(objectCompare(obj1, obj2)).toBe(true);
expect(objectCompare(obj1, obj2, true)).toBe(true);
});
});

172
src/object/nested.test.ts Normal file
View File

@@ -0,0 +1,172 @@
import { describe, expect, test } from "vitest";
import { getNestedValue, setNestedValue } from "./nested";
describe("getNestedValue", () => {
const testObj = {
user: {
name: "John",
contacts: [
{ email: "john@example.com", phone: "123-456" },
{ email: "john.alt@example.com", phone: "789-012" },
],
settings: {
notifications: {
email: true,
push: false,
},
},
},
meta: {
created: "2023-01-01",
tags: ["important", "user"],
},
};
test("should get simple property values", () => {
expect(getNestedValue("user.name", testObj)).toBe("John");
expect(getNestedValue("meta.created", testObj)).toBe("2023-01-01");
});
test("should get nested property values", () => {
expect(getNestedValue("user.settings.notifications.email", testObj)).toBe(
true
);
expect(getNestedValue("user.settings.notifications.push", testObj)).toBe(
false
);
});
test("should handle array indexing", () => {
expect(getNestedValue("user.contacts[0].email", testObj)).toBe(
"john@example.com"
);
expect(getNestedValue("user.contacts[1].phone", testObj)).toBe("789-012");
expect(getNestedValue("meta.tags[0]", testObj)).toBe("important");
});
test("should handle invalid paths", () => {
expect(getNestedValue("user.invalid.path", testObj)).toBeUndefined();
expect(getNestedValue("invalid", testObj)).toBeUndefined();
expect(getNestedValue("user.contacts[5].email", testObj)).toBeUndefined();
});
test("should handle empty or invalid inputs", () => {
expect(getNestedValue("", testObj)).toBeUndefined();
expect(getNestedValue("user.contacts.[]", testObj)).toBeUndefined();
expect(getNestedValue("user..name", testObj)).toBeUndefined();
});
});
describe("setNestedValue", () => {
test("should set simple property values", () => {
const obj = { user: { name: "John" } };
setNestedValue("user.name", "Jane", obj);
expect(obj.user.name).toBe("Jane");
});
test("should create missing objects", () => {
const obj = {};
setNestedValue("user.settings.theme", "dark", obj);
expect(obj).toEqual({
user: {
settings: {
theme: "dark",
},
},
});
});
test("should handle array indexing and creation", () => {
const obj = { users: [] };
setNestedValue("users[0].name", "John", obj);
setNestedValue("users[1].name", "Jane", obj);
expect(obj).toEqual({
users: [{ name: "John" }, { name: "Jane" }],
});
});
test("should handle mixed object and array paths", () => {
const obj = {};
setNestedValue("company.departments[0].employees[0].name", "John", obj);
expect(obj).toEqual({
company: {
departments: [
{
employees: [{ name: "John" }],
},
],
},
});
});
test("should modify existing nested arrays", () => {
const obj = {
users: [{ contacts: [{ email: "old@example.com" }] }],
};
setNestedValue("users[0].contacts[0].email", "new@example.com", obj);
expect(obj.users[0].contacts[0].email).toBe("new@example.com");
});
test("should handle complex nested structures", () => {
const obj = {};
setNestedValue("a.b[0].c.d[1].e", "value", obj);
expect(obj).toEqual({
a: {
b: [
{
c: {
d: [undefined, { e: "value" }],
},
},
],
},
});
});
test("should override existing values with different types", () => {
const obj = {
data: "string",
};
setNestedValue("data.nested", "value", obj);
expect(obj.data).toEqual({ nested: "value" });
});
test("should return the modified object", () => {
const obj = {};
const result = setNestedValue("a.b.c", "value", obj);
expect(result).toBe(obj);
expect(result).toEqual({
a: {
b: {
c: "value",
},
},
});
});
test("should handle numeric array indices properly", () => {
const obj = {};
setNestedValue("items[0]", "first", obj);
setNestedValue("items[2]", "third", obj);
expect(obj).toEqual({
items: ["first", undefined, "third"],
});
});
test("should handle edge cases", () => {
const obj: any = {};
// Empty path segments
setNestedValue("a..b", "value", obj);
expect(obj.a.b).toBe("value");
// Numeric property names
setNestedValue("prop.0.value", "test", obj);
expect(obj.prop["0"].value).toBe("test");
// Setting value on existing array
obj.list = [];
setNestedValue("list[0]", "item", obj);
expect(obj.list[0]).toBe("item");
});
});

View File

@@ -5,42 +5,99 @@
* @returns Value at path or undefined if path invalid
*/
export function getNestedValue(path: string, obj: Record<string, any>): any {
return path
if (!path || !obj) return undefined;
// Check for invalid path patterns
if (path.includes("..") || path.includes("[]") || /\[\s*\]/.test(path)) {
return undefined;
}
const parts = path
.replace(/\[(\w+)\]/g, ".$1") // Convert brackets to dot notation
.split(".") // Split path into parts
.reduce((prev, curr) => prev?.[curr], obj); // Traverse object
}
.split(".")
.filter(Boolean); // Remove empty segments
if (parts.length === 0) return undefined;
/**
* Sets a nested value in an object using a path string
* @param path - Dot notation path (e.g. 'user.contacts[0].email')
* @param value - Value to set at path
* @param obj - Target object to modify
* @returns Modified object
*/
/**
* Sets a nested value, creating objects and arrays if needed
*/
export function setNestedValue(path: string, value: any, obj: Record<string, any>): Record<string, any> {
const parts = path.replace(/\[(\w+)\]/g, ".$1").split(".");
const lastKey = parts.pop()!;
const target = parts.reduce((prev, curr) => {
// Handle array indices
if (/^\d+$/.test(curr)) {
if (!Array.isArray(prev[curr])) {
prev[curr] = [];
}
}
// Create missing objects
else if (!prev[curr]) {
prev[curr] = {};
}
return parts.reduce((prev, curr) => {
if (prev === undefined) return undefined;
return prev[curr];
}, obj);
}
target[lastKey] = value;
/**
* Sets a nested value in an object using a path string
* @param path - Dot notation path (e.g. 'user.contacts[0].email')
* @param value - Value to set at path
* @param obj - Target object to modify
* @returns Modified object
*/
export function setNestedValue(
path: string,
value: any,
obj: Record<string, any>
): Record<string, any> {
if (!path || !obj) return obj;
const parts = path
.replace(/\[(\w+)\]/g, ".$1")
.split(".")
.filter(Boolean); // Remove empty segments
if (parts.length === 0) return obj;
const lastKey = parts.pop()!;
let current = obj;
for (let i = 0; i < parts.length; i++) {
const key = parts[i];
const nextKey = parts[i + 1] || lastKey;
const shouldBeArray = /^\d+$/.test(nextKey);
// If current key doesn't exist or needs type conversion
if (
!(key in current) ||
(shouldBeArray && !Array.isArray(current[key])) ||
(!shouldBeArray && typeof current[key] !== "object")
) {
// Create appropriate container based on next key
current[key] = shouldBeArray ? [] : {};
}
current = current[key];
}
// Handle the last key - determine if parent should be an array
if (/^\d+$/.test(lastKey) && !Array.isArray(current)) {
const tempObj = current;
const maxIndex = Math.max(
...Object.keys(tempObj)
.filter((k) => /^\d+$/.test(k))
.map(Number)
.concat(-1)
);
const arr = new Array(Math.max(maxIndex + 1, Number(lastKey) + 1));
// Copy existing numeric properties to array
Object.keys(tempObj)
.filter((k) => /^\d+$/.test(k))
.forEach((k) => {
arr[Number(k)] = tempObj[k];
});
// Replace object with array while preserving non-numeric properties
Object.keys(tempObj).forEach((k) => {
if (!/^\d+$/.test(k)) {
arr[k] = tempObj[k];
}
});
Object.keys(tempObj).forEach((k) => delete tempObj[k]);
Object.assign(tempObj, arr);
current = tempObj;
}
// Set the final value
current[lastKey] = value;
return obj;
}
}

139
src/object/utils.test.ts Normal file
View File

@@ -0,0 +1,139 @@
import { describe, expect, test } from 'vitest';
import { createSelectOptions } from './util';
describe('createSelectOptions', () => {
// Test basic object transformation
test('should transform basic object with default options', () => {
const input = {
key1: 'Label 1',
key2: 'Label 2',
key3: 'Label 3'
};
const expected = [
{ label: 'Label 1', value: 'key1' },
{ label: 'Label 2', value: 'key2' },
{ label: 'Label 3', value: 'key3' }
];
expect(createSelectOptions(input)).toEqual(expected);
});
// Test custom key names
test('should use custom property names when provided', () => {
const input = {
key1: 'Label 1',
key2: 'Label 2'
};
const expected = [
{ text: 'Label 1', id: 'key1' },
{ text: 'Label 2', id: 'key2' }
];
expect(createSelectOptions(input, { labelKey: 'text', valueKey: 'id' })).toEqual(expected);
});
// Test array input
test('should return array as-is when input is an array', () => {
const input = [
{ label: 'Option 1', value: '1' },
{ label: 'Option 2', value: '2' }
];
expect(createSelectOptions(input)).toBe(input);
});
// Test different value types
test('should handle different value types', () => {
const input = {
key1: 42,
key2: true,
key3: { nested: 'value' }
};
const expected = [
{ label: 42, value: 'key1' },
{ label: true, value: 'key2' },
{ label: { nested: 'value' }, value: 'key3' }
];
expect(createSelectOptions(input)).toEqual(expected);
});
// Test null and undefined inputs
test('should handle null and undefined inputs', () => {
expect(createSelectOptions(null)).toEqual([]);
expect(createSelectOptions(undefined)).toEqual([]);
});
// Test invalid inputs
test('should handle invalid inputs', () => {
expect(createSelectOptions(42 as any)).toEqual([]);
expect(createSelectOptions('string' as any)).toEqual([]);
expect(createSelectOptions(true as any)).toEqual([]);
});
// Test empty object
test('should handle empty object', () => {
expect(createSelectOptions({})).toEqual([]);
});
// Test partial options
test('should handle partial options configuration', () => {
const input = {
key1: 'Label 1',
key2: 'Label 2'
};
// Only labelKey provided
expect(createSelectOptions(input, { labelKey: 'text' })).toEqual([
{ text: 'Label 1', value: 'key1' },
{ text: 'Label 2', value: 'key2' }
]);
// Only valueKey provided
expect(createSelectOptions(input, { valueKey: 'id' })).toEqual([
{ label: 'Label 1', id: 'key1' },
{ label: 'Label 2', id: 'key2' }
]);
});
// Test type safety
test('should maintain type safety with generic types', () => {
interface CustomType {
name: string;
count: number;
}
const input: Record<string, CustomType> = {
key1: { name: 'Item 1', count: 1 },
key2: { name: 'Item 2', count: 2 }
};
const result = createSelectOptions<CustomType>(input);
expect(result).toEqual([
{ label: { name: 'Item 1', count: 1 }, value: 'key1' },
{ label: { name: 'Item 2', count: 2 }, value: 'key2' }
]);
});
// Test with special characters in keys
test('should handle special characters in keys', () => {
const input = {
'@special': 'Special',
'key.with.dots': 'Dots',
'key-with-dashes': 'Dashes',
'key with spaces': 'Spaces'
};
const expected = [
{ label: 'Special', value: '@special' },
{ label: 'Dots', value: 'key.with.dots' },
{ label: 'Dashes', value: 'key-with-dashes' },
{ label: 'Spaces', value: 'key with spaces' }
];
expect(createSelectOptions(input)).toEqual(expected);
});
});