diff --git a/tests/pageobjects/Toolbar.ts b/tests/pageobjects/Toolbar.ts index 9b807a26fd..708fe6546e 100644 --- a/tests/pageobjects/Toolbar.ts +++ b/tests/pageobjects/Toolbar.ts @@ -5,6 +5,7 @@ const AUDIO_UNMUTE = 'Unmute microphone'; const CHAT = 'Open chat'; const CLOSE_CHAT = 'Close chat'; const CLOSE_PARTICIPANTS_PANE = 'Close participants pane'; +const DESKTOP = 'Start sharing your screen'; const HANGUP = 'Leave the meeting'; const OVERFLOW_MENU = 'More actions menu'; const OVERFLOW = 'More actions'; @@ -12,6 +13,7 @@ const PARTICIPANTS = 'Open participants pane'; const PROFILE = 'Edit your profile'; const RAISE_HAND = 'Raise your hand'; const SETTINGS = 'Open settings'; +const STOP_DESKTOP = 'Stop sharing your screen'; const ENTER_TILE_VIEW_BUTTON = 'Enter tile view'; const EXIT_TILE_VIEW_BUTTON = 'Exit tile view'; const VIDEO_QUALITY = 'Manage video quality'; @@ -164,6 +166,20 @@ export default class Toolbar extends BasePageObject { await this.getButton(CLOSE_CHAT).click(); } + /** + * Clicks on the desktop sharing button that starts desktop sharing. + */ + async clickDesktopSharingButton() { + await this.getButton(DESKTOP).click(); + } + + /** + * Clicks on the desktop sharing button to stop it. + */ + async clickStopDesktopSharingButton() { + await this.getButton(STOP_DESKTOP).click(); + } + /** * Clicks on the tile view button which enables tile layout. */ diff --git a/tests/specs/4way/desktopSharing.spec.ts b/tests/specs/4way/desktopSharing.spec.ts new file mode 100644 index 0000000000..0c693536d7 --- /dev/null +++ b/tests/specs/4way/desktopSharing.spec.ts @@ -0,0 +1,353 @@ +import { SET_AUDIO_ONLY } from '../../../react/features/base/audio-only/actionTypes'; +import type { Participant } from '../../helpers/Participant'; +import { + ensureFourParticipants, + ensureOneParticipant, + ensureThreeParticipants, + ensureTwoParticipants +} from '../../helpers/participants'; + +describe('Desktop sharing - ', () => { + it('start', async () => { + await ensureTwoParticipants(ctx, { + configOverwrite: { + p2p: { + enabled: true + } + } + }); + const { p1, p2 } = ctx; + + await p2.getToolbar().clickDesktopSharingButton(); + + // Check if a remote screen share tile is created on p1. + await checkForScreensharingTile(p2, p1); + + // Check if a local screen share tile is created on p2. + await checkForScreensharingTile(p2, p2); + + expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + }); + + it('stop', async () => { + const { p1, p2 } = ctx; + + await p2.getToolbar().clickStopDesktopSharingButton(); + + // Check if the local screen share thumbnail disappears on p2. + await checkForScreensharingTile(p2, p2, true); + + // Check if the remote screen share thumbnail disappears on p1. + await checkForScreensharingTile(p1, p2, true); + }); + + /** + * Ensures screen share is still visible when the call switches from p2p to jvb connection. + */ + it('p2p to jvb switch', async () => { + await ctx.p2.getToolbar().clickDesktopSharingButton(); + + await ensureThreeParticipants(ctx); + const { p1, p2, p3 } = ctx; + + // Check if a remote screen share tile is created on all participants. + await checkForScreensharingTile(p2, p1); + await checkForScreensharingTile(p2, p2); + await checkForScreensharingTile(p2, p2); + + expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + }); + + /** + * Ensure screen share is still visible when the call switches from jvb to p2p and back. + */ + it('p2p to jvb switch and back', async () => { + const { p1, p2, p3 } = ctx; + + await p3.hangup(); + + // Check if a remote screen share tile is created on p1 and p2 after switching back to p2p. + await checkForScreensharingTile(p2, p1); + await checkForScreensharingTile(p2, p2); + + // The video should be playing. + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + + // Start desktop share on p1. + await p1.getToolbar().clickDesktopSharingButton(); + + // Check if a new tile for p1's screen share is created on both p1 and p2. + await checkForScreensharingTile(p1, p1); + await checkForScreensharingTile(p1, p2); + + await ensureThreeParticipants(ctx); + + await checkForScreensharingTile(p1, p3); + await checkForScreensharingTile(p2, p3); + + // The large video should be playing on p3. + expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + }); + + /** + * Ensure that screen share is still visible in jvb connection when share is toggled while the users are + * in p2p mode, i.e., share is restarted when user is in p2p mode and then the call switches over to jvb mode. + */ + it('stop screen sharing and back', async () => { + const { p1, p2, p3 } = ctx; + + // Stop share on both p1 and p2. + await p1.getToolbar().clickStopDesktopSharingButton(); + await p2.getToolbar().clickStopDesktopSharingButton(); + + await p3.hangup(); + + // Start share on both p1 and p2. + await p1.getToolbar().clickDesktopSharingButton(); + await p2.getToolbar().clickDesktopSharingButton(); + + // Check if p1 and p2 can see each other's shares in p2p. + await checkForScreensharingTile(p1, p2); + await checkForScreensharingTile(p2, p1); + + // Add p3 back to the conference and check if p1 and p2's shares are visible on p3. + await ensureThreeParticipants(ctx); + + await checkForScreensharingTile(p1, p3); + await checkForScreensharingTile(p2, p3); + + // The large video should be playing on p3. + expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + }); + + /** + * Ensures screen share is visible when a muted screen share track is added to the conference, i.e., + * users starts and stops the share before anyone else joins the call. + * The call switches to jvb and then back to p2p. + */ + it('screen sharing toggle before others join', async () => { + await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]); + + await ensureOneParticipant(ctx, { + configOverwrite: { + p2p: { + enabled: true + } + } + }); + const { p1 } = ctx; + + // p1 starts share when alone in the call. + await p1.getToolbar().clickDesktopSharingButton(); + await checkForScreensharingTile(p1, p1); + + // p1 stops share. + await p1.getToolbar().clickStopDesktopSharingButton(); + + // Call switches to jvb. + await ensureThreeParticipants(ctx); + const { p2, p3 } = ctx; + + // p1 starts share again when call switches to jvb. + await p1.getToolbar().clickDesktopSharingButton(); + + // Check p2 and p3 are able to see p1's share. + await checkForScreensharingTile(p1, p2); + await checkForScreensharingTile(p1, p3); + + expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + expect(await p3.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + + // p3 leaves the call. + await p3.hangup(); + + // Make sure p2 see's p1's share after the call switches back to p2p. + await checkForScreensharingTile(p1, p2); + expect(await p2.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + + // p2 starts share when in p2p. + await p2.getToolbar().clickDesktopSharingButton(); + + // Makes sure p2's share is visible on p1. + await checkForScreensharingTile(p2, p1); + expect(await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + }); + + /** + * A case where a non-dominant speaker is sharing screen for a participant in low bandwidth mode + * where only a screen share can be received. A bug fixed in jvb 0c5dd91b where the video was not received. + */ + it('audio only and non dominant screen share', async () => { + await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]); + + await ensureOneParticipant(ctx); + const { p1 } = ctx; + + // a workaround to directly set audio only mode without going through the rest of the settings in the UI + await p1.driver.execute(type => { + APP.store.dispatch({ + type, + audioOnly: true + }); + APP.conference.onToggleAudioOnly(); + }, SET_AUDIO_ONLY); + await p1.getToolbar().clickAudioMuteButton(); + + await ensureThreeParticipants(ctx, { skipInMeetingChecks: true }); + const { p2, p3 } = ctx; + + await p3.getToolbar().clickAudioMuteButton(); + await p3.getToolbar().clickDesktopSharingButton(); + + await checkForScreensharingTile(p3, p1); + await checkForScreensharingTile(p3, p2); + + // the video should be playing + await p1.driver.waitUntil(async () => + await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), { + timeout: 5_000, + timeoutMsg: 'expected remote screen share to be on large' + }); + }); + + /** + * A case where first participant is muted (a&v) and enters low bandwidth mode, + * the second one is audio muted only and the one sharing (the third) is dominant speaker. + * A problem fixed in jitsi-meet 3657c19e and d6ab0a72. + */ + it('audio only and dominant screen share', async () => { + await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]); + + await ensureOneParticipant(ctx, { + configOverwrite: { + startWithAudioMuted: true, + startWithVideoMuted: true + } + }); + const { p1 } = ctx; + + // a workaround to directly set audio only mode without going through the rest of the settings in the UI + await p1.driver.execute(type => { + APP.store.dispatch({ + type, + audioOnly: true + }); + APP.conference.onToggleAudioOnly(); + }, SET_AUDIO_ONLY); + + await ensureTwoParticipants(ctx, { + skipInMeetingChecks: true, + configOverwrite: { + startWithAudioMuted: true + } + }); + await ensureThreeParticipants(ctx, { + skipInMeetingChecks: true + }); + const { p2, p3 } = ctx; + + await p3.getToolbar().clickDesktopSharingButton(); + + await checkForScreensharingTile(p3, p1); + await checkForScreensharingTile(p3, p2); + + // The desktop sharing participant should be on large + expect(await p1.driver.execute(() => APP.UI.getLargeVideoID())).toBe(`${await p3.getEndpointId()}-v1`); + + // the video should be playing + await p1.driver.waitUntil(async () => + await p1.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived()), { + timeout: 5_000, + timeoutMsg: 'expected remote screen share to be on large' + }); + }); + + /** + * Test screensharing with lastN. We add p4 with lastN=2 and verify that it receives the expected streams. + */ + it('with lastN', async () => { + await Promise.all([ ctx.p1.hangup(), ctx.p2.hangup(), ctx.p3.hangup() ]); + + await ensureThreeParticipants(ctx); + const { p1, p2, p3 } = ctx; + + await p3.getToolbar().clickDesktopSharingButton(); + + await p1.getToolbar().clickAudioMuteButton(); + await p3.getToolbar().clickAudioMuteButton(); + + await ensureFourParticipants(ctx, { + configOverwrite: { + channelLastN: 2, + startWithAudioMuted: true + } + }); + const { p4 } = ctx; + + // We now have p1, p2, p3, p4. + // p3 is screensharing. + // p1, p3, p4 are audio muted, so p2 should eventually become dominant speaker. + // Participants should display p3 on-stage because it is screensharing. + await checkForScreensharingTile(p3, p1); + await checkForScreensharingTile(p3, p2); + await checkForScreensharingTile(p3, p4); + + // And the video should be playing + expect(await p4.driver.execute(() => JitsiMeetJS.app.testing.isLargeVideoReceived())).toBe(true); + + const p1EndpointId = await p1.getEndpointId(); + + // p4 has lastN=2 and has selected p3. With p2 being dominant speaker p4 should eventually + // see video for [p3, p2] and p1 as ninja. + await p4.driver.$(`//span[@id='participant_${p1EndpointId}']//span[@class='connection_ninja']`) + .waitForDisplayed({ + timeout: 15_000 + }); + + const p2EndpointId = await p2.getEndpointId(); + + await p4.driver.waitUntil(async () => + await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`), + p2EndpointId) && await p4.driver.$( + `//span[@id="participant_${p2EndpointId}" and contains(@class, "display-video")]`).isExisting(), { + timeout: 15_000, + timeoutMsg: 'expected remote video to be received 15s' + }); + + await p4.driver.waitUntil(async () => + await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`), + p2EndpointId) && await p4.driver.$( + `//span[@id="participant_${p2EndpointId}" and contains(@class, "display-video")]`).isExisting(), { + timeout: 15_000, + timeoutMsg: 'expected remote video to be received 15s' + }); + + // Let's switch and check, muting participant 2 and unmuting 1 will leave participant 1 as dominant + await p1.getToolbar().clickAudioUnmuteButton(); + await p2.getToolbar().clickAudioMuteButton(); + + // Participant4 should eventually see video for [p3, p1] and p2 as a ninja. + await p4.driver.$(`//span[@id='participant_${p2EndpointId}']//span[@class='connection_ninja']`) + .waitForDisplayed({ + timeout: 15_000 + }); + + await p4.driver.waitUntil(async () => + await p4.driver.execute(epId => JitsiMeetJS.app.testing.isRemoteVideoReceived(`${epId}`), + p1EndpointId) && await p4.driver.$( + `//span[@id="participant_${p1EndpointId}" and contains(@class, "display-video")]`).isExisting(), { + timeout: 15_000, + timeoutMsg: 'expected remote video to be received 15s' + }); + }); +}); + +/** + * Check if a screensharing tile is displayed on the observer. + */ +async function checkForScreensharingTile(sharer: Participant, observer: Participant, reverse = false) { + await observer.driver.$(`//span[@id='participant_${await sharer.getEndpointId()}-v1']`).waitForDisplayed({ + timeout: 3_000, + reverse + }); +} diff --git a/tests/wdio.conf.ts b/tests/wdio.conf.ts index 1463cd76f8..e0df0746a8 100644 --- a/tests/wdio.conf.ts +++ b/tests/wdio.conf.ts @@ -17,6 +17,8 @@ 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', @@ -33,7 +35,8 @@ 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=${process.env.REMOTE_RESOURCE_PATH || 'tests/resources'}/fakeAudioStream.wav` + `--use-file-for-fake-audio-capture=${ + usingGrid ? process.env.REMOTE_RESOURCE_PATH : 'tests/resources'}/fakeAudioStream.wav` ]; if (process.env.RESOLVER_RULES) { diff --git a/tests/wdio.firefox.conf.ts b/tests/wdio.firefox.conf.ts index 028acf86f3..74ded693fb 100644 --- a/tests/wdio.firefox.conf.ts +++ b/tests/wdio.firefox.conf.ts @@ -21,7 +21,10 @@ if (process.env.HEADLESS === 'true') { const ffExcludes = [ 'specs/2way/iFrameParticipantsPresence.spec.ts', // FF does not support uploading files (uploadFile) - 'specs/3way/activeSpeaker.spec.ts' // FF does not support setting a file as mic input + + // FF does not support setting a file as mic input, no dominant speaker events + 'specs/3way/activeSpeaker.spec.ts', + 'specs/4way/desktopSharing.spec.ts' ]; const mergedConfig = merge(defaultConfig, { diff --git a/tests/wdio.grid.conf.ts b/tests/wdio.grid.conf.ts index 07015bfdea..36280eacb8 100644 --- a/tests/wdio.grid.conf.ts +++ b/tests/wdio.grid.conf.ts @@ -4,7 +4,7 @@ import { merge } from 'lodash-es'; import { URL } from 'url'; // @ts-ignore -import { config as defaultConfig } from './wdio.conf.ts'; +import { config as defaultConfig } from './wdio.conf.ts?grid=true'; const gridUrl = new URL(process.env.GRID_HOST_URL as string); const protocol = gridUrl.protocol.replace(':', '');