Selaa lähdekoodia

feat(tests): Adds follow-me and invite dialog test. (#15476)

* feat(tests): Adds follow-me test.

* feat(tests): Adds invite dialog test.

* squash: fix lint.
factor2
Дамян Минков 9 kuukautta sitten
vanhempi
commit
c21f84c15a
No account linked to committer's email address

+ 3
- 0
tests/env.example Näytä tiedosto

@@ -30,3 +30,6 @@
30 30
 #WEBHOOKS_PROXY_URL=
31 31
 # A shared secret to authenticate the webhook proxy connection
32 32
 #WEBHOOKS_PROXY_SHARED_SECRET=
33
+
34
+# A rest URL to be used by dial-in tests to invite jigasi to the conference
35
+#DIAL_IN_REST_URL=

+ 25
- 6
tests/helpers/Participant.ts Näytä tiedosto

@@ -238,11 +238,22 @@ export class Participant {
238 238
             async () => await this.driver.execute(() => document.readyState === 'complete'),
239 239
             {
240 240
                 timeout: 30_000, // 30 seconds
241
-                timeoutMsg: 'Timeout waiting for Page Load Request to complete.'
241
+                timeoutMsg: `Timeout waiting for Page Load Request to complete for ${this.name}.`
242 242
             }
243 243
         );
244 244
     }
245 245
 
246
+    /**
247
+     * Waits for the tile view to display.
248
+     */
249
+    async waitForTileViewDisplay(reverse = false) {
250
+        await this.driver.$('//div[@id="videoconference_page" and contains(@class, "tile-view")]').waitForDisplayed({
251
+            reverse,
252
+            timeout: 10_000,
253
+            timeoutMsg: `Tile view did not display in 10s for ${this.name}`
254
+        });
255
+    }
256
+
246 257
     /**
247 258
      * Checks if the participant is in the meeting.
248 259
      */
@@ -284,7 +295,7 @@ export class Participant {
284 295
             () => this.isInMuc(),
285 296
             {
286 297
                 timeout: 10_000, // 10 seconds
287
-                timeoutMsg: 'Timeout waiting to join muc.'
298
+                timeoutMsg: `Timeout waiting to join muc for ${this.name}`
288 299
             }
289 300
         );
290 301
     }
@@ -300,7 +311,7 @@ export class Participant {
300 311
         return driver.waitUntil(async () =>
301 312
             await driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
302 313
             timeout: 15_000,
303
-            timeoutMsg: 'expected ICE to be connected for 15s'
314
+            timeoutMsg: `expected ICE to be connected for 15s for ${this.name}`
304 315
         });
305 316
     }
306 317
 
