Skip to content

Commit 4bd91ec

Browse files
committed
Mock VS Code fully instead of DI
1 parent fd84d22 commit 4bd91ec

File tree

9 files changed

+838
-583
lines changed

9 files changed

+838
-583
lines changed

src/__mocks__/testHelpers.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { vi } from "vitest";
2+
import * as vscode from "vscode";
3+
4+
/**
5+
* Mock configuration provider that integrates with the vscode workspace configuration mock.
6+
* Use this to set configuration values that will be returned by vscode.workspace.getConfiguration().
7+
*/
8+
export class MockConfigurationProvider {
9+
private config = new Map<string, unknown>();
10+
11+
/**
12+
* Set a configuration value that will be returned by vscode.workspace.getConfiguration().get()
13+
*/
14+
set(key: string, value: unknown): void {
15+
this.config.set(key, value);
16+
this.setupVSCodeMock();
17+
}
18+
19+
/**
20+
* Get a configuration value (for testing purposes)
21+
*/
22+
get<T>(key: string): T | undefined;
23+
get<T>(key: string, defaultValue: T): T;
24+
get<T>(key: string, defaultValue?: T): T | undefined {
25+
const value = this.config.get(key);
26+
return value !== undefined ? (value as T) : defaultValue;
27+
}
28+
29+
/**
30+
* Clear all configuration values
31+
*/
32+
clear(): void {
33+
this.config.clear();
34+
this.setupVSCodeMock();
35+
}
36+
37+
/**
38+
* Setup the vscode.workspace.getConfiguration mock to return our values
39+
*/
40+
setupVSCodeMock(): void {
41+
vi.mocked(vscode.workspace.getConfiguration).mockReturnValue({
42+
get: vi.fn((key: string, defaultValue?: unknown) => {
43+
const value = this.config.get(key);
44+
return value !== undefined ? value : defaultValue;
45+
}),
46+
has: vi.fn((key: string) => this.config.has(key)),
47+
inspect: vi.fn(),
48+
update: vi.fn(),
49+
} as unknown as vscode.WorkspaceConfiguration);
50+
}
51+
}
52+
53+
/**
54+
* Mock progress reporter that integrates with vscode.window.withProgress.
55+
* Use this to control progress reporting behavior and cancellation in tests.
56+
*/
57+
export class MockProgressReporter {
58+
private shouldCancel = false;
59+
private progressReports: Array<{ message?: string; increment?: number }> = [];
60+
61+
/**
62+
* Set whether the progress should be cancelled
63+
*/
64+
setCancellation(cancel: boolean): void {
65+
this.shouldCancel = cancel;
66+
}
67+
68+
/**
69+
* Get all progress reports that were made
70+
*/
71+
getProgressReports(): Array<{ message?: string; increment?: number }> {
72+
return [...this.progressReports];
73+
}
74+
75+
/**
76+
* Clear all progress reports
77+
*/
78+
clearProgressReports(): void {
79+
this.progressReports = [];
80+
}
81+
82+
/**
83+
* Setup the vscode.window.withProgress mock
84+
*/
85+
setupVSCodeMock(): void {
86+
vi.mocked(vscode.window.withProgress).mockImplementation(
87+
async <T>(
88+
_options: vscode.ProgressOptions,
89+
task: (
90+
progress: vscode.Progress<{ message?: string; increment?: number }>,
91+
token: vscode.CancellationToken,
92+
) => Thenable<T>,
93+
): Promise<T> => {
94+
const progress = {
95+
report: vi.fn((value: { message?: string; increment?: number }) => {
96+
this.progressReports.push(value);
97+
}),
98+
};
99+
100+
const cancellationToken: vscode.CancellationToken = {
101+
isCancellationRequested: this.shouldCancel,
102+
onCancellationRequested: vi.fn((listener: (x: unknown) => void) => {
103+
if (this.shouldCancel) {
104+
setTimeout(listener, 0);
105+
}
106+
return { dispose: vi.fn() };
107+
}),
108+
};
109+
110+
return task(progress, cancellationToken);
111+
},
112+
);
113+
}
114+
}
115+
116+
/**
117+
* Mock user interaction that integrates with vscode.window message dialogs.
118+
* Use this to control user responses in tests.
119+
*/
120+
export class MockUserInteraction {
121+
private responses = new Map<string, string | undefined>();
122+
private externalUrls: string[] = [];
123+
124+
/**
125+
* Set a response for a specific message or set a default response
126+
*/
127+
setResponse(response: string | undefined): void;
128+
setResponse(message: string, response: string | undefined): void;
129+
setResponse(
130+
messageOrResponse: string | undefined,
131+
response?: string | undefined,
132+
): void {
133+
if (response === undefined && messageOrResponse !== undefined) {
134+
// Single argument - set default response
135+
this.responses.set("default", messageOrResponse);
136+
} else if (messageOrResponse !== undefined) {
137+
// Two arguments - set specific response
138+
this.responses.set(messageOrResponse, response);
139+
}
140+
}
141+
142+
/**
143+
* Get all URLs that were opened externally
144+
*/
145+
getExternalUrls(): string[] {
146+
return [...this.externalUrls];
147+
}
148+
149+
/**
150+
* Clear all external URLs
151+
*/
152+
clearExternalUrls(): void {
153+
this.externalUrls = [];
154+
}
155+
156+
/**
157+
* Clear all responses
158+
*/
159+
clearResponses(): void {
160+
this.responses.clear();
161+
}
162+
163+
/**
164+
* Setup the vscode.window message dialog mocks
165+
*/
166+
setupVSCodeMock(): void {
167+
const getResponse = (message: string): string | undefined => {
168+
return this.responses.get(message) ?? this.responses.get("default");
169+
};
170+
171+
vi.mocked(vscode.window.showErrorMessage).mockImplementation(
172+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
173+
(message: string): Thenable<any> => {
174+
const response = getResponse(message);
175+
return Promise.resolve(response);
176+
},
177+
);
178+
179+
vi.mocked(vscode.window.showWarningMessage).mockImplementation(
180+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
181+
(message: string): Thenable<any> => {
182+
const response = getResponse(message);
183+
return Promise.resolve(response);
184+
},
185+
);
186+
187+
vi.mocked(vscode.window.showInformationMessage).mockImplementation(
188+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
189+
(message: string): Thenable<any> => {
190+
const response = getResponse(message);
191+
return Promise.resolve(response);
192+
},
193+
);
194+
195+
vi.mocked(vscode.env.openExternal).mockImplementation(
196+
(target: vscode.Uri): Promise<boolean> => {
197+
this.externalUrls.push(target.toString());
198+
return Promise.resolve(true);
199+
},
200+
);
201+
}
202+
}
203+
204+
/**
205+
* Helper function to setup all VS Code mocks for testing.
206+
* Call this in your test setup to initialize all the mock integrations.
207+
*/
208+
export function setupVSCodeMocks(): {
209+
mockConfig: MockConfigurationProvider;
210+
mockProgress: MockProgressReporter;
211+
mockUI: MockUserInteraction;
212+
} {
213+
const mockConfig = new MockConfigurationProvider();
214+
const mockProgress = new MockProgressReporter();
215+
const mockUI = new MockUserInteraction();
216+
217+
// Setup all the VS Code API mocks
218+
mockConfig.setupVSCodeMock();
219+
mockProgress.setupVSCodeMock();
220+
mockUI.setupVSCodeMock();
221+
222+
return { mockConfig, mockProgress, mockUI };
223+
}

