From 1b2130e0014c36e28500ef2bb079d8409defe90d Mon Sep 17 00:00:00 2001 From: Saravana Achu Mac Date: Mon, 4 May 2026 17:53:37 -0700 Subject: [PATCH] fix(D7): add scoped keyboard shortcuts --- web/src/components/layout/Header.dom.test.tsx | 12 ++++++- web/src/components/layout/Header.tsx | 16 +++++++++- .../strategy/CodeStrategyEditor.dom.test.tsx | 31 +++++++++++++++++++ .../strategy/CodeStrategyEditor.tsx | 22 +++++++++++-- .../strategy/VisualRuleBuilder.dom.test.tsx | 21 ++++++++++++- .../components/strategy/VisualRuleBuilder.tsx | 16 +++++++++- 6 files changed, 111 insertions(+), 7 deletions(-) diff --git a/web/src/components/layout/Header.dom.test.tsx b/web/src/components/layout/Header.dom.test.tsx index f1aa471..1052ee8 100644 --- a/web/src/components/layout/Header.dom.test.tsx +++ b/web/src/components/layout/Header.dom.test.tsx @@ -1,6 +1,6 @@ // @vitest-environment jsdom import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; -import { act, render } from '@testing-library/react'; +import { act, fireEvent, render, screen } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { AppContext, type AppContextValue } from '../../context/AppContext'; import { Header } from './Header'; @@ -87,4 +87,14 @@ describe('Header market index polling', () => { }); expect(fetchMarketIndicesMock).toHaveBeenCalledTimes(2); }); + + it('focuses ticker search with Cmd/Ctrl-K', () => { + renderHeader(); + + const search = screen.getByLabelText('Search ticker'); + expect(search).not.toHaveFocus(); + + fireEvent.keyDown(window, { key: 'k', metaKey: true }); + expect(search).toHaveFocus(); + }); }); diff --git a/web/src/components/layout/Header.tsx b/web/src/components/layout/Header.tsx index 29b5413..68ba94a 100644 --- a/web/src/components/layout/Header.tsx +++ b/web/src/components/layout/Header.tsx @@ -45,6 +45,18 @@ export function Header() { inputRef.current?.blur(); }; + useEffect(() => { + const handleShortcut = (event: KeyboardEvent) => { + if ((event.metaKey || event.ctrlKey) && event.key.toLowerCase() === 'k') { + event.preventDefault(); + inputRef.current?.focus(); + } + }; + + window.addEventListener('keydown', handleShortcut); + return () => window.removeEventListener('keydown', handleShortcut); + }, []); + return (
setQuery(e.target.value)} onKeyDown={e => { if (e.key === 'Enter') handleSearch(query); }} - placeholder="Ask about a company..." + placeholder="Ask about a company... (Cmd/Ctrl K)" + title="Focus search with Cmd/Ctrl+K" style={{ width: '100%', paddingLeft: 34, diff --git a/web/src/components/strategy/CodeStrategyEditor.dom.test.tsx b/web/src/components/strategy/CodeStrategyEditor.dom.test.tsx index f698a41..0057e17 100644 --- a/web/src/components/strategy/CodeStrategyEditor.dom.test.tsx +++ b/web/src/components/strategy/CodeStrategyEditor.dom.test.tsx @@ -22,6 +22,14 @@ vi.mock('../../lib/profileApi', () => ({ createTradeProfile: (...args: any[]) => createTradeProfileMock(...args), })); +vi.mock('../../lib/authSession', () => ({ + getPlatformAccessToken: vi.fn(async () => 'test-token'), +})); + +vi.mock('../../lib/runtime', () => ({ + tradingRuntime: { tradingApiUrl: 'https://trading.test' }, +})); + describe('CodeStrategyEditor save behavior', () => { let setItemSpy: ReturnType; @@ -33,6 +41,7 @@ describe('CodeStrategyEditor save behavior', () => { afterEach(() => { vi.useRealTimers(); + vi.unstubAllGlobals(); setItemSpy.mockRestore(); }); @@ -86,4 +95,26 @@ describe('CodeStrategyEditor save behavior', () => { expect(clearTimeoutSpy).toHaveBeenCalled(); clearTimeoutSpy.mockRestore(); }); + + it('saves with Cmd/Ctrl-S while focused in the editor', async () => { + render(); + + fireEvent.keyDown(screen.getByLabelText('strategy code'), { key: 's', metaKey: true }); + + await waitFor(() => expect(createTradeProfileMock).toHaveBeenCalledTimes(1)); + }); + + it('runs a backtest with Cmd/Ctrl-Enter while focused in the editor', async () => { + const fetchMock = vi.fn().mockResolvedValue({ + ok: true, + json: async () => ({ results: { trades: 0 } }), + }); + vi.stubGlobal('fetch', fetchMock); + + render(); + fireEvent.keyDown(screen.getByLabelText('strategy code'), { key: 'Enter', metaKey: true }); + + await waitFor(() => expect(fetchMock).toHaveBeenCalledTimes(1)); + expect(String(fetchMock.mock.calls[0][0])).toBe('https://trading.test/api/backtest/run'); + }); }); diff --git a/web/src/components/strategy/CodeStrategyEditor.tsx b/web/src/components/strategy/CodeStrategyEditor.tsx index d95c589..d898ed6 100644 --- a/web/src/components/strategy/CodeStrategyEditor.tsx +++ b/web/src/components/strategy/CodeStrategyEditor.tsx @@ -176,16 +176,32 @@ export function CodeStrategyEditor({ navigator.clipboard.writeText(code).catch(() => {}); }; + const handleShortcut = (event: React.KeyboardEvent) => { + if (!(event.metaKey || event.ctrlKey)) return; + const key = event.key.toLowerCase(); + + if (key === 's') { + event.preventDefault(); + if (!saving) void handleSave(); + } else if (event.key === 'Enter' && !running) { + event.preventDefault(); + void handleRunBacktest(); + } + }; + const fmt = (n: number | undefined, suffix = '') => n != null ? `${n.toFixed(2)}${suffix}` : '—'; return ( -
+
{/* Toolbar */}
Code Editor — {symbol} + + Cmd/Ctrl-S save · Cmd/Ctrl-Enter backtest +
- -