Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions test/mocks/coderApi.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { onTestFinished } from "vitest";

import { CoderApi } from "@/api/coderApi";

import { createMockLogger } from "./testHelpers";

/** Real `CoderApi` with `overrides` applied; unstubbed methods hit `baseUrl`. */
export function createTestCoderApi<T extends Partial<CoderApi>>(
options: { baseUrl?: string; token?: string; overrides?: T } = {},
): CoderApi & T {
const client = CoderApi.create(
options.baseUrl ?? "",
options.token,
createMockLogger(),
);
onTestFinished(() => client.dispose());
return Object.assign(client, options.overrides);
}
37 changes: 37 additions & 0 deletions test/mocks/commands.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { vi } from "vitest";

import { Commands } from "@/commands";

import { createTestCoderApi } from "./coderApi";
import { createTestTelemetryService } from "./telemetry";
import { createMockLogger } from "./testHelpers";

import type { CoderApi } from "@/api/coderApi";
import type { ServiceContainer } from "@/core/container";
import type { DeploymentManager } from "@/deployment/deploymentManager";

/** Build `Commands`; services left unnamed stand in as empty objects. */
export function createTestCommands(
options: {
services?: Record<string, unknown>;
baseUrl?: string;
client?: Partial<CoderApi>;
} = {},
): Commands {
const services: Record<string, unknown> = {
getTelemetryService: createTestTelemetryService(),
getLogger: createMockLogger(),
getMementoManager: { setStartupMode: vi.fn() },
getDuplicateWorkspaceIpc: {
sendPing: vi.fn().mockResolvedValue(undefined),
},
...options.services,
};
return new Commands(
new Proxy({} as ServiceContainer, {
get: (_, name: string) => () => services[name] ?? {},
}),
createTestCoderApi({ baseUrl: options.baseUrl, overrides: options.client }),
{} as DeploymentManager,
);
}
31 changes: 0 additions & 31 deletions test/mocks/testHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ import * as fs from "node:fs/promises";
import { onTestFinished, vi } from "vitest";
import * as vscode from "vscode";

import { Commands } from "@/commands";
import {
SessionStore,
type SessionData,
Expand Down Expand Up @@ -44,7 +43,6 @@ import type { ContextManager } from "@/core/contextManager";
import type { MementoManager } from "@/core/mementoManager";
import type { PathResolver } from "@/core/pathResolver";
import type { SecretsManager } from "@/core/secretsManager";
import type { DeploymentManager } from "@/deployment/deploymentManager";
import type { Deployment } from "@/deployment/types";
import type { ConnectionLogBuffer } from "@/logging/logBuffer";
import type { Logger } from "@/logging/logger";
Expand Down Expand Up @@ -664,35 +662,6 @@ export function createMockServiceContainer(
} as ServiceContainer;
}

/** Build `Commands`; services left unnamed stand in as empty objects. */
export function createTestCommands(
options: {
services?: Record<string, unknown>;
baseUrl?: string;
client?: Partial<CoderApi>;
} = {},
): Commands {
const services: Record<string, unknown> = {
getTelemetryService: createTestTelemetryService(),
getLogger: createMockLogger(),
getMementoManager: { setStartupMode: vi.fn() },
getDuplicateWorkspaceIpc: {
sendPing: vi.fn().mockResolvedValue(undefined),
},
...options.services,
};
return new Commands(
new Proxy({} as ServiceContainer, {
get: (_, name: string) => () => services[name] ?? {},
}),
{
getAxiosInstance: () => ({ defaults: { baseURL: options.baseUrl } }),
...options.client,
} as unknown as CoderApi,
{} as DeploymentManager,
);
}

