Previously, we went over how to get set up with WebdriverIO. With that in place, let’s write a straightforward test: use the search widget to get from the blog index page to the about page on this site. This will be a self-contained test file; in the next post we’ll peel apart the test logic and the page interaction logic, which is a cleaner setup for larger testing codebases and for accommodating potential page changes.
Step 1: What are the steps?
In the simplest view there are 3 steps to this test:
- Open the blog page
- Type “about” into the search bar and submit
- Find and click on the About page
To translate this into a test, though, we need something we can check after each action. Let’s break it down more.
- Open the blog page and check the title
- Type “about” into the search bar and submit; check title of resulting page
- Scan entries of results page and check the about page is among them
- Click the about entry and check title of resulting page
From experience, I would also insert an entry in second place of taking action (potentially) to make sure the search bar is visible/usable – on this site it is visible but scrolled out of view, and on Android devices I have historically had problems in that situation. I also plan to adapt this test to a second site (in a later post) where the search is not immediately visible.
Step 2: Convert steps to tests
The vocabulary in automated testing can be a hindrance to communication. Strictly speaking, a “test” is a little piece contained in an “it” (or synonymously, “specify”) statement, and ought to be one assertion preceded by as little activity as possible to make that assertion true. You can put more than one assertion into an “it” statement, but if it’s a separate thing to check, it is clearer to make it a separate test.
Tests are bundled up into “test suites,” which are contained in “describe” (or “context”) statements; suites can be nested for organization.
Here is a simple beginning to the test code:
describe('Finding About from Blog via Search', function() { it('should open the blog page', function() { browser.url('https://www.rweber.net/blog/'); expect(browser.getTitle()).to.equal("Blog - rweber.net"); }); it('should find the search bar', function() { // to be written expect(browser.isVisible('input.search-field')).to.be.true; }); it('should search for about', function() { browser.setValue('input.search-field', 'about'); browser.click('input.search-submit'); expect(browser.getTitle()).to.contain('You searched for about'); }); it('should see About in the search results', function() { var resultTitles = browser.getText('article h2'); expect(resultTitles).to.contain("About Rebecca"); }); it('should reach the About page', function() { browser.click('a=About Rebecca'); expect(browser.getTitle()).to.equal("About Rebecca - rweber.net"); }); });
The WebdriverIO API reference includes many commands you can use to interact with pages. I won’t go through them here, but I have notes on a few. Whenever you are specifying an element of the page you’ll most commonly use CSS selectors, but there are two other useful options. In the final test above I’ve selected by text: a=About Rebecca
selects the anchor link(s) containing the text “About Rebecca”. You can also use XPath selectors, about which more in a later post.
WebdriverIO has both setValue and addValue, and they are not interchangeable – addValue does just that, appending to any existing content in the form field, and setValue overwrites whatever may have been there before. Likewise, $ and $$ are not interchangeable. If you use $ with a selector that matches more than one element in the page, it will fail.
For everything chained to expect
, you’ll need the Chai BDD API reference. The only potential gotcha I’ve found there is that “true” is literally true, not truthy. To check for truthy values use “ok”.
Step 3: Debug
A lot of these tips are beyond the scope of this example, but all are drawn from my experience.
If your test doesn’t work the first time, the first thing to check is that all selectors are spelled, capitalized and punctuated exactly as they are in the page, and that they are specific enough not to be capturing unintended elements. Matching on text also requires care with capitalization – JavaScript running on the page reads the source capitalization, but Selenium reads the displayed capitalization, including any changes due to CSS. You may wish to preemptively lowercase all the text you read before doing comparisons.
As you get into more complicated tests you’ll also have to consider whether the actions are overwriting each other and need to be reordered or some actions duplicated (e.g. drop-down selection A is used in assertion B but unset by action C in between).
Documentation for certain commands is sparse and experimentation may be necessary to work out the required types/formats for inputs and the nature of the return values. I may or may not ever write a post on WebElements, but they were a key instance of this. If you are having difficulty figuring out the state of things when errors occur, I found the easiest way to spill information was via custom error messages – in a sense, the automated testing equivalent of console.log. The debug interface was too flighty for me. Just note that what you pass to “expect” as the second input must be a string; passing something that has to be evaluated will give an error, regardless of what it evaluates to.
Two other quick tips: it’s best to use “true” in your assertions when you can, but if you can’t figure out what is wrong try “ok” to rule out a value being merely truthy when you thought it was actually true. Finally, if you can’t decipher the error message, try a different browser (in particular, although I dislike chromedriver’s need to bring the window to the top repeatedly while running a test, it gives better error messages). In one case Chrome gave me the message “Error: An element command could not be completed because the element is not visible on the page” when Firefox was saying “Error: Remote end sent an unknown status code.” Exactly one of those messages was helpful.
Step 4: Fill in blanks and add details; debug again
For screenshots, add browser.saveScreenshot("path/to/filename.png");
lines where needed. For both that and the missing actions to find the search field, I like to use information about the platform/browser being tested on – my tests are typically just as much to make sure the looks are correct on different browsers as to make sure the functionality is correct, so keeping screenshots separate per device is important.
For the search field I just want to target Android devices and scroll down on them. Full disclosure: I do not know why the following works, but it cleared some errors related to out-of-viewport selection for me, so I present it without warranty:
function isAndroid() { // various places this has shown up for me in capabilities code var bN = browser.desiredCapabilities.browserName, pN = browser.desiredCapabilities.platformName, justP = browser.desiredCapabilities.platform; if ((bN && bN.toLowerCase() === "android") || (pN && pN.toLowerCase() === "android") || (justP && justP.toLowerCase() === "android")) { return true; } else { return false; } }
With that above your test suite code, the search bar test can be filled out:
it('should find the search bar', function() { if (isAndroid()) { browser.execute("mobile: scroll", {direction: 'down'}); } expect(browser.isVisible('input.search-field')).to.be.true; });
That scrolling syntax came from StackOverflow; I don’t know why it helps to scroll with no destination but… it did for me. On Android, which was the only platform that had problems in the first place.
That’s it for this version! The full code for the test can be found on GitHub as all-in-one.js, plus a jazzed-up version called all-in-one-extended.js.
Calculator robot by DirtyOpi on Pixabay.