Skip to content

Latest commit

 

History

History
1310 lines (993 loc) · 65.2 KB

README.md

File metadata and controls

1310 lines (993 loc) · 65.2 KB

Karate Driver

UI Test Automation Made Simple.

This is new, and this first version 0.9.X should be considered BETA.

Hello World

Index

Start ZIP Release | Java | Maven Quickstart | Karate - Main Index
Config driver | configure driver | configure driverTarget | Docker / karate-chrome | Driver Types
Concepts Syntax | Special Keys | Short Cuts | Chaining | Function Composition | Browser JavaScript | Debugging | Retries | Waits
Locators Locator Types | Wildcards | Friendly Locators | rightOf() | leftOf() | above() | below() | near() | Locator Lookup
Browser driver.url | driver.dimensions | refresh() | reload() | back() | forward() | maximize() | minimize() | fullscreen() | quit()
Page dialog() | switchPage() | switchFrame() | close() | driver.title | screenshot()
Actions click() | input() | submit() | focus() | clear() | value(set) | select() | scroll() | mouse() | highlight()
State html() | text() | value() | attribute() | enabled() | exists() | position() | findAll()
Wait / JS retry() | waitFor() | waitForAny() | waitForUrl() | waitForText() | waitForEnabled() | waitUntil() | delay() | script() | scripts() | Karate vs the Browser
Cookies cookie() | driver.cookie | driver.cookies | deleteCookie() | clearCookies()
Chrome Java API | pdf() | screenshotFull()

Capabilities

Examples

Web Browser

Windows

Driver Configuration

configure driver

This below declares that the native (direct) Chrome integration should be used, on both Mac OS and Windows - from the default installed location.

* configure driver = { type: 'chrome' }

If you want to customize the start-up, you can use a batch-file:

* configure driver = { type: 'chrome', executable: 'chrome' }

Here a batch-file called chrome can be placed in the system PATH (and made executable) with the following contents:

"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" $*

For Windows it would be chrome.bat in the system PATH as follows:

"C:\Program Files (x86)\Google\Chrome\Application\chrome" %*

Another example for WebDriver, again assuming that chromedriver is in the PATH:

{ type: 'chromedriver', port: 9515, executable: 'chromedriver' }
key description
type see driver types
executable if present, Karate will attempt to invoke this, if not in the system PATH, you can use a full-path instead of just the name of the executable. batch files should also work
start default true, Karate will attempt to start the executable - and if the executable is not defined, Karate will even try to assume the default for the OS in use
port optional, and Karate would choose the "traditional" port for the given type
headless headless mode only applies to { type: 'chrome' } for now, also see DockerTarget
showDriverLog default false, will include webdriver HTTP traffic in Karate report, useful for troubleshooting or bug reports
showProcessLog default false, will include even executable (webdriver or browser) logs in the Karate report
addOptions default null, has to be a list / JSON array that will be appended as additional CLI arguments to the executable, e.g. ['--no-sandbox', '--windows-size=1920,1080']

For more advanced options such as for Docker, CI, headless, cloud-environments or custom needs, see configure driverTarget.

configure driverTarget

The configure driver options are fine for testing on "localhost" and when not in headless mode. But when the time comes for running your web-UI automation tests on a continuous integration server, things get interesting. To support all the various options such as Docker, headless Chrome, cloud-providers etc., Karate introduces the concept of a pluggable "target" where you just have to implement three methods:

public interface Target {        
    
    Map<String, Object> start();
    
    Map<String, Object> stop();

    void setLogger(com.intuit.karate.Logger logger)
    
}
  • start(): The Map returned will be used as the generated driver configuration. And the start() method will be invoked as soon as any Scenario requests for a web-browser instance (for the first time) via the driver keyword.

  • stop(): Karate will call this method at the end of every top-level Scenario (that has not been call-ed by another Scenario).

  • setLogger(): You can choose to ignore this method, but if you use the provided Logger instance in your Target code, any logging you perform will nicely appear in-line with test-steps in the HTML report, which is great for troubleshooting or debugging tests.

Combined with Docker, headless Chrome and Karate's parallel-execution capabilities - this simple start() and stop() lifecycle can effectively run web UI automation tests in parallel on a single node.

DockerTarget

Karate has a built-in implementation for Docker (DockerTarget) that supports 2 existing Docker images out of the box:

To use either of the above, you do this in a Karate test:

* configure driverTarget = { docker: 'justinribeiro/chrome-headless', showDriverLog: true }

Or for more flexibility, you could do this in karate-config.js and perform conditional logic based on karate.env. One very convenient aspect of configure driverTarget is that if in-scope, it will over-ride any configure driver directives that exist. This means that you can have the below snippet activate only for your CI build, and you can leave your feature files set to point to what you would use in "dev-local" mode.

function fn() {
    var config = {
        baseUrl: 'https://qa.mycompany.com'
    };
    if (karate.env == 'ci') {
        karate.configure('driverTarget', { docker: 'ptrthomas/karate-chrome' });
    }
    return config;
}

To use the recommended --security-opt seccomp=chrome.json Docker option, add a secComp property to the driverTarget configuration. And if you need to view the container display via VNC, set the vncPort to map the port exposed by Docker.

