* feat(tests): Simplifies display names and participant create.

* feat(tests): Simplifies display names and participant create.

Moves token creation only when needed.

* squash: Skip webhook check of user id for guest participants.

* squash: Waits for kick reason dialog.

* squash: Simplifies by matching participant name and display name.

* squash: Drop displayname field.
This commit is contained in:
Дамян Минков
2025-04-11 09:22:55 -05:00
committed by GitHub
parent c486dc07db
commit 3cd29fd63e
11 changed files with 120 additions and 160 deletions

View File

@@ -25,10 +25,10 @@ import VideoQualityDialog from '../pageobjects/VideoQualityDialog';
import { LOG_PREFIX, logInfo } from './browserLogger';
import { IContext, IJoinOptions } from './types';
export const P1_DISPLAY_NAME = 'p1';
export const P2_DISPLAY_NAME = 'p2';
export const P3_DISPLAY_NAME = 'p3';
export const P4_DISPLAY_NAME = 'p4';
export const P1 = 'p1';
export const P2 = 'p2';
export const P3 = 'p3';
export const P4 = 'p4';
interface IWaitForSendReceiveDataOptions {
checkReceive?: boolean;
@@ -47,7 +47,6 @@ export class Participant {
* @private
*/
private _name: string;
private _displayName: string;
private _endpointId: string;
private _jwt?: string;
@@ -164,13 +163,6 @@ export class Participant {
return this._name;
}
/**
* The name.
*/
get displayName() {
return this._displayName || this.name;
}
/**
* Adds a log to the participants log file.
*
@@ -203,7 +195,7 @@ export class Participant {
if (!options.skipDisplayName) {
// @ts-ignore
config.userInfo = {
displayName: this._displayName = options.displayName || this._name
displayName: this._name
};
}
@@ -756,8 +748,7 @@ export class Participant {
/**
* Returns the audio level for a participant.
*
* @param observer
* @param participant
* @param p
* @return
*/
async getRemoteAudioLevel(p: Participant) {
@@ -818,15 +809,11 @@ export class Participant {
// When testing for muted we don't want to have
// the condition succeeded
if (muted) {
const name = await testee.displayName;
assert.fail(`There was some sound coming from muted: '${name}'`);
assert.fail(`There was some sound coming from muted: '${this.name}'`);
} // else we're good for unmuted participant
} catch (_timeoutE) {
if (!muted) {
const name = await testee.displayName;
assert.fail(`There was no sound from unmuted: '${name}'`);
assert.fail(`There was no sound from unmuted: '${this.name}'`);
} // else we're good for muted participant
}
}
@@ -844,7 +831,7 @@ export class Participant {
endpointId) && !await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to not be received 15s by ${this.displayName}`
timeoutMsg: `expected remote video for ${endpointId} to not be received 15s by ${this.name}`
});
} else {
await this.driver.waitUntil(async () =>
@@ -852,7 +839,7 @@ export class Participant {
endpointId) && await this.driver.$(
`//span[@id="participant_${endpointId}" and contains(@class, "display-video")]`).isExisting(), {
timeout: 15_000,
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.displayName}`
timeoutMsg: `expected remote video for ${endpointId} to be received 15s by ${this.name}`
});
}
}
@@ -872,7 +859,7 @@ export class Participant {
await this.driver.$('//span[contains(@class,"videocontainer")]//span[contains(@class,"connection_ninja")]')
.waitForDisplayed({
timeout: 5_000,
timeoutMsg: `expected ninja icon to be displayed in 5s by ${this.displayName}`
timeoutMsg: `expected ninja icon to be displayed in 5s by ${this.name}`
});
}
}

View File

@@ -3,7 +3,7 @@ import jwt from 'jsonwebtoken';
import process from 'node:process';
import { v4 as uuidv4 } from 'uuid';
import { P1_DISPLAY_NAME, P2_DISPLAY_NAME, P3_DISPLAY_NAME, P4_DISPLAY_NAME, Participant } from './Participant';
import { P1, P2, P3, P4, Participant } from './Participant';
import { IContext, IJoinOptions } from './types';
const SUBJECT_XPATH = '//div[starts-with(@class, "subject-text")]';
@@ -31,18 +31,8 @@ export async function ensureThreeParticipants(ctx: IContext, options: IJoinOptio
// these need to be all, so we get the error when one fails
await Promise.all([
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
}),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: P3_DISPLAY_NAME,
...options
})
_joinParticipant(P2, ctx, options),
_joinParticipant(P3, ctx, options)
]);
if (options.skipInMeetingChecks) {
@@ -80,12 +70,7 @@ export function joinFirstParticipant(ctx: IContext, options: IJoinOptions = {}):
* @returns {Promise<void>}
*/
export function joinSecondParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
return _joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
});
return _joinParticipant(P2, ctx, options);
}
/**
@@ -96,12 +81,7 @@ export function joinSecondParticipant(ctx: IContext, options: IJoinOptions = {})
* @returns {Promise<void>}
*/
export function joinThirdParticipant(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
return _joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: P3_DISPLAY_NAME,
...options
});
return _joinParticipant(P3, ctx, options);
}
/**
@@ -116,24 +96,9 @@ export async function ensureFourParticipants(ctx: IContext, options: IJoinOption
// these need to be all, so we get the error when one fails
await Promise.all([
_joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
}),
_joinParticipant('participant3', ctx.p3, p => {
ctx.p3 = p;
}, {
displayName: P3_DISPLAY_NAME,
...options
}),
_joinParticipant('participant4', ctx.p4, p => {
ctx.p4 = p;
}, {
displayName: P4_DISPLAY_NAME,
...options
})
_joinParticipant(P2, ctx, options),
_joinParticipant(P3, ctx, options),
_joinParticipant(P4, ctx, options)
]);
if (options.skipInMeetingChecks) {
@@ -162,28 +127,8 @@ export async function ensureFourParticipants(ctx: IContext, options: IJoinOption
* @returns {Promise<void>}
*/
async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
const p1DisplayName = P1_DISPLAY_NAME;
let token;
if (!options?.skipFirstModerator) {
// we prioritize the access token when iframe is not used and private key is set,
// otherwise if private key is not specified we use the access token if set
if (process.env.JWT_ACCESS_TOKEN
&& ((ctx.jwtPrivateKeyPath && !ctx.iframeAPI && !options?.preferGenerateToken)
|| !ctx.jwtPrivateKeyPath)) {
token = process.env.JWT_ACCESS_TOKEN;
} else if (ctx.jwtPrivateKeyPath) {
token = getToken(ctx, p1DisplayName);
}
}
// make sure the first participant is moderator, if supported by deployment
await _joinParticipant('participant1', ctx.p1, p => {
ctx.p1 = p;
}, {
displayName: p1DisplayName,
...options
}, token);
await _joinParticipant(P1, ctx, options);
}
/**
@@ -195,12 +140,7 @@ async function joinTheModeratorAsP1(ctx: IContext, options?: IJoinOptions) {
export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions = {}): Promise<void> {
await joinTheModeratorAsP1(ctx, options);
await _joinParticipant('participant2', ctx.p2, p => {
ctx.p2 = p;
}, {
displayName: P2_DISPLAY_NAME,
...options
}, options.preferGenerateToken ? getToken(ctx, P2_DISPLAY_NAME) : undefined);
await _joinParticipant(P2, ctx, options);
if (options.skipInMeetingChecks) {
return Promise.resolve();
@@ -219,17 +159,17 @@ export async function ensureTwoParticipants(ctx: IContext, options: IJoinOptions
/**
* Creates a participant instance or prepares one for re-joining.
* @param name - The name of the participant.
* @param p - The participant instance to prepare or undefined if new one is needed.
* @param setter - The setter to use for setting the new participant instance into the context if needed.
* @param {IContext} ctx - The context.
* @param {boolean} options - Join options.
* @param {string?} jwtToken - The token to use if any.
*/
async function _joinParticipant( // eslint-disable-line max-params
name: string,
p: Participant,
setter: (p: Participant) => void,
options: IJoinOptions = {},
jwtToken?: string) {
ctx: IContext,
options: IJoinOptions = {}) {
// @ts-ignore
const p = ctx[name] as Participant;
if (p) {
if (ctx.iframeAPI) {
await p.switchInPage();
@@ -250,12 +190,34 @@ async function _joinParticipant( // eslint-disable-line max-params
// we want the participant instance re-recreated so we clear any kept state, like endpoint ID
}
let jwtToken;
if (name === P1) {
if (!options?.skipFirstModerator) {
// we prioritize the access token when iframe is not used and private key is set,
// otherwise if private key is not specified we use the access token if set
if (process.env.JWT_ACCESS_TOKEN
&& ((ctx.jwtPrivateKeyPath && !ctx.iframeAPI && !options?.preferGenerateToken)
|| !ctx.jwtPrivateKeyPath)) {
jwtToken = process.env.JWT_ACCESS_TOKEN;
} else if (ctx.jwtPrivateKeyPath) {
jwtToken = getToken(ctx, name);
}
}
} else if (name === P2) {
jwtToken = options.preferGenerateToken ? getToken(ctx, P2) : undefined;
}
const newParticipant = new Participant(name, jwtToken);
// set the new participant instance, pass it to setter
setter(newParticipant);
// set the new participant instance
// @ts-ignore
ctx[name] = newParticipant;
await newParticipant.joinConference(ctx, options);
await newParticipant.joinConference(ctx, {
displayName: name,
...options
});
}
/**

View File

@@ -99,8 +99,8 @@ export default class Filmstrip extends BasePageObject {
async () => await this.participant.getLargeVideo().getId() === videoIdToSwitchTo,
{
timeout: 3_000,
timeoutMsg: `${this.participant.displayName} did not switch the large video to ${
participant.displayName}`
timeoutMsg: `${this.participant.name} did not switch the large video to ${
participant.name}`
}
);
}
@@ -120,7 +120,7 @@ export default class Filmstrip extends BasePageObject {
await this.participant.driver.$(`//div[ @id="pin-indicator-${epId}" ]`).waitForDisplayed({
timeout: 2_000,
timeoutMsg: `${this.participant.displayName} did not unpin ${participant.displayName}`,
timeoutMsg: `${this.participant.name} did not unpin ${participant.name}`,
reverse: true
});
}

