KMPwright

Actions

Click, fill, type, press, check, select, drag and use the mouse, keyboard and touchscreen, with the auto-wait rules each action applies on every backend.

val search = page.getByRole("searchbox", name = "Search")
search.fill("kotlin multiplatform")
search.press("Enter")

page.getByRole("checkbox", name = "Stable releases only").check()
page.locator("select#sort").selectOption("newest")
page.getByRole("link", name = "Next").click(timeoutMs = 5_000)

page.locator("#editor").press("Control+A")
page.locator("#editor").pressSequentially("Hello", delayMs = 50)

Actions are suspend functions on Locator. Options are named parameters, as in click(button = MouseButton.Right, force = true). Each one resolves the locator, waits until the element is ready for that action, performs it, and returns. Nothing is retried after the action runs. To wait for the result, follow up with an assertion or page.waitForURL.

For how locators are built and matched, see Locators.

Auto-waiting

Before an action, KMPwright polls the element until it passes the checks that action needs, or until the action timeout expires. The in-page runtime does this on Android WebView, on iOS WKWebView, and on JVM when InputMode.JsOnly is set. The JVM native-input path behaves differently. See JVM native input.

CheckPasses when
AttachedThe locator matches an element that is connected to the document.
VisibleThe element has a non-empty bounding box and visibility is not hidden or collapse.
EnabledThe element does not match :disabled, and neither it nor an ancestor has aria-disabled="true".
EditableThe element is an input (not checkbox, radio, file, button, submit, reset, image, hidden, range or color), a textarea, a select or [contenteditable], and is not readonly or aria-readonly="true".
StableThe bounding box is unchanged (within 0.5 px) across two consecutive probes about two animation frames (34 ms) apart.
Receives eventselementFromPoint at the action point returns the element, a descendant, a <label> whose control is the element, or the element's shadow host.

When an element is not fully inside the viewport, actions that use a pointer scroll it into view first, centred, before the stable and receives-events checks.

ActionAttachedVisibleEnabledEditableStableReceives eventsScrolls
click, dblclick, tapyesyesyesyesyesyes
check, uncheck, setCheckedyesyesyesyesyesyes
hoveryesyesyesyesyes
fill, clearyesyesyesyesyes
pressSequentiallyyesyesyesyes
selectOptionyesyesyesyes
dragTo (source)yesyesyesyesyes
dragTo (target)yesyes
scrollIntoViewIfNeededyesyesyes
press, focus, blur, dispatchEvent, evaluateyes

check, uncheck and setChecked read the current state before they wait. If nothing matches yet, they throw at once instead of waiting.

Polling backs off over 0, 20, 50, 100, 100, 250 and 500 ms, then repeats every 500 ms. An unstable element is re-probed after 34 ms. When time runs out, the action throws KmpwrightTimeoutException. The message names the failing check and the element:

Element #save is not enabled after 30000ms while trying to click: <button id="save" disabled>Save</button>
Element .buy would not receive the click: it is covered by <div class="backdrop"></div> (pass force = true only when that is intended) after 30000ms while trying to click: <button class="buy">Buy</button>

Some failures happen immediately: a strict mode violation, an invalid selector, and filling an element that can never be filled (for example fill on a checkbox). Playwright: same.

force

force = true (a parameter of click, dblclick, tap, hover, fill, clear, check, uncheck, setChecked, selectOption and setInputFiles) skips the visible, enabled, stable and receives-events checks. For fill it also allows readonly elements. The element still has to be attached, and fill still rejects element types that can't be filled. press, pressSequentially and focus have no force parameter.

JVM native input

On JVM with the default InputMode.Hybrid, click, dblclick, tap, hover, fill, clear, pressSequentially, press and dragTo send trusted Chrome input (Input.dispatchMouseEvent, Input.dispatchKeyEvent, Input.insertText, Input.dispatchTouchEvent). These paths check readiness differently:

  • They wait for the element to be visible (skipped with force). press and dragTo don't wait.
  • They scroll the element to the centre of the viewport, then check once that it is enabled, inside the viewport and not covered (skipped with force). fill, pressSequentially and press instead focus the element and fail if it is disabled, read-only or can't take focus.
  • If one of those checks fails, the action throws KmpwrightException right away. It does not retry, and there is no stability check.
  • A timeout throws KmpwrightException("Timeout performing action on '<selector>'").