@@ -309,7 +320,8 @@ export class Participant {
309 320
      *
310 321
      * @returns {Promise<void>}
311 322
      */
312
-    async waitForSendReceiveData(timeout = 15_000, msg = 'expected to receive/send data in 15s'): Promise<void> {
323
+    async waitForSendReceiveData(
324
+            timeout = 15_000, msg = `expected to receive/send data in 15s for ${this.name}`): Promise<void> {
313 325
         const driver = this.driver;
314 326
 
315 327
         return driver.waitUntil(async () =>
@@ -340,7 +352,7 @@ export class Participant {
340 352
         return driver.waitUntil(async () =>
341 353
             await driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
342 354
             timeout: 15_000,
343
-            timeoutMsg: 'expected remote streams in 15s'
355
+            timeoutMsg: `expected remote streams in 15s for ${this.name}`
344 356
         });
345 357
     }
346 358
 
@@ -357,7 +369,7 @@ export class Participant {
357 369
         return driver.waitUntil(async () =>
358 370
             await driver.execute(count => APP.conference.listMembers().length === count, number), {
359 371
             timeout: 15_000,
360
-            timeoutMsg: msg || `not the expected participants ${number} in 15s`
372
+            timeoutMsg: msg || `not the expected participants ${number} in 15s for ${this.name}`
361 373
         });
362 374
     }
363 375
 
@@ -531,6 +543,13 @@ export class Participant {
531 543
         return await this.driver.execute(() => APP.UI.getLargeVideoID());
532 544
     }
533 545
 
546
+    /**
547
+     * Returns the source of the large video currently shown.
548
+     */
549
+    async getLargeVideoId() {
550
+        return await this.driver.execute('return document.getElementById("largeVideo").srcObject.id');
551
+    }
552
+
534 553
     /**
535 554
      * Makes sure that the avatar is displayed in the local thumbnail and that the video is not displayed.
536 555
      * There are 3 options for avatar:

+ 48
- 0
tests/pageobjects/Filmstrip.ts Näytä tiedosto

@@ -53,6 +53,28 @@ export default class Filmstrip extends BasePageObject {
53 53
         return await remoteDisplayName.getText();
54 54
     }
55 55
 
56
+    /**
57
+     * Returns the remote video id of a participant with endpointID.
58
+     * @param endpointId
59
+     */
60
+    async getRemoteVideoId(endpointId: string) {
61
+        const remoteDisplayName = this.participant.driver.$(`span[id="participant_${endpointId}"]`);
62
+
63
+        await remoteDisplayName.moveTo();
64
+
65
+        return await this.participant.driver.execute(eId =>
66
+            document.evaluate(`//span[@id="participant_${eId}"]//video`,
67
+                document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue?.srcObject?.id, endpointId);
68
+    }
69
+
70
+    /**
71
+     * Returns the local video id.
72
+     */
73
+    async getLocalVideoId() {
74
+        return await this.participant.driver.execute(
75
+            'return document.getElementById("localVideo_container").srcObject.id');
76
+    }
77
+
56 78
     /**
57 79
      * Pins a participant by clicking on their thumbnail.
58 80
      * @param participant The participant.
@@ -162,4 +184,30 @@ export default class Filmstrip extends BasePageObject {
162 184
             timeoutMsg: `Local video thumbnail is${hidden ? '' : ' not'} displayed for ${this.participant.name}`
163 185
         });
164 186
     }
187
+
188
+    /**
189
+     * Toggles the filmstrip.
190
+     */
191
+    async toggle() {
192
+        const toggleButton = this.participant.driver.$('#toggleFilmstripButton');
193
+
194
+        await toggleButton.moveTo();
195
+        await toggleButton.waitForDisplayed();
196
+        await toggleButton.click();
197
+    }
198
+
199
+    /**
200
+     * Asserts that the remote videos are hidden or not.
201
+     * @param reverse
202
+     */
203
+    async assertRemoteVideosHidden(reverse = false) {
204
+        await this.participant.driver.waitUntil(
205
+            async () =>
206
+                await this.participant.driver.$$('//div[@id="remoteVideos" and contains(@class, "hidden")]').length > 0,
207
+            {
208
+                timeout: 10_000, // 10 seconds
209
+                timeoutMsg: `Timeout waiting fore remote videos to be hidden: ${!reverse}.`
210
+            }
211
+        );
212
+    }
165 213
 }

+ 60
- 2
tests/pageobjects/InviteDialog.ts Näytä tiedosto

@@ -1,7 +1,10 @@
1 1
 import BaseDialog from './BaseDialog';
2 2
 
3 3
 const CONFERENCE_ID = 'conference-id';
4
+const CONFERENCE_URL = 'invite-more-dialog-conference-url';
4 5
 const DIALOG_CONTAINER = 'invite-more-dialog';
6
+const MORE_NUMBERS = 'more-numbers';
7
+const PHONE_NUMBER = 'phone-number';
5 8
 
6 9
 /**
7 10
  * Represents the invite dialog in a particular participant.
@@ -31,7 +34,18 @@ export default class InviteDialog extends BaseDialog {
31 34
     async getPinNumber() {
32 35
         await this.open();
33 36
 
34
-        const elem = this.participant.driver.$(`.${CONFERENCE_ID}`);
37
+        return (await this.getValueAfterColon(CONFERENCE_ID)).replace(/[# ]/g, '');
38
+    }
39
+
40
+    /**
41
+     * Private helper to get values after colons. The invite dialog lists conference specific information
42
+     * after a label, followed by a colon.
43
+     *
44
+     * @param className
45
+     * @private
46
+     */
47
+    private async getValueAfterColon(className: string) {
48
+        const elem = this.participant.driver.$(`.${className}`);
35 49
 
36 50
         await elem.waitForExist({ timeout: 5000 });
37 51
 
@@ -39,6 +53,50 @@ export default class InviteDialog extends BaseDialog {
39 53
 
40 54
         this.participant.log(`Extracted text in invite dialog: ${fullText}`);
41 55
 
42
-        return fullText.split(':')[1].trim().replace(/[# ]/g, '');
56
+        return fullText.split(':')[1].trim();
57
+    }
58
+
59
+    /**
60
+     * Returns the meeting url displayed in the dialog.
61
+     */
62
+    async getMeetingURL() {
63
+        const elem = this.participant.driver.$(`.${CONFERENCE_URL}`);
64
+
65
+        await elem.waitForExist();
66
+
67
+        return (await elem.getText())?.trim();
68
+    }
69
+
70
+    /**
71
+     * Waits for the dialog to be open or closed.
72
+     * @param reverse
73
+     */
74
+    async waitTillOpen(reverse = false) {
75
+        await this.participant.driver.waitUntil(
76
+            /* eslint-disable no-extra-parens */
77
+            async () => (reverse ? !await this.isOpen() : await this.isOpen()),
78
+            {
79
+                timeout: 2_000,
80
+                timeoutMsg: `invite dialog did not ${reverse ? 'close' : 'open'}`
81
+            }
82
+        );
83
+    }
84
+
85
+    /**
86
+     * Gets the string that contains the dial in number for the current conference.
87
+     */
88
+    async getDialInNumber() {
89
+        return await this.getValueAfterColon(PHONE_NUMBER);
90
+    }
91
+
92
+    /**
93
+     * Clicks the link to open a page to show all available dial in numbers.
94
+     */
95
+    async openDialInNumbersPage() {
96
+        const moreNumbers = this.participant.driver.$(`.${MORE_NUMBERS}`);
97
+
98
+        await moreNumbers.waitForExist();
99
+        await moreNumbers.waitForClickable();
100
+        await moreNumbers.click();
43 101
     }
44 102
 }

+ 48
- 4
tests/pageobjects/SettingsDialog.ts Näytä tiedosto

@@ -1,8 +1,10 @@
1 1
 import BaseDialog from './BaseDialog';
2 2
 
3 3
 const EMAIL_FIELD = '#setEmail';
4
+const FOLLOW_ME_CHECKBOX = '//input[@name="follow-me"]';
4 5
 const HIDE_SELF_VIEW_CHECKBOX = '//input[@name="hide-self-view"]';
5 6
 const SETTINGS_DIALOG_CONTENT = '.settings-pane';
7
+const X_PATH_MODERATOR_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="Moderator"]';
6 8
 const X_PATH_MORE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="General"]';
7 9
 const X_PATH_PROFILE_TAB = '//div[contains(@class, "settings-dialog")]//*[text()="Profile"]';
8 10
 
@@ -37,12 +39,19 @@ export default class SettingsDialog extends BaseDialog {
37 39
     }
38 40
 
39 41
     /**
40
-     * Selects the Profile tab to be displayed.
42
+     * Selects the More tab to be displayed.
41 43
      */
42 44
     async openMoreTab() {
43 45
         await this.openTab(X_PATH_MORE_TAB);
44 46
     }
45 47
 
48
+    /**
49
+     * Selects the moderator tab to be displayed.
50
+     */
51
+    async openModeratorTab() {
52
+        await this.openTab(X_PATH_MODERATOR_TAB);
53
+    }
54
+
46 55
     /**
47 56
      * Enters the passed in email into the email field.
48 57
      * @param email
@@ -75,14 +84,49 @@ export default class SettingsDialog extends BaseDialog {
75 84
     async setHideSelfView(hideSelfView: boolean) {
76 85
         await this.openMoreTab();
77 86
 
78
-        const checkbox = this.participant.driver.$(HIDE_SELF_VIEW_CHECKBOX);
87
+        await this.setCheckbox(HIDE_SELF_VIEW_CHECKBOX, hideSelfView);
88
+    }
89
+
90
+    /**
91
+     * Sets the follow me feature to enabled/disabled.
92
+     * @param enable
93
+     */
94
+    async setFollowMe(enable: boolean) {
95
+        await this.openModeratorTab();
96
+
97
+        await this.setCheckbox(FOLLOW_ME_CHECKBOX, enable);
98
+    }
99
+
100
+    /**
101
+     * Returns true if the follow me checkbox is displayed in the settings dialog.
102
+     */
103
+    async isFollowMeDisplayed() {
104
+        const elem = this.participant.driver.$(X_PATH_MODERATOR_TAB);
105
+
106
+        if (!await elem.isExisting()) {
107
+            return false;
108
+        }
109
+
110
+        await this.openModeratorTab();
111
+
112
+        return await this.participant.driver.$$(FOLLOW_ME_CHECKBOX).length > 0;
113
+    }
114
+
115
+    /**
116
+     * Sets the state of a checkbox.
117
+     * @param selector
118
+     * @param enable
119
+     * @private
120
+     */
121
+    private async setCheckbox(selector: string, enable: boolean) {
122
+        const checkbox = this.participant.driver.$(selector);
79 123
 
80 124
         await checkbox.waitForExist();
81 125
 
82
-        if (hideSelfView !== await checkbox.isSelected()) {
126
+        if (enable !== await checkbox.isSelected()) {
83 127
             // we show a div with svg and text after the input and those elements grab the click
84 128
             // so we need to click on the parent element
85
-            await this.participant.driver.$(`${HIDE_SELF_VIEW_CHECKBOX}//ancestor::div[1]`).click();
129
+            await this.participant.driver.$(`${selector}//ancestor::div[1]`).click();
86 130
         }
87 131
     }
88 132
 }

+ 0
- 1
tests/pageobjects/Toolbar.ts Näytä tiedosto

@@ -113,7 +113,6 @@ export default class Toolbar extends BasePageObject {
113 113
         await this.getButton(CLOSE_PARTICIPANTS_PANE).click();
114 114
     }
115 115
 
116
-
117 116
     /**
118 117
      * Clicks Participants pane button.
119 118
      *

+ 2
- 3
tests/specs/2way/fakeDialInAudio.spec.ts Näytä tiedosto

@@ -1,7 +1,7 @@
1 1
 import process from 'node:process';
2 2
 
3 3
 import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
4
-import { cleanup, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
4
+import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
5 5
 
6 6
 describe('Fake Dial-In - ', () => {
7 7
     it('join participant', async () => {
@@ -17,8 +17,7 @@ describe('Fake Dial-In - ', () => {
17 17
         await ensureOneParticipant(ctx);
18 18
 
19 19
         // check dial-in is enabled, so skip
20
-        if (await ctx.p1.driver.execute(() => Boolean(
21
-            config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts?.muc))) {
20
+        if (await isDialInEnabled(ctx.p1)) {
22 21
             ctx.skipSuiteTests = true;
23 22
         }
24 23
     });

+ 93
- 0
tests/specs/3way/followMe.spec.ts Näytä tiedosto

@@ -0,0 +1,93 @@
1
+import { ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants';
2
+
3
+describe('Follow Me - ', () => {
4
+    it('joining the meeting', async () => {
5
+        await ensureTwoParticipants(ctx);
6
+
7
+        const { p1 } = ctx;
8
+
9
+        await p1.getToolbar().clickSettingsButton();
10
+
11
+        const settings = p1.getSettingsDialog();
12
+
13
+        await settings.waitForDisplay();
14
+        await settings.setFollowMe(true);
15
+        await settings.submit();
16
+    });
17
+
18
+    it('follow me checkbox visible only for moderators', async () => {
19
+        const { p2 } = ctx;
20
+
21
+        if (!await p2.isModerator()) {
22
+            await p2.getToolbar().clickSettingsButton();
23
+
24
+            const settings = p2.getSettingsDialog();
25
+
26
+            await settings.waitForDisplay();
27
+            expect(await settings.isFollowMeDisplayed()).toBe(false);
28
+
29
+            await settings.clickCloseButton();
30
+        }
31
+    });
32
+
33
+    it('filmstrip commands', async () => {
34
+        const { p1, p2 } = ctx;
35
+
36
+        const p1Filmstrip = p1.getFilmstrip();
37
+        const p2Filmstrip = p2.getFilmstrip();
38
+
39
+        await p1Filmstrip.toggle();
40
+
41
+        await p1Filmstrip.assertRemoteVideosHidden();
42
+        await p2Filmstrip.assertRemoteVideosHidden();
43
+    });
44
+
45
+    it('tile view', async () => {
46
+        await ensureThreeParticipants(ctx);
47
+
48
+        const { p1, p2, p3 } = ctx;
49
+
50
+        await p1.waitForTileViewDisplay();
51
+
52
+        await p1.getToolbar().clickExitTileViewButton();
53
+
54
+        await Promise.all([
55
+            p1.waitForTileViewDisplay(true),
56
+            p2.waitForTileViewDisplay(true),
57
+            p3.waitForTileViewDisplay(true)
58
+        ]);
59
+
60
+        await p1.getToolbar().clickEnterTileViewButton();
61
+
62
+        await Promise.all([
63
+            p1.waitForTileViewDisplay(),
64
+            p2.waitForTileViewDisplay(),
65
+            p3.waitForTileViewDisplay()
66
+        ]);
67
+    });
68
+
69
+    it('next on stage', async () => {
70
+        const { p1, p2, p3 } = ctx;
71
+
72
+        await p1.getFilmstrip().pinParticipant(p2);
73
+
74
+        const p2Filmstrip = p2.getFilmstrip();
75
+        const localVideoId = await p2Filmstrip.getLocalVideoId();
76
+
77
+        await p2.driver.waitUntil(
78
+            async () => await localVideoId === await p2.getLargeVideoId(),
79
+            {
80
+                timeout: 5_000,
81
+                timeoutMsg: 'The pinned participant is not displayed on stage for p2'
82
+            });
83
+
84
+        const p2VideoIdOnp3 = await p3.getFilmstrip().getRemoteVideoId(await p2.getEndpointId());
85
+
86
+        await p3.driver.waitUntil(
87
+            async () => p2VideoIdOnp3 === await p3.getLargeVideoId(),
88
+            {
89
+                timeout: 5_000,
90
+                timeoutMsg: 'The pinned participant is not displayed on stage for p3'
91
+            });
92
+    });
93
+});

+ 2
- 3
tests/specs/alone/dialInAudio.spec.ts Näytä tiedosto

@@ -2,7 +2,7 @@ import https from 'node:https';
2 2
 import process from 'node:process';
3 3
 
4 4
 import { ensureOneParticipant } from '../../helpers/participants';
5
-import { cleanup, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
5
+import { cleanup, isDialInEnabled, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
6 6
 
7 7
 describe('Dial-In - ', () => {
8 8
     it('join participant', async () => {
@@ -16,8 +16,7 @@ describe('Dial-In - ', () => {
16 16
         await ensureOneParticipant(ctx);
17 17
 
18 18
         // check dial-in is enabled
19
-        if (!await ctx.p1.driver.execute(() => Boolean(
20
-            config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts?.muc))) {
19
+        if (!await isDialInEnabled(ctx.p1)) {
21 20
             ctx.skipSuiteTests = true;
22 21
         }
23 22
     });

+ 87
- 0
tests/specs/alone/invite.spec.ts Näytä tiedosto

@@ -0,0 +1,87 @@
1
+import { ensureOneParticipant } from '../../helpers/participants';
2
+import { isDialInEnabled } from '../helpers/DialIn';
3
+
4
+describe('Invite - ', () => {
5
+    it('join participant', async () => {
6
+        await ensureOneParticipant(ctx);
7
+    });
8
+
9
+    it('url displayed', async () => {
10
+        const { p1 } = ctx;
11
+        const inviteDialog = p1.getInviteDialog();
12
+
13
+        await inviteDialog.open();
14
+        await inviteDialog.waitTillOpen();
15
+
16
+        const driverUrl = await p1.driver.getUrl();
17
+
18
+        expect(driverUrl.includes(await inviteDialog.getMeetingURL())).toBe(true);
19
+
20
+        await inviteDialog.clickCloseButton();
21
+
22
+        await inviteDialog.waitTillOpen(true);
23
+    });
24
+
25
+    it('dial-in displayed', async () => {
26
+        const { p1 } = ctx;
27
+
28
+        if (!await isDialInEnabled(p1)) {
29
+            return;
30
+        }
31
+
32
+        const inviteDialog = p1.getInviteDialog();
33
+
34
+        await inviteDialog.open();
35
+        await inviteDialog.waitTillOpen();
36
+
37
+        expect((await inviteDialog.getDialInNumber()).length > 0).toBe(true);
38
+        expect((await inviteDialog.getPinNumber()).length > 0).toBe(true);
39
+    });
40
+
41
+    it('view more numbers', async () => {
42
+        const { p1 } = ctx;
43
+
44
+        if (!await isDialInEnabled(p1)) {
45
+            return;
46
+        }
47
+
48
+        const inviteDialog = p1.getInviteDialog();
49
+
50
+        await inviteDialog.open();
51
+        await inviteDialog.waitTillOpen();
52
+
53
+        const windows = await p1.driver.getWindowHandles();
54
+
55
+        expect(windows.length).toBe(1);
56
+
57
+        const meetingWindow = windows[0];
58
+
59
+        const displayedNumber = await inviteDialog.getDialInNumber();
60
+        const displayedPin = await inviteDialog.getPinNumber();
61
+
62
+        await inviteDialog.openDialInNumbersPage();
63
+
64
+        const newWindow = (await p1.driver.getWindowHandles()).filter(w => w !== meetingWindow);
65
+
66
+        expect(newWindow.length).toBe(1);
67
+
68
+        const moreNumbersWindow = newWindow[0];
69
+
70
+        await p1.driver.switchWindow(moreNumbersWindow);
71
+
72
+        await browser.pause(5000);
73
+
74
+        await p1.driver.$('.dial-in-numbers-list').waitForExist();
75
+
76
+        const conferenceIdMessage = p1.driver.$('//div[contains(@class, "pinLabel")]');
77
+
78
+        expect((await conferenceIdMessage.getText()).replace(/ /g, '').includes(displayedPin)).toBe(true);
79
+
80
+        const numbers = p1.driver.$$('.dial-in-number');
81
+
82
+        const nums = await numbers.filter(
83
+            async el => (await el.getText()).trim() === displayedNumber);
84
+
85
+        expect(nums.length).toBe(1);
86
+    });
87
+});

+ 9
- 0
tests/specs/helpers/DialIn.ts Näytä tiedosto

@@ -37,3 +37,12 @@ export async function cleanup(participant: Participant) {
37 37
         await participant.getFilmstrip().kickParticipant(jigasiEndpointId);
38 38
     }
39 39
 }
40
+
41
+/**
42
+ * Checks if the dial-in is enabled.
43
+ * @param participant
44
+ */
45
+export async function isDialInEnabled(participant: Participant) {
46
+    return await participant.driver.execute(() => Boolean(
47
+        config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts?.muc));
48
+}

Loading…
Peruuta
Tallenna