"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const selenium_webdriver_1 = require("selenium-webdriver");
const locators_1 = require("./locators");
const logger_1 = require("./logger");
const util_1 = require("./util");
let clientSideScripts = require('./clientsidescripts');
let logger = new logger_1.Logger('element');
class WebdriverWebElement {
}
exports.WebdriverWebElement = WebdriverWebElement;
let WEB_ELEMENT_FUNCTIONS = [
    'click', 'sendKeys', 'getTagName', 'getCssValue', 'getAttribute', 'getText', 'getSize',
    'getLocation', 'isEnabled', 'isSelected', 'submit', 'clear', 'isDisplayed', 'getId',
    'takeScreenshot'
];
/**
 * ElementArrayFinder is used for operations on an array of elements (as opposed
 * to a single element).
 *
 * The ElementArrayFinder is used to set up a chain of conditions that identify
 * an array of elements. In particular, you can call all(locator) and
 * filter(filterFn) to return a new ElementArrayFinder modified by the
 * conditions, and you can call get(index) to return a single ElementFinder at
 * position 'index'.
 *
 * Similar to jquery, ElementArrayFinder will search all branches of the DOM
 * to find the elements that satisfy the conditions (i.e. all, filter, get).
 * However, an ElementArrayFinder will not actually retrieve the elements until
 * an action is called, which means it can be set up in helper files (i.e.
 * page objects) before the page is available, and reused as the page changes.
 *
 * You can treat an ElementArrayFinder as an array of WebElements for most
 * purposes, in particular, you may perform actions (i.e. click, getText) on
 * them as you would an array of WebElements. The action will apply to
 * every element identified by the ElementArrayFinder. ElementArrayFinder
 * extends Promise, and once an action is performed on an ElementArrayFinder,
 * the latest result can be accessed using then, and will be returned as an
 * array of the results; the array has length equal to the length of the
 * elements found by the ElementArrayFinder and each result represents the
 * result of performing the action on the element. Unlike a WebElement, an
 * ElementArrayFinder will wait for the angular app to settle before
 * performing finds or actions.
 *
 * @alias element.all(locator)
 * @view
 * <ul class="items">
 *   <li>First</li>
 *   <li>Second</li>
 *   <li>Third</li>
 * </ul>
 *
 * @example
 * element.all(by.css('.items li')).then(function(items) {
 *   expect(items.length).toBe(3);
 *   expect(items[0].getText()).toBe('First');
 * });
 *
 * // Or using the shortcut $$() notation instead of element.all(by.css()):
 *
 * $$('.items li').then(function(items) {
 *   expect(items.length).toBe(3);
 *   expect(items[0].getText()).toBe('First');
 * });
 *
 * @constructor
 * @param {ProtractorBrowser} browser A browser instance.
 * @param {function(): Array.<webdriver.WebElement>} getWebElements A function
 *    that returns a list of the underlying Web Elements.
 * @param {webdriver.Locator} locator The most relevant locator. It is only
 *    used for error reporting and ElementArrayFinder.locator.
 * @param {Array.<webdriver.promise.Promise>} opt_actionResults An array
 *    of promises which will be retrieved with then. Resolves to the latest
 *    action result, or null if no action has been called.
 * @returns {ElementArrayFinder}
 */
