Sample app
The Compose Multiplatform sample puts an agent console, runnable recipes and an inspector next to a live WebView on Android and iOS.
The repository ships one shared sample app, written once in Compose Multiplatform and hosted on Android and iOS. It drives a real WebView/WKWebView with KMPwright while you watch, so you can try selectors, commands and recipes without writing a test harness.
Run it
Requirements: JDK 17, the Android SDK for Android, macOS with Xcode for iOS. Run commands from the repository root.
Install on a connected device or emulator (API 26+):
./gradlew :example-android:installDebugThe app enables WebView.setWebContentsDebuggingEnabled(true), so you can also attach chrome://inspect to the automated page.
What is on screen
The app is built with Material 3 Expressive (dynamic color, expressive motion, ButtonGroup-style toggles, LoadingIndicator, MaterialShapes). It has a live browser and three screens: Agent, Recipes and Inspect. They all share one Browser and one page, so whatever you do on one screen is visible on the others. Page operations are serialized, so the console, a running recipe and the ref overlay never interleave actions on the same WebView.
On launch the app loads a bundled playground page (a docs search box, a sign-up form and a counter) and takes an interactive snapshot.
Agent
An agent console for the runCommand grammar. Type a command such as open en.wikipedia.org/wiki/Kotlin, snapshot -i, click @e3 or fill @e3 "hello". Results appear newest first, right under the composer: the command, its text output, how long it took, and, for screenshot, the captured image. Every @eN in an output is tappable and writes click @eN into the composer. Suggestion chips run common commands (snapshot -i, screenshot, get title, scroll down, back, help), and starter cards walk through the loop the first time. Typing clear empties the console.
This is the same loop an LLM runs: snapshot, read refs, act on a ref.
Live ref badges
After every snapshot and every command that can change the page, the app takes a fresh interactive snapshot and draws a badge with each @e ref over its element in the live page.
- Tap a badge to run
click @eN. - Long-press a badge to switch to the Agent screen with a command prefilled:
fill @eN ""for text boxes, search boxes and combo boxes,click @eNfor everything else. - Badges stay glued to their elements while the page scrolls. The
#toggle next to the address bar shows or hides them.
The badges are built from public APIs only: page.snapshot(interactive = true) for the refs, then page.locator(ref).boundingBox() for each position. Boxes are stored in document coordinates, and the overlay adds the WebView's native scroll offset every frame, so scrolling never needs a JavaScript round trip.
Recipes
Runnable scripts, each showing the Kotlin code it executes and a step timeline that marks every step as pending, running, done or failed, with its output (text or a screenshot). Pressing Run recipe collapses the panel so you watch the page being driven, with the current step (and then a finished or failed result) shown in a banner. The grid leads with the agent loop, then groups recipes into For agents and Playwright basics:
| Recipe | What it shows |
|---|---|
| Agent loop | runCommand("snapshot -i"), then acting on a ref |
| Click and assert | locator, click, expect(...).toHaveText |
| Fill a form | fill, selectOption, check, waiting for the result |
| Type like a person | pressSequentially with a delay between keys |
| Full-page screenshot | page.screenshot(fullPage = true) |
| Mock the network | page.route answering an API call from Kotlin |
| Save and restore a session | storageState() and setStorageState() |
| Read for an LLM | page.read (HTTP fetch as Markdown) |
| Accessibility audit | accessibilityAudit() with axe-core |
The code shown for each recipe is kept identical to what its steps run, so you can copy it.
Inspect
Tabs for the page's console messages (page.consoleMessages()), recorded network requests (page.network.requests()), storage (cookies, localStorage and sessionStorage), and an accessibility audit (A11y).
Adaptive layout
- Phones: the browser fills the screen and the active screen lives in a bottom sheet. Collapsed, the sheet always shows the Agent / Recipes / Inspect switcher and the command composer; focusing the composer expands the sheet so the keyboard never covers it.
- Tablets and unfolded foldables (840 dp wide and up): the active screen as a side panel, with the browser filling the rest.
The same WebView instance moves between layouts when the window size changes; it is not recreated.
iOS navigation
| iOS version | Navigation |
|---|---|
| 26 and later | Native SwiftUI TabView and NavigationStack, so the system draws the Liquid Glass tab bar and navigation bar. Compose renders each screen. |
| 15 to 25 | Compose draws everything, navigation included. |
On iOS 26, a Kotlin NativeStudio object owns the single WKWebView, Browser and page shared by every tab. Swift asks it for a Compose view controller per tab (tabViewController(tab:)) and per pushed recipe (recipeViewController(id:)); each screen reports screenDidAppear(key:) and the visible screen adopts the shared WKWebView, so the page you are driving follows you between tabs. Compose sends navigation back to Swift through onSelectTab and onOpenRecipe callbacks.
How the sample wires KMPwright
The integration is small and is a good template for your own app:
- Each platform creates a
PlatformContextaround one WebView. On Android that is aWebViewwith JavaScript and DOM storage enabled, file and content access disabled, and mixed content blocked, wrapped inAndroidPlatformContext(context, webView). - Shared code creates
Browser(platformContext)and usesbrowser.page(). - When the screen leaves composition, it calls
browser.dispose()(the non-suspending teardown) and destroys the WebView.
val state = remember(platformContext) {
StudioState(Browser(platformContext), scope) { releaseWebViewKeyboard(platformContext) }
}
DisposableEffect(state) {
onDispose {
state.dispose() // calls browser.dispose()
disposePlatformContext(platformContext)
}
}After automation types into the page, the sample hides the soft keyboard the page opened (releaseWebViewKeyboard) without touching its own text fields.
See kmpwright-example-shared/src/commonMain for the full source.
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.
Coming from Playwright
A Playwright (JavaScript) to KMPwright mapping for lifecycle, navigation, locators, assertions, network, storage, emulation, input, dialogs, frames, downloads, screenshots and tracing, plus the differences that matter.