Bläddra i källkod

feat(tests): Adds breakout tests. (#15414)

* feat(tests): Introduces BasePageObject.

* fix(tests): Use wdio aria selector where possible.

* fix(tests): Correct test exclusion for Firefox.

* fix(tests): Rearrange code.

* feat(tests): Adds breakout tests.
factor2
Дамян Минков 5 månader sedan
förälder
incheckning
c6cce9253c
Inget konto är kopplat till bidragsgivarens mejladress

+ 26
- 0
tests/helpers/Participant.ts Visa fil

@@ -4,6 +4,7 @@ import { multiremotebrowser } from '@wdio/globals';
4 4
 
5 5
 import { IConfig } from '../../react/features/base/config/configType';
6 6
 import { urlObjectToString } from '../../react/features/base/util/uri';
7
+import BreakoutRooms from '../pageobjects/BreakoutRooms';
7 8
 import Filmstrip from '../pageobjects/Filmstrip';
8 9
 import IframeAPI from '../pageobjects/IframeAPI';
9 10
 import Notifications from '../pageobjects/Notifications';
@@ -251,6 +252,22 @@ export class Participant {
251 252
             && APP.store?.getState()['features/base/participants']?.local?.role === 'moderator');
252 253
     }
253 254
 
255
+    /**
256
+     * Checks if the meeting supports breakout rooms.
257
+     */
258
+    async isBreakoutRoomsSupported() {
259
+        return await this.driver.execute(() => typeof APP !== 'undefined'
260
+            && APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isSupported());
261
+    }
262
+
263
+    /**
264
+     * Checks if the participant is in breakout room.
265
+     */
266
+    async isInBreakoutRoom() {
267
+        return await this.driver.execute(() => typeof APP !== 'undefined'
268
+            && APP.store?.getState()['features/base/conference'].conference?.getBreakoutRooms()?.isBreakoutRoom());
269
+    }
270
+
254 271
     /**
255 272
      * Waits to join the muc.
256 273
      *
@@ -321,6 +338,15 @@ export class Participant {
321 338
         });
322 339
     }
323 340
 
341
+    /**
342
+     * Returns the BreakoutRooms for this participant.
343
+     *
344
+     * @returns {BreakoutRooms}
345
+     */
346
+    getBreakoutRooms(): BreakoutRooms {
347
+        return new BreakoutRooms(this);
348
+    }
349
+
324 350
     /**
325 351
      * Returns the toolbar for this participant.
326 352
      *

+ 18
- 0
tests/helpers/participants.ts Visa fil

@@ -6,6 +6,8 @@ import { v4 as uuidv4 } from 'uuid';
6 6
 import { Participant } from './Participant';
7 7
 import { IContext, IJoinOptions } from './types';
8 8
 
9
+const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
10
+
9 11
 /**
10 12
  * Ensure that there is on participant.
11 13
  *
@@ -237,3 +239,19 @@ export function parseJid(str: string): {
237 239
         resource: domainParts.length > 0 ? domainParts[1] : undefined
238 240
     };
239 241
 }
242
+
243
+/**
244
+ * Check the subject of the participant.
245
+ * @param participant
246
+ * @param subject
247
+ */
248
+export async function checkSubject(participant: Participant, subject: string) {
249
+    const localTile = participant.driver.$(SUBJECT_XPATH);
250
+
251
+    await localTile.waitForExist();
252
+    await localTile.moveTo();
253
+
254
+    const txt = await localTile.getText();
255
+
256
+    expect(txt.startsWith(subject)).toBe(true);
257
+}

+ 2
- 12
tests/pageobjects/AVModerationMenu.ts Visa fil

@@ -1,4 +1,4 @@
1
-import { Participant } from '../helpers/Participant';
1
+import BasePageObject from './BasePageObject';
2 2
 
3 3
 const START_AUDIO_MODERATION = 'participants-pane-context-menu-start-audio-moderation';
4 4
 const STOP_AUDIO_MODERATION = 'participants-pane-context-menu-stop-audio-moderation';
@@ -8,17 +8,7 @@ const STOP_VIDEO_MODERATION = 'participants-pane-context-menu-stop-video-moderat
8 8
 /**
9 9
  * Represents the Audio Video Moderation menu in the participants pane.
10 10
  */
