2026-09-12 11:34:08 -04:00
|
|
|
/*
|
|
|
|
|
Copyright 2026 Lotus Guild
|
|
|
|
|
|
|
|
|
|
SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Element-Commercial
|
|
|
|
|
Please see LICENSE in the repository root for full details.
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { EventEmitter } from "events";
|
|
|
|
|
import { afterEach, beforeEach, expect, test, vi } from "vitest";
|
|
|
|
|
|
|
|
|
|
import { setAudioEnabled$ } from "../controls";
|
|
|
|
|
import { startLotusDeafen } from "./lotusDeafen";
|
|
|
|
|
import { LotusWidgetActions } from "./lotusActions";
|
2026-09-18 23:54:25 -04:00
|
|
|
import { setScreenshareAudioMuted$ } from "./lotusScreenshareAudio";
|
2026-09-12 11:34:08 -04:00
|
|
|
|
|
|
|
|
const lazyActions = new EventEmitter();
|
|
|
|
|
|
|
|
|
|
vi.mock("../widget", () => ({
|
|
|
|
|
widget: {
|
|
|
|
|
api: { transport: { reply: vi.fn().mockResolvedValue(undefined) } },
|
|
|
|
|
// Getter: `vi.mock` factories run at import time, before the const above.
|
|
|
|
|
get lazyActions(): EventEmitter {
|
|
|
|
|
return lazyActions;
|
|
|
|
|
},
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
function send(data: unknown): void {
|
|
|
|
|
lazyActions.emit(LotusWidgetActions.SetDeafen, {
|
|
|
|
|
detail: { data },
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Records everything pushed to the global audio-output subject. */
|
|
|
|
|
let emissions: boolean[];
|
|
|
|
|
let sub: { unsubscribe: () => void };
|
|
|
|
|
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
emissions = [];
|
|
|
|
|
sub = setAudioEnabled$.subscribe((v) => emissions.push(v));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
sub.unsubscribe();
|
|
|
|
|
lazyActions.removeAllListeners();
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("deafen mutes, and undeafen unmutes, EC's global audio output", () => {
|
2026-09-18 23:54:25 -04:00
|
|
|
const stop = startLotusDeafen();
|
2026-09-12 11:34:08 -04:00
|
|
|
|
|
|
|
|
send({ deafened: true, screenshareAudioMuted: false });
|
|
|
|
|
expect(emissions).toEqual([false]);
|
|
|
|
|
|
|
|
|
|
send({ deafened: false, screenshareAudioMuted: false });
|
|
|
|
|
expect(emissions).toEqual([false, true]);
|
|
|
|
|
|
|
|
|
|
stop();
|
|
|
|
|
expect(emissions).toEqual([false, true]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("re-sending the same state is idempotent (host resendForkState)", () => {
|
2026-09-18 23:54:25 -04:00
|
|
|
const stop = startLotusDeafen();
|
2026-09-12 11:34:08 -04:00
|
|
|
|
|
|
|
|
send({ deafened: true, screenshareAudioMuted: false });
|
|
|
|
|
send({ deafened: true, screenshareAudioMuted: false });
|
|
|
|
|
send({ deafened: true, screenshareAudioMuted: false });
|
|
|
|
|
expect(emissions).toEqual([false]);
|
|
|
|
|
|
|
|
|
|
stop();
|
|
|
|
|
// Teardown restores the user's own state, which was "audio enabled".
|
|
|
|
|
expect(emissions).toEqual([false, true]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("undeafen does not re-enable audio the user had muted themselves", () => {
|
2026-09-18 23:54:25 -04:00
|
|
|
const stop = startLotusDeafen();
|
2026-09-12 11:34:08 -04:00
|
|
|
|
|
|
|
|
// The user mutes all audio through EC's own control first.
|
|
|
|
|
setAudioEnabled$.next(false);
|
|
|
|
|
expect(emissions).toEqual([false]);
|
|
|
|
|
|
|
|
|
|
send({ deafened: true, screenshareAudioMuted: false });
|
|
|
|
|
send({ deafened: false, screenshareAudioMuted: false });
|
|
|
|
|
// Nothing further was pushed: audio was already off and stays off.
|
|
|
|
|
expect(emissions).toEqual([false]);
|
|
|
|
|
|
|
|
|
|
stop();
|
|
|
|
|
expect(emissions).toEqual([false]);
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-18 23:54:25 -04:00
|
|
|
test("deafen never touches the screenshare-audio flag", () => {
|
|
|
|
|
const stop = startLotusDeafen();
|
2026-09-12 11:34:08 -04:00
|
|
|
|
|
|
|
|
send({ deafened: true, screenshareAudioMuted: false });
|
|
|
|
|
send({ deafened: false, screenshareAudioMuted: false });
|
2026-09-18 23:54:25 -04:00
|
|
|
expect(setScreenshareAudioMuted$.value).toBe(false);
|
2026-09-12 11:34:08 -04:00
|
|
|
|
|
|
|
|
stop();
|
|
|
|
|
});
|
|
|
|
|
|
2026-09-18 23:54:25 -04:00
|
|
|
test("screenshare-audio mute is a global flag the renderer reads, reset on teardown", () => {
|
|
|
|
|
const seen: boolean[] = [];
|
|
|
|
|
const flagSub = setScreenshareAudioMuted$.subscribe((v) => seen.push(v));
|
|
|
|
|
const stop = startLotusDeafen();
|
2026-09-12 11:34:08 -04:00
|
|
|
|
2026-09-18 23:54:25 -04:00
|
|
|
// Not muted yet: nothing pushed beyond the BehaviorSubject's initial value.
|
2026-09-12 11:34:08 -04:00
|
|
|
send({ deafened: false, screenshareAudioMuted: false });
|
2026-09-18 23:54:25 -04:00
|
|
|
expect(seen).toEqual([false]);
|
2026-09-12 11:34:08 -04:00
|
|
|
|
|
|
|
|
send({ deafened: false, screenshareAudioMuted: true });
|
2026-09-18 23:54:25 -04:00
|
|
|
expect(setScreenshareAudioMuted$.value).toBe(true);
|
|
|
|
|
// Re-applying the same state (host resend after reconnect) is a no-op.
|
|
|
|
|
send({ deafened: false, screenshareAudioMuted: true });
|
|
|
|
|
expect(seen).toEqual([false, true]);
|
2026-09-12 11:34:08 -04:00
|
|
|
|
2026-09-18 23:54:25 -04:00
|
|
|
// Deafen + undeafen while screenshare audio is muted leaves it muted.
|
|
|
|
|
send({ deafened: true, screenshareAudioMuted: true });
|
|
|
|
|
send({ deafened: false, screenshareAudioMuted: true });
|
|
|
|
|
expect(setScreenshareAudioMuted$.value).toBe(true);
|
2026-09-12 11:34:08 -04:00
|
|
|
|
2026-09-18 23:54:25 -04:00
|
|
|
// Leaving the call clears it so the next call starts clean.
|
2026-09-12 11:34:08 -04:00
|
|
|
stop();
|
2026-09-18 23:54:25 -04:00
|
|
|
expect(setScreenshareAudioMuted$.value).toBe(false);
|
|
|
|
|
flagSub.unsubscribe();
|
2026-09-12 11:34:08 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
test("a partial payload only moves the flag it names", () => {
|
2026-09-18 23:54:25 -04:00
|
|
|
const stop = startLotusDeafen();
|
2026-09-12 11:34:08 -04:00
|
|
|
|
|
|
|
|
send({ deafened: true });
|
|
|
|
|
expect(emissions).toEqual([false]);
|
|
|
|
|
send({ screenshareAudioMuted: true });
|
|
|
|
|
// Still deafened: no change to the global mute.
|
|
|
|
|
expect(emissions).toEqual([false]);
|
|
|
|
|
|
|
|
|
|
stop();
|
|
|
|
|
});
|