KMPwright

Quick start

Attach KMPwright to an Android WebView or iOS WKWebView, or launch Chromium on the JVM, then click, assert, screenshot and run your first agent loop.

Every backend gives you the same Page. Pick your platform, get a page, then the rest of the code is shared.

This page assumes you added the dependency (Installation) and imported com.bernaferrari.kmpwright.core.*.

Get a page

Attach to a WebView your app already displays with AndroidPlatformContext. Create the Browser on the main thread.

import android.os.Bundle
import android.webkit.WebView
import androidx.activity.ComponentActivity
import androidx.lifecycle.lifecycleScope
import com.bernaferrari.kmpwright.core.*
import kotlinx.coroutines.launch

class MainActivity : ComponentActivity() {
    private lateinit var browser: Browser

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        val webView = WebView(this).apply {
            settings.javaScriptEnabled = true   // KMPwright automates through page scripts
            settings.domStorageEnabled = true
        }
        setContentView(webView)

        browser = Browser(AndroidPlatformContext(this, webView)) // main thread
        lifecycleScope.launch {
            val shot = checkExample(browser.page())
            // shot.bytes is a PNG
        }
    }

    override fun onDestroy() {
        browser.dispose() // non-suspending teardown for lifecycle callbacks
        super.onDestroy()
    }
}

AndroidPlatformContext(context, webView, delegateWebViewClient, delegateWebChromeClient):

ParameterMeaning
contextAny Context; the app's files directory is remembered for saved state.
webViewYour WebView. When null, KMPwright creates and owns a detached WebView (not on screen).
delegateWebViewClient, delegateWebChromeClientReceive every callback. When null, the clients already installed on webView keep receiving them; they are restored when the page is disposed.

Enable JavaScript on a WebView you supply. A WebView KMPwright creates itself has JavaScript and DOM storage enabled, file and content access disabled, and mixed content blocked.

Act, assert, screenshot

This function is the checkExample used above. It is plain commonMain code and supplies its own HTML, so your first workflow needs no external website or demo server. The screenshot is returned after the form displays Subscribed.

suspend fun checkExample(page: Page): ScreenshotResult {
    page.setContent("""
        <label>Email <input id="email" type="email"></label>
        <button onclick="document.querySelector('#status').textContent = 'Subscribed'">
            Subscribe
        </button>
        <p id="status" role="status">Ready</p>
    """.trimIndent())

    page.getByLabel("Email").fill("ada@example.com")
    page.getByRole("button", name = "Subscribe").click()
    expect(page.locator("#status")).toHaveText("Subscribed")
    return page.screenshot()
}
  • locator / getBy* return lazy locators. Actions wait until the element is attached, visible, enabled, stable and hit-testable, and retry until the action timeout (30 s by default).
  • expect(...) assertions retry until they hold or TimeoutConfig.waitForSelector passes, then throw KmpwrightTimeoutException.
  • ScreenshotResult has bytes, width, height (and annotations for annotated screenshots).

Your first agent loop

page.runCommand takes one agent-browser style command line and returns text for a model. A minimal loop: snapshot, let the model pick a ref, act, repeat.

suspend fun agentStep(page: Page, askModel: suspend (String) -> String): String {
    val observation = page.runCommand("snapshot -i").output
    // @e1 [heading] "Example Domain" [level=1]
    // @e2 [link] "Learn more"
    val command = askModel(observation)            // e.g. "click @e2"
    return try {
        page.runCommand(command).output            // "Clicked @e2"
    } catch (error: KmpwrightException) {
        "Error: ${error.message}"                  // return failures to the model too
    }
}
  • Refs such as @e2 come from the latest snapshot and stay valid until the next one. Take a new snapshot after the page changes.
  • Targets can also be any selector: click "role=button[name=\"Save\"]", fill label=Email "a@b.co".
  • runCommand("help") returns the command reference (AgentCommands.HELP), which you can put in the tool description.
  • The result also carries structured data: result.snapshot for snapshot, result.screenshot for screenshot.

Wrap page text for the model in a content boundary so it can tell page content from instructions: see Security. The Agents section covers tool wiring in depth.

Close what you open

CallKindUse it when
page.close()suspendYou are in a coroutine and want to wait until the page's resources are released. Idempotent.
browser.close()suspendCloses every page and disconnects, waiting for teardown. Idempotent.
launched.close()suspendJVM: also stops the launched Chromium and deletes its temporary profile (caller-provided profile directories are kept).
page.dispose(), browser.dispose()non-suspendingLifecycle callbacks such as Activity.onDestroy or Compose onDispose. Returns immediately, best effort, idempotent.

On Android and iOS, closing a page you attached to your own WebView/WKWebView detaches automation (and restores your original Android clients); the view stays yours. A page backed by a WebView KMPwright created is destroyed.

use {} runs a block and always closes the receiver afterwards, even on failure or cancellation. It works on Browser, BrowserContext, Page and the JVM LaunchedBrowser:

browser.newPage().use { page ->
    page.goto("https://example.com")
    page.getByText("Learn more").click()
}

Next

On this page