Prepare for repo merge

Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
Michael Telatynski 2024-10-15 11:35:21 +01:00
parent 0f670b8dc0
commit b084ff2313
No known key found for this signature in database
GPG key ID: A2B008A5F49F5D0D
807 changed files with 0 additions and 0 deletions

View file

@ -1,74 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import EventEmitter from "events";
import { SimpleObservable } from "matrix-widget-api";
import { Playback, PlaybackState } from "../../src/audio/Playback";
import { PlaybackClock } from "../../src/audio/PlaybackClock";
import { UPDATE_EVENT } from "../../src/stores/AsyncStore";
import { PublicInterface } from "../@types/common";
export const createTestPlayback = (overrides: Partial<Playback> = {}): Playback => {
const eventEmitter = new EventEmitter();
return {
thumbnailWaveform: [1, 2, 3],
sizeBytes: 23,
waveform: [4, 5, 6],
waveformData: new SimpleObservable<number[]>(),
destroy: jest.fn(),
play: jest.fn(),
prepare: jest.fn(),
pause: jest.fn(),
stop: jest.fn(),
toggle: jest.fn(),
skipTo: jest.fn(),
isPlaying: false,
clockInfo: createTestPlaybackClock(),
currentState: PlaybackState.Stopped,
emit: (event: PlaybackState, ...args: any[]): boolean => {
eventEmitter.emit(event, ...args);
eventEmitter.emit(UPDATE_EVENT, event, ...args);
return true;
},
// EventEmitter
on: eventEmitter.on.bind(eventEmitter) as Playback["on"],
once: eventEmitter.once.bind(eventEmitter) as Playback["once"],
off: eventEmitter.off.bind(eventEmitter) as Playback["off"],
addListener: eventEmitter.addListener.bind(eventEmitter) as Playback["addListener"],
removeListener: eventEmitter.removeListener.bind(eventEmitter) as Playback["removeListener"],
removeAllListeners: eventEmitter.removeAllListeners.bind(eventEmitter) as Playback["removeAllListeners"],
getMaxListeners: eventEmitter.getMaxListeners.bind(eventEmitter) as Playback["getMaxListeners"],
setMaxListeners: eventEmitter.setMaxListeners.bind(eventEmitter) as Playback["setMaxListeners"],
listeners: eventEmitter.listeners.bind(eventEmitter) as Playback["listeners"],
rawListeners: eventEmitter.rawListeners.bind(eventEmitter) as Playback["rawListeners"],
listenerCount: eventEmitter.listenerCount.bind(eventEmitter) as Playback["listenerCount"],
eventNames: eventEmitter.eventNames.bind(eventEmitter) as Playback["eventNames"],
prependListener: eventEmitter.prependListener.bind(eventEmitter) as Playback["prependListener"],
prependOnceListener: eventEmitter.prependOnceListener.bind(eventEmitter) as Playback["prependOnceListener"],
liveData: new SimpleObservable<number[]>(),
durationSeconds: 31415,
timeSeconds: 3141,
...overrides,
} as PublicInterface<Playback> as Playback;
};
export const createTestPlaybackClock = (): PlaybackClock => {
return {
durationSeconds: 31,
timeSeconds: 41,
liveData: new SimpleObservable<number[]>(),
populatePlaceholdersFrom: jest.fn(),
flagLoadTime: jest.fn(),
flagStart: jest.fn(),
flagStop: jest.fn(),
syncTo: jest.fn(),
destroy: jest.fn(),
} as PublicInterface<PlaybackClock> as PlaybackClock;
};

View file

@ -1,209 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { MockedObject } from "jest-mock";
import {
MatrixClient,
MatrixEvent,
Beacon,
getBeaconInfoIdentifier,
ContentHelpers,
LocationAssetType,
M_BEACON,
M_BEACON_INFO,
} from "matrix-js-sdk/src/matrix";
import { getMockGeolocationPositionError } from "./location";
import { makeRoomWithStateEvents } from "./room";
type InfoContentProps = {
timeout: number;
isLive?: boolean;
assetType?: LocationAssetType;
description?: string;
timestamp?: number;
};
const DEFAULT_INFO_CONTENT_PROPS: InfoContentProps = {
timeout: 3600000,
};
/**
* Create an m.beacon_info event
* all required properties are mocked
* override with contentProps
*/
export const makeBeaconInfoEvent = (
sender: string,
roomId: string,
contentProps: Partial<InfoContentProps> = {},
eventId?: string,
): MatrixEvent => {
const { timeout, isLive, description, assetType, timestamp } = {
...DEFAULT_INFO_CONTENT_PROPS,
...contentProps,
};
const event = new MatrixEvent({
type: M_BEACON_INFO.name,
room_id: roomId,
state_key: sender,
sender,
content: ContentHelpers.makeBeaconInfoContent(timeout, isLive, description, assetType, timestamp),
});
event.event.origin_server_ts = Date.now();
// live beacons use the beacon_info event id
// set or default this
event.replaceLocalEventId(eventId || `$${Math.random()}-${Math.random()}`);
return event;
};
type ContentProps = {
geoUri: string;
timestamp: number;
beaconInfoId: string;
description?: string;
};
const DEFAULT_CONTENT_PROPS: ContentProps = {
geoUri: "geo:-36.24484561954707,175.46884959563613;u=10",
timestamp: 123,
beaconInfoId: "$123",
};
/**
* Create an m.beacon event
* all required properties are mocked
* override with contentProps
*/
export const makeBeaconEvent = (
sender: string,
contentProps: Partial<ContentProps> = {},
roomId?: string,
): MatrixEvent => {
const { geoUri, timestamp, beaconInfoId, description } = {
...DEFAULT_CONTENT_PROPS,
...contentProps,
};
return new MatrixEvent({
type: M_BEACON.name,
room_id: roomId,
sender,
content: ContentHelpers.makeBeaconContent(geoUri, timestamp, beaconInfoId, description),
origin_server_ts: 0,
});
};
/**
* Create a mock geolocation position
* defaults all required properties
*/
export const makeGeolocationPosition = ({
timestamp,
coords,
}: {
timestamp?: number;
coords?: Partial<GeolocationCoordinates>;
}): GeolocationPosition =>
({
timestamp: timestamp ?? 1647256791840,
coords: {
accuracy: 1,
latitude: 54.001927,
longitude: -8.253491,
altitude: null,
altitudeAccuracy: null,
heading: null,
speed: null,
...coords,
},
}) as unknown as GeolocationPosition;
/**
* Creates a basic mock of Geolocation
* sets navigator.geolocation to the mock
* and returns mock
*/
export const mockGeolocation = (): MockedObject<Geolocation> => {
const mockGeolocation = {
clearWatch: jest.fn(),
getCurrentPosition: jest.fn().mockImplementation((callback) => callback(makeGeolocationPosition({}))),
watchPosition: jest.fn().mockImplementation((callback) => callback(makeGeolocationPosition({}))),
} as unknown as MockedObject<Geolocation>;
// jest jsdom does not provide geolocation
// @ts-ignore illegal assignment to readonly property
navigator.geolocation = mockGeolocation;
return mockGeolocation;
};
/**
* Creates a mock watchPosition implementation
* that calls success callback at the provided delays
* ```
* geolocation.watchPosition.mockImplementation([0, 1000, 5000, 50])
* ```
* will call the provided handler with a mock position at
* next tick, 1000ms, 6000ms, 6050ms
*
* to produce errors provide an array of error codes
* that will be applied to the delay with the same index
* eg:
* ```
* // return two good positions, then a permission denied error
* geolocation.watchPosition.mockImplementation(watchPositionMockImplementation(
* [0, 1000, 3000], [0, 0, 1]),
* );
* ```
* See for error codes: https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
*/
export const watchPositionMockImplementation = (delays: number[], errorCodes: number[] = []) => {
return (callback: PositionCallback, error: PositionErrorCallback): number => {
const position = makeGeolocationPosition({});
let totalDelay = 0;
delays.map((delayMs, index) => {
totalDelay += delayMs;
const timeout = window.setTimeout(() => {
if (errorCodes[index]) {
error(getMockGeolocationPositionError(errorCodes[index], "error message"));
} else {
callback({ ...position, timestamp: position.timestamp + totalDelay });
}
}, totalDelay);
return timeout;
});
return totalDelay;
};
};
/**
* Creates a room with beacon events
* sets given locations on beacons
* returns beacons
*/
export const makeRoomWithBeacons = (
roomId: string,
mockClient: MockedObject<MatrixClient>,
beaconInfoEvents: MatrixEvent[],
locationEvents?: MatrixEvent[],
): Beacon[] => {
const room = makeRoomWithStateEvents(beaconInfoEvents, { roomId, mockClient });
const beacons = beaconInfoEvents.map((event) => room.currentState.beacons.get(getBeaconInfoIdentifier(event))!);
if (locationEvents) {
beacons.forEach((beacon) => {
// this filtering happens in roomState, which is bypassed here
const validLocationEvents = locationEvents?.filter((event) => event.getSender() === beacon.beaconInfoOwner);
beacon.addLocations(validLocationEvents);
});
}
return beacons;
};

View file

