Chrome DevTools as Automation Protocol

Over the last few years a lot of new testing frameworks have evolved that provide different capabilities but also come with certain limitations. While tools like Selenium are often seen as slow and flaky and others like Cypress.io or Puppeteer are hyped as the future in the testing space. In reality all tools have their own use cases, have different levels of support and are based on a completely different architecture. Comparing them is rather useless as it ends up comparing apples with oranges. In larger test suites flakiness and race conditions can happen using all of these tools so that we can safely say that up to this point none of them (including WebdriverIO) has found the ultimate solution for world /(domin|autom)ation/.

At the end of the day there are more or less two different approaches to how you can automate a browser. One is using an official W3C web standard called WebDriver and the other is the native browser interface that some of the browsers expose these days. The WebDriver protocol is the de-facto standard automation technique that allows you to not only automate all desktop browsers but also run automation on arbitrary user agents that include mobile devices, desktop applications or even Smart TVs. If you don't use WebDriver your automation framework most likely uses the native browser interfaces to run its automation on. While in the past every browser had its own (often not documented) protocol, these days a lot of browsers, including Chrome, Edge and soon even Firefox, come with a somewhat unified interface that is close to what is called the Chrome DevTools Protocol. While WebDriver provides true cross-browser support and allows you to run tests on a large scale in the cloud using vendors like Sauce Labs, native browser interfaces often allow many more automation capabilities like listening and interacting with network or DOM events while often being limited to a single browser only.

With the release of WebdriverIO v5.13 we now introduce a new option that allows you to specify the automation protocol for your test and leverage the capabilities of both worlds. With that you can now decide whether to run your tests using WebDriver or Chrome DevTools (via Puppeteer). Nothing actually changes for your tests, just the automation happens using different technologies. We've created a new NPM package that we call devtools which has all WebDriver commands implemented but executes them using Puppeteer. It is a new type of plugin that even allows you to build your own WebDriver based automation package to automate an arbitrary device with WebdriverIO. The new option, called automationProtocol, expects a string and is by default set to "webdriver" to run automation using WebdriverIOs own webdriver bindings. However if you install the new devtools package via:

$ npm install --save-dev devtools webdriverio

and set automationProtocol: 'devtools' in your options, all the automation happens via Chrome DevTools (more specifically via Puppeteer) while using the same WebdriverIO command interface:

const { remote } = require('webdriverio')
let client;
(async function () {
client = await remote({
automationProtocol: 'devtools', // CHANGE PROTOCOL HERE
capabilities: { browserName: 'chrome' } // or `browserName: 'firefox'`
})
await client.url('https://webdriver.io')
console.log(await client.getTitle())
await client.deleteSession()
})().catch(async (e) => {
console.error(e.stack)
await client.deleteSession()
})

This is especially great for local testing since you are no longer required to download a browser driver which often causes confusion as to where to download and how to run them. In addition to that, tests will run much quicker since WebdriverIO is directly connected to the browser.

As mentioned before, you can now leverage the advantages of running true cross-browser tests at scale with WebDriver, as well as leveraging all the additional automation capabilities of DevTools in one single tool. That said, given the fact that it is not possible to switch the protocols during a session, you might want to have one set of tests that require Puppeteer involvement, while also having a second set that you run cross-browser in the cloud.

In order to access Puppeteer in your tests, we have introduced a command called getPuppeteer(), which returns the browser class of the Puppeteer framework. From there on you can access the Puppeteer interface and all of its other classes. Note that these interfaces provide commands that are soley promise-based and WebdriverIO does not wrap them as you might know it using the WDIO testrunner. In this case we recommend wrapping all Puppeteer command calls manually within the call command that ensures that all promises are resolved before it moves on with other commands. For example, the following script shows how you can use WebdriverIO using the devtools automation protocol via Puppeteer in the WDIO testrunner world:

describe('my e2e tests', () => {
// ...
it('replaces the WebdriverIO logo with the Puppeteer logo', () => {
browser.url('https://webdriver.io')
/**
* run Puppeteer code with promises to intercept network requests
* and replace the WebdriverIO logo in the docs with the Puppeteer logo
*/
browser.call(async () => {
const puppeteerBrowser = browser.getPuppeteer()
const page = (await puppeteerBrowser.pages())[0]
await page.setRequestInterception(true)
page.on('request', interceptedRequest => {
if (interceptedRequest.url().endsWith('webdriverio.png')) {
return interceptedRequest.continue({
url: 'https://user-images.githubusercontent.com/10379601/29446482-04f7036a-841f-11e7-9872-91d1fc2ea683.png'
})
}
interceptedRequest.continue()
})
})
// continue with sync WebdriverIO commands
browser.refresh()
browser.pause(2000)
})
// ...
})

We also made sure that you can use the devtools automation protocol with WDIO testrunner services like @wdio/devtools-service to make the experience as seamless as possible. You can find the complete script example to run in standalone mode in the WebdriverIO example directory.

We recommend only running tests on the devtools protocol if your functional test requires some sort of automation capability that is not provided by WebDriver. It also makes sense to switch to devtools whenever running tests locally as the test execution will be much faster. If you follow our best practices you should split up your wdio config files per environment (e.g. local testing vs. run tests against Sauce Labs or in your grid). You can now have a config that defines a set of tests that require some Puppeteer interaction:

// wdio.devtools.conf.js
const { config } = require('./wdio.conf.js')
exports.config = Object.assign(config, {
/**
* set automation protocol to devtools
*/
automationProtocol: 'devtools',
/**
* define a set of tests that require Puppeteer involvement
*/
specs: [
'tests/e2e/devtools/**'
],
/**
* run tests only on Firefox and Chrome in headless mode
*/
capabilities: [{
browserName: 'chrome',
'goog:chromeOptions': {
headless: true
}
}, {
browserName: 'firefox',
'moz:firefoxOptions': {
headless: true
}
}]
// other specific options ...
})

With the new automationProtocol option, we've opened the project up to automation technologies beyond WebDriver. There are advantages and disadvantages using both approaches and with this feature we allow you to use both in one single tool. The devtools package is still work in progress and we are finalising all of its features within the upcoming weeks. If you have any feedback, bugs or comments on this please reach out via Twitter or on our Gitter support chat.

Thanks!

WebdriverIO V5 now also supports CucumberJS

We are pleased to announce that we now have support for CucumberJS in WebdriverIO V5!!!! This has been a great challenge for the project committers so we're all very thankful for the time and effort they put into this. So normally we would say

npm install @wdio/cucumber-framework --save-dev

and go with the flow, but this time it is different. When you upgrade to the latest version of the Cucumber framework you also need to upgrade to the latest version of WebdriverIO. In this blog post we want to give you some guidelines on how to do that.

Where to start#

Upgrading to the latest version of the Cucumber framework isn't that simple, because as said, you also need to migrate to version 5 of WebdriverIO. To understand what you need to do we have created some steps you might want to follow which will make the migration a lot easier for you. But before we explain the steps, you first need to understand the differences between WebdriverIO V4 and WebdriverIO V5.

NodeJS support#

WebdriverIO now needs NodeJS 8 or higher. Be aware that NodeJS 8 will end it's LTS support at the end of 2019, so upgrading to 10 would be better. If you want to know more about the NodeJS LTS support check this. This might help you convince your colleagues / DEVOPS engineers to upgrade you NodeJS instances.

As a side note, if you want to know what is supported by NodeJS by default you can check node.green and follow all upcoming changes.

W3C support#

WebdriverIO is now fully supporting the W3C protocol, this has a lot of advantages, but for your existing scripts some minor downsides. One of the downsides might be that you are using methods that are based on the JSONWire Protocol, that are not supported by the newest drivers like for example ChromeDriver 74+. This might result in errors like for example browser.positionClick() is not a function. If you see this error you are using a not supported method for the W3C supported Driver. See the API documentation here to see which command is a WebDriver Protocol (W3C) or a JSONWire protocol command.

