Merge pull request #5910 from matrix-org/travis/tests/array-obj-utils
Add unit tests for various collection-based utility functions
This commit is contained in:
commit
09af2a8891
11 changed files with 1242 additions and 17 deletions
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -15,23 +15,47 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Quickly resample an array to have less data points. This isn't a perfect representation,
|
* Quickly resample an array to have less/more data points. If an input which is larger
|
||||||
* though this does work best if given a large array to downsample to a much smaller array.
|
* than the desired size is provided, it will be downsampled. Similarly, if the input
|
||||||
* @param {number[]} input The input array to downsample.
|
* is smaller than the desired size then it will be upsampled.
|
||||||
|
* @param {number[]} input The input array to resample.
|
||||||
* @param {number} points The number of samples to end up with.
|
* @param {number} points The number of samples to end up with.
|
||||||
* @returns {number[]} The downsampled array.
|
* @returns {number[]} The resampled array.
|
||||||
*/
|
*/
|
||||||
export function arrayFastResample(input: number[], points: number): number[] {
|
export function arrayFastResample(input: number[], points: number): number[] {
|
||||||
// Heavily inpired by matrix-media-repo (used with permission)
|
if (input.length === points) return input; // short-circuit a complicated call
|
||||||
|
|
||||||
|
// Heavily inspired by matrix-media-repo (used with permission)
|
||||||
// https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10
|
// https://github.com/turt2live/matrix-media-repo/blob/abe72c87d2e29/util/util_audio/fastsample.go#L10
|
||||||
|
let samples: number[] = [];
|
||||||
|
if (input.length > points) {
|
||||||
|
// Danger: this loop can cause out of memory conditions if the input is too small.
|
||||||
const everyNth = Math.round(input.length / points);
|
const everyNth = Math.round(input.length / points);
|
||||||
const samples: number[] = [];
|
|
||||||
for (let i = 0; i < input.length; i += everyNth) {
|
for (let i = 0; i < input.length; i += everyNth) {
|
||||||
samples.push(input[i]);
|
samples.push(input[i]);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
// Smaller inputs mean we have to spread the values over the desired length. We
|
||||||
|
// end up overshooting the target length in doing this, so we'll resample down
|
||||||
|
// before returning. This recursion is risky, but mathematically should not go
|
||||||
|
// further than 1 level deep.
|
||||||
|
const spreadFactor = Math.ceil(points / input.length);
|
||||||
|
for (const val of input) {
|
||||||
|
samples.push(...arraySeed(val, spreadFactor));
|
||||||
|
}
|
||||||
|
samples = arrayFastResample(samples, points);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sanity fill, just in case
|
||||||
while (samples.length < points) {
|
while (samples.length < points) {
|
||||||
samples.push(input[input.length - 1]);
|
samples.push(input[input.length - 1]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sanity trim, just in case
|
||||||
|
if (samples.length > points) {
|
||||||
|
samples = samples.slice(0, points);
|
||||||
|
}
|
||||||
|
|
||||||
return samples;
|
return samples;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -178,6 +202,13 @@ export class GroupedArray<K, T> {
|
||||||
constructor(private val: Map<K, T[]>) {
|
constructor(private val: Map<K, T[]>) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The value of this group, after all applicable alterations.
|
||||||
|
*/
|
||||||
|
public get value(): Map<K, T[]> {
|
||||||
|
return this.val;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Orders the grouping into an array using the provided key order.
|
* Orders the grouping into an array using the provided key order.
|
||||||
* @param keyOrder The key order.
|
* @param keyOrder The key order.
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -19,11 +19,23 @@ limitations under the License.
|
||||||
* @param e The enum.
|
* @param e The enum.
|
||||||
* @returns The enum values.
|
* @returns The enum values.
|
||||||
*/
|
*/
|
||||||
export function getEnumValues<T>(e: any): T[] {
|
export function getEnumValues(e: any): (string | number)[] {
|
||||||
|
// String-based enums will simply be objects ({Key: "value"}), but number-based
|
||||||
|
// enums will instead map themselves twice: in one direction for {Key: 12} and
|
||||||
|
// the reverse for easy lookup, presumably ({12: Key}). In the reverse mapping,
|
||||||
|
// the key is a string, not a number.
|
||||||
|
//
|
||||||
|
// For this reason, we try to determine what kind of enum we're dealing with.
|
||||||
|
|
||||||
const keys = Object.keys(e);
|
const keys = Object.keys(e);
|
||||||
return keys
|
const values: (string | number)[] = [];
|
||||||
.filter(k => ['string', 'number'].includes(typeof(e[k])))
|
for (const key of keys) {
|
||||||
.map(k => e[k]);
|
const value = e[key];
|
||||||
|
if (Number.isFinite(value) || e[value.toString()] !== Number(key)) {
|
||||||
|
values.push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
/*
|
/*
|
||||||
Copyright 2020 The Matrix.org Foundation C.I.C.
|
Copyright 2020, 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
Licensed under the Apache License, Version 2.0 (the "License");
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
you may not use this file except in compliance with the License.
|
you may not use this file except in compliance with the License.
|
||||||
|
@ -141,3 +141,21 @@ export function objectKeyChanges<O extends {}>(a: O, b: O): (keyof O)[] {
|
||||||
export function objectClone<O extends {}>(obj: O): O {
|
export function objectClone<O extends {}>(obj: O): O {
|
||||||
return JSON.parse(JSON.stringify(obj));
|
return JSON.parse(JSON.stringify(obj));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Converts a series of entries to an object.
|
||||||
|
* @param entries The entries to convert.
|
||||||
|
* @returns The converted object.
|
||||||
|
*/
|
||||||
|
// NOTE: Deprecated once we have Object.fromEntries() support.
|
||||||
|
// @ts-ignore - return type is complaining about non-string keys, but we know better
|
||||||
|
export function objectFromEntries<K, V>(entries: Iterable<[K, V]>): {[k: K]: V} {
|
||||||
|
const obj: {
|
||||||
|
// @ts-ignore - same as return type
|
||||||
|
[k: K]: V} = {};
|
||||||
|
for (const e of entries) {
|
||||||
|
// @ts-ignore - same as return type
|
||||||
|
obj[e[0]] = e[1];
|
||||||
|
}
|
||||||
|
return obj;
|
||||||
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import {Singleflight} from "../src/utils/Singleflight";
|
import {Singleflight} from "../../src/utils/Singleflight";
|
||||||
|
|
||||||
describe('Singleflight', () => {
|
describe('Singleflight', () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
294
test/utils/arrays-test.ts
Normal file
294
test/utils/arrays-test.ts
Normal file
|
@ -0,0 +1,294 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
arrayDiff,
|
||||||
|
arrayFastClone,
|
||||||
|
arrayFastResample,
|
||||||
|
arrayHasDiff,
|
||||||
|
arrayHasOrderChange,
|
||||||
|
arrayMerge,
|
||||||
|
arraySeed,
|
||||||
|
arrayUnion,
|
||||||
|
ArrayUtil,
|
||||||
|
GroupedArray,
|
||||||
|
} from "../../src/utils/arrays";
|
||||||
|
import {objectFromEntries} from "../../src/utils/objects";
|
||||||
|
|
||||||
|
function expectSample(i: number, input: number[], expected: number[]) {
|
||||||
|
console.log(`Resample case index: ${i}`); // for debugging test failures
|
||||||
|
const result = arrayFastResample(input, expected.length);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(expected.length);
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('arrays', () => {
|
||||||
|
describe('arrayFastResample', () => {
|
||||||
|
it('should downsample', () => {
|
||||||
|
[
|
||||||
|
{input: [1, 2, 3, 4, 5], output: [1, 4]}, // Odd -> Even
|
||||||
|
{input: [1, 2, 3, 4, 5], output: [1, 3, 5]}, // Odd -> Odd
|
||||||
|
{input: [1, 2, 3, 4], output: [1, 2, 3]}, // Even -> Odd
|
||||||
|
{input: [1, 2, 3, 4], output: [1, 3]}, // Even -> Even
|
||||||
|
].forEach((c, i) => expectSample(i, c.input, c.output));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should upsample', () => {
|
||||||
|
[
|
||||||
|
{input: [1, 2, 3], output: [1, 1, 2, 2, 3, 3]}, // Odd -> Even
|
||||||
|
{input: [1, 2, 3], output: [1, 1, 2, 2, 3]}, // Odd -> Odd
|
||||||
|
{input: [1, 2], output: [1, 1, 1, 2, 2]}, // Even -> Odd
|
||||||
|
{input: [1, 2], output: [1, 1, 1, 2, 2, 2]}, // Even -> Even
|
||||||
|
].forEach((c, i) => expectSample(i, c.input, c.output));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain sample', () => {
|
||||||
|
[
|
||||||
|
{input: [1, 2, 3], output: [1, 2, 3]}, // Odd
|
||||||
|
{input: [1, 2], output: [1, 2]}, // Even
|
||||||
|
].forEach((c, i) => expectSample(i, c.input, c.output));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arraySeed', () => {
|
||||||
|
it('should create an array of given length', () => {
|
||||||
|
const val = 1;
|
||||||
|
const output = [val, val, val];
|
||||||
|
const result = arraySeed(val, output.length);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(output.length);
|
||||||
|
expect(result).toEqual(output);
|
||||||
|
});
|
||||||
|
it('should maintain pointers', () => {
|
||||||
|
const val = {}; // this works because `{} !== {}`, which is what toEqual checks
|
||||||
|
const output = [val, val, val];
|
||||||
|
const result = arraySeed(val, output.length);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(output.length);
|
||||||
|
expect(result).toEqual(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arrayFastClone', () => {
|
||||||
|
it('should break pointer reference on source array', () => {
|
||||||
|
const val = {}; // we'll test to make sure the values maintain pointers too
|
||||||
|
const input = [val, val, val];
|
||||||
|
const result = arrayFastClone(input);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(input.length);
|
||||||
|
expect(result).toEqual(input); // we want the array contents to match...
|
||||||
|
expect(result).not.toBe(input); // ... but be a different reference
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arrayHasOrderChange', () => {
|
||||||
|
it('should flag true on B ordering difference', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [3, 2, 1];
|
||||||
|
const result = arrayHasOrderChange(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag false on no ordering difference', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 3];
|
||||||
|
const result = arrayHasOrderChange(a, b);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag true on A length > B length', () => {
|
||||||
|
const a = [1, 2, 3, 4];
|
||||||
|
const b = [1, 2, 3];
|
||||||
|
const result = arrayHasOrderChange(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag true on A length < B length', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 3, 4];
|
||||||
|
const result = arrayHasOrderChange(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arrayHasDiff', () => {
|
||||||
|
it('should flag true on A length > B length', () => {
|
||||||
|
const a = [1, 2, 3, 4];
|
||||||
|
const b = [1, 2, 3];
|
||||||
|
const result = arrayHasDiff(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag true on A length < B length', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 3, 4];
|
||||||
|
const result = arrayHasDiff(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag true on element differences', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [4, 5, 6];
|
||||||
|
const result = arrayHasDiff(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag false if same but order different', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [3, 1, 2];
|
||||||
|
const result = arrayHasDiff(a, b);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag false if same', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 3];
|
||||||
|
const result = arrayHasDiff(a, b);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arrayDiff', () => {
|
||||||
|
it('should see added from A->B', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 3, 4];
|
||||||
|
const result = arrayDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.added).toEqual([4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see removed from A->B', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2];
|
||||||
|
const result = arrayDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(1);
|
||||||
|
expect(result.removed).toEqual([3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see added and removed in the same set', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 4]; // note diff
|
||||||
|
const result = arrayDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.removed).toHaveLength(1);
|
||||||
|
expect(result.added).toEqual([4]);
|
||||||
|
expect(result.removed).toEqual([3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arrayUnion', () => {
|
||||||
|
it('should return a union', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 4]; // note diff
|
||||||
|
const result = arrayUnion(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array on no matches', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [4, 5, 6];
|
||||||
|
const result = arrayUnion(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('arrayMerge', () => {
|
||||||
|
it('should merge 3 arrays with deduplication', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 4, 5]; // note missing 3
|
||||||
|
const c = [6, 7, 8, 9];
|
||||||
|
const result = arrayMerge(a, b, c);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(9);
|
||||||
|
expect(result).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should deduplicate a single array', () => {
|
||||||
|
// dev note: this is technically an edge case, but it is described behaviour if the
|
||||||
|
// function is only provided one function (it'll merge the array against itself)
|
||||||
|
const a = [1, 1, 2, 2, 3, 3];
|
||||||
|
const result = arrayMerge(a);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result).toEqual([1, 2, 3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ArrayUtil', () => {
|
||||||
|
it('should maintain the pointer to the given array', () => {
|
||||||
|
const input = [1, 2, 3];
|
||||||
|
const result = new ArrayUtil(input);
|
||||||
|
expect(result.value).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should group appropriately', () => {
|
||||||
|
const input = [['a', 1], ['b', 2], ['c', 3], ['a', 4], ['a', 5], ['b', 6]];
|
||||||
|
const output = {
|
||||||
|
'a': [['a', 1], ['a', 4], ['a', 5]],
|
||||||
|
'b': [['b', 2], ['b', 6]],
|
||||||
|
'c': [['c', 3]],
|
||||||
|
};
|
||||||
|
const result = new ArrayUtil(input).groupBy(p => p[0]);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.value).toBeDefined();
|
||||||
|
|
||||||
|
const asObject = objectFromEntries(result.value.entries());
|
||||||
|
expect(asObject).toMatchObject(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('GroupedArray', () => {
|
||||||
|
it('should maintain the pointer to the given map', () => {
|
||||||
|
const input = new Map([
|
||||||
|
['a', [1, 2, 3]],
|
||||||
|
['b', [7, 8, 9]],
|
||||||
|
['c', [4, 5, 6]],
|
||||||
|
]);
|
||||||
|
const result = new GroupedArray(input);
|
||||||
|
expect(result.value).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ordering by the provided key order', () => {
|
||||||
|
const input = new Map([
|
||||||
|
['a', [1, 2, 3]],
|
||||||
|
['b', [7, 8, 9]], // note counting diff
|
||||||
|
['c', [4, 5, 6]],
|
||||||
|
]);
|
||||||
|
const output = [4, 5, 6, 1, 2, 3, 7, 8, 9];
|
||||||
|
const keyOrder = ['c', 'a', 'b']; // note weird order to cause the `output` to be strange
|
||||||
|
const result = new GroupedArray(input).orderBy(keyOrder);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.value).toBeDefined();
|
||||||
|
expect(result.value).toEqual(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
67
test/utils/enums-test.ts
Normal file
67
test/utils/enums-test.ts
Normal file
|
@ -0,0 +1,67 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {getEnumValues, isEnumValue} from "../../src/utils/enums";
|
||||||
|
|
||||||
|
enum TestStringEnum {
|
||||||
|
First = "__first__",
|
||||||
|
Second = "__second__",
|
||||||
|
}
|
||||||
|
|
||||||
|
enum TestNumberEnum {
|
||||||
|
FirstKey = 10,
|
||||||
|
SecondKey = 20,
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('enums', () => {
|
||||||
|
describe('getEnumValues', () => {
|
||||||
|
it('should work on string enums', () => {
|
||||||
|
const result = getEnumValues(TestStringEnum);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result).toEqual(['__first__', '__second__']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work on number enums', () => {
|
||||||
|
const result = getEnumValues(TestNumberEnum);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result).toEqual([10, 20]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isEnumValue', () => {
|
||||||
|
it('should return true on values in a string enum', () => {
|
||||||
|
const result = isEnumValue(TestStringEnum, '__first__');
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false on values not in a string enum', () => {
|
||||||
|
const result = isEnumValue(TestStringEnum, 'not a value');
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true on values in a number enum', () => {
|
||||||
|
const result = isEnumValue(TestNumberEnum, 10);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false on values not in a number enum', () => {
|
||||||
|
const result = isEnumValue(TestStringEnum, 99);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
77
test/utils/iterables-test.ts
Normal file
77
test/utils/iterables-test.ts
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {iterableDiff, iterableUnion} from "../../src/utils/iterables";
|
||||||
|
|
||||||
|
describe('iterables', () => {
|
||||||
|
describe('iterableUnion', () => {
|
||||||
|
it('should return a union', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 4]; // note diff
|
||||||
|
const result = iterableUnion(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(2);
|
||||||
|
expect(result).toEqual([1, 2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty array on no matches', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [4, 5, 6];
|
||||||
|
const result = iterableUnion(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('iterableDiff', () => {
|
||||||
|
it('should see added from A->B', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 3, 4];
|
||||||
|
const result = iterableDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.added).toEqual([4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see removed from A->B', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2];
|
||||||
|
const result = iterableDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(1);
|
||||||
|
expect(result.removed).toEqual([3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should see added and removed in the same set', () => {
|
||||||
|
const a = [1, 2, 3];
|
||||||
|
const b = [1, 2, 4]; // note diff
|
||||||
|
const result = iterableDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.removed).toHaveLength(1);
|
||||||
|
expect(result.added).toEqual([4]);
|
||||||
|
expect(result.removed).toEqual([3]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
245
test/utils/maps-test.ts
Normal file
245
test/utils/maps-test.ts
Normal file
|
@ -0,0 +1,245 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {EnhancedMap, mapDiff, mapKeyChanges} from "../../src/utils/maps";
|
||||||
|
|
||||||
|
describe('maps', () => {
|
||||||
|
describe('mapDiff', () => {
|
||||||
|
it('should indicate no differences when the pointers are the same', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const result = mapDiff(a, a);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.changed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate no differences when there are none', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const b = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const result = mapDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.changed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate added properties', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const b = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
|
||||||
|
const result = mapDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.changed).toHaveLength(0);
|
||||||
|
expect(result.added).toEqual([4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate removed properties', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const b = new Map([[1, 1], [2, 2]]);
|
||||||
|
const result = mapDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(1);
|
||||||
|
expect(result.changed).toHaveLength(0);
|
||||||
|
expect(result.removed).toEqual([3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate changed properties', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const b = new Map([[1, 1], [2, 2], [3, 4]]); // note change
|
||||||
|
const result = mapDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.changed).toHaveLength(1);
|
||||||
|
expect(result.changed).toEqual([3]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate changed, added, and removed properties', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const b = new Map([[1, 1], [2, 8], [4, 4]]); // note change
|
||||||
|
const result = mapDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.removed).toHaveLength(1);
|
||||||
|
expect(result.changed).toHaveLength(1);
|
||||||
|
expect(result.added).toEqual([4]);
|
||||||
|
expect(result.removed).toEqual([3]);
|
||||||
|
expect(result.changed).toEqual([2]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate changes for difference in pointers', () => {
|
||||||
|
const a = new Map([[1, {}]]); // {} always creates a new object
|
||||||
|
const b = new Map([[1, {}]]);
|
||||||
|
const result = mapDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.changed).toHaveLength(1);
|
||||||
|
expect(result.changed).toEqual([1]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapKeyChanges', () => {
|
||||||
|
it('should indicate no changes for unchanged pointers', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const result = mapKeyChanges(a, a);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate no changes for unchanged maps with different pointers', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const b = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const result = mapKeyChanges(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate changes for added properties', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const b = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
|
||||||
|
const result = mapKeyChanges(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result).toEqual([4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate changes for removed properties', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
|
||||||
|
const b = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const result = mapKeyChanges(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result).toEqual([4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate changes for changed properties', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3], [4, 4]]);
|
||||||
|
const b = new Map([[1, 1], [2, 2], [3, 3], [4, 55]]);
|
||||||
|
const result = mapKeyChanges(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result).toEqual([4]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate changes for properties with different pointers', () => {
|
||||||
|
const a = new Map([[1, {}]]); // {} always creates a new object
|
||||||
|
const b = new Map([[1, {}]]);
|
||||||
|
const result = mapKeyChanges(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(1);
|
||||||
|
expect(result).toEqual([1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate changes for changed, added, and removed properties', () => {
|
||||||
|
const a = new Map([[1, 1], [2, 2], [3, 3]]);
|
||||||
|
const b = new Map([[1, 1], [2, 8], [4, 4]]); // note change
|
||||||
|
const result = mapKeyChanges(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result).toEqual([3, 4, 2]); // order irrelevant, but the test cares
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('EnhancedMap', () => {
|
||||||
|
// Most of these tests will make sure it implements the Map<K, V> class
|
||||||
|
|
||||||
|
it('should be empty by default', () => {
|
||||||
|
const result = new EnhancedMap();
|
||||||
|
expect(result.size).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the provided entries', () => {
|
||||||
|
const obj = {a: 1, b: 2};
|
||||||
|
const result = new EnhancedMap(Object.entries(obj));
|
||||||
|
expect(result.size).toBe(2);
|
||||||
|
expect(result.get('a')).toBe(1);
|
||||||
|
expect(result.get('b')).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create keys if they do not exist', () => {
|
||||||
|
const key = 'a';
|
||||||
|
const val = {}; // we'll check pointers
|
||||||
|
|
||||||
|
const result = new EnhancedMap<string, any>();
|
||||||
|
expect(result.size).toBe(0);
|
||||||
|
|
||||||
|
let get = result.getOrCreate(key, val);
|
||||||
|
expect(get).toBeDefined();
|
||||||
|
expect(get).toBe(val);
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
|
||||||
|
get = result.getOrCreate(key, 44); // specifically change `val`
|
||||||
|
expect(get).toBeDefined();
|
||||||
|
expect(get).toBe(val);
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
|
||||||
|
get = result.get(key); // use the base class function
|
||||||
|
expect(get).toBeDefined();
|
||||||
|
expect(get).toBe(val);
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should proxy remove to delete and return it', () => {
|
||||||
|
const val = {};
|
||||||
|
const result = new EnhancedMap<string, any>();
|
||||||
|
result.set('a', val);
|
||||||
|
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
|
||||||
|
const removed = result.remove('a');
|
||||||
|
expect(result.size).toBe(0);
|
||||||
|
expect(removed).toBeDefined();
|
||||||
|
expect(removed).toBe(val);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support removing unknown keys', () => {
|
||||||
|
const val = {};
|
||||||
|
const result = new EnhancedMap<string, any>();
|
||||||
|
result.set('a', val);
|
||||||
|
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
|
||||||
|
const removed = result.remove('not-a');
|
||||||
|
expect(result.size).toBe(1);
|
||||||
|
expect(removed).not.toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
163
test/utils/numbers-test.ts
Normal file
163
test/utils/numbers-test.ts
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {clamp, defaultNumber, percentageOf, percentageWithin, sum} from "../../src/utils/numbers";
|
||||||
|
|
||||||
|
describe('numbers', () => {
|
||||||
|
describe('defaultNumber', () => {
|
||||||
|
it('should use the default when the input is not a number', () => {
|
||||||
|
const def = 42;
|
||||||
|
|
||||||
|
let result = defaultNumber(null, def);
|
||||||
|
expect(result).toBe(def);
|
||||||
|
|
||||||
|
result = defaultNumber(undefined, def);
|
||||||
|
expect(result).toBe(def);
|
||||||
|
|
||||||
|
result = defaultNumber(Number.NaN, def);
|
||||||
|
expect(result).toBe(def);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should use the number when it is a number', () => {
|
||||||
|
const input = 24;
|
||||||
|
const def = 42;
|
||||||
|
const result = defaultNumber(input, def);
|
||||||
|
expect(result).toBe(input);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('clamp', () => {
|
||||||
|
it('should clamp high numbers', () => {
|
||||||
|
const input = 101;
|
||||||
|
const min = 0;
|
||||||
|
const max = 100;
|
||||||
|
const result = clamp(input, min, max);
|
||||||
|
expect(result).toBe(max);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp low numbers', () => {
|
||||||
|
const input = -1;
|
||||||
|
const min = 0;
|
||||||
|
const max = 100;
|
||||||
|
const result = clamp(input, min, max);
|
||||||
|
expect(result).toBe(min);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not clamp numbers in range', () => {
|
||||||
|
const input = 50;
|
||||||
|
const min = 0;
|
||||||
|
const max = 100;
|
||||||
|
const result = clamp(input, min, max);
|
||||||
|
expect(result).toBe(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clamp floats', () => {
|
||||||
|
const min = -0.10;
|
||||||
|
const max = +0.10;
|
||||||
|
|
||||||
|
let result = clamp(-1.2, min, max);
|
||||||
|
expect(result).toBe(min);
|
||||||
|
|
||||||
|
result = clamp(1.2, min, max);
|
||||||
|
expect(result).toBe(max);
|
||||||
|
|
||||||
|
result = clamp(0.02, min, max);
|
||||||
|
expect(result).toBe(0.02);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('sum', () => {
|
||||||
|
it('should sum', () => { // duh
|
||||||
|
const result = sum(1, 2, 1, 4);
|
||||||
|
expect(result).toBe(8);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('percentageWithin', () => {
|
||||||
|
it('should work within 0-100', () => {
|
||||||
|
const result = percentageWithin(0.4, 0, 100);
|
||||||
|
expect(result).toBe(40);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work within 0-100 when pct > 1', () => {
|
||||||
|
const result = percentageWithin(1.4, 0, 100);
|
||||||
|
expect(result).toBe(140);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work within 0-100 when pct < 0', () => {
|
||||||
|
const result = percentageWithin(-1.4, 0, 100);
|
||||||
|
expect(result).toBe(-140);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with ranges other than 0-100', () => {
|
||||||
|
const result = percentageWithin(0.4, 10, 20);
|
||||||
|
expect(result).toBe(14);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with ranges other than 0-100 when pct > 1', () => {
|
||||||
|
const result = percentageWithin(1.4, 10, 20);
|
||||||
|
expect(result).toBe(24);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with ranges other than 0-100 when pct < 0', () => {
|
||||||
|
const result = percentageWithin(-1.4, 10, 20);
|
||||||
|
expect(result).toBe(-4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with floats', () => {
|
||||||
|
const result = percentageWithin(0.4, 10.2, 20.4);
|
||||||
|
expect(result).toBe(14.28);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// These are the inverse of percentageWithin
|
||||||
|
describe('percentageOf', () => {
|
||||||
|
it('should work within 0-100', () => {
|
||||||
|
const result = percentageOf(40, 0, 100);
|
||||||
|
expect(result).toBe(0.4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work within 0-100 when val > 100', () => {
|
||||||
|
const result = percentageOf(140, 0, 100);
|
||||||
|
expect(result).toBe(1.40);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work within 0-100 when val < 0', () => {
|
||||||
|
const result = percentageOf(-140, 0, 100);
|
||||||
|
expect(result).toBe(-1.40);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with ranges other than 0-100', () => {
|
||||||
|
const result = percentageOf(14, 10, 20);
|
||||||
|
expect(result).toBe(0.4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with ranges other than 0-100 when val > 100', () => {
|
||||||
|
const result = percentageOf(24, 10, 20);
|
||||||
|
expect(result).toBe(1.4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with ranges other than 0-100 when val < 0', () => {
|
||||||
|
const result = percentageOf(-4, 10, 20);
|
||||||
|
expect(result).toBe(-1.4);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should work with floats', () => {
|
||||||
|
const result = percentageOf(14.28, 10.2, 20.4);
|
||||||
|
expect(result).toBe(0.4);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
262
test/utils/objects-test.ts
Normal file
262
test/utils/objects-test.ts
Normal file
|
@ -0,0 +1,262 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {
|
||||||
|
objectClone,
|
||||||
|
objectDiff,
|
||||||
|
objectExcluding,
|
||||||
|
objectFromEntries,
|
||||||
|
objectHasDiff,
|
||||||
|
objectKeyChanges,
|
||||||
|
objectShallowClone,
|
||||||
|
objectWithOnly,
|
||||||
|
} from "../../src/utils/objects";
|
||||||
|
|
||||||
|
describe('objects', () => {
|
||||||
|
describe('objectExcluding', () => {
|
||||||
|
it('should exclude the given properties', () => {
|
||||||
|
const input = {hello: "world", test: true};
|
||||||
|
const output = {hello: "world"};
|
||||||
|
const props = ["test", "doesnotexist"]; // we also make sure it doesn't explode on missing props
|
||||||
|
const result = objectExcluding(input, <any>props); // any is to test the missing prop
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toMatchObject(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectWithOnly', () => {
|
||||||
|
it('should exclusively use the given properties', () => {
|
||||||
|
const input = {hello: "world", test: true};
|
||||||
|
const output = {hello: "world"};
|
||||||
|
const props = ["hello", "doesnotexist"]; // we also make sure it doesn't explode on missing props
|
||||||
|
const result = objectWithOnly(input, <any>props); // any is to test the missing prop
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toMatchObject(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectShallowClone', () => {
|
||||||
|
it('should create a new object', () => {
|
||||||
|
const input = {test: 1};
|
||||||
|
const result = objectShallowClone(input);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).not.toBe(input);
|
||||||
|
expect(result).toMatchObject(input);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should only clone the top level properties', () => {
|
||||||
|
const input = {a: 1, b: {c: 2}};
|
||||||
|
const result = objectShallowClone(input);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toMatchObject(input);
|
||||||
|
expect(result.b).toBe(input.b);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support custom clone functions', () => {
|
||||||
|
const input = {a: 1, b: 2};
|
||||||
|
const output = {a: 4, b: 8};
|
||||||
|
const result = objectShallowClone(input, (k, v) => {
|
||||||
|
// XXX: inverted expectation for ease of assertion
|
||||||
|
expect(Object.keys(input)).toContain(k);
|
||||||
|
|
||||||
|
return v * 4;
|
||||||
|
});
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toMatchObject(output);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectHasDiff', () => {
|
||||||
|
it('should return false for the same pointer', () => {
|
||||||
|
const a = {};
|
||||||
|
const result = objectHasDiff(a, a);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if keys for A > keys for B', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const b = {a: 1};
|
||||||
|
const result = objectHasDiff(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true if keys for A < keys for B', () => {
|
||||||
|
const a = {a: 1};
|
||||||
|
const b = {a: 1, b: 2};
|
||||||
|
const result = objectHasDiff(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the objects are the same but different pointers', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const b = {a: 1, b: 2};
|
||||||
|
const result = objectHasDiff(a, b);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should consider pointers when testing values', () => {
|
||||||
|
const a = {a: {}, b: 2}; // `{}` is shorthand for `new Object()`
|
||||||
|
const b = {a: {}, b: 2};
|
||||||
|
const result = objectHasDiff(a, b);
|
||||||
|
expect(result).toBe(true); // even though the keys are the same, the value pointers vary
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectDiff', () => {
|
||||||
|
it('should return empty sets for the same object', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const b = {a: 1, b: 2};
|
||||||
|
const result = objectDiff(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toHaveLength(0);
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return empty sets for the same object pointer', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const result = objectDiff(a, a);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toHaveLength(0);
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate when property changes are made', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const b = {a: 11, b: 2};
|
||||||
|
const result = objectDiff(a, b);
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toHaveLength(1);
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.changed).toEqual(['a']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate when properties are added', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const b = {a: 1, b: 2, c: 3};
|
||||||
|
const result = objectDiff(a, b);
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toHaveLength(0);
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.removed).toHaveLength(0);
|
||||||
|
expect(result.added).toEqual(['c']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate when properties are removed', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const b = {a: 1};
|
||||||
|
const result = objectDiff(a, b);
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toHaveLength(0);
|
||||||
|
expect(result.added).toHaveLength(0);
|
||||||
|
expect(result.removed).toHaveLength(1);
|
||||||
|
expect(result.removed).toEqual(['b']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should indicate when multiple aspects change', () => {
|
||||||
|
const a = {a: 1, b: 2, c: 3};
|
||||||
|
const b: (typeof a | {d: number}) = {a: 1, b: 22, d: 4};
|
||||||
|
const result = objectDiff(a, b);
|
||||||
|
expect(result.changed).toBeDefined();
|
||||||
|
expect(result.added).toBeDefined();
|
||||||
|
expect(result.removed).toBeDefined();
|
||||||
|
expect(result.changed).toHaveLength(1);
|
||||||
|
expect(result.added).toHaveLength(1);
|
||||||
|
expect(result.removed).toHaveLength(1);
|
||||||
|
expect(result.changed).toEqual(['b']);
|
||||||
|
expect(result.removed).toEqual(['c']);
|
||||||
|
expect(result.added).toEqual(['d']);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectKeyChanges', () => {
|
||||||
|
it('should return an empty set if no properties changed', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const b = {a: 1, b: 2};
|
||||||
|
const result = objectKeyChanges(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an empty set if no properties changed for the same pointer', () => {
|
||||||
|
const a = {a: 1, b: 2};
|
||||||
|
const result = objectKeyChanges(a, a);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return properties which were changed, added, or removed', () => {
|
||||||
|
const a = {a: 1, b: 2, c: 3};
|
||||||
|
const b: (typeof a | {d: number}) = {a: 1, b: 22, d: 4};
|
||||||
|
const result = objectKeyChanges(a, b);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toHaveLength(3);
|
||||||
|
expect(result).toEqual(['c', 'd', 'b']); // order isn't important, but the test cares
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectClone', () => {
|
||||||
|
it('should deep clone an object', () => {
|
||||||
|
const a = {
|
||||||
|
hello: "world",
|
||||||
|
test: {
|
||||||
|
another: "property",
|
||||||
|
test: 42,
|
||||||
|
third: {
|
||||||
|
prop: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const result = objectClone(a);
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).not.toBe(a);
|
||||||
|
expect(result).toMatchObject(a);
|
||||||
|
expect(result.test).not.toBe(a.test);
|
||||||
|
expect(result.test.third).not.toBe(a.test.third);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('objectFromEntries', () => {
|
||||||
|
it('should create an object from an array of entries', () => {
|
||||||
|
const output = {a: 1, b: 2, c: 3};
|
||||||
|
const result = objectFromEntries(Object.entries(output));
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toMatchObject(output);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should maintain pointers in values', () => {
|
||||||
|
const output = {a: {}, b: 2, c: 3};
|
||||||
|
const result = objectFromEntries(Object.entries(output));
|
||||||
|
expect(result).toBeDefined();
|
||||||
|
expect(result).toMatchObject(output);
|
||||||
|
expect(result['a']).toBe(output.a);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
56
test/utils/sets-test.ts
Normal file
56
test/utils/sets-test.ts
Normal file
|
@ -0,0 +1,56 @@
|
||||||
|
/*
|
||||||
|
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import {setHasDiff} from "../../src/utils/sets";
|
||||||
|
|
||||||
|
describe('sets', () => {
|
||||||
|
describe('setHasDiff', () => {
|
||||||
|
it('should flag true on A length > B length', () => {
|
||||||
|
const a = new Set([1, 2, 3, 4]);
|
||||||
|
const b = new Set([1, 2, 3]);
|
||||||
|
const result = setHasDiff(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag true on A length < B length', () => {
|
||||||
|
const a = new Set([1, 2, 3]);
|
||||||
|
const b = new Set([1, 2, 3, 4]);
|
||||||
|
const result = setHasDiff(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag true on element differences', () => {
|
||||||
|
const a = new Set([1, 2, 3]);
|
||||||
|
const b = new Set([4, 5, 6]);
|
||||||
|
const result = setHasDiff(a, b);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag false if same but order different', () => {
|
||||||
|
const a = new Set([1, 2, 3]);
|
||||||
|
const b = new Set([3, 1, 2]);
|
||||||
|
const result = setHasDiff(a, b);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flag false if same', () => {
|
||||||
|
const a = new Set([1, 2, 3]);
|
||||||
|
const b = new Set([1, 2, 3]);
|
||||||
|
const result = setHasDiff(a, b);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in a new issue