Migrate batch of tests to vitest (#34106)

* Migrate autocomplete tests to vitest

* Migrate utils/beacon tests to vitest

* Migrate utils/device tests to vitest

* Migrate utils/crypto tests to vitest

* Migrate utils/localRoom tests to vitest

* Migrate notifications tests to vitest

* Fix types

* Make jest test happy

* Make sonar happier
This commit is contained in:
Michael Telatynski
2026-07-03 10:10:04 +00:00
committed by GitHub
parent 78273c569b
commit 275ea25eca
21 changed files with 195 additions and 147 deletions
+96
View File
@@ -0,0 +1,96 @@
/*
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 { describe, it, expect } from "vitest";
import { Beacon } from "matrix-js-sdk/src/matrix";
import { makeBeaconEvent, makeBeaconInfoEvent } from "test-utils";
import { type Bounds, getBeaconBounds } from "./bounds";
describe("getBeaconBounds()", () => {
const userId = "@user:server";
const roomId = "!room:server";
const makeBeaconWithLocation = (latLon: { lat: number; lon: number }) => {
const geoUri = `geo:${latLon.lat},${latLon.lon}`;
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId, { isLive: true }));
// @ts-ignore private prop, sets internal live property so addLocations works
beacon.checkLiveness();
const location = makeBeaconEvent(userId, {
beaconInfoId: beacon.beaconInfoId,
geoUri,
timestamp: Date.now() + 1,
});
beacon.addLocations([location]);
return beacon;
};
const geo = {
// northern hemi
// west of greenwich
london: { lat: 51.5, lon: -0.14 },
reykjavik: { lat: 64.08, lon: -21.82 },
// east of greenwich
paris: { lat: 48.85, lon: 2.29 },
// southern hemi
// east
auckland: { lat: -36.85, lon: 174.76 }, // nz
// west
lima: { lat: -12.013843, lon: -77.008388 }, // peru
};
const london = makeBeaconWithLocation(geo.london);
const reykjavik = makeBeaconWithLocation(geo.reykjavik);
const paris = makeBeaconWithLocation(geo.paris);
const auckland = makeBeaconWithLocation(geo.auckland);
const lima = makeBeaconWithLocation(geo.lima);
it("should return undefined when there are no beacons", () => {
expect(getBeaconBounds([])).toBeUndefined();
});
it("should return undefined when no beacons have locations", () => {
const beacon = new Beacon(makeBeaconInfoEvent(userId, roomId));
expect(getBeaconBounds([beacon])).toBeUndefined();
});
type TestCase = [string, Beacon[], Bounds];
it.each<TestCase>([
[
"one beacon",
[london],
{ north: geo.london.lat, south: geo.london.lat, east: geo.london.lon, west: geo.london.lon },
],
[
"beacons in the northern hemisphere, west of meridian",
[london, reykjavik],
{ north: geo.reykjavik.lat, south: geo.london.lat, east: geo.london.lon, west: geo.reykjavik.lon },
],
[
"beacons in the northern hemisphere, both sides of meridian",
[london, reykjavik, paris],
// reykjavik northmost and westmost, paris southmost and eastmost
{ north: geo.reykjavik.lat, south: geo.paris.lat, east: geo.paris.lon, west: geo.reykjavik.lon },
],
[
"beacons in the southern hemisphere",
[auckland, lima],
// lima northmost and westmost, auckland southmost and eastmost
{ north: geo.lima.lat, south: geo.auckland.lat, east: geo.auckland.lon, west: geo.lima.lon },
],
[
"beacons in both hemispheres",
[auckland, lima, paris],
{ north: geo.paris.lat, south: geo.auckland.lat, east: geo.auckland.lon, west: geo.lima.lon },
],
])("gets correct bounds for %s", (_description, beacons, expectedBounds) => {
expect(getBeaconBounds(beacons)).toEqual(expectedBounds);
});
});
+109
View File
@@ -0,0 +1,109 @@
/*
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, afterAll, beforeEach } from "vitest";
import { M_TIMESTAMP, Beacon } from "matrix-js-sdk/src/matrix";
import { makeBeaconInfoEvent } from "test-utils";
import { msUntilExpiry, sortBeaconsByLatestExpiry, sortBeaconsByLatestCreation } from "./duration";
describe("beacon utils", () => {
// 14.03.2022 16:15
const now = 1647270879403;
const HOUR_MS = 3600000;
beforeEach(() => {
vi.spyOn(global.Date, "now").mockReturnValue(now);
});
afterAll(() => {
vi.spyOn(global.Date, "now").mockRestore();
});
describe("msUntilExpiry", () => {
it("returns remaining duration", () => {
const start = now - HOUR_MS;
const durationMs = HOUR_MS * 3;
expect(msUntilExpiry(start, durationMs)).toEqual(HOUR_MS * 2);
});
it("returns 0 when expiry has already passed", () => {
// created 3h ago
const start = now - HOUR_MS * 3;
// 1h durations
const durationMs = HOUR_MS;
expect(msUntilExpiry(start, durationMs)).toEqual(0);
});
});
describe("sortBeaconsByLatestExpiry()", () => {
const roomId = "!room:server";
const aliceId = "@alive:server";
// 12h old, 12h left
const beacon1 = new Beacon(
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS * 24, timestamp: now - 12 * HOUR_MS }, "$1"),
);
// 10h left
const beacon2 = new Beacon(
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS * 10, timestamp: now }, "$2"),
);
// 1ms left
const beacon3 = new Beacon(
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS + 1, timestamp: now - HOUR_MS }, "$3"),
);
const noTimestampEvent = makeBeaconInfoEvent(
aliceId,
roomId,
{ timeout: HOUR_MS + 1, timestamp: undefined },
"$3",
);
// beacon info helper defaults to date when timestamp is falsy
// hard set it to undefined
// @ts-ignore
noTimestampEvent.event.content[M_TIMESTAMP.name] = undefined;
const beaconNoTimestamp = new Beacon(noTimestampEvent);
it("sorts beacons by descending expiry time", () => {
expect([beacon2, beacon3, beacon1].sort(sortBeaconsByLatestExpiry)).toEqual([beacon1, beacon2, beacon3]);
});
it("sorts beacons with timestamps before beacons without", () => {
expect([beaconNoTimestamp, beacon3].sort(sortBeaconsByLatestExpiry)).toEqual([beacon3, beaconNoTimestamp]);
});
});
describe("sortBeaconsByLatestCreation()", () => {
const roomId = "!room:server";
const aliceId = "@alive:server";
// 12h old, 12h left
const beacon1 = new Beacon(
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS * 24, timestamp: now - 12 * HOUR_MS }, "$1"),
);
// 10h left
const beacon2 = new Beacon(
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS * 10, timestamp: now }, "$2"),
);
// 1ms left
const beacon3 = new Beacon(
makeBeaconInfoEvent(aliceId, roomId, { timeout: HOUR_MS + 1, timestamp: now - HOUR_MS }, "$3"),
);
it("sorts beacons by descending creation time", () => {
expect([beacon1, beacon2, beacon3].sort(sortBeaconsByLatestCreation)).toEqual([beacon2, beacon3, beacon1]);
});
});
});
@@ -0,0 +1,229 @@
/*
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, afterEach, type Mocked } from "vitest";
import { logger } from "matrix-js-sdk/src/logger";
import { makeGeolocationPosition, mockGeolocation, getMockGeolocationPositionError } from "test-utils";
import {
type GenericPosition,
GeolocationError,
getGeoUri,
mapGeolocationError,
mapGeolocationPositionToTimedGeo,
watchPosition,
getCurrentPosition,
} from "./geolocation";
describe("geolocation utilities", () => {
let geolocation: Mocked<Geolocation>;
const defaultPosition = makeGeolocationPosition({});
// 14.03.2022 16:15
const now = 1647270879403;
beforeEach(() => {
geolocation = mockGeolocation();
vi.spyOn(Date, "now").mockReturnValue(now);
});
afterEach(() => {
vi.spyOn(Date, "now").mockRestore();
vi.spyOn(logger, "error").mockRestore();
});
describe("getGeoUri", () => {
it("Renders a URI with only lat and lon", () => {
const pos: GenericPosition = {
latitude: 43.2,
longitude: 12.4,
altitude: undefined,
accuracy: undefined,
timestamp: 12334,
};
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
});
it("Nulls in location are not shown in URI", () => {
const pos: GenericPosition = {
latitude: 43.2,
longitude: 12.4,
timestamp: 12334,
};
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4");
});
it("Renders a URI with 3 coords", () => {
const pos: GenericPosition = {
latitude: 43.2,
longitude: 12.4,
altitude: 332.54,
accuracy: undefined,
timestamp: 12334,
};
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,332.54");
});
it("Renders a URI with accuracy", () => {
const pos: GenericPosition = {
latitude: 43.2,
longitude: 12.4,
altitude: undefined,
accuracy: 21,
timestamp: 12334,
};
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4;u=21");
});
it("Renders a URI with accuracy and altitude", () => {
const pos = {
latitude: 43.2,
longitude: 12.4,
altitude: 12.3,
accuracy: 21,
timestamp: 12334,
};
expect(getGeoUri(pos)).toEqual("geo:43.2,12.4,12.3;u=21");
});
});
describe("mapGeolocationError", () => {
beforeEach(() => {
// suppress expected errors from test log
vi.spyOn(logger, "error").mockImplementation(() => {});
});
it("returns default for other error", () => {
const error = new Error("oh no..");
expect(mapGeolocationError(error)).toEqual(GeolocationError.Default);
});
it("returns unavailable for unavailable error", () => {
const error = new Error(GeolocationError.Unavailable);
expect(mapGeolocationError(error)).toEqual(GeolocationError.Unavailable);
});
it("maps geo error permissiondenied correctly", () => {
const error = getMockGeolocationPositionError(1, "message");
expect(mapGeolocationError(error)).toEqual(GeolocationError.PermissionDenied);
});
it("maps geo position unavailable error correctly", () => {
const error = getMockGeolocationPositionError(2, "message");
expect(mapGeolocationError(error)).toEqual(GeolocationError.PositionUnavailable);
});
it("maps geo timeout error correctly", () => {
const error = getMockGeolocationPositionError(3, "message");
expect(mapGeolocationError(error)).toEqual(GeolocationError.Timeout);
});
});
describe("mapGeolocationPositionToTimedGeo()", () => {
it("maps geolocation position correctly", () => {
expect(mapGeolocationPositionToTimedGeo(defaultPosition)).toEqual({
timestamp: now,
geoUri: "geo:54.001927,-8.253491;u=1",
});
});
});
describe("watchPosition()", () => {
it("throws with unavailable error when geolocation is not available", () => {
// suppress expected errors from test log
vi.spyOn(logger, "error").mockImplementation(() => {});
// remove the mock we added
vi.spyOn(navigator, "geolocation", "get").mockRestore();
const positionHandler = vi.fn();
const errorHandler = vi.fn();
expect(() => watchPosition(positionHandler, errorHandler)).toThrow(GeolocationError.Unavailable);
});
it("sets up position handler with correct options", () => {
const positionHandler = vi.fn();
const errorHandler = vi.fn();
watchPosition(positionHandler, errorHandler);
const [, , options] = geolocation.watchPosition.mock.calls[0];
expect(options).toEqual({
maximumAge: 60000,
timeout: 10000,
});
});
it("returns clearWatch function", () => {
const watchId = 1;
geolocation.watchPosition.mockReturnValue(watchId);
const positionHandler = vi.fn();
const errorHandler = vi.fn();
const clearWatch = watchPosition(positionHandler, errorHandler);
clearWatch();
expect(geolocation.clearWatch).toHaveBeenCalledWith(watchId);
});
it("calls position handler with position", () => {
const positionHandler = vi.fn();
const errorHandler = vi.fn();
watchPosition(positionHandler, errorHandler);
expect(positionHandler).toHaveBeenCalledWith(defaultPosition);
});
it("maps geolocation position error and calls error handler", () => {
// suppress expected errors from test log
vi.spyOn(logger, "error").mockImplementation(() => {});
geolocation.watchPosition.mockImplementation((_callback, error) => {
error!(getMockGeolocationPositionError(1, "message"));
return -1;
});
const positionHandler = vi.fn();
const errorHandler = vi.fn();
watchPosition(positionHandler, errorHandler);
expect(errorHandler).toHaveBeenCalledWith(GeolocationError.PermissionDenied);
});
});
describe("getCurrentPosition()", () => {
it("throws with unavailable error when geolocation is not available", async () => {
// suppress expected errors from test log
vi.spyOn(logger, "error").mockImplementation(() => {});
// remove the mock we added
vi.spyOn(navigator, "geolocation", "get").mockRestore();
await expect(() => getCurrentPosition()).rejects.toThrow(GeolocationError.Unavailable);
});
it("throws with geolocation error when geolocation.getCurrentPosition fails", async () => {
// suppress expected errors from test log
vi.spyOn(logger, "error").mockImplementation(() => {});
const timeoutError = getMockGeolocationPositionError(3, "message");
geolocation.getCurrentPosition.mockImplementation((callback, error) => error!(timeoutError));
await expect(() => getCurrentPosition()).rejects.toThrow(GeolocationError.Timeout);
});
it("resolves with current location", async () => {
geolocation.getCurrentPosition.mockImplementation((callback, error) => callback(defaultPosition));
const result = await getCurrentPosition();
expect(result).toEqual(defaultPosition);
});
});
});
@@ -0,0 +1,41 @@
/*
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 { describe, it, expect } from "vitest";
import { EventType, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { makeBeaconInfoEvent, stubClient } from "test-utils";
import { shouldDisplayAsBeaconTile } from "./timeline";
describe("shouldDisplayAsBeaconTile", () => {
const userId = "@user:server";
const roomId = "!room:server";
const liveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: true });
const notLiveBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: false });
const memberEvent = new MatrixEvent({ type: EventType.RoomMember });
const redactedBeacon = makeBeaconInfoEvent(userId, roomId, { isLive: false });
redactedBeacon.makeRedacted(redactedBeacon, new Room(roomId, stubClient(), userId));
it("returns true for a beacon with live property set to true", () => {
expect(shouldDisplayAsBeaconTile(liveBeacon)).toBe(true);
});
it("returns true for a redacted beacon", () => {
expect(shouldDisplayAsBeaconTile(redactedBeacon)).toBe(true);
});
it("returns false for a beacon with live property set to false", () => {
expect(shouldDisplayAsBeaconTile(notLiveBeacon)).toBe(false);
});
it("returns false for a non beacon event", () => {
expect(shouldDisplayAsBeaconTile(memberEvent)).toBe(false);
});
});
@@ -0,0 +1,74 @@
/*
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, type Mocked } from "vitest";
import { type Device, type MatrixClient } from "matrix-js-sdk/src/matrix";
import { getMockClientWithEventEmitter, mockClientMethodsCrypto } from "test-utils";
import { getDeviceCryptoInfo, getUserDeviceIds } from "./deviceInfo";
describe("getDeviceCryptoInfo()", () => {
let mockClient: Mocked<MatrixClient>;
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsCrypto() });
});
it("should return undefined on clients with no crypto", async () => {
vi.spyOn(mockClient, "getCrypto").mockReturnValue(undefined);
await expect(getDeviceCryptoInfo(mockClient, "@user:id", "device_id")).resolves.toBeUndefined();
});
it("should return undefined for unknown users", async () => {
vi.mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(new Map());
await expect(getDeviceCryptoInfo(mockClient, "@user:id", "device_id")).resolves.toBeUndefined();
});
it("should return undefined for unknown devices", async () => {
vi.mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(new Map([["@user:id", new Map()]]));
await expect(getDeviceCryptoInfo(mockClient, "@user:id", "device_id")).resolves.toBeUndefined();
});
it("should return the right result for known devices", async () => {
const mockDevice = { deviceId: "device_id" } as Device;
vi.mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(
new Map([["@user:id", new Map([["device_id", mockDevice]])]]),
);
await expect(getDeviceCryptoInfo(mockClient, "@user:id", "device_id")).resolves.toBe(mockDevice);
expect(mockClient.getCrypto()!.getUserDeviceInfo).toHaveBeenCalledWith(["@user:id"], undefined);
});
});
describe("getUserDeviceIds", () => {
let mockClient: Mocked<MatrixClient>;
beforeEach(() => {
mockClient = getMockClientWithEventEmitter({ ...mockClientMethodsCrypto() });
});
it("should return empty set on clients with no crypto", async () => {
vi.spyOn(mockClient, "getCrypto").mockReturnValue(undefined);
await expect(getUserDeviceIds(mockClient, "@user:id")).resolves.toEqual(new Set());
});
it("should return empty set for unknown users", async () => {
vi.mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(new Map());
await expect(getUserDeviceIds(mockClient, "@user:id")).resolves.toEqual(new Set());
});
it("should return the right result for known users", async () => {
const mockDevice = { deviceId: "device_id" } as Device;
vi.mocked(mockClient.getCrypto()!.getUserDeviceInfo).mockResolvedValue(
new Map([["@user:id", new Map([["device_id", mockDevice]])]]),
);
await expect(getUserDeviceIds(mockClient, "@user:id")).resolves.toEqual(new Set(["device_id"]));
expect(mockClient.getCrypto()!.getUserDeviceInfo).toHaveBeenCalledWith(["@user:id"]);
});
});
@@ -0,0 +1,64 @@
/*
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 { getMockClientWithEventEmitter } from "test-utils";
import { shouldForceDisableEncryption } from "./shouldForceDisableEncryption";
describe("shouldForceDisableEncryption()", () => {
const mockClient = getMockClientWithEventEmitter({
getClientWellKnown: vi.fn(),
});
beforeEach(() => {
mockClient.getClientWellKnown.mockReturnValue(undefined);
});
it("should return false when there is no e2ee well known", () => {
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
});
it("should return false when there is no force_disable property", () => {
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
// empty
},
});
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
});
it("should return false when force_disable property is falsy", () => {
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
force_disable: false,
},
});
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
});
it("should return false when force_disable property is not equal to true", () => {
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
force_disable: 1,
},
});
expect(shouldForceDisableEncryption(mockClient)).toEqual(false);
});
it("should return true when force_disable property is true", () => {
mockClient.getClientWellKnown.mockReturnValue({
"io.element.e2ee": {
force_disable: true,
},
});
expect(shouldForceDisableEncryption(mockClient)).toEqual(true);
});
});
@@ -0,0 +1,124 @@
/*
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, afterAll, beforeEach } from "vitest";
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
import { getMockClientWithEventEmitter } from "test-utils";
import type BasePlatform from "../../BasePlatform";
import { type IConfigOptions } from "../../IConfigOptions";
import { getDeviceClientInformation, recordClientInformation } from "./clientInformation";
import { DEFAULTS } from "../../SdkConfig";
import { type DeepReadonly } from "../../@types/common";
describe("recordClientInformation()", () => {
const deviceId = "my-device-id";
const version = "1.2.3";
const isElectron = window.electron;
const mockClient = getMockClientWithEventEmitter({
getDeviceId: vi.fn().mockReturnValue(deviceId),
setAccountData: vi.fn(),
});
const sdkConfig: DeepReadonly<IConfigOptions> = {
...DEFAULTS,
brand: "Test Brand",
element_call: { use_exclusively: false, brand: "Element Call" },
};
const platform = {
getAppVersion: vi.fn().mockResolvedValue(version),
} as unknown as BasePlatform;
beforeEach(() => {
vi.clearAllMocks();
window.electron = undefined;
});
afterAll(() => {
// restore global
window.electron = isElectron;
});
it("saves client information without url for electron clients", async () => {
window.electron = {} as Electron;
await recordClientInformation(mockClient, sdkConfig, platform);
expect(mockClient.setAccountData).toHaveBeenCalledWith(`io.element.matrix_client_information.${deviceId}`, {
name: sdkConfig.brand,
version,
url: undefined,
});
});
it("saves client information with url for non-electron clients", async () => {
await recordClientInformation(mockClient, sdkConfig, platform);
expect(mockClient.setAccountData).toHaveBeenCalledWith(`io.element.matrix_client_information.${deviceId}`, {
name: sdkConfig.brand,
version,
url: "localhost",
});
});
});
describe("getDeviceClientInformation()", () => {
const deviceId = "my-device-id";
const mockClient = getMockClientWithEventEmitter({
getAccountData: vi.fn(),
});
beforeEach(() => {
vi.resetAllMocks();
});
it("returns an empty object when no event exists for the device", () => {
expect(getDeviceClientInformation(mockClient, deviceId)).toEqual({});
expect(mockClient.getAccountData).toHaveBeenCalledWith(`io.element.matrix_client_information.${deviceId}`);
});
it("returns client information for the device", () => {
const eventContent = {
name: "Element Web",
version: "1.2.3",
url: "test.com",
};
const event = new MatrixEvent({
type: `io.element.matrix_client_information.${deviceId}`,
content: eventContent,
});
mockClient.getAccountData.mockReturnValue(event);
expect(getDeviceClientInformation(mockClient, deviceId)).toEqual(eventContent);
});
it("excludes values with incorrect types", () => {
const eventContent = {
extraField: "hello",
name: "Element Web",
// wrong format
version: { value: "1.2.3" },
url: "test.com",
};
const event = new MatrixEvent({
type: `io.element.matrix_client_information.${deviceId}`,
content: eventContent,
});
mockClient.getAccountData.mockReturnValue(event);
// invalid fields excluded
expect(getDeviceClientInformation(mockClient, deviceId)).toEqual({
name: eventContent.name,
url: eventContent.url,
});
});
});
@@ -0,0 +1,137 @@
/*
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 { DeviceType, type ExtendedDeviceInformation, parseUserAgent } from "./parseUserAgent";
const makeDeviceExtendedInfo = (
deviceType: DeviceType,
deviceModel?: string,
deviceOperatingSystem?: string,
clientName?: string,
clientVersion?: string,
): ExtendedDeviceInformation => ({
deviceType,
deviceModel,
deviceOperatingSystem,
client: clientName && [clientName, clientVersion].filter(Boolean).join(" "),
});
/* eslint-disable max-len */
const ANDROID_UA = [
// New User Agent Implementation
"Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google (Nexus) 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google (Nexus) (5); Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
// Legacy User Agent Implementation
"Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)",
"Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)",
"Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
];
const ANDROID_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Mobile, "Xiaomi Mi 9T", "Android 11"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G960F", "Android 6.0.1"),
makeDeviceExtendedInfo(DeviceType.Mobile, "LG Nexus 5", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) 5", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) (5)", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-A510F", "Android 6.0.1"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G610M", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G973U", "Android 9", "Chrome", "69.0.3497.100"),
];
const IOS_UA = [
"Element/1.8.21 (iPhone; iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPad Pro (11-inch); iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPad Pro (12.9-inch) (3rd generation); iOS 15.2; Scale/3.00)",
"Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
"Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
];
const IOS_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone XS Max", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (11-inch)", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (12.9-inch) (3rd generation)", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Web, "Apple iPad", "iOS", "Mobile Safari", "8.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 8.4.1", "Mobile Safari", "8.0"),
];
const DESKTOP_UA = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102" +
" Electron/20.1.1 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36",
];
const DESKTOP_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Desktop, "Apple Macintosh", "Mac OS", "Electron", "20.1.1"),
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Windows", "Electron", "20.1.1"),
];
const WEB_UA = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18",
"Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
];
const WEB_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Chrome", "104.0.5112.102"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Chrome", "104.0.5112.102"),
makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Firefox", "39.0"),
makeDeviceExtendedInfo(DeviceType.Web, "Apple Macintosh", "Mac OS", "Safari", "8.0.3"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Firefox", "40.0"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows", "Edge", "12.246"),
];
const MISC_UA = [
"AppleTV11,1/11.1",
"Curl Client/1.0",
"banana",
"",
// fluffy chat ios
"Dart/2.18 (dart:io)",
];
const MISC_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Unknown, "Apple Apple TV", undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
];
/* eslint-disable max-len */
describe("parseUserAgent()", () => {
it("returns deviceType unknown when user agent is falsy", () => {
expect(parseUserAgent(undefined)).toEqual({
deviceType: DeviceType.Unknown,
});
});
type TestCase = [string, ExtendedDeviceInformation];
const testPlatform = (platform: string, userAgents: string[], results: ExtendedDeviceInformation[]): void => {
const testCases: TestCase[] = userAgents.map((userAgent, index) => [userAgent, results[index]]);
describe(`on platform ${platform}`, () => {
it.each(testCases)("should parse the user agent correctly - %s", (userAgent, expectedResult) => {
expect(parseUserAgent(userAgent)).toEqual(expectedResult);
});
});
};
testPlatform("Android", ANDROID_UA, ANDROID_EXPECTED_RESULT);
testPlatform("iOS", IOS_UA, IOS_EXPECTED_RESULT);
testPlatform("Desktop", DESKTOP_UA, DESKTOP_EXPECTED_RESULT);
testPlatform("Web", WEB_UA, WEB_EXPECTED_RESULT);
testPlatform("Misc", MISC_UA, MISC_EXPECTED_RESULT);
});
@@ -0,0 +1,97 @@
/*
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, afterAll, beforeEach } from "vitest";
import { logger } from "matrix-js-sdk/src/logger";
import {
isBulkUnverifiedDeviceReminderSnoozed,
snoozeBulkUnverifiedDeviceReminder,
} from "./snoozeBulkUnverifiedDeviceReminder";
const SNOOZE_KEY = "mx_snooze_bulk_unverified_device_nag";
describe("snooze bulk unverified device nag", () => {
const localStorageSetSpy = vi.spyOn(localStorage.__proto__, "setItem");
const localStorageGetSpy = vi.spyOn(localStorage.__proto__, "getItem");
const localStorageRemoveSpy = vi.spyOn(localStorage.__proto__, "removeItem");
// 14.03.2022 16:15
const now = 1647270879403;
beforeEach(() => {
localStorageSetSpy.mockClear().mockImplementation(() => {});
localStorageGetSpy.mockClear().mockReturnValue(null);
localStorageRemoveSpy.mockClear().mockImplementation(() => {});
vi.spyOn(Date, "now").mockReturnValue(now);
});
afterAll(() => {
vi.restoreAllMocks();
});
describe("snoozeBulkUnverifiedDeviceReminder()", () => {
it("sets the current time in local storage", () => {
snoozeBulkUnverifiedDeviceReminder();
expect(localStorageSetSpy).toHaveBeenCalledWith(SNOOZE_KEY, now.toString());
});
it("catches an error from localstorage", () => {
const loggerErrorSpy = vi.spyOn(logger, "error");
localStorageSetSpy.mockImplementation(() => {
throw new Error("oups");
});
snoozeBulkUnverifiedDeviceReminder();
expect(loggerErrorSpy).toHaveBeenCalled();
});
});
describe("isBulkUnverifiedDeviceReminderSnoozed()", () => {
it("returns false when there is no snooze in storage", () => {
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(localStorageGetSpy).toHaveBeenCalledWith(SNOOZE_KEY);
expect(result).toBe(false);
});
it("catches an error from localstorage and returns false", () => {
const loggerErrorSpy = vi.spyOn(logger, "error");
localStorageGetSpy.mockImplementation(() => {
throw new Error("oups");
});
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(result).toBe(false);
expect(loggerErrorSpy).toHaveBeenCalled();
});
it("returns false when snooze timestamp in storage is not a number", () => {
localStorageGetSpy.mockReturnValue("test");
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(result).toBe(false);
});
it("returns false when snooze timestamp in storage is over a week ago", () => {
const msDay = 1000 * 60 * 60 * 24;
// snoozed 8 days ago
localStorageGetSpy.mockReturnValue(now - msDay * 8);
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(result).toBe(false);
});
it("returns true when snooze timestamp in storage is less than a week ago", () => {
const msDay = 1000 * 60 * 60 * 24;
// snoozed 8 days ago
localStorageGetSpy.mockReturnValue(now - msDay * 6);
const result = isBulkUnverifiedDeviceReminderSnoozed();
expect(result).toBe(true);
});
});
});
@@ -0,0 +1,43 @@
/*
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 { describe, it, expect, beforeEach } from "vitest";
import { Room } from "matrix-js-sdk/src/matrix";
import { createTestClient } from "test-utils";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom";
import { isLocalRoom } from "./isLocalRoom";
describe("isLocalRoom", () => {
let room: Room;
let localRoom: LocalRoom;
beforeEach(() => {
const client = createTestClient();
room = new Room("!room:example.com", client, client.getUserId()!);
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, client.getUserId()!);
});
it("should return false for a Room", () => {
expect(isLocalRoom(room)).toBe(false);
});
it("should return false for a non-local room ID", () => {
expect(isLocalRoom(room.roomId)).toBe(false);
});
it("should return true for LocalRoom", () => {
expect(isLocalRoom(localRoom)).toBe(true);
});
it("should return true for local room ID", () => {
expect(isLocalRoom(LOCAL_ROOM_ID_PREFIX + "test")).toBe(true);
});
});
@@ -0,0 +1,125 @@
/*
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 } from "vitest";
import { EventType, type MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { createTestClient, makeMembershipEvent, mkEvent } from "test-utils";
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../models/LocalRoom";
import { DirectoryMember } from "../direct-messages";
import { isRoomReady } from "./isRoomReady";
describe("isRoomReady", () => {
const userId1 = "@user1:example.com";
const member1 = new DirectoryMember({ user_id: userId1 });
const userId2 = "@user2:example.com";
let room1: Room;
let localRoom: LocalRoom;
let client: MatrixClient;
beforeEach(() => {
client = createTestClient();
room1 = new Room("!room1:example.com", client, userId1);
room1.getMyMembership = () => KnownMembership.Join;
localRoom = new LocalRoom(LOCAL_ROOM_ID_PREFIX + "test", client, "@test:example.com");
});
beforeEach(() => {
localRoom.targets = [member1];
});
it("should return false if the room has no actual room id", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("for a room with an actual room id", () => {
beforeEach(() => {
localRoom.actualRoomId = room1.roomId;
vi.mocked(client.getRoom).mockReturnValue(null);
});
it("should return false", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("and the room is known to the client", () => {
beforeEach(() => {
vi.mocked(client.getRoom).mockImplementation((roomId?: string) => {
if (roomId === room1.roomId) return room1;
return null;
});
});
it("should return false", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("and all members have been invited or joined", () => {
beforeEach(() => {
room1.currentState.setStateEvents([
makeMembershipEvent(room1.roomId, userId1, KnownMembership.Join),
makeMembershipEvent(room1.roomId, userId2, KnownMembership.Invite),
]);
});
it("should return false", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("and a RoomHistoryVisibility event", () => {
beforeEach(() => {
room1.currentState.setStateEvents([
mkEvent({
user: userId1,
event: true,
type: EventType.RoomHistoryVisibility,
room: room1.roomId,
content: {},
}),
]);
});
it("should return true", () => {
expect(isRoomReady(client, localRoom)).toBe(true);
});
describe("and an encrypted room", () => {
beforeEach(() => {
localRoom.encrypted = true;
});
it("should return false", () => {
expect(isRoomReady(client, localRoom)).toBe(false);
});
describe("and a room encryption state event", () => {
beforeEach(() => {
room1.currentState.setStateEvents([
mkEvent({
user: userId1,
event: true,
type: EventType.RoomEncryption,
room: room1.roomId,
content: {},
}),
]);
});
it("should return true", () => {
expect(isRoomReady(client, localRoom)).toBe(true);
});
});
});
});
});
});
});
});