You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@cordova.apache.org by mm...@apache.org on 2014/04/24 20:30:34 UTC

[7/9] git commit: Replacing with new test harness

Replacing with new test harness


Project: http://git-wip-us.apache.org/repos/asf/cordova-labs/repo
Commit: http://git-wip-us.apache.org/repos/asf/cordova-labs/commit/908dd288
Tree: http://git-wip-us.apache.org/repos/asf/cordova-labs/tree/908dd288
Diff: http://git-wip-us.apache.org/repos/asf/cordova-labs/diff/908dd288

Branch: refs/heads/cdvtest
Commit: 908dd2888583b07bed661a1e33d63cdad902270a
Parents: fce9894
Author: Michal Mocny <mm...@gmail.com>
Authored: Tue Apr 22 11:09:24 2014 -0400
Committer: Michal Mocny <mm...@gmail.com>
Committed: Thu Apr 24 11:07:06 2014 -0400

----------------------------------------------------------------------
 cordova-app-test-harness/www/index.html         |  18 +-
 .../www/jasmine-jsreporter.js                   | 256 -------------------
 cordova-app-test-harness/www/jasmine-medic.js   | 129 ++++++++++
 cordova-app-test-harness/www/jasmine_helpers.js |  77 ++++++
 cordova-app-test-harness/www/main.css           |  53 +++-
 cordova-app-test-harness/www/main.js            | 256 ++++++-------------
 cordova-app-test-harness/www/medic.js           |  38 +++
 cordova-app-test-harness/www/tests.js           |  64 +++++
 8 files changed, 436 insertions(+), 455 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/cordova-labs/blob/908dd288/cordova-app-test-harness/www/index.html
----------------------------------------------------------------------
diff --git a/cordova-app-test-harness/www/index.html b/cordova-app-test-harness/www/index.html
index 2d0c2c7..1af7178 100644
--- a/cordova-app-test-harness/www/index.html
+++ b/cordova-app-test-harness/www/index.html
@@ -12,14 +12,26 @@
 
     <script type="text/javascript" src="assets/jasmine-2.0.0/jasmine.js"></script>
     <script type="text/javascript" src="assets/jasmine-2.0.0/jasmine-html.js"></script>
-    <script type="text/javascript" src="jasmine-jsreporter.js"></script>
+
     <script type="text/javascript" src="cordova.js"></script>
+
+    <!-- App scripts -->
+    <script type="text/javascript" src="tests.js"></script>
+    <script type="text/javascript" src="jasmine_helpers.js"></script>
+    <script type="text/javascript" src="jasmine-medic.js"></script>
+    <script type="text/javascript" src="medic.js"></script>
     <script type="text/javascript" src="main.js"></script>
   </head>
 
   <body>
     <div id='title'></div>
-    <div id='content'></div>
-    <div id='log'></div>
+    <div id='middle'>
+      <div id='buttons'></div>
+      <div id='content'></div>
+    </div>
+    <div id='log'>
+      <div id='log--title'>Log</div>
+      <div id='log--content'></div>
+    </div>
   </body>
 </html>