@ -1,107 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { MatrixWidgetType } from "matrix-widget-api";
import type { GroupCall, Room, RoomMember, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { mkEvent } from "./test-utils";
import { Call, ConnectionState, ElementCall, JitsiCall } from "../../src/models/Call";
import { CallStore } from "../../src/stores/CallStore";
export class MockedCall extends Call {
public static readonly EVENT_TYPE = "org.example.mocked_call";
public readonly STUCK_DEVICE_TIMEOUT_MS = 1000 * 60 * 60; // 1 hour
private constructor(
room: Room,
public readonly event: MatrixEvent,
) {
super(
{
id: event.getStateKey()!,
eventId: "$1:example.org",
roomId: room.roomId,
type: MatrixWidgetType.Custom,
url: "https://example.org",
name: "Group call",
creatorUserId: "@alice:example.org",
// waitForIframeLoad = false, makes the widget API wait for the 'contentLoaded' event.
waitForIframeLoad: false,
},
room.client,
);
this.groupCall = { creationTs: this.event.getTs() } as unknown as GroupCall;
}
public static get(room: Room): MockedCall | null {
const [event] = room.currentState.getStateEvents(this.EVENT_TYPE);
return event === undefined || "m.terminated" in event.getContent() ? null : new MockedCall(room, event);
}
public static create(room: Room, id: string) {
room.addLiveEvents([
mkEvent({
event: true,
type: this.EVENT_TYPE,
room: room.roomId,
user: "@alice:example.org",
content: { "m.type": "m.video", "m.intent": "m.prompt" },
skey: id,
ts: Date.now(),
}),
]);
// @ts-ignore deliberately calling a private method
// Let CallStore know that a call might now exist
CallStore.instance.updateRoom(room);
}
public readonly groupCall: GroupCall;
public get participants(): Map<RoomMember, Set<string>> {
return super.participants;
}
public set participants(value: Map<RoomMember, Set<string>>) {
super.participants = value;
}
public setConnectionState(value: ConnectionState): void {
super.connectionState = value;
}
// No action needed for any of the following methods since this is just a mock
public async clean(): Promise<void> {}
// Public to allow spying
public async performConnection(): Promise<void> {}
public async performDisconnection(): Promise<void> {}
public destroy() {
// Terminate the call for good measure
this.room.addLiveEvents([
mkEvent({
event: true,
type: MockedCall.EVENT_TYPE,
room: this.room.roomId,
user: "@alice:example.org",
content: { ...this.event.getContent(), "m.terminated": "Call ended" },
skey: this.widget.id,
ts: Date.now(),
}),
]);
super.destroy();
}
}
/**
* Sets up the call store to use mocked calls.
*/
export const useMockedCalls = () => {
Call.get = (room) => MockedCall.get(room);
JitsiCall.create = async (room) => MockedCall.create(room, "1");
ElementCall.create = async (room) => MockedCall.create(room, "1");
};

View file

@ -1,175 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import EventEmitter from "events";
import { MethodLikeKeys, mocked, MockedObject, PropertyLikeKeys } from "jest-mock";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { MatrixClient, Room, MatrixError, User } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
/**
* Mocked generic class with a real EventEmitter.
* Useful for mocks which need event emitters.
*/
export class MockEventEmitter<T> extends EventEmitter {
/**
* Construct a new event emitter with additional properties/functions. The event emitter functions
* like .emit and .on will be real.
* @param mockProperties An object with the mock property or function implementations. 'getters'
* are correctly cloned to this event emitter.
*/
constructor(mockProperties: Partial<Record<MethodLikeKeys<T> | PropertyLikeKeys<T>, unknown>> = {}) {
super();
// We must use defineProperties and not assign as the former clones getters correctly,
// whereas the latter invokes the getter and sets the return value permanently on the
// destination object.
Object.defineProperties(this, Object.getOwnPropertyDescriptors(mockProperties));
}
}
/**
* Mock client with real event emitter
* useful for testing code that listens
* to MatrixClient events
*/
export class MockClientWithEventEmitter extends EventEmitter {
constructor(mockProperties: Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> = {}) {
super();
Object.assign(this, mockProperties);
}
}
/**
* - make a mock client
* - cast the type to mocked(MatrixClient)
* - spy on MatrixClientPeg.get to return the mock
* eg
* ```
* const mockClient = getMockClientWithEventEmitter({
getUserId: jest.fn().mockReturnValue(aliceId),
});
* ```
*
* See also {@link stubClient} which does something similar but uses a more complete mock client.
*/
export const getMockClientWithEventEmitter = (
mockProperties: Partial<Record<keyof MatrixClient, unknown>>,
): MockedObject<MatrixClient> => {
const mock = mocked(new MockClientWithEventEmitter(mockProperties) as unknown as MatrixClient);
jest.spyOn(MatrixClientPeg, "get").mockReturnValue(mock);
jest.spyOn(MatrixClientPeg, "safeGet").mockReturnValue(mock);
// @ts-ignore simplified test stub
mock.canSupport = new Map();
Object.keys(Feature).forEach((feature) => {
mock.canSupport.set(feature as Feature, ServerSupport.Stable);
});
return mock;
};
export const unmockClientPeg = () => {
jest.spyOn(MatrixClientPeg, "get").mockRestore();
jest.spyOn(MatrixClientPeg, "safeGet").mockRestore();
};
/**
* Returns basic mocked client methods related to the current user
* ```
* const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser('@mytestuser:domain'),
});
* ```
*/
export const mockClientMethodsUser = (userId = "@alice:domain") => ({
getUserId: jest.fn().mockReturnValue(userId),
getDomain: jest.fn().mockReturnValue(userId.split(":")[1]),
getSafeUserId: jest.fn().mockReturnValue(userId),
getUser: jest.fn().mockReturnValue(new User(userId)),
isGuest: jest.fn().mockReturnValue(false),
mxcUrlToHttp: jest.fn().mockReturnValue("mock-mxcUrlToHttp"),
credentials: { userId },
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
getAccessToken: jest.fn(),
getDeviceId: jest.fn(),
getAccountData: jest.fn(),
});
/**
* Returns basic mocked client methods related to rendering events
* ```
* const mockClient = getMockClientWithEventEmitter({
...mockClientMethodsUser('@mytestuser:domain'),
});
* ```
*/
export const mockClientMethodsEvents = () => ({
decryptEventIfNeeded: jest.fn(),
getPushActionsForEvent: jest.fn(),
});
/**
* Returns basic mocked client methods related to server support
*/
export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getIdentityServerUrl: jest.fn(),
getHomeserverUrl: jest.fn(),
getCapabilities: jest.fn().mockResolvedValue({}),
getClientWellKnown: jest.fn().mockReturnValue({}),
waitForClientWellKnown: jest.fn().mockResolvedValue({}),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
isVersionSupported: jest.fn().mockResolvedValue(false),
getVersions: jest.fn().mockResolvedValue({}),
isFallbackICEServerAllowed: jest.fn(),
getAuthIssuer: jest.fn().mockRejectedValue(new MatrixError({ errcode: "M_UNKNOWN" }, 404)),
});
export const mockClientMethodsDevice = (
deviceId = "test-device-id",
): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getDeviceId: jest.fn().mockReturnValue(deviceId),
getDevices: jest.fn().mockResolvedValue({ devices: [] }),
});
export const mockClientMethodsCrypto = (): Partial<
Record<MethodLikeKeys<MatrixClient> & PropertyLikeKeys<MatrixClient>, unknown>
> => ({
isCryptoEnabled: jest.fn(),
isCrossSigningReady: jest.fn(),
isKeyBackupKeyStored: jest.fn(),
getCrossSigningCacheCallbacks: jest.fn().mockReturnValue({ getCrossSigningKeyCache: jest.fn() }),
getStoredCrossSigningForUser: jest.fn(),
getKeyBackupVersion: jest.fn().mockResolvedValue(null),
secretStorage: { hasKey: jest.fn() },
getCrypto: jest.fn().mockReturnValue({
getUserDeviceInfo: jest.fn(),
getCrossSigningStatus: jest.fn().mockResolvedValue({
publicKeysOnDevice: true,
privateKeysInSecretStorage: false,
privateKeysCachedLocally: {
masterKey: true,
selfSigningKey: true,
userSigningKey: true,
},
}),
isCrossSigningReady: jest.fn().mockResolvedValue(true),
isSecretStorageReady: jest.fn(),
getSessionBackupPrivateKey: jest.fn(),
getVersion: jest.fn().mockReturnValue("Version 0"),
getOwnDeviceKeys: jest.fn().mockReturnValue(new Promise(() => {})),
getCrossSigningKeyId: jest.fn(),
}),
});
export const mockClientMethodsRooms = (rooms: Room[] = []): Partial<Record<MethodLikeKeys<MatrixClient>, unknown>> => ({
getRooms: jest.fn().mockReturnValue(rooms),
getRoom: jest.fn((roomId) => rooms.find((r) => r.roomId === roomId) ?? null),
isRoomEncrypted: jest.fn(),
});

View file

@ -1,31 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { act, fireEvent, RenderResult } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
export const addTextToComposer = (container: HTMLElement, text: string) =>
act(() => {
// couldn't get input event on contenteditable to work
// paste works without illegal private method access
const pasteEvent: Partial<ClipboardEvent> = {
clipboardData: {
types: [],
files: [],
getData: (type: string) => (type === "text/plain" ? text : undefined),
} as unknown as DataTransfer,
};
fireEvent.paste(container.querySelector('[role="textbox"]')!, pasteEvent);
});
export const addTextToComposerRTL = async (renderResult: RenderResult, text: string): Promise<void> => {
await act(async () => {
await userEvent.click(renderResult.getByLabelText("Send a message…"));
await userEvent.keyboard(text);
});
};

View file

@ -1,46 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
type FilteredConsole = Pick<Console, "log" | "error" | "info" | "debug" | "warn">;
/**
* Allows to filter out specific messages in console.*.
* Call this from any describe block.
* Automagically restores the original function by implementing an afterAll hook.
*
* @param ignoreList Messages to be filtered
*/
export const filterConsole = (...ignoreList: string[]): void => {
const originalFunctions: FilteredConsole = {
log: console.log,
error: console.error,
info: console.info,
debug: console.debug,
warn: console.warn,
};
beforeAll(() => {
for (const [key, originalFunction] of Object.entries(originalFunctions)) {
window.console[key as keyof FilteredConsole] = (...data: any[]) => {
const message = data?.[0]?.message || data?.[0];
if (typeof message === "string" && ignoreList.some((i) => message.includes(i))) {
return;
}
originalFunction(...data);
};
}
});
afterAll(() => {
for (const [key, originalFunction] of Object.entries(originalFunctions)) {
window.console[key as keyof FilteredConsole] = originalFunction;
}
});
};

View file

@ -1,27 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
export const REPEATABLE_DATE = new Date(2022, 10, 17, 16, 58, 32, 517);
// allow setting default locale and set timezone
// defaults to en-GB / Europe/London
// so tests run the same everywhere
export const mockIntlDateTimeFormat = (defaultLocale = "en-GB", defaultTimezone = "Europe/London"): void => {
// unmock so we can use real DateTimeFormat in mockImplementation
if (jest.isMockFunction(global.Intl.DateTimeFormat)) {
unmockIntlDateTimeFormat();
}
const DateTimeFormat = Intl.DateTimeFormat;
jest.spyOn(global.Intl, "DateTimeFormat").mockImplementation(
(locale, options) => new DateTimeFormat(locale || defaultLocale, { ...options, timeZone: defaultTimezone }),
);
};
export const unmockIntlDateTimeFormat = (): void => {
jest.spyOn(global.Intl, "DateTimeFormat").mockRestore();
};

View file

