diff --git a/Gruntfile.js b/Gruntfile.js new file mode 100644 index 0000000..c513c8d --- /dev/null +++ b/Gruntfile.js @@ -0,0 +1,84 @@ +/* + * grunt-selenium-webdriver + * https://github.com/connectid/grunt-selenium-webdriver + * + * Copyright (c) 2014 Paul Cook + * Licensed under the MIT license. + */ + +'use strict'; + +module.exports = function(grunt) { + + // Project configuration. + grunt.initConfig({ + jshint: { + all: [ + 'Gruntfile.js', + 'tasks/*.js', + '<%= nodeunit.tests %>', + ], + options: { + jshintrc: '.jshintrc', + }, + }, + + // Before generating any new files, remove any previously-created files. + clean: { + tests: ['tmp'], + }, + + selenium_start: { + options: {} + }, + selenium_phantom_hub: { + options: {} + }, + selenium_stop: { + options: {} + }, + + // Configuration to be run (and then tested). + selenium_webdriver: { + default_options: { + options: { + }, + files: { + 'tmp/default_options': ['test/fixtures/testing', 'test/fixtures/123'], + }, + }, + custom_options: { + options: { + separator: ': ', + punctuation: ' !!!', + }, + files: { + 'tmp/custom_options': ['test/fixtures/testing', 'test/fixtures/123'], + }, + }, + }, + + // Unit tests. + nodeunit: { + tests: ['test/*_test.js'], + }, + + }); + + // Actually load this plugin's task(s). + grunt.loadTasks('tasks'); + + // These plugins provide necessary tasks. + grunt.loadNpmTasks('grunt-contrib-jshint'); + grunt.loadNpmTasks('grunt-contrib-clean'); + grunt.loadNpmTasks('grunt-contrib-nodeunit'); + + // Whenever the "test" task is run, first clean the "tmp" dir, then run this + // plugin's task(s), then test the result. + grunt.registerTask('test', ['clean', 'selenium_webdriver', 'nodeunit']); + + // By default, lint and run all tests. + grunt.registerTask('default', ['jshint', 'test']); + grunt.registerTask('startstop', ['selenium_start', 'selenium_stop']); + +}; diff --git a/LICENSE-MIT b/LICENSE-MIT new file mode 100644 index 0000000..97adb71 --- /dev/null +++ b/LICENSE-MIT @@ -0,0 +1,22 @@ +Copyright (c) 2014 Paul Cook + +Permission is hereby granted, free of charge, to any person +obtaining a copy of this software and associated documentation +files (the "Software"), to deal in the Software without +restriction, including without limitation the rights to use, +copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following +conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES +OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT +HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR +OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..f758029 --- /dev/null +++ b/README.md @@ -0,0 +1,89 @@ +# grunt-selenium-webdriver + +> Starts and stops webdriver in griid or hub mode for use with 3rd party CI platforms + +## Getting Started +This plugin requires Grunt `~0.4.2` + +If you haven't used [Grunt](http://gruntjs.com/) before, be sure to check out the [Getting Started](http://gruntjs.com/getting-started) guide, as it explains how to create a [Gruntfile](http://gruntjs.com/sample-gruntfile) as well as install and use Grunt plugins. Once you're familiar with that process, you may install this plugin with this command: + +```shell +npm install grunt-selenium-webdriver --save-dev +``` + +Once the plugin has been installed, it may be enabled inside your Gruntfile with this line of JavaScript: + +```js +grunt.loadNpmTasks('grunt-selenium-webdriver'); +``` + +## The "selenium_webdriver" task + +### Overview +In your project's Gruntfile, add a section named `selenium_webdriver` to the data object passed into `grunt.initConfig()`. + +```js +grunt.initConfig({ + selenium_webdriver: { + options: { + // Task-specific options go here. + }, + your_target: { + // Target-specific file lists and/or options go here. + }, + }, +}); +``` + +### Options + +#### options.separator +Type: `String` +Default value: `', '` + +A string value that is used to do something with whatever. + +#### options.punctuation +Type: `String` +Default value: `'.'` + +A string value that is used to do something else with whatever else. + +### Usage Examples + +#### Default Options +In this example, the default options are used to do something with whatever. So if the `testing` file has the content `Testing` and the `123` file had the content `1 2 3`, the generated result would be `Testing, 1 2 3.` + +```js +grunt.initConfig({ + selenium_webdriver: { + options: {}, + files: { + 'dest/default_options': ['src/testing', 'src/123'], + }, + }, +}); +``` + +#### Custom Options +In this example, custom options are used to do something else with whatever else. So if the `testing` file has the content `Testing` and the `123` file had the content `1 2 3`, the generated result in this case would be `Testing: 1 2 3 !!!` + +```js +grunt.initConfig({ + selenium_webdriver: { + options: { + separator: ': ', + punctuation: ' !!!', + }, + files: { + 'dest/default_options': ['src/testing', 'src/123'], + }, + }, +}); +``` + +## Contributing +In lieu of a formal styleguide, take care to maintain the existing coding style. Add unit tests for any new or changed functionality. Lint and test your code using [Grunt](http://gruntjs.com/). + +## Release History +_(Nothing yet)_ diff --git a/java/selenium-server-standalone-2.39.0.jar b/java/selenium-server-standalone-2.39.0.jar new file mode 100644 index 0000000..8387a34 Binary files /dev/null and b/java/selenium-server-standalone-2.39.0.jar differ diff --git a/package.json b/package.json new file mode 100644 index 0000000..ad877d8 --- /dev/null +++ b/package.json @@ -0,0 +1,57 @@ +{ + "name": "grunt-selenium-webdriver", + "description": "Starts and stops webdriver in grid or hub mode for use with 3rd party CI platforms where phantomjs / chromedriver can have issue with selenium", + "version": "0.1.0", + "homepage": "https://github.com/connectid/grunt-selenium-webdriver", + "contibutors": [ + { + "name": "Paul Cook", + "email": "paul@connectid.me", + "url": "https://github.com/levexis" + }, + { + "name": "Dan Woodward", + "email": "dan@connectid.me", + "url": "https://github.com/deedubbleyoo" + } + ], + "repository": { + "type": "git", + "url": "git://github.com/connectid/grunt-selenium-webdriver.git" + }, + "bugs": { + "url": "https://github.com/connectid/grunt-selenium-webdriver/issues" + }, + "licenses": [ + { + "type": "MIT", + "url": "https://github.com/connectid/grunt-selenium-webdriver/blob/master/LICENSE-MIT" + } + ], + "engines": { + "node": ">= 0.8.0" + }, + "scripts": { + "test": "grunt test" + }, + "dependencies": { + "saucelabs": "latest", + "selenium-webdriver": "latest", + "phantomjs": "latest", + "q": "latest", + "grunt-promise-q": "latest" + + }, + "devDependencies": { + "grunt-contrib-jshint": "~0.6.0", + "grunt-contrib-clean": "~0.4.0", + "grunt-contrib-nodeunit": "~0.2.0", + "grunt": "~0.4.2" + }, + "peerDependencies": { + "grunt": "~0.4.2" + }, + "keywords": [ + "gruntplugin" + ] +} \ No newline at end of file diff --git a/tasks/selenium_webdriver.js b/tasks/selenium_webdriver.js new file mode 100644 index 0000000..9495a40 --- /dev/null +++ b/tasks/selenium_webdriver.js @@ -0,0 +1,197 @@ +/* + * grunt-selenium-webdriver + * https://github.com/connectid/grunt-selenium-webdriver + * + * Copyright (c) 2014 ConnectiD + * Licensed under the MIT license. + */ +/* + * starts and stops selenium in webdriver grid mode as standard + * but in single hub mode for phantom. This is to ensure compatibility + * with versions provied on platforms like codeship and circlci + * requires java runtime installed + */ + +"use strict"; +var spawn = require('child_process').spawn, + starting = false, + started = false, + os = require('os'), + selOptions = [ '-jar' , 'java/selenium-server-standalone-2.39.0.jar'], + Q = require("q"), + asyncTask = require("grunt-promise-q"); + +// selenium = require('selenium-webdriver'); // use their start server? + + +var phantomLoc = __dirname + "/../node_modules/phantomjs/bin"; + + +var seleniumServerProcess = null, + phantomProcess = null; + + +function startPhantom ( next ) { + + phantomProcess = spawn( phantomLoc +'/phantomjs' , [ '--webdriver', '8080', '--webdriver-selenium-grid-hub=http://127.0.0.1:4444' ]); + + phantomProcess.stderr.setEncoding('utf8'); + phantomProcess.stderr.on('data', function(data) { + data = data.trim(); + }); + phantomProcess.stdout.setEncoding('utf8'); + // wait for client ready message before proceeding + phantomProcess.stdout.on('data', function( msg ) { + // look for msg that indicates it's ready and then stop logging messages + if ( !started && msg.indexOf( 'Registered with grid' ) > -1) { + console.log ('phantom client ready'); + started = true; + starting = false; + if (typeof next === 'function') { + return next(); + } + } + }); +} + +/** + * starts a selenium server with access to default browsers + * @param next callback function + * @param isHeadless will start bundled phantomjs single client with selenium in hub mode + * @private + */ +function start( next, isHeadless ) { + + if ( started) { + return next(console.log('already started')); + } + + if ( isHeadless ) { + selOptions.push ( '-role'); + selOptions.push ( 'hub'); + } + + seleniumServerProcess = spawn( 'java', selOptions ); + // selenium webdriver has a port prober in it which could be factored in. + seleniumServerProcess.on('uncaughtException', function(err) { + if(err.errno === 'EADDRINUSE' ){ + console.log ('PORT already IN USE, assume selenium running'); + next(); + } else { + console.trace(err); + process.exit(1); + } + }); + + seleniumServerProcess.stderr.setEncoding('utf8'); + // listen to procee output until server is actually ready, otherwise next task will break + seleniumServerProcess.stderr.on('data', function(data) { + var errMsg; + data = data.trim(); + if ( isHeadless) { + // check for hub + if ( data.indexOf( 'Started SocketConnector' ) > -1) { + console.log ('selenium hub ready'); + return startPhantom(next); + } else if ( data.indexOf ('Address already in use') > -1 ) { + // throw error if already started + errMsg = 'FATAL ERROR starting selenium: ' + data + ' maybe try killall -9 java'; + throw errMsg; + } + } else if ( data && + // throw error if something unexpected happens + data.indexOf('org.openqa.grid.selenium.GridLauncher main') === -1 && + data.indexOf('Setting system property') === -1 && + data.indexOf('INFO') === -1 && + data.indexOf('WARNING') === -1 && + !started + ) { + errMsg = 'FATAL ERROR starting selenium: ' + data; + throw errMsg; + } + }); + seleniumServerProcess.stdout.setEncoding('utf8'); + seleniumServerProcess.stdout.on('data', function( msg ) { + // look for msg that indicates it's ready and then stop logging messages + if ( !started && ( msg.indexOf( 'Started org.openqa.jetty.jetty.servlet.ServletHandler' ) > -1 ) ) { + console.log ('seleniumrc', 'server ready'); + started = true; + starting = false; + if (typeof next === 'function') { + return next(); + } + } else if ( msg.indexOf( 'should connect to' ) > -1 ) { + // log this message so you can see which port + console.log ('seleniumrc starting', msg.substr(20)); + } + }); +} + + +/** + * Stop the servers + */ +function stop(next) { + if (phantomProcess) { + console.log ('phantom stop request pid',phantomProcess.pid); + seleniumServerProcess.on('close', function (code, signal) { + console.log('phantom process terminated'); + // this should really resolve both callbacks rather than guessing phantom wrapper will terminate instantly + if (typeof next === 'function' && !seleniumServerProcess ) { + next(); + } + }); + // kill the child process + // SIGTERM is supposed to let the process end cleanly + phantomProcess.kill('SIGTERM'); + started = false; + starting = false; + } + if (seleniumServerProcess) { + console.log ('seleniumrc stop request pid',seleniumServerProcess.pid); + seleniumServerProcess.on('close', function (code, signal) { + console.log('selenium process terminated'); + if (typeof next === 'function' ) { + // need to stub out the other callback + next(); + } + }); + // kill the child process + // SIGTERM is supposed to let the process end cleanly + seleniumServerProcess.kill('SIGTERM'); + started = false; + starting = false; + } +} + + +// stop the child processes if this process exits +process.on('exit', function onProcessExit() { + if (started) { + stop(); + } +}); + +/** + * Exports + */ +module.exports= function ( grunt) { + var trueFn = function () { +// console.log ('finished'); + return true; + }; + asyncTask.register(grunt, 'selenium_start' , 'Starts and stops webdriver in grid or hub mode for use with 3rd party CI platforms' , function () { + return Q.nfcall( start ,trueFn , false ); + }); + asyncTask.register('selenium_phantom_hub', 'Starts selenium in hub mode and attaches a single phantonjs to it for headless env', function() { + return Q.nfcall( start ,trueFn , true ); + }); + asyncTask.register('selenium_stop', 'Stops webdriver in grid or hub mode for use with 3rd party CI platforms', function() { + return Q.nfcall( stop ,trueFn ); + }); +}; + + +//start(function() { stop ( function() { return console.log('finished'); } ) }); + + diff --git a/test/expected/custom_options b/test/expected/custom_options new file mode 100644 index 0000000..e597128 --- /dev/null +++ b/test/expected/custom_options @@ -0,0 +1 @@ +Testing: 1 2 3 !!! \ No newline at end of file diff --git a/test/expected/default_options b/test/expected/default_options new file mode 100644 index 0000000..5f8b72f --- /dev/null +++ b/test/expected/default_options @@ -0,0 +1 @@ +Testing, 1 2 3. \ No newline at end of file diff --git a/test/fixtures/123 b/test/fixtures/123 new file mode 100644 index 0000000..703ca85 --- /dev/null +++ b/test/fixtures/123 @@ -0,0 +1 @@ +1 2 3 \ No newline at end of file diff --git a/test/fixtures/testing b/test/fixtures/testing new file mode 100644 index 0000000..0a90125 --- /dev/null +++ b/test/fixtures/testing @@ -0,0 +1 @@ +Testing \ No newline at end of file diff --git a/test/selenium_webdriver_test.js b/test/selenium_webdriver_test.js new file mode 100644 index 0000000..84726ad --- /dev/null +++ b/test/selenium_webdriver_test.js @@ -0,0 +1,48 @@ +'use strict'; + +var grunt = require('grunt'); + +/* + ======== A Handy Little Nodeunit Reference ======== + https://github.com/caolan/nodeunit + + Test methods: + test.expect(numAssertions) + test.done() + Test assertions: + test.ok(value, [message]) + test.equal(actual, expected, [message]) + test.notEqual(actual, expected, [message]) + test.deepEqual(actual, expected, [message]) + test.notDeepEqual(actual, expected, [message]) + test.strictEqual(actual, expected, [message]) + test.notStrictEqual(actual, expected, [message]) + test.throws(block, [error], [message]) + test.doesNotThrow(block, [error], [message]) + test.ifError(value) +*/ + +exports.selenium_webdriver = { + setUp: function(done) { + // setup here if necessary + done(); + }, + default_options: function(test) { + test.expect(1); + + var actual = grunt.file.read('tmp/default_options'); + var expected = grunt.file.read('test/expected/default_options'); + test.equal(actual, expected, 'should describe what the default behavior is.'); + + test.done(); + }, + custom_options: function(test) { + test.expect(1); + + var actual = grunt.file.read('tmp/custom_options'); + var expected = grunt.file.read('test/expected/custom_options'); + test.equal(actual, expected, 'should describe what the custom option(s) behavior is.'); + + test.done(); + }, +};