http://git-wip-us.apache.org/repos/asf/cordova-labs/blob/908dd288/cordova-app-test-harness/www/jasmine-jsreporter.js
----------------------------------------------------------------------
diff --git a/cordova-app-test-harness/www/jasmine-jsreporter.js b/cordova-app-test-harness/www/jasmine-jsreporter.js
deleted file mode 100644
index de59eb6..0000000
--- a/cordova-app-test-harness/www/jasmine-jsreporter.js
+++ /dev/null
@@ -1,256 +0,0 @@
-jasmineRequire.CouchDB = function(j$) {
-  j$.CouchDBReporter = jasmineRequire.CouchDBReporter(j$);
-};
-
-jasmineRequire.CouchDBReporter = function(j$) {
-  
-  var noopTimer = {
-  start: function() {},
-  elapsed: function() { return 0; }
-  };
-  
-  function CouchDBReporter(options) {
-    var env = options.env || {},
-    couch = options.couch || {serverip: 'http://localhost:5900',serverpublic: 'http://localhost:5900',sha: 'test'},
-    getContainer = options.getContainer,
-    createElement = options.createElement,
-    createTextNode = options.createTextNode,
-    onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {},
-    timer = options.timer || noopTimer,
-    results = [],
-    specsExecuted = 0,
-    failureCount = 0,
-    pendingSpecCount = 0,
-    symbols;
-
-
-    var serverip = couch.serverip,
-    serverpublic = couch.serverpublic || serverip,
-    sha = couch.sha;
-    
-    var totalSpecsDefined;
-    this.jasmineStarted = function(options) {
-      totalSpecsDefined = options.totalSpecsDefined || 0;
-      timer.start();
-    };
-    
-    var topResults = new j$.ResultsNode({}, "", null),
-    currentParent = topResults;
-    
-    this.suiteStarted = function(result) {
-    };
-    
-    this.suiteDone = function(result) {
-
-    };
-    
-    this.specStarted = function(result) {
-      // Start timing this spec
-    };
-    
-    var failures = [];
-    this.specDone = function(result) {
-      if (result.status != "disabled") {
-        specsExecuted++;
-      }
-      if (result.status == "failed") {
-        failureCount++;
-      }
-      if (result.status == "pending") {
-        pendingSpecCount++;
-      }
-    };
-    
-    this.jasmineDone = function() {
-      var p = device.platform.toLowerCase();
-      this.postTests({
-                     mobilespec:results,
-                     sha:this.sha,
-                     platform:(platformMap.hasOwnProperty(p) ? platformMap[p] : p),
-                     version:device.version.toLowerCase(),
-                     timestamp:Math.round(Math.floor((new Date()).getTime() / 1000)),
-                     model:device.model || device.name
-                     });
-      
-    };
-    
-
-    logresult = function(){
-      if(failureCount>0 ) {
-        console.log('[[[ TEST OK ]]]');
-      } else {
-        console.log('[[[ TEST FAILED ]]]');
-      }
-      logfinished();
-    };
-    
-    var logfinished = function(){
-      console.log('>>> DONE <<<');
-    };
-    
-    this.postTests = function(json) {
-      console.log('posting tests');
-      var doc_id = [  sha, json.version, json.model].map(encodeURIComponent).join('__');
-      var doc_url =  serverip + '/mobilespec_results/' + doc_id;
-      var publicdoc_url =  serverpublic + '/mobilespec_results/' + doc_id;
-      console.log('Test Results URL = '+publicdoc_url+' <<<end test result>>>');
-
-      var xhr = new XMLHttpRequest();
-      xhr.open("PUT", doc_url, true);
-      xhr.onreadystatechange=function() {
-        console.log('onreadystatechange');
-        if (xhr.readyState==4) {
-          console.log('readystate==4, status: ' + xhr.status);
-          if (xhr.status==201) { // HTTP 201 Created the doc successfully
-            logresult();
-          } else if (xhr.status == 409) { // HTTP 409 Conflict - doc exists already
-            console.log('conflict on couch');
-            // doc already exists. now let's GET it, grab the rev, delete it, and try again.
-            var exehar = new XMLHttpRequest();
-            exehar.open('GET', doc_url, true);
-            exehar.onreadystatechange=function() {
-              if (exehar.readyState==4) {
-                if (exehar.status==200) {
-                  var existing_doc = JSON.parse(exehar.responseText);
-                  var rev = existing_doc._rev;
-                  var eksatschargh = new XMLHttpRequest();
-                  eksatschargh.open('DELETE', doc_url + '?rev=' + rev, true);
-                  eksatschargh.onreadystatechange=function() {
-                    if (eksatschargh.readyState==4) {
-                      if (eksatschargh.status==200) {
-                        var x_h_r = new XMLHttpRequest();
-                        x_h_r.open('PUT', doc_url, true);
-                        x_h_r.onreadystatechange=function() {
-                          if (x_h_r.readyState==4) {
-                            if (x_h_r.status==201) {
-                              logresult();
-                            } else {
-                              console.log('the round trip delete+create failed. i give up. status was: ' + x_h_r.status);
-                              console.log(x_h_r.responseText);
-                            }
-                          }
-                        };
-                        x_h_r.send(JSON.stringify(json));
-                      } else {
-                        console.log('We tried to add the results to couch. it said it already exists. now im trying to DELETE it. delete failed. status on the DELETE: ' + eksatschargh.status);
-                      }
-                    }
-                  };
-                  eksatschargh.send(null);
-                } else {
-                  console.log('look, we tried to add the results to couch. it said it already exists. now im trying to GET it so i can DELETE it. Get failed. status on the GET: ' + exehar.status);
-                }
-              }
-            };
-            exehar.send(null);
-          } else {
-            console.log('Unexpected couchDB error. status code: ' + xhr.status);
-            console.log(xhr.responseText);
-            logfinished();
-          }
-        }
-      };
-      xhr.send(JSON.stringify(json));
-    }
-    return this;
-  }
-  
-  /*
-postTests: function(json) {
-  console.log('posting tests');
-  var xhr = new XMLHttpRequest();
-  var doc_id = [ this.sha, json.version, json.model].map(encodeURIComponent).join('__');
-  // TODO: expose the db in this url for customization
-  var doc_url = this.serverip + '/mobilespec_results/' + doc_id;
-  var publicdoc_url = this.serverpublic + '/mobilespec_results/' + doc_id;
-  console.log('Test Results URL = '+publicdoc_url+' <<<end test result>>>');
-  xhr.open("PUT", doc_url, true);
-  
-  xhr.onreadystatechange=function() {
-    console.log('onreadystatechange');
-    if (xhr.readyState==4) {
-      console.log('readystate==4, status: ' + xhr.status);
-      if (xhr.status==201) {
-        // HTTP 201 Created
-        // we added the doc, hooray
-        if(!(jasmine.runnerResults.failed)) {
-          console.log('[[[ TEST OK ]]]');
-        } else {
-          console.log('[[[ TEST FAILED ]]]');
-        }
-        console.log('>>> DONE <<<');
-       } else if (xhr.status == 409) {
-        console.log('conflict on couch');
-        // HTTP 409 Conflict
-        // doc already exists. now let's GET it, grab the rev, delete it, and try again.
-        var exehar = new XMLHttpRequest();
-        exehar.open('GET', doc_url, true);
-        exehar.onreadystatechange=function() {
-          if (exehar.readyState==4) {
-            if (exehar.status==200) {
-              var existing_doc = JSON.parse(exehar.responseText);
-              var rev = existing_doc._rev;
-              var eksatschargh = new XMLHttpRequest();
-              eksatschargh.open('DELETE', doc_url + '?rev=' + rev, true);
-              eksatschargh.onreadystatechange=function() {
-                if (eksatschargh.readyState==4) {
-                  if (eksatschargh.status==200) {
-                    var x_h_r = new XMLHttpRequest();
-                    x_h_r.open('PUT', doc_url, true);
-                    x_h_r.onreadystatechange=function() {
-                      if (x_h_r.readyState==4) {
-                        if (x_h_r.status==201) {
-                          if(!(jasmine.runnerResults.failed)) {
-                            console.log('[[[ TEST OK ]]]');
-                          } else {
-                            console.log('[[[ TEST FAILED ]]]');
-                          }
-                          console.log('>>> DONE <<<');
-                        } else {
-                          console.log('the round trip delete+create failed. i give up. status was: ' + x_h_r.status);
-                          console.log(x_h_r.responseText);
-                        }
-                      }
-                    };
-                    x_h_r.send(JSON.stringify(json));
-                  } else {
-                    console.log('We tried to add the results to couch. it said it already exists. now im trying to DELETE it. delete failed. status on the DELETE: ' + eksatschargh.status);
-                  }
-                }
-              };
-              eksatschargh.send(null);
-            } else {
-              console.log('look, we tried to add the results to couch. it said it already exists. now im trying to GET it so i can DELETE it. Get failed. status on the GET: ' + exehar.status);
-            }
-          }
-        };
-        exehar.send(null);
-      } else {
-        console.log('Unexpected couchDB error. status code: ' + xhr.status);
-        console.log(xhr.responseText);
-        console.log('>>> DONE <<<');
-      }
-    }
-  };
-  xhr.send(JSON.stringify(json));
-}
-   */
-  
-   /**
-   * Calculate elapsed time, in Seconds.
-   * @param startMs Start time in Milliseconds
-   * @param finishMs Finish time in Milliseconds
-   * @return Elapsed time in Seconds */
-  function elapsedSec (startMs, finishMs) {
-    return (finishMs - startMs) / 1000;
-  }
-
-  var platformMap = {
-    'ipod touch':'ios',
-    'iphone':'ios'
-  };
-
-  return CouchDBReporter;
-};
-
-

