12 min read time

The Promise of the LeanFT JavaScript SDK—making the most of continuous testing

by   in DevOps Cloud (ADM)

Thanks to Anton Kaminsky from the  LeanFT R&D Team for providing this article!

Introduction

A JavaScript Promise represents a single asynchronous operation that hasn't completed yet, but is expected in the future.

You can see a detailed explanation of the JavaScript Promise here.

The LeanFT JavaScript SDK is heavily based on promises. Practically every command such as click() or text() returns a promise, which is fulfilled with a result of the command (or rejected in case of an error).

By default, the LeanFT JavaScript SDK synchronizes promises to allow a simplified and more readable test syntax.

The purpose of this blog is to demonstrate and explain the promise mechanism. Keep reading to see detailed examples of what occurs in a variety of scenarios and how to handle errors. 

This blog is compatible with LeanFT 12.54 and above.

The LeanFT Promise

The Promise returned by LeanFT commands is a custom Promise and not the JavaScript build-in Promise.

The API supported by the LeanFT Promise:

  • Promise.then(onFulfilled, onRejected)
    Appends fulfillment and rejection handlers to the promise, and returns a new promise resolving to the return value of the called handler, or to its original settled value if the promise was not handled, both onFulfilled and onRejected parameters are optional.
  • Promise.catch(onRejected)
    Appends a rejection handler callback to the promise, and returns a new promise resolving to the return value of the callback if it is called, or to its original fulfillment value if the promise is instead fulfilled (catch is equivalent to calling .then(null, onRejected)).

Execution Synchronization

By default every LeanFT command is synchronized using an internal entity called the PromiseManagerFor example, in order to synchronize two click operations without the execution synchronization the code is:

someTestObject.click().then(function () {
    return anotherTestObject.click();
});

 The following code is equivalent to the above but with the LeanFT execution synchronization:

someTestObject.click();
anotherTestObject.click();

A click on someTestObject is performed and, when the operation is completed, a click on anotherTestObject is performed.

The execution synchronization can be turned off, see the “disable execution synchronization” section.

Using value returned from a command

Commands like click are synchronized automatically, so there is no need to use the ‘then’ to synchronize them.

In other commands, where you would want to do something with the value returned from the command, there are two options:

Option 1 – Using then

someEditField.setValue("someText");
someEditField.value().then(function (text) {
    expect(text).toEqual("someText");
});

 

Option 2 – Using LeanFT’s expect function:

To use the LeanFT’s expect function you will need to require it first as follows: 

var expect = require("leanft/expect");

 Then you can write the following: 

someEditField.setValue("someText");
expect(someEditField.value()).toEqual("someText");

The LeanFT provided expect can receive functions, which return a promise. It will perform the expectation once the promise will be resolved, so using this syntax removed the need for the ‘then’.

If you would like to do something other than verification with the result, or perform several verifications on it, then only option 1 is relevant.

Promise chaining and promise tree

The LeanFT PromiseManager chains all commands into a promise chain. Furthermore a command such as click is executed after the entire promise tree of the previous command is completed.

In the following example the click on the aThirdTestObject is performed only once the click on someTestObject and the click on anotherTestObject are completed.

someTestObject.click().then(function () {
    anotherTestObject.click();
});

aThirdTestObject.click();

This can be extended into a tree of promises as follows:

var promise = someTestObject.click();
promise.then(function () {
    anotherTestObject.click();
});
promise.then(function () {
    yetAnotherTestObject.click();
});

aThirdTestObject.click();

Here, the promise returned form clicking someTestObject is chained with two more promises, creating a promise tree whose root is the someTestObject.click promise.

The click on the aThirdTestObject is performed only once all three promises are completed.

Note: in this example the clicks on anotherTestObject and on yetAnotherTestObject will be performed independent of each other.

Error handling

As explained in the previous section each synchronized command, such as click, waits for the entire promise tree rooted by the previous command to end before beginning execution.

If an error occurs in any of these promises and is not handled by a catch or a then with a reject function, all the synchronized commands that follow will not be executed until the error is caught.

Examples:

  • The following example attempts to click on nonExistingTestObject:
    nonExistingTestObject.click().catch(function () {});
    someTestObject.click();

 

