Skip to content

Commit

Permalink
feat(elementExplorer): Combine browser.pause with elementExplorer
Browse files Browse the repository at this point in the history
* reuse logic for browser.pause for elementExplorer
* introduce browser.enterRepl
* allow customization of driver for elementExplorer
* fix bug where repl cannot return an ElementFinder (related angular#1600)

Closes angular#1314, angular#1315
  • Loading branch information
hankduan committed Jan 7, 2015
1 parent 0b93003 commit 1f80b8f
Show file tree
Hide file tree
Showing 8 changed files with 217 additions and 24 deletions.
Original file line number Diff line number Diff line change
@@ -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
Expand Down
93 changes: 93 additions & 0 deletions lib/debugger/clients/wdrepl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
var repl = require('repl');
var baseDebugger = require('_debugger');
var CommandRepl = require('../modes/commandRepl');

/**
* 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;
this.cmdRepl;
};

/**
* Initiate debugger client.
* @private
*/
WdDebugger.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
*/
WdDebugger.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
*/
WdDebugger.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
*/
WdDebugger.prototype.init = function() {
console.log('Type <tab> 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 wdDebugger = new WdDebugger();
wdDebugger.init();
File renamed without changes.
File renamed without changes.
22 changes: 22 additions & 0 deletions lib/frameworks/repl.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
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) {
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
});
});
});
};
25 changes: 23 additions & 2 deletions lib/launcher.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 ' +
Expand All @@ -153,6 +154,26 @@ var init = function(configFile, additionalConfig) {
}
});
}).then(function() {
// 3) If we're in `elementExplorer` mode, run only that.
if (config.elementExplorer) {
if (config.multiCapabilities.length != 1) {
throw new Error('Must specify only 1 browser while using elementExplorer');
} else {
config.capabilities = config.multiCapabilities[0];
}
config.framework = 'repl';

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) {
Expand Down
91 changes: 72 additions & 19 deletions lib/protractor.js
Original file line number Diff line number Diff line change
Expand Up @@ -589,22 +589,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() {
Expand Down Expand Up @@ -654,9 +648,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');
});
Expand All @@ -665,8 +659,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/*.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
Expand All @@ -689,8 +683,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;
Expand Down Expand Up @@ -746,9 +746,62 @@ Protractor.prototype.pause = function(opt_debugPort) {
}
};

global.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;
});
};

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/wdrepl.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.
*
Expand Down
6 changes: 5 additions & 1 deletion lib/runner.js
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ Runner.prototype.run = function() {
plugins,
browser_;

if (!this.config_.specs.length) {
if (this.config_.framework !== 'repl' && !this.config_.specs.length) {
throw new Error('Spec patterns did not match any files.');
}

Expand Down Expand Up @@ -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 === 'repl') {
// Private framework. Do not use.
frameworkPath = './frameworks/repl.js';
} else {
throw new Error('config.framework (' + self.config_.framework +
') is not a valid framework.');
Expand Down

0 comments on commit 1f80b8f

Please sign in to comment.