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.Assume.assumeFalse;
029import static org.junit.Assert.assertEquals;
030import static org.junit.Assert.assertTrue;
031
032import java.io.File;
033import java.io.IOException;
034import java.lang.reflect.Constructor;
035import java.lang.reflect.Field;
036import java.lang.reflect.Method;
037import java.net.MalformedURLException;
038import java.net.URI;
039import java.net.URL;
040import java.net.URLClassLoader;
041import java.util.ArrayList;
042import java.util.Arrays;
043import java.util.List;
044import java.util.concurrent.TimeUnit;
045import java.util.jar.Attributes;
046import java.util.jar.JarFile;
047
048import org.apache.commons.lang.SystemUtils;
049import org.apache.commons.logging.Log;
050import org.apache.commons.logging.LogFactory;
051import org.browsermob.proxy.ProxyServer;
052import org.junit.After;
053import org.junit.AfterClass;
054import org.junit.BeforeClass;
055import org.junit.Rule;
056import org.junit.rules.MethodRule;
057
058import org.nuxeo.common.Environment;
059import org.nuxeo.common.utils.FileUtils;
060import org.nuxeo.functionaltests.fragment.WebFragment;
061import org.nuxeo.functionaltests.pages.AbstractPage;
062import org.nuxeo.functionaltests.pages.DocumentBasePage;
063import org.nuxeo.functionaltests.pages.DocumentBasePage.UserNotConnectedException;
064import org.nuxeo.functionaltests.pages.FileDocumentBasePage;
065import org.nuxeo.functionaltests.pages.LoginPage;
066import org.nuxeo.functionaltests.pages.NoteDocumentBasePage;
067import org.nuxeo.functionaltests.pages.forms.CollectionCreationFormPage;
068import org.nuxeo.functionaltests.pages.forms.DublinCoreCreationDocumentFormPage;
069import org.nuxeo.functionaltests.pages.forms.FileCreationFormPage;
070import org.nuxeo.functionaltests.pages.forms.NoteCreationFormPage;
071import org.nuxeo.functionaltests.pages.forms.WorkspaceFormPage;
072import org.nuxeo.functionaltests.pages.tabs.CollectionContentTabSubPage;
073import org.nuxeo.runtime.api.Framework;
074
075import org.openqa.selenium.By;
076import org.openqa.selenium.JavascriptExecutor;
077import org.openqa.selenium.NoSuchElementException;
078import org.openqa.selenium.Proxy;
079import org.openqa.selenium.TimeoutException;
080import org.openqa.selenium.WebDriver;
081import org.openqa.selenium.WebElement;
082import org.openqa.selenium.chrome.ChromeDriver;
083import org.openqa.selenium.chrome.ChromeOptions;
084import org.openqa.selenium.firefox.FirefoxDriver;
085import org.openqa.selenium.firefox.FirefoxProfile;
086import org.openqa.selenium.internal.WrapsElement;
087import org.openqa.selenium.remote.CapabilityType;
088import org.openqa.selenium.remote.Command;
089import org.openqa.selenium.remote.DesiredCapabilities;
090import org.openqa.selenium.remote.DriverCommand;
091import org.openqa.selenium.remote.RemoteWebDriver;
092import org.openqa.selenium.support.PageFactory;
093import org.openqa.selenium.support.ui.FluentWait;
094import org.openqa.selenium.support.ui.Wait;
095
096import com.google.common.base.Function;
097import com.google.common.collect.ImmutableMap;
098import net.jsourcerer.webdriver.jserrorcollector.JavaScriptError;
099
100/**
101 * Base functions for all pages.
102 */
103public abstract class AbstractTest {
104
105    /**
106     * @since 5.9.2
107     */
108    public final static String TEST_USERNAME = "jdoe";
109
110    /**
111     * @since 5.9.2
112     */
113    public final static String TEST_PASSWORD = "test";
114
115    /**
116     * Polling frequency in milliseconds.
117     *
118     * @since 5.9.2
119     */
120    public static final int POLLING_FREQUENCY_MILLISECONDS = 100;
121
122    /**
123     * Page Load timeout in seconds.
124     *
125     * @since 5.9.2
126     */
127    public static final int PAGE_LOAD_TIME_OUT_SECONDS = 60;
128
129    /**
130     * @since 5.7
131     */
132    public static final String CHROME_DRIVER_DEFAULT_PATH_LINUX = "/usr/bin/chromedriver";
133
134    /**
135     * @since 5.7 "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome" doesn't work
136     */
137    public static final String CHROME_DRIVER_DEFAULT_PATH_MAC = "/Applications/chromedriver";
138
139    /**
140     * @since 5.7
141     */
142    public static final String CHROME_DRIVER_DEFAULT_PATH_WINVISTA = SystemUtils.getUserHome().getPath()
143            + "\\AppData\\Local\\Google\\Chrome\\Application\\chromedriver.exe";
144
145    /**
146     * @since 5.7
147     */
148    public static final String CHROME_DRIVER_DEFAULT_PATH_WINXP = SystemUtils.getUserHome().getPath()
149            + "\\Local Settings\\Application Data\\Google\\Chrome\\Application\\chromedriver.exe";
150
151    /**
152     * @since 5.7
153     */
154    public static final String CHROME_DRIVER_DEFAULT_EXECUTABLE_NAME = "chromedriver";
155
156    /**
157     * @since 5.7
158     */
159    public static final String CHROME_DRIVER_WINDOWS_EXECUTABLE_NAME = "chromedriver.exe";
160
161    static final Log log = LogFactory.getLog(AbstractTest.class);
162
163    public static final String NUXEO_URL = System.getProperty("nuxeoURL", "http://localhost:8080/nuxeo").replaceAll(
164            "/$", "");
165
166    public static final int LOAD_TIMEOUT_SECONDS = 30;
167
168    public static final int LOAD_SHORT_TIMEOUT_SECONDS = 2;
169
170    public static final int AJAX_TIMEOUT_SECONDS = 10;
171
172    public static final int AJAX_SHORT_TIMEOUT_SECONDS = 2;
173
174    public static final int POLLING_FREQUENCY_SECONDS = 1;
175
176    private static final String FIREBUG_XPI = "firebug-1.6.2-fx.xpi";
177
178    private static final String FIREBUG_VERSION = "1.6.2";
179
180    private static final String FIREBUG_M2 = "firebug/firebug/1.6.2-fx";
181
182    private static final int PROXY_PORT = 4444;
183
184    private static final String HAR_NAME = "http-headers.json";
185
186    public static final String SYSPROP_CHROME_DRIVER_PATH = "webdriver.chrome.driver";
187
188    public static RemoteWebDriver driver;
189
190    protected static File tmp_firebug_xpi;
191
192    protected static ProxyServer proxyServer = null;
193
194    /**
195     * Logger method to follow what's being run on server logs and take a screenshot of the last page in case of failure
196     */
197    @Rule
198    public MethodRule watchman = new LogTestWatchman(driver, NUXEO_URL);
199
200    /**
201     * This method will be executed before any method registered with JUnit After annotation.
202     *
203     * @since 5.8
204     */
205    public void runBeforeAfters() {
206        ((LogTestWatchman) watchman).runBeforeAfters();
207    }
208
209    @BeforeClass
210    public static void initDriver() throws Exception {
211        String browser = System.getProperty("browser", "firefox");
212        // Use the same strings as command-line Selenium
213        if (browser.equals("chrome") || browser.equals("firefox")) {
214            initFirefoxDriver();
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    }
222
223    protected static void initFirefoxDriver() throws Exception {
224        DesiredCapabilities dc = DesiredCapabilities.firefox();
225        FirefoxProfile profile = new FirefoxProfile();
226        // Disable native events (makes things break on Windows)
227        profile.setEnableNativeEvents(false);
228        // Set English as default language
229        profile.setPreference("general.useragent.locale", "en");
230        profile.setPreference("intl.accept_languages", "en");
231        // Set other confs to speed up FF
232
233        // Speed up firefox by pipelining requests on a single connection
234        profile.setPreference("network.http.keep-alive", true);
235        profile.setPreference("network.http.pipelining", true);
236        profile.setPreference("network.http.proxy.pipelining", true);
237        profile.setPreference("network.http.pipelining.maxrequests", 8);
238
239        // Try to use less memory
240        profile.setPreference("browser.sessionhistory.max_entries", 10);
241        profile.setPreference("browser.sessionhistory.max_total_viewers", 4);
242        profile.setPreference("browser.sessionstore.max_tabs_undo", 4);
243        profile.setPreference("browser.sessionstore.interval", 1800000);
244
245        // disable unresponsive script alerts
246        profile.setPreference("dom.max_script_run_time", 0);
247        profile.setPreference("dom.max_chrome_script_run_time", 0);
248
249        // don't skip proxy for localhost
250        profile.setPreference("network.proxy.no_proxies_on", "");
251
252        // prevent different kinds of popups/alerts
253        profile.setPreference("browser.tabs.warnOnClose", false);
254        profile.setPreference("browser.tabs.warnOnOpen", false);
255        profile.setPreference("extensions.newAddons", false);
256        profile.setPreference("extensions.update.notifyUser", false);
257
258        // disable autoscrolling
259        profile.setPreference("browser.urlbar.autocomplete.enabled", false);
260
261        // downloads conf
262        profile.setPreference("browser.download.useDownloadDir", false);
263
264        // prevent FF from running in offline mode when there's no network
265        // connection
266        profile.setPreference("toolkit.networkmanager.disable", true);
267
268        // prevent FF from giving health reports
269        profile.setPreference("datareporting.policy.dataSubmissionEnabled", false);
270        profile.setPreference("datareporting.healthreport.uploadEnabled", false);
271        profile.setPreference("datareporting.healthreport.service.firstRun", false);
272        profile.setPreference("datareporting.healthreport.service.enabled", false);
273        profile.setPreference("datareporting.healthreport.logging.consoleEnabled", false);
274
275        // start page conf to speed up FF
276        profile.setPreference("browser.startup.homepage", "about:blank");
277        profile.setPreference("pref.browser.homepage.disable_button.bookmark_page", false);
278        profile.setPreference("pref.browser.homepage.disable_button.restore_default", false);
279
280        // misc confs to avoid useless updates
281        profile.setPreference("browser.search.update", false);
282        profile.setPreference("browser.bookmarks.restore_default_bookmarks", false);
283
284        // misc confs to speed up FF
285        profile.setPreference("extensions.ui.dictionary.hidden", true);
286        profile.setPreference("layout.spellcheckDefault", 0);
287        // For FF > 40 ?
288        profile.setPreference("startup.homepage_welcome_url.additional", "about:blank");
289
290        // webdriver logging
291        if (Boolean.TRUE.equals(Boolean.valueOf(System.getenv("nuxeo.log.webriver")))) {
292            String location = System.getProperty("basedir") + File.separator + "target";
293            File outputFolder = new File(location);
294            if (!outputFolder.exists() || !outputFolder.isDirectory()) {
295                outputFolder = null;
296            }
297            File webdriverlogFile = File.createTempFile("webdriver", ".log", outputFolder);
298            profile.setPreference("webdriver.log.file", webdriverlogFile.getAbsolutePath());
299            log.warn("Webdriver logs saved in " + webdriverlogFile);
300        }
301
302        addFireBug(profile);
303        JavaScriptError.addExtension(profile);
304        Proxy proxy = startProxy();
305        if (proxy != null) {
306            // Does not work, but leave code for when it does
307            // Workaround: use 127.0.0.2
308            proxy.setNoProxy("");
309            // setProxyPreferences method does not exist with selenium version 2.43.0
310            // profile.setProxyPreferences(proxy);
311            dc.setCapability(CapabilityType.PROXY, proxy);
312        }
313        dc.setCapability(FirefoxDriver.PROFILE, profile);
314        driver = new FirefoxDriver(dc);
315    }
316
317    protected static void initChromeDriver() throws Exception {
318        if (System.getProperty(SYSPROP_CHROME_DRIVER_PATH) == null) {
319            String chromeDriverDefaultPath = null;
320            String chromeDriverExecutableName = CHROME_DRIVER_DEFAULT_EXECUTABLE_NAME;
321            if (SystemUtils.IS_OS_LINUX) {
322                chromeDriverDefaultPath = CHROME_DRIVER_DEFAULT_PATH_LINUX;
323            } else if (SystemUtils.IS_OS_MAC) {
324                chromeDriverDefaultPath = CHROME_DRIVER_DEFAULT_PATH_MAC;
325            } else if (SystemUtils.IS_OS_WINDOWS_XP) {
326                chromeDriverDefaultPath = CHROME_DRIVER_DEFAULT_PATH_WINXP;
327                chromeDriverExecutableName = CHROME_DRIVER_WINDOWS_EXECUTABLE_NAME;
328            } else if (SystemUtils.IS_OS_WINDOWS_VISTA) {
329                chromeDriverDefaultPath = CHROME_DRIVER_DEFAULT_PATH_WINVISTA;
330                chromeDriverExecutableName = CHROME_DRIVER_WINDOWS_EXECUTABLE_NAME;
331            } else if (SystemUtils.IS_OS_WINDOWS) {
332                // Unknown default path on other Windows OS. To be completed.
333                chromeDriverExecutableName = CHROME_DRIVER_WINDOWS_EXECUTABLE_NAME;
334            }
335
336            if (chromeDriverDefaultPath != null && new File(chromeDriverDefaultPath).exists()) {
337                log.warn(String.format("Missing property %s but found %s. Using it...", SYSPROP_CHROME_DRIVER_PATH,
338                        chromeDriverDefaultPath));
339                System.setProperty(SYSPROP_CHROME_DRIVER_PATH, chromeDriverDefaultPath);
340            } else {
341                // Can't find chromedriver in default location, check system
342                // path
343                File chromeDriverExecutable = findExecutableOnPath(chromeDriverExecutableName);
344                if ((chromeDriverExecutable != null) && (chromeDriverExecutable.exists())) {
345                    log.warn(String.format("Missing property %s but found %s. Using it...", SYSPROP_CHROME_DRIVER_PATH,
346                            chromeDriverExecutable.getCanonicalPath()));
347                    System.setProperty(SYSPROP_CHROME_DRIVER_PATH, chromeDriverExecutable.getCanonicalPath());
348                } else {
349                    log.error(String.format("Could not find the Chrome driver looking at %s or system path."
350                            + " Download it from %s and set its path with " + "the System property %s.",
351                            chromeDriverDefaultPath, "http://code.google.com/p/chromedriver/downloads/list",
352                            SYSPROP_CHROME_DRIVER_PATH));
353                }
354            }
355        }
356        DesiredCapabilities dc = DesiredCapabilities.chrome();
357        ChromeOptions options = new ChromeOptions();
358        options.addArguments(Arrays.asList("--ignore-certificate-errors"));
359        Proxy proxy = startProxy();
360        if (proxy != null) {
361            proxy.setNoProxy("");
362            dc.setCapability(CapabilityType.PROXY, proxy);
363        }
364        dc.setCapability(ChromeOptions.CAPABILITY, options);
365        driver = new ChromeDriver(dc);
366    }
367
368    /**
369     * @since 5.7
370     */
371    protected static File findExecutableOnPath(String executableName) {
372        String systemPath = System.getenv("PATH");
373        String[] pathDirs = systemPath.split(File.pathSeparator);
374        File fullyQualifiedExecutable = null;
375        for (String pathDir : pathDirs) {
376            File file = new File(pathDir, executableName);
377            if (file.isFile()) {
378                fullyQualifiedExecutable = file;
379                break;
380            }
381        }
382        return fullyQualifiedExecutable;
383    }
384
385    /**
386     * @since 7.1
387     */
388    @After
389    public void checkJavascriptError() {
390        if (driver != null) {
391            new JavaScriptErrorCollector(driver).checkForErrors();
392        }
393    }
394
395    @AfterClass
396    public static void quitDriver() {
397        if (driver != null) {
398            driver.quit();
399            driver = null;
400        }
401        removeFireBug();
402
403        try {
404            stopProxy();
405        } catch (Exception e) {
406            log.error("Could not stop proxy: " + e.getMessage());
407        }
408    }
409
410    /**
411     * Introspects the classpath and returns the list of files in it. FIXME: should use
412     * HarnessRuntime#getClassLoaderFiles that returns the same thing
413     *
414     * @throws Exception
415     */
416    protected static List<String> getClassLoaderFiles() throws Exception {
417        ClassLoader cl = AbstractTest.class.getClassLoader();
418        URL[] urls = null;
419        if (cl instanceof URLClassLoader) {
420            urls = ((URLClassLoader) cl).getURLs();
421        } else if (cl.getClass().getName().equals("org.apache.tools.ant.AntClassLoader")) {
422            Method method = cl.getClass().getMethod("getClasspath");
423            String cp = (String) method.invoke(cl);
424            String[] paths = cp.split(File.pathSeparator);
425            urls = new URL[paths.length];
426            for (int i = 0; i < paths.length; i++) {
427                urls[i] = new URL("file:" + paths[i]);
428            }
429        } else {
430            System.err.println("Unknown classloader type: " + cl.getClass().getName());
431            return null;
432        }
433
434        JarFile surefirebooterJar = null;
435        for (URL url : urls) {
436            URI uri = url.toURI();
437            if (uri.getPath().matches(".*/nuxeo-runtime-[^/]*\\.jar")) {
438                break;
439            } else if (uri.getScheme().equals("file") && uri.getPath().contains("surefirebooter")) {
440                surefirebooterJar = new JarFile(new File(uri));
441            }
442        }
443
444        // special case for maven surefire with useManifestOnlyJar
445        if (surefirebooterJar != null) {
446            try {
447                try {
448                    String cp = surefirebooterJar.getManifest()
449                                                 .getMainAttributes()
450                                                 .getValue(Attributes.Name.CLASS_PATH);
451                    if (cp != null) {
452                        String[] cpe = cp.split(" ");
453                        URL[] newUrls = new URL[cpe.length];
454                        for (int i = 0; i < cpe.length; i++) {
455                            // Don't need to add 'file:' with maven
456                            // surefire >= 2.4.2
457                            String newUrl = cpe[i].startsWith("file:") ? cpe[i] : "file:" + cpe[i];
458                            newUrls[i] = new URL(newUrl);
459                        }
460                        urls = newUrls;
461                    }
462                } finally {
463                    surefirebooterJar.close();
464                }
465            } catch (Exception e) {
466                // skip
467            }
468        }
469        // turn into files
470        List<String> files = new ArrayList<>(urls.length);
471        for (URL url : urls) {
472            files.add(url.toURI().getPath());
473        }
474        return files;
475    }
476
477    private static final String M2_REPO = "repository/";
478
479    protected static void addFireBug(FirefoxProfile profile) throws Exception {
480        // this is preventing from running tests in eclipse
481        // profile.addExtension(AbstractTest.class, "/firebug.xpi");
482
483        File xpi = null;
484        List<String> clf = getClassLoaderFiles();
485        for (String f : clf) {
486            if (f.endsWith("/" + FIREBUG_XPI)) {
487                xpi = new File(f);
488            }
489        }
490        if (xpi == null) {
491            String customM2Repo = System.getProperty("M2_REPO", M2_REPO).replaceAll("/$", "");
492            // try to guess the location in the M2 repo
493            for (String f : clf) {
494                if (f.contains(customM2Repo)) {
495                    String m2 = f.substring(0, f.indexOf(customM2Repo) + customM2Repo.length());
496                    xpi = new File(m2 + "/" + FIREBUG_M2 + "/" + FIREBUG_XPI);
497                    break;
498                }
499            }
500        }
501        if (xpi == null || !xpi.exists()) {
502            log.warn(FIREBUG_XPI + " not found in classloader or local M2 repository");
503            return;
504        }
505        profile.addExtension(xpi);
506
507        // avoid "first run" page
508        profile.setPreference("extensions.firebug.currentVersion", FIREBUG_VERSION);
509    }
510
511    protected static void removeFireBug() {
512        if (tmp_firebug_xpi != null) {
513            tmp_firebug_xpi.delete();
514            tmp_firebug_xpi.getParentFile().delete();
515        }
516    }
517
518    protected static Proxy startProxy() throws Exception {
519        if (Boolean.TRUE.equals(Boolean.valueOf(System.getProperty("useProxy", "false")))) {
520            proxyServer = new ProxyServer(PROXY_PORT);
521            proxyServer.start();
522            proxyServer.setCaptureHeaders(true);
523            // Block access to tracking sites
524            proxyServer.blacklistRequests("https?://www\\.nuxeo\\.com/embedded/wizard.*", 410);
525            proxyServer.blacklistRequests("https?://.*\\.mktoresp\\.com/.*", 410);
526            proxyServer.blacklistRequests(".*_mchId.*", 410);
527            proxyServer.blacklistRequests("https?://.*\\.google-analytics\\.com/.*", 410);
528            proxyServer.newHar("webdriver-test");
529            Proxy proxy = proxyServer.seleniumProxy();
530            return proxy;
531        } else {
532            return null;
533        }
534    }
535
536    protected static void stopProxy() throws Exception {
537        if (proxyServer != null) {
538            String target = System.getProperty(Environment.NUXEO_LOG_DIR);
539            File harFile;
540            if (target == null) {
541                harFile = new File(HAR_NAME);
542            } else {
543                harFile = new File(target, HAR_NAME);
544            }
545            proxyServer.getHar().writeTo(harFile);
546            proxyServer.stop();
547        }
548    }
549
550    public static <T> T get(String url, Class<T> pageClassToProxy) {
551        if (driver != null) {
552            new JavaScriptErrorCollector(driver).checkForErrors();
553        }
554        driver.get(url);
555        return asPage(pageClassToProxy);
556    }
557
558    /**
559     * Do not wait for page load. Do not handle error. Do not give explicit error in case of failure. This is a very raw
560     * get.
561     *
562     * @since 6.0
563     */
564    public static <T> T getWithoutErrorHandler(String url, Class<T> pageClassToProxy) throws IOException {
565        Command command = new Command(AbstractTest.driver.getSessionId(), DriverCommand.GET,
566                ImmutableMap.of("url", url));
567        AbstractTest.driver.getCommandExecutor().execute(command);
568        return asPage(pageClassToProxy);
569    }
570
571    public static WebDriver getPopup() {
572        String currentWindow = driver.getWindowHandle();
573        for (String popup : driver.getWindowHandles()) {
574            if (popup.equals(currentWindow)) {
575                continue;
576            }
577            return driver.switchTo().window(popup);
578        }
579        return null;
580    }
581
582    public static <T> T asPage(Class<T> pageClassToProxy) {
583        T page = instantiatePage(pageClassToProxy);
584        return fillElement(pageClassToProxy, page);
585    }
586
587    public static <T extends WebFragment> T getWebFragment(By by, Class<T> webFragmentClass) {
588        WebElement element = Locator.findElementWithTimeout(by);
589        return getWebFragment(element, webFragmentClass);
590    }
591
592    public static <T extends WebFragment> T getWebFragment(WebElement element, Class<T> webFragmentClass) {
593        T webFragment = instantiateWebFragment(element, webFragmentClass);
594        webFragment = fillElement(webFragmentClass, webFragment);
595        // fillElement somehow overwrite the 'element' field, reset it.
596        webFragment.setElement(element);
597        return webFragment;
598    }
599
600    /**
601     * Fills an instantiated page/form/widget attributes
602     *
603     * @since 5.7
604     */
605    public static <T> T fillElement(Class<T> pageClassToProxy, T page) {
606        PageFactory.initElements(new VariableElementLocatorFactory(driver, AJAX_TIMEOUT_SECONDS), page);
607        // check all required WebElements on the page and wait for their
608        // loading
609        final List<String> fieldNames = new ArrayList<>();
610        final List<WrapsElement> elements = new ArrayList<>();
611        for (Field field : pageClassToProxy.getDeclaredFields()) {
612            if (field.getAnnotation(Required.class) != null) {
613                try {
614                    field.setAccessible(true);
615                    fieldNames.add(field.getName());
616                    elements.add((WrapsElement) field.get(page));
617                } catch (Exception e) {
618                    throw new RuntimeException(e);
619                }
620            }
621        }
622
623        Wait<T> wait = new FluentWait<>(page).withTimeout(LOAD_TIMEOUT_SECONDS, TimeUnit.SECONDS).pollingEvery(
624                POLLING_FREQUENCY_MILLISECONDS, TimeUnit.MILLISECONDS);
625        try {
626            return wait.until(new Function<T, T>() {
627                @Override
628                public T apply(T aPage) {
629                    String notLoaded = anyElementNotLoaded(elements, fieldNames);
630                    if (notLoaded == null) {
631                        return aPage;
632                    } else {
633                        return null;
634                    }
635                }
636            });
637        } catch (TimeoutException e) {
638            throw new TimeoutException("not loaded: " + anyElementNotLoaded(elements, fieldNames), e);
639        }
640    }
641
642    protected static String anyElementNotLoaded(List<WrapsElement> proxies, List<String> fieldNames) {
643        for (int i = 0; i < proxies.size(); i++) {
644            WrapsElement proxy = proxies.get(i);
645            try {
646                // method implemented in LocatingElementHandler
647                proxy.getWrappedElement();
648            } catch (NoSuchElementException e) {
649                return fieldNames.get(i);
650            }
651        }
652        return null;
653    }
654
655    // private in PageFactory...
656    protected static <T> T instantiatePage(Class<T> pageClassToProxy) {
657        try {
658            try {
659                Constructor<T> constructor = pageClassToProxy.getConstructor(WebDriver.class);
660                return constructor.newInstance(driver);
661            } catch (NoSuchMethodException e) {
662                return pageClassToProxy.newInstance();
663            }
664        } catch (RuntimeException e) {
665            throw e;
666        } catch (Exception e) {
667            throw new RuntimeException(e);
668        }
669    }
670
671    protected static <T extends WebFragment> T instantiateWebFragment(WebElement element, Class<T> webFragmentClass) {
672        try {
673            try {
674                Constructor<T> constructor = webFragmentClass.getConstructor(WebDriver.class, WebElement.class);
675                return constructor.newInstance(driver, element);
676            } catch (NoSuchMethodException e) {
677                return webFragmentClass.newInstance();
678            }
679        } catch (RuntimeException e) {
680            throw e;
681        } catch (Exception e) {
682            throw new RuntimeException(e);
683        }
684    }
685
686    public LoginPage getLoginPage() {
687        return get(NUXEO_URL + "/logout", LoginPage.class);
688    }
689
690    public LoginPage logout() {
691        return getLoginPage();
692    }
693
694    /**
695     * navigate to a link text. wait until the link is available and click on it.
696     */
697    public <T extends AbstractPage> T nav(Class<T> pageClass, String linkText) {
698        WebElement link = Locator.findElementWithTimeout(By.linkText(linkText));
699        if (link == null) {
700            return null;
701        }
702        link.click();
703        return asPage(pageClass);
704    }
705
706    /**
707     * Navigate to a specified url
708     *
709     * @param urlString url
710     * @throws MalformedURLException
711     */
712    public void navToUrl(String urlString) throws MalformedURLException {
713        URL url = new URL(urlString);
714        driver.navigate().to(url);
715    }
716
717    /**
718     * Login as Administrator
719     *
720     * @return the Document base page (by default returned by CAP)
721     * @throws UserNotConnectedException
722     */
723    public DocumentBasePage login() throws UserNotConnectedException {
724        return login("Administrator", "Administrator");
725    }
726
727    public DocumentBasePage login(String username, String password) throws UserNotConnectedException {
728        DocumentBasePage documentBasePage = getLoginPage().login(username, password, DocumentBasePage.class);
729        documentBasePage.checkUserConnected(username);
730        return documentBasePage;
731    }
732
733    /**
734     * Login as default test user.
735     *
736     * @since 5.9.2
737     */
738    public DocumentBasePage loginAsTestUser() throws UserNotConnectedException {
739        return login(TEST_USERNAME, TEST_PASSWORD);
740    }
741
742    /**
743     * Login using an invalid credential.
744     *
745     * @param username
746     * @param password
747     */
748    public LoginPage loginInvalid(String username, String password) {
749        LoginPage loginPage = getLoginPage().login(username, password, LoginPage.class);
750        return loginPage;
751    }
752
753    /**
754     * Init the repository with a test Workspace form the {@code currentPage}.
755     *
756     * @param currentPage the current page
757     * @return the created Workspace page
758     * @throws Exception if initializing repository fails
759     */
760    protected DocumentBasePage initRepository(DocumentBasePage currentPage) throws Exception {
761        return createWorkspace(currentPage, "Test Workspace", "Test Workspace for my dear WebDriver.");
762    }
763
764    /**
765     * Cleans the repository (delete the test Workspace) from the {@code currentPage}.
766     *
767     * @param currentPage the current page
768     * @throws Exception if cleaning repository fails
769     */
770    protected void cleanRepository(DocumentBasePage currentPage) throws Exception {
771        deleteWorkspace(currentPage, "Test Workspace");
772    }
773
774    /**
775     * Creates a Workspace form the {@code currentPage}.
776     *
777     * @param currentPage the current page
778     * @param workspaceTitle the workspace title
779     * @param workspaceDescription the workspace description
780     * @return the created Workspace page
781     */
782    protected DocumentBasePage createWorkspace(DocumentBasePage currentPage, String workspaceTitle,
783            String workspaceDescription) {
784        // Go to Workspaces
785        DocumentBasePage workspacesPage = currentPage.getNavigationSubPage().goToDocument("Workspaces");
786        // Get Workspace creation form page
787        WorkspaceFormPage workspaceCreationFormPage = workspacesPage.getWorkspacesContentTab().getWorkspaceCreatePage();
788        // Create Workspace
789        DocumentBasePage workspacePage = workspaceCreationFormPage.createNewWorkspace(workspaceTitle,
790                workspaceDescription);
791        return workspacePage;
792    }
793
794    /**
795     * Deletes the Workspace with title {@code workspaceTitle} from the {@code currentPage}.
796     *
797     * @param currentPage the current page
798     * @param workspaceTitle the workspace title
799     */
800    protected void deleteWorkspace(DocumentBasePage currentPage, String workspaceTitle) {
801        // Go to Workspaces
802        DocumentBasePage workspacesPage = currentPage.getNavigationSubPage().goToDocument("Workspaces");
803        // Delete the Workspace
804        workspacesPage.getContentTab().removeDocument(workspaceTitle);
805    }
806
807    /**
808     * Creates a File form the {@code currentPage}.
809     *
810     * @param currentPage the current page
811     * @param fileTitle the file title
812     * @param fileDescription the file description
813     * @param uploadBlob true if a blob needs to be uploaded (temporary file created for this purpose)
814     * @param filePrefix the file prefix
815     * @param fileSuffix the file suffix
816     * @param fileContent the file content
817     * @return the created File page
818     * @throws IOException if temporary file creation fails
819     */
820    protected FileDocumentBasePage createFile(DocumentBasePage currentPage, String fileTitle, String fileDescription,
821            boolean uploadBlob, String filePrefix, String fileSuffix, String fileContent) throws IOException {
822        // Get File creation form page
823        FileCreationFormPage fileCreationFormPage = currentPage.getContentTab().getDocumentCreatePage("File",
824                FileCreationFormPage.class);
825        // Create File
826        FileDocumentBasePage filePage = fileCreationFormPage.createFileDocument(fileTitle, fileDescription, uploadBlob,
827                filePrefix, fileSuffix, fileDescription);
828        return filePage;
829    }
830
831    /**
832     * Creates a Collections container form the {@code currentPage}.
833     *
834     * @param currentPage the current page
835     * @param collectionsTitle the Collections container title
836     * @param fileDescription the collections description
837     * @return the created Collections page
838     */
839    protected DocumentBasePage createCollections(DocumentBasePage currentPage, String collectionsTitle,
840            String fileDescription) {
841        DublinCoreCreationDocumentFormPage dublinCoreDocumentFormPage = currentPage.getContentTab()
842                                                                                   .getDocumentCreatePage(
843                                                                                           "Collections",
844                                                                                           DublinCoreCreationDocumentFormPage.class);
845        // Create File
846        DocumentBasePage documentBasePage = dublinCoreDocumentFormPage.createDocument(collectionsTitle, fileDescription);
847        return documentBasePage;
848    }
849
850    /**
851     * Creates a Collection form the {@code currentPage}.
852     *
853     * @param currentPage the current page
854     * @param collectionsTitle the Collections container title
855     * @param fileDescription the collection description
856     * @return the created Collections page
857     */
858    protected CollectionContentTabSubPage createCollection(DocumentBasePage currentPage, String collectionsTitle,
859            String fileDescription) {
860        CollectionCreationFormPage collectionCreationFormPage = currentPage.getContentTab().getDocumentCreatePage(
861                "Collection", CollectionCreationFormPage.class);
862        // Create File
863        CollectionContentTabSubPage documentBasePage = collectionCreationFormPage.createDocument(collectionsTitle,
864                fileDescription);
865        return documentBasePage;
866    }
867
868    /**
869     * Creates a temporary file and returns its absolute path.
870     *
871     * @param filePrefix the file prefix
872     * @param fileSuffix the file suffix
873     * @param fileContent the file content
874     * @return the temporary file to upload path
875     * @throws IOException if temporary file creation fails
876     * @since 5.9.3
877     */
878    public static String getTmpFileToUploadPath(String filePrefix, String fileSuffix, String fileContent)
879            throws IOException {
880        // Create tmp file, deleted on exit
881        File tmpFile = Framework.createTempFile(filePrefix, fileSuffix);
882        tmpFile.deleteOnExit();
883        FileUtils.writeFile(tmpFile, fileContent);
884        assertTrue(tmpFile.exists());
885
886        // Check file URI protocol
887        assertEquals("file", tmpFile.toURI().toURL().getProtocol());
888
889        // Return file absolute path
890        return tmpFile.getAbsolutePath();
891    }
892
893    /**
894     * Get the current document id stored in the javascript ctx.currentDocument variable of the current page.
895     *
896     * @return the current document id
897     * @since 5.7
898     */
899    protected String getCurrentDocumentId() {
900        return (String) ((JavascriptExecutor) driver).executeScript(String.format("return ctx.currentDocument;"));
901    }
902
903    /**
904     * Creates a Note form the {@code currentPage}.
905     *
906     * @param currentPage the current page
907     * @param noteTitle the note title
908     * @param noteDescription the note description
909     * @param defineNote true if the content of the note needs to be defined
910     * @param noteContent the content of the note
911     * @return the created note page.
912     * @throws IOException
913     * @since 5.9.4
914     */
915    protected NoteDocumentBasePage createNote(DocumentBasePage currentPage, String noteTitle, String noteDescription,
916            boolean defineNote, String noteContent) throws IOException {
917        // Get the Note creation form
918        NoteCreationFormPage noteCreationPage = currentPage.getContentTab().getDocumentCreatePage("Note",
919                NoteCreationFormPage.class);
920        // Create a Note
921        NoteDocumentBasePage notePage = noteCreationPage.createNoteDocument(noteTitle, noteDescription, defineNote,
922                noteContent);
923        return notePage;
924    }
925
926    /**
927     * Do not run on windows with Firefox 26 (NXP-17848).
928     *
929     * @since 7.10
930     */
931    protected void doNotRunOnWindowsWithFF26() {
932        String browser, browserVersion = null;
933        try {
934            browser = driver.getCapabilities().getBrowserName();
935            browserVersion = driver.getCapabilities().getVersion();
936            Float iBrowserVersion = Float.parseFloat(browserVersion);
937            assumeFalse(SystemUtils.IS_OS_WINDOWS && browser.equals("firefox") && iBrowserVersion <= 28.0);
938        } catch (NumberFormatException e) {
939            log.warn("Could not parse browser version: " + browserVersion);
940        }
941    }
942
943}