Injecting Mouse And Keyboard
Communityskydoves
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.
Overview
- Repository
- skydoves/android-testing-skills
- Publisher
- skydoves
- Stars
- 347
- Forks
- 14
- Bundled files
- 0
- License
- Apache-2.0
- Updated
- May 25, 2026
Installation
tmp=$(mktemp -d)
git clone --depth 1 https://github.com/skydoves/android-testing-skills.git "$tmp"
mkdir -p ~/.claude/skills
cp -R "$tmp/compose/actions/injecting-mouse-and-keyboard" ~/.claude/skills/injecting-mouse-and-keyboard
rm -rf "$tmp"tmp=$(mktemp -d)
git clone --depth 1 https://github.com/skydoves/android-testing-skills.git "$tmp"
mkdir -p ~/.codex/skills
cp -R "$tmp/compose/actions/injecting-mouse-and-keyboard" ~/.codex/skills/injecting-mouse-and-keyboard
rm -rf "$tmp"Install the Injecting Mouse And Keyboard agent skill from https://github.com/skydoves/android-testing-skills/tree/main/compose/actions/injecting-mouse-and-keyboard into this project's skills folder.Instructions
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 perform.*Input blocks on the same node.
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(orui-testforrunComposeUiTest) 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()orModifier.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 toTouchInjectionScope.MouseInjectionScopeextendsInjectionScopesocenter,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) |
doubleClick(position = center, button = Primary) |
midway between min/max double-tap window | click(); advanceEventTime(delay); click() |
tripleClick(position = center, button = Primary) |
same delays as double | three sequential clicks |
longClick(position = center, button = Primary) |
longPressTimeoutMillis + 100 ms hold |
press, hold, release |
animateMoveTo(position, durationMillis = 300) |
300 ms | streams move events along a linear path |
animateMoveBy(delta, durationMillis = 300) |
relative form | — |
animateMoveAlong(curve, durationMillis = 300) |
arbitrary curve | — |
dragAndDrop(start, end, button = Primary, durationMillis = 300) |
primary drag | updatePointerTo(start); press; animateMoveTo(end); release |
smoothScroll(scrollAmount, durationMillis = 300, scrollWheel = Vertical) |
vertical | streams scroll events |
Drop to low-level events when needed:
press(button: MouseButton = Primary)/release(button: MouseButton = Primary).moveTo(position, delayMillis = eventPeriodMillis)/moveBy(delta, delayMillis = eventPeriodMillis).updatePointerTo(position)/updatePointerBy(delta)— adjust position without sending an event.enter(position)/exit(position)— explicit hover-enter / hover-exit.scroll(delta, scrollWheel = Vertical)— single wheel tick.cancel(delayMillis)— emit anACTION_CANCEL.currentPosition: Offset— last known mouse position.
Reference
MouseButtonandScrollWheelfromMouse.kt:MouseButton.Primary,MouseButton.Secondary,MouseButton.Tertiary—expect value class MouseButton.ScrollWheel.Vertical,ScrollWheel.Horizontal—value class ScrollWheel.
Keyboard: performKeyInput
Open a key scope —
performKeyInput(block: KeyInjectionScope.() -> Unit)(Actions.kt:532-545).Send key events. Core surface (KeyInjectionScope.kt:55-107):
keyDown(key: Key)/keyUp(key: Key)— primitives. ThrowIllegalStateExceptionif state is invalid (already-down, already-up).isKeyDown(key: Key): Boolean— query injection state.- Modifier-state vals:
isCtrlDown,isAltDown,isShiftDown,isMetaDown,isFnDown(KeyInjectionScope.kt:236-269),isCapsLockOn,isNumLockOn,isScrollLockOn(KeyInjectionScope.kt:64-81). These reflect the injected state, not the host machine.
Use the helpers (KeyInjectionScope.kt:146-229):
pressKey(key, pressDurationMillis = 50L)—keyDown(key); advanceEventTime(50); keyUp(key).withKeyDown(key) { … }— runsblockwhilekeyis held; auto-releases infinally. The held key MUST NOT be used insideblock.withKeysDown(listOf(key1, key2)) { … }— same but multiple keys held simultaneously.withKeyToggled(key) { … }/withKeysToggled(keys) { … }—pressKeybefore and afterblock. Useful forCapsLock,NumLock,ScrollLock.
Repeat-key behavior. Holding a key down and advancing the event time (via
advanceEventTimefromInjectionScope) produces repeat events: the first repeat fires after 500 ms, then every 50 ms (KeyInjectionScope.kt:43-50). This is NOT triggered byMainTestClock.advanceTimeBy— that one advances the test clock, not the injection event time.Direct KeyEvent injection —
SemanticsNodeInteraction.performKeyPress(KeyEvent): BooleanfromKeyInputHelpers.kt:27. Returnstrueif the event was consumed. Use it when you already have aKeyEvent(e.g. constructed viaKeyEvent(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" intouchis still down whenmouseruns 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.*Inputcalls.
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.
Pattern: Hover-driven tooltip
From CombinedClickableTest.kt:3415-3422:
rule.onNodeWithTag("myClickable").performMouseInput { enter(center) }
rule.runOnIdle {
assertThat(interactions).hasSize(1)
assertThat(interactions.first()).isInstanceOf(HoverInteraction.Enter::class.java)
}
rule.onNodeWithTag("myClickable").performMouseInput { exit(Offset(-1f, -1f)) }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/withKeysDownso the auto-releasefinallycleans up after a thrown assertion. ManualkeyDown/keyUppairs leak modifier state across tests. - MUST use
advanceEventTime(theInjectionScopeclock) — 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
performMouseInputto 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.sleepto wait between key or mouse events. UseadvanceEventTimeinside the block (event-time delay) ormainClock.advanceTimeByoutside it (frame clock delay). - PREFERRED: select
performMultiModalInputonly when modalities truly interleave; otherwise separateperformMouseInputandperformKeyInputcalls read better.
Verification
- Every modifier-key combo uses
withKeyDown/withKeysDownrather than rawkeyDown/keyUppairs. - No
Thread.sleepbetween or insideperformMouseInput/performKeyInputblocks. - No hardcoded screen coordinates — every
Offset(...)is derived fromcenter,top*,bottom*,percentOffset(...), or relative deltas. - Tests asserting key autorepeat behaviour use
advanceEventTime, notmainClock.advanceTimeBy. - Assertions following an action run inside
rule.runOnIdle { … }. -
./gradlew :app:connectedDebugAndroidTest(or the equivalent host / desktop task) passes for the test under change.
References
- Compose testing overview: https://developer.android.com/develop/ui/compose/testing
- Compose Multiplatform testing: https://www.jetbrains.com/help/kotlin-multiplatform-dev/compose-test.html
- Compose testing cheat sheet: https://developer.android.com/develop/ui/compose/testing-cheatsheet
compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Actions.kt—performMouseInput(Actions.kt:448),performKeyInput(Actions.kt:532),performMultiModalInput(Actions.kt:582),performTrackpadInput,performRotaryScrollInput.compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/MouseInjectionScope.kt—press,release,moveTo,moveBy,enter,exit,scroll,cancel, plus extensionsclick,rightClick,doubleClick,tripleClick,longClick,animateMoveTo,animateMoveBy,animateMoveAlong,dragAndDrop,smoothScroll.compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInjectionScope.kt—keyDown,keyUp,isKeyDown, modifier-state vals,pressKey,withKeyDown,withKeysDown,withKeyToggled,withKeysToggled, repeat-key contract.compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/KeyInputHelpers.kt—performKeyPress(KeyEvent): Boolean(KeyInputHelpers.kt:27).compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/Mouse.kt—MouseButton.Primary/Secondary/Tertiary,ScrollWheel.Horizontal/Vertical.compose/ui/ui-test/src/commonMain/kotlin/androidx/compose/ui/test/MultiModalInjectionScope.kt—touch,mouse,key,rotary,trackpad,indirectPointer.compose/foundation/foundation/src/androidDeviceTest/.../CombinedClickableTest.kt—performMouseInput { enter(center) },performMouseInput { exit(Offset(-1f, -1f)) }(CombinedClickableTest.kt:3415-3422).- skydoves — compose-performance-skills: https://github.com/skydoves/compose-performance-skills
FAQ
What does Injecting Mouse And Keyboard do?
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.
Who made Injecting Mouse And Keyboard?
skydoves (a community member) publishes it in the skydoves/android-testing-skills repository on GitHub.
Is Injecting Mouse And Keyboard free?
Yes. Injecting Mouse And Keyboard is open source, published by skydoves under the Apache-2.0 license.
What comes with Injecting Mouse And Keyboard?
Just its instructions. Everything the agent needs is in one SKILL.md file.
Which agents can use Injecting Mouse And Keyboard?
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.