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:
Michael Telatynski
2026-06-15 13:24:33 +00:00
committed by GitHub
parent 7949980a7e
commit 2f4e6a4ec4
37 changed files with 475 additions and 326 deletions
+122
View File
@@ -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);
});
});
+53
View File
@@ -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 }));
});
});
+26
View File
@@ -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);
});
});
+18
View File
@@ -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();
});
+29
View File
@@ -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);
}
}
});
});
+89
View File
@@ -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();
});
});
});
+222
View File
@@ -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);
});
});
+57
View File
@@ -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);
});
});
+47
View File
@@ -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();
});
});
+497
View File
@@ -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]);
});
});
});
+18
View File
@@ -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>');
});
});
+44
View File
@@ -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();
});
});
});
+61
View File
@@ -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);
});
});
});
+71
View File
@@ -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]);
});
});
});
+24
View File
@@ -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);
});
});
+238
View File
@@ -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();
});
});
});
+50
View File
@@ -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",
);
});
});
});
+58
View File
@@ -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]);
});
});
});
+50
View File
@@ -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);
});
});
});
+189
View File
@@ -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);
});
});
+106
View File
@@ -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");
});
});
+126
View File
@@ -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" });
});
});
+67
View File
@@ -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");
});
});