The click fails (with a ReplayObjectNotFound error). Because we have a catch statement on this click operation, the error is caught and the click on the someTestObject is performed.

  • In the following example notice that there is no catch on the attempt to click on nonExistingTestObject:
    nonExistingTestObject.click();
    someTestObject.click().catch(function () {});
    anotherTestObject.click();

    In this case, the error thrown by clicking the nonExistingTestObject is not caught immediately, so the click on someTestObject is not performed.
    However the click on someTestObject does have a catch, the error thrown from the click on nonExistingTestObject is caught at this point.
    The next click operation, on anotherTestObject is then performed.
  • In the following example, an error is thrown from a then function attached to the click on someTestObject:
    someTestObject.click().then(function () {throw new Error("some error in sub tree");});
    anotherTestObject.click().catch(function () {});

    As explained above, the click on anotherTestObject waits for the entire promise tree of the click on someTestObject to end.
    Because one of promises attached to the click on someTestObject throws an error, that is not caught, the click on anotherTestObject is not performed.
    The error is caught by the catch attached to the click on anotherTestObject.
  • The following example is similar to the pervious one, except that here the error thrown in the then statement is caught immediately, and the click on anotherTestObject is performed.
    someTestObject.click().then(function () {throw new Error("some error in sub tree");}).catch(function () {});
    anotherTestObject.click();
     

whenDone(done)

In the supported Jasmine and Mocha BDD style frameworks each test case (it statement) can receive a done callback parameter:

it("some test case", function (done) {
    //place your test here
});

The done callback is used in asynchronous tests to signal the framework when the test is done. It may be difficult to locate the correct point in the test where the done callback should be called.

To simplify this, the LeanFT JavaScript SDK provides a helper function called whenDone, which calls the done callback with correct status once all synchronized commands and their sub trees are completed.

The call to whenDone should be the last statement in the test case: 

it("some test case", function (done) {
    //place your test here

    LFT.whenDone(done);
});

 

It is important to point out that this is not a must, it is a helper function.

For advanced users: You can determine the point to end or fail the test by calling the done callback directly at that point.

Parallel execution with execution synchronization

When using the execution synchronization all commands such as click are synchronized. However all then functions attached to such a command are performed independently from each other.

Example:

In the following example, first the click on someTestObject is performed. Both of the then functions attached to that promise are executed, independent of each other.

Since JavaScript is a single threaded language, it first performs the code in the first then function until it reaches an asynchronous statement. It then probably moves to execute the second then function, and vice versa.

Since each synchronized command waits for the entire sub tree of the previous command to end, once both then functions end, the click on anotherTestObject is performed.

var promise = someTestObject.click();
promise.then(function () {
    testObject1.click();
    testObject2.click();
});

promise.then(function () {
    testObject3.click();
    testObject4.click();
});

anotherTestObject.click();

The expected order of performance in this example is:

  1. someTestObject.click()
  2. testObject1.click()
  3. testObject3.click()
  4. testObject2.click()
  5. testObject4.click()
  6. anotherTestObject.click()

It is important to understand that synchronized command inside a then function are synchronized among themselves in the same way as synchronized commands outside the then function. So in the following case, the click on testObject2 is not performed until the click on testObject1 is completed: 

someTestObject.click().then(function () {
    testObject1.click();
    testObject2.click();
});

 Disable execution synchronization

The execution synchronization can be disabled by passing the following configuration to the LFT.init function:

LFT.init({executionSynchronization: false})

This disables the commands synchronization. Instead they will behave as regular promises.

So the following code will perform click on anotherTestObject after the click on someTestObject was sent, but before it was completed (executed by the LeanFT): 

someTestObject.click();
anotherTestObject.click();

 To achieve the same synchronization as with execution synchronization enabled, a then must be used: 

someTestObject.click().then(function () {
    anotherTestObject.click();
});

In the complete examples section, there is an example of a test case with execution synchronization disabled.

Complete code sample

The full code sample demonstrates some of the principals and concepts described in this blog. The sample also demonstrates some common technics, such as, how to perform accumulation, which can be useful in many cases.

var LFT = require("leanft");
var Web = LFT.Web;
var expect = require("leanft/expect");
jasmine.DEFAULT_TIMEOUT_INTERVAL = 120000; //increase the timeout of the Jasmine framework - will not work in Mocha

