/* 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 { of } from "rxjs"; import { Track } from "livekit-client"; import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { setAudioEnabled$ } from "../controls"; import { startLotusDeafen } from "./lotusDeafen"; import { LotusWidgetActions } from "./lotusActions"; 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; }, }, })); /** Minimal CallViewModel stub: no connections, so no livekit rooms. */ function mockVm(participants: unknown[] = []): CallViewModel { const livekitRoom = { remoteParticipants: new Map(participants.map((p, i) => [String(i), p])), on: vi.fn(), off: vi.fn(), }; return { allConnections$: of({ getConnections: () => [{ livekitRoom }] }), } as unknown as CallViewModel; } 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(mockVm()); 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(mockVm()); 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(mockVm()); // 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 per-participant volume", () => { const participant = { setVolume: vi.fn() }; const stop = startLotusDeafen(mockVm([participant])); send({ deafened: true, screenshareAudioMuted: false }); send({ deafened: false, screenshareAudioMuted: false }); expect(participant.setVolume).not.toHaveBeenCalled(); stop(); }); test("screenshare-audio mute is applied per source and only undone for participants we muted", () => { const participant = { setVolume: vi.fn() }; const stop = startLotusDeafen(mockVm([participant])); // Not muted yet: no volume writes at all. send({ deafened: false, screenshareAudioMuted: false }); expect(participant.setVolume).not.toHaveBeenCalled(); send({ deafened: false, screenshareAudioMuted: true }); expect(participant.setVolume).toHaveBeenCalledWith( 0, Track.Source.ScreenShareAudio, ); participant.setVolume.mockClear(); send({ deafened: false, screenshareAudioMuted: false }); expect(participant.setVolume).toHaveBeenCalledWith( 1, Track.Source.ScreenShareAudio, ); // Releasing again writes nothing: we no longer own that participant. participant.setVolume.mockClear(); send({ deafened: false, screenshareAudioMuted: false }); expect(participant.setVolume).not.toHaveBeenCalled(); stop(); }); test("a partial payload only moves the flag it names", () => { const stop = startLotusDeafen(mockVm()); send({ deafened: true }); expect(emissions).toEqual([false]); send({ screenshareAudioMuted: true }); // Still deafened: no change to the global mute. expect(emissions).toEqual([false]); stop(); });