Merge "js/core: Add WebDriver library for UI Testing"
diff --git a/test/ui/README.md b/test/ui/README.md
new file mode 100644
index 0000000..12d70c9
--- /dev/null
+++ b/test/ui/README.md
@@ -0,0 +1,57 @@
+# Vanadium JavaScript UI Test
+
+## About
+
+The UI tests use the maven architecture. This package manager uses
+the build information from pom.xml to compile a WebDriver program.
+
+The UI library in this repository defines VanadiumUITestBase, which
+uses WebDriver to run Chrome and install the Vanadium Extension.
+
+It also defines HTMLReporter, which is used to grab screenshots and
+generate an HTML report of the WebDriver test.
+
+## Usage
+
+The expected use of the library is to subclass VanadiumUITestBase and
+write a JUnit test function. The HTMLReporter should be started at the
+beginning of the test and generated upon test completion.
+
+A distinct pom.xml should be written for the tests, and it should
+include a dependency to this library.
+
+In order to install and use the Vanadium Extension, it is important to
+have the credentials of a Google account. It is not recommended that
+these credentials be visible to the public. Maven properties and
+environment variables can be used to keep them private.
+
+See release.projects.browser and www for example usages.
+
+## Display Port
+
+To run a test, one will generally set the DISPLAY environment variable
+to :0 or run Xvfb at a specific display port. Set this value in the
+test project's pom.xml file.
+
+Note that when running with :0, the test will run with the current
+display. Mouse and key actions performed during this period will interfere with the WebDriver test.
+
+While :0 is good for watching tests as they run, the screenshots taken
+are not guaranteed to be accurate.
+
+## Invoking maven
+
+At least 4 flags are required when running the maven test:
+- chromeDriverBin: Location of the Chrome WebDriver binary
+- htmlReportsRelativePath: Directory to output an HTML report (relative to $WORKSPACE)
+- googleBotUsername: Username for a Google account
+- googleBotPassword: Password for that user
+
+Here is an example invocation:
+WORKSPACE=$WORKSPACE mvn test \
+ -f=$LOCATION_OF_POM_XML \
+ -Dtest=$UI_TEST_NAME \
+ -DchromeDriverBin=$CHROME_WEBDRIVER \
+ -DhtmlReportsRelativePath=$HTML_REPORT_RELATIVE_PATH \
+ -DgoogleBotUsername=$BOT_USERNAME \
+ -DgoogleBotPassword=$BOT_PASSWORD
diff --git a/test/ui/pom.xml b/test/ui/pom.xml
new file mode 100644
index 0000000..f233058
--- /dev/null
+++ b/test/ui/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+
+ <name>Vanadium Webdriver</name>
+ <groupId>io.v.webdriver</groupId>
+ <artifactId>vanadium_webdriver</artifactId>
+ <version>0.1</version>
+
+ <dependencies>
+ <dependency>
+ <groupId>commons-io</groupId>
+ <artifactId>commons-io</artifactId>
+ <version>2.4</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <version>4.12</version>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.freemarker</groupId>
+ <artifactId>freemarker</artifactId>
+ <version>2.3.22</version>
+ </dependency>
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-chrome-driver</artifactId>
+ <version>2.45.0</version>
+ </dependency>
+ <dependency>
+ <groupId>org.seleniumhq.selenium</groupId>
+ <artifactId>selenium-support</artifactId>
+ <version>2.45.0</version>
+ </dependency>
+ </dependencies>
+
+ <build>
+ <plugins>
+ <plugin>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <version>3.2</version>
+ <configuration>
+ <source>1.7</source>
+ <target>1.7</target>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/test/ui/src/test/java/io/v/webdriver/RobotHelper.java b/test/ui/src/test/java/io/v/webdriver/RobotHelper.java
new file mode 100644
index 0000000..bca09c4
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/RobotHelper.java
@@ -0,0 +1,81 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver;
+
+import java.awt.AWTException;
+import java.awt.Robot;
+import java.awt.Toolkit;
+import java.awt.datatransfer.Clipboard;
+import java.awt.datatransfer.StringSelection;
+import java.awt.event.KeyEvent;
+
+/**
+ * A helper class for Robot related functions.
+ * <p>
+ * Most of the automation should be done through WebDriver APIs, but some elements (e.g. Chrome's
+ * native Sign-in page or the confirmation dialog after an extension is installed) cannot be
+ * accessed and controlled by WebDriver. In those cases, we use Robot to send key strokes, mouse
+ * clicks, etc.
+ *
+ * @author jingjin@google.com
+ */
+public class RobotHelper {
+ /**
+ * This is a singleton instance.
+ */
+ private static RobotHelper instance;
+
+ private final Robot robot;
+
+ public static RobotHelper sharedInstance() throws AWTException {
+ if (instance == null) {
+ instance = new RobotHelper();
+ }
+ return instance;
+ }
+
+ private RobotHelper() throws AWTException {
+ robot = new Robot();
+ robot.setAutoDelay(200);
+ robot.setAutoWaitForIdle(true);
+ }
+
+ /**
+ * Enters the given text to the focused element.
+ *
+ * <p>To make the process easier, we copy the given text to system clipboard and paste it into the
+ * target element. Otherwise, we have to call "keyPress" and "keyRelease" on each key stroke.
+ *
+ * @param text the text to enter.
+ */
+ public void enterText(String text) {
+ // Copy text to clipboard.
+ Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
+ StringSelection stringSelection = new StringSelection(text);
+ clipboard.setContents(stringSelection, stringSelection);
+
+ // Send Ctrl+V to paste.
+ robot.keyPress(KeyEvent.VK_CONTROL);
+ robot.keyPress(KeyEvent.VK_V);
+ robot.keyRelease(KeyEvent.VK_V);
+ robot.keyRelease(KeyEvent.VK_CONTROL);
+ }
+
+ /**
+ * Presses TAB.
+ */
+ public void tab() {
+ robot.keyPress(KeyEvent.VK_TAB);
+ robot.keyRelease(KeyEvent.VK_TAB);
+ }
+
+ /**
+ * Presses Enter.
+ */
+ public void enter() {
+ robot.keyPress(KeyEvent.VK_ENTER);
+ robot.keyRelease(KeyEvent.VK_ENTER);
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/TestFailureWatcher.java b/test/ui/src/test/java/io/v/webdriver/TestFailureWatcher.java
new file mode 100644
index 0000000..68dcd47
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/TestFailureWatcher.java
@@ -0,0 +1,49 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver;
+
+import org.junit.rules.TestWatcher;
+import org.junit.runner.Description;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.chrome.ChromeDriverService;
+
+import io.v.webdriver.htmlreport.HTMLReportData;
+import io.v.webdriver.htmlreport.HTMLReporter;
+
+/**
+ * A class for performing actions when any test case fails.
+ *
+ * @author jingjin@google.com
+ */
+public class TestFailureWatcher extends TestWatcher {
+ private VanadiumUITestBase uiTest;
+ private WebDriver driver;
+ private ChromeDriverService service;
+
+ public void setup(VanadiumUITestBase uiTest, WebDriver driver, ChromeDriverService service) {
+ this.uiTest = uiTest;
+ this.driver = driver;
+ this.service = service;
+ }
+
+ @Override
+ protected void failed(Throwable e, Description description) {
+ // Take a screenshot for the current screen and write the html report.
+ HTMLReportData data = uiTest.getCurrentHTMLReportData();
+ Util.takeScreenshot("test-failed.png", "Test Failed", data);
+ HTMLReporter reporter = new HTMLReporter(data);
+ try {
+ reporter.generateReport();
+ } catch (Exception e1) {
+ System.err.println("Failed to write html report.\n" + Util.getStackTrace(e1));
+ }
+ }
+
+ @Override
+ protected void finished(Description description) {
+ driver.quit();
+ service.stop();
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/Util.java b/test/ui/src/test/java/io/v/webdriver/Util.java
new file mode 100644
index 0000000..46e81a0
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/Util.java
@@ -0,0 +1,85 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver;
+
+import io.v.webdriver.htmlreport.HTMLReportData;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+/**
+ * Utility functions.
+ *
+ * @author jingjin@google.com
+ */
+public class Util {
+
+ /**
+ * Sleeps for given milliseconds.
+ *
+ * <p>This is mainly used for waiting between Robot related operations. For WebDriver, we should
+ * use "wait.until".
+ *
+ * @param ms number of milliseconds to sleep.
+ */
+ public static void sleep(int ms) {
+ try {
+ Thread.sleep(ms);
+ } catch (InterruptedException e) {
+ e.printStackTrace();
+ }
+ }
+
+ /**
+ * Converts the given exception's stack trace to a string.
+ *
+ * @param e the exception to convert to string.
+ */
+ public static String getStackTrace(Exception e) {
+ StringWriter sw = new StringWriter();
+ PrintWriter pw = new PrintWriter(sw);
+ e.printStackTrace(pw);
+ return sw.toString();
+ }
+
+ /**
+ * Replaces illegal character in the given file name.
+ *
+ * @param filename the original file name.
+ */
+ public static String getSafeFilename(String filename) {
+ return filename.replaceAll("[^a-zA-Z0-9.-]", "_").toLowerCase();
+ }
+
+ /**
+ * Takes a screenshot.
+ *
+ * <p>It uses the "import" command, saves it to the given file, and records it in the given
+ * htmlReportData.
+ *
+ * @param fileName the file to save the screenshot to.
+ * @param caption the caption of the screenshot.
+ * @param htmlReportData the data model to add screenshot data to.
+ */
+ public static void takeScreenshot(String fileName, String caption,
+ HTMLReportData htmlReportData) {
+ String fullFileName =
+ String.format("%s-%s", getSafeFilename(htmlReportData.getTestName()), fileName);
+ Runtime rt = Runtime.getRuntime();
+ try {
+ String imagePath = String.format("%s/%s", htmlReportData.getReportDir(), fullFileName);
+ Process pr = rt.exec(
+ String.format("import -window root -crop 1004x748+10+10 -resize 800 %s", imagePath));
+ int retValue = pr.waitFor();
+ if (retValue != 0) {
+ System.err.println(String.format("Failed to capture screenshot: %s", imagePath));
+ }
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+
+ htmlReportData.addScreenshotData(fullFileName, caption);
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/VanadiumUITestBase.java b/test/ui/src/test/java/io/v/webdriver/VanadiumUITestBase.java
new file mode 100644
index 0000000..129bf3e
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/VanadiumUITestBase.java
@@ -0,0 +1,147 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver;
+
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.chrome.ChromeDriver;
+import org.openqa.selenium.chrome.ChromeDriverService;
+
+import io.v.webdriver.htmlreport.HTMLReportData;
+import io.v.webdriver.commonpages.ChromeSignInPage;
+import io.v.webdriver.commonpages.ExtensionInstallationPage;
+import io.v.webdriver.commonpages.ExtensionOptionPage;
+
+import java.io.File;
+import java.io.IOException;
+
+/**
+ * The base class for all Vanadium UI tests.
+ *
+ * @author jingjin@google.com
+ */
+public class VanadiumUITestBase {
+ /**
+ * System property name for Chrome driver binary. This will be set from the mvn command line.
+ */
+ private static final String PROPERTY_CHROME_DRIVER_BIN = "chromeDriverBin";
+
+ /**
+ * System property name for html report path relative to Jenkins workspace dir. This will be set
+ * from the mvn command line.
+ */
+ private static final String PROPERTY_HTML_REPORTS_RELATIVE_PATH = "htmlReportsRelativePath";
+
+ /**
+ * System property name for the bot username and bot password. These will be set from the mvn command line.
+ */
+ private static final String PROPERTY_GOOGLE_BOT_USERNAME = "googleBotUsername";
+ private static final String PROPERTY_GOOGLE_BOT_PASSWORD = "googleBotPassword";
+
+ /**
+ * The base dir to store html reports
+ */
+ protected final String htmlReportsDir;
+
+ /**
+ * A variable to keep track of the current htmlReportData.
+ */
+ protected HTMLReportData curHTMLReportData;
+
+ /**
+ * The username and password of the bot account used to sign in to Google/Chrome.
+ */
+ protected String botUsername;
+ protected String botPassword;
+
+ /**
+ * Vanadium extension installation url.
+ */
+ private static final String URL_EXTENSION = "https://chrome.google.com/webstore/detail/"
+ + "vanadium-extension/jcaelnibllfoobpedofhlaobfcoknpap";
+
+ protected ChromeDriverService service;
+
+ protected WebDriver driver;
+
+ public VanadiumUITestBase() {
+ htmlReportsDir = createReportsDir();
+ }
+
+ /**
+ * This "Rule" is used to catch failed test cases so we can act on it.
+ */
+ @Rule
+ public TestFailureWatcher testFailureWatcher = new TestFailureWatcher();
+
+ /**
+ * Basic WebDriver and other setup tasks. This will be executed before each test case.
+ */
+ @Before
+ public void setup() throws IOException {
+ String chromeDriverBin = System.getProperty(PROPERTY_CHROME_DRIVER_BIN);
+ botUsername = System.getProperty(PROPERTY_GOOGLE_BOT_USERNAME);
+ botPassword = System.getProperty(PROPERTY_GOOGLE_BOT_PASSWORD);
+ System.out.println("ChromeDriver binary: " + chromeDriverBin);
+ service = new ChromeDriverService.Builder().usingDriverExecutable(new File(chromeDriverBin))
+ .usingAnyFreePort().build();
+ service.stop();
+ service.start();
+ driver = new ChromeDriver(service);
+ driver.manage().window().maximize();
+
+ testFailureWatcher.setup(this, driver, service);
+ }
+
+ public HTMLReportData getCurrentHTMLReportData() {
+ return curHTMLReportData;
+ }
+
+ private String createReportsDir() {
+ String workspaceRoot = System.getenv("WORKSPACE");
+ if (workspaceRoot == null) {
+ workspaceRoot = System.getenv("HOME");
+ }
+ if (workspaceRoot == null) {
+ return "";
+ }
+ String reportsDir = String.format("%s/%s", workspaceRoot,
+ System.getProperty(PROPERTY_HTML_REPORTS_RELATIVE_PATH, "htmlReports"));
+ File reportsFile = new File(reportsDir);
+ if (!reportsFile.exists()) {
+ if (reportsFile.mkdirs()) {
+ System.out.println(String.format("Reports dir '%s' creatred", reportsDir));
+ } else {
+ throw new RuntimeException(String.format("Failed to create reports dir '%s'", reportsDir));
+ }
+ }
+ return reportsDir;
+ }
+
+ /**
+ * UI tests will commonly need to install the extension.
+ * The process involves signing into Chrome, installing the extension, and
+ * verifying that it was installed successfully.
+ */
+ protected void installExtension(HTMLReportData reportData) throws Exception {
+ // Sign into Chrome.
+ ChromeSignInPage chromeSignInPage = new ChromeSignInPage(driver, reportData);
+ chromeSignInPage.go();
+ chromeSignInPage.signIn(botUsername, botPassword);
+
+ // Install Vanadium extension.
+ ExtensionInstallationPage extensionInstallationPage =
+ new ExtensionInstallationPage(driver, URL_EXTENSION, reportData);
+ extensionInstallationPage.go();
+ extensionInstallationPage.login(botPassword);
+ extensionInstallationPage.install();
+
+ // Check Vanadium extension option page.
+ ExtensionOptionPage extensionOptionPage = new ExtensionOptionPage(driver, reportData);
+ extensionOptionPage.go();
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/commonpages/ChromeSignInPage.java b/test/ui/src/test/java/io/v/webdriver/commonpages/ChromeSignInPage.java
new file mode 100644
index 0000000..6b51909
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/commonpages/ChromeSignInPage.java
@@ -0,0 +1,55 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver.commonpages;
+
+import org.openqa.selenium.WebDriver;
+
+import io.v.webdriver.RobotHelper;
+import io.v.webdriver.Util;
+import io.v.webdriver.htmlreport.HTMLReportData;
+
+import java.awt.AWTException;
+
+/**
+ * Chrome's native Sign-In page.
+ *
+ * @author jingjin@google.com
+ */
+public class ChromeSignInPage extends PageBase {
+ private static final String URL_CHROME_SIGNIN = "chrome://chrome-signin";
+
+ public ChromeSignInPage(WebDriver driver, HTMLReportData htmlReportData) {
+ super(driver, URL_CHROME_SIGNIN, htmlReportData);
+ }
+
+ @Override
+ public void go() {
+ super.goWithoutTakingScreenshot();
+
+ // For this page we need to wait a little bit before entering username/password.
+ Util.sleep(2000);
+ takeScreenshotUsingPageName();
+ }
+
+ public void signIn(String username, String password) throws AWTException {
+ // Sign in.
+ log("Sign in");
+ RobotHelper robotHelper = RobotHelper.sharedInstance();
+ robotHelper.enterText(username);
+ robotHelper.tab();
+ robotHelper.enter();
+ robotHelper.enterText(password);
+ robotHelper.tab();
+ Util.takeScreenshot("before-chrome-signin.png", "Before Signing In Chrome", htmlReportData);
+ robotHelper.enter();
+ Util.sleep(2000);
+
+ // Dismiss a "Sign-in successful" popup.
+ // This popup is not accessible by WebDriver.
+ log("Dismiss 'Sign-in successful' prompt");
+ robotHelper.enter();
+ Util.takeScreenshot("after-chrome-signin.png", "After Signing In Chrome", htmlReportData);
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/commonpages/ExtensionInstallationPage.java b/test/ui/src/test/java/io/v/webdriver/commonpages/ExtensionInstallationPage.java
new file mode 100644
index 0000000..8a81494
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/commonpages/ExtensionInstallationPage.java
@@ -0,0 +1,66 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver.commonpages;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.TimeoutException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+
+import io.v.webdriver.RobotHelper;
+import io.v.webdriver.Util;
+import io.v.webdriver.htmlreport.HTMLReportData;
+
+import java.awt.AWTException;
+import java.io.UnsupportedEncodingException;
+import java.net.URLEncoder;
+
+/**
+ * Vanadium extension installation page.
+ *
+ * @author jingjin@google.com
+ */
+public class ExtensionInstallationPage extends PageBase {
+ private static final String GOOGLE_LOGIN_URL = "https://accounts.google.com/ServiceLogin";
+
+ public ExtensionInstallationPage(WebDriver driver, String extentionUrl,
+ HTMLReportData htmlReportData) throws UnsupportedEncodingException {
+ super(driver, GOOGLE_LOGIN_URL + "?continue=" + URLEncoder.encode(extentionUrl, "UTF-8"),
+ htmlReportData);
+ }
+
+ public void login(String password) throws TimeoutException {
+ log("Log in using Google account");
+ WebElement btnSignin = wait.until(ExpectedConditions.elementToBeClickable(By.id("signIn")));
+ WebElement passwdTextField = driver.findElement(By.id("Passwd"));
+ passwdTextField.sendKeys(password);
+ Util.takeScreenshot("google-account-signin.png", "Google Account Sign-In", htmlReportData);
+ btnSignin.click();
+ }
+
+ public void install() throws AWTException, TimeoutException {
+ log("Install extension");
+ WebElement btnAddToChrome = wait.until(
+ ExpectedConditions.elementToBeClickable(By.cssSelector("div[aria-label='Add to Chrome']")));
+ Util.takeScreenshot("before-install-extension.png", "Before Installing Extension",
+ htmlReportData);
+ btnAddToChrome.click();
+ Util.sleep(2000);
+
+ // Click on the "Add" button in the extension installation popup.
+ // This popup is not accessible by WebDriver.
+ log("Confirm adding extension to Chrome");
+ RobotHelper robotHelper = RobotHelper.sharedInstance();
+ robotHelper.tab();
+ robotHelper.enter();
+ wait.until(ExpectedConditions.elementToBeClickable(
+ By.cssSelector("div[aria-label='Added to Chrome']")));
+ // It might take some time for the extension to actually be installed.
+ Util.sleep(3000);
+ Util.takeScreenshot("after-install-extension.png", "After Installing Extension",
+ htmlReportData);
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/commonpages/ExtensionOptionPage.java b/test/ui/src/test/java/io/v/webdriver/commonpages/ExtensionOptionPage.java
new file mode 100644
index 0000000..30885c6
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/commonpages/ExtensionOptionPage.java
@@ -0,0 +1,39 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver.commonpages;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+
+import io.v.webdriver.Util;
+import io.v.webdriver.htmlreport.HTMLReportData;
+
+/**
+ * Vanadium extension's option page.
+ *
+ * @author jingjin@google.com
+ */
+public class ExtensionOptionPage extends PageBase {
+ private static final String URL_OPTIONS =
+ "chrome-extension://jcaelnibllfoobpedofhlaobfcoknpap/html/options.html";
+
+ public ExtensionOptionPage(WebDriver driver, HTMLReportData htmlReportData) {
+ super(driver, URL_OPTIONS, htmlReportData);
+ }
+
+ @Override
+ public void go() {
+ super.goWithoutTakingScreenshot();
+
+ // For this page we need to do an extra refresh once to make the page appear.
+ driver.navigate().refresh();
+ Util.sleep(1000);
+ takeScreenshotUsingPageName();
+
+ // Verify the "Reload plugin" link is present.
+ wait.until(ExpectedConditions.presenceOfElementLocated(By.linkText("Reload plugin")));
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/commonpages/OAuthLoginPage.java b/test/ui/src/test/java/io/v/webdriver/commonpages/OAuthLoginPage.java
new file mode 100644
index 0000000..bb5bc95
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/commonpages/OAuthLoginPage.java
@@ -0,0 +1,39 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver.commonpages;
+
+import org.openqa.selenium.By;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+
+import io.v.webdriver.Util;
+import io.v.webdriver.htmlreport.HTMLReportData;
+
+/**
+ * Google OAuth login page.
+ *
+ * @author jingjin@google.com
+ */
+public class OAuthLoginPage extends PageBase {
+ public OAuthLoginPage(WebDriver driver, HTMLReportData htmlReportData) {
+ // This page is often triggered by some other page.
+ super(driver, "", htmlReportData);
+ }
+
+ public void login() {
+ log("OAuth login");
+ Util.takeScreenshot("oauth-login.png", "OAuth Login", htmlReportData);
+ WebElement btnSignInGoogle = wait.until(ExpectedConditions.elementToBeClickable(
+ By.xpath("//button[contains(text(), 'Sign in with a Google Account')]")));
+ btnSignInGoogle.click();
+
+ log("Accept access info");
+ Util.takeScreenshot("accept-access-info.png", "Accepting Access Info", htmlReportData);
+ WebElement btnAccept =
+ wait.until(ExpectedConditions.elementToBeClickable(By.id("submit_approve_access")));
+ btnAccept.click();
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/commonpages/PageBase.java b/test/ui/src/test/java/io/v/webdriver/commonpages/PageBase.java
new file mode 100644
index 0000000..bd30fd2
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/commonpages/PageBase.java
@@ -0,0 +1,70 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver.commonpages;
+
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import io.v.webdriver.Util;
+import io.v.webdriver.htmlreport.HTMLReportData;
+
+/**
+ * Base page class.
+ *
+ * @author jingjin@google.com
+ */
+public class PageBase {
+ /**
+ * Default timeout in seconds for WebDriver to wait for a condition.
+ */
+ public static final int TIMEOUT_SECONDS = 10;
+
+ /**
+ * Page URL.
+ */
+ private String url;
+
+ /**
+ * Informative page name used in logging.
+ */
+ private final String pageName;
+
+ protected final WebDriver driver;
+
+ protected final WebDriverWait wait;
+
+ protected final HTMLReportData htmlReportData;
+
+ public PageBase(WebDriver driver, String url, HTMLReportData htmlReportData) {
+ this.driver = driver;
+ this.url = url;
+ this.htmlReportData = htmlReportData;
+ this.pageName = this.getClass().getSimpleName();
+
+ wait = new WebDriverWait(driver, TIMEOUT_SECONDS);
+ }
+
+ public void go() {
+ goWithoutTakingScreenshot();
+ takeScreenshotUsingPageName();
+ }
+
+ public void goWithoutTakingScreenshot() {
+ log("Go to " + url);
+ driver.get(url);
+ }
+
+ protected void log(String msg) {
+ System.out.println(String.format("[%s]: %s", pageName, msg));
+ }
+
+ /**
+ * Takes a screenshot for the current page and names it using pageName.
+ */
+ protected void takeScreenshotUsingPageName() {
+ Util.takeScreenshot(String.format("%s.png", Util.getSafeFilename(pageName)), pageName,
+ htmlReportData);
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/htmlreport/HTMLReportData.java b/test/ui/src/test/java/io/v/webdriver/htmlreport/HTMLReportData.java
new file mode 100644
index 0000000..998258d
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/htmlreport/HTMLReportData.java
@@ -0,0 +1,51 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver.htmlreport;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Data model class for used in HTML report generation.
+ *
+ * @author jingjin@google.com
+ */
+public class HTMLReportData {
+ /**
+ * A list of screenshots in the report.
+ */
+ private final List<ScreenshotData> screenshots = new ArrayList<ScreenshotData>();
+
+ /**
+ * The report's base dir.
+ */
+ private final String reportDir;
+
+ /**
+ * The name of the test associated with the data.
+ */
+ private final String testName;
+
+ public HTMLReportData(String testName, String reportDir) {
+ this.testName = testName;
+ this.reportDir = reportDir;
+ }
+
+ public String getReportDir() {
+ return reportDir;
+ }
+
+ public String getTestName() {
+ return testName;
+ }
+
+ public void addScreenshotData(String fileName, String caption) {
+ screenshots.add(new ScreenshotData(fileName, caption));
+ }
+
+ public List<ScreenshotData> getScreenshots() {
+ return screenshots;
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/htmlreport/HTMLReporter.java b/test/ui/src/test/java/io/v/webdriver/htmlreport/HTMLReporter.java
new file mode 100644
index 0000000..9bb9497
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/htmlreport/HTMLReporter.java
@@ -0,0 +1,63 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver.htmlreport;
+
+import freemarker.template.Configuration;
+import freemarker.template.Template;
+import freemarker.template.TemplateExceptionHandler;
+
+import io.v.webdriver.Util;
+
+import java.io.File;
+import java.io.FileWriter;
+import java.io.Writer;
+import java.util.HashMap;
+import java.util.Locale;
+import java.util.Map;
+
+/**
+ * Generate HTML report that has screenshots of all test steps.
+ *
+ * @author jingjin@google.com
+ */
+public class HTMLReporter {
+ private final String reportFileName;
+ private final HTMLReportData data;
+
+ public HTMLReporter(HTMLReportData data) {
+ this.reportFileName = String.format("%s.html", Util.getSafeFilename(data.getTestName()));
+ this.data = data;
+ }
+
+ public void generateReport() throws Exception {
+ // Setup and load template.
+ Configuration cfg = new Configuration(Configuration.VERSION_2_3_22);
+ cfg.setClassForTemplateLoading(this.getClass(), "");
+ cfg.setDefaultEncoding("UTF-8");
+ cfg.setLocale(Locale.US);
+ cfg.setTemplateExceptionHandler(TemplateExceptionHandler.RETHROW_HANDLER);
+
+ // Figure out where the report.ftl file is.
+ File f = new File(System.getenv("V23_ROOT") + "/release/javascript/core/test/ui/src/test/java/io/v/webdriver/htmlreport");
+ String path = f.getAbsolutePath();
+ cfg.setDirectoryForTemplateLoading(new File(path));
+
+ // This template formats the HTML report.
+ Template template = cfg.getTemplate("report.ftl");
+
+ // Prepare data.
+ Map<String, Object> input = new HashMap<String, Object>();
+ input.put("data", data);
+
+ // Generate output.
+ String reportPath = String.format("%s/%s", data.getReportDir(), reportFileName);
+ Writer fileWriter = new FileWriter(new File(reportPath));
+ try {
+ template.process(input, fileWriter);
+ } finally {
+ fileWriter.close();
+ }
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/htmlreport/ScreenshotData.java b/test/ui/src/test/java/io/v/webdriver/htmlreport/ScreenshotData.java
new file mode 100644
index 0000000..db7c77f
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/htmlreport/ScreenshotData.java
@@ -0,0 +1,35 @@
+// Copyright 2015 The Vanadium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package io.v.webdriver.htmlreport;
+
+/**
+ * Data model for a screenshot image.
+ *
+ * @author jingjin@google.com
+ */
+public class ScreenshotData {
+ /**
+ * The base file name (not including the full path) of the screenshot image file.
+ */
+ private final String fileName;
+
+ /**
+ * The caption of the screenshot.
+ */
+ private final String caption;
+
+ public ScreenshotData(String fileName, String caption) {
+ this.fileName = fileName;
+ this.caption = caption;
+ }
+
+ public String getFileName() {
+ return fileName;
+ }
+
+ public String getCaption() {
+ return caption;
+ }
+}
diff --git a/test/ui/src/test/java/io/v/webdriver/htmlreport/report.ftl b/test/ui/src/test/java/io/v/webdriver/htmlreport/report.ftl
new file mode 100644
index 0000000..166a919
--- /dev/null
+++ b/test/ui/src/test/java/io/v/webdriver/htmlreport/report.ftl
@@ -0,0 +1,69 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>${data.testName}</title>
+ <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
+ <link href="https://cdnjs.cloudflare.com/ajax/libs/fotorama/4.6.3/fotorama.css" rel="stylesheet">
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/fotorama/4.6.3/fotorama.js"></script>
+ <script>
+ // Set "Test Failed" caption to red background.
+ $(function() {
+ $('.fotorama').on('fotorama:show', function(e, fotorama, extra) {
+ $(".fotorama__caption__wrap").each(function(index) {
+ if ($(this).text() == "Test Failed") {
+ $(this).addClass('failed-test');
+ }
+ });
+ });
+ });
+ </script>
+ <style>
+ body {
+ font-family: Helvetica, Arial, sans-serif;
+ }
+
+ h1 {
+ font-size: 20px;
+ color: #0097A7;
+ }
+
+ .failed-test {
+ background-color: rgba(200, 0, 0, 0.9) !important;
+ color: white !important;
+ }
+
+ .fotorama__caption {
+ left: auto !important;
+ right: 0px;
+ top: 0px;
+ }
+
+ .fotorama__caption__wrap {
+ border: solid 1px #AAA;
+ font-weight: bold;
+ }
+
+ .fotorama__thumb-border {
+ border-color: #0097A7 !important;
+ }
+
+ .fotorama__wrap {
+ margin: 0 auto;
+ }
+ </style>
+ </head>
+ <body>
+ <h1>${data.testName}</h1>
+ <div class="fotorama"
+ data-width="800px"
+ data-ratio="800/600"
+ data-nav="thumbs"
+ data-keyboard="true"
+ data-thumbwidth=60
+ data-thumbheight=45>
+ <#list data.screenshots as screenshot>
+ <img class="failed-test" src="${screenshot.fileName}" data-caption="${screenshot.caption}"/>
+ </#list>
+ </div>
+ </body>
+</html>
\ No newline at end of file