Browse Source

feat(tests): First test from torture to meet. (#15298)

* feat(tests): First test from torture to meet.

* squash: Fixes logging as per comments.

* squash: Fixes some eslint errors.

* squash: Drop no needed await and async declarations.

* squash: Simplify syntax.

* squash: Disable blur everywhere not only FF.

* squash: Use allSettled.

* squash: Prettify intervals and timeouts.

* squash: Use uuids for torture rooms.

* squash: Introduce helper methods in Participant for toolbar and filmstrip.

* squash: Changes headless resolution to a standard 720p.

* squash: Adds env BASE_URL.

* squash: Fix some eslint errors.

* squash: Fix js error.

* squash: Fix participant logs.

* squash: Move bag to Promise.all.

* squash: More types thing.

* squash: Fix more ts errors.

* squash: Bumps version to include 6d146cd332

* squash: More ts stuff.

* squash: Fixes last ts errors.

* squash: Drop eslint rule.

* squash: Update default configs.

* squash: Drop and docs eslint.
factor2
Дамян Минков 11 months ago
parent
commit
5cd7b9be38
No account linked to committer's email address

+ 5
- 0
.gitignore View File

109
 react-native-sdk/react
109
 react-native-sdk/react
110
 react-native-sdk/service
110
 react-native-sdk/service
111
 react-native-sdk/sounds
111
 react-native-sdk/sounds
112
+
113
+# tests
114
+tests/.env
115
+test-results
116
+

+ 0
- 1
config.js View File

1629
      iAmRecorder
1629
      iAmRecorder
1630
      iAmSipGateway
1630
      iAmSipGateway
1631
      microsoftApiApplicationClientID
1631
      microsoftApiApplicationClientID
1632
-     requireDisplayName
1633
      */
1632
      */
1634
 
1633
 
1635
     /**
1634
     /**

+ 8514
- 24
package-lock.json
File diff suppressed because it is too large
View File


+ 11
- 1
package.json View File

49
     "@vladmandic/human-models": "2.5.9",
49
     "@vladmandic/human-models": "2.5.9",
50
     "@xmldom/xmldom": "0.8.7",
50
     "@xmldom/xmldom": "0.8.7",
51
     "abab": "2.0.6",
51
     "abab": "2.0.6",
52
+    "allure-commandline": "2.32.0",
52
     "amplitude-js": "8.21.9",
53
     "amplitude-js": "8.21.9",
53
     "base64-js": "1.5.1",
54
     "base64-js": "1.5.1",
54
     "bc-css-flags": "3.0.0",
55
     "bc-css-flags": "3.0.0",
138
     "@types/amplitude-js": "8.16.5",
139
     "@types/amplitude-js": "8.16.5",
139
     "@types/audioworklet": "0.0.29",
140
     "@types/audioworklet": "0.0.29",
140
     "@types/dom-screen-wake-lock": "1.0.1",
141
     "@types/dom-screen-wake-lock": "1.0.1",
142
+    "@types/jasmine": "5.1.4",
141
     "@types/js-md5": "0.4.3",
143
     "@types/js-md5": "0.4.3",
142
     "@types/lodash-es": "4.17.12",
144
     "@types/lodash-es": "4.17.12",
143
     "@types/moment-duration-format": "2.2.6",
145
     "@types/moment-duration-format": "2.2.6",
158
     "@types/zxcvbn": "4.4.1",
160
     "@types/zxcvbn": "4.4.1",
159
     "@typescript-eslint/eslint-plugin": "5.59.5",
161
     "@typescript-eslint/eslint-plugin": "5.59.5",
160
     "@typescript-eslint/parser": "5.59.5",
162
     "@typescript-eslint/parser": "5.59.5",
163
+    "@wdio/allure-reporter": "9.2.14",
164
+    "@wdio/cli": "9.2.14",
165
+    "@wdio/globals": "9.2.14",
166
+    "@wdio/jasmine-framework": "9.2.14",
167
+    "@wdio/junit-reporter": "9.2.14",
168
+    "@wdio/local-runner": "9.2.15",
161
     "babel-loader": "9.1.0",
169
     "babel-loader": "9.1.0",
162
     "babel-plugin-optional-require": "0.3.1",
170
     "babel-plugin-optional-require": "0.3.1",
163
     "circular-dependency-plugin": "5.2.0",
171
     "circular-dependency-plugin": "5.2.0",
179
     "ts-loader": "9.4.2",
187
     "ts-loader": "9.4.2",
180
     "typescript": "5.0.4",
188
     "typescript": "5.0.4",
181
     "unorm": "1.6.0",
189
     "unorm": "1.6.0",
190
+    "webdriverio": "9.2.14",
182
     "webpack": "5.95.0",
191
     "webpack": "5.95.0",
183
     "webpack-bundle-analyzer": "4.4.2",
192
     "webpack-bundle-analyzer": "4.4.2",
184
     "webpack-cli": "4.9.0",
193
     "webpack-cli": "4.9.0",
205
     "validate": "npm ls",
214
     "validate": "npm ls",
206
     "tsc-test:web": "tsc --project tsconfig.web.json --listFilesOnly | grep -v node_modules | grep native",
215
     "tsc-test:web": "tsc --project tsconfig.web.json --listFilesOnly | grep -v node_modules | grep native",
207
     "tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
216
     "tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
208
-    "start": "make dev"
217
+    "start": "make dev",
218
+    "test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts"
209
   },
219
   },
210
   "resolutions": {
220
   "resolutions": {
211
     "@types/react": "17.0.14",
221
     "@types/react": "17.0.14",

+ 15
- 0
tests/.eslintrc.js View File

1
+module.exports = {
2
+    'extends': [
3
+        '../.eslintrc.js'
4
+    ],
5
+    'overrides': [
6
+        {
7
+            'files': [ '*.ts', '*.tsx' ],
8
+            extends: [ '@jitsi/eslint-config/typescript' ],
9
+            parserOptions: {
10
+                sourceType: 'module',
11
+                project: [ './tests/tsconfig.json' ]
12
+            }
13
+        }
14
+    ]
15
+};

+ 15
- 0
tests/env.example View File

1
+# The base url that will be used for the test (default will be using "https://alpha.jitsi.net")
2
+#BASE_URL=
3
+
4
+# To be able to match a domain to a specific address
5
+# The format is "MAP example.com 1.2.3.4"
6
+#RESOLVER_RULES=
7
+
8
+# Ignore certificate errors (self-signed certificates)
9
+#ALLOW_INSECURE_CERTS=true
10
+
11
+# Whether to run the browser in headless mode
12
+#HEADLESS=false
13
+
14
+# The path to the browser video capture file
15
+#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m

+ 289
- 0
tests/helpers/Participant.ts View File

1
+/* global APP $ */
2
+
3
+import { multiremotebrowser } from '@wdio/globals';
4
+
5
+import { IConfig } from '../../react/features/base/config/configType';
6
+import { urlObjectToString } from '../../react/features/base/util/uri';
7
+import Filmstrip from '../pageobjects/Filmstrip';
8
+import Toolbar from '../pageobjects/Toolbar';
9
+
10
+import { LOG_PREFIX, logInfo } from './browserLogger';
11
+import { IContext } from './participants';
12
+
13
+/**
14
+ * Participant.
15
+ */
16
+export class Participant {
17
+    /**
18
+     * The current context.
19
+     *
20
+     * @private
21
+     */
22
+    private context: { roomName: string; };
23
+    private _name: string;
24
+    private _endpointId: string;
25
+
26
+    /**
27
+     * The default config to use when joining.
28
+     *
29
+     * @private
30
+     */
31
+    private config = {
32
+        analytics: {
33
+            disabled: true
34
+        },
35
+        debug: true,
36
+        requireDisplayName: false,
37
+        testing: {
38
+            testMode: true
39
+        },
40
+        disableAP: true,
41
+        disable1On1Mode: true,
42
+        disableModeratorIndicator: true,
43
+        enableTalkWhileMuted: false,
44
+        gatherStats: true,
45
+        p2p: {
46
+            enabled: false,
47
+            useStunTurn: false
48
+        },
49
+        pcStatsInterval: 1500,
50
+        prejoinConfig: {
51
+            enabled: false
52
+        },
53
+        toolbarConfig: {
54
+            alwaysVisible: true
55
+        }
56
+    } as IConfig;
57
+
58
+    /**
59
+     * Creates a participant with given name.
60
+     *
61
+     * @param {string} name - The name of the participant.
62
+     */
63
+    constructor(name: string) {
64
+        this._name = name;
65
+    }
66
+
67
+    /**
68
+     * Returns participant endpoint ID.
69
+     *
70
+     * @returns {Promise<string>} The endpoint ID.
71
+     */
72
+    async getEndpointId() {
73
+        if (!this._endpointId) {
74
+            this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
75
+                return APP.conference.getMyUserId();
76
+            });
77
+        }
78
+
79
+        return this._endpointId;
80
+    }
81
+
82
+    /**
83
+     * The driver it uses.
84
+     */
85
+    get driver() {
86
+        return multiremotebrowser.getInstance(this._name);
87
+    }
88
+
89
+    /**
90
+     * The name.
91
+     */
92
+    get name() {
93
+        return this._name;
94
+    }
95
+
96
+    /**
97
+     * Adds a log to the participants log file.
98
+     *
99
+     * @param {string} message - The message to log.
100
+     * @returns {void}
101
+     */
102
+    log(message: string) {
103
+        logInfo(this.driver, message);
104
+    }
105
+
106
+    /**
107
+     * Joins conference.
108
+     *
109
+     * @param {IContext} context - The context.
110
+     * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
111
+     * @returns {Promise<void>}
112
+     */
113
+    async joinConference(context: IContext, skipInMeetingChecks = false) {
114
+        this.context = context;
115
+
116
+        const url = urlObjectToString({
117
+            room: context.roomName,
118
+            configOverwrite: this.config,
119
+            interfaceConfigOverwrite: {
120
+                SHOW_CHROME_EXTENSION_BANNER: false
121
+            },
122
+            userInfo: {
123
+                displayName: this._name
124
+            }
125
+        }) || '';
126
+
127
+        await this.driver.setTimeout({ 'pageLoad': 30000 });
128
+
129
+        await this.driver.url(url);
130
+
131
+        await this.waitForPageToLoad();
132
+
133
+        await this.waitToJoinMUC();
134
+
135
+        await this.postLoadProcess(skipInMeetingChecks);
136
+    }
137
+
138
+    /**
139
+     * Loads stuff after the page loads.
140
+     *
141
+     * @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
142
+     * @returns {Promise<void>}
143
+     * @private
144
+     */
145
+    private async postLoadProcess(skipInMeetingChecks: boolean) {
146
+        const driver = this.driver;
147
+
148
+        const parallel = [];
149
+
150
+        parallel.push(driver.execute((name, sessionId, prefix) => {
151
+            APP.UI.dockToolbar(true);
152
+
153
+            // disable keyframe animations (.fadeIn and .fadeOut classes)
154
+            $('<style>.notransition * { '
155
+                + 'animation-duration: 0s !important; -webkit-animation-duration: 0s !important; transition:none; '
156
+                + '} </style>') // @ts-ignore
157
+                    .appendTo(document.head);
158
+
159
+            // @ts-ignore
160
+            $('body').toggleClass('notransition');
161
+
162
+            document.title = `${name}`;
163
+
164
+            console.log(`${new Date().toISOString()} ${prefix} sessionId: ${sessionId}`);
165
+
166
+            // disable the blur effect in firefox as it has some performance issues
167
+            const blur = document.querySelector('.video_blurred_container');
168
+
169
+            if (blur) {
170
+                // @ts-ignore
171
+                document.querySelector('.video_blurred_container').style.display = 'none';
172
+            }
173
+        }, this._name, driver.sessionId, LOG_PREFIX));
174
+
175
+        if (skipInMeetingChecks) {
176
+            await Promise.allSettled(parallel);
177
+
178
+            return;
179
+        }
180
+
181
+        parallel.push(this.waitForIceConnected());
182
+        parallel.push(this.waitForSendReceiveData());
183
+
184
+        await Promise.all(parallel);
185
+    }
186
+
187
+    /**
188
+     * Waits for the page to load.
189
+     *
190
+     * @returns {Promise<void>}
191
+     */
192
+    async waitForPageToLoad() {
193
+        return this.driver.waitUntil(
194
+            () => this.driver.execute(() => document.readyState === 'complete'),
195
+            {
196
+                timeout: 30_000, // 30 seconds
197
+                timeoutMsg: 'Timeout waiting for Page Load Request to complete.'
198
+            }
199
+        );
200
+    }
201
+
202
+    /**
203
+     * Waits to join the muc.
204
+     *
205
+     * @returns {Promise<void>}
206
+     */
207
+    async waitToJoinMUC() {
208
+        return this.driver.waitUntil(
209
+            () => this.driver.execute(() => APP.conference.isJoined()),
210
+            {
211
+                timeout: 10_000, // 10 seconds
212
+                timeoutMsg: 'Timeout waiting to join muc.'
213
+            }
214
+        );
215
+    }
216
+
217
+    /**
218
+     * Waits for ICE to get connected.
219
+     *
220
+     * @returns {Promise<void>}
221
+     */
222
+    async waitForIceConnected() {
223
+        const driver = this.driver;
224
+
225
+        return driver.waitUntil(async () =>
226
+            driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
227
+            timeout: 15_000,
228
+            timeoutMsg: 'expected ICE to be connected for 15s'
229
+        });
230
+    }
231
+
232
+    /**
233
+     * Waits for send and receive data.
234
+     *
235
+     * @returns {Promise<void>}
236
+     */
237
+    async waitForSendReceiveData() {
238
+        const driver = this.driver;
239
+
240
+        return driver.waitUntil(async () =>
241
+            driver.execute(() => {
242
+                const stats = APP.conference.getStats();
243
+                const bitrateMap = stats?.bitrate || {};
244
+                const rtpStats = {
245
+                    uploadBitrate: bitrateMap.upload || 0,
246
+                    downloadBitrate: bitrateMap.download || 0
247
+                };
248
+
249
+                return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
250
+            }), {
251
+            timeout: 15_000,
252
+            timeoutMsg: 'expected to receive/send data in 15s'
253
+        });
254
+    }
255
+
256
+    /**
257
+     * Waits for remote streams.
258
+     *
259
+     * @param {number} number - The number of remote streams o wait for.
260
+     * @returns {Promise<void>}
261
+     */
262
+    waitForRemoteStreams(number: number) {
263
+        const driver = this.driver;
264
+
265
+        return driver.waitUntil(async () =>
266
+            driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
267
+            timeout: 15_000,
268
+            timeoutMsg: 'expected remote streams in 15s'
269
+        });
270
+    }
271
+
272
+    /**
273
+     * Returns the toolbar for this participant.
274
+     *
275
+     * @returns {Toolbar}
276
+     */
277
+    getToolbar() {
278
+        return new Toolbar(this);
279
+    }
280
+
281
+    /**
282
+     * Returns the filmstrip for this participant.
283
+     *
284
+     * @returns {Filmstrip}
285
+     */
286
+    getFilmstrip() {
287
+        return new Filmstrip(this);
288
+    }
289
+}

