diff --git a/package-lock.json b/package-lock.json index ea3c7c367a20..6ac347af4233 100644 --- a/package-lock.json +++ b/package-lock.json @@ -136,6 +136,7 @@ "@types/js-md5": "0.4.3", "@types/jsonwebtoken": "9.0.7", "@types/lodash-es": "4.17.12", + "@types/minimatch": "5.1.2", "@types/mocha": "10.0.10", "@types/moment-duration-format": "2.2.6", "@types/offscreencanvas": "2019.7.2", @@ -7176,6 +7177,13 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "node_modules/@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/mocha": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", @@ -29785,6 +29793,12 @@ "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, + "@types/minimatch": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-5.1.2.tgz", + "integrity": "sha512-K0VQKziLUWkVKiRVrx4a40iPaxTUefQmjtkQofBkYRcoaaL/8rhwDWww9qWbrgicNOgnpIsMxyNIUM4+n6dUIA==", + "dev": true + }, "@types/mocha": { "version": "10.0.10", "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-10.0.10.tgz", diff --git a/package.json b/package.json index 04d3192b42ca..10d329184ea7 100644 --- a/package.json +++ b/package.json @@ -142,6 +142,7 @@ "@types/js-md5": "0.4.3", "@types/jsonwebtoken": "9.0.7", "@types/lodash-es": "4.17.12", + "@types/minimatch": "5.1.2", "@types/mocha": "10.0.10", "@types/moment-duration-format": "2.2.6", "@types/offscreencanvas": "2019.7.2", diff --git a/tests/helpers/Participant.ts b/tests/helpers/Participant.ts index 15ef37207d85..6ea306351048 100644 --- a/tests/helpers/Participant.ts +++ b/tests/helpers/Participant.ts @@ -5,6 +5,7 @@ import { multiremotebrowser } from '@wdio/globals'; import { IConfig } from '../../react/features/base/config/configType'; import { urlObjectToString } from '../../react/features/base/util/uri'; import BreakoutRooms from '../pageobjects/BreakoutRooms'; +import ChatPanel from '../pageobjects/ChatPanel'; import Filmstrip from '../pageobjects/Filmstrip'; import IframeAPI from '../pageobjects/IframeAPI'; import Notifications from '../pageobjects/Notifications'; @@ -121,7 +122,10 @@ export class Participant { async joinConference(ctx: IContext, options: IJoinOptions = {}): Promise { const config = { room: ctx.roomName, - configOverwrite: this.config, + configOverwrite: { + ...this.config, + ...options.configOverwrite || {} + }, interfaceConfigOverwrite: { SHOW_CHROME_EXTENSION_BANNER: false } @@ -338,6 +342,13 @@ export class Participant { }); } + /** + * Returns the chat panel for this participant. + */ + getChatPanel(): ChatPanel { + return new ChatPanel(this); + } + /** * Returns the BreakoutRooms for this participant. * diff --git a/tests/helpers/participants.ts b/tests/helpers/participants.ts index 57e47de0cc5f..3d30217bbe02 100644 --- a/tests/helpers/participants.ts +++ b/tests/helpers/participants.ts @@ -28,26 +28,25 @@ export async function ensureOneParticipant(ctx: IContext, options?: IJoinOptions * Ensure that there are three participants. * * @param {Object} ctx - The context. + * @param {IJoinOptions} options - The options to use when joining the participant. * @returns {Promise} */ -export async function ensureThreeParticipants(ctx: IContext): Promise { - await joinTheModeratorAsP1(ctx); - - const p2 = new Participant('participant2'); - const p3 = new Participant('participant3'); - - ctx.p2 = p2; - ctx.p3 = p3; +export async function ensureThreeParticipants(ctx: IContext, options?: IJoinOptions): Promise { + await joinTheModeratorAsP1(ctx, options); // these need to be all, so we get the error when one fails await Promise.all([ - p2.joinConference(ctx), - p3.joinConference(ctx) + _joinParticipant('participant2', ctx.p2, p => { + ctx.p2 = p; + }, options), + _joinParticipant('participant3', ctx.p3, p => { + ctx.p3 = p; + }, options) ]); await Promise.all([ - p2.waitForRemoteStreams(2), - p3.waitForRemoteStreams(2) + ctx.p2.waitForRemoteStreams(2), + ctx.p3.waitForRemoteStreams(2) ]); } diff --git a/tests/helpers/types.ts b/tests/helpers/types.ts index e03e82ea1a10..c86b2bc474a5 100644 --- a/tests/helpers/types.ts +++ b/tests/helpers/types.ts @@ -1,3 +1,5 @@ +import { IConfig } from '../../react/features/base/config/configType'; + import type { Participant } from './Participant'; import WebhookProxy from './WebhookProxy'; @@ -17,6 +19,11 @@ export type IContext = { export type IJoinOptions = { + /** + * Config overwrites to use. + */ + configOverwrite?: IConfig; + /** * Whether to skip setting display name. */ diff --git a/tests/pageobjects/ChatPanel.ts b/tests/pageobjects/ChatPanel.ts new file mode 100644 index 000000000000..ef18bc93f3ae --- /dev/null +++ b/tests/pageobjects/ChatPanel.ts @@ -0,0 +1,22 @@ +import BasePageObject from './BasePageObject'; + +/** + * Chat panel elements. + */ +export default class ChatPanel extends BasePageObject { + /** + * Is chat panel open. + */ + async isOpen() { + return await this.participant.driver.$('#sideToolbarContainer').isExisting(); + } + + /** + * Presses the "chat" keyboard shortcut which opens or closes the chat + * panel. + */ + async pressShortcut() { + await this.participant.driver.$('body').click(); + await this.participant.driver.keys([ 'c' ]); + } +} diff --git a/tests/pageobjects/Toolbar.ts b/tests/pageobjects/Toolbar.ts index e855fc9af13d..ff839a170847 100644 --- a/tests/pageobjects/Toolbar.ts +++ b/tests/pageobjects/Toolbar.ts @@ -2,6 +2,8 @@ import BasePageObject from './BasePageObject'; const AUDIO_MUTE = 'Mute microphone'; const AUDIO_UNMUTE = 'Unmute microphone'; +const CHAT = 'Open chat'; +const CLOSE_CHAT = 'Close chat'; const CLOSE_PARTICIPANTS_PANE = 'Close participants pane'; const OVERFLOW_MENU = 'More actions menu'; const OVERFLOW = 'More actions'; @@ -142,6 +144,22 @@ export default class Toolbar extends BasePageObject { await this.getButton(RAISE_HAND).click(); } + /** + * Clicks on the chat button that opens chat panel. + */ + async clickChatButton(): Promise { + this.participant.log('Clicking on: Chat Button'); + await this.getButton(CHAT).click(); + } + + /** + * Clicks on the chat button that closes chat panel. + */ + async clickCloseChatButton(): Promise { + this.participant.log('Clicking on: Close Chat Button'); + await this.getButton(CLOSE_CHAT).click(); + } + /** * Ensure the overflow menu is open and clicks on a specified button. * @param accessibilityLabel The accessibility label of the button to be clicked. diff --git a/tests/specs/3way/codecSelection.spec.ts b/tests/specs/3way/codecSelection.spec.ts new file mode 100644 index 000000000000..c81f6419ca64 --- /dev/null +++ b/tests/specs/3way/codecSelection.spec.ts @@ -0,0 +1,121 @@ +import { ensureOneParticipant, ensureThreeParticipants, ensureTwoParticipants } from '../../helpers/participants'; + +describe('Codec selection - ', () => { + it('asymmetric codecs', async () => { + await ensureOneParticipant(ctx, { + configOverwrite: { + videoQuality: { + codecPreferenceOrder: [ 'VP9', 'VP8', 'AV1' ] + } + } + }); + + await ensureTwoParticipants(ctx, { + configOverwrite: { + videoQuality: { + codecPreferenceOrder: [ 'VP8', 'VP9', 'AV1' ] + } + } + }); + const { p1, p2 } = ctx; + + // Check if media is playing on both endpoints. + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + + // Check if p1 is sending VP9 and p2 is sending VP8 as per their codec preferences. + // Except on Firefox because it doesn't support VP9 encode. + if (p1.driver.isFirefox) { + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true); + } else { + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true); + } + + expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true); + }); + + it('asymmetric codecs with AV1', async () => { + await ensureThreeParticipants(ctx, { + configOverwrite: { + videoQuality: { + codecPreferenceOrder: [ 'AV1', 'VP9', 'VP8' ] + } + } + }); + const { p1, p2, p3 } = ctx; + + // Check if media is playing on p3. + expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + + // Check if p1 is encoding in VP9, p2 in VP8 and p3 in AV1 as per their codec preferences. + // Except on Firefox because it doesn't support AV1/VP9 encode and AV1 decode. + if (p1.driver.isFirefox) { + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true); + } else { + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true); + } + + expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true); + + // If there is a Firefox ep in the call, all other eps will switch to VP9. + if (p1.driver.isFirefox) { + expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true); + } else { + expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingAv1())).toBe(true); + } + }); + + it('codec switch over', async () => { + await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]); + + await ensureTwoParticipants(ctx, { + configOverwrite: { + videoQuality: { + codecPreferenceOrder: [ 'VP9', 'VP8', 'AV1' ] + } + } + }); + const { p1, p2 } = ctx; + + // Disable this test on Firefox because it doesn't support VP9 encode. + if (p1.driver.isFirefox) { + return; + } + + // Check if p1 and p2 are encoding in VP9 which is the default codec. + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true); + expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9())).toBe(true); + + await ensureThreeParticipants(ctx, { + configOverwrite: { + videoQuality: { + codecPreferenceOrder: [ 'VP8' ] + } + } + }); + const { p3 } = ctx; + + // Check if all three participants are encoding in VP8 now. + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true); + expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true); + expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp8())).toBe(true); + + await p3.hangup(); + + // Check of p1 and p2 have switched to VP9. + await p1.driver.waitUntil( + async () => await p1.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()), + { + timeout: 10000, + timeoutMsg: 'p1 did not switch back to VP9' + } + ); + await p2.driver.waitUntil( + async () => await p2.driver.execute(() => JitsiMeetJS.app.testing.isLocalCameraEncodingVp9()), + { + timeout: 10000, + timeoutMsg: 'p1 did not switch back to VP9' + } + ); + }); +}); diff --git a/tests/specs/alone/chatPanel.spec.ts b/tests/specs/alone/chatPanel.spec.ts new file mode 100644 index 000000000000..bd4f2f6cd91b --- /dev/null +++ b/tests/specs/alone/chatPanel.spec.ts @@ -0,0 +1,34 @@ +import { ensureOneParticipant } from '../../helpers/participants'; + +describe('Chat Panel - ', () => { + it('join participant', async () => { + await ensureOneParticipant(ctx); + }); + it('start closed', async () => { + expect(await ctx.p1.getChatPanel().isOpen()).toBe(false); + }); + it('open', async () => { + const { p1 } = ctx; + + await p1.getToolbar().clickChatButton(); + expect(await p1.getChatPanel().isOpen()).toBe(true); + }); + it('use shortcut to close', async () => { + const chatPanel = ctx.p1.getChatPanel(); + + await chatPanel.pressShortcut(); + expect(await chatPanel.isOpen()).toBe(false); + }); + it('use shortcut to open', async () => { + const chatPanel = ctx.p1.getChatPanel(); + + await chatPanel.pressShortcut(); + expect(await chatPanel.isOpen()).toBe(true); + }); + it('use button to open', async () => { + const { p1 } = ctx; + + await p1.getToolbar().clickCloseChatButton(); + expect(await p1.getChatPanel().isOpen()).toBe(false); + }); +}); diff --git a/tests/wdio.conf.ts b/tests/wdio.conf.ts index b6ffa6382b3f..1280cc9444bb 100644 --- a/tests/wdio.conf.ts +++ b/tests/wdio.conf.ts @@ -1,6 +1,7 @@ import AllureReporter from '@wdio/allure-reporter'; import { multiremotebrowser } from '@wdio/globals'; import { Buffer } from 'buffer'; +import minimatch from 'minimatch'; import path from 'node:path'; import process from 'node:process'; import pretty from 'pretty'; @@ -62,7 +63,7 @@ export const config: WebdriverIO.MultiremoteConfig = { specs: [ 'specs/**' ], - maxInstances: 1, + maxInstances: 1, // if changing check onWorkerStart logic baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture/', tsConfigPath: './tsconfig.json', @@ -236,13 +237,36 @@ export const config: WebdriverIO.MultiremoteConfig = { * @param {Object} context - The context object. */ beforeTest(test, context) { - ctx.skipSuiteTests && context.skip(); + if (ctx.skipSuiteTests) { + context.skip(); + + return; + } multiremotebrowser.instances.forEach((instance: string) => { logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.title} ===---`); }); }, + /** + * Gets executed before a worker process is spawned and can be used to initialize specific service + * for that worker as well as modify runtime environments in an async fashion. + */ + onWorkerStart(...args) { + // We run a worker per suite, and replay on this logic here + if (args[2].length > 1) { + console.warn('Our worker is supposed to get a single suite, but got more than one'); + + return; + } + + // We skip the suite tests if the suite is marked as such, we used that from firefox overwrite + // @ts-ignore + if (config?.ffExcludes.some((e: string) => minimatch(args[2][0].replace('file://', ''), `${__dirname}/${e}`))) { + args[2].pop(); + } + }, + /** * Function to be executed after a test (in Mocha/Jasmine only). * diff --git a/tests/wdio.firefox.conf.ts b/tests/wdio.firefox.conf.ts index f9b13ce3ab39..028acf86f3c8 100644 --- a/tests/wdio.firefox.conf.ts +++ b/tests/wdio.firefox.conf.ts @@ -24,7 +24,8 @@ const ffExcludes = [ 'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input ]; -export const config = merge(defaultConfig, { +const mergedConfig = merge(defaultConfig, { + ffExcludes, capabilities: { participant1: { capabilities: { @@ -62,3 +63,9 @@ export const config = merge(defaultConfig, { } } }, { clone: false }); + +// Remove the chrome options from the first participant +// @ts-ignore +mergedConfig.capabilities.participant1.capabilities['goog:chromeOptions'] = undefined; + +export const config = mergedConfig;