This repository has been archived by the owner on Jul 29, 2024. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 2.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(wddebugger): enable repl (with autocomplete) for browser.pause
- Loading branch information
Showing
5 changed files
with
463 additions
and
158 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
var REPL_INITIAL_SUGGESTIONS = [ | ||
'element(by.id(\'\'))', | ||
'element(by.css(\'\'))', | ||
'element(by.name(\'\'))', | ||
'element(by.binding(\'\'))', | ||
'element(by.xpath(\'\'))', | ||
'element(by.tagName(\'\'))', | ||
'element(by.className(\'\'))' | ||
]; | ||
|
||
/** | ||
* Repl to interactively run code. | ||
* | ||
* @param {Client} node debugger client. | ||
* @constructor | ||
*/ | ||
var CommandRepl = function(client) { | ||
this.client = client; | ||
this.prompt = '> '; | ||
}; | ||
|
||
/** | ||
* Eval function for processing a single step in repl. | ||
* Call callback with the result when complete. | ||
* | ||
* @public | ||
* @param {string} expression | ||
* @param {function} callback | ||
*/ | ||
CommandRepl.prototype.stepEval = function(expression, callback) { | ||
expression = expression.replace(/"/g, '\\\"'); | ||
var expr = 'browser.dbgCodeExecutor_.execute("' + expression + '")'; | ||
this.evaluate_(expr, callback); | ||
}; | ||
|
||
/** | ||
* Autocomplete user entries. | ||
* Call callback with the suggestions. | ||
* | ||
* @public | ||
* @param {string} line Initial user entry | ||
* @param {function} callback | ||
*/ | ||
CommandRepl.prototype.complete = function(line, callback) { | ||
if (line === '') { | ||
callback(null, [REPL_INITIAL_SUGGESTIONS, '']); | ||
} else { | ||
line = line.replace(/"/g, '\\\"'); | ||
var expr = 'browser.dbgCodeExecutor_.complete("' + line + '")'; | ||
this.evaluate_(expr, callback); | ||
} | ||
}; | ||
|
||
/** | ||
* Helper function to evaluate an expression remotely, and callback with | ||
* the result. The expression can be a promise, in which case, the method | ||
* will wait for the result and callback with the resolved value. | ||
* | ||
* @private | ||
* @param {string} expression Expression to evaluate | ||
* @param {function} callback | ||
*/ | ||
CommandRepl.prototype.evaluate_ = function(expression, callback) { | ||
var self = this; | ||
var onbreak_ = function() { | ||
self.client.req({ | ||
command: 'evaluate', | ||
arguments: { | ||
frame: 0, | ||
maxStringLength: 1000, | ||
expression: 'browser.dbgCodeExecutor_.resultReady()' | ||
} | ||
}, function(err, res) { | ||
// If code finished executing, get result. | ||
if (res.value) { | ||
self.client.req({ | ||
command: 'evaluate', | ||
arguments: { | ||
frame: 0, | ||
maxStringLength: 2000, | ||
expression: 'browser.dbgCodeExecutor_.getResult()' | ||
} | ||
}, function(err, res) { | ||
try { | ||
var result = res.value === undefined ? | ||
undefined : JSON.parse(res.value); | ||
callback(err, result); | ||
} catch(e) { | ||
callback(e, null); | ||
} | ||
self.client.removeListener('break', onbreak_); | ||
}); | ||
} else { | ||
// If we need more loops for the code to finish executing, continue | ||
// until the next execute step. | ||
self.client.reqContinue(function() { | ||
// Intentionally blank. | ||
}); | ||
} | ||
}); | ||
}; | ||
|
||
this.client.on('break', onbreak_); | ||
|
||
this.client.req({ | ||
command: 'evaluate', | ||
arguments: { | ||
frame: 0, | ||
maxStringLength: 1000, | ||
expression: expression | ||
} | ||
}, function() { | ||
self.client.reqContinue(function() { | ||
// Intentionally blank. | ||
}); | ||
}); | ||
}; | ||
|
||
module.exports = CommandRepl; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,128 @@ | ||
var util = require('util'); | ||
|
||
var DBG_INITIAL_SUGGESTIONS = | ||
['repl', 'c', 'frame', 'scopes', 'scripts', 'source', 'backtrace', 'd']; | ||
|
||
/** | ||
* Repl to step through code. | ||
* | ||
* @param {Client} node debugger client. | ||
* @constructor | ||
*/ | ||
var DebuggerRepl = function(client) { | ||
this.client = client; | ||
this.prompt = 'wd-debug> '; | ||
}; | ||
|
||
/** | ||
* Eval function for processing a single step in repl. | ||
* Call callback with the result when complete. | ||
* | ||
* @public | ||
* @param {string} cmd | ||
* @param {function} callback | ||
*/ | ||
DebuggerRepl.prototype.stepEval = function(cmd, callback) { | ||
switch (cmd) { | ||
case 'c': | ||
this.printControlFlow_(callback); | ||
this.client.reqContinue(function() { | ||
// Intentionally blank. | ||
}); | ||
break; | ||
case 'frame': | ||
this.client.req({command: 'frame'}, function(err, res) { | ||
console.log(util.inspect(res, {colors: true})); | ||
callback(); | ||
}); | ||
break; | ||
case 'scopes': | ||
this.client.req({command: 'scopes'}, function(err, res) { | ||
console.log(util.inspect(res, {depth: 4, colors: true})); | ||
callback(); | ||
}); | ||
break; | ||
case 'scripts': | ||
this.client.req({command: 'scripts'}, function(err, res) { | ||
console.log(util.inspect(res, {depth: 4, colors: true})); | ||
callback(); | ||
}); | ||
break; | ||
case 'source': | ||
this.client.req({command: 'source'}, function(err, res) { | ||
console.log(util.inspect(res, {depth: 4, colors: true})); | ||
callback(); | ||
}); | ||
break; | ||
case 'backtrace': | ||
this.client.req({command: 'backtrace'}, function(err, res) { | ||
console.log(util.inspect(res, {depth: 4, colors: true})); | ||
callback(); | ||
}); | ||
break; | ||
case 'd': | ||
this.client.req({command: 'disconnect'}, function() { | ||
// Intentionally blank. | ||
}); | ||
break; | ||
default: | ||
console.log('Unrecognized command.'); | ||
callback(); | ||
break; | ||
} | ||
}; | ||
|
||
/** | ||
* Autocomplete user entries. | ||
* Call callback with the suggestions. | ||
* | ||
* @public | ||
* @param {string} line Initial user entry | ||
* @param {function} callback | ||
*/ | ||
DebuggerRepl.prototype.complete = function(line, callback) { | ||
var suggestions = DBG_INITIAL_SUGGESTIONS.filter(function(suggestion) { | ||
return suggestion.indexOf(line) === 0; | ||
}); | ||
callback(null, [suggestions, line]); | ||
}; | ||
|
||
/** | ||
* Print the controlflow. | ||
* | ||
* @private | ||
* @param {function} callback | ||
*/ | ||
DebuggerRepl.prototype.printControlFlow_ = function(callback) { | ||
var self = this; | ||
var onBreak_ = function() { | ||
self.client.req({ | ||
command: 'evaluate', | ||
arguments: { | ||
frame: 0, | ||
maxStringLength: 2000, | ||
expression: 'protractor.promise.controlFlow().getControlFlowText()' | ||
} | ||
}, function(err, controlFlowResponse) { | ||
if (!err) { | ||
self.client.req({ | ||
command: 'evaluate', | ||
arguments: { | ||
frame: 0, | ||
maxStringLength: 1000, | ||
expression: 'command.getName()' | ||
} | ||
}, function(err, res) { | ||
if (res.value) { | ||
console.log('-- Next command: ' + res.value); | ||
} | ||
console.log(controlFlowResponse.value); | ||
callback(); | ||
}); | ||
} | ||
}); | ||
}; | ||
this.client.once('break', onBreak_); | ||
}; | ||
|
||
module.exports = DebuggerRepl; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,126 @@ | ||
var repl = require('repl'); | ||
var baseDebugger = require('_debugger'); | ||
var CommandRepl = require('./commandRepl'); | ||
var DebuggerRepl = require('./debuggerRepl'); | ||
|
||
/** | ||
* BETA BETA BETA | ||
* Custom protractor debugger which steps through one control flow task | ||
* at a time. | ||
* | ||
* @constructor | ||
*/ | ||
var WdDebugger = function() { | ||
this.client = new baseDebugger.Client(); | ||
this.replServer; | ||
|
||
// repl is broken into 'command repl' and 'debugger repl'. | ||
this.cmdRepl; | ||
this.dbgRepl; | ||
// currentRepl is a pointer to one of them. | ||
this.currentRepl; | ||
}; | ||
|
||
/** | ||
* Initiate debugger client. | ||
* @private | ||
*/ | ||
WdDebugger.prototype.initClient_ = function() { | ||
var client = this.client; | ||
|
||
client.once('ready', function() { | ||
console.log(' ready\n'); | ||
|
||
client.setBreakpoint({ | ||
type: 'scriptRegExp', | ||
target: 'selenium-webdriver/executors.js', | ||
line: 37 | ||
}, function() { | ||
console.log('press c to continue to the next webdriver command'); | ||
console.log('press d to continue to the next debugger statement'); | ||
console.log('type "repl" to enter interactive mode'); | ||
console.log('type "exit" to break out of interactive mode'); | ||
console.log('press ^C to exit'); | ||
console.log(); | ||
}); | ||
}); | ||
|
||
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 | ||
*/ | ||
WdDebugger.prototype.stepEval_ = 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. | ||
cmd = cmd.slice(1, cmd.length - 2); | ||
|
||
if (this.currentRepl === this.dbgRepl && cmd === 'repl' || | ||
this.currentRepl === this.cmdRepl && cmd === 'exit') { | ||
// switch repl mode | ||
this.currentRepl = | ||
this.currentRepl === this.dbgRepl ? this.cmdRepl : this.dbgRepl; | ||
this.replServer.prompt = this.currentRepl.prompt; | ||
this.replServer.complete = this.currentRepl.complete.bind(this.currentRepl); | ||
callback(); | ||
} else { | ||
this.currentRepl.stepEval(cmd, callback); | ||
} | ||
}; | ||
|
||
/** | ||
* Instantiate all repl objects, and debuggerRepl as current and start repl. | ||
* @private | ||
*/ | ||
WdDebugger.prototype.initRepl_ = function() { | ||
var self = this; | ||
this.cmdRepl = new CommandRepl(this.client); | ||
this.dbgRepl = new DebuggerRepl(this.client); | ||
this.currentRepl = this.dbgRepl; | ||
|
||
// We want the prompt to show up only after the controlflow text prints. | ||
this.dbgRepl.printControlFlow_(function() { | ||
self.replServer = repl.start({ | ||
prompt: self.currentRepl.prompt, | ||
input: process.stdin, | ||
output: process.stdout, | ||
eval: self.stepEval_.bind(self), | ||
useGlobal: false, | ||
ignoreUndefined: true | ||
}); | ||
|
||
self.replServer.complete = self.currentRepl.complete.bind(self.currentRepl); | ||
|
||
self.replServer.on('exit', function() { | ||
console.log('Exiting debugger.'); | ||
self.client.req({command: 'disconnect'}, function() { | ||
// Intentionally blank. | ||
}); | ||
}); | ||
}); | ||
}; | ||
|
||
/** | ||
* Initiate the debugger. | ||
* @public | ||
*/ | ||
WdDebugger.prototype.init = function() { | ||
console.log('------- WebDriver Debugger -------'); | ||
this.initClient_(); | ||
this.initRepl_(); | ||
}; | ||
|
||
var wdDebugger = new WdDebugger(); | ||
wdDebugger.init(); |
Oops, something went wrong.