http://git-wip-us.apache.org/repos/asf/cordova-labs/blob/908dd288/cordova-app-test-harness/www/jasmine-medic.js
----------------------------------------------------------------------
diff --git a/cordova-app-test-harness/www/jasmine-medic.js b/cordova-app-test-harness/www/jasmine-medic.js
new file mode 100644
index 0000000..5f6d13c
--- /dev/null
+++ b/cordova-app-test-harness/www/jasmine-medic.js
@@ -0,0 +1,129 @@
+jasmineRequire.medic = function(j$) {
+  j$.MedicReporter = jasmineRequire.MedicReporter(j$);
+};
+
+jasmineRequire.MedicReporter = function(j$) {
+  
+  var noopTimer = {
+  start: function() {},
+  elapsed: function() { return 0; }
+  };
+  
+  function MedicReporter(options) {
+    var env = options.env || {},
+    logoptions = options.log || {logurl: 'http://localhost:6800'},
+    getContainer = options.getContainer,
+    createElement = options.createElement,
+    createTextNode = options.createTextNode,
+    onRaiseExceptionsClick = options.onRaiseExceptionsClick || function() {},
+    timer = options.timer || noopTimer,
+    results = [],
+    specsExecuted = 0,
+    failureCount = 0,
+    pendingSpecCount = 0,
+    symbols;
+
+
+    var serverurl = logoptions.logurl;
+
+    this.initialize = function() {
+    }
+    
+    var totalSpecsDefined;
+    this.jasmineStarted = function(options) {
+      totalSpecsDefined = options.totalSpecsDefined || 0;
+      timer.start();
+    };
+    
+    var topResults = new j$.ResultsNode({}, "", null),
+    currentParent = topResults;
+    
+    this.suiteStarted = function(result) {
+    };
+    
+    this.suiteDone = function(result) {
+
+    };
+    
+    this.specStarted = function(result) {
+      // Start timing this spec
+    };
+    
+    var failures = [];
+    this.specDone = function(result) {
+      if (result.status != "disabled") {
+        specsExecuted++;
+      }
+      if (result.status == "failed") {
+        failureCount++;
+        results.push(result);
+      }
+      if (result.status == "pending") {
+        pendingSpecCount++;
+      }
+    };
+
+    buildResults = function(){
+      var json ={specs:specsExecuted, failures:failureCount, results: results};
+      return json;
+    }
+    
+    this.jasmineDone = function() {
+      var p = 'Desktop';
+      var devmodel='none';
+      if(typeof device != 'undefined') {
+        p = device.platform.toLowerCase();
+        devmodel=device.model || device.name;
+      }
+
+      this.postTests({
+          mobilespec:buildResults(),
+          platform:(platformMap.hasOwnProperty(p) ? platformMap[p] : p),
+          version:p,
+          timestamp:Math.round(Math.floor((new Date()).getTime() / 1000)),
+          model:devmodel
+          });
+      
+    };
+    
+
+    logresult = function(){
+      if(failureCount>0 ) {
+        console.log('[[[ TEST OK ]]]');
+      } else {
+        console.log('[[[ TEST FAILED ]]]');
+      }
+      logfinished();
+    };
+    
+    var logfinished = function(){
+      console.log('>>> DONE <<<');
+    };
+    
+    this.postTests = function(json) {
+      console.log('posting tests');
+
+      var xhr = new XMLHttpRequest();
+      xhr.open("POST", serverurl+'/result', true);
+      xhr.setRequestHeader("Content-Type","application/json")
+      xhr.send(JSON.stringify(json));
+    }
+    return this;
+  }
+  
+   /**
+   * Calculate elapsed time, in Seconds.
+   * @param startMs Start time in Milliseconds
+   * @param finishMs Finish time in Milliseconds
+   * @return Elapsed time in Seconds */
+  function elapsedSec(startMs, finishMs) {
+    return (finishMs - startMs) / 1000;
+  }
+
+  var platformMap = {
+    'ipod touch':'ios',
+    'iphone':'ios'
+  };
+
+  return MedicReporter;
+};