@ -1,34 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { MsgType } from "matrix-js-sdk/src/matrix";
interface MessageContent {
msgtype: MsgType;
body: string;
format?: string;
formatted_body?: string;
}
/**
* Creates the `content` for an `m.room.message` event based on input.
* @param text The text to put in the event.
* @param html Optional HTML to put in the event.
* @returns A complete `content` object for an `m.room.message` event.
*/
export function createMessageEventContent(text: string, html?: string): MessageContent {
const content: MessageContent = {
msgtype: MsgType.Text,
body: text,
};
if (html) {
content.format = "org.matrix.custom.html";
content.formatted_body = html;
}
return content;
}

View file

@ -1,21 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
export * from "./beacon";
export * from "./client";
export * from "./location";
export * from "./platform";
export * from "./poll";
export * from "./room";
export * from "./test-utils";
export * from "./call";
export * from "./wrappers";
export * from "./utilities";
export * from "./date";
export * from "./relations";
export * from "./console";

View file

@ -1,42 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import React, { ReactElement } from "react";
// eslint-disable-next-line no-restricted-imports
import { render, RenderOptions } from "@testing-library/react";
import { TooltipProvider } from "@vector-im/compound-web";
const wrapWithTooltipProvider = (Wrapper: RenderOptions["wrapper"]) => {
return ({ children }: { children: React.ReactNode }) => {
if (Wrapper) {
return (
<Wrapper>
<TooltipProvider>{children}</TooltipProvider>
</Wrapper>
);
} else {
return <TooltipProvider>{children}</TooltipProvider>;
}
};
};
const customRender = (ui: ReactElement, options: RenderOptions = {}) => {
return render(ui, {
...options,
wrapper: wrapWithTooltipProvider(options?.wrapper) as RenderOptions["wrapper"],
}) as ReturnType<typeof render>;
};
// eslint-disable-next-line no-restricted-imports
export * from "@testing-library/react";
/**
* This custom render function wraps your component with a TooltipProvider.
* See https://testing-library.com/docs/react-testing-library/setup/#custom-render
*/
export { customRender as render };

View file

@ -1,47 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { LocationAssetType, M_LOCATION, MatrixEvent, EventType, ContentHelpers } from "matrix-js-sdk/src/matrix";
let id = 1;
export const makeLegacyLocationEvent = (geoUri: string): MatrixEvent => {
return new MatrixEvent({
event_id: `$${++id}`,
type: EventType.RoomMessage,
content: {
body: "Something about where I am",
msgtype: "m.location",
geo_uri: geoUri,
},
origin_server_ts: 0,
});
};
export const makeLocationEvent = (geoUri: string, assetType?: LocationAssetType): MatrixEvent => {
return new MatrixEvent({
event_id: `$${++id}`,
type: M_LOCATION.name,
content: ContentHelpers.makeLocationContent(
`Found at ${geoUri} at 2021-12-21T12:22+0000`,
geoUri,
252523,
"Human-readable label",
assetType,
),
origin_server_ts: 0,
});
};
// https://developer.mozilla.org/en-US/docs/Web/API/GeolocationPositionError
export const getMockGeolocationPositionError = (code: number, message: string): GeolocationPositionError => ({
code,
message,
PERMISSION_DENIED: 1,
POSITION_UNAVAILABLE: 2,
TIMEOUT: 3,
});

View file

@ -1,46 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { OidcClientConfig } from "matrix-js-sdk/src/matrix";
import { ValidatedIssuerMetadata } from "matrix-js-sdk/src/oidc/validate";
/**
* Makes a valid OidcClientConfig with minimum valid values
* @param issuer used as the base for all other urls
* @returns OidcClientConfig
*/
export const makeDelegatedAuthConfig = (issuer = "https://auth.org/"): OidcClientConfig => {
const metadata = mockOpenIdConfiguration(issuer);
return {
accountManagementEndpoint: issuer + "account",
registrationEndpoint: metadata.registration_endpoint,
authorizationEndpoint: metadata.authorization_endpoint,
tokenEndpoint: metadata.token_endpoint,
metadata,
};
};
/**
* Useful for mocking <issuer>/.well-known/openid-configuration
* @param issuer used as the base for all other urls
* @returns ValidatedIssuerMetadata
*/
export const mockOpenIdConfiguration = (issuer = "https://auth.org/"): ValidatedIssuerMetadata => ({
issuer,
revocation_endpoint: issuer + "revoke",
token_endpoint: issuer + "token",
authorization_endpoint: issuer + "auth",
registration_endpoint: issuer + "registration",
device_authorization_endpoint: issuer + "device",
jwks_uri: issuer + "jwks",
response_types_supported: ["code"],
grant_types_supported: ["authorization_code", "refresh_token"],
code_challenge_methods_supported: ["S256"],
account_management_uri: issuer + "account",
});

View file

@ -1,38 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { MethodLikeKeys, mocked, MockedObject } from "jest-mock";
import BasePlatform from "../../src/BasePlatform";
import PlatformPeg from "../../src/PlatformPeg";
// doesn't implement abstract
// @ts-ignore
class MockPlatform extends BasePlatform {
constructor(platformMocks: Partial<Record<keyof BasePlatform, unknown>>) {
super();
Object.assign(this, platformMocks);
}
}
/**
* Mock Platform Peg
* Creates a mock BasePlatform class
* spies on PlatformPeg.get and returns mock platform
* @returns MockPlatform instance
*/
export const mockPlatformPeg = (
platformMocks: Partial<Record<MethodLikeKeys<BasePlatform>, unknown>> = {},
): MockedObject<BasePlatform> => {
const mockPlatform = new MockPlatform(platformMocks);
jest.spyOn(PlatformPeg, "get").mockReturnValue(mockPlatform);
return mocked(mockPlatform);
};
export const unmockPlatformPeg = () => {
jest.spyOn(PlatformPeg, "get").mockRestore();
};

View file

@ -1,145 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { Mocked } from "jest-mock";
import {
MatrixClient,
MatrixEvent,
Room,
M_POLL_START,
PollAnswer,
M_POLL_KIND_DISCLOSED,
M_POLL_END,
M_POLL_RESPONSE,
M_TEXT,
} from "matrix-js-sdk/src/matrix";
import { uuid4 } from "@sentry/utils";
import { flushPromises } from "./utilities";
type Options = {
roomId: string;
ts: number;
id: string;
};
export const makePollStartEvent = (
question: string,
sender: string,
answers?: PollAnswer[],
{ roomId, ts, id }: Partial<Options> = {},
): MatrixEvent => {
if (!answers) {
answers = [
{ id: "socks", [M_TEXT.name]: "Socks" },
{ id: "shoes", [M_TEXT.name]: "Shoes" },
];
}
return new MatrixEvent({
event_id: id || "$mypoll",
room_id: roomId || "#myroom:example.com",
sender: sender,
type: M_POLL_START.name,
content: {
[M_POLL_START.name]: {
question: {
[M_TEXT.name]: question,
},
kind: M_POLL_KIND_DISCLOSED.name,
answers: answers,
},
[M_TEXT.name]: `${question}: answers`,
},
origin_server_ts: ts || 0,
});
};
export const makePollEndEvent = (
pollStartEventId: string,
roomId: string,
sender: string,
ts = 0,
id?: string,
): MatrixEvent => {
return new MatrixEvent({
event_id: id || uuid4(),
room_id: roomId,
origin_server_ts: ts,
type: M_POLL_END.name,
sender: sender,
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: pollStartEventId,
},
[M_POLL_END.name]: {},
[M_TEXT.name]: "The poll has ended. Something.",
},
});
};
export const makePollResponseEvent = (
pollId: string,
answerIds: string[],
sender: string,
roomId: string,
ts = 0,
): MatrixEvent =>
new MatrixEvent({
event_id: uuid4(),
room_id: roomId,
origin_server_ts: ts,
type: M_POLL_RESPONSE.name,
sender,
content: {
"m.relates_to": {
rel_type: "m.reference",
event_id: pollId,
},
[M_POLL_RESPONSE.name]: {
answers: answerIds,
},
},
});
/**
* Creates a room with attached poll events
* Returns room from mockClient
* mocks relations api
* @param mxEvent - poll start event
* @param relationEvents - returned by relations api
* @param endEvents - returned by relations api
* @param mockClient - client in use
* @returns
*/
export const setupRoomWithPollEvents = async (
pollStartEvents: MatrixEvent[],
relationEvents: Array<MatrixEvent>,
endEvents: Array<MatrixEvent> = [],
mockClient: Mocked<MatrixClient>,
existingRoom?: Room,
): Promise<Room> => {
const room = existingRoom || new Room(pollStartEvents[0].getRoomId()!, mockClient, mockClient.getSafeUserId());
room.processPollEvents([...pollStartEvents, ...relationEvents, ...endEvents]);
// set redaction allowed for current user only
// poll end events are validated against this
jest.spyOn(room.currentState, "maySendRedactionForEvent").mockImplementation((_evt: MatrixEvent, id: string) => {
return id === mockClient.getSafeUserId();
});
// wait for events to process on room
await flushPromises();
mockClient.getRoom.mockReturnValue(room);
mockClient.relations.mockImplementation(async (_roomId: string, eventId: string) => {
return {
events: [...relationEvents, ...endEvents].filter((event) => event.getRelation()?.event_id === eventId),
};
});
return room;
};

View file

