/* 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 { RoomEvent, Track } from "livekit-client"; import { BehaviorSubject } from "rxjs"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { type CallViewModel } from "../state/CallViewModel/CallViewModel"; import { ALONE_MS, NO_FRAMES_MS, startLotusScreenshareWatch, } from "./lotusScreenshareWatch"; const sent: unknown[] = []; vi.mock("../widget", () => ({ widget: { api: {} } })); vi.mock("./lotusWidget", () => ({ lotusSendToHost: (action: string, data: unknown): boolean => { sent.push({ action, data }); return true; }, })); class FakeTrack extends EventTarget {} const makeRoom = (): { room: Record; emit: (event: string, ...args: unknown[]) => void; remote: Map; } => { const handlers = new Map void)[]>(); const remote = new Map(); const room = { remoteParticipants: remote, localParticipant: { trackPublications: new Map() }, on: (event: string, h: (...args: unknown[]) => void): void => { handlers.set(event, [...(handlers.get(event) ?? []), h]); }, off: (event: string, h: (...args: unknown[]) => void): void => { handlers.set( event, (handlers.get(event) ?? []).filter((x) => x !== h), ); }, }; return { room, remote, emit: (event, ...args): void => { (handlers.get(event) ?? []).forEach((h) => h(...args)); }, }; }; describe("startLotusScreenshareWatch", () => { let clock = 0; beforeEach(() => { sent.length = 0; clock = 1_000_000; vi.useFakeTimers(); }); afterEach(() => vi.useRealTimers()); const start = (): ReturnType & { stop: () => void } => { const fake = makeRoom(); const connections = new BehaviorSubject({ getConnections: () => [{ livekitRoom: fake.room }], }); const stop = startLotusScreenshareWatch( { allConnections$: connections } as unknown as CallViewModel, () => clock, ); return { ...fake, stop }; }; it("reports a share whose track ended", () => { const { emit, stop } = start(); const mst = new FakeTrack(); emit(RoomEvent.LocalTrackPublished, { source: Track.Source.ScreenShare, track: { mediaStreamTrack: mst }, }); mst.dispatchEvent(new Event("ended")); expect(sent).toEqual([ { action: "io.lotus.screenshare_notice", data: { kind: "ended" } }, ]); stop(); }); it("reports no frames after a sustained mute, once, and not after an unmute", () => { const { emit, stop } = start(); const mst = new FakeTrack(); emit(RoomEvent.LocalTrackPublished, { source: Track.Source.ScreenShare, track: { mediaStreamTrack: mst }, }); mst.dispatchEvent(new Event("mute")); vi.advanceTimersByTime(NO_FRAMES_MS / 2); mst.dispatchEvent(new Event("unmute")); vi.advanceTimersByTime(NO_FRAMES_MS); expect(sent).toEqual([]); mst.dispatchEvent(new Event("mute")); vi.advanceTimersByTime(NO_FRAMES_MS); mst.dispatchEvent(new Event("mute")); vi.advanceTimersByTime(NO_FRAMES_MS); expect(sent).toEqual([ { action: "io.lotus.screenshare_notice", data: { kind: "no-frames" } }, ]); stop(); }); it("nudges after 30 min of sharing with nobody else, never while others are present", () => { const { emit, remote, stop } = start(); emit(RoomEvent.LocalTrackPublished, { source: Track.Source.ScreenShare, track: { mediaStreamTrack: new FakeTrack() }, }); remote.set("bob", {}); clock += ALONE_MS + 60_000; vi.advanceTimersByTime(60_000); expect(sent).toEqual([]); remote.clear(); vi.advanceTimersByTime(60_000); vi.advanceTimersByTime(60_000); expect(sent).toEqual([ { action: "io.lotus.screenshare_notice", data: { kind: "alone" } }, ]); stop(); }); it("reports ended from the unpublish when LiveKit's handler ran first", () => { const { emit, stop } = start(); const share = new FakeTrack() as FakeTrack & { readyState: string }; share.readyState = "ended"; const pub = { source: Track.Source.ScreenShare, track: { mediaStreamTrack: share }, }; emit(RoomEvent.LocalTrackPublished, pub); emit(RoomEvent.LocalTrackUnpublished, pub); share.dispatchEvent(new Event("ended")); expect(sent).toEqual([ { action: "io.lotus.screenshare_notice", data: { kind: "ended" } }, ]); stop(); }); it("ignores non-screenshare publications and stops watching on unpublish", () => { const { emit, stop } = start(); const cam = new FakeTrack(); emit(RoomEvent.LocalTrackPublished, { source: Track.Source.Camera, track: { mediaStreamTrack: cam }, }); cam.dispatchEvent(new Event("ended")); const share = new FakeTrack(); const pub = { source: Track.Source.ScreenShare, track: { mediaStreamTrack: share }, }; emit(RoomEvent.LocalTrackPublished, pub); emit(RoomEvent.LocalTrackUnpublished, pub); share.dispatchEvent(new Event("ended")); expect(sent).toEqual([]); stop(); }); });