Catch KmpwrightException, the base type, to handle errors from both paths. Set KmpwrightConfig(inputMode = InputMode.JsOnly) to use the retrying runtime on JVM, at the cost of untrusted events.

Timeouts

Every action takes timeoutMs: Long?. null uses the page's KmpwrightConfig.timeouts:

TimeoutConfig fieldDefaultUsed by
action30 000 msLocator actions, Page action shortcuts, input-device calls
waitForSelector30 000 mslocator.waitFor, page.waitForSelector, Page query shortcuts, assertions
navigation30 000 msgoto, reload, goBack, goForward, setContent, waitForNavigation
default30 000 msScript evaluation, screenshots, PDF and other page operations
val config = KmpwrightConfig(timeouts = TimeoutConfig(action = 10_000))
page.getByText("Pay").click(timeoutMs = 2_000)   // per-call override

A timeout must be positive. 0 or less is rejected with an exception.

Click and tap

page.getByRole("button", name = "Save").click()
page.locator("#canvas").click(position = Position(10.0, 20.0))
page.locator(".row").click(button = MouseButton.Right)
page.locator(".word").dblclick()
page.locator(".card").tap()

click parameters

click and dblclick take these parameters; dblclick has no clickCount.

Prop

Type

On the in-page runtime, a click dispatches the full sequence in this order: pointerover, pointerenter, mouseover, mouseenter, pointermove, mousemove, pointerdown, mousedown, pointerup, mouseup, click. On mousedown it focuses the nearest focusable ancestor. A canceled pointerdown suppresses mousedown and mouseup (but not click), as in browsers.

tap parameters

page.touchscreen.tap(x, y, modifiers, timeoutMs) takes the same modifiers and timeoutMs, without force.

Prop

Type

On the in-page runtime, tap fires pointerover, pointerdown, touchstart, pointerup, touchend and pointerout, then the compatibility mouseover, mousemove, mousedown, mouseup and click unless the page canceled the touch events.

Fill and clear

page.getByLabel("Email").fill("ada@example.com")
page.getByLabel("Notes").fill("line 1\nline 2")      // textarea
page.locator("[contenteditable]").fill("Rich text")
page.locator("input[type=date]").fill("2026-10-12")
page.getByLabel("Email").clear()                      // same as fill("")

fill(value, force = false, timeoutMs = null) replaces the whole value of an input, textarea, select or [contenteditable] element. Playwright: same, including select support.

Prop

Type

On the in-page runtime (Android, iOS, JVM JsOnly), the value is set like this:

  1. The element is focused and its content is selected.
  2. A cancelable beforeinput is dispatched. If the page cancels it, the value is left unchanged. Date, time, month, week and datetime-local inputs skip this step.
  3. The value is written through the HTMLInputElement/HTMLTextAreaElement/HTMLSelectElement prototype value setter, captured when the runtime was installed. React, Vue and other frameworks with controlled inputs track changes through an instance-level setter, so writing through the prototype is how a real edit looks to them.
  4. input (with inputType insertText, or deleteContentBackward for an empty value), then change, are dispatched.

Per element type:

  • [contenteditable]: content is selected and replaced with document.execCommand('insertText'). If that fails, textContent is set and beforeinput/input are dispatched.
  • select: the option is chosen by value, then by visible label. input and change are dispatched. A missing option fails.
  • input[type=number]: a non-numeric value fails.
  • date, time, month, week, datetime-local: the browser must accept the value as-is (for example 2026-10-12). Otherwise the fill fails with Malformed value.

On JVM native input, fill focuses the element, selects its content and calls Input.insertText. An empty value presses Backspace instead. Chrome's own editing produces trusted beforeinput/input events, but no change event fires until the element loses focus. This path accepts only elements that can be selected as text (input, textarea, [contenteditable]). Use selectOption for select.

Typing and key presses

val input = page.getByLabel("Search")
input.pressSequentially("kmp", delayMs = 100)
input.press("Enter")
input.press("Shift+Tab")
page.locator("textarea").press("Control+A")
page.locator("textarea").press("Backspace")
  • fill sets the value in one step. Use it unless the page reacts to individual keystrokes.
  • pressSequentially(text) types one character at a time. Playwright: same name; formerly type.
  • press(key) presses one key or chord on the element, focusing it first.

