snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
1193
tests/helpers/Participant.ts
Normal file
1193
tests/helpers/Participant.ts
Normal file
File diff suppressed because it is too large
Load Diff
128
tests/helpers/TestProperties.ts
Normal file
128
tests/helpers/TestProperties.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
/**
|
||||
* An interface that tests can export (as a TEST_PROPERTIES property) to define what they require.
|
||||
*/
|
||||
export type ITestProperties = {
|
||||
/**
|
||||
* A more detailed description of what the test does, to be included in the Allure report.
|
||||
*/
|
||||
description?: string;
|
||||
/** The test requires the webhook proxy to be available. */
|
||||
requireWebhookProxy: boolean;
|
||||
/** Whether the test should be retried. */
|
||||
retry: boolean;
|
||||
/** The test requires jaas, it should be skipped when the jaas configuration is not enabled. */
|
||||
useJaas: boolean;
|
||||
/** The test uses the webhook proxy if available. */
|
||||
useWebhookProxy: boolean;
|
||||
usesBrowsers: string[];
|
||||
};
|
||||
|
||||
const defaultProperties: ITestProperties = {
|
||||
useWebhookProxy: false,
|
||||
requireWebhookProxy: false,
|
||||
retry: false,
|
||||
useJaas: false,
|
||||
usesBrowsers: [ 'p1' ]
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* Maps a test filename to its registered properties.
|
||||
*/
|
||||
export const testProperties: Record<string, ITestProperties> = {};
|
||||
|
||||
/**
|
||||
* Set properties for a test file. This was needed because I couldn't find a hook that executes with describe() before
|
||||
* the code in wdio.conf.ts's before() hook. The intention is for tests to execute this directly. The properties don't
|
||||
* change dynamically.
|
||||
*
|
||||
* @param filename the absolute path to the test file
|
||||
* @param properties the properties to set for the test file, defaults will be applied for missing properties
|
||||
*/
|
||||
export function setTestProperties(filename: string, properties: Partial<ITestProperties>): void {
|
||||
if (testProperties[filename]) {
|
||||
console.warn(`Test properties for ${filename} are already set. Overwriting.`);
|
||||
}
|
||||
|
||||
testProperties[filename] = { ...defaultProperties, ...properties };
|
||||
}
|
||||
|
||||
let testFilesLoaded = false;
|
||||
|
||||
/**
|
||||
* Loads test files to populate the testProperties registry. This function:
|
||||
* 1. Mocks test framework globals to prevent test registration
|
||||
* 2. require()s each file to trigger setTestProperties calls
|
||||
* 3. Restores original test framework functions
|
||||
*
|
||||
* @param files - Array of file names to load
|
||||
*/
|
||||
export function loadTestFiles(files: string[]): void {
|
||||
if (testFilesLoaded) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Temporarily override test functions to prevent tests registering at this stage. We only want TestProperties to be
|
||||
// loaded.
|
||||
const originalTestFunctions: Record<string, any> = {};
|
||||
const testGlobals = [ 'describe', 'it', 'test', 'expect', 'beforeEach', 'afterEach', 'before', 'after', 'beforeAll',
|
||||
'afterAll', 'suite', 'setup', 'teardown' ];
|
||||
|
||||
testGlobals.forEach(fn => {
|
||||
originalTestFunctions[fn] = (global as any)[fn];
|
||||
const stub: any = () => {
|
||||
// do nothing
|
||||
};
|
||||
|
||||
// Tolerate any expect.extend({...}) calls a spec or helper might do at module load.
|
||||
if (fn === 'expect') {
|
||||
stub.extend = () => { /* no-op */ };
|
||||
}
|
||||
|
||||
(global as any)[fn] = stub;
|
||||
});
|
||||
|
||||
try {
|
||||
// Load all spec files to trigger setTestProperties calls
|
||||
files.forEach(file => {
|
||||
try {
|
||||
require(file);
|
||||
if (!testProperties[file]) {
|
||||
// If no properties were set, apply defaults
|
||||
setTestProperties(file, { ...defaultProperties });
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`Warning: Could not analyze ${file}:`, (error as Error).message);
|
||||
}
|
||||
});
|
||||
testFilesLoaded = true;
|
||||
|
||||
} finally {
|
||||
// Restore original functions
|
||||
testGlobals.forEach(fn => {
|
||||
if (originalTestFunctions[fn] !== undefined) {
|
||||
(global as any)[fn] = originalTestFunctions[fn];
|
||||
} else {
|
||||
delete (global as any)[fn];
|
||||
}
|
||||
});
|
||||
// Clear require cache for analyzed files so they can be loaded fresh by WebDriverIO
|
||||
files.forEach(file => {
|
||||
delete require.cache[file];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param testFilePath - The absolute path to the test file
|
||||
* @returns Promise<ITestProperties> - The test properties with defaults applied
|
||||
*/
|
||||
export async function getTestProperties(testFilePath: string): Promise<ITestProperties> {
|
||||
const properties = testProperties[testFilePath] || { ...defaultProperties };
|
||||
|
||||
if (properties.requireWebhookProxy && !properties.useWebhookProxy) {
|
||||
properties.useWebhookProxy = true;
|
||||
}
|
||||
|
||||
return properties;
|
||||
}
|
||||
76
tests/helpers/TestsConfig.ts
Normal file
76
tests/helpers/TestsConfig.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* Global configuration that the tests are run with. Loaded from environment variables.
|
||||
*/
|
||||
export const config = {
|
||||
/** Enable debug logging. Note this includes private information from .env */
|
||||
debug: Boolean(process.env.JITSI_DEBUG?.trim()),
|
||||
expectationsFile: process.env.EXPECTATIONS?.trim(),
|
||||
jaas: {
|
||||
customerId: (() => {
|
||||
if (typeof process.env.JAAS_TENANT !== 'undefined') {
|
||||
return process.env.JAAS_TENANT?.trim()?.replace('vpaas-magic-cookie-', '');
|
||||
}
|
||||
|
||||
return process.env.IFRAME_TENANT?.trim()?.replace('vpaas-magic-cookie-', '');
|
||||
})(),
|
||||
/** Whether the configuration for JaaS specific tests is enabled. */
|
||||
enabled: Boolean(
|
||||
(process.env.JAAS_TENANT || process.env.IFRAME_TENANT)
|
||||
&& (process.env.JAAS_PRIVATE_KEY_PATH || process.env.JWT_PRIVATE_KEY_PATH)
|
||||
&& (process.env.JAAS_KID || process.env.JWT_KID)),
|
||||
/** The JaaS key ID, used to sign the tokens. */
|
||||
kid: (() => {
|
||||
if (typeof process.env.JAAS_KID !== 'undefined') {
|
||||
return process.env.JAAS_KID?.trim();
|
||||
}
|
||||
|
||||
return process.env.JWT_KID?.trim();
|
||||
})(),
|
||||
/** The path to the JaaS private key, used to sign JaaS tokens. */
|
||||
privateKeyPath: (() => {
|
||||
if (typeof process.env.JAAS_PRIVATE_KEY_PATH != 'undefined') {
|
||||
return process.env.JAAS_PRIVATE_KEY_PATH?.trim();
|
||||
}
|
||||
|
||||
return process.env.JWT_PRIVATE_KEY_PATH?.trim();
|
||||
})(),
|
||||
/** The JaaS tenant (vpaas-magic-cookie-<ID>) . */
|
||||
tenant: (() => {
|
||||
if (typeof process.env.JAAS_TENANT !== 'undefined') {
|
||||
return process.env.JAAS_TENANT?.trim();
|
||||
}
|
||||
|
||||
return process.env.IFRAME_TENANT?.trim();
|
||||
})()
|
||||
},
|
||||
jwt: {
|
||||
kid: process.env.JWT_KID?.trim(),
|
||||
/** A pre-configured token used by some tests. */
|
||||
preconfiguredJwt: process.env.JWT_ACCESS_TOKEN?.trim(),
|
||||
preconfiguredToken: (() => {
|
||||
if (process.env.JWT_ACCESS_TOKEN) {
|
||||
return { jwt: process.env.JWT_ACCESS_TOKEN?.trim() };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
})(),
|
||||
privateKeyPath: process.env.JWT_PRIVATE_KEY_PATH?.trim()
|
||||
},
|
||||
roomName: {
|
||||
/** Optional fixed room name that overrides the generated one entirely. */
|
||||
name: process.env.ROOM_NAME?.trim(),
|
||||
/** Optional prefix for room names used for tests. */
|
||||
prefix: process.env.ROOM_NAME_PREFIX?.trim(),
|
||||
/** Optional suffix for room names used for tests. */
|
||||
suffix: process.env.ROOM_NAME_SUFFIX?.trim()
|
||||
},
|
||||
webhooksProxy: {
|
||||
enabled: Boolean(process.env.WEBHOOKS_PROXY_URL && process.env.WEBHOOKS_PROXY_SHARED_SECRET),
|
||||
sharedSecret: process.env.WEBHOOKS_PROXY_SHARED_SECRET?.trim(),
|
||||
url: process.env.WEBHOOKS_PROXY_URL?.trim(),
|
||||
}
|
||||
};
|
||||
|
||||
if (config.debug) {
|
||||
console.log('TestsConfig:', JSON.stringify(config, null, 2));
|
||||
}
|
||||
241
tests/helpers/WebhookProxy.ts
Normal file
241
tests/helpers/WebhookProxy.ts
Normal file
@@ -0,0 +1,241 @@
|
||||
import fs from 'node:fs';
|
||||
import WebSocket from 'ws';
|
||||
|
||||
/**
|
||||
* Uses the webhook proxy service to proxy events to the testing clients.
|
||||
*/
|
||||
export default class WebhookProxy {
|
||||
private readonly url;
|
||||
private readonly secret;
|
||||
private logFile;
|
||||
private ws: WebSocket | undefined;
|
||||
private cache = new Map<string, any[]>();
|
||||
private listeners = new Map();
|
||||
private consumers = new Map<string, Array<{
|
||||
callback: (event: any) => void;
|
||||
predicate?: (event: any) => boolean;
|
||||
}>>();
|
||||
|
||||
private _defaultMeetingSettings: object | undefined;
|
||||
|
||||
/**
|
||||
* Initializes the webhook proxy.
|
||||
* @param url
|
||||
* @param secret
|
||||
* @param logFile
|
||||
*/
|
||||
constructor(url: string, secret: string, logFile: string) {
|
||||
this.url = url;
|
||||
this.secret = secret;
|
||||
this.logFile = logFile;
|
||||
}
|
||||
|
||||
/**
|
||||
* Connects.
|
||||
*/
|
||||
connect() {
|
||||
this.ws = new WebSocket(this.url, {
|
||||
headers: {
|
||||
Authorization: this.secret
|
||||
}
|
||||
});
|
||||
|
||||
this.ws.on('error', console.error);
|
||||
|
||||
this.ws.on('open', () => {
|
||||
console.log('WebhookProxy connected');
|
||||
this.logInfo('connected');
|
||||
});
|
||||
|
||||
this.ws.on('message', (data: any) => {
|
||||
const msg = JSON.parse(data.toString());
|
||||
|
||||
this.logInfo(`${msg.eventType} event: ${JSON.stringify(msg)}`);
|
||||
|
||||
if (msg.eventType) {
|
||||
let processed = false;
|
||||
|
||||
if (this.consumers.has(msg.eventType)) {
|
||||
const list = this.consumers.get(msg.eventType)!;
|
||||
const idx = list.findIndex(c => !c.predicate || c.predicate(msg));
|
||||
|
||||
if (idx !== -1) {
|
||||
const consumer = list[idx];
|
||||
|
||||
list.splice(idx, 1);
|
||||
if (list.length === 0) {
|
||||
this.consumers.delete(msg.eventType);
|
||||
}
|
||||
consumer.callback(msg);
|
||||
processed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!processed) {
|
||||
if (!this.cache.has(msg.eventType)) {
|
||||
this.cache.set(msg.eventType, []);
|
||||
}
|
||||
this.cache.get(msg.eventType)!.push(msg);
|
||||
}
|
||||
|
||||
if (this.listeners.has(msg.eventType)) {
|
||||
this.listeners.get(msg.eventType)(msg);
|
||||
processed = true;
|
||||
}
|
||||
|
||||
if (!processed && msg.eventType === 'SETTINGS_PROVISIONING') {
|
||||
// just in case to not be empty
|
||||
let response: any = { someField: 'someValue' };
|
||||
|
||||
if (this._defaultMeetingSettings) {
|
||||
response = this._defaultMeetingSettings;
|
||||
}
|
||||
|
||||
this.ws?.send(JSON.stringify(response));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds event consumer. Consumers receive the event single time and we remove them from the list of consumers.
|
||||
* @param eventType
|
||||
* @param callback
|
||||
* @param predicate Optional filter; only events for which predicate returns true are consumed.
|
||||
*/
|
||||
addConsumer(eventType: string, callback: (event: any) => void, predicate?: (event: any) => boolean) {
|
||||
if (this.cache.has(eventType)) {
|
||||
const list = this.cache.get(eventType)!;
|
||||
const idx = list.findIndex(e => !predicate || predicate(e));
|
||||
|
||||
if (idx !== -1) {
|
||||
const event = list[idx];
|
||||
|
||||
list.splice(idx, 1);
|
||||
if (list.length === 0) {
|
||||
this.cache.delete(eventType);
|
||||
}
|
||||
callback(event);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (!this.consumers.has(eventType)) {
|
||||
this.consumers.set(eventType, []);
|
||||
}
|
||||
this.consumers.get(eventType)!.push({ callback, predicate });
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear any stored event.
|
||||
*/
|
||||
clearCache() {
|
||||
this.logInfo('cache cleared');
|
||||
this.cache.clear();
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the event to be received.
|
||||
* @param eventType
|
||||
* @param predicateOrTimeout Optional predicate to filter events, or a timeout in ms.
|
||||
* @param timeout
|
||||
*/
|
||||
async waitForEvent(
|
||||
eventType: string,
|
||||
predicateOrTimeout?: ((event: any) => boolean) | number,
|
||||
timeout = 120000): Promise<any> {
|
||||
let predicate: ((event: any) => boolean) | undefined;
|
||||
let actualTimeout = timeout;
|
||||
|
||||
if (typeof predicateOrTimeout === 'function') {
|
||||
predicate = predicateOrTimeout;
|
||||
} else if (typeof predicateOrTimeout === 'number') {
|
||||
actualTimeout = predicateOrTimeout;
|
||||
}
|
||||
|
||||
// we create the error here so we have a meaningful stack trace
|
||||
const error = new Error(`Timeout waiting for event:${eventType}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const waiter = setTimeout(() => {
|
||||
this.logInfo(error.message);
|
||||
|
||||
return reject(error);
|
||||
}, actualTimeout);
|
||||
|
||||
this.addConsumer(eventType, event => {
|
||||
clearTimeout(waiter);
|
||||
|
||||
resolve(event);
|
||||
}, predicate);
|
||||
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for the event type.
|
||||
* @param eventType
|
||||
* @param callback
|
||||
*/
|
||||
addListener(eventType: string, callback: (data: any) => void) {
|
||||
this.listeners.set(eventType, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a listener for the event type.
|
||||
* @param eventType
|
||||
*/
|
||||
removeListener(eventType: string) {
|
||||
this.listeners.delete(eventType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disconnects the webhook proxy.
|
||||
*/
|
||||
disconnect() {
|
||||
if (this.ws) {
|
||||
this.ws.close();
|
||||
console.log('WebhookProxy disconnected');
|
||||
this.ws = undefined;
|
||||
this.logInfo('disconnected');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Logs a message in the logfile.
|
||||
*
|
||||
* @param {string} message - The message to add.
|
||||
* @returns {void}
|
||||
*/
|
||||
logInfo(message: string) {
|
||||
try {
|
||||
// @ts-ignore
|
||||
fs.appendFileSync(this.logFile, `${new Date().toISOString()} ${message}\n`);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the settings provider.
|
||||
* @param value
|
||||
*/
|
||||
set defaultMeetingSettings(value: {
|
||||
asyncTranscriptions?: boolean;
|
||||
autoAudioRecording?: boolean;
|
||||
autoTranscriptions?: boolean;
|
||||
autoVideoRecording?: boolean;
|
||||
lobbyEnabled?: boolean;
|
||||
lobbyType?: 'WAIT_FOR_APPROVAL' | 'WAIT_FOR_MODERATOR';
|
||||
maxOccupants?: number;
|
||||
outboundPhoneNo?: string;
|
||||
participantsSoftLimit?: number;
|
||||
passcode?: string;
|
||||
transcriberType?: 'GOOGLE' | 'ORACLE_CLOUD_AI_SPEECH' | 'EGHT_WHISPER';
|
||||
visitorsEnabled?: boolean;
|
||||
visitorsLive?: boolean;
|
||||
}) {
|
||||
this._defaultMeetingSettings = value;
|
||||
}
|
||||
}
|
||||
83
tests/helpers/browserLogger.ts
Normal file
83
tests/helpers/browserLogger.ts
Normal file
@@ -0,0 +1,83 @@
|
||||
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} fileName - The name of the file.
|
||||
* @param {string} folder - The folder to save the file.
|
||||
* @returns {void}
|
||||
*/
|
||||
export function initLogger(driver: WebdriverIO.Browser, fileName: string, folder: string) {
|
||||
// @ts-ignore
|
||||
driver.logFile = `${folder}/${fileName}.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');
|
||||
}
|
||||
|
||||
/**
|
||||
* Appends value to the log file.
|
||||
* @param {WebdriverIO.Browser} driver - The driver which log file is requested.
|
||||
* @param {string} value - The content to add to the file.
|
||||
*/
|
||||
export function saveLogs(driver: WebdriverIO.Browser, value: string) {
|
||||
// @ts-ignore
|
||||
if (!driver.logFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// @ts-ignore
|
||||
fs.appendFileSync(driver.logFile, value);
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 async function logInfo(driver: WebdriverIO.Browser, message: string) {
|
||||
// @ts-ignore
|
||||
if (!driver.logFile) {
|
||||
return;
|
||||
}
|
||||
|
||||
return driver.execute((prefix, msg) =>
|
||||
console.log(`${new Date().toISOString()} ${prefix} ${msg}\n`),
|
||||
LOG_PREFIX, message);
|
||||
}
|
||||
|
||||
88
tests/helpers/expectations.ts
Normal file
88
tests/helpers/expectations.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import fs from 'fs';
|
||||
import { merge } from 'lodash-es';
|
||||
|
||||
import { config } from './TestsConfig';
|
||||
|
||||
const defaultExpectations = {
|
||||
dialIn: {
|
||||
/*
|
||||
* The dial-in functionality is enabled.
|
||||
* true -> assert the config is enabled, the UI elements are displayed, and the feature works.
|
||||
* false -> assert the config is disabled and the UI elements are not displayed.
|
||||
* null -> if the config is enabled, assert the UI elements are displayed and the feature works.
|
||||
*/
|
||||
enabled: null,
|
||||
minPinLength: 8,
|
||||
/* Whether to verify that the "more numbers" page is displayed. Note that only the positive verification is
|
||||
performed: when set to 'false' we do not assert that the "more numbers" page is not displayed. */
|
||||
moreNumbersPage: true
|
||||
},
|
||||
iframe: {
|
||||
// Whether the iframe integration is enabled (the inverse of `disableIframeAPI` from config.js)
|
||||
enabled: true
|
||||
},
|
||||
jaas: {
|
||||
fileSharingEnabled: true,
|
||||
liveStreamingEnabled: true,
|
||||
recordingEnabled: true,
|
||||
transcriptionEnabled: true,
|
||||
transcription: {
|
||||
// Expect "async" transcription to be enabled.
|
||||
asyncTranscription: true,
|
||||
// Expect the JaaS transcription webhooks to fire in "async" mode.
|
||||
asyncTranscriptionWebhook: true,
|
||||
// Expect (deprecated) jigasi-based transcription to be enabled.
|
||||
jigasiTranscription: true
|
||||
},
|
||||
/**
|
||||
* Whether the jaas account is configured with the account-level setting to allow unauthenticated users to join.
|
||||
*/
|
||||
unauthenticatedJoins: false,
|
||||
visitors: true
|
||||
},
|
||||
moderation: {
|
||||
// Everyone is a moderator.
|
||||
allModerators: false,
|
||||
// When a moderator leaves, another one is elected.
|
||||
autoModerator: true,
|
||||
// The first to join is a moderator.
|
||||
firstModerator: true,
|
||||
// The grantOwner function is available.
|
||||
grantModerator: true,
|
||||
// Whether the ability to set a password is available (there's a backend options which makes moderators unable
|
||||
// to set a room password unless they also happen to have a token (any valid token?))
|
||||
setPasswordAvailable: true
|
||||
},
|
||||
connectivity: {
|
||||
jvb: {
|
||||
direct: true,
|
||||
turn: true
|
||||
},
|
||||
p2p: {
|
||||
direct: false,
|
||||
turn: true
|
||||
}
|
||||
},
|
||||
// We can create conferences under any tenant.
|
||||
useTenant: true
|
||||
};
|
||||
|
||||
let overrides: any = {};
|
||||
|
||||
if (config.expectationsFile) {
|
||||
try {
|
||||
const str = fs.readFileSync(config.expectationsFile, 'utf8');
|
||||
|
||||
// Remove comments and multiline comments.
|
||||
overrides = JSON.parse(str.replace(/\/\/.*|\/\*[\s\S]*?\*\//g, ''));
|
||||
} catch (e) {
|
||||
console.error('Error reading expectations file', e);
|
||||
}
|
||||
console.log('Loaded expectations from', config.expectationsFile);
|
||||
}
|
||||
|
||||
export const expectations = merge(defaultExpectations, overrides);
|
||||
|
||||
if (!process.env.WDIO_WORKER_ID) {
|
||||
console.log('Expectations:', expectations);
|
||||
}
|
||||
41
tests/helpers/jaas.ts
Normal file
41
tests/helpers/jaas.ts
Normal file
@@ -0,0 +1,41 @@
|
||||
import { Participant } from './Participant';
|
||||
import { config } from './TestsConfig';
|
||||
import { joinMuc } from './joinMuc';
|
||||
import { IToken, ITokenOptions, generateToken } from './token';
|
||||
import { IParticipantJoinOptions, IParticipantOptions } from './types';
|
||||
|
||||
export function generateJaasToken(options: ITokenOptions): IToken {
|
||||
if (!config.jaas.enabled) {
|
||||
throw new Error('JaaS is not configured.');
|
||||
}
|
||||
|
||||
// Don't override the keyId and keyPath if they are already set in options, allow tests to set them.
|
||||
return generateToken({
|
||||
...options,
|
||||
keyId: options.keyId || config.jaas.kid,
|
||||
keyPath: options.keyPath || config.jaas.privateKeyPath
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new Participant and joins the MUC with the given options. The jaas-specific properties must be set as
|
||||
* environment variables (see env.example and TestsConfig.ts). If no room name is specified, the default room name
|
||||
* from the context is used.
|
||||
*
|
||||
* @param participantOptions
|
||||
* @param joinOptions options to use when joining the MUC.
|
||||
* @returns {Promise<Participant>} The Participant that has joined the MUC.
|
||||
*/
|
||||
export async function joinJaasMuc(
|
||||
participantOptions?: Partial<IParticipantOptions>,
|
||||
joinOptions?: Partial<IParticipantJoinOptions>): Promise<Participant> {
|
||||
|
||||
if (!config.jaas.enabled) {
|
||||
throw new Error('JaaS is not configured.');
|
||||
}
|
||||
|
||||
return await joinMuc(participantOptions, {
|
||||
...joinOptions,
|
||||
tenant: joinOptions?.tenant || config.jaas.tenant
|
||||
});
|
||||
}
|
||||
50
tests/helpers/joinMuc.ts
Normal file
50
tests/helpers/joinMuc.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import { Participant } from './Participant';
|
||||
import { IParticipantJoinOptions, IParticipantOptions } from './types';
|
||||
|
||||
/**
|
||||
* Creates a new Participant and joins the MUC with the given options. If no room name is specified, the default room
|
||||
* name from the context is used.
|
||||
*
|
||||
* @param participantOptions
|
||||
* @param joinOptions options to use when joining the MUC.
|
||||
* @returns {Promise<Participant>} The Participant that has joined the MUC.
|
||||
*/
|
||||
export async function joinMuc(
|
||||
participantOptions?: Partial<IParticipantOptions>,
|
||||
joinOptions?: Partial<IParticipantJoinOptions>): Promise<Participant> {
|
||||
|
||||
const name = participantOptions?.name || 'p1';
|
||||
|
||||
// @ts-ignore
|
||||
const p = ctx[name] as Participant;
|
||||
|
||||
if (p) {
|
||||
// Load a blank page to make sure the page is reloaded (in case the new participant uses the same URL). Using
|
||||
// 'about:blank' was causing problems in the past, if we notice any issues we can change to "base.html".
|
||||
await p.driver.url('about:blank');
|
||||
}
|
||||
|
||||
const newParticipant = new Participant({
|
||||
iFrameApi: participantOptions?.iFrameApi || false,
|
||||
name,
|
||||
token: participantOptions?.token
|
||||
});
|
||||
|
||||
// @ts-ignore
|
||||
ctx[name] = newParticipant;
|
||||
|
||||
return await newParticipant.joinConference({
|
||||
...joinOptions,
|
||||
roomName: joinOptions?.roomName || ctx.roomName,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until all participants have ICE connected and have sent and received data (their PC stats are ready).
|
||||
* @param participants
|
||||
*/
|
||||
export async function waitForMedia(participants: Participant[]) {
|
||||
await Promise.all(participants.map(p =>
|
||||
p.waitForIceConnected().then(() => p.waitForSendReceiveData())
|
||||
));
|
||||
}
|
||||
48
tests/helpers/matchers.ts
Normal file
48
tests/helpers/matchers.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
// Register custom matchers on the global expect set up by @wdio/runner during _initSession.
|
||||
// Must be called from a wdio config hook that runs after _initSession (e.g. `before`); calling
|
||||
// at module load extends a stale `globalThis.expect` that gets replaced before tests run.
|
||||
export function registerCustomMatchers(): void {
|
||||
expect.extend({
|
||||
toPartiallyMatch(received: string, other: string, receivedLabel = 'received', otherLabel = 'other') {
|
||||
const pass = received.includes(other) || other.includes(received);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () => pass
|
||||
? `Expected ${receivedLabel} NOT to partially match ${otherLabel}.\n ${receivedLabel}: "${received}"\n ${otherLabel}: "${other}"`
|
||||
: `Expected ${receivedLabel} to partially match ${otherLabel} (one should contain the other).\n ${receivedLabel}: "${received}"\n ${otherLabel}: "${other}"`
|
||||
};
|
||||
},
|
||||
|
||||
toStartWith(received: string, prefix: string, label = 'value') {
|
||||
const pass = received.startsWith(prefix);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () => pass
|
||||
? `Expected ${label} NOT to start with "${prefix}".\n ${label}: "${received}"`
|
||||
: `Expected ${label} to start with "${prefix}".\n ${label}: "${received}"`
|
||||
};
|
||||
},
|
||||
|
||||
toBeInteger(received: unknown, label = 'value') {
|
||||
const pass = Number.isInteger(received);
|
||||
|
||||
return {
|
||||
pass,
|
||||
message: () => pass
|
||||
? `Expected ${label} NOT to be an integer.\n ${label}: ${JSON.stringify(received)}`
|
||||
: `Expected ${label} to be an integer.\n ${label}: ${JSON.stringify(received)}`
|
||||
};
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
declare module 'expect' {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
interface Matchers<R> {
|
||||
toBeInteger: (label?: string) => R;
|
||||
toPartiallyMatch: (other: string, receivedLabel?: string, otherLabel?: string) => R;
|
||||
toStartWith: (prefix: string, label?: string) => R;
|
||||
}
|
||||
}
|
||||
330
tests/helpers/participants.ts
Normal file
330
tests/helpers/participants.ts
Normal file
@@ -0,0 +1,330 @@
|
||||
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<void>}
|
||||
*/
|
||||
export async function ensureOneParticipant(
|
||||
options?: IJoinOptions, participantOptions?: IParticipantOptions): Promise<void> {
|
||||
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<void>}
|
||||
*/
|
||||
export async function ensureThreeParticipants(options?: IJoinOptions): Promise<void> {
|
||||
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<void>}
|
||||
*/
|
||||
export function joinFirstParticipant(options: IJoinOptions = { }): Promise<void> {
|
||||
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<Participant>}
|
||||
*/
|
||||
export function joinSecondParticipant(options?: IJoinOptions): Promise<Participant> {
|
||||
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<Participant>}
|
||||
*/
|
||||
export function joinThirdParticipant(options?: IJoinOptions): Promise<Participant> {
|
||||
return joinParticipant({ name: P3 }, options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure that there are four participants.
|
||||
*
|
||||
* @param {IJoinOptions} options - The options to use when joining the participant.
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
export async function ensureFourParticipants(options?: IJoinOptions): Promise<void> {
|
||||
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<void>}
|
||||
*/
|
||||
export async function ensureSixParticipants(options?: IJoinOptions): Promise<void> {
|
||||
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<void> {
|
||||
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<Participant>} - The participant instance.
|
||||
*/
|
||||
async function joinParticipant( // eslint-disable-line max-params
|
||||
participantOptions: IParticipantOptions,
|
||||
options?: IJoinOptions
|
||||
): Promise<Participant> {
|
||||
|
||||
// @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<void>}
|
||||
*/
|
||||
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()));
|
||||
}
|
||||
140
tests/helpers/token.ts
Normal file
140
tests/helpers/token.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import fs from 'fs';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { config } from './TestsConfig';
|
||||
|
||||
export type ITokenOptions = {
|
||||
displayName?: string;
|
||||
/**
|
||||
* The duration for which the token is valid, e.g. "1h" for one hour.
|
||||
*/
|
||||
exp?: string;
|
||||
/**
|
||||
* Additional JWT features to include (merged with the defaults).
|
||||
* Keys are feature names (e.g. 'file-upload'), values are boolean or string.
|
||||
*/
|
||||
features?: Record<string, boolean | string>;
|
||||
/**
|
||||
* The key ID to use for the token.
|
||||
* If not provided, the kid configured with environment variables will be used (see env.example).
|
||||
*/
|
||||
keyId?: string;
|
||||
/**
|
||||
* The path to the private key file used to sign the token.
|
||||
* If not provided, the path configured with environment variables will be used (see env.example).
|
||||
*/
|
||||
keyPath?: string;
|
||||
/**
|
||||
* Whether to set the 'moderator' flag.
|
||||
*/
|
||||
moderator?: boolean;
|
||||
/**
|
||||
* The room for which the token is valid, or '*'. Defaults to '*'.
|
||||
*/
|
||||
room?: string;
|
||||
sub?: string;
|
||||
/**
|
||||
* Whether to set the 'visitor' flag.
|
||||
*/
|
||||
visitor?: boolean;
|
||||
};
|
||||
|
||||
export type IToken = {
|
||||
/**
|
||||
* The JWT headers, for easy reference.
|
||||
*/
|
||||
headers?: any;
|
||||
/**
|
||||
* The signed JWT.
|
||||
*/
|
||||
jwt: string;
|
||||
/**
|
||||
* The options used to generate the token.
|
||||
*/
|
||||
options?: ITokenOptions;
|
||||
/**
|
||||
* The token's payload, for easy reference.
|
||||
*/
|
||||
payload?: any;
|
||||
};
|
||||
|
||||
export function generatePayload(options: ITokenOptions): any {
|
||||
const payload = {
|
||||
'aud': 'jitsi',
|
||||
'iss': 'chat',
|
||||
'sub': options?.sub || '',
|
||||
'context': {
|
||||
'user': {
|
||||
'name': options.displayName,
|
||||
'id': uuidv4(),
|
||||
'avatar': 'https://avatars0.githubusercontent.com/u/3671647',
|
||||
'email': 'john.doe@jitsi.org'
|
||||
},
|
||||
'group': uuidv4(),
|
||||
'features': {
|
||||
'outbound-call': 'true',
|
||||
'transcription': 'true',
|
||||
'recording': 'true',
|
||||
'sip-outbound-call': true,
|
||||
'livestreaming': true,
|
||||
...options.features
|
||||
},
|
||||
},
|
||||
'room': options.room || '*'
|
||||
};
|
||||
|
||||
if (options.moderator) {
|
||||
// @ts-ignore
|
||||
payload.context.user.moderator = true;
|
||||
} else if (options.visitor) {
|
||||
// @ts-ignore
|
||||
payload.context.user.role = 'visitor';
|
||||
}
|
||||
|
||||
return payload;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a signed token.
|
||||
*/
|
||||
export function generateToken(options: ITokenOptions): IToken {
|
||||
const keyId = options.keyId || config.jwt.kid;
|
||||
const keyPath = options.keyPath || config.jwt.privateKeyPath;
|
||||
const headers = {
|
||||
algorithm: 'RS256',
|
||||
noTimestamp: true,
|
||||
expiresIn: options.exp || '24h',
|
||||
keyid: keyId,
|
||||
};
|
||||
|
||||
if (!keyId) {
|
||||
throw new Error('No keyId provided (JWT_KID is not set?)');
|
||||
}
|
||||
|
||||
if (!keyPath) {
|
||||
throw new Error('No keyPath provided (JWT_PRIVATE_KEY_PATH is not set?)');
|
||||
}
|
||||
|
||||
const key = fs.readFileSync(keyPath);
|
||||
const payload = generatePayload({
|
||||
...options,
|
||||
displayName: options?.displayName || '',
|
||||
sub: keyId.substring(0, keyId.indexOf('/'))
|
||||
});
|
||||
|
||||
return {
|
||||
headers,
|
||||
// @ts-ignore
|
||||
jwt: jwt.sign(payload, key, headers),
|
||||
options,
|
||||
payload
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Generated a signed token and return just the JWT string.
|
||||
*/
|
||||
export function generateJwt(options: ITokenOptions): string {
|
||||
return generateToken(options).jwt;
|
||||
}
|
||||
142
tests/helpers/types.ts
Normal file
142
tests/helpers/types.ts
Normal file
@@ -0,0 +1,142 @@
|
||||
import { IConfig } from '../../react/features/base/config/configType';
|
||||
|
||||
import type { Participant } from './Participant';
|
||||
import { ITestProperties } from './TestProperties';
|
||||
import type WebhookProxy from './WebhookProxy';
|
||||
import { IToken, ITokenOptions } from './token';
|
||||
|
||||
export type IContext = {
|
||||
/**
|
||||
* The name (describe block title) of the suite in which a test failed. Used to skip the remaining tests in that
|
||||
* suite while still running tests in other describe blocks in the same file.
|
||||
*/
|
||||
failedSuite?: string;
|
||||
|
||||
/**
|
||||
* The up-to-six browser instances provided by the framework. These can be initialized using
|
||||
* ensureOneParticipant, ensureTwoParticipants, etc. from participants.ts.
|
||||
**/
|
||||
p1: Participant;
|
||||
p2: Participant;
|
||||
p3: Participant;
|
||||
p4: Participant;
|
||||
p5: Participant;
|
||||
p6: Participant;
|
||||
|
||||
/** A room name automatically generated by the framework for convenience. */
|
||||
roomName: string;
|
||||
/**
|
||||
* A flag that tests can set, which signals to the framework that the (rest of the) test suite should be skipped.
|
||||
* A string value indicates the reason for the skipped (to be included in the Allure report).
|
||||
*/
|
||||
skipSuiteTests: boolean | string;
|
||||
/**
|
||||
* Test properties provided by the test file via TestProperties.setTestProperties. Used by the framework to
|
||||
* set up the context appropriately.
|
||||
**/
|
||||
testProperties: ITestProperties;
|
||||
times: any;
|
||||
/**
|
||||
* A WebhooksProxy instance generated by the framework and available for tests to use, if configured.
|
||||
* Note that this is only configured for roomName, if a test wishes to use a different room name it can set up
|
||||
* a WebhooksProxy instance itself.
|
||||
*/
|
||||
webhooksProxy: WebhookProxy;
|
||||
};
|
||||
|
||||
export type IParticipantOptions = {
|
||||
/** Whether it should use the iFrame API. */
|
||||
iFrameApi?: boolean;
|
||||
/** Determines the browser instance to use. */
|
||||
name: 'p1' | 'p2' | 'p3' | 'p4' | 'p5' | 'p6';
|
||||
/** An optional token to use. */
|
||||
token?: IToken;
|
||||
};
|
||||
|
||||
/**
|
||||
* Options for joinConference.
|
||||
*/
|
||||
export type IParticipantJoinOptions = {
|
||||
|
||||
/**
|
||||
* Config overwrites to use.
|
||||
*/
|
||||
configOverwrite?: IConfig;
|
||||
|
||||
/** The name of the room to join */
|
||||
roomName: string;
|
||||
|
||||
/**
|
||||
* Whether to skip setting display name.
|
||||
*/
|
||||
skipDisplayName?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip the prejoin button 'Join' click when joining.
|
||||
*/
|
||||
skipPrejoinButtonClick?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip waiting for the participant to join the room. Cases like lobby where we do not succeed to join
|
||||
* based on the logic of the test.
|
||||
*/
|
||||
skipWaitToJoin?: boolean;
|
||||
|
||||
/**
|
||||
* An optional tenant to use. If provided it overrides the default.
|
||||
*/
|
||||
tenant?: string;
|
||||
|
||||
/**
|
||||
* An optional string to append to the URL when joining the room.
|
||||
*/
|
||||
urlAppendString?: string;
|
||||
};
|
||||
|
||||
export type IJoinOptions = {
|
||||
|
||||
/**
|
||||
* Config overwrites to pass to IParticipantJoinOptions.
|
||||
*/
|
||||
configOverwrite?: IConfig;
|
||||
|
||||
/**
|
||||
* To be able to override the ctx generated room name. If missing the one from the context will be used.
|
||||
*/
|
||||
roomName?: string;
|
||||
|
||||
/**
|
||||
*The skip display name setting to pass to IParticipantJoinOptions.
|
||||
*/
|
||||
skipDisplayName?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip in meeting checks like ice connected and send receive data. For single in meeting participant.
|
||||
*/
|
||||
skipInMeetingChecks?: boolean;
|
||||
|
||||
/**
|
||||
* Whether to skip the prejoin button 'Join' click when joining.
|
||||
*/
|
||||
skipPrejoinButtonClick?: boolean;
|
||||
|
||||
/**
|
||||
* The skip waiting for the participant to join the room setting to pass to IParticipantJoinOptions.
|
||||
*/
|
||||
skipWaitToJoin?: boolean;
|
||||
|
||||
/**
|
||||
* An optional tenant to use. If provided the default tenant is changed to it.
|
||||
*/
|
||||
tenant?: string;
|
||||
|
||||
/**
|
||||
* Options used when generating a token.
|
||||
*/
|
||||
tokenOptions?: ITokenOptions;
|
||||
|
||||
/**
|
||||
* An optional string to append to the URL when joining the room.
|
||||
*/
|
||||
urlAppendString?: string;
|
||||
};
|
||||
57
tests/helpers/utils.ts
Normal file
57
tests/helpers/utils.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import { config as testsConfig } from './TestsConfig';
|
||||
|
||||
const https = require('https');
|
||||
|
||||
export function generateRoomName(testName: string) {
|
||||
if (testsConfig.roomName.name) {
|
||||
return testsConfig.roomName.name;
|
||||
}
|
||||
|
||||
const rand = (Math.floor(Math.random() * 400) + 1).toString();
|
||||
let roomName = `${testName}-${rand}`;
|
||||
|
||||
if (testsConfig.roomName.prefix) {
|
||||
roomName = `${testsConfig.roomName.prefix}_${roomName}`;
|
||||
}
|
||||
if (testsConfig.roomName.suffix) {
|
||||
roomName += `_${testsConfig.roomName.suffix}`;
|
||||
}
|
||||
|
||||
return roomName.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetches JSON data from a given URL.
|
||||
* @param {string} url - The URL to fetch data from.
|
||||
* @returns {Promise<Object>} - A promise that resolves to the parsed JSON object.
|
||||
*/
|
||||
export async function fetchJson(url) {
|
||||
return new Promise((resolve, reject) => {
|
||||
https.get(url, res => {
|
||||
let data = '';
|
||||
|
||||
// Handle HTTP errors
|
||||
if (res.statusCode < 200 || res.statusCode >= 300) {
|
||||
return reject(new Error(`HTTP Status Code: ${res.statusCode}`));
|
||||
}
|
||||
|
||||
// Collect data chunks
|
||||
res.on('data', chunk => {
|
||||
data += chunk;
|
||||
});
|
||||
|
||||
// Parse JSON when the response ends
|
||||
res.on('end', () => {
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
|
||||
resolve(json);
|
||||
} catch (err) {
|
||||
reject(new Error('Invalid JSON response'));
|
||||
}
|
||||
});
|
||||
}).on('error', err => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user