001/*
002 * (C) Copyright 2016 Nuxeo SA (http://nuxeo.com/) and others.
003 *
004 * Licensed under the Apache License, Version 2.0 (the "License");
005 * you may not use this file except in compliance with the License.
006 * You may obtain a copy of the License at
007 *
008 *     http://www.apache.org/licenses/LICENSE-2.0
009 *
010 * Unless required by applicable law or agreed to in writing, software
011 * distributed under the License is distributed on an "AS IS" BASIS,
012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 * See the License for the specific language governing permissions and
014 * limitations under the License.
015 *
016 * Contributors:
017 *     <a href="mailto:grenard@nuxeo.com">Guillaume</a>
018 *     Yannis JULIENNE
019 */
020package org.nuxeo.functionaltests;
021
022import java.util.Arrays;
023import java.util.List;
024import java.util.concurrent.TimeUnit;
025
026import org.apache.commons.logging.Log;
027import org.apache.commons.logging.LogFactory;
028import org.openqa.selenium.By;
029import org.openqa.selenium.InvalidSelectorException;
030import org.openqa.selenium.JavascriptExecutor;
031import org.openqa.selenium.NoSuchElementException;
032import org.openqa.selenium.NotFoundException;
033import org.openqa.selenium.StaleElementReferenceException;
034import org.openqa.selenium.TimeoutException;
035import org.openqa.selenium.WebDriver;
036import org.openqa.selenium.WebDriverException;
037import org.openqa.selenium.WebElement;
038import org.openqa.selenium.support.ui.ExpectedCondition;
039import org.openqa.selenium.support.ui.ExpectedConditions;
040import org.openqa.selenium.support.ui.FluentWait;
041import org.openqa.selenium.support.ui.Wait;
042import org.openqa.selenium.support.ui.WebDriverWait;
043
044import com.google.common.base.Function;
045
046/**
047 * Helper class providing find and wait methods with or without timeout. When requiring timeout, the polling frequency
048 * is every 100 milliseconds if not specified.
049 *
050 * @since 5.9.2
051 */
052public class Locator {
053
054    // Timeout for waitUntilURLDifferentFrom in seconds
055    public static final int URLCHANGE_MAX_WAIT = 30;
056
057    public static WebElement findElement(By by) {
058        return AbstractTest.driver.findElement(by);
059    }
060
061    /**
062     * Finds the first {@link WebElement} using the given method, with the default timeout. Then waits until the element
063     * is enabled, with the default timeout.
064     *
065     * @param by the locating mechanism
066     * @return the first matching element on the current page, if found
067     * @throws NotFoundException if the element is not found or not enabled
068     */
069    public static WebElement findElementAndWaitUntilEnabled(By by) throws NotFoundException {
070        return findElementAndWaitUntilEnabled(by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000,
071                AbstractTest.AJAX_TIMEOUT_SECONDS * 1000);
072    }
073
074    /**
075     * Finds the first {@link WebElement} using the given method, with a {@code findElementTimeout}. Then waits until
076     * the element is enabled, with a {@code waitUntilEnabledTimeout}.
077     *
078     * @param by the locating mechanism
079     * @param findElementTimeout the find element timeout in milliseconds
080     * @param waitUntilEnabledTimeout the wait until enabled timeout in milliseconds
081     * @return the first matching element on the current page, if found
082     * @throws NotFoundException if the element is not found or not enabled
083     */
084    public static WebElement findElementAndWaitUntilEnabled(final By by, final int findElementTimeout,
085            final int waitUntilEnabledTimeout) throws NotFoundException {
086        return findElementAndWaitUntilEnabled(null, by, findElementTimeout, waitUntilEnabledTimeout);
087    }
088
089    /**
090     * Finds the first {@link WebElement} using the given method, with a {@code findElementTimeout}, inside an optional
091     * {@code parentElement}. Then waits until the element is enabled, with a {@code waitUntilEnabledTimeout}.
092     *
093     * @param parentElement the parent element (can be null)
094     * @param by the locating mechanism
095     * @param findElementTimeout the find element timeout in milliseconds
096     * @param waitUntilEnabledTimeout the wait until enabled timeout in milliseconds
097     * @return the first matching element on the current page, if found, with optional parent element
098     * @throws NotFoundException if the element is not found or not enabled
099     * @since 8.3
100     */
101    public static WebElement findElementAndWaitUntilEnabled(WebElement parentElement, final By by,
102            final int findElementTimeout, final int waitUntilEnabledTimeout) throws NotFoundException {
103        Wait<WebDriver> wait = getFluentWait();
104        Function<WebDriver, WebElement> function = new Function<WebDriver, WebElement>() {
105            @Override
106            public WebElement apply(WebDriver driver) {
107                WebElement element = null;
108                try {
109                    // Find the element.
110                    element = findElementWithTimeout(by, findElementTimeout, parentElement);
111
112                    // Try to wait until the element is enabled.
113                    waitUntilEnabled(element, waitUntilEnabledTimeout);
114                } catch (StaleElementReferenceException sere) {
115                    AbstractTest.log.debug("StaleElementReferenceException: " + sere.getMessage());
116                    return null;
117                }
118                return element;
119            }
120        };
121
122        return wait.until(function);
123
124    }
125
126    public static List<WebElement> findElementsWithTimeout(final By by) throws NoSuchElementException {
127        FluentWait<WebDriver> wait = getFluentWait();
128        wait.ignoring(NoSuchElementException.class);
129        return wait.until(new Function<WebDriver, List<WebElement>>() {
130            @Override
131            public List<WebElement> apply(WebDriver driver) {
132                List<WebElement> elements = driver.findElements(by);
133                return elements.isEmpty() ? null : elements;
134            }
135        });
136    }
137
138    /**
139     * Finds the first {@link WebElement} using the given method, with the default timeout. Then waits until the element
140     * is enabled, with the default timeout. Then clicks on the element.
141     *
142     * @param by the locating mechanism
143     * @throws NotFoundException if the element is not found or not enabled
144     */
145    public static void findElementWaitUntilEnabledAndClick(By by) throws NotFoundException {
146        findElementWaitUntilEnabledAndClick(null, by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000,
147                AbstractTest.AJAX_TIMEOUT_SECONDS * 1000);
148    }
149
150    /**
151     * Finds the first {@link WebElement} using the given method, with the default timeout, inside an optional
152     * {@code parentElement}. Then waits until the element is enabled, with the default timeout. Then clicks on the
153     * element.
154     *
155     * @param parentElement the parent element (can be null)
156     * @param by the locating mechanism
157     * @throws NotFoundException if the element is not found or not enabled
158     * @since 8.3
159     */
160    public static void findElementWaitUntilEnabledAndClick(WebElement parentElement, By by) throws NotFoundException {
161        findElementWaitUntilEnabledAndClick(parentElement, by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000,
162                AbstractTest.AJAX_TIMEOUT_SECONDS * 1000);
163    }
164
165    /**
166     * Finds the first {@link WebElement} using the given method, with a timeout.
167     *
168     * @param by the locating mechanism
169     * @return the first matching element on the current page, if found
170     * @throws NoSuchElementException when not found
171     */
172    public static WebElement findElementWithTimeout(By by) throws NoSuchElementException {
173        return findElementWithTimeout(by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000);
174    }
175
176    /**
177     * Checks if a corresponding elements is present, with a timeout.
178     *
179     * @param by the locating mechanism
180     * @return true if element exists, false otherwise
181     * @since 9.3
182     */
183    public static boolean hasElementWithTimeout(By by) {
184        try {
185            return findElementWithTimeout(by) != null;
186        } catch (NoSuchElementException nsee) {
187            return false;
188        }
189    }
190
191    /**
192     * Finds the first {@link WebElement} using the given method, with a timeout.
193     *
194     * @param by the locating mechanism
195     * @param timeout the timeout in milliseconds
196     * @return the first matching element on the current page, if found
197     * @throws NoSuchElementException when not found
198     */
199    public static WebElement findElementWithTimeout(By by, int timeout) throws NoSuchElementException {
200        return findElementWithTimeout(by, timeout, null);
201    }
202
203    /**
204     * Checks if a corresponding elements is present, with a timeout.
205     *
206     * @param by the locating mechanism
207     * @param timeout the timeout in milliseconds
208     * @return true if element exists, false otherwise
209     * @since 9.3
210     */
211    public static boolean hasElementWithTimeout(By by, int timeout) {
212        try {
213            return findElementWithTimeout(by, timeout) != null;
214        } catch (NoSuchElementException nsee) {
215            return false;
216        }
217    }
218
219    /**
220     * Finds the first {@link WebElement} using the given method, with a timeout.
221     *
222     * @param by the locating mechanism
223     * @param timeout the timeout in milliseconds
224     * @param parentElement find from the element
225     * @return the first matching element on the current page, if found
226     * @throws NoSuchElementException when not found
227     */
228    public static WebElement findElementWithTimeout(final By by, int timeout, final WebElement parentElement)
229            throws NoSuchElementException {
230        FluentWait<WebDriver> wait = getFluentWait();
231        wait.withTimeout(timeout, TimeUnit.MILLISECONDS).ignoring(StaleElementReferenceException.class);
232        try {
233            return wait.until(new Function<WebDriver, WebElement>() {
234                @Override
235                public WebElement apply(WebDriver driver) {
236                    try {
237                        if (parentElement == null) {
238                            return driver.findElement(by);
239                        } else {
240                            return parentElement.findElement(by);
241                        }
242                    } catch (NoSuchElementException e) {
243                        return null;
244                    }
245                }
246            });
247        } catch (TimeoutException e) {
248            throw new NoSuchElementException(String.format("Couldn't find element '%s' after timeout", by));
249        }
250    }
251
252    /**
253     * Finds the first {@link WebElement} using the given method, with a timeout.
254     *
255     * @param by the locating mechanism
256     * @param timeout the timeout in milliseconds
257     * @param parentElement find from the element
258     * @return the first matching element on the current page, if found
259     * @throws NoSuchElementException when not found
260     */
261    public static WebElement findElementWithTimeout(By by, WebElement parentElement) throws NoSuchElementException {
262        return findElementWithTimeout(by, AbstractTest.LOAD_TIMEOUT_SECONDS * 1000, parentElement);
263    }
264
265    public static FluentWait<WebDriver> getFluentWait() {
266        FluentWait<WebDriver> wait = new FluentWait<WebDriver>(AbstractTest.driver);
267        wait.withTimeout(AbstractTest.LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
268            .pollingEvery(AbstractTest.POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
269        return wait;
270    }
271
272    /**
273     * Fluent wait for text to be not present in the given element.
274     *
275     * @since 5.7.3
276     */
277    public static void waitForTextNotPresent(final WebElement element, final String text) {
278        Wait<WebDriver> wait = getFluentWait();
279        wait.until((new Function<WebDriver, Boolean>() {
280            @Override
281            public Boolean apply(WebDriver driver) {
282                try {
283                    return !element.getText().contains(text);
284                } catch (StaleElementReferenceException e) {
285                    return null;
286                }
287            }
288        }));
289    }
290
291    /**
292     * Fluent wait for text to be present in the element retrieved with the given method.
293     *
294     * @since 5.7.3
295     */
296    public static void waitForTextPresent(By locator, String text) {
297        Wait<WebDriver> wait = getFluentWait();
298        wait.until(ExpectedConditions.textToBePresentInElementLocated(locator, text));
299    }
300
301    /**
302     * Fluent wait for text to be present in the given element.
303     *
304     * @since 5.7.3
305     */
306    public static void waitForTextPresent(final WebElement element, final String text) {
307        Wait<WebDriver> wait = getFluentWait();
308        wait.until((new Function<WebDriver, Boolean>() {
309            @Override
310            public Boolean apply(WebDriver driver) {
311                try {
312                    return element.getText().contains(text);
313                } catch (StaleElementReferenceException e) {
314                    return null;
315                }
316            }
317        }));
318    }
319
320    /**
321     * Finds the first {@link WebElement} using the given method, with a {@code findElementTimeout}. Then waits until
322     * the element is enabled, with a {@code waitUntilEnabledTimeout}. Scroll to it, then clicks on the element.
323     *
324     * @param by the locating mechanism
325     * @param findElementTimeout the find element timeout in milliseconds
326     * @param waitUntilEnabledTimeout the wait until enabled timeout in milliseconds
327     * @throws NotFoundException if the element is not found or not enabled
328     * @deprecated since 8.3, use {@link #findElementWaitUntilEnabledAndClick(WebElement, By)}
329     */
330    @Deprecated
331    public static void findElementWaitUntilEnabledAndClick(final By by, final int findElementTimeout,
332            final int waitUntilEnabledTimeout) throws NotFoundException {
333        findElementWaitUntilEnabledAndClick(null, by, findElementTimeout, waitUntilEnabledTimeout);
334    }
335
336    /**
337     * Finds the first {@link WebElement} using the given method, with a {@code findElementTimeout}, inside an optional
338     * {@code parentElement}. Then waits until the element is enabled, with a {@code waitUntilEnabledTimeout}. Scroll to
339     * it, then clicks on the element.
340     *
341     * @param parentElement the parent element (can be null)
342     * @param by the locating mechanism
343     * @param findElementTimeout the find element timeout in milliseconds
344     * @param waitUntilEnabledTimeout the wait until enabled timeout in milliseconds
345     * @throws NotFoundException if the element is not found or not enabled
346     * @since 8.3
347     */
348    public static void findElementWaitUntilEnabledAndClick(WebElement parentElement, final By by,
349            final int findElementTimeout, final int waitUntilEnabledTimeout) throws NotFoundException {
350        WebElement element = findElementAndWaitUntilEnabled(parentElement, by, findElementTimeout,
351                waitUntilEnabledTimeout);
352        waitUntilGivenFunctionIgnoring(new Function<WebDriver, Boolean>() {
353            @Override
354            public Boolean apply(WebDriver driver) {
355                return scrollAndForceClick(element);
356            }
357        }, StaleElementReferenceException.class);
358    }
359
360    /**
361     * Waits until the element is enabled, with a default timeout. Then clicks on the element.
362     *
363     * @param element the element
364     * @throws NotFoundException if the element is not found or not enabled
365     * @since 8.3
366     */
367    public static void waitUntilEnabledAndClick(final WebElement element) throws NotFoundException {
368        waitUntilEnabledAndClick(element, AbstractTest.AJAX_TIMEOUT_SECONDS * 1000);
369    }
370
371    /**
372     * Waits until the element is enabled, with a {@code waitUntilEnabledTimeout}. Scroll to it, then clicks on the
373     * element.
374     *
375     * @param element the element
376     * @param waitUntilEnabledTimeout the wait until enabled timeout in milliseconds
377     * @throws NotFoundException if the element is not found or not enabled
378     * @since 8.3
379     */
380    public static void waitUntilEnabledAndClick(final WebElement element, final int waitUntilEnabledTimeout)
381            throws NotFoundException {
382        waitUntilEnabled(element, waitUntilEnabledTimeout);
383        waitUntilGivenFunctionIgnoring(new Function<WebDriver, Boolean>() {
384            @Override
385            public Boolean apply(WebDriver driver) {
386                return scrollAndForceClick(element);
387            }
388        }, StaleElementReferenceException.class);
389    }
390
391    /**
392     * Finds the first {@link WebElement} using the given method, with a {@code findElementTimeout}. Then clicks on the
393     * element.
394     *
395     * @param by the locating mechanism
396     * @throws NotFoundException if the element is not found or not enabled
397     * @since 5.9.4
398     */
399    public static void findElementWithTimeoutAndClick(final By by) throws NotFoundException {
400
401        waitUntilGivenFunctionIgnoring(new Function<WebDriver, Boolean>() {
402            @Override
403            public Boolean apply(WebDriver driver) {
404                // Find the element.
405                WebElement element = findElementWithTimeout(by);
406
407                element.click();
408                return true;
409            }
410        }, StaleElementReferenceException.class);
411    }
412
413    /**
414     * Fluent wait for an element not to be present, checking every 100 ms.
415     *
416     * @since 5.7.2
417     */
418    public static void waitUntilElementNotPresent(final By locator) {
419        Wait<WebDriver> wait = getFluentWait();
420        wait.until((new Function<WebDriver, By>() {
421            @Override
422            public By apply(WebDriver driver) {
423                try {
424                    driver.findElement(locator);
425                } catch (NoSuchElementException ex) {
426                    // ok
427                    return locator;
428                }
429                return null;
430            }
431        }));
432    }
433
434    /**
435     * @since 9.3
436     */
437    public static void waitUntilWindowClosed(final String windowHandle) {
438        Wait<WebDriver> wait = getFluentWait();
439        wait.until(driver -> !driver.getWindowHandles().contains(windowHandle));
440    }
441
442    /**
443     * Fluent wait for an element to be present, checking every 100 ms.
444     *
445     * @since 5.7.2
446     */
447    public static void waitUntilElementPresent(final By locator) {
448        FluentWait<WebDriver> wait = getFluentWait();
449        wait.ignoring(NoSuchElementException.class);
450        wait.until(new Function<WebDriver, WebElement>() {
451            @Override
452            public WebElement apply(WebDriver driver) {
453                return driver.findElement(locator);
454            }
455        });
456    }
457
458    /**
459     * Waits until an element is enabled, with a timeout.
460     *
461     * @param element the element
462     */
463    public static void waitUntilEnabled(WebElement element) throws NotFoundException {
464        waitUntilEnabled(element, AbstractTest.AJAX_TIMEOUT_SECONDS * 1000);
465    }
466
467    /**
468     * Waits until an element is enabled, with a timeout.
469     *
470     * @param element the element
471     * @param timeout the timeout in milliseconds
472     */
473    public static void waitUntilEnabled(final WebElement element, int timeout) throws NotFoundException {
474        FluentWait<WebDriver> wait = getFluentWait();
475        wait.withTimeout(timeout, TimeUnit.MILLISECONDS);
476        Function<WebDriver, Boolean> function = new Function<WebDriver, Boolean>() {
477            @Override
478            public Boolean apply(WebDriver driver) {
479                return element.isEnabled();
480            }
481        };
482        try {
483            wait.until(function);
484        } catch (TimeoutException e) {
485            throw new NotFoundException("Element not enabled after timeout: " + element);
486        }
487    }
488
489    /**
490     * Fluent wait on a the given function, checking every 100 ms.
491     *
492     * @param function
493     * @since 5.9.2
494     */
495    public static void waitUntilGivenFunction(Function<WebDriver, Boolean> function) {
496        waitUntilGivenFunctionIgnoring(function, null);
497    }
498
499    /**
500     * Fluent wait on a the given function, checking every 100 ms.
501     *
502     * @param function
503     * @param ignoredExceptions the types of exceptions to ignore.
504     * @since 5.9.2
505     */
506    @SafeVarargs
507    public static <K extends java.lang.Throwable> void waitUntilGivenFunctionIgnoreAll(
508            Function<WebDriver, Boolean> function, java.lang.Class<? extends K>... ignoredExceptions) {
509        FluentWait<WebDriver> wait = getFluentWait();
510        if (ignoredExceptions != null) {
511            if (ignoredExceptions.length == 1) {
512                wait.ignoring(ignoredExceptions[0]);
513            } else {
514                wait.ignoreAll(Arrays.asList(ignoredExceptions));
515            }
516
517        }
518        wait.until(function);
519    }
520
521    /**
522     * Fluent wait on a the given function, checking every 100 ms.
523     *
524     * @param function
525     * @param ignoredException the type of exception to ignore.
526     * @since 5.9.2
527     */
528    public static <K extends java.lang.Throwable> void waitUntilGivenFunctionIgnoring(
529            Function<WebDriver, Boolean> function, java.lang.Class<? extends K> ignoredException) {
530        FluentWait<WebDriver> wait = getFluentWait();
531        if (ignoredException != null) {
532            wait.ignoring(ignoredException);
533        }
534        wait.until(function);
535    }
536
537    /**
538     * Waits until the URL contains the string given in parameter, with a timeout.
539     *
540     * @param string the string that is to be contained
541     * @since 5.9.2
542     */
543    public static void waitUntilURLContains(String string) {
544        waitUntilURLContainsOrNot(string, true);
545    }
546
547    /**
548     * @since 5.9.2
549     */
550    private static void waitUntilURLContainsOrNot(String string, final boolean contain) {
551        final String refurl = string;
552        ExpectedCondition<Boolean> condition = new ExpectedCondition<Boolean>() {
553            @Override
554            public Boolean apply(WebDriver d) {
555                String currentUrl = d.getCurrentUrl();
556                boolean result = !(currentUrl.contains(refurl) ^ contain);
557                if (!result) {
558                    AbstractTest.log.debug("currentUrl is : " + currentUrl);
559                    AbstractTest.log.debug((contain ? "It should contains : " : "It should not contains : ") + refurl);
560                }
561                return result;
562            }
563        };
564        WebDriverWait wait = new WebDriverWait(AbstractTest.driver, URLCHANGE_MAX_WAIT);
565        wait.until(condition);
566    }
567
568    /**
569     * Waits until the URL is different from the one given in parameter, with a timeout.
570     *
571     * @param url the URL to compare to
572     */
573    public static void waitUntilURLDifferentFrom(String url) {
574        final String refurl = url;
575        AbstractTest.log.debug("Watch URL: " + refurl);
576        ExpectedCondition<Boolean> urlchanged = d -> {
577            if (d == null) {
578                return false;
579            }
580
581            String currentUrl = d.getCurrentUrl();
582            AbstractTest.log.debug("currentUrl is still: " + currentUrl);
583            return !currentUrl.equals(refurl);
584        };
585        WebDriverWait wait = new WebDriverWait(AbstractTest.driver, URLCHANGE_MAX_WAIT);
586        wait.until(urlchanged);
587        if (AbstractTest.driver.getCurrentUrl().equals(refurl)) {
588            AbstractTest.log.warn("Page change failed");
589        }
590    }
591
592    /**
593     * Waits until the URL does not contain the string given in parameter, with a timeout.
594     *
595     * @param string the string that is not to be contained
596     * @since 5.9.2
597     */
598    public static void waitUntilURLNotContain(String string) {
599        waitUntilURLContainsOrNot(string, false);
600    }
601
602    /**
603     * Return parent element with given tag name.
604     * <p>
605     * Throws a {@link NoSuchElementException} error if no element found.
606     *
607     * @since 7.3
608     */
609    public static WebElement findParentTag(WebElement elt, String tagName) {
610        try {
611            By parentBy = By.xpath("..");
612            WebElement p = elt.findElement(parentBy);
613            while (p != null) {
614                if (tagName.equals(p.getTagName())) {
615                    return p;
616                }
617                p = p.findElement(parentBy);
618            }
619        } catch (InvalidSelectorException e) {
620        }
621        throw new NoSuchElementException(String.format("No parent element found with tag %s.", tagName));
622    }
623
624    /**
625     * Scrolls to the element in the view: allows to safely click on it afterwards.
626     *
627     * @param executor the javascript executor, usually {@link WebDriver}
628     * @param element the element to scroll to
629     * @since 8.3
630     */
631    public static final void scrollToElement(WebElement element) {
632        ((JavascriptExecutor) AbstractTest.driver).executeScript("arguments[0].scrollIntoView(false);", element);
633    }
634
635    /**
636     * Forces a click on an element, to workaround non-effective clicks in miscellaneous situations, after having
637     * scrolled to it.
638     *
639     * @param executor the javascript executor, usually {@link WebDriver}
640     * @param element the element to scroll to
641     * @return true if element is clickable
642     * @since 8.3
643     */
644    public static final boolean scrollAndForceClick(WebElement element) {
645        JavascriptExecutor executor = (JavascriptExecutor) AbstractTest.driver;
646        scrollToElement(element);
647        try {
648            // forced click to workaround non-effective clicks in miscellaneous situations
649            executor.executeScript("arguments[0].click();", element);
650            return true;
651        } catch (WebDriverException e) {
652            if (e.getMessage().contains("Element is not clickable at point")) {
653                AbstractTest.log.debug("Element is not clickable yet");
654                return false;
655            }
656            throw e;
657        }
658    }
659
660}