Cookbook for OPA5Locate this document in the navigation structure

Best practices for OPA tests.

The following sections provide common use cases for retrieving controls.

Retrieving a Control by its ID

Example:

new sap.ui.test.Opa5().waitFor({
            id : "page-title",
            viewName : "Category",
            viewNamespace : "my.Application.",
            success : function (oTitle) {
                ok(oTitle.getVisible(), "the title was visible");
            }
        });

In this example, we search for a control with the ID "page-title". The control is located in the my.Application.Category view.

After finding the control, OPA5 invokes the check function until it returns true. This time, the check function has another parameter with the control instance.

Another example:

new sap.ui.test.Opa5().waitFor({
                    id : "productList",
                    viewName : "Category",
                    success : function (oList) {
                        ok(oList.getItems().length, "The list did contain products");
                    }
                });

In this example, the check function is omitted. In this case, OPA5 creates its own check function that waits until the control is found.

Triggering Clicks on Controls

The easiest way to trigger events is using the tap event for mobile controls or the click event for common controls in jQuery:

iPressOnTheFirstItemInAList : function (myList) {
                var oFirstItem = myList.getItems()[0];

                oFirstItem.$().trigger("tap");
                return this;
            },

This triggers an event even if the control cannot be clicked by an end user. If that is an issue, you can add functionality to check the clickability of a control.

Retrieving a Control that does not have an ID

Sometimes you face the issue that you want to test for a control that has no explicit ID set and maybe you cannot or you do not want to change this for your test. To get around this issue, use a custom check function to filter for a control that fits your expectation. Let's assume we have a view called "Detail" and there are multiple sap.m.ObjectHeaders on this page. We want to wait until there is an object header with the title "myTitle".

For achieving this, use the following code:

