Skip Navigation
Resources Blog Testing Nexus with Selenium: A Lesson in Complex UI Testing ...

Testing Nexus with Selenium: A Lesson in Complex UI Testing (Part 2)


Part one

Specific Challenges with Nexus and ExtJS

When applying these rules specifically to Nexus - and its underlying UI framework, ExtJS - there are a few issues that actually work against the best practices when creating Selenium locators. For example, ExtJS automatically generates ID attributes for the underlying HTML elements that it creates when using the various UI components, such as a popup dialog box, form container, or sliding "drawer" present on the left hand side of Nexus.

These generated IDs look like ext-419, where 419 is a somewhat unpredictable/random number based on browser timing issues and page construction events that may change from page load to page load. What that means is that if you are recording a test, you might find these IDs referenced in your script, but if you were to play back the test, it might not work the next time around. This is clearly a problem - our "best practice" is no longer working for us.

The other issue we encountered was dealing with hidden or otherwise invisible components. ExtJS has a nice design that only generates the HTML elements for a component if it needs to be displayed. This "lazy loading" approach improves performance on the browser itself, but also makes your Selenium tests a little more difficult to write. This is because it's sometimes hard to tell whether you should use an "element present" check or an "element visible" check, and often need to use both to properly synchronize your tests.

Working with ExtJS and the Page Object Pattern

So given these problems with ExtJS, what can be done to make test creation easier? Fortunately, there's a great article that discusses the approach we used when developing the Nexus test case system. In short, the article suggests creating Java classes that represent the logical page-level components (i.e.: ChangePasswordWindow) that generate dynamic locators based on the ExtJS API.

For example, in Nexus, the window for changing your password is given an ID of "change-password-window," but the individual form fields are not given any ID that we could easily use in our locators. Fortunately, ExtJS provides a nice API for first locating the window and then locating the text field relative to the window:

var cpWindow = Ext.getCmp('change-password-window');
var textField = cpWindow.findBy(function(c) {
    return c.fieldLabel == 'Current Password';
})[0];

This code uses the Ext.getCmp() function, which returns a component located somewhere on the page by ID. From there, we then use the findBy() function, which takes in an anonymous function that we use to filter down all components in the cpWindow and extract only the one we're interested in.

What this means is that we could write a Selenium expression that types into that field using the following expression:

document=Ext.getCmp('change-password-window').findBy(function(c) { return c.fieldLabel == 'Current Password' })[0]

While that would work, clearly it's a lot of text and wouldn't be very maintainable. But what we can do is extract these various bits into Java classes that will be smart enough to abstract all this complexity away. Our Selenium test, now written in Java, will look very simple:

public class ChangePasswordTest extends SeleniumTest {
    @Test
    public void changePasswordSuccess() {
        main.clickLogin()

                .populate(User.ADMIN)
                .loginExpectingSuccess();

        ChangePasswordWindow window = main.securityPanel().clickChangePassword();

        // ...
        
        PasswordChangedWindow passwordChangedWindow = window

                .populate("password", "newPassword", "newPassword")
                .changePasswordExpectingSuccess();

        passwordChangedWindow.clickOk();
    }
}

What we've done is abstract out and conceptualize all the major components of the Nexus UI into simple, reusable Java classes. First, we can see that ChangePasswordTest extends SeleniumTest. SeleniumTest is a class we created that automatically sets up Selenium according to a few infrastructure requirements and settings (more on that later). Most importantly, it creates a protected "main" variable that we reference at the start of this test.

The main variable is a MainPage object, designed to represent all the top-level interactions a user can do with Nexus, such as click the "login" link in the upper right-hand corner. This has been abstracted out so you can simply call clickLogin(), which returns a LoginWindow object, which can be populated with the login credentials for the admin, and then finally logged in and told to expect success. If the login fails, the LoginWindow code is designed to throw an exception and fail the test.

Next, we navigate to the security panel on the left-hand side of the Nexus UI with securityPanel() and then click the "change password" link with clickChangePassword(), which returns a ChangePasswordWindow. With a handle to this window, we can populate the embedded form with the populate() function, and then finally click the button that saves the changes.

The entire result is a clean test, but unless you have seen an example of one of these underlying objects, it may look like a bunch of magic. Let's look at the ChangePasswordWindow.java source:

 
public class ChangePasswordWindow extends Window {
    private TextField currentPassword;
    private TextField newPassword;
    private TextField confirmPassword;
    private Button button;

 

    public ChangePasswordWindow(Selenium selenium) {
        super(selenium, "window.Ext.getCmp('change-password-window')");
 
        currentPassword = new TextField(this, ".findBy(function(c) { return c.fieldLabel == 'Current Password' })[0]");
        newPassword = new TextField(this, ".findBy(function(c) { return c.fieldLabel == 'New Password' })[0]");
        confirmPassword = new TextField(this, ".findBy(function(c) { return c.fieldLabel == 'Confirm Password' })[0]");
        button = new Button(selenium, "window.Ext.getCmp('change-password-button')");
    }
 
    public ChangePasswordWindow populate(String current, String newPass, String confirm) {

        currentPassword.type(current);

        newPassword.type(newPass);
        confirmPassword.type(confirm);
 

        return this;

    }

 

    public PasswordChangedWindow changePasswordExpectingSuccess() {
        button.click();
        waitForHidden();
 
        return new PasswordChangedWindow(selenium);
    }
}

A few important notes: ChangePasswordWindow extends Window, another class we've created that provides access to standard capabilities that ExtJS exposes in any window component. Window itself extends a Component class, which also exposes generic functionality that any ExtJS component can provide, such as waiting for the component to be hidden with the waitForHidden() method.

In the constructor, you can also see that we define the JavaScript expression that gets a handle to the window itself, but also create four additional objects that represent the critical form elements we want to interact with. The key thing to note is that some of these components can take in another component (the ChangePasswordWindow) while others take in the Selenium object itself.

The difference here is that components that take in another component can have a partial locator expression, since it will be strung together using the parent component's locator string. Alternatively, those given a Selenium object directly must be given a fully qualified and standalone component locator. In this example, you can see both uses, since the button had an ID that we could reference, but the text fields did not.

This approach is commonly referred to as the Page Object Pattern and is becoming increasingly popular among the Selenium community as a way to write tests that will withstand the test of time. But even a well designed test is only half the battle: if the underlying application state isn't reliably recreated each time the test runs, the tests will likely have difficulty passing consistently.

Picture of Brian Fox

Written by Brian Fox

Brian Fox, CTO and co-founder of Sonatype, is a Governing Board Member for the Open Source Security Foundation (OpenSSF), a Governing Board Member for the Fintech Open Source Foundation (FINOS), a member of the Monetary Authority of Singapore Cyber and Technology Resilience Experts (CTREX) Panel, a ...