Skip to content

Commit 749d4d3

Browse files
committed
feat(elementExplorer): Combine browser.pause with elementExplorer
* 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
1 parent 0b93003 commit 749d4d3

File tree

9 files changed

+226
-26
lines changed

9 files changed

+226
-26
lines changed

lib/cli.js

+10-2
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ var optimist = require('optimist').
5151
describe('framework', 'Test framework to use: jasmine, cucumber or mocha').
5252
describe('resultJsonOutputFile', 'Path to save JSON test result').
5353
describe('troubleshoot', 'Turn on troubleshooting output').
54+
describe('elementExplorer', 'Interactively test Protractor commands').
5455
alias('browser', 'capabilities.browserName').
5556
alias('name', 'capabilities.name').
5657
alias('platform', 'capabilities.platform').
@@ -70,6 +71,11 @@ var optimist = require('optimist').
7071

7172
var argv = optimist.parse(args);
7273

74+
if (argv.help) {
75+
optimist.showHelp();
76+
process.exit(0);
77+
}
78+
7379
if (argv.version) {
7480
console.log('Version ' + require(path.join(__dirname, '../package.json')).version);
7581
process.exit(0);
@@ -123,8 +129,10 @@ if (!configFile) {
123129
configFile = './protractor.conf.js';
124130
}
125131
}
126-
if (!configFile && args.length < 3) {
127-
optimist.showHelp();
132+
133+
if (!configFile && !argv.elementExplorer && args.length < 3) {
134+
console.log('you must either specify a configuration file ' +
135+
'or at least 3 options. See "protractor --help" for more info.');
128136
process.exit(1);
129137
}
130138

lib/debugger/wddebugger.js renamed to lib/debugger/clients/wddebugger.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
var repl = require('repl');
22
var baseDebugger = require('_debugger');
3-
var CommandRepl = require('./commandRepl');
4-
var DebuggerRepl = require('./debuggerRepl');
3+
var CommandRepl = require('../modes/commandRepl');
4+
var DebuggerRepl = require('../modes/debuggerRepl');
55

66
/**
77
* BETA BETA BETA

lib/debugger/clients/wdrepl.js

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
var repl = require('repl');
2+
var baseDebugger = require('_debugger');
3+
var CommandRepl = require('../modes/commandRepl');
4+
5+
/**
6+
* BETA BETA BETA
7+
* Custom explorer to test protractor commands.
8+
*
9+
* @constructor
10+
*/
11+
var WdRepl = function() {
12+
this.client = new baseDebugger.Client();
13+
this.replServer;
14+
this.cmdRepl;
15+
};
16+
17+
/**
18+
* Initiate debugger client.
19+
* @private
20+
*/
21+
WdRepl.prototype.initClient_ = function() {
22+
var client = this.client;
23+
24+
client.once('ready', function() {
25+
26+
client.setBreakpoint({
27+
type: 'scriptRegExp',
28+
target: 'selenium-webdriver/executors.js',
29+
line: 37
30+
}, function() {});
31+
});
32+
33+
var host = 'localhost';
34+
var port = process.argv[2] || 5858;
35+
client.connect(port, host); // TODO - might want to add retries here.
36+
};
37+
38+
/**
39+
* Eval function for processing a single step in repl.
40+
* @private
41+
* @param {string} cmd
42+
* @param {object} context
43+
* @param {string} filename
44+
* @param {function} callback
45+
*/
46+
WdRepl.prototype.stepEval_ = function(cmd, context, filename, callback) {
47+
cmd = cmd.slice(1, cmd.length - 2);
48+
this.cmdRepl.stepEval(cmd, callback);
49+
};
50+
51+
/**
52+
* Instantiate all repl objects, and debuggerRepl as current and start repl.
53+
* @private
54+
*/
55+
WdRepl.prototype.initRepl_ = function() {
56+
var self = this;
57+
this.cmdRepl = new CommandRepl(this.client);
58+
59+
self.replServer = repl.start({
60+
prompt: self.cmdRepl.prompt,
61+
input: process.stdin,
62+
output: process.stdout,
63+
eval: self.stepEval_.bind(self),
64+
useGlobal: false,
65+
ignoreUndefined: true
66+
});
67+
68+
self.replServer.complete = self.cmdRepl.complete.bind(self.cmdRepl);
69+
70+
self.replServer.on('exit', function() {
71+
console.log('Exiting...');
72+
self.client.req({command: 'disconnect'}, function() {
73+
// Intentionally blank.
74+
});
75+
});
76+
};
77+
78+
/**
79+
* Initiate the debugger.
80+
* @public
81+
*/
82+
WdRepl.prototype.init = function() {
83+
console.log('Type <tab> to see a list of locator strategies.');
84+
console.log('Use the `list` helper function to find elements by strategy:');
85+
console.log(' e.g., list(by.binding(\'\')) gets all bindings.');
86+
87+
this.initClient_();
88+
this.initRepl_();
89+
};
90+
91+
var wdRepl = new WdRepl();
92+
wdRepl.init();
File renamed without changes.
File renamed without changes.

lib/frameworks/repl.js

+22
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
var q = require('q');
2+
3+
/**
4+
* A framework which does not actually run any tests. It allows users to drop
5+
* into a repl loop to experiment with protractor commands.
6+
*
7+
* @param {Runner} runner The current Protractor Runner.
8+
* @return {q.Promise} Promise resolved with the test results
9+
*/
10+
exports.run = function(runner) {
11+
return q.promise(function (resolve) {
12+
if (runner.getConfig().baseUrl) {
13+
browser.get(runner.getConfig().baseUrl);
14+
}
15+
browser.enterRepl();
16+
browser.executeScript_('', 'empty debugger hook').then(function() {
17+
resolve({
18+
failedCount: 0
19+
});
20+
});
21+
});
22+
};

lib/launcher.js

+23-2
Original file line numberDiff line numberDiff line change
@@ -118,9 +118,8 @@ var init = function(configFile, additionalConfig) {
118118
// Run beforeLaunch
119119
helper.runFilenameOrFn_(config.configDir, config.beforeLaunch).then(function() {
120120

121-
// Set `multicapabilities` using `capabilities`, `multicapabilites`,
122-
// `getMultiCapabilities()`, or default
123121
return q.promise(function(resolve) {
122+
// 1) If getMultiCapabilities is set, resolve that as `multiCapabilities`.
124123
if (config.getMultiCapabilities &&
125124
typeof config.getMultiCapabilities === 'function') {
126125
if (config.multiCapabilities.length || config.capabilities) {
@@ -136,6 +135,8 @@ var init = function(configFile, additionalConfig) {
136135
resolve();
137136
}
138137
}).then(function() {
138+
// 2) Set `multicapabilities` using `capabilities`, `multicapabilites`,
139+
// or default
139140
if (config.capabilities) {
140141
if (config.multiCapabilities.length) {
141142
log.warn('You have specified both capabilites and ' +
@@ -153,6 +154,26 @@ var init = function(configFile, additionalConfig) {
153154
}
154155
});
155156
}).then(function() {
157+
// 3) If we're in `elementExplorer` mode, run only that.
158+
if (config.elementExplorer) {
159+
if (config.multiCapabilities.length != 1) {
160+
throw new Error('Must specify only 1 browser while using elementExplorer');
161+
} else {
162+
config.capabilities = config.multiCapabilities[0];
163+
}
164+
config.framework = 'repl';
165+
166+
var Runner = require('./runner');
167+
var runner = new Runner(config);
168+
return runner.run().then(function(exitCode) {
169+
process.exit(exitCode);
170+
}, function(err) {
171+
log_(err);
172+
process.exit(1);
173+
});
174+
}
175+
}).then(function() {
176+
// 4) Run tests.
156177
var scheduler = new TaskScheduler(config);
157178

158179
process.on('exit', function(code) {

lib/protractor.js

+72-19
Original file line numberDiff line numberDiff line change
@@ -589,22 +589,16 @@ Protractor.prototype.debugger = function() {
589589
};
590590

591591
/**
592-
* Beta (unstable) pause function for debugging webdriver tests. Use
593-
* browser.pause() in your test to enter the protractor debugger from that
594-
* point in the control flow.
595-
* Does not require changes to the command line (no need to add 'debug').
596-
* Note, if you are wrapping your own instance of Protractor, you must
597-
* expose globals 'browser' and 'protractor' for pause to work.
598-
*
599-
* @example
600-
* element(by.id('foo')).click();
601-
* browser.pause();
602-
* // Execution will stop before the next click action.
603-
* element(by.id('bar')).click();
592+
* Helper function to:
593+
* 1) Set up helper functions for debugger clients to call on (e.g.
594+
* getControlFlowText, execute code, get autocompletion).
595+
* 2) Enter process into debugger mode. (i.e. process._debugProcess).
596+
* 3) Invoke the debugger client specified by debuggerClientPath.
604597
*
598+
* @param {string=} debuggerClientPath Absolute path of debugger client to use
605599
* @param {number=} opt_debugPort Optional port to use for the debugging process
606600
*/
607-
Protractor.prototype.pause = function(opt_debugPort) {
601+
Protractor.prototype.initDebugger_ = function(debuggerClientPath, opt_debugPort) {
608602
// Patch in a function to help us visualize what's going on in the control
609603
// flow.
610604
webdriver.promise.ControlFlow.prototype.getControlFlowText = function() {
@@ -654,9 +648,9 @@ Protractor.prototype.pause = function(opt_debugPort) {
654648
var flow = webdriver.promise.controlFlow();
655649
var pausePromise = flow.execute(function() {
656650
log.puts('Starting WebDriver debugger in a child process. Pause is ' +
657-
'still beta, please report issues at github.com/angular/protractor');
651+
'still beta, please report issues at github.com/angular/protractor\n');
658652
var nodedebug = require('child_process').
659-
fork(__dirname + '/debugger/wddebugger.js', [process.debugPort]);
653+
fork(debuggerClientPath, [process.debugPort]);
660654
process.on('exit', function() {
661655
nodedebug.kill('SIGTERM');
662656
});
@@ -665,8 +659,8 @@ Protractor.prototype.pause = function(opt_debugPort) {
665659
var vm_ = require('vm');
666660
var browserUnderDebug = this;
667661

668-
// Helper used only by './debugger/wddebugger.js' to insert code into the
669-
// control flow.
662+
// Helper used only by debuggers at './debugger/modes/*.js' to insert code
663+
// into the control flow.
670664
// In order to achieve this, we maintain a promise at the top of the control
671665
// flow, so that we can insert frames into it.
672666
// To be able to simulate callback/asynchronous code, we poll this object
@@ -689,8 +683,14 @@ Protractor.prototype.pause = function(opt_debugPort) {
689683
self.execPromiseResult_ = self.execPromiseError_ = undefined;
690684

691685
self.execPromise_ = self.execPromise_.
692-
then(execFn_).
693-
then(function(result) {
686+
then(function() {
687+
var result = execFn_();
688+
if (webdriver.promise.isPromise(result)) {
689+
return result.then(function(val) {return val});
690+
} else {
691+
return result;
692+
}
693+
}).then(function(result) {
694694
self.execPromiseResult_ = result;
695695
}, function(err) {
696696
self.execPromiseError_ = err;
@@ -746,9 +746,62 @@ Protractor.prototype.pause = function(opt_debugPort) {
746746
}
747747
};
748748

749+
global.list = function(locator) {
750+
return browser.findElements(locator).then(function(arr) {
751+
var found = [];
752+
for (var i = 0; i < arr.length; ++i) {
753+
arr[i].getText().then(function(text) {
754+
found.push(text);
755+
});
756+
}
757+
return found;
758+
});
759+
};
760+
749761
flow.timeout(1000, 'waiting for debugger to attach');
750762
};
751763

764+
/**
765+
* Beta (unstable) enterRepl function for entering the repl loop from
766+
* any point in the control flow. Use browser.enterRepl() in your test.
767+
* Does not require changes to the command line (no need to add 'debug').
768+
* Note, if you are wrapping your own instance of Protractor, you must
769+
* expose globals 'browser' and 'protractor' for pause to work.
770+
*
771+
* @example
772+
* element(by.id('foo')).click();
773+
* browser.enterRepl();
774+
* // Execution will stop before the next click action.
775+
* element(by.id('bar')).click();
776+
*
777+
* @param {number=} opt_debugPort Optional port to use for the debugging process
778+
*/
779+
Protractor.prototype.enterRepl = function(opt_debugPort) {
780+
var debuggerClientPath = __dirname + '/debugger/clients/wdrepl.js';
781+
this.initDebugger_(debuggerClientPath, opt_debugPort);
782+
};
783+
784+
/**
785+
* Beta (unstable) pause function for debugging webdriver tests. Use
786+
* browser.pause() in your test to enter the protractor debugger from that
787+
* point in the control flow.
788+
* Does not require changes to the command line (no need to add 'debug').
789+
* Note, if you are wrapping your own instance of Protractor, you must
790+
* expose globals 'browser' and 'protractor' for pause to work.
791+
*
792+
* @example
793+
* element(by.id('foo')).click();
794+
* browser.pause();
795+
* // Execution will stop before the next click action.
796+
* element(by.id('bar')).click();
797+
*
798+
* @param {number=} opt_debugPort Optional port to use for the debugging process
799+
*/
800+
Protractor.prototype.pause = function(opt_debugPort) {
801+
var debuggerClientPath = __dirname + '/debugger/clients/wddebugger.js';
802+
this.initDebugger_(debuggerClientPath, opt_debugPort);
803+
};
804+
752805
/**
753806
* Create a new instance of Protractor by wrapping a webdriver instance.
754807
*

lib/runner.js

+5-1
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ Runner.prototype.run = function() {
234234
plugins,
235235
browser_;
236236

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

@@ -270,7 +270,11 @@ Runner.prototype.run = function() {
270270
} else if (self.config_.framework === 'cucumber') {
271271
frameworkPath = './frameworks/cucumber.js';
272272
} else if (self.config_.framework === 'debugprint') {
273+
// Private framework. Do not use.
273274
frameworkPath = './frameworks/debugprint.js';
275+
} else if (self.config_.framework === 'repl') {
276+
// Private framework. Do not use.
277+
frameworkPath = './frameworks/repl.js';
274278
} else {
275279
throw new Error('config.framework (' + self.config_.framework +
276280
') is not a valid framework.');

0 commit comments

Comments
 (0)