+ 67
- 0
tests/helpers/browserLogger.ts View File

1
+import fs from 'node:fs';
2
+
3
+/**
4
+ * A prefix to use for all messages we add to the console log.
5
+ */
6
+export const LOG_PREFIX = '[MeetTest] ';
7
+
8
+/**
9
+ * Initialize logger for a driver.
10
+ *
11
+ * @param {WebdriverIO.Browser} driver - The driver.
12
+ * @param {string} name - The name of the participant.
13
+ * @param {string} folder - The folder to save the file.
14
+ * @returns {void}
15
+ */
16
+export function initLogger(driver: WebdriverIO.Browser, name: string, folder: string) {
17
+    // @ts-ignore
18
+    driver.logFile = `${folder}/${name}.log`;
19
+    driver.sessionSubscribe({ events: [ 'log.entryAdded' ] });
20
+
21
+    driver.on('log.entryAdded', (entry: any) => {
22
+        try {
23
+            // @ts-ignore
24
+            fs.appendFileSync(driver.logFile, `${entry.text}\n`);
25
+        } catch (err) {
26
+            console.error(err);
27
+        }
28
+    });
29
+}
30
+
31
+/**
32
+ * Returns the content of the log file.
33
+ *
34
+ * @param {WebdriverIO.Browser} driver - The driver which log file is requested.
35
+ * @returns {string} The content of the log file.
36
+ */
37
+export function getLogs(driver: WebdriverIO.Browser) {
38
+    // @ts-ignore
39
+    if (!driver.logFile) {
40
+        return;
41
+    }
42
+
43
+    // @ts-ignore
44
+    return fs.readFileSync(driver.logFile, 'utf8');
45
+}
46
+
47
+/**
48
+ * Logs a message in the logfile.
49
+ *
50
+ * @param {WebdriverIO.Browser} driver - The participant in which log file to write.
51
+ * @param {string} message - The message to add.
52
+ * @returns {void}
53
+ */
54
+export function logInfo(driver: WebdriverIO.Browser, message: string) {
55
+    // @ts-ignore
56
+    if (!driver.logFile) {
57
+        return;
58
+    }
59
+
60
+    try {
61
+        // @ts-ignore
62
+        fs.appendFileSync(driver.logFile, `${new Date().toISOString()} ${LOG_PREFIX} ${message}\n`);
63
+    } catch (err) {
64
+        console.error(err);
65
+    }
66
+}
67
+

