/* 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"; import { setScreenshareAudioMuted$ } from "./lotusScreenshareAudio"; 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", () => { const stop = startLotusDeafen(); 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)", () => { const stop = startLotusDeafen(); 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", () => { const stop = startLotusDeafen(); // 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]); }); test("deafen never touches the screenshare-audio flag", () => { const stop = startLotusDeafen(); send({ deafened: true, screenshareAudioMuted: false }); send({ deafened: false, screenshareAudioMuted: false }); expect(setScreenshareAudioMuted$.value).toBe(false); stop(); }); 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(); // Not muted yet: nothing pushed beyond the BehaviorSubject's initial value. send({ deafened: false, screenshareAudioMuted: false }); expect(seen).toEqual([false]); send({ deafened: false, screenshareAudioMuted: true }); 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]); // 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); // Leaving the call clears it so the next call starts clean. stop(); expect(setScreenshareAudioMuted$.value).toBe(false); flagSub.unsubscribe(); }); test("a partial payload only moves the flag it names", () => { const stop = startLotusDeafen(); send({ deafened: true }); expect(emissions).toEqual([false]); send({ screenshareAudioMuted: true }); // Still deafened: no change to the global mute. expect(emissions).toEqual([false]); stop(); });