Example Automated Test with Node.js, Selenium, and Jasmine

To illustrate the basic concepts of automated testing, the example below uses the following tooling to perform a test within Skuid:

  • Node.js: A Javascript runtime environment, commonly used for server-side logic. It can also be run from the command line on a local machine. In this case, Node provides the runtime environment to run the test suite—allowing the test to be written in JavaScript.
  • Selenium: A cross-platform, cross-browser suite of automated testing tools. More specifically, this example uses WebDriverJS to work in a Node.js environment. Selenium provides the automated point-and-click functionality used to open and test web pages, while WebDriverJS’s bindings determine the language used to write the automated actions of the test.
    • Selenium also requires a driver for the browser that will run the tests. The example below uses the ChromeDriver.
  • Jasmine: A behavior-driven JavaScript testing framework, used in conjunction with Selenium to process test results. The testing framework determines the syntax used to write the expectations of tests.

Any of the the above may be substituted with tooling of your choice, but the samples below provide a starting point from which to build your own custom tests and implementations.

Note

This example assumes that you have installed the software noted above and are working on a Linux or macOS system.

Prerequisites

  • The tests below use an example page that must exist within your Skuid site. Copy and paste this XML into a Skuid page named SeleniumTest in your chosen environment.

  • The sections below also discuss the construction of the example-spec JS file. You may combine the two code samples below and save the file yourself, or download the file here and follow along.

  • You’ll also need to install the software listed above. You may do so manually at each of the above linked sites, or you may run the following commands from your terminal.

    # Install node Node.js from their download page: https://nodejs.org/en/download/
    # Or via a package manager: https://nodejs.org/en/download/package-manager/
    # For example, with Homebrew:
    brew install node
    
    # Next, install jasmine globally
    npm install -g jasmine
    
    # Finally, navigate to the directory with example-spec.js
    # And install these node modules there so they are available
    # for the tests
    cd /path/to/example-spec-file/
    npm install --save chromedriver
    npm install --save selenium-webdriver
    

Accessing Skuid through automation software

Before any testing can begin, first write the logic to log in to a Skuid site. After writing this logic in your chosen language, you can reuse it for all future tests.

The example below, written in nodeJS, does the following:

  • Imports the required modules used by Selenium for automated testing.
  • Looks to the environment variables of the current runtime for Skuid credentials.
  • Establishes which HTML elements to target for entering credentials and logging in.
  • Checks whether SSO has been enabled for the Skuid site, navigating to the basic authentication page if so.
  • Enters the username and password
  • Clicks the Log In button element to log in.

See the emphasized lines below for this logic:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// Require modules used in the logic below
const {Builder, By, Key, until} = require('selenium-webdriver');

// You can use a remote Selenium Hub, but we are not doing that here
require('chromedriver');
const driver = new Builder()
    .forBrowser('chrome')
    .build();

// Get environment variables for Skuid site credentials
const baseUrl = process.env.SKUID_HOST
const username = process.env.SKUID_UN
const password = process.env.SKUID_PW

// Logic to login to a Skuid site using the above credentials
// Note: This function does not check if already logged in. 
var login = async function() {
    // Define login elements
    let loginContainer = By.css('.public-main-wrapper');
    let inpUsername = By.xpath('//*[@id="username-input-field"]//input');
    let inpPassword = By.xpath('//*[@id="password-input-field"]//input');
    let btnLoginWithoutSso = By.id('login-without-sso-button');
    let btnLogin = By.id('userpass-login-button');
    var enterCredentialsAndLogin = async function() {
        console.log('Entering credentials...')
        // Wait until an input element appears
        await driver.wait(until.elementLocated(inpUsername), 10 * 1000);
        // Enter credentials and log in
        await driver.findElement(inpUsername).sendKeys(username);
        await driver.findElement(inpPassword).sendKeys(password);
        await driver.findElement(btnLogin).click();
    }

    // Load the login page
    await driver.get(baseUrl + '/ui/login');

    // Wait until the page is loaded
    await driver.wait(until.elementLocated(loginContainer), 10 * 1000);
    console.log('Login screen loaded.')

    // Check to see if SSO has been enabled for this site.
    // If it has, click through to log in with credentials instead.
    await driver.findElement(btnLoginWithoutSso).then(async function(){
        console.log ('SSO enabled. Logging in with username and password instead.')
        await driver.findElement(btnLoginWithoutSso).click()
        enterCredentialsAndLogin()
    }, async function(){
        await enterCredentialsAndLogin()
    })

    // Wait to be logged in, assuming it was was successful 
    // once the Log in button has gone "stale."
    await driver.wait(until.stalenessOf(driver.findElement(btnLogin)));
    console.log('Logged in.')
}
// Configure Jasmine's timeout value to account for longer tests.
// Adjust this value if you find our tests failing due to timeouts.
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000;

