gloomberb 0.7.2 → 0.7.3
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 +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +49 -15
- package/src/components/chart/chart-types.ts +2 -2
- package/src/components/chart/chart.test.ts +27 -8
- package/src/components/chart/comparison-stock-chart.tsx +84 -121
- package/src/components/chart/native/chart-rasterizer.ts +43 -17
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +1 -1
- package/src/components/chart/stock-chart.tsx +59 -41
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +528 -383
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +1 -0
- package/src/components/command-bar/view-model.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +11 -51
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator.ts +8 -1
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +258 -44
- package/src/plugins/builtin/chat.test.tsx +257 -6
- package/src/plugins/builtin/chat.tsx +225 -60
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/news-wire/index.tsx +1 -1
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +11 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +117 -18
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +282 -2
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +252 -41
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +0 -6
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +442 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/state/app-bootstrap.ts +3 -3
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +128 -0
- package/src/utils/api-client.ts +112 -7
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
|
@@ -5,6 +5,7 @@ import { join } from "path";
|
|
|
5
5
|
import { act, useReducer } from "react";
|
|
6
6
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
7
7
|
import { AppPersistence } from "../../data/app-persistence";
|
|
8
|
+
import { TickerRepository } from "../../data/ticker-repository";
|
|
8
9
|
import { AppContext, appReducer, createInitialState, PaneInstanceProvider, type AppAction } from "../../state/app-context";
|
|
9
10
|
import { AssetDataRouter } from "../../sources/provider-router";
|
|
10
11
|
import { cloneLayout, createDefaultConfig, type AppConfig, type BrokerInstanceConfig } from "../../types/config";
|
|
@@ -12,8 +13,10 @@ import type { DataProvider } from "../../types/data-provider";
|
|
|
12
13
|
import type { Quote } from "../../types/financials";
|
|
13
14
|
import type { TickerRecord } from "../../types/ticker";
|
|
14
15
|
import { MarketDataCoordinator, setSharedMarketDataCoordinator } from "../../market-data/coordinator";
|
|
16
|
+
import { instrumentFromTicker } from "../../market-data/request-types";
|
|
15
17
|
import { createTestPluginRuntime } from "../../test-support/plugin-runtime";
|
|
16
18
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "../plugin-runtime";
|
|
19
|
+
import { PluginRegistry, setSharedMarketDataForTests, setSharedRegistryForTests } from "../registry";
|
|
17
20
|
import type { BrokerAdapter } from "../../types/broker";
|
|
18
21
|
import { portfolioListPlugin } from "./portfolio-list";
|
|
19
22
|
|
|
@@ -24,6 +27,7 @@ let harnessDispatch: React.Dispatch<AppAction> | null = null;
|
|
|
24
27
|
let sharedCoordinator: MarketDataCoordinator | null = null;
|
|
25
28
|
let harnessState: ReturnType<typeof createInitialState> | null = null;
|
|
26
29
|
const tempPaths: string[] = [];
|
|
30
|
+
const tempPersistences: AppPersistence[] = [];
|
|
27
31
|
|
|
28
32
|
const PortfolioPane = portfolioListPlugin.panes![0]!.component as (props: {
|
|
29
33
|
paneId: string;
|
|
@@ -154,6 +158,75 @@ function createPortfolioConfigWithColumns(
|
|
|
154
158
|
}
|
|
155
159
|
return config;
|
|
156
160
|
}
|
|
161
|
+
|
|
162
|
+
function createManualCollectionConfig(collectionId: string): AppConfig {
|
|
163
|
+
const config = createDefaultConfig("/tmp/gloomberb-portfolio-list");
|
|
164
|
+
const layout = {
|
|
165
|
+
dockRoot: { kind: "pane" as const, instanceId: TEST_PANE_ID },
|
|
166
|
+
instances: [{
|
|
167
|
+
instanceId: TEST_PANE_ID,
|
|
168
|
+
paneId: "portfolio-list",
|
|
169
|
+
binding: { kind: "none" as const },
|
|
170
|
+
params: { collectionId },
|
|
171
|
+
}],
|
|
172
|
+
floating: [],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
...config,
|
|
177
|
+
layout,
|
|
178
|
+
layouts: [{ name: "Default", layout: cloneLayout(layout) }],
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function installQuickAddRegistry(provider: DataProvider): PluginRegistry {
|
|
183
|
+
const persistence = new AppPersistence(createTempDbPath("quick-add"));
|
|
184
|
+
tempPersistences.push(persistence);
|
|
185
|
+
const registry = new PluginRegistry(provider, new TickerRepository(persistence.tickers), persistence);
|
|
186
|
+
registry.getConfigFn = () => harnessState?.config ?? createDefaultConfig("/tmp/gloomberb-portfolio-list");
|
|
187
|
+
return registry;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function createQuickAddProvider(match = true): DataProvider {
|
|
191
|
+
return {
|
|
192
|
+
id: "quick-add-test",
|
|
193
|
+
name: "Quick Add Test",
|
|
194
|
+
async getTickerFinancials() {
|
|
195
|
+
return {
|
|
196
|
+
annualStatements: [],
|
|
197
|
+
quarterlyStatements: [],
|
|
198
|
+
priceHistory: [],
|
|
199
|
+
quote: makeQuote({ symbol: "MSFT", price: 420, change: 5.2, changePercent: 1.25, name: "Microsoft" }),
|
|
200
|
+
};
|
|
201
|
+
},
|
|
202
|
+
async getQuote(symbol) {
|
|
203
|
+
if (match && symbol === "MSFT") {
|
|
204
|
+
return makeQuote({ symbol: "MSFT", price: 420, change: 5.2, changePercent: 1.25, name: "Microsoft" });
|
|
205
|
+
}
|
|
206
|
+
throw new Error(`No quote for ${symbol}`);
|
|
207
|
+
},
|
|
208
|
+
async getExchangeRate() {
|
|
209
|
+
return 1;
|
|
210
|
+
},
|
|
211
|
+
async search(query) {
|
|
212
|
+
if (!match || query !== "MSFT") return [];
|
|
213
|
+
return [{
|
|
214
|
+
symbol: "MSFT",
|
|
215
|
+
name: "Microsoft",
|
|
216
|
+
exchange: "NASDAQ",
|
|
217
|
+
currency: "USD",
|
|
218
|
+
type: "STK",
|
|
219
|
+
}];
|
|
220
|
+
},
|
|
221
|
+
async getArticleSummary() {
|
|
222
|
+
return null;
|
|
223
|
+
},
|
|
224
|
+
async getPriceHistory() {
|
|
225
|
+
return [];
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
|
|
157
230
|
function createPortfolioState(
|
|
158
231
|
config: AppConfig,
|
|
159
232
|
collectionId: string,
|
|
@@ -193,6 +266,7 @@ function PortfolioHarness({
|
|
|
193
266
|
exchangeRates,
|
|
194
267
|
stateMutator,
|
|
195
268
|
runtime = createTestPluginRuntime(),
|
|
269
|
+
paneHeight = 24,
|
|
196
270
|
}: {
|
|
197
271
|
config: AppConfig;
|
|
198
272
|
collectionId: string;
|
|
@@ -203,6 +277,7 @@ function PortfolioHarness({
|
|
|
203
277
|
exchangeRates?: Map<string, number>;
|
|
204
278
|
stateMutator?: (state: ReturnType<typeof createInitialState>) => void;
|
|
205
279
|
runtime?: PluginRuntimeAccess;
|
|
280
|
+
paneHeight?: number;
|
|
206
281
|
}) {
|
|
207
282
|
const initialState = createPortfolioState(config, collectionId, expanded, {
|
|
208
283
|
ticker,
|
|
@@ -224,7 +299,7 @@ function PortfolioHarness({
|
|
|
224
299
|
paneType="portfolio-list"
|
|
225
300
|
focused
|
|
226
301
|
width={100}
|
|
227
|
-
height={
|
|
302
|
+
height={paneHeight}
|
|
228
303
|
/>
|
|
229
304
|
</PluginRenderProvider>
|
|
230
305
|
</PaneInstanceProvider>
|
|
@@ -249,7 +324,12 @@ afterEach(async () => {
|
|
|
249
324
|
harnessDispatch = null;
|
|
250
325
|
sharedCoordinator = null;
|
|
251
326
|
setSharedMarketDataCoordinator(null);
|
|
327
|
+
setSharedMarketDataForTests(undefined);
|
|
328
|
+
setSharedRegistryForTests(undefined);
|
|
252
329
|
harnessState = null;
|
|
330
|
+
for (const persistence of tempPersistences.splice(0)) {
|
|
331
|
+
persistence.close();
|
|
332
|
+
}
|
|
253
333
|
for (const path of tempPaths.splice(0)) {
|
|
254
334
|
if (existsSync(path)) rmSync(path, { force: true });
|
|
255
335
|
}
|
|
@@ -291,6 +371,151 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
291
371
|
expect(pinned).toEqual([{ symbol: "AAPL", options: { floating: true, paneType: "ticker-detail" } }]);
|
|
292
372
|
});
|
|
293
373
|
|
|
374
|
+
test("quick-add validates and adds an exact watchlist ticker", async () => {
|
|
375
|
+
const config = createManualCollectionConfig("watchlist");
|
|
376
|
+
const notifications: Array<{ type?: string; body: string }> = [];
|
|
377
|
+
installQuickAddRegistry(createQuickAddProvider(true));
|
|
378
|
+
|
|
379
|
+
testSetup = await testRender(
|
|
380
|
+
<PortfolioHarness
|
|
381
|
+
config={config}
|
|
382
|
+
collectionId="watchlist"
|
|
383
|
+
ticker={makeTicker({ portfolios: [], watchlists: [], positions: [] })}
|
|
384
|
+
runtime={createTestPluginRuntime({
|
|
385
|
+
notify: (notification) => notifications.push(notification),
|
|
386
|
+
})}
|
|
387
|
+
paneHeight={12}
|
|
388
|
+
/>,
|
|
389
|
+
{ width: 100, height: 12 },
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
await flushFrame();
|
|
393
|
+
await act(async () => {
|
|
394
|
+
testSetup!.mockInput.pressKey("a");
|
|
395
|
+
await testSetup!.renderOnce();
|
|
396
|
+
});
|
|
397
|
+
await act(async () => {
|
|
398
|
+
await testSetup!.mockInput.typeText("MSFT");
|
|
399
|
+
await testSetup!.renderOnce();
|
|
400
|
+
});
|
|
401
|
+
await act(async () => {
|
|
402
|
+
await new Promise((resolve) => setTimeout(resolve, 360));
|
|
403
|
+
});
|
|
404
|
+
await flushFrame();
|
|
405
|
+
|
|
406
|
+
const previewFrame = testSetup.captureCharFrame();
|
|
407
|
+
expect(previewFrame).toContain("420");
|
|
408
|
+
expect(previewFrame).not.toMatch(/MSFT\s+420/);
|
|
409
|
+
expect(previewFrame).toContain("+1.25%");
|
|
410
|
+
|
|
411
|
+
await act(async () => {
|
|
412
|
+
testSetup!.mockInput.pressEnter();
|
|
413
|
+
await Promise.resolve();
|
|
414
|
+
await testSetup!.renderOnce();
|
|
415
|
+
});
|
|
416
|
+
await flushFrame();
|
|
417
|
+
|
|
418
|
+
expect(harnessState?.tickers.get("MSFT")?.metadata.watchlists).toEqual(["watchlist"]);
|
|
419
|
+
expect(notifications.at(-1)).toMatchObject({
|
|
420
|
+
type: "success",
|
|
421
|
+
body: "Added MSFT to Watchlist.",
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
test("quick-add adds an exact ticker to a manual portfolio", async () => {
|
|
426
|
+
const config = createManualCollectionConfig("main");
|
|
427
|
+
const notifications: Array<{ type?: string; body: string }> = [];
|
|
428
|
+
installQuickAddRegistry(createQuickAddProvider(true));
|
|
429
|
+
|
|
430
|
+
testSetup = await testRender(
|
|
431
|
+
<PortfolioHarness
|
|
432
|
+
config={config}
|
|
433
|
+
collectionId="main"
|
|
434
|
+
ticker={makeTicker({ portfolios: [], watchlists: [], positions: [] })}
|
|
435
|
+
runtime={createTestPluginRuntime({
|
|
436
|
+
notify: (notification) => notifications.push(notification),
|
|
437
|
+
})}
|
|
438
|
+
paneHeight={12}
|
|
439
|
+
/>,
|
|
440
|
+
{ width: 100, height: 12 },
|
|
441
|
+
);
|
|
442
|
+
|
|
443
|
+
await flushFrame();
|
|
444
|
+
await act(async () => {
|
|
445
|
+
testSetup!.mockInput.pressKey("n");
|
|
446
|
+
await testSetup!.renderOnce();
|
|
447
|
+
});
|
|
448
|
+
await act(async () => {
|
|
449
|
+
await testSetup!.mockInput.typeText("MSFT");
|
|
450
|
+
await testSetup!.renderOnce();
|
|
451
|
+
});
|
|
452
|
+
await act(async () => {
|
|
453
|
+
await new Promise((resolve) => setTimeout(resolve, 360));
|
|
454
|
+
});
|
|
455
|
+
await flushFrame();
|
|
456
|
+
|
|
457
|
+
await act(async () => {
|
|
458
|
+
testSetup!.mockInput.pressEnter();
|
|
459
|
+
await Promise.resolve();
|
|
460
|
+
await testSetup!.renderOnce();
|
|
461
|
+
});
|
|
462
|
+
await flushFrame();
|
|
463
|
+
|
|
464
|
+
expect(harnessState?.tickers.get("MSFT")?.metadata.portfolios).toEqual(["main"]);
|
|
465
|
+
expect(notifications.at(-1)).toMatchObject({
|
|
466
|
+
type: "success",
|
|
467
|
+
body: "Added MSFT to Main Portfolio.",
|
|
468
|
+
});
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("quick-add rejects unresolved ticker input", async () => {
|
|
472
|
+
const config = createManualCollectionConfig("watchlist");
|
|
473
|
+
const notifications: Array<{ type?: string; body: string }> = [];
|
|
474
|
+
installQuickAddRegistry(createQuickAddProvider(false));
|
|
475
|
+
|
|
476
|
+
testSetup = await testRender(
|
|
477
|
+
<PortfolioHarness
|
|
478
|
+
config={config}
|
|
479
|
+
collectionId="watchlist"
|
|
480
|
+
ticker={makeTicker({ portfolios: [], watchlists: [], positions: [] })}
|
|
481
|
+
runtime={createTestPluginRuntime({
|
|
482
|
+
notify: (notification) => notifications.push(notification),
|
|
483
|
+
})}
|
|
484
|
+
paneHeight={12}
|
|
485
|
+
/>,
|
|
486
|
+
{ width: 100, height: 12 },
|
|
487
|
+
);
|
|
488
|
+
|
|
489
|
+
await flushFrame();
|
|
490
|
+
await act(async () => {
|
|
491
|
+
testSetup!.mockInput.pressKey("n");
|
|
492
|
+
await testSetup!.renderOnce();
|
|
493
|
+
});
|
|
494
|
+
await act(async () => {
|
|
495
|
+
await testSetup!.mockInput.typeText("NOPE");
|
|
496
|
+
await testSetup!.renderOnce();
|
|
497
|
+
});
|
|
498
|
+
await act(async () => {
|
|
499
|
+
await new Promise((resolve) => setTimeout(resolve, 360));
|
|
500
|
+
});
|
|
501
|
+
await flushFrame();
|
|
502
|
+
|
|
503
|
+
expect(testSetup.captureCharFrame()).toContain("No exact ticker match");
|
|
504
|
+
|
|
505
|
+
await act(async () => {
|
|
506
|
+
testSetup!.mockInput.pressEnter();
|
|
507
|
+
await Promise.resolve();
|
|
508
|
+
await testSetup!.renderOnce();
|
|
509
|
+
});
|
|
510
|
+
await flushFrame();
|
|
511
|
+
|
|
512
|
+
expect(harnessState?.tickers.has("NOPE")).toBe(false);
|
|
513
|
+
expect(notifications.at(-1)).toMatchObject({
|
|
514
|
+
type: "error",
|
|
515
|
+
body: "No exact ticker match",
|
|
516
|
+
});
|
|
517
|
+
});
|
|
518
|
+
|
|
294
519
|
test("keeps non-broker portfolios unchanged", async () => {
|
|
295
520
|
const config = createDefaultConfig("/tmp/gloomberb-portfolio-list");
|
|
296
521
|
const layout = {
|
|
@@ -317,6 +542,43 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
317
542
|
expect(frame).not.toContain("Cash & Margin");
|
|
318
543
|
});
|
|
319
544
|
|
|
545
|
+
test("renders one-month sparkline column when price history is loaded", async () => {
|
|
546
|
+
const config = createPortfolioConfigWithColumns(
|
|
547
|
+
"broker:ibkr-flex:DU12345",
|
|
548
|
+
["ticker", "price", "sparkline"],
|
|
549
|
+
[createBrokerInstance("flex")],
|
|
550
|
+
);
|
|
551
|
+
|
|
552
|
+
testSetup = await testRender(
|
|
553
|
+
<PortfolioHarness
|
|
554
|
+
config={config}
|
|
555
|
+
collectionId="broker:ibkr-flex:DU12345"
|
|
556
|
+
stateMutator={(state) => {
|
|
557
|
+
state.financials = new Map([[
|
|
558
|
+
"AAPL",
|
|
559
|
+
{
|
|
560
|
+
annualStatements: [],
|
|
561
|
+
quarterlyStatements: [],
|
|
562
|
+
quote: makeQuote(),
|
|
563
|
+
priceHistory: [118, 121, 119, 124, 127, 126, 130].map((close, index) => ({
|
|
564
|
+
date: `2026-03-${20 + index}T00:00:00Z` as unknown as Date,
|
|
565
|
+
close,
|
|
566
|
+
})),
|
|
567
|
+
},
|
|
568
|
+
]]);
|
|
569
|
+
}}
|
|
570
|
+
/>,
|
|
571
|
+
{ width: 100, height: 12 },
|
|
572
|
+
);
|
|
573
|
+
|
|
574
|
+
await flushFrame();
|
|
575
|
+
|
|
576
|
+
const frame = testSetup.captureCharFrame();
|
|
577
|
+
expect(frame).toContain("1M");
|
|
578
|
+
const row = frame.split("\n").find((line) => line.includes("AAPL")) ?? "";
|
|
579
|
+
expect(row).toMatch(/[\u2800-\u28ff]/);
|
|
580
|
+
});
|
|
581
|
+
|
|
320
582
|
test("keeps native price and avg cost while converting market value and pnl to base currency", async () => {
|
|
321
583
|
const portfolioId = "broker:ibkr-flex:DU12345";
|
|
322
584
|
const config = createPortfolioConfigWithColumns(
|
|
@@ -738,6 +1000,8 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
738
1000
|
["ticker", "market_cap", "pe", "forward_pe"],
|
|
739
1001
|
[createBrokerInstance("flex")],
|
|
740
1002
|
);
|
|
1003
|
+
const seededTicker = makeTicker();
|
|
1004
|
+
const seededQuote = makeQuote();
|
|
741
1005
|
let calls = 0;
|
|
742
1006
|
let resolveFinancials!: (value: {
|
|
743
1007
|
annualStatements: [];
|
|
@@ -782,10 +1046,26 @@ describe("PortfolioListPane cash and margin UI", () => {
|
|
|
782
1046
|
},
|
|
783
1047
|
};
|
|
784
1048
|
sharedCoordinator = new MarketDataCoordinator(provider);
|
|
1049
|
+
const instrument = instrumentFromTicker(seededTicker, seededTicker.metadata.ticker);
|
|
1050
|
+
if (!instrument) throw new Error("expected ticker instrument");
|
|
1051
|
+
sharedCoordinator.primeCachedFinancials([{
|
|
1052
|
+
instrument,
|
|
1053
|
+
financials: {
|
|
1054
|
+
annualStatements: [],
|
|
1055
|
+
quarterlyStatements: [],
|
|
1056
|
+
priceHistory: [],
|
|
1057
|
+
quote: seededQuote,
|
|
1058
|
+
},
|
|
1059
|
+
}]);
|
|
785
1060
|
setSharedMarketDataCoordinator(sharedCoordinator);
|
|
786
1061
|
|
|
787
1062
|
testSetup = await testRender(
|
|
788
|
-
<PortfolioHarness
|
|
1063
|
+
<PortfolioHarness
|
|
1064
|
+
config={config}
|
|
1065
|
+
collectionId="broker:ibkr-flex:DU12345"
|
|
1066
|
+
ticker={seededTicker}
|
|
1067
|
+
quote={seededQuote}
|
|
1068
|
+
/>,
|
|
789
1069
|
{ width: 100, height: 12 },
|
|
790
1070
|
);
|
|
791
1071
|
|
|
@@ -1,10 +1,13 @@
|
|
|
1
1
|
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act } from "react";
|
|
2
3
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
3
4
|
import type { TickerFinancials } from "../../types/financials";
|
|
4
5
|
import type { TickerRecord } from "../../types/ticker";
|
|
5
6
|
import { createDefaultConfig } from "../../types/config";
|
|
6
7
|
import { AppContext, createInitialState, PaneInstanceProvider } from "../../state/app-context";
|
|
7
8
|
import { QuoteMonitorPane } from "./ticker-detail";
|
|
9
|
+
import { PluginRenderProvider, type PluginRuntimeAccess } from "../plugin-runtime";
|
|
10
|
+
import type { PinTickerOptions } from "../../types/plugin";
|
|
8
11
|
|
|
9
12
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
10
13
|
|
|
@@ -15,27 +18,57 @@ afterEach(() => {
|
|
|
15
18
|
}
|
|
16
19
|
});
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
{ instanceId: "quote-monitor:test", columnIndex: 1, order: 0 },
|
|
23
|
-
];
|
|
24
|
-
config.layout.instances.push({
|
|
25
|
-
instanceId: "quote-monitor:test",
|
|
26
|
-
paneId: "quote-monitor",
|
|
27
|
-
title: "MSFT",
|
|
28
|
-
binding: { kind: "fixed", symbol: "MSFT" },
|
|
29
|
-
});
|
|
21
|
+
interface PinTickerCall {
|
|
22
|
+
symbol: string;
|
|
23
|
+
options?: PinTickerOptions;
|
|
24
|
+
}
|
|
30
25
|
|
|
31
|
-
|
|
26
|
+
function makeRuntime(options: {
|
|
27
|
+
pinCalls?: PinTickerCall[];
|
|
28
|
+
settingsCalls?: Array<string | undefined>;
|
|
29
|
+
} = {}): PluginRuntimeAccess {
|
|
30
|
+
return {
|
|
31
|
+
getMarketData: () => null,
|
|
32
|
+
getCapability: () => null,
|
|
33
|
+
getBrokerAdapter: () => null,
|
|
34
|
+
connectBrokerInstance: async () => {},
|
|
35
|
+
updateBrokerInstance: async () => {},
|
|
36
|
+
syncBrokerInstance: async () => {},
|
|
37
|
+
removeBrokerInstance: async () => {},
|
|
38
|
+
pinTicker: (symbol, pinOptions) => {
|
|
39
|
+
options.pinCalls?.push({ symbol, options: pinOptions });
|
|
40
|
+
},
|
|
41
|
+
navigateTicker: () => {},
|
|
42
|
+
selectTicker: () => {},
|
|
43
|
+
switchTab: () => {},
|
|
44
|
+
switchPanel: () => {},
|
|
45
|
+
openCommandBar: () => {},
|
|
46
|
+
showPane: () => {},
|
|
47
|
+
hidePane: () => {},
|
|
48
|
+
openPaneSettings: (paneId) => {
|
|
49
|
+
options.settingsCalls?.push(paneId);
|
|
50
|
+
},
|
|
51
|
+
openPluginCommandWorkflow: () => {},
|
|
52
|
+
notify: () => {},
|
|
53
|
+
subscribeResumeState: () => () => {},
|
|
54
|
+
getResumeState: () => null,
|
|
55
|
+
setResumeState: () => {},
|
|
56
|
+
deleteResumeState: () => {},
|
|
57
|
+
getConfigState: () => null,
|
|
58
|
+
setConfigState: async () => {},
|
|
59
|
+
setConfigStates: async () => {},
|
|
60
|
+
deleteConfigState: async () => {},
|
|
61
|
+
getConfigStateKeys: () => [],
|
|
62
|
+
};
|
|
63
|
+
}
|
|
32
64
|
|
|
33
|
-
|
|
65
|
+
function makeTicker(symbol: string, name: string): TickerRecord {
|
|
66
|
+
return {
|
|
34
67
|
metadata: {
|
|
35
|
-
ticker:
|
|
68
|
+
ticker: symbol,
|
|
36
69
|
exchange: "NASDAQ",
|
|
37
70
|
currency: "USD",
|
|
38
|
-
name
|
|
71
|
+
name,
|
|
39
72
|
portfolios: [],
|
|
40
73
|
watchlists: [],
|
|
41
74
|
positions: [],
|
|
@@ -43,28 +76,71 @@ function createQuoteMonitorHarness() {
|
|
|
43
76
|
tags: [],
|
|
44
77
|
},
|
|
45
78
|
};
|
|
79
|
+
}
|
|
46
80
|
|
|
47
|
-
|
|
81
|
+
function makeFinancials(symbol: string, price: number, change: number, changePercent: number): TickerFinancials {
|
|
82
|
+
return {
|
|
48
83
|
quote: {
|
|
49
|
-
symbol
|
|
50
|
-
price
|
|
84
|
+
symbol,
|
|
85
|
+
price,
|
|
51
86
|
currency: "USD",
|
|
52
|
-
change
|
|
53
|
-
changePercent
|
|
87
|
+
change,
|
|
88
|
+
changePercent,
|
|
54
89
|
lastUpdated: Date.now(),
|
|
55
90
|
},
|
|
56
91
|
annualStatements: [],
|
|
57
92
|
quarterlyStatements: [],
|
|
58
|
-
priceHistory:
|
|
93
|
+
priceHistory: Array.from({ length: 12 }, (_, index) => ({
|
|
94
|
+
date: new Date(Date.UTC(2026, 0, index + 1)),
|
|
95
|
+
close: price - 6 + index,
|
|
96
|
+
})),
|
|
59
97
|
};
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function createQuoteMonitorHarness(options: {
|
|
101
|
+
symbols?: string[];
|
|
102
|
+
pinCalls?: PinTickerCall[];
|
|
103
|
+
settingsCalls?: Array<string | undefined>;
|
|
104
|
+
} = {}) {
|
|
105
|
+
const symbols = options.symbols ?? ["MSFT"];
|
|
106
|
+
const config = createDefaultConfig("/tmp/gloomberb-test");
|
|
107
|
+
(config.layout as typeof config.layout & { docked: Array<{ instanceId: string; columnIndex: number; order: number }> }).docked = [
|
|
108
|
+
{ instanceId: "ticker-detail:main", columnIndex: 0, order: 0 },
|
|
109
|
+
{ instanceId: "quote-monitor:test", columnIndex: 1, order: 0 },
|
|
110
|
+
];
|
|
111
|
+
config.layout.instances.push({
|
|
112
|
+
instanceId: "quote-monitor:test",
|
|
113
|
+
paneId: "quote-monitor",
|
|
114
|
+
title: symbols.join(" · "),
|
|
115
|
+
binding: { kind: "fixed", symbol: symbols[0]! },
|
|
116
|
+
settings: {
|
|
117
|
+
symbol: symbols[0]!,
|
|
118
|
+
symbols,
|
|
119
|
+
symbolsText: symbols.join(", "),
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
const state = createInitialState(config);
|
|
60
124
|
|
|
61
|
-
|
|
62
|
-
|
|
125
|
+
const tickers = [
|
|
126
|
+
makeTicker("MSFT", "Microsoft"),
|
|
127
|
+
makeTicker("AAPL", "Apple"),
|
|
128
|
+
];
|
|
129
|
+
state.tickers = new Map(tickers.map((ticker) => [ticker.metadata.ticker, ticker]));
|
|
130
|
+
state.financials = new Map([
|
|
131
|
+
["MSFT", makeFinancials("MSFT", 356.15, -9.82, -2.68)],
|
|
132
|
+
["AAPL", makeFinancials("AAPL", 202.12, 3.05, 1.53)],
|
|
133
|
+
]);
|
|
63
134
|
|
|
64
135
|
return (
|
|
65
136
|
<AppContext value={{ state, dispatch: () => {} }}>
|
|
66
137
|
<PaneInstanceProvider paneId="quote-monitor:test">
|
|
67
|
-
<
|
|
138
|
+
<PluginRenderProvider pluginId="ticker-detail" runtime={makeRuntime({
|
|
139
|
+
pinCalls: options.pinCalls,
|
|
140
|
+
settingsCalls: options.settingsCalls,
|
|
141
|
+
})}>
|
|
142
|
+
<QuoteMonitorPane paneId="quote-monitor:test" paneType="quote-monitor" focused width={72} height={7} />
|
|
143
|
+
</PluginRenderProvider>
|
|
68
144
|
</PaneInstanceProvider>
|
|
69
145
|
</AppContext>
|
|
70
146
|
);
|
|
@@ -84,6 +160,63 @@ describe("QuoteMonitorPane", () => {
|
|
|
84
160
|
expect(frame).toContain("$356.15");
|
|
85
161
|
expect(frame).toContain("-2.68%");
|
|
86
162
|
expect(frame).toContain("-9.82");
|
|
163
|
+
expect(frame).toMatch(/[⠁-⣿]/);
|
|
87
164
|
});
|
|
88
165
|
|
|
166
|
+
test("renders multiple configured tickers", async () => {
|
|
167
|
+
testSetup = await testRender(createQuoteMonitorHarness({ symbols: ["MSFT", "AAPL"] }), {
|
|
168
|
+
width: 72,
|
|
169
|
+
height: 8,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
await testSetup.renderOnce();
|
|
173
|
+
|
|
174
|
+
const frame = testSetup.captureCharFrame();
|
|
175
|
+
expect(frame).toContain("MSFT");
|
|
176
|
+
expect(frame).toContain("AAPL");
|
|
177
|
+
expect(frame).toContain("$356.15");
|
|
178
|
+
expect(frame).toContain("$202.12");
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
test("opens a ticker detail pane on the second card click", async () => {
|
|
182
|
+
const pinCalls: PinTickerCall[] = [];
|
|
183
|
+
testSetup = await testRender(createQuoteMonitorHarness({ pinCalls }), {
|
|
184
|
+
width: 72,
|
|
185
|
+
height: 7,
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
await testSetup.renderOnce();
|
|
189
|
+
const frame = testSetup.captureCharFrame();
|
|
190
|
+
const row = frame.split("\n").findIndex((line) => line.includes("MSFT"));
|
|
191
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
192
|
+
|
|
193
|
+
await act(async () => {
|
|
194
|
+
await testSetup!.mockMouse.click(3, row);
|
|
195
|
+
await testSetup!.renderOnce();
|
|
196
|
+
await testSetup!.mockMouse.click(3, row);
|
|
197
|
+
await testSetup!.renderOnce();
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
expect(pinCalls).toEqual([{
|
|
201
|
+
symbol: "MSFT",
|
|
202
|
+
options: { paneType: "ticker-detail", floating: true },
|
|
203
|
+
}]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test("opens pane settings when t is pressed", async () => {
|
|
207
|
+
const settingsCalls: Array<string | undefined> = [];
|
|
208
|
+
testSetup = await testRender(createQuoteMonitorHarness({ settingsCalls }), {
|
|
209
|
+
width: 72,
|
|
210
|
+
height: 7,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
await testSetup.renderOnce();
|
|
214
|
+
|
|
215
|
+
await act(async () => {
|
|
216
|
+
testSetup!.mockInput.pressKey("t");
|
|
217
|
+
await testSetup!.renderOnce();
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
expect(settingsCalls).toEqual(["quote-monitor:test"]);
|
|
221
|
+
});
|
|
89
222
|
});
|