karate.configure('driverTarget', { docker: 'ptrthomas/karate-chrome', secComp: 'src/test/java/chrome.json', vncPort: 5900 });

Custom Target

If you have a custom implementation of a Target, you can easily construct any custom Java class and pass it to configure driverTarget. Here below is the equivalent of the above, done the "hard way":

var DockerTarget = Java.type('com.intuit.karate.driver.DockerTarget');
var options = { showDriverLog: true };
var target = new DockerTarget(options);
target.command = function(port){ return 'docker run -d -p ' 
    + port + ':9222 --security-opt seccomp=./chrome.json justinribeiro/chrome-headless' };
karate.configure('driverTarget', target);

The built-in DockerTarget is a good example of how to:

  • perform any pre-test set-up actions
  • provision a free port and use it to shape the start() command dynamically
  • execute the command to start the target process
  • perform an HTTP health check to wait until we are ready to receive connections
  • and when stop() is called, indicate if a video recording is present (after retrieving it from the stopped container)

Controlling this flow from Java can take a lot of complexity out your build pipeline and keep things cross-platform. And you don't need to line-up an assortment of shell-scripts to do all these things. You can potentially include the steps of deploying (and un-deploying) the application-under-test using this approach - but probably the top-level JUnit test-suite would be the right place for those.

karate-chrome

The karate-chrome Docker is an image created from scratch, using just Ubuntu as a base and with the following features:

  • Chrome in "full" mode (non-headless)
  • Chrome DevTools protocol exposed on port 9222
  • VNC server exposed on port 5900 so that you can watch the browser in real-time
  • a video of the entire test is saved to /tmp/karate.mp4
  • after the test, when stop() is called, the DockerTarget will embed the video into the HTML report (expand the last step in the Scenario to view)

To try this or especially when you need to investigate why a test is not behaving properly when running within Docker, these are the steps:

  • start the container:
    • docker run -d -p 9222:9222 -p 5900:5900 --cap-add=SYS_ADMIN ptrthomas/karate-chrome
    • it is recommended to use --security-opt seccomp=chrome.json instead of --cap-add=SYS_ADMIN
  • point your VNC client to localhost:5900 (password: karate)
    • for example on a Mac you can use this command: open vnc://localhost:5900
  • run a test using the following driver configuration, and this is one of the few times you would ever need to set the start flag to false
    • * configure driver = { type: 'chrome', start: false, showDriverLog: true }
  • you can even use the Karate UI to step-through a test
  • after stopping the container, you can dump the logs and video recording using this command:
    • docker cp <container-id>:/tmp .
    • this would include the stderr and stdout logs from Chrome, which can be helpful for troubleshooting

Driver Types

type default
port
default
executable
description
chrome 9222 mac: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
win: C:/Program Files (x86)/Google/Chrome/Application/chrome.exe
"native" Chrome automation via the DevTools protocol
chromedriver 9515 chromedriver W3C Chrome Driver
geckodriver 4444 geckodriver W3C Gecko Driver (Firefox)
safaridriver 5555 safaridriver W3C Safari Driver
mswebdriver 17556 MicrosoftWebDriver W3C Microsoft Edge WebDriver
msedge 9222 MicrosoftEdge very experimental - using the DevTools protocol
winappdriver 4727 C:/Program Files (x86)/Windows Application Driver/WinAppDriver Windows Desktop automation, similar to Appium
android 4723 appium android automation via Appium
ios 4723 appium iOS automation via Appium

Locators

The standard locator syntax is supported. For example for web-automation, a / prefix means XPath and else it would be evaluated as a "CSS selector".

And input('input[name=someName]', 'test input')
When submit().click("//input[@name='commit']")
platform prefix means example
web (none) css selector input[name=someName]
web
android
ios
/ xpath //input[@name='commit']
web {} exact text content {a}Click Me
web {^} partial text content {^a}Click Me
win
android
ios
(none) name Submit
win
android
ios
@ accessibility id @CalculatorResults
win
android
ios
# id #MyButton
ios : -ios predicate string :name == 'OK' type == XCUIElementTypeButton
ios ^ -ios class chain ^**/XCUIElementTypeTable[name == 'dataTable']
android - -android uiautomator -input[name=someName]

Wildcard Locators

The "{}" and "{^}" locator-prefixes are designed to make finding an HTML element by text content super-easy. You will typically also match against a specific HTML tag (which is preferred, and faster at run-time). But even if you use "{*}" (or "{}" which is the equivalent short-cut) to match any tag, you are selecting based on what the user sees on the page.

When you use CSS and XPath, you need to understand the internal CSS class-names and XPath structure of the page. But when you use the visible text-content, for example the text within a <button> or hyperlink (<a>), performing a "selection" can be far easier. And this kind of locator is likely to be more stable and resistant to cosmetic changes to the underlying HTML.

You have the option to adjust the "scope" of the match, and here are examples:

Locator Description
click('{a}Click Me') the first <a> where the text-content is exactly: Click Me
click('{^span}Click') the first <span> where the text-content contains: Click
click('{div:1}Click Me') the second <div> where the text-content is exactly: Click Me
click('{span/a}Click Me') the first <a> where a <span> is the immediate parent, and where the text-content is exactly: Click Me
click('{^*:3}Me') the fourth HTML element (of any tag name) where the text-content contains: Me