// Define a category of tests using test framework, in this case Jasmine
describe("Basic element tests", function() {
    // Before every test, open a browser and login
    // using the logic written above.
    beforeEach(async function() {
        await login();
        console.log('Test beginning.')
    });
    // After each test, close the browser.
    afterAll(async function() {
        await driver.quit();
    });

    // Specify a test
    it("Click the button, Verify Correct UI Block Message", async function() {
        // Provide basic data used to evaluate the test.
        // This test should pass.
        var testData = {
            pageName: 'SeleniumTest',
            button: By.css('#test-button'),
            blockMessage: By.css('div.blockUI.blockMsg')
        }
        console.log('Running test...')

        // Preview the test page
        await driver.get(baseUrl + '/ui/page/preview/' + testData.pageName);

        // Wait for button
        await driver.wait(until.elementLocated(testData.button), 10 * 1000);

        // Verify button is present
        expect(await driver.findElement(testData.button).isDisplayed()).toBe(true);

        // Click button
        await driver.findElement(testData.button).click();

        // Wait for the blocked UI message to appear
        await driver.wait(until.elementLocated(testData.blockMessage), 10 * 1000);

        // Verify the text of the message, which should match the example page XML
        expect(await driver.findElement(testData.blockMessage).getText()).toBe('The button renders and is clickable.');
    });

    // Specify a second test
    it("Click the button, Verify Incorrect UI Block Message", async function() {
        // Provide basic data used to evaluate the test.
        // This test should fail.
        var testData = {
            pageName: 'SeleniumTest',
            button: By.css('#test-button'),
            blockMessage: By.css('div.blockUI.blockMsg')
        }

        // Preview the test page
        await driver.get(baseUrl + '/ui/page/preview/' + testData.pageName);

        // Wait for button
        await driver.wait(until.elementLocated(testData.button), 10 * 1000);

        // Verify button is present
        expect(await driver.findElement(testData.button).isDisplayed()).toBe(true);

        // Click button
        await driver.findElement(testData.button).click();

        // Wait for and Verify Correct UI Block Message
        await driver.wait(until.elementLocated(testData.blockMessage), 10 * 1000);

        // Verify the text of the message, which should *not* match the example page XML
        expect(await driver.findElement(testData.blockMessage).getText()).toBe('These aren\'t the Droids you\'re looking for..');
    });
});

Designing tests

For the rest of the example-spec JS file, we detail two specs. Both will click the button on the example page and then attempt to verify the text within its Show Message and Block UI logic. One test will succeed, and the next test will fail.

See the emphasized lines below, beginning at line 56, for this logic:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
// Require modules used in the logic below
const {Builder, By, Key, until} = require('selenium-webdriver');

// You can use a remote Selenium Hub, but we are not doing that here
require('chromedriver');
const driver = new Builder()
    .forBrowser('chrome')
    .build();

// Get environment variables for Skuid site credentials
const baseUrl = process.env.SKUID_HOST
const username = process.env.SKUID_UN
const password = process.env.SKUID_PW

// Logic to login to a Skuid site using the above credentials
// Note: This function does not check if already logged in. 
var login = async function() {
    // Define login elements
    let loginContainer = By.css('.public-main-wrapper');
    let inpUsername = By.xpath('//*[@id="username-input-field"]//input');
    let inpPassword = By.xpath('//*[@id="password-input-field"]//input');
    let btnLoginWithoutSso = By.id('login-without-sso-button');
    let btnLogin = By.id('userpass-login-button');
    var enterCredentialsAndLogin = async function() {
        console.log('Entering credentials...')
        // Wait until an input element appears
        await driver.wait(until.elementLocated(inpUsername), 10 * 1000);
        // Enter credentials and log in
        await driver.findElement(inpUsername).sendKeys(username);
        await driver.findElement(inpPassword).sendKeys(password);
        await driver.findElement(btnLogin).click();
    }

    // Load the login page
    await driver.get(baseUrl + '/ui/login');

    // Wait until the page is loaded
    await driver.wait(until.elementLocated(loginContainer), 10 * 1000);
    console.log('Login screen loaded.')

    // Check to see if SSO has been enabled for this site.
    // If it has, click through to log in with credentials instead.
    await driver.findElement(btnLoginWithoutSso).then(async function(){
        console.log ('SSO enabled. Logging in with username and password instead.')
        await driver.findElement(btnLoginWithoutSso).click()
        enterCredentialsAndLogin()
    }, async function(){
        await enterCredentialsAndLogin()
    })

    // Wait to be logged in, assuming it was was successful 
    // once the Log in button has gone "stale."
    await driver.wait(until.stalenessOf(driver.findElement(btnLogin)));
    console.log('Logged in.')
}
// Configure Jasmine's timeout value to account for longer tests.
// Adjust this value if you find our tests failing due to timeouts.
jasmine.DEFAULT_TIMEOUT_INTERVAL = 20 * 1000;

