浏览代码

feat(tests): Adds dial-in test. (#15470)

* feat(tests): Adds dial-in test.

* feat(tests): Adds fake dial-in test.

* squash: switch to performance.now().
factor2
Дамян Минков 9 个月前
父节点
当前提交
ada6150971
没有帐户链接到提交者的电子邮件

+ 3
- 1
package.json 查看文件

221
     "tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
221
     "tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
222
     "start": "make dev",
222
     "start": "make dev",
223
     "test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts",
223
     "test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts",
224
+    "test-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts --spec",
224
     "test-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts",
225
     "test-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.firefox.conf.ts",
225
     "test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
226
     "test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
226
-    "test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts"
227
+    "test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts",
228
+    "test-grid-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts --spec"
227
   },
229
   },
228
   "resolutions": {
230
   "resolutions": {
229
     "@types/react": "17.0.14",
231
     "@types/react": "17.0.14",

+ 31
- 4
tests/helpers/Participant.ts 查看文件

9
 import ChatPanel from '../pageobjects/ChatPanel';
9
 import ChatPanel from '../pageobjects/ChatPanel';
10
 import Filmstrip from '../pageobjects/Filmstrip';
10
 import Filmstrip from '../pageobjects/Filmstrip';
11
 import IframeAPI from '../pageobjects/IframeAPI';
11
 import IframeAPI from '../pageobjects/IframeAPI';
12
+import InviteDialog from '../pageobjects/InviteDialog';
12
 import Notifications from '../pageobjects/Notifications';
13
 import Notifications from '../pageobjects/Notifications';
13
 import ParticipantsPane from '../pageobjects/ParticipantsPane';
14
 import ParticipantsPane from '../pageobjects/ParticipantsPane';
14
 import SettingsDialog from '../pageobjects/SettingsDialog';
15
 import SettingsDialog from '../pageobjects/SettingsDialog';
308
      *
309
      *
309
      * @returns {Promise<void>}
310
      * @returns {Promise<void>}
310
      */
311
      */
311
-    async waitForSendReceiveData(): Promise<void> {
312
+    async waitForSendReceiveData(timeout = 15_000, msg = 'expected to receive/send data in 15s'): Promise<void> {
312
         const driver = this.driver;
313
         const driver = this.driver;
313
 
314
 
314
         return driver.waitUntil(async () =>
315
         return driver.waitUntil(async () =>
322
 
323
 
323
                 return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
324
                 return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
324
             }), {
325
             }), {
325
-            timeout: 15_000,
326
-            timeoutMsg: 'expected to receive/send data in 15s'
326
+            timeout,
327
+            timeoutMsg: msg
327
         });
328
         });
328
     }
329
     }
329
 
330
 
330
     /**
331
     /**
331
      * Waits for remote streams.
332
      * Waits for remote streams.
332
      *
333
      *
333
-     * @param {number} number - The number of remote streams o wait for.
334
+     * @param {number} number - The number of remote streams to wait for.
334
      * @returns {Promise<void>}
335
      * @returns {Promise<void>}
335
      */
336
      */
336
     waitForRemoteStreams(number: number): Promise<void> {
337
     waitForRemoteStreams(number: number): Promise<void> {
343
         });
344
         });
344
     }
345
     }
345
 
346
 
347
+    /**
348
+     * Waits for number of participants.
349
+     *
350
+     * @param {number} number - The number of participant to wait for.
351
+     * @param {string} msg - A custom message to use.
352
+     * @returns {Promise<void>}
353
+     */
354
+    waitForParticipants(number: number, msg?: string): Promise<void> {
355
+        const driver = this.driver;
356
+
357
+        return driver.waitUntil(async () =>
358
+            await driver.execute(count => APP.conference.listMembers().length === count, number), {
359
+            timeout: 15_000,
360
+            timeoutMsg: msg || `not the expected participants ${number} in 15s`
361
+        });
362
+    }
363
+
346
     /**
364
     /**
347
      * Returns the chat panel for this participant.
365
      * Returns the chat panel for this participant.
348
      */
366
      */
377
         return new Filmstrip(this);
395
         return new Filmstrip(this);
378
     }
396
     }
379
 
397
 
