hakalab-mcp 2.0.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IDE_INTEGRATION_EXAMPLES.md +1497 -0
- package/MCP_API_SPECIFICATION.md +932 -0
- package/README.md +237 -150
- package/SESSIONS_GUIDE.md +393 -0
- package/TECHNICAL_GUIDE.md +960 -0
- package/index.js +371 -633
- package/index.js.backup +1521 -0
- package/package.json +2 -2
- package/test_simple.js +34 -0
- package/tools_definition.js +585 -0
- package/README_MCP.md +0 -436
|
@@ -0,0 +1,1497 @@
|
|
|
1
|
+
# Ejemplos de Integración IDE - Hakalab-MCP
|
|
2
|
+
|
|
3
|
+
**Ejemplos prácticos de código para integración con plugins de IntelliJ IDEA y PyCharm**
|
|
4
|
+
|
|
5
|
+
## 📋 Contenido
|
|
6
|
+
|
|
7
|
+
1. [Configuración del Cliente MCP](#configuración-del-cliente-mcp)
|
|
8
|
+
2. [Implementación de Herramientas](#implementación-de-herramientas)
|
|
9
|
+
3. [UI Components](#ui-components)
|
|
10
|
+
4. [AI Agent Integration](#ai-agent-integration)
|
|
11
|
+
5. [Code Generation](#code-generation)
|
|
12
|
+
6. [Test Execution](#test-execution)
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 🔧 Configuración del Cliente MCP
|
|
17
|
+
|
|
18
|
+
### MCPClient.kt
|
|
19
|
+
```kotlin
|
|
20
|
+
package com.hakalab.mcp.client
|
|
21
|
+
|
|
22
|
+
import com.fasterxml.jackson.databind.ObjectMapper
|
|
23
|
+
import com.fasterxml.jackson.module.kotlin.KotlinModule
|
|
24
|
+
import kotlinx.coroutines.*
|
|
25
|
+
import java.io.*
|
|
26
|
+
import java.util.concurrent.TimeUnit
|
|
27
|
+
|
|
28
|
+
class MCPClient(private val config: MCPConfig) {
|
|
29
|
+
private val objectMapper = ObjectMapper().registerModule(KotlinModule())
|
|
30
|
+
private var process: Process? = null
|
|
31
|
+
private var writer: BufferedWriter? = null
|
|
32
|
+
private var reader: BufferedReader? = null
|
|
33
|
+
private var requestId = 0
|
|
34
|
+
|
|
35
|
+
suspend fun connect(): Boolean = withContext(Dispatchers.IO) {
|
|
36
|
+
try {
|
|
37
|
+
val processBuilder = ProcessBuilder(config.command, *config.args.toTypedArray())
|
|
38
|
+
processBuilder.environment().putAll(config.env)
|
|
39
|
+
|
|
40
|
+
process = processBuilder.start()
|
|
41
|
+
writer = process!!.outputStream.bufferedWriter()
|
|
42
|
+
reader = process!!.inputStream.bufferedReader()
|
|
43
|
+
|
|
44
|
+
// Initialize MCP connection
|
|
45
|
+
val initRequest = MCPRequest(
|
|
46
|
+
jsonrpc = "2.0",
|
|
47
|
+
id = getNextRequestId(),
|
|
48
|
+
method = "initialize",
|
|
49
|
+
params = mapOf(
|
|
50
|
+
"protocolVersion" to "2024-11-05",
|
|
51
|
+
"capabilities" to mapOf(
|
|
52
|
+
"tools" to emptyMap<String, Any>()
|
|
53
|
+
),
|
|
54
|
+
"clientInfo" to mapOf(
|
|
55
|
+
"name" to "hakalab-ide-plugin",
|
|
56
|
+
"version" to "1.0.0"
|
|
57
|
+
)
|
|
58
|
+
)
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
sendRequest(initRequest)
|
|
62
|
+
val response = readResponse()
|
|
63
|
+
response.error == null
|
|
64
|
+
} catch (e: Exception) {
|
|
65
|
+
false
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
suspend fun callTool(toolName: String, arguments: Map<String, Any>): MCPResponse {
|
|
70
|
+
return withContext(Dispatchers.IO) {
|
|
71
|
+
val request = MCPRequest(
|
|
72
|
+
jsonrpc = "2.0",
|
|
73
|
+
id = getNextRequestId(),
|
|
74
|
+
method = "tools/call",
|
|
75
|
+
params = mapOf(
|
|
76
|
+
"name" to toolName,
|
|
77
|
+
"arguments" to arguments
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
sendRequest(request)
|
|
82
|
+
readResponse()
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
suspend fun listTools(): List<MCPTool> {
|
|
87
|
+
return withContext(Dispatchers.IO) {
|
|
88
|
+
val request = MCPRequest(
|
|
89
|
+
jsonrpc = "2.0",
|
|
90
|
+
id = getNextRequestId(),
|
|
91
|
+
method = "tools/list",
|
|
92
|
+
params = emptyMap<String, Any>()
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
sendRequest(request)
|
|
96
|
+
val response = readResponse()
|
|
97
|
+
|
|
98
|
+
response.result?.get("tools")?.let { tools ->
|
|
99
|
+
objectMapper.convertValue(tools, Array<MCPTool>::class.java).toList()
|
|
100
|
+
} ?: emptyList()
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun sendRequest(request: MCPRequest) {
|
|
105
|
+
val json = objectMapper.writeValueAsString(request)
|
|
106
|
+
writer?.write(json)
|
|
107
|
+
writer?.newLine()
|
|
108
|
+
writer?.flush()
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private fun readResponse(): MCPResponse {
|
|
112
|
+
val line = reader?.readLine() ?: throw IOException("No response from MCP server")
|
|
113
|
+
return objectMapper.readValue(line, MCPResponse::class.java)
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private fun getNextRequestId(): String = (++requestId).toString()
|
|
117
|
+
|
|
118
|
+
fun disconnect() {
|
|
119
|
+
writer?.close()
|
|
120
|
+
reader?.close()
|
|
121
|
+
process?.destroyForcibly()
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
data class MCPConfig(
|
|
126
|
+
val command: String = "npx",
|
|
127
|
+
val args: List<String> = listOf("-y", "hakalab-mcp"),
|
|
128
|
+
val env: Map<String, String> = mapOf(
|
|
129
|
+
"NODE_ENV" to "production",
|
|
130
|
+
"DEBUG" to "false"
|
|
131
|
+
),
|
|
132
|
+
val timeout: Long = 30000
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
data class MCPRequest(
|
|
136
|
+
val jsonrpc: String,
|
|
137
|
+
val id: String,
|
|
138
|
+
val method: String,
|
|
139
|
+
val params: Map<String, Any>
|
|
140
|
+
)
|
|
141
|
+
|
|
142
|
+
data class MCPResponse(
|
|
143
|
+
val jsonrpc: String,
|
|
144
|
+
val id: String,
|
|
145
|
+
val result: Map<String, Any>? = null,
|
|
146
|
+
val error: MCPError? = null
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
data class MCPError(
|
|
150
|
+
val code: Int,
|
|
151
|
+
val message: String,
|
|
152
|
+
val data: Any? = null
|
|
153
|
+
)
|
|
154
|
+
|
|
155
|
+
data class MCPTool(
|
|
156
|
+
val name: String,
|
|
157
|
+
val description: String,
|
|
158
|
+
val inputSchema: Map<String, Any>
|
|
159
|
+
)
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## 🛠️ Implementación de Herramientas
|
|
165
|
+
|
|
166
|
+
### ToolExecutor.kt
|
|
167
|
+
```kotlin
|
|
168
|
+
package com.hakalab.mcp.tools
|
|
169
|
+
|
|
170
|
+
import com.intellij.openapi.progress.ProgressIndicator
|
|
171
|
+
import com.intellij.openapi.progress.Task
|
|
172
|
+
import com.intellij.openapi.project.Project
|
|
173
|
+
import kotlinx.coroutines.runBlocking
|
|
174
|
+
|
|
175
|
+
class ToolExecutor(
|
|
176
|
+
private val project: Project,
|
|
177
|
+
private val mcpClient: MCPClient
|
|
178
|
+
) {
|
|
179
|
+
|
|
180
|
+
fun executeBrowserAutomation(workflow: BrowserWorkflow) {
|
|
181
|
+
object : Task.Backgroundable(project, "Executing Browser Automation", true) {
|
|
182
|
+
override fun run(indicator: ProgressIndicator) {
|
|
183
|
+
runBlocking {
|
|
184
|
+
try {
|
|
185
|
+
indicator.text = "Opening browser..."
|
|
186
|
+
val browserResult = mcpClient.callTool("open_browser", workflow.browserConfig)
|
|
187
|
+
|
|
188
|
+
if (!browserResult.isSuccess()) {
|
|
189
|
+
throw Exception("Failed to open browser: ${browserResult.error?.message}")
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
workflow.steps.forEachIndexed { index, step ->
|
|
193
|
+
indicator.text = "Executing step ${index + 1}/${workflow.steps.size}: ${step.description}"
|
|
194
|
+
indicator.fraction = (index + 1).toDouble() / workflow.steps.size
|
|
195
|
+
|
|
196
|
+
val result = mcpClient.callTool(step.toolName, step.parameters)
|
|
197
|
+
|
|
198
|
+
if (!result.isSuccess()) {
|
|
199
|
+
throw Exception("Step failed: ${result.error?.message}")
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Optional: Take screenshot after each step
|
|
203
|
+
if (step.takeScreenshot) {
|
|
204
|
+
mcpClient.callTool("take_screenshot", mapOf(
|
|
205
|
+
"filename" to "step_${index + 1}_${step.toolName}.png"
|
|
206
|
+
))
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
indicator.text = "Closing browser..."
|
|
211
|
+
mcpClient.callTool("close_browser", emptyMap())
|
|
212
|
+
|
|
213
|
+
showSuccessNotification("Browser automation completed successfully")
|
|
214
|
+
|
|
215
|
+
} catch (e: Exception) {
|
|
216
|
+
showErrorNotification("Browser automation failed: ${e.message}")
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}.queue()
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
fun createTestSuite(testSuiteConfig: TestSuiteConfig) {
|
|
224
|
+
object : Task.Backgroundable(project, "Creating Test Suite", false) {
|
|
225
|
+
override fun run(indicator: ProgressIndicator) {
|
|
226
|
+
runBlocking {
|
|
227
|
+
try {
|
|
228
|
+
indicator.text = "Creating test suite..."
|
|
229
|
+
|
|
230
|
+
val result = mcpClient.callTool("create_test_suite", mapOf(
|
|
231
|
+
"feature_name" to testSuiteConfig.featureName,
|
|
232
|
+
"scenarios" to testSuiteConfig.scenarios,
|
|
233
|
+
"page_elements" to testSuiteConfig.pageElements,
|
|
234
|
+
"tags" to testSuiteConfig.tags
|
|
235
|
+
))
|
|
236
|
+
|
|
237
|
+
if (result.isSuccess()) {
|
|
238
|
+
// Refresh project files
|
|
239
|
+
project.baseDir.refresh(false, true)
|
|
240
|
+
showSuccessNotification("Test suite created successfully")
|
|
241
|
+
|
|
242
|
+
// Open generated files
|
|
243
|
+
openGeneratedFiles(testSuiteConfig.featureName)
|
|
244
|
+
} else {
|
|
245
|
+
throw Exception(result.error?.message ?: "Unknown error")
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
} catch (e: Exception) {
|
|
249
|
+
showErrorNotification("Failed to create test suite: ${e.message}")
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}.queue()
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
fun executeTests(testConfig: TestExecutionConfig) {
|
|
257
|
+
object : Task.Backgroundable(project, "Executing Tests", true) {
|
|
258
|
+
override fun run(indicator: ProgressIndicator) {
|
|
259
|
+
runBlocking {
|
|
260
|
+
try {
|
|
261
|
+
indicator.text = "Executing tests..."
|
|
262
|
+
|
|
263
|
+
val result = mcpClient.callTool("execute_tests", mapOf(
|
|
264
|
+
"tags" to testConfig.tags,
|
|
265
|
+
"feature_file" to testConfig.featureFile,
|
|
266
|
+
"parallel" to testConfig.parallel
|
|
267
|
+
))
|
|
268
|
+
|
|
269
|
+
if (result.isSuccess()) {
|
|
270
|
+
indicator.text = "Generating Allure report..."
|
|
271
|
+
mcpClient.callTool("generate_allure_report", emptyMap())
|
|
272
|
+
|
|
273
|
+
showTestResults(result)
|
|
274
|
+
} else {
|
|
275
|
+
throw Exception(result.error?.message ?: "Test execution failed")
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
} catch (e: Exception) {
|
|
279
|
+
showErrorNotification("Test execution failed: ${e.message}")
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}.queue()
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
private fun MCPResponse.isSuccess(): Boolean = error == null
|
|
287
|
+
|
|
288
|
+
private fun showSuccessNotification(message: String) {
|
|
289
|
+
// Implementation for success notification
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private fun showErrorNotification(message: String) {
|
|
293
|
+
// Implementation for error notification
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
private fun openGeneratedFiles(featureName: String) {
|
|
297
|
+
// Implementation to open generated files in editor
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private fun showTestResults(result: MCPResponse) {
|
|
301
|
+
// Implementation to show test results in tool window
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
data class BrowserWorkflow(
|
|
306
|
+
val name: String,
|
|
307
|
+
val browserConfig: Map<String, Any>,
|
|
308
|
+
val steps: List<WorkflowStep>
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
data class WorkflowStep(
|
|
312
|
+
val toolName: String,
|
|
313
|
+
val parameters: Map<String, Any>,
|
|
314
|
+
val description: String,
|
|
315
|
+
val takeScreenshot: Boolean = false
|
|
316
|
+
)
|
|
317
|
+
|
|
318
|
+
data class TestSuiteConfig(
|
|
319
|
+
val featureName: String,
|
|
320
|
+
val scenarios: List<Map<String, Any>>,
|
|
321
|
+
val pageElements: List<Map<String, Any>>,
|
|
322
|
+
val tags: List<String>
|
|
323
|
+
)
|
|
324
|
+
|
|
325
|
+
data class TestExecutionConfig(
|
|
326
|
+
val tags: List<String> = emptyList(),
|
|
327
|
+
val featureFile: String? = null,
|
|
328
|
+
val parallel: Boolean = false
|
|
329
|
+
)
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
---
|
|
333
|
+
|
|
334
|
+
## 🎨 UI Components
|
|
335
|
+
|
|
336
|
+
### MCPToolWindow.kt
|
|
337
|
+
```kotlin
|
|
338
|
+
package com.hakalab.mcp.ui
|
|
339
|
+
|
|
340
|
+
import com.intellij.openapi.project.Project
|
|
341
|
+
import com.intellij.openapi.wm.ToolWindow
|
|
342
|
+
import com.intellij.openapi.wm.ToolWindowFactory
|
|
343
|
+
import com.intellij.ui.content.ContentFactory
|
|
344
|
+
import javax.swing.*
|
|
345
|
+
import java.awt.*
|
|
346
|
+
import java.awt.event.ActionEvent
|
|
347
|
+
|
|
348
|
+
class MCPToolWindowFactory : ToolWindowFactory {
|
|
349
|
+
override fun createToolWindowContent(project: Project, toolWindow: ToolWindow) {
|
|
350
|
+
val mcpToolWindow = MCPToolWindow(project)
|
|
351
|
+
val content = ContentFactory.SERVICE.getInstance()
|
|
352
|
+
.createContent(mcpToolWindow.getContent(), "", false)
|
|
353
|
+
toolWindow.contentManager.addContent(content)
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
class MCPToolWindow(private val project: Project) {
|
|
358
|
+
private val mcpClient = MCPClient(MCPConfig())
|
|
359
|
+
private val toolExecutor = ToolExecutor(project, mcpClient)
|
|
360
|
+
|
|
361
|
+
fun getContent(): JComponent {
|
|
362
|
+
val panel = JPanel(BorderLayout())
|
|
363
|
+
|
|
364
|
+
// Create tabbed pane
|
|
365
|
+
val tabbedPane = JTabbedPane()
|
|
366
|
+
|
|
367
|
+
// Browser Automation Tab
|
|
368
|
+
tabbedPane.addTab("Browser Automation", createBrowserAutomationPanel())
|
|
369
|
+
|
|
370
|
+
// Test Creation Tab
|
|
371
|
+
tabbedPane.addTab("Test Creation", createTestCreationPanel())
|
|
372
|
+
|
|
373
|
+
// Test Execution Tab
|
|
374
|
+
tabbedPane.addTab("Test Execution", createTestExecutionPanel())
|
|
375
|
+
|
|
376
|
+
// Tools Tab
|
|
377
|
+
tabbedPane.addTab("Available Tools", createToolsPanel())
|
|
378
|
+
|
|
379
|
+
panel.add(tabbedPane, BorderLayout.CENTER)
|
|
380
|
+
|
|
381
|
+
return panel
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
private fun createBrowserAutomationPanel(): JPanel {
|
|
385
|
+
val panel = JPanel(GridBagLayout())
|
|
386
|
+
val gbc = GridBagConstraints()
|
|
387
|
+
|
|
388
|
+
// Engine selection
|
|
389
|
+
gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST
|
|
390
|
+
panel.add(JLabel("Engine:"), gbc)
|
|
391
|
+
|
|
392
|
+
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
|
|
393
|
+
val engineCombo = JComboBox(arrayOf("selenium", "playwright"))
|
|
394
|
+
panel.add(engineCombo, gbc)
|
|
395
|
+
|
|
396
|
+
// Browser selection
|
|
397
|
+
gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE
|
|
398
|
+
panel.add(JLabel("Browser:"), gbc)
|
|
399
|
+
|
|
400
|
+
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
|
|
401
|
+
val browserCombo = JComboBox(arrayOf("chrome", "firefox", "edge", "safari"))
|
|
402
|
+
panel.add(browserCombo, gbc)
|
|
403
|
+
|
|
404
|
+
// URL field
|
|
405
|
+
gbc.gridx = 0; gbc.gridy = 2; gbc.fill = GridBagConstraints.NONE
|
|
406
|
+
panel.add(JLabel("URL:"), gbc)
|
|
407
|
+
|
|
408
|
+
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
|
|
409
|
+
val urlField = JTextField("https://")
|
|
410
|
+
panel.add(urlField, gbc)
|
|
411
|
+
|
|
412
|
+
// Actions area
|
|
413
|
+
gbc.gridx = 0; gbc.gridy = 3; gbc.gridwidth = 2; gbc.fill = GridBagConstraints.BOTH
|
|
414
|
+
val actionsArea = JTextArea(10, 40)
|
|
415
|
+
actionsArea.text = """
|
|
416
|
+
# Example automation script:
|
|
417
|
+
# 1. Open browser
|
|
418
|
+
# 2. Navigate to URL
|
|
419
|
+
# 3. Fill form fields
|
|
420
|
+
# 4. Click submit
|
|
421
|
+
# 5. Take screenshot
|
|
422
|
+
# 6. Close browser
|
|
423
|
+
""".trimIndent()
|
|
424
|
+
panel.add(JScrollPane(actionsArea), gbc)
|
|
425
|
+
|
|
426
|
+
// Execute button
|
|
427
|
+
gbc.gridy = 4; gbc.fill = GridBagConstraints.NONE
|
|
428
|
+
val executeButton = JButton("Execute Automation")
|
|
429
|
+
executeButton.addActionListener { executeAutomation(engineCombo, browserCombo, urlField, actionsArea) }
|
|
430
|
+
panel.add(executeButton, gbc)
|
|
431
|
+
|
|
432
|
+
return panel
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
private fun createTestCreationPanel(): JPanel {
|
|
436
|
+
val panel = JPanel(GridBagLayout())
|
|
437
|
+
val gbc = GridBagConstraints()
|
|
438
|
+
|
|
439
|
+
// Feature name
|
|
440
|
+
gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST
|
|
441
|
+
panel.add(JLabel("Feature Name:"), gbc)
|
|
442
|
+
|
|
443
|
+
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
|
|
444
|
+
val featureNameField = JTextField()
|
|
445
|
+
panel.add(featureNameField, gbc)
|
|
446
|
+
|
|
447
|
+
// Scenarios
|
|
448
|
+
gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE
|
|
449
|
+
panel.add(JLabel("Scenarios:"), gbc)
|
|
450
|
+
|
|
451
|
+
gbc.gridx = 1; gbc.gridy = 1; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 0.5
|
|
452
|
+
val scenariosArea = JTextArea()
|
|
453
|
+
scenariosArea.text = """
|
|
454
|
+
Scenario: User login
|
|
455
|
+
Given I am on the login page
|
|
456
|
+
When I enter valid credentials
|
|
457
|
+
Then I should be logged in
|
|
458
|
+
""".trimIndent()
|
|
459
|
+
panel.add(JScrollPane(scenariosArea), gbc)
|
|
460
|
+
|
|
461
|
+
// Page elements
|
|
462
|
+
gbc.gridy = 2
|
|
463
|
+
val elementsArea = JTextArea()
|
|
464
|
+
elementsArea.text = """
|
|
465
|
+
username_field,ID,username
|
|
466
|
+
password_field,ID,password
|
|
467
|
+
login_button,CSS,.login-btn
|
|
468
|
+
""".trimIndent()
|
|
469
|
+
panel.add(JScrollPane(elementsArea), gbc)
|
|
470
|
+
|
|
471
|
+
// Create button
|
|
472
|
+
gbc.gridy = 3; gbc.fill = GridBagConstraints.NONE; gbc.weighty = 0.0
|
|
473
|
+
val createButton = JButton("Create Test Suite")
|
|
474
|
+
createButton.addActionListener { createTestSuite(featureNameField, scenariosArea, elementsArea) }
|
|
475
|
+
panel.add(createButton, gbc)
|
|
476
|
+
|
|
477
|
+
return panel
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
private fun createTestExecutionPanel(): JPanel {
|
|
481
|
+
val panel = JPanel(GridBagLayout())
|
|
482
|
+
val gbc = GridBagConstraints()
|
|
483
|
+
|
|
484
|
+
// Tags filter
|
|
485
|
+
gbc.gridx = 0; gbc.gridy = 0; gbc.anchor = GridBagConstraints.WEST
|
|
486
|
+
panel.add(JLabel("Tags:"), gbc)
|
|
487
|
+
|
|
488
|
+
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
|
|
489
|
+
val tagsField = JTextField("selenium,critical")
|
|
490
|
+
panel.add(tagsField, gbc)
|
|
491
|
+
|
|
492
|
+
// Feature file
|
|
493
|
+
gbc.gridx = 0; gbc.gridy = 1; gbc.fill = GridBagConstraints.NONE
|
|
494
|
+
panel.add(JLabel("Feature File:"), gbc)
|
|
495
|
+
|
|
496
|
+
gbc.gridx = 1; gbc.fill = GridBagConstraints.HORIZONTAL
|
|
497
|
+
val featureFileField = JTextField()
|
|
498
|
+
panel.add(featureFileField, gbc)
|
|
499
|
+
|
|
500
|
+
// Parallel execution
|
|
501
|
+
gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2
|
|
502
|
+
val parallelCheckbox = JCheckBox("Parallel Execution")
|
|
503
|
+
panel.add(parallelCheckbox, gbc)
|
|
504
|
+
|
|
505
|
+
// Execute button
|
|
506
|
+
gbc.gridy = 3
|
|
507
|
+
val executeButton = JButton("Execute Tests")
|
|
508
|
+
executeButton.addActionListener { executeTests(tagsField, featureFileField, parallelCheckbox) }
|
|
509
|
+
panel.add(executeButton, gbc)
|
|
510
|
+
|
|
511
|
+
// Results area
|
|
512
|
+
gbc.gridy = 4; gbc.fill = GridBagConstraints.BOTH; gbc.weightx = 1.0; gbc.weighty = 1.0
|
|
513
|
+
val resultsArea = JTextArea()
|
|
514
|
+
resultsArea.isEditable = false
|
|
515
|
+
panel.add(JScrollPane(resultsArea), gbc)
|
|
516
|
+
|
|
517
|
+
return panel
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
private fun createToolsPanel(): JPanel {
|
|
521
|
+
val panel = JPanel(BorderLayout())
|
|
522
|
+
|
|
523
|
+
// Tools list
|
|
524
|
+
val toolsModel = DefaultListModel<String>()
|
|
525
|
+
val toolsList = JList(toolsModel)
|
|
526
|
+
|
|
527
|
+
// Load available tools
|
|
528
|
+
SwingUtilities.invokeLater {
|
|
529
|
+
loadAvailableTools(toolsModel)
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
panel.add(JScrollPane(toolsList), BorderLayout.CENTER)
|
|
533
|
+
|
|
534
|
+
// Tool details
|
|
535
|
+
val detailsArea = JTextArea()
|
|
536
|
+
detailsArea.isEditable = false
|
|
537
|
+
panel.add(JScrollPane(detailsArea), BorderLayout.SOUTH)
|
|
538
|
+
|
|
539
|
+
// Show tool details on selection
|
|
540
|
+
toolsList.addListSelectionListener { e ->
|
|
541
|
+
if (!e.valueIsAdjusting) {
|
|
542
|
+
val selectedTool = toolsList.selectedValue
|
|
543
|
+
if (selectedTool != null) {
|
|
544
|
+
showToolDetails(selectedTool, detailsArea)
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return panel
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
private fun executeAutomation(
|
|
553
|
+
engineCombo: JComboBox<String>,
|
|
554
|
+
browserCombo: JComboBox<String>,
|
|
555
|
+
urlField: JTextField,
|
|
556
|
+
actionsArea: JTextArea
|
|
557
|
+
) {
|
|
558
|
+
val workflow = BrowserWorkflow(
|
|
559
|
+
name = "Custom Automation",
|
|
560
|
+
browserConfig = mapOf(
|
|
561
|
+
"engine" to engineCombo.selectedItem,
|
|
562
|
+
"browser" to browserCombo.selectedItem
|
|
563
|
+
),
|
|
564
|
+
steps = parseAutomationSteps(actionsArea.text, urlField.text)
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
toolExecutor.executeBrowserAutomation(workflow)
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
private fun createTestSuite(
|
|
571
|
+
featureNameField: JTextField,
|
|
572
|
+
scenariosArea: JTextArea,
|
|
573
|
+
elementsArea: JTextArea
|
|
574
|
+
) {
|
|
575
|
+
val config = TestSuiteConfig(
|
|
576
|
+
featureName = featureNameField.text,
|
|
577
|
+
scenarios = parseScenarios(scenariosArea.text),
|
|
578
|
+
pageElements = parsePageElements(elementsArea.text),
|
|
579
|
+
tags = listOf("automated", "ui")
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
toolExecutor.createTestSuite(config)
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private fun executeTests(
|
|
586
|
+
tagsField: JTextField,
|
|
587
|
+
featureFileField: JTextField,
|
|
588
|
+
parallelCheckbox: JCheckBox
|
|
589
|
+
) {
|
|
590
|
+
val config = TestExecutionConfig(
|
|
591
|
+
tags = tagsField.text.split(",").map { it.trim() },
|
|
592
|
+
featureFile = featureFileField.text.takeIf { it.isNotBlank() },
|
|
593
|
+
parallel = parallelCheckbox.isSelected
|
|
594
|
+
)
|
|
595
|
+
|
|
596
|
+
toolExecutor.executeTests(config)
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
private fun loadAvailableTools(model: DefaultListModel<String>) {
|
|
600
|
+
// Load tools asynchronously
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
private fun showToolDetails(toolName: String, detailsArea: JTextArea) {
|
|
604
|
+
// Show tool details
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
private fun parseAutomationSteps(actionsText: String, url: String): List<WorkflowStep> {
|
|
608
|
+
// Parse automation steps from text
|
|
609
|
+
return listOf(
|
|
610
|
+
WorkflowStep("navigate_to_url", mapOf("url" to url), "Navigate to URL")
|
|
611
|
+
)
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
private fun parseScenarios(scenariosText: String): List<Map<String, Any>> {
|
|
615
|
+
// Parse scenarios from text
|
|
616
|
+
return emptyList()
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private fun parsePageElements(elementsText: String): List<Map<String, Any>> {
|
|
620
|
+
// Parse page elements from CSV-like text
|
|
621
|
+
return emptyList()
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
628
|
+
## 🤖 AI Agent Integration
|
|
629
|
+
|
|
630
|
+
### AIAssistant.kt
|
|
631
|
+
```kotlin
|
|
632
|
+
package com.hakalab.mcp.ai
|
|
633
|
+
|
|
634
|
+
import com.intellij.openapi.editor.Editor
|
|
635
|
+
import com.intellij.openapi.project.Project
|
|
636
|
+
import com.intellij.psi.PsiFile
|
|
637
|
+
|
|
638
|
+
class AIAssistant(
|
|
639
|
+
private val project: Project,
|
|
640
|
+
private val mcpClient: MCPClient
|
|
641
|
+
) {
|
|
642
|
+
|
|
643
|
+
fun generateTestFromCode(psiFile: PsiFile, editor: Editor): String {
|
|
644
|
+
val codeContext = analyzeCode(psiFile, editor)
|
|
645
|
+
|
|
646
|
+
return when (codeContext.type) {
|
|
647
|
+
CodeType.WEB_PAGE -> generateWebPageTest(codeContext)
|
|
648
|
+
CodeType.API_ENDPOINT -> generateAPITest(codeContext)
|
|
649
|
+
CodeType.FORM_HANDLER -> generateFormTest(codeContext)
|
|
650
|
+
else -> generateGenericTest(codeContext)
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
fun suggestAutomationSteps(context: String): List<AutomationSuggestion> {
|
|
655
|
+
val suggestions = mutableListOf<AutomationSuggestion>()
|
|
656
|
+
|
|
657
|
+
// Analyze context and suggest appropriate MCP tools
|
|
658
|
+
when {
|
|
659
|
+
context.contains("login", ignoreCase = true) -> {
|
|
660
|
+
suggestions.addAll(getLoginAutomationSuggestions())
|
|
661
|
+
}
|
|
662
|
+
context.contains("form", ignoreCase = true) -> {
|
|
663
|
+
suggestions.addAll(getFormAutomationSuggestions())
|
|
664
|
+
}
|
|
665
|
+
context.contains("navigation", ignoreCase = true) -> {
|
|
666
|
+
suggestions.addAll(getNavigationAutomationSuggestions())
|
|
667
|
+
}
|
|
668
|
+
context.contains("test", ignoreCase = true) -> {
|
|
669
|
+
suggestions.addAll(getTestCreationSuggestions())
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
|
|
673
|
+
return suggestions
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
fun generatePageObject(htmlContent: String): PageObjectCode {
|
|
677
|
+
val elements = extractElementsFromHTML(htmlContent)
|
|
678
|
+
|
|
679
|
+
val pageObjectCode = buildString {
|
|
680
|
+
appendLine("from selenium.webdriver.common.by import By")
|
|
681
|
+
appendLine()
|
|
682
|
+
appendLine("class GeneratedPage:")
|
|
683
|
+
appendLine(" def __init__(self, browser):")
|
|
684
|
+
appendLine(" self.browser = browser")
|
|
685
|
+
appendLine()
|
|
686
|
+
|
|
687
|
+
elements.forEach { element ->
|
|
688
|
+
appendLine(" ${element.name} = (By.${element.locatorType}, \"${element.locator}\")")
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
return PageObjectCode(
|
|
693
|
+
className = "GeneratedPage",
|
|
694
|
+
code = pageObjectCode,
|
|
695
|
+
elements = elements
|
|
696
|
+
)
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
fun optimizeSelector(originalSelector: String, context: ElementContext): SelectorOptimization {
|
|
700
|
+
val alternatives = generateSelectorAlternatives(originalSelector, context)
|
|
701
|
+
|
|
702
|
+
return SelectorOptimization(
|
|
703
|
+
original = originalSelector,
|
|
704
|
+
recommended = alternatives.first(),
|
|
705
|
+
alternatives = alternatives,
|
|
706
|
+
reasoning = "Optimized for stability and performance"
|
|
707
|
+
)
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
private fun analyzeCode(psiFile: PsiFile, editor: Editor): CodeContext {
|
|
711
|
+
// Analyze the code to understand its purpose and structure
|
|
712
|
+
return CodeContext(
|
|
713
|
+
type = CodeType.WEB_PAGE,
|
|
714
|
+
elements = emptyList(),
|
|
715
|
+
actions = emptyList(),
|
|
716
|
+
metadata = emptyMap()
|
|
717
|
+
)
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
private fun generateWebPageTest(context: CodeContext): String {
|
|
721
|
+
return buildString {
|
|
722
|
+
appendLine("Feature: ${context.getFeatureName()}")
|
|
723
|
+
appendLine(" Automated test for web page functionality")
|
|
724
|
+
appendLine()
|
|
725
|
+
appendLine(" Scenario: Basic page interaction")
|
|
726
|
+
appendLine(" Given I open the browser")
|
|
727
|
+
appendLine(" When I navigate to the page")
|
|
728
|
+
appendLine(" Then I should see the expected elements")
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
private fun getLoginAutomationSuggestions(): List<AutomationSuggestion> {
|
|
733
|
+
return listOf(
|
|
734
|
+
AutomationSuggestion(
|
|
735
|
+
tool = "open_browser",
|
|
736
|
+
description = "Open browser for login testing",
|
|
737
|
+
parameters = mapOf("browser" to "chrome", "engine" to "playwright"),
|
|
738
|
+
priority = 1
|
|
739
|
+
),
|
|
740
|
+
AutomationSuggestion(
|
|
741
|
+
tool = "navigate_to_url",
|
|
742
|
+
description = "Navigate to login page",
|
|
743
|
+
parameters = mapOf("url" to "https://example.com/login"),
|
|
744
|
+
priority = 2
|
|
745
|
+
),
|
|
746
|
+
AutomationSuggestion(
|
|
747
|
+
tool = "fill_input",
|
|
748
|
+
description = "Fill username field",
|
|
749
|
+
parameters = mapOf("selector" to "#username", "text" to "testuser"),
|
|
750
|
+
priority = 3
|
|
751
|
+
),
|
|
752
|
+
AutomationSuggestion(
|
|
753
|
+
tool = "fill_input",
|
|
754
|
+
description = "Fill password field",
|
|
755
|
+
parameters = mapOf("selector" to "#password", "text" to "password123"),
|
|
756
|
+
priority = 4
|
|
757
|
+
),
|
|
758
|
+
AutomationSuggestion(
|
|
759
|
+
tool = "click_element",
|
|
760
|
+
description = "Click login button",
|
|
761
|
+
parameters = mapOf("selector" to "button[type='submit']"),
|
|
762
|
+
priority = 5
|
|
763
|
+
),
|
|
764
|
+
AutomationSuggestion(
|
|
765
|
+
tool = "take_screenshot",
|
|
766
|
+
description = "Capture login result",
|
|
767
|
+
parameters = mapOf("filename" to "login-result.png"),
|
|
768
|
+
priority = 6
|
|
769
|
+
)
|
|
770
|
+
)
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
private fun getFormAutomationSuggestions(): List<AutomationSuggestion> {
|
|
774
|
+
return listOf(
|
|
775
|
+
AutomationSuggestion(
|
|
776
|
+
tool = "fill_input",
|
|
777
|
+
description = "Fill form fields",
|
|
778
|
+
parameters = mapOf("selector" to "input[name='field']", "text" to "value"),
|
|
779
|
+
priority = 1
|
|
780
|
+
),
|
|
781
|
+
AutomationSuggestion(
|
|
782
|
+
tool = "select_option",
|
|
783
|
+
description = "Select dropdown option",
|
|
784
|
+
parameters = mapOf("selector" to "select[name='dropdown']", "value" to "option1"),
|
|
785
|
+
priority = 2
|
|
786
|
+
),
|
|
787
|
+
AutomationSuggestion(
|
|
788
|
+
tool = "click_element",
|
|
789
|
+
description = "Submit form",
|
|
790
|
+
parameters = mapOf("selector" to "input[type='submit']"),
|
|
791
|
+
priority = 3
|
|
792
|
+
)
|
|
793
|
+
)
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
private fun getNavigationAutomationSuggestions(): List<AutomationSuggestion> {
|
|
797
|
+
return listOf(
|
|
798
|
+
AutomationSuggestion(
|
|
799
|
+
tool = "navigate_to_url",
|
|
800
|
+
description = "Navigate to page",
|
|
801
|
+
parameters = mapOf("url" to "https://example.com"),
|
|
802
|
+
priority = 1
|
|
803
|
+
),
|
|
804
|
+
AutomationSuggestion(
|
|
805
|
+
tool = "click_element",
|
|
806
|
+
description = "Click navigation link",
|
|
807
|
+
parameters = mapOf("selector" to "a[href='/page']"),
|
|
808
|
+
priority = 2
|
|
809
|
+
),
|
|
810
|
+
AutomationSuggestion(
|
|
811
|
+
tool = "go_back",
|
|
812
|
+
description = "Navigate back",
|
|
813
|
+
parameters = emptyMap(),
|
|
814
|
+
priority = 3
|
|
815
|
+
)
|
|
816
|
+
)
|
|
817
|
+
}
|
|
818
|
+
|
|
819
|
+
private fun getTestCreationSuggestions(): List<AutomationSuggestion> {
|
|
820
|
+
return listOf(
|
|
821
|
+
AutomationSuggestion(
|
|
822
|
+
tool = "create_test_suite",
|
|
823
|
+
description = "Create comprehensive test suite",
|
|
824
|
+
parameters = mapOf(
|
|
825
|
+
"feature_name" to "New Feature",
|
|
826
|
+
"scenarios" to listOf(mapOf("name" to "Basic scenario"))
|
|
827
|
+
),
|
|
828
|
+
priority = 1
|
|
829
|
+
),
|
|
830
|
+
AutomationSuggestion(
|
|
831
|
+
tool = "create_page_object",
|
|
832
|
+
description = "Create page object model",
|
|
833
|
+
parameters = mapOf(
|
|
834
|
+
"page_name" to "HomePage",
|
|
835
|
+
"elements" to listOf(mapOf("name" to "element", "type" to "ID", "locator" to "id"))
|
|
836
|
+
),
|
|
837
|
+
priority = 2
|
|
838
|
+
)
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
private fun extractElementsFromHTML(htmlContent: String): List<PageElement> {
|
|
843
|
+
// Parse HTML and extract interactive elements
|
|
844
|
+
return emptyList()
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
private fun generateSelectorAlternatives(selector: String, context: ElementContext): List<String> {
|
|
848
|
+
// Generate alternative selectors based on context
|
|
849
|
+
return listOf(selector)
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
data class CodeContext(
|
|
854
|
+
val type: CodeType,
|
|
855
|
+
val elements: List<String>,
|
|
856
|
+
val actions: List<String>,
|
|
857
|
+
val metadata: Map<String, Any>
|
|
858
|
+
) {
|
|
859
|
+
fun getFeatureName(): String = metadata["featureName"] as? String ?: "Generated Feature"
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
enum class CodeType {
|
|
863
|
+
WEB_PAGE, API_ENDPOINT, FORM_HANDLER, GENERIC
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
data class AutomationSuggestion(
|
|
867
|
+
val tool: String,
|
|
868
|
+
val description: String,
|
|
869
|
+
val parameters: Map<String, Any>,
|
|
870
|
+
val priority: Int
|
|
871
|
+
)
|
|
872
|
+
|
|
873
|
+
data class PageObjectCode(
|
|
874
|
+
val className: String,
|
|
875
|
+
val code: String,
|
|
876
|
+
val elements: List<PageElement>
|
|
877
|
+
)
|
|
878
|
+
|
|
879
|
+
data class PageElement(
|
|
880
|
+
val name: String,
|
|
881
|
+
val locatorType: String,
|
|
882
|
+
val locator: String
|
|
883
|
+
)
|
|
884
|
+
|
|
885
|
+
data class ElementContext(
|
|
886
|
+
val tagName: String,
|
|
887
|
+
val attributes: Map<String, String>,
|
|
888
|
+
val parentContext: String
|
|
889
|
+
)
|
|
890
|
+
|
|
891
|
+
data class SelectorOptimization(
|
|
892
|
+
val original: String,
|
|
893
|
+
val recommended: String,
|
|
894
|
+
val alternatives: List<String>,
|
|
895
|
+
val reasoning: String
|
|
896
|
+
)
|
|
897
|
+
```
|
|
898
|
+
|
|
899
|
+
---
|
|
900
|
+
|
|
901
|
+
## 🔧 Code Generation
|
|
902
|
+
|
|
903
|
+
### CodeGenerator.kt
|
|
904
|
+
```kotlin
|
|
905
|
+
package com.hakalab.mcp.codegen
|
|
906
|
+
|
|
907
|
+
import com.intellij.openapi.command.WriteCommandAction
|
|
908
|
+
import com.intellij.openapi.editor.Editor
|
|
909
|
+
import com.intellij.openapi.project.Project
|
|
910
|
+
import com.intellij.psi.PsiFile
|
|
911
|
+
import com.intellij.psi.PsiFileFactory
|
|
912
|
+
|
|
913
|
+
class CodeGenerator(private val project: Project) {
|
|
914
|
+
|
|
915
|
+
fun generateBDDFeature(config: FeatureConfig): String {
|
|
916
|
+
return buildString {
|
|
917
|
+
// Feature header
|
|
918
|
+
if (config.tags.isNotEmpty()) {
|
|
919
|
+
appendLine(config.tags.joinToString(" ") { "@$it" })
|
|
920
|
+
}
|
|
921
|
+
appendLine("Feature: ${config.name}")
|
|
922
|
+
appendLine(" ${config.description}")
|
|
923
|
+
appendLine()
|
|
924
|
+
|
|
925
|
+
// Background (if any)
|
|
926
|
+
config.background?.let { background ->
|
|
927
|
+
appendLine(" Background:")
|
|
928
|
+
background.steps.forEach { step ->
|
|
929
|
+
appendLine(" $step")
|
|
930
|
+
}
|
|
931
|
+
appendLine()
|
|
932
|
+
}
|
|
933
|
+
|
|
934
|
+
// Scenarios
|
|
935
|
+
config.scenarios.forEach { scenario ->
|
|
936
|
+
if (scenario.tags.isNotEmpty()) {
|
|
937
|
+
appendLine(" ${scenario.tags.joinToString(" ") { "@$it" }}")
|
|
938
|
+
}
|
|
939
|
+
appendLine(" Scenario: ${scenario.name}")
|
|
940
|
+
if (scenario.description.isNotEmpty()) {
|
|
941
|
+
appendLine(" ${scenario.description}")
|
|
942
|
+
}
|
|
943
|
+
scenario.steps.forEach { step ->
|
|
944
|
+
appendLine(" $step")
|
|
945
|
+
}
|
|
946
|
+
appendLine()
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
fun generateStepDefinitions(feature: String, language: Language = Language.PYTHON): String {
|
|
952
|
+
val steps = extractStepsFromFeature(feature)
|
|
953
|
+
|
|
954
|
+
return when (language) {
|
|
955
|
+
Language.PYTHON -> generatePythonSteps(steps)
|
|
956
|
+
Language.JAVA -> generateJavaSteps(steps)
|
|
957
|
+
Language.JAVASCRIPT -> generateJavaScriptSteps(steps)
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
fun generatePageObject(elements: List<PageElement>, language: Language = Language.PYTHON): String {
|
|
962
|
+
return when (language) {
|
|
963
|
+
Language.PYTHON -> generatePythonPageObject(elements)
|
|
964
|
+
Language.JAVA -> generateJavaPageObject(elements)
|
|
965
|
+
Language.JAVASCRIPT -> generateJavaScriptPageObject(elements)
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
fun generateAutomationScript(workflow: AutomationWorkflow): String {
|
|
970
|
+
return buildString {
|
|
971
|
+
appendLine("# Generated automation script")
|
|
972
|
+
appendLine("# Workflow: ${workflow.name}")
|
|
973
|
+
appendLine()
|
|
974
|
+
|
|
975
|
+
// Imports
|
|
976
|
+
appendLine("import asyncio")
|
|
977
|
+
appendLine("from hakalab_mcp_client import MCPClient")
|
|
978
|
+
appendLine()
|
|
979
|
+
|
|
980
|
+
// Main function
|
|
981
|
+
appendLine("async def main():")
|
|
982
|
+
appendLine(" client = MCPClient()")
|
|
983
|
+
appendLine(" await client.connect()")
|
|
984
|
+
appendLine()
|
|
985
|
+
|
|
986
|
+
// Workflow steps
|
|
987
|
+
workflow.steps.forEach { step ->
|
|
988
|
+
appendLine(" # ${step.description}")
|
|
989
|
+
appendLine(" result = await client.call_tool('${step.toolName}', {")
|
|
990
|
+
step.parameters.forEach { (key, value) ->
|
|
991
|
+
appendLine(" '$key': ${formatValue(value)},")
|
|
992
|
+
}
|
|
993
|
+
appendLine(" })")
|
|
994
|
+
appendLine(" print(f'${step.toolName}: {result}')")
|
|
995
|
+
appendLine()
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
appendLine(" await client.disconnect()")
|
|
999
|
+
appendLine()
|
|
1000
|
+
appendLine("if __name__ == '__main__':")
|
|
1001
|
+
appendLine(" asyncio.run(main())")
|
|
1002
|
+
}
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
fun insertCodeIntoEditor(editor: Editor, code: String, insertAtCursor: Boolean = true) {
|
|
1006
|
+
WriteCommandAction.runWriteCommandAction(project) {
|
|
1007
|
+
val document = editor.document
|
|
1008
|
+
val offset = if (insertAtCursor) editor.caretModel.offset else document.textLength
|
|
1009
|
+
document.insertString(offset, code)
|
|
1010
|
+
}
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
fun createNewFile(fileName: String, content: String, directory: String = ""): PsiFile {
|
|
1014
|
+
val fileType = getFileTypeFromExtension(fileName)
|
|
1015
|
+
|
|
1016
|
+
return WriteCommandAction.computeAndWait<PsiFile, Exception> {
|
|
1017
|
+
PsiFileFactory.getInstance(project).createFileFromText(fileName, fileType, content)
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
private fun generatePythonSteps(steps: List<StepInfo>): String {
|
|
1022
|
+
return buildString {
|
|
1023
|
+
appendLine("from behave import given, when, then")
|
|
1024
|
+
appendLine("from assertpy import assert_that")
|
|
1025
|
+
appendLine("import asyncio")
|
|
1026
|
+
appendLine("from hakalab_mcp_client import MCPClient")
|
|
1027
|
+
appendLine()
|
|
1028
|
+
|
|
1029
|
+
steps.forEach { step ->
|
|
1030
|
+
val stepType = step.type.lowercase()
|
|
1031
|
+
val functionName = step.text
|
|
1032
|
+
.replace(Regex("[^\\w\\s]"), "")
|
|
1033
|
+
.replace(Regex("\\s+"), "_")
|
|
1034
|
+
.lowercase()
|
|
1035
|
+
|
|
1036
|
+
appendLine("@$stepType('${step.text}')")
|
|
1037
|
+
appendLine("def ${functionName}(context):")
|
|
1038
|
+
appendLine(" \"\"\"")
|
|
1039
|
+
appendLine(" Step: ${step.text}")
|
|
1040
|
+
appendLine(" \"\"\"")
|
|
1041
|
+
appendLine(" # TODO: Implement step logic using MCP tools")
|
|
1042
|
+
appendLine(" # Example:")
|
|
1043
|
+
appendLine(" # result = await context.mcp_client.call_tool('click_element', {")
|
|
1044
|
+
appendLine(" # 'selector': '#element-id'")
|
|
1045
|
+
appendLine(" # })")
|
|
1046
|
+
appendLine(" pass")
|
|
1047
|
+
appendLine()
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
|
|
1052
|
+
private fun generateJavaSteps(steps: List<StepInfo>): String {
|
|
1053
|
+
return buildString {
|
|
1054
|
+
appendLine("import io.cucumber.java.en.*;")
|
|
1055
|
+
appendLine("import static org.junit.Assert.*;")
|
|
1056
|
+
appendLine()
|
|
1057
|
+
appendLine("public class StepDefinitions {")
|
|
1058
|
+
appendLine()
|
|
1059
|
+
|
|
1060
|
+
steps.forEach { step ->
|
|
1061
|
+
val methodName = step.text
|
|
1062
|
+
.replace(Regex("[^\\w\\s]"), "")
|
|
1063
|
+
.replace(Regex("\\s+"), "_")
|
|
1064
|
+
.lowercase()
|
|
1065
|
+
|
|
1066
|
+
appendLine(" @${step.type}(\"${step.text}\")")
|
|
1067
|
+
appendLine(" public void ${methodName}() {")
|
|
1068
|
+
appendLine(" // TODO: Implement step logic using MCP tools")
|
|
1069
|
+
appendLine(" // Example MCP tool call would go here")
|
|
1070
|
+
appendLine(" }")
|
|
1071
|
+
appendLine()
|
|
1072
|
+
}
|
|
1073
|
+
|
|
1074
|
+
appendLine("}")
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
|
|
1078
|
+
private fun generateJavaScriptSteps(steps: List<StepInfo>): String {
|
|
1079
|
+
return buildString {
|
|
1080
|
+
appendLine("const { Given, When, Then } = require('@cucumber/cucumber');")
|
|
1081
|
+
appendLine("const { MCPClient } = require('hakalab-mcp-client');")
|
|
1082
|
+
appendLine()
|
|
1083
|
+
|
|
1084
|
+
steps.forEach { step ->
|
|
1085
|
+
val stepType = step.type
|
|
1086
|
+
appendLine("${stepType}('${step.text}', async function () {")
|
|
1087
|
+
appendLine(" // TODO: Implement step logic using MCP tools")
|
|
1088
|
+
appendLine(" // const result = await this.mcpClient.callTool('tool_name', {});")
|
|
1089
|
+
appendLine("});")
|
|
1090
|
+
appendLine()
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
private fun generatePythonPageObject(elements: List<PageElement>): String {
|
|
1096
|
+
return buildString {
|
|
1097
|
+
appendLine("from selenium.webdriver.common.by import By")
|
|
1098
|
+
appendLine()
|
|
1099
|
+
appendLine("class GeneratedPage:")
|
|
1100
|
+
appendLine(" def __init__(self, browser):")
|
|
1101
|
+
appendLine(" self.browser = browser")
|
|
1102
|
+
appendLine()
|
|
1103
|
+
|
|
1104
|
+
elements.forEach { element ->
|
|
1105
|
+
appendLine(" ${element.name} = (By.${element.locatorType}, \"${element.locator}\")")
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
appendLine()
|
|
1109
|
+
appendLine(" def wait_for_page_load(self):")
|
|
1110
|
+
appendLine(" # Wait for page to load completely")
|
|
1111
|
+
appendLine(" pass")
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
|
|
1115
|
+
private fun generateJavaPageObject(elements: List<PageElement>): String {
|
|
1116
|
+
return buildString {
|
|
1117
|
+
appendLine("import org.openqa.selenium.By;")
|
|
1118
|
+
appendLine("import org.openqa.selenium.WebDriver;")
|
|
1119
|
+
appendLine("import org.openqa.selenium.WebElement;")
|
|
1120
|
+
appendLine()
|
|
1121
|
+
appendLine("public class GeneratedPage {")
|
|
1122
|
+
appendLine(" private WebDriver driver;")
|
|
1123
|
+
appendLine()
|
|
1124
|
+
appendLine(" public GeneratedPage(WebDriver driver) {")
|
|
1125
|
+
appendLine(" this.driver = driver;")
|
|
1126
|
+
appendLine(" }")
|
|
1127
|
+
appendLine()
|
|
1128
|
+
|
|
1129
|
+
elements.forEach { element ->
|
|
1130
|
+
appendLine(" private By ${element.name}Locator = By.${element.locatorType.lowercase()}(\"${element.locator}\");")
|
|
1131
|
+
appendLine()
|
|
1132
|
+
appendLine(" public WebElement get${element.name.capitalize()}() {")
|
|
1133
|
+
appendLine(" return driver.findElement(${element.name}Locator);")
|
|
1134
|
+
appendLine(" }")
|
|
1135
|
+
appendLine()
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
appendLine("}")
|
|
1139
|
+
}
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
private fun generateJavaScriptPageObject(elements: List<PageElement>): String {
|
|
1143
|
+
return buildString {
|
|
1144
|
+
appendLine("class GeneratedPage {")
|
|
1145
|
+
appendLine(" constructor(page) {")
|
|
1146
|
+
appendLine(" this.page = page;")
|
|
1147
|
+
appendLine(" }")
|
|
1148
|
+
appendLine()
|
|
1149
|
+
|
|
1150
|
+
elements.forEach { element ->
|
|
1151
|
+
val selectorMethod = when (element.locatorType.uppercase()) {
|
|
1152
|
+
"ID" -> "page.locator('#${element.locator}')"
|
|
1153
|
+
"CSS_SELECTOR" -> "page.locator('${element.locator}')"
|
|
1154
|
+
"XPATH" -> "page.locator('xpath=${element.locator}')"
|
|
1155
|
+
else -> "page.locator('${element.locator}')"
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
appendLine(" get ${element.name}() {")
|
|
1159
|
+
appendLine(" return this.$selectorMethod;")
|
|
1160
|
+
appendLine(" }")
|
|
1161
|
+
appendLine()
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
appendLine("}")
|
|
1165
|
+
appendLine()
|
|
1166
|
+
appendLine("module.exports = GeneratedPage;")
|
|
1167
|
+
}
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
private fun extractStepsFromFeature(feature: String): List<StepInfo> {
|
|
1171
|
+
val steps = mutableListOf<StepInfo>()
|
|
1172
|
+
val stepPattern = Regex("^\\s*(Given|When|Then|And|But)\\s+(.+)$", RegexOption.MULTILINE)
|
|
1173
|
+
|
|
1174
|
+
stepPattern.findAll(feature).forEach { match ->
|
|
1175
|
+
val type = match.groupValues[1]
|
|
1176
|
+
val text = match.groupValues[2]
|
|
1177
|
+
steps.add(StepInfo(type, text))
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
return steps
|
|
1181
|
+
}
|
|
1182
|
+
|
|
1183
|
+
private fun formatValue(value: Any): String {
|
|
1184
|
+
return when (value) {
|
|
1185
|
+
is String -> "'$value'"
|
|
1186
|
+
is Number -> value.toString()
|
|
1187
|
+
is Boolean -> value.toString()
|
|
1188
|
+
else -> "'$value'"
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
private fun getFileTypeFromExtension(fileName: String): com.intellij.openapi.fileTypes.FileType {
|
|
1193
|
+
// Return appropriate file type based on extension
|
|
1194
|
+
return com.intellij.openapi.fileTypes.PlainTextFileType.INSTANCE
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
|
|
1198
|
+
data class FeatureConfig(
|
|
1199
|
+
val name: String,
|
|
1200
|
+
val description: String,
|
|
1201
|
+
val tags: List<String>,
|
|
1202
|
+
val background: BackgroundConfig?,
|
|
1203
|
+
val scenarios: List<ScenarioConfig>
|
|
1204
|
+
)
|
|
1205
|
+
|
|
1206
|
+
data class BackgroundConfig(
|
|
1207
|
+
val steps: List<String>
|
|
1208
|
+
)
|
|
1209
|
+
|
|
1210
|
+
data class ScenarioConfig(
|
|
1211
|
+
val name: String,
|
|
1212
|
+
val description: String,
|
|
1213
|
+
val tags: List<String>,
|
|
1214
|
+
val steps: List<String>
|
|
1215
|
+
)
|
|
1216
|
+
|
|
1217
|
+
data class StepInfo(
|
|
1218
|
+
val type: String,
|
|
1219
|
+
val text: String
|
|
1220
|
+
)
|
|
1221
|
+
|
|
1222
|
+
data class AutomationWorkflow(
|
|
1223
|
+
val name: String,
|
|
1224
|
+
val description: String,
|
|
1225
|
+
val steps: List<WorkflowStep>
|
|
1226
|
+
)
|
|
1227
|
+
|
|
1228
|
+
enum class Language {
|
|
1229
|
+
PYTHON, JAVA, JAVASCRIPT
|
|
1230
|
+
}
|
|
1231
|
+
```
|
|
1232
|
+
|
|
1233
|
+
---
|
|
1234
|
+
|
|
1235
|
+
## 🧪 Test Execution
|
|
1236
|
+
|
|
1237
|
+
### TestRunner.kt
|
|
1238
|
+
```kotlin
|
|
1239
|
+
package com.hakalab.mcp.testing
|
|
1240
|
+
|
|
1241
|
+
import com.intellij.execution.configurations.ConfigurationFactory
|
|
1242
|
+
import com.intellij.execution.configurations.ConfigurationType
|
|
1243
|
+
import com.intellij.execution.configurations.RunConfiguration
|
|
1244
|
+
import com.intellij.execution.configurations.RunConfigurationBase
|
|
1245
|
+
import com.intellij.openapi.project.Project
|
|
1246
|
+
import kotlinx.coroutines.runBlocking
|
|
1247
|
+
|
|
1248
|
+
class MCPTestRunner(
|
|
1249
|
+
private val project: Project,
|
|
1250
|
+
private val mcpClient: MCPClient
|
|
1251
|
+
) {
|
|
1252
|
+
|
|
1253
|
+
fun runSingleTest(testFile: String): TestResult {
|
|
1254
|
+
return runBlocking {
|
|
1255
|
+
try {
|
|
1256
|
+
val result = mcpClient.callTool("execute_tests", mapOf(
|
|
1257
|
+
"feature_file" to testFile
|
|
1258
|
+
))
|
|
1259
|
+
|
|
1260
|
+
if (result.error == null) {
|
|
1261
|
+
// Generate report
|
|
1262
|
+
mcpClient.callTool("generate_allure_report", emptyMap())
|
|
1263
|
+
|
|
1264
|
+
TestResult.success(
|
|
1265
|
+
testFile = testFile,
|
|
1266
|
+
output = result.result?.get("message") as? String ?: "Test completed",
|
|
1267
|
+
duration = 0L // TODO: Extract duration from result
|
|
1268
|
+
)
|
|
1269
|
+
} else {
|
|
1270
|
+
TestResult.failure(
|
|
1271
|
+
testFile = testFile,
|
|
1272
|
+
error = result.error.message,
|
|
1273
|
+
output = result.error.data as? String ?: ""
|
|
1274
|
+
)
|
|
1275
|
+
}
|
|
1276
|
+
} catch (e: Exception) {
|
|
1277
|
+
TestResult.failure(
|
|
1278
|
+
testFile = testFile,
|
|
1279
|
+
error = e.message ?: "Unknown error",
|
|
1280
|
+
output = e.stackTraceToString()
|
|
1281
|
+
)
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
fun runTestSuite(tags: List<String> = emptyList(), parallel: Boolean = false): TestSuiteResult {
|
|
1287
|
+
return runBlocking {
|
|
1288
|
+
try {
|
|
1289
|
+
val result = mcpClient.callTool("execute_tests", mapOf(
|
|
1290
|
+
"tags" to tags,
|
|
1291
|
+
"parallel" to parallel
|
|
1292
|
+
))
|
|
1293
|
+
|
|
1294
|
+
if (result.error == null) {
|
|
1295
|
+
// Generate report
|
|
1296
|
+
val reportResult = mcpClient.callTool("generate_allure_report", emptyMap())
|
|
1297
|
+
|
|
1298
|
+
TestSuiteResult.success(
|
|
1299
|
+
totalTests = 0, // TODO: Extract from result
|
|
1300
|
+
passedTests = 0,
|
|
1301
|
+
failedTests = 0,
|
|
1302
|
+
skippedTests = 0,
|
|
1303
|
+
duration = 0L,
|
|
1304
|
+
reportPath = extractReportPath(reportResult)
|
|
1305
|
+
)
|
|
1306
|
+
} else {
|
|
1307
|
+
TestSuiteResult.failure(
|
|
1308
|
+
error = result.error.message,
|
|
1309
|
+
output = result.error.data as? String ?: ""
|
|
1310
|
+
)
|
|
1311
|
+
}
|
|
1312
|
+
} catch (e: Exception) {
|
|
1313
|
+
TestSuiteResult.failure(
|
|
1314
|
+
error = e.message ?: "Unknown error",
|
|
1315
|
+
output = e.stackTraceToString()
|
|
1316
|
+
)
|
|
1317
|
+
}
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
fun validateTestFiles(): ValidationResult {
|
|
1322
|
+
return runBlocking {
|
|
1323
|
+
try {
|
|
1324
|
+
val result = mcpClient.callTool("sanitize_tests", emptyMap())
|
|
1325
|
+
|
|
1326
|
+
if (result.error == null) {
|
|
1327
|
+
ValidationResult.success(
|
|
1328
|
+
message = result.result?.get("message") as? String ?: "All tests are valid"
|
|
1329
|
+
)
|
|
1330
|
+
} else {
|
|
1331
|
+
ValidationResult.failure(
|
|
1332
|
+
issues = parseValidationIssues(result.error.data as? String ?: "")
|
|
1333
|
+
)
|
|
1334
|
+
}
|
|
1335
|
+
} catch (e: Exception) {
|
|
1336
|
+
ValidationResult.failure(
|
|
1337
|
+
issues = listOf(ValidationIssue(
|
|
1338
|
+
file = "unknown",
|
|
1339
|
+
line = 0,
|
|
1340
|
+
message = e.message ?: "Validation failed",
|
|
1341
|
+
severity = ValidationSeverity.ERROR
|
|
1342
|
+
))
|
|
1343
|
+
)
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
fun listAvailableTests(): List<TestFileInfo> {
|
|
1349
|
+
return runBlocking {
|
|
1350
|
+
try {
|
|
1351
|
+
val result = mcpClient.callTool("list_test_files", mapOf(
|
|
1352
|
+
"file_type" to "all"
|
|
1353
|
+
))
|
|
1354
|
+
|
|
1355
|
+
if (result.error == null) {
|
|
1356
|
+
parseTestFilesList(result.result?.get("message") as? String ?: "")
|
|
1357
|
+
} else {
|
|
1358
|
+
emptyList()
|
|
1359
|
+
}
|
|
1360
|
+
} catch (e: Exception) {
|
|
1361
|
+
emptyList()
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
private fun extractReportPath(reportResult: MCPResponse): String? {
|
|
1367
|
+
return reportResult.result?.get("report_path") as? String
|
|
1368
|
+
}
|
|
1369
|
+
|
|
1370
|
+
private fun parseValidationIssues(output: String): List<ValidationIssue> {
|
|
1371
|
+
// Parse validation output and extract issues
|
|
1372
|
+
return emptyList()
|
|
1373
|
+
}
|
|
1374
|
+
|
|
1375
|
+
private fun parseTestFilesList(output: String): List<TestFileInfo> {
|
|
1376
|
+
// Parse test files list output
|
|
1377
|
+
return emptyList()
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
|
|
1381
|
+
sealed class TestResult {
|
|
1382
|
+
data class Success(
|
|
1383
|
+
val testFile: String,
|
|
1384
|
+
val output: String,
|
|
1385
|
+
val duration: Long
|
|
1386
|
+
) : TestResult()
|
|
1387
|
+
|
|
1388
|
+
data class Failure(
|
|
1389
|
+
val testFile: String,
|
|
1390
|
+
val error: String,
|
|
1391
|
+
val output: String
|
|
1392
|
+
) : TestResult()
|
|
1393
|
+
|
|
1394
|
+
companion object {
|
|
1395
|
+
fun success(testFile: String, output: String, duration: Long) = Success(testFile, output, duration)
|
|
1396
|
+
fun failure(testFile: String, error: String, output: String) = Failure(testFile, error, output)
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
sealed class TestSuiteResult {
|
|
1401
|
+
data class Success(
|
|
1402
|
+
val totalTests: Int,
|
|
1403
|
+
val passedTests: Int,
|
|
1404
|
+
val failedTests: Int,
|
|
1405
|
+
val skippedTests: Int,
|
|
1406
|
+
val duration: Long,
|
|
1407
|
+
val reportPath: String?
|
|
1408
|
+
) : TestSuiteResult()
|
|
1409
|
+
|
|
1410
|
+
data class Failure(
|
|
1411
|
+
val error: String,
|
|
1412
|
+
val output: String
|
|
1413
|
+
) : TestSuiteResult()
|
|
1414
|
+
|
|
1415
|
+
companion object {
|
|
1416
|
+
fun success(
|
|
1417
|
+
totalTests: Int,
|
|
1418
|
+
passedTests: Int,
|
|
1419
|
+
failedTests: Int,
|
|
1420
|
+
skippedTests: Int,
|
|
1421
|
+
duration: Long,
|
|
1422
|
+
reportPath: String?
|
|
1423
|
+
) = Success(totalTests, passedTests, failedTests, skippedTests, duration, reportPath)
|
|
1424
|
+
|
|
1425
|
+
fun failure(error: String, output: String) = Failure(error, output)
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
|
|
1429
|
+
sealed class ValidationResult {
|
|
1430
|
+
data class Success(val message: String) : ValidationResult()
|
|
1431
|
+
data class Failure(val issues: List<ValidationIssue>) : ValidationResult()
|
|
1432
|
+
|
|
1433
|
+
companion object {
|
|
1434
|
+
fun success(message: String) = Success(message)
|
|
1435
|
+
fun failure(issues: List<ValidationIssue>) = Failure(issues)
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
data class ValidationIssue(
|
|
1440
|
+
val file: String,
|
|
1441
|
+
val line: Int,
|
|
1442
|
+
val message: String,
|
|
1443
|
+
val severity: ValidationSeverity
|
|
1444
|
+
)
|
|
1445
|
+
|
|
1446
|
+
enum class ValidationSeverity {
|
|
1447
|
+
ERROR, WARNING, INFO
|
|
1448
|
+
}
|
|
1449
|
+
|
|
1450
|
+
data class TestFileInfo(
|
|
1451
|
+
val name: String,
|
|
1452
|
+
val type: TestFileType,
|
|
1453
|
+
val path: String,
|
|
1454
|
+
val lastModified: Long
|
|
1455
|
+
)
|
|
1456
|
+
|
|
1457
|
+
enum class TestFileType {
|
|
1458
|
+
FEATURE, STEPS, PAGE_OBJECT
|
|
1459
|
+
}
|
|
1460
|
+
|
|
1461
|
+
// Run Configuration for MCP Tests
|
|
1462
|
+
class MCPTestConfiguration(
|
|
1463
|
+
project: Project,
|
|
1464
|
+
factory: ConfigurationFactory,
|
|
1465
|
+
name: String
|
|
1466
|
+
) : RunConfigurationBase<Any>(project, factory, name) {
|
|
1467
|
+
|
|
1468
|
+
var testFile: String = ""
|
|
1469
|
+
var tags: String = ""
|
|
1470
|
+
var parallel: Boolean = false
|
|
1471
|
+
|
|
1472
|
+
override fun getConfigurationEditor() = MCPTestConfigurationEditor()
|
|
1473
|
+
|
|
1474
|
+
override fun getState(executor: com.intellij.execution.Executor, environment: com.intellij.execution.runners.ExecutionEnvironment) =
|
|
1475
|
+
MCPTestRunState(environment, this)
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
class MCPTestConfigurationType : ConfigurationType {
|
|
1479
|
+
override fun getDisplayName() = "MCP Test"
|
|
1480
|
+
override fun getConfigurationTypeDescription() = "Run tests using Hakalab-MCP"
|
|
1481
|
+
override fun getIcon() = null // TODO: Add icon
|
|
1482
|
+
override fun getId() = "MCPTestConfiguration"
|
|
1483
|
+
override fun getConfigurationFactories() = arrayOf(MCPTestConfigurationFactory(this))
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
class MCPTestConfigurationFactory(type: ConfigurationType) : ConfigurationFactory(type) {
|
|
1487
|
+
override fun createTemplateConfiguration(project: Project): RunConfiguration {
|
|
1488
|
+
return MCPTestConfiguration(project, this, "MCP Test")
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
override fun getId() = "MCPTestConfigurationFactory"
|
|
1492
|
+
}
|
|
1493
|
+
```
|
|
1494
|
+
|
|
1495
|
+
---
|
|
1496
|
+
|
|
1497
|
+
Esta guía técnica proporciona todos los detalles necesarios para integrar Hakalab-MCP con plugins de IntelliJ IDEA y PyCharm, incluyendo ejemplos completos de código, configuraciones, y patrones de implementación para que la IA pueda utilizar efectivamente todas las capacidades del MCP.
|