EXTENSIONS
UI Testing
Automate cross-browser web testing using Karate's built-in driver. Karate provides Chrome DevTools Protocol support for native Chrome automation, W3C WebDriver compatibility for cross-browser testing, and Playwright integration for advanced scenarios — all with the same simple Gherkin syntax.
Karate v2 features a completely rewritten CDP driver with auto-wait, browser pooling, and Shadow DOM support. Chrome/Chromium/Edge via CDP is the primary driver. W3C WebDriver is fully supported for cross-browser testing (Firefox, Safari, Edge). Playwright support is planned. See What's New in v2 for details.
On this page:
- Basic Setup - Launch browsers and navigate to pages
- Driver Configuration - Browser options, headless, remote WebDriver, proxy
- Driver Types - Chrome, Playwright, WebDriver, Appium
- Locators - CSS, XPath, wildcards, friendly locators, tree walking
- Element Interactions - Click, input, select, mouse, file upload
- Waiting Strategies - waitFor, retry, waitUntil, chaining
- Browser JavaScript - script(), scriptAll(), function composition
- Pages and Frames - Tabs, iframes, dialogs
- Cookies - Set, get, delete cookies
- Screenshots - Capture, PDF, visual regression
- HTTP Interception - Mock browser requests with Karate mocks
- Hybrid Tests - Combine API and UI testing
- Mobile Testing - Device emulation, Appium
- Docker and CI - karate-chrome, Selenium Grid, distributed testing
- Debugging - VS Code extension, karate.stop(), highlight
- Code Reuse - Shared features, locator patterns
- Java API - Driver and Chrome programmatic APIs
To understand how Karate compares to other UI automation frameworks, read: The world needs an alternative to Selenium - so we built one.
Basic Browser Setup
Configure the driver to launch a browser and navigate to a page:
Feature: Simple browser test
Scenario: Open a webpage
# Tell Karate to use Chrome browser
* configure driver = { type: 'chrome' }
# Open the GitHub login page - this launches the browser
* driver 'https://github.com/login'
# Verify the page loaded by checking the title
* match driver.title contains 'GitHub'
The driver keyword navigates to a URL and initializes the browser instance. All subsequent steps use this driver until the scenario ends.
- The browser launches on the first
driverkeyword - The browser closes automatically when the scenario ends
- Use
driver.quit()only if you need explicit cleanup mid-scenario
Browser Pooling (v2)
Karate v2 automatically pools browser instances when running in parallel — no configuration needed:
Runner.path("features/")
.parallel(4); // pool of 4 browser instances auto-created
- Browser instances are reused across scenarios
- Pool size auto-scales to match the parallelism level
- Clean state reset between scenarios (
about:blank, clear cookies)
For custom browser sources (e.g., Testcontainers), extend PooledDriverProvider. See Docker and CI.
Driver Configuration
Configuration Options
Configure browser behavior with the configure driver statement:
Feature: Driver configuration
Scenario: Chrome with options
# headless: run without visible window, showDriverLog: log HTTP traffic for debugging
* configure driver = { type: 'chrome', headless: true, showDriverLog: true }
* driver 'https://example.com'
| Key | Description | Default |
|---|---|---|
type | Browser type (see Driver Types) | - |
executable | Path to browser or driver executable | Auto-detected |
start | Whether Karate should start the executable | true |
stop | Whether to quit browser after scenario (set false for debugging — see Keep Browser Open) | true |
port | Port for driver communication | Varies by type |
host | Host for driver communication | localhost |
headless | Run without visible browser window (Chrome only) | false |
timeout | Page load timeout in milliseconds | 30000 |
retryCount | Auto-wait attempts before an element op (click, input, etc.) gives up | 3 |
retryInterval | Milliseconds between auto-wait attempts | 500 |
showDriverLog | Include WebDriver HTTP traffic in report | false |
showProcessLog | Include executable/driver logs in report | false |
showBrowserLog | Include browser console logs in report | true |
addOptions | Additional CLI arguments as array | null |
beforeStart | OS command to run before scenario (e.g., start video recording) | null |
afterStop | OS command to run after scenario (e.g., stop video recording) | null |
videoFile | Path to video file to embed in HTML report | null |
httpConfig | HTTP client config for remote WebDriver, e.g., { readTimeout: 120000 } | null |
attach | URL pattern to attach to existing Chrome session (type: chrome, start: false) | null |
userDataDir | Path to Chrome user data directory (pass null to use system defaults) | Auto-created |
highlight | Highlight elements before actions (for demos/debugging) | false |
highlightDuration | Duration to highlight elements in milliseconds | 3000 |
screenshotOnFailure | Auto-capture screenshot on step failure and embed in the report — see Auto-Screenshot on Failure | true |
Headless Mode
Run tests without a visible browser window. Ideal for CI/CD pipelines where no display is available:
Feature: Headless testing
Scenario: Run in headless mode
# headless: true runs Chrome without opening a visible window
* configure driver = { type: 'chrome', headless: true }
* driver 'https://example.com'
# You can still take screenshots in headless mode
* screenshot()
Chrome Options
Pass Chrome-specific arguments and preferences. This example shows how to set up driver config globally in karate-config.js:
function fn() {
var config = {
driverConfig: {
type: 'chrome',
// Chrome command-line arguments
addOptions: [
'--disable-notifications', // Block notification popups
'--disable-popup-blocking', // Allow popups (useful for OAuth flows)
'--start-maximized' // Open browser maximized
],
// Chrome preferences (browser settings)
prefs: {
'download.default_directory': '/tmp/downloads' // Set download folder
}
}
};
// Apply driver config globally for all scenarios
karate.configure('driver', config.driverConfig);
return config;
}
webDriverUrl
Connect to a remote WebDriver server (Selenium Grid, Zalenium, cloud providers):
Feature: Remote WebDriver
Scenario: Connect to Selenium Grid
# start: false means don't launch a local browser - connect to remote instead
# webDriverUrl points to the Selenium Grid hub
* configure driver = { type: 'chromedriver', start: false, webDriverUrl: 'http://localhost:4444/wd/hub' }
* driver 'https://example.com'
webDriverSession
Pass custom capabilities to WebDriver. This is useful for headless mode, window size, or browser-specific options:
Feature: Custom WebDriver session
Background:
# Define WebDriver capabilities - this controls browser behavior
# alwaysMatch: capabilities that must be supported by the browser
* def session = { capabilities: { alwaysMatch: { browserName: 'chrome', 'goog:chromeOptions': { args: ['--headless', 'window-size=1280,720'] } } } }
Scenario: Chrome with custom capabilities
# Pass the session config to the driver
* configure driver = { type: 'chromedriver', webDriverSession: '#(session)' }
* driver 'https://example.com'
Common capabilities you can customize:
acceptInsecureCerts- Accept self-signed certificatesmoz:firefoxOptions- Firefox-specific options (e.g., headless mode)proxy- Proxy configuration (see Proxy Configuration)
A driver.sessionId accessor for cloud-side artifact retrieval is on the v2 roadmap; for now use driver.intercept(...) or your provider's REST API keyed off the webDriverUrl.
webDriverPath
For Appium or Selenium Grid on localhost, you may need to append a path:
Feature: Appium with path
Scenario: Connect to local Appium
* configure driver = { type: 'android', webDriverPath: '/wd/hub' }
* driver { webDriverSession: { desiredCapabilities: { app: 'com.example.app' } } }
Driver Types
Choose the driver type based on your testing needs:
| Type | Port | Description |
|---|---|---|
chrome | 9222 | Native Chrome via DevTools Protocol. Recommended for development. |
playwright | 4444 | Playwright integration for cross-browser testing. |
msedge | 9222 | Microsoft Edge (Chromium) via DevTools Protocol. |
chromedriver | 9515 | W3C Chrome WebDriver. |
geckodriver | 4444 | W3C Firefox WebDriver. |
safaridriver | 5555 | W3C Safari WebDriver (macOS only). |
msedgedriver | 9515 | W3C Microsoft Edge WebDriver. |
mswebdriver | 17556 | Microsoft Edge Legacy WebDriver (deprecated). |
iedriver | 5555 | Internet Explorer 11 WebDriver (deprecated). |
android | 4723 | Android automation via Appium. |
ios | 4723 | iOS automation via Appium. |
winappdriver | 4727 | Windows desktop application automation. |
Start with type: 'chrome' for development — it's fastest and requires no additional setup. In Karate v2, Chrome CDP is the primary driver with auto-wait and browser pooling built in.
Karate v2 fully supports W3C WebDriver for cross-browser testing. The chromedriver, geckodriver, safaridriver, and msedgedriver types all work with the W3C backend. Chrome CDP remains the recommended driver for development due to its speed and extra capabilities (request interception, PDF generation). Appium and WinAppDriver types are planned for a future release.
Playwright Integration
Playwright emulation support is planned for Karate v2, replacing the v1 experimental integration. This will enable Firefox and WebKit testing via Playwright's CDP interface. The documentation below applies to Karate v1.
Playwright provides cross-browser support with Chromium, Firefox, and WebKit engines.
Setup
Add the Playwright dependency before karate-core:
<dependency>
<groupId>io.karate</groupId>
<artifactId>karate-playwright</artifactId>
<scope>test</scope>
</dependency>
Basic Usage
Feature: Playwright browser
Scenario: Test with Playwright
* configure driver = { type: 'playwright' }
* driver 'https://example.com'
* match driver.title == 'Example Domain'
Playwright Options
Configure browser type, context, and other Playwright-specific settings:
Feature: Playwright options
Background:
* def pwOptions = { browserType: 'firefox', context: { viewport: { width: 1280, height: 720 } } }
Scenario: Firefox with Playwright
* configure driver = { type: 'playwright', playwrightOptions: '#(pwOptions)' }
* driver 'https://example.com'
| Option | Description | Default |
|---|---|---|
browserType | chromium, firefox, or webkit | chromium |
channel | Browser channel (e.g., chrome, msedge) | chrome |
context | Playwright browser context options | - |
installBrowsers | Auto-download browsers on first run | true |
For connecting to a remote Playwright server, use playwrightUrl:
Feature: Remote Playwright
Scenario: Connect to Playwright server
* configure driver = { type: 'playwright', start: false, playwrightUrl: 'ws://localhost:4444' }
* driver 'https://example.com'
Playwright Legacy (NodeJS)
For environments requiring a separate Playwright server, you can start one using NodeJS:
npm i -D playwright
Create a server script (playwright/server.js):
const playwright = require('playwright');
const port = process.argv[2] || 4444;
const browserType = process.argv[3] || 'chromium';
const headless = process.argv[4] == 'true';
const serverPromise = playwright[browserType].launchServer({ headless: headless, port: port });
serverPromise.then(bs => console.log(bs.wsEndpoint()));
Create a batch file to start the server, and configure Karate to use it:
Feature: Playwright Legacy
Scenario: Use external Playwright server
* configure driver = { type: 'playwright', executable: 'path/to/start-server' }
* driver 'https://example.com'
Karate scans the log for ws:// URLs and passes three arguments to the executable: port, browserType, and headless.
W3C WebDriver (v2)
Karate v2 includes a fully compliant W3C WebDriver backend for cross-browser testing with Firefox, Safari, Edge, and Chrome. This uses the standard WebDriver protocol — the same one used by Selenium — but with Karate's simple Gherkin syntax.
Quick Examples
Chrome via chromedriver:
Feature: Chrome WebDriver
Scenario: Test with chromedriver
* configure driver = { type: 'chromedriver', headless: true }
* driver 'https://example.com'
* match driver.title == 'Example Domain'
Firefox via geckodriver:
Feature: Firefox WebDriver
Scenario: Test with Firefox
* configure driver = { type: 'geckodriver' }
* driver 'https://example.com'
* match driver.title == 'Example Domain'
Safari via safaridriver (macOS only):
Feature: Safari WebDriver
Scenario: Test with Safari
* configure driver = { type: 'safaridriver' }
* driver 'https://example.com'
* match driver.title == 'Example Domain'
W3C Driver Types
| Type | Executable | Default Port | Browser |
|---|---|---|---|
chromedriver | chromedriver | 9515 | Chrome |
geckodriver | geckodriver | 4444 | Firefox |
safaridriver | safaridriver | 5555 | Safari |
msedgedriver | msedgedriver | 9515 | Edge |
Karate auto-launches the driver executable. Ensure it's on your PATH or set the executable option.
Custom Capabilities
Use the capabilities option to pass W3C capabilities. They're merged into alwaysMatch:
Feature: Custom capabilities
Scenario: Headless Chrome via WebDriver
* configure driver = { type: 'chromedriver', capabilities: { 'goog:chromeOptions': { args: ['--headless'] } } }
* driver 'https://example.com'
For full control over the session payload, use webDriverSession:
Feature: Full session control
Scenario: Custom session
* def session = { capabilities: { alwaysMatch: { browserName: 'chrome', 'goog:chromeOptions': { args: ['--headless', 'window-size=1280,720'] } } } }
* configure driver = { type: 'chromedriver', webDriverSession: '#(session)' }
* driver 'https://example.com'
Remote WebDriver (Selenium Grid, Cloud)
Connect to remote WebDriver servers like Selenium Grid, SauceLabs, or BrowserStack:
Feature: Remote WebDriver
Scenario: Selenium Grid
* configure driver = { type: 'chromedriver', start: false, webDriverUrl: 'http://grid:4444/wd/hub' }
* driver 'https://example.com'
Feature: SauceLabs
Scenario: Cloud testing
* def caps = { platformName: 'Windows 10', browserVersion: 'latest', 'sauce:options': { tunnelId: 'my-tunnel' } }
* configure driver = { type: 'chromedriver', start: false, webDriverUrl: 'https://ondemand.saucelabs.com:443/wd/hub', capabilities: '#(caps)' }
* driver 'https://example.com'
CDP vs W3C WebDriver
| Feature | CDP (chrome) | W3C WebDriver (chromedriver, etc.) |
|---|---|---|
| Speed | Fastest (WebSocket) | Fast (HTTP) |
| Cross-browser | Chrome/Edge only | Any W3C browser |
| Request interception | Yes | No |
| PDF generation | Yes | No |
| Mouse (coordinate) | Yes | No |
| Keyboard input | Yes | Yes (W3C Actions API) |
| Remote/cloud | Via webSocketUrl | Via webDriverUrl |
| Setup | No extra executable | Requires driver executable |
Recommendation: Use CDP (type: 'chrome') for development and Chrome-only CI. Use W3C WebDriver for cross-browser testing or when connecting to Selenium Grid / cloud providers.
Proxy Configuration
For Chrome, use addOptions:
Feature: Chrome proxy
Scenario: With proxy
* configure driver = { type: 'chrome', addOptions: ['--proxy-server="https://proxy:5000"'] }
* driver 'https://example.com'
For WebDriver types, use webDriverSession:
Feature: WebDriver proxy
Background:
* def session = { capabilities: { browserName: 'chrome', proxy: { proxyType: 'manual', httpProxy: 'proxy:5000' } } }
Scenario: With proxy
* configure driver = { type: 'chromedriver', webDriverSession: '#(session)' }
* driver 'https://example.com'
Locators
CSS Selectors
Use CSS selectors to target elements:
Feature: CSS selectors
Scenario: Common CSS patterns
* driver 'https://example.com'
# By ID
* click('#login-button')
# By class
* click('.btn-primary')
# By attribute
* click('[data-testid="submit"]')
# Descendant
* input('.form-group input[name="email"]', 'test@example.com')
# Pseudo-class
* click('ul.menu li:first-child')
XPath Expressions
Use XPath for complex element selection (prefix with /):
Feature: XPath selectors
Scenario: XPath patterns
* driver 'https://example.com'
# By text content
* click('//button[text()="Submit"]')
# Partial text match
* click('//a[contains(text(), "Learn More")]')
# Navigate table structure
* def email = text('//tr[td[text()="John"]]/td[2]')
Wildcard Locators
Find elements by visible text content using {} syntax:
| Locator | Description |
|---|---|
{a}Click Me | First <a> with exact text "Click Me" |
{}Click Me | First element (any tag) with exact text "Click Me" |
{^}Click | First element containing text "Click" |
{^span}Click | First <span> containing text "Click" |
{div:2}Click Me | Second <div> with exact text "Click Me" |
{span/a}Click Me | First <a> inside <span> with exact text "Click Me" |
Feature: Wildcard locators
Scenario: Text-based selection
* driver 'https://example.com'
# Exact text match with tag
* click('{button}Submit')
# Partial text match
* click('{^a}Learn')
# Any tag with exact text
* waitFor('{}Success').exists
Wildcard locators select based on what users see, making tests more readable and resistant to CSS/HTML changes.
Friendly Locators
Find elements by their position relative to visible text—useful for form fields near labels:
| Method | Finds Element |
|---|---|
rightOf() | To the right of given locator |
leftOf() | To the left of given locator |
above() | Above given locator |
below() | Below given locator |
near() | Near given locator (any direction) |
Feature: Friendly locators
Scenario: Form input by label position
* driver 'https://example.com/form'
# Input field to the right of "Username" label
* rightOf('{}Username').input('john_doe')
# Checkbox to the left of "Remember me" text
* leftOf('{}Remember me').click()
# Dropdown below "Country" label
* below('{}Country').select('United States')
# Button near "Submit" text (handles varied layouts)
* near('{}Submit').click()
By default, friendly locators search for <input> elements. Override with find():
Feature: Friendly locator with find
Scenario: Find specific element type
* driver 'https://example.com'
# Find a span instead of input
* rightOf('{}Label').find('span').click()
# Find by text content
* rightOf('{}Label').find('{}Click Me').click()
Tree Walking
Karate v2's DOM navigation surface is intentionally lean and selector-based, mirroring the native W3C DOM Element API:
| API | Returns | Purpose |
|---|---|---|
closest(selector) | Element | Nearest ancestor (or self) matching a CSS selector — the W3C DOM Element.closest() |
matches(selector) | boolean | Does this element match the selector — W3C DOM Element.matches() |
locate(childSelector) | Element | Scoped descendant lookup |
locateAll(childSelector) | Element[] | Scoped descendant collection |
script(jsExpression) | any | Escape hatch — run arbitrary JS in the browser with the element bound to _ |
Feature: Tree walking
Scenario: Navigate from a labelled input to its form
* driver 'https://example.com/form'
# Walk up by selector — robust to markup changes
* def form = locate('#username').closest('form')
* match form.attribute('id') == 'test-form'
# Test membership against a selector
* match locate('#username').matches('input[type=text]') == true
# Walk from a cell up to its row, then enumerate sibling cells
* def cells = locate('//td[text()="John"]').closest('tr').locateAll('td')
* match cells.length == 4
# Arbitrary DOM walk — drop into the browser via script()
* def nextId = locate('#anchor').script('_.nextElementSibling.id')
closest('form') survives a designer wrapping the input in an extra <div>. Counting hops (e.parent.parent) does not. Think of the DOM in terms of CSS selectors — the same vocabulary you use for locate() — not parent-chain arithmetic.
Karate v1 exposed parent, children, firstChild, lastChild, previousSibling, and nextSibling on every element. v2 drops all of them by design. Rewrite in terms of selectors and scoped lookups:
| v1 pattern | v2 replacement |
|---|---|
row.parent | row.closest('tr') (or whatever the structural parent is) |
row.parent.children | row.closest('tr').locateAll('td') |
el.firstChild | el.locate(':scope > *:first-child') or el.locateAll('> *')[0] |
el.nextSibling | el.script('_.nextElementSibling') — arbitrary DOM via script() |
el.previousSibling | el.script('_.previousElementSibling') |
The script() escape hatch handles any DOM relationship CSS can't express. It runs in the browser with the element bound to _, and the return value comes back to Karate.
Locator Lookup Pattern
Maintain locators in a JSON file for reusability across tests:
{
"login": {
"username": "#username",
"password": "#password",
"submit": "[data-testid='login-btn']"
},
"dashboard": {
"welcome": "{}Welcome",
"logout": "{a}Logout"
}
}
Feature: Locator lookup
Background:
* call read('locators.json')
Scenario: Use named locators
* driver 'https://example.com/login'
* input(login.username, 'john')
* input(login.password, 'secret')
* click(login.submit)
* waitFor(dashboard.welcome).exists
This pattern provides the benefits of centralized locators without the complexity of traditional Page Object Models.
Shadow DOM (v2)
CSS and wildcard locators work inside Shadow DOM elements in Karate v2:
Feature: Shadow DOM
Scenario: Interact with shadow DOM elements
* driver 'https://example.com/web-components'
# CSS selectors pierce shadow boundaries
* click('shadow-host >> button.inner')
# Wildcard locators also work inside shadow DOM
* click('{button}Submit')
Element Interactions
Click and Input
Basic element interactions for filling forms and clicking buttons:
Feature: Basic interactions
Scenario: Form interaction
* driver 'https://example.com/login'
# input(locator, value) - types text into a field
* input('#username', 'john@example.com')
* input('#password', 'secret123')
# click(locator) - clicks a button or link
* click('button[type="submit"]')
# text(locator) - gets the visible text from an element
* match text('#welcome') contains 'Welcome'
Special Keys
Use the Key object for special keystrokes:
Feature: Special keys
Scenario: Keyboard input
* driver 'https://example.com'
# Enter key — string concatenation works for terminal keystrokes
* input('#search', 'karate testing' + Key.ENTER)
# Array form — sends each value in sequence (literal text + special keys)
* input('#search', ['karate testing', Key.ENTER])
# Key combinations
* input('#editor', Key.CONTROL + 'a')
# Tab through form
* input('#field1', 'value')
* input('#field1', Key.TAB)
# Escape to close modal
* input('body', Key.ESCAPE)
Delayed Input
Add delays between keystrokes for JavaScript-heavy forms:
Feature: Delayed input
Scenario: Slow typing for autocomplete
* driver 'https://example.com'
# 100ms delay between each character
* input('#search', 'new york', 100)
# Or with array syntax
* input('#search', ['n', 'e', 'w', Key.SPACE, 'y', 'o', 'r', 'k'], 100)
Select Dropdowns
For native HTML <select> elements:
Feature: Select dropdown
Scenario: Dropdown selection
* driver 'https://example.com/form'
# By visible text
* select('select[name="country"]', '{}United States')
# By value attribute
* select('select[name="state"]', 'CA')
# By index
* select('select[name="city"]', 2)
JavaScript-Powered Dropdowns
Modern dropdowns (Bootstrap, Material UI, etc.) require mouse interactions because they're not native HTML <select> elements:
Feature: JavaScript dropdown
Scenario: Bootstrap dropdown
* driver 'https://example.com'
# First click opens the dropdown menu
* mouse('.dropdown-toggle').click()
# Second click selects an item - use mouse() for JS-rendered elements
* mouse('{a}Option One').click()
For iterating through all dropdown items (useful for testing each option):
Feature: Loop through dropdown
Scenario: Select each option
* driver 'https://example.com'
# Get all dropdown items as an array
* def items = locateAll('a.dropdown-item')
# Define a function that opens dropdown, clicks item, waits
* def selectItem = function(item) { mouse('.dropdown-toggle').click(); item.mouse().click(); delay(500) }
# Loop through each item
* items.forEach(selectItem)
File Uploads
For native <input type="file"> elements, set the path with value() or use
input() directly — the browser treats the path string as a file selection:
Feature: File upload
Scenario: Native file input
* driver 'https://example.com/upload'
* value('#file-upload', '/path/to/document.pdf')
* click('#upload-button')
For cross-browser, dependency-free uploads, prefer Karate's HTTP multipart support (no driver round-trip):
Feature: Multipart upload
Scenario: HTTP-based upload
Given url 'https://example.com/upload'
And multipart file file = { read: 'classpath:document.pdf', filename: 'document.pdf', contentType: 'application/pdf' }
When method post
Then status 200
Mouse Actions
Complex mouse interactions:
Feature: Mouse actions
Scenario: Mouse operations
* driver 'https://example.com'
# Hover
* mouse('.menu-trigger').move()
* waitFor('.submenu').exists
# Click at coordinates
* mouse(100, 200).click()
# Double-click
* mouse('#item').doubleClick()
# Right-click (context menu)
* mouse('#file').rightClick()
# Drag and drop
* mouse('.draggable').down()
* mouse('.drop-zone').move().up()
Scroll
Scroll elements into view:
Feature: Scrolling
Scenario: Scroll to element
* driver 'https://example.com'
* scroll('#footer')
# Chain with click
* scroll('#hidden-button').click()
Waiting Strategies
Karate v2 automatically waits before element operations (click, input, etc.), significantly reducing the need for explicit waitFor() calls. Most interactions "just work" without manual waits. Use the strategies below for complex cases where auto-wait isn't sufficient.
Karate provides multiple wait strategies to handle dynamic content. Choose the right one for your use case:
waitFor()
Wait for an element to appear. Essential for pages with AJAX or lazy-loaded content:
Feature: Wait for element
Scenario: Wait for dynamic content
* driver 'https://example.com'
# Click triggers an async operation
* click('#load-data')
# waitFor() blocks until element exists, then returns the element
* waitFor('#results').exists
# Now safe to check text content
* match text('#results') contains 'Loaded'
waitForUrl()
Wait for the browser URL to change. Use after click actions that trigger page navigation:
Feature: Wait for URL
Scenario: Wait after navigation
* driver 'https://example.com'
# This click navigates to another page
* click('#next-page')
# waitForUrl uses "contains" match - no need for full URL
* waitForUrl('/page-2')
# URL now guaranteed to contain '/page-2'
* match driver.url contains '/page-2'
waitForText()
Wait for specific text content to appear in an element. Useful for status messages or loading states:
Feature: Wait for text
Scenario: Wait for status message
* driver 'https://example.com'
# Submit triggers processing
* click('#submit')
# Wait until element contains the text "Complete" (uses "contains" match)
* waitForText('#status', 'Complete')
waitForEnabled()
Wait for an element to become enabled. Common for form validation where submit is disabled until valid:
Feature: Wait for enabled
Scenario: Wait for button to enable
* driver 'https://example.com'
# Toggle checkbox to accept terms
* input('#terms', Key.SPACE)
# Button becomes enabled after checkbox is checked - wait then click
* waitForEnabled('#submit').click()
waitForResultCount()
Wait for a specific number of elements to exist. Perfect for data tables that load incrementally:
Feature: Wait for results
Scenario: Wait for table rows
* driver 'https://example.com'
# Search triggers loading of results
* click('#search')
# Wait until exactly 5 result rows exist, returns the elements
* def rows = waitForResultCount('.result-row', 5)
* match rows.length == 5
waitForAny()
Wait for any one of multiple possible elements to appear. Useful when different outcomes are possible:
Feature: Wait for any
Scenario: Handle conditional UI
* driver 'https://example.com'
# Don't know if we'll get success or error — wait for either
* retry(5, 10000).waitForAny('#success', '#error')
# Array form is equivalent — useful when locators are computed
* waitForAny(['#success', '#error'])
# optional() safely clicks if element exists, does nothing if not
* optional('#success').click()
* optional('#error').click()
waitUntil()
Wait for a JavaScript condition to become true. The most flexible wait - use for custom conditions:
Feature: Wait until condition
Scenario: Wait for page ready
* driver 'https://example.com'
# Wait for document to fully load (JS runs in browser context)
* waitUntil("document.readyState == 'complete'")
# With locator: _ is the element. Wait for progress bar to reach 100%
* waitUntil('#progress', "_.style.width == '100%'")
# Wait for element to not be disabled (! means "not")
* waitUntil('#submit', '!_.disabled')
retry()
Temporarily override the retry / wait settings for the next action. Use this when specific parts of your flow need longer waits. There are 3 forms:
retry()— use default retry settings (3 attempts, 3 second intervals)retry(count)— custom number of retry attemptsretry(count, interval)— custom attempts AND interval in milliseconds between them
The total wait time is count × interval. For example, retry(40, 250) waits up to 10 seconds.
Feature: Retry examples
Scenario: Retry with actions and waits
* driver 'https://example.com'
# retry implies waitFor before action — element must exist
* retry().click('#slow-button')
* retry(5).click('#very-slow-button')
* retry(5, 10000).click('#extremely-slow-button')
# retry with waitUntil — wait for JS condition to become true
* retry(40, 250).waitUntil("!document.querySelector('#loading-spinner')")
# retry with other wait methods
* retry(5, 10000).waitFor('#dynamic-content')
* retry(5, 10000).waitForEnabled('#submit')
* retry(5, 10000).waitForText('#status', 'Complete')
* retry(5, 10000).waitForUrl('/dashboard')
For actions like click() and input(), retry() implies a waitFor() before the action — the element must appear within the retry window or the test fails.
Default retry settings: 3 attempts with 3000ms intervals. Configure globally in karate-config.js:
// Change default retry for all scenarios
karate.configure('retry', { count: 5, interval: 2000 });
Chaining
Chain wait methods with actions for fluent, readable tests:
Feature: Method chaining
Scenario: Fluent chains
* driver 'https://example.com'
# Wait then click
* waitFor('#button').click()
# Retry with wait then click
* retry(5, 10000).waitForEnabled('#submit').click()
# Scroll then input
* scroll('#hidden-field').input('value')
# Mouse chain
* mouse('#menu').move()
* waitFor('.submenu').exists
Wait API Summary
All wait methods retry internally using the configured retry settings. Use retry() only when you need to override the defaults for a specific action:
| Method | Description |
|---|---|
waitFor('#id') | Wait for element to exist |
waitForText('#id', 'text') | Wait for element to contain text (string contains match) |
waitForEnabled('#id') | Wait for element to be enabled (shortcut for waitUntil('#id', '!_.disabled')) |
waitForUrl('path') | Wait for URL to contain string |
waitForResultCount('.class', n) | Wait for exactly n matching elements |
waitForAny('#a', '#b') | Wait for any one of multiple elements to appear |
waitUntil('expression') | Wait for browser JS expression to be truthy |
waitUntil('#id', '_.value == "x"') | Wait for element JS condition (_ is the element) |
retry(n, ms).waitFor('#id') | Override retry settings for this wait |
retry(n, ms).click('#id') | Wait for element then click (waitFor implied) |
- Use
waitFor()for the first element on a newly loaded page. Stick to this for 95% of tests. - Use
retry()only when you need to override the default wait time, e.g. for slow-loading content. retry().click('#id')is equivalent towaitFor('#id').click()— prefer the latter for readability.
Browser JavaScript
script()
Execute JavaScript in the browser:
Feature: Browser JavaScript
Scenario: Execute script
* driver 'https://example.com'
# Simple evaluation
* def result = script('1 + 2')
* match result == 3
# DOM manipulation
* script("document.querySelector('#hidden').style.display = 'block'")
# Get element property
* match script('#myDiv', '_.innerHTML') contains 'Hello'
# Trigger event
* waitFor('#input').script("_.dispatchEvent(new Event('change'))")
scriptAll()
Execute JavaScript on all matching elements:
Feature: Script all elements
Scenario: Extract table data
* driver 'https://example.com'
* def texts = scriptAll('table td', '_.textContent')
* match texts contains 'Expected Value'
scriptAll() with Filter
Filter results using a JavaScript predicate:
Feature: Filter script results
Scenario: Get specific cells
* driver 'https://example.com'
# Filter for cells containing "data"
* def filtered = scriptAll('td', '_.textContent', function(x){ return x.contains('data') })
* match filtered == ['data1', 'data2']
locateAll() with Filter
Filter located elements:
Feature: Filter located elements
Scenario: Find elements by attribute pattern
* driver 'https://example.com'
* def filter = function(el){ return el.attribute('data-id').startsWith('user_') }
* def userElements = locateAll('[data-id]', filter)
* userElements[0].click()
Function Composition
Create reusable JavaScript functions:
Feature: Function composition
Background:
# Reusable function to extract text from elements
* def getTexts = function(locator){ return scriptAll(locator, '_.textContent') }
Scenario: Use composed function
* driver 'https://example.com'
* def menuItems = getTexts('.menu-item')
* match menuItems contains 'Home'
Looping
Looping Over Elements
Iterate through located elements:
Feature: Loop over elements
Scenario: Click all items
* driver 'https://example.com'
* def buttons = locateAll('.action-button')
* buttons.forEach(btn => btn.click())
Loop Until
Repeat an action until a condition is met:
Feature: Loop until condition
Scenario: Delete all rows
* driver 'https://example.com'
* def deleteRow =
"""
function() {
if (!exists('.data-row')) return true;
click('.delete-button');
delay(500);
}
"""
* waitUntil(deleteRow)
Pages and Frames
Page Navigation
Control browser navigation:
Feature: Page navigation
Scenario: Navigate browser history
* driver 'https://example.com/page1'
* click('#next')
* waitForUrl('/page2')
# Go back
* driver.back()
* match driver.url contains '/page1'
# Go forward
* driver.forward()
# Refresh
* driver.refresh()
# Hard reload (clears cache)
* reload()
Multiple Windows and Tabs
Switch between browser windows:
Feature: Multiple windows
Scenario: Handle new tab
* driver 'https://example.com'
* click('#open-new-tab')
# Switch by index
* switchPage(1)
* match driver.title contains 'New Page'
# Switch back
* switchPage(0)
# Switch by title (contains match)
* switchPage('New Page')
# Close current tab
* close()
Working with Iframes
Switch context to an iframe:
Feature: Iframe handling
Scenario: Interact with iframe content
* driver 'https://example.com'
# Switch by selector
* switchFrame('#editor-iframe')
* input('#content', 'Hello from iframe')
# Switch back to main page
* switchFrame(null)
# Switch by index
* switchFrame(0)
Dialogs
Handle JavaScript dialogs (alert, confirm, prompt):
Feature: Dialog handling
Scenario: Handle dialogs
* driver 'https://example.com'
* click('#show-alert')
# Get dialog text
* match driver.dialogText == 'Are you sure?'
# Accept dialog
* dialog(true)
# Or cancel
* dialog(false)
# Enter text in prompt and accept
* dialog(true, 'User input')
Cookies
Manage browser cookies:
Feature: Cookie management
Scenario: Cookie operations
* driver 'https://example.com'
# Set cookie
* cookie({ name: 'session', value: 'abc123', domain: '.example.com' })
# Get specific cookie
* def myCookie = cookie('session')
* match myCookie.value == 'abc123'
# Get all cookies
* def allCookies = driver.cookies
# Delete specific cookie
* deleteCookie('session')
# Clear all cookies
* clearCookies()
* match driver.cookies == '#[0]'
Bulk-set with setCookies() — handy when restoring an authenticated session
from a fixture file:
Scenario: Restore cookies from fixture
* driver 'https://example.com'
* def saved = read('cookies.json')
* setCookies(saved)
* driver.refresh()
Screenshots and Visual Testing
Capturing Screenshots
Take screenshots during tests:
Feature: Screenshots
Scenario: Capture screenshots
* driver 'https://example.com'
# Full page screenshot (auto-embedded in report)
* screenshot()
# Element-clipped screenshot — bounding rect via CDP clip / W3C element/screenshot
* def png = screenshot('#main-content')
# Skip the report embed and save the bytes yourself
* def bytes = screenshot(false)
* def elementBytes = screenshot('#main-content', false)
* karate.write(elementBytes, 'product.png')
Auto-Screenshot on Failure
When a Gherkin step fails and a driver is active, Karate automatically captures a full-page PNG and embeds it against the failed step in the HTML report. Enabled by default — no screenshot() call needed in your scenarios.
function fn() {
return {
driverConfig: {
type: 'chrome',
// screenshotOnFailure: true (default — explicit here for clarity)
}
}
}
Disable it per-scenario or globally:
* configure driver = { type: 'chrome', screenshotOnFailure: false }
The flag is resolved from the live configure driver map at failure time, so per-scenario overrides take effect even when a pooled driver is being reused.
Graceful recovery. If the screenshot itself fails (browser crashed, transport dropped, page unresponsive), the failure is warn-logged and the scenario error stays clean — a dead browser never escalates into a second failure on top of your real one.
Layer custom diagnostics alongside the auto-screenshot via the onStepFailure hook — attach page HTML, console logs, or any other artefact directly to the failed step:
Background:
* configure onStepFailure =
"""
function(info) {
var html = driver.script('document.documentElement.outerHTML')
info.embed(html, 'text/html', 'page-snapshot.html')
}
"""
PDF Generation
Generate PDF from the current page (Chrome only):
Feature: PDF generation
Scenario: Save page as PDF
* configure driver = { type: 'chrome' }
* driver 'https://example.com/report'
* def pdfBytes = pdf({ orientation: 'landscape' })
* karate.write(pdfBytes, 'report.pdf')
Visual Regression Testing
Compare screenshots against baselines with the karate-image extension (activated via boot.ext('image')). A small screenGrab recipe captures, compares, and reports:
Feature: Visual regression
Scenario: Compare with baseline
* driver 'https://example.com'
* waitFor('#product-image')
* screenGrab('product')
See Image Comparison for the screenGrab recipe, the image.diff/resolve/write primitives, and detailed visual testing options.
Intercepting HTTP Requests
Intercept browser HTTP requests and route them to Karate mock servers. This enables:
- Simulating API failures and edge cases
- Replacing slow backend calls with fast mocks
- Testing without external dependencies
driver.intercept()
Set up request interception (Chrome only). Routes matching URLs to a Karate mock feature:
Feature: HTTP interception
Scenario: Mock API responses
* configure driver = { type: 'chrome' }
# Start with blank page to set up interception before any requests
* driver 'about:blank'
# Intercept any URL matching pattern and route to mock feature
# patterns: array of URL patterns (* = wildcard)
# mock: path to Karate mock feature file
* driver.intercept({ patterns: [{ urlPattern: '*api.example.com/*' }], mock: 'mock-api.feature' })
# Now navigate - requests will be intercepted
* driver 'https://example.com'
* click('#load-data')
# The mock feature returned this response
* waitForText('#result', 'Mocked Response')
Mock Feature Setup
Create a mock feature file to handle intercepted requests. Each Scenario matches different URL patterns:
@ignore
Feature: API mock
Background:
# Enable CORS - browser needs this for cross-origin requests
* configure cors = true
Scenario: pathMatches('/users/{id}')
# pathParams.id contains the {id} from URL, e.g., /users/123 -> id = "123"
* def response = { id: '#(pathParams.id)', name: 'Mock User' }
Scenario: pathMatches('/data')
# Return data from a JSON file
* def response = read('mock-data.json')
Scenario:
# Catch-all for unmatched requests - return 404
* def responseStatus = 404
Inline JS Handler
Instead of a mock feature file, you can use an inline JavaScript function. The handler receives the intercepted request and returns a response map — or null to let the request continue to the network:
Feature: Inline interception
Scenario: Mock with JS handler
* configure driver = { type: 'chrome' }
* driver 'about:blank'
* driver.intercept({ patterns: [{ urlPattern: '*/api/*' }], handler: function(req){ return { status: 200, body: '{"mocked":true}' } } })
* driver 'https://example.com'
* click('#load-data')
* waitForText('#result', 'mocked')
The handler function receives an InterceptRequest object with:
req.url— the full request URLreq.method— HTTP method (GET, POST, etc.)req.headers— request headers as a mapreq.postData— request body (for POST/PUT)
Return a map with status, body, and optionally headers to mock the response. Return null to let the request pass through to the network:
Scenario: Selective interception
* driver.intercept({ patterns: [{ urlPattern: '*' }], handler: function(req){ if (req.url.contains('/api/')) return { status: 200, body: '{}' }; return null } })
driver.intercept()works only withtype: 'chrome'- Call
intercept()once per scenario
Hybrid Tests
Combine API and UI testing in a single flow. Karate's unique strength is seamlessly mixing HTTP API calls with browser automation:
Feature: Hybrid API and UI test
Background:
# Set up API base URL for HTTP calls
* url 'https://api.example.com'
Scenario: Create via API, verify in UI
# STEP 1: Create test data via REST API (faster than UI)
Given path 'orders'
And request { product: 'Widget', quantity: 2, customerId: 123 }
When method post
Then status 201
# Save the order ID from API response
* def orderId = response.id
# STEP 2: Switch to browser and verify the order appears in UI
* configure driver = { type: 'chrome' }
* driver 'https://example.com/orders/' + orderId
* match text('.order-status') == 'Pending'
* match text('.quantity') == '2'
Pre-authenticated Sessions
Skip the login UI by getting auth credentials via API and injecting cookies. This dramatically speeds up tests:
Feature: Skip login with API auth
Background:
# STEP 1: Get auth token via API (no browser needed)
* url 'https://api.example.com'
* path 'auth/login'
* request { username: 'test', password: 'secret' }
* method post
# Save the token from API response
* def authToken = response.token
Scenario: Access protected page directly
* configure driver = { type: 'chrome' }
# STEP 2: Start browser on blank page (needed to set cookies)
* driver 'about:blank'
# STEP 3: Set the auth cookie in the browser
* cookie({ name: 'auth_token', value: authToken, domain: '.example.com' })
# STEP 4: Now navigate to protected page - already logged in!
* driver 'https://example.com/dashboard'
* match text('#welcome') contains 'Welcome'
Pre-authenticating via API can save 5-10 seconds per test by avoiding login UI interactions.
Mobile Testing
Appium integration and native mobile testing are planned for a future Karate v2 release. Device emulation via Chrome CDP works in v2. The Appium documentation below applies to Karate v1.
Device Emulation
Until a dedicated emulateDevice() helper lands in v2, set the viewport via
driver.dimensions and override the user-agent through Chrome args at
configure-time:
Feature: Mobile emulation
Scenario: iPhone-sized viewport
* configure driver = { type: 'chrome', addOptions: ["--user-agent=Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X)"] }
* driver 'https://example.com'
* driver.dimensions = { width: 375, height: 812 }
* waitFor('.mobile-menu')
Custom Viewport
Test responsive layouts:
Feature: Responsive testing
Scenario: Test breakpoints
* configure driver = { type: 'chrome' }
* driver 'https://example.com'
# Desktop
* driver.dimensions = { width: 1920, height: 1080 }
* waitFor('.desktop-nav').exists
# Tablet
* driver.dimensions = { width: 768, height: 1024 }
* waitFor('.tablet-menu').exists
# Mobile
* driver.dimensions = { width: 375, height: 667 }
* waitFor('.mobile-hamburger').exists
Appium Integration
Test native mobile apps with Appium:
Feature: Appium mobile test
Scenario: Android app test
* configure driver = { type: 'android', webDriverPath: '/wd/hub' }
* driver { webDriverSession: { desiredCapabilities: { app: 'com.example.myapp' } } }
* click('@login-button')
* input('#username', 'test')
| Locator Prefix | Platform | Description |
|---|---|---|
| (none) | android/ios | Name |
@ | android/ios | Accessibility ID |
# | android/ios | ID |
: | ios | iOS predicate string |
^ | ios | iOS class chain |
- | android | Android UIAutomator |
Screen Recording
Record test execution (Appium only):
Feature: Screen recording
Scenario: Record mobile test
* configure driver = { type: 'android' }
* driver.startRecordingScreen()
# ... test steps ...
* driver.saveRecordingScreen('test-recording.mp4', true)
You can also use driver.stopRecordingScreen() for more control, and both methods accept recording options as JSON input.
hideKeyboard
Dismiss the mobile keyboard (Appium only):
Feature: Hide keyboard
Scenario: Dismiss keyboard after input
* input('#search', 'test query')
* driver.hideKeyboard()
* click('#submit')
Docker and CI
Testcontainers (v2 Recommended)
Karate v2 works with any Chromium Docker image via Testcontainers and the PooledDriverProvider pattern:
public class ContainerDriverProvider extends PooledDriverProvider {
private final ChromeContainer container;
public ContainerDriverProvider(ChromeContainer container) {
super();
this.container = container;
}
@Override
protected Driver createDriver(Map<String, Object> config) {
return CdpDriver.connect(container.getCdpUrl(),
CdpDriverOptions.fromMap(config));
}
}
Recommended Docker images:
chromedp/headless-shell— ~200MB, fast startupselenium/standalone-chrome— includes Chrome + ChromeDriverbrowserless/chrome— Chrome with CDP support
Environment variables for Docker/CI:
export KARATE_CHROME_EXECUTABLE=/usr/bin/chromium
export KARATE_CHROME_ARGS="--no-sandbox --disable-gpu --disable-dev-shm-usage"
export KARATE_DRIVER_HEADLESS=true
Google does not publish Chrome for arm64 Linux, so selenium/standalone-chrome, selenium/hub, and selenium/node-chromium are amd64-only — they run on M-series Macs but only under QEMU emulation, where Chrome is roughly 5–10× slower than native and frequently times out. Use the community arm64 builds for local development:
seleniarm/standalone-chromium— drop-in forselenium/standalone-chromeseleniarm/hub— drop-in forselenium/hubseleniarm/node-chromium— drop-in forselenium/node-chromium
These are backed by Chromium (not Chrome) but speak the same W3C wire protocol and accept browserName: chrome capabilities, so the rest of your test code is unchanged. CI on ubuntu-latest (amd64) stays on the upstream selenium/* images. With Testcontainers' BrowserWebDriverContainer, wrap the seleniarm image with DockerImageName.parse("seleniarm/standalone-chromium:latest").asCompatibleSubstituteFor("selenium/standalone-chrome") to satisfy its allowlist.
karate-todo ships a complete Testcontainers setup you can clone and run:
ChromeContainer.java— wrapschromedp/headless-shell, exposes CDP viagetCdpUrl(), reaches the host app viahost.docker.internalContainerDriverProvider.java— extendsPooledDriverProvider, one tab per pooled slotUiTest.java— starts an in-process app, boots the container, runs the combined API + UI suite in one go
Two URLs, one app: serverUrl (browser → host.docker.internal:<port>) versus apiUrl (karate HTTP on the host → localhost:<port>). See CI/CD for the wiring.
Live report from the UiTest run: simple.feature with embedded screenshots.
Testcontainers' docker-java-api drags in an old jackson-annotations 2.10.3 that shadows the newer version Thymeleaf's JS serializer needs for embedded-server apps. If you see NoClassDefFoundError: JsonSerializeAs on first page render, pin jackson-annotations explicitly in your pom.xml to match the jackson-databind you're using (e.g. 2.21).
For autonomous browser testing via LLM agents, the commercial Karate Agent Docker container (karatelabs/karate-agent) provides Chrome, VNC, and a heredoc-over-HTTP API out of the box.
Custom DriverProvider
For more involved CI setups — sibling Docker containers, dynamic image selection, video recording, custom credentials — extend PooledDriverProvider and override createDriver(Map config). The provider sits between configure driver = { ... } (which still supplies user-tunable options) and the actual Driver construction, so you keep declarative config in karate-config.js while owning the lifecycle in Java:
public class CustomProvider extends PooledDriverProvider {
@Override
protected Driver createDriver(Map<String, Object> config) {
// example: dial a CDP container started elsewhere in the suite
String cdpUrl = MyContainerRegistry.acquireFor(Thread.currentThread());
return CdpDriver.connect(cdpUrl, CdpDriverOptions.fromMap(config));
}
@Override
public void release(ScenarioRuntime sr, Driver d) {
// e.g. POST test status back to a cloud provider, save video, etc.
super.release(sr, d);
}
}
// in your runner
Runner.path("features/")
.driverProvider(new CustomProvider())
.parallel(4);
The pool size auto-matches parallel(N). For an end-to-end Testcontainers reference, see karate-todo (linked above).
Selenium Grid
Connect to Selenium Grid or cloud providers:
Feature: Selenium Grid
Scenario: BrowserStack example
* def session = { capabilities: { browserName: 'chrome', 'bstack:options': { os: 'Windows', osVersion: '11' } } }
* configure driver = { type: 'chromedriver', start: false, webDriverUrl: 'https://user:key@hub.browserstack.com/wd/hub', webDriverSession: '#(session)' }
* driver 'https://example.com'
Debugging
VS Code Extension
The Karate extension for VS Code provides step-through debugging for UI tests. You can pause execution, inspect variables, and even step backwards to re-run steps.
Watch the debugging demo video for a walkthrough.
karate.stop()
Pause test execution to inspect browser state:
Feature: Debug pause
Scenario: Pause for inspection
* driver 'https://example.com'
* input('#username', 'admin')
# Pause here - open localhost:9000 to continue
* karate.stop(9000)
* click('#submit')
When paused, the console shows:
*** waiting for socket, type the command below:
curl http://localhost:9000
Open the URL in a browser or use curl to continue execution.
Always remove karate.stop() calls before committing tests!
Keep Browser Open (stop: false)
Leave the browser running after a scenario finishes so you can inspect the DOM with devtools. Works for both Chrome CDP (type: 'chrome') and W3C WebDriver (chromedriver, geckodriver, etc.):
Feature: Keep browser open
Scenario: Inspect after failure
* configure driver = { type: 'chromedriver', stop: false }
* driver 'https://example.com'
* click('#submit')
When stop: false is set, two things happen at scenario init/exit, each logged as a WARN to the console:
- The suite's browser pool is bypassed for that scenario — Karate creates a fresh driver instance directly rather than acquiring one from the pool, so the pool's lifecycle won't auto-quit it.
- At scenario exit, neither
driver.quit()nor a pool release fires — the browser and its driver process (e.g.,chromedriver) stay alive.
A JVM-exit shutdown hook reliably kills the driver process when the JVM finally terminates — clean exit, Ctrl-C, or OOM are all covered — so a forgotten stop: false in a local debug session can't leave a runaway browser eating memory after you abandon the run. To actually inspect the page mid-debug, keep the JVM alive: karate.stop(), an IDE breakpoint, or karate.pause(ms).
Each scenario with stop: false holds onto its browser for the rest of the run (the shutdown hook only fires at JVM exit, not at scenario boundaries). Use this for one-off local debugging — never in parallel or CI runs, where the leak persists for hours.
highlight()
Visually highlight elements during debugging:
Feature: Visual debugging
Scenario: Highlight elements
* configure driver = { type: 'chrome', highlight: true, highlightDuration: 2000 }
* driver 'https://example.com'
# Manual highlight (uses configured highlightDuration)
* highlight('#important-element')
# Override duration for a single call (millis)
* highlight('#important-element', 500)
* click('#important-element')
highlightAll()
Highlight all matching elements:
Feature: Highlight all
Scenario: Highlight multiple elements
* driver 'https://example.com'
* highlightAll('input')
# Or with a custom duration
* highlightAll('input', 500)
timeout()
Temporarily change the operation timeout for slow-loading pages. timeout(n)
sets and returns the driver for chaining; timeout() (no args) returns the
current value:
Feature: Timeout adjustment
Scenario: Wait for slow page
* configure driver = { type: 'chrome' }
* def original = timeout()
# Wait up to 3 minutes for page load
* timeout(3 * 60 * 1000)
* driver 'https://example.com/slow-page'
# Restore the previous timeout
* timeout(original)
Console Logs
Browser console capture is planned for a future v2 release; until then, hook
into the page from your test by overriding console.error/console.warn
before navigation:
Feature: Console logs
Scenario: Check for errors
* driver 'https://example.com'
* script("window.__errors = []; var orig = console.error; console.error = function(...a){ window.__errors.push(a.join(' ')); orig.apply(console, a); }")
* click('#trigger-action')
* def errors = script('window.__errors')
* match errors == '#[0]'
Code Reuse
Pattern 1: Caller Owns the Driver (Recommended)
The cleanest approach — start the driver in the top-level scenario and call features that perform actions:
Feature: User workflow
Background:
* configure driver = { type: 'chrome' }
* def baseUrl = 'https://example.com'
Scenario: Complete user action
* driver baseUrl + '/login'
* call read('login-steps.feature')
* call read('create-item-steps.feature')
@ignore
Feature: Login steps
Scenario:
* input('#username', 'testuser')
* input('#password', 'secret')
* click('#login-button')
* waitForUrl('/dashboard')
Called features inherit the driver automatically — no driver configuration needed inside them.
Pattern 2: Called Feature Starts the Driver
If the called feature initializes the driver, it automatically propagates back to the caller for shared-scope calls (call read(...) without a result variable). This matches v1 behavior — no special configuration needed:
@ignore
Feature: Login flow
Background:
* configure driver = { type: 'chrome' }
Scenario:
* driver baseUrl + '/login'
* input('#username', username)
* input('#password', password)
* click('#login-button')
* waitForUrl('/dashboard')
Feature: User workflow
Background:
* def baseUrl = 'https://example.com'
* def username = 'testuser'
* def password = 'secret'
* call read('login.feature')
Scenario: Complete user action
* click('#create-new')
* input('#name', 'Test Item')
* click('#save')
Browser instances cannot be shared across scenarios with callonce. Use call in shared scope instead.
JavaScript Function Reuse
When reusing functions defined before driver initialization, use karate.get('driver'):
Feature: Function reuse
Background:
* def deleteAllRows =
"""
function() {
var driver = karate.get('driver');
while (driver.exists('.data-row')) {
driver.click('.delete-button');
karate.pause(500);
}
}
"""
Scenario: Clean up data
* configure driver = { type: 'chrome' }
* driver 'https://example.com/admin'
* deleteAllRows()
Driving the driver from JS — karate.driver
karate.driver is a property (not a function call). Reading it returns the active
Driver for the current scenario, initialising one lazily from the live
configure driver = { ... } map on first read. It's the JS-side equivalent of the
* driver ... Gherkin step, useful when driver lifecycle is orchestrated inside a
JS function — for example, looping over a list of browser configs — where Gherkin
steps aren't reachable per iteration.
Scenario: open a page from a JS function
* configure driver = { type: 'chrome' }
* def openPage =
"""
function(url) {
karate.driver.setUrl(url) // triggers init on first read
}
"""
* openPage('https://example.com')
* match driver.title contains 'Example'
The instance returned by karate.driver is the same one bound to the root driver
variable — after karate.driver (or * driver ...) initialises it, plain driver.foo()
calls in JS just work. Reading karate.driver with no configure driver in scope throws,
with a message pointing at configure driver.
Multi-browser grid runs
Karate v2 makes it safe to call driver.quit() and then start another browser inside the
same scenario — the runtime releases the terminated driver back to the pool before
re-initialising. This unlocks the grid pattern of running a list of tests under multiple
browser configurations in succession from a single scenario, useful with grid providers
(BrowserStack, Sauce Labs, LambdaTest, Selenium Grid):
Feature: run a suite under multiple browsers
@lock=*
Scenario: cycle through browser configs
* def runWith =
"""
function (cfg, testPaths) {
karate.configure('driver', cfg)
karate.driver.setUrl(baseUrl) // init this browser
karate.map(testPaths, function(p) {
driver.clearCookies()
driver.setUrl(baseUrl)
karate.call(p)
})
driver.quit() // release; next iteration re-inits cleanly
}
"""
* def browsers = [chromeCfg, firefoxCfg, edgeCfg]
* def tests = ['login.feature', 'cart.feature']
* karate.map(browsers, cfg => runWith(cfg, tests))
@lock=* keeps the scenario from interleaving with anything else when you intentionally
serialise driver acquisition. For parallel grid runs, split the browser list into
chunks — one chunk per parallel thread — by using karate.suite.threadCount to size the
chunks (typically inside an @setup scenario that produces the rows of a dynamic outline).
Each chunk runs sequentially on its thread; chunks run in parallel.
Java API
Driver Java API
Start a driver programmatically from Java:
import io.karatelabs.driver.Driver;
import io.karatelabs.driver.cdp.CdpDriver;
// v2 uses CdpDriver or W3cDriver directly
Map<String, Object> config = Map.of("type", "chrome", "headless", true);
// typically use Runner.path().parallel() instead for Gherkin tests
Chrome Java API
Use the CDP driver directly for common tasks like HTML-to-PDF conversion or screenshots:
import io.karatelabs.driver.cdp.CdpDriver;
import io.karatelabs.common.FileUtils;
import java.io.File;
import java.nio.file.Files;
public class ChromeExample {
public static void main(String[] args) throws Exception {
CdpDriver chrome = CdpDriver.startHeadless();
chrome.setUrl("https://example.com");
// Generate PDF
byte[] pdf = chrome.pdf();
Files.write(new File("page.pdf").toPath(), pdf);
// Screenshot
byte[] screenshot = chrome.screenshot();
Files.write(new File("screenshot.png").toPath(), screenshot);
chrome.quit();
}
}
For custom Chrome options, build a CdpDriverOptions and call CdpDriver.start(options).
The pdf() method accepts a Map of options documented in Chrome DevTools Page.printToPDF.
Driver Command Reference
Navigation
| Command | Description |
|---|---|
driver 'url' | Navigate to URL (initializes driver on first call) |
driver.url = 'url' | Navigate to URL |
driver.back() | Go back in history |
driver.forward() | Go forward in history |
driver.refresh() | Reload page (keeps cache) |
reload() | Hard reload (clears cache) |
maximize() | Maximize window |
minimize() | Minimize window |
fullscreen() | Enter fullscreen |
close() | Close current tab/page |
driver.quit() | Close browser |
Element Actions
| Command | Description |
|---|---|
click(locator) | Click element |
input(locator, value) | Type into element |
input(locator, [str, Key.ENTER, ...]) | Send a sequence: literal text + special keys |
input(locator, value, delayMs) | Type with per-character delay |
clear(locator) | Clear input field |
submit() | Submit form (chain before click for page load) |
focus(locator) | Focus element |
scroll(locator) | Scroll element into view |
select(locator, value) | Select dropdown option |
mouse(locator) | Get Mouse instance for advanced actions |
highlight(locator) | Visually highlight element |
highlight(locator, millis) | Highlight with custom duration |
highlightAll(locator) | Highlight all matching elements |
highlightAll(locator, millis) | Highlight all with custom duration |
Element State
| Command | Description |
|---|---|
text(locator) | Get text content |
html(locator) | Get outer HTML |
value(locator) | Get input value |
value(locator, val) | Set input value |
attribute(locator, name) | Get HTML attribute value |
property(locator, name) | Get DOM property value |
enabled(locator) | Check if enabled |
exists(locator) | Check if exists (boolean) |
optional(locator) | Get element or no-op wrapper |
locate(locator) | Get Element instance |
locateAll(locator) | Get all matching Elements |
locateAll(locator, filter) | Get matching Elements with filter function |
position(locator) | Get absolute element position and size |
position(locator, true) | Get viewport-relative position and size |
Waiting
| Command | Description |
|---|---|
waitFor(locator) | Wait for element to exist |
waitForUrl(partial) | Wait for URL to contain string |
waitForText(locator, text) | Wait for text to appear |
waitForEnabled(locator) | Wait for element to be enabled |
waitForResultCount(locator, n) | Wait for n matching elements |
waitForAny(loc1, loc2, ...) | Wait for any element |
waitUntil(jsExpression) | Wait for JS condition |
retry() | Apply retry to next action |
delay(ms) | Sleep (avoid if possible) |
timeout(ms) | Set HTTP read timeout for slow pages |
Screenshots, PDF, Interception
| Command | Description |
|---|---|
screenshot() | Full-page PNG, embedded in the report |
screenshot(true | false) | Full-page, control whether to embed |
screenshot(locator) | Element-clipped PNG (CDP Page.captureScreenshot clip; W3C element/screenshot) |
screenshot(locator, false) | Element-clipped without embedding |
pdf() | Page rendered to PDF bytes (CDP only) |
pdf({ orientation: 'landscape', ... }) | PDF with Page.printToPDF options (CDP only) |
driver.intercept(config) | Intercept browser HTTP requests (CDP only) |
stopIntercept() | Disable interception |
Cookies
| Command | Description |
|---|---|
cookie(name) | Read a cookie by name |
cookie({ name, value, domain, ... }) | Set a single cookie |
setCookies([{...}, {...}]) | Bulk-set cookies (e.g. from a fixture file) |
deleteCookie(name) | Delete one cookie |
clearCookies() | Clear all cookies |
driver.cookies | All cookies as a list |
Window & Lifecycle
| Command | Description |
|---|---|
maximize() / minimize() / fullscreen() | Window state |
activate() | Bring browser tab to front |
driver.dimensions | Get/set { width, height, x, y } |
close() | Close current tab/page |
quit() / driver.quit() | Shut down the browser |
Positional Finders
| Command | Description |
|---|---|
rightOf(refLocator) / leftOf(refLocator) | Element to the right/left, with vertical overlap |
above(refLocator) / below(refLocator) | Element above/below, with horizontal overlap |
near(refLocator) | Closest element by Euclidean centre distance (50px default) |
.find(candidateLocator) | Resolve the first match (sorted by distance) |
.findAll(candidateLocator) | Resolve all matches |
.within(pixels) | Override the proximity tolerance |
Driver Properties
| Property | Description |
|---|---|
driver.url | Current URL (read/write — assigning navigates) |
driver.title | Page title |
driver.dimensions | { width, height, x, y } window rect (read/write) |
driver.cookies | All cookies as a list |
driver.dialogText | Current dialog message (alert/confirm/prompt) |
Next Steps
- Visual testing: Image Comparison
- Mock servers: Test Doubles
- Performance testing: Performance Testing
- Reusable features: Calling Features
- More examples: Examples and Demos