Prop

Type

Key names

KindNames
Named keysEnter, Tab, Escape, Backspace, Delete, ArrowLeft, ArrowUp, ArrowRight, ArrowDown, Home, End, PageUp, PageDown, F1–F12. Case-sensitive: use the DOM key spelling.
CharactersAny single character: "a", "A", "7", "?", " " (space).
ModifiersShift, Control (Ctrl), Alt (Option), Meta (Cmd, Command). Case-insensitive.
ChordsModifiers and a key joined by +: "Control+A", "Shift+Tab", "Meta+Shift+K". "Control++" presses +.

For a space, press " ". The name "Space" is not mapped to the space character.

ControlOrMeta (Meta on macOS, Control elsewhere) works only on JVM native input. Elsewhere it fails with Unknown modifier.

Key events carry key, code and keyCode for a US layout. With Shift held, letters become uppercase and digits and punctuation become their shifted characters (Shift+1 produces !). With Control, Alt or Meta held, no text is inserted.

Default key actions

On the in-page runtime, press and pressSequentially dispatch keydown, keypress (for printable keys and Enter) and keyup, then perform the browser's default action themselves. Canceling keydown skips keypress and the default action. Canceling keypress skips text insertion.

KeyDefault action
Printable characterInserted at the caret with beforeinput and input. Respects maxlength, readonly and disabled.
Entertextarea: inserts a newline. [contenteditable]: inserts a paragraph. Text input inside a form: implicit submission. If the form has a submit button, the first one is clicked, or nothing happens when it is disabled. Without one, requestSubmit() is called. button, a[href], summary, input[type=submit|button|reset|image]: clicks it.
Backspace / DeleteIn a text field: deletes the selection, or one character before/after the caret.
Tab / Shift+TabMoves focus to the next/previous visible tabbable element, in tabindex order, wrapping around.
ArrowLeft, ArrowRight, Home, EndMove the caret in input and textarea.
" " (space)On button, summary, checkbox, radio and button-type inputs, or role=button|checkbox|radio|switch: clicks it on keyup instead of inserting a space.
Control+A / Meta+ASelects all content of the field or [contenteditable].
Other keys and chordsEvents only.

pressSequentially treats "\n" as Enter. It does not fire change.

On JVM native input, Chrome performs the default actions itself.

Hover, focus and blur

page.getByRole("menuitem", name = "File").hover()
page.getByLabel("Name").focus()
page.getByLabel("Name").blur()

hover(force = false, timeoutMs = null) moves the pointer over the element's centre. On the in-page runtime it dispatches pointerover/pointerenter/mouseover/mouseenter/pointermove/mousemove, plus pointerout/mouseout/pointerleave/mouseleave on the previously hovered element. On the runtime, a disabled element can be hovered. On JVM native input, hover fails on a disabled element unless force = true.

focus(timeoutMs) focuses the element without scrolling. blur(timeoutMs) waits for the element to be attached and calls el.blur().

Checkboxes and radio buttons

page.getByLabel("Accept terms").check()
page.getByRole("switch", name = "Wi-Fi").setChecked(false)
page.locator("#newsletter").uncheck(force = true)

check, uncheck and setChecked(Boolean) work on input[type=checkbox], input[type=radio] and elements with aria-checked:

  1. Read the current state (indeterminate/aria-checked="mixed" counts as checked). Any other element throws is not a checkbox or radio button.
  2. Return if the element is already in the requested state.
  3. Click it with the given force and timeoutMs, using the backend's click.
  4. Read the state again. If it did not change, throw Clicking … did not check it.

Unchecking a radio button throws. Check another option in the group instead. Playwright: same.

Select options

page.locator("select#country").selectOption("PT")              // by value or label
page.getByLabel("Toppings").selectOption(listOf("ham", "Olives"))  // multi-select
page.getByLabel("Toppings").selectOption(emptyList())           // deselect all

selectOption(value: String, force = false, timeoutMs = null) and selectOption(values: List<String>, force = false, timeoutMs = null) match each value against option.value first, then against the option's whitespace-normalised label. Then they set the selection and dispatch input and change. Every backend uses the in-page runtime for this action.

  • The element must be a select. Anything else fails immediately.
  • More than one value on a single select fails.
  • A disabled option, or an option in a disabled optgroup, fails.
  • A value with no matching option is retried until the timeout. The error lists the available options as value (label).

