Skip to content

Commit 054a4e4

Browse files
committed
feat: improve focus input mechanism with promise-based polling
- Replace fragile setTimeout(100ms) with robust promise-based polling - Add waitForProvider() function that polls up to 10 times with 50ms intervals - Add isWebviewReady() method to check webview availability - Remove unused postMessageToWebviewWithConfirmation() method - Add comprehensive test suite with 6 test cases - Improve error handling to prevent focus failures from breaking main operation This makes the keyboard shortcut more reliable on slower systems and ensures the webview is ready before sending focus messages.
1 parent 3e853dd commit 054a4e4

File tree

3 files changed

+390
-12
lines changed

3 files changed

+390
-12
lines changed
Lines changed: 320 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,320 @@
1+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
2+
import * as vscode from "vscode"
3+
import { registerCodeActions } from "../registerCodeActions"
4+
import { ClineProvider } from "../../core/webview/ClineProvider"
5+
import { EditorUtils } from "../../integrations/editor/EditorUtils"
6+
7+
vi.mock("vscode", () => ({
8+
commands: {
9+
registerCommand: vi.fn(),
10+
executeCommand: vi.fn(),
11+
},
12+
ExtensionContext: vi.fn(),
13+
workspace: {
14+
workspaceFolders: undefined,
15+
getConfiguration: vi.fn().mockReturnValue({
16+
get: vi.fn().mockReturnValue(true),
17+
}),
18+
onDidChangeConfiguration: vi.fn(),
19+
onDidChangeWorkspaceFolders: vi.fn(),
20+
onDidOpenTextDocument: vi.fn(),
21+
onDidCloseTextDocument: vi.fn(),
22+
onDidChangeTextDocument: vi.fn(),
23+
},
24+
env: {
25+
uriScheme: "vscode",
26+
machineId: "test-machine-id",
27+
sessionId: "test-session-id",
28+
language: "en",
29+
appName: "Visual Studio Code",
30+
},
31+
window: {
32+
showErrorMessage: vi.fn(),
33+
showInformationMessage: vi.fn(),
34+
showWarningMessage: vi.fn(),
35+
createTextEditorDecorationType: vi.fn().mockReturnValue({
36+
dispose: vi.fn(),
37+
}),
38+
onDidChangeActiveTextEditor: vi.fn(),
39+
onDidChangeTextEditorSelection: vi.fn(),
40+
onDidChangeVisibleTextEditors: vi.fn(),
41+
activeTextEditor: undefined,
42+
},
43+
Uri: {
44+
file: vi.fn((path) => ({ fsPath: path, scheme: "file", path })),
45+
parse: vi.fn((str) => ({ fsPath: str, scheme: "file", path: str })),
46+
},
47+
Range: vi.fn().mockImplementation((startLine, startChar, endLine, endChar) => ({
48+
start: { line: startLine, character: startChar },
49+
end: { line: endLine, character: endChar },
50+
})),
51+
Position: vi.fn().mockImplementation((line, char) => ({ line, character: char })),
52+
EventEmitter: vi.fn().mockImplementation(() => ({
53+
fire: vi.fn(),
54+
event: vi.fn(),
55+
dispose: vi.fn(),
56+
})),
57+
Disposable: vi.fn().mockImplementation(() => ({
58+
dispose: vi.fn(),
59+
})),
60+
ExtensionMode: {
61+
Development: 2,
62+
Production: 1,
63+
Test: 3,
64+
},
65+
version: "1.0.0",
66+
}))
67+
68+
vi.mock("../../core/webview/ClineProvider")
69+
vi.mock("../../integrations/editor/EditorUtils")
70+
71+
describe("registerCodeActions - Focus Input Improvement", () => {
72+
let mockContext: any
73+
let mockProvider: any
74+
let registerCommandSpy: any
75+
76+
beforeEach(() => {
77+
vi.clearAllMocks()
78+
79+
mockContext = {
80+
subscriptions: {
81+
push: vi.fn(),
82+
},
83+
}
84+
85+
mockProvider = {
86+
postMessageToWebview: vi.fn().mockResolvedValue(undefined),
87+
isWebviewReady: vi.fn().mockReturnValue(true),
88+
}
89+
90+
registerCommandSpy = vi.spyOn(vscode.commands, "registerCommand")
91+
})
92+
93+
afterEach(() => {
94+
vi.restoreAllMocks()
95+
})
96+
97+
describe("addToContext command", () => {
98+
it("should wait for provider to become available before sending focus message", async () => {
99+
// Setup: No provider initially available
100+
const getVisibleInstanceSpy = vi
101+
.spyOn(ClineProvider, "getVisibleInstance")
102+
.mockReturnValueOnce(undefined) // First call returns undefined
103+
.mockReturnValueOnce(undefined) // Second call still undefined (during wait)
104+
.mockReturnValueOnce(mockProvider) // Third call returns provider
105+
106+
const executeCommandSpy = vi.spyOn(vscode.commands, "executeCommand").mockResolvedValue(undefined)
107+
108+
// Register the code actions
109+
registerCodeActions(mockContext)
110+
111+
// Get the registered command handler for addToContext
112+
const commandCall = registerCommandSpy.mock.calls.find(
113+
(call: any[]) => call[0] === "roo-cline.addToContext",
114+
)
115+
expect(commandCall).toBeDefined()
116+
117+
const commandHandler = commandCall[1]
118+
119+
// Mock editor context
120+
vi.spyOn(EditorUtils, "getEditorContext").mockReturnValue({
121+
filePath: "/test/file.ts",
122+
selectedText: "test code",
123+
startLine: 1,
124+
endLine: 5,
125+
})
126+
127+
// Execute the command
128+
await commandHandler()
129+
130+
// Verify the sidebar was focused
131+
expect(executeCommandSpy).toHaveBeenCalledWith("roo-cline.SidebarProvider.focus")
132+
133+
// Verify provider was checked multiple times (polling)
134+
expect(getVisibleInstanceSpy).toHaveBeenCalledTimes(3)
135+
136+
// Verify focus message was sent after provider became available
137+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
138+
type: "action",
139+
action: "focusInput",
140+
})
141+
})
142+
143+
it("should check if webview is ready before sending focus message", async () => {
144+
// Setup: Provider is available but webview not ready initially
145+
mockProvider.isWebviewReady
146+
.mockReturnValueOnce(false) // First check - not ready
147+
.mockReturnValueOnce(true) // Second check - ready
148+
149+
vi.spyOn(ClineProvider, "getVisibleInstance").mockReturnValue(mockProvider)
150+
151+
// Register the code actions
152+
registerCodeActions(mockContext)
153+
154+
// Get the registered command handler
155+
const commandCall = registerCommandSpy.mock.calls.find(
156+
(call: any[]) => call[0] === "roo-cline.addToContext",
157+
)
158+
const commandHandler = commandCall[1]
159+
160+
// Mock editor context
161+
vi.spyOn(EditorUtils, "getEditorContext").mockReturnValue({
162+
filePath: "/test/file.ts",
163+
selectedText: "test code",
164+
startLine: 1,
165+
endLine: 5,
166+
})
167+
168+
// Execute the command
169+
await commandHandler()
170+
171+
// Verify webview readiness was checked
172+
expect(mockProvider.isWebviewReady).toHaveBeenCalled()
173+
174+
// Verify message was sent only after webview was ready
175+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
176+
type: "action",
177+
action: "focusInput",
178+
})
179+
})
180+
181+
it("should handle errors gracefully without breaking the main operation", async () => {
182+
// Setup: Provider throws error when posting message
183+
mockProvider.postMessageToWebview.mockRejectedValue(new Error("Webview error"))
184+
vi.spyOn(ClineProvider, "getVisibleInstance").mockReturnValue(mockProvider)
185+
186+
const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {})
187+
188+
// Register the code actions
189+
registerCodeActions(mockContext)
190+
191+
// Get the registered command handler
192+
const commandCall = registerCommandSpy.mock.calls.find(
193+
(call: any[]) => call[0] === "roo-cline.addToContext",
194+
)
195+
const commandHandler = commandCall[1]
196+
197+
// Mock editor context
198+
vi.spyOn(EditorUtils, "getEditorContext").mockReturnValue({
199+
filePath: "/test/file.ts",
200+
selectedText: "test code",
201+
startLine: 1,
202+
endLine: 5,
203+
})
204+
205+
// Execute the command - should not throw
206+
await expect(commandHandler()).resolves.not.toThrow()
207+
208+
// Verify error was logged
209+
expect(consoleErrorSpy).toHaveBeenCalledWith("Failed to focus input field:", expect.any(Error))
210+
211+
consoleErrorSpy.mockRestore()
212+
})
213+
214+
it("should not attempt to focus if no provider becomes available", async () => {
215+
// Setup: No provider ever becomes available
216+
vi.spyOn(ClineProvider, "getVisibleInstance").mockReturnValue(undefined)
217+
218+
const executeCommandSpy = vi.spyOn(vscode.commands, "executeCommand").mockResolvedValue(undefined)
219+
220+
// Register the code actions
221+
registerCodeActions(mockContext)
222+
223+
// Get the registered command handler
224+
const commandCall = registerCommandSpy.mock.calls.find(
225+
(call: any[]) => call[0] === "roo-cline.addToContext",
226+
)
227+
const commandHandler = commandCall[1]
228+
229+
// Mock editor context
230+
vi.spyOn(EditorUtils, "getEditorContext").mockReturnValue({
231+
filePath: "/test/file.ts",
232+
selectedText: "test code",
233+
startLine: 1,
234+
endLine: 5,
235+
})
236+
237+
// Execute the command
238+
await commandHandler()
239+
240+
// Verify sidebar focus was attempted
241+
expect(executeCommandSpy).toHaveBeenCalledWith("roo-cline.SidebarProvider.focus")
242+
243+
// Verify no message was sent (since no provider available)
244+
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
245+
})
246+
247+
it("should immediately send focus message if provider is already available and ready", async () => {
248+
// Setup: Provider is immediately available and ready
249+
vi.spyOn(ClineProvider, "getVisibleInstance").mockReturnValue(mockProvider)
250+
mockProvider.isWebviewReady.mockReturnValue(true)
251+
252+
const executeCommandSpy = vi.spyOn(vscode.commands, "executeCommand")
253+
254+
// Register the code actions
255+
registerCodeActions(mockContext)
256+
257+
// Get the registered command handler
258+
const commandCall = registerCommandSpy.mock.calls.find(
259+
(call: any[]) => call[0] === "roo-cline.addToContext",
260+
)
261+
const commandHandler = commandCall[1]
262+
263+
// Mock editor context
264+
vi.spyOn(EditorUtils, "getEditorContext").mockReturnValue({
265+
filePath: "/test/file.ts",
266+
selectedText: "test code",
267+
startLine: 1,
268+
endLine: 5,
269+
})
270+
271+
// Execute the command
272+
await commandHandler()
273+
274+
// Verify no sidebar focus was needed
275+
expect(executeCommandSpy).not.toHaveBeenCalledWith("roo-cline.SidebarProvider.focus")
276+
277+
// Verify focus message was sent immediately
278+
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
279+
type: "action",
280+
action: "focusInput",
281+
})
282+
})
283+
})
284+
285+
describe("other commands", () => {
286+
it("should not attempt to focus input for non-addToContext commands", async () => {
287+
vi.spyOn(ClineProvider, "getVisibleInstance").mockReturnValue(mockProvider)
288+
289+
// Register the code actions
290+
registerCodeActions(mockContext)
291+
292+
// Test other commands
293+
const otherCommands = ["roo-cline.explainCode", "roo-cline.fixCode", "roo-cline.improveCode"]
294+
295+
for (const commandName of otherCommands) {
296+
const commandCall = registerCommandSpy.mock.calls.find((call: any[]) => call[0] === commandName)
297+
expect(commandCall).toBeDefined()
298+
299+
const commandHandler = commandCall[1]
300+
301+
// Mock editor context
302+
vi.spyOn(EditorUtils, "getEditorContext").mockReturnValue({
303+
filePath: "/test/file.ts",
304+
selectedText: "test code",
305+
startLine: 1,
306+
endLine: 5,
307+
})
308+
309+
// Execute the command
310+
await commandHandler()
311+
312+
// Verify no focus message was sent
313+
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalledWith({
314+
type: "action",
315+
action: "focusInput",
316+
})
317+
}
318+
})
319+
})
320+
})

0 commit comments

Comments
 (0)