chat: Add a simple chat web test

The test
- does the basic stuff (sign into chrome, get the extension...)
- visits the page and checks that the member is recognized (serving)
- sends a generic message "Vanadium Bot says Hello!" to everyone
  and checks that it can be retrieved.

MultiPart: 1/2
Change-Id: I8d1b64e4e2255fc5e4b92590c306d94a4dcddfac
diff --git a/.gitignore b/.gitignore
index 6ffb93f..8f65158 100644
--- a/.gitignore
+++ b/.gitignore
@@ -2,7 +2,8 @@
 /clients/shell/credentials
 /clients/shell/go/bin
 /clients/shell/go/pkg
+/clients/web/test/ui/target
+/htmlReports
 /node_modules
 /tmp
 /.v23
-
diff --git a/Makefile b/Makefile
index e55e677..18281a5 100644
--- a/Makefile
+++ b/Makefile
@@ -190,11 +190,55 @@
 	$(MAKE) -C $(VANADIUM_JS)/extension build-test
 	prova clients/web/test/test-*.js -f $(APP_FRAME) $(PROVA_OPTS) $(BROWSER_OPTS) $(BROWSER_OUTPUT_LOCAL)
 
+
+# Run UI tests for the chat web client.
+# These tests do not normally need to be run locally, but they can be if you
+# want to verify that the a specific version of chat is compatible with a
+# local (or live) version of the Vanadium extension.
+#
+# This test takes additional environment variables (typically temporary)
+# - GOOGLE_BOT_USERNAME and GOOGLE_BOT_PASSWORD (To sign into Google/Chrome)
+# - CHROME_WEBDRIVER (The path to the chrome web driver)
+# - WORKSPACE (optional, defaults to $V23_ROOT/release/projects/chat)
+# - TEST_URL (optional, defaults to https://chat.staging.v.io)
+# - NO_XVFB (optional, defaults to using Xvfb. Set to true to watch the test.)
+# - BUILD_EXTENSION (optional, defaults to using the live one. Set to true to
+#                    use a local build of the Vanadium extension.)
+#
+# In addition, this test requires that maven, Xvfb, and xvfb-run be installed.
+# The HTML report will be in $V23_ROOT/release/projects/chat/htmlReports
+WORKSPACE ?= $(V23_ROOT)/release/projects/chat
+TEST_URL ?= https://chat.staging.v.io
+ifndef NO_XVFB
+	XVFB := TMPDIR=/tmp xvfb-run -s '-ac -screen 0 1024x768x24'
+endif
+
+ifdef BUILD_EXTENSION
+	BUILD_EXTENSION_PROPERTY := "-DvanadiumExtensionPath=$(VANADIUM_JS)/extension/build"
+endif
+
+test-ui:
+ifdef BUILD_EXTENSION
+	make -B -C $(VANADIUM_JS)/extension build-dev
+endif
+	WORKSPACE=$(WORKSPACE) $(XVFB) \
+	  mvn test \
+	  -f=$(V23_ROOT)/release/projects/chat/clients/web/test/ui/pom.xml \
+	  -Dtest=ChatUITest \
+	  -DchromeDriverBin=$(CHROME_WEBDRIVER) \
+	  -DhtmlReportsRelativePath=htmlReports \
+	  -DgoogleBotUsername=$(GOOGLE_BOT_USERNAME) \
+	  -DgoogleBotPassword=$(GOOGLE_BOT_PASSWORD) \
+	  $(BUILD_EXTENSION_PROPERTY) \
+	  -DtestUrl=$(TEST_URL)
+
 clean:
-	rm -rf node_modules
+	rm -rf build
 	rm -rf clients/shell/go/{bin,pkg}
 	rm -rf clients/shell/credentials
-	rm -rf build
+	rm -rf clients/web/test/ui/target
+	rm -rf htmlReports
+	rm -rf node_modules
 
 lint: node_modules
 	jshint .
