Testing React Apps with Selenium
  March 26, 2019

When I was young, I loved to play browser-based games. Several of them were essentially HTML text adventures -- you would navigate by clicking links or images, then the page would reload with the results of whatever action you took.

Click.

Page goes white.

Images slowly load from top to bottom.

Click.

White page.

Images load.

Click.

White.

Sigh.

Fidget while images load.

It was fun at the time, but it's a downright painful and frustrating experience by modern web standards. There's no need to load a new page on every action now. ReactJS is one of the many technologies that allow us to move away from tedious non-interactive pages that have to reload to display new content.

So imagine this: you've decided to update your site with some neat features from the React library. Maybe you've added a live search that updates with every character typed or instant input checking on your forms so users can quickly correct invalid data before submission. Everything now 'reacts' to what the user is doing, the site flows better, and your manual testers love it!

All you have left to do is run automated Selenium tests to make sure your changes work as expected across different browsers. The results look good initially -- all your desktop browsers seem to be passing. But then the mobile tests begin to finish and you see that they're failing tests for simple things like filling in forms. Investigation time.

You watch the video recording of the test and it's like the site isn't reacting to any input. Maybe you grab a phone or simulator and go through the steps in the failed tests manually to see what happened. To your surprise, it works perfectly when conducted manually.

To illustrate this, I'll show the results of running a Selenium test to a site with a search bar that uses React to update the results after each keystroke and display all Pokemon with matching names (the repo for this site is here). Here's the important part of the test:

search_box = self.driver.find_element_by_xpath('//*[@id="root"]/div/div/input')

search_box.send_keys('vaporeon')

result = self.driver.find_element_by_css_selector('#root > div > ul > li > div > p')

self.assertEqual(result.text, 'Vaporeon'

I enter “vaporeon” into the search box, then check that page automatically updates the results so that the name of the first result is "Vaporeon.”

The test runs fine on Windows 10 Chrome 72 (note that I've added waits and sent each letter of the word individually to make the live search easier to see -- this applies to later examples as well).

However, it doesn't behave correctly on iOS:

What's going on? You search and search until you stumble on an open issue in the Appium repo. Basically, Appium fills input fields in a way that React doesn't detect. Therefore, it won't...react. And the issue is still open -- no fix is in place yet. How are you going to test your site now?

It will take forever to run all required tests manually on mobile, especially since you can't do those in parallel unless you get help from someone else. But it doesn't look like you have too many options...you either have to use something other than Selenium (which would require re-writing all of your tests), test mobiles manually, or stop using React on your site. Right?

Thankfully there's another option that allows you to run automated tests on mobile for sites using React. This comment by gvozdeva-ira from the issue page provides the code for the workaround. But what exactly is it doing?

webFile = requests.get('https://unpkg.com/react-trigger-change/dist/react-trigger-change.js')

self.driver.execute_script(webFile.text)

self.driver.find_element(*self._search_input_locator).send_keys('some text')

search_input = self.driver.find_element(*self._search_input_locator)

self.driver.execute_script("reactTriggerChange(arguments[0]);", search_input)

The first line grabs a js file from this npm package. This file has functions from the React test utilities that allow one to simulate/force an onChange event. As a side note, it is also possible to make the React test utilities available on your test site so you can directly call them with a script execute command via Selenium (there is information on that within the Github issue page linked above). However, bundling those debugging functions into a production site is not advised.

Once the file is downloaded, the second line injects those functions into the browser/page that your selenium session has loaded, which makes `reactTriggerChange()` available for use during that session. The next lines send text to an element, save the locator for that element to a variable, and then run `reactTriggerChange()` on that element in order to force React to recognize that a change has occurred.

Let's see how this works when added to our example test:

webFile = requests.get('https://unpkg.com/react-trigger-change/dist/react-trigger-change.js')

self.driver.execute_script(webFile.text)

search_box = self.driver.find_element_by_xpath('//*[@id="root"]/div/div/input')

search_box.send_keys('vaporeon')

self.driver.execute_script("reactTriggerChange(arguments[0]);", search_box)

result = self.driver.find_element_by_css_selector('#root > div > ul > li > div > p')

self.assertEqual(result.text, 'Vaporeon')

Note that I used a modified version of this code to make it easier to see what's going on -- I added some waits and sent each letter individually. I also sent self.driver.execute_script("reactTriggerChange(arguments[0]);", search_box) after each letter to force an update each time.

And it works as intended! Once again, this method doesn't require making the testing utilities available as part of the site, nor any other modifications to the site itself, so it can be used against production environments, too.

You can also save the react-trigger-change.js file so that you don't end up requesting it every time you run a test. Here's a quick js (Nightwatch, specifically) example of how you'd implement this if you saved the file as `helper_script.js`

var fs = require('fs');

var script = fs.readFileSync('helper_script.js', encoding='utf-8');

this.toDos = function(browser) {

//Code to access your site and anything else you want to do before inputting text

browser.sendKeys('.selector-for-desired-element', 'Text you want to enter');

browser.execute(`(function registerChange(){

reactTriggerChange(document.querySelector('.selector-for-desired-element'));

})()`);

//Code for the rest of your test

}

Thanks to GitHub user gvozdeva-ira for their comment on how to use react-trigger-change.js in a Selenium test, GitHub user vitalyq for creating the repo/package that contains the js file used, and GitHub user alik0211 for the Pokedex site that the example tests were run to.