Prepare for Element Call integration (#9224)
* Improve accessibility and testability of Tooltip Adding a role to Tooltip was motivated by React Testing Library's reliance on accessibility-related attributes to locate elements. * Make the ReadyWatchingStore constructor safer The ReadyWatchingStore constructor previously had a chance to immediately call onReady, which was dangerous because it was potentially calling the derived class's onReady at a point when the derived class hadn't even finished construction yet. In normal usage, I guess this never was a problem, but it was causing some of the tests I was writing to crash. This is solved by separating out the onReady call into a start method. * Rename 1:1 call components to 'LegacyCall' to reflect the fact that they're slated for removal, and to not clash with the new Call code. * Refactor VideoChannelStore into Call and CallStore Call is an abstract class that currently only has a Jitsi implementation, but this will make it easy to later add an Element Call implementation. * Remove WidgetReady, ClientReady, and ForceHangupCall hacks These are no longer used by the new Jitsi call implementation, and can be removed. * yarn i18n * Delete call map entries instead of inserting nulls * Allow multiple active calls and consolidate call listeners * Fix a race condition when creating a video room * Un-hardcode the media device fallback labels * Apply misc code review fixes * yarn i18n * Disconnect from calls more politely on logout * Fix some strict mode errors * Fix another updateRoom race condition
This commit is contained in:
parent
50f6986f6c
commit
0d6a550c33
107 changed files with 2573 additions and 2157 deletions
|
@ -19,9 +19,9 @@ import { CallEvent, CallState, CallType } from 'matrix-js-sdk/src/webrtc/call';
|
|||
import EventEmitter from 'events';
|
||||
import { mocked } from 'jest-mock';
|
||||
|
||||
import CallHandler, {
|
||||
CallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
|
||||
} from '../src/CallHandler';
|
||||
import LegacyCallHandler, {
|
||||
LegacyCallHandlerEvent, PROTOCOL_PSTN, PROTOCOL_PSTN_PREFIXED, PROTOCOL_SIP_NATIVE, PROTOCOL_SIP_VIRTUAL,
|
||||
} from '../src/LegacyCallHandler';
|
||||
import { stubClient, mkStubRoom, untilDispatch } from './test-utils';
|
||||
import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
||||
import DMRoomMap from '../src/utils/DMRoomMap';
|
||||
|
@ -109,7 +109,7 @@ class FakeCall extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
function untilCallHandlerEvent(callHandler: CallHandler, event: CallHandlerEvent): Promise<void> {
|
||||
function untilCallHandlerEvent(callHandler: LegacyCallHandler, event: LegacyCallHandlerEvent): Promise<void> {
|
||||
return new Promise<void>((resolve) => {
|
||||
callHandler.addListener(event, () => {
|
||||
resolve();
|
||||
|
@ -117,7 +117,7 @@ function untilCallHandlerEvent(callHandler: CallHandler, event: CallHandlerEvent
|
|||
});
|
||||
}
|
||||
|
||||
describe('CallHandler', () => {
|
||||
describe('LegacyCallHandler', () => {
|
||||
let dmRoomMap;
|
||||
let callHandler;
|
||||
let audioElement: HTMLAudioElement;
|
||||
|
@ -145,7 +145,7 @@ describe('CallHandler', () => {
|
|||
});
|
||||
};
|
||||
|
||||
callHandler = new CallHandler();
|
||||
callHandler = new LegacyCallHandler();
|
||||
callHandler.start();
|
||||
|
||||
mocked(getFunctionalMembers).mockReturnValue([
|
||||
|
@ -251,7 +251,7 @@ describe('CallHandler', () => {
|
|||
callHandler.stop();
|
||||
DMRoomMap.setShared(null);
|
||||
// @ts-ignore
|
||||
window.mxCallHandler = null;
|
||||
window.mxLegacyCallHandler = null;
|
||||
fakeCall = null;
|
||||
MatrixClientPeg.unset();
|
||||
|
||||
|
@ -295,14 +295,14 @@ describe('CallHandler', () => {
|
|||
it('should move calls between rooms when remote asserted identity changes', async () => {
|
||||
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
|
||||
|
||||
await untilCallHandlerEvent(callHandler, CallHandlerEvent.CallState);
|
||||
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
|
||||
|
||||
// We placed the call in Alice's room so it should start off there
|
||||
expect(callHandler.getCallForRoom(NATIVE_ROOM_ALICE)).toBe(fakeCall);
|
||||
|
||||
let callRoomChangeEventCount = 0;
|
||||
const roomChangePromise = new Promise<void>(resolve => {
|
||||
callHandler.addListener(CallHandlerEvent.CallChangeRoom, () => {
|
||||
callHandler.addListener(LegacyCallHandlerEvent.CallChangeRoom, () => {
|
||||
++callRoomChangeEventCount;
|
||||
resolve();
|
||||
});
|
||||
|
@ -343,9 +343,9 @@ describe('CallHandler', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('CallHandler without third party protocols', () => {
|
||||
describe('LegacyCallHandler without third party protocols', () => {
|
||||
let dmRoomMap;
|
||||
let callHandler: CallHandler;
|
||||
let callHandler: LegacyCallHandler;
|
||||
let audioElement: HTMLAudioElement;
|
||||
let fakeCall;
|
||||
|
||||
|
@ -363,7 +363,7 @@ describe('CallHandler without third party protocols', () => {
|
|||
throw new Error("Endpoint unsupported.");
|
||||
};
|
||||
|
||||
callHandler = new CallHandler();
|
||||
callHandler = new LegacyCallHandler();
|
||||
callHandler.start();
|
||||
|
||||
const nativeRoomAlice = mkStubDM(NATIVE_ROOM_ALICE, NATIVE_ALICE);
|
||||
|
@ -406,7 +406,7 @@ describe('CallHandler without third party protocols', () => {
|
|||
callHandler.stop();
|
||||
DMRoomMap.setShared(null);
|
||||
// @ts-ignore
|
||||
window.mxCallHandler = null;
|
||||
window.mxLegacyCallHandler = null;
|
||||
fakeCall = null;
|
||||
MatrixClientPeg.unset();
|
||||
|
||||
|
@ -417,7 +417,7 @@ describe('CallHandler without third party protocols', () => {
|
|||
it('should still start a native call', async () => {
|
||||
callHandler.placeCall(NATIVE_ROOM_ALICE, CallType.Voice);
|
||||
|
||||
await untilCallHandlerEvent(callHandler, CallHandlerEvent.CallState);
|
||||
await untilCallHandlerEvent(callHandler, LegacyCallHandlerEvent.CallState);
|
||||
|
||||
// Check that a call was started: its room on the protocol level
|
||||
// should be the virtual room
|
|
@ -23,7 +23,7 @@ import { MatrixClientPeg } from '../src/MatrixClientPeg';
|
|||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from '../src/models/LocalRoom';
|
||||
import { RoomViewStore } from '../src/stores/RoomViewStore';
|
||||
import SettingsStore from '../src/settings/SettingsStore';
|
||||
import CallHandler from '../src/CallHandler';
|
||||
import LegacyCallHandler from '../src/LegacyCallHandler';
|
||||
|
||||
describe('SlashCommands', () => {
|
||||
let client: MatrixClient;
|
||||
|
@ -120,7 +120,7 @@ describe('SlashCommands', () => {
|
|||
describe("isEnabled", () => {
|
||||
describe("when virtual rooms are supported", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(CallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(true);
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(true);
|
||||
});
|
||||
|
||||
it("should return true for Room", () => {
|
||||
|
@ -136,7 +136,7 @@ describe('SlashCommands', () => {
|
|||
|
||||
describe("when virtual rooms are not supported", () => {
|
||||
beforeEach(() => {
|
||||
jest.spyOn(CallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(false);
|
||||
jest.spyOn(LegacyCallHandler.instance, "getSupportsVirtualRooms").mockReturnValue(false);
|
||||
});
|
||||
|
||||
it("should return false for Room", () => {
|
||||
|
|
|
@ -20,14 +20,14 @@ import { CallState } from "matrix-js-sdk/src/webrtc/call";
|
|||
|
||||
import { stubClient } from '../../test-utils';
|
||||
import { MatrixClientPeg } from '../../../src/MatrixClientPeg';
|
||||
import CallEventGrouper, { CustomCallState } from "../../../src/components/structures/CallEventGrouper";
|
||||
import LegacyCallEventGrouper, { CustomCallState } from "../../../src/components/structures/LegacyCallEventGrouper";
|
||||
|
||||
const MY_USER_ID = "@me:here";
|
||||
const THEIR_USER_ID = "@they:here";
|
||||
|
||||
let client: MatrixClient;
|
||||
|
||||
describe('CallEventGrouper', () => {
|
||||
describe('LegacyCallEventGrouper', () => {
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
client = MatrixClientPeg.get();
|
||||
|
@ -37,7 +37,7 @@ describe('CallEventGrouper', () => {
|
|||
});
|
||||
|
||||
it("detects a missed call", () => {
|
||||
const grouper = new CallEventGrouper();
|
||||
const grouper = new LegacyCallEventGrouper();
|
||||
|
||||
grouper.add({
|
||||
getContent: () => {
|
||||
|
@ -57,8 +57,8 @@ describe('CallEventGrouper', () => {
|
|||
});
|
||||
|
||||
it("detects an ended call", () => {
|
||||
const grouperHangup = new CallEventGrouper();
|
||||
const grouperReject = new CallEventGrouper();
|
||||
const grouperHangup = new LegacyCallEventGrouper();
|
||||
const grouperReject = new LegacyCallEventGrouper();
|
||||
|
||||
grouperHangup.add({
|
||||
getContent: () => {
|
||||
|
@ -119,7 +119,7 @@ describe('CallEventGrouper', () => {
|
|||
});
|
||||
|
||||
it("detects call type", () => {
|
||||
const grouper = new CallEventGrouper();
|
||||
const grouper = new LegacyCallEventGrouper();
|
||||
|
||||
grouper.add({
|
||||
getContent: () => {
|
|
@ -15,110 +15,103 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from "enzyme";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient, IMyDevice } from "matrix-js-sdk/src/client";
|
||||
import { render, screen, act, fireEvent, waitFor, cleanup } from "@testing-library/react";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { MatrixWidgetType } from "matrix-widget-api";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import type { Call } from "../../../src/models/Call";
|
||||
import {
|
||||
stubClient,
|
||||
stubVideoChannelStore,
|
||||
StubVideoChannelStore,
|
||||
mkRoom,
|
||||
mkRoomMember,
|
||||
wrapInMatrixClientContext,
|
||||
mockStateEventImplementation,
|
||||
mkVideoChannelMember,
|
||||
useMockedCalls,
|
||||
MockedCall,
|
||||
setupAsyncStoreWithClient,
|
||||
} from "../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
|
||||
import { VIDEO_CHANNEL_MEMBER } from "../../../src/utils/VideoChannelUtils";
|
||||
import WidgetStore from "../../../src/stores/WidgetStore";
|
||||
import _VideoRoomView from "../../../src/components/structures/VideoRoomView";
|
||||
import VideoLobby from "../../../src/components/views/voip/VideoLobby";
|
||||
import AppTile from "../../../src/components/views/elements/AppTile";
|
||||
import { VideoRoomView as UnwrappedVideoRoomView } from "../../../src/components/structures/VideoRoomView";
|
||||
import { WidgetMessagingStore } from "../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { CallStore } from "../../../src/stores/CallStore";
|
||||
import { ConnectionState } from "../../../src/models/Call";
|
||||
|
||||
const VideoRoomView = wrapInMatrixClientContext(_VideoRoomView);
|
||||
const VideoRoomView = wrapInMatrixClientContext(UnwrappedVideoRoomView);
|
||||
|
||||
describe("VideoRoomView", () => {
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([{
|
||||
id: "1",
|
||||
eventId: "$1:example.org",
|
||||
roomId: "!1:example.org",
|
||||
type: MatrixWidgetType.JitsiMeet,
|
||||
url: "https://example.org",
|
||||
name: "Video channel",
|
||||
creatorUserId: "@alice:example.org",
|
||||
avatar_url: null,
|
||||
data: { isVideoChannel: true },
|
||||
}]);
|
||||
useMockedCalls();
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: { enumerateDevices: () => [] },
|
||||
value: {
|
||||
enumerateDevices: async () => [],
|
||||
getUserMedia: () => null,
|
||||
},
|
||||
});
|
||||
|
||||
let cli: MatrixClient;
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let store: StubVideoChannelStore;
|
||||
let call: Call;
|
||||
let widget: Widget;
|
||||
let alice: RoomMember;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
jest.spyOn(WidgetStore.instance, "matrixClient", "get").mockReturnValue(cli);
|
||||
store = stubVideoChannelStore();
|
||||
room = mkRoom(cli, "!1:example.org");
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
jest.spyOn(room, "getMember").mockImplementation(userId => userId === alice.userId ? alice : null);
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
call = CallStore.instance.get(room.roomId);
|
||||
if (call === null) throw new Error("Failed to create call");
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
|
||||
it("removes stuck devices on mount", async () => {
|
||||
// Simulate an unclean disconnect
|
||||
store.roomId = "!1:example.org";
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
|
||||
const devices: IMyDevice[] = [
|
||||
{
|
||||
device_id: cli.getDeviceId(),
|
||||
last_seen_ts: new Date().valueOf(),
|
||||
},
|
||||
{
|
||||
device_id: "went offline 2 hours ago",
|
||||
last_seen_ts: new Date().valueOf() - 1000 * 60 * 60 * 2,
|
||||
},
|
||||
];
|
||||
mocked(cli).getDevices.mockResolvedValue({ devices });
|
||||
const renderView = async (): Promise<void> => {
|
||||
render(<VideoRoomView room={room} resizing={false} />);
|
||||
await act(() => Promise.resolve()); // Let effects settle
|
||||
};
|
||||
|
||||
// Make both devices be stuck
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
mkVideoChannelMember(cli.getUserId(), devices.map(d => d.device_id)),
|
||||
]));
|
||||
|
||||
mount(<VideoRoomView room={room} resizing={false} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
// All devices should have been removed
|
||||
expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
"!1:example.org",
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
{ devices: [], expires_ts: expect.any(Number) },
|
||||
cli.getUserId(),
|
||||
);
|
||||
it("calls clean on mount", async () => {
|
||||
const cleanSpy = jest.spyOn(call, "clean");
|
||||
await renderView();
|
||||
expect(cleanSpy).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("shows lobby and keeps widget loaded when disconnected", async () => {
|
||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
expect(view.find(VideoLobby).exists()).toEqual(true);
|
||||
expect(view.find(AppTile).exists()).toEqual(true);
|
||||
await renderView();
|
||||
screen.getByRole("button", { name: "Join" });
|
||||
screen.getAllByText(/\bwidget\b/i);
|
||||
});
|
||||
|
||||
it("only shows widget when connected", async () => {
|
||||
store.connect("!1:example.org");
|
||||
const view = mount(<VideoRoomView room={room} resizing={false} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
|
||||
expect(view.find(VideoLobby).exists()).toEqual(false);
|
||||
expect(view.find(AppTile).exists()).toEqual(true);
|
||||
await renderView();
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Connected));
|
||||
expect(screen.queryByRole("button", { name: "Join" })).toBe(null);
|
||||
screen.getAllByText(/\bwidget\b/i);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -93,7 +93,7 @@ describe("AppTile", () => {
|
|||
url: "https://example.com",
|
||||
name: "Example 1",
|
||||
creatorUserId: cli.getUserId(),
|
||||
avatar_url: null,
|
||||
avatar_url: undefined,
|
||||
};
|
||||
app2 = {
|
||||
id: "1",
|
||||
|
@ -103,7 +103,7 @@ describe("AppTile", () => {
|
|||
url: "https://example.com",
|
||||
name: "Example 2",
|
||||
creatorUserId: cli.getUserId(),
|
||||
avatar_url: null,
|
||||
avatar_url: undefined,
|
||||
};
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockImplementation(roomId => {
|
||||
if (roomId === "r1") return [app1];
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; top: 6px; left: 0px; transform: translate(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -15,6 +16,7 @@ exports[`<TooltipTarget /> displays Bottom aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; top: -50px; left: 0px; transform: translate(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -27,6 +29,7 @@ exports[`<TooltipTarget /> displays InnerBottom aligned tooltip on mouseover 1`]
|
|||
exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; right: 1030px; top: 0px; transform: translateY(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -39,6 +42,7 @@ exports[`<TooltipTarget /> displays Left aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -51,6 +55,7 @@ exports[`<TooltipTarget /> displays Natural aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; left: 6px; top: 0px; transform: translateY(-50%);"
|
||||
>
|
||||
<div
|
||||
|
@ -63,6 +68,7 @@ exports[`<TooltipTarget /> displays Right aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; top: -6px; left: 0px; transform: translate(-50%, -100%);"
|
||||
>
|
||||
<div
|
||||
|
@ -75,6 +81,7 @@ exports[`<TooltipTarget /> displays Top aligned tooltip on mouseover 1`] = `
|
|||
exports[`<TooltipTarget /> displays TopRight aligned tooltip on mouseover 1`] = `
|
||||
<div
|
||||
class="mx_Tooltip test tooltipClassName mx_Tooltip_visible"
|
||||
role="tooltip"
|
||||
style="display: block; top: -6px; right: 1024px; transform: translateY(-100%);"
|
||||
>
|
||||
<div
|
||||
|
|
|
@ -15,148 +15,131 @@ limitations under the License.
|
|||
*/
|
||||
|
||||
import React from "react";
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from "enzyme";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { render, screen, act } from "@testing-library/react";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import {
|
||||
stubClient,
|
||||
mockStateEventImplementation,
|
||||
mkRoom,
|
||||
mkVideoChannelMember,
|
||||
stubVideoChannelStore,
|
||||
StubVideoChannelStore,
|
||||
mkRoomMember,
|
||||
MockedCall,
|
||||
useMockedCalls,
|
||||
setupAsyncStoreWithClient,
|
||||
} from "../../../test-utils";
|
||||
import { STUCK_DEVICE_TIMEOUT_MS } from "../../../../src/utils/VideoChannelUtils";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
import RoomTile from "../../../../src/components/views/rooms/RoomTile";
|
||||
import SettingsStore from "../../../../src/settings/SettingsStore";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import PlatformPeg from "../../../../src/PlatformPeg";
|
||||
import BasePlatform from "../../../../src/BasePlatform";
|
||||
|
||||
const mockGetMember = (room: Room, getMembership: (userId: string) => string = () => "join") => {
|
||||
mocked(room).getMember.mockImplementation(userId => ({
|
||||
userId,
|
||||
membership: getMembership(userId),
|
||||
name: userId,
|
||||
rawDisplayName: userId,
|
||||
roomId: "!1:example.org",
|
||||
getAvatarUrl: () => {},
|
||||
getMxcAvatarUrl: () => {},
|
||||
}) as unknown as RoomMember);
|
||||
};
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
|
||||
describe("RoomTile", () => {
|
||||
jest.spyOn(PlatformPeg, 'get')
|
||||
jest.spyOn(PlatformPeg, "get")
|
||||
.mockReturnValue({ overrideBrowserShortcuts: () => false } as unknown as BasePlatform);
|
||||
useMockedCalls();
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: { enumerateDevices: async () => [] },
|
||||
});
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
|
||||
let cli: MatrixClient;
|
||||
let store: StubVideoChannelStore;
|
||||
beforeEach(() => {
|
||||
const realGetValue = SettingsStore.getValue;
|
||||
SettingsStore.getValue = <T, >(name: string, roomId?: string): T => {
|
||||
if (name === "feature_video_rooms") {
|
||||
return true as unknown as T;
|
||||
}
|
||||
return realGetValue(name, roomId);
|
||||
};
|
||||
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
store = stubVideoChannelStore();
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
DMRoomMap.makeShared();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks();
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
describe("video rooms", () => {
|
||||
describe("call subtitle", () => {
|
||||
let room: Room;
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
|
||||
beforeEach(() => {
|
||||
room = mkRoom(cli, "!1:example.org");
|
||||
mocked(room.isElementVideoRoom).mockReturnValue(true);
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
call = CallStore.instance.get(room.roomId) as MockedCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
|
||||
render(
|
||||
<RoomTile
|
||||
room={room}
|
||||
showMessagePreview={false}
|
||||
isMinimized={false}
|
||||
tag={DefaultTagID.Untagged}
|
||||
/>,
|
||||
);
|
||||
});
|
||||
|
||||
const mountTile = () => mount(
|
||||
<RoomTile
|
||||
room={room}
|
||||
showMessagePreview={false}
|
||||
isMinimized={false}
|
||||
tag={DefaultTagID.Untagged}
|
||||
/>,
|
||||
);
|
||||
|
||||
it("tracks connection state", () => {
|
||||
const tile = mountTile();
|
||||
expect(tile.find(".mx_VideoRoomSummary_indicator").text()).toEqual("Video");
|
||||
|
||||
act(() => { store.startConnect("!1:example.org"); });
|
||||
tile.update();
|
||||
expect(tile.find(".mx_VideoRoomSummary_indicator").text()).toEqual("Joining…");
|
||||
|
||||
act(() => { store.connect("!1:example.org"); });
|
||||
tile.update();
|
||||
expect(tile.find(".mx_VideoRoomSummary_indicator").text()).toEqual("Joined");
|
||||
|
||||
act(() => { store.disconnect(); });
|
||||
tile.update();
|
||||
expect(tile.find(".mx_VideoRoomSummary_indicator").text()).toEqual("Video");
|
||||
afterEach(() => {
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
|
||||
it("displays connected members", () => {
|
||||
mockGetMember(room, userId => userId === "@chris:example.org" ? "leave" : "join");
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
// A user connected from 2 devices
|
||||
mkVideoChannelMember("@alice:example.org", ["device 1", "device 2"]),
|
||||
// A disconnected user
|
||||
mkVideoChannelMember("@bob:example.org", []),
|
||||
// A user that claims to have a connected device, but has left the room
|
||||
mkVideoChannelMember("@chris:example.org", ["device 1"]),
|
||||
]));
|
||||
it("tracks connection state", async () => {
|
||||
screen.getByText("Video");
|
||||
|
||||
const tile = mountTile();
|
||||
// Insert an await point in the connection method so we can inspect
|
||||
// the intermediate connecting state
|
||||
let completeConnection: () => void;
|
||||
const connectionCompleted = new Promise<void>(resolve => completeConnection = resolve);
|
||||
jest.spyOn(call, "performConnection").mockReturnValue(connectionCompleted);
|
||||
|
||||
// Only Alice should display as connected
|
||||
expect(tile.find(".mx_VideoRoomSummary_participants").text()).toEqual("1");
|
||||
await Promise.all([
|
||||
(async () => {
|
||||
await screen.findByText("Joining…");
|
||||
const joinedFound = screen.findByText("Joined");
|
||||
completeConnection();
|
||||
await joinedFound;
|
||||
})(),
|
||||
call.connect(),
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
screen.findByText("Video"),
|
||||
call.disconnect(),
|
||||
]);
|
||||
});
|
||||
|
||||
it("reflects local echo in connected members", () => {
|
||||
mockGetMember(room);
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
// Make the remote echo claim that we're connected, while leaving the store disconnected
|
||||
mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]),
|
||||
]));
|
||||
it("tracks participants", () => {
|
||||
const alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
const bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
const carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||
|
||||
const tile = mountTile();
|
||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||
|
||||
// Because of our local echo, we should still appear as disconnected
|
||||
expect(tile.find(".mx_VideoRoomSummary_participants").exists()).toEqual(false);
|
||||
});
|
||||
act(() => { call.participants = new Set([alice]); });
|
||||
expect(screen.getByLabelText("1 participant").textContent).toBe("1");
|
||||
|
||||
it("doesn't count members whose device data has expired", () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
act(() => { call.participants = new Set([alice, bob, carol]); });
|
||||
expect(screen.getByLabelText("3 participants").textContent).toBe("3");
|
||||
|
||||
mockGetMember(room);
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
mkVideoChannelMember("@alice:example.org", ["device 1"], STUCK_DEVICE_TIMEOUT_MS),
|
||||
]));
|
||||
|
||||
const tile = mountTile();
|
||||
|
||||
expect(tile.find(".mx_VideoRoomSummary_participants").text()).toEqual("1");
|
||||
// Expire Alice's device data
|
||||
act(() => { jest.advanceTimersByTime(STUCK_DEVICE_TIMEOUT_MS); });
|
||||
tile.update();
|
||||
expect(tile.find(".mx_VideoRoomSummary_participants").exists()).toEqual(false);
|
||||
act(() => { call.participants = new Set(); });
|
||||
expect(screen.queryByLabelText(/participant/)).toBe(null);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
181
test/components/views/voip/CallLobby-test.tsx
Normal file
181
test/components/views/voip/CallLobby-test.tsx
Normal file
|
@ -0,0 +1,181 @@
|
|||
/*
|
||||
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 { zip } from "lodash";
|
||||
import { render, screen, act, fireEvent, waitFor } from "@testing-library/react";
|
||||
import { mocked, Mocked } from "jest-mock";
|
||||
import { MatrixClient, PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import {
|
||||
stubClient,
|
||||
mkRoomMember,
|
||||
MockedCall,
|
||||
useMockedCalls,
|
||||
setupAsyncStoreWithClient,
|
||||
} from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import { CallLobby } from "../../../../src/components/views/voip/CallLobby";
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
|
||||
describe("CallLobby", () => {
|
||||
useMockedCalls();
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: {
|
||||
enumerateDevices: jest.fn(),
|
||||
getUserMedia: () => null,
|
||||
},
|
||||
});
|
||||
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let call: MockedCall;
|
||||
let widget: Widget;
|
||||
let alice: RoomMember;
|
||||
|
||||
beforeEach(() => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([]);
|
||||
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
jest.spyOn(room, "getMember").mockImplementation(userId => userId === alice.userId ? alice : null);
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(room, "1");
|
||||
call = CallStore.instance.get(room.roomId) as MockedCall;
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
});
|
||||
|
||||
const renderLobby = async (): Promise<void> => {
|
||||
render(<CallLobby room={room} call={call} />);
|
||||
await act(() => Promise.resolve()); // Let effects settle
|
||||
};
|
||||
|
||||
it("tracks participants", async () => {
|
||||
const bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
const carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||
|
||||
const expectAvatars = (userIds: string[]) => {
|
||||
const avatars = screen.queryAllByRole("button", { name: "Avatar" });
|
||||
expect(userIds.length).toBe(avatars.length);
|
||||
|
||||
for (const [userId, avatar] of zip(userIds, avatars)) {
|
||||
fireEvent.focus(avatar!);
|
||||
screen.getByRole("tooltip", { name: userId });
|
||||
}
|
||||
};
|
||||
|
||||
await renderLobby();
|
||||
expect(screen.queryByLabelText(/joined/)).toBe(null);
|
||||
expectAvatars([]);
|
||||
|
||||
act(() => { call.participants = new Set([alice]); });
|
||||
screen.getByText("1 person joined");
|
||||
expectAvatars([alice.userId]);
|
||||
|
||||
act(() => { call.participants = new Set([alice, bob, carol]); });
|
||||
screen.getByText("3 people joined");
|
||||
expectAvatars([alice.userId, bob.userId, carol.userId]);
|
||||
|
||||
act(() => { call.participants = new Set(); });
|
||||
expect(screen.queryByLabelText(/joined/)).toBe(null);
|
||||
expectAvatars([]);
|
||||
});
|
||||
|
||||
describe("device buttons", () => {
|
||||
it("hide when no devices are available", async () => {
|
||||
await renderLobby();
|
||||
expect(screen.queryByRole("button", { name: /microphone/ })).toBe(null);
|
||||
expect(screen.queryByRole("button", { name: /camera/ })).toBe(null);
|
||||
});
|
||||
|
||||
it("show without dropdown when only one device is available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([{
|
||||
deviceId: "1",
|
||||
groupId: "1",
|
||||
label: "Webcam",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
}]);
|
||||
|
||||
await renderLobby();
|
||||
screen.getByRole("button", { name: /camera/ });
|
||||
expect(screen.queryByRole("button", { name: "Video devices" })).toBe(null);
|
||||
});
|
||||
|
||||
it("show with dropdown when multiple devices are available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([
|
||||
{
|
||||
deviceId: "1",
|
||||
groupId: "1",
|
||||
label: "Headphones",
|
||||
kind: "audioinput",
|
||||
toJSON: () => {},
|
||||
},
|
||||
{
|
||||
deviceId: "2",
|
||||
groupId: "1",
|
||||
label: "", // Should fall back to "Audio input 2"
|
||||
kind: "audioinput",
|
||||
toJSON: () => {},
|
||||
},
|
||||
]);
|
||||
|
||||
await renderLobby();
|
||||
screen.getByRole("button", { name: /microphone/ });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Audio devices" }));
|
||||
screen.getByRole("menuitem", { name: "Headphones" });
|
||||
screen.getByRole("menuitem", { name: "Audio input 2" });
|
||||
});
|
||||
});
|
||||
|
||||
describe("join button", () => {
|
||||
it("works", async () => {
|
||||
await renderLobby();
|
||||
const connectSpy = jest.spyOn(call, "connect");
|
||||
fireEvent.click(screen.getByRole("button", { name: "Join" }));
|
||||
await waitFor(() => expect(connectSpy).toHaveBeenCalled(), { interval: 1 });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -1,193 +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";
|
||||
// eslint-disable-next-line deprecate/import
|
||||
import { mount } from "enzyme";
|
||||
import { act } from "react-dom/test-utils";
|
||||
import { mocked } from "jest-mock";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
|
||||
import {
|
||||
stubClient,
|
||||
stubVideoChannelStore,
|
||||
StubVideoChannelStore,
|
||||
mkRoom,
|
||||
mkVideoChannelMember,
|
||||
mockStateEventImplementation,
|
||||
} from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import FacePile from "../../../../src/components/views/elements/FacePile";
|
||||
import MemberAvatar from "../../../../src/components/views/avatars/MemberAvatar";
|
||||
import VideoLobby from "../../../../src/components/views/voip/VideoLobby";
|
||||
|
||||
describe("VideoLobby", () => {
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: {
|
||||
enumerateDevices: jest.fn(),
|
||||
getUserMedia: () => null,
|
||||
},
|
||||
});
|
||||
jest.spyOn(HTMLMediaElement.prototype, "play").mockImplementation(async () => {});
|
||||
|
||||
let cli: MatrixClient;
|
||||
let store: StubVideoChannelStore;
|
||||
let room: Room;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = MatrixClientPeg.get();
|
||||
store = stubVideoChannelStore();
|
||||
room = mkRoom(cli, "!1:example.org");
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([]);
|
||||
});
|
||||
|
||||
describe("connected members", () => {
|
||||
it("hides when no one is connected", async () => {
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
expect(lobby.find(".mx_VideoLobby_connectedMembers").exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it("is shown when someone is connected", async () => {
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
// A user connected from 2 devices
|
||||
mkVideoChannelMember("@alice:example.org", ["device 1", "device 2"]),
|
||||
// A disconnected user
|
||||
mkVideoChannelMember("@bob:example.org", []),
|
||||
// A user that claims to have a connected device, but has left the room
|
||||
mkVideoChannelMember("@chris:example.org", ["device 1"]),
|
||||
]));
|
||||
|
||||
mocked(room).getMember.mockImplementation(userId => ({
|
||||
userId,
|
||||
membership: userId === "@chris:example.org" ? "leave" : "join",
|
||||
name: userId,
|
||||
rawDisplayName: userId,
|
||||
roomId: "!1:example.org",
|
||||
getAvatarUrl: () => {},
|
||||
getMxcAvatarUrl: () => {},
|
||||
}) as unknown as RoomMember);
|
||||
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
// Only Alice should display as connected
|
||||
const memberText = lobby.find(".mx_VideoLobby_connectedMembers").children().at(0).text();
|
||||
expect(memberText).toEqual("1 person joined");
|
||||
expect(lobby.find(FacePile).find(MemberAvatar).props().member.userId).toEqual("@alice:example.org");
|
||||
});
|
||||
|
||||
it("doesn't include remote echo of this device being connected", async () => {
|
||||
mocked(room.currentState).getStateEvents.mockImplementation(mockStateEventImplementation([
|
||||
// Make the remote echo claim that we're connected, while leaving the store disconnected
|
||||
mkVideoChannelMember(cli.getUserId(), [cli.getDeviceId()]),
|
||||
]));
|
||||
|
||||
mocked(room).getMember.mockImplementation(userId => ({
|
||||
userId,
|
||||
membership: "join",
|
||||
name: userId,
|
||||
rawDisplayName: userId,
|
||||
roomId: "!1:example.org",
|
||||
getAvatarUrl: () => {},
|
||||
getMxcAvatarUrl: () => {},
|
||||
}) as unknown as RoomMember);
|
||||
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
// Because of our local echo, we should still appear as disconnected
|
||||
expect(lobby.find(".mx_VideoLobby_connectedMembers").exists()).toEqual(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("device buttons", () => {
|
||||
it("hides when no devices are available", async () => {
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
expect(lobby.find("DeviceButton").children().exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it("hides device list when only one device is available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([{
|
||||
deviceId: "1",
|
||||
groupId: "1",
|
||||
label: "Webcam",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
}]);
|
||||
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
expect(lobby.find(".mx_VideoLobby_deviceListButton").exists()).toEqual(false);
|
||||
});
|
||||
|
||||
it("shows device list when multiple devices are available", async () => {
|
||||
mocked(navigator.mediaDevices.enumerateDevices).mockResolvedValue([
|
||||
{
|
||||
deviceId: "1",
|
||||
groupId: "1",
|
||||
label: "Front camera",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
},
|
||||
{
|
||||
deviceId: "2",
|
||||
groupId: "1",
|
||||
label: "Back camera",
|
||||
kind: "videoinput",
|
||||
toJSON: () => {},
|
||||
},
|
||||
]);
|
||||
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
expect(lobby.find(".mx_VideoLobby_deviceListButton").exists()).toEqual(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("join button", () => {
|
||||
it("works", async () => {
|
||||
const lobby = mount(<VideoLobby room={room} />);
|
||||
// Wait for state to settle
|
||||
await act(() => Promise.resolve());
|
||||
lobby.update();
|
||||
|
||||
act(() => {
|
||||
lobby.find("AccessibleButton.mx_VideoLobby_joinButton").simulate("click");
|
||||
});
|
||||
expect(store.connect).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
});
|
|
@ -23,7 +23,7 @@ import { stubClient, setupAsyncStoreWithClient, mockPlatformPeg } from "./test-u
|
|||
import { MatrixClientPeg } from "../src/MatrixClientPeg";
|
||||
import WidgetStore from "../src/stores/WidgetStore";
|
||||
import WidgetUtils from "../src/utils/WidgetUtils";
|
||||
import { VIDEO_CHANNEL_MEMBER } from "../src/utils/VideoChannelUtils";
|
||||
import { JitsiCall } from "../src/models/Call";
|
||||
import createRoom, { canEncryptToAllUsers } from '../src/createRoom';
|
||||
|
||||
describe("createRoom", () => {
|
||||
|
@ -51,7 +51,7 @@ describe("createRoom", () => {
|
|||
},
|
||||
events: {
|
||||
"im.vector.modular.widgets": widgetPower,
|
||||
[VIDEO_CHANNEL_MEMBER]: videoMemberPower,
|
||||
[JitsiCall.MEMBER_EVENT_TYPE]: jitsiMemberPower,
|
||||
},
|
||||
},
|
||||
}]] = mocked(client.createRoom).mock.calls as any; // no good type
|
||||
|
@ -64,7 +64,7 @@ describe("createRoom", () => {
|
|||
expect(widgetStateKey).toEqual("im.vector.modular.widgets");
|
||||
|
||||
// All members should be able to update their connected devices
|
||||
expect(videoMemberPower).toEqual(0);
|
||||
expect(jitsiMemberPower).toEqual(0);
|
||||
// Jitsi widget should be immutable for admins
|
||||
expect(widgetPower).toBeGreaterThan(100);
|
||||
// and we should have been reset back to admin
|
||||
|
|
339
test/models/Call-test.ts
Normal file
339
test/models/Call-test.ts
Normal file
|
@ -0,0 +1,339 @@
|
|||
/*
|
||||
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 EventEmitter from "events";
|
||||
import { isEqual } from "lodash";
|
||||
import { mocked } from "jest-mock";
|
||||
import { waitFor } from "@testing-library/react";
|
||||
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { Mocked } from "jest-mock";
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import type { Call } from "../../src/models/Call";
|
||||
import { stubClient, mkEvent, mkRoomMember, setupAsyncStoreWithClient, mockPlatformPeg } from "../test-utils";
|
||||
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../src/MediaDeviceHandler";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import { CallEvent, ConnectionState, JitsiCall } from "../../src/models/Call";
|
||||
import WidgetStore from "../../src/stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingStore";
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../src/stores/ActiveWidgetStore";
|
||||
import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions";
|
||||
|
||||
describe("JitsiCall", () => {
|
||||
mockPlatformPeg({ supportsJitsiScreensharing: () => true });
|
||||
jest.spyOn(MediaDeviceHandler, "getDevices").mockResolvedValue({
|
||||
[MediaDeviceKindEnum.AudioInput]: [
|
||||
{ deviceId: "1", groupId: "1", kind: "audioinput", label: "Headphones", toJSON: () => {} },
|
||||
],
|
||||
[MediaDeviceKindEnum.VideoInput]: [
|
||||
{ deviceId: "2", groupId: "2", kind: "videoinput", label: "Built-in webcam", toJSON: () => {} },
|
||||
],
|
||||
[MediaDeviceKindEnum.AudioOutput]: [],
|
||||
});
|
||||
jest.spyOn(MediaDeviceHandler, "getAudioInput").mockReturnValue("1");
|
||||
jest.spyOn(MediaDeviceHandler, "getVideoInput").mockReturnValue("2");
|
||||
|
||||
let client: Mocked<MatrixClient>;
|
||||
let room: Room;
|
||||
let alice: RoomMember;
|
||||
let bob: RoomMember;
|
||||
let carol: RoomMember;
|
||||
let call: Call;
|
||||
let widget: Widget;
|
||||
let messaging: Mocked<ClientWidgetApi>;
|
||||
let audioMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
let videoMutedSpy: jest.SpyInstance<boolean, []>;
|
||||
|
||||
beforeEach(async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
|
||||
stubClient();
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
|
||||
room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
alice = mkRoomMember(room.roomId, "@alice:example.org");
|
||||
bob = mkRoomMember(room.roomId, "@bob:example.org");
|
||||
carol = mkRoomMember(room.roomId, "@carol:example.org");
|
||||
jest.spyOn(room, "getMember").mockImplementation(userId => {
|
||||
switch (userId) {
|
||||
case alice.userId: return alice;
|
||||
case bob.userId: return bob;
|
||||
case carol.userId: return carol;
|
||||
default: return null;
|
||||
}
|
||||
});
|
||||
jest.spyOn(room, "getMyMembership").mockReturnValue("join");
|
||||
|
||||
client.getRoom.mockImplementation(roomId => roomId === room.roomId ? room : null);
|
||||
client.getRooms.mockReturnValue([room]);
|
||||
client.getUserId.mockReturnValue(alice.userId);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
client.sendStateEvent.mockImplementation(async (roomId, eventType, content, stateKey = "") => {
|
||||
if (roomId !== room.roomId) throw new Error("Unknown room");
|
||||
const event = mkEvent({
|
||||
event: true,
|
||||
type: eventType,
|
||||
room: roomId,
|
||||
user: alice.userId,
|
||||
skey: stateKey,
|
||||
content,
|
||||
});
|
||||
room.addLiveEvents([event]);
|
||||
return { event_id: event.getId() };
|
||||
});
|
||||
|
||||
setupAsyncStoreWithClient(WidgetStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
await JitsiCall.create(room);
|
||||
call = JitsiCall.get(room);
|
||||
if (call === null) throw new Error("Failed to create call");
|
||||
|
||||
widget = new Widget(call.widget);
|
||||
|
||||
const eventEmitter = new EventEmitter();
|
||||
messaging = {
|
||||
on: eventEmitter.on.bind(eventEmitter),
|
||||
off: eventEmitter.off.bind(eventEmitter),
|
||||
once: eventEmitter.once.bind(eventEmitter),
|
||||
emit: eventEmitter.emit.bind(eventEmitter),
|
||||
stop: jest.fn(),
|
||||
transport: {
|
||||
send: jest.fn(async action => {
|
||||
if (action === ElementWidgetActions.JoinCall) {
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
} else if (action === ElementWidgetActions.HangupCall) {
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
}
|
||||
return {};
|
||||
}),
|
||||
reply: jest.fn(),
|
||||
},
|
||||
} as unknown as Mocked<ClientWidgetApi>;
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||
|
||||
audioMutedSpy = jest.spyOn(MediaDeviceHandler, "startWithAudioMuted", "get");
|
||||
videoMutedSpy = jest.spyOn(MediaDeviceHandler, "startWithVideoMuted", "get");
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
call.destroy();
|
||||
client.reEmitter.stopReEmitting(room, [RoomStateEvent.Events]);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
jest.clearAllMocks();
|
||||
audioMutedSpy.mockRestore();
|
||||
videoMutedSpy.mockRestore();
|
||||
});
|
||||
|
||||
it("connects muted", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
audioMutedSpy.mockReturnValue(true);
|
||||
videoMutedSpy.mockReturnValue(true);
|
||||
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
|
||||
audioInput: null,
|
||||
videoInput: null,
|
||||
});
|
||||
});
|
||||
|
||||
it("connects unmuted", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
audioMutedSpy.mockReturnValue(false);
|
||||
videoMutedSpy.mockReturnValue(false);
|
||||
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.JoinCall, {
|
||||
audioInput: "Headphones",
|
||||
videoInput: "Built-in webcam",
|
||||
});
|
||||
});
|
||||
|
||||
it("waits for messaging when connecting", async () => {
|
||||
// Temporarily remove the messaging to simulate connecting while the
|
||||
// widget is still initializing
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, room.roomId);
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
const connect = call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connecting);
|
||||
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, room.roomId, messaging);
|
||||
await connect;
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
});
|
||||
|
||||
it("handles remote disconnection", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
|
||||
});
|
||||
|
||||
it("handles instant remote disconnection when connecting", async () => {
|
||||
mocked(messaging.transport).send.mockImplementation(async action => {
|
||||
if (action === ElementWidgetActions.JoinCall) {
|
||||
// Emit the hangup event *before* the join event to fully
|
||||
// exercise the race condition
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.HangupCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
messaging.emit(
|
||||
`action:${ElementWidgetActions.JoinCall}`,
|
||||
new CustomEvent("widgetapirequest", { detail: {} }),
|
||||
);
|
||||
}
|
||||
return {};
|
||||
});
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
// Should disconnect on its own almost instantly
|
||||
await waitFor(() => expect(call.connectionState).toBe(ConnectionState.Disconnected), { interval: 5 });
|
||||
});
|
||||
|
||||
it("disconnects", async () => {
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
await call.connect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Connected);
|
||||
await call.disconnect();
|
||||
expect(call.connectionState).toBe(ConnectionState.Disconnected);
|
||||
});
|
||||
|
||||
it("tracks participants in room state", async () => {
|
||||
expect([...call.participants]).toEqual([]);
|
||||
|
||||
// A participant with multiple devices (should only show up once)
|
||||
await client.sendStateEvent(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
{ devices: ["bobweb", "bobdesktop"], expires_ts: 1000 * 60 * 10 },
|
||||
bob.userId,
|
||||
);
|
||||
// A participant with an expired device (should not show up)
|
||||
await client.sendStateEvent(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
{ devices: ["carolandroid"], expires_ts: -1000 * 60 },
|
||||
carol.userId,
|
||||
);
|
||||
|
||||
// Now, stub out client.sendStateEvent so we can test our local echo
|
||||
client.sendStateEvent.mockReset();
|
||||
await call.connect();
|
||||
expect([...call.participants]).toEqual([bob, alice]);
|
||||
|
||||
await call.disconnect();
|
||||
expect([...call.participants]).toEqual([bob]);
|
||||
});
|
||||
|
||||
it("updates room state when connecting and disconnecting", async () => {
|
||||
const now1 = Date.now();
|
||||
await call.connect();
|
||||
await waitFor(() => expect(
|
||||
room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE, alice.userId).getContent(),
|
||||
).toEqual({
|
||||
devices: [client.getDeviceId()],
|
||||
expires_ts: now1 + JitsiCall.STUCK_DEVICE_TIMEOUT_MS,
|
||||
}), { interval: 5 });
|
||||
|
||||
const now2 = Date.now();
|
||||
await call.disconnect();
|
||||
await waitFor(() => expect(
|
||||
room.currentState.getStateEvents(JitsiCall.MEMBER_EVENT_TYPE, alice.userId).getContent(),
|
||||
).toEqual({
|
||||
devices: [],
|
||||
expires_ts: now2 + JitsiCall.STUCK_DEVICE_TIMEOUT_MS,
|
||||
}), { interval: 5 });
|
||||
});
|
||||
|
||||
it("repeatedly updates room state while connected", async () => {
|
||||
await call.connect();
|
||||
await waitFor(() => expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
{ devices: [client.getDeviceId()], expires_ts: expect.any(Number) },
|
||||
alice.userId,
|
||||
), { interval: 5 });
|
||||
|
||||
client.sendStateEvent.mockClear();
|
||||
jest.advanceTimersByTime(JitsiCall.STUCK_DEVICE_TIMEOUT_MS);
|
||||
await waitFor(() => expect(client.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
room.roomId,
|
||||
JitsiCall.MEMBER_EVENT_TYPE,
|
||||
{ devices: [client.getDeviceId()], expires_ts: expect.any(Number) },
|
||||
alice.userId,
|
||||
), { interval: 5 });
|
||||
});
|
||||
|
||||
it("emits events when connection state changes", async () => {
|
||||
const events: ConnectionState[] = [];
|
||||
const onConnectionState = (state: ConnectionState) => events.push(state);
|
||||
call.on(CallEvent.ConnectionState, onConnectionState);
|
||||
|
||||
await call.connect();
|
||||
await call.disconnect();
|
||||
expect(events).toEqual([
|
||||
ConnectionState.Connecting,
|
||||
ConnectionState.Connected,
|
||||
ConnectionState.Disconnecting,
|
||||
ConnectionState.Disconnected,
|
||||
]);
|
||||
});
|
||||
|
||||
it("emits events when participants change", async () => {
|
||||
const events: Set<RoomMember>[] = [];
|
||||
const onParticipants = (participants: Set<RoomMember>) => {
|
||||
if (!isEqual(participants, events[events.length - 1])) events.push(participants);
|
||||
};
|
||||
call.on(CallEvent.Participants, onParticipants);
|
||||
|
||||
await call.connect();
|
||||
await call.disconnect();
|
||||
expect(events).toEqual([new Set([alice]), new Set()]);
|
||||
});
|
||||
|
||||
it("switches to spotlight layout when the widget becomes a PiP", async () => {
|
||||
await call.connect();
|
||||
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Undock);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.SpotlightLayout, {});
|
||||
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Dock);
|
||||
expect(messaging.transport.send).toHaveBeenCalledWith(ElementWidgetActions.TileLayout, {});
|
||||
});
|
||||
});
|
|
@ -1,225 +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 { mocked, Mocked } from "jest-mock";
|
||||
import {
|
||||
Widget,
|
||||
ClientWidgetApi,
|
||||
MatrixWidgetType,
|
||||
WidgetApiAction,
|
||||
IWidgetApiRequest,
|
||||
IWidgetApiRequestData,
|
||||
} from "matrix-widget-api";
|
||||
import { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
|
||||
import { stubClient, setupAsyncStoreWithClient, mkRoom } from "../test-utils";
|
||||
import { MatrixClientPeg } from "../../src/MatrixClientPeg";
|
||||
import WidgetStore, { IApp } from "../../src/stores/WidgetStore";
|
||||
import { WidgetMessagingStore } from "../../src/stores/widgets/WidgetMessagingStore";
|
||||
import ActiveWidgetStore, { ActiveWidgetStoreEvent } from "../../src/stores/ActiveWidgetStore";
|
||||
import { ElementWidgetActions } from "../../src/stores/widgets/ElementWidgetActions";
|
||||
import { VIDEO_CHANNEL_MEMBER, STUCK_DEVICE_TIMEOUT_MS } from "../../src/utils/VideoChannelUtils";
|
||||
import VideoChannelStore, { VideoChannelEvent } from "../../src/stores/VideoChannelStore";
|
||||
|
||||
describe("VideoChannelStore", () => {
|
||||
const store = VideoChannelStore.instance;
|
||||
|
||||
const widget = { id: "1" } as unknown as Widget;
|
||||
const app = {
|
||||
id: "1",
|
||||
eventId: "$1:example.org",
|
||||
roomId: "!1:example.org",
|
||||
type: MatrixWidgetType.JitsiMeet,
|
||||
url: "",
|
||||
name: "Video channel",
|
||||
creatorUserId: "@alice:example.org",
|
||||
avatar_url: null,
|
||||
data: { isVideoChannel: true },
|
||||
} as IApp;
|
||||
|
||||
// Set up mocks to simulate the remote end of the widget API
|
||||
let sendMock: (action: WidgetApiAction, data: IWidgetApiRequestData) => void;
|
||||
let onMock: (action: string, listener: (ev: CustomEvent<IWidgetApiRequest>) => void) => void;
|
||||
let onceMock: (action: string, listener: (ev: CustomEvent<IWidgetApiRequest>) => void) => void;
|
||||
let messaging: ClientWidgetApi;
|
||||
let cli: Mocked<MatrixClient>;
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
cli = mocked(MatrixClientPeg.get());
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, cli);
|
||||
setupAsyncStoreWithClient(store, cli);
|
||||
cli.getRoom.mockReturnValue(mkRoom(cli, "!1:example.org"));
|
||||
|
||||
sendMock = jest.fn();
|
||||
onMock = jest.fn();
|
||||
onceMock = jest.fn();
|
||||
|
||||
jest.spyOn(WidgetStore.instance, "getApps").mockReturnValue([app]);
|
||||
messaging = {
|
||||
on: onMock,
|
||||
off: () => {},
|
||||
stop: () => {},
|
||||
once: onceMock,
|
||||
transport: {
|
||||
send: sendMock,
|
||||
reply: () => {},
|
||||
},
|
||||
} as unknown as ClientWidgetApi;
|
||||
});
|
||||
|
||||
afterEach(() => jest.useRealTimers());
|
||||
|
||||
const getRequest = <T extends IWidgetApiRequestData>(): Promise<[WidgetApiAction, T]> =>
|
||||
new Promise<[WidgetApiAction, T]>(resolve => {
|
||||
mocked(sendMock).mockImplementationOnce((action, data) => resolve([action, data as T]));
|
||||
});
|
||||
|
||||
const widgetReady = () => {
|
||||
// Tell the WidgetStore that the widget is ready
|
||||
const [, ready] = mocked(onceMock).mock.calls.find(([action]) =>
|
||||
action === `action:${ElementWidgetActions.WidgetReady}`,
|
||||
);
|
||||
ready({ detail: {} } as unknown as CustomEvent<IWidgetApiRequest>);
|
||||
};
|
||||
|
||||
const confirmConnect = async () => {
|
||||
// Wait for the store to contact the widget API
|
||||
await getRequest();
|
||||
// Then, locate the callback that will confirm the join
|
||||
const [, join] = mocked(onMock).mock.calls.find(([action]) =>
|
||||
action === `action:${ElementWidgetActions.JoinCall}`,
|
||||
);
|
||||
// Confirm the join, and wait for the store to update
|
||||
const waitForConnect = new Promise<void>(resolve =>
|
||||
store.once(VideoChannelEvent.Connect, resolve),
|
||||
);
|
||||
join(new CustomEvent("widgetapirequest", { detail: {} }) as CustomEvent<IWidgetApiRequest>);
|
||||
await waitForConnect;
|
||||
};
|
||||
|
||||
const confirmDisconnect = async () => {
|
||||
// Locate the callback that will perform the hangup
|
||||
const [, hangup] = mocked(onceMock).mock.calls.find(([action]) =>
|
||||
action === `action:${ElementWidgetActions.HangupCall}`,
|
||||
);
|
||||
// Hangup and wait for the store, once again
|
||||
const waitForHangup = new Promise<void>(resolve =>
|
||||
store.once(VideoChannelEvent.Disconnect, resolve),
|
||||
);
|
||||
hangup(new CustomEvent("widgetapirequest", { detail: {} }) as CustomEvent<IWidgetApiRequest>);
|
||||
await waitForHangup;
|
||||
};
|
||||
|
||||
it("connects and disconnects", async () => {
|
||||
jest.useFakeTimers();
|
||||
jest.setSystemTime(0);
|
||||
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging);
|
||||
widgetReady();
|
||||
expect(store.roomId).toBeFalsy();
|
||||
expect(store.connected).toEqual(false);
|
||||
|
||||
const connectConfirmed = confirmConnect();
|
||||
const connectPromise = store.connect("!1:example.org", null, null);
|
||||
await connectConfirmed;
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
expect(store.roomId).toEqual("!1:example.org");
|
||||
expect(store.connected).toEqual(true);
|
||||
|
||||
// Our device should now appear as connected
|
||||
expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
"!1:example.org",
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
{ devices: [cli.getDeviceId()], expires_ts: expect.any(Number) },
|
||||
cli.getUserId(),
|
||||
);
|
||||
cli.sendStateEvent.mockClear();
|
||||
|
||||
// Our devices should be resent within the timeout period to prevent
|
||||
// the data from becoming stale
|
||||
jest.advanceTimersByTime(STUCK_DEVICE_TIMEOUT_MS);
|
||||
expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
"!1:example.org",
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
{ devices: [cli.getDeviceId()], expires_ts: expect.any(Number) },
|
||||
cli.getUserId(),
|
||||
);
|
||||
cli.sendStateEvent.mockClear();
|
||||
|
||||
const disconnectPromise = store.disconnect();
|
||||
await confirmDisconnect();
|
||||
await expect(disconnectPromise).resolves.toBeUndefined();
|
||||
expect(store.roomId).toBeFalsy();
|
||||
expect(store.connected).toEqual(false);
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org");
|
||||
|
||||
// Our device should now be marked as disconnected
|
||||
expect(cli.sendStateEvent).toHaveBeenLastCalledWith(
|
||||
"!1:example.org",
|
||||
VIDEO_CHANNEL_MEMBER,
|
||||
{ devices: [], expires_ts: expect.any(Number) },
|
||||
cli.getUserId(),
|
||||
);
|
||||
});
|
||||
|
||||
it("waits for messaging when connecting", async () => {
|
||||
const connectConfirmed = confirmConnect();
|
||||
const connectPromise = store.connect("!1:example.org", null, null);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging);
|
||||
widgetReady();
|
||||
await connectConfirmed;
|
||||
await expect(connectPromise).resolves.toBeUndefined();
|
||||
expect(store.roomId).toEqual("!1:example.org");
|
||||
expect(store.connected).toEqual(true);
|
||||
|
||||
store.disconnect();
|
||||
await confirmDisconnect();
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org");
|
||||
});
|
||||
|
||||
it("rejects if the widget's messaging gets stopped mid-connect", async () => {
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging);
|
||||
widgetReady();
|
||||
expect(store.roomId).toBeFalsy();
|
||||
expect(store.connected).toEqual(false);
|
||||
|
||||
const requestPromise = getRequest();
|
||||
const connectPromise = store.connect("!1:example.org", null, null);
|
||||
// Wait for the store to contact the widget API, then stop the messaging
|
||||
await requestPromise;
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org");
|
||||
await expect(connectPromise).rejects.toBeDefined();
|
||||
expect(store.roomId).toBeFalsy();
|
||||
expect(store.connected).toEqual(false);
|
||||
});
|
||||
|
||||
it("switches to spotlight mode when the widget becomes a PiP", async () => {
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, "!1:example.org", messaging);
|
||||
widgetReady();
|
||||
confirmConnect();
|
||||
await store.connect("!1:example.org", null, null);
|
||||
|
||||
const request = getRequest<IWidgetApiRequestData>();
|
||||
ActiveWidgetStore.instance.emit(ActiveWidgetStoreEvent.Undock);
|
||||
const [action, data] = await request;
|
||||
expect(action).toEqual(ElementWidgetActions.SpotlightLayout);
|
||||
expect(data).toEqual({});
|
||||
|
||||
store.disconnect();
|
||||
await confirmDisconnect();
|
||||
WidgetMessagingStore.instance.stopMessaging(widget, "!1:example.org");
|
||||
});
|
||||
});
|
|
@ -38,21 +38,27 @@ describe('VoiceRecordingStore', () => {
|
|||
[room3Id]: undefined,
|
||||
};
|
||||
|
||||
const mkStore = (): VoiceRecordingStore => {
|
||||
const store = new VoiceRecordingStore();
|
||||
store.start();
|
||||
return store;
|
||||
};
|
||||
|
||||
describe('startRecording()', () => {
|
||||
it('throws when roomId is falsy', () => {
|
||||
const store = new VoiceRecordingStore();
|
||||
const store = mkStore();
|
||||
expect(() => store.startRecording(undefined)).toThrow("Recording must be associated with a room");
|
||||
});
|
||||
|
||||
it('throws when room already has a recording', () => {
|
||||
const store = new VoiceRecordingStore();
|
||||
const store = mkStore();
|
||||
// @ts-ignore
|
||||
store.storeState = state;
|
||||
expect(() => store.startRecording(room2Id)).toThrow("A recording is already in progress");
|
||||
});
|
||||
|
||||
it('creates and adds recording to state', async () => {
|
||||
const store = new VoiceRecordingStore();
|
||||
const store = mkStore();
|
||||
const result = store.startRecording(room2Id);
|
||||
|
||||
await flushPromises();
|
||||
|
@ -64,7 +70,7 @@ describe('VoiceRecordingStore', () => {
|
|||
|
||||
describe('disposeRecording()', () => {
|
||||
it('destroys recording for a room if it exists in state', async () => {
|
||||
const store = new VoiceRecordingStore();
|
||||
const store = mkStore();
|
||||
// @ts-ignore
|
||||
store.storeState = state;
|
||||
|
||||
|
@ -74,7 +80,7 @@ describe('VoiceRecordingStore', () => {
|
|||
});
|
||||
|
||||
it('removes room from state when it has a recording', async () => {
|
||||
const store = new VoiceRecordingStore();
|
||||
const store = mkStore();
|
||||
// @ts-ignore
|
||||
store.storeState = state;
|
||||
|
||||
|
@ -84,7 +90,7 @@ describe('VoiceRecordingStore', () => {
|
|||
});
|
||||
|
||||
it('removes room from state when it has a falsy recording', async () => {
|
||||
const store = new VoiceRecordingStore();
|
||||
const store = mkStore();
|
||||
// @ts-ignore
|
||||
store.storeState = state;
|
||||
|
||||
|
|
|
@ -14,51 +14,91 @@ See the License for the specific language governing permissions and
|
|||
limitations under the License.
|
||||
*/
|
||||
|
||||
import { stubClient, stubVideoChannelStore, mkRoom } from "../../../test-utils";
|
||||
import { mocked, MockedObject } from "jest-mock";
|
||||
import { PendingEventOrdering } from "matrix-js-sdk/src/client";
|
||||
import { Room } from "matrix-js-sdk/src/models/room";
|
||||
import { RoomStateEvent } from "matrix-js-sdk/src/models/room-state";
|
||||
import { Widget } from "matrix-widget-api";
|
||||
|
||||
import type { MatrixClient } from "matrix-js-sdk/src/client";
|
||||
import type { ClientWidgetApi } from "matrix-widget-api";
|
||||
import { stubClient, setupAsyncStoreWithClient, useMockedCalls, MockedCall } from "../../../test-utils";
|
||||
import { MatrixClientPeg } from "../../../../src/MatrixClientPeg";
|
||||
import DMRoomMap from "../../../../src/utils/DMRoomMap";
|
||||
import { DefaultTagID } from "../../../../src/stores/room-list/models";
|
||||
import { SortAlgorithm, ListAlgorithm } from "../../../../src/stores/room-list/algorithms/models";
|
||||
import "../../../../src/stores/room-list/RoomListStore"; // must be imported before Algorithm to avoid cycles
|
||||
import { Algorithm } from "../../../../src/stores/room-list/algorithms/Algorithm";
|
||||
import { CallStore } from "../../../../src/stores/CallStore";
|
||||
import { WidgetMessagingStore } from "../../../../src/stores/widgets/WidgetMessagingStore";
|
||||
|
||||
describe("Algorithm", () => {
|
||||
let videoChannelStore;
|
||||
let algorithm;
|
||||
let textRoom;
|
||||
let videoRoom;
|
||||
useMockedCalls();
|
||||
|
||||
let client: MockedObject<MatrixClient>;
|
||||
let algorithm: Algorithm;
|
||||
|
||||
beforeEach(() => {
|
||||
stubClient();
|
||||
const cli = MatrixClientPeg.get();
|
||||
client = mocked(MatrixClientPeg.get());
|
||||
DMRoomMap.makeShared();
|
||||
videoChannelStore = stubVideoChannelStore();
|
||||
|
||||
algorithm = new Algorithm();
|
||||
algorithm.start();
|
||||
|
||||
textRoom = mkRoom(cli, "!text:example.org");
|
||||
videoRoom = mkRoom(cli, "!video:example.org");
|
||||
videoRoom.isElementVideoRoom.mockReturnValue(true);
|
||||
algorithm.populateTags(
|
||||
{ [DefaultTagID.Untagged]: SortAlgorithm.Alphabetic },
|
||||
{ [DefaultTagID.Untagged]: ListAlgorithm.Natural },
|
||||
);
|
||||
algorithm.setKnownRooms([textRoom, videoRoom]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
algorithm.stop();
|
||||
});
|
||||
|
||||
it("sticks video rooms to the top when they connect", () => {
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]);
|
||||
videoChannelStore.connect("!video:example.org");
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]);
|
||||
});
|
||||
it("sticks rooms with calls to the top when they're connected", async () => {
|
||||
const room = new Room("!1:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
const roomWithCall = new Room("!2:example.org", client, "@alice:example.org", {
|
||||
pendingEventOrdering: PendingEventOrdering.Detached,
|
||||
});
|
||||
|
||||
it("unsticks video rooms from the top when they disconnect", () => {
|
||||
videoChannelStore.connect("!video:example.org");
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([videoRoom, textRoom]);
|
||||
videoChannelStore.disconnect();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([textRoom, videoRoom]);
|
||||
client.getRoom.mockImplementation(roomId => {
|
||||
switch (roomId) {
|
||||
case room.roomId: return room;
|
||||
case roomWithCall.roomId: return roomWithCall;
|
||||
default: return null;
|
||||
}
|
||||
});
|
||||
client.getRooms.mockReturnValue([room, roomWithCall]);
|
||||
client.reEmitter.reEmit(room, [RoomStateEvent.Events]);
|
||||
client.reEmitter.reEmit(roomWithCall, [RoomStateEvent.Events]);
|
||||
|
||||
for (const room of client.getRooms()) jest.spyOn(room, "getMyMembership").mockReturnValue("join");
|
||||
algorithm.setKnownRooms(client.getRooms());
|
||||
|
||||
setupAsyncStoreWithClient(CallStore.instance, client);
|
||||
setupAsyncStoreWithClient(WidgetMessagingStore.instance, client);
|
||||
|
||||
MockedCall.create(roomWithCall, "1");
|
||||
const call = CallStore.instance.get(roomWithCall.roomId);
|
||||
if (call === null) throw new Error("Failed to create call");
|
||||
|
||||
const widget = new Widget(call.widget);
|
||||
WidgetMessagingStore.instance.storeMessaging(widget, roomWithCall.roomId, {
|
||||
stop: () => {},
|
||||
} as unknown as ClientWidgetApi);
|
||||
|
||||
Object.defineProperty(navigator, "mediaDevices", {
|
||||
value: { enumerateDevices: async () => [] },
|
||||
});
|
||||
|
||||
// End of setup
|
||||
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
|
||||
await call.connect();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([roomWithCall, room]);
|
||||
await call.disconnect();
|
||||
expect(algorithm.getOrderedRooms()[DefaultTagID.Untagged]).toEqual([room, roomWithCall]);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -18,7 +18,7 @@ import { mocked } from "jest-mock";
|
|||
import { Room } from "matrix-js-sdk/src/matrix";
|
||||
|
||||
import { VisibilityProvider } from "../../../../src/stores/room-list/filters/VisibilityProvider";
|
||||
import CallHandler from "../../../../src/CallHandler";
|
||||
import LegacyCallHandler from "../../../../src/LegacyCallHandler";
|
||||
import VoipUserMapper from "../../../../src/VoipUserMapper";
|
||||
import { LocalRoom, LOCAL_ROOM_ID_PREFIX } from "../../../../src/models/LocalRoom";
|
||||
import { RoomListCustomisations } from "../../../../src/customisations/RoomList";
|
||||
|
@ -28,7 +28,7 @@ jest.mock("../../../../src/VoipUserMapper", () => ({
|
|||
sharedInstance: jest.fn(),
|
||||
}));
|
||||
|
||||
jest.mock("../../../../src/CallHandler", () => ({
|
||||
jest.mock("../../../../src/LegacyCallHandler", () => ({
|
||||
instance: {
|
||||
getSupportsVirtualRooms: jest.fn(),
|
||||
},
|
||||
|
@ -82,7 +82,7 @@ describe("VisibilityProvider", () => {
|
|||
describe("isRoomVisible", () => {
|
||||
describe("for a virtual room", () => {
|
||||
beforeEach(() => {
|
||||
mocked(CallHandler.instance.getSupportsVirtualRooms).mockReturnValue(true);
|
||||
mocked(LegacyCallHandler.instance.getSupportsVirtualRooms).mockReturnValue(true);
|
||||
mocked(mockVoipUserMapper.isVirtualRoom).mockReturnValue(true);
|
||||
});
|
||||
|
||||
|
|
92
test/test-utils/call.ts
Normal file
92
test/test-utils/call.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
/*
|
||||
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 { MatrixWidgetType } from "matrix-widget-api";
|
||||
|
||||
import type { Room } from "matrix-js-sdk/src/models/room";
|
||||
import type { RoomMember } from "matrix-js-sdk/src/models/room-member";
|
||||
import { mkEvent } from "./test-utils";
|
||||
import { Call } from "../../src/models/Call";
|
||||
|
||||
export class MockedCall extends Call {
|
||||
private static EVENT_TYPE = "org.example.mocked_call";
|
||||
|
||||
private constructor(private readonly room: Room, private readonly id: string) {
|
||||
super({
|
||||
id,
|
||||
eventId: "$1:example.org",
|
||||
roomId: room.roomId,
|
||||
type: MatrixWidgetType.Custom,
|
||||
url: "https://example.org",
|
||||
name: "Group call",
|
||||
creatorUserId: "@alice:example.org",
|
||||
});
|
||||
}
|
||||
|
||||
public static get(room: Room): MockedCall | null {
|
||||
const [event] = room.currentState.getStateEvents(this.EVENT_TYPE);
|
||||
return event?.getContent().terminated ?? true ? null : new MockedCall(room, event.getStateKey()!);
|
||||
}
|
||||
|
||||
public static create(room: Room, id: string) {
|
||||
// Update room state to let CallStore know that a call might now exist
|
||||
room.addLiveEvents([mkEvent({
|
||||
event: true,
|
||||
type: this.EVENT_TYPE,
|
||||
room: room.roomId,
|
||||
user: "@alice:example.org",
|
||||
content: { terminated: false },
|
||||
skey: id,
|
||||
})]);
|
||||
}
|
||||
|
||||
public get participants(): Set<RoomMember> {
|
||||
return super.participants;
|
||||
}
|
||||
public set participants(value: Set<RoomMember>) {
|
||||
super.participants = value;
|
||||
}
|
||||
|
||||
// No action needed for any of the following methods since this is just a mock
|
||||
public async clean(): Promise<void> {}
|
||||
// Public to allow spying
|
||||
public async performConnection(
|
||||
audioInput: MediaDeviceInfo | null,
|
||||
videoInput: MediaDeviceInfo | null,
|
||||
): Promise<void> {}
|
||||
public async performDisconnection(): Promise<void> {}
|
||||
|
||||
public destroy() {
|
||||
// Terminate the call for good measure
|
||||
this.room.addLiveEvents([mkEvent({
|
||||
event: true,
|
||||
type: MockedCall.EVENT_TYPE,
|
||||
room: this.room.roomId,
|
||||
user: "@alice:example.org",
|
||||
content: { terminated: true },
|
||||
skey: this.id,
|
||||
})]);
|
||||
|
||||
super.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets up the call store to use mocked calls.
|
||||
*/
|
||||
export const useMockedCalls = () => {
|
||||
Call.get = room => MockedCall.get(room);
|
||||
};
|
|
@ -21,6 +21,6 @@ export * from './platform';
|
|||
export * from './poll';
|
||||
export * from './room';
|
||||
export * from './test-utils';
|
||||
export * from './video';
|
||||
export * from './call';
|
||||
export * from './wrappers';
|
||||
export * from './utilities';
|
||||
|
|
|
@ -32,6 +32,7 @@ import {
|
|||
IUnsigned,
|
||||
} from 'matrix-js-sdk/src/matrix';
|
||||
import { normalize } from "matrix-js-sdk/src/utils";
|
||||
import { ReEmitter } from "matrix-js-sdk/src/ReEmitter";
|
||||
|
||||
import { MatrixClientPeg as peg } from '../../src/MatrixClientPeg';
|
||||
import dis from '../../src/dispatcher/dispatcher';
|
||||
|
@ -175,6 +176,8 @@ export function createTestClient(): MatrixClient {
|
|||
encryptAndSendToDevices: jest.fn().mockResolvedValue(undefined),
|
||||
} as unknown as MatrixClient;
|
||||
|
||||
client.reEmitter = new ReEmitter(client);
|
||||
|
||||
Object.defineProperty(client, "pollingTurnServers", {
|
||||
configurable: true,
|
||||
get: () => true,
|
||||
|
@ -325,6 +328,18 @@ export function mkMembership(opts: MakeEventPassThruProps & {
|
|||
return e;
|
||||
}
|
||||
|
||||
export function mkRoomMember(roomId: string, userId: string, membership = "join"): RoomMember {
|
||||
return {
|
||||
userId,
|
||||
membership,
|
||||
name: userId,
|
||||
rawDisplayName: userId,
|
||||
roomId,
|
||||
getAvatarUrl: () => {},
|
||||
getMxcAvatarUrl: () => {},
|
||||
} as unknown as RoomMember;
|
||||
}
|
||||
|
||||
export type MessageEventProps = MakeEventPassThruProps & {
|
||||
room: Room["roomId"];
|
||||
relatesTo?: IEventRelation;
|
||||
|
|
|
@ -1,65 +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 { EventEmitter } from "events";
|
||||
import { MatrixEvent } from "matrix-js-sdk/src/models/event";
|
||||
|
||||
import { mkEvent } from "./test-utils";
|
||||
import { VIDEO_CHANNEL_MEMBER, STUCK_DEVICE_TIMEOUT_MS } from "../../src/utils/VideoChannelUtils";
|
||||
import VideoChannelStore, { VideoChannelEvent, IJitsiParticipant } from "../../src/stores/VideoChannelStore";
|
||||
|
||||
export class StubVideoChannelStore extends EventEmitter {
|
||||
private _roomId: string | null;
|
||||
public get roomId(): string | null { return this._roomId; }
|
||||
public set roomId(value: string | null) { this._roomId = value; }
|
||||
private _connected: boolean;
|
||||
public get connected(): boolean { return this._connected; }
|
||||
public get participants(): IJitsiParticipant[] { return []; }
|
||||
|
||||
public startConnect = (roomId: string) => {
|
||||
this.roomId = roomId;
|
||||
this.emit(VideoChannelEvent.StartConnect, roomId);
|
||||
};
|
||||
public connect = jest.fn((roomId: string) => {
|
||||
this.roomId = roomId;
|
||||
this._connected = true;
|
||||
this.emit(VideoChannelEvent.Connect, roomId);
|
||||
});
|
||||
public disconnect = jest.fn(() => {
|
||||
const roomId = this._roomId;
|
||||
this.roomId = null;
|
||||
this._connected = false;
|
||||
this.emit(VideoChannelEvent.Disconnect, roomId);
|
||||
});
|
||||
}
|
||||
|
||||
export const stubVideoChannelStore = (): StubVideoChannelStore => {
|
||||
const store = new StubVideoChannelStore();
|
||||
jest.spyOn(VideoChannelStore, "instance", "get").mockReturnValue(store as unknown as VideoChannelStore);
|
||||
return store;
|
||||
};
|
||||
|
||||
export const mkVideoChannelMember = (userId: string, devices: string[], expiresAt?: number): MatrixEvent => mkEvent({
|
||||
event: true,
|
||||
type: VIDEO_CHANNEL_MEMBER,
|
||||
room: "!1:example.org",
|
||||
user: userId,
|
||||
skey: userId,
|
||||
content: {
|
||||
devices,
|
||||
expires_ts: expiresAt == null ? Date.now() + STUCK_DEVICE_TIMEOUT_MS : expiresAt,
|
||||
},
|
||||
});
|
Loading…
Add table
Add a link
Reference in a new issue