11
-export default class AVModerationMenu {
12
-    private participant: Participant;
13
-
14
-    /**
15
-     * Represents the Audio Video Moderation menu in the participants pane.
16
-     * @param participant
17
-     */
18
-    constructor(participant: Participant) {
19
-        this.participant = participant;
20
-    }
21
-
11
+export default class AVModerationMenu extends BasePageObject {
22 12
     /**
23 13
      * Clicks the start audio moderation menu item.
24 14
      */

+ 2
- 13
tests/pageobjects/BaseDialog.ts Visa fil

@@ -1,4 +1,4 @@
1
-import { Participant } from '../helpers/Participant';
1
+import BasePageObject from './BasePageObject';
2 2
 
3 3
 const CLOSE_BUTTON = 'modal-header-close-button';
4 4
 const OK_BUTTON = 'modal-dialog-ok-button';
@@ -6,18 +6,7 @@ const OK_BUTTON = 'modal-dialog-ok-button';
6 6
 /**
7 7
  * Base class for all dialogs.
8 8
  */
9
-export default class BaseDialog {
10
-    participant: Participant;
11
-
12
-    /**
13
-     * Initializes for a participant.
14
-     *
15
-     * @param {Participant} participant - The participant.
16
-     */
17
-    constructor(participant: Participant) {
18
-        this.participant = participant;
19
-    }
20
-
9
+export default class BaseDialog extends BasePageObject {
21 10
     /**
22 11
      *  Clicks on the X (close) button.
23 12
      */

+ 16
- 0
tests/pageobjects/BasePageObject.ts Visa fil

@@ -0,0 +1,16 @@
1
+import { Participant } from '../helpers/Participant';
2
+
3
+/**
4
+ * Represents the base page object.
5
+ * All page object has the current participant (holding the driver/browser session).
6
+ */
7
+export default class BasePageObject {
8
+    participant: Participant;
9
+
10
+    /**
11
+     * Represents the base page object.
12
+     */
13
+    constructor(participant: Participant) {
14
+        this.participant = participant;
15
+    }
16
+}

+ 230
- 0
tests/pageobjects/BreakoutRooms.ts Visa fil

@@ -0,0 +1,230 @@
1
+import { Participant } from '../helpers/Participant';
2
+
3
+import BaseDialog from './BaseDialog';
4
+import BasePageObject from './BasePageObject';
5
+
6
+const BREAKOUT_ROOMS_CLASS = 'breakout-room-container';
7
+const ADD_BREAKOUT_ROOM = 'Add breakout room';
8
+const MORE_LABEL = 'More';
9
+const LEAVE_ROOM_LABEL = 'Leave breakout room';
10
+const AUTO_ASSIGN_LABEL = 'Auto assign to breakout rooms';
11
+
12
+/**
13
+ * Represents a single breakout room and the operations for it.
14
+ */
15
+class BreakoutRoom extends BasePageObject {
16
+    title: string;
17
+    id: string;
18
+    count: number;
19
+
20
+    /**
21
+     * Constructs a breakout room.
22
+     */
23
+    constructor(participant: Participant, title: string, id: string) {
24
+        super(participant);
25
+
26
+        this.title = title;
27
+        this.id = id;
28
+
29
+        const tMatch = title.match(/.*\((.*)\)/);
30
+
31
+        if (tMatch) {
32
+            this.count = parseInt(tMatch[1], 10);
33
+        }
34
+    }
35
+
36
+    /**
37
+     * Returns room name.
38
+     */
39
+    get name() {
40
+        return this.title.split('(')[0].trim();
41
+    }
42
+
43
+    /**
44
+     * Returns the number of participants in the room.
45
+     */
46
+    get participantCount() {
47
+        return this.count;
48
+    }
49
+
50
+    /**
51
+     * Collapses the breakout room.
52
+     */
53
+    async collapse() {
54
+        const collapseElem = this.participant.driver.$(
55
+            `div[data-testid="${this.id}"]`);
56
+
57
+        await collapseElem.click();
58
+    }
59
+
60
+    /**
61
+     * Joins the breakout room.
62
+     */
63
+    async joinRoom() {
64
+        const joinButton = this.participant.driver
65
+            .$(`button[data-testid="join-room-${this.id}"]`);
66
+
67
+        await joinButton.waitForClickable();
68
+        await joinButton.click();
69
+    }
70
+
71
+    /**
72
+     * Removes the breakout room.
73
+     */
74
+    async removeRoom() {
75
+        await this.openContextMenu();
76
+
77
+        const removeButton = this.participant.driver.$(`#remove-room-${this.id}`);
78
+
79
+        await removeButton.waitForClickable();
80
+        await removeButton.click();
81
+    }
82
+
83
+    /**
84
+     * Renames the breakout room.
85
+     */
86
+    async renameRoom(newName: string) {
87
+        await this.openContextMenu();
88
+
89
+        const renameButton = this.participant.driver.$(`#rename-room-${this.id}`);
90
+
91
+        await renameButton.click();
92
+
93
+        const newNameInput = this.participant.driver.$('input[name="breakoutRoomName"]');
94
+
95
+        await newNameInput.waitForStable();
96
+        await newNameInput.setValue(newName);
97
+
98
+        await new BaseDialog(this.participant).clickOkButton();
99
+    }
100
+
101
+    /**
102
+     * Closes the breakout room.
103
+     */
104
+    async closeRoom() {
105
+        await this.openContextMenu();
106
+
107
+        const closeButton = this.participant.driver.$(`#close-room-${this.id}`);
108
+
109
+        await closeButton.waitForClickable();
110
+        await closeButton.click();
111
+    }
112
+
113
+    /**
114
+     * Opens the context menu.
115
+     * @private
116
+     */
117
+    private async openContextMenu() {
118
+        const listItem = this.participant.driver.$(`div[data-testid="${this.id}"]`);
119
+
120
+        await listItem.click();
121
+
122
+        const button = listItem.$(`aria/${MORE_LABEL}`);
123
+
124
+        await button.waitForClickable();
125
+        await button.click();
126
+    }
127
+}
128
+
129
+/**
130
+ * All breakout rooms objects and operations.
131
+ */
132
+export default class BreakoutRooms extends BasePageObject {
133
+    /**
134
+     * Returns the number of breakout rooms.
135
+     */
136
+    async getRoomsCount() {
137
+        const participantsPane = this.participant.getParticipantsPane();
138
+
139
+        if (!await participantsPane.isOpen()) {
140
+            await participantsPane.open();
141
+        }
142
+
143
+        return await this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`).length;
144
+    }
145
+
146
+    /**
147
+     * Adds a breakout room.
148
+     */
149
+    async addBreakoutRoom() {
150
+        const participantsPane = this.participant.getParticipantsPane();
151
+
152
+        if (!await participantsPane.isOpen()) {
153
+            await participantsPane.open();
154
+        }
155
+
156
+        const addBreakoutButton = this.participant.driver.$(`aria/${ADD_BREAKOUT_ROOM}`);
157
+
158
+        await addBreakoutButton.waitForDisplayed();
159
+        await addBreakoutButton.click();
160
+    }
161
+
162
+    /**
163
+     * Returns all breakout rooms.
164
+     */
165
+    async getRooms(): Promise<BreakoutRoom[]> {
166
+        const rooms = this.participant.driver.$$(`.${BREAKOUT_ROOMS_CLASS}`);
167
+
168
+        return rooms.map(async room => new BreakoutRoom(
169
+                this.participant, await room.$('span').getText(), await room.getAttribute('data-testid')));
170
+    }
171
+
172
+    /**
173
+     * Leave by clicking the leave button in participant pane.
174
+     */
175
+    async leaveBreakoutRoom() {
176
+        const participantsPane = this.participant.getParticipantsPane();
177
+
178
+        if (!await participantsPane.isOpen()) {
179
+            await participantsPane.open();
180
+        }
181
+
182
+        const leaveButton = this.participant.driver.$(`aria/${LEAVE_ROOM_LABEL}`);
183
+
184
+        await leaveButton.isClickable();
185
+        await leaveButton.click();
186
+    }
187
+
188
+    /**
189
+     * Auto assign participants to breakout rooms.
190
+     */
191
+    async autoAssignToBreakoutRooms() {
192
+        const button = this.participant.driver.$(`aria/${AUTO_ASSIGN_LABEL}`);
193
+
194
+        await button.waitForClickable();
195
+        await button.click();
196
+    }
197
+
198
+    /**
199
+     * Tries to send a participant to a breakout room.
200
+     */
201
+    async sendParticipantToBreakoutRoom(participant: Participant, roomName: string) {
202
+        const participantsPane = this.participant.getParticipantsPane();
203
+
204
+        await participantsPane.selectParticipant(participant);
205
+        await participantsPane.openParticipantContextMenu(participant);
206
+
207
+        const sendButton = this.participant.driver.$(`aria/${roomName}`);
208
+
209
+        await sendButton.waitForClickable();
210
+        await sendButton.click();
211
+    }
212
+
213
+    // /**
214
+    //  * Open context menu for given participant.
215
+    //  */
216
+    // async openParticipantContextMenu(participant: Participant) {
217
+    //     const listItem = this.participant.driver.$(
218
+    //         `div[@id="participant-item-${await participant.getEndpointId()}"]`);
219
+    //
220
+    //     await listItem.waitForDisplayed();
221
+    //     await listItem.moveTo();
222
+    //
223
+    //     const button = listItem.$(`aria/${PARTICIPANT_MORE_LABEL}`);
224
+    //
225
+    //     await button.waitForClickable();
226
+    //     await button.click();
227
+    // }
228
+}
229
+
230
+

+ 2
- 12
tests/pageobjects/Filmstrip.ts Visa fil

@@ -1,22 +1,12 @@
1 1
 import { Participant } from '../helpers/Participant';
2 2
 
3 3
 import BaseDialog from './BaseDialog';
4
+import BasePageObject from './BasePageObject';
4 5
 
5 6
 /**
6 7
  * Filmstrip elements.
7 8
  */
8
-export default class Filmstrip {
9
-    private participant: Participant;
10
-
11
-    /**
12
-     * Initializes for a participant.
13
-     *
14
-     * @param {Participant} participant - The participant.
15
-     */
16
-    constructor(participant: Participant) {
17
-        this.participant = participant;
18
-    }
19
-
9
+export default class Filmstrip extends BasePageObject {
20 10
     /**
21 11
      * Asserts that {@code participant} shows or doesn't show the audio
22 12
      * mute icon for the conference participant identified by

+ 3
- 12
tests/pageobjects/IframeAPI.ts Visa fil

@@ -1,20 +1,11 @@
1
-import { Participant } from '../helpers/Participant';
2 1
 import { LOG_PREFIX } from '../helpers/browserLogger';
3 2
 
3
+import BasePageObject from './BasePageObject';
4
+
4 5
 /**
5 6
  * The Iframe API and helpers from iframeAPITest.html
6 7
  */
7
-export default class IframeAPI {
8
-    private participant: Participant;
9
-
10
-    /**
11
-     * Initializes for a participant.
12
-     * @param participant
13
-     */
14
-    constructor(participant: Participant) {
15
-        this.participant = participant;
16
-    }
17
-
8
+export default class IframeAPI extends BasePageObject {
18 9
     /**
19 10
      * Returns the json object from the iframeAPI helper.
20 11
      * @param event

+ 2
- 12
tests/pageobjects/Notifications.ts Visa fil

@@ -1,4 +1,4 @@
1
-import { Participant } from '../helpers/Participant';
1
+import BasePageObject from './BasePageObject';
2 2
 
3 3
 const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
4 4
 const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
@@ -9,17 +9,7 @@ const RAISE_HAND_NOTIFICATION_ID = 'notify.raisedHand';
9 9
 /**
10 10
  * Gathers all notifications logic in the UI and obtaining those.
11 11
  */
12
-export default class Notifications {
13
-    private participant: Participant;
14
-
15
-    /**
16
-     * Represents the Audio Video Moderation menu in the participants pane.
17
-     * @param participant
18
-     */
19
-    constructor(participant: Participant) {
20
-        this.participant = participant;
21
-    }
22
-
12
+export default class Notifications extends BasePageObject {
23 13
     /**
24 14
      * Waits for the raised hand notification to be displayed.
25 15
      * The notification on moderators page when the participant tries to unmute.

+ 32
- 26
tests/pageobjects/ParticipantsPane.ts Visa fil

@@ -1,6 +1,7 @@
1 1
 import { Participant } from '../helpers/Participant';
2 2
 
3 3
 import AVModerationMenu from './AVModerationMenu';
4
+import BasePageObject from './BasePageObject';
4 5
 
5 6
 /**
6 7
  * Classname of the closed/hidden participants pane
@@ -10,18 +11,7 @@ const PARTICIPANTS_PANE = 'participants_pane';
10 11
 /**
11 12
  * Represents the participants pane from the UI.
12 13
  */
13
-export default class ParticipantsPane {
14
-    private participant: Participant;
15
-
16
-    /**
17
-     * Initializes for a participant.
18
-     *
19
-     * @param {Participant} participant - The participant.
20
-     */
21
-    constructor(participant: Participant) {
22
-        this.participant = participant;
23
-    }
24
-
14
+export default class ParticipantsPane extends BasePageObject {
25 15
     /**
26 16
      * Gets the audio video moderation menu.
27 17
      */
@@ -138,22 +128,10 @@ export default class ParticipantsPane {
138 128
         await this.participant.getNotifications().dismissAnyJoinNotification();
139 129
 
140 130
         const participantId = await participantToUnmute.getEndpointId();
141
-        const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
142
-
143
-        await participantItem.waitForExist();
144
-        await participantItem.waitForStable();
145
-        await participantItem.waitForDisplayed();
146
-        await participantItem.moveTo();
147 131
 
132
+        await this.selectParticipant(participantToUnmute);
148 133
         if (fromContextMenu) {
149
-            const meetingParticipantMoreOptions = this.participant.driver
150
-                .$(`[data-testid="participant-more-options-${participantId}"]`);
151
-
152
-            await meetingParticipantMoreOptions.waitForExist();
153
-            await meetingParticipantMoreOptions.waitForDisplayed();
154
-            await meetingParticipantMoreOptions.waitForStable();
155
-            await meetingParticipantMoreOptions.moveTo();
156
-            await meetingParticipantMoreOptions.click();
134
+            await this.openParticipantContextMenu(participantToUnmute);
157 135
         }
158 136
 
159 137
         const unmuteButton = this.participant.driver
@@ -162,4 +140,32 @@ export default class ParticipantsPane {
162 140
         await unmuteButton.waitForExist();
163 141
         await unmuteButton.click();
164 142
     }
143
+
144
+    /**
145
+     * Open context menu for given participant.
146
+     */
147
+    async selectParticipant(participant: Participant) {
148
+        const participantId = await participant.getEndpointId();
149
+        const participantItem = this.participant.driver.$(`#participant-item-${participantId}`);
150
+
151
+        await participantItem.waitForExist();
152
+        await participantItem.waitForStable();
153
+        await participantItem.waitForDisplayed();
154
+        await participantItem.moveTo();
155
+    }
156
+
157
+    /**
158
+     * Open context menu for given participant.
159
+     */
160
+    async openParticipantContextMenu(participant: Participant) {
161
+        const participantId = await participant.getEndpointId();
162
+        const meetingParticipantMoreOptions = this.participant.driver
163
+            .$(`[data-testid="participant-more-options-${participantId}"]`);
164
+
165
+        await meetingParticipantMoreOptions.waitForExist();
166
+        await meetingParticipantMoreOptions.waitForDisplayed();
167
+        await meetingParticipantMoreOptions.waitForStable();
168
+        await meetingParticipantMoreOptions.moveTo();
169
+        await meetingParticipantMoreOptions.click();
170
+    }
165 171
 }

+ 9
- 18
tests/pageobjects/Toolbar.ts Visa fil

@@ -1,5 +1,4 @@
1
-// eslint-disable-next-line no-unused-vars
2
-import { Participant } from '../helpers/Participant';
1
+import BasePageObject from './BasePageObject';
3 2
 
4 3
 const AUDIO_MUTE = 'Mute microphone';
5 4
 const AUDIO_UNMUTE = 'Unmute microphone';
@@ -16,18 +15,7 @@ const VIDEO_UNMUTE = 'Start camera';
16 15
 /**
17 16
  * The toolbar elements.
18 17
  */
19
-export default class Toolbar {
20
-    private participant: Participant;
21
-
22
-    /**
23
-     * Creates toolbar for a participant.
24
-     *
25
-     * @param {Participant} participant - The participants.
26
-     */
27
-    constructor(participant: Participant) {
28
-        this.participant = participant;
29
-    }
30
-
18
+export default class Toolbar extends BasePageObject {
31 19
     /**
32 20
      * Returns the button.
33 21
      *
@@ -36,7 +24,7 @@ export default class Toolbar {
36 24
      * @private
37 25
      */
38 26
     private getButton(accessibilityCSSSelector: string) {
39
-        return this.participant.driver.$(`[aria-label^="${accessibilityCSSSelector}"]`);
27
+        return this.participant.driver.$(`aria/${accessibilityCSSSelector}`);
40 28
     }
41 29
 
42 30
     /**
@@ -125,7 +113,10 @@ export default class Toolbar {
125 113
      */
126 114
     async clickParticipantsPaneButton(): Promise<void> {
127 115
         this.participant.log('Clicking on: Participants pane Button');
128
-        await this.getButton(PARTICIPANTS).click();
116
+
117
+        // Special case for participants pane button, as it contains the number of participants and its label
118
+        // is changing
119
+        await this.participant.driver.$(`[aria-label^="${PARTICIPANTS}"]`).click();
129 120
     }
130 121
 
131 122
     /**
@@ -170,7 +161,7 @@ export default class Toolbar {
170 161
      * @private
171 162
      */
172 163
     private async isOverflowMenuOpen() {
173
-        return await this.participant.driver.$$(`[aria-label^="${OVERFLOW_MENU}"]`).length > 0;
164
+        return await this.participant.driver.$$(`aria/${OVERFLOW_MENU}`).length > 0;
174 165
     }
175 166
 
176 167
     /**
@@ -215,7 +206,7 @@ export default class Toolbar {
215 206
      * @private
216 207
      */
217 208
     private async waitForOverFlowMenu(visible: boolean) {
218
-        await this.participant.driver.$(`[aria-label^="${OVERFLOW_MENU}"]`).waitForDisplayed({
209
+        await this.getButton(OVERFLOW_MENU).waitForDisplayed({
219 210
             reverse: !visible,
220 211
             timeout: 3000,
221 212
             timeoutMsg: `Overflow menu is not ${visible ? 'visible' : 'hidden'}`

+ 30
- 30
tests/specs/2way/audioOnly.spec.ts Visa fil

@@ -33,36 +33,6 @@ describe('Audio only - ', () => {
33 33
         await setAudioOnlyAndCheck(false);
34 34
     });
35 35
 
36
-    /**
37
-     * Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
38
-     * p2 participant sees a video mute state for the former.
39
-     * @param enable
40
-     */
41
-    async function setAudioOnlyAndCheck(enable: boolean) {
42
-        const { p1 } = ctx;
43
-
44
-        await p1.getVideoQualityDialog().setVideoQuality(enable);
45
-
46
-        await verifyVideoMute(enable);
47
-
48
-        await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
49
-            .waitForDisplayed({ reverse: !enable });
50
-    }
51
-
52
-    /**
53
-     * Verifies that p1 and p2 see p1 as video muted or not.
54
-     * @param muted
55
-     */
56
-    async function verifyVideoMute(muted: boolean) {
57
-        const { p1, p2 } = ctx;
58
-
59
-        // Verify the observer sees the testee in the desired muted state.
60
-        await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
61
-
62
-        // Verify the testee sees itself in the desired muted state.
63
-        await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
64
-    }
65
-
66 36
     /**
67 37
      * Mutes video on participant1, toggles audio-only twice and then verifies if both participants see participant1
68 38
      * as video muted.
@@ -92,3 +62,33 @@ describe('Audio only - ', () => {
92 62
         await verifyVideoMute(false);
93 63
     });
94 64
 });
65
+
66
+/**
67
+ * Toggles the audio only state of a p1 participant and verifies participant sees the audio only label and that
68
+ * p2 participant sees a video mute state for the former.
69
+ * @param enable
70
+ */
71
+async function setAudioOnlyAndCheck(enable: boolean) {
72
+    const { p1 } = ctx;
73
+
74
+    await p1.getVideoQualityDialog().setVideoQuality(enable);
75
+
76
+    await verifyVideoMute(enable);
77
+
78
+    await p1.driver.$('//div[@id="videoResolutionLabel"][contains(@class, "audio-only")]')
79
+        .waitForDisplayed({ reverse: !enable });
80
+}
81
+
82
+/**
83
+ * Verifies that p1 and p2 see p1 as video muted or not.
84
+ * @param muted
85
+ */
86
+async function verifyVideoMute(muted: boolean) {
87
+    const { p1, p2 } = ctx;
88
+
89
+    // Verify the observer sees the testee in the desired muted state.
90
+    await p2.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
91
+
92
+    // Verify the testee sees itself in the desired muted state.
93
+    await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p1, !muted);
94
+}

+ 464
- 0
tests/specs/3way/breakoutRooms.spec.ts Visa fil

@@ -0,0 +1,464 @@
1
+import type { ChainablePromiseElement } from 'webdriverio';
2
+
3
+import type { Participant } from '../../helpers/Participant';
4
+import { checkSubject, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
5
+
6
+const MAIN_ROOM_NAME = 'Main room';
7
+const BREAKOUT_ROOMS_LIST_ID = 'breakout-rooms-list';
8
+const LIST_ITEM_CONTAINER = 'list-item-container';
9
+
10
+describe('BreakoutRooms ', () => {
11
+    it('check support', async () => {
12
+        await ensureTwoParticipants(ctx);
13
+
14
+        if (!await ctx.p1.isBreakoutRoomsSupported()) {
15
+            ctx.skipSuiteTests = true;
16
+        }
17
+    });
18
+
19
+    it('add breakout room', async () => {
20
+        const { p1, p2 } = ctx;
21
+        const p1BreakoutRooms = p1.getBreakoutRooms();
22
+
23
+        // there should be no breakout rooms initially, list is sent with a small delay
24
+        await p1.driver.pause(2000);
25
+        expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
26
+
27
+        // add one breakout room
28
+        await p1BreakoutRooms.addBreakoutRoom();
29
+
30
+        await p1.driver.waitUntil(
31
+            async () => await p1BreakoutRooms.getRoomsCount() === 1, {
32
+                timeout: 2000,
33
+                timeoutMsg: 'No breakout room added for p1'
34
+            });
35
+
36
+
37
+        // second participant should also see one breakout room
38
+        await p2.driver.waitUntil(
39
+            async () => await p2.getBreakoutRooms().getRoomsCount() === 1, {
40
+                timeout: 2000,
41
+                timeoutMsg: 'No breakout room seen by p2'
42
+            });
43
+    });
44
+
45
+    it('join breakout room', async () => {
46
+        const { p1, p2 } = ctx;
47
+        const p1BreakoutRooms = p1.getBreakoutRooms();
48
+
49
+        // there should be one breakout room
50
+        await p1.driver.waitUntil(
51
+            async () => await p1BreakoutRooms.getRoomsCount() === 1, {
52
+                timeout: 1000,
53
+                timeoutMsg: 'No breakout room seen by p1'
54
+            });
55
+
56
+        const roomsList = await p1BreakoutRooms.getRooms();
57
+
58
+        expect(roomsList.length).toBe(1);
59
+
60
+        // join the room
61
+        await roomsList[0].joinRoom();
62
+
63
+        // the participant should see the main room as the only breakout room
64
+        await p1.driver.waitUntil(
65
+            async () => {
66
+                if (await p1BreakoutRooms.getRoomsCount() !== 1) {
67
+                    return false;
68
+                }
69
+
70
+                const list = await p1BreakoutRooms.getRooms();
71
+
72
+                if (list?.length !== 1) {
73
+                    return false;
74
+                }
75
+
76
+                return list[0].name === MAIN_ROOM_NAME;
77
+            }, {
78
+                timeout: 2000,
79
+                timeoutMsg: 'P1 did not join breakout room'
80
+            });
81
+
82
+        // the second participant should see one participant in the breakout room
83
+        await p2.driver.waitUntil(
84
+            async () => {
85
+                const list = await p2.getBreakoutRooms().getRooms();
86
+
87
+                if (list?.length !== 1) {
88
+                    return false;
89
+                }
90
+
91
+                return list[0].participantCount === 1;
92
+            }, {
93
+                timeout: 2000,
94
+                timeoutMsg: 'P2 is not seeing p1 in the breakout room'
95
+            });
96
+    });
97
+
98
+    it('leave breakout room', async () => {
99
+        const { p1, p2 } = ctx;
100
+        const p1BreakoutRooms = p1.getBreakoutRooms();
101
+
102
+        // leave room
103
+        await p1BreakoutRooms.leaveBreakoutRoom();
104
+
105
+        // there should be one breakout room and that should not be the main room
106
+        await p1.driver.waitUntil(
107
+            async () => {
108
+                if (await p1BreakoutRooms.getRoomsCount() !== 1) {
109
+                    return false;
110
+                }
111
+
112
+                const list = await p1BreakoutRooms.getRooms();
113
+
114
+                if (list?.length !== 1) {
115
+                    return false;
116
+                }
117
+
118
+                return list[0].name !== MAIN_ROOM_NAME;
119
+            }, {
120
+                timeout: 2000,
121
+                timeoutMsg: 'P1 did not leave breakout room'
122
+            });
123
+
124
+        // the second participant should see no participants in the breakout room
125
+        await p2.driver.waitUntil(
126
+            async () => {
127
+                const list = await p2.getBreakoutRooms().getRooms();
128
+
129
+                if (list?.length !== 1) {
130
+                    return false;
131
+                }
132
+
133
+                return list[0].participantCount === 0;
134
+            }, {
135
+                timeout: 2000,
136
+                timeoutMsg: 'P2 is seeing p1 in the breakout room'
137
+            });
138
+    });
139
+
140
+    it('remove breakout room', async () => {
141
+        const { p1, p2 } = ctx;
142
+        const p1BreakoutRooms = p1.getBreakoutRooms();
143
+
144
+        // remove the room
145
+        await (await p1BreakoutRooms.getRooms())[0].removeRoom();
146
+
147
+        // there should be no breakout rooms
148
+        await p1.driver.waitUntil(
149
+            async () => await p1BreakoutRooms.getRoomsCount() === 0, {
150
+                timeout: 2000,
151
+                timeoutMsg: 'Breakout room was not removed for p1'
152
+            });
153
+
154
+        // the second participant should also see no breakout rooms
155
+        await p2.driver.waitUntil(
156
+            async () => await p2.getBreakoutRooms().getRoomsCount() === 0, {
157
+                timeout: 2000,
158
+                timeoutMsg: 'Breakout room was not removed for p2'
159
+            });
160
+    });
161
+
162
+    it('auto assign', async () => {
163
+        await ensureThreeParticipants(ctx);
164
+        const { p1, p2 } = ctx;
165
+        const p1BreakoutRooms = p1.getBreakoutRooms();
166
+
167
+        // create two rooms
168
+        await p1BreakoutRooms.addBreakoutRoom();
169
+        await p1BreakoutRooms.addBreakoutRoom();
170
+
171
+        // there should be two breakout rooms
172
+        await p1.driver.waitUntil(
173
+            async () => await p1BreakoutRooms.getRoomsCount() === 2, {
174
+                timeout: 2000,
175
+                timeoutMsg: 'Breakout room was not created by p1'
176
+            });
177
+
178
+        // auto assign participants to rooms
179
+        await p1BreakoutRooms.autoAssignToBreakoutRooms();
180
+
181
+        // each room should have one participant
182
+        await p1.driver.waitUntil(
183
+            async () => {
184
+                if (await p1BreakoutRooms.getRoomsCount() !== 2) {
185
+                    return false;
186
+                }
187
+
188
+                const list = await p1BreakoutRooms.getRooms();
189
+
190
+                if (list?.length !== 2) {
191
+                    return false;
192
+                }
193
+
194
+                return list[0].participantCount === 1 && list[1].participantCount === 1;
195
+            }, {
196
+                timeout: 2000,
197
+                timeoutMsg: 'P1 did not auto assigned participants to breakout rooms'
198
+            });
199
+
200
+        // the second participant should see one participant in the main room
201
+        const p2BreakoutRooms = p2.getBreakoutRooms();
202
+
203
+        await p2.driver.waitUntil(
204
+            async () => {
205
+                if (await p2BreakoutRooms.getRoomsCount() !== 2) {
206
+                    return false;
207
+                }
208
+
209
+                const list = await p2BreakoutRooms.getRooms();
210
+
211
+                if (list?.length !== 2) {
212
+                    return false;
213
+                }
214
+
215
+                return list[0].participantCount === 1 && list[1].participantCount === 1
216
+                    && (list[0].name === MAIN_ROOM_NAME || list[1].name === MAIN_ROOM_NAME);
217
+            }, {
218
+                timeout: 2000,
219
+                timeoutMsg: 'P2 is not seeing p1 in the main room'
220
+            });
221
+    });
222
+
223
+    it('close breakout room', async () => {
224
+        const { p1, p2, p3 } = ctx;
225
+        const p1BreakoutRooms = p1.getBreakoutRooms();
226
+
227
+        // there should be two non-empty breakout rooms
228
+        await p1.driver.waitUntil(
229
+            async () => {
230
+                if (await p1BreakoutRooms.getRoomsCount() !== 2) {
231
+                    return false;
232
+                }
233
+
234
+                const list = await p1BreakoutRooms.getRooms();
235
+
236
+                if (list?.length !== 2) {
237
+                    return false;
238
+                }
239
+
240
+                return list[0].participantCount === 1 && list[1].participantCount === 1;
241
+            }, {
242
+                timeout: 2000,
243
+                timeoutMsg: 'P1 is not seeing two breakout rooms'
244
+            });
245
+
246
+        // close the first room
247
+        await (await p1BreakoutRooms.getRooms())[0].closeRoom();
248
+
249
+        // there should be two rooms and first one should be empty
250
+        await p1.driver.waitUntil(
251
+            async () => {
252
+                if (await p1BreakoutRooms.getRoomsCount() !== 2) {
253
+                    return false;
254
+                }
255
+
256
+                const list = await p1BreakoutRooms.getRooms();
257
+
258
+                if (list?.length !== 2) {
259
+                    return false;
260
+                }
261
+
262
+                return list[0].participantCount === 0 || list[1].participantCount === 0;
263
+            }, {
264
+                timeout: 2000,
265
+                timeoutMsg: 'P1 is not seeing an empty breakout room'
266
+            });
267
+
268
+        // there should be two participants in the main room, either p2 or p3 got moved to the main room
269
+        const checkParticipants = async (p: Participant) => {
270
+            await p.driver.waitUntil(
271
+                async () => {
272
+                    const isInBreakoutRoom = await p.isInBreakoutRoom();
273
+                    const breakoutRooms = p.getBreakoutRooms();
274
+
275
+                    if (isInBreakoutRoom) {
276
+                        if (await breakoutRooms.getRoomsCount() !== 2) {
277
+                            return false;
278
+                        }
279
+
280
+                        const list = await breakoutRooms.getRooms();
281
+
282
+                        if (list?.length !== 2) {
283
+                            return false;
284
+                        }
285
+
286
+                        return list.every(r => { // eslint-disable-line arrow-body-style
287
+                            return r.name === MAIN_ROOM_NAME ? r.participantCount === 2 : r.participantCount === 0;
288
+                        });
289
+                    }
290
+
291
+                    if (await breakoutRooms.getRoomsCount() !== 2) {
292
+                        return false;
293
+                    }
294
+
295
+                    const list = await breakoutRooms.getRooms();
296
+
297
+                    if (list?.length !== 2) {
298
+                        return false;
299
+                    }
300
+
301
+                    return list[0].participantCount + list[1].participantCount === 1;
302
+                }, {
303
+                    timeout: 2000,
304
+                    timeoutMsg: `${p.name} is not seeing an empty breakout room and one with one participant`
305
+                });
306
+        };
307
+
308
+        await checkParticipants(p2);
309
+        await checkParticipants(p3);
310
+    });
311
+
312
+    it('send participants to breakout room', async () => {
313
+        await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]);
314
+
315
+        // because the participants rejoin so fast, the meeting is not properly ended,
316
+        // so the previous breakout rooms would still be there.
317
+        // To avoid this issue we use a different meeting
318
+        ctx.roomName += '-breakout-rooms';
319
+
320
+        await ensureTwoParticipants(ctx);
321
+        const { p1, p2 } = ctx;
322
+        const p1BreakoutRooms = p1.getBreakoutRooms();
323
+
324
+        // there should be no breakout rooms
325
+        expect(await p1BreakoutRooms.getRoomsCount()).toBe(0);
326
+
327
+        // add one breakout room
328
+        await p1BreakoutRooms.addBreakoutRoom();
329
+
330
+        // there should be one empty room
331
+        await p1.driver.waitUntil(
332
+            async () => await p1BreakoutRooms.getRoomsCount() === 1
333
+                && (await p1BreakoutRooms.getRooms())[0].participantCount === 0, {
334
+                timeout: 2000,
335
+                timeoutMsg: 'No breakout room added for p1'
336
+            });
337
+
338
+        // send the second participant to the first breakout room
339
+        await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, (await p1BreakoutRooms.getRooms())[0].name);
340
+
341
+        // there should be one room with one participant
342
+        await p1.driver.waitUntil(
343
+            async () => {
344
+                const list = await p1BreakoutRooms.getRooms();
345
+
346
+                if (list?.length !== 1) {
347
+                    return false;
348
+                }
349
+
350
+                return list[0].participantCount === 1;
351
+            }, {
352
+                timeout: 2000,
353
+                timeoutMsg: 'P1 is not seeing p2 in the breakout room'
354
+            });
355
+    });
356
+
357
+    it('collapse breakout room', async () => {
358
+        const { p1 } = ctx;
359
+        const p1BreakoutRooms = p1.getBreakoutRooms();
360
+
361
+        // there should be one breakout room with one participant
362
+        await p1.driver.waitUntil(
363
+            async () => {
364
+                const list = await p1BreakoutRooms.getRooms();
365
+
366
+                if (list?.length !== 1) {
367
+                    return false;
368
+                }
369
+
370
+                return list[0].participantCount === 1;
371
+            }, {
372
+                timeout: 2000,
373
+                timeoutMsg: 'P1 is not seeing p2 in the breakout room'
374
+            });
375
+
376
+        // get id of the breakout room participant
377
+        const breakoutList = p1.driver.$(`#${BREAKOUT_ROOMS_LIST_ID}`);
378
+        const breakoutRoomItem = await breakoutList.$$(`.${LIST_ITEM_CONTAINER}`).find(
379
+            async el => {
380
+                const id = await el.getAttribute('id');
381
+
382
+                return id !== '' && id !== null;
383
+            }) as ChainablePromiseElement;
384
+
385
+        const pId = await breakoutRoomItem.getAttribute('id');
386
+        const breakoutParticipant = p1.driver.$(`//div[@id="${pId}"]`);
387
+
388
+        expect(await breakoutParticipant.isDisplayed()).toBe(true);
389
+
390
+        // collapse the first
391
+        await (await p1BreakoutRooms.getRooms())[0].collapse();
392
+
393
+        // the participant should not be visible
394
+        expect(await breakoutParticipant.isDisplayed()).toBe(false);
395
+
396
+        // the collapsed room should still have one participant
397
+        expect((await p1BreakoutRooms.getRooms())[0].participantCount).toBe(1);
398
+    });
399
+
400
+    it('rename breakout room', async () => {
401
+        const myNewRoomName = `breakout-${crypto.randomUUID()}`;
402
+        const { p1, p2 } = ctx;
403
+        const p1BreakoutRooms = p1.getBreakoutRooms();
404
+
405
+        // let's rename breakout room and see it in local and remote
406
+        await (await p1BreakoutRooms.getRooms())[0].renameRoom(myNewRoomName);
407
+
408
+        await p1.driver.waitUntil(
409
+            async () => {
410
+                const list = await p1BreakoutRooms.getRooms();
411
+
412
+                if (list?.length !== 1) {
413
+                    return false;
414
+                }
415
+
416
+                return list[0].name === myNewRoomName;
417
+            }, {
418
+                timeout: 2000,
419
+                timeoutMsg: 'The breakout room was not renamed for p1'
420
+            });
421
+
422
+        await checkSubject(p2, myNewRoomName);
423
+
424
+        // leave room
425
+        await p2.getBreakoutRooms().leaveBreakoutRoom();
426
+
427
+        // there should be one empty room
428
+        await p1.driver.waitUntil(
429
+            async () => {
430
+                const list = await p1BreakoutRooms.getRooms();
431
+
432
+                if (list?.length !== 1) {
433
+                    return false;
434
+                }
435
+
436
+                return list[0].participantCount === 0;
437
+            }, {
438
+                timeout: 2000,
439
+                timeoutMsg: 'The breakout room was not renamed for p1'
440
+            });
441
+
442
+        expect((await p2.getBreakoutRooms().getRooms())[0].name).toBe(myNewRoomName);
443
+
444
+        // send the second participant to the first breakout room
445
+        await p1BreakoutRooms.sendParticipantToBreakoutRoom(p2, myNewRoomName);
446
+
447
+        // there should be one room with one participant
448
+        await p1.driver.waitUntil(
449
+            async () => {
450
+                const list = await p1BreakoutRooms.getRooms();
451
+
452
+                if (list?.length !== 1) {
453
+                    return false;
454
+                }
455
+
456
+                return list[0].participantCount === 1;
457
+            }, {
458
+                timeout: 2000,
459
+                timeoutMsg: 'The breakout room was not rename for p1'
460
+            });
461
+
462
+        await checkSubject(p2, myNewRoomName);
463
+    });
464
+});

+ 1
- 4
tests/wdio.conf.ts Visa fil

@@ -178,9 +178,6 @@ export const config: WebdriverIO.MultiremoteConfig = {
178 178
                 return;
179 179
             }
180 180
 
181
-            // if (process.env.GRID_HOST_URL) {
182
-            // TODO: make sure we use uploadFile only with chrome (it does not work with FF),
183
-            // we need to test it with the grid and FF, does it work there
184 181
             const rpath = await bInstance.uploadFile('tests/resources/iframeAPITest.html');
185 182
 
186 183
             // @ts-ignore
@@ -199,7 +196,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
199 196
     after() {
200 197
         const { ctx }: any = global;
201 198
 
202
-        if (ctx.webhooksProxy) {
199
+        if (ctx?.webhooksProxy) {
203 200
             ctx.webhooksProxy.disconnect();
204 201
         }
205 202
     },

+ 2
- 2
tests/wdio.dev.conf.ts Visa fil

@@ -1,11 +1,11 @@
1 1
 // wdio.dev.conf.ts
2 2
 // extends the main configuration file for the development environment (make dev)
3 3
 // it will connect to the webpack-dev-server running locally on port 8080
4
-import { deepmerge } from 'deepmerge-ts';
4
+import { merge } from 'lodash-es';
5 5
 
6 6
 // @ts-ignore
7 7
 import { config as defaultConfig } from './wdio.conf.ts';
8 8
 
9
-export const config = deepmerge(defaultConfig, {
9
+export const config = merge(defaultConfig, {
10 10
     baseUrl: 'https://127.0.0.1:8080/torture'
11 11
 }, { clone: false });

+ 29
- 4
tests/wdio.firefox.conf.ts Visa fil

@@ -19,11 +19,12 @@ if (process.env.HEADLESS === 'true') {
19 19
     ffArgs.push('--headless');
20 20
 }
21 21
 
22
+const ffExcludes = [
23
+    'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile)
24
+    'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input
25
+];
26
+
22 27
 export const config = merge(defaultConfig, {
23
-    exclude: [
24
-        'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile)
25
-        'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input
26
-    ],
27 28
     capabilities: {
28 29
         participant1: {
29 30
             capabilities: {
@@ -34,6 +35,30 @@ export const config = merge(defaultConfig, {
34 35
                 },
35 36
                 acceptInsecureCerts: process.env.ALLOW_INSECURE_CERTS === 'true'
36 37
             }
38
+        },
39
+        participant2: {
40
+            capabilities: {
41
+                'wdio:exclude': [
42
+                    ...defaultConfig.capabilities.participant2.capabilities['wdio:exclude'],
43
+                    ...ffExcludes
44
+                ]
45
+            }
46
+        },
47
+        participant3: {
48
+            capabilities: {
49
+                'wdio:exclude': [
50
+                    ...defaultConfig.capabilities.participant3.capabilities['wdio:exclude'],
51
+                    ...ffExcludes
52
+                ]
53
+            }
54
+        },
55
+        participant4: {
56
+            capabilities: {
57
+                'wdio:exclude': [
58
+                    ...defaultConfig.capabilities.participant4.capabilities['wdio:exclude'],
59
+                    ...ffExcludes
60
+                ]
61
+            }
37 62
         }
38 63
     }
39 64
 }, { clone: false });

+ 2
- 2
tests/wdio.grid.conf.ts Visa fil

@@ -1,6 +1,6 @@
1 1
 // wdio.grid.conf.ts
2 2
 // extends the main configuration file to add the selenium grid address
3
-import { deepmerge } from 'deepmerge-ts';
3
+import { merge } from 'lodash-es';
4 4
 import { URL } from 'url';
5 5
 
6 6
 // @ts-ignore
@@ -9,7 +9,7 @@ import { config as defaultConfig } from './wdio.conf.ts';
9 9
 const gridUrl = new URL(process.env.GRID_HOST_URL as string);
10 10
 const protocol = gridUrl.protocol.replace(':', '');
11 11
 
12
-export const config = deepmerge(defaultConfig, {
12
+export const config = merge(defaultConfig, {
13 13
     protocol,
14 14
     hostname: gridUrl.hostname,
15 15
     port: gridUrl.port ? parseInt(gridUrl.port, 10) // Convert port to number

Laddar…
Avbryt
Spara