Selenium on JavaScript : User List Test

I wasn’t quite sure what to name this article.  The Selenium on JS example here can be used to scan any table to ensure every entry on a list of strings exists; should I name it Test Web Page Has All Your Important Data?   The test also uses a separate NodeJS module to configure that list of important string, in my case user account names; Using NodeJS modules to configure repetitive data lists?  It also employes the Promise construct inherent in Selenium on NodeJS so maybe The Right Way To Wait Before Testing Data?

Instead I chose “User List Test” because that is what my test case does.

In this example I want to test a list of user accounts I deem “critical test users” to ensure that none of the user accounts have gone missing after upgrading our staging or production server.  Each user account was selected because it stress tests specific parts of our MySLP SaaS application.    That means I also want to make sure the user accounts are still active before delving deeper into the test suite.

MySLP Dashboard 2017-09-26_22-06-56.png

This example is a bit more complex than the prior Selenium articles written this week and builds on an excellent resource I found online.  It talks about the Promise model and how to properly wait for things to exist versus using the “hack” driver.sleep() command to wait for stuff to appear.

Let’s get into it.    Here is a test that loops through a list of users and checks to make sure they all appear in an HTML table that is the list of users.

The App

Before I get into the code I want to show you the web app interface.  It is a modified variant of the WordPress User table.   This is where we will test that the table cells have the users I’m looking for.

MySLP Customer Blur Partial 2017-09-26_20-56-09.png

The Source

The primary test case.

var config = require( '../config/myslp_config' );

var webdriver = require('selenium-webdriver'),
    Builder = webdriver.Builder,
    By = webdriver.By,
    logging = webdriver.logging,
    until = webdriver.until;

// Setup Logging
//
var log = logging.getLogger('webdriver.http');
logging.installConsoleHandler();
log.setLevel( logging.Level.DEBUG );
log.info( 'Customer Testing' );

// Setup the Web Driver
//
var driver = new Builder().forBrowser('safari').build();
driver.manage().window().setSize( 1200 , 800 );
driver.manage().window().setPosition( 100 , 100 );

// Get the Dashboard
//
var previous_el;
driver.get( config.url_for.dashboard )

    // Login To Admin
    //
    .then( _ => driver.findElement( { 'id' : 'email'          } ).sendKeys( config.admin.user ) )
    .then( _ => driver.findElement( { 'id' : 'login-password' } ).sendKeys( config.admin.pwd  ) )
    .then( function() {
        previous_el = driver.findElement( { 'css' : '[type=submit]' } );
        previous_el.click();
        })
    .then( function() {
        driver.wait(until.stalenessOf( previous_el ), config.setting.standard_wait );
        log.info( 'Admin user logged in.' );
        })

    // Get Next Customer List URL
    //
    .then( _ => driver.get( config.url_for.dashboard + '/wp-admin/network/admin.php?page=customer_list&order=desc&orderby=payment') )
    .then( function() {
        driver.wait(until.elementLocated( By.xpath( '//h1[contains(.,"MySLP Customers")]' ) ) , config.setting.standard_wait )
            .then( _ => {
                log.info( 'List Customers admin page opened.' );

                var customers = require( '../config/active_customers' );
                const test_customer = require( '../lib/customer_tests');

                for ( var cnt = 0 , all = customers.active.length ; cnt < all ; cnt++  ) {
                    test_customer.on_list( driver , config , customers.active[cnt].username );
                }
                test_customer.reset_tested();
            })
        })

    // Exit
    //
    .then( _ => driver.sleep( config.setting.long_wait ))
    .then( _ => driver.quit())
    ;

The customer tests module.

var webdriver = require('selenium-webdriver'),
    Builder = webdriver.Builder,
    By = webdriver.By,
    logging = webdriver.logging,
    until = webdriver.until;

var log = logging.getLogger('webdriver.http');
logging.installConsoleHandler();
log.setLevel( logging.Level.INFO );

var tested = 1;

