9 things I wish I knew when learning to use Selenium Webdriver with Node.JS
Tags: node, selenium, javascript, testing
Published: 2023-12-18 01:35:50

A short while ago I was tasked with setting up automated testing on a website. I inherited some existing tests written in Java, but, being a web developer, I felt that the tests would be easier for me to write in JavaScript on Node.js.

This was for the most part true, but there were certain areas lacking in the [official Selenium Webdriver documentation](https://www.selenium.dev/documentation/webdriver) for JavaScript.

So, to save others the trouble of trying to convert Python or Java examples to JavaScript, here's some things that I learnt along the way. Note: I am using Cucumber.js as well.

> I'm using the npm packages [selenium-webdriver](https://www.npmjs.com/package/selenium-webdriver) and [Cucumber.js](https://www.npmjs.com/package/@cucumber/cucumber).

## Turning off Geo Location

This is set an an **option** in your browser settings before you run `build` against. For example, for FireFox:

```javascript
let options = new firefox.Options();
    options.setPreference('geo.enabled', false);
const driver = new Builder()
    .forBrowser('firefox')
    .setFirefoxOptions(options)
    .build();
```

## Dismissing alerts
The Selenium documentation gives the way to dismiss alerts in JavaScript as:

`await alert.dismiss();`

For the life of me, I could not get this to work. An alternative is to automatically dismiss alerts as a browser setting, like above:
```javascript
let options = new firefox.Options();
    options.setAlertBehavior('dismiss');
const driver = new Builder()
    .forBrowser('firefox')
    .setFirefoxOptions(options)
    .build();
```
 

## Getting clean text from an element
Retrieving text content is relatively easy with Selenium's `.getText()` method. However some browsers like Safari also grab a bunch of junk like line breaks and empty spaces. So it's better to clean this up with regular JavaScript, as such:
```javascript
var resultsCountText = await driver.findElement(By.className('search-results_count')).getText();
resultsCountText = resultsCountText.replace(/\n|\r|/g, "");
resultsCountText = resultsCountText.trim();
```

## Getting text from a hidden element
Sometimes you might just want to get text content from an element like a modal or mega-menu that is just not visible on the page. That's where `.getAttribute("innerText")` comes in handy.
```javascript
let modalContent = await driver.findElement(By.className('modal-body')).getAttribute("innerText");
```
Remember this may need cleaning up, as above.


## Repeating the same test for multiple pages
This can be achieved using Cucumber's `Scenario Outline`. Say, if I wanted to ensure the footer is present on a give list of pages, my `.feature` file might look like:

```yml
    Scenario Outline: Run a check to see if the footer is present
        Given I am on the '<page>' page
        Then the footer section should be present

    Examples: 
        | page           |
        | about-us       |
        | products       |
        | contact-us     |
        | store-locations|
```
The values in the above table are paths to the aforementioned pages. In my steps .js file I would have something like:
```javascript
Given('I am on the {string} page', async function (string) {
    this.driver = new Builder()
        .forBrowser('firefox')
        .build();
this.driver.wait(until.elementLocated(By.className('logo')));
    await this.driver.get('https://www.your-website.com/' + string);
});

Then('the footer section should be present', async function() {
  var footerContent = await webdriver.driver.findElement(By.css('footer')).getText();
  assert.ok(footerContent.length>1);
});
```

## Iterating through a series of elements
Say you have a group of `.card` elements and simply want to test if each has a title. `driver.findElements()` (note the **s**) returns a collection of elements which you can simply loop through. This works particularly well with [Cucumber/Gherkin datatables](https://www.baeldung.com/cucumber-data-tables).

```javascript
Then('the prod card titles should be', async function (dataTable) {
    var productCard = await this.driver.findElements(By.className('product-card'));
    var expectations = dataTable.hashes();
    for (let i = 0; i < expectations.length; i++) {
        const productName = await productElements[i].findElement(By.className('product-card_title')).getText();
    assert.equal(productName, expectations[i].productName);
    }
});
```
## Locating multiple elements that share the same class with Xpath
Xpath is a way of accessing specific pieces of content in an XML file. Selenium can make use of this way of locating data by converting the HTML page to an XML file. This is specifically handy when pulling data from HTML elements that all share the same class name.

Consider trying to obtain the price data from the following HTML:

```HTML
<div class="prod-card">
  <div class="prices">
    <div class="price-wrap">
      From: 
      <div class="price">$9.99</div>
    </div>
    <div class="price-wrap">
      To: 
      <div class="price">$19.99</div>
    </div>
  </div>
</div>
```
Obviously the issue here is there are no unique identifiers between the 2 `.price` elements.

However, we can use Selenium's Xpath to locate the first and second instance of `.price`, as such:

```javascript
let lowerPrice = await driver.findElement(By.xpath("(//div[@class='price'])[1]")).getText();
let higherPrice = await driver.findElement(By.xpath("(//div[@class='price'])[2]")).getText();

```

## Using different kinds of Explicit Waits when Implicit Waits just won't work
Happy to be proved wrong, but the [Implicit Waits as per the Selenium Documentation](https://www.selenium.dev/documentation/webdriver/waits/) just never worked for me. At all.

Instead, I had to get creative using Explicit Waits (`until.`).

For example, on a page whose content is altered with JavaScript (eg a Store Locator page that uses the Google Maps API) I had to wait until the text of an element changed, eg:
```javascript
await driver.wait(until.stalenessOf(driver.findElement(By.id('location'))));
await driver.wait(until.elementTextContains(driver.findElement(By.id('location')), 'New content!'));
```
Note I had to pair this with `until.stalenessOf()`. Instead of waiting a couple of sections for the JavaScript on the page to do its thing and alter the content on the DOM, I instead waited for a particular element's text content to change.

## The comments in the files in the `/lib` directory are very useful

For some reason, with the Selenium Webdriver npm package at least, there seem to be way more undocumented methods available. I found it actually incredibly useful to simply poke around the `/lib` directory of the `selenium-webdriver` package.

For example, the file `node_modules/selenium-webdriver/lib/until.js` lists a whole host of very useful Explicit Wait methods:

```javascript
module.exports = {
  elementTextMatches,
  elementTextContains,
  elementTextIs,
  elementIsNotSelected,
  elementIsSelected,
  elementIsDisabled,
  ableToSwitchToFrame,
  elementIsEnabled,
  elementIsNotVisible,
  elementIsVisible,
  stalenessOf,
  elementsLocated,
  elementLocated,
  urlMatches,
  urlContains,
  urlIs,
  titleMatches,
  titleContains,
  alertIsPresent,
  titleIs,
}
```

## Extra: Checking for broken links
I've written another tutorial for how to check for broken links [here](https://dev.to/adamdsherman/automated-testing-how-to-check-for-broken-links-using-selenium-webdriver-on-nodejs-1akf
).

---
> Cover photo by <a href="https://unsplash.com/@markusspiske?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Markus Spiske</a> on <a href="https://unsplash.com/photos/colorful-software-or-web-code-on-a-computer-monitor-Skf7HxARcoc?utm_content=creditCopyText&utm_medium=referral&utm_source=unsplash">Unsplash</a>