Start voice call in PiP (not in fullscreen) (#34055)

* start voice call in pip

* Update jest.config.ts

* fix "join" button to use correct intent

* Add regression test for join button

* Update RoomHeader-test.tsx

* fmt

* Add playwright test

* Fix expected intent on "join" button press

* Add additional pip check to exising:
`should be able to join a ${callType} call in progress` test

* Update RoomHeader-test.tsx
This commit is contained in:
Timo
2026-07-06 15:57:26 +00:00
committed by GitHub
parent 3153912cc1
commit 20587b3495
5 changed files with 123 additions and 5 deletions
@@ -609,6 +609,30 @@ describe("RoomHeader", () => {
expect(joinButton).not.toHaveAttribute("aria-disabled", "true");
});
it("clicking the join button of an ongoing video call joins as a video call", async () => {
const user = userEvent.setup();
mockRoomMembers(room, 3);
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Video, true));
render(<RoomHeader room={room} />, getWrapper());
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
await user.click(getByLabelText(document.body, "Join video call"));
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true, voiceOnly: false }));
});
it("clicking the join button of an ongoing voice call joins as a voice call", async () => {
const user = userEvent.setup();
mockRoomMembers(room, 3);
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(createMockCall(ROOM_ID, 3, CallType.Voice, true));
render(<RoomHeader room={room} />, getWrapper());
const dispatcherSpy = jest.spyOn(dispatcher, "dispatch").mockImplementation();
await user.click(getByLabelText(document.body, "Join voice call"));
expect(dispatcherSpy).toHaveBeenCalledWith(expect.objectContaining({ view_call: true, voiceOnly: true }));
});
it("join button is disabled if there is an other ongoing call", async () => {
mockRoomMembers(room, 3);
// Mock CallStore to return a call with 3 participants
@@ -919,6 +943,7 @@ function createMockCall(
roomId: string = "!1:example.org",
participantCount: number = 0,
callType: CallType = CallType.Video,
isElementCall: boolean = false,
): Call {
const participants = new Map();
@@ -936,7 +961,7 @@ function createMockCall(
return {
roomId,
participants,
widget: { id: "test-widget" },
widget: { id: "test-widget", type: isElementCall ? "m.call" : undefined },
connectionState: "disconnected",
callType,
on: jest.fn(),
@@ -48,7 +48,8 @@ import { CallStore } from "../../../src/stores/CallStore";
import { MatrixClientPeg } from "../../../src/MatrixClientPeg";
import MediaDeviceHandler, { MediaDeviceKindEnum } from "../../../src/MediaDeviceHandler";
import { storeRoomAliasInCache } from "../../../src/RoomAliasCache.ts";
import { type Call } from "../../../src/models/Call.ts";
import { type Call, ConnectionState } from "../../../src/models/Call.ts";
import ActiveWidgetStore from "../../../src/stores/ActiveWidgetStore";
import { ModuleApi } from "../../../src/modules/Api";
jest.mock("../../../src/Modal");
@@ -430,6 +431,60 @@ describe("RoomViewStore", function () {
expect(call.presented).toEqual(true);
});
it("opens a voice-intent call directly in picture-in-picture rather than maximised", async () => {
const call = {
presented: false,
connectionState: ConnectionState.Disconnected,
widget: { id: "!widget:example.org" },
start: jest.fn(),
} as unknown as Call;
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(call);
const persistenceSpy = jest.spyOn(ActiveWidgetStore.instance, "setWidgetPersistence");
await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
voiceOnly: true,
metricsTrigger: undefined,
});
await untilDispatch(Action.ViewRoom, dis);
// The call is started and marked persistent so it renders in the PiP container...
expect(call.presented).toEqual(true);
expect(persistenceSpy).toHaveBeenCalledWith("!widget:example.org", roomId, true);
expect(call.start).toHaveBeenCalledWith(expect.objectContaining({ voiceOnly: true }));
// ...but the room is not switched to the maximised call view.
expect(roomViewStore.isViewingCall()).toEqual(false);
});
it("opens a video-intent call maximised in the room", async () => {
const call = {
presented: false,
connectionState: ConnectionState.Disconnected,
widget: { id: "!widget:example.org" },
start: jest.fn(),
} as unknown as Call;
jest.spyOn(CallStore.instance, "getCall").mockReturnValue(call);
const persistenceSpy = jest.spyOn(ActiveWidgetStore.instance, "setWidgetPersistence");
await setupAsyncStoreWithClient(CallStore.instance, MatrixClientPeg.safeGet());
dis.dispatch<ViewRoomPayload>({
action: Action.ViewRoom,
room_id: roomId,
view_call: true,
voiceOnly: false,
metricsTrigger: undefined,
});
await untilDispatch(Action.ViewRoom, dis);
expect(call.presented).toEqual(true);
expect(persistenceSpy).not.toHaveBeenCalled();
expect(call.start).toHaveBeenCalledWith(expect.objectContaining({ voiceOnly: false }));
expect(roomViewStore.isViewingCall()).toEqual(true);
});
it("should display an error message when the room is unreachable via the roomId", async () => {
// When
// View and wait for the room