/** Recently opened entries on one path; a multi-root file has no folder URI. */
export function mockRecentlyOpened(
authorities: string[],
Expand Down
2 changes: 1 addition & 1 deletion test/unit/command/updateWorkspace.telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ import * as vscode from "vscode";

import { workspace } from "@repo/mocks";

import { createTestCommands } from "../../mocks/commands";
import { createTelemetryHarness } from "../../mocks/telemetry";
import { createTestCommands } from "../../mocks/testHelpers";

import type { CoderApi } from "@/api/coderApi";

Expand Down
16 changes: 5 additions & 11 deletions test/unit/commands.netcheck.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ import * as vscode from "vscode";
import { Commands } from "@/commands";
import { toSafeHost } from "@/util/uri";

import { createTestCoderApi } from "../mocks/coderApi";
import { createTelemetryHarness } from "../mocks/telemetry";
import { createMockLogger, MockUserInteraction } from "../mocks/testHelpers";

import type { CoderApi } from "@/api/coderApi";
import type { ServiceContainer } from "@/core/container";
import type { DeploymentManager } from "@/deployment/deploymentManager";
import type { NetcheckPanelFactory } from "@/webviews/netcheck/netcheckPanelFactory";
Expand All @@ -17,12 +17,6 @@ vi.mock("@/workspace/workspacesProvider", () => ({
WorkspaceTreeItem: class {},
}));

function clientWithBaseUrl(baseURL: string | undefined): CoderApi {
return {
getAxiosInstance: () => ({ defaults: { baseURL } }),
} as unknown as CoderApi;
}

function setup(options: { extensionBaseUrl?: string } = {}) {
vi.clearAllMocks();
new MockUserInteraction();
Expand All @@ -44,7 +38,7 @@ function setup(options: { extensionBaseUrl?: string } = {}) {

const commands = new Commands(
serviceContainer,
clientWithBaseUrl(options.extensionBaseUrl),
createTestCoderApi({ baseUrl: options.extensionBaseUrl }),
{} as DeploymentManager,
);

Expand Down Expand Up @@ -87,9 +81,9 @@ describe("Commands.netcheck", () => {

it("prefers the remote workspace client over the extension client", async () => {
const { commands } = setup({ extensionBaseUrl: "https://ext.coder.test" });
commands.remoteWorkspaceClient = clientWithBaseUrl(
"https://remote.coder.test",
);
commands.remoteWorkspaceClient = createTestCoderApi({
baseUrl: "https://remote.coder.test",
});
const title = captureProgressTitle();

await commands.netcheck();
Expand Down
2 changes: 1 addition & 1 deletion test/unit/commands.openDevContainer.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

import { createTestCommands } from "../mocks/commands";
import {
createTestCommands,
MockConfigurationProvider,
mockRecentlyOpened,
openedAuthority,
Expand Down
7 changes: 2 additions & 5 deletions test/unit/commands.openSshConfig.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,8 @@ import * as vscode from "vscode";

import { PathResolver } from "@/core/pathResolver";

import {
createTestCommands,
MockConfigurationProvider,
useEditor,
} from "../mocks/testHelpers";
import { createTestCommands } from "../mocks/commands";
import { MockConfigurationProvider, useEditor } from "../mocks/testHelpers";

const pathResolver = new PathResolver("/data", "/logs");
const configPath = (editorId: string) =>
Expand Down
2 changes: 1 addition & 1 deletion test/unit/commands.openWorkspace.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@ import {
workspace as createWorkspace,
} from "@repo/mocks";

import { createTestCommands } from "../mocks/commands";
import {
createTestCommands,
MockConfigurationProvider,
mockRecentlyOpened,
openedAuthority,
Expand Down
9 changes: 5 additions & 4 deletions test/unit/commands.supportBundle.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {

import { agent, resource, workspace } from "@repo/mocks";

import { createTestCoderApi } from "../mocks/coderApi";
import { createTelemetryHarness } from "../mocks/telemetry";
import {
config,
Expand Down Expand Up @@ -102,10 +103,10 @@ function setup(options: { cliVersion?: string } = {}) {
getNetcheckPanelFactory: () => ({}),
} as unknown as ServiceContainer;

const client = {
getAxiosInstance: () => ({ defaults: { baseURL: "https://coder.test" } }),
getSessionToken: () => "token",
} as unknown as CoderApi;
const client = createTestCoderApi({
baseUrl: "https://coder.test",
token: "token",
});

const commands = new Commands(
serviceContainer,
Expand Down
6 changes: 2 additions & 4 deletions test/unit/commands.telemetry.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,14 @@ import { describe, expect, it, vi } from "vitest";
import { Commands } from "@/commands";
import { maybeAskUrl } from "@/promptUtils";

import { createTestCoderApi } from "../mocks/coderApi";
import { createTelemetryHarness } from "../mocks/telemetry";
import {
createMockLogger,
createMockUser,
MockUserInteraction,
} from "../mocks/testHelpers";

import type { CoderApi } from "@/api/coderApi";
import type { CliManager } from "@/core/cliManager";
import type { ServiceContainer } from "@/core/container";
import type { MementoManager } from "@/core/mementoManager";
Expand Down Expand Up @@ -127,9 +127,7 @@ function setup(options: SetupOptions = {}) {
}),
} as ServiceContainer;

const extensionClient = {
getAxiosInstance: () => ({ defaults: { baseURL: TEST_URL } }),
} as unknown as CoderApi;
const extensionClient = createTestCoderApi({ baseUrl: TEST_URL });

const commands = new Commands(
serviceContainer,
Expand Down
43 changes: 24 additions & 19 deletions test/unit/webviews/tasks/tasksPanelProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
type TaskIdParams,
} from "@repo/shared";

import { createTestCoderApi } from "../../../mocks/coderApi";
import {
createAxiosError,
createMockLogger,
Expand Down Expand Up @@ -85,23 +86,28 @@ type TasksPanelClient = Pick<

type MockClient = { [K in keyof TasksPanelClient]: Mock<TasksPanelClient[K]> };

function createClient(baseUrl = "https://coder.example.com"): MockClient {
return {
getTasks: vi.fn().mockResolvedValue([]),
getTask: vi.fn(),
getTaskLogs: vi.fn().mockResolvedValue({ logs: [] }),
createTask: vi.fn(),
deleteTask: vi.fn().mockResolvedValue(undefined),
getTemplates: vi.fn().mockResolvedValue([]),
getTemplateVersionPresets: vi.fn().mockResolvedValue([]),
startWorkspace: vi.fn().mockResolvedValue(undefined),
stopWorkspace: vi.fn().mockResolvedValue(undefined),
pauseTask: vi.fn().mockResolvedValue(undefined),
resumeTask: vi.fn().mockResolvedValue(undefined),
sendTaskInput: vi.fn().mockResolvedValue(undefined),
getHost: vi.fn().mockReturnValue(baseUrl),
getWorkspace: vi.fn().mockResolvedValue(workspace()),
};
function createClient(
baseUrl = "https://coder.example.com",
): CoderApi & MockClient {
return createTestCoderApi<MockClient>({
baseUrl,
overrides: {
getTasks: vi.fn().mockResolvedValue([]),
getTask: vi.fn(),
getTaskLogs: vi.fn().mockResolvedValue({ logs: [] }),
createTask: vi.fn(),
deleteTask: vi.fn().mockResolvedValue(undefined),
getTemplates: vi.fn().mockResolvedValue([]),
getTemplateVersionPresets: vi.fn().mockResolvedValue([]),
startWorkspace: vi.fn().mockResolvedValue(undefined),
stopWorkspace: vi.fn().mockResolvedValue(undefined),
pauseTask: vi.fn().mockResolvedValue(undefined),
resumeTask: vi.fn().mockResolvedValue(undefined),
sendTaskInput: vi.fn().mockResolvedValue(undefined),
getHost: vi.fn().mockReturnValue(baseUrl),
getWorkspace: vi.fn().mockResolvedValue(workspace()),
},
});
}

interface Harness {
Expand All @@ -126,8 +132,7 @@ function createHarness(): Harness {
const client = createClient();
const panel = new TasksPanelProvider(
vscode.Uri.file("/test/extension"),
// Cast needed: mock only implements the subset of CoderApi methods used by TasksPanelProvider
client as unknown as CoderApi,
client,
createMockLogger(),
);

Expand Down
24 changes: 13 additions & 11 deletions test/unit/workspace/workspaceMonitor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
workspace as createWorkspace,
} from "@repo/mocks";

import { createTestCoderApi } from "../../mocks/coderApi";
import {
createTestTelemetryService,
enableLocalTelemetry,
Expand All @@ -30,7 +31,6 @@ import type {
WorkspaceAgentScript,
} from "coder/site/src/api/typesGenerated";

import type { CoderApi } from "@/api/coderApi";
import type { TelemetryService } from "@/telemetry/service";

function workspaceEvent(
Expand All @@ -57,16 +57,18 @@ describe("WorkspaceMonitor", () => {
const statusBar = new MockStatusBarItem();
const contextManager = new MockContextManager();
const logger = createMockLogger();
const client = {
watchWorkspace: vi.fn().mockResolvedValue(stream),
getTemplate: vi.fn().mockResolvedValue({
active_version_id: "version-2",
}),
getTemplateVersion: vi.fn().mockResolvedValue({
message: "template v2",
}),
getWorkspaceAgentLogs: vi.fn(),
} as unknown as CoderApi;
const client = createTestCoderApi({
overrides: {
watchWorkspace: vi.fn().mockResolvedValue(stream),
getTemplate: vi.fn().mockResolvedValue({
active_version_id: "version-2",
}),
getTemplateVersion: vi.fn().mockResolvedValue({
message: "template v2",
}),
getWorkspaceAgentLogs: vi.fn(),
},
});
const monitor = await WorkspaceMonitor.create(
initialWorkspace,
client,
Expand Down
11 changes: 8 additions & 3 deletions test/webview/setup.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import "@testing-library/jest-dom/vitest";
import { vi } from "vitest";
import { afterAll, vi } from "vitest";

// Lit's dev build emits warnings we can't avoid (resolve.conditions is
// additive so we can't force the production bundle). Filter them out.
Expand Down Expand Up @@ -49,13 +49,18 @@ HTMLCanvasElement.prototype.getContext = ((type: string) =>

// VscodeSingleSelect fires internal slot-change events that read properties
// jsdom doesn't support (e.g. textContent of slotted elements). Suppress
// these uncaught errors from the third-party web component.
process.on("uncaughtException", (err: Error) => {
// these uncaught errors from the third-party web component. Removed per file
// because vmForks shares `process` across files.
function ignoreWebComponentErrors(err: Error): void {
const msg = err?.message ?? "";
if (msg.includes("reading 'trim'") || msg.includes("reading 'unobserve'")) {
return;
}
throw err;
}
process.on("uncaughtException", ignoreWebComponentErrors);
afterAll(() => {
process.off("uncaughtException", ignoreWebComponentErrors);
});

// Form controls call setFormValue/setValidity which jsdom doesn't provide.
Expand Down
3 changes: 3 additions & 0 deletions vitest.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ export default defineConfig({
include: ["test/webview/**/*.test.{ts,tsx}"],
exclude: ["**/node_modules/**", "**/out/**", "**/*.d.ts"],
environment: "jsdom",
// Much faster for jsdom. Not used for the extension project: Node
// built-ins return cross-realm values that break `instanceof Error`.
pool: "vmForks",
globals: true,
setupFiles: ["test/webview/setup.ts"],
},
Expand Down
Loading