class ElementArrayFinder extends WebdriverWebElement {
    constructor(browser_, getWebElements = null, locator_, actionResults_ = null) {
        super();
        this.browser_ = browser_;
        this.getWebElements = getWebElements;
        this.locator_ = locator_;
        this.actionResults_ = actionResults_;
        // TODO(juliemr): might it be easier to combine this with our docs and just
        // wrap each one explicity with its own documentation?
        WEB_ELEMENT_FUNCTIONS.forEach((fnName) => {
            this[fnName] = (...args) => {
                let actionFn = (webElem) => {
                    return webElem[fnName].apply(webElem, args);
                };
                return this.applyAction_(actionFn);
            };
        });
    }
    /**
     * Create a shallow copy of ElementArrayFinder.
     *
     * @returns {!ElementArrayFinder} A shallow copy of this.
     */
    clone() {
        // A shallow copy is all we need since the underlying fields can never be
        // modified. (Locator can be modified by the user, but that should
        // rarely/never happen and it doesn't affect functionalities).
        return new ElementArrayFinder(this.browser_, this.getWebElements, this.locator_, this.actionResults_);
    }
    /**
     * Calls to ElementArrayFinder may be chained to find an array of elements
     * using the current elements in this ElementArrayFinder as the starting
     * point. This function returns a new ElementArrayFinder which would contain
     * the children elements found (and could also be empty).
     *
     * @alias element.all(locator).all(locator)
     * @view
     * <div id='id1' class="parent">
     *   <ul>
     *     <li class="foo">1a</li>
     *     <li class="baz">1b</li>
     *   </ul>
     * </div>
     * <div id='id2' class="parent">
     *   <ul>
     *     <li class="foo">2a</li>
     *     <li class="bar">2b</li>
     *   </ul>
     * </div>
     *
     * @example
     * let foo = element.all(by.css('.parent')).all(by.css('.foo'));
     * expect(foo.getText()).toEqual(['1a', '2a']);
     * let baz = element.all(by.css('.parent')).all(by.css('.baz'));
     * expect(baz.getText()).toEqual(['1b']);
     * let nonexistent = element.all(by.css('.parent'))
     *   .all(by.css('.NONEXISTENT'));
     * expect(nonexistent.getText()).toEqual(['']);
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let foo = $$('.parent').$$('.foo');
     * expect(foo.getText()).toEqual(['1a', '2a']);
     * let baz = $$('.parent').$$('.baz');
     * expect(baz.getText()).toEqual(['1b']);
     * let nonexistent = $$('.parent').$$('.NONEXISTENT');
     * expect(nonexistent.getText()).toEqual(['']);
     *
     * @param {webdriver.Locator} subLocator
     * @returns {ElementArrayFinder}
     */
    all(locator) {
        let ptor = this.browser_;
        let getWebElements = () => {
            if (this.getWebElements === null) {
                // This is the first time we are looking for an element
                return ptor.waitForAngular('Locator: ' + locator)
                    .then(() => {
                    if (locators_1.isProtractorLocator(locator)) {
                        return locator.findElementsOverride(ptor.driver, null, ptor.rootEl);
                    }
                    else {
                        return ptor.driver.findElements(locator);
                    }
                });
            }
            else {
                return this.getWebElements().then((parentWebElements) => {
                    // For each parent web element, find their children and construct
                    // a list of Promise<List<child_web_element>>
                    let childrenPromiseList = parentWebElements.map((parentWebElement) => {
                        return locators_1.isProtractorLocator(locator) ?
                            locator.findElementsOverride(ptor.driver, parentWebElement, ptor.rootEl) :
                            parentWebElement.findElements(locator);
                    });
                    // Resolve the list of Promise<List<child_web_elements>> and merge
                    // into a single list
                    return selenium_webdriver_1.promise.all(childrenPromiseList)
                        .then((resolved) => {
                        return resolved.reduce((childrenList, resolvedE) => {
                            return childrenList.concat(resolvedE);
                        }, []);
                    });
                });
            }
        };
        return new ElementArrayFinder(this.browser_, getWebElements, locator);
    }
    /**
     * Apply a filter function to each element within the ElementArrayFinder.
     * Returns a new ElementArrayFinder with all elements that pass the filter
     * function. The filter function receives the ElementFinder as the first
     * argument and the index as a second arg. This does not actually retrieve
     * the underlying list of elements, so it can be used in page objects.
     *
     * @alias element.all(locator).filter(filterFn)
     * @view
     * <ul class="items">
     *   <li class="one">First</li>
     *   <li class="two">Second</li>
     *   <li class="three">Third</li>
     * </ul>
     *
     * @example
     * element.all(by.css('.items li')).filter(function(elem, index) {
     *   return elem.getText().then(function(text) {
     *     return text === 'Third';
     *   });
     * }).first().click();
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * $$('.items li').filter(function(elem, index) {
     *   return elem.getText().then(function(text) {
     *     return text === 'Third';
     *   });
     * }).first().click();
     *
     * @param {function(ElementFinder, number): webdriver.WebElement.Promise}
     * filterFn
     *     Filter function that will test if an element should be returned.
     *     filterFn can either return a boolean or a promise that resolves to a
     * boolean
     * @returns {!ElementArrayFinder} A ElementArrayFinder that represents an
     * array
     *     of element that satisfy the filter function.
     */
    filter(filterFn) {
        let getWebElements = () => {
            return this.getWebElements().then((parentWebElements) => {
                let list = parentWebElements.map((parentWebElement, index) => {
                    let elementFinder = ElementFinder.fromWebElement_(this.browser_, parentWebElement, this.locator_);
                    return filterFn(elementFinder, index);
                });
                return selenium_webdriver_1.promise.all(list).then((resolvedList) => {
                    return parentWebElements.filter((parentWebElement, index) => {
                        return resolvedList[index];
                    });
                });
            });
        };
        return new ElementArrayFinder(this.browser_, getWebElements, this.locator_);
    }
    /**
     * Get an element within the ElementArrayFinder by index. The index starts at 0.
     * Negative indices are wrapped (i.e. -i means ith element from last)
     * This does not actually retrieve the underlying element.
     *
     * @alias element.all(locator).get(index)
     * @view
     * <ul class="items">
     *   <li>First</li>
     *   <li>Second</li>
     *   <li>Third</li>
     * </ul>
     *
     * @example
     * let list = element.all(by.css('.items li'));
     * expect(list.get(0).getText()).toBe('First');
     * expect(list.get(1).getText()).toBe('Second');
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let list = $$('.items li');
     * expect(list.get(0).getText()).toBe('First');
     * expect(list.get(1).getText()).toBe('Second');
     *
     * @param {number|webdriver.promise.Promise} index Element index.
     * @returns {ElementFinder} finder representing element at the given index.
     */
    get(index) {
        let getWebElements = () => {
            return selenium_webdriver_1.promise.all([index, this.getWebElements()]).then(([i, parentWebElements]) => {
                if (i < 0) {
                    i += parentWebElements.length;
                }
                if (i < 0 || i >= parentWebElements.length) {
                    throw new selenium_webdriver_1.error.NoSuchElementError('Index out of bound. Trying to access element at index: ' + index +
                        ', but there are only ' + parentWebElements.length + ' elements that match ' +
                        'locator ' + this.locator_.toString());
                }
                return [parentWebElements[i]];
            });
        };
        return new ElementArrayFinder(this.browser_, getWebElements, this.locator_).toElementFinder_();
    }
    /**
     * Get the first matching element for the ElementArrayFinder. This does not
     * actually retrieve the underlying element.
     *
     * @alias element.all(locator).first()
     * @view
     * <ul class="items">
     *   <li>First</li>
     *   <li>Second</li>
     *   <li>Third</li>
     * </ul>
     *
     * @example
     * let first = element.all(by.css('.items li')).first();
     * expect(first.getText()).toBe('First');
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let first = $$('.items li').first();
     * expect(first.getText()).toBe('First');
     *
     * @returns {ElementFinder} finder representing the first matching element
     */
    first() {
        return this.get(0);
    }
    ;
    /**
     * Get the last matching element for the ElementArrayFinder. This does not
     * actually retrieve the underlying element.
     *
     * @alias element.all(locator).last()
     * @view
     * <ul class="items">
     *   <li>First</li>
     *   <li>Second</li>
     *   <li>Third</li>
     * </ul>
     *
     * @example
     * let last = element.all(by.css('.items li')).last();
     * expect(last.getText()).toBe('Third');
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let last = $$('.items li').last();
     * expect(last.getText()).toBe('Third');
     *
     * @returns {ElementFinder} finder representing the last matching element
     */
    last() {
        return this.get(-1);
    }
    /**
     * Shorthand function for finding arrays of elements by css.
     * `element.all(by.css('.abc'))` is equivalent to `$$('.abc')`
     *
     * @alias $$(cssSelector)
     * @view
     * <div class="count">
     *   <span class="one">First</span>
     *   <span class="two">Second</span>
     * </div>
     *
     * @example
     * // The following two blocks of code are equivalent.
     * let list = element.all(by.css('.count span'));
     * expect(list.count()).toBe(2);
     * expect(list.get(0).getText()).toBe('First');
     * expect(list.get(1).getText()).toBe('Second');
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let list = $$('.count span');
     * expect(list.count()).toBe(2);
     * expect(list.get(0).getText()).toBe('First');
     * expect(list.get(1).getText()).toBe('Second');
     *
     * @param {string} selector a css selector
     * @returns {ElementArrayFinder} which identifies the
     *     array of the located {@link webdriver.WebElement}s.
     */
    $$(selector) {
        return this.all(selenium_webdriver_1.By.css(selector));
    }
    /**
     * Returns an ElementFinder representation of ElementArrayFinder. It ensures
     * that the ElementArrayFinder resolves to one and only one underlying
     * element.
     *
     * @returns {ElementFinder} An ElementFinder representation
     * @private
     */
    toElementFinder_() {
        return new ElementFinder(this.browser_, this);
    }
    /**
     * Count the number of elements represented by the ElementArrayFinder.
     *
     * @alias element.all(locator).count()
     * @view
     * <ul class="items">
     *   <li>First</li>
     *   <li>Second</li>
     *   <li>Third</li>
     * </ul>
     *
     * @example
     * let list = element.all(by.css('.items li'));
     * expect(list.count()).toBe(3);
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let list = $$('.items li');
     * expect(list.count()).toBe(3);
     *
     * @returns {!webdriver.promise.Promise} A promise which resolves to the
     *     number of elements matching the locator.
     */
    count() {
        return this.getWebElements().then((arr) => {
            return arr.length;
        }, (err) => {
            if (err instanceof selenium_webdriver_1.error.NoSuchElementError) {
                return 0;
            }
            else {
                throw err;
            }
        });
    }
    /**
     * Returns true if there are any elements present that match the finder.
     *
     * @alias element.all(locator).isPresent()
     *
     * @example
     * expect($('.item').isPresent()).toBeTruthy();
     *
     * @returns {Promise<boolean>}
     */
    isPresent() {
        return this.count().then((count) => {
            return count > 0;
        });
    }
    /**
     * Returns the most relevant locator.
     *
     * @example
     * // returns by.css('#ID1')
     * $('#ID1').locator();
     *
     * // returns by.css('#ID2')
     * $('#ID1').$('#ID2').locator();
     *
     * // returns by.css('#ID1')
     * $$('#ID1').filter(filterFn).get(0).click().locator();
     *
     * @returns {webdriver.Locator}
     */
    locator() {
        return this.locator_;
    }
    /**
     * Apply an action function to every element in the ElementArrayFinder,
     * and return a new ElementArrayFinder that contains the results of the
     * actions.
     *
     * @param {function(ElementFinder)} actionFn
     *
     * @returns {ElementArrayFinder}
     * @private
     */
    // map<U>(callbackfn: (value: T, index: number, array: T[]) => U, thisArg?: any): U[];
    applyAction_(actionFn) {
        let callerError = new Error();
        let actionResults = this.getWebElements()
            .then((arr) => selenium_webdriver_1.promise.all(arr.map(actionFn)))
            .then((value) => {
            return { passed: true, value: value };
        }, (error) => {
            return { passed: false, value: error };
        });
        let getWebElements = () => actionResults.then(() => this.getWebElements());
        actionResults = actionResults.then((result) => {
            if (result.passed) {
                return result.value;
            }
            else {
                let noSuchErr;
                if (result.value instanceof Error) {
                    noSuchErr = result.value;
                    noSuchErr.stack = noSuchErr.stack + callerError.stack;
                }
                else {
                    noSuchErr = new Error(result.value);
                    noSuchErr.stack = callerError.stack;
                }
                throw noSuchErr;
            }
        });
        return new ElementArrayFinder(this.browser_, getWebElements, this.locator_, actionResults);
    }
    /**
     * Represents the ElementArrayFinder as an array of ElementFinders.
     *
     * @returns {Array.<ElementFinder>} Return a promise, which resolves to a list
     *     of ElementFinders specified by the locator.
     */
    asElementFinders_() {
        return this.getWebElements().then((arr) => {
            return arr.map((webElem) => {
                return ElementFinder.fromWebElement_(this.browser_, webElem, this.locator_);
            });
        });
    }
    /**
     * Retrieve the elements represented by the ElementArrayFinder. The input
     * function is passed to the resulting promise, which resolves to an
     * array of ElementFinders.
     *
     * @alias element.all(locator).then(thenFunction)
     * @view
     * <ul class="items">
     *   <li>First</li>
     *   <li>Second</li>
     *   <li>Third</li>
     * </ul>
     *
     * @example
     * element.all(by.css('.items li')).then(function(arr) {
     *   expect(arr.length).toEqual(3);
     * });
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * $$('.items li').then(function(arr) {
     *   expect(arr.length).toEqual(3);
     * });
     *
     * @param {function(Array.<ElementFinder>)} fn
     * @param {function(Error)} errorFn
     *
     * @returns {!webdriver.promise.Promise} A promise which will resolve to
     *     an array of ElementFinders represented by the ElementArrayFinder.
     */
    then(fn, errorFn) {
        if (this.actionResults_) {
            return this.actionResults_.then(fn, errorFn);
        }
        else {
            return this.asElementFinders_().then(fn, errorFn);
        }
    }
    /**
     * Calls the input function on each ElementFinder represented by the
     * ElementArrayFinder.
     *
     * @alias element.all(locator).each(eachFunction)
     * @view
     * <ul class="items">
     *   <li>First</li>
     *   <li>Second</li>
     *   <li>Third</li>
     * </ul>
     *
     * @example
     * element.all(by.css('.items li')).each(function(element, index) {
     *   // Will print 0 First, 1 Second, 2 Third.
     *   element.getText().then(function (text) {
     *     console.log(index, text);
     *   });
     * });
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * $$('.items li').each(function(element, index) {
     *   // Will print 0 First, 1 Second, 2 Third.
     *   element.getText().then(function (text) {
     *     console.log(index, text);
     *   });
     * });
     *
     * @param {function(ElementFinder)} fn Input function
     *
     * @returns {!webdriver.promise.Promise} A promise that will resolve when the
     *     function has been called on all the ElementFinders. The promise will
     *     resolve to null.
     */
    each(fn) {
        return this.map(fn).then(() => {
            return null;
        });
    }
    /**
     * Apply a map function to each element within the ElementArrayFinder. The
     * callback receives the ElementFinder as the first argument and the index as
     * a second arg.
     *
     * @alias element.all(locator).map(mapFunction)
     * @view
     * <ul class="items">
     *   <li class="one">First</li>
     *   <li class="two">Second</li>
     *   <li class="three">Third</li>
     * </ul>
     *
     * @example
     * let items = element.all(by.css('.items li')).map(function(elm, index) {
     *   return {
     *     index: index,
     *     text: elm.getText(),
     *     class: elm.getAttribute('class')
     *   };
     * });
     * expect(items).toEqual([
     *   {index: 0, text: 'First', class: 'one'},
     *   {index: 1, text: 'Second', class: 'two'},
     *   {index: 2, text: 'Third', class: 'three'}
     * ]);
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let items = $$('.items li').map(function(elm, index) {
     *   return {
     *     index: index,
     *     text: elm.getText(),
     *     class: elm.getAttribute('class')
     *   };
     * });
     * expect(items).toEqual([
     *   {index: 0, text: 'First', class: 'one'},
     *   {index: 1, text: 'Second', class: 'two'},
     *   {index: 2, text: 'Third', class: 'three'}
     * ]);
     *
     * @param {function(ElementFinder, number)} mapFn Map function that
     *     will be applied to each element.
     * @returns {!webdriver.promise.Promise} A promise that resolves to an array
     *     of values returned by the map function.
     */
    map(mapFn) {
        return this.asElementFinders_().then((arr) => {
            let list = arr.map((elementFinder, index) => {
                let mapResult = mapFn(elementFinder, index);
                // All nested arrays and objects will also be fully resolved.
                return selenium_webdriver_1.promise.fullyResolved(mapResult);
            });
            return selenium_webdriver_1.promise.all(list);
        });
    }
    ;
    /**
     * Apply a reduce function against an accumulator and every element found
     * using the locator (from left-to-right). The reduce function has to reduce
     * every element into a single value (the accumulator). Returns promise of
     * the accumulator. The reduce function receives the accumulator, current
     * ElementFinder, the index, and the entire array of ElementFinders,
     * respectively.
     *
     * @alias element.all(locator).reduce(reduceFn)
     * @view
     * <ul class="items">
     *   <li class="one">First</li>
     *   <li class="two">Second</li>
     *   <li class="three">Third</li>
     * </ul>
     *
     * @example
     * let value = element.all(by.css('.items li')).reduce(function(acc, elem) {
     *   return elem.getText().then(function(text) {
     *     return acc + text + ' ';
     *   });
     * }, '');
     *
     * expect(value).toEqual('First Second Third ');
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let value = $$('.items li').reduce(function(acc, elem) {
     *   return elem.getText().then(function(text) {
     *     return acc + text + ' ';
     *   });
     * }, '');
     *
     * expect(value).toEqual('First Second Third ');
     *
     * @param {function(number, ElementFinder, number, Array.<ElementFinder>)}
     *     reduceFn Reduce function that reduces every element into a single
     * value.
     * @param {*} initialValue Initial value of the accumulator.
     * @returns {!webdriver.promise.Promise} A promise that resolves to the final
     *     value of the accumulator.
     */
    reduce(reduceFn, initialValue) {
        let valuePromise = selenium_webdriver_1.promise.when(initialValue);
        return this.asElementFinders_().then((arr) => {
            return arr.reduce((valuePromise, elementFinder, index) => {
                return valuePromise.then((value) => {
                    return reduceFn(value, elementFinder, index, arr);
                });
            }, valuePromise);
        });
    }
    /**
     * Evaluates the input as if it were on the scope of the current underlying
     * elements.
     *
     * @view
     * <span class="foo">{{letiableInScope}}</span>
     *
     * @example
     * let value = element.all(by.css('.foo')).evaluate('letiableInScope');
     *
     * // Or using the shortcut $$() notation instead of element.all(by.css()):
     *
     * let value = $$('.foo').evaluate('letiableInScope');
     *
     * @param {string} expression
     *
     * @returns {ElementArrayFinder} which resolves to the
     *     evaluated expression for each underlying element.
     *     The result will be resolved as in
     *     {@link webdriver.WebDriver.executeScript}. In summary - primitives will
     *     be resolved as is, functions will be converted to string, and elements
     *     will be returned as a WebElement.
     */
    evaluate(expression) {
        let evaluationFn = (webElem) => {
            return webElem.getDriver().executeScript(clientSideScripts.evaluate, webElem, expression);
        };
        return this.applyAction_(evaluationFn);
    }
    /**
     * Determine if animation is allowed on the current underlying elements.
     * @param {string} value
     *
     * @example
     * // Turns off ng-animate animations for all elements in the <body>
     * element(by.css('body')).allowAnimations(false);
     *
     * // Or using the shortcut $() notation instead of element(by.css()):
     *
     * $('body').allowAnimations(false);
     *
     * @returns {ElementArrayFinder} which resolves to whether animation is
     * allowed.
     */
    allowAnimations(value) {
        let allowAnimationsTestFn = (webElem) => {
            return webElem.getDriver().executeScript(clientSideScripts.allowAnimations, webElem, value);
        };
        return this.applyAction_(allowAnimationsTestFn);
    }
}
exports.ElementArrayFinder = ElementArrayFinder;
/**
 * The ElementFinder simply represents a single element of an
 * ElementArrayFinder (and is more like a convenience object). As a result,
 * anything that can be done with an ElementFinder, can also be done using
 * an ElementArrayFinder.
 *
 * The ElementFinder can be treated as a WebElement for most purposes, in
 * particular, you may perform actions (i.e. click, getText) on them as you
 * would a WebElement. Once an action is performed on an ElementFinder, the
 * latest result from the chain can be accessed using the then method.
 * Unlike a WebElement, an ElementFinder will wait for angular to settle before
 * performing finds or actions.
 *
 * ElementFinder can be used to build a chain of locators that is used to find
 * an element. An ElementFinder does not actually attempt to find the element
 * until an action is called, which means they can be set up in helper files
 * before the page is available.
 *
 * @alias element(locator)
 * @view
 * <span>{{person.name}}</span>
 * <span ng-bind="person.email"></span>
 * <input type="text" ng-model="person.name"/>
 *
 * @example
 * // Find element with {{scopelet}} syntax.
 * element(by.binding('person.name')).getText().then(function(name) {
 *   expect(name).toBe('Foo');
 * });
 *
 * // Find element with ng-bind="scopelet" syntax.
 * expect(element(by.binding('person.email')).getText()).toBe('foo@bar.com');
 *
 * // Find by model.
 * let input = element(by.model('person.name'));
 * input.sendKeys('123');
 * expect(input.getAttribute('value')).toBe('Foo123');
 *
 * @constructor
 * @extends {webdriver.WebElement}
 * @param {ProtractorBrowser} browser_ A browser instance.
 * @param {ElementArrayFinder} elementArrayFinder The ElementArrayFinder
 *     that this is branched from.
 * @returns {ElementFinder}
 */