Note that "{:3}" can be used as a short-cut instead of "{*:3}".

You can experiment by using XPath snippets like the "span/a" seen above for even more "narrowing down", but try to expand the "scope modifier" (the part within curly braces) only when you need to do "de-duping" in case the same user-facing text appears multiple times on a page.

Friendly Locators

The "wildcard" locators are great when the human-facing visible text is within the HTML element that you want to interact with. But this approach doesn't work when you have to deal with data-entry and <input> fields. This is where the "friendly locators" come in. You can ask for an element by its relative position to another element which is visible - such as a <span>, <div> or <label> and for which the locator is easy to obtain.

Method Finds Element
rightOf() to right of given locator
leftOf() to left of given locator
above() above given locator
below() below given locator
near() near given locator in any direction

The above methods return a chainable Finder instance. For example if you have HTML like this:

<input type="checkbox"><span>Check Three</span>

To click on the checkbox, you just need to do this:

* leftOf('{}Check Three').click()

By default, the HTML tag that will be searched for will be input. While rarely needed, you can over-ride this by calling the find(tagName) method like this:

* rightOf('{}Some Text').find('span').click()

rightOf()

* rightOf('{}Input On Right').input('input right')

leftOf()

* leftOf('{}Input On Left').clear().input('input left')

above()

* above('{}Input On Right').click()

below()

* below('{}Input On Right').input('input below')

near()

The typical reason why you would need near() is because an <input> field may either be on the right or below the label depending on whether the "container" element had enough width to fit both on the same horizontal line. Of course this can be used if the element you are seeking is diagonally offset from the locator you have.

 * near('{}Go to Page One').click()

Keywords

Only one keyword sets up UI automation in Karate, typically by specifying the URL to open in a browser. And then you would use the built-in driver JS object for all other operations, combined with Karate's match syntax for assertions where needed.

driver

Navigates to a new page / address. If this is the first instance in a test, this step also initializes the driver instance for future step operations as per what is configured.

Given driver 'https://github.com/login'

And yes, you can use variable expressions from karate-config.js. For example:

* driver webUrlBase + '/page-01'

As seen above, you don't have to force all your steps to use the Given, When, Then BDD convention, and you can just use "*" instead.

driver JSON

A variation where the argument is JSON instead of a URL / address-string, used only if you are testing a desktop (or mobile) application, and for Windows, you can provide the app, appArguments and other parameters expected by the WinAppDriver. For example:

Given driver { app: 'Microsoft.WindowsCalculator_8wekyb3d8bbwe!App' }

Syntax

The built-in driver JS object is where you script UI automation. It will be initialized only after the driver keyword has been used to navigate to a web-page (or application).

You can refer to the Java interface definition of the driver object to better understand what the various operations are. Note that Map<String, Object> translates to JSON, and JavaBean getters and setters translate to JS properties - e.g. driver.getTitle() becomes driver.title.

Methods

As a convenience, all the methods on the driver have been injected into the context as special (JavaScript) variables so you can omit the "driver." part and save a lot of typing. For example instead of:

And driver.input('#eg02InputId', Key.SHIFT)
Then match driver.text('#eg02DivId') == '16'

You can shorten all that to:

And input('#eg02InputId', Key.SHIFT)
Then match text('#eg02DivId') == '16'

When it comes to JavaBean getters and setters, you could call them directly, but the driver.propertyName form is much better to read, and you save the trouble of typing out round brackets. So instead of doing this:

And match getUrl() contains 'page-01'
When setUrl(webUrlBase + '/page-02')

You should prefer this form, which is more readable:

And match driver.url contains 'page-01'
When driver.url = webUrlBase + '/page-02'

Note that to navigate to a new address you can use driver - which is more concise.

Chaining

All the methods that return the following Java object types are "chain-able". This means that you can combine them to concisely express certain types of "intent" - without having to repeat the locator.

For example, to retry() until an HTML element is present and then click() it:

# retry returns a "Driver" instance
* retry().click('#someId')

Or to wait until a button is enabled using the default retry configuration:

# waitUntilEnabled() returns an "Element" instance
* waitUntilEnabled('#someBtn').click()

Or to temporarily over-ride the retry configuration and wait:

* retry(5, 10000).waitUntilEnabled('#someBtn').click()

Or to move the mouse() to a given [x, y] co-ordinate and perform a click:

* mouse(100, 200).click()

Or to use Friendly Locators:

* rightOf('{}Input On Right').input('input right')

Also see waits.

Syntax

driver.url

Get the current URL / address for matching. Example:

Then match driver.url == webUrlBase + '/page-02'

This can also be used as a "setter" to navigate to a new URL during a test. But always use the driver keyword when you start a test and you can choose to prefer that shorter form in general.

* driver.url = 'http://localhost:8080/test'

driver.title

Get the current page title for matching. Example:

Then match driver.title == 'Test Page'

driver.dimensions

Set the size of the browser window:

 And driver.dimensions = { x: 0, y: 0, width: 300, height: 800 }

This also works as a "getter" to get the current window dimensions.

* def dims = driver.dimensions

The result JSON will be in the form: { x: '#number', y: '#number', width: '#number', height: '#number' }

position()

Get the position and size of an element by locator as follows:

* def pos = position('#someid')

The result JSON will be in the form: { x: '#number', y: '#number', width: '#number', height: '#number' }

input()

2 string arguments: locator and value to enter.

* input('input[name=someName]', 'test input')

As a convenience, there is a second form where you can pass an array as the second argument:

* input('input[name=someName]', ['test', ' input', Key.ENTER])

Special Keys

Special keys such as ENTER, TAB etc. can be specified like this:

* input('#someInput', 'test input' + Key.ENTER)

A special variable called Key will be available and you can see all the possible key codes here.

Also see value(locator, value) and clear()

submit()

Karate has an elegant approach to handling any action such as click() that results in a new page load. You "signal" that a submit is expected by calling the submit() function (which returns a Driver object) and then "chaining" the action that is expected to trigger a page load.

When submit().click('*Page Three')

The advantage of this approach is that it works with any of the actions. So even if your next step is the ENTER key, you can do this:

When submit().input('#someform', Key.ENTER)

Karate will do the best it can to detect a page change and wait for the load to complete before proceeding to any step that follows.

You can even mix this into mouse() actions.

For some SPAs (Single Page Applications) the detection of a "page load" may be difficult because page-navigation (and the browser history) is taken over by JavaScript. In such cases, you can always fall-back to a waitForUrl() or a more generic waitFor().

waitForUrl() instead of submit()

Sometimes, because of an HTTP re-direct, it can be difficult for Karate to detect a page URL change, or it will be detected too soon, causing your test to fail. In such cases, you can use waitForUrl(). For convenience, it will do a string contains match (not an exact match) so you don't need to worry about http vs https for example. Just supply a portion of the URL you are expecting. As another convenience, it will return a string which is the actual URL in case you need to use it for further actions in the test script.

So instead of this, which uses submit():

Given driver 'https://google.com'
And input('input[name=q]', 'karate dsl')
When submit().click('input[name=btnI]')
Then match driver.url == 'https://github.com/intuit/karate'

You can do this. Note that waitForUrl() will also act as an assertion, so you don't have to do an extra match.

Given driver 'https://google.com'
And input('input[name=q]', 'karate dsl')
When click('input[name=btnI]')
And waitForUrl('https://github.com/intuit/karate')

And you can even chain a retry() before the waitForUrl() if you know that it is going to take a long time:

And retry(5, 10000).waitForUrl('https://github.com/intuit/karate')

waitFor() instead of submit()

This is very convenient to use for the first element you need to interact with on a freshly-loaded page. It can be used instead of waitForUrl() and you can still perform a page URL assertion as seen below.

Here is an example of waiting for a search box to appear after a click(), and note how we re-use the Element reference returned by waitFor() to proceed with the flow. We even slip in a page-URL assertion without missing a beat.

When click('{a}Find File')
And def search = waitFor('input[name=query]')
Then match driver.url == 'https://github.com/intuit/karate/find/master'
Given search.input('karate-logo.png')

Of course if you did not care about the page URL assertion (you can still do it later), you could do this

waitFor('input[name=query]').input('karate-logo.png')

delay()

Of course, resorting to a "sleep" in a UI test is considered a very bad-practice and you should always use retry() instead. But sometimes it is un-avoidable, for example to wait for animations to render - before taking a screenshot. The nice thing here is that it returns a Driver instance, so you can chain any other method and the "intent" will be clear. For example:

* delay(1000).screenshot()

The other situation where we have found a delay() un-avoidable is for some super-secure sign-in forms - where a few milliseconds delay before hitting the submit button is needed.

click()

Just triggers a click event on the DOM element:

* click('input[name=someName]')

Also see submit() and mouse().

select()

You can use this for plain-vanilla <select> boxes that have not been overly "ehnanced" by JavaScript. Nowadays, most "select" (or "multi-select") user experiences are JavaScript widgets, so you would be needing to fire a click() or two to get things done. But if you are really dealing with an HTML <select>, then read on.

There are four variations and use the locator prefix conventions for exact and contains matches against the <option> text-content.

# select by displayed text
Given select('select[name=data1]', '{}Option Two')

# select by partial displayed text
And select('select[name=data1]', '{^}Two')

# select by `value`
Given select('select[name=data1]', 'option2')

# select by index
Given select('select[name=data1]', 2)

If you have trouble with <select> boxes, try using script() to execute custom JavaScript within the page as a work-around.

focus()

* focus('.myClass')

clear()

* clear('#myInput')

If this does not work, try value(selector, value).

scroll()

Scrolls to the element.

* scroll('#myInput')

Since a scroll() + click() (or input()) is a common combination, you can chain these:

* scroll('#myBtn').click()
* scroll('#myTxt').input('hello')

mouse()

This returns an instance of Mouse on which you can chain actions. A common need is to move (or hover) the mouse, and for this you call the move() method.