Playwright: selectOption also accepts { index } and { label } objects. KMPwright takes strings only.

File inputs

page.locator("input[type=file]").setInputFiles(listOf("/tmp/report.pdf"))
page.locator("input[type=file]").setInputFiles(emptyList())   // clear the selection
BackendsetInputFiles(files)
JVM (CDP)Sets the files with DOM.setFileInputFiles. Paths are made absolute and must be existing regular files.
Android WebView, iOS WKWebViewOnly emptyList() works: it clears the value and dispatches input and change. A non-empty list throws setInputFiles is not supported in mobile WebView.

setInputFiles does not run the actionability checks. It ignores the force and timeoutMs you pass. For file chooser dialogs opened by a click, see page.waitForFileChooser on Dialogs and frames.

Drag and drop

page.locator("#card-1").dragTo(page.locator("#done-column"))
page.locator(".handle").dragTo(
    page.locator(".track"),
    sourcePosition = Position(5.0, 5.0),
    targetPosition = Position(200.0, 10.0),
)
page.dragAndDrop("#card-1", "#done-column")

Prop

Type

The target must be a locator from the same page.

  • In-page runtime: if the source or an ancestor has draggable=true, HTML5 drag events are dispatched with a shared DataTransfer, in 8 steps: dragstart, drag, dragenter/dragleave, dragover, drop (only when the last dragover was canceled) and dragend. Otherwise the drag is a pointer and mouse sequence (pointerdown/mousedown, 8 moves, pointerup/mouseup), which drag libraries built on pointer events understand.
  • JVM native input: a trusted mouse move to the source, press, 10-step move to the target, then release.

Scrolling

page.locator("#footer").scrollIntoViewIfNeeded()
page.locator("#row-200").scrollIntoViewIfNeeded(block = ScrollPosition.Start)
page.scroll(ScrollDirection.Down, 600.0)
page.scrollToBottom()

scrollIntoViewIfNeeded(block, inline, timeoutMs) waits for the element to be visible and stable, then calls scrollIntoView with the given alignment.

Prop

Type

Page-level scrolling uses window.scrollBy/window.scrollTo on every backend:

MethodEffect
page.scroll(direction: ScrollDirection, amount: Double = 300.0)Scrolls the window by exactly amount CSS pixels Up, Down, Left or Right. amount must be finite and non-negative. agent-browser: scroll down 300.
page.scrollToTop()window.scrollTo(0, 0)
page.scrollToBottom()Scrolls to the bottom of the document.

To scroll an inner scrollable element, scroll a child into view, or use page.mouse.wheel (JVM, iOS) or page.touchscreen.scroll over it (see Input devices).

Dispatching events and evaluating

page.locator("#upload-zone").dispatchEvent("dragenter")
val width = page.locator("#chart").evaluate("el => el.getBoundingClientRect().width").value
page.locator("#bug").highlight(color = "rgba(0,128,255,0.3)", durationMs = 0)
  • dispatchEvent(type, timeoutMs) waits for the element to be attached and dispatches a bubbling, cancelable, composed event. The constructor follows the type: MouseEvent for click, dblclick, mouse*, contextmenu and auxclick; PointerEvent for pointer*; KeyboardEvent for key*; FocusEvent for focus*/blur; DragEvent for drag*/drop; TouchEvent for touch*; InputEvent for input/beforeinput; Event otherwise. Playwright: there is no eventInit argument.
  • evaluate(expression, timeoutMs) calls a JavaScript function with the element and returns JsResult(value, type). The function runs synchronously. A returned Promise is not awaited. It throws when KmpwrightConfig.allowEval = false.
  • highlight(color, borderColor, durationMs) draws a fixed-position overlay over the element's current box, for debugging.

Prop

Type

Page shortcuts

Each shortcut is page.locator(selector).<action>(…). The selector accepts every engine and snapshot refs. Playwright: same names on page.

