gloomberb 0.3.3 → 0.3.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -8
- package/package.json +1 -1
- package/src/components/chart/chart-renderer.ts +7 -2
- package/src/components/chart/chart.test.ts +26 -0
- package/src/components/chart/stock-chart.tsx +36 -13
- package/src/plugins/ibkr/gateway-service.test.ts +30 -0
- package/src/plugins/ibkr/gateway-service.ts +38 -2
- package/src/plugins/prediction-markets/chart.test.tsx +139 -0
- package/src/plugins/prediction-markets/chart.tsx +65 -436
- package/src/plugins/prediction-markets/controller-data.ts +6 -2
- package/src/plugins/prediction-markets/detail/outcomes.tsx +2 -6
- package/src/plugins/prediction-markets/detail/pane.tsx +4 -25
- package/src/plugins/prediction-markets/detail/shared.tsx +0 -1
- package/src/plugins/prediction-markets/detail.test.tsx +4 -0
- package/src/plugins/prediction-markets/pane.test.tsx +41 -1
- package/src/plugins/prediction-markets/pane.tsx +1 -31
- package/src/sources/provider-router.test.ts +43 -0
- package/src/sources/provider-router.ts +8 -6
- package/src/utils/price-history.test.ts +25 -0
- package/src/utils/price-history.ts +29 -5
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -25,20 +25,16 @@ For the best experience, use a [Kitty](https://sw.kovidgoyal.net/kitty/)-compati
|
|
|
25
25
|
## 🚀 Install
|
|
26
26
|
|
|
27
27
|
```bash
|
|
28
|
-
bun install -g gloomberb
|
|
29
|
-
# or
|
|
30
28
|
curl -fsSL gloomberb.com/install | bash
|
|
29
|
+
# or
|
|
30
|
+
bun install -g gloomberb
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
Then run `gloomberb
|
|
34
|
-
|
|
35
|
-
When developing from source, launch with a real Bun runtime, for example `bun src/index.tsx`.
|
|
36
|
-
If `bun --help` prints Gloomberb help instead of Bun help, your Bun binary has been overwritten and needs to be reinstalled.
|
|
37
|
-
In-app self-updates are disabled while running from source so Gloomberb does not replace the Bun executable.
|
|
33
|
+
Then run `gloomberb`.
|
|
38
34
|
|
|
39
35
|
## 🧩 Plugins
|
|
40
36
|
|
|
41
|
-
|
|
37
|
+
Everything, from the portfolio list to broker integrations, is a plugin.
|
|
42
38
|
|
|
43
39
|
Plugins can add tabs, columns, commands, status bar widgets, and more.
|
|
44
40
|
|
|
@@ -98,3 +94,7 @@ Toggleable plugins can be enabled/disabled from the command bar screen (`Ctrl+p`
|
|
|
98
94
|
## License
|
|
99
95
|
|
|
100
96
|
MIT
|
|
97
|
+
|
|
98
|
+
## Credits
|
|
99
|
+
|
|
100
|
+
- [OpenTUI](https://opentui.com/) for the layout
|
package/package.json
CHANGED
|
@@ -917,6 +917,7 @@ export interface RenderChartOptions {
|
|
|
917
917
|
axisMode?: ChartAxisMode;
|
|
918
918
|
currency?: string;
|
|
919
919
|
colors: ResolvedChartPalette;
|
|
920
|
+
timeAxisDates?: Array<Date | string | number>;
|
|
920
921
|
}
|
|
921
922
|
|
|
922
923
|
export interface ChartScene {
|
|
@@ -1027,6 +1028,8 @@ export function buildChartScene(
|
|
|
1027
1028
|
? null
|
|
1028
1029
|
: max - (cursorY / Math.max(chartRows - 1, 1)) * range;
|
|
1029
1030
|
|
|
1031
|
+
const timeAxisDates = opts.timeAxisDates ?? points.map((point) => point.date);
|
|
1032
|
+
|
|
1030
1033
|
return {
|
|
1031
1034
|
points,
|
|
1032
1035
|
width: opts.width,
|
|
@@ -1045,7 +1048,7 @@ export function buildChartScene(
|
|
|
1045
1048
|
dateAtCursor: activePoint.date,
|
|
1046
1049
|
changeAtCursor: activePoint.close - points[0]!.close,
|
|
1047
1050
|
changePctAtCursor: points[0]!.close ? ((activePoint.close - points[0]!.close) / points[0]!.close) * 100 : 0,
|
|
1048
|
-
timeLabels: buildTimeAxis(
|
|
1051
|
+
timeLabels: buildTimeAxis(timeAxisDates, opts.width),
|
|
1049
1052
|
cursorX,
|
|
1050
1053
|
cursorY,
|
|
1051
1054
|
cursorColumn,
|
|
@@ -1138,10 +1141,12 @@ export function renderChart(
|
|
|
1138
1141
|
);
|
|
1139
1142
|
}
|
|
1140
1143
|
|
|
1144
|
+
const timeAxisDates = opts.timeAxisDates ?? points.map((point) => point.date);
|
|
1145
|
+
|
|
1141
1146
|
return {
|
|
1142
1147
|
lines: bufferToBrailleLines(buf, palette.bgColor),
|
|
1143
1148
|
axisLabels: [...axisLabelsByRow.entries()].map(([row, label]) => ({ row, label })),
|
|
1144
|
-
timeLabels: buildTimeAxis(
|
|
1149
|
+
timeLabels: buildTimeAxis(timeAxisDates, width),
|
|
1145
1150
|
activePoint,
|
|
1146
1151
|
priceAtCursor,
|
|
1147
1152
|
crosshairPrice,
|
|
@@ -206,6 +206,32 @@ describe("renderChart", () => {
|
|
|
206
206
|
expect(buildTimeAxis(yearlyDates, 48)).toBe("2020 2021 2022 2023 2024 2025");
|
|
207
207
|
});
|
|
208
208
|
|
|
209
|
+
test("uses source time-axis dates instead of projected extrema dates when provided", () => {
|
|
210
|
+
const sourceHistory = Array.from({ length: 96 }, (_, index) => ({
|
|
211
|
+
date: new Date(Date.UTC(2026, 2, 25, index * 3)),
|
|
212
|
+
close: index % 2 === 0 ? 0.7 + index * 0.001 : 0.3 + index * 0.001,
|
|
213
|
+
volume: 100 + index,
|
|
214
|
+
})) as PricePoint[];
|
|
215
|
+
const sourceDates = sourceHistory.map((point) => point.date);
|
|
216
|
+
const projection = projectChartData(sourceHistory, 48, "area", true);
|
|
217
|
+
|
|
218
|
+
const result = renderChart(projection.points, {
|
|
219
|
+
width: 48,
|
|
220
|
+
height: 8,
|
|
221
|
+
showVolume: false,
|
|
222
|
+
volumeHeight: 0,
|
|
223
|
+
cursorX: null,
|
|
224
|
+
cursorY: null,
|
|
225
|
+
mode: "area",
|
|
226
|
+
axisMode: "price",
|
|
227
|
+
currency: "USD",
|
|
228
|
+
colors: palette,
|
|
229
|
+
timeAxisDates: sourceDates,
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
expect(result.timeLabels).toBe(buildTimeAxis(sourceDates, 48));
|
|
233
|
+
});
|
|
234
|
+
|
|
209
235
|
test("maps active bucket metadata across all render modes", () => {
|
|
210
236
|
const cases: Array<{ mode: ChartRenderMode; width: number; cursorX: number }> = [
|
|
211
237
|
{ mode: "area", width: 12, cursorX: 4 },
|
|
@@ -69,6 +69,8 @@ interface StockChartProps {
|
|
|
69
69
|
interactive?: boolean;
|
|
70
70
|
compact?: boolean;
|
|
71
71
|
axisMode?: ChartAxisMode;
|
|
72
|
+
historyOverride?: PricePoint[] | null;
|
|
73
|
+
currencyOverride?: string | null;
|
|
72
74
|
}
|
|
73
75
|
|
|
74
76
|
interface DragState {
|
|
@@ -505,7 +507,16 @@ function resolveSelectionCursor(
|
|
|
505
507
|
};
|
|
506
508
|
}
|
|
507
509
|
|
|
508
|
-
export function StockChart({
|
|
510
|
+
export function StockChart({
|
|
511
|
+
width,
|
|
512
|
+
height,
|
|
513
|
+
focused,
|
|
514
|
+
interactive,
|
|
515
|
+
compact,
|
|
516
|
+
axisMode = "price",
|
|
517
|
+
historyOverride = null,
|
|
518
|
+
currencyOverride = null,
|
|
519
|
+
}: StockChartProps) {
|
|
509
520
|
const renderer = useRenderer();
|
|
510
521
|
const { state, dispatch } = useAppState();
|
|
511
522
|
const paneId = usePaneInstanceId();
|
|
@@ -637,9 +648,10 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
637
648
|
const chartWidth = Math.max(width - axisWidth - axisGap, compact ? 12 : 20);
|
|
638
649
|
const maxCursorX = chartWidth - 1;
|
|
639
650
|
const panStep = Math.max(Math.floor(chartWidth / 10), 1);
|
|
640
|
-
const baseHistory =
|
|
641
|
-
|
|
642
|
-
|
|
651
|
+
const baseHistory = historyOverride
|
|
652
|
+
?? (rangeHistory && rangeHistory.length > 0
|
|
653
|
+
? rangeHistory
|
|
654
|
+
: (financials?.priceHistory ?? []));
|
|
643
655
|
const detailRequest = useMemo(() => {
|
|
644
656
|
if (compact || !instrumentRef || baseHistory.length < 2 || viewState.zoomLevel <= 1) return null;
|
|
645
657
|
const window = getVisibleWindow(baseHistory, viewState, chartWidth);
|
|
@@ -751,11 +763,18 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
751
763
|
queueMicrotask(() => renderer.requestRender());
|
|
752
764
|
}, [chartHeight, chartWidth, compact, historyRenderKey, renderer, ticker?.metadata.ticker, viewState.renderMode]);
|
|
753
765
|
|
|
754
|
-
const chartWindow = useMemo(() =>
|
|
755
|
-
|
|
766
|
+
const chartWindow = useMemo(() => {
|
|
767
|
+
if (historyOverride) {
|
|
768
|
+
return { points: history, startIdx: 0, endIdx: history.length };
|
|
769
|
+
}
|
|
770
|
+
return isDetailView
|
|
756
771
|
? { points: history, startIdx: 0, endIdx: history.length }
|
|
757
|
-
: getVisibleWindow(history, viewState, chartWidth)
|
|
758
|
-
|
|
772
|
+
: getVisibleWindow(history, viewState, chartWidth);
|
|
773
|
+
}, [chartWidth, history, historyOverride, isDetailView, viewState.panOffset, viewState.timeRange, viewState.zoomLevel]);
|
|
774
|
+
const timeAxisDates = useMemo(
|
|
775
|
+
() => chartWindow.points.map((point) => point.date),
|
|
776
|
+
[chartWindow.points],
|
|
777
|
+
);
|
|
759
778
|
|
|
760
779
|
const projection = useMemo(() => (
|
|
761
780
|
projectChartData(chartWindow.points, chartWidth, viewState.renderMode, !!compact)
|
|
@@ -891,7 +910,7 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
891
910
|
negative: colors.negative,
|
|
892
911
|
}, trend);
|
|
893
912
|
}, [chartWindow.points]);
|
|
894
|
-
const chartCurrency = financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
|
|
913
|
+
const chartCurrency = currencyOverride ?? financials?.quote?.currency ?? ticker?.metadata.currency ?? "USD";
|
|
895
914
|
|
|
896
915
|
const cursorX = viewState.cursorX !== null ? clamp(viewState.cursorX, 0, chartWidth - 1) : null;
|
|
897
916
|
const cursorY = viewState.cursorY !== null ? clamp(viewState.cursorY, 0, chartHeight - 1) : null;
|
|
@@ -918,7 +937,8 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
918
937
|
mode: projection.effectiveMode,
|
|
919
938
|
axisMode,
|
|
920
939
|
colors: chartColors,
|
|
921
|
-
|
|
940
|
+
timeAxisDates,
|
|
941
|
+
}), [axisMode, chartColors, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, selectionCursorX, selectionCursorY, showVolume, timeAxisDates, volumeHeight]);
|
|
922
942
|
const snappedSelectionCursorX = selectionScene
|
|
923
943
|
? getPointTerminalColumn(selectionScene.activeIdx, projection.points.length, chartWidth, projection.effectiveMode)
|
|
924
944
|
: null;
|
|
@@ -962,7 +982,8 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
962
982
|
mode: projection.effectiveMode,
|
|
963
983
|
axisMode,
|
|
964
984
|
colors: chartColors,
|
|
965
|
-
|
|
985
|
+
timeAxisDates,
|
|
986
|
+
}), [axisMode, chartColors, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
966
987
|
|
|
967
988
|
const rendererState = resolveChartRendererState(preferredRenderer, kittySupport, renderer.resolution);
|
|
968
989
|
const effectiveRenderer: ResolvedChartRenderer = rendererState.renderer;
|
|
@@ -978,7 +999,8 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
978
999
|
axisMode,
|
|
979
1000
|
currency: chartCurrency,
|
|
980
1001
|
colors: chartColors,
|
|
981
|
-
|
|
1002
|
+
timeAxisDates,
|
|
1003
|
+
}), [axisMode, chartColors, chartCurrency, chartHeight, chartWidth, compact, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
982
1004
|
|
|
983
1005
|
const interactiveResult = useMemo(() => (
|
|
984
1006
|
effectiveRenderer === "kitty"
|
|
@@ -994,8 +1016,9 @@ export function StockChart({ width, height, focused, interactive, compact, axisM
|
|
|
994
1016
|
axisMode,
|
|
995
1017
|
currency: chartCurrency,
|
|
996
1018
|
colors: chartColors,
|
|
1019
|
+
timeAxisDates,
|
|
997
1020
|
})
|
|
998
|
-
), [axisMode, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, projection.effectiveMode, projection.points, showVolume, volumeHeight]);
|
|
1021
|
+
), [axisMode, chartColors, chartCurrency, chartHeight, chartWidth, compact, displayCursorX, displayCursorY, effectiveRenderer, projection.effectiveMode, projection.points, showVolume, timeAxisDates, volumeHeight]);
|
|
999
1022
|
|
|
1000
1023
|
const result = effectiveRenderer === "kitty" ? staticResult : interactiveResult!;
|
|
1001
1024
|
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
applyTickByTickBidAskToQuote,
|
|
7
7
|
diagnoseLocalIbkrPortIssue,
|
|
8
8
|
IbkrGatewayService,
|
|
9
|
+
parseIbkrHistoricalBarTime,
|
|
9
10
|
resolveGatewayConnection,
|
|
10
11
|
summarizeBrokerAccount,
|
|
11
12
|
} from "./gateway-service";
|
|
@@ -239,6 +240,35 @@ describe("tick-by-tick quote updates", () => {
|
|
|
239
240
|
});
|
|
240
241
|
});
|
|
241
242
|
|
|
243
|
+
describe("parseIbkrHistoricalBarTime", () => {
|
|
244
|
+
test("parses compact day bars without treating them as epoch seconds", () => {
|
|
245
|
+
const date = parseIbkrHistoricalBarTime(20250328);
|
|
246
|
+
|
|
247
|
+
expect(Number.isNaN(date.getTime())).toBe(false);
|
|
248
|
+
expect(date.getFullYear()).toBe(2025);
|
|
249
|
+
expect(date.getMonth()).toBe(2);
|
|
250
|
+
expect(date.getDate()).toBe(28);
|
|
251
|
+
expect(date.getHours()).toBe(0);
|
|
252
|
+
expect(date.getMinutes()).toBe(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
test("parses compact intraday bar timestamps", () => {
|
|
256
|
+
const date = parseIbkrHistoricalBarTime("20250328 09:30:00");
|
|
257
|
+
|
|
258
|
+
expect(Number.isNaN(date.getTime())).toBe(false);
|
|
259
|
+
expect(date.getFullYear()).toBe(2025);
|
|
260
|
+
expect(date.getMonth()).toBe(2);
|
|
261
|
+
expect(date.getDate()).toBe(28);
|
|
262
|
+
expect(date.getHours()).toBe(9);
|
|
263
|
+
expect(date.getMinutes()).toBe(30);
|
|
264
|
+
expect(date.getSeconds()).toBe(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
test("preserves epoch-second timestamps for streaming-style bars", () => {
|
|
268
|
+
expect(parseIbkrHistoricalBarTime(1_700_000_000).getTime()).toBe(1_700_000_000_000);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
242
272
|
describe("diagnoseLocalIbkrPortIssue", () => {
|
|
243
273
|
test("reports a detected local IBKR listener on a different port", async () => {
|
|
244
274
|
const issue = await diagnoseLocalIbkrPortIssue({
|
|
@@ -310,6 +310,42 @@ const GENERIC_BAR_SIZE_MAP: Record<string, BarSizeSetting> = {
|
|
|
310
310
|
"1w": BarSizeSetting.WEEKS_ONE,
|
|
311
311
|
};
|
|
312
312
|
|
|
313
|
+
export function parseIbkrHistoricalBarTime(value: string | number): Date {
|
|
314
|
+
if (typeof value === "number") {
|
|
315
|
+
if (!Number.isFinite(value)) return new Date(Number.NaN);
|
|
316
|
+
|
|
317
|
+
const compactDate = String(Math.trunc(value));
|
|
318
|
+
if (/^\d{8}$/.test(compactDate)) {
|
|
319
|
+
return parseIbkrHistoricalBarTime(compactDate);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
return new Date(value > 10_000_000_000 ? value : value * 1000);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
const trimmed = value.trim();
|
|
326
|
+
if (!trimmed) return new Date(Number.NaN);
|
|
327
|
+
|
|
328
|
+
const parsed = Date.parse(trimmed);
|
|
329
|
+
if (Number.isFinite(parsed)) {
|
|
330
|
+
return new Date(parsed);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
const compactMatch = trimmed.match(/^(\d{4})(\d{2})(\d{2})(?:\D+(\d{2}):(\d{2}):(\d{2}))?/);
|
|
334
|
+
if (!compactMatch) {
|
|
335
|
+
return new Date(Number.NaN);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
const [, yearText, monthText, dayText, hourText, minuteText, secondText] = compactMatch;
|
|
339
|
+
const year = Number(yearText);
|
|
340
|
+
const month = Number(monthText) - 1;
|
|
341
|
+
const day = Number(dayText);
|
|
342
|
+
const hour = Number(hourText ?? "0");
|
|
343
|
+
const minute = Number(minuteText ?? "0");
|
|
344
|
+
const second = Number(secondText ?? "0");
|
|
345
|
+
|
|
346
|
+
return new Date(year, month, day, hour, minute, second);
|
|
347
|
+
}
|
|
348
|
+
|
|
313
349
|
function normalizeQuoteStreamTarget(target: QuoteSubscriptionTarget): QuoteSubscriptionTarget | null {
|
|
314
350
|
const symbol = target.symbol.trim().toUpperCase();
|
|
315
351
|
if (!symbol) return null;
|
|
@@ -1006,7 +1042,7 @@ export class IbkrGatewayService {
|
|
|
1006
1042
|
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1007
1043
|
|
|
1008
1044
|
return bars.map((bar) => ({
|
|
1009
|
-
date:
|
|
1045
|
+
date: parseIbkrHistoricalBarTime(bar.time),
|
|
1010
1046
|
open: normalizeIbkrPriceValue(bar.open, priceDivisor),
|
|
1011
1047
|
high: normalizeIbkrPriceValue(bar.high, priceDivisor),
|
|
1012
1048
|
low: normalizeIbkrPriceValue(bar.low, priceDivisor),
|
|
@@ -1064,7 +1100,7 @@ export class IbkrGatewayService {
|
|
|
1064
1100
|
const priceDivisor = getIbkrPriceDivisor(contract, details);
|
|
1065
1101
|
|
|
1066
1102
|
return bars.map((bar) => ({
|
|
1067
|
-
date:
|
|
1103
|
+
date: parseIbkrHistoricalBarTime(bar.time),
|
|
1068
1104
|
open: normalizeIbkrPriceValue(bar.open, priceDivisor),
|
|
1069
1105
|
high: normalizeIbkrPriceValue(bar.high, priceDivisor),
|
|
1070
1106
|
low: normalizeIbkrPriceValue(bar.low, priceDivisor),
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ScrollBoxRenderable } from "@opentui/core";
|
|
3
|
+
import { createTestRenderer } from "@opentui/core/testing";
|
|
4
|
+
import { createRoot } from "@opentui/react";
|
|
5
|
+
import { act, useEffect, useReducer, useRef } from "react";
|
|
6
|
+
import {
|
|
7
|
+
AppContext,
|
|
8
|
+
PaneInstanceProvider,
|
|
9
|
+
appReducer,
|
|
10
|
+
createInitialState,
|
|
11
|
+
} from "../../state/app-context";
|
|
12
|
+
import { createDefaultConfig } from "../../types/config";
|
|
13
|
+
import { getNativeSurfaceManager } from "../../components/chart/native/surface-manager";
|
|
14
|
+
import { PredictionMarketChart } from "./chart";
|
|
15
|
+
|
|
16
|
+
const TEST_PANE_ID = "prediction-scroll:test";
|
|
17
|
+
const SURFACE_ID = `chart-surface:${TEST_PANE_ID}:compact:base`;
|
|
18
|
+
|
|
19
|
+
let testSetup: Awaited<ReturnType<typeof createTestRenderer>> | undefined;
|
|
20
|
+
let root: ReturnType<typeof createRoot> | undefined;
|
|
21
|
+
let scrollBoxRef: ScrollBoxRenderable | null = null;
|
|
22
|
+
const actEnvironment = globalThis as typeof globalThis & {
|
|
23
|
+
IS_REACT_ACT_ENVIRONMENT?: boolean;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
function ChartScrollHarness() {
|
|
27
|
+
const [state, dispatch] = useReducer(
|
|
28
|
+
appReducer,
|
|
29
|
+
(() => {
|
|
30
|
+
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
31
|
+
config.chartPreferences.renderer = "kitty";
|
|
32
|
+
const initial = createInitialState(config);
|
|
33
|
+
initial.focusedPaneId = TEST_PANE_ID;
|
|
34
|
+
return initial;
|
|
35
|
+
})(),
|
|
36
|
+
);
|
|
37
|
+
const scrollRef = useRef<ScrollBoxRenderable>(null);
|
|
38
|
+
|
|
39
|
+
useEffect(() => {
|
|
40
|
+
scrollBoxRef = scrollRef.current;
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<AppContext value={{ state, dispatch }}>
|
|
45
|
+
<PaneInstanceProvider paneId={TEST_PANE_ID}>
|
|
46
|
+
<scrollbox ref={scrollRef} height={10} scrollY>
|
|
47
|
+
<box flexDirection="column">
|
|
48
|
+
<box height={14}>
|
|
49
|
+
<text>filler</text>
|
|
50
|
+
</box>
|
|
51
|
+
<PredictionMarketChart
|
|
52
|
+
history={[
|
|
53
|
+
{ date: new Date("2026-04-01T00:00:00Z"), close: 0.45 },
|
|
54
|
+
{ date: new Date("2026-04-02T00:00:00Z"), close: 0.48 },
|
|
55
|
+
{ date: new Date("2026-04-03T00:00:00Z"), close: 0.51 },
|
|
56
|
+
{ date: new Date("2026-04-04T00:00:00Z"), close: 0.49 },
|
|
57
|
+
]}
|
|
58
|
+
width={60}
|
|
59
|
+
height={12}
|
|
60
|
+
range="1M"
|
|
61
|
+
onRangeSelect={() => {}}
|
|
62
|
+
/>
|
|
63
|
+
</box>
|
|
64
|
+
</scrollbox>
|
|
65
|
+
</PaneInstanceProvider>
|
|
66
|
+
</AppContext>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async function flushFrames(count = 4) {
|
|
71
|
+
for (let index = 0; index < count; index += 1) {
|
|
72
|
+
await act(async () => {
|
|
73
|
+
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
74
|
+
await Promise.resolve();
|
|
75
|
+
await testSetup!.renderOnce();
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
afterEach(() => {
|
|
81
|
+
scrollBoxRef = null;
|
|
82
|
+
if (root) {
|
|
83
|
+
act(() => {
|
|
84
|
+
root!.unmount();
|
|
85
|
+
});
|
|
86
|
+
root = undefined;
|
|
87
|
+
}
|
|
88
|
+
if (testSetup) {
|
|
89
|
+
testSetup.renderer.destroy();
|
|
90
|
+
testSetup = undefined;
|
|
91
|
+
}
|
|
92
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = false;
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe("PredictionMarketChart kitty scrolling", () => {
|
|
96
|
+
test("creates a native chart surface when scrolled into view", async () => {
|
|
97
|
+
actEnvironment.IS_REACT_ACT_ENVIRONMENT = true;
|
|
98
|
+
testSetup = await createTestRenderer({ width: 100, height: 24 });
|
|
99
|
+
(testSetup.renderer as { _capabilities: unknown })._capabilities = {
|
|
100
|
+
kitty_graphics: true,
|
|
101
|
+
};
|
|
102
|
+
(testSetup.renderer as { _resolution: unknown })._resolution = {
|
|
103
|
+
width: 1000,
|
|
104
|
+
height: 720,
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
root = createRoot(testSetup.renderer);
|
|
108
|
+
act(() => {
|
|
109
|
+
root!.render(<ChartScrollHarness />);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
await flushFrames();
|
|
113
|
+
|
|
114
|
+
const manager = getNativeSurfaceManager(testSetup.renderer as never) as unknown as {
|
|
115
|
+
surfaces: Map<
|
|
116
|
+
string,
|
|
117
|
+
{
|
|
118
|
+
snapshot: {
|
|
119
|
+
visibleRect: { x: number; y: number; width: number; height: number } | null;
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
>;
|
|
123
|
+
};
|
|
124
|
+
|
|
125
|
+
const hiddenSurface = manager.surfaces.get(SURFACE_ID);
|
|
126
|
+
expect(hiddenSurface).toBeUndefined();
|
|
127
|
+
|
|
128
|
+
act(() => {
|
|
129
|
+
scrollBoxRef!.scrollTop = 14;
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
await flushFrames();
|
|
133
|
+
|
|
134
|
+
const visibleSurface = manager.surfaces.get(SURFACE_ID);
|
|
135
|
+
expect(visibleSurface).toBeDefined();
|
|
136
|
+
expect(visibleSurface?.snapshot.visibleRect).not.toBeNull();
|
|
137
|
+
expect(testSetup.captureCharFrame()).toContain("1M");
|
|
138
|
+
});
|
|
139
|
+
});
|