mirror of https://github.com/rancher/dashboard.git
497 lines
14 KiB
TypeScript
497 lines
14 KiB
TypeScript
import { reactive, isReactive } from 'vue';
|
|
import {
|
|
clone, get, getter, isEmpty, toDictionary, remove, diff, definedKeys, deepToRaw, mergeWithReplace, convertKVToString, convertStringToKV
|
|
} from '@shell/utils/object';
|
|
|
|
describe('fx: get', () => {
|
|
describe('should return value of an object', () => {
|
|
it('given a path', () => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = get(obj, 'key1');
|
|
|
|
expect(result).toStrictEqual(obj.key1);
|
|
});
|
|
|
|
it('given a nested path', () => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = get(obj, 'key2.bat');
|
|
|
|
expect(result).toStrictEqual(obj.key2.bat);
|
|
});
|
|
|
|
it('given a complex path', () => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = get(obj, "key2.'with.dots'");
|
|
|
|
expect(result).toStrictEqual(obj.key2['with.dots']);
|
|
});
|
|
});
|
|
|
|
it.each([
|
|
'key2.nonsense',
|
|
'non.sense',
|
|
])('should catch error and return undefined', (path) => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = get(obj, path);
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(() => result).not.toThrow();
|
|
});
|
|
});
|
|
|
|
describe('fx: getter', () => {
|
|
it('should return a function', () => {
|
|
const callBack = getter('key1');
|
|
|
|
expect(typeof callBack).toBe('function');
|
|
});
|
|
|
|
describe('should return value of an object', () => {
|
|
it('given a path', () => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = getter('key1')(obj);
|
|
|
|
expect(result).toStrictEqual(obj.key1);
|
|
});
|
|
|
|
it('given a nested path', () => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = getter('key2.bat')(obj);
|
|
|
|
expect(result).toStrictEqual(obj.key2.bat);
|
|
});
|
|
|
|
it('given a complex path', () => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = getter("key2.'with.dots'")(obj);
|
|
|
|
expect(result).toStrictEqual(obj.key2['with.dots']);
|
|
});
|
|
|
|
it.each([
|
|
'key2.nonsense',
|
|
'non.sense',
|
|
])('should catch error and return undefined', (path) => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = getter(path)(obj);
|
|
|
|
expect(result).toBeUndefined();
|
|
expect(() => result).not.toThrow();
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('fx: clone', () => {
|
|
it('should return a shallow copy of the object', () => {
|
|
const obj = { key1: 'value', key2: { bat: 42, 'with.dots': 43 } };
|
|
|
|
const result = clone(obj);
|
|
|
|
expect(result).not.toBe(obj);
|
|
expect(result).toStrictEqual(obj);
|
|
});
|
|
});
|
|
|
|
describe('fx: isEmpty', () => {
|
|
it.each([
|
|
[{}, true],
|
|
[{ key1: 'value' }, false],
|
|
[[], true],
|
|
[['key1'], false],
|
|
['', true],
|
|
// ['key1', true],
|
|
[0, true],
|
|
[42, true],
|
|
[null, true],
|
|
[undefined, true],
|
|
])('should evaluate %p as %p', (value, expected) => {
|
|
const result = isEmpty(value);
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
|
|
it.each([
|
|
[{ value: 'value' }, true],
|
|
[{ enumerable: true }, false],
|
|
])('should verify if %s is enumerable (%o)', (value, expected) => {
|
|
const enumerable = Object.defineProperty({}, 'key1', value);
|
|
|
|
const result = isEmpty(enumerable);
|
|
|
|
expect(result).toBe(expected);
|
|
});
|
|
});
|
|
|
|
describe('fx: toDictionary', () => {
|
|
it('should return a dictionary from an array', () => {
|
|
const array = ['a', 'b', 'c'];
|
|
const asd = (value: string) => value.toUpperCase();
|
|
const expectation = {
|
|
a: 'A', b: 'B', c: 'C'
|
|
};
|
|
|
|
const result = toDictionary(array, asd);
|
|
|
|
expect(result).toStrictEqual(expectation);
|
|
});
|
|
});
|
|
|
|
describe('fx: remove', () => {
|
|
it.each([
|
|
[{}, '', {}],
|
|
[{}, 'not_there', {}],
|
|
[{}, 'not.there.again', {}],
|
|
[{ level1: true }, 'level1', {}],
|
|
[{ level1: true }, 'not_there', { level1: true }],
|
|
[{ level1: { level2: true } }, 'level1.level2', { level1: { } }],
|
|
[{ level1: { level2: true, other: true } }, 'level1.level2', { level1: { other: true } }],
|
|
[{ level1: { level2: true }, other: true }, 'level1.level2', { level1: { }, other: true }],
|
|
[{ level1: { level2: true }, other: true }, 'not_there.level1.level2', { level1: { level2: true }, other: true }],
|
|
[{ level1: { level2: true }, other: true }, 'level1', { other: true }],
|
|
])('should evaluate %p as %p', (obj, path, expected) => {
|
|
const result = remove(obj, path);
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('fx: diff', () => {
|
|
it('should return an object including only the differences between two objects', () => {
|
|
const from = {
|
|
foo: 'bar',
|
|
baz: 'bang',
|
|
};
|
|
const to = {
|
|
foo: 'bar',
|
|
bang: 'baz'
|
|
};
|
|
|
|
const result = diff(from, to);
|
|
const expected = {
|
|
baz: null,
|
|
bang: 'baz'
|
|
};
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
it('should return an object and dot characters in object should still be respected', () => {
|
|
const from = {};
|
|
const to = { foo: { 'bar.baz': 'bang' } };
|
|
|
|
const result = diff(from, to);
|
|
const expected = { foo: { 'bar.baz': 'bang' } };
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('fx: definedKeys', () => {
|
|
it('should return an array of keys within an array', () => {
|
|
const obj = {
|
|
foo: 'bar',
|
|
baz: 'bang',
|
|
};
|
|
|
|
const result = definedKeys(obj);
|
|
const expected = ['"foo"', '"baz"'];
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
it('should return an array of keys with primitive values and their full nested path', () => {
|
|
const obj = {
|
|
foo: 'bar',
|
|
baz: { bang: 'bop' },
|
|
};
|
|
|
|
const result = definedKeys(obj);
|
|
const expected = ['"foo"', '"baz"."bang"'];
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
it('should return an array of keys with primitive values and their full nested path with quotation marks to escape keys with dots in them', () => {
|
|
const obj = {
|
|
foo: 'bar',
|
|
baz: { 'bang.bop': 'beep' },
|
|
};
|
|
|
|
const result = definedKeys(obj);
|
|
const expected = ['"foo"', '"baz"."bang.bop"'];
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('fx: deepToRaw', () => {
|
|
it('should return primitives as is', () => {
|
|
expect(deepToRaw(null)).toBeNull();
|
|
expect(deepToRaw(undefined)).toBeUndefined();
|
|
expect(deepToRaw(42)).toBe(42);
|
|
expect(deepToRaw('test')).toBe('test');
|
|
expect(deepToRaw(true)).toBe(true);
|
|
|
|
const sym = Symbol('symbol');
|
|
|
|
expect(deepToRaw(sym)).toBe(sym);
|
|
});
|
|
|
|
it('should handle simple objects', () => {
|
|
const obj = { a: 1, b: 2 };
|
|
const result = deepToRaw(obj);
|
|
|
|
expect(result).toStrictEqual({ a: 1, b: 2 });
|
|
expect(result).not.toBe(obj); // Should not be the same reference
|
|
});
|
|
|
|
it('should handle arrays', () => {
|
|
const arr = [1, 2, 3];
|
|
const result = deepToRaw(arr);
|
|
|
|
expect(result).toStrictEqual([1, 2, 3]);
|
|
expect(result).not.toBe(arr); // Should not be the same reference
|
|
});
|
|
|
|
it('should handle nested objects', () => {
|
|
const obj = { a: { b: { c: 3 } } };
|
|
const result = deepToRaw(obj);
|
|
|
|
expect(result).toStrictEqual({ a: { b: { c: 3 } } });
|
|
expect(result).not.toBe(obj);
|
|
expect(result.a).not.toBe(obj.a);
|
|
expect(result.a.b).not.toBe(obj.a.b);
|
|
});
|
|
|
|
it('should handle nested arrays', () => {
|
|
const arr = [1, [2, [3]]];
|
|
const result = deepToRaw(arr);
|
|
|
|
expect(result).toStrictEqual([1, [2, [3]]]);
|
|
expect(result).not.toBe(arr);
|
|
expect(result[1]).not.toBe(arr[1]);
|
|
});
|
|
|
|
it('should handle reactive proxies (reactive object)', () => {
|
|
const reactiveObj = reactive({ a: 1, b: { c: 2 } });
|
|
const result = deepToRaw(reactiveObj);
|
|
|
|
expect(result).toStrictEqual({ a: 1, b: { c: 2 } });
|
|
expect(result).not.toBe(reactiveObj);
|
|
expect(isReactive(result)).toBe(false);
|
|
});
|
|
|
|
it('should handle nested reactive properties', () => {
|
|
const data = reactive({
|
|
num: 1,
|
|
str: 'test',
|
|
bool: true,
|
|
nil: null,
|
|
undef: undefined,
|
|
arr: [1, 2, { a: 3 }],
|
|
obj: { nested: reactive({ a: 1 }) },
|
|
func: null,
|
|
sym: null,
|
|
});
|
|
|
|
const result = deepToRaw(data);
|
|
|
|
expect(result).toStrictEqual({
|
|
num: 1,
|
|
str: 'test',
|
|
bool: true,
|
|
nil: null,
|
|
undef: undefined,
|
|
arr: [1, 2, { a: 3 }],
|
|
obj: { nested: { a: 1 } },
|
|
func: null,
|
|
sym: null,
|
|
});
|
|
|
|
expect(isReactive(result)).toBe(false);
|
|
expect(isReactive(result.obj)).toBe(false);
|
|
expect(isReactive(result.obj.nested)).toBe(false);
|
|
});
|
|
|
|
it('should handle circular references', () => {
|
|
const obj: { name: string; [key: string]: any } = { name: 'Alice' };
|
|
|
|
obj.self = obj; // Circular reference
|
|
|
|
const result = deepToRaw(obj);
|
|
|
|
expect(result).toStrictEqual({ name: 'Alice', self: result });
|
|
});
|
|
|
|
it('should handle objects with functions and symbols', () => {
|
|
const symbolKey = Symbol('key');
|
|
const obj = {
|
|
a: 1,
|
|
b() {},
|
|
[symbolKey]: 'symbolValue',
|
|
};
|
|
|
|
const result = deepToRaw(obj);
|
|
|
|
expect(result).toStrictEqual({ a: 1, b: null });
|
|
expect(result[symbolKey]).toBeUndefined(); // Symbols are skipped
|
|
});
|
|
|
|
it('should not mutate the original data', () => {
|
|
const obj = { a: { b: 2 } };
|
|
const original = JSON.stringify(obj);
|
|
|
|
deepToRaw(obj);
|
|
expect(JSON.stringify(obj)).toBe(original);
|
|
});
|
|
|
|
it('should handle complex data structures', () => {
|
|
const data = reactive({
|
|
num: 1,
|
|
str: 'test',
|
|
bool: true,
|
|
nil: null,
|
|
undef: undefined,
|
|
arr: [1, 2, { a: 3 }],
|
|
obj: { nested: { a: 1 } },
|
|
func: () => {},
|
|
sym: Symbol('sym'),
|
|
});
|
|
|
|
const result = deepToRaw(data);
|
|
|
|
expect(result).toStrictEqual({
|
|
num: 1,
|
|
str: 'test',
|
|
bool: true,
|
|
nil: null,
|
|
undef: undefined,
|
|
arr: [1, 2, { a: 3 }],
|
|
obj: { nested: { a: 1 } },
|
|
func: null,
|
|
sym: null,
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('fx: mergeWithReplace', () => {
|
|
const testCases: Array<[object?, object?, object?]> = [
|
|
// Some array test cases, an array from the first object should be replaced with the array from the second object
|
|
[{ a: ['one'] }, { a: [] }, { a: [] }],
|
|
[{ a: ['one', 'two'] }, { a: ['one', 'two', 'three'] }, { a: ['one', 'two', 'three'] }],
|
|
[{ a: ['one', 'two'], b: ['three', 'four'] }, { a: ['one'], b: [] }, { a: ['one'], b: [] }],
|
|
[{
|
|
a: ['one', 'two'], b: ['three', 'four'], c: 'five'
|
|
}, { a: ['one'], b: [] }, {
|
|
a: ['one'], b: [], c: 'five'
|
|
}],
|
|
// Some other test cases
|
|
[{ a: 'one' }, { b: 'two' }, { a: 'one', b: 'two' }],
|
|
[{ a: 'one' }, { a: '', b: 'two' }, { a: '', b: 'two' }],
|
|
[{ a: 'one', b: 'two' }, { a: 1, c: { d: null } }, {
|
|
a: 1, b: 'two', c: { d: null }
|
|
}],
|
|
[undefined, undefined, {}],
|
|
[{}, undefined, {}],
|
|
[undefined, {}, {}],
|
|
];
|
|
|
|
it.each(testCases)('should merge arrays properly', (obj1, obj2, expected) => {
|
|
const result = mergeWithReplace(obj1, obj2);
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
|
|
it.each([
|
|
[
|
|
{ a: { b: false, c: false } }, { a: { b: true, c: null } }, { a: { b: true, c: null } }
|
|
],
|
|
[
|
|
{
|
|
a: [{
|
|
b: 'test', c: 'test', value: true
|
|
}]
|
|
}, {
|
|
a: [{
|
|
b: 'test', c: 'test', operator: 'exists'
|
|
}]
|
|
}, {
|
|
a: [{
|
|
b: 'test', c: 'test', operator: 'exists'
|
|
}]
|
|
}
|
|
],
|
|
[
|
|
{
|
|
a: { enabled: false }, b: { enabled: false }, c: { enabled: false }
|
|
},
|
|
{ c: { enabled: true, stripUnderscores: true } },
|
|
{
|
|
a: { enabled: false }, b: { enabled: false }, c: { enabled: true, stripUnderscores: true }
|
|
}
|
|
]
|
|
])('should overwrite duplicate object properties when merging objects', (left, right, expected) => {
|
|
const result = mergeWithReplace(left, right, { replaceObjectProps: true });
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('fx: convertKVToString', () => {
|
|
it.each([
|
|
[{}, ''],
|
|
[{ foo: 'bar' }, 'foo,bar'],
|
|
[{ foo: 'bar', baz: 'bang' }, 'foo,bar,baz,bang'],
|
|
[{ foo: 'bar', baz: null }, 'foo,bar,baz,null'],
|
|
[{ ' spaced ': ' value ' }, ' spaced , value '], // preserves raw keys/values
|
|
[{ 0: 42, truthy: true }, '0,42,truthy,true'],
|
|
])('should convert %p -> "%s"', (input, expected) => {
|
|
const result = convertKVToString(input);
|
|
|
|
expect(result).toBe(expected);
|
|
});
|
|
|
|
it('should produce a string with an even number of comma-separated parts', () => {
|
|
const str = convertKVToString({
|
|
a: 1, b: 2, c: 3
|
|
});
|
|
const parts = str.split(',');
|
|
|
|
expect(parts.length % 2).toBe(0);
|
|
});
|
|
});
|
|
|
|
describe('fx: convertStringToKV', () => {
|
|
it.each([
|
|
['', {}],
|
|
['foo,bar', { foo: 'bar' }],
|
|
['foo,bar,baz,bang', { foo: 'bar', baz: 'bang' }],
|
|
[' foo , bar ', { foo: 'bar' }], // trims whitespace
|
|
['foo,bar,baz', { foo: 'bar' }], // dangling key ignored
|
|
['foo,bar,,bang', { foo: 'bar', '': 'bang' }], // empty key retained when present
|
|
])('should convert "%s" -> %p', (input, expected) => {
|
|
const result = convertStringToKV(input);
|
|
|
|
expect(result).toStrictEqual(expected);
|
|
});
|
|
});
|
|
|
|
describe('fx: convertStringToKV to convertKVToString and back (round-trip)', () => {
|
|
it.each([
|
|
'',
|
|
'foo,bar',
|
|
'foo,bar,baz,bang',
|
|
'answer,42,truthy,true,falsy,false',
|
|
])('should round-trip %p without loss', (input) => {
|
|
const asObj = convertStringToKV(input);
|
|
const backToString = convertKVToString(asObj);
|
|
|
|
expect(backToString).toStrictEqual(input);
|
|
});
|
|
});
|