Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Test improvements #15609

Merged
merged 16 commits into from
Feb 21, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,9 @@
"test-dev": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts",
"test-dev-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.dev.conf.ts --spec",
"test-grid": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts",
"test-grid-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts --spec"
"test-grid-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.conf.ts --spec",
"test-grid-ff": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.firefox.conf.ts",
"test-grid-ff-single": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.grid.firefox.conf.ts --spec"
},
"resolutions": {
"@types/react": "17.0.14",
Expand Down
3 changes: 3 additions & 0 deletions tests/env.example
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@
# The kid to use in the token
#JWT_KID=

# The count of workers that execute the tests in parallel
# MAX_INSTANCES=1

# The address of the webhooks proxy used to test the webhooks feature (e.g. wss://your.service/?tenant=sometenant)
#WEBHOOKS_PROXY_URL=
# A shared secret to authenticate the webhook proxy connection
Expand Down
6 changes: 3 additions & 3 deletions tests/helpers/browserLogger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,13 @@ 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} fileName - The name of the file.
* @param {string} folder - The folder to save the file.
* @returns {void}
*/
export function initLogger(driver: WebdriverIO.Browser, name: string, folder: string) {
export function initLogger(driver: WebdriverIO.Browser, fileName: string, folder: string) {
// @ts-ignore
driver.logFile = `${folder}/${name}.log`;
driver.logFile = `${folder}/${fileName}.log`;
driver.sessionSubscribe({ events: [ 'log.entryAdded' ] });

driver.on('log.entryAdded', (entry: any) => {
Expand Down
3 changes: 2 additions & 1 deletion tests/helpers/participants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,8 @@ export async function muteAudioAndCheck(testee: Participant, observer: Participa
* @param observer
*/
export async function unmuteAudioAndCheck(testee: Participant, observer: Participant) {
await testee.getNotifications().closeAskToUnmuteNotification();
await testee.getNotifications().closeAskToUnmuteNotification(true);
await testee.getNotifications().closeAVModerationMutedNotification(true);
await testee.getToolbar().clickAudioUnmuteButton();
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee, true);
Expand Down
1 change: 1 addition & 0 deletions tests/helpers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ export type IContext = {
iframeAPI: boolean;
jwtKid: string;
jwtPrivateKeyPath: string;
keepAlive: Array<any>;
p1: Participant;
p2: Participant;
p3: Participant;
Expand Down
3 changes: 1 addition & 2 deletions tests/pageobjects/LargeVideo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,10 +46,9 @@ export default class LargeVideo extends BasePageObject {
* Returns the source of the large video currently shown.
*/
getId() {
return this.participant.execute('return document.getElementById("largeVideo").srcObject.id');
return this.participant.execute(() => document.getElementById('largeVideo')?.srcObject?.id);
}


/**
* Checks if the large video is playing or not.
*
Expand Down
34 changes: 32 additions & 2 deletions tests/pageobjects/Notifications.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import BasePageObject from './BasePageObject';

const AV_MODERATION_MUTED_NOTIFICATION_ID = 'notify.moderationInEffectTitle';
const ASK_TO_UNMUTE_NOTIFICATION_ID = 'notify.hostAskedUnmute';
const JOIN_ONE_TEST_ID = 'notify.connectedOneMember';
const JOIN_TWO_TEST_ID = 'notify.connectedTwoMembers';
Expand Down Expand Up @@ -47,8 +48,15 @@ export default class Notifications extends BasePageObject {
/**
* Closes the ask to unmute notification.
*/
async closeAskToUnmuteNotification() {
return this.closeLobbyNotification(ASK_TO_UNMUTE_NOTIFICATION_ID);
async closeAVModerationMutedNotification(skipNonExisting = false) {
return this.closeNotification(AV_MODERATION_MUTED_NOTIFICATION_ID, skipNonExisting);
}

/**
* Closes the ask to unmute notification.
*/
async closeAskToUnmuteNotification(skipNonExisting = false) {
return this.closeNotification(ASK_TO_UNMUTE_NOTIFICATION_ID, skipNonExisting);
}

/**
Expand Down Expand Up @@ -86,6 +94,28 @@ export default class Notifications extends BasePageObject {
return this.getNotificationText(LOBBY_ENABLED_TEST_ID);
}

/**
* Closes a specific lobby notification.
* @param testId
* @param skipNonExisting
* @private
*/
private async closeNotification(testId: string, skipNonExisting = false) {
const notification = this.participant.driver.$(`[data-testid="${testId}"]`);

if (skipNonExisting && !await notification.isExisting()) {
return Promise.resolve();
}

await notification.waitForExist();
await notification.waitForStable();

const closeButton = notification.$('#close-notification');

await closeButton.moveTo();
await closeButton.click();
}

/**
* Closes a specific lobby notification.
* @param testId
Expand Down
2 changes: 1 addition & 1 deletion tests/pageobjects/ParticipantsPane.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export default class ParticipantsPane extends BasePageObject {
await this.openParticipantContextMenu(participantToUnmute);

const unmuteButton = this.participant.driver
.$(`button[data-testid="unmute-video-${participantId}"]`);
.$(`[data-testid="unmute-video-${participantId}"]`);

await unmuteButton.waitForExist();
await unmuteButton.click();
Expand Down
17 changes: 0 additions & 17 deletions tests/pageobjects/SecurityDialog.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import { Key } from 'webdriverio';

import BaseDialog from './BaseDialog';

const ADD_PASSWORD_LINK = 'add-password';
Expand Down Expand Up @@ -118,21 +116,6 @@ export default class SecurityDialog extends BaseDialog {

await this.participant.driver.keys(password);
await this.participant.driver.$('button=Add').click();

let validationMessage;

// There are two cases here, validation is enabled and the field passwordEntry maybe there
// with validation failed, or maybe successfully hidden after setting the password
// So let's give it some time to act on any of the above
if (!await passwordEntry.isExisting()) {
// validation had failed on password field as it is still on the page
validationMessage = passwordEntry.getAttribute('validationMessage');
}

if (validationMessage) {
await this.participant.driver.keys([ Key.Escape ]);
expect(validationMessage).toBe('');
}
}

/**
Expand Down
7 changes: 4 additions & 3 deletions tests/specs/3way/audioVideoModeration.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,8 +70,10 @@ describe('AVModeration', () => {
// participant3 was unmuted by unmuteByModerator
await unmuteAudioAndCheck(p2, p1);
await unmuteVideoAndCheck(p2, p1);
await unmuteAudioAndCheck(p1, p2);
await unmuteVideoAndCheck(p1, p2);

// make sure p1 is not muted after turning on and then off the AV moderation
await p1.getFilmstrip().assertAudioMuteIconIsDisplayed(p1, true);
await p2.getFilmstrip().assertAudioMuteIconIsDisplayed(p2, true);
});

it('hangup and change moderator', async () => {
Expand Down Expand Up @@ -244,7 +246,6 @@ async function unmuteByModerator(
await moderatorParticipantsPane.allowVideo(participant);
await moderatorParticipantsPane.askToUnmute(participant, false);
await participant.getNotifications().waitForAskToUnmuteNotification();
await participant.getNotifications().closeAskToUnmuteNotification();

await unmuteAudioAndCheck(participant, moderator);
await unmuteVideoAndCheck(participant, moderator);
Expand Down
20 changes: 19 additions & 1 deletion tests/specs/3way/avatars.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,21 @@ describe('Avatar', () => {
await p1.getParticipantsPane().assertVideoMuteIconIsDisplayed(p2);

// Start the third participant
await ensureThreeParticipants(ctx);
await ensureThreeParticipants(ctx, {
skipInMeetingChecks: true
});

const { p3 } = ctx;

// When the first participant is FF because of their audio mic feed it will never become dominant speaker
// and no audio track will be received by the third participant and video is muted,
// that's why we need to do a different check that expects any track just from p2
if (p1.driver.isFirefox) {
await Promise.all([ p2.waitForRemoteStreams(1), p3.waitForRemoteStreams(1) ]);
} else {
await Promise.all([ p2.waitForRemoteStreams(2), p3.waitForRemoteStreams(2) ]);
}

// Pin local video and verify avatars are displayed
await p3.getFilmstrip().pinParticipant(p3);

Expand Down Expand Up @@ -179,6 +191,12 @@ describe('Avatar', () => {
it('email persistence', async () => {
let { p1 } = ctx;

if (p1.driver.isFirefox) {
// strangely this test when FF is involved, missing source mapping from jvb
// and fails with an error of: expected number of remote streams:1 in 15s for participant1
return;
}

await p1.getToolbar().clickProfileButton();

expect(await p1.getSettingsDialog().getEmail()).toBe(EMAIL);
Expand Down
3 changes: 1 addition & 2 deletions tests/specs/3way/lobby.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,14 +179,13 @@ describe('Lobby', () => {
await enableLobby();
await enterLobby(p1);

// WebParticipant participant1 = getParticipant1();
const p1SecurityDialog = p1.getSecurityDialog();

await p1.getToolbar().clickSecurityButton();
await p1SecurityDialog.waitForDisplay();

await p1SecurityDialog.toggleLobby();
await p1SecurityDialog.waitForLobbyEnabled();
await p1SecurityDialog.waitForLobbyEnabled(true);

const { p3 } = ctx;

Expand Down
75 changes: 58 additions & 17 deletions tests/wdio.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,6 @@ const allure = require('allure-commandline');
// we need it to be able to reuse jitsi-meet code in tests
require.extensions['.web.ts'] = require.extensions['.ts'];

const usingGrid = Boolean(new URL(import.meta.url).searchParams.get('grid'));

const chromeArgs = [
'--allow-insecure-localhost',
'--use-fake-ui-for-media-stream',
Expand All @@ -35,8 +33,7 @@ const chromeArgs = [
// Avoids - "You are checking for animations on an inactive tab, animations do not run for inactive tabs"
// when executing waitForStable()
'--disable-renderer-backgrounding',
`--use-file-for-fake-audio-capture=${
usingGrid ? process.env.REMOTE_RESOURCE_PATH : 'tests/resources'}/fakeAudioStream.wav`
'--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav'
];

if (process.env.RESOLVER_RULES) {
Expand All @@ -47,7 +44,7 @@ if (process.env.ALLOW_INSECURE_CERTS === 'true') {
}
if (process.env.HEADLESS === 'true') {
chromeArgs.push('--headless');
chromeArgs.push('--window-size=1280,720');
chromeArgs.push('--window-size=1280,1024');
}
if (process.env.VIDEO_CAPTURE_FILE) {
chromeArgs.push(`--use-file-for-fake-video-capture=${process.env.VIDEO_CAPTURE_FILE}`);
Expand All @@ -66,7 +63,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
specs: [
'specs/**'
],
maxInstances: 1, // if changing check onWorkerStart logic
maxInstances: parseInt(process.env.MAX_INSTANCES || '1', 10), // if changing check onWorkerStart logic

baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture/',
tsConfigPath: './tsconfig.json',
Expand All @@ -84,7 +81,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
framework: 'mocha',

mochaOpts: {
timeout: 60_000
timeout: 180_000
},

capabilities: {
Expand Down Expand Up @@ -169,14 +166,36 @@ export const config: WebdriverIO.MultiremoteConfig = {
/**
* 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.
* We have overriden this function in beforeSession to be able to pass cid as first param.
*
* @returns {Promise<void>}
*/
async before() {
async before(cid, _, specs) {
if (specs.length !== 1) {
console.warn('We expect to run a single suite, but got more than one');
}

const testName = path.basename(specs[0]).replace('.spec.ts', '');

console.log(`Running test: ${testName} via worker: ${cid}`);

const globalAny: any = global;

globalAny.ctx = {
times: {}
} as IContext;
globalAny.ctx.keepAlive = [];

await Promise.all(multiremotebrowser.instances.map(async (instance: string) => {
const bInstance = multiremotebrowser.getInstance(instance);

initLogger(bInstance, instance, TEST_RESULTS_DIR);
// @ts-ignore
initLogger(bInstance, `${instance}-${cid}-${testName}`, TEST_RESULTS_DIR);

// setup keepalive
globalAny.ctx.keepAlive.push(setInterval(async () => {
await bInstance.execute(() => console.log('keep-alive'));
}, 20_000));

if (bInstance.isFirefox) {
return;
Expand All @@ -188,13 +207,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
bInstance.iframePageBase = `file://${path.dirname(rpath)}`;
}));

const globalAny: any = global;
const roomName = `jitsimeettorture-${crypto.randomUUID()}`;

globalAny.ctx = {
times: {}
} as IContext;
globalAny.ctx.roomName = roomName;
globalAny.ctx.roomName = `jitsimeettorture-${crypto.randomUUID()}`;
globalAny.ctx.jwtPrivateKeyPath = process.env.JWT_PRIVATE_KEY_PATH;
globalAny.ctx.jwtKid = process.env.JWT_KID;
},
Expand All @@ -205,6 +218,26 @@ export const config: WebdriverIO.MultiremoteConfig = {
if (ctx?.webhooksProxy) {
ctx.webhooksProxy.disconnect();
}

ctx.keepAlive?.forEach(clearInterval);
},

beforeSession(c, capabilities, specs, cid) {
const originalBefore = c.before;

if (!originalBefore || !Array.isArray(originalBefore) || originalBefore.length !== 1) {
console.warn('No before hook found or more than one found, skipping');

return;
}

if (originalBefore) {
c.before = [ async function(...args) {
// Call original with cid as first param, followed by original args
// @ts-ignore
return await originalBefore[0].call(c, cid, ...args);
} ];
}
},

/**
Expand Down Expand Up @@ -298,6 +331,14 @@ export const config: WebdriverIO.MultiremoteConfig = {
'image/png');
}));

// @ts-ignore
allProcessing.push(bInstance.execute(() => typeof APP !== 'undefined' && APP.connection?.getLogs())
.then(logs =>
logs && AllureReporter.addAttachment(
`debug-logs-${instance}`,
JSON.stringify(logs, null, ' '),
'text/plain'))
.catch(e => console.error('Failed grabbing debug logs', e)));

AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');

Expand All @@ -306,7 +347,7 @@ export const config: WebdriverIO.MultiremoteConfig = {
}));
});

await Promise.all(allProcessing);
await Promise.allSettled(allProcessing);
}
},

Expand Down
9 changes: 8 additions & 1 deletion tests/wdio.firefox.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,14 @@ const ffExcludes = [

// FF does not support setting a file as mic input, no dominant speaker events
'specs/3way/activeSpeaker.spec.ts',
'specs/4way/desktopSharing.spec.ts'
'specs/3way/startMuted.spec.ts', // bad audio levels
'specs/4way/desktopSharing.spec.ts',
'specs/4way/lastN.spec.ts',

// when unmuting a participant, we see the presence in debug logs imidiately,
// but for 15 seconds it is not received/processed by the client
// (also menu disappears after clicking one of the moderation option, does not happen manually)
'specs/3way/audioVideoModeration.spec.ts'
];

const mergedConfig = merge(defaultConfig, {
Expand Down
Loading
Loading