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