class ElementFinder extends WebdriverWebElement {
    constructor(browser_, elementArrayFinder) {
        super();
        this.browser_ = browser_;
        this.then = null;
        if (!elementArrayFinder) {
            throw new Error('BUG: elementArrayFinder cannot be empty');
        }
        this.parentElementArrayFinder = elementArrayFinder;
        // Only have a `then` method if the parent element array finder
        // has action results.
        if (this.parentElementArrayFinder.actionResults_) {
            // Access the underlying actionResult of ElementFinder.
            this.then =
                (fn, errorFn) => {
                    return this.elementArrayFinder_.then((actionResults) => {
                        if (!fn) {
                            return actionResults[0];
                        }
                        return fn(actionResults[0]);
                    }, errorFn);
                };
        }
        // This filter verifies that there is only 1 element returned by the
        // elementArrayFinder. It will warn if there are more than 1 element and
        // throw an error if there are no elements.
        let getWebElements = () => {
            return elementArrayFinder.getWebElements().then((webElements) => {
                if (webElements.length === 0) {
                    throw new selenium_webdriver_1.error.NoSuchElementError('No element found using locator: ' + elementArrayFinder.locator().toString());
                }
                else {
                    if (webElements.length > 1) {
                        logger.warn('more than one element found for locator ' +
                            elementArrayFinder.locator().toString() + ' - the first result will be used');
                    }
                    return [webElements[0]];
                }
            });
        };
        // Store a copy of the underlying elementArrayFinder, but with the more
        // restrictive getWebElements (which checks that there is only 1 element).
        this.elementArrayFinder_ = new ElementArrayFinder(this.browser_, getWebElements, elementArrayFinder.locator(), elementArrayFinder.actionResults_);
        WEB_ELEMENT_FUNCTIONS.forEach((fnName) => {
            (this)[fnName] = (...args) => {
                return (this.elementArrayFinder_)[fnName]
                    .apply(this.elementArrayFinder_, args)
                    .toElementFinder_();
            };
        });
    }
    static fromWebElement_(browser, webElem, locator) {
        let getWebElements = () => {
            return selenium_webdriver_1.promise.when([webElem]);
        };
        return new ElementArrayFinder(browser, getWebElements, locator).toElementFinder_();
    }
    /**
     * Create a shallow copy of ElementFinder.
     *
     * @returns {!ElementFinder} A shallow copy of this.
     */
    clone() {
        // A shallow copy is all we need since the underlying fields can never be
        // modified
        return new ElementFinder(this.browser_, this.parentElementArrayFinder);
    }
    /**
     * @see ElementArrayFinder.prototype.locator
     *
     * @returns {webdriver.Locator}
     */
    locator() {
        return this.elementArrayFinder_.locator();
    }
    /**
     * Returns the WebElement represented by this ElementFinder.
     * Throws the WebDriver error if the element doesn't exist.
     *
     * @alias element(locator).getWebElement()
     * @view
     * <div class="parent">
     *   some text
     * </div>
     *
     * @example
     * // The following four expressions are equivalent.
     * $('.parent').getWebElement();
     * element(by.css('.parent')).getWebElement();
     * browser.driver.findElement(by.css('.parent'));
     * browser.findElement(by.css('.parent'));
     *
     * @returns {webdriver.WebElementPromise}
     */
    getWebElement() {
        let id = this.elementArrayFinder_.getWebElements().then((parentWebElements) => {
            return parentWebElements[0];
        });
        return new selenium_webdriver_1.WebElementPromise(this.browser_.driver, id);
    }
    /**
     * Calls to {@code all} may be chained to find an array of elements within a
     * parent.
     *
     * @alias element(locator).all(locator)
     * @view
     * <div class="parent">
     *   <ul>
     *     <li class="one">First</li>
     *     <li class="two">Second</li>
     *     <li class="three">Third</li>
     *   </ul>
     * </div>
     *
     * @example
     * let items = element(by.css('.parent')).all(by.tagName('li'));
     *
     * // Or using the shortcut $() notation instead of element(by.css()):
     *
     * let items = $('.parent').all(by.tagName('li'));
     *
     * @param {webdriver.Locator} subLocator
     * @returns {ElementArrayFinder}
     */
    all(subLocator) {
        return this.elementArrayFinder_.all(subLocator);
    }
    /**
     * Calls to {@code element} may be chained to find elements within a parent.
     *
     * @alias element(locator).element(locator)
     * @view
     * <div class="parent">
     *   <div class="child">
     *     Child text
     *     <div>{{person.phone}}</div>
     *   </div>
     * </div>
     *
     * @example
     * // Chain 2 element calls.
     * let child = element(by.css('.parent')).
     *     element(by.css('.child'));
     * expect(child.getText()).toBe('Child text\n555-123-4567');
     *
     * // Chain 3 element calls.
     * let triple = element(by.css('.parent')).
     *     element(by.css('.child')).
     *     element(by.binding('person.phone'));
     * expect(triple.getText()).toBe('555-123-4567');
     *
     * // Or using the shortcut $() notation instead of element(by.css()):
     *
     * // Chain 2 element calls.
     * let child = $('.parent').$('.child');
     * expect(child.getText()).toBe('Child text\n555-123-4567');
     *
     * // Chain 3 element calls.
     * let triple = $('.parent').$('.child').
     *     element(by.binding('person.phone'));
     * expect(triple.getText()).toBe('555-123-4567');
     *
     * @param {webdriver.Locator} subLocator
     * @returns {ElementFinder}
     */
    element(subLocator) {
        return this.all(subLocator).toElementFinder_();
    }
    /**
     * Calls to {@code $$} may be chained to find an array of elements within a
     * parent.
     *
     * @alias element(locator).all(selector)
     * @view
     * <div class="parent">
     *   <ul>
     *     <li class="one">First</li>
     *     <li class="two">Second</li>
     *     <li class="three">Third</li>
     *   </ul>
     * </div>
     *
     * @example
     * let items = element(by.css('.parent')).$$('li');
     *
     * // Or using the shortcut $() notation instead of element(by.css()):
     *
     * let items = $('.parent').$$('li');
     *
     * @param {string} selector a css selector
     * @returns {ElementArrayFinder}
     */
    $$(selector) {
        return this.all(selenium_webdriver_1.By.css(selector));
    }
    /**
     * Calls to {@code $} may be chained to find elements within a parent.
     *
     * @alias element(locator).$(selector)
     * @view
     * <div class="parent">
     *   <div class="child">
     *     Child text
     *     <div>{{person.phone}}</div>
     *   </div>
     * </div>
     *
     * @example
     * // Chain 2 element calls.
     * let child = element(by.css('.parent')).
     *     $('.child');
     * expect(child.getText()).toBe('Child text\n555-123-4567');
     *
     * // Chain 3 element calls.
     * let triple = element(by.css('.parent')).
     *     $('.child').
     *     element(by.binding('person.phone'));
     * expect(triple.getText()).toBe('555-123-4567');
     *
     * // Or using the shortcut $() notation instead of element(by.css()):
     *
     * // Chain 2 element calls.
     * let child = $('.parent').$('.child');
     * expect(child.getText()).toBe('Child text\n555-123-4567');
     *
     * // Chain 3 element calls.
     * let triple = $('.parent').$('.child').
     *     element(by.binding('person.phone'));
     * expect(triple.getText()).toBe('555-123-4567');
     *
     * @param {string} selector A css selector
     * @returns {ElementFinder}
     */
    $(selector) {
        return this.element(selenium_webdriver_1.By.css(selector));
    }
    /**
     * Determine whether the element is present on the page.
     *
     * @view
     * <span>{{person.name}}</span>
     *
     * @example
     * // Element exists.
     * expect(element(by.binding('person.name')).isPresent()).toBe(true);
     *
     * // Element not present.
     * expect(element(by.binding('notPresent')).isPresent()).toBe(false);
     *
     * @returns {webdriver.promise.Promise<boolean>} which resolves to whether
     *     the element is present on the page.
     */
    isPresent() {
        return this.parentElementArrayFinder.getWebElements().then((arr) => {
            if (arr.length === 0) {
                return false;
            }
            return arr[0].isEnabled().then(() => {
                return true; // is present, whether it is enabled or not
            }, util_1.falseIfMissing);
        }, util_1.falseIfMissing);
    }
    /**
     * Same as ElementFinder.isPresent(), except this checks whether the element
     * identified by the subLocator is present, rather than the current element
     * finder, i.e.: `element(by.css('#abc')).element(by.css('#def')).isPresent()`
     * is identical to `element(by.css('#abc')).isElementPresent(by.css('#def'))`.
     *
     * // Or using the shortcut $() notation instead of element(by.css()):
     *
     * `$('#abc').$('#def').isPresent()` is identical to
     * `$('#abc').isElementPresent($('#def'))`.
     *
     * @see ElementFinder.isPresent
     *
     * @param {webdriver.Locator} subLocator Locator for element to look for.
     * @returns {webdriver.promise.Promise<boolean>} which resolves to whether
     *     the subelement is present on the page.
     */
    isElementPresent(subLocator) {
        if (!subLocator) {
            throw new Error('SubLocator is not supplied as a parameter to ' +
                '`isElementPresent(subLocator)`. You are probably looking for the ' +
                'function `isPresent()`.');
        }
        return this.element(subLocator).isPresent();
    }
    /**
     * Evaluates the input as if it were on the scope of the current element.
     * @see ElementArrayFinder.prototype.evaluate
     *
     * @view
     * <span id="foo">{{letiableInScope}}</span>
     *
     * @example
     * let value = element(by.id('foo')).evaluate('letiableInScope');
     *
     * @param {string} expression
     *
     * @returns {ElementFinder} which resolves to the evaluated expression.
     */
    evaluate(expression) {
        return this.elementArrayFinder_.evaluate(expression).toElementFinder_();
    }
    /**
     * @see ElementArrayFinder.prototype.allowAnimations.
     * @param {string} value
     *
     * @returns {ElementFinder} which resolves to whether animation is allowed.
     */
    allowAnimations(value) {
        return this.elementArrayFinder_.allowAnimations(value).toElementFinder_();
    }
    /**
     * Compares an element to this one for equality.
     *
     * @param {!ElementFinder|!webdriver.WebElement} The element to compare to.
     *
     * @returns {!webdriver.promise.Promise.<boolean>} A promise that will be
     *     resolved to whether the two WebElements are equal.
     */
    equals(element) {
        return selenium_webdriver_1.WebElement.equals(this.getWebElement(), element.getWebElement ? element.getWebElement() :
            element);
    }
}
exports.ElementFinder = ElementFinder;
/**
 * Shortcut for querying the document directly with css.
 * `element(by.css('.abc'))` is equivalent to `$('.abc')`
 *
 * @alias $(cssSelector)
 * @view
 * <div class="count">
 *   <span class="one">First</span>
 *   <span class="two">Second</span>
 * </div>
 *
 * @example
 * let item = $('.count .two');
 * expect(item.getText()).toBe('Second');
 *
 * @param {string} selector A css selector
 * @returns {ElementFinder} which identifies the located
 *     {@link webdriver.WebElement}
 */
exports.build$ = (element, by) => {
    return (selector) => {
        return element(by.css(selector));
    };
};
/**
 * Shortcut for querying the document directly with css.
 * `element.all(by.css('.abc'))` is equivalent to `$$('.abc')`
 *
 * @alias $$(cssSelector)
 * @view
 * <div class="count">
 *   <span class="one">First</span>
 *   <span class="two">Second</span>
 * </div>
 *
 * @example
 * // The following protractor expressions are equivalent.
 * let list = element.all(by.css('.count span'));
 * expect(list.count()).toBe(2);
 *
 * list = $$('.count span');
 * expect(list.count()).toBe(2);
 * expect(list.get(0).getText()).toBe('First');
 * expect(list.get(1).getText()).toBe('Second');
 *
 * @param {string} selector a css selector
 * @returns {ElementArrayFinder} which identifies the
 *     array of the located {@link webdriver.WebElement}s.
 */
exports.build$$ = (element, by) => {
    return (selector) => {
        return element.all(by.css(selector));
    };
};
//# sourceMappingURL=element.js.map