[gjs/wip/ptomato/tests: 12/12] WIP - minijasmine



commit 732c7a35815d9c645ae225fad4cdf81b5f2cf305
Author: Philip Chimento <philip chimento gmail com>
Date:   Wed Oct 26 00:59:41 2016 -0700

    WIP - minijasmine

 Makefile-insttest.am                     |    2 +-
 Makefile-test.am                         |   35 +-
 Makefile.am                              |    2 +-
 installed-tests/js/jasmine.js            | 3655 ++++++++++++++++++++++++++++++
 installed-tests/js/jsunit.gresources.xml |    2 +
 installed-tests/js/minijasmine.js        |  113 +
 installed-tests/js/test0010basic.js      |    7 -
 installed-tests/js/test0020importer.js   |   14 +-
 installed-tests/js/test0030basicBoxed.js |   15 +-
 installed-tests/js/test0040mainloop.js   |   28 +-
 installed-tests/js/testByteArray.js      |  230 +-
 installed-tests/js/testClass.js          |  199 +-
 installed-tests/js/testCoverage.js       | 2565 +++++++++------------
 installed-tests/js/testself.js           |   59 +-
 installed-tests/minijasmine.cpp          |  143 ++
 15 files changed, 5304 insertions(+), 1765 deletions(-)
---
diff --git a/Makefile-insttest.am b/Makefile-insttest.am
index cd53df7..2c179bb 100644
--- a/Makefile-insttest.am
+++ b/Makefile-insttest.am
@@ -20,7 +20,7 @@ pkglib_LTLIBRARIES =
 
 if BUILDOPT_INSTALL_TESTS
 
-gjsinsttest_PROGRAMS += jsunit
+## gjsinsttest_PROGRAMS += jsunit
 gjsinsttest_DATA += $(TEST_INTROSPECTION_TYPELIBS)
 installedtestmeta_DATA += jsunit.test testSystemExit.test
 jstests_DATA += $(common_jstests_files)
diff --git a/Makefile-test.am b/Makefile-test.am
index 7b7f042..efcc2ba 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -107,7 +107,7 @@ CLEANFILES +=                                               \
 # as well as installed if --enable-installed-tests is given at configure time.
 # See Makefile-insttest.am for the build rules installing the tests.
 
-check_PROGRAMS += gjs-tests jsunit
+check_PROGRAMS += gjs-tests minijasmine
 
 gjs_tests_CPPFLAGS =                           \
        $(AM_CPPFLAGS)                          \
@@ -134,23 +134,20 @@ gjs_tests_DEPENDENCIES =                          \
        mock-cache-invalidation-after.gresource         \
        $(NULL)
 
-jsunit_CPPFLAGS =                              \
+minijasmine_SOURCES =                  \
+       installed-tests/minijasmine.cpp \
+       jsunit-resources.c              \
+       jsunit-resources.h              \
+       $(NULL)
+
+minijasmine_CPPFLAGS =                         \
        $(AM_CPPFLAGS)                          \
        $(GJS_CFLAGS)                           \
-       -DPKGLIBDIR=\"$(pkglibdir)\"            \
-       -DINSTTESTDIR=\"$(gjsinsttestdir)\"     \
        -I$(top_srcdir)                         \
+       -DINSTTESTDIR=\"$(gjsinsttestdir)\"     \
        $(NULL)
 
-jsunit_LDADD = $(GJS_LIBS) libgjs.la
-
-jsunit_LDFLAGS = -rpath $(pkglibdir)
-
-jsunit_SOURCES = \
-       installed-tests/gjs-unit.cpp    \
-       jsunit-resources.c              \
-       jsunit-resources.h              \
-       $(NULL)
+minijasmine_LDADD = $(GJS_LIBS) libgjs.la
 
 ### TEST GIRS ##########################################################
 
@@ -243,7 +240,6 @@ CLEANFILES += $(TEST_INTROSPECTION_GIRS) $(TEST_INTROSPECTION_TYPELIBS)
 ### JAVASCRIPT TESTS ###################################################
 
 common_jstests_files =                                         \
-       installed-tests/js/test0010basic.js                     \
        installed-tests/js/test0020importer.js                  \
        installed-tests/js/test0030basicBoxed.js                \
        installed-tests/js/test0040mainloop.js                  \
@@ -307,7 +303,13 @@ AM_TESTS_ENVIRONMENT =                                             \
 
 simple_tests =                                         \
        test/testCommandLine.sh                         \
-       installed-tests/scripts/testSystemExit.js       \
+       installed-tests/js/test0020importer.js          \
+       installed-tests/js/test0030basicBoxed.js        \
+       installed-tests/js/test0040mainloop.js          \
+       installed-tests/js/testself.js                  \
+       installed-tests/js/testByteArray.js             \
+       installed-tests/js/testClass.js                 \
+       installed-tests/js/testCoverage.js              \
        $(NULL)
 EXTRA_DIST += $(simple_tests)
 TESTS += $(simple_tests)
@@ -316,7 +318,8 @@ LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
 LOG_COMPILER = $(top_srcdir)/test/run-test
 
 TEST_EXTENSIONS = .js
-JS_LOG_COMPILER = $(top_builddir)/gjs-console
+JS_LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
+JS_LOG_COMPILER = $(top_builddir)/minijasmine
 
 if CODE_COVERAGE_ENABLED
 AM_TESTS_ENVIRONMENT +=                                                \
diff --git a/Makefile.am b/Makefile.am
index f0cee34..332196f 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -14,7 +14,7 @@ CLEANFILES =
 EXTRA_DIST =
 check_PROGRAMS =
 check_LTLIBRARIES =
-TESTS = $(check_PROGRAMS)
+TESTS =
 INTROSPECTION_GIRS =
 ## ACLOCAL_AMFLAGS can be removed for Automake 1.13
 ACLOCAL_AMFLAGS = -I m4
