@@ -9,14 +9,17 @@ Please see LICENSE files in the repository root for full details.
99// @vitest -environment happy-dom
1010
1111import React from "react" ;
12- import { describe , it , expect , beforeEach , vi } from "vitest" ;
13- import { fireEvent , render , type RenderResult , waitFor } from "test-utils-rtl" ;
12+ import { describe , it , expect , beforeEach , afterEach , type Mock , vi } from "vitest" ;
13+ import { act , fireEvent , render , type RenderResult , waitFor } from "test-utils-rtl" ;
1414import { type MatrixClient , type Room , type MatrixEvent , EventType , JoinRule } from "matrix-js-sdk/src/matrix" ;
15+ import { RTC_SLOT_ENCRYPTION_PER_MEMBER } from "matrix-js-sdk/src/matrixrtc" ;
1516import { mkStubRoom , stubClient } from "test-utils" ;
1617
1718import { MatrixClientPeg } from "../../../../../MatrixClientPeg" ;
1819import { VoipRoomSettingsTab } from "./VoipRoomSettingsTab" ;
1920import { ElementCallEventType , ElementCallMemberEventType } from "../../../../../call-types" ;
21+ import SettingsStore from "../../../../../settings/SettingsStore" ;
22+ import { SettingLevel } from "../../../../../settings/SettingLevel" ;
2023
2124describe ( "VoipRoomSettingsTab" , ( ) => {
2225 const roomId = "!room:example.com" ;
@@ -134,5 +137,163 @@ describe("VoipRoomSettingsTab", () => {
134137 ) ;
135138 } ) ;
136139 } ) ;
140+
141+ describe ( "slot handling" , ( ) => {
142+ interface MockSlotSession {
143+ ensureRtcSlotOpen : Mock ;
144+ ensureRtcSlotClosed : Mock ;
145+ }
146+
147+ /**
148+ * Mocks the room's MatrixRTC session with the given slot status (`undefined` for no slot).
149+ * @returns the mocked session for asserting on the slot methods.
150+ */
151+ const mockSlotSession = ( slotStatus ?: "open" | "closed" ) : MockSlotSession => {
152+ const session : MockSlotSession = {
153+ ensureRtcSlotOpen : vi . fn ( ) . mockResolvedValue ( undefined ) ,
154+ ensureRtcSlotClosed : vi . fn ( ) . mockResolvedValue ( undefined ) ,
155+ } ;
156+ vi . mocked ( cli . matrixRTC . getRoomSession ) . mockReturnValue (
157+ session as unknown as ReturnType < typeof cli . matrixRTC . getRoomSession > ,
158+ ) ;
159+ vi . mocked ( cli . matrixRTC . isSlotClosed ) . mockReturnValue (
160+ slotStatus === undefined ? undefined : slotStatus === "closed" ,
161+ ) ;
162+ return session ;
163+ } ;
164+
165+ beforeEach ( ( ) => {
166+ SettingsStore . setValue ( "feature_matrixrtc_slots" , null , SettingLevel . DEVICE , true ) ;
167+ } ) ;
168+
169+ afterEach ( ( ) => {
170+ SettingsStore . setValue ( "feature_matrixrtc_slots" , null , SettingLevel . DEVICE , false ) ;
171+ } ) ;
172+
173+ it ( "opens/creates the slot when enabling Element calls" , async ( ) => {
174+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 100 } ) ;
175+ const session = mockSlotSession ( ) ;
176+
177+ const tab = renderTab ( ) ;
178+ fireEvent . click ( getElementCallSwitch ( tab ) ) ;
179+
180+ await waitFor ( ( ) => expect ( session . ensureRtcSlotOpen ) . toHaveBeenCalledWith ( { encryption : undefined } ) ) ;
181+ expect ( session . ensureRtcSlotClosed ) . not . toHaveBeenCalled ( ) ;
182+ } ) ;
183+
184+ it ( "reopens the slot when enabling Element calls" , async ( ) => {
185+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 100 } ) ;
186+ const session = mockSlotSession ( "closed" ) ;
187+
188+ const tab = renderTab ( ) ;
189+ fireEvent . click ( getElementCallSwitch ( tab ) ) ;
190+
191+ await waitFor ( ( ) => expect ( session . ensureRtcSlotOpen ) . toHaveBeenCalledWith ( { encryption : undefined } ) ) ;
192+ expect ( session . ensureRtcSlotClosed ) . not . toHaveBeenCalled ( ) ;
193+ } ) ;
194+
195+ it ( "declares per-member encryption when opening the slot in an encrypted room" , async ( ) => {
196+ vi . mocked ( room . hasEncryptionStateEvent ) . mockReturnValue ( true ) ;
197+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 100 } ) ;
198+ const session = mockSlotSession ( ) ;
199+
200+ const tab = renderTab ( ) ;
201+ fireEvent . click ( getElementCallSwitch ( tab ) ) ;
202+
203+ await waitFor ( ( ) =>
204+ expect ( session . ensureRtcSlotOpen ) . toHaveBeenCalledWith ( {
205+ encryption : { type : RTC_SLOT_ENCRYPTION_PER_MEMBER } ,
206+ } ) ,
207+ ) ;
208+ } ) ;
209+
210+ it ( "closes the slot when disabling Element calls" , async ( ) => {
211+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 0 } ) ;
212+ const session = mockSlotSession ( "open" ) ;
213+
214+ const tab = renderTab ( ) ;
215+ fireEvent . click ( getElementCallSwitch ( tab ) ) ;
216+
217+ await waitFor ( ( ) => expect ( session . ensureRtcSlotClosed ) . toHaveBeenCalled ( ) ) ;
218+ expect ( session . ensureRtcSlotOpen ) . not . toHaveBeenCalled ( ) ;
219+ } ) ;
220+
221+ it ( "renders the switch as off when no slot has ever been created, even though power levels say enabled" , ( ) => {
222+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 0 } ) ;
223+ mockSlotSession ( ) ;
224+
225+ const tab = renderTab ( ) ;
226+
227+ expect ( getElementCallSwitch ( tab ) ) . not . toBeChecked ( ) ;
228+ } ) ;
229+
230+ it ( "does not touch the slot when the labs flag is disabled" , async ( ) => {
231+ SettingsStore . setValue ( "feature_matrixrtc_slots" , null , SettingLevel . DEVICE , false ) ;
232+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 100 } ) ;
233+ const session = mockSlotSession ( ) ;
234+
235+ const tab = renderTab ( ) ;
236+ fireEvent . click ( getElementCallSwitch ( tab ) ) ;
237+
238+ await waitFor ( ( ) =>
239+ expect ( cli . sendStateEvent ) . toHaveBeenCalledWith (
240+ room . roomId ,
241+ EventType . RoomPowerLevels ,
242+ expect . anything ( ) ,
243+ ) ,
244+ ) ;
245+ expect ( session . ensureRtcSlotOpen ) . not . toHaveBeenCalled ( ) ;
246+ expect ( session . ensureRtcSlotClosed ) . not . toHaveBeenCalled ( ) ;
247+ } ) ;
248+
249+ it ( "does not touch power levels or the slot when the user cannot send RTCSlot state events" , ( ) => {
250+ vi . spyOn ( room . currentState , "maySendStateEvent" ) . mockImplementation (
251+ ( eventType ) => eventType !== EventType . RTCSlot ,
252+ ) ;
253+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 100 } ) ;
254+ const session = mockSlotSession ( ) ;
255+
256+ const tab = renderTab ( ) ;
257+ fireEvent . click ( getElementCallSwitch ( tab ) ) ;
258+
259+ expect ( cli . sendStateEvent ) . not . toHaveBeenCalled ( ) ;
260+ expect ( session . ensureRtcSlotOpen ) . not . toHaveBeenCalled ( ) ;
261+ expect ( session . ensureRtcSlotClosed ) . not . toHaveBeenCalled ( ) ;
262+ } ) ;
263+
264+ it ( "disables the switch when the feature is on and the user cannot send RTCSlot state events" , ( ) => {
265+ vi . spyOn ( room . currentState , "maySendStateEvent" ) . mockImplementation (
266+ ( eventType ) => eventType !== EventType . RTCSlot ,
267+ ) ;
268+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 100 } ) ;
269+
270+ const tab = renderTab ( ) ;
271+
272+ expect ( getElementCallSwitch ( tab ) ) . toBeDisabled ( ) ;
273+ } ) ;
274+
275+ it ( "does not disable the switch for missing RTCSlot permission when the feature is off" , ( ) => {
276+ act ( ( ) => {
277+ SettingsStore . setValue ( "feature_matrixrtc_slots" , null , SettingLevel . DEVICE , false ) ;
278+ } ) ;
279+ vi . spyOn ( room . currentState , "maySendStateEvent" ) . mockImplementation (
280+ ( eventType ) => eventType !== EventType . RTCSlot ,
281+ ) ;
282+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 100 } ) ;
283+
284+ const tab = renderTab ( ) ;
285+
286+ expect ( getElementCallSwitch ( tab ) ) . not . toBeDisabled ( ) ;
287+ } ) ;
288+
289+ it ( "renders the switch as off when the slot is closed, even though power levels say enabled" , ( ) => {
290+ mockPowerLevels ( { [ ElementCallMemberEventType . name ] : 0 } ) ;
291+ mockSlotSession ( "closed" ) ;
292+
293+ const tab = renderTab ( ) ;
294+
295+ expect ( getElementCallSwitch ( tab ) ) . not . toBeChecked ( ) ;
296+ } ) ;
297+ } ) ;
137298 } ) ;
138299} ) ;
0 commit comments