@ -1,359 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { IAnnotatedPushRule, IPushRule, IPushRules, PushRuleKind, RuleId } from "matrix-js-sdk/src/matrix";
/**
* Default set of push rules for a new account
* Use to mock push rule fetching, or use `getDefaultRuleWithKind`
* to use default examples of specific push rules
*/
export const DEFAULT_PUSH_RULES: IPushRules = Object.freeze({
global: {
underride: [
{
conditions: [{ kind: "event_match", key: "type", pattern: "m.call.invite" }],
actions: ["notify", { set_tweak: "sound", value: "ring" }, { set_tweak: "highlight", value: false }],
rule_id: ".m.rule.call",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "m.room.message" },
{ kind: "room_member_count", is: "2" },
],
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
rule_id: ".m.rule.room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "m.room.encrypted" },
{ kind: "room_member_count", is: "2" },
],
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
rule_id: ".m.rule.encrypted_room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "org.matrix.msc1767.encrypted" },
{ kind: "room_member_count", is: "2" },
{
kind: "org.matrix.msc3931.room_version_supports",
feature: "org.matrix.msc3932.extensible_events",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
rule_id: ".org.matrix.msc3933.rule.extensible.encrypted_room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "org.matrix.msc1767.message" },
{ kind: "room_member_count", is: "2" },
{
kind: "org.matrix.msc3931.room_version_supports",
feature: "org.matrix.msc3932.extensible_events",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
rule_id: ".org.matrix.msc3933.rule.extensible.message.room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "org.matrix.msc1767.file" },
{ kind: "room_member_count", is: "2" },
{
kind: "org.matrix.msc3931.room_version_supports",
feature: "org.matrix.msc3932.extensible_events",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
rule_id: ".org.matrix.msc3933.rule.extensible.file.room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "org.matrix.msc1767.image" },
{ kind: "room_member_count", is: "2" },
{
kind: "org.matrix.msc3931.room_version_supports",
feature: "org.matrix.msc3932.extensible_events",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
rule_id: ".org.matrix.msc3933.rule.extensible.image.room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "org.matrix.msc1767.video" },
{ kind: "room_member_count", is: "2" },
{
kind: "org.matrix.msc3931.room_version_supports",
feature: "org.matrix.msc3932.extensible_events",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
rule_id: ".org.matrix.msc3933.rule.extensible.video.room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "org.matrix.msc1767.audio" },
{ kind: "room_member_count", is: "2" },
{
kind: "org.matrix.msc3931.room_version_supports",
feature: "org.matrix.msc3932.extensible_events",
},
],
actions: ["notify", { set_tweak: "sound", value: "default" }, { set_tweak: "highlight", value: false }],
rule_id: ".org.matrix.msc3933.rule.extensible.audio.room_one_to_one",
default: true,
enabled: true,
},
{
conditions: [{ kind: "event_match", key: "type", pattern: "m.room.message" }],
actions: ["notify", { set_tweak: "highlight", value: false }],
rule_id: ".m.rule.message",
default: true,
enabled: true,
},
{
conditions: [{ kind: "event_match", key: "type", pattern: "m.room.encrypted" }],
actions: ["notify", { set_tweak: "highlight", value: false }],
rule_id: ".m.rule.encrypted",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "im.vector.modular.widgets" },
{ kind: "event_match", key: "content.type", pattern: "jitsi" },
{ kind: "event_match", key: "state_key", pattern: "*" },
],
actions: ["notify", { set_tweak: "highlight", value: false }],
rule_id: ".im.vector.jitsi",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "room_member_count", is: "2" },
{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.start" },
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
rule_id: ".org.matrix.msc3930.rule.poll_start_one_to_one",
default: true,
enabled: true,
},
{
conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.start" }],
actions: ["notify"],
rule_id: ".org.matrix.msc3930.rule.poll_start",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "room_member_count", is: "2" },
{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.end" },
],
actions: ["notify", { set_tweak: "sound", value: "default" }],
rule_id: ".org.matrix.msc3930.rule.poll_end_one_to_one",
default: true,
enabled: true,
},
{
conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.end" }],
actions: ["notify"],
rule_id: ".org.matrix.msc3930.rule.poll_end",
default: true,
enabled: true,
},
],
sender: [],
room: [],
content: [
{
actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }],
rule_id: ".m.rule.contains_user_name",
default: true,
pattern: "alice",
enabled: true,
},
],
override: [
{ conditions: [], actions: ["dont_notify"], rule_id: ".m.rule.master", default: true, enabled: false },
{
conditions: [{ kind: "event_match", key: "content.msgtype", pattern: "m.notice" }],
actions: ["dont_notify"],
rule_id: ".m.rule.suppress_notices",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "m.room.member" },
{ kind: "event_match", key: "content.membership", pattern: "invite" },
{ kind: "event_match", key: "state_key", pattern: "@alice:example.org" },
],
actions: ["notify", { set_tweak: "highlight", value: false }, { set_tweak: "sound", value: "default" }],
rule_id: ".m.rule.invite_for_me",
default: true,
enabled: true,
},
{
conditions: [{ kind: "event_match", key: "type", pattern: "m.room.member" }],
actions: ["dont_notify"],
rule_id: ".m.rule.member_event",
default: true,
enabled: true,
},
{
conditions: [
{
kind: "event_property_contains",
key: "content.m\\.mentions.user_ids",
value_type: "user_id",
},
],
actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }],
rule_id: ".m.rule.is_user_mention",
default: true,
enabled: true,
},
{
conditions: [{ kind: "contains_display_name" }],
actions: ["notify", { set_tweak: "highlight" }, { set_tweak: "sound", value: "default" }],
rule_id: ".m.rule.contains_display_name",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_property_is", key: "content.m\\.mentions.room", value: true },
{ kind: "sender_notification_permission", key: "room" },
],
actions: ["notify", { set_tweak: "highlight" }],
rule_id: ".m.rule.is_room_mention",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "sender_notification_permission", key: "room" },
{ kind: "event_match", key: "content.body", pattern: "@room" },
],
actions: ["notify", { set_tweak: "highlight" }],
rule_id: ".m.rule.roomnotif",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "m.room.tombstone" },
{ kind: "event_match", key: "state_key", pattern: "" },
],
actions: ["notify", { set_tweak: "highlight" }],
rule_id: ".m.rule.tombstone",
default: true,
enabled: true,
},
{
conditions: [{ kind: "event_match", key: "type", pattern: "m.reaction" }],
actions: [],
rule_id: ".m.rule.reaction",
default: true,
enabled: true,
},
{
conditions: [
{ kind: "event_match", key: "type", pattern: "m.room.server_acl" },
{ kind: "event_match", key: "state_key", pattern: "" },
],
actions: [],
rule_id: ".m.rule.room.server_acl",
default: true,
enabled: true,
},
{
conditions: [{ kind: "event_match", key: "type", pattern: "org.matrix.msc3381.poll.response" }],
actions: [],
rule_id: ".org.matrix.msc3930.rule.poll_response",
default: true,
enabled: true,
},
],
},
} as IPushRules);
/**
* Get rule by id from default rules
* @param ruleId
* @returns {IPushRule} matching push rule
* @returns {PushRuleKind}
* @throws when no rule is found with ruleId
*/
export const getDefaultRuleWithKind = (ruleId: RuleId | string): { rule: IPushRule; kind: PushRuleKind } => {
for (const kind of Object.keys(DEFAULT_PUSH_RULES.global)) {
const rule = DEFAULT_PUSH_RULES.global[kind as PushRuleKind]!.find((r: IPushRule) => r.rule_id === ruleId);
if (rule) {
return { rule, kind: kind as PushRuleKind };
}
}
throw new Error(`Could not find default rule for id ${ruleId}`);
};
/**
* Get rule by id from default rules as an IAnnotatedPushRule
* @param ruleId
* @returns
*/
export const getDefaultAnnotatedRule = (ruleId: RuleId | string): IAnnotatedPushRule => {
const { rule, kind } = getDefaultRuleWithKind(ruleId);
return {
...rule,
kind,
};
};
/**
* Make a push rule with default values
* @param ruleId
* @param ruleOverrides
* @returns IPushRule
*/
export const makePushRule = (ruleId: RuleId | string, ruleOverrides: Partial<IPushRule> = {}): IPushRule => ({
actions: [],
enabled: true,
default: false,
...ruleOverrides,
rule_id: ruleId,
});
export const makeAnnotatedPushRule = (
kind: PushRuleKind,
ruleId: RuleId | string,
ruleOverrides: Partial<IPushRule> = {},
): IAnnotatedPushRule => ({
...makePushRule(ruleId, ruleOverrides),
kind,
});

View file

@ -1,25 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { Relations } from "matrix-js-sdk/src/matrix";
import { RelationsContainer } from "matrix-js-sdk/src/models/relations-container";
import { PublicInterface } from "../@types/common";
export const mkRelations = (): Relations => {
return {} as PublicInterface<Relations> as Relations;
};
export const mkRelationsContainer = (): RelationsContainer => {
return {
aggregateChildEvent: jest.fn(),
aggregateParentEvent: jest.fn(),
getAllChildEventsForEvent: jest.fn(),
getChildEventsForEvent: jest.fn(),
} as PublicInterface<RelationsContainer> as RelationsContainer;
};

View file

