Web tests

For DOM based testing most of the functions from Sahi tests can be used (please note that Sakuli only implements the open source APIs).

The main difference between Sakuli v1 and Sakuli v2 is the usage of Promises in the action API, meaning that you have to await a click for example.

On the other hand, element selectors remain synchronized functions but will not do the actual DOM fetching anymore. While an expression like var $e=_link('Sakuli') did an actual DOM-access in Sakuli v1.x, it returns a kind of abstract query for an element now. So, action can fetch this element whenever it is required.

A detailed list of all available functions can be found in the Sahi API interface,

Accessor API

The Accessor API is described in the Accessor API interface.

Sakuli uses the concept of reusable Queries rather than directly working on an element-object (like in Selenium). Sakuli offers an expressive set of Accessors like _div, _textbox or _table. These accessors will not return an actual element or any reference to it. Rather it will create a SahiElementQuery. This query can then be used in various Actions like _click, _highlight or _isVisible. This concept could be compared with Locators in Selenium.

This architecture gives us two nice benefits:

  • Compatibility with Sahi API
  • Since Sakuli handles the actual fetching and validation of an element by performing retries, refreshes, implicit wait etc. which reduce annoying issues with Selenium a lot (e.g. StaleElementReferenceError)

Most accessors are defined in the same way: They are functions that take an AccessorIdentifier as a first parameter and a variadic list of Relations:

_NAME(identifier, ...relations): SahiElementQuery

The accessor adds a static Locator to the returned query. Since

a query object consists of a locator, an identifier and a list of relations, we will eventually get an entire query object. The locator basically is a CSS element selector which you would expect from the accessor name - so _div for example adds By.css('div'), _textbox adds By.css('input[type="text"], input:not([type])') and so on.

Accessors by HTML-Tag or attributes

HTML-Tag Accessor Function
<a> _link
<area> _area
<article> _article
<aside> _aside
<blockquote> _blockquote
<b> _bold
<button> _button
<button type="reset"> _reset
<button type="submit"> _submit
<canvas> _canvas
<code> _code
<dd> _dDesc
<details> _details
<div> _div
<dl> _dList
<dt> _dTerm
<em> _emphasis
<embed> _embed
<fieldset> _fieldset
<figcaption> _figcaption
<figure> _figure
<font> _font
<footer> _footer
<frame> _frame
<header> _header
<hr> _hr
<h1> _heading1
<h2> _heading2
<h3> _heading3
<h4> _heading4
<h5> _heading5
<h6> _heading6
<i> _italic
<iframe> _iframe
<iframe> _rte
<img> _image
<input type="checkbox"/> _checkbox
<input type="date"/> _datebox
<input type="datetime"/> _datetimebox
<input type="datetime-local"/> _datetimelocalbox
<input type="email"/> _emailbox
<input type="file"/> _file
<input type="hidden"/> _hidden
<input type="image"/> _imageSubmitButton
<input type="month"/> _monthbox
<input type="number"/> _numberbox
<input type="password"/> _password
<input type="radio"/> _radio
<input typerange"/> _rangebox
<input type="search"/> _searchbox
<input type="tel"/> _telephonebox
<input type="text"/> _textbox
<input type="time"/> _timebox
<input type="url"/> _urlbox
<input type="week"/> _weekbox
<label> _label
<main> _main
<map> _map
<mark> _mark
<nav> _nav
<object> _object
<p> _paragraph
<pre> _performatted
<section> _section
<select> _option
<select> _select
<span> _span
<strong> _strong
<summary> _summary
<ellipse/> _svg_ellipse
<circle/> _svg_circle
<line/> _svg_line
<path/> _svg_path
<polygon/> _svg_polygon
<polyline/> _svg_polyline
<rect/> _svg_rect
<text> _svg_text
<tspan> _svg_tspan
<table> _table
<td> _cell
<textarea type="text"/> _textarea
<time> _time
<th> _tableHeader
<tr> _row
<ul> _list
<li> _listItem
<video> _video
HTML-Attribute Accessor Function
<[HTML - tag] class='[class name]'></[HTML - tag]> _byClassName
<[HTML - tag] id='[id name]'></[HTML - tag]> _byID


Since Sakuli encapsulates the creation (through accessors) and the application (e.g. through actions) of a SahiElementQuery, a user will rarely get in touch with these objects directly. Nevertheless, it is good to understand how Sakuli works with queries. Let us consider this example:

await _click(_button('Sign In'));

The following will happen under the hood:

  1. _button creates a query with a locator to a button element and with 'Sign In' as an identifier and an empty list of relations

  2. This query is passed to the _click action. This action uses the AccessorUtil to fetch an element. It will:

    1. Fetch a list of all elements from the locator
    2. Reduce the list based on the relations (skipped when this list is empty)
    3. Reduce the list with the identifier logic
    4. Return the first entry of the remaining elements list


The identifer is another relict from Sahi that can be one of the following types:

Type Effect
number The identifier is considered as index. Sakuli picks the element at this index (zero-based) in step 2.3
RegExp Tests this RegExp against the following attributes of each element in the list at step 2.1: [aria-describedby], [name], [id], className, innerText, value, src
string The string is normalized and wrapped into a RegExp, therefore the same logic as for RegExp is applied
AccessorIdentifierAttributes This could be an object with the properties sahiIndex and/or sahiIndex, sahiText, className. The first two are handled like a number or a string identifer, respectively. The latter one works like a string identifier which only checks for the className property

Since we mostly apply the logic of Sahi comparisons against the class attribute are pretty dumb. While the attribute value is semantically a space separated list of class names. It is just handled as a usual string in Sahi (and therefore also in Sakuli so far).

Action API

The Action API is described in the Action Api interface.

Actions usually invoke a Selenium action sequence with an activated bridge mode to cover compatibility to most webdriver implementations. An action accepts a SahiElementQuery or a WebElement and tries to perform the action on this element several times. This approach reduces the count of StaleElementReferenceErrors dramatically, especially when a query is used.


Beside the fact that actions work asynchronously now, they behave like in Sahi. One exception is the _eval method, which accepts a string now containing some JavaScript code, which is performed on the website by the webdriver implementation (see executeAsyncScript method of Seleniums Thenablewebdriver).

const windowOuterHeight = await _eval(`return window.outerHeight`)

Fetch API

The Fetch API is described in the Fetch API interface.

These methods are useful to get deeper access to elements and element-attributes:

const [x,y] = await _position(_image('funny-cat-image.png'));

or let you perform checks (e.g. if an element exists).

if(await _exists(_div('cookie-banner'))) {
    await _click(_button('I agree'))

Selenium Fallbacks

Since Sakuli uses Seleniums webdriver it also provides various ways to access the functionality of this backend.

It is recommended to use Sakulis built-in functionalities rather than work with the driver instances or any WebElement directly. At the moment, Sakuli is built upon Selenium. Nevertheless, a switch to other technologies in the future is possible. Downwards compatibility is only possible for Sakulis built-in functionalities. Direct use of webdriver instance methods is not supported.

WebDriver instance

Sakuli test scripts provide a globally accessible object of the current WebDriver instance which can be used to invoke its native methods directly. This might be useful for switching between frames:

await driver.switchTo().frame(1);
await _click(_div('element-in-frame-1'));
await driver.switchTo().defaultContent();

WebElement instances

The Fetch API provides the _fetch function which returns the native WebElement instance from Seleniums webdriver for a query:

const webElement = await _fetch(_image('funny-cat-image.png')); 
const {width, height} = await webElement.getRect();