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')