+ 80
- 0
tests/helpers/participants.ts View File

1
+import { Participant } from './Participant';
2
+
3
+export type IContext = {
4
+    p1: Participant;
5
+    p2: Participant;
6
+    p3: Participant;
7
+    p4: Participant;
8
+    roomName: string;
9
+};
10
+
11
+/**
12
+ * Generate a random room name.
13
+ *
14
+ * @returns {string} - The random room name.
15
+ */
16
+function generateRandomRoomName(): string {
17
+    return `jitsimeettorture-${crypto.randomUUID()}}`;
18
+}
19
+
20
+/**
21
+ * Ensure that there is on participant.
22
+ *
23
+ * @param {IContext} context - The context.
24
+ * @returns {Promise<void>}
25
+ */
26
+export async function ensureOneParticipant(context: IContext): Promise<void> {
27
+    context.roomName = generateRandomRoomName();
28
+
29
+    context.p1 = new Participant('participant1');
30
+
31
+    await context.p1.joinConference(context, true);
32
+}
33
+
34
+/**
35
+ * Ensure that there are three participants.
36
+ *
37
+ * @param {Object} context - The context.
38
+ * @returns {Promise<void>}
39
+ */
40
+export async function ensureThreeParticipants(context: IContext): Promise<void> {
41
+    context.roomName = generateRandomRoomName();
42
+
43
+    const p1 = new Participant('participant1');
44
+    const p2 = new Participant('participant2');
45
+    const p3 = new Participant('participant3');
46
+
47
+    context.p1 = p1;
48
+    context.p2 = p2;
49
+    context.p3 = p3;
50
+
51
+    // these need to be all, so we get the error when one fails
52
+    await Promise.all([
53
+        p1.joinConference(context),
54
+        p2.joinConference(context),
55
+        p3.joinConference(context)
56
+    ]);
57
+
58
+    await Promise.all([
59
+        p1.waitForRemoteStreams(2),
60
+        p2.waitForRemoteStreams(2),
61
+        p3.waitForRemoteStreams(2)
62
+    ]);
63
+}
64
+
65
+/**
66
+ * Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
67
+ * conference participants sees a specific mute state for the former.
68
+ *
69
+ * @param {Participant} testee - The {@code Participant} which represents the Meet conference participant whose
70
+ * mute state is to be toggled.
71
+ * @param {Participant} observer - The {@code Participant} which represents the Meet conference participant to verify
72
+ * the mute state of {@code testee}.
73
+ * @returns {Promise<void>}
74
+ */
75
+export async function toggleMuteAndCheck(testee: Participant, observer: Participant): Promise<void> {
76
+    await testee.getToolbar().clickAudioMuteButton();
77
+
78
+    await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
79
+    await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
80
+}