398
+    /**
399
+     * Returns the invite dialog for this participant.
400
+     *
401
+     * @returns {InviteDialog}
402
+     */
403
+    getInviteDialog(): InviteDialog {
404
+        return new InviteDialog(this);
405
+    }
406
+
380
     /**
407
     /**
381
      * Returns the notifications.
408
      * Returns the notifications.
382
      */
409
      */

+ 2
- 0
tests/helpers/types.ts 查看文件

5
 
5
 
6
 export type IContext = {
6
 export type IContext = {
7
     conferenceJid: string;
7
     conferenceJid: string;
8
+    dialInPin: string;
8
     iframeAPI: boolean;
9
     iframeAPI: boolean;
9
     jwtKid: string;
10
     jwtKid: string;
10
     jwtPrivateKeyPath: string;
11
     jwtPrivateKeyPath: string;
14
     p4: Participant;
15
     p4: Participant;
15
     roomName: string;
16
     roomName: string;
16
     skipSuiteTests: boolean;
17
     skipSuiteTests: boolean;
18
+    times: any;
17
     webhooksProxy: WebhookProxy;
19
     webhooksProxy: WebhookProxy;
18
 };
20
 };
19
 
21
 

+ 9
- 0
tests/pageobjects/Filmstrip.ts 查看文件

99
         const popoverElement = this.participant.driver.$(
99
         const popoverElement = this.participant.driver.$(
100
             `//div[contains(@class, 'popover')]//div[contains(@class, '${linkClassname}')]`);
100
             `//div[contains(@class, 'popover')]//div[contains(@class, '${linkClassname}')]`);
101
 
101
 
102
+        await popoverElement.waitForExist();
102
         await popoverElement.waitForDisplayed();
103
         await popoverElement.waitForDisplayed();
103
         await popoverElement.click();
104
         await popoverElement.click();
104
 
105
 
127
         await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutevideolink', true);
128
         await this.clickOnRemoteMenuLink(await participant.getEndpointId(), 'mutevideolink', true);
128
     }
129
     }
129
 
130
 
131
+    /**
132
+     * Kicks a participant.
133
+     * @param participantId
134
+     */
135
+    async kickParticipant(participantId: string) {
136
+        await this.clickOnRemoteMenuLink(participantId, 'kicklink', true);
137
+    }
138
+
130
     /**
139
     /**
131
      * Clicks on the hide self view button from local video.
140
      * Clicks on the hide self view button from local video.
132
      */
141
      */

+ 44
- 0
tests/pageobjects/InviteDialog.ts 查看文件

1
+import BaseDialog from './BaseDialog';
2
+
3
+const CONFERENCE_ID = 'conference-id';
4
+const DIALOG_CONTAINER = 'invite-more-dialog';
5
+
6
+/**
7
+ * Represents the invite dialog in a particular participant.
8
+ */
9
+export default class InviteDialog extends BaseDialog {
10
+    /**
11
+     * Checks if the dialog is open.
12
+     */
13
+    async isOpen() {
14
+        return this.participant.driver.$(`.${DIALOG_CONTAINER}`).isExisting();
15
+    }
16
+
17
+    /**
18
+     * Open the invite dialog, if the info dialog is closed.
19
+     */
20
+    async open() {
21
+        if (await this.isOpen()) {
22
+            return;
23
+        }
24
+
25
+        await this.participant.getParticipantsPane().clickInvite();
26
+    }
27
+
28
+    /**
29
+     * Returns the PIN for the conference.
30
+     */
31
+    async getPinNumber() {
32
+        await this.open();
33
+
34
+        const elem = this.participant.driver.$(`.${CONFERENCE_ID}`);
35
+
36
+        await elem.waitForExist({ timeout: 5000 });
37
+
38
+        const fullText = await elem.getText();
39
+
40
+        this.participant.log(`Extracted text in invite dialog: ${fullText}`);
41
+
42
+        return fullText.split(':')[1].trim().replace(/[# ]/g, '');
43
+    }
44
+}

+ 16
- 0
tests/pageobjects/ParticipantsPane.ts 查看文件

8
  */
8
  */
9
 const PARTICIPANTS_PANE = 'participants_pane';
9
 const PARTICIPANTS_PANE = 'participants_pane';
10
 
10
 
11
+const INVITE = 'Invite someone';
12
+
11
 /**
13
 /**
12
  * Represents the participants pane from the UI.
14
  * Represents the participants pane from the UI.
13
  */
15
  */
168
         await meetingParticipantMoreOptions.moveTo();
170
         await meetingParticipantMoreOptions.moveTo();
169
         await meetingParticipantMoreOptions.click();
171
         await meetingParticipantMoreOptions.click();
170
     }
172
     }