// Define a category of tests using test framework, in this case Jasmine
describe("Basic element tests", function() {
    // Before every test, open a browser and login
    // using the logic written above.
    beforeEach(async function() {
        await login();
        console.log('Test beginning.')
    });
    // After each test, close the browser.
    afterAll(async function() {
        await driver.quit();
    });

    // Specify a test
    it("Click the button, Verify Correct UI Block Message", async function() {
        // Provide basic data used to evaluate the test.
        // This test should pass.
        var testData = {
            pageName: 'SeleniumTest',
            button: By.css('#test-button'),
            blockMessage: By.css('div.blockUI.blockMsg')
        }
        console.log('Running test...')

        // Preview the test page
        await driver.get(baseUrl + '/ui/page/preview/' + testData.pageName);

        // Wait for button
        await driver.wait(until.elementLocated(testData.button), 10 * 1000);

        // Verify button is present
        expect(await driver.findElement(testData.button).isDisplayed()).toBe(true);

        // Click button
        await driver.findElement(testData.button).click();

        // Wait for the blocked UI message to appear
        await driver.wait(until.elementLocated(testData.blockMessage), 10 * 1000);

        // Verify the text of the message, which should match the example page XML
        expect(await driver.findElement(testData.blockMessage).getText()).toBe('The button renders and is clickable.');
    });

    // Specify a second test
    it("Click the button, Verify Incorrect UI Block Message", async function() {
        // Provide basic data used to evaluate the test.
        // This test should fail.
        var testData = {
            pageName: 'SeleniumTest',
            button: By.css('#test-button'),
            blockMessage: By.css('div.blockUI.blockMsg')
        }

        // Preview the test page
        await driver.get(baseUrl + '/ui/page/preview/' + testData.pageName);

        // Wait for button
        await driver.wait(until.elementLocated(testData.button), 10 * 1000);

        // Verify button is present
        expect(await driver.findElement(testData.button).isDisplayed()).toBe(true);

        // Click button
        await driver.findElement(testData.button).click();

        // Wait for and Verify Correct UI Block Message
        await driver.wait(until.elementLocated(testData.blockMessage), 10 * 1000);

        // Verify the text of the message, which should *not* match the example page XML
        expect(await driver.findElement(testData.blockMessage).getText()).toBe('These aren\'t the Droids you\'re looking for..');
    });
});

With the code samples of the last two sections combined into one file, you have a complete test suite for a basic Skuid page.

Now run these tests locally to see how they pass or fail.

Running tests locally

First, environment variables must be set. Hardcoding credentials within scripts is not recommended for security purposes. Enter these credentials within the terminal, or source them from a file, before running the test:

export SKUID_UN=username
export SKUID_PW=password
export SKUID_HOST=https://example.skuidsite.com

# Or if stored within a file
source /path/to/skuid-creds/.site-credentials

With those variables set, initiate the test.

In the command line:

  1. Navigate to the directory containing the test file, example-spec.js.

  2. Initiate the test using the jasmine command:

    jasmine example-spec.js
    

After running this command, you should see the following:

  1. A Chrome window appears, navigating to the login page of the Skuid site.
  2. Selenium enters the credentials set within the environment variables.
  3. Selenium then clicks the login button.
  4. Next, Selenium performs both tests:
    • Click the button, Verify Correct UI Block Message
      • Selenium opens the SeleniumTest page within the site.
      • Selenium clicks the #test-button element.
      • Selenium reads the block UI message.
      • Selenium determines that the message within that div does match the expected value.
      • The test passes.
    • Click the button, Verify Incorrect UI Block Message
      • Selenium opens the SeleniumTest page within the site.
      • Selenium clicks the #test-button element.
      • Selenium reads the block UI message.
      • Selenium determines that the message within that div does not match the expected value.
      • The test fails.

The command line then reads out the results, noting which tests passed and which failed. And with that, you’ve successfully run a basic automated test!