+ 46
- 0
tests/pageobjects/Filmstrip.ts View File

1
+import { Participant } from '../helpers/Participant';
2
+
3
+/**
4
+ * Filmstrip elements.
5
+ */
6
+export default class Filmstrip {
7
+    private participant: Participant;
8
+
9
+    /**
10
+     * Initializes for a participant.
11
+     *
12
+     * @param {Participant} participant - The participant.
13
+     */
14
+    constructor(participant: Participant) {
15
+        this.participant = participant;
16
+    }
17
+
18
+    /**
19
+     * Asserts that {@code participant} shows or doesn't show the audio
20
+     * mute icon for the conference participant identified by
21
+     * {@code testee}.
22
+     *
23
+     * @param {Participant} testee - The {@code WebParticipant} for whom we're checking the status of audio muted icon.
24
+     * @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
25
+     * otherwise, it will assert its presence.
26
+     * @returns {Promise<void>}
27
+     */
28
+    async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false) {
29
+        let id;
30
+
31
+        if (testee === this.participant) {
32
+            id = 'localVideoContainer';
33
+        } else {
34
+            id = `participant_${await testee.getEndpointId()}`;
35
+        }
36
+
37
+        const mutedIconXPath
38
+            = `//span[@id='${id}']//span[contains(@id, 'audioMuted')]//*[local-name()='svg' and @id='mic-disabled']`;
39
+
40
+        await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
41
+            reverse,
42
+            timeout: 2000,
43
+            timeoutMsg: `Audio mute icon is not displayed for ${testee.name}`
44
+        });
45
+    }
46
+}

