gloomberb 0.2.0 → 0.3.1
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 +20 -19
- package/bin/gloomberb +3 -0
- package/package.json +5 -4
- package/patches/@opentui%2Fcore@0.1.90.patch +356 -0
- package/src/app.tsx +1330 -0
- package/src/brokers/sync-broker-instance.test.ts +100 -0
- package/src/brokers/sync-broker-instance.ts +341 -0
- package/src/cli/commands/plugins.ts +173 -0
- package/src/cli/commands/portfolio.ts +210 -0
- package/src/cli/commands/search.ts +120 -0
- package/src/cli/commands/ticker.ts +375 -0
- package/src/cli/commands/watchlist.ts +210 -0
- package/src/cli/context.ts +62 -0
- package/src/cli/errors.ts +13 -0
- package/src/cli/helpers.ts +85 -0
- package/src/cli/index.ts +143 -0
- package/src/cli/ticker-resolution.ts +39 -0
- package/src/cli/types.ts +15 -0
- package/src/cli.test.ts +361 -0
- package/src/components/chart/chart-data.ts +239 -0
- package/src/components/chart/chart-renderer.ts +1199 -0
- package/src/components/chart/chart-types.ts +82 -0
- package/src/components/chart/chart-viewport.ts +21 -0
- package/src/components/chart/chart.test.ts +439 -0
- package/src/components/chart/comparison-chart-data.test.ts +109 -0
- package/src/components/chart/comparison-chart-data.ts +316 -0
- package/src/components/chart/comparison-chart-renderer.test.ts +84 -0
- package/src/components/chart/comparison-chart-renderer.ts +354 -0
- package/src/components/chart/comparison-stock-chart.tsx +1486 -0
- package/src/components/chart/cursor-motion.ts +48 -0
- package/src/components/chart/native/chart-rasterizer.ts +732 -0
- package/src/components/chart/native/kitty-adapter.ts +12 -0
- package/src/components/chart/native/kitty-manager.ts +142 -0
- package/src/components/chart/native/kitty-protocol.ts +110 -0
- package/src/components/chart/native/kitty-support.ts +71 -0
- package/src/components/chart/native/kitty.test.ts +620 -0
- package/src/components/chart/native/renderer-selection.ts +24 -0
- package/src/components/chart/native/surface-manager.ts +225 -0
- package/src/components/chart/native/surface-sync.ts +34 -0
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +165 -0
- package/src/components/chart/stock-chart.test.ts +107 -0
- package/src/components/chart/stock-chart.tsx +1556 -0
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +137 -0
- package/src/components/command-bar/command-bar.test.tsx +1524 -0
- package/src/components/command-bar/command-bar.tsx +4823 -0
- package/src/components/command-bar/command-registry.ts +261 -0
- package/src/components/command-bar/helpers.test.ts +224 -0
- package/src/components/command-bar/helpers.ts +371 -0
- package/src/components/command-bar/root-shortcuts.ts +158 -0
- package/src/components/command-bar/view-model.test.ts +152 -0
- package/src/components/command-bar/view-model.ts +185 -0
- package/src/components/command-bar/workflow-ops.test.ts +70 -0
- package/src/components/command-bar/workflow-ops.ts +433 -0
- package/src/components/command-bar/workflow-types.ts +127 -0
- package/src/components/detail-feed-view.test.tsx +105 -0
- package/src/components/detail-feed-view.tsx +295 -0
- package/src/components/index.ts +32 -0
- package/src/components/layout/floating-pane.tsx +49 -0
- package/src/components/layout/header.tsx +129 -0
- package/src/components/layout/pane-header.tsx +44 -0
- package/src/components/layout/pane.tsx +49 -0
- package/src/components/layout/pixel-mouse-mode.test.ts +122 -0
- package/src/components/layout/pixel-mouse.test.ts +40 -0
- package/src/components/layout/shell.test.tsx +625 -0
- package/src/components/layout/shell.tsx +1200 -0
- package/src/components/layout/status-bar.test.tsx +173 -0
- package/src/components/layout/status-bar.tsx +138 -0
- package/src/components/markdown-editor.tsx +134 -0
- package/src/components/onboarding/onboarding-wizard.test.tsx +184 -0
- package/src/components/onboarding/onboarding-wizard.tsx +1365 -0
- package/src/components/pane-settings-dialog.tsx +468 -0
- package/src/components/pane-template-wizard.test.tsx +52 -0
- package/src/components/pane-template-wizard.tsx +199 -0
- package/src/components/price-selector-dialog.tsx +195 -0
- package/src/components/spinner.tsx +2 -0
- package/src/components/tab-bar.tsx +17 -0
- package/src/components/ticker-badge-text.test.tsx +160 -0
- package/src/components/ticker-badge-text.tsx +58 -0
- package/src/components/ticker-badge.tsx +88 -0
- package/src/components/ticker-list-table.tsx +188 -0
- package/src/components/toggle-list.tsx +74 -0
- package/src/components/ui/button.tsx +77 -0
- package/src/components/ui/data-table.tsx +210 -0
- package/src/components/ui/external-link.tsx +22 -0
- package/src/components/ui/fields.tsx +187 -0
- package/src/components/ui/frame.tsx +73 -0
- package/src/components/ui/index.ts +53 -0
- package/src/components/ui/list-view.tsx +166 -0
- package/src/components/ui/loading.tsx +70 -0
- package/src/components/ui/status.tsx +83 -0
- package/src/components/ui/tabs.tsx +47 -0
- package/src/components/ui/toggle.tsx +164 -0
- package/src/components/ui/ui.test.tsx +134 -0
- package/src/data/app-persistence.ts +25 -0
- package/src/data/config-store.test.ts +310 -0
- package/src/data/config-store.ts +469 -0
- package/src/data/plugin-state-store.ts +102 -0
- package/src/data/resource-store.ts +323 -0
- package/src/data/session-store.ts +52 -0
- package/src/data/sqlite-cache.test.ts +103 -0
- package/src/data/sqlite-database.ts +62 -0
- package/src/data/sqlite-json.ts +19 -0
- package/src/data/ticker-repository.ts +45 -0
- package/src/data/ticker-store.ts +46 -0
- package/src/index.tsx +67 -0
- package/src/market-data/coordinator.test.ts +369 -0
- package/src/market-data/coordinator.ts +580 -0
- package/src/market-data/hooks.tsx +234 -0
- package/src/market-data/query-store.ts +28 -0
- package/src/market-data/request-types.test.ts +64 -0
- package/src/market-data/request-types.ts +70 -0
- package/src/market-data/result-types.ts +56 -0
- package/src/market-data/selectors.ts +166 -0
- package/src/market-data/trace.ts +7 -0
- package/src/pane-settings.test.ts +71 -0
- package/src/pane-settings.ts +73 -0
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +338 -0
- package/src/plugins/builtin/ai/gloomberb-cli.ts +70 -0
- package/src/plugins/builtin/ai/index.test.tsx +139 -0
- package/src/plugins/builtin/ai/index.tsx +86 -0
- package/src/plugins/builtin/ai/providers.ts +99 -0
- package/src/plugins/builtin/ai/runner.ts +82 -0
- package/src/plugins/builtin/ai/screener-contract.test.ts +117 -0
- package/src/plugins/builtin/ai/screener-contract.ts +174 -0
- package/src/plugins/builtin/ai/screener-pane.test.tsx +365 -0
- package/src/plugins/builtin/ai/screener-pane.tsx +1161 -0
- package/src/plugins/builtin/ai/settings.ts +79 -0
- package/src/plugins/builtin/ai/ticker-context.ts +64 -0
- package/src/plugins/builtin/ai/utils.ts +71 -0
- package/src/plugins/builtin/ask-ai.test.tsx +258 -0
- package/src/plugins/builtin/ask-ai.tsx +9 -0
- package/src/plugins/builtin/chat-controller.test.ts +217 -0
- package/src/plugins/builtin/chat-controller.ts +340 -0
- package/src/plugins/builtin/chat.test.tsx +451 -0
- package/src/plugins/builtin/chat.tsx +620 -0
- package/src/plugins/builtin/comparison-chart.test.tsx +277 -0
- package/src/plugins/builtin/comparison-chart.tsx +169 -0
- package/src/plugins/builtin/data-providers.ts +16 -0
- package/src/plugins/builtin/debug.tsx +351 -0
- package/src/plugins/builtin/help.test.tsx +110 -0
- package/src/plugins/builtin/help.tsx +281 -0
- package/src/plugins/builtin/layout-manager.tsx +264 -0
- package/src/plugins/builtin/manual-entry.tsx +25 -0
- package/src/plugins/builtin/news.tsx +143 -0
- package/src/plugins/builtin/notes-files.ts +58 -0
- package/src/plugins/builtin/notes.tsx +396 -0
- package/src/plugins/builtin/options-availability.test.tsx +179 -0
- package/src/plugins/builtin/options-availability.ts +158 -0
- package/src/plugins/builtin/options.tsx +364 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +227 -0
- package/src/plugins/builtin/portfolio-list/index.tsx +99 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +436 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +533 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +280 -0
- package/src/plugins/builtin/portfolio-list/summary.tsx +270 -0
- package/src/plugins/builtin/portfolio-list/table.tsx +64 -0
- package/src/plugins/builtin/portfolio-list.test.ts +74 -0
- package/src/plugins/builtin/portfolio-list.test.tsx +1164 -0
- package/src/plugins/builtin/portfolio-metrics.test.ts +153 -0
- package/src/plugins/builtin/quote-monitor.test.tsx +114 -0
- package/src/plugins/builtin/sec.tsx +250 -0
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +43 -0
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +390 -0
- package/src/plugins/builtin/ticker-detail/index.tsx +80 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +242 -0
- package/src/plugins/builtin/ticker-detail/pane.tsx +183 -0
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +83 -0
- package/src/plugins/builtin/ticker-detail/settings.ts +132 -0
- package/src/plugins/builtin/ticker-detail-chart-tab-switch.test.tsx +193 -0
- package/src/plugins/builtin/ticker-detail.test.tsx +578 -0
- package/src/plugins/builtin/yahoo.ts +18 -0
- package/src/plugins/event-bus.ts +37 -0
- package/src/plugins/ibkr/__snapshots__/trade-tab.test.tsx.snap +35 -0
- package/src/plugins/ibkr/account-cache.test.ts +101 -0
- package/src/plugins/ibkr/account-cache.ts +137 -0
- package/src/plugins/ibkr/broker-adapter.ts +168 -0
- package/src/plugins/ibkr/config.test.ts +68 -0
- package/src/plugins/ibkr/config.ts +262 -0
- package/src/plugins/ibkr/dialogs.tsx +83 -0
- package/src/plugins/ibkr/flex.test.ts +107 -0
- package/src/plugins/ibkr/flex.ts +267 -0
- package/src/plugins/ibkr/fundamental-parser.ts +149 -0
- package/src/plugins/ibkr/gateway-helpers.ts +28 -0
- package/src/plugins/ibkr/gateway-selection.ts +51 -0
- package/src/plugins/ibkr/gateway-service.test.ts +450 -0
- package/src/plugins/ibkr/gateway-service.ts +2228 -0
- package/src/plugins/ibkr/index.test.tsx +183 -0
- package/src/plugins/ibkr/index.tsx +153 -0
- package/src/plugins/ibkr/instance-selection.test.ts +121 -0
- package/src/plugins/ibkr/instance-selection.ts +36 -0
- package/src/plugins/ibkr/price-normalization.test.ts +38 -0
- package/src/plugins/ibkr/price-normalization.ts +59 -0
- package/src/plugins/ibkr/trade-badge.tsx +27 -0
- package/src/plugins/ibkr/trade-tab.test.tsx +225 -0
- package/src/plugins/ibkr/trade-tab.tsx +1050 -0
- package/src/plugins/ibkr/trade-utils.test.ts +175 -0
- package/src/plugins/ibkr/trade-utils.ts +149 -0
- package/src/plugins/ibkr/trading-pane.tsx +438 -0
- package/src/plugins/ibkr/trading-state.test.ts +109 -0
- package/src/plugins/ibkr/trading-state.ts +339 -0
- package/src/plugins/loader.ts +71 -0
- package/src/plugins/pane-manager.test.ts +103 -0
- package/src/plugins/pane-manager.ts +974 -0
- package/src/plugins/plugin-persistence.ts +85 -0
- package/src/plugins/plugin-runtime.test.tsx +185 -0
- package/src/plugins/plugin-runtime.tsx +175 -0
- package/src/plugins/prediction-markets/cache.ts +114 -0
- package/src/plugins/prediction-markets/categories.ts +223 -0
- package/src/plugins/prediction-markets/chart.tsx +512 -0
- package/src/plugins/prediction-markets/columns.ts +133 -0
- package/src/plugins/prediction-markets/controller-data.ts +614 -0
- package/src/plugins/prediction-markets/controller.ts +725 -0
- package/src/plugins/prediction-markets/detail/book.tsx +118 -0
- package/src/plugins/prediction-markets/detail/outcomes.tsx +102 -0
- package/src/plugins/prediction-markets/detail/overview.tsx +73 -0
- package/src/plugins/prediction-markets/detail/pane.tsx +308 -0
- package/src/plugins/prediction-markets/detail/rules.tsx +16 -0
- package/src/plugins/prediction-markets/detail/shared.tsx +29 -0
- package/src/plugins/prediction-markets/detail/trades.tsx +30 -0
- package/src/plugins/prediction-markets/detail.test.tsx +47 -0
- package/src/plugins/prediction-markets/index.tsx +102 -0
- package/src/plugins/prediction-markets/keyboard.ts +64 -0
- package/src/plugins/prediction-markets/launch.test.ts +126 -0
- package/src/plugins/prediction-markets/launch.ts +240 -0
- package/src/plugins/prediction-markets/metrics.ts +311 -0
- package/src/plugins/prediction-markets/navigation.ts +90 -0
- package/src/plugins/prediction-markets/outcome-order.ts +19 -0
- package/src/plugins/prediction-markets/pane.test.tsx +425 -0
- package/src/plugins/prediction-markets/pane.tsx +294 -0
- package/src/plugins/prediction-markets/plugin.test.tsx +570 -0
- package/src/plugins/prediction-markets/rows.ts +229 -0
- package/src/plugins/prediction-markets/services/fetch.ts +97 -0
- package/src/plugins/prediction-markets/services/kalshi-adapter.ts +555 -0
- package/src/plugins/prediction-markets/services/polymarket-adapter.ts +660 -0
- package/src/plugins/prediction-markets/services/polymarket-ws.ts +108 -0
- package/src/plugins/prediction-markets/settings.ts +164 -0
- package/src/plugins/prediction-markets/status.ts +24 -0
- package/src/plugins/prediction-markets/table.tsx +84 -0
- package/src/plugins/prediction-markets/test-helpers.tsx +636 -0
- package/src/plugins/prediction-markets/types.ts +186 -0
- package/src/plugins/registry.ts +633 -0
- package/src/sources/exchange-rates.ts +18 -0
- package/src/sources/gloomberb-cloud.test.ts +199 -0
- package/src/sources/gloomberb-cloud.ts +326 -0
- package/src/sources/provider-errors.ts +19 -0
- package/src/sources/provider-router.test.ts +1476 -0
- package/src/sources/provider-router.ts +1405 -0
- package/src/sources/sec-edgar.test.ts +286 -0
- package/src/sources/sec-edgar.ts +526 -0
- package/src/sources/yahoo-finance.test.ts +14 -0
- package/src/sources/yahoo-finance.ts +1017 -0
- package/src/state/app-activity.test.ts +41 -0
- package/src/state/app-activity.ts +106 -0
- package/src/state/app-bootstrap.test.ts +471 -0
- package/src/state/app-bootstrap.ts +308 -0
- package/src/state/app-context.test.ts +406 -0
- package/src/state/app-context.test.tsx +132 -0
- package/src/state/app-context.tsx +969 -0
- package/src/state/selectors.ts +67 -0
- package/src/state/session-persistence.test.ts +128 -0
- package/src/state/session-persistence.ts +161 -0
- package/src/state/ticker-refresh-queue.test.ts +46 -0
- package/src/state/ticker-refresh-queue.ts +40 -0
- package/src/state/use-inline-tickers.ts +174 -0
- package/src/state/use-quote-streaming.test.ts +37 -0
- package/src/state/use-quote-streaming.test.tsx +70 -0
- package/src/state/use-quote-streaming.ts +80 -0
- package/src/state/use-session-snapshot.ts +84 -0
- package/src/test-support/data-provider.ts +22 -0
- package/src/theme/color-utils.ts +67 -0
- package/src/theme/colors.ts +128 -0
- package/src/theme/themes.test.ts +45 -0
- package/src/theme/themes.ts +491 -0
- package/src/types/broker.ts +105 -0
- package/src/types/config.ts +434 -0
- package/src/types/data-provider.ts +81 -0
- package/src/types/financials.ts +142 -0
- package/src/types/instrument.ts +29 -0
- package/src/types/persistence.ts +21 -0
- package/src/types/plugin.ts +354 -0
- package/src/types/ticker.ts +58 -0
- package/src/types/trading.ts +85 -0
- package/src/updater.test.ts +162 -0
- package/src/updater.ts +247 -0
- package/src/utils/api-client.ts +666 -0
- package/src/utils/ascii-chart.ts +172 -0
- package/src/utils/broker-instances.ts +47 -0
- package/src/utils/cli-output.ts +83 -0
- package/src/utils/currency-units.test.ts +49 -0
- package/src/utils/currency-units.ts +82 -0
- package/src/utils/debug-log.ts +133 -0
- package/src/utils/exchange-rate-map.ts +8 -0
- package/src/utils/format.ts +158 -0
- package/src/utils/fuzzy-search.ts +34 -0
- package/src/utils/market-status.ts +95 -0
- package/src/utils/options.ts +86 -0
- package/src/utils/price-history.ts +39 -0
- package/src/utils/quote-time.test.ts +22 -0
- package/src/utils/quote-time.ts +50 -0
- package/src/utils/sec.test.ts +55 -0
- package/src/utils/sec.ts +45 -0
- package/src/utils/ticker-list.test.ts +22 -0
- package/src/utils/ticker-list.ts +35 -0
- package/src/utils/ticker-metadata.ts +51 -0
- package/src/utils/ticker-search.test.ts +227 -0
- package/src/utils/ticker-search.ts +764 -0
- package/src/utils/ticker-tokenizer.test.ts +48 -0
- package/src/utils/ticker-tokenizer.ts +72 -0
- package/src/version.ts +1 -0
- package/install.mjs +0 -68
|
@@ -0,0 +1,1524 @@
|
|
|
1
|
+
import { afterEach, describe, expect, test } from "bun:test";
|
|
2
|
+
import { act, useReducer } from "react";
|
|
3
|
+
import { testRender } from "@opentui/react/test-utils";
|
|
4
|
+
import { DialogProvider } from "@opentui-ui/dialog/react";
|
|
5
|
+
import { CommandBar } from "./command-bar";
|
|
6
|
+
import { AppContext, type AppState, appReducer, createInitialState } from "../../state/app-context";
|
|
7
|
+
import { createTestDataProvider } from "../../test-support/data-provider";
|
|
8
|
+
import { cloneLayout, createDefaultConfig, type AppConfig } from "../../types/config";
|
|
9
|
+
import type { DataProvider } from "../../types/data-provider";
|
|
10
|
+
import type { TickerRecord } from "../../types/ticker";
|
|
11
|
+
import type { PluginRegistry } from "../../plugins/registry";
|
|
12
|
+
|
|
13
|
+
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (testSetup) {
|
|
17
|
+
testSetup.renderer.destroy();
|
|
18
|
+
testSetup = undefined;
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
async function waitForFrameToContain(text: string, attempts = 12, delayMs = 50): Promise<string> {
|
|
23
|
+
for (let attempt = 0; attempt < attempts; attempt++) {
|
|
24
|
+
const frame = testSetup!.captureCharFrame();
|
|
25
|
+
if (frame.includes(text)) {
|
|
26
|
+
return frame;
|
|
27
|
+
}
|
|
28
|
+
await Bun.sleep(delayMs);
|
|
29
|
+
await testSetup!.renderOnce();
|
|
30
|
+
}
|
|
31
|
+
throw new Error(`Timed out waiting for frame to contain "${text}".`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function clickFrameText(text: string): Promise<void> {
|
|
35
|
+
const frame = testSetup!.captureCharFrame();
|
|
36
|
+
const rows = frame.split("\n");
|
|
37
|
+
const row = rows.findIndex((line) => line.includes(text));
|
|
38
|
+
const col = row >= 0 ? rows[row]!.indexOf(text) : -1;
|
|
39
|
+
|
|
40
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
41
|
+
expect(col).toBeGreaterThanOrEqual(0);
|
|
42
|
+
|
|
43
|
+
await act(async () => {
|
|
44
|
+
await testSetup!.mockMouse.click(col + 1, row);
|
|
45
|
+
await testSetup!.renderOnce();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function expectSingleBackControl(frame: string): void {
|
|
50
|
+
expect(frame.match(/\bBack\b/g)?.length ?? 0).toBe(1);
|
|
51
|
+
expect(frame).not.toContain("esc");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function makeTicker(symbol: string, name: string): TickerRecord {
|
|
55
|
+
return {
|
|
56
|
+
metadata: {
|
|
57
|
+
ticker: symbol,
|
|
58
|
+
exchange: "NASDAQ",
|
|
59
|
+
currency: "USD",
|
|
60
|
+
name,
|
|
61
|
+
portfolios: [],
|
|
62
|
+
watchlists: [],
|
|
63
|
+
positions: [],
|
|
64
|
+
custom: {},
|
|
65
|
+
tags: [],
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function makeDataProvider(searchImpl: DataProvider["search"] = async () => []): DataProvider {
|
|
71
|
+
return createTestDataProvider({
|
|
72
|
+
id: "test",
|
|
73
|
+
search: searchImpl,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function makePluginRegistry(hasPaneSettings: (paneId: string) => boolean = () => false): PluginRegistry {
|
|
78
|
+
return {
|
|
79
|
+
panes: new Map<string, any>([
|
|
80
|
+
["portfolio-list", {
|
|
81
|
+
id: "portfolio-list",
|
|
82
|
+
name: "Portfolio List",
|
|
83
|
+
component: () => null,
|
|
84
|
+
defaultPosition: "left",
|
|
85
|
+
}],
|
|
86
|
+
["ticker-detail", {
|
|
87
|
+
id: "ticker-detail",
|
|
88
|
+
name: "Ticker Detail",
|
|
89
|
+
component: () => null,
|
|
90
|
+
defaultPosition: "right",
|
|
91
|
+
}],
|
|
92
|
+
["chat", {
|
|
93
|
+
id: "chat",
|
|
94
|
+
name: "Chat",
|
|
95
|
+
component: () => null,
|
|
96
|
+
defaultPosition: "right",
|
|
97
|
+
defaultMode: "floating",
|
|
98
|
+
}],
|
|
99
|
+
["quote-monitor", {
|
|
100
|
+
id: "quote-monitor",
|
|
101
|
+
name: "Quote Monitor",
|
|
102
|
+
component: () => null,
|
|
103
|
+
defaultPosition: "right",
|
|
104
|
+
defaultMode: "floating",
|
|
105
|
+
}],
|
|
106
|
+
["ibkr-trading", {
|
|
107
|
+
id: "ibkr-trading",
|
|
108
|
+
name: "IBKR Console",
|
|
109
|
+
component: () => null,
|
|
110
|
+
defaultPosition: "right",
|
|
111
|
+
defaultMode: "floating",
|
|
112
|
+
}],
|
|
113
|
+
]),
|
|
114
|
+
paneTemplates: new Map<string, any>([
|
|
115
|
+
["new-portfolio-pane", {
|
|
116
|
+
id: "new-portfolio-pane",
|
|
117
|
+
paneId: "portfolio-list",
|
|
118
|
+
label: "New Portfolio Pane",
|
|
119
|
+
description: "Open another portfolio list pane",
|
|
120
|
+
shortcut: { prefix: "PF" },
|
|
121
|
+
}],
|
|
122
|
+
["new-watchlist-pane", {
|
|
123
|
+
id: "new-watchlist-pane",
|
|
124
|
+
paneId: "portfolio-list",
|
|
125
|
+
label: "New Watchlist Pane",
|
|
126
|
+
description: "Open another watchlist pane",
|
|
127
|
+
}],
|
|
128
|
+
["new-ticker-detail-pane", {
|
|
129
|
+
id: "new-ticker-detail-pane",
|
|
130
|
+
paneId: "ticker-detail",
|
|
131
|
+
label: "New Ticker Detail Pane",
|
|
132
|
+
description: "Open another detail pane",
|
|
133
|
+
}],
|
|
134
|
+
["new-chat-pane", {
|
|
135
|
+
id: "new-chat-pane",
|
|
136
|
+
paneId: "chat",
|
|
137
|
+
label: "New Chat Pane",
|
|
138
|
+
description: "Open another floating chat window",
|
|
139
|
+
shortcut: { prefix: "CHAT" },
|
|
140
|
+
}],
|
|
141
|
+
["quote-monitor-pane", {
|
|
142
|
+
id: "quote-monitor-pane",
|
|
143
|
+
paneId: "quote-monitor",
|
|
144
|
+
label: "Quote Monitor",
|
|
145
|
+
description: "Open a compact quote monitor for the selected ticker",
|
|
146
|
+
shortcut: { prefix: "QQ", argPlaceholder: "ticker" },
|
|
147
|
+
}],
|
|
148
|
+
["new-ibkr-trading-pane", {
|
|
149
|
+
id: "new-ibkr-trading-pane",
|
|
150
|
+
paneId: "ibkr-trading",
|
|
151
|
+
label: "New IBKR Trading Pane",
|
|
152
|
+
description: "Open another floating IBKR trading console",
|
|
153
|
+
shortcut: { prefix: "IBKR" },
|
|
154
|
+
canCreate: (context: any) => context.config.brokerInstances.some((instance: any) => (
|
|
155
|
+
instance.brokerType === "ibkr"
|
|
156
|
+
&& instance.connectionMode === "gateway"
|
|
157
|
+
&& instance.enabled !== false
|
|
158
|
+
)),
|
|
159
|
+
}],
|
|
160
|
+
]),
|
|
161
|
+
commands: new Map<string, any>([
|
|
162
|
+
["plugin:scan", {
|
|
163
|
+
id: "plugin:scan",
|
|
164
|
+
label: "Scan Movers",
|
|
165
|
+
description: "Run a quick movers scan",
|
|
166
|
+
category: "plugins",
|
|
167
|
+
execute: async () => {},
|
|
168
|
+
}],
|
|
169
|
+
]),
|
|
170
|
+
tickerActions: new Map<string, any>([
|
|
171
|
+
["pin", {
|
|
172
|
+
id: "pin",
|
|
173
|
+
label: "Pin Ticker",
|
|
174
|
+
execute: async () => {},
|
|
175
|
+
}],
|
|
176
|
+
]),
|
|
177
|
+
brokers: new Map(),
|
|
178
|
+
allPlugins: new Map([
|
|
179
|
+
["news", { id: "news", name: "News", version: "1.0.0", description: "Latest headlines", toggleable: true }],
|
|
180
|
+
["notes", { id: "notes", name: "Notes", version: "1.0.0", description: "Ticker notes", toggleable: true }],
|
|
181
|
+
]),
|
|
182
|
+
getCommandPluginId: () => "news",
|
|
183
|
+
getPaneTemplatePluginId: (templateId: string) => (
|
|
184
|
+
templateId === "new-chat-pane" ? "news" : "notes"
|
|
185
|
+
),
|
|
186
|
+
getPluginPaneIds: () => [],
|
|
187
|
+
getPluginPaneTemplateIds: () => [],
|
|
188
|
+
hasPaneSettings,
|
|
189
|
+
events: { emit: () => {} },
|
|
190
|
+
hideWidget: () => {},
|
|
191
|
+
pinTickerFn: () => {},
|
|
192
|
+
showWidget: () => {},
|
|
193
|
+
updateLayoutFn: () => {},
|
|
194
|
+
getTermSizeFn: () => ({ width: 80, height: 24 }),
|
|
195
|
+
showToastFn: () => {},
|
|
196
|
+
createPaneFromTemplateFn: () => {},
|
|
197
|
+
createPaneFromTemplateAsyncFn: async () => {},
|
|
198
|
+
openPaneSettingsFn: () => {},
|
|
199
|
+
applyPaneSettingValueFn: async () => {},
|
|
200
|
+
resolvePaneSettings: () => null,
|
|
201
|
+
createBrokerInstanceFn: async () => { throw new Error("unused"); },
|
|
202
|
+
syncBrokerInstanceFn: async () => {},
|
|
203
|
+
removeBrokerInstanceFn: async () => {},
|
|
204
|
+
getConfigFn: () => createDefaultConfig("/tmp/gloomberb-test"),
|
|
205
|
+
} as unknown as PluginRegistry;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function CommandBarHarness({
|
|
209
|
+
query,
|
|
210
|
+
disabledPlugins = [],
|
|
211
|
+
selectedTicker,
|
|
212
|
+
live = false,
|
|
213
|
+
extraTickers = [],
|
|
214
|
+
showQueryState = false,
|
|
215
|
+
onSaveTicker,
|
|
216
|
+
configureConfig,
|
|
217
|
+
configureState,
|
|
218
|
+
configurePluginRegistry,
|
|
219
|
+
dataProvider = makeDataProvider(),
|
|
220
|
+
hasPaneSettings,
|
|
221
|
+
}: {
|
|
222
|
+
query: string;
|
|
223
|
+
disabledPlugins?: string[];
|
|
224
|
+
selectedTicker?: string;
|
|
225
|
+
live?: boolean;
|
|
226
|
+
extraTickers?: TickerRecord[];
|
|
227
|
+
showQueryState?: boolean;
|
|
228
|
+
onSaveTicker?: (ticker: TickerRecord) => void;
|
|
229
|
+
configureConfig?: (config: AppConfig) => AppConfig;
|
|
230
|
+
configureState?: (state: AppState) => AppState;
|
|
231
|
+
configurePluginRegistry?: (pluginRegistry: PluginRegistry) => void;
|
|
232
|
+
dataProvider?: DataProvider;
|
|
233
|
+
hasPaneSettings?: (paneId: string) => boolean;
|
|
234
|
+
}) {
|
|
235
|
+
let config = {
|
|
236
|
+
...createDefaultConfig("/tmp/gloomberb-test"),
|
|
237
|
+
recentTickers: ["AAPL", "MSFT"],
|
|
238
|
+
disabledPlugins,
|
|
239
|
+
};
|
|
240
|
+
if (configureConfig) {
|
|
241
|
+
config = configureConfig(config);
|
|
242
|
+
}
|
|
243
|
+
const tickers = [makeTicker("AAPL", "Apple Inc."), makeTicker("MSFT", "Microsoft Corp."), ...extraTickers];
|
|
244
|
+
let state: AppState = {
|
|
245
|
+
...createInitialState(config),
|
|
246
|
+
commandBarOpen: true,
|
|
247
|
+
commandBarQuery: query,
|
|
248
|
+
tickers: new Map(tickers.map((ticker) => [ticker.metadata.ticker, ticker])),
|
|
249
|
+
config: { ...config, disabledPlugins },
|
|
250
|
+
paneState: selectedTicker
|
|
251
|
+
? {
|
|
252
|
+
"portfolio-list:main": {
|
|
253
|
+
collectionId: "main",
|
|
254
|
+
cursorSymbol: selectedTicker,
|
|
255
|
+
},
|
|
256
|
+
}
|
|
257
|
+
: createInitialState(config).paneState,
|
|
258
|
+
};
|
|
259
|
+
if (configureState) {
|
|
260
|
+
state = configureState(state);
|
|
261
|
+
}
|
|
262
|
+
const tickerRepository = {
|
|
263
|
+
loadTicker: async () => null,
|
|
264
|
+
createTicker: async (metadata: TickerRecord["metadata"]) => ({
|
|
265
|
+
metadata,
|
|
266
|
+
}),
|
|
267
|
+
saveTicker: async (ticker: TickerRecord) => {
|
|
268
|
+
onSaveTicker?.(ticker);
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
const pluginRegistry = makePluginRegistry(hasPaneSettings);
|
|
272
|
+
configurePluginRegistry?.(pluginRegistry);
|
|
273
|
+
const [liveState, dispatch] = useReducer(appReducer, state);
|
|
274
|
+
const currentState = live ? liveState : state;
|
|
275
|
+
const currentDispatch = live ? dispatch : () => {};
|
|
276
|
+
|
|
277
|
+
return (
|
|
278
|
+
<AppContext value={{ state: currentState, dispatch: currentDispatch }}>
|
|
279
|
+
<DialogProvider dialogOptions={{ style: { backgroundColor: "#000000", borderColor: "#ffffff", borderStyle: "single" } }}>
|
|
280
|
+
{live && <text>{`theme:${currentState.config.theme}`}</text>}
|
|
281
|
+
{showQueryState && <text>{`query:${currentState.commandBarQuery}`}</text>}
|
|
282
|
+
{currentState.commandBarOpen && (
|
|
283
|
+
<CommandBar
|
|
284
|
+
dataProvider={dataProvider}
|
|
285
|
+
tickerRepository={tickerRepository as any}
|
|
286
|
+
pluginRegistry={pluginRegistry}
|
|
287
|
+
quitApp={() => {}}
|
|
288
|
+
/>
|
|
289
|
+
)}
|
|
290
|
+
</DialogProvider>
|
|
291
|
+
</AppContext>
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
describe("CommandBar", () => {
|
|
296
|
+
test("renders the default layout with opencode-style chrome", async () => {
|
|
297
|
+
testSetup = await testRender(<CommandBarHarness query="" selectedTicker="AAPL" />, {
|
|
298
|
+
width: 80,
|
|
299
|
+
height: 24,
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
await testSetup.renderOnce();
|
|
303
|
+
|
|
304
|
+
expect(testSetup.captureCharFrame()).toMatchSnapshot();
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
test("renders theme prefix results in the root query", async () => {
|
|
308
|
+
testSetup = await testRender(<CommandBarHarness query="TH " />, {
|
|
309
|
+
width: 80,
|
|
310
|
+
height: 24,
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
await testSetup.renderOnce();
|
|
314
|
+
|
|
315
|
+
const frame = testSetup.captureCharFrame();
|
|
316
|
+
expect(frame).toContain("Commands");
|
|
317
|
+
expect(frame).toContain("TH");
|
|
318
|
+
expect(frame).toContain("Themes");
|
|
319
|
+
expect(frame).toMatchSnapshot();
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
test("renders plugin toggle state", async () => {
|
|
323
|
+
testSetup = await testRender(<CommandBarHarness query="PL " disabledPlugins={["notes"]} />, {
|
|
324
|
+
width: 80,
|
|
325
|
+
height: 24,
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
await testSetup.renderOnce();
|
|
329
|
+
|
|
330
|
+
expect(testSetup.captureCharFrame()).toMatchSnapshot();
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
test("only surfaces PS when the focused pane exposes settings", async () => {
|
|
334
|
+
testSetup = await testRender(
|
|
335
|
+
<CommandBarHarness query="PS" hasPaneSettings={(paneId) => paneId === "portfolio-list:main"} />,
|
|
336
|
+
{ width: 80, height: 24 },
|
|
337
|
+
);
|
|
338
|
+
|
|
339
|
+
await testSetup.renderOnce();
|
|
340
|
+
expect(testSetup.captureCharFrame()).toContain("Pane Settings");
|
|
341
|
+
|
|
342
|
+
testSetup.renderer.destroy();
|
|
343
|
+
testSetup = await testRender(<CommandBarHarness query="PS" hasPaneSettings={() => false} />, {
|
|
344
|
+
width: 80,
|
|
345
|
+
height: 24,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
await testSetup.renderOnce();
|
|
349
|
+
expect(testSetup.captureCharFrame()).not.toContain("Pane Settings");
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("collapses the trailing column on narrow terminals", async () => {
|
|
353
|
+
testSetup = await testRender(<CommandBarHarness query="" selectedTicker="AAPL" />, {
|
|
354
|
+
width: 42,
|
|
355
|
+
height: 20,
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
await testSetup.renderOnce();
|
|
359
|
+
|
|
360
|
+
expect(testSetup.captureCharFrame()).toMatchSnapshot();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
test("moves the theme selector with arrow keys", async () => {
|
|
364
|
+
testSetup = await testRender(<CommandBarHarness query="TH " live />, {
|
|
365
|
+
width: 80,
|
|
366
|
+
height: 24,
|
|
367
|
+
});
|
|
368
|
+
|
|
369
|
+
await testSetup.renderOnce();
|
|
370
|
+
await act(async () => {
|
|
371
|
+
testSetup!.mockInput.pressArrow("down");
|
|
372
|
+
await testSetup!.renderOnce();
|
|
373
|
+
});
|
|
374
|
+
await testSetup.renderOnce();
|
|
375
|
+
|
|
376
|
+
const frame = testSetup.captureCharFrame();
|
|
377
|
+
expect(frame).toContain("theme:green");
|
|
378
|
+
});
|
|
379
|
+
|
|
380
|
+
test("keeps typed prefixes in the root query until a result is activated", async () => {
|
|
381
|
+
testSetup = await testRender(<CommandBarHarness query="T " />, {
|
|
382
|
+
width: 80,
|
|
383
|
+
height: 24,
|
|
384
|
+
});
|
|
385
|
+
|
|
386
|
+
await testSetup.renderOnce();
|
|
387
|
+
|
|
388
|
+
let frame = testSetup.captureCharFrame();
|
|
389
|
+
expect(frame).toContain("Commands");
|
|
390
|
+
expect(frame).toContain("T");
|
|
391
|
+
expect(frame).toContain("Type a ticker symbol");
|
|
392
|
+
expect(frame).not.toContain("Back");
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test("QQ without an active ticker opens inline ticker search on enter", async () => {
|
|
396
|
+
testSetup = await testRender(<CommandBarHarness query="QQ" />, {
|
|
397
|
+
width: 100,
|
|
398
|
+
height: 20,
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
await testSetup.renderOnce();
|
|
402
|
+
expect(testSetup.captureCharFrame()).toContain("Quote Monitor");
|
|
403
|
+
expect(testSetup.captureCharFrame()).not.toContain("Back");
|
|
404
|
+
|
|
405
|
+
await act(async () => {
|
|
406
|
+
testSetup!.mockInput.pressEnter();
|
|
407
|
+
await testSetup!.renderOnce();
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
const frame = testSetup.captureCharFrame();
|
|
411
|
+
expect(frame).toContain("Back");
|
|
412
|
+
expect(frame).toContain("Search Ticker");
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("QQ with an active ticker shows ghost completion and tab inserts the symbol", async () => {
|
|
416
|
+
testSetup = await testRender(<CommandBarHarness query="QQ" live selectedTicker="AAPL" showQueryState />, {
|
|
417
|
+
width: 100,
|
|
418
|
+
height: 20,
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
await testSetup.renderOnce();
|
|
422
|
+
expect(testSetup.captureCharFrame()).toContain("QQ AAPL");
|
|
423
|
+
expect(testSetup.captureCharFrame()).toContain("Shortcut: Quote Monitor for AAPL");
|
|
424
|
+
expect(testSetup.captureCharFrame()).toContain("query:QQ");
|
|
425
|
+
|
|
426
|
+
await act(async () => {
|
|
427
|
+
testSetup!.mockInput.pressTab();
|
|
428
|
+
await testSetup!.renderOnce();
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
const frame = testSetup.captureCharFrame();
|
|
432
|
+
expect(frame).toContain("query:QQ AAPL");
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
test("typing a shorthand and pressing enter executes the inferred quote monitor shortcut", async () => {
|
|
436
|
+
const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
|
|
437
|
+
|
|
438
|
+
testSetup = await testRender(<CommandBarHarness
|
|
439
|
+
query=""
|
|
440
|
+
live
|
|
441
|
+
selectedTicker="AAPL"
|
|
442
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
443
|
+
pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
|
|
444
|
+
created.push({ templateId, options });
|
|
445
|
+
};
|
|
446
|
+
}}
|
|
447
|
+
/>, {
|
|
448
|
+
width: 100,
|
|
449
|
+
height: 20,
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
await testSetup.renderOnce();
|
|
453
|
+
|
|
454
|
+
await act(async () => {
|
|
455
|
+
await testSetup!.mockInput.typeText("QQ");
|
|
456
|
+
testSetup!.mockInput.pressEnter();
|
|
457
|
+
await Bun.sleep(0);
|
|
458
|
+
await testSetup!.renderOnce();
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
expect(created).toEqual([{
|
|
462
|
+
templateId: "quote-monitor-pane",
|
|
463
|
+
options: {
|
|
464
|
+
arg: "AAPL",
|
|
465
|
+
symbol: "AAPL",
|
|
466
|
+
ticker: makeTicker("AAPL", "Apple Inc."),
|
|
467
|
+
},
|
|
468
|
+
}]);
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
test("clears the root query with cmd-backspace", async () => {
|
|
472
|
+
testSetup = await testRender(<CommandBarHarness query="T AMD" />, {
|
|
473
|
+
width: 80,
|
|
474
|
+
height: 24,
|
|
475
|
+
});
|
|
476
|
+
|
|
477
|
+
await testSetup.renderOnce();
|
|
478
|
+
expect(testSetup.captureCharFrame()).toContain("T AMD");
|
|
479
|
+
|
|
480
|
+
await act(async () => {
|
|
481
|
+
testSetup!.mockInput.pressKey("backspace", { meta: true });
|
|
482
|
+
await testSetup!.renderOnce();
|
|
483
|
+
});
|
|
484
|
+
|
|
485
|
+
const frame = testSetup.captureCharFrame();
|
|
486
|
+
expect(frame).toContain("Commands");
|
|
487
|
+
expect(frame).toContain("Search");
|
|
488
|
+
expect(frame).not.toContain("T AMD");
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
test("pressing the close shortcut at the root closes the command bar", async () => {
|
|
492
|
+
testSetup = await testRender(<CommandBarHarness query="" live />, {
|
|
493
|
+
width: 80,
|
|
494
|
+
height: 24,
|
|
495
|
+
});
|
|
496
|
+
|
|
497
|
+
await testSetup.renderOnce();
|
|
498
|
+
|
|
499
|
+
await act(async () => {
|
|
500
|
+
testSetup!.mockInput.pressKey("`");
|
|
501
|
+
await testSetup!.renderOnce();
|
|
502
|
+
});
|
|
503
|
+
|
|
504
|
+
expect(testSetup.captureCharFrame()).not.toContain("Commands");
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
test("loads provider-backed ticker search results in the root command results", async () => {
|
|
508
|
+
testSetup = await testRender(
|
|
509
|
+
<CommandBarHarness
|
|
510
|
+
query="T appl"
|
|
511
|
+
dataProvider={makeDataProvider(async () => [
|
|
512
|
+
{ providerId: "yahoo", symbol: "IVSX", name: "Invsivx Holdings", exchange: "NYSE", type: "ETF" },
|
|
513
|
+
{ providerId: "yahoo", symbol: "AAPL", name: "Apple Inc", exchange: "NASDAQ", type: "EQUITY" },
|
|
514
|
+
{ providerId: "yahoo", symbol: "AMAT", name: "Applied Materials", exchange: "NASDAQ", type: "EQUITY" },
|
|
515
|
+
{ providerId: "yahoo", symbol: "AAOI", name: "Applied Optoelectronics", exchange: "NASDAQ", type: "EQUITY" },
|
|
516
|
+
{ providerId: "yahoo", symbol: "APP", name: "AppLovin Corp", exchange: "NASDAQ", type: "EQUITY" },
|
|
517
|
+
])}
|
|
518
|
+
/>,
|
|
519
|
+
{ width: 80, height: 24 },
|
|
520
|
+
);
|
|
521
|
+
|
|
522
|
+
await testSetup.renderOnce();
|
|
523
|
+
const frame = await waitForFrameToContain("AAOI");
|
|
524
|
+
expect(frame).toContain("Saved");
|
|
525
|
+
expect(frame).toContain("Other Listings");
|
|
526
|
+
expect(frame).toContain("APP");
|
|
527
|
+
expect(frame).toContain("AAOI");
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test("T MSFT opens an exact ticker directly", async () => {
|
|
531
|
+
const pinned: string[] = [];
|
|
532
|
+
|
|
533
|
+
testSetup = await testRender(
|
|
534
|
+
<CommandBarHarness
|
|
535
|
+
query="T MSFT"
|
|
536
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
537
|
+
pluginRegistry.pinTickerFn = (symbol) => {
|
|
538
|
+
pinned.push(symbol);
|
|
539
|
+
};
|
|
540
|
+
}}
|
|
541
|
+
/>,
|
|
542
|
+
{ width: 100, height: 20 },
|
|
543
|
+
);
|
|
544
|
+
|
|
545
|
+
await testSetup.renderOnce();
|
|
546
|
+
await act(async () => {
|
|
547
|
+
testSetup!.mockInput.pressEnter();
|
|
548
|
+
await Bun.sleep(0);
|
|
549
|
+
await testSetup!.renderOnce();
|
|
550
|
+
await testSetup!.renderOnce();
|
|
551
|
+
});
|
|
552
|
+
|
|
553
|
+
expect(pinned).toEqual(["MSFT"]);
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
test("AW AAPL uses the active watchlist target by default", async () => {
|
|
557
|
+
const saved: TickerRecord[] = [];
|
|
558
|
+
|
|
559
|
+
testSetup = await testRender(
|
|
560
|
+
<CommandBarHarness
|
|
561
|
+
query="AW AAPL"
|
|
562
|
+
onSaveTicker={(ticker) => {
|
|
563
|
+
saved.push(ticker);
|
|
564
|
+
}}
|
|
565
|
+
configureState={(state) => ({
|
|
566
|
+
...state,
|
|
567
|
+
paneState: {
|
|
568
|
+
...state.paneState,
|
|
569
|
+
"portfolio-list:main": {
|
|
570
|
+
collectionId: "watchlist",
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
})}
|
|
574
|
+
/>,
|
|
575
|
+
{ width: 100, height: 20 },
|
|
576
|
+
);
|
|
577
|
+
|
|
578
|
+
await testSetup.renderOnce();
|
|
579
|
+
await act(async () => {
|
|
580
|
+
testSetup!.mockInput.pressEnter();
|
|
581
|
+
await Bun.sleep(0);
|
|
582
|
+
await testSetup!.renderOnce();
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
expect(saved.at(-1)?.metadata.watchlists).toEqual(["watchlist"]);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
test("bare AW without a compatible active target opens inline target selection", async () => {
|
|
589
|
+
testSetup = await testRender(
|
|
590
|
+
<CommandBarHarness
|
|
591
|
+
query="AW"
|
|
592
|
+
selectedTicker="AAPL"
|
|
593
|
+
/>,
|
|
594
|
+
{ width: 100, height: 20 },
|
|
595
|
+
);
|
|
596
|
+
|
|
597
|
+
await testSetup.renderOnce();
|
|
598
|
+
await act(async () => {
|
|
599
|
+
testSetup!.mockInput.pressEnter();
|
|
600
|
+
await testSetup!.renderOnce();
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
const frame = testSetup.captureCharFrame();
|
|
604
|
+
expect(frame).toContain("Add AAPL to Watchlist");
|
|
605
|
+
expect(frame).toContain("Watchlist");
|
|
606
|
+
expect(frame).toContain("Back");
|
|
607
|
+
});
|
|
608
|
+
|
|
609
|
+
test("pressing enter after arrow navigation ignores stale mouse hover", async () => {
|
|
610
|
+
const pinned: string[] = [];
|
|
611
|
+
|
|
612
|
+
testSetup = await testRender(
|
|
613
|
+
<CommandBarHarness
|
|
614
|
+
query=""
|
|
615
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
616
|
+
pluginRegistry.pinTickerFn = (symbol) => {
|
|
617
|
+
pinned.push(symbol);
|
|
618
|
+
};
|
|
619
|
+
}}
|
|
620
|
+
/>,
|
|
621
|
+
{ width: 80, height: 24 },
|
|
622
|
+
);
|
|
623
|
+
|
|
624
|
+
await testSetup.renderOnce();
|
|
625
|
+
|
|
626
|
+
const frame = testSetup.captureCharFrame();
|
|
627
|
+
const rows = frame.split("\n");
|
|
628
|
+
const hoveredRow = rows.findIndex((line) => line.includes("AAPL"));
|
|
629
|
+
const hoveredCol = rows[hoveredRow]?.indexOf("AAPL") ?? -1;
|
|
630
|
+
|
|
631
|
+
expect(hoveredRow).toBeGreaterThanOrEqual(0);
|
|
632
|
+
expect(hoveredCol).toBeGreaterThanOrEqual(0);
|
|
633
|
+
|
|
634
|
+
await act(async () => {
|
|
635
|
+
await testSetup!.mockMouse.moveTo(hoveredCol + 1, hoveredRow);
|
|
636
|
+
await testSetup!.renderOnce();
|
|
637
|
+
});
|
|
638
|
+
|
|
639
|
+
await act(async () => {
|
|
640
|
+
testSetup!.mockInput.pressArrow("down");
|
|
641
|
+
await testSetup!.renderOnce();
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
await act(async () => {
|
|
645
|
+
testSetup!.mockInput.pressEnter();
|
|
646
|
+
await Bun.sleep(0);
|
|
647
|
+
await testSetup!.renderOnce();
|
|
648
|
+
});
|
|
649
|
+
|
|
650
|
+
expect(pinned).toEqual(["MSFT"]);
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
test("pressing enter follows the displayed grouped order and opens confirm for the highlighted destructive item", async () => {
|
|
654
|
+
const executed: string[] = [];
|
|
655
|
+
|
|
656
|
+
testSetup = await testRender(
|
|
657
|
+
<CommandBarHarness
|
|
658
|
+
query="delete"
|
|
659
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
660
|
+
(pluginRegistry.commands as Map<string, any>).set("delete-layout", {
|
|
661
|
+
id: "delete-layout",
|
|
662
|
+
label: "Delete Layout",
|
|
663
|
+
description: "Delete the current layout preset",
|
|
664
|
+
category: "Layout Manager",
|
|
665
|
+
execute: async () => {
|
|
666
|
+
executed.push("delete-layout");
|
|
667
|
+
},
|
|
668
|
+
});
|
|
669
|
+
}}
|
|
670
|
+
/>,
|
|
671
|
+
{ width: 100, height: 20 },
|
|
672
|
+
);
|
|
673
|
+
|
|
674
|
+
await testSetup.renderOnce();
|
|
675
|
+
const initialFrame = testSetup.captureCharFrame();
|
|
676
|
+
expect(initialFrame).toContain("Delete Layout");
|
|
677
|
+
expect(initialFrame).toContain("Danger");
|
|
678
|
+
|
|
679
|
+
await act(async () => {
|
|
680
|
+
testSetup!.mockInput.pressEnter();
|
|
681
|
+
await testSetup!.renderOnce();
|
|
682
|
+
});
|
|
683
|
+
|
|
684
|
+
let frame = testSetup.captureCharFrame();
|
|
685
|
+
expect(frame).toContain("Delete Layout");
|
|
686
|
+
expect(frame).toContain("Back");
|
|
687
|
+
expectSingleBackControl(frame);
|
|
688
|
+
expect(executed).toEqual([]);
|
|
689
|
+
|
|
690
|
+
await act(async () => {
|
|
691
|
+
testSetup!.mockInput.pressEnter();
|
|
692
|
+
await Bun.sleep(0);
|
|
693
|
+
await testSetup!.renderOnce();
|
|
694
|
+
});
|
|
695
|
+
|
|
696
|
+
expect(executed).toEqual(["delete-layout"]);
|
|
697
|
+
});
|
|
698
|
+
|
|
699
|
+
test("moves through long result lists with the mouse wheel", async () => {
|
|
700
|
+
testSetup = await testRender(
|
|
701
|
+
<CommandBarHarness
|
|
702
|
+
query="NP scratch"
|
|
703
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
704
|
+
const paneTemplates = pluginRegistry.paneTemplates as Map<string, any>;
|
|
705
|
+
for (let index = 0; index < 20; index++) {
|
|
706
|
+
const suffix = String(index).padStart(2, "0");
|
|
707
|
+
paneTemplates.set(`scratch-${suffix}`, {
|
|
708
|
+
id: `scratch-${suffix}`,
|
|
709
|
+
paneId: "chat",
|
|
710
|
+
label: `Scratch Pane ${suffix}`,
|
|
711
|
+
description: `Open scratch pane ${suffix}`,
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
}}
|
|
715
|
+
/>,
|
|
716
|
+
{ width: 100, height: 18 },
|
|
717
|
+
);
|
|
718
|
+
|
|
719
|
+
await testSetup.renderOnce();
|
|
720
|
+
|
|
721
|
+
const initialFrame = testSetup.captureCharFrame();
|
|
722
|
+
expect(initialFrame).toContain("Scratch Pane 00");
|
|
723
|
+
expect(initialFrame).not.toContain("Scratch Pane 12");
|
|
724
|
+
|
|
725
|
+
const rows = initialFrame.split("\n");
|
|
726
|
+
const scrollRow = rows.findIndex((line) => line.includes("Scratch Pane 00"));
|
|
727
|
+
const scrollCol = rows[scrollRow]?.indexOf("Scratch Pane 00") ?? -1;
|
|
728
|
+
|
|
729
|
+
expect(scrollRow).toBeGreaterThanOrEqual(0);
|
|
730
|
+
expect(scrollCol).toBeGreaterThanOrEqual(0);
|
|
731
|
+
|
|
732
|
+
await act(async () => {
|
|
733
|
+
for (let index = 0; index < 12; index++) {
|
|
734
|
+
await testSetup!.mockMouse.scroll(scrollCol + 1, scrollRow, "down");
|
|
735
|
+
await testSetup!.renderOnce();
|
|
736
|
+
}
|
|
737
|
+
});
|
|
738
|
+
|
|
739
|
+
const scrolledFrame = testSetup.captureCharFrame();
|
|
740
|
+
expect(scrolledFrame).not.toContain("Scratch Pane 00");
|
|
741
|
+
expect(scrolledFrame).toContain("Scratch Pane 12");
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
test("closes when clicking outside the command bar", async () => {
|
|
745
|
+
testSetup = await testRender(<CommandBarHarness query="" live />, {
|
|
746
|
+
width: 80,
|
|
747
|
+
height: 24,
|
|
748
|
+
});
|
|
749
|
+
|
|
750
|
+
await testSetup.renderOnce();
|
|
751
|
+
|
|
752
|
+
await act(async () => {
|
|
753
|
+
await testSetup!.mockMouse.click(0, 0);
|
|
754
|
+
await testSetup!.renderOnce();
|
|
755
|
+
});
|
|
756
|
+
await testSetup.renderOnce();
|
|
757
|
+
|
|
758
|
+
expect(testSetup.captureCharFrame()).not.toContain("Commands");
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const layoutModeConfig = (config: AppConfig): AppConfig => {
|
|
762
|
+
const research = cloneLayout(config.layout);
|
|
763
|
+
research.dockRoot = { kind: "pane", instanceId: "portfolio-list:main" };
|
|
764
|
+
research.floating = [{ instanceId: "ticker-detail:main", x: 8, y: 2, width: 36, height: 12 }];
|
|
765
|
+
return {
|
|
766
|
+
...config,
|
|
767
|
+
layouts: [
|
|
768
|
+
{ name: "Default", layout: cloneLayout(config.layout) },
|
|
769
|
+
{ name: "Research", layout: research },
|
|
770
|
+
],
|
|
771
|
+
};
|
|
772
|
+
};
|
|
773
|
+
|
|
774
|
+
const layoutModeState = (state: AppState): AppState => ({
|
|
775
|
+
...state,
|
|
776
|
+
layoutHistory: {
|
|
777
|
+
0: {
|
|
778
|
+
past: [cloneLayout(state.config.layout)],
|
|
779
|
+
future: [],
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
});
|
|
783
|
+
|
|
784
|
+
test("renders layout mode with focused pane actions", async () => {
|
|
785
|
+
testSetup = await testRender(<CommandBarHarness
|
|
786
|
+
query="LAY "
|
|
787
|
+
configureConfig={layoutModeConfig}
|
|
788
|
+
configureState={layoutModeState}
|
|
789
|
+
/>, {
|
|
790
|
+
width: 90,
|
|
791
|
+
height: 28,
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
await testSetup.renderOnce();
|
|
795
|
+
|
|
796
|
+
const frame = testSetup.captureCharFrame();
|
|
797
|
+
expect(frame).toContain("Focused Pane");
|
|
798
|
+
expect(frame).toContain("Float Pane");
|
|
799
|
+
expect(frame).toContain("Undo Layout Change");
|
|
800
|
+
expect(frame).toContain("Current Layout");
|
|
801
|
+
});
|
|
802
|
+
|
|
803
|
+
test("renders filtered saved layouts with textual previews", async () => {
|
|
804
|
+
testSetup = await testRender(<CommandBarHarness
|
|
805
|
+
query="LAY Research"
|
|
806
|
+
configureConfig={layoutModeConfig}
|
|
807
|
+
configureState={layoutModeState}
|
|
808
|
+
/>, {
|
|
809
|
+
width: 120,
|
|
810
|
+
height: 18,
|
|
811
|
+
});
|
|
812
|
+
|
|
813
|
+
await testSetup.renderOnce();
|
|
814
|
+
|
|
815
|
+
const frame = testSetup.captureCharFrame();
|
|
816
|
+
expect(frame).toContain("Research");
|
|
817
|
+
expect(frame).toContain("1c / 1d");
|
|
818
|
+
});
|
|
819
|
+
|
|
820
|
+
test("renders new pane mode with plugin-defined pane templates", async () => {
|
|
821
|
+
testSetup = await testRender(<CommandBarHarness query="NP chat" />, {
|
|
822
|
+
width: 100,
|
|
823
|
+
height: 18,
|
|
824
|
+
});
|
|
825
|
+
|
|
826
|
+
await testSetup.renderOnce();
|
|
827
|
+
|
|
828
|
+
const frame = testSetup.captureCharFrame();
|
|
829
|
+
expect(frame).toContain("New Chat Pane");
|
|
830
|
+
expect(frame).toContain("float");
|
|
831
|
+
});
|
|
832
|
+
|
|
833
|
+
test("shows pane shortcuts in the default browse results", async () => {
|
|
834
|
+
testSetup = await testRender(<CommandBarHarness query="" selectedTicker="AAPL" />, {
|
|
835
|
+
width: 100,
|
|
836
|
+
height: 24,
|
|
837
|
+
});
|
|
838
|
+
|
|
839
|
+
await testSetup.renderOnce();
|
|
840
|
+
|
|
841
|
+
const frame = testSetup.captureCharFrame();
|
|
842
|
+
expect(frame).toContain("Panes");
|
|
843
|
+
expect(frame).toContain("Quote Monitor");
|
|
844
|
+
expect(frame).toContain("QQ");
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
test("matches direct pane shortcut queries", async () => {
|
|
848
|
+
testSetup = await testRender(<CommandBarHarness query="QQ MSFT" selectedTicker="AAPL" />, {
|
|
849
|
+
width: 100,
|
|
850
|
+
height: 18,
|
|
851
|
+
});
|
|
852
|
+
|
|
853
|
+
await testSetup.renderOnce();
|
|
854
|
+
|
|
855
|
+
const frame = testSetup.captureCharFrame();
|
|
856
|
+
expect(frame).toContain("Quote Monitor");
|
|
857
|
+
expect(frame).toContain("QQ");
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
test("opens plugin command wizards inline inside the command bar", async () => {
|
|
861
|
+
let submittedValues: Record<string, string> | undefined;
|
|
862
|
+
|
|
863
|
+
testSetup = await testRender(<CommandBarHarness
|
|
864
|
+
query="Plugin Login"
|
|
865
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
866
|
+
(pluginRegistry.commands as Map<string, any>).set("plugin:login", {
|
|
867
|
+
id: "plugin:login",
|
|
868
|
+
label: "Plugin Login",
|
|
869
|
+
description: "Authenticate without leaving the command bar",
|
|
870
|
+
category: "config",
|
|
871
|
+
wizard: [
|
|
872
|
+
{ key: "username", label: "Username", type: "text", placeholder: "vince" },
|
|
873
|
+
{ key: "password", label: "Password", type: "password", placeholder: "secret" },
|
|
874
|
+
{ key: "_validate", label: "Validating", type: "info", body: ["Validating…", "Connected."] },
|
|
875
|
+
],
|
|
876
|
+
execute: async (values?: Record<string, string>) => {
|
|
877
|
+
submittedValues = values;
|
|
878
|
+
},
|
|
879
|
+
} as any);
|
|
880
|
+
pluginRegistry.getCommandPluginId = (commandId: string) => (
|
|
881
|
+
commandId === "plugin:login" ? "notes" : "news"
|
|
882
|
+
);
|
|
883
|
+
}}
|
|
884
|
+
/>, {
|
|
885
|
+
width: 100,
|
|
886
|
+
height: 24,
|
|
887
|
+
});
|
|
888
|
+
|
|
889
|
+
await testSetup.renderOnce();
|
|
890
|
+
await act(async () => {
|
|
891
|
+
testSetup!.mockInput.pressEnter();
|
|
892
|
+
await testSetup!.renderOnce();
|
|
893
|
+
});
|
|
894
|
+
|
|
895
|
+
let frame = testSetup.captureCharFrame();
|
|
896
|
+
expect(frame).toContain("Plugin Login");
|
|
897
|
+
expect(frame).toContain("Username");
|
|
898
|
+
expect(frame).toContain("Password");
|
|
899
|
+
|
|
900
|
+
await act(async () => {
|
|
901
|
+
await testSetup!.mockInput.typeText("vince");
|
|
902
|
+
await testSetup!.renderOnce();
|
|
903
|
+
});
|
|
904
|
+
await act(async () => {
|
|
905
|
+
testSetup!.mockInput.pressTab();
|
|
906
|
+
await testSetup!.renderOnce();
|
|
907
|
+
});
|
|
908
|
+
await act(async () => {
|
|
909
|
+
await testSetup!.mockInput.typeText("secret");
|
|
910
|
+
await testSetup!.renderOnce();
|
|
911
|
+
});
|
|
912
|
+
|
|
913
|
+
frame = await waitForFrameToContain("******");
|
|
914
|
+
expect(frame).toContain("******");
|
|
915
|
+
expect(frame).not.toContain("secret");
|
|
916
|
+
expectSingleBackControl(frame);
|
|
917
|
+
|
|
918
|
+
await act(async () => {
|
|
919
|
+
testSetup!.mockInput.pressEnter();
|
|
920
|
+
await Bun.sleep(0);
|
|
921
|
+
await testSetup!.renderOnce();
|
|
922
|
+
});
|
|
923
|
+
|
|
924
|
+
expect(submittedValues).toEqual({
|
|
925
|
+
username: "vince",
|
|
926
|
+
password: "secret",
|
|
927
|
+
});
|
|
928
|
+
});
|
|
929
|
+
|
|
930
|
+
test("QQ MSFT executes directly without opening a secondary workflow", async () => {
|
|
931
|
+
const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
|
|
932
|
+
|
|
933
|
+
testSetup = await testRender(<CommandBarHarness
|
|
934
|
+
query="QQ MSFT"
|
|
935
|
+
selectedTicker="AAPL"
|
|
936
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
937
|
+
pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
|
|
938
|
+
created.push({ templateId, options });
|
|
939
|
+
};
|
|
940
|
+
}}
|
|
941
|
+
/>, {
|
|
942
|
+
width: 100,
|
|
943
|
+
height: 20,
|
|
944
|
+
});
|
|
945
|
+
|
|
946
|
+
await testSetup.renderOnce();
|
|
947
|
+
await act(async () => {
|
|
948
|
+
testSetup!.mockInput.pressEnter();
|
|
949
|
+
await Bun.sleep(0);
|
|
950
|
+
await testSetup!.renderOnce();
|
|
951
|
+
});
|
|
952
|
+
|
|
953
|
+
expect(created).toEqual([{
|
|
954
|
+
templateId: "quote-monitor-pane",
|
|
955
|
+
options: {
|
|
956
|
+
arg: "MSFT",
|
|
957
|
+
symbol: "MSFT",
|
|
958
|
+
ticker: makeTicker("MSFT", "Microsoft Corp."),
|
|
959
|
+
},
|
|
960
|
+
}]);
|
|
961
|
+
});
|
|
962
|
+
|
|
963
|
+
test("CMP AAPL,MSFT creates the comparison chart directly", async () => {
|
|
964
|
+
const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
|
|
965
|
+
|
|
966
|
+
testSetup = await testRender(<CommandBarHarness
|
|
967
|
+
query="CMP AAPL,MSFT"
|
|
968
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
969
|
+
(pluginRegistry.panes as Map<string, any>).set("comparison-chart", {
|
|
970
|
+
id: "comparison-chart",
|
|
971
|
+
name: "Comparison Chart",
|
|
972
|
+
component: () => null,
|
|
973
|
+
defaultPosition: "right",
|
|
974
|
+
});
|
|
975
|
+
(pluginRegistry.paneTemplates as Map<string, any>).set("comparison-chart-pane", {
|
|
976
|
+
id: "comparison-chart-pane",
|
|
977
|
+
paneId: "comparison-chart",
|
|
978
|
+
label: "Comparison Chart",
|
|
979
|
+
description: "Compare multiple symbols in one pane",
|
|
980
|
+
shortcut: { prefix: "CMP", argPlaceholder: "tickers", argKind: "ticker-list" },
|
|
981
|
+
});
|
|
982
|
+
pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
|
|
983
|
+
created.push({ templateId, options });
|
|
984
|
+
};
|
|
985
|
+
}}
|
|
986
|
+
/>, {
|
|
987
|
+
width: 100,
|
|
988
|
+
height: 20,
|
|
989
|
+
});
|
|
990
|
+
|
|
991
|
+
await testSetup.renderOnce();
|
|
992
|
+
await act(async () => {
|
|
993
|
+
testSetup!.mockInput.pressEnter();
|
|
994
|
+
await Bun.sleep(0);
|
|
995
|
+
await testSetup!.renderOnce();
|
|
996
|
+
});
|
|
997
|
+
|
|
998
|
+
expect(created).toEqual([{
|
|
999
|
+
templateId: "comparison-chart-pane",
|
|
1000
|
+
options: {
|
|
1001
|
+
arg: "AAPL,MSFT",
|
|
1002
|
+
symbols: ["AAPL", "MSFT"],
|
|
1003
|
+
},
|
|
1004
|
+
}]);
|
|
1005
|
+
});
|
|
1006
|
+
|
|
1007
|
+
test("CMP AAPL, opens inline completion when the ticker list is incomplete", async () => {
|
|
1008
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1009
|
+
query="CMP AAPL,"
|
|
1010
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1011
|
+
(pluginRegistry.panes as Map<string, any>).set("comparison-chart", {
|
|
1012
|
+
id: "comparison-chart",
|
|
1013
|
+
name: "Comparison Chart",
|
|
1014
|
+
component: () => null,
|
|
1015
|
+
defaultPosition: "right",
|
|
1016
|
+
});
|
|
1017
|
+
(pluginRegistry.paneTemplates as Map<string, any>).set("comparison-chart-pane", {
|
|
1018
|
+
id: "comparison-chart-pane",
|
|
1019
|
+
paneId: "comparison-chart",
|
|
1020
|
+
label: "Comparison Chart",
|
|
1021
|
+
description: "Compare multiple symbols in one pane",
|
|
1022
|
+
shortcut: { prefix: "CMP", argPlaceholder: "tickers", argKind: "ticker-list" },
|
|
1023
|
+
});
|
|
1024
|
+
}}
|
|
1025
|
+
/>, {
|
|
1026
|
+
width: 100,
|
|
1027
|
+
height: 20,
|
|
1028
|
+
});
|
|
1029
|
+
|
|
1030
|
+
await testSetup.renderOnce();
|
|
1031
|
+
await act(async () => {
|
|
1032
|
+
testSetup!.mockInput.pressEnter();
|
|
1033
|
+
await testSetup!.renderOnce();
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
const frame = testSetup.captureCharFrame();
|
|
1037
|
+
expect(frame).toContain("Comparison Chart");
|
|
1038
|
+
expect(frame).toContain("Tickers");
|
|
1039
|
+
expect(frame).toContain("AAPL,");
|
|
1040
|
+
});
|
|
1041
|
+
|
|
1042
|
+
test("AI <prompt> opens the inline workflow and prefills the textarea prompt", async () => {
|
|
1043
|
+
const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
|
|
1044
|
+
|
|
1045
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1046
|
+
query="AI quality compounders"
|
|
1047
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1048
|
+
(pluginRegistry.panes as Map<string, any>).set("ai-screener", {
|
|
1049
|
+
id: "ai-screener",
|
|
1050
|
+
name: "AI Screener",
|
|
1051
|
+
component: () => null,
|
|
1052
|
+
defaultPosition: "right",
|
|
1053
|
+
defaultMode: "floating",
|
|
1054
|
+
});
|
|
1055
|
+
(pluginRegistry.paneTemplates as Map<string, any>).set("new-ai-screener-pane", {
|
|
1056
|
+
id: "new-ai-screener-pane",
|
|
1057
|
+
paneId: "ai-screener",
|
|
1058
|
+
label: "AI Screener",
|
|
1059
|
+
description: "Create a prompt-driven screener pane.",
|
|
1060
|
+
shortcut: { prefix: "AI", argPlaceholder: "prompt", argKind: "text" },
|
|
1061
|
+
wizard: [
|
|
1062
|
+
{
|
|
1063
|
+
key: "providerId",
|
|
1064
|
+
label: "AI Provider",
|
|
1065
|
+
type: "select",
|
|
1066
|
+
defaultValue: "claude",
|
|
1067
|
+
options: [{ label: "Claude", value: "claude" }],
|
|
1068
|
+
},
|
|
1069
|
+
{
|
|
1070
|
+
key: "prompt",
|
|
1071
|
+
label: "Screener Prompt",
|
|
1072
|
+
type: "textarea",
|
|
1073
|
+
},
|
|
1074
|
+
],
|
|
1075
|
+
});
|
|
1076
|
+
pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
|
|
1077
|
+
created.push({ templateId, options });
|
|
1078
|
+
};
|
|
1079
|
+
}}
|
|
1080
|
+
/>, {
|
|
1081
|
+
width: 100,
|
|
1082
|
+
height: 24,
|
|
1083
|
+
});
|
|
1084
|
+
|
|
1085
|
+
await testSetup.renderOnce();
|
|
1086
|
+
await act(async () => {
|
|
1087
|
+
testSetup!.mockInput.pressEnter();
|
|
1088
|
+
await testSetup!.renderOnce();
|
|
1089
|
+
});
|
|
1090
|
+
|
|
1091
|
+
const frame = testSetup.captureCharFrame();
|
|
1092
|
+
expect(frame).toContain("AI Screener");
|
|
1093
|
+
expect(frame).toContain("AI Provider");
|
|
1094
|
+
expect(frame).toContain("quality compounders");
|
|
1095
|
+
expect(created).toEqual([]);
|
|
1096
|
+
});
|
|
1097
|
+
|
|
1098
|
+
test("submits typed AI screener prompts from the textarea field", async () => {
|
|
1099
|
+
const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
|
|
1100
|
+
|
|
1101
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1102
|
+
query="AI"
|
|
1103
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1104
|
+
(pluginRegistry.panes as Map<string, any>).set("ai-screener", {
|
|
1105
|
+
id: "ai-screener",
|
|
1106
|
+
name: "AI Screener",
|
|
1107
|
+
component: () => null,
|
|
1108
|
+
defaultPosition: "right",
|
|
1109
|
+
defaultMode: "floating",
|
|
1110
|
+
});
|
|
1111
|
+
(pluginRegistry.paneTemplates as Map<string, any>).set("new-ai-screener-pane", {
|
|
1112
|
+
id: "new-ai-screener-pane",
|
|
1113
|
+
paneId: "ai-screener",
|
|
1114
|
+
label: "AI Screener",
|
|
1115
|
+
description: "Create a prompt-driven screener pane.",
|
|
1116
|
+
shortcut: { prefix: "AI", argPlaceholder: "prompt", argKind: "text" },
|
|
1117
|
+
wizard: [
|
|
1118
|
+
{
|
|
1119
|
+
key: "providerId",
|
|
1120
|
+
label: "AI Provider",
|
|
1121
|
+
type: "select",
|
|
1122
|
+
defaultValue: "claude",
|
|
1123
|
+
options: [{ label: "Claude", value: "claude" }],
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
key: "prompt",
|
|
1127
|
+
label: "Screener Prompt",
|
|
1128
|
+
type: "textarea",
|
|
1129
|
+
},
|
|
1130
|
+
],
|
|
1131
|
+
});
|
|
1132
|
+
pluginRegistry.createPaneFromTemplateAsyncFn = async (templateId, options) => {
|
|
1133
|
+
created.push({ templateId, options });
|
|
1134
|
+
};
|
|
1135
|
+
}}
|
|
1136
|
+
/>, {
|
|
1137
|
+
width: 100,
|
|
1138
|
+
height: 24,
|
|
1139
|
+
});
|
|
1140
|
+
|
|
1141
|
+
await testSetup.renderOnce();
|
|
1142
|
+
await act(async () => {
|
|
1143
|
+
testSetup!.mockInput.pressEnter();
|
|
1144
|
+
await testSetup!.renderOnce();
|
|
1145
|
+
});
|
|
1146
|
+
await act(async () => {
|
|
1147
|
+
testSetup!.mockInput.pressTab();
|
|
1148
|
+
await testSetup!.renderOnce();
|
|
1149
|
+
});
|
|
1150
|
+
await act(async () => {
|
|
1151
|
+
await testSetup!.mockInput.typeText("humanoid robot suppliers");
|
|
1152
|
+
await testSetup!.renderOnce();
|
|
1153
|
+
});
|
|
1154
|
+
await clickFrameText("Create Pane");
|
|
1155
|
+
await act(async () => {
|
|
1156
|
+
await Bun.sleep(0);
|
|
1157
|
+
await testSetup!.renderOnce();
|
|
1158
|
+
});
|
|
1159
|
+
|
|
1160
|
+
expect(created).toEqual([{
|
|
1161
|
+
templateId: "new-ai-screener-pane",
|
|
1162
|
+
options: {
|
|
1163
|
+
arg: "humanoid robot suppliers",
|
|
1164
|
+
values: {
|
|
1165
|
+
providerId: "claude",
|
|
1166
|
+
prompt: "humanoid robot suppliers",
|
|
1167
|
+
},
|
|
1168
|
+
},
|
|
1169
|
+
}]);
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
test("edits pane settings inline inside the command bar", async () => {
|
|
1173
|
+
const appliedValues: Array<{ paneId: string; key: string; value: unknown }> = [];
|
|
1174
|
+
|
|
1175
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1176
|
+
query="PS"
|
|
1177
|
+
configureState={(state) => ({
|
|
1178
|
+
...state,
|
|
1179
|
+
focusedPaneId: "quote-monitor:main",
|
|
1180
|
+
})}
|
|
1181
|
+
hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
|
|
1182
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1183
|
+
pluginRegistry.resolvePaneSettings = () => ({
|
|
1184
|
+
paneId: "quote-monitor:main",
|
|
1185
|
+
pane: {
|
|
1186
|
+
instanceId: "quote-monitor:main",
|
|
1187
|
+
paneId: "quote-monitor",
|
|
1188
|
+
title: "Quote Monitor",
|
|
1189
|
+
settings: {},
|
|
1190
|
+
},
|
|
1191
|
+
paneDef: pluginRegistry.panes.get("quote-monitor")!,
|
|
1192
|
+
settingsDef: {
|
|
1193
|
+
title: "Quote Monitor Settings",
|
|
1194
|
+
fields: [{
|
|
1195
|
+
key: "symbol",
|
|
1196
|
+
label: "Symbol",
|
|
1197
|
+
type: "text",
|
|
1198
|
+
description: "Ticker symbol to track",
|
|
1199
|
+
}],
|
|
1200
|
+
},
|
|
1201
|
+
context: {
|
|
1202
|
+
config: createDefaultConfig("/tmp/gloomberb-test"),
|
|
1203
|
+
layout: cloneLayout(createDefaultConfig("/tmp/gloomberb-test").layout),
|
|
1204
|
+
paneId: "quote-monitor:main",
|
|
1205
|
+
paneType: "quote-monitor",
|
|
1206
|
+
pane: {
|
|
1207
|
+
instanceId: "quote-monitor:main",
|
|
1208
|
+
paneId: "quote-monitor",
|
|
1209
|
+
title: "Quote Monitor",
|
|
1210
|
+
settings: {},
|
|
1211
|
+
},
|
|
1212
|
+
settings: {},
|
|
1213
|
+
paneState: {},
|
|
1214
|
+
activeTicker: "AAPL",
|
|
1215
|
+
activeCollectionId: "main",
|
|
1216
|
+
},
|
|
1217
|
+
}) as any;
|
|
1218
|
+
pluginRegistry.applyPaneSettingValueFn = async (paneId, field, value) => {
|
|
1219
|
+
appliedValues.push({ paneId, key: field.key, value });
|
|
1220
|
+
};
|
|
1221
|
+
}}
|
|
1222
|
+
/>, {
|
|
1223
|
+
width: 100,
|
|
1224
|
+
height: 20,
|
|
1225
|
+
});
|
|
1226
|
+
|
|
1227
|
+
await testSetup.renderOnce();
|
|
1228
|
+
|
|
1229
|
+
await act(async () => {
|
|
1230
|
+
testSetup!.mockInput.pressEnter();
|
|
1231
|
+
await testSetup!.renderOnce();
|
|
1232
|
+
});
|
|
1233
|
+
let frame = testSetup.captureCharFrame();
|
|
1234
|
+
expect(frame).toContain("Quote Monitor Settings");
|
|
1235
|
+
expect(frame).toContain("Symbol");
|
|
1236
|
+
|
|
1237
|
+
await act(async () => {
|
|
1238
|
+
testSetup!.mockInput.pressEnter();
|
|
1239
|
+
await testSetup!.renderOnce();
|
|
1240
|
+
});
|
|
1241
|
+
frame = testSetup.captureCharFrame();
|
|
1242
|
+
await clickFrameText("Symbol");
|
|
1243
|
+
frame = testSetup.captureCharFrame();
|
|
1244
|
+
expect(frame).toContain("Apply");
|
|
1245
|
+
expect(frame).toContain("Symbol");
|
|
1246
|
+
|
|
1247
|
+
await act(async () => {
|
|
1248
|
+
await testSetup!.mockInput.typeText("MSFT");
|
|
1249
|
+
await testSetup!.renderOnce();
|
|
1250
|
+
});
|
|
1251
|
+
await act(async () => {
|
|
1252
|
+
testSetup!.mockInput.pressEnter();
|
|
1253
|
+
await Bun.sleep(0);
|
|
1254
|
+
await testSetup!.renderOnce();
|
|
1255
|
+
});
|
|
1256
|
+
|
|
1257
|
+
expect(appliedValues).toEqual([{
|
|
1258
|
+
paneId: "quote-monitor:main",
|
|
1259
|
+
key: "symbol",
|
|
1260
|
+
value: "MSFT",
|
|
1261
|
+
}]);
|
|
1262
|
+
});
|
|
1263
|
+
|
|
1264
|
+
test("renders pane-setting multi-select pickers only once inside the command bar", async () => {
|
|
1265
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1266
|
+
query="PS"
|
|
1267
|
+
configureState={(state) => ({
|
|
1268
|
+
...state,
|
|
1269
|
+
focusedPaneId: "quote-monitor:main",
|
|
1270
|
+
})}
|
|
1271
|
+
hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
|
|
1272
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1273
|
+
pluginRegistry.resolvePaneSettings = () => ({
|
|
1274
|
+
paneId: "quote-monitor:main",
|
|
1275
|
+
pane: {
|
|
1276
|
+
instanceId: "quote-monitor:main",
|
|
1277
|
+
paneId: "quote-monitor",
|
|
1278
|
+
title: "Quote Monitor",
|
|
1279
|
+
settings: {},
|
|
1280
|
+
},
|
|
1281
|
+
paneDef: pluginRegistry.panes.get("quote-monitor")!,
|
|
1282
|
+
settingsDef: {
|
|
1283
|
+
title: "Quote Monitor Settings",
|
|
1284
|
+
fields: [{
|
|
1285
|
+
key: "columns",
|
|
1286
|
+
label: "Columns",
|
|
1287
|
+
type: "ordered-multi-select",
|
|
1288
|
+
description: "Visible columns",
|
|
1289
|
+
options: [
|
|
1290
|
+
{ value: "volume", label: "AAA", description: "Volume column" },
|
|
1291
|
+
{ value: "spread", label: "BBB", description: "Spread column" },
|
|
1292
|
+
{ value: "beta", label: "CCC", description: "Beta column" },
|
|
1293
|
+
],
|
|
1294
|
+
}],
|
|
1295
|
+
},
|
|
1296
|
+
context: {
|
|
1297
|
+
config: createDefaultConfig("/tmp/gloomberb-test"),
|
|
1298
|
+
layout: cloneLayout(createDefaultConfig("/tmp/gloomberb-test").layout),
|
|
1299
|
+
paneId: "quote-monitor:main",
|
|
1300
|
+
paneType: "quote-monitor",
|
|
1301
|
+
pane: {
|
|
1302
|
+
instanceId: "quote-monitor:main",
|
|
1303
|
+
paneId: "quote-monitor",
|
|
1304
|
+
title: "Quote Monitor",
|
|
1305
|
+
settings: {},
|
|
1306
|
+
},
|
|
1307
|
+
settings: {
|
|
1308
|
+
columns: ["volume", "spread"],
|
|
1309
|
+
},
|
|
1310
|
+
paneState: {},
|
|
1311
|
+
activeTicker: "AAPL",
|
|
1312
|
+
activeCollectionId: "main",
|
|
1313
|
+
},
|
|
1314
|
+
}) as any;
|
|
1315
|
+
}}
|
|
1316
|
+
/>, {
|
|
1317
|
+
width: 100,
|
|
1318
|
+
height: 20,
|
|
1319
|
+
});
|
|
1320
|
+
|
|
1321
|
+
await testSetup.renderOnce();
|
|
1322
|
+
|
|
1323
|
+
await act(async () => {
|
|
1324
|
+
testSetup!.mockInput.pressEnter();
|
|
1325
|
+
await testSetup!.renderOnce();
|
|
1326
|
+
});
|
|
1327
|
+
await clickFrameText("Columns");
|
|
1328
|
+
|
|
1329
|
+
const frame = await waitForFrameToContain("Done");
|
|
1330
|
+
expect(frame).toContain("Columns");
|
|
1331
|
+
expect(frame).toContain("Done");
|
|
1332
|
+
expect(frame).not.toContain("Options");
|
|
1333
|
+
expectSingleBackControl(frame);
|
|
1334
|
+
});
|
|
1335
|
+
|
|
1336
|
+
test("matches the IBKR trading shortcut when a gateway profile exists", async () => {
|
|
1337
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1338
|
+
query="IBKR"
|
|
1339
|
+
configureConfig={(config) => ({
|
|
1340
|
+
...config,
|
|
1341
|
+
brokerInstances: [{
|
|
1342
|
+
id: "ibkr-paper",
|
|
1343
|
+
brokerType: "ibkr",
|
|
1344
|
+
label: "Paper",
|
|
1345
|
+
connectionMode: "gateway",
|
|
1346
|
+
config: { connectionMode: "gateway", gateway: { host: "127.0.0.1", port: 4002, clientId: 1 } },
|
|
1347
|
+
enabled: true,
|
|
1348
|
+
}],
|
|
1349
|
+
})}
|
|
1350
|
+
/>, {
|
|
1351
|
+
width: 100,
|
|
1352
|
+
height: 18,
|
|
1353
|
+
});
|
|
1354
|
+
|
|
1355
|
+
await testSetup.renderOnce();
|
|
1356
|
+
|
|
1357
|
+
const frame = testSetup.captureCharFrame();
|
|
1358
|
+
expect(frame).toContain("New IBKR Trading Pane");
|
|
1359
|
+
expect(frame).toContain("IBKR");
|
|
1360
|
+
});
|
|
1361
|
+
|
|
1362
|
+
test("matches the direct help command", async () => {
|
|
1363
|
+
testSetup = await testRender(<CommandBarHarness query="help" />, {
|
|
1364
|
+
width: 80,
|
|
1365
|
+
height: 18,
|
|
1366
|
+
});
|
|
1367
|
+
|
|
1368
|
+
await testSetup.renderOnce();
|
|
1369
|
+
|
|
1370
|
+
const frame = testSetup.captureCharFrame();
|
|
1371
|
+
expect(frame).toContain("Help");
|
|
1372
|
+
expect(frame).toContain("Navigation");
|
|
1373
|
+
});
|
|
1374
|
+
|
|
1375
|
+
test("skips pane templates whose canCreate throws", async () => {
|
|
1376
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1377
|
+
query=""
|
|
1378
|
+
selectedTicker="AAPL"
|
|
1379
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1380
|
+
(pluginRegistry.paneTemplates as Map<string, any>).set("broken-pane", {
|
|
1381
|
+
id: "broken-pane",
|
|
1382
|
+
paneId: "chat",
|
|
1383
|
+
label: "Broken Pane",
|
|
1384
|
+
description: "Should not crash the command bar",
|
|
1385
|
+
shortcut: { prefix: "IBKR" },
|
|
1386
|
+
canCreate: () => {
|
|
1387
|
+
throw new ReferenceError("getIbkrInstances is not defined");
|
|
1388
|
+
},
|
|
1389
|
+
});
|
|
1390
|
+
}}
|
|
1391
|
+
/>, {
|
|
1392
|
+
width: 100,
|
|
1393
|
+
height: 24,
|
|
1394
|
+
});
|
|
1395
|
+
|
|
1396
|
+
await testSetup.renderOnce();
|
|
1397
|
+
|
|
1398
|
+
const frame = testSetup.captureCharFrame();
|
|
1399
|
+
expect(frame).toContain("Quote Monitor");
|
|
1400
|
+
expect(frame).not.toContain("Broken Pane");
|
|
1401
|
+
});
|
|
1402
|
+
|
|
1403
|
+
test("groups ticker search sections and keeps saved matches above looser provider results", async () => {
|
|
1404
|
+
testSetup = await testRender(
|
|
1405
|
+
<CommandBarHarness
|
|
1406
|
+
query="T appl"
|
|
1407
|
+
dataProvider={makeDataProvider(async () => [
|
|
1408
|
+
{ providerId: "yahoo", symbol: "IVSX", name: "Invsivx Holdings", exchange: "NYSE", type: "ETF" },
|
|
1409
|
+
{ providerId: "yahoo", symbol: "AAPL", name: "Apple Inc", exchange: "NASDAQ", type: "EQUITY" },
|
|
1410
|
+
{ providerId: "yahoo", symbol: "AMAT", name: "Applied Materials", exchange: "NASDAQ", type: "EQUITY" },
|
|
1411
|
+
{ providerId: "yahoo", symbol: "AAOI", name: "Applied Optoelectronics", exchange: "NASDAQ", type: "EQUITY" },
|
|
1412
|
+
{ providerId: "yahoo", symbol: "APP", name: "AppLovin Corp", exchange: "NASDAQ", type: "EQUITY" },
|
|
1413
|
+
])}
|
|
1414
|
+
/>,
|
|
1415
|
+
{ width: 80, height: 24 },
|
|
1416
|
+
);
|
|
1417
|
+
|
|
1418
|
+
await testSetup.renderOnce();
|
|
1419
|
+
await waitForFrameToContain("AAOI");
|
|
1420
|
+
|
|
1421
|
+
const frame = testSetup.captureCharFrame();
|
|
1422
|
+
const rows = frame.split("\n");
|
|
1423
|
+
const savedHeadings = frame.split("\n").filter((line) => line.trim() === "Saved");
|
|
1424
|
+
const otherListingsHeadings = frame.split("\n").filter((line) => line.trim() === "Other Listings");
|
|
1425
|
+
const aaplRow = rows.findIndex((line) => line.trimStart().startsWith("AAPL") && line.includes("NASDAQ"));
|
|
1426
|
+
const appRow = rows.findIndex((line) => line.trimStart().startsWith("APP") && line.includes("NASDAQ"));
|
|
1427
|
+
expect(savedHeadings).toHaveLength(1);
|
|
1428
|
+
expect(otherListingsHeadings).toHaveLength(1);
|
|
1429
|
+
expect(aaplRow).toBeGreaterThanOrEqual(0);
|
|
1430
|
+
expect(appRow).toBeGreaterThanOrEqual(0);
|
|
1431
|
+
expect(aaplRow).toBeLessThan(appRow);
|
|
1432
|
+
expect(frame).toMatchSnapshot();
|
|
1433
|
+
});
|
|
1434
|
+
|
|
1435
|
+
test("renders form-layout wizard fields together on one screen", async () => {
|
|
1436
|
+
testSetup = await testRender(
|
|
1437
|
+
<CommandBarHarness
|
|
1438
|
+
query="auth login"
|
|
1439
|
+
live
|
|
1440
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1441
|
+
pluginRegistry.commands.set("auth-login", {
|
|
1442
|
+
id: "auth-login",
|
|
1443
|
+
label: "Auth Login",
|
|
1444
|
+
description: "Log in to your account",
|
|
1445
|
+
keywords: ["login", "auth"],
|
|
1446
|
+
category: "config",
|
|
1447
|
+
wizardLayout: "form",
|
|
1448
|
+
wizard: [
|
|
1449
|
+
{ key: "email", label: "Email", type: "text", placeholder: "email@example.com" },
|
|
1450
|
+
{ key: "password", label: "Password", type: "password", placeholder: "Your password" },
|
|
1451
|
+
],
|
|
1452
|
+
execute: async () => {},
|
|
1453
|
+
} as any);
|
|
1454
|
+
}}
|
|
1455
|
+
/>,
|
|
1456
|
+
{ width: 80, height: 24 },
|
|
1457
|
+
);
|
|
1458
|
+
|
|
1459
|
+
await testSetup.renderOnce();
|
|
1460
|
+
await clickFrameText("Auth Login");
|
|
1461
|
+
await act(async () => {
|
|
1462
|
+
await testSetup!.renderOnce();
|
|
1463
|
+
});
|
|
1464
|
+
|
|
1465
|
+
let frame = testSetup.captureCharFrame();
|
|
1466
|
+
expect(frame).toContain("Back");
|
|
1467
|
+
expect(frame).toContain("Email");
|
|
1468
|
+
expect(frame).toContain("Password");
|
|
1469
|
+
expect(frame).toContain("Your password");
|
|
1470
|
+
expectSingleBackControl(frame);
|
|
1471
|
+
});
|
|
1472
|
+
|
|
1473
|
+
test("submits single-field form-layout wizards", async () => {
|
|
1474
|
+
const submitted: Array<Record<string, string> | undefined> = [];
|
|
1475
|
+
|
|
1476
|
+
testSetup = await testRender(
|
|
1477
|
+
<CommandBarHarness
|
|
1478
|
+
query="workspace"
|
|
1479
|
+
live
|
|
1480
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1481
|
+
pluginRegistry.commands.set("new-workspace", {
|
|
1482
|
+
id: "new-workspace",
|
|
1483
|
+
label: "Workspace",
|
|
1484
|
+
description: "Create a workspace",
|
|
1485
|
+
keywords: ["workspace"],
|
|
1486
|
+
category: "config",
|
|
1487
|
+
wizardLayout: "form",
|
|
1488
|
+
wizard: [
|
|
1489
|
+
{ key: "name", label: "Name", type: "text", placeholder: "Research" },
|
|
1490
|
+
],
|
|
1491
|
+
execute: async (values) => {
|
|
1492
|
+
submitted.push(values);
|
|
1493
|
+
},
|
|
1494
|
+
} as any);
|
|
1495
|
+
}}
|
|
1496
|
+
/>,
|
|
1497
|
+
{ width: 80, height: 24 },
|
|
1498
|
+
);
|
|
1499
|
+
|
|
1500
|
+
await testSetup.renderOnce();
|
|
1501
|
+
|
|
1502
|
+
await act(async () => {
|
|
1503
|
+
testSetup!.mockInput.pressEnter();
|
|
1504
|
+
await Bun.sleep(0);
|
|
1505
|
+
await testSetup!.renderOnce();
|
|
1506
|
+
await testSetup!.renderOnce();
|
|
1507
|
+
});
|
|
1508
|
+
|
|
1509
|
+
await act(async () => {
|
|
1510
|
+
await testSetup!.mockInput.typeText("Research");
|
|
1511
|
+
await testSetup!.renderOnce();
|
|
1512
|
+
await testSetup!.renderOnce();
|
|
1513
|
+
});
|
|
1514
|
+
|
|
1515
|
+
await act(async () => {
|
|
1516
|
+
testSetup!.mockInput.pressEnter();
|
|
1517
|
+
await Bun.sleep(0);
|
|
1518
|
+
await testSetup!.renderOnce();
|
|
1519
|
+
await testSetup!.renderOnce();
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
expect(submitted).toEqual([{ name: "Research" }]);
|
|
1523
|
+
});
|
|
1524
|
+
});
|