With Sakuli v2+ we changed a lot under the hood of Sakuli and completely replaced the technology-stack. This resulted in a more flexible and future-ready architecture while remaining a maximum of backward compatibility.
Major Changes to Sakuli1 (Migration)
Nearly all functions and classes from Sakuli 1 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 paradigm changes in 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 NodeJs (have a look at this repl) and Sakulis core technologies (selenium-webdriver and Nut.Js) make heavy use of asynchronous operations. There are projects like fibers, but they rely on native (OS-dependent) libraries and 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 async function - because the engine will wrap the whole body of this function with a Promise. That means you need your code to be executed within an async function in order to make use of the
await keyword. In 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 the
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:
This is also a totally valid use of Sakuli. However, this approach has two downsides:
- The Sakuli documentation will mostly use the await / async syntax in the examples
For these reasons, we would advise you not to use the
.then(...) syntax, unless you are completely sure that you know what you are doing.
The most functions which implement the Sahi DSL - recognizable by prefixed underscore - return a Promise. That means you should put an
await in front of it. An exception from this pattern, are the Accesor 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 if you need an
await, you can put it any way since it will execute the function as expected. Since this is not a good practice, we are working on tooling which will help you identify async functions like Typescript support.
Sakuli has some classes - especially these 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 async operations. Methods of a Fluent API always return the object itself or at least an instance of the same class. An async (and awaitable) method rather needs to return a Promise. To still accomplish the goal of backward compatibility all classes which implement a fluent interface but have async 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, besides 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 which basically forces to implement a
then function 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 returned 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 testcases or different testcases on the same system, you might have come up with a modularisation of common functions. For example, the login to a system is always the same in all testcases, so you put it into a separate file.
In Sakuli v1 there were functions to load these files into your actual testcase:
_includeDynamic. The code usually looked like this:
Two interesting aspects can be observed here:
- Functions defined in the include script, were put to the global namespace
Sakuli now introduces support for ES-module syntax. So, the example above has to be rewritten to:
Besides 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 the more accurate term) if they should be used in other scripts. In the actual testcase script the
_include function is removed and replaced by an
import. The import ensures, that we only import symbols that are required in the current script and its global namespace. Each function which is required in the testcase script, has to be imported explicitly. An alternative syntax is to import everything within an own namespace: