feat(tests): First test from torture to meet. (#15298)

* feat(tests): First test from torture to meet.

* squash: Fixes logging as per comments.

* squash: Fixes some eslint errors.

* squash: Drop no needed await and async declarations.

* squash: Simplify syntax.

* squash: Disable blur everywhere not only FF.

* squash: Use allSettled.

* squash: Prettify intervals and timeouts.

* squash: Use uuids for torture rooms.

* squash: Introduce helper methods in Participant for toolbar and filmstrip.

* squash: Changes headless resolution to a standard 720p.

* squash: Adds env BASE_URL.

* squash: Fix some eslint errors.

* squash: Fix js error.

* squash: Fix participant logs.

* squash: Move bag to Promise.all.

* squash: More types thing.

* squash: Fix more ts errors.

* squash: Bumps version to include 6d146cd332

* squash: More ts stuff.

* squash: Fixes last ts errors.

* squash: Drop eslint rule.

* squash: Update default configs.

* squash: Drop and docs eslint.
This commit is contained in:
Дамян Минков
2024-11-15 06:26:35 -06:00
committed by GitHub
parent aa1c3c0ad8
commit 5cd7b9be38
14 changed files with 9487 additions and 26 deletions

5
.gitignore vendored
View File

@@ -109,3 +109,8 @@ react-native-sdk/node_modules
react-native-sdk/react
react-native-sdk/service
react-native-sdk/sounds
# tests
tests/.env
test-results

View File

@@ -1629,7 +1629,6 @@ var config = {
iAmRecorder
iAmSipGateway
microsoftApiApplicationClientID
requireDisplayName
*/
/**

8538
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -49,6 +49,7 @@
"@vladmandic/human-models": "2.5.9",
"@xmldom/xmldom": "0.8.7",
"abab": "2.0.6",
"allure-commandline": "2.32.0",
"amplitude-js": "8.21.9",
"base64-js": "1.5.1",
"bc-css-flags": "3.0.0",
@@ -138,6 +139,7 @@
"@types/amplitude-js": "8.16.5",
"@types/audioworklet": "0.0.29",
"@types/dom-screen-wake-lock": "1.0.1",
"@types/jasmine": "5.1.4",
"@types/js-md5": "0.4.3",
"@types/lodash-es": "4.17.12",
"@types/moment-duration-format": "2.2.6",
@@ -158,6 +160,12 @@
"@types/zxcvbn": "4.4.1",
"@typescript-eslint/eslint-plugin": "5.59.5",
"@typescript-eslint/parser": "5.59.5",
"@wdio/allure-reporter": "9.2.14",
"@wdio/cli": "9.2.14",
"@wdio/globals": "9.2.14",
"@wdio/jasmine-framework": "9.2.14",
"@wdio/junit-reporter": "9.2.14",
"@wdio/local-runner": "9.2.15",
"babel-loader": "9.1.0",
"babel-plugin-optional-require": "0.3.1",
"circular-dependency-plugin": "5.2.0",
@@ -179,6 +187,7 @@
"ts-loader": "9.4.2",
"typescript": "5.0.4",
"unorm": "1.6.0",
"webdriverio": "9.2.14",
"webpack": "5.95.0",
"webpack-bundle-analyzer": "4.4.2",
"webpack-cli": "4.9.0",
@@ -205,7 +214,8 @@
"validate": "npm ls",
"tsc-test:web": "tsc --project tsconfig.web.json --listFilesOnly | grep -v node_modules | grep native",
"tsc-test:native": "tsc --project tsconfig.native.json --listFilesOnly | grep -v node_modules | grep web",
"start": "make dev"
"start": "make dev",
"test": "DOTENV_CONFIG_PATH=tests/.env wdio run tests/wdio.conf.ts"
},
"resolutions": {
"@types/react": "17.0.14",

15
tests/.eslintrc.js Normal file
View File

@@ -0,0 +1,15 @@
module.exports = {
'extends': [
'../.eslintrc.js'
],
'overrides': [
{
'files': [ '*.ts', '*.tsx' ],
extends: [ '@jitsi/eslint-config/typescript' ],
parserOptions: {
sourceType: 'module',
project: [ './tests/tsconfig.json' ]
}
}
]
};

15
tests/env.example Normal file
View File

@@ -0,0 +1,15 @@
# The base url that will be used for the test (default will be using "https://alpha.jitsi.net")
#BASE_URL=
# To be able to match a domain to a specific address
# The format is "MAP example.com 1.2.3.4"
#RESOLVER_RULES=
# Ignore certificate errors (self-signed certificates)
#ALLOW_INSECURE_CERTS=true
# Whether to run the browser in headless mode
#HEADLESS=false
# The path to the browser video capture file
#VIDEO_CAPTURE_FILE=tests/resources/FourPeople_1280x720_30.y4m

View File

@@ -0,0 +1,289 @@
/* global APP $ */
import { multiremotebrowser } from '@wdio/globals';
import { IConfig } from '../../react/features/base/config/configType';
import { urlObjectToString } from '../../react/features/base/util/uri';
import Filmstrip from '../pageobjects/Filmstrip';
import Toolbar from '../pageobjects/Toolbar';
import { LOG_PREFIX, logInfo } from './browserLogger';
import { IContext } from './participants';
/**
* Participant.
*/
export class Participant {
/**
* The current context.
*
* @private
*/
private context: { roomName: string; };
private _name: string;
private _endpointId: string;
/**
* The default config to use when joining.
*
* @private
*/
private config = {
analytics: {
disabled: true
},
debug: true,
requireDisplayName: false,
testing: {
testMode: true
},
disableAP: true,
disable1On1Mode: true,
disableModeratorIndicator: true,
enableTalkWhileMuted: false,
gatherStats: true,
p2p: {
enabled: false,
useStunTurn: false
},
pcStatsInterval: 1500,
prejoinConfig: {
enabled: false
},
toolbarConfig: {
alwaysVisible: true
}
} as IConfig;
/**
* Creates a participant with given name.
*
* @param {string} name - The name of the participant.
*/
constructor(name: string) {
this._name = name;
}
/**
* Returns participant endpoint ID.
*
* @returns {Promise<string>} The endpoint ID.
*/
async getEndpointId() {
if (!this._endpointId) {
this._endpointId = await this.driver.execute(() => { // eslint-disable-line arrow-body-style
return APP.conference.getMyUserId();
});
}
return this._endpointId;
}
/**
* The driver it uses.
*/
get driver() {
return multiremotebrowser.getInstance(this._name);
}
/**
* The name.
*/
get name() {
return this._name;
}
/**
* Adds a log to the participants log file.
*
* @param {string} message - The message to log.
* @returns {void}
*/
log(message: string) {
logInfo(this.driver, message);
}
/**
* Joins conference.
*
* @param {IContext} context - The context.
* @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
* @returns {Promise<void>}
*/
async joinConference(context: IContext, skipInMeetingChecks = false) {
this.context = context;
const url = urlObjectToString({
room: context.roomName,
configOverwrite: this.config,
interfaceConfigOverwrite: {
SHOW_CHROME_EXTENSION_BANNER: false
},
userInfo: {
displayName: this._name
}
}) || '';
await this.driver.setTimeout({ 'pageLoad': 30000 });
await this.driver.url(url);
await this.waitForPageToLoad();
await this.waitToJoinMUC();
await this.postLoadProcess(skipInMeetingChecks);
}
/**
* Loads stuff after the page loads.
*
* @param {boolean} skipInMeetingChecks - Whether to skip in meeting checks.
* @returns {Promise<void>}
* @private
*/
private async postLoadProcess(skipInMeetingChecks: boolean) {
const driver = this.driver;
const parallel = [];
parallel.push(driver.execute((name, sessionId, prefix) => {
APP.UI.dockToolbar(true);
// disable keyframe animations (.fadeIn and .fadeOut classes)
$('<style>.notransition * { '
+ 'animation-duration: 0s !important; -webkit-animation-duration: 0s !important; transition:none; '
+ '} </style>') // @ts-ignore
.appendTo(document.head);
// @ts-ignore
$('body').toggleClass('notransition');
document.title = `${name}`;
console.log(`${new Date().toISOString()} ${prefix} sessionId: ${sessionId}`);
// disable the blur effect in firefox as it has some performance issues
const blur = document.querySelector('.video_blurred_container');
if (blur) {
// @ts-ignore
document.querySelector('.video_blurred_container').style.display = 'none';
}
}, this._name, driver.sessionId, LOG_PREFIX));
if (skipInMeetingChecks) {
await Promise.allSettled(parallel);
return;
}
parallel.push(this.waitForIceConnected());
parallel.push(this.waitForSendReceiveData());
await Promise.all(parallel);
}
/**
* Waits for the page to load.
*
* @returns {Promise<void>}
*/
async waitForPageToLoad() {
return this.driver.waitUntil(
() => this.driver.execute(() => document.readyState === 'complete'),
{
timeout: 30_000, // 30 seconds
timeoutMsg: 'Timeout waiting for Page Load Request to complete.'
}
);
}
/**
* Waits to join the muc.
*
* @returns {Promise<void>}
*/
async waitToJoinMUC() {
return this.driver.waitUntil(
() => this.driver.execute(() => APP.conference.isJoined()),
{
timeout: 10_000, // 10 seconds
timeoutMsg: 'Timeout waiting to join muc.'
}
);
}
/**
* Waits for ICE to get connected.
*
* @returns {Promise<void>}
*/
async waitForIceConnected() {
const driver = this.driver;
return driver.waitUntil(async () =>
driver.execute(() => APP.conference.getConnectionState() === 'connected'), {
timeout: 15_000,
timeoutMsg: 'expected ICE to be connected for 15s'
});
}
/**
* Waits for send and receive data.
*
* @returns {Promise<void>}
*/
async waitForSendReceiveData() {
const driver = this.driver;
return driver.waitUntil(async () =>
driver.execute(() => {
const stats = APP.conference.getStats();
const bitrateMap = stats?.bitrate || {};
const rtpStats = {
uploadBitrate: bitrateMap.upload || 0,
downloadBitrate: bitrateMap.download || 0
};
return rtpStats.uploadBitrate > 0 && rtpStats.downloadBitrate > 0;
}), {
timeout: 15_000,
timeoutMsg: 'expected to receive/send data in 15s'
});
}
/**
* Waits for remote streams.
*
* @param {number} number - The number of remote streams o wait for.
* @returns {Promise<void>}
*/
waitForRemoteStreams(number: number) {
const driver = this.driver;
return driver.waitUntil(async () =>
driver.execute(count => APP.conference.getNumberOfParticipantsWithTracks() >= count, number), {
timeout: 15_000,
timeoutMsg: 'expected remote streams in 15s'
});
}
/**
* Returns the toolbar for this participant.
*
* @returns {Toolbar}
*/
getToolbar() {
return new Toolbar(this);
}
/**
* Returns the filmstrip for this participant.
*
* @returns {Filmstrip}
*/
getFilmstrip() {
return new Filmstrip(this);
}
}

View File

@@ -0,0 +1,67 @@
import fs from 'node:fs';
/**
* A prefix to use for all messages we add to the console log.
*/
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} folder - The folder to save the file.
* @returns {void}
*/
export function initLogger(driver: WebdriverIO.Browser, name: string, folder: string) {
// @ts-ignore
driver.logFile = `${folder}/${name}.log`;
driver.sessionSubscribe({ events: [ 'log.entryAdded' ] });
driver.on('log.entryAdded', (entry: any) => {
try {
// @ts-ignore
fs.appendFileSync(driver.logFile, `${entry.text}\n`);
} catch (err) {
console.error(err);
}
});
}
/**
* Returns the content of the log file.
*
* @param {WebdriverIO.Browser} driver - The driver which log file is requested.
* @returns {string} The content of the log file.
*/
export function getLogs(driver: WebdriverIO.Browser) {
// @ts-ignore
if (!driver.logFile) {
return;
}
// @ts-ignore
return fs.readFileSync(driver.logFile, 'utf8');
}
/**
* Logs a message in the logfile.
*
* @param {WebdriverIO.Browser} driver - The participant in which log file to write.
* @param {string} message - The message to add.
* @returns {void}
*/
export function logInfo(driver: WebdriverIO.Browser, message: string) {
// @ts-ignore
if (!driver.logFile) {
return;
}
try {
// @ts-ignore
fs.appendFileSync(driver.logFile, `${new Date().toISOString()} ${LOG_PREFIX} ${message}\n`);
} catch (err) {
console.error(err);
}
}

View File

@@ -0,0 +1,80 @@
import { Participant } from './Participant';
export type IContext = {
p1: Participant;
p2: Participant;
p3: Participant;
p4: Participant;
roomName: string;
};
/**
* Generate a random room name.
*
* @returns {string} - The random room name.
*/
function generateRandomRoomName(): string {
return `jitsimeettorture-${crypto.randomUUID()}}`;
}
/**
* Ensure that there is on participant.
*
* @param {IContext} context - The context.
* @returns {Promise<void>}
*/
export async function ensureOneParticipant(context: IContext): Promise<void> {
context.roomName = generateRandomRoomName();
context.p1 = new Participant('participant1');
await context.p1.joinConference(context, true);
}
/**
* Ensure that there are three participants.
*
* @param {Object} context - The context.
* @returns {Promise<void>}
*/
export async function ensureThreeParticipants(context: IContext): Promise<void> {
context.roomName = generateRandomRoomName();
const p1 = new Participant('participant1');
const p2 = new Participant('participant2');
const p3 = new Participant('participant3');
context.p1 = p1;
context.p2 = p2;
context.p3 = p3;
// these need to be all, so we get the error when one fails
await Promise.all([
p1.joinConference(context),
p2.joinConference(context),
p3.joinConference(context)
]);
await Promise.all([
p1.waitForRemoteStreams(2),
p2.waitForRemoteStreams(2),
p3.waitForRemoteStreams(2)
]);
}
/**
* Toggles the mute state of a specific Meet conference participant and verifies that a specific other Meet
* conference participants sees a specific mute state for the former.
*
* @param {Participant} testee - The {@code Participant} which represents the Meet conference participant whose
* mute state is to be toggled.
* @param {Participant} observer - The {@code Participant} which represents the Meet conference participant to verify
* the mute state of {@code testee}.
* @returns {Promise<void>}
*/
export async function toggleMuteAndCheck(testee: Participant, observer: Participant): Promise<void> {
await testee.getToolbar().clickAudioMuteButton();
await observer.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
await testee.getFilmstrip().assertAudioMuteIconIsDisplayed(testee);
}

View File

@@ -0,0 +1,46 @@
import { Participant } from '../helpers/Participant';
/**
* Filmstrip elements.
*/
export default class Filmstrip {
private participant: Participant;
/**
* Initializes for a participant.
*
* @param {Participant} participant - The participant.
*/
constructor(participant: Participant) {
this.participant = participant;
}
/**
* Asserts that {@code participant} shows or doesn't show the audio
* mute icon for the conference participant identified by
* {@code testee}.
*
* @param {Participant} testee - The {@code WebParticipant} for whom we're checking the status of audio muted icon.
* @param {boolean} reverse - If {@code true}, the method will assert the absence of the "mute" icon;
* otherwise, it will assert its presence.
* @returns {Promise<void>}
*/
async assertAudioMuteIconIsDisplayed(testee: Participant, reverse = false) {
let id;
if (testee === this.participant) {
id = 'localVideoContainer';
} else {
id = `participant_${await testee.getEndpointId()}`;
}
const mutedIconXPath
= `//span[@id='${id}']//span[contains(@id, 'audioMuted')]//*[local-name()='svg' and @id='mic-disabled']`;
await this.participant.driver.$(mutedIconXPath).waitForDisplayed({
reverse,
timeout: 2000,
timeoutMsg: `Audio mute icon is not displayed for ${testee.name}`
});
}
}

View File

@@ -0,0 +1,66 @@
// eslint-disable-next-line no-unused-vars
import { Participant } from '../helpers/Participant';
const AUDIO_MUTE = 'Mute microphone';
const AUDIO_UNMUTE = 'Unmute microphone';
/**
* The toolbar elements.
*/
export default class Toolbar {
private participant: Participant;
/**
* Creates toolbar for a participant.
*
* @param {Participant} participant - The participants.
*/
constructor(participant: Participant) {
this.participant = participant;
}
/**
* Returns the button.
*
* @param {string} accessibilityCSSSelector - The selector to find the button.
* @returns {WebdriverIO.Element} The button.
* @private
*/
private getButton(accessibilityCSSSelector: string) {
return this.participant.driver.$(`[aria-label^="${accessibilityCSSSelector}"]`);
}
/**
* The audio mute button.
*/
get audioMuteBtn() {
return this.getButton(AUDIO_MUTE);
}
/**
* The audio unmute button.
*/
get audioUnMuteBtn() {
return this.getButton(AUDIO_UNMUTE);
}
/**
* Clicks audio mute button.
*
* @returns {Promise<void>}
*/
async clickAudioMuteButton() {
await this.participant.log('Clicking on: Audio Mute Button');
await this.audioMuteBtn.click();
}
/**
* Clicks audio unmute button.
*
* @returns {Promise<void>}
*/
async clickAudioUnmuteButton() {
await this.participant.log('Clicking on: Audio Unmute Button');
await this.audioUnMuteBtn.click();
}
}

View File

@@ -0,0 +1,94 @@
/* global APP */
import type { Participant } from '../../helpers/Participant';
import { IContext, ensureThreeParticipants, toggleMuteAndCheck } from '../../helpers/participants';
describe('ActiveSpeaker ', () => {
const context = {} as IContext;
it('testActiveSpeaker', async () => {
await ensureThreeParticipants(context);
await toggleMuteAndCheck(context.p1, context.p2);
await toggleMuteAndCheck(context.p2, context.p1);
await toggleMuteAndCheck(context.p3, context.p1);
// participant1 becomes active speaker - check from participant2's perspective
await testActiveSpeaker(context.p1, context.p2, context.p3);
// participant3 becomes active speaker - check from participant2's perspective
await testActiveSpeaker(context.p3, context.p2, context.p1);
// participant2 becomes active speaker - check from participant1's perspective
await testActiveSpeaker(context.p2, context.p1, context.p3);
// check the displayed speakers, there should be only one speaker
await assertOneDominantSpeaker(context.p1);
await assertOneDominantSpeaker(context.p2);
await assertOneDominantSpeaker(context.p3);
});
});
/**
* Tries to make given participant an active speaker by un-muting it.
* Verifies from {@code participant2}'s perspective that the active speaker
* has been displayed on the large video area. Mutes him back.
*
* @param {Participant} activeSpeaker - <tt>Participant</tt> instance of the participant who will be tested as an
* active speaker.
* @param {Participant} otherParticipant1 - <tt>Participant</tt> of the participant who will be observing and verifying
* active speaker change.
* @param {Participant} otherParticipant2 - Used only to print some debugging info.
* @returns {Promise<void>}
*/
async function testActiveSpeaker(
activeSpeaker: Participant, otherParticipant1: Participant, otherParticipant2: Participant) {
activeSpeaker.log(`Start testActiveSpeaker for participant: ${activeSpeaker.name}`);
const speakerEndpoint = await activeSpeaker.getEndpointId();
// just a debug print to go in logs
activeSpeaker.log('Unmuting in testActiveSpeaker');
// Unmute
await activeSpeaker.getToolbar().clickAudioUnmuteButton();
// just a debug print to go in logs
otherParticipant1.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
otherParticipant2.log(`Participant unmuted in testActiveSpeaker ${speakerEndpoint}`);
await activeSpeaker.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker, true);
// Verify that the user is now an active speaker from otherParticipant1's perspective
const otherParticipant1Driver = otherParticipant1.driver;
await otherParticipant1Driver.waitUntil(
() => otherParticipant1Driver.execute((id: string) => APP.UI.getLargeVideoID() === id, speakerEndpoint),
{
timeout: 30_1000, // 30 seconds
timeoutMsg: 'Active speaker not displayed on large video.'
});
// just a debug print to go in logs
activeSpeaker.log('Muting in testActiveSpeaker');
// Mute back again
await activeSpeaker.getToolbar().clickAudioMuteButton();
// just a debug print to go in logs
otherParticipant1.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
otherParticipant2.log(`Participant muted in testActiveSpeaker ${speakerEndpoint}`);
await otherParticipant1.getFilmstrip().assertAudioMuteIconIsDisplayed(activeSpeaker);
}
/**
* Asserts that the number of small videos with the dominant speaker
* indicator displayed equals 1.
*
* @param {Participant} participant - The participant to check.
* @returns {Promise<void>}
*/
async function assertOneDominantSpeaker(participant: Participant) {
expect(await participant.driver.$$(
'//span[not(contains(@class, "tile-view"))]//span[contains(@class,"dominant-speaker")]').length).toBe(1);
}