The mouse().move() method has two forms. You can pass 2 integers as the x and y co-ordinates or you can pass the locator string of the element to move to. Make sure you call go() at the end - if the last method in the chain is not click() or up().

* mouse().move(100, 200).go()
* mouse().move('#eg02RightDivId').click()
# this is a "click and drag" action
* mouse().down().move('#eg02LeftDivId').up()

You can even chain a submit() to wait for a page load if needed:

* mouse().move('#menuItem').submit().click();

Since moving the mouse is a common task, these short-cuts can be used:

* mouse('#menuItem32').click()
* mouse(100, 200).go()
* waitUntilEnabled('#someBtn').mouse().click()

These are useful in situations where the "normal" click() does not work - especially when the element you are clicking is not a normal hyperlink (<a href="">) or <button>.

close()

Close the page / tab.

quit()

Close the browser.

html()

Get the outerHTML, so will include the markup of the selected element. Useful for match contains assertions. Example:

And match html('#eg01DivId') == '<div id="eg01DivId">this div is outside the iframe</div>'

text()

Get the textContent. Example:

And match text('.myClass') == 'Class Locator Test'

value()

Get the HTML form-element value. Example:

And match value('.myClass') == 'some value'

value(set)

Set the HTML form-element value. Example:

When value('#eg01InputId', 'something more')

attribute()

Get the HTML element attribute value by attribute name. Example:

And match attribute('#eg01SubmitId', 'type') == 'submit'

enabled()

If the element is enabled and not disabled:

And match enabled('#eg01DisabledId') == false

Also see waitUntil() for an example of how to wait until an element is "enabled" or until any other element property becomes the target value.

waitForUrl()

Very handy for waiting for an expected URL change and asserting if it happened. See waitForUrl() instead of submit().

Also see waits.

waitForText()

This is just a convenience short-cut for waitUntil(locator, "_.textContent.includes('" + expected + "')") since it is so frequently needed. Note the use of the JavaScript String.includes() function to do a text contains match for convenience. The need to "wait until some text appears" is so common, and with this - you don't need to worry about dealing with white-space such as line-feeds and invisible tab characters.

Of course, try not to use single-quotes within the string to be matched, or escape them using a back-slash (\) character.

* waitForText('#eg01WaitId', 'APPEARED')

And if you really need to scan the whole page for some text, you can use this, but it is better to be more specific for better performance:

* waitForText('body', 'APPEARED')

waitForEnabled()

This is just a convenience short-cut for waitUntil(locator, '!_.disabled') since it is so frequently needed:

And waitForEnabled('#someId').click()

Also see waits.

waitFor()

This is typically used for the first element you need to interact with on a freshly loaded page. Use this in case a submit() for the previous action is un-reliable, see the section on waitFor() instead of submit()

This will wait until the element (by locator) is present in the page and uses the configured retry() settings. This will fail the test if the element does not appear after the configured number of re-tries have been attempted.

Since waitFor() returns an Element instance on which you can call "chained" methods, this can be the pattern you use, which is very convenient and readable:

And waitFor('#eg01WaitId').click()

Also see waits.

waitForAny()

Rarely used - but accepts multiple arguments for those tricky situations where a particular element may or may not be present in the page. It returns the Element representation of whichever element was found first, so that you can perform conditional logic to handle accordingly.

But since the exists() API is designed to handle the case when a given locator does not exist, you can write some very concise tests, without needing to examine the returned object from waitForAny().

Here is a real-life example combined with the use of retry():

* retry(5, 10000).waitForAny('#nextButton', '#randomButton')
* exists('#nextButton').click()
* exists('#randomButton').click()

If you have more than two locators you need to wait for, use the single array argument form, like this:

* waitForAny(['#nextButton', '#randomButton', '#blueMoonButton'])

Also see waits.

exists()

This method returns an Element instance which means it can be chained as you expect. But there is a twist. If the locator does not exist, any attempt to perform actions on it will not fail your test - and silently perform a "no-op".

This is designed specifically for the kind of situation described in the example for waitForAny(). If you wanted to check if the Element returned exists, you can use the "getter" as follows:

* assert exists('#someId').exists

But what is most useful is how you can now click only if element exists. As you can imagine this can handle un-predictable dialogs, advertisements and the like.

* exists('#elusiveButton').click()
# or if you need to click something else
* if (exists('#elusivePopup').exists) click('#elusiveButton')

And yes, you can use an if statement in Karate !

Note that the exists() API is a little different from the other Element actions, because it will not honor any intent to retry() and immediately check the HTML for the given locator. This is important because it is designed to answer the question: "does the element exist in the HTML page right now ?"

waitUntil()

Wait for the JS expression to evaluate to true. Will poll using the retry() settings configured.

* waitUntil("document.readyState == 'complete'")

waitUntil(locator,js)

A very useful variant that takes a locator parameter is where you supply a JavaScript "predicate" function that will be evaluated on the element returned by the locator in the HTML DOM. Most of the time you will prefer the short-cut boolean-expression form that begins with an underscore (or "!"), and Karate will inject the JavaScript DOM element reference into a variable named "_".

Here is a real-life example:

One limitation is that you cannot use double-quotes within these expressions, so stick to the pattern seen below.

