Merge remote-tracking branch 'origin/develop' into feat/add-message-edition-wysiwyg-composer

This commit is contained in:
Florian Duros 2022-10-21 10:15:46 +02:00
commit 4d089dcc05
No known key found for this signature in database
GPG key ID: 9700AA5870258A0B
109 changed files with 2374 additions and 1011 deletions

View file

@ -22,16 +22,18 @@ import RightPanelStore from "../src/stores/right-panel/RightPanelStore";
import { RoomViewStore } from "../src/stores/RoomViewStore";
import { SpaceStoreClass } from "../src/stores/spaces/SpaceStore";
import { WidgetLayoutStore } from "../src/stores/widgets/WidgetLayoutStore";
import { WidgetPermissionStore } from "../src/stores/widgets/WidgetPermissionStore";
import WidgetStore from "../src/stores/WidgetStore";
/**
* A class which provides the same API as Stores but adds additional unsafe setters which can
* A class which provides the same API as SdkContextClass but adds additional unsafe setters which can
* replace individual stores. This is useful for tests which need to mock out stores.
*/
export class TestStores extends SdkContextClass {
export class TestSdkContext extends SdkContextClass {
public _RightPanelStore?: RightPanelStore;
public _RoomNotificationStateStore?: RoomNotificationStateStore;
public _RoomViewStore?: RoomViewStore;
public _WidgetPermissionStore?: WidgetPermissionStore;
public _WidgetLayoutStore?: WidgetLayoutStore;
public _WidgetStore?: WidgetStore;
public _PosthogAnalytics?: PosthogAnalytics;

View file

@ -1,47 +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 React from "react";
import { render } from "@testing-library/react";
import { Icon, IconColour, IconSize, IconType } from "../../../src/components/atoms/Icon";
describe("Icon", () => {
it.each([
IconColour.Accent,
IconColour.LiveBadge,
])("should render the colour %s", (colour: IconColour) => {
const { container } = render(
<Icon
colour={colour}
type={IconType.Live}
/>,
);
expect(container).toMatchSnapshot();
});
it.each([
IconSize.S16,
])("should render the size %s", (size: IconSize) => {
const { container } = render(
<Icon
size={size}
type={IconType.Live}
/>,
);
expect(container).toMatchSnapshot();
});
});

View file

@ -1,34 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Icon should render the colour accent 1`] = `
<div>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_accent"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
`;
exports[`Icon should render the colour live-badge 1`] = `
<div>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
`;
exports[`Icon should render the size 16 1`] = `
<div>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_accent"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
`;

View file

@ -0,0 +1,158 @@
/*
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 { getByTestId, render, RenderResult, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { MsgType, RelationType } from "matrix-js-sdk/src/@types/event";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import { THREAD_RELATION_TYPE } from "matrix-js-sdk/src/models/thread";
import React, { useState } from "react";
import { act } from "react-dom/test-utils";
import ThreadView from "../../../src/components/structures/ThreadView";
import MatrixClientContext from "../../../src/contexts/MatrixClientContext";
import RoomContext from "../../../src/contexts/RoomContext";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import DMRoomMap from "../../../src/utils/DMRoomMap";
import ResizeNotifier from "../../../src/utils/ResizeNotifier";
import { mockPlatformPeg } from "../../test-utils/platform";
import { getRoomContext } from "../../test-utils/room";
import { stubClient } from "../../test-utils/test-utils";
import { mkThread } from "../../test-utils/threads";
describe("ThreadView", () => {
const ROOM_ID = "!roomId:example.org";
const SENDER = "@alice:example.org";
let mockClient: MatrixClient;
let room: Room;
let rootEvent: MatrixEvent;
let changeEvent: (event: MatrixEvent) => void;
function TestThreadView() {
const [event, setEvent] = useState(rootEvent);
changeEvent = setEvent;
return <MatrixClientContext.Provider value={mockClient}>
<RoomContext.Provider value={getRoomContext(room, {
canSendMessages: true,
})}>
<ThreadView
room={room}
onClose={jest.fn()}
mxEvent={event}
resizeNotifier={new ResizeNotifier()}
/>
</RoomContext.Provider>,
</MatrixClientContext.Provider>;
}
async function getComponent(): Promise<RenderResult> {
const renderResult = render(
<TestThreadView />,
);
await waitFor(() => {
expect(() => getByTestId(renderResult.container, 'spinner')).toThrow();
});
return renderResult;
}
async function sendMessage(container, text): Promise<void> {
const composer = getByTestId(container, "basicmessagecomposer");
await userEvent.click(composer);
await userEvent.keyboard(text);
const sendMessageBtn = getByTestId(container, "sendmessagebtn");
await userEvent.click(sendMessageBtn);
}
function expectedMessageBody(rootEvent, message) {
return {
"body": message,
"m.relates_to": {
"event_id": rootEvent.getId(),
"is_falling_back": true,
"m.in_reply_to": {
"event_id": rootEvent.getThread().lastReply((ev: MatrixEvent) => {
return ev.isRelation(THREAD_RELATION_TYPE.name);
}).getId(),
},
"rel_type": RelationType.Thread,
},
"msgtype": MsgType.Text,
};
}
beforeEach(() => {
jest.clearAllMocks();
stubClient();
mockPlatformPeg();
mockClient = mocked(MatrixClientPeg.get());
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const res = mkThread({
room,
client: mockClient,
authorId: mockClient.getUserId(),
participantUserIds: [mockClient.getUserId()],
});
rootEvent = res.rootEvent;
DMRoomMap.makeShared();
jest.spyOn(DMRoomMap.shared(), "getUserIdForRoomId").mockReturnValue(SENDER);
});
it("sends a message with the correct fallback", async () => {
const { container } = await getComponent();
await sendMessage(container, "Hello world!");
expect(mockClient.sendMessage).toHaveBeenCalledWith(
ROOM_ID, rootEvent.getId(), expectedMessageBody(rootEvent, "Hello world!"),
);
});
it("sends a message with the correct fallback", async () => {
const { container } = await getComponent();
const { rootEvent: rootEvent2 } = mkThread({
room,
client: mockClient,
authorId: mockClient.getUserId(),
participantUserIds: [mockClient.getUserId()],
});
act(() => {
changeEvent(rootEvent2);
});
await sendMessage(container, "yolo");
expect(mockClient.sendMessage).toHaveBeenCalledWith(
ROOM_ID, rootEvent2.getId(), expectedMessageBody(rootEvent2, "yolo"),
);
});
});

View file

@ -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_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 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\\" data-testid=\\"spinner\\"></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_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_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 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\\" data-testid=\\"basicmessagecomposer\\" 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>"`;
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\\" data-testid=\\"basicmessagecomposer\\" 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>"`;

View file

@ -0,0 +1,84 @@
/*
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 { getByTestId, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { mocked } from "jest-mock";
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
import { Room } from "matrix-js-sdk/src/models/room";
import React from "react";
import ThreadListContextMenu, {
ThreadListContextMenuProps,
} from "../../../../src/components/views/context_menus/ThreadListContextMenu";
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
import { RoomPermalinkCreator } from "../../../../src/utils/permalinks/Permalinks";
import { stubClient } from "../../../test-utils/test-utils";
import { mkThread } from "../../../test-utils/threads";
describe("ThreadListContextMenu", () => {
const ROOM_ID = "!123:matrix.org";
let room: Room;
let mockClient: MatrixClient;
let event: MatrixEvent;
function getComponent(props: Partial<ThreadListContextMenuProps>) {
return render(<ThreadListContextMenu
mxEvent={event}
{...props}
/>);
}
beforeEach(() => {
jest.clearAllMocks();
stubClient();
mockClient = mocked(MatrixClientPeg.get());
room = new Room(ROOM_ID, mockClient, mockClient.getUserId() ?? "", {
pendingEventOrdering: PendingEventOrdering.Detached,
});
const res = mkThread({
room,
client: mockClient,
authorId: mockClient.getUserId(),
participantUserIds: [mockClient.getUserId()],
});
event = res.rootEvent;
});
it("does not render the permalink", async () => {
const { container } = getComponent({});
const btn = getByTestId(container, "threadlist-dropdown-button");
await userEvent.click(btn);
expect(screen.queryByTestId("copy-thread-link")).toBeNull();
});
it("does render the permalink", async () => {
const { container } = getComponent({
permalinkCreator: new RoomPermalinkCreator(room, room.roomId, false),
});
const btn = getByTestId(container, "threadlist-dropdown-button");
await userEvent.click(btn);
expect(screen.queryByTestId("copy-thread-link")).not.toBeNull();
});
});

View file

@ -17,7 +17,7 @@ limitations under the License.
import React from "react";
import { act } from "react-dom/test-utils";
import { sleep } from "matrix-js-sdk/src/utils";
import { ISendEventResponse, MatrixClient, MsgType } from "matrix-js-sdk/src/matrix";
import { ISendEventResponse, MatrixClient, MsgType, RelationType } from "matrix-js-sdk/src/matrix";
// eslint-disable-next-line deprecate/import
import { mount } from 'enzyme';
import { mocked } from "jest-mock";
@ -291,7 +291,7 @@ describe('<SendMessageComposer/>', () => {
it('correctly sets the editorStateKey for threads', () => {
const relation = {
rel_type: "m.thread",
rel_type: RelationType.Thread,
event_id: "myFakeThreadId",
};
const includeReplyLegacyFallback = false;

View file

@ -20,13 +20,12 @@ import { act, render, screen, waitFor } from "@testing-library/react";
import { InputEventProcessor, Wysiwyg, WysiwygProps } from "@matrix-org/matrix-wysiwyg";
import MatrixClientContext from "../../../../../src/contexts/MatrixClientContext";
import RoomContext, { TimelineRenderingType } from "../../../../../src/contexts/RoomContext";
import RoomContext from "../../../../../src/contexts/RoomContext";
import defaultDispatcher from "../../../../../src/dispatcher/dispatcher";
import { Action } from "../../../../../src/dispatcher/actions";
import { IRoomState } from "../../../../../src/components/structures/RoomView";
import { Layout } from "../../../../../src/settings/enums/Layout";
import { WysiwygComposer } from "../../../../../src/components/views/rooms/wysiwyg_composer/components/WysiwygComposer";
import { createTestClient, mkEvent, mkStubRoom } from "../../../../test-utils";
import { createTestClient, getRoomContext, mkEvent, mkStubRoom } from "../../../../test-utils";
import SettingsStore from "../../../../../src/settings/SettingsStore";
// Work around missing ClipboardEvent type
@ -74,43 +73,7 @@ describe('WysiwygComposer', () => {
return eventId === mockEvent.getId() ? mockEvent : null;
});
const defaultRoomContext: IRoomState = {
room: mockRoom,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
canPeek: false,
showApps: false,
isPeeking: false,
showRightPanel: true,
joining: false,
atEndOfLiveTimeline: true,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canSendMessages: false,
layout: Layout.Group,
lowBandwidth: false,
alwaysShowTimestamps: false,
showTwelveHourTimestamps: false,
readMarkerInViewThresholdMs: 3000,
readMarkerOutOfViewThresholdMs: 30000,
showHiddenEvents: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
};
const defaultRoomContext: IRoomState = getRoomContext(mockRoom, {});
let sendMessage: () => void;
const customRender = (onChange = (_content: string) => void 0, disabled = false) => {

View file

@ -31,6 +31,7 @@ exports[`FontScalingPanel renders the font scaling UI 1`] = `
<div
aria-label="Loading..."
className="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style={
Object {

View file

@ -4,6 +4,7 @@ exports[`<LoginWithQR /> approves login and waits for new device 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class=""
@ -32,6 +33,7 @@ exports[`<LoginWithQR /> approves login and waits for new device 1`] = `
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
@ -61,6 +63,7 @@ exports[`<LoginWithQR /> displays confirmation digits after connected to rendezv
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class=""
@ -122,6 +125,7 @@ exports[`<LoginWithQR /> displays error when approving login fails 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_centreTitle"
@ -168,6 +172,7 @@ exports[`<LoginWithQR /> displays qr code after it is created 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class=""
@ -214,6 +219,7 @@ exports[`<LoginWithQR /> displays qr code after it is created 1`] = `
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>
@ -232,6 +238,7 @@ exports[`<LoginWithQR /> displays unknown error if connection to rendezvous fail
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_centreTitle"
@ -278,6 +285,7 @@ exports[`<LoginWithQR /> no content in case of no support 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class="mx_LoginWithQR_centreTitle"
@ -324,6 +332,7 @@ exports[`<LoginWithQR /> renders spinner while generating code 1`] = `
<div>
<div
class="mx_LoginWithQR"
data-testid="login-with-qr"
>
<div
class=""
@ -352,6 +361,7 @@ exports[`<LoginWithQR /> renders spinner while generating code 1`] = `
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>

View file

@ -13,7 +13,7 @@ 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 { fireEvent, render } from '@testing-library/react';
import React from 'react';
import SecurityUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/SecurityUserSettingsTab";
@ -26,6 +26,7 @@ import {
mockClientMethodsCrypto,
mockClientMethodsDevice,
mockPlatformPeg,
flushPromises,
} from '../../../../../test-utils';
describe('<SecurityUserSettingsTab />', () => {
@ -42,6 +43,12 @@ describe('<SecurityUserSettingsTab />', () => {
...mockClientMethodsCrypto(),
getRooms: jest.fn().mockReturnValue([]),
getIgnoredUsers: jest.fn(),
getVersions: jest.fn().mockResolvedValue({
unstable_features: {
'org.matrix.msc3882': true,
'org.matrix.msc3886': true,
},
}),
});
const getComponent = () =>
@ -70,4 +77,34 @@ describe('<SecurityUserSettingsTab />', () => {
expect(queryByTestId('devices-section')).toBeFalsy();
});
it('does not render qr code login section when disabled', () => {
settingsValueSpy.mockReturnValue(false);
const { queryByText } = render(getComponent());
expect(settingsValueSpy).toHaveBeenCalledWith('feature_qr_signin_reciprocate_show');
expect(queryByText('Sign in with QR code')).toBeFalsy();
});
it('renders qr code login section when enabled', async () => {
settingsValueSpy.mockImplementation(settingName => settingName === 'feature_qr_signin_reciprocate_show');
const { getByText } = render(getComponent());
// wait for versions call to settle
await flushPromises();
expect(getByText('Sign in with QR code')).toBeTruthy();
});
it('enters qr code login section when show QR code button clicked', async () => {
settingsValueSpy.mockImplementation(settingName => settingName === 'feature_qr_signin_reciprocate_show');
const { getByText, getByTestId } = render(getComponent());
// wait for versions call to settle
await flushPromises();
fireEvent.click(getByText('Show QR code'));
expect(getByTestId("login-with-qr")).toBeTruthy();
});
});

View file

@ -34,6 +34,7 @@ import {
import SessionManagerTab from '../../../../../../src/components/views/settings/tabs/user/SessionManagerTab';
import MatrixClientContext from '../../../../../../src/contexts/MatrixClientContext';
import {
flushPromises,
flushPromisesWithFakeTimers,
getMockClientWithEventEmitter,
mkPusher,
@ -47,6 +48,7 @@ import {
ExtendedDevice,
} from '../../../../../../src/components/views/settings/devices/types';
import { INACTIVE_DEVICE_AGE_MS } from '../../../../../../src/components/views/settings/devices/filter';
import SettingsStore from '../../../../../../src/settings/SettingsStore';
mockPlatformPeg();
@ -1142,4 +1144,50 @@ describe('<SessionManagerTab />', () => {
expect(checkbox.getAttribute('aria-checked')).toEqual("false");
});
describe('QR code login', () => {
const settingsValueSpy = jest.spyOn(SettingsStore, 'getValue');
beforeEach(() => {
settingsValueSpy.mockClear().mockReturnValue(false);
// enable server support for qr login
mockClient.getVersions.mockResolvedValue({
versions: [],
unstable_features: {
'org.matrix.msc3882': true,
'org.matrix.msc3886': true,
},
});
});
it('does not render qr code login section when disabled', () => {
settingsValueSpy.mockReturnValue(false);
const { queryByText } = render(getComponent());
expect(settingsValueSpy).toHaveBeenCalledWith('feature_qr_signin_reciprocate_show');
expect(queryByText('Sign in with QR code')).toBeFalsy();
});
it('renders qr code login section when enabled', async () => {
settingsValueSpy.mockImplementation(settingName => settingName === 'feature_qr_signin_reciprocate_show');
const { getByText } = render(getComponent());
// wait for versions call to settle
await flushPromises();
expect(getByText('Sign in with QR code')).toBeTruthy();
});
it('enters qr code login section when show QR code button clicked', async () => {
settingsValueSpy.mockImplementation(settingName => settingName === 'feature_qr_signin_reciprocate_show');
const { getByText, getByTestId } = render(getComponent());
// wait for versions call to settle
await flushPromises();
fireEvent.click(getByText('Show QR code'));
expect(getByTestId("login-with-qr")).toBeTruthy();
});
});
});

View file

@ -38,6 +38,8 @@ import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingSt
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../src/stores/ActiveWidgetStore";
import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions";
import SettingsStore from "../../src/settings/SettingsStore";
import Modal, { IHandle } from "../../src/Modal";
import PlatformPeg from "../../src/PlatformPeg";
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
[MediaDeviceKindEnum.AudioInput]: [
@ -807,6 +809,69 @@ describe("ElementCall", () => {
call.off(CallEvent.Layout, onLayout);
});
describe("screensharing", () => {
it("passes source id if we can get it", async () => {
const sourceId = "source_id";
jest.spyOn(Modal, "createDialog").mockReturnValue(
{ finished: new Promise((r) => r([sourceId])) } as IHandle<any[]>,
);
jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(true);
await call.connect();
messaging.emit(
`action:${ElementWidgetActions.Screenshare}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
waitFor(() => {
expect(messaging!.transport.reply).toHaveBeenCalledWith(
expect.objectContaining({}),
expect.objectContaining({ desktopCapturerSourceId: sourceId }),
);
});
});
it("passes failed if we couldn't get a source id", async () => {
jest.spyOn(Modal, "createDialog").mockReturnValue(
{ finished: new Promise((r) => r([null])) } as IHandle<any[]>,
);
jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(true);
await call.connect();
messaging.emit(
`action:${ElementWidgetActions.Screenshare}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
waitFor(() => {
expect(messaging!.transport.reply).toHaveBeenCalledWith(
expect.objectContaining({}),
expect.objectContaining({ failed: true }),
);
});
});
it("passes an empty object if we don't support desktop capturer", async () => {
jest.spyOn(PlatformPeg.get(), "supportsDesktopCapturer").mockReturnValue(false);
await call.connect();
messaging.emit(
`action:${ElementWidgetActions.Screenshare}`,
new CustomEvent("widgetapirequest", { detail: {} }),
);
waitFor(() => {
expect(messaging!.transport.reply).toHaveBeenCalledWith(
expect.objectContaining({}),
expect.objectContaining({}),
);
});
});
});
it("ends the call immediately if we're the last participant to leave", async () => {
await call.connect();
const onDestroy = jest.fn();

View file

@ -12,6 +12,7 @@ exports[`Module Components should override the factory for a ModuleSpinner 1`] =
<div
aria-label="Loading..."
className="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style={
Object {

View file

@ -27,7 +27,7 @@ import { MatrixDispatcher } from '../../src/dispatcher/dispatcher';
import { UPDATE_EVENT } from '../../src/stores/AsyncStore';
import { ActiveRoomChangedPayload } from '../../src/dispatcher/payloads/ActiveRoomChangedPayload';
import { SpaceStoreClass } from '../../src/stores/spaces/SpaceStore';
import { TestStores } from '../TestStores';
import { TestSdkContext } from '../TestSdkContext';
// mock out the injected classes
jest.mock('../../src/PosthogAnalytics');
@ -77,7 +77,7 @@ describe('RoomViewStore', function() {
// Make the RVS to test
dis = new MatrixDispatcher();
slidingSyncManager = new MockSlidingSyncManager();
const stores = new TestStores();
const stores = new TestSdkContext();
stores._SlidingSyncManager = slidingSyncManager;
stores._PosthogAnalytics = new MockPosthogAnalytics();
stores._SpaceStore = new MockSpaceStore();

View file

@ -17,13 +17,14 @@ limitations under the License.
import { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
import TypingStore from "../../src/stores/TypingStore";
import { LOCAL_ROOM_ID_PREFIX } from "../../src/models/LocalRoom";
import SettingsStore from "../../src/settings/SettingsStore";
import { TestSdkContext } from "../TestSdkContext";
jest.mock("../../src/settings/SettingsStore", () => ({
getValue: jest.fn(),
monitorSetting: jest.fn(),
}));
describe("TypingStore", () => {
@ -37,11 +38,12 @@ describe("TypingStore", () => {
const localRoomId = LOCAL_ROOM_ID_PREFIX + "test";
beforeEach(() => {
typingStore = new TypingStore();
mockClient = {
sendTyping: jest.fn(),
} as unknown as MatrixClient;
MatrixClientPeg.get = () => mockClient;
const context = new TestSdkContext();
context.client = mockClient;
typingStore = new TypingStore(context);
mocked(SettingsStore.getValue).mockImplementation((setting: string) => {
return settings[setting];
});

View file

@ -0,0 +1,107 @@
/*
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 { mocked } from "jest-mock";
import { MatrixClient } from "matrix-js-sdk/src/matrix";
import { Widget, WidgetKind } from "matrix-widget-api";
import { OIDCState, WidgetPermissionStore } from "../../../src/stores/widgets/WidgetPermissionStore";
import SettingsStore from "../../../src/settings/SettingsStore";
import { TestSdkContext } from "../../TestSdkContext";
import { SettingLevel } from "../../../src/settings/SettingLevel";
import { SdkContextClass } from "../../../src/contexts/SDKContext";
import { stubClient } from "../../test-utils";
jest.mock("../../../src/settings/SettingsStore");
describe("WidgetPermissionStore", () => {
let widgetPermissionStore: WidgetPermissionStore;
let mockClient: MatrixClient;
const userId = "@alice:localhost";
const roomId = "!room:localhost";
const w = new Widget({
id: "wid",
creatorUserId: userId,
type: "m.custom",
url: "https://invalid.address.here",
});
let settings = {}; // key value store
beforeEach(() => {
settings = {}; // clear settings
mocked(SettingsStore.getValue).mockImplementation((setting: string) => {
return settings[setting];
});
mocked(SettingsStore.setValue).mockImplementation((settingName: string,
roomId: string | null,
level: SettingLevel,
value: any,
): Promise<void> => {
// the store doesn't use any specific level or room ID (room IDs are packed into keys in `value`)
settings[settingName] = value;
return Promise.resolve();
});
mockClient = stubClient();
const context = new TestSdkContext();
context.client = mockClient;
widgetPermissionStore = new WidgetPermissionStore(context);
});
it("should persist OIDCState.Allowed for a widget", () => {
widgetPermissionStore.setOIDCState(w, WidgetKind.Account, null, OIDCState.Allowed);
// check it remembered the value
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Account, null),
).toEqual(OIDCState.Allowed);
});
it("should persist OIDCState.Denied for a widget", () => {
widgetPermissionStore.setOIDCState(w, WidgetKind.Account, null, OIDCState.Denied);
// check it remembered the value
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Account, null),
).toEqual(OIDCState.Denied);
});
it("should update OIDCState for a widget", () => {
widgetPermissionStore.setOIDCState(w, WidgetKind.Account, null, OIDCState.Allowed);
widgetPermissionStore.setOIDCState(w, WidgetKind.Account, null, OIDCState.Denied);
// check it remembered the latest value
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Account, null),
).toEqual(OIDCState.Denied);
});
it("should scope the location for a widget when setting OIDC state", () => {
// allow this widget for this room
widgetPermissionStore.setOIDCState(w, WidgetKind.Room, roomId, OIDCState.Allowed);
// check it remembered the value
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Room, roomId),
).toEqual(OIDCState.Allowed);
// check this is not the case for the entire account
expect(
widgetPermissionStore.getOIDCState(w, WidgetKind.Account, roomId),
).toEqual(OIDCState.Unknown);
});
it("is created once in SdkContextClass", () => {
const context = new SdkContextClass();
const store = context.widgetPermissionStore;
expect(store).toBeDefined();
const store2 = context.widgetPermissionStore;
expect(store2).toStrictEqual(store);
});
});

View file

@ -22,6 +22,9 @@ import {
Room,
} from "matrix-js-sdk/src/matrix";
import { IRoomState } from "../../src/components/structures/RoomView";
import { TimelineRenderingType } from "../../src/contexts/RoomContext";
import { Layout } from "../../src/settings/enums/Layout";
import { mkEvent } from "./test-utils";
export const makeMembershipEvent = (
@ -50,3 +53,45 @@ export const makeRoomWithStateEvents = (
mockClient.getRoom.mockReturnValue(room1);
return room1;
};
export function getRoomContext(room: Room, override: Partial<IRoomState>): IRoomState {
return {
room,
roomLoading: true,
peekLoading: false,
shouldPeek: true,
membersLoaded: false,
numUnreadMessages: 0,
canPeek: false,
showApps: false,
isPeeking: false,
showRightPanel: true,
joining: false,
atEndOfLiveTimeline: true,
showTopUnreadMessagesBar: false,
statusBarVisible: false,
canReact: false,
canSendMessages: false,
layout: Layout.Group,
lowBandwidth: false,
alwaysShowTimestamps: false,
showTwelveHourTimestamps: false,
readMarkerInViewThresholdMs: 3000,
readMarkerOutOfViewThresholdMs: 30000,
showHiddenEvents: false,
showReadReceipts: true,
showRedactions: true,
showJoinLeaves: true,
showAvatarChanges: true,
showDisplaynameChanges: true,
matrixClientIsReady: false,
timelineRenderingType: TimelineRenderingType.Room,
liveTimeline: undefined,
canSelfRedact: false,
resizing: false,
narrow: false,
activeCall: null,
...override,
};
}

View file

@ -14,7 +14,8 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import { MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent, RelationType, Room } from "matrix-js-sdk/src/matrix";
import { Thread } from "matrix-js-sdk/src/models/thread";
import { mkMessage, MessageEventProps } from "./test-utils";
@ -78,7 +79,7 @@ export const makeThreadEvents = ({
rootEvent.setUnsigned({
"m.relations": {
"m.thread": {
[RelationType.Thread]: {
latest_event: events[events.length - 1],
count: length,
current_user_participated: [...participantUserIds, authorId].includes(currentUserId),
@ -88,3 +89,36 @@ export const makeThreadEvents = ({
return { rootEvent, events };
};
type MakeThreadProps = {
room: Room;
client: MatrixClient;
authorId: string;
participantUserIds: string[];
length?: number;
ts?: number;
};
export const mkThread = ({
room,
client,
authorId,
participantUserIds,
length = 2,
ts = 1,
}: MakeThreadProps): { thread: Thread, rootEvent: MatrixEvent } => {
const { rootEvent, events } = makeThreadEvents({
roomId: room.roomId,
authorId,
participantUserIds,
length,
ts,
currentUserId: client.getUserId(),
});
const thread = room.createThread(rootEvent.getId(), rootEvent, events, true);
// So that we do not have to mock the thread loading
thread.initialEventsFetched = true;
return { thread, rootEvent };
};

View file

@ -17,11 +17,10 @@ limitations under the License.
import React from "react";
import { act, render, screen } from "@testing-library/react";
import { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import {
VoiceBroadcastBody,
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
VoiceBroadcastRecordingBody,
VoiceBroadcastRecordingsStore,
@ -30,8 +29,8 @@ import {
VoiceBroadcastPlayback,
VoiceBroadcastPlaybacksStore,
} from "../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../test-utils";
import { RelationsHelper } from "../../../src/events/RelationsHelper";
import { stubClient } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastRecordingBody", () => ({
VoiceBroadcastRecordingBody: jest.fn(),
@ -41,27 +40,15 @@ jest.mock("../../../src/voice-broadcast/components/molecules/VoiceBroadcastPlayb
VoiceBroadcastPlaybackBody: jest.fn(),
}));
jest.mock("../../../src/events/RelationsHelper");
describe("VoiceBroadcastBody", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
let infoEvent: MatrixEvent;
let stoppedEvent: MatrixEvent;
let testRecording: VoiceBroadcastRecording;
let testPlayback: VoiceBroadcastPlayback;
const mkVoiceBroadcastInfoEvent = (state: VoiceBroadcastInfoState) => {
return mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
user: client.getUserId(),
room: roomId,
content: {
state,
},
});
};
const renderVoiceBroadcast = () => {
render(<VoiceBroadcastBody
mxEvent={infoEvent}
@ -75,7 +62,25 @@ describe("VoiceBroadcastBody", () => {
beforeEach(() => {
client = stubClient();
infoEvent = mkVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Started);
room = new Room(roomId, client, client.getUserId());
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
if (getRoomId === roomId) return room;
});
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
);
stoppedEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Stopped,
client.getUserId(),
client.getDeviceId(),
infoEvent,
);
room.addEventsToTimeline([infoEvent], true, room.getLiveTimeline());
testRecording = new VoiceBroadcastRecording(infoEvent, client);
testPlayback = new VoiceBroadcastPlayback(infoEvent, client);
mocked(VoiceBroadcastRecordingBody).mockImplementation(({ recording }) => {
@ -107,7 +112,18 @@ describe("VoiceBroadcastBody", () => {
);
});
describe("when displaying a voice broadcast recording", () => {
describe("when there is a stopped voice broadcast", () => {
beforeEach(() => {
room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline());
renderVoiceBroadcast();
});
it("should render a voice broadcast playback body", () => {
screen.getByTestId("voice-broadcast-playback-body");
});
});
describe("when there is a started voice broadcast from the current user", () => {
beforeEach(() => {
renderVoiceBroadcast();
});
@ -118,13 +134,8 @@ describe("VoiceBroadcastBody", () => {
describe("and the recordings ends", () => {
beforeEach(() => {
const stoppedEvent = mkVoiceBroadcastInfoEvent(VoiceBroadcastInfoState.Stopped);
// get the RelationsHelper instanced used in VoiceBroadcastBody
const relationsHelper = mocked(RelationsHelper).mock.instances[5];
act(() => {
// invoke the callback of the VoiceBroadcastBody hook to simulate an ended broadcast
// @ts-ignore
mocked(relationsHelper.on).mock.calls[0][1](stoppedEvent);
room.addEventsToTimeline([stoppedEvent], true, room.getLiveTimeline());
});
});

View file

@ -1,45 +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 React from "react";
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { PlaybackControlButton, VoiceBroadcastPlaybackState } from "../../../../src/voice-broadcast";
describe("PlaybackControlButton", () => {
let onClick: () => void;
beforeEach(() => {
onClick = jest.fn();
});
it.each([
[VoiceBroadcastPlaybackState.Playing],
[VoiceBroadcastPlaybackState.Paused],
[VoiceBroadcastPlaybackState.Stopped],
])("should render state »%s« as expected", (state: VoiceBroadcastPlaybackState) => {
const result = render(<PlaybackControlButton state={state} onClick={onClick} />);
expect(result.container).toMatchSnapshot();
});
it("should call onClick on click", async () => {
render(<PlaybackControlButton state={VoiceBroadcastPlaybackState.Playing} onClick={onClick} />);
const button = screen.getByLabelText("pause voice broadcast");
await userEvent.click(button);
expect(onClick).toHaveBeenCalled();
});
});

View file

@ -1,45 +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 React from "react";
import { render, RenderResult } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { StopButton } from "../../../../src/voice-broadcast";
describe("StopButton", () => {
let result: RenderResult;
let onClick: () => {};
beforeEach(() => {
onClick = jest.fn();
result = render(<StopButton onClick={onClick} />);
});
it("should render as expected", () => {
expect(result.container).toMatchSnapshot();
});
describe("when clicking it", () => {
beforeEach(async () => {
await userEvent.click(result.getByLabelText("stop voice broadcast"));
});
it("should invoke the callback", () => {
expect(onClick).toHaveBeenCalled();
});
});
});

View file

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

View file

@ -5,11 +5,8 @@ exports[`LiveBadge should render the expected HTML 1`] = `
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>

View file

@ -1,55 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`PlaybackControlButton should render state »0« as expected 1`] = `
<div>
<div
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
</div>
`;
exports[`PlaybackControlButton should render state »1« as expected 1`] = `
<div>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
</div>
`;
exports[`PlaybackControlButton should render state »2« as expected 1`] = `
<div>
<div
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
</div>
`;

View file

@ -1,19 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`StopButton should render as expected 1`] = `
<div>
<div
aria-label="stop voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
/>
</div>
</div>
`;

View file

@ -0,0 +1,16 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastControl when rendering it should render as expected 1`] = `
<div>
<div
aria-label="test label"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
`;

View file

@ -22,11 +22,8 @@ exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadc
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
test user
@ -35,11 +32,8 @@ exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadc
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
@ -47,11 +41,8 @@ exports[`VoiceBroadcastHeader when rendering a live broadcast header with broadc
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
@ -81,11 +72,8 @@ exports[`VoiceBroadcastHeader when rendering a non-live broadcast header should
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
test user

View file

@ -64,9 +64,6 @@ describe("VoiceBroadcastPlaybackBody", () => {
describe("when rendering a buffering voice broadcast", () => {
beforeEach(() => {
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Buffering);
});
beforeEach(() => {
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
});
@ -75,18 +72,15 @@ describe("VoiceBroadcastPlaybackBody", () => {
});
});
describe("when rendering a broadcast", () => {
describe(`when rendering a ${VoiceBroadcastPlaybackState.Stopped} broadcast`, () => {
beforeEach(() => {
mocked(playback.getState).mockReturnValue(VoiceBroadcastPlaybackState.Stopped);
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("and clicking the play button", () => {
beforeEach(async () => {
await userEvent.click(renderResult.getByLabelText("resume voice broadcast"));
await userEvent.click(renderResult.getByLabelText("play voice broadcast"));
});
it("should toggle the recording", () => {
@ -94,4 +88,18 @@ describe("VoiceBroadcastPlaybackBody", () => {
});
});
});
describe.each([
VoiceBroadcastPlaybackState.Paused,
VoiceBroadcastPlaybackState.Playing,
])("when rendering a %s broadcast", (playbackState: VoiceBroadcastPlaybackState) => {
beforeEach(() => {
mocked(playback.getState).mockReturnValue(playbackState);
renderResult = render(<VoiceBroadcastPlaybackBody playback={playback} />);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
});
});

View file

@ -20,6 +20,7 @@ import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import {
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingBody,
} from "../../../../src/voice-broadcast";
@ -49,7 +50,7 @@ describe("VoiceBroadcastRecordingBody", () => {
room: roomId,
user: userId,
});
recording = new VoiceBroadcastRecording(infoEvent, client);
recording = new VoiceBroadcastRecording(infoEvent, client, VoiceBroadcastInfoState.Running);
});
describe("when rendering a live broadcast", () => {

View file

@ -22,12 +22,12 @@ import { MatrixClient, MatrixEvent } from "matrix-js-sdk/src/matrix";
import { sleep } from "matrix-js-sdk/src/utils";
import {
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingPip,
} from "../../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../../test-utils";
import { stubClient } from "../../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../../utils/test-utils";
// mock RoomAvatar, because it is doing too much fancy stuff
jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
@ -37,39 +37,52 @@ jest.mock("../../../../src/components/views/avatars/RoomAvatar", () => ({
}),
}));
jest.mock("../../../../src/audio/VoiceRecording");
describe("VoiceBroadcastRecordingPip", () => {
const userId = "@user:example.com";
const roomId = "!room:example.com";
let client: MatrixClient;
let infoEvent: MatrixEvent;
let recording: VoiceBroadcastRecording;
let renderResult: RenderResult;
const renderPip = (state: VoiceBroadcastInfoState) => {
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
state,
client.getUserId(),
client.getDeviceId(),
);
recording = new VoiceBroadcastRecording(infoEvent, client, state);
renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />);
};
beforeAll(() => {
client = stubClient();
infoEvent = mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
content: {},
room: roomId,
user: userId,
});
recording = new VoiceBroadcastRecording(infoEvent, client);
});
describe("when rendering", () => {
let renderResult: RenderResult;
describe("when rendering a started recording", () => {
beforeEach(() => {
renderResult = render(<VoiceBroadcastRecordingPip recording={recording} />);
renderPip(VoiceBroadcastInfoState.Started);
});
it("should create the expected result", () => {
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("and clicking the pause button", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("pause voice broadcast"));
});
it("should pause the recording", () => {
expect(recording.getState()).toBe(VoiceBroadcastInfoState.Paused);
});
});
describe("and clicking the stop button", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("stop voice broadcast"));
await userEvent.click(screen.getByLabelText("Stop Recording"));
// modal rendering has some weird sleeps
await sleep(100);
});
@ -89,4 +102,24 @@ describe("VoiceBroadcastRecordingPip", () => {
});
});
});
describe("when rendering a paused recording", () => {
beforeEach(() => {
renderPip(VoiceBroadcastInfoState.Paused);
});
it("should render as expected", () => {
expect(renderResult.container).toMatchSnapshot();
});
describe("and clicking the resume button", () => {
beforeEach(async () => {
await userEvent.click(screen.getByLabelText("resume voice broadcast"));
});
it("should resume the recording", () => {
expect(recording.getState()).toBe(VoiceBroadcastInfoState.Running);
});
});
});
});

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as expected 1`] = `
exports[`VoiceBroadcastPlaybackBody when rendering a 0 broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastPlaybackBody"
@ -25,11 +25,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
@ -38,11 +35,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
@ -50,11 +44,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
@ -64,15 +55,80 @@ exports[`VoiceBroadcastPlaybackBody when rendering a broadcast should render as
>
<div
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastPlaybackBody when rendering a 1 broadcast should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastPlaybackBody"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
</span>
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
</div>
<div
class="mx_LiveBadge"
>
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
</div>
<div
class="mx_VoiceBroadcastPlaybackBody_controls"
>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
@ -105,11 +161,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
@ -118,11 +171,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Voice broadcast
</div>
@ -130,11 +180,8 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
@ -148,6 +195,7 @@ exports[`VoiceBroadcastPlaybackBody when rendering a buffering voice broadcast s
<div
aria-label="Loading..."
class="mx_Spinner_icon"
data-testid="spinner"
role="progressbar"
style="width: 32px; height: 32px;"
/>

View file

@ -25,11 +25,8 @@ exports[`VoiceBroadcastRecordingBody when rendering a live broadcast should rend
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
@ -39,11 +36,8 @@ exports[`VoiceBroadcastRecordingBody when rendering a live broadcast should rend
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`VoiceBroadcastRecordingPip when rendering should create the expected result 1`] = `
exports[`VoiceBroadcastRecordingPip when rendering a paused recording should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastRecordingPip"
@ -25,25 +25,19 @@ exports[`VoiceBroadcastRecordingPip when rendering should create the expected re
<div
class="mx_VoiceBroadcastHeader_line"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@user:example.com
@userId:matrix.org
</span>
</div>
</div>
<div
class="mx_LiveBadge"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_live-badge"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
@ -55,16 +49,96 @@ exports[`VoiceBroadcastRecordingPip when rendering should create the expected re
class="mx_VoiceBroadcastRecordingPip_controls"
>
<div
aria-label="stop voice broadcast"
class="mx_AccessibleButton mx_BroadcastPlaybackControlButton"
aria-label="resume voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl mx_VoiceBroadcastControl-recording"
role="button"
tabindex="0"
>
<i
aria-hidden="true"
class="mx_Icon mx_Icon_16 mx_Icon_compound-secondary-content"
role="presentation"
style="mask-image: url(\\"image-file-stub\\");"
<div
class="mx_Icon mx_Icon_16"
/>
</div>
<div
aria-label="Stop Recording"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>
</div>
</div>
`;
exports[`VoiceBroadcastRecordingPip when rendering a started recording should render as expected 1`] = `
<div>
<div
class="mx_VoiceBroadcastRecordingPip"
>
<div
class="mx_VoiceBroadcastHeader"
>
<div
data-testid="room-avatar"
>
room avatar:
My room
</div>
<div
class="mx_VoiceBroadcastHeader_content"
>
<div
class="mx_VoiceBroadcastHeader_room"
>
My room
</div>
<div
class="mx_VoiceBroadcastHeader_line"
>
<div
class="mx_Icon mx_Icon_16"
/>
<span>
@userId:matrix.org
</span>
</div>
</div>
<div
class="mx_LiveBadge"
>
<div
class="mx_Icon mx_Icon_16"
/>
Live
</div>
</div>
<hr
class="mx_VoiceBroadcastRecordingPip_divider"
/>
<div
class="mx_VoiceBroadcastRecordingPip_controls"
>
<div
aria-label="pause voice broadcast"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
<div
aria-label="Stop Recording"
class="mx_AccessibleButton mx_VoiceBroadcastControl"
role="button"
tabindex="0"
>
<div
class="mx_Icon mx_Icon_16"
/>
</div>
</div>

View file

@ -20,6 +20,7 @@ import {
EventType,
MatrixClient,
MatrixEvent,
MatrixEventEvent,
MsgType,
RelationType,
Room,
@ -81,6 +82,7 @@ describe("VoiceBroadcastRecording", () => {
const setUpVoiceBroadcastRecording = () => {
voiceBroadcastRecording = new VoiceBroadcastRecording(infoEvent, client);
voiceBroadcastRecording.on(VoiceBroadcastRecordingEvent.StateChanged, onStateChanged);
jest.spyOn(voiceBroadcastRecording, "destroy");
jest.spyOn(voiceBroadcastRecording, "removeAllListeners");
};
@ -90,6 +92,25 @@ describe("VoiceBroadcastRecording", () => {
});
};
const itShouldSendAnInfoEvent = (state: VoiceBroadcastInfoState) => {
it(`should send a ${state} info event`, () => {
expect(client.sendStateEvent).toHaveBeenCalledWith(
roomId,
VoiceBroadcastInfoEventType,
{
device_id: client.getDeviceId(),
state,
["m.relates_to"]: {
rel_type: RelationType.Reference,
event_id: infoEvent.getId(),
},
},
client.getUserId(),
);
});
};
beforeEach(() => {
client = stubClient();
room = mkStubRoom(roomId, "Test Room", client);
@ -214,6 +235,18 @@ describe("VoiceBroadcastRecording", () => {
expect(voiceBroadcastRecorder.start).toHaveBeenCalled();
});
describe("and the info event is redacted", () => {
beforeEach(() => {
infoEvent.emit(MatrixEventEvent.BeforeRedaction, null, null);
});
itShouldBeInState(VoiceBroadcastInfoState.Stopped);
it("should destroy the recording", () => {
expect(voiceBroadcastRecording.destroy).toHaveBeenCalled();
});
});
describe("and receiving a call action", () => {
beforeEach(() => {
dis.dispatch({
@ -341,6 +374,26 @@ describe("VoiceBroadcastRecording", () => {
});
});
describe.each([
["pause", async () => voiceBroadcastRecording.pause()],
["toggle", async () => voiceBroadcastRecording.toggle()],
])("and calling %s", (_case: string, action: Function) => {
beforeEach(async () => {
await action();
});
itShouldBeInState(VoiceBroadcastInfoState.Paused);
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Paused);
it("should stop the recorder", () => {
expect(mocked(voiceBroadcastRecorder.stop)).toHaveBeenCalled();
});
it("should emit a paused state changed event", () => {
expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Paused);
});
});
describe("and calling destroy", () => {
beforeEach(() => {
voiceBroadcastRecording.destroy();
@ -356,6 +409,32 @@ describe("VoiceBroadcastRecording", () => {
});
});
});
describe("and it is in paused state", () => {
beforeEach(async () => {
await voiceBroadcastRecording.pause();
});
describe.each([
["resume", async () => voiceBroadcastRecording.resume()],
["toggle", async () => voiceBroadcastRecording.toggle()],
])("and calling %s", (_case: string, action: Function) => {
beforeEach(async () => {
await action();
});
itShouldBeInState(VoiceBroadcastInfoState.Running);
itShouldSendAnInfoEvent(VoiceBroadcastInfoState.Running);
it("should start the recorder", () => {
expect(mocked(voiceBroadcastRecorder.start)).toHaveBeenCalled();
});
it("should emit a running state changed event", () => {
expect(onStateChanged).toHaveBeenCalledWith(VoiceBroadcastInfoState.Running);
});
});
});
});
describe("when created for a Voice Broadcast Info with a Stopped relation", () => {
@ -363,7 +442,7 @@ describe("VoiceBroadcastRecording", () => {
infoEvent = mkVoiceBroadcastInfoEvent({
device_id: client.getDeviceId(),
state: VoiceBroadcastInfoState.Started,
chunk_length: 300,
chunk_length: 120,
});
const relationsContainer = {

View file

@ -18,28 +18,22 @@ import { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import {
VoiceBroadcastInfoEventType,
VoiceBroadcastRecordingsStore,
VoiceBroadcastRecordingsStoreEvent,
VoiceBroadcastRecording,
VoiceBroadcastInfoState,
} from "../../../src/voice-broadcast";
import { mkEvent, mkStubRoom, stubClient } from "../../test-utils";
jest.mock("../../../src/voice-broadcast/models/VoiceBroadcastRecording.ts", () => ({
VoiceBroadcastRecording: jest.fn().mockImplementation(
(
infoEvent: MatrixEvent,
client: MatrixClient,
) => ({ infoEvent, client }),
),
}));
import { mkStubRoom, stubClient } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
describe("VoiceBroadcastRecordingsStore", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
let infoEvent: MatrixEvent;
let otherInfoEvent: MatrixEvent;
let recording: VoiceBroadcastRecording;
let otherRecording: VoiceBroadcastRecording;
let recordings: VoiceBroadcastRecordingsStore;
let onCurrentChanged: (recording: VoiceBroadcastRecording) => void;
@ -51,22 +45,27 @@ describe("VoiceBroadcastRecordingsStore", () => {
return room;
}
});
infoEvent = mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
user: client.getUserId(),
room: roomId,
content: {},
});
recording = {
infoEvent,
} as unknown as VoiceBroadcastRecording;
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
);
otherInfoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
);
recording = new VoiceBroadcastRecording(infoEvent, client);
otherRecording = new VoiceBroadcastRecording(otherInfoEvent, client);
recordings = new VoiceBroadcastRecordingsStore();
onCurrentChanged = jest.fn();
recordings.on(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, onCurrentChanged);
});
afterEach(() => {
recording.destroy();
recordings.off(VoiceBroadcastRecordingsStoreEvent.CurrentChanged, onCurrentChanged);
});
@ -110,6 +109,32 @@ describe("VoiceBroadcastRecordingsStore", () => {
it("should emit a current changed event", () => {
expect(onCurrentChanged).toHaveBeenCalledWith(null);
});
it("and calling it again should work", () => {
recordings.clearCurrent();
expect(recordings.getCurrent()).toBeNull();
});
});
describe("and setting another recording and stopping the previous recording", () => {
beforeEach(() => {
recordings.setCurrent(otherRecording);
recording.stop();
});
it("should keep the current recording", () => {
expect(recordings.getCurrent()).toBe(otherRecording);
});
});
describe("and the recording stops", () => {
beforeEach(() => {
recording.stop();
});
it("should clear the current recording", () => {
expect(recordings.getCurrent()).toBeNull();
});
});
});
@ -133,10 +158,7 @@ describe("VoiceBroadcastRecordingsStore", () => {
});
it("should return the recording", () => {
expect(returnedRecording).toEqual({
infoEvent,
client,
});
expect(returnedRecording.infoEvent).toBe(infoEvent);
});
});
});

View file

@ -0,0 +1,111 @@
/*
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 { mocked } from "jest-mock";
import { ClientEvent, MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import {
findRoomLiveVoiceBroadcastFromUserAndDevice,
resumeVoiceBroadcastInRoom,
VoiceBroadcastInfoState,
VoiceBroadcastResumer,
} from "../../../src/voice-broadcast";
import { stubClient } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
jest.mock("../../../src/voice-broadcast/utils/findRoomLiveVoiceBroadcastFromUserAndDevice");
jest.mock("../../../src/voice-broadcast/utils/resumeVoiceBroadcastInRoom");
describe("VoiceBroadcastResumer", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
let resumer: VoiceBroadcastResumer;
let infoEvent: MatrixEvent;
beforeEach(() => {
client = stubClient();
jest.spyOn(client, "off");
room = new Room(roomId, client, client.getUserId());
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
if (getRoomId === roomId) return room;
});
resumer = new VoiceBroadcastResumer(client);
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("when there is no info event", () => {
beforeEach(() => {
client.emit(ClientEvent.Room, room);
});
it("should not resume a broadcast", () => {
expect(resumeVoiceBroadcastInRoom).not.toHaveBeenCalled();
});
});
describe("when there is an info event", () => {
beforeEach(() => {
mocked(findRoomLiveVoiceBroadcastFromUserAndDevice).mockImplementation((
findRoom: Room,
userId: string,
deviceId: string,
) => {
if (findRoom === room && userId === client.getUserId() && deviceId === client.getDeviceId()) {
return infoEvent;
}
});
client.emit(ClientEvent.Room, room);
});
it("should resume a broadcast", () => {
expect(resumeVoiceBroadcastInRoom).toHaveBeenCalledWith(
infoEvent,
room,
client,
);
});
describe("and emitting a room event again", () => {
beforeEach(() => {
client.emit(ClientEvent.Room, room);
});
it("should not resume the broadcast again", () => {
expect(resumeVoiceBroadcastInRoom).toHaveBeenCalledTimes(1);
});
});
});
describe("when calling destroy", () => {
beforeEach(() => {
resumer.destroy();
});
it("should deregister from the client", () => {
expect(client.off).toHaveBeenCalledWith(ClientEvent.Room, expect.any(Function));
});
});
});

View file

@ -23,7 +23,30 @@ exports[`startNewVoiceBroadcastRecording when the current user is allowed to sen
}
`;
exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of the current user should show an info dialog 1`] = `
exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there already is a live broadcast of the current user in the room should show an info dialog 1`] = `
[MockFunction] {
"calls": Array [
Array [
[Function],
Object {
"description": <p>
You are already recording a voice broadcast. Please end your current voice broadcast to start a new one.
</p>,
"hasCloseButton": true,
"title": "Can't start a new voice broadcast",
},
],
],
"results": Array [
Object {
"type": "return",
"value": undefined,
},
],
}
`;
exports[`startNewVoiceBroadcastRecording when the current user is allowed to send voice broadcast info state events when there is already a current voice broadcast should show an info dialog 1`] = `
[MockFunction] {
"calls": Array [
Array [

View file

@ -0,0 +1,127 @@
/*
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 { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import {
findRoomLiveVoiceBroadcastFromUserAndDevice,
VoiceBroadcastInfoEventType,
VoiceBroadcastInfoState,
} from "../../../src/voice-broadcast";
import { mkEvent, stubClient } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "./test-utils";
describe("findRoomLiveVoiceBroadcastFromUserAndDevice", () => {
const roomId = "!room:example.com";
let client: MatrixClient;
let room: Room;
const itShouldReturnNull = () => {
it("should return null", () => {
expect(findRoomLiveVoiceBroadcastFromUserAndDevice(
room,
client.getUserId(),
client.getDeviceId(),
)).toBeNull();
});
};
beforeAll(() => {
client = stubClient();
room = new Room(roomId, client, client.getUserId());
jest.spyOn(room.currentState, "getStateEvents");
mocked(client.getRoom).mockImplementation((getRoomId: string) => {
if (getRoomId === roomId) return room;
});
});
describe("when there is no info event", () => {
itShouldReturnNull();
});
describe("when there is an info event without content", () => {
beforeEach(() => {
room.currentState.setStateEvents([
mkEvent({
event: true,
type: VoiceBroadcastInfoEventType,
room: roomId,
user: client.getUserId(),
content: {},
}),
]);
});
itShouldReturnNull();
});
describe("when there is a stopped info event", () => {
beforeEach(() => {
room.currentState.setStateEvents([
mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Stopped,
client.getUserId(),
client.getDeviceId(),
),
]);
});
itShouldReturnNull();
});
describe("when there is a started info event from another device", () => {
beforeEach(() => {
const event = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Stopped,
client.getUserId(),
"JKL123",
);
room.currentState.setStateEvents([event]);
});
itShouldReturnNull();
});
describe("when there is a started info event", () => {
let event: MatrixEvent;
beforeEach(() => {
event = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
);
room.currentState.setStateEvents([event]);
});
it("should return this event", () => {
expect(room.currentState.getStateEvents).toHaveBeenCalledWith(
VoiceBroadcastInfoEventType,
client.getUserId(),
);
expect(findRoomLiveVoiceBroadcastFromUserAndDevice(
room,
client.getUserId(),
client.getDeviceId(),
)).toBe(event);
});
});
});

View file

@ -0,0 +1,60 @@
/*
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 { mocked } from "jest-mock";
import SdkConfig, { DEFAULTS } from "../../../src/SdkConfig";
import { getChunkLength } from "../../../src/voice-broadcast/utils/getChunkLength";
jest.mock("../../../src/SdkConfig");
describe("getChunkLength", () => {
afterEach(() => {
jest.resetAllMocks();
});
describe("when there is a value provided by Sdk config", () => {
beforeEach(() => {
mocked(SdkConfig.get).mockReturnValue({ chunk_length: 42 });
});
it("should return this value", () => {
expect(getChunkLength()).toBe(42);
});
});
describe("when Sdk config does not provide a value", () => {
beforeEach(() => {
DEFAULTS.voice_broadcast = {
chunk_length: 23,
};
});
it("should return this value", () => {
expect(getChunkLength()).toBe(23);
});
});
describe("if there are no defaults", () => {
beforeEach(() => {
DEFAULTS.voice_broadcast = undefined;
});
it("should return the fallback value", () => {
expect(getChunkLength()).toBe(120);
});
});
});

View file

@ -35,7 +35,12 @@ describe("hasRoomLiveVoiceBroadcast", () => {
sender: string,
) => {
room.currentState.setStateEvents([
mkVoiceBroadcastInfoStateEvent(room.roomId, state, sender),
mkVoiceBroadcastInfoStateEvent(
room.roomId,
state,
sender,
"ASD123",
),
]);
};

View file

@ -0,0 +1,110 @@
/*
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 { mocked } from "jest-mock";
import { MatrixClient, MatrixEvent, Room } from "matrix-js-sdk/src/matrix";
import {
resumeVoiceBroadcastInRoom,
VoiceBroadcastInfoState,
VoiceBroadcastRecording,
VoiceBroadcastRecordingsStore,
} from "../../../src/voice-broadcast";
import { stubClient } from "../../test-utils";
import { mkVoiceBroadcastInfoStateEvent } from "../utils/test-utils";
const mockRecording = jest.fn();
jest.mock("../../../src/voice-broadcast/models/VoiceBroadcastRecording", () => ({
...jest.requireActual("../../../src/voice-broadcast/models/VoiceBroadcastRecording") as object,
VoiceBroadcastRecording: jest.fn().mockImplementation(() => mockRecording),
}));
describe("resumeVoiceBroadcastInRoom", () => {
let client: MatrixClient;
const roomId = "!room:example.com";
let room: Room;
let startedInfoEvent: MatrixEvent;
let stoppedInfoEvent: MatrixEvent;
const itShouldStartAPausedRecording = () => {
it("should start a paused recording", () => {
expect(VoiceBroadcastRecording).toHaveBeenCalledWith(
startedInfoEvent,
client,
VoiceBroadcastInfoState.Paused,
);
expect(VoiceBroadcastRecordingsStore.instance().setCurrent).toHaveBeenCalledWith(mockRecording);
});
};
beforeEach(() => {
client = stubClient();
room = new Room(roomId, client, client.getUserId());
jest.spyOn(room, "findEventById");
jest.spyOn(VoiceBroadcastRecordingsStore.instance(), "setCurrent").mockImplementation();
startedInfoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
);
stoppedInfoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Stopped,
client.getUserId(),
client.getDeviceId(),
startedInfoEvent,
);
});
afterEach(() => {
jest.clearAllMocks();
});
describe("when called with a stopped info event", () => {
describe("and there is a related event", () => {
beforeEach(() => {
mocked(room.findEventById).mockReturnValue(startedInfoEvent);
resumeVoiceBroadcastInRoom(stoppedInfoEvent, room, client);
});
itShouldStartAPausedRecording();
});
describe("and there is no related event", () => {
beforeEach(() => {
mocked(room.findEventById).mockReturnValue(null);
resumeVoiceBroadcastInRoom(stoppedInfoEvent, room, client);
});
it("should not start a broadcast", () => {
expect(VoiceBroadcastRecording).not.toHaveBeenCalled();
expect(VoiceBroadcastRecordingsStore.instance().setCurrent).not.toHaveBeenCalled();
});
});
});
describe("when called with a started info event", () => {
beforeEach(() => {
resumeVoiceBroadcastInRoom(startedInfoEvent, room, client);
});
itShouldStartAPausedRecording();
});
});

View file

@ -67,9 +67,15 @@ describe("startNewVoiceBroadcastRecording", () => {
recordingsStore = {
setCurrent: jest.fn(),
getCurrent: jest.fn(),
} as unknown as VoiceBroadcastRecordingsStore;
infoEvent = mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Started, client.getUserId());
infoEvent = mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Started,
client.getUserId(),
client.getDeviceId(),
);
otherEvent = mkEvent({
event: true,
type: EventType.RoomMember,
@ -121,7 +127,7 @@ describe("startNewVoiceBroadcastRecording", () => {
roomId,
VoiceBroadcastInfoEventType,
{
chunk_length: 300,
chunk_length: 120,
device_id: client.getDeviceId(),
state: VoiceBroadcastInfoState.Started,
},
@ -132,10 +138,33 @@ describe("startNewVoiceBroadcastRecording", () => {
});
});
describe("when there already is a live broadcast of the current user", () => {
describe("when there is already a current voice broadcast", () => {
beforeEach(async () => {
mocked(recordingsStore.getCurrent).mockReturnValue(
new VoiceBroadcastRecording(infoEvent, client),
);
result = await startNewVoiceBroadcastRecording(room, client, recordingsStore);
});
it("should not start a voice broadcast", () => {
expect(result).toBeNull();
});
it("should show an info dialog", () => {
expect(Modal.createDialog).toMatchSnapshot();
});
});
describe("when there already is a live broadcast of the current user in the room", () => {
beforeEach(async () => {
room.currentState.setStateEvents([
mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Running, client.getUserId()),
mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Running,
client.getUserId(),
client.getDeviceId(),
),
]);
result = await startNewVoiceBroadcastRecording(room, client, recordingsStore);
@ -153,7 +182,12 @@ describe("startNewVoiceBroadcastRecording", () => {
describe("when there already is a live broadcast of another user", () => {
beforeEach(async () => {
room.currentState.setStateEvents([
mkVoiceBroadcastInfoStateEvent(roomId, VoiceBroadcastInfoState.Running, otherUserId),
mkVoiceBroadcastInfoStateEvent(
roomId,
VoiceBroadcastInfoState.Running,
otherUserId,
"ASD123",
),
]);
result = await startNewVoiceBroadcastRecording(room, client, recordingsStore);

View file

@ -22,16 +22,29 @@ import { mkEvent } from "../../test-utils";
export const mkVoiceBroadcastInfoStateEvent = (
roomId: string,
state: VoiceBroadcastInfoState,
sender: string,
senderId: string,
senderDeviceId: string,
startedInfoEvent?: MatrixEvent,
): MatrixEvent => {
const relationContent = {};
if (startedInfoEvent) {
relationContent["m.relates_to"] = {
event_id: startedInfoEvent.getId(),
rel_type: "m.reference",
};
}
return mkEvent({
event: true,
room: roomId,
user: sender,
user: senderId,
type: VoiceBroadcastInfoEventType,
skey: sender,
skey: senderId,
content: {
state,
device_id: senderDeviceId,
...relationContent,
},
});
};