001/*
002 * (C) Copyright 2014 Nuxeo SA (http://nuxeo.com/) and contributors.
003 *
004 * All rights reserved. This program and the accompanying materials
005 * are made available under the terms of the GNU Lesser General Public License
006 * (LGPL) version 2.1 which accompanies this distribution, and is available at
007 * http://www.gnu.org/licenses/lgpl-2.1.html
008 *
009 * This library is distributed in the hope that it will be useful,
010 * but WITHOUT ANY WARRANTY; without even the implied warranty of
011 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
012 * Lesser General Public License for more details.
013 *
014 * Contributors:
015 *     <a href="mailto:grenard@nuxeo.com">Guillaume</a>
016 */
017package org.nuxeo.functionaltests;
018
019import java.util.Arrays;
020import java.util.List;
021import java.util.concurrent.TimeUnit;
022
023import org.apache.commons.logging.Log;
024import org.apache.commons.logging.LogFactory;
025import org.openqa.selenium.By;
026import org.openqa.selenium.InvalidSelectorException;
027import org.openqa.selenium.NoSuchElementException;
028import org.openqa.selenium.NotFoundException;
029import org.openqa.selenium.StaleElementReferenceException;
030import org.openqa.selenium.TimeoutException;
031import org.openqa.selenium.WebDriver;
032import org.openqa.selenium.WebElement;
033import org.openqa.selenium.support.ui.ExpectedCondition;
034import org.openqa.selenium.support.ui.ExpectedConditions;
035import org.openqa.selenium.support.ui.FluentWait;
036import org.openqa.selenium.support.ui.Wait;
037import org.openqa.selenium.support.ui.WebDriverWait;
038
039import com.google.common.base.Function;
040
041/**
042 * Helper class providing find and wait methods with or without timeout. When requiring timeout, the polling frequency
043 * is every 100 milliseconds if not specified.
044 *
045 * @since 5.9.2
046 */
047public class Locator {
048
049    private static final Log log = LogFactory.getLog(Locator.class);
050
051    // Timeout for waitUntilURLDifferentFrom in seconds
052    public static int URLCHANGE_MAX_WAIT = 30;
053
054    public static WebElement findElement(By by) {
055        return AbstractTest.driver.findElement(by);
056    }
057
058    /**
059     * Finds the first {@link WebElement} using the given method, with the default timeout. Then waits until the element
060     * is enabled, with the default timeout.
061     *
062     * @param by the locating mechanism
063     * @return the first matching element on the current page, if found
064     * @throws NotFoundException if the element is not found or not enabled
065     */
066    public static WebElement findElementAndWaitUntilEnabled(By by) throws NotFoundException {
067        return findElementAndWaitUntilEnabled(by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000,
068                AbstractTest.AJAX_TIMEOUT_SECONDS * 1000);
069    }
070
071    /**
072     * Finds the first {@link WebElement} using the given method, with a {@code findElementTimeout}. Then waits until
073     * the element is enabled, with a {@code waitUntilEnabledTimeout}.
074     *
075     * @param by the locating mechanism
076     * @param findElementTimeout the find element timeout in milliseconds
077     * @param waitUntilEnabledTimeout the wait until enabled timeout in milliseconds
078     * @return the first matching element on the current page, if found
079     * @throws NotFoundException if the element is not found or not enabled
080     */
081    public static WebElement findElementAndWaitUntilEnabled(final By by, final int findElementTimeout,
082            final int waitUntilEnabledTimeout) throws NotFoundException {
083        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
084                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
085                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
086        Function<WebDriver, WebElement> function = new Function<WebDriver, WebElement>() {
087            @Override
088            public WebElement apply(WebDriver driver) {
089                WebElement element = null;
090                try {
091                    // Find the element.
092                    element = findElementWithTimeout(by, findElementTimeout);
093
094                    // Try to wait until the element is enabled.
095                    waitUntilEnabled(element, waitUntilEnabledTimeout);
096                } catch (StaleElementReferenceException sere) {
097                    AbstractTest.log.debug("StaleElementReferenceException: " + sere.getMessage());
098                    return null;
099                }
100                return element;
101            }
102        };
103
104        return wait.until(function);
105
106    }
107
108    public static List<WebElement> findElementsWithTimeout(final By by) throws NoSuchElementException {
109        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
110                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
111                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS).ignoring(
112                NoSuchElementException.class);
113        return wait.until(new Function<WebDriver, List<WebElement>>() {
114            @Override
115            public List<WebElement> apply(WebDriver driver) {
116                List<WebElement> elements = driver.findElements(by);
117                return elements.isEmpty() ? null : elements;
118            }
119        });
120    }
121
122    /**
123     * Finds the first {@link WebElement} using the given method, with the default timeout. Then waits until the element
124     * is enabled, with the default timeout. Then clicks on the element.
125     *
126     * @param by the locating mechanism
127     * @throws NotFoundException if the element is not found or not enabled
128     */
129    public static void findElementWaitUntilEnabledAndClick(By by) throws NotFoundException {
130        waitUntilElementEnabledAndClick(by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000,
131                AbstractTest.AJAX_TIMEOUT_SECONDS * 1000);
132    }
133
134    /**
135     * Finds the first {@link WebElement} using the given method, with a timeout.
136     *
137     * @param by the locating mechanism
138     * @param timeout the timeout in milliseconds
139     * @return the first matching element on the current page, if found
140     * @throws NoSuchElementException when not found
141     */
142    public static WebElement findElementWithTimeout(By by) throws NoSuchElementException {
143        return findElementWithTimeout(by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000);
144    }
145
146    /**
147     * Finds the first {@link WebElement} using the given method, with a timeout.
148     *
149     * @param by the locating mechanism
150     * @param timeout the timeout in milliseconds
151     * @return the first matching element on the current page, if found
152     * @throws NoSuchElementException when not found
153     */
154    public static WebElement findElementWithTimeout(By by, int timeout) throws NoSuchElementException {
155        return findElementWithTimeout(by, timeout, null);
156    }
157
158    /**
159     * Finds the first {@link WebElement} using the given method, with a timeout.
160     *
161     * @param by the locating mechanism
162     * @param timeout the timeout in milliseconds
163     * @param parentElement find from the element
164     * @return the first matching element on the current page, if found
165     * @throws NoSuchElementException when not found
166     */
167    public static WebElement findElementWithTimeout(final By by, int timeout, final WebElement parentElement)
168            throws NoSuchElementException {
169        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(timeout,
170                TimeUnit.MILLISECONDS).pollingEvery(AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS).ignoring(
171                StaleElementReferenceException.class);
172        try {
173            return wait.until(new Function<WebDriver, WebElement>() {
174                @Override
175                public WebElement apply(WebDriver driver) {
176                    try {
177                        if (parentElement == null) {
178                            return driver.findElement(by);
179                        } else {
180                            return parentElement.findElement(by);
181                        }
182                    } catch (NoSuchElementException e) {
183                        return null;
184                    }
185                }
186            });
187        } catch (TimeoutException e) {
188            throw new NoSuchElementException(String.format("Couldn't find element '%s' after timeout", by));
189        }
190    }
191
192    /**
193     * Finds the first {@link WebElement} using the given method, with a timeout.
194     *
195     * @param by the locating mechanism
196     * @param timeout the timeout in milliseconds
197     * @param parentElement find from the element
198     * @return the first matching element on the current page, if found
199     * @throws NoSuchElementException when not found
200     */
201    public static WebElement findElementWithTimeout(By by, WebElement parentElement) throws NoSuchElementException {
202        return findElementWithTimeout(by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000, parentElement);
203    }
204
205    /**
206     * Fluent wait for text to be not present in the given element.
207     *
208     * @since 5.7.3
209     */
210    public static void waitForTextNotPresent(final WebElement element, final String text) {
211        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
212                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
213                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
214        wait.until((new Function<WebDriver, Boolean>() {
215            @Override
216            public Boolean apply(WebDriver driver) {
217                try {
218                    return !element.getText().contains(text);
219                } catch (StaleElementReferenceException e) {
220                    return null;
221                }
222            }
223        }));
224    }
225
226    /**
227     * Fluent wait for text to be present in the element retrieved with the given method.
228     *
229     * @since 5.7.3
230     */
231    public static void waitForTextPresent(By locator, String text) {
232        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
233                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
234                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
235        wait.until(ExpectedConditions.textToBePresentInElement(locator, text));
236    }
237
238    /**
239     * Fluent wait for text to be present in the given element.
240     *
241     * @since 5.7.3
242     */
243    public static void waitForTextPresent(final WebElement element, final String text) {
244        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
245                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
246                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
247        wait.until((new Function<WebDriver, Boolean>() {
248            @Override
249            public Boolean apply(WebDriver driver) {
250                try {
251                    return element.getText().contains(text);
252                } catch (StaleElementReferenceException e) {
253                    return null;
254                }
255            }
256        }));
257    }
258
259    /**
260     * Finds the first {@link WebElement} using the given method, with a {@code findElementTimeout}. Then waits until
261     * the element is enabled, with a {@code waitUntilEnabledTimeout}. Then clicks on the element.
262     *
263     * @param by the locating mechanism
264     * @param findElementTimeout the find element timeout in milliseconds
265     * @param waitUntilEnabledTimeout the wait until enabled timeout in milliseconds
266     * @throws NotFoundException if the element is not found or not enabled
267     */
268    public static void waitUntilElementEnabledAndClick(final By by, final int findElementTimeout,
269            final int waitUntilEnabledTimeout) throws NotFoundException {
270
271        waitUntilGivenFunctionIgnoring(new Function<WebDriver, Boolean>() {
272            @Override
273            public Boolean apply(WebDriver driver) {
274                // Find the element.
275                WebElement element = findElementAndWaitUntilEnabled(by, findElementTimeout, waitUntilEnabledTimeout);
276
277                element.click();
278                return true;
279            }
280        }, StaleElementReferenceException.class);
281    }
282
283    /**
284     * Finds the first {@link WebElement} using the given method, with a {@code findElementTimeout}. Then clicks on the
285     * element.
286     *
287     * @param by the locating mechanism
288     * @throws NotFoundException if the element is not found or not enabled
289     * @since 5.9.4
290     */
291    public static void findElementWithTimeoutAndClick(final By by) throws NotFoundException {
292
293        waitUntilGivenFunctionIgnoring(new Function<WebDriver, Boolean>() {
294            @Override
295            public Boolean apply(WebDriver driver) {
296                // Find the element.
297                WebElement element = findElementWithTimeout(by);
298
299                element.click();
300                return true;
301            }
302        }, StaleElementReferenceException.class);
303    }
304
305    /**
306     * Fluent wait for an element not to be present, checking every 100 ms.
307     *
308     * @since 5.7.2
309     */
310    public static void waitUntilElementNotPresent(final By locator) {
311        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
312                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
313                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
314        wait.until((new Function<WebDriver, By>() {
315            @Override
316            public By apply(WebDriver driver) {
317                try {
318                    driver.findElement(locator);
319                } catch (NoSuchElementException ex) {
320                    // ok
321                    return locator;
322                }
323                return null;
324            }
325        }));
326    }
327
328    /**
329     * Fluent wait for an element to be present, checking every 100 ms.
330     *
331     * @since 5.7.2
332     */
333    public static void waitUntilElementPresent(final By locator) {
334        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
335                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
336                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS).ignoring(
337                NoSuchElementException.class);
338        wait.until(new Function<WebDriver, WebElement>() {
339            @Override
340            public WebElement apply(WebDriver driver) {
341                return driver.findElement(locator);
342            }
343        });
344    }
345
346    /**
347     * Waits until an element is enabled, with a timeout.
348     *
349     * @param element the element
350     */
351    public static void waitUntilEnabled(WebElement element) throws NotFoundException {
352        waitUntilEnabled(element, AbstractTest.AJAX_TIMEOUT_SECONDS * 1000);
353    }
354
355    /**
356     * Waits until an element is enabled, with a timeout.
357     *
358     * @param element the element
359     * @param timeout the timeout in milliseconds
360     */
361    public static void waitUntilEnabled(final WebElement element, int timeout) throws NotFoundException {
362        Wait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(timeout,
363                TimeUnit.MILLISECONDS).pollingEvery(AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
364        Function<WebDriver, Boolean> function = new Function<WebDriver, Boolean>() {
365            @Override
366            public Boolean apply(WebDriver driver) {
367                return element.isEnabled();
368            }
369        };
370        try {
371            wait.until(function);
372        } catch (TimeoutException e) {
373            throw new NotFoundException("Element not enabled after timeout: " + element);
374        }
375    }
376
377    /**
378     * Fluent wait on a the given function, checking every 100 ms.
379     *
380     * @param function
381     * @since 5.9.2
382     */
383    public static void waitUntilGivenFunction(Function<WebDriver, Boolean> function) {
384        waitUntilGivenFunctionIgnoring(function, null);
385    }
386
387    /**
388     * Fluent wait on a the given function, checking every 100 ms.
389     *
390     * @param function
391     * @param ignoredExceptions the types of exceptions to ignore.
392     * @since 5.9.2
393     */
394    public static <K extends java.lang.Throwable> void waitUntilGivenFunctionIgnoreAll(
395            Function<WebDriver, Boolean> function, java.lang.Class<? extends K>... ignoredExceptions) {
396        FluentWait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
397                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
398                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
399        if (ignoredExceptions != null) {
400            if (ignoredExceptions.length == 1) {
401                wait.ignoring(ignoredExceptions[0]);
402            } else {
403                wait.ignoreAll(Arrays.asList(ignoredExceptions));
404            }
405
406        }
407        wait.until(function);
408    }
409
410    /**
411     * Fluent wait on a the given function, checking every 100 ms.
412     *
413     * @param function
414     * @param ignoredException the type of exception to ignore.
415     * @since 5.9.2
416     */
417    public static <K extends java.lang.Throwable> void waitUntilGivenFunctionIgnoring(
418            Function<WebDriver, Boolean> function, java.lang.Class<? extends K> ignoredException) {
419        FluentWait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver).withTimeout(
420                AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
421                AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
422        if (ignoredException != null) {
423            wait.ignoring(ignoredException);
424        }
425        wait.until(function);
426    }
427
428    /**
429     * Waits until the URL contains the string given in parameter, with a timeout.
430     *
431     * @param string the string that is to be contained
432     * @since 5.9.2
433     */
434    public static void waitUntilURLContains(String string) {
435        waitUntilURLContainsOrNot(string, true);
436    }
437
438    /**
439     * @since 5.9.2
440     */
441    private static void waitUntilURLContainsOrNot(String string, final boolean contain) {
442        final String refurl = string;
443        ExpectedCondition<Boolean> condition = new ExpectedCondition<Boolean>() {
444            @Override
445            public Boolean apply(WebDriver d) {
446                String currentUrl = d.getCurrentUrl();
447                boolean result = !(currentUrl.contains(refurl) ^ contain);
448                if (!result) {
449                    AbstractTest.log.debug("currentUrl is : " + currentUrl);
450                    AbstractTest.log.debug((contain ? "It should contains : " : "It should not contains : ") + refurl);
451                }
452                return result;
453            }
454        };
455        WebDriverWait wait = new WebDriverWait(AbstractTest.driver, URLCHANGE_MAX_WAIT);
456        wait.until(condition);
457    }
458
459    /**
460     * Waits until the URL is different from the one given in parameter, with a timeout.
461     *
462     * @param url the URL to compare to
463     */
464    public static void waitUntilURLDifferentFrom(String url) {
465        final String refurl = url;
466        ExpectedCondition<Boolean> urlchanged = new ExpectedCondition<Boolean>() {
467            @Override
468            public Boolean apply(WebDriver d) {
469                String currentUrl = d.getCurrentUrl();
470                AbstractTest.log.debug("currentUrl is still: " + currentUrl);
471                return !currentUrl.equals(refurl);
472            }
473        };
474        WebDriverWait wait = new WebDriverWait(AbstractTest.driver, URLCHANGE_MAX_WAIT);
475        wait.until(urlchanged);
476        if (AbstractTest.driver.getCurrentUrl().equals(refurl)) {
477            log.warn("Page change failed");
478        }
479    }
480
481    /**
482     * Waits until the URL does not contain the string given in parameter, with a timeout.
483     *
484     * @param string the string that is not to be contained
485     * @since 5.9.2
486     */
487    public static void waitUntilURLNotContain(String string) {
488        waitUntilURLContainsOrNot(string, false);
489    }
490
491    /**
492     * Return parent element with given tag name.
493     * <p>
494     * Throws a {@link NoSuchElementException} error if no element found.
495     *
496     * @since 7.3
497     */
498    public static WebElement findParentTag(WebElement elt, String tagName) {
499        try {
500            By parentBy = By.xpath("..");
501            WebElement p = elt.findElement(parentBy);
502            while (p != null) {
503                if (tagName.equals(p.getTagName())) {
504                    return p;
505                }
506                p = p.findElement(parentBy);
507            }
508        } catch (InvalidSelectorException e) {
509        }
510        throw new NoSuchElementException(String.format("No parent element found with tag %s.", tagName));
511    }
512}