├── .gitignore ├── README.md ├── babel.config.js ├── package-lock.json ├── package.json ├── test ├── pages │ ├── page.js │ ├── search.page.js │ └── watches.page.js ├── resources │ └── index.js ├── specs │ ├── chai.js │ ├── search.js │ └── watches.js └── utilities │ └── helper.js └── wdio.conf.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | allure-report/ 3 | allure-results/ 4 | report/ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WebdriverIO Tutorial 2 | This repo is part of my WebdriverIO tutorial series on [YouTube](https://www.youtube.com/playlist?list=PL6AdzyjjD5HBbt9amjf3wIVMaobb28ZYN) 3 | 4 | ## To start 5 | - Access the appropriate branch related to the video - ex: "write-tests-3" 6 | - Clone the branch locally 7 | - Run `npm i` 8 | 9 | ### Run Tests 10 | - `npx wdio wdio.conf.js` 11 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | [ 4 | '@babel/preset-env', 5 | { 6 | targets: { 7 | node: 12, 8 | }, 9 | }, 10 | ], 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "webdriverio-test", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "devDependencies": { 13 | "@babel/cli": "^7.10.4", 14 | "@babel/core": "^7.10.4", 15 | "@babel/preset-env": "^7.10.4", 16 | "@babel/register": "^7.10.4", 17 | "@wdio/allure-reporter": "^6.3.0", 18 | "@wdio/browserstack-service": "^6.1.15", 19 | "@wdio/cli": "^6.1.22", 20 | "@wdio/junit-reporter": "^6.3.4", 21 | "@wdio/local-runner": "^6.1.22", 22 | "@wdio/mocha-framework": "^6.1.19", 23 | "@wdio/selenium-standalone-service": "^6.1.14", 24 | "@wdio/spec-reporter": "^6.1.14", 25 | "@wdio/sync": "^6.1.14", 26 | "chromedriver": "^83.0.0", 27 | "wdio-chromedriver-service": "^6.0.3" 28 | }, 29 | "dependencies": { 30 | "chai": "^4.2.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /test/pages/page.js: -------------------------------------------------------------------------------- 1 | export default class Page { 2 | open(path) { 3 | browser.url(path); 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /test/pages/search.page.js: -------------------------------------------------------------------------------- 1 | import Page from './page'; 2 | 3 | class SearchPage extends Page { 4 | get searchInput() { 5 | return $('#gh-ac'); 6 | } 7 | get searchBtn() { 8 | return $('#gh-btn'); 9 | } 10 | get category() { 11 | return $('#gh-cat option:nth-child(1)'); 12 | } 13 | 14 | open() { 15 | super.open('/'); 16 | } 17 | } 18 | 19 | export default new SearchPage(); 20 | -------------------------------------------------------------------------------- /test/pages/watches.page.js: -------------------------------------------------------------------------------- 1 | import Page from './page'; 2 | 3 | class WatchesPage extends Page { 4 | get promoBanner() { 5 | return $('section.b-promobanner'); // findElement 6 | } 7 | get infoTitle() { 8 | return $('.b-promobanner__info-title'); 9 | } 10 | get shopButton() { 11 | return $('.b-promobanner__info-btn'); 12 | } 13 | get watchesCategoryList() { 14 | return $$('section[id="s0-29-13-0-1[0]-0-0"] ul li'); // 6 elements 15 | } 16 | get fashionLink() { 17 | return $$('.hl-cat-nav__js-tab a[href*="Fashion"]')[0]; 18 | } 19 | get watchesLink() { 20 | return $('.hl-cat-nav__sub-cat-col a[href*="Watches-Parts"]'); 21 | } 22 | 23 | open() { 24 | super.open('/'); 25 | } 26 | 27 | getWatchesCategoryListText() { 28 | const watchesList = []; 29 | this.watchesCategoryList.map((element) => 30 | watchesList.push(element.getText()) 31 | ); 32 | return watchesList; 33 | } 34 | } 35 | 36 | export default new WatchesPage(); 37 | -------------------------------------------------------------------------------- /test/resources/index.js: -------------------------------------------------------------------------------- 1 | export default { 2 | homeTitle: 'Electronics, Cars, Fashion, Collectibles & More | eBay', 3 | laptopTitle: 'Laptop | eBay', 4 | watchesCategoryList: [ 5 | 'Jewelry & Watches', 6 | 'Watches, Parts & Accessories', 7 | 'Watches', 8 | 'Parts, Tools & Guides', 9 | 'Watch Accessories', 10 | 'Watches Mixed Lots', 11 | ], 12 | }; 13 | -------------------------------------------------------------------------------- /test/specs/chai.js: -------------------------------------------------------------------------------- 1 | import { expect as chaiExpect, assert } from 'chai'; 2 | import 'chai/register-should'; 3 | 4 | xdescribe('Watches Page', () => { 5 | it('should show the banner container', () => { 6 | browser.url( 7 | 'https://www.ebay.com/b/Watches-Parts-Accessories/260324/bn_2408535' 8 | ); 9 | const promoBanner = $('section.b-promobanner'); 10 | expect(promoBanner).toBeDisplayed(); 11 | }); 12 | 13 | it('should show the banner title', () => { 14 | const infoTitle = $('.b-promobanner__info-title'); 15 | const infoTitleText = infoTitle.getText(); 16 | 17 | expect(infoTitle).toHaveTextContaining('watches'); 18 | chaiExpect(infoTitleText).to.not.be.empty; 19 | infoTitle.should.not.be.empty; 20 | // assert.isEmpty(infoTitleText); 21 | }); 22 | 23 | it('should contain link on banner button and verify its clickable', () => { 24 | const shopButton = $('.b-promobanner__info-btn'); 25 | const tag = shopButton.getTagName(); 26 | 27 | expect(shopButton).toHaveLinkContaining('/fashion/'); 28 | expect(shopButton).toBeClickable(); 29 | chaiExpect(tag).to.equal('a'); 30 | tag.should.be.equal('a'); 31 | }); 32 | 33 | it('should click on the shop button and verify the new url', () => { 34 | const shopButton = $('.b-promobanner__info-btn'); 35 | shopButton.click(); 36 | 37 | const url = browser.getUrl(); 38 | chaiExpect(url).to.include('/fashion/'); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/specs/search.js: -------------------------------------------------------------------------------- 1 | import SearchPage from '../pages/search.page'; 2 | import { waitForTextChange } from '../utilities/helper'; 3 | import resources from '../resources'; 4 | import allureReporter from '@wdio/allure-reporter'; 5 | 6 | describe('Ebay Product Search', () => { 7 | it('should open the main url and verify the title', () => { 8 | SearchPage.open(); 9 | expect(browser).toHaveTitle(resources.homeTitle); 10 | }); 11 | 12 | it('should search for a product and verify the search text value', () => { 13 | allureReporter.addSeverity('Critical'); 14 | SearchPage.searchInput.addValue('Laptop'); 15 | SearchPage.searchBtn.click(); 16 | 17 | expect(SearchPage.searchInput).toHaveValue('Laptop'); 18 | }); 19 | 20 | it('should redirect to a new page and verify the title', () => { 21 | expect(browser).toHaveTitle('Laptop | eBay'); 22 | }); 23 | 24 | it('should update the search category', () => { 25 | allureReporter.addFeature('search category'); 26 | waitForTextChange(SearchPage.category, 'PC Laptops & Netbooks', 10000); 27 | expect(SearchPage.category).toHaveText('PC Laptops & Netbooks'); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /test/specs/watches.js: -------------------------------------------------------------------------------- 1 | import { expect as chaiExpect } from 'chai'; 2 | import WatchesPage from '../pages/watches.page'; 3 | import { waitAndClick } from '../utilities/helper'; 4 | import resources from '../resources'; 5 | 6 | describe('Watches Page', () => { 7 | before(() => { 8 | WatchesPage.open(); 9 | WatchesPage.fashionLink.moveTo(); 10 | // browser.pause(1000); // Implicit Wait 11 | waitAndClick(WatchesPage.watchesLink, 10000); 12 | }); 13 | 14 | it('should verify the watches category list', () => { 15 | const watchesCategoryList = WatchesPage.getWatchesCategoryListText(); 16 | chaiExpect(watchesCategoryList).to.deep.equal( 17 | resources.watchesCategoryList 18 | ); 19 | }); 20 | 21 | it('should show the banner container', () => { 22 | expect(WatchesPage.promoBanner).toBeDisplayed(); 23 | }); 24 | 25 | it('should show the banner title', () => { 26 | expect(WatchesPage.infoTitle).toHaveTextContaining('shoes'); 27 | }); 28 | 29 | it('should contain link on banner button and verify its clickable', () => { 30 | expect(WatchesPage.shopButton).toHaveLinkContaining('/fashion/'); 31 | expect(WatchesPage.shopButton).toBeClickable(); 32 | }); 33 | 34 | it('should click on the shop button and verify the new url', () => { 35 | WatchesPage.shopButton.click(); 36 | const url = browser.getUrl(); 37 | chaiExpect(url).to.include('/fashion/'); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /test/utilities/helper.js: -------------------------------------------------------------------------------- 1 | export const waitForTextChange = (el, text, timeout) => { 2 | browser.waitUntil( 3 | function () { 4 | return el.getText() === text; 5 | }, 6 | { timeout } 7 | ); 8 | }; 9 | 10 | export const waitAndClick = (el, timeout) => { 11 | el.waitForDisplayed({ timeout }); 12 | el.click(); 13 | }; 14 | -------------------------------------------------------------------------------- /wdio.conf.js: -------------------------------------------------------------------------------- 1 | exports.config = { 2 | // Browserstack Config 3 | user: process.env.BROWSERSTACK_USERNAME, 4 | key: process.env.BROWSERSTACK_KEY, 5 | 6 | // 7 | // ==================== 8 | // Runner Configuration 9 | // ==================== 10 | // 11 | // WebdriverIO allows it to run your tests in arbitrary locations (e.g. locally or 12 | // on a remote machine). 13 | runner: 'local', 14 | // 15 | // ================== 16 | // Specify Test Files 17 | // ================== 18 | // Define which test specs should run. The pattern is relative to the directory 19 | // from which `wdio` was called. Notice that, if you are calling `wdio` from an 20 | // NPM script (see https://docs.npmjs.com/cli/run-script) then the current working 21 | // directory is where your package.json resides, so `wdio` will be called from there. 22 | // 23 | specs: ['./test/specs/**/search.js'], 24 | // Patterns to exclude. 25 | exclude: [ 26 | // 'path/to/excluded/files' 27 | ], 28 | // 29 | // ============ 30 | // Capabilities 31 | // ============ 32 | // Define your capabilities here. WebdriverIO can run multiple capabilities at the same 33 | // time. Depending on the number of capabilities, WebdriverIO launches several test 34 | // sessions. Within your capabilities you can overwrite the spec and exclude options in 35 | // order to group specific specs to a specific capability. 36 | // 37 | // First, you can define how many instances should be started at the same time. Let's 38 | // say you have 3 different capabilities (Chrome, Firefox, and Safari) and you have 39 | // set maxInstances to 1; wdio will spawn 3 processes. Therefore, if you have 10 spec 40 | // files and you set maxInstances to 10, all spec files will get tested at the same time 41 | // and 30 processes will get spawned. The property handles how many capabilities 42 | // from the same test should run tests. 43 | // 44 | maxInstances: 10, 45 | // 46 | // If you have trouble getting all important capabilities together, check out the 47 | // Sauce Labs platform configurator - a great tool to configure your capabilities: 48 | // https://docs.saucelabs.com/reference/platforms-configurator 49 | // 50 | capabilities: [ 51 | { 52 | // maxInstances can get overwritten per capability. So if you have an in-house Selenium 53 | // grid with only 5 firefox instances available you can make sure that not more than 54 | // 5 instances get started at a time. 55 | maxInstances: 5, 56 | // 57 | browserName: 'chrome', 58 | // If outputDir is provided WebdriverIO can capture driver session logs 59 | // it is possible to configure which logTypes to include/exclude. 60 | // excludeDriverLogs: ['*'], // pass '*' to exclude all driver session logs 61 | // excludeDriverLogs: ['bugreport', 'server'], 62 | }, 63 | { 64 | maxInstances: 5, 65 | browserName: 'firefox', 66 | }, 67 | ], 68 | // 69 | // =================== 70 | // Test Configurations 71 | // =================== 72 | // Define all options that are relevant for the WebdriverIO instance here 73 | // 74 | // Level of logging verbosity: trace | debug | info | warn | error | silent 75 | logLevel: 'warn', 76 | // 77 | // Set specific log levels per logger 78 | // loggers: 79 | // - webdriver, webdriverio 80 | // - @wdio/applitools-service, @wdio/browserstack-service, @wdio/devtools-service, @wdio/sauce-service 81 | // - @wdio/mocha-framework, @wdio/jasmine-framework 82 | // - @wdio/local-runner, @wdio/lambda-runner 83 | // - @wdio/sumologic-reporter 84 | // - @wdio/cli, @wdio/config, @wdio/sync, @wdio/utils 85 | // Level of logging verbosity: trace | debug | info | warn | error | silent 86 | // logLevels: { 87 | // webdriver: 'info', 88 | // '@wdio/applitools-service': 'info' 89 | // }, 90 | // 91 | // If you only want to run your tests until a specific amount of tests have failed use 92 | // bail (default is 0 - don't bail, run all tests). 93 | bail: 0, 94 | // 95 | // Set a base URL in order to shorten url command calls. If your `url` parameter starts 96 | // with `/`, the base url gets prepended, not including the path portion of your baseUrl. 97 | // If your `url` parameter starts without a scheme or `/` (like `some/path`), the base url 98 | // gets prepended directly. 99 | baseUrl: 'https://www.ebay.com/', 100 | // 101 | // Default timeout for all waitFor* commands. 102 | waitforTimeout: 10000, 103 | // 104 | // Default timeout in milliseconds for request 105 | // if browser driver or grid doesn't send response 106 | connectionRetryTimeout: 120000, 107 | // 108 | // Default request retries count 109 | connectionRetryCount: 3, 110 | // 111 | // Test runner services 112 | // Services take over a specific job you don't want to take care of. They enhance 113 | // your test setup with almost no effort. Unlike plugins, they don't add new 114 | // commands. Instead, they hook themselves up into the test process. 115 | services: ['browserstack'], 116 | 117 | // Framework you want to run your specs with. 118 | // The following are supported: Mocha, Jasmine, and Cucumber 119 | // see also: https://webdriver.io/docs/frameworks.html 120 | // 121 | // Make sure you have the wdio adapter package for the specific framework installed 122 | // before running any tests. 123 | framework: 'mocha', 124 | // 125 | // The number of times to retry the entire specfile when it fails as a whole 126 | // specFileRetries: 1, 127 | // 128 | // Whether or not retried specfiles should be retried immediately or deferred to the end of the queue 129 | // specFileRetriesDeferred: false, 130 | // 131 | // Test reporter for stdout. 132 | // The only one supported by default is 'dot' 133 | // see also: https://webdriver.io/docs/dot-reporter.html 134 | reporters: [ 135 | [ 136 | 'allure', 137 | { 138 | outputDir: 'allure-results', 139 | }, 140 | ], 141 | [ 142 | 'junit', 143 | { 144 | outputDir: './report', 145 | outputFileFormat: function (options) { 146 | return `results-${new Date().getTime()}.xml`; 147 | }, 148 | }, 149 | ], 150 | ], 151 | 152 | // 153 | // Options to be passed to Mocha. 154 | // See the full list at http://mochajs.org/ 155 | mochaOpts: { 156 | ui: 'bdd', 157 | timeout: 60000, 158 | require: ['@babel/register'], 159 | }, 160 | // 161 | // ===== 162 | // Hooks 163 | // ===== 164 | // WebdriverIO provides several hooks you can use to interfere with the test process in order to enhance 165 | // it and to build services around it. You can either apply a single function or an array of 166 | // methods to it. If one of them returns with a promise, WebdriverIO will wait until that promise got 167 | // resolved to continue. 168 | /** 169 | * Gets executed once before all workers get launched. 170 | * @param {Object} config wdio configuration object 171 | * @param {Array.} capabilities list of capabilities details 172 | */ 173 | // onPrepare: function (config, capabilities) { 174 | // }, 175 | /** 176 | * Gets executed before a worker process is spawned and can be used to initialise specific service 177 | * for that worker as well as modify runtime environments in an async fashion. 178 | * @param {String} cid capability id (e.g 0-0) 179 | * @param {[type]} caps object containing capabilities for session that will be spawn in the worker 180 | * @param {[type]} specs specs to be run in the worker process 181 | * @param {[type]} args object that will be merged with the main configuration once worker is initialised 182 | * @param {[type]} execArgv list of string arguments passed to the worker process 183 | */ 184 | // onWorkerStart: function (cid, caps, specs, args, execArgv) { 185 | // }, 186 | /** 187 | * Gets executed just before initialising the webdriver session and test framework. It allows you 188 | * to manipulate configurations depending on the capability or spec. 189 | * @param {Object} config wdio configuration object 190 | * @param {Array.} capabilities list of capabilities details 191 | * @param {Array.} specs List of spec file paths that are to be run 192 | */ 193 | // beforeSession: function (config, capabilities, specs) { 194 | // }, 195 | /** 196 | * Gets executed before test execution begins. At this point you can access to all global 197 | * variables like `browser`. It is the perfect place to define custom commands. 198 | * @param {Array.} capabilities list of capabilities details 199 | * @param {Array.} specs List of spec file paths that are to be run 200 | */ 201 | // before: function (capabilities, specs) { 202 | // }, 203 | /** 204 | * Runs before a WebdriverIO command gets executed. 205 | * @param {String} commandName hook command name 206 | * @param {Array} args arguments that command would receive 207 | */ 208 | // beforeCommand: function (commandName, args) { 209 | // }, 210 | /** 211 | * Hook that gets executed before the suite starts 212 | * @param {Object} suite suite details 213 | */ 214 | // beforeSuite: function (suite) { 215 | // }, 216 | /** 217 | * Function to be executed before a test (in Mocha/Jasmine) starts. 218 | */ 219 | beforeTest: function (test, context) { 220 | browser.maximizeWindow(); 221 | }, 222 | /** 223 | * Hook that gets executed _before_ a hook within the suite starts (e.g. runs before calling 224 | * beforeEach in Mocha) 225 | */ 226 | // beforeHook: function (test, context) { 227 | // }, 228 | /** 229 | * Hook that gets executed _after_ a hook within the suite starts (e.g. runs after calling 230 | * afterEach in Mocha) 231 | */ 232 | // afterHook: function (test, context, { error, result, duration, passed, retries }) { 233 | // }, 234 | /** 235 | * Function to be executed after a test (in Mocha/Jasmine). 236 | */ 237 | afterTest: function ( 238 | test, 239 | context, 240 | { error, result, duration, passed, retries } 241 | ) { 242 | if (error) { 243 | browser.takeScreenshot(); 244 | } 245 | }, 246 | 247 | /** 248 | * Hook that gets executed after the suite has ended 249 | * @param {Object} suite suite details 250 | */ 251 | // afterSuite: function (suite) { 252 | // }, 253 | /** 254 | * Runs after a WebdriverIO command gets executed 255 | * @param {String} commandName hook command name 256 | * @param {Array} args arguments that command would receive 257 | * @param {Number} result 0 - command success, 1 - command error 258 | * @param {Object} error error object if any 259 | */ 260 | // afterCommand: function (commandName, args, result, error) { 261 | // }, 262 | /** 263 | * Gets executed after all tests are done. You still have access to all global variables from 264 | * the test. 265 | * @param {Number} result 0 - test pass, 1 - test fail 266 | * @param {Array.} capabilities list of capabilities details 267 | * @param {Array.} specs List of spec file paths that ran 268 | */ 269 | // after: function (result, capabilities, specs) { 270 | // }, 271 | /** 272 | * Gets executed right after terminating the webdriver session. 273 | * @param {Object} config wdio configuration object 274 | * @param {Array.} capabilities list of capabilities details 275 | * @param {Array.} specs List of spec file paths that ran 276 | */ 277 | // afterSession: function (config, capabilities, specs) { 278 | // }, 279 | /** 280 | * Gets executed after all workers got shut down and the process is about to exit. An error 281 | * thrown in the onComplete hook will result in the test run failing. 282 | * @param {Object} exitCode 0 - success, 1 - fail 283 | * @param {Object} config wdio configuration object 284 | * @param {Array.} capabilities list of capabilities details 285 | * @param {} results object containing test results 286 | */ 287 | // onComplete: function(exitCode, config, capabilities, results) { 288 | // }, 289 | /** 290 | * Gets executed when a refresh happens. 291 | * @param {String} oldSessionId session ID of the old session 292 | * @param {String} newSessionId session ID of the new session 293 | */ 294 | //onReload: function(oldSessionId, newSessionId) { 295 | //} 296 | }; 297 | --------------------------------------------------------------------------------