And waitUntil('.alert-message', "_.innerHTML.includes('Some Text')")

Karate vs the Browser

One thing you need to get used to is the "separation" between the code that is evaluated by Karate and the JavaScript that is sent to the browser (as a raw string) and evaluated. Pay attention to the fact that the includes() function you see in the above example - is pure JavaScript.

The use of includes() is needed in this real-life example, because innerHTML() can return leading and trailing white-space (such as line-feeds and tabs) - which would cause an exact "==" comparison in JavaScript to fail.

But guess what - this example is baked into a Karate API, see waitForText().

For an example of how JavaScript looks like on the "Karate side" see Function Composition.

This form of waitUntil() is very useful for waiting for some HTML element to stop being disabled. Note that Karate will fail the test if the waitUntil() returned false - even after the configured number of re-tries were attempted.

And waitUntil('#eg01WaitId', "function(e){ return e.innerHTML == 'APPEARED!' }")

# if the expression begins with "_" or "!", Karate will wrap the function for you !
And waitUntil('#eg01WaitId', "_.innerHTML == 'APPEARED!'")
And waitUntil('#eg01WaitId', '!_.disabled')

Also see waitForEnabled() which is the preferred short-cut for the last example above, also look at the examples for chaining and then the section on waits.

waitUntil(function)

A very powerful variation of waitUntil() takes a full-fledged JavaScript function as the argument. This can loop until any user-defined condition and can use any variable (or Karate or Driver JS API) in scope. The signal to stop the loop is to return any not-null object. And as a convenience, whatever object is returned, can be re-used in future steps.

This is best explained with an example. Note that scripts() will return an array, as opposed to script().

When search.input('karate-logo.png')

# note how we return null to keep looping
And def searchFunction =
  """
  function() {
    var results = scripts('.js-tree-browser-result-path', '_.innerText');
    return results.size() == 2 ? results : null;
  }
  """

# note how we returned an array from the above when the condition was met
And def searchResults = waitUntil(searchFunction)

# and now we can use the results like normal
Then match searchResults contains 'karate-core/src/main/resources/karate-logo.png'

Also see waits.

Function Composition

The above example can be re-factored in a very elegant way as follows, using Karate's native support for JavaScript:

# this can be a global re-usable function !
And def innerText = function(locator){ return scripts(locator, '_.innerText') }

# we compose a function using another function (the one above)
And def searchFunction =
  """
  function() {
    var results = innerText('.js-tree-browser-result-path');
    return results.size() == 2 ? results : null;
  }
  """

The great thing here is that the innnerText() function can be defined in a common feature which all your scripts can re-use. You can see how it can be re-used anywhere to scrape the contents out of any HTML tabular data, and all you need to do is supply the locator that matches the elements you are interested in.

retry()

For tests that need to wait for slow pages or deal with un-predictable element load-times or state / visibility changes, Karate allows you to temporarily tweak the internal retry settings. Here are the few things you need to know.

Retry Defaults

The default retry settings are:

  • count: 3, interval: 3000 milliseconds (try three times, and wait for 3 seconds before the next re-try attempt)
  • it is recommended that you stick to these defaults, which should suffice for most applications
  • if you really want, you can change this "globally" in karate-config.js like this:
    • configure('retry', { count: 10, interval: 5000 });
  • or any time within a script (*.feature file) like this:
    • * configure retry = { count: 10, interval: 5000 }

Retry Actions

By default, all actions such as click() will not be re-tried - and this is what you would stick to most of the time, for tests that run smoothly and quickly. But some troublesome parts of your flow will require re-tries, and this is where the retry() API comes in. There are 3 forms:

  • retry() - just signals that the next action will be re-tried if it fails, using the currently configured retry settings
  • retry(count) - the next action will temporarily use the count provided, as the limit for retry-attempts
  • retry(count, interval) - temporarily change the retry count and retry interval (in milliseconds) for the next action

And since you can chain the retry() API, you can have tests that clearly express the "intent to wait". This results in easily understandable one-liners, only at the point of need, and to anyone reading the test - it will be clear as to where extra "waits" have been applied.

Here are the various combinations for you to compare using click() as an example.

Script Description
click('#myId') Try to stick to this default form for 95% of your test. If the element is not found, the test will fail immediately. But your tests will run smoothly and super-fast.
waitFor('#myId').click() Use waitFor() for the first element on a newly loaded page or any element that takes time to load after the previous action. For the best performance, use this only if using submit() for the (previous) action (that triggered the page-load) is not reliable. It uses the currently configured retry settings. With the defaults, the test will fail after waiting for 3 x 3000 ms which is 9 seconds. Prefer this instead of any of the options below, or in other words - stick to the defaults as far as possible.
retry().click('#myId') This happens to be exactly equivalent to the above ! When you request a retry(), internally it is just a waitFor(). Prefer the above form as it is more readable. The advantage of this form is that it is easy to quickly add (and remove) when working on a test in development mode.
retry(5).click('#myId') Temporarily use 5 as the max retry attempts to use and apply a "wait". Since retry() expresses an intent to "wait", the waitFor() can be omitted for the chained action.
retry(5, 10000).click('#myId') Temporarily use 5 as the max retry attempts and 10 seconds as the time to wait before the next retry attempt. Again like the above, the waitFor() is implied. The test will fail if the element does not load within 50 seconds.

