Skip to content
5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -316,7 +316,6 @@
"eventsource": "^3.0.6",
"find-process": "https://github.com/coder/find-process#fix/sequoia-compat",
"jsonc-parser": "^3.3.1",
"memfs": "^4.17.1",
"node-forge": "^1.3.1",
"openpgp": "^6.2.0",
"pretty-bytes": "^7.0.0",
Expand All @@ -336,6 +335,7 @@
"@types/ws": "^8.18.1",
"@typescript-eslint/eslint-plugin": "^7.0.0",
"@typescript-eslint/parser": "^6.21.0",
"@vitest/coverage-v8": "^3.2.4",
"@vscode/test-cli": "^0.0.11",
"@vscode/test-electron": "^2.5.2",
"@vscode/vsce": "^3.6.0",
Expand All @@ -350,12 +350,13 @@
"eslint-plugin-prettier": "^5.4.1",
"glob": "^10.4.2",
"jsonc-eslint-parser": "^2.4.0",
"memfs": "^4.46.0",
"nyc": "^17.1.0",
"prettier": "^3.5.3",
"ts-loader": "^9.5.1",
"typescript": "^5.8.3",
"utf-8-validate": "^6.0.5",
"vitest": "^0.34.6",
"vitest": "^3.2.4",
"vscode-test": "^1.5.0",
"webpack": "^5.99.6",
"webpack-cli": "^5.1.4"
Expand Down
274 changes: 274 additions & 0 deletions src/__mocks__/testHelpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,274 @@
import { vi } from "vitest";
import * as vscode from "vscode";

/**
* Mock configuration provider that integrates with the vscode workspace configuration mock.
* Use this to set configuration values that will be returned by vscode.workspace.getConfiguration().
*/
export class MockConfigurationProvider {
private config = new Map<string, unknown>();

constructor() {
this.setupVSCodeMock();
}

/**
* Set a configuration value that will be returned by vscode.workspace.getConfiguration().get()
*/
set(key: string, value: unknown): void {
this.config.set(key, value);
}

/**
* Get a configuration value (for testing purposes)
*/
get<T>(key: string): T | undefined;
get<T>(key: string, defaultValue: T): T;
get<T>(key: string, defaultValue?: T): T | undefined {
const value = this.config.get(key);
return value !== undefined ? (value as T) : defaultValue;
}

/**
* Clear all configuration values
*/
clear(): void {
this.config.clear();
}

/**
* Setup the vscode.workspace.getConfiguration mock to return our values
*/
private setupVSCodeMock(): void {
vi.mocked(vscode.workspace.getConfiguration).mockImplementation(
(section?: string) => {
// Create a snapshot of the current config when getConfiguration is called
const snapshot = new Map(this.config);
const getFullKey = (part: string) =>
section ? `${section}.${part}` : part;

return {
get: vi.fn((key: string, defaultValue?: unknown) => {
const value = snapshot.get(getFullKey(key));
return value !== undefined ? value : defaultValue;
}),
has: vi.fn((key: string) => {
return snapshot.has(getFullKey(key));
}),
inspect: vi.fn(),
update: vi.fn((key: string, value: unknown) => {
this.config.set(getFullKey(key), value);
return Promise.resolve();
}),
};
},
);
}
}

/**
* Mock progress reporter that integrates with vscode.window.withProgress.
* Use this to control progress reporting behavior and cancellation in tests.
*/
export class MockProgressReporter {
private shouldCancel = false;
private progressReports: Array<{ message?: string; increment?: number }> = [];

constructor() {
this.setupVSCodeMock();
}

/**
* Set whether the progress should be cancelled
*/
setCancellation(cancel: boolean): void {
this.shouldCancel = cancel;
}

/**
* Get all progress reports that were made
*/
getProgressReports(): Array<{ message?: string; increment?: number }> {
return [...this.progressReports];
}

/**
* Clear all progress reports
*/
clearProgressReports(): void {
this.progressReports = [];
}

/**
* Setup the vscode.window.withProgress mock
*/
private setupVSCodeMock(): void {
vi.mocked(vscode.window.withProgress).mockImplementation(
async <T>(
_options: vscode.ProgressOptions,
task: (
progress: vscode.Progress<{ message?: string; increment?: number }>,
token: vscode.CancellationToken,
) => Thenable<T>,
): Promise<T> => {
const progress = {
report: vi.fn((value: { message?: string; increment?: number }) => {
this.progressReports.push(value);
}),
};

const cancellationToken: vscode.CancellationToken = {
isCancellationRequested: this.shouldCancel,
onCancellationRequested: vi.fn((listener: (x: unknown) => void) => {
if (this.shouldCancel) {
setTimeout(listener, 0);
}
return { dispose: vi.fn() };
}),
};

return task(progress, cancellationToken);
},
);
}
}

/**
* Mock user interaction that integrates with vscode.window message dialogs.
* Use this to control user responses in tests.
*/
export class MockUserInteraction {
private responses = new Map<string, string | undefined>();
private externalUrls: string[] = [];

constructor() {
this.setupVSCodeMock();
}

/**
* Set a response for a specific message
*/
setResponse(message: string, response: string | undefined): void {
this.responses.set(message, response);
}

/**
* Get all URLs that were opened externally
*/
getExternalUrls(): string[] {
return [...this.externalUrls];
}

/**
* Clear all external URLs
*/
clearExternalUrls(): void {
this.externalUrls = [];
}

/**
* Clear all responses
*/
clearResponses(): void {
this.responses.clear();
}

/**
* Setup the vscode.window message dialog mocks
*/
private setupVSCodeMock(): void {
const getResponse = (message: string): string | undefined => {
return this.responses.get(message);
};

vi.mocked(vscode.window.showErrorMessage).mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(message: string): Thenable<any> => {
const response = getResponse(message);
return Promise.resolve(response);
},
);

vi.mocked(vscode.window.showWarningMessage).mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(message: string): Thenable<any> => {
const response = getResponse(message);
return Promise.resolve(response);
},
);

vi.mocked(vscode.window.showInformationMessage).mockImplementation(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
(message: string): Thenable<any> => {
const response = getResponse(message);
return Promise.resolve(response);
},
);

vi.mocked(vscode.env.openExternal).mockImplementation(
(target: vscode.Uri): Promise<boolean> => {
this.externalUrls.push(target.toString());
return Promise.resolve(true);
},
);
}
}

// Simple in-memory implementation of Memento
export class InMemoryMemento implements vscode.Memento {
private storage = new Map<string, unknown>();

get<T>(key: string): T | undefined;
get<T>(key: string, defaultValue: T): T;
get<T>(key: string, defaultValue?: T): T | undefined {
return this.storage.has(key) ? (this.storage.get(key) as T) : defaultValue;
}

async update(key: string, value: unknown): Promise<void> {
if (value === undefined) {
this.storage.delete(key);
} else {
this.storage.set(key, value);
}
return Promise.resolve();
}

keys(): readonly string[] {
return Array.from(this.storage.keys());
}
}

// Simple in-memory implementation of SecretStorage
export class InMemorySecretStorage implements vscode.SecretStorage {
private secrets = new Map<string, string>();
private isCorrupted = false;

onDidChange: vscode.Event<vscode.SecretStorageChangeEvent> = () => ({
dispose: () => {},
});

async get(key: string): Promise<string | undefined> {
if (this.isCorrupted) {
return Promise.reject(new Error("Storage corrupted"));
}
return this.secrets.get(key);
}

async store(key: string, value: string): Promise<void> {
if (this.isCorrupted) {
return Promise.reject(new Error("Storage corrupted"));
}
this.secrets.set(key, value);
}

async delete(key: string): Promise<void> {
if (this.isCorrupted) {
return Promise.reject(new Error("Storage corrupted"));
}
this.secrets.delete(key);
}

corruptStorage(): void {
this.isCorrupted = true;
}
}
Loading