element.js 41 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. const selenium_webdriver_1 = require("selenium-webdriver");
  4. const locators_1 = require("./locators");
  5. const logger_1 = require("./logger");
  6. const util_1 = require("./util");
  7. let clientSideScripts = require('./clientsidescripts');
  8. let logger = new logger_1.Logger('element');
  9. class WebdriverWebElement {
  10. }
  11. exports.WebdriverWebElement = WebdriverWebElement;
  12. let WEB_ELEMENT_FUNCTIONS = [
  13. 'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getText', 'getSize',
  14. 'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getId',
  15. 'takeScreenshot'
  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 satisfy 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. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  60. *
  61. * $$('.items li').then(function(items) {
  62. * expect(items.length).toBe(3);
  63. * expect(items[0].getText()).toBe('First');
  64. * });
  65. *
  66. * @constructor
  67. * @param {ProtractorBrowser} browser A browser instance.
  68. * @param {function(): Array.<webdriver.WebElement>} getWebElements A function
  69. * that returns a list of the underlying Web Elements.
  70. * @param {webdriver.Locator} locator The most relevant locator. It is only
  71. * used for error reporting and ElementArrayFinder.locator.
  72. * @param {Array.<webdriver.promise.Promise>} opt_actionResults An array
  73. * of promises which will be retrieved with then. Resolves to the latest
  74. * action result, or null if no action has been called.
  75. * @returns {ElementArrayFinder}
  76. */
  77. class ElementArrayFinder extends WebdriverWebElement {
  78. constructor(browser_, getWebElements = null, locator_, actionResults_ = null) {
  79. super();
  80. this.browser_ = browser_;
  81. this.getWebElements = getWebElements;
  82. this.locator_ = locator_;
  83. this.actionResults_ = actionResults_;
  84. // TODO(juliemr): might it be easier to combine this with our docs and just
  85. // wrap each one explicity with its own documentation?
  86. WEB_ELEMENT_FUNCTIONS.forEach((fnName) => {
  87. this[fnName] = (...args) => {
  88. let actionFn = (webElem) => {
  89. return webElem[fnName].apply(webElem, args);
  90. };
  91. return this.applyAction_(actionFn);
  92. };
  93. });
  94. }
  95. /**
  96. * Create a shallow copy of ElementArrayFinder.
  97. *
  98. * @returns {!ElementArrayFinder} A shallow copy of this.
  99. */
  100. clone() {
  101. // A shallow copy is all we need since the underlying fields can never be
  102. // modified. (Locator can be modified by the user, but that should
  103. // rarely/never happen and it doesn't affect functionalities).
  104. return new ElementArrayFinder(this.browser_, this.getWebElements, this.locator_, this.actionResults_);
  105. }
  106. /**
  107. * Calls to ElementArrayFinder may be chained to find an array of elements
  108. * using the current elements in this ElementArrayFinder as the starting
  109. * point. This function returns a new ElementArrayFinder which would contain
  110. * the children elements found (and could also be empty).
  111. *
  112. * @alias element.all(locator).all(locator)
  113. * @view
  114. * <div id='id1' class="parent">
  115. * <ul>
  116. * <li class="foo">1a</li>
  117. * <li class="baz">1b</li>
  118. * </ul>
  119. * </div>
  120. * <div id='id2' class="parent">
  121. * <ul>
  122. * <li class="foo">2a</li>
  123. * <li class="bar">2b</li>
  124. * </ul>
  125. * </div>
  126. *
  127. * @example
  128. * let foo = element.all(by.css('.parent')).all(by.css('.foo'));
  129. * expect(foo.getText()).toEqual(['1a', '2a']);
  130. * let baz = element.all(by.css('.parent')).all(by.css('.baz'));
  131. * expect(baz.getText()).toEqual(['1b']);
  132. * let nonexistent = element.all(by.css('.parent'))
  133. * .all(by.css('.NONEXISTENT'));
  134. * expect(nonexistent.getText()).toEqual(['']);
  135. *
  136. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  137. *
  138. * let foo = $$('.parent').$$('.foo');
  139. * expect(foo.getText()).toEqual(['1a', '2a']);
  140. * let baz = $$('.parent').$$('.baz');
  141. * expect(baz.getText()).toEqual(['1b']);
  142. * let nonexistent = $$('.parent').$$('.NONEXISTENT');
  143. * expect(nonexistent.getText()).toEqual(['']);
  144. *
  145. * @param {webdriver.Locator} subLocator
  146. * @returns {ElementArrayFinder}
  147. */
  148. all(locator) {
  149. let ptor = this.browser_;
  150. let getWebElements = () => {
  151. if (this.getWebElements === null) {
  152. // This is the first time we are looking for an element
  153. return ptor.waitForAngular('Locator: ' + locator)
  154. .then(() => {
  155. if (locators_1.isProtractorLocator(locator)) {
  156. return locator.findElementsOverride(ptor.driver, null, ptor.rootEl);
  157. }
  158. else {
  159. return ptor.driver.findElements(locator);
  160. }
  161. });
  162. }
  163. else {
  164. return this.getWebElements().then((parentWebElements) => {
  165. // For each parent web element, find their children and construct
  166. // a list of Promise<List<child_web_element>>
  167. let childrenPromiseList = parentWebElements.map((parentWebElement) => {
  168. return locators_1.isProtractorLocator(locator) ?
  169. locator.findElementsOverride(ptor.driver, parentWebElement, ptor.rootEl) :
  170. parentWebElement.findElements(locator);
  171. });
  172. // Resolve the list of Promise<List<child_web_elements>> and merge
  173. // into a single list
  174. return selenium_webdriver_1.promise.all(childrenPromiseList)
  175. .then((resolved) => {
  176. return resolved.reduce((childrenList, resolvedE) => {
  177. return childrenList.concat(resolvedE);
  178. }, []);
  179. });
  180. });
  181. }
  182. };
  183. return new ElementArrayFinder(this.browser_, getWebElements, locator);
  184. }
  185. /**
  186. * Apply a filter function to each element within the ElementArrayFinder.
  187. * Returns a new ElementArrayFinder with all elements that pass the filter
  188. * function. The filter function receives the ElementFinder as the first
  189. * argument and the index as a second arg. This does not actually retrieve
  190. * the underlying list of elements, so it can be used in page objects.
  191. *
  192. * @alias element.all(locator).filter(filterFn)
  193. * @view
  194. * <ul class="items">
  195. * <li class="one">First</li>
  196. * <li class="two">Second</li>
  197. * <li class="three">Third</li>
  198. * </ul>
  199. *
  200. * @example
  201. * element.all(by.css('.items li')).filter(function(elem, index) {
  202. * return elem.getText().then(function(text) {
  203. * return text === 'Third';
  204. * });
  205. * }).first().click();
  206. *
  207. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  208. *
  209. * $$('.items li').filter(function(elem, index) {
  210. * return elem.getText().then(function(text) {
  211. * return text === 'Third';
  212. * });
  213. * }).first().click();
  214. *
  215. * @param {function(ElementFinder, number): webdriver.WebElement.Promise}
  216. * filterFn
  217. * Filter function that will test if an element should be returned.
  218. * filterFn can either return a boolean or a promise that resolves to a
  219. * boolean
  220. * @returns {!ElementArrayFinder} A ElementArrayFinder that represents an
  221. * array
  222. * of element that satisfy the filter function.
  223. */
  224. filter(filterFn) {
  225. let getWebElements = () => {
  226. return this.getWebElements().then((parentWebElements) => {
  227. let list = parentWebElements.map((parentWebElement, index) => {
  228. let elementFinder = ElementFinder.fromWebElement_(this.browser_, parentWebElement, this.locator_);
  229. return filterFn(elementFinder, index);
  230. });
  231. return selenium_webdriver_1.promise.all(list).then((resolvedList) => {
  232. return parentWebElements.filter((parentWebElement, index) => {
  233. return resolvedList[index];
  234. });
  235. });
  236. });
  237. };
  238. return new ElementArrayFinder(this.browser_, getWebElements, this.locator_);
  239. }
  240. /**
  241. * Get an element within the ElementArrayFinder by index. The index starts at 0.
  242. * Negative indices are wrapped (i.e. -i means ith element from last)
  243. * This does not actually retrieve the underlying element.
  244. *
  245. * @alias element.all(locator).get(index)
  246. * @view
  247. * <ul class="items">
  248. * <li>First</li>
  249. * <li>Second</li>
  250. * <li>Third</li>
  251. * </ul>
  252. *
  253. * @example
  254. * let list = element.all(by.css('.items li'));
  255. * expect(list.get(0).getText()).toBe('First');
  256. * expect(list.get(1).getText()).toBe('Second');
  257. *
  258. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  259. *
  260. * let list = $$('.items li');
  261. * expect(list.get(0).getText()).toBe('First');
  262. * expect(list.get(1).getText()).toBe('Second');
  263. *
  264. * @param {number|webdriver.promise.Promise} index Element index.
  265. * @returns {ElementFinder} finder representing element at the given index.
  266. */
  267. get(index) {
  268. let getWebElements = () => {
  269. return selenium_webdriver_1.promise.all([index, this.getWebElements()]).then(([i, parentWebElements]) => {
  270. if (i < 0) {
  271. i += parentWebElements.length;
  272. }
  273. if (i < 0 || i >= parentWebElements.length) {
  274. throw new selenium_webdriver_1.error.NoSuchElementError('Index out of bound. Trying to access element at index: ' + index +
  275. ', but there are only ' + parentWebElements.length + ' elements that match ' +
  276. 'locator ' + this.locator_.toString());
  277. }
  278. return [parentWebElements[i]];
  279. });
  280. };
  281. return new ElementArrayFinder(this.browser_, getWebElements, this.locator_).toElementFinder_();
  282. }
  283. /**
  284. * Get the first matching element for the ElementArrayFinder. This does not
  285. * actually retrieve the underlying element.
  286. *
  287. * @alias element.all(locator).first()
  288. * @view
  289. * <ul class="items">
  290. * <li>First</li>
  291. * <li>Second</li>
  292. * <li>Third</li>
  293. * </ul>
  294. *
  295. * @example
  296. * let first = element.all(by.css('.items li')).first();
  297. * expect(first.getText()).toBe('First');
  298. *
  299. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  300. *
  301. * let first = $$('.items li').first();
  302. * expect(first.getText()).toBe('First');
  303. *
  304. * @returns {ElementFinder} finder representing the first matching element
  305. */
  306. first() {
  307. return this.get(0);
  308. }
  309. ;
  310. /**
  311. * Get the last matching element for the ElementArrayFinder. This does not
  312. * actually retrieve the underlying element.
  313. *
  314. * @alias element.all(locator).last()
  315. * @view
  316. * <ul class="items">
  317. * <li>First</li>
  318. * <li>Second</li>
  319. * <li>Third</li>
  320. * </ul>
  321. *
  322. * @example
  323. * let last = element.all(by.css('.items li')).last();
  324. * expect(last.getText()).toBe('Third');
  325. *
  326. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  327. *
  328. * let last = $$('.items li').last();
  329. * expect(last.getText()).toBe('Third');
  330. *
  331. * @returns {ElementFinder} finder representing the last matching element
  332. */
  333. last() {
  334. return this.get(-1);
  335. }
  336. /**
  337. * Shorthand function for finding arrays of elements by css.
  338. * `element.all(by.css('.abc'))` is equivalent to `$$('.abc')`
  339. *
  340. * @alias $$(cssSelector)
  341. * @view
  342. * <div class="count">
  343. * <span class="one">First</span>
  344. * <span class="two">Second</span>
  345. * </div>
  346. *
  347. * @example
  348. * // The following two blocks of code are equivalent.
  349. * let list = element.all(by.css('.count span'));
  350. * expect(list.count()).toBe(2);
  351. * expect(list.get(0).getText()).toBe('First');
  352. * expect(list.get(1).getText()).toBe('Second');
  353. *
  354. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  355. *
  356. * let list = $$('.count span');
  357. * expect(list.count()).toBe(2);
  358. * expect(list.get(0).getText()).toBe('First');
  359. * expect(list.get(1).getText()).toBe('Second');
  360. *
  361. * @param {string} selector a css selector
  362. * @returns {ElementArrayFinder} which identifies the
  363. * array of the located {@link webdriver.WebElement}s.
  364. */
  365. $$(selector) {
  366. return this.all(selenium_webdriver_1.By.css(selector));
  367. }
  368. /**
  369. * Returns an ElementFinder representation of ElementArrayFinder. It ensures
  370. * that the ElementArrayFinder resolves to one and only one underlying
  371. * element.
  372. *
  373. * @returns {ElementFinder} An ElementFinder representation
  374. * @private
  375. */
  376. toElementFinder_() {
  377. return new ElementFinder(this.browser_, this);
  378. }
  379. /**
  380. * Count the number of elements represented by the ElementArrayFinder.
  381. *
  382. * @alias element.all(locator).count()
  383. * @view
  384. * <ul class="items">
  385. * <li>First</li>
  386. * <li>Second</li>
  387. * <li>Third</li>
  388. * </ul>
  389. *
  390. * @example
  391. * let list = element.all(by.css('.items li'));
  392. * expect(list.count()).toBe(3);
  393. *
  394. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  395. *
  396. * let list = $$('.items li');
  397. * expect(list.count()).toBe(3);
  398. *
  399. * @returns {!webdriver.promise.Promise} A promise which resolves to the
  400. * number of elements matching the locator.
  401. */
  402. count() {
  403. return this.getWebElements().then((arr) => {
  404. return arr.length;
  405. }, (err) => {
  406. if (err instanceof selenium_webdriver_1.error.NoSuchElementError) {
  407. return 0;
  408. }
  409. else {
  410. throw err;
  411. }
  412. });
  413. }
  414. /**
  415. * Returns true if there are any elements present that match the finder.
  416. *
  417. * @alias element.all(locator).isPresent()
  418. *
  419. * @example
  420. * expect($('.item').isPresent()).toBeTruthy();
  421. *
  422. * @returns {Promise<boolean>}
  423. */
  424. isPresent() {
  425. return this.count().then((count) => {
  426. return count > 0;
  427. });
  428. }
  429. /**
  430. * Returns the most relevant locator.
  431. *
  432. * @example
  433. * // returns by.css('#ID1')
  434. * $('#ID1').locator();
  435. *
  436. * // returns by.css('#ID2')
  437. * $('#ID1').$('#ID2').locator();
  438. *
  439. * // returns by.css('#ID1')
  440. * $$('#ID1').filter(filterFn).get(0).click().locator();
  441. *
  442. * @returns {webdriver.Locator}
  443. */
  444. locator() {
  445. return this.locator_;
  446. }
  447. /**
  448. * Apply an action function to every element in the ElementArrayFinder,
  449. * and return a new ElementArrayFinder that contains the results of the
  450. * actions.
  451. *
  452. * @param {function(ElementFinder)} actionFn
  453. *
  454. * @returns {ElementArrayFinder}
  455. * @private
  456. */
  457. // map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
  458. applyAction_(actionFn) {
  459. let callerError = new Error();
  460. let actionResults = this.getWebElements()
  461. .then((arr) => selenium_webdriver_1.promise.all(arr.map(actionFn)))
  462. .then((value) => {
  463. return { passed: true, value: value };
  464. }, (error) => {
  465. return { passed: false, value: error };
  466. });
  467. let getWebElements = () => actionResults.then(() => this.getWebElements());
  468. actionResults = actionResults.then((result) => {
  469. if (result.passed) {
  470. return result.value;
  471. }
  472. else {
  473. let noSuchErr;
  474. if (result.value instanceof Error) {
  475. noSuchErr = result.value;
  476. noSuchErr.stack = noSuchErr.stack + callerError.stack;
  477. }
  478. else {
  479. noSuchErr = new Error(result.value);
  480. noSuchErr.stack = callerError.stack;
  481. }
  482. throw noSuchErr;
  483. }
  484. });
  485. return new ElementArrayFinder(this.browser_, getWebElements, this.locator_, actionResults);
  486. }
  487. /**
  488. * Represents the ElementArrayFinder as an array of ElementFinders.
  489. *
  490. * @returns {Array.<ElementFinder>} Return a promise, which resolves to a list
  491. * of ElementFinders specified by the locator.
  492. */
  493. asElementFinders_() {
  494. return this.getWebElements().then((arr) => {
  495. return arr.map((webElem) => {
  496. return ElementFinder.fromWebElement_(this.browser_, webElem, this.locator_);
  497. });
  498. });
  499. }
  500. /**
  501. * Retrieve the elements represented by the ElementArrayFinder. The input
  502. * function is passed to the resulting promise, which resolves to an
  503. * array of ElementFinders.
  504. *
  505. * @alias element.all(locator).then(thenFunction)
  506. * @view
  507. * <ul class="items">
  508. * <li>First</li>
  509. * <li>Second</li>
  510. * <li>Third</li>
  511. * </ul>
  512. *
  513. * @example
  514. * element.all(by.css('.items li')).then(function(arr) {
  515. * expect(arr.length).toEqual(3);
  516. * });
  517. *
  518. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  519. *
  520. * $$('.items li').then(function(arr) {
  521. * expect(arr.length).toEqual(3);
  522. * });
  523. *
  524. * @param {function(Array.<ElementFinder>)} fn
  525. * @param {function(Error)} errorFn
  526. *
  527. * @returns {!webdriver.promise.Promise} A promise which will resolve to
  528. * an array of ElementFinders represented by the ElementArrayFinder.
  529. */
  530. then(fn, errorFn) {
  531. if (this.actionResults_) {
  532. return this.actionResults_.then(fn, errorFn);
  533. }
  534. else {
  535. return this.asElementFinders_().then(fn, errorFn);
  536. }
  537. }
  538. /**
  539. * Calls the input function on each ElementFinder represented by the
  540. * ElementArrayFinder.
  541. *
  542. * @alias element.all(locator).each(eachFunction)
  543. * @view
  544. * <ul class="items">
  545. * <li>First</li>
  546. * <li>Second</li>
  547. * <li>Third</li>
  548. * </ul>
  549. *
  550. * @example
  551. * element.all(by.css('.items li')).each(function(element, index) {
  552. * // Will print 0 First, 1 Second, 2 Third.
  553. * element.getText().then(function (text) {
  554. * console.log(index, text);
  555. * });
  556. * });
  557. *
  558. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  559. *
  560. * $$('.items li').each(function(element, index) {
  561. * // Will print 0 First, 1 Second, 2 Third.
  562. * element.getText().then(function (text) {
  563. * console.log(index, text);
  564. * });
  565. * });
  566. *
  567. * @param {function(ElementFinder)} fn Input function
  568. *
  569. * @returns {!webdriver.promise.Promise} A promise that will resolve when the
  570. * function has been called on all the ElementFinders. The promise will
  571. * resolve to null.
  572. */
  573. each(fn) {
  574. return this.map(fn).then(() => {
  575. return null;
  576. });
  577. }
  578. /**
  579. * Apply a map function to each element within the ElementArrayFinder. The
  580. * callback receives the ElementFinder as the first argument and the index as
  581. * a second arg.
  582. *
  583. * @alias element.all(locator).map(mapFunction)
  584. * @view
  585. * <ul class="items">
  586. * <li class="one">First</li>
  587. * <li class="two">Second</li>
  588. * <li class="three">Third</li>
  589. * </ul>
  590. *
  591. * @example
  592. * let items = element.all(by.css('.items li')).map(function(elm, index) {
  593. * return {
  594. * index: index,
  595. * text: elm.getText(),
  596. * class: elm.getAttribute('class')
  597. * };
  598. * });
  599. * expect(items).toEqual([
  600. * {index: 0, text: 'First', class: 'one'},
  601. * {index: 1, text: 'Second', class: 'two'},
  602. * {index: 2, text: 'Third', class: 'three'}
  603. * ]);
  604. *
  605. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  606. *
  607. * let items = $$('.items li').map(function(elm, index) {
  608. * return {
  609. * index: index,
  610. * text: elm.getText(),
  611. * class: elm.getAttribute('class')
  612. * };
  613. * });
  614. * expect(items).toEqual([
  615. * {index: 0, text: 'First', class: 'one'},
  616. * {index: 1, text: 'Second', class: 'two'},
  617. * {index: 2, text: 'Third', class: 'three'}
  618. * ]);
  619. *
  620. * @param {function(ElementFinder, number)} mapFn Map function that
  621. * will be applied to each element.
  622. * @returns {!webdriver.promise.Promise} A promise that resolves to an array
  623. * of values returned by the map function.
  624. */
  625. map(mapFn) {
  626. return this.asElementFinders_().then((arr) => {
  627. let list = arr.map((elementFinder, index) => {
  628. let mapResult = mapFn(elementFinder, index);
  629. // All nested arrays and objects will also be fully resolved.
  630. return selenium_webdriver_1.promise.fullyResolved(mapResult);
  631. });
  632. return selenium_webdriver_1.promise.all(list);
  633. });
  634. }
  635. ;
  636. /**
  637. * Apply a reduce function against an accumulator and every element found
  638. * using the locator (from left-to-right). The reduce function has to reduce
  639. * every element into a single value (the accumulator). Returns promise of
  640. * the accumulator. The reduce function receives the accumulator, current
  641. * ElementFinder, the index, and the entire array of ElementFinders,
  642. * respectively.
  643. *
  644. * @alias element.all(locator).reduce(reduceFn)
  645. * @view
  646. * <ul class="items">
  647. * <li class="one">First</li>
  648. * <li class="two">Second</li>
  649. * <li class="three">Third</li>
  650. * </ul>
  651. *
  652. * @example
  653. * let value = element.all(by.css('.items li')).reduce(function(acc, elem) {
  654. * return elem.getText().then(function(text) {
  655. * return acc + text + ' ';
  656. * });
  657. * }, '');
  658. *
  659. * expect(value).toEqual('First Second Third ');
  660. *
  661. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  662. *
  663. * let value = $$('.items li').reduce(function(acc, elem) {
  664. * return elem.getText().then(function(text) {
  665. * return acc + text + ' ';
  666. * });
  667. * }, '');
  668. *
  669. * expect(value).toEqual('First Second Third ');
  670. *
  671. * @param {function(number, ElementFinder, number, Array.<ElementFinder>)}
  672. * reduceFn Reduce function that reduces every element into a single
  673. * value.
  674. * @param {*} initialValue Initial value of the accumulator.
  675. * @returns {!webdriver.promise.Promise} A promise that resolves to the final
  676. * value of the accumulator.
  677. */
  678. reduce(reduceFn, initialValue) {
  679. let valuePromise = selenium_webdriver_1.promise.when(initialValue);
  680. return this.asElementFinders_().then((arr) => {
  681. return arr.reduce((valuePromise, elementFinder, index) => {
  682. return valuePromise.then((value) => {
  683. return reduceFn(value, elementFinder, index, arr);
  684. });
  685. }, valuePromise);
  686. });
  687. }
  688. /**
  689. * Evaluates the input as if it were on the scope of the current underlying
  690. * elements.
  691. *
  692. * @view
  693. * <span class="foo">{{letiableInScope}}</span>
  694. *
  695. * @example
  696. * let value = element.all(by.css('.foo')).evaluate('letiableInScope');
  697. *
  698. * // Or using the shortcut $$() notation instead of element.all(by.css()):
  699. *
  700. * let value = $$('.foo').evaluate('letiableInScope');
  701. *
  702. * @param {string} expression
  703. *
  704. * @returns {ElementArrayFinder} which resolves to the
  705. * evaluated expression for each underlying element.
  706. * The result will be resolved as in
  707. * {@link webdriver.WebDriver.executeScript}. In summary - primitives will
  708. * be resolved as is, functions will be converted to string, and elements
  709. * will be returned as a WebElement.
  710. */
  711. evaluate(expression) {
  712. let evaluationFn = (webElem) => {
  713. return webElem.getDriver().executeScript(clientSideScripts.evaluate, webElem, expression);
  714. };
  715. return this.applyAction_(evaluationFn);
  716. }
  717. /**
  718. * Determine if animation is allowed on the current underlying elements.
  719. * @param {string} value
  720. *
  721. * @example
  722. * // Turns off ng-animate animations for all elements in the <body>
  723. * element(by.css('body')).allowAnimations(false);
  724. *
  725. * // Or using the shortcut $() notation instead of element(by.css()):
  726. *
  727. * $('body').allowAnimations(false);
  728. *
  729. * @returns {ElementArrayFinder} which resolves to whether animation is
  730. * allowed.
  731. */
  732. allowAnimations(value) {
  733. let allowAnimationsTestFn = (webElem) => {
  734. return webElem.getDriver().executeScript(clientSideScripts.allowAnimations, webElem, value);
  735. };
  736. return this.applyAction_(allowAnimationsTestFn);
  737. }
  738. }
  739. exports.ElementArrayFinder = ElementArrayFinder;
  740. /**
  741. * The ElementFinder simply represents a single element of an
  742. * ElementArrayFinder (and is more like a convenience object). As a result,
  743. * anything that can be done with an ElementFinder, can also be done using
  744. * an ElementArrayFinder.
  745. *
  746. * The ElementFinder can be treated as a WebElement for most purposes, in
  747. * particular, you may perform actions (i.e. click, getText) on them as you
  748. * would a WebElement. Once an action is performed on an ElementFinder, the
  749. * latest result from the chain can be accessed using the then method.
  750. * Unlike a WebElement, an ElementFinder will wait for angular to settle before
  751. * performing finds or actions.
  752. *
  753. * ElementFinder can be used to build a chain of locators that is used to find
  754. * an element. An ElementFinder does not actually attempt to find the element
  755. * until an action is called, which means they can be set up in helper files
  756. * before the page is available.
  757. *
  758. * @alias element(locator)
  759. * @view
  760. * <span>{{person.name}}</span>
  761. * <span ng-bind="person.email"></span>
  762. * <input type="text" ng-model="person.name"/>
  763. *
  764. * @example
  765. * // Find element with {{scopelet}} syntax.
  766. * element(by.binding('person.name')).getText().then(function(name) {
  767. * expect(name).toBe('Foo');
  768. * });
  769. *
  770. * // Find element with ng-bind="scopelet" syntax.
  771. * expect(element(by.binding('person.email')).getText()).toBe('foo@bar.com');
  772. *
  773. * // Find by model.
  774. * let input = element(by.model('person.name'));
  775. * input.sendKeys('123');
  776. * expect(input.getAttribute('value')).toBe('Foo123');
  777. *
  778. * @constructor
  779. * @extends {webdriver.WebElement}
  780. * @param {ProtractorBrowser} browser_ A browser instance.
  781. * @param {ElementArrayFinder} elementArrayFinder The ElementArrayFinder
  782. * that this is branched from.
  783. * @returns {ElementFinder}
  784. */
  785. class ElementFinder extends WebdriverWebElement {
  786. constructor(browser_, elementArrayFinder) {
  787. super();
  788. this.browser_ = browser_;
  789. this.then = null;
  790. if (!elementArrayFinder) {
  791. throw new Error('BUG: elementArrayFinder cannot be empty');
  792. }
  793. this.parentElementArrayFinder = elementArrayFinder;
  794. // Only have a `then` method if the parent element array finder
  795. // has action results.
  796. if (this.parentElementArrayFinder.actionResults_) {
  797. // Access the underlying actionResult of ElementFinder.
  798. this.then =
  799. (fn, errorFn) => {
  800. return this.elementArrayFinder_.then((actionResults) => {
  801. if (!fn) {
  802. return actionResults[0];
  803. }
  804. return fn(actionResults[0]);
  805. }, errorFn);
  806. };
  807. }
  808. // This filter verifies that there is only 1 element returned by the
  809. // elementArrayFinder. It will warn if there are more than 1 element and
  810. // throw an error if there are no elements.
  811. let getWebElements = () => {
  812. return elementArrayFinder.getWebElements().then((webElements) => {
  813. if (webElements.length === 0) {
  814. throw new selenium_webdriver_1.error.NoSuchElementError('No element found using locator: ' + elementArrayFinder.locator().toString());
  815. }
  816. else {
  817. if (webElements.length > 1) {
  818. logger.warn('more than one element found for locator ' +
  819. elementArrayFinder.locator().toString() + ' - the first result will be used');
  820. }
  821. return [webElements[0]];
  822. }
  823. });
  824. };
  825. // Store a copy of the underlying elementArrayFinder, but with the more
  826. // restrictive getWebElements (which checks that there is only 1 element).
  827. this.elementArrayFinder_ = new ElementArrayFinder(this.browser_, getWebElements, elementArrayFinder.locator(), elementArrayFinder.actionResults_);
  828. WEB_ELEMENT_FUNCTIONS.forEach((fnName) => {
  829. (this)[fnName] = (...args) => {
  830. return (this.elementArrayFinder_)[fnName]
  831. .apply(this.elementArrayFinder_, args)
  832. .toElementFinder_();
  833. };
  834. });
  835. }
  836. static fromWebElement_(browser, webElem, locator) {
  837. let getWebElements = () => {
  838. return selenium_webdriver_1.promise.when([webElem]);
  839. };
  840. return new ElementArrayFinder(browser, getWebElements, locator).toElementFinder_();
  841. }
  842. /**
  843. * Create a shallow copy of ElementFinder.
  844. *
  845. * @returns {!ElementFinder} A shallow copy of this.
  846. */
  847. clone() {
  848. // A shallow copy is all we need since the underlying fields can never be
  849. // modified
  850. return new ElementFinder(this.browser_, this.parentElementArrayFinder);
  851. }
  852. /**
  853. * @see ElementArrayFinder.prototype.locator
  854. *
  855. * @returns {webdriver.Locator}
  856. */
  857. locator() {
  858. return this.elementArrayFinder_.locator();
  859. }
  860. /**
  861. * Returns the WebElement represented by this ElementFinder.
  862. * Throws the WebDriver error if the element doesn't exist.
  863. *
  864. * @alias element(locator).getWebElement()
  865. * @view
  866. * <div class="parent">
  867. * some text
  868. * </div>
  869. *
  870. * @example
  871. * // The following four expressions are equivalent.
  872. * $('.parent').getWebElement();
  873. * element(by.css('.parent')).getWebElement();
  874. * browser.driver.findElement(by.css('.parent'));
  875. * browser.findElement(by.css('.parent'));
  876. *
  877. * @returns {webdriver.WebElementPromise}
  878. */
  879. getWebElement() {
  880. let id = this.elementArrayFinder_.getWebElements().then((parentWebElements) => {
  881. return parentWebElements[0];
  882. });
  883. return new selenium_webdriver_1.WebElementPromise(this.browser_.driver, id);
  884. }
  885. /**
  886. * Calls to {@code all} may be chained to find an array of elements within a
  887. * parent.
  888. *
  889. * @alias element(locator).all(locator)
  890. * @view
  891. * <div class="parent">
  892. * <ul>
  893. * <li class="one">First</li>
  894. * <li class="two">Second</li>
  895. * <li class="three">Third</li>
  896. * </ul>
  897. * </div>
  898. *
  899. * @example
  900. * let items = element(by.css('.parent')).all(by.tagName('li'));
  901. *
  902. * // Or using the shortcut $() notation instead of element(by.css()):
  903. *
  904. * let items = $('.parent').all(by.tagName('li'));
  905. *
  906. * @param {webdriver.Locator} subLocator
  907. * @returns {ElementArrayFinder}
  908. */
  909. all(subLocator) {
  910. return this.elementArrayFinder_.all(subLocator);
  911. }
  912. /**
  913. * Calls to {@code element} may be chained to find elements within a parent.
  914. *
  915. * @alias element(locator).element(locator)
  916. * @view
  917. * <div class="parent">
  918. * <div class="child">
  919. * Child text
  920. * <div>{{person.phone}}</div>
  921. * </div>
  922. * </div>
  923. *
  924. * @example
  925. * // Chain 2 element calls.
  926. * let child = element(by.css('.parent')).
  927. * element(by.css('.child'));
  928. * expect(child.getText()).toBe('Child text\n555-123-4567');
  929. *
  930. * // Chain 3 element calls.
  931. * let triple = element(by.css('.parent')).
  932. * element(by.css('.child')).
  933. * element(by.binding('person.phone'));
  934. * expect(triple.getText()).toBe('555-123-4567');
  935. *
  936. * // Or using the shortcut $() notation instead of element(by.css()):
  937. *
  938. * // Chain 2 element calls.
  939. * let child = $('.parent').$('.child');
  940. * expect(child.getText()).toBe('Child text\n555-123-4567');
  941. *
  942. * // Chain 3 element calls.
  943. * let triple = $('.parent').$('.child').
  944. * element(by.binding('person.phone'));
  945. * expect(triple.getText()).toBe('555-123-4567');
  946. *
  947. * @param {webdriver.Locator} subLocator
  948. * @returns {ElementFinder}
  949. */
  950. element(subLocator) {
  951. return this.all(subLocator).toElementFinder_();
  952. }
  953. /**
  954. * Calls to {@code $$} may be chained to find an array of elements within a
  955. * parent.
  956. *
  957. * @alias element(locator).all(selector)
  958. * @view
  959. * <div class="parent">
  960. * <ul>
  961. * <li class="one">First</li>
  962. * <li class="two">Second</li>
  963. * <li class="three">Third</li>
  964. * </ul>
  965. * </div>
  966. *
  967. * @example
  968. * let items = element(by.css('.parent')).$$('li');
  969. *
  970. * // Or using the shortcut $() notation instead of element(by.css()):
  971. *
  972. * let items = $('.parent').$$('li');
  973. *
  974. * @param {string} selector a css selector
  975. * @returns {ElementArrayFinder}
  976. */
  977. $$(selector) {
  978. return this.all(selenium_webdriver_1.By.css(selector));
  979. }
  980. /**
  981. * Calls to {@code $} may be chained to find elements within a parent.
  982. *
  983. * @alias element(locator).$(selector)
  984. * @view
  985. * <div class="parent">
  986. * <div class="child">
  987. * Child text
  988. * <div>{{person.phone}}</div>
  989. * </div>
  990. * </div>
  991. *
  992. * @example
  993. * // Chain 2 element calls.
  994. * let child = element(by.css('.parent')).
  995. * $('.child');
  996. * expect(child.getText()).toBe('Child text\n555-123-4567');
  997. *
  998. * // Chain 3 element calls.
  999. * let triple = element(by.css('.parent')).
  1000. * $('.child').
  1001. * element(by.binding('person.phone'));
  1002. * expect(triple.getText()).toBe('555-123-4567');
  1003. *
  1004. * // Or using the shortcut $() notation instead of element(by.css()):
  1005. *
  1006. * // Chain 2 element calls.
  1007. * let child = $('.parent').$('.child');
  1008. * expect(child.getText()).toBe('Child text\n555-123-4567');
  1009. *
  1010. * // Chain 3 element calls.
  1011. * let triple = $('.parent').$('.child').
  1012. * element(by.binding('person.phone'));
  1013. * expect(triple.getText()).toBe('555-123-4567');
  1014. *
  1015. * @param {string} selector A css selector
  1016. * @returns {ElementFinder}
  1017. */
  1018. $(selector) {
  1019. return this.element(selenium_webdriver_1.By.css(selector));
  1020. }
  1021. /**
  1022. * Determine whether the element is present on the page.
  1023. *
  1024. * @view
  1025. * <span>{{person.name}}</span>
  1026. *
  1027. * @example
  1028. * // Element exists.
  1029. * expect(element(by.binding('person.name')).isPresent()).toBe(true);
  1030. *
  1031. * // Element not present.
  1032. * expect(element(by.binding('notPresent')).isPresent()).toBe(false);
  1033. *
  1034. * @returns {webdriver.promise.Promise<boolean>} which resolves to whether
  1035. * the element is present on the page.
  1036. */
  1037. isPresent() {
  1038. return this.parentElementArrayFinder.getWebElements().then((arr) => {
  1039. if (arr.length === 0) {
  1040. return false;
  1041. }
  1042. return arr[0].isEnabled().then(() => {
  1043. return true; // is present, whether it is enabled or not
  1044. }, util_1.falseIfMissing);
  1045. }, util_1.falseIfMissing);
  1046. }
  1047. /**
  1048. * Same as ElementFinder.isPresent(), except this checks whether the element
  1049. * identified by the subLocator is present, rather than the current element
  1050. * finder, i.e.: `element(by.css('#abc')).element(by.css('#def')).isPresent()`
  1051. * is identical to `element(by.css('#abc')).isElementPresent(by.css('#def'))`.
  1052. *
  1053. * // Or using the shortcut $() notation instead of element(by.css()):
  1054. *
  1055. * `$('#abc').$('#def').isPresent()` is identical to
  1056. * `$('#abc').isElementPresent($('#def'))`.
  1057. *
  1058. * @see ElementFinder.isPresent
  1059. *
  1060. * @param {webdriver.Locator} subLocator Locator for element to look for.
  1061. * @returns {webdriver.promise.Promise<boolean>} which resolves to whether
  1062. * the subelement is present on the page.
  1063. */
  1064. isElementPresent(subLocator) {
  1065. if (!subLocator) {
  1066. throw new Error('SubLocator is not supplied as a parameter to ' +
  1067. '`isElementPresent(subLocator)`. You are probably looking for the ' +
  1068. 'function `isPresent()`.');
  1069. }
  1070. return this.element(subLocator).isPresent();
  1071. }
  1072. /**
  1073. * Evaluates the input as if it were on the scope of the current element.
  1074. * @see ElementArrayFinder.prototype.evaluate
  1075. *
  1076. * @view
  1077. * <span id="foo">{{letiableInScope}}</span>
  1078. *
  1079. * @example
  1080. * let value = element(by.id('foo')).evaluate('letiableInScope');
  1081. *
  1082. * @param {string} expression
  1083. *
  1084. * @returns {ElementFinder} which resolves to the evaluated expression.
  1085. */
  1086. evaluate(expression) {
  1087. return this.elementArrayFinder_.evaluate(expression).toElementFinder_();
  1088. }
  1089. /**
  1090. * @see ElementArrayFinder.prototype.allowAnimations.
  1091. * @param {string} value
  1092. *
  1093. * @returns {ElementFinder} which resolves to whether animation is allowed.
  1094. */
  1095. allowAnimations(value) {
  1096. return this.elementArrayFinder_.allowAnimations(value).toElementFinder_();
  1097. }
  1098. /**
  1099. * Compares an element to this one for equality.
  1100. *
  1101. * @param {!ElementFinder|!webdriver.WebElement} The element to compare to.
  1102. *
  1103. * @returns {!webdriver.promise.Promise.<boolean>} A promise that will be
  1104. * resolved to whether the two WebElements are equal.
  1105. */
  1106. equals(element) {
  1107. return selenium_webdriver_1.WebElement.equals(this.getWebElement(), element.getWebElement ? element.getWebElement() :
  1108. element);
  1109. }
  1110. }
  1111. exports.ElementFinder = ElementFinder;
  1112. /**
  1113. * Shortcut for querying the document directly with css.
  1114. * `element(by.css('.abc'))` is equivalent to `$('.abc')`
  1115. *
  1116. * @alias $(cssSelector)
  1117. * @view
  1118. * <div class="count">
  1119. * <span class="one">First</span>
  1120. * <span class="two">Second</span>
  1121. * </div>
  1122. *
  1123. * @example
  1124. * let item = $('.count .two');
  1125. * expect(item.getText()).toBe('Second');
  1126. *
  1127. * @param {string} selector A css selector
  1128. * @returns {ElementFinder} which identifies the located
  1129. * {@link webdriver.WebElement}
  1130. */
  1131. exports.build$ = (element, by) => {
  1132. return (selector) => {
  1133. return element(by.css(selector));
  1134. };
  1135. };
  1136. /**
  1137. * Shortcut for querying the document directly with css.
  1138. * `element.all(by.css('.abc'))` is equivalent to `$$('.abc')`
  1139. *
  1140. * @alias $$(cssSelector)
  1141. * @view
  1142. * <div class="count">
  1143. * <span class="one">First</span>
  1144. * <span class="two">Second</span>
  1145. * </div>
  1146. *
  1147. * @example
  1148. * // The following protractor expressions are equivalent.
  1149. * let list = element.all(by.css('.count span'));
  1150. * expect(list.count()).toBe(2);
  1151. *
  1152. * list = $$('.count span');
  1153. * expect(list.count()).toBe(2);
  1154. * expect(list.get(0).getText()).toBe('First');
  1155. * expect(list.get(1).getText()).toBe('Second');
  1156. *
  1157. * @param {string} selector a css selector
  1158. * @returns {ElementArrayFinder} which identifies the
  1159. * array of the located {@link webdriver.WebElement}s.
  1160. */
  1161. exports.build$$ = (element, by) => {
  1162. return (selector) => {
  1163. return element.all(by.css(selector));
  1164. };
  1165. };
  1166. //# sourceMappingURL=element.js.map