http://git-wip-us.apache.org/repos/asf/cordova-labs/blob/908dd288/cordova-app-test-harness/www/jasmine_helpers.js
----------------------------------------------------------------------
diff --git a/cordova-app-test-harness/www/jasmine_helpers.js b/cordova-app-test-harness/www/jasmine_helpers.js
new file mode 100644
index 0000000..1224909
--- /dev/null
+++ b/cordova-app-test-harness/www/jasmine_helpers.js
@@ -0,0 +1,77 @@
+(function() {
+
+'use strict';
+
+var exports = window;
+
+exports.setUpJasmine = function() {
+  // Set up jasmine
+  var jasmine = jasmineRequire.core(jasmineRequire);
+  jasmineRequire.html(jasmine);
+  var jasmineEnv = jasmine.currentEnv_ = new jasmine.Env();
+
+  jasmine.DEFAULT_TIMEOUT_INTERVAL = 5000;
+  jasmineEnv.catchExceptions(false);
+
+  // Set up jasmine interface
+  var jasmineInterface = Object.create(null);
+  jasmineInterface.jasmine = jasmine;
+
+  // Fill in jasmineInterface with built-ins
+  var jasmine_env_functions = ['describe',
+                               'xdescribe',
+                               'it',
+                               'xit',
+                               'beforeEach',
+                               'afterEach',
+                               'expect',
+                               'pending',
+                               'spyOn',
+                               'addCustomEqualityTester',
+                               'addMatchers'];
+
+  jasmine_env_functions.forEach(function(fn) {
+    jasmineInterface[fn] = jasmineEnv[fn].bind(jasmineEnv);
+  });
+  jasmineInterface.clock = jasmineEnv.clock;
+
+  // Add Reporters
+  addJasmineReporters(jasmineInterface, jasmineEnv);
+
+  // Add Spec Filter
+  jasmineEnv.specFilter = function(spec) {
+    //console.log(spec.getFullName());
+    return true;
+  };
+
+  return jasmineInterface;
+}
+
+function addJasmineReporters(jasmineInterface, jasmineEnv) {
+  jasmineInterface.jsApiReporter = new jasmineInterface.jasmine.JsApiReporter({ timer: new jasmineInterface.jasmine.Timer() });
+  jasmineEnv.addReporter(jasmineInterface.jsApiReporter);
+
+  jasmineInterface.htmlReporter = new jasmineInterface.jasmine.HtmlReporter({
+    env: jasmineEnv,
+    queryString: function() { return null; },
+    onRaiseExceptionsClick: function() { },
+    getContainer: function() { return document.getElementById('content'); },
+    createElement: function() { return document.createElement.apply(document, arguments); },
+    createTextNode: function() { return document.createTextNode.apply(document, arguments); },
+    timer: new jasmineInterface.jasmine.Timer()
+  });
+  jasmineInterface.htmlReporter.initialize();
+  jasmineEnv.addReporter(jasmineInterface.htmlReporter);
+
+  if (window.medic.enabled) {
+    jasmineRequire.medic(jasmineInterface.jasmine);
+    jasmineInterface.MedicReporter = new jasmineInterface.jasmine.MedicReporter({
+      env: jasmineEnv,
+      log: { logurl: window.medic.logurl }
+    });
+    jasmineInterface.MedicReporter.initialize();
+    jasmineEnv.addReporter(jasmineInterface.MedicReporter);
+  }
+}
+
+}());

