;
+ let accountDataEventKey: string;
+ let accountDataStore = {};
beforeEach(() => {
accountDataStore = {};
+ mockClient = getMockClientWithEventEmitter({
+ getUserId: jest.fn().mockReturnValue("@bob:example.org"),
+ isGuest: jest.fn().mockReturnValue(false),
+ getAccountData: jest.fn().mockImplementation(eventType => accountDataStore[eventType]),
+ setAccountData: jest.fn().mockImplementation((eventType, content) => {
+ accountDataStore[eventType] = new MatrixEvent({
+ type: eventType,
+ content,
+ });
+ }),
+ decryptEventIfNeeded: jest.fn(),
+ getRoom: jest.fn(),
+ getPushActionsForEvent: jest.fn(),
+ });
+ accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId);
+
+ testRoom = mkRoom(mockClient, roomId);
+
MockPlatform = mockPlatformPeg({
supportsNotifications: jest.fn().mockReturnValue(true),
maySendNotifications: jest.fn().mockReturnValue(true),
@@ -55,6 +71,8 @@ describe("Notifier", () => {
});
Notifier.isBodyEnabled = jest.fn().mockReturnValue(true);
+
+ mockClient.getRoom.mockReturnValue(testRoom);
});
describe("_displayPopupNotification", () => {
@@ -82,4 +100,73 @@ describe("Notifier", () => {
expect(Notifier.getSoundForRoom).toHaveBeenCalledTimes(count);
});
});
+
+ describe("group call notifications", () => {
+ beforeEach(() => {
+ jest.spyOn(SettingsStore, "getValue").mockReturnValue(true);
+ jest.spyOn(ToastStore.sharedInstance(), "addOrReplaceToast");
+
+ mockClient.getPushActionsForEvent.mockReturnValue({
+ notify: true,
+ tweaks: {},
+ });
+
+ Notifier.onSyncStateChange("SYNCING");
+ });
+
+ afterEach(() => {
+ jest.resetAllMocks();
+ });
+
+ const callOnEvent = (type?: string) => {
+ const callEvent = {
+ getContent: () => { },
+ getRoomId: () => roomId,
+ isBeingDecrypted: () => false,
+ isDecryptionFailure: () => false,
+ getSender: () => "@alice:foo",
+ getType: () => type ?? ElementCall.CALL_EVENT_TYPE.name,
+ getStateKey: () => "state_key",
+ } as unknown as MatrixEvent;
+
+ Notifier.onEvent(callEvent);
+ return callEvent;
+ };
+
+ const setGroupCallsEnabled = (val: boolean) => {
+ jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
+ if (name === "feature_group_calls") return val;
+ });
+ };
+
+ it("should show toast when group calls are supported", () => {
+ setGroupCallsEnabled(true);
+
+ const callEvent = callOnEvent();
+
+ expect(ToastStore.sharedInstance().addOrReplaceToast).toHaveBeenCalledWith(expect.objectContaining({
+ key: `call_${callEvent.getStateKey()}`,
+ priority: 100,
+ component: IncomingCallToast,
+ bodyClassName: "mx_IncomingCallToast",
+ props: { callEvent },
+ }));
+ });
+
+ it("should not show toast when group calls are not supported", () => {
+ setGroupCallsEnabled(false);
+
+ callOnEvent();
+
+ expect(ToastStore.sharedInstance().addOrReplaceToast).not.toHaveBeenCalled();
+ });
+
+ it("should not show toast when calling with non-group call event", () => {
+ setGroupCallsEnabled(true);
+
+ callOnEvent("event_type");
+
+ expect(ToastStore.sharedInstance().addOrReplaceToast).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/test/TextForEvent-test.ts b/test/TextForEvent-test.ts
index c99fb56571..27f3090d3e 100644
--- a/test/TextForEvent-test.ts
+++ b/test/TextForEvent-test.ts
@@ -14,15 +14,17 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
-import { EventType, MatrixEvent, RoomMember } from "matrix-js-sdk/src/matrix";
+import { EventType, MatrixClient, MatrixEvent, Room, RoomMember } from "matrix-js-sdk/src/matrix";
import TestRenderer from 'react-test-renderer';
import { ReactElement } from "react";
+import { mocked } from "jest-mock";
import { getSenderName, textForEvent } from "../src/TextForEvent";
import SettingsStore from "../src/settings/SettingsStore";
-import { createTestClient } from './test-utils';
+import { createTestClient, stubClient } from './test-utils';
import { MatrixClientPeg } from '../src/MatrixClientPeg';
import UserIdentifierCustomisations from '../src/customisations/UserIdentifier';
+import { ElementCall } from "../src/models/Call";
jest.mock("../src/settings/SettingsStore");
jest.mock('../src/customisations/UserIdentifier', () => ({
@@ -444,4 +446,42 @@ describe('TextForEvent', () => {
expect(textForEvent(messageEvent)).toEqual('@a: test message');
});
});
+
+ describe("textForCallEvent()", () => {
+ let mockClient: MatrixClient;
+ let callEvent: MatrixEvent;
+
+ beforeEach(() => {
+ stubClient();
+ mockClient = MatrixClientPeg.get();
+
+ mocked(mockClient.getRoom).mockReturnValue({
+ name: "Test room",
+ } as unknown as Room);
+
+ callEvent = {
+ getRoomId: jest.fn(),
+ getType: jest.fn(),
+ isState: jest.fn().mockReturnValue(true),
+ } as unknown as MatrixEvent;
+ });
+
+ describe.each(ElementCall.CALL_EVENT_TYPE.names)("eventType=%s", (eventType: string) => {
+ beforeEach(() => {
+ mocked(callEvent).getType.mockReturnValue(eventType);
+ });
+
+ it("returns correct message for call event when supported", () => {
+ expect(textForEvent(callEvent)).toEqual('Video call started in Test room.');
+ });
+
+ it("returns correct message for call event when supported", () => {
+ mocked(mockClient).supportsVoip.mockReturnValue(false);
+
+ expect(textForEvent(callEvent)).toEqual(
+ 'Video call started in Test room. (not supported by this browser)',
+ );
+ });
+ });
+ });
});
diff --git a/test/components/views/context_menus/MessageContextMenu-test.tsx b/test/components/views/context_menus/MessageContextMenu-test.tsx
index 38c646cfe8..10017376bb 100644
--- a/test/components/views/context_menus/MessageContextMenu-test.tsx
+++ b/test/components/views/context_menus/MessageContextMenu-test.tsx
@@ -27,7 +27,7 @@ import {
EventType,
} from 'matrix-js-sdk/src/matrix';
import { ExtensibleEvent, MessageEvent, M_POLL_KIND_DISCLOSED, PollStartEvent } from 'matrix-events-sdk';
-import { Thread } from "matrix-js-sdk/src/models/thread";
+import { FeatureSupport, Thread } from "matrix-js-sdk/src/models/thread";
import { mocked } from "jest-mock";
import { act } from '@testing-library/react';
@@ -469,7 +469,7 @@ describe('MessageContextMenu', () => {
const eventContent = MessageEvent.from("hello");
const mxEvent = new MatrixEvent(eventContent.serialize());
- Thread.hasServerSideSupport = true;
+ Thread.hasServerSideSupport = FeatureSupport.Stable;
const context = {
canSendMessages: true,
};
diff --git a/test/components/views/messages/MessageActionBar-test.tsx b/test/components/views/messages/MessageActionBar-test.tsx
index d4dcb1a2df..670d39ec64 100644
--- a/test/components/views/messages/MessageActionBar-test.tsx
+++ b/test/components/views/messages/MessageActionBar-test.tsx
@@ -25,7 +25,7 @@ import {
MsgType,
Room,
} from 'matrix-js-sdk/src/matrix';
-import { Thread } from 'matrix-js-sdk/src/models/thread';
+import { FeatureSupport, Thread } from 'matrix-js-sdk/src/models/thread';
import MessageActionBar from '../../../../src/components/views/messages/MessageActionBar';
import {
@@ -388,13 +388,13 @@ describe('', () => {
describe('thread button', () => {
beforeEach(() => {
- Thread.setServerSideSupport(true, false);
+ Thread.setServerSideSupport(FeatureSupport.Stable);
});
describe('when threads feature is not enabled', () => {
it('does not render thread button when threads does not have server support', () => {
jest.spyOn(SettingsStore, 'getValue').mockReturnValue(false);
- Thread.setServerSideSupport(false, false);
+ Thread.setServerSideSupport(FeatureSupport.None);
const { queryByLabelText } = getComponent({ mxEvent: alicesMessageEvent });
expect(queryByLabelText('Reply in thread')).toBeFalsy();
});
diff --git a/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap b/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap
index 05c0ca8c98..96ec4a13bd 100644
--- a/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap
+++ b/test/components/views/settings/__snapshots__/DevicesPanel-test.tsx.snap
@@ -115,10 +115,14 @@ exports[` renders device panel with devices 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders device panel with devices 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders device panel with devices 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
', () => {
const defaultProps = {
@@ -41,4 +42,33 @@ describe('', () => {
const { container } = render(getComponent({ isSelected: true }));
expect(container).toMatchSnapshot();
});
+
+ it('renders an unknown device icon when no device type given', () => {
+ const { getByLabelText } = render(getComponent());
+ expect(getByLabelText('Unknown session type')).toBeTruthy();
+ });
+
+ it('renders a desktop device type', () => {
+ const deviceType = DeviceType.Desktop;
+ const { getByLabelText } = render(getComponent({ deviceType }));
+ expect(getByLabelText('Desktop session')).toBeTruthy();
+ });
+
+ it('renders a web device type', () => {
+ const deviceType = DeviceType.Web;
+ const { getByLabelText } = render(getComponent({ deviceType }));
+ expect(getByLabelText('Web session')).toBeTruthy();
+ });
+
+ it('renders a mobile device type', () => {
+ const deviceType = DeviceType.Mobile;
+ const { getByLabelText } = render(getComponent({ deviceType }));
+ expect(getByLabelText('Mobile session')).toBeTruthy();
+ });
+
+ it('renders an unknown device type', () => {
+ const deviceType = DeviceType.Unknown;
+ const { getByLabelText } = render(getComponent({ deviceType }));
+ expect(getByLabelText('Unknown session type')).toBeTruthy();
+ });
});
diff --git a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap
index a35b6bcbc2..73d7a10130 100644
--- a/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap
+++ b/test/components/views/settings/devices/__snapshots__/CurrentDeviceSection-test.tsx.snap
@@ -154,10 +154,14 @@ exports[` renders device and correct security card when
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders device and correct security card when
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders a device with no metadata 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders a verified device with no metadata 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders display name with a tooltip 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
separates metadata with a dot 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders a verified device 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders an unverified device 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders correctly when selected 1`] = `
class="mx_DeviceTypeIcon mx_DeviceTypeIcon_selected"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders unselected device tile with checkbox 1
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders current session section with a verified s
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
renders current session section with an unverifie
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
sets device verification status correctly 1`] = `
class="mx_DeviceTypeIcon"
>
+ class="mx_DeviceTypeIcon_deviceIconWrapper"
+ >
+
+
{
+ useMockedCalls();
+ Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: () => [] } });
+ jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => { });
+
+ let client: Mocked;
+ let room: Room;
+ let alice: RoomMember;
+ let bob: RoomMember;
+ let call: MockedCall;
+ let widget: Widget;
+ const dmRoomMap = {
+ getUserIdForRoomId: jest.fn(),
+ } as unknown as DMRoomMap;
+ const toastStore = {
+ dismissToast: jest.fn(),
+ } as unknown as ToastStore;
+
+ beforeEach(async () => {
+ stubClient();
+ client = mocked(MatrixClientPeg.get());
+
+ room = new Room("!1:example.org", client, "@alice:example.org");
+
+ alice = mkRoomMember(room.roomId, "@alice:example.org");
+ bob = mkRoomMember(room.roomId, "@bob:example.org");
+
+ client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
+ client.getRooms.mockReturnValue([room]);
+ client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
+
+ await Promise.all([CallStore.instance, WidgetMessagingStore.instance].map(
+ store => setupAsyncStoreWithClient(store, client),
+ ));
+
+ MockedCall.create(room, "1");
+ const maybeCall = CallStore.instance.get(room.roomId);
+ if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
+ call = maybeCall;
+
+ widget = new Widget(call.widget);
+ WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
+ stop: () => { },
+ } as unknown as ClientWidgetApi);
+
+ jest.spyOn(DMRoomMap, "shared").mockReturnValue(dmRoomMap);
+ jest.spyOn(ToastStore, "sharedInstance").mockReturnValue(toastStore);
+ });
+
+ afterEach(async () => {
+ cleanup(); // Unmount before we do any cleanup that might update the component
+ call.destroy();
+ WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
+ await Promise.all([CallStore.instance, WidgetMessagingStore.instance].map(resetAsyncStoreWithClient));
+ jest.restoreAllMocks();
+ });
+
+ const renderToast = () => { render(); };
+
+ it("correctly shows all the information", () => {
+ call.participants = new Set([alice, bob]);
+ renderToast();
+
+ screen.getByText("Video call started");
+ screen.getByText("Video");
+ screen.getByLabelText("2 participants");
+
+ screen.getByRole("button", { name: "Join" });
+ screen.getByRole("button", { name: "Close" });
+ });
+
+ it("correctly renders toast without a call", () => {
+ call.destroy();
+ renderToast();
+
+ screen.getByText("Video call started");
+ screen.getByText("Video");
+
+ screen.getByRole("button", { name: "Join" });
+ screen.getByRole("button", { name: "Close" });
+ });
+
+ it("joins the call and closes the toast", async () => {
+ renderToast();
+
+ const dispatcherSpy = jest.fn();
+ const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
+
+ fireEvent.click(screen.getByRole("button", { name: "Join" }));
+ await waitFor(() => expect(dispatcherSpy).toHaveBeenCalledWith({
+ action: Action.ViewRoom,
+ room_id: room.roomId,
+ view_call: true,
+ }));
+ await waitFor(() => expect(toastStore.dismissToast).toHaveBeenCalledWith(
+ getIncomingCallToastKey(call.event.getStateKey()!),
+ ));
+
+ defaultDispatcher.unregister(dispatcherRef);
+ });
+
+ it("closes the toast", async () => {
+ renderToast();
+
+ const dispatcherSpy = jest.fn();
+ const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
+
+ fireEvent.click(screen.getByRole("button", { name: "Close" }));
+ await waitFor(() => expect(toastStore.dismissToast).toHaveBeenCalledWith(
+ getIncomingCallToastKey(call.event.getStateKey()!),
+ ));
+
+ defaultDispatcher.unregister(dispatcherRef);
+ });
+});