├── .gitignore ├── .jshintrc ├── .npm └── package │ ├── .gitignore │ ├── README │ └── npm-shrinkwrap.json ├── .versions ├── History.md ├── LICENCE ├── README.md ├── lib ├── LongRunningChildProcess.coffee ├── meteor │ └── files.js └── spawner.js ├── package.js ├── polyfills └── url.js ├── server.js └── versions.json /.gitignore: -------------------------------------------------------------------------------- 1 | .build* 2 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "node": true, 3 | "browser": false, 4 | "esnext": false, 5 | "bitwise": true, 6 | "camelcase": false, 7 | "curly": true, 8 | "eqeqeq": true, 9 | "immed": true, 10 | "indent": 2, 11 | "latedef": "nofunc", 12 | "newcap": true, 13 | "noarg": true, 14 | "quotmark": "single", 15 | "undef": true, 16 | "unused": true, 17 | "strict": true, 18 | "globalstrict": true, 19 | "trailing": true, 20 | "smarttabs": true, 21 | "globals": { 22 | "process": false, 23 | "Meteor": false, 24 | "Assets": false, 25 | "Deps": false, 26 | "DDP": false, 27 | "check": false, 28 | "Npm": false, 29 | "Package": false, 30 | "DEBUG": false, 31 | "Velocity": false, 32 | "VelocityTestFiles": false, 33 | "VelocityTestReports": false, 34 | "VelocityAggregateReports": false, 35 | "VelocityLogs": false 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /.npm/package/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.npm/package/README: -------------------------------------------------------------------------------- 1 | This directory and the files immediately inside it are automatically generated 2 | when you change this package's NPM dependencies. Commit the files in this 3 | directory (npm-shrinkwrap.json, .gitignore, and this README) to source control 4 | so that others run the same versions of sub-dependencies. 5 | 6 | You should NOT check in the node_modules directory that Meteor automatically 7 | creates; if you are using git, the .gitignore file tells git to ignore it. 8 | -------------------------------------------------------------------------------- /.npm/package/npm-shrinkwrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "chromedriver": { 4 | "version": "2.14.1", 5 | "dependencies": { 6 | "adm-zip": { 7 | "version": "0.2.1" 8 | }, 9 | "kew": { 10 | "version": "0.1.7" 11 | }, 12 | "npmconf": { 13 | "version": "0.0.24", 14 | "dependencies": { 15 | "config-chain": { 16 | "version": "1.1.8", 17 | "dependencies": { 18 | "proto-list": { 19 | "version": "1.2.3" 20 | } 21 | } 22 | }, 23 | "inherits": { 24 | "version": "1.0.0" 25 | }, 26 | "once": { 27 | "version": "1.1.1" 28 | }, 29 | "osenv": { 30 | "version": "0.0.3" 31 | }, 32 | "nopt": { 33 | "version": "2.2.1", 34 | "dependencies": { 35 | "abbrev": { 36 | "version": "1.0.5" 37 | } 38 | } 39 | }, 40 | "semver": { 41 | "version": "1.1.4" 42 | }, 43 | "ini": { 44 | "version": "1.1.0" 45 | } 46 | } 47 | }, 48 | "mkdirp": { 49 | "version": "0.3.5" 50 | }, 51 | "rimraf": { 52 | "version": "2.0.3", 53 | "dependencies": { 54 | "graceful-fs": { 55 | "version": "1.1.14" 56 | } 57 | } 58 | } 59 | } 60 | }, 61 | "fs-extra": { 62 | "version": "0.12.0", 63 | "dependencies": { 64 | "ncp": { 65 | "version": "0.6.0" 66 | }, 67 | "mkdirp": { 68 | "version": "0.5.0", 69 | "dependencies": { 70 | "minimist": { 71 | "version": "0.0.8" 72 | } 73 | } 74 | }, 75 | "jsonfile": { 76 | "version": "2.0.0" 77 | }, 78 | "rimraf": { 79 | "version": "2.2.8" 80 | } 81 | } 82 | }, 83 | "webdriverio": { 84 | "version": "2.4.5", 85 | "dependencies": { 86 | "archiver": { 87 | "version": "0.6.1", 88 | "dependencies": { 89 | "readable-stream": { 90 | "version": "1.0.33", 91 | "dependencies": { 92 | "core-util-is": { 93 | "version": "1.0.1" 94 | }, 95 | "isarray": { 96 | "version": "0.0.1" 97 | }, 98 | "string_decoder": { 99 | "version": "0.10.31" 100 | }, 101 | "inherits": { 102 | "version": "2.0.1" 103 | } 104 | } 105 | }, 106 | "zip-stream": { 107 | "version": "0.2.3", 108 | "dependencies": { 109 | "lodash.defaults": { 110 | "version": "2.4.1", 111 | "dependencies": { 112 | "lodash.keys": { 113 | "version": "2.4.1", 114 | "dependencies": { 115 | "lodash._isnative": { 116 | "version": "2.4.1" 117 | }, 118 | "lodash.isobject": { 119 | "version": "2.4.1" 120 | }, 121 | "lodash._shimkeys": { 122 | "version": "2.4.1" 123 | } 124 | } 125 | }, 126 | "lodash._objecttypes": { 127 | "version": "2.4.1" 128 | } 129 | } 130 | }, 131 | "debug": { 132 | "version": "0.7.4" 133 | } 134 | } 135 | }, 136 | "lazystream": { 137 | "version": "0.1.0" 138 | }, 139 | "file-utils": { 140 | "version": "0.1.5", 141 | "dependencies": { 142 | "lodash": { 143 | "version": "2.1.0" 144 | }, 145 | "iconv-lite": { 146 | "version": "0.2.11" 147 | }, 148 | "rimraf": { 149 | "version": "2.2.8" 150 | }, 151 | "glob": { 152 | "version": "3.2.11", 153 | "dependencies": { 154 | "inherits": { 155 | "version": "2.0.1" 156 | }, 157 | "minimatch": { 158 | "version": "0.3.0", 159 | "dependencies": { 160 | "lru-cache": { 161 | "version": "2.5.0" 162 | }, 163 | "sigmund": { 164 | "version": "1.0.0" 165 | } 166 | } 167 | } 168 | } 169 | }, 170 | "minimatch": { 171 | "version": "0.2.14", 172 | "dependencies": { 173 | "lru-cache": { 174 | "version": "2.5.0" 175 | }, 176 | "sigmund": { 177 | "version": "1.0.0" 178 | } 179 | } 180 | }, 181 | "findup-sync": { 182 | "version": "0.1.3", 183 | "dependencies": { 184 | "lodash": { 185 | "version": "2.4.1" 186 | } 187 | } 188 | }, 189 | "isbinaryfile": { 190 | "version": "0.1.9" 191 | } 192 | } 193 | }, 194 | "lodash": { 195 | "version": "2.4.1" 196 | } 197 | } 198 | }, 199 | "async": { 200 | "version": "0.9.0" 201 | }, 202 | "chainit": { 203 | "version": "2.1.1", 204 | "dependencies": { 205 | "queue": { 206 | "version": "1.0.2" 207 | } 208 | } 209 | }, 210 | "css-parse": { 211 | "version": "1.7.0" 212 | }, 213 | "css-value": { 214 | "version": "0.0.1" 215 | }, 216 | "deepmerge": { 217 | "version": "0.2.7" 218 | }, 219 | "pragma-singleton": { 220 | "version": "1.0.3" 221 | }, 222 | "q": { 223 | "version": "1.2.0" 224 | }, 225 | "request": { 226 | "version": "2.34.0", 227 | "dependencies": { 228 | "qs": { 229 | "version": "0.6.6" 230 | }, 231 | "json-stringify-safe": { 232 | "version": "5.0.0" 233 | }, 234 | "forever-agent": { 235 | "version": "0.5.2" 236 | }, 237 | "node-uuid": { 238 | "version": "1.4.2" 239 | }, 240 | "mime": { 241 | "version": "1.2.11" 242 | }, 243 | "tough-cookie": { 244 | "version": "0.12.1", 245 | "dependencies": { 246 | "punycode": { 247 | "version": "1.3.2" 248 | } 249 | } 250 | }, 251 | "form-data": { 252 | "version": "0.1.4", 253 | "dependencies": { 254 | "combined-stream": { 255 | "version": "0.0.7", 256 | "dependencies": { 257 | "delayed-stream": { 258 | "version": "0.0.5" 259 | } 260 | } 261 | } 262 | } 263 | }, 264 | "tunnel-agent": { 265 | "version": "0.3.0" 266 | }, 267 | "http-signature": { 268 | "version": "0.10.1", 269 | "dependencies": { 270 | "assert-plus": { 271 | "version": "0.1.5" 272 | }, 273 | "asn1": { 274 | "version": "0.1.11" 275 | }, 276 | "ctype": { 277 | "version": "0.5.3" 278 | } 279 | } 280 | }, 281 | "oauth-sign": { 282 | "version": "0.3.0" 283 | }, 284 | "hawk": { 285 | "version": "1.0.0", 286 | "dependencies": { 287 | "hoek": { 288 | "version": "0.9.1" 289 | }, 290 | "boom": { 291 | "version": "0.4.2" 292 | }, 293 | "cryptiles": { 294 | "version": "0.2.2" 295 | }, 296 | "sntp": { 297 | "version": "0.2.4" 298 | } 299 | } 300 | }, 301 | "aws-sign2": { 302 | "version": "0.5.0" 303 | } 304 | } 305 | }, 306 | "rgb2hex": { 307 | "version": "0.1.0" 308 | }, 309 | "url": { 310 | "version": "0.10.2", 311 | "dependencies": { 312 | "punycode": { 313 | "version": "1.3.2" 314 | } 315 | } 316 | }, 317 | "wgxpath": { 318 | "version": "0.23.0" 319 | } 320 | } 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /.versions: -------------------------------------------------------------------------------- 1 | coffeescript@1.0.5 2 | meteor@1.1.4 3 | practicalmeteor:chai@1.9.2_3 4 | practicalmeteor:loglevel@1.1.0_3 5 | underscore@1.0.2 6 | xolvio:webdriver@0.2.5 7 | -------------------------------------------------------------------------------- /History.md: -------------------------------------------------------------------------------- 1 | #0.5.2 2 | 3 | * Increased phantom startup timeout 4 | 5 | #0.5.1 6 | 7 | * Fixed chrome driver npm path issue 8 | 9 | #0.5.0 10 | 11 | * Added ChromeDriver support 12 | 13 | #0.4.2 14 | 15 | * Fixed ChromeDriver issue (early commit!) 16 | 17 | #0.4.1 18 | 19 | * Added DEBUG_WEBDRIVER for only showing webdriver debug logs 20 | * Fixed false error reporting from phantom process 21 | * Made phantom poly-filling synchronous 22 | * Added debugging messages 23 | 24 | #0.4.0 25 | 26 | * Compromised speed for stability in this release 27 | * Now uses the same phantomjs as the installed meteor tools (no npm download!) 28 | * Reworked phantomjs spwaning so that it kills all phantomjs processes prior to starting (kill -9 so beware!) 29 | * Rewrote the screen grabbing on-error to maintain the event chain (plays nicer with cucumber) 30 | * Reduced (eliminated?) restarts when a webdriver error is encountered 31 | 32 | #0.3.1 33 | 34 | * Improved error visibility and log-level 35 | 36 | #0.3.0 37 | 38 | * Phantom polyfill capability 39 | * Added helper commands: 40 | waitForPresent - Ensures elements exist and are visible 41 | waitForAndClick - Runs a waitForPresent then clicks an element 42 | takeScreenshot - adds some smarts over the standard saveScreenshot command 43 | typeInto - Clicks an element then types keys 44 | * Automatically takes a screenshot when webdriver encounters an error 45 | * Bumped webdriver version 2.4.5 46 | * Bumped phantomjs version to 1.9.15 47 | 48 | #0.2.1 / 0.2.2 49 | 50 | * Add a PHANTOM_PATH environment variable to help with CI environments. 51 | 52 | #0.2.0 53 | 54 | * WebdriverIO can now run inside a mirror 55 | 56 | #0.1.8 57 | 58 | * Hotfix: Path issue 59 | 60 | #0.1.7 61 | 62 | * Clash with Jasmine Spawner 63 | 64 | #0.1.6 65 | 66 | * Fixed issue in child process 67 | 68 | #0.1.5 69 | 70 | * Using latest LongRunningChildProcess from Sanjo 71 | 72 | #0.1.4 73 | 74 | * Update to the spawn approach 75 | 76 | #0.1.3 77 | 78 | * Fixed Phantom spawning and timing issues 79 | 80 | #0.1.1 - 0.1.2 81 | 82 | * Void (connection issues messed up build on package server) 83 | 84 | #0.1.0 85 | 86 | * Actually using semvar now 87 | * Replaced process starting with practicalmeteor ChildProcessFactory 88 | 89 | #0.0.4 90 | Fixed phantom message 91 | 92 | #0.0.3 93 | No longer depends on Velocity 94 | 95 | #0.0.2 96 | Improved log messaging 97 | 98 | #0.0.1 99 | Initial release 100 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Xolv.io 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | 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, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | meteor-webdriver 2 | ================ 3 | 4 | ## THIS PACKAGE IS NO LONGER MAINTAINED Use at your own risk :) You might consider using [Chimp](https://github.com/xolvio/chimp) instead. 5 | We plan to put end-to-end testing inside Jasmine in the near future, you can speed this up by sponsoring this release. [See this page for details](http://xolv.io/services/velocity-premium-support/) 6 | 7 | A [WebdriverIO](http://webdriver.io) wrapper for UI testing using any testing framework. 8 | 9 | 1. Starts PhantomJS in webdriver mode 10 | 2. Provides you with a browser you can automate using the industry-standard Selenium Webdriver 11 | 12 | # Get the Book 13 | To learn more about testing with Meteor, consider purchasing our book [The Meteor Testing Manual](http://www.meteortesting.com/?utm_source=webdriver&utm_medium=banner&utm_campaign=webdriver). 14 | 15 | [![Meteor Testing Manual](http://www.meteortesting.com/img/tmtm.gif)](http://www.meteortesting.com/?utm_source=webdriver&utm_medium=banner&utm_campaign=webdriver) 16 | 17 | Your support helps us continue our work on Velocity and related frameworks. 18 | 19 | ## Installation 20 | 21 | ```sh 22 | meteor add xolvio:webdriver 23 | ``` 24 | 25 | ## Usage 26 | 27 | The following examples are for Mocha. You can also [use Webdriver.io with Jasmine](https://github.com/Sanjo/meteor-jasmine/wiki/End-to-End-Tests). 28 | 29 | ```javascript 30 | 31 | describe('Browser testing', function(done) { 32 | 33 | var _browser; 34 | 35 | before(function (done) { 36 | wdio.getGhostDriver(function(browser) { 37 | _browser = browser; 38 | done(); 39 | }); 40 | }) 41 | 42 | it('should have the correct title', function (done) { 43 | _browser. 44 | init(). 45 | url('http://www.google.com'). 46 | title(function(err, res) { 47 | console.log('Title was: ' + res.value); 48 | }). 49 | end(). 50 | call(done); 51 | }); 52 | 53 | }); 54 | 55 | ``` 56 | You can also use ChromeDriver like this (no need to download anything!): 57 | 58 | ```javascript 59 | 60 | describe('Browser testing', function (done) { 61 | 62 | var _browser; 63 | 64 | before(function(done) { 65 | wdio.getChromeDriver(function (browser) { 66 | _browser = browser; 67 | done(); 68 | }); 69 | }) 70 | 71 | // see above 72 | 73 | }); 74 | 75 | ``` 76 | 77 | 78 | For more examples and usage, see the [webdriver.io website](http://webdriver.io). 79 | 80 | ## Phantom.js and CI 81 | 82 | If this package is included for testing your Meteor app, it should work fine out of the box. You may encounter issues when you try 83 | to run on the CI server, as it doesn't seem to detect the right phantom.js binary to download. You can instead just `npm install -g phantomjs` 84 | and then set `PHANTOM_PATH` as an environment variable when you run your Meteor CI test build. 85 | 86 | ## Package Roadmap 87 | 88 | - [x] [WebdriverIO](http://webdriver.io) 89 | - [x] Use PhantomJS in GhostDriver mode 90 | - [x] Automatically Download [ChromeDriver](https://code.google.com/p/selenium/wiki/ChromeDriver) 91 | - [x] Automatically Download [Selenium Server](http://www.seleniumhq.org/download/) 92 | - [ ] Reuse the selenium webdriver session between tests so the browser does not flicker on and off 93 | - [ ] Support multiple window testing 94 | - [ ] Specify the browser matrix to run in development 95 | - [ ] Specify the browser matrix to run in continuous integration mode 96 | - [ ] SauceLabs & BrowserStack support 97 | -------------------------------------------------------------------------------- /lib/LongRunningChildProcess.coffee: -------------------------------------------------------------------------------- 1 | fs = Npm.require 'fs-extra' 2 | path = Npm.require 'path' 3 | assert = Npm.require 'assert' 4 | spawn = Npm.require('child_process').spawn 5 | 6 | @sanjo3 ?= {} 7 | 8 | class sanjo3.LongRunningChildProcess 9 | 10 | taskName: null 11 | child: null 12 | # Cache the pid, so we don't read the file each time. 13 | # This object should be the only one who writes to the pid file. 14 | pid: null 15 | dead: false 16 | 17 | constructor: (taskName) -> 18 | log.debug "LongRunningChildProcess.constructor(taskName=#{taskName})" 19 | 20 | @taskName = taskName 21 | @pid = @readPid() 22 | 23 | 24 | getTaskName: -> 25 | @taskName 26 | 27 | 28 | getChild: -> 29 | @child 30 | 31 | 32 | getPid: -> 33 | @pid 34 | 35 | 36 | _setPid: (pid) -> 37 | log.debug "LongRunningChildProcess._setPid(pid=#{pid})" 38 | @pid = pid 39 | log.debug "Saving #{@taskName} pid #{pid} to #{@_getPidFilePath()}" 40 | fs.outputFile(@_getPidFilePath(), "#{pid}") 41 | 42 | 43 | isDead: -> 44 | @dead 45 | 46 | 47 | isRunning: -> 48 | pid = @getPid() 49 | 50 | if not pid 51 | log.debug "LongRunningChildProcess.isRunning returns false" 52 | return false 53 | 54 | try 55 | # Check for the existence of the process without killing it, by sending signal 0. 56 | process.kill(pid, 0) 57 | # process is alive, otherwise an exception would have been thrown, so we need to exit. 58 | log.debug "LongRunningChildProcess.isRunning returns true" 59 | return true 60 | catch err 61 | log.trace err 62 | log.debug "LongRunningChildProcess.isRunning returns false" 63 | return false 64 | 65 | 66 | # Returns the pid of the main Meteor app process 67 | _getMeteorPid: -> 68 | parentPid = null 69 | # For Meteor < 1.0.3 70 | parentPidIndex = _.indexOf(process.argv, '--parent-pid') 71 | if parentPidIndex != -1 72 | parentPid = process.argv[parentPidIndex + 1] 73 | log.debug("The pid of the main Meteor app process is #{parentPid}") 74 | # For Meteor >= 1.0.3 75 | else if process.env.METEOR_PARENT_PID 76 | parentPid = process.env.METEOR_PARENT_PID 77 | log.debug("The pid of the main Meteor app process is #{parentPid}") 78 | else 79 | log.error('Could not find the pid of the main Meteor app process') 80 | 81 | return parentPid 82 | 83 | 84 | _getMeteorAppPath: -> 85 | @appPath = path.resolve(xFindAppDir()) if not @appPath 86 | return @appPath 87 | 88 | 89 | _getMeteorLocalPath: -> 90 | path.join(@_getMeteorAppPath(), '.meteor/local') 91 | 92 | 93 | _getPidFilePath: -> 94 | path.join(@_getMeteorLocalPath(), "run/#{@taskName}.pid") 95 | 96 | 97 | _getLogFilePath: -> 98 | path.join(@_getMeteorLocalPath(), "log/#{@taskName}.log") 99 | 100 | 101 | _getSpawnScriptPath: -> 102 | path.join(@_getMeteorLocalPath(), 103 | 'build/programs/server/assets/packages/xolvio_webdriver/lib/spawner.js' 104 | ) 105 | 106 | 107 | readPid: -> 108 | log.debug('LongRunningChildProcess.readPid()') 109 | try 110 | pid = parseInt(fs.readFileSync(@_getPidFilePath(), {encoding: 'utf8'}, 10)) 111 | log.debug("LongRunningChildProcess.readPid returns #{pid}") 112 | return pid 113 | catch err 114 | log.debug('LongRunningChildProcess.readPid returns null') 115 | return null 116 | 117 | 118 | spawn: (options) -> 119 | log.debug "LongRunningChildProcess.spawn()", options 120 | check options, Match.ObjectIncluding({ 121 | killSignals: Match.Optional([String]) 122 | logToConsole: Match.Optional(Boolean) 123 | command: String 124 | args: [String] 125 | options: Match.Optional(Match.ObjectIncluding({ 126 | cwd: Match.Optional(String) 127 | env: Match.Optional(Object) 128 | })) 129 | } 130 | ) 131 | 132 | if @isRunning() 133 | return false 134 | 135 | logFile = @_getLogFilePath() 136 | fs.ensureDirSync(path.dirname(logFile)) 137 | #@fout = fs.openSync(logFile, 'w') 138 | #@ferr = fs.openSync(logFile, 'w') 139 | 140 | spawnOptions = { 141 | cwd: @_getMeteorAppPath(), 142 | env: process.env, 143 | detached: true, 144 | stdio: ['ignore', 'pipe', 'pipe'] 145 | } 146 | command = path.basename options.command 147 | spawnScript = @_getSpawnScriptPath() 148 | commandArgs = [spawnScript, @_getMeteorPid(), options.command].concat(options.args) 149 | fs.chmodSync(spawnScript, 0o544) 150 | 151 | log.debug("LongRunningChildProcess.spawn is spawning '#{command}'") 152 | 153 | nodePath = process.execPath 154 | @child = spawn(nodePath, commandArgs, spawnOptions) 155 | @dead = false 156 | @_setPid(@child.pid) 157 | 158 | @child.on "exit", (code) => 159 | log.debug "LongRunningChildProcess: child_process.on 'exit': command=#{command} code=#{code}" 160 | fs.closeSync(@fout) 161 | 162 | return true 163 | 164 | 165 | kill: (signal = "SIGINT") -> 166 | log.debug "LongRunningChildProcess.kill(signal=#{signal})" 167 | 168 | unless @dead 169 | try 170 | # Providing a negative pid will kill the entire process group, 171 | # i.e. the process and all it's children 172 | # See man kill for more info 173 | #process.kill(-@child.pid, signal) 174 | if @child? 175 | @child.kill(signal) 176 | else 177 | pid = @getPid() 178 | process.kill(pid, signal) 179 | @dead = true 180 | @pid = null 181 | fs.removeSync(@_getPidFilePath()) 182 | catch err 183 | log.warn "Error: While killing process:\n", err 184 | -------------------------------------------------------------------------------- /lib/meteor/files.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Copied from Meteor tools/files.js. 4 | * 5 | * Includes: 6 | * - Helper to find the app root path 7 | */ 8 | 9 | var path = Npm.require('path'); 10 | var fs = Npm.require('fs'); 11 | 12 | // given a predicate function and a starting path, traverse upwards 13 | // from the path until we find a path that satisfies the predicate. 14 | // 15 | // returns either the path to the lowest level directory that passed 16 | // the test or null for none found. if starting path isn't given, use 17 | // cwd. 18 | var xFindUpwards = function (predicate, startPath) { 19 | var testDir = startPath || process.cwd(); 20 | while (testDir) { 21 | if (predicate(testDir)) { 22 | break; 23 | } 24 | var newDir = path.dirname(testDir); 25 | if (newDir === testDir) { 26 | testDir = null; 27 | } else { 28 | testDir = newDir; 29 | } 30 | } 31 | if (!testDir) 32 | return null; 33 | 34 | return testDir; 35 | }; 36 | 37 | // Determine if 'filepath' (a path, or omit for cwd) is within an app 38 | // directory. If so, return the top-level app directory. 39 | xFindAppDir = function (filepath) { 40 | var isAppDir = function (filepath) { 41 | // XXX once we are done with the transition to engine, this should 42 | // change to: `return fs.existsSync(path.join(filepath, '.meteor', 43 | // 'release'))` 44 | 45 | // .meteor/packages can be a directory, if .meteor is a warehouse 46 | // directory. since installing meteor initializes a warehouse at 47 | // $HOME/.meteor, we want to make sure your home directory (and all 48 | // subdirectories therein) don't count as being within a meteor app. 49 | try { // use try/catch to avoid the additional syscall to fs.existsSync 50 | return fs.statSync(path.join(filepath, '.meteor', 'packages')).isFile(); 51 | } catch (e) { 52 | return false; 53 | } 54 | }; 55 | 56 | return xFindUpwards(isAppDir, filepath); 57 | }; -------------------------------------------------------------------------------- /lib/spawner.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This script must be called like this: 3 | * node childProcessScript.js 4 | * 5 | * You can terminate this script by sending a SIGINT signal to it. 6 | * It will also terminate automatically when the process with the given 7 | * parent PID no longer runs. 8 | */ 9 | 10 | DEBUG = !!process.env.VELOCITY_DEBUG; 11 | 12 | /** 13 | * This is the same mechanism that Meteor currently use for parent alive checking. 14 | * @see webapp package. 15 | */ 16 | var startCheckForLiveParent = function (parentPid) { 17 | setInterval(function () { 18 | try { 19 | process.kill(parentPid, 0) 20 | } catch (err) { 21 | DEBUG && console.log("Parent process is dead! Exiting.") 22 | childProcess.kill('SIGINT') 23 | process.exit(1) 24 | } 25 | }, 3000) 26 | } 27 | 28 | var parentPid = process.argv[2] 29 | var command = process.argv[3] 30 | var commandArgumentsz = process.argv.slice(4) 31 | 32 | DEBUG && console.log('Spawn script arguments:') 33 | DEBUG && console.log('parentPid:', parentPid) 34 | DEBUG && console.log('command:', command) 35 | DEBUG && console.log('commandArguments:', commandArgumentsz) 36 | 37 | var spawn = require('child_process').spawn 38 | var childProcess = spawn(command, commandArgumentsz, { 39 | cwd: process.cwd, 40 | env: process.env, 41 | stdio: 'inherit' 42 | }) 43 | 44 | startCheckForLiveParent(parentPid) 45 | 46 | process.on('SIGINT', function () { 47 | DEBUG && console.log('Received SIGINT. Exiting spawn script.') 48 | childProcess.kill('SIGINT') 49 | process.exit(1) 50 | }) -------------------------------------------------------------------------------- /package.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | 'use strict'; 4 | 5 | Package.describe({ 6 | name: 'xolvio:webdriver', 7 | summary: 'Webdriver.io for Meteor', 8 | version: '0.5.2', 9 | git: 'https://github.com/xolvio/meteor-webdriver.git', 10 | debugOnly: true 11 | }); 12 | 13 | Npm.depends({ 14 | 'webdriverio': '2.4.5', 15 | 'fs-extra': '0.12.0', 16 | 'chromedriver': '2.14.1', 17 | 'phantomjs-bin': '1.0.1' 18 | 19 | // TODO add support for these 20 | //'chai': '2.0.0', 21 | //'chai-as-promised': '4.2.0' 22 | //'selenium-standalone': '2.43.1-5', 23 | }); 24 | 25 | Package.onUse(function (api) { 26 | api.use('underscore@1.0.2', 'server'); 27 | api.use('coffeescript@1.0.4', 'server'); 28 | 29 | api.addFiles([ 30 | 'lib/meteor/files.js', 31 | 'server.js' 32 | ], 'server'); 33 | api.addFiles(['lib/spawner.js'], 'server', {isAsset: true}); 34 | 35 | // PhantomJS Polyfills 36 | api.add_files(['polyfills/url.js'], 'server', {isAsset: true}); 37 | 38 | api.export('wdio', 'server'); 39 | }); 40 | 41 | })(); 42 | -------------------------------------------------------------------------------- /polyfills/url.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 3 | if (!window) { 4 | return; 5 | } 6 | 7 | if (window.URL && window.URL.prototype && ('href' in window.URL.prototype)) 8 | return; 9 | 10 | function URL(url, base) { 11 | if (!url) 12 | throw new TypeError('Invalid argument'); 13 | 14 | var doc = document.implementation.createHTMLDocument(''); 15 | if (base) { 16 | var baseElement = doc.createElement('base'); 17 | baseElement.href = base; 18 | doc.head.appendChild(baseElement); 19 | } 20 | var anchorElement = doc.createElement('a'); 21 | anchorElement.href = url; 22 | doc.body.appendChild(anchorElement); 23 | 24 | if (anchorElement.protocol === ':' || !/:/.test(anchorElement.href)) 25 | throw new TypeError('Invalid URL'); 26 | 27 | Object.defineProperty(this, '_anchorElement', {value: anchorElement}); 28 | } 29 | 30 | URL.prototype = { 31 | toString: function () { 32 | return this.href; 33 | }, 34 | 35 | get href() { 36 | return this._anchorElement.href; 37 | }, 38 | set href(value) { 39 | this._anchorElement.href = value; 40 | }, 41 | 42 | get protocol() { 43 | return this._anchorElement.protocol; 44 | }, 45 | set protocol(value) { 46 | this._anchorElement.protocol = value; 47 | }, 48 | 49 | // NOT IMPLEMENTED 50 | // get username() { 51 | // return this._anchorElement.username; 52 | // }, 53 | // set username(value) { 54 | // this._anchorElement.username = value; 55 | // }, 56 | 57 | // get password() { 58 | // return this._anchorElement.password; 59 | // }, 60 | // set password(value) { 61 | // this._anchorElement.password = value; 62 | // }, 63 | 64 | // get origin() { 65 | // return this._anchorElement.origin; 66 | // }, 67 | 68 | get host() { 69 | return this._anchorElement.host; 70 | }, 71 | set host(value) { 72 | this._anchorElement.host = value; 73 | }, 74 | 75 | get hostname() { 76 | return this._anchorElement.hostname; 77 | }, 78 | set hostname(value) { 79 | this._anchorElement.hostname = value; 80 | }, 81 | 82 | get port() { 83 | return this._anchorElement.port; 84 | }, 85 | set port(value) { 86 | this._anchorElement.port = value; 87 | }, 88 | 89 | get pathname() { 90 | return this._anchorElement.pathname; 91 | }, 92 | set pathname(value) { 93 | this._anchorElement.pathname = value; 94 | }, 95 | 96 | get search() { 97 | return this._anchorElement.search; 98 | }, 99 | set search(value) { 100 | this._anchorElement.search = value; 101 | }, 102 | 103 | get hash() { 104 | return this._anchorElement.hash; 105 | }, 106 | set hash(value) { 107 | this._anchorElement.hash = value; 108 | } 109 | }; 110 | 111 | var oldURL = window.URL || window.webkitURL || window.mozURL; 112 | 113 | URL.createObjectURL = function (blob) { 114 | return oldURL.createObjectURL.apply(oldURL, arguments); 115 | }; 116 | 117 | URL.revokeObjectURL = function (url) { 118 | return oldURL.revokeObjectURL.apply(oldURL, arguments); 119 | }; 120 | 121 | Object.defineProperty(URL.prototype, 'toString', {enumerable: false}); 122 | 123 | window.URL = URL; 124 | })(); -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | /*jshint -W117, -W030, -W016, -W061 */ 2 | /* global 3 | DEBUG:true, 4 | */ 5 | 6 | wdio = {}; 7 | 8 | DEBUG = !!process.env.WEBDRIVER_DEBUG || !!process.env.VELOCITY_DEBUG; 9 | 10 | (function () { 11 | 'use strict'; 12 | 13 | if (process.env.NODE_ENV !== 'development' || process.env.VELOCITY === '0') { 14 | return; 15 | } 16 | 17 | var path = Npm.require('path'), 18 | spawn = Npm.require('child_process').spawn, 19 | phantomjs = Npm.require('phantomjs-bin'), 20 | _screenshotCounter = 0; 21 | 22 | wdio.chromedriver = Npm.require('chromedriver'); 23 | wdio.instance = Npm.require('webdriverio'); 24 | var _phantomPath = process.env.PHANTOM_PATH || phantomjs.path; 25 | 26 | var defaultPhantomOptions = { 27 | desiredCapabilities: {browserName: 'PhantomJs'}, 28 | port: 4444, 29 | logLevel: 'silent', 30 | implicitWait: 5000 31 | }; 32 | 33 | var defaultChromeDriverOptions = { 34 | host: 'localhost', 35 | port: 9515, 36 | logLevel: 'silent', 37 | desiredCapabilities: { 38 | browserName: 'chrome' 39 | } 40 | }; 41 | 42 | wdio.startChromeDriver = Meteor.bindEnvironment(function (callback) { 43 | DEBUG && console.log('[xolvio:webdriver] startChromeDriver called'); 44 | wdio.killAllProcesses('chromedriver', function () { 45 | _startChromeDriver(callback); 46 | }); 47 | }); 48 | 49 | wdio.getChromeDriverRemote = Meteor.bindEnvironment(function (options, callback) { 50 | DEBUG && console.log('[xolvio:webdriver] getGhostDriverRemote called'); 51 | if (typeof options === 'function') { 52 | callback = options; 53 | options = {}; 54 | } 55 | options = _.defaults(options, defaultPhantomOptions); 56 | DEBUG && console.log('[xolvio:webdriver] getChromeDriverRemote options', options); 57 | var browser = wdio.instance.remote(options); 58 | _augmentedBrowser(browser, options); 59 | DEBUG && console.log('[xolvio:webdriver] finished getChromeDriverRemote'); 60 | callback(browser); 61 | }); 62 | 63 | wdio.getChromeDriver = Meteor.bindEnvironment(function (options, callback) { 64 | DEBUG && console.log('[xolvio:webdriver] getChromeDriver called'); 65 | if (typeof options === 'function') { 66 | callback = options; 67 | options = {}; 68 | } 69 | options = _.defaults(options, defaultChromeDriverOptions); 70 | wdio.startChromeDriver(function () { 71 | wdio.getChromeDriverRemote(options, callback); 72 | }); 73 | }); 74 | 75 | wdio.startPhantom = Meteor.bindEnvironment(function (options, callback) { 76 | if (typeof options === 'function') { 77 | callback = options; 78 | options = {}; 79 | } 80 | wdio.killAllProcesses('phantomjs', function () { 81 | _startPhantom(options.port, callback); 82 | }); 83 | }); 84 | 85 | wdio.getGhostDriverRemote = Meteor.bindEnvironment(function (options, callback) { 86 | 87 | DEBUG && console.log('[xolvio:webdriver] getGhostDriverRemote called'); 88 | 89 | if (typeof options === 'function') { 90 | callback = options; 91 | options = {}; 92 | } 93 | options = _.defaults(options, defaultPhantomOptions); 94 | DEBUG && console.log('[xolvio:webdriver] getGhostDriverRemote options', options); 95 | var browser = wdio.instance.remote(options); 96 | _polyfillPhantom(browser); 97 | _augmentedBrowser(browser, options); 98 | DEBUG && console.log('[xolvio:webdriver] finished getGhostDriverRemote'); 99 | callback(browser); 100 | }); 101 | 102 | wdio.getGhostDriver = Meteor.bindEnvironment(function (options, callback) { 103 | 104 | DEBUG && console.log('[xolvio:webdriver] getGhostDriver called'); 105 | 106 | if (typeof options === 'function') { 107 | callback = options; 108 | options = {}; 109 | } 110 | options = _.defaults(options, defaultPhantomOptions); 111 | 112 | wdio.startPhantom(options, function () { 113 | wdio.getGhostDriverRemote(options, callback); 114 | }); 115 | 116 | }); 117 | 118 | wdio.killAllProcesses = function (procName, callback) { 119 | spawn('pkill', ['-9', procName]).on('close', Meteor.bindEnvironment(callback)); 120 | }; 121 | 122 | function _polyfillPhantom(browser) { 123 | 124 | DEBUG && console.log('[xolvio:webdriver] Applying phantom polyfills'); 125 | var polyfills = []; 126 | polyfills.push(Assets.getText('polyfills/url.js')); 127 | 128 | var _url = browser.url; 129 | browser.url = function () { 130 | if (typeof arguments[arguments.length - 1] === 'function') { 131 | // this is a URL get not a navigation so do nothing 132 | return _url.apply(this, arguments); 133 | } 134 | var retVal = _url.apply(this, arguments); 135 | _.each(polyfills, function (code, index) { 136 | Meteor.wrapAsync(_applyPolyfill, browser, code, index); 137 | }); 138 | return retVal; 139 | }; 140 | DEBUG && console.log('[xolvio:webdriver] Done applying phantom polyfills'); 141 | } 142 | 143 | function _applyPolyfill(browser, code, index, callback) { 144 | browser.execute(function (code) { 145 | eval(code); 146 | }, code, function (err) { 147 | if (err) { 148 | console.error('Error applying polyfill ' + index + ', err'); 149 | } 150 | DEBUG && console.log('Applied polyfill', index); 151 | callback(); 152 | }); 153 | } 154 | 155 | function _augmentedBrowser(browser, options) { 156 | 157 | DEBUG && console.log('[xolvio:webdriver] Augmenting browser functions'); 158 | 159 | browser. 160 | addCommand('waitForPresent', function (selector, cb) { 161 | this 162 | .waitForExist(selector, options.implicitWait) 163 | .waitForVisible(selector, options.implicitWait) 164 | .call(cb); 165 | }). 166 | addCommand('waitForAndClick', function (selector, cb) { 167 | this 168 | .waitForPresent(selector) 169 | .click(selector) 170 | .call(cb); 171 | }). 172 | addCommand('typeInto', function (selector, value, cb) { 173 | this 174 | .click(selector) 175 | .keys(value) 176 | .call(cb); 177 | }). 178 | addCommand('takeScreenshot', function (filename, silent, cb) { 179 | 180 | if (typeof filename === 'function') { 181 | cb = filename; 182 | filename = null; 183 | } 184 | 185 | if (typeof silent === 'function') { 186 | cb = silent; 187 | silent = false; 188 | } 189 | 190 | if (!filename) { 191 | filename = 'screenshot' + _getNextScreenshotNumber() + '.png'; 192 | } 193 | 194 | if (!filename.match(/\.png$/) && !filename.match(/\.jpg$/)) { 195 | filename += '.png'; 196 | } 197 | var ssPath = path.join(process.env.PWD, filename); 198 | this 199 | .saveScreenshot(ssPath). 200 | call(function () { 201 | if (!silent) { 202 | console.log('\nSaved screenshot to ' + ssPath); 203 | } 204 | cb(); 205 | }); 206 | }); 207 | 208 | 209 | var listener = function (e) { 210 | DEBUG && console.log('[xolvio:webdriver] browser.on("error") listener heard', e); 211 | if (e.body) { 212 | // only take screenshot if error has a body (means it's from a page) 213 | var screenshotName = 'webdriver_error_' + _getNextScreenshotNumber() + '.png'; 214 | browser.takeScreenshot(screenshotName, true); 215 | console.error('Captured error screenshot at ' + 216 | path.join(process.env.PWD, screenshotName)); 217 | } 218 | DEBUG && console.log('[xolvio:webdriver] Removing browser.on("error") listener'); 219 | this.removeListener('error', listener); 220 | this.emit('error', e); 221 | }; 222 | browser.on('error', listener); 223 | 224 | DEBUG && console.log('[xolvio:webdriver] Finished augmenting browser functions'); 225 | 226 | } 227 | 228 | function _getNextScreenshotNumber() { 229 | return '' + _screenshotCounter++; 230 | } 231 | 232 | 233 | var _startPhantom = _.debounce(Meteor.bindEnvironment(__startPhantom)); 234 | 235 | function __startPhantom(port, next) { 236 | 237 | DEBUG && console.log('[xolvio:webdriver] Entering _startPhantom'); 238 | 239 | DEBUG && console.log('[xolvio:webdriver] Spawning phantom process binary', _phantomPath); 240 | var phantomChild = spawn(_phantomPath, ['--ignore-ssl-errors', 'yes', '--webdriver', '' + port]); 241 | DEBUG && console.log('[xolvio:webdriver] Spawned phantom process with pid', phantomChild.pid, 'on port', port); 242 | 243 | var phantomStartupTimeout = 20; 244 | var phantomStartupTimer = setTimeout(function () { 245 | console.error('Phantom failed to start in', phantomStartupTimeout, ' seconds', _phantomPath); 246 | phantomChild.kill('SIGKILL'); 247 | throw new Error('Phantom failed to start'); 248 | }, phantomStartupTimeout * 1000); 249 | 250 | var onPhantomData = Meteor.bindEnvironment(function (data) { 251 | var stdout = data.toString(); 252 | DEBUG && console.log('[xolvio:webdriver][phantom output]', stdout); 253 | if (stdout.match(/running/i)) { 254 | clearTimeout(phantomStartupTimer); 255 | phantomChild.stdout.removeListener('error', onPhantomData); 256 | DEBUG && console.log('[xolvio:webdriver] PhantomJS started.'); 257 | next(); 258 | } 259 | }); 260 | phantomChild.stdout.on('data', onPhantomData); 261 | 262 | phantomChild.on('error', function (err) { 263 | console.error('Error executing phantom at', _phantomPath); 264 | console.error(err.stack); 265 | }); 266 | 267 | DEBUG && console.log('[xolvio:webdriver] Finished _startPhantom'); 268 | 269 | } 270 | 271 | 272 | var _startChromeDriver = _.debounce(Meteor.bindEnvironment(__startChromeDriver)); 273 | 274 | function __startChromeDriver(next) { 275 | 276 | DEBUG && console.log('[xolvio:webdriver] Entering _startChromeDriver'); 277 | 278 | var chromeDriverBinPath = wdio.chromedriver.path; 279 | 280 | DEBUG && console.log('[xolvio:webdriver] Spawning phantom process binary', chromeDriverBinPath); 281 | var chromeDriverChild = spawn(chromeDriverBinPath, ['--url-base=/wd/hub']); 282 | DEBUG && console.log('[xolvio:webdriver] Spawned phantom process with pid', chromeDriverChild.pid); 283 | 284 | var chromeDriverStartupTimeout = 5; 285 | var chromeDriverStartupTimer = setTimeout(function () { 286 | console.error('ChromeDriver failed to start in', chromeDriverStartupTimeout, ' seconds', chromeDriverBinPath); 287 | chromeDriverChild.kill('SIGKILL'); 288 | throw new Error('Phantom failed to start'); 289 | }, chromeDriverStartupTimeout * 1000); 290 | 291 | var onChromeDriverData = Meteor.bindEnvironment(function (data) { 292 | var stdout = data.toString(); 293 | DEBUG && console.log('[xolvio:webdriver][chromedriver output]', stdout); 294 | if (stdout.match(/Only local connections are allowed/)) { 295 | clearTimeout(chromeDriverStartupTimer); 296 | chromeDriverChild.stdout.removeListener('error', onChromeDriverData); 297 | DEBUG && console.log('[xolvio:webdriver] ChromeDriver started.'); 298 | next(); 299 | } 300 | }); 301 | chromeDriverChild.stdout.on('data', onChromeDriverData); 302 | 303 | chromeDriverChild.on('error', function (err) { 304 | console.error('Error executing ChromeDriver at', chromeDriverBinPath); 305 | console.error(err.stack); 306 | }); 307 | 308 | DEBUG && console.log('[xolvio:webdriver] Finished _startChromeDriver'); 309 | 310 | } 311 | 312 | })(); 313 | -------------------------------------------------------------------------------- /versions.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | [ 4 | "base64", 5 | "1.0.1" 6 | ], 7 | [ 8 | "blaze", 9 | "2.0.3" 10 | ], 11 | [ 12 | "coffeescript", 13 | "1.0.4" 14 | ], 15 | [ 16 | "deps", 17 | "1.0.5" 18 | ], 19 | [ 20 | "ejson", 21 | "1.0.4" 22 | ], 23 | [ 24 | "geojson-utils", 25 | "1.0.1" 26 | ], 27 | [ 28 | "htmljs", 29 | "1.0.2" 30 | ], 31 | [ 32 | "id-map", 33 | "1.0.1" 34 | ], 35 | [ 36 | "jquery", 37 | "1.0.1" 38 | ], 39 | [ 40 | "json", 41 | "1.0.1" 42 | ], 43 | [ 44 | "meteor", 45 | "1.1.3" 46 | ], 47 | [ 48 | "minimongo", 49 | "1.0.5" 50 | ], 51 | [ 52 | "observe-sequence", 53 | "1.0.3" 54 | ], 55 | [ 56 | "ordered-dict", 57 | "1.0.1" 58 | ], 59 | [ 60 | "practicalmeteor:chai", 61 | "1.9.2_3" 62 | ], 63 | [ 64 | "practicalmeteor:loglevel", 65 | "1.1.0_3" 66 | ], 67 | [ 68 | "random", 69 | "1.0.1" 70 | ], 71 | [ 72 | "reactive-var", 73 | "1.0.3" 74 | ], 75 | [ 76 | "templating", 77 | "1.0.9" 78 | ], 79 | [ 80 | "tracker", 81 | "1.0.3" 82 | ], 83 | [ 84 | "underscore", 85 | "1.0.1" 86 | ] 87 | ], 88 | "pluginDependencies": [], 89 | "toolVersion": "meteor-tool@1.0.36", 90 | "format": "1.0" 91 | } --------------------------------------------------------------------------------