/* 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 { describe, expect, test, vi } from "vitest"; import { BehaviorSubject, of, Subject } from "rxjs"; import { withTestScheduler } from "../utils/test"; import { type UserMediaViewModel } from "../state/media/UserMediaViewModel"; import { type ScreenShareViewModel } from "../state/media/ScreenShareViewModel"; import { type LayoutMode } from "../state/LayoutSwitchViewModel"; import { createManualSpotlight, type ManualSpotlight, overrideSpotlight$, pinnedMedia$, resolveManualSpotlight, } from "./lotusSpotlight"; import { parseFocusPayload } from "./lotusFocus"; interface FakeMedia { id: string; userId: string; type: "user"; local: boolean; speaking$: BehaviorSubject; } function media( userId: string, deviceId: string, speaking = false, local = false, ): FakeMedia { return { id: `${userId}:${deviceId}`, userId, type: "user", local, speaking$: new BehaviorSubject(speaking), }; } const asUser = (m: FakeMedia): UserMediaViewModel => m as unknown as UserMediaViewModel; const screen = { id: "@a:x:d1:screen", userId: "@a:x", type: "screen share" }; const aliceDesk = media("@alice:x", "desk"); const alicePhone = media("@alice:x", "phone", true); const bob = media("@bob:x", "d"); const all = [aliceDesk, alicePhone, bob]; describe("resolveManualSpotlight (#30)", () => { const speaking = (m: FakeMedia): boolean => m.speaking$.value; test("null pin resolves to nothing", () => { expect(resolveManualSpotlight(null, all, speaking)).toBeUndefined(); }); test("prefers the media id when given", () => { expect( resolveManualSpotlight( { userId: "@alice:x", id: "@alice:x:desk" }, all, speaking, ), ).toBe(aliceDesk); }); test("falls back to the userId when the id is not present", () => { expect( resolveManualSpotlight( { userId: "@alice:x", id: "@alice:x:tablet" }, all, speaking, ), ).toBe(alicePhone); }); test("userId only: prefers the speaking device, else the first", () => { expect( resolveManualSpotlight({ userId: "@alice:x", id: null }, all, speaking), ).toBe(alicePhone); expect( resolveManualSpotlight( { userId: "@alice:x", id: null }, all, () => false, ), ).toBe(aliceDesk); }); test("absent user resolves to nothing", () => { expect( resolveManualSpotlight({ userId: "@carol:x", id: null }, all, speaking), ).toBeUndefined(); }); }); describe("pinnedMedia$", () => { test("follows the speaking device live for a userId-only pin", () => { withTestScheduler(({ expectObservable, schedule }) => { const desk = media("@alice:x", "desk", true); const phone = media("@alice:x", "phone", false); const pin$ = new BehaviorSubject({ userId: "@alice:x", id: null, }); schedule("-a", { a: () => { desk.speaking$.next(false); phone.speaking$.next(true); }, }); expectObservable( pinnedMedia$(pin$, of([asUser(desk), asUser(phone)])), ).toBe("dp", { d: desk, p: phone }); }); }); }); describe("overrideSpotlight$", () => { test("no pin: identical to upstream", () => { withTestScheduler(({ expectObservable }) => { const result$ = overrideSpotlight$( of(asUser(bob)), of(null), of([]), of(undefined), of(all.map(asUser)), ); expectObservable(result$.pipe()).toBe("(a|)", { a: expect.objectContaining({ spotlight: [bob] }), }); }); }); test("pin replaces the auto speaker without a screenshare", () => { withTestScheduler(({ expectObservable }) => { const result$ = overrideSpotlight$( of(asUser(bob)), of({ userId: "@alice:x", id: "@alice:x:desk" }), of([]), of(undefined), of(all.map(asUser)), ); expectObservable(result$).toBe("(a|)", { a: expect.objectContaining({ spotlight: [aliceDesk] }), }); }); }); test("#29: PiP keeps the auto speaker when a third party is pinned", () => { withTestScheduler(({ expectObservable }) => { let pip$; overrideSpotlight$( of(asUser(bob)), of({ userId: "@alice:x", id: null }), of([screen as unknown as ScreenShareViewModel]), of(undefined), of(all.map(asUser)), ).subscribe((r) => { expect(r.spotlight).toEqual([screen, alicePhone]); pip$ = r.pip$; }); expectObservable(pip$!).toBe("(b|)", { b: bob }); }); }); test("#29: PiP is blanked only when the auto speaker is the pinned one", () => { withTestScheduler(({ expectObservable }) => { let pip$; overrideSpotlight$( of(asUser(alicePhone)), of({ userId: "@alice:x", id: null }), of([screen as unknown as ScreenShareViewModel]), of(undefined), of(all.map(asUser)), ).subscribe((r) => { pip$ = r.pip$; }); expectObservable(pip$!).toBe("(u|)", { u: undefined }); }); }); }); describe("createManualSpotlight (#3 / #16)", () => { function setup(initialLayout: LayoutMode = "grid") { const pin$ = new BehaviorSubject(null); const layout$ = new BehaviorSubject(initialLayout); const setLayout = vi.fn((m: LayoutMode) => layout$.next(m)); const userMedia$ = new BehaviorSubject<{ userId: string }[]>([ { userId: "@alice:x" }, { userId: "@bob:x" }, ]); const leave$ = new Subject(); const ctl = createManualSpotlight( pin$, userMedia$, leave$, { layout$, setLayout }, 5000, ); const sub = ctl.effects$.subscribe(); return { pin$, layout$, setLayout, userMedia$, leave$, ctl, sub }; } test("pinning forces spotlight and clearing restores the displaced mode", () => { const { ctl, layout$, setLayout, pin$ } = setup("grid"); ctl.setManualSpotlight({ userId: "@alice:x", id: null }); expect(pin$.value).toEqual({ userId: "@alice:x", id: null }); expect(setLayout).toHaveBeenLastCalledWith("spotlight"); // Re-pinning someone else does not touch the layout again. ctl.setManualSpotlight({ userId: "@bob:x", id: null }); expect(setLayout).toHaveBeenCalledTimes(1); ctl.setManualSpotlight(null); expect(layout$.value).toBe("grid"); expect(setLayout).toHaveBeenCalledTimes(2); }); test("already in spotlight: nothing is forced or restored", () => { const { ctl, setLayout } = setup("spotlight"); ctl.setManualSpotlight({ userId: "@alice:x", id: null }); ctl.setManualSpotlight(null); expect(setLayout).not.toHaveBeenCalled(); }); test("does not clobber a layout the user chose while pinned", () => { const { ctl, layout$, setLayout } = setup("grid"); ctl.setManualSpotlight({ userId: "@alice:x", id: null }); layout$.next("grid"); // user switched back manually ctl.setManualSpotlight(null); expect(setLayout).toHaveBeenCalledTimes(1); }); test("#16: pin is cleared when the user is gone for 5 s, not on a blip", () => { vi.useFakeTimers(); try { const { ctl, pin$, userMedia$, layout$ } = setup("grid"); ctl.setManualSpotlight({ userId: "@alice:x", id: null }); // Transient absence: back within the window. userMedia$.next([{ userId: "@bob:x" }]); vi.advanceTimersByTime(2000); userMedia$.next([{ userId: "@alice:x" }, { userId: "@bob:x" }]); vi.advanceTimersByTime(6000); expect(pin$.value).not.toBeNull(); // Real leave. userMedia$.next([{ userId: "@bob:x" }]); vi.advanceTimersByTime(4999); expect(pin$.value).not.toBeNull(); vi.advanceTimersByTime(1); expect(pin$.value).toBeNull(); expect(layout$.value).toBe("grid"); } finally { vi.useRealTimers(); } }); test("#16: pin is cleared on leave$", () => { const { ctl, pin$, leave$, layout$ } = setup("grid"); ctl.setManualSpotlight({ userId: "@alice:x", id: null }); expect(layout$.value).toBe("spotlight"); leave$.next(); expect(pin$.value).toBeNull(); expect(layout$.value).toBe("grid"); }); }); describe("parseFocusPayload (#30)", () => { test("omitted userId leaves the pin alone", () => { expect(parseFocusPayload(undefined)).toBeUndefined(); expect(parseFocusPayload({})).toBeUndefined(); expect(parseFocusPayload({ id: "@a:x:d" })).toBeUndefined(); }); test("null / non-string userId clears", () => { expect(parseFocusPayload({ userId: null })).toBeNull(); expect(parseFocusPayload({ userId: 42 })).toBeNull(); }); test("userId with and without id", () => { expect(parseFocusPayload({ userId: "@a:x" })).toEqual({ userId: "@a:x", id: null, }); expect(parseFocusPayload({ userId: "@a:x", id: "@a:x:d" })).toEqual({ userId: "@a:x", id: "@a:x:d", }); expect(parseFocusPayload({ userId: "@a:x", id: 7 })).toEqual({ userId: "@a:x", id: null, }); }); });