mirror of
https://github.com/vector-im/element-call.git
synced 2026-08-02 19:49:23 +00:00
Test widget mode too.
This commit is contained in:
@@ -26,36 +26,36 @@ import {
|
|||||||
MockRTCSession,
|
MockRTCSession,
|
||||||
} from "../utils/test";
|
} from "../utils/test";
|
||||||
import { GroupCallView } from "./GroupCallView";
|
import { GroupCallView } from "./GroupCallView";
|
||||||
|
import { leaveRTCSession } from "../rtcSessionHelpers";
|
||||||
|
import { WidgetHelpers } from "../widget";
|
||||||
|
import { LazyEventEmitter } from "../LazyEventEmitter";
|
||||||
|
|
||||||
vitest.mock("../soundUtils");
|
vitest.mock("../soundUtils");
|
||||||
vitest.mock("../useAudioContext");
|
vitest.mock("../useAudioContext");
|
||||||
vitest.mock("./InCallView");
|
vitest.mock("./InCallView");
|
||||||
|
|
||||||
|
vitest.mock("../rtcSessionHelpers", async (importOriginal) => {
|
||||||
|
const orig = await importOriginal<typeof import("../rtcSessionHelpers")>();
|
||||||
|
vitest.spyOn(orig, "leaveRTCSession");
|
||||||
|
return orig;
|
||||||
|
});
|
||||||
|
|
||||||
let playSound: MockedFunction<
|
let playSound: MockedFunction<
|
||||||
NonNullable<ReturnType<typeof useAudioContext>>["playSound"]
|
NonNullable<ReturnType<typeof useAudioContext>>["playSound"]
|
||||||
>;
|
>;
|
||||||
|
|
||||||
const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
const localRtcMember = mockRtcMembership("@carol:example.org", "CCCC");
|
||||||
const aliceRtcMember = mockRtcMembership("@alice:example.org", "AAAA");
|
|
||||||
const bobRtcMember = mockRtcMembership("@bob:example.org", "BBBB");
|
|
||||||
const daveRtcMember = mockRtcMembership("@dave:example.org", "DDDD");
|
|
||||||
|
|
||||||
const alice = mockMatrixRoomMember(aliceRtcMember);
|
|
||||||
const bob = mockMatrixRoomMember(bobRtcMember);
|
|
||||||
const carol = mockMatrixRoomMember(localRtcMember);
|
const carol = mockMatrixRoomMember(localRtcMember);
|
||||||
const dave = mockMatrixRoomMember(daveRtcMember);
|
const roomMembers = new Map([carol].map((p) => [p.userId, p]));
|
||||||
|
|
||||||
const roomId = "!foo:bar";
|
const roomId = "!foo:bar";
|
||||||
|
const soundPromise = Promise.resolve(true);
|
||||||
const roomMembers = new Map(
|
|
||||||
[alice, bob, carol, dave].map((p) => [p.userId, p]),
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
(prefetchSounds as MockedFunction<typeof prefetchSounds>).mockResolvedValue({
|
(prefetchSounds as MockedFunction<typeof prefetchSounds>).mockResolvedValue({
|
||||||
sound: new ArrayBuffer(0),
|
sound: new ArrayBuffer(0),
|
||||||
});
|
});
|
||||||
playSound = vitest.fn().mockResolvedValue(undefined);
|
playSound = vitest.fn().mockReturnValue(soundPromise);
|
||||||
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
(useAudioContext as MockedFunction<typeof useAudioContext>).mockReturnValue({
|
||||||
playSound,
|
playSound,
|
||||||
});
|
});
|
||||||
@@ -71,8 +71,10 @@ beforeEach(() => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("a leave sound should be played when the user leaves the call", async () => {
|
function createGroupCallView(widget: WidgetHelpers | null): {
|
||||||
const user = userEvent.setup();
|
rtcSession: MockRTCSession;
|
||||||
|
getByText: ReturnType<typeof render>["getByText"];
|
||||||
|
} {
|
||||||
const history = createBrowserHistory();
|
const history = createBrowserHistory();
|
||||||
const client = {
|
const client = {
|
||||||
getUser: () => null,
|
getUser: () => null,
|
||||||
@@ -94,7 +96,7 @@ test("a leave sound should be played when the user leaves the call", async () =>
|
|||||||
room,
|
room,
|
||||||
localRtcMember,
|
localRtcMember,
|
||||||
[],
|
[],
|
||||||
).withMemberships(of([aliceRtcMember, bobRtcMember]));
|
).withMemberships(of([]));
|
||||||
const muteState = {
|
const muteState = {
|
||||||
audio: { enabled: false },
|
audio: { enabled: false },
|
||||||
video: { enabled: false },
|
video: { enabled: false },
|
||||||
@@ -110,10 +112,40 @@ test("a leave sound should be played when the user leaves the call", async () =>
|
|||||||
hideHeader={true}
|
hideHeader={true}
|
||||||
rtcSession={rtcSession as unknown as MatrixRTCSession}
|
rtcSession={rtcSession as unknown as MatrixRTCSession}
|
||||||
muteStates={muteState}
|
muteStates={muteState}
|
||||||
|
widget={widget}
|
||||||
/>
|
/>
|
||||||
</Router>,
|
</Router>,
|
||||||
);
|
);
|
||||||
|
return {
|
||||||
|
getByText,
|
||||||
|
rtcSession,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
test("will play a leave sound asynchronously in SPA mode", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const { getByText, rtcSession } = createGroupCallView(null);
|
||||||
|
const leaveButton = getByText("Leave");
|
||||||
|
await user.click(leaveButton);
|
||||||
|
expect(playSound).toHaveBeenCalledWith("left");
|
||||||
|
expect(leaveRTCSession).toHaveBeenCalledWith(rtcSession, undefined);
|
||||||
|
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("will play a leave sound synchronously in widget mode", async () => {
|
||||||
|
const user = userEvent.setup();
|
||||||
|
const widget = {
|
||||||
|
api: {
|
||||||
|
setAlwaysOnScreen: async () => Promise.resolve(true),
|
||||||
|
} as Partial<WidgetHelpers["api"]>,
|
||||||
|
lazyActions: new LazyEventEmitter(),
|
||||||
|
};
|
||||||
|
const { getByText, rtcSession } = createGroupCallView(
|
||||||
|
widget as WidgetHelpers,
|
||||||
|
);
|
||||||
const leaveButton = getByText("Leave");
|
const leaveButton = getByText("Leave");
|
||||||
await user.click(leaveButton);
|
await user.click(leaveButton);
|
||||||
expect(playSound).toHaveBeenCalledWith("left");
|
expect(playSound).toHaveBeenCalledWith("left");
|
||||||
|
expect(leaveRTCSession).toHaveBeenCalledWith(rtcSession, soundPromise);
|
||||||
|
expect(rtcSession.leaveRoomSession).toHaveBeenCalledOnce();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { Heading, Text } from "@vector-im/compound-web";
|
|||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
|
|
||||||
import type { IWidgetApiRequest } from "matrix-widget-api";
|
import type { IWidgetApiRequest } from "matrix-widget-api";
|
||||||
import { widget, ElementWidgetActions, JoinCallData } from "../widget";
|
import { ElementWidgetActions, JoinCallData, WidgetHelpers } from "../widget";
|
||||||
import { FullScreenView } from "../FullScreenView";
|
import { FullScreenView } from "../FullScreenView";
|
||||||
import { LobbyView } from "./LobbyView";
|
import { LobbyView } from "./LobbyView";
|
||||||
import { MatrixInfo } from "./VideoPreview";
|
import { MatrixInfo } from "./VideoPreview";
|
||||||
@@ -60,6 +60,7 @@ interface Props {
|
|||||||
hideHeader: boolean;
|
hideHeader: boolean;
|
||||||
rtcSession: MatrixRTCSession;
|
rtcSession: MatrixRTCSession;
|
||||||
muteStates: MuteStates;
|
muteStates: MuteStates;
|
||||||
|
widget: WidgetHelpers | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GroupCallView: FC<Props> = ({
|
export const GroupCallView: FC<Props> = ({
|
||||||
@@ -71,6 +72,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
hideHeader,
|
hideHeader,
|
||||||
rtcSession,
|
rtcSession,
|
||||||
muteStates,
|
muteStates,
|
||||||
|
widget,
|
||||||
}) => {
|
}) => {
|
||||||
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
const memberships = useMatrixRTCSessionMemberships(rtcSession);
|
||||||
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
const isJoined = useMatrixRTCSessionJoinState(rtcSession);
|
||||||
@@ -193,14 +195,14 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
ev.detail.data as unknown as JoinCallData,
|
ev.detail.data as unknown as JoinCallData,
|
||||||
);
|
);
|
||||||
await enterRTCSession(rtcSession, perParticipantE2EE);
|
await enterRTCSession(rtcSession, perParticipantE2EE);
|
||||||
widget!.api.transport.reply(ev.detail, {});
|
widget.api.transport.reply(ev.detail, {});
|
||||||
})().catch((e) => {
|
})().catch((e) => {
|
||||||
logger.error("Error joining RTC session", e);
|
logger.error("Error joining RTC session", e);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
widget.lazyActions.on(ElementWidgetActions.JoinCall, onJoin);
|
||||||
return (): void => {
|
return (): void => {
|
||||||
widget!.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
widget.lazyActions.off(ElementWidgetActions.JoinCall, onJoin);
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
// No lobby and no preload: we enter the rtc session right away
|
// No lobby and no preload: we enter the rtc session right away
|
||||||
@@ -214,7 +216,7 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
void enterRTCSession(rtcSession, perParticipantE2EE);
|
void enterRTCSession(rtcSession, perParticipantE2EE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [rtcSession, preload, skipLobby, perParticipantE2EE]);
|
}, [widget, rtcSession, preload, skipLobby, perParticipantE2EE]);
|
||||||
|
|
||||||
const [left, setLeft] = useState(false);
|
const [left, setLeft] = useState(false);
|
||||||
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
|
const [leaveError, setLeaveError] = useState<Error | undefined>(undefined);
|
||||||
@@ -254,18 +256,25 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
logger.error("Error leaving RTC session", e);
|
logger.error("Error leaving RTC session", e);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
[rtcSession, isPasswordlessUser, confineToRoom, leaveSoundContext, history],
|
[
|
||||||
|
widget,
|
||||||
|
rtcSession,
|
||||||
|
isPasswordlessUser,
|
||||||
|
confineToRoom,
|
||||||
|
leaveSoundContext,
|
||||||
|
history,
|
||||||
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (widget && isJoined) {
|
if (widget && isJoined) {
|
||||||
// set widget to sticky once joined.
|
// set widget to sticky once joined.
|
||||||
widget!.api.setAlwaysOnScreen(true).catch((e) => {
|
widget.api.setAlwaysOnScreen(true).catch((e) => {
|
||||||
logger.error("Error calling setAlwaysOnScreen(true)", e);
|
logger.error("Error calling setAlwaysOnScreen(true)", e);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onHangup = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
const onHangup = (ev: CustomEvent<IWidgetApiRequest>): void => {
|
||||||
widget!.api.transport.reply(ev.detail, {});
|
widget.api.transport.reply(ev.detail, {});
|
||||||
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
|
// Only sends matrix leave event. The Livekit session will disconnect once the ActiveCall-view unmounts.
|
||||||
leaveRTCSession(rtcSession).catch((e) => {
|
leaveRTCSession(rtcSession).catch((e) => {
|
||||||
logger.error("Failed to leave RTC session", e);
|
logger.error("Failed to leave RTC session", e);
|
||||||
@@ -273,10 +282,10 @@ export const GroupCallView: FC<Props> = ({
|
|||||||
};
|
};
|
||||||
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
widget.lazyActions.once(ElementWidgetActions.HangupCall, onHangup);
|
||||||
return (): void => {
|
return (): void => {
|
||||||
widget!.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
widget.lazyActions.off(ElementWidgetActions.HangupCall, onHangup);
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}, [isJoined, rtcSession]);
|
}, [widget, isJoined, rtcSession]);
|
||||||
|
|
||||||
const onReconnect = useCallback(() => {
|
const onReconnect = useCallback(() => {
|
||||||
setLeft(false);
|
setLeft(false);
|
||||||
|
|||||||
@@ -98,6 +98,7 @@ export const RoomPage: FC = () => {
|
|||||||
case "loaded":
|
case "loaded":
|
||||||
return (
|
return (
|
||||||
<GroupCallView
|
<GroupCallView
|
||||||
|
widget={widget}
|
||||||
client={client!}
|
client={client!}
|
||||||
rtcSession={groupCallState.rtcSession}
|
rtcSession={groupCallState.rtcSession}
|
||||||
isPasswordlessUser={passwordlessUser}
|
isPasswordlessUser={passwordlessUser}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ Please see LICENSE in the repository root for full details.
|
|||||||
*/
|
*/
|
||||||
import { map, Observable, of, SchedulerLike } from "rxjs";
|
import { map, Observable, of, SchedulerLike } from "rxjs";
|
||||||
import { RunHelpers, TestScheduler } from "rxjs/testing";
|
import { RunHelpers, TestScheduler } from "rxjs/testing";
|
||||||
import { expect, vi } from "vitest";
|
import { expect, vi, vitest } from "vitest";
|
||||||
import {
|
import {
|
||||||
RoomMember,
|
RoomMember,
|
||||||
Room as MatrixRoom,
|
Room as MatrixRoom,
|
||||||
@@ -255,6 +255,12 @@ export class MockRTCSession extends TypedEventEmitter<
|
|||||||
MatrixRTCSessionEvent,
|
MatrixRTCSessionEvent,
|
||||||
MatrixRTCSessionEventHandlerMap
|
MatrixRTCSessionEventHandlerMap
|
||||||
> {
|
> {
|
||||||
|
public readonly statistics = {
|
||||||
|
counters: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
public leaveRoomSession = vitest.fn().mockResolvedValue(undefined);
|
||||||
|
|
||||||
public constructor(
|
public constructor(
|
||||||
public readonly room: Room,
|
public readonly room: Room,
|
||||||
private localMembership: CallMembership,
|
private localMembership: CallMembership,
|
||||||
|
|||||||
Reference in New Issue
Block a user