├── .babelrc ├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmignore ├── LICENSE ├── README.md ├── index.js ├── lib └── devtools.js ├── package-lock.json └── package.json /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | ["env", { 4 | "targets": { 5 | "node": "4.4" 6 | } 7 | }], 8 | "stage-0" 9 | ], 10 | "plugins": [ 11 | "transform-runtime", 12 | "add-module-exports" 13 | ] 14 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | [*] 8 | 9 | indent_style = space 10 | indent_size = 4 11 | 12 | end_of_line = lf 13 | charset = utf-8 14 | trim_trailing_whitespace = true 15 | insert_final_newline = true 16 | 17 | [{.babelrc,.eslintrc,.codeclimate.yml,.travis.yml,*.json}] 18 | indent_size = 2 -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard", 3 | "rules": { 4 | "indent": [2, 4] 5 | }, 6 | "parser": "babel-eslint", 7 | "globals": { 8 | "browser": true 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | 8 | # Runtime data 9 | pids 10 | *.pid 11 | *.seed 12 | *.pid.lock 13 | 14 | # Directory for instrumented libs generated by jscoverage/JSCover 15 | lib-cov 16 | 17 | # Coverage directory used by tools like istanbul 18 | coverage 19 | 20 | # nyc test coverage 21 | .nyc_output 22 | 23 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 24 | .grunt 25 | 26 | # Bower dependency directory (https://bower.io/) 27 | bower_components 28 | 29 | # node-waf configuration 30 | .lock-wscript 31 | 32 | # Compiled binary addons (http://nodejs.org/api/addons.html) 33 | build/Release 34 | 35 | # Dependency directories 36 | node_modules/ 37 | jspm_packages/ 38 | 39 | # Typescript v1 declaration files 40 | typings/ 41 | 42 | # Optional npm cache directory 43 | .npm 44 | 45 | # Optional eslint cache 46 | .eslintcache 47 | 48 | # Optional REPL history 49 | .node_repl_history 50 | 51 | # Output of 'npm pack' 52 | *.tgz 53 | 54 | # Yarn Integrity file 55 | .yarn-integrity 56 | 57 | # dotenv environment variables file 58 | .env 59 | 60 | build 61 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | !./build 2 | lib -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 WebdriverIO 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | WDIO DevTools Service 2 | ===================== 3 | 4 | With Chrome v63 and up the browser [started to support](https://developers.google.com/web/updates/2017/10/devtools-release-notes#multi-client) multi clients allowing arbitrary clients to access the [Chrome DevTools Protocol](https://chromedevtools.github.io/devtools-protocol/). This provides interesting opportunities to automate Chrome beyond the [WebDriver protocol](https://www.w3.org/TR/webdriver/). With this service you can enhance the wdio browser object to leverage that access and call Chrome DevTools commands within your tests to e.g. intercept requests, throttle network capabilities or take CSS/JS coverage. 5 | 6 | __Note:__ this service currently only supports Chrome v63 and up! 7 | 8 | ## Installation 9 | 10 | The easiest way is to keep `wdio-devtools-service` as a devDependency in your `package.json`. 11 | 12 | ```json 13 | { 14 | "devDependencies": { 15 | "wdio-devtools-service": "~0.1.1" 16 | } 17 | } 18 | ``` 19 | 20 | You can simple do it by: 21 | 22 | ```bash 23 | npm install wdio-devtools-service --save-dev 24 | ``` 25 | 26 | Instructions on how to install `WebdriverIO` can be found [here.](http://webdriver.io/guide/getstarted/install.html) 27 | 28 | ## Configuration 29 | 30 | In order to use the service you just need to add the service to your service list in your `wdio.conf.js` like: 31 | 32 | ```js 33 | // wdio.conf.js 34 | export.config = { 35 | // ... 36 | services: ['devtools'], 37 | // ... 38 | }; 39 | ``` 40 | 41 | ## Usage 42 | 43 | For now the service allows two different ways to access the Chrome DevTools Protocol: 44 | 45 | ### Via `cdp` Command 46 | 47 | The `cdp` command is a custom command added to the browser scope that allows you to call directly commands to the protocol. 48 | 49 | ```js 50 | browser.cdp(, , ) 51 | ``` 52 | 53 | For example if you want to get the JavaScript coverage of your page you can do the following: 54 | 55 | ```js 56 | it('should take JS coverage', () => { 57 | /** 58 | * enable necessary domains 59 | */ 60 | browser.cdp('Profiler', 'enable') 61 | browser.cdp('Debugger', 'enable') 62 | 63 | /** 64 | * start test coverage profiler 65 | */ 66 | browser.cdp('Profiler', 'startPreciseCoverage', { 67 | callCount: true, 68 | detailed: true 69 | }) 70 | 71 | browser.url('http://google.com') 72 | 73 | /** 74 | * capture test coverage 75 | */ 76 | const { result } = browser.cdp('Profiler', 'takePreciseCoverage') 77 | const coverage = result.filter((res) => res.url !== '') 78 | console.log(coverage) 79 | }) 80 | ``` 81 | 82 | ### Via Event Listener 83 | 84 | In order to capture events in the browser you can register an event listener to a Chrome DevTools event like: 85 | 86 | ```js 87 | it('should listen on network events', () => { 88 | browser.cdp('Network', 'enable') 89 | browser.on('Network.responseReceived', (params) => { 90 | console.log(`Loaded ${params.response.url}`) 91 | }) 92 | browser.url('https://www.google.com') 93 | }) 94 | ``` 95 | 96 | ## Development 97 | 98 | All commands can be found in the package.json. The most important are: 99 | 100 | Watch changes: 101 | 102 | ```sh 103 | $ npm run watch 104 | ``` 105 | 106 | Build package: 107 | 108 | ```sh 109 | $ npm build 110 | ``` 111 | 112 | ---- 113 | 114 | For more information on WebdriverIO see the [homepage](http://webdriver.io). 115 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var DevToolsService = require('./build/devtools') 2 | module.exports = new DevToolsService() 3 | -------------------------------------------------------------------------------- /lib/devtools.js: -------------------------------------------------------------------------------- 1 | import CDP from 'chrome-remote-interface' 2 | import fs from 'fs' 3 | import path from 'path' 4 | 5 | const RE_DEVTOOLS_DEBUGGING_PORT_SWITCH = /--remote-debugging-port=(\d*)/ 6 | const RE_USER_DATA_DIR_SWITCH = /--user-data-dir=([^-]*)/ 7 | 8 | export default class DevToolsService { 9 | beforeSession (_, caps) { 10 | if (caps.browserName !== 'chrome' || (caps.version && caps.version < 63)) { 11 | console.error('The wdio-devtools-service currently only supports Chrome version 63 and up') 12 | } 13 | } 14 | 15 | async before () { 16 | this.chromePort = await this._findChromePort() 17 | this.client = await this._getCDPClient(this.chromePort) 18 | 19 | /** 20 | * allow to easily access the CDP from the browser object 21 | */ 22 | browser.addCommand('cdp', (domain, command, args = {}) => { 23 | if (!this.client[domain]) { 24 | throw new Error(`Domain "${domain}" doesn't exist in the Chrome DevTools protocol`) 25 | } 26 | 27 | if (!this.client[domain][command]) { 28 | throw new Error(`The "${domain}" domain doesn't have a method called "${command}"`) 29 | } 30 | 31 | return new Promise((resolve, reject) => this.client[domain][command](args, (err, result) => { 32 | if (err) { 33 | return reject(new Error(`Chrome DevTools Error: ${result.message}`)) 34 | } 35 | 36 | return resolve(result) 37 | })) 38 | }) 39 | 40 | /** 41 | * helper method to receive Chrome remote debugging connection data to 42 | * e.g. use external tools like lighthouse 43 | */ 44 | const { host, port } = this.client 45 | browser.addCommand('cdpConnection', () => ({ host, port })) 46 | 47 | /** 48 | * propagate CDP events to the browser event listener 49 | */ 50 | this.client.on('event', (event) => browser.emit(event.method || 'event', event.params)) 51 | } 52 | 53 | /** 54 | * Find Chrome DevTools Interface port by checking Chrome switches from the chrome://version 55 | * page. In case a newer version is used (+v65) we check the DevToolsActivePort file 56 | */ 57 | async _findChromePort () { 58 | try { 59 | await browser.url('chrome://version') 60 | const cmdLineText = await browser.getText('#command_line') 61 | let port = parseInt(cmdLineText.match(RE_DEVTOOLS_DEBUGGING_PORT_SWITCH)[1]) 62 | if (port === 0) { 63 | const userDataDir = cmdLineText.match(RE_USER_DATA_DIR_SWITCH)[1].trim() 64 | const devToolsActivePortFile = fs.readFileSync(path.join(userDataDir, 'DevToolsActivePort'), 'utf8') 65 | port = parseInt(devToolsActivePortFile.split('\n').shift(), 10) 66 | } 67 | 68 | return port 69 | } catch (err) { 70 | console.log(`Could not connect to chrome`) 71 | } 72 | } 73 | 74 | async _getCDPClient (port) { 75 | return new Promise((resolve) => CDP({ 76 | port, 77 | host: 'localhost', 78 | target: (targets) => targets.findIndex((t) => t.type === 'page') 79 | }, resolve)) 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "wdio-devtools-service", 3 | "version": "0.1.6", 4 | "description": "WebdriverIO service that enables DevTools functionality for you WebDriver session", 5 | "author": "Christian Bromann ", 6 | "license": "MIT", 7 | "main": "index.js", 8 | "homepage": "https://github.com/webdriverio/wdio-devtools-service#readme", 9 | "keywords": [ 10 | "wdio-service", 11 | "devtools", 12 | "webdriver", 13 | "selenium", 14 | "test", 15 | "wdio" 16 | ], 17 | "repository": { 18 | "type": "git", 19 | "url": "git+https://github.com/webdriverio/wdio-devtools-service.git" 20 | }, 21 | "bugs": { 22 | "url": "https://github.com/webdriverio/wdio-devtools-service/issues" 23 | }, 24 | "scripts": { 25 | "build": "run-s clean compile", 26 | "ci": "run-s test", 27 | "clean": "rm -rf ./build ./coverage", 28 | "compile": "babel lib -d build --source-maps inline", 29 | "eslint": "eslint ./lib ./test ./*.js", 30 | "release": "run-s release:patch", 31 | "release:patch": "np patch", 32 | "release:minor": "np minor", 33 | "release:major": "np major", 34 | "start": "node ./build", 35 | "test": "run-s eslint test:unit", 36 | "test:unit": "jest --coverage", 37 | "watch": "npm run compile -- --watch" 38 | }, 39 | "devDependencies": { 40 | "babel-cli": "^6.26.0", 41 | "babel-core": "^6.26.0", 42 | "babel-eslint": "^8.0.1", 43 | "babel-plugin-add-module-exports": "^0.2.1", 44 | "babel-plugin-transform-class-properties": "^6.24.1", 45 | "babel-plugin-transform-runtime": "^6.23.0", 46 | "babel-preset-env": "^1.6.0", 47 | "babel-preset-stage-0": "^6.24.1", 48 | "eslint": "^4.8.0", 49 | "eslint-config-standard": "^10.2.1", 50 | "eslint-plugin-import": "^2.7.0", 51 | "eslint-plugin-node": "^5.2.0", 52 | "eslint-plugin-promise": "^3.5.0", 53 | "eslint-plugin-standard": "^3.0.1", 54 | "jest": "^21.2.1", 55 | "np": "^2.16.0", 56 | "npm-run-all": "^4.1.2", 57 | "shelljs": "^0.7.8" 58 | }, 59 | "jest": { 60 | "coverageThreshold": { 61 | "global": { 62 | "branches": 90, 63 | "functions": 80, 64 | "lines": 90, 65 | "statements": 90 66 | } 67 | } 68 | }, 69 | "dependencies": { 70 | "babel-runtime": "^6.26.0", 71 | "chrome-remote-interface": "^0.25.3", 72 | "node-netstat": "^1.4.2" 73 | } 74 | } 75 | --------------------------------------------------------------------------------