describe("Demo promises with execution synchronization test cases", function () {
    var browser;

    beforeAll(function(done) {
        LFT.init();

        Web.Browser.launch(Web.BrowserType.Chrome)
            .then(function (returnedBrowser) {
                browser = returnedBrowser;
            });

        LFT.whenDone(done);
    });

    afterAll(function(done) {
        if(browser) {
            browser.close();
        }

        LFT.cleanup();
        LFT.whenDone(done);
    });

    beforeEach(function(done) {
        LFT.beforeTest();

        //navigate to the HP demo site
        browser.navigate("http://www.advantageonlineshopping.com/#");

        LFT.whenDone(done);
    });

    afterEach(function(done) {
        LFT.afterTest();
        LFT.whenDone(done);
    });

    it("simple click and expect test", function (done) {
        //click on the Tablets image
        browser.$(Web.Element({
                className:"categoryCell",
                tagName:"DIV",
                innerText:"TABLETS Shop Now "
            }
        )).click();

        //describe the HP Elite x2 tablet
        var EliteTablet = browser.$(Web.Element({
                tagName:"LI",
                innerText:"SOLD OUT SHOP NOW HP Elite x2 1011 G1 Tablet $1,279.00 "
            }
        ));

        //verify the element exists
        expect(EliteTablet.exists()).toBeTruthy();

        //click on the tablet
        EliteTablet.click();

        //describe the price element
        var priceElement = browser.$(Web.Element({
                className:"roboto-thin screen768 ng-binding",
                tagName:"H2"
            }
        ));

        //verify the price is correct
        expect(priceElement.innerText()).toContain("$1,279.00");

        LFT.whenDone(done);
    });

    it("accumulate values test", function (done) {
        //click on the Tablets image
        browser.$(Web.Element({
                className:"categoryCell",
                tagName:"DIV",
                innerText:"TABLETS Shop Now "
            }
        )).click();

        //describe the HP Elite x2 tablet
        var EliteTablet = browser.$(Web.Element({
                tagName:"LI",
                innerText:"SOLD OUT SHOP NOW HP Elite x2 1011 G1 Tablet $1,279.00 "
            }
        ));

        //click on the tablet
        EliteTablet.click();

        //describe the add to cart button
        var addToCartButton = browser.$(Web.Button({
                buttonType:"submit",
                tagName:"BUTTON",
                name:" ADD TO CART                        "
            }
        ));

        //add the tablet to the cart
        addToCartButton.click();

        //add the tablet to the cart again
        addToCartButton.click();

        //click on the tablets link, to go back to the tablets page
        browser.$(Web.Link({
                tagName:"A",
                innerText:"TABLETS "
            }
        )).click();

        //click on the HP ElitePad G2
        browser.$(Web.Element({
                tagName:"LI",
                innerText:"SOLD OUT SHOP NOW HP ElitePad 1000 G2 Tablet $1,009.00 "
            }
        )).click();

        //add this tablet to the cart also
        addToCartButton.click();

        //click the shopping cart icon to go to the shopping cart
        browser.$(Web.Element({
                accessibilityName:"",
                tagName:"svg",
                innerText:"",
                index:6
            }
        )).click();

        //describe the shopping cart table
        var shoppingCartTable = browser.$(Web.Table({
                role:"",
                accessibilityName:"",
                tagName:"TABLE",
                index:1
            }
        ));

        //accumulate the quantity of the items - should be 3
        var quantityOfItems = 0;

        shoppingCartTable.cells().then(function (shoppingCart) {
           for(var i=1; i<=2; i  ) {
               shoppingCart[i][3].text().then(function (quantity) {
                   //quantity is of format: QUANTITY: 1
                   quantityOfItems  = parseInt(quantity.substring(10));
               });
           }
        }).then(function () {
            expect(quantityOfItems).toEqual(3);
        });

        //another way to accumulate
        shoppingCartTable.cells().then(function (shoppingCart) {
            quantityOfItems = 0;
            var lastPromise;
            for(var i=1; i<=2; i  ) {
                lastPromise = shoppingCart[i][3].text().then(function (quantity) {
                    quantityOfItems  = parseInt(quantity.substring(10));
                });
            }

            lastPromise.then(function () {
                expect(quantityOfItems).toEqual(3);
            });
        });

        LFT.whenDone(done);
    });

    it("catch error example", function (done) {
        //click on the Tablets image
        browser.$(Web.Element({
                className:"categoryCell",
                tagName:"DIV",
                innerText:"TABLETS Shop Now "
            }
        )).click();

        //describe wrongly the HP Elite x2 tablet
        var EliteTabletWithWrongDescription = browser.$(Web.Element({
                tagName:"WrongTagName",
                innerText:"SOLD OUT SHOP NOW HP Elite x2 1011 G1 Tablet $1,279.00 "
            }
        ));

        //now try to click the HP Elite x2 tablet and an error will occur
        EliteTabletWithWrongDescription.click().catch(function (error) {
            expect(error.message).toContain("ReplayObjectNotFound");

            //click the 7.9' HP tablet link
            browser.$(Web.Element({
                    tagName:"LI",
                    innerText:"SOLD OUT SHOP NOW HP Pro Tablet 608 G1 $479.00 "
                }
            )).click();
        });

        //expect to be at the page of the 7.9' tablet (verify by checking existence of element with tablet name)
        //note: the catch will be called before the following exists, since our PromiseManager awaits completion of
        //the entire sub tree of a promise before it continues to the next promise.
        //So no 'then' is required between the catch the exists.
        expect(browser.$(Web.Element({
                className:"roboto-regular screen768 ng-binding",
                tagName:"H1",
                innerText:"HP PRO TABLET 608 G1 "
            }
        )).exists()).toBeTruthy();

        LFT.whenDone(done);
    });

    it("if an error is not caught, it will be passed on to the next promises in the chain until someone catches it", function (done) {
        //click on the Tablets image
        browser.$(Web.Element({
                className:"categoryCell",
                tagName:"DIV",
                innerText:"TABLETS Shop Now "
            }
        )).click();

        //describe wrongly the HP Elite x2 tablet
        var EliteTabletWithWrongDescription = browser.$(Web.Element({
                tagName:"WrongTagName",
                innerText:"SOLD OUT SHOP NOW HP Elite x2 1011 G1 Tablet $1,279.00 "
            }
        ));

        //now try to click the HP Elite x2 tablet and an error will occur
        EliteTabletWithWrongDescription.click();

        //click the 7.9' HP tablet link.
        //this click will not be performed, since there is no catch on the previous failed promise.
        browser.$(Web.Element({
                tagName:"LI",
                innerText:"SOLD OUT SHOP NOW HP Pro Tablet 608 G1 $479.00 "
            }
        )).click().catch(function (error) {
            //this catch will catch the promise chain error, previously not caught.
            expect(error.message).toContain("ReplayObjectNotFound");
        });

        //expect not to be at the page of the 7.9' tablet (verify by checking existence of element with tablet name)
        //since the click on the 7.9' tablet was not performed.
        expect(browser.$(Web.Element({
                className:"roboto-regular screen768 ng-binding",
                tagName:"H1",
                innerText:"HP PRO TABLET 608 G1 "
            }
        )).exists()).toBeFalsy();

        LFT.whenDone(done);
    });
});

