001/*
002 * (C) Copyright 2011-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 *     Sun Seng David TAN
018 *     Florent Guillaume
019 *     Benoit Delbosc
020 *     Antoine Taillefer
021 *     Anahide Tchertchian
022 *     Guillaume Renard
023 *     Mathieu Guillaume
024 *     Julien Carsique
025 */
026package org.nuxeo.functionaltests;
027
028import static org.junit.Assert.assertEquals;
029import static org.junit.Assert.assertTrue;
030import static org.junit.Assume.assumeFalse;
031import static org.nuxeo.functionaltests.Constants.ADMINISTRATOR;
032
033import java.io.File;
034import java.io.IOException;
035import java.lang.reflect.Constructor;
036import java.lang.reflect.Field;
037import java.net.MalformedURLException;
038import java.net.URL;
039import java.util.ArrayList;
040import java.util.List;
041import java.util.concurrent.TimeUnit;
042
043import org.apache.commons.lang.SystemUtils;
044import org.apache.commons.logging.Log;
045import org.apache.commons.logging.LogFactory;
046import org.junit.After;
047import org.junit.AfterClass;
048import org.junit.BeforeClass;
049import org.junit.Rule;
050import org.junit.rules.MethodRule;
051import org.nuxeo.common.utils.FileUtils;
052import org.nuxeo.functionaltests.drivers.ChromeDriverProvider;
053import org.nuxeo.functionaltests.drivers.FirefoxDriverProvider;
054import org.nuxeo.functionaltests.drivers.RemoteFirefoxDriverProvider;
055import org.nuxeo.functionaltests.fragment.WebFragment;
056import org.nuxeo.functionaltests.pages.AbstractPage;
057import org.nuxeo.functionaltests.pages.DocumentBasePage;
058import org.nuxeo.functionaltests.pages.DocumentBasePage.UserNotConnectedException;
059import org.nuxeo.functionaltests.pages.FileDocumentBasePage;
060import org.nuxeo.functionaltests.pages.LoginPage;
061import org.nuxeo.functionaltests.pages.NoteDocumentBasePage;
062import org.nuxeo.functionaltests.pages.tabs.CollectionContentTabSubPage;
063import org.nuxeo.functionaltests.proxy.ProxyManager;
064import org.nuxeo.runtime.api.Framework;
065import org.openqa.selenium.By;
066import org.openqa.selenium.NoSuchElementException;
067import org.openqa.selenium.Proxy;
068import org.openqa.selenium.TimeoutException;
069import org.openqa.selenium.WebDriver;
070import org.openqa.selenium.WebElement;
071import org.openqa.selenium.internal.WrapsElement;
072import org.openqa.selenium.remote.CapabilityType;
073import org.openqa.selenium.remote.Command;
074import org.openqa.selenium.remote.DesiredCapabilities;
075import org.openqa.selenium.remote.DriverCommand;
076import org.openqa.selenium.remote.RemoteWebDriver;
077import org.openqa.selenium.support.PageFactory;
078import org.openqa.selenium.support.ui.FluentWait;
079import org.openqa.selenium.support.ui.Wait;
080
081import com.google.common.collect.ImmutableMap;
082
083/**
084 * Base functions for all pages.
085 */
086public abstract class AbstractTest {
087
088    /**
089     * @since 5.9.2
090     */
091    public final static String TEST_USERNAME = "jdoe";
092
093    /**
094     * @since 5.9.2
095     */
096    public final static String TEST_PASSWORD = "test";
097
098    /**
099     * Polling frequency in milliseconds.
100     *
101     * @since 5.9.2
102     */
103    public static final int POLLING_FREQUENCY_MILLISECONDS = 100;
104
105    public static final int POLLING_FREQUENCY_SECONDS = 1;
106
107    /**
108     * Page Load timeout in seconds.
109     *
110     * @since 5.9.2
111     */
112    public static final int PAGE_LOAD_TIME_OUT_SECONDS = 60;
113
114    public static final int LOAD_TIMEOUT_SECONDS = 30;
115
116    /**
117     * Driver implicit wait in milliseconds.
118     *
119     * @since 8.3
120     */
121    public static final int IMPLICIT_WAIT_MILLISECONDS = 200;
122
123    public static final int LOAD_SHORT_TIMEOUT_SECONDS = 2;
124
125    public static final int AJAX_TIMEOUT_SECONDS = 10;
126
127    public static final int AJAX_SHORT_TIMEOUT_SECONDS = 2;
128
129    /**
130     * @since 5.7
131     * @deprecated since 8.3
132     * @see ChromeDriverProvider
133     */
134    @Deprecated
135    public static final String CHROME_DRIVER_DEFAULT_PATH_LINUX = ChromeDriverProvider.CHROME_DRIVER_DEFAULT_PATH_LINUX;
136
137    /**
138     * @since 5.7 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" doesn't work
139     * @deprecated since 8.3
140     * @see ChromeDriverProvider
141     */
142    @Deprecated
143    public static final String CHROME_DRIVER_DEFAULT_PATH_MAC = ChromeDriverProvider.CHROME_DRIVER_DEFAULT_PATH_MAC;
144
145    /**
146     * @since 5.7
147     * @deprecated since 8.3
148     * @see ChromeDriverProvider
149     */
150    @Deprecated
151    public static final String CHROME_DRIVER_DEFAULT_PATH_WINVISTA = ChromeDriverProvider.CHROME_DRIVER_DEFAULT_PATH_WINVISTA;
152
153    /**
154     * @since 5.7
155     * @deprecated since 8.3
156     * @see ChromeDriverProvider
157     */
158    @Deprecated
159    public static final String CHROME_DRIVER_DEFAULT_PATH_WINXP = ChromeDriverProvider.CHROME_DRIVER_DEFAULT_PATH_WINXP;
160
161    /**
162     * @since 5.7
163     * @deprecated since 8.3
164     * @see ChromeDriverProvider
165     */
166    @Deprecated
167    public static final String CHROME_DRIVER_DEFAULT_EXECUTABLE_NAME = ChromeDriverProvider.CHROME_DRIVER_DEFAULT_EXECUTABLE_NAME;
168
169    /**
170     * @since 5.7
171     * @deprecated since 8.3
172     * @see ChromeDriverProvider
173     */
174    @Deprecated
175    public static final String CHROME_DRIVER_WINDOWS_EXECUTABLE_NAME = ChromeDriverProvider.CHROME_DRIVER_WINDOWS_EXECUTABLE_NAME;
176
177    /**
178     * @deprecated since 8.3
179     * @see ChromeDriverProvider
180     */
181    @Deprecated
182    public static final String SYSPROP_CHROME_DRIVER_PATH = ChromeDriverProvider.SYSPROP_CHROME_DRIVER_PATH;
183
184    static final Log log = LogFactory.getLog(AbstractTest.class);
185
186    public static final String NUXEO_URL = System.getProperty("nuxeoURL", "http://localhost:8080/nuxeo")
187                                                 .replaceAll("/$", "");
188
189    public static RemoteWebDriver driver;
190
191    protected static ProxyManager proxyManager;
192
193    /**
194     * Logger method to follow what's being run on server logs and take a screenshot of the last page in case of failure
195     */
196    @Rule
197    public MethodRule watchman = new LogTestWatchman(driver, NUXEO_URL);
198
199    /**
200     * This method will be executed before any method registered with JUnit After annotation.
201     *
202     * @since 5.8
203     */
204    public void runBeforeAfters() {
205        ((LogTestWatchman) watchman).runBeforeAfters();
206    }
207
208    @BeforeClass
209    public static void initDriver() throws Exception {
210        String browser = System.getProperty("browser", "firefox");
211        // Use the same strings as command-line Selenium
212        if (browser.equals("chrome") || browser.equals("firefox")) {
213            initFirefoxDriver();
214        } else if (browser.equals("remotefirefox")) {
215            initRemoteFirefoxDriver();
216        } else if (browser.equals("googlechrome")) {
217            initChromeDriver();
218        } else {
219            throw new RuntimeException("Browser not supported: " + browser);
220        }
221        driver.manage().timeouts().pageLoadTimeout(PAGE_LOAD_TIME_OUT_SECONDS, TimeUnit.SECONDS);
222        driver.manage().timeouts().implicitlyWait(IMPLICIT_WAIT_MILLISECONDS, TimeUnit.MILLISECONDS);
223    }
224
225    protected static void initFirefoxDriver() throws Exception {
226        proxyManager = new ProxyManager();
227        Proxy proxy = proxyManager.startProxy();
228        if (proxy != null) {
229            proxy.setNoProxy("");
230        }
231        DesiredCapabilities dc = DesiredCapabilities.firefox();
232        dc.setCapability(CapabilityType.PROXY, proxy);
233        driver = new FirefoxDriverProvider().init(dc);
234    }
235
236    protected static void initRemoteFirefoxDriver() throws Exception {
237        proxyManager = new ProxyManager();
238        Proxy proxy = proxyManager.startProxy();
239        if (proxy != null) {
240            proxy.setNoProxy("");
241        }
242        DesiredCapabilities dc = DesiredCapabilities.firefox();
243        dc.setCapability(CapabilityType.PROXY, proxy);
244        driver = new RemoteFirefoxDriverProvider().init(dc);
245    }
246
247    protected static void initChromeDriver() throws Exception {
248        proxyManager = new ProxyManager();
249        Proxy proxy = proxyManager.startProxy();
250        DesiredCapabilities dc = DesiredCapabilities.chrome();
251        if (proxy != null) {
252            proxy.setNoProxy("");
253            dc.setCapability(CapabilityType.PROXY, proxy);
254        }
255        driver = new ChromeDriverProvider().init(dc);
256    }
257
258    /**
259     * @since 7.1
260     */
261    @After
262    public void checkJavascriptError() {
263        if (driver != null) {
264            new JavaScriptErrorCollector(driver).checkForErrors();
265        }
266    }
267
268    @AfterClass
269    public static void quitDriver() {
270        if (driver != null) {
271            driver.quit();
272            driver = null;
273        }
274
275        try {
276            proxyManager.stopProxy();
277            proxyManager = null;
278        } catch (Exception e) {
279            log.error("Could not stop proxy: " + e.getMessage());
280        }
281    }
282
283    public static <T> T get(String url, Class<T> pageClassToProxy) {
284        if (driver != null) {
285            new JavaScriptErrorCollector(driver).checkForErrors();
286        }
287        driver.get(url);
288        return asPage(pageClassToProxy);
289    }
290
291    /**
292     * Opens given url adding hardcoded Seam conversation named "0NXMAIN".
293     *
294     * @since 8.3
295     */
296    public static void open(String url) {
297        if (driver != null) {
298            new JavaScriptErrorCollector(driver).checkForErrors();
299        }
300        driver.get(NUXEO_URL + url + "?conversationId=0NXMAIN");
301    }
302
303    /**
304     * Do not wait for page load. Do not handle error. Do not give explicit error in case of failure. This is a very raw
305     * get.
306     *
307     * @since 6.0
308     */
309    public static <T> T getWithoutErrorHandler(String url, Class<T> pageClassToProxy) throws IOException {
310        Command command = new Command(AbstractTest.driver.getSessionId(), DriverCommand.GET,
311                ImmutableMap.of("url", url));
312        AbstractTest.driver.getCommandExecutor().execute(command);
313        return asPage(pageClassToProxy);
314    }
315
316    public static WebDriver getPopup() {
317        String currentWindow = driver.getWindowHandle();
318        for (String popup : driver.getWindowHandles()) {
319            if (popup.equals(currentWindow)) {
320                continue;
321            }
322            return driver.switchTo().window(popup);
323        }
324        return null;
325    }
326
327    public static <T> T asPage(Class<T> pageClassToProxy) {
328        T page = instantiatePage(pageClassToProxy);
329        return fillElement(pageClassToProxy, page);
330    }
331
332    public static <T extends WebFragment> T getWebFragment(By by, Class<T> webFragmentClass) {
333        WebElement element = Locator.findElementWithTimeout(by);
334        return getWebFragment(element, webFragmentClass);
335    }
336
337    public static <T extends WebFragment> T getWebFragment(WebElement element, Class<T> webFragmentClass) {
338        T webFragment = instantiateWebFragment(element, webFragmentClass);
339        webFragment = fillElement(webFragmentClass, webFragment);
340        // fillElement somehow overwrite the 'element' field, reset it.
341        webFragment.setElement(element);
342        return webFragment;
343    }
344
345    /**
346     * Fills an instantiated page/form/widget attributes
347     *
348     * @since 5.7
349     */
350    public static <T> T fillElement(Class<T> pageClassToProxy, T page) {
351        PageFactory.initElements(new VariableElementLocatorFactory(driver, AJAX_TIMEOUT_SECONDS), page);
352        // check all required WebElements on the page and wait for their
353        // loading
354        final List<String> fieldNames = new ArrayList<>();
355        final List<WrapsElement> elements = new ArrayList<>();
356        for (Field field : pageClassToProxy.getDeclaredFields()) {
357            if (field.getAnnotation(Required.class) != null) {
358                try {
359                    field.setAccessible(true);
360                    fieldNames.add(field.getName());
361                    elements.add((WrapsElement) field.get(page));
362                } catch (Exception e) {
363                    throw new RuntimeException(e);
364                }
365            }
366        }
367
368        Wait<T> wait = new FluentWait<>(page).withTimeout(LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS)
369                                             .pollingEvery(POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
370        try {
371            return wait.until(aPage -> {
372                String notLoaded = anyElementNotLoaded(elements, fieldNames);
373                if (notLoaded == null) {
374                    // check if there are Jquery ajax requests to complete
375                    if (pageClassToProxy.isAnnotationPresent(WaitForJQueryAjaxOnLoading.class)) {
376                        new AjaxRequestManager(driver).waitForJQueryRequests();
377                    }
378
379                    return aPage;
380                } else {
381                    return null;
382                }
383            });
384        } catch (TimeoutException e) {
385            throw new TimeoutException("not loaded: " + anyElementNotLoaded(elements, fieldNames), e);
386        }
387    }
388
389    protected static String anyElementNotLoaded(List<WrapsElement> proxies, List<String> fieldNames) {
390        for (int i = 0; i < proxies.size(); i++) {
391            WrapsElement proxy = proxies.get(i);
392            try {
393                // method implemented in LocatingElementHandler
394                proxy.getWrappedElement();
395            } catch (NoSuchElementException e) {
396                return fieldNames.get(i);
397            }
398        }
399        return null;
400    }
401
402    // private in PageFactory...
403    protected static <T> T instantiatePage(Class<T> pageClassToProxy) {
404        try {
405            try {
406                Constructor<T> constructor = pageClassToProxy.getConstructor(WebDriver.class);
407                return constructor.newInstance(driver);
408            } catch (NoSuchMethodException e) {
409                return pageClassToProxy.newInstance();
410            }
411        } catch (RuntimeException e) {
412            throw e;
413        } catch (Exception e) {
414            throw new RuntimeException(e);
415        }
416    }
417
418    protected static <T extends WebFragment> T instantiateWebFragment(WebElement element, Class<T> webFragmentClass) {
419        try {
420            try {
421                Constructor<T> constructor = webFragmentClass.getConstructor(WebDriver.class, WebElement.class);
422                return constructor.newInstance(driver, element);
423            } catch (NoSuchMethodException e) {
424                return webFragmentClass.newInstance();
425            }
426        } catch (RuntimeException e) {
427            throw e;
428        } catch (Exception e) {
429            throw new RuntimeException(e);
430        }
431    }
432
433    public LoginPage getLoginPage() {
434        return get(NUXEO_URL + "/logout", LoginPage.class);
435    }
436
437    public LoginPage logout() {
438        return getLoginPage();
439    }
440
441    /**
442     * navigate to a link text. wait until the link is available and click on it.
443     */
444    public <T extends AbstractPage> T nav(Class<T> pageClass, String linkText) {
445        WebElement link = Locator.findElementWithTimeout(By.linkText(linkText));
446        if (link == null) {
447            return null;
448        }
449        link.click();
450        return asPage(pageClass);
451    }
452
453    /**
454     * Navigate to a specified url
455     *
456     * @param urlString url
457     * @throws MalformedURLException
458     */
459    public void navToUrl(String urlString) throws MalformedURLException {
460        URL url = new URL(urlString);
461        driver.navigate().to(url);
462    }
463
464    /**
465     * Login as Administrator
466     *
467     * @return the Document base page (by default returned by CAP)
468     * @throws UserNotConnectedException
469     */
470    public DocumentBasePage login() throws UserNotConnectedException {
471        return login(ADMINISTRATOR, ADMINISTRATOR);
472    }
473
474    public DocumentBasePage login(String username, String password) throws UserNotConnectedException {
475        DocumentBasePage documentBasePage = getLoginPage().login(username, password, DocumentBasePage.class);
476        documentBasePage.checkUserConnected(username);
477        return documentBasePage;
478    }
479
480    /**
481     * Login as default test user.
482     *
483     * @since 5.9.2
484     */
485    public DocumentBasePage loginAsTestUser() throws UserNotConnectedException {
486        return login(TEST_USERNAME, TEST_PASSWORD);
487    }
488
489    /**
490     * Login using an invalid credential.
491     *
492     * @param username the username
493     * @param password the password
494     */
495    public LoginPage loginInvalid(String username, String password) {
496        return getLoginPage().login(username, password, LoginPage.class);
497    }
498
499    /**
500     * Init the repository with a test Workspace form the {@code currentPage}.
501     *
502     * @param currentPage the current page
503     * @return the created Workspace page
504     * @throws Exception if initializing repository fails
505     */
506    @Deprecated
507    protected DocumentBasePage initRepository(DocumentBasePage currentPage) throws Exception {
508        return createWorkspace(currentPage, "Test Workspace", "Test Workspace for my dear WebDriver.");
509    }
510
511    /**
512     * Cleans the repository (delete the test Workspace) from the {@code currentPage}.
513     *
514     * @param currentPage the current page
515     * @throws Exception if cleaning repository fails
516     */
517    @Deprecated
518    protected void cleanRepository(DocumentBasePage currentPage) throws Exception {
519        deleteWorkspace(currentPage, "Test Workspace");
520    }
521
522    /**
523     * Creates a Workspace from the {@code currentPage}.
524     *
525     * @param currentPage the current page
526     * @param workspaceTitle the workspace title
527     * @param workspaceDescription the workspace description
528     * @return the created Workspace page
529     * @deprecated since 8.3: use {@link DocumentBasePage#createWorkspace(String, String)} instead.
530     */
531    @Deprecated
532    protected DocumentBasePage createWorkspace(DocumentBasePage currentPage, String workspaceTitle,
533            String workspaceDescription) {
534        return currentPage.createWorkspace(workspaceTitle, workspaceDescription);
535    }
536
537    /**
538     * Deletes the Workspace with title {@code workspaceTitle} from the {@code currentPage}.
539     *
540     * @param currentPage the current page
541     * @param workspaceTitle the workspace title
542     * @deprecated since 8.3: use {@link DocumentBasePage#deleteWorkspace(String)} instead.
543     */
544    @Deprecated
545    protected void deleteWorkspace(DocumentBasePage currentPage, String workspaceTitle) {
546        currentPage.deleteWorkspace(workspaceTitle);
547    }
548
549    /**
550     * Creates a File form the {@code currentPage}.
551     *
552     * @param currentPage the current page
553     * @param fileTitle the file title
554     * @param fileDescription the file description
555     * @param uploadBlob true if a blob needs to be uploaded (temporary file created for this purpose)
556     * @param filePrefix the file prefix
557     * @param fileSuffix the file suffix
558     * @param fileContent the file content
559     * @return the created File page
560     * @throws IOException if temporary file creation fails
561     * @deprecated since 8.3: use {@link DocumentBasePage#createFile(String, String, boolean, String, String, String)}
562     *             instead.
563     */
564    @Deprecated
565    protected FileDocumentBasePage createFile(DocumentBasePage currentPage, String fileTitle, String fileDescription,
566            boolean uploadBlob, String filePrefix, String fileSuffix, String fileContent) throws IOException {
567        return currentPage.createFile(fileTitle, fileDescription, uploadBlob, filePrefix, fileSuffix, fileContent);
568    }
569
570    /**
571     * Creates a Collections container form the {@code currentPage}.
572     *
573     * @param currentPage the current page
574     * @param collectionsTitle the Collections container title
575     * @param fileDescription the collections description
576     * @return the created Collections page
577     * @deprecated since 8.3: use {@link DocumentBasePage#createCollections(String, String)} instead.
578     */
579    @Deprecated
580    protected DocumentBasePage createCollections(DocumentBasePage currentPage, String collectionsTitle,
581            String fileDescription) {
582        return currentPage.createCollections(collectionsTitle, fileDescription);
583    }
584
585    /**
586     * Creates a Collection form the {@code currentPage}.
587     *
588     * @param currentPage the current page
589     * @param collectionsTitle the Collections container title
590     * @param fileDescription the collection description
591     * @return the created Collections page
592     * @deprecated since 8.3: use {@link DocumentBasePage#createCollection(String, String)} instead.
593     */
594    @Deprecated
595    protected CollectionContentTabSubPage createCollection(DocumentBasePage currentPage, String collectionsTitle,
596            String fileDescription) {
597        return currentPage.createCollection(collectionsTitle, fileDescription);
598    }
599
600    /**
601     * Creates a temporary file and returns its absolute path.
602     *
603     * @param filePrefix the file prefix
604     * @param fileSuffix the file suffix
605     * @param fileContent the file content
606     * @return the temporary file to upload path
607     * @throws IOException if temporary file creation fails
608     * @since 5.9.3
609     */
610    public static String getTmpFileToUploadPath(String filePrefix, String fileSuffix, String fileContent)
611            throws IOException {
612        // Create tmp file, deleted on exit
613        File tmpFile = Framework.createTempFile(filePrefix, fileSuffix);
614        tmpFile.deleteOnExit();
615        FileUtils.writeFile(tmpFile, fileContent);
616        assertTrue(tmpFile.exists());
617
618        // Check file URI protocol
619        assertEquals("file", tmpFile.toURI().toURL().getProtocol());
620
621        // Return file absolute path
622        return tmpFile.getAbsolutePath();
623    }
624
625    /**
626     * Get the current document id stored in the javascript ctx.currentDocument variable of the current page.
627     *
628     * @return the current document id
629     * @since 5.7
630     */
631    protected String getCurrentDocumentId() {
632        return (String) driver.executeScript("return ctx.currentDocument;");
633    }
634
635    /**
636     * Creates a Note form the {@code currentPage}.
637     *
638     * @param currentPage the current page
639     * @param noteTitle the note title
640     * @param noteDescription the note description
641     * @param defineNote true if the content of the note needs to be defined
642     * @param noteContent the content of the note
643     * @return the created note page.
644     * @throws IOException
645     * @since 5.9.4
646     * @deprecated since 8.3: use {@link DocumentBasePage#createNote(String, String, boolean, String)} instead.
647     */
648    @Deprecated
649    protected NoteDocumentBasePage createNote(DocumentBasePage currentPage, String noteTitle, String noteDescription,
650            boolean defineNote, String noteContent) throws IOException {
651        return currentPage.createNote(noteTitle, noteDescription, defineNote, noteContent);
652    }
653
654    /**
655     * Do not run on windows with Firefox 26 (NXP-17848).
656     *
657     * @since 7.10
658     */
659    protected void doNotRunOnWindowsWithFF26() {
660        String browser, browserVersion = null;
661        try {
662            browser = driver.getCapabilities().getBrowserName();
663            browserVersion = driver.getCapabilities().getVersion();
664            Float iBrowserVersion = Float.parseFloat(browserVersion);
665            assumeFalse(SystemUtils.IS_OS_WINDOWS && browser.equals("firefox") && iBrowserVersion <= 28.0);
666        } catch (NumberFormatException e) {
667            log.warn("Could not parse browser version: " + browserVersion);
668        }
669    }
670
671}