mirror of
https://github.com/matrix-org/matrix-js-sdk.git
synced 2025-06-08 15:21:53 +03:00
* Ensure we disambiguate display names which look like MXIDs Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Make tests clearer Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
760 lines
28 KiB
TypeScript
760 lines
28 KiB
TypeScript
/*
|
||
Copyright 2023 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 * as utils from "../../src/utils";
|
||
import {
|
||
alphabetPad,
|
||
averageBetweenStrings,
|
||
baseToString,
|
||
deepSortedObjectEntries,
|
||
DEFAULT_ALPHABET,
|
||
lexicographicCompare,
|
||
nextString,
|
||
prevString,
|
||
recursiveMapToObject,
|
||
simpleRetryOperation,
|
||
stringToBase,
|
||
sortEventsByLatestContentTimestamp,
|
||
safeSet,
|
||
MapWithDefault,
|
||
globToRegexp,
|
||
escapeRegExp,
|
||
removeHiddenChars,
|
||
} from "../../src/utils";
|
||
import { logger } from "../../src/logger";
|
||
import { mkMessage } from "../test-utils/test-utils";
|
||
import { makeBeaconEvent } from "../test-utils/beacon";
|
||
import { ReceiptType } from "../../src/@types/read_receipts";
|
||
|
||
describe("utils", function () {
|
||
describe("encodeParams", function () {
|
||
it("should url encode and concat with &s", function () {
|
||
const params = {
|
||
foo: "bar",
|
||
baz: "beer@",
|
||
};
|
||
expect(utils.encodeParams(params).toString()).toEqual("foo=bar&baz=beer%40");
|
||
});
|
||
|
||
it("should handle boolean and numeric values", function () {
|
||
const params = {
|
||
string: "foobar",
|
||
number: 12345,
|
||
boolean: false,
|
||
};
|
||
expect(utils.encodeParams(params).toString()).toEqual("string=foobar&number=12345&boolean=false");
|
||
});
|
||
|
||
it("should handle string arrays", () => {
|
||
const params = {
|
||
via: ["one", "two", "three"],
|
||
};
|
||
expect(utils.encodeParams(params).toString()).toEqual("via=one&via=two&via=three");
|
||
});
|
||
});
|
||
|
||
describe("decodeParams", () => {
|
||
it("should be able to decode multiple values into an array", () => {
|
||
const params = "foo=bar&via=a&via=b&via=c";
|
||
expect(utils.decodeParams(params)).toEqual({
|
||
foo: "bar",
|
||
via: ["a", "b", "c"],
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("encodeUri", function () {
|
||
it("should replace based on object keys and url encode", function () {
|
||
const path = "foo/bar/%something/%here";
|
||
const vals = {
|
||
"%something": "baz",
|
||
"%here": "beer@",
|
||
};
|
||
expect(utils.encodeUri(path, vals)).toEqual("foo/bar/baz/beer%40");
|
||
});
|
||
});
|
||
|
||
describe("removeElement", function () {
|
||
it("should remove only 1 element if there is a match", function () {
|
||
const matchFn = function () {
|
||
return true;
|
||
};
|
||
const arr = [55, 66, 77];
|
||
utils.removeElement(arr, matchFn);
|
||
expect(arr).toEqual([66, 77]);
|
||
});
|
||
it("should be able to remove in reverse order", function () {
|
||
const matchFn = function () {
|
||
return true;
|
||
};
|
||
const arr = [55, 66, 77];
|
||
utils.removeElement(arr, matchFn, true);
|
||
expect(arr).toEqual([55, 66]);
|
||
});
|
||
it("should remove nothing if the function never returns true", function () {
|
||
const matchFn = function () {
|
||
return false;
|
||
};
|
||
const arr = [55, 66, 77];
|
||
utils.removeElement(arr, matchFn);
|
||
expect(arr).toEqual(arr);
|
||
});
|
||
});
|
||
|
||
describe("isFunction", function () {
|
||
it("should return true for functions", function () {
|
||
expect(utils.isFunction([])).toBe(false);
|
||
expect(utils.isFunction([5, 3, 7])).toBe(false);
|
||
expect(utils.isFunction(undefined)).toBe(false);
|
||
expect(utils.isFunction(null)).toBe(false);
|
||
expect(utils.isFunction({})).toBe(false);
|
||
expect(utils.isFunction("foo")).toBe(false);
|
||
expect(utils.isFunction(555)).toBe(false);
|
||
|
||
expect(utils.isFunction(function () {})).toBe(true);
|
||
const s = { foo: function () {} };
|
||
expect(utils.isFunction(s.foo)).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("checkObjectHasKeys", function () {
|
||
it("should throw for missing keys", function () {
|
||
expect(function () {
|
||
utils.checkObjectHasKeys({}, ["foo"]);
|
||
}).toThrow();
|
||
expect(function () {
|
||
utils.checkObjectHasKeys(
|
||
{
|
||
foo: "bar",
|
||
},
|
||
["foo"],
|
||
);
|
||
}).not.toThrow();
|
||
});
|
||
});
|
||
|
||
describe("deepCompare", function () {
|
||
const assert = {
|
||
isTrue: function (x: any) {
|
||
// eslint-disable-next-line jest/no-standalone-expect
|
||
expect(x).toBe(true);
|
||
},
|
||
isFalse: function (x: any) {
|
||
// eslint-disable-next-line jest/no-standalone-expect
|
||
expect(x).toBe(false);
|
||
},
|
||
};
|
||
|
||
it("should handle primitives", function () {
|
||
assert.isTrue(utils.deepCompare(null, null));
|
||
assert.isFalse(utils.deepCompare(null, undefined));
|
||
assert.isTrue(utils.deepCompare("hi", "hi"));
|
||
assert.isTrue(utils.deepCompare(5, 5));
|
||
assert.isFalse(utils.deepCompare(5, 10));
|
||
});
|
||
|
||
it("should handle regexps", function () {
|
||
assert.isTrue(utils.deepCompare(/abc/, /abc/));
|
||
assert.isFalse(utils.deepCompare(/abc/, /123/));
|
||
const r = /abc/;
|
||
assert.isTrue(utils.deepCompare(r, r));
|
||
});
|
||
|
||
it("should handle dates", function () {
|
||
assert.isTrue(utils.deepCompare(new Date("2011-03-31"), new Date("2011-03-31")));
|
||
assert.isFalse(utils.deepCompare(new Date("2011-03-31"), new Date("1970-01-01")));
|
||
});
|
||
|
||
it("should handle arrays", function () {
|
||
assert.isTrue(utils.deepCompare([], []));
|
||
assert.isTrue(utils.deepCompare([1, 2], [1, 2]));
|
||
assert.isFalse(utils.deepCompare([1, 2], [2, 1]));
|
||
assert.isFalse(utils.deepCompare([1, 2], [1, 2, 3]));
|
||
});
|
||
|
||
it("should handle simple objects", function () {
|
||
assert.isTrue(utils.deepCompare({}, {}));
|
||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 2 }));
|
||
assert.isTrue(utils.deepCompare({ a: 1, b: 2 }, { b: 2, a: 1 }));
|
||
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1, b: 3 }));
|
||
assert.isFalse(utils.deepCompare({ a: 1, b: 2 }, { a: 1 }));
|
||
assert.isFalse(utils.deepCompare({ a: 1 }, { a: 1, b: 2 }));
|
||
assert.isFalse(utils.deepCompare({ a: 1 }, { b: 1 }));
|
||
|
||
assert.isTrue(
|
||
utils.deepCompare(
|
||
{
|
||
1: { name: "mhc", age: 28 },
|
||
2: { name: "arb", age: 26 },
|
||
},
|
||
{
|
||
1: { name: "mhc", age: 28 },
|
||
2: { name: "arb", age: 26 },
|
||
},
|
||
),
|
||
);
|
||
|
||
assert.isFalse(
|
||
utils.deepCompare(
|
||
{
|
||
1: { name: "mhc", age: 28 },
|
||
2: { name: "arb", age: 26 },
|
||
},
|
||
{
|
||
1: { name: "mhc", age: 28 },
|
||
2: { name: "arb", age: 27 },
|
||
},
|
||
),
|
||
);
|
||
|
||
assert.isFalse(utils.deepCompare({}, null));
|
||
assert.isFalse(utils.deepCompare({}, undefined));
|
||
});
|
||
|
||
it("should handle functions", function () {
|
||
// no two different function is equal really, they capture their
|
||
// context variables so even if they have same toString(), they
|
||
// won't have same functionality
|
||
const func = function () {
|
||
return true;
|
||
};
|
||
const func2 = function () {
|
||
return true;
|
||
};
|
||
assert.isTrue(utils.deepCompare(func, func));
|
||
assert.isFalse(utils.deepCompare(func, func2));
|
||
assert.isTrue(utils.deepCompare({ a: { b: func } }, { a: { b: func } }));
|
||
assert.isFalse(utils.deepCompare({ a: { b: func } }, { a: { b: func2 } }));
|
||
});
|
||
});
|
||
|
||
describe("chunkPromises", function () {
|
||
it("should execute promises in chunks", async function () {
|
||
let promiseCount = 0;
|
||
|
||
async function fn1() {
|
||
await utils.sleep(1);
|
||
expect(promiseCount).toEqual(0);
|
||
++promiseCount;
|
||
}
|
||
|
||
async function fn2() {
|
||
expect(promiseCount).toEqual(1);
|
||
++promiseCount;
|
||
}
|
||
|
||
await utils.chunkPromises([fn1, fn2], 1);
|
||
expect(promiseCount).toEqual(2);
|
||
});
|
||
});
|
||
|
||
describe("simpleRetryOperation", () => {
|
||
it("should retry", async () => {
|
||
let count = 0;
|
||
const val = {};
|
||
const fn = (attempt: any) => {
|
||
count++;
|
||
|
||
// If this expectation fails then it can appear as a Jest Timeout due to
|
||
// the retry running beyond the test limit.
|
||
expect(attempt).toEqual(count);
|
||
|
||
if (count > 1) {
|
||
return Promise.resolve(val);
|
||
} else {
|
||
return Promise.reject(new Error("Iterative failure"));
|
||
}
|
||
};
|
||
|
||
const ret = await simpleRetryOperation(fn);
|
||
expect(ret).toBe(val);
|
||
expect(count).toEqual(2);
|
||
});
|
||
|
||
// We don't test much else of the function because then we're just testing that the
|
||
// underlying library behaves, which should be tested on its own. Our API surface is
|
||
// all that concerns us.
|
||
});
|
||
|
||
describe("DEFAULT_ALPHABET", () => {
|
||
it("should be usefully printable ASCII in order", () => {
|
||
expect(DEFAULT_ALPHABET).toEqual(
|
||
" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~",
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("alphabetPad", () => {
|
||
it("should pad to the alphabet length", () => {
|
||
const len = 12;
|
||
expect(alphabetPad("a", len)).toEqual("a" + "".padEnd(len - 1, DEFAULT_ALPHABET[0]));
|
||
expect(alphabetPad("a", len, "123")).toEqual("a" + "".padEnd(len - 1, "1"));
|
||
});
|
||
});
|
||
|
||
describe("baseToString", () => {
|
||
it("should calculate the appropriate string from numbers", () => {
|
||
// Verify the whole alphabet
|
||
for (let i = BigInt(1); i <= DEFAULT_ALPHABET.length; i++) {
|
||
logger.log({ i }); // for debugging
|
||
expect(baseToString(i)).toEqual(DEFAULT_ALPHABET[Number(i) - 1]);
|
||
}
|
||
|
||
// Just quickly double check that repeated characters aren't treated as padding, particularly
|
||
// at the beginning of the alphabet where they are most vulnerable to this behaviour.
|
||
expect(baseToString(BigInt(1))).toEqual(DEFAULT_ALPHABET[0].repeat(1));
|
||
expect(baseToString(BigInt(96))).toEqual(DEFAULT_ALPHABET[0].repeat(2));
|
||
expect(baseToString(BigInt(9121))).toEqual(DEFAULT_ALPHABET[0].repeat(3));
|
||
expect(baseToString(BigInt(866496))).toEqual(DEFAULT_ALPHABET[0].repeat(4));
|
||
expect(baseToString(BigInt(82317121))).toEqual(DEFAULT_ALPHABET[0].repeat(5));
|
||
expect(baseToString(BigInt(7820126496))).toEqual(DEFAULT_ALPHABET[0].repeat(6));
|
||
|
||
expect(baseToString(BigInt(10))).toEqual(DEFAULT_ALPHABET[9]);
|
||
expect(baseToString(BigInt(10), "abcdefghijklmnopqrstuvwxyz")).toEqual("j");
|
||
expect(baseToString(BigInt(6337))).toEqual("ab");
|
||
expect(baseToString(BigInt(80), "abcdefghijklmnopqrstuvwxyz")).toEqual("cb");
|
||
});
|
||
});
|
||
|
||
describe("stringToBase", () => {
|
||
it("should calculate the appropriate number for a string", () => {
|
||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(1))).toEqual(BigInt(1));
|
||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(2))).toEqual(BigInt(96));
|
||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(3))).toEqual(BigInt(9121));
|
||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(4))).toEqual(BigInt(866496));
|
||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(5))).toEqual(BigInt(82317121));
|
||
expect(stringToBase(DEFAULT_ALPHABET[0].repeat(6))).toEqual(BigInt(7820126496));
|
||
expect(stringToBase("a", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(1));
|
||
expect(stringToBase("a")).toEqual(BigInt(66));
|
||
expect(stringToBase("c", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(3));
|
||
expect(stringToBase("ab")).toEqual(BigInt(6337));
|
||
expect(stringToBase("cb", "abcdefghijklmnopqrstuvwxyz")).toEqual(BigInt(80));
|
||
});
|
||
});
|
||
|
||
describe("averageBetweenStrings", () => {
|
||
it("should average appropriately", () => {
|
||
expect(averageBetweenStrings(" ", "!!")).toEqual(" P");
|
||
expect(averageBetweenStrings(" ", "!")).toEqual(" ");
|
||
expect(averageBetweenStrings("A", "B")).toEqual("A ");
|
||
expect(averageBetweenStrings("AA", "BB")).toEqual("Aq");
|
||
expect(averageBetweenStrings("A", "z")).toEqual("]");
|
||
expect(averageBetweenStrings("a", "z", "abcdefghijklmnopqrstuvwxyz")).toEqual("m");
|
||
expect(averageBetweenStrings("AA", "zz")).toEqual("^.");
|
||
expect(averageBetweenStrings("aa", "zz", "abcdefghijklmnopqrstuvwxyz")).toEqual("mz");
|
||
expect(averageBetweenStrings("cat", "doggo")).toEqual("d9>Cw");
|
||
expect(averageBetweenStrings("cat", "doggo", "abcdefghijklmnopqrstuvwxyz")).toEqual("cumqh");
|
||
});
|
||
});
|
||
|
||
describe("nextString", () => {
|
||
it("should find the next string appropriately", () => {
|
||
expect(nextString("A")).toEqual("B");
|
||
expect(nextString("b", "abcdefghijklmnopqrstuvwxyz")).toEqual("c");
|
||
expect(nextString("cat")).toEqual("cau");
|
||
expect(nextString("cat", "abcdefghijklmnopqrstuvwxyz")).toEqual("cau");
|
||
});
|
||
});
|
||
|
||
describe("prevString", () => {
|
||
it("should find the next string appropriately", () => {
|
||
expect(prevString("B")).toEqual("A");
|
||
expect(prevString("c", "abcdefghijklmnopqrstuvwxyz")).toEqual("b");
|
||
expect(prevString("cau")).toEqual("cat");
|
||
expect(prevString("cau", "abcdefghijklmnopqrstuvwxyz")).toEqual("cat");
|
||
});
|
||
});
|
||
|
||
// Let's just ensure the ordering is sensible for lexicographic ordering
|
||
describe("string averaging unified", () => {
|
||
it("should be truly previous and next", () => {
|
||
let midpoint = "cat";
|
||
|
||
// We run this test 100 times to ensure we end up with a sane sequence.
|
||
for (let i = 0; i < 100; i++) {
|
||
const next = nextString(midpoint);
|
||
const prev = prevString(midpoint);
|
||
logger.log({ i, midpoint, next, prev }); // for test debugging
|
||
|
||
expect(lexicographicCompare(midpoint, next) < 0).toBe(true);
|
||
expect(lexicographicCompare(midpoint, prev) > 0).toBe(true);
|
||
expect(averageBetweenStrings(prev, next)).toBe(midpoint);
|
||
|
||
midpoint = next;
|
||
}
|
||
});
|
||
|
||
it("should roll over", () => {
|
||
const lastAlpha = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1];
|
||
const firstAlpha = DEFAULT_ALPHABET[0];
|
||
|
||
const highRoll = firstAlpha + firstAlpha;
|
||
const lowRoll = lastAlpha;
|
||
|
||
expect(nextString(lowRoll)).toEqual(highRoll);
|
||
expect(prevString(highRoll)).toEqual(lowRoll);
|
||
});
|
||
|
||
it("should be reversible on small strings", () => {
|
||
// Large scale reversibility is tested for max space order value
|
||
const input = "cats";
|
||
expect(prevString(nextString(input))).toEqual(input);
|
||
});
|
||
|
||
// We want to explicitly make sure that Space order values are supported and roll appropriately
|
||
it("should properly handle rolling over at 50 characters", () => {
|
||
// Note: we also test reversibility of large strings here.
|
||
|
||
const maxSpaceValue = DEFAULT_ALPHABET[DEFAULT_ALPHABET.length - 1].repeat(50);
|
||
const fiftyFirstChar = DEFAULT_ALPHABET[0].repeat(51);
|
||
|
||
expect(nextString(maxSpaceValue)).toBe(fiftyFirstChar);
|
||
expect(prevString(fiftyFirstChar)).toBe(maxSpaceValue);
|
||
|
||
// We're testing that the rollover happened, which means that the next string come before
|
||
// the maximum space order value lexicographically.
|
||
expect(lexicographicCompare(maxSpaceValue, fiftyFirstChar) > 0).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("lexicographicCompare", () => {
|
||
it("should work", () => {
|
||
// Simple tests
|
||
expect(lexicographicCompare("a", "b") < 0).toBe(true);
|
||
expect(lexicographicCompare("ab", "b") < 0).toBe(true);
|
||
expect(lexicographicCompare("cat", "dog") < 0).toBe(true);
|
||
|
||
// Simple tests (reversed)
|
||
expect(lexicographicCompare("b", "a") > 0).toBe(true);
|
||
expect(lexicographicCompare("b", "ab") > 0).toBe(true);
|
||
expect(lexicographicCompare("dog", "cat") > 0).toBe(true);
|
||
|
||
// Simple equality tests
|
||
expect(lexicographicCompare("a", "a") === 0).toBe(true);
|
||
expect(lexicographicCompare("A", "A") === 0).toBe(true);
|
||
|
||
// ASCII rule testing
|
||
expect(lexicographicCompare("A", "a") < 0).toBe(true);
|
||
expect(lexicographicCompare("a", "A") > 0).toBe(true);
|
||
});
|
||
});
|
||
|
||
describe("deepSortedObjectEntries", () => {
|
||
it("should auto-return non-objects", () => {
|
||
expect(deepSortedObjectEntries(42)).toEqual(42);
|
||
expect(deepSortedObjectEntries("not object")).toEqual("not object");
|
||
expect(deepSortedObjectEntries(true)).toEqual(true);
|
||
expect(deepSortedObjectEntries([42])).toEqual([42]);
|
||
expect(deepSortedObjectEntries(null)).toEqual(null);
|
||
expect(deepSortedObjectEntries(undefined)).toEqual(undefined);
|
||
});
|
||
|
||
it("should sort objects appropriately", () => {
|
||
const input = {
|
||
a: 42,
|
||
b: {
|
||
d: {},
|
||
a: "test",
|
||
b: "alpha",
|
||
},
|
||
[72]: "test",
|
||
};
|
||
const output: any = [
|
||
["72", "test"],
|
||
["a", 42],
|
||
[
|
||
"b",
|
||
[
|
||
["a", "test"],
|
||
["b", "alpha"],
|
||
["d", []],
|
||
],
|
||
],
|
||
];
|
||
|
||
expect(deepSortedObjectEntries(input)).toMatchObject(output);
|
||
});
|
||
});
|
||
|
||
describe("recursivelyAssign", () => {
|
||
it("doesn't override with null/undefined", () => {
|
||
const result = utils.recursivelyAssign<
|
||
{
|
||
string: string;
|
||
object: object;
|
||
float: number;
|
||
},
|
||
{}
|
||
>(
|
||
{
|
||
string: "Hello world",
|
||
object: {},
|
||
float: 0.1,
|
||
},
|
||
{
|
||
string: null,
|
||
object: undefined,
|
||
},
|
||
true,
|
||
);
|
||
|
||
expect(result).toStrictEqual({
|
||
string: "Hello world",
|
||
object: {},
|
||
float: 0.1,
|
||
});
|
||
});
|
||
|
||
it("assigns recursively", () => {
|
||
const result = utils.recursivelyAssign<
|
||
{
|
||
number: number;
|
||
object: object;
|
||
thing: string | object;
|
||
},
|
||
{}
|
||
>(
|
||
{
|
||
number: 42,
|
||
object: {
|
||
message: "Hello world",
|
||
day: "Monday",
|
||
langs: {
|
||
compiled: ["c++"],
|
||
},
|
||
},
|
||
thing: "string",
|
||
},
|
||
{
|
||
number: 2,
|
||
object: {
|
||
message: "How are you",
|
||
day: "Friday",
|
||
langs: {
|
||
compiled: ["c++", "c"],
|
||
},
|
||
},
|
||
thing: {
|
||
aSubThing: "something",
|
||
},
|
||
},
|
||
);
|
||
|
||
expect(result).toStrictEqual({
|
||
number: 2,
|
||
object: {
|
||
message: "How are you",
|
||
day: "Friday",
|
||
langs: {
|
||
compiled: ["c++", "c"],
|
||
},
|
||
},
|
||
thing: {
|
||
aSubThing: "something",
|
||
},
|
||
});
|
||
});
|
||
});
|
||
|
||
describe("sortEventsByLatestContentTimestamp", () => {
|
||
const roomId = "!room:server";
|
||
const userId = "@user:server";
|
||
const eventWithoutContentTimestamp = mkMessage({ room: roomId, user: userId, event: true });
|
||
// m.beacon events have timestamp in content
|
||
const beaconEvent1 = makeBeaconEvent(userId, { timestamp: 1648804528557 });
|
||
const beaconEvent2 = makeBeaconEvent(userId, { timestamp: 1648804528558 });
|
||
const beaconEvent3 = makeBeaconEvent(userId, { timestamp: 1648804528000 });
|
||
const beaconEvent4 = makeBeaconEvent(userId, { timestamp: 0 });
|
||
|
||
it("sorts events with timestamps as later than events without", () => {
|
||
expect(
|
||
[beaconEvent4, eventWithoutContentTimestamp, beaconEvent1].sort(
|
||
utils.sortEventsByLatestContentTimestamp,
|
||
),
|
||
).toEqual([beaconEvent1, beaconEvent4, eventWithoutContentTimestamp]);
|
||
});
|
||
|
||
it("sorts by content timestamps correctly", () => {
|
||
expect([beaconEvent1, beaconEvent2, beaconEvent3].sort(sortEventsByLatestContentTimestamp)).toEqual([
|
||
beaconEvent2,
|
||
beaconEvent1,
|
||
beaconEvent3,
|
||
]);
|
||
});
|
||
});
|
||
|
||
describe("isSupportedReceiptType", () => {
|
||
it("should support m.read", () => {
|
||
expect(utils.isSupportedReceiptType(ReceiptType.Read)).toBeTruthy();
|
||
});
|
||
|
||
it("should support m.read.private", () => {
|
||
expect(utils.isSupportedReceiptType(ReceiptType.ReadPrivate)).toBeTruthy();
|
||
});
|
||
|
||
it("should not support other receipt types", () => {
|
||
expect(utils.isSupportedReceiptType("this is a receipt type")).toBeFalsy();
|
||
});
|
||
});
|
||
|
||
describe("recursiveMapToObject", () => {
|
||
it.each([
|
||
// empty map
|
||
{
|
||
map: new Map(),
|
||
expected: {},
|
||
},
|
||
// one level map
|
||
{
|
||
map: new Map<any, any>([
|
||
["key1", "value 1"],
|
||
["key2", 23],
|
||
["key3", undefined],
|
||
["key4", null],
|
||
["key5", [1, 2, 3]],
|
||
]),
|
||
expected: { key1: "value 1", key2: 23, key3: undefined, key4: null, key5: [1, 2, 3] },
|
||
},
|
||
// two level map
|
||
{
|
||
map: new Map<any, any>([
|
||
[
|
||
"key1",
|
||
new Map<any, any>([
|
||
["key1_1", "value 1"],
|
||
["key1_2", "value 1.2"],
|
||
]),
|
||
],
|
||
["key2", "value 2"],
|
||
]),
|
||
expected: { key1: { key1_1: "value 1", key1_2: "value 1.2" }, key2: "value 2" },
|
||
},
|
||
// multi level map
|
||
{
|
||
map: new Map<any, any>([
|
||
["key1", new Map<any, any>([["key1_1", new Map<any, any>([["key1_1_1", "value 1.1.1"]])]])],
|
||
]),
|
||
expected: { key1: { key1_1: { key1_1_1: "value 1.1.1" } } },
|
||
},
|
||
// list of maps
|
||
{
|
||
map: new Map<any, any>([
|
||
[
|
||
"key1",
|
||
[new Map<any, any>([["key1_1", "value 1.1"]]), new Map<any, any>([["key1_2", "value 1.2"]])],
|
||
],
|
||
]),
|
||
expected: { key1: [{ key1_1: "value 1.1" }, { key1_2: "value 1.2" }] },
|
||
},
|
||
// map → array → array → map
|
||
{
|
||
map: new Map<any, any>([["key1", [[new Map<any, any>([["key2", "value 2"]])]]]]),
|
||
expected: {
|
||
key1: [
|
||
[
|
||
{
|
||
key2: "value 2",
|
||
},
|
||
],
|
||
],
|
||
},
|
||
},
|
||
])("%# should convert the value", ({ map, expected }) => {
|
||
expect(recursiveMapToObject(map)).toStrictEqual(expected);
|
||
});
|
||
});
|
||
|
||
describe("safeSet", () => {
|
||
it("should set a value", () => {
|
||
const obj: Record<string, string> = {};
|
||
safeSet(obj, "testProp", "test value");
|
||
expect(obj).toEqual({ testProp: "test value" });
|
||
});
|
||
|
||
it.each(["__proto__", "prototype", "constructor"])("should raise an error when setting »%s«", (prop) => {
|
||
expect(() => {
|
||
safeSet(<Record<string, string>>{}, prop, "teset value");
|
||
}).toThrow("Trying to modify prototype or constructor");
|
||
});
|
||
});
|
||
|
||
describe("MapWithDefault", () => {
|
||
it("getOrCreate should create the value if it does not exist", () => {
|
||
const newValue = {};
|
||
const map = new MapWithDefault(() => newValue);
|
||
|
||
// undefined before getOrCreate
|
||
expect(map.get("test")).toBeUndefined();
|
||
|
||
expect(map.getOrCreate("test")).toBe(newValue);
|
||
|
||
// default value after getOrCreate
|
||
expect(map.get("test")).toBe(newValue);
|
||
|
||
// test that it always returns the same value
|
||
expect(map.getOrCreate("test")).toBe(newValue);
|
||
});
|
||
});
|
||
|
||
describe("sleep", () => {
|
||
it("resolves", async () => {
|
||
await utils.sleep(0);
|
||
});
|
||
|
||
it("resolves with the provided value", async () => {
|
||
const expected = Symbol("hi");
|
||
const result = await utils.sleep(0, expected);
|
||
expect(result).toBe(expected);
|
||
});
|
||
});
|
||
|
||
describe("immediate", () => {
|
||
it("resolves", async () => {
|
||
await utils.immediate();
|
||
});
|
||
});
|
||
|
||
describe("escapeRegExp", () => {
|
||
it("should escape XYZ", () => {
|
||
expect(escapeRegExp("[FIT-Connect Zustelldienst \\(Testumgebung\\)]")).toMatchInlineSnapshot(
|
||
`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`,
|
||
);
|
||
});
|
||
});
|
||
|
||
describe("globToRegexp", () => {
|
||
it("should not explode when given regexes as globs", () => {
|
||
const result = globToRegexp("[FIT-Connect Zustelldienst \\(Testumgebung\\)]");
|
||
expect(result).toMatchInlineSnapshot(`"\\[FIT-Connect Zustelldienst \\\\\\(Testumgebung\\\\\\)\\]"`);
|
||
});
|
||
});
|
||
|
||
describe("removeHiddenChars", () => {
|
||
it.each([
|
||
["various width spaces U+2000 - U+200D", "\u2000\u200D", ""],
|
||
["LTR and RTL marks U+200E and U+200F", "\u200E\u200F", ""],
|
||
["LTR/RTL and other directional formatting marks U+202A - U+202F", "\u202A\u202F", ""],
|
||
["Arabic Letter RTL mark U+061C", "\u061C", ""],
|
||
["Combining characters U+0300 - U+036F", "\u3000\u036F", ""],
|
||
["Zero width no-break space (BOM) U+FEFF", "\uFEFF", ""],
|
||
["Blank/invisible characters (U2800, U2062-U2063)", "\u2800\u2062\u2063", ""],
|
||
["Zero Width Non Joiner", "", ""],
|
||
])("should strip invisible characters: %s", (_, input, expected) => {
|
||
expect(removeHiddenChars(input)).toBe(expected);
|
||
});
|
||
});
|
||
});
|