173
+
174
+    /**
175
+     * Clicks the invite button.
176
+     */
177
+    async clickInvite() {
178
+        if (!await this.isOpen()) {
179
+            await this.open();
180
+        }
181
+
182
+        const inviteButton = this.participant.driver.$(`aria/${INVITE}`);
183
+
184
+        await inviteButton.waitForDisplayed();
185
+        await inviteButton.click();
186
+    }
171
 }
187
 }

+ 53
- 0
tests/specs/2way/fakeDialInAudio.spec.ts 查看文件

1
+import process from 'node:process';
2
+
3
+import { ensureOneParticipant, ensureTwoParticipants } from '../../helpers/participants';
4
+import { cleanup, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
5
+
6
+describe('Fake Dial-In - ', () => {
7
+    it('join participant', async () => {
8
+        // we execute fake dial in only if the real dial in is not enabled
9
+
10
+        // check rest url is not configured
11
+        if (process.env.DIAL_IN_REST_URL) {
12
+            ctx.skipSuiteTests = true;
13
+
14
+            return;
15
+        }
16
+
17
+        await ensureOneParticipant(ctx);
18
+
19
+        // check dial-in is enabled, so skip
20
+        if (await ctx.p1.driver.execute(() => Boolean(
21
+            config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts && config.hosts.muc))) {
22
+            ctx.skipSuiteTests = true;
23
+        }
24
+    });
25
+
26
+    it('open invite dialog', async () => {
27
+        await ctx.p1.getInviteDialog().open();
28
+
29
+        await ctx.p1.getInviteDialog().clickCloseButton();
30
+    });
31
+
32
+    it('invite second participant', async () => {
33
+        if (!await ctx.p1.isInMuc()) {
34
+            // local participant did not join abort
35
+            return;
36
+        }
37
+
38
+        await ensureTwoParticipants(ctx);
39
+    });
40
+
41
+    it('wait for audio from second participant', async () => {
42
+        const { p1 } = ctx;
43
+
44
+        if (!await p1.isInMuc()) {
45
+            // local participant did not join abort
46
+            return;
47
+        }
48
+
49
+        await waitForAudioFromDialInParticipant(p1);
50
+
51
+        await cleanup(p1);
52
+    });
53
+});

+ 83
- 0
tests/specs/alone/dialInAudio.spec.ts 查看文件

1
+import https from 'node:https';
2
+import process from 'node:process';
3
+
4
+import { ensureOneParticipant } from '../../helpers/participants';
5
+import { cleanup, waitForAudioFromDialInParticipant } from '../helpers/DialIn';
6
+
7
+describe('Dial-In - ', () => {
8
+    it('join participant', async () => {
9
+        // check rest url is configured
10
+        if (!process.env.DIAL_IN_REST_URL) {
11
+            ctx.skipSuiteTests = true;
12
+
13
+            return;
14
+        }
15
+
16
+        await ensureOneParticipant(ctx);
17
+
18
+        // check dial-in is enabled
19
+        if (!await ctx.p1.driver.execute(() => Boolean(
20
+            config.dialInConfCodeUrl && config.dialInNumbersUrl && config.hosts && config.hosts.muc))) {
21
+            ctx.skipSuiteTests = true;
22
+        }
23
+    });
24
+
25
+    it('retrieve pin', async () => {
26
+        const dialInPin = await ctx.p1.getInviteDialog().getPinNumber();
27
+
28
+        await ctx.p1.getInviteDialog().clickCloseButton();
29
+
30
+        if (dialInPin.length === 0) {
31
+            console.error('dial-in.test.no-pin');
32
+        }
33
+
34
+        expect(dialInPin.length >= 9).toBe(true);
35
+
36
+        ctx.dialInPin = dialInPin;
37
+    });
38
+
39
+    it('invite dial-in participant', async () => {
40
+        if (!await ctx.p1.isInMuc()) {
41
+            // local participant did not join abort
42
+            return;
43
+        }
44
+
45
+        const restUrl = process.env.DIAL_IN_REST_URL?.replace('{0}', ctx.dialInPin);
46
+
47
+        // we have already checked in the first test that DIAL_IN_REST_URL exist so restUrl cannot be ''
48
+        const responseData: string = await new Promise((resolve, reject) => {
49
+            https.get(restUrl || '', res => {
50
+                let data = '';
51
+
52
+                res.on('data', chunk => {
53
+                    data += chunk;
54
+                });
55
+
56
+                res.on('end', () => {
57
+                    ctx.times.restAPIExecutionTS = performance.now();
58
+
59
+                    resolve(data);
60
+                });
61
+            }).on('error', err => {
62
+                console.error('dial-in.test.restAPI.request.fail');
63
+                console.error(err);
64
+                reject(err);
65
+            });
66
+        });
67
+
68
+        console.log(`dial-in.test.call_session_history_id:${JSON.parse(responseData).call_session_history_id}`);
69
+    });
70
+
71
+    it('wait for audio from dial-in participant', async () => {
72
+        const { p1 } = ctx;
73
+
74
+        if (!await p1.isInMuc()) {
75
+            // local participant did not join abort
76
+            return;
77
+        }
78
+
79
+        await waitForAudioFromDialInParticipant(p1);
80
+
81
+        await cleanup(p1);
82
+    });
83
+});

+ 39
- 0
tests/specs/helpers/DialIn.ts 查看文件

1
+import type { Participant } from '../../helpers/Participant';
2
+
3
+/**
4
+ * Helper functions for dial-in related operations.
5
+ * To be able to create a fake dial-in test that will run most of the logic for the real dial-in test.
6
+ */
7
+
8
+/**
9
+ * Waits for the audio from the dial-in participant.
10
+ * @param participant
11
+ */
12
+export async function waitForAudioFromDialInParticipant(participant: Participant) {
13
+    // waits 15 seconds for the participant to join
14
+    await participant.waitForParticipants(1, `dial-in.test.jigasi.participant.no.join.for:${
15
+        ctx.times.restAPIExecutionTS + 15_000} ms.`);
16
+
17
+    const joinedTS = performance.now();
18
+
19
+    console.log(`dial-in.test.jigasi.participant.join.after:${joinedTS - ctx.times.restAPIExecutionTS}`);
20
+
21
+    await participant.waitForIceConnected();
22
+    await participant.waitForRemoteStreams(1);
23
+
24
+    await participant.waitForSendReceiveData(20_000, 'dial-in.test.jigasi.participant.no.audio.after.join');
25
+    console.log(`dial-in.test.jigasi.participant.received.audio.after.join:${performance.now() - joinedTS} ms.`);
26
+}
27
+
28
+/**
29
+ * Cleans up the dial-in participant by kicking it if the local participant is a moderator.
30
+ * @param participant
31
+ */
32
+export async function cleanup(participant: Participant) {
33
+    // cleanup
34
+    if (await participant.isModerator()) {
35
+        const jigasiEndpointId = await participant.driver.execute(() => APP.conference.listMembers()[0].getId());
36
+
37
+        await participant.getFilmstrip().kickParticipant(jigasiEndpointId);
38
+    }
39
+}

+ 3
- 1
tests/wdio.conf.ts 查看文件

191
         const globalAny: any = global;
191
         const globalAny: any = global;
192
         const roomName = `jitsimeettorture-${crypto.randomUUID()}`;
192
         const roomName = `jitsimeettorture-${crypto.randomUUID()}`;
193
 
193
 
194
-        globalAny.ctx = {} as IContext;
194
+        globalAny.ctx = {
195
+            times: {}
196
+        } as IContext;
195
         globalAny.ctx.roomName = roomName;
197
         globalAny.ctx.roomName = roomName;
196
         globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
198
         globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
197
         globalAny.ctx.jwtKid = process.env.JWT_KID;
199
         globalAny.ctx.jwtKid = process.env.JWT_KID;

正在加载...
取消
保存