Spectre UI Automation
Communityrock3r
Use when the user is writing, running, or debugging end-to-end UI automation tests for a Compose for Desktop (aka Compose Multiplatform on desktop) app — including setting up Spectre, clicking or typing in a running window, finding UI nodes by tag or text, waiting for the UI to settle, capturing screenshots or video, or troubleshooting a Compose Desktop or IntelliJ-hosted UI under test. Also use when the user asks how to automate a live running Compose Desktop app, even if they don't mention Spectre by name.
Overview
- Repository
- rock3r/spectre
- Publisher
- rock3r
- Stars
- 40
- Forks
- 0
- Bundled files
- 2
- License
- Apache-2.0
- Updated
- Nov 14, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/rock3r/spectre.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/skill" ~/.claude/skills/spectre-ui-automation
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/rock3r/spectre.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/skill" ~/.codex/skills/spectre-ui-automation
rm -rf "$tmp"Install the Spectre UI Automation agent skill from https://github.com/rock3r/spectre/tree/main/skill into this project's skills folder.Instructions
Spectre UI Automation
Spectre reads the semantics tree of a running Compose Desktop UI. It sends synthetic input by default. Tests can opt into real OS input. See the user guide for setup and API details.
Use spectre in skills/spectre/SKILL.md for selectors, recording, attach, and IntelliJ.
Use spectre-capture for atomic capture.json workflows.
Choosing a test framework
ComposeTestRule(compose-ui-test) — tests a composable in isolation, without launching a real app. Use it for unit and component tests.- Spectre — automates a running app in the local process, a separate JVM via HTTP or Java-agent attach, or an IntelliJ plugin hosting Compose UI. Use it to test interactions across the application.
Use ComposeTestRule for an isolated composable. Use Spectre for a running application.
Common constraints
- Use
runSpectreTest.runTestcollapsesdelay()to zero. That breakslongClickholds,swipepacing, and clipboard polling.runSpectreTestuses real time and detects leaks. PlainrunBlockingremains a fallback. - Selectors read once.
findBy...andfindOneBy...do not wait. CallwaitForNodewhen a node may appear later. CallwaitUntilGoneafter dismissing a popup, menu, or dialog. UsewaitUntil(description) { ... }for tree conditions such as a node count. - Keep waits off the EDT. All five wait helpers throw
IllegalStateExceptionon the AWT event dispatch thread. JUnit test methods normally run outside the EDT. Move EDT calls withwithContext(Dispatchers.Default). - Expression-body tests. Write
@Test fun mySpec(): Unit = runSpectreTest { ... }. JUnit 5.14+ rejects non-void test methods, and Kotlin infers the return type from the last expression in therunSpectreTestbody.
Setup
In build.gradle.kts:
dependencies {
testImplementation("dev.sebastiano.spectre:spectre-core:$spectreVersion")
testImplementation("dev.sebastiano.spectre:spectre-testing:$spectreVersion")
// Required for video or native window-scoped screenshots:
testImplementation("dev.sebastiano.spectre:spectre-recording:$spectreVersion")
testRuntimeOnly("dev.sebastiano.spectre:spectre-recording-macos:$spectreVersion") // macOS helper
testRuntimeOnly("dev.sebastiano.spectre:spectre-recording-linux:$spectreVersion") // Linux helper
testRuntimeOnly("dev.sebastiano.spectre:spectre-recording-windows:$spectreVersion") // Windows helper
// optional:
testImplementation("dev.sebastiano.spectre:spectre-server:$spectreVersion") // cross-JVM HTTP transport
testImplementation("dev.sebastiano.spectre:spectre-agent:$spectreVersion") // agent attach API
testRuntimeOnly("dev.sebastiano.spectre:spectre-agent-runtime:$spectreVersion") // loadable Java agent
}Check the user guide for the current release and setup.
Test structure (JUnit 5)
For sequential tests, put the extension on the test class:
import dev.sebastiano.spectre.testing.ComposeAutomatorExtension
import dev.sebastiano.spectre.testing.runSpectreTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.RegisterExtension
class MyTest {
@JvmField @RegisterExtension
val automatorExt = ComposeAutomatorExtension()
@Test
fun myTest(): Unit = runSpectreTest {
launchMyApp() // your responsibility — Spectre manages the automator, not the window
val automator = automatorExt.automator
automator.waitForNode(tag = "root-content")
// interact and assert
}
}launchMyApp() must open a Compose Desktop window, through the app or a test harness.
The Spectre extension and rule manage the ComposeAutomator. The test harness manages the window.
For parallel tests, use @ExtendWith and parameter injection.
Each invocation receives its own automator:
import dev.sebastiano.spectre.core.ComposeAutomator
import dev.sebastiano.spectre.testing.ComposeAutomatorExtension
import dev.sebastiano.spectre.testing.runSpectreTest
import org.junit.jupiter.api.Test
import org.junit.jupiter.api.extension.ExtendWith
@ExtendWith(ComposeAutomatorExtension::class)
class MyTest {
@Test
fun myTest(automator: ComposeAutomator): Unit = runSpectreTest {
launchMyApp()
automator.waitForNode(tag = "root-content")
// interact and assert
}
}For JUnit 4, use @get:Rule val automatorRule = ComposeAutomatorRule() and access via automatorRule.automator.
Finding nodes
automator.findOneByTestTag("Submit") // most reliable; needs Modifier.testTag on the composable
automator.findOneByText("Submit") // exact, case-sensitive by default
automator.findByText("sub", exact = false) // substring, case-insensitive
automator.findByContentDescription("Send")
automator.findByRole(Role.Button)
automator.printTree() // dump the full semantics tree — use this first when debuggingPrefer testTag for nodes that tests click, scroll, or type into.
Copy and translation changes can break text selectors. Use text queries to check visible content.
Key AutomatorNode properties: testTag, text, texts, contentDescription, role, isFocused, isDisabled, isSelected, isClickable, toggleableState, progressBarRangeInfo, editableText, boundsInWindow, boundsOnScreen, centerOnScreen, children, parent.
Interactions
Interaction methods are suspend. Call them inside runSpectreTest { ... }.
Plain runBlocking is a fallback.
automator.click(node)
automator.doubleClick(node)
automator.longClick(node, holdFor = 600.milliseconds)
automator.swipe(from = firstNode, to = lastNode)
automator.swipe(startX = 100, startY = 400, endX = 100, endY = 100, steps = 16, duration = 200.milliseconds)
automator.scrollWheel(listNode, wheelClicks = 5) // negative = scroll up
automator.typeText("hello") // key events; supported ASCII only
automator.pasteText("こんにちは") // clipboard paste for large or Unicode text
automator.clearAndTypeText(node, "replacement") // click + clear + typeText
automator.pressKey(KeyEvent.VK_TAB)
automator.pressKey(KeyEvent.VK_S, modifiers = InputEvent.CTRL_DOWN_MASK)
automator.pressEnter()
val img: BufferedImage = automator.screenshot() // full virtual screen
val img = automator.screenshot(windowIndex = 0) // native single-window capture; needs recording + platform helper
val img = automator.screenshot(node) // native window capture; needs recording + platform helper
// Multi-window composite stills: automator.screenshotWindows(listOf(0, 1)) → scene.composite
// Opt-in JUnit golds: assertMatchesGold + ScreenshotTolerance — see long skill references/junit.mdtypeText dispatches key press/release pairs and does not touch the clipboard. Use pasteText for large strings or arbitrary Unicode.
Synchronization
// After launching — poll until a node appears
automator.waitForNode(tag = "root-content")
// After dismissing a popup / menu / dialog — poll until nothing matches in any tracked window
automator.waitUntilGone(tag = "popup.body")
// For a barrier no tag/text selector expresses — the predicate receives the AutomatorTree
automator.waitUntil(description = "at least five rows are showing") {
allNodes().count { it.testTag?.startsWith("row.") == true } >= 5
}
// After an interaction — wait for semantics tree + idling resources to settle
automator.waitForIdle()
// After an animation or visual change that doesn't affect semantics
automator.waitForVisualIdle()Typical post-interaction pattern:
automator.click(submit)
automator.waitForIdle() // semantics settled
automator.waitForVisualIdle() // pixels settled
val result = automator.findOneByTestTag("Result")All five waits are suspend and reject EDT callers. Their default timeout is 5 s.
waitUntil polls the semantics tree supplied to its predicate.
Use another tool to wait for a service flag, file, or HTTP response.
Input drivers
Use ComposeAutomator.inProcess() for most tests. By default, it sends
synthetic AWT events (RobotDriver.synthetic()) posted into the window hierarchy.
Switch drivers only when needed:
RobotDriver()— realjava.awt.Robotinput; moves the actual cursor and takes OS focus. On Linux Wayland it sends input through Spectre's portal session, which a person approves once withspectre permissions request; avoidRobotDriver(robot)there, because it bypasses that session.RobotDriver.synthetic(rootWindow = window)— same synthetic path as the default, pinned to a window.RobotDriver.headless()— read-only; every input or screenshot call throwsUnsupportedOperationException. Semantics-tree reads still work.ComposeAutomator.http(security, host = "localhost", port = 7654)— experimental authenticated cross-JVM HTTPS (bearer required); deliberate subset of in-process (windows/nodes/input/ screenshots; no live idling resources /withTracing). It needs matchingSpectreHttpSecurityand:serverin the target. Plaintext requires the explicit loopback-only test escape hatch. See https://spectre.sebastiano.dev/guide/cross-jvm/.AgentAttach.attach(pid)— attach to a running Compose JVM. The target does not needspectre-corepreinstalled: when core is absent, the agent runtime injects nestedMETA-INF/spectre/inject-runtime.jar. Prefer aspectre-coredependency for controlled target builds. The attacher needsspectre-agentplusspectre-agent-runtime.
For recording, IntelliJ-hosted Compose, advanced selectors, troubleshooting, and the full reference, see spectre.sebastiano.dev.
Bundled files
FAQ
What does Spectre UI Automation do?
Use when the user is writing, running, or debugging end-to-end UI automation tests for a Compose for Desktop (aka Compose Multiplatform on desktop) app — including setting up Spectre, clicking or typing in a running window, finding UI nodes by tag or text, waiting for the UI to settle, capturing screenshots or video, or troubleshooting a Compose Desktop or IntelliJ-hosted UI under test. Also use when the user asks how to automate a live running Compose Desktop app, even if they don't mention Spectre by name.
Who made Spectre UI Automation?
rock3r (a community member) publishes it in the rock3r/spectre repository on GitHub.
Is Spectre UI Automation free?
Yes. Spectre UI Automation is open source, published by rock3r under the Apache-2.0 license.
What comes with Spectre UI Automation?
Its instructions and 2 files the agent reads only when a task needs them, such as references, examples or scripts.
Which agents can use Spectre UI Automation?
Any coding agent that reads SKILL.md files, including Claude Code, Codex, Cursor, GitHub Copilot and Gemini CLI. The agent loads the skill when a task matches its description.
What are agent skills?
An agent skill is a folder with a SKILL.md file: instructions that teach a coding agent how to do one kind of task, and optionally files it can read while it works. The agent keeps only the skill's name and description in mind and reads the rest when a task calls for it.
How are agent skills different from MCP servers?
An MCP server gives an agent new tools to call, code that runs somewhere and returns results. A skill gives it knowledge: how to approach a task with the tools it already has. Many agents use both.
How can my agent test my Android app?
Robomori lets coding agents create, start and control Android emulators and devices from one command, so they can install your app, take screenshots and check dark mode, font sizes and languages.