ShortcutEquivalent
page.click(selector, button, clickCount, delayMs, position, force, timeoutMs)locator(selector).click(…)
page.dblclick(selector, button, delayMs, position, force, timeoutMs)locator(selector).dblclick(…)
page.tap(selector, force, modifiers, timeoutMs)locator(selector).tap(…)
page.fill(selector, value, force, timeoutMs)locator(selector).fill(…)
page.clear(selector, force, timeoutMs)locator(selector).clear(…)
page.type(selector, text, delayMs, timeoutMs)locator(selector).pressSequentially(text, delayMs, timeoutMs)
page.press(selector, key, delayMs, timeoutMs)locator(selector).press(…)
page.hover(selector, force, timeoutMs)locator(selector).hover(…)
page.focus(selector, timeoutMs)locator(selector).focus(…)
page.check / uncheck(selector, force, timeoutMs)locator(selector).check() / uncheck()
page.setChecked(selector, checked, force, timeoutMs)locator(selector).setChecked(…)
page.selectOption(selector, value | values, force, timeoutMs)locator(selector).selectOption(…)
page.setInputFiles(selector, files, force, timeoutMs)locator(selector).setInputFiles(…)
page.dragAndDrop(source, target, sourcePosition, targetPosition, timeoutMs)locator(source).dragTo(locator(target), …)
page.dispatchEvent(selector, type, timeoutMs)locator(selector).dispatchEvent(…)
page.highlight(selector, color, borderColor, durationMs)locator(selector).highlight(…)

The shortcuts are extension functions in com.bernaferrari.kmpwright.core. For the read-only shortcuts (textContent, isVisible, …), see Locators.

Input devices

page.mouse, page.keyboard and page.touchscreen act at viewport coordinates (CSS pixels, the same space as locator.boundingBox()) or on the focused element. They skip actionability checks.

val box = page.locator("#map").boundingBox()!!
page.mouse.move(box.x + 10, box.y + 10, steps = 5)
page.mouse.down()
page.mouse.move(box.x + 200, box.y + 80, steps = 20)
page.mouse.up()
page.mouse.wheel(0.0, 400.0)                       // positive deltaY scrolls down

page.keyboard.down("Shift")
page.keyboard.press("ArrowLeft")
page.keyboard.up("Shift")
page.keyboard.insertText("héllo")                  // no key events

page.touchscreen.swipe(300.0, 600.0, 300.0, 200.0, speed = 1200.0)
page.touchscreen.longTap(120.0, 300.0, durationMs = 1_000)
page.touchscreen.pinch(200.0, 300.0, scale = 2.0)
DeviceMethods
Mousemove(x, y, steps = 1, timeoutMs = null), click(x, y, button = MouseButton.Left, clickCount = 1, delayMs = 0, timeoutMs = null), dblclick(x, y, button = MouseButton.Left, delayMs = 0, timeoutMs = null), down(button = MouseButton.Left, clickCount = 1), up(button = MouseButton.Left), wheel(deltaX, deltaY, timeoutMs = null), drag(startX, startY, endX, endY, timeoutMs = null)
KeyboardinsertText(text), type(text, delayMs = 0, timeoutMs = null), down(key), up(key), press(key, delayMs = 0, timeoutMs = null)
Touchscreentap(x, y, modifiers = 0, timeoutMs = null), longTap(x, y, durationMs = 750, timeoutMs = null), scroll(x, y, deltaX = 0.0, deltaY = -100.0, steps = 5, timeoutMs = null), swipe(x1, y1, x2, y2, speed = 500.0, timeoutMs = null), pinch(x, y, scale, timeoutMs = null)

Use mouse.click(x, y, clickCount = 3) for a triple click. Touchscreen.scroll moves the finger by (deltaX, deltaY), so the default deltaY = -100.0 drags upward. swipe's speed is in CSS pixels per second. In pinch, a scale above 1 zooms in.

The timeoutMs parameters are enforced for touchscreen.tap, keyboard.type, keyboard.press and mouse.drag on every backend. Other device calls accept timeoutMs but a backend may not enforce it.

With InputMode.JsOnly, reading page.touchscreen throws KmpwrightUnsupportedException. page.mouse and page.keyboard require InputMode.Hybrid, the default, and throw otherwise.

Device behavior per backend