Wait API

The set of built-in functions that start with "wait" handle all the cases you would need to typically worry about. Keep in mind that:

  • all of these examples will retry() internally by default
  • you can prefix a retry() only if you need to over-ride the settings for this "wait" - as shown in the second row
Script Description
waitFor('#myId') waits for an element as described above
retry(10).waitFor('#myId') like the above, but temporarily over-rides the settings to wait for a longer time, and this can be done for all the below examples as well
waitForUrl('google.com') for convenience, this uses a string contains match - so for example you can omit the http or https prefix
waitForText('#myId', 'appeared') frequently needed short-cut for waiting until a string appears - and this uses a "string contains" match for convenience
waitForEnabled('#mySubmit') frequently needed short-cut for waitUntil(locator, '!_disabled')
waitForAny('#myId', '#maybe') handle if an element may or may not appear, and if it does, handle it - for e.g. to get rid of an ad popup or dialog
waitUntil(expression) wait until any user defined JavaScript statement to evaluate to true in the browser
waitUntil(function) use custom logic to handle any kind of situation where you need to wait, and use other API calls if needed

Also see the examples for chaining.

script()

Will actually attempt to evaluate the given string as JavaScript within the browser.

* assert 3 == script("1 + 2")

To avoid problems, stick to the pattern of using double-quotes to "wrap" the JavaScript snippet, and you can use single-quotes within.

* script("console.log('hello world')")

A more useful variation is to perform a JavaScript eval on a reference to the HTML DOM element retrieved by a locator. For example:

And match script('#eg01WaitId', "function(e){ return e.innerHTML }") == 'APPEARED!'
# which can be shortened to:
And match script('#eg01WaitId', '_.innerHTML') == 'APPEARED!'

Normally you would use text() to do the above, but you get the idea. Expressions follow the same short-cut rules as for waitUntil().

Also see the plural form scripts().

scripts()

Just like script(), but will perform the script eval() on all matching elements (not just the first) - and return the results as a JSON array / list. This is very useful for "bulk-scraping" data out of the HTML (such as <table> rows) - which you can then proceed to use in match assertions:

# get text for all elements that match css selector
When def list = scripts('div div', '_.textContent')
Then match list == '#[3]'
And match each list contains '@@data'

See Function Composition for another good example. Also see the singular form script().

findAll()

This will return all elements that match the locator as a list of Element instances. You can now use Karate's core API and call chained methods. Here are some examples:

# find all elements with the text-content "Click Me"
* def elements = findAll('{}Click Me')
* match karate.sizeOf(elements) == 7
* elements.get(6).click()
* match elements.get(3).script('_.tagName') == 'BUTTON'

Take a look at how to loop and transform data for more ideas.

refresh()

Normal page reload, does not clear cache.

reload()

Hard page reload, which will clear the cache.

back()

forward()

maximize()

minimize()

fullscreen()

driver.cookie

Set a cookie:

Given def cookie2 = { name: 'hello', value: 'world' }
When driver.cookie = cookie2
Then match driver.cookies contains '#(^cookie2)'

cookie()

Get a cookie by name:

* def cookie1 = { name: 'foo', value: 'bar' }
And match driver.cookies contains '#(^cookie1)'
And match cookie('foo') contains cookie1

driver.cookies

See above examples.

deleteCookie()

Delete a cookie by name:

When deleteCookie('foo')
Then match driver.cookies !contains '#(^cookie1)'

clearCookies()

Clear all cookies.

When clearCookies()
Then match driver.cookies == '#[0]'

dialog()

There are two forms. The first takes a single boolean argument - whether to "accept" or "cancel". The second form has an additional string argument which is the text to enter for cases where the dialog is expecting user input.

Also works as a "getter" to retrieve the text of the currently visible dialog:

* match driver.dialog == 'Please enter your name:'

switchPage()

When multiple browser tabs are present, allows you to switch to one based on page title (or URL).

When switchPage('Page Two')

switchFrame()

This "sets context" to a chosen frame (or <iframe>) within the page. There are 2 variants, one that takes an integer as the param, in which case the frame is selected based on the order of appearance in the page:

When switchFrame(0)

Or you use a locator that points to the <iframe> element that you need to "switch to".

When switchFrame('#frame01')

After you have switched, any future actions such as click() would operate within the "selected" <iframe>. To "reset" so that you are back to the "root" page, just switch to null (or integer value -1):

When switchFrame(null)

screenshot()

There are two forms, if a locator is provided - only that HTML element will be captured, else the entire browser viewport will be captured. This method returns a byte array.

This will also do automatically perform a karate.embed() - so that the image appears in the HTML report.

* screenshot()
# or
* screenshot('#someDiv')

If you want to disable the "auto-embedding" into the HTML report, pass an additional boolean argument as false, e.g:

* screenshot(false)
# or
* screenshot('#someDiv', false)

highlight()

To visually highlight an element in the browser, especially useful when working in the Karate UI

* highlight('#eg01DivId')

Debugging