@ -1,100 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { MockedObject } from "jest-mock";
import { EventTimeline, EventType, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { IRoomState, MainSplitContentType } from "../../src/components/structures/RoomView";
import { TimelineRenderingType } from "../../src/contexts/RoomContext";
import { Layout } from "../../src/settings/enums/Layout";
import { mkEvent } from "./test-utils";
export const makeMembershipEvent = (roomId: string, userId: string, membership = KnownMembership.Join) =>
mkEvent({
event: true,
type: EventType.RoomMember,
room: roomId,
user: userId,
skey: userId,
content: { membership },
ts: Date.now(),
});
/**
* Creates a room
* sets state events on the room
* Sets client getRoom to return room
* returns room
*/
export const makeRoomWithStateEvents = (
stateEvents: MatrixEvent[] = [],
{ roomId, mockClient }: { roomId: string; mockClient: MockedObject<MatrixClient> },
): Room => {
const room1 = new Room(roomId, mockClient, "@user:server.org");
room1.currentState.setStateEvents(stateEvents);
mockClient.getRoom.mockReturnValue(room1);
return room1;
};
export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoomState {
return {
room,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
canPeek: false,
showApps: false,
isPeeking: false,
showRightPanel: true,
joining: false,
atEndOfLiveTimeline: true,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canSendMessages: false,
layout: Layout.Group,
lowBandwidth: false,
alwaysShowTimestamps: false,
userTimezone: undefined,
showTwelveHourTimestamps: false,
readMarkerInViewThresholdMs: 3000,
readMarkerOutOfViewThresholdMs: 30000,
showHiddenEvents: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false,
timelineRenderingType: TimelineRenderingType.Room,
mainSplitContentType: MainSplitContentType.Timeline,
liveTimeline: undefined,
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
msc3946ProcessDynamicPredecessor: false,
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
...override,
};
}
export const setupRoomWithEventsTimeline = (room: Room, events: MatrixEvent[] = []): void => {
const timelineSet = room.getUnfilteredTimelineSet();
const getTimelineForEventSpy = jest.spyOn(timelineSet, "getTimelineForEvent");
const eventTimeline = {
getEvents: jest.fn().mockReturnValue(events),
} as unknown as EventTimeline;
getTimelineForEventSpy.mockReturnValue(eventTimeline);
};

View file

@ -1,838 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022, 2023 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import EventEmitter from "events";
import { mocked, MockedObject } from "jest-mock";
import {
MatrixEvent,
Room,
User,
IContent,
IEvent,
RoomMember,
MatrixClient,
EventTimeline,
RoomState,
EventType,
IEventRelation,
IUnsigned,
IPusher,
RoomType,
KNOWN_SAFE_ROOM_VERSION,
ConditionKind,
IPushRules,
RelationType,
JoinRule,
OidcClientConfig,
} from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { normalize } from "matrix-js-sdk/src/utils";
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
import { MediaHandler } from "matrix-js-sdk/src/webrtc/mediaHandler";
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { MapperOpts } from "matrix-js-sdk/src/event-mapper";
import { MatrixRTCSessionManager, MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc";
import type { GroupCall } from "matrix-js-sdk/src/matrix";
import type { Membership } from "matrix-js-sdk/src/types";
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
import { ValidatedServerConfig } from "../../src/utils/ValidatedServerConfig";
import { EnhancedMap } from "../../src/utils/maps";
import { AsyncStoreWithClient } from "../../src/stores/AsyncStoreWithClient";
import MatrixClientBackedSettingsHandler from "../../src/settings/handlers/MatrixClientBackedSettingsHandler";
/**
* Stub out the MatrixClient, and configure the MatrixClientPeg object to
* return it when get() is called.
*
* TODO: once the components are updated to get their MatrixClients from
* the react context, we can get rid of this and just inject a test client
* via the context instead.
*
* See also {@link getMockClientWithEventEmitter} which does something similar but different.
*/
export function stubClient(): MatrixClient {
const client = createTestClient();
// stub out the methods in MatrixClientPeg
//
// 'sandbox.restore()' doesn't work correctly on inherited methods,
// so we do this for each method
jest.spyOn(peg, "get");
jest.spyOn(peg, "safeGet");
jest.spyOn(peg, "unset");
jest.spyOn(peg, "replaceUsingCreds");
// MatrixClientPeg.safeGet() is called a /lot/, so implement it with our own
// fast stub function rather than a sinon stub
peg.get = () => client;
peg.safeGet = () => client;
MatrixClientBackedSettingsHandler.matrixClient = client;
return client;
}
/**
* Create a stubbed-out MatrixClient
*
* @returns {object} MatrixClient stub
*/
export function createTestClient(): MatrixClient {
const eventEmitter = new EventEmitter();
let txnId = 1;
const client = {
getHomeserverUrl: jest.fn(),
getIdentityServerUrl: jest.fn(),
getDomain: jest.fn().mockReturnValue("matrix.org"),
getUserId: jest.fn().mockReturnValue("@userId:matrix.org"),
getSafeUserId: jest.fn().mockReturnValue("@userId:matrix.org"),
getUserIdLocalpart: jest.fn().mockResolvedValue("userId"),
getUser: jest.fn().mockReturnValue({ on: jest.fn(), off: jest.fn() }),
getDevice: jest.fn(),
getDeviceId: jest.fn().mockReturnValue("ABCDEFGHI"),
getStoredCrossSigningForUser: jest.fn(),
getStoredDevice: jest.fn(),
requestVerification: jest.fn(),
deviceId: "ABCDEFGHI",
getDevices: jest.fn().mockResolvedValue({ devices: [{ device_id: "ABCDEFGHI" }] }),
getSessionId: jest.fn().mockReturnValue("iaszphgvfku"),
credentials: { userId: "@userId:matrix.org" },
bootstrapCrossSigning: jest.fn(),
hasSecretStorageKey: jest.fn(),
getKeyBackupVersion: jest.fn(),
secretStorage: {
get: jest.fn(),
isStored: jest.fn().mockReturnValue(false),
checkKey: jest.fn().mockResolvedValue(false),
},
store: {
getPendingEvents: jest.fn().mockResolvedValue([]),
setPendingEvents: jest.fn().mockResolvedValue(undefined),
storeRoom: jest.fn(),
removeRoom: jest.fn(),
},
crypto: {
deviceList: {
downloadKeys: jest.fn(),
},
},
getCrypto: jest.fn().mockReturnValue({
getOwnDeviceKeys: jest.fn(),
getUserDeviceInfo: jest.fn(),
getUserVerificationStatus: jest.fn(),
getDeviceVerificationStatus: jest.fn(),
resetKeyBackup: jest.fn(),
isEncryptionEnabledInRoom: jest.fn(),
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
setDeviceIsolationMode: jest.fn(),
prepareToEncrypt: jest.fn(),
}),
getPushActionsForEvent: jest.fn(),
getRoom: jest.fn().mockImplementation((roomId) => mkStubRoom(roomId, "My room", client)),
getRooms: jest.fn().mockReturnValue([]),
getVisibleRooms: jest.fn().mockReturnValue([]),
loginFlows: jest.fn(),
on: eventEmitter.on.bind(eventEmitter),
off: eventEmitter.off.bind(eventEmitter),
removeListener: eventEmitter.removeListener.bind(eventEmitter),
emit: eventEmitter.emit.bind(eventEmitter),
isRoomEncrypted: jest.fn().mockReturnValue(false),
peekInRoom: jest.fn().mockResolvedValue(mkStubRoom(undefined, undefined, undefined)),
stopPeeking: jest.fn(),
paginateEventTimeline: jest.fn().mockResolvedValue(undefined),
sendReadReceipt: jest.fn().mockResolvedValue(undefined),
getRoomIdForAlias: jest.fn().mockResolvedValue(undefined),
getRoomDirectoryVisibility: jest.fn().mockResolvedValue(undefined),
getProfileInfo: jest.fn().mockResolvedValue({}),
getThirdpartyProtocols: jest.fn().mockResolvedValue({}),
getClientWellKnown: jest.fn().mockReturnValue(null),
waitForClientWellKnown: jest.fn().mockResolvedValue({}),
supportsVoip: jest.fn().mockReturnValue(true),
getTurnServers: jest.fn().mockReturnValue([]),
getTurnServersExpiry: jest.fn().mockReturnValue(2 ^ 32),
getThirdpartyUser: jest.fn().mockResolvedValue([]),
getAccountData: jest.fn().mockImplementation((type) => {
return mkEvent({
user: "@user:example.com",
room: undefined,
type,
event: true,
content: {},
});
}),
mxcUrlToHttp: jest.fn().mockImplementation((mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`),
scheduleAllGroupSessionsForBackup: jest.fn().mockResolvedValue(undefined),
setAccountData: jest.fn(),
setRoomAccountData: jest.fn(),
setRoomTopic: jest.fn(),
setRoomReadMarkers: jest.fn().mockResolvedValue({}),
sendTyping: jest.fn().mockResolvedValue({}),
sendMessage: jest.fn().mockResolvedValue({}),
sendStateEvent: jest.fn().mockResolvedValue(undefined),
getSyncState: jest.fn().mockReturnValue("SYNCING"),
generateClientSecret: () => "t35tcl1Ent5ECr3T",
isGuest: jest.fn().mockReturnValue(false),
getRoomHierarchy: jest.fn().mockReturnValue({
rooms: [],
}),
createRoom: jest.fn().mockResolvedValue({ room_id: "!1:example.org" }),
setPowerLevel: jest.fn().mockResolvedValue(undefined),
pushRules: {},
decryptEventIfNeeded: () => Promise.resolve(),
isUserIgnored: jest.fn().mockReturnValue(false),
getCapabilities: jest.fn().mockResolvedValue({}),
supportsThreads: jest.fn().mockReturnValue(false),
supportsIntentionalMentions: jest.fn().mockReturnValue(false),
getRoomUpgradeHistory: jest.fn().mockReturnValue([]),
getOpenIdToken: jest.fn().mockResolvedValue(undefined),
registerWithIdentityServer: jest.fn().mockResolvedValue({}),
getIdentityAccount: jest.fn().mockResolvedValue({}),
getTerms: jest.fn().mockResolvedValue({ policies: [] }),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(undefined),
isVersionSupported: jest.fn().mockResolvedValue(undefined),
getPushRules: jest.fn().mockResolvedValue(undefined),
getPushers: jest.fn().mockResolvedValue({ pushers: [] }),
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
bulkLookupThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
setAvatarUrl: jest.fn().mockResolvedValue(undefined),
setDisplayName: jest.fn().mockResolvedValue(undefined),
setPusher: jest.fn().mockResolvedValue(undefined),
setPushRuleEnabled: jest.fn().mockResolvedValue(undefined),
setPushRuleActions: jest.fn().mockResolvedValue(undefined),
relations: jest.fn().mockResolvedValue({
events: [],
}),
isCryptoEnabled: jest.fn().mockReturnValue(false),
hasLazyLoadMembersEnabled: jest.fn().mockReturnValue(false),
isInitialSyncComplete: jest.fn().mockReturnValue(true),
downloadKeys: jest.fn(),
fetchRoomEvent: jest.fn().mockRejectedValue({}),
makeTxnId: jest.fn().mockImplementation(() => `t${txnId++}`),
sendToDevice: jest.fn().mockResolvedValue(undefined),
queueToDevice: jest.fn().mockResolvedValue(undefined),
encryptAndSendToDevices: jest.fn().mockResolvedValue(undefined),
cancelPendingEvent: jest.fn(),
getMediaHandler: jest.fn().mockReturnValue({
setVideoInput: jest.fn(),
setAudioInput: jest.fn(),
setAudioSettings: jest.fn(),
stopAllStreams: jest.fn(),
} as unknown as MediaHandler),
uploadContent: jest.fn(),
getEventMapper: (_options?: MapperOpts) => (event: Partial<IEvent>) => new MatrixEvent(event),
leaveRoomChain: jest.fn((roomId) => ({ [roomId]: null })),
requestPasswordEmailToken: jest.fn().mockRejectedValue({}),
setPassword: jest.fn().mockRejectedValue({}),
groupCallEventHandler: { groupCalls: new Map<string, GroupCall>() },
redactEvent: jest.fn(),
createMessagesRequest: jest.fn().mockResolvedValue({
chunk: [],
}),
sendEvent: jest.fn().mockImplementation((roomId, type, content) => {
return new MatrixEvent({
type,
sender: "@me:localhost",
content,
event_id: "$9999999999999999999999999999999999999999999",
room_id: roomId,
});
}),
_unstable_sendDelayedEvent: jest.fn(),
_unstable_sendDelayedStateEvent: jest.fn(),
_unstable_updateDelayedEvent: jest.fn(),
searchUserDirectory: jest.fn().mockResolvedValue({ limited: false, results: [] }),
setDeviceVerified: jest.fn(),
joinRoom: jest.fn(),
getSyncStateData: jest.fn(),
getDehydratedDevice: jest.fn(),
exportRoomKeys: jest.fn(),
knockRoom: jest.fn(),
leave: jest.fn(),
getVersions: jest.fn().mockResolvedValue({ versions: ["v1.1"] }),
requestAdd3pidEmailToken: jest.fn(),
requestAdd3pidMsisdnToken: jest.fn(),
submitMsisdnTokenOtherUrl: jest.fn(),
deleteThreePid: jest.fn().mockResolvedValue({}),
bindThreePid: jest.fn().mockResolvedValue({}),
unbindThreePid: jest.fn().mockResolvedValue({}),
requestEmailToken: jest.fn(),
addThreePidOnly: jest.fn(),
requestMsisdnToken: jest.fn(),
submitMsisdnToken: jest.fn(),
getMediaConfig: jest.fn(),
baseUrl: "https://matrix-client.matrix.org",
matrixRTC: createStubMatrixRTC(),
isFallbackICEServerAllowed: jest.fn().mockReturnValue(false),
getAuthIssuer: jest.fn(),
getOrCreateFilter: jest.fn(),
} as unknown as MatrixClient;
client.reEmitter = new ReEmitter(client);
client.canSupport = new Map();
Object.keys(Feature).forEach((feature) => {
client.canSupport.set(feature as Feature, ServerSupport.Stable);
});
Object.defineProperty(client, "pollingTurnServers", {
configurable: true,
get: () => true,
});
return client;
}
export function createStubMatrixRTC(): MatrixRTCSessionManager {
const eventEmitterMatrixRTCSessionManager = new EventEmitter();
const mockGetRoomSession = jest.fn();
mockGetRoomSession.mockImplementation((roomId) => {
const session = new EventEmitter() as MatrixRTCSession;
session.memberships = [];
session.getOldestMembership = () => undefined;
return session;
});
return {
start: jest.fn(),
stop: jest.fn(),
getActiveRoomSession: jest.fn(),
getRoomSession: mockGetRoomSession,
on: eventEmitterMatrixRTCSessionManager.on.bind(eventEmitterMatrixRTCSessionManager),
off: eventEmitterMatrixRTCSessionManager.off.bind(eventEmitterMatrixRTCSessionManager),
removeListener: eventEmitterMatrixRTCSessionManager.removeListener.bind(eventEmitterMatrixRTCSessionManager),
emit: eventEmitterMatrixRTCSessionManager.emit.bind(eventEmitterMatrixRTCSessionManager),
} as unknown as MatrixRTCSessionManager;
}
type MakeEventPassThruProps = {
user: User["userId"];
relatesTo?: IEventRelation;
event?: boolean;
ts?: number;
skey?: string;
};
type MakeEventProps = MakeEventPassThruProps & {
/** If provided will be used as event Id. Else an Id is generated. */
id?: string;
type: string;
redacts?: string;
content: IContent;
room?: Room["roomId"]; // to-device messages are roomless
// eslint-disable-next-line camelcase
prev_content?: IContent;
unsigned?: IUnsigned;
};
export const mkRoomCreateEvent = (userId: string, roomId: string, content?: IContent): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomCreate,
content: {
creator: userId,
room_version: KNOWN_SAFE_ROOM_VERSION,
...content,
},
skey: "",
user: userId,
room: roomId,
});
};
/**
* Create an Event.
* @param {Object} opts Values for the event.
* @param {string} opts.type The event.type
* @param {string} opts.room The event.room_id
* @param {string} opts.user The event.user_id
* @param {string=} opts.skey Optional. The state key (auto inserts empty string)
* @param {number=} opts.ts Optional. Timestamp for the event
* @param {Object} opts.content The event.content
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {unsigned=} opts.unsigned
* @return {Object} a JSON object representing this event.
*/
export function mkEvent(opts: MakeEventProps): MatrixEvent {
if (!opts.type || !opts.content) {
throw new Error("Missing .type or .content =>" + JSON.stringify(opts));
}
const event: Partial<IEvent> = {
type: opts.type,
room_id: opts.room,
sender: opts.user,
content: opts.content,
event_id: opts.id ?? "$" + Math.random() + "-" + Math.random(),
origin_server_ts: opts.ts ?? 0,
unsigned: {
...opts.unsigned,
prev_content: opts.prev_content,
},
redacts: opts.redacts,
};
if (opts.skey !== undefined) {
event.state_key = opts.skey;
} else if (
[
"m.room.name",
"m.room.topic",
"m.room.create",
"m.room.join_rules",
"m.room.power_levels",
"m.room.topic",
"m.room.history_visibility",
"m.room.encryption",
"m.room.member",
"com.example.state",
"m.room.guest_access",
"m.room.tombstone",
].indexOf(opts.type) !== -1
) {
event.state_key = "";
}
const mxEvent = opts.event ? new MatrixEvent(event) : (event as unknown as MatrixEvent);
if (!mxEvent.sender && opts.user && opts.room) {
mxEvent.sender = {
userId: opts.user,
membership: KnownMembership.Join,
name: opts.user,
rawDisplayName: opts.user,
roomId: opts.room,
getAvatarUrl: () => {},
getMxcAvatarUrl: () => {},
} as unknown as RoomMember;
}
return mxEvent;
}
/**
* Create an m.room.member event.
* @param {Object} opts Values for the membership.
* @param {string} opts.room The room ID for the event.
* @param {string} opts.mship The content.membership for the event.
* @param {string} opts.prevMship The prev_content.membership for the event.
* @param {number=} opts.ts Optional. Timestamp for the event
* @param {string} opts.user The user ID for the event.
* @param {RoomMember} opts.target The target of the event.
* @param {string=} opts.skey The other user ID for the event if applicable
* e.g. for invites/bans.
* @param {string} opts.name The content.displayname for the event.
* @param {string=} opts.url The content.avatar_url for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @return {Object|MatrixEvent} The event
*/
export function mkMembership(
opts: MakeEventPassThruProps & {
room: Room["roomId"];
mship: Membership;
prevMship?: Membership;
name?: string;
url?: string;
skey?: string;
target?: RoomMember;
},
): MatrixEvent {
const event: MakeEventProps = {
...opts,
type: "m.room.member",
content: {
membership: opts.mship,
},
};
if (!opts.skey) {
event.skey = opts.user;
}
if (!opts.mship) {
throw new Error("Missing .mship => " + JSON.stringify(opts));
}
if (opts.prevMship) {
event.prev_content = { membership: opts.prevMship };
}
if (opts.name) {
event.content.displayname = opts.name;
}
if (opts.url) {
event.content.avatar_url = opts.url;
}
const e = mkEvent(event);
if (opts.target) {
e.target = opts.target;
}
return e;
}
export function mkRoomMember(
roomId: string,
userId: string,
membership = KnownMembership.Join,
isKicked = false,
prevMemberContent: Partial<IContent> = {},
): RoomMember {
return {
userId,
membership,
name: userId,
rawDisplayName: userId,
roomId,
events: {
member: {
getSender: () => undefined,
getPrevContent: () => prevMemberContent,
},
},
isKicked: () => isKicked,
getAvatarUrl: () => {},
getMxcAvatarUrl: () => {},
getDMInviter: () => {},
off: () => {},
} as unknown as RoomMember;
}
export type MessageEventProps = MakeEventPassThruProps & {
room: Room["roomId"];
relatesTo?: IEventRelation;
msg?: string;
};
/**
* Creates a "🙃" reaction for the given event.
* Uses the same room and user as for the event.
*
* @returns The reaction event
*/
export const mkReaction = (event: MatrixEvent, opts: Partial<MakeEventProps> = {}): MatrixEvent => {
return mkEvent({
event: true,
room: event.getRoomId(),
type: EventType.Reaction,
user: event.getSender()!,
content: {
"m.relates_to": {
rel_type: RelationType.Annotation,
event_id: event.getId(),
key: "🙃",
},
},
...opts,
});
};
/**
* Create an m.room.message event.
* @param {Object} opts Values for the message
* @param {string} opts.room The room ID for the event.
* @param {string} opts.user The user ID for the event.
* @param {number} opts.ts The timestamp for the event.
* @param {boolean} opts.event True to make a MatrixEvent.
* @param {string=} opts.msg Optional. The content.body for the event.
* @param {string=} opts.format Optional. The content.format for the event.
* @param {string=} opts.formattedMsg Optional. The content.formatted_body for the event.
* @return {Object|MatrixEvent} The event
*/
export function mkMessage({
msg,
format,
formattedMsg,
relatesTo,
...opts
}: MakeEventPassThruProps &
Pick<MakeEventProps, "id"> & {
room: Room["roomId"];
msg?: string;
format?: string;
formattedMsg?: string;
}): MatrixEvent {
if (!opts.room || !opts.user) {
throw new Error("Missing .room or .user from options");
}
const message = msg ?? "Random->" + Math.random();
const event: MakeEventProps = {
ts: 0,
...opts,
type: "m.room.message",
content: {
msgtype: "m.text",
body: message,
...(format && formattedMsg ? { format, formatted_body: formattedMsg } : {}),
["m.relates_to"]: relatesTo,
},
};
return mkEvent(event);
}
export function mkStubRoom(
roomId: string | null | undefined = null,
name: string | undefined,
client: MatrixClient | undefined,
): Room {
const stubTimeline = {
getEvents: (): MatrixEvent[] => [],
getState: (): RoomState | undefined => undefined,
} as unknown as EventTimeline;
return {
canInvite: jest.fn(),
client,
findThreadForEvent: jest.fn(),
createThreadsTimelineSets: jest.fn().mockReturnValue(new Promise(() => {})),
currentState: {
getStateEvents: jest.fn((_type, key) => (key === undefined ? [] : null)),
getMember: jest.fn(),
mayClientSendStateEvent: jest.fn().mockReturnValue(true),
maySendStateEvent: jest.fn().mockReturnValue(true),
maySendRedactionForEvent: jest.fn().mockReturnValue(true),
maySendEvent: jest.fn().mockReturnValue(true),
members: {},
getJoinRule: jest.fn().mockReturnValue(JoinRule.Invite),
on: jest.fn(),
off: jest.fn(),
} as unknown as RoomState,
eventShouldLiveIn: jest.fn().mockReturnValue({ shouldLiveInRoom: true, shouldLiveInThread: false }),
fetchRoomThreads: jest.fn().mockReturnValue(Promise.resolve()),
findEventById: jest.fn().mockReturnValue(undefined),
findPredecessor: jest.fn().mockReturnValue({ roomId: "", eventId: null }),
getAccountData: (_: EventType | string) => undefined as MatrixEvent | undefined,
getAltAliases: jest.fn().mockReturnValue([]),
getAvatarUrl: () => "mxc://avatar.url/room.png",
getCanonicalAlias: jest.fn(),
getDMInviter: jest.fn(),
getEventReadUpTo: jest.fn(() => null),
getInvitedAndJoinedMemberCount: jest.fn().mockReturnValue(1),
getJoinRule: jest.fn().mockReturnValue("invite"),
getJoinedMemberCount: jest.fn().mockReturnValue(1),
getJoinedMembers: jest.fn().mockReturnValue([]),
getLiveTimeline: jest.fn().mockReturnValue(stubTimeline),
getLastLiveEvent: jest.fn().mockReturnValue(undefined),
getMember: jest.fn().mockReturnValue({
userId: "@member:domain.bla",
name: "Member",
rawDisplayName: "Member",
roomId: roomId,
getAvatarUrl: () => "mxc://avatar.url/image.png",
getMxcAvatarUrl: () => "mxc://avatar.url/image.png",
events: {},
isKicked: () => false,
}),
getMembers: jest.fn().mockReturnValue([]),
getMembersWithMembership: jest.fn().mockReturnValue([]),
getMxcAvatarUrl: () => "mxc://avatar.url/room.png",
getMyMembership: jest.fn().mockReturnValue(KnownMembership.Join),
getPendingEvents: () => [] as MatrixEvent[],
getReceiptsForEvent: jest.fn().mockReturnValue([]),
getRecommendedVersion: jest.fn().mockReturnValue(Promise.resolve("")),
getThreads: jest.fn().mockReturnValue([]),
getType: jest.fn().mockReturnValue(undefined),
getUnfilteredTimelineSet: jest.fn(),
getUnreadNotificationCount: jest.fn(() => 0),
getRoomUnreadNotificationCount: jest.fn().mockReturnValue(0),
getVersion: jest.fn().mockReturnValue("1"),
hasMembershipState: () => false,
isElementVideoRoom: jest.fn().mockReturnValue(false),
isSpaceRoom: jest.fn().mockReturnValue(false),
isCallRoom: jest.fn().mockReturnValue(false),
hasEncryptionStateEvent: jest.fn().mockReturnValue(false),
loadMembersIfNeeded: jest.fn(),
maySendMessage: jest.fn().mockReturnValue(true),
myUserId: client?.getUserId(),
name,
normalizedName: normalize(name || ""),
off: jest.fn(),
on: jest.fn(),
removeListener: jest.fn(),
roomId,
setBlacklistUnverifiedDevices: jest.fn(),
setUnreadNotificationCount: jest.fn(),
tags: {},
timeline: [],
} as unknown as Room;
}
export function mkServerConfig(
hsUrl: string,
isUrl: string,
delegatedAuthentication?: OidcClientConfig,
): ValidatedServerConfig {
return {
hsUrl,
hsName: "TEST_ENVIRONMENT",
hsNameIsDifferent: false, // yes, we lie
isUrl,
delegatedAuthentication,
} as ValidatedServerConfig;
}
// These methods make some use of some private methods on the AsyncStoreWithClient to simplify getting into a consistent
// ready state without needing to wire up a dispatcher and pretend to be a js-sdk client.
export const setupAsyncStoreWithClient = async <T extends Object = any>(
store: AsyncStoreWithClient<T>,
client: MatrixClient,
) => {
// @ts-ignore protected access
store.readyStore.useUnitTestClient(client);
// @ts-ignore protected access
await store.onReady();
};
export const resetAsyncStoreWithClient = async <T extends Object = any>(store: AsyncStoreWithClient<T>) => {
// @ts-ignore protected access
await store.onNotReady();
};
export const mockStateEventImplementation = (events: MatrixEvent[]) => {
const stateMap = new EnhancedMap<string, Map<string, MatrixEvent>>();
events.forEach((event) => {
stateMap.getOrCreate(event.getType(), new Map()).set(event.getStateKey()!, event);
});
// recreate the overloading in RoomState
function getStateEvents(eventType: EventType | string): MatrixEvent[];
function getStateEvents(eventType: EventType | string, stateKey: string): MatrixEvent;
function getStateEvents(eventType: EventType | string, stateKey?: string) {
if (stateKey || stateKey === "") {
return stateMap.get(eventType)?.get(stateKey) || null;
}
return Array.from(stateMap.get(eventType)?.values() || []);
}
return getStateEvents;
};
export const mkRoom = (
client: MatrixClient,
roomId: string,
rooms?: ReturnType<typeof mkStubRoom>[],
): MockedObject<Room> => {
const room = mocked(mkStubRoom(roomId, roomId, client));
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([]));
rooms?.push(room);
return room;
};
/**
* Upserts given events into room.currentState
* @param room
* @param events
*/
export const upsertRoomStateEvents = (room: Room, events: MatrixEvent[]): void => {
const eventsMap = events.reduce((acc, event) => {
const eventType = event.getType();
if (!acc.has(eventType)) {
acc.set(eventType, new Map());
}
acc.get(eventType)?.set(event.getStateKey()!, event);
return acc;
}, room.currentState.events || new Map<string, Map<string, MatrixEvent>>());
room.currentState.events = eventsMap;
};
export const mkSpace = (
client: MatrixClient,
spaceId: string,
rooms?: ReturnType<typeof mkStubRoom>[],
children: string[] = [],
): MockedObject<Room> => {
const space = mocked(mkRoom(client, spaceId, rooms));
space.isSpaceRoom.mockReturnValue(true);
space.getType.mockReturnValue(RoomType.Space);
mocked(space.currentState).getStateEvents.mockImplementation(
mockStateEventImplementation(
children.map((roomId) =>
mkEvent({
event: true,
type: EventType.SpaceChild,
room: spaceId,
user: "@user:server",
skey: roomId,
content: { via: [] },
ts: Date.now(),
}),
),
),
);
return space;
};
export const mkRoomMemberJoinEvent = (user: string, room: string, content?: IContent): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomMember,
content: {
membership: KnownMembership.Join,
...content,
},
skey: user,
user,
room,
});
};
export const mkRoomCanonicalAliasEvent = (userId: string, roomId: string, alias: string): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomCanonicalAlias,
content: {
alias,
},
skey: "",
user: userId,
room: roomId,
});
};
export const mkThirdPartyInviteEvent = (user: string, displayName: string, room: string): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomThirdPartyInvite,
content: {
display_name: displayName,
},
skey: "test" + Math.random(),
user,
room,
});
};
export const mkPusher = (extra: Partial<IPusher> = {}): IPusher => ({
app_display_name: "app",
app_id: "123",
data: {},
device_display_name: "name",
kind: "http",
lang: "en",
pushkey: "pushpush",
...extra,
});
/** Add a mute rule for a room. */
export function muteRoom(room: Room): void {
const client = room.client!;
client.pushRules = client.pushRules ?? ({ global: [] } as IPushRules);
client.pushRules.global = client.pushRules.global ?? {};
client.pushRules.global.override = [
{
default: true,
enabled: true,
rule_id: "rule_id",
conditions: [
{
kind: ConditionKind.EventMatch,
key: "room_id",
pattern: room.roomId,
},
],
actions: [],
},
];
}

View file

@ -1,162 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import { MatrixClient, MatrixEvent, MatrixEventEvent, RelationType, Room, Thread } from "matrix-js-sdk/src/matrix";
import { mkMessage, MessageEventProps } from "./test-utils";
export const makeThreadEvent = ({
rootEventId,
replyToEventId,
...props
}: MessageEventProps & {
rootEventId: string;
replyToEventId: string;
}): MatrixEvent =>
mkMessage({
...props,
relatesTo: {
event_id: rootEventId,
rel_type: "m.thread",
["m.in_reply_to"]: {
event_id: replyToEventId,
},
},
});
type MakeThreadEventsProps = {
roomId: Room["roomId"];
// root message user id
authorId: string;
// user ids of thread replies
// cycled through until thread length is fulfilled
participantUserIds: string[];
// number of messages in the thread, root message included
// optional, default 2
length?: number;
ts?: number;
// provide to set current_user_participated accurately
currentUserId?: string;
};
export const makeThreadEvents = ({
roomId,
authorId,
participantUserIds,
length = 2,
ts = 1,
currentUserId,
}: MakeThreadEventsProps): { rootEvent: MatrixEvent; events: MatrixEvent[] } => {
const rootEvent = mkMessage({
user: authorId,
event: true,
room: roomId,
msg: "root event message " + Math.random(),
ts,
});
const rootEventId = rootEvent.getId()!;
const events = [rootEvent];
for (let i = 1; i < length; i++) {
const prevEvent = events[i - 1];
const replyToEventId = prevEvent.getId()!;
const user = participantUserIds[i % participantUserIds.length];
events.push(
makeThreadEvent({
user,
room: roomId,
event: true,
msg: `reply ${i} by ${user}`,
rootEventId,
replyToEventId,
// replies are 1ms after each other
ts: ts + i,
}),
);
}
rootEvent.setUnsigned({
"m.relations": {
[RelationType.Thread]: {
latest_event: events[events.length - 1],
count: length,
current_user_participated: [...participantUserIds, authorId].includes(currentUserId!),
},
},
});
return { rootEvent, events };
};
type MakeThreadProps = {
room: Room;
client: MatrixClient;
authorId: string;
participantUserIds: string[];
length?: number;
ts?: number;
};
/**
* Create a thread but don't actually populate it with events - see
* populateThread for what you probably want to do.
*
* Leaving this here in case it is needed by some people, but I (andyb) would
* expect us to move to use populateThread exclusively.
*/
export const mkThread = ({
room,
client,
authorId,
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): { thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] } => {
const { rootEvent, events } = makeThreadEvents({
roomId: room.roomId,
authorId,
participantUserIds,
length,
ts,
currentUserId: client.getUserId()!,
});
expect(rootEvent).toBeTruthy();
for (const evt of events) {
room?.reEmitter.reEmit(evt, [MatrixEventEvent.BeforeRedaction]);
}
const thread = room.createThread(rootEvent.getId()!, rootEvent, events, true);
return { thread, rootEvent, events };
};
/**
* Create a thread, and make sure the events added to the thread and the room's
* timeline as if they came in via sync.
*
* Note that mkThread doesn't actually add the events properly to the room.
*/
export const populateThread = async ({
room,
client,
authorId,
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): Promise<{ thread: Thread; rootEvent: MatrixEvent; events: MatrixEvent[] }> => {
const ret = mkThread({ room, client, authorId, participantUserIds, length, ts });
// So that we do not have to mock the thread loading, tell the thread
// that it is already loaded, and send the events again to the room
// so they are added to the thread timeline.
ret.thread.initialEventsFetched = true;
await room.addLiveEvents(ret.events);
return ret;
};

View file

@ -1,288 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import EventEmitter from "events";
import { ActionPayload } from "../../src/dispatcher/payloads";
import defaultDispatcher from "../../src/dispatcher/dispatcher";
import { DispatcherAction } from "../../src/dispatcher/actions";
import Modal from "../../src/Modal";
export const emitPromise = (e: EventEmitter, k: string | symbol) => new Promise((r) => e.once(k, r));
/**
* Waits for a certain payload to be dispatched.
* @param waitForAction The action string to wait for or the callback which is invoked for every dispatch. If this returns true, stops waiting.
* @param timeout The max time to wait before giving up and stop waiting. If 0, no timeout.
* @param dispatcher The dispatcher to listen on.
* @returns A promise which resolves when the callback returns true. Resolves with the payload that made it stop waiting.
* Rejects when the timeout is reached.
*/
export function untilDispatch(
waitForAction: DispatcherAction | ((payload: ActionPayload) => boolean),
dispatcher = defaultDispatcher,
timeout = 1000,
): Promise<ActionPayload> {
const callerLine = new Error().stack!.toString().split("\n")[2];
if (typeof waitForAction === "string") {
const action = waitForAction;
waitForAction = (payload) => {
return payload.action === action;
};
}
const callback = waitForAction as (payload: ActionPayload) => boolean;
return new Promise((resolve, reject) => {
let fulfilled = false;
let timeoutId: number;
// set a timeout handler if needed
if (timeout > 0) {
timeoutId = window.setTimeout(() => {
if (!fulfilled) {
reject(new Error(`untilDispatch: timed out at ${callerLine}`));
fulfilled = true;
}
}, timeout);
}
// listen for dispatches
const token = dispatcher.register((p: ActionPayload) => {
const finishWaiting = callback(p);
if (finishWaiting || fulfilled) {
// wait until we're told or we timeout
// if we haven't timed out, resolve now with the payload.
if (!fulfilled) {
resolve(p);
fulfilled = true;
}
// cleanup
dispatcher.unregister(token);
if (timeoutId) {
clearTimeout(timeoutId);
}
}
});
});
}
/**
* Waits for a certain event to be emitted.
* @param emitter The EventEmitter to listen on.
* @param eventName The event string to wait for.
* @param check Optional function which is invoked when the event fires. If this returns true, stops waiting.
* @param timeout The max time to wait before giving up and stop waiting. If 0, no timeout.
* @returns A promise which resolves when the callback returns true or when the event is emitted if
* no callback is provided. Rejects when the timeout is reached.
*/
export function untilEmission(
emitter: EventEmitter,
eventName: string,
check?: (...args: any[]) => boolean,
timeout = 1000,
): Promise<void> {
const callerLine = new Error().stack!.toString().split("\n")[2];
return new Promise((resolve, reject) => {
let fulfilled = false;
let timeoutId: number;
// set a timeout handler if needed
if (timeout > 0) {
timeoutId = window.setTimeout(() => {
if (!fulfilled) {
reject(new Error(`untilEmission: timed out at ${callerLine}`));
fulfilled = true;
}
}, timeout);
}
const callback = (...args: any[]) => {
// if they supplied a check function, call it now. Bail if it returns false.
if (check) {
if (!check(...args)) {
return;
}
}
// we didn't time out, resolve. Otherwise, we already rejected so don't resolve now.
if (!fulfilled) {
resolve();
fulfilled = true;
}
// cleanup
emitter.off(eventName, callback);
if (timeoutId) {
clearTimeout(timeoutId);
}
};
// listen for emissions
emitter.on(eventName, callback);
});
}
export const flushPromises = async () => await new Promise<void>((resolve) => window.setTimeout(resolve));
// with jest's modern fake timers process.nextTick is also mocked,
// flushing promises in the normal way then waits for some advancement
// of the fake timers
// https://gist.github.com/apieceofbart/e6dea8d884d29cf88cdb54ef14ddbcc4?permalink_comment_id=4018174#gistcomment-4018174
export const flushPromisesWithFakeTimers = async (): Promise<void> => {
const promise = new Promise((resolve) => process.nextTick(resolve));
jest.advanceTimersByTime(1);
await promise;
};
/**
* Call fn before calling componentDidUpdate on a react component instance, inst.
* @param {React.Component} inst an instance of a React component.
* @param {number} updates Number of updates to wait for. (Defaults to 1.)
* @returns {Promise} promise that resolves when componentDidUpdate is called on
* given component instance.
*/
export function waitForUpdate(inst: React.Component, updates = 1): Promise<void> {
return new Promise<void>((resolve, reject) => {
const cdu = inst.componentDidUpdate;
console.log(`Waiting for ${updates} update(s)`);
inst.componentDidUpdate = (prevProps, prevState, snapshot) => {
updates--;
console.log(`Got update, ${updates} remaining`);
if (updates == 0) {
inst.componentDidUpdate = cdu;
resolve();
}
if (cdu) cdu(prevProps, prevState, snapshot);
};
});
}
/**
* Advance jests fake timers and Date.now mock by ms
* Useful for testing code using timeouts or intervals
* that also checks timestamps
*/
export const advanceDateAndTime = (ms: number) => {
jest.spyOn(global.Date, "now").mockReturnValue(Date.now() + ms);
jest.advanceTimersByTime(ms);
};
/**
* A horrible hack necessary to wait enough time to ensure any modal is shown after a
* `Modal.createDialog(...)` call. We have to contend with the Modal code which renders
* things asyncronhously and has weird sleeps which we should strive to remove.
*/
export const waitEnoughCyclesForModal = async ({
useFakeTimers = false,
}: {
useFakeTimers?: boolean;
} = {}): Promise<void> => {
// XXX: Maybe in the future with Jest 29.5.0+, we could use `runAllTimersAsync` instead.
const flushFunc = useFakeTimers ? flushPromisesWithFakeTimers : flushPromises;
await flushFunc();
await flushFunc();
await flushFunc();
};
/**
* A horrible hack necessary to make sure modals don't leak and pollute tests.
* `jest-matrix-react` automatic cleanup function does not pick up the async modal
* rendering and the modals don't unmount when the component unmounts. We should strive
* to fix this.
*/
export const clearAllModals = async (): Promise<void> => {
// Prevent modals from leaking and polluting other tests
let keepClosingModals = true;
while (keepClosingModals) {
keepClosingModals = Modal.closeCurrentModal();
// Then wait for the screen to update (probably React rerender and async/await).
// Important for tests using Jest fake timers to not get into an infinite loop
// of removing the same modal because the promises don't flush otherwise.
//
// XXX: Maybe in the future with Jest 29.5.0+, we could use `runAllTimersAsync` instead.
// this is called in some places where timers are not faked
// which causes a lot of noise in the console
// to make a hack even hackier check if timers are faked using a weird trick from github
// then call the appropriate promise flusher
// https://github.com/facebook/jest/issues/10555#issuecomment-1136466942
const jestTimersFaked = setTimeout.name === "setTimeout";
if (jestTimersFaked) {
await flushPromisesWithFakeTimers();
} else {
await flushPromises();
}
}
};
/** Install a stub object at `navigator.mediaDevices` */
export function useMockMediaDevices(): void {
// @ts-ignore assignment of a thing that isn't a `MediaDevices` to read-only property
navigator["mediaDevices"] = {
enumerateDevices: jest.fn().mockResolvedValue([]),
getUserMedia: jest.fn(),
};
}
/**
* Clean up the JSDOM after each test.
*
* Registers `beforeEach` and `afterEach` functions which will deregister any event listeners and timers from the
* `window` and `document` objects.
*
* Also clears out `localStorage` and `sessionStorage`.
*/
export function resetJsDomAfterEach(): void {
// list of calls to run in afterEach
const resetCalls: (() => void)[] = [];
beforeEach(() => {
// intercept `window.addEventListener` and `document.addEventListener`, and register 'removeEventListener' calls
// for `afterEach`.
for (const obj of [window, document]) {
const originalFn = obj.addEventListener;
obj.addEventListener = (...args: Parameters<Window["addEventListener"]>) => {
originalFn.apply(obj, args);
resetCalls.push(() => obj.removeEventListener(...args));
};
// also reset the intercept after the test
resetCalls.push(() => {
obj.addEventListener = originalFn;
});
}
// intercept setTimeout and setInterval, and clear them at the end.
//
// *Don't* use jest.spyOn for this because it makes the DOM testing library think we are using fake timers.
//
["setTimeout", "setInterval"].forEach((name) => {
const originalFn = window[name as keyof Window];
// @ts-ignore assignment to read-only property
window[name] = (...args) => {
const result = originalFn.apply(window, args);
resetCalls.push(() => window.clearTimeout(result));
return result;
};
resetCalls.push(() => {
// @ts-ignore assignment to read-only property
window[name] = originalFn;
});
});
});
afterEach(() => {
// clean up event listeners, timers, etc.
for (const call of resetCalls) {
call();
}
resetCalls.splice(0);
// other cleanup
localStorage.clear();
sessionStorage.clear();
});
}

View file

@ -1,64 +0,0 @@
/*
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
Please see LICENSE files in the repository root for full details.
*/
import React, { ComponentType, Ref } from "react";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { RenderOptions } from "jest-matrix-react";
import { MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
import MatrixClientContext from "../../src/contexts/MatrixClientContext";
import { SDKContext, SdkContextClass } from "../../src/contexts/SDKContext";
type WrapperProps<T> = { wrappedRef?: Ref<ComponentType<T>> } & T;
export function wrapInMatrixClientContext<T>(WrappedComponent: ComponentType<T>): ComponentType<WrapperProps<T>> {
class Wrapper extends React.Component<WrapperProps<T>> {
_matrixClient: MatrixClient;
constructor(props: WrapperProps<T>) {
super(props);
this._matrixClient = peg.safeGet();
}
render() {
return (
<MatrixClientContext.Provider value={this._matrixClient}>
<WrappedComponent ref={this.props.wrappedRef} {...this.props} />
</MatrixClientContext.Provider>
);
}
}
return Wrapper;
}
export function wrapInSdkContext<T>(
WrappedComponent: ComponentType<T>,
sdkContext: SdkContextClass,
): ComponentType<WrapperProps<T>> {
return class extends React.Component<WrapperProps<T>> {
render() {
return (
<SDKContext.Provider value={sdkContext}>
<WrappedComponent {...this.props} />
</SDKContext.Provider>
);
}
};
}
/**
* Test helper to generate React testing library render options for wrapping with a MatrixClientContext.Provider
* @param client the MatrixClient instance to expose via the provider
*/
export function withClientContextRenderOptions(client: MatrixClient): RenderOptions {
return {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={client}>{children}</MatrixClientContext.Provider>
),
};
}