snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
This commit is contained in:
71
tests/specs/jaas/chat.spec.ts
Normal file
71
tests/specs/jaas/chat.spec.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
import { fetchJson } from '../../helpers/utils';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('JaaS CHAT_UPLOADED webhook.', () => {
|
||||
const tenant = testsConfig.jaas.tenant;
|
||||
const customerId = tenant?.replace('vpaas-magic-cookie-', '');
|
||||
let p1: Participant, p2: Participant;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
let fqn: string;
|
||||
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
p1 = await joinJaasMuc({ name: 'p1', token: t({ room }) });
|
||||
p2 = await joinJaasMuc({ name: 'p2', token: t({ room }) });
|
||||
fqn = `${testsConfig.jaas.tenant}/${room}`;
|
||||
});
|
||||
|
||||
it('test webhook', async () => {
|
||||
await p1.getChatPanel().sendMessage('foo');
|
||||
await p2.getChatPanel().sendMessage('bar');
|
||||
await p1.getChatPanel().sendMessage('baz');
|
||||
|
||||
await p1.hangup();
|
||||
await p2.hangup();
|
||||
|
||||
const event: {
|
||||
appId: string;
|
||||
customerId: string;
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
fqn: string;
|
||||
} = await webhooksProxy.waitForEvent('CHAT_UPLOADED');
|
||||
|
||||
expect(event.appId).toBe(tenant);
|
||||
expect(event.customerId).toBe(customerId);
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
expect(event.eventType).toBe('CHAT_UPLOADED');
|
||||
expect(event.fqn).toBe(fqn);
|
||||
|
||||
const uploadedChat: any = await fetchJson(event.data.preAuthenticatedLink);
|
||||
|
||||
expect(uploadedChat.meetingFqn).toBe(fqn);
|
||||
expect(uploadedChat.messageType).toBe('CHAT');
|
||||
|
||||
const messages: any[] = uploadedChat.messages;
|
||||
|
||||
expect(messages).toBeDefined();
|
||||
expect(messages.length).toBe(3);
|
||||
console.log(JSON.stringify(messages));
|
||||
expect(messages.some(m => m.name === 'p1' && m.content === 'foo')).toBe(true);
|
||||
expect(messages.some(m => m.name === 'p2' && m.content === 'bar')).toBe(true);
|
||||
expect(messages.some(m => m.name === 'p1' && m.content === 'baz')).toBe(true);
|
||||
messages.forEach(m => {
|
||||
expect(typeof m.timestamp).toBe('number');
|
||||
});
|
||||
});
|
||||
});
|
||||
82
tests/specs/jaas/dial/dialin.spec.ts
Normal file
82
tests/specs/jaas/dial/dialin.spec.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import type { Participant } from '../../../helpers/Participant';
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../../helpers/TestsConfig';
|
||||
import WebhookProxy from '../../../helpers/WebhookProxy';
|
||||
import { expectations } from '../../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
import {
|
||||
assertDialInDisplayed, assertUrlDisplayed,
|
||||
dialIn,
|
||||
isDialInEnabled, verifyMoreNumbersPage,
|
||||
} from '../../helpers/DialIn';
|
||||
|
||||
import { verifyEndedWebhook, verifyStartedWebhooks, waitForMedia } from './util';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true
|
||||
});
|
||||
|
||||
describe('Dial-in', () => {
|
||||
let p1: Participant, webhooksProxy: WebhookProxy;
|
||||
const customerId: string = testsConfig.jaas.customerId || '';
|
||||
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
if (!process.env.DIAL_IN_REST_URL) {
|
||||
ctx.skipSuiteTests = 'DIAL_IN_REST_URL is not set.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
p1 = await joinJaasMuc({ name: 'p1', token: t({ room, moderator: true }) });
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
if (!webhooksProxy) {
|
||||
console.error('WebhooksProxy is not available, will not verify webhooks.');
|
||||
}
|
||||
|
||||
expect(await p1.isInMuc()).toBe(true);
|
||||
if (expectations.dialIn.enabled !== null) {
|
||||
expect(await isDialInEnabled(p1)).toBe(expectations.dialIn.enabled);
|
||||
}
|
||||
expect(customerId).toBeDefined();
|
||||
});
|
||||
|
||||
it ('Invite UI', async () => {
|
||||
await assertUrlDisplayed(p1);
|
||||
if (expectations.dialIn.enabled !== null) {
|
||||
await assertDialInDisplayed(p1, expectations.dialIn.enabled);
|
||||
}
|
||||
if (expectations.dialIn.moreNumbersPage) {
|
||||
// TODO: assert the page is NOT shown when the expectation is false.
|
||||
await verifyMoreNumbersPage(p1);
|
||||
}
|
||||
});
|
||||
|
||||
it('dial-in', async () => {
|
||||
const dialInPin = await p1.getDialInPin();
|
||||
|
||||
expect(dialInPin.length).toBeGreaterThanOrEqual(8);
|
||||
|
||||
await dialIn(dialInPin);
|
||||
await waitForMedia(p1);
|
||||
|
||||
let startedPayload: any;
|
||||
|
||||
if (webhooksProxy) {
|
||||
startedPayload
|
||||
= await verifyStartedWebhooks(webhooksProxy, 'in', 'DIAL_IN_STARTED', customerId);
|
||||
}
|
||||
|
||||
const endpointId = await p1.execute(() => APP?.conference?.listMembers()[0].getId());
|
||||
|
||||
await p1.getFilmstrip().kickParticipant(endpointId);
|
||||
|
||||
if (webhooksProxy) {
|
||||
await verifyEndedWebhook(webhooksProxy, 'DIAL_IN_ENDED', customerId, startedPayload);
|
||||
}
|
||||
|
||||
await p1.hangup();
|
||||
});
|
||||
});
|
||||
50
tests/specs/jaas/dial/dialout.spec.ts
Normal file
50
tests/specs/jaas/dial/dialout.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import type { Participant } from '../../../helpers/Participant';
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../../helpers/TestsConfig';
|
||||
import WebhookProxy from '../../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
import { verifyEndedWebhook, verifyStartedWebhooks, waitForMedia } from './util';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
describe('Dial-out', () => {
|
||||
let p1: Participant, webhooksProxy: WebhookProxy;
|
||||
const dialOutUrl = process.env.DIAL_OUT_URL || '';
|
||||
const customerId = testsConfig.jaas.customerId || '';
|
||||
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
if (!dialOutUrl) {
|
||||
ctx.skipSuiteTests = 'DIAL_OUT_URL is not set.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
p1 = await joinJaasMuc({ name: 'p1', iFrameApi: true, token: t({ room, moderator: true }) });
|
||||
|
||||
expect(await p1.isInMuc()).toBe(true);
|
||||
expect(Boolean(await p1.execute(() => config.dialOutAuthUrl))).toBe(true);
|
||||
});
|
||||
|
||||
it('dial-out', async () => {
|
||||
await p1.switchToMainFrame();
|
||||
await p1.getIframeAPI().invitePhone(dialOutUrl);
|
||||
await p1.switchToIFrame();
|
||||
await p1.waitForParticipants(1);
|
||||
await waitForMedia(p1);
|
||||
|
||||
const startedPayload
|
||||
= await verifyStartedWebhooks(webhooksProxy, 'out', 'DIAL_OUT_STARTED', customerId);
|
||||
const endpointId = await p1.execute(() => APP?.conference?.listMembers()[0].getId());
|
||||
|
||||
await p1.getFilmstrip().kickParticipant(endpointId);
|
||||
|
||||
await verifyEndedWebhook(webhooksProxy, 'DIAL_OUT_ENDED', customerId, startedPayload);
|
||||
});
|
||||
});
|
||||
79
tests/specs/jaas/dial/sipjibri.spec.ts
Normal file
79
tests/specs/jaas/dial/sipjibri.spec.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
import type { Participant } from '../../../helpers/Participant';
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../../helpers/TestsConfig';
|
||||
import WebhookProxy from '../../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
import { waitForMedia } from './util';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
});
|
||||
|
||||
|
||||
describe('SIP jibri invite', () => {
|
||||
let p1: Participant, webhooksProxy: WebhookProxy;
|
||||
const customerId = testsConfig.jaas.customerId || '';
|
||||
const dialOutUrl = process.env.SIP_JIBRI_DIAL_OUT_URL || '';
|
||||
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
if (!dialOutUrl) {
|
||||
ctx.skipSuiteTests = 'SIP_JIBRI_DIAL_OUT_URL is not set.';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
p1 = await joinJaasMuc({ name: 'p1', iFrameApi: true, token: t({ room, moderator: true }) });
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
|
||||
expect(await p1.isInMuc()).toBe(true);
|
||||
expect(Boolean(await p1.execute(() => config.inviteServiceUrl))).toBe(true);
|
||||
});
|
||||
|
||||
it('sip jibri', async () => {
|
||||
await p1.switchToMainFrame();
|
||||
await p1.getIframeAPI().inviteSIP(dialOutUrl);
|
||||
await p1.switchToIFrame();
|
||||
await p1.waitForParticipants(1);
|
||||
await waitForMedia(p1);
|
||||
|
||||
const startedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
participantFullJid: string;
|
||||
participantId: string;
|
||||
participantJid: string;
|
||||
sipAddress: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('SIP_CALL_OUT_STARTED');
|
||||
|
||||
expect('SIP_CALL_OUT_STARTED').toBe(startedEvent.eventType);
|
||||
expect(startedEvent.data.sipAddress).toBe(`sip:${process.env.SIP_JIBRI_DIAL_OUT_URL}`);
|
||||
expect(startedEvent.customerId).toBe(customerId);
|
||||
|
||||
const endpointId = await p1.execute(() => APP?.conference?.listMembers()[0].getId());
|
||||
|
||||
await p1.getFilmstrip().kickParticipant(endpointId);
|
||||
|
||||
const endedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
direction: string;
|
||||
participantFullJid: string;
|
||||
participantId: string;
|
||||
participantJid: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('SIP_CALL_OUT_ENDED');
|
||||
|
||||
expect('SIP_CALL_OUT_ENDED').toBe(endedEvent.eventType);
|
||||
expect(endedEvent.customerId).toBe(customerId);
|
||||
expect(endedEvent.data.participantFullJid).toBe(startedEvent.data.participantFullJid);
|
||||
expect(endedEvent.data.participantId).toBe(startedEvent.data.participantId);
|
||||
expect(endedEvent.data.participantJid).toBe(startedEvent.data.participantJid);
|
||||
});
|
||||
});
|
||||
81
tests/specs/jaas/dial/util.ts
Normal file
81
tests/specs/jaas/dial/util.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import { Participant } from '../../../helpers/Participant';
|
||||
import WebhookProxy from '../../../helpers/WebhookProxy';
|
||||
|
||||
interface IStartedWebhookPayload {
|
||||
direction: string;
|
||||
participantFullJid: string;
|
||||
participantId: string;
|
||||
participantJid: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the dial events for a participant and clean up at the end.
|
||||
* @param webhooksProxy
|
||||
* @param startedEventName
|
||||
* @param direction
|
||||
* @param customerId
|
||||
*/
|
||||
export async function verifyStartedWebhooks(
|
||||
webhooksProxy: WebhookProxy,
|
||||
direction: 'in' | 'out',
|
||||
startedEventName: string,
|
||||
customerId: string): Promise<IStartedWebhookPayload> {
|
||||
|
||||
const startedEvent: {
|
||||
customerId: string;
|
||||
data: IStartedWebhookPayload;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent(startedEventName);
|
||||
|
||||
expect(startedEventName).toBe(startedEvent.eventType);
|
||||
expect(startedEvent.data.direction).toBe(direction);
|
||||
expect(startedEvent.customerId).toBe(customerId);
|
||||
|
||||
const usageEvent: {
|
||||
customerId: string;
|
||||
data: any;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('USAGE');
|
||||
|
||||
expect('USAGE').toBe(usageEvent.eventType);
|
||||
expect(usageEvent.customerId).toBe(customerId);
|
||||
|
||||
expect(usageEvent.data.some((el: any) =>
|
||||
el.participantId === startedEvent.data.participantId && el.callDirection === direction)).toBe(true);
|
||||
|
||||
return startedEvent.data;
|
||||
}
|
||||
|
||||
export async function verifyEndedWebhook(
|
||||
webhooksProxy: WebhookProxy,
|
||||
endedEventName: string,
|
||||
customerId: string,
|
||||
startedPayload: IStartedWebhookPayload) {
|
||||
const endedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
direction: string;
|
||||
participantFullJid: string;
|
||||
participantId: string;
|
||||
participantJid: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent(endedEventName);
|
||||
|
||||
expect(endedEventName).toBe(endedEvent.eventType);
|
||||
expect(endedEvent.customerId).toBe(customerId);
|
||||
expect(endedEvent.data.participantFullJid).toBe(startedPayload.participantFullJid);
|
||||
expect(endedEvent.data.participantId).toBe(startedPayload.participantId);
|
||||
expect(endedEvent.data.participantJid).toBe(startedPayload.participantJid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until there is at least one remote participant, ICE is connected, the participant has a stream, and data is
|
||||
* both received and sent.
|
||||
*/
|
||||
export async function waitForMedia(p: Participant) {
|
||||
await p.waitForParticipants(1);
|
||||
await p.waitForIceConnected();
|
||||
await p.waitForRemoteStreams(1);
|
||||
await p.waitForSendReceiveData(20_000);
|
||||
}
|
||||
172
tests/specs/jaas/fileSharing.spec.ts
Normal file
172
tests/specs/jaas/fileSharing.spec.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
import path from 'node:path';
|
||||
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
// __dirname-based (not CWD-relative) - this is a local path read on the machine running the
|
||||
// tests (uploaded to the remote grid session via wdio's own uploadFile), so it needs to resolve
|
||||
// correctly regardless of whether the process's CWD is the repo root or tests/ itself.
|
||||
const TEST_FILE_PATH = path.join(__dirname, '..', '..', 'resources', 'test-upload.txt');
|
||||
const TEST_FILE_NAME = 'test-upload.txt';
|
||||
|
||||
describe('File sharing', () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
let fileSharingEnabled: boolean;
|
||||
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
p1 = await joinJaasMuc({
|
||||
name: 'p1',
|
||||
token: t({ room, features: { 'file-upload': 'true' } })
|
||||
});
|
||||
|
||||
fileSharingEnabled = await p1.execute(
|
||||
() => Boolean((window as any).config?.fileSharing?.enabled && (window as any).config?.fileSharing?.apiUrl)
|
||||
);
|
||||
|
||||
expect(fileSharingEnabled).toBe(expectations.jaas.fileSharingEnabled);
|
||||
});
|
||||
|
||||
it('upload button enabled with file-upload feature', async () => {
|
||||
if (!fileSharingEnabled) {
|
||||
return;
|
||||
}
|
||||
const panel = p1.getFileSharingPanel();
|
||||
|
||||
await panel.open();
|
||||
|
||||
expect(await panel.isUploadButtonEnabled()).toBe(true);
|
||||
});
|
||||
|
||||
it('upload button disabled without file-upload feature', async () => {
|
||||
if (!fileSharingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
p2 = await joinJaasMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
|
||||
|
||||
const panel = p2.getFileSharingPanel();
|
||||
|
||||
await panel.open();
|
||||
|
||||
expect(await panel.isUploadButtonEnabled()).toBe(false);
|
||||
});
|
||||
|
||||
it('user with file-upload can delete files uploaded by another participant', async () => {
|
||||
if (!fileSharingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// p1 (with file-upload) uploads a file
|
||||
const p1Panel = p1.getFileSharingPanel();
|
||||
|
||||
await p1Panel.open();
|
||||
await p1Panel.uploadFile(TEST_FILE_PATH);
|
||||
await p1Panel.waitForFile(TEST_FILE_NAME);
|
||||
|
||||
// p2 (joined earlier without file-upload) rejoins with file-upload to test deletion of p1's file
|
||||
p2 = await joinJaasMuc({
|
||||
name: 'p2',
|
||||
token: t({ room: ctx.roomName, features: { 'file-upload': 'true' } })
|
||||
});
|
||||
|
||||
const p2Panel = p2.getFileSharingPanel();
|
||||
|
||||
await p2Panel.open();
|
||||
await p2Panel.waitForFile(TEST_FILE_NAME);
|
||||
|
||||
// p2 should be able to see and use the remove button for p1's file
|
||||
expect(await p2Panel.canRemoveFile(TEST_FILE_NAME)).toBe(true);
|
||||
|
||||
await p2Panel.removeFile(TEST_FILE_NAME);
|
||||
|
||||
await p1Panel.waitForFileGone(TEST_FILE_NAME);
|
||||
});
|
||||
|
||||
it('user without file-upload can download but not delete files', async () => {
|
||||
if (!fileSharingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// p1 (with file-upload) uploads a fresh file
|
||||
const p1Panel = p1.getFileSharingPanel();
|
||||
|
||||
await p1Panel.open();
|
||||
await p1Panel.uploadFile(TEST_FILE_PATH);
|
||||
await p1Panel.waitForFile(TEST_FILE_NAME);
|
||||
|
||||
// p2 rejoins without file-upload
|
||||
p2 = await joinJaasMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
|
||||
|
||||
const p2Panel = p2.getFileSharingPanel();
|
||||
|
||||
await p2Panel.open();
|
||||
await p2Panel.waitForFile(TEST_FILE_NAME);
|
||||
|
||||
// Download button should be present, remove button should not
|
||||
expect(await p2Panel.canDownloadFile(TEST_FILE_NAME)).toBe(true);
|
||||
expect(await p2Panel.canRemoveFile(TEST_FILE_NAME)).toBe(false);
|
||||
});
|
||||
|
||||
it('dragging into conference opens file sharing tab', async () => {
|
||||
if (!fileSharingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// p1 already has file-upload feature
|
||||
const panel = p1.getFileSharingPanel();
|
||||
|
||||
// Ensure chat is closed before starting
|
||||
if (await panel.isChatOpen()) {
|
||||
await p1.getToolbar().clickCloseChatButton();
|
||||
}
|
||||
|
||||
await panel.simulateDragIntoConference();
|
||||
|
||||
await p1.driver.waitUntil(
|
||||
() => panel.isChatOpen(),
|
||||
{ timeout: 3000, timeoutMsg: 'Chat did not open after drag' }
|
||||
);
|
||||
|
||||
expect(await panel.isActive()).toBe(true);
|
||||
});
|
||||
|
||||
it('dragging on pre-join screen does not open file sharing', async () => {
|
||||
if (!fileSharingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Join with iFrame API, file-upload feature, and pre-join screen enabled — but do NOT click join
|
||||
p1 = await joinJaasMuc(
|
||||
{
|
||||
name: 'p1',
|
||||
iFrameApi: true,
|
||||
token: t({ room: ctx.roomName, features: { 'file-upload': 'true' } })
|
||||
},
|
||||
{
|
||||
configOverwrite: { prejoinConfig: { enabled: true } },
|
||||
skipPrejoinButtonClick: true,
|
||||
skipWaitToJoin: true
|
||||
}
|
||||
);
|
||||
|
||||
// Wait for pre-join screen to appear
|
||||
await p1.getPreJoinScreen().waitForLoading();
|
||||
|
||||
// Simulate drag while on pre-join screen
|
||||
const panel = p1.getFileSharingPanel();
|
||||
|
||||
await panel.simulateDragIntoConference();
|
||||
|
||||
// Chat/file sharing should NOT have opened
|
||||
expect(await panel.isChatOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
113
tests/specs/jaas/joinMuc.spec.ts
Normal file
113
tests/specs/jaas/joinMuc.spec.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
import { TOKEN_AUTH_FAILED_TEST_ID, TOKEN_AUTH_FAILED_TITLE_TEST_ID } from '../../pageobjects/Notifications';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
describe('XMPP login and MUC join', () => {
|
||||
it('with a valid token (wildcard room)', async () => {
|
||||
console.log('Joining a MUC with a valid token (wildcard room)');
|
||||
const p = await joinJaasMuc({ token: t({ room: '*' }) });
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
});
|
||||
|
||||
it('with a valid token (specific room)', async () => {
|
||||
console.log('Joining a MUC with a valid token (specific room)');
|
||||
const p = await joinJaasMuc({ token: t({ room: ctx.roomName }) });
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
});
|
||||
|
||||
it('with a token with bad signature', async () => {
|
||||
console.log('Joining a MUC with a token with bad signature');
|
||||
const token = t({ room: ctx.roomName });
|
||||
|
||||
token.jwt = token.jwt + 'badSignature';
|
||||
|
||||
const p = await joinJaasMuc({ token });
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID)
|
||||
|| await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TITLE_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('not allowed to join');
|
||||
});
|
||||
|
||||
it('with an expired token', async () => {
|
||||
console.log('Joining a MUC with an expired token');
|
||||
const p = await joinJaasMuc({ token: t({ exp: '-1m' }) });
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TITLE_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('Token is expired');
|
||||
});
|
||||
|
||||
it('with a token using the wrong key ID', async () => {
|
||||
console.log('Joining a MUC with a token using the wrong key ID');
|
||||
const p = await joinJaasMuc({ token: t({ keyId: 'invalid-key-id' }) });
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('not allowed to join');
|
||||
});
|
||||
|
||||
it('with a token for a different room', async () => {
|
||||
console.log('Joining a MUC with a token for a different room');
|
||||
const p = await joinJaasMuc({ token: t({ room: ctx.roomName + 'different' }) });
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('not allowed to join');
|
||||
});
|
||||
|
||||
it('with a moderator token', async () => {
|
||||
console.log('Joining a MUC with a moderator token');
|
||||
const p = await joinJaasMuc({ token: t({ moderator: true }) });
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(true);
|
||||
});
|
||||
|
||||
// This is dependent on jaas account configuration. All tests under jaas/ expect that "unauthenticated access" is
|
||||
// disabled.
|
||||
it('without a token', async () => {
|
||||
console.log('Joining a MUC without a token');
|
||||
const p = await joinJaasMuc();
|
||||
|
||||
if (expectations.jaas.unauthenticatedJoins) {
|
||||
expect(Boolean(await p.isInMuc())).toBe(true);
|
||||
} else {
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
|
||||
const errorText = await p.getNotifications().getNotificationText(TOKEN_AUTH_FAILED_TEST_ID);
|
||||
|
||||
expect(errorText).toContain('not allowed to join');
|
||||
}
|
||||
});
|
||||
|
||||
it('without sending a conference-request', async () => {
|
||||
console.log('Joining a MUC without sending a conference-request');
|
||||
const p = await joinJaasMuc({
|
||||
token: t({ room: `${ctx.roomName}-no-cf` })
|
||||
}, {
|
||||
configOverwrite: {
|
||||
disableFocus: true // this effectively disables the sending of a conference-request
|
||||
}
|
||||
});
|
||||
|
||||
expect(Boolean(await p.isInMuc())).toBe(false);
|
||||
});
|
||||
});
|
||||
31
tests/specs/jaas/maxOccupants.spec.ts
Normal file
31
tests/specs/jaas/maxOccupants.spec.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('MaxOccupants', () => {
|
||||
it('test maxOccupants limit', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
maxOccupants: 2
|
||||
};
|
||||
|
||||
const p1 = await joinJaasMuc({ token: t({ room: ctx.roomName }) });
|
||||
const p2 = await joinJaasMuc({ name: 'p2', token: t({ room: ctx.roomName }) });
|
||||
|
||||
expect(await p1.isInMuc()).toBe(true);
|
||||
expect(await p2.isInMuc()).toBe(true);
|
||||
|
||||
// Third participant should be rejected (exceeding maxOccupants), even if it's a moderator
|
||||
let p3 = await joinJaasMuc({ name: 'p3', token: t({ room: ctx.roomName, moderator: true }) });
|
||||
|
||||
expect(Boolean(await p3.isInMuc())).toBe(false);
|
||||
|
||||
await p1.hangup();
|
||||
p3 = await joinJaasMuc({ name: 'p3', token: t({ room: ctx.roomName }) });
|
||||
expect(await p3.isInMuc()).toBe(true);
|
||||
});
|
||||
});
|
||||
37
tests/specs/jaas/mobileBrowserDeepLinking.spec.ts
Normal file
37
tests/specs/jaas/mobileBrowserDeepLinking.spec.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
// JaaS deployments set deeplinking.[android|ios].appScheme to 'com.8x8.meet',
|
||||
// which makes getDeepLinkingPage() short-circuit on mobile browsers (see
|
||||
// react/features/deep-linking/functions.web.ts). This test verifies that
|
||||
// behavior by emulating an iPhone and asserting the meeting is reached.
|
||||
describe('JaaS mobile browser deep linking', () => {
|
||||
before(async () => {
|
||||
// Make Chrome look like an iPhone so Platform.OS resolves to 'ios' and
|
||||
// isMobileBrowser() returns true. emulate() persists for the session.
|
||||
await multiRemoteBrowser.getInstance('p1').emulate('device', 'iPhone 15');
|
||||
});
|
||||
|
||||
it('does not show the deep linking page', async () => {
|
||||
// joinJaasMuc waits for the prejoin screen with a 3s timeout. If the
|
||||
// deep linking mobile page had rendered instead, that lookup would
|
||||
// throw, failing the test. Reaching the assertion below means the
|
||||
// prejoin was found.
|
||||
const p = await joinJaasMuc({
|
||||
token: t({ room: '*' })
|
||||
}, {
|
||||
skipWaitToJoin: true
|
||||
});
|
||||
|
||||
// Belt-and-suspenders: the deep linking mobile page renders the
|
||||
// 'launchMeetingLabel' string; assert it isn't on the page.
|
||||
const deepLinkLabel = p.driver.$(
|
||||
'//*[contains(text(),"How do you want to join this meeting?")]');
|
||||
|
||||
expect(await deepLinkLabel.isExisting()).toBe(false);
|
||||
});
|
||||
});
|
||||
51
tests/specs/jaas/passcode.spec.ts
Normal file
51
tests/specs/jaas/passcode.spec.ts
Normal file
@@ -0,0 +1,51 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
import { IToken } from '../../helpers/token';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
const passcode = '1234';
|
||||
|
||||
describe('Setting passcode through settings provisioning', () => {
|
||||
it('With a valid passcode', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
passcode: passcode,
|
||||
visitorsEnabled: true
|
||||
};
|
||||
|
||||
// We want to keep the room from getting destroyed, because the visitors queue has a timeout and causes
|
||||
// problems. We could use different rooms instead, but the webhooksProxy is only configured for the default room.
|
||||
await joinWithPassword('p1', t({ room: ctx.roomName }));
|
||||
await joinWithPassword('p2', t({ room: ctx.roomName, moderator: true }));
|
||||
await joinWithPassword('p2', t({ room: ctx.roomName, visitor: true }));
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Join a password-protected room. Assert that a password is required, that a wrong password does not work, and that
|
||||
* the correct password does work.
|
||||
*/
|
||||
async function joinWithPassword(instanceId: string, token: IToken) {
|
||||
// @ts-ignore
|
||||
const p = await joinJaasMuc({ name: instanceId, token }, { roomName: ctx.roomName });
|
||||
|
||||
await p.waitForMucJoinedOrError();
|
||||
expect(await p.isInMuc()).toBe(false);
|
||||
expect(await p.getPasswordDialog().isOpen()).toBe(true);
|
||||
|
||||
await p.getPasswordDialog().submitPassword('wrong password');
|
||||
await p.waitForMucJoinedOrError();
|
||||
expect(await p.isInMuc()).toBe(false);
|
||||
expect(await p.getPasswordDialog().isOpen()).toBe(true);
|
||||
|
||||
await p.getPasswordDialog().submitPassword(passcode);
|
||||
await p.waitToJoinMUC();
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.getPasswordDialog().isOpen()).toBe(false);
|
||||
}
|
||||
|
||||
25
tests/specs/jaas/passcodeInvalid.spec.ts
Normal file
25
tests/specs/jaas/passcodeInvalid.spec.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
// This test is separate from passcode.spec.ts, because it needs to use a different room name, and webhooksProxy is only
|
||||
// setup for the default room name.
|
||||
describe('Setting invalid passcode through settings provisioning', () => {
|
||||
it('With an invalid passcode', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
passcode: 'passcode-must-be-digits-only'
|
||||
};
|
||||
|
||||
const p = await joinJaasMuc({ token: t({ room: ctx.roomName }) }, { roomName: ctx.roomName });
|
||||
|
||||
// The settings provisioning contains an invalid passcode, the expected result is that the room is not
|
||||
// configured to require a passcode.
|
||||
await p.waitToJoinMUC();
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.getPasswordDialog().isOpen()).toBe(false);
|
||||
});
|
||||
});
|
||||
176
tests/specs/jaas/presence.spec.ts
Normal file
176
tests/specs/jaas/presence.spec.ts
Normal file
@@ -0,0 +1,176 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests the basic webhooks fired for participants joining, leaving, and creating/destroying a conference:
|
||||
* PARTICIPANT_JOINED, PARTICIPANT_LEFT, ROOM_CREATED, ROOM_DESTROYED, ROLE_CHANGED, USAGE.
|
||||
*/
|
||||
describe('Create/destroy/join/leave webhooks', () => {
|
||||
let conferenceJid: string = '';
|
||||
let p1: Participant, p2: Participant;
|
||||
let p1EpId: string, p2EpId: string;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
let room: string;
|
||||
|
||||
async function checkParticipantJoinedHook(p: Participant) {
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
moderator: boolean;
|
||||
name: string;
|
||||
participantId: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
|
||||
expect(event.eventType).toBe('PARTICIPANT_JOINED');
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
expect(event.data.moderator).toBe(p.getToken()?.options?.moderator);
|
||||
expect(event.data.name).toBe(await p.getLocalDisplayName());
|
||||
expect(event.data.participantId).toBe(await p.getEndpointId());
|
||||
expect(event.data.name).toBe(p.name);
|
||||
}
|
||||
async function checkParticipantLeftHook(p: Participant, reason: string) {
|
||||
|
||||
const event: {
|
||||
customerId: string;
|
||||
data: {
|
||||
conference: string;
|
||||
disconnectReason: string;
|
||||
group: string;
|
||||
id: string;
|
||||
isBreakout: boolean;
|
||||
name: string;
|
||||
participantId: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
|
||||
|
||||
expect(event.eventType).toBe('PARTICIPANT_LEFT');
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
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.name);
|
||||
|
||||
const jwtPayload = p.getToken()?.payload;
|
||||
|
||||
expect(event.data.id).toBe(jwtPayload?.context?.user?.id);
|
||||
expect(event.data.group).toBe(jwtPayload?.context?.group);
|
||||
expect(event.customerId).toBe(testsConfig.jaas.customerId);
|
||||
}
|
||||
|
||||
it('setup', async () => {
|
||||
room = ctx.roomName;
|
||||
conferenceJid = `${room}@conference.${testsConfig.jaas.tenant}.${new URL(process.env.BASE_URL || '').hostname}`;
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
p1 = await joinJaasMuc({ name: 'p1', iFrameApi: true, token: t({ room, moderator: true }) });
|
||||
p1EpId = await p1.getEndpointId();
|
||||
expect(await p1.isModerator()).toBe(true);
|
||||
await checkParticipantJoinedHook(p1);
|
||||
await p1.switchToMainFrame();
|
||||
p2 = await joinJaasMuc({ name: 'p2', token: t({ room }) });
|
||||
p2EpId = await p2.getEndpointId();
|
||||
expect(await p2.isModerator()).toBe(false);
|
||||
await checkParticipantJoinedHook(p2);
|
||||
});
|
||||
|
||||
it('USAGE webhook', async () => {
|
||||
const event: {
|
||||
data: [
|
||||
{ participantId: string; }
|
||||
];
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('USAGE');
|
||||
|
||||
expect(event.eventType).toBe('USAGE');
|
||||
|
||||
expect(event.data.some(d => d.participantId === p1EpId));
|
||||
expect(event.data.some(d => d.participantId === p2EpId));
|
||||
});
|
||||
|
||||
it('ROOM_CREATED webhook', async () => {
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROOM_CREATED');
|
||||
|
||||
expect(event.eventType).toBe('ROOM_CREATED');
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
});
|
||||
|
||||
it('ROLE_CHANGED webhook', async () => {
|
||||
await p1.getIframeAPI().executeCommand('grantModerator', p2EpId);
|
||||
|
||||
const event: {
|
||||
data: {
|
||||
grantedBy: {
|
||||
participantId: string;
|
||||
};
|
||||
grantedTo: {
|
||||
participantId: string;
|
||||
};
|
||||
role: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROLE_CHANGED');
|
||||
|
||||
expect(event.eventType).toBe('ROLE_CHANGED');
|
||||
expect(event.data.role).toBe('moderator');
|
||||
expect(event.data.grantedBy.participantId).toBe(p1EpId);
|
||||
expect(event.data.grantedTo.participantId).toBe(p2EpId);
|
||||
});
|
||||
|
||||
it('kick participant', async () => {
|
||||
webhooksProxy.clearCache();
|
||||
await p1.getIframeAPI().executeCommand('kickParticipant', p2EpId);
|
||||
await checkParticipantLeftHook(p2, 'kicked');
|
||||
});
|
||||
|
||||
it('join after kick', async () => {
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
// join again
|
||||
p2 = await joinJaasMuc({ name: 'p2', token: t({ room }) });
|
||||
p2EpId = await p2.getEndpointId();
|
||||
|
||||
await checkParticipantJoinedHook(p2);
|
||||
});
|
||||
|
||||
it('hangup', async () => {
|
||||
await p2.hangup();
|
||||
await checkParticipantLeftHook(p2, 'left');
|
||||
});
|
||||
|
||||
it('dispose conference', async () => {
|
||||
await p1.getIframeAPI().executeCommand('hangup');
|
||||
await checkParticipantLeftHook(p1, 'left');
|
||||
|
||||
const event: {
|
||||
data: {
|
||||
conference: string;
|
||||
isBreakout: boolean;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
expect(event.eventType).toBe('ROOM_DESTROYED');
|
||||
expect(event.data.conference).toBe(conferenceJid);
|
||||
expect(event.data.isBreakout).toBe(false);
|
||||
});
|
||||
});
|
||||
224
tests/specs/jaas/recording.spec.ts
Normal file
224
tests/specs/jaas/recording.spec.ts
Normal file
@@ -0,0 +1,224 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
retry: true,
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests the recording and live-streaming functionality of JaaS (including relevant webhooks) exercising the iFrame API
|
||||
* commands and functions.
|
||||
* TODO: also assert "this meeting is being recorded" notifications are show/played?
|
||||
*/
|
||||
describe('Recording and live-streaming', () => {
|
||||
const tenant = testsConfig.jaas.tenant;
|
||||
const customerId = tenant?.replace('vpaas-magic-cookie-', '');
|
||||
let recordingEnabled: boolean;
|
||||
let liveStreamingEnabled: boolean;
|
||||
let p: Participant;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
|
||||
it('setup', async () => {
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
p = await joinJaasMuc({ iFrameApi: true, token: t({ moderator: true }) }, { roomName: ctx.roomName });
|
||||
|
||||
recordingEnabled = Boolean(await p.execute(() => config.recordingService?.enabled));
|
||||
expect(recordingEnabled).toBe(expectations.jaas.recordingEnabled);
|
||||
|
||||
liveStreamingEnabled = Boolean(await p.execute(() => config.liveStreaming?.enabled));
|
||||
expect(liveStreamingEnabled).toBe(expectations.jaas.liveStreamingEnabled);
|
||||
|
||||
if (liveStreamingEnabled && !process.env.YTUBE_TEST_STREAM_KEY) {
|
||||
liveStreamingEnabled = false;
|
||||
console.log('Skipping live-streaming tests because YTUBE_TEST_STREAM_KEY is not set.');
|
||||
}
|
||||
|
||||
await p.switchToMainFrame();
|
||||
});
|
||||
|
||||
/**
|
||||
* Starts recording and asserts that the expected iFrame and JaaS events are received.
|
||||
* @param command whether to use the "command" or the "function" iFrame API.
|
||||
*/
|
||||
async function startRecording(command: boolean) {
|
||||
await p.getIframeAPI().addEventListener('recordingStatusChanged');
|
||||
await p.getIframeAPI().addEventListener('recordingLinkAvailable');
|
||||
|
||||
if (command) {
|
||||
await p.getIframeAPI().executeCommand('startRecording', {
|
||||
mode: 'file'
|
||||
});
|
||||
} else {
|
||||
await p.getIframeAPI().startRecording({
|
||||
mode: 'file'
|
||||
});
|
||||
}
|
||||
|
||||
const jaasEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_STARTED');
|
||||
|
||||
expect('RECORDING_STARTED').toBe(jaasEvent.eventType);
|
||||
expect(jaasEvent.customerId).toBe(customerId);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
const iFrameEvent = await p.driver.waitUntil(() =>
|
||||
p.getIframeAPI().getEventResult('recordingStatusChanged'), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'recordingStatusChanged event not received'
|
||||
});
|
||||
|
||||
expect(iFrameEvent.mode).toBe('file');
|
||||
expect(iFrameEvent.on).toBe(true);
|
||||
|
||||
const linkEvent = await p.driver.waitUntil(() =>
|
||||
p.getIframeAPI().getEventResult('recordingLinkAvailable'), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'recordingLinkAvailable event not received'
|
||||
});
|
||||
|
||||
expect(linkEvent.link).toStartWith('https://', 'recording link');
|
||||
expect(linkEvent.link).toContain(tenant);
|
||||
expect(linkEvent.ttl).toBeGreaterThan(0);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stops recording and asserts that the expected iFrame and JaaS events are received.
|
||||
* @param command whether to use the "command" or the "function" iFrame API.
|
||||
*/
|
||||
async function stopRecording(command: boolean) {
|
||||
if (command) {
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file');
|
||||
} else {
|
||||
await p.getIframeAPI().stopRecording('file');
|
||||
}
|
||||
|
||||
const jaasEndedEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_ENDED');
|
||||
|
||||
expect('RECORDING_ENDED').toBe(jaasEndedEvent.eventType);
|
||||
expect(jaasEndedEvent.customerId).toBe(customerId);
|
||||
|
||||
const jaasUploadedEvent: {
|
||||
customerId: string;
|
||||
data: {
|
||||
initiatorId: string;
|
||||
participants: Array<string>;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('RECORDING_UPLOADED');
|
||||
|
||||
const jwtPayload = p.getToken()?.payload;
|
||||
|
||||
expect(jaasUploadedEvent.data.initiatorId).toBe(jwtPayload?.context?.user?.id);
|
||||
expect(jaasUploadedEvent.data.participants.some(
|
||||
// @ts-ignore
|
||||
e => e.id === jwtPayload?.context?.user?.id)).toBe(true);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
const iFrameEvent = (await p.getIframeAPI().getEventResult('recordingStatusChanged'));
|
||||
|
||||
expect(iFrameEvent.mode).toBe('file');
|
||||
expect(iFrameEvent.on).toBe(false);
|
||||
|
||||
await p.getIframeAPI().clearEventResults('recordingStatusChanged');
|
||||
}
|
||||
|
||||
it('start/stop recording using the iFrame command', async () => {
|
||||
if (!recordingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await startRecording(true);
|
||||
await stopRecording(true);
|
||||
|
||||
// to avoid rate limits
|
||||
await p.driver.pause(30000);
|
||||
});
|
||||
|
||||
it('start/stop recording using the iFrame function', async () => {
|
||||
if (!recordingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await startRecording(false);
|
||||
await stopRecording(false);
|
||||
|
||||
// to avoid rate limits
|
||||
await p.driver.pause(30000);
|
||||
});
|
||||
|
||||
it('start/stop live-streaming using the iFrame command', async () => {
|
||||
if (!liveStreamingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
await p.getIframeAPI().addEventListener('recordingStatusChanged');
|
||||
|
||||
await p.getIframeAPI().executeCommand('startRecording', {
|
||||
youtubeBroadcastID: process.env.YTUBE_TEST_BROADCAST_ID,
|
||||
mode: 'stream',
|
||||
youtubeStreamKey: process.env.YTUBE_TEST_STREAM_KEY
|
||||
});
|
||||
|
||||
const jaasEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('LIVE_STREAM_STARTED');
|
||||
|
||||
expect('LIVE_STREAM_STARTED').toBe(jaasEvent.eventType);
|
||||
expect(jaasEvent.customerId).toBe(customerId);
|
||||
|
||||
const iFrameEvent = await p.driver.waitUntil(async () => {
|
||||
const e = await p.getIframeAPI().getEventResult('recordingStatusChanged');
|
||||
|
||||
return e && e.mode === 'stream' && e.on === true ? e : false;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'recordingStatusChanged event with mode=stream, on=true not received'
|
||||
});
|
||||
|
||||
expect(iFrameEvent.mode).toBe('stream');
|
||||
expect(iFrameEvent.on).toBe(true);
|
||||
|
||||
if (process.env.YTUBE_TEST_BROADCAST_ID) {
|
||||
const liveStreamUrl = await p.getIframeAPI().getLivestreamUrl();
|
||||
|
||||
expect(liveStreamUrl.livestreamUrl).toBeDefined();
|
||||
}
|
||||
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'stream');
|
||||
|
||||
const jaasEndedEvent: {
|
||||
customerId: string;
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('LIVE_STREAM_ENDED');
|
||||
|
||||
expect(jaasEndedEvent.eventType).toBe('LIVE_STREAM_ENDED');
|
||||
expect(jaasEndedEvent.customerId).toBe(customerId);
|
||||
|
||||
const iFrameEndedEvent = await p.driver.waitUntil(async () => {
|
||||
const e = await p.getIframeAPI().getEventResult('recordingStatusChanged');
|
||||
|
||||
return e && e.mode === 'stream' && e.on === false ? e : false;
|
||||
}, {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'recordingStatusChanged event with mode=stream, on=false not received'
|
||||
});
|
||||
|
||||
expect(iFrameEndedEvent.mode).toBe('stream');
|
||||
expect(iFrameEndedEvent.on).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
138
tests/specs/jaas/recordingDialogConfig.spec.ts
Normal file
138
tests/specs/jaas/recordingDialogConfig.spec.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
/**
|
||||
* Joins (or rejoins) p1 as a moderator granting only the given JWT feature claims, with no
|
||||
* config.js overrides anywhere in this file — see recordButtonProps() / isLiveStreamingButtonVisible()
|
||||
* in react/features/recording: on a JaaS (vpaas) tenant there is no config-only fallback once the
|
||||
* JWT lacks a feature claim, unlike the non-JaaS tenant, so toolbar visibility here is driven purely
|
||||
* by these feature claims against the environment's ambient (always-on, per expectations.jaas.*)
|
||||
* config. The config-driven side of this same dialog (no custom JWT, varying config.js instead) is
|
||||
* covered separately by tests/specs/misc/recordingDialogConfig.spec.ts.
|
||||
*
|
||||
* A fresh join is required (rather than ensureOneParticipant, which no-ops if already in the
|
||||
* conference) because each test in this file exercises a different set of JWT feature claims.
|
||||
*/
|
||||
async function joinWithFeatures(features: Record<string, boolean>): Promise<void> {
|
||||
await joinJaasMuc({
|
||||
name: 'p1',
|
||||
token: t({
|
||||
moderator: true,
|
||||
features
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Mirrors LocalRecordingManager.isSupported() plus the embedding guard that
|
||||
* supportsLocalRecording() adds — the same probe tests/specs/misc/recordingDialogConfig.spec.ts and
|
||||
* recordingButtonVisibility.spec.ts use, because these browser capability gates are frequently
|
||||
* unmet in the automated test browser regardless of any JWT feature claim.
|
||||
*/
|
||||
async function isLocalRecordingSupportedByBrowser(): Promise<boolean> {
|
||||
return ctx.p1.execute(() => {
|
||||
const browser = JitsiMeetJS.util.browser;
|
||||
const PREFERRED_MEDIA_TYPE = 'video/webm;codecs=vp8,opus';
|
||||
|
||||
const isSupported = browser.isChromiumBased()
|
||||
&& !browser.isElectron()
|
||||
&& !browser.isReactNative()
|
||||
&& !browser.isMobileDevice()
|
||||
|
||||
// @ts-ignore
|
||||
&& Boolean(navigator.mediaDevices.setCaptureHandleConfig)
|
||||
|
||||
// @ts-ignore
|
||||
&& typeof window.showSaveFilePicker !== 'undefined'
|
||||
&& MediaRecorder.isTypeSupported(PREFERRED_MEDIA_TYPE);
|
||||
|
||||
let embeddedOk = true;
|
||||
|
||||
try {
|
||||
if (window.self !== window.top) {
|
||||
embeddedOk = window.self.location.host === window.parent.location.host;
|
||||
}
|
||||
} catch (e) {
|
||||
embeddedOk = false;
|
||||
}
|
||||
|
||||
return isSupported && embeddedOk;
|
||||
});
|
||||
}
|
||||
|
||||
setTestProperties(__filename, {
|
||||
description: 'Recording & Transcription dialog toolbar visibility across JWT feature claims',
|
||||
usesBrowsers: [ 'p1' ],
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
/*
|
||||
* Full 2^3 matrix over the three independently grantable JWT feature claims: recording,
|
||||
* transcription and livestreaming. No config.js override is used anywhere here — the environment's
|
||||
* ambient config is assumed to always enable recordingService, transcription and liveStreaming (see
|
||||
* expectations.jaas.*), so every combination below isolates the JWT-feature-gated branches of
|
||||
* getRecordButtonProps() and isLiveStreamingButtonVisible() rather than config:
|
||||
*
|
||||
* - On a JaaS tenant, getRecordButtonProps() has no config-only fallback: the toolbar Record &
|
||||
* Transcribe button's visibility reduces to localRecordingAvailable || recording || transcription
|
||||
* (the JWT claims), since the ambient config enables both services.
|
||||
* - The Live Streaming toolbar button is fully orthogonal and purely JWT-gated: only the
|
||||
* livestreaming claim matters (see LiveStream/AbstractLiveStreamButton.ts).
|
||||
*
|
||||
* localRecordingAvailable also depends on browser capabilities (MediaRecorder/File System Access
|
||||
* support) that are frequently unmet in the automated test browser — probed once up front so the
|
||||
* per-combination expectations stay accurate regardless of what the test browser actually supports.
|
||||
*/
|
||||
describe('Recording dialog JWT feature matrix — recording × transcription × live streaming', () => {
|
||||
// A const-bound holder object, rather than a reassigned `let`, so the it() closures created
|
||||
// inside the loop below don't trip @typescript-eslint/no-loop-func — only its property (set
|
||||
// once, in 'setup', before any of the loop's tests run) actually changes.
|
||||
const localRecordingSupport = { byBrowser: false };
|
||||
|
||||
it('setup', async () => {
|
||||
if (!expectations.jaas.recordingEnabled
|
||||
|| !expectations.jaas.transcriptionEnabled
|
||||
|| !expectations.jaas.liveStreamingEnabled) {
|
||||
ctx.skipSuiteTests = 'Recording, transcription and live streaming must all be enabled '
|
||||
+ 'in this environment for the JWT feature claims to be the only variable';
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
await joinWithFeatures({
|
||||
recording: true,
|
||||
transcription: true,
|
||||
livestreaming: true
|
||||
});
|
||||
localRecordingSupport.byBrowser = await isLocalRecordingSupportedByBrowser();
|
||||
});
|
||||
|
||||
const BOOLEANS = [ true, false ];
|
||||
|
||||
for (const recordingFeature of BOOLEANS) {
|
||||
for (const transcriptionFeature of BOOLEANS) {
|
||||
for (const livestreamingFeature of BOOLEANS) {
|
||||
const title = `recording=${recordingFeature} `
|
||||
+ `transcription=${transcriptionFeature} `
|
||||
+ `livestreaming=${livestreamingFeature}`;
|
||||
|
||||
it(title, async () => {
|
||||
await joinWithFeatures({
|
||||
recording: recordingFeature,
|
||||
transcription: transcriptionFeature,
|
||||
livestreaming: livestreamingFeature
|
||||
});
|
||||
|
||||
const p1 = ctx.p1;
|
||||
|
||||
expect(await p1.getToolbar().hasLiveStreamingButton()).toBe(livestreamingFeature);
|
||||
|
||||
const dialogCanOpen = recordingFeature || transcriptionFeature || localRecordingSupport.byBrowser;
|
||||
|
||||
expect(await p1.getToolbar().hasRecordingButton()).toBe(dialogCanOpen);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
617
tests/specs/jaas/recordingTranscription.spec.ts
Normal file
617
tests/specs/jaas/recordingTranscription.spec.ts
Normal file
@@ -0,0 +1,617 @@
|
||||
import { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../helpers/TestsConfig';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true
|
||||
});
|
||||
|
||||
// Counter used to give each test its own room — see joinAsModerator().
|
||||
let roomCounter = 0;
|
||||
|
||||
/**
|
||||
* Builds a fresh, per-test room name out of ctx.roomName (see joinAsModerator()).
|
||||
*
|
||||
* ctx.roomName already has the environment's ROOM_NAME_SUFFIX (e.g. "_alpha-canary") appended as
|
||||
* its trailing component (see generateRoomName() in helpers/utils.ts), and some environments rely
|
||||
* on that suffix being the tail of the room name (e.g. to route/map the conference correctly).
|
||||
* Simply appending "-${roomCounter}" to ctx.roomName would push the suffix out of that position,
|
||||
* so the counter is inserted right before it instead.
|
||||
*/
|
||||
function uniqueRoomName(): string {
|
||||
const suffix = testsConfig.roomName.suffix;
|
||||
|
||||
if (suffix && ctx.roomName.endsWith(`_${suffix}`)) {
|
||||
return `${ctx.roomName.slice(0, -`_${suffix}`.length)}-${roomCounter}_${suffix}`;
|
||||
}
|
||||
|
||||
return `${ctx.roomName}-${roomCounter}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Joins a JaaS meeting as a moderator using the iFrame API wrapper.
|
||||
*
|
||||
* Each call joins a unique room. All tests in this spec otherwise share ctx.roomName, and joinMuc
|
||||
* rejoins it each test, so prosody's per-room jibri start/stop throttle (mod_filter_iq_jibri,
|
||||
* max_number_room_attempts_per_minute, default 3 — both start and stop count) accumulates across
|
||||
* the suite and trips the "started a recording too quickly" policy error. A fresh room per test
|
||||
* resets that throttle, keeping each test's ≤2 start/stop attempts well under the limit. The
|
||||
* moderator token is room-agnostic (room defaults to '*'), so it is valid for any room.
|
||||
*/
|
||||
async function joinAsModerator(): Promise<Participant> {
|
||||
roomCounter += 1;
|
||||
|
||||
const p = await joinJaasMuc(
|
||||
{ iFrameApi: true, token: t({ moderator: true }) },
|
||||
{ roomName: uniqueRoomName() });
|
||||
|
||||
// joinJaasMuc leaves the driver focused inside the Jitsi iframe, but the iFrame API
|
||||
// (window.jitsiAPI) lives on the wrapper page. Switch to the main frame so executeCommand()
|
||||
// works, matching the setup in recording.spec.ts / transcriptions.spec.ts.
|
||||
await p.switchToMainFrame();
|
||||
|
||||
return p;
|
||||
}
|
||||
|
||||
/**
|
||||
* Minimum wall-clock gap enforced between successive Jibri start/stop commands sent by this
|
||||
* spec, in ms.
|
||||
*
|
||||
* Prosody's mod_filter_iq_jibri throttles jibri start/stop IQs per source IP
|
||||
* (max_number_ip_attempts_per_minute, default 9/min), separately from the per-room throttle
|
||||
* joinAsModerator()'s fresh room already dodges — a fresh room resets the room-scoped throttle
|
||||
* only, not this one. This suite alone sends far more than 9 start/stop IQs across its tests, so
|
||||
* without pacing it trips the per-IP throttle (observed as "policy-violation"/wait on the start
|
||||
* or stop IQ, surfacing as a session that never actually starts or stops). 8s keeps this file's
|
||||
* rate under the throttle's refill rate (60s / 9 ≈ 6.7s), with headroom for other jaas specs
|
||||
* sharing the same CI egress IP.
|
||||
*/
|
||||
const MIN_JIBRI_COMMAND_GAP_MS = 8_000;
|
||||
let lastJibriCommandTime = 0;
|
||||
|
||||
/**
|
||||
* Waits out whatever is left of MIN_JIBRI_COMMAND_GAP_MS since the last call, then marks now as
|
||||
* the new last call time. Await this immediately before every jibri start/stop command.
|
||||
*/
|
||||
async function paceJibriCommand(): Promise<void> {
|
||||
const wait = MIN_JIBRI_COMMAND_GAP_MS - (Date.now() - lastJibriCommandTime);
|
||||
|
||||
if (wait > 0) {
|
||||
await new Promise(resolve => setTimeout(resolve, wait));
|
||||
}
|
||||
|
||||
lastJibriCommandTime = Date.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the startRecording command and waits until jicofo actually accepts it (a live FILE
|
||||
* session shows up in the recording state), retrying on silent rejection.
|
||||
*
|
||||
* A start IQ that reaches jicofo right after joining can be rejected with policy-violation
|
||||
* (type=wait) while the conference is still being set up, and the client surfaces nothing —
|
||||
* the recording just never starts. The error type says "retry later", so do that. Waiting for
|
||||
* the JVB session before starting instead is NOT an option: jicofo does not allocate a session
|
||||
* for a lone participant at all, it does so only when a second endpoint (e.g. jibri) joins.
|
||||
*
|
||||
* Expects the driver on the main frame; leaves it there.
|
||||
*/
|
||||
async function startFileRecording(p: Participant, options: { mode: string; transcription?: boolean; }) {
|
||||
for (let attempt = 1; attempt <= 3; attempt++) {
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('startRecording', options);
|
||||
await p.switchToIFrame();
|
||||
|
||||
const accepted = await p.driver.waitUntil(
|
||||
() => p.execute(() => Boolean(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
APP.store.getState()['features/recording'].sessionDatas.some(
|
||||
(s: any) => s.mode === 'file' && (s.status === 'pending' || s.status === 'on')))),
|
||||
{ timeout: 5_000, timeoutMsg: '' }
|
||||
).catch(() => false);
|
||||
|
||||
await p.switchToMainFrame();
|
||||
|
||||
if (accepted) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error('startRecording was not accepted by jicofo after 3 attempts');
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits for the "dialog.recording" notification to appear inside the current iframe context.
|
||||
* The notification fires whenever recording or transcription starts (or both).
|
||||
*/
|
||||
async function waitForRecordingNotification(p: Participant, timeoutMsg: string): Promise<void> {
|
||||
await p.driver.$('[data-testid="dialog.recording"]').waitForExist({
|
||||
timeout: 30_000,
|
||||
timeoutMsg
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Dismisses the current "dialog.recording" notification (if it is showing) and waits for it
|
||||
* to disappear, so subsequent assertions start from a clean slate.
|
||||
*/
|
||||
async function dismissRecordingNotification(p: Participant): Promise<void> {
|
||||
const dismissButton = p.driver.$('[data-testid="dialog.recording-dismiss"]');
|
||||
|
||||
if (await dismissButton.isExisting()) {
|
||||
await dismissButton.moveTo();
|
||||
await dismissButton.click();
|
||||
await p.driver.$('[data-testid="dialog.recording"]')
|
||||
.waitForExist({ reverse: true, timeout: 3_000 });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the recording feature state reports that a FILE-mode session is fully ON.
|
||||
* A PENDING session is not enough: jicofo rejects a stop request for a session whose jibri
|
||||
* is still starting (policy-violation/wait), so acting on a PENDING session races the backend.
|
||||
*/
|
||||
async function waitForRecordingRunning(p: Participant): Promise<void> {
|
||||
await p.driver.waitUntil(
|
||||
() => p.execute(() => Boolean(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
APP.store.getState()['features/recording'].sessionDatas.some(
|
||||
(s: any) => s.mode === 'file' && s.status === 'on'))),
|
||||
{ timeout: 30_000, timeoutMsg: 'Recording session did not become active' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the transcription feature state reports that transcription is active.
|
||||
*/
|
||||
async function waitForTranscriptionRunning(p: Participant): Promise<void> {
|
||||
await p.driver.waitUntil(
|
||||
() => p.execute(() => Boolean(APP.store.getState()['features/transcribing'].isTranscribing)),
|
||||
{ timeout: 30_000, timeoutMsg: 'Transcription did not become active' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the transcription feature state reports that transcription is no longer active.
|
||||
*
|
||||
* @param {Participant} p - The participant.
|
||||
* @param {number} timeout - How long to wait, in ms.
|
||||
*/
|
||||
async function waitForTranscriptionStopped(p: Participant, timeout = 20_000): Promise<void> {
|
||||
await p.driver.waitUntil(
|
||||
() => p.execute(() => !APP.store.getState()['features/transcribing'].isTranscribing),
|
||||
{ timeout, timeoutMsg: 'Transcription did not stop' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits until the recording feature state reports that no FILE session is active.
|
||||
*
|
||||
* @param {Participant} p - The participant.
|
||||
* @param {number} timeout - How long to wait, in ms.
|
||||
*/
|
||||
async function waitForRecordingStopped(p: Participant, timeout = 20_000): Promise<void> {
|
||||
await p.driver.waitUntil(
|
||||
() => p.execute(() => Boolean(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
APP.store.getState()['features/recording'].sessionDatas.every(
|
||||
(s: any) => s.mode !== 'file' || s.status === 'off'))),
|
||||
{ timeout, timeoutMsg: 'Recording session did not stop' }
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Best-effort wait, used in cleanup, for a stopRecording command to actually take effect before the
|
||||
* participant leaves. Never fails the test — a timeout here just means the stop was slow to reflect,
|
||||
* which must not turn a passing test red during teardown.
|
||||
*
|
||||
* @param {Participant} p - The participant.
|
||||
* @param {boolean} transcription - Whether transcription was also stopped (stopRecording(..., true)).
|
||||
*/
|
||||
async function waitForRecordingStopExecuted(p: Participant, transcription = false): Promise<void> {
|
||||
// Short, bounded wait — this is best-effort teardown, not an assertion, so don't burn the full
|
||||
// 20s assertion timeout here.
|
||||
const cleanupTimeout = 5_000;
|
||||
|
||||
await p.switchToIFrame();
|
||||
|
||||
try {
|
||||
await waitForRecordingStopped(p, cleanupTimeout);
|
||||
|
||||
if (transcription) {
|
||||
await waitForTranscriptionStopped(p, cleanupTimeout);
|
||||
}
|
||||
} catch (e) {
|
||||
// Best-effort cleanup — ignore timeouts so teardown never fails the test.
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Nudge notification tests ────────────────────────────────────────────────
|
||||
|
||||
describe('Recording & transcription nudge notifications', () => {
|
||||
it('nudge shows "Start transcribing" action when only recording starts', async () => {
|
||||
if (!expectations.jaas.recordingEnabled || !expectations.jaas.transcriptionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await startFileRecording(p, { mode: 'file' });
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForRecordingNotification(p, 'Recording-started notification did not appear');
|
||||
|
||||
const nudgeButton = p.driver.$('[data-testid="dialog.startTranscribing"]');
|
||||
|
||||
// A "recording pending" notification appears (same data-testid) before the "recording
|
||||
// started" notification that actually carries the nudge, so wait for the nudge to show up
|
||||
// rather than checking immediately. The iFrame startRecording path sets
|
||||
// isTranscribingEnabled:false, so transcription never auto-starts here and the
|
||||
// "Start transcribing" nudge always applies once recording is running.
|
||||
await nudgeButton.waitForExist({
|
||||
timeout: 30_000,
|
||||
timeoutMsg: '"Start transcribing" nudge did not appear'
|
||||
});
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file');
|
||||
await waitForRecordingStopExecuted(p);
|
||||
}
|
||||
});
|
||||
|
||||
it('nudge shows "Start recording" action when only transcription starts', async () => {
|
||||
if (!expectations.jaas.transcriptionEnabled || !expectations.jaas.recordingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('startRecording', { transcription: true });
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForRecordingNotification(p, 'Transcription-started notification did not appear');
|
||||
|
||||
const nudgeButton = p.driver.$('[data-testid="dialog.startRecording"]');
|
||||
|
||||
expect(await nudgeButton.isExisting()).toBe(true);
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
await waitForRecordingStopExecuted(p, true);
|
||||
}
|
||||
});
|
||||
|
||||
it('no spurious "recording started" notification when stopping transcription while recording runs', async () => {
|
||||
if (!expectations.jaas.recordingEnabled || !expectations.jaas.transcriptionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await startFileRecording(p, {
|
||||
mode: 'file',
|
||||
transcription: true
|
||||
});
|
||||
|
||||
await p.switchToIFrame();
|
||||
await waitForRecordingNotification(p, 'Recording+transcription start notification did not appear');
|
||||
await dismissRecordingNotification(p);
|
||||
await p.switchToMainFrame();
|
||||
|
||||
// Stop only transcription while recording keeps running.
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
|
||||
await p.switchToIFrame();
|
||||
|
||||
// Give the UI time to settle — a spurious notification would appear within ~3 s.
|
||||
await p.driver.pause(4_000);
|
||||
|
||||
// The description keys used on a new "recording started" notification —
|
||||
// neither must appear after a transcription-only stop.
|
||||
expect(await p.driver.$('[data-testid="recording.on"]').isExisting()).toBe(false);
|
||||
expect(await p.driver.$('[data-testid="recording.onWithTranscription"]').isExisting()).toBe(false);
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file');
|
||||
await waitForRecordingStopExecuted(p);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Manage-mode (session active) dialog tests ───────────────────────────────
|
||||
|
||||
describe('Recording & transcription dialog — manage mode', () => {
|
||||
it('recording section shows Stop when recording is running', async () => {
|
||||
if (!expectations.jaas.recordingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await startFileRecording(p, { mode: 'file' });
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForRecordingRunning(p);
|
||||
|
||||
await p.getToolbar().clickRecordingButton();
|
||||
|
||||
const dialog = p.getRecordingTranscriptionDialog();
|
||||
|
||||
await dialog.waitForDisplay();
|
||||
|
||||
expect(await dialog.hasStopRecordingButton()).toBe(true);
|
||||
expect(await dialog.hasStartTranscriptionButton()).toBe(true);
|
||||
|
||||
// Mixed state — the footer offers both bulk actions.
|
||||
expect(await dialog.hasStopBothButton()).toBe(true);
|
||||
expect(await dialog.hasStartBothButton()).toBe(true);
|
||||
|
||||
await dialog.cancel();
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file');
|
||||
await waitForRecordingStopExecuted(p);
|
||||
}
|
||||
});
|
||||
|
||||
it('transcription section shows Stop when transcription is running', async () => {
|
||||
if (!expectations.jaas.transcriptionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('startRecording', { transcription: true });
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForTranscriptionRunning(p);
|
||||
|
||||
await p.getToolbar().clickRecordingButton();
|
||||
|
||||
const dialog = p.getRecordingTranscriptionDialog();
|
||||
|
||||
await dialog.waitForDisplay();
|
||||
|
||||
expect(await dialog.hasStopTranscriptionButton()).toBe(true);
|
||||
expect(await dialog.hasStartRecordingButton()).toBe(true);
|
||||
|
||||
// Mixed state — the footer offers both bulk actions.
|
||||
expect(await dialog.hasStopBothButton()).toBe(true);
|
||||
expect(await dialog.hasStartBothButton()).toBe(true);
|
||||
|
||||
await dialog.cancel();
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
await waitForRecordingStopExecuted(p, true);
|
||||
}
|
||||
});
|
||||
|
||||
it('both sections show Stop and footer shows only "Stop both" when both run', async () => {
|
||||
if (!expectations.jaas.recordingEnabled || !expectations.jaas.transcriptionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await startFileRecording(p, {
|
||||
mode: 'file',
|
||||
transcription: true
|
||||
});
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForRecordingRunning(p);
|
||||
await waitForTranscriptionRunning(p);
|
||||
|
||||
await p.getToolbar().clickRecordingButton();
|
||||
|
||||
const dialog = p.getRecordingTranscriptionDialog();
|
||||
|
||||
await dialog.waitForDisplay();
|
||||
|
||||
expect(await dialog.hasStopRecordingButton()).toBe(true);
|
||||
expect(await dialog.hasStopTranscriptionButton()).toBe(true);
|
||||
expect(await dialog.hasStopBothButton()).toBe(true);
|
||||
expect(await dialog.hasStartBothButton()).toBe(false);
|
||||
|
||||
await dialog.cancel();
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file');
|
||||
await waitForRecordingStopExecuted(p);
|
||||
}
|
||||
});
|
||||
|
||||
it('"Recorder follows me" is locked while recording runs', async () => {
|
||||
if (!expectations.jaas.recordingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await startFileRecording(p, { mode: 'file' });
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForRecordingRunning(p);
|
||||
|
||||
await p.getToolbar().clickRecordingButton();
|
||||
|
||||
const dialog = p.getRecordingTranscriptionDialog();
|
||||
|
||||
await dialog.waitForDisplay();
|
||||
await dialog.toggleRecordingOptions();
|
||||
|
||||
// The switch only shows for moderators with a cloud service selected.
|
||||
if (await dialog.hasFollowMeRecorderSwitch()) {
|
||||
// The recorder cannot pick up the change mid-session.
|
||||
expect(await dialog.isFollowMeRecorderEnabled()).toBe(false);
|
||||
}
|
||||
|
||||
await dialog.cancel();
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file');
|
||||
await waitForRecordingStopExecuted(p);
|
||||
}
|
||||
});
|
||||
|
||||
it('can stop transcription independently while recording keeps running', async () => {
|
||||
if (!expectations.jaas.recordingEnabled || !expectations.jaas.transcriptionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await startFileRecording(p, {
|
||||
mode: 'file',
|
||||
transcription: true
|
||||
});
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForRecordingRunning(p);
|
||||
await waitForTranscriptionRunning(p);
|
||||
|
||||
await p.getToolbar().clickRecordingButton();
|
||||
|
||||
const dialog = p.getRecordingTranscriptionDialog();
|
||||
|
||||
await dialog.waitForDisplay();
|
||||
|
||||
// Stop only transcription — the action applies immediately and closes the dialog.
|
||||
await paceJibriCommand();
|
||||
await dialog.stopTranscription();
|
||||
|
||||
// Transcription should stop.
|
||||
await waitForTranscriptionStopped(p);
|
||||
|
||||
// Recording should still be running.
|
||||
const recordingStillOn = await p.execute(() => Boolean(
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
APP.store.getState()['features/recording'].sessionDatas.some(
|
||||
(s: any) => s.mode === 'file' && s.status !== 'off')
|
||||
));
|
||||
|
||||
expect(recordingStillOn).toBe(true);
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file');
|
||||
await waitForRecordingStopExecuted(p);
|
||||
}
|
||||
});
|
||||
|
||||
it('can stop recording independently while transcription keeps running', async () => {
|
||||
if (!expectations.jaas.recordingEnabled || !expectations.jaas.transcriptionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await startFileRecording(p, {
|
||||
mode: 'file',
|
||||
transcription: true
|
||||
});
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForRecordingRunning(p);
|
||||
await waitForTranscriptionRunning(p);
|
||||
|
||||
await p.getToolbar().clickRecordingButton();
|
||||
|
||||
const dialog = p.getRecordingTranscriptionDialog();
|
||||
|
||||
await dialog.waitForDisplay();
|
||||
|
||||
// Stop only recording — the action applies immediately and closes the dialog.
|
||||
await paceJibriCommand();
|
||||
await dialog.stopRecording();
|
||||
|
||||
// Recording should stop.
|
||||
await waitForRecordingStopped(p);
|
||||
|
||||
// Transcription should still be running.
|
||||
const transcriptionStillOn = await p.execute(
|
||||
() => Boolean(APP.store.getState()['features/transcribing'].isTranscribing)
|
||||
);
|
||||
|
||||
expect(transcriptionStillOn).toBe(true);
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
await waitForRecordingStopExecuted(p, true);
|
||||
}
|
||||
});
|
||||
|
||||
it('"Stop both" stops recording and transcription together', async () => {
|
||||
if (!expectations.jaas.recordingEnabled || !expectations.jaas.transcriptionEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinAsModerator();
|
||||
|
||||
try {
|
||||
await startFileRecording(p, {
|
||||
mode: 'file',
|
||||
transcription: true
|
||||
});
|
||||
await p.switchToIFrame();
|
||||
|
||||
await waitForRecordingRunning(p);
|
||||
await waitForTranscriptionRunning(p);
|
||||
|
||||
await p.getToolbar().clickRecordingButton();
|
||||
|
||||
const dialog = p.getRecordingTranscriptionDialog();
|
||||
|
||||
await dialog.waitForDisplay();
|
||||
await paceJibriCommand();
|
||||
await dialog.stopBoth();
|
||||
|
||||
await waitForRecordingStopped(p);
|
||||
await waitForTranscriptionStopped(p);
|
||||
} finally {
|
||||
await p.switchToMainFrame();
|
||||
await paceJibriCommand();
|
||||
await p.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
await waitForRecordingStopExecuted(p, true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Button visibility ────────────────────────────────────────────────────────
|
||||
|
||||
describe('Recording button visibility', () => {
|
||||
it('moderator button has aria-label "Record & Transcribe"', async () => {
|
||||
if (!expectations.jaas.recordingEnabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
const p = await joinJaasMuc({ token: t({ moderator: true }) });
|
||||
|
||||
expect(await p.getToolbar().hasRecordingButton()).toBe(true);
|
||||
|
||||
// This participant joined without the iFrame API, so hang up via the app, not window.jitsiAPI.
|
||||
await p.hangup();
|
||||
});
|
||||
});
|
||||
251
tests/specs/jaas/transcriptions.spec.ts
Normal file
251
tests/specs/jaas/transcriptions.spec.ts
Normal file
@@ -0,0 +1,251 @@
|
||||
import type { Participant } from '../../helpers/Participant';
|
||||
import { setTestProperties } from '../../helpers/TestProperties';
|
||||
import type WebhookProxy from '../../helpers/WebhookProxy';
|
||||
import { expectations } from '../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
const asyncTranscriptionValues = [
|
||||
...expectations.jaas.transcription.jigasiTranscription ? [ false ] : [],
|
||||
...expectations.jaas.transcription.asyncTranscription ? [ true ] : []
|
||||
];
|
||||
|
||||
for (const asyncTranscriptions of asyncTranscriptionValues) {
|
||||
describe(`Transcription (async=${asyncTranscriptions})`, () => {
|
||||
let p1: Participant, p2: Participant;
|
||||
let webhooksProxy: WebhookProxy;
|
||||
|
||||
async function clearTranscriptionStatusChange() {
|
||||
await p1.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().clearEventResults('transcribingStatusChanged');
|
||||
}
|
||||
|
||||
/**
|
||||
* Wait until a transcribingStatusChanged iFrame event is received for both p1 and p2, with a specific value
|
||||
* for the "on" field.
|
||||
* Note that addEventListener for 'transcriptionChunkReceived' should have been called prior to this function.
|
||||
*/
|
||||
async function waitForTranscriptionStatusChange(expectedOn: boolean) {
|
||||
for (const p of [ p1, p2 ]) {
|
||||
const event = await p.driver.waitUntil(
|
||||
() => p.getIframeAPI().getEventResult('transcribingStatusChanged'),
|
||||
{
|
||||
timeout: 10000,
|
||||
timeoutMsg: `transcribingStatusChanged event not received on ${p.name}`
|
||||
});
|
||||
|
||||
if (event.on !== expectedOn) {
|
||||
throw new Error(`Expected transcribing to be ${expectedOn} for ${p.name}, got ${event.on}`);
|
||||
}
|
||||
if (!expectedOn && !asyncTranscriptions) {
|
||||
// The "stopped" event is sometimes fired before the jigasi participant leaves. If we re-start
|
||||
// transcription before jigasi has left, jicofo will reject the request.
|
||||
await p.switchToIFrame();
|
||||
await p.waitForParticipants(
|
||||
1,
|
||||
'Unexpected number of participants. Jigasi failed to leave?');
|
||||
await p.switchToMainFrame();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
it('setup', async () => {
|
||||
const room = ctx.roomName;
|
||||
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
webhooksProxy.defaultMeetingSettings = { asyncTranscriptions };
|
||||
|
||||
p1 = await joinJaasMuc({
|
||||
name: 'p1',
|
||||
token: t({ room, moderator: true }),
|
||||
iFrameApi: true
|
||||
});
|
||||
|
||||
const transcriptionEnabled = await p1.execute(() => config.transcription?.enabled);
|
||||
|
||||
expect(transcriptionEnabled).toBe(expectations.jaas.transcriptionEnabled);
|
||||
|
||||
const roomMetadata = await p1.getRoomMetadata();
|
||||
|
||||
if (asyncTranscriptions) {
|
||||
expect(roomMetadata.asyncTranscription).toBe(true);
|
||||
} else {
|
||||
expect(roomMetadata.asyncTranscription).toBeFalsy();
|
||||
}
|
||||
|
||||
p2 = await joinJaasMuc({
|
||||
name: 'p2',
|
||||
token: t({ room }),
|
||||
iFrameApi: true
|
||||
}, {
|
||||
configOverwrite: {
|
||||
startWithAudioMuted: true
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all([
|
||||
p1.switchToMainFrame(),
|
||||
p2.switchToMainFrame(),
|
||||
]);
|
||||
|
||||
expect(await p1.getIframeAPI().getEventResult('isModerator')).toBe(true);
|
||||
expect(await p1.getIframeAPI().getEventResult('videoConferenceJoined')).toBeDefined();
|
||||
|
||||
await p1.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
await p2.getIframeAPI().addEventListener('transcribingStatusChanged');
|
||||
await p1.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().addEventListener('transcriptionChunkReceived');
|
||||
|
||||
});
|
||||
|
||||
it('toggle subtitles', async () => {
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
await waitForTranscriptionStatusChange(true);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().executeCommand('toggleSubtitles');
|
||||
|
||||
await waitForTranscriptionStatusChange(false);
|
||||
});
|
||||
|
||||
it('set subtitles on and off', async () => {
|
||||
// we need to clear results or the last one will be used, from the previous time subtitles were on
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', true, true);
|
||||
|
||||
await waitForTranscriptionStatusChange(true);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().executeCommand('setSubtitles', false);
|
||||
|
||||
await waitForTranscriptionStatusChange(false);
|
||||
});
|
||||
|
||||
it('start/stop transcriptions via recording', async () => {
|
||||
// we need to clear results or the last one will be used, from the previous time subtitles were on
|
||||
await p1.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await p2.getIframeAPI().clearEventResults('transcriptionChunkReceived');
|
||||
await clearTranscriptionStatusChange();
|
||||
webhooksProxy.clearCache();
|
||||
|
||||
await p1.getIframeAPI().executeCommand('startRecording', { transcription: true });
|
||||
|
||||
await waitForTranscriptionStatusChange(true);
|
||||
await checkReceivingChunks(p1, p2, webhooksProxy, asyncTranscriptions);
|
||||
|
||||
await clearTranscriptionStatusChange();
|
||||
await p1.getIframeAPI().executeCommand('stopRecording', 'file', true);
|
||||
|
||||
await waitForTranscriptionStatusChange(false);
|
||||
|
||||
await p1.getIframeAPI().executeCommand('hangup');
|
||||
await p2.getIframeAPI().executeCommand('hangup');
|
||||
|
||||
// sometimes events are not immediately received,
|
||||
// let's wait for destroy event before waiting for those that depends on it
|
||||
await webhooksProxy.waitForEvent('ROOM_DESTROYED');
|
||||
|
||||
if (!asyncTranscriptions || expectations.jaas.transcription.asyncTranscriptionWebhook) {
|
||||
const event: {
|
||||
data: {
|
||||
preAuthenticatedLink: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('TRANSCRIPTION_UPLOADED');
|
||||
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_UPLOADED');
|
||||
expect(event.data.preAuthenticatedLink).toBeDefined();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param p1
|
||||
* @param p2
|
||||
* @param webhooksProxy
|
||||
* @param asyncTranscription Whether async transciptions are used.
|
||||
*/
|
||||
async function checkReceivingChunks(p1: Participant, p2: Participant, webhooksProxy: WebhookProxy, asyncTranscription = false) {
|
||||
const p1Promise = p1.driver.waitUntil(() => p1.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p1 side'
|
||||
});
|
||||
|
||||
const p2Promise = p2.driver.waitUntil(() => p2.getIframeAPI()
|
||||
.getEventResult('transcriptionChunkReceived'), {
|
||||
timeout: 60000,
|
||||
timeoutMsg: 'transcriptionChunkReceived event not received on p2 side'
|
||||
});
|
||||
|
||||
const [ p1Event, p2Event ] = await Promise.all([ p1Promise, p2Promise ]);
|
||||
const p1Id = await p1.getEndpointId();
|
||||
|
||||
const p1Transcript = p1Event.data.stable || p1Event.data.final;
|
||||
const p2Transcript = p2Event.data.stable || p2Event.data.final;
|
||||
|
||||
expect(p2Transcript).toPartiallyMatch(p1Transcript, 'p2 transcript', 'p1 transcript');
|
||||
expect(p2Event.data.language).toBe(p1Event.data.language);
|
||||
expect(p2Event.data.messageID).toBe(p1Event.data.messageID);
|
||||
expect(p1Event.data.participant.id).toBe(p1Id);
|
||||
expect(p2Event.data.participant.id).toBe(p1Id);
|
||||
|
||||
if (!asyncTranscription) {
|
||||
expect(p1Event.data.participant.name).toBe(p1.name);
|
||||
expect(p2Event.data.participant.name).toBe(p1.name);
|
||||
}
|
||||
|
||||
if (!asyncTranscription || expectations.jaas.transcription.asyncTranscriptionWebhook) {
|
||||
// In async mode the backend re-IDs and re-segments chunks after post-processing, so live (iframe) and
|
||||
// async (webhook) ID spaces don't overlap and transcript text won't correlate. Match by participant only
|
||||
// and skip the content comparison.
|
||||
const event: {
|
||||
data: {
|
||||
final: string;
|
||||
language: string;
|
||||
messageID: string;
|
||||
participant: {
|
||||
id: string;
|
||||
name: string;
|
||||
};
|
||||
stable: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent(
|
||||
'TRANSCRIPTION_CHUNK_RECEIVED',
|
||||
asyncTranscription
|
||||
? e => e.data.participant.id === p1Id
|
||||
: e => e.data.messageID === p1Event.data.messageID
|
||||
);
|
||||
|
||||
expect(event.eventType).toBe('TRANSCRIPTION_CHUNK_RECEIVED');
|
||||
|
||||
if (!asyncTranscription) {
|
||||
const webhookTranscript = event.data.final;
|
||||
|
||||
expect(webhookTranscript).toPartiallyMatch(p1Transcript, 'webhook transcript', 'p1 transcript');
|
||||
}
|
||||
if (p1Event.data.language) {
|
||||
expect(event.data.language).toBe(p1Event.data.language);
|
||||
}
|
||||
expect(event.data.participant.id).toBe(p1Id);
|
||||
if (!asyncTranscription) {
|
||||
expect(event.data.participant.name).toBe(p1.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
49
tests/specs/jaas/visitors/participantsSoftLimit.spec.ts
Normal file
49
tests/specs/jaas/visitors/participantsSoftLimit.spec.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
useJaas: true,
|
||||
useWebhookProxy: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Visitors triggered by reaching participantsSoftLimit', () => {
|
||||
it('test participantsSoftLimit', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
participantsSoftLimit: 2,
|
||||
visitorsEnabled: true
|
||||
};
|
||||
|
||||
/// XXX the "name" of the participant MUST match one of the "capabilities" defined in wdio. It's not a "participant", it's an instance configuration!
|
||||
const m = await joinJaasMuc({
|
||||
token: t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
|
||||
});
|
||||
|
||||
expect(await m.isInMuc()).toBe(true);
|
||||
expect(await m.isModerator()).toBe(true);
|
||||
expect(await m.isVisitor()).toBe(false);
|
||||
console.log('Moderator joined');
|
||||
|
||||
// Joining with a participant token before participantSoftLimit has been reached
|
||||
const p = await joinJaasMuc({
|
||||
name: 'p2',
|
||||
token: t({ room: ctx.roomName, displayName: 'Parti Cipant' })
|
||||
});
|
||||
|
||||
expect(await p.isInMuc()).toBe(true);
|
||||
expect(await p.isModerator()).toBe(false);
|
||||
expect(await p.isVisitor()).toBe(false);
|
||||
console.log('Participant joined');
|
||||
|
||||
// Joining with a participant token after participantSoftLimit has been reached
|
||||
const v = await joinJaasMuc({
|
||||
name: 'p3',
|
||||
token: t({ room: ctx.roomName, displayName: 'Visi Tor' })
|
||||
});
|
||||
|
||||
expect(await v.isInMuc()).toBe(true);
|
||||
expect(await v.isModerator()).toBe(false);
|
||||
expect(await v.isVisitor()).toBe(true);
|
||||
console.log('Visitor joined');
|
||||
});
|
||||
});
|
||||
60
tests/specs/jaas/visitors/videoWithSingleSender.spec.ts
Normal file
60
tests/specs/jaas/visitors/videoWithSingleSender.spec.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3', 'p4' ]
|
||||
});
|
||||
|
||||
/**
|
||||
* This is a case which fails if jitsi-videobridge doesn't properly forward PLIs from visitors.
|
||||
*/
|
||||
describe('Visitor receiving video from a single remote participant', () => {
|
||||
it('joining the meeting', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
visitorsEnabled: true,
|
||||
visitorsLive: true,
|
||||
};
|
||||
|
||||
// Force a connection via JVB.
|
||||
const configOverwrite = {
|
||||
p2p: {
|
||||
enabled: false
|
||||
}
|
||||
};
|
||||
const sender = await joinJaasMuc({
|
||||
token: t({ room: ctx.roomName, displayName: 'Sender', moderator: true })
|
||||
}, {
|
||||
configOverwrite
|
||||
});
|
||||
const senderEndpointId = await sender.getEndpointId();
|
||||
|
||||
const testVisitor = async function(instanceId: 'p1' | 'p2' | 'p3' | 'p4') {
|
||||
const visitor = await joinJaasMuc({
|
||||
name: instanceId,
|
||||
token: t({ room: ctx.roomName, displayName: 'Visitor', visitor: true })
|
||||
}, {
|
||||
configOverwrite
|
||||
});
|
||||
|
||||
await visitor.waitForIceConnected();
|
||||
|
||||
const iceConnected = performance.now();
|
||||
|
||||
await visitor.driver.waitUntil(
|
||||
() => visitor.isRemoteVideoReceivedAndDisplayed(senderEndpointId), {
|
||||
timeout: 10_000,
|
||||
timeoutMsg: `Visitor (${instanceId}) is not receiving video from the sender`
|
||||
});
|
||||
|
||||
const duration = performance.now() - iceConnected;
|
||||
|
||||
console.log(`Video displayed after ${duration} ms after ICE connected (${instanceId})`);
|
||||
};
|
||||
|
||||
await testVisitor('p2');
|
||||
await testVisitor('p3');
|
||||
await testVisitor('p4');
|
||||
});
|
||||
});
|
||||
140
tests/specs/jaas/visitors/visitorTokens.spec.ts
Normal file
140
tests/specs/jaas/visitors/visitorTokens.spec.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import { Participant } from '../../../helpers/Participant';
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { config as testsConfig } from '../../../helpers/TestsConfig';
|
||||
import WebhookProxy from '../../../helpers/WebhookProxy';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2', 'p3' ]
|
||||
});
|
||||
|
||||
describe('Visitors triggered by visitor tokens', () => {
|
||||
let webhooksProxy: WebhookProxy;
|
||||
let room: string;
|
||||
|
||||
async function verifyJoinedWebhook(participant: Participant) {
|
||||
const context = participant.getToken()?.payload.context;
|
||||
const event: {
|
||||
customerId: string;
|
||||
data: {
|
||||
avatar: string;
|
||||
email: string;
|
||||
group: string;
|
||||
id: string;
|
||||
name: string;
|
||||
participantJid: string;
|
||||
role: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_JOINED');
|
||||
|
||||
expect('PARTICIPANT_JOINED').toBe(event.eventType);
|
||||
expect(event.data.avatar).toBe(context.user.avatar);
|
||||
expect(event.data.email).toBe(context.user.email);
|
||||
expect(event.data.id).toBe(context.user.id);
|
||||
expect(event.data.group).toBe(context.group);
|
||||
expect(event.data.name).toBe(context.user.name);
|
||||
if (context.user.visitor) {
|
||||
expect(event.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
|
||||
expect(event.data.role).toBe('visitor');
|
||||
}
|
||||
expect(event.customerId).toBe(testsConfig.jaas.customerId);
|
||||
}
|
||||
|
||||
async function verifyLeftWebhook(participant: Participant) {
|
||||
const context = participant.getToken()?.payload.context;
|
||||
const eventLeft: {
|
||||
customerId: string;
|
||||
data: {
|
||||
avatar: string;
|
||||
email: string;
|
||||
group: string;
|
||||
id: string;
|
||||
name: string;
|
||||
participantJid: string;
|
||||
role: string;
|
||||
};
|
||||
eventType: string;
|
||||
} = await webhooksProxy.waitForEvent('PARTICIPANT_LEFT');
|
||||
|
||||
expect('PARTICIPANT_LEFT').toBe(eventLeft.eventType);
|
||||
expect(eventLeft.data.avatar).toBe(context.user.avatar);
|
||||
expect(eventLeft.data.email).toBe(context.user.email);
|
||||
expect(eventLeft.data.id).toBe(context.user.id);
|
||||
expect(eventLeft.data.group).toBe(context.group);
|
||||
expect(eventLeft.data.name).toBe(context.user.name);
|
||||
if (context.user.visitor) {
|
||||
expect(eventLeft.data.participantJid.indexOf('meet.jitsi') != -1).toBe(true);
|
||||
expect(eventLeft.data.role).toBe('visitor');
|
||||
}
|
||||
expect(eventLeft.customerId).toBe(testsConfig.jaas.customerId);
|
||||
}
|
||||
|
||||
it('setup', async () => {
|
||||
webhooksProxy = ctx.webhooksProxy;
|
||||
webhooksProxy.defaultMeetingSettings = {
|
||||
visitorsEnabled: true
|
||||
};
|
||||
room = ctx.roomName;
|
||||
});
|
||||
|
||||
it('test visitor tokens', async () => {
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
const moderatorToken = t({ room, displayName: 'Mo de Rator', moderator: true });
|
||||
const moderator = await joinJaasMuc({ name: 'p1', token: moderatorToken });
|
||||
|
||||
expect(await moderator.isInMuc()).toBe(true);
|
||||
expect(await moderator.isModerator()).toBe(true);
|
||||
expect(await moderator.isVisitor()).toBe(false);
|
||||
await verifyJoinedWebhook(moderator);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a participant token before any visitors
|
||||
const participantToken = t({ room, displayName: 'Parti Cipant' });
|
||||
const participant = await joinJaasMuc({ name: 'p2', token: participantToken });
|
||||
|
||||
expect(await participant.isInMuc()).toBe(true);
|
||||
expect(await participant.isModerator()).toBe(false);
|
||||
expect(await participant.isVisitor()).toBe(false);
|
||||
await verifyJoinedWebhook(participant);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a visitor token
|
||||
const visitorToken = t({ room, displayName: 'Visi Tor', visitor: true });
|
||||
const visitor = await joinJaasMuc({ name: 'p3', token: visitorToken });
|
||||
|
||||
expect(await visitor.isInMuc()).toBe(true);
|
||||
expect(await visitor.isModerator()).toBe(false);
|
||||
expect(await visitor.isVisitor()).toBe(true);
|
||||
await verifyJoinedWebhook(visitor);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await participant.hangup();
|
||||
await verifyLeftWebhook(participant);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
// Joining with a participant token after visitors -> visitor
|
||||
const participantToken2 = t({ room, displayName: 'Visi Tor 2' });
|
||||
const visitor2 = await joinJaasMuc({ name: 'p2', token: participantToken2 });
|
||||
|
||||
expect(await visitor2.isInMuc()).toBe(true);
|
||||
expect(await visitor2.isModerator()).toBe(false);
|
||||
expect(await visitor2.isVisitor()).toBe(true);
|
||||
await verifyJoinedWebhook(visitor2);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await visitor.hangup();
|
||||
await verifyLeftWebhook(visitor);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await visitor2.hangup();
|
||||
await verifyLeftWebhook(visitor2);
|
||||
|
||||
webhooksProxy.clearCache();
|
||||
await moderator.hangup();
|
||||
await verifyLeftWebhook(moderator);
|
||||
});
|
||||
});
|
||||
73
tests/specs/jaas/visitors/visitorsLive.spec.ts
Normal file
73
tests/specs/jaas/visitors/visitorsLive.spec.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import { Participant } from '../../../helpers/Participant';
|
||||
import { setTestProperties } from '../../../helpers/TestProperties';
|
||||
import { expectations } from '../../../helpers/expectations';
|
||||
import { joinJaasMuc, generateJaasToken as t } from '../../../helpers/jaas';
|
||||
|
||||
setTestProperties(__filename, {
|
||||
requireWebhookProxy: true,
|
||||
useJaas: true,
|
||||
usesBrowsers: [ 'p1', 'p2' ]
|
||||
});
|
||||
|
||||
describe('Visitors live', () => {
|
||||
let visitor: Participant, moderator: Participant;
|
||||
|
||||
it('setup', async () => {
|
||||
ctx.webhooksProxy.defaultMeetingSettings = {
|
||||
visitorsEnabled: true,
|
||||
visitorsLive: false
|
||||
};
|
||||
|
||||
moderator = await joinJaasMuc({
|
||||
name: 'p1',
|
||||
token: t({ room: ctx.roomName, displayName: 'Mo de Rator', moderator: true })
|
||||
});
|
||||
|
||||
if (expectations.jaas.visitors) {
|
||||
await moderator.driver.waitUntil(() => moderator.execute(() => APP.conference._room.isVisitorsSupported()), {
|
||||
timeout: 2000
|
||||
}).catch(e => {
|
||||
throw new Error(`isVisitorsSupported() returned an error: ${e}.`);
|
||||
});
|
||||
}
|
||||
|
||||
visitor = await joinJaasMuc({
|
||||
name: 'p2',
|
||||
token: t({ room: ctx.roomName, displayName: 'Visi Tor', visitor: true })
|
||||
}, {
|
||||
skipWaitToJoin: true
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
it('go live', async () => {
|
||||
|
||||
const vVisitors = visitor.getVisitors();
|
||||
const mVisitors = moderator.getVisitors();
|
||||
|
||||
await visitor.driver.waitUntil(async () => vVisitors.isVisitorsQueueUIShown(), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Missing visitors queue UI'
|
||||
});
|
||||
|
||||
await moderator.driver.waitUntil(async () => await mVisitors.getWaitingVisitorsInQueue()
|
||||
=== 'Viewers waiting in queue: 1', {
|
||||
timeout: 15000,
|
||||
timeoutMsg: 'Missing visitors queue count in UI'
|
||||
});
|
||||
|
||||
await mVisitors.goLive();
|
||||
|
||||
await visitor.waitToJoinMUC();
|
||||
await visitor.waitForReceiveMedia(15000, 'Visitor is not receiving media');
|
||||
await visitor.waitForRemoteStreams(1);
|
||||
|
||||
await visitor.driver.waitUntil(() => vVisitors.hasVisitorsDialog(), {
|
||||
timeout: 5000,
|
||||
timeoutMsg: 'Missing visitors dialog'
|
||||
});
|
||||
|
||||
expect((await mVisitors.getVisitorsCount()).trim()).toBe('1');
|
||||
expect((await mVisitors.getVisitorsHeaderFromParticipantsPane()).trim()).toBe('Viewers 1');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user