Files
OwnCord/Client/tests/unit/audio-elements.test.ts
J3vbandClaude 7365a31b45 refactor: flatten Client/tauri-client into Client (B1-1) (#1411)
* refactor: move Client/tauri-client to Client (pure move, no content change)

* refactor: re-point paths after the Client flatten (mechanical, no behaviour change)

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-08-26 08:55:06 +02:00

734 lines
26 KiB
TypeScript

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
const { mockLoadPref, mockSavePref } = vi.hoisted(() => ({
mockLoadPref: vi.fn((_key: string, defaultVal: unknown) => defaultVal),
mockSavePref: vi.fn(),
}));
vi.mock("@components/settings/helpers", () => ({
loadPref: (key: string, defaultVal: unknown) => mockLoadPref(key, defaultVal),
savePref: (key: string, val: unknown) => mockSavePref(key, val),
}));
vi.mock("@lib/logger", () => ({
createLogger: () => ({
debug: vi.fn(),
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
}),
}));
vi.mock("livekit-client", () => ({
Track: {
Source: {
Microphone: "microphone",
Camera: "camera",
ScreenShare: "screenShare",
ScreenShareAudio: "screenShareAudio",
},
},
}));
const mockVoiceStoreState = {
localDeafened: false,
currentChannelId: null as number | null,
};
vi.mock("@stores/voice.store", () => ({
voiceStore: {
getState: () => mockVoiceStoreState,
},
}));
vi.mock("@lib/livekitSession", () => ({
parseUserId: (identity: string) => {
const match = identity.match(/^user-(\d+)$/);
if (match !== null && match[1] !== undefined) return parseInt(match[1], 10);
return 0;
},
}));
import { AudioElements, setAudioVolumeHost } from "../../src/lib/audioElements";
function createMockTrack(kind: string, sid: string) {
const audioEl = document.createElement("audio");
// The track passed to handleTrackSubscribedAudio is a RemoteTrack directly
// (not wrapped in a publication), so detach/attach are on the top-level object
const track = {
kind,
sid,
detach: vi.fn(() => [audioEl]),
attach: vi.fn(() => audioEl),
};
return { track, audioEl };
}
describe("AudioElements", () => {
let elements: AudioElements;
beforeEach(() => {
vi.clearAllMocks();
mockVoiceStoreState.localDeafened = false;
mockVoiceStoreState.currentChannelId = null;
elements = new AudioElements();
});
describe("initial state", () => {
it("has default output volume multiplier of 1.0", () => {
expect(elements.getOutputVolumeMultiplier()).toBe(1.0);
});
it("returns effective volume with default multiplier", () => {
expect(elements.getEffectiveVolume(1)).toBe(1.0);
});
});
describe("setRoom", () => {
it("accepts null without throwing", () => {
expect(() => elements.setRoom(null)).not.toThrow();
});
});
describe("handleTrackSubscribedAudio — mic", () => {
it("attaches mic audio element to DOM", () => {
const { track, audioEl } = createMockTrack("audio", "track-1");
const publication = { source: "microphone" };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
expect(track.attach).toHaveBeenCalled();
expect(audioEl.style.display).toBe("none");
expect(participant.setVolume).toHaveBeenCalledWith(1.0);
});
});
describe("handleTrackSubscribedAudio — screenshare", () => {
it("attaches screenshare audio element to DOM", () => {
const { track, audioEl } = createMockTrack("audio", "track-ss-1");
const publication = { source: "screenShareAudio" };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
expect(track.attach).toHaveBeenCalled();
expect(audioEl.volume).toBe(1);
});
it("inherits muted state from previous tracks", () => {
elements.muteScreenshareAudio(42, true);
const { track, audioEl } = createMockTrack("audio", "track-ss-2");
const publication = { source: "screenShareAudio" };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
expect(audioEl.muted).toBe(true);
});
it("does not leak the previously-attached element into the tracking set on a fast re-subscribe (OC-0135)", () => {
// LiveKit can fire TrackSubscribed for an already-attached screenshare-
// audio track before the old TrackUnsubscribed lands (fast reconnect).
// The same underlying track's detach() then returns the element from
// the prior attach(), which the handler removes from the DOM but must
// also drop from screenshareAudioElements — otherwise it lives on in
// the Set forever.
let lastEl: HTMLAudioElement | null = null;
const track = {
kind: "audio",
sid: "track-ss-resub",
attach: vi.fn(() => {
const el = document.createElement("audio");
lastEl = el;
return el;
}),
detach: vi.fn(() => (lastEl === null ? [] : [lastEl])),
};
const publication = { source: "screenShareAudio" };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
const firstEl = lastEl as unknown as HTMLAudioElement;
// Re-fire subscribe for the same track before any unsubscribe arrives.
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
const secondEl = lastEl as unknown as HTMLAudioElement;
const trackedEls = (elements as any).screenshareAudioElements.get(
42,
) as Set<HTMLAudioElement>;
expect(trackedEls.has(firstEl)).toBe(false);
expect(trackedEls.has(secondEl)).toBe(true);
expect(trackedEls.size).toBe(1);
});
});
describe("handleTrackUnsubscribedAudio", () => {
it("removes mic audio element from tracking", () => {
const { track } = createMockTrack("audio", "track-1");
const publication = { source: "microphone" };
const participant = { identity: "user-42", setVolume: vi.fn() };
// Subscribe first
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
// Then unsubscribe
elements.handleTrackUnsubscribedAudio(track as any, publication as any, participant as any);
expect(track.detach).toHaveBeenCalled();
});
});
describe("setUserVolume", () => {
it("saves clamped volume to preferences", () => {
elements.setUserVolume(42, 150);
expect(mockSavePref).toHaveBeenCalledWith("userVolume_42", 150);
});
it("clamps to 0-200 range", () => {
elements.setUserVolume(42, -10);
expect(mockSavePref).toHaveBeenCalledWith("userVolume_42", 0);
elements.setUserVolume(42, 250);
expect(mockSavePref).toHaveBeenCalledWith("userVolume_42", 200);
});
it("applies volume to matching remote participant", () => {
const mockParticipant = {
identity: "user-42",
setVolume: vi.fn(),
};
const mockRoom = {
remoteParticipants: new Map([["user-42", mockParticipant]]),
} as any;
elements.setRoom(mockRoom);
elements.setUserVolume(42, 80);
expect(mockParticipant.setVolume).toHaveBeenCalledWith(0.8);
});
});
describe("getUserVolume", () => {
it("returns default volume of 100", () => {
expect(elements.getUserVolume(42)).toBe(100);
});
});
describe("per-user volume — host scoping (B3-6)", () => {
afterEach(() => {
// currentHost is module-level state that outlives a single test.
setAudioVolumeHost(null);
});
it("falls back to the legacy unscoped key when no host is set", () => {
elements.setUserVolume(42, 150);
expect(mockSavePref).toHaveBeenCalledWith("userVolume_42", 150);
});
it("scopes the saved-volume key to the current host", () => {
setAudioVolumeHost("a.example.com");
elements.setUserVolume(42, 150);
expect(mockSavePref).toHaveBeenCalledWith("userVolume_42:a.example.com", 150);
});
it("reads the volume back under the scoped key, not the unscoped one", () => {
setAudioVolumeHost("a.example.com");
elements.getUserVolume(42);
expect(mockLoadPref).toHaveBeenCalledWith("userVolume_42:a.example.com", 100);
});
it("does not leak a volume set on one host onto another", () => {
// Regression for B3-6: a mute (volume 0) set for user 7 on server A
// must not silence user 7 on server B.
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
if (key === "userVolume_7:a.example.com") return 0;
return defaultVal;
});
setAudioVolumeHost("a.example.com");
expect(elements.getUserVolume(7)).toBe(0);
setAudioVolumeHost("b.example.com");
expect(elements.getUserVolume(7)).toBe(100);
});
it("migrates a pre-scoping legacy volume through to the scoped key without leaking to a different host", () => {
// A save made before host-scoping existed lives at the bare
// `userVolume_7` key. It must still apply once a host is set, and the
// migration must write under THAT host's scoped key specifically —
// not clobber a different host's own explicit choice.
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
if (key === "userVolume_7") return 30;
if (key === "userVolume_7:b.example.com") return 80;
return defaultVal;
});
setAudioVolumeHost("a.example.com");
expect(elements.getUserVolume(7)).toBe(30);
expect(mockSavePref).toHaveBeenCalledWith("userVolume_7:a.example.com", 30);
setAudioVolumeHost("b.example.com");
expect(elements.getUserVolume(7)).toBe(80);
});
});
describe("setOutputVolume", () => {
it("saves clamped volume to preferences", () => {
elements.setOutputVolume(80);
expect(mockSavePref).toHaveBeenCalledWith("outputVolume", 80);
});
it("clamps to 0-200 range", () => {
elements.setOutputVolume(-10);
expect(mockSavePref).toHaveBeenCalledWith("outputVolume", 0);
elements.setOutputVolume(250);
expect(mockSavePref).toHaveBeenCalledWith("outputVolume", 200);
});
it("updates screenshare audio element volumes", () => {
const audioEl = document.createElement("audio");
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
elements.setOutputVolume(80);
expect(audioEl.volume).toBe(0.8);
});
it("clamps screenshare volume to browser max of 1.0", () => {
const audioEl = document.createElement("audio");
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
elements.setOutputVolume(150);
expect(audioEl.volume).toBe(1);
});
});
describe("screenshare audio", () => {
it("setScreenshareAudioVolume does not throw for unknown userId", () => {
expect(() => elements.setScreenshareAudioVolume(999, 0.5)).not.toThrow();
});
it("setScreenshareAudioVolume sets volume on tracked elements", () => {
const audioEl = document.createElement("audio");
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
elements.setScreenshareAudioVolume(42, 0.7);
expect(audioEl.volume).toBe(0.7);
});
it("setScreenshareAudioVolume clamps to 0-1 range", () => {
const audioEl = document.createElement("audio");
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
elements.setScreenshareAudioVolume(42, -0.5);
expect(audioEl.volume).toBe(0);
elements.setScreenshareAudioVolume(42, 2.0);
expect(audioEl.volume).toBe(1);
});
it("getScreenshareAudioVolume defaults to 1 and reflects stored value", () => {
expect(elements.getScreenshareAudioVolume(42)).toBe(1);
elements.setScreenshareAudioVolume(42, 0.4);
expect(elements.getScreenshareAudioVolume(42)).toBe(0.4);
});
it("volume chosen before the track attaches applies on attach", () => {
elements.setScreenshareAudioVolume(42, 0.25);
const { track, audioEl } = createMockTrack("audio", "track-ss-early");
const publication = { source: "screenShareAudio" };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
expect(audioEl.volume).toBe(0.25);
});
it("setOutputVolume preserves per-user screenshare volume", () => {
const audioEl = document.createElement("audio");
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
elements.setScreenshareAudioVolume(42, 0.5);
elements.setOutputVolume(50);
expect(audioEl.volume).toBe(0.25); // 0.5 user * 0.5 master
});
it("setScreenshareAudioVolume scales by the master output multiplier", () => {
const audioEl = document.createElement("audio");
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
elements.setOutputVolume(50);
elements.setScreenshareAudioVolume(42, 0.8);
expect(audioEl.volume).toBe(0.4); // 0.8 user * 0.5 master
});
it("muteScreenshareAudio persists muted state", () => {
elements.muteScreenshareAudio(42, true);
expect(elements.getScreenshareAudioMuted(42)).toBe(true);
});
it("muteScreenshareAudio toggles back to unmuted", () => {
elements.muteScreenshareAudio(42, true);
elements.muteScreenshareAudio(42, false);
expect(elements.getScreenshareAudioMuted(42)).toBe(false);
});
it("muteScreenshareAudio applies muted to tracked audio elements", () => {
const audioEl = document.createElement("audio");
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
elements.muteScreenshareAudio(42, true);
expect(audioEl.muted).toBe(true);
elements.muteScreenshareAudio(42, false);
expect(audioEl.muted).toBe(false);
});
it("getScreenshareAudioMuted returns false for unknown userId", () => {
expect(elements.getScreenshareAudioMuted(999)).toBe(false);
});
it("getScreenshareAudioMuted reads from audio element when no persisted state", () => {
const audioEl = document.createElement("audio");
audioEl.muted = true;
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
// No call to muteScreenshareAudio, so no persisted state for user 42
expect(elements.getScreenshareAudioMuted(42)).toBe(true);
});
it("getScreenshareAudioMuted returns false for empty audio element set", () => {
(elements as any).screenshareAudioElements = new Map([[42, new Set()]]);
expect(elements.getScreenshareAudioMuted(42)).toBe(false);
});
});
describe("applyRemoteAudioSubscriptionState", () => {
it("does not throw when no room is set", () => {
expect(() => elements.applyRemoteAudioSubscriptionState(true)).not.toThrow();
});
it("unsubscribes voice audio when deafened", () => {
const mockPub = { source: "microphone", setSubscribed: vi.fn() };
const mockParticipant = {
audioTrackPublications: new Map([["pub-1", mockPub]]),
};
const mockRoom = {
remoteParticipants: new Map([["user-1", mockParticipant]]),
} as any;
elements.setRoom(mockRoom);
elements.applyRemoteAudioSubscriptionState(true);
expect(mockPub.setSubscribed).toHaveBeenCalledWith(false);
});
it("re-subscribes voice audio when undeafened", () => {
const mockPub = { source: "microphone", setSubscribed: vi.fn() };
const mockParticipant = {
audioTrackPublications: new Map([["pub-1", mockPub]]),
};
const mockRoom = {
remoteParticipants: new Map([["user-1", mockParticipant]]),
} as any;
elements.setRoom(mockRoom);
elements.applyRemoteAudioSubscriptionState(false);
expect(mockPub.setSubscribed).toHaveBeenCalledWith(true);
});
it("leaves screenshare audio subscribed when deafened", () => {
// Muting/deafening yourself gates voices, not the stream someone is
// sharing — its audio keeps playing under its own per-tile controls.
const micPub = { source: "microphone", setSubscribed: vi.fn() };
const streamPub = { source: "screenShareAudio", setSubscribed: vi.fn() };
const mockParticipant = {
audioTrackPublications: new Map([
["pub-mic", micPub],
["pub-stream", streamPub],
]),
};
const mockRoom = {
remoteParticipants: new Map([["user-1", mockParticipant]]),
} as any;
elements.setRoom(mockRoom);
elements.applyRemoteAudioSubscriptionState(true);
expect(micPub.setSubscribed).toHaveBeenCalledWith(false);
expect(streamPub.setSubscribed).not.toHaveBeenCalled();
});
});
describe("getEffectiveVolume", () => {
it("returns 1.0 for default user with default multiplier", () => {
expect(elements.getEffectiveVolume(42)).toBe(1.0);
});
it("returns outputVolumeMultiplier for userId <= 0", () => {
// userId <= 0 uses default 100, so effective = (100/100) * multiplier
expect(elements.getEffectiveVolume(0)).toBe(1.0);
expect(elements.getEffectiveVolume(-1)).toBe(1.0);
});
it("scales with output volume multiplier", () => {
elements.setOutputVolume(50);
expect(elements.getEffectiveVolume(42)).toBe(0.5);
});
});
describe("applyAllVolumes", () => {
it("does not throw when no room is set", () => {
expect(() => elements.applyAllVolumes()).not.toThrow();
});
it("applies effective volume to all remote participants", () => {
const mockParticipant1 = { identity: "user-10", setVolume: vi.fn() };
const mockParticipant2 = { identity: "user-20", setVolume: vi.fn() };
const mockRoom = {
remoteParticipants: new Map([
["user-10", mockParticipant1],
["user-20", mockParticipant2],
]),
} as any;
elements.setRoom(mockRoom);
elements.applyAllVolumes();
expect(mockParticipant1.setVolume).toHaveBeenCalledWith(1.0);
expect(mockParticipant2.setVolume).toHaveBeenCalledWith(1.0);
});
});
describe("handleTrackUnsubscribedAudio — screenshare", () => {
it("removes screenshare audio elements and cleans up tracking", () => {
const audioEl = document.createElement("audio");
const { track } = createMockTrack("audio", "ss-track-1");
track.detach = vi.fn(() => [audioEl]);
const publication = { source: "screenShareAudio" };
const participant = { identity: "user-42", setVolume: vi.fn() };
// Set up screenshare elements manually
(elements as any).screenshareAudioElements = new Map([[42, new Set([audioEl])]]);
elements.handleTrackUnsubscribedAudio(track as any, publication as any, participant as any);
expect(track.detach).toHaveBeenCalled();
// Should clean up from tracking map
expect((elements as any).screenshareAudioElements.has(42)).toBe(false);
});
it("handles screenshare unsubscribe when no audio elements are tracked", () => {
const { track } = createMockTrack("audio", "ss-track-2");
const publication = { source: "screenShareAudio" };
const participant = { identity: "user-42", setVolume: vi.fn() };
// No screenshare elements tracked
expect(() => {
elements.handleTrackUnsubscribedAudio(track as any, publication as any, participant as any);
}).not.toThrow();
});
});
describe("handleTrackSubscribedAudio — mic with undefined sid", () => {
it("handles track with undefined sid gracefully", () => {
const audioEl = document.createElement("audio");
const track = {
kind: "audio",
sid: undefined,
detach: vi.fn(() => [audioEl]),
attach: vi.fn(() => audioEl),
};
const publication = { source: "microphone" };
const participant = { identity: "user-42", setVolume: vi.fn() };
expect(() => {
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
}).not.toThrow();
});
});
describe("handleTrackSubscribedAudio — with saved output device", () => {
it("calls setSinkId for mic audio when audioOutputDevice preference is set", () => {
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
if (key === "audioOutputDevice") return "device-123";
return defaultVal;
});
const audioEl = document.createElement("audio");
audioEl.setSinkId = vi.fn().mockResolvedValue(undefined);
const track = {
kind: "audio",
sid: "track-dev-1",
detach: vi.fn(() => [audioEl]),
attach: vi.fn(() => audioEl),
};
const publication = { source: "microphone" };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
expect(audioEl.setSinkId).toHaveBeenCalledWith("device-123");
});
it("calls setSinkId for screenshare audio when audioOutputDevice preference is set", () => {
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
if (key === "audioOutputDevice") return "device-456";
return defaultVal;
});
const audioEl = document.createElement("audio");
audioEl.setSinkId = vi.fn().mockResolvedValue(undefined);
const track = {
kind: "audio",
sid: "track-ss-dev-1",
detach: vi.fn(() => [audioEl]),
attach: vi.fn(() => audioEl),
};
const publication = { source: "screenShareAudio" };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
expect(audioEl.setSinkId).toHaveBeenCalledWith("device-456");
});
it("handles setSinkId failure gracefully for screenshare audio", () => {
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
if (key === "audioOutputDevice") return "bad-device";
return defaultVal;
});
const audioEl = document.createElement("audio");
audioEl.setSinkId = vi.fn().mockRejectedValue(new Error("Device not found"));
const track = {
kind: "audio",
sid: "track-ss-err",
detach: vi.fn(() => [audioEl]),
attach: vi.fn(() => audioEl),
};
const publication = { source: "screenShareAudio" };
const participant = { identity: "user-42", setVolume: vi.fn() };
expect(() => {
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
}).not.toThrow();
});
it("handles setSinkId failure gracefully for mic audio", () => {
mockLoadPref.mockImplementation((key: string, defaultVal: unknown) => {
if (key === "audioOutputDevice") return "bad-device";
return defaultVal;
});
const audioEl = document.createElement("audio");
audioEl.setSinkId = vi.fn().mockRejectedValue(new Error("Device not found"));
const track = {
kind: "audio",
sid: "track-mic-err",
detach: vi.fn(() => [audioEl]),
attach: vi.fn(() => audioEl),
};
const publication = { source: "microphone" };
const participant = { identity: "user-42", setVolume: vi.fn() };
expect(() => {
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
}).not.toThrow();
});
});
describe("setUserVolume — without room", () => {
it("saves volume even when no room is set", () => {
elements.setUserVolume(42, 120);
expect(mockSavePref).toHaveBeenCalledWith("userVolume_42", 120);
});
});
describe("deafen guard — handleTrackSubscribedAudio", () => {
it("does not attach mic audio when locally deafened", () => {
mockVoiceStoreState.localDeafened = true;
const { track, audioEl } = createMockTrack("audio", "track-deaf-1");
const publication = { source: "microphone", setSubscribed: vi.fn() };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
// Should NOT attach any audio element
expect(track.attach).not.toHaveBeenCalled();
// Should unsubscribe the publication
expect(publication.setSubscribed).toHaveBeenCalledWith(false);
});
it("attaches screenshare audio even when locally deafened", () => {
// Stream audio is exempt from the deafen gate: muting/deafening
// yourself silences voices, not the content someone is streaming.
mockVoiceStoreState.localDeafened = true;
const { track } = createMockTrack("audio", "track-deaf-ss");
const publication = { source: "screenShareAudio", setSubscribed: vi.fn() };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
expect(track.attach).toHaveBeenCalled();
expect(publication.setSubscribed).not.toHaveBeenCalled();
});
it("attaches mic audio normally when not deafened", () => {
mockVoiceStoreState.localDeafened = false;
const { track } = createMockTrack("audio", "track-ok-1");
const publication = { source: "microphone", setSubscribed: vi.fn() };
const participant = { identity: "user-42", setVolume: vi.fn() };
elements.handleTrackSubscribedAudio(track as any, publication as any, participant as any);
expect(track.attach).toHaveBeenCalled();
expect(publication.setSubscribed).not.toHaveBeenCalled();
});
});
describe("cleanupAllAudioElements", () => {
it("does not throw when empty", () => {
expect(() => elements.cleanupAllAudioElements()).not.toThrow();
});
it("removes all tracked audio elements", () => {
const micEl = document.createElement("audio");
const ssEl = document.createElement("audio");
const removeMic = vi.spyOn(micEl, "remove");
const removeSs = vi.spyOn(ssEl, "remove");
(elements as any).remoteMicAudioElements = new Map([["track-1", micEl]]);
(elements as any).screenshareAudioElements = new Map([[42, new Set([ssEl])]]);
elements.cleanupAllAudioElements();
expect(removeMic).toHaveBeenCalled();
expect(removeSs).toHaveBeenCalled();
});
it("preserves muted-by-user state for reconnecting tracks", () => {
elements.muteScreenshareAudio(42, true);
elements.cleanupAllAudioElements();
expect(elements.getScreenshareAudioMuted(42)).toBe(true);
});
it("clears muted-by-user state with full cleanup", () => {
elements.muteScreenshareAudio(42, true);
elements.cleanupAllAudioElementsFull();
expect(elements.getScreenshareAudioMuted(42)).toBe(false);
});
});
});