CallAndroid WebViewiOS WKWebViewJVM (CDP)
touchscreen.tap, longTap, scroll, swipeNative MotionEvents through WebView.dispatchTouchEventSynthetic TouchEvents dispatched in the page at elementFromPointTrusted Input.dispatchTouchEvent with touch emulation enabled
touchscreen.pinchWebView.zoomBy(scale); x/y are ignoredThrows KmpwrightUnsupportedExceptionTwo-finger trusted touch gesture
mouse.clickA native touch tap per click; button is ignoredSynthetic mouse events plus a synthetic touch tapTrusted press/release pairs with increasing clickCount
mouse.moveSynthetic mousemove on the element at the pointSynthetic move events, interpolated over stepsTrusted mouseMoved, interpolated over steps
mouse.down / upSynthetic mousedown/mouseup on documentSynthetic, at the last positionTrusted, at the last position
mouse.wheelNative WebView.scrollBy; no wheel eventSynthetic WheelEvent at the last position, then scrolls unless preventedTrusted mouseWheel at the last position
mouse.dragNative touch swipeSynthetic mouse and touch eventsTrusted move, press, interpolated moves, release
keyboard.down / up / pressNative KeyEvents; printable characters through the virtual key mapSynthetic KeyboardEvents. press also inserts printable characters and applies DOM editing for Backspace, Delete, Enter, Tab, ArrowLeft, ArrowRight, Home and EndTrusted Input.dispatchKeyEvent
Chords in keyboard.press("Control+A")Not parsed; use down/press/upNot parsed; use down/press/upParsed, including ControlOrMeta
keyboard.insertTextexecCommand('insertText') in the focused fieldDOM insertion with beforeinput/inputTrusted Input.insertText
keyboard.typeinsertText per character, no key eventsinsertText per character, no key eventsKey events per character

For chords that work on every backend, use locator.press("Control+A"), or hold the modifier with keyboard.down("Control"), keyboard.press("a"), keyboard.up("Control").

Per-backend summary

Locator actions on Android and iOS always run in the in-page runtime, whatever the InputMode. The events they dispatch are untrusted (event.isTrusted == false). Pages that ignore untrusted events need the JVM backend or page.touchscreen/page.keyboard on Android.

Locator actionAndroid WebView, iOS WKWebViewJVM, InputMode.Hybrid (default)JVM, InputMode.TouchOnly
click, dblclickRuntime pointer and mouse eventsTrusted mouseTrusted touch tap
tapRuntime pointer, touch and mouse eventsTrusted touchTrusted touch
hover, fill, clear, pressSequentially, press, dragToRuntimeTrusted Chrome inputRuntime
check, uncheck, setCheckedRuntime clickTrusted clickTrusted touch tap
selectOption, focus, blur, scrollIntoViewIfNeeded, dispatchEvent, evaluate, highlightRuntimeRuntimeRuntime
setInputFilesClear onlyDOM.setFileInputFilesDOM.setFileInputFiles

With InputMode.JsOnly, the JVM backend uses the runtime for every locator action, like the WebViews.

Each page reports its input support through capabilities:

PageCapabilityAndroidiOSJVMWhat the backend notes say
TouchInputSupportedPartialPartialAndroid dispatches native WebView events. iOS uses touch-first event synthesis. JVM uses CDP touch emulation for taps, swipes, scrolls and pinch.
MouseInputPartialPartialSupportedAndroid mouse "may fall back to hover/tap approximations". JVM uses native CDP Input events.
KeyboardInputSupportedPartialSupportediOS has modifier-aware printable keys and DOM text-input fallbacks; "low-level key semantics remain partial".
page.capabilities().require(PageCapability.MouseInput)   // throws on Android and iOS

Action policy

When an ActionPolicy is configured, each action is checked against it before it runs. A denied action throws KmpwrightException. A Confirm result calls the page's confirm callback.

ActionTypeActions
Clickclick, dblclick, tap, check, uncheck, setChecked, dispatchEvent, mouse.click/dblclick/down/up, touchscreen.tap/longTap
Fillfill, clear, setInputFiles
TypepressSequentially, keyboard.type, keyboard.insertText
Presspress, keyboard.press/down/up
Hoverhover, mouse.move
Focusfocus, blur
SelectselectOption
DragdragTo, mouse.drag, touchscreen.swipe
ScrollscrollIntoViewIfNeeded, page.scroll/scrollToTop/scrollToBottom, mouse.wheel, touchscreen.scroll/pinch
Evaluateevaluate
Screenshotscreenshot

On this page