mirror of
https://github.com/cline/cline.git
synced 2026-09-24 23:20:16 +08:00
Add multi-root workspace support to e2e tests (#6106)
* Add multi-root workspace support to e2e diff editor tests - Extract shared diff editor test logic into reusable function - Add WorkspaceType interface and workspace fixture configuration - Create test.code-workspace file for multi-root workspace testing - Add workspace_2 fixture directory with README - Update test helpers to support both single and multi-root workspaces - Add dedicated test case for diff editor in multi-root workspace * Add verbose logging control and improve workspace parameter naming in e2e tests - Add CLINE_E2E_TESTS_VERBOSE environment variable to control mock server logging - Replace console.log with conditional log function in ClineApiServerMock - Rename 'workspace' parameter to 'workspaceType' for clarity - Rename 'workspace' function parameter to 'workspacePath' for better semantics * Add doc string * rename
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { expect } from "@playwright/test"
|
||||
import { e2e } from "./utils/helpers"
|
||||
import { e2e, e2eMultiRoot } from "./utils/helpers"
|
||||
|
||||
e2e("Chat - can send messages and switch between modes", async ({ helper, page, sidebar }) => {
|
||||
e2e("Chat - can send messages and switch between modes", async ({ helper, sidebar }) => {
|
||||
// Sign in
|
||||
await helper.signin(sidebar)
|
||||
|
||||
@@ -48,7 +48,7 @@ e2e("Chat - can send messages and switch between modes", async ({ helper, page,
|
||||
await expect(sidebar.getByText("API Request Failed")).toBeVisible()
|
||||
})
|
||||
|
||||
e2e("Chat - slash commands preserve following text", async ({ helper, page, sidebar }) => {
|
||||
e2e("Chat - slash commands preserve following text", async ({ helper, sidebar }) => {
|
||||
// Sign in
|
||||
await helper.signin(sidebar)
|
||||
|
||||
@@ -68,7 +68,7 @@ e2e("Chat - slash commands preserve following text", async ({ helper, page, side
|
||||
await expect(inputbox).toHaveValue("/newtask following text should be preserved")
|
||||
})
|
||||
|
||||
e2e("Chat - @ mentions preserve following text", async ({ helper, page, sidebar }) => {
|
||||
e2e("Chat - @ mentions preserve following text", async ({ helper, sidebar }) => {
|
||||
// Sign in
|
||||
await helper.signin(sidebar)
|
||||
|
||||
@@ -88,7 +88,7 @@ e2e("Chat - @ mentions preserve following text", async ({ helper, page, sidebar
|
||||
await expect(inputbox).toHaveValue("@problems following text should be preserved")
|
||||
})
|
||||
|
||||
e2e("Chat - partial slash command completion preserves text", async ({ helper, page, sidebar }) => {
|
||||
e2e("Chat - partial slash command completion preserves text", async ({ helper, sidebar }) => {
|
||||
// Sign in
|
||||
await helper.signin(sidebar)
|
||||
|
||||
@@ -108,7 +108,7 @@ e2e("Chat - partial slash command completion preserves text", async ({ helper, p
|
||||
await expect(inputbox).toHaveValue("/newtask some important text after")
|
||||
})
|
||||
|
||||
e2e("Chat - partial @ mention completion preserves text", async ({ helper, page, sidebar }) => {
|
||||
e2eMultiRoot("[Multi-roots] Chat - partial @ mention completion preserves text", async ({ helper, sidebar }) => {
|
||||
// Sign in
|
||||
await helper.signin(sidebar)
|
||||
|
||||
|
||||
@@ -1,8 +1,13 @@
|
||||
import { expect } from "@playwright/test"
|
||||
import { expect, Frame, Page } from "@playwright/test"
|
||||
import { cleanChatView } from "./utils/common"
|
||||
import { e2e } from "./utils/helpers"
|
||||
import { e2e, e2eMultiRoot } from "./utils/helpers"
|
||||
|
||||
e2e("Diff editor", async ({ page, sidebar, helper }) => {
|
||||
/**
|
||||
* Shared test logic for diff editor tests
|
||||
* @param page - Playwright page object
|
||||
* @param sidebar - Sidebar frame for the Cline extension
|
||||
*/
|
||||
async function testDiffEditor(page: Page, sidebar: Frame) {
|
||||
await sidebar.getByRole("button", { name: "Get Started for Free" }).click({ delay: 100 })
|
||||
// Submit a message
|
||||
await cleanChatView(page)
|
||||
@@ -43,4 +48,12 @@ e2e("Diff editor", async ({ page, sidebar, helper }) => {
|
||||
await expect(diffEditor).toBeVisible()
|
||||
|
||||
await page.close()
|
||||
}
|
||||
|
||||
e2e("Diff editor", async ({ page, sidebar }) => {
|
||||
await testDiffEditor(page, sidebar)
|
||||
})
|
||||
|
||||
e2eMultiRoot("[Multi-roots] Diff editor", async ({ page, sidebar }) => {
|
||||
await testDiffEditor(page, sidebar)
|
||||
})
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { expect } from "@playwright/test"
|
||||
import { expect, Frame, Page } from "@playwright/test"
|
||||
import { addSelectedCodeToClineWebview, getClineEditorWebviewFrame, openTab, toggleNotifications } from "./utils/common"
|
||||
import { e2e } from "./utils/helpers"
|
||||
import { e2e, e2eMultiRoot } from "./utils/helpers"
|
||||
|
||||
e2e("code actions and editor panel", async ({ page, sidebar, helper }) => {
|
||||
async function editorTest(page: Page, sidebar: Frame) {
|
||||
await sidebar.getByRole("button", { name: "Get Started for Free" }).click({ delay: 100 })
|
||||
// Sidebar - input should start empty
|
||||
const sidebarInput = sidebar.getByTestId("chat-input")
|
||||
@@ -32,4 +32,12 @@ e2e("code actions and editor panel", async ({ page, sidebar, helper }) => {
|
||||
await expect(clineEditorWebview.getByTestId("chat-input")).toBeEmpty()
|
||||
await addSelectedCodeToClineWebview(page)
|
||||
await expect(clineEditorWebview.getByTestId("chat-input")).not.toBeEmpty()
|
||||
}
|
||||
|
||||
e2e("Code actions and editor panel", async ({ page, sidebar }) => {
|
||||
await editorTest(page, sidebar)
|
||||
})
|
||||
|
||||
e2eMultiRoot("[Multi-roots] Code actions and editor panel", async ({ page, sidebar }) => {
|
||||
await editorTest(page, sidebar)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"folders": [
|
||||
{
|
||||
"name": "Workspace 1",
|
||||
"path": "workspace"
|
||||
},
|
||||
{
|
||||
"name": "Workspace 2",
|
||||
"path": "workspace_2"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -10,6 +10,13 @@ const E2E_API_SERVER_PORT = 7777
|
||||
|
||||
export const MOCK_CLINE_API_SERVER_URL = `http://localhost:${E2E_API_SERVER_PORT}`
|
||||
|
||||
const useVerboseLogging = process.env.CLINE_E2E_TESTS_VERBOSE === "true"
|
||||
function log(...args: unknown[]) {
|
||||
if (useVerboseLogging) {
|
||||
console.log("[ClineApiServerMock]", ...args)
|
||||
}
|
||||
}
|
||||
|
||||
export class ClineApiServerMock {
|
||||
static globalSharedServer: ClineApiServerMock | null = null
|
||||
static globalSockets: Set<Socket> = new Set()
|
||||
@@ -106,10 +113,13 @@ export class ClineApiServerMock {
|
||||
|
||||
// Starts the global shared server
|
||||
public static async startGlobalServer(): Promise<ClineApiServerMock> {
|
||||
log("=== SERVER FIXTURE CALLED ===")
|
||||
if (ClineApiServerMock.globalSharedServer) {
|
||||
log("Using existing global server")
|
||||
return ClineApiServerMock.globalSharedServer
|
||||
}
|
||||
|
||||
log("Starting global server...")
|
||||
const server = createServer((req: IncomingMessage, res: ServerResponse) => {
|
||||
// Parse URL and method
|
||||
const parsedUrl = parse(req.url || "", true)
|
||||
@@ -136,7 +146,7 @@ export class ClineApiServerMock {
|
||||
|
||||
// Helper to send API response
|
||||
const sendApiResponse = (data: unknown, status = 200) => {
|
||||
console.log(`API Response: ${JSON.stringify(data)}`)
|
||||
log(`API Response: ${JSON.stringify(data)}`)
|
||||
sendJson({ success: true, data }, status)
|
||||
}
|
||||
|
||||
@@ -157,7 +167,7 @@ export class ClineApiServerMock {
|
||||
|
||||
// Authenticate the token and set current user
|
||||
if (isAuthRequired && authToken) {
|
||||
console.log(`Authenticating token: ${authToken}`)
|
||||
log(`Authenticating token: ${authToken}`)
|
||||
const user = ClineApiServerMock.globalSharedServer!.API_USER.getUserByToken(authToken)
|
||||
if (!user) {
|
||||
return sendApiError("Invalid token", 401)
|
||||
@@ -165,12 +175,12 @@ export class ClineApiServerMock {
|
||||
ClineApiServerMock.globalSharedServer!.setCurrentUser(user)
|
||||
}
|
||||
|
||||
console.log("=== MOCK SERVER REQUEST ===")
|
||||
console.log("Method:", method)
|
||||
console.log("Path:", path)
|
||||
console.log("Query:", JSON.stringify(query))
|
||||
console.log("Headers:", JSON.stringify(req.headers))
|
||||
console.log("===============")
|
||||
log("=== MOCK SERVER REQUEST ===")
|
||||
log("Method:", method)
|
||||
log("Path:", path)
|
||||
log("Query:", JSON.stringify(query))
|
||||
log("Headers:", JSON.stringify(req.headers))
|
||||
log("===============")
|
||||
|
||||
// Route handling
|
||||
const handleRequest = async () => {
|
||||
@@ -253,7 +263,7 @@ export class ClineApiServerMock {
|
||||
}
|
||||
const body = await readBody()
|
||||
const { orgId } = params
|
||||
console.log("Fetching organization usage transactions for", {
|
||||
log("Fetching organization usage transactions for", {
|
||||
orgId,
|
||||
body,
|
||||
})
|
||||
@@ -264,7 +274,7 @@ export class ClineApiServerMock {
|
||||
|
||||
if (endpoint === "/users/active-account" && method === "PUT") {
|
||||
const body = await readBody()
|
||||
console.log("Switching active account")
|
||||
log("Switching active account")
|
||||
const { organizationId } = JSON.parse(body)
|
||||
controller.setUserHasOrganization(!!organizationId)
|
||||
const currentUser = controller.API_USER.getCurrentUser()
|
||||
@@ -481,7 +491,7 @@ export class ClineApiServerMock {
|
||||
console.error(`Failed to start server on port ${E2E_API_SERVER_PORT}:`, error)
|
||||
reject(error)
|
||||
} else {
|
||||
console.log(`ClineApiServerMock listening on port ${E2E_API_SERVER_PORT}`)
|
||||
log(`ClineApiServerMock listening on port ${E2E_API_SERVER_PORT}`)
|
||||
resolve()
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Test Workspace 2
|
||||
|
||||
This workspace is used for testing the extension in a controlled environment.
|
||||
@@ -0,0 +1,9 @@
|
||||
import anthropic
|
||||
|
||||
anthropic.Anthropic().messages.create(
|
||||
model="claude-sonnet-4-20250514",
|
||||
max_tokens=1024,
|
||||
messages=[
|
||||
{"role": "user", "content": "Hello, world"}
|
||||
]
|
||||
)
|
||||
@@ -0,0 +1,10 @@
|
||||
from google import genai
|
||||
|
||||
client = genai.Client()
|
||||
|
||||
response = client.models.generate_content(
|
||||
model="gemini-2.5-flash",
|
||||
contents="Explain how AI works in a few words",
|
||||
)
|
||||
|
||||
print(response.text)
|
||||
@@ -0,0 +1,9 @@
|
||||
from openai import OpenAI
|
||||
client = OpenAI()
|
||||
|
||||
response = client.responses.create(
|
||||
model="gpt-5",
|
||||
input="Write a one-sentence bedtime story about a unicorn."
|
||||
)
|
||||
|
||||
print(response.output_text)
|
||||
@@ -0,0 +1,22 @@
|
||||
from openai import OpenAI
|
||||
|
||||
client = OpenAI(
|
||||
base_url="https://openrouter.ai/api/v1",
|
||||
api_key="<OPENROUTER_API_KEY>",
|
||||
)
|
||||
|
||||
completion = client.chat.completions.create(
|
||||
extra_headers={
|
||||
"HTTP-Referer": "<YOUR_SITE_URL>", # Optional. Site URL for rankings on openrouter.ai.
|
||||
"X-Title": "<YOUR_SITE_NAME>", # Optional. Site title for rankings on openrouter.ai.
|
||||
},
|
||||
model="openai/gpt-4o",
|
||||
messages=[
|
||||
{
|
||||
"role": "user",
|
||||
"content": "What is the meaning of life?"
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
print(completion.choices[0].message.content)
|
||||
@@ -0,0 +1,16 @@
|
||||
import os
|
||||
|
||||
from xai_sdk import Client
|
||||
from xai_sdk.chat import user, system
|
||||
|
||||
client = Client(
|
||||
api_key=os.getenv("XAI_API_KEY"),
|
||||
timeout=3600, # Override default timeout with longer timeout for reasoning models
|
||||
)
|
||||
|
||||
chat = client.chat.create(model="grok-4")
|
||||
chat.append(system("You are a PhD-level mathematician."))
|
||||
chat.append(user("What is 2 + 2?"))
|
||||
|
||||
response = chat.sample()
|
||||
print(response.content)
|
||||
@@ -8,10 +8,15 @@ import { ClineApiServerMock } from "../fixtures/server"
|
||||
|
||||
interface E2ETestDirectories {
|
||||
workspaceDir: string
|
||||
multiRootWorkspaceDir: string
|
||||
userDataDir: string
|
||||
extensionsDir: string
|
||||
}
|
||||
|
||||
export interface WorkspaceType {
|
||||
workspaceType: "single" | "multi"
|
||||
}
|
||||
|
||||
export class E2ETestHelper {
|
||||
// Constants
|
||||
public static readonly CODEBASE_ROOT_DIR = path.resolve(__dirname, "..", "..", "..", "..")
|
||||
@@ -160,6 +165,8 @@ export class E2ETestHelper {
|
||||
* This test configuration provides a comprehensive setup for end-to-end testing of the Cline VS Code extension,
|
||||
* including server mocking, temporary directories, VS Code instance management, and helper utilities.
|
||||
*
|
||||
* NOTE: Default to run in single-root workspace; use `e2eMultiRoot` for multi-root workspace tests.
|
||||
*
|
||||
* @extends test - Base Playwright test with multiple fixture extensions
|
||||
*
|
||||
* Fixtures provided:
|
||||
@@ -201,14 +208,9 @@ export class E2ETestHelper {
|
||||
export const e2e = test
|
||||
.extend<{ server: ClineApiServerMock | null }>({
|
||||
server: async ({}, use) => {
|
||||
console.log("=== SERVER FIXTURE CALLED ===")
|
||||
// Start server if it doesn't exist
|
||||
if (!ClineApiServerMock.globalSharedServer) {
|
||||
console.log("Starting global server...")
|
||||
await ClineApiServerMock.startGlobalServer()
|
||||
console.log("Global server started successfully")
|
||||
} else {
|
||||
console.log("Using existing global server")
|
||||
}
|
||||
await use(ClineApiServerMock.globalSharedServer)
|
||||
},
|
||||
@@ -217,6 +219,10 @@ export const e2e = test
|
||||
workspaceDir: async ({}, use) => {
|
||||
await use(path.join(E2ETestHelper.E2E_TESTS_DIR, "fixtures", "workspace"))
|
||||
},
|
||||
multiRootWorkspaceDir: async ({}, use) => {
|
||||
// DOCS: https://code.visualstudio.com/docs/editing/workspaces/multi-root-workspaces
|
||||
await use(path.join(E2ETestHelper.E2E_TESTS_DIR, "fixtures", "multiroots.code-workspace"))
|
||||
},
|
||||
userDataDir: async ({}, use) => {
|
||||
await use(mkdtempSync(path.join(os.tmpdir(), "vsce")))
|
||||
},
|
||||
@@ -224,11 +230,14 @@ export const e2e = test
|
||||
await use(mkdtempSync(path.join(os.tmpdir(), "vsce")))
|
||||
},
|
||||
})
|
||||
.extend<{ openVSCode: () => Promise<ElectronApplication> }>({
|
||||
openVSCode: async ({ workspaceDir, userDataDir, extensionsDir }, use, testInfo) => {
|
||||
.extend<WorkspaceType>({
|
||||
workspaceType: "single",
|
||||
})
|
||||
.extend<{ openVSCode: (workspacePath: string) => Promise<ElectronApplication> }>({
|
||||
openVSCode: async ({ userDataDir, extensionsDir }, use, testInfo) => {
|
||||
const executablePath = await downloadAndUnzipVSCode("stable", undefined, new SilentReporter())
|
||||
|
||||
await use(async () => {
|
||||
await use(async (workspacePath: string) => {
|
||||
const app = await _electron.launch({
|
||||
executablePath,
|
||||
env: {
|
||||
@@ -238,7 +247,6 @@ export const e2e = test
|
||||
CLINE_ENVIRONMENT: "local",
|
||||
GRPC_RECORDER_FILE_NAME: E2ETestHelper.generateTestFileName(testInfo.title, testInfo.project.name),
|
||||
// GRPC_RECORDER_ENABLED: "true",
|
||||
// GRPC_RECORDER_TESTS_FILTERS_ENABLED: "true"
|
||||
// IS_DEV: "true",
|
||||
// DEV_WORKSPACE_FOLDER: E2ETestHelper.CODEBASE_ROOT_DIR,
|
||||
},
|
||||
@@ -255,7 +263,7 @@ export const e2e = test
|
||||
`--extensions-dir=${extensionsDir}`,
|
||||
`--install-extension=${path.join(E2ETestHelper.CODEBASE_ROOT_DIR, "dist", "e2e.vsix")}`,
|
||||
`--extensionDevelopmentPath=${E2ETestHelper.CODEBASE_ROOT_DIR}`,
|
||||
workspaceDir,
|
||||
workspacePath,
|
||||
],
|
||||
})
|
||||
await E2ETestHelper.waitUntil(() => app.windows().length > 0)
|
||||
@@ -264,8 +272,9 @@ export const e2e = test
|
||||
},
|
||||
})
|
||||
.extend<{ app: ElectronApplication }>({
|
||||
app: async ({ openVSCode, userDataDir, extensionsDir }, use) => {
|
||||
const app = await openVSCode()
|
||||
app: async ({ openVSCode, userDataDir, extensionsDir, workspaceType, workspaceDir, multiRootWorkspaceDir }, use) => {
|
||||
const workspacePath = workspaceType === "single" ? workspaceDir : multiRootWorkspaceDir
|
||||
const app = await openVSCode(workspacePath)
|
||||
|
||||
try {
|
||||
await use(app)
|
||||
@@ -299,5 +308,12 @@ export const e2e = test
|
||||
},
|
||||
})
|
||||
|
||||
/**
|
||||
* Multi-root workspace variant of the e2e test fixture
|
||||
*/
|
||||
export const e2eMultiRoot = e2e.extend<WorkspaceType>({
|
||||
workspaceType: "multi",
|
||||
})
|
||||
|
||||
// Backward compatibility exports
|
||||
export const getResultsDir = E2ETestHelper.getResultsDir
|
||||
|
||||
Reference in New Issue
Block a user