selenium-webdriver 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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