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