Files
element-call/src/lotus/lotusDeafen.test.ts
T

155 lines
4.6 KiB
TypeScript
Raw Normal View History

/*
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();
});