describe("promises with execution synchronization disabled test cases", function () {
    var browser;

    beforeAll(function (done) {
        LFT.init({executionSynchronization: false})
            .then(function () {
                return Web.Browser.launch(Web.BrowserType.Chrome)
                    .then(function (b) {
                        browser = b;
                    });
            }).then(done);
    });

    afterAll(function (done) {
        browser.close().then(function () {
            return LFT.cleanup();
        }).then(done);
    });

    beforeEach(function(done) {
        LFT.beforeTest();

        browser.navigate("http://54.175.66.142:8080/#").then(done);
    });

    afterEach(function() {
        LFT.afterTest();
    });

    it("simple promise test without execution synchronization", function (done) {
        //click on the Tablets image
        browser.$(Web.Element({
                className:"categoryCell",
                tagName:"DIV",
                innerText:"TABLETS Shop Now "
            }
        )).click().then(function () {
            //describe the HP Elite x2 tablet
            var EliteTablet = browser.$(Web.Element({
                    tagName:"LI",
                    innerText:"SOLD OUT SHOP NOW HP Elite x2 1011 G1 Tablet $1,279.00 "
                }
            ));

            //verify the element exists
            return EliteTablet.exists().then(function (exists) {
                expect(exists).toBeTruthy();

                //click on the tablet
                return EliteTablet.click().then(function () {
                    //describe the price element
                    var priceElement = browser.$(Web.Element({
                            className:"roboto-thin screen768 ng-binding",
                            tagName:"H2"
                        }
                    ));

                    //verify the price in the opened page matches the price in the previous page
                    return priceElement.innerText().then(function (priceText) {
                        expect(priceText).toContain("$1,279.00");
                    });
                });
            });
        }).then(done, done.fail);
    });
});

  

Summary

The LeanFT JavaScript SDK provides an execution synchronization mechanism, which can greatly simplify and improve the readability of your LeanFT JavaScript tests.

Labels:

Functional Testing