return new Opa5().waitFor({
            controlType : "sap.m.ObjectHeader",
            viewName : "Detail",
            matchers : new sap.ui.test.matchers.PropertyStrictEquals({
                                         name : "title",
                                         value: "myTitle"
                                   }),
            success : function (aObjectHeaders) {
                strictEqual(aObjectHeaders.length, 1, "was there was only one Object header with this title on the page");
                strictEqual(aObjectHeaders[0].getTitle(), "myTitle", "was on the correct Title");
            }

As no ID is specified, OPA passes an array of controls into the check function. The array contains all visible object header instances in the Detail view. However, a build in support for comparing properties does not exist (yet). So we implement a custom check to achieve this.

More About Matchers

You can use the following predefined matchers to retrieve controls:

sap.ui.test.matchers.Properties: This matcher checks if the controls have properties with given values. The values may also be defined as RegExp expressions for the string type properties.

return new Opa5().waitFor({
            controlType : "sap.ui.commons.TreeNode",
            matchers : new sap.ui.test.matchers.Properties({
                text: new RegExp("root", "i"),
                isSelected: true
            }),
            success : function (aNodes) {
                ok(aNodes[0], "Root node is selected")
            },
            errorMessage: "No selected root node found"
});

sap.ui.test.matchers.Ancestor: This matcher checks if the control has the given ancestor (ancestor is of a control type).

var oRootNode = getRootNode();
return new Opa5().waitFor({
            controlType : "sap.ui.commons.TreeNode",
            matchers : new sap.ui.test.matchers.Ancestor(oRootNode),
            success : function (aNodes) {
                notStrictEqual(aNodes.length, 0, "Found nodes in a root node")
            },
            errorMessage: "No nodes in a root node found"
});

You can also define a matcher as an inline function: The first parameter of the function is a control to match. If the control matches, return true to pass the control on to the next matcher and/or to check and success functions.

return new Opa5().waitFor({
            controlType : "sap.ui.commons.TreeNode",
            matchers : function(oNode) {
                return oNode.$().hasClass("specialNode");
            },
            success : function (aNodes) {
                notStrictEqual(aNodes.length, 0, "Found special nodes")
            },
            errorMessage: "No special nodes found"
});

If you return some 'truthy' value from matcher, but not a Boolean, it will be used then as an input parameter for the next matchers and/or check and success. This allows you to build a matchers pipeline.

return new Opa5().waitFor({
            controlType : "sap.ui.commons.TreeNode",
            matchers : [
                function(oNode) {
                    // returns truthy value — jQuery instance of control
                    return oNode.$();
                },
                function($node) {
                    // $node is a previously returned value
                    return $node.hasClass("specialNode");
                }
            ],
            success : function (aNodes) {
                // aNodes is an array of matching controls jQuery instances
                aNodes[0].trigger("click");
            },
            errorMessage: "No special nodes found"
});
Searching for Controls Inside a Dialog

This example covers the following use case: We want to press a button with Order Now text on it inside a dialog. To achieve this, we set the searchOpenDialogs option to true and then restrict the controlType we want to search to sap.m.Button. We use the check function to search for a button with the text Order Now and save it to the outer scope. After we found it, we trigger a tap event:

iPressOrderNow : function () {
                var oOrderNowButton = null;
                this.waitFor({
                    searchOpenDialogs : true,
                    controlType : "sap.m.Button",
                    check : function (aButtons) {
                        return aButtons.filter(function (oButton) {
                            if(oButton.getText() !== "Order Now") {
                                return false;
                            }

                            oOrderNowButton = oButton;
                            return true;
                        });
                    },
                    success : function () {
                        oOrderNowButton.$().trigger("tap");
                    },
                    errorMessage : "Did not find the Order Now button"
                });
                return this;
            }
Executing a Single Statement After Other waitFor are Done

If you skip all parameters except for the success parameter, you can execute something after other waitFors are done. As there is no check function, OPA runs directly to success.

iChangeTheHashToTheThirdProduct : function () {
        return this.waitFor({
            success : function () {
                sap.ui.test.Opa5.getWindow().location.hash = "#/Products(2)";
            }
        });
    },
Passing a Parameter from One waitFor to another

To check some special conditions, for example, how one control relates to another control, you might need to pass a control found in one waitFor statement as input for another waitFor statement. The following two options exist:

  • Storing the control in a variable in the outer scope: Use this option if you have a common outer scope, like the same functions or the same page object file.

  • Storing the control in the OPA context: Use this option if you need to pass the parameter, for example, across some page objects.

iDoSomething: function () {
        var oControl;
        this.waitFor({
            id : "myControlId",
            success : function (oCtrl) {
                //store control in outer scope
                oControl = oCtrl;
                
                //as alternative you could store the control in the Opa context
                sap.ui.test.Opa.getContext().control = oCtrl;
            }
        });
        return this.waitFor({
            controlType : "some.other.control"
            check: function (aControlsFromThisWaitFor) {
                //now you can compare oControl with aControlsFromThisWaitFor

                //or you can compare sap.ui.test.Opa.getContext().control with aControlsFromThisWaitFor
            }
        });
    },
Writing Nested Arrangements and Actions

UI elements may be recursive, for example, in a tree. Instead of triggering the action for each known element, you can also define it recursively, see the code snippet below. OPA takes care that the waitFor statements triggered in a success handler are executed before the next arrangement, action, or assertion. That also allows you to work with a not yet known number of entries, for example in a list. First, you wait for the list, and then trigger actions on each list item in the success handler.

iExpandRecursively : function() {
    return this.waitFor({
        controlType : "sap.ui.commons.TreeNode",
        matchers : new sap.ui.test.matchers.PropertyStrictEquals({
            name : "expanded", 
            value : false
        }),
        success : function (aTreeNodes) {
            var that = this;
            aTreeNodes.forEach(function(oTreeNode) {
                if (oTreeNode.getNodes().length){
                    oTreeNode.expand();
                    that.iExpandRecursively()
                }
            });
        },
        errorMessage : "Didn't find collapsed tree nodes"
    });
}
Structuring OPA Tests With Page Objects

The page object design pattern supports UI-based tests with improved readability, fostering the don't repeat yourself (DRY) principle of software development that is aimed at reducing repetition of any kind of information. A page object wraps an HTML page or fragment with an application-specific API, thus making is easy to find a control and provide reuse across multiple tests. If you have multiple pages or UI areas that have several operations, you can place them as reuse functionality in page object. The page object groups all OPA arrangements, actions, and assertions that logically belong to some part of the screen. As only the test will know if an action is used to setup the test case or to act on the application under test, the page object will combine actions and arrangements into actions. In contrast to the general guidance of Selenium and Martin Fowler, OPA page objects also provide assertions, as the corresponding testing via waitFor statements better fit into the page objects. When you define actions or assertions in your page object, have in mind how the test would spell them and if that would be similar to the way you would explain a scenario to your colleagues.

Page objects accept parameters, so you can parametrize your tests either by writing multiple tests, or by letting your test being repeated on a set of parameters defined in the code. It is also possible to put test fragments into a separate file and refer to this file in the test. This enables you to reuse the same test fragments in different test pages with different setups.

You can also share utility functionality between page objects. Simulating clicks, for example, is interesting for most page objects and should be placed in a base class that you can create. As the page objects extend the base class, the functions provided in the base class are available for the page objects. If, for example, you want to share tree handling functions in all tree-based page objects, create a TreeBase class by extending the base class. Tree-based page objects such as repository browser and outline then specify TreeBase as baseClass instead of the generic base class.

OPA5 provides a static method to create page objects, see Explored - OPA Samples.

Opa5.createPageObjects({

       //give a meaningfull name for the test code
       inThe<Page Object> : {
            //Optional: a class extending Opa5, with utility functionality
            baseClass : fnSomeClassExtendingOpa5,
             
            actions : {
               //place all arrangements and actions here
               <iDoSomething> : function(){
                   //always return this or a waitFor to allow chaining
                    return this.waitFor({
                        //see documentation for possibilities
                    });
                }
           },
           assertions : {
               //place all assertions here
               <iCheckSomething> : function(){
                   //always return this or a waitFor to allow chaining
                    return this.waitFor({
                        //see documentation for possibilities
                    });
                }
           }
       }
   });

The method in your test finds all actions at the Given and When object, the assertions will be at the Then object. Everything is prefixed with the page object name.

When.inThe<Page Object>.<iDoSomething>();

Then.inThe<Page Object>.<iCheckSomething>();

Be careful with Opa5.extendConfig(), if you give arrangements, actions, or assertions, all previously loaded page objects will be overwritten. So in case of mixing them, call extendConfig before loading the page objects, see Explored - OPA Samples.

Pitfalls and Troubleshooting

OPA is Not Starting - sinon.js

If you require sinon-qunit.js, it overwrites the browser functions setTimeout and setIntervall. OPA needs these functions and without them, the tests will not start. You can either set the fakeTimers to false in you test setup, or maybe you do not use sinon together with OPA.

Finding out What Goes Wrong With the Checks

Turn the debug mode on by setting sap-ui-debug=true as URL parameter and have a look into the console. OPA has many logging messages. If you require sinon-qunit.js, it overwrites the setTimeout and setIntervall functions of the browser. As OPA uses these functions for polling, it will not even start to run and just time out. To work around this, either do not load it, or set fakeTimers of sinon for OPA to false:

    module("Opatests", {
        setup : function () {
            sinon.config.useFakeTimers = false;
        },
        teardown : function () {
            sinon.config.useFakeTimers = true;
        }
    });