1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272 | 1
1
1
13450
1664154
1
23099
7264
7264
7264
361024
361024
18776
7264
7264
7264
7264
7264
7264
1159662
983
983
983
8
8
983
972
972
972
972
972
972
326
326
8
318
318
972
330
330
114
216
642
7264
1059
7264
23099
1
1
496
3360
3360
3360
3360
3360
3294
3360
14
3360
1434
1434
1411
23
3360
3360
3360
3360
3220
140
2
2
524
2
2
59
59
2
2
2
340
2
104
2
2
2
8
8
8
2
2
2
2
2
951
2
152
2
2
2
| var __slice = Array.prototype.slice,
Q = require('q'),
_ = require('lodash'),
EventEmitter = require('events').EventEmitter,
slice = Array.prototype.slice.call.bind(Array.prototype.slice),
utils = require('./utils');
// The method below returns no result, so we are able hijack the result to
// preserve the element scope.
// This alows for thing like: field.click().clear().input('hello').getValue()
var elementChainableMethods = ['clear','click','doubleClick','doubleclick',
'flick','sendKeys','submit','type','keys','moveTo','sleep','noop'];
// gets the list of methods to be promisified.
function filterPromisedMethods(Obj) {
return _(Obj).functions().filter(function(fname) {
return !fname.match('^toString$|^_') &&
!EventEmitter.prototype[fname];
}).value();
}
// enriches a promise with the browser + element methods.
function enrich(obj, browser) {
// There are cases were enrich may be called on non-promise objects.
// It is easier and safer to check within the method.
if(utils.isPromise(obj) && !obj.__wd_promise_enriched) {
var promise = obj;
// __wd_promise_enriched is there to avoid enriching twice.
promise.__wd_promise_enriched = true;
// making sure all the sub-promises are also enriched.
_(promise).functions().each(function(fname) {
var _orig = promise[fname];
promise[fname] = function() {
return enrich(
_orig.apply(this, __slice.call(arguments, 0))
, browser);
};
});
// we get the list of methods first cause we need it in the enrich method.
var browserProto = Object.getPrototypeOf(browser);
var Element = browserProto._Element;
// we get the list of methods first cause we need it in the enrich method.
var promisedMethods = filterPromisedMethods(browserProto);
var elementPromisedMethods =
Element ? filterPromisedMethods(Element.prototype) : [];
var allPromisedMethods = _.union(promisedMethods, elementPromisedMethods);
// adding browser + element methods to the current promise.
_(allPromisedMethods).each(function(fname) {
promise[fname] = function() {
var args = __slice.call(arguments, 0);
// This is a hint to figure out if we need to call a browser method or
// an element method.
// "<" --> browser method
// ">" --> element method
var scopeHint;
if(args && args[0] && typeof args[0] === 'string' && args[0].match(/^[<>]$/)) {
scopeHint = args[0];
args = _.rest(args);
}
return this.then(function(res) {
var el;
// if the result is an element it has priority
if(Element && res instanceof Element) { el = res; }
// testing the water for the next call scope
var isBrowserMethod =
_.indexOf(promisedMethods, fname) >= 0;
var isElementMethod =
el && _.indexOf(elementPromisedMethods, fname) >= 0;
Iif(!isBrowserMethod && !isElementMethod) {
// doesn't look good
throw new Error("Invalid method " + fname);
}
if(isBrowserMethod && isElementMethod) {
// we need to resolve the conflict.
Iif(scopeHint === '<') {
isElementMethod = false;
} else if(scopeHint === '>') {
isBrowserMethod = false;
} else Iif(fname.match(/element/) || (Element && args[0] instanceof Element)) {
// method with element locators are browser scoped by default.
// When an element is passed, we are also obviously in the global scope.
isElementMethod = false;
} else {
// otherwise we stay in the element scope to allow sequential calls
isBrowserMethod = false;
}
}
if(isElementMethod) {
// element method case.
return el[fname].apply(el, args).then(function(res) {
if(_.indexOf(elementChainableMethods, fname) >= 0) {
// method like click, where no result is expected, we return
// the element to make it chainable
return el;
} else {
return res; // we have no choice but loosing the scope
}
});
}else{
// browser case.
return browser[fname].apply(browser, args);
}
});
};
});
// transfering _enrich
promise._enrich = function(target) {
return browser._enrich(target);
};
// adding print error helper
promise.printError = function() {
return promise.catch(function(err) {
console.log(err);
throw err;
});
};
}
return obj;
}
module.exports = function(WebDriver, chainable) {
// wraps element + browser call in an enriched promise.
// This is the same as in the first promise version, but enrichment +
// event logging were added.
function wrap(fn, fname) {
return function() {
var _this = this;
var callback;
var args = slice(arguments);
var deferred = Q.defer();
deferred.promise.then(function() {
_this.emit("promise", _this, fname , args , "finished");
});
// Remove any undefined values from the end of the arguments array
// as these interfere with our callback detection below
for (var i = args.length - 1; i >= 0 && args[i] === undefined; i--) {
args.pop();
}
// If the last argument is a function assume that it's a callback
// (Based on the API as of 2012/12/1 this assumption is always correct)
if(typeof args[args.length - 1] === 'function')
{
// Remove to replace it with our callback and then call it
// appropriately when the promise is resolved or rejected
callback = args.pop();
deferred.promise.then(function(value) {
callback(null, value);
}, function(error) {
callback(error);
});
}
args.push(deferred.makeNodeResolver());
_this.emit("promise", _this, fname , args , "calling");
fn.apply(this, args);
if(chainable) {
return enrich(deferred.promise, this);
} else {
return deferred.promise;
}
};
}
var Element = WebDriver.prototype._Element;
// promisify element shortcuts too
var promiseElement = function() {
return Element.apply(this, arguments);
};
// Element replacement.
promiseElement.prototype = Object.create(Element.prototype);
// WebDriver replacement.
var promiseWebdriver = function() {
var args = 1 <= arguments.length ? __slice.call(arguments, 0) : [];
return WebDriver.apply(this, args);
};
promiseWebdriver.prototype = Object.create(WebDriver.prototype);
promiseWebdriver.prototype._Element = promiseElement;
// wrapping browser methods with promises.
_(filterPromisedMethods(WebDriver.prototype)).each(function(fname) {
promiseWebdriver.prototype[fname] = wrap(WebDriver.prototype[fname], fname);
});
// wrapping element methods with promises.
_(filterPromisedMethods(Element.prototype)).each(function(fname) {
promiseElement.prototype[fname] = wrap(Element.prototype[fname], fname);
});
/**
* Starts the chain (promised driver only)
* browser.chain()
* element.chain()
*/
promiseWebdriver.prototype.chain = promiseWebdriver.prototype.noop;
promiseElement.prototype.chain = promiseElement.prototype.noop;
/**
* Resolves the promise (promised driver only)
* browser.resolve(promise)
* element.resolve(promise)
*/
promiseWebdriver.prototype.resolve = function(promise) {
var qPromise = new Q(promise);
this._enrich(qPromise);
return qPromise;
};
promiseElement.prototype.resolve = function(promise) {
var qPromise = new Q(promise);
this._enrich(qPromise, this.browser);
return qPromise;
};
// used to by chai-as-promised and custom methods
promiseWebdriver.prototype._enrich = function(target) {
Eif(chainable) { enrich(target, this); }
};
promiseElement.prototype._enrich = function(target) {
Eif(chainable) { enrich(target, this.browser); }
};
// used to wrap custom methods
promiseWebdriver._wrapAsync = wrap;
// helper to allow easier promise debugging.
promiseWebdriver.prototype._debugPromise = function() {
this.on('promise', function(context, method, args, status) {
args = _.clone(args);
if(context instanceof promiseWebdriver) {
context = '';
} else {
context = ' [element ' + context.value + ']';
}
if(typeof _.last(args) === 'function') {
args.pop();
}
args = ' ( ' + _(args).map(function(arg) {
if(arg instanceof Element) {
return arg.toString();
} else if(typeof arg === 'object') {
return JSON.stringify(arg);
} else {
return arg;
}
}).join(', ') + ' )';
console.log(' --> ' + status + context + " " + method + args);
});
};
return promiseWebdriver;
};
|