+ 66
- 0
tests/pageobjects/Toolbar.ts View File

1
+// eslint-disable-next-line no-unused-vars
2
+import { Participant } from '../helpers/Participant';
3
+
4
+const AUDIO_MUTE = 'Mute microphone';
5
+const AUDIO_UNMUTE = 'Unmute microphone';
6
+
7
+/**
8
+ * The toolbar elements.
9
+ */
10
+export default class Toolbar {
11
+    private participant: Participant;
12
+
13
+    /**
14
+     * Creates toolbar for a participant.
15
+     *
16
+     * @param {Participant} participant - The participants.
17
+     */
18
+    constructor(participant: Participant) {
19
+        this.participant = participant;
20
+    }
21
+
22
+    /**
23
+     * Returns the button.
24
+     *
25
+     * @param {string} accessibilityCSSSelector - The selector to find the button.
26
+     * @returns {WebdriverIO.Element} The button.
27
+     * @private
28
+     */
29
+    private getButton(accessibilityCSSSelector: string) {
30
+        return this.participant.driver.$(`[aria-label^="${accessibilityCSSSelector}"]`);
31
+    }
32
+
33
+    /**
34
+     * The audio mute button.
35
+     */
36
+    get audioMuteBtn() {
37
+        return this.getButton(AUDIO_MUTE);
38
+    }
39
+
40
+    /**
41
+     * The audio unmute button.
42
+     */
43
+    get audioUnMuteBtn() {
44
+        return this.getButton(AUDIO_UNMUTE);
45
+    }
46
+
47
+    /**
48
+     * Clicks audio mute button.
49
+     *
50
+     * @returns {Promise<void>}
51
+     */
52
+    async clickAudioMuteButton() {
53
+        await this.participant.log('Clicking on: Audio Mute Button');
54
+        await this.audioMuteBtn.click();
55
+    }
56
+
57
+    /**
58
+     * Clicks audio unmute button.
59
+     *
60
+     * @returns {Promise<void>}
61
+     */
62
+    async clickAudioUnmuteButton() {
63
+        await this.participant.log('Clicking on: Audio Unmute Button');
64
+        await this.audioUnMuteBtn.click();
65
+    }
66
+}