View File

@@ -75,8 +75,8 @@ export default class ParticipantsPane extends BasePageObject {
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
reverse,
timeout: 2000,
timeoutMsg: `Video mute icon is${reverse ? '' : ' not'} displayed for ${testee.displayName} at ${
this.participant.displayName} side.`
timeoutMsg: `Video mute icon is${reverse ? '' : ' not'} displayed for ${testee.name} at ${
this.participant.name} side.`
});
if (!isOpen) {
@@ -107,8 +107,8 @@ export default class ParticipantsPane extends BasePageObject {
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
reverse,
timeout: 2000,
timeoutMsg: `Audio mute icon is${reverse ? '' : ' not'} displayed for ${testee.displayName} at ${
this.participant.displayName} side.`
timeoutMsg: `Audio mute icon is${reverse ? '' : ' not'} displayed for ${testee.name} at ${
this.participant.name} side.`
});
if (!isOpen) {

View File

@@ -1,13 +1,13 @@
import { isEqual } from 'lodash-es';
import { P1_DISPLAY_NAME, P2_DISPLAY_NAME, Participant } from '../../helpers/Participant';
import { P1, P2, Participant } from '../../helpers/Participant';
import { ensureTwoParticipants, parseJid } from '../../helpers/participants';
import { IContext } from '../../helpers/types';
/**
* Tests PARTICIPANT_LEFT webhook.
*/
async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: string) {
async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: string, checkId = false) {
const { webhooksProxy } = ctx;
if (webhooksProxy) {
@@ -32,13 +32,15 @@ async function checkParticipantLeftHook(ctx: IContext, p: Participant, reason: s
expect(event.data.disconnectReason).toBe(reason);
expect(event.data.isBreakout).toBe(false);
expect(event.data.participantId).toBe(await p.getEndpointId());
expect(event.data.name).toBe(p.displayName);
expect(event.data.name).toBe(p.name);
const jwtPayload = ctx.data[`${p.displayName}-jwt-payload`];
if (checkId) {
const jwtPayload = ctx.data[`${p.name}-jwt-payload`];
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
expect(event.data.group).toBe(jwtPayload?.context?.group);
expect(event.customerId).toBe(process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', ''));
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
expect(event.data.group).toBe(jwtPayload?.context?.group);
expect(event.customerId).toBe(process.env.IFRAME_TENANT?.replace('vpaas-magic-cookie-', ''));
}
}
}
@@ -239,14 +241,14 @@ describe('Participants presence', () => {
const eventP1 = await p1.driver.waitUntil(() => p1.getIframeAPI().getEventResult('participantKickedOut'), {
timeout: 2000,
timeoutMsg: 'participantKickedOut event not received on participant1 side'
timeoutMsg: 'participantKickedOut event not received on p1 side'
});
const eventP2 = await p2.driver.waitUntil(() => p2.getIframeAPI().getEventResult('participantKickedOut'), {
timeout: 2000,
timeoutMsg: 'participantKickedOut event not received on participant2 side'
timeoutMsg: 'participantKickedOut event not received on p2 side'
});
await checkParticipantLeftHook(ctx, p2, 'kicked');
await checkParticipantLeftHook(ctx, p2, 'kicked', true);
expect(eventP1).toBeDefined();
expect(eventP2).toBeDefined();
@@ -318,7 +320,7 @@ describe('Participants presence', () => {
expect(event.data.moderator).toBe(false);
expect(event.data.name).toBe(await p2.getLocalDisplayName());
expect(event.data.participantId).toBe(await p2.getEndpointId());
expect(event.data.name).toBe(p2.displayName);
expect(event.data.name).toBe(p2.name);
}
await p1.switchToAPI();
@@ -343,8 +345,8 @@ describe('Participants presence', () => {
const p1EpId = await p1.getEndpointId();
const p2EpId = await p2.getEndpointId();
const newP1Name = P1_DISPLAY_NAME;
const newP2Name = P2_DISPLAY_NAME;
const newP1Name = P1;
const newP2Name = P2;
const newNames: ({ id: string; name: string; })[] = [ {
id: p2EpId,
name: newP2Name
@@ -412,7 +414,7 @@ describe('Participants presence', () => {
expect(eventConferenceLeft).toBeDefined();
expect(eventConferenceLeft.roomName).toBe(roomName);
await checkParticipantLeftHook(ctx, p1, 'left');
await checkParticipantLeftHook(ctx, p1, 'left', true);
if (webhooksProxy) {
// ROOM_DESTROYED webhook
// @ts-ignore

View File

@@ -157,13 +157,13 @@ async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksPr
allTranscripts.push(await p1.driver.waitUntil(() => p1.getIframeAPI()
.getEventResult('transcriptionChunkReceived'), {
timeout: 60000,
timeoutMsg: 'transcriptionChunkReceived event not received on participant1 side'
timeoutMsg: 'transcriptionChunkReceived event not received on p1 side'
}));
allTranscripts.push(await p2.driver.waitUntil(() => p2.getIframeAPI()
.getEventResult('transcriptionChunkReceived'), {
timeout: 60000,
timeoutMsg: 'transcriptionChunkReceived event not received on participant2 side'
timeoutMsg: 'transcriptionChunkReceived event not received on p2 side'
}));
if (webhooksProxy) {
@@ -220,6 +220,6 @@ async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksPr
expect(tr.language).toBe(language);
expect(tr.messageID).toBe(messageID);
expect(tr.participant.id).toBe(p1Id);
expect(tr.participant.name).toBe(p1.displayName);
expect(tr.participant.name).toBe(p1.name);
});
}

View File

@@ -35,5 +35,10 @@ async function kickParticipant2AndCheck() {
await p1.waitForParticipants(0);
// check that the kicked participant sees the kick reason dialog
expect(await p2.isLeaveReasonDialogOpen()).toBe(true);
// let's wait for this to appear at least 2 seconds
await p2.driver.waitUntil(
async () => p2.isLeaveReasonDialogOpen(), {
timeout: 2000,
timeoutMsg: 'No leave reason dialog shown for p2'
});
}

View File

@@ -1,4 +1,4 @@
import { P1_DISPLAY_NAME, P3_DISPLAY_NAME, Participant } from '../../helpers/Participant';
import { P1, P3, Participant } from '../../helpers/Participant';
import {
ensureOneParticipant,
ensureThreeParticipants,
@@ -34,8 +34,8 @@ describe('Lobby', () => {
const notificationText = await p2.getNotifications().getLobbyParticipantAccessGranted();
expect(notificationText.includes(P1_DISPLAY_NAME)).toBe(true);
expect(notificationText.includes(P3_DISPLAY_NAME)).toBe(true);
expect(notificationText.includes(P1)).toBe(true);
expect(notificationText.includes(P3)).toBe(true);
await p2.getNotifications().closeLobbyParticipantAccessGranted();
@@ -49,7 +49,7 @@ describe('Lobby', () => {
// now check third one display name in the room, is the one set in the prejoin screen
const name = await p1.getFilmstrip().getRemoteDisplayName(await p3.getEndpointId());
expect(name).toBe(P3_DISPLAY_NAME);
expect(name).toBe(P3);
await p3.hangup();
});
@@ -67,8 +67,8 @@ describe('Lobby', () => {
// deny notification on 2nd participant
const notificationText = await p2.getNotifications().getLobbyParticipantAccessDenied();
expect(notificationText.includes(P1_DISPLAY_NAME)).toBe(true);
expect(notificationText.includes(P3_DISPLAY_NAME)).toBe(true);
expect(notificationText.includes(P1)).toBe(true);
expect(notificationText.includes(P3)).toBe(true);
await p2.getNotifications().closeLobbyParticipantAccessDenied();
@@ -108,7 +108,7 @@ describe('Lobby', () => {
// now check third one display name in the room, is the one set in the prejoin screen
const name = await p1.getFilmstrip().getRemoteDisplayName(await p3.getEndpointId());
expect(name).toBe(P3_DISPLAY_NAME);
expect(name).toBe(P3);
await p3.hangup();
});
@@ -349,7 +349,7 @@ describe('Lobby', () => {
// check that moderator (participant 1) sees notification about participant in lobby
const name = await p1.getNotifications().getKnockingParticipantName();
expect(name).toBe(P3_DISPLAY_NAME);
expect(name).toBe(P3);
expect(await lobbyScreen.isLobbyRoomJoined()).toBe(true);
await p1ParticipantsPane.open();
@@ -379,7 +379,7 @@ async function enableLobby() {
await p1SecurityDialog.toggleLobby();
await p1SecurityDialog.waitForLobbyEnabled();
expect((await p2.getNotifications().getLobbyEnabledText()).includes(p1.displayName)).toBe(true);
expect((await p2.getNotifications().getLobbyEnabledText()).includes(p1.name)).toBe(true);
await p2.getNotifications().closeLobbyEnabled();
@@ -467,7 +467,7 @@ async function enterLobby(participant: Participant, enterDisplayName = false, us
// this check needs to be added once the functionality exists
// enter display name
await screen.enterDisplayName(P3_DISPLAY_NAME);
await screen.enterDisplayName(P3);
// check join button is enabled
classes = await joinButton.getAttribute('class');
@@ -495,7 +495,7 @@ async function enterLobby(participant: Participant, enterDisplayName = false, us
// check that moderator (participant 1) sees notification about participant in lobby
const name = await participant.getNotifications().getKnockingParticipantName();
expect(name).toBe(P3_DISPLAY_NAME);
expect(name).toBe(P3);
expect(await screen.isLobbyRoomJoined()).toBe(true);
return name;

View File

@@ -85,7 +85,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
},
capabilities: {
participant1: {
// participant1
p1: {
capabilities: {
browserName: 'chrome',
browserVersion: process.env.BROWSER_CHROME_BETA ? 'beta' : undefined,
@@ -95,7 +96,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
}
}
},
participant2: {
// participant2
p2: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
@@ -107,7 +109,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
]
}
},
participant3: {
// participant3
p3: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
@@ -120,7 +123,8 @@ export const config: WebdriverIO.MultiremoteConfig = {
]
}
},
participant4: {
// participant4
p4: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {

View File

@@ -38,7 +38,7 @@ const ffExcludes = [
const mergedConfig = merge(defaultConfig, {
ffExcludes,
capabilities: {
participant1: {
p1: {
capabilities: {
browserName: 'firefox',
browserVersion: process.env.BROWSER_FF_BETA ? 'beta' : undefined,
@@ -49,26 +49,26 @@ const mergedConfig = merge(defaultConfig, {
acceptInsecureCerts: process.env.ALLOW_INSECURE_CERTS === 'true'
}
},
participant2: {
p2: {
capabilities: {
'wdio:exclude': [
...defaultConfig.capabilities.participant2.capabilities['wdio:exclude'],
...defaultConfig.capabilities.p2.capabilities['wdio:exclude'],
...ffExcludes
]
}
},
participant3: {
p3: {
capabilities: {
'wdio:exclude': [
...defaultConfig.capabilities.participant3.capabilities['wdio:exclude'],
...defaultConfig.capabilities.p3.capabilities['wdio:exclude'],
...ffExcludes
]
}
},
participant4: {
p4: {
capabilities: {
'wdio:exclude': [
...defaultConfig.capabilities.participant4.capabilities['wdio:exclude'],
...defaultConfig.capabilities.p4.capabilities['wdio:exclude'],
...ffExcludes
]
}
@@ -78,6 +78,6 @@ const mergedConfig = merge(defaultConfig, {
// Remove the chrome options from the first participant
// @ts-ignore
mergedConfig.capabilities.participant1.capabilities['goog:chromeOptions'] = undefined;
mergedConfig.capabilities.p1.capabilities['goog:chromeOptions'] = undefined;
export const config = mergedConfig;

View File

@@ -17,14 +17,14 @@ const mergedConfig = {
path: gridUrl.pathname
};
mergedConfig.capabilities.participant1.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.participant1.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.participant2.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.participant2.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.participant3.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.participant3.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.participant4.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.participant4.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.p1.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.p1.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.p2.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.p2.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.p3.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.p3.capabilities['goog:chromeOptions'].args);
mergedConfig.capabilities.p4.capabilities['goog:chromeOptions'].args
= updateRemoteResource(mergedConfig.capabilities.p4.capabilities['goog:chromeOptions'].args);
export const config = mergedConfig;