Merge branch 'develop' into t3chguy/fix/27078

This commit is contained in:
Michael Telatynski
2024-12-04 10:02:26 +00:00
committed by GitHub
473 changed files with 6113 additions and 19397 deletions

View File

@@ -44,17 +44,20 @@ export class MockedCall extends Call {
}
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(),
}),
]);
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(),
}),
],
{ addToState: true },
);
// @ts-ignore deliberately calling a private method
// Let CallStore know that a call might now exist
CallStore.instance.updateRoom(room);
@@ -81,17 +84,20 @@ export class MockedCall extends Call {
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(),
}),
]);
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(),
}),
],
{ addToState: true },
);
super.destroy();
}

View File

@@ -85,7 +85,7 @@ export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoom
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: false,
...override,
};
}

View File

@@ -116,7 +116,7 @@ export function createTestClient(): MatrixClient {
getCrypto: jest.fn().mockReturnValue({
getOwnDeviceKeys: jest.fn(),
getUserDeviceInfo: jest.fn(),
getUserDeviceInfo: jest.fn().mockResolvedValue(new Map()),
getUserVerificationStatus: jest.fn(),
getDeviceVerificationStatus: jest.fn(),
resetKeyBackup: jest.fn(),
@@ -135,6 +135,7 @@ export function createTestClient(): MatrixClient {
loadSessionBackupPrivateKeyFromSecretStorage: jest.fn(),
storeSessionBackupPrivateKey: jest.fn(),
getKeyBackupInfo: jest.fn().mockResolvedValue(null),
getEncryptionInfoForEvent: jest.fn().mockResolvedValue(null),
}),
getPushActionsForEvent: jest.fn(),

View File

@@ -157,6 +157,6 @@ export const populateThread = async ({
// 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);
await room.addLiveEvents(ret.events, { addToState: false });
return ret;
};

View File

@@ -39,10 +39,6 @@ import { Action } from "../../src/dispatcher/actions";
import { getFunctionalMembers } from "../../src/utils/room/getFunctionalMembers";
import SettingsStore from "../../src/settings/SettingsStore";
import { UIFeature } from "../../src/settings/UIFeature";
import { VoiceBroadcastInfoState, VoiceBroadcastPlayback, VoiceBroadcastRecording } from "../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils";
import { SdkContextClass } from "../../src/contexts/SDKContext";
import Modal from "../../src/Modal";
import { createAudioContext } from "../../src/audio/compat";
import * as ManagedHybrid from "../../src/widgets/ManagedHybrid";
@@ -403,53 +399,6 @@ describe("LegacyCallHandler", () => {
await callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
expect(spy).toHaveBeenCalledWith(MatrixClientPeg.safeGet().getRoom(NATIVE_ROOM_ALICE));
});
describe("when listening to a voice broadcast", () => {
let voiceBroadcastPlayback: VoiceBroadcastPlayback;
beforeEach(() => {
voiceBroadcastPlayback = new VoiceBroadcastPlayback(
mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
MatrixClientPeg.safeGet().getSafeUserId(),
"d42",
),
MatrixClientPeg.safeGet(),
SdkContextClass.instance.voiceBroadcastRecordingsStore,
);
SdkContextClass.instance.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback);
jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation();
});
it("and placing a call should pause the broadcast", async () => {
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
expect(voiceBroadcastPlayback.pause).toHaveBeenCalled();
});
});
describe("when recording a voice broadcast", () => {
beforeEach(() => {
SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(
new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
MatrixClientPeg.safeGet().getSafeUserId(),
"d42",
),
MatrixClientPeg.safeGet(),
),
);
});
it("and placing a call should show the info dialog", async () => {
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
expect(Modal.createDialog).toMatchSnapshot();
});
});
});
describe("LegacyCallHandler without third party protocols", () => {
@@ -528,9 +477,6 @@ describe("LegacyCallHandler without third party protocols", () => {
audioElement.id = "remoteAudio";
document.body.appendChild(audioElement);
SdkContextClass.instance.voiceBroadcastPlaybacksStore.clearCurrent();
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
fetchMock.get(
"/media/ring.mp3",
{ body: new Blob(["1", "2", "3", "4"], { type: "audio/mpeg" }) },

View File

@@ -11,8 +11,6 @@ import fetchMockJest from "fetch-mock-jest";
import { advanceDateAndTime, stubClient } from "../test-utils";
import { IMatrixClientPeg, MatrixClientPeg as peg } from "../../src/MatrixClientPeg";
import SettingsStore from "../../src/settings/SettingsStore";
import { SettingLevel } from "../../src/settings/SettingLevel";
jest.useFakeTimers();
@@ -81,27 +79,18 @@ describe("MatrixClientPeg", () => {
});
it("should initialise the rust crypto library by default", async () => {
const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined);
const cryptoStoreKey = new Uint8Array([1, 2, 3, 4]);
await testPeg.start({ rustCryptoStoreKey: cryptoStoreKey });
expect(mockInitRustCrypto).toHaveBeenCalledWith({ storageKey: cryptoStoreKey });
// we should have stashed the setting in the settings store
expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true);
});
it("Should migrate existing login", async () => {
const mockSetValue = jest.spyOn(SettingsStore, "setValue").mockResolvedValue(undefined);
const mockInitRustCrypto = jest.spyOn(testPeg.safeGet(), "initRustCrypto").mockResolvedValue(undefined);
await testPeg.start();
expect(mockInitRustCrypto).toHaveBeenCalledTimes(1);
// we should have stashed the setting in the settings store
expect(mockSetValue).toHaveBeenCalledWith("feature_rust_crypto", null, SettingLevel.DEVICE, true);
});
});
});

View File

