From 5a074e637a811c6d6468a555f306317286bb8ff9 Mon Sep 17 00:00:00 2001 From: Robin Date: Thu, 26 Mar 2026 11:28:48 +0100 Subject: [PATCH] Fix room list often showing the wrong icons for calls (#32881) * Give rooms with calls a proper accessible description Besides improving accessibility, this makes it possible to check for the presence of a call indicator in the room list in Playwright tests. * Make room list react to calls in a room, even when not connected to them To use the results of CallStore.getRoom reactively, you need to listen for Call events, not ConnectedCalls events. * Don't assume that every call starts off as a video call If a Call object is created by way of someone starting a voice call, then of course the call's initial type needs to be 'voice'. * Make room list items react to changes in call type The type of a call may change over time; therefore room list items explicitly need to react to the changes. * Update a call's type before notifying listeners of the change If we notify listeners of a change in a call's type before actually making that change, the listeners will be working with glitched state. This would cause the room list to show the wrong call type in certain situations. * Ignore the Vitest attachments directory --- .gitignore | 1 + .../playwright/e2e/voip/element-call.spec.ts | 42 ++- apps/web/src/models/Call.ts | 13 +- .../room-list/RoomListItemViewModel.ts | 14 +- apps/web/test/test-utils/call.ts | 2 + apps/web/test/test-utils/test-utils.ts | 1 + apps/web/test/unit-tests/models/Call-test.ts | 30 ++ .../room-list/RoomListItemViewModel-test.tsx | 25 +- .../with-video-call-auto.png | Bin 0 -> 7378 bytes .../with-voice-call-auto.png | Bin 0 -> 7436 bytes .../src/i18n/strings/en_EN.json | 4 +- .../RoomListItemView.stories.tsx | 36 ++ .../RoomListItemView.test.tsx | 12 + .../RoomListItemView/RoomListItemView.tsx | 4 + .../RoomListItemView.test.tsx.snap | 328 +++++++++++++++++- 15 files changed, 468 insertions(+), 44 deletions(-) create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemView/RoomListItemView.stories.tsx/with-video-call-auto.png create mode 100644 packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemView/RoomListItemView.stories.tsx/with-voice-call-auto.png diff --git a/.gitignore b/.gitignore index 6cf32a0680..34a8640e70 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ storybook-static /packages/shared-components/node_modules /packages/shared-components/dist /packages/shared-components/src/i18nKeys.d.ts +/packages/shared-components/.vitest-attachments # TSC incremental compilation information *.tsbuildinfo diff --git a/apps/web/playwright/e2e/voip/element-call.spec.ts b/apps/web/playwright/e2e/voip/element-call.spec.ts index da956dfe15..813d9a0601 100644 --- a/apps/web/playwright/e2e/voip/element-call.spec.ts +++ b/apps/web/playwright/e2e/voip/element-call.spec.ts @@ -189,25 +189,31 @@ test.describe("Element Call", () => { expect(hash.get("skipLobby")).toEqual("true"); }); - test("should be able to join a call in progress", async ({ page, user, bot, room, app }) => { - await app.viewRoomById(room.roomId); - // Allow bob to create a call - await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible(); - await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50); - // Fake a start of a call - await sendRTCState(bot, room.roomId); - const button = page.getByTestId("join-call-button"); - await expect(button).toBeInViewport({ timeout: 5000 }); - // And test joining - await button.click(); - const frameUrlStr = await page.locator("iframe").getAttribute("src"); - await expect(frameUrlStr).toBeDefined(); - const url = new URL(frameUrlStr); - const hash = new URLSearchParams(url.hash.slice(1)); - assertCommonCallParameters(url.searchParams, hash, user, room); + ["voice", "video"].forEach((callType) => { + test(`should be able to join a ${callType} call in progress`, async ({ page, user, bot, room, app }) => { + await app.viewRoomById(room.roomId); + // Allow bob to create a call + await expect(page.getByText("Bob and one other were invited and joined")).toBeVisible(); + await app.client.setPowerLevel(room.roomId, bot.credentials.userId, 50); + // Fake a start of a call + await sendRTCState(bot, room.roomId, undefined, callType === "voice" ? "audio" : "video"); + const button = page.getByTestId("join-call-button"); + await expect(button).toBeInViewport({ timeout: 5000 }); + // Room list should show that a call is ongoing + await expect( + page.getByRole("option", { name: `Open room TestRoom with a ${callType} call.` }), + ).toBeVisible(); + // And test joining + await button.click(); + const frameUrlStr = await page.locator("iframe").getAttribute("src"); + await expect(frameUrlStr).toBeDefined(); + const url = new URL(frameUrlStr); + const hash = new URLSearchParams(url.hash.slice(1)); + assertCommonCallParameters(url.searchParams, hash, user, room); - expect(hash.get("intent")).toEqual("join_existing"); - expect(hash.get("skipLobby")).toEqual(null); + expect(hash.get("intent")).toEqual("join_existing"); + expect(hash.get("skipLobby")).toEqual(null); + }); }); [true, false].forEach((skipLobbyToggle) => { diff --git a/apps/web/src/models/Call.ts b/apps/web/src/models/Call.ts index 535c04f2f2..4f7feec84d 100644 --- a/apps/web/src/models/Call.ts +++ b/apps/web/src/models/Call.ts @@ -108,16 +108,15 @@ export abstract class Call extends TypedEventEmitter { @@ -128,16 +129,25 @@ export class RoomListItemViewModel this.updateItem(); }; + /** + * Handler for call type changes. Only updates the item if the call type is actually present in the snapshot. + */ + private onCallTypeChanged = (): void => { + if (this.snapshot.current.notification.callType !== undefined) this.updateItem(); + }; + /** * Listen to participant changes for the current call in this room (if any) to trigger updates when participants join/leave the call. */ private listenToCallParticipants(): void { const call = CallStore.instance.getCall(this.props.room.roomId); - // Remove listener from previous call (if any) and add to new call to track participant changes + // Remove listeners from previous call (if any) and add to new call to track changes if (call !== this.currentCall) { this.currentCall?.off(CallEvent.Participants, this.onCallParticipantsChanged); + this.currentCall?.off(CallEvent.CallTypeChanged, this.onCallTypeChanged); call?.on(CallEvent.Participants, this.onCallParticipantsChanged); + call?.on(CallEvent.CallTypeChanged, this.onCallTypeChanged); } this.currentCall = call; } diff --git a/apps/web/test/test-utils/call.ts b/apps/web/test/test-utils/call.ts index 90ef5d1ea2..bbfe7eaee8 100644 --- a/apps/web/test/test-utils/call.ts +++ b/apps/web/test/test-utils/call.ts @@ -18,6 +18,7 @@ import { RoomStateEvent, type IContent, } from "matrix-js-sdk/src/matrix"; +import { CallType } from "matrix-js-sdk/src/webrtc/call"; import { mocked, type Mocked } from "jest-mock"; import { type MatrixRTCSession } from "matrix-js-sdk/src/matrixrtc"; @@ -52,6 +53,7 @@ export class MockedCall extends Call { waitForIframeLoad: false, }, room.client, + CallType.Video, ); this.groupCall = { creationTs: this.event.getTs() } as unknown as GroupCall; } diff --git a/apps/web/test/test-utils/test-utils.ts b/apps/web/test/test-utils/test-utils.ts index dce7257b83..4118960072 100644 --- a/apps/web/test/test-utils/test-utils.ts +++ b/apps/web/test/test-utils/test-utils.ts @@ -381,6 +381,7 @@ export function createStubMatrixRTC(): MatrixRTCSessionManager { const session = new EventEmitter() as MatrixRTCSession; session.memberships = []; session.getOldestMembership = () => undefined; + session.getConsensusCallIntent = () => "video"; return session; }); return { diff --git a/apps/web/test/unit-tests/models/Call-test.ts b/apps/web/test/unit-tests/models/Call-test.ts index 39f44ed4eb..ba3b264b54 100644 --- a/apps/web/test/unit-tests/models/Call-test.ts +++ b/apps/web/test/unit-tests/models/Call-test.ts @@ -26,6 +26,7 @@ import { MatrixRTCSession, MatrixRTCSessionEvent, } from "matrix-js-sdk/src/matrixrtc"; +import { CallType } from "matrix-js-sdk/src/webrtc/call"; import type { Mocked } from "jest-mock"; import type { ClientWidgetApi } from "matrix-widget-api"; @@ -987,6 +988,35 @@ describe("ElementCall", () => { call.off(CallEvent.Participants, onParticipants); }); + it("emits events when call type changes", async () => { + const onCallTypeChanged = jest.fn(); + call.on(CallEvent.CallTypeChanged, onCallTypeChanged); + // Should default to video when unknown + expect(call.callType).toBe(CallType.Video); + + // Change call type to voice + roomSession.memberships = [ + { sender: alice.userId, deviceId: "alices_device", callIntent: "audio" } as Mocked, + ]; + roomSession.getConsensusCallIntent.mockReturnValue("audio"); + roomSession.emit(MatrixRTCSessionEvent.MembershipsChanged, [], []); + + expect(call.callType).toBe(CallType.Voice); + expect(onCallTypeChanged.mock.calls).toEqual([[CallType.Voice]]); + + // Change call type back to video + roomSession.memberships = [ + { sender: alice.userId, deviceId: "alices_device", callIntent: "video" } as Mocked, + ]; + roomSession.getConsensusCallIntent.mockReturnValue("video"); + roomSession.emit(MatrixRTCSessionEvent.MembershipsChanged, [], []); + + expect(call.callType).toBe(CallType.Video); + expect(onCallTypeChanged.mock.calls).toEqual([[CallType.Voice], [CallType.Video]]); + + call.off(CallEvent.CallTypeChanged, onCallTypeChanged); + }); + it("ends the call immediately if the session ended", async () => { await connect(call, widgetApi); const onDestroy = jest.fn(); diff --git a/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx b/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx index de023d4dda..e3b349e085 100644 --- a/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx +++ b/apps/web/test/viewmodels/room-list/RoomListItemViewModel-test.tsx @@ -5,6 +5,7 @@ * Please see LICENSE files in the repository root for full details. */ +import EventEmitter from "events"; import { type MatrixClient, type MatrixEvent, @@ -26,7 +27,7 @@ import { DefaultTagID } from "../../../src/stores/room-list-v3/skip-list/tag"; import dispatcher from "../../../src/dispatcher/dispatcher"; import { Action } from "../../../src/dispatcher/actions"; import { CallStore } from "../../../src/stores/CallStore"; -import type { Call } from "../../../src/models/Call"; +import { CallEvent, type Call } from "../../../src/models/Call"; import { RoomListItemViewModel } from "../../../src/viewmodels/room-list/RoomListItemViewModel"; jest.mock("../../../src/viewmodels/room-list/utils", () => ({ @@ -436,6 +437,28 @@ describe("RoomListItemViewModel", () => { // The new call must have a listener registered expect(secondCall.on).toHaveBeenCalledWith("participants", expect.any(Function)); }); + + it("should listen to call type changes", async () => { + // Start with a voice call + let callType = CallType.Voice; + const mockCall = new (class extends EventEmitter { + get callType() { + return callType; + } + participants = new Map([[matrixClient.getUserId()!, {}]]); + })() as unknown as Call; + jest.spyOn(CallStore.instance, "getCall").mockReturnValue(mockCall); + + viewModel = new RoomListItemViewModel({ room, client: matrixClient }); + await flushPromises(); + + expect(viewModel.getSnapshot().notification.callType).toBe("voice"); + + // Now turn it into a video call + callType = CallType.Video; + mockCall.emit(CallEvent.CallTypeChanged, callType); + expect(viewModel.getSnapshot().notification.callType).toBe("video"); + }); }); describe("Room name updates", () => { diff --git a/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemView/RoomListItemView.stories.tsx/with-video-call-auto.png b/packages/shared-components/__vis__/linux/__baselines__/room-list/RoomListItemView/RoomListItemView.stories.tsx/with-video-call-auto.png new file mode 100644 index 0000000000000000000000000000000000000000..e96aa0573038db9c36be1dc43df59bad110a5846 GIT binary patch literal 7378 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", () => { expect(container).toMatchSnapshot(); }); + it("renders WithVoiceCall story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + + it("renders WithVideoCall story", () => { + const { container } = render(); + expect(container).toMatchSnapshot(); + }); + it("renders Invitation story", () => { const { container } = render(); expect(container).toMatchSnapshot(); diff --git a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.tsx b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.tsx index 2ba83f11a9..04e7238407 100644 --- a/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.tsx +++ b/packages/shared-components/src/room-list/RoomListItemView/RoomListItemView.tsx @@ -35,6 +35,10 @@ function getA11yLabel(roomName: string, notification: NotificationDecorationData return _t("room_list|a11y|mention", { roomName, count: notification.count }); } else if (notification.hasUnreadCount && notification.count) { return _t("room_list|a11y|unread", { roomName, count: notification.count }); + } else if (notification.callType === "voice") { + return _t("room_list|a11y|voice_call", { roomName }); + } else if (notification.callType === "video") { + return _t("room_list|a11y|video_call", { roomName }); } else { return _t("room_list|a11y|default", { roomName }); } diff --git a/packages/shared-components/src/room-list/RoomListItemView/__snapshots__/RoomListItemView.test.tsx.snap b/packages/shared-components/src/room-list/RoomListItemView/__snapshots__/RoomListItemView.test.tsx.snap index 441589bd07..084ce3ec52 100644 --- a/packages/shared-components/src/room-list/RoomListItemView/__snapshots__/RoomListItemView.test.tsx.snap +++ b/packages/shared-components/src/room-list/RoomListItemView/__snapshots__/RoomListItemView.test.tsx.snap @@ -319,11 +319,11 @@ exports[` > renders Invitation story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_2i_" + aria-labelledby="_r_3i_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_2g_" + id="radix-_r_3g_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -351,11 +351,11 @@ exports[` > renders Invitation story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_2p_" + aria-labelledby="_r_3p_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_2n_" + id="radix-_r_3n_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -461,11 +461,11 @@ exports[` > renders NoMessagePreview story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_3i_" + aria-labelledby="_r_4i_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_3g_" + id="radix-_r_4g_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -493,11 +493,11 @@ exports[` > renders NoMessagePreview story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_3p_" + aria-labelledby="_r_4p_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_3n_" + id="radix-_r_4n_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -721,11 +721,11 @@ exports[` > renders UnsentMessage story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_32_" + aria-labelledby="_r_42_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_30_" + id="radix-_r_40_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -753,11 +753,11 @@ exports[` > renders UnsentMessage story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_39_" + aria-labelledby="_r_49_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_37_" + id="radix-_r_47_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -869,11 +869,11 @@ exports[` > renders WithHoverMenu story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="More Options" - aria-labelledby="_r_42_" + aria-labelledby="_r_52_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_40_" + id="radix-_r_50_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -901,11 +901,11 @@ exports[` > renders WithHoverMenu story 1`] = ` aria-expanded="false" aria-haspopup="menu" aria-label="Notification options" - aria-labelledby="_r_49_" + aria-labelledby="_r_59_" class="_icon-button_1215g_8" data-kind="primary" data-state="closed" - id="radix-_r_47_" + id="radix-_r_57_" role="button" style="--cpd-icon-button-size: 24px; padding: 2px;" tabindex="0" @@ -1234,3 +1234,299 @@ exports[` > renders WithNotification story 1`] = ` `; + +exports[` > renders WithVideoCall story 1`] = ` +
+
+ + +
+ +
+ + + + +`; + +exports[` > renders WithVoiceCall story 1`] = ` +
+
+ + +
+ +
+ + + + +`;