gloomberb 0.7.2 → 0.7.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +79 -132
- package/package.json +1 -1
- package/src/app.tsx +73 -67
- package/src/components/chart/chart-data.ts +7 -2
- package/src/components/chart/chart-pane-settings.ts +7 -4
- package/src/components/chart/chart-renderer.ts +158 -47
- package/src/components/chart/chart-types.ts +20 -2
- package/src/components/chart/chart.test.ts +179 -13
- package/src/components/chart/comparison-chart-renderer.test.ts +14 -30
- package/src/components/chart/comparison-chart-renderer.ts +50 -11
- package/src/components/chart/comparison-stock-chart.tsx +129 -129
- package/src/components/chart/market-session.ts +187 -0
- package/src/components/chart/native/chart-rasterizer.ts +108 -19
- package/src/components/chart/price-axis-labels.tsx +25 -9
- package/src/components/chart/stock-chart-renderer-switch.test.tsx +97 -1
- package/src/components/chart/stock-chart.tsx +95 -49
- package/src/components/chart/time-axis-label.tsx +56 -2
- package/src/components/command-bar/command-bar.test.tsx +446 -120
- package/src/components/command-bar/command-bar.tsx +535 -384
- package/src/components/command-bar/command-registry.ts +16 -6
- package/src/components/command-bar/list-model.ts +8 -4
- package/src/components/command-bar/root-shortcuts.ts +4 -5
- package/src/components/command-bar/view-model.test.ts +21 -0
- package/src/components/command-bar/view-model.ts +17 -4
- package/src/components/command-bar/workflow-ops.ts +11 -32
- package/src/components/data-table-view.test.tsx +24 -1
- package/src/components/data-table-view.tsx +3 -2
- package/src/components/feed-data-table-stack-view.tsx +3 -2
- package/src/components/layout/detached-pane-shell.tsx +2 -2
- package/src/components/layout/floating-pane.tsx +9 -2
- package/src/components/layout/pane-footer.tsx +1 -1
- package/src/components/layout/pane-header.tsx +4 -2
- package/src/components/layout/pane-sizing.ts +12 -0
- package/src/components/layout/pane.tsx +24 -2
- package/src/components/layout/shell.tsx +24 -10
- package/src/components/layout/status-bar.test.tsx +4 -1
- package/src/components/layout/status-bar.tsx +4 -24
- package/src/components/markdown-text.tsx +53 -8
- package/src/components/onboarding/onboarding-wizard.tsx +7 -6
- package/src/components/pane-settings-dialog.tsx +9 -6
- package/src/components/pane-template-wizard.tsx +3 -2
- package/src/components/price-selector-dialog.tsx +3 -2
- package/src/components/price-sparkline-view.tsx +463 -0
- package/src/components/price-sparkline.ts +46 -0
- package/src/components/table-view-shared.tsx +21 -3
- package/src/components/ticker-list-table-view.tsx +3 -2
- package/src/components/ticker-list-table.tsx +15 -66
- package/src/components/toggle-list.tsx +3 -3
- package/src/components/ui/choice-dialog.tsx +3 -2
- package/src/components/ui/multi-select-dialog.tsx +3 -2
- package/src/components/ui/native-select.tsx +3 -3
- package/src/core/state/app-state.ts +210 -80
- package/src/core/state/session-persistence.ts +25 -0
- package/src/data/config-store-node.ts +113 -25
- package/src/data/config-store.test.ts +65 -3
- package/src/data/plugin-state-store.ts +39 -13
- package/src/data/sqlite-cache.test.ts +13 -0
- package/src/market-data/coordinator-subscriptions.test.ts +102 -18
- package/src/market-data/coordinator.test.ts +122 -2
- package/src/market-data/coordinator.ts +200 -56
- package/src/market-data/hooks.test.tsx +31 -0
- package/src/market-data/hooks.tsx +1 -0
- package/src/news/aggregator.test.ts +41 -0
- package/src/news/aggregator.ts +61 -9
- package/src/plugins/builtin/ai/ask-ai-detail-tab.tsx +25 -6
- package/src/plugins/builtin/ai/screener-pane.tsx +3 -0
- package/src/plugins/builtin/ai/settings.ts +3 -0
- package/src/plugins/builtin/broker-manager/index.tsx +4 -3
- package/src/plugins/builtin/chat-controller.test.ts +398 -17
- package/src/plugins/builtin/chat-controller.ts +263 -48
- package/src/plugins/builtin/chat.test.tsx +312 -6
- package/src/plugins/builtin/chat.tsx +245 -66
- package/src/plugins/builtin/comparison-chart.test.tsx +117 -3
- package/src/plugins/builtin/correlation/colors.test.ts +39 -0
- package/src/plugins/builtin/correlation/colors.ts +87 -0
- package/src/plugins/builtin/correlation/index.test.tsx +151 -0
- package/src/plugins/builtin/correlation/index.tsx +92 -21
- package/src/plugins/builtin/debug.tsx +4 -3
- package/src/plugins/builtin/econ/index.tsx +3 -2
- package/src/plugins/builtin/help.test.tsx +2 -2
- package/src/plugins/builtin/holders/index.tsx +25 -13
- package/src/plugins/builtin/layout-manager.tsx +1 -7
- package/src/plugins/builtin/market-movers/index.tsx +72 -40
- package/src/plugins/builtin/news-wire/breaking-pane.test.tsx +156 -0
- package/src/plugins/builtin/news-wire/breaking-pane.tsx +0 -90
- package/src/plugins/builtin/news-wire/index.tsx +1 -3
- package/src/plugins/builtin/news-wire/news-detail-view.tsx +3 -2
- package/src/plugins/builtin/news-wire/news-preset-pane.tsx +4 -1
- package/src/plugins/builtin/news-wire/persisted-articles.ts +7 -1
- package/src/plugins/builtin/options.tsx +9 -17
- package/src/plugins/builtin/portfolio-list/cli/commands.ts +6 -10
- package/src/plugins/builtin/portfolio-list/metrics.ts +13 -2
- package/src/plugins/builtin/portfolio-list/mutations.ts +17 -0
- package/src/plugins/builtin/portfolio-list/pane.tsx +139 -23
- package/src/plugins/builtin/portfolio-list/quick-add.tsx +422 -0
- package/src/plugins/builtin/portfolio-list/settings.ts +20 -12
- package/src/plugins/builtin/portfolio-list/table.tsx +3 -3
- package/src/plugins/builtin/portfolio-list.test.tsx +311 -9
- package/src/plugins/builtin/quote-monitor.test.tsx +158 -25
- package/src/plugins/builtin/sectors/index.tsx +275 -43
- package/src/plugins/builtin/sectors/sector-data.ts +41 -1
- package/src/plugins/builtin/ticker-detail/chart-tab.tsx +3 -2
- package/src/plugins/builtin/ticker-detail/index.tsx +30 -9
- package/src/plugins/builtin/ticker-detail/overview-tab.tsx +25 -15
- package/src/plugins/builtin/ticker-detail/pane.tsx +18 -7
- package/src/plugins/builtin/ticker-detail/quote-monitor.tsx +445 -63
- package/src/plugins/builtin/ticker-detail/settings.ts +84 -3
- package/src/plugins/builtin/ticker-detail.test.tsx +2 -2
- package/src/plugins/builtin/world-indices/index.tsx +58 -19
- package/src/plugins/ibkr/trading-pane.tsx +3 -2
- package/src/plugins/plugin-runtime.tsx +2 -2
- package/src/plugins/prediction-markets/settings.ts +4 -0
- package/src/plugins/registry.test.ts +55 -0
- package/src/plugins/registry.ts +11 -4
- package/src/plugins/ticker-navigation.test.ts +35 -5
- package/src/plugins/ticker-navigation.ts +16 -22
- package/src/renderers/electrobun/bun/application-menu-click.test.ts +9 -0
- package/src/renderers/electrobun/bun/application-menu-click.ts +7 -4
- package/src/renderers/electrobun/bun/application-menu.ts +6 -1
- package/src/renderers/electrobun/bun/desktop-workspace.ts +47 -29
- package/src/renderers/electrobun/bun/index.ts +52 -13
- package/src/renderers/electrobun/view/app-services.ts +51 -9
- package/src/renderers/electrobun/view/backend-rpc.ts +5 -8
- package/src/renderers/electrobun/view/desktop-controls.tsx +4 -4
- package/src/renderers/electrobun/view/main.tsx +0 -16
- package/src/renderers/electrobun/view/styles.css +23 -21
- package/src/renderers/electrobun/view/ui-host.tsx +6 -4
- package/src/renderers/opentui/host.tsx +2 -1
- package/src/renderers/opentui/start.tsx +1 -1
- package/src/renderers/opentui/test-utils.tsx +6 -5
- package/src/sources/gloomberb-cloud.test.ts +55 -2
- package/src/sources/gloomberb-cloud.ts +180 -29
- package/src/sources/provider-router.test.ts +567 -10
- package/src/sources/provider-router.ts +323 -93
- package/src/sources/yahoo-finance.test.ts +37 -0
- package/src/sources/yahoo-finance.ts +48 -2
- package/src/state/app-bootstrap.test.ts +50 -23
- package/src/state/app-bootstrap.ts +29 -13
- package/src/state/app-context.test.ts +113 -0
- package/src/state/app-context.tsx +8 -4
- package/src/state/session-persistence.test.ts +5 -3
- package/src/state/use-inline-tickers.ts +3 -0
- package/src/state/use-quote-streaming.ts +11 -0
- package/src/theme/colors.ts +13 -14
- package/src/types/config.ts +9 -4
- package/src/types/data-provider.ts +5 -1
- package/src/types/financials.ts +1 -0
- package/src/types/plugin.ts +8 -1
- package/src/ui/context-menu.tsx +4 -4
- package/src/utils/api-client.test.ts +248 -0
- package/src/utils/api-client.ts +178 -26
- package/src/utils/exchanges.test.ts +16 -0
- package/src/utils/exchanges.ts +33 -0
- package/src/utils/fuzzy-search.ts +12 -1
- package/src/utils/keyboard.ts +31 -0
- package/src/utils/options.ts +1 -1
- package/src/utils/price-history.test.ts +28 -1
- package/src/utils/price-history.ts +16 -0
- package/src/utils/quote-freshness.test.ts +150 -0
- package/src/utils/quote-freshness.ts +42 -1
- package/src/utils/quote-resolution.ts +2 -0
- package/src/utils/quote-time.test.ts +34 -0
- package/src/utils/quote-time.ts +18 -5
- package/src/utils/ticker-search.test.ts +2 -2
- package/src/utils/ticker-search.ts +48 -1
- package/src/version.ts +1 -1
- package/src/plugins/builtin/news-wire/digest-store.ts +0 -44
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { afterEach, describe, expect, test } from "bun:test";
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
2
2
|
import { act, useCallback, useMemo, useRef, useState } from "react";
|
|
3
3
|
import { PaneFooterBar, PaneFooterProvider } from "../../components/layout/pane-footer";
|
|
4
4
|
import { testRender } from "../../renderers/opentui/test-utils";
|
|
@@ -8,7 +8,7 @@ import { colors } from "../../theme/colors";
|
|
|
8
8
|
import { createDefaultConfig, findPaneInstance, type PaneInstanceConfig } from "../../types/config";
|
|
9
9
|
import type { PersistedResourceValue } from "../../types/persistence";
|
|
10
10
|
import type { PluginPersistence } from "../../types/plugin";
|
|
11
|
-
import { Box } from "../../ui";
|
|
11
|
+
import { Box, TextAttributes } from "../../ui";
|
|
12
12
|
import { apiClient, type ChatChannel, type ChatMessage } from "../../utils/api-client";
|
|
13
13
|
import { PluginRenderProvider, type PluginRuntimeAccess } from "../plugin-runtime";
|
|
14
14
|
import { setSharedMarketDataForTests, setSharedRegistryForTests } from "../registry";
|
|
@@ -21,6 +21,8 @@ const TRANSCRIPT_SOURCE = "server";
|
|
|
21
21
|
const TRANSCRIPT_SCHEMA_VERSION = 2;
|
|
22
22
|
const originalConnectChannel = apiClient.connectChannel.bind(apiClient);
|
|
23
23
|
const originalGetChannels = apiClient.getChannels.bind(apiClient);
|
|
24
|
+
const originalGetChatPresence = apiClient.getChatPresence.bind(apiClient);
|
|
25
|
+
const originalUpdateChatChannelState = apiClient.updateChatChannelState.bind(apiClient);
|
|
24
26
|
|
|
25
27
|
let testSetup: Awaited<ReturnType<typeof testRender>> | undefined;
|
|
26
28
|
|
|
@@ -150,6 +152,8 @@ function createController(options: {
|
|
|
150
152
|
controller.attachPersistence(persistence);
|
|
151
153
|
controller.refreshSession = async () => {};
|
|
152
154
|
controller.refreshMessages = async () => {};
|
|
155
|
+
controller.refreshPresence = async () => {};
|
|
156
|
+
controller.refreshChatState = async () => {};
|
|
153
157
|
return controller;
|
|
154
158
|
}
|
|
155
159
|
|
|
@@ -248,11 +252,23 @@ function hexToRgbaInts(hex: string) {
|
|
|
248
252
|
].join(",");
|
|
249
253
|
}
|
|
250
254
|
|
|
255
|
+
beforeEach(() => {
|
|
256
|
+
apiClient.getChatPresence = async () => ({ onlineCount: 0 });
|
|
257
|
+
apiClient.updateChatChannelState = async (channelId, body) => ({
|
|
258
|
+
channelId,
|
|
259
|
+
notificationsEnabled: body.notificationsEnabled ?? false,
|
|
260
|
+
lastReadMessageId: body.readThroughMessageId ?? null,
|
|
261
|
+
unreadCount: 0,
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
251
265
|
afterEach(async () => {
|
|
252
266
|
setSharedRegistryForTests(undefined);
|
|
253
267
|
setSharedMarketDataForTests(undefined);
|
|
254
268
|
apiClient.connectChannel = originalConnectChannel;
|
|
255
269
|
apiClient.getChannels = originalGetChannels;
|
|
270
|
+
apiClient.getChatPresence = originalGetChatPresence;
|
|
271
|
+
apiClient.updateChatChannelState = originalUpdateChatChannelState;
|
|
256
272
|
apiClient.setSessionToken(null);
|
|
257
273
|
|
|
258
274
|
if (testSetup) {
|
|
@@ -468,6 +484,136 @@ describe("ChatContent", () => {
|
|
|
468
484
|
expect(testSetup.captureCharFrame()).toContain("#options");
|
|
469
485
|
});
|
|
470
486
|
|
|
487
|
+
test("renders unread sidebar channels in bold and clears them when opened", async () => {
|
|
488
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
489
|
+
installServerChannels(controller);
|
|
490
|
+
controller.refreshChannels = async () => {};
|
|
491
|
+
controller.refreshChannelMessages = async () => {};
|
|
492
|
+
const optionsState = (controller as any).ensureChannelState("options");
|
|
493
|
+
optionsState.unreadCount = 2;
|
|
494
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
495
|
+
|
|
496
|
+
function ChannelPane() {
|
|
497
|
+
const [channelId, setChannelId] = useState("equities");
|
|
498
|
+
return (
|
|
499
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
500
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
|
|
501
|
+
<ChatContent
|
|
502
|
+
controller={controller}
|
|
503
|
+
width={90}
|
|
504
|
+
height={12}
|
|
505
|
+
focused
|
|
506
|
+
channelId={channelId}
|
|
507
|
+
onChannelChange={setChannelId}
|
|
508
|
+
/>
|
|
509
|
+
</PluginRenderProvider>
|
|
510
|
+
</AppContext>
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
await act(async () => {
|
|
515
|
+
testSetup = await testRender(<ChannelPane />, {
|
|
516
|
+
width: 90,
|
|
517
|
+
height: 12,
|
|
518
|
+
});
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
await flushFrame();
|
|
522
|
+
const unreadLine = testSetup.captureSpans().lines.find((line) => lineText(line).includes("options"));
|
|
523
|
+
const unreadSpan = unreadLine?.spans.find((span) => span.text.includes("options"));
|
|
524
|
+
expect((unreadSpan?.attributes ?? 0) & TextAttributes.BOLD).toBe(TextAttributes.BOLD);
|
|
525
|
+
|
|
526
|
+
const lines = testSetup.captureCharFrame().split("\n");
|
|
527
|
+
const row = lines.findIndex((line) => line.includes("options"));
|
|
528
|
+
const col = lines[row]?.indexOf("options") ?? -1;
|
|
529
|
+
|
|
530
|
+
await act(async () => {
|
|
531
|
+
await testSetup!.mockMouse.click(col + 1, row);
|
|
532
|
+
await testSetup!.renderOnce();
|
|
533
|
+
await testSetup!.renderOnce();
|
|
534
|
+
});
|
|
535
|
+
await flushFrame();
|
|
536
|
+
|
|
537
|
+
const readLine = testSetup.captureSpans().lines.find((line) => lineText(line).includes("#options"));
|
|
538
|
+
const readSpan = readLine?.spans.find((span) => span.text.includes("options"));
|
|
539
|
+
expect((readSpan?.attributes ?? 0) & TextAttributes.BOLD).toBe(0);
|
|
540
|
+
});
|
|
541
|
+
|
|
542
|
+
test("toggles sidebar channel notifications without selecting the channel", async () => {
|
|
543
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
544
|
+
installServerChannels(controller);
|
|
545
|
+
controller.refreshChannels = async () => {};
|
|
546
|
+
controller.refreshChannelMessages = async () => {};
|
|
547
|
+
const toggles: Array<{ channelId: string; enabled: boolean }> = [];
|
|
548
|
+
controller.setChannelNotificationsEnabled = (channelId: string, enabled: boolean) => {
|
|
549
|
+
toggles.push({ channelId, enabled });
|
|
550
|
+
};
|
|
551
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat"));
|
|
552
|
+
|
|
553
|
+
function ChannelPane() {
|
|
554
|
+
const [channelId, setChannelId] = useState("equities");
|
|
555
|
+
return (
|
|
556
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
557
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
|
|
558
|
+
<ChatContent
|
|
559
|
+
controller={controller}
|
|
560
|
+
width={90}
|
|
561
|
+
height={12}
|
|
562
|
+
focused
|
|
563
|
+
channelId={channelId}
|
|
564
|
+
onChannelChange={setChannelId}
|
|
565
|
+
/>
|
|
566
|
+
</PluginRenderProvider>
|
|
567
|
+
</AppContext>
|
|
568
|
+
);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
await act(async () => {
|
|
572
|
+
testSetup = await testRender(<ChannelPane />, {
|
|
573
|
+
width: 90,
|
|
574
|
+
height: 12,
|
|
575
|
+
});
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
await flushFrame();
|
|
579
|
+
const frame = testSetup.captureCharFrame();
|
|
580
|
+
const lines = frame.split("\n");
|
|
581
|
+
const row = lines.findIndex((line) => line.includes("options"));
|
|
582
|
+
const col = lines[row]?.lastIndexOf("·") ?? -1;
|
|
583
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
584
|
+
expect(col).toBeGreaterThanOrEqual(0);
|
|
585
|
+
|
|
586
|
+
await act(async () => {
|
|
587
|
+
await testSetup!.mockMouse.click(col, row);
|
|
588
|
+
await testSetup!.renderOnce();
|
|
589
|
+
await testSetup!.renderOnce();
|
|
590
|
+
});
|
|
591
|
+
await flushFrame();
|
|
592
|
+
|
|
593
|
+
expect(toggles).toEqual([{ channelId: "options", enabled: true }]);
|
|
594
|
+
expect(testSetup.captureCharFrame()).toContain("#equities");
|
|
595
|
+
expect(testSetup.captureCharFrame()).not.toContain("#options");
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
test("shows the sidebar online count footer", async () => {
|
|
599
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
600
|
+
installServerChannels(controller);
|
|
601
|
+
controller.refreshChannels = async () => {};
|
|
602
|
+
controller.refreshChannelMessages = async () => {};
|
|
603
|
+
(controller as any).onlineCount = 6;
|
|
604
|
+
|
|
605
|
+
await act(async () => {
|
|
606
|
+
testSetup = await testRender(createHarness(controller, { width: 90, height: 12 }), {
|
|
607
|
+
width: 90,
|
|
608
|
+
height: 12,
|
|
609
|
+
});
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
await flushFrame();
|
|
613
|
+
|
|
614
|
+
expect(testSetup.captureCharFrame()).toContain("● 6 online");
|
|
615
|
+
});
|
|
616
|
+
|
|
471
617
|
test("uses arrows to move between channel sidebar and chat content", async () => {
|
|
472
618
|
const controller = createController({ sessionToken: "token-123" });
|
|
473
619
|
installServerChannels(controller);
|
|
@@ -818,6 +964,34 @@ describe("ChatContent", () => {
|
|
|
818
964
|
expect(frame).toContain("Reply to @user2...");
|
|
819
965
|
});
|
|
820
966
|
|
|
967
|
+
test("modified k does not move message selection", async () => {
|
|
968
|
+
const controller = createController({
|
|
969
|
+
messages: [makeMessage(1), makeMessage(2)],
|
|
970
|
+
});
|
|
971
|
+
|
|
972
|
+
await act(async () => {
|
|
973
|
+
testSetup = await testRender(createHarness(controller, {
|
|
974
|
+
width: 72,
|
|
975
|
+
height: 12,
|
|
976
|
+
}), {
|
|
977
|
+
width: 72,
|
|
978
|
+
height: 12,
|
|
979
|
+
});
|
|
980
|
+
});
|
|
981
|
+
|
|
982
|
+
await flushFrame();
|
|
983
|
+
|
|
984
|
+
await emitKeypress({ name: "up", sequence: "\u001b[A" });
|
|
985
|
+
await emitKeypress({ name: "k", sequence: "k", meta: true });
|
|
986
|
+
await emitKeypress({ name: "return", sequence: "\r" });
|
|
987
|
+
await flushFrame();
|
|
988
|
+
|
|
989
|
+
const frame = testSetup.captureCharFrame();
|
|
990
|
+
expect(frame).toContain("replying to @user2");
|
|
991
|
+
expect(frame).toContain("Reply to @user2...");
|
|
992
|
+
expect(frame).not.toContain("replying to @user1");
|
|
993
|
+
});
|
|
994
|
+
|
|
821
995
|
test("shows the reply action next to the selected message timestamp", async () => {
|
|
822
996
|
const controller = createController({
|
|
823
997
|
messages: [makeMessage(1), makeMessage(2)],
|
|
@@ -1291,6 +1465,61 @@ describe("ChatContent", () => {
|
|
|
1291
1465
|
expect(sentMessages).toEqual(["first line\nsecond line"]);
|
|
1292
1466
|
});
|
|
1293
1467
|
|
|
1468
|
+
test("clears the composer locally after an accepted send", async () => {
|
|
1469
|
+
const controller = createController({ sessionToken: "token-123" });
|
|
1470
|
+
const sentMessages: string[] = [];
|
|
1471
|
+
(controller as any).send = (content: string) => {
|
|
1472
|
+
sentMessages.push(content);
|
|
1473
|
+
return true;
|
|
1474
|
+
};
|
|
1475
|
+
(controller as any).setDraft = () => {};
|
|
1476
|
+
|
|
1477
|
+
await act(async () => {
|
|
1478
|
+
testSetup = await testRender(createHarness(controller), {
|
|
1479
|
+
width: 60,
|
|
1480
|
+
height: 12,
|
|
1481
|
+
});
|
|
1482
|
+
});
|
|
1483
|
+
|
|
1484
|
+
await flushFrame();
|
|
1485
|
+
|
|
1486
|
+
const frameBeforeClick = testSetup.captureCharFrame().split("\n");
|
|
1487
|
+
const inputRow = frameBeforeClick.findIndex((line) => line.includes("Type a message..."));
|
|
1488
|
+
const inputCol = frameBeforeClick[inputRow]?.indexOf("Type a message...") ?? -1;
|
|
1489
|
+
|
|
1490
|
+
expect(inputRow).toBeGreaterThanOrEqual(0);
|
|
1491
|
+
expect(inputCol).toBeGreaterThanOrEqual(0);
|
|
1492
|
+
|
|
1493
|
+
await act(async () => {
|
|
1494
|
+
await testSetup!.mockMouse.click(inputCol + 1, inputRow);
|
|
1495
|
+
await testSetup!.renderOnce();
|
|
1496
|
+
await testSetup!.renderOnce();
|
|
1497
|
+
});
|
|
1498
|
+
|
|
1499
|
+
await act(async () => {
|
|
1500
|
+
await testSetup!.mockInput.typeText("hello");
|
|
1501
|
+
await testSetup!.renderOnce();
|
|
1502
|
+
await testSetup!.renderOnce();
|
|
1503
|
+
});
|
|
1504
|
+
|
|
1505
|
+
await act(async () => {
|
|
1506
|
+
testSetup!.mockInput.pressEnter();
|
|
1507
|
+
await testSetup!.renderOnce();
|
|
1508
|
+
await testSetup!.renderOnce();
|
|
1509
|
+
});
|
|
1510
|
+
|
|
1511
|
+
expect(sentMessages).toEqual(["hello"]);
|
|
1512
|
+
expect(testSetup.captureCharFrame()).not.toContain("> hello");
|
|
1513
|
+
|
|
1514
|
+
await act(async () => {
|
|
1515
|
+
testSetup!.mockInput.pressEnter();
|
|
1516
|
+
await testSetup!.renderOnce();
|
|
1517
|
+
await testSetup!.renderOnce();
|
|
1518
|
+
});
|
|
1519
|
+
|
|
1520
|
+
expect(sentMessages).toEqual(["hello"]);
|
|
1521
|
+
});
|
|
1522
|
+
|
|
1294
1523
|
test("keeps typed shortcut letters in the composer instead of moving message selection", async () => {
|
|
1295
1524
|
const controller = createController({
|
|
1296
1525
|
messages: Array.from({ length: 18 }, (_, index) => makeMessage(index + 1)),
|
|
@@ -1448,7 +1677,7 @@ describe("ChatContent", () => {
|
|
|
1448
1677
|
expect(frame).not.toContain("message 1");
|
|
1449
1678
|
});
|
|
1450
1679
|
|
|
1451
|
-
test("
|
|
1680
|
+
test("preserves a manually scrolled transcript across focus and snapshot refreshes", async () => {
|
|
1452
1681
|
const controller = createController({
|
|
1453
1682
|
messages: [
|
|
1454
1683
|
makeMessage(1),
|
|
@@ -1506,13 +1735,90 @@ describe("ChatContent", () => {
|
|
|
1506
1735
|
|
|
1507
1736
|
await act(async () => {
|
|
1508
1737
|
setFocused?.(true);
|
|
1738
|
+
(controller as any).emit("everyone");
|
|
1739
|
+
await testSetup!.renderOnce();
|
|
1740
|
+
});
|
|
1741
|
+
await flushFrame();
|
|
1742
|
+
|
|
1743
|
+
const refreshedFrame = testSetup.captureCharFrame();
|
|
1744
|
+
expect(refreshedFrame).toContain("message 2");
|
|
1745
|
+
expect(refreshedFrame).not.toContain("message 8");
|
|
1746
|
+
});
|
|
1747
|
+
|
|
1748
|
+
test("opens a newly selected channel at the latest message", async () => {
|
|
1749
|
+
const controller = createController({
|
|
1750
|
+
messages: Array.from({ length: 8 }, (_, index) => makeMessage(index + 1)),
|
|
1751
|
+
});
|
|
1752
|
+
installServerChannels(controller, TEST_CHAT_CHANNELS.slice(0, 2));
|
|
1753
|
+
controller.refreshChannels = async () => {};
|
|
1754
|
+
controller.refreshChannelMessages = async () => {};
|
|
1755
|
+
|
|
1756
|
+
const equitiesMessages = Array.from({ length: 8 }, (_, index) => ({
|
|
1757
|
+
...makeMessage(index + 1),
|
|
1758
|
+
id: `o${index + 1}`,
|
|
1759
|
+
channelId: "equities",
|
|
1760
|
+
content: `equities message ${index + 1}`,
|
|
1761
|
+
}));
|
|
1762
|
+
const equitiesState = (controller as any).ensureChannelState("equities");
|
|
1763
|
+
equitiesState.messages = equitiesMessages;
|
|
1764
|
+
equitiesState.lastCursor = "o8";
|
|
1765
|
+
|
|
1766
|
+
const state = createInitialState(createDefaultConfig("/tmp/gloomberb-chat-channel-scroll"));
|
|
1767
|
+
|
|
1768
|
+
function ChannelPane() {
|
|
1769
|
+
const [channelId, setChannelId] = useState("everyone");
|
|
1770
|
+
return (
|
|
1771
|
+
<AppContext value={{ state, dispatch: () => {} }}>
|
|
1772
|
+
<PluginRenderProvider pluginId="gloomberb-cloud" runtime={createTestPluginRuntime()}>
|
|
1773
|
+
<ChatContent
|
|
1774
|
+
controller={controller}
|
|
1775
|
+
width={90}
|
|
1776
|
+
height={13}
|
|
1777
|
+
focused
|
|
1778
|
+
channelId={channelId}
|
|
1779
|
+
onChannelChange={setChannelId}
|
|
1780
|
+
/>
|
|
1781
|
+
</PluginRenderProvider>
|
|
1782
|
+
</AppContext>
|
|
1783
|
+
);
|
|
1784
|
+
}
|
|
1785
|
+
|
|
1786
|
+
await act(async () => {
|
|
1787
|
+
testSetup = await testRender(<ChannelPane />, {
|
|
1788
|
+
width: 90,
|
|
1789
|
+
height: 13,
|
|
1790
|
+
});
|
|
1791
|
+
});
|
|
1792
|
+
|
|
1793
|
+
await flushFrame();
|
|
1794
|
+
expect(testSetup.captureCharFrame()).toContain("message 8");
|
|
1795
|
+
|
|
1796
|
+
await act(async () => {
|
|
1797
|
+
await testSetup!.mockMouse.scroll(24, 4, "up");
|
|
1798
|
+
await testSetup!.mockMouse.scroll(24, 4, "up");
|
|
1799
|
+
await testSetup!.mockMouse.scroll(24, 4, "up");
|
|
1800
|
+
await testSetup!.renderOnce();
|
|
1801
|
+
await testSetup!.renderOnce();
|
|
1802
|
+
});
|
|
1803
|
+
await flushFrame();
|
|
1804
|
+
expect(testSetup.captureCharFrame()).toContain("message 2");
|
|
1805
|
+
|
|
1806
|
+
const lines = testSetup.captureCharFrame().split("\n");
|
|
1807
|
+
const row = lines.findIndex((line) => line.includes("equities"));
|
|
1808
|
+
const col = lines[row]?.indexOf("equities") ?? -1;
|
|
1809
|
+
expect(row).toBeGreaterThanOrEqual(0);
|
|
1810
|
+
expect(col).toBeGreaterThanOrEqual(0);
|
|
1811
|
+
|
|
1812
|
+
await act(async () => {
|
|
1813
|
+
await testSetup!.mockMouse.click(col + 1, row);
|
|
1814
|
+
await testSetup!.renderOnce();
|
|
1509
1815
|
await testSetup!.renderOnce();
|
|
1510
1816
|
});
|
|
1511
1817
|
await flushFrame();
|
|
1512
1818
|
|
|
1513
|
-
const
|
|
1514
|
-
expect(
|
|
1515
|
-
expect(
|
|
1819
|
+
const channelFrame = testSetup.captureCharFrame();
|
|
1820
|
+
expect(channelFrame).toContain("equities message 8");
|
|
1821
|
+
expect(channelFrame).not.toContain("equities message 1");
|
|
1516
1822
|
});
|
|
1517
1823
|
|
|
1518
1824
|
test("keeps the bottom reachable after narrowing the chat pane", async () => {
|