@@ -43,8 +43,6 @@ import { mkThread } from "../test-utils/threads";
import dis from "../../src/dispatcher/dispatcher";
import { ThreadPayload } from "../../src/dispatcher/payloads/ThreadPayload";
import { Action } from "../../src/dispatcher/actions";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "./voice-broadcast/utils/test-utils";
import { addReplyToMessageContent } from "../../src/utils/Reply";
jest.mock("../../src/utils/notifications", () => ({
@@ -85,16 +83,13 @@ describe("Notifier", () => {
});
};
const mkAudioEvent = (broadcastChunkContent?: object): MatrixEvent => {
const chunkContent = broadcastChunkContent ? { [VoiceBroadcastChunkEventType]: broadcastChunkContent } : {};
const mkAudioEvent = (): MatrixEvent => {
return mkEvent({
event: true,
type: EventType.RoomMessage,
user: "@user:example.com",
room: "!room:example.com",
content: {
...chunkContent,
msgtype: MsgType.Audio,
body: "test audio message",
},
@@ -320,24 +315,6 @@ describe("Notifier", () => {
);
});
it("should display the expected notification for a broadcast chunk with sequence = 1", () => {
const audioEvent = mkAudioEvent({ sequence: 1 });
Notifier.displayPopupNotification(audioEvent, testRoom);
expect(MockPlatform.displayNotification).toHaveBeenCalledWith(
"@user:example.com (!room1:server)",
"@user:example.com started a voice broadcast",
"data:image/png;base64,00",
testRoom,
audioEvent,
);
});
it("should display the expected notification for a broadcast chunk with sequence = 2", () => {
const audioEvent = mkAudioEvent({ sequence: 2 });
Notifier.displayPopupNotification(audioEvent, testRoom);
expect(MockPlatform.displayNotification).not.toHaveBeenCalled();
});
it("should strip reply fallback", () => {
const event = mkMessage({
msg: "Test",
@@ -581,24 +558,6 @@ describe("Notifier", () => {
Notifier.evaluateEvent(mkAudioEvent());
expect(Notifier.displayPopupNotification).toHaveBeenCalledTimes(1);
});
it("should not show a notification for broadcast info events in any case", () => {
// Let client decide to show a notification
mockClient.getPushActionsForEvent.mockReturnValue({
notify: true,
tweaks: {},
});
const broadcastStartedEvent = mkVoiceBroadcastInfoStateEvent(
"!other:example.org",
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
Notifier.evaluateEvent(broadcastStartedEvent);
expect(Notifier.displayPopupNotification).not.toHaveBeenCalled();
});
});
describe("setPromptHidden", () => {
@@ -624,8 +583,7 @@ describe("Notifier", () => {
content: { body: "this is a thread root" },
}),
testRoom.threadsTimelineSets[0]!.getLiveTimeline(),
false,
false,
{ toStartOfTimeline: false, fromCache: false, addToState: true },
);
expect(fn).not.toHaveBeenCalled();

View File

@@ -147,7 +147,7 @@ describe("RoomNotifs test", () => {
const itShouldCountPredecessorHighlightWhenThereIsAPredecessorInTheCreateEvent = (): void => {
it("and there is a predecessor in the create event, it should count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]);
room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)], { addToState: true });
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
expect(getUnreadNotificationCount(room, NotificationCountType.Highlight, false)).toBe(7);
@@ -157,7 +157,7 @@ describe("RoomNotifs test", () => {
const itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent = (): void => {
it("and there is a predecessor event, it should count predecessor highlight", () => {
client.getVisibleRooms();
room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)]);
room.addLiveEvents([mkCreateEvent(OLD_ROOM_ID)], { addToState: true });
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
@@ -185,7 +185,7 @@ describe("RoomNotifs test", () => {
itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent();
it("and there is only a predecessor event, it should not count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent()]);
room.addLiveEvents([mkCreateEvent()], { addToState: true });
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2);
@@ -204,7 +204,7 @@ describe("RoomNotifs test", () => {
itShouldCountPredecessorHighlightWhenThereIsAPredecessorEvent();
it("and there is only a predecessor event, it should count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent()]);
room.addLiveEvents([mkCreateEvent()], { addToState: true });
upsertRoomStateEvents(room, [mkPredecessorEvent(OLD_ROOM_ID)]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(8);
@@ -212,7 +212,7 @@ describe("RoomNotifs test", () => {
});
it("and there is an unknown room in the predecessor event, it should not count predecessor highlight", () => {
room.addLiveEvents([mkCreateEvent()]);
room.addLiveEvents([mkCreateEvent()], { addToState: true });
upsertRoomStateEvents(room, [mkPredecessorEvent("!unknon:example.com")]);
expect(getUnreadNotificationCount(room, NotificationCountType.Total, false)).toBe(2);

View File

@@ -18,10 +18,6 @@ describe("SdkConfig", () => {
describe("with custom values", () => {
beforeEach(() => {
SdkConfig.put({
voice_broadcast: {
chunk_length: 42,
max_length: 1337,
},
feedback: {
existing_issues_url: "https://existing",
} as any,
@@ -30,8 +26,6 @@ describe("SdkConfig", () => {
it("should return the custom config", () => {
const customConfig = JSON.parse(JSON.stringify(DEFAULTS));
customConfig.voice_broadcast.chunk_length = 42;
customConfig.voice_broadcast.max_length = 1337;
customConfig.feedback.existing_issues_url = "https://existing";
expect(SdkConfig.get()).toEqual(customConfig);
});

View File

@@ -66,10 +66,10 @@ describe("SupportedBrowser", () => {
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/18.0 Safari/605.1.15",
// Firefox 131 on macOS Sonoma
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:131.0) Gecko/20100101 Firefox/131.0",
// Edge 129 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79",
// Edge 129 on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36 Edg/129.0.2792.79",
// Edge 131 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70",
// Edge 131 on macOS
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.2903.70",
// Firefox 131 on Windows
"Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:131.0) Gecko/20100101 Firefox/131.0",
// Firefox 131 on Linux

View File

@@ -16,29 +16,21 @@ import { SpaceStoreClass } from "../../src/stores/spaces/SpaceStore";
import { WidgetLayoutStore } from "../../src/stores/widgets/WidgetLayoutStore";
import { WidgetPermissionStore } from "../../src/stores/widgets/WidgetPermissionStore";
import WidgetStore from "../../src/stores/WidgetStore";
import {
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecordingsStore,
} from "../../src/voice-broadcast";
/**
* A class which provides the same API as SdkContextClass but adds additional unsafe setters which can
* replace individual stores. This is useful for tests which need to mock out stores.
*/
export class TestSdkContext extends SdkContextClass {
public declare _RightPanelStore?: RightPanelStore;
public declare _RoomNotificationStateStore?: RoomNotificationStateStore;
public declare _RoomViewStore?: RoomViewStore;
public declare _WidgetPermissionStore?: WidgetPermissionStore;
public declare _WidgetLayoutStore?: WidgetLayoutStore;
public declare _WidgetStore?: WidgetStore;
public declare _PosthogAnalytics?: PosthogAnalytics;
public declare _SlidingSyncManager?: SlidingSyncManager;
public declare _SpaceStore?: SpaceStoreClass;
public declare _VoiceBroadcastRecordingsStore?: VoiceBroadcastRecordingsStore;
public declare _VoiceBroadcastPreRecordingStore?: VoiceBroadcastPreRecordingStore;
public declare _VoiceBroadcastPlaybacksStore?: VoiceBroadcastPlaybacksStore;
declare public _RightPanelStore?: RightPanelStore;
declare public _RoomNotificationStateStore?: RoomNotificationStateStore;
declare public _RoomViewStore?: RoomViewStore;
declare public _WidgetPermissionStore?: WidgetPermissionStore;
declare public _WidgetLayoutStore?: WidgetLayoutStore;
declare public _WidgetStore?: WidgetStore;
declare public _PosthogAnalytics?: PosthogAnalytics;
declare public _SlidingSyncManager?: SlidingSyncManager;
declare public _SpaceStore?: SpaceStoreClass;
constructor() {
super();

View File

@@ -138,7 +138,7 @@ describe("Unread", () => {
room: roomId,
content: {},
});
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: true });
// Don't care about the code path of hidden events.
mocked(haveRendererForEvent).mockClear().mockReturnValue(true);
@@ -157,7 +157,7 @@ describe("Unread", () => {
content: {},
});
// Only for timeline events.
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: true });
expect(doesRoomHaveUnreadMessages(room, false)).toBe(false);
});
@@ -201,7 +201,7 @@ describe("Unread", () => {
content: {},
});
// Only for timeline events.
room.addLiveEvents([event2]);
room.addLiveEvents([event2], { addToState: true });
expect(doesRoomHaveUnreadMessages(room, false)).toBe(true);
});
@@ -403,7 +403,7 @@ describe("Unread", () => {
redactedEvent.makeRedacted(redactedEvent, room);
console.log("Event Id", redactedEvent.getId());
// Only for timeline events.
room.addLiveEvents([redactedEvent]);
room.addLiveEvents([redactedEvent], { addToState: true });
expect(doesRoomHaveUnreadMessages(room, true)).toBe(true);
expect(logger.warn).toHaveBeenCalledWith(
@@ -448,7 +448,7 @@ describe("Unread", () => {
room: roomId,
content: {},
});
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: true });
});
it("an unthreaded receipt for the event makes the room read", () => {
@@ -502,7 +502,7 @@ describe("Unread", () => {
ts: 100,
currentUserId: myId,
});
room.addLiveEvents(events);
room.addLiveEvents(events, { addToState: true });
threadEvent = events[1];
});
@@ -555,7 +555,7 @@ describe("Unread", () => {
room: roomId,
content: {},
});
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: true });
// It still returns false
expect(doesRoomHaveUnreadThreads(room)).toBe(false);

View File

@@ -1,24 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`LegacyCallHandler when recording a voice broadcast and placing a call should show the info dialog 1`] = `
[MockFunction] {
"calls": [
[
[Function],
{
"description": <p>
You cant start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.
</p>,
"hasCloseButton": true,
"title": "Cant start a call",
},
],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
}
`;

View File

@@ -7,13 +7,13 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { EventTimelineSet, PendingEventOrdering, Room } from "matrix-js-sdk/src/matrix";
import { EventTimelineSet, PendingEventOrdering, Room, RoomEvent } from "matrix-js-sdk/src/matrix";
import { screen, render, waitFor } from "jest-matrix-react";
import { mocked } from "jest-mock";
import FilePanel from "../../../../src/components/structures/FilePanel";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import { stubClient } from "../../../test-utils";
import { mkEvent, stubClient } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
jest.mock("matrix-js-sdk/src/matrix", () => ({
@@ -47,4 +47,43 @@ describe("FilePanel", () => {
});
expect(asFragment()).toMatchSnapshot();
});
describe("addEncryptedLiveEvent", () => {
it("should add file msgtype event to filtered timelineSet", async () => {
const cli = MatrixClientPeg.safeGet();
const room = new Room("!room:server", cli, cli.getSafeUserId(), {
pendingEventOrdering: PendingEventOrdering.Detached,
});
cli.reEmitter.reEmit(room, [RoomEvent.Timeline]);
const timelineSet = new EventTimelineSet(room);
room.getOrCreateFilteredTimelineSet = jest.fn().mockReturnValue(timelineSet);
mocked(cli.getRoom).mockReturnValue(room);
let filePanel: FilePanel | null;
render(
<FilePanel
roomId={room.roomId}
onClose={jest.fn()}
resizeNotifier={new ResizeNotifier()}
ref={(ref) => (filePanel = ref)}
/>,
);
await screen.findByText("No files visible in this room");
const event = mkEvent({
type: "m.room.message",
user: cli.getSafeUserId(),
room: room.roomId,
content: {
body: "hello",
url: "mxc://matrix.org/1234",
msgtype: "m.file",
},
event: true,
});
filePanel!.addEncryptedLiveEvent(event);
expect(timelineSet.getLiveTimeline().getEvents()).toContain(event);
});
});
});

View File

@@ -44,7 +44,6 @@ import {
} from "../../../test-utils";
import * as leaveRoomUtils from "../../../../src/utils/leave-behaviour";
import { OidcClientError } from "../../../../src/utils/oidc/error";
import * as voiceBroadcastUtils from "../../../../src/voice-broadcast/utils/cleanUpBroadcasts";
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
import { CallStore } from "../../../../src/stores/CallStore";
import { Call } from "../../../../src/models/Call";
@@ -811,7 +810,6 @@ describe("<MatrixChat />", () => {
jest.spyOn(LegacyCallHandler.instance, "hangupAllCalls")
.mockClear()
.mockImplementation(() => {});
jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockImplementation(async () => {});
jest.spyOn(PosthogAnalytics.instance, "logout").mockImplementation(() => {});
jest.spyOn(EventIndexPeg, "deleteEventIndex").mockImplementation(async () => {});
@@ -831,22 +829,12 @@ describe("<MatrixChat />", () => {
jest.spyOn(logger, "warn").mockClear();
});
afterAll(() => {
jest.spyOn(voiceBroadcastUtils, "cleanUpBroadcasts").mockRestore();
});
it("should hangup all legacy calls", async () => {
await getComponentAndWaitForReady();
await dispatchLogoutAndWait();
expect(LegacyCallHandler.instance.hangupAllCalls).toHaveBeenCalled();
});
it("should cleanup broadcasts", async () => {
await getComponentAndWaitForReady();
await dispatchLogoutAndWait();
expect(voiceBroadcastUtils.cleanUpBroadcasts).toHaveBeenCalled();
});
it("should disconnect all calls", async () => {
await getComponentAndWaitForReady();
await dispatchLogoutAndWait();

View File

@@ -30,6 +30,7 @@ import {
import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import { IRoomState } from "../../../../src/components/structures/RoomView";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../src/utils/beacon", () => ({
useBeacon: jest.fn(),
@@ -91,9 +92,9 @@ describe("MessagePanel", function () {
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) => (
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={{ ...defaultRoomContext, ...roomContext }}>
<ScopedRoomContextProvider {...defaultRoomContext} {...roomContext}>
<MessagePanel {...defaultProps} {...props} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);

View File

@@ -10,7 +10,7 @@ import React from "react";
import { mocked, Mocked } from "jest-mock";
import { screen, render, act, cleanup } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { MatrixClient, PendingEventOrdering, Room, MatrixEvent, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent } from "matrix-js-sdk/src/matrix";
import { Widget, ClientWidgetApi } from "matrix-widget-api";
import { UserEvent } from "@testing-library/user-event/dist/types/setup/setup";
@@ -26,7 +26,6 @@ import {
wrapInSdkContext,
mkRoomCreateEvent,
mockPlatformPeg,
flushPromises,
useMockMediaDevices,
} from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
@@ -39,17 +38,7 @@ import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
import { TestSdkContext } from "../../TestSdkContext";
import {
VoiceBroadcastInfoState,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastPreRecording,
VoiceBroadcastPreRecordingStore,
VoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from "../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils";
import { RoomViewStore } from "../../../../src/stores/RoomViewStore";
import { IRoomStateEventsActionPayload } from "../../../../src/actions/MatrixActionCreators";
import { Container, WidgetLayoutStore } from "../../../../src/stores/widgets/WidgetLayoutStore";
import WidgetStore from "../../../../src/stores/WidgetStore";
import { WidgetType } from "../../../../src/widgets/WidgetType";
@@ -76,13 +65,6 @@ describe("PipContainer", () => {
let room: Room;
let room2: Room;
let alice: RoomMember;
let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore;
let voiceBroadcastPreRecordingStore: VoiceBroadcastPreRecordingStore;
let voiceBroadcastPlaybacksStore: VoiceBroadcastPlaybacksStore;
const actFlushPromises = async () => {
await flushPromises();
};
beforeEach(async () => {
useMockMediaDevices();
@@ -125,13 +107,7 @@ describe("PipContainer", () => {
sdkContext = new TestSdkContext();
// @ts-ignore PipContainer uses SDKContext in the constructor
SdkContextClass.instance = sdkContext;
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
voiceBroadcastPreRecordingStore = new VoiceBroadcastPreRecordingStore();
voiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(voiceBroadcastRecordingsStore);
sdkContext.client = client;
sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore;
sdkContext._VoiceBroadcastPreRecordingStore = voiceBroadcastPreRecordingStore;
sdkContext._VoiceBroadcastPlaybacksStore = voiceBroadcastPlaybacksStore;
});
afterEach(async () => {
@@ -190,51 +166,10 @@ describe("PipContainer", () => {
ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId);
};
const makeVoiceBroadcastInfoStateEvent = (): MatrixEvent => {
return mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Started,
alice.userId,
client.getDeviceId() || "",
);
};
const setUpVoiceBroadcastRecording = () => {
const infoEvent = makeVoiceBroadcastInfoStateEvent();
const voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client);
voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording);
};
const setUpVoiceBroadcastPreRecording = () => {
const voiceBroadcastPreRecording = new VoiceBroadcastPreRecording(
room,
alice,
client,
voiceBroadcastPlaybacksStore,
voiceBroadcastRecordingsStore,
);
voiceBroadcastPreRecordingStore.setCurrent(voiceBroadcastPreRecording);
};
const setUpRoomViewStore = () => {
sdkContext._RoomViewStore = new RoomViewStore(defaultDispatcher, sdkContext);
};
const mkVoiceBroadcast = (room: Room): MatrixEvent => {
const infoEvent = makeVoiceBroadcastInfoStateEvent();
room.currentState.setStateEvents([infoEvent]);
defaultDispatcher.dispatch<IRoomStateEventsActionPayload>(
{
action: "MatrixActions.RoomState.events",
event: infoEvent,
state: room.currentState,
lastStateEvent: null,
},
true,
);
return infoEvent;
};
it("hides if there's no content", () => {
renderPip();
expect(screen.queryByRole("complementary")).toBeNull();
@@ -339,138 +274,4 @@ describe("PipContainer", () => {
WidgetStore.instance.removeVirtualWidget("1", room.roomId);
});
describe("when there is a voice broadcast recording and pre-recording", () => {
beforeEach(async () => {
setUpVoiceBroadcastPreRecording();
setUpVoiceBroadcastRecording();
renderPip();
await actFlushPromises();
});
it("should render the voice broadcast recording PiP", () => {
// check for the „Live“ badge to be present
expect(screen.queryByText("Live")).toBeInTheDocument();
});
it("and a call it should show both, the call and the recording", async () => {
await withCall(async () => {
// Broadcast: Check for the „Live“ badge to be present
expect(screen.queryByText("Live")).toBeInTheDocument();
// Call: Check for the „Leave“ button to be present
screen.getByRole("button", { name: "Leave" });
});
});
});
describe("when there is a voice broadcast playback and pre-recording", () => {
beforeEach(async () => {
mkVoiceBroadcast(room);
setUpVoiceBroadcastPreRecording();
renderPip();
await actFlushPromises();
});
it("should render the voice broadcast pre-recording PiP", () => {
// check for the „Go live“ button
expect(screen.queryByText("Go live")).toBeInTheDocument();
});
});
describe("when there is a voice broadcast pre-recording", () => {
beforeEach(async () => {
setUpVoiceBroadcastPreRecording();
renderPip();
await actFlushPromises();
});
it("should render the voice broadcast pre-recording PiP", () => {
// check for the „Go live“ button
expect(screen.queryByText("Go live")).toBeInTheDocument();
});
});
describe("when listening to a voice broadcast in a room and then switching to another room", () => {
beforeEach(async () => {
setUpRoomViewStore();
viewRoom(room.roomId);
mkVoiceBroadcast(room);
await actFlushPromises();
expect(voiceBroadcastPlaybacksStore.getCurrent()).toBeTruthy();
await voiceBroadcastPlaybacksStore.getCurrent()?.start();
viewRoom(room2.roomId);
renderPip();
});
it("should render the small voice broadcast playback PiP", () => {
// check for the „pause voice broadcast“ button
expect(screen.getByLabelText("pause voice broadcast")).toBeInTheDocument();
// check for the absence of the „30s forward“ button
expect(screen.queryByLabelText("30s forward")).not.toBeInTheDocument();
});
});
describe("when viewing a room with a live voice broadcast", () => {
let startEvent!: MatrixEvent;
beforeEach(async () => {
setUpRoomViewStore();
viewRoom(room.roomId);
startEvent = mkVoiceBroadcast(room);
renderPip();
await actFlushPromises();
});
it("should render the voice broadcast playback pip", () => {
// check for the „resume voice broadcast“ button
expect(screen.queryByLabelText("play voice broadcast")).toBeInTheDocument();
});
describe("and the broadcast stops", () => {
beforeEach(async () => {
const stopEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Stopped,
alice.userId,
client.getDeviceId() || "",
startEvent,
);
await act(async () => {
room.currentState.setStateEvents([stopEvent]);
defaultDispatcher.dispatch<IRoomStateEventsActionPayload>(
{
action: "MatrixActions.RoomState.events",
event: stopEvent,
state: room.currentState,
lastStateEvent: stopEvent,
},
true,
);
await flushPromises();
});
});
it("should not render the voice broadcast playback pip", () => {
// check for the „resume voice broadcast“ button
expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument();
});
});
describe("and leaving the room", () => {
beforeEach(async () => {
await act(async () => {
viewRoom(room2.roomId);
await flushPromises();
});
});
it("should not render the voice broadcast playback pip", () => {
// check for the „resume voice broadcast“ button
expect(screen.queryByLabelText("play voice broadcast")).not.toBeInTheDocument();
});
});
});
});

View File

@@ -91,7 +91,7 @@ describe("RightPanel", () => {
if (name !== "RightPanel.phases") return realGetValue(name, roomId);
if (roomId === "r1") {
return {
history: [{ phase: RightPanelPhases.RoomMemberList }],
history: [{ phase: RightPanelPhases.MemberList }],
isOpen: true,
};
}
@@ -123,7 +123,7 @@ describe("RightPanel", () => {
await rpsUpdated;
await waitFor(() => expect(screen.queryByTestId("spinner")).not.toBeInTheDocument());
// room one will be in the RoomMemberList phase - confirm this is rendered
// room one will be in the MemberList phase - confirm this is rendered
expect(container.getElementsByClassName("mx_MemberList")).toHaveLength(1);
// wait for RPS room 2 updates to fire, then rerender

View File

@@ -10,18 +10,19 @@ import React, { createRef, RefObject } from "react";
import { mocked, MockedObject } from "jest-mock";
import {
ClientEvent,
EventTimeline,
EventType,
IEvent,
JoinRule,
MatrixClient,
MatrixError,
MatrixEvent,
Room,
RoomEvent,
EventType,
JoinRule,
MatrixError,
RoomStateEvent,
MatrixEvent,
SearchResult,
IEvent,
} from "matrix-js-sdk/src/matrix";
import { CryptoApi, UserVerificationStatus } from "matrix-js-sdk/src/crypto-api";
import { CryptoApi, UserVerificationStatus, CryptoEvent } from "matrix-js-sdk/src/crypto-api";
import { KnownMembership } from "matrix-js-sdk/src/types";
import {
fireEvent,
@@ -34,6 +35,7 @@ import {
cleanup,
} from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { defer } from "matrix-js-sdk/src/utils";
import {
stubClient,
@@ -73,6 +75,7 @@ import { ViewRoomErrorPayload } from "../../../../src/dispatcher/payloads/ViewRo
import { SearchScope } from "../../../../src/Searching";
import { MEGOLM_ENCRYPTION_ALGORITHM } from "../../../../src/utils/crypto";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import { ViewUserPayload } from "../../../../src/dispatcher/payloads/ViewUserPayload.ts";
describe("RoomView", () => {
let cli: MockedObject<MatrixClient>;
@@ -87,8 +90,7 @@ describe("RoomView", () => {
beforeEach(() => {
mockPlatformPeg({ reload: () => {} });
stubClient();
cli = mocked(MatrixClientPeg.safeGet());
cli = mocked(stubClient());
room = new Room(`!${roomCount++}:example.org`, cli, "@alice:example.org");
jest.spyOn(room, "findPredecessor");
@@ -201,6 +203,21 @@ describe("RoomView", () => {
return ref.current!;
};
it("should show member list right panel phase on Action.ViewUser without `payload.member`", async () => {
const spy = jest.spyOn(stores.rightPanelStore, "showOrHidePhase");
await renderRoomView(false);
defaultDispatcher.dispatch<ViewUserPayload>(
{
action: Action.ViewUser,
member: undefined,
},
true,
);
expect(spy).toHaveBeenCalledWith(RightPanelPhases.MemberList);
});
it("when there is no room predecessor, getHiddenHighlightCount should return 0", async () => {
const instance = await getRoomViewInstance();
expect(instance.getHiddenHighlightCount()).toBe(0);
@@ -247,8 +264,9 @@ describe("RoomView", () => {
it("updates url preview visibility on encryption state change", async () => {
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
// we should be starting unencrypted
expect(cli.isRoomEncrypted(room.roomId)).toEqual(false);
expect(await cli.getCrypto()?.isEncryptionEnabledInRoom(room.roomId)).toEqual(false);
const roomViewInstance = await getRoomViewInstance();
@@ -263,23 +281,38 @@ describe("RoomView", () => {
expect(roomViewInstance.state.showUrlPreview).toBe(true);
// now enable encryption
cli.isRoomEncrypted.mockReturnValue(true);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
// and fake an encryption event into the room to prompt it to re-check
await act(() =>
room.addLiveEvents([
new MatrixEvent({
type: "m.room.encryption",
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
}),
]),
);
act(() => {
const encryptionEvent = new MatrixEvent({
type: EventType.RoomEncryption,
sender: cli.getUserId()!,
content: {},
event_id: "someid",
room_id: room.roomId,
});
const roomState = room.getLiveTimeline().getState(EventTimeline.FORWARDS)!;
cli.emit(RoomStateEvent.Events, encryptionEvent, roomState, null);
});
// URL previews should now be disabled
expect(roomViewInstance.state.showUrlPreview).toBe(false);
await waitFor(() => expect(roomViewInstance.state.showUrlPreview).toBe(false));
});
it("should not display the timeline when the room encryption is loading", async () => {
jest.spyOn(room, "getMyMembership").mockReturnValue(KnownMembership.Join);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
const deferred = defer<boolean>();
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockImplementation(() => deferred.promise);
const { asFragment, container } = await mountRoomView();
expect(container.querySelector(".mx_RoomView_messagePanel")).toBeNull();
expect(asFragment()).toMatchSnapshot();
deferred.resolve(true);
await waitFor(() => expect(container.querySelector(".mx_RoomView_messagePanel")).not.toBeNull());
expect(asFragment()).toMatchSnapshot();
});
it("updates live timeline when a timeline reset happens", async () => {
@@ -290,6 +323,32 @@ describe("RoomView", () => {
expect(roomViewInstance.state.liveTimeline).not.toEqual(oldTimeline);
});
it("should update when the e2e status when the user verification changed", async () => {
room.currentState.setStateEvents([
mkRoomMemberJoinEvent(cli.getSafeUserId(), room.roomId),
mkRoomMemberJoinEvent("user@example.com", room.roomId),
]);
room.getMyMembership = jest.fn().mockReturnValue(KnownMembership.Join);
// Not all the calls to cli.isRoomEncrypted are migrated, so we need to mock both.
mocked(cli.isRoomEncrypted).mockReturnValue(true);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(
new UserVerificationStatus(false, false, false),
);
jest.spyOn(cli.getCrypto()!, "getUserDeviceInfo").mockResolvedValue(
new Map([["user@example.com", new Map<string, any>()]]),
);
const { container } = await renderRoomView();
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_normal")).toBeInTheDocument());
const verificationStatus = new UserVerificationStatus(true, true, false);
jest.spyOn(cli.getCrypto()!, "getUserVerificationStatus").mockResolvedValue(verificationStatus);
cli.emit(CryptoEvent.UserTrustStatusChanged, cli.getSafeUserId(), verificationStatus);
await waitFor(() => expect(container.querySelector(".mx_E2EIcon_verified")).toBeInTheDocument());
});
describe("with virtual rooms", () => {
it("checks for a virtual room on initial load", async () => {
const { container } = await renderRoomView();
@@ -427,7 +486,8 @@ describe("RoomView", () => {
]);
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(cli.getSafeUserId());
jest.spyOn(DMRoomMap.shared(), "getRoomIds").mockReturnValue(new Set([room.roomId]));
mocked(cli).isRoomEncrypted.mockReturnValue(true);
jest.spyOn(cli, "getCrypto").mockReturnValue(crypto);
jest.spyOn(cli.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
await renderRoomView();
});
@@ -653,7 +713,7 @@ describe("RoomView", () => {
skey: id,
ts,
});
room.addLiveEvents([widgetEvent]);
room.addLiveEvents([widgetEvent], { addToState: false });
room.currentState.setStateEvents([widgetEvent]);
cli.emit(RoomStateEvent.Events, widgetEvent, room.currentState, null);
await flushPromises();

View File

@@ -0,0 +1,117 @@
/*
Copyright 2024 New Vector Ltd.
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 from "react";
import { mocked, MockedObject } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { render, cleanup, screen, fireEvent } from "jest-matrix-react";
import { stubClient, mockPlatformPeg, unmockPlatformPeg, withClientContextRenderOptions } from "../../../test-utils";
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
import SpaceRoomView from "../../../../src/components/structures/SpaceRoomView.tsx";
import ResizeNotifier from "../../../../src/utils/ResizeNotifier.ts";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks.ts";
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore.ts";
import DMRoomMap from "../../../../src/utils/DMRoomMap.ts";
describe("SpaceRoomView", () => {
let cli: MockedObject<MatrixClient>;
let space: Room;
beforeEach(() => {
mockPlatformPeg({ reload: () => {} });
cli = mocked(stubClient());
space = new Room(`!space:example.org`, cli, cli.getSafeUserId());
space.currentState.setStateEvents([
new MatrixEvent({
type: "m.room.create",
room_id: space.roomId,
sender: cli.getSafeUserId(),
state_key: "",
content: {
creator: cli.getSafeUserId(),
type: "m.space",
},
}),
new MatrixEvent({
type: "m.room.member",
room_id: space.roomId,
sender: cli.getSafeUserId(),
state_key: cli.getSafeUserId(),
content: {
membership: "join",
},
}),
new MatrixEvent({
type: "m.room.member",
room_id: space.roomId,
sender: "@userA:server",
state_key: "@userA:server",
content: {
membership: "join",
},
}),
new MatrixEvent({
type: "m.room.member",
room_id: space.roomId,
sender: "@userB:server",
state_key: "@userB:server",
content: {
membership: "join",
},
}),
new MatrixEvent({
type: "m.room.member",
room_id: space.roomId,
sender: "@userC:server",
state_key: "@userC:server",
content: {
membership: "join",
},
}),
]);
space.updateMyMembership("join");
DMRoomMap.makeShared(cli);
});
afterEach(() => {
unmockPlatformPeg();
jest.clearAllMocks();
cleanup();
});
const renderSpaceRoomView = async (): Promise<ReturnType<typeof render>> => {
const resizeNotifier = new ResizeNotifier();
const permalinkCreator = new RoomPermalinkCreator(space);
const spaceRoomView = render(
<SpaceRoomView
space={space}
resizeNotifier={resizeNotifier}
permalinkCreator={permalinkCreator}
onJoinButtonClicked={jest.fn()}
onRejectButtonClicked={jest.fn()}
/>,
withClientContextRenderOptions(cli),
);
return spaceRoomView;
};
describe("SpaceLanding", () => {
it("should show member list right panel phase on members click on landing", async () => {
const spy = jest.spyOn(RightPanelStore.instance, "setCard");
const { container } = await renderSpaceRoomView();
await expect(screen.findByText("Welcome to")).resolves.toBeVisible();
fireEvent.click(container.querySelector(".mx_FacePile")!);
expect(spy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
});
});
});

View File

@@ -20,7 +20,6 @@ import {
import ThreadPanel, { ThreadFilterType, ThreadPanelHeader } from "../../../../src/components/structures/ThreadPanel";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../src/contexts/RoomContext";
import { _t } from "../../../../src/languageHandler";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
@@ -28,6 +27,7 @@ import ResizeNotifier from "../../../../src/utils/ResizeNotifier";
import { createTestClient, getRoomContext, mkRoom, mockPlatformPeg, stubClient } from "../../../test-utils";
import { mkThread } from "../../../test-utils/threads";
import { IRoomState } from "../../../../src/components/structures/RoomView";
import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../src/utils/Feedback");
@@ -81,11 +81,11 @@ describe("ThreadPanel", () => {
room: mockRoom,
} as unknown as IRoomState;
const { container } = render(
<RoomContext.Provider value={roomContextObject}>
<ScopedRoomContextProvider {...roomContextObject}>
<MatrixClientContext.Provider value={mockClient}>
<ThreadPanelHeader filterOption={ThreadFilterType.All} setFilterOption={() => undefined} />
</MatrixClientContext.Provider>
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
fireEvent.click(getByRole(container, "button", { name: "Mark all as read" }));
await waitFor(() =>
@@ -114,8 +114,8 @@ describe("ThreadPanel", () => {
const TestThreadPanel = () => (
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider
value={getRoomContext(room, {
<ScopedRoomContextProvider
{...getRoomContext(room, {
canSendMessages: true,
})}
>
@@ -125,7 +125,7 @@ describe("ThreadPanel", () => {
resizeNotifier={new ResizeNotifier()}
permalinkCreator={new RoomPermalinkCreator(room)}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
@@ -209,11 +209,11 @@ describe("ThreadPanel", () => {
return event ? Promise.resolve(event) : Promise.reject();
});
const [allThreads, myThreads] = room.threadsTimelineSets;
allThreads!.addLiveEvent(otherThread.rootEvent);
allThreads!.addLiveEvent(mixedThread.rootEvent);
allThreads!.addLiveEvent(ownThread.rootEvent);
myThreads!.addLiveEvent(mixedThread.rootEvent);
myThreads!.addLiveEvent(ownThread.rootEvent);
allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true });
allThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true });
allThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true });
myThreads!.addLiveEvent(mixedThread.rootEvent, { addToState: true });
myThreads!.addLiveEvent(ownThread.rootEvent, { addToState: true });
const renderResult = render(<TestThreadPanel />);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());
@@ -258,7 +258,7 @@ describe("ThreadPanel", () => {
return event ? Promise.resolve(event) : Promise.reject();
});
const [allThreads] = room.threadsTimelineSets;
allThreads!.addLiveEvent(otherThread.rootEvent);
allThreads!.addLiveEvent(otherThread.rootEvent, { addToState: true });
const renderResult = render(<TestThreadPanel />);
await waitFor(() => expect(renderResult.container.querySelector(".mx_AutoHideScrollbar")).toBeFalsy());

View File

@@ -23,7 +23,6 @@ import React, { useState } from "react";
import ThreadView from "../../../../src/components/structures/ThreadView";
import MatrixClientContext from "../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../src/contexts/RoomContext";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import { Action } from "../../../../src/dispatcher/actions";
import dispatcher from "../../../../src/dispatcher/dispatcher";
@@ -34,6 +33,7 @@ import { mockPlatformPeg } from "../../../test-utils/platform";
import { getRoomContext } from "../../../test-utils/room";
import { mkMessage, stubClient } from "../../../test-utils/test-utils";
import { mkThread } from "../../../test-utils/threads";
import { ScopedRoomContextProvider } from "../../../../src/contexts/ScopedRoomContext.tsx";
describe("ThreadView", () => {
const ROOM_ID = "!roomId:example.org";
@@ -51,8 +51,8 @@ describe("ThreadView", () => {
return (
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider
value={getRoomContext(room, {
<ScopedRoomContextProvider
{...getRoomContext(room, {
canSendMessages: true,
})}
>
@@ -63,7 +63,7 @@ describe("ThreadView", () => {
initialEvent={initialEvent}
resizeNotifier={new ResizeNotifier()}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
,
</MatrixClientContext.Provider>
);

View File

@@ -66,7 +66,7 @@ const mkTimeline = (room: Room, events: MatrixEvent[]): [EventTimeline, EventTim
getPendingEvents: () => [] as MatrixEvent[],
} as unknown as EventTimelineSet;
const timeline = new EventTimeline(timelineSet);
events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false }));
events.forEach((event) => timeline.addEvent(event, { toStartOfTimeline: false, addToState: true }));
return [timeline, timelineSet];
};
@@ -150,9 +150,11 @@ const setupPagination = (
mocked(client).paginateEventTimeline.mockImplementation(async (tl, { backwards }) => {
if (tl === timeline) {
if (backwards) {
forEachRight(previousPage ?? [], (event) => tl.addEvent(event, { toStartOfTimeline: true }));
forEachRight(previousPage ?? [], (event) =>
tl.addEvent(event, { toStartOfTimeline: true, addToState: true }),
);
} else {
(nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false }));
(nextPage ?? []).forEach((event) => tl.addEvent(event, { toStartOfTimeline: false, addToState: true }));
}
// Prevent any further pagination attempts in this direction
tl.setPaginationToken(null, backwards ? EventTimeline.BACKWARDS : EventTimeline.FORWARDS);
@@ -256,7 +258,7 @@ describe("TimelinePanel", () => {
describe("and reading the timeline", () => {
beforeEach(async () => {
await renderTimelinePanel();
timelineSet.addLiveEvent(ev1, {});
timelineSet.addLiveEvent(ev1, { addToState: true });
await flushPromises();
// @ts-ignore
await timelinePanel.sendReadReceipts();
@@ -284,11 +286,11 @@ describe("TimelinePanel", () => {
});
it("and forgetting the read markers, should send the stored marker again", async () => {
timelineSet.addLiveEvent(ev2, {});
timelineSet.addLiveEvent(ev2, { addToState: true });
// Add the event to the room as well as the timeline, so we can find it when we
// call findEventById in getEventReadUpTo. This is odd because in our test
// setup, timelineSet is not actually the timelineSet of the room.
await room.addLiveEvents([ev2], {});
await room.addLiveEvents([ev2], { addToState: true });
room.addEphemeralEvents([newReceipt(ev2.getId()!, userId, 222, 200)]);
await timelinePanel!.forgetReadMarker();
expect(client.setRoomReadMarkers).toHaveBeenCalledWith(roomId, ev2.getId());
@@ -314,7 +316,7 @@ describe("TimelinePanel", () => {
it("should send a fully read marker and a private receipt", async () => {
await renderTimelinePanel();
act(() => timelineSet.addLiveEvent(ev1, {}));
act(() => timelineSet.addLiveEvent(ev1, { addToState: true }));
await flushPromises();
// @ts-ignore
@@ -361,7 +363,7 @@ describe("TimelinePanel", () => {
it("should send receipts but no fully_read when reading the thread timeline", async () => {
await renderTimelinePanel();
act(() => timelineSet.addLiveEvent(threadEv1, {}));
act(() => timelineSet.addLiveEvent(threadEv1, { addToState: true }));
await flushPromises();
// @ts-ignore
@@ -871,7 +873,9 @@ describe("TimelinePanel", () => {
// @ts-ignore
thread.fetchEditsWhereNeeded = () => Promise.resolve();
await thread.addEvent(reply1, false, true);
await allThreads.getLiveTimeline().addEvent(thread.rootEvent!, { toStartOfTimeline: true });
await allThreads
.getLiveTimeline()
.addEvent(thread.rootEvent!, { toStartOfTimeline: true, addToState: true });
const replyToEvent = jest.spyOn(thread, "replyToEvent", "get");
const dom = render(
@@ -907,7 +911,9 @@ describe("TimelinePanel", () => {
// @ts-ignore
realThread.fetchEditsWhereNeeded = () => Promise.resolve();
await realThread.addEvent(reply1, true);
await allThreads.getLiveTimeline().addEvent(realThread.rootEvent!, { toStartOfTimeline: true });
await allThreads
.getLiveTimeline()
.addEvent(realThread.rootEvent!, { toStartOfTimeline: true, addToState: true });
const replyToEvent = jest.spyOn(realThread, "replyToEvent", "get");
// @ts-ignore
@@ -968,7 +974,9 @@ describe("TimelinePanel", () => {
events.push(rootEvent);
events.forEach((event) => timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true }));
events.forEach((event) =>
timelineSet.getLiveTimeline().addEvent(event, { toStartOfTimeline: true, addToState: true }),
);
const roomMembership = mkMembership({
mship: KnownMembership.Join,
@@ -988,7 +996,10 @@ describe("TimelinePanel", () => {
jest.spyOn(roomState, "getMember").mockReturnValue(member);
jest.spyOn(timelineSet.getLiveTimeline(), "getState").mockReturnValue(roomState);
timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), { toStartOfTimeline: false });
timelineSet.addEventToTimeline(roomMembership, timelineSet.getLiveTimeline(), {
toStartOfTimeline: false,
addToState: true,
});
for (const event of events) {
jest.spyOn(event, "isDecryptionFailure").mockReturnValue(true);

View File

@@ -7,20 +7,14 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { act, render, RenderResult, screen, waitFor } from "jest-matrix-react";
import { DEVICE_CODE_SCOPE, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { render, screen, waitFor } from "jest-matrix-react";
import { DEVICE_CODE_SCOPE, MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
import { mocked } from "jest-mock";
import fetchMock from "fetch-mock-jest";
import UnwrappedUserMenu from "../../../../src/components/structures/UserMenu";
import { stubClient, wrapInSdkContext } from "../../../test-utils";
import {
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from "../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../voice-broadcast/utils/test-utils";
import { TestSdkContext } from "../../TestSdkContext";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import LogoutDialog from "../../../../src/components/views/dialogs/LogoutDialog";
@@ -34,71 +28,12 @@ import { UserTab } from "../../../../src/components/views/dialogs/UserTab";
describe("<UserMenu>", () => {
let client: MatrixClient;
let renderResult: RenderResult;
let sdkContext: TestSdkContext;
beforeEach(() => {
sdkContext = new TestSdkContext();
});
describe("<UserMenu> when video broadcast", () => {
let voiceBroadcastInfoEvent: MatrixEvent;
let voiceBroadcastRecording: VoiceBroadcastRecording;
let voiceBroadcastRecordingsStore: VoiceBroadcastRecordingsStore;
beforeAll(() => {
client = stubClient();
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
client.getUserId() || "",
client.getDeviceId() || "",
);
});
beforeEach(() => {
voiceBroadcastRecordingsStore = new VoiceBroadcastRecordingsStore();
sdkContext._VoiceBroadcastRecordingsStore = voiceBroadcastRecordingsStore;
voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client);
});
describe("when rendered", () => {
beforeEach(() => {
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
renderResult = render(<UserMenu isPanelCollapsed={true} />);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("and a live voice broadcast starts", () => {
beforeEach(() => {
act(() => {
voiceBroadcastRecordingsStore.setCurrent(voiceBroadcastRecording);
});
});
it("should render the live voice broadcast avatar addon", () => {
expect(renderResult.queryByTestId("user-menu-live-vb")).toBeInTheDocument();
});
describe("and the broadcast ends", () => {
beforeEach(() => {
act(() => {
voiceBroadcastRecordingsStore.clearCurrent();
});
});
it("should not render the live voice broadcast avatar addon", () => {
expect(renderResult.queryByTestId("user-menu-live-vb")).not.toBeInTheDocument();
});
});
});
});
});
describe("<UserMenu> logout", () => {
beforeEach(() => {
client = stubClient();
@@ -106,7 +41,7 @@ describe("<UserMenu>", () => {
it("should logout directly if no crypto", async () => {
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
renderResult = render(<UserMenu isPanelCollapsed={true} />);
render(<UserMenu isPanelCollapsed={true} />);
mocked(client.getRooms).mockReturnValue([
{
@@ -128,7 +63,7 @@ describe("<UserMenu>", () => {
it("should logout directly if no encrypted rooms", async () => {
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
renderResult = render(<UserMenu isPanelCollapsed={true} />);
render(<UserMenu isPanelCollapsed={true} />);
mocked(client.getRooms).mockReturnValue([
{
@@ -152,7 +87,7 @@ describe("<UserMenu>", () => {
it("should show dialog if some encrypted rooms", async () => {
const UserMenu = wrapInSdkContext(UnwrappedUserMenu, sdkContext);
renderResult = render(<UserMenu isPanelCollapsed={true} />);
render(<UserMenu isPanelCollapsed={true} />);
mocked(client.getRooms).mockReturnValue([
{

View File

@@ -62,7 +62,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby=":rbc:"
aria-labelledby=":rg4:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -78,7 +78,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby=":rbh:"
aria-labelledby=":rg9:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -103,7 +103,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</button>
<button
aria-label="Room info"
aria-labelledby=":rbm:"
aria-labelledby=":rge:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -128,7 +128,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
</button>
<button
aria-label="Threads"
aria-labelledby=":rbr:"
aria-labelledby=":rgj:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -157,7 +157,7 @@ exports[`RoomView for a local room in state CREATING should match the snapshot 1
>
<div
aria-label="2 members"
aria-labelledby=":rc0:"
aria-labelledby=":rgo:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -280,7 +280,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby=":rca:"
aria-labelledby=":rh2:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -296,7 +296,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby=":rcf:"
aria-labelledby=":rh7:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -321,7 +321,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</button>
<button
aria-label="Room info"
aria-labelledby=":rck:"
aria-labelledby=":rhc:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -346,7 +346,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
</button>
<button
aria-label="Threads"
aria-labelledby=":rcp:"
aria-labelledby=":rhh:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -375,7 +375,7 @@ exports[`RoomView for a local room in state ERROR should match the snapshot 1`]
>
<div
aria-label="2 members"
aria-labelledby=":rcu:"
aria-labelledby=":rhm:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -583,7 +583,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby=":r70:"
aria-labelledby=":rbo:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -599,7 +599,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby=":r75:"
aria-labelledby=":rbt:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -624,7 +624,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</button>
<button
aria-label="Room info"
aria-labelledby=":r7a:"
aria-labelledby=":rc2:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -649,7 +649,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
</button>
<button
aria-label="Threads"
aria-labelledby=":r7f:"
aria-labelledby=":rc7:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -678,7 +678,7 @@ exports[`RoomView for a local room in state NEW should match the snapshot 1`] =
>
<div
aria-label="2 members"
aria-labelledby=":r7k:"
aria-labelledby=":rcc:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -963,7 +963,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
style="--cpd-icon-button-size: 100%;"
>
<svg
aria-labelledby=":r96:"
aria-labelledby=":rdu:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
@@ -979,7 +979,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
<button
aria-disabled="false"
aria-label="Voice call"
aria-labelledby=":r9b:"
aria-labelledby=":re3:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -1004,7 +1004,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</button>
<button
aria-label="Room info"
aria-labelledby=":r9g:"
aria-labelledby=":re8:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -1029,7 +1029,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</button>
<button
aria-label="Threads"
aria-labelledby=":r9l:"
aria-labelledby=":red:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -1058,7 +1058,7 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
>
<div
aria-label="2 members"
aria-labelledby=":r9q:"
aria-labelledby=":rei:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -1276,6 +1276,571 @@ exports[`RoomView for a local room in state NEW that is encrypted should match t
</div>
`;
exports[`RoomView should not display the timeline when the room encryption is loading 1`] = `
<DocumentFragment>
<div
class="mx_RoomView"
>
<canvas
aria-hidden="true"
height="768"
style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;"
width="0"
/>
<div
class="mx_MainSplit"
>
<div
class="mx_RoomView_body mx_MainSplit_timeline"
data-layout="group"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<button
aria-label="Open room settings"
aria-live="off"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="button"
style="--cpd-avatar-size: 40px;"
tabindex="-1"
>
!
</button>
<button
aria-label="Room info"
class="mx_RoomHeader_infoWrapper"
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
style="--mx-box-flex: 1;"
>
<div
aria-level="1"
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading"
dir="auto"
role="heading"
>
<span
class="mx_RoomHeader_truncated mx_lineClamp"
>
!6:example.org
</span>
</div>
</div>
</button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
aria-disabled="true"
aria-label="There's no one here to call"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
>
<svg
aria-labelledby=":r2c:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z"
/>
</svg>
</div>
</button>
<button
aria-disabled="true"
aria-label="There's no one here to call"
aria-labelledby=":r2h:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z"
/>
</svg>
</div>
</button>
<button
aria-label="Room info"
aria-labelledby=":r2m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button
aria-label="Threads"
aria-labelledby=":r2r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
/>
</svg>
</div>
</button>
</div>
<div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
>
<div
aria-label="0 members"
aria-labelledby=":r30:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
>
<div
class="_stacked-avatars_mcap2_111"
/>
0
</div>
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
<div />
</div>
<main
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
/>
<div
aria-label="Room status bar"
class="mx_RoomView_statusArea"
role="region"
>
<div
class="mx_RoomView_statusAreaBox"
>
<div
class="mx_RoomView_statusAreaBox_line"
/>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`RoomView should not display the timeline when the room encryption is loading 2`] = `
<DocumentFragment>
<div
class="mx_RoomView"
>
<canvas
aria-hidden="true"
height="768"
style="display: block; z-index: 999999; pointer-events: none; position: fixed; top: 0px; right: 0px;"
width="0"
/>
<div
class="mx_MainSplit"
>
<div
class="mx_RoomView_body mx_MainSplit_timeline"
data-layout="group"
>
<header
class="mx_Flex mx_RoomHeader light-panel"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-3x);"
>
<button
aria-label="Open room settings"
aria-live="off"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="button"
style="--cpd-avatar-size: 40px;"
tabindex="-1"
>
!
</button>
<button
aria-label="Room info"
class="mx_RoomHeader_infoWrapper"
tabindex="0"
>
<div
class="mx_Box mx_RoomHeader_info mx_Box--flex"
style="--mx-box-flex: 1;"
>
<div
aria-level="1"
class="_typography_yh5dq_162 _font-body-lg-semibold_yh5dq_83 mx_RoomHeader_heading"
dir="auto"
role="heading"
>
<span
class="mx_RoomHeader_truncated mx_lineClamp"
>
!6:example.org
</span>
</div>
</div>
</button>
<div
class="mx_Flex"
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
aria-disabled="true"
aria-label="There's no one here to call"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
>
<svg
aria-labelledby=":r2c:"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M6 4h10a2 2 0 0 1 2 2v4.286l3.35-2.871a1 1 0 0 1 1.65.76v7.65a1 1 0 0 1-1.65.76L18 13.715V18a2 2 0 0 1-2 2H6a4 4 0 0 1-4-4V8a4 4 0 0 1 4-4Z"
/>
</svg>
</div>
</button>
<button
aria-disabled="true"
aria-label="There's no one here to call"
aria-labelledby=":r2h:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%; --cpd-color-icon-tertiary: var(--cpd-color-icon-disabled);"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="m20.958 16.374.039 3.527c0 .285-.11.537-.33.756-.22.22-.472.33-.756.33a15.97 15.97 0 0 1-6.57-1.105 16.223 16.223 0 0 1-5.563-3.663 16.084 16.084 0 0 1-3.653-5.573 16.313 16.313 0 0 1-1.115-6.56c0-.285.11-.537.33-.757.22-.22.471-.329.755-.329l3.528.039a1.069 1.069 0 0 1 1.085.93l.543 3.954c.026.181.013.349-.039.504a1.088 1.088 0 0 1-.271.426l-1.64 1.64c.337.672.721 1.308 1.154 1.909.433.6 1.444 1.696 1.444 1.696s1.095 1.01 1.696 1.444c.6.433 1.237.817 1.909 1.153l1.64-1.64a1.08 1.08 0 0 1 .426-.27c.155-.052.323-.065.504-.04l3.954.543a1.069 1.069 0 0 1 .93 1.085Z"
/>
</svg>
</div>
</button>
<button
aria-label="Room info"
aria-labelledby=":r2m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 17a.97.97 0 0 0 .713-.288A.968.968 0 0 0 13 16v-4a.968.968 0 0 0-.287-.713A.968.968 0 0 0 12 11a.968.968 0 0 0-.713.287A.968.968 0 0 0 11 12v4c0 .283.096.52.287.712.192.192.43.288.713.288Zm0-8c.283 0 .52-.096.713-.287A.967.967 0 0 0 13 8a.967.967 0 0 0-.287-.713A.968.968 0 0 0 12 7a.968.968 0 0 0-.713.287A.967.967 0 0 0 11 8c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 13a9.738 9.738 0 0 1-3.9-.788 10.099 10.099 0 0 1-3.175-2.137c-.9-.9-1.612-1.958-2.137-3.175A9.738 9.738 0 0 1 2 12a9.74 9.74 0 0 1 .788-3.9 10.099 10.099 0 0 1 2.137-3.175c.9-.9 1.958-1.612 3.175-2.137A9.738 9.738 0 0 1 12 2a9.74 9.74 0 0 1 3.9.788 10.098 10.098 0 0 1 3.175 2.137c.9.9 1.613 1.958 2.137 3.175A9.738 9.738 0 0 1 22 12a9.738 9.738 0 0 1-.788 3.9 10.098 10.098 0 0 1-2.137 3.175c-.9.9-1.958 1.613-3.175 2.137A9.738 9.738 0 0 1 12 22Z"
/>
</svg>
</div>
</button>
<button
aria-label="Threads"
aria-labelledby=":r2r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
tabindex="0"
>
<div
class="_indicator-icon_133tf_26"
style="--cpd-icon-button-size: 100%;"
>
<svg
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M4 3h16a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H6l-2.293 2.293c-.63.63-1.707.184-1.707-.707V5a2 2 0 0 1 2-2Zm3 7h10a.97.97 0 0 0 .712-.287A.967.967 0 0 0 18 9a.967.967 0 0 0-.288-.713A.968.968 0 0 0 17 8H7a.968.968 0 0 0-.713.287A.968.968 0 0 0 6 9c0 .283.096.52.287.713.192.191.43.287.713.287Zm0 4h6c.283 0 .52-.096.713-.287A.968.968 0 0 0 14 13a.968.968 0 0 0-.287-.713A.968.968 0 0 0 13 12H7a.967.967 0 0 0-.713.287A.968.968 0 0 0 6 13c0 .283.096.52.287.713.192.191.43.287.713.287Z"
/>
</svg>
</div>
</button>
</div>
<div
class="_typography_yh5dq_162 _font-body-sm-medium_yh5dq_50"
>
<div
aria-label="0 members"
aria-labelledby=":r30:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
>
<div
class="_stacked-avatars_mcap2_111"
/>
0
</div>
</div>
</header>
<div
class="mx_AutoHideScrollbar mx_AuxPanel"
role="region"
tabindex="-1"
>
<div />
</div>
<main
class="mx_RoomView_timeline mx_RoomView_timeline_rr_enabled"
>
<div
class="mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel"
tabindex="-1"
>
<div
class="mx_RoomView_messageListWrapper"
>
<ol
aria-live="polite"
class="mx_RoomView_MessageList"
style="height: 400px;"
/>
</div>
</div>
</main>
<div
aria-label="Room status bar"
class="mx_RoomView_statusArea"
role="region"
>
<div
class="mx_RoomView_statusAreaBox"
>
<div
class="mx_RoomView_statusAreaBox_line"
/>
</div>
</div>
<div
aria-label="Message composer"
class="mx_MessageComposer mx_MessageComposer_e2eStatus"
role="region"
>
<div
class="mx_MessageComposer_wrapper"
>
<div
class="mx_MessageComposer_row"
>
<div
class="mx_MessageComposer_e2eIconWrapper"
>
<span
tabindex="0"
>
<div
aria-labelledby=":r3e:"
class="mx_E2EIcon mx_E2EIcon_verified mx_MessageComposer_e2eIcon"
/>
</span>
</div>
<div
class="mx_SendMessageComposer"
>
<div
class="mx_BasicMessageComposer"
>
<div
aria-label="Formatting"
class="mx_MessageComposerFormatBar"
role="toolbar"
>
<button
aria-label="Bold"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold"
role="button"
tabindex="0"
type="button"
/>
<button
aria-label="Italics"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic"
role="button"
tabindex="-1"
type="button"
/>
<button
aria-label="Strikethrough"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough"
role="button"
tabindex="-1"
type="button"
/>
<button
aria-label="Code block"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode"
role="button"
tabindex="-1"
type="button"
/>
<button
aria-label="Quote"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote"
role="button"
tabindex="-1"
type="button"
/>
<button
aria-label="Insert link"
class="mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink"
role="button"
tabindex="-1"
type="button"
/>
</div>
<div
aria-autocomplete="list"
aria-disabled="false"
aria-haspopup="listbox"
aria-label="Send an encrypted message…"
aria-multiline="true"
class="mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty"
contenteditable="true"
data-testid="basicmessagecomposer"
dir="auto"
role="textbox"
style="--placeholder: 'Send\\ an\\ encrypted\\ message…';"
tabindex="0"
translate="no"
>
<div>
<br />
</div>
</div>
</div>
</div>
<div
class="mx_MessageComposer_actions"
>
<div
aria-label="Emoji"
class="mx_AccessibleButton mx_EmojiButton mx_MessageComposer_button mx_EmojiButton_icon"
role="button"
tabindex="0"
/>
<div
aria-label="Attachment"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload"
role="button"
tabindex="0"
/>
<div
aria-label="More options"
class="mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu"
role="button"
tabindex="0"
/>
<input
multiple=""
style="display: none;"
type="file"
/>
</div>
</div>
</div>
</div>
</div>
</div>
</div>
</DocumentFragment>
`;
exports[`RoomView should show error view if failed to look up room alias 1`] = `
<DocumentFragment>
<div
@@ -1332,7 +1897,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
aria-label="Open room settings"
aria-live="off"
class="_avatar_mcap2_17 mx_BaseAvatar _avatar-imageless_mcap2_61"
data-color="3"
data-color="6"
data-testid="avatar-img"
data-type="round"
role="button"
@@ -1359,7 +1924,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
<span
class="mx_RoomHeader_truncated mx_lineClamp"
>
!10:example.org
!13:example.org
</span>
</div>
</div>
@@ -1370,7 +1935,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<button
aria-label="Room info"
aria-labelledby=":r2k:"
aria-labelledby=":r7c:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -1395,7 +1960,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Chat"
aria-labelledby=":r2p:"
aria-labelledby=":r7h:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -1420,7 +1985,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby=":r2u:"
aria-labelledby=":r7m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -1449,7 +2014,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
>
<div
aria-label="0 members"
aria-labelledby=":r33:"
aria-labelledby=":r7r:"
class="mx_AccessibleButton mx_FacePile"
role="button"
tabindex="0"
@@ -1487,7 +2052,7 @@ exports[`RoomView video rooms should render joined video room view 1`] = `
</p>
</div>
<button
aria-labelledby=":r3c:"
aria-labelledby=":r84:"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
data-testid="base-card-close-button"
role="button"

View File

@@ -1,33 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UserMenu> <UserMenu> when video broadcast when rendered should render as expected 1`] = `
<div>
<div
class="mx_UserMenu"
>
<div
aria-expanded="false"
aria-haspopup="true"
aria-label="User menu"
class="mx_AccessibleButton mx_UserMenu_contextMenuButton"
role="button"
tabindex="0"
>
<div
class="mx_UserMenu_userAvatar"
>
<span
class="_avatar_mcap2_17 mx_BaseAvatar mx_UserMenu_userAvatar_BaseAvatar _avatar-imageless_mcap2_61"
data-color="2"
data-testid="avatar-img"
data-type="round"
role="presentation"
style="--cpd-avatar-size: 32px;"
>
u
</span>
</div>
</div>
</div>
</div>
`;

View File

@@ -15,10 +15,11 @@ import RecordingPlayback, {
PlaybackLayout,
} from "../../../../../src/components/views/audio_messages/RecordingPlayback";
import { Playback } from "../../../../../src/audio/Playback";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { createAudioContext } from "../../../../../src/audio/compat";
import { flushPromises } from "../../../../test-utils";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/WorkerManager", () => ({
WorkerManager: jest.fn(() => ({
@@ -56,9 +57,9 @@ describe("<RecordingPlayback />", () => {
const defaultRoom = { roomId: "!room:server.org", timelineRenderingType: TimelineRenderingType.File } as IRoomState;
const getComponent = (props: React.ComponentProps<typeof RecordingPlayback>, room = defaultRoom) =>
render(
<RoomContext.Provider value={room}>
<ScopedRoomContextProvider {...room}>
<RecordingPlayback {...props} />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
beforeEach(() => {

View File

@@ -12,11 +12,11 @@ import { MatrixClient, PendingEventOrdering, Room, RoomMember } from "matrix-js-
import React, { ComponentProps } from "react";
import MemberAvatar from "../../../../../src/components/views/avatars/MemberAvatar";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { getRoomContext } from "../../../../test-utils/room";
import { stubClient } from "../../../../test-utils/test-utils";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("MemberAvatar", () => {
const ROOM_ID = "roomId";
@@ -27,9 +27,9 @@ describe("MemberAvatar", () => {
function getComponent(props: Partial<ComponentProps<typeof MemberAvatar>>) {
return (
<RoomContext.Provider value={getRoomContext(room, {})}>
<ScopedRoomContextProvider {...getRoomContext(room, {})}>
<MemberAvatar member={null} size="35px" {...props} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
);
}

View File

@@ -27,7 +27,7 @@ import { mocked } from "jest-mock";
import userEvent from "@testing-library/user-event";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { canEditContent } from "../../../../../src/utils/EventUtils";
import { copyPlaintext, getSelectedText } from "../../../../../src/utils/strings";
@@ -37,9 +37,8 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { ReadPinsEventId } from "../../../../../src/components/views/right_panel/types";
import { Action } from "../../../../../src/dispatcher/actions";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { createMessageEventContent } from "../../../../test-utils/events";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/utils/strings", () => ({
copyPlaintext: jest.fn(),
@@ -233,17 +232,6 @@ describe("MessageContextMenu", () => {
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
});
it("should not allow forwarding a voice broadcast", () => {
const broadcastStartEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
createMenu(broadcastStartEvent);
expect(document.querySelector('li[aria-label="Forward"]')).toBeFalsy();
});
describe("forwarding beacons", () => {
const aliceId = "@alice:server.org";
@@ -546,8 +534,8 @@ function createMenu(
client.getRoom = jest.fn().mockReturnValue(room);
return render(
<RoomContext.Provider value={context as IRoomState}>
<ScopedRoomContextProvider {...(context as IRoomState)}>
<MessageContextMenu mxEvent={mxEvent} onFinished={jest.fn()} {...props} />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
}

View File

@@ -120,7 +120,7 @@ describe("RoomGeneralContextMenu", () => {
user: "@user:id",
ts: 1000,
});
room.addLiveEvents([event], {});
room.addLiveEvents([event], { addToState: true });
const { container } = getComponent({});

View File

@@ -6,14 +6,11 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { Feature, ServerSupport } from "matrix-js-sdk/src/feature";
import { MatrixClient, MatrixEvent, RelationType } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { screen, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { flushPromises, mkEvent, stubClient } from "../../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { createRedactEventDialog } from "../../../../../src/components/views/dialogs/ConfirmRedactDialog";
describe("ConfirmRedactDialog", () => {
@@ -21,15 +18,6 @@ describe("ConfirmRedactDialog", () => {
let client: MatrixClient;
let mxEvent: MatrixEvent;
const setUpVoiceBroadcastStartedEvent = () => {
mxEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId()!,
client.deviceId!,
);
};
const confirmDeleteVoiceBroadcastStartedEvent = async () => {
act(() => createRedactEventDialog({ mxEvent }));
// double-flush promises required for the dialog to show up
@@ -68,44 +56,4 @@ describe("ConfirmRedactDialog", () => {
`cannot redact event ${mxEvent.getId()} without room ID`,
);
});
describe("when redacting a voice broadcast started event", () => {
beforeEach(() => {
setUpVoiceBroadcastStartedEvent();
});
describe("and the server does not support relation based redactions", () => {
beforeEach(() => {
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unsupported);
});
describe("and displaying and confirm the dialog for a voice broadcast", () => {
beforeEach(async () => {
await confirmDeleteVoiceBroadcastStartedEvent();
});
it("should call redact without `with_rel_types`", () => {
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {});
});
});
});
describe("and the server supports relation based redactions", () => {
beforeEach(() => {
client.canSupport.set(Feature.RelationBasedRedactions, ServerSupport.Unstable);
});
describe("and displaying and confirm the dialog for a voice broadcast", () => {
beforeEach(async () => {
await confirmDeleteVoiceBroadcastStartedEvent();
});
it("should call redact with `with_rel_types`", () => {
expect(client.redactEvent).toHaveBeenCalledWith(roomId, mxEvent.getId(), undefined, {
with_rel_types: [RelationType.Reference],
});
});
});
});
});
});

View File

@@ -67,7 +67,6 @@ describe("ForwardDialog", () => {
getAccountData: jest.fn().mockReturnValue(accountDataEvent),
getPushActionsForEvent: jest.fn(),
mxcUrlToHttp: jest.fn().mockReturnValue(""),
isRoomEncrypted: jest.fn().mockReturnValue(false),
getProfileInfo: jest.fn().mockResolvedValue({
displayname: "Alice",
}),

View File

@@ -141,16 +141,19 @@ describe("InviteDialog", () => {
jest.clearAllMocks();
room = new Room(roomId, mockClient, mockClient.getSafeUserId());
room.addLiveEvents([
mkMessage({
msg: "Hello",
relatesTo: undefined,
event: true,
room: roomId,
user: mockClient.getSafeUserId(),
ts: Date.now(),
}),
]);
room.addLiveEvents(
[
mkMessage({
msg: "Hello",
relatesTo: undefined,
event: true,
room: roomId,
user: mockClient.getSafeUserId(),
ts: Date.now(),
}),
],
{ addToState: true },
);
room.currentState.setStateEvents([
mkRoomCreateEvent(bobId, roomId),
mkMembership({

View File

@@ -1,104 +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 React from "react";
import { render, screen } from "jest-matrix-react";
import { Device, MatrixClient } from "matrix-js-sdk/src/matrix";
import { stubClient } from "../../../../test-utils";
import { ManualDeviceKeyVerificationDialog } from "../../../../../src/components/views/dialogs/ManualDeviceKeyVerificationDialog";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
describe("ManualDeviceKeyVerificationDialog", () => {
let mockClient: MatrixClient;
function renderDialog(userId: string, device: Device, onLegacyFinished: (confirm: boolean) => void) {
return render(
<MatrixClientContext.Provider value={mockClient}>
<ManualDeviceKeyVerificationDialog userId={userId} device={device} onFinished={onLegacyFinished} />
</MatrixClientContext.Provider>,
);
}
beforeEach(() => {
mockClient = stubClient();
});
it("should display the device", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const { container } = renderDialog(mockClient.getUserId()!, device, jest.fn());
// Then
expect(container).toMatchSnapshot();
});
it("should display the device of another user", () => {
// When
const userId = "@alice:example.com";
const deviceId = "XYZ";
const device = new Device({
userId,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const { container } = renderDialog(userId, device, jest.fn());
// Then
expect(container).toMatchSnapshot();
});
it("should call onFinished and matrixClient.setDeviceVerified", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const onFinished = jest.fn();
renderDialog(mockClient.getUserId()!, device, onFinished);
screen.getByRole("button", { name: "Verify session" }).click();
// Then
expect(onFinished).toHaveBeenCalledWith(true);
expect(mockClient.setDeviceVerified).toHaveBeenCalledWith(mockClient.getUserId(), deviceId, true);
});
it("should call onFinished and not matrixClient.setDeviceVerified", () => {
// When
const deviceId = "XYZ";
const device = new Device({
userId: mockClient.getUserId()!,
deviceId,
displayName: "my device",
algorithms: [],
keys: new Map([[`ed25519:${deviceId}`, "ABCDEFGH"]]),
});
const onFinished = jest.fn();
renderDialog(mockClient.getUserId()!, device, onFinished);
screen.getByRole("button", { name: "Cancel" }).click();
// Then
expect(onFinished).toHaveBeenCalledWith(false);
expect(mockClient.setDeviceVerified).not.toHaveBeenCalled();
});
});

View File

@@ -7,111 +7,139 @@ Please see LICENSE files in the repository root for full details.
*/
import React from "react";
import { EventTimeline, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { render, RenderOptions } from "jest-matrix-react";
import { MatrixClient, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { render, screen, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { waitFor } from "@testing-library/dom";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { _t } from "../../../../../src/languageHandler";
import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog";
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
import { UIFeature } from "../../../../../src/settings/UIFeature";
import { stubClient } from "../../../../test-utils";
jest.mock("../../../../../src/utils/ShieldUtils");
function getWrapper(): RenderOptions {
return {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={MatrixClientPeg.safeGet()}>{children}</MatrixClientContext.Provider>
),
};
}
import { stubClient, withClientContextRenderOptions } from "../../../../test-utils";
import * as StringsModule from "../../../../../src/utils/strings";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks.ts";
describe("ShareDialog", () => {
let client: MatrixClient;
let room: Room;
const ROOM_ID = "!1:example.org";
const copyTextFunc = jest.fn();
beforeEach(async () => {
stubClient();
room = new Room(ROOM_ID, MatrixClientPeg.get()!, "@alice:example.org");
client = stubClient();
room = new Room("!1:example.org", client, "@alice:example.org");
jest.spyOn(StringsModule, "copyPlaintext").mockImplementation(copyTextFunc);
});
afterEach(() => {
jest.restoreAllMocks();
copyTextFunc.mockClear();
});
it("renders room share dialog", () => {
const { container: withoutEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
expect(withoutEvents).toHaveTextContent(_t("share|title_room"));
function renderComponent(target: Room | RoomMember | URL) {
return render(<ShareDialog target={target} onFinished={jest.fn()} />, withClientContextRenderOptions(client));
}
jest.spyOn(room, "getLiveTimeline").mockReturnValue({ getEvents: () => [{} as MatrixEvent] } as EventTimeline);
const { container: withEvents } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
expect(withEvents).toHaveTextContent(_t("share|permalink_most_recent"));
const getUrl = () => new URL("https://matrix.org/");
const getRoomMember = () => new RoomMember(room.roomId, "@alice:example.org");
test.each([
{ name: "an URL", title: "Share Link", url: "https://matrix.org/", getTarget: getUrl },
{
name: "a room member",
title: "Share User",
url: "https://matrix.to/#/@alice:example.org",
getTarget: getRoomMember,
},
])("should render a share dialog for $name", async ({ title, url, getTarget }) => {
const { asFragment } = renderComponent(getTarget());
expect(screen.getByRole("heading", { name: title })).toBeInTheDocument();
expect(screen.getByText(url)).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Copy link" }));
expect(copyTextFunc).toHaveBeenCalledWith(url);
});
it("renders user share dialog", () => {
mockRoomMembers(room, 1);
const { container } = render(
<ShareDialog target={room.getJoinedMembers()[0]} onFinished={jest.fn()} />,
getWrapper(),
it("should render a share dialog for a room", async () => {
const expectedURL = "https://matrix.to/#/!1:example.org";
jest.spyOn(room.getLiveTimeline(), "getEvents").mockReturnValue([new MatrixEvent({ event_id: "!eventId" })]);
const { asFragment } = renderComponent(room);
expect(screen.getByRole("heading", { name: "Share Room" })).toBeInTheDocument();
expect(screen.getByText(expectedURL)).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: "Link to most recent message" })).toBeInTheDocument();
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Copy link" }));
expect(copyTextFunc).toHaveBeenCalledWith(expectedURL);
// Click on the checkbox to link to the most recent message
await userEvent.click(screen.getByRole("checkbox", { name: "Link to most recent message" }));
const newExpectedURL = "https://matrix.to/#/!1:example.org/!eventId";
expect(screen.getByText(newExpectedURL)).toBeInTheDocument();
});
it("should render a share dialog for a matrix event", async () => {
const matrixEvent = new MatrixEvent({ event_id: "!eventId" });
const permalinkCreator = new RoomPermalinkCreator(room);
const expectedURL = "https://matrix.to/#/!1:example.org/!eventId";
const { asFragment } = render(
<ShareDialog target={matrixEvent} permalinkCreator={permalinkCreator} onFinished={jest.fn()} />,
withClientContextRenderOptions(client),
);
expect(container).toHaveTextContent(_t("share|title_user"));
expect(screen.getByRole("heading", { name: "Share Room Message" })).toBeInTheDocument();
expect(screen.getByText(expectedURL)).toBeInTheDocument();
expect(screen.getByRole("checkbox", { name: "Link to selected message" })).toBeChecked();
expect(asFragment()).toMatchSnapshot();
await userEvent.click(screen.getByRole("button", { name: "Copy link" }));
expect(copyTextFunc).toHaveBeenCalledWith(expectedURL);
// Click on the checkbox to link to the room
await userEvent.click(screen.getByRole("checkbox", { name: "Link to selected message" }));
expect(screen.getByText("https://matrix.to/#/!1:example.org")).toBeInTheDocument();
});
it("renders link share dialog", () => {
mockRoomMembers(room, 1);
const { container } = render(
<ShareDialog target={new URL("https://matrix.org")} onFinished={jest.fn()} />,
getWrapper(),
);
expect(container).toHaveTextContent(_t("share|title_link"));
it("should change the copy button text when clicked", async () => {
jest.useFakeTimers();
const user = userEvent.setup({ advanceTimers: jest.advanceTimersByTime });
// To not be bother with rtl warnings about QR code state update
jest.spyOn(SettingsStore, "getValue").mockReturnValue(false);
renderComponent(room);
await user.click(screen.getByRole("button", { name: "Copy link" }));
// Move after `copyPlaintext`
await jest.advanceTimersToNextTimerAsync();
expect(screen.getByRole("button", { name: "Link copied" })).toBeInTheDocument();
// 2 sec after the button should be back to normal
act(() => jest.advanceTimersByTime(2000));
await waitFor(() => expect(screen.getByRole("button", { name: "Copy link" })).toBeInTheDocument());
});
it("renders the QR code if configured", () => {
it("should not render the QR code if disabled", () => {
const originalGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
if (feature === UIFeature.ShareQRCode) return true;
if (feature === UIFeature.ShareQRCode) return false;
return originalGetValue(feature);
});
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_qrcode_container").length > 0;
expect(qrCodesVisible).toBe(true);
const { asFragment } = renderComponent(room);
expect(screen.queryByRole("img", { name: "QR code" })).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
it("renders the social button if configured", () => {
it("should not render the socials if disabled", () => {
const originalGetValue = SettingsStore.getValue;
jest.spyOn(SettingsStore, "getValue").mockImplementation((feature) => {
if (feature === UIFeature.ShareSocial) return true;
if (feature === UIFeature.ShareSocial) return false;
return originalGetValue(feature);
});
const { container } = render(<ShareDialog target={room} onFinished={jest.fn()} />, getWrapper());
const qrCodesVisible = container.getElementsByClassName("mx_ShareDialog_social_container").length > 0;
expect(qrCodesVisible).toBe(true);
});
it("renders custom title and subtitle", () => {
const { container } = render(
<ShareDialog
target={room}
customTitle="test_title_123"
subtitle="custom_subtitle_1234"
onFinished={jest.fn()}
/>,
getWrapper(),
);
expect(container).toHaveTextContent("test_title_123");
expect(container).toHaveTextContent("custom_subtitle_1234");
const { asFragment } = renderComponent(room);
expect(screen.queryByRole("link", { name: "Reddit" })).toBeNull();
expect(asFragment()).toMatchSnapshot();
});
});
/**
*
* @param count the number of users to create
*/
function mockRoomMembers(room: Room, count: number) {
const members = Array(count)
.fill(0)
.map((_, index) => new RoomMember(room.roomId, "@alice:example.org"));
room.currentState.setJoinedMemberCount(members.length);
room.getJoinedMembers = jest.fn().mockReturnValue(members);
}

View File

@@ -0,0 +1,59 @@
/*
* Copyright 2024 New Vector Ltd.
*
* 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 from "react";
import { Device, MatrixClient, User } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { stubClient } from "../../../../test-utils";
import UntrustedDeviceDialog from "../../../../../src/components/views/dialogs/UntrustedDeviceDialog.tsx";
describe("<UntrustedDeviceDialog />", () => {
let client: MatrixClient;
let user: User;
let device: Device;
const onFinished = jest.fn();
beforeEach(() => {
client = stubClient();
user = User.createUser("@alice:example.org", client);
user.setDisplayName("Alice");
device = new Device({ deviceId: "device_id", userId: user.userId, algorithms: [], keys: new Map() });
});
afterEach(() => {
onFinished.mockReset();
});
function renderComponent() {
return render(<UntrustedDeviceDialog user={user} device={device} onFinished={onFinished} />);
}
it("should display the dialog for the device of another user", () => {
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
it("should display the dialog for the device of the current user", () => {
jest.spyOn(client, "getUserId").mockReturnValue(user.userId);
const { asFragment } = renderComponent();
expect(asFragment()).toMatchSnapshot();
});
it("should call onFinished without parameter when Done is clicked", () => {
renderComponent();
screen.getByRole("button", { name: "Done" }).click();
expect(onFinished).toHaveBeenCalledWith();
});
it("should call onFinished with sas when Interactively verify by emoji is clicked", () => {
renderComponent();
screen.getByRole("button", { name: "Interactively verify by emoji" }).click();
expect(onFinished).toHaveBeenCalledWith("sas");
});
});

View File

@@ -185,33 +185,6 @@ exports[`DevtoolsDialog renders the devtools dialog 1`] = `
/>
</div>
</div>
<div
class="mx_SettingsFlag"
>
<label
class="mx_SettingsFlag_label"
for="mx_SettingsFlag_4yVCeEefiPqp"
>
<span
class="mx_SettingsFlag_labelText"
>
Force 15s voice broadcast chunk length
</span>
</label>
<div
aria-checked="false"
aria-disabled="false"
aria-label="Force 15s voice broadcast chunk length"
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
id="mx_SettingsFlag_4yVCeEefiPqp"
role="switch"
tabindex="0"
>
<div
class="mx_ToggleSwitch_ball"
/>
</div>
</div>
</div>
</div>
<div

View File

@@ -1,231 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ManualDeviceKeyVerificationDialog should display the device 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Verify session
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div>
<p>
Confirm by comparing the following with the User Settings in your other session:
</p>
<div
class="mx_DeviceVerifyDialog_cryptoSection"
>
<ul>
<li>
<label>
Session name
:
</label>
<span>
my device
</span>
</li>
<li>
<label>
Session ID
:
</label>
<span>
<code>
XYZ
</code>
</span>
</li>
<li>
<label>
Session key
:
</label>
<span>
<code>
<strong>
ABCD EFGH
</strong>
</code>
</span>
</li>
</ul>
</div>
<p>
If they don't match, the security of your communication may be compromised.
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Cancel
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Verify session
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;
exports[`ManualDeviceKeyVerificationDialog should display the device of another user 1`] = `
<div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_QuestionDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Verify session
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<div>
<p>
Confirm this user's session by comparing the following with their User Settings:
</p>
<div
class="mx_DeviceVerifyDialog_cryptoSection"
>
<ul>
<li>
<label>
Session name
:
</label>
<span>
my device
</span>
</li>
<li>
<label>
Session ID
:
</label>
<span>
<code>
XYZ
</code>
</span>
</li>
<li>
<label>
Session key
:
</label>
<span>
<code>
<strong>
ABCD EFGH
</strong>
</code>
</span>
</li>
</ul>
</div>
<p>
If they don't match, the security of your communication may be compromised.
</p>
</div>
</div>
<div
class="mx_Dialog_buttons"
>
<span
class="mx_Dialog_buttons_row"
>
<button
data-testid="dialog-cancel-button"
type="button"
>
Cancel
</button>
<button
class="mx_Dialog_primary"
data-testid="dialog-primary-button"
type="button"
>
Verify session
</button>
</span>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</div>
`;

View File

@@ -0,0 +1,852 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ShareDialog should not render the QR code if disabled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Room
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<span>
https://matrix.to/#/!1:example.org
</span>
</div>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should not render the socials if disabled 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Room
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.to/#/!1:example.org
</span>
</div>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should render a share dialog for a matrix event 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Room Message
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.to/#/!1:example.org/!eventId
</span>
</div>
<label>
<div
class="_container_1wloq_18"
>
<input
checked=""
class="_input_1wloq_26"
type="checkbox"
/>
<div
class="_ui_1wloq_27"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>
Link to selected message
</label>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.to/#/!1:example.org/!eventId"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should render a share dialog for a room 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Room
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.to/#/!1:example.org
</span>
</div>
<label>
<div
class="_container_1wloq_18"
>
<input
class="_input_1wloq_26"
type="checkbox"
/>
<div
class="_ui_1wloq_27"
>
<svg
aria-hidden="true"
fill="currentColor"
height="1em"
viewBox="0 0 24 24"
width="1em"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M9.55 17.575c-.133 0-.258-.02-.375-.063a.876.876 0 0 1-.325-.212L4.55 13c-.183-.183-.27-.42-.263-.713.009-.291.105-.529.288-.712a.948.948 0 0 1 .7-.275.95.95 0 0 1 .7.275L9.55 15.15l8.475-8.475c.183-.183.42-.275.713-.275.291 0 .529.092.712.275.183.183.275.42.275.713 0 .291-.092.529-.275.712l-9.2 9.2c-.1.1-.208.17-.325.212a1.106 1.106 0 0 1-.375.063Z"
/>
</svg>
</div>
</div>
Link to most recent message
</label>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.to/#/!1:example.org"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should render a share dialog for a room member 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share User
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.to/#/@alice:example.org
</span>
</div>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.to/#/@alice:example.org"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`ShareDialog should render a share dialog for an URL 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-describedby="mx_Dialog_content"
aria-labelledby="mx_BaseDialog_title"
class="mx_ShareDialog"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
Share Link
</h1>
</div>
<div
class="mx_ShareDialog_content"
>
<div
class="mx_ShareDialog_top"
>
<div
class="mx_QRCode"
>
<div
class="mx_Spinner"
>
<div
aria-label="Loading…"
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
</div>
</div>
<span>
https://matrix.org/
</span>
</div>
<button
class="_button_i91xf_17 _has-icon_i91xf_66"
data-kind="primary"
data-size="lg"
role="button"
tabindex="0"
>
<svg
aria-hidden="true"
fill="currentColor"
height="20"
viewBox="0 0 24 24"
width="20"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M12 19.071c-.978.978-2.157 1.467-3.536 1.467-1.378 0-2.557-.489-3.535-1.467-.978-.978-1.467-2.157-1.467-3.536 0-1.378.489-2.557 1.467-3.535L7.05 9.879c.2-.2.436-.3.707-.3.271 0 .507.1.707.3.2.2.301.436.301.707 0 .27-.1.506-.3.707l-2.122 2.121a2.893 2.893 0 0 0-.884 2.122c0 .824.295 1.532.884 2.12.59.59 1.296.885 2.121.885s1.533-.295 2.122-.884l2.121-2.121c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707L12 19.07Zm-1.414-4.243c-.2.2-.436.3-.707.3a.967.967 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l4.243-4.242c.2-.2.436-.301.707-.301.271 0 .507.1.707.3.2.2.3.437.3.708 0 .27-.1.506-.3.707l-4.242 4.242Zm6.364-.707c-.2.2-.436.3-.707.3a.968.968 0 0 1-.707-.3.969.969 0 0 1-.301-.707c0-.27.1-.507.3-.707l2.122-2.121c.59-.59.884-1.297.884-2.122s-.295-1.532-.884-2.12a2.893 2.893 0 0 0-2.121-.885c-.825 0-1.532.295-2.122.884l-2.121 2.121c-.2.2-.436.301-.707.301a.968.968 0 0 1-.707-.3.97.97 0 0 1-.3-.708c0-.27.1-.506.3-.707L12 4.93c.978-.978 2.157-1.467 3.536-1.467 1.378 0 2.557.489 3.535 1.467.978.978 1.467 2.157 1.467 3.535 0 1.38-.489 2.558-1.467 3.536l-2.121 2.121Z"
/>
</svg>
Copy link
</button>
<div
class="mx_ShareDialog_social"
>
<a
href="https://www.facebook.com/sharer/sharer.php?u=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="Facebook"
>
<img
alt="Facebook"
src="image-file-stub"
/>
</a>
<a
href="https://twitter.com/home?status=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="Twitter"
>
<img
alt="Twitter"
src="image-file-stub"
/>
</a>
<a
href="https://www.linkedin.com/shareArticle?mini=true&url=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="LinkedIn"
>
<img
alt="LinkedIn"
src="image-file-stub"
/>
</a>
<a
href="https://www.reddit.com/submit?url=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="Reddit"
>
<img
alt="Reddit"
src="image-file-stub"
/>
</a>
<a
href="mailto:?body=https://matrix.org/"
rel="noreferrer noopener"
target="_blank"
title="email"
>
<img
alt="email"
src="image-file-stub"
/>
</a>
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View File

@@ -0,0 +1,149 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<UntrustedDeviceDialog /> should display the dialog for the device of another user 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<div
class="mx_E2EIcon mx_E2EIcon_warning"
style="width: 24px; height: 24px;"
/>
Not Trusted
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<p>
Alice (@alice:example.org) signed in to a new session without verifying it:
</p>
<p>
(device_id)
</p>
<p>
Ask this user to verify their session, or manually verify it below.
</p>
</div>
<div
class="mx_Dialog_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Interactively verify by emoji
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Done
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;
exports[`<UntrustedDeviceDialog /> should display the dialog for the device of the current user 1`] = `
<DocumentFragment>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
<div
aria-labelledby="mx_BaseDialog_title"
class="mx_UntrustedDeviceDialog mx_Dialog_fixedWidth"
data-focus-lock-disabled="false"
role="dialog"
>
<div
class="mx_Dialog_header"
>
<h1
class="mx_Heading_h3 mx_Dialog_title"
id="mx_BaseDialog_title"
>
<div
class="mx_E2EIcon mx_E2EIcon_warning"
style="width: 24px; height: 24px;"
/>
Not Trusted
</h1>
</div>
<div
class="mx_Dialog_content"
id="mx_Dialog_content"
>
<p>
You signed in to a new session without verifying it:
</p>
<p>
(device_id)
</p>
<p>
Verify your other session using one of the options below.
</p>
</div>
<div
class="mx_Dialog_buttons"
>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary_outline"
role="button"
tabindex="0"
>
Interactively verify by emoji
</div>
<div
class="mx_AccessibleButton mx_AccessibleButton_hasKind mx_AccessibleButton_kind_primary"
role="button"
tabindex="0"
>
Done
</div>
</div>
<div
aria-label="Close dialog"
class="mx_AccessibleButton mx_Dialog_cancelButton"
role="button"
tabindex="0"
/>
</div>
<div
data-focus-guard="true"
style="width: 1px; height: 0px; padding: 0px; overflow: hidden; position: fixed; top: 1px; left: 1px;"
tabindex="0"
/>
</DocumentFragment>
`;

View File

@@ -9,7 +9,8 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { screen, fireEvent, render, waitFor, act } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Crypto, IMegolmSessionData } from "matrix-js-sdk/src/matrix";
import { IMegolmSessionData } from "matrix-js-sdk/src/matrix";
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
import * as MegolmExportEncryption from "../../../../../../src/utils/MegolmExportEncryption";
import ExportE2eKeysDialog from "../../../../../../src/async-components/views/dialogs/security/ExportE2eKeysDialog";
@@ -62,7 +63,7 @@ describe("ExportE2eKeysDialog", () => {
cli.getCrypto = () => {
return {
exportRoomKeysAsJson,
} as unknown as Crypto.CryptoApi;
} as unknown as CryptoApi;
};
// Mock the result of encrypting the sessions. If we don't do this, the

View File

@@ -9,7 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { fireEvent, render, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { Crypto } from "matrix-js-sdk/src/matrix";
import { CryptoApi } from "matrix-js-sdk/src/crypto-api";
import ImportE2eKeysDialog from "../../../../../../src/async-components/views/dialogs/security/ImportE2eKeysDialog";
import * as MegolmExportEncryption from "../../../../../../src/utils/MegolmExportEncryption";
@@ -67,7 +67,7 @@ describe("ImportE2eKeysDialog", () => {
cli.getCrypto = () => {
return {
importRoomKeysAsJson,
} as unknown as Crypto.CryptoApi;
} as unknown as CryptoApi;
};
// Mock the result of decrypting the sessions, to avoid needing to

View File

@@ -86,7 +86,7 @@ describe("<Pill>", () => {
room: room1Id,
msg: "Room 1 Message",
});
room1.addLiveEvents([room1Message]);
room1.addLiveEvents([room1Message], { addToState: true });
room2 = new Room(room2Id, client, user1Id);
room2.currentState.setStateEvents([mkRoomMemberJoinEvent(user2Id, room2Id)]);

View File

@@ -41,7 +41,7 @@ describe("<RoomTopic/>", () => {
ts: 123,
event: true,
});
room.addLiveEvents([topicEvent]);
room.addLiveEvents([topicEvent], { addToState: true });
return room;
}

View File

@@ -20,7 +20,8 @@ import {
import { MediaEventHelper } from "../../../../../src/utils/MediaEventHelper";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import MFileBody from "../../../../../src/components/views/messages/MFileBody.tsx";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext.ts";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("matrix-encrypt-attachment", () => ({
decryptAttachment: jest.fn(),
@@ -72,14 +73,14 @@ describe("<MFileBody/>", () => {
it("should show a download button in file rendering type", async () => {
const { container, getByRole } = render(
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.File } as any}>
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.File } as any)}>
<MFileBody
{...props}
mxEvent={mediaEvent}
mediaEventHelper={new MediaEventHelper(mediaEvent)}
showGenericPlaceholder={false}
/>
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
expect(getByRole("link", { name: "Download" })).toBeInTheDocument();

View File

@@ -35,6 +35,7 @@ import dispatcher from "../../../../../src/dispatcher/dispatcher";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { Action } from "../../../../../src/dispatcher/actions";
import PinningUtils from "../../../../../src/utils/PinningUtils";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/dispatcher/dispatcher");
@@ -117,9 +118,9 @@ describe("<MessageActionBar />", () => {
} as unknown as IRoomState;
const getComponent = (props = {}, roomContext: Partial<IRoomState> = {}) =>
render(
<RoomContext.Provider value={{ ...defaultRoomContext, ...roomContext }}>
<ScopedRoomContextProvider {...defaultRoomContext} {...roomContext}>
<MessageActionBar {...defaultProps} {...props} />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
beforeEach(() => {

View File

@@ -14,7 +14,6 @@ import fs from "fs";
import path from "path";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { VoiceBroadcastInfoEventType, VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { mkEvent, mkRoom, stubClient } from "../../../../test-utils";
import MessageEvent from "../../../../../src/components/views/messages/MessageEvent";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
@@ -24,10 +23,6 @@ jest.mock("../../../../../src/components/views/messages/UnknownBody", () => ({
default: () => <div data-testid="unknown-body" />,
}));
jest.mock("../../../../../src/voice-broadcast/components/VoiceBroadcastBody", () => ({
VoiceBroadcastBody: () => <div data-testid="voice-broadcast-body" />,
}));
jest.mock("../../../../../src/components/views/messages/MImageBody", () => ({
__esModule: true,
default: () => <div data-testid="image-body" />,
@@ -81,27 +76,6 @@ describe("MessageEvent", () => {
jest.spyOn(SettingsStore, "unwatchSetting").mockImplementation(jest.fn());
});
describe("when a voice broadcast start event occurs", () => {
let result: RenderResult;
beforeEach(() => {
event = mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
user: client.getUserId()!,
room: room.roomId,
content: {
state: VoiceBroadcastInfoState.Started,
},
});
result = renderMessageEvent();
});
it("should render a VoiceBroadcast component", () => {
result.getByTestId("voice-broadcast-body");
});
});
describe("when an image with a caption is sent", () => {
let result: RenderResult;

View File

@@ -20,9 +20,9 @@ import {
} from "../../../../../src/components/views/messages/RoomPredecessorTile";
import { stubClient, upsertRoomStateEvents } from "../../../../test-utils/test-utils";
import { Action } from "../../../../../src/dispatcher/actions";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { filterConsole, getRoomContext } from "../../../../test-utils";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/dispatcher/dispatcher");
@@ -99,9 +99,9 @@ describe("<RoomPredecessorTile />", () => {
expect(createEvent).toBeTruthy();
return render(
<RoomContext.Provider value={getRoomContext(room, {})}>
<ScopedRoomContextProvider {...getRoomContext(room, {})}>
<RoomPredecessorTile mxEvent={createEvent} />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
}

View File

@@ -15,7 +15,7 @@ import userEvent from "@testing-library/user-event";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import RoomSummaryCard from "../../../../../src/components/views/right_panel/RoomSummaryCard";
import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog";
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
import ExportDialog from "../../../../../src/components/views/dialogs/ExportDialog";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
@@ -30,7 +30,8 @@ import { _t } from "../../../../../src/languageHandler";
import { tagRoom } from "../../../../../src/utils/room/tagRoom";
import { DefaultTagID } from "../../../../../src/stores/room-list/models";
import { Action } from "../../../../../src/dispatcher/actions";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/utils/room/tagRoom");
@@ -172,14 +173,14 @@ describe("<RoomSummaryCard />", () => {
const onSearchChange = jest.fn();
const { rerender } = render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.Search } as any}>
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.Search } as any)}>
<RoomSummaryCard
room={room}
permalinkCreator={new RoomPermalinkCreator(room)}
onSearchChange={onSearchChange}
focusRoomSearch={true}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
@@ -188,13 +189,13 @@ describe("<RoomSummaryCard />", () => {
rerender(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={{ timelineRenderingType: TimelineRenderingType.Room } as any}>
<ScopedRoomContextProvider {...({ timelineRenderingType: TimelineRenderingType.Room } as any)}>
<RoomSummaryCard
room={room}
permalinkCreator={new RoomPermalinkCreator(room)}
onSearchChange={onSearchChange}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
expect(screen.getByPlaceholderText("Search messages…")).toHaveValue("");
@@ -254,10 +255,7 @@ describe("<RoomSummaryCard />", () => {
fireEvent.click(getByText("People"));
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith(
{ phase: RightPanelPhases.RoomMemberList },
true,
);
expect(RightPanelStore.instance.pushCard).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList }, true);
});
it("opens room threads list on button click", () => {

View File

@@ -49,7 +49,7 @@ import ErrorDialog from "../../../../../src/components/views/dialogs/ErrorDialog
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
import { UIComponent } from "../../../../../src/settings/UIFeature";
import { Action } from "../../../../../src/dispatcher/actions";
import ShareDialog from "../../../../../src/components/views/dialogs/ShareDialog";
import { ShareDialog } from "../../../../../src/components/views/dialogs/ShareDialog";
import BulkRedactDialog from "../../../../../src/components/views/dialogs/BulkRedactDialog";
jest.mock("../../../../../src/utils/direct-messages", () => ({
@@ -188,7 +188,7 @@ describe("<UserInfo />", () => {
const defaultProps = {
user: defaultUser,
// idk what is wrong with this type
phase: RightPanelPhases.RoomMemberInfo as RightPanelPhases.RoomMemberInfo,
phase: RightPanelPhases.MemberInfo as RightPanelPhases.MemberInfo,
onClose: jest.fn(),
};
@@ -455,7 +455,7 @@ describe("<UserInfo />", () => {
mockCrypto.getUserVerificationStatus.mockResolvedValue(new UserVerificationStatus(false, false, false));
const { container } = renderComponent({
phase: RightPanelPhases.SpaceMemberInfo,
phase: RightPanelPhases.MemberInfo,
verificationRequest,
room: mockRoom,
});
@@ -649,7 +649,7 @@ describe("<UserInfo />", () => {
mockClient.getDomain.mockReturnValue("example.com");
const { container } = renderComponent({
phase: RightPanelPhases.RoomMemberInfo,
phase: RightPanelPhases.MemberInfo,
room: mockRoom,
});

View File

@@ -88,7 +88,7 @@ exports[`<UserInfo /> with crypto enabled renders <BasicUserInfo /> 1`] = `
</p>
</div>
<button
aria-labelledby=":r6s:"
aria-labelledby=":r74:"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
data-testid="base-card-close-button"
role="button"
@@ -402,7 +402,7 @@ exports[`<UserInfo /> with crypto enabled should render a deactivate button for
</p>
</div>
<button
aria-labelledby=":r92:"
aria-labelledby=":r9a:"
class="_icon-button_bh2qc_17 _subtle-bg_bh2qc_38"
data-testid="base-card-close-button"
role="button"

View File

@@ -27,12 +27,12 @@ import {
import DocumentOffset from "../../../../../src/editor/offset";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import EditorStateTransfer from "../../../../../src/utils/EditorStateTransfer";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import Autocompleter, { IProviderCompletions } from "../../../../../src/autocomplete/Autocompleter";
import NotifProvider from "../../../../../src/autocomplete/NotifProvider";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("<EditMessageComposer/>", () => {
const userId = "@alice:server.org";
@@ -79,7 +79,7 @@ describe("<EditMessageComposer/>", () => {
render(<EditMessageComposerWithMatrixClient editState={editState} />, {
wrapper: ({ children }) => (
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={roomContext}>{children}</RoomContext.Provider>
<ScopedRoomContextProvider {...roomContext}>{children}</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
),
});
@@ -128,7 +128,7 @@ describe("<EditMessageComposer/>", () => {
const expectedBody = {
...editedEvent.getContent(),
"body": " * original message + edit",
"body": "* original message + edit",
"m.new_content": {
"body": "original message + edit",
"msgtype": "m.text",
@@ -160,7 +160,7 @@ describe("<EditMessageComposer/>", () => {
const content = createEditContent(model, editedEvent);
expect(content).toEqual({
"body": " * hello world",
"body": "* hello world",
"msgtype": "m.text",
"m.new_content": {
"body": "hello world",
@@ -183,10 +183,10 @@ describe("<EditMessageComposer/>", () => {
const content = createEditContent(model, editedEvent);
expect(content).toEqual({
"body": " * hello *world*",
"body": "* hello *world*",
"msgtype": "m.text",
"format": "org.matrix.custom.html",
"formatted_body": " * hello <em>world</em>",
"formatted_body": "* hello <em>world</em>",
"m.new_content": {
"body": "hello *world*",
"msgtype": "m.text",
@@ -210,10 +210,10 @@ describe("<EditMessageComposer/>", () => {
const content = createEditContent(model, editedEvent);
expect(content).toEqual({
"body": " * blinks __quickly__",
"body": "* blinks __quickly__",
"msgtype": "m.emote",
"format": "org.matrix.custom.html",
"formatted_body": " * blinks <strong>quickly</strong>",
"formatted_body": "* blinks <strong>quickly</strong>",
"m.new_content": {
"body": "blinks __quickly__",
"msgtype": "m.emote",
@@ -238,7 +238,7 @@ describe("<EditMessageComposer/>", () => {
const content = createEditContent(model, editedEvent);
expect(content).toEqual({
"body": " * ✨sparkles✨",
"body": "* ✨sparkles✨",
"msgtype": "m.emote",
"m.new_content": {
"body": "✨sparkles✨",
@@ -264,7 +264,7 @@ describe("<EditMessageComposer/>", () => {
// TODO Edits do not properly strip the double slash used to skip
// command processing.
expect(content).toEqual({
"body": " * //dev/null is my favourite place",
"body": "* //dev/null is my favourite place",
"msgtype": "m.text",
"m.new_content": {
"body": "//dev/null is my favourite place",

View File

@@ -30,7 +30,7 @@ import { mkEncryptedMatrixEvent } from "matrix-js-sdk/src/testing";
import EventTile, { EventTileProps } from "../../../../../src/components/views/rooms/EventTile";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import { filterConsole, flushPromises, getRoomContext, mkEvent, mkMessage, stubClient } from "../../../../test-utils";
import { mkThread } from "../../../../test-utils/threads";
@@ -40,6 +40,7 @@ import { Action } from "../../../../../src/dispatcher/actions";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import PinningUtils from "../../../../../src/utils/PinningUtils";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("EventTile", () => {
const ROOM_ID = "!roomId:example.org";
@@ -56,13 +57,13 @@ describe("EventTile", () => {
}) {
return (
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={props.roomContext}>
<ScopedRoomContextProvider {...props.roomContext}>
<EventTile
mxEvent={mxEvent}
replacingEventId={mxEvent.replacingEventId()}
{...(props.eventTilePropertyOverrides ?? {})}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
}
@@ -70,9 +71,11 @@ describe("EventTile", () => {
function getComponent(
overrides: Partial<EventTileProps> = {},
renderingType: TimelineRenderingType = TimelineRenderingType.Room,
roomContext: Partial<IRoomState> = {},
) {
const context = getRoomContext(room, {
timelineRenderingType: renderingType,
...roomContext,
});
return render(<WrappedEventTile roomContext={context} eventTilePropertyOverrides={overrides} />);
}
@@ -301,6 +304,8 @@ describe("EventTile", () => {
[EventShieldReason.UNKNOWN_DEVICE, "unknown or deleted device"],
[EventShieldReason.AUTHENTICITY_NOT_GUARANTEED, "can't be guaranteed"],
[EventShieldReason.MISMATCHED_SENDER_KEY, "Encrypted by an unverified session"],
[EventShieldReason.SENT_IN_CLEAR, "Not encrypted"],
[EventShieldReason.VERIFICATION_VIOLATION, "Sender's verified identity has changed"],
])("shows the correct reason code for %i (%s)", async (reasonCode: EventShieldReason, expectedText: string) => {
mxEvent = await mkEncryptedMatrixEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
@@ -434,8 +439,6 @@ describe("EventTile", () => {
});
it("should update the warning when the event is replaced with an unencrypted one", async () => {
jest.spyOn(client, "isRoomEncrypted").mockReturnValue(true);
// we start out with an event from the trusted device
mxEvent = await mkEncryptedMatrixEvent({
plainContent: { msgtype: "m.text", body: "msg1" },
@@ -449,7 +452,7 @@ describe("EventTile", () => {
shieldReason: null,
} as EventEncryptionInfo);
const roomContext = getRoomContext(room, {});
const roomContext = getRoomContext(room, { isRoomEncrypted: true });
const { container, rerender } = render(<WrappedEventTile roomContext={roomContext} />);
await flushPromises();
@@ -578,4 +581,28 @@ describe("EventTile", () => {
});
});
});
it("should display the not encrypted status for an unencrypted event when the room becomes encrypted", async () => {
jest.spyOn(client.getCrypto()!, "getEncryptionInfoForEvent").mockResolvedValue({
shieldColour: EventShieldColour.NONE,
shieldReason: null,
});
const { rerender } = getComponent();
await flushPromises();
// The room and the event are unencrypted, the tile should not show the not encrypted status
expect(screen.queryByText("Not encrypted")).toBeNull();
// The room is now encrypted
rerender(
<WrappedEventTile
roomContext={getRoomContext(room, {
isRoomEncrypted: true,
})}
/>,
);
// The event tile should now show the not encrypted status
await waitFor(() => expect(screen.getByText("Not encrypted")).toBeInTheDocument());
});
});

View File

@@ -7,7 +7,7 @@ Please see LICENSE files in the repository root for full details.
*/
import * as React from "react";
import { EventType, MatrixEvent, Room, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix";
import { EventType, MatrixEvent, RoomMember, THREAD_RELATION_TYPE } from "matrix-js-sdk/src/matrix";
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
@@ -19,17 +19,14 @@ import {
mkStubRoom,
mockPlatformPeg,
stubClient,
waitEnoughCyclesForModal,
} from "../../../../test-utils";
import MessageComposer from "../../../../../src/components/views/rooms/MessageComposer";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import ResizeNotifier from "../../../../../src/utils/ResizeNotifier";
import { RoomPermalinkCreator } from "../../../../../src/utils/permalinks/Permalinks";
import { LocalRoom } from "../../../../../src/models/LocalRoom";
import { Features } from "../../../../../src/settings/Settings";
import SettingsStore from "../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../src/settings/SettingLevel";
import dis from "../../../../../src/dispatcher/dispatcher";
@@ -37,9 +34,7 @@ import { E2EStatus } from "../../../../../src/utils/ShieldUtils";
import { addTextToComposerRTL } from "../../../../test-utils/composer";
import UIStore, { UI_EVENTS } from "../../../../../src/stores/UIStore";
import { Action } from "../../../../../src/dispatcher/actions";
import { VoiceBroadcastInfoState, VoiceBroadcastRecording } from "../../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { SdkContextClass } from "../../../../../src/contexts/SDKContext";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
const openStickerPicker = async (): Promise<void> => {
await userEvent.click(screen.getByLabelText("More options"));
@@ -51,15 +46,6 @@ const startVoiceMessage = async (): Promise<void> => {
await userEvent.click(screen.getByLabelText("Voice Message"));
};
const setCurrentBroadcastRecording = (room: Room, state: VoiceBroadcastInfoState): void => {
const recording = new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(room.roomId, state, "@user:example.com", "ABC123"),
MatrixClientPeg.safeGet(),
state,
);
act(() => SdkContextClass.instance.voiceBroadcastRecordingsStore.setCurrent(recording));
};
const expectVoiceMessageRecordingTriggered = (): void => {
// Checking for the voice message dialog text, if no mic can be found.
// By this we know at least that starting a voice message was triggered.
@@ -78,14 +64,11 @@ describe("MessageComposer", () => {
await clearAllModals();
jest.useRealTimers();
SdkContextClass.instance.voiceBroadcastRecordingsStore.clearCurrent();
// restore settings
act(() => {
[
"MessageComposerInput.showStickersButton",
"MessageComposerInput.showPollsButton",
Features.VoiceBroadcast,
"feature_wysiwyg_composer",
].forEach((setting: string): void => {
SettingsStore.setValue(setting, null, SettingLevel.DEVICE, SettingsStore.getDefaultValue(setting));
@@ -212,10 +195,6 @@ describe("MessageComposer", () => {
setting: "MessageComposerInput.showPollsButton",
buttonLabel: "Poll",
},
{
setting: Features.VoiceBroadcast,
buttonLabel: "Voice broadcast",
},
].forEach(({ setting, buttonLabel }) => {
[true, false].forEach((value: boolean) => {
describe(`when ${setting} = ${value}`, () => {
@@ -437,34 +416,6 @@ describe("MessageComposer", () => {
expectVoiceMessageRecordingTriggered();
});
});
describe("when recording a voice broadcast and trying to start a voice message", () => {
beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Started);
wrapAndRender({ room });
await startVoiceMessage();
await waitEnoughCyclesForModal();
});
it("should not start a voice message and display the info dialog", async () => {
expect(screen.queryByLabelText("Stop recording")).not.toBeInTheDocument();
expect(screen.getByText("Can't start voice message")).toBeInTheDocument();
});
});
describe("when there is a stopped voice broadcast recording and trying to start a voice message", () => {
beforeEach(async () => {
setCurrentBroadcastRecording(room, VoiceBroadcastInfoState.Stopped);
wrapAndRender({ room });
await startVoiceMessage();
await waitEnoughCyclesForModal();
});
it("should try to start a voice message and should not display the info dialog", async () => {
expect(screen.queryByText("Can't start voice message")).not.toBeInTheDocument();
expectVoiceMessageRecordingTriggered();
});
});
});
describe("for a LocalRoom", () => {
@@ -512,9 +463,9 @@ function wrapAndRender(
const getRawComponent = (props = {}, context = roomContext, client = mockClient) => (
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={context}>
<ScopedRoomContextProvider {...context}>
<MessageComposer {...defaultProps} {...props} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
return {

View File

@@ -10,11 +10,11 @@ import React from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../src/contexts/RoomContext";
import { createTestClient, getRoomContext, mkStubRoom } from "../../../../test-utils";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { MatrixClientPeg } from "../../../../../src/MatrixClientPeg";
import MessageComposerButtons from "../../../../../src/components/views/rooms/MessageComposerButtons";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
describe("MessageComposerButtons", () => {
// @ts-ignore - we're deliberately not implementing the whole interface here, but
@@ -54,7 +54,7 @@ describe("MessageComposerButtons", () => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>{component}</RoomContext.Provider>
<ScopedRoomContextProvider {...defaultRoomContext}>{component}</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
}
@@ -168,27 +168,4 @@ describe("MessageComposerButtons", () => {
]);
});
});
describe("with showVoiceBroadcastButton = true", () => {
it("should render the »Voice broadcast« button", () => {
wrapAndRender(
<MessageComposerButtons
{...mockProps}
isMenuOpen={true}
showLocationButton={true}
showPollsButton={true}
showStickersButton={true}
showVoiceBroadcastButton={true}
/>,
false,
);
expect(getButtonLabels()).toEqual([
"Emoji",
"Attachment",
"More options",
["Sticker", "Voice Message", "Voice broadcast", "Poll", "Location"],
]);
});
});
});

View File

@@ -13,19 +13,19 @@ import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { LocalRoom } from "../../../../../src/models/LocalRoom";
import { filterConsole, mkRoomMemberJoinEvent, mkThirdPartyInviteEvent, stubClient } from "../../../../test-utils";
import RoomContext from "../../../../../src/contexts/RoomContext";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import NewRoomIntro from "../../../../../src/components/views/rooms/NewRoomIntro";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import { DirectoryMember } from "../../../../../src/utils/direct-messages";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
const renderNewRoomIntro = (client: MatrixClient, room: Room | LocalRoom) => {
render(
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={{ room, roomId: room.roomId } as unknown as IRoomState}>
<ScopedRoomContextProvider {...({ room, roomId: room.roomId } as unknown as IRoomState)}>
<NewRoomIntro />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};

View File

@@ -165,7 +165,7 @@ describe("UnreadNotificationBadge", () => {
},
ts: 5,
});
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: true });
const { container } = render(getComponent(THREAD_ID));
expect(container.querySelector(".mx_NotificationBadge_dot")).toBeTruthy();

View File

@@ -224,7 +224,7 @@ describe("<PinnedMessageBanner />", () => {
// The Right panel is opened on another card
jest.spyOn(RightPanelStore.instance, "isOpenForRoom").mockReturnValue(true);
jest.spyOn(RightPanelStore.instance, "currentCard", "get").mockReturnValue({
phase: RightPanelPhases.RoomMemberList,
phase: RightPanelPhases.MemberList,
});
renderBanner();

View File

@@ -158,7 +158,7 @@ describe("RoomHeader", () => {
fireEvent.click(facePile);
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.RoomMemberList });
expect(setCardSpy).toHaveBeenCalledWith({ phase: RightPanelPhases.MemberList });
});
it("has room info icon that opens the room info panel", async () => {
@@ -589,7 +589,7 @@ describe("RoomHeader", () => {
state_key: "",
room_id: room.roomId,
});
room.addLiveEvents([joinRuleEvent]);
room.addLiveEvents([joinRuleEvent], { addToState: true });
render(<RoomHeader room={room} />, getWrapper());

View File

@@ -19,7 +19,7 @@ import {
} from "../../../../../../src/components/views/rooms/RoomHeader/CallGuestLinkButton";
import Modal from "../../../../../../src/Modal";
import SdkConfig from "../../../../../../src/SdkConfig";
import ShareDialog from "../../../../../../src/components/views/dialogs/ShareDialog";
import { ShareDialog } from "../../../../../../src/components/views/dialogs/ShareDialog";
import { _t } from "../../../../../../src/languageHandler";
import SettingsStore from "../../../../../../src/settings/SettingsStore";

View File

@@ -9,14 +9,7 @@ Please see LICENSE files in the repository root for full details.
import React from "react";
import { render, screen, act, RenderResult } from "jest-matrix-react";
import { mocked, Mocked } from "jest-mock";
import {
MatrixClient,
PendingEventOrdering,
Room,
MatrixEvent,
RoomStateEvent,
Thread,
} from "matrix-js-sdk/src/matrix";
import { MatrixClient, PendingEventOrdering, Room, RoomStateEvent, Thread } from "matrix-js-sdk/src/matrix";
import { KnownMembership } from "matrix-js-sdk/src/types";
import { Widget } from "matrix-widget-api";
@@ -40,8 +33,6 @@ import DMRoomMap from "../../../../../src/utils/DMRoomMap";
import PlatformPeg from "../../../../../src/PlatformPeg";
import BasePlatform from "../../../../../src/BasePlatform";
import { WidgetMessagingStore } from "../../../../../src/stores/widgets/WidgetMessagingStore";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
import { TestSdkContext } from "../../../TestSdkContext";
import { SDKContext } from "../../../../../src/contexts/SDKContext";
import { shouldShowComponent } from "../../../../../src/customisations/helpers/UIComponents";
@@ -61,20 +52,6 @@ describe("RoomTile", () => {
} as unknown as BasePlatform);
useMockedCalls();
const setUpVoiceBroadcast = async (state: VoiceBroadcastInfoState): Promise<void> => {
voiceBroadcastInfoEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
state,
client.getSafeUserId(),
client.getDeviceId()!,
);
await act(async () => {
room.currentState.setStateEvents([voiceBroadcastInfoEvent]);
await flushPromises();
});
};
const renderRoomTile = (): RenderResult => {
return render(
<SDKContext.Provider value={sdkContext}>
@@ -89,7 +66,6 @@ describe("RoomTile", () => {
};
let client: Mocked<MatrixClient>;
let voiceBroadcastInfoEvent: MatrixEvent;
let room: Room;
let sdkContext: TestSdkContext;
let showMessagePreview = false;
@@ -303,49 +279,6 @@ describe("RoomTile", () => {
});
expect(screen.queryByLabelText(/participant/)).toBe(null);
});
describe("and a live broadcast starts", () => {
beforeEach(async () => {
renderRoomTile();
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
});
it("should still render the call subtitle", () => {
expect(screen.queryByText("Video")).toBeInTheDocument();
expect(screen.queryByText("Live")).not.toBeInTheDocument();
});
});
});
describe("when a live voice broadcast starts", () => {
beforeEach(async () => {
renderRoomTile();
await setUpVoiceBroadcast(VoiceBroadcastInfoState.Started);
});
it("should render the »Live« subtitle", () => {
expect(screen.queryByText("Live")).toBeInTheDocument();
});
describe("and the broadcast stops", () => {
beforeEach(async () => {
const stopEvent = mkVoiceBroadcastInfoStateEvent(
room.roomId,
VoiceBroadcastInfoState.Stopped,
client.getSafeUserId(),
client.getDeviceId()!,
voiceBroadcastInfoEvent,
);
await act(async () => {
room.currentState.setStateEvents([stopEvent]);
await flushPromises();
});
});
it("should not render the »Live« subtitle", () => {
expect(screen.queryByText("Live")).not.toBeInTheDocument();
});
});
});
});

View File

@@ -18,7 +18,7 @@ import SendMessageComposer, {
isQuickReaction,
} from "../../../../../src/components/views/rooms/SendMessageComposer";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import EditorModel from "../../../../../src/editor/model";
import { createPartCreator } from "../../../editor/mock";
import { createTestClient, mkEvent, mkStubRoom, stubClient } from "../../../../test-utils";
@@ -30,6 +30,7 @@ import { IRoomState, MainSplitContentType } from "../../../../../src/components/
import { mockPlatformPeg } from "../../../../test-utils/platform";
import { doMaybeLocalRoomAction } from "../../../../../src/utils/local-room";
import { addTextToComposer } from "../../../../test-utils/composer";
import { ScopedRoomContextProvider } from "../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../src/utils/local-room", () => ({
doMaybeLocalRoomAction: jest.fn(),
@@ -77,6 +78,7 @@ describe("<SendMessageComposer/>", () => {
canAskToJoin: false,
promptAskToJoin: false,
viewRoomOpts: { buttons: [] },
isRoomEncrypted: false,
};
describe("createMessageContent", () => {
it("sends plaintext messages correctly", () => {
@@ -364,9 +366,9 @@ describe("<SendMessageComposer/>", () => {
};
const getRawComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => (
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={roomContext}>
<ScopedRoomContextProvider {...roomContext}>
<SendMessageComposer {...defaultProps} {...props} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>
);
const getComponent = (props = {}, roomContext = defaultRoomContext, client = mockClient) => {

View File

@@ -47,7 +47,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
style="--mx-flex-display: flex; --mx-flex-direction: row; --mx-flex-align: center; --mx-flex-justify: start; --mx-flex-gap: var(--cpd-space-2x);"
>
<button
aria-labelledby=":r154:"
aria-labelledby=":r16c:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -73,7 +73,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
<button
aria-disabled="true"
aria-label="There's no one here to call"
aria-labelledby=":r159:"
aria-labelledby=":r16h:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -98,7 +98,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button>
<button
aria-label="Room info"
aria-labelledby=":r15e:"
aria-labelledby=":r16m:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"
@@ -123,7 +123,7 @@ exports[`RoomHeader dm does not show the face pile for DMs 1`] = `
</button>
<button
aria-label="Threads"
aria-labelledby=":r15j:"
aria-labelledby=":r16r:"
class="_icon-button_bh2qc_17"
role="button"
style="--cpd-icon-button-size: 32px;"

View File

@@ -11,7 +11,6 @@ import React from "react";
import { fireEvent, render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../src/contexts/RoomContext";
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../../src/dispatcher/actions";
import { flushPromises, mkEvent } from "../../../../../test-utils";
@@ -23,6 +22,7 @@ import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispa
import { ActionPayload } from "../../../../../../src/dispatcher/payloads";
import * as EmojiButton from "../../../../../../src/components/views/rooms/EmojiButton";
import { createMocks } from "./utils";
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx";
describe("EditWysiwygComposer", () => {
afterEach(() => {
@@ -39,9 +39,9 @@ describe("EditWysiwygComposer", () => {
) => {
return render(
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={roomContext}>
<ScopedRoomContextProvider {...roomContext}>
<EditWysiwygComposer disabled={disabled} editorStateTransfer={_editorStateTransfer} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};
@@ -64,9 +64,9 @@ describe("EditWysiwygComposer", () => {
rerender(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={{ ...defaultRoomContext, room: undefined }}>
<ScopedRoomContextProvider {...defaultRoomContext} room={undefined}>
<EditWysiwygComposer disabled={false} editorStateTransfer={editorStateTransfer} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
@@ -196,9 +196,9 @@ describe("EditWysiwygComposer", () => {
// Then
screen.getByText("Save").click();
const expectedContent = {
"body": ` * foo bar`,
"body": `* foo bar`,
"format": "org.matrix.custom.html",
"formatted_body": ` * foo bar`,
"formatted_body": `* foo bar`,
"m.new_content": {
body: "foo bar",
format: "org.matrix.custom.html",
@@ -275,10 +275,10 @@ describe("EditWysiwygComposer", () => {
);
render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>
<ScopedRoomContextProvider {...defaultRoomContext}>
<EditWysiwygComposer editorStateTransfer={editorStateTransfer} />
<Emoji menuPosition={{ chevronFace: ChevronFace.Top }} />
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
// Same behavior as in RoomView.tsx

View File

@@ -11,7 +11,6 @@ import React from "react";
import { act, fireEvent, render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../src/contexts/RoomContext";
import defaultDispatcher from "../../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../../src/dispatcher/actions";
import { flushPromises } from "../../../../../test-utils";
@@ -20,6 +19,7 @@ import { aboveLeftOf } from "../../../../../../src/components/structures/Context
import { ComposerInsertPayload, ComposerType } from "../../../../../../src/dispatcher/payloads/ComposerInsertPayload";
import { setSelection } from "../../../../../../src/components/views/rooms/wysiwyg_composer/utils/selection";
import { createMocks } from "./utils";
import { ScopedRoomContextProvider } from "../../../../../../src/contexts/ScopedRoomContext.tsx";
jest.mock("../../../../../../src/components/views/rooms/EmojiButton", () => ({
EmojiButton: ({ addEmoji }: { addEmoji: (emoji: string) => void }) => {
@@ -66,7 +66,7 @@ describe("SendWysiwygComposer", () => {
) => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>
<ScopedRoomContextProvider {...defaultRoomContext}>
<SendWysiwygComposer
onChange={onChange}
onSend={onSend}
@@ -75,7 +75,7 @@ describe("SendWysiwygComposer", () => {
menuPosition={aboveLeftOf({ top: 0, bottom: 0, right: 0 })}
placeholder={placeholder}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};

View File

@@ -14,7 +14,7 @@ import { PlainTextComposer } from "../../../../../../../src/components/views/roo
import * as mockUseSettingsHook from "../../../../../../../src/hooks/useSettings";
import * as mockKeyboard from "../../../../../../../src/Keyboard";
import { createMocks } from "../utils";
import RoomContext from "../../../../../../../src/contexts/RoomContext";
import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx";
describe("PlainTextComposer", () => {
const customRender = (
@@ -275,9 +275,9 @@ describe("PlainTextComposer", () => {
const { defaultRoomContext } = createMocks();
render(
<RoomContext.Provider value={defaultRoomContext}>
<ScopedRoomContextProvider {...defaultRoomContext}>
<PlainTextComposer onChange={jest.fn()} onSend={jest.fn()} disabled={false} initialContent="" />
</RoomContext.Provider>,
</ScopedRoomContextProvider>,
);
expect(screen.getByTestId("autocomplete-wrapper")).toBeInTheDocument();

View File

@@ -11,12 +11,12 @@ import React, { createRef } from "react";
import { render, screen, waitFor } from "jest-matrix-react";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../../src/contexts/RoomContext";
import { WysiwygAutocomplete } from "../../../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygAutocomplete";
import { getRoomContext, mkStubRoom, stubClient } from "../../../../../../test-utils";
import Autocomplete from "../../../../../../../src/components/views/rooms/Autocomplete";
import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplete/Autocompleter";
import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider";
import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx";
const mockCompletion: ICompletion[] = [
{
@@ -71,7 +71,7 @@ describe("WysiwygAutocomplete", () => {
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={mockRoomContext}>
<ScopedRoomContextProvider {...mockRoomContext}>
<WysiwygAutocomplete
ref={autocompleteRef}
suggestion={null}
@@ -80,7 +80,7 @@ describe("WysiwygAutocomplete", () => {
handleAtRoomMention={mockHandleAtRoomMention}
{...props}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};

View File

@@ -18,7 +18,6 @@ import defaultDispatcher from "../../../../../../../src/dispatcher/dispatcher";
import * as EventUtils from "../../../../../../../src/utils/EventUtils";
import { Action } from "../../../../../../../src/dispatcher/actions";
import MatrixClientContext from "../../../../../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../../../../../src/contexts/RoomContext";
import {
ComposerContext,
getDefaultContextValue,
@@ -32,20 +31,21 @@ import Autocompleter, { ICompletion } from "../../../../../../../src/autocomplet
import AutocompleteProvider from "../../../../../../../src/autocomplete/AutocompleteProvider";
import * as Permalinks from "../../../../../../../src/utils/permalinks/Permalinks";
import { PermalinkParts } from "../../../../../../../src/utils/permalinks/PermalinkConstructor";
import { ScopedRoomContextProvider } from "../../../../../../../src/contexts/ScopedRoomContext.tsx";
describe("WysiwygComposer", () => {
const customRender = (onChange = jest.fn(), onSend = jest.fn(), disabled = false, initialContent?: string) => {
const { mockClient, defaultRoomContext } = createMocks();
return render(
<MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={defaultRoomContext}>
<ScopedRoomContextProvider {...defaultRoomContext}>
<WysiwygComposer
onChange={onChange}
onSend={onSend}
disabled={disabled}
initialContent={initialContent}
/>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};
@@ -523,7 +523,7 @@ describe("WysiwygComposer", () => {
) => {
return render(
<MatrixClientContext.Provider value={client}>
<RoomContext.Provider value={roomContext}>
<ScopedRoomContextProvider {...roomContext}>
<ComposerContext.Provider
value={getDefaultContextValue({ editorStateTransfer: _editorStateTransfer })}
>
@@ -537,7 +537,7 @@ describe("WysiwygComposer", () => {
}
/>
</ComposerContext.Provider>
</RoomContext.Provider>
</ScopedRoomContextProvider>
</MatrixClientContext.Provider>,
);
};

View File

@@ -88,9 +88,9 @@ describe("createMessageContent", () => {
// Then
expect(content).toEqual({
"body": " * *__hello__ world*",
"body": "* *__hello__ world*",
"format": "org.matrix.custom.html",
"formatted_body": ` * ${message}`,
"formatted_body": `* ${message}`,
"msgtype": "m.text",
"m.new_content": {
body: "*__hello__ world*",

View File

@@ -418,8 +418,8 @@ describe("message", () => {
// Then
const { msgtype, format } = mockEvent.getContent();
const expectedContent = {
"body": ` * ${newMessage}`,
"formatted_body": ` * ${newMessage}`,
"body": `* ${newMessage}`,
"formatted_body": `* ${newMessage}`,
"m.new_content": {
body: "Replying to this new content",
format: "org.matrix.custom.html",

View File

@@ -915,7 +915,7 @@ describe("<Notifications />", () => {
user: "@alice:example.org",
ts: 1,
});
await room.addLiveEvents([message]);
await room.addLiveEvents([message], { addToState: true });
const { container } = await getComponentAndWait();
const clearNotificationEl = getByTestId(container, "clear-notifications");

View File

@@ -56,7 +56,6 @@ describe("<LoginWithQRSection />", () => {
const defaultProps = {
onShowQr: () => {},
versions: makeVersions({ "org.matrix.msc4108": true }),
wellKnown: {},
};
const getComponent = (props = {}) => <LoginWithQRSection {...defaultProps} {...props} />;

View File

@@ -716,7 +716,7 @@ describe("<Notifications />", () => {
user: "@alice:example.org",
ts: 1,
});
room.addLiveEvents([message]);
room.addLiveEvents([message], { addToState: true });
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
const user = userEvent.setup();

View File

@@ -17,7 +17,6 @@ import userEvent from "@testing-library/user-event";
import RolesRoomSettingsTab from "../../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
import { mkStubRoom, withClientContextRenderOptions, stubClient } from "../../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../../src/MatrixClientPeg";
import { VoiceBroadcastInfoEventType } from "../../../../../../../src/voice-broadcast";
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
import { ElementCall } from "../../../../../../../src/models/Call";
@@ -34,14 +33,6 @@ describe("RolesRoomSettingsTab", () => {
return renderResult;
};
const getVoiceBroadcastsSelect = async (): Promise<Element> => {
return (await renderTab()).container.querySelector("select[label='Voice broadcasts']")!;
};
const getVoiceBroadcastsSelectedOption = async (): Promise<Element> => {
return (await renderTab()).container.querySelector("select[label='Voice broadcasts'] option:checked")!;
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.safeGet();
@@ -76,26 +67,6 @@ describe("RolesRoomSettingsTab", () => {
expect(container.querySelector(`[placeholder="@admin:server"]`)).toBeDisabled();
});
it("should initially show »Moderator« permission for »Voice broadcasts«", async () => {
expect((await getVoiceBroadcastsSelectedOption()).textContent).toBe("Moderator");
});
describe("when setting »Default« permission for »Voice broadcasts«", () => {
beforeEach(async () => {
fireEvent.change(await getVoiceBroadcastsSelect(), {
target: { value: 0 },
});
});
it("should update the power levels", () => {
expect(cli.sendStateEvent).toHaveBeenCalledWith(roomId, EventType.RoomPowerLevels, {
events: {
[VoiceBroadcastInfoEventType]: 0,
},
});
});
});
describe("Element Call", () => {
const setGroupCallsEnabled = (val: boolean): void => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {

View File

@@ -75,7 +75,7 @@ describe("<SecurityRoomSettingsTab />", () => {
beforeEach(async () => {
client.sendStateEvent.mockReset().mockResolvedValue({ event_id: "test" });
client.isRoomEncrypted.mockReturnValue(false);
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
client.getClientWellKnown.mockReturnValue(undefined);
jest.spyOn(SettingsStore, "getValue").mockRestore();
@@ -313,7 +313,7 @@ describe("<SecurityRoomSettingsTab />", () => {
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
fireEvent.click(screen.getByLabelText("Encrypted"));
@@ -330,7 +330,7 @@ describe("<SecurityRoomSettingsTab />", () => {
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
fireEvent.click(screen.getByLabelText("Encrypted"));
@@ -416,12 +416,12 @@ describe("<SecurityRoomSettingsTab />", () => {
expect(screen.getByText("Once enabled, encryption cannot be disabled.")).toBeInTheDocument();
});
it("displays unencrypted rooms with toggle disabled", () => {
it("displays unencrypted rooms with toggle disabled", async () => {
const room = new Room(roomId, client, userId);
setRoomStateEvents(room);
getComponent(room);
expect(screen.getByLabelText("Encrypted")).not.toBeChecked();
await waitFor(() => expect(screen.getByLabelText("Encrypted")).not.toBeChecked());
expect(screen.getByLabelText("Encrypted").getAttribute("aria-disabled")).toEqual("true");
expect(screen.queryByText("Once enabled, encryption cannot be disabled.")).not.toBeInTheDocument();
expect(screen.getByText("Your server requires encryption to be disabled.")).toBeInTheDocument();

View File

@@ -11,11 +11,8 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { OidcClientStore } from "../../../src/stores/oidc/OidcClientStore";
import { UserProfilesStore } from "../../../src/stores/UserProfilesStore";
import { VoiceBroadcastPreRecordingStore } from "../../../src/voice-broadcast";
import { createTestClient } from "../../test-utils";
jest.mock("../../../src/voice-broadcast/stores/VoiceBroadcastPreRecordingStore");
describe("SdkContextClass", () => {
let sdkContext = SdkContextClass.instance;
let client: MatrixClient;
@@ -33,12 +30,6 @@ describe("SdkContextClass", () => {
expect(SdkContextClass.instance).toBe(globalInstance);
});
it("voiceBroadcastPreRecordingStore should always return the same VoiceBroadcastPreRecordingStore", () => {
const first = sdkContext.voiceBroadcastPreRecordingStore;
expect(first).toBeInstanceOf(VoiceBroadcastPreRecordingStore);
expect(sdkContext.voiceBroadcastPreRecordingStore).toBe(first);
});
it("userProfilesStore should raise an error without a client", () => {
expect(() => sdkContext.userProfilesStore).toThrow("Unable to create UserProfilesStore without a client");
});

View File

@@ -7,19 +7,16 @@ Please see LICENSE files in the repository root for full details.
*/
import { mocked } from "jest-mock";
import { EventType, MatrixClient, MatrixEvent, MsgType, RelationType, Room } from "matrix-js-sdk/src/matrix";
import { EventType, MatrixClient, MatrixEvent, MsgType, Room } from "matrix-js-sdk/src/matrix";
import {
JSONEventFactory,
MessageEventFactory,
pickFactory,
RoomCreateEventFactory,
TextualEventFactory,
} from "../../../src/events/EventTileFactory";
import SettingsStore from "../../../src/settings/SettingsStore";
import { VoiceBroadcastChunkEventType, VoiceBroadcastInfoState } from "../../../src/voice-broadcast";
import { createTestClient, mkEvent } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
const roomId = "!room:example.com";
@@ -31,11 +28,7 @@ describe("pickFactory", () => {
let createEventWithoutPredecessor: MatrixEvent;
let dynamicPredecessorEvent: MatrixEvent;
let voiceBroadcastStartedEvent: MatrixEvent;
let voiceBroadcastStoppedEvent: MatrixEvent;
let voiceBroadcastChunkEvent: MatrixEvent;
let utdEvent: MatrixEvent;
let utdBroadcastChunkEvent: MatrixEvent;
let audioMessageEvent: MatrixEvent;
beforeAll(() => {
@@ -82,29 +75,6 @@ describe("pickFactory", () => {
last_known_event_id: null,
},
});
voiceBroadcastStartedEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId()!,
client.deviceId!,
);
room.addLiveEvents([voiceBroadcastStartedEvent]);
voiceBroadcastStoppedEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Stopped,
client.getUserId()!,
client.deviceId!,
);
voiceBroadcastChunkEvent = mkEvent({
event: true,
type: EventType.RoomMessage,
user: client.getUserId()!,
room: roomId,
content: {
msgtype: MsgType.Audio,
[VoiceBroadcastChunkEventType]: {},
},
});
audioMessageEvent = mkEvent({
event: true,
type: EventType.RoomMessage,
@@ -123,20 +93,6 @@ describe("pickFactory", () => {
msgtype: "m.bad.encrypted",
},
});
utdBroadcastChunkEvent = mkEvent({
event: true,
type: EventType.RoomMessage,
user: client.getUserId()!,
room: roomId,
content: {
"msgtype": "m.bad.encrypted",
"m.relates_to": {
rel_type: RelationType.Reference,
event_id: voiceBroadcastStartedEvent.getId(),
},
},
});
jest.spyOn(utdBroadcastChunkEvent, "isDecryptionFailure").mockReturnValue(true);
});
it("should return JSONEventFactory for a no-op m.room.power_levels event", () => {
@@ -151,10 +107,6 @@ describe("pickFactory", () => {
});
describe("when showing hidden events", () => {
it("should return a JSONEventFactory for a voice broadcast event", () => {
expect(pickFactory(voiceBroadcastChunkEvent, client, true)).toBe(JSONEventFactory);
});
it("should return a JSONEventFactory for a room create event without predecessor", () => {
room.currentState.events.set(
EventType.RoomCreate,
@@ -164,17 +116,9 @@ describe("pickFactory", () => {
expect(pickFactory(createEventWithoutPredecessor, client, true)).toBe(JSONEventFactory);
});
it("should return a TextualEventFactory for a voice broadcast stopped event", () => {
expect(pickFactory(voiceBroadcastStoppedEvent, client, true)).toBe(TextualEventFactory);
});
it("should return a MessageEventFactory for an audio message event", () => {
expect(pickFactory(audioMessageEvent, client, true)).toBe(MessageEventFactory);
});
it("should return a MessageEventFactory for a UTD broadcast chunk event", () => {
expect(pickFactory(utdBroadcastChunkEvent, client, true)).toBe(MessageEventFactory);
});
});
describe("when not showing hidden events", () => {
@@ -252,14 +196,6 @@ describe("pickFactory", () => {
});
});
it("should return undefined for a voice broadcast event", () => {
expect(pickFactory(voiceBroadcastChunkEvent, client, false)).toBeUndefined();
});
it("should return a TextualEventFactory for a voice broadcast stopped event", () => {
expect(pickFactory(voiceBroadcastStoppedEvent, client, false)).toBe(TextualEventFactory);
});
it("should return a MessageEventFactory for an audio message event", () => {
expect(pickFactory(audioMessageEvent, client, false)).toBe(MessageEventFactory);
});
@@ -267,9 +203,5 @@ describe("pickFactory", () => {
it("should return a MessageEventFactory for a UTD event", () => {
expect(pickFactory(utdEvent, client, false)).toBe(MessageEventFactory);
});
it("should return undefined for a UTD broadcast chunk event", () => {
expect(pickFactory(utdBroadcastChunkEvent, client, false)).toBeUndefined();
});
});
});

View File

@@ -332,7 +332,7 @@ describe("linkify-matrix", () => {
const event = new MouseEvent("mousedown");
event.preventDefault = jest.fn();
handlers.click(event);
handlers!.click(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({
@@ -372,7 +372,7 @@ describe("linkify-matrix", () => {
const event = new MouseEvent("mousedown");
event.preventDefault = jest.fn();
handlers.click(event);
handlers!.click(event);
expect(event.preventDefault).toHaveBeenCalled();
expect(dispatchSpy).toHaveBeenCalledWith(
expect.objectContaining({

View File

@@ -119,7 +119,7 @@ const setUpClientRoomAndStores = (): {
skey: stateKey,
content: content as IContent,
});
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: true });
return { event_id: event.getId()! };
});

View File

@@ -201,6 +201,7 @@ describe("MemberListStore", () => {
function addEventToRoom(room: Room, ev: MatrixEvent) {
room.getLiveTimeline().addEvent(ev, {
toStartOfTimeline: false,
addToState: true,
});
}

View File

@@ -24,13 +24,6 @@ import { ActiveRoomChangedPayload } from "../../../src/dispatcher/payloads/Activ
import { SpaceStoreClass } from "../../../src/stores/spaces/SpaceStore";
import { TestSdkContext } from "../TestSdkContext";
import { ViewRoomPayload } from "../../../src/dispatcher/payloads/ViewRoomPayload";
import {
VoiceBroadcastInfoState,
VoiceBroadcastPlayback,
VoiceBroadcastPlaybacksStore,
VoiceBroadcastRecording,
} from "../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
import Modal from "../../../src/Modal";
import ErrorDialog from "../../../src/components/views/dialogs/ErrorDialog";
import { CancelAskToJoinPayload } from "../../../src/dispatcher/payloads/CancelAskToJoinPayload";
@@ -160,7 +153,6 @@ describe("RoomViewStore", function () {
stores._SlidingSyncManager = slidingSyncManager;
stores._PosthogAnalytics = new MockPosthogAnalytics();
stores._SpaceStore = new MockSpaceStore();
stores._VoiceBroadcastPlaybacksStore = new VoiceBroadcastPlaybacksStore(stores.voiceBroadcastRecordingsStore);
roomViewStore = new RoomViewStore(dis, stores);
stores._RoomViewStore = roomViewStore;
});
@@ -343,88 +335,6 @@ describe("RoomViewStore", function () {
});
});
describe("when listening to a voice broadcast", () => {
let voiceBroadcastPlayback: VoiceBroadcastPlayback;
beforeEach(() => {
voiceBroadcastPlayback = new VoiceBroadcastPlayback(
mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
mockClient.getSafeUserId(),
"d42",
),
mockClient,
stores.voiceBroadcastRecordingsStore,
);
stores.voiceBroadcastPlaybacksStore.setCurrent(voiceBroadcastPlayback);
jest.spyOn(voiceBroadcastPlayback, "pause").mockImplementation();
});
it("and viewing a call it should pause the current broadcast", async () => {
await viewCall();
expect(voiceBroadcastPlayback.pause).toHaveBeenCalled();
expect(roomViewStore.isViewingCall()).toBe(true);
});
});
describe("when recording a voice broadcast", () => {
beforeEach(() => {
stores.voiceBroadcastRecordingsStore.setCurrent(
new VoiceBroadcastRecording(
mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
mockClient.getSafeUserId(),
"d42",
),
mockClient,
),
);
});
it("and trying to view a call, it should not actually view it and show the info dialog", async () => {
await viewCall();
expect(Modal.createDialog).toMatchSnapshot();
expect(roomViewStore.isViewingCall()).toBe(false);
});
describe("and viewing a room with a broadcast", () => {
beforeEach(async () => {
const broadcastEvent = mkVoiceBroadcastInfoStateEvent(
roomId2,
VoiceBroadcastInfoState.Started,
mockClient.getSafeUserId(),
"ABC123",
);
room2.addLiveEvents([broadcastEvent]);
stores.voiceBroadcastPlaybacksStore.getByInfoEvent(broadcastEvent, mockClient);
dis.dispatch({ action: Action.ViewRoom, room_id: roomId2 });
await untilDispatch(Action.ActiveRoomChanged, dis);
});
it("should continue recording", () => {
expect(stores.voiceBroadcastPlaybacksStore.getCurrent()).toBeNull();
expect(stores.voiceBroadcastRecordingsStore.getCurrent()?.getState()).toBe(
VoiceBroadcastInfoState.Started,
);
});
describe("and stopping the recording", () => {
beforeEach(async () => {
await stores.voiceBroadcastRecordingsStore.getCurrent()?.stop();
// check test precondition
expect(stores.voiceBroadcastRecordingsStore.getCurrent()).toBeNull();
});
it("should view the broadcast", () => {
expect(stores.voiceBroadcastPlaybacksStore.getCurrent()?.infoEvent.getRoomId()).toBe(roomId2);
});
});
});
});
describe("Sliding Sync", function () {
beforeEach(() => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName, roomId, value) => {

View File

@@ -18,26 +18,3 @@ exports[`RoomViewStore should display the generic error message when the roomId
"title": "Failed to join",
}
`;
exports[`RoomViewStore when recording a voice broadcast and trying to view a call, it should not actually view it and show the info dialog 1`] = `
[MockFunction] {
"calls": [
[
[Function],
{
"description": <p>
You cant start a call as you are currently recording a live broadcast. Please end your live broadcast in order to start a call.
</p>,
"hasCloseButton": true,
"title": "Cant start a call",
},
],
],
"results": [
{
"type": "return",
"value": undefined,
},
],
}
`;

View File

@@ -18,6 +18,9 @@ import { ActiveRoomChangedPayload } from "../../../../src/dispatcher/payloads/Ac
import RightPanelStore from "../../../../src/stores/right-panel/RightPanelStore";
import { RightPanelPhases } from "../../../../src/stores/right-panel/RightPanelStorePhases";
import SettingsStore from "../../../../src/settings/SettingsStore";
import { pendingVerificationRequestForUser } from "../../../../src/verification.ts";
jest.mock("../../../../src/verification");
describe("RightPanelStore", () => {
// Mock out the settings store so the right panel store can't persist values between tests
@@ -97,7 +100,7 @@ describe("RightPanelStore", () => {
it("does nothing if given an invalid state", async () => {
await viewRoom("!1:example.org");
// Needs a member specified to be valid
store.setCard({ phase: RightPanelPhases.RoomMemberInfo }, true, "!1:example.org");
store.setCard({ phase: RightPanelPhases.MemberInfo }, true, "!1:example.org");
expect(store.roomPhaseHistory).toEqual([]);
});
it("only creates a single history entry if given the same card twice", async () => {
@@ -114,15 +117,15 @@ describe("RightPanelStore", () => {
it("overwrites history if changing the phase", async () => {
await viewRoom("!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomSummary }, true, "!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomMemberList }, true, "!1:example.org");
expect(store.roomPhaseHistory).toEqual([{ phase: RightPanelPhases.RoomMemberList, state: {} }]);
store.setCard({ phase: RightPanelPhases.MemberList }, true, "!1:example.org");
expect(store.roomPhaseHistory).toEqual([{ phase: RightPanelPhases.MemberList, state: {} }]);
});
});
describe("setCards", () => {
it("overwrites history", async () => {
await viewRoom("!1:example.org");
store.setCard({ phase: RightPanelPhases.RoomMemberList }, true, "!1:example.org");
store.setCard({ phase: RightPanelPhases.MemberList }, true, "!1:example.org");
store.setCards(
[{ phase: RightPanelPhases.RoomSummary }, { phase: RightPanelPhases.PinnedMessages }],
true,
@@ -200,21 +203,40 @@ describe("RightPanelStore", () => {
store.setCards(
[
{
phase: RightPanelPhases.RoomMemberList,
phase: RightPanelPhases.MemberList,
},
{
phase: RightPanelPhases.RoomMemberInfo,
phase: RightPanelPhases.MemberInfo,
state: { member: new RoomMember("!1:example.org", "@alice:example.org") },
},
],
true,
"!1:example.org",
);
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberInfo);
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.MemberInfo);
// Switch away and back
await viewRoom("!2:example.org");
await viewRoom("!1:example.org");
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.RoomMemberList);
expect(store.currentCardForRoom("!1:example.org").phase).toEqual(RightPanelPhases.MemberList);
});
it("should redirect to verification if set to phase MemberInfo for a user with a pending verification", async () => {
const member = new RoomMember("!1:example.org", "@alice:example.org");
const verificationRequest = { mockVerificationRequest: true } as any;
mocked(pendingVerificationRequestForUser).mockReturnValue(verificationRequest);
await viewRoom("!1:example.org");
store.setCard(
{
phase: RightPanelPhases.MemberInfo,
state: { member },
},
true,
"!1:example.org",
);
expect(store.currentCard).toEqual({
phase: RightPanelPhases.EncryptionPanel,
state: { member, verificationRequest },
});
});
});

View File

@@ -30,7 +30,7 @@ describe("onView3pidInvite()", () => {
};
onView3pidInvite(payload, rightPanelStore);
expect(rightPanelStore.showOrHidePhase).toHaveBeenCalledWith(RightPanelPhases.RoomMemberList);
expect(rightPanelStore.showOrHidePhase).toHaveBeenCalledWith(RightPanelPhases.MemberList);
expect(rightPanelStore.pushCard).not.toHaveBeenCalled();
});
@@ -43,7 +43,7 @@ describe("onView3pidInvite()", () => {
expect(rightPanelStore.showOrHidePhase).not.toHaveBeenCalled();
expect(rightPanelStore.pushCard).toHaveBeenCalledWith({
phase: RightPanelPhases.Room3pidMemberInfo,
phase: RightPanelPhases.ThreePidMemberInfo,
state: { memberInfoEvent: payload.event },
});
});

View File

@@ -35,7 +35,7 @@ describe("MessagePreviewStore", () => {
event: MatrixEvent,
fireAction = true,
): Promise<void> {
room.addLiveEvents([event]);
room.addLiveEvents([event], { addToState: true });
if (fireAction) {
// @ts-ignore private access
await store.onAction({

View File

@@ -47,11 +47,11 @@ describe("RecentAlgorithm", () => {
room.getMyMembership = () => KnownMembership.Join;
room.addLiveEvents([event1]);
room.addLiveEvents([event1], { addToState: true });
expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(5);
expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(5);
room.addLiveEvents([event2]);
room.addLiveEvents([event2], { addToState: true });
expect(algorithm.getLastTs(room, "@jane:matrix.org")).toBe(10);
expect(algorithm.getLastTs(room, "@john:matrix.org")).toBe(10);
@@ -94,8 +94,8 @@ describe("RecentAlgorithm", () => {
event: true,
});
room1.addLiveEvents([evt]);
room2.addLiveEvents([evt2]);
room1.addLiveEvents([evt], { addToState: true });
room2.addLiveEvents([evt2], { addToState: true });
expect(algorithm.sortRooms([room2, room1], DefaultTagID.Untagged)).toEqual([room1, room2]);
});
@@ -115,7 +115,7 @@ describe("RecentAlgorithm", () => {
event: true,
});
room1.addLiveEvents([evt]);
room1.addLiveEvents([evt], { addToState: true });
expect(algorithm.sortRooms([room2, room1], DefaultTagID.Untagged)).toEqual([room2, room1]);
@@ -127,7 +127,7 @@ describe("RecentAlgorithm", () => {
ts: 12,
});
room1.addLiveEvents(events);
room1.addLiveEvents(events, { addToState: true });
});
it("orders rooms based on thread replies too", () => {
@@ -145,7 +145,7 @@ describe("RecentAlgorithm", () => {
ts: 12,
length: 5,
});
room1.addLiveEvents(events1);
room1.addLiveEvents(events1, { addToState: true });
const { events: events2 } = mkThread({
room: room2,
@@ -155,7 +155,7 @@ describe("RecentAlgorithm", () => {
ts: 14,
length: 10,
});
room2.addLiveEvents(events2);
room2.addLiveEvents(events2, { addToState: true });
expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room2, room1]);
@@ -169,7 +169,7 @@ describe("RecentAlgorithm", () => {
// replies are 1ms after each other
ts: 50,
});
room1.addLiveEvents([threadReply]);
room1.addLiveEvents([threadReply], { addToState: true });
expect(algorithm.sortRooms([room1, room2], DefaultTagID.Untagged)).toEqual([room1, room2]);
});

View File

@@ -71,18 +71,5 @@ describe("MessageEventPreview", () => {
});
expect(preview.getTextFor(event)).toBe(`${userId}: test new content body`);
});
it("when called with a broadcast chunk event it should return null", () => {
const event = mkEvent({
event: true,
content: {
body: "test body",
["io.element.voice_broadcast_chunk"]: {},
},
user: userId,
type: "m.room.message",
});
expect(preview.getTextFor(event)).toBeNull();
});
});
});

View File

@@ -70,7 +70,7 @@ describe("ReactionEventPreview", () => {
room: roomId,
});
room.getUnfilteredTimelineSet().addLiveEvent(message, {});
room.getUnfilteredTimelineSet().addLiveEvent(message, { addToState: true });
const event = mkEvent({
event: true,
@@ -107,7 +107,7 @@ describe("ReactionEventPreview", () => {
room: roomId,
});
room.getUnfilteredTimelineSet().addLiveEvent(message, {});
room.getUnfilteredTimelineSet().addLiveEvent(message, { addToState: true });
const event = mkEvent({
event: true,

View File

@@ -1,54 +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 { Room } from "matrix-js-sdk/src/matrix";
import { VoiceBroadcastPreview } from "../../../../../src/stores/room-list/previews/VoiceBroadcastPreview";
import { VoiceBroadcastInfoState } from "../../../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../../voice-broadcast/utils/test-utils";
describe("VoiceBroadcastPreview.getTextFor", () => {
const roomId = "!room:example.com";
const userId = "@user:example.com";
const deviceId = "d42";
let preview: VoiceBroadcastPreview;
beforeAll(() => {
preview = new VoiceBroadcastPreview();
});
it("when passing an event with empty content, it should return null", () => {
const event = mkEvent({
event: true,
content: {},
user: userId,
type: "m.room.message",
});
expect(preview.getTextFor(event)).toBeNull();
});
it("when passing a broadcast started event, it should return null", () => {
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId);
expect(preview.getTextFor(event)).toBeNull();
});
it("when passing a broadcast stopped event, it should return the expected text", () => {
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId);
expect(preview.getTextFor(event)).toBe("@user:example.com ended a voice broadcast");
});
it("when passing a redacted broadcast stopped event, it should return null", () => {
const event = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Stopped, userId, deviceId);
event.makeRedacted(
mkEvent({ event: true, content: {}, user: userId, type: "m.room.redaction" }),
new Room(roomId, stubClient(), userId),
);
expect(preview.getTextFor(event)).toBeNull();
});
});

View File

@@ -22,9 +22,6 @@ import { waitFor } from "jest-matrix-react";
import { stubClient, mkRoom, mkEvent } from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { StopGapWidget } from "../../../../src/stores/widgets/StopGapWidget";
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
import { VoiceBroadcastInfoEventType, VoiceBroadcastRecording } from "../../../../src/voice-broadcast";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
import SettingsStore from "../../../../src/settings/SettingsStore";
@@ -225,41 +222,6 @@ describe("StopGapWidget", () => {
expect(messaging.feedEvent).toHaveBeenLastCalledWith(event.getEffectiveEvent(), "!1:example.org");
});
});
describe("when there is a voice broadcast recording", () => {
let voiceBroadcastInfoEvent: MatrixEvent;
let voiceBroadcastRecording: VoiceBroadcastRecording;
beforeEach(() => {
voiceBroadcastInfoEvent = mkEvent({
event: true,
room: client.getRoom("x")?.roomId,
user: client.getUserId()!,
type: VoiceBroadcastInfoEventType,
content: {},
});
voiceBroadcastRecording = new VoiceBroadcastRecording(voiceBroadcastInfoEvent, client);
jest.spyOn(voiceBroadcastRecording, "pause");
jest.spyOn(SdkContextClass.instance.voiceBroadcastRecordingsStore, "getCurrent").mockReturnValue(
voiceBroadcastRecording,
);
});
describe(`and receiving a action:${ElementWidgetActions.JoinCall} message`, () => {
beforeEach(async () => {
messaging.on.mock.calls.find(([event, listener]) => {
if (event === `action:${ElementWidgetActions.JoinCall}`) {
listener();
return true;
}
});
});
it("should pause the current voice broadcast recording", () => {
expect(voiceBroadcastRecording.pause).toHaveBeenCalled();
});
});
});
});
describe("StopGapWidget with stickyPromise", () => {
let client: MockedObject<MatrixClient>;

View File

@@ -29,7 +29,7 @@ describe("useTopic", () => {
event: true,
});
room.addLiveEvents([topic]);
room.addLiveEvents([topic], { addToState: true });
function RoomTopic() {
const topic = useTopic(room);
@@ -52,7 +52,7 @@ describe("useTopic", () => {
});
act(() => {
room.addLiveEvents([updatedTopic]);
room.addLiveEvents([updatedTopic], { addToState: true });
});
expect(screen.queryByText("New topic")).toBeInTheDocument();

View File

@@ -1,46 +0,0 @@
/*
Copyright 2024 New Vector Ltd.
Copyright 2022 The Matrix.org Foundation C.I.C.
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/
import { getEventDisplayInfo } from "../../../src/utils/EventRenderingUtils";
import { VoiceBroadcastInfoState } from "../../../src/voice-broadcast";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
import { createTestClient } from "../../test-utils";
describe("getEventDisplayInfo", () => {
const mkBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => {
return mkVoiceBroadcastInfoStateEvent("!room:example.com", state, "@user:example.com", "ASD123");
};
it("should return the expected value for a broadcast started event", () => {
expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Started), false))
.toMatchInlineSnapshot(`
{
"hasRenderer": true,
"isBubbleMessage": false,
"isInfoMessage": false,
"isLeftAlignedBubbleMessage": false,
"isSeeingThroughMessageHiddenForModeration": false,
"noBubbleEvent": true,
}
`);
});
it("should return the expected value for a broadcast stopped event", () => {
expect(getEventDisplayInfo(createTestClient(), mkBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped), false))
.toMatchInlineSnapshot(`
{
"hasRenderer": true,
"isBubbleMessage": false,
"isInfoMessage": true,
"isLeftAlignedBubbleMessage": false,
"isSeeingThroughMessageHiddenForModeration": false,
"noBubbleEvent": true,
}
`);
});
});

View File

@@ -35,8 +35,6 @@ import {
import { getMockClientWithEventEmitter, makeBeaconInfoEvent, makePollStartEvent, stubClient } from "../../test-utils";
import dis from "../../../src/dispatcher/dispatcher";
import { Action } from "../../../src/dispatcher/actions";
import { mkVoiceBroadcastInfoStateEvent } from "../voice-broadcast/utils/test-utils";
import { VoiceBroadcastInfoState } from "../../../src/voice-broadcast/types";
jest.mock("../../../src/dispatcher/dispatcher");
@@ -148,20 +146,6 @@ describe("EventUtils", () => {
},
});
const voiceBroadcastStart = mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Started,
"@user:example.com",
"ABC123",
);
const voiceBroadcastStop = mkVoiceBroadcastInfoStateEvent(
"!room:example.com",
VoiceBroadcastInfoState.Stopped,
"@user:example.com",
"ABC123",
);
describe("isContentActionable()", () => {
type TestCase = [string, MatrixEvent];
it.each<TestCase>([
@@ -172,7 +156,6 @@ describe("EventUtils", () => {
["room member event", roomMemberEvent],
["event without msgtype", noMsgType],
["event without content body property", noContentBody],
["broadcast stop event", voiceBroadcastStop],
])("returns false for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(false);
});
@@ -183,7 +166,6 @@ describe("EventUtils", () => {
["event with empty content body", emptyContentBody],
["event with a content body", niceTextMessage],
["beacon_info event", beaconInfoEvent],
["broadcast start event", voiceBroadcastStart],
])("returns true for %s", (_description, event) => {
expect(isContentActionable(event)).toBe(true);
});

View File

@@ -593,18 +593,21 @@ describe("HTMLExport", () => {
it("should not make /messages requests when exporting 'Current Timeline'", async () => {
client.createMessagesRequest.mockRejectedValue(new Error("Should never be called"));
room.addLiveEvents([
new MatrixEvent({
event_id: `$eventId`,
type: EventType.RoomMessage,
sender: client.getSafeUserId(),
origin_server_ts: 123456789,
content: {
msgtype: "m.text",
body: `testing testing`,
},
}),
]);
room.addLiveEvents(
[
new MatrixEvent({
event_id: `$eventId`,
type: EventType.RoomMessage,
sender: client.getSafeUserId(),
origin_server_ts: 123456789,
content: {
msgtype: "m.text",
body: `testing testing`,
},
}),
],
{ addToState: true },
);
const exporter = new HTMLExporter(
room,

View File

@@ -121,7 +121,7 @@ describe("notifications", () => {
user: USER_ID,
msg: "Hello",
});
room.addLiveEvents([message]);
room.addLiveEvents([message], { addToState: true });
sendReadReceiptSpy = jest.spyOn(client, "sendReadReceipt").mockResolvedValue({});
jest.spyOn(client, "getRooms").mockReturnValue([room]);
jest.spyOn(SettingsStore, "getValue").mockImplementation((name) => {
@@ -187,7 +187,7 @@ describe("notifications", () => {
user: USER_ID,
ts: 1,
});
room.addLiveEvents([message]);
room.addLiveEvents([message], { addToState: true });
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
await clearAllNotifications(client);
@@ -202,7 +202,7 @@ describe("notifications", () => {
user: USER_ID,
ts: 1,
});
room.addLiveEvents([message]);
room.addLiveEvents([message], { addToState: true });
room.setUnreadNotificationCount(NotificationCountType.Total, 1);
jest.spyOn(SettingsStore, "getValue").mockReset().mockReturnValue(false);

View File

@@ -1,251 +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 { Optional } from "matrix-events-sdk";
import { VoiceRecording } from "../../../../src/audio/VoiceRecording";
import SdkConfig from "../../../../src/SdkConfig";
import { concat } from "../../../../src/utils/arrays";
import {
ChunkRecordedPayload,
createVoiceBroadcastRecorder,
VoiceBroadcastRecorder,
VoiceBroadcastRecorderEvent,
} from "../../../../src/voice-broadcast";
// mock VoiceRecording because it contains all the audio APIs
jest.mock("../../../../src/audio/VoiceRecording", () => ({
VoiceRecording: jest.fn().mockReturnValue({
disableMaxLength: jest.fn(),
emit: jest.fn(),
liveData: {
onUpdate: jest.fn(),
},
start: jest.fn(),
stop: jest.fn(),
destroy: jest.fn(),
}),
}));
jest.mock("../../../../src/settings/SettingsStore");
describe("VoiceBroadcastRecorder", () => {
describe("createVoiceBroadcastRecorder", () => {
beforeEach(() => {
jest.spyOn(SdkConfig, "get").mockImplementation((key: string) => {
if (key === "voice_broadcast") {
return {
chunk_length: 1337,
};
}
});
});
afterEach(() => {
mocked(SdkConfig.get).mockRestore();
});
it("should return a VoiceBroadcastRecorder instance with targetChunkLength from config", () => {
const voiceBroadcastRecorder = createVoiceBroadcastRecorder();
expect(voiceBroadcastRecorder).toBeInstanceOf(VoiceBroadcastRecorder);
expect(voiceBroadcastRecorder.targetChunkLength).toBe(1337);
});
});
describe("instance", () => {
const chunkLength = 30;
// 0... OpusHead
const headers1 = new Uint8Array([...Array(28).fill(0), 79, 112, 117, 115, 72, 101, 97, 100]);
// 0... OpusTags
const headers2 = new Uint8Array([...Array(28).fill(0), 79, 112, 117, 115, 84, 97, 103, 115]);
const chunk1 = new Uint8Array([5, 6]);
const chunk2a = new Uint8Array([7, 8]);
const chunk2b = new Uint8Array([9, 10]);
const contentType = "test content type";
let voiceRecording: VoiceRecording;
let voiceBroadcastRecorder: VoiceBroadcastRecorder;
let onChunkRecorded: (chunk: ChunkRecordedPayload) => void;
const simulateFirstChunk = (): void => {
// send headers in wrong order and multiple times to test robustness for that
voiceRecording.onDataAvailable!(headers2);
voiceRecording.onDataAvailable!(headers1);
voiceRecording.onDataAvailable!(headers1);
voiceRecording.onDataAvailable!(headers2);
// set recorder seconds to something greater than the test chunk length of 30
// @ts-ignore
voiceRecording.recorderSeconds = 42;
voiceRecording.onDataAvailable!(chunk1);
voiceRecording.onDataAvailable!(headers1);
};
const expectOnFirstChunkRecorded = (): void => {
expect(onChunkRecorded).toHaveBeenNthCalledWith(1, {
buffer: concat(headers1, headers2, chunk1),
length: 42,
});
};
const itShouldNotEmitAChunkRecordedEvent = (): void => {
it("should not emit a ChunkRecorded event", (): void => {
expect(voiceRecording.emit).not.toHaveBeenCalledWith(
VoiceBroadcastRecorderEvent.ChunkRecorded,
expect.anything(),
);
});
};
beforeEach(() => {
voiceRecording = new VoiceRecording();
// @ts-ignore
voiceRecording.recorderSeconds = 23;
// @ts-ignore
voiceRecording.contentType = contentType;
voiceBroadcastRecorder = new VoiceBroadcastRecorder(voiceRecording, chunkLength);
jest.spyOn(voiceBroadcastRecorder, "removeAllListeners");
onChunkRecorded = jest.fn();
voiceBroadcastRecorder.on(VoiceBroadcastRecorderEvent.ChunkRecorded, onChunkRecorded);
});
afterEach(() => {
voiceBroadcastRecorder.destroy();
});
it("start should forward the call to VoiceRecording.start", async () => {
await voiceBroadcastRecorder.start();
expect(voiceRecording.start).toHaveBeenCalled();
});
describe("stop", () => {
beforeEach(async () => {
await voiceBroadcastRecorder.stop();
});
it("should forward the call to VoiceRecording.stop", async () => {
expect(voiceRecording.stop).toHaveBeenCalled();
});
itShouldNotEmitAChunkRecordedEvent();
});
describe("when calling destroy", () => {
beforeEach(() => {
voiceBroadcastRecorder.destroy();
});
it("should call VoiceRecording.destroy", () => {
expect(voiceRecording.destroy).toHaveBeenCalled();
});
it("should remove all listeners", () => {
expect(voiceBroadcastRecorder.removeAllListeners).toHaveBeenCalled();
});
});
it("contentType should return the value from VoiceRecording", () => {
expect(voiceBroadcastRecorder.contentType).toBe(contentType);
});
describe("when the first header from recorder has been received", () => {
beforeEach(() => {
voiceRecording.onDataAvailable!(headers1);
});
itShouldNotEmitAChunkRecordedEvent();
});
describe("when the second header from recorder has been received", () => {
beforeEach(() => {
voiceRecording.onDataAvailable!(headers1);
voiceRecording.onDataAvailable!(headers2);
});
itShouldNotEmitAChunkRecordedEvent();
});
describe("when a third page from recorder has been received", () => {
beforeEach(() => {
voiceRecording.onDataAvailable!(headers1);
voiceRecording.onDataAvailable!(headers2);
voiceRecording.onDataAvailable!(chunk1);
});
itShouldNotEmitAChunkRecordedEvent();
describe("and calling stop", () => {
let stopPayload: Optional<ChunkRecordedPayload>;
beforeEach(async () => {
stopPayload = await voiceBroadcastRecorder.stop();
});
it("should return the remaining chunk", () => {
expect(stopPayload).toEqual({
buffer: concat(headers1, headers2, chunk1),
length: 23,
});
});
describe("and calling start again and receiving some data", () => {
beforeEach(() => {
simulateFirstChunk();
});
it("should emit the ChunkRecorded event for the first chunk", () => {
expectOnFirstChunkRecorded();
});
});
});
describe("and calling stop() with recording.stop error)", () => {
let stopPayload: Optional<ChunkRecordedPayload>;
beforeEach(async () => {
mocked(voiceRecording.stop).mockRejectedValue("Error");
stopPayload = await voiceBroadcastRecorder.stop();
});
it("should return the remaining chunk", () => {
expect(stopPayload).toEqual({
buffer: concat(headers1, headers2, chunk1),
length: 23,
});
});
});
});
describe("when some chunks have been received", () => {
beforeEach(() => {
simulateFirstChunk();
// simulate a second chunk
voiceRecording.onDataAvailable!(chunk2a);
// send headers again to test robustness for that
voiceRecording.onDataAvailable!(headers2);
// add another 30 seconds for the next chunk
// @ts-ignore
voiceRecording.recorderSeconds = 72;
voiceRecording.onDataAvailable!(chunk2b);
});
it("should emit ChunkRecorded events", () => {
expectOnFirstChunkRecorded();
expect(onChunkRecorded).toHaveBeenNthCalledWith(2, {
buffer: concat(headers1, headers2, chunk2a, chunk2b),
length: 72 - 42, // 72 (position at second chunk) - 42 (position of first chunk)
});
});
});
});
});

View File

@@ -1,171 +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";
import { act, render, screen } from "jest-matrix-react";
import { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import {
VoiceBroadcastBody as UnwrappedVoiceBroadcastBody,
VoiceBroadcastInfoState,
VoiceBroadcastRecordingBody,
VoiceBroadcastRecording,
VoiceBroadcastPlaybackBody,
VoiceBroadcastPlayback,
VoiceBroadcastRecordingsStore,
} from "../../../../src/voice-broadcast";
import { withClientContextRenderOptions, stubClient, wrapInSdkContext } from "../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
import { MediaEventHelper } from "../../../../src/utils/MediaEventHelper";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { SdkContextClass } from "../../../../src/contexts/SDKContext";
jest.mock("../../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({
VoiceBroadcastRecordingBody: jest.fn(),
}));
jest.mock("../../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlaybackBody", () => ({
VoiceBroadcastPlaybackBody: jest.fn(),
}));
jest.mock("../../../../src/utils/permalinks/Permalinks");
jest.mock("../../../../src/utils/MediaEventHelper");
jest.mock("../../../../src/stores/WidgetStore");
jest.mock("../../../../src/stores/widgets/WidgetLayoutStore");
describe("VoiceBroadcastBody", () => {
const roomId = "!room:example.com";
let userId: string;
let deviceId: string;
let client: MatrixClient;
let room: Room;
let infoEvent: MatrixEvent;
let stoppedEvent: MatrixEvent;
let testRecording: VoiceBroadcastRecording;
let testPlayback: VoiceBroadcastPlayback;
const renderVoiceBroadcast = () => {
const VoiceBroadcastBody = wrapInSdkContext(UnwrappedVoiceBroadcastBody, SdkContextClass.instance);
render(
<VoiceBroadcastBody
mxEvent={infoEvent}
mediaEventHelper={new MediaEventHelper(infoEvent)}
onHeightChanged={() => {}}
onMessageAllowed={() => {}}
permalinkCreator={new RoomPermalinkCreator(room)}
/>,
withClientContextRenderOptions(client),
);
testRecording = SdkContextClass.instance.voiceBroadcastRecordingsStore.getByInfoEvent(infoEvent, client);
};
beforeEach(() => {
client = stubClient();
userId = client.getUserId() || "";
deviceId = client.getDeviceId() || "";
mocked(client.relations).mockClear();
mocked(client.relations).mockResolvedValue({ events: [] });
room = new Room(roomId, client, userId);
mocked(client.getRoom).mockImplementation((getRoomId?: string) => {
if (getRoomId === roomId) return room;
return null;
});
infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, userId, deviceId);
stoppedEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Stopped,
userId,
deviceId,
infoEvent,
);
room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline());
testRecording = new VoiceBroadcastRecording(infoEvent, client);
testPlayback = new VoiceBroadcastPlayback(infoEvent, client, new VoiceBroadcastRecordingsStore());
mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }): ReactElement | null => {
if (testRecording === recording) {
return <div data-testid="voice-broadcast-recording-body" />;
}
return null;
});
mocked(VoiceBroadcastPlaybackBody).mockImplementation(({ playback }): ReactElement | null => {
if (testPlayback === playback) {
return <div data-testid="voice-broadcast-playback-body" />;
}
return null;
});
jest.spyOn(SdkContextClass.instance.voiceBroadcastRecordingsStore, "getByInfoEvent").mockImplementation(
(getEvent: MatrixEvent, getClient: MatrixClient): VoiceBroadcastRecording => {
if (getEvent === infoEvent && getClient === client) {
return testRecording;
}
throw new Error("unexpected event");
},
);
jest.spyOn(SdkContextClass.instance.voiceBroadcastPlaybacksStore, "getByInfoEvent").mockImplementation(
(getEvent: MatrixEvent): VoiceBroadcastPlayback => {
if (getEvent === infoEvent) {
return testPlayback;
}
throw new Error("unexpected event");
},
);
});
describe("when there is a stopped voice broadcast", () => {
beforeEach(() => {
room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline());
renderVoiceBroadcast();
});
it("should render a voice broadcast playback body", () => {
screen.getByTestId("voice-broadcast-playback-body");
});
});
describe("when there is a started voice broadcast from the current user", () => {
beforeEach(() => {
renderVoiceBroadcast();
});
it("should render a voice broadcast recording body", () => {
screen.getByTestId("voice-broadcast-recording-body");
});
describe("and the recordings ends", () => {
beforeEach(() => {
act(() => {
room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline());
});
});
it("should render a voice broadcast playback body", () => {
screen.getByTestId("voice-broadcast-playback-body");
});
});
});
describe("when displaying a voice broadcast playback", () => {
beforeEach(() => {
mocked(client).getUserId.mockReturnValue("@other:example.com");
renderVoiceBroadcast();
});
it("should render a voice broadcast playback body", () => {
screen.getByTestId("voice-broadcast-playback-body");
});
});
});

View File

@@ -1,24 +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 from "react";
import { render } from "jest-matrix-react";
import { LiveBadge } from "../../../../../src/voice-broadcast";
describe("LiveBadge", () => {
it("should render as expected with default props", () => {
const { container } = render(<LiveBadge />);
expect(container).toMatchSnapshot();
});
it("should render in grey as expected", () => {
const { container } = render(<LiveBadge grey={true} />);
expect(container).toMatchSnapshot();
});
});

View File

@@ -1,44 +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 from "react";
import { render, RenderResult, screen } from "jest-matrix-react";
import userEvent from "@testing-library/user-event";
import { VoiceBroadcastControl } from "../../../../../src/voice-broadcast";
import { Icon as StopIcon } from "../../../../res/img/compound/stop-16.svg";
describe("VoiceBroadcastControl", () => {
let result: RenderResult;
let onClick: () => void;
beforeEach(() => {
onClick = jest.fn();
});
describe("when rendering it", () => {
beforeEach(() => {
const stopIcon = <StopIcon className="mx_Icon mx_Icon_16" />;
result = render(<VoiceBroadcastControl onClick={onClick} label="test label" icon={stopIcon} />);
});
it("should render as expected", () => {
expect(result.container).toMatchSnapshot();
});
describe("when clicking it", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("test label"));
});
it("should call onClick", () => {
expect(onClick).toHaveBeenCalled();
});
});
});
});

View File

@@ -1,89 +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 from "react";
import { MatrixClient, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import { render, RenderResult } from "jest-matrix-react";
import { VoiceBroadcastHeader, VoiceBroadcastLiveness } from "../../../../../src/voice-broadcast";
import { mkRoom, stubClient } from "../../../../test-utils";
// mock RoomAvatar, because it is doing too much fancy stuff
jest.mock("../../../../../src/components/views/avatars/RoomAvatar", () => ({
__esModule: true,
default: jest.fn().mockImplementation(({ room }) => {
return <div data-testid="room-avatar">room avatar: {room.name}</div>;
}),
}));
describe("VoiceBroadcastHeader", () => {
const userId = "@user:example.com";
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
const sender = new RoomMember(roomId, userId);
let container: RenderResult["container"];
const renderHeader = (live: VoiceBroadcastLiveness, showBroadcast?: boolean, buffering?: boolean): RenderResult => {
return render(
<VoiceBroadcastHeader
live={live}
microphoneLabel={sender.name}
room={room}
showBroadcast={showBroadcast}
showBuffering={buffering}
/>,
);
};
beforeAll(() => {
client = stubClient();
room = mkRoom(client, roomId);
sender.name = "test user";
});
describe("when rendering a live broadcast header with broadcast info", () => {
beforeEach(() => {
container = renderHeader("live", true, true).container;
});
it("should render the header with a red live badge", () => {
expect(container).toMatchSnapshot();
});
});
describe("when rendering a buffering live broadcast header with broadcast info", () => {
beforeEach(() => {
container = renderHeader("live", true).container;
});
it("should render the header with a red live badge", () => {
expect(container).toMatchSnapshot();
});
});
describe("when rendering a live (grey) broadcast header with broadcast info", () => {
beforeEach(() => {
container = renderHeader("grey", true).container;
});
it("should render the header with a grey live badge", () => {
expect(container).toMatchSnapshot();
});
});
describe("when rendering a non-live broadcast header", () => {
beforeEach(() => {
container = renderHeader("not-live").container;
});
it("should render the header without a live badge", () => {
expect(container).toMatchSnapshot();
});
});
});

Some files were not shown because too many files have changed in this diff Show More