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