Just a little side note, we tried to keep all browser and element commands, see the link above, agnostic to the protocol. Nothing changed here for you. To provide you some insight on how this works please check for example the keys command, you will find the support for both protocols here.

If you want to use W3C with cloud vendors, like for example Sauce Labs or Browserstack, you need to use a vendor specific prefix in your capabilities. Please check the websites of the vendors to see what you need to do.

But, you always need to end with the advantages. So, with W3C you will now see that the browsers follow a common web standard for web automation. This helps WebdriverIO to build a reliable framework on top of it. And last but not least, with W3C support from all browser vendors we now also get better support for Appium, check the latest post of Apple here. So, W3C is a major step for us all!!

Command changes#

Over the years WebdriverIO added more and more commands for different automation protocols without applying a pattern to it which resulted in having a bunch of duplication and inconsistent naming. Even though the list looks exhausting, most of the commands that have changed were used internally. Please check the changelog of V5 to see all the changes.

Breaking changes#

When creating a better product and thus releasing a major version, you will always have breaking changes. We can't repeat it enough but please check the changelog of V5 to see all the breaking changes.

Don't only read the changes in the V5.0.0 release, but also read the rest of the changes!

It's not only WebdriverIO who evolved!#

When you are going to use the lastest version of the @wdio/cucumber-framework, you'll also get the latest version of Cucumber. This means you also need to look at the breaking changes between CucumberJS 2 and CucumberJS 5. Please check the changelog of CucumberJS from version 3 till 5 to see what changed in CucumberJS.

Migration steps#

So enough about the differences between V4 and V5, please follow the steps below to make the migration a little bit smoother. The idea behind these steps is to migrate with small baby steps. The advantage of doing it in small baby steps is that you also have some time to look at your code again and maybe refactor it or remove duplicate and ugly not needed code.

1. Start with a clean project#

We advise you to create a fresh new project which you can easily copy to your old project and migrate 1 feature file and it's steps per scenario. When you do this you can easily disable scenario's in your old project, and run the new migrated tests in the new project, maybe even embed it in your pipeline.

Before installing dependencies, we need to initialize an empty NPM project (this will allow us to the cli to install needed dependencies to our local project). To do this, run:

$ mkdir webdriverio-test && cd webdriverio-test
$ npm init -y

The -y will answer 'yes' to all the prompts, giving us a standard NPM project. Feel free to omit the -y if you'd like to specify your own project details.

2. Install WebdriverIO CLI#

We recommend using the test runner because it comes with a lot of useful features that makes your life easier. With WebdriverIO v5 and up, the testrunner has moved into the @wdio/cli NPM package.

Now we need to install the cli. Do that by running:

$ npm i --save-dev @wdio/cli

3. Generate Configuration File#

We'll next want to generate a configuration file that stores all of our WebdriverIO settings. To do that just run the configuration utility:

$ npx wdio config

A question interface pops up. It will help to create the config easy and fast and install all needed dependencies. Check the file and read the comments, some things changed so reading them might help you understand what changed in the configuration file.

NOTE: If you were using a compiler in your cucumberOpts you need to be aware of the fact that CucumberJS removed the compiler. This means that WebdriverIO can't provide you with this option, but, as you might expect from our hard working contributors, there is a different solution. Please check Babel or TypeScript for the new way of using a compiler.

4. Create the same folder structure#

Now that everything has been set up, it's best to create the same folder structure you now have in your project.

DON'T COPY THE FILE, ONLY THE FOLDER STRUCTURE

5. Migrate feature file per feature file#

When you have the folder structure, copy 1 feature file to the new project. Start with the easiest file and if you have more than 1 scenario in it, comment out all scenario's and leave 1 active. Now make sure you migrate the steps that belong to that specific scenario, including all pageobjects that belong to the implementation, to the new project. Keep in mind that there are breaking changes in WebdriverIO in selecting elements and so on, see above. If you were using the defineSupportCode from CucumberJS, please check the CucumberJS changelog for V4. That is deprecated now.

