Use vitest for some EW unit tests (#33816)
* Use vitest for some EW unit tests * Ensure library builds are done before unit tests * Stabilise jest tests * Move more tests over * Make sonar happier * Update types/node for happy-dom compat again * Decrease max-workers to stabilise jest tests * Split jest over 3 runners to alleviate memory woes * Switch jest to runInBand * Attempt to deflake jest tests * tweak coverage * tweak coverage
This commit is contained in:
@@ -0,0 +1,122 @@
|
||||
/*
|
||||
Copyright 2019-2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import EventEmitter from "node:events";
|
||||
|
||||
import UserActivity from "./UserActivity";
|
||||
|
||||
class FakeDomEventEmitter extends EventEmitter {
|
||||
addEventListener(what: string, l: (...args: any[]) => void) {
|
||||
this.on(what, l);
|
||||
}
|
||||
|
||||
removeEventListener(what: string, l: (...args: any[]) => void) {
|
||||
this.removeListener(what, l);
|
||||
}
|
||||
}
|
||||
|
||||
describe("UserActivity", function () {
|
||||
let fakeWindow: FakeDomEventEmitter;
|
||||
let fakeDocument: FakeDomEventEmitter & { hasFocus?(): boolean };
|
||||
let userActivity: UserActivity;
|
||||
|
||||
beforeEach(function () {
|
||||
fakeWindow = new FakeDomEventEmitter();
|
||||
fakeDocument = new FakeDomEventEmitter();
|
||||
userActivity = new UserActivity(fakeWindow as unknown as Window, fakeDocument as unknown as Document);
|
||||
userActivity.start();
|
||||
vi.useFakeTimers();
|
||||
});
|
||||
|
||||
afterEach(function () {
|
||||
userActivity.stop();
|
||||
});
|
||||
|
||||
it("should return the same shared instance", function () {
|
||||
expect(UserActivity.sharedInstance()).toBe(UserActivity.sharedInstance());
|
||||
});
|
||||
|
||||
it("should consider user inactive if no activity", function () {
|
||||
expect(userActivity.userActiveNow()).toBe(false);
|
||||
});
|
||||
|
||||
it("should consider user not active recently if no activity", function () {
|
||||
expect(userActivity.userActiveRecently()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not consider user active after activity if no window focus", function () {
|
||||
fakeDocument.hasFocus = vi.fn().mockReturnValue(false);
|
||||
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
expect(userActivity.userActiveNow()).toBe(false);
|
||||
expect(userActivity.userActiveRecently()).toBe(false);
|
||||
});
|
||||
|
||||
it("should consider user active shortly after activity", function () {
|
||||
fakeDocument.hasFocus = vi.fn().mockReturnValue(true);
|
||||
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
expect(userActivity.userActiveNow()).toBe(true);
|
||||
expect(userActivity.userActiveRecently()).toBe(true);
|
||||
vi.advanceTimersByTime(200);
|
||||
expect(userActivity.userActiveNow()).toBe(true);
|
||||
expect(userActivity.userActiveRecently()).toBe(true);
|
||||
});
|
||||
|
||||
it("should consider user not active after 10s of no activity", function () {
|
||||
fakeDocument.hasFocus = vi.fn().mockReturnValue(true);
|
||||
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(userActivity.userActiveNow()).toBe(false);
|
||||
});
|
||||
|
||||
it("should consider user passive after 10s of no activity", function () {
|
||||
fakeDocument.hasFocus = vi.fn().mockReturnValue(true);
|
||||
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
vi.advanceTimersByTime(10000);
|
||||
expect(userActivity.userActiveRecently()).toBe(true);
|
||||
});
|
||||
|
||||
it("should not consider user passive after 10s if window un-focused", function () {
|
||||
fakeDocument.hasFocus = vi.fn().mockReturnValue(true);
|
||||
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
vi.advanceTimersByTime(10000);
|
||||
|
||||
fakeDocument.hasFocus = vi.fn().mockReturnValue(false);
|
||||
fakeWindow.emit("blur", {});
|
||||
|
||||
expect(userActivity.userActiveRecently()).toBe(false);
|
||||
});
|
||||
|
||||
it("should not consider user passive after 3 mins", function () {
|
||||
fakeDocument.hasFocus = vi.fn().mockReturnValue(true);
|
||||
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
vi.advanceTimersByTime(3 * 60 * 1000);
|
||||
|
||||
expect(userActivity.userActiveRecently()).toBe(false);
|
||||
});
|
||||
|
||||
it("should extend timer on activity", function () {
|
||||
fakeDocument.hasFocus = vi.fn().mockReturnValue(true);
|
||||
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
vi.advanceTimersByTime(1 * 60 * 1000);
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
vi.advanceTimersByTime(1 * 60 * 1000);
|
||||
userActivity.onUserActivity({ type: "event" } as Event);
|
||||
vi.advanceTimersByTime(1 * 60 * 1000);
|
||||
|
||||
expect(userActivity.userActiveRecently()).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
|
||||
import { WorkerManager } from "./WorkerManager";
|
||||
|
||||
describe("WorkerManager", () => {
|
||||
it("should generate consecutive sequence numbers for each call", () => {
|
||||
const postMessage = vi.fn();
|
||||
const manager = new WorkerManager({ postMessage } as unknown as Worker);
|
||||
|
||||
manager.call({ data: "One" });
|
||||
manager.call({ data: "Two" });
|
||||
manager.call({ data: "Three" });
|
||||
|
||||
const one = postMessage.mock.calls.find((c) => c[0].data === "One")!;
|
||||
const two = postMessage.mock.calls.find((c) => c[0].data === "Two")!;
|
||||
const three = postMessage.mock.calls.find((c) => c[0].data === "Three")!;
|
||||
|
||||
expect(one[0].seq).toBe(0);
|
||||
expect(two[0].seq).toBe(1);
|
||||
expect(three[0].seq).toBe(2);
|
||||
});
|
||||
|
||||
it("should support resolving out of order", async () => {
|
||||
const postMessage = vi.fn();
|
||||
const worker = { postMessage } as unknown as Worker;
|
||||
const manager = new WorkerManager(worker);
|
||||
|
||||
const oneProm = manager.call({ data: "One" });
|
||||
const twoProm = manager.call({ data: "Two" });
|
||||
const threeProm = manager.call({ data: "Three" });
|
||||
|
||||
const one = postMessage.mock.calls.find((c) => c[0].data === "One")![0].seq;
|
||||
const two = postMessage.mock.calls.find((c) => c[0].data === "Two")![0].seq;
|
||||
const three = postMessage.mock.calls.find((c) => c[0].data === "Three")![0].seq;
|
||||
|
||||
worker.onmessage!({ data: { seq: one, data: 1 } } as MessageEvent);
|
||||
await expect(oneProm).resolves.toEqual(expect.objectContaining({ data: 1 }));
|
||||
|
||||
worker.onmessage!({ data: { seq: three, data: 3 } } as MessageEvent);
|
||||
await expect(threeProm).resolves.toEqual(expect.objectContaining({ data: 3 }));
|
||||
|
||||
worker.onmessage!({ data: { seq: two, data: 2 } } as MessageEvent);
|
||||
await expect(twoProm).resolves.toEqual(expect.objectContaining({ data: 2 }));
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,26 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { looksValid } from "./email";
|
||||
|
||||
describe("looksValid", () => {
|
||||
it.each([
|
||||
["", false],
|
||||
["alice", false],
|
||||
["@", false],
|
||||
["@alice:example.com", false],
|
||||
["@b.org", false],
|
||||
["alice@example", false],
|
||||
["a@b.org", true],
|
||||
["alice@example.com", true],
|
||||
])("for »%s« should return %s", (value: string, expected: boolean) => {
|
||||
expect(looksValid(value)).toBe(expected);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { beforeEach } from "vitest";
|
||||
import fetchMock, { manageFetchMockGlobally } from "@fetch-mock/vitest";
|
||||
|
||||
manageFetchMockGlobally();
|
||||
|
||||
beforeEach(() => {
|
||||
// set up fetch API mock
|
||||
fetchMock.hardReset();
|
||||
fetchMock.catch(404);
|
||||
fetchMock.mockGlobal();
|
||||
});
|
||||
@@ -0,0 +1,29 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 Šimon Brandner <simon.bra.ag@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { lerp } from "./AnimationUtils";
|
||||
|
||||
describe("lerp", () => {
|
||||
it("correctly interpolates", () => {
|
||||
expect(lerp(0, 100, 0.5)).toBe(50);
|
||||
expect(lerp(50, 100, 0.5)).toBe(75);
|
||||
expect(lerp(0, 1, 0.1)).toBe(0.1);
|
||||
});
|
||||
|
||||
it("clamps the interpolant", () => {
|
||||
expect(lerp(0, 100, 50)).toBe(100);
|
||||
expect(lerp(0, 100, -50)).toBe(0);
|
||||
});
|
||||
|
||||
it("handles negative numbers", () => {
|
||||
expect(lerp(-100, 0, 0.5)).toBe(-50);
|
||||
expect(lerp(100, -100, 0.5)).toBe(0);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { FixedRollingArray } from "./FixedRollingArray";
|
||||
|
||||
describe("FixedRollingArray", () => {
|
||||
it("should seed the array with the given value", () => {
|
||||
const seed = "test";
|
||||
const width = 24;
|
||||
const array = new FixedRollingArray(width, seed);
|
||||
|
||||
expect(array.value.length).toBe(width);
|
||||
expect(array.value.every((v) => v === seed)).toBe(true);
|
||||
});
|
||||
|
||||
it("should insert at the correct end", () => {
|
||||
const seed = "test";
|
||||
const value = "changed";
|
||||
const width = 24;
|
||||
const array = new FixedRollingArray(width, seed);
|
||||
array.pushValue(value);
|
||||
|
||||
expect(array.value.length).toBe(width);
|
||||
expect(array.value[0]).toBe(value);
|
||||
});
|
||||
|
||||
it("should roll over", () => {
|
||||
const seed = -1;
|
||||
const width = 24;
|
||||
const array = new FixedRollingArray(width, seed);
|
||||
|
||||
const maxValue = width * 2;
|
||||
const minValue = width; // because we're forcing a rollover
|
||||
for (let i = 0; i <= maxValue; i++) {
|
||||
array.pushValue(i);
|
||||
}
|
||||
|
||||
expect(array.value.length).toBe(width);
|
||||
|
||||
for (let i = 1; i < width; i++) {
|
||||
const current = array.value[i];
|
||||
const previous = array.value[i - 1];
|
||||
expect(previous - current).toBe(1);
|
||||
|
||||
if (i === 1) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(previous).toBe(maxValue);
|
||||
} else if (i === width) {
|
||||
// eslint-disable-next-line jest/no-conditional-expect
|
||||
expect(current).toBe(minValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,89 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
import { blobIsAnimated, mayBeAnimated } from "./Image";
|
||||
|
||||
const imagesDir = path.resolve(__dirname, "../../test/unit-tests/images");
|
||||
|
||||
describe("Image", () => {
|
||||
describe("mayBeAnimated", () => {
|
||||
it("image/gif", async () => {
|
||||
expect(mayBeAnimated("image/gif")).toBeTruthy();
|
||||
});
|
||||
it("image/webp", async () => {
|
||||
expect(mayBeAnimated("image/webp")).toBeTruthy();
|
||||
});
|
||||
it("image/png", async () => {
|
||||
expect(mayBeAnimated("image/png")).toBeTruthy();
|
||||
});
|
||||
it("image/apng", async () => {
|
||||
expect(mayBeAnimated("image/apng")).toBeTruthy();
|
||||
});
|
||||
it("image/jpeg", async () => {
|
||||
expect(mayBeAnimated("image/jpeg")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("blobIsAnimated", () => {
|
||||
it("Animated GIF", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(imagesDir, "animated-logo.gif")).slice()], {
|
||||
type: "image/gif",
|
||||
});
|
||||
expect(await blobIsAnimated(img)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Static GIF", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(imagesDir, "static-logo.gif")).slice()], {
|
||||
type: "image/gif",
|
||||
});
|
||||
expect(await blobIsAnimated(img)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Animated WEBP", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(imagesDir, "animated-logo.webp")).slice()], {
|
||||
type: "image/webp",
|
||||
});
|
||||
expect(await blobIsAnimated(img)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Static WEBP", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(imagesDir, "static-logo.webp")).slice()], {
|
||||
type: "image/webp",
|
||||
});
|
||||
expect(await blobIsAnimated(img)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Static WEBP in extended file format", async () => {
|
||||
const img = new Blob(
|
||||
[fs.readFileSync(path.resolve(imagesDir, "static-logo-extended-file-format.webp")).slice()],
|
||||
{ type: "image/webp" },
|
||||
);
|
||||
expect(await blobIsAnimated(img)).toBeFalsy();
|
||||
});
|
||||
|
||||
it("Animated PNG", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(imagesDir, "animated-logo.apng")).slice()]);
|
||||
const pngBlob = img.slice(0, img.size, "image/png");
|
||||
const apngBlob = img.slice(0, img.size, "image/apng");
|
||||
expect(await blobIsAnimated(pngBlob)).toBeTruthy();
|
||||
expect(await blobIsAnimated(apngBlob)).toBeTruthy();
|
||||
});
|
||||
|
||||
it("Static PNG", async () => {
|
||||
const img = new Blob([fs.readFileSync(path.resolve(imagesDir, "static-logo.png")).slice()]);
|
||||
const pngBlob = img.slice(0, img.size, "image/png");
|
||||
const apngBlob = img.slice(0, img.size, "image/apng");
|
||||
expect(await blobIsAnimated(pngBlob)).toBeFalsy();
|
||||
expect(await blobIsAnimated(apngBlob)).toBeFalsy();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,222 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { logger } from "matrix-js-sdk/src/logger";
|
||||
|
||||
import { LruCache } from "./LruCache";
|
||||
|
||||
describe("LruCache", () => {
|
||||
it("when creating a cache with negative capacity it should raise an error", () => {
|
||||
expect(() => new LruCache(-23)).toThrow("Cache capacity must be at least 1");
|
||||
});
|
||||
|
||||
it("when creating a cache with 0 capacity it should raise an error", () => {
|
||||
expect(() => new LruCache(0)).toThrow("Cache capacity must be at least 1");
|
||||
});
|
||||
|
||||
describe("when there is a cache with a capacity of 3", () => {
|
||||
let cache: LruCache<string, string>;
|
||||
|
||||
beforeEach(() => {
|
||||
cache = new LruCache(3);
|
||||
});
|
||||
|
||||
it("has() should return false", () => {
|
||||
expect(cache.has("a")).toBe(false);
|
||||
});
|
||||
|
||||
it("get() should return undefined", () => {
|
||||
expect(cache.get("a")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("values() should return an empty iterator", () => {
|
||||
expect(Array.from(cache.values())).toEqual([]);
|
||||
});
|
||||
|
||||
it("delete() should not raise an error", () => {
|
||||
cache.delete("a");
|
||||
});
|
||||
|
||||
describe("when the cache contains 2 items", () => {
|
||||
beforeEach(() => {
|
||||
cache.set("a", "a value");
|
||||
cache.set("b", "b value");
|
||||
});
|
||||
|
||||
it("has() should return false for an item not in the cache", () => {
|
||||
expect(cache.has("c")).toBe(false);
|
||||
});
|
||||
|
||||
it("get() should return undefined for an item not in the cahce", () => {
|
||||
expect(cache.get("c")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("values() should return the items in the cache", () => {
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "b value"]);
|
||||
});
|
||||
|
||||
it("clear() should clear the cache", () => {
|
||||
cache.clear();
|
||||
expect(cache.has("a")).toBe(false);
|
||||
expect(cache.has("b")).toBe(false);
|
||||
expect(Array.from(cache.values())).toEqual([]);
|
||||
});
|
||||
|
||||
it("when an error occurs while setting an item the cache should be cleard", () => {
|
||||
vi.spyOn(logger, "warn");
|
||||
const err = new Error("Something weng wrong :(");
|
||||
|
||||
// @ts-ignore
|
||||
cache.safeSet = () => {
|
||||
throw err;
|
||||
};
|
||||
cache.set("c", "c value");
|
||||
expect(Array.from(cache.values())).toEqual([]);
|
||||
|
||||
expect(logger.warn).toHaveBeenCalledWith("LruCache error", err);
|
||||
});
|
||||
|
||||
describe("and adding another item", () => {
|
||||
beforeEach(() => {
|
||||
cache.set("c", "c value");
|
||||
});
|
||||
|
||||
it("deleting an unkonwn item should not raise an error", () => {
|
||||
cache.delete("unknown");
|
||||
});
|
||||
|
||||
it("deleting the first item should work", () => {
|
||||
cache.delete("a");
|
||||
expect(Array.from(cache.values())).toEqual(["b value", "c value"]);
|
||||
|
||||
// add an item after delete should work work
|
||||
cache.set("d", "d value");
|
||||
expect(Array.from(cache.values())).toEqual(["b value", "c value", "d value"]);
|
||||
});
|
||||
|
||||
it("deleting the item in the middle should work", () => {
|
||||
cache.delete("b");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "c value"]);
|
||||
|
||||
// add an item after delete should work work
|
||||
cache.set("d", "d value");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "c value", "d value"]);
|
||||
});
|
||||
|
||||
it("deleting the last item should work", () => {
|
||||
cache.delete("c");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "b value"]);
|
||||
|
||||
// add an item after delete should work work
|
||||
cache.set("d", "d value");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "b value", "d value"]);
|
||||
});
|
||||
|
||||
it("deleting all items should work", () => {
|
||||
cache.delete("a");
|
||||
cache.delete("b");
|
||||
cache.delete("c");
|
||||
// should not raise an error
|
||||
cache.delete("a");
|
||||
cache.delete("b");
|
||||
cache.delete("c");
|
||||
|
||||
expect(Array.from(cache.values())).toEqual([]);
|
||||
|
||||
// add an item after delete should work work
|
||||
cache.set("d", "d value");
|
||||
expect(Array.from(cache.values())).toEqual(["d value"]);
|
||||
});
|
||||
|
||||
it("deleting and adding some items should work", () => {
|
||||
cache.set("d", "d value");
|
||||
cache.get("b");
|
||||
cache.delete("b");
|
||||
cache.set("e", "e value");
|
||||
expect(Array.from(cache.values())).toEqual(["c value", "d value", "e value"]);
|
||||
});
|
||||
|
||||
describe("and accesing the first added item and adding another item", () => {
|
||||
beforeEach(() => {
|
||||
cache.get("a");
|
||||
cache.set("d", "d value");
|
||||
});
|
||||
|
||||
it("should contain the last recently accessed items", () => {
|
||||
expect(cache.has("a")).toBe(true);
|
||||
expect(cache.get("a")).toEqual("a value");
|
||||
expect(cache.has("c")).toBe(true);
|
||||
expect(cache.get("c")).toEqual("c value");
|
||||
expect(cache.has("d")).toBe(true);
|
||||
expect(cache.get("d")).toEqual("d value");
|
||||
expect(Array.from(cache.values())).toEqual(["a value", "c value", "d value"]);
|
||||
});
|
||||
|
||||
it("should not contain the least recently accessed items", () => {
|
||||
expect(cache.has("b")).toBe(false);
|
||||
expect(cache.get("b")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("and adding 2 additional items", () => {
|
||||
beforeEach(() => {
|
||||
cache.set("d", "d value");
|
||||
cache.set("e", "e value");
|
||||
});
|
||||
|
||||
it("has() should return false for expired items", () => {
|
||||
expect(cache.has("a")).toBe(false);
|
||||
expect(cache.has("b")).toBe(false);
|
||||
});
|
||||
|
||||
it("has() should return true for items in the caceh", () => {
|
||||
expect(cache.has("c")).toBe(true);
|
||||
expect(cache.has("d")).toBe(true);
|
||||
expect(cache.has("e")).toBe(true);
|
||||
});
|
||||
|
||||
it("get() should return undefined for expired items", () => {
|
||||
expect(cache.get("a")).toBeUndefined();
|
||||
expect(cache.get("b")).toBeUndefined();
|
||||
});
|
||||
|
||||
it("get() should return the items in the cache", () => {
|
||||
expect(cache.get("c")).toBe("c value");
|
||||
expect(cache.get("d")).toBe("d value");
|
||||
expect(cache.get("e")).toBe("e value");
|
||||
});
|
||||
|
||||
it("values() should return the items in the cache", () => {
|
||||
expect(Array.from(cache.values())).toEqual(["c value", "d value", "e value"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("when the cache contains some items where one of them is a replacement", () => {
|
||||
beforeEach(() => {
|
||||
cache.set("a", "a value");
|
||||
cache.set("b", "b value");
|
||||
cache.set("c", "c value");
|
||||
cache.set("a", "a value 2");
|
||||
cache.set("d", "d value");
|
||||
});
|
||||
|
||||
it("should contain the last recently set items", () => {
|
||||
expect(cache.has("a")).toBe(true);
|
||||
expect(cache.get("a")).toEqual("a value 2");
|
||||
expect(cache.has("c")).toBe(true);
|
||||
expect(cache.get("c")).toEqual("c value");
|
||||
expect(cache.has("d")).toBe(true);
|
||||
expect(cache.get("d")).toEqual("d value");
|
||||
expect(Array.from(cache.values())).toEqual(["a value 2", "c value", "d value"]);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { doesRoomVersionSupport, PreferredRoomVersions } from "./PreferredRoomVersions";
|
||||
|
||||
describe("doesRoomVersionSupport", () => {
|
||||
it("should detect unstable as unsupported", () => {
|
||||
expect(doesRoomVersionSupport("org.example.unstable", "1")).toBe(false);
|
||||
expect(doesRoomVersionSupport("1.2-beta", "1")).toBe(false);
|
||||
});
|
||||
|
||||
it("should detect support properly", () => {
|
||||
expect(doesRoomVersionSupport("1", "2")).toBe(false); // older
|
||||
expect(doesRoomVersionSupport("2", "2")).toBe(true); // exact
|
||||
expect(doesRoomVersionSupport("3", "2")).toBe(true); // newer
|
||||
});
|
||||
|
||||
it("should handle decimal versions", () => {
|
||||
expect(doesRoomVersionSupport("1.1", "2.2")).toBe(false); // older
|
||||
expect(doesRoomVersionSupport("2.1", "2.2")).toBe(false); // exact-ish
|
||||
expect(doesRoomVersionSupport("2.2", "2.2")).toBe(true); // exact
|
||||
expect(doesRoomVersionSupport("2.3", "2.2")).toBe(true); // exact-ish
|
||||
expect(doesRoomVersionSupport("3.1", "2.2")).toBe(true); // newer
|
||||
});
|
||||
|
||||
it("should detect knock rooms in v7 and above", () => {
|
||||
expect(doesRoomVersionSupport("6", PreferredRoomVersions.KnockRooms)).toBe(false);
|
||||
expect(doesRoomVersionSupport("7", PreferredRoomVersions.KnockRooms)).toBe(true);
|
||||
expect(doesRoomVersionSupport("8", PreferredRoomVersions.KnockRooms)).toBe(true);
|
||||
expect(doesRoomVersionSupport("9", PreferredRoomVersions.KnockRooms)).toBe(true);
|
||||
expect(doesRoomVersionSupport("10", PreferredRoomVersions.KnockRooms)).toBe(true);
|
||||
});
|
||||
|
||||
it("should detect restricted rooms in v9 and v10", () => {
|
||||
// Dev note: we consider it a feature that v8 rooms have to upgrade considering the bug in v8.
|
||||
// https://spec.matrix.org/v1.3/rooms/v8/#redactions
|
||||
expect(doesRoomVersionSupport("8", PreferredRoomVersions.RestrictedRooms)).toBe(false);
|
||||
expect(doesRoomVersionSupport("9", PreferredRoomVersions.RestrictedRooms)).toBe(true);
|
||||
expect(doesRoomVersionSupport("10", PreferredRoomVersions.RestrictedRooms)).toBe(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { SnakedObject, snakeToCamel } from "./SnakedObject";
|
||||
|
||||
describe("snakeToCamel", () => {
|
||||
it("should convert snake_case to camelCase in simple scenarios", () => {
|
||||
expect(snakeToCamel("snake_case")).toBe("snakeCase");
|
||||
expect(snakeToCamel("snake_case_but_longer")).toBe("snakeCaseButLonger");
|
||||
expect(snakeToCamel("numbered_123")).toBe("numbered123"); // not a thing we would see normally
|
||||
});
|
||||
|
||||
// Not really something we expect to see, but it's defined behaviour of the function
|
||||
it("should not camelCase a trailing or leading underscore", () => {
|
||||
expect(snakeToCamel("_snake")).toBe("_snake");
|
||||
expect(snakeToCamel("snake_")).toBe("snake_");
|
||||
expect(snakeToCamel("_snake_case")).toBe("_snakeCase");
|
||||
expect(snakeToCamel("snake_case_")).toBe("snakeCase_");
|
||||
});
|
||||
|
||||
// Another thing we don't really expect to see, but is "defined behaviour"
|
||||
it("should be predictable with double underscores", () => {
|
||||
expect(snakeToCamel("__snake__")).toBe("_Snake_");
|
||||
expect(snakeToCamel("snake__case")).toBe("snake_case");
|
||||
});
|
||||
});
|
||||
|
||||
describe("SnakedObject", () => {
|
||||
/* eslint-disable camelcase*/
|
||||
const input = {
|
||||
snake_case: "woot",
|
||||
snakeCase: "oh no", // ensure different value from snake_case for tests
|
||||
camelCase: "fallback",
|
||||
};
|
||||
const snake = new SnakedObject(input);
|
||||
/* eslint-enable camelcase*/
|
||||
|
||||
it("should prefer snake_case keys", () => {
|
||||
expect(snake.get("snake_case")).toBe(input.snake_case);
|
||||
expect(snake.get("snake_case", "camelCase")).toBe(input.snake_case);
|
||||
});
|
||||
|
||||
it("should fall back to camelCase keys when needed", () => {
|
||||
// @ts-ignore - we're deliberately supplying a key that doesn't exist
|
||||
expect(snake.get("camel_case")).toBe(input.camelCase);
|
||||
|
||||
// @ts-ignore - we're deliberately supplying a key that doesn't exist
|
||||
expect(snake.get("e_no_exist", "camelCase")).toBe(input.camelCase);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { abbreviateUrl, parseUrl, unabbreviateUrl } from "./UrlUtils";
|
||||
|
||||
describe("abbreviateUrl", () => {
|
||||
it("should return empty string if passed falsey", () => {
|
||||
expect(abbreviateUrl(undefined)).toEqual("");
|
||||
});
|
||||
|
||||
it("should abbreviate to host if empty pathname", () => {
|
||||
expect(abbreviateUrl("https://foo/")).toEqual("foo");
|
||||
});
|
||||
|
||||
it("should not abbreviate if has path parts", () => {
|
||||
expect(abbreviateUrl("https://foo/path/parts")).toEqual("https://foo/path/parts");
|
||||
});
|
||||
});
|
||||
|
||||
describe("unabbreviateUrl", () => {
|
||||
it("should return empty string if passed falsey", () => {
|
||||
expect(unabbreviateUrl(undefined)).toEqual("");
|
||||
});
|
||||
|
||||
it("should prepend https to input if it lacks it", () => {
|
||||
expect(unabbreviateUrl("element.io")).toEqual("https://element.io");
|
||||
});
|
||||
|
||||
it("should not prepend https to input if it has it", () => {
|
||||
expect(unabbreviateUrl("https://element.io")).toEqual("https://element.io");
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseUrl", () => {
|
||||
it("should not throw on no proto", () => {
|
||||
expect(() => parseUrl("test")).not.toThrow();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,497 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
arrayDiff,
|
||||
arrayFastClone,
|
||||
arrayFastResample,
|
||||
arrayHasDiff,
|
||||
arrayHasOrderChange,
|
||||
arrayUnion,
|
||||
arrayRescale,
|
||||
arraySeed,
|
||||
arraySmoothingResample,
|
||||
arrayTrimFill,
|
||||
arrayIntersection,
|
||||
ArrayUtil,
|
||||
GroupedArray,
|
||||
concat,
|
||||
asyncEvery,
|
||||
asyncSome,
|
||||
asyncSomeParallel,
|
||||
asyncFilter,
|
||||
} from "./arrays";
|
||||
|
||||
type TestParams = { input: number[]; output: number[] };
|
||||
type TestCase = [string, TestParams];
|
||||
|
||||
function expectSample(input: number[], expected: number[], smooth = false) {
|
||||
const result = (smooth ? arraySmoothingResample : arrayFastResample)(input, expected.length);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(expected.length);
|
||||
expect(result).toEqual(expected);
|
||||
}
|
||||
|
||||
describe("arrays", () => {
|
||||
describe("arrayFastResample", () => {
|
||||
const downsampleCases: TestCase[] = [
|
||||
["Odd -> Even", { input: [1, 2, 3, 4, 5], output: [1, 4] }],
|
||||
["Odd -> Odd", { input: [1, 2, 3, 4, 5], output: [1, 3, 5] }],
|
||||
["Even -> Odd", { input: [1, 2, 3, 4], output: [1, 2, 3] }],
|
||||
["Even -> Even", { input: [1, 2, 3, 4], output: [1, 3] }],
|
||||
];
|
||||
it.each(downsampleCases)("downsamples correctly from %s", (_d, { input, output }) =>
|
||||
expectSample(input, output),
|
||||
);
|
||||
|
||||
const upsampleCases: TestCase[] = [
|
||||
["Odd -> Even", { input: [1, 2, 3], output: [1, 1, 2, 2, 3, 3] }],
|
||||
["Odd -> Odd", { input: [1, 2, 3], output: [1, 1, 2, 2, 3] }],
|
||||
["Even -> Odd", { input: [1, 2], output: [1, 1, 1, 2, 2] }],
|
||||
["Even -> Even", { input: [1, 2], output: [1, 1, 1, 2, 2, 2] }],
|
||||
];
|
||||
it.each(upsampleCases)("upsamples correctly from %s", (_d, { input, output }) => expectSample(input, output));
|
||||
|
||||
const maintainSampleCases: TestCase[] = [
|
||||
["Odd", { input: [1, 2, 3], output: [1, 2, 3] }], // Odd
|
||||
["Even", { input: [1, 2], output: [1, 2] }], // Even
|
||||
];
|
||||
|
||||
it.each(maintainSampleCases)("maintains samples for %s", (_d, { input, output }) =>
|
||||
expectSample(input, output),
|
||||
);
|
||||
});
|
||||
|
||||
describe("arraySmoothingResample", () => {
|
||||
// Dev note: these aren't great samples, but they demonstrate the bare minimum. Ideally
|
||||
// we'd be feeding a thousand values in and seeing what a curve of 250 values looks like,
|
||||
// but that's not really feasible to manually verify accuracy.
|
||||
const downsampleCases: TestCase[] = [
|
||||
["Odd -> Even", { input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3, 3] }],
|
||||
["Odd -> Odd", { input: [4, 4, 1, 4, 4, 1, 4, 4, 1], output: [3, 3, 3] }],
|
||||
["Even -> Odd", { input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3, 3] }],
|
||||
["Even -> Even", { input: [4, 4, 1, 4, 4, 1, 4, 4], output: [3, 3] }],
|
||||
];
|
||||
|
||||
it.each(downsampleCases)("downsamples correctly from %s", (_d, { input, output }) =>
|
||||
expectSample(input, output, true),
|
||||
);
|
||||
|
||||
const upsampleCases: TestCase[] = [
|
||||
["Odd -> Even", { input: [2, 0, 2], output: [2, 2, 0, 0, 2, 2] }],
|
||||
["Odd -> Odd", { input: [2, 0, 2], output: [2, 2, 0, 0, 2] }],
|
||||
["Even -> Odd", { input: [2, 0], output: [2, 2, 2, 0, 0] }],
|
||||
["Even -> Even", { input: [2, 0], output: [2, 2, 2, 0, 0, 0] }],
|
||||
];
|
||||
it.each(upsampleCases)("upsamples correctly from %s", (_d, { input, output }) =>
|
||||
expectSample(input, output, true),
|
||||
);
|
||||
|
||||
const maintainCases: TestCase[] = [
|
||||
["Odd", { input: [2, 0, 2], output: [2, 0, 2] }],
|
||||
["Even", { input: [2, 0], output: [2, 0] }],
|
||||
];
|
||||
it.each(maintainCases)("maintains samples for %s", (_d, { input, output }) => expectSample(input, output));
|
||||
});
|
||||
|
||||
describe("arrayRescale", () => {
|
||||
it("should rescale", () => {
|
||||
const input = [8, 9, 1, 0, 2, 7, 10];
|
||||
const output = [80, 90, 10, 0, 20, 70, 100];
|
||||
const result = arrayRescale(input, 0, 100);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayTrimFill", () => {
|
||||
it("should shrink arrays", () => {
|
||||
const input = [1, 2, 3];
|
||||
const output = [1, 2];
|
||||
const seed = [4, 5, 6];
|
||||
const result = arrayTrimFill(input, output.length, seed);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
|
||||
it("should expand arrays", () => {
|
||||
const input = [1, 2, 3];
|
||||
const output = [1, 2, 3, 4, 5];
|
||||
const seed = [4, 5, 6];
|
||||
const result = arrayTrimFill(input, output.length, seed);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(output);
|
||||
});
|
||||
|
||||
it("should keep arrays the same", () => {
|
||||
const input = [1, 2, 3];
|
||||
const output = [1, 2, 3];
|
||||
const seed = [4, 5, 6];
|
||||
const result = arrayTrimFill(input, output.length, seed);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(output.length);
|
||||
expect(result).toEqual(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("arrayIntersection", () => {
|
||||
it("should return the intersection", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 4]; // note diff
|
||||
const result = arrayIntersection(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 = arrayIntersection(a, b);
|
||||
expect(result).toBeDefined();
|
||||
expect(result).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("arrayUnion", () => {
|
||||
it("should union 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 = arrayUnion(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 array (it'll merge the array against itself)
|
||||
const a = [1, 1, 2, 2, 3, 3];
|
||||
const result = arrayUnion(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 = Object.fromEntries(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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("concat", () => {
|
||||
const emptyArray = () => new Uint8Array(0);
|
||||
const array1 = () => new Uint8Array([1, 2, 3]);
|
||||
const array2 = () => new Uint8Array([4, 5, 6]);
|
||||
const array3 = () => new Uint8Array([7, 8, 9]);
|
||||
|
||||
it("should work for empty arrays", () => {
|
||||
expect(concat(emptyArray(), emptyArray())).toEqual(emptyArray());
|
||||
});
|
||||
|
||||
it("should concat an empty and non-empty array", () => {
|
||||
expect(concat(emptyArray(), array1())).toEqual(array1());
|
||||
});
|
||||
|
||||
it("should concat an non-empty and empty array", () => {
|
||||
expect(concat(array1(), emptyArray())).toEqual(array1());
|
||||
});
|
||||
|
||||
it("should concat two arrays", () => {
|
||||
expect(concat(array1(), array2())).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6]));
|
||||
});
|
||||
|
||||
it("should concat three arrays", () => {
|
||||
expect(concat(array1(), array2(), array3())).toEqual(new Uint8Array([1, 2, 3, 4, 5, 6, 7, 8, 9]));
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncEvery", () => {
|
||||
it("when called with an empty array, it should return true", async () => {
|
||||
expect(await asyncEvery([], vi.fn().mockResolvedValue(true))).toBe(true);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to true for all of them, it should return true", async () => {
|
||||
const predicate = vi.fn().mockResolvedValue(true);
|
||||
expect(await asyncEvery([1, 2, 3], predicate)).toBe(true);
|
||||
expect(predicate).toHaveBeenCalledTimes(3);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
expect(predicate).toHaveBeenCalledWith(2);
|
||||
expect(predicate).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to false for all of them, it should return false", async () => {
|
||||
const predicate = vi.fn().mockResolvedValue(false);
|
||||
expect(await asyncEvery([1, 2, 3], predicate)).toBe(false);
|
||||
expect(predicate).toHaveBeenCalledTimes(1);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to false for one of them, it should return false", async () => {
|
||||
const predicate = vi.fn().mockResolvedValueOnce(true).mockResolvedValueOnce(false);
|
||||
expect(await asyncEvery([1, 2, 3], predicate)).toBe(false);
|
||||
expect(predicate).toHaveBeenCalledTimes(2);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
expect(predicate).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncSome", () => {
|
||||
it("when called with an empty array, it should return false", async () => {
|
||||
expect(await asyncSome([], vi.fn().mockResolvedValue(true))).toBe(false);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to false for all of them, it should return false", async () => {
|
||||
const predicate = vi.fn().mockResolvedValue(false);
|
||||
expect(await asyncSome([1, 2, 3], predicate)).toBe(false);
|
||||
expect(predicate).toHaveBeenCalledTimes(3);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
expect(predicate).toHaveBeenCalledWith(2);
|
||||
expect(predicate).toHaveBeenCalledWith(3);
|
||||
});
|
||||
|
||||
it("when called with some items and the predicate resolves to true, it should short-circuit and return true", async () => {
|
||||
const predicate = vi.fn().mockResolvedValueOnce(false).mockResolvedValueOnce(true);
|
||||
expect(await asyncSome([1, 2, 3], predicate)).toBe(true);
|
||||
expect(predicate).toHaveBeenCalledTimes(2);
|
||||
expect(predicate).toHaveBeenCalledWith(1);
|
||||
expect(predicate).toHaveBeenCalledWith(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncSomeParallel", () => {
|
||||
it("when called with an empty array, it should return false", async () => {
|
||||
expect(await asyncSomeParallel([], vi.fn().mockResolvedValue(true))).toBe(false);
|
||||
});
|
||||
|
||||
it("when all the predicates return false", async () => {
|
||||
expect(await asyncSomeParallel([1, 2, 3], vi.fn().mockResolvedValue(false))).toBe(false);
|
||||
});
|
||||
|
||||
it("when all the predicates return true", async () => {
|
||||
expect(await asyncSomeParallel([1, 2, 3], vi.fn().mockResolvedValue(true))).toBe(true);
|
||||
});
|
||||
|
||||
it("when one of the predicate return true", async () => {
|
||||
const predicate = vi.fn().mockImplementation((value) => Promise.resolve(value === 2));
|
||||
expect(await asyncSomeParallel([1, 2, 3], predicate)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("asyncFilter", () => {
|
||||
it("when called with an empty array, it should return an empty array", async () => {
|
||||
expect(await asyncFilter([], vi.fn().mockResolvedValue(true))).toEqual([]);
|
||||
});
|
||||
|
||||
it("should filter the content", async () => {
|
||||
const predicate = vi.fn().mockImplementation((value) => Promise.resolve(value === 2));
|
||||
expect(await asyncFilter([1, 2, 3], predicate)).toEqual([2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,18 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 Emmanuel Ezeka <eec.studies@gmail.com>
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { textToHtmlRainbow } from "./colour";
|
||||
|
||||
describe("textToHtmlRainbow", () => {
|
||||
it("correctly transform text to html without splitting the emoji in two", () => {
|
||||
expect(textToHtmlRainbow("🐻")).toBe('<span data-mx-color="#ff00be">🐻</span>');
|
||||
expect(textToHtmlRainbow("🐕🦺")).toBe('<span data-mx-color="#ff00be">🐕🦺</span>');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, type Mock } from "vitest";
|
||||
import { type ClientEvent, type ClientEventHandlerMap, SyncState } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { createReconnectedListener } from "./connection";
|
||||
|
||||
describe("createReconnectedListener", () => {
|
||||
let reconnectedListener: ClientEventHandlerMap[ClientEvent.Sync];
|
||||
let onReconnect: Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
onReconnect = vi.fn();
|
||||
reconnectedListener = createReconnectedListener(onReconnect);
|
||||
});
|
||||
|
||||
[
|
||||
[SyncState.Prepared, SyncState.Syncing],
|
||||
[SyncState.Syncing, SyncState.Reconnecting],
|
||||
[SyncState.Reconnecting, SyncState.Syncing],
|
||||
].forEach(([from, to]) => {
|
||||
it(`should invoke the callback on a transition from ${from} to ${to}`, () => {
|
||||
reconnectedListener(to, from);
|
||||
expect(onReconnect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
[
|
||||
[SyncState.Syncing, SyncState.Syncing],
|
||||
[SyncState.Catchup, SyncState.Error],
|
||||
[SyncState.Reconnecting, SyncState.Error],
|
||||
].forEach(([from, to]) => {
|
||||
it(`should not invoke the callback on a transition from ${from} to ${to}`, () => {
|
||||
reconnectedListener(to, from);
|
||||
expect(onReconnect).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { getEnumValues, isEnumValue } from "./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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,71 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { iterableDiff, iterableIntersection } from "./iterables";
|
||||
|
||||
describe("iterables", () => {
|
||||
describe("iterableIntersection", () => {
|
||||
it("should return the intersection", () => {
|
||||
const a = [1, 2, 3];
|
||||
const b = [1, 2, 4]; // note diff
|
||||
const result = iterableIntersection(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 = iterableIntersection(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]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,24 @@
|
||||
/*
|
||||
* Copyright 2026 Element Creations Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { keepIfSame } from "./keepIfSame";
|
||||
|
||||
describe("keepIfSame", () => {
|
||||
it("returns the next value if the current and next values are not deeply equal", () => {
|
||||
const current = { a: 1 };
|
||||
const next = { a: 2 };
|
||||
expect(keepIfSame(current, next)).toBe(next);
|
||||
});
|
||||
|
||||
it("returns the current value if the current and next values are deeply equal", () => {
|
||||
const current = { a: 1 };
|
||||
const next = { a: 1 };
|
||||
expect(keepIfSame(current, next)).toBe(current);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import {
|
||||
objectClone,
|
||||
objectDiff,
|
||||
objectExcluding,
|
||||
objectHasDiff,
|
||||
objectKeyChanges,
|
||||
objectShallowClone,
|
||||
objectWithOnly,
|
||||
} from "./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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2023 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach } from "vitest";
|
||||
import { type IdTokenClaims } from "oidc-client-ts";
|
||||
import { decodeIdToken } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import {
|
||||
getStoredOidcClientId,
|
||||
getStoredOidcIdToken,
|
||||
getStoredOidcIdTokenClaims,
|
||||
getStoredOidcTokenIssuer,
|
||||
persistOidcAuthenticatedSettings,
|
||||
} from "./persistOidcSettings";
|
||||
|
||||
vi.mock("matrix-js-sdk/src/matrix");
|
||||
|
||||
describe("persist OIDC settings", () => {
|
||||
vi.spyOn(localStorage, "getItem");
|
||||
vi.spyOn(localStorage, "setItem");
|
||||
|
||||
beforeEach(() => {
|
||||
localStorage.clear();
|
||||
});
|
||||
|
||||
const clientId = "test-client-id";
|
||||
const issuer = "https://auth.org/";
|
||||
const idToken = "test-id-token";
|
||||
const idTokenClaims: IdTokenClaims = {
|
||||
// audience is this client
|
||||
aud: "123",
|
||||
// issuer matches
|
||||
iss: issuer,
|
||||
sub: "123",
|
||||
exp: 123,
|
||||
iat: 456,
|
||||
};
|
||||
|
||||
describe("persistOidcAuthenticatedSettings", () => {
|
||||
it("should set clientId and issuer in localStorage", () => {
|
||||
persistOidcAuthenticatedSettings(clientId, issuer, idToken);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_client_id", clientId);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_token_issuer", issuer);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_id_token", idToken);
|
||||
});
|
||||
|
||||
it("should not set idToken in localStorage when idToken is undefined", () => {
|
||||
persistOidcAuthenticatedSettings(clientId, issuer, undefined);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_client_id", clientId);
|
||||
expect(localStorage.setItem).toHaveBeenCalledWith("mx_oidc_token_issuer", issuer);
|
||||
expect(localStorage.getItem("mx_oidc_id_token")).toBeFalsy();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcTokenIssuer()", () => {
|
||||
it("should return issuer from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_token_issuer", issuer);
|
||||
expect(getStoredOidcTokenIssuer()).toEqual(issuer);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_token_issuer");
|
||||
});
|
||||
|
||||
it("should return undefined when no issuer in localStorage", () => {
|
||||
expect(getStoredOidcTokenIssuer()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcClientId()", () => {
|
||||
it("should return clientId from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_client_id", clientId);
|
||||
expect(getStoredOidcClientId()).toEqual(clientId);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_client_id");
|
||||
});
|
||||
it("should throw when no clientId in localStorage", () => {
|
||||
expect(() => getStoredOidcClientId()).toThrow("Oidc client id not found in storage");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcIdToken()", () => {
|
||||
it("should return token from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token", idToken);
|
||||
expect(getStoredOidcIdToken()).toEqual(idToken);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token");
|
||||
});
|
||||
|
||||
it("should return undefined when no token in localStorage", () => {
|
||||
expect(getStoredOidcIdToken()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getStoredOidcIdTokenClaims()", () => {
|
||||
it("should return claims from localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token_claims", JSON.stringify(idTokenClaims));
|
||||
expect(getStoredOidcIdTokenClaims()).toEqual(idTokenClaims);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token_claims");
|
||||
});
|
||||
|
||||
it("should return claims extracted from id_token in localStorage", () => {
|
||||
localStorage.setItem("mx_oidc_id_token", idToken);
|
||||
vi.mocked(decodeIdToken).mockReturnValue(idTokenClaims);
|
||||
expect(getStoredOidcIdTokenClaims()).toEqual(idTokenClaims);
|
||||
expect(decodeIdToken).toHaveBeenCalledWith(idToken);
|
||||
expect(localStorage.getItem).toHaveBeenCalledWith("mx_oidc_id_token_claims");
|
||||
});
|
||||
|
||||
it("should return undefined when no claims in localStorage", () => {
|
||||
expect(getStoredOidcIdTokenClaims()).toBeUndefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2026 Element Creations Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { getManageDeviceUrl } from "./urls";
|
||||
|
||||
describe("OIDC urls", () => {
|
||||
const accountManagementEndpoint = "https://auth.com/manage";
|
||||
const deviceId = "DEVICEID1234";
|
||||
|
||||
describe("getManageDeviceUrl()", () => {
|
||||
it("prefers stable action", async () => {
|
||||
expect(
|
||||
getManageDeviceUrl(
|
||||
accountManagementEndpoint,
|
||||
["org.matrix.session_view", "session_view", "org.matrix.device_view"],
|
||||
deviceId,
|
||||
),
|
||||
).toEqual("https://auth.com/manage?action=org.matrix.device_view&device_id=DEVICEID1234");
|
||||
});
|
||||
it("defaults to stable action when no known action is supported", async () => {
|
||||
expect(getManageDeviceUrl(accountManagementEndpoint, [], deviceId)).toEqual(
|
||||
"https://auth.com/manage?action=org.matrix.device_view&device_id=DEVICEID1234",
|
||||
);
|
||||
expect(getManageDeviceUrl(accountManagementEndpoint, ["foo"], deviceId)).toEqual(
|
||||
"https://auth.com/manage?action=org.matrix.device_view&device_id=DEVICEID1234",
|
||||
);
|
||||
});
|
||||
it("defaults to backwards compatible action when no supported actions are provided", async () => {
|
||||
expect(getManageDeviceUrl(accountManagementEndpoint, undefined, deviceId)).toEqual(
|
||||
"https://auth.com/manage?action=org.matrix.session_view&device_id=DEVICEID1234",
|
||||
);
|
||||
});
|
||||
it("uses unstable org.matrix.session_view", async () => {
|
||||
expect(getManageDeviceUrl(accountManagementEndpoint, ["org.matrix.session_view"], deviceId)).toEqual(
|
||||
"https://auth.com/manage?action=org.matrix.session_view&device_id=DEVICEID1234",
|
||||
);
|
||||
});
|
||||
it("uses unstable session_view", async () => {
|
||||
expect(getManageDeviceUrl(accountManagementEndpoint, ["session_view"], deviceId)).toEqual(
|
||||
"https://auth.com/manage?action=session_view&device_id=DEVICEID1234",
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/*
|
||||
* Copyright 2024 New Vector Ltd.
|
||||
*
|
||||
* SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
* Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { vi, describe, it, expect, afterEach } from "vitest";
|
||||
|
||||
import { batch } from "./promise.ts";
|
||||
|
||||
describe("promise.ts", () => {
|
||||
describe("batch", () => {
|
||||
afterEach(() => vi.useRealTimers());
|
||||
|
||||
it("should batch promises into groups of a given size", async () => {
|
||||
const promises = [() => Promise.resolve(1), () => Promise.resolve(2), () => Promise.resolve(3)];
|
||||
const batchSize = 2;
|
||||
const result = await batch(promises, batchSize);
|
||||
expect(result).toEqual([1, 2, 3]);
|
||||
});
|
||||
|
||||
it("should wait for the current batch to finish to request the next one", async () => {
|
||||
vi.useFakeTimers();
|
||||
|
||||
let promise1Called = false;
|
||||
const promise1 = () =>
|
||||
new Promise<number>((resolve) => {
|
||||
promise1Called = true;
|
||||
resolve(1);
|
||||
});
|
||||
let promise2Called = false;
|
||||
const promise2 = () =>
|
||||
new Promise<number>((resolve) => {
|
||||
promise2Called = true;
|
||||
setTimeout(() => {
|
||||
resolve(2);
|
||||
}, 10);
|
||||
});
|
||||
|
||||
let promise3Called = false;
|
||||
const promise3 = () =>
|
||||
new Promise<number>((resolve) => {
|
||||
promise3Called = true;
|
||||
resolve(3);
|
||||
});
|
||||
const batchSize = 2;
|
||||
const batchPromise = batch([promise1, promise2, promise3], batchSize);
|
||||
|
||||
expect(promise1Called).toBe(true);
|
||||
expect(promise2Called).toBe(true);
|
||||
expect(promise3Called).toBe(false);
|
||||
|
||||
vi.advanceTimersByTime(11);
|
||||
expect(await batchPromise).toEqual([1, 2, 3]);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,50 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { setHasDiff } from "./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);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,189 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2021 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
import { sortBy } from "lodash";
|
||||
import { averageBetweenStrings, DEFAULT_ALPHABET } from "matrix-js-sdk/src/utils";
|
||||
|
||||
import { midPointsBetweenStrings, reorderLexicographically } from "./stringOrderField";
|
||||
|
||||
const moveLexicographicallyTest = (
|
||||
orders: Array<string | undefined>,
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
expectedChanges: number,
|
||||
maxLength?: number,
|
||||
): void => {
|
||||
const ops = reorderLexicographically(orders, fromIndex, toIndex, maxLength);
|
||||
|
||||
const zipped: Array<[number, string | undefined]> = orders.map((o, i) => [i, o]);
|
||||
ops.forEach(({ index, order }) => {
|
||||
zipped[index][1] = order;
|
||||
});
|
||||
|
||||
const newOrders = sortBy(zipped, (i) => i[1]);
|
||||
expect(newOrders[toIndex][0]).toBe(fromIndex);
|
||||
expect(ops).toHaveLength(expectedChanges);
|
||||
};
|
||||
|
||||
describe("stringOrderField", () => {
|
||||
describe("midPointsBetweenStrings", () => {
|
||||
it("should work", () => {
|
||||
expect(averageBetweenStrings("!!", "##")).toBe('""');
|
||||
const midpoints = ["a", ...midPointsBetweenStrings("a", "e", 3, 1), "e"].sort();
|
||||
expect(midpoints[0]).toBe("a");
|
||||
expect(midpoints[4]).toBe("e");
|
||||
expect(midPointsBetweenStrings(" ", "!'Tu:}", 1, 50)).toStrictEqual([" S:J\\~"]);
|
||||
});
|
||||
|
||||
it("should return empty array when the request is not possible", () => {
|
||||
expect(midPointsBetweenStrings("a", "e", 0, 1)).toStrictEqual([]);
|
||||
expect(midPointsBetweenStrings("a", "e", 4, 1)).toStrictEqual([]);
|
||||
});
|
||||
});
|
||||
|
||||
describe("reorderLexicographically", () => {
|
||||
it("should work when moving left", () => {
|
||||
moveLexicographicallyTest(["a", "c", "e", "g", "i"], 2, 1, 1);
|
||||
});
|
||||
|
||||
it("should work when moving right", () => {
|
||||
moveLexicographicallyTest(["a", "c", "e", "g", "i"], 1, 2, 1);
|
||||
});
|
||||
|
||||
it("should work when all orders are undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined, undefined, undefined], 4, 1, 2);
|
||||
});
|
||||
|
||||
it("should work when moving to end and all orders are undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined, undefined, undefined], 1, 4, 5);
|
||||
});
|
||||
|
||||
it("should work when moving left and some orders are undefined", () => {
|
||||
moveLexicographicallyTest(["a", "c", "e", undefined, undefined, undefined], 5, 2, 1);
|
||||
|
||||
moveLexicographicallyTest(["a", "a", "e", undefined, undefined, undefined], 5, 1, 2);
|
||||
});
|
||||
|
||||
it("should work moving to the start when all is undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined], 2, 0, 1);
|
||||
});
|
||||
|
||||
it("should work moving to the end when all is undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined], 1, 3, 4);
|
||||
});
|
||||
|
||||
it("should work moving left when all is undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined, undefined, undefined], 4, 1, 2);
|
||||
});
|
||||
|
||||
it("should work moving right when all is undefined", () => {
|
||||
moveLexicographicallyTest([undefined, undefined, undefined, undefined], 1, 2, 3);
|
||||
});
|
||||
|
||||
it("should work moving more right when all is undefined", () => {
|
||||
moveLexicographicallyTest(
|
||||
[undefined, undefined, undefined, undefined, undefined, /**/ undefined, undefined],
|
||||
1,
|
||||
4,
|
||||
5,
|
||||
);
|
||||
});
|
||||
|
||||
it("should work moving left when right is undefined", () => {
|
||||
moveLexicographicallyTest(["20", undefined, undefined, undefined, undefined, undefined], 4, 2, 2);
|
||||
});
|
||||
|
||||
it("should work moving right when right is undefined", () => {
|
||||
moveLexicographicallyTest(
|
||||
["50", undefined, undefined, undefined, undefined, /**/ undefined, undefined],
|
||||
1,
|
||||
4,
|
||||
4,
|
||||
);
|
||||
});
|
||||
|
||||
it("should work moving left when right is defined", () => {
|
||||
moveLexicographicallyTest(["10", "20", "30", "40", undefined, undefined], 3, 1, 1);
|
||||
});
|
||||
|
||||
it("should work moving right when right is defined", () => {
|
||||
moveLexicographicallyTest(["10", "20", "30", "40", "50", undefined], 1, 3, 1);
|
||||
});
|
||||
|
||||
it("should work moving left when all is defined", () => {
|
||||
moveLexicographicallyTest(["11", "13", "15", "17", "19"], 2, 1, 1);
|
||||
});
|
||||
|
||||
it("should work moving right when all is defined", () => {
|
||||
moveLexicographicallyTest(["11", "13", "15", "17", "19"], 1, 2, 1);
|
||||
});
|
||||
|
||||
it("should work moving left into no left space", () => {
|
||||
moveLexicographicallyTest(["11", "12", "13", "14", "19"], 3, 1, 2, 2);
|
||||
|
||||
moveLexicographicallyTest(
|
||||
[
|
||||
DEFAULT_ALPHABET.charAt(0),
|
||||
// Target
|
||||
DEFAULT_ALPHABET.charAt(1),
|
||||
DEFAULT_ALPHABET.charAt(2),
|
||||
DEFAULT_ALPHABET.charAt(3),
|
||||
DEFAULT_ALPHABET.charAt(4),
|
||||
DEFAULT_ALPHABET.charAt(5),
|
||||
],
|
||||
5,
|
||||
1,
|
||||
5,
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("should work moving right into no right space", () => {
|
||||
moveLexicographicallyTest(["15", "16", "17", "18", "19"], 1, 3, 3, 2);
|
||||
|
||||
moveLexicographicallyTest(
|
||||
[
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
|
||||
],
|
||||
1,
|
||||
3,
|
||||
3,
|
||||
1,
|
||||
);
|
||||
});
|
||||
|
||||
it("should work moving right into no left space", () => {
|
||||
moveLexicographicallyTest(["11", "12", "13", "14", "15", "16", undefined], 1, 3, 3);
|
||||
|
||||
moveLexicographicallyTest(["0", "1", "2", "3", "4", "5"], 1, 3, 3, 1);
|
||||
});
|
||||
|
||||
it("should work moving left into no right space", () => {
|
||||
moveLexicographicallyTest(["15", "16", "17", "18", "19"], 4, 3, 4, 2);
|
||||
|
||||
moveLexicographicallyTest(
|
||||
[
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 5),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 4),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 3),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 2),
|
||||
DEFAULT_ALPHABET.charAt(DEFAULT_ALPHABET.length - 1),
|
||||
],
|
||||
4,
|
||||
3,
|
||||
4,
|
||||
1,
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { validateNumberInRange } from "./numberInRange";
|
||||
|
||||
describe("validateNumberInRange", () => {
|
||||
const min = 1;
|
||||
const max = 10;
|
||||
it("returns false when value is a not a number", () => {
|
||||
expect(validateNumberInRange(min, max)("test" as unknown as number)).toEqual(false);
|
||||
});
|
||||
it("returns false when value is undefined", () => {
|
||||
expect(validateNumberInRange(min, max)(undefined)).toEqual(false);
|
||||
});
|
||||
it("returns false when value is NaN", () => {
|
||||
expect(validateNumberInRange(min, max)(NaN)).toEqual(false);
|
||||
});
|
||||
it("returns true when value is equal to min", () => {
|
||||
expect(validateNumberInRange(min, max)(min)).toEqual(true);
|
||||
});
|
||||
it("returns true when value is equal to max", () => {
|
||||
expect(validateNumberInRange(min, max)(max)).toEqual(true);
|
||||
});
|
||||
it("returns true when value is an int in range", () => {
|
||||
expect(validateNumberInRange(min, max)(2)).toEqual(true);
|
||||
});
|
||||
it("returns true when value is a float in range", () => {
|
||||
expect(validateNumberInRange(min, max)(2.2)).toEqual(true);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterAll } from "vitest";
|
||||
import fetchMock from "@fetch-mock/vitest";
|
||||
|
||||
import { getVectorConfig } from "./getconfig";
|
||||
|
||||
describe("getVectorConfig()", () => {
|
||||
const elementDomain = "app.element.io";
|
||||
const now = 1234567890;
|
||||
const specificConfig = {
|
||||
brand: "specific",
|
||||
};
|
||||
const generalConfig = {
|
||||
brand: "general",
|
||||
};
|
||||
|
||||
beforeEach(() => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: { href: `https://${elementDomain}`, hostname: elementDomain },
|
||||
writable: true,
|
||||
});
|
||||
|
||||
// stable value for cachebuster
|
||||
vi.spyOn(Date, "now").mockReturnValue(now);
|
||||
vi.clearAllMocks();
|
||||
fetchMock.removeRoutes();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
vi.spyOn(Date, "now").mockRestore();
|
||||
});
|
||||
|
||||
it("requests specific config for document domain", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json*", specificConfig);
|
||||
fetchMock.getOnce("express:/config.json*", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(specificConfig);
|
||||
});
|
||||
|
||||
it("adds trailing slash to relativeLocation when not an empty string", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", specificConfig);
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig("..")).resolves.toEqual(specificConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config succeeds but is empty", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", {});
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config 404s", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { status: 404 });
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config is fetched from a file and is empty", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", 0);
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config returns a non-200 status", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { status: 401 });
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("returns general config when specific config returns an error", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err1" });
|
||||
fetchMock.getOnce("express:/config.json", generalConfig);
|
||||
|
||||
await expect(getVectorConfig()).resolves.toEqual(generalConfig);
|
||||
});
|
||||
|
||||
it("rejects with an error when general config rejects", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" });
|
||||
fetchMock.getOnce("express:/config.json", { throws: "err-general" });
|
||||
|
||||
await expect(getVectorConfig()).rejects.toBe("err-general");
|
||||
});
|
||||
|
||||
it("rejects with an error when config is invalid JSON", async () => {
|
||||
fetchMock.getOnce("express:/config.app.element.io.json", { throws: "err-specific" });
|
||||
fetchMock.getOnce("express:/config.json", '{"invalid": "json",}');
|
||||
|
||||
// We can't assert it'll be a SyntaxError as node-fetch behaves differently
|
||||
// https://github.com/wheresrhys/fetch-mock/issues/270
|
||||
await expect(getVectorConfig()).rejects.toThrow("in JSON at position 19");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
/*
|
||||
Copyright 2024 New Vector Ltd.
|
||||
Copyright 2022 The Matrix.org Foundation C.I.C.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
// @vitest-environment happy-dom
|
||||
|
||||
import { vi, describe, it, expect, beforeEach, afterAll } from "vitest";
|
||||
|
||||
import { getInitialScreenAfterLogin, init, onNewScreen } from "./routing";
|
||||
import type MatrixChat from "../components/structures/MatrixChat.tsx";
|
||||
|
||||
describe("onNewScreen", () => {
|
||||
it("should replace history if stripping via fields", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
hash: "#/room/!room:server?via=abc",
|
||||
replace: vi.fn(),
|
||||
assign: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
onNewScreen("room/!room:server");
|
||||
expect(window.location.assign).not.toHaveBeenCalled();
|
||||
expect(window.location.replace).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("should not replace history if changing rooms", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
hash: "#/room/!room1:server?via=abc",
|
||||
replace: vi.fn(),
|
||||
assign: vi.fn(),
|
||||
},
|
||||
writable: true,
|
||||
});
|
||||
onNewScreen("room/!room2:server");
|
||||
expect(window.location.assign).toHaveBeenCalled();
|
||||
expect(window.location.replace).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe("getInitialScreenAfterLogin", () => {
|
||||
beforeEach(() => {
|
||||
vi.spyOn(sessionStorage, "getItem").mockClear().mockReturnValue(null);
|
||||
vi.spyOn(sessionStorage, "setItem").mockClear();
|
||||
});
|
||||
|
||||
const makeMockLocation = (hash = "") => {
|
||||
const url = new URL("https://test.org");
|
||||
url.hash = hash;
|
||||
return url as unknown as Location;
|
||||
};
|
||||
|
||||
describe("when current url has no hash", () => {
|
||||
it("does not set an initial screen in session storage", () => {
|
||||
getInitialScreenAfterLogin(makeMockLocation());
|
||||
expect(sessionStorage.setItem).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("returns undefined when there is no initial screen in session storage", () => {
|
||||
expect(getInitialScreenAfterLogin(makeMockLocation())).toBeUndefined();
|
||||
});
|
||||
|
||||
it("returns initial screen from session storage", () => {
|
||||
const screen = {
|
||||
screen: "/room/!test",
|
||||
};
|
||||
vi.spyOn(sessionStorage, "getItem").mockReturnValue(JSON.stringify(screen));
|
||||
expect(getInitialScreenAfterLogin(makeMockLocation())).toEqual(screen);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when current url has a hash", () => {
|
||||
it("sets an initial screen in session storage", () => {
|
||||
const hash = "/room/!test";
|
||||
getInitialScreenAfterLogin(makeMockLocation(hash));
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith(
|
||||
"mx_screen_after_login",
|
||||
JSON.stringify({
|
||||
screen: "room/!test",
|
||||
params: {},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("sets an initial screen in session storage with params", () => {
|
||||
const hash = "/room/!test?param=test";
|
||||
getInitialScreenAfterLogin(makeMockLocation(hash));
|
||||
expect(sessionStorage.setItem).toHaveBeenCalledWith(
|
||||
"mx_screen_after_login",
|
||||
JSON.stringify({
|
||||
screen: "room/!test",
|
||||
params: { param: "test" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("init", () => {
|
||||
afterAll(() => {
|
||||
// @ts-ignore
|
||||
delete window.matrixChat;
|
||||
});
|
||||
|
||||
it("should call showScreen on MatrixChat on hashchange", () => {
|
||||
Object.defineProperty(window, "location", {
|
||||
value: {
|
||||
hash: "#/room/!room:server?via=abc",
|
||||
},
|
||||
});
|
||||
|
||||
window.matrixChat = {
|
||||
showScreen: vi.fn(),
|
||||
} as unknown as MatrixChat;
|
||||
|
||||
init();
|
||||
window.dispatchEvent(new HashChangeEvent("hashchange"));
|
||||
|
||||
expect(window.matrixChat.showScreen).toHaveBeenCalledWith("room/!room:server", { via: "abc" });
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
Copyright 2020-2024 New Vector Ltd.
|
||||
|
||||
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||
Please see LICENSE files in the repository root for full details.
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from "vitest";
|
||||
|
||||
import { parseAppUrl, parseQsFromFragment, searchParamsToQueryDict } from "./url_utils";
|
||||
|
||||
// @ts-ignore
|
||||
const location: Location = {
|
||||
hash: "",
|
||||
search: "",
|
||||
};
|
||||
|
||||
describe("parseQsFromFragment", () => {
|
||||
it("should parse correctly", () => {
|
||||
location.hash = "#/home?foo=bar";
|
||||
expect(parseQsFromFragment(location)).toEqual({
|
||||
location: "/home",
|
||||
params: new URLSearchParams({
|
||||
foo: "bar",
|
||||
}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("searchParamsToQueryDict", () => {
|
||||
it("should handle arrays correctly", () => {
|
||||
const u = new URLSearchParams("a=b&b=c&c=d&a=e&a=f");
|
||||
expect(searchParamsToQueryDict(u)).toEqual({
|
||||
a: ["b", "e", "f"],
|
||||
b: "c",
|
||||
c: "d",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("parseUrlParameters", () => {
|
||||
it("should parse legacy sso parameters from query", () => {
|
||||
const u = new URL("https://app.element.io?loginToken=foobar");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.legacy_sso?.loginToken).toEqual("foobar");
|
||||
});
|
||||
|
||||
it("should parse oidc parameters from fragment", () => {
|
||||
const u = new URL("https://app.element.io/#code=foobar&state=barfoo");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.oidc_fragment?.code).toEqual("foobar");
|
||||
expect(parsed.params.oidc_fragment?.state).toEqual("barfoo");
|
||||
});
|
||||
|
||||
it("should parse oidc parameters from query", () => {
|
||||
const u = new URL("https://app.element.io/?code=foobar&state=barfoo");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.oidc_query?.code).toEqual("foobar");
|
||||
expect(parsed.params.oidc_query?.state).toEqual("barfoo");
|
||||
});
|
||||
|
||||
it("should parse guest parameters", () => {
|
||||
const u = new URL("https://app.element.io?foo=bar#/room/!roomId:server?guest_access_token=foobar");
|
||||
const parsed = parseAppUrl(u);
|
||||
expect(parsed.params.guest?.guest_access_token).toEqual("foobar");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user