Use this skill to drive Jetpack Compose UI from tests with non-touch input — performMouseInput (click, rightClick, doubleClick, tripleClick, longClick, animateMoveTo, dragAndDrop, smoothScroll, enter / exit, press / release / scroll), performKeyInput (keyDown, keyUp, isKeyDown, modifier-state vals isCtrlDown / isShiftDown / isAltDown / isMetaDown / isFnDown / isCapsLockOn / isNumLockOn / isScrollLockOn, helpers pressKey, withKeyDown, withKeysDown, withKeyToggled, withKeysToggled), performKeyPress for direct KeyEvent injection, and performMultiModalInput for hover-then-click flows. Covers MouseButton (Primary, Secondary, Tertiary), ScrollWheel (Horizontal, Vertical), and the repeat-key behaviour driven by advanceEventTime. Use when the developer asks "how do I right-click in a Compose test", "test a Ctrl+S shortcut", "send a keyboard shortcut", "simulate hover", "test a tooltip", "scroll the mouse wheel", or "drag and drop with a mouse" on desktop or Compose Multiplatform.
Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.
Quelldateien prüfen
Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.
Mit Codex oder Claude installieren Kopieren Sie diesen Prompt, fügen Sie ihn in Codex, Claude oder einen anderen Assistant ein und lassen Sie die Skill-Seite prüfen und installieren.
Ein direkter Befehl überspringt den Prüf-Prompt. Prüfen Sie die Quelle, bevor Sie ihn ausführen.
Use this skill to drive Jetpack Compose UI from tests with non-touch input — performMouseInput (click, rightClick, doubleClick, tripleClick, longClick, animateMoveTo, dragAndDrop, smoothScroll, enter / exit, press / release / scroll), performKeyInput (keyDown, keyUp, isKeyDown, modifier-state vals isCtrlDown / isShiftDown / isAltDown / isMetaDown / isFnDown / isCapsLockOn / isNumLockOn / isScrollLockOn, helpers pressKey, withKeyDown, withKeysDown, withKeyToggled, withKeysToggled), performKeyPress for direct KeyEvent injection, and performMultiModalInput for hover-then-click flows. Covers MouseButton (Primary, Secondary, Tertiary), ScrollWheel (Horizontal, Vertical), and the repeat-key behaviour driven by advanceEventTime. Use when the developer asks "how do I right-click in a Compose test", "test a Ctrl+S shortcut", "send a keyboard shortcut", "simulate hover", "test a tooltip", "scroll the mouse wheel", or "drag and drop with a mouse" on desktop or Compose Multiplatform.
license
Apache-2.0. See LICENSE for complete terms.
metadata
{"author":"Jaewoong Eum (skydoves)","keywords":["jetpack-compose","ui-testing","performMouseInput","performKeyInput","performMultiModalInput","mouse-button","scroll-wheel","keyboard-shortcut","hover","compose-multiplatform"]}
Injecting Mouse and Keyboard — performMouseInput, performKeyInput, performMultiModalInput
Mouse and keyboard input are first-class modalities in Compose tests. Each has its own injection scope and entry point, plus a shared performMultiModalInput that combines all of them inside one batched gesture (touch, mouse, key, rotary, trackpad, indirect pointer). Coordinates remain node-local — the same rules as touch — and the entire injection state survives across blocks on the same node.
perform.*Input
When to use this skill
The test exercises a desktop or Compose Multiplatform UI that responds to clicks, hover, right-click, scroll wheel, or modifier keys.
The test must drive a keyboard shortcut (e.g. Ctrl+S, Shift+Tab, arrow-key navigation).
The test must verify hover-only behaviour: tooltip pop, color change on Modifier.hoverable, mouse-only ripple state.
The test simulates a drag-and-drop with a mouse pointer.
The test must combine modalities — for example "hover at (x, y) then click and hold while pressing Shift".
When NOT to use this skill
The interaction is a finger tap or drag — use ../injecting-touch-gestures/SKILL.md. Mouse is not interchangeable with touch on Android device tests.
A simple performClick() already routes through the right modality per platform — use ../clicking-and-scrolling/SKILL.md.
The test types into a TextField — use ../entering-text/SKILL.md. Key input here is for shortcuts, not text entry.
The test waits for a hover-driven animation to settle — pause the clock first per ../../synchronization/testing-animations-deterministically/SKILL.md.
Prerequisites
androidx.compose.ui:ui-test-junit4 (or ui-test for runComposeUiTest) configured per ../../setup/configuring-test-dependencies/SKILL.md.
For mouse hover events on Android: API level that supports hover (typically API 23+) — Robolectric host tests work but check @Config(minSdk = …).
For key input that targets a focusable node: the node must be focusable (e.g. wrapped in Modifier.focusable() or Modifier.focusRequester(...)). Otherwise key events route to the focused root.
Workflow
Mouse: performMouseInput
Open a mouse scope — performMouseInput(block: MouseInjectionScope.() -> Unit) (Actions.kt:448-461). Like touch, events are batched and flushed when the block returns; recomposition cannot interleave inside the block.
Use node-local coordinates.(0, 0) is the node's top-left, identical to TouchInjectionScope. MouseInjectionScope extends InjectionScope so center, topLeft, bottomRight, percentOffset(...) are all available (InjectionScope.kt:34-206).
Pick the right helper. All in MouseInjectionScope.kt:
Helper
Default
What it does
click(position = center, button = MouseButton.Primary)
primary click at center
press → 60 ms wait → release
rightClick(position = center)
secondary click at center
shorthand for click(position, MouseButton.Secondary)
withKeyDown(key) { … } — runs block while key is held; auto-releases in finally. The held key MUST NOT be used inside block.
withKeysDown(listOf(key1, key2)) { … } — same but multiple keys held simultaneously.
withKeyToggled(key) { … } / withKeysToggled(keys) { … } — pressKey before and after block. Useful for CapsLock, NumLock, ScrollLock.
Repeat-key behavior. Holding a key down and advancing the event time (via advanceEventTime from InjectionScope) produces repeat events: the first repeat fires after 500 ms, then every 50 ms (KeyInjectionScope.kt:43-50). This is NOT triggered by MainTestClock.advanceTimeBy — that one advances the test clock, not the injection event time.
Direct KeyEvent injection — SemanticsNodeInteraction.performKeyPress(KeyEvent): Boolean from KeyInputHelpers.kt:27. Returns true if the event was consumed. Use it when you already have a KeyEvent (e.g. constructed via KeyEvent(NativeKeyEvent(...)) for fine-grained source/scancode control); otherwise prefer the DSL.
Multi-modal: performMultiModalInput
Open a multi-modal scope — performMultiModalInput(block: MultiModalInjectionScope.() -> Unit) (Actions.kt:582-594).
Dispatch into sub-scopes — touch { … }, mouse { … }, key { … }, rotary { … }, trackpad { … }, indirectPointer(...) (MultiModalInjectionScope.kt:51-90). All sub-scopes share the same injection state, so a finger left "down" in touch is still down when mouse runs next.
Pick this entry point when a single test step combines modalities — e.g. hover-while-shift-held — instead of stitching together two separate perform.*Input calls.
Patterns
Pattern: Right-click menu
// RIGHT
rule.onNodeWithTag(RowTag).performMouseInput {
rightClick(center)
}
rule.onNodeWithText("Delete").assertIsDisplayed()
rightClick is shorthand for click(position, MouseButton.Secondary) (MouseInjectionScope.kt:356-357). The label says "right" for familiarity, but it actually triggers the secondary button — correct on left-handed mice as well.
enter(position) emits a hover-enter; exit(position) emits a hover-exit. Pass an off-node Offset(-1f, -1f) to exit to mimic the cursor leaving the surface.
Pattern: Ctrl+S shortcut — verbose vs idiomatic
// WRONG
rule.onNodeWithTag(EditorTag).performKeyInput {
keyDown(Key.CtrlLeft)
pressKey(Key.S)
keyUp(Key.CtrlLeft)
}
// WRONG because: a thrown assertion or early-return inside the block leaks Key.CtrlLeft// in the "down" state, polluting subsequent tests. The release is not in a finally.
// RIGHT
rule.onNodeWithTag(EditorTag).performKeyInput {
withKeyDown(Key.CtrlLeft) {
pressKey(Key.S)
}
}
withKeyDown releases the key in a finally block (KeyInjectionScope.kt:164-171). For multi-modifier shortcuts use withKeysDown(listOf(Key.CtrlLeft, Key.ShiftLeft)) { pressKey(Key.S) }.
Pattern: Repeat-key autorepeat
rule.onNodeWithTag(InputTag).performKeyInput {
keyDown(Key.DirectionDown)
advanceEventTime(800) // 500 ms initial delay + 6 repeats at 50 ms
keyUp(Key.DirectionDown)
}
advanceEventTime is the input-dispatcher clock — it triggers repeat events at the documented cadence (first repeat at 500 ms, then every 50 ms — KeyInjectionScope.kt:43-50). MUST NOT substitute rule.mainClock.advanceTimeBy(…): the main test clock advances Compose's frame clock, not the input dispatcher's event time, and no repeat events will be enqueued.
Pattern: Drag-and-drop with mouse
rule.onNodeWithTag(SourceTag).performMouseInput {
dragAndDrop(
start = center,
end = Offset(center.x + 200f, center.y),
durationMillis = 250,
)
}
dragAndDrop updates the pointer to start, presses, animates a move to end, then releases (MouseInjectionScope.kt:523-533). Default button is MouseButton.Primary; pass button = MouseButton.Tertiary for middle-click drag.
Pattern: Smooth scroll
rule.onNodeWithTag(ScrollAreaTag).performMouseInput {
smoothScroll(
scrollAmount = -10f, // negative scrollAmount = scroll back; new content appears at the top (MouseInjectionScope.kt:537-542)
durationMillis = 200,
scrollWheel = ScrollWheel.Vertical,
)
}
Positive scrollAmount reveals content from the bottom of a vertical column or from the end of a horizontal row (MouseInjectionScope.kt:537-542). For a single-tick wheel event, drop to scroll(delta, scrollWheel).
Pattern: Multi-modal — hover then click while Shift held
MultiModalInjectionScope exposes mouse { }, key { }, touch { } etc. as sub-scopes; modifier-key state from one key { withKeyDown(...) { } } invocation persists across the rest of the surrounding performMultiModalInput, so a subsequent mouse { } block is dispatched while Shift is still down. The withKeyDown block itself has KeyInjectionScope as its receiver — calling mouse { } from inside it does not compile.
rule.onNodeWithTag(NodeTag).performMultiModalInput {
mouse { enter(center) }
key { keyDown(Key.ShiftLeft) }
mouse { click() } // dispatched with Shift still held
key { keyUp(Key.ShiftLeft) }
}
A single performMultiModalInput keeps every modality in one batched flush; the modifier-key state, pointer position, and pressed buttons are shared across sub-scopes (MultiModalInjectionScope.kt). Use it when interleaving matters; otherwise two separate performMouseInput / performKeyInput calls are clearer.
Pattern: Direct KeyEvent injection for precise control
import androidx.compose.ui.input.key.KeyEvent
import androidx.compose.ui.test.performKeyPress
val event = KeyEvent(NativeKeyEvent(NativeKeyEvent.ACTION_DOWN, NativeKeyEvent.KEYCODE_TAB))
rule.onRoot().performKeyPress(event)
performKeyPress returns whether the event was consumed (KeyInputHelpers.kt:27). It bypasses the DSL — useful for replaying a captured KeyEvent or testing IME-derived keystrokes.
Mandatory rules
MUST wrap modifier-key shortcuts in withKeyDown / withKeysDown so the auto-release finally cleans up after a thrown assertion. Manual keyDown / keyUp pairs leak modifier state across tests.
MUST use advanceEventTime (the InjectionScope clock) — NOTMainTestClock.advanceTimeBy — when expecting key autorepeat events. Repeats are produced by the input dispatcher's event-time stream (KeyInjectionScope.kt:43-50).
MUST keep coordinates node-local using center, topLeft, bottomRight, percentOffset(...). Hardcoded screen coordinates make the test device-shaped — see ../injecting-touch-gestures/SKILL.md.
MUST funnel post-action assertions through rule.runOnIdle { … } (skydoves hot take #5).
MUST NOT use performMouseInput to test a finger tap on Android device tests — performClick() resolves to a touch tap on Android and a mouse click on desktop, automatically.
MUST NOT use Thread.sleep to wait between key or mouse events. Use advanceEventTime inside the block (event-time delay) or mainClock.advanceTimeBy outside it (frame clock delay).
PREFERRED: select performMultiModalInput only when modalities truly interleave; otherwise separate performMouseInput and performKeyInput calls read better.
Verification
Every modifier-key combo uses withKeyDown / withKeysDown rather than raw keyDown/keyUp pairs.
No Thread.sleep between or inside performMouseInput / performKeyInput blocks.
No hardcoded screen coordinates — every Offset(...) is derived from center, top*, bottom*, percentOffset(...), or relative deltas.
Tests asserting key autorepeat behaviour use advanceEventTime, not mainClock.advanceTimeBy.
Assertions following an action run inside rule.runOnIdle { … }.
./gradlew :app:connectedDebugAndroidTest (or the equivalent host / desktop task) passes for the test under change.