12
tests/tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"include": ["**/*.ts", "../globals.d.ts"],
"extends": "../tsconfig.web",
"compilerOptions": {
"types": [
"node",
"@wdio/globals/types",
"@types/jasmine",
"@wdio/jasmine-framework"
]
}
}

273
tests/wdio.conf.ts Normal file
View File

@@ -0,0 +1,273 @@
import AllureReporter from '@wdio/allure-reporter';
import { multiremotebrowser } from '@wdio/globals';
import { Buffer } from 'buffer';
import process from 'node:process';
import { getLogs, initLogger, logInfo } from './helpers/browserLogger';
// eslint-disable-next-line @typescript-eslint/no-var-requires
const allure = require('allure-commandline');
// This is deprecated without alternative (https://github.com/nodejs/node/issues/32483)
// we need it to be able to reuse jitsi-meet code in tests
require.extensions['.web.ts'] = require.extensions['.ts'];
const chromeArgs = [
'--allow-insecure-localhost',
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
'--disable-plugins',
'--mute-audio',
'--disable-infobars',
'--autoplay-policy=no-user-gesture-required',
'--auto-select-desktop-capture-source=Your Entire screen',
'--no-sandbox',
'--disable-dev-shm-usage',
'--disable-setuid-sandbox',
'--use-file-for-fake-audio-capture=tests/resources/fakeAudioStream.wav'
];
if (process.env.RESOLVER_RULES) {
chromeArgs.push(`--host-resolver-rules=${process.env.RESOLVER_RULES}`);
}
if (process.env.ALLOW_INSECURE_CERTS === 'true') {
chromeArgs.push('--ignore-certificate-errors');
}
if (process.env.HEADLESS === 'true') {
chromeArgs.push('--headless');
chromeArgs.push('--window-size=1280,720');
}
if (process.env.VIDEO_CAPTURE_FILE) {
chromeArgs.push(`use-file-for-fake-video-capture=${process.env.VIDEO_CAPTURE_FILE}`);
}
const chromePreferences = {
'intl.accept_languages': 'en-US'
};
const TEST_RESULTS_DIR = 'test-results';
export const config: WebdriverIO.MultiremoteConfig = {
runner: 'local',
specs: [
'specs/**'
],
maxInstances: 1,
baseUrl: process.env.BASE_URL || 'https://alpha.jitsi.net/torture',
tsConfigPath: './tsconfig.json',
// Default timeout for all waitForXXX commands.
waitforTimeout: 1000,
// Default timeout in milliseconds for request
// if browser driver or grid doesn't send response
connectionRetryTimeout: 15_000,
// Default request retries count
connectionRetryCount: 3,
framework: 'jasmine',
jasmineOpts: {
defaultTimeoutInterval: 60_000
},
capabilities: {
participant1: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: chromeArgs,
prefs: chromePreferences
}
}
},
participant2: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: chromeArgs,
prefs: chromePreferences
},
'wdio:exclude': [
'specs/alone/**'
]
}
},
participant3: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: chromeArgs,
prefs: chromePreferences
},
'wdio:exclude': [
'specs/2way/**'
]
}
},
participant4: {
capabilities: {
browserName: 'chrome',
'goog:chromeOptions': {
args: chromeArgs,
prefs: chromePreferences
},
'wdio:exclude': [
'specs/3way/**'
]
}
}
},
// Level of logging verbosity: trace | debug | info | warn | error | silent
logLevel: 'trace',
logLevels: {
webdriver: 'info'
},
// Set directory to store all logs into
outputDir: TEST_RESULTS_DIR,
reporters: [
[ 'junit', {
outputDir: TEST_RESULTS_DIR,
outputFileFormat(options) { // optional
return `results-${options.cid}.xml`;
}
} ],
[ 'allure', {
// addConsoleLogs: true,
outputDir: `${TEST_RESULTS_DIR}/allure-results`,
disableWebdriverStepsReporting: true,
disableWebdriverScreenshotsReporting: true,
useCucumberStepReporter: false
} ]
],
// =====
// Hooks
// =====
/**
* 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.
*
* @returns {Promise<void>}
*/
before() {
multiremotebrowser.instances.forEach((instance: string) => {
initLogger(multiremotebrowser.getInstance(instance), instance, TEST_RESULTS_DIR);
});
},
/**
* Gets executed before the suite starts (in Mocha/Jasmine only).
*
* @param {Object} suite - Suite details.
* @returns {Promise<void>}
*/
beforeSuite(suite) {
multiremotebrowser.instances.forEach((instance: string) => {
logInfo(multiremotebrowser.getInstance(instance),
`---=== Begin ${suite.file.substring(suite.file.lastIndexOf('/') + 1)} ===---`);
});
},
/**
* Function to be executed before a test (in Mocha/Jasmine only).
*
* @param {Object} test - Test object.
* @returns {Promise<void>}
*/
beforeTest(test) {
multiremotebrowser.instances.forEach((instance: string) => {
logInfo(multiremotebrowser.getInstance(instance), `---=== Start test ${test.fullName} ===---`);
});
},
/**
* Function to be executed after a test (in Mocha/Jasmine only).
*
* @param {Object} test - Test object.
* @param {Object} context - Scope object the test was executed with.
* @param {Error} error - Error object in case the test fails, otherwise `undefined`.
* @returns {Promise<void>}
*/
async afterTest(test, context, { error }) {
multiremotebrowser.instances.forEach((instance: string) =>
logInfo(multiremotebrowser.getInstance(instance), `---=== End test ${test.fullName} ===---`));
if (error) {
const allProcessing: Promise<any>[] = [];
multiremotebrowser.instances.forEach((instance: string) => {
const bInstance = multiremotebrowser.getInstance(instance);
allProcessing.push(bInstance.takeScreenshot().then(shot => {
AllureReporter.addAttachment(
`Screenshot-${instance}`,
Buffer.from(shot, 'base64'),
'image/png');
}));
AllureReporter.addAttachment(`console-logs-${instance}`, getLogs(bInstance) || '', 'text/plain');
allProcessing.push(bInstance.getPageSource().then(source => {
AllureReporter.addAttachment(`html-source-${instance}`, source, 'text/plain');
}));
});
await Promise.all(allProcessing);
}
},
/**
* Hook that gets executed after the suite has ended (in Mocha/Jasmine only).
*
* @param {Object} suite - Suite details.
* @returns {Promise<void>}
*/
afterSuite(suite) {
multiremotebrowser.instances.forEach((instance: string) => {
logInfo(multiremotebrowser.getInstance(instance),
`---=== End ${suite.file.substring(suite.file.lastIndexOf('/') + 1)} ===---`);
});
},
/**
* Gets executed after all workers have shut down and the process is about to exit.
* An error thrown in the `onComplete` hook will result in the test run failing.
*
* @returns {Promise<void>}
*/
onComplete() {
const reportError = new Error('Could not generate Allure report');
const generation = allure([
'generate', `${TEST_RESULTS_DIR}/allure-results`,
'--clean', '--single-file',
'--report-dir', `${TEST_RESULTS_DIR}/allure-report`
]);
return new Promise<void>((resolve, reject) => {
const generationTimeout = setTimeout(
() => reject(reportError),
5000);
// @ts-ignore
generation.on('exit', eCode => {
clearTimeout(generationTimeout);
if (eCode !== 0) {
return reject(reportError);
}
console.log('Allure report successfully generated');
resolve();
});
});
}
};