-
Notifications
You must be signed in to change notification settings - Fork 212
Expand file tree
/
Copy pathregisterCommands.spec.ts
More file actions
351 lines (291 loc) · 10.4 KB
/
Copy pathregisterCommands.spec.ts
File metadata and controls
351 lines (291 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
import type { Mock } from "vitest"
import * as vscode from "vscode"
import { ClineProvider } from "../../core/webview/ClineProvider"
import { getVisibleProviderOrLog, registerCommands, setPanel } from "../registerCommands"
vi.mock("execa", () => ({
execa: vi.fn(),
}))
vi.mock("vscode", () => ({
CodeActionKind: {
QuickFix: { value: "quickfix" },
RefactorRewrite: { value: "refactor.rewrite" },
},
window: {
createTextEditorDecorationType: vi.fn().mockReturnValue({ dispose: vi.fn() }),
},
workspace: {
workspaceFolders: [
{
uri: {
fsPath: "/mock/workspace",
},
},
],
},
commands: {
registerCommand: vi.fn(),
executeCommand: vi.fn(),
},
}))
vi.mock("../../core/webview/ClineProvider")
vi.mock("../../shared/package", () => ({
Package: {
name: "zoo-code",
},
}))
vi.mock("@roo-code/telemetry", () => ({
TelemetryService: {
instance: {
captureTitleButtonClicked: vi.fn(),
},
},
}))
vi.mock("../../utils/focusPanel", () => ({
focusPanel: vi.fn().mockResolvedValue(undefined),
}))
vi.mock("../handleTask", () => ({
handleNewTask: vi.fn(),
}))
vi.mock("../../core/config/importExport", () => ({
importSettingsWithFeedback: vi.fn(),
}))
vi.mock("../../services/code-index/manager", () => ({
CodeIndexManager: {
getInstance: vi.fn(),
},
}))
vi.mock("../../services/mdm/MdmService", () => ({
MdmService: {
getInstance: vi.fn(),
},
}))
vi.mock("../../core/config/ContextProxy", () => ({
ContextProxy: {
getInstance: vi.fn(),
},
}))
vi.mock("../../i18n", () => ({
t: (key: string) => key,
}))
vi.mock("../../services/ripgrep/diagnostic", () => ({
registerRipgrepDiagnosticCommand: vi.fn().mockReturnValue({ dispose: vi.fn() }),
}))
describe("getVisibleProviderOrLog", () => {
let mockOutputChannel: vscode.OutputChannel
beforeEach(() => {
mockOutputChannel = {
appendLine: vi.fn(),
append: vi.fn(),
clear: vi.fn(),
hide: vi.fn(),
name: "mock",
replace: vi.fn(),
show: vi.fn(),
dispose: vi.fn(),
}
vi.clearAllMocks()
})
it("returns the visible provider if found", () => {
const mockProvider = {} as ClineProvider
;(ClineProvider.getVisibleInstance as Mock).mockReturnValue(mockProvider)
const result = getVisibleProviderOrLog(mockOutputChannel)
expect(result).toBe(mockProvider)
expect(mockOutputChannel.appendLine).not.toHaveBeenCalled()
})
it("logs and returns undefined if no provider found", () => {
;(ClineProvider.getVisibleInstance as Mock).mockReturnValue(undefined)
const result = getVisibleProviderOrLog(mockOutputChannel)
expect(result).toBeUndefined()
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith("Cannot find any visible Roo Code instances.")
})
})
describe("registerCommands handlers", () => {
let mockOutputChannel: vscode.OutputChannel
let mockContext: vscode.ExtensionContext
let mockVisibleProvider: { postMessageToWebview: Mock }
let mockProvider: { postMessageToWebview: Mock }
let handlers: Record<string, (...args: unknown[]) => unknown>
beforeEach(() => {
vi.clearAllMocks()
handlers = {}
mockOutputChannel = {
appendLine: vi.fn(),
append: vi.fn(),
clear: vi.fn(),
hide: vi.fn(),
name: "mock",
replace: vi.fn(),
show: vi.fn(),
dispose: vi.fn(),
}
mockContext = {
subscriptions: [],
} as unknown as vscode.ExtensionContext
mockVisibleProvider = {
postMessageToWebview: vi.fn().mockResolvedValue(undefined),
}
mockProvider = {
postMessageToWebview: vi.fn().mockResolvedValue(undefined),
}
;(ClineProvider.getVisibleInstance as Mock).mockReturnValue(mockVisibleProvider)
;(vscode.commands.registerCommand as Mock).mockImplementation(
(id: string, cb: (...args: unknown[]) => unknown) => {
handlers[id] = cb
return { dispose: vi.fn() }
},
)
registerCommands({
context: mockContext,
outputChannel: mockOutputChannel,
provider: mockProvider as unknown as ClineProvider,
})
})
afterEach(() => {
// Reset module-level panel state to prevent leakage between tests.
setPanel(undefined, "sidebar")
setPanel(undefined, "tab")
})
it("registers the ripgrep diagnostic command and stores its disposable in context.subscriptions", async () => {
const { registerRipgrepDiagnosticCommand } = await import("../../services/ripgrep/diagnostic")
const mock = vi.mocked(registerRipgrepDiagnosticCommand)
const disposable = mock.mock.results[0]?.value
expect(mock).toHaveBeenCalled()
expect(mockContext.subscriptions).toContain(disposable)
})
it("settingsButtonClicked posts both settingsButtonClicked and didBecomeVisible actions", () => {
handlers["zoo-code.settingsButtonClicked"]()
expect(mockVisibleProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "action",
action: "settingsButtonClicked",
})
expect(mockVisibleProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "action",
action: "didBecomeVisible",
})
expect(mockVisibleProvider.postMessageToWebview).toHaveBeenCalledTimes(2)
})
it("settingsButtonClicked is a no-op when no visible provider", () => {
;(ClineProvider.getVisibleInstance as Mock).mockReturnValue(undefined)
handlers["zoo-code.settingsButtonClicked"]()
expect(mockVisibleProvider.postMessageToWebview).not.toHaveBeenCalled()
})
it("historyButtonClicked posts historyButtonClicked action", () => {
handlers["zoo-code.historyButtonClicked"]()
expect(mockVisibleProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "action",
action: "historyButtonClicked",
})
})
it("marketplaceButtonClicked posts marketplaceButtonClicked action", () => {
handlers["zoo-code.marketplaceButtonClicked"]()
expect(mockVisibleProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "action",
action: "marketplaceButtonClicked",
})
})
it("acceptInput posts acceptInput message", () => {
handlers["zoo-code.acceptInput"]()
expect(mockVisibleProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "acceptInput",
})
})
it("toggleAutoApprove awaits postMessage with toggleAutoApprove action", async () => {
// Deferred-promise pattern: pin that the handler actually awaits
// postMessageToWebview rather than fire-and-forgetting it. If `await`
// were dropped in the handler, handlerPromise would resolve before
// resolvePost() is called and `settled` would flip true at the
// microtask flush below, failing the pending-state assertion.
let resolvePost!: () => void
const postPromise = new Promise<void>((resolve) => {
resolvePost = resolve
})
mockVisibleProvider.postMessageToWebview.mockReturnValueOnce(postPromise)
const handlerPromise = handlers["zoo-code.toggleAutoApprove"]() as Promise<unknown>
let settled = false
void handlerPromise.then(() => {
settled = true
})
await Promise.resolve()
expect(settled).toBe(false)
resolvePost()
await handlerPromise
expect(mockVisibleProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "action",
action: "toggleAutoApprove",
})
})
it("focusInput awaits postMessage on the registered provider when a sidebar panel is active", async () => {
const fakeSidebar = {} as vscode.WebviewView
setPanel(fakeSidebar, "sidebar")
// Same deferred-promise pattern as above. focusInput first awaits
// focusPanel() (mocked to resolve sync) and then awaits
// provider.postMessageToWebview — so we flush two microtasks before
// asserting the pending state, to let the handler advance past the
// focusPanel await and suspend on the deferred postPromise.
let resolvePost!: () => void
const postPromise = new Promise<void>((resolve) => {
resolvePost = resolve
})
mockProvider.postMessageToWebview.mockReturnValueOnce(postPromise)
const handlerPromise = handlers["zoo-code.focusInput"]() as Promise<unknown>
let settled = false
void handlerPromise.then(() => {
settled = true
})
await Promise.resolve()
await Promise.resolve()
expect(settled).toBe(false)
resolvePost()
await handlerPromise
expect(mockProvider.postMessageToWebview).toHaveBeenCalledWith({
type: "action",
action: "focusInput",
})
})
it("focusInput does not post when no sidebar panel is active", async () => {
await handlers["zoo-code.focusInput"]()
expect(mockProvider.postMessageToWebview).not.toHaveBeenCalled()
})
// Representative coverage for the .catch arm on all five void-prefixed
// postMessageToWebview sites in registerCommands.ts (settingsButtonClicked
// posts twice, plus historyButtonClicked, marketplaceButtonClicked, and
// acceptInput). Each handler is synchronous, so the .catch arm runs on a
// microtask; setImmediate ensures all microtasks are flushed before we assert. The
// log messages carry a `[<handlerName>]` prefix so multi-failure logs
// remain unambiguous; the prefix is per-handler, not per-call (both of
// settingsButtonClicked's posts share the same prefix).
it.each([
{ command: "zoo-code.settingsButtonClicked", prefix: "settingsButtonClicked", expectedCalls: 2 },
{ command: "zoo-code.historyButtonClicked", prefix: "historyButtonClicked", expectedCalls: 1 },
{ command: "zoo-code.marketplaceButtonClicked", prefix: "marketplaceButtonClicked", expectedCalls: 1 },
{ command: "zoo-code.acceptInput", prefix: "acceptInput", expectedCalls: 1 },
])(
"$command logs to outputChannel when postMessageToWebview rejects",
async ({ command, prefix, expectedCalls }) => {
const boom = new Error("boom")
mockVisibleProvider.postMessageToWebview.mockReset()
mockVisibleProvider.postMessageToWebview.mockRejectedValue(boom)
handlers[command]()
// Flush microtasks so the chained .catch arm runs.
await new Promise((resolve) => setImmediate(resolve))
expect(mockOutputChannel.appendLine).toHaveBeenCalledTimes(expectedCalls)
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
`[${prefix}] postMessageToWebview failed: ${boom}`,
)
},
)
it("toggleAutoApprove logs to outputChannel when postMessageToWebview rejects", async () => {
// toggleAutoApprove is `async` and awaits postMessageToWebview inside a
// try/catch (rather than relying on a `.catch` microtask like the
// void-prefixed sites), so awaiting the handler itself is sufficient to
// observe the appendLine call.
const boom = new Error("boom")
mockVisibleProvider.postMessageToWebview.mockReset()
mockVisibleProvider.postMessageToWebview.mockRejectedValue(boom)
await handlers["zoo-code.toggleAutoApprove"]()
expect(mockOutputChannel.appendLine).toHaveBeenCalledTimes(1)
expect(mockOutputChannel.appendLine).toHaveBeenCalledWith(
`[toggleAutoApprove] postMessageToWebview failed: ${boom}`,
)
})
})