module.exports = {
    on_list: function( driver , config , customer ) {
        driver.wait( until.elementLocated( By.xpath( '//td[contains(.,"' + customer + '" )]' ) ) , config.setting.short_wait )
            .then( found_el => { log.info( '(' + tested++ + ') Found ' + customer ); } , missing_el => { log.info( 'Could not find ' + missing_el ); } );
    },
    reset_tested: function() { tested = 1; }
};

The config module.

/**
 * Set the environment to staging or production.
 *
 * The admin property will be set to the values configured in ./env/staging-real.js or ../env/production-real.js
 *
 * Default environment is staging.  Rather than edit this code you can set the environment variable NODE_ENV:
 *
 * $ NODE_ENV=production node login.js
 * $ NODE_ENV=staging node login.js
 */
var environment = process.env.NODE_ENV|| 'staging';

// Admin Login - configure in the env/<environment>-real.js file
var admin = require( '../env/' + environment + '-real' );

// URLs
var url_for = {};
if ( environment == 'production' ) {
    url_for = {
        'my': 'https://my.storelocatorplus.com',
        'dashboard': 'https://dashboard.storelocatorplus.com',
    };
} else if ( environment == 'dev' ) {
    url_for = {
        'my': 'http://my.wordpress.dev',
        'dashboard': 'http://dashboard.wordpress.dev',
    };
} else {
    url_for = {
        'my': 'https://mybeta.storelocatorplus.com',
        'dashboard': 'https://dashbeta.storelocatorplus.com',
    };
}

// test User
var test_user = [
    { 'email' : 'test1@slp.guru', 'first' : 'TestOne'}
    ];

// Process
var setting = {
    short_wait: 2000,
    standard_wait: 6000,
    long_wait: 10000,
};

// What to export
module.exports = {
    admin: admin,
    setting: setting,
    url_for: url_for,
    test_user: test_user,
};

The customer list module.

// What to export
module.exports = {
    active: [
        { 'username' : 'basil_'            },
        { 'username' : 'icecream_'               },
        { 'username' : 'mary_'         },
        { 'username' : 'shannon_'         },
        { 'username' : 'mell_'    },
    ]
};

The sample credentials module.

var user = 'admin@slp.guru';
var pwd = 'myeasyadminpassword';

module.exports = {
    user: user,
    pwd: pwd
};

The Walk Through

I’ve already written articles on how to setup Selenium on JavaScript to drive Safari (SoJa / Safari).   It requires Safari 10+ with automation enabled plus NodeJS with the Selenium Driver and Webdriver modules installed.  I run on a MacOS from within phpStorm. You’ll find my examples and screen shots based on that environment.

The Setup

The setup has been covered in prior articles as well.  You can review those for details.  The short version of this step:

  • load the config module using the NodeJS require() method
  • add some aliases for the web driver modules we use most often
  • configure logging via the logging module
  • start up the webdriver to get our NodeJS environment talking to Safari and set the browser window size and position

Starting The Test

We start by logging in to the dashboard.   A standard driver.get() with our dashboard URL is going to kick things off.   We string together our Promise fulfillments via a series of then() methods.   In case you missed the prior articles the then() executes when the method they are attached to has “fulfilled a promise” (is done by failure or success).   Basically we are saying “go to this URL and when the page loads then do some other things.

Our other things?  Find the email element then type in our admin username.   Find the password element and type that in.  Then submit the form.

If you are wondering where the values are being set, that is coming from the config module that reads staging-example.js or production-example.js depending on what I’ve set NODE_ENV to when starting up my node script.    This way I can have separate credentials for live or production servers and only need to change my environment variable to switch not only which URL I’m testing against but my username/password to login.   You can read more about this and why the config file loads -real.js instead of -example.js in the configuration article.

Timing The After Login Stuff

One of the first things I started to realize is a simple string of .then commands was not going to work.  Selenium runs the browser command FAR faster than the server responds or a typical user types things in.   This means building commands based on waiting for things to appear in the browser.      As mentioned previously, the sleep() trick is a horrible way to manage this.

