Merge remote-tracking branch 'origin/develop' into feat/matrix-wysisyg-integration

This commit is contained in:
Florian Duros
2022-10-10 17:04:27 +02:00
150 changed files with 3632 additions and 980 deletions
+156 -17
View File
@@ -14,28 +14,31 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { mocked, MockedObject } from "jest-mock";
import { ClientEvent, MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { SyncState } from "matrix-js-sdk/src/sync";
import BasePlatform from "../src/BasePlatform";
import { ElementCall } from "../src/models/Call";
import Notifier from "../src/Notifier";
import { getLocalNotificationAccountDataEventType } from "../src/utils/notifications";
import SettingsStore from "../src/settings/SettingsStore";
import ToastStore from "../src/stores/ToastStore";
import {
createLocalNotificationSettingsIfNeeded,
getLocalNotificationAccountDataEventType,
} from "../src/utils/notifications";
import { getMockClientWithEventEmitter, mkEvent, mkRoom, mockPlatformPeg } from "./test-utils";
import { IncomingCallToast } from "../src/toasts/IncomingCallToast";
jest.mock("../src/utils/notifications", () => ({
// @ts-ignore
...jest.requireActual("../src/utils/notifications"),
createLocalNotificationSettingsIfNeeded: jest.fn(),
}));
describe("Notifier", () => {
let MockPlatform;
let accountDataStore = {};
const 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,
});
}),
});
const accountDataEventKey = getLocalNotificationAccountDataEventType(mockClient.deviceId);
const roomId = "!room1:server";
const testEvent = mkEvent({
event: true,
@@ -44,10 +47,33 @@ describe("Notifier", () => {
room: roomId,
content: {},
});
const testRoom = mkRoom(mockClient, roomId);
let MockPlatform: MockedObject<BasePlatform>;
let mockClient: MockedObject<MatrixClient>;
let testRoom: MockedObject<Room>;
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 +81,8 @@ describe("Notifier", () => {
});
Notifier.isBodyEnabled = jest.fn().mockReturnValue(true);
mockClient.getRoom.mockReturnValue(testRoom);
});
describe("_displayPopupNotification", () => {
@@ -82,4 +110,115 @@ 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(SyncState.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();
});
});
describe('local notification settings', () => {
const createLocalNotificationSettingsIfNeededMock = mocked(createLocalNotificationSettingsIfNeeded);
let hasStartedNotiferBefore = false;
beforeEach(() => {
// notifier defines some listener functions in start
// and references them in stop
// so blows up if stopped before it was started
if (hasStartedNotiferBefore) {
Notifier.stop();
}
Notifier.start();
hasStartedNotiferBefore = true;
createLocalNotificationSettingsIfNeededMock.mockClear();
});
afterAll(() => {
Notifier.stop();
});
it('does not create local notifications event after a sync error', () => {
mockClient.emit(ClientEvent.Sync, SyncState.Error, SyncState.Syncing);
expect(createLocalNotificationSettingsIfNeededMock).not.toHaveBeenCalled();
});
it('does not create local notifications event after sync stops', () => {
mockClient.emit(ClientEvent.Sync, SyncState.Stopped, SyncState.Syncing);
expect(createLocalNotificationSettingsIfNeededMock).not.toHaveBeenCalled();
});
it('does not create local notifications event after a cached sync', () => {
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing, {
fromCache: true,
});
expect(createLocalNotificationSettingsIfNeededMock).not.toHaveBeenCalled();
});
it('creates local notifications event after a non-cached sync', () => {
mockClient.emit(ClientEvent.Sync, SyncState.Syncing, SyncState.Syncing, {});
expect(createLocalNotificationSettingsIfNeededMock).toHaveBeenCalled();
});
});
});
+42 -2
View File
@@ -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)',
);
});
});
});
});
@@ -1,9 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_RoomHeader_e2eIcon\\"><div class=\\"mx_E2EIcon mx_E2EIcon_normal\\"></div></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><div class=\\"mx_RoomView_body\\"><div class=\\"mx_LargeLoader\\"><div class=\\"mx_Spinner\\"><div class=\\"mx_Spinner_icon\\" style=\\"width: 45px; height: 45px;\\" aria-label=\\"Loading...\\" role=\\"progressbar\\"></div></div><div class=\\"mx_LargeLoader_text\\">We're creating a room with @user:example.com</div></div></div></div>"`;
exports[`RoomView for a local room in state CREATING should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><div class=\\"mx_RoomView_body\\"><div class=\\"mx_LargeLoader\\"><div class=\\"mx_Spinner\\"><div class=\\"mx_Spinner_icon\\" style=\\"width: 45px; height: 45px;\\" aria-label=\\"Loading...\\" role=\\"progressbar\\"></div></div><div class=\\"mx_LargeLoader_text\\">We're creating a room with @user:example.com</div></div></div></div>"`;
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_RoomHeader_e2eIcon\\"><div class=\\"mx_E2EIcon mx_E2EIcon_normal\\"></div></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_RoomStatusBar mx_RoomStatusBar_unsentMessages\\"><div role=\\"alert\\"><div class=\\"mx_RoomStatusBar_unsentBadge\\"><div class=\\"mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char\\"><span class=\\"mx_NotificationBadge_count\\">!</span></div></div><div><div class=\\"mx_RoomStatusBar_unsentTitle\\">Some of your messages have not been sent</div></div><div class=\\"mx_RoomStatusBar_unsentButtonBar\\"><div role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_RoomStatusBar_unsentRetry\\">Retry</div></div></div></div></main></div>"`;
exports[`RoomView for a local room in state ERROR should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_RoomStatusBar mx_RoomStatusBar_unsentMessages\\"><div role=\\"alert\\"><div class=\\"mx_RoomStatusBar_unsentBadge\\"><div class=\\"mx_NotificationBadge mx_NotificationBadge_visible mx_NotificationBadge_highlighted mx_NotificationBadge_2char\\"><span class=\\"mx_NotificationBadge_count\\">!</span></div></div><div><div class=\\"mx_RoomStatusBar_unsentTitle\\">Some of your messages have not been sent</div></div><div class=\\"mx_RoomStatusBar_unsentButtonBar\\"><div role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_RoomStatusBar_unsentRetry\\">Retry</div></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_RoomHeader_e2eIcon\\"><div class=\\"mx_E2EIcon mx_E2EIcon_normal\\"></div></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><li class=\\"mx_NewRoomIntro\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon_warning\\"><div class=\\"mx_EventTileBubble_title\\">End-to-end encryption isn't enabled</div><div class=\\"mx_EventTileBubble_subtitle\\"><span> Your private messages are normally encrypted, but this room isn't. Usually this is due to an unsupported device or method being used, like email invites. </span></div></div><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_RoomHeader_e2eIcon\\"><div class=\\"mx_E2EIcon mx_E2EIcon_normal\\"></div></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon\\"><div class=\\"mx_EventTileBubble_title\\">Encryption enabled</div><div class=\\"mx_EventTileBubble_subtitle\\">Messages in this chat will be end-to-end encrypted.</div></div><li class=\\"mx_NewRoomIntro\\"><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
exports[`RoomView for a local room in state NEW that is encrypted should match the snapshot 1`] = `"<div class=\\"mx_RoomView mx_RoomView--local\\"><header class=\\"mx_RoomHeader light-panel\\"><div class=\\"mx_RoomHeader_wrapper\\"><div class=\\"mx_RoomHeader_avatar\\"><div class=\\"mx_DecoratedRoomAvatar\\"><span class=\\"mx_BaseAvatar\\" role=\\"presentation\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 15.600000000000001px; width: 24px; line-height: 24px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 24px; height: 24px;\\" aria-hidden=\\"true\\"></span></div></div><div class=\\"mx_E2EIcon mx_E2EIcon_normal mx_RoomHeader_icon\\"></div><div class=\\"mx_RoomHeader_name mx_RoomHeader_name--textonly\\"><div dir=\\"auto\\" class=\\"mx_RoomHeader_nametext\\" title=\\"@user:example.com\\" role=\\"heading\\" aria-level=\\"1\\">@user:example.com</div></div><div class=\\"mx_RoomHeader_topic mx_RoomTopic\\" dir=\\"auto\\"><div tabindex=\\"0\\"><div><span dir=\\"auto\\"></span></div></div></div></div></header><main class=\\"mx_RoomView_body\\"><div class=\\"mx_RoomView_timeline\\"><div class=\\"mx_AutoHideScrollbar mx_ScrollPanel mx_RoomView_messagePanel\\" tabindex=\\"-1\\"><div class=\\"mx_RoomView_messageListWrapper\\"><ol class=\\"mx_RoomView_MessageList\\" aria-live=\\"polite\\" style=\\"height: 400px;\\"><div class=\\"mx_EventTileBubble mx_cryptoEvent mx_cryptoEvent_icon\\"><div class=\\"mx_EventTileBubble_title\\">Encryption enabled</div><div class=\\"mx_EventTileBubble_subtitle\\">Messages in this chat will be end-to-end encrypted.</div></div><li class=\\"mx_NewRoomIntro\\"><span aria-label=\\"Avatar\\" aria-live=\\"off\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_BaseAvatar\\"><span class=\\"mx_BaseAvatar_initial\\" aria-hidden=\\"true\\" style=\\"font-size: 33.800000000000004px; width: 52px; line-height: 52px;\\">U</span><img class=\\"mx_BaseAvatar_image\\" src=\\"data:image/png;base64,00\\" alt=\\"\\" style=\\"width: 52px; height: 52px;\\" aria-hidden=\\"true\\"></span><h2>@user:example.com</h2><p><span>Send your first message to invite <b>@user:example.com</b> to chat</span></p></li></ol></div></div></div><div class=\\"mx_MessageComposer\\"><div class=\\"mx_MessageComposer_wrapper\\"><div class=\\"mx_MessageComposer_row\\"><div class=\\"mx_SendMessageComposer\\"><div class=\\"mx_BasicMessageComposer\\"><div class=\\"mx_MessageComposerFormatBar\\"><button type=\\"button\\" aria-label=\\"Bold\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconBold\\"></button><button type=\\"button\\" aria-label=\\"Italics\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconItalic\\"></button><button type=\\"button\\" aria-label=\\"Strikethrough\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconStrikethrough\\"></button><button type=\\"button\\" aria-label=\\"Code block\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconCode\\"></button><button type=\\"button\\" aria-label=\\"Quote\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconQuote\\"></button><button type=\\"button\\" aria-label=\\"Insert link\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposerFormatBar_button mx_MessageComposerFormatBar_buttonIconInsertLink\\"></button></div><div class=\\"mx_BasicMessageComposer_input mx_BasicMessageComposer_input_shouldShowPillAvatar mx_BasicMessageComposer_inputEmpty\\" contenteditable=\\"true\\" tabindex=\\"0\\" aria-label=\\"Send a message…\\" role=\\"textbox\\" aria-multiline=\\"true\\" aria-autocomplete=\\"list\\" aria-haspopup=\\"listbox\\" dir=\\"auto\\" aria-disabled=\\"false\\" style=\\"--placeholder: 'Send a message…';\\"><div><br></div></div></div></div><div aria-label=\\"Emoji\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_emoji\\"></div><div aria-label=\\"Attachment\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_upload\\"></div><div aria-label=\\"More options\\" role=\\"button\\" tabindex=\\"0\\" class=\\"mx_AccessibleButton mx_MessageComposer_button mx_MessageComposer_buttonMenu\\"></div><input type=\\"file\\" style=\\"display: none;\\" multiple=\\"\\"></div></div></div></main></div>"`;
@@ -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,
};
@@ -4,7 +4,7 @@ exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
role="tooltip"
style="display: block; top: 6px; left: 0px; transform: translate(-50%);"
style="display: block; top: 6px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))));"
>
<div
class="mx_Tooltip_chevron"
@@ -17,7 +17,7 @@ exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`]
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
role="tooltip"
style="display: block; top: -50px; left: 0px; transform: translate(-50%);"
style="display: block; top: -50px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))));"
>
<div
class="mx_Tooltip_chevron"
@@ -69,7 +69,7 @@ exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
<div
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
role="tooltip"
style="display: block; top: -6px; left: 0px; transform: translate(-50%, -100%);"
style="display: block; top: -6px; transform: translate(max(10px, min(calc(0px - 50%), calc(100vw - 100% - 10px))), -100%);"
>
<div
class="mx_Tooltip_chevron"
@@ -42,36 +42,53 @@ exports[`<LocationShareMenu /> with live location disabled goes to labs flag scr
Enable live location sharing
</span>
<_default
aria-label="Enable live location sharing"
checked={false}
onChange={[Function]}
title="Enable live location sharing"
>
<AccessibleButton
<AccessibleTooltipButton
aria-checked={false}
aria-disabled={false}
aria-label="Enable live location sharing"
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
element="div"
onClick={[Function]}
role="switch"
tabIndex={0}
title="Enable live location sharing"
>
<div
<AccessibleButton
aria-checked={false}
aria-disabled={false}
aria-label="Enable live location sharing"
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
element="div"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
className="mx_ToggleSwitch_ball"
/>
</div>
</AccessibleButton>
aria-checked={false}
aria-disabled={false}
aria-label="Enable live location sharing"
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
className="mx_ToggleSwitch_ball"
/>
</div>
</AccessibleButton>
</AccessibleTooltipButton>
</_default>
</div>
</LabelledToggleSwitch>
@@ -82,7 +82,7 @@ describe("CallEvent", () => {
));
MockedCall.create(room, "1");
const maybeCall = CallStore.instance.get(room.roomId);
const maybeCall = CallStore.instance.getCall(room.roomId);
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
call = maybeCall;
@@ -113,7 +113,7 @@ describe("CallEvent", () => {
});
it("shows placeholder info if the call isn't loaded yet", () => {
jest.spyOn(CallStore.instance, "get").mockReturnValue(null);
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(null);
jest.advanceTimersByTime(90000);
renderEvent();
@@ -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('<MessageActionBar />', () => {
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();
});
@@ -268,6 +268,7 @@ function createRoomState(room: Room, narrow: boolean): IRoomState {
liveTimeline: undefined,
resizing: false,
narrow,
activeCall: null,
};
}
+148 -5
View File
@@ -25,6 +25,8 @@ import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
import { CallType } from "matrix-js-sdk/src/webrtc/call";
import { ClientWidgetApi, Widget } from "matrix-widget-api";
import EventEmitter from "events";
import type { MatrixClient } from "matrix-js-sdk/src/client";
import type { MatrixEvent } from "matrix-js-sdk/src/models/event";
@@ -53,6 +55,10 @@ import LegacyCallHandler from "../../../../src/LegacyCallHandler";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
import WidgetStore from "../../../../src/stores/WidgetStore";
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import WidgetUtils from "../../../../src/utils/WidgetUtils";
import { ElementWidgetActions } from "../../../../src/stores/widgets/ElementWidgetActions";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../../src/MediaDeviceHandler";
describe('RoomHeader (Enzyme)', () => {
it('shows the room avatar in a room with only ourselves', () => {
@@ -173,13 +179,13 @@ describe('RoomHeader (Enzyme)', () => {
it("should render buttons if not passing showButtons (default true)", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room);
expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(1);
expect(wrapper.find(".mx_RoomHeader_button")).not.toHaveLength(0);
});
it("should not render buttons if passing showButtons = false", () => {
const room = createRoom({ name: "Room", isDm: false, userIds: [] });
const wrapper = mountHeader(room, { showButtons: false });
expect(wrapper.find(".mx_RoomHeader_buttons")).toHaveLength(0);
expect(wrapper.find(".mx_RoomHeader_button")).toHaveLength(0);
});
it("should render the room options context menu if not passing enableRoomOptionsMenu (default true)", () => {
@@ -252,6 +258,8 @@ function mountHeader(room: Room, propsOverride = {}, roomContext?: Partial<IRoom
searchScope: SearchScope.Room,
searchCount: 0,
},
viewingCall: false,
activeCall: null,
...propsOverride,
};
@@ -381,6 +389,12 @@ describe("RoomHeader (React Testing Library)", () => {
await Promise.all([CallStore.instance, WidgetStore.instance].map(
store => setupAsyncStoreWithClient(store, client),
));
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
[MediaDeviceKindEnum.AudioInput]: [],
[MediaDeviceKindEnum.VideoInput]: [],
[MediaDeviceKindEnum.AudioOutput]: [],
});
});
afterEach(async () => {
@@ -419,6 +433,32 @@ describe("RoomHeader (React Testing Library)", () => {
const mockLegacyCall = () => {
jest.spyOn(LegacyCallHandler.instance, "getCallForRoom").mockReturnValue({} as unknown as MatrixCall);
};
const withCall = async (fn: (call: ElementCall) => (void | Promise<void>)): Promise<void> => {
await ElementCall.create(room);
const call = CallStore.instance.getCall(room.roomId);
if (!(call instanceof ElementCall)) throw new Error("Failed to create call");
const widget = new Widget(call.widget);
const eventEmitter = new EventEmitter();
const messaging = {
on: eventEmitter.on.bind(eventEmitter),
off: eventEmitter.off.bind(eventEmitter),
once: eventEmitter.once.bind(eventEmitter),
emit: eventEmitter.emit.bind(eventEmitter),
stop: jest.fn(),
transport: {
send: jest.fn(),
reply: jest.fn(),
},
} as unknown as Mocked<ClientWidgetApi>;
WidgetMessagingStore.instance.storeMessaging(widget, call.roomId, messaging);
await fn(call);
call.destroy();
WidgetMessagingStore.instance.stopMessaging(widget, call.roomId);
};
const renderHeader = (props: Partial<RoomHeaderProps> = {}, roomContext: Partial<IRoomState> = {}) => {
render(
@@ -437,6 +477,8 @@ describe("RoomHeader (React Testing Library)", () => {
searchScope: SearchScope.Room,
searchCount: 0,
}}
viewingCall={false}
activeCall={null}
{...props}
/>
</RoomContext.Provider>,
@@ -463,7 +505,9 @@ describe("RoomHeader (React Testing Library)", () => {
+ "and there's an ongoing call",
async () => {
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } });
SdkConfig.put(
{ element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } },
);
await ElementCall.create(room);
renderHeader();
@@ -477,7 +521,9 @@ describe("RoomHeader (React Testing Library)", () => {
+ "use Element Call exclusively",
async () => {
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } });
SdkConfig.put(
{ element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } },
);
renderHeader();
expect(screen.queryByRole("button", { name: "Voice call" })).toBeNull();
@@ -499,7 +545,9 @@ describe("RoomHeader (React Testing Library)", () => {
+ "and the user lacks permission",
() => {
mockEnabledSettings(["showCallButtonsInComposer", "feature_group_calls"]);
SdkConfig.put({ element_call: { url: "https://call.element.io", use_exclusively: true } });
SdkConfig.put(
{ element_call: { url: "https://call.element.io", use_exclusively: true, brand: "Element Call" } },
);
mockEventPowerLevels({ [ElementCall.CALL_EVENT_TYPE.name]: 100 });
renderHeader();
@@ -724,4 +772,99 @@ describe("RoomHeader (React Testing Library)", () => {
expect(screen.getByRole("button", { name: "Voice call" })).toHaveAttribute("aria-disabled", "true");
expect(screen.getByRole("button", { name: "Video call" })).toHaveAttribute("aria-disabled", "true");
});
it("shows a close button when viewing a call lobby that returns to the timeline when pressed", async () => {
mockEnabledSettings(["feature_group_calls"]);
renderHeader({ viewingCall: true });
const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
fireEvent.click(screen.getByRole("button", { name: /close/i }));
await waitFor(() => expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: false,
}));
defaultDispatcher.unregister(dispatcherRef);
});
it("shows a reduce button when viewing a call that returns to the timeline when pressed", async () => {
mockEnabledSettings(["feature_group_calls"]);
await withCall(async call => {
renderHeader({ viewingCall: true, activeCall: call });
const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
fireEvent.click(screen.getByRole("button", { name: /timeline/i }));
await waitFor(() => expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: false,
}));
defaultDispatcher.unregister(dispatcherRef);
});
});
it("shows a layout button when viewing a call that shows a menu when pressed", async () => {
mockEnabledSettings(["feature_group_calls"]);
await withCall(async call => {
await call.connect();
const messaging = WidgetMessagingStore.instance.getMessagingForUid(WidgetUtils.getWidgetUid(call.widget));
renderHeader({ viewingCall: true, activeCall: call });
// Should start with Freedom selected
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
screen.getByRole("menuitemradio", { name: "Freedom", checked: true });
// Clicking Spotlight should tell the widget to switch and close the menu
fireEvent.click(screen.getByRole("menuitemradio", { name: "Spotlight" }));
expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
expect(screen.queryByRole("menu")).toBeNull();
// When the widget responds and the user reopens the menu, they should see Spotlight selected
act(() => {
messaging.emit(
`action:${ElementWidgetActions.SpotlightLayout}`,
new CustomEvent("widgetapirequest", { detail: { data: {} } }),
);
});
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
screen.getByRole("menuitemradio", { name: "Spotlight", checked: true });
// Now try switching back to Freedom
fireEvent.click(screen.getByRole("menuitemradio", { name: "Freedom" }));
expect(mocked(messaging.transport).send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {});
expect(screen.queryByRole("menu")).toBeNull();
// When the widget responds and the user reopens the menu, they should see Freedom selected
act(() => {
messaging.emit(
`action:${ElementWidgetActions.TileLayout}`,
new CustomEvent("widgetapirequest", { detail: { data: {} } }),
);
});
fireEvent.click(screen.getByRole("button", { name: /layout/i }));
screen.getByRole("menuitemradio", { name: "Freedom", checked: true });
});
});
it("shows an invite button in video rooms", () => {
mockEnabledSettings(["feature_video_rooms", "feature_element_call_video_rooms"]);
mockRoomType(RoomType.UnstableCall);
const onInviteClick = jest.fn();
renderHeader({ onInviteClick, viewingCall: true });
fireEvent.click(screen.getByRole("button", { name: /invite/i }));
expect(onInviteClick).toHaveBeenCalled();
});
it("hides the invite button in non-video rooms when viewing a call", () => {
renderHeader({ onInviteClick: () => {}, viewingCall: true });
expect(screen.queryByRole("button", { name: /invite/i })).toBeNull();
});
});
@@ -77,7 +77,7 @@ describe("RoomTile", () => {
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
MockedCall.create(room, "1");
call = CallStore.instance.get(room.roomId) as MockedCall;
call = CallStore.instance.getCall(room.roomId) as MockedCall;
widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
@@ -91,6 +91,7 @@ describe('<SendMessageComposer/>', () => {
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
};
describe("createMessageContent", () => {
const permalinkCreator = jest.fn() as any;
@@ -112,16 +112,20 @@ exports[`<DevicesPanel /> renders device panel with devices 1`] = `
data-testid="device-tile-device_1"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -235,16 +239,20 @@ exports[`<DevicesPanel /> renders device panel with devices 1`] = `
data-testid="device-tile-device_2"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -317,16 +325,20 @@ exports[`<DevicesPanel /> renders device panel with devices 1`] = `
data-testid="device-tile-device_3"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -18,37 +18,54 @@ exports[`<Notifications /> main notification switches email switches renders ema
Enable email notifications for tester@test.com
</span>
<_default
aria-label="Enable email notifications for tester@test.com"
checked={false}
disabled={false}
onChange={[Function]}
title="Enable email notifications for tester@test.com"
>
<AccessibleButton
<AccessibleTooltipButton
aria-checked={false}
aria-disabled={false}
aria-label="Enable email notifications for tester@test.com"
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
element="div"
onClick={[Function]}
role="switch"
tabIndex={0}
title="Enable email notifications for tester@test.com"
>
<div
<AccessibleButton
aria-checked={false}
aria-disabled={false}
aria-label="Enable email notifications for tester@test.com"
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
element="div"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
className="mx_ToggleSwitch_ball"
/>
</div>
</AccessibleButton>
aria-checked={false}
aria-disabled={false}
aria-label="Enable email notifications for tester@test.com"
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
className="mx_ToggleSwitch_ball"
/>
</div>
</AccessibleButton>
</AccessibleTooltipButton>
</_default>
</div>
</LabelledToggleSwitch>
@@ -84,37 +101,54 @@ exports[`<Notifications /> main notification switches renders only enable notifi
</Caption>
</span>
<_default
aria-label="Enable notifications for this account"
checked={false}
disabled={false}
onChange={[Function]}
title="Enable notifications for this account"
>
<AccessibleButton
<AccessibleTooltipButton
aria-checked={false}
aria-disabled={false}
aria-label="Enable notifications for this account"
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
element="div"
onClick={[Function]}
role="switch"
tabIndex={0}
title="Enable notifications for this account"
>
<div
<AccessibleButton
aria-checked={false}
aria-disabled={false}
aria-label="Enable notifications for this account"
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
className="mx_ToggleSwitch mx_ToggleSwitch_enabled"
element="div"
onBlur={[Function]}
onClick={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onFocus={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
className="mx_ToggleSwitch_ball"
/>
</div>
</AccessibleButton>
aria-checked={false}
aria-disabled={false}
aria-label="Enable notifications for this account"
className="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_enabled"
onBlur={[Function]}
onClick={[Function]}
onFocus={[Function]}
onKeyDown={[Function]}
onKeyUp={[Function]}
onMouseLeave={[Function]}
onMouseOver={[Function]}
role="switch"
tabIndex={0}
>
<div
className="mx_ToggleSwitch_ball"
/>
</div>
</AccessibleButton>
</AccessibleTooltipButton>
</_default>
</div>
</LabelledToggleSwitch>
@@ -19,6 +19,7 @@ import { fireEvent, render } from '@testing-library/react';
import { act } from 'react-dom/test-utils';
import CurrentDeviceSection from '../../../../../src/components/views/settings/devices/CurrentDeviceSection';
import { DeviceType } from '../../../../../src/utils/device/parseUserAgent';
describe('<CurrentDeviceSection />', () => {
const deviceId = 'alices_device';
@@ -26,10 +27,12 @@ describe('<CurrentDeviceSection />', () => {
const alicesVerifiedDevice = {
device_id: deviceId,
isVerified: false,
deviceType: DeviceType.Unknown,
};
const alicesUnverifiedDevice = {
device_id: deviceId,
isVerified: false,
deviceType: DeviceType.Unknown,
};
const defaultProps = {
@@ -19,6 +19,7 @@ import { fireEvent, render, RenderResult } from '@testing-library/react';
import { DeviceDetailHeading } from '../../../../../src/components/views/settings/devices/DeviceDetailHeading';
import { flushPromisesWithFakeTimers } from '../../../../test-utils';
import { DeviceType } from '../../../../../src/utils/device/parseUserAgent';
jest.useFakeTimers();
@@ -27,6 +28,7 @@ describe('<DeviceDetailHeading />', () => {
device_id: '123',
display_name: 'My device',
isVerified: true,
deviceType: DeviceType.Unknown,
};
const defaultProps = {
device,
@@ -20,11 +20,13 @@ import { PUSHER_ENABLED } from 'matrix-js-sdk/src/@types/event';
import DeviceDetails from '../../../../../src/components/views/settings/devices/DeviceDetails';
import { mkPusher } from '../../../../test-utils/test-utils';
import { DeviceType } from '../../../../../src/utils/device/parseUserAgent';
describe('<DeviceDetails />', () => {
const baseDevice = {
device_id: 'my-device',
isVerified: false,
deviceType: DeviceType.Unknown,
};
const defaultProps = {
device: baseDevice,
@@ -58,7 +60,10 @@ describe('<DeviceDetails />', () => {
display_name: 'My Device',
last_seen_ip: '123.456.789',
last_seen_ts: now - 60000000,
clientName: 'Element Web',
appName: 'Element Web',
client: 'Firefox 100',
deviceModel: 'Iphone X',
deviceOperatingSystem: 'Windows 95',
};
const { container } = render(getComponent({ device }));
expect(container).toMatchSnapshot();
@@ -19,12 +19,14 @@ import { render } from '@testing-library/react';
import { IMyDevice } from 'matrix-js-sdk/src/matrix';
import DeviceTile from '../../../../../src/components/views/settings/devices/DeviceTile';
import { DeviceType } from '../../../../../src/utils/device/parseUserAgent';
describe('<DeviceTile />', () => {
const defaultProps = {
device: {
device_id: '123',
isVerified: false,
deviceType: DeviceType.Unknown,
},
};
const getComponent = (props = {}) => (
@@ -1,44 +0,0 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { DeviceType } from '../../../../../src/components/views/settings/devices/DeviceType';
describe('<DeviceType />', () => {
const defaultProps = {
isVerified: false,
isSelected: false,
};
const getComponent = (props = {}) =>
<DeviceType {...defaultProps} {...props} />;
it('renders an unverified device', () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it('renders a verified device', () => {
const { container } = render(getComponent({ isVerified: true }));
expect(container).toMatchSnapshot();
});
it('renders correctly when selected', () => {
const { container } = render(getComponent({ isSelected: true }));
expect(container).toMatchSnapshot();
});
});
@@ -0,0 +1,74 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { DeviceTypeIcon } from '../../../../../src/components/views/settings/devices/DeviceTypeIcon';
import { DeviceType } from '../../../../../src/utils/device/parseUserAgent';
describe('<DeviceTypeIcon />', () => {
const defaultProps = {
isVerified: false,
isSelected: false,
};
const getComponent = (props = {}) =>
<DeviceTypeIcon {...defaultProps} {...props} />;
it('renders an unverified device', () => {
const { container } = render(getComponent());
expect(container).toMatchSnapshot();
});
it('renders a verified device', () => {
const { container } = render(getComponent({ isVerified: true }));
expect(container).toMatchSnapshot();
});
it('renders correctly when selected', () => {
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();
});
});
@@ -20,6 +20,7 @@ import { act, fireEvent, render } from '@testing-library/react';
import { FilteredDeviceList } from '../../../../../src/components/views/settings/devices/FilteredDeviceList';
import { DeviceSecurityVariation } from '../../../../../src/components/views/settings/devices/types';
import { flushPromises, mockPlatformPeg } from '../../../../test-utils';
import { DeviceType } from '../../../../../src/utils/device/parseUserAgent';
mockPlatformPeg();
@@ -31,14 +32,26 @@ describe('<FilteredDeviceList />', () => {
last_seen_ip: '123.456.789',
display_name: 'My Device',
isVerified: true,
deviceType: DeviceType.Unknown,
};
const unverifiedNoMetadata = { device_id: 'unverified-no-metadata', isVerified: false };
const verifiedNoMetadata = { device_id: 'verified-no-metadata', isVerified: true };
const hundredDaysOld = { device_id: '100-days-old', isVerified: true, last_seen_ts: Date.now() - (MS_DAY * 100) };
const unverifiedNoMetadata = {
device_id: 'unverified-no-metadata',
isVerified: false,
deviceType: DeviceType.Unknown };
const verifiedNoMetadata = {
device_id: 'verified-no-metadata',
isVerified: true,
deviceType: DeviceType.Unknown };
const hundredDaysOld = {
device_id: '100-days-old',
isVerified: true,
last_seen_ts: Date.now() - (MS_DAY * 100),
deviceType: DeviceType.Unknown };
const hundredDaysOldUnverified = {
device_id: 'unverified-100-days-old',
isVerified: false,
last_seen_ts: Date.now() - (MS_DAY * 100),
deviceType: DeviceType.Unknown,
};
const defaultProps = {
onFilterChange: jest.fn(),
@@ -19,6 +19,7 @@ import React from 'react';
import { act } from 'react-dom/test-utils';
import SelectableDeviceTile from '../../../../../src/components/views/settings/devices/SelectableDeviceTile';
import { DeviceType } from '../../../../../src/utils/device/parseUserAgent';
describe('<SelectableDeviceTile />', () => {
const device = {
@@ -26,6 +27,7 @@ describe('<SelectableDeviceTile />', () => {
device_id: 'my-device',
last_seen_ip: '123.456.789',
isVerified: false,
deviceType: DeviceType.Unknown,
};
const defaultProps = {
onClick: jest.fn(),
@@ -151,16 +151,20 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -267,16 +271,20 @@ exports[`<CurrentDeviceSection /> renders device and correct security card when
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -181,6 +181,18 @@ exports[`<DeviceDetails /> renders device with metadata 1`] = `
my-device
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Client
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Firefox 100
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
@@ -233,6 +245,30 @@ exports[`<DeviceDetails /> renders device with metadata 1`] = `
</tr>
</thead>
<tbody>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Model
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Iphone X
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
>
Operating system
</td>
<td
class="mxDeviceDetails_metadataValue"
>
Windows 95
</td>
</tr>
<tr>
<td
class="mxDeviceDetails_metadataLabel"
@@ -7,16 +7,20 @@ exports[`<DeviceTile /> renders a device with no metadata 1`] = `
data-testid="device-tile-123"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -58,16 +62,20 @@ exports[`<DeviceTile /> renders a verified device with no metadata 1`] = `
data-testid="device-tile-123"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -109,16 +117,20 @@ exports[`<DeviceTile /> renders display name with a tooltip 1`] = `
data-testid="device-tile-123"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -160,16 +172,20 @@ exports[`<DeviceTile /> separates metadata with a dot 1`] = `
data-testid="device-tile-123"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -1,58 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DeviceType /> renders a verified device 1`] = `
<div>
<div
class="mx_DeviceType"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
<div
aria-label="Verified"
class="mx_DeviceType_verificationIcon verified"
role="img"
/>
</div>
</div>
`;
exports[`<DeviceType /> renders an unverified device 1`] = `
<div>
<div
class="mx_DeviceType"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
role="img"
/>
</div>
</div>
`;
exports[`<DeviceType /> renders correctly when selected 1`] = `
<div>
<div
class="mx_DeviceType mx_DeviceType_selected"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
role="img"
/>
</div>
</div>
`;
@@ -0,0 +1,70 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<DeviceTypeIcon /> renders a verified device 1`] = `
<div>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Verified"
class="mx_DeviceTypeIcon_verificationIcon verified"
role="img"
/>
</div>
</div>
`;
exports[`<DeviceTypeIcon /> renders an unverified device 1`] = `
<div>
<div
class="mx_DeviceTypeIcon"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
</div>
`;
exports[`<DeviceTypeIcon /> renders correctly when selected 1`] = `
<div>
<div
class="mx_DeviceTypeIcon mx_DeviceTypeIcon_selected"
>
<div
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
</div>
`;
@@ -39,16 +39,20 @@ exports[`<SelectableDeviceTile /> renders unselected device tile with checkbox 1
data-testid="device-tile-my-device"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -20,18 +20,38 @@ import {
import {
DeviceSecurityVariation,
} from "../../../../../src/components/views/settings/devices/types";
import { DeviceType } from "../../../../../src/utils/device/parseUserAgent";
const MS_DAY = 86400000;
describe('filterDevicesBySecurityRecommendation()', () => {
const unverifiedNoMetadata = { device_id: 'unverified-no-metadata', isVerified: false };
const verifiedNoMetadata = { device_id: 'verified-no-metadata', isVerified: true };
const hundredDaysOld = { device_id: '100-days-old', isVerified: true, last_seen_ts: Date.now() - (MS_DAY * 100) };
const unverifiedNoMetadata = {
device_id: 'unverified-no-metadata',
isVerified: false,
deviceType: DeviceType.Unknown,
};
const verifiedNoMetadata = {
device_id: 'verified-no-metadata',
isVerified: true,
deviceType: DeviceType.Unknown,
};
const hundredDaysOld = {
device_id: '100-days-old',
isVerified: true,
last_seen_ts: Date.now() - (MS_DAY * 100),
deviceType: DeviceType.Unknown,
};
const hundredDaysOldUnverified = {
device_id: 'unverified-100-days-old',
isVerified: false,
last_seen_ts: Date.now() - (MS_DAY * 100),
deviceType: DeviceType.Unknown,
};
const fiftyDaysOld = {
device_id: '50-days-old',
isVerified: true,
last_seen_ts: Date.now() - (MS_DAY * 50),
deviceType: DeviceType.Unknown,
};
const fiftyDaysOld = { device_id: '50-days-old', isVerified: true, last_seen_ts: Date.now() - (MS_DAY * 50) };
const devices = [
unverifiedNoMetadata,
@@ -16,30 +16,35 @@ limitations under the License.
import React from "react";
import { fireEvent, render, RenderResult } from "@testing-library/react";
import { EventType, MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { EventType } from "matrix-js-sdk/src/@types/event";
import RolesRoomSettingsTab from "../../../../../../src/components/views/settings/tabs/room/RolesRoomSettingsTab";
import { mkStubRoom, 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";
describe("RolesRoomSettingsTab", () => {
const roomId = "!room:example.com";
let rolesRoomSettingsTab: RenderResult;
let cli: MatrixClient;
const renderTab = (): RenderResult => {
return render(<RolesRoomSettingsTab roomId={roomId} />);
};
const getVoiceBroadcastsSelect = () => {
return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts']");
return renderTab().container.querySelector("select[label='Voice broadcasts']");
};
const getVoiceBroadcastsSelectedOption = () => {
return rolesRoomSettingsTab.container.querySelector("select[label='Voice broadcasts'] option:checked");
return renderTab().container.querySelector("select[label='Voice broadcasts'] option:checked");
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.get();
rolesRoomSettingsTab = render(<RolesRoomSettingsTab roomId={roomId} />);
mkStubRoom(roomId, "test room", cli);
});
@@ -66,4 +71,96 @@ describe("RolesRoomSettingsTab", () => {
);
});
});
describe("Element Call", () => {
const setGroupCallsEnabled = (val: boolean): void => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
if (name === "feature_group_calls") return val;
});
};
const getStartCallSelect = (tab: RenderResult) => {
return tab.container.querySelector("select[label='Start Element Call calls']");
};
const getStartCallSelectedOption = (tab: RenderResult) => {
return tab.container.querySelector("select[label='Start Element Call calls'] option:checked");
};
const getJoinCallSelect = (tab: RenderResult) => {
return tab.container.querySelector("select[label='Join Element Call calls']");
};
const getJoinCallSelectedOption = (tab: RenderResult) => {
return tab.container.querySelector("select[label='Join Element Call calls'] option:checked");
};
describe("Element Call enabled", () => {
beforeEach(() => {
setGroupCallsEnabled(true);
});
describe("Join Element calls", () => {
it("defaults to moderator for joining calls", () => {
expect(getJoinCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
});
it("can change joining calls power level", () => {
const tab = renderTab();
fireEvent.change(getJoinCallSelect(tab), {
target: { value: 0 },
});
expect(getJoinCallSelectedOption(tab)?.textContent).toBe("Default");
expect(cli.sendStateEvent).toHaveBeenCalledWith(
roomId,
EventType.RoomPowerLevels,
{
events: {
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
},
},
);
});
});
describe("Start Element calls", () => {
it("defaults to moderator for starting calls", () => {
expect(getStartCallSelectedOption(renderTab())?.textContent).toBe("Moderator");
});
it("can change starting calls power level", () => {
const tab = renderTab();
fireEvent.change(getStartCallSelect(tab), {
target: { value: 0 },
});
expect(getStartCallSelectedOption(tab)?.textContent).toBe("Default");
expect(cli.sendStateEvent).toHaveBeenCalledWith(
roomId,
EventType.RoomPowerLevels,
{
events: {
[ElementCall.CALL_EVENT_TYPE.name]: 0,
},
},
);
});
});
});
it("hides when group calls disabled", () => {
setGroupCallsEnabled(false);
const tab = renderTab();
expect(getStartCallSelect(tab)).toBeFalsy();
expect(getStartCallSelectedOption(tab)).toBeFalsy();
expect(getJoinCallSelect(tab)).toBeFalsy();
expect(getJoinCallSelectedOption(tab)).toBeFalsy();
});
});
});
@@ -0,0 +1,141 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { fireEvent, render, RenderResult, waitFor } from "@testing-library/react";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { EventType } from "matrix-js-sdk/src/@types/event";
import { JoinRule } from "matrix-js-sdk/src/@types/partials";
import { mkStubRoom, stubClient } from "../../../../../test-utils";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { VoipRoomSettingsTab } from "../../../../../../src/components/views/settings/tabs/room/VoipRoomSettingsTab";
import { ElementCall } from "../../../../../../src/models/Call";
describe("RolesRoomSettingsTab", () => {
const roomId = "!room:example.com";
let cli: MatrixClient;
let room: Room;
const renderTab = (): RenderResult => {
return render(<VoipRoomSettingsTab roomId={roomId} />);
};
beforeEach(() => {
stubClient();
cli = MatrixClientPeg.get();
room = mkStubRoom(roomId, "test room", cli);
jest.spyOn(cli, "sendStateEvent");
jest.spyOn(cli, "getRoom").mockReturnValue(room);
});
describe("Element Call", () => {
const mockPowerLevels = (events): void => {
jest.spyOn(room.currentState, "getStateEvents").mockReturnValue({
getContent: () => ({
events,
}),
} as unknown as MatrixEvent);
};
const getElementCallSwitch = (tab: RenderResult): HTMLElement => {
return tab.container.querySelector("[data-testid='element-call-switch']");
};
describe("correct state", () => {
it("shows enabled when call member power level is 0", () => {
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 });
const tab = renderTab();
expect(getElementCallSwitch(tab).querySelector("[aria-checked='true']")).toBeTruthy();
});
it.each([1, 50, 100])("shows disabled when call member power level is 0", (level: number) => {
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: level });
const tab = renderTab();
expect(getElementCallSwitch(tab).querySelector("[aria-checked='false']")).toBeTruthy();
});
});
describe("enabling/disabling", () => {
describe("enabling Element calls", () => {
beforeEach(() => {
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 100 });
});
it("enables Element calls in public room", async () => {
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Public);
const tab = renderTab();
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch"));
await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomPowerLevels,
expect.objectContaining({
events: {
[ElementCall.CALL_EVENT_TYPE.name]: 50,
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
},
}),
));
});
it("enables Element calls in private room", async () => {
jest.spyOn(room, "getJoinRule").mockReturnValue(JoinRule.Invite);
const tab = renderTab();
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch"));
await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomPowerLevels,
expect.objectContaining({
events: {
[ElementCall.CALL_EVENT_TYPE.name]: 0,
[ElementCall.MEMBER_EVENT_TYPE.name]: 0,
},
}),
));
});
});
it("disables Element calls", async () => {
mockPowerLevels({ [ElementCall.MEMBER_EVENT_TYPE.name]: 0 });
const tab = renderTab();
fireEvent.click(getElementCallSwitch(tab).querySelector(".mx_ToggleSwitch"));
await waitFor(() => expect(cli.sendStateEvent).toHaveBeenCalledWith(
room.roomId,
EventType.RoomPowerLevels,
expect.objectContaining({
events: {
[ElementCall.CALL_EVENT_TYPE.name]: 100,
[ElementCall.MEMBER_EVENT_TYPE.name]: 100,
},
}),
));
});
});
});
});
@@ -0,0 +1,121 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { fireEvent, render, RenderResult, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import PreferencesUserSettingsTab from
"../../../../../../src/components/views/settings/tabs/user/PreferencesUserSettingsTab";
import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
import { mockPlatformPeg, stubClient } from "../../../../../test-utils";
import SettingsStore from "../../../../../../src/settings/SettingsStore";
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
describe("PreferencesUserSettingsTab", () => {
beforeEach(() => {
mockPlatformPeg();
});
const renderTab = (): RenderResult => {
return render(<PreferencesUserSettingsTab closeSettingsFn={() => {}} />);
};
describe("send read receipts", () => {
beforeEach(() => {
stubClient();
jest.spyOn(SettingsStore, "setValue");
jest.spyOn(window, "matchMedia").mockReturnValue({ matches: false } as MediaQueryList);
});
afterEach(() => {
jest.resetAllMocks();
});
const getToggle = () => renderTab().getByRole("switch", { name: "Send read receipts" });
const mockIsVersionSupported = (val: boolean) => {
const client = MatrixClientPeg.get();
jest.spyOn(client, "isVersionSupported").mockImplementation(async (version: string) => {
if (version === "v1.4") return val;
});
};
const mockGetValue = (val: boolean) => {
const copyOfGetValueAt = SettingsStore.getValueAt;
SettingsStore.getValueAt = (level: SettingLevel, name: string, roomId?: string, isExplicit?: boolean) => {
if (name === "sendReadReceipts") return val;
return copyOfGetValueAt(level, name, roomId, isExplicit);
};
};
const expectSetValueToHaveBeenCalled = (
name: string,
roomId: string,
level: SettingLevel,
value: boolean,
) => expect(SettingsStore.setValue).toHaveBeenCalledWith(name, roomId, level, value);
describe("with server support", () => {
beforeEach(() => {
mockIsVersionSupported(true);
});
it("can be enabled", async () => {
mockGetValue(false);
const toggle = getToggle();
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
fireEvent.click(toggle);
expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, true);
});
it("can be disabled", async () => {
mockGetValue(true);
const toggle = getToggle();
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
fireEvent.click(toggle);
expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, false);
});
});
describe("without server support", () => {
beforeEach(() => {
mockIsVersionSupported(false);
});
it("can be enabled", async () => {
mockGetValue(false);
const toggle = getToggle();
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
fireEvent.click(toggle);
expectSetValueToHaveBeenCalled("sendReadReceipts", undefined, SettingLevel.ACCOUNT, true);
});
it("cannot be disabled", async () => {
mockGetValue(true);
const toggle = getToggle();
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "true"));
fireEvent.click(toggle);
expect(SettingsStore.setValue).not.toHaveBeenCalled();
});
});
});
});
@@ -44,7 +44,7 @@ import Modal from '../../../../../../src/Modal';
import LogoutDialog from '../../../../../../src/components/views/dialogs/LogoutDialog';
import {
DeviceSecurityVariation,
DeviceWithVerification,
ExtendedDevice,
} from '../../../../../../src/components/views/settings/devices/types';
import { INACTIVE_DEVICE_AGE_MS } from '../../../../../../src/components/views/settings/devices/filter';
@@ -104,7 +104,7 @@ describe('<SessionManagerTab />', () => {
const toggleDeviceDetails = (
getByTestId: ReturnType<typeof render>['getByTestId'],
deviceId: DeviceWithVerification['device_id'],
deviceId: ExtendedDevice['device_id'],
) => {
// open device detail
const tile = getByTestId(`device-tile-${deviceId}`);
@@ -114,7 +114,7 @@ describe('<SessionManagerTab />', () => {
const toggleDeviceSelection = (
getByTestId: ReturnType<typeof render>['getByTestId'],
deviceId: DeviceWithVerification['device_id'],
deviceId: ExtendedDevice['device_id'],
) => {
const checkbox = getByTestId(`device-tile-checkbox-${deviceId}`);
fireEvent.click(checkbox);
@@ -135,7 +135,7 @@ describe('<SessionManagerTab />', () => {
const isDeviceSelected = (
getByTestId: ReturnType<typeof render>['getByTestId'],
deviceId: DeviceWithVerification['device_id'],
deviceId: ExtendedDevice['device_id'],
): boolean => !!(getByTestId(`device-tile-checkbox-${deviceId}`) as HTMLInputElement).checked;
const isSelectAllChecked = (
@@ -94,16 +94,20 @@ exports[`<SessionManagerTab /> renders current session section with a verified s
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Verified"
class="mx_DeviceType_verificationIcon verified"
class="mx_DeviceTypeIcon_verificationIcon verified"
role="img"
/>
</div>
@@ -196,16 +200,20 @@ exports[`<SessionManagerTab /> renders current session section with an unverifie
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Unverified"
class="mx_DeviceType_verificationIcon unverified"
class="mx_DeviceTypeIcon_verificationIcon unverified"
role="img"
/>
</div>
@@ -298,16 +306,20 @@ exports[`<SessionManagerTab /> sets device verification status correctly 1`] = `
data-testid="device-tile-alices_device"
>
<div
class="mx_DeviceType"
class="mx_DeviceTypeIcon"
>
<div
aria-label="Unknown device type"
class="mx_DeviceType_deviceIcon"
role="img"
/>
class="mx_DeviceTypeIcon_deviceIconWrapper"
>
<div
aria-label="Unknown session type"
class="mx_DeviceTypeIcon_deviceIcon"
role="img"
/>
</div>
<div
aria-label="Verified"
class="mx_DeviceType_verificationIcon verified"
class="mx_DeviceTypeIcon_verificationIcon verified"
role="img"
/>
</div>
+22 -3
View File
@@ -22,6 +22,7 @@ import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { Widget } from "matrix-widget-api";
import "@testing-library/jest-dom";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import type { ClientWidgetApi } from "matrix-widget-api";
@@ -38,6 +39,7 @@ import { CallView as _CallView } from "../../../../src/components/views/voip/Cal
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import { CallStore } from "../../../../src/stores/CallStore";
import { Call, ConnectionState } from "../../../../src/models/Call";
import SdkConfig from "../../../../src/SdkConfig";
const CallView = wrapInMatrixClientContext(_CallView);
@@ -90,7 +92,7 @@ describe("CallLobby", () => {
beforeEach(() => {
MockedCall.create(room, "1");
const maybeCall = CallStore.instance.get(room.roomId);
const maybeCall = CallStore.instance.getCall(room.roomId);
if (!(maybeCall instanceof MockedCall)) throw new Error("Failed to create call");
call = maybeCall;
@@ -163,6 +165,23 @@ describe("CallLobby", () => {
fireEvent.click(screen.getByRole("button", { name: "Join" }));
await waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
});
it("disables join button when the participant limit has been exceeded", async () => {
const bob = mkRoomMember(room.roomId, "@bob:example.org");
const carol = mkRoomMember(room.roomId, "@carol:example.org");
SdkConfig.put({
"element_call": { participant_limit: 2, url: "", use_exclusively: false, brand: "Element Call" },
});
call.participants = new Set([bob, carol]);
await renderView();
const connectSpy = jest.spyOn(call, "connect");
const joinButton = screen.getByRole("button", { name: "Join" });
expect(joinButton).toHaveAttribute("aria-disabled", "true");
fireEvent.click(joinButton);
await waitFor(() => expect(connectSpy).not.toHaveBeenCalled(), { interval: 1 });
});
});
describe("without an existing call", () => {
@@ -171,8 +190,8 @@ describe("CallLobby", () => {
expect(Call.get(room)).toBeNull();
fireEvent.click(screen.getByRole("button", { name: "Join" }));
await waitFor(() => expect(CallStore.instance.get(room.roomId)).not.toBeNull());
const call = CallStore.instance.get(room.roomId)!;
await waitFor(() => expect(CallStore.instance.getCall(room.roomId)).not.toBeNull());
const call = CallStore.instance.getCall(room.roomId)!;
const widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
+175
View File
@@ -0,0 +1,175 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { mocked, Mocked } from "jest-mock";
import { screen, render, act, cleanup, fireEvent, waitFor } from "@testing-library/react";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { Room } from "matrix-js-sdk/src/models/room";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { Widget, ClientWidgetApi } from "matrix-widget-api";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {
useMockedCalls,
MockedCall,
mkRoomMember,
stubClient,
setupAsyncStoreWithClient,
resetAsyncStoreWithClient,
wrapInMatrixClientContext,
} from "../../../test-utils";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { CallStore } from "../../../../src/stores/CallStore";
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
import UnwrappedPipView from "../../../../src/components/views/voip/PipView";
import ActiveWidgetStore from "../../../../src/stores/ActiveWidgetStore";
import DMRoomMap from "../../../../src/utils/DMRoomMap";
import defaultDispatcher from "../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../src/dispatcher/actions";
import { ViewRoomPayload } from "../../../../src/dispatcher/payloads/ViewRoomPayload";
const PipView = wrapInMatrixClientContext(UnwrappedPipView);
describe("PipView", () => {
useMockedCalls();
Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: () => [] } });
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
let client: Mocked<MatrixClient>;
let room: Room;
let alice: RoomMember;
beforeEach(async () => {
stubClient();
client = mocked(MatrixClientPeg.get());
DMRoomMap.makeShared();
room = new Room("!1:example.org", client, "@alice:example.org", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
client.getRooms.mockReturnValue([room]);
alice = mkRoomMember(room.roomId, "@alice:example.org");
jest.spyOn(room, "getMember").mockImplementation(userId => userId === alice.userId ? alice : null);
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),
));
});
afterEach(async () => {
cleanup();
await Promise.all([CallStore.instance, WidgetMessagingStore.instance].map(resetAsyncStoreWithClient));
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
jest.restoreAllMocks();
});
const renderPip = () => { render(<PipView />); };
const viewRoom = (roomId: string) =>
defaultDispatcher.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
metricsTrigger: undefined,
}, true);
const withCall = async (fn: () => Promise<void>): Promise<void> => {
MockedCall.create(room, "1");
const call = CallStore.instance.getCall(room.roomId);
if (!(call instanceof MockedCall)) throw new Error("Failed to create call");
const widget = new Widget(call.widget);
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
stop: () => {},
} as unknown as ClientWidgetApi);
await act(async () => {
await call.connect();
ActiveWidgetStore.instance.setWidgetPersistence(widget.id, room.roomId, true);
});
await fn();
cleanup();
call.destroy();
ActiveWidgetStore.instance.destroyPersistentWidget(widget.id, room.roomId);
};
const withWidget = (fn: () => void): void => {
act(() => ActiveWidgetStore.instance.setWidgetPersistence("1", room.roomId, true));
fn();
cleanup();
ActiveWidgetStore.instance.destroyPersistentWidget("1", room.roomId);
};
it("hides if there's no content", () => {
renderPip();
expect(screen.queryByRole("complementary")).toBeNull();
});
it("shows an active call with a maximise button", async () => {
renderPip();
await withCall(async () => {
screen.getByRole("complementary");
screen.getByText(room.roomId);
expect(screen.queryByRole("button", { name: "Pin" })).toBeNull();
expect(screen.queryByRole("button", { name: /return/i })).toBeNull();
// The maximise button should jump to the call
const dispatcherSpy = jest.fn();
const dispatcherRef = defaultDispatcher.register(dispatcherSpy);
fireEvent.click(screen.getByRole("button", { name: "Fill screen" }));
await waitFor(() => expect(dispatcherSpy).toHaveBeenCalledWith({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
}));
defaultDispatcher.unregister(dispatcherRef);
});
});
it("shows a persistent widget with pin and maximise buttons when viewing the room", () => {
viewRoom(room.roomId);
renderPip();
withWidget(() => {
screen.getByRole("complementary");
screen.getByText(room.roomId);
screen.getByRole("button", { name: "Pin" });
screen.getByRole("button", { name: "Fill screen" });
expect(screen.queryByRole("button", { name: /return/i })).toBeNull();
});
});
it("shows a persistent widget with a return button when not viewing the room", () => {
viewRoom("!2:example.org");
renderPip();
withWidget(() => {
screen.getByRole("complementary");
screen.getByText(room.roomId);
expect(screen.queryByRole("button", { name: "Pin" })).toBeNull();
expect(screen.queryByRole("button", { name: "Fill screen" })).toBeNull();
screen.getByRole("button", { name: /return/i });
});
});
});
+22 -1
View File
@@ -25,6 +25,7 @@ import WidgetStore from "../src/stores/WidgetStore";
import WidgetUtils from "../src/utils/WidgetUtils";
import { JitsiCall, ElementCall } from "../src/models/Call";
import createRoom, { canEncryptToAllUsers } from '../src/createRoom';
import SettingsStore from "../src/settings/SettingsStore";
describe("createRoom", () => {
mockPlatformPeg();
@@ -85,7 +86,7 @@ describe("createRoom", () => {
[ElementCall.MEMBER_EVENT_TYPE.name]: callMemberPower,
},
},
}]] = client.createRoom.mock.calls as any; // no good type
}]] = client.createRoom.mock.calls;
// We should have had enough power to be able to set up the call
expect(userPower).toBeGreaterThanOrEqual(callPower);
@@ -109,6 +110,26 @@ describe("createRoom", () => {
expect(createJitsiCallSpy).not.toHaveBeenCalled();
expect(createElementCallSpy).not.toHaveBeenCalled();
});
it("correctly sets up MSC3401 power levels", async () => {
jest.spyOn(SettingsStore, "getValue").mockImplementation((name: string) => {
if (name === "feature_group_calls") return true;
});
await createRoom({});
const [[{
power_level_content_override: {
events: {
[ElementCall.CALL_EVENT_TYPE.name]: callPower,
[ElementCall.MEMBER_EVENT_TYPE.name]: callMemberPower,
},
},
}]] = client.createRoom.mock.calls;
expect(callPower).toBe(100);
expect(callMemberPower).toBe(100);
});
});
describe("canEncryptToAllUsers", () => {
+79 -26
View File
@@ -15,7 +15,6 @@ limitations under the License.
*/
import EventEmitter from "events";
import { isEqual } from "lodash";
import { mocked } from "jest-mock";
import { waitFor } from "@testing-library/react";
import { RoomType } from "matrix-js-sdk/src/@types/event";
@@ -28,7 +27,7 @@ import type { Mocked } from "jest-mock";
import type { MatrixClient, IMyDevice } from "matrix-js-sdk/src/client";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import type { ClientWidgetApi } from "matrix-widget-api";
import type { JitsiCallMemberContent, ElementCallMemberContent } from "../../src/models/Call";
import { JitsiCallMemberContent, ElementCallMemberContent, Layout } from "../../src/models/Call";
import { stubClient, mkEvent, mkRoomMember, setupAsyncStoreWithClient, mockPlatformPeg } from "../test-utils";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../src/MediaDeviceHandler";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
@@ -404,30 +403,35 @@ describe("JitsiCall", () => {
});
it("emits events when connection state changes", async () => {
const events: ConnectionState[] = [];
const onConnectionState = (state: ConnectionState) => events.push(state);
const onConnectionState = jest.fn();
call.on(CallEvent.ConnectionState, onConnectionState);
await call.connect();
await call.disconnect();
expect(events).toEqual([
ConnectionState.Connecting,
ConnectionState.Connected,
ConnectionState.Disconnecting,
ConnectionState.Disconnected,
expect(onConnectionState.mock.calls).toEqual([
[ConnectionState.Connecting, ConnectionState.Disconnected],
[ConnectionState.Connected, ConnectionState.Connecting],
[ConnectionState.Disconnecting, ConnectionState.Connected],
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
]);
call.off(CallEvent.ConnectionState, onConnectionState);
});
it("emits events when participants change", async () => {
const events: Set<RoomMember>[] = [];
const onParticipants = (participants: Set<RoomMember>) => {
if (!isEqual(participants, events[events.length - 1])) events.push(participants);
};
const onParticipants = jest.fn();
call.on(CallEvent.Participants, onParticipants);
await call.connect();
await call.disconnect();
expect(events).toEqual([new Set([alice]), new Set()]);
expect(onParticipants.mock.calls).toEqual([
[new Set([alice]), new Set()],
[new Set([alice]), new Set([alice])],
[new Set(), new Set([alice])],
[new Set(), new Set()],
]);
call.off(CallEvent.Participants, onParticipants);
});
it("switches to spotlight layout when the widget becomes a PiP", async () => {
@@ -725,31 +729,80 @@ describe("ElementCall", () => {
expect([...call.participants]).toEqual([bob]);
});
it("tracks layout", async () => {
await call.connect();
expect(call.layout).toBe(Layout.Tile);
messaging.emit(
`action:${ElementWidgetActions.SpotlightLayout}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
expect(call.layout).toBe(Layout.Spotlight);
messaging.emit(
`action:${ElementWidgetActions.TileLayout}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
expect(call.layout).toBe(Layout.Tile);
});
it("sets layout", async () => {
await call.connect();
await call.setLayout(Layout.Spotlight);
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
await call.setLayout(Layout.Tile);
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {});
});
it("emits events when connection state changes", async () => {
const events: ConnectionState[] = [];
const onConnectionState = (state: ConnectionState) => events.push(state);
const onConnectionState = jest.fn();
call.on(CallEvent.ConnectionState, onConnectionState);
await call.connect();
await call.disconnect();
expect(events).toEqual([
ConnectionState.Connecting,
ConnectionState.Connected,
ConnectionState.Disconnecting,
ConnectionState.Disconnected,
expect(onConnectionState.mock.calls).toEqual([
[ConnectionState.Connecting, ConnectionState.Disconnected],
[ConnectionState.Connected, ConnectionState.Connecting],
[ConnectionState.Disconnecting, ConnectionState.Connected],
[ConnectionState.Disconnected, ConnectionState.Disconnecting],
]);
call.off(CallEvent.ConnectionState, onConnectionState);
});
it("emits events when participants change", async () => {
const events: Set<RoomMember>[] = [];
const onParticipants = (participants: Set<RoomMember>) => {
if (!isEqual(participants, events[events.length - 1])) events.push(participants);
};
const onParticipants = jest.fn();
call.on(CallEvent.Participants, onParticipants);
await call.connect();
await call.disconnect();
expect(events).toEqual([new Set([alice]), new Set()]);
expect(onParticipants.mock.calls).toEqual([
[new Set([alice]), new Set()],
[new Set(), new Set()],
[new Set(), new Set([alice])],
]);
call.off(CallEvent.Participants, onParticipants);
});
it("emits events when layout changes", async () => {
await call.connect();
const onLayout = jest.fn();
call.on(CallEvent.Layout, onLayout);
messaging.emit(
`action:${ElementWidgetActions.SpotlightLayout}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
messaging.emit(
`action:${ElementWidgetActions.TileLayout}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
expect(onLayout.mock.calls).toEqual([[Layout.Spotlight], [Layout.Tile]]);
call.off(CallEvent.Layout, onLayout);
});
it("ends the call immediately if we're the last participant to leave", async () => {
@@ -81,7 +81,7 @@ describe("Algorithm", () => {
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
MockedCall.create(roomWithCall, "1");
const call = CallStore.instance.get(roomWithCall.roomId);
const call = CallStore.instance.getCall(roomWithCall.roomId);
if (call === null) throw new Error("Failed to create call");
const widget = new Widget(call.widget);
@@ -209,7 +209,6 @@ describe("StopGapWidgetDriver", () => {
});
await expect(driver.readEventRelations('$event')).resolves.toEqual({
originalEvent: expect.objectContaining({ content: {} }),
chunk: [],
nextBatch: undefined,
prevBatch: undefined,
@@ -218,24 +217,6 @@ describe("StopGapWidgetDriver", () => {
expect(client.relations).toBeCalledWith('!this-room-id', '$event', null, null, {});
});
it('reads related events if the original event is missing', async () => {
client.relations.mockResolvedValue({
// the relations function can return an undefined event, even
// though the typings don't permit an undefined value.
originalEvent: undefined as any,
events: [],
});
await expect(driver.readEventRelations('$event', '!room-id')).resolves.toEqual({
originalEvent: undefined,
chunk: [],
nextBatch: undefined,
prevBatch: undefined,
});
expect(client.relations).toBeCalledWith('!room-id', '$event', null, null, {});
});
it('reads related events from a selected room', async () => {
client.relations.mockResolvedValue({
originalEvent: new MatrixEvent(),
@@ -244,7 +225,6 @@ describe("StopGapWidgetDriver", () => {
});
await expect(driver.readEventRelations('$event', '!room-id')).resolves.toEqual({
originalEvent: expect.objectContaining({ content: {} }),
chunk: [
expect.objectContaining({ content: {} }),
expect.objectContaining({ content: {} }),
@@ -272,7 +252,6 @@ describe("StopGapWidgetDriver", () => {
25,
'f',
)).resolves.toEqual({
originalEvent: expect.objectContaining({ content: {} }),
chunk: [],
nextBatch: undefined,
prevBatch: undefined,
+1
View File
@@ -160,6 +160,7 @@ export function createTestClient(): MatrixClient {
getIdentityAccount: jest.fn().mockResolvedValue({}),
getTerms: jest.fn().mockResolvedValueOnce(undefined),
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(undefined),
isVersionSupported: jest.fn().mockResolvedValue(undefined),
getPushRules: jest.fn().mockResolvedValue(undefined),
getPushers: jest.fn().mockResolvedValue({ pushers: [] }),
getThreePids: jest.fn().mockResolvedValue({ threepids: [] }),
+115
View File
@@ -0,0 +1,115 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { setTheme } from "../src/theme";
describe('theme', () => {
describe('setTheme', () => {
let lightTheme;
let darkTheme;
let spyQuerySelectorAll: jest.MockInstance<NodeListOf<Element>, [selectors: string]>;
beforeEach(() => {
const styles = [
{
attributes: {
'data-mx-theme': {
value: 'light',
},
},
disabled: true,
href: 'urlLight',
onload: () => void 0,
},
{
attributes: {
'data-mx-theme': {
value: 'dark',
},
},
disabled: true,
href: 'urlDark',
onload: () => void 0,
},
];
lightTheme = styles[0];
darkTheme = styles[1];
jest.spyOn(document.body, 'style', 'get').mockReturnValue([] as any);
spyQuerySelectorAll = jest.spyOn(document, 'querySelectorAll').mockReturnValue(styles as any);
});
afterEach(() => {
jest.restoreAllMocks();
jest.useRealTimers();
});
it('should switch theme on onload call', async () => {
// When
await new Promise(resolve => {
setTheme('light').then(resolve);
lightTheme.onload();
});
// Then
expect(spyQuerySelectorAll).toHaveBeenCalledWith('[data-mx-theme]');
expect(spyQuerySelectorAll).toBeCalledTimes(1);
expect(lightTheme.disabled).toBe(false);
expect(darkTheme.disabled).toBe(true);
});
it('should reject promise on onerror call', () => {
return expect(new Promise(resolve => {
setTheme('light').catch(e => resolve(e));
lightTheme.onerror('call onerror');
})).resolves.toBe('call onerror');
});
it('should switch theme if CSS are preloaded', async () => {
// When
jest.spyOn(document, 'styleSheets', 'get').mockReturnValue([lightTheme] as any);
await setTheme('light');
// Then
expect(lightTheme.disabled).toBe(false);
expect(darkTheme.disabled).toBe(true);
});
it('should switch theme if CSS is loaded during pooling', async () => {
// When
jest.useFakeTimers();
await new Promise(resolve => {
setTheme('light').then(resolve);
jest.spyOn(document, 'styleSheets', 'get').mockReturnValue([lightTheme] as any);
jest.advanceTimersByTime(200);
});
// Then
expect(lightTheme.disabled).toBe(false);
expect(darkTheme.disabled).toBe(true);
});
it('should reject promise if pooling maximum value is reached', () => {
jest.useFakeTimers();
return new Promise(resolve => {
setTheme('light').catch(resolve);
jest.advanceTimersByTime(200 * 10);
});
});
});
});
+172
View File
@@ -0,0 +1,172 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import React from "react";
import { render, screen, cleanup, fireEvent, waitFor } from "@testing-library/react";
import "@testing-library/jest-dom";
import { mocked, Mocked } from "jest-mock";
import { Room } from "matrix-js-sdk/src/models/room";
import { MatrixClient } from "matrix-js-sdk/src/client";
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
import { ClientWidgetApi, Widget } from "matrix-widget-api";
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
import {
useMockedCalls,
MockedCall,
stubClient,
mkRoomMember,
setupAsyncStoreWithClient,
resetAsyncStoreWithClient,
} from "../test-utils";
import defaultDispatcher from "../../src/dispatcher/dispatcher";
import { Action } from "../../src/dispatcher/actions";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import { CallStore } from "../../src/stores/CallStore";
import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingStore";
import DMRoomMap from "../../src/utils/DMRoomMap";
import ToastStore from "../../src/stores/ToastStore";
import { getIncomingCallToastKey, IncomingCallToast } from "../../src/toasts/IncomingCallToast";
describe("IncomingCallEvent", () => {
useMockedCalls();
Object.defineProperty(navigator, "mediaDevices", { value: { enumerateDevices: () => [] } });
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => { });
let client: Mocked<MatrixClient>;
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.getCall(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(<IncomingCallToast callEvent={call.event} />); };
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);
});
it("closes toast when the call lobby is viewed", async () => {
renderToast();
defaultDispatcher.dispatch({
action: Action.ViewRoom,
room_id: room.roomId,
view_call: true,
});
await waitFor(() => expect(toastStore.dismissToast).toHaveBeenCalledWith(
getIncomingCallToastKey(call.event.getStateKey()!),
));
});
});
+41
View File
@@ -0,0 +1,41 @@
/*
Copyright 2022 Oliver Sand
Copyright 2022 Nordeck IT + Consulting GmbH.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import WidgetUtils from "../../src/utils/WidgetUtils";
import { mockPlatformPeg } from "../test-utils";
describe("getLocalJitsiWrapperUrl", () => {
it('should generate jitsi URL (for defaults)', () => {
mockPlatformPeg();
expect(WidgetUtils.getLocalJitsiWrapperUrl()).toEqual(
'https://app.element.io/jitsi.html'
+ '#conferenceDomain=$domain'
+ '&conferenceId=$conferenceId'
+ '&isAudioOnly=$isAudioOnly'
+ '&isVideoChannel=$isVideoChannel'
+ '&displayName=$matrix_display_name'
+ '&avatarUrl=$matrix_avatar_url'
+ '&userId=$matrix_user_id'
+ '&roomId=$matrix_room_id'
+ '&theme=$theme'
+ '&roomName=$roomName'
+ '&supportsScreensharing=true'
+ '&language=$org.matrix.msc2873.client_language',
);
});
});
+1 -1
View File
@@ -36,7 +36,7 @@ describe('recordClientInformation()', () => {
const sdkConfig: IConfigOptions = {
brand: 'Test Brand',
element_call: { url: '', use_exclusively: false },
element_call: { url: '', use_exclusively: false, brand: "Element Call" },
};
const platform = {
+145
View File
@@ -0,0 +1,145 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import { DeviceType, ExtendedDeviceInformation, parseUserAgent } from "../../../src/utils/device/parseUserAgent";
const makeDeviceExtendedInfo = (
deviceType: DeviceType,
deviceModel?: string,
deviceOperatingSystem?: string,
clientName?: string,
clientVersion?: string,
): ExtendedDeviceInformation => ({
deviceType,
deviceModel,
deviceOperatingSystem,
client: clientName && [clientName, clientVersion].filter(Boolean).join(' '),
});
/* eslint-disable max-len */
const ANDROID_UA = [
// New User Agent Implementation
"Element dbg/1.5.0-dev (Xiaomi Mi 9T; Android 11; RKQ1.200826.002 test-keys; Flavour GooglePlay; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Samsung SM-G960F; Android 6.0.1; RKQ1.200826.002; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google Nexus 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google (Nexus) 5; Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
"Element/1.5.0 (Google (Nexus) (5); Android 7.0; RKQ1.200826.002 test test; Flavour FDroid; MatrixAndroidSdk2 1.5.2)",
// Legacy User Agent Implementation
"Element/1.0.0 (Linux; U; Android 6.0.1; SM-A510F Build/MMB29; Flavour GPlay; MatrixAndroidSdk2 1.0)",
"Element/1.0.0 (Linux; Android 7.0; SM-G610M Build/NRD90M; Flavour GPlay; MatrixAndroidSdk2 1.0)",
];
const ANDROID_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Mobile, "Xiaomi Mi 9T", "Android 11"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G960F", "Android 6.0.1"),
makeDeviceExtendedInfo(DeviceType.Mobile, "LG Nexus 5", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) 5", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Google (Nexus) (5)", "Android 7.0"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-A510F", "Android 6.0.1"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Samsung SM-G610M", "Android 7.0"),
];
const IOS_UA = [
"Element/1.8.21 (iPhone; iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPhone XS Max; iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPad Pro (11-inch); iOS 15.2; Scale/3.00)",
"Element/1.8.21 (iPad Pro (12.9-inch) (3rd generation); iOS 15.2; Scale/3.00)",
];
const IOS_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "Apple iPhone XS Max", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (11-inch)", "iOS 15.2"),
makeDeviceExtendedInfo(DeviceType.Mobile, "iPad Pro (12.9-inch) (3rd generation)", "iOS 15.2"),
];
const DESKTOP_UA = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102" +
" Electron/20.1.1 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) ElementNightly/2022091301 Chrome/104.0.5112.102 Electron/20.1.1 Safari/537.36",
];
const DESKTOP_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Mac OS 10.15.7", "Electron", "20"),
makeDeviceExtendedInfo(DeviceType.Desktop, undefined, "Windows 10", "Electron", "20"),
];
const WEB_UA = [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
"Mozilla/5.0 (Windows NT 10.0) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.102 Safari/537.36",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10.10; rv:39.0) Gecko/20100101 Firefox/39.0",
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_2) AppleWebKit/600.3.18 (KHTML, like Gecko) Version/8.0.3 Safari/600.3.18",
"Mozilla/5.0 (Windows NT 6.0; rv:40.0) Gecko/20100101 Firefox/40.0",
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/42.0.2311.135 Safari/537.36 Edge/12.246",
// using mobile browser
"Mozilla/5.0 (iPad; CPU OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
"Mozilla/5.0 (iPhone; CPU iPhone OS 8_4_1 like Mac OS X) AppleWebKit/600.1.4 (KHTML, like Gecko) Version/8.0 Mobile/12H321 Safari/600.1.4",
"Mozilla/5.0 (Linux; Android 9; SM-G973U Build/PPR1.180610.011) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.100 Mobile Safari/537.36",
];
const WEB_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.15.7", "Chrome", "104"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows 10", "Chrome", "104"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.10", "Firefox", "39"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Mac OS 10.10.2", "Safari", "8"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows Vista", "Firefox", "40"),
makeDeviceExtendedInfo(DeviceType.Web, undefined, "Windows 10", "Edge", "12"),
// using mobile browser
makeDeviceExtendedInfo(DeviceType.Web, "Apple iPad", "iOS 8.4.1", "Mobile Safari", "8"),
makeDeviceExtendedInfo(DeviceType.Web, "Apple iPhone", "iOS 8.4.1", "Mobile Safari", "8"),
makeDeviceExtendedInfo(DeviceType.Web, "Samsung SM-G973U", "Android 9", "Chrome", "69"),
];
const MISC_UA = [
"AppleTV11,1/11.1",
"Curl Client/1.0",
"banana",
"",
];
const MISC_EXPECTED_RESULT = [
makeDeviceExtendedInfo(DeviceType.Unknown, "Apple Apple TV", undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
makeDeviceExtendedInfo(DeviceType.Unknown, undefined, undefined, undefined, undefined),
];
/* eslint-disable max-len */
describe('parseUserAgent()', () => {
it('returns deviceType unknown when user agent is falsy', () => {
expect(parseUserAgent(undefined)).toEqual({
deviceType: DeviceType.Unknown,
});
});
type TestCase = [string, ExtendedDeviceInformation];
const testPlatform = (platform: string, userAgents: string[], results: ExtendedDeviceInformation[]): void => {
const testCases: TestCase[] = userAgents.map((userAgent, index) => [userAgent, results[index]]);
describe(platform, () => {
it.each(
testCases,
)('Parses user agent correctly - %s', (userAgent, expectedResult) => {
expect(parseUserAgent(userAgent)).toEqual(expectedResult);
});
});
};
testPlatform('Android', ANDROID_UA, ANDROID_EXPECTED_RESULT);
testPlatform('iOS', IOS_UA, IOS_EXPECTED_RESULT);
testPlatform('Desktop', DESKTOP_UA, DESKTOP_EXPECTED_RESULT);
testPlatform('Web', WEB_UA, WEB_EXPECTED_RESULT);
testPlatform('Misc', MISC_UA, MISC_EXPECTED_RESULT);
});
+34
View File
@@ -20,6 +20,8 @@ import { mocked } from "jest-mock";
import {
localNotificationsAreSilenced,
getLocalNotificationAccountDataEventType,
createLocalNotificationSettingsIfNeeded,
deviceNotificationSettingsKeys,
} from "../../src/utils/notifications";
import SettingsStore from "../../src/settings/SettingsStore";
import { getMockClientWithEventEmitter } from "../test-utils/client";
@@ -46,6 +48,38 @@ describe('notifications', () => {
mocked(SettingsStore).getValue.mockReturnValue(false);
});
describe('createLocalNotification', () => {
it('creates account data event', async () => {
await createLocalNotificationSettingsIfNeeded(mockClient);
const event = mockClient.getAccountData(accountDataEventKey);
expect(event?.getContent().is_silenced).toBe(true);
});
it.each(deviceNotificationSettingsKeys)(
'unsilenced for existing sessions when %s setting is truthy',
async (settingKey) => {
mocked(SettingsStore)
.getValue
.mockImplementation((key) => {
return key === settingKey;
});
await createLocalNotificationSettingsIfNeeded(mockClient);
const event = mockClient.getAccountData(accountDataEventKey);
expect(event?.getContent().is_silenced).toBe(false);
});
it("does not override an existing account event data", async () => {
mockClient.setAccountData(accountDataEventKey, {
is_silenced: false,
});
await createLocalNotificationSettingsIfNeeded(mockClient);
const event = mockClient.getAccountData(accountDataEventKey);
expect(event?.getContent().is_silenced).toBe(false);
});
});
describe('localNotificationsAreSilenced', () => {
it('defaults to true when no setting exists', () => {
expect(localNotificationsAreSilenced(mockClient)).toBeTruthy();