/** * @file ConversationView.hidden.test.tsx * @description A chat kept mounted in a hidden pane (another workspace or * tab, `display:none`) does not follow the live stream: a new_event for its * session fetches nothing while its box is off screen, and showing it again * catches the tail up once. On screen, a burst of events is throttled to one * fetch at once plus one after the burst. * @author Nguyễn Ngọc Trí Vĩ */ import { act, render, screen } from "@testing-library/react"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { ConversationView } from "../ConversationView"; import { api } from "../../../lib/api"; import { eventBus } from "../../../lib/eventBus"; vi.mock("../../../lib/api", async (importOriginal) => { const actual = await importOriginal>(); return { ...actual, api: { sessions: { transcripts: vi.fn(async () => ({ transcripts: [] })), transcript: vi.fn(async () => ({ messages: [ { type: "user", content: [{ type: "text", text: "hello" }], timestamp: "2026-10-09T00:00:00Z", }, ], total: 1, has_more: false, first_line: 1, last_line: 1, })), }, }, }; }); let intersect: ((visible: boolean) => void) | null = null; const original = globalThis.IntersectionObserver; beforeEach(() => { intersect = null; // @ts-expect-error test stub globalThis.IntersectionObserver = class { constructor(cb: (e: { isIntersecting: boolean }[]) => void) { intersect = (visible) => cb([{ isIntersecting: visible }]); } observe() {} disconnect() {} }; }); afterEach(() => { globalThis.IntersectionObserver = original; vi.useRealTimers(); vi.clearAllMocks(); }); const event = () => act(() => eventBus.publish({ type: "new_event", data: { session_id: "s1" } } as never)); const calls = () => vi.mocked(api.sessions.transcript).mock.calls.length; describe("ConversationView in a hidden pane", () => { it("fetches nothing while hidden and catches up once when shown", async () => { render(); await screen.findByText("hello"); const before = calls(); act(() => intersect?.(false)); event(); event(); expect(calls()).toBe(before); act(() => intersect?.(true)); await act(async () => {}); expect(calls()).toBe(before + 1); }); it("throttles a burst of events on screen", async () => { render(); await screen.findByText("hello"); vi.useFakeTimers(); const before = calls(); event(); event(); event(); // The first fetches at once; the rest wait for the throttle window. expect(calls()).toBe(before + 1); await act(async () => { await vi.advanceTimersByTimeAsync(1600); }); expect(calls()).toBe(before + 2); }); });