337 lines
11 KiB
TypeScript
337 lines
11 KiB
TypeScript
import { setTestProperties } from '../../helpers/TestProperties';
|
|
import { ensureOneParticipant, hangupAllParticipants } from '../../helpers/participants';
|
|
|
|
setTestProperties(__filename, {
|
|
usesBrowsers: [ 'p1' ]
|
|
});
|
|
|
|
const V2_CONFIG = {
|
|
disableVirtualBackground: false,
|
|
virtualBackground: {
|
|
enableV2: true
|
|
}
|
|
};
|
|
|
|
/**
|
|
* Reads the current virtual-background slice of the Redux store.
|
|
*/
|
|
async function getVBState() {
|
|
return ctx.p1.execute(() => APP.store.getState()['features/virtual-background']);
|
|
}
|
|
|
|
/**
|
|
* Reads the virtualBackground config from the Redux store.
|
|
*/
|
|
async function getVBConfig() {
|
|
return ctx.p1.execute(
|
|
() => APP.store.getState()['features/base/config'].virtualBackground);
|
|
}
|
|
|
|
/**
|
|
* Opens the virtual background dialog via the toolbar button and waits for it to be displayed.
|
|
*/
|
|
async function openVBDialog() {
|
|
await ctx.p1.getToolbar().clickVirtualBackgroundButton();
|
|
await ctx.p1.getVirtualBackgroundDialog().waitForDisplay();
|
|
}
|
|
|
|
/**
|
|
* Reads the V2 effect's processor state from the local video JitsiLocalTrack.
|
|
* Returns `{ isReady, frameCount }` or `null` when the effect or processor is absent.
|
|
*
|
|
* Used to test the mute/unmute fix directly without depending on pixel rendering of the
|
|
* stage or filmstrip thumbnail — both of which are unreliable in headless Chrome.
|
|
*/
|
|
async function getV2ProcessorState(): Promise<{ frameCount: number; isReady: boolean; } | null> {
|
|
return ctx.p1.execute(() => {
|
|
const tracks = APP.store.getState()['features/base/tracks'];
|
|
const localVideo = tracks.find((t: any) => t.local && t.mediaType === 'video');
|
|
const processor = (localVideo?.jitsiTrack as any)?._streamEffect?._processor;
|
|
|
|
if (!processor) {
|
|
return null;
|
|
}
|
|
|
|
return {
|
|
frameCount: processor._frameCount ?? 0,
|
|
isReady: Boolean(processor._isReady)
|
|
};
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Waits until the V2 effect's inference worker has finished initialising. Redux flips
|
|
* backgroundEffectEnabled to true the moment setEffect dispatches — well before the worker
|
|
* has loaded the segmentation model. Tests that immediately mute/unmute after waiting only
|
|
* on the Redux flag race against the worker init, intermittently failing on slower runners.
|
|
*
|
|
* @param {number} timeout - How long to wait in ms.
|
|
*/
|
|
async function waitForV2EffectReady(timeout = 15000): Promise<void> {
|
|
await ctx.p1.driver.waitUntil(
|
|
async () => Boolean((await getV2ProcessorState())?.isReady),
|
|
{ timeout, timeoutMsg: `V2 effect processor not ready within ${timeout}ms` }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Waits until backgroundEffectEnabled settles to the expected value.
|
|
* When expecting true, the value must remain stable for stabilityWindow ms to confirm the
|
|
* async setEffect() did not roll back.
|
|
*
|
|
* @param {boolean} expected - The expected settled value.
|
|
* @param {number} timeout - How long to wait in ms.
|
|
* @param {number} stabilityWindow - How long the expected value must remain unchanged (ms).
|
|
*/
|
|
async function waitForEffectEnabled(expected: boolean, timeout = 15000, stabilityWindow = 500): Promise<void> {
|
|
let matchedAt: number | undefined;
|
|
const requiredStableMs = expected ? stabilityWindow : 0;
|
|
|
|
await ctx.p1.driver.waitUntil(
|
|
async () => {
|
|
const state = await getVBState();
|
|
|
|
if (state.backgroundEffectEnabled !== expected) {
|
|
matchedAt = undefined;
|
|
|
|
return false;
|
|
}
|
|
|
|
if (requiredStableMs === 0) {
|
|
return true;
|
|
}
|
|
|
|
const now = Date.now();
|
|
|
|
if (matchedAt === undefined) {
|
|
matchedAt = now;
|
|
|
|
return false;
|
|
}
|
|
|
|
return now - matchedAt >= requiredStableMs;
|
|
},
|
|
{ timeout, timeoutMsg: `backgroundEffectEnabled did not settle to ${expected} within ${timeout}ms` }
|
|
);
|
|
}
|
|
|
|
describe('Virtual backgrounds V2 engine', () => {
|
|
it('joining the meeting with V2 enabled', async () => {
|
|
await ensureOneParticipant({
|
|
configOverwrite: V2_CONFIG
|
|
});
|
|
|
|
// Verify V2 config is active in the Redux store.
|
|
const vbConfig = await getVBConfig();
|
|
|
|
expect(vbConfig?.enableV2).toBe(true);
|
|
});
|
|
|
|
it('select blur with V2 engine — effect enables and redux state correct', async () => {
|
|
await openVBDialog();
|
|
const vbDialog = ctx.p1.getVirtualBackgroundDialog();
|
|
|
|
await vbDialog.clickBlur();
|
|
expect(await vbDialog.isBlurChecked()).toBe(true);
|
|
|
|
await vbDialog.confirm();
|
|
await waitForEffectEnabled(true);
|
|
|
|
// Worker model load is async; the next test mute/unmutes immediately after, so wait
|
|
// for the processor to actually be ready before letting the suite proceed.
|
|
await waitForV2EffectReady();
|
|
|
|
const state = await getVBState();
|
|
|
|
expect(state.backgroundEffectEnabled).toBe(true);
|
|
expect(state.backgroundType).toBe('blur');
|
|
expect(state.blurValue).toBe(25);
|
|
expect(state.selectedThumbnail).toBe('blur');
|
|
});
|
|
|
|
it('mute and unmute camera while blur is active — processor stays ready across cycle', async () => {
|
|
// Blur was enabled by the prior test (which also waited for the processor to be ready).
|
|
let state = await getVBState();
|
|
|
|
expect(state.backgroundEffectEnabled).toBe(true);
|
|
expect(state.backgroundType).toBe('blur');
|
|
|
|
const baseline = await getV2ProcessorState();
|
|
|
|
expect(baseline).not.toBeNull();
|
|
expect(baseline!.isReady).toBe(true);
|
|
|
|
// Mute then unmute. Pre-fix this disposed the processor (_isReady -> false) and unmute
|
|
// had to reload the model (~1-2s). Post-fix the processor stays alive throughout.
|
|
// Hold mute for 2s so that the mute fully settles before unmuting.
|
|
await ctx.p1.getToolbar().clickVideoMuteButton();
|
|
await ctx.p1.driver.pause(2000);
|
|
await ctx.p1.getToolbar().clickVideoUnmuteButton();
|
|
await ctx.p1.driver.pause(1000);
|
|
|
|
// Immediately after unmute the processor must still be ready (no dispose on mute).
|
|
// If the dispose-on-stopEffect regression returned, _isReady would be false for the
|
|
// ~1-2s it takes to reload the worker model.
|
|
const afterUnmute = await getV2ProcessorState();
|
|
|
|
expect(afterUnmute).not.toBeNull();
|
|
expect(afterUnmute!.isReady).toBe(true);
|
|
|
|
// Effect state should remain consistent.
|
|
state = await getVBState();
|
|
expect(state.backgroundEffectEnabled).toBe(true);
|
|
expect(state.backgroundType).toBe('blur');
|
|
});
|
|
|
|
it('select slight blur with V2 engine — redux state correct', async () => {
|
|
await openVBDialog();
|
|
const vbDialog = ctx.p1.getVirtualBackgroundDialog();
|
|
|
|
await vbDialog.clickSlightBlur();
|
|
expect(await vbDialog.isSlightBlurChecked()).toBe(true);
|
|
|
|
await vbDialog.confirm();
|
|
await waitForEffectEnabled(true);
|
|
|
|
const state = await getVBState();
|
|
|
|
expect(state.backgroundEffectEnabled).toBe(true);
|
|
expect(state.backgroundType).toBe('blur');
|
|
expect(state.blurValue).toBe(8);
|
|
expect(state.selectedThumbnail).toBe('slight-blur');
|
|
});
|
|
|
|
it('disable V2 background — redux state resets', async () => {
|
|
await openVBDialog();
|
|
const vbDialog = ctx.p1.getVirtualBackgroundDialog();
|
|
|
|
await vbDialog.clickNone();
|
|
await vbDialog.confirm();
|
|
await waitForEffectEnabled(false);
|
|
|
|
const state = await getVBState();
|
|
|
|
expect(state.backgroundEffectEnabled).toBe(false);
|
|
expect(state.selectedThumbnail).toBe('none');
|
|
});
|
|
});
|
|
|
|
describe('Virtual backgrounds V2 with tier override', () => {
|
|
it('hangup and rejoin with tier override config', async () => {
|
|
await hangupAllParticipants();
|
|
});
|
|
|
|
it('joining with V2 + low tier override', async () => {
|
|
await ensureOneParticipant({
|
|
configOverwrite: {
|
|
...V2_CONFIG,
|
|
virtualBackground: {
|
|
...V2_CONFIG.virtualBackground,
|
|
advanced: {
|
|
tierOverride: 'low'
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const vbConfig = await getVBConfig();
|
|
|
|
expect(vbConfig?.enableV2).toBe(true);
|
|
expect(vbConfig?.advanced?.tierOverride).toBe('low');
|
|
});
|
|
|
|
it('blur works on low tier', async () => {
|
|
await openVBDialog();
|
|
const vbDialog = ctx.p1.getVirtualBackgroundDialog();
|
|
|
|
await vbDialog.clickBlur();
|
|
await vbDialog.confirm();
|
|
await waitForEffectEnabled(true);
|
|
|
|
const state = await getVBState();
|
|
|
|
expect(state.backgroundEffectEnabled).toBe(true);
|
|
expect(state.backgroundType).toBe('blur');
|
|
expect(state.blurValue).toBe(25);
|
|
});
|
|
|
|
it('disable effect on low tier — clean shutdown', async () => {
|
|
await openVBDialog();
|
|
const vbDialog = ctx.p1.getVirtualBackgroundDialog();
|
|
|
|
await vbDialog.clickNone();
|
|
await vbDialog.confirm();
|
|
await waitForEffectEnabled(false);
|
|
|
|
const state = await getVBState();
|
|
|
|
expect(state.backgroundEffectEnabled).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('Virtual backgrounds V2 with insertable streams disabled', () => {
|
|
it('hangup and rejoin with captureStream config', async () => {
|
|
await hangupAllParticipants();
|
|
});
|
|
|
|
it('joining with V2 + captureStream fallback', async () => {
|
|
await ensureOneParticipant({
|
|
configOverwrite: {
|
|
...V2_CONFIG,
|
|
virtualBackground: {
|
|
...V2_CONFIG.virtualBackground,
|
|
advanced: {
|
|
useInsertableStreams: false
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
const vbConfig = await getVBConfig();
|
|
|
|
expect(vbConfig?.enableV2).toBe(true);
|
|
expect(vbConfig?.advanced?.useInsertableStreams).toBe(false);
|
|
});
|
|
|
|
it('blur works on captureStream path', async () => {
|
|
await openVBDialog();
|
|
const vbDialog = ctx.p1.getVirtualBackgroundDialog();
|
|
|
|
await vbDialog.clickBlur();
|
|
await vbDialog.confirm();
|
|
await waitForEffectEnabled(true);
|
|
|
|
const state = await getVBState();
|
|
|
|
expect(state.backgroundEffectEnabled).toBe(true);
|
|
expect(state.backgroundType).toBe('blur');
|
|
expect(state.blurValue).toBe(25);
|
|
});
|
|
|
|
it('disable and re-enable on captureStream path', async () => {
|
|
// Disable.
|
|
await openVBDialog();
|
|
await ctx.p1.getVirtualBackgroundDialog().clickNone();
|
|
await ctx.p1.getVirtualBackgroundDialog().confirm();
|
|
await waitForEffectEnabled(false);
|
|
|
|
// Re-enable with slight blur.
|
|
await openVBDialog();
|
|
await ctx.p1.getVirtualBackgroundDialog().clickSlightBlur();
|
|
await ctx.p1.getVirtualBackgroundDialog().confirm();
|
|
await waitForEffectEnabled(true);
|
|
|
|
const state = await getVBState();
|
|
|
|
expect(state.backgroundEffectEnabled).toBe(true);
|
|
expect(state.backgroundType).toBe('blur');
|
|
expect(state.blurValue).toBe(8);
|
|
|
|
// Clean up.
|
|
await openVBDialog();
|
|
await ctx.p1.getVirtualBackgroundDialog().clickNone();
|
|
await ctx.p1.getVirtualBackgroundDialog().confirm();
|
|
await waitForEffectEnabled(false);
|
|
});
|
|
});
|