diff --git a/bin/elementexplorer.js b/bin/elementexplorer.js index 184b56c7a..169806617 100755 --- a/bin/elementexplorer.js +++ b/bin/elementexplorer.js @@ -32,124 +32,15 @@ * Typing tab at a blank prompt will fill in a suggestion for finding * elements. */ - -var webdriver = require('selenium-webdriver'); -var protractor = require('../lib/protractor.js'); -var repl = require('repl'); -var util = require('util'); -var vm = require('vm'); - -var driver, browser; - -var INITIAL_SUGGESTIONS = [ - 'element(by.id(\'\'))', - 'element(by.css(\'\'))', - 'element(by.name(\'\'))', - 'element(by.binding(\'\'))', - 'element(by.xpath(\'\'))', - 'element(by.tagName(\'\'))', - 'element(by.className(\'\'))' -]; - -var list = function(locator) { - return browser.findElements(locator).then(function(arr) { - var found = []; - for (var i = 0; i < arr.length; ++i) { - arr[i].getText().then(function(text) { - found.push(text); - }); - } - return found; - }); -}; - -var flowEval = function(code, context, file, callback) { - - var vmErr, - result, - flow = webdriver.promise.controlFlow(); - - flow.execute(function() { - try { - result = vm.runInThisContext(code, file); - } catch (e) { - vmErr = e; - callback(vmErr, null); - } - if (vmErr && process.domain) { - process.domain.emit('error', vmErr); - process.domain.exit(); - } - - if (webdriver.promise.isPromise(result)) { - return result.then(function(val) {return val}); - } else { - return result; - } - }).then(function(res) { - if (!vmErr) { - callback(null, res); - } - }, function(err) { - callback('There was a webdriver error: ' + err.name + ' ' + err.message, - null); - }); -}; - -var startRepl = function() { - var flowRepl = repl.start({ - 'useGlobal': true, - 'eval': flowEval - }); - - var originalComplete = flowRepl.complete; - - flowRepl.complete = function(line, completeCallback) { - if (line == '') { - completeCallback(null, [INITIAL_SUGGESTIONS, '']); - } else { - originalComplete.apply(this, arguments); - } - }; - - flowRepl.on('exit', function() { - driver.quit(); - util.puts('Shutting down. Goodbye.'); - }); -}; - -var startUp = function() { - driver = new webdriver.Builder(). - usingServer('http://localhost:4444/wd/hub'). - withCapabilities({'browserName': 'chrome'}).build(); - - driver.getSession().then(function(session) { - driver.manage().timeouts().setScriptTimeout(11000); - - browser = protractor.wrapDriver(driver); - - // Set up globals to be available from the command line. - global.driver = driver; - global.protractor = protractor; - global.browser = browser; - global.$ = browser.$; - global.$$ = browser.$$; - global.element = browser.element; - global.by = global.By = protractor.By; - global.list = list; - - - util.puts('Type to see a list of locator strategies.'); - util.puts('Use the `list` helper function to find elements by strategy:'); - util.puts(' e.g., list(by.binding(\'\')) gets all bindings.'); - util.puts(''); - - var url = process.argv[2] || 'about:blank'; - util.puts('Getting page at: ' + url); - driver.get(url); - - startRepl(); - }); -}; - -startUp(); +console.log('Please use "protractor [configFile] [options] --elementExplorer"' + + ' for full functionality\n'); + +if (process.argv.length > 3) { + console.log('usage: elementexplorer.js [urL]'); + process.exit(1); +} else if (process.argv.length === 3) { + process.argv[2] = ('--baseUrl=' + process.argv[2]); +} + +process.argv.push('--elementExplorer'); +require('../lib/cli.js'); diff --git a/lib/cli.js b/lib/cli.js index b4dd50bfd..9adcaf139 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -51,6 +51,7 @@ var optimist = require('optimist'). describe('framework', 'Test framework to use: jasmine, cucumber or mocha'). describe('resultJsonOutputFile', 'Path to save JSON test result'). describe('troubleshoot', 'Turn on troubleshooting output'). + describe('elementExplorer', 'Interactively test Protractor commands'). alias('browser', 'capabilities.browserName'). alias('name', 'capabilities.name'). alias('platform', 'capabilities.platform'). @@ -70,6 +71,11 @@ var optimist = require('optimist'). var argv = optimist.parse(args); +if (argv.help) { + optimist.showHelp(); + process.exit(0); +} + if (argv.version) { console.log('Version ' + require(path.join(__dirname, '../package.json')).version); process.exit(0); @@ -123,7 +129,10 @@ if (!configFile) { configFile = './protractor.conf.js'; } } -if (!configFile && args.length < 3) { + +if (!configFile && !argv.elementExplorer && args.length < 3) { + console.log('**you must either specify a configuration file ' + + 'or at least 3 options. See below for the options:\n'); optimist.showHelp(); process.exit(1); } diff --git a/lib/debugger/clients/explorer.js b/lib/debugger/clients/explorer.js new file mode 100644 index 000000000..767368e1f --- /dev/null +++ b/lib/debugger/clients/explorer.js @@ -0,0 +1,92 @@ +var repl = require('repl'); +var baseDebugger = require('_debugger'); +var CommandRepl = require('../modes/commandRepl'); + +/** + * BETA BETA BETA + * Custom explorer to test protractor commands. + * + * @constructor + */ +var WdRepl = function() { + this.client = new baseDebugger.Client(); + this.replServer; + this.cmdRepl; +}; + +/** + * Initiate debugger client. + * @private + */ +WdRepl.prototype.initClient_ = function() { + var client = this.client; + + client.once('ready', function() { + + client.setBreakpoint({ + type: 'scriptRegExp', + target: 'selenium-webdriver/executors.js', + line: 37 + }, function() {}); + }); + + var host = 'localhost'; + var port = process.argv[2] || 5858; + client.connect(port, host); // TODO - might want to add retries here. +}; + +/** + * Eval function for processing a single step in repl. + * @private + * @param {string} cmd + * @param {object} context + * @param {string} filename + * @param {function} callback + */ +WdRepl.prototype.stepEval_ = function(cmd, context, filename, callback) { + cmd = cmd.slice(1, cmd.length - 2); + this.cmdRepl.stepEval(cmd, callback); +}; + +/** + * Instantiate all repl objects, and debuggerRepl as current and start repl. + * @private + */ +WdRepl.prototype.initRepl_ = function() { + var self = this; + this.cmdRepl = new CommandRepl(this.client); + + self.replServer = repl.start({ + prompt: self.cmdRepl.prompt, + input: process.stdin, + output: process.stdout, + eval: self.stepEval_.bind(self), + useGlobal: false, + ignoreUndefined: true + }); + + self.replServer.complete = self.cmdRepl.complete.bind(self.cmdRepl); + + self.replServer.on('exit', function() { + console.log('Exiting...'); + self.client.req({command: 'disconnect'}, function() { + // Intentionally blank. + }); + }); +}; + +/** + * Initiate the debugger. + * @public + */ +WdRepl.prototype.init = function() { + console.log('Type to see a list of locator strategies.'); + console.log('Use the `list` helper function to find elements by strategy:'); + console.log(' e.g., list(by.binding(\'\')) gets all bindings.'); + + this.initClient_(); + this.initRepl_(); +}; + +var wdRepl = new WdRepl(); +wdRepl.init(); diff --git a/lib/debugger/wddebugger.js b/lib/debugger/clients/wddebugger.js similarity index 97% rename from lib/debugger/wddebugger.js rename to lib/debugger/clients/wddebugger.js index f6ec41fe0..287fd5ec5 100644 --- a/lib/debugger/wddebugger.js +++ b/lib/debugger/clients/wddebugger.js @@ -1,7 +1,7 @@ var repl = require('repl'); var baseDebugger = require('_debugger'); -var CommandRepl = require('./commandRepl'); -var DebuggerRepl = require('./debuggerRepl'); +var CommandRepl = require('../modes/commandRepl'); +var DebuggerRepl = require('../modes/debuggerRepl'); /** * BETA BETA BETA diff --git a/lib/debugger/commandRepl.js b/lib/debugger/modes/commandRepl.js similarity index 100% rename from lib/debugger/commandRepl.js rename to lib/debugger/modes/commandRepl.js diff --git a/lib/debugger/debuggerRepl.js b/lib/debugger/modes/debuggerRepl.js similarity index 100% rename from lib/debugger/debuggerRepl.js rename to lib/debugger/modes/debuggerRepl.js diff --git a/lib/frameworks/explorer.js b/lib/frameworks/explorer.js new file mode 100644 index 000000000..6763d9267 --- /dev/null +++ b/lib/frameworks/explorer.js @@ -0,0 +1,23 @@ +var q = require('q'); + +/** + * A framework which does not actually run any tests. It allows users to drop + * into a repl loop to experiment with protractor commands. + * + * @param {Runner} runner The current Protractor Runner. + * @return {q.Promise} Promise resolved with the test results + */ +exports.run = function(runner) { + /* globals browser */ + return q.promise(function (resolve) { + if (runner.getConfig().baseUrl) { + browser.get(runner.getConfig().baseUrl); + } + browser.enterRepl(); + browser.executeScript_('', 'empty debugger hook').then(function() { + resolve({ + failedCount: 0 + }); + }); + }); +}; diff --git a/lib/launcher.js b/lib/launcher.js index be3387862..eb3a982fc 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -118,9 +118,8 @@ var init = function(configFile, additionalConfig) { // Run beforeLaunch helper.runFilenameOrFn_(config.configDir, config.beforeLaunch).then(function() { - // Set `multicapabilities` using `capabilities`, `multicapabilites`, - // `getMultiCapabilities()`, or default return q.promise(function(resolve) { + // 1) If getMultiCapabilities is set, resolve that as `multiCapabilities`. if (config.getMultiCapabilities && typeof config.getMultiCapabilities === 'function') { if (config.multiCapabilities.length || config.capabilities) { @@ -136,6 +135,8 @@ var init = function(configFile, additionalConfig) { resolve(); } }).then(function() { + // 2) Set `multicapabilities` using `capabilities`, `multicapabilites`, + // or default if (config.capabilities) { if (config.multiCapabilities.length) { log.warn('You have specified both capabilites and ' + @@ -153,6 +154,26 @@ var init = function(configFile, additionalConfig) { } }); }).then(function() { + // 3) If we're in `elementExplorer` mode, run only that. + if (config.elementExplorer || config.framework === 'explorer') { + if (config.multiCapabilities.length != 1) { + throw new Error('Must specify only 1 browser while using elementExplorer'); + } else { + config.capabilities = config.multiCapabilities[0]; + } + config.framework = 'explorer'; + + var Runner = require('./runner'); + var runner = new Runner(config); + return runner.run().then(function(exitCode) { + process.exit(exitCode); + }, function(err) { + log_(err); + process.exit(1); + }); + } + }).then(function() { + // 4) Run tests. var scheduler = new TaskScheduler(config); process.on('exit', function(code) { diff --git a/lib/protractor.js b/lib/protractor.js index 911ccdfd0..33c5b93bd 100644 --- a/lib/protractor.js +++ b/lib/protractor.js @@ -600,22 +600,16 @@ Protractor.prototype.debugger = function() { }; /** - * Beta (unstable) pause function for debugging webdriver tests. Use - * browser.pause() in your test to enter the protractor debugger from that - * point in the control flow. - * Does not require changes to the command line (no need to add 'debug'). - * Note, if you are wrapping your own instance of Protractor, you must - * expose globals 'browser' and 'protractor' for pause to work. - * - * @example - * element(by.id('foo')).click(); - * browser.pause(); - * // Execution will stop before the next click action. - * element(by.id('bar')).click(); + * Helper function to: + * 1) Set up helper functions for debugger clients to call on (e.g. + * getControlFlowText, execute code, get autocompletion). + * 2) Enter process into debugger mode. (i.e. process._debugProcess). + * 3) Invoke the debugger client specified by debuggerClientPath. * + * @param {string=} debuggerClientPath Absolute path of debugger client to use * @param {number=} opt_debugPort Optional port to use for the debugging process */ -Protractor.prototype.pause = function(opt_debugPort) { +Protractor.prototype.initDebugger_ = function(debuggerClientPath, opt_debugPort) { // Patch in a function to help us visualize what's going on in the control // flow. webdriver.promise.ControlFlow.prototype.getControlFlowText = function() { @@ -665,9 +659,9 @@ Protractor.prototype.pause = function(opt_debugPort) { var flow = webdriver.promise.controlFlow(); var pausePromise = flow.execute(function() { log.puts('Starting WebDriver debugger in a child process. Pause is ' + - 'still beta, please report issues at github.com/angular/protractor'); + 'still beta, please report issues at github.com/angular/protractor\n'); var nodedebug = require('child_process'). - fork(__dirname + '/debugger/wddebugger.js', [process.debugPort]); + fork(debuggerClientPath, [process.debugPort]); process.on('exit', function() { nodedebug.kill('SIGTERM'); }); @@ -676,8 +670,8 @@ Protractor.prototype.pause = function(opt_debugPort) { var vm_ = require('vm'); var browserUnderDebug = this; - // Helper used only by './debugger/wddebugger.js' to insert code into the - // control flow. + // Helper used only by debuggers at './debugger/modes/*.js' to insert code + // into the control flow. // In order to achieve this, we maintain a promise at the top of the control // flow, so that we can insert frames into it. // To be able to simulate callback/asynchronous code, we poll this object @@ -700,8 +694,14 @@ Protractor.prototype.pause = function(opt_debugPort) { self.execPromiseResult_ = self.execPromiseError_ = undefined; self.execPromise_ = self.execPromise_. - then(execFn_). - then(function(result) { + then(function() { + var result = execFn_(); + if (webdriver.promise.isPromise(result)) { + return result.then(function(val) {return val;}); + } else { + return result; + } + }).then(function(result) { self.execPromiseResult_ = result; }, function(err) { self.execPromiseError_ = err; @@ -757,9 +757,63 @@ Protractor.prototype.pause = function(opt_debugPort) { } }; + global.list = function(locator) { + /* globals browser */ + return browser.findElements(locator).then(function(arr) { + var found = []; + for (var i = 0; i < arr.length; ++i) { + arr[i].getText().then(function(text) { + found.push(text); + }); + } + return found; + }); + }; + flow.timeout(1000, 'waiting for debugger to attach'); }; +/** + * Beta (unstable) enterRepl function for entering the repl loop from + * any point in the control flow. Use browser.enterRepl() in your test. + * Does not require changes to the command line (no need to add 'debug'). + * Note, if you are wrapping your own instance of Protractor, you must + * expose globals 'browser' and 'protractor' for pause to work. + * + * @example + * element(by.id('foo')).click(); + * browser.enterRepl(); + * // Execution will stop before the next click action. + * element(by.id('bar')).click(); + * + * @param {number=} opt_debugPort Optional port to use for the debugging process + */ +Protractor.prototype.enterRepl = function(opt_debugPort) { + var debuggerClientPath = __dirname + '/debugger/clients/explorer.js'; + this.initDebugger_(debuggerClientPath, opt_debugPort); +}; + +/** + * Beta (unstable) pause function for debugging webdriver tests. Use + * browser.pause() in your test to enter the protractor debugger from that + * point in the control flow. + * Does not require changes to the command line (no need to add 'debug'). + * Note, if you are wrapping your own instance of Protractor, you must + * expose globals 'browser' and 'protractor' for pause to work. + * + * @example + * element(by.id('foo')).click(); + * browser.pause(); + * // Execution will stop before the next click action. + * element(by.id('bar')).click(); + * + * @param {number=} opt_debugPort Optional port to use for the debugging process + */ +Protractor.prototype.pause = function(opt_debugPort) { + var debuggerClientPath = __dirname + '/debugger/clients/wddebugger.js'; + this.initDebugger_(debuggerClientPath, opt_debugPort); +}; + /** * Create a new instance of Protractor by wrapping a webdriver instance. * diff --git a/lib/runner.js b/lib/runner.js index 428bf96b2..104e79e90 100644 --- a/lib/runner.js +++ b/lib/runner.js @@ -234,7 +234,7 @@ Runner.prototype.run = function() { plugins, browser_; - if (!this.config_.specs.length) { + if (this.config_.framework !== 'explorer' && !this.config_.specs.length) { throw new Error('Spec patterns did not match any files.'); } @@ -270,7 +270,11 @@ Runner.prototype.run = function() { } else if (self.config_.framework === 'cucumber') { frameworkPath = './frameworks/cucumber.js'; } else if (self.config_.framework === 'debugprint') { + // Private framework. Do not use. frameworkPath = './frameworks/debugprint.js'; + } else if (self.config_.framework === 'explorer') { + // Private framework. Do not use. + frameworkPath = './frameworks/explorer.js'; } else { throw new Error('config.framework (' + self.config_.framework + ') is not a valid framework.');