Automated Testing - WebDriverIO
◼️ What is WebDriverIO?
- Next-gen browser and mobile automation test framework for Node.js
◼️ Setup WDIO
- Initiate a WebdriverIO Setup form the root directory of an existing project directory or create a new project directory
//Run from existingnpm init wdio .//Creating new directory for the projectnpm init wdio ./path/to/new/project
- Select default configurations values for WDIO Configuration Helper during the setup.
- Run the test
npx wdio run ./wdio.conf.js
- Run the specific file
npx wdio run ./wdio.conf.js --spec example.e2e.js
- Run tests with desired logLevel details.
- Level of logging verbosity: trace | debug | info | warn | error | silent
npx wdio run ./wdio.conf.js --spec example.e2e.js --logLevel traceios.test.local.sim --spec login.spec.js --logLevel trace
◼️ Setting up Reporters
▪️ Dot Reporter
- Install the reporter as a dependency. for example, to install
dot
reporter:
npm install @wdio/dot-reporter --save-dev
- Add
'dot'
as reporter to thereporters
array in configuration file:
module.exports = {// ...reporters: ['dot'],// ...};
▪️ Allure Reporter
Setup
- Install dependency:
npm install @wdio/allure-reporter --save-dev
- Add reporter config to wdio configuration file:
reporters: ['dot',['allure', {outputDir: 'allure-results',disableWebdriverStepsReporting: true,disableWebdriverScreenshotsReporting: true,}]],
- Run the test & you'll get the an xml file with test results under folder named
allure-results
.
Generating & Displaying the Allure Report:
Install the Allure command-line tool, and process the results directory:
npm install -g allure-commandline --save-dev
Manual generation of the report:
Run the command. This will generate a folder named allure-results
in the project directory & also open an html version of the report by starting a web server for you.
allure generate [allure_output_dir] && allure openallure generate allure-results && allure open
If you run the report second time, you'll get an error as shown below:
Allure: Target directory /Users/username/folder-name/allure-report for the report is already in use, add a '--clean' option to overwrite
Resolve it by adding --clean option to overwrite the existing report.
allure generate allure-results --clean && allure open
You can add this command to package.json
.
"scripts": {"wdio": "wdio run ./config/wdio.conf.js","allure-report": "allure generate allure-results --clean && allure open"}
While running using the script command from package.json, if you get an error Error 404 - Not Found. No context on this server matched or handled this request......
Here is the reason:
Reason: Allure Report is SPA, that uses AJAX to get page content. Chrome prevents access to files on file system that causes the above error. (such checks can be disabled using --allow-file-access-from-files
option)
allure serve
command is shortcut for allure generate && allure open
therefore, you can also run the server as below and it will generate the report to a temp directory:
allure serve
Automatic generation of the report:
You can also auto generate the report by using the Allure command line tool programmatically. To do so, Make sure you already have allure-commandline
installed or install the package in your project by:
npm i allure-commandline
Then add or extend your onComplete
hook or create a custom service for this:
// wdio.conf.jsconst allure = require('allure-commandline')exports.config = {// ...onComplete: function() {const reportError = new Error('Could not generate Allure report')const generation = allure(['generate', 'allure-results', '--clean'])return new Promise((resolve, reject) => {const generationTimeout = setTimeout(() => reject(reportError),5000)generation.on('exit', function(exitCode) {clearTimeout(generationTimeout)if (exitCode !== 0) {return reject(reportError)}console.log('Allure report successfully generated')resolve()})})}// ...}
Using Allure API
const allureReporter = require('@wdio/allure-reporter').default
const LoginPage = require('../../pageobjects/login.page');const SecurePage = require('../../pageobjects/secure.page');const allureReporter = require('@wdio/allure-reporter').defaultdescribe('My Login application', () => {it('should login with valid credentials', async () => {allureReporter.addFeature('Login Feature')allureReporter.addStory('Login Story')await LoginPage.open();await LoginPage.login('tomsmith', 'SuperSecretPassword!');await expect(SecurePage.flashAlert).toBeExisting();await expect(SecurePage.flashAlert).toHaveTextContaining('You logged into a secure area!');});it('should logout from the site', async () => {allureReporter.addFeature('Logout Story')await SecurePage.logOut();await expect(SecurePage.flashAlert).toBeExisting();await expect(SecurePage.flashAlert).toHaveTextContaining('You logged out of the secure area!');});});
▪️ Video Reporter
npm install wdio-video-reporter
const video = require('wdio-video-reporter');
[video, {outputDir: './_video_results_/',saveAllVideos: true, // If true, also saves videos for successful test casesvideoSlowdownMultiplier: 3, // Higher to get slower videos, lower for faster videos [Value 1-100]}],
◼️ Creating Page Objects
//page.js/*** main page object containing all methods, selectors and functionality* that is shared across all page objects*/module.exports = class Page {/*** Opens a sub page of the page* @param path path of the sub page (e.g. /path/to/page.html)*/open(path) {return browser.url(`https://the-internet.herokuapp.com/${path}`)}}
//home.page.jsconst Page = require('./page');class HomePage extends Page {/*** define selectors using getter methods*/get pageHeading() {return $('.heading');}get pageSubheading() {return $('#content > h2')}get pageFooter() {return $('#page-footer')}async getPageHeading() {return await this.pageHeading.getText();}async getPageSubheading() {return await this.pageSubheading.getText();}open() {return browser.url(`https://the-internet.herokuapp.com/`)}}module.exports = new HomePage();
Notes:
- The
get
syntax binds an object property to a function that will be called when that property is looked up - The
$
command is a short way to call thefindElement
command in order to fetch a single element on the page. - The
$$
command is a short way to call thefindElements
command in order to fetch multiple elements on the page. It will return to you an array of elements that you can use to act on those elements as you see fit.
◼️ Writing Test
We are using Mocha syntax, so each test suite and test case is defined by a describe
and an it
describe("Interacting with elements", () => {it("should get the text for the element", async () => {allureReporter.addStory('Home Page Validation')await HomePage.open();let heading = await HomePage.getPageHeading();let subHeading = await HomePage.getPageSubheading();expect(heading).to.equal("Welcome to the-internet");expect(subHeading).to.equal("Available Examples");//Use below assertion when using custom version of Chai provided by WebDriverIO// await expect(HomePage.pageHeading).toHaveTextContaining("Welcome to the-internet")// await expect(HomePage.pageSubheading).toHaveTextContaining("Available Examples")})})
▪️ Mocha Hooks
- MochaJS provides a feature called "hooks" that allow you to run custom test code around your tests.
- Each
describe
block can have any or all of the following hooks implemented:- before
- beforeEach
- afterEach
- after
before
andafter
only run at the start/end of the test suite..beforeEach
andafterEach
, however, execute around each test.- Tou can add
.only
to theit
ordescribe
function call to run a single test (or single set of tests).
describe('A block for executing tests', () => {it('This test will be skipped', () => {});it.only('This test will run', () => {});describe.only('Only this Suite will run', () => {it('This test will run', () => {});it('This test will also run', () => {});});});
- You can add
.skip
to either thedescribe
orit
function call to skip a single suite or a single test.
describe('A block for executing tests', () => {it('This test will run', () => {});it.skip('This test will be skipped', () => {});describe.skip('This test will be Skipped', function () {it('This test will be skipped', () => {});it('This test will also be skipped', () => {});});});
- If there are multiple tests marked with
only
, all of them will run.
▪️ Grouping Tests by Suites
Grouping your tests by suites makes it easier for you to run a suite of tests versus running everything.
- Go into the “package.json” and under
scripts
section, add a script with your desired name. For example, to cretae a script optioninteracting-with-elements
with suite nameelements
"scripts": {"wdio": "npx wdio run wdio.conf.js","wdio-login-test": "npx wdio run ./wdio.conf.js --spec example.e2e.js","allure-report": "allure generate allure-results --clean && allure open","interacting-with-elements": "npx wdio run ./wdio.conf.js --suite elements"}
- Go into “wdio.conf.js” file and add a
suites
section
specs: ['./test/specs/**/*.js'],suites: {elements: ['./test/specs/elements/*.js'],login: ['./test/specs/login/*.js']},// Patterns to exclude.exclude: [// 'path/to/excluded/files'],
◼️ Using Data Files with Tests
- Create a folder named
data
- Create a file for data & export as a module
//login-data.jsmodule.exports = {userName: 'tomsmith',password: 'SuperSecretPassword!'}
- Import data file into test file
const loginData = require('../../../data/login-data');
- Using the datait('should login with valid credentials', async () => {allureReporter.addStory('Login Story')await LoginPage.open();await LoginPage.login(loginData.userName, loginData.password);await expect(SecurePage.flashAlert).toBeExisting();await expect(SecurePage.flashAlert).toHaveTextContaining('You logged into a secure area!');});
◼️ Using Environment Variables
- Create files with data for the values that will be passed using environment variable
//urls-data.jsmodule.exports = {"qa": "https://site-address.qa.com/","dev": "https://site-address.dev.com/","prod": "https://site-address.prod.com/"}
- Import the file in wdio.config.js
const urls = require('./data/urls-data');
- Use the ENV variable in the wdio.config.js
const ENV = process.env.ENVif (!ENV || !['qa', 'dev', 'prod'].includes(ENV)) {console.log('Please use the following format when running the test script: ENV=qa|dev|staging')process.exit()}exports.config = {...baseUrl: url[process.env.ENV]...}
- Pass data from command line:
ENV=qa yarn login-suite
◼️ Running Firefox locally
Let’s configure Firefox browser to run locally on your machine.
- Make sure you have Firefox installed locally.
- Install Java JDK 8 on your machine. If you are on macOS, you can run below command to setup openjdk:
brew install adoptopenjdk/openjdk/adoptopenjdk8
- Install `
selenium-standalone-service
`
yarn add -D @wdio/selenium-standalone-service
- Update
wdio.conf.js
and add firefox capabilities undercapabilities
andservices
section
{capabilites: [{maxInstances: 5,browserName: 'chrome',acceptInsecureCerts: true,},{maxInstances: 5,browserName: 'firefox',acceptInsecureCerts: true,}],services: ['chromedriver', 'selenium-standalone'],}
If you run the command yarn wdio run wdio.conf.js
now, it will trigger both test on Firefox and Chrome browsers.
◼️ Working With Web Elements
▪️ How to locate elements using Chrome DevTools?
The DOM panel of Chrome DevTools provides a special tool called "find " to locate the web elements depending on specified criteria.
After opening the Chrome Developer Tools, press "Ctrl + f ", this will open a find bar.
▪️ Selector Examples:
Official W3C list of supported selector types:
- CSS selector
- Link text selector
- Partial link text selector
- Tag name
- XPath selector
▪️ Tag Selector
$('h1')$('div')
▪️ Link Text Selector:
- By adding an equal (i.e.,
=
), WebdriverIO will look for a link text full match
$('=Projects')
- By passing in a custom element type for the text on an element, WebDriverIO will match the correct element type
$('button=Sign in')
▪️ Partial Link Text Selector:
By adding an asterisk (i.e., *
), WebdriverIO will look for a partial text match
$('*=chive')
▪️ CSS Selectros:
Grab the <ul>
by class name:
$('ul.items')
Grabs the two li
elements by class as well:
$$('li.item')
Here are a few recommend resources to learn more about CSS Selectors:
- TutsPlus "The 30 CSS Selectors You Must Memorize"
- Sauce Labs "CSS Selector Tips"
- CSS Tricks "CSS Almanac / Selectors"
- Ghost Selector "CSS Selector Strategies for Automated Browser Testing"
Attribute Selectors
Attributes are the properties of HTML elements (e.g., the "class" part of <li class="item">
).
Using CSS Attribute Selectors (shorter version), we can target the ng-click
attribute of the link, like so:
$('[ng-click="showProjects()"]');
We can also combine attribute selectors to be more specific with our link:
$('[dropdown-menu] a[ng-click="showProjects()"]');
nth-child Selectors
Another method to find your element is to base it on the position in the HTML. The first-child
selector allows us to do this:
nth-child
takes a numeric value, which corresponds to the position in the HTML (it is not zero-indexed, so counting starts at 1).
$('li:first-child');$('[dropdown-menu] li:first-child');$('[dropdown-menu] li:nth-child(2)');
Note: Selecting multiple elements is consistently slower than grabbing a single one, so using nth-child
can help keep our tests speedy.
▪️ XPath
- Grab the
<ul>
by ID:
$('//ul[@id="main-menu"]').
- Get
li
elements by class:
$$('//li[@class="item"]').
- Selecting an element by text content
$('//a[contains(text(),"chive")]')
- Using XPath to select a parent of an element
$('//a[contains(text(),"chive")]/ancestor::ul')
▪️ Chaining Selectors:
- Select a container & then select the
span
inside that container using tag selector
$('=Projects').$('span')
$('ul').$$('li')
▪️ Using Custom Data Attributes:
- Add a custom attribute to your HTML components solely to facilitate automation testing. HTML5 introduced a formal "data" attribute type that is used for this purpose.
- Adding a custom data attribute as mentioned above can really limit the effect of an HTML restructuring.
- A custom attribute named with
test-id
is much less likely to be changed during project life cycle without warning so makes automation more stable. - Examples:$('[data-test-id="main-menu"]')#app > footer > div > span'[data-qa-id= "site-footer"] '
▪️ Making Locators More Reliable:
- If you can change the HTML (or can ask the developer to do so), add a custom data attribute for testing purposes only.
- If I don't have control of the HTML, come up with selectors that are specific, but not overly tied to the HTML structure.
◼️ What is POM?
The Page Object Model is a design pattern which allows test maintenance and reduction of duplicate code.
It's an object-oriented class that serves as an interface to a page or a section of an application. It normally contains locators, or elements, and functions that will interact with these elements. It may also contain other functions that the test will use.
◼️ Performing Common Tasks:
Note: Below common tasks are performed for the demo website: https://the-internet.herokuapp.com/
▪️ Validating Page Title:
browser.url('https://the-internet.herokuapp.com')**expect(browser).toHaveUrl('https://the-internet.herokuapp.com/add_remove_elements/')expect(browser).toHaveTitle('The Internet')**
▪️ Validating Element Text:
//home.page.jsget pageHeading() {return $('h1.heading');}//elements.e2e.jslet heading = await HomePage.pageHeading;**expect(heading).toHaveTextContaining("Welcome to the-internet");**
▪️ Sending Text to an Input Field using setValue**
it('Should set a value', async () => {await browser.url('https://jqueryui.com/')await $('[name="s"]').setValue("CSS Framework")})
▪️ Sending a Text to an Input Field using addValue
$(‘selector’).addValue(value) is another way to send the text to an input field. If the element value needs to be appended, you can use addValue.
it('Should set a value', async () => {await browser.url('https://jqueryui.com/')await $('[name="s"]').addValue("CSS Framework")})
▪️ Click an element:
//home.page.jsget pageLinkAddRemoveElement() {return $('ul > li:nth-child(2) > a')}//elements.e2e.jsawait HomePage.open();await HomePage.pageLinkAddRemoveElement.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/add_remove_elements/')
▪️ Get a List of Elements:
//home.page.jsasync getLiText() {return await this.itemList.map((item) => {return item.getText()})}//home.test.jsconst arrayList = await HomePage.getLiText();assert.strictEqual(arrayList.length, 44)
Alternate:
//home.page.jsget itemList() { return $('ul').$$('li') }//elements.e2e.jsawait HomePage.open();const pageLinksLists = await HomePage.itemListconst pageLinksArray = []for(const li of pageLinksLists) {pageLinksArray.push(await li.getText())}console.log('pageLinksArray', pageLinksArray)
▪️ Saving a Screenshot
await HomePage.open();await HomePage.pageLinkAddRemoveElement.click();const elem = await AddRemoveElementPage.pageHeading;**await elem.saveScreenshot('./screenshots/elemScreenshot.png');**
▪️ Scrolling an element into view
await browser.url('https://the-internet.herokuapp.com/')await browser.saveScreenshot('screenshots/before-scrolling.png')**await HomePage.pageFooter.scrollIntoView()****await expect(HomePage.pageFooter).toBeDisplayedInViewport()**await browser.saveScreenshot('./screenshots/after-scrolling.png')
▪️ Clicking a Checkbox
await HomePage.open();await HomePage.pageLinkCheckbox.click()expect(await CheckboxPage.pageHeading.getText()).toHaveTextContaining('Checkboxes');**await CheckboxPage.checkBox1.click()**console.log('isSelected: ', **await CheckboxPage.checkBox1.isSelected()**);**await CheckboxPage.checkBox2.click()**console.log('isNotSelected: ', **await CheckboxPage.checkBox2.isSelected()**);
▪️ Hover over an element
await HomePage.open();await HomePage.pageLinkHover.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/hovers')**await HoversPage.hoverUser1.moveTo();**expect(await HoversPage.user1Name.getText()).toHaveTextContaining('name: user1')await HoversPage.user1ProfileLink.click()await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/users/1')
▪️ Sending a keyboard key
await HomePage.open();await HomePage.pageLinkKeyPresses.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/key_presses')expect(await KeyPressesPage.pageHeading.getText()).toHaveTextContaining('Key Presses')await browser.keys('Enter')expect(await KeyPressesPage.result.getText()).toHaveTextContaining('You entered: Enter')
▪️ Opening a new Window
await HomePage.open();await HomePage.pageLinkMultipleWindows.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/windows')expect(await MultipleWindowsPage.pageHeading.getText()).toHaveTextContaining('Opening a new window')await MultipleWindowsPage.newWindowLink.click();await browser.switchWindow('https://the-internet.herokuapp.com/windows/new')expect(await browser.getTitle()).toHaveTextContaining('New Window')expect(await MultipleWindowsPage.newWindowHeading.getText()).toHaveTextContaining('New Window')
▪️ Switching Frames
await HomePage.open();await HomePage.pageFrames.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/frames')expect(await FramesPage.pageHeading.getText()).toHaveTextContaining('Frames')await FramesPage.linkIFrame.click()await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/iframe')expect(await browser.getTitle()).toHaveTextContaining('New Window')expect(await FramesPage.pageHeading.getText()).toHaveTextContaining('An iFrame containing the TinyMCE WYSIWYG Editor')//Switch iFrame**await browser.switchToFrame(0)**expect(await FramesPage.iframeBody.getText()).toHaveTextContaining('Your content goes here.')console.log('Text inside frame: ' + await FramesPage.iframeBody.getText())await FramesPage.iframeBody.clearValue();await FramesPage.iframeBody.click();await FramesPage.iframeBody.setValue('Testing in Progress')expect(await FramesPage.iframeBody.getValue()).toHaveTextContaining('Testing in Progress')browser.switchToParentFrame()expect(await FramesPage.pageHeading.getText()).toHaveTextContaining('An iFrame containing the TinyMCE WYSIWYG Editor')console.log('Webpage Heading on parent frame: ' + await FramesPage.pageHeading.getText())
▪️ Drag & Drop
await HomePage.open();await HomePage.pageLinkDragAndDrop.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/drag_and_drop')expect(await DragAndDropPage.pageHeading.getText()).toHaveTextContaining('Drag and Drop')const source = await DragAndDropPage.sourceColumnconst target = await DragAndDropPage.targetColumn// drag and drop source to target element**await source.dragAndDrop(target)**// await DragAndDropPage.sourceColumn.dragAndDrop(DragAndDropPage.targetColumn)expect(await DragAndDropPage.targetColumnHeader.getText()).toHaveTextContaining('A')
▪️ Dropdown List
await HomePage.open();await HomePage.pageLinkDropdownList.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/dropdown')expect(await DropdownListPage.pageHeading.getText()).toHaveTextContaining('Dropdown List')const option1 = await DropdownListPage.option1const option2 = await DropdownListPage.option2await DropdownListPage.dropdown.click()option1.click()console.log('option 1: ', await option1.getText())option2.click()console.log('option 2: ', await option2.getText())
▪️ Handling of Alerts
it('should demonstrate the handling of JS Alerts', async () => {await HomePage.open();await HomePage.pageLinkJavascriptAlerts.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/javascript_alerts')expect(await AlertsPage.pageHeading.getText()).toHaveTextContaining('JavaScript Alerts')// Alerts not working for WebdriverIO7: https://github.com/webdriverio/webdriverio/issues/6423await AlertsPage.jsAlert.click();browser.acceptAlert();// const alertText = await browser.getAlertText()// console.log('alertText', alertText)// expect(await browser.getAlertText()).toHaveTextContaining('I am a JS Alert')// browser.acceptAlert();// await AlertsPage.jsConfirm.click();// expect(await browser.getAlertText()).toHaveTextContaining('I am a JS Confirm')// browser.acceptAlert();// await AlertsPage.jsPrompt.click();// await browser.sendAlertText('Testing')// expect(await browser.getAlertText()).toHaveTextContaining('Testing')// browser.acceptAlert();})
▪️ Getting html value using getHTML
- If you need to get the text of a hidden element, use
getHTML
asgetText
will return an empty string for an element that is hidden on a page (for e.g. options for dropdown menu) getHTML
returns the HTML value of an element, including the text inside it.
$('.error-messages li').getHTML();// returns "<li>email can't be blank</li>"
getHTML
accepts a single parameter, which is a boolean flag to include the selector element tag or not. By default it's true, and therefore includes that tag. If we pass in false, then selector is not included in the result:
$('.error-messages li').getHTML(false);// returns "email can't be blank"
- Note that boolean flag only works on the main element, therefore, any child tags will still be included in the results.
▪️ Dynamic Controls - Enabled/Disabled State
it('should demonstrate the use of dynamic controls - Enabled/Disabled State', async () => {await HomePage.open();await HomePage.pageLinkDynamicControls.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/dynamic_controls')expect(await DynamicControlsPage.pageHeading.getText()).toHaveTextContaining('Dynamic Controls')let inputState = await DynamicControlsPage.inputField.isEnabled()console.log('inputState:(', inputState)await DynamicControlsPage.btnInputExample.click()await DynamicControlsPage.inputField.waitForEnabled()inputState = await DynamicControlsPage.inputField.isEnabled()console.log('inputState:*)', inputState)if(inputState) {await DynamicControlsPage.inputField.setValue('Testing in progress')await DynamicControlsPage.btnInputExample.click()await DynamicControlsPage.inputField.waitForEnabled({reverse: true})expect(await DynamicControlsPage.inputField.getValue()).toHaveTextContaining('Testing in progress')}})
▪️ Dynamic Controls - Existence/non-existence of an element
it.only('should demonstrate the use of dynamic controls - Existence/non-existence of an element', async () => {await HomePage.open();await HomePage.pageLinkDynamicControls.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/dynamic_controls')expect(await DynamicControlsPage.pageHeading.getText()).toHaveTextContaining('Dynamic Controls')let existState = await DynamicControlsPage.checkBox.isExisting()console.log('should exists:', existState)if(existState) {await DynamicControlsPage.checkBox.click()console.log('Checkbox is in checked state: ' , await DynamicControlsPage.checkBox.isSelected())}await DynamicControlsPage.btnCheckboxExample.click()await DynamicControlsPage.checkBox.waitForDisplayed({reverse: true})existState = await DynamicControlsPage.checkBox.isExisting()console.log('should exists:', existState)if(!existState) {console.log('Checkbox is not displayed anymore...')expect(await DynamicControlsPage.message.getText()).toHaveTextContaining("It's gone!")}await DynamicControlsPage.btnCheckboxExample.click()await DynamicControlsPage.checkBox.waitForDisplayed()expect(await DynamicControlsPage.message.getText()).toHaveTextContaining("It's back!")})
▪️ Usage of 'waitUntil' for a certain condition
describe("Usage of 'waitUntil' for a certain condition", () => {it("should wait until a certain condition occurs", async () => {await HomePage.open();await HomePage.pageLinkDynamicControls.click();await expect(browser).toHaveUrl('https://the-internet.herokuapp.com/dynamic_controls')expect(await DynamicControlsPage.pageHeading.getText()).toHaveTextContaining('Dynamic Controls')await DynamicControlsPage.btnCheckboxExample.click()// $(selector).waitUntil(condition, { timeout, timeoutMsg, interval })await DynamicControlsPage.btnCheckboxExample.waitUntil(async () => {return (await this.getText()) === 'Add'}, {timeout: 10000,timeoutMsg: 'expected text to be different after 10s'});})})
◼️ Assertions?
▪️ What is an Assertion?
- Assertions are validation points that conclusively determine if a test case passed or failed.
- Assertions are validation points that conclusively determine if a test case passed or failed.
▪️ Node.JS default Assert Library:
**const** assert = require('assert');await assert.strictEqual(await browser.getTitle(), 'Conduit');
▪️ Chai Assertions
What is Chai?
- Chai is a BDD / TDD assertion library for node and the browser that can be delightfully paired with any javascript testing framework.
- To install chai use below command
npm install chai --save-dev
- To install Chai WebdriverIO
npm install chai-webdriverio --save-dev
- Chai module contains
assert
,expect
&should
, therefore you can use any of these assertion method.
Allowing global access to Chai Assertions
- Use webdriverio config file to allow access to chai assertion library throughout the project.
- Update Config for
beforeTest
as below by putting it in the “before” snippet. As a result, this code snippet will run before every test, regardless of where the file is located.
beforeTest: function (test, context) {const chai = require('chai')const chaiWebdriver = require('chai-webdriverio').defaultchai.use(chaiWebdriver(browser))global.assert = chai.assertglobal.should = chai.shouldglobal.expect = chai.expect},
Chai Usage Examples:
expect(browser.getUrl()).equals('http://the-internet.herokuapp.com/abtest')
▪️ WebDriverIO Expect
- We can perform different types of assertions using WebDriverIO built-in assertion methods.
- Aside from the WebdriverIO specific assertions
expect-webdriverio
adds, you also gain access to the built-in ExpectJS assertions
expect(await browser.getUrl()).toEqual('http://localhost:8080/');
- Here are few examples of using WebDriverIO Expect assertions:
Focus:
isFocused
isFocused() returns true or false, depending on the element statetoBeFocused()
is an assertion equivalent to the isFocused() method.
describe("Test case description goes here", () => {it('Should get a focus after a click', async () => {await browser.url('https://the-internet.herokuapp.com/inputs')const inputBox = await $('input[type=number]')console.log("Before Clicking the input box...")console.log('isFocused State: ', await inputBox.isFocused())await inputBox.click()console.log("Before Clicking the input box...")await expect(inputBox).toBeFocused()console.log('isFocused State: ', await inputBox.isFocused())})})
Attributes
- Use
toHaveAttribute
to validate existance of certain attributes.
it.only("should perform a certain action", async () => {await browser.url('https://the-internet.herokuapp.com/redirector')const content = await $('#content')const redirectLink = await $('#redirect')await expect(content).toHaveAttribute('class', 'large-12 columns')await expect(redirectLink).toHaveAttribute('href', 'redirect')})
- Use
toHaveAttributeContaining
to check if an element has an attribute that contains a certain value.
it.only("should check if attribute value includes specific text", async () => {await browser.url('https://the-internet.herokuapp.com/broken_images')const userImage = $('img:nth-child(4)')await expect(userImage).toHaveAttrContaining('src', '.jpg')})
Class
- Use
toHaveElementClass
to check if an element has a certain class name.
it.only("should check if an element has a certain class name", async () => {await browser.url('https://the-internet.herokuapp.com/')const pageHeading = await $('<h1>')await expect(pageHeading).toHaveElementClass('heading')})
Array List Size
Use toBeElementsArrayOfSize
to Check number of fetched elements using $$
command.
it.only("should check number of li items present on the page", async () => {await browser.url('https://the-internet.herokuapp.com/')const listItems = await $$('ul > li')console.log('listItems count: ', listItems)await expect(listItems).toBeElementsArrayOfSize(44) // 44 items in the listawait expect(listItems).toBeElementsArrayOfSize({ lte: 50 })})
toHaveLength
// Check that there are exactly five links on the pageconst links = $$('a');expect(links).toHaveLength(5);//Use the .not switch to flip our assertionexpect(links).not.toHaveLength(5);
To check all available assertions checkout Expect on WebDriverIO site.
◼️ Resources
- WDIO Official Documentation
- Allure Report Documentation
- Cheatsheet comparing CSS vs XPath: devhints.io is invaluable