reader/android: adds proof of concept UI tests

Adds a rough, proof of concept version of some automated UI tests. The test
added here will bless the reader application and return to the device list view.
Additional tests and refinement are expected so that we can better duplicate
user scenarios and recreate configurations we know to be problematic so that
debugging can be simplified.

Change-Id: I40e0c32235be3ac314f1cbc52ce29b358c3c9696
diff --git a/.gitignore b/.gitignore
index ee53436..98c6353 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,16 +1,19 @@
 /.jiri
 #TODO(nlacasse): Get rid of .v23 below once v23->jiri transition is complete.
 /.v23
+
+node_modules
+*.log
+
 web/bin/principal
 web/bin/syncbased
 web/coverage
 web/credentials
 web/disk.html
-web/node_modules
-web/npm-debug.log
 web/public/bundle.*
 web/tmp
 web/public/pdf-web-view.js
+web/public/*.pdf
 
 # VDL generated stubs should be ignored
 web/browser/vanadium/v.io/*
diff --git a/.jshintignore b/.jshintignore
new file mode 100644
index 0000000..691931c
--- /dev/null
+++ b/.jshintignore
@@ -0,0 +1,5 @@
+*/node_modules
+web/public/*.js
+web/coverage
+web/browser/vanadium/vdl/*
+web/browser/vanadium/v.io/*
diff --git a/web/.jshintrc b/.jshintrc
similarity index 100%
rename from web/.jshintrc
rename to .jshintrc
diff --git a/android/Makefile b/android/Makefile
new file mode 100644
index 0000000..1d6b75d
--- /dev/null
+++ b/android/Makefile
@@ -0,0 +1,54 @@
+MAKEFLAGS += --warn-undefined-variables
+PATH := node_modules/.bin:$(PATH)
+NODE_DIR := $(shell jiri v23-profile list --info Target.InstallationDir nodejs)
+PATH := $(PATH):$(NODE_DIR)/bin
+SHELL := /bin/bash
+VDLPATH := $(JIRI_ROOT)/release/go/src:$(shell cd ../common; pwd)
+
+.SHELLFLAGS := -eu -o pipefail -c
+.DEFAULT_GOAL := all
+.SUFFIXES:
+
+port ?= 4723
+udid ?= ${DEVICE_ID}
+log-level ?= warn
+
+.PHONY:
+all: bin node_modules
+	@true
+
+.PHONY:
+bin:
+	$(MAKE) -C ../web bin
+
+# TODO(jasoncampbell): Add a task for building the account manager APK.
+
+.PHONY:
+distclean:
+	@$(RM) -fr node_modules
+
+.PHONY:
+clean:
+	./gradlew clean
+
+.DELETE_ON_ERROR:
+node_modules: package.json
+	@npm prune
+	@npm install
+	@touch $@
+
+.PHONY:
+apk: app/build/outputs/apk/app-debug.apk
+
+app/build/outputs/apk/app-debug.apk:
+	./gradlew :app:assembleDebug
+
+.PHONY:
+appium-server: node_modules apk
+	appium --port $(port) --log-level $(log-level)
+
+.PHONY:
+test-integration: all
+	APK="$(realpath app/build/outputs/apk/app-debug.apk)" \
+	DEVICE_ID="$(udid)" \
+	tape test/test-*.js
diff --git a/android/README.md b/android/README.md
index 422c861..2d02bea 100644
--- a/android/README.md
+++ b/android/README.md
@@ -42,8 +42,62 @@
 To make the synchronization work properly,
 there needs to be a cloud Syncbase instance running, which hosts the Syncgroup for the Reader app.
 
-To run the cloudsync instance, run the following command from the `reader/web` directory:
+To run the cloudsync instance, run the following command from this directory:
 
-    make cloudsync
+    make -C `git rev-parse --show-toplevel`/web clean cloudsync
 
+# Testing
 
+There is an automated UI testing ability enabled by Appium which is a work in progress. To run the tests you will need to run an Appium server, the `cloudsync` syncbase instance, and then the test. In the future this may be simplified so that a single CLI task can manage all the test dependencies.
+
+Be sure to set the `$ANDROID_HOME` env var with:
+
+    export ANDROID_HOME=<pathname>/Android/sdk/
+
+## Setup
+
+First you will need to install all the dependencies, the default make task will handle this (it might take a minute or so):
+
+    make
+
+This will install the following testing dependencies:
+
+* Any testing dependencies declared in the `package.json` (Appium, tape, etc.).
+* Vanadium dependencies for the `syncbased` and `principal` commands.
+
+Next you should build the APK for the reader application, this can be done via Android studio or with Gradle via the Makefile:
+
+    make apk
+
+Once the dependencies are in order and the application has be built you will need to plug in an Android device, and retrieve it's unique device id using `adb`. Make note of the device id for later.
+
+    adb devices -l
+
+## Services
+
+Appium uses the a client/server architecture similar to Selenuim (in fact the clients libraries are the same). The tests are currently written in JS but they could be written in any language with a web driver client. Before tests can be run an Appium server needs to be available to handle HTTP requests from the client.
+
+To run the Appium server:
+
+    udid=<device-id> make appium-server
+
+The reader app needs a cloud available peer to synchronize with, to provide this run the `cloudsync` Syncbase instance with the blessings of the same user who owns the Android device.
+
+    make -C `git rev-parse --show-toplevel`/web clean cloudsync
+
+**NOTE**: Be sure to login to the Google OAuth with the same email address as the account that owns the phone.
+
+## Run
+
+With the `cloudsync` service and the Appium server running the tests can now be run with one caveat: **be sure to unlock the phone**.
+
+    device-id=<device-id> make test-integration
+
+Either the variable `device-id` or the environment variable `$DEVICE_ID` can be used. The command above will execute the tests written in JS that match `test/test-*.js`.
+
+The make tasks are provided as a convenience so that any dependencies can be resolved automatically. It is possible to run the tests directly with tape once the initial setup is done and the required Appium server and cloudsync peer are running.
+
+    tape test/test-my-new-test.js
+    node test/test-my-new-test.js # This works too.
+
+[Appium]: http://appium.io/
diff --git a/android/app/src/main/res/layout/activity_device_set_chooser.xml b/android/app/src/main/res/layout/activity_device_set_chooser.xml
index 8c5bbb0..36690eb 100644
--- a/android/app/src/main/res/layout/activity_device_set_chooser.xml
+++ b/android/app/src/main/res/layout/activity_device_set_chooser.xml
@@ -19,6 +19,7 @@
 
     <android.support.design.widget.FloatingActionButton
         android:id="@+id/button_add_device_set"
+        android:contentDescription="@string/action_add_pdf"
         android:layout_width="wrap_content"
         android:layout_height="wrap_content"
         android:layout_alignParentBottom="true"
diff --git a/android/app/src/main/res/values/strings.xml b/android/app/src/main/res/values/strings.xml
index 1c91eba..49ab1eb 100644
--- a/android/app/src/main/res/values/strings.xml
+++ b/android/app/src/main/res/values/strings.xml
@@ -2,4 +2,5 @@
     <string name="app_name">PDF Reader</string>
     <string name="action_settings">Settings</string>
     <string name="action_link">Link Page</string>
+    <string name="action_add_pdf">Add PDF</string>
 </resources>
diff --git a/android/package.json b/android/package.json
new file mode 100644
index 0000000..1866d12
--- /dev/null
+++ b/android/package.json
@@ -0,0 +1,23 @@
+{
+  "private": true,
+  "name": "",
+  "version": "1.0.0",
+  "description": "",
+  "main": "",
+  "directories": {
+    "test": "test"
+  },
+  "dependencies": {
+    "appium": "^1.5.0-beta9",
+    "debug": "^2.2.0",
+    "getport": "^0.1.0",
+    "ms": "^0.7.1",
+    "run-series": "^1.1.4",
+    "run-waterfall": "^1.1.3",
+    "tape": "^4.2.2",
+    "verror": "^1.6.0",
+    "wd": "^0.4.0",
+    "xtend": "^4.0.1"
+  },
+  "devDependencies": {}
+}
diff --git a/android/test/config.json b/android/test/config.json
new file mode 100644
index 0000000..b3d1b79
--- /dev/null
+++ b/android/test/config.json
@@ -0,0 +1,9 @@
+{
+  "devices": [
+    {
+      "udid": "8XV5T15A23006055",
+      "version": "6.0",
+      "name": "nexus"
+    }
+  ]
+}
diff --git a/android/test/helpers/configure.js b/android/test/helpers/configure.js
new file mode 100644
index 0000000..3fe57a1
--- /dev/null
+++ b/android/test/helpers/configure.js
@@ -0,0 +1,71 @@
+// 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.
+
+var verror = require('verror');
+var getport = require('getport');
+var waterfall = require('run-waterfall');
+
+module.exports = configure;
+
+function configure(callback) {
+  var json;
+
+  try {
+    json = require('../config.json');
+  } catch (e) {
+    var err = verror(e, 'Missing configuration file.');
+    callback(err);
+    return;
+  }
+
+  var devices = json.devices;
+  var length = devices.length;
+  var jobs = new Array(length);
+  for (var i = 0; i < length; i++) {
+    console.log('device: ', devices[i]);
+    jobs[i] = task.bind(null, devices[i]);
+  }
+
+  waterfall(jobs, function done(err, ports) {
+    if (err) {
+      return callback(err);
+    }
+
+    callback(null, json);
+  });
+
+  // The first call will be missing the starting port.
+  function task(device, start, callback) {
+    if (typeof start === 'function') {
+      callback = start;
+      getport(done);
+    } else {
+      getport(start, done);
+    }
+
+    function done(err, port) {
+      if (err) {
+        callback(err);
+        return;
+      }
+
+      device.port = port;
+      callback(null, port + 10);
+    }
+  }
+}
+
+function device(options) {
+  if (!(this instanceof arguments.callee)) {
+    return new arguments.callee(arguments);
+  }
+
+  this.client = {
+
+  };
+
+  this.server = {
+
+  };
+}
diff --git a/android/test/helpers/devices.js b/android/test/helpers/devices.js
new file mode 100644
index 0000000..9dedbdd
--- /dev/null
+++ b/android/test/helpers/devices.js
@@ -0,0 +1,3 @@
+// 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.
diff --git a/android/test/helpers/remote.js b/android/test/helpers/remote.js
new file mode 100644
index 0000000..ed1dfc7
--- /dev/null
+++ b/android/test/helpers/remote.js
@@ -0,0 +1,26 @@
+// 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.
+
+var wd = require('wd');
+var debug = require('debug')('driver');
+
+module.exports = remote;
+
+// Wraps the Web Driver module's `remote(...)` method for logging purposes.
+function remote() {
+  debug('creating driver');
+
+  var driver = wd.remote({
+    host: 'localhost',
+    port: 4723
+  });
+
+  driver.on('status', function(status) {
+    debug('status: %s', status.trim());
+  });
+  driver.on('command', debug.bind(debug, 'command: %s "%s" => %o'));
+  driver.on('http', debug.bind(debug, 'http: %s "%s" => %o'));
+
+  return driver;
+}
diff --git a/android/test/helpers/setup.js b/android/test/helpers/setup.js
new file mode 100644
index 0000000..e399b1b
--- /dev/null
+++ b/android/test/helpers/setup.js
@@ -0,0 +1,27 @@
+// 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.
+
+var configure = require('./configure');
+
+module.exports = setup;
+
+function setup(fn) {
+  // debug('initializing');
+
+  return test;
+
+  function test(t) {
+    // debug('running');
+
+    configure(function(err, config) {
+      if (err) {
+        t.error(err);
+        t.end();
+        return;
+      }
+
+      // debug('config %o', config);
+    });
+  }
+}
diff --git a/android/test/test-simple.js b/android/test/test-simple.js
new file mode 100644
index 0000000..3f2f28f
--- /dev/null
+++ b/android/test/test-simple.js
@@ -0,0 +1,190 @@
+// 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.
+
+var test = require('tape');
+var remote = require('./helpers/remote');
+var debug = require('debug')('test');
+var ms = require('ms');
+var waterfall = require('run-waterfall');
+var extend = require('xtend');
+
+test('simple', function(t) {
+  var driver = remote();
+
+  // Hook into the test's end event to shutdown the driver session.
+  t.on('end', function end() {
+    debug('test ending, closing driver');
+
+    driver.quit(function onclose(err) {
+      if (err) {
+        debug('error: %o', err);
+        throw err;
+      }
+    });
+  });
+
+  var tasks = [
+    init,
+    wait(ms('4s')),
+    getCurrentActivity,
+    contexts,
+    wait(ms('4s')),
+    selectBlessing
+  ];
+
+  // Run all the tasks above, one after the other.
+  waterfall(tasks, function done(err, res) {
+    if (err) {
+      t.end(err);
+      return;
+    }
+
+    debug('res: %o', res);
+
+    t.ok(true);
+    t.end();
+  });
+
+  function init(callback) {
+    var defaults = {
+      browserName: '',
+      'appium-version': '1.4.13',
+      platformName: 'Android',
+    };
+
+    // TODO(jasoncampbell): Support some kind of cinfiguration file for all
+    // these options.
+    var options = extend(defaults, {
+      platformVersion: '6.0',
+      deviceName: process.env.DEVICE_ID,
+      app: process.env.APK
+    });
+
+    debug('initializing driver with: %o', options);
+
+    driver.init(options, done);
+
+    function done(err) {
+      if (err) {
+        return callback(err);
+      }
+
+      callback(null, {});
+    }
+  }
+
+  function wait(miliseconds) {
+    miliseconds = miliseconds || 60;
+    return task;
+
+    function task(res, callback) {
+      setTimeout(function timeout() {
+        callback(null, res);
+      }, miliseconds);
+    }
+  }
+
+  function getCurrentActivity(res, callback) {
+    driver.getCurrentActivity(function done(err, activity) {
+      if (err) {
+        return callback(err);
+      }
+
+      res.activity = activity;
+      callback(null, res);
+    });
+  }
+
+  function contexts(res, callback) {
+    driver.contexts(function done(err, contexts) {
+      if (err) {
+        return callback(err);
+      }
+
+      res.contexts = contexts;
+      callback(null, res);
+    });
+  }
+
+  function selectBlessing(res, callback) {
+    var selector = [
+      'new UiSelector()',
+      '.className("android.widget.CheckedTextView")',
+      '.index(0)'
+    ].join('');
+
+    driver.elementByAndroidUIAutomator(selector, function(err, element) {
+      if (err) {
+        return callback(err);
+      }
+
+      t.ok(element, 'Blessing selection UI should exist');
+
+      element.click(function(err) {
+        if (err) {
+          return callback(err);
+        }
+
+        res['check-box'] = element;
+
+        var selector = [
+          'new UiSelector()',
+          '.className("android.widget.Button")',
+          '.text("OK")'
+        ].join('');
+
+        driver.elementByAndroidUIAutomator(selector, function(err, element) {
+          if (err) {
+            return callback(err);
+          }
+
+          element.click(function(err) {
+            if (err) {
+              return callback(err);
+            }
+
+            var selector = [
+              'new UiSelector()',
+              '.className("android.widget.Button")',
+              '.text("Allow")'
+            ].join('');
+
+            driver
+              .elementByAndroidUIAutomator(selector, function(err, element) {
+
+              if (err) {
+                return callback(err);
+              }
+
+              element.click(function(err) {
+                if (err) {
+                  return callback(err);
+                }
+
+                callback(null, res);
+              });
+            });
+
+
+          });
+        });
+      });
+    });
+  }
+});
+
+// NOTE: Below is a WIP example of how an API for running a single test which
+// spans multiple devices might work. Ideally the managment of Appium clients,
+// Appium servers, and the cloud instance could be managed by the setup helper.
+var setup = require('./helpers/setup');
+
+test.skip('bless application', setup(function(t, devices) {
+  devices.bless(function onbless(err) {
+    if (err) {
+      return t.error(err);
+    }
+
+    t.end();
+  });
+}));
diff --git a/web/.jshintignore b/web/.jshintignore
deleted file mode 100644
index efe23e2..0000000
--- a/web/.jshintignore
+++ /dev/null
@@ -1,5 +0,0 @@
-node_modules
-public/*.js
-coverage
-browser/vanadium/vdl/*
-browser/vanadium/v.io/*
diff --git a/web/Makefile b/web/Makefile
index a10e452..b9da02e 100644
--- a/web/Makefile
+++ b/web/Makefile
@@ -19,9 +19,14 @@
 cloudsync_port ?= 8000
 id ?= $(shell if test -e tmp/id; then cat tmp/id; else PATH=$(PATH) bin/uuid; fi)
 
+.PHONY:
 all: public/bundle.js public/pdf-web-view.js
 	@true  # silences watch
 
+.PHONY:
+bin: bin/principal bin/syncbased
+	@true
+
 .DELETE_ON_ERROR:
 node_modules: package.json
 	@npm prune
@@ -49,6 +54,8 @@
 .PHONY:
 distclean: clean
 	@$(RM) -fr node_modules
+	@$(RM) -f bin/principal
+	@$(RM) -f bin/syncbased
 	@jiri goext distclean
 
 .PHONY:
@@ -57,8 +64,6 @@
 	@$(RM) -fr public/bundle.js
 	@$(RM) -fr tmp
 	@$(RM) -fr credentials
-	@$(RM) -f bin/principal
-	@$(RM) -f bin/syncbased
 	@$(RM) -fr browser/vanadium/v.io
 	@$(RM) -fr browser/vanadium/vdl