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

Commit cb38ed0

Browse files
committed
Refactor element explorer to work with selenium-webdriver 3 (#3828)
This implementation now relies mostly on promises explicitly, so the control flow is only used to add one large task to the queue. This should pave the way for the eventual removal of the control flow, as well as getting element explorer to work immediately. BREAKING CHANGE You can no longer use the `repl` command from within `browser.pause()`. Instead, use `broser.explore()` to directly enter the repl.
1 parent 604fdbf commit cb38ed0

File tree

10 files changed

+198
-153
lines changed

10 files changed

+198
-153
lines changed

lib/breakpointhook.js

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
module.exports = function() {
2+
return true;
3+
};
4+
5+
/**
6+
* The reason this file exists is so that we can set a breakpoint via
7+
* script name, and then control when that breakpoint is set in
8+
* our library code by importing and calling this function. The
9+
* breakpoint will always be on line 2.
10+
*/

lib/browser.ts

+25-18
Original file line numberDiff line numberDiff line change
@@ -972,37 +972,46 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
972972
}
973973

974974
/**
975-
* Beta (unstable) enterRepl function for entering the repl loop from
976-
* any point in the control flow. Use browser.enterRepl() in your test.
975+
* See browser.explore().
976+
*/
977+
enterRepl(opt_debugPort?: number) {
978+
return this.explore(opt_debugPort);
979+
}
980+
981+
/**
982+
* Beta (unstable) explore function for entering the repl loop from
983+
* any point in the control flow. Use browser.explore() in your test.
977984
* Does not require changes to the command line (no need to add 'debug').
978985
* Note, if you are wrapping your own instance of Protractor, you must
979986
* expose globals 'browser' and 'protractor' for pause to work.
980987
*
981988
* @example
982989
* element(by.id('foo')).click();
983-
* browser.enterRepl();
990+
* browser.explore();
984991
* // Execution will stop before the next click action.
985992
* element(by.id('bar')).click();
986993
*
987994
* @param {number=} opt_debugPort Optional port to use for the debugging
988995
* process
989996
*/
990-
enterRepl(opt_debugPort?: number) {
997+
explore(opt_debugPort?: number) {
991998
let debuggerClientPath = __dirname + '/debugger/clients/explorer.js';
992-
let onStartFn = () => {
993-
logger.info();
994-
logger.info('------- Element Explorer -------');
995-
logger.info(
996-
'Starting WebDriver debugger in a child process. Element ' +
997-
'Explorer is still beta, please report issues at ' +
998-
'github.com/angular/protractor');
999-
logger.info();
1000-
logger.info('Type <tab> to see a list of locator strategies.');
1001-
logger.info('Use the `list` helper function to find elements by strategy:');
1002-
logger.info(' e.g., list(by.binding(\'\')) gets all bindings.');
999+
let onStartFn = (firstTime: boolean) => {
10031000
logger.info();
1001+
if (firstTime) {
1002+
logger.info('------- Element Explorer -------');
1003+
logger.info(
1004+
'Starting WebDriver debugger in a child process. Element ' +
1005+
'Explorer is still beta, please report issues at ' +
1006+
'github.com/angular/protractor');
1007+
logger.info();
1008+
logger.info('Type <tab> to see a list of locator strategies.');
1009+
logger.info('Use the `list` helper function to find elements by strategy:');
1010+
logger.info(' e.g., list(by.binding(\'\')) gets all bindings.');
1011+
logger.info();
1012+
}
10041013
};
1005-
this.debugHelper.init(debuggerClientPath, onStartFn, opt_debugPort);
1014+
this.debugHelper.initBlocking(debuggerClientPath, onStartFn, opt_debugPort);
10061015
}
10071016

10081017
/**
@@ -1040,8 +1049,6 @@ export class ProtractorBrowser extends AbstractExtendedWebDriver {
10401049
logger.info();
10411050
logger.info('press c to continue to the next webdriver command');
10421051
logger.info('press ^D to detach debugger and resume code execution');
1043-
logger.info('type "repl" to enter interactive mode');
1044-
logger.info('type "exit" to break out of interactive mode');
10451052
logger.info();
10461053
}
10471054
};

lib/debugger.ts

+54-45
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {Locator} from './locators';
77
import {Logger} from './logger';
88
import {Ptor} from './ptor';
99
import * as helper from './util';
10+
let breakpointHook = require('./breakpointhook.js');
1011

1112
declare var global: any;
1213
declare var process: any;
@@ -25,32 +26,36 @@ export class DebugHelper {
2526

2627
constructor(private browserUnderDebug_: ProtractorBrowser) {}
2728

29+
30+
initBlocking(debuggerClientPath: string, onStartFn: Function, opt_debugPort?: number) {
31+
this.init_(debuggerClientPath, true, onStartFn, opt_debugPort);
32+
}
33+
34+
init(debuggerClientPath: string, onStartFn: Function, opt_debugPort?: number) {
35+
this.init_(debuggerClientPath, false, onStartFn, opt_debugPort);
36+
}
37+
2838
/**
2939
* 1) Set up helper functions for debugger clients to call on (e.g.
30-
* getControlFlowText, execute code, get autocompletion).
40+
* execute code, get autocompletion).
3141
* 2) Enter process into debugger mode. (i.e. process._debugProcess).
3242
* 3) Invoke the debugger client specified by debuggerClientPath.
3343
*
3444
* @param {string} debuggerClientPath Absolute path of debugger client to use.
45+
* @param {boolean} blockUntilExit Whether to block the flow until process exit or resume
46+
* immediately.
3547
* @param {Function} onStartFn Function to call when the debugger starts. The
3648
* function takes a single parameter, which represents whether this is the
3749
* first time that the debugger is called.
3850
* @param {number=} opt_debugPort Optional port to use for the debugging
3951
* process.
52+
*
53+
* @return {Promise} If blockUntilExit, a promise resolved when the debugger process
54+
* exits. Otherwise, resolved when the debugger process is ready to begin.
4055
*/
41-
init(debuggerClientPath: string, onStartFn: Function, opt_debugPort?: number) {
42-
(wdpromise.ControlFlow as any).prototype.getControlFlowText = function() {
43-
let controlFlowText = this.getSchedule(/* opt_includeStackTraces */ true);
44-
// This filters the entire control flow text, not just the stack trace, so
45-
// unless we maintain a good (i.e. non-generic) set of keywords in
46-
// STACK_SUBSTRINGS_TO_FILTER, we run the risk of filtering out non stack
47-
// trace. The alternative though, which is to reimplement
48-
// webdriver.promise.ControlFlow.prototype.getSchedule() here is much
49-
// hackier, and involves messing with the control flow's internals /
50-
// private variables.
51-
return helper.filterStackTrace(controlFlowText);
52-
};
53-
56+
init_(
57+
debuggerClientPath: string, blockUntilExit: boolean, onStartFn: Function,
58+
opt_debugPort?: number) {
5459
const vm_ = require('vm');
5560
let flow = wdpromise.controlFlow();
5661

@@ -75,8 +80,11 @@ export class DebugHelper {
7580
}
7681
let sandbox = vm_.createContext(context);
7782

78-
let debuggerReadyPromise = wdpromise.defer();
79-
flow.execute(() => {
83+
let debuggingDone = wdpromise.defer();
84+
85+
// We run one flow.execute block for the debugging session. All
86+
// subcommands should be scheduled under this task.
87+
let executePromise = flow.execute(() => {
8088
process['debugPort'] = opt_debugPort || process['debugPort'];
8189
this.validatePortAvailability_(process['debugPort']).then((firstTime: boolean) => {
8290
onStartFn(firstTime);
@@ -93,34 +101,30 @@ export class DebugHelper {
93101
.on('message',
94102
(m: string) => {
95103
if (m === 'ready') {
96-
debuggerReadyPromise.fulfill();
104+
breakpointHook();
105+
if (!blockUntilExit) {
106+
debuggingDone.fulfill();
107+
}
97108
}
98109
})
99110
.on('exit', () => {
100-
logger.info('Debugger exiting');
101111
// Clear this so that we know it's ok to attach a debugger
102112
// again.
103113
this.dbgCodeExecutor = null;
114+
debuggingDone.fulfill();
104115
});
105116
});
106-
});
107-
108-
let pausePromise = flow.execute(() => {
109-
return debuggerReadyPromise.promise.then(() => {
110-
// Necessary for backward compatibility with node < 0.12.0
111-
return this.browserUnderDebug_.executeScriptWithDescription('', 'empty debugger hook');
112-
});
113-
});
117+
return debuggingDone.promise;
118+
}, 'debugging tasks');
114119

115120
// Helper used only by debuggers at './debugger/modes/*.js' to insert code
116-
// into the control flow.
117-
// In order to achieve this, we maintain a promise at the top of the control
121+
// into the control flow, via debugger 'evaluate' protocol.
122+
// In order to achieve this, we maintain a task at the top of the control
118123
// flow, so that we can insert frames into it.
119124
// To be able to simulate callback/asynchronous code, we poll this object
120-
// for a result at every run of DeferredExecutor.execute.
121-
let browserUnderDebug = this.browserUnderDebug_;
125+
// whenever `breakpointHook` is called.
122126
this.dbgCodeExecutor = {
123-
execPromise_: pausePromise, // Promise pointing to current stage of flow.
127+
execPromise_: undefined, // Promise pointing to currently executing command.
124128
execPromiseResult_: undefined, // Return value of promise.
125129
execPromiseError_: undefined, // Error from promise.
126130

@@ -137,20 +141,19 @@ export class DebugHelper {
137141
execute_: function(execFn_: Function) {
138142
this.execPromiseResult_ = this.execPromiseError_ = undefined;
139143

140-
this.execPromise_ = this.execPromise_.then(execFn_).then(
144+
this.execPromise_ = execFn_();
145+
// Note: This needs to be added after setting execPromise to execFn,
146+
// or else we cause this.execPromise_ to get stuck in pending mode
147+
// at our next breakpoint.
148+
this.execPromise_.then(
141149
(result: Object) => {
142150
this.execPromiseResult_ = result;
151+
breakpointHook();
143152
},
144153
(err: Error) => {
145154
this.execPromiseError_ = err;
155+
breakpointHook();
146156
});
147-
148-
// This dummy command is necessary so that the DeferredExecutor.execute
149-
// break point can find something to stop at instead of moving on to the
150-
// next real command.
151-
this.execPromise_.then(() => {
152-
return browserUnderDebug.executeScriptWithDescription('', 'empty debugger hook');
153-
});
154157
},
155158

156159
// Execute a piece of code.
@@ -159,7 +162,12 @@ export class DebugHelper {
159162
let execFn_ = () => {
160163
// Run code through vm so that we can maintain a local scope which is
161164
// isolated from the rest of the execution.
162-
let res = vm_.runInContext(code, sandbox);
165+
let res;
166+
try {
167+
res = vm_.runInContext(code, sandbox);
168+
} catch (e) {
169+
res = 'Error while evaluating command: ' + e;
170+
}
163171
if (!wdpromise.isPromise(res)) {
164172
res = wdpromise.fulfilled(res);
165173
}
@@ -190,14 +198,14 @@ export class DebugHelper {
190198
deferred.fulfill(JSON.stringify(res));
191199
}
192200
});
193-
return deferred;
201+
return deferred.promise;
194202
};
195203
this.execute_(execFn_);
196204
},
197205

198206
// Code finished executing.
199207
resultReady: function() {
200-
return !this.execPromise_.isPending();
208+
return !(this.execPromise_.state_ === 'pending');
201209
},
202210

203211
// Get asynchronous results synchronously.
@@ -213,7 +221,7 @@ export class DebugHelper {
213221
}
214222
};
215223

216-
return pausePromise;
224+
return executePromise;
217225
}
218226

219227
/**
@@ -227,7 +235,7 @@ export class DebugHelper {
227235
* is done. The promise will resolve to a boolean which represents whether
228236
* this is the first time that the debugger is called.
229237
*/
230-
private validatePortAvailability_(port: number): wdpromise.Promise<any> {
238+
private validatePortAvailability_(port: number): wdpromise.Promise<boolean> {
231239
if (this.debuggerValidated_) {
232240
return wdpromise.fulfilled(false);
233241
}
@@ -256,8 +264,9 @@ export class DebugHelper {
256264
});
257265

258266
return doneDeferred.promise.then(
259-
() => {
267+
(firstTime: boolean) => {
260268
this.debuggerValidated_ = true;
269+
return firstTime;
261270
},
262271
(err: string) => {
263272
console.error(err);

lib/debugger/clients/explorer.js

+17-5
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,10 @@ WdRepl.prototype.initServer_ = function(port) {
3838
// Intentionally blank.
3939
});
4040
sock.end();
41+
// TODO(juliemr): Investigate why this is necessary. At this point, there
42+
// should be no active listeners so this process should just exit
43+
// by itself.
44+
process.exit(0);
4145
} else if (input[input.length - 1] === '\t') {
4246
// If the last character is the TAB key, this is an autocomplete
4347
// request. We use everything before the TAB as the init data to feed
@@ -98,15 +102,17 @@ WdRepl.prototype.initRepl_ = function() {
98102
output: process.stdout,
99103
eval: stepEval,
100104
useGlobal: false,
101-
ignoreUndefined: true
105+
ignoreUndefined: true,
106+
completer: cmdRepl.complete.bind(cmdRepl)
102107
});
103108

104-
replServer.complete = cmdRepl.complete.bind(cmdRepl);
105-
106109
replServer.on('exit', function() {
107-
console.log('Exiting...');
110+
console.log('Element Explorer Exiting...');
108111
self.client.req({command: 'disconnect'}, function() {
109-
// Intentionally blank.
112+
// TODO(juliemr): Investigate why this is necessary. At this point, there
113+
// should be no active listeners so this process should just exit
114+
// by itself.
115+
process.exit(0);
110116
});
111117
});
112118
};
@@ -137,6 +143,12 @@ WdRepl.prototype.init = function() {
137143
var self = this;
138144
this.client = debuggerCommons.attachDebugger(process.argv[2], process.argv[3]);
139145
this.client.once('ready', function() {
146+
debuggerCommons.setEvaluateBreakpoint(self.client, function() {
147+
process.send('ready');
148+
self.client.reqContinue(function() {
149+
// Intentionally blank.
150+
});
151+
});
140152
self.initReplOrServer_();
141153
});
142154
};

0 commit comments

Comments
 (0)