With Sakuli v2+ we changed a lot under the hood and completely replaced the technology-stack. This resulted in a more flexible and future-ready architecture while preserving a maximum of backward compatibility.
Major Changes to Sakuli v1 (Migration)
Nearly all functions and classes from Sakuli v1 are available in Sakuli v2+. They are provided by the @sakuli/legacy package.
Async / Await
You might have wondered where the
Why we need it
- Due to the runtime and core libraries, asynchronous operations are required (and cannot be turned into synchronous operations)
If you ever worked with Sakuli and followed our “Getting started“ guide, you might have noticed one of the most obvious paradigmatic changes regarding the test syntax: The wrapping
(async () => /*...*/)().then(done) and the extensive use of
“Unfortunately”, it is not (easily) possible to turn non-blocking operations into blocking operations in Node (have a look at this repl) and Sakuli’s core technologies (Selenium-WebDriver and nut.js) make heavy use of asynchronous operations. There are tools like fibers that can help you, but they rely on native (OS-dependent) libraries. Therefore, we have decided to avoid another OS specific library in Sakuli.
The async wrapper
You might have noticed that if a testcase in Sakuli v2 is wrapped within
await keyword is only available within an asynchronous function - because the engine will wrap the whole body of this function within a Promise. That means, that in order to be able to make use of the
await keyword, your code has to be executed within an asynchronous function. On the top level IIFE, we declare the actual function as
async and can make use of
await. Since the result of this function is automatically turned into a Promise, we can invoke
then and pass the
done function to it.
If you want to avoid this construct completely, you can use the
then function of a Promise. The example code of the “Getting started“ guide would look like this then:
This is also a totally valid format that can be used within Sakuli. However, this approach has two downsides:
- The examples mentioned in the Sakuli documentation will mostly use the await/async syntax
Due to those reasons, we would advise you not to use the
.then(...) syntax, unless you are completely sure about what you are doing.
Most functions which implement the Sahi DSL - recognizable by a prefixed underscore - return a Promise. That means, that you should put an
await in front of it. Exceptions from this pattern are Accessor functions, which create query objects to access elements in the DOM. You can check if the function returns a Promise in the API docs of SahiAPI. If you are not sure that you need an
await, you can put it anyway since the function will be executed as expected. Considering that this is not a good practice, we are working on tools which will help you identify async functions like Typescript support.
Sakuli has some classes - especially those for native interactions - which implement the Fluent Interface pattern, where you can chain method invocations on the same object. Unfortunately, this does not really play nice with asynchronous operations. Methods of a fluent API always return the object itself or at least an instance of the same class. But an asynchronous (and awaitable) method rather needs to return a Promise. To still accomplish the goal of backward compatibility, all classes implementing a fluent interface and including asynchronous methods are wrapped in a
Thenable<ClassName> form of itself. This concept is highly inspired by Selenium-WebDrivers ThenableWebDriver. From an end-user perspective, you can use the fluent interface almost like before, except of a little
await at the beginning:
Thenable Classes are:
The technical trick behind this is, that there are two implementations: The actual class with the async functions and a
thenable class, which implements the PromiseLike interface. The PromiseLike interface basically forces a
then function to be implemented and is therefore “awaitable”. The wrapper class also holds a Promise with the instance to the actual class. When a method of the thenable class is invoked, it delegates the call to the same method of the actual class and returns itself with the Promise given back by the actual method. Since the thenable class itself is a Promise, it will resolve to an instance of the actual class.
_include and _includeDynamic
If you ever wrote larger or different testcases on the same system, you might have come up with a modularization of commonly used functions. For example, the login to a system is always the same in all testcases, so you put it into a separate file.
Sakuli v1 includes functions to load these files into your actual testcase:
_includeDynamic. The code usually looks like this:
Two interesting aspects can be observed here:
- Functions defined in the included script were added to the global namespace
Sakuli now introduces support for ES-module syntax. To adapt the example above, it has to be rewritten as follows:
Beside the added async/await keywords, we can see that the first line of each script changed. In the first script, all functions (or classes, enums, constants, etc) have to be explicitly exported by the script (“module” would be a more accurate term) if they should be used in other scripts. For the actual testcase script the
_include function will be removed and replaced by an
import ensures that we only import symbols that are required in the current script and its global namespace. Each function required in the testcase script has to be imported explicitly. An alternative syntax is to import everything within its own namespace: