gloomberb 0.3.7 → 0.4.0
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 +19 -8
- package/package.json +1 -1
- package/src/app.tsx +29 -65
- package/src/cli/base-converter.ts +31 -0
- package/src/cli/commands/search.ts +11 -3
- package/src/cli/commands/ticker.ts +22 -11
- package/src/cli/context.ts +27 -32
- package/src/cli/helpers.ts +1 -4
- package/src/cli/index.ts +140 -123
- package/src/cli/registry.test.ts +226 -0
- package/src/cli/registry.ts +201 -0
- package/src/cli.test.ts +219 -0
- package/src/components/chart/chart-controller.ts +309 -0
- package/src/components/chart/chart-data.ts +5 -6
- package/src/components/chart/chart-pane-settings.ts +27 -0
- package/src/components/chart/chart-resolution.test.ts +56 -0
- package/src/components/chart/chart-resolution.ts +362 -0
- package/src/components/chart/chart-types.ts +12 -4
- package/src/components/chart/chart-viewport.test.ts +25 -0
- package/src/components/chart/chart-viewport.ts +28 -0
- package/src/components/chart/comparison-chart-data.ts +20 -27
- package/src/components/chart/comparison-stock-chart.tsx +374 -99
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +124 -0
- package/src/components/chart/stock-chart.tsx +455 -106
- package/src/components/command-bar/__snapshots__/command-bar.test.tsx.snap +4 -4
- package/src/components/command-bar/command-bar.test.tsx +650 -5
- package/src/components/command-bar/command-bar.tsx +359 -63
- package/src/components/command-bar/command-registry.ts +8 -0
- package/src/components/command-bar/helpers.test.ts +23 -1
- package/src/components/command-bar/helpers.ts +14 -5
- package/src/components/command-bar/pane-template-display.ts +12 -0
- package/src/components/command-bar/root-shortcuts.ts +2 -1
- package/src/components/command-bar/workflow-ops.ts +4 -2
- package/src/components/detail-feed-view.test.tsx +124 -49
- package/src/components/detail-feed-view.tsx +396 -160
- package/src/components/layout/floating-pane.tsx +1 -1
- package/src/components/layout/pane-header.tsx +26 -2
- package/src/components/layout/shell.tsx +62 -50
- package/src/components/layout/status-bar.test.tsx +66 -0
- package/src/components/layout/status-bar.tsx +26 -5
- package/src/components/onboarding/onboarding-wizard.test.tsx +65 -0
- package/src/components/onboarding/onboarding-wizard.tsx +5 -6
- package/src/components/ticker-list-table.test.tsx +83 -0
- package/src/components/ticker-list-table.tsx +174 -97
- package/src/components/ui/data-table.tsx +27 -2
- package/src/components/ui/index.ts +1 -0
- package/src/components/ui/page-stack-view.tsx +60 -0
- package/src/components/use-double-click-activation.ts +31 -0
- package/src/index.tsx +10 -10
- package/src/market-data/coordinator.test.ts +92 -4
- package/src/market-data/coordinator.ts +32 -5
- package/src/market-data/hooks.test.tsx +120 -0
- package/src/market-data/hooks.tsx +53 -16
- package/src/market-data/request-types.ts +4 -2
- package/src/market-data/selectors.ts +24 -58
- package/src/plugins/builtin/chat-controller.test.ts +370 -3
- package/src/plugins/builtin/chat-controller.ts +310 -35
- package/src/plugins/builtin/chat.test.tsx +118 -6
- package/src/plugins/builtin/chat.tsx +175 -116
- package/src/plugins/builtin/comparison-chart.test.tsx +66 -5
- package/src/plugins/builtin/comparison-chart.tsx +21 -1
- package/src/plugins/builtin/news.tsx +1 -11
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +656 -0
- package/src/plugins/builtin/portfolio-list/command-bar.ts +122 -0
- package/src/plugins/builtin/portfolio-list/header.tsx +16 -34
- package/src/plugins/builtin/portfolio-list/index.tsx +5 -0
- package/src/plugins/builtin/portfolio-list/metrics.test.ts +61 -0
- package/src/plugins/builtin/portfolio-list/metrics.ts +2 -2
- package/src/plugins/builtin/portfolio-list/mutations.test.ts +187 -0
- package/src/plugins/builtin/portfolio-list/mutations.ts +226 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +39 -17
- package/src/plugins/builtin/portfolio-list/table.tsx +11 -2
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.test.tsx +163 -0
- package/src/plugins/builtin/portfolio-list/use-throttled-cursor-symbol.ts +81 -0
- package/src/plugins/builtin/sec.tsx +1 -11
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +13 -2
- package/src/plugins/builtin/ticker-detail/financials-tab.tsx +21 -0
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +45 -10
- package/src/plugins/builtin/ticker-detail/pane.tsx +35 -17
- package/src/plugins/builtin/ticker-detail/settings.ts +19 -1
- package/src/plugins/builtin/ticker-detail.test.tsx +47 -0
- package/src/plugins/catalog.ts +65 -0
- package/src/plugins/ibkr/broker-adapter.ts +28 -0
- package/src/plugins/ibkr/gateway-service.test.ts +45 -1
- package/src/plugins/ibkr/gateway-service.ts +141 -7
- package/src/plugins/pane-manager.test.ts +1 -1
- package/src/plugins/plugin-runtime.tsx +27 -7
- package/src/plugins/prediction-markets/cli.ts +28 -0
- package/src/plugins/prediction-markets/controller-data.ts +11 -3
- package/src/plugins/prediction-markets/controller.ts +141 -121
- package/src/plugins/prediction-markets/index.tsx +2 -0
- package/src/plugins/prediction-markets/launch.ts +51 -16
- package/src/plugins/prediction-markets/metrics.ts +9 -5
- package/src/plugins/prediction-markets/pane.test.tsx +119 -57
- package/src/plugins/prediction-markets/pane.tsx +74 -153
- package/src/plugins/prediction-markets/table.tsx +6 -3
- package/src/plugins/prediction-markets/types.ts +0 -1
- package/src/sources/gloomberb-cloud.test.ts +29 -0
- package/src/sources/gloomberb-cloud.ts +89 -1
- package/src/sources/provider-router.test.ts +265 -3
- package/src/sources/provider-router.ts +239 -11
- package/src/sources/yahoo-finance.test.ts +26 -0
- package/src/sources/yahoo-finance.ts +54 -3
- package/src/state/app-context-selector.test.tsx +99 -0
- package/src/state/app-context.test.ts +18 -0
- package/src/state/app-context.tsx +170 -65
- package/src/state/selectors.ts +25 -0
- package/src/theme/colors.ts +8 -8
- package/src/theme/themes.test.ts +2 -0
- package/src/theme/themes.ts +22 -10
- package/src/types/broker.ts +21 -0
- package/src/types/config.ts +17 -1
- package/src/types/data-provider.ts +18 -0
- package/src/types/financials.ts +31 -1
- package/src/types/plugin.ts +70 -0
- package/src/utils/api-client.test.ts +57 -0
- package/src/utils/api-client.ts +35 -12
- package/src/utils/back-navigation.ts +27 -0
- package/src/utils/format.test.ts +14 -0
- package/src/utils/format.ts +3 -3
- package/src/utils/market-status.ts +46 -1
- package/src/utils/quote-freshness.ts +127 -0
- package/src/utils/quote-resolution.test.ts +277 -0
- package/src/utils/quote-resolution.ts +641 -0
- package/src/utils/slugify.ts +3 -0
- package/src/utils/timestamp.ts +25 -0
- package/src/version.ts +1 -1
- package/src/cli/commands/portfolio.ts +0 -210
- package/src/cli/commands/watchlist.ts +0 -210
- package/src/plugins/builtin/data-providers.ts +0 -16
- package/src/sources/exchange-rates.ts +0 -18
- package/src/utils/ascii-chart.ts +0 -172
- package/src/utils/ticker-tokenizer.test.ts +0 -48
|
@@ -46,12 +46,37 @@ async function clickFrameText(text: string): Promise<void> {
|
|
|
46
46
|
});
|
|
47
47
|
}
|
|
48
48
|
|
|
49
|
+
async function emitKeypress(
|
|
50
|
+
renderer: Awaited<ReturnType<typeof testRender>>,
|
|
51
|
+
event: { name?: string; sequence?: string; ctrl?: boolean; meta?: boolean; shift?: boolean; option?: boolean },
|
|
52
|
+
): Promise<void> {
|
|
53
|
+
await act(async () => {
|
|
54
|
+
renderer.renderer.keyInput.emit("keypress", {
|
|
55
|
+
ctrl: false,
|
|
56
|
+
meta: false,
|
|
57
|
+
option: false,
|
|
58
|
+
shift: false,
|
|
59
|
+
eventType: "press",
|
|
60
|
+
repeated: false,
|
|
61
|
+
stopPropagation: () => {},
|
|
62
|
+
preventDefault: () => {},
|
|
63
|
+
...event,
|
|
64
|
+
} as any);
|
|
65
|
+
await renderer.renderOnce();
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function renderFrames(count = 2): Promise<void> {
|
|
70
|
+
for (let index = 0; index < count; index += 1) {
|
|
71
|
+
await testSetup!.renderOnce();
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
49
75
|
function expectSingleBackControl(frame: string): void {
|
|
50
76
|
expect(frame.match(/\bBack\b/g)?.length ?? 0).toBe(1);
|
|
51
|
-
expect(frame).not.toContain("esc");
|
|
52
77
|
}
|
|
53
78
|
|
|
54
|
-
function makeTicker(symbol: string, name: string): TickerRecord {
|
|
79
|
+
function makeTicker(symbol: string, name: string, overrides: Partial<TickerRecord["metadata"]> = {}): TickerRecord {
|
|
55
80
|
return {
|
|
56
81
|
metadata: {
|
|
57
82
|
ticker: symbol,
|
|
@@ -63,6 +88,7 @@ function makeTicker(symbol: string, name: string): TickerRecord {
|
|
|
63
88
|
positions: [],
|
|
64
89
|
custom: {},
|
|
65
90
|
tags: [],
|
|
91
|
+
...overrides,
|
|
66
92
|
},
|
|
67
93
|
};
|
|
68
94
|
}
|
|
@@ -399,6 +425,32 @@ describe("CommandBar", () => {
|
|
|
399
425
|
expect(frame).toContain("theme:green");
|
|
400
426
|
});
|
|
401
427
|
|
|
428
|
+
test("keeps the selected theme after pressing enter in root theme mode", async () => {
|
|
429
|
+
testSetup = await testRender(<CommandBarHarness query="TH " live />, {
|
|
430
|
+
width: 80,
|
|
431
|
+
height: 24,
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
await testSetup.renderOnce();
|
|
435
|
+
await act(async () => {
|
|
436
|
+
testSetup!.mockInput.pressArrow("down");
|
|
437
|
+
await testSetup!.renderOnce();
|
|
438
|
+
});
|
|
439
|
+
await testSetup.renderOnce();
|
|
440
|
+
|
|
441
|
+
await act(async () => {
|
|
442
|
+
testSetup!.mockInput.pressEnter();
|
|
443
|
+
await Bun.sleep(0);
|
|
444
|
+
await testSetup!.renderOnce();
|
|
445
|
+
});
|
|
446
|
+
await testSetup.renderOnce();
|
|
447
|
+
|
|
448
|
+
const frame = testSetup.captureCharFrame();
|
|
449
|
+
expect(frame).toContain("theme:green");
|
|
450
|
+
expect(frame).not.toContain("theme:amber");
|
|
451
|
+
expect(frame).not.toContain("Commands");
|
|
452
|
+
});
|
|
453
|
+
|
|
402
454
|
test("keeps typed prefixes in the root query until a result is activated", async () => {
|
|
403
455
|
testSetup = await testRender(<CommandBarHarness query="T " />, {
|
|
404
456
|
width: 80,
|
|
@@ -628,6 +680,432 @@ describe("CommandBar", () => {
|
|
|
628
680
|
expect(frame).toContain("Back");
|
|
629
681
|
});
|
|
630
682
|
|
|
683
|
+
test("typing add still surfaces Add to Portfolio for a ticker already in the active manual portfolio", async () => {
|
|
684
|
+
testSetup = await testRender(
|
|
685
|
+
<CommandBarHarness
|
|
686
|
+
query="add"
|
|
687
|
+
selectedTicker="AAPL"
|
|
688
|
+
configureConfig={(config) => ({
|
|
689
|
+
...config,
|
|
690
|
+
portfolios: [{ id: "research", name: "Research", currency: "USD" }],
|
|
691
|
+
})}
|
|
692
|
+
configureState={(state) => ({
|
|
693
|
+
...state,
|
|
694
|
+
paneState: {
|
|
695
|
+
...state.paneState,
|
|
696
|
+
"portfolio-list:main": {
|
|
697
|
+
collectionId: "research",
|
|
698
|
+
cursorSymbol: "AAPL",
|
|
699
|
+
},
|
|
700
|
+
},
|
|
701
|
+
})}
|
|
702
|
+
extraTickers={[makeTicker("AAPL", "Apple Inc.", {
|
|
703
|
+
portfolios: ["research"],
|
|
704
|
+
})]}
|
|
705
|
+
/>,
|
|
706
|
+
{ width: 100, height: 20 },
|
|
707
|
+
);
|
|
708
|
+
|
|
709
|
+
await testSetup.renderOnce();
|
|
710
|
+
|
|
711
|
+
const frame = testSetup.captureCharFrame();
|
|
712
|
+
expect(frame).toContain("Add AAPL to Portfolio");
|
|
713
|
+
expect(frame).toContain('in "Research"');
|
|
714
|
+
});
|
|
715
|
+
|
|
716
|
+
test("AP opens the add-to-portfolio workflow and prefills avg cost from the current price", async () => {
|
|
717
|
+
testSetup = await testRender(
|
|
718
|
+
<CommandBarHarness
|
|
719
|
+
query="AP AAPL"
|
|
720
|
+
selectedTicker="AAPL"
|
|
721
|
+
configureConfig={(config) => ({
|
|
722
|
+
...config,
|
|
723
|
+
portfolios: [{ id: "research", name: "Research", currency: "USD" }],
|
|
724
|
+
})}
|
|
725
|
+
configureState={(state) => ({
|
|
726
|
+
...state,
|
|
727
|
+
paneState: {
|
|
728
|
+
...state.paneState,
|
|
729
|
+
"portfolio-list:main": {
|
|
730
|
+
collectionId: "research",
|
|
731
|
+
cursorSymbol: "AAPL",
|
|
732
|
+
},
|
|
733
|
+
},
|
|
734
|
+
financials: new Map([["AAPL", {
|
|
735
|
+
annualStatements: [],
|
|
736
|
+
quarterlyStatements: [],
|
|
737
|
+
priceHistory: [],
|
|
738
|
+
quote: {
|
|
739
|
+
symbol: "AAPL",
|
|
740
|
+
price: 205.5,
|
|
741
|
+
currency: "USD",
|
|
742
|
+
change: 1.25,
|
|
743
|
+
changePercent: 0.61,
|
|
744
|
+
lastUpdated: Date.now(),
|
|
745
|
+
},
|
|
746
|
+
}]]),
|
|
747
|
+
})}
|
|
748
|
+
extraTickers={[makeTicker("AAPL", "Apple Inc.", {
|
|
749
|
+
portfolios: ["research"],
|
|
750
|
+
})]}
|
|
751
|
+
/>,
|
|
752
|
+
{ width: 100, height: 30 },
|
|
753
|
+
);
|
|
754
|
+
|
|
755
|
+
await testSetup.renderOnce();
|
|
756
|
+
await act(async () => {
|
|
757
|
+
testSetup!.mockInput.pressEnter();
|
|
758
|
+
await testSetup!.renderOnce();
|
|
759
|
+
});
|
|
760
|
+
|
|
761
|
+
const frame = await waitForFrameToContain("Avg Cost");
|
|
762
|
+
expect(frame).toContain("Shares");
|
|
763
|
+
expect(frame).toContain("Avg Cost");
|
|
764
|
+
expect(frame).toContain("205.5");
|
|
765
|
+
expectSingleBackControl(frame);
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
test("add-to-portfolio can still add membership without entering a position", async () => {
|
|
769
|
+
const saved: TickerRecord[] = [];
|
|
770
|
+
|
|
771
|
+
testSetup = await testRender(
|
|
772
|
+
<CommandBarHarness
|
|
773
|
+
query="AP AAPL"
|
|
774
|
+
selectedTicker="AAPL"
|
|
775
|
+
onSaveTicker={(ticker) => {
|
|
776
|
+
saved.push(ticker);
|
|
777
|
+
}}
|
|
778
|
+
configureConfig={(config) => ({
|
|
779
|
+
...config,
|
|
780
|
+
portfolios: [{ id: "research", name: "Research", currency: "USD" }],
|
|
781
|
+
})}
|
|
782
|
+
configureState={(state) => ({
|
|
783
|
+
...state,
|
|
784
|
+
paneState: {
|
|
785
|
+
...state.paneState,
|
|
786
|
+
"portfolio-list:main": {
|
|
787
|
+
collectionId: "research",
|
|
788
|
+
cursorSymbol: "AAPL",
|
|
789
|
+
},
|
|
790
|
+
},
|
|
791
|
+
})}
|
|
792
|
+
/>,
|
|
793
|
+
{ width: 100, height: 30 },
|
|
794
|
+
);
|
|
795
|
+
|
|
796
|
+
await testSetup.renderOnce();
|
|
797
|
+
await act(async () => {
|
|
798
|
+
testSetup!.mockInput.pressEnter();
|
|
799
|
+
await testSetup!.renderOnce();
|
|
800
|
+
});
|
|
801
|
+
|
|
802
|
+
const frame = await waitForFrameToContain("Avg Cost");
|
|
803
|
+
expect(frame).toContain("Shares");
|
|
804
|
+
expect(frame).toContain("Avg Cost");
|
|
805
|
+
|
|
806
|
+
await clickFrameText("Add to Portfolio");
|
|
807
|
+
await act(async () => {
|
|
808
|
+
await Bun.sleep(0);
|
|
809
|
+
await testSetup!.renderOnce();
|
|
810
|
+
});
|
|
811
|
+
|
|
812
|
+
expect(saved.at(-1)?.metadata.portfolios).toEqual(["research"]);
|
|
813
|
+
expect(saved.at(-1)?.metadata.positions).toEqual([]);
|
|
814
|
+
});
|
|
815
|
+
|
|
816
|
+
test("only surfaces Set Portfolio Position when a manual portfolio exists", async () => {
|
|
817
|
+
testSetup = await testRender(
|
|
818
|
+
<CommandBarHarness query="Set Portfolio Position" />,
|
|
819
|
+
{ width: 100, height: 20 },
|
|
820
|
+
);
|
|
821
|
+
|
|
822
|
+
await testSetup.renderOnce();
|
|
823
|
+
expect(testSetup.captureCharFrame()).toContain("Set Portfolio Position");
|
|
824
|
+
|
|
825
|
+
testSetup.renderer.destroy();
|
|
826
|
+
testSetup = await testRender(
|
|
827
|
+
<CommandBarHarness
|
|
828
|
+
query="Set Portfolio Position"
|
|
829
|
+
configureConfig={(config) => ({
|
|
830
|
+
...config,
|
|
831
|
+
portfolios: [{
|
|
832
|
+
id: "broker:ibkr",
|
|
833
|
+
name: "IBKR Account",
|
|
834
|
+
currency: "USD",
|
|
835
|
+
brokerId: "ibkr",
|
|
836
|
+
brokerInstanceId: "ibkr-live",
|
|
837
|
+
}],
|
|
838
|
+
})}
|
|
839
|
+
/>,
|
|
840
|
+
{ width: 100, height: 20 },
|
|
841
|
+
);
|
|
842
|
+
|
|
843
|
+
await testSetup.renderOnce();
|
|
844
|
+
const frame = testSetup.captureCharFrame();
|
|
845
|
+
expect(frame).toContain('No matches for "Set Portfolio Position"');
|
|
846
|
+
expect(frame).not.toContain("Create or update a manual position in a portfolio");
|
|
847
|
+
});
|
|
848
|
+
|
|
849
|
+
test("matches set portfolio position when searching edit position", async () => {
|
|
850
|
+
testSetup = await testRender(
|
|
851
|
+
<CommandBarHarness
|
|
852
|
+
query="edit position"
|
|
853
|
+
selectedTicker="AAPL"
|
|
854
|
+
configureConfig={(config) => ({
|
|
855
|
+
...config,
|
|
856
|
+
portfolios: [{ id: "research", name: "Research", currency: "USD" }],
|
|
857
|
+
})}
|
|
858
|
+
configureState={(state) => ({
|
|
859
|
+
...state,
|
|
860
|
+
paneState: {
|
|
861
|
+
...state.paneState,
|
|
862
|
+
"portfolio-list:main": {
|
|
863
|
+
collectionId: "research",
|
|
864
|
+
cursorSymbol: "AAPL",
|
|
865
|
+
},
|
|
866
|
+
},
|
|
867
|
+
})}
|
|
868
|
+
/>,
|
|
869
|
+
{ width: 100, height: 20 },
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
await testSetup.renderOnce();
|
|
873
|
+
|
|
874
|
+
const frame = testSetup.captureCharFrame();
|
|
875
|
+
expect(frame).toContain("Set Position for AAPL");
|
|
876
|
+
expect(frame).toContain('in "Research"');
|
|
877
|
+
});
|
|
878
|
+
|
|
879
|
+
test("prefills the portfolio position workflow from the active manual portfolio and ticker", async () => {
|
|
880
|
+
testSetup = await testRender(
|
|
881
|
+
<CommandBarHarness
|
|
882
|
+
query="Set Position for AAPL"
|
|
883
|
+
selectedTicker="AAPL"
|
|
884
|
+
configureConfig={(config) => ({
|
|
885
|
+
...config,
|
|
886
|
+
portfolios: [{ id: "research", name: "Research", currency: "USD" }],
|
|
887
|
+
})}
|
|
888
|
+
configureState={(state) => ({
|
|
889
|
+
...state,
|
|
890
|
+
paneState: {
|
|
891
|
+
...state.paneState,
|
|
892
|
+
"portfolio-list:main": {
|
|
893
|
+
collectionId: "research",
|
|
894
|
+
cursorSymbol: "AAPL",
|
|
895
|
+
},
|
|
896
|
+
},
|
|
897
|
+
})}
|
|
898
|
+
extraTickers={[makeTicker("AAPL", "Apple Inc.", {
|
|
899
|
+
portfolios: ["research"],
|
|
900
|
+
positions: [{
|
|
901
|
+
portfolio: "research",
|
|
902
|
+
shares: 10,
|
|
903
|
+
avgCost: 180,
|
|
904
|
+
currency: "USD",
|
|
905
|
+
broker: "manual",
|
|
906
|
+
}],
|
|
907
|
+
})]}
|
|
908
|
+
/>,
|
|
909
|
+
{ width: 100, height: 30 },
|
|
910
|
+
);
|
|
911
|
+
|
|
912
|
+
await testSetup.renderOnce();
|
|
913
|
+
await act(async () => {
|
|
914
|
+
testSetup!.mockInput.pressEnter();
|
|
915
|
+
await testSetup!.renderOnce();
|
|
916
|
+
});
|
|
917
|
+
|
|
918
|
+
const frame = await waitForFrameToContain("Avg Cost");
|
|
919
|
+
expect(frame).toContain("Research");
|
|
920
|
+
expect(frame).toContain("AAPL");
|
|
921
|
+
expect(frame).toContain("10");
|
|
922
|
+
expect(frame).toContain("180");
|
|
923
|
+
expectSingleBackControl(frame);
|
|
924
|
+
});
|
|
925
|
+
|
|
926
|
+
test("submits the portfolio position workflow and persists a manual position", async () => {
|
|
927
|
+
const saved: TickerRecord[] = [];
|
|
928
|
+
|
|
929
|
+
testSetup = await testRender(
|
|
930
|
+
<CommandBarHarness
|
|
931
|
+
query="Set Position for AAPL"
|
|
932
|
+
selectedTicker="AAPL"
|
|
933
|
+
onSaveTicker={(ticker) => {
|
|
934
|
+
saved.push(ticker);
|
|
935
|
+
}}
|
|
936
|
+
configureConfig={(config) => ({
|
|
937
|
+
...config,
|
|
938
|
+
portfolios: [{ id: "research", name: "Research", currency: "USD" }],
|
|
939
|
+
})}
|
|
940
|
+
configureState={(state) => ({
|
|
941
|
+
...state,
|
|
942
|
+
paneState: {
|
|
943
|
+
...state.paneState,
|
|
944
|
+
"portfolio-list:main": {
|
|
945
|
+
collectionId: "research",
|
|
946
|
+
cursorSymbol: "AAPL",
|
|
947
|
+
},
|
|
948
|
+
},
|
|
949
|
+
})}
|
|
950
|
+
/>,
|
|
951
|
+
{ width: 100, height: 30 },
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
await testSetup.renderOnce();
|
|
955
|
+
await act(async () => {
|
|
956
|
+
testSetup!.mockInput.pressEnter();
|
|
957
|
+
await testSetup!.renderOnce();
|
|
958
|
+
});
|
|
959
|
+
|
|
960
|
+
await act(async () => {
|
|
961
|
+
testSetup!.mockInput.pressTab();
|
|
962
|
+
await testSetup!.renderOnce();
|
|
963
|
+
});
|
|
964
|
+
await act(async () => {
|
|
965
|
+
testSetup!.mockInput.pressTab();
|
|
966
|
+
await testSetup!.renderOnce();
|
|
967
|
+
});
|
|
968
|
+
await act(async () => {
|
|
969
|
+
await testSetup!.mockInput.typeText("10");
|
|
970
|
+
await testSetup!.renderOnce();
|
|
971
|
+
});
|
|
972
|
+
await act(async () => {
|
|
973
|
+
testSetup!.mockInput.pressTab();
|
|
974
|
+
await testSetup!.renderOnce();
|
|
975
|
+
});
|
|
976
|
+
await act(async () => {
|
|
977
|
+
await testSetup!.mockInput.typeText("180");
|
|
978
|
+
await testSetup!.renderOnce();
|
|
979
|
+
});
|
|
980
|
+
await act(async () => {
|
|
981
|
+
testSetup!.mockInput.pressTab();
|
|
982
|
+
await testSetup!.renderOnce();
|
|
983
|
+
});
|
|
984
|
+
await act(async () => {
|
|
985
|
+
await testSetup!.mockInput.typeText("EUR");
|
|
986
|
+
await testSetup!.renderOnce();
|
|
987
|
+
});
|
|
988
|
+
await act(async () => {
|
|
989
|
+
testSetup!.mockInput.pressEnter();
|
|
990
|
+
await Bun.sleep(0);
|
|
991
|
+
await testSetup!.renderOnce();
|
|
992
|
+
});
|
|
993
|
+
|
|
994
|
+
expect(saved.at(-1)?.metadata.portfolios).toEqual(["research"]);
|
|
995
|
+
expect(saved.at(-1)?.metadata.positions).toEqual([{
|
|
996
|
+
portfolio: "research",
|
|
997
|
+
shares: 10,
|
|
998
|
+
avgCost: 180,
|
|
999
|
+
currency: "EUR",
|
|
1000
|
+
broker: "manual",
|
|
1001
|
+
}]);
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
test("removing a ticker from a manual portfolio also removes its position", async () => {
|
|
1005
|
+
const saved: TickerRecord[] = [];
|
|
1006
|
+
|
|
1007
|
+
testSetup = await testRender(
|
|
1008
|
+
<CommandBarHarness
|
|
1009
|
+
query="RP AAPL"
|
|
1010
|
+
selectedTicker="AAPL"
|
|
1011
|
+
onSaveTicker={(ticker) => {
|
|
1012
|
+
saved.push(ticker);
|
|
1013
|
+
}}
|
|
1014
|
+
configureConfig={(config) => ({
|
|
1015
|
+
...config,
|
|
1016
|
+
portfolios: [{ id: "research", name: "Research", currency: "USD" }],
|
|
1017
|
+
})}
|
|
1018
|
+
configureState={(state) => ({
|
|
1019
|
+
...state,
|
|
1020
|
+
paneState: {
|
|
1021
|
+
...state.paneState,
|
|
1022
|
+
"portfolio-list:main": {
|
|
1023
|
+
collectionId: "research",
|
|
1024
|
+
cursorSymbol: "AAPL",
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
})}
|
|
1028
|
+
extraTickers={[makeTicker("AAPL", "Apple Inc.", {
|
|
1029
|
+
portfolios: ["research"],
|
|
1030
|
+
positions: [{
|
|
1031
|
+
portfolio: "research",
|
|
1032
|
+
shares: 4,
|
|
1033
|
+
avgCost: 175,
|
|
1034
|
+
currency: "USD",
|
|
1035
|
+
broker: "manual",
|
|
1036
|
+
}],
|
|
1037
|
+
})]}
|
|
1038
|
+
/>,
|
|
1039
|
+
{ width: 100, height: 20 },
|
|
1040
|
+
);
|
|
1041
|
+
|
|
1042
|
+
await testSetup.renderOnce();
|
|
1043
|
+
await act(async () => {
|
|
1044
|
+
testSetup!.mockInput.pressEnter();
|
|
1045
|
+
await Bun.sleep(0);
|
|
1046
|
+
await testSetup!.renderOnce();
|
|
1047
|
+
});
|
|
1048
|
+
|
|
1049
|
+
expect(saved.at(-1)?.metadata.portfolios).toEqual([]);
|
|
1050
|
+
expect(saved.at(-1)?.metadata.positions).toEqual([]);
|
|
1051
|
+
});
|
|
1052
|
+
|
|
1053
|
+
test("deleting a manual portfolio also cleans saved ticker positions", async () => {
|
|
1054
|
+
const saved: TickerRecord[] = [];
|
|
1055
|
+
|
|
1056
|
+
testSetup = await testRender(
|
|
1057
|
+
<CommandBarHarness
|
|
1058
|
+
query="Delete Portfolio"
|
|
1059
|
+
onSaveTicker={(ticker) => {
|
|
1060
|
+
saved.push(ticker);
|
|
1061
|
+
}}
|
|
1062
|
+
configureConfig={(config) => ({
|
|
1063
|
+
...config,
|
|
1064
|
+
portfolios: [{ id: "research", name: "Research", currency: "USD" }],
|
|
1065
|
+
})}
|
|
1066
|
+
configureState={(state) => ({
|
|
1067
|
+
...state,
|
|
1068
|
+
paneState: {
|
|
1069
|
+
...state.paneState,
|
|
1070
|
+
"portfolio-list:main": {
|
|
1071
|
+
collectionId: "research",
|
|
1072
|
+
cursorSymbol: "AAPL",
|
|
1073
|
+
},
|
|
1074
|
+
},
|
|
1075
|
+
})}
|
|
1076
|
+
extraTickers={[makeTicker("AAPL", "Apple Inc.", {
|
|
1077
|
+
portfolios: ["research"],
|
|
1078
|
+
positions: [{
|
|
1079
|
+
portfolio: "research",
|
|
1080
|
+
shares: 2,
|
|
1081
|
+
avgCost: 160,
|
|
1082
|
+
currency: "USD",
|
|
1083
|
+
broker: "manual",
|
|
1084
|
+
}],
|
|
1085
|
+
})]}
|
|
1086
|
+
/>,
|
|
1087
|
+
{ width: 100, height: 20 },
|
|
1088
|
+
);
|
|
1089
|
+
|
|
1090
|
+
await testSetup.renderOnce();
|
|
1091
|
+
await act(async () => {
|
|
1092
|
+
testSetup!.mockInput.pressEnter();
|
|
1093
|
+
await testSetup!.renderOnce();
|
|
1094
|
+
});
|
|
1095
|
+
await act(async () => {
|
|
1096
|
+
testSetup!.mockInput.pressEnter();
|
|
1097
|
+
await testSetup!.renderOnce();
|
|
1098
|
+
});
|
|
1099
|
+
await act(async () => {
|
|
1100
|
+
testSetup!.mockInput.pressEnter();
|
|
1101
|
+
await Bun.sleep(0);
|
|
1102
|
+
await testSetup!.renderOnce();
|
|
1103
|
+
});
|
|
1104
|
+
|
|
1105
|
+
expect(saved.at(-1)?.metadata.portfolios).toEqual([]);
|
|
1106
|
+
expect(saved.at(-1)?.metadata.positions).toEqual([]);
|
|
1107
|
+
});
|
|
1108
|
+
|
|
631
1109
|
test("pressing enter after arrow navigation ignores stale mouse hover", async () => {
|
|
632
1110
|
const pinned: string[] = [];
|
|
633
1111
|
|
|
@@ -718,6 +1196,41 @@ describe("CommandBar", () => {
|
|
|
718
1196
|
expect(executed).toEqual(["delete-layout"]);
|
|
719
1197
|
});
|
|
720
1198
|
|
|
1199
|
+
test("uses backspace to leave confirm routes", async () => {
|
|
1200
|
+
testSetup = await testRender(
|
|
1201
|
+
<CommandBarHarness
|
|
1202
|
+
query="delete"
|
|
1203
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1204
|
+
(pluginRegistry.commands as Map<string, any>).set("delete-layout", {
|
|
1205
|
+
id: "delete-layout",
|
|
1206
|
+
label: "Delete Layout",
|
|
1207
|
+
description: "Delete the current layout preset",
|
|
1208
|
+
category: "Layout Manager",
|
|
1209
|
+
execute: async () => {},
|
|
1210
|
+
});
|
|
1211
|
+
}}
|
|
1212
|
+
/>,
|
|
1213
|
+
{ width: 100, height: 20 },
|
|
1214
|
+
);
|
|
1215
|
+
|
|
1216
|
+
await testSetup.renderOnce();
|
|
1217
|
+
await act(async () => {
|
|
1218
|
+
testSetup!.mockInput.pressEnter();
|
|
1219
|
+
await testSetup!.renderOnce();
|
|
1220
|
+
});
|
|
1221
|
+
await renderFrames();
|
|
1222
|
+
|
|
1223
|
+
await act(async () => {
|
|
1224
|
+
testSetup!.mockInput.pressBackspace();
|
|
1225
|
+
await testSetup!.renderOnce();
|
|
1226
|
+
});
|
|
1227
|
+
await renderFrames();
|
|
1228
|
+
|
|
1229
|
+
const frame = testSetup.captureCharFrame();
|
|
1230
|
+
expect(frame).toContain("Delete Layout");
|
|
1231
|
+
expect(frame).not.toContain("Back Delete Layout");
|
|
1232
|
+
});
|
|
1233
|
+
|
|
721
1234
|
test("moves through long result lists with the mouse wheel", async () => {
|
|
722
1235
|
testSetup = await testRender(
|
|
723
1236
|
<CommandBarHarness
|
|
@@ -848,8 +1361,8 @@ describe("CommandBar", () => {
|
|
|
848
1361
|
await testSetup.renderOnce();
|
|
849
1362
|
|
|
850
1363
|
const frame = testSetup.captureCharFrame();
|
|
851
|
-
expect(frame).toContain("
|
|
852
|
-
expect(frame).toContain("float");
|
|
1364
|
+
expect(frame).toContain("Chat");
|
|
1365
|
+
expect(frame).not.toContain("float");
|
|
853
1366
|
});
|
|
854
1367
|
|
|
855
1368
|
test("shows pane shortcuts in the default browse results", async () => {
|
|
@@ -949,6 +1462,48 @@ describe("CommandBar", () => {
|
|
|
949
1462
|
});
|
|
950
1463
|
});
|
|
951
1464
|
|
|
1465
|
+
test("does not treat backspace as Back inside workflow text fields", async () => {
|
|
1466
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1467
|
+
query="Plugin Login"
|
|
1468
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1469
|
+
(pluginRegistry.commands as Map<string, any>).set("plugin:login", {
|
|
1470
|
+
id: "plugin:login",
|
|
1471
|
+
label: "Plugin Login",
|
|
1472
|
+
description: "Authenticate without leaving the command bar",
|
|
1473
|
+
category: "config",
|
|
1474
|
+
wizard: [
|
|
1475
|
+
{ key: "username", label: "Username", type: "text", placeholder: "vince" },
|
|
1476
|
+
],
|
|
1477
|
+
execute: async () => {},
|
|
1478
|
+
} as any);
|
|
1479
|
+
pluginRegistry.getCommandPluginId = () => "notes";
|
|
1480
|
+
}}
|
|
1481
|
+
/>, {
|
|
1482
|
+
width: 100,
|
|
1483
|
+
height: 24,
|
|
1484
|
+
});
|
|
1485
|
+
|
|
1486
|
+
await testSetup.renderOnce();
|
|
1487
|
+
await act(async () => {
|
|
1488
|
+
testSetup!.mockInput.pressEnter();
|
|
1489
|
+
await testSetup!.renderOnce();
|
|
1490
|
+
});
|
|
1491
|
+
|
|
1492
|
+
await act(async () => {
|
|
1493
|
+
await testSetup!.mockInput.typeText("vince");
|
|
1494
|
+
await testSetup!.renderOnce();
|
|
1495
|
+
});
|
|
1496
|
+
await act(async () => {
|
|
1497
|
+
testSetup!.mockInput.pressBackspace();
|
|
1498
|
+
await testSetup!.renderOnce();
|
|
1499
|
+
});
|
|
1500
|
+
|
|
1501
|
+
const frame = testSetup.captureCharFrame();
|
|
1502
|
+
expect(frame).toContain("Plugin Login");
|
|
1503
|
+
expect(frame).toContain("vinc");
|
|
1504
|
+
expect(frame).toContain("Back");
|
|
1505
|
+
});
|
|
1506
|
+
|
|
952
1507
|
test("QQ MSFT executes directly without opening a secondary workflow", async () => {
|
|
953
1508
|
const created: Array<{ templateId: string; options?: Record<string, unknown> }> = [];
|
|
954
1509
|
|
|
@@ -1283,6 +1838,96 @@ describe("CommandBar", () => {
|
|
|
1283
1838
|
}]);
|
|
1284
1839
|
});
|
|
1285
1840
|
|
|
1841
|
+
test("uses backspace as back only when a pane-settings route query is empty", async () => {
|
|
1842
|
+
testSetup = await testRender(<CommandBarHarness
|
|
1843
|
+
query="PS"
|
|
1844
|
+
configureState={(state) => ({
|
|
1845
|
+
...state,
|
|
1846
|
+
focusedPaneId: "quote-monitor:main",
|
|
1847
|
+
})}
|
|
1848
|
+
hasPaneSettings={(paneId) => paneId === "quote-monitor:main"}
|
|
1849
|
+
configurePluginRegistry={(pluginRegistry) => {
|
|
1850
|
+
pluginRegistry.resolvePaneSettings = () => ({
|
|
1851
|
+
paneId: "quote-monitor:main",
|
|
1852
|
+
pane: {
|
|
1853
|
+
instanceId: "quote-monitor:main",
|
|
1854
|
+
paneId: "quote-monitor",
|
|
1855
|
+
title: "Quote Monitor",
|
|
1856
|
+
settings: {},
|
|
1857
|
+
},
|
|
1858
|
+
paneDef: pluginRegistry.panes.get("quote-monitor")!,
|
|
1859
|
+
settingsDef: {
|
|
1860
|
+
title: "Quote Monitor Settings",
|
|
1861
|
+
fields: [{
|
|
1862
|
+
key: "symbol",
|
|
1863
|
+
label: "Symbol",
|
|
1864
|
+
type: "text",
|
|
1865
|
+
description: "Ticker symbol to track",
|
|
1866
|
+
}],
|
|
1867
|
+
},
|
|
1868
|
+
context: {
|
|
1869
|
+
config: createDefaultConfig("/tmp/gloomberb-test"),
|
|
1870
|
+
layout: cloneLayout(createDefaultConfig("/tmp/gloomberb-test").layout),
|
|
1871
|
+
paneId: "quote-monitor:main",
|
|
1872
|
+
paneType: "quote-monitor",
|
|
1873
|
+
pane: {
|
|
1874
|
+
instanceId: "quote-monitor:main",
|
|
1875
|
+
paneId: "quote-monitor",
|
|
1876
|
+
title: "Quote Monitor",
|
|
1877
|
+
settings: {},
|
|
1878
|
+
},
|
|
1879
|
+
settings: {},
|
|
1880
|
+
paneState: {},
|
|
1881
|
+
activeTicker: "AAPL",
|
|
1882
|
+
activeCollectionId: "main",
|
|
1883
|
+
},
|
|
1884
|
+
}) as any;
|
|
1885
|
+
}}
|
|
1886
|
+
/>, {
|
|
1887
|
+
width: 100,
|
|
1888
|
+
height: 20,
|
|
1889
|
+
});
|
|
1890
|
+
|
|
1891
|
+
await testSetup.renderOnce();
|
|
1892
|
+
|
|
1893
|
+
await act(async () => {
|
|
1894
|
+
testSetup!.mockInput.pressEnter();
|
|
1895
|
+
await testSetup!.renderOnce();
|
|
1896
|
+
});
|
|
1897
|
+
await renderFrames();
|
|
1898
|
+
|
|
1899
|
+
let frame = testSetup.captureCharFrame();
|
|
1900
|
+
expect(frame).toContain("Quote Monitor Settings");
|
|
1901
|
+
|
|
1902
|
+
await act(async () => {
|
|
1903
|
+
await testSetup!.mockInput.typeText("s");
|
|
1904
|
+
await testSetup!.renderOnce();
|
|
1905
|
+
});
|
|
1906
|
+
|
|
1907
|
+
frame = testSetup.captureCharFrame();
|
|
1908
|
+
expect(frame).toContain("Quote Monitor Settings");
|
|
1909
|
+
|
|
1910
|
+
await emitKeypress(testSetup, { name: "backspace", sequence: "\b" });
|
|
1911
|
+
|
|
1912
|
+
frame = testSetup.captureCharFrame();
|
|
1913
|
+
expect(frame).toContain("Quote Monitor Settings");
|
|
1914
|
+
|
|
1915
|
+
await act(async () => {
|
|
1916
|
+
testSetup!.mockInput.pressBackspace();
|
|
1917
|
+
await testSetup!.renderOnce();
|
|
1918
|
+
});
|
|
1919
|
+
await renderFrames();
|
|
1920
|
+
await act(async () => {
|
|
1921
|
+
testSetup!.mockInput.pressBackspace();
|
|
1922
|
+
await testSetup!.renderOnce();
|
|
1923
|
+
});
|
|
1924
|
+
await renderFrames();
|
|
1925
|
+
|
|
1926
|
+
frame = testSetup.captureCharFrame();
|
|
1927
|
+
expect(frame).not.toContain("Quote Monitor Settings");
|
|
1928
|
+
expect(frame).not.toContain("Back Pane Settings");
|
|
1929
|
+
});
|
|
1930
|
+
|
|
1286
1931
|
test("renders pane-setting multi-select pickers only once inside the command bar", async () => {
|
|
1287
1932
|
testSetup = await testRender(<CommandBarHarness
|
|
1288
1933
|
query="PS"
|
|
@@ -1377,7 +2022,7 @@ describe("CommandBar", () => {
|
|
|
1377
2022
|
await testSetup.renderOnce();
|
|
1378
2023
|
|
|
1379
2024
|
const frame = testSetup.captureCharFrame();
|
|
1380
|
-
expect(frame).toContain("
|
|
2025
|
+
expect(frame).toContain("IBKR Trading");
|
|
1381
2026
|
expect(frame).toContain("IBKR");
|
|
1382
2027
|
});
|
|
1383
2028
|
|