Add VSCode Webview Integration Tests (#1373)

* update ci/cd

* ci: more git actions

* remove duplicitive git actions file

* remove duplicative git actions file

* add default permissions

* chore: sync package-lock.json with main

* ci: Fix job name to match branch protection rule

* ci: Remove test file with formatting issues

* style: Fix formatting in modified files

* ci: Remove warning mode, keep webview tests

* fixed npm build issue

* fix formatting again

* Add webview tests

* fix formatting

* remove temporary docs

* remove unecessary auth and types

* move tmp in gitignore

* spare line

* restore prior comments in vscode-test

* remove redundant infrastructure

* prettier

* removed vestigial webviews

---------

Co-authored-by: Saoud Rizwan <7799382+saoudrizwan@users.noreply.github.com>
This commit is contained in:
akfoster 2025-01-21 23:59:06 -06:00 committed by GitHub
parent 29725445cc
commit 26ee05dd07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 206 additions and 6 deletions

View file

@ -6,6 +6,12 @@ on:
branches:
- main
# Set default permissions for all jobs
permissions:
contents: read # Needed to check out code
checks: write # Needed to report test results
pull-requests: write # Needed to add comments/annotations to PRs
jobs:
test:
runs-on: ubuntu-latest
@ -51,6 +57,5 @@ jobs:
- name: Prettier / Format Check
run: npm run format
- name: Tests
- name: Extension Tests
run: xvfb-run -a npm run test
if: runner.os == 'Linux'

1
.gitignore vendored
View file

@ -1,6 +1,7 @@
out
dist
node_modules
tmp
.vscode-test/
*.vsix

View file

@ -1,8 +1,14 @@
import { defineConfig } from "@vscode/test-cli"
import path from "path"
export default defineConfig({
files: "out/**/*.test.js",
files: "out/test/**/*.test.js",
mocha: {
ui: "bdd",
timeout: 20000, // Maximum time (in ms) that a test can run before failing
},
workspaceFolder: "test-workspace",
version: "stable",
extensionDevelopmentPath: path.resolve("./"),
launchArgs: ["--disable-extensions"],
})

1
package-lock.json generated
View file

@ -60,6 +60,7 @@
"@vscode/test-electron": "^2.4.0",
"esbuild": "^0.21.5",
"eslint": "^8.57.0",
"glob": "^10.3.10",
"husky": "^9.1.7",
"npm-run-all": "^4.1.5",
"prettier": "^3.3.3",

View file

@ -4,7 +4,7 @@ import path from "path"
import "should"
import * as vscode from "vscode"
const packagePath = path.join(__dirname, "..", "..", "..", "package.json")
const packagePath = path.join(__dirname, "..", "..", "package.json")
describe("Cline Extension", () => {
after(() => {
@ -23,4 +23,69 @@ describe("Cline Extension", () => {
await new Promise((resolve) => setTimeout(resolve, 400))
await vscode.commands.executeCommand("cline.plusButtonClicked")
})
// New test to verify xvfb and webview functionality
it("should create and display a webview panel", async () => {
// Create a webview panel
const panel = vscode.window.createWebviewPanel("testWebview", "CI/CD Test", vscode.ViewColumn.One, {
enableScripts: true,
})
// Set some HTML content
panel.webview.html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>xvfb Test</title>
</head>
<body>
<div id="test">Testing xvfb display server</div>
</body>
</html>
`
// Verify panel exists
should.exist(panel)
panel.visible.should.be.true()
// Clean up
panel.dispose()
})
// Test webview message passing
it("should handle webview messages", async () => {
const panel = vscode.window.createWebviewPanel("testWebview", "Message Test", vscode.ViewColumn.One, {
enableScripts: true,
})
// Set up message handling
const messagePromise = new Promise<string>((resolve) => {
panel.webview.onDidReceiveMessage((message) => resolve(message.text), undefined)
})
// Add message sending script
panel.webview.html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Message Test</title>
</head>
<body>
<script>
const vscode = acquireVsCodeApi();
vscode.postMessage({ text: 'test-message' });
</script>
</body>
</html>
`
// Wait for message
const message = await messagePromise
message.should.equal("test-message")
// Clean up
panel.dispose()
})
})

View file

@ -0,0 +1,116 @@
import * as vscode from "vscode"
import { describe, it, beforeEach, afterEach } from "mocha"
import { strict as assert } from "assert"
import { join } from "path"
describe("Chat Integration Tests", () => {
let panel: vscode.WebviewPanel
let disposables: vscode.Disposable[] = []
beforeEach(async () => {
// Create VSCode webview panel
panel = vscode.window.createWebviewPanel("testWebview", "Chat Test", vscode.ViewColumn.One, {
enableScripts: true,
retainContextWhenHidden: true,
})
// Set up minimal test webview
panel.webview.html = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script>
const vscode = acquireVsCodeApi();
window.addEventListener('message', event => {
const message = event.data;
switch (message.type) {
case 'sendMessage':
vscode.postMessage({ type: 'newTask', text: message.text });
break;
case 'toggleMode':
vscode.postMessage({ type: 'chatSettings', chatSettings: { mode: 'act' } });
break;
case 'invoke':
if (message.invoke === 'primaryButtonClick') {
vscode.postMessage({ type: 'askResponse', askResponse: 'yesButtonClicked' });
}
break;
}
});
</script>
</head>
<body>
<div id="test-webview"></div>
</body>
</html>
`
})
afterEach(() => {
panel.dispose()
disposables.forEach((d) => d.dispose())
disposables = []
})
it("should send chat messages", async () => {
// Set up message listener
const messagePromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (message.type === "newTask") {
resolve(message)
}
})
})
// Trigger send message
await panel.webview.postMessage({
type: "sendMessage",
text: "Create a hello world app",
})
// Verify message was sent
const message = await messagePromise
assert.equal(message.type, "newTask")
assert.equal(message.text, "Create a hello world app")
})
it("should toggle between plan and act modes", async () => {
// Set up state change listener
const stateChangePromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (message.type === "chatSettings") {
resolve(message)
}
})
})
// Trigger mode toggle
await panel.webview.postMessage({ type: "toggleMode" })
// Verify mode changed
const stateChange = await stateChangePromise
assert.equal(stateChange.chatSettings.mode, "act")
})
it("should handle tool approval flow", async () => {
// Set up approval listener
const approvalPromise = new Promise<any>((resolve) => {
panel.webview.onDidReceiveMessage((message) => {
if (message.type === "askResponse") {
resolve(message)
}
})
})
// Trigger tool approval
await panel.webview.postMessage({
type: "invoke",
invoke: "primaryButtonClick",
})
// Verify approval was sent
const response = await approvalPromise
assert.equal(response.type, "askResponse")
assert.equal(response.askResponse, "yesButtonClicked")
})
})

View file

@ -8,7 +8,9 @@
"compilerOptions": {
"module": "commonjs",
"moduleResolution": "node",
"types": ["node", "mocha", "should", "vscode"]
"types": ["node", "mocha", "should", "vscode"],
"outDir": "out",
"rootDir": "src"
},
"include": ["src/**/*.test.ts"]
}

View file

@ -803,6 +803,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}}
/>
<DynamicTextArea
data-testid="chat-input"
ref={(el) => {
if (typeof ref === "function") {
ref(el)
@ -909,6 +910,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
}}
/> */}
<div
data-testid="send-button"
className={`input-icon-button ${textAreaDisabled ? "disabled" : ""} codicon codicon-send`}
onClick={() => {
if (!textAreaDisabled) {
@ -923,6 +925,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
<ControlsContainer>
<ButtonGroup>
<VSCodeButton
data-testid="context-button"
appearance="icon"
aria-label="Add Context"
disabled={textAreaDisabled}
@ -935,6 +938,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</VSCodeButton>
<VSCodeButton
data-testid="images-button"
appearance="icon"
aria-label="Add Images"
disabled={shouldDisableImages}
@ -985,7 +989,7 @@ const ChatTextArea = forwardRef<HTMLTextAreaElement, ChatTextAreaProps>(
</ModelContainer>
</ButtonGroup>
<SwitchContainer disabled={textAreaDisabled} onClick={onModeToggle}>
<SwitchContainer data-testid="mode-switch" disabled={textAreaDisabled} onClick={onModeToggle}>
<Slider isAct={chatSettings.mode === "act"} />
<SwitchOption isActive={chatSettings.mode === "plan"}>Plan</SwitchOption>
<SwitchOption isActive={chatSettings.mode === "act"}>Act</SwitchOption>