+ 94
- 0
tests/specs/3way/activeSpeaker.spec.ts View File

1
+/* global APP */
2
+import type { Participant } from '../../helpers/Participant';
3
+import { IContext, ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants';
4
+
5
+describe('ActiveSpeaker ', () => {
6
+    const context = {} as IContext;
7
+
8
+    it('testActiveSpeaker', async () => {
9
+        await ensureThreeParticipants(context);
10
+
11
+        await toggleMuteAndCheck(context.p1, context.p2);
12
+        await toggleMuteAndCheck(context.p2, context.p1);
13
+        await toggleMuteAndCheck(context.p3, context.p1);
14
+
15
+        // participant1 becomes active speaker - check from participant2's perspective
16
+        await testActiveSpeaker(context.p1, context.p2, context.p3);
17
+
18
+        // participant3 becomes active speaker - check from participant2's perspective
19
+        await testActiveSpeaker(context.p3, context.p2, context.p1);
20
+
21
+        // participant2 becomes active speaker - check from participant1's perspective
22
+        await testActiveSpeaker(context.p2, context.p1, context.p3);
23
+
24
+        // check the displayed speakers, there should be only one speaker
25
+        await assertOneDominantSpeaker(context.p1);
26
+        await assertOneDominantSpeaker(context.p2);
27
+        await assertOneDominantSpeaker(context.p3);
28
+    });
29
+});
30
+
31
+/**
32
+ * Tries to make given participant an active speaker by un-muting it.
33
+ * Verifies from {@code participant2}'s perspective that the active speaker
34
+ * has been displayed on the large video area. Mutes him back.
35
+ *
36
+ * @param {Participant} activeSpeaker - <tt>Participant</tt> instance of the participant who will be tested as an
37
+ * active speaker.
38
+ * @param {Participant} otherParticipant1 - <tt>Participant</tt> of the participant who will be observing and verifying
39
+ * active speaker change.
40
+ * @param {Participant} otherParticipant2 - Used only to print some debugging info.
41
+ * @returns {Promise<void>}
42
+ */
43
+async function testActiveSpeaker(
44
+        activeSpeaker: Participant, otherParticipant1: Participant, otherParticipant2: Participant) {
45
+    activeSpeaker.log(`Start testActiveSpeaker for participant: ${activeSpeaker.name}`);
46
+
47
+    const speakerEndpoint = await activeSpeaker.getEndpointId();
48
+
49
+    // just a debug print to go in logs
50
+    activeSpeaker.log('Unmuting in testActiveSpeaker');
51
+
52
+    // Unmute
53
+    await activeSpeaker.getToolbar().clickAudioUnmuteButton();
54
+
55
+    // just a debug print to go in logs
56
+    otherParticipant1.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
57
+    otherParticipant2.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
58
+
59
+    await activeSpeaker.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker, true);
60
+
61
+    // Verify that the user is now an active speaker from otherParticipant1's perspective
62
+    const otherParticipant1Driver = otherParticipant1.driver;
63
+
64
+    await otherParticipant1Driver.waitUntil(
65
+        () => otherParticipant1Driver.execute((id: string) => APP.UI.getLargeVideoID() === id, speakerEndpoint),
66
+        {
67
+            timeout: 30_1000, // 30 seconds
68
+            timeoutMsg: 'Active speaker not displayed on large video.'
69
+        });
70
+
71
+    // just a debug print to go in logs
72
+    activeSpeaker.log('Muting in testActiveSpeaker');
73
+
74
+    // Mute back again
75
+    await activeSpeaker.getToolbar().clickAudioMuteButton();
76
+
77
+    // just a debug print to go in logs
78
+    otherParticipant1.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
79
+    otherParticipant2.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
80
+
81
+    await otherParticipant1.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker);
82
+}
83
+
84
+/**
85
+ * Asserts that the number of small videos with the dominant speaker
86
+ * indicator displayed equals 1.
87
+ *
88
+ * @param {Participant} participant - The participant to check.
89
+ * @returns {Promise<void>}
90
+ */
91
+async function assertOneDominantSpeaker(participant: Participant) {
92
+    expect(await participant.driver.$$(
93
+        '//span[not(contains(@class, "tile-view"))]//span[contains(@class,"dominant-speaker")]').length).toBe(1);
94
+}

+ 12
- 0
tests/tsconfig.json View File

1
+{
2
+    "include": ["**/*.ts", "../globals.d.ts"],
3
+    "extends": "../tsconfig.web",
4
+    "compilerOptions": {
5
+        "types": [
6
+            "node",
7
+            "@wdio/globals/types",
8
+            "@types/jasmine",
9
+            "@wdio/jasmine-framework"
10
+        ]
11
+    }
12
+}

+ 273
- 0
tests/wdio.conf.ts View File

