selenium-webdriver 0.0.8 → 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,217 @@
1
+ /** @license
2
+ Copyright 2007-2009 WebDriver committers
3
+ Copyright 2007-2009 Google Inc.
4
+
5
+ Licensed under the Apache License, Version 2.0 (the "License");
6
+ you may not use this file except in compliance with the License.
7
+ You may obtain a copy of the License at
8
+
9
+ http://www.apache.org/licenses/LICENSE-2.0
10
+
11
+ Unless required by applicable law or agreed to in writing, software
12
+ distributed under the License is distributed on an "AS IS" BASIS,
13
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ See the License for the specific language governing permissions and
15
+ limitations under the License.
16
+ */
17
+
18
+ /**
19
+ * @fileoverview A test case that uses the WebDriver Javascript API. Each phase
20
+ * of a test (setUp, test function, and tearDown) will be called with an
21
+ * instance of a {@code webdriver.WebDriver} that can be used to schedule
22
+ * commands for controlling the browser (e.g. clicking or typing on an
23
+ * element).
24
+ * <p>
25
+ * Unlike pure JavaScript test frameworks like Selenium, WebDriver controls the
26
+ * browser directly, allowing for more accurate simulation of user actions in a
27
+ * web application.
28
+ * <p>
29
+ * See below for a basic example of using WebDriver to test cut and paste in
30
+ * a contentEditable document.
31
+ * <pre>
32
+ * goog.require('goog.dom');
33
+ * goog.require('webdriver.asserts');
34
+ * goog.require('webdriver.jsunit');
35
+ *
36
+ * var richTextFrame;
37
+ *
38
+ * function setUp() {
39
+ * richTextFrame = goog.dom.$('rtframe');
40
+ * richTextFrame.contentWindow.document.designMode = 'on';
41
+ * richTextFrame.contentWindow.document.body.innerHTML = '';
42
+ * }
43
+ *
44
+ * function testCutAndPaste(driver) {
45
+ * driver.switchToFrame('rtframe');
46
+ * var body = driver.findElement({xpath: '//body'});
47
+ * body.sendKeys('abc', webdriver.Key.CONTROL, 'axvv');
48
+ * driver.callFunction(function() {
49
+ * assertEquals('abcabc',
50
+ * richTextFrame.contentWindow.document.body.innerHTML);
51
+ * });
52
+ * }
53
+ * </pre>
54
+ *
55
+ * @author jmleyba@gmail.com (Jason Leyba)
56
+ */
57
+
58
+ goog.provide('webdriver.TestCase');
59
+ goog.provide('webdriver.TestCase.Test');
60
+
61
+ goog.require('goog.events');
62
+ goog.require('goog.testing.TestCase');
63
+ goog.require('goog.testing.TestCase.Test');
64
+ goog.require('goog.testing.asserts');
65
+ goog.require('webdriver.Command');
66
+
67
+
68
+
69
+ /**
70
+ * A specialized test case for running jsunit tests with the WebDriver
71
+ * framework. Each phase of a test (setUp, test, and tearDown) will be given an
72
+ * instance of {@code webdriver.WebDriver} that can be used to schedule
73
+ * commands for controlling the browser.
74
+ * @param {string} name The name of the test case.
75
+ * @param {function(): webdriver.WebDriver} driverFactoryFn Factory function to
76
+ * use for creating {@code webdriver.WebDriver} instances for each test.
77
+ * @extends {goog.testing.TestCase}
78
+ * @constructor
79
+ */
80
+ webdriver.TestCase = function(name, driverFactoryFn) {
81
+ goog.testing.TestCase.call(this, name);
82
+
83
+ /**
84
+ * Factory function use for creating {@code webdriver.WebDriver}
85
+ * instances for each test.
86
+ * @type {function(): webdriver.WebDriver}
87
+ * @private
88
+ */
89
+ this.driverFactoryFn_ = driverFactoryFn;
90
+ };
91
+ goog.inherits(webdriver.TestCase, goog.testing.TestCase);
92
+
93
+
94
+ /** @override */
95
+ webdriver.TestCase.prototype.cycleTests = function() {
96
+ this.saveMessage('Start');
97
+ this.batchTime_ = this.now_();
98
+ this.startTest_();
99
+ };
100
+
101
+
102
+ /**
103
+ * Starts a test.
104
+ * @private
105
+ */
106
+ webdriver.TestCase.prototype.startTest_ = function() {
107
+ var test = this.next();
108
+ if (!test || !this.running) {
109
+ this.finalize(); // Tests are done.
110
+ return;
111
+ }
112
+
113
+ // TODO(jleyba): result_ should be exposed using a public accessor.
114
+ this.result_.runCount++;
115
+ this.log('Running test: ' + test.name);
116
+ goog.testing.TestCase.currentTestName = test.name;
117
+
118
+ var driver;
119
+ try {
120
+ driver = this.driverFactoryFn_();
121
+
122
+ // Attach an error handler to record each command failure as an error for
123
+ // the current test. After each error, the currently pending command and
124
+ // all of its subcommands so we can continue the test.
125
+ goog.events.listen(driver, webdriver.Command.ERROR_EVENT,
126
+ function(e) {
127
+ var failingCommand = (/** @type {webdriver.Command} */e.target);
128
+ if (!failingCommand.getResponse()) {
129
+ // This should never happen, but just in case.
130
+ test.errors.push('Unknown error');
131
+ } else {
132
+ test.errors.push(failingCommand.getResponse().getErrorMessage());
133
+ }
134
+ driver.abortCommand(null);
135
+ }, /*capture=*/false);
136
+
137
+ // TODO(jleyba): make this automatic upon creating an instance.
138
+ driver.newSession(true);
139
+
140
+ // If setup fails, we don't want to run the test function, so group setup
141
+ // and the test function together in a function command.
142
+ driver.callFunction(function() {
143
+ this.setUp(driver);
144
+ // Wrap the call to the actual test function in a function command. This
145
+ // will ensure all of the commands scheduled in setUp will executed before
146
+ // the test function is called.
147
+ driver.callFunction(function() {
148
+ test.ref.call(test.scope, driver);
149
+ });
150
+ }, this);
151
+
152
+ // Call tearDown once all setup and test commands have completed.
153
+ driver.callFunction(function() {
154
+ this.tearDown(driver);
155
+ }, this);
156
+
157
+ // Likewise, once tearDown is completely finished, finish the test.
158
+ driver.callFunction(function() {
159
+ this.finishTest_(test, driver);
160
+ }, this);
161
+ } catch (e) {
162
+ test.errors.push(e);
163
+ this.finishTest_(test, driver);
164
+ }
165
+ };
166
+
167
+ /**
168
+ * Completes a test.
169
+ * @param {webdriver.TestCase.Test} test The test to complete.
170
+ * @param {webdriver.WebDriver} driver The driver instance used by the test.
171
+ * @private
172
+ */
173
+ webdriver.TestCase.prototype.finishTest_ = function(test, driver) {
174
+ if (driver) {
175
+ driver.dispose();
176
+ }
177
+ goog.testing.TestCase.currentTestName = null;
178
+ var numErrors = test.errors.length;
179
+ if (numErrors) {
180
+ for (var i = 0; i < numErrors; i++) {
181
+ this.doError(test, test.errors[i]);
182
+ }
183
+ } else {
184
+ this.doSuccess(test);
185
+ }
186
+ this.startTest_(); // Start the next test.
187
+ };
188
+
189
+
190
+ /** @override */
191
+ webdriver.TestCase.prototype.createTestFromAutoDiscoveredFunction =
192
+ function(name, ref) {
193
+ return new webdriver.TestCase.Test(name, ref, goog.global);
194
+ };
195
+
196
+
197
+ /**
198
+ * Represents a single test function that will be run by a
199
+ * {@code webdriver.TestCase}.
200
+ * @param {string} name The test name.
201
+ * @param {function} ref Reference to the test function.
202
+ * @param {Object} opt_scope Optional scope that the test function should be
203
+ * called in.
204
+ * @constructor
205
+ * @extends {goog.testing.TestCase.Test}
206
+ */
207
+ webdriver.TestCase.Test = function(name, ref, opt_scope) {
208
+ goog.testing.TestCase.Test.call(this, name, ref, opt_scope);
209
+
210
+ /**
211
+ * The errors that occurred while running this test.
212
+ * @type {Array.<string|Error>}
213
+ */
214
+ this.errors = [];
215
+ };
216
+ goog.inherits(webdriver.TestCase.Test, goog.testing.TestCase.Test);
217
+
@@ -24,14 +24,15 @@ goog.provide('webdriver.WebDriver');
24
24
  goog.provide('webdriver.WebDriver.EventType');
