capybara-ng 0.0.3

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,896 @@
1
+ module Angular
2
+ # Build the helper 'element' function for a given instance of Protractor.
3
+ #
4
+ # @private
5
+ # @param {Protractor} ptor
6
+ # @return {function(webdriver.Locator): ElementFinder}
7
+ class ElementHelper
8
+ def by_binding
9
+ end
10
+ end
11
+ end
12
+
13
+ <<-JS
14
+
15
+ var buildElementHelper = function(ptor) {
16
+
17
+ /**
18
+ * ElementArrayFinder is used for operations on an array of elements (as opposed
19
+ * to a single element).
20
+ *
21
+ * The ElementArrayFinder is used to set up a chain of conditions that identify
22
+ * an array of elements. In particular, you can call all(locator) and
23
+ * filter(filterFn) to return a new ElementArrayFinder modified by the
24
+ * conditions, and you can call get(index) to return a single ElementFinder at
25
+ * position 'index'.
26
+ *
27
+ * Similar to jquery, ElementArrayFinder will search all branches of the DOM
28
+ * to find the elements that satify the conditions (i.e. all, filter, get).
29
+ * However, an ElementArrayFinder will not actually retrieve the elements until
30
+ * an action is called, which means it can be set up in helper files (i.e.
31
+ * page objects) before the page is available, and reused as the page changes.
32
+ *
33
+ * You can treat an ElementArrayFinder as an array of WebElements for most
34
+ * purposes, in particular, you may perform actions (i.e. click, getText) on
35
+ * them as you would an array of WebElements. The action will apply to
36
+ * every element identified by the ElementArrayFinder. ElementArrayFinder
37
+ * extends Promise, and once an action is performed on an ElementArrayFinder,
38
+ * the latest result can be accessed using then, and will be returned as an
39
+ * array of the results; the array has length equal to the length of the
40
+ * elements found by the ElementArrayFinder and each result represents the
41
+ * result of performing the action on the element. Unlike a WebElement, an
42
+ * ElementArrayFinder will wait for the angular app to settle before
43
+ * performing finds or actions.
44
+ *
45
+ * @alias element.all(locator)
46
+ * @view
47
+ * <ul class="items">
48
+ * <li>First</li>
49
+ * <li>Second</li>
50
+ * <li>Third</li>
51
+ * </ul>
52
+ *
53
+ * @example
54
+ * element.all(by.css('.items li')).then(function(items) {
55
+ * expect(items.length).toBe(3);
56
+ * expect(items[0].getText()).toBe('First');
57
+ * });
58
+ *
59
+ * @constructor
60
+ * @param {function(): Array.<webdriver.WebElement>} getWebElements A function
61
+ * that returns a list of the underlying Web Elements.
62
+ * @param {webdriver.Locator} locator The most relevant locator. It is only
63
+ * used for error reporting and ElementArrayFinder.locator.
64
+ * @param {Array.<webdriver.promise.Promise>} opt_actionResults An array
65
+ * of promises which will be retrieved with then. Resolves to the latest
66
+ * action result, or null if no action has been called.
67
+ * @return {ElementArrayFinder}
68
+ */
69
+ var ElementArrayFinder = function(getWebElements, locator, opt_actionResults) {
70
+ this.getWebElements = getWebElements || null;
71
+ this.actionResults_ = opt_actionResults;
72
+ this.locator_ = locator;
73
+
74
+ var self = this;
75
+ WEB_ELEMENT_FUNCTIONS.forEach(function(fnName) {
76
+ self[fnName] = function() {
77
+ var callerError = new Error();
78
+ var args = arguments;
79
+ var actionFn = function(webElem) {
80
+ return webElem[fnName].apply(webElem, args).then(null, function(e) {
81
+ e.stack = e.stack + '\n' + callerError.stack;
82
+ throw e;
83
+ });
84
+ };
85
+ return self.applyAction_(actionFn);
86
+ };
87
+ });
88
+ };
89
+ util.inherits(ElementArrayFinder, webdriver.promise.Promise);
90
+
91
+ /**
92
+ * Calls to ElementArrayFinder may be chained to find an array of elements
93
+ * using the current elements in this ElementArrayFinder as the starting point.
94
+ * This function returns a new ElementArrayFinder which would contain the
95
+ * children elements found (and could also be empty).
96
+ *
97
+ * @alias element.all(locator).all(locator)
98
+ * @view
99
+ * <div id='id1' class="parent">
100
+ * <ul>
101
+ * <li class="foo">1a</li>
102
+ * <li class="baz">1b</li>
103
+ * </ul>
104
+ * </div>
105
+ * <div id='id2' class="parent">
106
+ * <ul>
107
+ * <li class="foo">2a</li>
108
+ * <li class="bar">2b</li>
109
+ * </ul>
110
+ * </div>
111
+ *
112
+ * @example
113
+ * var foo = element.all(by.css('.parent')).all(by.css('.foo'))
114
+ * expect(foo.getText()).toEqual(['1a', '2a'])
115
+ * var baz = element.all(by.css('.parent')).all(by.css('.baz'))
116
+ * expect(baz.getText()).toEqual(['1b'])
117
+ * var nonexistent = element.all(by.css('.parent')).all(by.css('.NONEXISTENT'))
118
+ * expect(nonexistent.getText()).toEqual([''])
119
+ *
120
+ * @param {webdriver.Locator} subLocator
121
+ * @return {ElementArrayFinder}
122
+ */
123
+ ElementArrayFinder.prototype.all = function(locator) {
124
+ var self = this;
125
+ var getWebElements = function() {
126
+ if (self.getWebElements === null) {
127
+ // This is the first time we are looking for an element
128
+ return ptor.waitForAngular().then(function() {
129
+ if (locator.findElementsOverride) {
130
+ return locator.findElementsOverride(ptor.driver);
131
+ } else {
132
+ return ptor.driver.findElements(locator);
133
+ }
134
+ });
135
+ } else {
136
+ return self.getWebElements().then(function(parentWebElements) {
137
+ var childrenPromiseList = [];
138
+ // For each parent web element, find their children and construct a
139
+ // list of Promise<List<child_web_element>>
140
+ parentWebElements.forEach(function(parentWebElement) {
141
+ var childrenPromise = locator.findElementsOverride ?
142
+ locator.findElementsOverride(ptor.driver, parentWebElement) :
143
+ parentWebElement.findElements(locator);
144
+ childrenPromiseList.push(childrenPromise);
145
+ });
146
+
147
+
148
+ // Resolve the list of Promise<List<child_web_elements>> and merge into
149
+ // a single list
150
+ return webdriver.promise.all(childrenPromiseList).then(
151
+ function(resolved) {
152
+ var childrenList = [];
153
+ resolved.forEach(function(resolvedE) {
154
+ childrenList = childrenList.concat(resolvedE);
155
+ });
156
+ return childrenList;
157
+ });
158
+ });
159
+ }
160
+ };
161
+ return new ElementArrayFinder(getWebElements, locator);
162
+ };
163
+
164
+ /**
165
+ * Apply a filter function to each element within the ElementArrayFinder. Returns
166
+ * a new ElementArrayFinder with all elements that pass the filter function. The
167
+ * filter function receives the ElementFinder as the first argument
168
+ * and the index as a second arg.
169
+ * This does not actually retrieve the underlying list of elements, so it can
170
+ * be used in page objects.
171
+ *
172
+ * @alias element.all(locator).filter(filterFn)
173
+ * @view
174
+ * <ul class="items">
175
+ * <li class="one">First</li>
176
+ * <li class="two">Second</li>
177
+ * <li class="three">Third</li>
178
+ * </ul>
179
+ *
180
+ * @example
181
+ * element.all(by.css('.items li')).filter(function(elem, index) {
182
+ * return elem.getText().then(function(text) {
183
+ * return text === 'Third';
184
+ * });
185
+ * }).then(function(filteredElements) {
186
+ * filteredElements[0].click();
187
+ * });
188
+ *
189
+ * @param {function(ElementFinder, number): webdriver.WebElement.Promise} filterFn
190
+ * Filter function that will test if an element should be returned.
191
+ * filterFn can either return a boolean or a promise that resolves to a boolean.
192
+ * @return {!ElementArrayFinder} A ElementArrayFinder that represents an array
193
+ * of element that satisfy the filter function.
194
+ */
195
+ ElementArrayFinder.prototype.filter = function(filterFn) {
196
+ var self = this;
197
+ var getWebElements = function() {
198
+ return self.getWebElements().then(function(parentWebElements) {
199
+ var list = [];
200
+ parentWebElements.forEach(function(parentWebElement, index) {
201
+ var elementFinder = self.get(index); // Wrap in ElementFinder
202
+ var filterResults = filterFn(elementFinder, index);
203
+ if (filterResults instanceof webdriver.promise.Promise) {
204
+ filterResults.then(function(satisfies) {
205
+ if (satisfies) {
206
+ list.push(parentWebElements[index]);
207
+ }
208
+ });
209
+ } else if (filterResults) {
210
+ list.push(parentWebElements[index]);
211
+ }
212
+ });
213
+ return list;
214
+ });
215
+ };
216
+ return new ElementArrayFinder(getWebElements, this.locator_);
217
+ };
218
+
219
+ /**
220
+ * Get an element within the ElementArrayFinder by index. The index starts at 0.
221
+ * This does not actually retrieve the underlying element.
222
+ *
223
+ * @alias element.all(locator).get(index)
224
+ * @view
225
+ * <ul class="items">
226
+ * <li>First</li>
227
+ * <li>Second</li>
228
+ * <li>Third</li>
229
+ * </ul>
230
+ *
231
+ * @example
232
+ * var list = element.all(by.css('.items li'));
233
+ * expect(list.get(0).getText()).toBe('First');
234
+ * expect(list.get(1).getText()).toBe('Second');
235
+ *
236
+ * @param {number} index Element index.
237
+ * @return {ElementFinder} finder representing element at the given index.
238
+ */
239
+ ElementArrayFinder.prototype.get = function(index) {
240
+ var self = this;
241
+ var getWebElements = function() {
242
+ return self.getWebElements().then(function(parentWebElements) {
243
+ if (index === -1) {
244
+ // -1 is special and means last
245
+ index = parentWebElements.length - 1;
246
+ }
247
+ if (index >= parentWebElements.length) {
248
+ throw new Error('Index out of bound. Trying to access element at ' +
249
+ 'index:' + index + ', but there are only ' +
250
+ parentWebElements.length + ' elements');
251
+ }
252
+ return [parentWebElements[index]];
253
+ });
254
+ };
255
+ return new ElementArrayFinder(getWebElements, this.locator_).toElementFinder_();
256
+ };
257
+
258
+ /**
259
+ * Get the first matching element for the ElementArrayFinder. This does not
260
+ * actually retrieve the underlying element.
261
+ *
262
+ * @alias element.all(locator).first()
263
+ * @view
264
+ * <ul class="items">
265
+ * <li>First</li>
266
+ * <li>Second</li>
267
+ * <li>Third</li>
268
+ * </ul>
269
+ *
270
+ * @example
271
+ * var first = element.all(by.css('.items li')).first();
272
+ * expect(first.getText()).toBe('First');
273
+ *
274
+ * @return {ElementFinder} finder representing the first matching element
275
+ */
276
+ ElementArrayFinder.prototype.first = function() {
277
+ return this.get(0);
278
+ };
279
+
280
+ /**
281
+ * Get the last matching element for the ElementArrayFinder. This does not
282
+ * actually retrieve the underlying element.
283
+ *
284
+ * @alias element.all(locator).last()
285
+ * @view
286
+ * <ul class="items">
287
+ * <li>First</li>
288
+ * <li>Second</li>
289
+ * <li>Third</li>
290
+ * </ul>
291
+ *
292
+ * @example
293
+ * var last = element.all(by.css('.items li')).last();
294
+ * expect(last.getText()).toBe('Third');
295
+ *
296
+ * @return {ElementFinder} finder representing the last matching element
297
+ */
298
+ ElementArrayFinder.prototype.last = function() {
299
+ return this.get(-1);
300
+ };
301
+
302
+ /**
303
+ * Shorthand function for finding arrays of elements by css.
304
+ *
305
+ * @type {function(string): ElementArrayFinder}
306
+ */
307
+ ElementArrayFinder.prototype.$$ = function(selector) {
308
+ return this.all(webdriver.By.css(selector));
309
+ };
310
+
311
+ /**
312
+ * Returns an ElementFinder representation of ElementArrayFinder. It ensures
313
+ * that the ElementArrayFinder resolves to one and only one underlying element.
314
+ *
315
+ * @return {ElementFinder} An ElementFinder representation
316
+ * @private
317
+ */
318
+ ElementArrayFinder.prototype.toElementFinder_ = function() {
319
+ return new ElementFinder(this);
320
+ };
321
+
322
+ /**
323
+ * Count the number of elements represented by the ElementArrayFinder.
324
+ *
325
+ * @alias element.all(locator).count()
326
+ * @view
327
+ * <ul class="items">
328
+ * <li>First</li>
329
+ * <li>Second</li>
330
+ * <li>Third</li>
331
+ * </ul>
332
+ *
333
+ * @example
334
+ * var list = element.all(by.css('.items li'));
335
+ * expect(list.count()).toBe(3);
336
+ *
337
+ * @return {!webdriver.promise.Promise} A promise which resolves to the
338
+ * number of elements matching the locator.
339
+ */
340
+ ElementArrayFinder.prototype.count = function() {
341
+ return this.getWebElements().then(function(arr) {
342
+ return arr.length;
343
+ }, function(err) {
344
+ if (err.code == webdriver.error.ErrorCode.NO_SUCH_ELEMENT) {
345
+ return 0;
346
+ } else {
347
+ throw err;
348
+ }
349
+ });
350
+ };
351
+
352
+ /**
353
+ * Returns the most relevant locator.
354
+ * i.e.
355
+ * $('#ID1').locator() returns by.css('#ID1')
356
+ * $('#ID1').$('#ID2').locator() returns by.css('#ID2')
357
+ * $$('#ID1').filter(filterFn).get(0).click().locator() returns by.css('#ID1')
358
+ * @return {webdriver.Locator}
359
+ */
360
+ ElementArrayFinder.prototype.locator = function() {
361
+ return this.locator_;
362
+ };
363
+
364
+ /**
365
+ * Apply an action function to every element in the ElementArrayFinder,
366
+ * and return a new ElementArrayFinder that contains the results of the actions.
367
+ *
368
+ * @param {function(ElementFinder)} actionFn
369
+ *
370
+ * @return {ElementArrayFinder}
371
+ * @private
372
+ */
373
+ ElementArrayFinder.prototype.applyAction_ = function(actionFn) {
374
+ var actionResults = this.getWebElements().then(function(arr) {
375
+ var list = [];
376
+ arr.forEach(function(webElem) {
377
+ list.push(actionFn(webElem));
378
+ });
379
+ return webdriver.promise.all(list);
380
+ });
381
+ return new ElementArrayFinder(this.getWebElements, this.locator_, actionResults);
382
+ };
383
+
384
+ /**
385
+ * Represents the ElementArrayFinder as an array of ElementFinders.
386
+ *
387
+ * @return {Array.<ElementFinder>} Return a promise, which resolves to a list
388
+ * of ElementFinders specified by the locator.
389
+ */
390
+ ElementArrayFinder.prototype.asElementFinders_ = function() {
391
+ var self = this;
392
+ return this.getWebElements().then(function(arr) {
393
+ var list = [];
394
+ arr.forEach(function(webElem, index) {
395
+ list.push(self.get(index));
396
+ });
397
+ return list;
398
+ });
399
+ };
400
+
401
+ /**
402
+ * Retrieve the elements represented by the ElementArrayFinder. The input
403
+ * function is passed to the resulting promise, which resolves to an
404
+ * array of ElementFinders.
405
+ *
406
+ * @alias element.all(locator).then(thenFunction)
407
+ * @view
408
+ * <ul class="items">
409
+ * <li>First</li>
410
+ * <li>Second</li>
411
+ * <li>Third</li>
412
+ * </ul>
413
+ *
414
+ * @example
415
+ * element.all(by.css('.items li')).then(function(arr) {
416
+ * expect(arr.length).toEqual(3);
417
+ * });
418
+ *
419
+ * @param {function(Array.<ElementFinder>)} fn
420
+ * @param {function(Error)} errorFn
421
+ *
422
+ * @type {webdriver.promise.Promise} a promise which will resolve to
423
+ * an array of ElementFinders represented by the ElementArrayFinder.
424
+ */
425
+ ElementArrayFinder.prototype.then = function(fn, errorFn) {
426
+ if (this.actionResults_) {
427
+ return this.actionResults_.then(fn, errorFn);
428
+ } else {
429
+ return this.asElementFinders_().then(fn, errorFn);
430
+ }
431
+ };
432
+
433
+ /**
434
+ * Calls the input function on each ElementFinder represented by the ElementArrayFinder.
435
+ *
436
+ * @alias element.all(locator).each(eachFunction)
437
+ * @view
438
+ * <ul class="items">
439
+ * <li>First</li>
440
+ * <li>Second</li>
441
+ * <li>Third</li>
442
+ * </ul>
443
+ *
444
+ * @example
445
+ * element.all(by.css('.items li')).each(function(element) {
446
+ * // Will print First, Second, Third.
447
+ * element.getText().then(console.log);
448
+ * });
449
+ *
450
+ * @param {function(ElementFinder)} fn Input function
451
+ */
452
+ ElementArrayFinder.prototype.each = function(fn) {
453
+ return this.asElementFinders_().then(function(arr) {
454
+ arr.forEach(function(elementFinder, index) {
455
+ fn(elementFinder, index);
456
+ });
457
+ });
458
+ };
459
+
460
+ /**
461
+ * Apply a map function to each element within the ElementArrayFinder. The
462
+ * callback receives the ElementFinder as the first argument and the index as
463
+ * a second arg.
464
+ *
465
+ * @alias element.all(locator).map(mapFunction)
466
+ * @view
467
+ * <ul class="items">
468
+ * <li class="one">First</li>
469
+ * <li class="two">Second</li>
470
+ * <li class="three">Third</li>
471
+ * </ul>
472
+ *
473
+ * @example
474
+ * var items = element.all(by.css('.items li')).map(function(elm, index) {
475
+ * return {
476
+ * index: index,
477
+ * text: elm.getText(),
478
+ * class: elm.getAttribute('class')
479
+ * };
480
+ * });
481
+ * expect(items).toEqual([
482
+ * {index: 0, text: 'First', class: 'one'},
483
+ * {index: 1, text: 'Second', class: 'two'},
484
+ * {index: 2, text: 'Third', class: 'three'}
485
+ * ]);
486
+ *
487
+ * @param {function(ElementFinder, number)} mapFn Map function that
488
+ * will be applied to each element.
489
+ * @return {!webdriver.promise.Promise} A promise that resolves to an array
490
+ * of values returned by the map function.
491
+ */
492
+ ElementArrayFinder.prototype.map = function(mapFn) {
493
+ return this.asElementFinders_().then(function(arr) {
494
+ var list = [];
495
+ arr.forEach(function(elementFinder, index) {
496
+ var mapResult = mapFn(elementFinder, index);
497
+ // All nested arrays and objects will also be fully resolved.
498
+ webdriver.promise.fullyResolved(mapResult).then(function(resolved) {
499
+ list.push(resolved);
500
+ });
501
+ });
502
+ return list;
503
+ });
504
+ };
505
+
506
+ /**
507
+ * Apply a reduce function against an accumulator and every element found
508
+ * using the locator (from left-to-right). The reduce function has to reduce
509
+ * every element into a single value (the accumulator). Returns promise of
510
+ * the accumulator. The reduce function receives the accumulator, current
511
+ * ElementFinder, the index, and the entire array of ElementFinders,
512
+ * respectively.
513
+ *
514
+ * @alias element.all(locator).reduce(reduceFn)
515
+ * @view
516
+ * <ul class="items">
517
+ * <li class="one">First</li>
518
+ * <li class="two">Second</li>
519
+ * <li class="three">Third</li>
520
+ * </ul>
521
+ *
522
+ * @example
523
+ * var value = element.all(by.css('.items li')).reduce(function(acc, elem) {
524
+ * return elem.getText().then(function(text) {
525
+ * return acc + text + ' ';
526
+ * });
527
+ * });
528
+ *
529
+ * expect(value).toEqual('First Second Third ');
530
+ *
531
+ * @param {function(number, ElementFinder, number, Array.<ElementFinder>)}
532
+ * reduceFn Reduce function that reduces every element into a single value.
533
+ * @param {*} initialValue Initial value of the accumulator.
534
+ * @return {!webdriver.promise.Promise} A promise that resolves to the final
535
+ * value of the accumulator.
536
+ */
537
+ ElementArrayFinder.prototype.reduce = function(reduceFn, initialValue) {
538
+ var valuePromise = webdriver.promise.fulfilled(initialValue);
539
+ return this.asElementFinders_().then(function(arr) {
540
+ arr.forEach(function(elementFinder, index) {
541
+ valuePromise = valuePromise.then(function(value) {
542
+ return reduceFn(value, elementFinder, index, arr);
543
+ });
544
+ });
545
+ return valuePromise;
546
+ });
547
+ };
548
+
549
+ /**
550
+ * Evaluates the input as if it were on the scope of the current underlying
551
+ * elements.
552
+ * @param {string} expression
553
+ *
554
+ * @return {ElementArrayFinder} which resolves to the
555
+ * evaluated expression for each underlying element.
556
+ * The result will be resolved as in
557
+ * {@link webdriver.WebDriver.executeScript}. In summary - primitives will
558
+ * be resolved as is, functions will be converted to string, and elements
559
+ * will be returned as a WebElement.
560
+ */
561
+ ElementArrayFinder.prototype.evaluate = function(expression) {
562
+ var evaluationFn = function(webElem) {
563
+ return webElem.getDriver().executeScript(
564
+ clientSideScripts.evaluate, webElem, expression);
565
+ };
566
+ return this.applyAction_(evaluationFn);
567
+ };
568
+
569
+ /**
570
+ * Determine if animation is allowed on the current underlying elements.
571
+ * @param {string} value
572
+ *
573
+ * @return {ElementArrayFinder} which resolves to whether animation is allowed.
574
+ */
575
+ ElementArrayFinder.prototype.allowAnimations = function(value) {
576
+ var allowAnimationsTestFn = function(webElem) {
577
+ return webElem.getDriver().executeScript(
578
+ clientSideScripts.allowAnimations, webElem, value);
579
+ };
580
+ return this.applyAction_(allowAnimationsTestFn);
581
+ };
582
+
583
+ /**
584
+ * The ElementFinder simply represents a single element of an
585
+ * ElementArrayFinder (and is more like a convenience object). As a result,
586
+ * anything that can be done with an ElementFinder, can also be done using
587
+ * an ElementArrayFinder.
588
+ *
589
+ * The ElementFinder can be treated as a WebElement for most purposes, in
590
+ * particular, you may perform actions (i.e. click, getText) on them as you
591
+ * would a WebElement. ElementFinders extend Promise, and once an action
592
+ * is performed on an ElementFinder, the latest result from the chain can be
593
+ * accessed using then. Unlike a WebElement, an ElementFinder will wait for
594
+ * angular to settle before performing finds or actions.
595
+ *
596
+ * ElementFinder can be used to build a chain of locators that is used to find
597
+ * an element. An ElementFinder does not actually attempt to find the element
598
+ * until an action is called, which means they can be set up in helper files
599
+ * before the page is available.
600
+ *
601
+ * @alias element(locator)
602
+ * @view
603
+ * <span>{{person.name}}</span>
604
+ * <span ng-bind="person.email"></span>
605
+ * <input type="text" ng-model="person.name"/>
606
+ *
607
+ * @example
608
+ * // Find element with {{scopeVar}} syntax.
609
+ * element(by.binding('person.name')).getText().then(function(name) {
610
+ * expect(name).toBe('Foo');
611
+ * });
612
+ *
613
+ * // Find element with ng-bind="scopeVar" syntax.
614
+ * expect(element(by.binding('person.email')).getText()).toBe('foo@bar.com');
615
+ *
616
+ * // Find by model.
617
+ * var input = element(by.model('person.name'));
618
+ * input.sendKeys('123');
619
+ * expect(input.getAttribute('value')).toBe('Foo123');
620
+ *
621
+ * @constructor
622
+ * @param {ElementArrayFinder} elementArrayFinder The ElementArrayFinder
623
+ * that this is branched from.
624
+ * @return {ElementFinder}
625
+ */
626
+ var ElementFinder = function(elementArrayFinder) {
627
+ if (!elementArrayFinder) {
628
+ throw new Error('BUG: elementArrayFinder cannot be empty');
629
+ }
630
+ this.parentElementArrayFinder = elementArrayFinder;
631
+
632
+ // This filter verifies that there is only 1 element returned by the
633
+ // elementArrayFinder. It will warn if there are more than 1 element and
634
+ // throw an error if there are no elements.
635
+ var getWebElements = function() {
636
+ return elementArrayFinder.getWebElements().then(function(webElements) {
637
+ if (webElements.length === 0) {
638
+ throw new webdriver.error.Error(
639
+ webdriver.error.ErrorCode.NO_SUCH_ELEMENT,
640
+ 'No element found using locator: ' +
641
+ elementArrayFinder.locator_.toString());
642
+ } else {
643
+ if (webElements.length > 1) {
644
+ console.log('warning: more than one element found for locator ' +
645
+ elementArrayFinder.locator_.toString() +
646
+ ' - you may need to be more specific');
647
+ }
648
+ return [webElements[0]];
649
+ }
650
+ });
651
+ };
652
+
653
+ // Store a copy of the underlying elementArrayFinder, but with the more
654
+ // restrictive getWebElements (which checks that there is only 1 element).
655
+ this.elementArrayFinder_ = new ElementArrayFinder(
656
+ getWebElements, elementArrayFinder.locator_,
657
+ elementArrayFinder.actionResults_);
658
+
659
+ // Decorate ElementFinder with webdriver functions. Simply calls the
660
+ // underlying elementArrayFinder to perform these functions.
661
+ var self = this;
662
+ WEB_ELEMENT_FUNCTIONS.forEach(function(fnName) {
663
+ self[fnName] = function() {
664
+ return self.elementArrayFinder_[fnName].
665
+ apply(self.elementArrayFinder_, arguments).toElementFinder_();
666
+ };
667
+ });
668
+ };
669
+ util.inherits(ElementFinder, webdriver.promise.Promise);
670
+
671
+ /**
672
+ * See ElementArrayFinder.prototype.locator
673
+ * @return {webdriver.Locator}
674
+ */
675
+ ElementFinder.prototype.locator = function() {
676
+ return this.elementArrayFinder_.locator();
677
+ };
678
+
679
+ /**
680
+ * Returns the WebElement represented by this ElementFinder.
681
+ * Throws the WebDriver error if the element doesn't exist.
682
+ *
683
+ * @example
684
+ * The following three expressions are equivalent.
685
+ * - element(by.css('.parent')).getWebElement();
686
+ * - browser.waitForAngular(); browser.driver.findElement(by.css('.parent'));
687
+ * - browser.findElement(by.css('.parent'))
688
+ *
689
+ * @alias element(locator).getWebElement()
690
+ * @return {webdriver.WebElement}
691
+ */
692
+ ElementFinder.prototype.getWebElement = function() {
693
+ var id = this.elementArrayFinder_.getWebElements().then(
694
+ function(parentWebElements) {
695
+ return parentWebElements[0];
696
+ });
697
+ return new webdriver.WebElementPromise(ptor.driver, id);
698
+ };
699
+
700
+ /**
701
+ * Access the underlying actionResult of ElementFinder. Implementation allows
702
+ * ElementFinder to be used as a webdriver.promise.Promise
703
+ * @param {function(webdriver.promise.Promise)} fn Function which takes
704
+ * the value of the underlying actionResult.
705
+ *
706
+ * @return {webdriver.promise.Promise} Promise which contains the results of
707
+ * evaluating fn.
708
+ */
709
+ ElementFinder.prototype.then = function(fn, errorFn) {
710
+ return this.elementArrayFinder_.then(function(actionResults) {
711
+ return fn(actionResults[0]);
712
+ }, errorFn);
713
+ };
714
+
715
+ /**
716
+ * Calls to element may be chained to find an array of elements within a parent.
717
+ *
718
+ * @alias element(locator).all(locator)
719
+ * @view
720
+ * <div class="parent">
721
+ * <ul>
722
+ * <li class="one">First</li>
723
+ * <li class="two">Second</li>
724
+ * <li class="three">Third</li>
725
+ * </ul>
726
+ * </div>
727
+ *
728
+ * @example
729
+ * var items = element(by.css('.parent')).all(by.tagName('li'))
730
+ *
731
+ * @param {webdriver.Locator} subLocator
732
+ * @return {ElementArrayFinder}
733
+ */
734
+ ElementFinder.prototype.all = function(subLocator) {
735
+ return this.elementArrayFinder_.all(subLocator);
736
+ };
737
+
738
+ /**
739
+ * Calls to element may be chained to find elements within a parent.
740
+ *
741
+ * @alias element(locator).element(locator)
742
+ * @view
743
+ * <div class="parent">
744
+ * <div class="child">
745
+ * Child text
746
+ * <div>{{person.phone}}</div>
747
+ * </div>
748
+ * </div>
749
+ *
750
+ * @example
751
+ * // Chain 2 element calls.
752
+ * var child = element(by.css('.parent')).
753
+ * element(by.css('.child'));
754
+ * expect(child.getText()).toBe('Child text\n555-123-4567');
755
+ *
756
+ * // Chain 3 element calls.
757
+ * var triple = element(by.css('.parent')).
758
+ * element(by.css('.child')).
759
+ * element(by.binding('person.phone'));
760
+ * expect(triple.getText()).toBe('555-123-4567');
761
+ *
762
+ * @param {webdriver.Locator} subLocator
763
+ * @return {ElementFinder}
764
+ */
765
+ ElementFinder.prototype.element = function(subLocator) {
766
+ return this.all(subLocator).toElementFinder_();
767
+ };
768
+
769
+ /**
770
+ * Shortcut for querying the document directly with css.
771
+ *
772
+ * @alias $$(cssSelector)
773
+ * @view
774
+ * <div class="count">
775
+ * <span class="one">First</span>
776
+ * <span class="two">Second</span>
777
+ * </div>
778
+ *
779
+ * @example
780
+ * // The following protractor expressions are equivalent.
781
+ * var list = element.all(by.css('.count span'));
782
+ * expect(list.count()).toBe(2);
783
+ *
784
+ * list = $$('.count span');
785
+ * expect(list.count()).toBe(2);
786
+ * expect(list.get(0).getText()).toBe('First');
787
+ * expect(list.get(1).getText()).toBe('Second');
788
+ *
789
+ * @param {string} selector a css selector
790
+ * @return {ElementArrayFinder} which identifies the
791
+ * array of the located {@link webdriver.WebElement}s.
792
+ */
793
+ ElementFinder.prototype.$$ = function(selector) {
794
+ return this.all(webdriver.By.css(selector));
795
+ };
796
+
797
+ /**
798
+ * Shortcut for querying the document directly with css.
799
+ *
800
+ * @alias $(cssSelector)
801
+ * @view
802
+ * <div class="count">
803
+ * <span class="one">First</span>
804
+ * <span class="two">Second</span>
805
+ * </div>
806
+ *
807
+ * @example
808
+ * var item = $('.count .two');
809
+ * expect(item.getText()).toBe('Second');
810
+ *
811
+ * @param {string} selector A css selector
812
+ * @return {ElementFinder} which identifies the located
813
+ * {@link webdriver.WebElement}
814
+ */
815
+ ElementFinder.prototype.$ = function(selector) {
816
+ return this.element(webdriver.By.css(selector));
817
+ };
818
+
819
+ /**
820
+ * Determine whether the element is present on the page.
821
+ *
822
+ * @view
823
+ * <span>{{person.name}}</span>
824
+ *
825
+ * @example
826
+ * // Element exists.
827
+ * expect(element(by.binding('person.name')).isPresent()).toBe(true);
828
+ *
829
+ * // Element not present.
830
+ * expect(element(by.binding('notPresent')).isPresent()).toBe(false);
831
+ *
832
+ * @return {ElementFinder} which resolves to whether
833
+ * the element is present on the page.
834
+ */
835
+ ElementFinder.prototype.isPresent = function() {
836
+ return this.parentElementArrayFinder.count().then(function(count) {
837
+ return !!count;
838
+ });
839
+ };
840
+
841
+ /**
842
+ * Override for WebElement.prototype.isElementPresent so that protractor waits
843
+ * for Angular to settle before making the check.
844
+ *
845
+ * @see ElementFinder.isPresent
846
+ *
847
+ * @param {webdriver.Locator} subLocator Locator for element to look for.
848
+ * @return {ElementFinder} which resolves to whether
849
+ * the element is present on the page.
850
+ */
851
+ ElementFinder.prototype.isElementPresent = function(subLocator) {
852
+ return this.element(subLocator).isPresent();
853
+ };
854
+
855
+ /**
856
+ * Evaluates the input as if it were on the scope of the current element.
857
+ * @param {string} expression
858
+ *
859
+ * @return {ElementFinder} which resolves to the evaluated expression.
860
+ */
861
+ ElementFinder.prototype.evaluate = function(expression) {
862
+ return this.elementArrayFinder_.evaluate(expression).toElementFinder_();
863
+ };
864
+
865
+ /**
866
+ * See ElementArrayFinder.prototype.allowAnimations.
867
+ * @param {string} value
868
+ *
869
+ * @return {ElementFinder} which resolves to whether animation is allowed.
870
+ */
871
+ ElementFinder.prototype.allowAnimations = function(value) {
872
+ return this.elementArrayFinder_.allowAnimations(value).toElementFinder_();
873
+ };
874
+
875
+ /**
876
+ * Webdriver relies on this function to be present on Promises, so adding
877
+ * this dummy function as we inherited from webdriver.promise.Promise, but
878
+ * this function is irrelevant to our usage
879
+ *
880
+ * @return {boolean} Always false as ElementFinder is never in pending state.
881
+ */
882
+ ElementFinder.prototype.isPending = function() {
883
+ return false;
884
+ };
885
+
886
+ var element = function(locator) {
887
+ return new ElementArrayFinder().all(locator).toElementFinder_();
888
+ };
889
+
890
+ element.all = function(locator) {
891
+ return new ElementArrayFinder().all(locator);
892
+ };
893
+
894
+ return element;
895
+ };
896
+ JS