mirror of
https://github.com/J3vb/OwnCord.git
synced 2026-09-03 03:50:00 +03:00
- livekit-session: assert joining→securing→connected on join, idle on leave, connected after auto-reconnect - voice-widget: assert each status label + secured badge visibility, and controls disabled with reason while the socket is down, re-enabled on reconnect - voice-callbacks: assert join/leave/disconnect do not send over a down socket - voice.store: assert join seeds joining, leave resets idle, setter writes status - thread voiceStatus through existing full-state fixtures; keep an active-call socket live in widget fixtures so control-click tests still operate enabled Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
346 lines
10 KiB
TypeScript
346 lines
10 KiB
TypeScript
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Mocks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
const {
|
|
mockVoiceStoreGetState,
|
|
mockJoinVoiceChannel,
|
|
mockLeaveVoiceChannel,
|
|
mockVoiceSessionLeave,
|
|
mockSetMuted,
|
|
mockSetDeafened,
|
|
mockEnableCamera,
|
|
mockDisableCamera,
|
|
mockEnableScreenshare,
|
|
mockDisableScreenshare,
|
|
mockUiGetState,
|
|
} = vi.hoisted(() => ({
|
|
mockVoiceStoreGetState: vi.fn(),
|
|
mockJoinVoiceChannel: vi.fn(),
|
|
mockLeaveVoiceChannel: vi.fn(),
|
|
mockVoiceSessionLeave: vi.fn(),
|
|
mockSetMuted: vi.fn(),
|
|
mockSetDeafened: vi.fn(),
|
|
mockEnableCamera: vi.fn(() => Promise.resolve()),
|
|
mockDisableCamera: vi.fn(() => Promise.resolve()),
|
|
mockEnableScreenshare: vi.fn(() => Promise.resolve()),
|
|
mockDisableScreenshare: vi.fn(() => Promise.resolve()),
|
|
mockUiGetState: vi.fn(() => ({ connectionStatus: "connected" })),
|
|
}));
|
|
|
|
vi.mock("@lib/logger", () => ({
|
|
createLogger: () => ({
|
|
debug: vi.fn(),
|
|
info: vi.fn(),
|
|
warn: vi.fn(),
|
|
error: vi.fn(),
|
|
}),
|
|
}));
|
|
|
|
vi.mock("@stores/voice.store", () => ({
|
|
voiceStore: { getState: mockVoiceStoreGetState },
|
|
joinVoiceChannel: mockJoinVoiceChannel,
|
|
leaveVoiceChannel: mockLeaveVoiceChannel,
|
|
}));
|
|
|
|
vi.mock("@stores/ui.store", () => ({
|
|
uiStore: { getState: mockUiGetState },
|
|
}));
|
|
|
|
vi.mock("@lib/livekitSession", () => ({
|
|
leaveVoice: mockVoiceSessionLeave,
|
|
setMuted: mockSetMuted,
|
|
setDeafened: mockSetDeafened,
|
|
enableCamera: mockEnableCamera,
|
|
disableCamera: mockDisableCamera,
|
|
enableScreenshare: mockEnableScreenshare,
|
|
disableScreenshare: mockDisableScreenshare,
|
|
}));
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Imports
|
|
// ---------------------------------------------------------------------------
|
|
|
|
import {
|
|
createVoiceWidgetCallbacks,
|
|
createSidebarVoiceCallbacks,
|
|
} from "../../src/pages/main-page/VoiceCallbacks";
|
|
import type { WsClient } from "../../src/lib/ws";
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Helpers
|
|
// ---------------------------------------------------------------------------
|
|
|
|
function makeWs(): WsClient {
|
|
return { send: vi.fn() } as unknown as WsClient;
|
|
}
|
|
|
|
function makeLimiters(voiceAllowed = true, videoAllowed = true) {
|
|
return {
|
|
voice: { tryConsume: vi.fn(() => voiceAllowed) },
|
|
voiceVideo: { tryConsume: vi.fn(() => videoAllowed) },
|
|
};
|
|
}
|
|
|
|
interface VoiceStateStub {
|
|
currentChannelId: number | null;
|
|
localMuted: boolean;
|
|
localDeafened: boolean;
|
|
localCamera: boolean;
|
|
localScreenshare: boolean;
|
|
}
|
|
|
|
function makeVoiceState(overrides: Partial<VoiceStateStub> = {}): VoiceStateStub {
|
|
return {
|
|
currentChannelId: 10,
|
|
localMuted: false,
|
|
localDeafened: false,
|
|
localCamera: false,
|
|
localScreenshare: false,
|
|
...overrides,
|
|
};
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Voice Widget Callbacks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("createVoiceWidgetCallbacks", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockVoiceStoreGetState.mockReturnValue(makeVoiceState());
|
|
mockUiGetState.mockReturnValue({ connectionStatus: "connected" });
|
|
});
|
|
|
|
describe("onDisconnect", () => {
|
|
it("sends voice_leave and cleans up", () => {
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onDisconnect();
|
|
|
|
expect(mockVoiceSessionLeave).toHaveBeenCalledWith(false);
|
|
expect(mockLeaveVoiceChannel).toHaveBeenCalled();
|
|
expect(ws.send).toHaveBeenCalledWith({ type: "voice_leave", payload: {} });
|
|
});
|
|
|
|
it("does nothing when not in a voice channel", () => {
|
|
mockVoiceStoreGetState.mockReturnValue(makeVoiceState({ currentChannelId: null }));
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onDisconnect();
|
|
|
|
expect(ws.send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("does not send over a down socket while reconnecting", () => {
|
|
mockUiGetState.mockReturnValue({ connectionStatus: "reconnecting" });
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onDisconnect();
|
|
|
|
expect(mockVoiceSessionLeave).not.toHaveBeenCalled();
|
|
expect(ws.send).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("onMuteToggle", () => {
|
|
it("mutes when unmuted", () => {
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onMuteToggle();
|
|
|
|
expect(mockSetMuted).toHaveBeenCalledWith(true);
|
|
expect(ws.send).toHaveBeenCalledWith({ type: "voice_mute", payload: { muted: true } });
|
|
});
|
|
|
|
it("unmutes and undeafens when muted+deafened", () => {
|
|
mockVoiceStoreGetState.mockReturnValue(
|
|
makeVoiceState({ localMuted: true, localDeafened: true }),
|
|
);
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onMuteToggle();
|
|
|
|
expect(mockSetMuted).toHaveBeenCalledWith(false);
|
|
expect(mockSetDeafened).toHaveBeenCalledWith(false);
|
|
expect(ws.send).toHaveBeenCalledWith({ type: "voice_deafen", payload: { deafened: false } });
|
|
expect(ws.send).toHaveBeenCalledWith({ type: "voice_mute", payload: { muted: false } });
|
|
});
|
|
|
|
it("respects rate limiter", () => {
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters(false));
|
|
|
|
cbs.onMuteToggle();
|
|
|
|
expect(mockSetMuted).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("onDeafenToggle", () => {
|
|
it("deafens and mutes when undeafened", () => {
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onDeafenToggle();
|
|
|
|
expect(mockSetDeafened).toHaveBeenCalledWith(true);
|
|
expect(mockSetMuted).toHaveBeenCalledWith(true);
|
|
});
|
|
|
|
it("undeafens and unmutes when deafened", () => {
|
|
mockVoiceStoreGetState.mockReturnValue(
|
|
makeVoiceState({ localDeafened: true, localMuted: true }),
|
|
);
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onDeafenToggle();
|
|
|
|
expect(mockSetDeafened).toHaveBeenCalledWith(false);
|
|
expect(mockSetMuted).toHaveBeenCalledWith(false);
|
|
});
|
|
|
|
it("does not double-mute when already muted", () => {
|
|
mockVoiceStoreGetState.mockReturnValue(makeVoiceState({ localMuted: true }));
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onDeafenToggle();
|
|
|
|
// Should deafen but not send another mute since already muted
|
|
expect(mockSetDeafened).toHaveBeenCalledWith(true);
|
|
expect(mockSetMuted).not.toHaveBeenCalled();
|
|
expect(ws.send).toHaveBeenCalledWith({ type: "voice_deafen", payload: { deafened: true } });
|
|
expect(ws.send).not.toHaveBeenCalledWith(expect.objectContaining({ type: "voice_mute" }));
|
|
});
|
|
});
|
|
|
|
describe("onCameraToggle", () => {
|
|
it("enables camera when off", () => {
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onCameraToggle();
|
|
|
|
expect(mockEnableCamera).toHaveBeenCalled();
|
|
});
|
|
|
|
it("disables camera when on", () => {
|
|
mockVoiceStoreGetState.mockReturnValue(makeVoiceState({ localCamera: true }));
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onCameraToggle();
|
|
|
|
expect(mockDisableCamera).toHaveBeenCalled();
|
|
});
|
|
|
|
it("respects video rate limiter", () => {
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters(true, false));
|
|
|
|
cbs.onCameraToggle();
|
|
|
|
expect(mockEnableCamera).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
|
|
describe("onScreenshareToggle", () => {
|
|
it("enables screenshare when off", () => {
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onScreenshareToggle();
|
|
|
|
expect(mockEnableScreenshare).toHaveBeenCalled();
|
|
expect(mockDisableScreenshare).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("disables screenshare when on", () => {
|
|
mockVoiceStoreGetState.mockReturnValue(makeVoiceState({ localScreenshare: true }));
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters());
|
|
|
|
cbs.onScreenshareToggle();
|
|
|
|
expect(mockDisableScreenshare).toHaveBeenCalled();
|
|
expect(mockEnableScreenshare).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("respects video rate limiter", () => {
|
|
const ws = makeWs();
|
|
const cbs = createVoiceWidgetCallbacks(ws, makeLimiters(true, false));
|
|
|
|
cbs.onScreenshareToggle();
|
|
|
|
expect(mockEnableScreenshare).not.toHaveBeenCalled();
|
|
expect(mockDisableScreenshare).not.toHaveBeenCalled();
|
|
});
|
|
});
|
|
});
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sidebar Voice Callbacks
|
|
// ---------------------------------------------------------------------------
|
|
|
|
describe("createSidebarVoiceCallbacks", () => {
|
|
beforeEach(() => {
|
|
vi.clearAllMocks();
|
|
mockUiGetState.mockReturnValue({ connectionStatus: "connected" });
|
|
});
|
|
|
|
it("onVoiceJoin sends voice_join and updates store", () => {
|
|
const ws = makeWs();
|
|
const cbs = createSidebarVoiceCallbacks(ws);
|
|
|
|
cbs.onVoiceJoin(42);
|
|
|
|
expect(mockJoinVoiceChannel).toHaveBeenCalledWith(42);
|
|
expect(ws.send).toHaveBeenCalledWith({
|
|
type: "voice_join",
|
|
payload: { channel_id: 42 },
|
|
});
|
|
});
|
|
|
|
it("onVoiceLeave sends voice_leave and cleans up", () => {
|
|
const ws = makeWs();
|
|
const cbs = createSidebarVoiceCallbacks(ws);
|
|
|
|
cbs.onVoiceLeave();
|
|
|
|
expect(mockVoiceSessionLeave).toHaveBeenCalledWith(false);
|
|
expect(mockLeaveVoiceChannel).toHaveBeenCalled();
|
|
expect(ws.send).toHaveBeenCalledWith({ type: "voice_leave", payload: {} });
|
|
});
|
|
|
|
it("onVoiceJoin does not send over a down socket", () => {
|
|
mockUiGetState.mockReturnValue({ connectionStatus: "disconnected" });
|
|
const ws = makeWs();
|
|
const cbs = createSidebarVoiceCallbacks(ws);
|
|
|
|
cbs.onVoiceJoin(42);
|
|
|
|
expect(mockJoinVoiceChannel).not.toHaveBeenCalled();
|
|
expect(ws.send).not.toHaveBeenCalled();
|
|
});
|
|
|
|
it("onVoiceLeave does not send over a down socket", () => {
|
|
mockUiGetState.mockReturnValue({ connectionStatus: "reconnecting" });
|
|
const ws = makeWs();
|
|
const cbs = createSidebarVoiceCallbacks(ws);
|
|
|
|
cbs.onVoiceLeave();
|
|
|
|
expect(mockVoiceSessionLeave).not.toHaveBeenCalled();
|
|
expect(ws.send).not.toHaveBeenCalled();
|
|
});
|
|
});
|