Skip to content

Latest commit

 

History

History
403 lines (318 loc) · 14.1 KB

File metadata and controls

403 lines (318 loc) · 14.1 KB

Going further with Nightwatch

Setup global launch URL

In test settings, we can define launch_url property so that we can define a base url globally.

You need to add "launchUrl": "https://google.com", in testing settings defined in nightwatch.json as follows

    "test_settings" : {
        "default" : {
            "launchUrl": "https://google.com",
            "desiredCapabilities": {
                "browserName": "chrome"
            }
        }
    }

Then you can use the launchUrl in test and page objects. In test file you can use browser.launchUrl e.g.

module.exports = {
  'Demo test' : function (browser) {
    browser
      .url(browser.launchUrl)
      // ...
      .end();
  }
};

In page objects you can will update the url property of page object as function to dynamically compute page URL

i.e. in our searchPage.js we update url property as

url: function() {
    return this.api.launchUrl;
},

Explore different locate strategies

Nightwatch uses css selector as locate strategy by default but we can switch to xpath selector anytime between the test. To switch between xpath/css selectors, we can use functions [useXpath()](https://nightwatchjs.org/api/useXpath.html), [useCss()](https://nightwatchjs.org/api/useCss.html)

In test files you can use these as follows:

browser
    .useXpath() // every selector now must be xpath
    .click("//tr[@data-recordid]/span[text()='Search Text']")
    .useCss() // we're back to CSS now
    .setValue('input[type=text]', 'nightwatch')

Typically, we define selectors as elements in page objects and therefore, we may not need to use the above in most cases. In page objects, we can define locateStrategy per each element i.e. for search field in searchPage.js we change the selector searchField: 'input[type=text]' to following in case we want to use xpath

searchField: {
  selector: '//input[@type="text"]',
  locateStrategy: "xpath"
},

For elements you can also define other properties as mentioned in here

Explore page object sections

Based on the page layout and different visual section in a page, you can group related elements and commands as sections so that you can find elements relative to the section parent itself.

i.e. we add a section to interact with sub elements of the complete search field that includes search icon, input field and voice icon as follows. We add this in searchPage.js page object.

Also, note how we can use xpath for section selector and css for section elements' selectors.

sections: {
  inputSection: {
    selector: "//div[@class='SDkEP']",
    locateStrategy: 'xpath',
    elements: {
      searchIcon: '.iblpc',
      searchinput: 'input',
      speakIcon: '[aria-label="Search by voice"]'
    }
  }
},

We then need to refer to these section elements and commands as this.section.inputSection. e.g we update the searchAndSubmit command to use the section as follows

searchAndSubmit: function(searchString) {
    this.section.inputSection
        .waitForElementVisible('@searchinput')
        .setValue('@searchinput', searchString)
    return this.submitClick();
},

Chaining of commands

Note, how we are able to chain our nightwatch commands in our tests and/or page objects. i.e. when we first started, we refered the browser parameter (nightwatch instance) only once and performed all operations using . notation:

module.exports = {
  'Demo test Google' : function (browser) {
    browser
      .url('https://www.google.com')
      .waitForElementVisible('body')
      .setValue('input[type=text]', 'nightwatch')
      .waitForElementVisible('input[name=btnK]')
      .click('input[name=btnK]')
      .pause(1000)
      .assert.containsText('#main', 'Night Watch')
      .end();
  }
};

This is possible because most of the nightwatch commands return the nightwatch instance when used in tests.

The same chaining of commands is also possible in page objects like we did in our searchPage.js

searchAndSubmit: function(searchString) {
    return this
        .waitForElementVisible('@searchField')
        .setValue('@searchField', searchString)
        .waitForElementVisible('@searchButton', 1000)
        .click('@searchButton')
        .waitForElementNotPresent('@searchButton');
}

However, in case of page objects each nighwatch api returns a page object instance and not the whole nigthwatch instance.

Explore this object

What this means is that we can access section, elements, commands, props, nightwatch api of a page object within that page object as follows.

commands: [
  {
    searchAndSubmit: function(searchString) {
      this.section.inputSection
          .waitForElementVisible('@searchinput')
          .setValue('@searchinput', searchString)
      return this.submitClick();
    },

    submitClick: function() {
      return this
          .waitForElementVisible('@searchButton', 1000)
          .click('@searchButton')
          .waitForElementNotPresent('@searchButton');
    }
  }
]

In the above code observe that,

  • Through this.section.inputSection we can access a specific section in current page object.
  • You can access nightwatch api with this as used in this.waitForElementVisible('@searchButton', 1000) or this.section.inputSection.waitForElementVisible('@searchinput')
  • Note that not all nightwatch api commands are available through this in page object/section object commands as you can see here. However, you can use the rest by using this.api. This will then start to point towards nightwatch instance and expore all nightwatch commands
  • You can return this in your custom commands to allow for chaining the commands in test files, like you can see how the two commands are chained in FirstTest.js
searchResults
  .verifyResultsContain('Nightwatch')
  .readTextForFirst();
  • Note how the scope change for subsequent operations change when you use this, this.api, this.section.sectionName in page object commands
    • this will point to current page object and therefore allows page object operations to be chained
    • this.api will start to point to nightwatch instance and therefore expose all nightwatch api but the current page object elemetns/section/commands cannot be chained.
    • this.section.sectionName will start to point the section's object and therefore only section elements, commands, and limited nightwatch api commands can be accessed.

Understand different assertions

Nightwatch provides assertion APIs

Classical assertion library .assert/.verify

  • Can be used in nightwatch browser object and page/section objects.
  • Can be used as browser.assert.attributeContains('#someElement', 'href', 'google.com');
  • .assert will end the test if the assertion fails and will report test failure
  • .verify will continue with the test if the assertion fails and will report test failure
  • Returns nightwatch/page/section objects to allow for chaining like other commands

You can see all the available assertion here

BDD based assertion library .expect

  • Can be used in nightwatch browser object and page/section objects.
  • Can be used as browser.expect.element('#main').to.be.present;
  • Does not returns nightwatch/page/section objects and therefore cannot be chained.

Read more about it here Also, see all the expect type assertions here

Explore callbacks

Like, we saw that the nightwatch commands mostly returns nightwatch instance itself to allow for command chaining. Hence, the nightwatch api commands that are suppose to obtain and return some value from the browser, they provide the results through callbacks. e.g

To understand, we observed how the following did not print our desired text.

readTextForFirst: function() {
  var actualText;
  actualText = this.getText('@firstResult');
  console.log(actualText);  
}

Even the following where we used callback to set the value of a global did not print the desired value

readTextForFirst: function() {
  var actualText;
  this.getText('@firstResult', text => {
    actualText = text.value;
  });
  console.log(actualText);
}

We were able to use the text by using .perform utility function as follows

readTextForFirst: function() {
  var actualText;
  this.getText('@firstResult', text => {
    actualText = text.value;
  }).perform(() => {
    console.log(actualText);
  });
  return this;
}

Also, note that we cannot return the value from readTextForFirst itself. However, we can use callback functions to provide the value as callback from readTextForFirst. i.e.

readTextForFirst: function(cb) {
  this.getText('@firstResult', text => {
    cb(text.value);
  })
  return this;
}

Here and here you can understand more about perform .

Explore ES6 async await

Since v1.1 of nightwatch you can use async/await with most of the commands to get avoid the mess of callback and async behavior discussed above.

i.e. make your test/command as async and use await with the async command. You can update the above command as follows to directly get the text value.

readTextForFirst: async function() {
  const firstResultSelector = this.elements.firstResult.selector;
  var actualText = await this.api.getText(firstResultSelector)
  return actualText;
}

then you can update the test as follows to use the text syncronously within the same step

module.exports = {
  'Demo test' : async function (browser) {
    const searchPage = browser.page.searchPage();
    const searchResults = browser.page.searchResults();

    searchPage
      .navigate()
      .searchAndSubmit('nightwatch');

    const firstText = await searchResults
      .verifyResultsContain('Nightwatch')
      .readTextForFirst();

    console.log(firstText);

    browser
      .end();
  }
};

Note, this will output something like

{
  sessionId: 'b9bfd31e8fc143deefbff8f84f5f9133',
  status: 0,
  value: 'Nightwatch.js | Node.js powered End-to-End testing framework\nhttps://nightwatchjs.org\nWrite efficient end-to-end tests in Node.js and run them against W3C WebDriver.\nWhat is Nightwatch? · nightwatch.json · Nightwatch Runner · Using Mocha'
}

You can update the function as follows to return text value

readTextForFirst: async function() {
  const firstResultSelector = this.elements.firstResult.selector;
  var actualText = await this.api.getText(firstResultSelector, text => {
    return Promise.resolve(text.value)
  })
  return actualText;
}

Divide test in steps

You can divide the test in multiple steps as follows. Each step is executed syncronously

module.exports = {
  'Open Google and search for resutls' : async function (browser) {
    const searchPage = browser.page.searchPage();

    searchPage
      .navigate()
      .searchAndSubmit('nightwatch');
  },

  'Verify search results and get first text' : async function (browser) {
    const searchResults = browser.page.searchResults();

    const firstText = await searchResults
      .verifyResultsContain('Nightwatch')
      .readTextForFirst();

    console.log(firstText.value);

    browser
      .end();
  }
};

Make use of hooks

You can use before/after/beforeEach/afterEach hooks to pre and post requisites of tests (for more read here)

In the example below, we initialize the page object variables and set the window size in the before hook.

Then in beforeEach hook, we perform the navigation to search page

Then we have two test / test steps that search for nightwatch and selenium and perform respective assertions

Then we close the browser session in after hook.

let searchPage;
let searchResults;

module.exports = {

  before: (browser) => {
    searchPage = browser.page.searchPage();
    searchResults = browser.page.searchResults();
    browser.setWindowSize(1027,920);
  },

  beforeEach: () => {
    searchPage.navigate();
  },

  after: (browser) => {
    browser.end();
  },

  'Check search results for nightwatch' : async function (browser) {

    searchPage
      .searchAndSubmit('nightwatch');

    const firstText = await searchResults
      .verifyResultsContain('Nightwatch')
      .readTextForFirst();

    console.log(firstText.value);

  },

  'Check search results for something else' : async function (browser) {

    searchPage
      .searchAndSubmit('selenium');

    const firstText = await searchResults
      .verifyResultsContain('Selenium')
      .readTextForFirst();

    console.log(firstText.value);

  },
};

Further Reading Material