1
+import AllureReporter from '@wdio/allure-reporter';
2
+import { multiremotebrowser } from '@wdio/globals';
3
+import { Buffer } from 'buffer';
4
+import process from 'node:process';
5
+
6
+import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
7
+
8
+// eslint-disable-next-line @typescript-eslint/no-var-requires
9
+const allure = require('allure-commandline');
10
+
11
+// This is deprecated without alternative (https://github.com/nodejs/node/issues/32483)
12
+// we need it to be able to reuse jitsi-meet code in tests
13
+require.extensions['.web.ts'] = require.extensions['.ts'];
14
+
15
+const chromeArgs = [
16
+    '--allow-insecure-localhost',
17
+    '--use-fake-ui-for-media-stream',
18
+    '--use-fake-device-for-media-stream',
19
+    '--disable-plugins',
20
+    '--mute-audio',
21
+    '--disable-infobars',
22
+    '--autoplay-policy=no-user-gesture-required',
23
+    '--auto-select-desktop-capture-source=Your Entire screen',
24
+    '--no-sandbox',
25
+    '--disable-dev-shm-usage',
26
+    '--disable-setuid-sandbox',
27
+    '--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav'
28
+];
29
+
30
+if (process.env.RESOLVER_RULES) {
31
+    chromeArgs.push(`--host-resolver-rules=${process.env.RESOLVER_RULES}`);
32
+}
33
+if (process.env.ALLOW_INSECURE_CERTS === 'true') {
34
+    chromeArgs.push('--ignore-certificate-errors');
35
+}
36
+if (process.env.HEADLESS === 'true') {
37
+    chromeArgs.push('--headless');
38
+    chromeArgs.push('--window-size=1280,720');
39
+}
40
+if (process.env.VIDEO_CAPTURE_FILE) {
41
+    chromeArgs.push(`use-file-for-fake-video-capture=${process.env.VIDEO_CAPTURE_FILE}`);
42
+}
43
+
44
+const chromePreferences = {
45
+    'intl.accept_languages': 'en-US'
46
+};
47
+
48
+const TEST_RESULTS_DIR = 'test-results';
49
+
50
+export const config: WebdriverIO.MultiremoteConfig = {
51
+
52
+    runner: 'local',
53
+
54
+    specs: [
55
+        'specs/**'
56
+    ],
57
+    maxInstances: 1,
58
+
59
+    baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture',
60
+    tsConfigPath: './tsconfig.json',
61
+
62
+    // Default timeout for all waitForXXX commands.
63
+    waitforTimeout: 1000,
64
+
65
+    // Default timeout in milliseconds for request
66
+    // if browser driver or grid doesn't send response
67
+    connectionRetryTimeout: 15_000,
68
+
69
+    // Default request retries count
70
+    connectionRetryCount: 3,
71
+
72
+    framework: 'jasmine',
73
+
74
+    jasmineOpts: {
75
+        defaultTimeoutInterval: 60_000
76
+    },
77
+
78
+    capabilities: {
79
+        participant1: {
80
+            capabilities: {
81
+                browserName: 'chrome',
82
+                'goog:chromeOptions': {
83
+                    args: chromeArgs,
84
+                    prefs: chromePreferences
85
+                }
86
+            }
87
+        },
88
+        participant2: {
89
+            capabilities: {
90
+                browserName: 'chrome',
91
+                'goog:chromeOptions': {
92
+                    args: chromeArgs,
93
+                    prefs: chromePreferences
94
+                },
95
+                'wdio:exclude': [
96
+                    'specs/alone/**'
97
+                ]
98
+            }
99
+        },
100
+        participant3: {
101
+            capabilities: {
102
+                browserName: 'chrome',
103
+                'goog:chromeOptions': {
104
+                    args: chromeArgs,
105
+                    prefs: chromePreferences
106
+                },
107
+                'wdio:exclude': [
108
+                    'specs/2way/**'
109
+                ]
110
+            }
111
+        },
112
+        participant4: {
113
+            capabilities: {
114
+                browserName: 'chrome',
115
+                'goog:chromeOptions': {
116
+                    args: chromeArgs,
117
+                    prefs: chromePreferences
118
+                },
119
+                'wdio:exclude': [
120
+                    'specs/3way/**'
121
+                ]
122
+            }
123
+        }
124
+    },
125
+
126
+    // Level of logging verbosity: trace | debug | info | warn | error | silent
127
+    logLevel: 'trace',
128
+    logLevels: {
129
+        webdriver: 'info'
130
+    },
131
+
132
+    // Set directory to store all logs into
133
+    outputDir: TEST_RESULTS_DIR,
134
+
135
+    reporters: [
136
+        [ 'junit', {
137
+            outputDir: TEST_RESULTS_DIR,
138
+            outputFileFormat(options) { // optional
139
+                return `results-${options.cid}.xml`;
140
+            }
141
+        } ],
142
+        [ 'allure', {
143
+            // addConsoleLogs: true,
144
+            outputDir: `${TEST_RESULTS_DIR}/allure-results`,
145
+            disableWebdriverStepsReporting: true,
146
+            disableWebdriverScreenshotsReporting: true,
147
+            useCucumberStepReporter: false
148
+        } ]
149
+    ],
150
+
151
+    // =====
152
+    // Hooks
153
+    // =====
154
+    /**
155
+     * Gets executed before test execution begins. At this point you can access to all global
156
+     * variables like `browser`. It is the perfect place to define custom commands.
157
+     *
158
+     * @returns {Promise<void>}
159
+     */
160
+    before() {
161
+        multiremotebrowser.instances.forEach((instance: string) => {
162
+            initLogger(multiremotebrowser.getInstance(instance), instance, TEST_RESULTS_DIR);
163
+        });
164
+    },
165
+
166
+    /**
167
+     * Gets executed before the suite starts (in Mocha/Jasmine only).
168
+     *
169
+     * @param {Object} suite - Suite details.
170
+     * @returns {Promise<void>}
171
+     */
172
+    beforeSuite(suite) {
173
+        multiremotebrowser.instances.forEach((instance: string) => {
174
+            logInfo(multiremotebrowser.getInstance(instance),
175
+                `---=== Begin ${suite.file.substring(suite.file.lastIndexOf('/') + 1)} ===---`);
176
+        });
177
+    },
178
+
179
+    /**
180
+     * Function to be executed before a test (in Mocha/Jasmine only).
181
+     *
182
+     * @param {Object} test - Test object.
183
+     * @returns {Promise<void>}
184
+     */
185
+    beforeTest(test) {
186
+        multiremotebrowser.instances.forEach((instance: string) => {
187
+            logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.fullName} ===---`);
188
+        });
189
+    },
190
+
191
+    /**
192
+     * Function to be executed after a test (in Mocha/Jasmine only).
193
+     *
194
+     * @param {Object} test - Test object.
195
+     * @param {Object} context - Scope object the test was executed with.
196
+     * @param {Error}  error - Error object in case the test fails, otherwise `undefined`.
197
+     * @returns {Promise<void>}
198
+     */
199
+    async afterTest(test, context, { error }) {
200
+        multiremotebrowser.instances.forEach((instance: string) =>
201
+            logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.fullName} ===---`));
202
+
203
+        if (error) {
204
+            const allProcessing: Promise<any>[] = [];
205
+
206
+            multiremotebrowser.instances.forEach((instance: string) => {
207
+                const bInstance = multiremotebrowser.getInstance(instance);
208
+
209
+                allProcessing.push(bInstance.takeScreenshot().then(shot => {
210
+                    AllureReporter.addAttachment(
211
+                        `Screenshot-${instance}`,
212
+                        Buffer.from(shot, 'base64'),
213
+                        'image/png');
214
+                }));
215
+
216
+
217
+                AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');
218
+
219
+                allProcessing.push(bInstance.getPageSource().then(source => {
220
+                    AllureReporter.addAttachment(`html-source-${instance}`, source, 'text/plain');
221
+                }));
222
+            });
223
+
224
+            await Promise.all(allProcessing);
225
+        }
226
+    },
227
+
228
+    /**
229
+     * Hook that gets executed after the suite has ended (in Mocha/Jasmine only).
230
+     *
231
+     * @param {Object} suite - Suite details.
232
+     * @returns {Promise<void>}
233
+     */
234
+    afterSuite(suite) {
235
+        multiremotebrowser.instances.forEach((instance: string) => {
236
+            logInfo(multiremotebrowser.getInstance(instance),
237
+                `---=== End ${suite.file.substring(suite.file.lastIndexOf('/') + 1)} ===---`);
238
+        });
239
+    },
240
+
241
+    /**
242
+     * Gets executed after all workers have shut down and the process is about to exit.
243
+     * An error thrown in the `onComplete` hook will result in the test run failing.
244
+     *
245
+     * @returns {Promise<void>}
246
+     */
247
+    onComplete() {
248
+        const reportError = new Error('Could not generate Allure report');
249
+        const generation = allure([
250
+            'generate', `${TEST_RESULTS_DIR}/allure-results`,
251
+            '--clean', '--single-file',
252
+            '--report-dir', `${TEST_RESULTS_DIR}/allure-report`
253
+        ]);
254
+
255
+        return new Promise<void>((resolve, reject) => {
256
+            const generationTimeout = setTimeout(
257
+                () => reject(reportError),
258
+                5000);
259
+
260
+            // @ts-ignore
261
+            generation.on('exit', eCode => {
262
+                clearTimeout(generationTimeout);
263
+
264
+                if (eCode !== 0) {
265
+                    return reject(reportError);
266
+                }
267
+
268
+                console.log('Allure report successfully generated');
269
+                resolve();
270
+            });
271
+        });
272
+    }
273
+};

Loading…
Cancel
Save