compose-ui-test-server
0.2.0indexedExpose an HTTP server enabling agents to control a running desktop app: click buttons, input text, wait for elements, capture screenshots, add custom endpoints, zero-configuration launcher.
Expose an HTTP server enabling agents to control a running desktop app: click buttons, input text, wait for elements, capture screenshots, add custom endpoints, zero-configuration launcher.
A library that enables coding agents and automation tools to control Compose Desktop applications at runtime via HTTP. Agents can click buttons, enter text, wait for UI elements, and capture screenshots through simple REST API calls.
This library is designed for AI coding agents (like Claude Code) to interact with running Compose Desktop applications. When enabled, the app exposes an HTTP server that agents can use to:
Add the dependency to your desktop source set in build.gradle.kts:
kotlin {
sourceSets {
@OptIn(org.jetbrains.compose.ExperimentalComposeLibrary::class)
val desktopMain by getting {
dependencies {
implementation("io.github.forketyfork:compose-ui-test-server:0.2.0")
// Required: Compose UI Test framework
implementation(compose.uiTest)
}
}
}
}
Replace your main() function with runApplication:
import io.github.forketyfork.composeuittest.WindowConfig
import io.github.forketyfork.composeuittest.runApplication
fun main() =
runApplication(
windowConfig = WindowConfig(
title = "My App",
minimumWidth = 1024,
minimumHeight = 768,
),
) {
App()
}
That's it! Your app now supports agent control with zero additional configuration.
For agents to interact with UI elements, add test tags:
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.testTag
Button(
onClick = { /* ... */ },
modifier = Modifier.testTag("login_button")
) {
Text("Login")
}
TextField(
value = username,
onValueChange = { username = it },
modifier = Modifier.testTag("username_field")
)
Normal mode (default):
./gradlew run
Agent-controlled mode:
COMPOSE_UI_TEST_SERVER_ENABLED=true ./gradlew run
When the environment variable is set, your app automatically starts with an HTTP server that agents can use to control the UI.
Once running in agent-controlled mode:
# Health check
curl http://localhost:54345/health
# Click a button by test tag
curl http://localhost:54345/onNodeWithTag/submit_button/performClick
# Enter text into a field
curl "http://localhost:54345/onNodeWithTag/username_field/performTextInput?text=myuser"
# Wait for an element to appear
curl "http://localhost:54345/waitUntilExactlyOneExists/tag/welcome_screen?timeout=5000"
# Capture a screenshot
curl "http://localhost:54345/captureScreenshot?path=/tmp/current_state.png"
| Variable | Description | Default |
|---|---|---|
COMPOSE_UI_TEST_SERVER_ENABLED | Enable agent-controlled mode |
For more control over the test server, pass a server configuration:
import io.github.forketyfork.composeuittest.ComposeUiTestServerConfig
import io.github.forketyfork.composeuittest.WindowConfig
import io.github.forketyfork.composeuittest.runApplication
fun main() =
runApplication(
windowConfig = WindowConfig(title = "My App"),
serverConfig = ComposeUiTestServerConfig(
port = 8080,
host = "0.0.0.0",
defaultScreenshotPath = "/tmp/app_screenshot.png",
defaultTimeout = 10_000L,
),
) {
App()
}
Add app-specific shortcuts for common agent workflows:
To use custom endpoints, use the lower-level API:
import androidx.compose.ui.test.runComposeUiTest
import androidx.compose.ui.window.Window
import androidx.compose.ui.window.application
import io.github.forketyfork.composeuittest.ComposeUiTestServer
import io.github.forketyfork.composeuittest.isTestServerEnabled
fun main() {
if (isTestServerEnabled()) {
runComposeUiTest {
setContent { App() }
ComposeUiTestServer()
.registerEndpoint(LoginEndpoint(, ))
.start()
.awaitTermination()
}
} {
application {
Window(onCloseRequest = ::exitApplication, title = ) {
App()
}
}
}
}
Currently supports Desktop (JVM) only due to Skia dependencies for screenshot capture.
This library includes a skill that teaches Claude Code how to control Compose Desktop apps and set up new projects. Install it to enable automatic UI control capabilities.
mkdir -p ~/.claude/skills/compose-ui-control
curl -o ~/.claude/skills/compose-ui-control/SKILL.md \
https://raw.githubusercontent.com/forketyfork/compose-ui-test-server/main/SKILL.md
mkdir -p .claude/skills/compose-ui-control
curl -o .claude/skills/compose-ui-control/SKILL.md \
https://raw.githubusercontent.com/forketyfork/compose-ui-test-server/main/SKILL.md
Once installed, Claude Code can:
/compose-ui-controlExample prompts that trigger the skill:
See SKILL.md for the full skill documentation that Claude Code uses.
MIT License - see LICENSE for details.
| Endpoint | Description |
|---|
GET /health | Health check - returns "OK" |
GET /onNodeWithTag/{tag}/performClick | Click element by test tag |
GET /onNodeWithTag/{tag}/performTextInput?text=... | Enter text into element |
GET /onNodeWithText/{text}/performClick | Click element by display text |
GET /waitUntilExactlyOneExists/tag/{tag}?timeout=5000 | Wait for element by tag |
GET /waitUntilExactlyOneExists/text/{text}?exact=true&timeout=5000 | Wait for element by text |
GET /waitForIdle | Wait for UI to become idle |
GET /captureScreenshot?path=/tmp/screenshot.png | Capture screenshot to file |
false |
COMPOSE_UI_TEST_SERVER_PORT | Server port | 54345 |
import io.github.forketyfork.composeuittest.TestEndpoint
import androidx.compose.ui.test.ComposeUiTest
import androidx.compose.ui.test.onNodeWithTag
import androidx.compose.ui.test.performTextInput
import androidx.compose.ui.test.performClick
import io.ktor.server.routing.Route
import io.ktor.server.routing.get
import io.ktor.server.response.respondText
class LoginEndpoint(
private val username: String,
private val password: String
) : TestEndpoint {
override fun Route.configure(composeTest: ComposeUiTest) {
get("/shortcuts/login") {
composeTest.onNodeWithTag("username").performTextInput(username)
composeTest.onNodeWithTag("password").performTextInput(password)
composeTest.onNodeWithTag("login_button").performClick()
call.respondText("Login completed")
}
}
}
Surfaced from shared tags and platforms — no rankings paid for.