If we boil down the test case itself to its essence we get something like this:

get( The Main Login Page )
.then( find the email input ) .sendKeys( the email login )
.then( find the password input) .sendKeys( the password )
.then( find the submit button and click it )

If you look at the source that last submit button and click it code looks different.    Let’s get into that for a moment, but first here is that code snippet:

driver.get( config.url_for.dashboard )

    // Login To Admin
    //
    .then( _ => driver.findElement( { 'id' : 'email'          } ).sendKeys( config.admin.user ) )
    .then( _ => driver.findElement( { 'id' : 'login-password' } ).sendKeys( config.admin.pwd  ) )
    .then( function() {
        previous_el = driver.findElement( { 'css' : '[type=submit]' } );
        previous_el.click();
        })

 

The first thing to know is that then() is a function that takes 2 parameters.  The first parameter is a function to run when the prior thing that then is attached to was succesful.   The second parameter is a function to run when the prior thing failed; we’ll get to that later.   In this case for the sake of simplicity we ignore the failure.   Another important tidbit here is to know that both the success and failure functions get passed a parameter.    That parameter is the result of the promise that the .then() is attached to; our driver.get() in this case.  We’ll ignore what that parameter actually is for now.

On the first 2 then calls the format looks a little different. What is that underscore and pointer thing?   It is syntactic sugar that keeps the code readable with less braces and brackets.   .then( _ => blah() ) is basically saying “assign the paramter we get for the success function to _ so we can get to it later and then run the blah() function with it.   Since we are going to throw away the parameter we use _ because it is not visually distracting.   So the first 2 then() calls say “ignore the parameter driver.get() spit out and just go find the HTML element with the ID email , or password, and type some text in there.

The third call after we find the email and password fields says “when you’ve succesfully finished those two things go run the following function”.    We first find the submit button element using the css type=submit locator and assign that to the previous_el variable.   Why we do that will be clear in the next task.    Since previous_el is now assigned to our submit button we can use that shorthand to click it.

Side Note: In general it is always faster to assign an element that a JavaScript library like our Selenium Driver here , or jQuery lookups, to a variable so you don’t keep scanning the DOM to locate the thing you just located.  

So we’re done with logging in.    We go to a specific site URL, when it finishes loading the page we find the email and password inputs and type in our login info, then find the submit button (that we saved ot our previous_el variable) and click it.

Onward and upward.

We Logged In, Now What?

Moving down a line we see another then() attached to our prior then that clicked the submit button.   Again we are using a more explicit form of the success function.   Why?  Because we want to log something to the console to tell us the login worked.   Not critical, but it is nice to know your tests are doing what you expect even when they are working.

.then( function() {
    driver.wait(until.stalenessOf( previous_el ), config.setting.standard_wait );
    log.info( 'Admin user logged in.' );
    })

You’ll also notice we are now using a new until.stalenessOf() method in this function.    This is where our previous_el comes in.    Selenium WebDriver has a timer-based method built in that will loop around until a specific condition is met or until the specified timeout has been reached.  In this case we are going to wait until our submit button from the previous page has been marked “stale” for up to 6 seconds (config.setting.standard_wait = 6000 in our config file).

Now you know why we recorded the WebElement for the submit button in our previous then test when we found it.     Using stalenessOf( element from a prior page ) is a great way to ensure you’ve left the previous page.    It does not require that you know anything about the next page you are about to load.     We’ll worry about that in a moment.

Loading and Testing The Customer List

Now that we’ve performed the login and waited to ensure we got past the login page we can now continue with our next step in the original driver.get().then() sequence.   Here we are going to go a little deeper into the then() nesting by “latching on” to the a new driver.wait() call.

The essense of the code:

driver.get( our customer list page )
.then( run our list testing function )

OK, that is a LOT simpler than the code we have written.   Let’s dig a little deeper and expand on the “run our list testing function” statement to this:

wait( until.elementLocated( the H1 tag with MySLP Customers is found ) )
.then( 
output some text
get a list of customers
load our customer test lib
and test all the entries are found 
)

Here it is important to note that we do not just string along another then() on our original driver.get() series.    It is important to specifically wait not only for the new customer list page URL to load but also to ensure that page has content.    For this example we are going to assume that if the header tag “MySLP Customers” is rendered that our customer table is complete.  In reality we will check for a specific HTML marker that is output only after the customer list is fully rendered.

The code is loading the URL then calling a function to do a lot more stuff.   That function says “don’t do anything until we see that MySLP Customers header”.     We use the standard Promise .then() method once we do see that  header to start our processing.  The firs thing we do is output something to the log to tell use the customer list page is open and ready for testing.

Testing The List

Let’s start with the code that does the test plus our couple of actual code lines outlined above.

// Get Next Customer List URL
//
.then( _ => driver.get( config.url_for.dashboard + '/wp-admin/network/admin.php?page=customer_list&order=desc&orderby=payment') )
.then( function() {
    driver.wait(until.elementLocated( By.xpath( '//h1[contains(.,"MySLP Customers")]' ) ) , config.setting.standard_wait )
        .then( _ => {
            log.info( 'List Customers admin page opened.' );

            var customers = require( '../config/active_customers' );
            const test_customer = require( '../lib/customer_tests');

            for ( var cnt = 0 , all = customers.active.length ; cnt < all ; cnt++  ) {
                test_customer.on_list( driver , config , customers.active[cnt].username );
            }
            test_customer.reset_tested();
        })
    })

What is var customers = require() doing?

It is loading our active_customers module.  That module sets up a JavaScript object that is assigned to customers that we can later reference.  This lets us later update the active_customer.js module in our config directory so we don’t have to hack up the main test code to add users.   It keeps our future edits somewhat isolated and will allow us ot later use other non-code-editor tools to allow our QA team to easily load a “check this user” list.    In this case the active customers we want to test will end up in the customers.active[] array.  It is an array of objects with a username key that holds the usernames we want to test.

Next Up: test_customer…

We assign our customer_tests library of customer test functions to the test_customer constant (we could have used a var just as easily) which gives us access to all our re-usable test methods.    The library, outside of loading up a bunch of our shorthand aliases and enabling logging for our module scope, provides two simple methods.   An on_list() method and a reset_tested() method.    reset_tested() only re-sets our test counter to 1, fairly simple and not really doing much in this single test.

on_list() is passed the driver, config, and customer name we are looking for.  Driver and config are only so we dont’ have to re-invoke another copy of each within every submodule.   Customer is the string that will change every time our loop moves to the next customer.

The guts of the customer_tests.js module:

var tested = 1;

module.exports = {
    on_list: function( driver , config , customer ) {
        driver.wait( until.elementLocated( By.xpath( '//td[contains(.,"' + customer + '" )]' ) ) , config.setting.short_wait )
            .then( found_el => { log.info( '(' + tested++ + ') Found ' + customer ); } , missing_el => { log.info( 'Could not find ' + missing_el ); } );
    },
    reset_tested: function() { tested = 1; }
};

Within the function we do a simple “find a TD tag that has text containing the customer string we are seeking”.   You can see that we are using the more advanced form of then() where we not only have a success and failure function but also name our parameters.    If the thing we are looking for is found within 6 seconds we print out a found message with the customer string preceding by a count of how many we’ve found so far.    If the TD with the customer cannot be located on the page we log a could not found message instead.

When It Runs

Our NodeJS app fires up a Safari window, goes to the production or staging site, logs in our admin user, goes to the customer list page, then looks for a dozen-or-so key customer accounts to ensure they are still on our customer list.   We wait 10 seconds then close the test Safari browser.

This is what we see in our phpStorm NodeJS test execution window when the test runs:

phpStorm NodeJS Customer List Test Output 2017-09-26_22-04-47.png

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.