diff --git a/clients/web/test/ui/pom.xml b/clients/web/test/ui/pom.xml
new file mode 100644
index 0000000..5fe189e
--- /dev/null
+++ b/clients/web/test/ui/pom.xml
@@ -0,0 +1,72 @@
+<?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 Chat</name>
+  <groupId>io.v.webdriver</groupId>
+  <artifactId>vanadium_chat</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>
+    </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-surefire-plugin</artifactId>
+        <version>2.18</version>
+        <configuration>
+          <forkMode>always</forkMode>
+        </configuration>
+      </plugin>
+      <plugin>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>3.2</version>
+        <configuration>
+            <source>1.7</source>
+            <target>1.7</target>
+        </configuration>
+      </plugin>
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+            <execution>
+                <phase>generate-sources</phase>
+                <goals><goal>add-source</goal></goals>
+                <configuration>
+                    <sources>
+                        <source>${env.V23_ROOT}/release/javascript/core/test/ui/</source>
+                    </sources>
+                </configuration>
+            </execution>
+        </executions>
+    </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/clients/web/test/ui/src/test/java/io/v/webdriver/chat/ChatUITest.java b/clients/web/test/ui/src/test/java/io/v/webdriver/chat/ChatUITest.java
new file mode 100644
index 0000000..7020a81
--- /dev/null
+++ b/clients/web/test/ui/src/test/java/io/v/webdriver/chat/ChatUITest.java
@@ -0,0 +1,58 @@
+// 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.chat;
+
+import org.junit.Test;
+
+import io.v.webdriver.VanadiumUITestBase;
+import io.v.webdriver.commonpages.OAuthLoginPage;
+import io.v.webdriver.htmlreport.HTMLReportData;
+import io.v.webdriver.htmlreport.HTMLReporter;
+
+/**
+ * UI tests for Vanadium Chat Web Application.
+ *
+ * @author alexfandrianto@google.com
+ */
+public class ChatUITest extends VanadiumUITestBase {
+  /**
+   * System property name for the test url. This will be set from the mvn command line.
+   */
+  private static final String PROPERTY_TEST_URL = "testUrl";
+
+  private static final String TEST_NAME_INIT_PROCESS = "Chat Initialization Process";
+
+  /**
+   * Tests initialization process.
+   * <p>
+   * The process includes signing into Chrome, installing Vanadium plugin, authenticating OAuth, and
+   * visiting Chat's landing page and sending a single message.
+   */
+  @Test
+  public void testInitProcess() throws Exception {
+    HTMLReportData reportData = new HTMLReportData(TEST_NAME_INIT_PROCESS, htmlReportsDir);
+    curHTMLReportData = reportData;
+
+    super.signInAndInstallExtension(reportData);
+
+    // Get the url for the Chat web app.
+    String url = System.getProperty(PROPERTY_TEST_URL);
+    System.out.printf("Url: %s\n", url);
+    MainPage mainPage = new MainPage(driver, url, reportData);
+    if (url.equals("https://chat.staging.v.io") || url.equals("https://chat.v.io")) {
+      // These are OAuth protected pages.
+      OAuthLoginPage oauthLoginPage = mainPage.goToPage();
+      oauthLoginPage.login();
+    } else {
+      mainPage.goWithoutTakingScreenshot();
+    }
+    super.handleCaveatTab(reportData);
+    mainPage.validatePage();
+
+    // Write html report.
+    HTMLReporter reporter = new HTMLReporter(reportData);
+    reporter.generateReport();
+  }
+}
diff --git a/clients/web/test/ui/src/test/java/io/v/webdriver/chat/MainPage.java b/clients/web/test/ui/src/test/java/io/v/webdriver/chat/MainPage.java
new file mode 100644
index 0000000..540aa8d
--- /dev/null
+++ b/clients/web/test/ui/src/test/java/io/v/webdriver/chat/MainPage.java
@@ -0,0 +1,172 @@
+// 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.chat;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+
+import org.junit.Assert;
+import org.openqa.selenium.By;
+import org.openqa.selenium.Keys;
+import org.openqa.selenium.TakesScreenshot;
+import org.openqa.selenium.TimeoutException;
+import org.openqa.selenium.WebDriver;
+import org.openqa.selenium.WebElement;
+import org.openqa.selenium.support.ui.ExpectedConditions;
+import org.openqa.selenium.support.ui.WebDriverWait;
+
+import io.v.webdriver.Util;
+import io.v.webdriver.commonpages.OAuthLoginPage;
+import io.v.webdriver.commonpages.PageBase;
+import io.v.webdriver.htmlreport.HTMLReportData;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * The main page of Vanadium Chat.
+ *
+ * @author alexfandrianto@google.com
+ */
+public class MainPage extends PageBase {
+
+  /**
+   * The google username is set when this page is constructed.
+   * It is used to identify the current chat user during the test.
+   */
+  private static final String PROPERTY_GOOGLE_BOT_USERNAME = "googleBotUsername";
+  private final String username;
+
+  /**
+   * Checks whether the specified user shows up in the members section.
+   * Note: Does not confirm that this particular tab has connected, so don't
+   * rely on this to work if the user is already in the chatroom from some other
+   * source.
+   */
+  private static class CheckMember implements Predicate<WebDriver> {
+    private final String user;
+
+    public CheckMember(String user) {
+      this.user = user;
+    }
+
+    @Override
+    public boolean apply(WebDriver driver) {
+      // The members are in a div with the 'members' class.
+      // The div has an unordered list of members, and the list items will eventually
+      // have the user inside of it.
+      System.out.println("There are " + driver.findElements(
+        By.xpath("//div[@class='members']/ul/li")).size() + " users in the chatroom.");
+      List<WebElement> matchingMembers = driver.findElements(
+        By.xpath("//div[@class='members']/ul/li/span[text()='" + user + "']"));
+
+      return matchingMembers.size() > 0;
+    }
+  }
+
+  /**
+   * Checks whether the given user sent a specific message.
+   * Note: This check is dumb, so don't rely on this to be accurate when
+   * checking if a message was sent twice.
+   */
+  private static class CheckMessage implements Predicate<WebDriver> {
+    private final String user;
+    private final String message;
+
+    public CheckMessage(String user, String message) {
+      this.user = user;
+      this.message = message;
+    }
+
+    @Override
+    public boolean apply(WebDriver driver) {
+      // The messages are in a div with the 'messages' class.
+      // Each message contains multiple spans. 'sender' contains the sender's
+      // username, while 'text' contains the message text.
+      List<WebElement> allMessages = driver.findElements(
+        By.xpath("//div[@class='messages']/div"));
+
+      System.out.println("There are " + allMessages.size() + " messages in the chatroom.");
+
+      for (WebElement messageElem : allMessages) {
+        // sender's text must match the user.
+        WebElement sender = messageElem.findElement(By.xpath("span[@class='sender' and text()='" + user + "']"));
+
+        // text's text must match the message.
+        WebElement text = messageElem.findElement(By.xpath("span[@class='text' and text()='" + message + "']"));
+
+        // We found a matching message if both sender and text are not null.
+        if (sender != null && text != null) {
+          return true;
+        }
+      }
+
+      return false;
+    }
+  }
+
+  public MainPage(WebDriver driver, String url, HTMLReportData htmlReportData) {
+    super(driver, url, htmlReportData);
+
+    username = System.getProperty(PROPERTY_GOOGLE_BOT_USERNAME);
+  }
+
+  public OAuthLoginPage goToPage() {
+    super.goWithoutTakingScreenshot();
+    // The first time going to the main page, it will ask for oauth login.
+    return new OAuthLoginPage(driver, htmlReportData);
+  }
+
+  public void validatePage() {
+    // Verify that the user shows up in the member location.
+    log("Check that user is a member.");
+    checkUserIsMember();
+
+    // Verify that the user can send a chat and see it appear.
+    // Note: This check assumes the chats go through the server before showing
+    // up on the client side. As of writing, this is the case.
+    log("Send chat message.");
+    checkMessageIsDelivered();
+  }
+
+  public void checkUserIsMember() {
+    CheckMember memberChecker = new CheckMember(username);
+    try {
+      wait.until(memberChecker);
+    } catch(TimeoutException e) {
+      e.printStackTrace();
+      Assert.fail(e.toString());
+    }
+    Util.takeScreenshot((TakesScreenshot)driver, "is-member-" + username + ".png", "Is Member? " + username, htmlReportData);
+  }
+
+  // Sends a non-obtrusive message to all chatters using the chat app.
+  // Note: All users of Vanadium chat are in the same chat room, so the message
+  // sent should not be spammy.
+  public void checkMessageIsDelivered() {
+    // First, let's find the text box where we can enter our message.
+    // Since its React ID could easily change (autogenerated), we use xpath to identify it.
+    WebElement input = driver.findElement(By.xpath("//div[@class='compose']/form/input"));
+    String message = "Vanadium Bot says Hello!";
+    input.sendKeys(message);
+    Util.takeScreenshot((TakesScreenshot)driver, "message-written.png", "Message Written", htmlReportData);
+
+    // And then send the text.
+    input.sendKeys(Keys.RETURN);
+    Util.takeScreenshot((TakesScreenshot)driver, "message-sent.png", "Message Sent", htmlReportData);
+
+    // Now, wait until the text shows up on screen!
+    CheckMessage messageChecker = new CheckMessage(username, message);
+    try {
+      wait.until(messageChecker);
+    } catch(TimeoutException e) {
+      e.printStackTrace();
+      Assert.fail(e.toString());
+    }
+    Util.takeScreenshot((TakesScreenshot)driver, "message-received.png", "Message Received", htmlReportData);
+  }
+}