@@ -5,7 +5,6 @@ import androidx.compose.foundation.layout.*
55import androidx.compose.foundation.lazy.LazyColumn
66import androidx.compose.foundation.lazy.items
77import androidx.compose.foundation.lazy.rememberLazyListState
8- import androidx.compose.foundation.text.BasicTextField
98import androidx.compose.material3.*
109import androidx.compose.runtime.*
1110import androidx.compose.ui.Alignment
@@ -19,10 +18,8 @@ import androidx.compose.ui.window.rememberWindowState
1918import cc.unitmesh.agent.e2etest.E2ETestAgent
2019import cc.unitmesh.agent.e2etest.E2ETestConfig
2120import cc.unitmesh.agent.e2etest.E2ETestInput
22- import cc.unitmesh.agent.e2etest.TestMemory
2321import cc.unitmesh.agent.e2etest.executor.*
24- import cc.unitmesh.agent.e2etest.model.*
25- import cc.unitmesh.agent.e2etest.perception.PageStateExtractor
22+ import cc.unitmesh.config.ConfigManager
2623import cc.unitmesh.llm.LLMService
2724import cc.unitmesh.llm.ModelConfig
2825import cc.unitmesh.llm.LLMProviderType
@@ -103,8 +100,8 @@ private fun E2ETestAgentDemoApp() {
103100 var logs by remember { mutableStateOf(listOf<LogEntry >()) }
104101 var testStatus by remember { mutableStateOf(" Ready" ) }
105102 var isRunning by remember { mutableStateOf(false ) }
106- var targetUrl by remember { mutableStateOf(" https://www.phodal .com" ) }
107- var testGoal by remember { mutableStateOf(" Navigate to the blog section and find an article about AI " ) }
103+ var targetUrl by remember { mutableStateOf(" https://www.google .com" ) }
104+ var testGoal by remember { mutableStateOf(" Search for 'Kotlin Multiplatform' and click on the first result " ) }
108105 var aiReasoning by remember { mutableStateOf(" " ) }
109106 var currentStep by remember { mutableStateOf(0 ) }
110107 var totalSteps by remember { mutableStateOf(0 ) }
@@ -123,6 +120,50 @@ private fun E2ETestAgentDemoApp() {
123120 val browserDriver = remember { bridge.asBrowserDriver() }
124121 val pageStateExtractor = remember { bridge.asPageStateExtractor() }
125122
123+ // Auto-run test on startup
124+ LaunchedEffect (Unit ) {
125+ println (" [E2ETestDemo] LaunchedEffect started, waiting for KCEF..." )
126+ // Wait for KCEF to initialize
127+ delay(3000 )
128+
129+ println (" [E2ETestDemo] Starting auto-test..." )
130+ addLog(" Auto-starting E2E test..." , LogType .INFO )
131+ testStatus = " Starting..."
132+
133+ isRunning = true
134+ logs = emptyList()
135+ aiReasoning = " "
136+ currentStep = 0
137+ totalSteps = 0
138+
139+ try {
140+ runAIE2ETest(
141+ bridge = bridge,
142+ browserDriver = browserDriver,
143+ pageStateExtractor = pageStateExtractor,
144+ targetUrl = targetUrl,
145+ testGoal = testGoal,
146+ onLog = { msg, type ->
147+ println (" [E2ETestDemo] $msg " )
148+ addLog(msg, type)
149+ },
150+ onStatusChange = { testStatus = it },
151+ onReasoningUpdate = { aiReasoning = it },
152+ onStepUpdate = { step, total ->
153+ currentStep = step
154+ totalSteps = total
155+ }
156+ )
157+ } catch (e: Exception ) {
158+ println (" [E2ETestDemo] Error: ${e.message} " )
159+ e.printStackTrace()
160+ }
161+
162+ isRunning = false
163+ addLog(" Test completed!" , LogType .SUCCESS )
164+ println (" [E2ETestDemo] Test completed!" )
165+ }
166+
126167 Row (modifier = Modifier .fillMaxSize()) {
127168 // Left panel: Controls and Logs
128169 Column (
@@ -323,43 +364,90 @@ private suspend fun runAIE2ETest(
323364 onStepUpdate : (Int , Int ) -> Unit
324365) {
325366 try {
326- // Step 1: Check for LLM API key
327- val apiKey = System .getenv(" OPENAI_API_KEY" )
328- ? : System .getenv(" ANTHROPIC_API_KEY" )
329- ? : System .getenv(" DEEPSEEK_API_KEY" )
330-
331- if (apiKey.isNullOrBlank()) {
332- onLog(" No LLM API key found. Using rule-based fallback." , LogType .WARNING )
333- onReasoningUpdate(" No API key configured. Set OPENAI_API_KEY, ANTHROPIC_API_KEY, or DEEPSEEK_API_KEY environment variable." )
334- runRuleBasedTest(bridge, browserDriver, pageStateExtractor, testGoal, onLog, onStatusChange, onStepUpdate)
335- return
367+ // Step 1: Load LLM config from ConfigManager (~/.autodev/config.yaml)
368+ onLog(" Loading LLM config from ~/.autodev/config.yaml..." , LogType .INFO )
369+
370+ val configWrapper = ConfigManager .load()
371+ val activeConfig = configWrapper.getActiveModelConfig()
372+
373+ val modelConfig: ModelConfig ? = if (activeConfig != null && configWrapper.isValid()) {
374+ onLog(" Found config: ${configWrapper.getActiveName()} (${activeConfig.provider} /${activeConfig.modelName} )" , LogType .SUCCESS )
375+ activeConfig
376+ } else {
377+ // Fallback to environment variables
378+ onLog(" No valid config in ~/.autodev/config.yaml, checking environment variables..." , LogType .INFO )
379+
380+ val apiKey = System .getenv(" OPENAI_API_KEY" )
381+ ? : System .getenv(" ANTHROPIC_API_KEY" )
382+ ? : System .getenv(" DEEPSEEK_API_KEY" )
383+
384+ if (apiKey.isNullOrBlank()) {
385+ null
386+ } else {
387+ val provider = when {
388+ System .getenv(" DEEPSEEK_API_KEY" ) != null -> LLMProviderType .DEEPSEEK
389+ System .getenv(" ANTHROPIC_API_KEY" ) != null -> LLMProviderType .ANTHROPIC
390+ else -> LLMProviderType .OPENAI
391+ }
392+
393+ val modelName = when (provider) {
394+ LLMProviderType .DEEPSEEK -> " deepseek-chat"
395+ LLMProviderType .ANTHROPIC -> " claude-3-5-sonnet-20241022"
396+ else -> " gpt-4o-mini"
397+ }
398+
399+ onLog(" Using environment variable: $provider / $modelName " , LogType .INFO )
400+
401+ ModelConfig (
402+ provider = provider,
403+ modelName = modelName,
404+ apiKey = apiKey,
405+ temperature = 0.3 ,
406+ maxTokens = 2048
407+ )
408+ }
336409 }
337410
338- val provider = when {
339- System .getenv(" DEEPSEEK_API_KEY" ) != null -> LLMProviderType .DEEPSEEK
340- System .getenv(" ANTHROPIC_API_KEY" ) != null -> LLMProviderType .ANTHROPIC
341- else -> LLMProviderType .OPENAI
411+ if (modelConfig == null ) {
412+ onLog(" No LLM API key found. Please configure in ~/.autodev/config.yaml or set environment variable." , LogType .ERROR )
413+ onStatusChange(" No API Key" )
414+ onReasoningUpdate(" No API key configured. Please configure in ~/.autodev/config.yaml or set OPENAI_API_KEY/ANTHROPIC_API_KEY/DEEPSEEK_API_KEY environment variable." )
415+ return
342416 }
343417
344- val modelName = when (provider) {
345- LLMProviderType .DEEPSEEK -> " deepseek-chat"
346- LLMProviderType .ANTHROPIC -> " claude-3-5-sonnet-20241022"
347- else -> " gpt-4o-mini"
418+ onLog(" Using LLM: ${modelConfig.provider} / ${modelConfig.modelName} " , LogType .INFO )
419+
420+ val llmService = LLMService .create(modelConfig)
421+
422+ // Step 2: Navigate to target URL first (required for bridge.isReady to become true)
423+ onLog(" Navigating to $targetUrl ..." , LogType .INFO )
424+ onStatusChange(" Navigating..." )
425+ bridge.navigateTo(targetUrl)
426+
427+ // Wait for page load - bridge.isReady becomes true after page loads
428+ var waitCount = 0
429+ while (! bridge.isReady.value && waitCount < 100 ) {
430+ delay(100 )
431+ waitCount++
348432 }
433+ delay(2000 ) // Extra time for rendering and JS bridge initialization
349434
350- onLog(" Using LLM: $provider / $modelName " , LogType .INFO )
435+ if (! bridge.isReady.value) {
436+ onLog(" Page failed to load within timeout" , LogType .ERROR )
437+ onStatusChange(" Page Load Failed" )
438+ onReasoningUpdate(" Failed to load page: $targetUrl " )
439+ return
440+ }
351441
352- val modelConfig = ModelConfig (
353- provider = provider,
354- modelName = modelName,
355- apiKey = apiKey,
356- temperature = 0.3 ,
357- maxTokens = 2048
358- )
442+ onLog(" Page loaded successfully" , LogType .SUCCESS )
359443
360- val llmService = LLMService .create(modelConfig)
444+ // Step 3: Refresh accessibility tree and actionable elements
445+ onLog(" Extracting page state..." , LogType .INFO )
446+ bridge.refreshAccessibilityTree()
447+ bridge.refreshActionableElements()
448+ delay(500 )
361449
362- // Step 2 : Create and initialize E2ETestAgent
450+ // Step 4 : Create and initialize E2ETestAgent
363451 onLog(" Initializing E2ETestAgent..." , LogType .INFO )
364452 onStatusChange(" Initializing Agent..." )
365453
@@ -379,9 +467,9 @@ private suspend fun runAIE2ETest(
379467 agent.initializeWithDriver(browserDriver, pageStateExtractor)
380468
381469 if (! agent.isAvailable) {
382- onLog(" E2ETestAgent is not available. Using fallback. " , LogType .WARNING )
383- onReasoningUpdate (" Agent initialization failed. Falling back to rule-based approach. " )
384- runRuleBasedTest(bridge, browserDriver, pageStateExtractor, testGoal, onLog, onStatusChange, onStepUpdate )
470+ onLog(" E2ETestAgent is not available" , LogType .ERROR )
471+ onStatusChange (" Agent Not Available " )
472+ onReasoningUpdate( " Agent initialization failed. Check browser driver and page state extractor. " )
385473 return
386474 }
387475
@@ -433,76 +521,6 @@ private suspend fun runAIE2ETest(
433521 }
434522}
435523
436- /* *
437- * Rule-based fallback when LLM is not available
438- */
439- private suspend fun runRuleBasedTest (
440- bridge : JvmWebEditBridge ,
441- browserDriver : BrowserDriver ,
442- pageStateExtractor : cc.unitmesh.agent.e2etest.perception.PageStateExtractor ,
443- testGoal : String ,
444- onLog : (String , LogType ) -> Unit ,
445- onStatusChange : (String ) -> Unit ,
446- onStepUpdate : (Int , Int ) -> Unit
447- ) {
448- onLog(" Running rule-based test for: $testGoal " , LogType .INFO )
449- onStatusChange(" Rule-based execution..." )
450-
451- val pageState = pageStateExtractor.extractPageState()
452- val executor = JvmBrowserActionExecutor .withDriver(browserDriver)
453-
454- // Simple heuristic: find elements matching keywords in the goal
455- val keywords = testGoal.lowercase().split(" " ).filter { it.length > 3 }
456-
457- val matchingElements = pageState.actionableElements.filter { element ->
458- val name = element.name?.lowercase() ? : " "
459- keywords.any { keyword -> name.contains(keyword) }
460- }
461-
462- if (matchingElements.isEmpty()) {
463- onLog(" No elements matching goal keywords found" , LogType .WARNING )
464- onStatusChange(" No matching elements" )
465- return
466- }
467-
468- onLog(" Found ${matchingElements.size} elements matching goal" , LogType .SUCCESS )
469- onStepUpdate(0 , matchingElements.size.coerceAtMost(3 ))
470-
471- // Click up to 3 matching elements
472- matchingElements.take(3 ).forEachIndexed { index, element ->
473- onStepUpdate(index + 1 , matchingElements.size.coerceAtMost(3 ))
474- onLog(" Clicking: [${element.tagId} ] ${element.name} " , LogType .INFO )
475-
476- val context = ActionExecutionContext (
477- tagMapping = pageState.actionableElements.associateBy { it.tagId }
478- )
479- executor.setContext(context)
480-
481- val result = executor.click(element.tagId, ClickOptions ())
482- if (result.success) {
483- onLog(" Click succeeded" , LogType .SUCCESS )
484- } else {
485- onLog(" Click failed: ${result.error} " , LogType .ERROR )
486- }
487-
488- delay(1000 )
489- }
490-
491- onStatusChange(" Rule-based test completed" )
492- }
493-
494- private fun getTargetIdFromAction (action : TestAction ): Int? {
495- return when (action) {
496- is TestAction .Click -> action.targetId
497- is TestAction .Type -> action.targetId
498- is TestAction .Hover -> action.targetId
499- is TestAction .Assert -> action.targetId
500- is TestAction .Select -> action.targetId
501- is TestAction .Scroll -> action.targetId
502- else -> null
503- }
504- }
505-
506524@Composable
507525private fun RestartView () {
508526 Box (modifier = Modifier .fillMaxSize(), contentAlignment = Alignment .Center ) {
0 commit comments