Prepare for repo merge
Signed-off-by: Michael Telatynski <7t3chguy@gmail.com>
This commit is contained in:
parent
0f670b8dc0
commit
b084ff2313
807 changed files with 0 additions and 0 deletions
|
@ -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;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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");
|
||||
};
|
|
@ -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(),
|
||||
});
|
|
@ -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);
|
||||
});
|
||||
};
|
|
@ -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;
|
||||
}
|
||||
});
|
||||
};
|
|
@ -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();
|
||||
};
|
|
@ -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;
|
||||
}
|
|
@ -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";
|
|
@ -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 };
|
|
@ -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,
|
||||
});
|
|
@ -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",
|
||||
});
|
|
@ -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();
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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,
|
||||
});
|
|
@ -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;
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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: [],
|
||||
},
|
||||
];
|
||||
}
|
|
@ -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;
|
||||
};
|
|
@ -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();
|
||||
});
|
||||
}
|
|
@ -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>
|
||||
),
|
||||
};
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue