import { afterEach, describe, expect, test } from "bun:test"; import { act, createElement, forwardRef, useMemo, useState, type ForwardedRef, type ReactNode, } from "react"; import type { ResolvedSeries, TimeSeriesPoint } from "../../../time-series/types"; import { testRender } from "../../../renderers/opentui/test-utils"; import { Box, Text, UiHostProvider, useNativeRenderer, useRendererHost, useUiHost, type BoxRenderable, } from "../../../ui"; import { InputHostProvider, type InputHost, type KeyEventLike, } from "../../../react/input"; import { CompositeChart } from "./composite-chart"; import { createDefaultConfig } from "../../../types/config"; import { AppContext, createInitialState, PaneInstanceProvider } from "../../../state/app/context"; import { buildCompositeNavigationFrame, zoomCompositeViewport, } from "./interactions"; import { COMPOSITE_RIGHT_OFFSET_RATIO } from "./time-scale"; import { getThemeColors, syncTheme } from "../../../theme/colors"; import { ThemeProvider } from "../../../theme/theme-context"; import { DEFAULT_THEME } from "../../../theme/themes"; let testSetup: Awaited> | undefined; let chartShortcut: ((event: KeyEventLike) => void) | null = null; let capturedSurfaceProps: Record | null = null; let capturedSurfaceNode: BoxRenderable | null = null; const chartInputHost: InputHost = { useShortcut(handler) { chartShortcut = handler; }, useViewport() { return { width: 80, height: 24 }; }, }; function assignRef(ref: ForwardedRef, value: any) { if (typeof ref === "function") ref(value); else if (ref) ref.current = value; } function CaptureChartSurfaceProvider({ children, canvasCharts = false, }: { children: ReactNode; canvasCharts?: boolean; }) { const baseUi = useUiHost(); const renderer = useRendererHost(); const nativeRenderer = useNativeRenderer(); const CapturingChartSurface = useMemo(() => { const BaseChartSurface = baseUi.ChartSurface; return forwardRef>(function CapturingSurface(props, ref) { capturedSurfaceProps = props; return createElement(BaseChartSurface as any, { ...props, ref: (node: BoxRenderable | null) => { capturedSurfaceNode = node; assignRef(ref, node); }, }); }); }, [baseUi]); const ui = useMemo( () => ({ ...baseUi, ChartSurface: CapturingChartSurface, capabilities: canvasCharts ? { ...baseUi.capabilities, canvasCharts: true, nativeCharts: false } : baseUi.capabilities, }), [CapturingChartSurface, baseUi, canvasCharts], ); return ( {children} ); } function pointerEvent( localX: number, localY: number, options: { ctrl?: boolean; shift?: boolean; alt?: boolean; scroll?: { direction: "up" | "down" | "left" | "right"; delta: number }; } = {}, ) { const node = capturedSurfaceNode!; return { x: (node.x as number) + localX, y: (node.y as number) + localY, modifiers: { shift: options.shift === true, alt: options.alt === true, ctrl: options.ctrl === true, }, scroll: options.scroll, preventDefault() {}, stopPropagation() {}, }; } function keyEvent(name: string, shift = false): KeyEventLike { let defaultPrevented = false; let propagationStopped = false; return { key: name, name, sequence: name, ctrl: false, shift, alt: false, meta: false, get defaultPrevented() { return defaultPrevented; }, get propagationStopped() { return propagationStopped; }, preventDefault() { defaultPrevented = true; }, stopPropagation() { propagationStopped = true; }, }; } afterEach(async () => { if (testSetup) await act(async () => testSetup!.renderer.destroy()); testSetup = undefined; chartShortcut = null; capturedSurfaceProps = null; capturedSurfaceNode = null; syncTheme(DEFAULT_THEME); }); function point(date: string, value: number): TimeSeriesPoint { const observedAt = new Date(`${date}T00:00:00.000Z`); return { date: observedAt, observedAt, value }; } function timestampPoint(timestamp: string, value: number): TimeSeriesPoint { const observedAt = new Date(timestamp); return { date: observedAt, observedAt, value }; } function series(id: string, panelId: string, axis: ResolvedSeries["axis"], unit: string, values: number[]): ResolvedSeries { return { id, label: id === "price" ? "ACME Price" : "OTHER Revenue", color: id === "price" ? "#00ff66" : "#ffaa00", unit, unitGroup: unit === "USD" ? "currency" : "percent", nativeFrequency: id === "price" ? "daily" : "quarterly", dataShape: "scalar", style: id === "price" ? "line" : "step", transform: "raw", axis, panelId, interpolation: id === "price" ? "none" : "step-after", points: values.map((value, index) => point(`2025-01-0${index + 1}`, value)), }; } function twoSessionCandles(): ResolvedSeries { const points = [ ...Array.from({ length: 7 }, (_, index) => ( Date.UTC(2025, 0, 2, 17, index * 30) )), ...Array.from({ length: 8 }, (_, index) => ( Date.UTC(2025, 0, 3, 13, 30 + index * 30) )), ].map((timestamp, index) => { const date = new Date(timestamp); const value = 100 + index * 0.1; return { date, observedAt: date, value, open: value, high: value + 1, low: value - 1, close: value, volume: 100, }; }); return { ...series("price", "main", "left", "USD", []), nativeFrequency: "intraday", dataShape: "ohlc", style: "candles", timeBasis: { kind: "market", timeZone: "America/New_York", cadenceMs: 30 * 60_000, }, points, }; } describe("CompositeChart", () => { test("repaints cached chart rasters when the theme changes", async () => { let setThemeId: ((themeId: string) => void) | null = null; const chartSeries = [series("price", "main", "left", "USD", [100, 101, 102])]; function ThemedChart() { const [themeId, setTheme] = useState(DEFAULT_THEME); setThemeId = setTheme; return ( ); } testSetup = await testRender(, { width: 62, height: 14 }); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const nextThemeId = "github-light"; await act(async () => { setThemeId?.(nextThemeId); await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const bitmap = capturedSurfaceProps!.bitmaps[0] as { pixels: Uint8Array }; const expectedBackground = getThemeColors(nextThemeId).bg; const expectedRgb = [1, 3, 5].map((offset) => parseInt(expectedBackground.slice(offset, offset + 2), 16)); expect(Array.from(bitmap.pixels.slice(0, 3))).toEqual(expectedRgb); }); test("does not allocate a native raster when braille rendering is forced", async () => { const config = createDefaultConfig("/tmp/gloomberb-composite-braille"); config.chartPreferences.renderer = "braille"; testSetup = await testRender( {} }}> , { width: 62, height: 14 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(capturedSurfaceProps!.bitmaps).toBeNull(); }); test("shows the regular-session move beside the latest intraday value", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("ACME Price $110 +10.65%"); }); test("holds a lower panel's rows while its series has no observations in view", async () => { const price: ResolvedSeries = { ...series("price", "main", "left", "USD", []), points: [ point("2025-01-01", 100), point("2025-01-02", 103), point("2025-01-03", 101), point("2025-01-04", 104), point("2025-01-05", 102), ], }; const volume: ResolvedSeries = { ...series("volume", "volume", "left", "shares", []), label: "Volume", unitGroup: "volume", style: "columns", points: [point("2025-01-01", 4_000), point("2025-01-02", 6_000)], }; const panels = [{ id: "main", height: 3 }, { id: "volume", height: 1 }]; const renderChart = async ( volumeSeries: ResolvedSeries, viewport?: { start: Date; end: Date }, ) => { testSetup = await testRender( , { width: 80, height: 20 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const frame = testSetup!.captureCharFrame(); await act(async () => testSetup!.renderer.destroy()); testSetup = undefined; return frame; }; // Price labels and the price line stay above the lower panel's first row, // and would spill into it the moment the lower panel gave up its rows. const lastPriceRow = (frame: string) => frame .split("\n") .reduce((row, line, index) => /[$•]/.test(line) ? index : row, -1); const volumeTop = (frame: string) => frame.split("\n").findIndex((line) => /^\s+[\d.]+K /.test(line)); const loaded = await renderChart(volume); // Panned to a window the upper panel covers and the lower panel has no bars in. const panned = await renderChart(volume, { start: new Date("2025-01-03T00:00:00.000Z"), end: new Date("2025-01-05T00:00:00.000Z"), }); // The same chart before the lower panel's data has loaded at all. const loading = await renderChart({ ...volume, points: [] }); const lowerPanelTop = volumeTop(loaded); expect(lowerPanelTop).toBeGreaterThan(0); expect(volumeTop(panned)).toBe(lowerPanelTop); expect(lastPriceRow(panned)).toBeLessThan(lowerPanelTop); expect(lastPriceRow(loading)).toBe(lastPriceRow(loaded)); expect(lastPriceRow(loaded)).toBe(lowerPanelTop - 1); // A panel with loaded history keeps its axis while the window holds no bars. expect(panned).toMatch(/\dK/); }); test("lays out mixed panels with one shared legend and time axis", async () => { testSetup = await testRender( , { width: 80, height: 20 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const frame = testSetup.captureCharFrame(); expect(frame).toContain("2025-01-02"); expect(frame).toContain("ACME Price"); expect(frame).toContain("OTHER Revenue"); expect(frame).toContain("$103"); expect(frame.match(/Jan 1/g)).toHaveLength(1); expect(frame.match(/Jan 3/g)).toHaveLength(1); }); test("keeps legend items compact and anchors an accessory to the right edge", async () => { testSetup = await testRender( + add series )} />, { width: 80, height: 14 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const legend = testSetup.captureCharFrame() .split("\n") .find((line) => line.includes("+ add series")); expect(legend).toBeDefined(); expect(legend).toContain("ACME Price $103"); expect(legend).toContain("OTHER Revenue 6.00%"); const accessoryStart = legend!.indexOf("+ add series"); const lastLegendEnd = legend!.indexOf("6.00%") + "6.00%".length; expect(accessoryStart).toBe(78 - 14); expect(accessoryStart - lastLegendEnd).toBeGreaterThan(1); }); test("shortens long comparison names before clipping yield precision or units at 80 columns", async () => { testSetup = await testRender( + add series} legendAccessoryWidth={14} />, { width: 80, height: 14 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const legend = testSetup.captureCharFrame().split("\n").find((line) => line.includes("+ add series"))!; expect(legend).toContain("5.68%"); expect(legend).toContain("7.42%"); expect(legend.indexOf("7.42%") + 5).toBeLessThan(legend.indexOf("+ add series")); }); test("leaving a historical chart restores its window's legend value instead of the navigation buffer", async () => { let setCursor: (date: Date | null) => void = () => {}; const margin: ResolvedSeries = { ...series("margin", "main", "left", "%", []), label: "MSFT Operating Margin", style: "line", interpolation: "none", points: [point("2024-06-30", 44.64), point("2025-06-30", 45.62), point("2026-06-30", 46.78)], }; function HistoricalChart() { const [cursor, updateCursor] = useState(null); setCursor = updateCursor; return ; } testSetup = await testRender(, { width: 102, height: 14 }); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const legend = () => testSetup!.captureCharFrame().split("\n")[0]!; expect(legend()).toContain("45.6%"); expect(legend()).not.toContain("46.8%"); await act(async () => { setCursor(new Date("2024-06-30")); }); await act(async () => { await testSetup!.renderOnce(); }); expect(legend()).toContain("44.6%"); await act(async () => { setCursor(null); }); await act(async () => { await testSetup!.renderOnce(); }); expect(legend()).toContain("45.6%"); expect(legend()).not.toContain("46.8%"); }); test("retains financial values and negative signs when long legend names need truncation", async () => { testSetup = await testRender( , { width: 102, height: 14 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const legend = testSetup.captureCharFrame().split("\n")[0]!; expect(legend).toContain("MSFT:XNAS Revenue $90.01B"); expect(legend).toContain("... -49.6%"); expect(legend).toContain("腾讯控股腾讯控股"); expect(legend.match(/\$90\.01B/g)).toHaveLength(2); }); test("gives legend names the row's room and drops the listing exchange before cutting a name", async () => { const legendAt = async (width: number) => { if (testSetup) await act(async () => testSetup!.renderer.destroy()); testSetup = await testRender( , { width: width + 2, height: 12 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); return testSetup.captureCharFrame().split("\n")[0]!; }; expect(await legendAt(60)).toContain("Volume AAPL:XNAS Price 8.00%"); const narrow = await legendAt(50); expect(narrow).toContain("AAPL:XNAS Price $101"); expect(narrow).toContain("Volume AAPL Price 8.00%"); expect(narrow).not.toContain("..."); }); test("keeps non-plotted legend series available for restoring", async () => { const price = series("price", "main", "left", "USD", [100, 103, 101]); const hiddenRevenue = series("revenue", "main", "right", "%", [4, 6, 8]); testSetup = await testRender( , { width: 80, height: 14 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(testSetup.captureCharFrame()).toContain("OTHER Revenue"); }); test("uses buffered market anchors so closed sessions do not create chart holes", async () => { const candles = twoSessionCandles(); const currentSession = { ...candles, points: candles.points.filter((point) => point.date.getUTCDate() === 3), }; testSetup = await testRender( , { width: 74, height: 14 }, ); await act(async () => testSetup!.renderOnce()); const bodyColumns = [...new Set(testSetup.captureCharFrame() .split("\n") .flatMap((line) => [...line].flatMap((cell, index) => cell === "█" ? [index] : [])))] .sort((left, right) => left - right); const largestGap = bodyColumns.slice(1).reduce( (largest, column, index) => Math.max(largest, column - bodyColumns[index]!), 0, ); expect(bodyColumns.length).toBeGreaterThan(10); expect(largestGap).toBeLessThan(10); }); test("toggles a series from its legend entry", async () => { const toggled: string[] = []; testSetup = await testRender( toggled.push(seriesId)} />, { width: 60, height: 12 }, ); await act(async () => { await testSetup!.renderOnce(); }); const priceColumn = testSetup.captureCharFrame().split("\n")[0]!.indexOf("ACME Price"); expect(priceColumn).toBeGreaterThan(0); await act(async () => { await testSetup!.mockMouse.click(priceColumn, 0); await testSetup!.renderOnce(); }); expect(toggled).toEqual(["price"]); }); test("scrolls an overflowing legend horizontally", async () => { const labels = ["FIRST LONG SERIES", "SECOND LONG SERIES", "THIRD LONG SERIES", "LAST LONG SERIES"]; const overflowingSeries = labels.map((label, index) => ({ ...series(`series-${index}`, "main", "left", "USD", [index + 1, index + 2]), label, })); testSetup = await testRender( , { width: 40, height: 12 }, ); await act(async () => { await testSetup!.renderOnce(); }); expect(testSetup.captureCharFrame().split("\n")[0]).toContain("FIRST LONG SERIES"); expect(testSetup.captureCharFrame().split("\n")[0]).not.toContain("LAST LONG SERIES"); await act(async () => { for (let index = 0; index < 120; index += 1) { await testSetup!.mockMouse.scroll(20, 0, "down"); } await testSetup!.renderOnce(); }); expect(testSetup.captureCharFrame().split("\n")[0]).toContain("LAST LONG SERIES"); }); test("reaches and toggles overflowing legend series from the keyboard", async () => { const toggled: string[] = []; const labels = ["FIRST LONG SERIES", "SECOND LONG SERIES", "THIRD LONG SERIES", "LAST LONG SERIES"]; const overflowingSeries = labels.map((label, index) => ({ ...series(`series-${index}`, "main", "left", "USD", [index + 1, index + 2]), label, })); testSetup = await testRender( toggled.push(seriesId)} /> , { width: 40, height: 12 }, ); await act(async () => { await testSetup!.renderOnce(); for (let index = 0; index < overflowingSeries.length; index += 1) { chartShortcut?.(keyEvent("]")); } await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(testSetup.captureCharFrame().split("\n")[0]).toContain("LAST LONG SERIES"); await act(async () => { chartShortcut?.(keyEvent("space")); await testSetup!.renderOnce(); }); expect(toggled).toEqual(["series-3"]); }); test("keeps the legend accessory visible when the pane is narrower than its legend", async () => { testSetup = await testRender( + add series )} legendAccessoryWidth={14} />, { width: 22, height: 12 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const legend = testSetup.captureCharFrame() .split("\n") .find((line) => line.includes("+ add series")); expect(legend).toBeDefined(); expect(legend).toContain("●"); }); test("moves and clears the shared cursor from the keyboard while focused", async () => { const cursorChanges: Array = []; testSetup = await testRender( cursorChanges.push(date?.toISOString() ?? null)} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => chartShortcut?.(keyEvent("left"))); await act(async () => testSetup!.renderOnce()); expect(cursorChanges).toEqual(["2025-01-03T00:00:00.000Z"]); const firstCursorFrame = testSetup.captureCharFrame(); expect(firstCursorFrame).toContain("2025-01-03"); // A keyboard cursor knows its column, not a level: the axis reads the // series value and no horizontal line crosses the plot. expect(firstCursorFrame).toContain("│"); expect(firstCursorFrame).toContain("$101"); expect(firstCursorFrame).not.toContain("────────"); await act(async () => chartShortcut?.(keyEvent("left"))); await act(async () => testSetup!.renderOnce()); expect(cursorChanges).toEqual([ "2025-01-03T00:00:00.000Z", "2025-01-02T00:00:00.000Z", ]); expect(testSetup.captureCharFrame()).toContain("2025-01-02"); await act(async () => chartShortcut?.(keyEvent("escape"))); await act(async () => testSetup!.renderOnce()); // Escape drops the cursor, so its date leaves the time axis with it. expect(testSetup.captureCharFrame()).not.toContain("2025-01-02"); }); test("does not emit the same snapped cursor timestamp twice", async () => { const cursorChanges: string[] = []; testSetup = await testRender( { if (date) cursorChanges.push(date.toISOString()); }} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => chartShortcut?.(keyEvent("right"))); await act(async () => chartShortcut?.(keyEvent("left"))); expect(cursorChanges).toEqual(["2025-01-01T00:00:00.000Z"]); }); test("zooms with plus and resets the interaction viewport with zero", async () => { const viewportChanges: Array<{ start: string; end: string } | null> = []; const viewportInteractions: string[] = []; testSetup = await testRender( { viewportInteractions.push(interaction); viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null); }} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("Jan 1"); expect(viewportChanges).toEqual([]); const zoomIn = keyEvent("="); zoomIn.sequence = "+"; zoomIn.shift = true; await act(async () => chartShortcut?.(zoomIn)); await act(async () => testSetup!.renderOnce()); expect(zoomIn.defaultPrevented).toBe(true); expect(testSetup.captureCharFrame()).not.toContain("Jan 1"); expect(viewportChanges.at(-1)?.start).not.toBe("2025-01-01T00:00:00.000Z"); expect(viewportInteractions.at(-1)).toBe("zoom"); await act(async () => chartShortcut?.(keyEvent("0"))); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("Jan 1"); expect(viewportChanges.at(-1)).toBeNull(); expect(viewportInteractions.at(-1)).toBe("reset"); }); test("keeps ownership of the window when zoom returns to the authored range", async () => { const viewportChanges: Array<{ start: string; end: string } | null> = []; testSetup = await testRender( viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null)} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); const zoomIn = keyEvent("="); zoomIn.sequence = "+"; zoomIn.shift = true; await act(async () => chartShortcut?.(zoomIn)); await act(async () => testSetup!.renderOnce()); expect(viewportChanges.at(-1)).not.toBeNull(); await act(async () => chartShortcut?.(keyEvent("-"))); await act(async () => testSetup!.renderOnce()); // Reporting null here would make an owner that echoes navigated ranges // back as the authored viewport reload its original range instead. expect(viewportChanges.at(-1)).toEqual({ start: "2025-01-01T00:00:00.000Z", end: "2025-01-09T00:00:00.000Z", }); }); test("preserves a zoomed viewport when adaptive data refreshes its buffer", async () => { const viewportChanges: Array<{ start: string; end: string } | null> = []; let replacePoints: ((points: TimeSeriesPoint[]) => void) | null = null; const initialPoints = series( "price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108], ).points; function Harness() { const [points, setPoints] = useState(initialPoints); replacePoints = setPoints; return ( viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null)} /> ); } testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); for (let index = 0; index < 2; index += 1) { const zoomIn = keyEvent("="); zoomIn.sequence = "+"; zoomIn.shift = true; await act(async () => chartShortcut?.(zoomIn)); await act(async () => testSetup!.renderOnce()); } const zoomedViewport = viewportChanges.at(-1); expect(zoomedViewport?.start).not.toBe("2025-01-01T00:00:00.000Z"); const changeCount = viewportChanges.length; await act(async () => replacePoints?.(initialPoints.slice(2))); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(viewportChanges).toHaveLength(changeCount); expect(viewportChanges.at(-1)).toEqual(zoomedViewport); }); test("keeps a panned viewport exactly where it was when backfill replaces the data", async () => { const viewportChanges: Array<{ start: string; end: string } | null> = []; const viewportInteractions: string[] = []; let replacePoints: ((points: TimeSeriesPoint[]) => void) | null = null; const initialPoints = series( "price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108], ).points; const olderPoints = Array.from({ length: 11 }, (_, index) => { const date = new Date( Date.UTC(2024, 11, 25 + index) - (index === 10 ? 60_000 : 0), ); return { date, observedAt: date, value: 90 + index }; }); function Harness() { const [points, setPoints] = useState(initialPoints); replacePoints = setPoints; return ( { viewportInteractions.push(interaction); viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null); }} /> ); } testSetup = await testRender(, { width: 62, height: 14 }); await act(async () => testSetup!.renderOnce()); for (let index = 0; index < 120; index += 1) { const panOlder = keyEvent("left"); panOlder.shift = true; await act(async () => chartShortcut?.(panOlder)); await act(async () => testSetup!.renderOnce()); } const pannedViewport = viewportChanges.at(-1); expect(pannedViewport).toEqual({ start: "2025-01-01T00:00:00.000Z", end: "2025-01-04T00:00:00.000Z", }); const changeCount = viewportChanges.length; await act(async () => replacePoints?.(olderPoints)); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); // The refreshed buffer ends a minute before the window does. The window // stays put and simply shows what is loaded; nothing is echoed upward. expect(viewportChanges).toHaveLength(changeCount); expect(viewportChanges.at(-1)).toEqual(pannedViewport); expect(viewportInteractions).not.toContain("sync"); expect(testSetup.captureCharFrame()).not.toContain("No chart data"); expect(testSetup.captureCharFrame()).not.toContain("Jan 9"); }); test("keeps the latest pan through delayed adaptive viewport and series echoes", async () => { const viewportChanges: Array<{ start: string; end: string } | null> = []; let publishViewport: (( next: { start: string; end: string }, showSecondary?: boolean, ) => void) | null = null; let publishExternalViewport: (() => void) | null = null; function Harness() { const [viewport, setViewport] = useState({ start: new Date("2025-01-05T00:00:00.000Z"), end: new Date("2025-01-09T00:00:00.000Z"), }); const [showSecondary, setShowSecondary] = useState(false); const [viewportResetKey, setViewportResetKey] = useState("price:1D:auto"); publishViewport = (next, nextShowSecondary = showSecondary) => { setViewport({ start: new Date(next.start), end: new Date(next.end), }); setShowSecondary(nextShowSecondary); }; publishExternalViewport = () => { setViewport({ start: new Date("2025-01-01T00:00:00.000Z"), end: new Date("2025-01-09T00:00:00.000Z"), }); setViewportResetKey("price:1W:auto"); }; return ( { viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null); }} /> ); } testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); for (let index = 0; index < 10; index += 1) { await act(async () => { capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, { scroll: { direction: "up", delta: 1 }, })); }); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); } const firstPan = viewportChanges[0]!; const latestPan = viewportChanges.at(-1)!; expect(firstPan).not.toBeNull(); expect(latestPan).not.toBeNull(); expect(latestPan).not.toEqual(firstPan); await act(async () => publishViewport!(firstPan, true)); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(viewportChanges).not.toContain(null); expect(viewportChanges.at(-1)).toEqual(latestPan); await act(async () => publishViewport!(latestPan)); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(viewportChanges).not.toContain(null); expect(viewportChanges.at(-1)).toEqual(latestPan); for (let index = 0; index < 32; index += 1) { await act(async () => { capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, { scroll: { direction: "up", delta: 100 }, })); }); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); } const boundaryViewport = viewportChanges.at(-1)!; expect(boundaryViewport).not.toBeNull(); await act(async () => publishViewport!(boundaryViewport)); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const boundaryChangeCount = viewportChanges.length; await act(async () => { capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, { scroll: { direction: "up", delta: 100 }, })); }); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(viewportChanges).toHaveLength(boundaryChangeCount); expect(viewportChanges).not.toContain(null); await act(async () => publishExternalViewport!()); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(viewportChanges.at(-1)).toBeNull(); }); test("activates and navigates a buffered viewport from the first mouse gesture", async () => { let activations = 0; const cursorChanges: string[] = []; testSetup = await testRender( { activations += 1; }} onCursorDateChange={(date) => { if (date) cursorChanges.push(date.toISOString()); }} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("Jan 5"); await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(10, 3)); capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 3)); capturedSurfaceProps!.onMouseUp(pointerEvent(30, 3)); }); await act(async () => testSetup!.renderOnce()); expect(activations).toBe(1); expect(cursorChanges.length).toBeGreaterThan(0); expect(testSetup.captureCharFrame()).toContain("Jan 3"); }); test("zooms to a shift-drag selection instead of panning", async () => { const viewportChanges: Array<{ start: string; end: string } | null> = []; testSetup = await testRender( { viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null); }} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(10, 3, { shift: true })); capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 3, { shift: true })); }); await act(async () => testSetup!.renderOnce()); // The selection must not pan the way an unmodified drag would, and the // range has to read as text for renderers that cannot draw the band. expect(viewportChanges).toHaveLength(0); expect(testSetup.captureCharFrame()).toContain("→"); await act(async () => { capturedSurfaceProps!.onMouseUp(pointerEvent(30, 3, { shift: true })); }); await act(async () => testSetup!.renderOnce()); const zoomed = viewportChanges.at(-1); expect(zoomed).toBeTruthy(); expect(new Date(zoomed!.start).getTime()).toBeGreaterThan(Date.parse("2025-01-01T00:00:00.000Z")); expect(new Date(zoomed!.end).getTime()).toBeLessThan(Date.parse("2025-01-09T00:00:00.000Z")); }); test("reports an alt-drag measurement without moving the viewport", async () => { const viewportChanges: Array<{ start: string; end: string } | null> = []; testSetup = await testRender( { viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null); }} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6, { alt: true })); capturedSurfaceProps!.onMouseDrag(pointerEvent(34, 1, { alt: true })); }); await act(async () => testSetup!.renderOnce()); const measuringFrame = testSetup.captureCharFrame(); // The readout sits in the middle of the box it measures, not in the legend. expect(measuringFrame).toContain("Δ"); expect(measuringFrame).toContain("bars"); expect(measuringFrame.split("\n")[0]).not.toContain("Δ"); expect(viewportChanges).toHaveLength(0); await act(async () => { capturedSurfaceProps!.onMouseUp(pointerEvent(34, 1, { alt: true })); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).not.toContain("Δ"); expect(viewportChanges).toHaveLength(0); }); test("arms a measurement from the keyboard when a modifier drag cannot reach the app", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => chartShortcut?.(keyEvent("m", true))); await act(async () => testSetup!.renderOnce()); // No modifier on the drag: the armed tool is what makes it a measurement. await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6)); capturedSurfaceProps!.onMouseDrag(pointerEvent(34, 1)); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("Δ"); await act(async () => { capturedSurfaceProps!.onMouseUp(pointerEvent(34, 1)); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).not.toContain("Δ"); // Sticky: the tool still owns the next drag until it is dismissed. await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6)); capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2)); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("Δ"); }); test("arms and disarms a chart tool from the toolbar", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); // Pressing the ruler icon has to arm the tool for an unmodified drag. const toolbarRow = testSetup.captureCharFrame().split("\n").findIndex((line) => line.includes("↔")); const rulerX = testSetup.captureCharFrame().split("\n")[toolbarRow]!.indexOf("↔"); await act(async () => { await testSetup!.mockMouse.moveTo(rulerX, toolbarRow); await testSetup!.mockMouse.click(rulerX, toolbarRow); }); await act(async () => testSetup!.renderOnce()); await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6)); capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2)); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("Δ"); await act(async () => { capturedSurfaceProps!.onMouseUp(pointerEvent(30, 2)); await testSetup!.mockMouse.click(rulerX, toolbarRow); }); await act(async () => testSetup!.renderOnce()); await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6)); capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2)); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).not.toContain("Δ"); }); test("draws a line, then grabs its end to reshape it", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => chartShortcut?.(keyEvent("d", true))); await act(async () => testSetup!.renderOnce()); const base = capturedSurfaceProps!.bitmaps?.[0] as { pixels: Uint8Array } | undefined; await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6)); capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2)); capturedSurfaceProps!.onMouseUp(pointerEvent(30, 2)); }); await act(async () => testSetup!.renderOnce()); const drawn = capturedSurfaceProps!.bitmaps?.[0] as { pixels: Uint8Array } | undefined; expect(Buffer.from(drawn!.pixels).equals(Buffer.from(base!.pixels))).toBe(false); // Grabbing the endpoint that was just dropped has to reshape, not redraw. await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6)); capturedSurfaceProps!.onMouseDrag(pointerEvent(14, 8)); capturedSurfaceProps!.onMouseUp(pointerEvent(14, 8)); }); await act(async () => testSetup!.renderOnce()); const reshaped = capturedSurfaceProps!.bitmaps?.[0] as { pixels: Uint8Array } | undefined; expect(reshaped).toBeTruthy(); expect(Buffer.from(reshaped!.pixels).equals(Buffer.from(drawn!.pixels))).toBe(false); }); test("places a trend line from the keyboard and leaves Backspace to the pane when nothing is picked", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); const bitmap = () => Buffer.from((capturedSurfaceProps!.bitmaps?.[0] as { pixels: Uint8Array }).pixels); const press = async (name: string, shift = false) => { const event = keyEvent(name, shift); await act(async () => chartShortcut?.(event)); await act(async () => testSetup!.renderOnce()); return event; }; const base = bitmap(); await press("d", true); // Enter anchors at the newest bar; Left walks the end back along the price. expect((await press("return")).defaultPrevented).toBe(true); await press("left"); await press("left"); await press("left"); expect(testSetup.captureCharFrame()).toContain("Δ -$3.00"); await press("return"); expect(testSetup.captureCharFrame()).not.toContain("Δ"); const drawn = bitmap(); expect(drawn.equals(base)).toBe(false); // Nothing picked and no tool in hand: Backspace is the pane's back key. await press("escape"); expect((await press("backspace")).defaultPrevented).toBeFalsy(); expect(bitmap().equals(base)).toBe(false); // With the line tool, ] picks the drawing and Backspace deletes it. await press("d", true); await press("]"); expect((await press("backspace")).defaultPrevented).toBe(true); expect(bitmap().equals(base)).toBe(true); }); test("steps the cursor of a focused chart that does not navigate", async () => { const cursorChanges: Array = []; testSetup = await testRender( cursorChanges.push(date?.toISOString() ?? null)} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => chartShortcut?.(keyEvent("left"))); expect(cursorChanges).toEqual(["2025-01-03T00:00:00.000Z"]); // Panning stays with charts that navigate, so the key is left alone. const pan = keyEvent("a"); await act(async () => chartShortcut?.(pan)); expect(pan.defaultPrevented).toBe(false); }); test("disarms a chart tool with escape", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => chartShortcut?.(keyEvent("z", true))); await act(async () => chartShortcut?.(keyEvent("escape"))); await act(async () => testSetup!.renderOnce()); // Disarmed: an unmodified drag pans instead of selecting a range. await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6)); capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 2)); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).not.toContain("→"); }); test("keeps a measurement readable in a narrow pane", async () => { testSetup = await testRender( , { width: 30, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(2, 6, { alt: true })); capturedSurfaceProps!.onMouseDrag(pointerEvent(14, 1, { alt: true })); }); await act(async () => testSetup!.renderOnce()); // Too narrow for the whole summary, but the change still has to be visible. expect(testSetup.captureCharFrame()).toContain("Δ"); }); test("ends a tool drag whose release landed on another pane", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(8, 6, { alt: true })); capturedSurfaceProps!.onMouseDrag(pointerEvent(34, 1, { alt: true })); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("Δ"); // No mouse-up arrives: the pointer was released over a floating pane. The // next plain move only happens with the button already up. await act(async () => { capturedSurfaceProps!.onMouseMove(pointerEvent(20, 4)); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).not.toContain("Δ"); }); test("maps the pointer crosshair level through both axes and labels its date", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); const plotWidth = capturedSurfaceNode!.width as number; const plotHeight = capturedSurfaceNode!.height as number; await act(async () => { capturedSurfaceProps!.onMouseMove(pointerEvent( (plotWidth - 1) * (2 / 3), (plotHeight - 1) / 2, )); }); await act(async () => testSetup!.renderOnce()); const frame = testSetup.captureCharFrame(); expect(frame).toContain("$150"); expect(frame).toContain("106%"); expect(frame).toContain("2025-01-03"); expect(frame).toContain("────────"); }); test.each([ ["USD", "currency-total:USD", 90_007_000_000, "$90.01B"], ["EUR", "currency-total:EUR", -12_345_600_000, "-€12.35B"], ["CAD", "currency-total:CAD", 123_456_000, "123.46M CAD"], ["USD", "price:USD", 1_234_567.89, "$1,234,567.89"], ["USD", "price:USD", 0.0123, "$0.0123"], ["USD", "price:USD", 1e30, "…"], ] as const)("renders a complete cursor marker for %s %s %s", async (unit, unitGroup, value, label) => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain(label); expect(capturedSurfaceNode!.width).toBeGreaterThanOrEqual(30); }); test("static overview charts reserve the complete tiny-price marker without losing the plot", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("$0.00000526"); expect(capturedSurfaceNode!.width).toBeGreaterThanOrEqual(30); }); test("preserves integer-domain cursor decimals without moving the plot and honors hidden axes", async () => { let setAxisWidth: ((value: number) => void) | null = null; function Harness() { const [axisWidth, setWidth] = useState(9); setAxisWidth = setWidth; return ; } testSetup = await testRender(, { width: 62, height: 14 }); await act(async () => testSetup!.renderOnce()); const plotWidth = capturedSurfaceNode!.width as number; const plotHeight = capturedSurfaceNode!.height as number; await act(async () => capturedSurfaceProps!.onMouseMove(pointerEvent( (plotWidth - 1) / 2, (plotHeight - 1) * 0.4975, ))); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("$150.28"); expect(capturedSurfaceNode!.width).toBe(plotWidth); await act(async () => setAxisWidth?.(0)); await act(async () => testSetup!.renderOnce()); expect(capturedSurfaceNode!.width).toBe(60); expect(testSetup.captureCharFrame()).not.toContain("$"); }); test("keeps the requested window while older cached observations partially overlap it", async () => { testSetup = await testRender( , { width: 92, height: 16 }, ); await act(async () => testSetup!.renderOnce()); const frame = testSetup.captureCharFrame(); expect(frame).toContain("Aug 10"); expect(frame).toContain("Sep 10"); expect(frame).not.toContain("Jul 31"); }); test("zooms around the mouse pointer with control-wheel", async () => { testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).toContain("Jan 9"); await act(async () => { capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, { ctrl: true, scroll: { direction: "up", delta: 4 }, })); }); await act(async () => testSetup!.renderOnce()); expect(testSetup.captureCharFrame()).not.toContain("Jan 9"); }); test("uses a supplied market timeline to keep control-wheel zoom under the pointer", async () => { const anchor = twoSessionCandles(); const display = { ...anchor, id: "secondary", label: "Secondary", timeBasis: undefined, }; const viewport = { start: new Date(anchor.points[0]!.date.getTime()), end: new Date(anchor.points.at(-1)!.date.getTime()), }; const viewportChanges: Array<{ start: string; end: string } | null> = []; testSetup = await testRender( viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null)} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); const pointerX = 35; const plotWidth = capturedSurfaceNode!.width as number; const frame = buildCompositeNavigationFrame([display], [anchor])!; const expected = zoomCompositeViewport( frame, viewport, 1 + 4 * 0.04, // The pointer reads in plot space, which is wider than the viewport by // the reserved right offset. pointerX / Math.max(plotWidth - 1, 1) * (1 + COMPOSITE_RIGHT_OFFSET_RATIO), ); await act(async () => { capturedSurfaceProps!.onMouseScroll(pointerEvent(pointerX, 3, { ctrl: true, scroll: { direction: "up", delta: 4 }, })); }); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(viewportChanges.at(-1)).toEqual({ start: expected.start.toISOString(), end: expected.end.toISOString(), }); }); test("returns a drag to its origin without handing the window back", async () => { const viewportChanges: Array<{ start: string; end: string } | null> = []; const interactions: string[] = []; testSetup = await testRender( { interactions.push(interaction); viewportChanges.push(next ? { start: next.start.toISOString(), end: next.end.toISOString() } : null); }} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => { capturedSurfaceProps!.onMouseDown(pointerEvent(20, 3)); capturedSurfaceProps!.onMouseDrag(pointerEvent(30, 3)); }); await act(async () => testSetup!.renderOnce()); expect(viewportChanges.at(-1)).not.toBeNull(); await act(async () => { capturedSurfaceProps!.onMouseDrag(pointerEvent(20, 3)); capturedSurfaceProps!.onMouseUp(pointerEvent(20, 3)); }); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(viewportChanges.at(-1)).toEqual({ start: "2025-01-05T00:00:00.000Z", end: "2025-01-09T00:00:00.000Z", }); expect(interactions.at(-1)).toBe("pan"); }); test("always treats horizontal control-wheel movement as a pan", async () => { const interactions: string[] = []; testSetup = await testRender( interactions.push(interaction)} /> , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); await act(async () => { capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, { ctrl: true, scroll: { direction: "left", delta: 4 }, })); }); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); expect(interactions.at(-1)).toBe("pan"); }); test("keeps the date axis and mouse recovery when refreshed data misses the zoomed window", async () => { let replacePoints: ((points: TimeSeriesPoint[]) => void) | null = null; let lastWindow: { start: Date; end: Date } | null = null; function Harness() { const [points, setPoints] = useState( series("price", "main", "left", "USD", [100, 101, 102, 103, 104, 105, 106, 107, 108]).points, ); replacePoints = setPoints; return ( { lastWindow = next; }} /> ); } testSetup = await testRender( , { width: 62, height: 14 }, ); await act(async () => testSetup!.renderOnce()); for (let index = 0; index < 8; index += 1) { await act(async () => { capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, { ctrl: true, scroll: { direction: "up", delta: 8 }, })); }); await act(async () => testSetup!.renderOnce()); } await act(async () => { replacePoints?.([ point("2025-01-01", 100), point("2025-01-09", 108), ]); }); await act(async () => testSetup!.renderOnce()); // An observation-free window keeps the chart frame and its axes; only the // plotted points go away. const emptyFrame = testSetup.captureCharFrame(); expect(emptyFrame).not.toContain("No chart data"); expect(emptyFrame).not.toContain("•"); expect(emptyFrame).toContain("$108"); expect(emptyFrame).toContain("Jan "); expect(typeof capturedSurfaceProps!.onMouseScroll).toBe("function"); await act(async () => { capturedSurfaceProps!.onMouseScroll(pointerEvent(25, 3, { ctrl: true, scroll: { direction: "down", delta: 4 }, })); }); await act(async () => testSetup!.renderOnce()); const recoveredFrame = testSetup.captureCharFrame(); expect(recoveredFrame).toContain("•"); expect(lastWindow!.start.getTime()).toBeLessThanOrEqual(Date.parse("2025-01-01")); expect(lastWindow!.end.getTime()).toBeGreaterThanOrEqual(Date.parse("2025-01-09")); }); test("restores persisted drawings from pane settings on mount", async () => { const renderWithSettings = async (settings: Record) => { const config = createDefaultConfig("/tmp/gloomberb-composite-drawings"); config.layout.instances.push({ instanceId: "chart:test", paneId: "chart-composer", title: "Chart", binding: { kind: "fixed", symbol: "ACME" }, settings, }); testSetup = await testRender( {} }}> , { width: 62, height: 14 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const bitmap = capturedSurfaceProps!.bitmaps?.[0] as { pixels: Uint8Array }; const pixels = Buffer.from(bitmap.pixels); await act(async () => testSetup!.renderer.destroy()); testSetup = undefined; return pixels; }; const blank = await renderWithSettings({}); const restored = await renderWithSettings({ chartDrawings: [{ id: "drawing-1", panelId: "main", color: "#f5a524", points: [ { time: Date.UTC(2025, 0, 2), value: 101 }, { time: Date.UTC(2025, 0, 8), value: 107 }, ], }], }); expect(restored.equals(blank)).toBe(false); }); test("shows useful UTC times for an intraday shared cursor and time axis", async () => { const intraday = { ...series("price", "main", "left", "USD", []), points: [ timestampPoint("2025-01-02T09:30:00.000Z", 100), timestampPoint("2025-01-02T12:05:00.000Z", 103), timestampPoint("2025-01-02T16:00:00.000Z", 101), ], }; testSetup = await testRender( , { width: 80, height: 14 }, ); await act(async () => { await testSetup!.renderOnce(); await testSetup!.renderOnce(); }); const frame = testSetup.captureCharFrame(); expect(frame).toContain("12:05 UTC"); expect(frame).toContain("09:30 UTC"); expect(frame).toContain("16:00 UTC"); }); });