JavaScript: Synchronization, promises, and error handling

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.

Synchronize LeanFT steps and tests

LeanFT steps are asynchronous. They return promises of values that are returned when (if) the promise is fulfilled.

The promise returned by LeanFT commands is a custom promise and not the JavaScript built-in promise.

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)).

For steps that do not require verification or other processing on the returned results, then statements are not necessary. If one of these steps fails, an exception is thrown and subsequent steps will not be performed.

But where a subsequent step is dependent on the result of the previous step, add those steps in a then callback.  For example: 

list.selectedItems()
    .then(function (items) {
        assert.equal(items.length, 2);
        assert.strictEqual(items[0], "Socks");
        assert.strictEqual(items[1], "Cape");
    });

In this example, if an assert fails, the entire test fails.

Alternatively, you can provide a catch callback to handle the error. For more details about validating values, see Validate values.

Synchronizing tests

At the end of each test case, as well as at the end of your beforeEach and afterEach sections, you must call the LeanFT whenDone method and supply the done callback of your testing framework (Jasmine/Mocha). The whenDone method calls the appropriate variant of the done callback when the test is finished.

For example:

list.selectedItems()
    .then(function (items) {
        assert.equal(items.length, 0);
    })
    whenDone(done);

Back to top

Execution Synchronization

By default, every LeanFT command is synchronized using an internal entity called the PromiseManager.

For 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 uses execution synchronization. In this code, a click on someTestObject is performed and, when the operation is completed, a click on anotherTestObject is performed.

someTestObject.click();

anotherTestObject.click();

Note: Execution synchronization can be turned off. For details, see , see Disabling execution synchronization.

Back to top

Using a value returned from a command

Commands like click are synchronized automatically so there is no need to use a then to synchronize them.

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

Option 1- Use then

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

Option 2- Use 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 expect provided with LeanFT can receive functions which return a promise. It performs the expectation once the promise is resolved, thus removing the need for a then.

Note: To do something other than verification with the result, or to perform several verifications on it, only Option 1 is relevant.

Back to top

Promise chaining and promise trees

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 aThirdTestObject is performed only once both 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 promise tree as follows:

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

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

A click on aThirdTestObject is performed only once all three promises are complete.

Note: In this example the clicks on anotherTestObject and on yetAnotherTestObject are performed independent of each other. For more information, see Parallel execution with execution synchronization.

Back to top

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 with any of these promises and is not handled by a catch or a then with the reject function, all the synchronized commands that follow are not executed until the error is caught.

Examples:

  • The following example is an attempt to click nonExistingTestObject.

    nonExistingTestObject.click().catch(function () {});
    someTestObject.click();
    

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

  • In the following example, notice that there is no catch on non-existing object.

    nonExistingTestObject.click();
    someTestObject.click().catch(function () {});
    anotherTestObject.click();
    

    In this case, the error thrown by clicking nonExistingTestObject is not caught immediately, so the click on someTestObject is not performed.

    However, because 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 the 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 previous 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) receives 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 the correct status once all the 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);
});

Note:  

  • whenDone is a help function, and is not mandatory.

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

Back to top

Advanced

Parallel execution with execution synchronization

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

For 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 performs the code in the first then function until it reaches an asynchronous statement. It then most likely moves to execute the second then function, and vice versa.

Each synchronized command waits for the entire sub tree of the previous command to end. Therefore, when both then functions have ended, 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 commands inside a then function are synchronized among themselves in the same way as synchronized commands outside a 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();
});

Disabling execution synchronization

Execution synchronization can be disabled by passing the following configuration to the LeanFT.init() function:

LFT.init({executionSynchronization: false})

This disables command synchronization. Instead, they behave as regular promises.

So the following code performs a 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 code sample below, there is an example of a test case with execution synchronization disabled.

Back to top

Complete code sample

The full code examples demonstrates some of the principals and concepts described in this document. Also the examples demonstrate some common techniques, 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://54.210.48.65:8080/#");

		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 hpEliteTablet = browser.$(Web.Element({
			tagName:"LI",
			innerText:"SOLD OUT SHOP NOW HP Elite x2 1011 G1 Tablet $1,279.00 "
		   }
		));

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

		//click on the tablet
		hpEliteTablet.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 hpEliteTablet = browser.$(Web.Element({
			tagName:"LI",
			innerText:"SOLD OUT SHOP NOW HP Elite x2 1011 G1 Tablet $1,279.00 "
		   }
		));

		//click on the tablet
		hpEliteTablet.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 3var 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(qunatityOfItems).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(qunatityOfItems).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 hpEliteTabletWithWrongDescription = 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
		hpEliteTabletWithWrongDescription.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 tabletvar hpEliteTabletWithWrongDescription = 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
		hpEliteTabletWithWrongDescription.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 hpEliteTablet = browser.$(Web.Element({
				tagName:"LI",
				innerText:"SOLD OUT SHOP NOW HP Elite x2 1011 G1 Tablet $1,279.00 "
			   }
			));

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

				//click on the tablet
				return hpEliteTablet.click().then(function () {
					//describe the price elementvar 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);
	});
});