import { P1, P2, P3, P4, P5, P6, Participant } from './Participant'; import { config } from './TestsConfig'; import { IJoinOptions, IParticipantOptions } from './types'; const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]'; /** * Ensure that there is on participant. * Ensure that the first participant is moderator if there is such an option. * * @param {IJoinOptions} options - The options to use when joining the participant. * @param participantOptions * @returns {Promise} */ export async function ensureOneParticipant( options?: IJoinOptions, participantOptions?: IParticipantOptions): Promise { if (!participantOptions) { participantOptions = { name: P1 }; } participantOptions.name = P1; if (!participantOptions.token) { participantOptions.token = config.jwt.preconfiguredToken; } // make sure the first participant is moderator, if supported by deployment await joinParticipant(participantOptions, options); } /** * Ensure that there are three participants. * * @param {IJoinOptions} options - The options to use when joining the participant. * @returns {Promise} */ export async function ensureThreeParticipants(options?: IJoinOptions): Promise { await ensureOneParticipant(options); // these need to be all, so we get the error when one fails await Promise.all([ joinParticipant({ name: P2 }, options), joinParticipant({ name: P3 }, options) ]); if (options?.skipInMeetingChecks) { return Promise.resolve(); } await Promise.all([ ctx.p1.waitForIceConnected(), ctx.p2.waitForIceConnected(), ctx.p3.waitForIceConnected() ]); await Promise.all([ ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)), ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)), ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)), ]); } /** * Creates the first participant instance or prepares one for re-joining. * * @param {IJoinOptions} options - The options to use when joining the participant. * @returns {Promise} */ export function joinFirstParticipant(options: IJoinOptions = { }): Promise { return ensureOneParticipant(options); } /** * Creates the second participant instance or prepares one for re-joining. * * @param {IJoinOptions} options - The options to use when joining the participant. * @returns {Promise} */ export function joinSecondParticipant(options?: IJoinOptions): Promise { return joinParticipant({ name: P2 }, options); } /** * Creates the third participant instance or prepares one for re-joining. * * @param {IJoinOptions} options - The options to use when joining the participant. * @returns {Promise} */ export function joinThirdParticipant(options?: IJoinOptions): Promise { return joinParticipant({ name: P3 }, options); } /** * Ensure that there are four participants. * * @param {IJoinOptions} options - The options to use when joining the participant. * @returns {Promise} */ export async function ensureFourParticipants(options?: IJoinOptions): Promise { await ensureOneParticipant(options); // these need to be all, so we get the error when one fails await Promise.all([ joinParticipant({ name: P2 }, options), joinParticipant({ name: P3 }, options), joinParticipant({ name: P4 }, options) ]); if (options?.skipInMeetingChecks) { return Promise.resolve(); } await Promise.all([ ctx.p1.waitForIceConnected(), ctx.p2.waitForIceConnected(), ctx.p3.waitForIceConnected(), ctx.p4.waitForIceConnected() ]); await Promise.all([ ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)), ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)), ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)), ctx.p4.waitForSendReceiveData().then(() => ctx.p4.waitForRemoteStreams(1)), ]); } /** * Ensure that there are six participants. * * @param {IJoinOptions} options - The options to use when joining the participant. * @returns {Promise} */ export async function ensureSixParticipants(options?: IJoinOptions): Promise { await ensureOneParticipant(options); // Join participants in batches await Promise.all([ joinParticipant({ name: P2 }, options), joinParticipant({ name: P3 }, options), joinParticipant({ name: P4 }, options) ]); await Promise.all([ joinParticipant({ name: P5 }, options), joinParticipant({ name: P6 }, options) ]); if (options?.skipInMeetingChecks) { return Promise.resolve(); } await Promise.all([ ctx.p1.waitForIceConnected(), ctx.p2.waitForIceConnected(), ctx.p3.waitForIceConnected(), ctx.p4.waitForIceConnected(), ctx.p5.waitForIceConnected(), ctx.p6.waitForIceConnected() ]); await Promise.all([ ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)), ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)), ctx.p3.waitForSendReceiveData().then(() => ctx.p3.waitForRemoteStreams(1)), ctx.p4.waitForSendReceiveData().then(() => ctx.p4.waitForRemoteStreams(1)), ctx.p5.waitForSendReceiveData().then(() => ctx.p5.waitForRemoteStreams(1)), ctx.p6.waitForSendReceiveData().then(() => ctx.p6.waitForRemoteStreams(1)) ]); } /** * Ensure that there are two participants. * * @param {IJoinOptions} options - The options to join. * @param participantOptions */ export async function ensureTwoParticipants( options?: IJoinOptions, participantOptions?: IParticipantOptions): Promise { await ensureOneParticipant(options, participantOptions); if (!participantOptions) { participantOptions = { name: P2 }; } participantOptions.name = P2; await joinParticipant(participantOptions, options); if (options?.skipInMeetingChecks) { return Promise.resolve(); } await Promise.all([ ctx.p1.waitForIceConnected(), ctx.p2.waitForIceConnected() ]); await Promise.all([ ctx.p1.waitForSendReceiveData().then(() => ctx.p1.waitForRemoteStreams(1)), ctx.p2.waitForSendReceiveData().then(() => ctx.p2.waitForRemoteStreams(1)) ]); } /** * Creates a new participant instance, or returns an existing one if it is already joined. * @param participantOptions - The participant options, with required name set. * @param options - Join options. * @returns {Promise} - The participant instance. */ async function joinParticipant( // eslint-disable-line max-params participantOptions: IParticipantOptions, options?: IJoinOptions ): Promise { // @ts-ignore const p = ctx[participantOptions.name] as Participant; if (p) { // Skip the isInMuc check (and the iframe context switch it requires) when the participant // is already on base.html — they cannot be in the MUC and there is no iframe to switch into. const alreadyOnBasePage = (await p.driver.getUrl()).endsWith('/base.html'); if (!alreadyOnBasePage) { if (participantOptions.iFrameApi) { await p.switchToIFrame(); } if (await p.isInMuc()) { return p; } if (participantOptions.iFrameApi) { // when loading url make sure we are on the top page context or strange errors may occur await p.switchToMainFrame(); } // Change the page so we can reload same url if we need to, base.html is supposed to be empty or close to empty await p.driver.url('/base.html'); } } const newParticipant = new Participant(participantOptions); // set the new participant instance // @ts-ignore ctx[participantOptions.name] = newParticipant; return await newParticipant.joinConference({ ...options, roomName: options?.roomName || ctx.roomName, }); } /** * Parse a JID string. * @param str the string to parse. */ export function parseJid(str: string): { domain: string; node: string; resource: string | undefined; } { const parts = str.split('@'); const domainParts = parts[1].split('/'); return { node: parts[0], domain: domainParts[0], resource: domainParts.length > 0 ? domainParts[1] : undefined }; } /** * Check the subject of the participant. * @param participant * @param subject */ export async function checkSubject(participant: Participant, subject: string) { const localTile = participant.driver.$(SUBJECT_XPATH); await localTile.waitForExist(); await localTile.moveTo(); const txt = await localTile.getText(); expect(txt.startsWith(subject)).toBe(true); } /** * Check if a screensharing tile is displayed on the observer. * Expects there was already a video by this participant and screen sharing will be the second video `-v1`. */ export async function checkForScreensharingTile(sharer: Participant, observer: Participant, reverse = false) { const selector = `//span[@id='participant_${await sharer.getEndpointId()}-v1']`; // Re-runs the selector on every poll, unlike waitForDisplayed(), which keeps checking the element it // matched first. Thumbnails get replaced by React while the wait is running, and losing the matched one // is not something wdio recovers from here: over WebDriver BiDi the node handle is passed to the // visibility check as a script argument, so Chrome rejects it with `invalid argument - Invalid input in // "arguments"/0`, which wdio's refetch-on-stale handling does not recognize as staleness. Every // remaining poll then reuses the dead handle, failing the wait while a tile is on screen. await observer.driver.waitUntil( async () => { try { const displayed = await observer.driver.$(selector).isDisplayed(); return displayed !== reverse; } catch (e) { // Look the element up again on the next poll. return false; } }, { timeout: 3_000, timeoutMsg: `Screensharing tile of ${sharer.name} is ${ reverse ? 'still displayed' : 'not displayed'} on ${observer.name}` } ); } /** * Hangs up all participants (p1, p2, p3, p4, p5, and p6) * @returns {Promise} */ export function hangupAllParticipants() { return Promise.all([ ctx.p1?.hangup(), ctx.p2?.hangup(), ctx.p3?.hangup(), ctx.p4?.hangup(), ctx.p5?.hangup(), ctx.p6?.hangup() ].map(p => p ?? Promise.resolve())); }