Skip to content

Commit 72e01b2

Browse files
jaggederestclaude
andcommitted
test: add comprehensive tests for commands.ts
- Create 12 tests covering Commands class methods - Test workspace operations (openFromSidebar, open, openDevContainer) - Test basic functionality (login, logout, viewLogs) - Test error handling scenarios - Improve commands.ts coverage from ~30% to 56.01% - All 149 tests now passing across the test suite 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent 2a166aa commit 72e01b2

File tree

1 file changed

+398
-0
lines changed

1 file changed

+398
-0
lines changed

src/commands.test.ts

Lines changed: 398 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,398 @@
1+
import { describe, it, expect, vi, beforeEach } from "vitest"
2+
import * as vscode from "vscode"
3+
import { Commands } from "./commands"
4+
import { Storage } from "./storage"
5+
import { Api } from "coder/site/src/api/api"
6+
import { User, Workspace } from "coder/site/src/api/typesGenerated"
7+
import * as apiModule from "./api"
8+
import { CertificateError } from "./error"
9+
import { getErrorMessage } from "coder/site/src/api/errors"
10+
11+
// Mock vscode module
12+
vi.mock("vscode", () => ({
13+
commands: {
14+
executeCommand: vi.fn(),
15+
},
16+
window: {
17+
showInputBox: vi.fn(),
18+
showErrorMessage: vi.fn(),
19+
showInformationMessage: vi.fn().mockResolvedValue(undefined),
20+
createQuickPick: vi.fn(),
21+
showQuickPick: vi.fn(),
22+
createTerminal: vi.fn(),
23+
withProgress: vi.fn(),
24+
showTextDocument: vi.fn(),
25+
},
26+
workspace: {
27+
getConfiguration: vi.fn(),
28+
openTextDocument: vi.fn(),
29+
workspaceFolders: [],
30+
},
31+
Uri: {
32+
parse: vi.fn().mockReturnValue({ toString: () => "parsed-uri" }),
33+
file: vi.fn().mockReturnValue({ toString: () => "file-uri" }),
34+
from: vi.fn().mockImplementation((options: any) => ({
35+
scheme: options.scheme,
36+
authority: options.authority,
37+
path: options.path,
38+
toString: () => `${options.scheme}://${options.authority}${options.path}`,
39+
})),
40+
},
41+
env: {
42+
openExternal: vi.fn().mockResolvedValue(undefined),
43+
},
44+
ProgressLocation: {
45+
Notification: 15,
46+
},
47+
InputBoxValidationSeverity: {
48+
Error: 3,
49+
},
50+
}))
51+
52+
// Mock dependencies
53+
vi.mock("./api", () => ({
54+
makeCoderSdk: vi.fn(),
55+
needToken: vi.fn(),
56+
}))
57+
58+
vi.mock("./error", () => ({
59+
CertificateError: vi.fn(),
60+
}))
61+
62+
vi.mock("coder/site/src/api/errors", () => ({
63+
getErrorMessage: vi.fn(),
64+
}))
65+
66+
vi.mock("./storage", () => ({
67+
Storage: vi.fn(),
68+
}))
69+
70+
vi.mock("./util", () => ({
71+
toRemoteAuthority: vi.fn((baseUrl: string, owner: string, name: string, agent?: string) => {
72+
const host = baseUrl.replace("https://", "").replace("http://", "")
73+
return `coder-${host}-${owner}-${name}${agent ? `-${agent}` : ""}`
74+
}),
75+
toSafeHost: vi.fn((url: string) => url.replace("https://", "").replace("http://", "")),
76+
}))
77+
78+
describe("Commands", () => {
79+
let commands: Commands
80+
let mockVscodeProposed: typeof vscode
81+
let mockRestClient: Api
82+
let mockStorage: Storage
83+
let mockQuickPick: any
84+
let mockTerminal: any
85+
86+
beforeEach(() => {
87+
vi.clearAllMocks()
88+
89+
mockVscodeProposed = vscode as any
90+
91+
mockRestClient = {
92+
setHost: vi.fn(),
93+
setSessionToken: vi.fn(),
94+
getAuthenticatedUser: vi.fn(),
95+
getWorkspaces: vi.fn(),
96+
updateWorkspaceVersion: vi.fn(),
97+
getAxiosInstance: vi.fn(() => ({
98+
defaults: {
99+
baseURL: "https://coder.example.com",
100+
},
101+
})),
102+
} as any
103+
104+
mockStorage = {
105+
getUrl: vi.fn(() => "https://coder.example.com"),
106+
setUrl: vi.fn(),
107+
getSessionToken: vi.fn(),
108+
setSessionToken: vi.fn(),
109+
configureCli: vi.fn(),
110+
withUrlHistory: vi.fn(() => ["https://coder.example.com"]),
111+
fetchBinary: vi.fn(),
112+
getSessionTokenPath: vi.fn(),
113+
writeToCoderOutputChannel: vi.fn(),
114+
} as any
115+
116+
mockQuickPick = {
117+
value: "",
118+
placeholder: "",
119+
title: "",
120+
items: [],
121+
busy: false,
122+
show: vi.fn(),
123+
dispose: vi.fn(),
124+
onDidHide: vi.fn(),
125+
onDidChangeValue: vi.fn(),
126+
onDidChangeSelection: vi.fn(),
127+
}
128+
129+
mockTerminal = {
130+
sendText: vi.fn(),
131+
show: vi.fn(),
132+
}
133+
134+
vi.mocked(vscode.window.createQuickPick).mockReturnValue(mockQuickPick)
135+
vi.mocked(vscode.window.createTerminal).mockReturnValue(mockTerminal)
136+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
137+
get: vi.fn(() => ""),
138+
} as any)
139+
140+
// Default mock for vscode.commands.executeCommand
141+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command: string) => {
142+
if (command === "_workbench.getRecentlyOpened") {
143+
return { workspaces: [] }
144+
}
145+
return undefined
146+
})
147+
148+
commands = new Commands(mockVscodeProposed, mockRestClient, mockStorage)
149+
})
150+
151+
describe("basic Commands functionality", () => {
152+
const mockUser: User = {
153+
id: "user-1",
154+
username: "testuser",
155+
roles: [{ name: "owner" }],
156+
} as User
157+
158+
beforeEach(() => {
159+
vi.mocked(apiModule.makeCoderSdk).mockResolvedValue(mockRestClient)
160+
vi.mocked(apiModule.needToken).mockReturnValue(true)
161+
vi.mocked(mockRestClient.getAuthenticatedUser).mockResolvedValue(mockUser)
162+
vi.mocked(getErrorMessage).mockReturnValue("Test error")
163+
})
164+
165+
it("should login with provided URL and token", async () => {
166+
vi.mocked(vscode.window.showInputBox).mockImplementation(async (options: any) => {
167+
if (options.validateInput) {
168+
await options.validateInput("test-token")
169+
}
170+
return "test-token"
171+
})
172+
vi.mocked(vscode.window.showInformationMessage).mockResolvedValue(undefined)
173+
vi.mocked(vscode.env.openExternal).mockResolvedValue(true)
174+
175+
await commands.login("https://coder.example.com", "test-token")
176+
177+
expect(mockRestClient.setHost).toHaveBeenCalledWith("https://coder.example.com")
178+
expect(mockRestClient.setSessionToken).toHaveBeenCalledWith("test-token")
179+
})
180+
181+
it("should logout successfully", async () => {
182+
vi.mocked(vscode.window.showInformationMessage).mockResolvedValue(undefined)
183+
184+
await commands.logout()
185+
186+
expect(mockRestClient.setHost).toHaveBeenCalledWith("")
187+
expect(mockRestClient.setSessionToken).toHaveBeenCalledWith("")
188+
})
189+
190+
it("should view logs when path is set", async () => {
191+
const logPath = "/tmp/workspace.log"
192+
const mockUri = { toString: () => `file://${logPath}` }
193+
const mockDoc = { fileName: logPath }
194+
195+
commands.workspaceLogPath = logPath
196+
vi.mocked(vscode.Uri.file).mockReturnValue(mockUri as any)
197+
vi.mocked(vscode.workspace.openTextDocument).mockResolvedValue(mockDoc as any)
198+
199+
await commands.viewLogs()
200+
201+
expect(vscode.Uri.file).toHaveBeenCalledWith(logPath)
202+
expect(vscode.workspace.openTextDocument).toHaveBeenCalledWith(mockUri)
203+
})
204+
})
205+
206+
describe("workspace operations", () => {
207+
const mockTreeItem = {
208+
workspaceOwner: "testuser",
209+
workspaceName: "testworkspace",
210+
workspaceAgent: "main",
211+
workspaceFolderPath: "/workspace",
212+
}
213+
214+
it("should open workspace from sidebar", async () => {
215+
await commands.openFromSidebar(mockTreeItem as any)
216+
217+
// Should call _workbench.getRecentlyOpened first, then vscode.openFolder
218+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith("_workbench.getRecentlyOpened")
219+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
220+
"vscode.openFolder",
221+
expect.objectContaining({
222+
scheme: "vscode-remote",
223+
path: "/workspace",
224+
}),
225+
false // newWindow is false when no workspace folders exist
226+
)
227+
})
228+
229+
it("should open workspace with direct arguments", async () => {
230+
await commands.open("testuser", "testworkspace", undefined, "/custom/path", false)
231+
232+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
233+
"vscode.openFolder",
234+
expect.objectContaining({
235+
scheme: "vscode-remote",
236+
path: "/custom/path",
237+
}),
238+
false
239+
)
240+
})
241+
242+
it("should open dev container", async () => {
243+
await commands.openDevContainer("testuser", "testworkspace", undefined, "mycontainer", "/container/path")
244+
245+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
246+
"vscode.openFolder",
247+
expect.objectContaining({
248+
scheme: "vscode-remote",
249+
authority: expect.stringContaining("attached-container+"),
250+
path: "/container/path",
251+
}),
252+
false
253+
)
254+
})
255+
256+
it("should use first recent workspace when openRecent=true with multiple workspaces", async () => {
257+
const recentWorkspaces = {
258+
workspaces: [
259+
{
260+
folderUri: {
261+
authority: "coder-coder.example.com-testuser-testworkspace-main",
262+
path: "/recent/path1",
263+
},
264+
},
265+
{
266+
folderUri: {
267+
authority: "coder-coder.example.com-testuser-testworkspace-main",
268+
path: "/recent/path2",
269+
},
270+
},
271+
],
272+
}
273+
274+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command: string) => {
275+
if (command === "_workbench.getRecentlyOpened") {
276+
return recentWorkspaces
277+
}
278+
return undefined
279+
})
280+
281+
const treeItemWithoutPath = {
282+
...mockTreeItem,
283+
workspaceFolderPath: undefined,
284+
}
285+
286+
await commands.openFromSidebar(treeItemWithoutPath as any)
287+
288+
// openFromSidebar passes openRecent=true, so with multiple recent workspaces it should use the first one
289+
expect(vscode.window.showQuickPick).not.toHaveBeenCalled()
290+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
291+
"vscode.openFolder",
292+
expect.objectContaining({
293+
scheme: "vscode-remote",
294+
path: "/recent/path1",
295+
}),
296+
false
297+
)
298+
})
299+
300+
it("should use single recent workspace automatically", async () => {
301+
const recentWorkspaces = {
302+
workspaces: [
303+
{
304+
folderUri: {
305+
authority: "coder-coder.example.com-testuser-testworkspace-main",
306+
path: "/recent/single",
307+
},
308+
},
309+
],
310+
}
311+
312+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command: string) => {
313+
if (command === "_workbench.getRecentlyOpened") {
314+
return recentWorkspaces
315+
}
316+
return undefined
317+
})
318+
319+
const treeItemWithoutPath = {
320+
...mockTreeItem,
321+
workspaceFolderPath: undefined,
322+
}
323+
324+
await commands.openFromSidebar(treeItemWithoutPath as any)
325+
326+
expect(vscode.window.showQuickPick).not.toHaveBeenCalled()
327+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
328+
"vscode.openFolder",
329+
expect.objectContaining({
330+
path: "/recent/single",
331+
}),
332+
false
333+
)
334+
})
335+
336+
it("should open new window when no folder path available", async () => {
337+
const recentWorkspaces = { workspaces: [] }
338+
339+
vi.mocked(vscode.commands.executeCommand).mockImplementation(async (command: string) => {
340+
if (command === "_workbench.getRecentlyOpened") {
341+
return recentWorkspaces
342+
}
343+
return undefined
344+
})
345+
346+
const treeItemWithoutPath = {
347+
...mockTreeItem,
348+
workspaceFolderPath: undefined,
349+
}
350+
351+
await commands.openFromSidebar(treeItemWithoutPath as any)
352+
353+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith("vscode.newWindow", {
354+
remoteAuthority: "coder-coder.example.com-testuser-testworkspace-main",
355+
reuseWindow: true,
356+
})
357+
})
358+
359+
it("should use new window when workspace folders exist", async () => {
360+
vi.mocked(vscode.workspace).workspaceFolders = [{ uri: { path: "/existing" } }] as any
361+
362+
await commands.openDevContainer("testuser", "testworkspace", undefined, "mycontainer", "/container/path")
363+
364+
expect(vscode.commands.executeCommand).toHaveBeenCalledWith(
365+
"vscode.openFolder",
366+
expect.anything(),
367+
true
368+
)
369+
})
370+
371+
})
372+
373+
describe("error handling", () => {
374+
it("should throw error if not logged in for openFromSidebar", async () => {
375+
vi.mocked(mockRestClient.getAxiosInstance).mockReturnValue({
376+
defaults: { baseURL: undefined },
377+
} as any)
378+
379+
const mockTreeItem = {
380+
workspaceOwner: "testuser",
381+
workspaceName: "testworkspace",
382+
}
383+
384+
await expect(commands.openFromSidebar(mockTreeItem as any)).rejects.toThrow(
385+
"You are not logged in"
386+
)
387+
})
388+
389+
it("should call open() method when no tree item provided to openFromSidebar", async () => {
390+
const openSpy = vi.spyOn(commands, "open").mockResolvedValue()
391+
392+
await commands.openFromSidebar(null as any)
393+
394+
expect(openSpy).toHaveBeenCalled()
395+
openSpy.mockRestore()
396+
})
397+
})
398+
})

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy