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