25
25
  goog.provide('webdriver.WebDriver.Speed');
26
26
 
27
+ goog.require('goog.debug.Logger');
27
28
  goog.require('goog.events');
28
29
  goog.require('goog.events.EventTarget');
30
+ goog.require('webdriver.By.Locator');
29
31
  goog.require('webdriver.Command');
30
32
  goog.require('webdriver.CommandName');
31
33
  goog.require('webdriver.Context');
32
34
  goog.require('webdriver.Response');
33
35
  goog.require('webdriver.WebElement');
34
- goog.require('webdriver.logging');
35
36
  goog.require('webdriver.timing');
36
37
 
37
38
 
@@ -68,6 +69,13 @@ goog.require('webdriver.timing');
68
69
  webdriver.WebDriver = function(commandProcessor) {
69
70
  goog.events.EventTarget.call(this);
70
71
 
72
+ /**
73
+ * The logger for this instance.
74
+ * @type {!goog.debug.Logger}
75
+ * @private
76
+ */
77
+ this.logger_ = goog.debug.Logger.getLogger('webdriver.WebDriver');
78
+
71
79
  /**
72
80
  * The command processor to use for executing commands.
73
81
  * @type {Object}
@@ -109,15 +117,6 @@ webdriver.WebDriver = function(commandProcessor) {
109
117
  */
110
118
  this.context_ = new webdriver.Context();
111
119
 
112
- /**
113
- * Whether this instance is locked into its current session. Once locked in,
114
- * any further calls to {@code webdriver.WebDriver.prototype.newSession} will
115
- * be ignored.
116
- * @type {boolean}
117
- * @private
118
- */
119
- this.sessionLocked_ = false;
120
-
121
120
  /**
122
121
  * This instance's current session ID. Set with the
123
122
  * {@code webdriver.WebDriver.prototype.newSession} command.
@@ -221,6 +220,10 @@ webdriver.WebDriver.prototype.addCommand = function(name, opt_element) {
221
220
  * commands).
222
221
  */
223
222
  webdriver.WebDriver.prototype.isIdle = function() {
223
+ if (this.isDisposed()) {
224
+ return true;
225
+ }
226
+
224
227
  // If there is a finished command on the pending command queue, but it
225
228
  // failed, then the failure hasn't been dealt with yet and the driver will
226
229
  // not process any more commands, so we consider this idle.
@@ -236,14 +239,15 @@ webdriver.WebDriver.prototype.isIdle = function() {
236
239
 
237
240
  /**
238
241
  * Aborts the specified command and all of its pending subcommands.
239
- * @param {webdriver.Command} command The command to abort.
242
+ * @param {webdriver.Command|webdriver.WebDriver} command The command to abort.
240
243
  * @return {number} The total number of commands aborted. A value of 0
241
244
  * indicates that the given command was not a pending command.
242
245
  */
243
246
  webdriver.WebDriver.prototype.abortCommand = function(command) {
244
- var index = goog.array.findIndexRight(this.pendingCommands_, function(cmd) {
245
- return cmd == command;
246
- });
247
+ var index = (null == command || this == command) ? 0 :
248
+ goog.array.findIndexRight(this.pendingCommands_, function(cmd) {
249
+ return cmd == command;
250
+ });
247
251
  if (index >= 0) {
248
252
  var numAborted = this.pendingCommands_.length - index;
249
253
  var totalNumAborted = numAborted;
@@ -267,7 +271,7 @@ webdriver.WebDriver.prototype.abortCommand = function(command) {
267
271
  */
268
272
  webdriver.WebDriver.prototype.pauseImmediately = function() {
269
273
  this.isPaused_ = true;
270
- webdriver.logging.debug('Webdriver paused');
274
+ this.logger_.fine('WebDriver paused');
271
275
  this.dispatchEvent(webdriver.WebDriver.EventType.PAUSED);
272
276
  };
273
277
 
@@ -278,7 +282,7 @@ webdriver.WebDriver.prototype.pauseImmediately = function() {
278
282
  */
279
283
  webdriver.WebDriver.prototype.resume = function() {
280
284
  this.isPaused_ = false;
281
- webdriver.logging.debug('Webdriver resumed');
285
+ this.logger_.fine('WebDriver resumed');
282
286
  this.dispatchEvent(webdriver.WebDriver.EventType.RESUMED);
283
287
  };
284
288
 
@@ -295,7 +299,7 @@ webdriver.WebDriver.prototype.processCommands_ = function() {
295
299
 
296
300
  if (pendingCommand && pendingCommand.getResponse().isFailure) {
297
301
  // Or should we be throwing this to be caught by window.onerror?
298
- webdriver.logging.error(
302
+ this.logger_.severe(
299
303
  'Unhandled command failure; halting command processing:\n' +
300
304
  pendingCommand.getResponse().getErrorMessage());
301
305
  return;
@@ -315,7 +319,7 @@ webdriver.WebDriver.prototype.processCommands_ = function() {
315
319
  nextCommand.setParentEventTarget(parentTarget);
316
320
  this.pendingCommands_.push(nextCommand);
317
321
  this.queuedCommands_.push([]);
318
- this.commandProcessor_.execute(nextCommand, this.sessionId_, this.context_);
322
+ this.commandProcessor_.execute(nextCommand);
319
323
  }
320
324
  };
321
325
 
@@ -390,6 +394,40 @@ webdriver.WebDriver.prototype.catchExpectedError = function(opt_errorMsg,
390
394
  };
391
395
 
392
396
 
397
+ /**
398
+ * Queueus a command to call the given function if and only if the previous
399
+ * command fails. Since failed commands do not have a result, the function
400
+ * called will not be given the return value of the previous command.
401
+ * @param {function} fn The function to call if the previous command fails.
402
+ * @param {Object} opt_selfObj The object in whose scope to call the function.
403
+ * @param {*} var_args Any arguments to pass to the function.
404
+ */
405
+ webdriver.WebDriver.prototype.ifPreviousCommandFailsCall = function(
406
+ fn, opt_selfObj, var_args) {
407
+ var args = arguments;
408
+ var currentFrame = goog.array.peek(this.queuedCommands_);
409
+ var previousCommand = goog.array.peek(currentFrame);
410
+ if (!previousCommand) {
411
+ throw new Error('No commands in the queue to expect an error from');
412
+ }
413
+ var commandFailed = false;
414
+ var key = goog.events.listenOnce(previousCommand,
415
+ webdriver.Command.ERROR_EVENT, function(e) {
416
+ commandFailed = true;
417
+ this.abortCommand(e.currentTarget);
418
+ e.preventDefault();
419
+ e.stopPropagation();
420
+ return false;
421
+ }, /*capture phase*/true, this);
422
+ this.callFunction(function() {
423
+ goog.events.unlistenByKey(key);
424
+ if (commandFailed) {
425
+ return this.callFunction.apply(this, args);
426
+ }
427
+ }, this);
428
+ };
429
+
430
+
393
431
  /**
394
432
  * Adds a command to pause this driver so it will not execute anymore commands
395
433
  * until {@code #resume()} is called. When this command executes, a
@@ -425,12 +463,9 @@ webdriver.WebDriver.prototype.callFunction = function(fn, opt_selfObj,
425
463
  var args = goog.array.slice(arguments, 2);
426
464
  var frame = goog.array.peek(this.queuedCommands_);
427
465
  var previousCommand = goog.array.peek(frame);
428
- var wrappedFunction = goog.bind(function() {
429
- args.push(previousCommand ? previousCommand.getResponse() : null);
430
- return fn.apply(opt_selfObj, args);
431
- }, this);
466
+ args.push(previousCommand ? previousCommand.getFutureResult() : null);
432
467
  return this.addCommand(webdriver.CommandName.FUNCTION).
433
- setParameters(wrappedFunction).
468
+ setParameters(fn, opt_selfObj, args).
434
469
  getFutureResult();
435
470
  };
436
471
 
@@ -478,7 +513,13 @@ webdriver.WebDriver.prototype.wait = function(conditionFn, timeout, opt_self,
478
513
  if (ellapsed > timeout) {
479
514
  throw Error('Wait timed out after ' + ellapsed + 'ms');
480
515
  }
481
- callFunction(pollFunction, null, startTime, pendingFuture);
516
+ // If we pass the pending future in as is, the AbstractCommandProcessor
517
+ // will try to resolve it to its value. However, if we're scheduling
518
+ // this function, it's because the future has not been set yet, which
519
+ // will lead to an error. To avoid this, wrap up the pollFunction in an
520
+ // anonymous function so the AbstractCommandProcessor does not
521
+ // interfere.
522
+ callFunction(goog.bind(pollFunction, null, startTime, pendingFuture));
482
523
  }
483
524
  }
484
525
 
@@ -486,9 +527,8 @@ webdriver.WebDriver.prototype.wait = function(conditionFn, timeout, opt_self,
486
527
  checkValue(result);
487
528
  }
488
529
 
489
- // Binding pollFunction for our initial values.
490
- var initialPoll = goog.bind(pollFunction, null, 0, null);
491
- this.addCommand(webdriver.CommandName.WAIT).setParameters(initialPoll);
530
+ this.addCommand(webdriver.CommandName.WAIT).
531
+ setParameters(pollFunction, null, [0, null]);
492
532
  };
493
533
 
494
534
 
@@ -513,24 +553,15 @@ webdriver.WebDriver.prototype.waitNot = function(conditionFn, timeout,
513
553
 
514
554
 
515
555
  /**
516
- * Request a new session ID. This is a no-op if this instance is already locked
517
- * into a session.
518
- * @param {boolean} lockSession Whether to lock this instance into the returned
519
- * session. Once locked into a session, the driver cannot ask for a new
520
- * session (a new instance must be created).
556
+ * Request a new session ID.
521
557
  */
522
- webdriver.WebDriver.prototype.newSession = function(lockSession) {
523
- if (lockSession) {
524
- this.addCommand(webdriver.CommandName.NEW_SESSION).
525
- setSuccessCallback(function(response) {
526
- this.sessionLocked_ = lockSession;
527
- this.sessionId_ = response.value;
528
- this.context_ = response.context;
529
- }, this);
530
- } else {
531
- webdriver.logging.warn(
532
- 'Cannot start new session; driver is locked into current session');
533
- }
558
+ webdriver.WebDriver.prototype.newSession = function() {
559
+ this.callFunction(function() {
560
+ this.addCommand(webdriver.CommandName.NEW_SESSION);
561
+ this.callFunction(function(value) {
562
+ this.sessionId_ = value;
563
+ }, this);
564
+ }, this);
534
565
  };
535
566
 
536
567
 
@@ -542,11 +573,11 @@ webdriver.WebDriver.prototype.newSession = function(lockSession) {
542
573
  * {@code #getWindowHandle()} or {@code #getAllWindowHandles()}.
543
574
  */
544
575
  webdriver.WebDriver.prototype.switchToWindow = function(name) {
545
- this.addCommand(webdriver.CommandName.SWITCH_TO_WINDOW).
546
- setParameters(name).
547
- setSuccessCallback(function(response) {
548
- this.context_ = response.value;
549
- }, this);
576
+ this.callFunction(function() {
577
+ this.addCommand(webdriver.CommandName.SWITCH_TO_WINDOW).
578
+ setParameters(name);
579
+ this.callFunction(this.setContext, this);
580
+ }, this);
550
581
  };
551
582
 
552
583
 
@@ -561,15 +592,14 @@ webdriver.WebDriver.prototype.switchToWindow = function(name) {
561
592
  * to transfer control to.
562
593
  */
563
594
  webdriver.WebDriver.prototype.switchToFrame = function(frame) {
564
- var commandName = webdriver.CommandName.SWITCH_TO_FRAME;
565
- var command;
566
- if (goog.isString(frame) || goog.isNumber(frame)) {
567
- command = this.addCommand(commandName).setParameters(frame);
568
- } else {
569
- command = this.addCommand(commandName, frame);
570
- }
571
- command.setSuccessCallback(function(response) {
572
- this.context_ = response.context;
595
+ this.callFunction(function() {
596
+ var commandName = webdriver.CommandName.SWITCH_TO_FRAME;
597
+ var command;
598
+ if (goog.isString(frame) || goog.isNumber(frame)) {
599
+ command = this.addCommand(commandName).setParameters(frame);
600
+ } else {
601
+ command = this.addCommand(commandName, frame);
602
+ }
573
603
  }, this);
574
604
  };
575
605
 
@@ -579,11 +609,10 @@ webdriver.WebDriver.prototype.switchToFrame = function(frame) {
579
609
  * contains iframes.
580
610
  */
581
611
  webdriver.WebDriver.prototype.switchToDefaultContent = function() {
582
- this.addCommand(webdriver.CommandName.SWITCH_TO_DEFAULT_CONTENT).
583
- setParameters(null).
584
- setSuccessCallback(function(response) {
585
- this.context_ = response.context;
586
- }, this);
612
+ this.callFunction(function() {
613
+ this.addCommand(webdriver.CommandName.SWITCH_TO_DEFAULT_CONTENT).
614
+ setParameters(null);
615
+ }, this);
587
616
  };
588
617
 
589
618
 
@@ -690,12 +719,13 @@ webdriver.WebDriver.prototype.executeScript = function(script, var_args) {
690
719
  var args = goog.array.map(
691
720
  goog.array.slice(arguments, 1),
692
721
  webdriver.WebDriver.wrapScriptArgument_);
693
- return this.addCommand(webdriver.CommandName.EXECUTE_SCRIPT).
694
- setParameters(script, args).
695
- setSuccessCallback(function(response) {
696
- response.value = this.unwrapScriptResult_(response.value);
697
- }, this).
698
- getFutureResult();
722
+ return this.callFunction(function() {
723
+ this.addCommand(webdriver.CommandName.EXECUTE_SCRIPT).
724
+ setParameters(script, args);
725
+ return this.callFunction(function(prevResult) {
726
+ return this.unwrapScriptResult_(prevResult);
727
+ }, this);
728
+ }, this);
699
729
  };
700
730
 
701
731
 
@@ -704,11 +734,10 @@ webdriver.WebDriver.prototype.executeScript = function(script, var_args) {
704
734
  * @param {goog.Uri|string} url The URL to fetch.
705
735
  */
706
736
  webdriver.WebDriver.prototype.get = function(url) {
707
- this.addCommand(webdriver.CommandName.GET).
708
- setParameters(url.toString()).
709
- setSuccessCallback(function(response) {
710
- this.context_ = response.context;
711
- }, this);
737
+ this.callFunction(function() {
738
+ this.addCommand(webdriver.CommandName.GET).
739
+ setParameters(url.toString());
740
+ }, this);
712
741
  };
713
742
 
714
743
 
@@ -765,7 +794,12 @@ webdriver.WebDriver.prototype.getTitle = function() {
765
794
  * issue commands against the located element.
766
795
  */
767
796
  webdriver.WebDriver.prototype.findElement = function(by) {
768
- return webdriver.WebElement.findElement(this, by);
797
+ var webElement = new webdriver.WebElement(this);
798
+ var locator = webdriver.By.Locator.checkLocator(by);
799
+ var command = this.addCommand(webdriver.CommandName.FIND_ELEMENT).
800
+ setParameters(locator.type, locator.target);
801
+ webElement.getId().setValue(command.getFutureResult());
802
+ return webElement;
769
803
  };
770
804
 
771
805
 
@@ -779,7 +813,24 @@ webdriver.WebDriver.prototype.findElement = function(by) {
779
813
  * @see webdriver.By.Locator.createFromObj
780
814
  */
781
815
  webdriver.WebDriver.prototype.isElementPresent = function(by) {
782
- return webdriver.WebElement.isElementPresent(this, by);
816
+ var locator = webdriver.By.Locator.checkLocator(by);
817
+ return this.callFunction(function() {
818
+ var findCommand = this.addCommand(webdriver.CommandName.FIND_ELEMENT).
819
+ setParameters(locator.type, locator.target);
820
+ var commandFailed = false;
821
+ var key = goog.events.listenOnce(findCommand,
822
+ webdriver.Command.ERROR_EVENT, function(e) {
823
+ commandFailed = true;
824
+ this.abortCommand(e.currentTarget);
825
+ e.preventDefault();
826
+ e.stopPropagation();
827
+ return false;
828
+ }, /*capture phase*/true, this);
829
+ return this.callFunction(function() {
830
+ goog.events.unlistenByKey(key);
831
+ return !commandFailed;
832
+ });
833
+ }, this);
783
834
  };
784
835
 
785
836
 
@@ -789,9 +840,9 @@ webdriver.WebDriver.prototype.isElementPresent = function(by) {
789
840
  * operation can be accessed from the last saved {@code webdriver.Response}
790
841
  * object:
791
842
  * driver.findElements({xpath: '//div'});
792
- * driver.callFunction(function(response) {
793
- * response.value[0].click();
794
- * response.value[1].click();
843
+ * driver.callFunction(function(value) {
844
+ * value[0].click();
845
+ * value[1].click();
795
846
  * // etc.
796
847
  * });
797
848
  * @param {webdriver.By.Locator|{*: string}} by The locator to use for finding
@@ -799,7 +850,22 @@ webdriver.WebDriver.prototype.isElementPresent = function(by) {
799
850
  * @see webdriver.By.Locator.createFromObj
800
851
  */
801
852
  webdriver.WebDriver.prototype.findElements = function(by) {
802
- return webdriver.WebElement.findElements(this, by);
853
+ var locator = webdriver.By.Locator.checkLocator(by);
854
+ return this.callFunction(function() {
855
+ this.addCommand(webdriver.CommandName.FIND_ELEMENTS).
856
+ setParameters(locator.type, locator.target);
857
+ return this.callFunction(function(ids) {
858
+ var elements = [];
859
+ for (var i = 0; i < ids.length; i++) {
860
+ if (ids[i]) {
861
+ var element = new webdriver.WebElement(this);
862
+ element.getId().setValue(ids[i]);
863
+ elements.push(element);
864
+ }
865
+ }
866
+ return elements;
867
+ }, this);
868
+ }, this);
803
869
  };
804
870
 
805
871