http://git-wip-us.apache.org/repos/asf/cordova-labs/blob/908dd288/cordova-app-test-harness/www/main.css
----------------------------------------------------------------------
diff --git a/cordova-app-test-harness/www/main.css b/cordova-app-test-harness/www/main.css
index c85be21..7475c90 100644
--- a/cordova-app-test-harness/www/main.css
+++ b/cordova-app-test-harness/www/main.css
@@ -1,13 +1,13 @@
+*, *:before, *:after {
+  -webkit-box-sizing: border-box;
+  box-sizing: border-box;
+}
+
 html, body {
   height: 100%;
   width: 100%;
-  padding: 0;
   margin: 0;
-}
-
-*, *:before, *:after {
-  -webkit-box-sizing: border-box;
-  box-sizing: border-box;
+  padding: 0;
 }
 
 #title {
@@ -17,29 +17,54 @@ html, body {
   top: 0;
   z-index: 1000;
 
-  background-color: #58b;
+  background-color: #75B2F0;
   font-size: 25px;
   text-align: center;
   font-weight: bold;
 }
 
-#content {
-  padding-top: 30px;
-  padding-bottom: 150px;
+#middle {
+  position: absolute;
+  top: 30px;
+  bottom: 20px;
+  width: 100%;
+  overflow-y: auto;
+  overflow-x: auto;
 }
 
 #log {
   position: fixed;
-  height: 150px;
+  height: 20px;
   width: 100%;
   bottom: 0;
   z-index: 1000;
+  border-top: 2px solid #777;
+  transition: 0.25s ease;
+}
+
+#log.expanded {
+  height: 60%;
+}
 
+#log--title {
+  position: absolute;
+  top: 0;
+  height: 20px;
+  width: 100%;
+  background-color: #93AAC2;
+}
+
+#log--content {
+  position: absolute;
+  top: 20px;
+  bottom: 0;
+  width: 100%;
+  overflow-x: none;
+  overflow-y: auto;
   background-color: white;
-  border-top: 2px solid #777;
-  white-space: pre;
 }
 
-#log > div {
+#log--content--line {
   border-bottom: 1px solid #ccc;
+  white-space: pre;
 }

