From 06bd573cbc2471c719a8504f906468fb672d4097 Mon Sep 17 00:00:00 2001 From: Julie Date: Mon, 3 Mar 2014 18:25:46 -0800 Subject: [PATCH] feat(pause): add the browser.pause method to enter a webdriver-specific debugger Warning: this is still beta, there may be issues. Usage: In test code, insert a `browser.pause()` statement. This will stop the test at that point in the webdriver control flow. No need to change the command line you use to start the test. Once paused, you can step forward, pausing before each webdriver command, and interact with the browser. Exit the debugger to continue the tests. --- lib/protractor.js | 96 +++++++++++++++++++++++++---- lib/wddebugger.js | 150 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 235 insertions(+), 11 deletions(-) create mode 100644 lib/wddebugger.js diff --git a/lib/protractor.js b/lib/protractor.js index b1abc0355..f67b1b5f4 100644 --- a/lib/protractor.js +++ b/lib/protractor.js @@ -13,6 +13,17 @@ var WEB_ELEMENT_FUNCTIONS = [ 'getSize', 'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getOuterHtml', 'getInnerHtml']; +var STACK_SUBSTRINGS_TO_FILTER = [ + 'node_modules/minijasminenode/lib/', + 'node_modules/selenium-webdriver', + 'at Module.', + 'at Object.Module.', + 'at Function.Module', + '(timers.js:', + 'jasminewd/index.js', + 'protractor/lib/' +]; + /* * Mix in other webdriver functionality to be accessible via protractor. */ @@ -933,6 +944,78 @@ Protractor.prototype.debugger = function() { }, 'add breakpoint to control flow'); }; +/** + * 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'). + */ +Protractor.prototype.pause = function() { + // Patch in a function to help us visualize what's going on in the control + // flow. + webdriver.promise.ControlFlow.prototype.getControlFlowText = function() { + var descriptions = []; + + var getDescriptions = function(frameOrTask, descriptions) { + if (frameOrTask.getDescription) { + var getRelevantStack = function(stack) { + return stack.filter(function(line) { + var include = true; + for (var i = 0; i < STACK_SUBSTRINGS_TO_FILTER.length; ++i) { + if (line.toString().indexOf(STACK_SUBSTRINGS_TO_FILTER[i]) !== + -1) { + include = false; + } + } + return include; + }); + }; + descriptions.push({ + description: frameOrTask.getDescription(), + stack: getRelevantStack(frameOrTask.snapshot_.getStacktrace()) + }); + } else { + for (var i = 0; i < frameOrTask.children_.length; ++i) { + getDescriptions(frameOrTask.children_[i], descriptions); + } + } + }; + if (this.history_.length) { + getDescriptions(this.history_[this.history_.length - 1], descriptions); + } + if (this.activeFrame_.getPendingTask()) { + getDescriptions(this.activeFrame_.getPendingTask(), descriptions); + } + getDescriptions(this.activeFrame_.getRoot(), descriptions); + var asString = '-- WebDriver control flow schedule \n'; + for (var i = 0; i < descriptions.length; ++i) { + asString += ' |- ' + descriptions[i].description; + if (descriptions[i].stack.length) { + asString += '\n |---' + descriptions[i].stack.join('\n |---'); + } + if (!(i == descriptions.length - 1)) { + asString += '\n'; + } + } + return asString; + }; + + // Call this private function instead of sending SIGUSR1 because Windows. + process._debugProcess(process.pid); + var flow = webdriver.promise.controlFlow(); + + flow.execute(function() { + console.log('Starting WebDriver debugger in a child process. Pause is ' + + 'still beta, please report issues at github.com/angular/protractor'); + var nodedebug = require('child_process'). + fork(__dirname + '/wddebugger.js', ['localhost:5858']); + process.on('exit', function() { + nodedebug.kill('SIGTERM'); + }) + }); + flow.timeout(1000, 'waiting for debugger to attach'); +}; + /** * Builds a single web element from a locator with a findElementsOverride. * Throws an error if an element is not found, and issues a warning @@ -1004,20 +1087,11 @@ exports.filterStackTrace = function(text) { if (!text) { return text; } - var substringsToFilter = [ - 'node_modules/minijasminenode/lib/', - 'node_modules/selenium-webdriver', - 'at Module.', - 'at Object.Module.', - 'at Function.Module', - '(timers.js:', - 'jasminewd/index.js' - ]; var lines = []; text.split(/\n/).forEach(function(line) { var include = true; - for (var i = 0; i < substringsToFilter.length; ++i) { - if (line.indexOf(substringsToFilter[i]) !== -1) { + for (var i = 0; i < STACK_SUBSTRINGS_TO_FILTER.length; ++i) { + if (line.indexOf(STACK_SUBSTRINGS_TO_FILTER[i]) !== -1) { include = false; } } diff --git a/lib/wddebugger.js b/lib/wddebugger.js new file mode 100644 index 000000000..292dcbc68 --- /dev/null +++ b/lib/wddebugger.js @@ -0,0 +1,150 @@ +console.log('------- WebDriver Debugger -------'); + +var util = require('util'); +var repl = require('repl'); +/** + * BETA BETA BETA + * Custom protractor debugger which steps through one control flow task + * at a time. + */ + +var baseDebugger = require('_debugger'); + +var client = new baseDebugger.Client(); + +var host = 'localhost'; +var port = 5858; + +var debuggerRepl; + +var resumeReplCallback = null; +var resume = function() { + if (resumeReplCallback) { + resumeReplCallback(); + } + resumeReplCallback = null; +} + + +var debugStepperEval = function(cmd, context, filename, callback) { + // The loop won't come back until 'callback' is called. + // Strip out the () which the REPL adds and the new line. + // Note - node's debugger gets around this by adding custom objects + // named 'c', 's', etc to the REPL context. They have getters which + // perform the desired function, and the callback is stored for later use. + // Think about whether this is a better pattern. + var cmd = cmd.slice(1, cmd.length - 2); + switch (cmd) { + case 'c': + resumeReplCallback = callback; + client.reqContinue(function(err, res) { + // Intentionally blank. + }); + break; + case 'frame': + client.req({command: 'frame'}, function(err, res) { + console.log('frame response: ' + util.inspect(res)); + callback(null, 1); + }); + break; + case 'scopes': + client.req({command: 'scopes'}, function(err, res) { + console.log('scopes response: ' + util.inspect(res, {depth: 4})); + callback(null, 1); + }); + break; + case 'scripts': + client.req({command: 'scripts'}, function(err, res) { + console.log('scripts response: ' + util.inspect(res, {depth: 4})); + callback(null, 1); + }); + break; + case 'source': + client.req({command: 'source'}, function(err, res) { + console.log('source response: ' + util.inspect(res, {depth: 4})); + callback(null, 1); + }); + break; + case 'backtrace': + client.req({command: 'backtrace'}, function(err, res) { + console.log('backtrace response: ' + util.inspect(res, {depth: 4})); + callback(null, 1); + }); + break; + case 'd': + client.req({command: 'disconnect'}, function(err, res) {}); + callback(null, 1); + break; + default: + console.log('Unrecognized command.'); + callback(null, undefined); + break; + } +} + +var replOpts = { + prompt: 'wd-debug> ', + input: process.stdin, + output: process.stdout, + eval: debugStepperEval, + useGlobal: false, + ignoreUndefined: true +}; + +var initializeRepl = function() { + debuggerRepl = repl.start(replOpts); + + debuggerRepl.on('exit', function() { + process.exit(0); + }); +}; + +client.once('ready', function() { + console.log(' ready\n'); + + client.setBreakpoint({ + type: 'scriptRegExp', + target: 'selenium-webdriver/executors.js', + line: 37 + }, + function(err, res) { + console.log('press c to continue to the next webdriver command'); + console.log('press d to continue to the next debugger statement'); + console.log('press ^C to exit'); + }); +}); + +// TODO - might want to add retries here. +client.connect(port, host); + +client.on('break', function(res) { + client.req({ + command: 'evaluate', + arguments: { + frame: 0, + maxStringLength: 2000, + expression: 'protractor.promise.controlFlow().getControlFlowText()' + } + }, function(err, controlFlowResponse) { + if (!err) { + client.req({ + command: 'evaluate', + arguments: { + frame: 0, + maxStringLength: 1000, + expression: 'command.getName()' + } + }, function (err, response) { + if (response.value) { + console.log('-- Next command: ' + response.value); + } + console.log(controlFlowResponse.value); + if (!debuggerRepl) { + initializeRepl(); + } + resume(); + }); + } + }); +}); +