import { afterEach, describe, expect, test } from "bun:test"; import { act, useState } from "react"; import { testRender } from "../../renderers/opentui/test-utils"; import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator"; import { createTestDataProvider } from "../../test-support/data-provider"; import { AppProvider, PaneInstanceProvider } from "../../state/app/context"; import { createDefaultConfig } from "../../types/config"; import { useLiveStreamingSetting } from "../../plugins/builtin/shared/live-streaming"; import { useLiveQuoteEntries, useQuoteStreaming, useQuoteUpdates } from "./quote-streaming"; import { PaneInViewProvider, setAppActive, setAppVisible } from "../app/activity"; let testSetup: Awaited> | undefined; let bumpHarness: (() => void) | null = null; let toggleStreamingPriority: (() => void) | null = null; let togglePollingPriority: (() => void) | null = null; let updateLiveTargets: ((symbol: string) => void) | null = null; let updateFreshnessScope: ((scope: string) => void) | null = null; let observedSubscriptionStartedAt = 0; function QuoteStreamingHarness() { const [tick, setTick] = useState(0); bumpHarness = () => setTick((current) => current + 1); useQuoteStreaming([{ symbol: "AAPL", exchange: "NASDAQ", }]); return {String(tick)}; } function QuoteStreamingPriorityHarness() { const [selected, setSelected] = useState(false); toggleStreamingPriority = () => setSelected((current) => !current); useQuoteStreaming([{ symbol: "AAPL", exchange: "NASDAQ", context: { brokerId: "ibkr", brokerInstanceId: "ibkr-work", instrument: { brokerId: "ibkr", brokerInstanceId: "ibkr-work", conId: 1001, symbol: "AAPL", }, }, route: "provider", surface: selected ? "detail" : "portfolio", visible: true, selected, weight: selected ? 100 : 10, }]); return {selected ? "selected" : "idle"}; } let setHarnessPaneInView: ((inView: boolean) => void) | null = null; /** A selected, visible board row inside a pane that can be covered. */ function CoverablePaneHarness() { const [inView, setInView] = useState(true); setHarnessPaneInView = setInView; return ( ); } function CoverablePaneStream() { useQuoteStreaming([{ symbol: "AAPL", exchange: "NASDAQ", surface: "screener", visible: true, selected: true, weight: 100 }]); return stream; } function QuotePollingHarness() { useQuoteUpdates([{ symbol: "AAPL", exchange: "NASDAQ", }], { liveStreaming: false, pollIntervalMs: 30, }); return polling; } function QuotePollingPriorityHarness() { const [selected, setSelected] = useState(false); togglePollingPriority = () => setSelected((current) => !current); useQuoteUpdates([{ symbol: "AAPL", exchange: "NASDAQ", selected, weight: selected ? 100 : 10, }], { liveStreaming: false, pollIntervalMs: 1_000, }); return {selected ? "selected" : "idle"}; } function FollowedQuoteStreamingHarness() { const liveStreaming = useLiveStreamingSetting(); useQuoteUpdates([{ symbol: "AMD", exchange: "NASDAQ", }], { liveStreaming }); return {liveStreaming ? "live" : "polling"}; } function LiveQuoteFreshnessHarness() { const [symbol, setSymbol] = useState("AAPL"); const [freshnessScopeKey, setFreshnessScopeKey] = useState("expiration-1"); updateLiveTargets = setSymbol; updateFreshnessScope = setFreshnessScopeKey; observedSubscriptionStartedAt = useLiveQuoteEntries( [{ symbol, exchange: "OPTIONS" }], { freshnessScopeKey }, ).subscriptionStartedAt; return {symbol}; } afterEach(async () => { if (testSetup) { await act(async () => { testSetup!.renderer.destroy(); }); testSetup = undefined; } bumpHarness = null; setHarnessPaneInView = null; toggleStreamingPriority = null; togglePollingPriority = null; updateLiveTargets = null; updateFreshnessScope = null; observedSubscriptionStartedAt = 0; setSharedMarketDataCoordinator(null); setAppActive(true); setAppVisible(true); }); describe("useQuoteStreaming", () => { test("keeps streaming while another app has focus and pauses only while hidden", async () => { let subscribeCalls = 0; let unsubscribeCalls = 0; const coordinator = { subscribeQuotes: () => { subscribeCalls += 1; return () => { unsubscribeCalls += 1; }; }, }; setSharedMarketDataCoordinator(coordinator as unknown as MarketDataCoordinator); testSetup = await testRender(, { width: 20, height: 1 }); await act(async () => testSetup!.renderOnce()); expect(subscribeCalls).toBe(1); // Watching quotes from a second screen while typing elsewhere. await act(async () => { setAppActive(false); await testSetup!.renderOnce(); }); expect(unsubscribeCalls).toBe(0); await act(async () => { setAppVisible(false); await testSetup!.renderOnce(); }); expect(unsubscribeCalls).toBe(1); await act(async () => { setAppVisible(true); await testSetup!.renderOnce(); }); expect(subscribeCalls).toBe(2); }); test("does not resubscribe when the component rerenders with the same targets", async () => { let subscribeCalls = 0; let unsubscribeCalls = 0; const coordinator = { subscribeQuotes: () => { subscribeCalls += 1; return () => { unsubscribeCalls += 1; }; }, }; setSharedMarketDataCoordinator(coordinator as unknown as MarketDataCoordinator); testSetup = await testRender(, { width: 20, height: 1, }); await act(async () => { await testSetup!.renderOnce(); }); expect(subscribeCalls).toBe(1); expect(unsubscribeCalls).toBe(0); await act(async () => { bumpHarness?.(); await Promise.resolve(); }); await act(async () => { await testSetup!.renderOnce(); }); expect(subscribeCalls).toBe(1); expect(unsubscribeCalls).toBe(0); }); test("a covered pane keeps its stream at the off-screen priority instead of dropping it", async () => { type CoordinatorTargets = Parameters[0]; const subscribed: CoordinatorTargets[] = []; const updates: CoordinatorTargets[] = []; let unsubscribeCalls = 0; setSharedMarketDataCoordinator({ subscribeQuotes: (targets: CoordinatorTargets) => { subscribed.push(targets); return Object.assign( () => { unsubscribeCalls += 1; }, { update: (nextTargets: CoordinatorTargets) => updates.push(nextTargets) }, ); }, } as unknown as MarketDataCoordinator); testSetup = await testRender(, { width: 20, height: 1 }); await act(async () => testSetup!.renderOnce()); expect(subscribed[0]?.[0]?.priority).toMatchObject({ visible: true, selected: true, weight: 100 }); await act(async () => { setHarnessPaneInView?.(false); await testSetup!.renderOnce(); }); // Same subscription, so totals stay current and nothing reconnects. expect(subscribed).toHaveLength(1); expect(unsubscribeCalls).toBe(0); expect(updates.at(-1)?.[0]?.priority).toMatchObject({ visible: false, selected: false, weight: 10 }); await act(async () => { setHarnessPaneInView?.(true); await testSetup!.renderOnce(); }); expect(updates.at(-1)?.[0]?.priority).toMatchObject({ visible: true, selected: true, weight: 100 }); // Only a hidden app drops the stream. await act(async () => { setAppVisible(false); await testSetup!.renderOnce(); }); expect(unsubscribeCalls).toBe(1); }); test("updates priorities without replacing the hook subscription", async () => { type CoordinatorTargets = Parameters[0]; const initialTargets: CoordinatorTargets[] = []; const updates: CoordinatorTargets[] = []; let unsubscribeCalls = 0; const coordinator = { subscribeQuotes: (targets: CoordinatorTargets) => { initialTargets.push(targets); return Object.assign( () => { unsubscribeCalls += 1; }, { update: (nextTargets: CoordinatorTargets) => updates.push(nextTargets) }, ); }, }; setSharedMarketDataCoordinator(coordinator as unknown as MarketDataCoordinator); testSetup = await testRender(, { width: 20, height: 1, }); await act(async () => testSetup!.renderOnce()); expect(initialTargets).toHaveLength(1); expect(initialTargets[0]?.[0]).toMatchObject({ instrument: { symbol: "AAPL", exchange: "NASDAQ", brokerId: "ibkr", brokerInstanceId: "ibkr-work", instrument: { conId: 1001, symbol: "AAPL" }, }, priority: { route: "provider", surface: "portfolio", visible: true, selected: false, weight: 10, }, }); await act(async () => { toggleStreamingPriority?.(); await Promise.resolve(); await testSetup!.renderOnce(); }); expect(initialTargets).toHaveLength(1); expect(unsubscribeCalls).toBe(0); expect(updates).toHaveLength(1); expect(updates[0]?.[0]).toMatchObject({ instrument: { symbol: "AAPL", brokerId: "ibkr", brokerInstanceId: "ibkr-work", instrument: { conId: 1001 }, }, priority: { route: "provider", surface: "detail", visible: true, selected: true, weight: 100, }, }); }); test("uses forced polling instead of a subscription when live streaming is disabled", async () => { let subscribeCalls = 0; let loadCalls = 0; const coordinator = { subscribeQuotes: () => { subscribeCalls += 1; return () => {}; }, loadQuotesBatch: async (_instruments: unknown[], options: { forceRefresh?: boolean }) => { expect(options.forceRefresh).toBe(true); loadCalls += 1; return []; }, }; setSharedMarketDataCoordinator(coordinator as unknown as MarketDataCoordinator); testSetup = await testRender(, { width: 20, height: 1 }); await act(async () => testSetup!.renderOnce()); expect(subscribeCalls).toBe(0); expect(loadCalls).toBe(1); await act(async () => { await new Promise((resolve) => setTimeout(resolve, 45)); }); expect(loadCalls).toBeGreaterThanOrEqual(2); }); test("inherits disabled streaming from a followed portfolio pane", async () => { let subscribeCalls = 0; let loadCalls = 0; const config = createDefaultConfig("/tmp/gloomberb-live-streaming-test"); const portfolioPane = config.layout.instances.find((pane) => pane.instanceId === "portfolio-list:main"); if (!portfolioPane) throw new Error("expected default portfolio pane"); portfolioPane.settings = { ...portfolioPane.settings, liveStreaming: false }; setSharedMarketDataCoordinator({ subscribeQuotes: () => { subscribeCalls += 1; return () => {}; }, loadQuotesBatch: async () => { loadCalls += 1; return []; }, } as unknown as MarketDataCoordinator); testSetup = await testRender( , { width: 20, height: 1 }, ); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("polling"); expect(subscribeCalls).toBe(0); expect(loadCalls).toBe(1); }); test("does not restart polling when only subscription priority changes", async () => { let loadCalls = 0; const coordinator = { subscribeQuotes: () => () => {}, loadQuotesBatch: async () => { loadCalls += 1; return []; }, }; setSharedMarketDataCoordinator(coordinator as unknown as MarketDataCoordinator); testSetup = await testRender(, { width: 20, height: 1 }); await act(async () => testSetup!.renderOnce()); expect(loadCalls).toBe(1); await act(async () => { togglePollingPriority?.(); await Promise.resolve(); }); await act(async () => testSetup!.renderOnce()); expect(loadCalls).toBe(1); }); test("keeps quote freshness stable while targets move within one surface", async () => { const originalDateNow = Date.now; let now = 100; Date.now = () => now; setSharedMarketDataCoordinator(new MarketDataCoordinator(createTestDataProvider({ subscribeQuotes: () => () => {}, }))); try { testSetup = await testRender(, { width: 20, height: 1, }); await act(async () => { await testSetup!.renderOnce(); }); expect(observedSubscriptionStartedAt).toBe(100); now = 200; await act(async () => { updateLiveTargets?.("MSFT"); await Promise.resolve(); await testSetup!.renderOnce(); }); expect(observedSubscriptionStartedAt).toBe(100); now = 300; await act(async () => { updateFreshnessScope?.("expiration-2"); await Promise.resolve(); await testSetup!.renderOnce(); }); expect(observedSubscriptionStartedAt).toBe(300); } finally { Date.now = originalDateNow; } }); });