Do this for each scenario, migrate it step by step, if you face issues, fix them and proceed. And don't forget to clean up the coding mess you, or your colleagues, might have made in the past.

6. When you're done#

Because you created a clean project you can now easily do the following:

  1. Remove all WebdriverIO V4 dependencies in your old project.
  2. Copy all dependencies from the new project to the old project.
  3. Remove all test related files.
  4. Copy all new test related files to your project.

And you're done, time to party.

Support#

If you need support you can find us on the WebdriverIO- Gitter channel by clicking on this link Gitter chat. When you ask for support we only have 1 question for you, please provide us a detailed description of your issue, what you already did and so on. Otherwise you ask us to find a needle in a haystack and trust me, that will be very hard for us.

Happy testing!

Grtz,

The Blue Guy

File Uploads with WebdriverIO

Testing an upload scenario in the browser is a rare but not uncommon case in the automation testing space. It is always important to evaluate the importance of such a test because in many situations you end up testing the browser more than your application. So always keep in mind how much additional functionality your frontend application puts on top of the default upload behavior of the browser. If for example most of the magic happens in the backend it makes much more sense to mimik an upload using a simple Node.js POST request using packages like request or axios.

Find and expose file inputs#

Let's say our frontend app does a lot of things on top of just uploading a file (e.g. validation or some other frontend side manipulation of the file that is about to be uploaded). Now the first thing we should do is to find the input elements from type file. Be aware that apps build in React, Angular or other frameworks often hide these elements as there are hard to style using pure CSS. Therefor they hide the elements and mimik the input with a div or other more styleable HTML tags.

// Two hidden input elements, 1 for single file upload 1 for multiple.
<input class="upload-data-file-input hidden" type="file">
<input class="upload-data-file-input-multiple hidden" type="file">

In order to become capable to modify the value of this element we need to make it visible. The WebDriver spec defines input elements to be interactable in order to change their value. So let's do that:

/**
* The css class name "upload-data-file-input hidden" is just an example
* and you can replace with your app.
*/
const fileUpload = $('.upload-data-file-input');
browser.execute(
// assign style to elem in the browser
(el) => el.style.display = 'block',
// pass in element so we don't need to query it again in the browser
fileUpload
);
fileUpload.waitForDisplayed();

With the execute we can simply modify the element properties to either remove the hidden class or give the element displayedness.

Uploading the file#

Unfortunately the mechansim to upload a file with a browser highly depends on your test setup. At the end of the day the browser needs to be able to access the file that you want tp upload. For the local scenario it is super simple. Since you run the browser on your local machine and the file that you want to upload also exists on your local machine, all you need to do is to set the value of the file path to the input element:

/**
* it is recommended to always use the absolute path of the file as it ensures
* that it can be found by the browser.
*/
const path = require('path');
const filePath = path.join(__dirname, 'path/to/your/file');
fileUpload.setValue(filePath);

If you automate a browser that is running on a remote machine this approach won't work anymore because the file that is located locally (or wherever the tests are running) does not exist on the remote machine where the browser is running. For these scenarios the Selenium project created a file that is currently only supported when running Chrome or using a Selenium Grid with the Selenium standalone server. The command expects the file payload to be passed in as base64 string. Since this is quite inconvenient to use WebdriverIO has implemented an upload command that allows you to pass in just the file name and the framework takes care of parsing it properly. The upload example will now look like:

const path = require('path');
const filePath = path.join(__dirname, 'path/to/your/file');
const remoteFilePath = browser.uploadFile(filePath);
$('.upload-data-file-input').setValue(remoteFilePath);

Note that the remote file name is different from your local filename. Therefor you need to set the value based on the remote file name you get from the uploadFile command.

That was it!!! Happy Uploading 😉🙌🏻 ❤️