diff --git a/jest.config.js b/jest.config.js index 02a4a78289..d0efc8eaaf 100644 --- a/jest.config.js +++ b/jest.config.js @@ -34,6 +34,7 @@ module.exports = { transformIgnorePatterns: [ "node_modules/(?!(@modelcontextprotocol|delay|p-wait-for|globby|serialize-error|strip-ansi|default-shell|os-name)/)", ], + roots: ["/src", "/webview-ui/src"], modulePathIgnorePatterns: [".vscode-test"], reporters: [["jest-simple-dot-reporter", {}]], setupFiles: [], diff --git a/src/activate/registerCommands.ts b/src/activate/registerCommands.ts index fa650c6e38..bbfc716f6d 100644 --- a/src/activate/registerCommands.ts +++ b/src/activate/registerCommands.ts @@ -75,7 +75,7 @@ const openClineInNewTab = async ({ context, outputChannel }: Omit, used to recreate a deallocated webview, but we don't need this since we use retainContextWhenHidden - //token: vscode.CancellationToken - ): void | Thenable { + async resolveWebviewView(webviewView: vscode.WebviewView | vscode.WebviewPanel) { this.outputChannel.appendLine("Resolving webview view") this.view = webviewView @@ -277,7 +273,7 @@ export class ClineProvider implements vscode.WebviewViewProvider { webviewView.webview.html = this.context.extensionMode === vscode.ExtensionMode.Development - ? this.getHMRHtmlContent(webviewView.webview) + ? await this.getHMRHtmlContent(webviewView.webview) : this.getHtmlContent(webviewView.webview) // Sets up an event listener to listen for messages passed from the webview view context @@ -402,9 +398,22 @@ export class ClineProvider implements vscode.WebviewViewProvider { await this.view?.webview.postMessage(message) } - private getHMRHtmlContent(webview: vscode.Webview): string { - const nonce = getNonce() + private async getHMRHtmlContent(webview: vscode.Webview): Promise { + const localPort = "5173" + const localServerUrl = `localhost:${localPort}` + // Check if local dev server is running. + try { + await axios.get(`http://${localServerUrl}`) + } catch (error) { + vscode.window.showErrorMessage( + "Local development server is not running, HMR will not work. Please run 'npm run dev' before launching the extension to enable HMR.", + ) + + return this.getHtmlContent(webview) + } + + const nonce = getNonce() const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"]) const codiconsUri = getUri(webview, this.context.extensionUri, [ "node_modules", @@ -415,8 +424,6 @@ export class ClineProvider implements vscode.WebviewViewProvider { ]) const file = "src/index.tsx" - const localPort = "5173" - const localServerUrl = `localhost:${localPort}` const scriptUri = `http://${localServerUrl}/${file}` const reactRefresh = /*html*/ ` diff --git a/src/core/webview/__tests__/ClineProvider.test.ts b/src/core/webview/__tests__/ClineProvider.test.ts index 664b7b5f0c..1f249401b3 100644 --- a/src/core/webview/__tests__/ClineProvider.test.ts +++ b/src/core/webview/__tests__/ClineProvider.test.ts @@ -1,13 +1,17 @@ -import { ClineProvider } from "../ClineProvider" +// npx jest src/core/webview/__tests__/ClineProvider.test.ts + import * as vscode from "vscode" +import axios from "axios" + +import { ClineProvider } from "../ClineProvider" import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage" import { setSoundEnabled } from "../../../utils/sound" -import { defaultModeSlug, modes } from "../../../shared/modes" -import { addCustomInstructions } from "../../prompts/sections/custom-instructions" -import { experimentDefault, experiments } from "../../../shared/experiments" +import { defaultModeSlug } from "../../../shared/modes" +import { experimentDefault } from "../../../shared/experiments" // Mock custom-instructions module const mockAddCustomInstructions = jest.fn() + jest.mock("../../prompts/sections/custom-instructions", () => ({ addCustomInstructions: mockAddCustomInstructions, })) @@ -202,7 +206,6 @@ describe("ClineProvider", () => { let mockOutputChannel: vscode.OutputChannel let mockWebviewView: vscode.WebviewView let mockPostMessage: jest.Mock - let visibilityChangeCallback: (e?: unknown) => void beforeEach(() => { // Reset mocks @@ -270,13 +273,13 @@ describe("ClineProvider", () => { return { dispose: jest.fn() } }), onDidChangeVisibility: jest.fn().mockImplementation((callback) => { - visibilityChangeCallback = callback return { dispose: jest.fn() } }), } as unknown as vscode.WebviewView provider = new ClineProvider(mockContext, mockOutputChannel) - // @ts-ignore - accessing private property for testing + + // @ts-ignore - Accessing private property for testing. provider.customModesManager = mockCustomModesManager }) @@ -288,18 +291,36 @@ describe("ClineProvider", () => { expect(ClineProvider.getVisibleInstance()).toBe(provider) }) - test("resolveWebviewView sets up webview correctly", () => { - provider.resolveWebviewView(mockWebviewView) + test("resolveWebviewView sets up webview correctly", async () => { + await provider.resolveWebviewView(mockWebviewView) expect(mockWebviewView.webview.options).toEqual({ enableScripts: true, localResourceRoots: [mockContext.extensionUri], }) + + expect(mockWebviewView.webview.html).toContain("") + }) + + test("resolveWebviewView sets up webview correctly in development mode even if local server is not running", async () => { + provider = new ClineProvider( + { ...mockContext, extensionMode: vscode.ExtensionMode.Development }, + mockOutputChannel, + ) + ;(axios.get as jest.Mock).mockRejectedValueOnce(new Error("Network error")) + + await provider.resolveWebviewView(mockWebviewView) + + expect(mockWebviewView.webview.options).toEqual({ + enableScripts: true, + localResourceRoots: [mockContext.extensionUri], + }) + expect(mockWebviewView.webview.html).toContain("") }) test("postMessageToWebview sends message to webview", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const mockState: ExtensionState = { version: "1.0.0", @@ -341,7 +362,7 @@ describe("ClineProvider", () => { }) test("handles webviewDidLaunch message", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) // Get the message handler from onDidReceiveMessage const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] @@ -420,7 +441,7 @@ describe("ClineProvider", () => { }) test("handles writeDelayMs message", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] await messageHandler({ type: "writeDelayMs", value: 2000 }) @@ -430,7 +451,7 @@ describe("ClineProvider", () => { }) test("updates sound utility when sound setting changes", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) // Get the message handler from onDidReceiveMessage const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] @@ -470,7 +491,7 @@ describe("ClineProvider", () => { }) test("loads saved API config when switching modes", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock ConfigManager methods @@ -491,7 +512,7 @@ describe("ClineProvider", () => { }) test("saves current config when switching to mode without config", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock ConfigManager methods @@ -519,7 +540,7 @@ describe("ClineProvider", () => { }) test("saves config as default for current mode when loading config", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] provider.configManager = { @@ -540,7 +561,7 @@ describe("ClineProvider", () => { }) test("handles request delay settings messages", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Test alwaysApproveResubmit @@ -555,7 +576,7 @@ describe("ClineProvider", () => { }) test("handles updatePrompt message correctly", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock existing prompts @@ -650,7 +671,7 @@ describe("ClineProvider", () => { ) }) test("handles mode-specific custom instructions updates", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock existing prompts @@ -707,7 +728,7 @@ describe("ClineProvider", () => { // Create new provider with updated mock context provider = new ClineProvider(mockContext, mockOutputChannel) - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] provider.configManager = { @@ -732,10 +753,10 @@ describe("ClineProvider", () => { }) describe("deleteMessage", () => { - beforeEach(() => { + beforeEach(async () => { // Mock window.showInformationMessage ;(vscode.window.showInformationMessage as jest.Mock) = jest.fn() - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) }) test('handles "Just this message" deletion correctly', async () => { @@ -861,9 +882,9 @@ describe("ClineProvider", () => { }) describe("getSystemPrompt", () => { - beforeEach(() => { + beforeEach(async () => { mockPostMessage.mockClear() - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) // Reset and setup mock mockAddCustomInstructions.mockClear() mockAddCustomInstructions.mockImplementation( @@ -1111,7 +1132,7 @@ describe("ClineProvider", () => { }) // Resolve webview and trigger getSystemPrompt - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const architectHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] await architectHandler({ type: "getSystemPrompt" }) @@ -1125,9 +1146,9 @@ describe("ClineProvider", () => { }) describe("handleModeSwitch", () => { - beforeEach(() => { + beforeEach(async () => { // Set up webview for each test - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) }) test("loads saved API config when switching modes", async () => { @@ -1188,7 +1209,7 @@ describe("ClineProvider", () => { describe("updateCustomMode", () => { test("updates both file and state when updating custom mode", async () => { - provider.resolveWebviewView(mockWebviewView) + await provider.resolveWebviewView(mockWebviewView) const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0] // Mock CustomModesManager methods diff --git a/src/test/task.test.ts b/src/test/task.test.ts index cda766c713..63f4d4777a 100644 --- a/src/test/task.test.ts +++ b/src/test/task.test.ts @@ -31,7 +31,7 @@ suite("Roo Code Task", () => { try { // Initialize provider with panel. - provider.resolveWebviewView(panel) + await provider.resolveWebviewView(panel) // Wait for webview to launch. let startTime = Date.now()