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):
| Parameter | Meaning |
|---|---|
context | Any Context; the app's files directory is remembered for saved state. |
webView | Your WebView. When null, KMPwright creates and owns a detached WebView (not on screen). |
delegateWebViewClient, delegateWebChromeClient | Receive 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 orTimeoutConfig.waitForSelectorpasses, then throwKmpwrightTimeoutException.ScreenshotResulthasbytes,width,height(andannotationsfor 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
@e2come 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.snapshotforsnapshot,result.screenshotforscreenshot.
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
| Call | Kind | Use it when |
|---|---|---|
page.close() | suspend | You are in a coroutine and want to wait until the page's resources are released. Idempotent. |
browser.close() | suspend | Closes every page and disconnects, waiting for teardown. Idempotent. |
launched.close() | suspend | JVM: also stops the launched Chromium and deletes its temporary profile (caller-provided profile directories are kept). |
page.dispose(), browser.dispose() | non-suspending | Lifecycle 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
Installation
Add kmpwright-core (and optionally kmpwright-compose) to a Kotlin Multiplatform, Android, iOS or JVM project, with the minimum versions and threading rules each platform needs.
Sample app
The Compose Multiplatform sample puts an agent console, runnable recipes and an inspector next to a live WebView on Android and iOS.