/ testing

Webdriver.io Quick Reference

Here's a quick and dirty webdriver.io reference!

Intro

Webdriver.io provides selenium bindings for Node. There's a few other solutions out there, but it's the best that I've used by a long shot. No promises or callbacks to deal with, everything is synchronous. Follow their quick start guide to get up and running.

I currently have tests for each page of the app I'm working on. If there's /blog/post then I have a file in tests/blog/post.js. Very straightforward and easy. Here's my config file in case anyone wants a quick setup:

let capabilities = [
  {
    maxInstances: 10,
    browserName: 'chrome'
  }
]
let services = ['selenium-standalone']
if (process.env.BROWSERSTACK_USERNAME) {
  services = null
  capabilities = [
    {
      browserName: 'Chrome',
      browser_version: '55.0',
      os: 'OS X',
      os_version: 'Sierra',
      resolution: '1920x1080',
      'browserstack.local': true
    },
    {
      'browserName': 'iPhone',
      'platform': 'MAC',
      'device': 'iPhone 6S',
      'browserstack.local': true
    },
    {
      'browserName': 'iPad',
      'platform': 'MAC',
      'device': 'iPad Pro',
      'browserstack.local': true
    },
    {
      'os': 'Windows',
      'os_version': '7',
      'browser': 'IE',
      'browser_version': '11.0',
      'resolution': '1024x768',
      'browserstack.local': true
    }
  ]
}

exports.config = {
  bail: 0,
  services,
  sync: true,
  capabilities,
  maxInstances: process.env.BROWSERSTACK_USERNAME ? 4 : 10,
  reporters: ['dot'],
  logLevel: 'silent',
  coloredLogs: true,
  framework: 'mocha',
  waitforTimeout: 25000,
  connectionRetryCount: 3,
  mochaOpts: {
    ui: 'bdd',
    timeout: 25000
  },
  screenshotPath: './errors',
  baseUrl: process.env.WD_BASE,
  connectionRetryTimeout: 90000,
  specs: [ './tests/e2e/**/*.js' ],
  user: process.env.BROWSERSTACK_USERNAME,
  key: process.env.BROWSERSTACK_KEY,

  beforeSuite: function (suite) {
    require('./init')
  }
}

selenium-standalone is a plugin (find it here) that will boot up selenium when you run your tests. No need to start it in a separate window. If the browserstack env is setup, we set four devices to run tests on. The rest is self explanatory. If you have any questions, leave a comment!

Setup file

I include an init.js file that allows me to setup babel and a few global variables. Here's what it looks like:

require('babel-register')({
  'presets': ['es2015'],
  'plugins': [
    ['transform-runtime', {
      'polyfill': false
    }]
  ]
})
require('./setup')

//setup.js

import { expect } from 'chai'
global.expect = expect
global.timeout = 25000

global.waitForUrl = urls => {
  browser.waitUntil(() => {
    if (typeof urls === 'string') return browser.getUrl() === process.env.WD_BASE + urls
    return urls.find(url => browser.getUrl() === process.env.WD_BASE + url)
  }, timeout)
}
global.url = url => process.env.WD_BASE + url

browser.timeouts('page load', timeout)
browser.timeouts('script', timeout)
browser.timeouts('implicit', timeout)

global.clearLocalStorage = () => browser.execute(() => window.localStorage.clear())

Let me quickly go over the few global functions I have.

waitForUrl is a great function, but in some cases I'm expecting one of two urls, so I made a helper that will let you wait for one of X urls to be navigated to.

url I prefix an env variable here because we run this codebase across many domains

The timeouts are high due to browserstack being slow sometimes.

clearLocalStorage is used to wipe the browser's storage. Webdriver.io has this built in but it will not work on iOS and various other devices, so I made this quick fix instead.

Run your tests

With your config files in place, add wdio wdio.js to your package.json and run the command! If you set the env variables for browserstack, they will run there. Otherwise, it will boot up selenium and run them locally.

Reference

Filling in forms

it('submits form and gets success', () => {
  browser.setValue('#name', 'Ken Wheeler')
  browser.setValue('#email', [email protected]')
  browser.setValue('#content', 'Your site sucks, go workout more.')

  browser.click('.ss-send')
  browser.waitForExist('.form_response')
  expect(browser.element('.form_response').getText()).to.equal('Message sent successfully')
})

The key here is waitForExist. It will wait until this element is in the DOM and then move on to the expect statement!

Check for array of elements

browser.click('.ss-send')
let expectedErrors = [
  'The name field is required.',
  'The email field is required.',
  'The content field is required.'
]

browser.waitForExist('.form_response', timeout)

let errors = browser.elements('.form_response')

errors.value.forEach((error, index) => (
  expect(error.getText())
    .to.equal(expectedErrors[index]))
)

Here, we click submit on a form, and assert for three validation errors when they pop up!

Check Page Title

it('loads with correct title', () => {
  expect(browser.getTitle())
    .to.equal('About')
})

Reload Page on each test

beforeEach(() => browser.url('/about'))

If you use mocha like me, you can add this at the top, just inside your describe block to reload the page on each it.

Elements that may not exist

Do you have a button on mobile to open a menu, but on a big screen, the menu is showing by default? This may seem self explanatory, but here's how I accomplish it.

it('goes to employers page', () => {
  try {
    browser.click('.external_nav_button')
  } catch (e) {
    // not on mobile :)
  }
  browser.click('a.-active')

  browser.waitUntil(() => (
    browser.getTitle() === `Employers`
  ))
})

Conclusion

There's a lot more you can do. Check out the official reference for more. I will periodically update this post with more things as I write more tests. I only started using it on Monday! Let me know if there's anything I can do better or feedback in general. Thanks for reading!!