mirror of
https://gitcode.com/GitHub_Trending/ji/jitsi-meet.git
synced 2025-12-30 03:12:29 +00:00
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.
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -109,3 +109,8 @@ react-native-sdk/node_modules
|
||||
react-native-sdk/react
|
||||
react-native-sdk/service
|
||||
react-native-sdk/sounds
|
||||
|
||||
# tests
|
||||
tests/.env
|
||||
test-results
|
||||
|
||||
|
||||
@@ -1629,7 +1629,6 @@ var config = {
|
||||
iAmRecorder
|
||||
iAmSipGateway
|
||||
microsoftApiApplicationClientID
|
||||
requireDisplayName
|
||||
*/
|
||||
|
||||
/**
|
||||
|
||||
8538
package-lock.json
generated
8538
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
12
package.json
12
package.json
@@ -49,6 +49,7 @@
|
||||
"@vladmandic/human-models": "2.5.9",
|
||||
"@xmldom/xmldom": "0.8.7",
|
||||
"abab": "2.0.6",
|
||||
"allure-commandline": "2.32.0",
|
||||
"amplitude-js": "8.21.9",
|
||||
"base64-js": "1.5.1",
|
||||
"bc-css-flags": "3.0.0",
|
||||
@@ -138,6 +139,7 @@
|
||||
"@types/amplitude-js": "8.16.5",
|
||||
"@types/audioworklet": "0.0.29",
|
||||
"@types/dom-screen-wake-lock": "1.0.1",
|
||||
"@types/jasmine": "5.1.4",
|
||||
"@types/js-md5": "0.4.3",
|
||||
"@types/lodash-es": "4.17.12",
|
||||
"@types/moment-duration-format": "2.2.6",
|
||||
@@ -158,6 +160,12 @@
|
||||
"@types/zxcvbn": "4.4.1",
|
||||
"@typescript-eslint/eslint-plugin": "5.59.5",
|
||||
"@typescript-eslint/parser": "5.59.5",
|
||||
"@wdio/allure-reporter": "9.2.14",
|
||||
"@wdio/cli": "9.2.14",
|
||||
"@wdio/globals": "9.2.14",
|
||||
"@wdio/jasmine-framework": "9.2.14",
|
||||
"@wdio/junit-reporter": "9.2.14",
|
||||
"@wdio/local-runner": "9.2.15",
|
||||
"babel-loader": "9.1.0",
|
||||
"babel-plugin-optional-require": "0.3.1",
|
||||
"circular-dependency-plugin": "5.2.0",
|
||||
@@ -179,6 +187,7 @@
|
||||
"ts-loader": "9.4.2",
|
||||
"typescript": "5.0.4",
|
||||
"unorm": "1.6.0",
|
||||
"webdriverio": "9.2.14",
|
||||
"webpack": "5.95.0",
|
||||
"webpack-bundle-analyzer": "4.4.2",
|
||||
"webpack-cli": "4.9.0",
|
||||
@@ -205,7 +214,8 @@
|
||||
"validate": "npm ls",
|
||||
"tsc-test:web": "tsc --project tsconfig.web.json --listFilesOnly | grep -v node_modules | grep native",
|
||||
"tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
|
||||
"start": "make dev"
|
||||
"start": "make dev",
|
||||
"test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts"
|
||||
},
|
||||
"resolutions": {
|
||||
"@types/react": "17.0.14",
|
||||
|
||||
15
tests/.eslintrc.js
Normal file
15
tests/.eslintrc.js
Normal file
@@ -0,0 +1,15 @@
|
||||
module.exports = {
|
||||
'extends': [
|
||||
'../.eslintrc.js'
|
||||
],
|
||||
'overrides': [
|
||||
{
|
||||
'files': [ '*.ts', '*.tsx' ],
|
||||
extends: [ '@jitsi/eslint-config/typescript' ],
|
||||
parserOptions: {
|
||||
sourceType: 'module',
|
||||
project: [ './tests/tsconfig.json' ]
|
||||
}
|
||||
}
|
||||
]
|
||||
};
|
||||
15
tests/env.example
Normal file
15
tests/env.example
Normal file
@@ -0,0 +1,15 @@
|
||||
# The base url that will be used for the test (default will be using "https://alpha.jitsi.net")
|
||||
#BASE_URL=
|
||||
|
||||
# To be able to match a domain to a specific address
|
||||
# The format is "MAP example.com 1.2.3.4"
|
||||
#RESOLVER_RULES=
|
||||
|
||||
# Ignore certificate errors (self-signed certificates)
|
||||
#ALLOW_INSECURE_CERTS=true
|
||||
|
||||
# Whether to run the browser in headless mode
|
||||
#HEADLESS=false
|
||||
|
||||
# The path to the browser video capture file
|
||||
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m
|
||||
289
tests/helpers/Participant.ts
Normal file
289
tests/helpers/Participant.ts
Normal file
@@ -0,0 +1,289 @@
|
||||
/* global APP $ */
|
||||
|
||||
import { multiremotebrowser } from '@wdio/globals';
|
||||
|
||||
import { IConfig } from '../../react/features/base/config/configType';
|
||||
import { urlObjectToString } from '../../react/features/base/util/uri';
|
||||
import Filmstrip from '../pageobjects/Filmstrip';
|
||||
import Toolbar from '../pageobjects/Toolbar';
|
||||
|
||||
import { LOG_PREFIX, logInfo } from './browserLogger';
|
||||
import { IContext } from './participants';
|
||||
|
||||
/**
|
||||
* Participant.
|
||||
*/
|
||||
export class Participant {
|
||||
/**
|
||||
* The current context.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private context: { roomName: string; };
|
||||
private _name: string;
|
||||
private _endpointId: string;
|
||||
|
||||
/**
|
||||
* The default config to use when joining.
|
||||
*
|
||||
* @private
|
||||
*/
|
||||
private config = {
|
||||
analytics: {
|
||||
disabled: true
|
||||
},
|
||||
debug: true,
|
||||
requireDisplayName: false,
|
||||
testing: {
|
||||
testMode: true
|
||||
},
|
||||
disableAP: true,
|
||||
disable1On1Mode: true,
|
||||
disableModeratorIndicator: true,
|
||||
enableTalkWhileMuted: false,
|
||||
gatherStats: true,
|
||||
p2p: {
|
||||
enabled: false,
|
||||
useStunTurn: false
|
||||
},
|
||||
pcStatsInterval: 1500,
|
||||
prejoinConfig: {
|
||||
enabled: false
|
||||
},
|
||||
toolbarConfig: {
|
||||
alwaysVisible: true
|
||||
}
|
||||
} as IConfig;
|
||||
|
||||
/**
|
||||
* Creates a participant with given name.
|
||||
*
|
||||
* @param {string} name - The name of the participant.
|
||||
*/
|
||||
constructor(name: string) {
|
||||
this._name = name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns participant endpoint ID.
|
||||
*
|
||||
* @returns {Promise<string>} The endpoint ID.
|
||||
*/
|
||||
async getEndpointId() {
|
||||
if (!this._endpointId) {
|
||||
this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
|
||||
return APP.conference.getMyUserId();
|
||||
});
|
||||
}
|
||||
|
||||
return this._endpointId;
|
||||
}
|
||||
|
||||
/**
|
||||
* The driver it uses.
|
||||
*/
|
||||
get driver() {
|
||||
return multiremotebrowser.getInstance(this._name);
|
||||
}
|
||||
|
||||
/**
|
||||
* The name.
|
||||
*/
|
||||
get name() {
|
||||
return this._name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a log to the participants log file.
|
||||
*
|
||||
* @param {string} message - The message to log.
|
||||
* @returns {void}
|
||||
*/
|
||||
log(message: string) {
|
||||
logInfo(this.driver, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins conference.
|
||||
*
|
||||
* @param {IContext} context - The context.
|
||||
* @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async joinConference(context: IContext, skipInMeetingChecks = false) {
|
||||
this.context = context;
|
||||
|
||||
const url = urlObjectToString({
|
||||
room: context.roomName,
|
||||
configOverwrite: this.config,
|
||||
interfaceConfigOverwrite: {
|
||||
SHOW_CHROME_EXTENSION_BANNER: false
|
||||
},
|
||||
userInfo: {
|
||||
displayName: this._name
|
||||
}
|
||||
}) || '';
|
||||
|
||||
await this.driver.setTimeout({ 'pageLoad': 30000 });
|
||||
|
||||
await this.driver.url(url);
|
||||
|
||||
await this.waitForPageToLoad();
|
||||
|
||||
await this.waitToJoinMUC();
|
||||
|
||||
await this.postLoadProcess(skipInMeetingChecks);
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads stuff after the page loads.
|
||||
*
|
||||
* @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
|
||||
* @returns {Promise<void>}
|
||||
* @private
|
||||
*/
|
||||
private async postLoadProcess(skipInMeetingChecks: boolean) {
|
||||
const driver = this.driver;
|
||||
|
||||
const parallel = [];
|
||||
|
||||
parallel.push(driver.execute((name, sessionId, prefix) => {
|
||||
APP.UI.dockToolbar(true);
|
||||
|
||||
// disable keyframe animations (.fadeIn and .fadeOut classes)
|
||||
$('<style>.notransition * { '
|
||||
+ 'animation-duration: 0s !important; -webkit-animation-duration: 0s !important; transition:none; '
|
||||
+ '} </style>') // @ts-ignore
|
||||
.appendTo(document.head);
|
||||
|
||||
// @ts-ignore
|
||||
$('body').toggleClass('notransition');
|
||||
|
||||
document.title = `${name}`;
|
||||
|
||||
console.log(`${new Date().toISOString()} ${prefix} sessionId: ${sessionId}`);
|
||||
|
||||
// disable the blur effect in firefox as it has some performance issues
|
||||
const blur = document.querySelector('.video_blurred_container');
|
||||
|
||||
if (blur) {
|
||||
// @ts-ignore
|
||||
document.querySelector('.video_blurred_container').style.display = 'none';
|
||||
}
|
||||
}, this._name, driver.sessionId, LOG_PREFIX));
|
||||
|
||||
if (skipInMeetingChecks) {
|
||||
await Promise.allSettled(parallel);
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
parallel.push(this.waitForIceConnected());
|
||||
parallel.push(this.waitForSendReceiveData());
|
||||
|
||||
await Promise.all(parallel);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the page to load.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForPageToLoad() {
|
||||
return this.driver.waitUntil(
|
||||
() => this.driver.execute(() => document.readyState === 'complete'),
|
||||
{
|
||||
timeout: 30_000, // 30 seconds
|
||||
timeoutMsg: 'Timeout waiting for Page Load Request to complete.'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits to join the muc.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitToJoinMUC() {
|
||||
return this.driver.waitUntil(
|
||||
() => this.driver.execute(() => APP.conference.isJoined()),
|
||||
{
|
||||
timeout: 10_000, // 10 seconds
|
||||
timeoutMsg: 'Timeout waiting to join muc.'
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for ICE to get connected.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForIceConnected() {
|
||||
const driver = this.driver;
|
||||
|
||||
return driver.waitUntil(async () =>
|
||||
driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
|
||||
timeout: 15_000,
|
||||
timeoutMsg: 'expected ICE to be connected for 15s'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for send and receive data.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async waitForSendReceiveData() {
|
||||
const driver = this.driver;
|
||||
|
||||
return driver.waitUntil(async () =>
|
||||
driver.execute(() => {
|
||||
const stats = APP.conference.getStats();
|
||||
const bitrateMap = stats?.bitrate || {};
|
||||
const rtpStats = {
|
||||
uploadBitrate: bitrateMap.upload || 0,
|
||||
downloadBitrate: bitrateMap.download || 0
|
||||
};
|
||||
|
||||
return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
|
||||
}), {
|
||||
timeout: 15_000,
|
||||
timeoutMsg: 'expected to receive/send data in 15s'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for remote streams.
|
||||
*
|
||||
* @param {number} number - The number of remote streams o wait for.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
waitForRemoteStreams(number: number) {
|
||||
const driver = this.driver;
|
||||
|
||||
return driver.waitUntil(async () =>
|
||||
driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
|
||||
timeout: 15_000,
|
||||
timeoutMsg: 'expected remote streams in 15s'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the toolbar for this participant.
|
||||
*
|
||||
* @returns {Toolbar}
|
||||
*/
|
||||
getToolbar() {
|
||||
return new Toolbar(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the filmstrip for this participant.
|
||||
*
|
||||
* @returns {Filmstrip}
|
||||
*/
|
||||
getFilmstrip() {
|
||||
return new Filmstrip(this);
|
||||
}
|
||||
}
|
||||
67
tests/helpers/browserLogger.ts
Normal file
67
tests/helpers/browserLogger.ts
Normal file
@@ -0,0 +1,67 @@
|
||||
import fs from 'node:fs';
|
||||
|
||||
/**
|
||||
* A prefix to use for all messages we add to the console log.
|
||||
*/
|
||||
export const LOG_PREFIX = '[MeetTest] ';
|
||||
|
||||
/**
|
||||
* Initialize logger for a driver.
|
||||
*
|
||||
* @param {WebdriverIO.Browser} driver - The driver.
|
||||
* @param {string} name - The name of the participant.
|
||||
* @param {string} folder - The folder to save the file.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function initLogger(driver: WebdriverIO.Browser, name: string, folder: string) {
|
||||
// @ts-ignore
|
||||
driver.logFile = `${folder}/${name}.log`;
|
||||
driver.sessionSubscribe({ events: [ 'log.entryAdded' ] });
|
||||
|
||||
driver.on('log.entryAdded', (entry: any) => {
|
||||
try {
|
||||
// @ts-ignore
|
||||
fs.appendFileSync(driver.logFile, `${entry.text}\n`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the content of the log file.
|
||||
*
|
||||
* @param {WebdriverIO.Browser} driver - The driver which log file is requested.
|
||||
* @returns {string} The content of the log file.
|
||||
*/
|
||||
export function getLogs(driver: WebdriverIO.Browser) {
|
||||
// @ts-ignore
|
||||
if (!driver.logFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return fs.readFileSync(driver.logFile, 'utf8');
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message in the logfile.
|
||||
*
|
||||
* @param {WebdriverIO.Browser} driver - The participant in which log file to write.
|
||||
* @param {string} message - The message to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function logInfo(driver: WebdriverIO.Browser, message: string) {
|
||||
// @ts-ignore
|
||||
if (!driver.logFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
fs.appendFileSync(driver.logFile, `${new Date().toISOString()} ${LOG_PREFIX} ${message}\n`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
80
tests/helpers/participants.ts
Normal file
80
tests/helpers/participants.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { Participant } from './Participant';
|
||||
|
||||
export type IContext = {
|
||||
p1: Participant;
|
||||
p2: Participant;
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
roomName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate a random room name.
|
||||
*
|
||||
* @returns {string} - The random room name.
|
||||
*/
|
||||
function generateRandomRoomName(): string {
|
||||
return `jitsimeettorture-${crypto.randomUUID()}}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there is on participant.
|
||||
*
|
||||
* @param {IContext} context - The context.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureOneParticipant(context: IContext): Promise<void> {
|
||||
context.roomName = generateRandomRoomName();
|
||||
|
||||
context.p1 = new Participant('participant1');
|
||||
|
||||
await context.p1.joinConference(context, true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are three participants.
|
||||
*
|
||||
* @param {Object} context - The context.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureThreeParticipants(context: IContext): Promise<void> {
|
||||
context.roomName = generateRandomRoomName();
|
||||
|
||||
const p1 = new Participant('participant1');
|
||||
const p2 = new Participant('participant2');
|
||||
const p3 = new Participant('participant3');
|
||||
|
||||
context.p1 = p1;
|
||||
context.p2 = p2;
|
||||
context.p3 = p3;
|
||||
|
||||
// these need to be all, so we get the error when one fails
|
||||
await Promise.all([
|
||||
p1.joinConference(context),
|
||||
p2.joinConference(context),
|
||||
p3.joinConference(context)
|
||||
]);
|
||||
|
||||
await Promise.all([
|
||||
p1.waitForRemoteStreams(2),
|
||||
p2.waitForRemoteStreams(2),
|
||||
p3.waitForRemoteStreams(2)
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
|
||||
* conference participants sees a specific mute state for the former.
|
||||
*
|
||||
* @param {Participant} testee - The {@code Participant} which represents the Meet conference participant whose
|
||||
* mute state is to be toggled.
|
||||
* @param {Participant} observer - The {@code Participant} which represents the Meet conference participant to verify
|
||||
* the mute state of {@code testee}.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function toggleMuteAndCheck(testee: Participant, observer: Participant): Promise<void> {
|
||||
await testee.getToolbar().clickAudioMuteButton();
|
||||
|
||||
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
|
||||
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
|
||||
}
|
||||
46
tests/pageobjects/Filmstrip.ts
Normal file
46
tests/pageobjects/Filmstrip.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
/**
|
||||
* Filmstrip elements.
|
||||
*/
|
||||
export default class Filmstrip {
|
||||
private participant: Participant;
|
||||
|
||||
/**
|
||||
* Initializes for a participant.
|
||||
*
|
||||
* @param {Participant} participant - The participant.
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that {@code participant} shows or doesn't show the audio
|
||||
* mute icon for the conference participant identified by
|
||||
* {@code testee}.
|
||||
*
|
||||
* @param {Participant} testee - The {@code WebParticipant} for whom we're checking the status of audio muted icon.
|
||||
* @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
|
||||
* otherwise, it will assert its presence.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false) {
|
||||
let id;
|
||||
|
||||
if (testee === this.participant) {
|
||||
id = 'localVideoContainer';
|
||||
} else {
|
||||
id = `participant_${await testee.getEndpointId()}`;
|
||||
}
|
||||
|
||||
const mutedIconXPath
|
||||
= `//span[@id='${id}']//span[contains(@id, 'audioMuted')]//*[local-name()='svg' and @id='mic-disabled']`;
|
||||
|
||||
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
|
||||
reverse,
|
||||
timeout: 2000,
|
||||
timeoutMsg: `Audio mute icon is not displayed for ${testee.name}`
|
||||
});
|
||||
}
|
||||
}
|
||||
66
tests/pageobjects/Toolbar.ts
Normal file
66
tests/pageobjects/Toolbar.ts
Normal file
@@ -0,0 +1,66 @@
|
||||
// eslint-disable-next-line no-unused-vars
|
||||
import { Participant } from '../helpers/Participant';
|
||||
|
||||
const AUDIO_MUTE = 'Mute microphone';
|
||||
const AUDIO_UNMUTE = 'Unmute microphone';
|
||||
|
||||
/**
|
||||
* The toolbar elements.
|
||||
*/
|
||||
export default class Toolbar {
|
||||
private participant: Participant;
|
||||
|
||||
/**
|
||||
* Creates toolbar for a participant.
|
||||
*
|
||||
* @param {Participant} participant - The participants.
|
||||
*/
|
||||
constructor(participant: Participant) {
|
||||
this.participant = participant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the button.
|
||||
*
|
||||
* @param {string} accessibilityCSSSelector - The selector to find the button.
|
||||
* @returns {WebdriverIO.Element} The button.
|
||||
* @private
|
||||
*/
|
||||
private getButton(accessibilityCSSSelector: string) {
|
||||
return this.participant.driver.$(`[aria-label^="${accessibilityCSSSelector}"]`);
|
||||
}
|
||||
|
||||
/**
|
||||
* The audio mute button.
|
||||
*/
|
||||
get audioMuteBtn() {
|
||||
return this.getButton(AUDIO_MUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* The audio unmute button.
|
||||
*/
|
||||
get audioUnMuteBtn() {
|
||||
return this.getButton(AUDIO_UNMUTE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks audio mute button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clickAudioMuteButton() {
|
||||
await this.participant.log('Clicking on: Audio Mute Button');
|
||||
await this.audioMuteBtn.click();
|
||||
}
|
||||
|
||||
/**
|
||||
* Clicks audio unmute button.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async clickAudioUnmuteButton() {
|
||||
await this.participant.log('Clicking on: Audio Unmute Button');
|
||||
await this.audioUnMuteBtn.click();
|
||||
}
|
||||
}
|
||||
94
tests/specs/3way/activeSpeaker.spec.ts
Normal file
94
tests/specs/3way/activeSpeaker.spec.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
/* global APP */
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { IContext, ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants';
|
||||
|
||||
describe('ActiveSpeaker ', () => {
|
||||
const context = {} as IContext;
|
||||
|
||||
it('testActiveSpeaker', async () => {
|
||||
await ensureThreeParticipants(context);
|
||||
|
||||
await toggleMuteAndCheck(context.p1, context.p2);
|
||||
await toggleMuteAndCheck(context.p2, context.p1);
|
||||
await toggleMuteAndCheck(context.p3, context.p1);
|
||||
|
||||
// participant1 becomes active speaker - check from participant2's perspective
|
||||
await testActiveSpeaker(context.p1, context.p2, context.p3);
|
||||
|
||||
// participant3 becomes active speaker - check from participant2's perspective
|
||||
await testActiveSpeaker(context.p3, context.p2, context.p1);
|
||||
|
||||
// participant2 becomes active speaker - check from participant1's perspective
|
||||
await testActiveSpeaker(context.p2, context.p1, context.p3);
|
||||
|
||||
// check the displayed speakers, there should be only one speaker
|
||||
await assertOneDominantSpeaker(context.p1);
|
||||
await assertOneDominantSpeaker(context.p2);
|
||||
await assertOneDominantSpeaker(context.p3);
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Tries to make given participant an active speaker by un-muting it.
|
||||
* Verifies from {@code participant2}'s perspective that the active speaker
|
||||
* has been displayed on the large video area. Mutes him back.
|
||||
*
|
||||
* @param {Participant} activeSpeaker - <tt>Participant</tt> instance of the participant who will be tested as an
|
||||
* active speaker.
|
||||
* @param {Participant} otherParticipant1 - <tt>Participant</tt> of the participant who will be observing and verifying
|
||||
* active speaker change.
|
||||
* @param {Participant} otherParticipant2 - Used only to print some debugging info.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function testActiveSpeaker(
|
||||
activeSpeaker: Participant, otherParticipant1: Participant, otherParticipant2: Participant) {
|
||||
activeSpeaker.log(`Start testActiveSpeaker for participant: ${activeSpeaker.name}`);
|
||||
|
||||
const speakerEndpoint = await activeSpeaker.getEndpointId();
|
||||
|
||||
// just a debug print to go in logs
|
||||
activeSpeaker.log('Unmuting in testActiveSpeaker');
|
||||
|
||||
// Unmute
|
||||
await activeSpeaker.getToolbar().clickAudioUnmuteButton();
|
||||
|
||||
// just a debug print to go in logs
|
||||
otherParticipant1.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
otherParticipant2.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
|
||||
await activeSpeaker.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker, true);
|
||||
|
||||
// Verify that the user is now an active speaker from otherParticipant1's perspective
|
||||
const otherParticipant1Driver = otherParticipant1.driver;
|
||||
|
||||
await otherParticipant1Driver.waitUntil(
|
||||
() => otherParticipant1Driver.execute((id: string) => APP.UI.getLargeVideoID() === id, speakerEndpoint),
|
||||
{
|
||||
timeout: 30_1000, // 30 seconds
|
||||
timeoutMsg: 'Active speaker not displayed on large video.'
|
||||
});
|
||||
|
||||
// just a debug print to go in logs
|
||||
activeSpeaker.log('Muting in testActiveSpeaker');
|
||||
|
||||
// Mute back again
|
||||
await activeSpeaker.getToolbar().clickAudioMuteButton();
|
||||
|
||||
// just a debug print to go in logs
|
||||
otherParticipant1.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
otherParticipant2.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
|
||||
|
||||
await otherParticipant1.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker);
|
||||
}
|
||||
|
||||
/**
|
||||
* Asserts that the number of small videos with the dominant speaker
|
||||
* indicator displayed equals 1.
|
||||
*
|
||||
* @param {Participant} participant - The participant to check.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async function assertOneDominantSpeaker(participant: Participant) {
|
||||
expect(await participant.driver.$$(
|
||||
'//span[not(contains(@class, "tile-view"))]//span[contains(@class,"dominant-speaker")]').length).toBe(1);
|
||||
}
|
||||
12
tests/tsconfig.json
Normal file
12
tests/tsconfig.json
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"include": ["**/*.ts", "../globals.d.ts"],
|
||||
"extends": "../tsconfig.web",
|
||||
"compilerOptions": {
|
||||
"types": [
|
||||
"node",
|
||||
"@wdio/globals/types",
|
||||
"@types/jasmine",
|
||||
"@wdio/jasmine-framework"
|
||||
]
|
||||
}
|
||||
}
|
||||
273
tests/wdio.conf.ts
Normal file
273
tests/wdio.conf.ts
Normal file
@@ -0,0 +1,273 @@
|
||||
import AllureReporter from '@wdio/allure-reporter';
|
||||
import { multiremotebrowser } from '@wdio/globals';
|
||||
import { Buffer } from 'buffer';
|
||||
import process from 'node:process';
|
||||
|
||||
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const allure = require('allure-commandline');
|
||||
|
||||
// This is deprecated without alternative (https://github.com/nodejs/node/issues/32483)
|
||||
// we need it to be able to reuse jitsi-meet code in tests
|
||||
require.extensions['.web.ts'] = require.extensions['.ts'];
|
||||
|
||||
const chromeArgs = [
|
||||
'--allow-insecure-localhost',
|
||||
'--use-fake-ui-for-media-stream',
|
||||
'--use-fake-device-for-media-stream',
|
||||
'--disable-plugins',
|
||||
'--mute-audio',
|
||||
'--disable-infobars',
|
||||
'--autoplay-policy=no-user-gesture-required',
|
||||
'--auto-select-desktop-capture-source=Your Entire screen',
|
||||
'--no-sandbox',
|
||||
'--disable-dev-shm-usage',
|
||||
'--disable-setuid-sandbox',
|
||||
'--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav'
|
||||
];
|
||||
|
||||
if (process.env.RESOLVER_RULES) {
|
||||
chromeArgs.push(`--host-resolver-rules=${process.env.RESOLVER_RULES}`);
|
||||
}
|
||||
if (process.env.ALLOW_INSECURE_CERTS === 'true') {
|
||||
chromeArgs.push('--ignore-certificate-errors');
|
||||
}
|
||||
if (process.env.HEADLESS === 'true') {
|
||||
chromeArgs.push('--headless');
|
||||
chromeArgs.push('--window-size=1280,720');
|
||||
}
|
||||
if (process.env.VIDEO_CAPTURE_FILE) {
|
||||
chromeArgs.push(`use-file-for-fake-video-capture=${process.env.VIDEO_CAPTURE_FILE}`);
|
||||
}
|
||||
|
||||
const chromePreferences = {
|
||||
'intl.accept_languages': 'en-US'
|
||||
};
|
||||
|
||||
const TEST_RESULTS_DIR = 'test-results';
|
||||
|
||||
export const config: WebdriverIO.MultiremoteConfig = {
|
||||
|
||||
runner: 'local',
|
||||
|
||||
specs: [
|
||||
'specs/**'
|
||||
],
|
||||
maxInstances: 1,
|
||||
|
||||
baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture',
|
||||
tsConfigPath: './tsconfig.json',
|
||||
|
||||
// Default timeout for all waitForXXX commands.
|
||||
waitforTimeout: 1000,
|
||||
|
||||
// Default timeout in milliseconds for request
|
||||
// if browser driver or grid doesn't send response
|
||||
connectionRetryTimeout: 15_000,
|
||||
|
||||
// Default request retries count
|
||||
connectionRetryCount: 3,
|
||||
|
||||
framework: 'jasmine',
|
||||
|
||||
jasmineOpts: {
|
||||
defaultTimeoutInterval: 60_000
|
||||
},
|
||||
|
||||
capabilities: {
|
||||
participant1: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: chromeArgs,
|
||||
prefs: chromePreferences
|
||||
}
|
||||
}
|
||||
},
|
||||
participant2: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: chromeArgs,
|
||||
prefs: chromePreferences
|
||||
},
|
||||
'wdio:exclude': [
|
||||
'specs/alone/**'
|
||||
]
|
||||
}
|
||||
},
|
||||
participant3: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: chromeArgs,
|
||||
prefs: chromePreferences
|
||||
},
|
||||
'wdio:exclude': [
|
||||
'specs/2way/**'
|
||||
]
|
||||
}
|
||||
},
|
||||
participant4: {
|
||||
capabilities: {
|
||||
browserName: 'chrome',
|
||||
'goog:chromeOptions': {
|
||||
args: chromeArgs,
|
||||
prefs: chromePreferences
|
||||
},
|
||||
'wdio:exclude': [
|
||||
'specs/3way/**'
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
// Level of logging verbosity: trace | debug | info | warn | error | silent
|
||||
logLevel: 'trace',
|
||||
logLevels: {
|
||||
webdriver: 'info'
|
||||
},
|
||||
|
||||
// Set directory to store all logs into
|
||||
outputDir: TEST_RESULTS_DIR,
|
||||
|
||||
reporters: [
|
||||
[ 'junit', {
|
||||
outputDir: TEST_RESULTS_DIR,
|
||||
outputFileFormat(options) { // optional
|
||||
return `results-${options.cid}.xml`;
|
||||
}
|
||||
} ],
|
||||
[ 'allure', {
|
||||
// addConsoleLogs: true,
|
||||
outputDir: `${TEST_RESULTS_DIR}/allure-results`,
|
||||
disableWebdriverStepsReporting: true,
|
||||
disableWebdriverScreenshotsReporting: true,
|
||||
useCucumberStepReporter: false
|
||||
} ]
|
||||
],
|
||||
|
||||
// =====
|
||||
// Hooks
|
||||
// =====
|
||||
/**
|
||||
* Gets executed before test execution begins. At this point you can access to all global
|
||||
* variables like `browser`. It is the perfect place to define custom commands.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
before() {
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
initLogger(multiremotebrowser.getInstance(instance), instance, TEST_RESULTS_DIR);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets executed before the suite starts (in Mocha/Jasmine only).
|
||||
*
|
||||
* @param {Object} suite - Suite details.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
beforeSuite(suite) {
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
logInfo(multiremotebrowser.getInstance(instance),
|
||||
`---=== Begin ${suite.file.substring(suite.file.lastIndexOf('/') + 1)} ===---`);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to be executed before a test (in Mocha/Jasmine only).
|
||||
*
|
||||
* @param {Object} test - Test object.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
beforeTest(test) {
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.fullName} ===---`);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to be executed after a test (in Mocha/Jasmine only).
|
||||
*
|
||||
* @param {Object} test - Test object.
|
||||
* @param {Object} context - Scope object the test was executed with.
|
||||
* @param {Error} error - Error object in case the test fails, otherwise `undefined`.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async afterTest(test, context, { error }) {
|
||||
multiremotebrowser.instances.forEach((instance: string) =>
|
||||
logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.fullName} ===---`));
|
||||
|
||||
if (error) {
|
||||
const allProcessing: Promise<any>[] = [];
|
||||
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
const bInstance = multiremotebrowser.getInstance(instance);
|
||||
|
||||
allProcessing.push(bInstance.takeScreenshot().then(shot => {
|
||||
AllureReporter.addAttachment(
|
||||
`Screenshot-${instance}`,
|
||||
Buffer.from(shot, 'base64'),
|
||||
'image/png');
|
||||
}));
|
||||
|
||||
|
||||
AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');
|
||||
|
||||
allProcessing.push(bInstance.getPageSource().then(source => {
|
||||
AllureReporter.addAttachment(`html-source-${instance}`, source, 'text/plain');
|
||||
}));
|
||||
});
|
||||
|
||||
await Promise.all(allProcessing);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Hook that gets executed after the suite has ended (in Mocha/Jasmine only).
|
||||
*
|
||||
* @param {Object} suite - Suite details.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
afterSuite(suite) {
|
||||
multiremotebrowser.instances.forEach((instance: string) => {
|
||||
logInfo(multiremotebrowser.getInstance(instance),
|
||||
`---=== End ${suite.file.substring(suite.file.lastIndexOf('/') + 1)} ===---`);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets executed after all workers have shut down and the process is about to exit.
|
||||
* An error thrown in the `onComplete` hook will result in the test run failing.
|
||||
*
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
onComplete() {
|
||||
const reportError = new Error('Could not generate Allure report');
|
||||
const generation = allure([
|
||||
'generate', `${TEST_RESULTS_DIR}/allure-results`,
|
||||
'--clean', '--single-file',
|
||||
'--report-dir', `${TEST_RESULTS_DIR}/allure-report`
|
||||
]);
|
||||
|
||||
return new Promise<void>((resolve, reject) => {
|
||||
const generationTimeout = setTimeout(
|
||||
() => reject(reportError),
|
||||
5000);
|
||||
|
||||
// @ts-ignore
|
||||
generation.on('exit', eCode => {
|
||||
clearTimeout(generationTimeout);
|
||||
|
||||
if (eCode !== 0) {
|
||||
return reject(reportError);
|
||||
}
|
||||
|
||||
console.log('Allure report successfully generated');
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user