snapshot of https://github.com/jitsi/jitsi-meet.git @ fd1749dd28f4138aa80234fc7f311b19aaffa0e7 (2026-09-15)
Some checks failed
Prosody Plugin Tests / test (push) Failing after 10m11s
Close stale issues and PRs / stale (push) Failing after 2s

This commit is contained in:
upstream-mirror
2026-09-15 15:56:32 +05:00
commit 1d2bfcac0c
3159 changed files with 464524 additions and 0 deletions

View File

@@ -0,0 +1,371 @@
import { setTestProperties } from '../../helpers/TestProperties';
import { config as testsConfig } from '../../helpers/TestsConfig';
import { joinMuc } from '../../helpers/joinMuc';
/**
* Joins (or rejoins) p1 as a moderator with the given config.js overrides, using the shared
* preconfigured token (see TestsConfig.ts) rather than a self-signed one. This suite only ever
* varies config.js, never JWT feature claims — on this (non-JaaS) tenant, getRecordButtonProps()
* and friends fall back to config alone once the JWT carries no feature claims, so the shared
* token is enough. The JWT-feature-driven side of this same dialog (only reachable on a JaaS
* tenant, where that config-only fallback doesn't apply) is covered separately by
* tests/specs/jaas/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 config.js combination.
*
* Waits for p1's moderator role to actually land client-side before returning: the server grants
* it (the JWT's moderator claim) on every join, but the toolbar's recording button renders the
* non-moderator "Record" label (instead of "Record & Transcribe") until that presence update is
* processed — a gap the rest of this file otherwise has no reason to guard against itself.
*/
async function joinWithConfig(configOverwrite: Record<string, unknown>): Promise<void> {
await joinMuc({
name: 'p1',
token: testsConfig.jwt.preconfiguredToken
}, { configOverwrite });
await ctx.p1.driver.waitUntil(() => ctx.p1.isModerator(), {
timeout: 5_000,
timeoutMsg: 'p1 did not become a moderator in time'
});
}
/**
* Mirrors LocalRecordingManager.isSupported() plus the embedding guard that
* supportsLocalRecording() adds — the same probe recordingButtonVisibility.spec.ts uses, because
* these browser capability gates are frequently unmet in the automated test browser regardless of
* the localRecording.disable config value.
*/
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;
});
}
/**
* Whether Dropbox is configured as a recording storage integration in this deployment
* (config.dropbox.appKey), independent of anything this file overrides. AbstractStartRecordingDialog
* treats it as a fallback recording service whenever recordingService.enabled is false (and prefers
* it over an empty selection, though local recording still wins over it when both are available) —
* so wherever "is some recording service available" is checked, Dropbox has to be accounted for too
* on any deployment where it happens to be configured, exactly like localRecordingSupport.byBrowser.
*/
async function isDropboxEnabledInDeployment(): Promise<boolean> {
return ctx.p1.execute(() => typeof config.dropbox?.appKey === 'string');
}
/**
* Fakes a "recording running" / "transcription running" state by dispatching the same low-level,
* side-effect-free redux actions the app's own middleware dispatches once a session is actually
* running, without going through the real start flow — which would either need a Jibri/Jigasi
* backend (cloud recording/transcription) or hit the browser's native showSaveFilePicker dialog
* (local recording), neither of which this environment can drive.
*
* - "recording running": SET_LOCAL_RECORDING_RUNNING only sets the reducer's localRecordingRunning
* flag (recording/middleware.ts has no side-effecting case for it, unlike START_LOCAL_RECORDING);
* isRecordingRunning() ORs it with cloud-recording state, so the dialog can't tell it apart from a
* real recording session.
* - "transcription running": isRecorderTranscriptionsRunning() requires BOTH
* features/transcribing.isTranscribing (set by TRANSCRIBER_JOINED/LEFT, a pure reducer
* transition — see transcribing/reducer.ts) AND the conference metadata's
* recording.isTranscribingEnabled flag (set via UPDATE_CONFERENCE_METADATA).
*/
async function setRunningState(recordingRunning: boolean, transcriptionRunning: boolean): Promise<void> {
await ctx.p1.execute((recording: boolean, transcription: boolean) => {
const { dispatch } = APP.store;
dispatch({ type: 'SET_LOCAL_RECORDING_RUNNING',
running: recording });
if (transcription) {
dispatch({ type: 'TRANSCRIBER_JOINED',
transcriberJID: 'fake-transcriber-for-testing' });
dispatch({
type: 'UPDATE_CONFERENCE_METADATA',
metadata: { recording: { isTranscribingEnabled: true } }
});
} else {
dispatch({ type: 'TRANSCRIBER_LEFT' });
dispatch({ type: 'UPDATE_CONFERENCE_METADATA',
metadata: null });
}
}, recordingRunning, transcriptionRunning);
}
setTestProperties(__filename, {
description: 'Recording & Transcription dialog rendering across config.js flag combinations',
usesBrowsers: [ 'p1' ]
});
/*
* Full 2^3 matrix over the three independently togglable config.js flags: recordingService.enabled,
* transcription.enabled and localRecording.disable. Expected behavior for each combination, derived
* from the source (AbstractStartRecordingDialog(Content), recording/functions.ts#getRecordButtonProps):
*
* - Every environment this suite runs against grants the local participant a default "recording"
* JWT feature claim regardless of authentication — confirmed via diagnostics logged below
* (localParticipantFeatures.recording is "true" even with no token/JWT at all). getRecordButtonProps()
* checks that claim *before* falling back to config-only visibility, so that fallback (which would
* let transcription.enabled alone drive visibility) is unreachable here: the toolbar button's
* visibility (and therefore whether the recording section can be inspected at all) reduces to
* recordingService.enabled || dropboxAvailable || localRecordingAvailable, same as
* recordingEnabled in getRecordButtonProps (which ORs in Dropbox too) — transcription.enabled never
* factors into it.
* - Whenever the dialog *can* open, the recording section renders — what changes is which storage
* service ends up selected by default: "Recording service" takes priority, then "Local recording",
* then "Dropbox" (see the constructor's own priority chain in AbstractStartRecordingDialog.ts).
* - The transcription section, its Start button, and the footer's "Start both" button are each
* governed purely by transcription.enabled (independent of the other two flags, since nothing is
* actually started in these tests) — but only reachable at all when the dialog can open in the
* first place, i.e. when some recording service is also available.
*
* The Live Streaming toolbar button is JWT-feature-gated with no config-only fallback (see
* LiveStream/AbstractLiveStreamButton.ts), so it can't be driven by config.js alone here — that's
* covered on the JaaS side instead, by tests/specs/jaas/recordingDialogConfig.spec.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, exactly
* like recordingButtonVisibility.spec.ts does, so the per-combination expectations stay accurate
* regardless of what the test browser actually supports. dropboxAvailable is similarly probed once,
* since it depends on this deployment's config rather than anything this file overrides.
*/
describe('Recording dialog config matrix — recording × transcription × local recording', () => {
// 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 properties (set
// once, in 'setup', before any of the loop's tests run) actually change.
const environment = { dropboxAvailable: false, localRecordingByBrowser: false };
it('setup', async () => {
await joinWithConfig({});
environment.localRecordingByBrowser = await isLocalRecordingSupportedByBrowser();
environment.dropboxAvailable = await isDropboxEnabledInDeployment();
});
const BOOLEANS = [ true, false ];
for (const recordingServiceEnabled of BOOLEANS) {
for (const transcriptionEnabled of BOOLEANS) {
for (const localRecordingDisabled of BOOLEANS) {
const title = `recordingService.enabled=${recordingServiceEnabled} `
+ `transcription.enabled=${transcriptionEnabled} `
+ `localRecording.disable=${localRecordingDisabled}`;
it(title, async () => {
await joinWithConfig({
recordingService: { enabled: recordingServiceEnabled },
transcription: { enabled: transcriptionEnabled },
localRecording: { disable: localRecordingDisabled }
});
const p1 = ctx.p1;
const localRecordingAvailable = !localRecordingDisabled && environment.localRecordingByBrowser;
const dialogCanOpen
= recordingServiceEnabled || localRecordingAvailable || environment.dropboxAvailable;
expect(await p1.getToolbar().hasRecordingButton()).toBe(dialogCanOpen);
if (!dialogCanOpen) {
// Nothing else is inspectable: the dialog cannot be opened at all.
return;
}
const dialog = p1.getRecordingTranscriptionDialog();
await p1.getToolbar().clickRecordingButton();
await dialog.waitForDisplay();
// The recording section always renders here (same condition as dialogCanOpen);
// which service ends up selected is what differs.
await dialog.toggleRecordingOptions();
const expectedService = recordingServiceEnabled ? 'Recording service'
: localRecordingAvailable ? 'Local recording'
: 'Dropbox';
expect(await dialog.getSelectedService()).toBe(expectedService);
expect(await dialog.hasTranscriptionOptions()).toBe(transcriptionEnabled);
expect(await dialog.hasStartTranscriptionButton()).toBe(transcriptionEnabled);
// The footer only makes sense when both sections are present; the recording
// section is always present on this branch, so this reduces to transcriptionEnabled.
expect(await dialog.hasStartBothButton()).toBe(transcriptionEnabled);
await dialog.cancel();
});
}
}
}
});
/*
* With both services enabled and available, the 2x2 matrix of which one is actually running.
* Expectations are read directly off web/StartRecordingDialogContent.tsx: each section swaps its
* own Start/Stop button independently, and the footer's _renderFooter() shows "Stop both" whenever
* ANY service is running and "Start both" whenever ANY service is NOT running — so in a mixed state
* (one running, one not) BOTH footer buttons render at once: "Stop both" stops the running one,
* "Start both" starts the missing one.
*/
describe('Recording dialog config — mixed running state (recording vs transcription)', () => {
const RUNNING_STATE_COMBINATIONS: Array<[boolean, boolean]> = [
[ false, false ],
[ true, false ],
[ false, true ],
[ true, true ]
];
for (const [ recordingRunning, transcriptionRunning ] of RUNNING_STATE_COMBINATIONS) {
it(`recording running=${recordingRunning}, transcription running=${transcriptionRunning}`, async () => {
await joinWithConfig({
recordingService: { enabled: true },
transcription: { enabled: true }
});
await setRunningState(recordingRunning, transcriptionRunning);
const dialog = ctx.p1.getRecordingTranscriptionDialog();
await ctx.p1.getToolbar().clickRecordingButton();
await dialog.waitForDisplay();
expect(await dialog.hasStartRecordingButton()).toBe(!recordingRunning);
expect(await dialog.hasStopRecordingButton()).toBe(recordingRunning);
expect(await dialog.hasStartTranscriptionButton()).toBe(!transcriptionRunning);
expect(await dialog.hasStopTranscriptionButton()).toBe(transcriptionRunning);
expect(await dialog.hasStopBothButton()).toBe(recordingRunning || transcriptionRunning);
expect(await dialog.hasStartBothButton()).toBe(!recordingRunning || !transcriptionRunning);
await dialog.cancel();
});
}
});
describe('Recording dialog config — recordingService.sharingEnabled', () => {
it('sharingEnabled: true shows the "share the recording link" switch', async () => {
await joinWithConfig({
recordingService: {
enabled: true,
sharingEnabled: true
}
});
const dialog = ctx.p1.getRecordingTranscriptionDialog();
await ctx.p1.getToolbar().clickRecordingButton();
await dialog.waitForDisplay();
await dialog.toggleRecordingOptions();
expect(await dialog.getSelectedService()).toBe('Recording service');
expect(await dialog.hasFileSharingSwitch()).toBe(true);
await dialog.cancel();
});
it('sharingEnabled: false hides the "share the recording link" switch', async () => {
await joinWithConfig({
recordingService: {
enabled: true,
sharingEnabled: false
}
});
const dialog = ctx.p1.getRecordingTranscriptionDialog();
await ctx.p1.getToolbar().clickRecordingButton();
await dialog.waitForDisplay();
await dialog.toggleRecordingOptions();
expect(await dialog.getSelectedService()).toBe('Recording service');
expect(await dialog.hasFileSharingSwitch()).toBe(false);
await dialog.cancel();
});
});
describe('Recording dialog config — localRecording.notifyAllParticipants', () => {
const NOTIFICATION_WARNING = 'will not be announced to other participants';
let localRecordingSupported: boolean;
it('setup', async () => {
await joinWithConfig({
localRecording: {
disable: false,
notifyAllParticipants: true
},
recordingService: { enabled: false }
});
localRecordingSupported = await isLocalRecordingSupportedByBrowser();
});
it('notifyAllParticipants: true — no "will not be announced" warning is shown', async () => {
if (!localRecordingSupported) {
return;
}
const dialog = ctx.p1.getRecordingTranscriptionDialog();
await ctx.p1.getToolbar().clickRecordingButton();
await dialog.waitForDisplay();
await dialog.toggleRecordingOptions();
expect(await dialog.getRecordingOptionsBodyText()).not.toContain(NOTIFICATION_WARNING);
await dialog.cancel();
});
it('notifyAllParticipants: false — the warning clears once "record only myself" is on', async () => {
if (!localRecordingSupported) {
return;
}
await joinWithConfig({
localRecording: {
disable: false,
notifyAllParticipants: false
},
recordingService: { enabled: false }
});
const dialog = ctx.p1.getRecordingTranscriptionDialog();
await ctx.p1.getToolbar().clickRecordingButton();
await dialog.waitForDisplay();
await dialog.toggleRecordingOptions();
expect(await dialog.getRecordingOptionsBodyText()).toContain(NOTIFICATION_WARNING);
await dialog.toggleLocalRecordingOnlySelfSwitch();
expect(await dialog.getRecordingOptionsBodyText()).not.toContain(NOTIFICATION_WARNING);
await dialog.cancel();
});
});