http://git-wip-us.apache.org/repos/asf/cordova-labs/blob/908dd288/cordova-app-test-harness/www/main.js
----------------------------------------------------------------------
diff --git a/cordova-app-test-harness/www/main.js b/cordova-app-test-harness/www/main.js
index 4be5c47..54750b0 100644
--- a/cordova-app-test-harness/www/main.js
+++ b/cordova-app-test-harness/www/main.js
@@ -1,31 +1,49 @@
 (function() {
 
-/******************************************************************************/
-
 'use strict';
 
-function getURLParameter(name) {
-  return decodeURIComponent(
-      (new RegExp('[?|&]' + name + '=' + '([^&;]+?)(&|#|;|$)').exec(location.search) || [,""])[1].replace(/\+/g, '%20')
-    ) || null;
-}
+/******************************************************************************/
 
-function getMode() {
-  return getURLParameter('mode') || localStorage['mode'] || 'main';
+function getMode(callback) {
+  var mode = localStorage.getItem('mode') || 'main';
+  console.log(mode);
+  callback(mode);
 }
 
 function setMode(mode) {
-  localStorage['mode'] = mode;
-  location.href = 'index.html?mode=' + mode;
+  var handlers = {
+    'main': runMain,
+    'auto': runAutoTests,
+    'manual': runManualTests
+  }
+  if (!handlers.hasOwnProperty(mode)) {
+    return console.error("Unsopported mode: " + mode);
+  }
+
+  localStorage.setItem('mode', mode);
+  window.clearContent();
+
+  handlers[mode]();
 }
 
-function setTitle(title) {
+/******************************************************************************/
+
+window.clearContent = function() {
+  var content = document.getElementById('content');
+  content.innerHTML = '';
+  var log = document.getElementById('log--content');
+  log.innerHTML = '';
+  var buttons = document.getElementById('buttons');
+  buttons.innerHTML = '';
+}
+
+window.setTitle = function(title) {
   var el = document.getElementById('title');
   el.textContent = title;
 }
 
-function createButton(title, callback) {
-  var content = document.getElementById('content');
+window.createActionButton = function(title, callback) {
+  var buttons = document.getElementById('buttons');
   var div = document.createElement('div');
   var button = document.createElement('a');
   button.textContent = title;
@@ -35,209 +53,83 @@ function createButton(title, callback) {
   };
   button.classList.add('topcoat-button');
   div.appendChild(button);
-  content.appendChild(div);
+  buttons.appendChild(div);
 }
 
-function logger() {
-  console.log.apply(console, Array.prototype.slice.apply(arguments));
-  //console.trace();
-  var el = document.getElementById('log');
+// TODO: make a better logger
+window.logger = function() {
+  console.log.apply(console, arguments);
+  window.medic.log.apply(window.medic.log, arguments);
+
+  var el = document.getElementById('log--content');
   var div = document.createElement('div');
+  div.classList.add('log--content--line');
   div.textContent = Array.prototype.slice.apply(arguments).map(function(arg) {
       return (typeof arg === 'string') ? arg : JSON.stringify(arg);
     }).join(' ');
   el.appendChild(div);
+  // scroll to bottom
   el.scrollTop = el.scrollHeight;
 }
 
 /******************************************************************************/
 
-function runMain() {
-  setTitle('Cordova Tests');
-  createButton('Auto Tests', function() { setMode('autotests'); });
-  createButton('Manual Tests', function() { setMode('manualtests'); });
-
-  setDeviceInfo();
-}
-
-function setDeviceInfo() {
-  var el = document.getElementById('content');
-  function display() {
-    var div = document.createElement('div');
-    div.textContent = Array.prototype.slice.apply(arguments).map(function(arg) {
-        return (typeof arg === 'string') ? arg : JSON.stringify(arg);
-      }).join(' ');
-    el.appendChild(div);
-  }
-  display("Platform: ", device.platform);
-  display("Version: ", device.version);
-  display("Uuid: ", device.uuid);
-  display("Model: ", device.model);
-  display("Width: ", screen.width);
-  display("Height: ", screen.height);
-  display("Color-Depth: ", screen.colorDepth);
-  display("User-Agent: ", navigator.userAgent);
-}
-
-/******************************************************************************/
-
-function getPluginTestsJsModules() {
-  return cordova.require('cordova/plugin_list')
-    .map(function(jsmodule) {
-      return jsmodule.id;
-    })
-    .filter(function(id) {
-      return /.tests$/.test(id);
-    });
-}
-
-/******************************************************************************/
-
 function runAutoTests() {
   setTitle('Auto Tests');
-  createButton('Back', function() { setMode('main'); });
-
-  // Set up jasmine
-  var jasmine = jasmineRequire.core(jasmineRequire);
-  jasmineRequire.html(jasmine);
-  jasmineRequire.CouchDB(jasmine);
-  var jasmineEnv = jasmine.getEnv();
 
-  jasmine.DEFAULT_TIMEOUT_INTERVAL = 300;
+  createActionButton('Again', setMode.bind(null, 'auto'));
+  createActionButton('Reset App', location.reload.bind(location));
+  createActionButton('Back', setMode.bind(null, 'main'));
 
-  var catchingExceptions = getURLParameter("catch");
-  jasmineEnv.catchExceptions(typeof catchingExceptions === "undefined" ? true : catchingExceptions);
-
-  /*
-  var specFilter = new jasmine.HtmlSpecFilter({
-    filterString: function() { return getURLParameter("spec"); }
-  });
-
-  jasmineEnv.specFilter = function(spec) {
-    return specFilter.matches(spec.getFullName());
-  };
-  */
-
-  createHtmlReporter(jasmine);
-  createCouchdbReporter(jasmine, function() {
-    var test = cordova.require('org.apache.cordova.test-framework.test');
-    test.initForAutoTests(jasmine);
-
-    // Define our tests
-    getPluginTestsJsModules().forEach(function(id) {
-      var tests;
-      try {
-        tests = cordova.require(id);
-      } catch(ex) {
-        logger('Failed to load:', id);
-        return;
-      }
-      tests.init();
-      logger('Loaded:', id);
-    });
-
-    // Run!
-    test.runAutoTests();
-  });
-}
+  var jasmineInterface = window.setUpJasmine();
+  // Attach jasmineInterface to global object
+  for (var property in jasmineInterface) {
+    window[property] = jasmineInterface[property];
+  }
+  window.defineAutoTests(jasmineInterface);
 
-function createHtmlReporter(jasmine) {
-   // Set up jasmine html reporter
+  // Run the tests!
   var jasmineEnv = jasmine.getEnv();
-  var contentEl = document.getElementById('content');
-  var htmlReporter = new jasmine.HtmlReporter({
-    env: jasmineEnv,
-    queryString: getURLParameter,
-    onRaiseExceptionsClick: function() { /*queryString.setParam("catch", !jasmineEnv.catchingExceptions());*/ },
-    getContainer: function() { return contentEl; },
-    createElement: function() { return document.createElement.apply(document, arguments); },
-    createTextNode: function() { return document.createTextNode.apply(document, arguments); },
-    timer: new jasmine.Timer()
-  });
-  htmlReporter.initialize();
-
-  jasmineEnv.addReporter(htmlReporter);
-}
-
-function createCouchdbReporter(jasmine, callback) {
-  var settings = cordova.require('org.apache.cordova.appsettings.appsettings');
-  var win = function(dbsettings) {
-    configureCouchReporter(dbsettings,jasmine,callback);
-  };
-  var fail = function() {
-    configureCouchReporter(null,jasmine,callback);
-  };
-  settings.get(win, fail, ["CouchdbUrl", "CouchdbPrivateUrl", "TestSha"]);
-}
-
-function configureCouchReporter(dbsettings, jasmine, callback) {
-    if (!dbsettings) {
-      console.warn('Not reporting results to CouchDB.');
-      return callback();
-    }
-
-    try {
-      var reporteroptions = {
-        serverip: dbsettings['CouchdbUrl'],
-        serverpublic: dbsettings['CouchdbPrivateUrl'],
-        sha: dbsettings['TestSha'],
-      };
-      var ciReporter = new jasmine.CouchDBReporter({
-        env: jasmine.getEnv(),
-        queryString: getURLParameter,
-        onRaiseExceptionsClick: function() { /*queryString.setParam("catch", !jasmineEnv.catchingExceptions());*/ },
-        getContainer: function() { return contentEl; },
-        createElement: function() { return document.createElement.apply(document, arguments); },
-        createTextNode: function() { return document.createTextNode.apply(document, arguments); },
-        timer: new jasmine.Timer(),
-        couch: reporteroptions
-      });
-
-      jasmine.getEnv().addReporter(ciReporter);
-    } catch(ex) {
-      logger('Invalid CouchDB settings:', ex);
-    }
-
-    return callback();
+  jasmineEnv.execute();
 }
 
 /******************************************************************************/
 
 function runManualTests() {
   setTitle('Manual Tests');
-  createButton('Back', function() { setMode('main'); });
+
+  createActionButton('Reset App', location.reload.bind(location));
+  createActionButton('Back', setMode.bind(null, 'main'));
 
   var contentEl = document.getElementById('content');
-  var test = cordova.require('org.apache.cordova.test-framework.test');
-  test.initForManualTests();
+  var beforeEach = function() {
+    window.clearContent();
+    createActionButton('Reset App', location.reload.bind(location));
+    createActionButton('Back', setMode.bind(null, 'manual'));
+  }
+  window.defineManualTests(contentEl, beforeEach, createActionButton);
 }
 
 /******************************************************************************/
 
-function runUnknownMode() {
-  setTitle('Unknown Mode');
-  createButton('Reset', function() { setMode('main'); });
+function runMain() {
+  setTitle('Cordova Tests');
+
+  createActionButton('Auto Tests', setMode.bind(null, 'auto'));
+  createActionButton('Manual Tests', setMode.bind(null, 'manual'));
+  createActionButton('Reset App', location.reload.bind(location));
 }
 
 /******************************************************************************/
 
-document.addEventListener("DOMContentLoaded", function() {
-});
-
 document.addEventListener("deviceready", function() {
-  var contentEl = document.getElementById('content');
-  var test = cordova.require('org.apache.cordova.test-framework.test');
-  test.init(contentEl, createButton, logger);
-
-  var mode = getMode();
-  if (mode === 'main')
-    runMain();
-  else if (mode === 'autotests')
-    runAutoTests();
-  else if (mode === 'manualtests')
-    runManualTests();
-  else
-    runUnknownMode();
+  window.medic.load(function() {
+    if (window.medic.enabled) {
+      setMode('auto');
+    } else {
+      getMode(setMode);
+    }
+  });
 });
 
 /******************************************************************************/

http://git-wip-us.apache.org/repos/asf/cordova-labs/blob/908dd288/cordova-app-test-harness/www/medic.js
----------------------------------------------------------------------
diff --git a/cordova-app-test-harness/www/medic.js b/cordova-app-test-harness/www/medic.js
new file mode 100644
index 0000000..4a1791b
--- /dev/null
+++ b/cordova-app-test-harness/www/medic.js
@@ -0,0 +1,38 @@
+(function() {
+
+'use strict';
+
+var exports = window.medic = {};
+
+exports.logurl = 'http://127.0.0.1:7800';
+
+exports.enabled = false;
+
+exports.log = function() {
+  if (!window.medic.enabled)
+    return;
+  var xhr = new XMLHttpRequest();
+  xhr.open("POST", exports.logurl, true);
+  xhr.setRequestHeader("Content-Type", "text/plain");
+  xhr.send(Array.prototype.slice.apply(arguments));
+};
+
+exports.load = function (callback) {
+  var xhr = new XMLHttpRequest();
+  xhr.open("GET", "medic.json", true);
+  xhr.onload = function() {
+    if (xhr.readyState == 4 && xhr.status == 200) {
+      var cfg = JSON.parse(xhr.responseText);
+      exports.logurl = cfg.logurl;
+      exports.enabled = true;
+      console.log('Loaded Medic Config: logurl=' + exports.logurl);
+    }
+    callback();
+  }
+  xhr.onerror = function() {
+   callback();
+  }
+  xhr.send();
+}
+
+}());

http://git-wip-us.apache.org/repos/asf/cordova-labs/blob/908dd288/cordova-app-test-harness/www/tests.js
----------------------------------------------------------------------
diff --git a/cordova-app-test-harness/www/tests.js b/cordova-app-test-harness/www/tests.js
new file mode 100644
index 0000000..a5421d9
--- /dev/null
+++ b/cordova-app-test-harness/www/tests.js
@@ -0,0 +1,64 @@
+(function() {
+
+'use strict';
+
+var exports = window;
+
+exports.tests = Object.create(null);
+
+function getTestsObject(api) {
+  return window.tests[api] = window.tests[api] || { enabled: true };
+}
+
+// Usage:
+// registerAutoTests('apiName', function() {
+//   define('foo', function() {
+//     .. jasmine tests ..
+//   });
+// });
+exports.registerAutoTests = function(api, fn) {
+  var apiTests = getTestsObject(api);
+  apiTests.defineAutoTests = function(jasmineInterface) {
+    jasmineInterface.describe(api + ' >>', function() {
+      fn(jasmineInterface); // Note: don't pass fn directly to jasmine.describe, since describe does async magic if fn takes an arg
+    });
+  };
+};
+
+exports.defineAutoTests = function(jasmineInterface) {
+  Object.keys(exports.tests).forEach(function(key) {
+    if (!exports.tests[key].enabled)
+      return;
+    if (!exports.tests[key].hasOwnProperty('defineAutoTests'))
+      return;
+    exports.tests[key].defineAutoTests(jasmineInterface);
+  });
+};
+
+// Usage:
+// registerManualTests('apiName', function(contentEl, addButton) {
+//   .. setup ..
+//   addButton('Test Description', function() { ... });
+//   addButton('Test 2', function() { ... });
+// });
+exports.registerManualTests = function(api, fn) {
+  var apiTests = getTestsObject(api);
+  apiTests.defineManualTests = function(contentEl, addButton) {
+    fn(contentEl, addButton);
+  };
+}
+
+exports.defineManualTests = function(contentEl, beforeEach, createActionButton) {
+  Object.keys(exports.tests).forEach(function(key) {
+    if (!exports.tests[key].enabled)
+      return;
+    if (!exports.tests[key].hasOwnProperty('defineManualTests'))
+      return;
+    createActionButton(key, function() {
+      beforeEach();
+      exports.tests[key].defineManualTests(contentEl, createActionButton);
+    });
+  });
+};
+
+}());