Skip to content
This repository has been archived by the owner on Jul 29, 2024. It is now read-only.

Commit

Permalink
feat(wddebugger): enable repl (with autocomplete) for browser.pause
Browse files Browse the repository at this point in the history
  • Loading branch information
hankduan committed Dec 10, 2014
1 parent a877268 commit 4368842
Show file tree
Hide file tree
Showing 5 changed files with 463 additions and 158 deletions.
119 changes: 119 additions & 0 deletions lib/debugger/commandRepl.js
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;
128 changes: 128 additions & 0 deletions lib/debugger/debuggerRepl.js
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;
126 changes: 126 additions & 0 deletions lib/debugger/wddebugger.js
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();
Loading

0 comments on commit 4368842

Please sign in to comment.