You can use the Karate UI for stepping through and debugging a test. You can see a demo video here.

But many a time, you would like to pause a test in the middle of a flow and look at the browser developer tools to see what CSS selectors you need to use. For this you can use karate.stop() - but of course, NEVER forget to remove this before you move on to something else !

* karate.stop()

And then you would see something like this in the console:

*** waiting for socket, type the command below:
curl http://localhost:61963
in a new terminal (or open the URL in a web-browser) to proceed ...

In most IDE-s, you would even see the URL above as a clickable hyperlink, so just clicking it would end the stop(). This is really convenient in "dev-local" mode.

Locator Lookup

Other UI automation frameworks spend a lot of time encouraging you to follow a so-called "Page Object Model" for your tests. The Karate project team is of the opinion that things can be made simpler.

One indicator of a good automation framework is how much work a developer needs to do in order to perform any automation action - such as clicking a button, or retrieving the value of some HTML object / property. In Karate - these are typically one-liners. And especially when it comes to test-automation, we have found that attempts to apply patterns in the pursuit of code re-use, more often than not - results in hard-to-maintain code, and severely impacts readability.

That said, there is some benefit to re-use of just locators and Karate's support for JSON and reading files turns out to be a great way to achieve DRY-ness in tests. Here is one suggested pattern you can adopt.

First, you can maintain a JSON "map" of your application locators. It can look something like this. Observe how you can mix different locator types, because they are all just string-values that behave differently depending on whether the first character is a "/" (XPath), "{}" (wildcard), or not (CSS). Also note that this is pure JSON which means that you have excellent IDE support for syntax-coloring, formatting, indenting, and ensuring well-formed-ness. And you can have a "nested" heirarchy, which means you can neatly "name-space" your locator reference look-ups - as you will see later below.

{
  "testAccounts": {
    "numTransactions": "input[name=numTransactions]",
    "submit": "#submitButton"
  },
  "leftNav": {
    "home": "{span}Home",
    "invoices": "{span}Invoices",
    "transactions": "{span}Transactions"
  },
  "transactions": {
    "addFirst": ".transactions .qwl-secondary-button",
    "descriptionInput": ".description-cell input",
    "description": ".description-cell .header5",
    "amount": ".amount-cell input",
  }
}

Karate has great options for re-usability, so once the above JSON is saved as locators.json, you can do this in a common.feature:

* call read 'locators.json'

This looks deceptively simple, but what happens is very interesting. It will inject all top-level "keys" of the JSON file into the Karate "context" as global variables. In normal programming languages, global variables are a bad thing, but for test-automation (when you know what you are doing) - this can be really convenient.

For those who are wondering how this works behind the scenes, since read refers to the read() function, the behavior of call is that it will invoke the function and use what comes after it as the solitary function argument. And this call is using shared scope.

So now you have testAccounts, leftNav and transactions as variables, and you have a nice "name-spacing" of locators to refer to - within your different feature files:

* input(testAccounts.numTransactions, '0')
* click(testAccounts.submit)
* click(leftNav.transactions)

* retry().click(transactions.addFirst)
* retry().input(transactions.descriptionInput, 'test')

And this is how you can have all your locators defined in one place and re-used across multiple tests. You can experiment for yourself (probably depending on the size of your test-automation team) if this leads to any appreciable benefits, because the down-side is that you need to keep switching between 2 files - when writing and maintaining tests.

Chrome Java API

Karate also has a Java API to automate the Chrome browser directly, designed for common needs such as converting HTML to PDF - or taking a screenshot of a page. Here is an example:

import com.intuit.karate.FileUtils;
import com.intuit.karate.driver.chrome.Chrome;
import java.io.File;
import java.util.Collections;

public class Test {

    public static void main(String[] args) {
        Chrome chrome = Chrome.startHeadless();
        chrome.setLocation("https://github.com/login");
        byte[] bytes = chrome.pdf(Collections.EMPTY_MAP);
        FileUtils.writeToFile(new File("target/github.pdf"), bytes);
        bytes = chrome.screenshot();
        // this will attempt to capture the whole page, not just the visible part
        // bytes = chrome.screenshotFull();
        FileUtils.writeToFile(new File("target/github.png"), bytes);
        chrome.quit();
    }
    
}

Note that in addition to driver.screenshot() there is a driver.screenshotFull() API that will attempt to capture the whole "scrollable" page area, not just the part currently visible in the viewport.

The parameters that you can optionally customize via the Map argument to the pdf() method are documented here: Page.printToPDF .

If Chrome is not installed in the default location, you can pass a String argument like this:

Chrome.startHeadless(executable)
// or
Chrome.start(executable)

For more control or custom options, the start() method takes a Map<String, Object> argument where the following keys (all optional) are supported:

  • executable - (String) path to the Chrome executable or batch file that starts Chrome
  • headless - (Boolean) if headless
  • maxPayloadSize - (Integer) defaults to 4194304 (bytes, around 4 MB), but you can override it if you deal with very large output / binary payloads

screenshotFull()

Only supported for driver type chrome. See Chrome Java API. This will snapshot the entire page, not just what is visible in the viewport.

pdf()

Only supported for driver type chrome. See Chrome Java API.