src/__mocks__/vscode.runtime.ts

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
import { vi } from "vitest";
2+
3+
// enum-like helpers
4+
const E = <T extends Record<string, number>>(o: T) => Object.freeze(o);
5+
6+
export const ProgressLocation = E({
7+
SourceControl: 1,
8+
Window: 10,
9+
Notification: 15,
10+
});
11+
export const ViewColumn = E({
12+
Active: -1,
13+
Beside: -2,
14+
One: 1,
15+
Two: 2,
16+
Three: 3,
17+
});
18+
export const ConfigurationTarget = E({
19+
Global: 1,
20+
Workspace: 2,
21+
WorkspaceFolder: 3,
22+
});
23+
export const TreeItemCollapsibleState = E({
24+
None: 0,
25+
Collapsed: 1,
26+
Expanded: 2,
27+
});
28+
export const StatusBarAlignment = E({ Left: 1, Right: 2 });
29+
export const ExtensionMode = E({ Production: 1, Development: 2, Test: 3 });
30+
export const UIKind = E({ Desktop: 1, Web: 2 });
31+
32+
export class Uri {
33+
constructor(
34+
public scheme: string,
35+
public path: string,
36+
) {}
37+
static file(p: string) {
38+
return new Uri("file", p);
39+
}
40+
static parse(v: string) {
41+
if (v.startsWith("file://")) {
42+
return Uri.file(v.slice("file://".length));
43+
}
44+
const [scheme, ...rest] = v.split(":");
45+
return new Uri(scheme, rest.join(":"));
46+
}
47+
toString() {
48+
return this.scheme === "file"
49+
? `file://${this.path}`
50+
: `${this.scheme}:${this.path}`;
51+
}
52+
static joinPath(base: Uri, ...paths: string[]) {
53+
const sep = base.path.endsWith("/") ? "" : "/";
54+
return new Uri(base.scheme, base.path + sep + paths.join("/"));
55+
}
56+
}
57+
58+
// mini event
59+
const makeEvent = <T>() => {
60+
const listeners = new Set<(e: T) => void>();
61+
const event = (listener: (e: T) => void) => {
62+
listeners.add(listener);
63+
return { dispose: () => listeners.delete(listener) };
64+
};
65+
return { event, fire: (e: T) => listeners.forEach((l) => l(e)) };
66+
};
67+
68+
const onDidChangeConfiguration = makeEvent<unknown>();
69+
const onDidChangeWorkspaceFolders = makeEvent<unknown>();
70+
71+
export const window = {
72+
showInformationMessage: vi.fn(),
73+
showWarningMessage: vi.fn(),
74+
showErrorMessage: vi.fn(),
75+
showQuickPick: vi.fn(),
76+
showInputBox: vi.fn(),
77+
withProgress: vi.fn(),
78+
createOutputChannel: vi.fn(() => ({
79+
appendLine: vi.fn(),
80+
append: vi.fn(),
81+
show: vi.fn(),
82+
hide: vi.fn(),
83+
dispose: vi.fn(),
84+
clear: vi.fn(),
85+
})),
86+
};
87+
88+
export const commands = {
89+
registerCommand: vi.fn(),
90+
executeCommand: vi.fn(),
91+
};
92+
93+
export const workspace = {
94+
getConfiguration: vi.fn(), // your helpers override this
95+
workspaceFolders: [] as unknown[],
96+
fs: {
97+
readFile: vi.fn(),
98+
writeFile: vi.fn(),
99+
stat: vi.fn(),
100+
readDirectory: vi.fn(),
101+
},
102+
onDidChangeConfiguration: onDidChangeConfiguration.event,
103+
onDidChangeWorkspaceFolders: onDidChangeWorkspaceFolders.event,
104+
105+
// test-only triggers:
106+
__fireDidChangeConfiguration: onDidChangeConfiguration.fire,
107+
__fireDidChangeWorkspaceFolders: onDidChangeWorkspaceFolders.fire,
108+
};
109+
110+
export const env = {
111+
appName: "Visual Studio Code",
112+
appRoot: "/app",
113+
language: "en",
114+
machineId: "test-machine-id",
115+
sessionId: "test-session-id",
116+
remoteName: undefined as string | undefined,
117+
shell: "/bin/bash",
118+
openExternal: vi.fn(),
119+
};
120+
121+
export const extensions = {
122+
getExtension: vi.fn(),
123+
all: [] as unknown[],
124+
};
125+
126+
const vscode = {
127+
ProgressLocation,
128+
ViewColumn,
129+
ConfigurationTarget,
130+
TreeItemCollapsibleState,
131+
StatusBarAlignment,
132+
ExtensionMode,
133+
UIKind,
134+
Uri,
135+
window,
136+
commands,
137+
workspace,
138+
env,
139+
extensions,
140+
};
141+
142+
export default vscode;

0 commit comments

Comments
 (0)