diff --git a/installed-tests/js/jasmine.js b/installed-tests/js/jasmine.js
new file mode 100644
index 0000000..7cab7e0
--- /dev/null
+++ b/installed-tests/js/jasmine.js
@@ -0,0 +1,3655 @@
+/*
+Copyright (c) 2008-2016 Pivotal Labs
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+*/
+var getJasmineRequireObj = (function (jasmineGlobal) {
+  var jasmineRequire;
+
+  if (typeof module !== 'undefined' && module.exports && typeof exports !== 'undefined') {
+    if (typeof global !== 'undefined') {
+      jasmineGlobal = global;
+    } else {
+      jasmineGlobal = {};
+    }
+    jasmineRequire = exports;
+  } else {
+    if (typeof window !== 'undefined' && typeof window.toString === 'function' && window.toString() === 
'[object GjsGlobal]') {
+      jasmineGlobal = window;
+    }
+    jasmineRequire = jasmineGlobal.jasmineRequire = jasmineGlobal.jasmineRequire || {};
+  }
+
+  function getJasmineRequire() {
+    return jasmineRequire;
+  }
+
+  getJasmineRequire().core = function(jRequire) {
+    var j$ = {};
+
+    jRequire.base(j$, jasmineGlobal);
+    j$.util = jRequire.util();
+    j$.errors = jRequire.errors();
+    j$.formatErrorMsg = jRequire.formatErrorMsg();
+    j$.Any = jRequire.Any(j$);
+    j$.Anything = jRequire.Anything(j$);
+    j$.CallTracker = jRequire.CallTracker(j$);
+    j$.MockDate = jRequire.MockDate();
+    j$.Clock = jRequire.Clock();
+    j$.DelayedFunctionScheduler = jRequire.DelayedFunctionScheduler();
+    j$.Env = jRequire.Env(j$);
+    j$.ExceptionFormatter = jRequire.ExceptionFormatter();
+    j$.Expectation = jRequire.Expectation();
+    j$.buildExpectationResult = jRequire.buildExpectationResult();
+    j$.JsApiReporter = jRequire.JsApiReporter();
+    j$.matchersUtil = jRequire.matchersUtil(j$);
+    j$.ObjectContaining = jRequire.ObjectContaining(j$);
+    j$.ArrayContaining = jRequire.ArrayContaining(j$);
+    j$.pp = jRequire.pp(j$);
+    j$.QueueRunner = jRequire.QueueRunner(j$);
+    j$.ReportDispatcher = jRequire.ReportDispatcher();
+    j$.Spec = jRequire.Spec(j$);
+    j$.SpyRegistry = jRequire.SpyRegistry(j$);
+    j$.SpyStrategy = jRequire.SpyStrategy(j$);
+    j$.StringMatching = jRequire.StringMatching(j$);
+    j$.Suite = jRequire.Suite(j$);
+    j$.Timer = jRequire.Timer();
+    j$.TreeProcessor = jRequire.TreeProcessor();
+    j$.version = jRequire.version();
+    j$.Order = jRequire.Order();
+
+    j$.matchers = jRequire.requireMatchers(jRequire, j$);
+
+    return j$;
+  };
+
+  return getJasmineRequire;
+})(this);
+
+getJasmineRequireObj().requireMatchers = function(jRequire, j$) {
+  var availableMatchers = [
+      'toBe',
+      'toBeCloseTo',
+      'toBeDefined',
+      'toBeFalsy',
+      'toBeGreaterThan',
+      'toBeGreaterThanOrEqual',
+      'toBeLessThanOrEqual',
+      'toBeLessThan',
+      'toBeNaN',
+      'toBeNull',
+      'toBeTruthy',
+      'toBeUndefined',
+      'toContain',
+      'toEqual',
+      'toHaveBeenCalled',
+      'toHaveBeenCalledWith',
+      'toHaveBeenCalledTimes',
+      'toMatch',
+      'toThrow',
+      'toThrowError'
+    ],
+    matchers = {};
+
+  for (var i = 0; i < availableMatchers.length; i++) {
+    var name = availableMatchers[i];
+    matchers[name] = jRequire[name](j$);
+  }
+
+  return matchers;
+};
+
+getJasmineRequireObj().base = function(j$, jasmineGlobal) {
+  j$.unimplementedMethod_ = function() {
+    throw new Error('unimplemented method');
+  };
+
+  j$.MAX_PRETTY_PRINT_DEPTH = 40;
+  j$.MAX_PRETTY_PRINT_ARRAY_LENGTH = 100;
+  j$.DEFAULT_TIMEOUT_INTERVAL = 5000;
+
+  j$.getGlobal = function() {
+    return jasmineGlobal;
+  };
+
+  j$.getEnv = function(options) {
+    var env = j$.currentEnv_ = j$.currentEnv_ || new j$.Env(options);
+    //jasmine. singletons in here (setTimeout blah blah).
+    return env;
+  };
+
+  j$.isArray_ = function(value) {
+    return j$.isA_('Array', value);
+  };
+
+  j$.isString_ = function(value) {
+    return j$.isA_('String', value);
+  };
+
+  j$.isNumber_ = function(value) {
+    return j$.isA_('Number', value);
+  };
+
+  j$.isFunction_ = function(value) {
+    return j$.isA_('Function', value);
+  };
+
+  j$.isA_ = function(typeName, value) {
+    return Object.prototype.toString.apply(value) === '[object ' + typeName + ']';
+  };
+
+  j$.isDomNode = function(obj) {
+    return obj.nodeType > 0;
+  };
+
+  j$.fnNameFor = function(func) {
+    if (func.name) {
+      return func.name;
+    }
+
+    var matches = func.toString().match(/^\s*function\s*(\w*)\s*\(/);
+    return matches ? matches[1] : '<anonymous>';
+  };
+
+  j$.any = function(clazz) {
+    return new j$.Any(clazz);
+  };
+
+  j$.anything = function() {
+    return new j$.Anything();
+  };
+
+  j$.objectContaining = function(sample) {
+    return new j$.ObjectContaining(sample);
+  };
+
+  j$.stringMatching = function(expected) {
+    return new j$.StringMatching(expected);
+  };
+
+  j$.arrayContaining = function(sample) {
+    return new j$.ArrayContaining(sample);
+  };
+
+  j$.createSpy = function(name, originalFn) {
+
+    var spyStrategy = new j$.SpyStrategy({
+        name: name,
+        fn: originalFn,
+        getSpy: function() { return spy; }
+      }),
+      callTracker = new j$.CallTracker(),
+      spy = function() {
+        var callData = {
+          object: this,
+          args: Array.prototype.slice.apply(arguments)
+        };
+
+        callTracker.track(callData);
+        var returnValue = spyStrategy.exec.apply(this, arguments);
+        callData.returnValue = returnValue;
+
+        return returnValue;
+      };
+
+    for (var prop in originalFn) {
+      if (prop === 'and' || prop === 'calls') {
+        throw new Error('Jasmine spies would overwrite the \'and\' and \'calls\' properties on the object 
being spied upon');
+      }
+
+      spy[prop] = originalFn[prop];
+    }
+
+    spy.and = spyStrategy;
+    spy.calls = callTracker;
+
+    return spy;
+  };
+
+  j$.isSpy = function(putativeSpy) {
+    if (!putativeSpy) {
+      return false;
+    }
+    return putativeSpy.and instanceof j$.SpyStrategy &&
+      putativeSpy.calls instanceof j$.CallTracker;
+  };
+
+  j$.createSpyObj = function(baseName, methodNames) {
+    if (j$.isArray_(baseName) && j$.util.isUndefined(methodNames)) {
+      methodNames = baseName;
+      baseName = 'unknown';
+    }
+
+    if (!j$.isArray_(methodNames) || methodNames.length === 0) {
+      throw 'createSpyObj requires a non-empty array of method names to create spies for';
+    }
+    var obj = {};
+    for (var i = 0; i < methodNames.length; i++) {
+      obj[methodNames[i]] = j$.createSpy(baseName + '.' + methodNames[i]);
+    }
+    return obj;
+  };
+};
+
+getJasmineRequireObj().util = function() {
+
+  var util = {};
+
+  util.inherit = function(childClass, parentClass) {
+    var Subclass = function() {
+    };
+    Subclass.prototype = parentClass.prototype;
+    childClass.prototype = new Subclass();
+  };
+
+  util.htmlEscape = function(str) {
+    if (!str) {
+      return str;
+    }
+    return str.replace(/&/g, '&amp;')
+      .replace(/</g, '&lt;')
+      .replace(/>/g, '&gt;');
+  };
+
+  util.argsToArray = function(args) {
+    var arrayOfArgs = [];
+    for (var i = 0; i < args.length; i++) {
+      arrayOfArgs.push(args[i]);
+    }
+    return arrayOfArgs;
+  };
+
+  util.isUndefined = function(obj) {
+    return obj === void 0;
+  };
+
+  util.arrayContains = function(array, search) {
+    var i = array.length;
+    while (i--) {
+      if (array[i] === search) {
+        return true;
+      }
+    }
+    return false;
+  };
+
+  util.clone = function(obj) {
+    if (Object.prototype.toString.apply(obj) === '[object Array]') {
+      return obj.slice();
+    }
+
+    var cloned = {};
+    for (var prop in obj) {
+      if (obj.hasOwnProperty(prop)) {
+        cloned[prop] = obj[prop];
+      }
+    }
+
+    return cloned;
+  };
+
+  return util;
+};
+
+getJasmineRequireObj().Spec = function(j$) {
+  function Spec(attrs) {
+    this.expectationFactory = attrs.expectationFactory;
+    this.resultCallback = attrs.resultCallback || function() {};
+    this.id = attrs.id;
+    this.description = attrs.description || '';
+    this.queueableFn = attrs.queueableFn;
+    this.beforeAndAfterFns = attrs.beforeAndAfterFns || function() { return {befores: [], afters: []}; };
+    this.userContext = attrs.userContext || function() { return {}; };
+    this.onStart = attrs.onStart || function() {};
+    this.getSpecName = attrs.getSpecName || function() { return ''; };
+    this.expectationResultFactory = attrs.expectationResultFactory || function() { };
+    this.queueRunnerFactory = attrs.queueRunnerFactory || function() {};
+    this.catchingExceptions = attrs.catchingExceptions || function() { return true; };
+    this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
+
+    if (!this.queueableFn.fn) {
+      this.pend();
+    }
+
+    this.result = {
+      id: this.id,
+      description: this.description,
+      fullName: this.getFullName(),
+      failedExpectations: [],
+      passedExpectations: [],
+      pendingReason: ''
+    };
+  }
+
+  Spec.prototype.addExpectationResult = function(passed, data, isError) {
+    var expectationResult = this.expectationResultFactory(data);
+    if (passed) {
+      this.result.passedExpectations.push(expectationResult);
+    } else {
+      this.result.failedExpectations.push(expectationResult);
+
+      if (this.throwOnExpectationFailure && !isError) {
+        throw new j$.errors.ExpectationFailed();
+      }
+    }
+  };
+
+  Spec.prototype.expect = function(actual) {
+    return this.expectationFactory(actual, this);
+  };
+
+  Spec.prototype.execute = function(onComplete, enabled) {
+    var self = this;
+
+    this.onStart(this);
+
+    if (!this.isExecutable() || this.markedPending || enabled === false) {
+      complete(enabled);
+      return;
+    }
+
+    var fns = this.beforeAndAfterFns();
+    var allFns = fns.befores.concat(this.queueableFn).concat(fns.afters);
+
+    this.queueRunnerFactory({
+      queueableFns: allFns,
+      onException: function() { self.onException.apply(self, arguments); },
+      onComplete: complete,
+      userContext: this.userContext()
+    });
+
+    function complete(enabledAgain) {
+      self.result.status = self.status(enabledAgain);
+      self.resultCallback(self.result);
+
+      if (onComplete) {
+        onComplete();
+      }
+    }
+  };
+
+  Spec.prototype.onException = function onException(e) {
+    if (Spec.isPendingSpecException(e)) {
+      this.pend(extractCustomPendingMessage(e));
+      return;
+    }
+
+    if (e instanceof j$.errors.ExpectationFailed) {
+      return;
+    }
+
+    this.addExpectationResult(false, {
+      matcherName: '',
+      passed: false,
+      expected: '',
+      actual: '',
+      error: e
+    }, true);
+  };
+
+  Spec.prototype.disable = function() {
+    this.disabled = true;
+  };
+
+  Spec.prototype.pend = function(message) {
+    this.markedPending = true;
+    if (message) {
+      this.result.pendingReason = message;
+    }
+  };
+
+  Spec.prototype.getResult = function() {
+    this.result.status = this.status();
+    return this.result;
+  };
+
+  Spec.prototype.status = function(enabled) {
+    if (this.disabled || enabled === false) {
+      return 'disabled';
+    }
+
+    if (this.markedPending) {
+      return 'pending';
+    }
+
+    if (this.result.failedExpectations.length > 0) {
+      return 'failed';
+    } else {
+      return 'passed';
+    }
+  };
+
+  Spec.prototype.isExecutable = function() {
+    return !this.disabled;
+  };
+
+  Spec.prototype.getFullName = function() {
+    return this.getSpecName(this);
+  };
+
+  var extractCustomPendingMessage = function(e) {
+    var fullMessage = e.toString(),
+        boilerplateStart = fullMessage.indexOf(Spec.pendingSpecExceptionMessage),
+        boilerplateEnd = boilerplateStart + Spec.pendingSpecExceptionMessage.length;
+
+    return fullMessage.substr(boilerplateEnd);
+  };
+
+  Spec.pendingSpecExceptionMessage = '=> marked Pending';
+
+  Spec.isPendingSpecException = function(e) {
+    return !!(e && e.toString && e.toString().indexOf(Spec.pendingSpecExceptionMessage) !== -1);
+  };
+
+  return Spec;
+};
+
+if (typeof window == void 0 && typeof exports == 'object') {
+  exports.Spec = jasmineRequire.Spec;
+}
+
+/*jshint bitwise: false*/
+
+getJasmineRequireObj().Order = function() {
+  function Order(options) {
+    this.random = 'random' in options ? options.random : true;
+    var seed = this.seed = options.seed || generateSeed();
+    this.sort = this.random ? randomOrder : naturalOrder;
+
+    function naturalOrder(items) {
+      return items;
+    }
+
+    function randomOrder(items) {
+      var copy = items.slice();
+      copy.sort(function(a, b) {
+        return jenkinsHash(seed + a.id) - jenkinsHash(seed + b.id);
+      });
+      return copy;
+    }
+
+    function generateSeed() {
+      return String(Math.random()).slice(-5);
+    }
+
+    // Bob Jenkins One-at-a-Time Hash algorithm is a non-cryptographic hash function
+    // used to get a different output when the key changes slighly.
+    // We use your return to sort the children randomly in a consistent way when
+    // used in conjunction with a seed
+
+    function jenkinsHash(key) {
+      var hash, i;
+      for(hash = i = 0; i < key.length; ++i) {
+        hash += key.charCodeAt(i);
+        hash += (hash << 10);
+        hash ^= (hash >> 6);
+      }
+      hash += (hash << 3);
+      hash ^= (hash >> 11);
+      hash += (hash << 15);
+      return hash;
+    }
+
+  }
+
+  return Order;
+};
+
+getJasmineRequireObj().Env = function(j$) {
+  function Env(options) {
+    options = options || {};
+
+    var self = this;
+    var global = options.global || j$.getGlobal();
+
+    var totalSpecsDefined = 0;
+
+    var catchExceptions = true;
+
+    var realSetTimeout = j$.getGlobal().setTimeout;
+    var realClearTimeout = j$.getGlobal().clearTimeout;
+    this.clock = new j$.Clock(global, function () { return new j$.DelayedFunctionScheduler(); }, new 
j$.MockDate(global));
+
+    var runnableResources = {};
+
+    var currentSpec = null;
+    var currentlyExecutingSuites = [];
+    var currentDeclarationSuite = null;
+    var throwOnExpectationFailure = false;
+    var random = false;
+    var seed = null;
+
+    var currentSuite = function() {
+      return currentlyExecutingSuites[currentlyExecutingSuites.length - 1];
+    };
+
+    var currentRunnable = function() {
+      return currentSpec || currentSuite();
+    };
+
+    var reporter = new j$.ReportDispatcher([
+      'jasmineStarted',
+      'jasmineDone',
+      'suiteStarted',
+      'suiteDone',
+      'specStarted',
+      'specDone'
+    ]);
+
+    this.specFilter = function() {
+      return true;
+    };
+
+    this.addCustomEqualityTester = function(tester) {
+      if(!currentRunnable()) {
+        throw new Error('Custom Equalities must be added in a before function or a spec');
+      }
+      runnableResources[currentRunnable().id].customEqualityTesters.push(tester);
+    };
+
+    this.addMatchers = function(matchersToAdd) {
+      if(!currentRunnable()) {
+        throw new Error('Matchers must be added in a before function or a spec');
+      }
+      var customMatchers = runnableResources[currentRunnable().id].customMatchers;
+      for (var matcherName in matchersToAdd) {
+        customMatchers[matcherName] = matchersToAdd[matcherName];
+      }
+    };
+
+    j$.Expectation.addCoreMatchers(j$.matchers);
+
+    var nextSpecId = 0;
+    var getNextSpecId = function() {
+      return 'spec' + nextSpecId++;
+    };
+
+    var nextSuiteId = 0;
+    var getNextSuiteId = function() {
+      return 'suite' + nextSuiteId++;
+    };
+
+    var expectationFactory = function(actual, spec) {
+      return j$.Expectation.Factory({
+        util: j$.matchersUtil,
+        customEqualityTesters: runnableResources[spec.id].customEqualityTesters,
+        customMatchers: runnableResources[spec.id].customMatchers,
+        actual: actual,
+        addExpectationResult: addExpectationResult
+      });
+
+      function addExpectationResult(passed, result) {
+        return spec.addExpectationResult(passed, result);
+      }
+    };
+
+    var defaultResourcesForRunnable = function(id, parentRunnableId) {
+      var resources = {spies: [], customEqualityTesters: [], customMatchers: {}};
+
+      if(runnableResources[parentRunnableId]){
+        resources.customEqualityTesters = 
j$.util.clone(runnableResources[parentRunnableId].customEqualityTesters);
+        resources.customMatchers = j$.util.clone(runnableResources[parentRunnableId].customMatchers);
+      }
+
+      runnableResources[id] = resources;
+    };
+
+    var clearResourcesForRunnable = function(id) {
+        spyRegistry.clearSpies();
+        delete runnableResources[id];
+    };
+
+    var beforeAndAfterFns = function(suite) {
+      return function() {
+        var befores = [],
+          afters = [];
+
+        while(suite) {
+          befores = befores.concat(suite.beforeFns);
+          afters = afters.concat(suite.afterFns);
+
+          suite = suite.parentSuite;
+        }
+
+        return {
+          befores: befores.reverse(),
+          afters: afters
+        };
+      };
+    };
+
+    var getSpecName = function(spec, suite) {
+      var fullName = [spec.description],
+          suiteFullName = suite.getFullName();
+
+      if (suiteFullName !== '') {
+        fullName.unshift(suiteFullName);
+      }
+      return fullName.join(' ');
+    };
+
+    // TODO: we may just be able to pass in the fn instead of wrapping here
+    var buildExpectationResult = j$.buildExpectationResult,
+        exceptionFormatter = new j$.ExceptionFormatter(),
+        expectationResultFactory = function(attrs) {
+          attrs.messageFormatter = exceptionFormatter.message;
+          attrs.stackFormatter = exceptionFormatter.stack;
+
+          return buildExpectationResult(attrs);
+        };
+
+    // TODO: fix this naming, and here's where the value comes in
+    this.catchExceptions = function(value) {
+      catchExceptions = !!value;
+      return catchExceptions;
+    };
+
+    this.catchingExceptions = function() {
+      return catchExceptions;
+    };
+
+    var maximumSpecCallbackDepth = 20;
+    var currentSpecCallbackDepth = 0;
+
+    function clearStack(fn) {
+      currentSpecCallbackDepth++;
+      if (currentSpecCallbackDepth >= maximumSpecCallbackDepth) {
+        currentSpecCallbackDepth = 0;
+        realSetTimeout(fn, 0);
+      } else {
+        fn();
+      }
+    }
+
+    var catchException = function(e) {
+      return j$.Spec.isPendingSpecException(e) || catchExceptions;
+    };
+
+    this.throwOnExpectationFailure = function(value) {
+      throwOnExpectationFailure = !!value;
+    };
+
+    this.throwingExpectationFailures = function() {
+      return throwOnExpectationFailure;
+    };
+
+    this.randomizeTests = function(value) {
+      random = !!value;
+    };
+
+    this.randomTests = function() {
+      return random;
+    };
+
+    this.seed = function(value) {
+      if (value) {
+        seed = value;
+      }
+      return seed;
+    };
+
+    var queueRunnerFactory = function(options) {
+      options.catchException = catchException;
+      options.clearStack = options.clearStack || clearStack;
+      options.timeout = {setTimeout: realSetTimeout, clearTimeout: realClearTimeout};
+      options.fail = self.fail;
+
+      new j$.QueueRunner(options).execute();
+    };
+
+    var topSuite = new j$.Suite({
+      env: this,
+      id: getNextSuiteId(),
+      description: 'Jasmine__TopLevel__Suite',
+      expectationFactory: expectationFactory,
+      expectationResultFactory: expectationResultFactory
+    });
+    defaultResourcesForRunnable(topSuite.id);
+    currentDeclarationSuite = topSuite;
+
+    this.topSuite = function() {
+      return topSuite;
+    };
+
+    this.execute = function(runnablesToRun) {
+      if(!runnablesToRun) {
+        if (focusedRunnables.length) {
+          runnablesToRun = focusedRunnables;
+        } else {
+          runnablesToRun = [topSuite.id];
+        }
+      }
+
+      var order = new j$.Order({
+        random: random,
+        seed: seed
+      });
+
+      var processor = new j$.TreeProcessor({
+        tree: topSuite,
+        runnableIds: runnablesToRun,
+        queueRunnerFactory: queueRunnerFactory,
+        nodeStart: function(suite) {
+          currentlyExecutingSuites.push(suite);
+          defaultResourcesForRunnable(suite.id, suite.parentSuite.id);
+          reporter.suiteStarted(suite.result);
+        },
+        nodeComplete: function(suite, result) {
+          if (!suite.disabled) {
+            clearResourcesForRunnable(suite.id);
+          }
+          currentlyExecutingSuites.pop();
+          reporter.suiteDone(result);
+        },
+        orderChildren: function(node) {
+          return order.sort(node.children);
+        }
+      });
+
+      if(!processor.processTree().valid) {
+        throw new Error('Invalid order: would cause a beforeAll or afterAll to be run multiple times');
+      }
+
+      reporter.jasmineStarted({
+        totalSpecsDefined: totalSpecsDefined
+      });
+
+      currentlyExecutingSuites.push(topSuite);
+
+      processor.execute(function() {
+        clearResourcesForRunnable(topSuite.id);
+        currentlyExecutingSuites.pop();
+
+        reporter.jasmineDone({
+          order: order,
+          failedExpectations: topSuite.result.failedExpectations
+        });
+      });
+    };
+
+    this.addReporter = function(reporterToAdd) {
+      reporter.addReporter(reporterToAdd);
+    };
+
+    this.provideFallbackReporter = function(reporterToAdd) {
+      reporter.provideFallbackReporter(reporterToAdd);
+    };
+
+    this.clearReporters = function() {
+      reporter.clearReporters();
+    };
+
+    var spyRegistry = new j$.SpyRegistry({currentSpies: function() {
+      if(!currentRunnable()) {
+        throw new Error('Spies must be created in a before function or a spec');
+      }
+      return runnableResources[currentRunnable().id].spies;
+    }});
+
+    this.allowRespy = function(allow){
+      spyRegistry.allowRespy(allow);
+    };
+
+    this.spyOn = function() {
+      return spyRegistry.spyOn.apply(spyRegistry, arguments);
+    };
+
+    var suiteFactory = function(description) {
+      var suite = new j$.Suite({
+        env: self,
+        id: getNextSuiteId(),
+        description: description,
+        parentSuite: currentDeclarationSuite,
+        expectationFactory: expectationFactory,
+        expectationResultFactory: expectationResultFactory,
+        throwOnExpectationFailure: throwOnExpectationFailure
+      });
+
+      return suite;
+    };
+
+    this.describe = function(description, specDefinitions) {
+      var suite = suiteFactory(description);
+      if (specDefinitions.length > 0) {
+        throw new Error('describe does not expect any arguments');
+      }
+      if (currentDeclarationSuite.markedPending) {
+        suite.pend();
+      }
+      addSpecsToSuite(suite, specDefinitions);
+      return suite;
+    };
+
+    this.xdescribe = function(description, specDefinitions) {
+      var suite = suiteFactory(description);
+      suite.pend();
+      addSpecsToSuite(suite, specDefinitions);
+      return suite;
+    };
+
+    var focusedRunnables = [];
+
+    this.fdescribe = function(description, specDefinitions) {
+      var suite = suiteFactory(description);
+      suite.isFocused = true;
+
+      focusedRunnables.push(suite.id);
+      unfocusAncestor();
+      addSpecsToSuite(suite, specDefinitions);
+
+      return suite;
+    };
+
+    function addSpecsToSuite(suite, specDefinitions) {
+      var parentSuite = currentDeclarationSuite;
+      parentSuite.addChild(suite);
+      currentDeclarationSuite = suite;
+
+      var declarationError = null;
+      try {
+        specDefinitions.call(suite);
+      } catch (e) {
+        declarationError = e;
+      }
+
+      if (declarationError) {
+        self.it('encountered a declaration exception', function() {
+          throw declarationError;
+        });
+      }
+
+      currentDeclarationSuite = parentSuite;
+    }
+
+    function findFocusedAncestor(suite) {
+      while (suite) {
+        if (suite.isFocused) {
+          return suite.id;
+        }
+        suite = suite.parentSuite;
+      }
+
+      return null;
+    }
+
+    function unfocusAncestor() {
+      var focusedAncestor = findFocusedAncestor(currentDeclarationSuite);
+      if (focusedAncestor) {
+        for (var i = 0; i < focusedRunnables.length; i++) {
+          if (focusedRunnables[i] === focusedAncestor) {
+            focusedRunnables.splice(i, 1);
+            break;
+          }
+        }
+      }
+    }
+
+    var specFactory = function(description, fn, suite, timeout) {
+      totalSpecsDefined++;
+      var spec = new j$.Spec({
+        id: getNextSpecId(),
+        beforeAndAfterFns: beforeAndAfterFns(suite),
+        expectationFactory: expectationFactory,
+        resultCallback: specResultCallback,
+        getSpecName: function(spec) {
+          return getSpecName(spec, suite);
+        },
+        onStart: specStarted,
+        description: description,
+        expectationResultFactory: expectationResultFactory,
+        queueRunnerFactory: queueRunnerFactory,
+        userContext: function() { return suite.clonedSharedUserContext(); },
+        queueableFn: {
+          fn: fn,
+          timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+        },
+        throwOnExpectationFailure: throwOnExpectationFailure
+      });
+
+      if (!self.specFilter(spec)) {
+        spec.disable();
+      }
+
+      return spec;
+
+      function specResultCallback(result) {
+        clearResourcesForRunnable(spec.id);
+        currentSpec = null;
+        reporter.specDone(result);
+      }
+
+      function specStarted(spec) {
+        currentSpec = spec;
+        defaultResourcesForRunnable(spec.id, suite.id);
+        reporter.specStarted(spec.result);
+      }
+    };
+
+    this.it = function(description, fn, timeout) {
+      var spec = specFactory(description, fn, currentDeclarationSuite, timeout);
+      if (currentDeclarationSuite.markedPending) {
+        spec.pend();
+      }
+      currentDeclarationSuite.addChild(spec);
+      return spec;
+    };
+
+    this.xit = function() {
+      var spec = this.it.apply(this, arguments);
+      spec.pend('Temporarily disabled with xit');
+      return spec;
+    };
+
+    this.fit = function(description, fn, timeout){
+      var spec = specFactory(description, fn, currentDeclarationSuite, timeout);
+      currentDeclarationSuite.addChild(spec);
+      focusedRunnables.push(spec.id);
+      unfocusAncestor();
+      return spec;
+    };
+
+    this.expect = function(actual) {
+      if (!currentRunnable()) {
+        throw new Error('\'expect\' was used when there was no current spec, this could be because an 
asynchronous test timed out');
+      }
+
+      return currentRunnable().expect(actual);
+    };
+
+    this.beforeEach = function(beforeEachFunction, timeout) {
+      currentDeclarationSuite.beforeEach({
+        fn: beforeEachFunction,
+        timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+      });
+    };
+
+    this.beforeAll = function(beforeAllFunction, timeout) {
+      currentDeclarationSuite.beforeAll({
+        fn: beforeAllFunction,
+        timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+      });
+    };
+
+    this.afterEach = function(afterEachFunction, timeout) {
+      currentDeclarationSuite.afterEach({
+        fn: afterEachFunction,
+        timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+      });
+    };
+
+    this.afterAll = function(afterAllFunction, timeout) {
+      currentDeclarationSuite.afterAll({
+        fn: afterAllFunction,
+        timeout: function() { return timeout || j$.DEFAULT_TIMEOUT_INTERVAL; }
+      });
+    };
+
+    this.pending = function(message) {
+      var fullMessage = j$.Spec.pendingSpecExceptionMessage;
+      if(message) {
+        fullMessage += message;
+      }
+      throw fullMessage;
+    };
+
+    this.fail = function(error) {
+      var message = 'Failed';
+      if (error) {
+        message += ': ';
+        message += error.message || error;
+      }
+
+      currentRunnable().addExpectationResult(false, {
+        matcherName: '',
+        passed: false,
+        expected: '',
+        actual: '',
+        message: message,
+        error: error && error.message ? error : null
+      });
+    };
+  }
+
+  return Env;
+};
+
+getJasmineRequireObj().JsApiReporter = function() {
+
+  var noopTimer = {
+    start: function(){},
+    elapsed: function(){ return 0; }
+  };
+
+  function JsApiReporter(options) {
+    var timer = options.timer || noopTimer,
+        status = 'loaded';
+
+    this.started = false;
+    this.finished = false;
+    this.runDetails = {};
+
+    this.jasmineStarted = function() {
+      this.started = true;
+      status = 'started';
+      timer.start();
+    };
+
+    var executionTime;
+
+    this.jasmineDone = function(runDetails) {
+      this.finished = true;
+      this.runDetails = runDetails;
+      executionTime = timer.elapsed();
+      status = 'done';
+    };
+
+    this.status = function() {
+      return status;
+    };
+
+    var suites = [],
+      suites_hash = {};
+
+    this.suiteStarted = function(result) {
+      suites_hash[result.id] = result;
+    };
+
+    this.suiteDone = function(result) {
+      storeSuite(result);
+    };
+
+    this.suiteResults = function(index, length) {
+      return suites.slice(index, index + length);
+    };
+
+    function storeSuite(result) {
+      suites.push(result);
+      suites_hash[result.id] = result;
+    }
+
+    this.suites = function() {
+      return suites_hash;
+    };
+
+    var specs = [];
+
+    this.specDone = function(result) {
+      specs.push(result);
+    };
+
+    this.specResults = function(index, length) {
+      return specs.slice(index, index + length);
+    };
+
+    this.specs = function() {
+      return specs;
+    };
+
+    this.executionTime = function() {
+      return executionTime;
+    };
+
+  }
+
+  return JsApiReporter;
+};
+
+getJasmineRequireObj().CallTracker = function(j$) {
+
+  function CallTracker() {
+    var calls = [];
+    var opts = {};
+
+    function argCloner(context) {
+      var clonedArgs = [];
+      var argsAsArray = j$.util.argsToArray(context.args);
+      for(var i = 0; i < argsAsArray.length; i++) {
+        if(Object.prototype.toString.apply(argsAsArray[i]).match(/^\[object/)) {
+          clonedArgs.push(j$.util.clone(argsAsArray[i]));
+        } else {
+          clonedArgs.push(argsAsArray[i]);
+        }
+      }
+      context.args = clonedArgs;
+    }
+
+    this.track = function(context) {
+      if(opts.cloneArgs) {
+        argCloner(context);
+      }
+      calls.push(context);
+    };
+
+    this.any = function() {
+      return !!calls.length;
+    };
+
+    this.count = function() {
+      return calls.length;
+    };
+
+    this.argsFor = function(index) {
+      var call = calls[index];
+      return call ? call.args : [];
+    };
+
+    this.all = function() {
+      return calls;
+    };
+
+    this.allArgs = function() {
+      var callArgs = [];
+      for(var i = 0; i < calls.length; i++){
+        callArgs.push(calls[i].args);
+      }
+
+      return callArgs;
+    };
+
+    this.first = function() {
+      return calls[0];
+    };
+
+    this.mostRecent = function() {
+      return calls[calls.length - 1];
+    };
+
+    this.reset = function() {
+      calls = [];
+    };
+
+    this.saveArgumentsByValue = function() {
+      opts.cloneArgs = true;
+    };
+
+  }
+
+  return CallTracker;
+};
+
+getJasmineRequireObj().Clock = function() {
+  function Clock(global, delayedFunctionSchedulerFactory, mockDate) {
+    var self = this,
+      realTimingFunctions = {
+        setTimeout: global.setTimeout,
+        clearTimeout: global.clearTimeout,
+        setInterval: global.setInterval,
+        clearInterval: global.clearInterval
+      },
+      fakeTimingFunctions = {
+        setTimeout: setTimeout,
+        clearTimeout: clearTimeout,
+        setInterval: setInterval,
+        clearInterval: clearInterval
+      },
+      installed = false,
+      delayedFunctionScheduler,
+      timer;
+
+
+    self.install = function() {
+      if(!originalTimingFunctionsIntact()) {
+        throw new Error('Jasmine Clock was unable to install over custom global timer functions. Is the 
clock already installed?');
+      }
+      replace(global, fakeTimingFunctions);
+      timer = fakeTimingFunctions;
+      delayedFunctionScheduler = delayedFunctionSchedulerFactory();
+      installed = true;
+
+      return self;
+    };
+
+    self.uninstall = function() {
+      delayedFunctionScheduler = null;
+      mockDate.uninstall();
+      replace(global, realTimingFunctions);
+
+      timer = realTimingFunctions;
+      installed = false;
+    };
+
+    self.withMock = function(closure) {
+      this.install();
+      try {
+        closure();
+      } finally {
+        this.uninstall();
+      }
+    };
+
+    self.mockDate = function(initialDate) {
+      mockDate.install(initialDate);
+    };
+
+    self.setTimeout = function(fn, delay, params) {
+      if (legacyIE()) {
+        if (arguments.length > 2) {
+          throw new Error('IE < 9 cannot support extra params to setTimeout without a polyfill');
+        }
+        return timer.setTimeout(fn, delay);
+      }
+      return Function.prototype.apply.apply(timer.setTimeout, [global, arguments]);
+    };
+
+    self.setInterval = function(fn, delay, params) {
+      if (legacyIE()) {
+        if (arguments.length > 2) {
+          throw new Error('IE < 9 cannot support extra params to setInterval without a polyfill');
+        }
+        return timer.setInterval(fn, delay);
+      }
+      return Function.prototype.apply.apply(timer.setInterval, [global, arguments]);
+    };
+
+    self.clearTimeout = function(id) {
+      return Function.prototype.call.apply(timer.clearTimeout, [global, id]);
+    };
+
+    self.clearInterval = function(id) {
+      return Function.prototype.call.apply(timer.clearInterval, [global, id]);
+    };
+
+    self.tick = function(millis) {
+      if (installed) {
+        delayedFunctionScheduler.tick(millis, function(millis) { mockDate.tick(millis); });
+      } else {
+        throw new Error('Mock clock is not installed, use jasmine.clock().install()');
+      }
+    };
+
+    return self;
+
+    function originalTimingFunctionsIntact() {
+      return global.setTimeout === realTimingFunctions.setTimeout &&
+        global.clearTimeout === realTimingFunctions.clearTimeout &&
+        global.setInterval === realTimingFunctions.setInterval &&
+        global.clearInterval === realTimingFunctions.clearInterval;
+    }
+
+    function legacyIE() {
+      //if these methods are polyfilled, apply will be present
+      return !(realTimingFunctions.setTimeout || realTimingFunctions.setInterval).apply;
+    }
+
+    function replace(dest, source) {
+      for (var prop in source) {
+        dest[prop] = source[prop];
+      }
+    }
+
+    function setTimeout(fn, delay) {
+      return delayedFunctionScheduler.scheduleFunction(fn, delay, argSlice(arguments, 2));
+    }
+
+    function clearTimeout(id) {
+      return delayedFunctionScheduler.removeFunctionWithId(id);
+    }
+
+    function setInterval(fn, interval) {
+      return delayedFunctionScheduler.scheduleFunction(fn, interval, argSlice(arguments, 2), true);
+    }
+
+    function clearInterval(id) {
+      return delayedFunctionScheduler.removeFunctionWithId(id);
+    }
+
+    function argSlice(argsObj, n) {
+      return Array.prototype.slice.call(argsObj, n);
+    }
+  }
+
+  return Clock;
+};
+
+getJasmineRequireObj().DelayedFunctionScheduler = function() {
+  function DelayedFunctionScheduler() {
+    var self = this;
+    var scheduledLookup = [];
+    var scheduledFunctions = {};
+    var currentTime = 0;
+    var delayedFnCount = 0;
+
+    self.tick = function(millis, tickDate) {
+      millis = millis || 0;
+      var endTime = currentTime + millis;
+
+      runScheduledFunctions(endTime, tickDate);
+      currentTime = endTime;
+    };
+
+    self.scheduleFunction = function(funcToCall, millis, params, recurring, timeoutKey, runAtMillis) {
+      var f;
+      if (typeof(funcToCall) === 'string') {
+        /* jshint evil: true */
+        f = function() { return eval(funcToCall); };
+        /* jshint evil: false */
+      } else {
+        f = funcToCall;
+      }
+
+      millis = millis || 0;
+      timeoutKey = timeoutKey || ++delayedFnCount;
+      runAtMillis = runAtMillis || (currentTime + millis);
+
+      var funcToSchedule = {
+        runAtMillis: runAtMillis,
+        funcToCall: f,
+        recurring: recurring,
+        params: params,
+        timeoutKey: timeoutKey,
+        millis: millis
+      };
+
+      if (runAtMillis in scheduledFunctions) {
+        scheduledFunctions[runAtMillis].push(funcToSchedule);
+      } else {
+        scheduledFunctions[runAtMillis] = [funcToSchedule];
+        scheduledLookup.push(runAtMillis);
+        scheduledLookup.sort(function (a, b) {
+          return a - b;
+        });
+      }
+
+      return timeoutKey;
+    };
+
+    self.removeFunctionWithId = function(timeoutKey) {
+      for (var runAtMillis in scheduledFunctions) {
+        var funcs = scheduledFunctions[runAtMillis];
+        var i = indexOfFirstToPass(funcs, function (func) {
+          return func.timeoutKey === timeoutKey;
+        });
+
+        if (i > -1) {
+          if (funcs.length === 1) {
+            delete scheduledFunctions[runAtMillis];
+            deleteFromLookup(runAtMillis);
+          } else {
+            funcs.splice(i, 1);
+          }
+
+          // intervals get rescheduled when executed, so there's never more
+          // than a single scheduled function with a given timeoutKey
+          break;
+        }
+      }
+    };
+
+    return self;
+
+    function indexOfFirstToPass(array, testFn) {
+      var index = -1;
+
+      for (var i = 0; i < array.length; ++i) {
+        if (testFn(array[i])) {
+          index = i;
+          break;
+        }
+      }
+
+      return index;
+    }
+
+    function deleteFromLookup(key) {
+      var value = Number(key);
+      var i = indexOfFirstToPass(scheduledLookup, function (millis) {
+        return millis === value;
+      });
+
+      if (i > -1) {
+        scheduledLookup.splice(i, 1);
+      }
+    }
+
+    function reschedule(scheduledFn) {
+      self.scheduleFunction(scheduledFn.funcToCall,
+        scheduledFn.millis,
+        scheduledFn.params,
+        true,
+        scheduledFn.timeoutKey,
+        scheduledFn.runAtMillis + scheduledFn.millis);
+    }
+
+    function forEachFunction(funcsToRun, callback) {
+      for (var i = 0; i < funcsToRun.length; ++i) {
+        callback(funcsToRun[i]);
+      }
+    }
+
+    function runScheduledFunctions(endTime, tickDate) {
+      tickDate = tickDate || function() {};
+      if (scheduledLookup.length === 0 || scheduledLookup[0] > endTime) {
+        tickDate(endTime - currentTime);
+        return;
+      }
+
+      do {
+        var newCurrentTime = scheduledLookup.shift();
+        tickDate(newCurrentTime - currentTime);
+
+        currentTime = newCurrentTime;
+
+        var funcsToRun = scheduledFunctions[currentTime];
+        delete scheduledFunctions[currentTime];
+
+        forEachFunction(funcsToRun, function(funcToRun) {
+          if (funcToRun.recurring) {
+            reschedule(funcToRun);
+          }
+        });
+
+        forEachFunction(funcsToRun, function(funcToRun) {
+          funcToRun.funcToCall.apply(null, funcToRun.params || []);
+        });
+      } while (scheduledLookup.length > 0 &&
+              // checking first if we're out of time prevents setTimeout(0)
+              // scheduled in a funcToRun from forcing an extra iteration
+                 currentTime !== endTime  &&
+                 scheduledLookup[0] <= endTime);
+
+      // ran out of functions to call, but still time left on the clock
+      if (currentTime !== endTime) {
+        tickDate(endTime - currentTime);
+      }
+    }
+  }
+
+  return DelayedFunctionScheduler;
+};
+
+getJasmineRequireObj().ExceptionFormatter = function() {
+  function ExceptionFormatter() {
+    this.message = function(error) {
+      var message = '';
+
+      if (error.name && error.message) {
+        message += error.name + ': ' + error.message;
+      } else {
+        message += error.toString() + ' thrown';
+      }
+
+      if (error.fileName || error.sourceURL) {
+        message += ' in ' + (error.fileName || error.sourceURL);
+      }
+
+      if (error.line || error.lineNumber) {
+        message += ' (line ' + (error.line || error.lineNumber) + ')';
+      }
+
+      return message;
+    };
+
+    this.stack = function(error) {
+      return error ? error.stack : null;
+    };
+  }
+
+  return ExceptionFormatter;
+};
+
+getJasmineRequireObj().Expectation = function() {
+
+  function Expectation(options) {
+    this.util = options.util || { buildFailureMessage: function() {} };
+    this.customEqualityTesters = options.customEqualityTesters || [];
+    this.actual = options.actual;
+    this.addExpectationResult = options.addExpectationResult || function(){};
+    this.isNot = options.isNot;
+
+    var customMatchers = options.customMatchers || {};
+    for (var matcherName in customMatchers) {
+      this[matcherName] = Expectation.prototype.wrapCompare(matcherName, customMatchers[matcherName]);
+    }
+  }
+
+  Expectation.prototype.wrapCompare = function(name, matcherFactory) {
+    return function() {
+      var args = Array.prototype.slice.call(arguments, 0),
+        expected = args.slice(0),
+        message = '';
+
+      args.unshift(this.actual);
+
+      var matcher = matcherFactory(this.util, this.customEqualityTesters),
+          matcherCompare = matcher.compare;
+
+      function defaultNegativeCompare() {
+        var result = matcher.compare.apply(null, args);
+        result.pass = !result.pass;
+        return result;
+      }
+
+      if (this.isNot) {
+        matcherCompare = matcher.negativeCompare || defaultNegativeCompare;
+      }
+
+      var result = matcherCompare.apply(null, args);
+
+      if (!result.pass) {
+        if (!result.message) {
+          args.unshift(this.isNot);
+          args.unshift(name);
+          message = this.util.buildFailureMessage.apply(null, args);
+        } else {
+          if (Object.prototype.toString.apply(result.message) === '[object Function]') {
+            message = result.message();
+          } else {
+            message = result.message;
+          }
+        }
+      }
+
+      if (expected.length == 1) {
+        expected = expected[0];
+      }
+
+      // TODO: how many of these params are needed?
+      this.addExpectationResult(
+        result.pass,
+        {
+          matcherName: name,
+          passed: result.pass,
+          message: message,
+          actual: this.actual,
+          expected: expected // TODO: this may need to be arrayified/sliced
+        }
+      );
+    };
+  };
+
+  Expectation.addCoreMatchers = function(matchers) {
+    var prototype = Expectation.prototype;
+    for (var matcherName in matchers) {
+      var matcher = matchers[matcherName];
+      prototype[matcherName] = prototype.wrapCompare(matcherName, matcher);
+    }
+  };
+
+  Expectation.Factory = function(options) {
+    options = options || {};
+
+    var expect = new Expectation(options);
+
+    // TODO: this would be nice as its own Object - NegativeExpectation
+    // TODO: copy instead of mutate options
+    options.isNot = true;
+    expect.not = new Expectation(options);
+
+    return expect;
+  };
+
+  return Expectation;
+};
+
+//TODO: expectation result may make more sense as a presentation of an expectation.
+getJasmineRequireObj().buildExpectationResult = function() {
+  function buildExpectationResult(options) {
+    var messageFormatter = options.messageFormatter || function() {},
+      stackFormatter = options.stackFormatter || function() {};
+
+    var result = {
+      matcherName: options.matcherName,
+      message: message(),
+      stack: stack(),
+      passed: options.passed
+    };
+
+    if(!result.passed) {
+      result.expected = options.expected;
+      result.actual = options.actual;
+    }
+
+    return result;
+
+    function message() {
+      if (options.passed) {
+        return 'Passed.';
+      } else if (options.message) {
+        return options.message;
+      } else if (options.error) {
+        return messageFormatter(options.error);
+      }
+      return '';
+    }
+
+    function stack() {
+      if (options.passed) {
+        return '';
+      }
+
+      var error = options.error;
+      if (!error) {
+        try {
+          throw new Error(message());
+        } catch (e) {
+          error = e;
+        }
+      }
+      return stackFormatter(error);
+    }
+  }
+
+  return buildExpectationResult;
+};
+
+getJasmineRequireObj().MockDate = function() {
+  function MockDate(global) {
+    var self = this;
+    var currentTime = 0;
+
+    if (!global || !global.Date) {
+      self.install = function() {};
+      self.tick = function() {};
+      self.uninstall = function() {};
+      return self;
+    }
+
+    var GlobalDate = global.Date;
+
+    self.install = function(mockDate) {
+      if (mockDate instanceof GlobalDate) {
+        currentTime = mockDate.getTime();
+      } else {
+        currentTime = new GlobalDate().getTime();
+      }
+
+      global.Date = FakeDate;
+    };
+
+    self.tick = function(millis) {
+      millis = millis || 0;
+      currentTime = currentTime + millis;
+    };
+
+    self.uninstall = function() {
+      currentTime = 0;
+      global.Date = GlobalDate;
+    };
+
+    createDateProperties();
+
+    return self;
+
+    function FakeDate() {
+      switch(arguments.length) {
+        case 0:
+          return new GlobalDate(currentTime);
+        case 1:
+          return new GlobalDate(arguments[0]);
+        case 2:
+          return new GlobalDate(arguments[0], arguments[1]);
+        case 3:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2]);
+        case 4:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3]);
+        case 5:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
+                                arguments[4]);
+        case 6:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
+                                arguments[4], arguments[5]);
+        default:
+          return new GlobalDate(arguments[0], arguments[1], arguments[2], arguments[3],
+                                arguments[4], arguments[5], arguments[6]);
+      }
+    }
+
+    function createDateProperties() {
+      FakeDate.prototype = GlobalDate.prototype;
+
+      FakeDate.now = function() {
+        if (GlobalDate.now) {
+          return currentTime;
+        } else {
+          throw new Error('Browser does not support Date.now()');
+        }
+      };
+
+      FakeDate.toSource = GlobalDate.toSource;
+      FakeDate.toString = GlobalDate.toString;
+      FakeDate.parse = GlobalDate.parse;
+      FakeDate.UTC = GlobalDate.UTC;
+    }
+       }
+
+  return MockDate;
+};
+
+getJasmineRequireObj().pp = function(j$) {
+
+  function PrettyPrinter() {
+    this.ppNestLevel_ = 0;
+    this.seen = [];
+  }
+
+  PrettyPrinter.prototype.format = function(value) {
+    this.ppNestLevel_++;
+    try {
+      if (j$.util.isUndefined(value)) {
+        this.emitScalar('undefined');
+      } else if (value === null) {
+        this.emitScalar('null');
+      } else if (value === 0 && 1/value === -Infinity) {
+        this.emitScalar('-0');
+      } else if (value === j$.getGlobal()) {
+        this.emitScalar('<global>');
+      } else if (value.jasmineToString) {
+        this.emitScalar(value.jasmineToString());
+      } else if (typeof value === 'string') {
+        this.emitString(value);
+      } else if (j$.isSpy(value)) {
+        this.emitScalar('spy on ' + value.and.identity());
+      } else if (value instanceof RegExp) {
+        this.emitScalar(value.toString());
+      } else if (typeof value === 'function') {
+        this.emitScalar('Function');
+      } else if (typeof value.nodeType === 'number') {
+        this.emitScalar('HTMLNode');
+      } else if (value instanceof Date) {
+        this.emitScalar('Date(' + value + ')');
+      } else if (value.toString && typeof value === 'object' && !(value instanceof Array) && value.toString 
!== Object.prototype.toString) {
+        this.emitScalar(value.toString());
+      } else if (j$.util.arrayContains(this.seen, value)) {
+        this.emitScalar('<circular reference: ' + (j$.isArray_(value) ? 'Array' : 'Object') + '>');
+      } else if (j$.isArray_(value) || j$.isA_('Object', value)) {
+        this.seen.push(value);
+        if (j$.isArray_(value)) {
+          this.emitArray(value);
+        } else {
+          this.emitObject(value);
+        }
+        this.seen.pop();
+      } else {
+        this.emitScalar(value.toString());
+      }
+    } finally {
+      this.ppNestLevel_--;
+    }
+  };
+
+  PrettyPrinter.prototype.iterateObject = function(obj, fn) {
+    for (var property in obj) {
+      if (!Object.prototype.hasOwnProperty.call(obj, property)) { continue; }
+      fn(property, obj.__lookupGetter__ ? (!j$.util.isUndefined(obj.__lookupGetter__(property)) &&
+          obj.__lookupGetter__(property) !== null) : false);
+    }
+  };
+
+  PrettyPrinter.prototype.emitArray = j$.unimplementedMethod_;
+  PrettyPrinter.prototype.emitObject = j$.unimplementedMethod_;
+  PrettyPrinter.prototype.emitScalar = j$.unimplementedMethod_;
+  PrettyPrinter.prototype.emitString = j$.unimplementedMethod_;
+
+  function StringPrettyPrinter() {
+    PrettyPrinter.call(this);
+
+    this.string = '';
+  }
+
+  j$.util.inherit(StringPrettyPrinter, PrettyPrinter);
+
+  StringPrettyPrinter.prototype.emitScalar = function(value) {
+    this.append(value);
+  };
+
+  StringPrettyPrinter.prototype.emitString = function(value) {
+    this.append('\'' + value + '\'');
+  };
+
+  StringPrettyPrinter.prototype.emitArray = function(array) {
+    if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+      this.append('Array');
+      return;
+    }
+    var length = Math.min(array.length, j$.MAX_PRETTY_PRINT_ARRAY_LENGTH);
+    this.append('[ ');
+    for (var i = 0; i < length; i++) {
+      if (i > 0) {
+        this.append(', ');
+      }
+      this.format(array[i]);
+    }
+    if(array.length > length){
+      this.append(', ...');
+    }
+
+    var self = this;
+    var first = array.length === 0;
+    this.iterateObject(array, function(property, isGetter) {
+      if (property.match(/^\d+$/)) {
+        return;
+      }
+
+      if (first) {
+        first = false;
+      } else {
+        self.append(', ');
+      }
+
+      self.formatProperty(array, property, isGetter);
+    });
+
+    this.append(' ]');
+  };
+
+  StringPrettyPrinter.prototype.emitObject = function(obj) {
+    var constructorName = obj.constructor ? j$.fnNameFor(obj.constructor) : 'null';
+    this.append(constructorName);
+
+    if (this.ppNestLevel_ > j$.MAX_PRETTY_PRINT_DEPTH) {
+      return;
+    }
+
+    var self = this;
+    this.append('({ ');
+    var first = true;
+
+    this.iterateObject(obj, function(property, isGetter) {
+      if (first) {
+        first = false;
+      } else {
+        self.append(', ');
+      }
+
+      self.formatProperty(obj, property, isGetter);
+    });
+
+    this.append(' })');
+  };
+
+  StringPrettyPrinter.prototype.formatProperty = function(obj, property, isGetter) {
+      this.append(property);
+      this.append(': ');
+      if (isGetter) {
+        this.append('<getter>');
+      } else {
+        this.format(obj[property]);
+      }
+  };
+
+  StringPrettyPrinter.prototype.append = function(value) {
+    this.string += value;
+  };
+
+  return function(value) {
+    var stringPrettyPrinter = new StringPrettyPrinter();
+    stringPrettyPrinter.format(value);
+    return stringPrettyPrinter.string;
+  };
+};
+
+getJasmineRequireObj().QueueRunner = function(j$) {
+
+  function once(fn) {
+    var called = false;
+    return function() {
+      if (!called) {
+        called = true;
+        fn();
+      }
+      return null;
+    };
+  }
+
+  function QueueRunner(attrs) {
+    this.queueableFns = attrs.queueableFns || [];
+    this.onComplete = attrs.onComplete || function() {};
+    this.clearStack = attrs.clearStack || function(fn) {fn();};
+    this.onException = attrs.onException || function() {};
+    this.catchException = attrs.catchException || function() { return true; };
+    this.userContext = attrs.userContext || {};
+    this.timeout = attrs.timeout || {setTimeout: setTimeout, clearTimeout: clearTimeout};
+    this.fail = attrs.fail || function() {};
+  }
+
+  QueueRunner.prototype.execute = function() {
+    this.run(this.queueableFns, 0);
+  };
+
+  QueueRunner.prototype.run = function(queueableFns, recursiveIndex) {
+    var length = queueableFns.length,
+      self = this,
+      iterativeIndex;
+
+
+    for(iterativeIndex = recursiveIndex; iterativeIndex < length; iterativeIndex++) {
+      var queueableFn = queueableFns[iterativeIndex];
+      if (queueableFn.fn.length > 0) {
+        attemptAsync(queueableFn);
+        return;
+      } else {
+        attemptSync(queueableFn);
+      }
+    }
+
+    var runnerDone = iterativeIndex >= length;
+
+    if (runnerDone) {
+      this.clearStack(this.onComplete);
+    }
+
+    function attemptSync(queueableFn) {
+      try {
+        queueableFn.fn.call(self.userContext);
+      } catch (e) {
+        handleException(e, queueableFn);
+      }
+    }
+
+    function attemptAsync(queueableFn) {
+      var clearTimeout = function () {
+          Function.prototype.apply.apply(self.timeout.clearTimeout, [j$.getGlobal(), [timeoutId]]);
+        },
+        next = once(function () {
+          clearTimeout(timeoutId);
+          self.run(queueableFns, iterativeIndex + 1);
+        }),
+        timeoutId;
+
+      next.fail = function() {
+        self.fail.apply(null, arguments);
+        next();
+      };
+
+      if (queueableFn.timeout) {
+        timeoutId = Function.prototype.apply.apply(self.timeout.setTimeout, [j$.getGlobal(), [function() {
+          var error = new Error('Timeout - Async callback was not invoked within timeout specified by 
jasmine.DEFAULT_TIMEOUT_INTERVAL.');
+          onException(error);
+          next();
+        }, queueableFn.timeout()]]);
+      }
+
+      try {
+        queueableFn.fn.call(self.userContext, next);
+      } catch (e) {
+        handleException(e, queueableFn);
+        next();
+      }
+    }
+
+    function onException(e) {
+      self.onException(e);
+    }
+
+    function handleException(e, queueableFn) {
+      onException(e);
+      if (!self.catchException(e)) {
+        //TODO: set a var when we catch an exception and
+        //use a finally block to close the loop in a nice way..
+        throw e;
+      }
+    }
+  };
+
+  return QueueRunner;
+};
+
+getJasmineRequireObj().ReportDispatcher = function() {
+  function ReportDispatcher(methods) {
+
+    var dispatchedMethods = methods || [];
+
+    for (var i = 0; i < dispatchedMethods.length; i++) {
+      var method = dispatchedMethods[i];
+      this[method] = (function(m) {
+        return function() {
+          dispatch(m, arguments);
+        };
+      }(method));
+    }
+
+    var reporters = [];
+    var fallbackReporter = null;
+
+    this.addReporter = function(reporter) {
+      reporters.push(reporter);
+    };
+
+    this.provideFallbackReporter = function(reporter) {
+      fallbackReporter = reporter;
+    };
+
+    this.clearReporters = function() {
+      reporters = [];
+    };
+
+    return this;
+
+    function dispatch(method, args) {
+      if (reporters.length === 0 && fallbackReporter !== null) {
+          reporters.push(fallbackReporter);
+      }
+      for (var i = 0; i < reporters.length; i++) {
+        var reporter = reporters[i];
+        if (reporter[method]) {
+          reporter[method].apply(reporter, args);
+        }
+      }
+    }
+  }
+
+  return ReportDispatcher;
+};
+
+
+getJasmineRequireObj().SpyRegistry = function(j$) {
+
+  var getErrorMsg = j$.formatErrorMsg('<spyOn>', 'spyOn(<object>, <methodName>)');
+
+  function SpyRegistry(options) {
+    options = options || {};
+    var currentSpies = options.currentSpies || function() { return []; };
+
+    this.allowRespy = function(allow){
+      this.respy = allow;
+    };
+
+    this.spyOn = function(obj, methodName) {
+
+      if (j$.util.isUndefined(obj)) {
+        throw new Error(getErrorMsg('could not find an object to spy upon for ' + methodName + '()'));
+      }
+
+      if (j$.util.isUndefined(methodName)) {
+        throw new Error(getErrorMsg('No method name supplied'));
+      }
+
+      if (j$.util.isUndefined(obj[methodName])) {
+        throw new Error(getErrorMsg(methodName + '() method does not exist'));
+      }
+
+      if (obj[methodName] && j$.isSpy(obj[methodName])  ) {
+        if ( !!this.respy ){
+          return obj[methodName];
+        }else {
+          throw new Error(getErrorMsg(methodName + ' has already been spied upon'));
+        }
+      }
+
+      var descriptor;
+      try {
+        descriptor = Object.getOwnPropertyDescriptor(obj, methodName);
+      } catch(e) {
+        // IE 8 doesn't support `definePropery` on non-DOM nodes
+      }
+
+      if (descriptor && !(descriptor.writable || descriptor.set)) {
+        throw new Error(getErrorMsg(methodName + ' is not declared writable or has no setter'));
+      }
+
+      var originalMethod = obj[methodName],
+        spiedMethod = j$.createSpy(methodName, originalMethod),
+        restoreStrategy;
+
+      if (Object.prototype.hasOwnProperty.call(obj, methodName)) {
+        restoreStrategy = function() {
+          obj[methodName] = originalMethod;
+        };
+      } else {
+        restoreStrategy = function() {
+          if (!delete obj[methodName]) {
+            obj[methodName] = originalMethod;
+          }
+        };
+      }
+
+      currentSpies().push({
+        restoreObjectToOriginalState: restoreStrategy
+      });
+
+      obj[methodName] = spiedMethod;
+
+      return spiedMethod;
+    };
+
+    this.clearSpies = function() {
+      var spies = currentSpies();
+      for (var i = spies.length - 1; i >= 0; i--) {
+        var spyEntry = spies[i];
+        spyEntry.restoreObjectToOriginalState();
+      }
+    };
+  }
+
+  return SpyRegistry;
+};
+
+getJasmineRequireObj().SpyStrategy = function(j$) {
+
+  function SpyStrategy(options) {
+    options = options || {};
+
+    var identity = options.name || 'unknown',
+        originalFn = options.fn || function() {},
+        getSpy = options.getSpy || function() {},
+        plan = function() {};
+
+    this.identity = function() {
+      return identity;
+    };
+
+    this.exec = function() {
+      return plan.apply(this, arguments);
+    };
+
+    this.callThrough = function() {
+      plan = originalFn;
+      return getSpy();
+    };
+
+    this.returnValue = function(value) {
+      plan = function() {
+        return value;
+      };
+      return getSpy();
+    };
+
+    this.returnValues = function() {
+      var values = Array.prototype.slice.call(arguments);
+      plan = function () {
+        return values.shift();
+      };
+      return getSpy();
+    };
+
+    this.throwError = function(something) {
+      var error = (something instanceof Error) ? something : new Error(something);
+      plan = function() {
+        throw error;
+      };
+      return getSpy();
+    };
+
+    this.callFake = function(fn) {
+      if(!j$.isFunction_(fn)) {
+        throw new Error('Argument passed to callFake should be a function, got ' + fn);
+      }
+      plan = fn;
+      return getSpy();
+    };
+
+    this.stub = function(fn) {
+      plan = function() {};
+      return getSpy();
+    };
+  }
+
+  return SpyStrategy;
+};
+
+getJasmineRequireObj().Suite = function(j$) {
+  function Suite(attrs) {
+    this.env = attrs.env;
+    this.id = attrs.id;
+    this.parentSuite = attrs.parentSuite;
+    this.description = attrs.description;
+    this.expectationFactory = attrs.expectationFactory;
+    this.expectationResultFactory = attrs.expectationResultFactory;
+    this.throwOnExpectationFailure = !!attrs.throwOnExpectationFailure;
+
+    this.beforeFns = [];
+    this.afterFns = [];
+    this.beforeAllFns = [];
+    this.afterAllFns = [];
+    this.disabled = false;
+
+    this.children = [];
+
+    this.result = {
+      id: this.id,
+      description: this.description,
+      fullName: this.getFullName(),
+      failedExpectations: []
+    };
+  }
+
+  Suite.prototype.expect = function(actual) {
+    return this.expectationFactory(actual, this);
+  };
+
+  Suite.prototype.getFullName = function() {
+    var fullName = [];
+    for (var parentSuite = this; parentSuite; parentSuite = parentSuite.parentSuite) {
+      if (parentSuite.parentSuite) {
+        fullName.unshift(parentSuite.description);
+      }
+    }
+    return fullName.join(' ');
+  };
+
+  Suite.prototype.disable = function() {
+    this.disabled = true;
+  };
+
+  Suite.prototype.pend = function(message) {
+    this.markedPending = true;
+  };
+
+  Suite.prototype.beforeEach = function(fn) {
+    this.beforeFns.unshift(fn);
+  };
+
+  Suite.prototype.beforeAll = function(fn) {
+    this.beforeAllFns.push(fn);
+  };
+
+  Suite.prototype.afterEach = function(fn) {
+    this.afterFns.unshift(fn);
+  };
+
+  Suite.prototype.afterAll = function(fn) {
+    this.afterAllFns.push(fn);
+  };
+
+  Suite.prototype.addChild = function(child) {
+    this.children.push(child);
+  };
+
+  Suite.prototype.status = function() {
+    if (this.disabled) {
+      return 'disabled';
+    }
+
+    if (this.markedPending) {
+      return 'pending';
+    }
+
+    if (this.result.failedExpectations.length > 0) {
+      return 'failed';
+    } else {
+      return 'finished';
+    }
+  };
+
+  Suite.prototype.isExecutable = function() {
+    return !this.disabled;
+  };
+
+  Suite.prototype.canBeReentered = function() {
+    return this.beforeAllFns.length === 0 && this.afterAllFns.length === 0;
+  };
+
+  Suite.prototype.getResult = function() {
+    this.result.status = this.status();
+    return this.result;
+  };
+
+  Suite.prototype.sharedUserContext = function() {
+    if (!this.sharedContext) {
+      this.sharedContext = this.parentSuite ? clone(this.parentSuite.sharedUserContext()) : {};
+    }
+
+    return this.sharedContext;
+  };
+
+  Suite.prototype.clonedSharedUserContext = function() {
+    return clone(this.sharedUserContext());
+  };
+
+  Suite.prototype.onException = function() {
+    if (arguments[0] instanceof j$.errors.ExpectationFailed) {
+      return;
+    }
+
+    if(isAfterAll(this.children)) {
+      var data = {
+        matcherName: '',
+        passed: false,
+        expected: '',
+        actual: '',
+        error: arguments[0]
+      };
+      this.result.failedExpectations.push(this.expectationResultFactory(data));
+    } else {
+      for (var i = 0; i < this.children.length; i++) {
+        var child = this.children[i];
+        child.onException.apply(child, arguments);
+      }
+    }
+  };
+
+  Suite.prototype.addExpectationResult = function () {
+    if(isAfterAll(this.children) && isFailure(arguments)){
+      var data = arguments[1];
+      this.result.failedExpectations.push(this.expectationResultFactory(data));
+      if(this.throwOnExpectationFailure) {
+        throw new j$.errors.ExpectationFailed();
+      }
+    } else {
+      for (var i = 0; i < this.children.length; i++) {
+        var child = this.children[i];
+        try {
+          child.addExpectationResult.apply(child, arguments);
+        } catch(e) {
+          // keep going
+        }
+      }
+    }
+  };
+
+  function isAfterAll(children) {
+    return children && children[0].result.status;
+  }
+
+  function isFailure(args) {
+    return !args[0];
+  }
+
+  function clone(obj) {
+    var clonedObj = {};
+    for (var prop in obj) {
+      if (obj.hasOwnProperty(prop)) {
+        clonedObj[prop] = obj[prop];
+      }
+    }
+
+    return clonedObj;
+  }
+
+  return Suite;
+};
+
+if (typeof window == void 0 && typeof exports == 'object') {
+  exports.Suite = jasmineRequire.Suite;
+}
+
+getJasmineRequireObj().Timer = function() {
+  var defaultNow = (function(Date) {
+    return function() { return new Date().getTime(); };
+  })(Date);
+
+  function Timer(options) {
+    options = options || {};
+
+    var now = options.now || defaultNow,
+      startTime;
+
+    this.start = function() {
+      startTime = now();
+    };
+
+    this.elapsed = function() {
+      return now() - startTime;
+    };
+  }
+
+  return Timer;
+};
+
+getJasmineRequireObj().TreeProcessor = function() {
+  function TreeProcessor(attrs) {
+    var tree = attrs.tree,
+        runnableIds = attrs.runnableIds,
+        queueRunnerFactory = attrs.queueRunnerFactory,
+        nodeStart = attrs.nodeStart || function() {},
+        nodeComplete = attrs.nodeComplete || function() {},
+        orderChildren = attrs.orderChildren || function(node) { return node.children; },
+        stats = { valid: true },
+        processed = false,
+        defaultMin = Infinity,
+        defaultMax = 1 - Infinity;
+
+    this.processTree = function() {
+      processNode(tree, false);
+      processed = true;
+      return stats;
+    };
+
+    this.execute = function(done) {
+      if (!processed) {
+        this.processTree();
+      }
+
+      if (!stats.valid) {
+        throw 'invalid order';
+      }
+
+      var childFns = wrapChildren(tree, 0);
+
+      queueRunnerFactory({
+        queueableFns: childFns,
+        userContext: tree.sharedUserContext(),
+        onException: function() {
+          tree.onException.apply(tree, arguments);
+        },
+        onComplete: done
+      });
+    };
+
+    function runnableIndex(id) {
+      for (var i = 0; i < runnableIds.length; i++) {
+        if (runnableIds[i] === id) {
+          return i;
+        }
+      }
+    }
+
+    function processNode(node, parentEnabled) {
+      var executableIndex = runnableIndex(node.id);
+
+      if (executableIndex !== undefined) {
+        parentEnabled = true;
+      }
+
+      parentEnabled = parentEnabled && node.isExecutable();
+
+      if (!node.children) {
+        stats[node.id] = {
+          executable: parentEnabled && node.isExecutable(),
+          segments: [{
+            index: 0,
+            owner: node,
+            nodes: [node],
+            min: startingMin(executableIndex),
+            max: startingMax(executableIndex)
+          }]
+        };
+      } else {
+        var hasExecutableChild = false;
+
+        var orderedChildren = orderChildren(node);
+
+        for (var i = 0; i < orderedChildren.length; i++) {
+          var child = orderedChildren[i];
+
+          processNode(child, parentEnabled);
+
+          if (!stats.valid) {
+            return;
+          }
+
+          var childStats = stats[child.id];
+
+          hasExecutableChild = hasExecutableChild || childStats.executable;
+        }
+
+        stats[node.id] = {
+          executable: hasExecutableChild
+        };
+
+        segmentChildren(node, orderedChildren, stats[node.id], executableIndex);
+
+        if (!node.canBeReentered() && stats[node.id].segments.length > 1) {
+          stats = { valid: false };
+        }
+      }
+    }
+
+    function startingMin(executableIndex) {
+      return executableIndex === undefined ? defaultMin : executableIndex;
+    }
+
+    function startingMax(executableIndex) {
+      return executableIndex === undefined ? defaultMax : executableIndex;
+    }
+
+    function segmentChildren(node, orderedChildren, nodeStats, executableIndex) {
+      var currentSegment = { index: 0, owner: node, nodes: [], min: startingMin(executableIndex), max: 
startingMax(executableIndex) },
+          result = [currentSegment],
+          lastMax = defaultMax,
+          orderedChildSegments = orderChildSegments(orderedChildren);
+
+      function isSegmentBoundary(minIndex) {
+        return lastMax !== defaultMax && minIndex !== defaultMin && lastMax < minIndex - 1;
+      }
+
+      for (var i = 0; i < orderedChildSegments.length; i++) {
+        var childSegment = orderedChildSegments[i],
+          maxIndex = childSegment.max,
+          minIndex = childSegment.min;
+
+        if (isSegmentBoundary(minIndex)) {
+          currentSegment = {index: result.length, owner: node, nodes: [], min: defaultMin, max: defaultMax};
+          result.push(currentSegment);
+        }
+
+        currentSegment.nodes.push(childSegment);
+        currentSegment.min = Math.min(currentSegment.min, minIndex);
+        currentSegment.max = Math.max(currentSegment.max, maxIndex);
+        lastMax = maxIndex;
+      }
+
+      nodeStats.segments = result;
+    }
+
+    function orderChildSegments(children) {
+      var specifiedOrder = [],
+          unspecifiedOrder = [];
+
+      for (var i = 0; i < children.length; i++) {
+        var child = children[i],
+            segments = stats[child.id].segments;
+
+        for (var j = 0; j < segments.length; j++) {
+          var seg = segments[j];
+
+          if (seg.min === defaultMin) {
+            unspecifiedOrder.push(seg);
+          } else {
+            specifiedOrder.push(seg);
+          }
+        }
+      }
+
+      specifiedOrder.sort(function(a, b) {
+        return a.min - b.min;
+      });
+
+      return specifiedOrder.concat(unspecifiedOrder);
+    }
+
+    function executeNode(node, segmentNumber) {
+      if (node.children) {
+        return {
+          fn: function(done) {
+            nodeStart(node);
+
+            queueRunnerFactory({
+              onComplete: function() {
+                nodeComplete(node, node.getResult());
+                done();
+              },
+              queueableFns: wrapChildren(node, segmentNumber),
+              userContext: node.sharedUserContext(),
+              onException: function() {
+                node.onException.apply(node, arguments);
+              }
+            });
+          }
+        };
+      } else {
+        return {
+          fn: function(done) { node.execute(done, stats[node.id].executable); }
+        };
+      }
+    }
+
+    function wrapChildren(node, segmentNumber) {
+      var result = [],
+          segmentChildren = stats[node.id].segments[segmentNumber].nodes;
+
+      for (var i = 0; i < segmentChildren.length; i++) {
+        result.push(executeNode(segmentChildren[i].owner, segmentChildren[i].index));
+      }
+
+      if (!stats[node.id].executable) {
+        return result;
+      }
+
+      return node.beforeAllFns.concat(result).concat(node.afterAllFns);
+    }
+  }
+
+  return TreeProcessor;
+};
+
+getJasmineRequireObj().Any = function(j$) {
+
+  function Any(expectedObject) {
+    if (typeof expectedObject === 'undefined') {
+      throw new TypeError(
+        'jasmine.any() expects to be passed a constructor function. ' +
+        'Please pass one or use jasmine.anything() to match any object.'
+      );
+    }
+    this.expectedObject = expectedObject;
+  }
+
+  Any.prototype.asymmetricMatch = function(other) {
+    if (this.expectedObject == String) {
+      return typeof other == 'string' || other instanceof String;
+    }
+
+    if (this.expectedObject == Number) {
+      return typeof other == 'number' || other instanceof Number;
+    }
+
+    if (this.expectedObject == Function) {
+      return typeof other == 'function' || other instanceof Function;
+    }
+
+    if (this.expectedObject == Object) {
+      return typeof other == 'object';
+    }
+
+    if (this.expectedObject == Boolean) {
+      return typeof other == 'boolean';
+    }
+
+    return other instanceof this.expectedObject;
+  };
+
+  Any.prototype.jasmineToString = function() {
+    return '<jasmine.any(' + j$.fnNameFor(this.expectedObject) + ')>';
+  };
+
+  return Any;
+};
+
+getJasmineRequireObj().Anything = function(j$) {
+
+  function Anything() {}
+
+  Anything.prototype.asymmetricMatch = function(other) {
+    return !j$.util.isUndefined(other) && other !== null;
+  };
+
+  Anything.prototype.jasmineToString = function() {
+    return '<jasmine.anything>';
+  };
+
+  return Anything;
+};
+
+getJasmineRequireObj().ArrayContaining = function(j$) {
+  function ArrayContaining(sample) {
+    this.sample = sample;
+  }
+
+  ArrayContaining.prototype.asymmetricMatch = function(other) {
+    var className = Object.prototype.toString.call(this.sample);
+    if (className !== '[object Array]') { throw new Error('You must provide an array to arrayContaining, not 
\'' + this.sample + '\'.'); }
+
+    for (var i = 0; i < this.sample.length; i++) {
+      var item = this.sample[i];
+      if (!j$.matchersUtil.contains(other, item)) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  ArrayContaining.prototype.jasmineToString = function () {
+    return '<jasmine.arrayContaining(' + jasmine.pp(this.sample) +')>';
+  };
+
+  return ArrayContaining;
+};
+
+getJasmineRequireObj().ObjectContaining = function(j$) {
+
+  function ObjectContaining(sample) {
+    this.sample = sample;
+  }
+
+  function getPrototype(obj) {
+    if (Object.getPrototypeOf) {
+      return Object.getPrototypeOf(obj);
+    }
+
+    if (obj.constructor.prototype == obj) {
+      return null;
+    }
+
+    return obj.constructor.prototype;
+  }
+
+  function hasProperty(obj, property) {
+    if (!obj) {
+      return false;
+    }
+
+    if (Object.prototype.hasOwnProperty.call(obj, property)) {
+      return true;
+    }
+
+    return hasProperty(getPrototype(obj), property);
+  }
+
+  ObjectContaining.prototype.asymmetricMatch = function(other) {
+    if (typeof(this.sample) !== 'object') { throw new Error('You must provide an object to objectContaining, 
not \''+this.sample+'\'.'); }
+
+    for (var property in this.sample) {
+      if (!hasProperty(other, property) ||
+          !j$.matchersUtil.equals(this.sample[property], other[property])) {
+        return false;
+      }
+    }
+
+    return true;
+  };
+
+  ObjectContaining.prototype.jasmineToString = function() {
+    return '<jasmine.objectContaining(' + j$.pp(this.sample) + ')>';
+  };
+
+  return ObjectContaining;
+};
+
+getJasmineRequireObj().StringMatching = function(j$) {
+
+  function StringMatching(expected) {
+    if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) {
+      throw new Error('Expected is not a String or a RegExp');
+    }
+
+    this.regexp = new RegExp(expected);
+  }
+
+  StringMatching.prototype.asymmetricMatch = function(other) {
+    return this.regexp.test(other);
+  };
+
+  StringMatching.prototype.jasmineToString = function() {
+    return '<jasmine.stringMatching(' + this.regexp + ')>';
+  };
+
+  return StringMatching;
+};
+
+getJasmineRequireObj().errors = function() {
+  function ExpectationFailed() {}
+
+  ExpectationFailed.prototype = new Error();
+  ExpectationFailed.prototype.constructor = ExpectationFailed;
+
+  return {
+    ExpectationFailed: ExpectationFailed
+  };
+};
+getJasmineRequireObj().formatErrorMsg = function() {
+  function generateErrorMsg(domain, usage) {
+    var usageDefinition = usage ? '\nUsage: ' + usage : '';
+
+    return function errorMsg(msg) {
+      return domain + ' : ' + msg + usageDefinition;
+    };
+  }
+
+  return generateErrorMsg;
+};
+
+getJasmineRequireObj().matchersUtil = function(j$) {
+  // TODO: what to do about jasmine.pp not being inject? move to JSON.stringify? gut PrettyPrinter?
+
+  return {
+    equals: function(a, b, customTesters) {
+      customTesters = customTesters || [];
+
+      return eq(a, b, [], [], customTesters);
+    },
+
+    contains: function(haystack, needle, customTesters) {
+      customTesters = customTesters || [];
+
+      if ((Object.prototype.toString.apply(haystack) === '[object Array]') ||
+        (!!haystack && !haystack.indexOf))
+      {
+        for (var i = 0; i < haystack.length; i++) {
+          if (eq(haystack[i], needle, [], [], customTesters)) {
+            return true;
+          }
+        }
+        return false;
+      }
+
+      return !!haystack && haystack.indexOf(needle) >= 0;
+    },
+
+    buildFailureMessage: function() {
+      var args = Array.prototype.slice.call(arguments, 0),
+        matcherName = args[0],
+        isNot = args[1],
+        actual = args[2],
+        expected = args.slice(3),
+        englishyPredicate = matcherName.replace(/[A-Z]/g, function(s) { return ' ' + s.toLowerCase(); });
+
+      var message = 'Expected ' +
+        j$.pp(actual) +
+        (isNot ? ' not ' : ' ') +
+        englishyPredicate;
+
+      if (expected.length > 0) {
+        for (var i = 0; i < expected.length; i++) {
+          if (i > 0) {
+            message += ',';
+          }
+          message += ' ' + j$.pp(expected[i]);
+        }
+      }
+
+      return message + '.';
+    }
+  };
+
+  function isAsymmetric(obj) {
+    return obj && j$.isA_('Function', obj.asymmetricMatch);
+  }
+
+  function asymmetricMatch(a, b) {
+    var asymmetricA = isAsymmetric(a),
+        asymmetricB = isAsymmetric(b);
+
+    if (asymmetricA && asymmetricB) {
+      return undefined;
+    }
+
+    if (asymmetricA) {
+      return a.asymmetricMatch(b);
+    }
+
+    if (asymmetricB) {
+      return b.asymmetricMatch(a);
+    }
+  }
+
+  // Equality function lovingly adapted from isEqual in
+  //   [Underscore](http://underscorejs.org)
+  function eq(a, b, aStack, bStack, customTesters) {
+    var result = true;
+
+    var asymmetricResult = asymmetricMatch(a, b);
+    if (!j$.util.isUndefined(asymmetricResult)) {
+      return asymmetricResult;
+    }
+
+    for (var i = 0; i < customTesters.length; i++) {
+      var customTesterResult = customTesters[i](a, b);
+      if (!j$.util.isUndefined(customTesterResult)) {
+        return customTesterResult;
+      }
+    }
+
+    if (a instanceof Error && b instanceof Error) {
+      return a.message == b.message;
+    }
+
+    // Identical objects are equal. `0 === -0`, but they aren't identical.
+    // See the [Harmony `egal` proposal](http://wiki.ecmascript.org/doku.php?id=harmony:egal).
+    if (a === b) { return a !== 0 || 1 / a == 1 / b; }
+    // A strict comparison is necessary because `null == undefined`.
+    if (a === null || b === null) { return a === b; }
+    var className = Object.prototype.toString.call(a);
+    if (className != Object.prototype.toString.call(b)) { return false; }
+    switch (className) {
+      // Strings, numbers, dates, and booleans are compared by value.
+      case '[object String]':
+        // Primitives and their corresponding object wrappers are equivalent; thus, `"5"` is
+        // equivalent to `new String("5")`.
+        return a == String(b);
+      case '[object Number]':
+        // `NaN`s are equivalent, but non-reflexive. An `egal` comparison is performed for
+        // other numeric values.
+        return a != +a ? b != +b : (a === 0 ? 1 / a == 1 / b : a == +b);
+      case '[object Date]':
+      case '[object Boolean]':
+        // Coerce dates and booleans to numeric primitive values. Dates are compared by their
+        // millisecond representations. Note that invalid dates with millisecond representations
+        // of `NaN` are not equivalent.
+        return +a == +b;
+      // RegExps are compared by their source patterns and flags.
+      case '[object RegExp]':
+        return a.source == b.source &&
+          a.global == b.global &&
+          a.multiline == b.multiline &&
+          a.ignoreCase == b.ignoreCase;
+    }
+    if (typeof a != 'object' || typeof b != 'object') { return false; }
+
+    var aIsDomNode = j$.isDomNode(a);
+    var bIsDomNode = j$.isDomNode(b);
+    if (aIsDomNode && bIsDomNode) {
+      // At first try to use DOM3 method isEqualNode
+      if (a.isEqualNode) {
+        return a.isEqualNode(b);
+      }
+      // IE8 doesn't support isEqualNode, try to use outerHTML && innerText
+      var aIsElement = a instanceof Element;
+      var bIsElement = b instanceof Element;
+      if (aIsElement && bIsElement) {
+        return a.outerHTML == b.outerHTML;
+      }
+      if (aIsElement || bIsElement) {
+        return false;
+      }
+      return a.innerText == b.innerText && a.textContent == b.textContent;
+    }
+    if (aIsDomNode || bIsDomNode) {
+      return false;
+    }
+
+    // Assume equality for cyclic structures. The algorithm for detecting cyclic
+    // structures is adapted from ES 5.1 section 15.12.3, abstract operation `JO`.
+    var length = aStack.length;
+    while (length--) {
+      // Linear search. Performance is inversely proportional to the number of
+      // unique nested structures.
+      if (aStack[length] == a) { return bStack[length] == b; }
+    }
+    // Add the first object to the stack of traversed objects.
+    aStack.push(a);
+    bStack.push(b);
+    var size = 0;
+    // Recursively compare objects and arrays.
+    // Compare array lengths to determine if a deep comparison is necessary.
+    if (className == '[object Array]') {
+      size = a.length;
+      if (size !== b.length) {
+        return false;
+      }
+
+      while (size--) {
+        result = eq(a[size], b[size], aStack, bStack, customTesters);
+        if (!result) {
+          return false;
+        }
+      }
+    } else {
+
+      // Objects with different constructors are not equivalent, but `Object`s
+      // or `Array`s from different frames are.
+      var aCtor = a.constructor, bCtor = b.constructor;
+      if (aCtor !== bCtor && !(isObjectConstructor(aCtor) &&
+                               isObjectConstructor(bCtor))) {
+        return false;
+      }
+    }
+
+    // Deep compare objects.
+    var aKeys = keys(a, className == '[object Array]'), key;
+    size = aKeys.length;
+
+    // Ensure that both objects contain the same number of properties before comparing deep equality.
+    if (keys(b, className == '[object Array]').length !== size) { return false; }
+
+    while (size--) {
+      key = aKeys[size];
+      // Deep compare each member
+      result = has(b, key) && eq(a[key], b[key], aStack, bStack, customTesters);
+
+      if (!result) {
+        return false;
+      }
+    }
+    // Remove the first object from the stack of traversed objects.
+    aStack.pop();
+    bStack.pop();
+
+    return result;
+
+    function keys(obj, isArray) {
+      var allKeys = Object.keys ? Object.keys(obj) :
+        (function(o) {
+            var keys = [];
+            for (var key in o) {
+                if (has(o, key)) {
+                    keys.push(key);
+                }
+            }
+            return keys;
+        })(obj);
+
+      if (!isArray) {
+        return allKeys;
+      }
+
+      var extraKeys = [];
+      if (allKeys.length === 0) {
+          return allKeys;
+      }
+
+      for (var x = 0; x < allKeys.length; x++) {
+          if (!allKeys[x].match(/^[0-9]+$/)) {
+              extraKeys.push(allKeys[x]);
+          }
+      }
+
+      return extraKeys;
+    }
+  }
+
+  function has(obj, key) {
+    return Object.prototype.hasOwnProperty.call(obj, key);
+  }
+
+  function isFunction(obj) {
+    return typeof obj === 'function';
+  }
+
+  function isObjectConstructor(ctor) {
+    // aCtor instanceof aCtor is true for the Object and Function
+    // constructors (since a constructor is-a Function and a function is-a
+    // Object). We don't just compare ctor === Object because the constructor
+    // might come from a different frame with different globals.
+    return isFunction(ctor) && ctor instanceof ctor;
+  }
+};
+
+getJasmineRequireObj().toBe = function() {
+  function toBe() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual === expected
+        };
+      }
+    };
+  }
+
+  return toBe;
+};
+
+getJasmineRequireObj().toBeCloseTo = function() {
+
+  function toBeCloseTo() {
+    return {
+      compare: function(actual, expected, precision) {
+        if (precision !== 0) {
+          precision = precision || 2;
+        }
+
+        return {
+          pass: Math.abs(expected - actual) < (Math.pow(10, -precision) / 2)
+        };
+      }
+    };
+  }
+
+  return toBeCloseTo;
+};
+
+getJasmineRequireObj().toBeDefined = function() {
+  function toBeDefined() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: (void 0 !== actual)
+        };
+      }
+    };
+  }
+
+  return toBeDefined;
+};
+
+getJasmineRequireObj().toBeFalsy = function() {
+  function toBeFalsy() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: !!!actual
+        };
+      }
+    };
+  }
+
+  return toBeFalsy;
+};
+
+getJasmineRequireObj().toBeGreaterThan = function() {
+
+  function toBeGreaterThan() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual > expected
+        };
+      }
+    };
+  }
+
+  return toBeGreaterThan;
+};
+
+
+getJasmineRequireObj().toBeGreaterThanOrEqual = function() {
+
+  function toBeGreaterThanOrEqual() {
+    return {
+      compare: function(actual, expected) {
+        return {
+          pass: actual >= expected
+        };
+      }
+    };
+  }
+
+  return toBeGreaterThanOrEqual;
+};
+
+getJasmineRequireObj().toBeLessThan = function() {
+  function toBeLessThan() {
+    return {
+
+      compare: function(actual, expected) {
+        return {
+          pass: actual < expected
+        };
+      }
+    };
+  }
+
+  return toBeLessThan;
+};
+getJasmineRequireObj().toBeLessThanOrEqual = function() {
+  function toBeLessThanOrEqual() {
+    return {
+
+      compare: function(actual, expected) {
+        return {
+          pass: actual <= expected
+        };
+      }
+    };
+  }
+
+  return toBeLessThanOrEqual;
+};
+
+getJasmineRequireObj().toBeNaN = function(j$) {
+
+  function toBeNaN() {
+    return {
+      compare: function(actual) {
+        var result = {
+          pass: (actual !== actual)
+        };
+
+        if (result.pass) {
+          result.message = 'Expected actual not to be NaN.';
+        } else {
+          result.message = function() { return 'Expected ' + j$.pp(actual) + ' to be NaN.'; };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toBeNaN;
+};
+
+getJasmineRequireObj().toBeNull = function() {
+
+  function toBeNull() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: actual === null
+        };
+      }
+    };
+  }
+
+  return toBeNull;
+};
+
+getJasmineRequireObj().toBeTruthy = function() {
+
+  function toBeTruthy() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: !!actual
+        };
+      }
+    };
+  }
+
+  return toBeTruthy;
+};
+
+getJasmineRequireObj().toBeUndefined = function() {
+
+  function toBeUndefined() {
+    return {
+      compare: function(actual) {
+        return {
+          pass: void 0 === actual
+        };
+      }
+    };
+  }
+
+  return toBeUndefined;
+};
+
+getJasmineRequireObj().toContain = function() {
+  function toContain(util, customEqualityTesters) {
+    customEqualityTesters = customEqualityTesters || [];
+
+    return {
+      compare: function(actual, expected) {
+
+        return {
+          pass: util.contains(actual, expected, customEqualityTesters)
+        };
+      }
+    };
+  }
+
+  return toContain;
+};
+
+getJasmineRequireObj().toEqual = function() {
+
+  function toEqual(util, customEqualityTesters) {
+    customEqualityTesters = customEqualityTesters || [];
+
+    return {
+      compare: function(actual, expected) {
+        var result = {
+          pass: false
+        };
+
+        result.pass = util.equals(actual, expected, customEqualityTesters);
+
+        return result;
+      }
+    };
+  }
+
+  return toEqual;
+};
+
+getJasmineRequireObj().toHaveBeenCalled = function(j$) {
+
+  var getErrorMsg = j$.formatErrorMsg('<toHaveBeenCalled>', 'expect(<spyObj>).toHaveBeenCalled()');
+
+  function toHaveBeenCalled() {
+    return {
+      compare: function(actual) {
+        var result = {};
+
+        if (!j$.isSpy(actual)) {
+          throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.'));
+        }
+
+        if (arguments.length > 1) {
+          throw new Error(getErrorMsg('Does not take arguments, use toHaveBeenCalledWith'));
+        }
+
+        result.pass = actual.calls.any();
+
+        result.message = result.pass ?
+          'Expected spy ' + actual.and.identity() + ' not to have been called.' :
+          'Expected spy ' + actual.and.identity() + ' to have been called.';
+
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalled;
+};
+
+getJasmineRequireObj().toHaveBeenCalledTimes = function(j$) {
+
+  var getErrorMsg = j$.formatErrorMsg('<toHaveBeenCalledTimes>', 
'expect(<spyObj>).toHaveBeenCalledTimes(<Number>)');
+
+  function toHaveBeenCalledTimes() {
+    return {
+      compare: function(actual, expected) {
+        if (!j$.isSpy(actual)) {
+          throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.'));
+        }
+
+        var args = Array.prototype.slice.call(arguments, 0),
+          result = { pass: false };
+
+        if (!j$.isNumber_(expected)){
+          throw new Error(getErrorMsg('The expected times failed is a required argument and must be a 
number.'));
+        }
+
+        actual = args[0];
+        var calls = actual.calls.count();
+        var timesMessage = expected === 1 ? 'once' : expected + ' times';
+        result.pass = calls === expected;
+        result.message = result.pass ?
+          'Expected spy ' + actual.and.identity() + ' not to have been called ' + timesMessage + '. It was 
called ' +  calls + ' times.' :
+          'Expected spy ' + actual.and.identity() + ' to have been called ' + timesMessage + '. It was 
called ' +  calls + ' times.';
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalledTimes;
+};
+
+getJasmineRequireObj().toHaveBeenCalledWith = function(j$) {
+
+  var getErrorMsg = j$.formatErrorMsg('<toHaveBeenCalledWith>', 
'expect(<spyObj>).toHaveBeenCalledWith(...arguments)');
+
+  function toHaveBeenCalledWith(util, customEqualityTesters) {
+    return {
+      compare: function() {
+        var args = Array.prototype.slice.call(arguments, 0),
+          actual = args[0],
+          expectedArgs = args.slice(1),
+          result = { pass: false };
+
+        if (!j$.isSpy(actual)) {
+          throw new Error(getErrorMsg('Expected a spy, but got ' + j$.pp(actual) + '.'));
+        }
+
+        if (!actual.calls.any()) {
+          result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been 
called with ' + j$.pp(expectedArgs) + ' but it was never called.'; };
+          return result;
+        }
+
+        if (util.contains(actual.calls.allArgs(), expectedArgs, customEqualityTesters)) {
+          result.pass = true;
+          result.message = function() { return 'Expected spy ' + actual.and.identity() + ' not to have been 
called with ' + j$.pp(expectedArgs) + ' but it was.'; };
+        } else {
+          result.message = function() { return 'Expected spy ' + actual.and.identity() + ' to have been 
called with ' + j$.pp(expectedArgs) + ' but actual calls were ' + j$.pp(actual.calls.allArgs()).replace(/^\[ 
| \]$/g, '') + '.'; };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toHaveBeenCalledWith;
+};
+
+getJasmineRequireObj().toMatch = function(j$) {
+
+  var getErrorMsg = j$.formatErrorMsg('<toMatch>', 'expect(<expectation>).toMatch(<string> || <regexp>)');
+
+  function toMatch() {
+    return {
+      compare: function(actual, expected) {
+        if (!j$.isString_(expected) && !j$.isA_('RegExp', expected)) {
+          throw new Error(getErrorMsg('Expected is not a String or a RegExp'));
+        }
+
+        var regexp = new RegExp(expected);
+
+        return {
+          pass: regexp.test(actual)
+        };
+      }
+    };
+  }
+
+  return toMatch;
+};
+
+getJasmineRequireObj().toThrow = function(j$) {
+
+  var getErrorMsg = j$.formatErrorMsg('<toThrow>', 'expect(function() {<expectation>}).toThrow()');
+
+  function toThrow(util) {
+    return {
+      compare: function(actual, expected) {
+        var result = { pass: false },
+          threw = false,
+          thrown;
+
+        if (typeof actual != 'function') {
+          throw new Error(getErrorMsg('Actual is not a Function'));
+        }
+
+        try {
+          actual();
+        } catch (e) {
+          threw = true;
+          thrown = e;
+        }
+
+        if (!threw) {
+          result.message = 'Expected function to throw an exception.';
+          return result;
+        }
+
+        if (arguments.length == 1) {
+          result.pass = true;
+          result.message = function() { return 'Expected function not to throw, but it threw ' + 
j$.pp(thrown) + '.'; };
+
+          return result;
+        }
+
+        if (util.equals(thrown, expected)) {
+          result.pass = true;
+          result.message = function() { return 'Expected function not to throw ' + j$.pp(expected) + '.'; };
+        } else {
+          result.message = function() { return 'Expected function to throw ' + j$.pp(expected) + ', but it 
threw ' +  j$.pp(thrown) + '.'; };
+        }
+
+        return result;
+      }
+    };
+  }
+
+  return toThrow;
+};
+
+getJasmineRequireObj().toThrowError = function(j$) {
+
+  var getErrorMsg =  j$.formatErrorMsg('<toThrowError>', 'expect(function() 
{<expectation>}).toThrowError(<ErrorConstructor>, <message>)');
+
+  function toThrowError () {
+    return {
+      compare: function(actual) {
+        var threw = false,
+          pass = {pass: true},
+          fail = {pass: false},
+          thrown;
+
+        if (typeof actual != 'function') {
+          throw new Error(getErrorMsg('Actual is not a Function'));
+        }
+
+        var errorMatcher = getMatcher.apply(null, arguments);
+
+        try {
+          actual();
+        } catch (e) {
+          threw = true;
+          thrown = e;
+        }
+
+        if (!threw) {
+          fail.message = 'Expected function to throw an Error.';
+          return fail;
+        }
+
+        if (!(thrown instanceof Error)) {
+          fail.message = function() { return 'Expected function to throw an Error, but it threw ' + 
j$.pp(thrown) + '.'; };
+          return fail;
+        }
+
+        if (errorMatcher.hasNoSpecifics()) {
+          pass.message = 'Expected function not to throw an Error, but it threw ' + j$.fnNameFor(thrown) + 
'.';
+          return pass;
+        }
+
+        if (errorMatcher.matches(thrown)) {
+          pass.message = function() {
+            return 'Expected function not to throw ' + errorMatcher.errorTypeDescription + 
errorMatcher.messageDescription() + '.';
+          };
+          return pass;
+        } else {
+          fail.message = function() {
+            return 'Expected function to throw ' + errorMatcher.errorTypeDescription + 
errorMatcher.messageDescription() +
+              ', but it threw ' + errorMatcher.thrownDescription(thrown) + '.';
+          };
+          return fail;
+        }
+      }
+    };
+
+    function getMatcher() {
+      var expected = null,
+          errorType = null;
+
+      if (arguments.length == 2) {
+        expected = arguments[1];
+        if (isAnErrorType(expected)) {
+          errorType = expected;
+          expected = null;
+        }
+      } else if (arguments.length > 2) {
+        errorType = arguments[1];
+        expected = arguments[2];
+        if (!isAnErrorType(errorType)) {
+          throw new Error(getErrorMsg('Expected error type is not an Error.'));
+        }
+      }
+
+      if (expected && !isStringOrRegExp(expected)) {
+        if (errorType) {
+          throw new Error(getErrorMsg('Expected error message is not a string or RegExp.'));
+        } else {
+          throw new Error(getErrorMsg('Expected is not an Error, string, or RegExp.'));
+        }
+      }
+
+      function messageMatch(message) {
+        if (typeof expected == 'string') {
+          return expected == message;
+        } else {
+          return expected.test(message);
+        }
+      }
+
+      return {
+        errorTypeDescription: errorType ? j$.fnNameFor(errorType) : 'an exception',
+        thrownDescription: function(thrown) {
+          var thrownName = errorType ? j$.fnNameFor(thrown.constructor) : 'an exception',
+              thrownMessage = '';
+
+          if (expected) {
+            thrownMessage = ' with message ' + j$.pp(thrown.message);
+          }
+
+          return thrownName + thrownMessage;
+        },
+        messageDescription: function() {
+          if (expected === null) {
+            return '';
+          } else if (expected instanceof RegExp) {
+            return ' with a message matching ' + j$.pp(expected);
+          } else {
+            return ' with message ' + j$.pp(expected);
+          }
+        },
+        hasNoSpecifics: function() {
+          return expected === null && errorType === null;
+        },
+        matches: function(error) {
+          return (errorType === null || error instanceof errorType) &&
+            (expected === null || messageMatch(error.message));
+        }
+      };
+    }
+
+    function isStringOrRegExp(potential) {
+      return potential instanceof RegExp || (typeof potential == 'string');
+    }
+
+    function isAnErrorType(type) {
+      if (typeof type !== 'function') {
+        return false;
+      }
+
+      var Surrogate = function() {};
+      Surrogate.prototype = type.prototype;
+      return (new Surrogate()) instanceof Error;
+    }
+  }
+
+  return toThrowError;
+};
+
+getJasmineRequireObj().interface = function(jasmine, env) {
+  var jasmineInterface = {
+    describe: function(description, specDefinitions) {
+      return env.describe(description, specDefinitions);
+    },
+
+    xdescribe: function(description, specDefinitions) {
+      return env.xdescribe(description, specDefinitions);
+    },
+
+    fdescribe: function(description, specDefinitions) {
+      return env.fdescribe(description, specDefinitions);
+    },
+
+    it: function() {
+      return env.it.apply(env, arguments);
+    },
+
+    xit: function() {
+      return env.xit.apply(env, arguments);
+    },
+
+    fit: function() {
+      return env.fit.apply(env, arguments);
+    },
+
+    beforeEach: function() {
+      return env.beforeEach.apply(env, arguments);
+    },
+
+    afterEach: function() {
+      return env.afterEach.apply(env, arguments);
+    },
+
+    beforeAll: function() {
+      return env.beforeAll.apply(env, arguments);
+    },
+
+    afterAll: function() {
+      return env.afterAll.apply(env, arguments);
+    },
+
+    expect: function(actual) {
+      return env.expect(actual);
+    },
+
+    pending: function() {
+      return env.pending.apply(env, arguments);
+    },
+
+    fail: function() {
+      return env.fail.apply(env, arguments);
+    },
+
+    spyOn: function(obj, methodName) {
+      return env.spyOn(obj, methodName);
+    },
+
+    jsApiReporter: new jasmine.JsApiReporter({
+      timer: new jasmine.Timer()
+    }),
+
+    jasmine: jasmine
+  };
+
+  jasmine.addCustomEqualityTester = function(tester) {
+    env.addCustomEqualityTester(tester);
+  };
+
+  jasmine.addMatchers = function(matchers) {
+    return env.addMatchers(matchers);
+  };
+
+  jasmine.clock = function() {
+    return env.clock;
+  };
+
+  return jasmineInterface;
+};
+
+getJasmineRequireObj().version = function() {
+  return '2.5.2';
+};
diff --git a/installed-tests/js/jsunit.gresources.xml b/installed-tests/js/jsunit.gresources.xml
index 944626b..da983d4 100644
--- a/installed-tests/js/jsunit.gresources.xml
+++ b/installed-tests/js/jsunit.gresources.xml
@@ -2,6 +2,8 @@
 <gresources>
   <gresource prefix="/org/gjs/jsunit">
     <file preprocess="xml-stripblanks">complex.ui</file>
+    <file>jasmine.js</file>
+    <file>minijasmine.js</file>
     <file>modules/alwaysThrows.js</file>
     <file>modules/foobar.js</file>
     <file>modules/modunicode.js</file>
diff --git a/installed-tests/js/minijasmine.js b/installed-tests/js/minijasmine.js
new file mode 100644
index 0000000..0345ec3
--- /dev/null
+++ b/installed-tests/js/minijasmine.js
@@ -0,0 +1,113 @@
+#!/usr/bin/env gjs
+
+const GLib = imports.gi.GLib;
+const Lang = imports.lang;
+
+function _removeNewlines(str) {
+    let allNewlines = /\n/g;
+    return str.replace(allNewlines, '\\n');
+}
+
+function _filterStack(stack) {
+    return stack.split('\n')
+        .filter(stackLine => stackLine.indexOf('resource:///org/gjs/jsunit') === -1)
+        .filter(stackLine => stackLine.indexOf('<jasmine-start>') === -1)
+        .join('\n');
+}
+
+function _setTimeoutInternal(continueTimeout, func, time) {
+    return GLib.timeout_add(GLib.PRIORITY_DEFAULT, time, function () {
+        func();
+        return continueTimeout;
+    });
+}
+
+function _clearTimeoutInternal(id) {
+    if (id > 0)
+        GLib.source_remove(id);
+}
+
+// Install the browser setTimeout/setInterval API on the global object
+window.setTimeout = _setTimeoutInternal.bind(undefined, GLib.SOURCE_REMOVE);
+window.setInterval = _setTimeoutInternal.bind(undefined, GLib.SOURCE_CONTINUE);
+window.clearTimeout = window.clearInterval = _clearTimeoutInternal;
+
+let jasmineRequire = imports.jasmine.getJasmineRequireObj();
+let jasmineCore = jasmineRequire.core(jasmineRequire);
+window._jasmineEnv = jasmineCore.getEnv();
+
+window._jasmineMain = GLib.MainLoop.new(null, false);
+window._jasmineRetval = 0;
+
+// Install Jasmine API on the global object
+let jasmineInterface = jasmineRequire.interface(jasmineCore, window._jasmineEnv);
+Lang.copyProperties(jasmineInterface, window);
+
+// Reporter that outputs according to the Test Anything Protocol
+// See http://testanything.org/tap-specification.html
+const TapReporter = new Lang.Class({
+    Name: 'TapReporter',
+
+    _init: function () {
+        this._failedSuites = [];
+        this._specCount = 0;
+    },
+
+    jasmineStarted: function (info) {
+        print('1..' + info.totalSpecsDefined);
+    },
+
+    jasmineDone: function () {
+        this._failedSuites.forEach(failure => {
+            failure.failedExpectations.forEach(result => {
+                print('not ok - An error was thrown outside a test');
+                print('# ' + result.message);
+            });
+        });
+
+        window._jasmineMain.quit();
+    },
+
+    suiteDone: function (result) {
+        if (result.failedExpectations && result.failedExpectations.length > 0) {
+            window._jasmineRetval = 1;
+            this._failedSuites.push(result);
+        }
+
+        if (result.status === 'disabled') {
+            print('# Suite was disabled:', result.fullName);
+        }
+    },
+
+    specStarted: function () {
+        this._specCount++;
+    },
+
+    specDone: function (result) {
+        let tap_report;
+        if (result.status === 'failed') {
+            window._jasmineRetval = 1;
+            tap_report = 'not ok';
+        } else {
+            tap_report = 'ok';
+        }
+        tap_report += ' ' + this._specCount + ' ' + result.fullName;
+        if (result.status === 'pending' || result.status === 'disabled') {
+            let reason = result.pendingReason || result.status;
+            tap_report += ' # SKIP ' + reason;
+        }
+        print(tap_report);
+
+        // Print additional diagnostic info on failure
+        if (result.status === 'failed' && result.failedExpectations) {
+            result.failedExpectations.forEach((failedExpectation) => {
+                print('# Message:', _removeNewlines(failedExpectation.message));
+                print('# Stack:');
+                let stackTrace = _filterStack(failedExpectation.stack).trim();
+                print(stackTrace.split('\n').map((str) => '#   ' + str).join('\n'));
+            });
+        }
+    },
+});
+
+window._jasmineEnv.addReporter(new TapReporter());
diff --git a/installed-tests/js/test0020importer.js b/installed-tests/js/test0020importer.js
index 33586bd..a65241c 100644
--- a/installed-tests/js/test0020importer.js
+++ b/installed-tests/js/test0020importer.js
@@ -1,8 +1,6 @@
-const JSUnit = imports.jsUnit;
-
-function testImporter1() {
-    var GLib = imports.gi.GLib;
-    JSUnit.assertEquals(GLib.MAJOR_VERSION, 2);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+describe('Importer', function () {
+    it('can import GI modules', function () {
+        var GLib = imports.gi.GLib;
+        expect(GLib.MAJOR_VERSION).toEqual(2);
+    });
+});
diff --git a/installed-tests/js/test0030basicBoxed.js b/installed-tests/js/test0030basicBoxed.js
index f736c8a..bf4ad9c 100644
--- a/installed-tests/js/test0030basicBoxed.js
+++ b/installed-tests/js/test0030basicBoxed.js
@@ -1,10 +1,9 @@
-const JSUnit = imports.jsUnit;
 const Regress = imports.gi.Regress;
 
-function testBasicBoxed() {
-    var a = new Regress.TestSimpleBoxedA();
-    a.some_int = 42;
-    JSUnit.assertEquals(a.some_int, 42);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+describe('GI boxed type', function () {
+    it('has readable and writable fields', function () {
+        var a = new Regress.TestSimpleBoxedA();
+        a.some_int = 42;
+        expect(a.some_int).toEqual(42);
+    });
+});
diff --git a/installed-tests/js/test0040mainloop.js b/installed-tests/js/test0040mainloop.js
index 638688d..3a671e7 100644
--- a/installed-tests/js/test0040mainloop.js
+++ b/installed-tests/js/test0040mainloop.js
@@ -1,17 +1,15 @@
-const JSUnit = imports.jsUnit;
 var Mainloop = imports.mainloop;
 
-function testBasicMainloop() {
-    log('running mainloop test');
-    Mainloop.idle_add(function() { Mainloop.quit('testMainloop'); });
-    Mainloop.run('testMainloop');
-    log('mainloop test done');
-}
-
-/* A dangling mainloop idle should get removed and not leaked */
-function testDanglingIdle() {
-    Mainloop.idle_add(function() { return true; });
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
-
+describe('Mainloop module', function () {
+    it('runs a main loop', function (done) {
+        Mainloop.idle_add(function () {
+            Mainloop.quit('testMainloop');
+            done();
+        });
+        Mainloop.run('testMainloop');
+    });
+
+    it('removes a dangling idle and does not leak it', function () {
+        Mainloop.idle_add(function() { return true; });
+    });
+});
diff --git a/installed-tests/js/testByteArray.js b/installed-tests/js/testByteArray.js
index 7a357dc..97f93e3 100644
--- a/installed-tests/js/testByteArray.js
+++ b/installed-tests/js/testByteArray.js
@@ -1,121 +1,113 @@
-// tests for imports.lang module
-
-const JSUnit = imports.jsUnit;
 const ByteArray = imports.byteArray;
-const Gio = imports.gi.Gio;
-
-function testEmptyByteArray() {
-    let a = new ByteArray.ByteArray();
-    JSUnit.assertEquals("length is 0 for empty array", 0, a.length);
-}
-
-function testInitialSizeByteArray() {
-    let a = new ByteArray.ByteArray(10);
-    JSUnit.assertEquals("length is 10 for initially-sized-10 array", 10, a.length);
-
-    let i;
-
-    for (i = 0; i < a.length; ++i) {
-        JSUnit.assertEquals("new array initialized to zeroes", 0, a[i]);
-    }
-
-    JSUnit.assertEquals("array had proper number of elements post-construct (counting for)",
-                 10, i);
-}
-
-function testAssignment() {
-    let a = new ByteArray.ByteArray(256);
-    JSUnit.assertEquals("length is 256 for initially-sized-256 array", 256, a.length);
-
-    let i;
-    let count;
-
-    count = 0;
-    for (i = 0; i < a.length; ++i) {
-        JSUnit.assertEquals("new array initialized to zeroes", 0, a[i]);
-        a[i] = 255 - i;
-        count += 1;
-    }
-
-    JSUnit.assertEquals("set proper number of values", 256, count);
-
-    count = 0;
-    for (i = 0; i < a.length; ++i) {
-        JSUnit.assertEquals("assignment set expected value", 255 - i, a[i]);
-        count += 1;
-    }
-
-    JSUnit.assertEquals("checked proper number of values", 256, count);
-}
-
-function testAssignmentPastEnd() {
-    let a = new ByteArray.ByteArray();
-    JSUnit.assertEquals("length is 0 for empty array", 0, a.length);
-
-    a[2] = 5;
-    JSUnit.assertEquals("implicitly made length 3", 3, a.length);
-    JSUnit.assertEquals("implicitly-created zero byte", 0, a[0]);
-    JSUnit.assertEquals("implicitly-created zero byte", 0, a[1]);
-    JSUnit.assertEquals("stored 5 in autocreated position", 5, a[2]);
-}
-
-function testAssignmentToLength() {
-    let a = new ByteArray.ByteArray(20);
-    JSUnit.assertEquals("length is 20 for new array", 20, a.length);
-
-    a.length = 5;
-
-    JSUnit.assertEquals("length is 5 after setting it to 5", 5, a.length);
-}
-
-function testNonIntegerAssignment() {
-    let a = new ByteArray.ByteArray();
-
-    a[0] = 5;
-    JSUnit.assertEquals("assigning 5 gives a byte 5", 5, a[0]);
-
-    a[0] = null;
-    JSUnit.assertEquals("assigning null gives a zero byte", 0, a[0]);
-
-    a[0] = 5;
-    JSUnit.assertEquals("assigning 5 gives a byte 5", 5, a[0]);
-
-    a[0] = undefined;
-    JSUnit.assertEquals("assigning undefined gives a zero byte", 0, a[0]);
-
-    a[0] = 3.14;
-    JSUnit.assertEquals("assigning a double rounds off", 3, a[0]);
-}
-
-function testFromString() {
-    let a = ByteArray.fromString('abcd');
-    JSUnit.assertEquals("from string 'abcd' gives length 4", 4, a.length);
-    JSUnit.assertEquals("'a' results in 97", 97, a[0]);
-    JSUnit.assertEquals("'b' results in 98", 98, a[1]);
-    JSUnit.assertEquals("'c' results in 99", 99, a[2]);
-    JSUnit.assertEquals("'d' results in 100", 100, a[3]);
-}
-
-function testFromArray() {
-    let a = ByteArray.fromArray([ 1, 2, 3, 4 ]);
-    JSUnit.assertEquals("from array [1,2,3,4] gives length 4", 4, a.length);
-    JSUnit.assertEquals("a[0] == 1", 1, a[0]);
-    JSUnit.assertEquals("a[1] == 2", 2, a[1]);
-    JSUnit.assertEquals("a[2] == 3", 3, a[2]);
-    JSUnit.assertEquals("a[3] == 4", 4, a[3]);
-}
-
-function testToString() {
-    let a = new ByteArray.ByteArray();
-    a[0] = 97;
-    a[1] = 98;
-    a[2] = 99;
-    a[3] = 100;
-
-    let s = a.toString();
-    JSUnit.assertEquals("toString() on 4 ascii bytes gives length 4", 4, s.length);
-    JSUnit.assertEquals("toString() gives 'abcd'", "abcd", s);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
 
+describe('Byte array', function () {
+    it('has length 0 for empty array', function () {
+        let a = new ByteArray.ByteArray();
+        expect(a.length).toEqual(0);
+    });
+
+    describe('initially sized to 10', function () {
+        let a;
+        beforeEach(function () {
+            a = new ByteArray.ByteArray(10);
+        });
+
+        it('has length 10', function () {
+            expect(a.length).toEqual(10);
+        });
+
+        it('is initialized to zeroes', function () {
+            for (let i = 0; i < a.length; ++i) {
+                expect(a[i]).toEqual(0);
+            }
+        });
+    });
+
+    it('assigns values correctly', function () {
+        let a = new ByteArray.ByteArray(256);
+
+        for (let i = 0; i < a.length; ++i) {
+            a[i] = 255 - i;
+        }
+
+        for (let i = 0; i < a.length; ++i) {
+            expect(a[i]).toEqual(255 - i);
+        }
+    });
+
+    describe('assignment past end', function () {
+        let a;
+        beforeEach(function () {
+            a = new ByteArray.ByteArray();
+            a[2] = 5;
+        });
+
+        it('implicitly lengthens the array', function () {
+            expect(a.length).toEqual(3);
+            expect(a[2]).toEqual(5);
+        });
+
+        it('implicitly creates zero bytes', function () {
+            expect(a[0]).toEqual(0);
+            expect(a[1]).toEqual(0);
+        });
+    });
+
+    it('changes the length when assigning to length property', function () {
+        let a = new ByteArray.ByteArray(20);
+        expect(a.length).toEqual(20);
+        a.length = 5;
+        expect(a.length).toEqual(5);
+    });
+
+    describe('conversions', function () {
+        let a;
+        beforeEach(function () {
+            a = new ByteArray.ByteArray();
+            a[0] = 255;
+        });
+
+        it('gives a byte 5 when assigning 5', function () {
+            a[0] = 5;
+            expect(a[0]).toEqual(5);
+        });
+
+        it('gives a byte 0 when assigning null', function () {
+            a[0] = null;
+            expect(a[0]).toEqual(0);
+        });
+
+        it('gives a byte 0 when assigning undefined', function () {
+            a[0] = undefined;
+            expect(a[0]).toEqual(0);
+        });
+
+        it('rounds off when assigning a double', function () {
+            a[0] = 3.14;
+            expect(a[0]).toEqual(3);
+        });
+    });
+
+    it('can be created from a string', function () {
+        let a = ByteArray.fromString('abcd');
+        expect(a.length).toEqual(4);
+        [97, 98, 99, 100].forEach((val, ix) => expect(a[ix]).toEqual(val));
+    });
+
+    it('can be created from an array', function () {
+        let a = ByteArray.fromArray([ 1, 2, 3, 4 ]);
+        expect(a.length).toEqual(4);
+        [1, 2, 3, 4].forEach((val, ix) => expect(a[ix]).toEqual(val));
+    });
+
+    it('can be converted to a string of ASCII characters', function () {
+        let a = new ByteArray.ByteArray();
+        a[0] = 97;
+        a[1] = 98;
+        a[2] = 99;
+        a[3] = 100;
+        let s = a.toString();
+        expect(s.length).toEqual(4);
+        expect(s).toEqual('abcd');
+    });
+});
diff --git a/installed-tests/js/testClass.js b/installed-tests/js/testClass.js
index f41fcb9..066134e 100644
--- a/installed-tests/js/testClass.js
+++ b/installed-tests/js/testClass.js
@@ -1,15 +1,7 @@
 // -*- mode: js; indent-tabs-mode: nil -*-
 
-const JSUnit = imports.jsUnit;
 const Lang = imports.lang;
 
-function assertArrayEquals(expected, got) {
-    JSUnit.assertEquals(expected.length, got.length);
-    for (let i = 0; i < expected.length; i ++) {
-        JSUnit.assertEquals(expected[i], got[i]);
-    }
-}
-
 const MagicBase = new Lang.Class({
     Name: 'MagicBase',
 
@@ -51,15 +43,6 @@ const Magic = new Lang.Class({
     }
 });
 
-const ToStringOverride = new Lang.Class({
-    Name: 'ToStringOverride',
-
-    toString: function() {
-        let oldToString = this.parent();
-        return oldToString + '; hello';
-    }
-});
-
 const Accessor = new Lang.Class({
     Name: 'AccessorMagic',
 
@@ -87,120 +70,126 @@ const AbstractBase = new Lang.Class({
     }
 });
 
-const AbstractImpl = new Lang.Class({
-    Name: 'AbstractImpl',
-    Extends: AbstractBase,
-
-    _init: function() {
-        this.parent();
-        this.bar = 42;
-    }
-});
-
-const AbstractImpl2 = new Lang.Class({
-    Name: 'AbstractImpl2',
-    Extends: AbstractBase,
+describe('Class framework', function () {
+    it('calls _init constructors', function () {
+        let newMagic = new MagicBase('A');
+        expect(newMagic.a).toEqual('A');
+    });
 
-    // no _init here, we inherit the parent one
-});
+    it('calls parent constructors', function () {
+        let buffer = [];
 
-const CustomConstruct = new Lang.Class({
-    Name: 'CustomConstruct',
+        let newMagic = new Magic('a', 'b', buffer);
+        expect(buffer).toEqual(['a', 'b']);
 
-    _construct: function(one, two) {
-        return [one, two];
-    }
-});
+        buffer = [];
+        let val = newMagic.foo(10, 20, buffer);
+        expect(buffer).toEqual([10, 20]);
+        expect(val).toEqual(10 * 6);
+    });
 
-function testClassFramework() {
-    let newMagic = new MagicBase('A');
-    JSUnit.assertEquals('A',  newMagic.a);
-}
+    it('sets the right constructor properties', function () {
+        expect(Magic.prototype.constructor).toBe(Magic);
 
-function testInheritance() {
-    let buffer = [];
+        let newMagic = new Magic();
+        expect(newMagic.constructor).toBe(Magic);
+    });
 
-    let newMagic = new Magic('a', 'b', buffer);
-    assertArrayEquals(['a', 'b'], buffer);
+    it('sets up instanceof correctly', function () {
+        let newMagic = new Magic();
 
-    buffer = [];
-    let val = newMagic.foo(10, 20, buffer);
-    assertArrayEquals([10, 20], buffer);
-    JSUnit.assertEquals(10*6, val);
-}
+        expect(newMagic instanceof Magic).toBeTruthy();
+        expect(newMagic instanceof MagicBase).toBeTruthy();
+    });
 
-function testConstructor() {
-    JSUnit.assertEquals(Magic, Magic.prototype.constructor);
+    it('reports a sensible value for toString()', function () {
+        let newMagic = new MagicBase();
+        expect(newMagic.toString()).toEqual('[object MagicBase]');
+    });
 
-    let newMagic = new Magic();
-    JSUnit.assertEquals(Magic, newMagic.constructor);
-}
+    it('allows overriding toString()', function () {
+        const ToStringOverride = new Lang.Class({
+            Name: 'ToStringOverride',
 
-function testInstanceOf() {
-    let newMagic = new Magic();
+            toString: function() {
+                let oldToString = this.parent();
+                return oldToString + '; hello';
+            }
+        });
 
-    JSUnit.assertTrue(newMagic instanceof Magic);
-    JSUnit.assertTrue(newMagic instanceof MagicBase);
-}
+        let override = new ToStringOverride();
+        expect(override.toString()).toEqual('[object ToStringOverride]; hello');
+    });
 
-function testToString() {
-    let newMagic = new MagicBase();
-    JSUnit.assertEquals('[object MagicBase]', newMagic.toString());
+    it('is not configurable', function () {
+        let newMagic = new MagicBase();
 
-    let override = new ToStringOverride();
-    JSUnit.assertEquals('[object ToStringOverride]; hello', override.toString());
-}
+        delete newMagic.foo;
+        expect(newMagic.foo).toBeDefined();
+    });
 
-function testConfigurable() {
-    let newMagic = new MagicBase();
+    it('allows accessors for properties', function () {
+        let newAccessor = new Accessor(11);
 
-    delete newMagic.foo;
-    JSUnit.assertNotUndefined(newMagic.foo);
-}
+        expect(newAccessor.value).toEqual(11);
+        expect(() => newAccessor.value = 12).toThrow();
 
-function testAccessor() {
-    let newAccessor = new Accessor(11);
+        newAccessor.value = 42;
+        expect(newAccessor.value).toEqual(42);
+    });
 
-    JSUnit.assertEquals(11, newAccessor.value);
-    JSUnit.assertRaises(function() {
-        newAccessor.value = 12;
+    it('raises an exception when creating an abstract class', function () {
+        expect(() => new AbstractBase()).toThrow();
     });
 
-    newAccessor.value = 42;
-    JSUnit.assertEquals(42, newAccessor.value);
-}
+    it('inherits properties from abstract base classes', function () {
+        const AbstractImpl = new Lang.Class({
+            Name: 'AbstractImpl',
+            Extends: AbstractBase,
+
+            _init: function() {
+                this.parent();
+                this.bar = 42;
+            }
+        });
 
-function testAbstract() {
-    JSUnit.assertRaises(function() {
-        let newAbstract = new AbstractBase();
+        let newAbstract = new AbstractImpl();
+        expect(newAbstract.foo).toEqual(42);
+        expect(newAbstract.bar).toEqual(42);
     });
 
-    let newAbstract = new AbstractImpl();
-    JSUnit.assertEquals(42, newAbstract.foo);
-    JSUnit.assertEquals(42, newAbstract.bar);
+    it('inherits constructors from abstract base classes', function () {
+        const AbstractImpl = new Lang.Class({
+            Name: 'AbstractImpl',
+            Extends: AbstractBase,
+        });
 
-    newAbstract = new AbstractImpl2();
-    JSUnit.assertEquals(42, newAbstract.foo);
-}
+        let newAbstract = new AbstractImpl();
+        expect(newAbstract.foo).toEqual(42);
+    });
 
-function testCrossCall() {
-    // test that a method can call another without clobbering
-    // __caller__
-    let newMagic = new Magic();
-    let buffer = [];
+    it('lets methods call other methods without clobbering __caller__', function () {
+        let newMagic = new Magic();
+        let buffer = [];
 
-    let res = newMagic.bar(10, buffer);
-    assertArrayEquals([10, 20], buffer);
-    JSUnit.assertEquals(50, res);
-}
+        let res = newMagic.bar(10, buffer);
+        expect(buffer).toEqual([10, 20]);
+        expect(res).toEqual(50);
+    });
 
-function testConstruct() {
-    let instance = new CustomConstruct(1, 2);
+    it('allows custom return values from constructors', function () {
+        const CustomConstruct = new Lang.Class({
+            Name: 'CustomConstruct',
 
-    JSUnit.assertTrue(instance instanceof Array);
-    JSUnit.assertTrue(!(instance instanceof CustomConstruct));
+            _construct: function(one, two) {
+                return [one, two];
+            }
+        });
 
-    assertArrayEquals([1, 2], instance);
-}
+        let instance = new CustomConstruct(1, 2);
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+        expect(instance instanceof Array).toBeTruthy();
+        expect(instance instanceof CustomConstruct).toBeFalsy();
+        expect(instance).toEqual([1, 2]);
+    });
+});
diff --git a/installed-tests/js/testCoverage.js b/installed-tests/js/testCoverage.js
index 6b48adb..8224d93 100644
--- a/installed-tests/js/testCoverage.js
+++ b/installed-tests/js/testCoverage.js
@@ -1,1499 +1,1156 @@
-const JSUnit = imports.jsUnit;
 const Coverage = imports.coverage;
 
-function parseScriptForExpressionLines(script) {
-    const ast = Reflect.parse(script);
-    return Coverage.expressionLinesForAST(ast);
-}
-
-function assertArrayEquals(actual, expected, assertion) {
-    if (actual.length != expected.length)
-        throw new Error("Arrays not equal length. Actual array was " +
-                        actual.length + " and Expected array was " +
-                        expected.length);
-
-    for (let i = 0; i < actual.length; i++)
-        assertion(expected[i], actual[i]);
-}
-
-function testExpressionLinesWithNoTrailingNewline() {
-    let foundLines = parseScriptForExpressionLines("let x;\n" +
-                                                   "let y;");
-    assertArrayEquals(foundLines, [1, 2], JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForAssignmentExpressionSides() {
-    let foundLinesOnBothExpressionSides =
-        parseScriptForExpressionLines("var x;\n" +
-                                      "x = (function() {\n" +
-                                      "    return 10;\n" +
-                                      "})();\n");
-    assertArrayEquals(foundLinesOnBothExpressionSides,
-                      [1, 2, 3],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForLinesInsideFunctions() {
-    let foundLinesInsideNamedFunction =
-        parseScriptForExpressionLines("function f(a, b) {\n" +
-                                      "    let x = a;\n" +
-                                      "    let y = b;\n" +
-                                      "    return x + y;\n" +
-                                      "}\n" +
-                                      "\n" +
-                                      "var z = f(1, 2);\n");
-    assertArrayEquals(foundLinesInsideNamedFunction,
-                      [2, 3, 4, 7],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForLinesInsideAnonymousFunctions() {
-    let foundLinesInsideAnonymousFunction =
-        parseScriptForExpressionLines("var z = (function f(a, b) {\n" +
-                                      "     let x = a;\n" +
-                                      "     let y = b;\n" +
-                                      "     return x + y;\n" +
-                                      " })();\n");
-    assertArrayEquals(foundLinesInsideAnonymousFunction,
-                      [1, 2, 3, 4],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForBodyOfFunctionProperty() {
-    let foundLinesInsideFunctionProperty =
-        parseScriptForExpressionLines("var o = {\n" +
-                                      "    foo: function() {\n" +
-                                      "        let x = a;\n" +
-                                      "    }\n" +
-                                      "};\n");
-    assertArrayEquals(foundLinesInsideFunctionProperty,
-                      [1, 2, 3],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForCallArgsOfFunctionProperty() {
-    let foundLinesInsideCallArgs =
-        parseScriptForExpressionLines("function f(a) {\n" +
-                                      "}\n" +
-                                      "f({\n" +
-                                      "    foo: function() {\n" +
-                                      "        let x = a;\n" +
-                                      "    }\n" +
-                                      "});\n");
-    assertArrayEquals(foundLinesInsideCallArgs,
-                      [1, 3, 4, 5],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForMultilineCallArgs() {
-    let foundLinesInsideMultilineCallArgs =
-        parseScriptForExpressionLines("function f(a, b, c) {\n" +
-                                      "}\n" +
-                                      "f(1,\n" +
-                                      "  2,\n" +
-                                      "  3);\n");
-    assertArrayEquals(foundLinesInsideMultilineCallArgs,
-                      [1, 3, 4, 5],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForNewCallWithObject() {
-    let foundLinesInsideObjectCallArg =
-        parseScriptForExpressionLines("function f(o) {\n" +
-                                      "}\n" +
-                                      "let obj = {\n" +
-                                      "    Name: new f({ a: 1,\n" +
-                                      "                  b: 2,\n" +
-                                      "                  c: 3\n" +
-                                      "                })\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideObjectCallArg,
-                      [1, 3, 4, 5, 6],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForWhileLoop() {
-    let foundLinesInsideWhileLoop =
-        parseScriptForExpressionLines("var a = 0;\n" +
-                                      "while (a < 1) {\n" +
-                                      "    let x = 0;\n" +
-                                      "    let y = 1;\n" +
-                                      "    a++;" +
-                                      "\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideWhileLoop,
-                      [1, 2, 3, 4, 5],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForTryCatchFinally() {
-    let foundLinesInsideTryCatchFinally =
-        parseScriptForExpressionLines("var a = 0;\n" +
-                                      "try {\n" +
-                                      "    a++;\n" +
-                                      "} catch (e) {\n" +
-                                      "    a++;\n" +
-                                      "} finally {\n" +
-                                      "    a++;\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideTryCatchFinally,
-                      [1, 2, 3, 4, 5, 7],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForCaseStatements() {
-    let foundLinesInsideCaseStatements =
-        parseScriptForExpressionLines("var a = 0;\n" +
-                                      "switch (a) {\n" +
-                                      "case 1:\n" +
-                                      "    a++;\n" +
-                                      "    break;\n" +
-                                      "case 2:\n" +
-                                      "    a++;\n" +
-                                      "    break;\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideCaseStatements,
-                      [1, 2, 4, 5, 7, 8],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForCaseStatementsCharacters() {
-    let foundLinesInsideCaseStatements =
-        parseScriptForExpressionLines("var a = 'a';\n" +
-                                      "switch (a) {\n" +
-                                      "case 'a':\n" +
-                                      "    a++;\n" +
-                                      "    break;\n" +
-                                      "case 'b':\n" +
-                                      "    a++;\n" +
-                                      "    break;\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideCaseStatements,
-                      [1, 2, 4, 5, 7, 8],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForLoop() {
-    let foundLinesInsideLoop =
-        parseScriptForExpressionLines("for (let i = 0; i < 1; i++) {\n" +
-                                      "    let x = 0;\n" +
-                                      "    let y = 1;\n" +
-                                      "\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideLoop,
-                      [1, 2, 3],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForIfExits() {
-    let foundLinesInsideIfExits =
-        parseScriptForExpressionLines("if (1 > 0) {\n" +
-                                      "    let i = 0;\n" +
-                                      "} else {\n" +
-                                      "    let j = 1;\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideIfExits,
-                      [1, 2, 4],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForAllLinesOfMultilineIfTests() {
-    let foundLinesInsideMultilineIfTest =
-        parseScriptForExpressionLines("if (1 > 0 &&\n" +
-                                      "    2 > 0 &&\n" +
-                                      "    3 > 0) {\n" +
-                                      "    let a = 3;\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideMultilineIfTest,
-                      [1, 2, 3, 4],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForObjectPropertyLiterals() {
-    let foundLinesInsideObjectPropertyLiterals =
-        parseScriptForExpressionLines("var a = {\n" +
-                                      "    Name: 'foo',\n" +
-                                      "    Ex: 'bar'\n" +
-                                      "};\n");
-    assertArrayEquals(foundLinesInsideObjectPropertyLiterals,
-                      [1, 2, 3],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForObjectPropertyFunction() {
-    let foundLinesInsideObjectPropertyFunction =
-        parseScriptForExpressionLines("var a = {\n" +
-                                      "    Name: function() {},\n" +
-                                      "};\n");
-    assertArrayEquals(foundLinesInsideObjectPropertyFunction,
-                      [1, 2],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForObjectPropertyObjectExpression() {
-    let foundLinesInsideObjectPropertyObjectExpression =
-        parseScriptForExpressionLines("var a = {\n" +
-                                      "    Name: {},\n" +
-                                      "};\n");
-    assertArrayEquals(foundLinesInsideObjectPropertyObjectExpression,
-                      [1, 2],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForObjectPropertyArrayExpression() {
-    let foundLinesInsideObjectPropertyObjectExpression =
-        parseScriptForExpressionLines("var a = {\n" +
-                                      "    Name: [],\n" +
-                                      "};\n");
-    assertArrayEquals(foundLinesInsideObjectPropertyObjectExpression,
-                      [1, 2],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForObjectArgsToReturn() {
-    let foundLinesInsideObjectArgsToReturn =
-        parseScriptForExpressionLines("function f() {\n" +
-                                      "    return {};\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideObjectArgsToReturn,
-                      [2],
-                      JSUnit.assertEquals);
-}
-
-function testExpressionLinesFoundForObjectArgsToThrow() {
-    let foundLinesInsideObjectArgsToThrow =
-        parseScriptForExpressionLines("function f() {\n" +
-                                      "    throw {\n" +
-                                      "        a: 1,\n" +
-                                      "        b: 2\n" +
-                                      "    }\n" +
-                                      "}\n");
-    assertArrayEquals(foundLinesInsideObjectArgsToThrow,
-                      [2, 3, 4],
-                      JSUnit.assertEquals);
-}
-
-
-function parseScriptForFunctionNames(script) {
-    const ast = Reflect.parse(script);
-    return Coverage.functionsForAST(ast);
-}
-
-function functionDeclarationsEqual(actual, expected) {
-    JSUnit.assertEquals(expected.key, actual.key);
-    JSUnit.assertEquals(expected.line, actual.line);
-    JSUnit.assertEquals(expected.n_params, actual.n_params);
-}
-
-function testFunctionsFoundNoTrailingNewline() {
-    let foundFuncs = parseScriptForFunctionNames("function f1() {}\n" +
-                                                 "function f2() {}");
-    assertArrayEquals(foundFuncs,
-                      [
-                          { key: "f1:1:0", line: 1, n_params: 0 },
-                          { key: "f2:2:0", line: 2, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsFoundForDeclarations() {
-    let foundFunctionDeclarations =
-        parseScriptForFunctionNames("function f1() {}\n" +
-                                    "function f2() {}\n" +
-                                    "function f3() {}\n");
-    assertArrayEquals(foundFunctionDeclarations,
-                      [
-                          { key: "f1:1:0", line: 1, n_params: 0 },
-                          { key: "f2:2:0", line: 2, n_params: 0 },
-                          { key: "f3:3:0", line: 3, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsFoundForNestedFunctions() {
-    let foundFunctions =
-        parseScriptForFunctionNames("function f1() {\n" +
-                                    "    let f2 = function() {\n" +
-                                    "        let f3 = function() {\n" +
-                                    "        }\n" +
-                                    "    }\n" +
-                                    "}\n");
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "f1:1:0", line: 1, n_params: 0 },
-                          { key: "(anonymous):2:0", line: 2, n_params: 0 },
-                          { key: "(anonymous):3:0", line: 3, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsFoundOnSameLineButDifferentiatedOnArgs() {
-    /* Note the lack of newlines. This is all on
-     * one line */
-    let foundFunctionsOnSameLine =
-        parseScriptForFunctionNames("function f1() {" +
-                                    "    return (function(a) {" +
-                                    "        return function(a, b) {}" +
-                                    "    });" +
-                                    "}");
-    assertArrayEquals(foundFunctionsOnSameLine,
-                      [
-                          { key: "f1:1:0", line: 1, n_params: 0 },
-                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideArrayExpression() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a = [function() {}];\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 },
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideArrowExpression() {
-    let foundFunctions =
-        parseScriptForFunctionNames("(a) => (function() {})();\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideSequence() {
-    let foundFunctions =
-        parseScriptForFunctionNames("(function(a) {})()," +
-                                    "(function(a, b) {})();\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                          { key: "(anonymous):1:2", line: 1, n_params: 2 },
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideUnaryExpression() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a = (function() {}())++;\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 },
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideBinaryExpression() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a = function(a) {}() +" +
-                                    " function(a, b) {}();\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideAssignmentExpression() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a = function() {}();\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideUpdateExpression() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a;\n" +
-                                    "a += function() {}();\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):2:0", line: 2, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideIfConditions() {
-    let foundFunctions =
-        parseScriptForFunctionNames("if (function(a) {}(a) >" +
-                                    "    function(a, b) {}(a, b)) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideWhileConditions() {
-    let foundFunctions =
-        parseScriptForFunctionNames("while (function(a) {}(a) >" +
-                                    "       function(a, b) {}(a, b)) {};\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideForInitializer() {
-    let foundFunctions =
-        parseScriptForFunctionNames("for (function() {}; ;) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-/* SpiderMonkey parses for (let i = <init>; <cond>; <update>) as though
- * they were let i = <init> { for (; <cond> <update>) } so test the
- * LetStatement initializer case too */
-function testFunctionsInsideForLetInitializer() {
-    let foundFunctions =
-        parseScriptForFunctionNames("for (let i = function() {}; ;) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideForVarInitializer() {
-    let foundFunctions =
-        parseScriptForFunctionNames("for (var i = function() {}; ;) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideForCondition() {
-    let foundFunctions =
-        parseScriptForFunctionNames("for (; function() {}();) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideForIncrement() {
-    let foundFunctions =
-        parseScriptForFunctionNames("for (; ;function() {}()) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideForInObject() {
-    let foundFunctions =
-        parseScriptForFunctionNames("for (let x in function() {}()) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideForEachInObject() {
-    let foundFunctions =
-        parseScriptForFunctionNames("for each (x in function() {}()) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInsideForOfObject() {
-    let foundFunctions =
-        parseScriptForFunctionNames("for (x of (function() {}())) {}\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsUsedAsObjectFound() {
-    let foundFunctions =
-        parseScriptForFunctionNames("f = function() {}.bind();\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsUsedAsObjectDynamicProp() {
-    let foundFunctions =
-        parseScriptForFunctionNames("f = function() {}['bind']();\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsOnEitherSideOfLogicalExpression() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let f = function(a) {} ||" +
-                                    " function(a, b) {};\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):1:1", line: 1, n_params: 1 },
-                          { key: "(anonymous):1:2", line: 1, n_params: 2 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsOnEitherSideOfConditionalExpression() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a\n" +
-                                    "let f = a ? function(a) {}() :" +
-                                    " function(a, b) {}();\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):2:1", line: 2, n_params: 1 },
-                          { key: "(anonymous):2:2", line: 2, n_params: 2 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsYielded() {
-    let foundFunctions =
-        parseScriptForFunctionNames("function a() { yield function (){} };\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "a:1:0", line: 1, n_params: 0 },
-                          { key: "(anonymous):1:0", line: 1, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInArrayComprehensionBody() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a = new Array(1);\n" +
-                                    "let b = [function() {} for (i of a)];\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):2:0", line: 2, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInArrayComprehensionBlock() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a = new Array(1);\n" +
-                                    "let b = [i for (i of function() {})];\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):2:0", line: 2, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function testFunctionsInArrayComprehensionFilter() {
-    let foundFunctions =
-        parseScriptForFunctionNames("let a = new Array(1);\n" +
-                                    "let b = [i for (i of a)" +
-                                    "if (function() {}())];\n");
-
-    assertArrayEquals(foundFunctions,
-                      [
-                          { key: "(anonymous):2:0", line: 2, n_params: 0 }
-                      ],
-                      functionDeclarationsEqual);
-}
-
-function parseScriptForBranches(script) {
-    const ast = Reflect.parse(script);
-    return Coverage.branchesForAST(ast);
-}
-
-function branchInfoEqual(actual, expected) {
-    JSUnit.assertEquals(expected.point, actual.point);
-    assertArrayEquals(expected.exits, actual.exits, JSUnit.assertEquals);
-}
-
-function testFindBranchWhereNoTrailingNewline() {
-    let foundBranchExits = parseScriptForBranches("if (1) { let a = 1; }");
-    assertArrayEquals(foundBranchExits,
-                      [
-                          { point: 1, exits: [1] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testBothBranchExitsFoundForSimpleBranch() {
-    let foundBranchExitsForSimpleBranch =
-        parseScriptForBranches("if (1) {\n" +
-                               "    let a = 1;\n" +
-                               "} else {\n" +
-                               "    let b = 2;\n" +
-                               "}\n");
-    assertArrayEquals(foundBranchExitsForSimpleBranch,
-                      [
-                          { point: 1, exits: [2, 4] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testSingleExitFoundForBranchWithOneConsequent() {
-    let foundBranchExitsForSingleConsequentBranch =
-        parseScriptForBranches("if (1) {\n" +
-                               "    let a = 1.0;\n" +
-                               "}\n");
-    assertArrayEquals(foundBranchExitsForSingleConsequentBranch,
-                      [
-                          { point: 1, exits: [2] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testMultipleBranchesFoundForNestedIfElseBranches() {
-    let foundBranchesForNestedIfElseBranches =
-        parseScriptForBranches("if (1) {\n" +
-                               "    let a = 1.0;\n" +
-                               "} else if (2) {\n" +
-                               "    let b = 2.0;\n" +
-                               "} else if (3) {\n" +
-                               "    let c = 3.0;\n" +
-                               "} else {\n" +
-                               "    let d = 4.0;\n" +
-                               "}\n");
-    assertArrayEquals(foundBranchesForNestedIfElseBranches,
-                      [
-                          /* the 'else if' line is actually an
-                           * exit for the first branch */
-                          { point: 1, exits: [2, 3] },
-                          { point: 3, exits: [4, 5] },
-                          /* 'else' by itself is not executable,
-                           * it is the block it contains whcih
-                           * is */
-                          { point: 5, exits: [6, 8] }
-                      ],
-                      branchInfoEqual);
-}
-
-
-function testSimpleTwoExitBranchWithoutBlocks() {
-    let foundBranches =
-        parseScriptForBranches("let a, b;\n" +
-                               "if (1)\n" +
-                               "    a = 1.0\n" +
-                               "else\n" +
-                               "    b = 2.0\n" +
-                               "\n");
-    assertArrayEquals(foundBranches,
-                      [
-                          { point: 2, exits: [3, 5] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testNoBranchFoundIfConsequentWasEmpty() {
-    let foundBranches =
-        parseScriptForBranches("let a, b;\n" +
-                               "if (1) {}\n");
-    assertArrayEquals(foundBranches,
-                      [],
-                      branchInfoEqual);
-}
-
-function testSingleExitFoundIfOnlyAlternateExitDefined() {
-    let foundBranchesForOnlyAlternateDefinition =
-        parseScriptForBranches("let a, b;\n" +
-                               "if (1) {}\n" +
-                               "else\n" +
-                               "    a++;\n");
-    assertArrayEquals(foundBranchesForOnlyAlternateDefinition,
-                      [
-                          { point: 2, exits: [4] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testImplicitBranchFoundForWhileStatement() {
-    let foundBranchesForWhileStatement =
-        parseScriptForBranches("while (1) {\n" +
-                               "    let a = 1;\n" +
-                               "}\n" +
-                               "let b = 2;");
-    assertArrayEquals(foundBranchesForWhileStatement,
-                      [
-                          { point: 1, exits: [2] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testImplicitBranchFoundForDoWhileStatement() {
-    let foundBranchesForDoWhileStatement =
-        parseScriptForBranches("do {\n" +
-                               "    let a = 1;\n" +
-                               "} while (1)\n" +
-                               "let b = 2;");
-    assertArrayEquals(foundBranchesForDoWhileStatement,
-                      [
-                          /* For do-while loops the branch-point is
-                           * at the 'do' condition and not the
-                           * 'while' */
-                          { point: 1, exits: [2] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testAllExitsFoundForCaseStatements() {
-    let foundExitsInCaseStatement =
-        parseScriptForBranches("let a = 1;\n" +
-                               "switch (1) {\n" +
-                               "case '1':\n" +
-                               "    a++;\n" +
-                               "    break;\n" +
-                               "case '2':\n" +
-                               "    a++\n" +
-                               "    break;\n" +
-                               "default:\n" +
-                               "    a++\n" +
-                               "    break;\n" +
-                               "}\n");
-    assertArrayEquals(foundExitsInCaseStatement,
-                      [
-                          /* There are three potential exits here */
-                          { point: 2, exits: [4, 7, 10] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testAllExitsFoundForFallthroughCaseStatements() {
-    let foundExitsInCaseStatement =
-        parseScriptForBranches("let a = 1;\n" +
-                               "switch (1) {\n" +
-                               "case '1':\n" +
-                               "case 'a':\n" +
-                               "case 'b':\n" +
-                               "    a++;\n" +
-                               "    break;\n" +
-                               "case '2':\n" +
-                               "    a++\n" +
-                               "    break;\n" +
-                               "default:\n" +
-                               "    a++\n" +
-                               "    break;\n" +
-                               "}\n");
-    assertArrayEquals(foundExitsInCaseStatement,
-                      [
-                          /* There are three potential exits here */
-                          { point: 2, exits: [6, 9, 12] }
-                      ],
-                      branchInfoEqual);
-}
-
-function testAllNoExitsFoundForCaseStatementsWithNoopLabels() {
-    let foundExitsInCaseStatement =
-        parseScriptForBranches("let a = 1;\n" +
-                               "switch (1) {\n" +
-                               "case '1':\n" +
-                               "case '2':\n" +
-                               "default:\n" +
-                               "}\n");
-    assertArrayEquals(foundExitsInCaseStatement,
-                      [],
-                      branchInfoEqual);
-}
-
-
-function testGetNumberOfLinesInScript() {
-    let script = "\n\n";
-    let number = Coverage._getNumberOfLinesForScript(script);
-    JSUnit.assertEquals(3, number);
-}
-
-function testZeroExpressionLinesToCounters() {
-    let expressionLines = [];
-    let nLines = 1;
-    let counters = Coverage._expressionLinesToCounters(expressionLines, nLines);
-
-    assertArrayEquals([undefined, undefined], counters, JSUnit.assertEquals);
-}
-
-function testSingleExpressionLineToCounters() {
-    let expressionLines = [1, 2];
-    let nLines = 4;
-    let counters = Coverage._expressionLinesToCounters(expressionLines, nLines);
-
-    assertArrayEquals([undefined, 0, 0, undefined, undefined],
-                      counters, JSUnit.assertEquals);
-}
-
-const MockFoundBranches = [
-    {
-        point: 5,
-        exits: [6, 8]
-    },
-    {
-        point: 1,
-        exits: [2, 4]
-    }
-];
-
-const MockNLines = 9;
-
-function testGetsSameNumberOfCountersAsNLinesPlusOne() {
-    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
-    JSUnit.assertEquals(MockNLines + 1, counters.length);
-}
-
-function testEmptyArrayReturnedForNoBranches() {
-    let counters = Coverage._branchesToBranchCounters([], 1);
-    assertArrayEquals([undefined, undefined], counters, JSUnit.assertEquals);
-}
-
-function testBranchesOnLinesForArrayIndicies() {
-    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
-    JSUnit.assertNotEquals(undefined, counters[1]);
-    JSUnit.assertNotEquals(undefined, counters[5]);
-}
-
-function testExitsSetForBranch() {
-    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
-    let countersForFirstBranch = counters[1];
-
-    assertArrayEquals(countersForFirstBranch.exits,
-                      [
-                          { line: 2, hitCount: 0 },
-                          { line: 4, hitCount: 0 }
-                      ],
-                      function(expectedExit, actualExit) {
-                          JSUnit.assertEquals(expectedExit.line, actualExit.line);
-                          JSUnit.assertEquals(expectedExit.hitCount, actualExit.hitCount);
-                      });
-}
-
-function testLastExitIsSetToHighestExitStartLine() {
-    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
-    let countersForFirstBranch = counters[1];
-
-    JSUnit.assertEquals(4, countersForFirstBranch.lastExit);
-}
-
-function testHitIsAlwaysInitiallyFalse() {
-    let counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
-    let countersForFirstBranch = counters[1];
-
-    JSUnit.assertEquals(false, countersForFirstBranch.hit);
-}
-
-function testFunctionForKeyFromFunctionWithNameMatchesSchema() {
-    let expectedFunctionKey = 'f:1:2';
-    let functionKeyForFunctionName =
-        Coverage._getFunctionKeyFromReflectedFunction({
-            id: {
-                name: 'f'
-            },
-            loc: {
-              start: {
-                  line: 1
-              }
-            },
-            params: ['a', 'b']
+describe('Coverage.expressionLinesForAST', function () {
+    let testTable = {
+        'works with no trailing newline': [
+            "let x;\n" +
+            "let y;",
+            [1, 2],
+        ],
+
+        'finds lines on both sides of an assignment expression': [
+            "var x;\n" +
+            "x = (function() {\n" +
+            "    return 10;\n" +
+            "})();\n",
+            [1, 2, 3],
+        ],
+
+        'finds lines inside functions': [
+            "function f(a, b) {\n" +
+            "    let x = a;\n" +
+            "    let y = b;\n" +
+            "    return x + y;\n" +
+            "}\n" +
+            "\n" +
+            "var z = f(1, 2);\n",
+            [2, 3, 4, 7],
+        ],
+
+        'finds lines inside anonymous functions': [
+            "var z = (function f(a, b) {\n" +
+            "     let x = a;\n" +
+            "     let y = b;\n" +
+            "     return x + y;\n" +
+            " })();\n",
+            [1, 2, 3, 4],
+        ],
+
+        'finds lines inside body of function property': [
+            "var o = {\n" +
+            "    foo: function() {\n" +
+            "        let x = a;\n" +
+            "    }\n" +
+            "};\n",
+            [1, 2, 3],
+        ],
+
+        'finds lines inside arguments of function property': [
+            "function f(a) {\n" +
+            "}\n" +
+            "f({\n" +
+            "    foo: function() {\n" +
+            "        let x = a;\n" +
+            "    }\n" +
+            "});\n",
+            [1, 3, 4, 5],
+        ],
+
+        'finds lines inside multiline function arguments': [
+            "function f(a, b, c) {\n" +
+            "}\n" +
+            "f(1,\n" +
+            "  2,\n" +
+            "  3);\n",
+            [1, 3, 4, 5],
+        ],
+
+        'finds lines inside function argument that is an object': [
+            "function f(o) {\n" +
+            "}\n" +
+            "let obj = {\n" +
+            "    Name: new f({ a: 1,\n" +
+            "                  b: 2,\n" +
+            "                  c: 3\n" +
+            "                })\n" +
+            "}\n",
+            [1, 3, 4, 5, 6],
+        ],
+
+        'finds lines inside a while loop': [
+            "var a = 0;\n" +
+            "while (a < 1) {\n" +
+            "    let x = 0;\n" +
+            "    let y = 1;\n" +
+            "    a++;" +
+            "\n" +
+            "}\n",
+            [1, 2, 3, 4, 5],
+        ],
+
+        'finds lines inside try, catch, and finally': [
+            "var a = 0;\n" +
+            "try {\n" +
+            "    a++;\n" +
+            "} catch (e) {\n" +
+            "    a++;\n" +
+            "} finally {\n" +
+            "    a++;\n" +
+            "}\n",
+            [1, 2, 3, 4, 5, 7],
+        ],
+
+        'finds lines inside case statements': [
+            "var a = 0;\n" +
+            "switch (a) {\n" +
+            "case 1:\n" +
+            "    a++;\n" +
+            "    break;\n" +
+            "case 2:\n" +
+            "    a++;\n" +
+            "    break;\n" +
+            "}\n",
+            [1, 2, 4, 5, 7, 8],
+        ],
+
+        'finds lines inside case statements with character cases': [
+            "var a = 'a';\n" +
+            "switch (a) {\n" +
+            "case 'a':\n" +
+            "    a++;\n" +
+            "    break;\n" +
+            "case 'b':\n" +
+            "    a++;\n" +
+            "    break;\n" +
+            "}\n",
+            [1, 2, 4, 5, 7, 8],
+        ],
+
+        'finds lines inside a for loop': [
+            "for (let i = 0; i < 1; i++) {\n" +
+            "    let x = 0;\n" +
+            "    let y = 1;\n" +
+            "\n" +
+            "}\n",
+            [1, 2, 3],
+        ],
+
+        'finds lines inside if-statement branches': [
+            "if (1 > 0) {\n" +
+            "    let i = 0;\n" +
+            "} else {\n" +
+            "    let j = 1;\n" +
+            "}\n",
+            [1, 2, 4],
+        ],
+
+        'finds all lines of multiline if-conditions': [
+            "if (1 > 0 &&\n" +
+            "    2 > 0 &&\n" +
+            "    3 > 0) {\n" +
+            "    let a = 3;\n" +
+            "}\n",
+            [1, 2, 3, 4],
+        ],
+
+        'finds lines for object property literals': [
+            "var a = {\n" +
+            "    Name: 'foo',\n" +
+            "    Ex: 'bar'\n" +
+            "};\n",
+            [1, 2, 3],
+        ],
+
+        'finds lines for function-valued object properties': [
+            "var a = {\n" +
+            "    Name: function() {},\n" +
+            "};\n",
+            [1, 2],
+        ],
+
+        'finds lines inside object-valued object properties': [
+            "var a = {\n" +
+            "    Name: {},\n" +
+            "};\n",
+            [1, 2],
+        ],
+
+        'finds lines inside array-valued object properties': [
+            "var a = {\n" +
+            "    Name: [],\n" +
+            "};\n",
+            [1, 2],
+        ],
+
+        'finds lines inside object-valued argument to return statement': [
+            "function f() {\n" +
+            "    return {};\n" +
+            "}\n",
+            [2],
+        ],
+
+        'finds lines inside object-valued argument to throw statement': [
+            "function f() {\n" +
+            "    throw {\n" +
+            "        a: 1,\n" +
+            "        b: 2\n" +
+            "    }\n" +
+            "}\n",
+            [2, 3, 4],
+        ],
+    };
+
+    Object.keys(testTable).forEach(testcase => {
+        it(testcase, function () {
+            const ast = Reflect.parse(testTable[testcase][0]);
+            let foundLines = Coverage.expressionLinesForAST(ast);
+            expect(foundLines).toEqual(testTable[testcase][1]);
         });
+    });
+});
+
+describe('Coverage.functionsForAST', function () {
+    let testTable = {
+        'works with no trailing newline': [
+            "function f1() {}\n" +
+            "function f2() {}",
+            [
+                { key: "f1:1:0", line: 1, n_params: 0 },
+                { key: "f2:2:0", line: 2, n_params: 0 },
+            ],
+        ],
+
+        'finds functions': [
+            "function f1() {}\n" +
+            "function f2() {}\n" +
+            "function f3() {}\n",
+            [
+                { key: "f1:1:0", line: 1, n_params: 0 },
+                { key: "f2:2:0", line: 2, n_params: 0 },
+                { key: "f3:3:0", line: 3, n_params: 0 }
+            ],
+        ],
+
+        'finds nested functions': [
+            "function f1() {\n" +
+            "    let f2 = function() {\n" +
+            "        let f3 = function() {\n" +
+            "        }\n" +
+            "    }\n" +
+            "}\n",
+            [
+                { key: "f1:1:0", line: 1, n_params: 0 },
+                { key: "(anonymous):2:0", line: 2, n_params: 0 },
+                { key: "(anonymous):3:0", line: 3, n_params: 0 }
+            ],
+        ],
+
+        /* Note the lack of newlines. This is all on one line */
+        'finds functions on the same line but with different arguments': [
+            "function f1() {" +
+            "    return (function(a) {" +
+            "        return function(a, b) {}" +
+            "    });" +
+            "}",
+            [
+                { key: "f1:1:0", line: 1, n_params: 0 },
+                { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                { key: "(anonymous):1:2", line: 1, n_params: 2 }
+            ],
+        ],
+
+        'finds functions inside an array expression': [
+            "let a = [function() {}];\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 },
+            ],
+        ],
+
+        'finds functions inside an arrow expression': [
+            "(a) => (function() {})();\n",
+            [
+                { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside a sequence': [
+            "(function(a) {})()," +
+            "(function(a, b) {})();\n",
+            [
+                { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                { key: "(anonymous):1:2", line: 1, n_params: 2 },
+            ],
+        ],
+
+        'finds functions inside a unary expression': [
+            "let a = (function() {}())++;\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 },
+            ],
+        ],
+
+        'finds functions inside a binary expression': [
+            "let a = function(a) {}() +" +
+            " function(a, b) {}();\n",
+            [
+                { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                { key: "(anonymous):1:2", line: 1, n_params: 2 }
+            ],
+        ],
+
+        'finds functions inside an assignment expression': [
+            "let a = function() {}();\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside a reflexive assignment expression': [
+            "let a;\n" +
+            "a += function() {}();\n",
+            [
+                { key: "(anonymous):2:0", line: 2, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside if-statement conditions': [
+            "if (function(a) {}(a) >" +
+            "    function(a, b) {}(a, b)) {}\n",
+            [
+                { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                { key: "(anonymous):1:2", line: 1, n_params: 2 }
+            ],
+        ],
+
+        'finds functions inside while-statement conditions': [
+            "while (function(a) {}(a) >" +
+            "       function(a, b) {}(a, b)) {};\n",
+            [
+                { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                { key: "(anonymous):1:2", line: 1, n_params: 2 }
+            ],
+        ],
+
+        'finds functions inside for-statement initializer': [
+            "for (function() {}; ;) {}\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        /* SpiderMonkey parses for (let i = <init>; <cond>; <update>) as though
+         * they were let i = <init> { for (; <cond> <update>) } so test the
+         * LetStatement initializer case too */
+        'finds functions inside let-statement in for-statement initializer': [
+            "for (let i = function() {}; ;) {}\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside var-statement inside for-statement initializer': [
+            "for (var i = function() {}; ;) {}\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside for-statement condition': [
+            "for (; function() {}();) {}\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside for-statement increment': [
+            "for (; ;function() {}()) {}\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside for-in-statement': [
+            "for (let x in function() {}()) {}\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside for-each statement': [
+            "for each (x in function() {}()) {}\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions inside for-of statement': [
+            "for (x of (function() {}())) {}\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds function literals used as an object': [
+            "f = function() {}.bind();\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds function literals used as an object in a dynamic property expression': [
+            "f = function() {}['bind']();\n",
+            [
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions on either side of a logical expression': [
+            "let f = function(a) {} ||" +
+            " function(a, b) {};\n",
+            [
+                { key: "(anonymous):1:1", line: 1, n_params: 1 },
+                { key: "(anonymous):1:2", line: 1, n_params: 2 }
+            ],
+        ],
+
+        'finds functions on either side of a conditional expression': [
+            "let a\n" +
+            "let f = a ? function(a) {}() :" +
+            " function(a, b) {}();\n",
+            [
+                { key: "(anonymous):2:1", line: 2, n_params: 1 },
+                { key: "(anonymous):2:2", line: 2, n_params: 2 }
+            ],
+        ],
+
+        'finds functions as the argument of a yield statement': [
+            "function a() { yield function (){} };\n",
+            [
+                { key: "a:1:0", line: 1, n_params: 0 },
+                { key: "(anonymous):1:0", line: 1, n_params: 0 }
+            ],
+        ],
+
+        'finds functions in an array comprehension body': [
+            "let a = new Array(1);\n" +
+            "let b = [function() {} for (i of a)];\n",
+            [
+                { key: "(anonymous):2:0", line: 2, n_params: 0 }
+            ],
+        ],
+
+        'finds functions in an array comprehension block': [
+            "let a = new Array(1);\n" +
+            "let b = [i for (i of function() {})];\n",
+            [
+                { key: "(anonymous):2:0", line: 2, n_params: 0 }
+            ],
+        ],
+
+        'finds functions in an array comprehension filter': [
+            "let a = new Array(1);\n" +
+            "let b = [i for (i of a)" +
+            "if (function() {}())];\n",
+            [
+                { key: "(anonymous):2:0", line: 2, n_params: 0 }
+            ],
+        ],
+    };
 
-    JSUnit.assertEquals(expectedFunctionKey, functionKeyForFunctionName);
-}
-
-function testFunctionKeyFromFunctionWithoutNameIsAnonymous() {
-    let expectedFunctionKey = '(anonymous):2:3';
-    let functionKeyForAnonymousFunction =
-        Coverage._getFunctionKeyFromReflectedFunction({
-            id: null,
-            loc: {
-              start: {
-                  line: 2
-              }
-            },
-            params: ['a', 'b', 'c']
+    Object.keys(testTable).forEach(testcase => {
+        it(testcase, function () {
+            const ast = Reflect.parse(testTable[testcase][0]);
+            let foundFuncs = Coverage.functionsForAST(ast);
+            expect(foundFuncs).toEqual(testTable[testcase][1]);
         });
+    });
+});
+
+describe('Coverage.branchesForAST', function () {
+    let testTable = {
+        'works with no trailing newline': [
+            "if (1) { let a = 1; }",
+            [
+                { point: 1, exits: [1] },
+            ],
+        ],
+
+        'finds both branch exits for a simple branch': [
+            "if (1) {\n" +
+            "    let a = 1;\n" +
+            "} else {\n" +
+            "    let b = 2;\n" +
+            "}\n",
+            [
+                { point: 1, exits: [2, 4] }
+            ],
+        ],
+
+        'finds a single exit for a branch with one consequent': [
+            "if (1) {\n" +
+            "    let a = 1.0;\n" +
+            "}\n",
+            [
+                { point: 1, exits: [2] }
+            ],
+        ],
+
+        'finds multiple exits for nested if-else branches': [
+            "if (1) {\n" +
+            "    let a = 1.0;\n" +
+            "} else if (2) {\n" +
+            "    let b = 2.0;\n" +
+            "} else if (3) {\n" +
+            "    let c = 3.0;\n" +
+            "} else {\n" +
+            "    let d = 4.0;\n" +
+            "}\n",
+            [
+                // the 'else if' line is actually an exit for the first branch
+                { point: 1, exits: [2, 3] },
+                { point: 3, exits: [4, 5] },
+                // 'else' by itself is not executable, it is the block it
+                // contains which is
+                { point: 5, exits: [6, 8] }
+            ],
+        ],
+
+        'finds a simple two-exit branch without blocks': [
+            "let a, b;\n" +
+            "if (1)\n" +
+            "    a = 1.0\n" +
+            "else\n" +
+            "    b = 2.0\n" +
+            "\n",
+            [
+                { point: 2, exits: [3, 5] }
+            ],
+        ],
+
+        'does not find a branch if the consequent was empty': [
+            "let a, b;\n" +
+            "if (1) {}\n",
+            [],
+        ],
+
+        'finds a single exit if only the alternate exit was defined': [
+            "let a, b;\n" +
+            "if (1) {}\n" +
+            "else\n" +
+            "    a++;\n",
+            [
+                { point: 2, exits: [4] }
+            ],
+        ],
+
+        'finds an implicit branch for while statement': [
+            "while (1) {\n" +
+            "    let a = 1;\n" +
+            "}\n" +
+            "let b = 2;",
+            [
+                { point: 1, exits: [2] }
+            ],
+        ],
+
+        'finds an implicit branch for a do-while statement': [
+            "do {\n" +
+            "    let a = 1;\n" +
+            "} while (1)\n" +
+            "let b = 2;",
+            [
+                // For do-while loops the branch-point is at the 'do' condition
+                // and not the 'while'
+                { point: 1, exits: [2] }
+            ],
+        ],
+
+        'finds all exits for case statements': [
+            "let a = 1;\n" +
+            "switch (1) {\n" +
+            "case '1':\n" +
+            "    a++;\n" +
+            "    break;\n" +
+            "case '2':\n" +
+            "    a++\n" +
+            "    break;\n" +
+            "default:\n" +
+            "    a++\n" +
+            "    break;\n" +
+            "}\n",
+            [
+                /* There are three potential exits here */
+                { point: 2, exits: [4, 7, 10] }
+            ],
+        ],
+
+        'finds all exits for case statements with fallthrough': [
+            "let a = 1;\n" +
+            "switch (1) {\n" +
+            "case '1':\n" +
+            "case 'a':\n" +
+            "case 'b':\n" +
+            "    a++;\n" +
+            "    break;\n" +
+            "case '2':\n" +
+            "    a++\n" +
+            "    break;\n" +
+            "default:\n" +
+            "    a++\n" +
+            "    break;\n" +
+            "}\n",
+            [
+                /* There are three potential exits here */
+                { point: 2, exits: [6, 9, 12] }
+            ],
+        ],
+
+        'finds no exits for case statements with only no-ops': [
+            "let a = 1;\n" +
+            "switch (1) {\n" +
+            "case '1':\n" +
+            "case '2':\n" +
+            "default:\n" +
+            "}\n",
+            [],
+        ],
+    };
 
-    JSUnit.assertEquals(expectedFunctionKey, functionKeyForAnonymousFunction);
-}
+    Object.keys(testTable).forEach(testcase => {
+        it(testcase, function () {
+            const ast = Reflect.parse(testTable[testcase][0]);
+            let foundBranchExits = Coverage.branchesForAST(ast);
+            expect(foundBranchExits).toEqual(testTable[testcase][1]);
+        });
+    });
+});
 
-function testFunctionCounterMapReturnedForFunctionKeys() {
-    let ast = {
-        body: [{
-            type: 'FunctionDeclaration',
-            id: {
-                name: 'name'
-            },
-            loc: {
-              start: {
-                  line: 1
-              }
-            },
-            params: [],
-            body: {
-                type: 'BlockStatement',
-                body: []
-            }
-        }]
-    };
+describe('Coverage', function () {
+    it('gets the number of lines in the script', function () {
+        let script = "\n\n";
+        let number = Coverage._getNumberOfLinesForScript(script);
+        expect(number).toEqual(3);
+    });
 
-    let detectedFunctions = Coverage.functionsForAST(ast);
-    let functionCounters = Coverage._functionsToFunctionCounters('script',
-                                                                 detectedFunctions);
+    it('turns zero expression lines into counters', function () {
+        let expressionLines = [];
+        let nLines = 1;
+        let counters = Coverage._expressionLinesToCounters(expressionLines, nLines);
 
-    JSUnit.assertEquals(0, functionCounters.name['1']['0'].hitCount);
-}
+        expect(counters).toEqual([undefined, undefined]);
+    });
 
-function _fetchLogMessagesFrom(func) {
-    let oldLog = window.log;
-    let collectedMessages = [];
-    window.log = function(message) {
-        collectedMessages.push(message);
-    };
+    it('turns a single expression line into counters', function () {
+        let expressionLines = [1, 2];
+        let nLines = 4;
+        let counters = Coverage._expressionLinesToCounters(expressionLines, nLines);
 
-    try {
-        func.apply(this, arguments);
-    } finally {
-        window.log = oldLog;
-    }
-
-    return collectedMessages;
-}
-
-function testErrorReportedWhenTwoIndistinguishableFunctionsPresent() {
-    let ast = {
-        body: [{
-            type: 'FunctionDeclaration',
-            id: {
-                name: '(anonymous)'
-            },
-            loc: {
-              start: {
-                  line: 1
-              }
-            },
-            params: [],
-            body: {
-                type: 'BlockStatement',
-                body: []
-            }
-        }, {
-            type: 'FunctionDeclaration',
-            id: {
-                name: '(anonymous)'
-            },
-            loc: {
-              start: {
-                  line: 1
-              }
-            },
-            params: [],
-            body: {
-                type: 'BlockStatement',
-                body: []
-            }
-        }]
-    };
+        expect(counters).toEqual([undefined, 0, 0, undefined, undefined]);
+    });
 
-    let detectedFunctions = Coverage.functionsForAST(ast);
-    let messages = _fetchLogMessagesFrom(function() {
-        Coverage._functionsToFunctionCounters('script', detectedFunctions);
+    it('returns empty array for no branches', function () {
+        let counters = Coverage._branchesToBranchCounters([], 1);
+        expect(counters).toEqual([undefined, undefined]);
     });
 
-    JSUnit.assertEquals('script:1 Function identified as (anonymous):1:0 ' +
-                        'already seen in this file. Function coverage will ' +
-                        'be incomplete.',
-                        messages[0]);
-}
-
-function testKnownFunctionsArrayPopulatedForFunctions() {
-    let functions = [
-        { line: 1 },
-        { line: 2 }
-    ];
-
-    let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 4);
-
-    assertArrayEquals(knownFunctionsArray,
-                      [undefined, true, true, undefined, undefined],
-                      JSUnit.assertEquals);
-}
-
-function testIncrementFunctionCountersForFunctionOnSameExecutionStartLine() {
-    let functionCounters = Coverage._functionsToFunctionCounters('script', [
-        { key: 'f:1:0',
-          line: 1,
-          n_params: 0 }
-    ]);
-    Coverage._incrementFunctionCounters(functionCounters, null, 'f', 1, 0);
-
-    JSUnit.assertEquals(functionCounters.f['1']['0'].hitCount, 1);
-}
-
-function testIncrementFunctionCountersCanDisambiguateTwoFunctionsWithSameName() {
-    let functionCounters = Coverage._functionsToFunctionCounters('script', [
-        { key: '(anonymous):1:0',
-          line: 1,
-          n_params: 0 },
-        { key: '(anonymous):2:0',
-          line: 2,
-          n_params: 0 }
-    ]);
-    Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 0);
-    Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 2, 0);
-
-    JSUnit.assertEquals(functionCounters['(anonymous)']['1']['0'].hitCount, 1);
-    JSUnit.assertEquals(functionCounters['(anonymous)']['2']['0'].hitCount, 1);
-}
-
-function testIncrementFunctionCountersCanDisambiguateTwoFunctionsOnSameLineWithDifferentParams() {
-    let functionCounters = Coverage._functionsToFunctionCounters('script', [
-        { key: '(anonymous):1:0',
-          line: 1,
-          n_params: 0 },
-        { key: '(anonymous):1:1',
-          line: 1,
-          n_params: 1 }
-    ]);
-    Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 0);
-    Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 1);
-
-    JSUnit.assertEquals(functionCounters['(anonymous)']['1']['0'].hitCount, 1);
-    JSUnit.assertEquals(functionCounters['(anonymous)']['1']['1'].hitCount, 1);
-}
-
-function testIncrementFunctionCountersCanDisambiguateTwoFunctionsOnSameLineByGuessingClosestParams() {
-    let functionCounters = Coverage._functionsToFunctionCounters('script', [
-        { key: '(anonymous):1:0',
-          line: 1,
-          n_params: 0 },
-        { key: '(anonymous):1:3',
-          line: 1,
-          n_params: 3 }
-    ]);
-
-    /* Eg, we called the function with 3 params with just two arguments. We
-     * should be able to work out that we probably intended to call the
-     * latter function as opposed to the former. */
-    Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 2);
-
-    JSUnit.assertEquals(functionCounters['(anonymous)']['1']['0'].hitCount, 0);
-    JSUnit.assertEquals(functionCounters['(anonymous)']['1']['3'].hitCount, 1);
-}
-
-function testIncrementFunctionCountersForFunctionOnEarlierStartLine() {
-    let ast = {
-        body: [{
-            type: 'FunctionDeclaration',
-            id: {
-                name: 'name'
+    describe('branch counters', function () {
+        const MockFoundBranches = [
+            {
+                point: 5,
+                exits: [6, 8]
             },
-            loc: {
-              start: {
-                  line: 1
-              }
-            },
-            params: [],
-            body: {
-                type: 'BlockStatement',
-                body: []
+            {
+                point: 1,
+                exits: [2, 4]
             }
-        }]
-    };
+        ];
+
+        const MockNLines = 9;
 
-    let detectedFunctions = Coverage.functionsForAST(ast);
-    let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3);
-    let functionCounters = Coverage._functionsToFunctionCounters('script',
-                                                                 detectedFunctions);
+        let counters;
+        beforeEach(function () {
+            counters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
+        });
 
-    /* We're entering at line two, but the function definition was actually
-     * at line one */
-    Coverage._incrementFunctionCounters(functionCounters, knownFunctionsArray, 'name', 2, 0);
+        it('gets same number of counters as number of lines plus one', function () {
+            expect(counters.length).toEqual(MockNLines + 1);
+        });
 
-    JSUnit.assertEquals(functionCounters.name['1']['0'].hitCount, 1);
-}
+        it('branches on lines for array indices', function () {
+            expect(counters[1]).toBeDefined();
+            expect(counters[5]).toBeDefined();
+        });
 
-function testIncrementFunctionCountersThrowsErrorOnUnexpectedFunction() {
-    let ast = {
-        body: [{
-            type: 'FunctionDeclaration',
-            id: {
-                name: 'name'
-            },
-            loc: {
-              start: {
-                  line: 1
-              }
-            },
-            params: [],
-            body: {
-                type: 'BlockStatement',
-                body: []
-            }
-        }]
-    };
-    let detectedFunctions = Coverage.functionsForAST(ast);
-    let functionKey = Coverage._getFunctionKeyFromReflectedFunction(ast.body[0]);
-    let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3);
-    let functionCounters = Coverage._functionsToFunctionCounters('script',
-                                                                 detectedFunctions);
-
-    /* We're entering at line two, but the function definition was actually
-     * at line one */
-    JSUnit.assertRaises(function() {
-        Coverage._incrementFunctionCounters(functionCounters,
-                                            knownFunctionsArray,
-                                            'doesnotexist',
-                                            2,
-                                            0);
+        it('sets exits for branch', function () {
+            expect(counters[1].exits).toEqual([
+                { line: 2, hitCount: 0 },
+                { line: 4, hitCount: 0 },
+            ]);
+        });
+
+        it('sets last exit to highest exit start line', function () {
+            expect(counters[1].lastExit).toEqual(4);
+        });
+
+        it('always has hit initially false', function () {
+            expect(counters[1].hit).toBeFalsy();
+        });
+
+        describe('branch tracker', function () {
+            let branchTracker;
+            beforeEach(function () {
+                branchTracker = new Coverage._BranchTracker(counters);
+            });
+
+            it('sets branch to hit on point execution', function () {
+                branchTracker.incrementBranchCounters(1);
+                expect(counters[1].hit).toBeTruthy();
+            });
+
+            it('sets exit to hit on execution', function () {
+                branchTracker.incrementBranchCounters(1);
+                branchTracker.incrementBranchCounters(2);
+                expect(counters[1].exits[0].hitCount).toEqual(1);
+            });
+
+            it('finds next branch', function () {
+                branchTracker.incrementBranchCounters(1);
+                branchTracker.incrementBranchCounters(2);
+                branchTracker.incrementBranchCounters(5);
+                expect(counters[5].hit).toBeTruthy();
+            });
+        });
     });
-}
 
-function testIncrementExpressionCountersThrowsIfLineOutOfRange() {
-    let expressionCounters = [
-        undefined,
-        0
-    ];
+    it('function key from function with name matches schema', function () {
+        let functionKeyForFunctionName =
+            Coverage._getFunctionKeyFromReflectedFunction({
+                id: {
+                    name: 'f'
+                },
+                loc: {
+                  start: {
+                      line: 1
+                  }
+                },
+                params: ['a', 'b']
+            });
+        expect(functionKeyForFunctionName).toEqual('f:1:2');
+    });
 
-    JSUnit.assertRaises(function() {
-        Coverage._incrementExpressionCounters(expressionCounters, 'script', 2);
+    it('function key from function without name is anonymous', function () {
+        let functionKeyForAnonymousFunction =
+            Coverage._getFunctionKeyFromReflectedFunction({
+                id: null,
+                loc: {
+                  start: {
+                      line: 2
+                  }
+                },
+                params: ['a', 'b', 'c']
+            });
+        expect(functionKeyForAnonymousFunction).toEqual('(anonymous):2:3');
     });
-}
-
-function testIncrementExpressionCountersIncrementsIfInRange() {
-    let expressionCounters = [
-        undefined,
-        0
-    ];
-
-    Coverage._incrementExpressionCounters(expressionCounters, 'script', 1);
-    JSUnit.assertEquals(1, expressionCounters[1]);
-}
-
-function testWarnsIfWeHitANonExecutableLine() {
-    let expressionCounters = [
-        undefined,
-        0,
-        undefined
-    ];
-
-    let messages = _fetchLogMessagesFrom(function() {
-        Coverage._incrementExpressionCounters(expressionCounters, 'script', 2);
+
+    it('returns a function counter map for function keys', function () {
+        let ast = {
+            body: [{
+                type: 'FunctionDeclaration',
+                id: {
+                    name: 'name'
+                },
+                loc: {
+                  start: {
+                      line: 1
+                  }
+                },
+                params: [],
+                body: {
+                    type: 'BlockStatement',
+                    body: []
+                }
+            }]
+        };
+
+        let detectedFunctions = Coverage.functionsForAST(ast);
+        let functionCounters =
+            Coverage._functionsToFunctionCounters('script', detectedFunctions);
+        expect(functionCounters.name['1']['0'].hitCount).toEqual(0);
     });
 
-    JSUnit.assertEquals(messages[0],
-                        "script:2 Executed line previously marked " +
-                        "non-executable by Reflect");
-    JSUnit.assertEquals(expressionCounters[2], 1);
-}
+    it('reports an error when two indistinguishable functions are present', function () {
+        spyOn(window, 'log');
+        let ast = {
+            body: [{
+                type: 'FunctionDeclaration',
+                id: {
+                    name: '(anonymous)'
+                },
+                loc: {
+                  start: {
+                      line: 1
+                  }
+                },
+                params: [],
+                body: {
+                    type: 'BlockStatement',
+                    body: []
+                }
+            }, {
+                type: 'FunctionDeclaration',
+                id: {
+                    name: '(anonymous)'
+                },
+                loc: {
+                  start: {
+                      line: 1
+                  }
+                },
+                params: [],
+                body: {
+                    type: 'BlockStatement',
+                    body: []
+                }
+            }]
+        };
+
+        let detectedFunctions = Coverage.functionsForAST(ast);
+        Coverage._functionsToFunctionCounters('script', detectedFunctions);
 
-function testBranchTrackerSetsBranchToHitOnPointExecution() {
-    let branchCounters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
-    let branchTracker = new Coverage._BranchTracker(branchCounters);
+        expect(window.log).toHaveBeenCalledWith('script:1 Function ' +
+            'identified as (anonymous):1:0 already seen in this file. ' +
+            'Function coverage will be incomplete.');
+    });
 
-    branchTracker.incrementBranchCounters(1);
+    it('populates a known functions array', function () {
+        let functions = [
+            { line: 1 },
+            { line: 2 }
+        ];
 
-    JSUnit.assertEquals(true, branchCounters[1].hit);
-}
+        let knownFunctionsArray = Coverage._populateKnownFunctions(functions, 4);
 
-function testBranchTrackerSetsExitToHitOnExecution() {
-    let branchCounters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
-    let branchTracker = new Coverage._BranchTracker(branchCounters);
+        expect(knownFunctionsArray)
+            .toEqual([undefined, true, true, undefined, undefined]);
+    });
 
-    branchTracker.incrementBranchCounters(1);
-    branchTracker.incrementBranchCounters(2);
+    it('converts function counters to an array', function () {
+        let functionsMap = {
+            '(anonymous)': {
+                '2': {
+                    '0': {
+                        hitCount: 1
+                    },
+                },
+            },
+            'name': {
+                '1': {
+                    '0': {
+                        hitCount: 0
+                    },
+                },
+            }
+        };
 
-    JSUnit.assertEquals(1, branchCounters[1].exits[0].hitCount);
-}
+        let expectedFunctionCountersArray = [
+            jasmine.objectContaining({ name: '(anonymous):2:0', hitCount: 1 }),
+            jasmine.objectContaining({ name: 'name:1:0', hitCount: 0 })
+        ];
 
-function testBranchTrackerFindsNextBranch() {
-    let branchCounters = Coverage._branchesToBranchCounters(MockFoundBranches, MockNLines);
-    let branchTracker = new Coverage._BranchTracker(branchCounters);
+        let convertedFunctionCounters = Coverage._convertFunctionCountersToArray(functionsMap);
 
-    branchTracker.incrementBranchCounters(1);
-    branchTracker.incrementBranchCounters(2);
-    branchTracker.incrementBranchCounters(5);
+        expect(convertedFunctionCounters).toEqual(expectedFunctionCountersArray);
+    });
+});
+
+describe('Coverage.incrementFunctionCounters', function () {
+    it('increments for function on same execution start line', function () {
+        let functionCounters = Coverage._functionsToFunctionCounters('script', [
+            { key: 'f:1:0',
+              line: 1,
+              n_params: 0 }
+        ]);
+        Coverage._incrementFunctionCounters(functionCounters, null, 'f', 1, 0);
+
+        expect(functionCounters.f['1']['0'].hitCount).toEqual(1);
+    });
 
-    JSUnit.assertEquals(true, branchCounters[5].hit);
-}
+    it('can disambiguate two functions with the same name', function () {
+        let functionCounters = Coverage._functionsToFunctionCounters('script', [
+            { key: '(anonymous):1:0',
+              line: 1,
+              n_params: 0 },
+            { key: '(anonymous):2:0',
+              line: 2,
+              n_params: 0 }
+        ]);
+        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 0);
+        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 2, 0);
+
+        expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(1);
+        expect(functionCounters['(anonymous)']['2']['0'].hitCount).toEqual(1);
+    });
 
-function testConvertFunctionCountersToArray() {
-    let functionsMap = {
-        '(anonymous)': {
-            '2': {
-                '0': {
-                    hitCount: 1
+    it('can disambiguate two functions on same line with different params', function () {
+        let functionCounters = Coverage._functionsToFunctionCounters('script', [
+            { key: '(anonymous):1:0',
+              line: 1,
+              n_params: 0 },
+            { key: '(anonymous):1:1',
+              line: 1,
+              n_params: 1 }
+        ]);
+        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 0);
+        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 1);
+
+        expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(1);
+        expect(functionCounters['(anonymous)']['1']['1'].hitCount).toEqual(1);
+    });
+
+    it('can disambiguate two functions on same line by guessing closest params', function () {
+        let functionCounters = Coverage._functionsToFunctionCounters('script', [
+            { key: '(anonymous):1:0',
+              line: 1,
+              n_params: 0 },
+            { key: '(anonymous):1:3',
+              line: 1,
+              n_params: 3 }
+        ]);
+
+        /* Eg, we called the function with 3 params with just two arguments. We
+         * should be able to work out that we probably intended to call the
+         * latter function as opposed to the former. */
+        Coverage._incrementFunctionCounters(functionCounters, null, '(anonymous)', 1, 2);
+
+        expect(functionCounters['(anonymous)']['1']['0'].hitCount).toEqual(0);
+        expect(functionCounters['(anonymous)']['1']['3'].hitCount).toEqual(1);
+    });
+
+    it('increments for function on earlier start line', function () {
+        let ast = {
+            body: [{
+                type: 'FunctionDeclaration',
+                id: {
+                    name: 'name'
                 },
-            },
-        },
-        'name': {
-            '1': {
-                '0': {
-                    hitCount: 0
+                loc: {
+                  start: {
+                      line: 1
+                  }
                 },
-            },
-        }
-    };
+                params: [],
+                body: {
+                    type: 'BlockStatement',
+                    body: []
+                }
+            }]
+        };
+
+        let detectedFunctions = Coverage.functionsForAST(ast);
+        let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3);
+        let functionCounters = Coverage._functionsToFunctionCounters('script',
+                                                                     detectedFunctions);
+
+        /* We're entering at line two, but the function definition was actually
+         * at line one */
+        Coverage._incrementFunctionCounters(functionCounters, knownFunctionsArray, 'name', 2, 0);
+
+        expect(functionCounters.name['1']['0'].hitCount).toEqual(1);
+    });
 
-    let expectedFunctionCountersArray = [
-        { name: '(anonymous):2:0', hitCount: 1 },
-        { name: 'name:1:0', hitCount: 0 }
-    ];
-
-    let convertedFunctionCounters = Coverage._convertFunctionCountersToArray(functionsMap);
-
-    assertArrayEquals(expectedFunctionCountersArray,
-                      convertedFunctionCounters,
-                      function(expected, actual) {
-                          JSUnit.assertEquals(expected.name, actual.name);
-                          JSUnit.assertEquals(expected.hitCount, actual.hitCount);
-                      });
-}
-
-function testConvertFunctionCountersToArrayIsSorted() {
-    let functionsMap = {
-        '(anonymous)': {
-            '2': {
-                '0': {
-                    hitCount: 1
+    it('throws an error on unexpected function', function () {
+        let ast = {
+            body: [{
+                type: 'FunctionDeclaration',
+                id: {
+                    name: 'name'
                 },
-            },
-        },
-        'name': {
-            '1': {
-                '0': {
-                    hitCount: 0
+                loc: {
+                  start: {
+                      line: 1
+                  }
                 },
-            },
-        }
-    };
+                params: [],
+                body: {
+                    type: 'BlockStatement',
+                    body: []
+                }
+            }]
+        };
+        let detectedFunctions = Coverage.functionsForAST(ast);
+        let knownFunctionsArray = Coverage._populateKnownFunctions(detectedFunctions, 3);
+        let functionCounters = Coverage._functionsToFunctionCounters('script',
+                                                                     detectedFunctions);
+
+        /* We're entering at line two, but the function definition was actually
+         * at line one */
+        expect(() => {
+            Coverage._incrementFunctionCounters(functionCounters,
+                                                knownFunctionsArray,
+                                                'doesnotexist',
+                                                2,
+                                                0);
+        }).toThrow();
+    });
+
+    it('throws if line out of range', function () {
+        let expressionCounters = [
+            undefined,
+            0
+        ];
+
+        expect(() => {
+            Coverage._incrementExpressionCounters(expressionCounters, 'script', 2);
+        }).toThrow();
+    });
+
+    it('increments if in range', function () {
+        let expressionCounters = [
+            undefined,
+            0
+        ];
 
-    let expectedFunctionCountersArray = [
-        { name: '(anonymous):2:0', hitCount: 1 },
-        { name: 'name:1:0', hitCount: 0 }
-    ];
-
-    let convertedFunctionCounters = Coverage._convertFunctionCountersToArray(functionsMap);
-
-    assertArrayEquals(expectedFunctionCountersArray,
-                      convertedFunctionCounters,
-                      function(expected, actual) {
-                          JSUnit.assertEquals(expected.name, actual.name);
-                          JSUnit.assertEquals(expected.hitCount, actual.hitCount);
-                      });
-}
-
-const MockFiles = {
-    'filename': "function f() {\n" +
-                "    return 1;\n" +
-                "}\n" +
-                "if (f())\n" +
-                "    f = 0;\n" +
-                "\n",
-    'uncached': "function f() {\n" +
-                "    return 1;\n" +
-                "}\n"
-};
-
-const MockFilenames = (function() {
-    let keys = Object.keys(MockFiles);
-    keys.push('nonexistent');
-    return keys;
-})();
-
-Coverage.getFileContents = function(filename) {
-    if (MockFiles[filename])
-        return MockFiles[filename];
-    return undefined;
-};
-
-Coverage.getFileChecksum = function(filename) {
-    return "abcd";
-};
-
-Coverage.getFileModificationTime = function(filename) {
-    return [1, 2];
-};
-
-function testCoverageStatisticsContainerFetchesValidStatisticsForFile() {
-    let container = new Coverage.CoverageStatisticsContainer(MockFilenames);
-
-    let statistics = container.fetchStatistics('filename');
-    JSUnit.assertNotEquals(undefined, statistics);
-
-    let files = container.getCoveredFiles();
-    assertArrayEquals(files, ['filename'], JSUnit.assertEquals);
-}
-
-function testCoverageStatisticsContainerThrowsForNonExistingFile() {
-    let container = new Coverage.CoverageStatisticsContainer(MockFilenames);
-
-    JSUnit.assertRaises(function() {
-        container.fetchStatistics('nonexistent');
+        Coverage._incrementExpressionCounters(expressionCounters, 'script', 1);
+        expect(expressionCounters[1]).toEqual(1);
     });
-}
-
-const MockCache = '{ \
-    "filename": { \
-        "mtime": [1, 2], \
-        "checksum": null, \
-        "lines": [2, 4, 5], \
-        "branches": [ \
-            { \
-                "point": 4, \
-                "exits": [5] \
-            } \
-        ], \
-        "functions": [ \
-            { \
-                "key": "f:1:0", \
-                "line": 1 \
-            } \
-        ] \
-    } \
-}';
-
-/* A simple wrapper to monkey-patch object[functionProperty] with
- * a wrapper that checks to see if it was called. Returns true
- * if the function was called at all */
-function _checkIfCalledWhilst(object, functionProperty, clientCode) {
-    let original = object[functionProperty];
-    let called = false;
-
-    object[functionProperty] = function() {
-        called = true;
-        return original.apply(this, arguments);
+
+    it('warns if we hit a non-executable line', function () {
+        spyOn(window, 'log');
+        let expressionCounters = [
+            undefined,
+            0,
+            undefined
+        ];
+
+        Coverage._incrementExpressionCounters(expressionCounters, 'script', 2);
+
+        expect(window.log).toHaveBeenCalledWith("script:2 Executed line " +
+            "previously marked non-executable by Reflect");
+        expect(expressionCounters[2]).toEqual(1);
+    });
+});
+
+describe('Coverage statistics container', function () {
+    const MockFiles = {
+        'filename': "function f() {\n" +
+                    "    return 1;\n" +
+                    "}\n" +
+                    "if (f())\n" +
+                    "    f = 0;\n" +
+                    "\n",
+        'uncached': "function f() {\n" +
+                    "    return 1;\n" +
+                    "}\n"
     };
 
-    clientCode();
-
-    object[functionProperty] = original;
-    return called;
-}
-
-function testCoverageCountersFetchedFromCache() {
-    let called = _checkIfCalledWhilst(Coverage,
-                                      '_fetchCountersFromReflection',
-                                      function() {
-                                          let container = new 
Coverage.CoverageStatisticsContainer(MockFilenames,
-                                                                                                   
MockCache);
-                                          let statistics = container.fetchStatistics('filename');
-                                      });
-    JSUnit.assertFalse(called);
-}
-
-function testCoverageCountersFetchedFromReflectionIfMissed() {
-    let called = _checkIfCalledWhilst(Coverage,
-                                      '_fetchCountersFromReflection',
-                                      function() {
-                                          let container = new 
Coverage.CoverageStatisticsContainer(MockFilenames,
-                                                                                                   
MockCache);
-                                          let statistics = container.fetchStatistics('uncached');
-                                      });
-    JSUnit.assertTrue(called);
-}
-
-function testCoverageContainerCacheNotStaleIfAllHit() {
-    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
-                                                             MockCache);
-    let statistics = container.fetchStatistics('filename');
-    JSUnit.assertFalse(container.staleCache());
-}
-
-function testCoverageContainerCacheStaleIfMiss() {
-    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
-                                                             MockCache);
-    let statistics = container.fetchStatistics('uncached');
-    JSUnit.assertTrue(container.staleCache());
-}
-
-function testCoverageCountersFromCacheHaveSameExecutableLinesAsReflection() {
-    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
-                                                             MockCache);
-    let statistics = container.fetchStatistics('filename');
-
-    let containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
-    let statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
-
-    assertArrayEquals(statisticsWithNoCaching.expressionCounters,
-                      statistics.expressionCounters,
-                      JSUnit.assertEquals);
-}
-
-function testCoverageCountersFromCacheHaveSameBranchExitsAsReflection() {
-    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
-                                                             MockCache);
-    let statistics = container.fetchStatistics('filename');
-
-    let containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
-    let statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
-
-    /* Branch starts on line 4 */
-    JSUnit.assertEquals(statisticsWithNoCaching.branchCounters[4].exits[0].line,
-                        statistics.branchCounters[4].exits[0].line);
-}
-
-function testCoverageCountersFromCacheHaveSameBranchPointsAsReflection() {
-    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
-                                                             MockCache);
-    let statistics = container.fetchStatistics('filename');
-
-    let containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
-    let statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
-    JSUnit.assertEquals(statisticsWithNoCaching.branchCounters[4].point,
-                        statistics.branchCounters[4].point);
-}
-
-function testCoverageCountersFromCacheHaveSameFunctionKeysAsReflection() {
-    let container = new Coverage.CoverageStatisticsContainer(MockFilenames,
-                                                             MockCache);
-    let statistics = container.fetchStatistics('filename');
-
-    let containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
-    let statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
-
-    /* Functions start on line 1 */
-    assertArrayEquals(Object.keys(statisticsWithNoCaching.functionCounters),
-                      Object.keys(statistics.functionCounters),
-                      JSUnit.assertEquals);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    const MockFilenames = Object.keys(MockFiles).concat(['nonexistent']);
+
+    beforeEach(function () {
+        Coverage.getFileContents =
+            jasmine.createSpy('getFileContents').and.callFake(f => MockFiles[f]);
+        Coverage.getFileChecksum =
+            jasmine.createSpy('getFileChecksum').and.returnValue('abcd');
+        Coverage.getFileModificationTime =
+            jasmine.createSpy('getFileModificationTime').and.returnValue([1, 2]);
+    });
+
+    it('fetches valid statistics for file', function () {
+        let container = new Coverage.CoverageStatisticsContainer(MockFilenames);
+
+        let statistics = container.fetchStatistics('filename');
+        expect(statistics).toBeDefined();
+
+        let files = container.getCoveredFiles();
+        expect(files).toEqual(['filename']);
+    });
+
+    it('throws for nonexisting file', function () {
+        let container = new Coverage.CoverageStatisticsContainer(MockFilenames);
+        expect(() => container.fetchStatistics('nonexistent')).toThrow();
+    });
+
+    const MockCache = '{ \
+        "filename": { \
+            "mtime": [1, 2], \
+            "checksum": null, \
+            "lines": [2, 4, 5], \
+            "branches": [ \
+                { \
+                    "point": 4, \
+                    "exits": [5] \
+                } \
+            ], \
+            "functions": [ \
+                { \
+                    "key": "f:1:0", \
+                    "line": 1 \
+                } \
+            ] \
+        } \
+    }';
+
+    describe('with cache', function () {
+        let container;
+        beforeEach(function () {
+            spyOn(Coverage, '_fetchCountersFromReflection').and.callThrough();
+            container = new Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                                 MockCache);
+        });
+
+        it('fetches counters from cache', function () {
+            container.fetchStatistics('filename');
+            expect(Coverage._fetchCountersFromReflection).not.toHaveBeenCalled();
+        });
+
+        it('fetches counters from reflection if missed', function () {
+            container.fetchStatistics('uncached');
+            expect(Coverage._fetchCountersFromReflection).toHaveBeenCalled();
+        });
+
+        it('cache is not stale if all hit', function () {
+            container.fetchStatistics('filename');
+            expect(container.staleCache()).toBeFalsy();
+        });
+
+        it('cache is stale if missed', function () {
+            container.fetchStatistics('uncached');
+            expect(container.staleCache()).toBeTruthy();
+        });
+    });
+
+    describe('coverage counters from cache', function () {
+        let container, statistics;
+        let containerWithNoCaching, statisticsWithNoCaching;
+        beforeEach(function () {
+            container = new Coverage.CoverageStatisticsContainer(MockFilenames,
+                                                                 MockCache);
+            statistics = container.fetchStatistics('filename');
+
+            containerWithNoCaching = new Coverage.CoverageStatisticsContainer(MockFilenames);
+            statisticsWithNoCaching = containerWithNoCaching.fetchStatistics('filename');
+        });
+
+        it('have same executable lines as reflection', function () {
+            expect(statisticsWithNoCaching.expressionCounters)
+                .toEqual(statistics.expressionCounters);
+        });
+
+        it('have same branch exits as reflection', function () {
+            /* Branch starts on line 4 */
+            expect(statisticsWithNoCaching.branchCounters[4].exits[0].line)
+                .toEqual(statistics.branchCounters[4].exits[0].line);
+        });
+
+        it('have same branch points as reflection', function () {
+            expect(statisticsWithNoCaching.branchCounters[4].point)
+                .toEqual(statistics.branchCounters[4].point);
+        });
+
+        it('have same function keys as reflection', function () {
+            /* Functions start on line 1 */
+            expect(Object.keys(statisticsWithNoCaching.functionCounters))
+                .toEqual(Object.keys(statistics.functionCounters));
+        });
+    });
+});
diff --git a/installed-tests/js/testself.js b/installed-tests/js/testself.js
index a07de52..91f89a1 100644
--- a/installed-tests/js/testself.js
+++ b/installed-tests/js/testself.js
@@ -1,37 +1,34 @@
-const JSUnit = imports.jsUnit;
+describe('Test harness internal consistency', function () {
+    it('', function () {
+        var someUndefined;
+        var someNumber = 1;
+        var someOtherNumber = 42;
+        var someString = "hello";
+        var someOtherString = "world";
 
-var someUndefined;
-var someNumber = 1;
-var someOtherNumber = 42;
-var someString = "hello";
-var someOtherString = "world";
+        expect(true).toBeTruthy();
+        expect(false).toBeFalsy();
 
-JSUnit.assert(true);
-JSUnit.assertTrue(true);
-JSUnit.assertFalse(false);
+        expect(someNumber).toEqual(someNumber);
+        expect(someString).toEqual(someString);
 
-JSUnit.assertEquals(someNumber, someNumber);
-JSUnit.assertEquals(someString, someString);
+        expect(someNumber).not.toEqual(someOtherNumber);
+        expect(someString).not.toEqual(someOtherString);
 
-JSUnit.assertNotEquals(someNumber, someOtherNumber);
-JSUnit.assertNotEquals(someString, someOtherString);
+        expect(null).toBeNull();
+        expect(someNumber).not.toBeNull();
+        expect(someNumber).toBeDefined();
+        expect(someUndefined).not.toBeDefined();
+        expect(0 / 0).toBeNaN();
+        expect(someNumber).not.toBeNaN();
 
-JSUnit.assertNull(null);
-JSUnit.assertNotNull(someNumber);
-JSUnit.assertUndefined(someUndefined);
-JSUnit.assertNotUndefined(someNumber);
-JSUnit.assertNaN(0/0);
-JSUnit.assertNotNaN(someNumber);
+        expect(function () { throw {}; }).toThrow();
 
-// test assertRaises()
-JSUnit.assertRaises(function() { throw new Object(); });
-try {   // calling assertRaises with non-function is an error, not assertion failure
-    JSUnit.assertRaises(true);
-} catch(e) {
-    JSUnit.assertUndefined(e.isJsUnitException);
-}
-try {   // function not throwing an exception is assertion failure
-    JSUnit.assertRaises(function() { return true; });
-} catch(e) {
-    JSUnit.assertTrue(e.isJsUnitException);
-}
+        expect(function () {
+            // expecting toThrow with non-function is an error, not assertion failure
+            expect(true).toThrow();
+        }).toThrow();
+
+        expect(function () { return true; }).not.toThrow();
+    });
+});
diff --git a/installed-tests/minijasmine.cpp b/installed-tests/minijasmine.cpp
new file mode 100644
index 0000000..27e6a3d
--- /dev/null
+++ b/installed-tests/minijasmine.cpp
@@ -0,0 +1,143 @@
+/* -*- mode: C; c-basic-offset: 4; indent-tabs-mode: nil; -*- */
+/*
+ * Copyright (c) 2016 Philip Chimento
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to
+ * deal in the Software without restriction, including without limitation the
+ * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
+ * sell copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+ * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
+ * IN THE SOFTWARE.
+ */
+
+#include "config.h"
+
+#include <locale.h>
+#include <unistd.h>
+
+#include <glib.h>
+#include <glib/gstdio.h>
+
+#include "gjs/coverage.h"
+#include "gjs/gjs.h"
+#include "gjs/mem.h"
+
+G_GNUC_NORETURN
+void
+bail_out(const char *msg)
+{
+    g_print("Bail out! %s\n", msg);
+    exit(1);
+}
+
+int
+main(int argc, char **argv)
+{
+    if (argc < 2)
+        g_error("Need a test file");
+
+    /* The tests are known to fail in the presence of the JIT;
+     * we leak objects.
+     * https://bugzilla.gnome.org/show_bug.cgi?id=616193
+     */
+    g_setenv("GJS_DISABLE_JIT", "1", false);
+    /* The fact that this isn't the default is kind of lame... */
+    g_setenv("GJS_DEBUG_OUTPUT", "stderr", false);
+    /* Jasmine library has some code style nits that trip this */
+    g_setenv("GJS_DISABLE_EXTRA_WARNINGS", "1", false);
+
+    setlocale(LC_ALL, "");
+
+    if (g_getenv ("GJS_USE_UNINSTALLED_FILES") != NULL) {
+        g_irepository_prepend_search_path(g_getenv("TOP_BUILDDIR"));
+    } else {
+        g_irepository_prepend_search_path(INSTTESTDIR);
+    }
+
+    const char *coverage_prefix = g_getenv("GJS_UNIT_COVERAGE_PREFIX");
+    const char *coverage_output_path = g_getenv("GJS_UNIT_COVERAGE_OUTPUT");
+    const char *search_path[] = { "resource:///org/gjs/jsunit", NULL };
+
+    GjsContext *cx = gjs_context_new_with_search_path((char **)search_path);
+    GjsCoverage *coverage = NULL;
+
+    if (coverage_prefix) {
+        const char *coverage_prefixes[2] = { coverage_prefix, NULL };
+
+        if (coverage_output_path) {
+            bail_out("GJS_UNIT_COVERAGE_OUTPUT is required when using GJS_UNIT_COVERAGE_PREFIX");
+        }
+
+        char *path_to_cache_file = g_build_filename(coverage_output_path,
+                                                    ".internal-coverage-cache",
+                                                    NULL);
+        coverage = gjs_coverage_new_from_cache((const char **) coverage_prefixes,
+                                               cx, path_to_cache_file);
+        g_free(path_to_cache_file);
+    }
+
+    GError *error = NULL;
+    bool success;
+    int code;
+
+    success = gjs_context_eval(cx, "imports.minijasmine;", -1,
+                               "<jasmine>", &code, &error);
+    if (!success)
+        bail_out(error->message);
+
+    success = gjs_context_eval_file(cx, argv[1], &code, &error);
+    if (!success)
+        bail_out(error->message);
+
+    /* jasmineEnv.execute() queues up all the tests and runs them
+     * asynchronously. This should start after the main loop starts, otherwise
+     * we will hit the main loop only after several tests have already run. For
+     * consistency we should guarantee that there is a main loop running during
+     * all tests. */
+    const char *start_suite_script =
+        "const GLib = imports.gi.GLib;\n"
+        "GLib.idle_add(GLib.PRIORITY_DEFAULT, function () {\n"
+        "    try {\n"
+        "        window._jasmineEnv.execute();\n"
+        "    } catch (e) {\n"
+        "        print('Bail out! Exception occurred inside Jasmine:', e);\n"
+        "        window._jasmineRetval = 1;\n"
+        "        window._jasmineMain.quit();\n"
+        "    }\n"
+        "    return GLib.SOURCE_REMOVE;\n"
+        "});\n"
+        "window._jasmineMain.run();\n"
+        "window._jasmineRetval;";
+    success = gjs_context_eval(cx, start_suite_script, -1, "<jasmine-start>",
+                               &code, &error);
+    if (!success)
+        bail_out(error->message);
+
+    if (code != 0)
+        g_print("# Test script failed; assertions will be in gjs.log\n");
+
+    if (coverage) {
+        gjs_coverage_write_statistics(coverage, coverage_output_path);
+        g_clear_object(&coverage);
+    }
+
+    gjs_memory_report("before destroying context", false);
+    g_object_unref(cx);
+    gjs_memory_report("after destroying context", true);
+
+    /* For TAP, should actually be return 0; as a nonzero return code would
+     * indicate an error in the test harness. But that would be quite silly
+     * when running the tests outside of the TAP driver. */
+    return code;
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]