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

Commit 4368842

Browse files
committed
feat(wddebugger): enable repl (with autocomplete) for browser.pause
1 parent a877268 commit 4368842

File tree

5 files changed

+463
-158
lines changed

5 files changed

+463
-158
lines changed

lib/debugger/commandRepl.js

+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
var REPL_INITIAL_SUGGESTIONS = [
2+
'element(by.id(\'\'))',
3+
'element(by.css(\'\'))',
4+
'element(by.name(\'\'))',
5+
'element(by.binding(\'\'))',
6+
'element(by.xpath(\'\'))',
7+
'element(by.tagName(\'\'))',
8+
'element(by.className(\'\'))'
9+
];
10+
11+
/**
12+
* Repl to interactively run code.
13+
*
14+
* @param {Client} node debugger client.
15+
* @constructor
16+
*/
17+
var CommandRepl = function(client) {
18+
this.client = client;
19+
this.prompt = '> ';
20+
};
21+
22+
/**
23+
* Eval function for processing a single step in repl.
24+
* Call callback with the result when complete.
25+
*
26+
* @public
27+
* @param {string} expression
28+
* @param {function} callback
29+
*/
30+
CommandRepl.prototype.stepEval = function(expression, callback) {
31+
expression = expression.replace(/"/g, '\\\"');
32+
var expr = 'browser.dbgCodeExecutor_.execute("' + expression + '")';
33+
this.evaluate_(expr, callback);
34+
};
35+
36+
/**
37+
* Autocomplete user entries.
38+
* Call callback with the suggestions.
39+
*
40+
* @public
41+
* @param {string} line Initial user entry
42+
* @param {function} callback
43+
*/
44+
CommandRepl.prototype.complete = function(line, callback) {
45+
if (line === '') {
46+
callback(null, [REPL_INITIAL_SUGGESTIONS, '']);
47+
} else {
48+
line = line.replace(/"/g, '\\\"');
49+
var expr = 'browser.dbgCodeExecutor_.complete("' + line + '")';
50+
this.evaluate_(expr, callback);
51+
}
52+
};
53+
54+
/**
55+
* Helper function to evaluate an expression remotely, and callback with
56+
* the result. The expression can be a promise, in which case, the method
57+
* will wait for the result and callback with the resolved value.
58+
*
59+
* @private
60+
* @param {string} expression Expression to evaluate
61+
* @param {function} callback
62+
*/
63+
CommandRepl.prototype.evaluate_ = function(expression, callback) {
64+
var self = this;
65+
var onbreak_ = function() {
66+
self.client.req({
67+
command: 'evaluate',
68+
arguments: {
69+
frame: 0,
70+
maxStringLength: 1000,
71+
expression: 'browser.dbgCodeExecutor_.resultReady()'
72+
}
73+
}, function(err, res) {
74+
// If code finished executing, get result.
75+
if (res.value) {
76+
self.client.req({
77+
command: 'evaluate',
78+
arguments: {
79+
frame: 0,
80+
maxStringLength: 2000,
81+
expression: 'browser.dbgCodeExecutor_.getResult()'
82+
}
83+
}, function(err, res) {
84+
try {
85+
var result = res.value === undefined ?
86+
undefined : JSON.parse(res.value);
87+
callback(err, result);
88+
} catch(e) {
89+
callback(e, null);
90+
}
91+
self.client.removeListener('break', onbreak_);
92+
});
93+
} else {
94+
// If we need more loops for the code to finish executing, continue
95+
// until the next execute step.
96+
self.client.reqContinue(function() {
97+
// Intentionally blank.
98+
});
99+
}
100+
});
101+
};
102+
103+
this.client.on('break', onbreak_);
104+
105+
this.client.req({
106+
command: 'evaluate',
107+
arguments: {
108+
frame: 0,
109+
maxStringLength: 1000,
110+
expression: expression
111+
}
112+
}, function() {
113+
self.client.reqContinue(function() {
114+
// Intentionally blank.
115+
});
116+
});
117+
};
118+
119+
module.exports = CommandRepl;

lib/debugger/debuggerRepl.js

+128
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
var util = require('util');
2+
3+
var DBG_INITIAL_SUGGESTIONS =
4+
['repl', 'c', 'frame', 'scopes', 'scripts', 'source', 'backtrace', 'd'];
5+
6+
/**
7+
* Repl to step through code.
8+
*
9+
* @param {Client} node debugger client.
10+
* @constructor
11+
*/
12+
var DebuggerRepl = function(client) {
13+
this.client = client;
14+
this.prompt = 'wd-debug> ';
15+
};
16+
17+
/**
18+
* Eval function for processing a single step in repl.
19+
* Call callback with the result when complete.
20+
*
21+
* @public
22+
* @param {string} cmd
23+
* @param {function} callback
24+
*/
25+
DebuggerRepl.prototype.stepEval = function(cmd, callback) {
26+
switch (cmd) {
27+
case 'c':
28+
this.printControlFlow_(callback);
29+
this.client.reqContinue(function() {
30+
// Intentionally blank.
31+
});
32+
break;
33+
case 'frame':
34+
this.client.req({command: 'frame'}, function(err, res) {
35+
console.log(util.inspect(res, {colors: true}));
36+
callback();
37+
});
38+
break;
39+
case 'scopes':
40+
this.client.req({command: 'scopes'}, function(err, res) {
41+
console.log(util.inspect(res, {depth: 4, colors: true}));
42+
callback();
43+
});
44+
break;
45+
case 'scripts':
46+
this.client.req({command: 'scripts'}, function(err, res) {
47+
console.log(util.inspect(res, {depth: 4, colors: true}));
48+
callback();
49+
});
50+
break;
51+
case 'source':
52+
this.client.req({command: 'source'}, function(err, res) {
53+
console.log(util.inspect(res, {depth: 4, colors: true}));
54+
callback();
55+
});
56+
break;
57+
case 'backtrace':
58+
this.client.req({command: 'backtrace'}, function(err, res) {
59+
console.log(util.inspect(res, {depth: 4, colors: true}));
60+
callback();
61+
});
62+
break;
63+
case 'd':
64+
this.client.req({command: 'disconnect'}, function() {
65+
// Intentionally blank.
66+
});
67+
break;
68+
default:
69+
console.log('Unrecognized command.');
70+
callback();
71+
break;
72+
}
73+
};
74+
75+
/**
76+
* Autocomplete user entries.
77+
* Call callback with the suggestions.
78+
*
79+
* @public
80+
* @param {string} line Initial user entry
81+
* @param {function} callback
82+
*/
83+
DebuggerRepl.prototype.complete = function(line, callback) {
84+
var suggestions = DBG_INITIAL_SUGGESTIONS.filter(function(suggestion) {
85+
return suggestion.indexOf(line) === 0;
86+
});
87+
callback(null, [suggestions, line]);
88+
};
89+
90+
/**
91+
* Print the controlflow.
92+
*
93+
* @private
94+
* @param {function} callback
95+
*/
96+
DebuggerRepl.prototype.printControlFlow_ = function(callback) {
97+
var self = this;
98+
var onBreak_ = function() {
99+
self.client.req({
100+
command: 'evaluate',
101+
arguments: {
102+
frame: 0,
103+
maxStringLength: 2000,
104+
expression: 'protractor.promise.controlFlow().getControlFlowText()'
105+
}
106+
}, function(err, controlFlowResponse) {
107+
if (!err) {
108+
self.client.req({
109+
command: 'evaluate',
110+
arguments: {
111+
frame: 0,
112+
maxStringLength: 1000,
113+
expression: 'command.getName()'
114+
}
115+
}, function(err, res) {
116+
if (res.value) {
117+
console.log('-- Next command: ' + res.value);
118+
}
119+
console.log(controlFlowResponse.value);
120+
callback();
121+
});
122+
}
123+
});
124+
};
125+
this.client.once('break', onBreak_);
126+
};
127+
128+
module.exports = DebuggerRepl;

lib/debugger/wddebugger.js

+126
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
var repl = require('repl');
2+
var baseDebugger = require('_debugger');
3+
var CommandRepl = require('./commandRepl');
4+
var DebuggerRepl = require('./debuggerRepl');
5+
6+
/**
7+
* BETA BETA BETA
8+
* Custom protractor debugger which steps through one control flow task
9+
* at a time.
10+
*
11+
* @constructor
12+
*/
13+
var WdDebugger = function() {
14+
this.client = new baseDebugger.Client();
15+
this.replServer;
16+
17+
// repl is broken into 'command repl' and 'debugger repl'.
18+
this.cmdRepl;
19+
this.dbgRepl;
20+
// currentRepl is a pointer to one of them.
21+
this.currentRepl;
22+
};
23+
24+
/**
25+
* Initiate debugger client.
26+
* @private
27+
*/
28+
WdDebugger.prototype.initClient_ = function() {
29+
var client = this.client;
30+
31+
client.once('ready', function() {
32+
console.log(' ready\n');
33+
34+
client.setBreakpoint({
35+
type: 'scriptRegExp',
36+
target: 'selenium-webdriver/executors.js',
37+
line: 37
38+
}, function() {
39+
console.log('press c to continue to the next webdriver command');
40+
console.log('press d to continue to the next debugger statement');
41+
console.log('type "repl" to enter interactive mode');
42+
console.log('type "exit" to break out of interactive mode');
43+
console.log('press ^C to exit');
44+
console.log();
45+
});
46+
});
47+
48+
var host = 'localhost';
49+
var port = process.argv[2] || 5858;
50+
client.connect(port, host); // TODO - might want to add retries here.
51+
};
52+
53+
/**
54+
* Eval function for processing a single step in repl.
55+
* @private
56+
* @param {string} cmd
57+
* @param {object} context
58+
* @param {string} filename
59+
* @param {function} callback
60+
*/
61+
WdDebugger.prototype.stepEval_ = function(cmd, context, filename, callback) {
62+
// The loop won't come back until 'callback' is called.
63+
// Strip out the () which the REPL adds and the new line.
64+
// Note - node's debugger gets around this by adding custom objects
65+
// named 'c', 's', etc to the REPL context. They have getters which
66+
// perform the desired function, and the callback is stored for later use.
67+
// Think about whether this is a better pattern.
68+
cmd = cmd.slice(1, cmd.length - 2);
69+
70+
if (this.currentRepl === this.dbgRepl && cmd === 'repl' ||
71+
this.currentRepl === this.cmdRepl && cmd === 'exit') {
72+
// switch repl mode
73+
this.currentRepl =
74+
this.currentRepl === this.dbgRepl ? this.cmdRepl : this.dbgRepl;
75+
this.replServer.prompt = this.currentRepl.prompt;
76+
this.replServer.complete = this.currentRepl.complete.bind(this.currentRepl);
77+
callback();
78+
} else {
79+
this.currentRepl.stepEval(cmd, callback);
80+
}
81+
};
82+
83+
/**
84+
* Instantiate all repl objects, and debuggerRepl as current and start repl.
85+
* @private
86+
*/
87+
WdDebugger.prototype.initRepl_ = function() {
88+
var self = this;
89+
this.cmdRepl = new CommandRepl(this.client);
90+
this.dbgRepl = new DebuggerRepl(this.client);
91+
this.currentRepl = this.dbgRepl;
92+
93+
// We want the prompt to show up only after the controlflow text prints.
94+
this.dbgRepl.printControlFlow_(function() {
95+
self.replServer = repl.start({
96+
prompt: self.currentRepl.prompt,
97+
input: process.stdin,
98+
output: process.stdout,
99+
eval: self.stepEval_.bind(self),
100+
useGlobal: false,
101+
ignoreUndefined: true
102+
});
103+
104+
self.replServer.complete = self.currentRepl.complete.bind(self.currentRepl);
105+
106+
self.replServer.on('exit', function() {
107+
console.log('Exiting debugger.');
108+
self.client.req({command: 'disconnect'}, function() {
109+
// Intentionally blank.
110+
});
111+
});
112+
});
113+
};
114+
115+
/**
116+
* Initiate the debugger.
117+
* @public
118+
*/
119+
WdDebugger.prototype.init = function() {
120+
console.log('------- WebDriver Debugger -------');
121+
this.initClient_();
122+
this.initRepl_();
123+
};
124+
125+
var wdDebugger = new WdDebugger();
126+
wdDebugger.init();

0 commit comments

Comments
 (0)