[gjs/wip/ptomato/tests: 25/26] tests: Use embedded copy of Jasmine to run tests



commit 5e3694acef84834e25d1100344a36ee9637b094e
Author: Philip Chimento <philip chimento gmail com>
Date:   Sat Dec 17 22:43:54 2016 -0800

    tests: Use embedded copy of Jasmine to run tests
    
    This replaces the old JSUnit test harness with an embedded copy of
    Jasmine [1], which makes writing tests less of a pain, includes more
    handy test facilities, and produces better output.
    
    jasmine.js is a copy of upstream Jasmine 2.5.2. minijasmine.js contains
    code for starting up Jasmine, adapting it to the GJS environment, and
    producing TAP output. minijasmine.cpp makes an executable which loads the
    preceding two files from the unit test GResource.
    
    All the tests in installed-tests/js are converted to use Jasmine's
    describe()/it() style. Quite often this allows simplifying them since
    Jasmine has features like array and object equality, spies, and clock
    ticks.
    
    [1] https://jasmine.github.io/2.5/introduction.html
    
    https://bugzilla.gnome.org/show_bug.cgi?id=775444

 Makefile-insttest.am                             |   29 +-
 Makefile-test.am                                 |   67 +-
 Makefile.am                                      |    1 -
 installed-tests/gjs-unit.cpp                     |  235 --
 installed-tests/js/jasmine.js                    | 3655 ++++++++++++++++++++++
 installed-tests/js/jsunit.gresources.xml         |    2 +
 installed-tests/js/minijasmine.js                |  113 +
 installed-tests/js/testByteArray.js              |  230 +-
 installed-tests/js/testCairo.js                  |  358 ++-
 installed-tests/js/testClass.js                  |  199 +-
 installed-tests/js/testCoverage.js               | 2565 +++++++---------
 installed-tests/js/testEverythingBasic.js        | 1213 ++++----
 installed-tests/js/testEverythingEncapsulated.js |  335 ++-
 installed-tests/js/testExceptions.js             |  265 +-
 installed-tests/js/testFormat.js                 |  120 +-
 installed-tests/js/testFundamental.js            |   21 +-
 installed-tests/js/testGDBus.js                  |  560 ++---
 installed-tests/js/testGIMarshalling.js          |  899 ++++---
 installed-tests/js/testGLib.js                   |   66 +-
 installed-tests/js/testGObjectClass.js           |  286 +-
 installed-tests/js/testGObjectInterface.js       |  503 ++--
 installed-tests/js/testGTypeClass.js             |   93 +-
 installed-tests/js/testGettext.js                |   13 +-
 installed-tests/js/testGtk.js                    |   87 +-
 installed-tests/js/testImporter.js               |  231 +-
 installed-tests/js/testInterface.js              |  445 ++--
 installed-tests/js/testLang.js                   |  203 +-
 installed-tests/js/testLocale.js                 |   49 +-
 installed-tests/js/testMainloop.js               |  185 +-
 installed-tests/js/testMetaClass.js              |   93 +-
 installed-tests/js/testNamespace.js              |   13 +-
 installed-tests/js/testParamSpec.js              |  129 +-
 installed-tests/js/testSignals.js                |  238 +-
 installed-tests/js/testSystem.js                 |   28 +-
 installed-tests/js/testTweener.js                |  818 +++---
 installed-tests/js/testself.js                   |   55 +-
 installed-tests/jsunit.test.in                   |    3 -
 installed-tests/minijasmine.cpp                  |  143 +
 installed-tests/minijasmine.test.in              |    4 +
 39 files changed, 8844 insertions(+), 5708 deletions(-)
---
diff --git a/Makefile-insttest.am b/Makefile-insttest.am
index 0eec7b0..4e56bb3 100644
--- a/Makefile-insttest.am
+++ b/Makefile-insttest.am
@@ -1,11 +1,9 @@
 EXTRA_DIST += \
-       installed-tests/jsunit.test.in                  \
+       installed-tests/minijasmine.test.in             \
        installed-tests/script.test.in                  \
        installed-tests/js/jsunit.gresources.xml        \
        $(NULL)
 
-MAINTAINERCLEANFILES += jsunit.test
-
 gjsinsttestdir = $(pkglibexecdir)/installed-tests
 installedtestmetadir = $(datadir)/installed-tests/gjs
 jstestsdir = $(gjsinsttestdir)/js
@@ -20,27 +18,18 @@ pkglib_LTLIBRARIES =
 
 if BUILDOPT_INSTALL_TESTS
 
-gjsinsttest_PROGRAMS += jsunit
+gjsinsttest_PROGRAMS += minijasmine
 gjsinsttest_DATA += $(TEST_INTROSPECTION_TYPELIBS)
-installedtestmeta_DATA += jsunit.test
-jstests_DATA += $(common_jstests_files)
+installedtestmeta_DATA += $(jasmine_tests:.js=.test)
+jstests_DATA += $(jasmine_tests)
 pkglib_LTLIBRARIES += libregress.la libwarnlib.la libgimarshallingtests.la
 
-if ENABLE_CAIRO
-jstests_DATA += installed-tests/js/testCairo.js
-endif
-
-if ENABLE_GTK
-jstests_DATA += installed-tests/js/testGtk.js
-endif
-
-if DBUS_TESTS
-jstests_DATA += installed-tests/js/testGDBus.js
-endif
-
-jsunit.test: installed-tests/jsunit.test.in Makefile
+%.test: %.js installed-tests/minijasmine.test.in Makefile
        $(AM_V_GEN)$(MKDIR_P) $(@D) && \
-       $(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir), < $< > $@.tmp && mv $@.tmp $@
+       $(SED) -e s,@pkglibexecdir\@,$(pkglibexecdir),g \
+               -e s,@name\@,$(notdir $<), \
+               < $(srcdir)/installed-tests/minijasmine.test.in > $@.tmp && \
+       mv $@.tmp $@
 
 %.test: installed-tests/scripts/%.js installed-tests/script.test.in Makefile
        $(AM_V_GEN)$(MKDIR_P) $(@D) && \
diff --git a/Makefile-test.am b/Makefile-test.am
index 96b6ff4..4404f82 100644
--- a/Makefile-test.am
+++ b/Makefile-test.am
@@ -3,8 +3,6 @@ EXTRA_DIST +=                   \
        test/run-test           \
        $(NULL)
 
-SKIPPED_TESTS =
-
 if XVFB_TESTS
 XVFB_INVOCATION = $(XVFB) -ac -noreset -screen 0 1024x768x16
 XIDS = 101 102 103 104 105 106 107 197 199 211 223 227 293 307 308 309 310 311 \
@@ -24,18 +22,6 @@ else
 XVFB_START =
 endif
 
-if !DBUS_TESTS
-SKIPPED_TESTS += /js/GDBus
-endif
-
-if !ENABLE_GTK
-SKIPPED_TESTS += /js/Gtk
-endif
-
-if !ENABLE_CAIRO
-SKIPPED_TESTS += /js/Cairo
-endif
-
 ### TEST RESOURCES #####################################################
 
 mock_js_resources_files := $(shell glib-compile-resources --sourcedir=$(srcdir) --generate-dependencies 
$(srcdir)/test/mock-js-resources.gresource.xml)
@@ -95,7 +81,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)                          \
@@ -122,23 +108,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 ##########################################################
 
@@ -258,6 +241,20 @@ common_jstests_files =                                             \
        installed-tests/js/testTweener.js                       \
        $(NULL)
 
+jasmine_tests = $(common_jstests_files)
+
+if DBUS_TESTS
+jasmine_tests += installed-tests/js/testGDBus.js
+endif
+
+if ENABLE_GTK
+jasmine_tests += installed-tests/js/testGtk.js
+endif
+
+if ENABLE_CAIRO
+jasmine_tests += installed-tests/js/testCairo.js
+endif
+
 EXTRA_DIST +=                                          \
        $(common_jstests_files)                         \
        installed-tests/js/testCairo.js                 \
@@ -270,10 +267,8 @@ EXTRA_DIST +=                                              \
 # GJS_PATH is empty here since we want to force the use of our own
 # resources. G_FILENAME_ENCODING ensures filenames are not UTF-8.
 AM_TESTS_ENVIRONMENT =                                 \
-       export TOP_SRCDIR="$(abs_top_srcdir)";          \
        export TOP_BUILDDIR="$(abs_top_builddir)";      \
        export GJS_USE_UNINSTALLED_FILES=1;             \
-       export GJS_TEST_SKIP="$(SKIPPED_TESTS)";        \
        export GJS_PATH=;                               \
        export GI_TYPELIB_PATH="$(builddir):$${GI_TYPELIB_PATH:+:$$GI_TYPELIB_PATH}"; \
        export LD_LIBRARY_PATH="$(builddir)/.libs:$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH}"; \
@@ -283,16 +278,28 @@ AM_TESTS_ENVIRONMENT =                                    \
 
 simple_tests = test/testCommandLine.sh
 EXTRA_DIST += $(simple_tests)
-TESTS += $(simple_tests)
+
+TESTS =                                \
+       gjs-tests               \
+       $(simple_tests)         \
+       $(jasmine_tests)        \
+       $(NULL)
+
+TEST_EXTENSIONS = .js
 
 LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
+JS_LOG_DRIVER = env AM_TAP_AWK='$(AWK)' $(SHELL) $(top_srcdir)/tap-driver.sh
 
 if DBUS_TESTS
 LOG_COMPILER = $(DBUS_RUN_SESSION)
 AM_LOG_FLAGS = --config-file=$(srcdir)/test/test-bus.conf -- $(top_srcdir)/test/run-test
+JS_LOG_COMPILER = $(DBUS_RUN_SESSION)
+AM_JS_LOG_FLAGS = --config-file=$(srcdir)/test/test-bus.conf -- $(top_builddir)/minijasmine
 else
 LOG_COMPILER = $(top_srcdir)/test/run-test
 AM_LOG_FLAGS =
+JS_LOG_COMPILER = $(top_builddir)/minijasmine
+AM_JS_LOG_FLAGS =
 endif !DBUS_TESTS
 
 if CODE_COVERAGE_ENABLED
diff --git a/Makefile.am b/Makefile.am
index 66c82d5..1d6f3d8 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -14,7 +14,6 @@ CLEANFILES =
 EXTRA_DIST =
 check_PROGRAMS =
 check_LTLIBRARIES =
-TESTS = $(check_PROGRAMS)
 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/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/testCairo.js b/installed-tests/js/testCairo.js
index 5a6a892..6dc6f1f 100644
--- a/installed-tests/js/testCairo.js
+++ b/installed-tests/js/testCairo.js
@@ -1,180 +1,210 @@
-const JSUnit = imports.jsUnit;
 const Cairo = imports.cairo;
-const Everything = imports.gi.Regress;
+const Regress = imports.gi.Regress;
 
 function _ts(obj) {
     return obj.toString().slice(8, -1);
 }
 
-function _createSurface() {
-    return new Cairo.ImageSurface(Cairo.Format.ARGB32, 1, 1);
-}
-
-function _createContext() {
-    return new Cairo.Context(_createSurface());
-}
-
-function testContext() {
-    let cr = _createContext();
-    JSUnit.assertTrue(cr instanceof Cairo.Context);
-}
-
-function testContextMethods() {
-    let cr = _createContext();
-    JSUnit.assertTrue(cr instanceof Cairo.Context);
-    cr.save();
-    cr.restore();
-
-    let surface = _createSurface();
-    JSUnit.assertEquals(_ts(cr.getTarget()), "CairoImageSurface");
-
-    let pattern = Cairo.SolidPattern.createRGB(1, 2, 3);
-    cr.setSource(pattern);
-    JSUnit.assertEquals(_ts(cr.getSource()), "CairoSolidPattern");
-    cr.setSourceSurface(surface, 0, 0);
-
-    cr.pushGroup();
-    cr.popGroup();
-
-    cr.pushGroupWithContent(Cairo.Content.COLOR);
-    cr.popGroupToSource();
-
-    cr.setSourceRGB(1, 2, 3);
-    cr.setSourceRGBA(1, 2, 3, 4);
-
-    cr.setAntialias(Cairo.Antialias.NONE);
-    JSUnit.assertEquals("antialias", cr.getAntialias(), Cairo.Antialias.NONE);
-
-    cr.setFillRule(Cairo.FillRule.EVEN_ODD);
-    JSUnit.assertEquals("fillRule", cr.getFillRule(), Cairo.FillRule.EVEN_ODD);
-
-    cr.setLineCap(Cairo.LineCap.ROUND);
-    JSUnit.assertEquals("lineCap", cr.getLineCap(), Cairo.LineCap.ROUND);
-
-    cr.setLineJoin(Cairo.LineJoin.ROUND);
-    JSUnit.assertEquals("lineJoin", cr.getLineJoin(), Cairo.LineJoin.ROUND);
-
-    cr.setLineWidth(1138);
-    JSUnit.assertEquals("lineWidth", cr.getLineWidth(), 1138);
-
-    cr.setMiterLimit(42);
-    JSUnit.assertEquals("miterLimit", cr.getMiterLimit(), 42);
-
-    cr.setOperator(Cairo.Operator.IN);
-    JSUnit.assertEquals("operator", cr.getOperator(), Cairo.Operator.IN);
-
-    cr.setTolerance(144);
-    JSUnit.assertEquals("tolerance", cr.getTolerance(), 144);
-
-    cr.clip();
-    cr.clipPreserve();
-    let rv = cr.clipExtents();
-    JSUnit.assertEquals("clipExtents", rv.length, 4);
-
-    cr.fill();
-    cr.fillPreserve();
-    let rv = cr.fillExtents();
-    JSUnit.assertEquals("fillExtents", rv.length, 4);
-
-    cr.mask(pattern);
-    cr.maskSurface(surface, 0, 0);
-
-    cr.paint();
-    cr.paintWithAlpha(1);
-
-    cr.stroke();
-    cr.strokePreserve();
-    let rv = cr.strokeExtents();
-    JSUnit.assertEquals("strokeExtents", rv.length, 4);
-
-    cr.inFill(0, 0);
-    cr.inStroke(0, 0);
-    cr.copyPage();
-    cr.showPage();
-
-    let dc = cr.getDashCount();
-    JSUnit.assertEquals("dashCount", dc, 0);
-
-    cr.translate(10, 10);
-    cr.scale(10, 10);
-    cr.rotate(180);
-    cr.identityMatrix();
-    let rv = cr.userToDevice(0, 0);
-    JSUnit.assertEquals("userToDevice", rv.length, 2);
-
-    let rv = cr.userToDeviceDistance(0, 0);
-    JSUnit.assertEquals("userToDeviceDistance", rv.length, 2);
-
-    let rv = cr.deviceToUser(0, 0);
-    JSUnit.assertEquals("deviceToUser", rv.length, 2);
-
-    let rv = cr.deviceToUserDistance(0, 0);
-    JSUnit.assertEquals("deviceToUserDistance", rv.length, 2);
+describe('Cairo', function () {
+    let cr, surface;
+    beforeEach(function () {
+        surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 1, 1);
+        cr = new Cairo.Context(surface);
+    });
 
-    cr.showText("foobar");
+    describe('context', function () {
+        it('has the right type', function () {
+            expect(cr instanceof Cairo.Context).toBeTruthy();
+        });
+
+        it('reports its target surface', function () {
+            expect(_ts(cr.getTarget())).toEqual('CairoImageSurface');
+        });
 
-    cr.moveTo(0, 0);
-    cr.setDash([1, 0.5], 1);
-    cr.lineTo(1, 0);
-    cr.lineTo(1, 1);
-    cr.lineTo(0, 1);
-    cr.closePath();
-    let path = cr.copyPath();
-    cr.fill();
-    cr.appendPath(path);
-    cr.stroke();
-}
+        it('can set its source to a pattern', function () {
+            let pattern = Cairo.SolidPattern.createRGB(1, 2, 3);
+            cr.setSource(pattern);
+            expect(_ts(cr.getSource())).toEqual('CairoSolidPattern');
+        });
+
+        it('can set its antialias', function () {
+            cr.setAntialias(Cairo.Antialias.NONE);
+            expect(cr.getAntialias()).toEqual(Cairo.Antialias.NONE);
+        });
 
-function testSolidPattern() {
-    let cr = _createContext();
+        it('can set its fill rule', function () {
+            cr.setFillRule(Cairo.FillRule.EVEN_ODD);
+            expect(cr.getFillRule()).toEqual(Cairo.FillRule.EVEN_ODD);
+        });
 
-    let p1 = Cairo.SolidPattern.createRGB(1, 2, 3);
-    JSUnit.assertEquals(_ts(p1), "CairoSolidPattern");
-    cr.setSource(p1);
-    JSUnit.assertEquals(_ts(cr.getSource()), "CairoSolidPattern");
+        it('can set its line cap', function () {
+            cr.setLineCap(Cairo.LineCap.ROUND);
+            expect(cr.getLineCap()).toEqual(Cairo.LineCap.ROUND);
+        });
 
-    let p2 = Cairo.SolidPattern.createRGBA(1, 2, 3, 4);
-    JSUnit.assertEquals(_ts(p2), "CairoSolidPattern");
-    cr.setSource(p2);
-    JSUnit.assertEquals(_ts(cr.getSource()), "CairoSolidPattern");
-}
-
-function testSurfacePattern() {
-    let cr = _createContext();
-    let surface = _createSurface();
-    let p1 = new Cairo.SurfacePattern(surface);
-    JSUnit.assertEquals(_ts(p1), "CairoSurfacePattern");
-    cr.setSource(p1);
-    JSUnit.assertEquals(_ts(cr.getSource()), "CairoSurfacePattern");
-}
+        it('can set its line join', function () {
+            cr.setLineJoin(Cairo.LineJoin.ROUND);
+            expect(cr.getLineJoin()).toEqual(Cairo.LineJoin.ROUND);
+        });
+
+        it('can set its line width', function () {
+            cr.setLineWidth(1138);
+            expect(cr.getLineWidth()).toEqual(1138);
+        });
+
+        it('can set its miter limit', function () {
+            cr.setMiterLimit(42);
+            expect(cr.getMiterLimit()).toEqual(42);
+        });
+
+        it('can set its operator', function () {
+            cr.setOperator(Cairo.Operator.IN);
+            expect(cr.getOperator()).toEqual(Cairo.Operator.IN);
+        });
+
+        it('can set its tolerance', function () {
+            cr.setTolerance(144);
+            expect(cr.getTolerance()).toEqual(144);
+        });
+
+        it('has a rectangle as clip extents', function () {
+            expect(cr.clipExtents().length).toEqual(4);
+        });
+
+        it('has a rectangle as fill extents', function () {
+            expect(cr.fillExtents().length).toEqual(4);
+        });
+
+        it('has a rectangle as stroke extents', function () {
+            expect(cr.strokeExtents().length).toEqual(4);
+        });
+
+        it('has zero dashes initially', function () {
+            expect(cr.getDashCount()).toEqual(0);
+        });
+
+        it('transforms user to device coordinates', function () {
+            expect(cr.userToDevice(0, 0).length).toEqual(2);
+        });
+
+        it('transforms user to device distance', function () {
+            expect(cr.userToDeviceDistance(0, 0).length).toEqual(2);
+        });
+
+        it('transforms device to user coordinates', function () {
+            expect(cr.deviceToUser(0, 0).length).toEqual(2);
+        });
+
+        it('transforms device to user distance', function () {
+            expect(cr.deviceToUserDistance(0, 0).length).toEqual(2);
+        });
+
+        it('can call various, otherwise untested, methods without crashing', function () {
+            expect(() => {
+                cr.save();
+                cr.restore();
+
+                cr.setSourceSurface(surface, 0, 0);
+
+                cr.pushGroup();
+                cr.popGroup();
+
+                cr.pushGroupWithContent(Cairo.Content.COLOR);
+                cr.popGroupToSource();
+
+                cr.setSourceRGB(1, 2, 3);
+                cr.setSourceRGBA(1, 2, 3, 4);
+
+                cr.clip();
+                cr.clipPreserve();
+
+                cr.fill();
+                cr.fillPreserve();
+
+                let pattern = Cairo.SolidPattern.createRGB(1, 2, 3);
+                cr.mask(pattern);
+                cr.maskSurface(surface, 0, 0);
+
+                cr.paint();
+                cr.paintWithAlpha(1);
+
+                cr.stroke();
+                cr.strokePreserve();
+
+                cr.inFill(0, 0);
+                cr.inStroke(0, 0);
+                cr.copyPage();
+                cr.showPage();
+
+                cr.translate(10, 10);
+                cr.scale(10, 10);
+                cr.rotate(180);
+                cr.identityMatrix();
+
+                cr.showText("foobar");
+
+                cr.moveTo(0, 0);
+                cr.setDash([1, 0.5], 1);
+                cr.lineTo(1, 0);
+                cr.lineTo(1, 1);
+                cr.lineTo(0, 1);
+                cr.closePath();
+                let path = cr.copyPath();
+                cr.fill();
+                cr.appendPath(path);
+                cr.stroke();
+            }).not.toThrow();
+        });
+
+        it('can be marshalled through a signal handler', function () {
+            let o = new Regress.TestObj();
+            let foreignSpy = jasmine.createSpy('sig-with-foreign-struct');
+            o.connect('sig-with-foreign-struct', foreignSpy);
+            o.emit_sig_with_foreign_struct();
+            expect(foreignSpy).toHaveBeenCalledWith(o, cr);
+        });
+    });
 
-function testLinearGradient() {
-    let cr = _createContext();
-    let surface = _createSurface();
-    let p1 = new Cairo.LinearGradient(1, 2, 3, 4);
-    JSUnit.assertEquals(_ts(p1), "CairoLinearGradient");
-    cr.setSource(p1);
-    JSUnit.assertEquals(_ts(cr.getSource()), "CairoLinearGradient");
-}
+    describe('solid pattern', function () {
+        it('can be created from RGB static method', function () {
+            let p1 = Cairo.SolidPattern.createRGB(1, 2, 3);
+            expect(_ts(p1)).toEqual('CairoSolidPattern');
+            cr.setSource(p1);
+            expect(_ts(cr.getSource())).toEqual('CairoSolidPattern');
+        });
+
+        it('can be created from RGBA static method', function () {
+            let p2 = Cairo.SolidPattern.createRGBA(1, 2, 3, 4);
+            expect(_ts(p2)).toEqual('CairoSolidPattern');
+            cr.setSource(p2);
+            expect(_ts(cr.getSource())).toEqual('CairoSolidPattern');
+        });
+    });
 
-function testRadialGradient() {
-    let cr = _createContext();
-    let surface = _createSurface();
-    let p1 = new Cairo.RadialGradient(1, 2, 3, 4, 5, 6);
-    JSUnit.assertEquals(_ts(p1), "CairoRadialGradient");
-    cr.setSource(p1);
-    JSUnit.assertEquals(_ts(cr.getSource()), "CairoRadialGradient");
-}
+    describe('surface pattern', function () {
+        it('can be created and added as a source', function () {
+            let p1 = new Cairo.SurfacePattern(surface);
+            expect(_ts(p1)).toEqual('CairoSurfacePattern');
+            cr.setSource(p1);
+            expect(_ts(cr.getSource())).toEqual('CairoSurfacePattern');
+        });
+    });
 
-function testCairoSignal() {
-    let o = new Everything.TestObj();
-    let called = false;
-    o.connect('sig-with-foreign-struct', function(o, cr) {
-        called = true;
-        JSUnit.assertEquals(_ts(cr), "CairoContext");
+    describe('linear gradient', function () {
+        it('can be created and added as a source', function () {
+            let p1 = new Cairo.LinearGradient(1, 2, 3, 4);
+            expect(_ts(p1)).toEqual('CairoLinearGradient');
+            cr.setSource(p1);
+            expect(_ts(cr.getSource())).toEqual('CairoLinearGradient');
+        });
     });
-    o.emit_sig_with_foreign_struct();
-    JSUnit.assertTrue(called);
-}
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    describe('radial gradient', function () {
+        it('can be created and added as a source', function () {
+            let p1 = new Cairo.RadialGradient(1, 2, 3, 4, 5, 6);
+            expect(_ts(p1)).toEqual('CairoRadialGradient');
+            cr.setSource(p1);
+            expect(_ts(cr.getSource())).toEqual('CairoRadialGradient');
+        });
+    });
+});
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/testEverythingBasic.js b/installed-tests/js/testEverythingBasic.js
index e982172..143ef08 100644
--- a/installed-tests/js/testEverythingBasic.js
+++ b/installed-tests/js/testEverythingBasic.js
@@ -1,7 +1,4 @@
-// This used to be called "Everything"
-
-const JSUnit = imports.jsUnit;
-const Everything = imports.gi.Regress;
+const Regress = imports.gi.Regress;
 const WarnLib = imports.gi.WarnLib;
 
 // We use Gio to have some objects that we know exist
@@ -10,661 +7,649 @@ const Gio = imports.gi.Gio;
 const GObject = imports.gi.GObject;
 const Lang = imports.lang;
 
-const INT8_MIN = (-128);
-const INT16_MIN = (-32767-1);
-const INT32_MIN = (-2147483647-1);
-const INT64_MIN = (-9223372036854775807-1);
+describe('Life, the Universe and Everything', function () {
+    it('includes booleans', function () {
+        expect(Regress.test_boolean(false)).toBe(false);
+        expect(Regress.test_boolean(true)).toBe(true);
+    });
 
-const INT8_MAX = (127);
-const INT16_MAX = (32767);
-const INT32_MAX = (2147483647);
-const INT64_MAX = (9223372036854775807);
+    [8, 16, 32, 64].forEach(bits => {
+        it('includes ' + bits + '-bit integers', function () {
+            let method = 'test_int' + bits;
+            expect(Regress[method](42)).toBe(42);
+            expect(Regress[method](-42)).toBe(-42);
+        });
+
+        it('includes unsigned ' + bits + '-bit integers', function () {
+            let method = 'test_uint' + bits;
+            expect(Regress[method](42)).toBe(42);
+        });
+    });
 
-const UINT8_MAX = (255);
-const UINT16_MAX = (65535);
-const UINT32_MAX = (4294967295);
-const UINT64_MAX = (18446744073709551615);
+    ['short', 'int', 'long', 'ssize', 'float', 'double'].forEach(type => {
+        it('includes ' + type + 's', function () {
+            let method = 'test_' + type;
+            expect(Regress[method](42)).toBe(42);
+            expect(Regress[method](-42)).toBe(-42);
+        });
+    });
 
-function testLifeUniverseAndEverything() {
-    JSUnit.assertEquals(false, Everything.test_boolean(false));
-    JSUnit.assertEquals(true, Everything.test_boolean(true));
+    ['ushort', 'uint', 'ulong', 'size'].forEach(type => {
+        it('includes ' + type + 's', function () {
+            let method = 'test_' + type;
+            expect(Regress[method](42)).toBe(42);
+        });
+    });
 
-    JSUnit.assertEquals(42, Everything.test_int8(42));
-    JSUnit.assertEquals(-42, Everything.test_int8(-42));
+    it('includes wide characters', function () {
+        expect(Regress.test_unichar('c')).toBe('c');
+        expect(Regress.test_unichar('')).toBe('');
+        expect(Regress.test_unichar('\u2665')).toBe('\u2665');
+    });
 
-    JSUnit.assertEquals(42, Everything.test_uint8(42));
+    it('includes time_t', function () {
+        let now = Math.floor(new Date().getTime() / 1000);
+        let bounced = Math.floor(Regress.test_timet(now));
+        expect(bounced).toEqual(now);
+    });
 
-    JSUnit.assertEquals(42, Everything.test_int16(42));
-    JSUnit.assertEquals(-42, Everything.test_int16(-42));
+    describe('Limits', function () {
+        const Limits = {
+            '8': {
+                MIN: -128,
+                MAX: 127,
+                UMAX: 255,
+            },
+            '16': {
+                MIN: -32767 - 1,
+                MAX: 32767,
+                UMAX: 65535,
+            },
+            '32': {
+                MIN: -2147483647 - 1,
+                MAX: 2147483647,
+                UMAX: 4294967295,
+            },
+            '64': {
+                MIN: -9223372036854775807 - 1,
+                MAX: 9223372036854775807,
+                UMAX: 18446744073709551615,
+            },
+        };
+
+        const skip = {
+            'UMAX64': true,  // FAIL: expected 18446744073709552000, got 0
+            'MAX64': true,   // FAIL: expected 9223372036854776000, got -9223372036854776000
+        };
+
+        function run_test(bytes, limit, method_stem) {
+            if(skip[limit + bytes])
+                pending("This test doesn't work");
+            let val = Limits[bytes][limit];
+            expect(Regress[method_stem + bytes](val)).toBe(val);
+        }
+        ['8', '16', '32', '64'].forEach(bytes => {
+            it('marshals max value of unsigned ' + bytes + '-bit integers', function () {
+                run_test(bytes, 'UMAX', 'test_uint');
+            });
+
+            it('marshals min value of signed ' + bytes + '-bit integers', function () {
+                run_test(bytes, 'MIN', 'test_int');
+            });
+
+            it('marshals max value of signed ' + bytes + '-bit integers', function () {
+                run_test(bytes, 'MAX', 'test_int');
+            });
+        });
+    });
 
-    JSUnit.assertEquals(42, Everything.test_uint16(42));
+    describe('No implicit conversion to unsigned', function () {
+        ['uint8', 'uint16', 'uint32', 'uint64', 'uint', 'size'].forEach(type => {
+            it('for ' + type, function () {
+                expect(() => Regress['test_' + type](-42)).toThrow();
+            });
+        });
+    });
 
-    JSUnit.assertEquals(42, Everything.test_int32(42));
-    JSUnit.assertEquals(-42, Everything.test_int32(-42));
+    it('throws when constructor called without new', function () {
+        expect(() => Gio.AppLaunchContext())
+            .toThrowError(/Constructor called as normal method/);
+    });
 
-    JSUnit.assertEquals(42, Everything.test_uint32(42));
+    describe('String arrays', function () {
+        it('marshalling in', function () {
+            expect(Regress.test_strv_in(['1', '2', '3'])).toBeTruthy();
+            // Second two are deliberately not strings
+            expect(() => Regress.test_strv_in(['1', 2, 3])).toThrow();
+        });
+
+        it('marshalling out', function () {
+            expect(Regress.test_strv_out())
+                .toEqual(['thanks', 'for', 'all', 'the', 'fish']);
+        });
+
+        it('marshalling out with container transfer', function () {
+            expect(Regress.test_strv_out_container()).toEqual(['1', '2', '3']);
+        });
+    });
 
-    JSUnit.assertEquals(42, Everything.test_int64(42));
-    JSUnit.assertEquals(-42, Everything.test_int64(-42));
+    it('in after out', function () {
+        const str = "hello";
+        let len = Regress.test_int_out_utf8(str);
+        expect(len).toEqual(str.length);
+    });
 
-    JSUnit.assertEquals(42, Everything.test_uint64(42));
+    describe('UTF-8 strings', function () {
+        const CONST_STR = "const \u2665 utf8";
+        const NONCONST_STR = "nonconst \u2665 utf8";
 
-    JSUnit.assertEquals(42, Everything.test_short(42));
-    JSUnit.assertEquals(-42, Everything.test_short(-42));
+        it('as return types', function () {
+            expect(Regress.test_utf8_const_return()).toEqual(CONST_STR);
+            expect(Regress.test_utf8_nonconst_return()).toEqual(NONCONST_STR);
+        });
 
-    JSUnit.assertEquals(42, Everything.test_ushort(42));
+        it('as in parameters', function () {
+            Regress.test_utf8_const_in(CONST_STR);
+        });
 
-    JSUnit.assertEquals(42, Everything.test_int(42));
-    JSUnit.assertEquals(-42, Everything.test_int(-42));
+        it('as out parameters', function () {
+            expect(Regress.test_utf8_out()).toEqual(NONCONST_STR);
+        });
 
-    JSUnit.assertEquals(42, Everything.test_uint(42));
+        // FIXME: this is broken due to a change in gobject-introspection.
+        xit('as in-out parameters', function () {
+            expect(Regress.test_utf8_inout(CONST_STR)).toEqual(NONCONST_STR);
+        }).pend('https://bugzilla.gnome.org/show_bug.cgi?id=736517');
+    });
 
-    JSUnit.assertEquals(42, Everything.test_long(42));
-    JSUnit.assertEquals(-42, Everything.test_long(-42));
+    it('return values in filename encoding', function () {
+        let filenames = Regress.test_filename_return();
+        expect(filenames).toEqual(['\u00e5\u00e4\u00f6', '/etc/fstab']);
+    });
 
-    JSUnit.assertEquals(42, Everything.test_ulong(42));
+    it('static methods', function () {
+        let v = Regress.TestObj.new_from_file("/enoent");
+        expect(v instanceof Regress.TestObj).toBeTruthy();
+    });
 
-    JSUnit.assertEquals(42, Everything.test_ssize(42));
-    JSUnit.assertEquals(-42, Everything.test_ssize(-42));
+    it('closures', function () {
+        let callback = jasmine.createSpy('callback').and.returnValue(42);
+        expect(Regress.test_closure(callback)).toEqual(42);
+        expect(callback).toHaveBeenCalledWith();
+    });
 
-    JSUnit.assertEquals(42, Everything.test_size(42));
+    it('closures with one argument', function () {
+        let callback = jasmine.createSpy('callback')
+            .and.callFake(someValue => someValue);
+        expect(Regress.test_closure_one_arg(callback, 42)).toEqual(42);
+        expect(callback).toHaveBeenCalledWith(42);
+    });
 
-    JSUnit.assertEquals(42, Everything.test_float(42));
-    JSUnit.assertEquals(-42, Everything.test_float(-42));
+    it('callbacks', function () {
+        let callback = jasmine.createSpy('callback').and.returnValue(42);
+        expect(Regress.test_callback(callback)).toEqual(42);
+    });
 
-    JSUnit.assertEquals(42, Everything.test_double(42));
-    JSUnit.assertEquals(-42, Everything.test_double(-42));
+    it('null / undefined callback', function () {
+        expect(Regress.test_callback(null)).toEqual(0);
+        expect(() => Regress.test_callback(undefined)).toThrow();
+    });
 
-    JSUnit.assertEquals("c", Everything.test_unichar("c"));
-    JSUnit.assertEquals("", Everything.test_unichar(""));
-    JSUnit.assertEquals("\u2665", Everything.test_unichar("\u2665"));
+    it('array callbacks', function () {
+        let callback = jasmine.createSpy('callback').and.returnValue(7);
+        expect(Regress.test_array_callback(callback)).toEqual(14);
+        expect(callback).toHaveBeenCalledWith([-1, 0, 1, 2], ["one", "two", "three"]);
+    });
 
-    let now = Math.floor(new Date().getTime() / 1000);
-    let bounced = Math.floor(Everything.test_timet(now));
-    JSUnit.assertEquals(bounced, now);
-}
+    it('null array callback', function () {
+        expect(() => Regress.test_array_callback(null)).toThrow();
+    });
 
-function testLimits() {
-    JSUnit.assertEquals(UINT8_MAX, Everything.test_uint8(UINT8_MAX));
-    JSUnit.assertEquals(UINT16_MAX, Everything.test_uint16(UINT16_MAX));
-    JSUnit.assertEquals(UINT32_MAX, Everything.test_uint32(UINT32_MAX));
+    it('callback with transfer-full return value', function () {
+        function callback() {
+            return Regress.TestObj.new_from_file("/enoent");
+        }
+        Regress.test_callback_return_full(callback);
+    });
 
-    // FAIL: expected 18446744073709552000, got 0
-    //assertEquals(UINT64_MAX, Everything.test_uint64(UINT64_MAX));
+    it('callback with destroy-notify', function () {
+        let testObj = {
+            test: function (data) { return data; },
+        };
+        spyOn(testObj, 'test').and.callThrough();
+        expect(Regress.test_callback_destroy_notify(function () {
+            return testObj.test(42);
+        }.bind(testObj))).toEqual(42);
+        expect(testObj.test).toHaveBeenCalledTimes(1);
+        expect(Regress.test_callback_thaw_notifications()).toEqual(42);
+    });
 
-    JSUnit.assertEquals(INT8_MIN, Everything.test_int8(INT8_MIN));
-    JSUnit.assertEquals(INT8_MAX, Everything.test_int8(INT8_MAX));
-    JSUnit.assertEquals(INT16_MIN, Everything.test_int16(INT16_MIN));
-    JSUnit.assertEquals(INT16_MAX, Everything.test_int16(INT16_MAX));
-    JSUnit.assertEquals(INT32_MIN, Everything.test_int32(INT32_MIN));
-    JSUnit.assertEquals(INT32_MAX, Everything.test_int32(INT32_MAX));
-    JSUnit.assertEquals(INT64_MIN, Everything.test_int64(INT64_MIN));
+    it('async callback', function () {
+        Regress.test_callback_async(() => 44);
+        expect(Regress.test_callback_thaw_async()).toEqual(44);
+    });
 
-    // FAIL: expected 9223372036854776000, got -9223372036854776000
-    //assertEquals(INT64_MAX, Everything.test_int64(INT64_MAX));
-}
+    it('method taking a GValue', function () {
+        expect(Regress.test_int_value_arg(42)).toEqual(42);
+    });
 
-function testNoImplicitConversionToUnsigned() {
-    JSUnit.assertRaises(function() { return Everything.test_uint8(-42); });
-    JSUnit.assertRaises(function() { return Everything.test_uint16(-42); });
-    JSUnit.assertRaises(function() { return Everything.test_uint32(-42); });
-
-    JSUnit.assertRaises(function() { return Everything.test_uint64(-42); });
-
-    JSUnit.assertRaises(function() { return Everything.test_uint(-42); });
-    JSUnit.assertRaises(function() { return Everything.test_size(-42); });
-}
-
-
-function testBadConstructor() {
-    try {
-        Gio.AppLaunchContext();
-    } catch (e) {
-        JSUnit.assert(e.message.indexOf("Constructor called as normal method") >= 0);
-    }
-}
-
-function testStrv() {
-    JSUnit.assertTrue(Everything.test_strv_in(['1', '2', '3']));
-    // Second two are deliberately not strings
-    JSUnit.assertRaises(function() { Everything.test_strv_in(['1', 2, 3]); });
+    it('method returning a GValue', function () {
+        expect(Regress.test_value_return(42)).toEqual(42);
+    });
 
-    let strv = Everything.test_strv_out();
-    JSUnit.assertEquals(5, strv.length);
-    JSUnit.assertEquals("thanks", strv[0]);
-    JSUnit.assertEquals("for", strv[1]);
-    JSUnit.assertEquals("all", strv[2]);
-    JSUnit.assertEquals("the", strv[3]);
-    JSUnit.assertEquals("fish", strv[4]);
-
-    strv = Everything.test_strv_out_container();
-    JSUnit.assertEquals(3, strv.length);
-    JSUnit.assertEquals("1", strv[0]);
-    JSUnit.assertEquals("2", strv[1]);
-    JSUnit.assertEquals("3", strv[2]);
-}
-
-function testInAfterOut() {
-    const str = "hello";
-
-    let len = Everything.test_int_out_utf8(str);
-    JSUnit.assertEquals("testInAfterOut", str.length, len);
-}
-
-function testUtf8() {
-    const CONST_STR = "const \u2665 utf8";
-    const NONCONST_STR = "nonconst \u2665 utf8";
-
-    JSUnit.assertEquals(CONST_STR, Everything.test_utf8_const_return());
-    JSUnit.assertEquals(NONCONST_STR, Everything.test_utf8_nonconst_return());
-    Everything.test_utf8_const_in(CONST_STR);
-    JSUnit.assertEquals(NONCONST_STR, Everything.test_utf8_out());
-    // FIXME: these are broken due to a change in gobject-introspection.
-    // Disable them for now. See https://bugzilla.gnome.org/show_bug.cgi?id=736517
-    // JSUnit.assertEquals(NONCONST_STR, Everything.test_utf8_inout(CONST_STR));
-    // JSUnit.assertEquals(NONCONST_STR, Everything.test_utf8_inout(CONST_STR));
-    // JSUnit.assertEquals(NONCONST_STR, Everything.test_utf8_inout(CONST_STR));
-    // JSUnit.assertEquals(NONCONST_STR, Everything.test_utf8_inout(CONST_STR));
-}
-
-function testFilenameReturn() {
-    var filenames = Everything.test_filename_return();
-    JSUnit.assertEquals(2, filenames.length);
-    JSUnit.assertEquals('\u00e5\u00e4\u00f6', filenames[0]);
-    JSUnit.assertEquals('/etc/fstab', filenames[1]);
-}
-
-function testStaticMeth() {
-    let v = Everything.TestObj.new_from_file("/enoent");
-    JSUnit.assertTrue(v instanceof Everything.TestObj);
-}
-
-function testClosure() {
-    let arguments_length = -1;
-    let someCallback = function() {
-                           arguments_length = arguments.length;
-                           return 42;
-                       };
-
-    let i = Everything.test_closure(someCallback);
-
-    JSUnit.assertEquals('callback arguments length', 0, arguments_length);
-    JSUnit.assertEquals('callback return value', 42, i);
-}
-
-function testClosureOneArg() {
-    let arguments_length = -1;
-    let someCallback = function(someValue) {
-                           arguments_length = arguments.length;
-                           JSUnit.assertEquals(1, arguments.length);
-                           return someValue;
-                       };
-
-    let i = Everything.test_closure_one_arg(someCallback, 42);
-
-    JSUnit.assertEquals('callback arguments length', 1, arguments_length);
-    JSUnit.assertEquals('callback with one arg return value', 42, i);
-}
-
-function testCallback() {
-    let callback = function() {
-                       return 42;
-                   };
-    JSUnit.assertEquals('Callback', Everything.test_callback(callback), 42);
-
-    JSUnit.assertEquals('CallbackNull', Everything.test_callback(null), 0);
-    JSUnit.assertRaises('CallbackUndefined', function () { Everything.test_callback(undefined); });
-}
-
-function testArrayCallback() {
-    function arrayEqual(ref, one) {
-        JSUnit.assertEquals(ref.length, one.length);
-        for (let i = 0; i < ref.length; i++)
-            JSUnit.assertEquals(ref[i], one[i]);
-    }
-
-    let callback = function(ints, strings) {
-        JSUnit.assertEquals(2, arguments.length);
-
-        arrayEqual([-1, 0, 1, 2], ints);
-        arrayEqual(["one", "two", "three"], strings);
-
-        return 7;
-    };
-    JSUnit.assertEquals(Everything.test_array_callback(callback), 14);
-    JSUnit.assertRaises(function () { Everything.test_array_callback(null) });
-}
-
-function testCallbackTransferFull() {
-    let callback = function() {
-                       let obj = Everything.TestObj.new_from_file("/enoent");
-                       return obj;
-                   };
-
-    Everything.test_callback_return_full(callback);
-}
-
-function testCallbackDestroyNotify() {
-    let testObj = {
-        called: 0,
-        test: function(data) {
-            this.called++;
-            return data;
-        }
-    };
-    JSUnit.assertEquals('CallbackDestroyNotify',
-                 Everything.test_callback_destroy_notify(Lang.bind(testObj,
-                     function() {
-                         return testObj.test(42);
-                     })), 42);
-    JSUnit.assertEquals('CallbackDestroyNotify', testObj.called, 1);
-    JSUnit.assertEquals('CallbackDestroyNotify', Everything.test_callback_thaw_notifications(), 42);
-}
-
-function testCallbackAsync() {
-    let test = function() {
-                   return 44;
-               };
-    Everything.test_callback_async(test);
-    let i = Everything.test_callback_thaw_async();
-    JSUnit.assertEquals('testCallbackAsyncFinish', 44, i);
-}
-
-function testIntValueArg() {
-    let i = Everything.test_int_value_arg(42);
-    JSUnit.assertEquals('Method taking a GValue', 42, i);
-}
-
-function testValueReturn() {
-    let i = Everything.test_value_return(42);
-    JSUnit.assertEquals('Method returning a GValue', 42, i);
-}
-
-/* GList types */
-function testGListOut() {
-    JSUnit.assertEquals("1,2,3", Everything.test_glist_nothing_return().join(','));
-    JSUnit.assertEquals("1,2,3", Everything.test_glist_nothing_return2().join(','));
-    JSUnit.assertEquals("1,2,3", Everything.test_glist_container_return().join(','));
-    JSUnit.assertEquals("1,2,3", Everything.test_glist_everything_return().join(','));
-}
-function testGListIn() {
-    const STR_LIST = ["1", "2", "3" ];
-    Everything.test_glist_nothing_in(STR_LIST);
-    Everything.test_glist_nothing_in2(STR_LIST);
-    //Everything.test_glist_container_in(STR_LIST);
-}
-
-/* GSList types */
-function testGSListOut() {
-    JSUnit.assertEquals("1,2,3", Everything.test_gslist_nothing_return().join(','));
-    JSUnit.assertEquals("1,2,3", Everything.test_gslist_nothing_return2().join(','));
-    JSUnit.assertEquals("1,2,3", Everything.test_gslist_container_return().join(','));
-    JSUnit.assertEquals("1,2,3", Everything.test_gslist_everything_return().join(','));
-}
-function testGSListIn() {
-    const STR_LIST = ["1", "2", "3" ];
-    Everything.test_gslist_nothing_in(STR_LIST);
-    Everything.test_gslist_nothing_in2(STR_LIST);
-    //Everything.test_gslist_container_in(STR_LIST);
-}
-
-/* Array tests */
-function testArrayIn() {
-    JSUnit.assertEquals(10, Everything.test_array_int_in([1,2,3,4]));
-    JSUnit.assertEquals(10, Everything.test_array_gint8_in([1,2,3,4]));
-    JSUnit.assertEquals(10, Everything.test_array_gint16_in([1,2,3,4]));
-    JSUnit.assertEquals(10, Everything.test_array_gint32_in([1,2,3,4]));
-    JSUnit.assertEquals(10, Everything.test_array_gint64_in([1,2,3,4]));
-
-    // implicit conversions from strings to int arrays
-    JSUnit.assertEquals(10, Everything.test_array_gint8_in("\x01\x02\x03\x04"));
-    JSUnit.assertEquals(10, Everything.test_array_gint16_in("\x01\x02\x03\x04"));
-    JSUnit.assertEquals(2560, Everything.test_array_gint16_in("\u0100\u0200\u0300\u0400"));
-
-    // GType arrays
-    JSUnit.assertEquals('[GSimpleAction,GIcon,GBoxed,]',
-                 Everything.test_array_gtype_in([Gio.SimpleAction, Gio.Icon, GObject.TYPE_BOXED]));
-    JSUnit.assertRaises(function() {
-        Everything.test_array_gtype_in(42);
-    });
-    JSUnit.assertRaises(function() {
-        Everything.test_array_gtype_in([undefined]);
-    });
-    JSUnit.assertRaises(function() {
+    ['glist', 'gslist'].forEach(list => {
+        describe(list + ' types', function () {
+            const STR_LIST = ['1', '2', '3'];
+
+            it('return with transfer-none', function () {
+                expect(Regress['test_' + list + '_nothing_return']()).toEqual(STR_LIST);
+                expect(Regress['test_' + list + '_nothing_return2']()).toEqual(STR_LIST);
+            });
+
+            it('return with transfer-container', function () {
+                expect(Regress['test_' + list + '_container_return']()).toEqual(STR_LIST);
+            });
+
+            it('return with transfer-full', function () {
+                expect(Regress['test_' + list + '_everything_return']()).toEqual(STR_LIST);
+            });
+
+            it('in with transfer-none', function () {
+                Regress['test_' + list + '_nothing_in'](STR_LIST);
+                Regress['test_' + list + '_nothing_in2'](STR_LIST);
+            });
+
+            xit('in with transfer-container', function () {
+                Regress['test_' + list + '_container_in'](STR_LIST);
+            }).pend('Not sure why this is skipped');
+        });
+    });
+
+    ['int', 'gint8', 'gint16', 'gint32', 'gint64'].forEach(inttype => {
+        it('arrays of ' + inttype + ' in', function () {
+            expect(Regress['test_array_' + inttype + '_in']([1, 2, 3, 4])).toEqual(10);
+        });
+    });
+
+    it('implicit conversions from strings to int arrays', function () {
+        expect(Regress.test_array_gint8_in("\x01\x02\x03\x04")).toEqual(10);
+        expect(Regress.test_array_gint16_in("\x01\x02\x03\x04")).toEqual(10);
+        expect(Regress.test_array_gint16_in("\u0100\u0200\u0300\u0400")).toEqual(2560);
+    });
+
+    it('GType arrays', function () {
+        expect(Regress.test_array_gtype_in([Gio.SimpleAction, Gio.Icon, GObject.TYPE_BOXED]))
+            .toEqual('[GSimpleAction,GIcon,GBoxed,]');
+        expect(() => Regress.test_array_gtype_in(42)).toThrow();
+        expect(() => Regress.test_array_gtype_in([undefined])).toThrow();
         // 80 is G_TYPE_OBJECT, but we don't want it to work
-        Everything.test_array_gtype_in([80]);
-    });
-}
-
-function testArrayOut() {
-    function arrayEqual(ref, res) {
-        JSUnit.assertEquals(ref.length, res.length);
-        for (let i = 0; i < ref.length; i++)
-            JSUnit.assertEquals(ref[i], res[i]);
-    }
-
-    let array = Everything.test_array_int_out();
-    arrayEqual([0, 1, 2, 3, 4], array);
-
-    let array =  Everything.test_array_fixed_size_int_out();
-    JSUnit.assertEquals(0, array[0]);
-    JSUnit.assertEquals(4, array[4]);
-    array =  Everything.test_array_fixed_size_int_return();
-    JSUnit.assertEquals(0, array[0]);
-    JSUnit.assertEquals(4, array[4]);
-
-    array = Everything.test_array_int_none_out();
-    arrayEqual([1, 2, 3, 4, 5], array);
-
-    array = Everything.test_array_int_full_out();
-    arrayEqual([0, 1, 2, 3, 4], array);
-
-    array = Everything.test_array_int_null_out();
-    JSUnit.assertEquals(0, array.length);
-
-    Everything.test_array_int_null_in(null);
-}
-
-function testArrayOfStructsOut() {
-   let array = Everything.test_array_struct_out();
-   let ints = array.map(struct => struct.some_int);
-   JSUnit.assertEquals(22, ints[0]);
-   JSUnit.assertEquals(33, ints[1]);
-   JSUnit.assertEquals(44, ints[2]);
-}
-
-/* GHash type */
-
-// Convert an object to a predictable (not-hash-order-dependent) string
-function objToString(v) {
-    if (typeof(v) == "object") {
-        let keys = [];
-        for (let k in v)
-            keys.push(k);
-        keys.sort();
-        return "{" + keys.map(function(k) {
-            return k + ":" + objToString(v[k]);
-        }) + "}";
-    } else if (typeof(v) == "string") {
-        return '"' + v + '"';
-    } else {
-        return v;
-    }
-}
-
-function testGHashOut() {
-    const HASH_STR = '{baz:"bat",foo:"bar",qux:"quux"}';
-    JSUnit.assertEquals(null, Everything.test_ghash_null_return());
-    JSUnit.assertEquals(HASH_STR, objToString(Everything.test_ghash_nothing_return()));
-    JSUnit.assertEquals(HASH_STR, objToString(Everything.test_ghash_nothing_return2()));
-    JSUnit.assertEquals(HASH_STR, objToString(Everything.test_ghash_container_return()));
-    JSUnit.assertEquals(HASH_STR, objToString(Everything.test_ghash_everything_return()));
-}
-
-function testGHashIn() {
-    const STR_HASH = { foo: 'bar', baz: 'bat', qux: 'quux' };
-    Everything.test_ghash_null_in(null);
-    Everything.test_ghash_nothing_in(STR_HASH);
-    Everything.test_ghash_nothing_in2(STR_HASH);
-}
-
-function testNestedGHashOut() {
-    const HASH_STR = '{wibble:{baz:"bat",foo:"bar",qux:"quux"}}';
-    JSUnit.assertEquals(HASH_STR, objToString(Everything.test_ghash_nested_everything_return()));
-    JSUnit.assertEquals(HASH_STR, objToString(Everything.test_ghash_nested_everything_return2()));
-}
-
-/* Enums */
-function testEnumParam() {
-    let e;
-
-    e = Everything.test_enum_param(Everything.TestEnum.VALUE1);
-    JSUnit.assertEquals('Enum parameter', 'value1', e);
-    e = Everything.test_enum_param(Everything.TestEnum.VALUE3);
-    JSUnit.assertEquals('Enum parameter', 'value3', e);
-
-    e = Everything.test_unsigned_enum_param(Everything.TestEnumUnsigned.VALUE1);
-    JSUnit.assertEquals('Enum parameter', 'value1', e);
-    e = Everything.test_unsigned_enum_param(Everything.TestEnumUnsigned.VALUE2);
-    JSUnit.assertEquals('Enum parameter', 'value2', e);
-
-    JSUnit.assertNotUndefined("Enum $gtype", Everything.TestEnumUnsigned.$gtype);
-    JSUnit.assertTrue("Enum $gtype enumerable", "$gtype" in Everything.TestEnumUnsigned);
-
-    JSUnit.assertEquals(Number(Everything.TestError), Everything.TestError.quark());
-    JSUnit.assertEquals('value4', Everything.TestEnum.param(Everything.TestEnum.VALUE4));
-}
-
-function testSignal() {
-    let handlerCounter = 0;
-    let o = new Everything.TestObj();
-    let theObject = null;
-
-    let handlerId = o.connect('test', function(signalObject) {
-                                          handlerCounter ++;
-                                          theObject = signalObject;
-                                          o.disconnect(handlerId);
-                                      });
-
-    o.emit('test');
-    JSUnit.assertEquals('handler callled', 1, handlerCounter);
-    JSUnit.assertEquals('Signal handlers gets called with right object', o, theObject);
-    o.emit('test');
-    JSUnit.assertEquals('disconnected handler not called', 1, handlerCounter);
-}
-
-function testInvalidSignal() {
-    let o = new Everything.TestObj();
-
-    JSUnit.assertRaises('connect to invalid signal',
-                 function() { o.connect('invalid-signal', function(o) {}); });
-    JSUnit.assertRaises('emit invalid signal',
-                 function() { o.emit('invalid-signal'); });
-}
-
-function testSignalWithStaticScopeArg() {
-    let o = new Everything.TestObj();
-    let b = new Everything.TestSimpleBoxedA({ some_int: 42,
-                                              some_int8: 43,
-                                              some_double: 42.5,
-                                              some_enum: Everything.TestEnum.VALUE3 });
-
-    o.connect('test-with-static-scope-arg', function(signalObject, signalArg) {
-                                                signalArg.some_int = 44;
-                                            });
-
-    o.emit('test-with-static-scope-arg', b);
-    JSUnit.assertEquals('signal handler was passed arg as reference', 44, b.some_int);
-}
-
-function testSignalWithArrayLenParam() {
-    let o = new Everything.TestObj();
-    let array;
-    o.connect('sig-with-array-len-prop', function(signalObj, signalArray, shouldBeUndefined) {
-        array = signalArray;
-        JSUnit.assertUndefined('no extra length arg', shouldBeUndefined);
-    });
-
-    o.emit_sig_with_array_len_prop();
-    JSUnit.assertEquals('handler was passed array with length', array.length, 5);
-    for (let i = 0; i < 5; i++)
-        JSUnit.assertEquals('handler was passed correct array', array[i], i);
-
-    // FIXME not yet implemented:
-    // o.emit('sig-with-array-len-prop', [0, 1, 2, 3, 4]);
-    // JSUnit.assertEquals('handler was passed array with length', array.length, 5);
-    // for (let i = 0; i < 5; i++)
-    //     JSUnit.assertEquals('handler was passed correct array', array[i], i);
-    // o.emit('sig-with-array-len-prop', null);
-    // JSUnit.assertNull('handler was passed null array', array);
-}
-
-function testTortureSignature0() {
-    let [y, z, q] = Everything.test_torture_signature_0(42, 'foo', 7);
-    JSUnit.assertEquals(Math.floor(y), 42);
-    JSUnit.assertEquals(z, 84);
-    JSUnit.assertEquals(q, 10);
-}
-
-function testTortureSignature1Fail() {
-    JSUnit.assertRaises(function () {
-        let [success, y, z, q] = Everything.test_torture_signature_1(42, 'foo', 7);
-    });
-}
-
-function testTortureSignature1Success() {
-    let [success, y, z, q] = Everything.test_torture_signature_1(11, 'barbaz', 8);
-    JSUnit.assertEquals(Math.floor(y), 11);
-    JSUnit.assertEquals(z, 22);
-    JSUnit.assertEquals(q, 14);
-}
-
-function testTortureSignature2() {
-    let [y, z, q] = Everything.test_torture_signature_2(42, function () {
-        return 0;
-    }, 'foo', 7);
-    JSUnit.assertEquals(Math.floor(y), 42);
-    JSUnit.assertEquals(z, 84);
-    JSUnit.assertEquals(q, 10);
-}
-
-function testObjTortureSignature0() {
-    let o = new Everything.TestObj();
-    let [y, z, q] = o.torture_signature_0(42, 'foo', 7);
-    JSUnit.assertEquals(Math.floor(y), 42);
-    JSUnit.assertEquals(z, 84);
-    JSUnit.assertEquals(q, 10);
-}
-
-function testObjTortureSignature1Fail() {
-    let o = new Everything.TestObj();
-    JSUnit.assertRaises(function () {
-        let [success, y, z, q] = o.torture_signature_1(42, 'foo', 7);
-    });
-}
-
-function testObjTortureSignature1Success() {
-    let o = new Everything.TestObj();
-    let [success, y, z, q] = o.torture_signature_1(11, 'barbaz', 8);
-    JSUnit.assertEquals(Math.floor(y), 11);
-    JSUnit.assertEquals(z, 22);
-    JSUnit.assertEquals(q, 14);
-}
-
-function testStrvInGValue() {
-    let v = Everything.test_strv_in_gvalue();
-
-    JSUnit.assertEquals(v.length, 3);
-    JSUnit.assertEquals(v[0], "one");
-    JSUnit.assertEquals(v[1], "two");
-    JSUnit.assertEquals(v[2], "three");
-}
-
-function testVariant() {
-    // Cannot access the variant contents, for now
-    let ivar = Everything.test_gvariant_i();
-    JSUnit.assertEquals('i', ivar.get_type_string());
-    JSUnit.assertTrue(ivar.equal(GLib.Variant.new_int32(1)));
-
-    let svar = Everything.test_gvariant_s();
-    JSUnit.assertEquals('s', String.fromCharCode(svar.classify()));
-    JSUnit.assertEquals('one', svar.get_string()[0]);
-
-    let asvvar = Everything.test_gvariant_asv();
-    JSUnit.assertEquals(2, asvvar.n_children());
-
-    let asvar = Everything.test_gvariant_as();
-    let as = asvar.get_strv();
-    JSUnit.assertEquals('one', as[0]);
-    JSUnit.assertEquals('two', as[1]);
-    JSUnit.assertEquals('three', as[2]);
-    JSUnit.assertEquals(3, as.length);
-}
-
-function testGError() {
-    JSUnit.assertEquals(Gio.io_error_quark(), Number(Gio.IOErrorEnum));
-
-    try {
-        let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist");
-        file.read(null);
-    } catch (x) {
-        JSUnit.assertTrue(x instanceof Gio.IOErrorEnum);
-        JSUnit.assertTrue(x.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND));
-        JSUnit.assertTrue(x.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND));
-
-        JSUnit.assertEquals(Gio.io_error_quark(), x.domain);
-        JSUnit.assertEquals(Gio.IOErrorEnum.NOT_FOUND, x.code);
-    }
-
-    Everything.test_gerror_callback(function(e) {
-        JSUnit.assertTrue(e instanceof Gio.IOErrorEnum);
-        JSUnit.assertEquals(Gio.io_error_quark(), e.domain);
-        JSUnit.assertEquals(Gio.IOErrorEnum.NOT_SUPPORTED, e.code);
-        JSUnit.assertEquals('regression test error', e.message);
-    });
-    Everything.test_owned_gerror_callback(function(e) {
-        JSUnit.assertTrue(e instanceof Gio.IOErrorEnum);
-        JSUnit.assertEquals(Gio.io_error_quark(), e.domain);
-        JSUnit.assertEquals(Gio.IOErrorEnum.PERMISSION_DENIED, e.code);
-        JSUnit.assertEquals('regression test owned error', e.message);
+        expect(() => Regress.test_array_gtype_in([80])).toThrow();
     });
 
-    // Calling matches() on an unpaired error used to JSUnit.assert:
-    // https://bugzilla.gnome.org/show_bug.cgi?id=689482
-    try {
-        WarnLib.throw_unpaired();
-        JSUnit.assertTrue(false);
-    } catch (e) {
-        JSUnit.assertFalse(e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND));
-    }
-}
+    it('out arrays of integers', function () {
+        expect(Regress.test_array_int_out()).toEqual([0, 1, 2, 3, 4]);
+
+        let array = Regress.test_array_fixed_size_int_out();
+        expect(array[0]).toEqual(0);
+        expect(array[4]).toEqual(4);
+        let array = Regress.test_array_fixed_size_int_return();
+        expect(array[0]).toEqual(0);
+        expect(array[4]).toEqual(4);
+
+        expect(Regress.test_array_int_none_out()).toEqual([1, 2, 3, 4, 5]);
 
-function testWrongClassGObject() {
-    /* Function calls */
-    // Everything.func_obj_null_in expects a Everything.TestObj
-    JSUnit.assertRaises(function() {
-        Everything.func_obj_null_in(new Gio.SimpleAction);
+        expect(Regress.test_array_int_full_out()).toEqual([0, 1, 2, 3, 4]);
+
+        expect(Regress.test_array_int_null_out()).toEqual([]);
+    });
+
+    it('null in-array', function () {
+        Regress.test_array_int_null_in(null);
     });
-    JSUnit.assertRaises(function() {
-        Everything.func_obj_null_in(new GLib.KeyFile);
+
+    it('out arrays of structs', function () {
+        let array = Regress.test_array_struct_out();
+        let ints = array.map(struct => struct.some_int);
+        expect(ints).toEqual([22, 33, 44]);
+    });
+
+    describe('GHash type', function () {;
+        const EXPECTED_HASH = { baz: 'bat', foo: 'bar', qux: 'quux' };
+
+        it('null GHash in', function () {
+            Regress.test_ghash_null_in(null);
+        });
+
+        it('null GHash out', function () {
+            expect(Regress.test_ghash_null_return()).toBeNull();
+        });
+
+        it('out GHash', function () {
+            expect(Regress.test_ghash_nothing_return()).toEqual(EXPECTED_HASH);
+            expect(Regress.test_ghash_nothing_return2()).toEqual(EXPECTED_HASH);
+            expect(Regress.test_ghash_container_return()).toEqual(EXPECTED_HASH);
+            expect(Regress.test_ghash_everything_return()).toEqual(EXPECTED_HASH);
+        });
+
+        it('in GHash', function () {
+            Regress.test_ghash_nothing_in(EXPECTED_HASH);
+            Regress.test_ghash_nothing_in2(EXPECTED_HASH);
+        });
+
+        it('nested GHash', function () {
+            const EXPECTED_NESTED_HASH = { wibble: EXPECTED_HASH };
+
+            expect(Regress.test_ghash_nested_everything_return())
+                .toEqual(EXPECTED_NESTED_HASH);
+            expect(Regress.test_ghash_nested_everything_return2())
+                .toEqual(EXPECTED_NESTED_HASH);
+        });
     });
-    JSUnit.assertRaises(function() {
-        Everything.func_obj_null_in(Gio.File.new_for_path('/'));
+
+    it('enum parameter', function () {
+        expect(Regress.test_enum_param(Regress.TestEnum.VALUE1)).toEqual('value1');
+        expect(Regress.test_enum_param(Regress.TestEnum.VALUE3)).toEqual('value3');
     });
-    Everything.func_obj_null_in(new Everything.TestSubObj);
 
-    /* Method calls */
-    JSUnit.assertRaises(function() {
-        Everything.TestObj.prototype.instance_method.call(new Gio.SimpleAction);
+    it('unsigned enum parameter', function () {
+        expect(Regress.test_unsigned_enum_param(Regress.TestEnumUnsigned.VALUE1))
+            .toEqual('value1');
+        expect(Regress.test_unsigned_enum_param(Regress.TestEnumUnsigned.VALUE2))
+            .toEqual('value2');
     });
-    JSUnit.assertRaises(function() {
-        Everything.TestObj.prototype.instance_method.call(new GLib.KeyFile);
+
+    it('enum has a $gtype property', function () {
+        expect(Regress.TestEnumUnsigned.$gtype).toBeDefined();
     });
-    Everything.TestObj.prototype.instance_method.call(new Everything.TestSubObj);
-}
 
-function testWrongClassGBoxed() {
-    let simpleBoxed = new Everything.TestSimpleBoxedA;
-    // simpleBoxed.equals expects a Everything.TestSimpleBoxedA
-    JSUnit.assertRaises(function() {
-        simpleBoxed.equals(new Gio.SimpleAction);
+    it('enum $gtype property is enumerable', function () {
+        expect('$gtype' in Regress.TestEnumUnsigned).toBeTruthy();
     });
-    JSUnit.assertRaises(function() {
-        simpleBoxed.equals(new Everything.TestObj);
+
+    it('Number converts error to quark', function () {
+        expect(Regress.TestError.quark()).toEqual(Number(Regress.TestError));
     });
-    JSUnit.assertRaises(function() {
-        simpleBoxed.equals(new GLib.KeyFile);
+
+    it('converts enum to string', function () {
+        expect(Regress.TestEnum.param(Regress.TestEnum.VALUE4)).toEqual('value4');
     });
-    JSUnit.assertTrue(simpleBoxed.equals(simpleBoxed));
 
-    JSUnit.assertRaises(function() {
-        Everything.TestSimpleBoxedA.prototype.copy.call(new Gio.SimpleAction);
+    describe('Signal connection', function () {
+        let o;
+        beforeEach(function () {
+            o = new Regress.TestObj();
+        });
+
+        it('calls correct handlers with correct arguments', function () {
+            let handler = jasmine.createSpy('handler');
+            let handlerId = o.connect('test', handler);
+            handler.and.callFake(() => o.disconnect(handlerId));
+
+            o.emit('test');
+            expect(handler).toHaveBeenCalledTimes(1);
+            expect(handler).toHaveBeenCalledWith(o);
+
+            handler.calls.reset();
+            o.emit('test');
+            expect(handler).not.toHaveBeenCalled();
+        });
+
+        it('throws errors for invalid signals', function () {
+            expect(() => o.connect('invalid-signal', o => {})).toThrow();
+            expect(() => o.emit('invalid-signal')).toThrow();
+        });
+
+        it('signal handler with static scope arg gets arg passed by reference', function () {
+            let b = new Regress.TestSimpleBoxedA({
+                some_int: 42,
+                some_int8: 43,
+                some_double: 42.5,
+                some_enum: Regress.TestEnum.VALUE3,
+            });
+            o.connect('test-with-static-scope-arg', (signalObject, signalArg) => {
+                signalArg.some_int = 44;
+            });
+            o.emit('test-with-static-scope-arg', b);
+            expect(b.some_int).toEqual(44);
+        });
+
+        it('signal with array len parameter is not passed correct array and no length arg', function (done) {
+            o.connect('sig-with-array-len-prop', (signalObj, signalArray, shouldBeUndefined) => {
+                expect(shouldBeUndefined).not.toBeDefined();
+                expect(signalArray).toEqual([0, 1, 2, 3, 4]);
+                done();
+            });
+            o.emit_sig_with_array_len_prop();
+        });
+
+        xit('can pass parameter to signal with array len parameter via emit', function () {
+            o.connect('sig-with-array-len-prop', (signalObj, signalArray) => {
+                expect(signalArray).toEqual([0, 1, 2, 3, 4]);
+                done();
+            });
+            o.emit('sig-with-array-len-prop', [0, 1, 2, 3, 4]);
+        }).pend('Not yet implemented');
+
+        xit('can pass null to signal with array len parameter', function () {
+            let handler = jasmine.createSpy('handler');
+            o.connect('sig-with-array-len-prop', handler);
+            o.emit('sig-with-array-len-prop', null);
+            expect(handler).toHaveBeenCalledWith([jasmine.any(Object), null]);
+        }).pend('Not yet implemented');
     });
-    JSUnit.assertRaises(function() {
-        Everything.TestSimpleBoxedA.prototype.copy.call(new GLib.KeyFile);
+
+    it('torture signature 0', function () {
+        let [y, z, q] = Regress.test_torture_signature_0(42, 'foo', 7);
+        expect(Math.floor(y)).toEqual(42);
+        expect(z).toEqual(84);
+        expect(q).toEqual(10);
     });
-    Everything.TestSimpleBoxedA.prototype.copy.call(simpleBoxed);
-}
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('torture signature 1 fail', function () {
+        expect(() => Regress.test_torture_signature_1(42, 'foo', 7)).toThrow();
+    });
+
+    it('torture signature 1 success', function () {
+        let [, y, z, q] = Regress.test_torture_signature_1(11, 'barbaz', 8);
+        expect(Math.floor(y)).toEqual(11);
+        expect(z).toEqual(22);
+        expect(q).toEqual(14);
+    });
+
+    it('torture signature 2', function () {
+        let [y, z, q] = Regress.test_torture_signature_2(42, () => 0, 'foo', 7);
+        expect(Math.floor(y)).toEqual(42);
+        expect(z).toEqual(84);
+        expect(q).toEqual(10);
+    });
+
+    describe('Object torture signature', function () {
+        let o;
+        beforeEach(function () {
+            o = new Regress.TestObj();
+        });
+
+        it('0', function () {
+            let [y, z, q] = o.torture_signature_0(42, 'foo', 7);
+            expect(Math.floor(y)).toEqual(42);
+            expect(z).toEqual(84);
+            expect(q).toEqual(10);
+        });
+
+        it('1 fail', function () {
+            expect(() => o.torture_signature_1(42, 'foo', 7)).toThrow();
+        });
+
+        it('1 success', function () {
+            let [, y, z, q] = o.torture_signature_1(11, 'barbaz', 8);
+            expect(Math.floor(y)).toEqual(11);
+            expect(z).toEqual(22);
+            expect(q).toEqual(14);
+        });
+    });
+
+    it('strv in GValue', function () {
+        expect(Regress.test_strv_in_gvalue()).toEqual(['one', 'two', 'three']);
+    });
+
+    // Cannot access the variant contents, for now
+    it('integer GVariant', function () {
+        let ivar = Regress.test_gvariant_i();
+        expect(ivar.get_type_string()).toEqual('i');
+        expect(ivar.equal(GLib.Variant.new_int32(1))).toBeTruthy();
+    });
 
+    it('string GVariant', function () {
+        let svar = Regress.test_gvariant_s();
+        expect(String.fromCharCode(svar.classify())).toEqual('s');
+        expect(svar.get_string()[0]).toEqual('one');
+    });
+
+    it('a{sv} GVariant', function () {
+        let asvvar = Regress.test_gvariant_asv();
+        expect(asvvar.n_children()).toEqual(2);
+    });
+
+    it('as Variant', function () {
+        let asvar = Regress.test_gvariant_as();
+        expect(asvar.get_strv()).toEqual(['one', 'two', 'three']);
+    });
+
+    it('error enum names match error quarks', function () {
+        expect(Number(Gio.IOErrorEnum)).toEqual(Gio.io_error_quark());
+    });
+
+    describe('thrown GError', function () {
+        let err;
+        beforeEach(function () {
+            try {
+                let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist");
+                file.read(null);
+            } catch (x) {
+                err = x;
+            }
+        });
+
+        it('is an instance of error enum type', function () {
+            expect(err instanceof Gio.IOErrorEnum).toBeTruthy();
+        });
+
+        it('matches error domain and code', function () {
+            expect(err.matches(Gio.io_error_quark(), Gio.IOErrorEnum.NOT_FOUND))
+                .toBeTruthy();
+        });
+
+        it('has properties for domain and code', function () {
+            expect(err.domain).toEqual(Gio.io_error_quark());
+            expect(err.code).toEqual(Gio.IOErrorEnum.NOT_FOUND);
+        });
+    });
+
+    it('GError callback', function (done) {
+        Regress.test_gerror_callback(e => {
+            expect(e instanceof Gio.IOErrorEnum).toBeTruthy();
+            expect(e.domain).toEqual(Gio.io_error_quark());
+            expect(e.code).toEqual(Gio.IOErrorEnum.NOT_SUPPORTED);
+            done();
+        });
+    });
+
+    it('owned GError callback', function (done) {
+        Regress.test_owned_gerror_callback(e => {
+            expect(e instanceof Gio.IOErrorEnum).toBeTruthy();
+            expect(e.domain).toEqual(Gio.io_error_quark());
+            expect(e.code).toEqual(Gio.IOErrorEnum.PERMISSION_DENIED);
+            done();
+        });
+    });
+
+    // Calling matches() on an unpaired error used to JSUnit.assert:
+    // https://bugzilla.gnome.org/show_bug.cgi?id=689482
+    it('bug 689482', function () {
+        try {
+            WarnLib.throw_unpaired();
+            fail();
+        } catch (e) {
+            expect(e.matches(Gio.IOErrorEnum, Gio.IOErrorEnum.NOT_FOUND)).toBeFalsy();
+        }
+    });
+
+    describe('wrong type for GObject', function () {
+        let wrongObject, wrongBoxed, subclassObject;
+        beforeEach(function () {
+            wrongObject = new Gio.SimpleAction();
+            wrongBoxed = new GLib.KeyFile();
+            subclassObject = new Regress.TestSubObj();
+        });
+
+        // Everything.func_obj_null_in expects a Everything.TestObj
+        it('function does not accept a GObject of the wrong type', function () {
+            expect(() => Regress.func_obj_null_in(wrongObject)).toThrow();
+        });
+
+        it('function does not accept a GBoxed instead of GObject', function () {
+            expect(() => Regress.func_obj_null_in(wrongBoxed)).toThrow();
+        });
+
+        it('function does not accept returned GObject of the wrong type', function () {
+            let wrongReturnedObject = Gio.File.new_for_path('/');
+            expect(() => Regress.func_obj_null_in(wrongReturnedObject)).toThrow();
+        });
+
+        it('function accepts GObject of subclass of expected type', function () {
+            expect(() => Regress.func_obj_null_in(subclassObject)).not.toThrow();
+        });
+
+        it('method cannot be called on a GObject of the wrong type', function () {
+            expect(() => Regress.TestObj.prototype.instance_method.call(wrongObject))
+                .toThrow();
+        });
+
+        it('method cannot be called on a GBoxed', function () {
+            expect(() => Regress.TestObj.prototype.instance_method.call(wrongBoxed))
+                .toThrow();
+        });
+
+        it('method can be called on a GObject of subclass of expected type', function () {
+            expect(() => Regress.TestObj.prototype.instance_method.call(subclassObject))
+                .not.toThrow();
+        });
+    });
+
+    describe('wrong type for GBoxed', function () {
+        let simpleBoxed, wrongObject, wrongBoxed;
+        beforeEach(function () {
+            simpleBoxed = new Regress.TestSimpleBoxedA();
+            wrongObject = new Gio.SimpleAction();
+            wrongBoxed = new GLib.KeyFile();
+        });
+
+        // simpleBoxed.equals expects a Everything.TestSimpleBoxedA
+        it('function does not accept a GObject of the wrong type', function () {
+            expect(() => simpleBoxed.equals(wrongObject)).toThrow();
+        });
+
+        it('function does not accept a GBoxed of the wrong type', function () {
+            expect(() => simpleBoxed.equals(wrongBoxed)).toThrow();
+        });
+
+        it('function does accept a GBoxed of the correct type', function () {
+            expect(simpleBoxed.equals(simpleBoxed)).toBeTruthy();
+        });
+
+        it('method cannot be called on a GObject', function () {
+            expect(() => Regress.TestSimpleBoxedA.prototype.copy.call(wrongObject))
+                .toThrow();
+        });
+
+        it('method cannot be called on a GBoxed of the wrong type', function () {
+            expect(() => Regress.TestSimpleBoxedA.protoype.copy.call(wrongBoxed))
+                .toThrow();
+        });
+
+        it('method can be called on correct GBoxed type', function () {
+            expect(() => Regress.TestSimpleBoxedA.prototype.copy.call(simpleBoxed))
+                .not.toThrow();
+        });
+    });
+});
diff --git a/installed-tests/js/testEverythingEncapsulated.js 
b/installed-tests/js/testEverythingEncapsulated.js
index 3b7f5be..ee09b79 100644
--- a/installed-tests/js/testEverythingEncapsulated.js
+++ b/installed-tests/js/testEverythingEncapsulated.js
@@ -1,164 +1,191 @@
-// This used to be called "Everything"
-const JSUnit = imports.jsUnit;
-const Everything = imports.gi.Regress;
-
-function testStruct() {
-    let struct = new Everything.TestStructA();
-    struct.some_int = 42;
-    struct.some_int8 = 43;
-    struct.some_double = 42.5;
-    struct.some_enum = Everything.TestEnum.VALUE3;
-    JSUnit.assertEquals(42, struct.some_int);
-    JSUnit.assertEquals(43, struct.some_int8);
-    JSUnit.assertEquals(42.5, struct.some_double);
-    JSUnit.assertEquals(Everything.TestEnum.VALUE3, struct.some_enum);
-    let b = struct.clone();
-    JSUnit.assertEquals(42, b.some_int);
-    JSUnit.assertEquals(43, b.some_int8);
-    JSUnit.assertEquals(42.5, b.some_double);
-    JSUnit.assertEquals(Everything.TestEnum.VALUE3, b.some_enum);
-
-    struct = new Everything.TestStructB();
-    struct.some_int8 = 43;
-    struct.nested_a.some_int8 = 66;
-    JSUnit.assertEquals(43, struct.some_int8);
-    JSUnit.assertEquals(66, struct.nested_a.some_int8);
-    b = struct.clone();
-    JSUnit.assertEquals(43, b.some_int8);
-    JSUnit.assertEquals(66, struct.nested_a.some_int8);
-}
-
-function testStructConstructor()
-{
-    // "Copy" an object from a hash of field values
-    let struct = new Everything.TestStructA({ some_int: 42,
-                                              some_int8: 43,
-                                              some_double: 42.5,
-                                              some_enum: Everything.TestEnum.VALUE3 });
-
-    JSUnit.assertEquals(42, struct.some_int);
-    JSUnit.assertEquals(43, struct.some_int8);
-    JSUnit.assertEquals(42.5, struct.some_double);
-    JSUnit.assertEquals(Everything.TestEnum.VALUE3, struct.some_enum);
-
-    // Make sure we catch bad field names
-    JSUnit.assertRaises(function() {
-        let t = new Everything.TestStructA({ junk: 42 });
+const Regress = imports.gi.Regress;
+
+describe('Introspected structs', function () {
+    let struct;
+
+    describe('simple', function () {
+        beforeEach(function () {
+            struct = new Regress.TestStructA();
+            struct.some_int = 42;
+            struct.some_int8 = 43;
+            struct.some_double = 42.5;
+            struct.some_enum = Regress.TestEnum.VALUE3;
+        });
+
+        it('sets fields correctly', function () {
+            expect(struct.some_int).toEqual(42);
+            expect(struct.some_int8).toEqual(43);
+            expect(struct.some_double).toEqual(42.5);
+            expect(struct.some_enum).toEqual(Regress.TestEnum.VALUE3);
+        });
+
+        it('can clone', function () {
+            let b = struct.clone();
+            expect(b.some_int).toEqual(42);
+            expect(b.some_int8).toEqual(43);
+            expect(b.some_double).toEqual(42.5);
+            expect(b.some_enum).toEqual(Regress.TestEnum.VALUE3);
+        });
     });
 
-    // Copy an object from another object of the same type, shortcuts to memcpy()
-    let copy = new Everything.TestStructA(struct);
-
-    JSUnit.assertEquals(42, copy.some_int);
-    JSUnit.assertEquals(43, copy.some_int8);
-    JSUnit.assertEquals(42.5, copy.some_double);
-    JSUnit.assertEquals(Everything.TestEnum.VALUE3, copy.some_enum);
-}
-
-function testSimpleBoxed() {
-    let simple_boxed = new Everything.TestSimpleBoxedA();
-    simple_boxed.some_int = 42;
-    simple_boxed.some_int8 = 43;
-    simple_boxed.some_double = 42.5;
-    simple_boxed.some_enum = Everything.TestEnum.VALUE3;
-    JSUnit.assertEquals(42, simple_boxed.some_int);
-    JSUnit.assertEquals(43, simple_boxed.some_int8);
-    JSUnit.assertEquals(42.5, simple_boxed.some_double);
-    JSUnit.assertEquals(Everything.TestEnum.VALUE3, simple_boxed.some_enum);
-}
-
-function testBoxedCopyConstructor()
-{
-    // "Copy" an object from a hash of field values
-    let simple_boxed = new Everything.TestSimpleBoxedA({ some_int: 42,
-                                                         some_int8: 43,
-                                                         some_double: 42.5,
-                                                         some_enum: Everything.TestEnum.VALUE3 });
-
-    JSUnit.assertEquals(42, simple_boxed.some_int);
-    JSUnit.assertEquals(43, simple_boxed.some_int8);
-    JSUnit.assertEquals(42.5, simple_boxed.some_double);
-    JSUnit.assertEquals(Everything.TestEnum.VALUE3, simple_boxed.some_enum);
-
-    // Make sure we catch bad field names
-    JSUnit.assertRaises(function() {
-        let t = new Everything.TestSimpleBoxedA({ junk: 42 });
+    describe('nested', function () {
+        beforeEach(function () {
+            struct = new Regress.TestStructB();
+            struct.some_int8 = 43;
+            struct.nested_a.some_int8 = 66;
+        });
+
+        it('sets fields correctly', function () {
+            expect(struct.some_int8).toEqual(43);
+            expect(struct.nested_a.some_int8).toEqual(66);
+        });
+
+        it('can clone', function () {
+            let b = struct.clone();
+            expect(b.some_int8).toEqual(43);
+            expect(b.nested_a.some_int8).toEqual(66);
+        });
     });
 
-    // Copy an object from another object of the same type, shortcuts to the boxed copy
-    let copy = new Everything.TestSimpleBoxedA(simple_boxed);
-
-    JSUnit.assertTrue(copy instanceof Everything.TestSimpleBoxedA);
-    JSUnit.assertEquals(42, copy.some_int);
-    JSUnit.assertEquals(43, copy.some_int8);
-    JSUnit.assertEquals(42.5, copy.some_double);
-    JSUnit.assertEquals(Everything.TestEnum.VALUE3, copy.some_enum);
- }
-
-function testNestedSimpleBoxed() {
-    let simple_boxed = new Everything.TestSimpleBoxedB();
-
-    // Test reading fields and nested fields
-    simple_boxed.some_int8 = 42;
-    simple_boxed.nested_a.some_int = 43;
-    JSUnit.assertEquals(42, simple_boxed.some_int8);
-    JSUnit.assertEquals(43, simple_boxed.nested_a.some_int);
-
-    // Try assigning the nested struct field from an instance
-    simple_boxed.nested_a = new Everything.TestSimpleBoxedA({ some_int: 53 });
-    JSUnit.assertEquals(53, simple_boxed.nested_a.some_int);
-
-    // And directly from a hash of field values
-    simple_boxed.nested_a = { some_int: 63 };
-    JSUnit.assertEquals(63, simple_boxed.nested_a.some_int);
-
-    // Try constructing with a nested hash of field values
-    let simple2 = new Everything.TestSimpleBoxedB({
-        some_int8: 42,
-        nested_a: {
-            some_int: 43,
-            some_int8: 44,
-            some_double: 43.5
-        }
+    describe('constructors', function () {
+        beforeEach(function () {
+            struct = new Regress.TestStructA({
+                some_int: 42,
+                some_int8: 43,
+                some_double: 42.5,
+                some_enum: Regress.TestEnum.VALUE3,
+            });
+        });
+
+        it('"copies" an object from a hash of field values', function () {
+            expect(struct.some_int).toEqual(42);
+            expect(struct.some_int8).toEqual(43);
+            expect(struct.some_double).toEqual(42.5);
+            expect(struct.some_enum).toEqual(Regress.TestEnum.VALUE3);
+        });
+
+        it('catches bad field names', function () {
+            expect(() => new Regress.TestStructA({ junk: 42 })).toThrow();
+        });
+
+        it('copies an object from another object of the same type', function () {
+            let copy = new Regress.TestStructA(struct);
+            expect(copy.some_int).toEqual(42);
+            expect(copy.some_int8).toEqual(43);
+            expect(copy.some_double).toEqual(42.5);
+            expect(copy.some_enum).toEqual(Regress.TestEnum.VALUE3);
+        });
     });
-    JSUnit.assertEquals(42, simple2.some_int8);
-    JSUnit.assertEquals(43, simple2.nested_a.some_int);
-    JSUnit.assertEquals(44, simple2.nested_a.some_int8);
-    JSUnit.assertEquals(43.5, simple2.nested_a.some_double);
-}
-
-function testBoxed() {
-    let boxed = new Everything.TestBoxed();
-    boxed.some_int8 = 42;
-    JSUnit.assertEquals(42, boxed.some_int8);
-}
-
-function testTestStructFixedArray() {
-    let struct = new Everything.TestStructFixedArray();
-    struct.frob();
-    JSUnit.assertEquals(7, struct.just_int);
-    JSUnit.assertEquals(42, struct.array[0]);
-    JSUnit.assertEquals(43, struct.array[1]);
-    JSUnit.assertEquals(51, struct.array[9]);
-}
-
-function testComplexConstructor() {
-    let boxed = new Everything.TestBoxedD('abcd', 8);
-
-    JSUnit.assertEquals(12, boxed.get_magic());
-}
-
-function testComplexConstructorBackwardCompatibility() {
+
+    it('containing fixed array', function () {
+        let struct = new Regress.TestStructFixedArray();
+        struct.frob();
+        expect(struct.just_int).toEqual(7);
+        expect(struct.array).toEqual([42, 43, 44, 45, 46, 47, 48, 49, 50, 51]);
+    });
+});
+
+describe('Introspected boxed types', function () {
+    let simple_boxed;
+
+    it('sets fields correctly', function () {
+        simple_boxed = new Regress.TestSimpleBoxedA();
+        simple_boxed.some_int = 42;
+        simple_boxed.some_int8 = 43;
+        simple_boxed.some_double = 42.5;
+        simple_boxed.some_enum = Regress.TestEnum.VALUE3;
+        expect(simple_boxed.some_int).toEqual(42);
+        expect(simple_boxed.some_int8).toEqual(43);
+        expect(simple_boxed.some_double).toEqual(42.5);
+        expect(simple_boxed.some_enum).toEqual(Regress.TestEnum.VALUE3);
+
+        let boxed = new Regress.TestBoxed();
+        boxed.some_int8 = 42;
+        expect(boxed.some_int8).toEqual(42);
+    });
+
+    describe('copy constructors', function () {
+        beforeEach(function () {
+            simple_boxed = new Regress.TestSimpleBoxedA({
+                some_int: 42,
+                some_int8: 43,
+                some_double: 42.5,
+                some_enum: Regress.TestEnum.VALUE3,
+            });
+        });
+
+        it('"copies" an object from a hash of field values', function () {
+            expect(simple_boxed.some_int).toEqual(42);
+            expect(simple_boxed.some_int8).toEqual(43);
+            expect(simple_boxed.some_double).toEqual(42.5);
+            expect(simple_boxed.some_enum).toEqual(Regress.TestEnum.VALUE3);
+        });
+
+        it('catches bad field names', function () {
+            expect(() => new Regress.TestSimpleBoxedA({ junk: 42 })).toThrow();
+        });
+
+        it('copies an object from another object of the same type', function () {
+            let copy = new Regress.TestSimpleBoxedA(simple_boxed);
+            expect(copy instanceof Regress.TestSimpleBoxedA).toBeTruthy();
+            expect(copy.some_int).toEqual(42);
+            expect(copy.some_int8).toEqual(43);
+            expect(copy.some_double).toEqual(42.5);
+            expect(copy.some_enum).toEqual(Regress.TestEnum.VALUE3);
+        });
+    });
+
+    describe('nested', function () {
+        beforeEach(function () {
+            simple_boxed = new Regress.TestSimpleBoxedB();
+        });
+
+        it('reads fields and nested fields', function () {
+            simple_boxed.some_int8 = 42;
+            simple_boxed.nested_a.some_int = 43;
+            expect(simple_boxed.some_int8).toEqual(42);
+            expect(simple_boxed.nested_a.some_int).toEqual(43);
+        });
+
+        it('assigns nested struct field from an instance', function () {
+            simple_boxed.nested_a = new Regress.TestSimpleBoxedA({ some_int: 53 });
+            expect(simple_boxed.nested_a.some_int).toEqual(53);
+        });
+
+        it('assigns nested struct field directly from a hash of field values', function () {
+            simple_boxed.nested_a = { some_int: 63 };
+            expect(simple_boxed.nested_a.some_int).toEqual(63);
+        });
+    });
+
+    it('constructs with a nested hash of field values', function () {
+        let simple2 = new Regress.TestSimpleBoxedB({
+            some_int8: 42,
+            nested_a: {
+                some_int: 43,
+                some_int8: 44,
+                some_double: 43.5
+            }
+        });
+        expect(simple2.some_int8).toEqual(42);
+        expect(simple2.nested_a.some_int).toEqual(43);
+        expect(simple2.nested_a.some_int8).toEqual(44);
+        expect(simple2.nested_a.some_double).toEqual(43.5);
+    });
+
+    it('constructs using a custom constructor', function () {
+        let boxed = new Regress.TestBoxedD('abcd', 8);
+        expect(boxed.get_magic()).toEqual(12);
+    });
+
     // RegressTestBoxedB has a constructor that takes multiple
     // arguments, but since it is directly allocatable, we keep
     // the old style of passing an hash of fields.
     // The two real world structs that have this behavior are
     // Clutter.Color and Clutter.ActorBox.
-    let boxed = new Everything.TestBoxedB({ some_int8: 7, some_long: 5 });
-
-    JSUnit.assertEquals(7, boxed.some_int8);
-    JSUnit.assertEquals(5, boxed.some_long);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
-
+    it('constructs using a custom constructor in backwards compatibility mode', function () {
+        let boxed = new Regress.TestBoxedB({ some_int8: 7, some_long: 5 });
+        expect(boxed.some_int8).toEqual(7);
+        expect(boxed.some_long).toEqual(5);
+    });
+});
diff --git a/installed-tests/js/testExceptions.js b/installed-tests/js/testExceptions.js
index cf16ad8..d5fcd4b 100644
--- a/installed-tests/js/testExceptions.js
+++ b/installed-tests/js/testExceptions.js
@@ -1,4 +1,3 @@
-const JSUnit = imports.jsUnit;
 const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
@@ -29,157 +28,147 @@ const Bar = new Lang.Class({
     }
 });
 
-function testExceptionInPropertySetter() {
-    let foo = new Foo();
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-                             'JS ERROR: Error: set*');
+describe('Exceptions', function () {
+    it('are thrown from property setter', function () {
+        let foo = new Foo();
+        expect(() => foo.prop = 'bar').toThrowError(/set/);
+    });
 
-    try {
-       foo.prop = 'bar';
-    } catch (e) {
-       logError(e);
-    }
+    it('are thrown from property getter', function () {
+        let foo = new Foo();
+        expect(() => foo.prop).toThrowError(/get/);
+    });
 
-    GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
-                                                'testExceptionInPropertySetter');
-}
+    // FIXME: In the next cases the errors aren't thrown but logged
 
-function testExceptionInPropertyGetter() {
-    let foo = new Foo();
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-                             'JS ERROR: Error: get*');
+    it('are logged from constructor', function () {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: Error: set*');
 
-    try {
-       let bar = foo.prop;
-    } catch (e) {
-       logError(e);
-    }
+        new Foo({ prop: 'bar' });
 
-    GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
-                                                'testExceptionInPropertyGetter');
-}
+        GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
+            'testExceptionInPropertySetterFromConstructor');
+    });
 
-function testExceptionInPropertySetterFromConstructor() {
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-                             'JS ERROR: Error: set*');
+    it('are logged from property setter with binding', function () {
+        let foo = new Foo();
+        let bar = new Bar();
 
-    try {
-       let foo = new Foo({ prop: 'bar' });
-    } catch (e) {
-       logError(e);
-    }
+        bar.bind_property('prop',
+            foo, 'prop',
+            GObject.BindingFlags.DEFAULT);
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: Error: set*');
 
-    GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
-                                                'testExceptionInPropertySetterFromConstructor');
-}
-
-function testExceptionInPropertySetterWithBinding() {
-    let foo = new Foo();
-    let bar = new Bar();
-
-    bar.bind_property('prop',
-                     foo, 'prop',
-                     GObject.BindingFlags.DEFAULT);
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-                             'JS ERROR: Error: set*');
-
-    try {
-       // wake up the binding so that g_object_set() is called on foo
-       bar.notify('prop');
-    } catch (e) {
-       logError(e);
-    }
+        // wake up the binding so that g_object_set() is called on foo
+        bar.notify('prop');
 
-    GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
-                                                'testExceptionInPropertySetterWithBinding');
-}
-
-function testExceptionInPropertyGetterWithBinding() {
-    let foo = new Foo();
-    let bar = new Bar();
-
-    foo.bind_property('prop',
-                     bar, 'prop',
-                     GObject.BindingFlags.DEFAULT);
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-                             'JS ERROR: Error: get*');
-
-    try {
-       // wake up the binding so that g_object_get() is called on foo
-       foo.notify('prop');
-    } catch (e) {
-       logError(e);
-    }
+        GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
+            'testExceptionInPropertySetterWithBinding');
+    });
 
-    GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
-                                                'testExceptionInPropertyGetterWithBinding');
-}
-
-function testGErrorMessages() {
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: Gio.IOErrorEnum: *');
-    try {
-        let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist");
-        file.read(null);
-    } catch(e) {
-        logError(e);
-    }
+    it('are logged from property getter with binding', function () {
+        let foo = new Foo();
+        let bar = new Bar();
 
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: Gio.IOErrorEnum: a message\ntestGErrorMessages@*');
-    try {
-        throw new Gio.IOErrorEnum({ message: 'a message', code: 0 });
-    } catch(e) {
-        logError(e);
-    }
+        foo.bind_property('prop',
+            bar, 'prop',
+            GObject.BindingFlags.DEFAULT);
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: Error: get*');
 
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: Gio.IOErrorEnum: a message\ntestGErrorMessages@*');
-    logError(new Gio.IOErrorEnum({ message: 'a message', code: 0 }));
+        // wake up the binding so that g_object_get() is called on foo
+        foo.notify('prop');
 
-    // No stack for GLib.Error constructor
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: Gio.IOErrorEnum: a message');
-    logError(new GLib.Error(Gio.IOErrorEnum, 0, 'a message'));
+        GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
+            'testExceptionInPropertyGetterWithBinding');
+    });
+});
 
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: GLib.Error my-error: a message');
-    logError(new GLib.Error(GLib.quark_from_string('my-error'), 0, 'a message'));
+describe('logError', function () {
+    afterEach(function () {
+        GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js',
+            0, 'testGErrorMessages');
+    });
+
+    it('logs a warning for a GError', function () {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: Gio.IOErrorEnum: *');
+        try {
+            let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist");
+            file.read(null);
+        } catch(e) {
+            logError(e);
+        }
+    });
+
+    it('logs a warning with a message if given', function marker() {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: Gio.IOErrorEnum: a message\nmarker@*');
+        try {
+            throw new Gio.IOErrorEnum({ message: 'a message', code: 0 });
+        } catch(e) {
+            logError(e);
+        }
+    });
+
+    it('also logs an error for a created GError that is not thrown', function marker() {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: Gio.IOErrorEnum: a message\nmarker@*');
+        logError(new Gio.IOErrorEnum({ message: 'a message', code: 0 }));
+    });
+
+    it('logs an error with no stack trace for an error created with the GLib.Error constructor', function () 
{
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: Gio.IOErrorEnum: a message');
+        logError(new GLib.Error(Gio.IOErrorEnum, 0, 'a message'));
+    });
+
+    it('logs the quark for a JS-created GError type', function () {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: GLib.Error my-error: a message');
+        logError(new GLib.Error(GLib.quark_from_string('my-error'), 0, 'a message'));
+    });
 
     // Now with prefix
 
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: prefix: Gio.IOErrorEnum: *');
-    try {
-        let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist");
-        file.read(null);
-    } catch(e) {
-        logError(e, 'prefix');
-    }
-
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: prefix: Gio.IOErrorEnum: a message\ntestGErrorMessages@*');
-    try {
-        throw new Gio.IOErrorEnum({ message: 'a message', code: 0 });
-    } catch(e) {
-        logError(e, 'prefix');
-    }
-
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: prefix: Gio.IOErrorEnum: a message\ntestGErrorMessages@*');
-    logError(new Gio.IOErrorEnum({ message: 'a message', code: 0 }), 'prefix');
-
-    // No stack for GLib.Error constructor
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: prefix: Gio.IOErrorEnum: a message');
-    logError(new GLib.Error(Gio.IOErrorEnum, 0, 'a message'), 'prefix');
-
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-        'JS ERROR: prefix: GLib.Error my-error: a message');
-    logError(new GLib.Error(GLib.quark_from_string('my-error'), 0, 'a message'), 'prefix');
-
-    GLib.test_assert_expected_messages_internal('Gjs', 'testExceptions.js', 0,
-        'testGErrorMessages');
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('logs an error with a prefix if given', function () {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: prefix: Gio.IOErrorEnum: *');
+        try {
+            let file = Gio.file_new_for_path("\\/,.^!@&$_don't exist");
+            file.read(null);
+        } catch(e) {
+            logError(e, 'prefix');
+        }
+    });
+
+    it('logs an error with prefix and message', function marker() {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: prefix: Gio.IOErrorEnum: a message\nmarker@*');
+        try {
+            throw new Gio.IOErrorEnum({ message: 'a message', code: 0 });
+        } catch(e) {
+            logError(e, 'prefix');
+        }
+    });
+
+    it('logs a non-thrown error with prefix', function marker() {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: prefix: Gio.IOErrorEnum: a message\nmarker@*');
+        logError(new Gio.IOErrorEnum({ message: 'a message', code: 0 }), 'prefix');
+    });
+
+    it('logs a GLib.Error with prefix', function () {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: prefix: Gio.IOErrorEnum: a message');
+        logError(new GLib.Error(Gio.IOErrorEnum, 0, 'a message'), 'prefix');
+    });
+
+    it('logs a JS-created GLib.Error with prefix', function () {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: prefix: GLib.Error my-error: a message');
+        logError(new GLib.Error(GLib.quark_from_string('my-error'), 0, 'a message'), 'prefix');
+    });
+});
diff --git a/installed-tests/js/testFormat.js b/installed-tests/js/testFormat.js
index 000e78d..72d2028 100644
--- a/installed-tests/js/testFormat.js
+++ b/installed-tests/js/testFormat.js
@@ -1,66 +1,60 @@
-// tests for imports.format module
-
 const Format = imports.format;
-const JSUnit = imports.jsUnit;
-
-function testEscape() {
-    var foo = '%d%%'.format(10);
-    JSUnit.assertEquals("escaped '%'", "10%", foo);
-}
-
-function testStrings() {
-    var foo = '%s'.format("Foo");
-    var foobar = '%s %s'.format("Foo", "Bar");
-    var barfoo = '%2$s %1$s'.format("Foo", "Bar");
-
-    JSUnit.assertEquals("single string argument", "Foo", foo);
-    JSUnit.assertEquals("two string arguments", "Foo Bar", foobar);
-    JSUnit.assertEquals("two swapped string arguments", "Bar Foo", barfoo);
-}
-
-function testFixedNumbers() {
-    var foo = '%d'.format(42);
-    var bar = '%x'.format(42);
-
-    JSUnit.assertEquals("base-10 42", "42", foo);
-    JSUnit.assertEquals("base-16 42", "2a", bar);
-}
-
-function testFloating() {
-    var foo = '%f'.format(0.125);
-    var bar = '%.2f'.format(0.125);
-
-    JSUnit.assertEquals("0.125, no precision", "0.125", foo);
-    JSUnit.assertEquals("0.125, precision 2", "0.13", bar);
-}
-
-function testPadding() {
-    let zeroFormat = '%04d';
-    var foo1 = zeroFormat.format(1);
-    var foo10 = zeroFormat.format(10);
-    var foo100 = zeroFormat.format(100);
-
-    let spaceFormat = '%4d';
-    var bar1 = spaceFormat.format(1);
-    var bar10 = spaceFormat.format(10);
-    var bar100 = spaceFormat.format(100);
-
-    JSUnit.assertEquals("zero-padding 1", "0001", foo1);
-    JSUnit.assertEquals("zero-padding 10", "0010", foo10);
-    JSUnit.assertEquals("zero-padding 100", "0100", foo100);
-
-    JSUnit.assertEquals("space-padding 1", "   1", bar1);
-    JSUnit.assertEquals("space-padding 10", "  10", bar10);
-    JSUnit.assertEquals("space-padding 100", " 100", bar100);
-}
-
-function testErrors() {
-    JSUnit.assertRaises(function() { return '%z'.format(42); });
-    JSUnit.assertRaises(function() { return '%.2d'.format(42); });
-    JSUnit.assertRaises(function() { return '%Ix'.format(42); });
-    JSUnit.assertRaises(function() { return '%2$d %d %1$d'.format(1, 2, 3); });
-}
-
 String.prototype.format = Format.format;
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
 
+describe('imports.format', function () {
+    it('escapes % with another % character', function () {
+        expect('%d%%'.format(10)).toEqual('10%');
+    });
+
+    it('formats a single string argument', function () {
+        expect('%s'.format('Foo')).toEqual('Foo');
+    });
+
+    it('formats two string arguments', function () {
+        expect('%s %s'.format('Foo', 'Bar')).toEqual('Foo Bar');
+    });
+
+    it('formats two swapped string arguments', function () {
+        expect('%2$s %1$s'.format('Foo', 'Bar')).toEqual('Bar Foo');
+    });
+
+    it('formats a number in base 10', function () {
+        expect('%d'.format(42)).toEqual('42');
+    });
+
+    it('formats a number in base 16', function () {
+        expect('%x'.format(42)).toEqual('2a');
+    });
+
+    it('formats a floating point number with no precision', function () {
+        expect('%f'.format(0.125)).toEqual('0.125');
+    });
+
+    it('formats a floating point number with precision 2', function () {
+        expect('%.2f'.format(0.125)).toEqual('0.13');
+    });
+
+    it('pads with zeroes', function () {
+        let zeroFormat = '%04d';
+        expect(zeroFormat.format(1)).toEqual('0001');
+        expect(zeroFormat.format(10)).toEqual('0010');
+        expect(zeroFormat.format(100)).toEqual('0100');
+    });
+
+    it('pads with spaces', function () {
+        let spaceFormat = '%4d';
+        expect(spaceFormat.format(1)).toEqual('   1');
+        expect(spaceFormat.format(10)).toEqual('  10');
+        expect(spaceFormat.format(100)).toEqual(' 100');
+    });
+
+    it('throws an error when given incorrect modifiers for the conversion type', function () {
+        expect(() => '%z'.format(42)).toThrow();
+        expect(() => '%.2d'.format(42)).toThrow();
+        expect(() => '%Ix'.format(42)).toThrow();
+    });
+
+    it('throws an error when incorrectly instructed to swap arguments', function () {
+        expect(() => '%2$d %d %1$d'.format(1, 2, 3)).toThrow();
+    });
+});
diff --git a/installed-tests/js/testFundamental.js b/installed-tests/js/testFundamental.js
index 24031ca..ff94a23 100644
--- a/installed-tests/js/testFundamental.js
+++ b/installed-tests/js/testFundamental.js
@@ -1,16 +1,7 @@
-// application/javascript;version=1.8
+const Regress = imports.gi.Regress;
 
-const JSUnit = imports.jsUnit;
-const Everything = imports.gi.Regress;
-const WarnLib = imports.gi.WarnLib;
-
-const GLib = imports.gi.GLib;
-const Gio = imports.gi.Gio;
-const GObject = imports.gi.GObject;
-const Lang = imports.lang;
-
-function testFundamental() {
-    let f = new Everything.TestFundamentalSubObject('plop');
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+describe('Fundamental type support', function () {
+    it('constructs a subtype of a fundamental type', function () {
+        expect(() => new Regress.TestFundamentalSubObject('plop')).not.toThrow();
+    });
+});
diff --git a/installed-tests/js/testGDBus.js b/installed-tests/js/testGDBus.js
index 4a083fa..28e1f15 100644
--- a/installed-tests/js/testGDBus.js
+++ b/installed-tests/js/testGDBus.js
@@ -1,9 +1,6 @@
-
 const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 
-const JSUnit = imports.jsUnit;
-
 /* The methods list with their signatures.
  *
  * *** NOTE: If you add stuff here, you need to update testIntrospectReal
@@ -210,406 +207,249 @@ Test.prototype = {
     }
 };
 
-var own_name_id;
-var test;
-
-function testExportStuff() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    test = new Test();
-
-    own_name_id = Gio.DBus.session.own_name('org.gnome.gjs.Test',
-                                            Gio.BusNameOwnerFlags.NONE,
-                                            function(name) {
-                                                log("Acquired name " + name);
-                                                
-                                                loop.quit();
-                                            },
-                                            function(name) {
-                                                log("Lost name " + name);
-                                            });
-
-    loop.run();
-}
-
 const ProxyClass = Gio.DBusProxy.makeProxyWrapper(TestIface);
-var proxy;
-
-function testInitStuff() {
-    let loop = GLib.MainLoop.new(null, false);
 
-    var theError;
-    proxy = new ProxyClass(Gio.DBus.session,
-                           'org.gnome.gjs.Test',
-                           '/org/gnome/gjs/Test',
-                           function (obj, error) {
-                               theError = error;
-                               proxy = obj;
-
-                               loop.quit();
-                           });
-
-    loop.run();
-
-    JSUnit.assertNotNull(proxy);
-    JSUnit.assertNull(theError);
-}
-
-function testFrobateStuff() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.frobateStuffRemote({}, function(result, excp) {
-        JSUnit.assertNull(excp);
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
+describe('Exported DBus object', function () {
+    var own_name_id;
+    var test;
+    var proxy;
+    let loop;
+
+    beforeAll(function () {
+        loop = new GLib.MainLoop(null, false);
+
+        test = new Test();
+        own_name_id = Gio.DBus.session.own_name('org.gnome.gjs.Test',
+            Gio.BusNameOwnerFlags.NONE,
+            name => {
+                log("Acquired name " + name);
+                loop.quit();
+            },
+            name => {
+                log("Lost name " + name);
+            });
+        loop.run();
+        new ProxyClass(Gio.DBus.session, 'org.gnome.gjs.Test',
+            '/org/gnome/gjs/Test',
+            (obj, error) => {
+                expect(error).toBeNull();
+                proxy = obj;
+                expect(proxy).not.toBeNull();
+                loop.quit();
+            });
+        loop.run();
     });
 
-    loop.run();
-
-    JSUnit.assertEquals("world", theResult.hello.deep_unpack());
-}
-
-/* excp must be exactly the exception thrown by the remote method
-   (more or less) */
-function testThrowException() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-                             'JS ERROR: Exception in method call: alwaysThrowException: *');
-
-    let theResult, theExcp;
-    proxy.alwaysThrowExceptionRemote({}, function(result, excp) {
-        theResult = result;
-        theExcp = excp;
-        loop.quit();
+    afterAll(function () {
+        // Not really needed, but if we don't cleanup
+        // memory checking will complain
+        Gio.DBus.session.unown_name(own_name_id);
     });
 
-    loop.run();
-
-    JSUnit.assertNull(theResult);
-    JSUnit.assertNotNull(theExcp);
-}
-
-/* We check that the exception in the answer is not null when we try to call
- * a method that does not exist */
-function testDoesNotExist() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-
-    /* First remove the method from the object! */
-    delete Test.prototype.thisDoesNotExist;
-
-    proxy.thisDoesNotExistRemote(function (result, excp) {
-        theResult = result;
-        theExcp = excp;
-        loop.quit();
+    beforeEach(function () {
+        loop = new GLib.MainLoop(null, false);
     });
 
-    loop.run();
-
-    JSUnit.assertNotNull(theExcp);
-    JSUnit.assertNull(theResult);
-}
-
-function testNonJsonFrobateStuff() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.nonJsonFrobateStuffRemote(42, function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
+    it('can call a remote method', function () {
+        proxy.frobateStuffRemote({}, ([result], excp) => {
+            expect(excp).toBeNull();
+            expect(result.hello.deep_unpack()).toEqual('world');
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-
-    JSUnit.assertEquals("42 it is!", theResult);
-    JSUnit.assertNull(theExcp);
-}
-
-function testNoInParameter() {
-    let loop = GLib.MainLoop.new(null, false);
+    /* excp must be exactly the exception thrown by the remote method
+       (more or less) */
+    it('can handle an exception thrown by a remote method', function () {
+        GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+            'JS ERROR: Exception in method call: alwaysThrowException: *');
 
-    let theResult, theExcp;
-    proxy.noInParameterRemote(function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
+        proxy.alwaysThrowExceptionRemote({}, function(result, excp) {
+            expect(result).toBeNull();
+            expect(excp).not.toBeNull();
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
+    it('throws an exception when trying to call a method that does not exist', function () {
+        /* First remove the method from the object! */
+        delete Test.prototype.thisDoesNotExist;
 
-    JSUnit.assertEquals("Yes!", theResult);
-    JSUnit.assertNull(theExcp);
-}
-
-function testMultipleInArgs() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.multipleInArgsRemote(1, 2, 3, 4, 5, function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
+        proxy.thisDoesNotExistRemote(function (result, excp) {
+            expect(excp).not.toBeNull();
+            expect(result).toBeNull();
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-
-    JSUnit.assertEquals("1 2 3 4 5", theResult);
-    JSUnit.assertNull(theExcp);
-}
-
-function testNoReturnValue() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.noReturnValueRemote(function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
+    it('can pass a parameter to a remote method that is not a JSON object', function () {
+        proxy.nonJsonFrobateStuffRemote(42, ([result], excp) => {
+            expect(result).toEqual('42 it is!');
+            expect(excp).toBeNull();
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-
-    JSUnit.assertEquals(undefined, theResult);
-    JSUnit.assertNull(theExcp);
-}
-
-function testEmitSignal() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    let signalReceived = 0;
-    let signalArgument = null;
-    let id = proxy.connectSignal('signalFoo',
-                                 function(emitter, senderName, parameters) {
-                                     signalReceived ++;
-                                     [signalArgument] = parameters;
-
-                                     proxy.disconnectSignal(id);
-                                 });
-    proxy.emitSignalRemote(function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        if (excp)
-            log("Signal emission exception: " + excp);
-        loop.quit();
+    it('can call a remote method with no in parameter', function () {
+        proxy.noInParameterRemote(([result], excp) => {
+            expect(result).toEqual('Yes!');
+            expect(excp).toBeNull();
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-
-    JSUnit.assertUndefined('result should be undefined', theResult);
-    JSUnit.assertNull('no exception set', theExcp);
-    JSUnit.assertEquals('number of signals received', signalReceived, 1);
-    JSUnit.assertEquals('signal argument', signalArgument, "foobar");
-
-}
-
-function testMultipleOutValues() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.multipleOutValuesRemote(function(result, excp) {
-        theResult = result;
-        theExcp = excp;
-        loop.quit();
+    it('can call a remote method with multiple in parameters', function () {
+        proxy.multipleInArgsRemote(1, 2, 3, 4, 5, ([result], excp) => {
+            expect(result).toEqual('1 2 3 4 5');
+            expect(excp).toBeNull();
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-
-    JSUnit.assertEquals("Hello", theResult[0]);
-    JSUnit.assertEquals("World", theResult[1]);
-    JSUnit.assertEquals("!", theResult[2]);
-    JSUnit.assertNull(theExcp);
-}
-
-function testOneArrayOut() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.oneArrayOutRemote(function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
+    it('can call a remote method with no return value', function () {
+        proxy.noReturnValueRemote(([result], excp) => {
+            expect(result).not.toBeDefined();
+            expect(excp).toBeNull();
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-
-    JSUnit.assertEquals("Hello", theResult[0]);
-    JSUnit.assertEquals("World", theResult[1]);
-    JSUnit.assertEquals("!", theResult[2]);
-    JSUnit.assertNull(theExcp);
-}
-
-function testArrayOfArrayOut() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.arrayOfArrayOutRemote(function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
+    it('can emit a DBus signal', function () {
+        let handler = jasmine.createSpy('signalFoo');
+        let id = proxy.connectSignal('signalFoo', handler);
+        handler.and.callFake(() => proxy.disconnectSignal(id));
+
+        proxy.emitSignalRemote(([result], excp) => {
+            expect(result).not.toBeDefined();
+            expect(excp).toBeNull();
+            expect(handler).toHaveBeenCalledTimes(1);
+            expect(handler).toHaveBeenCalledWith(jasmine.anything(),
+                jasmine.anything(), ['foobar']);
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-
-    let a1 = theResult[0];
-    let a2 = theResult[1];
-
-    JSUnit.assertEquals("Hello", a1[0]);
-    JSUnit.assertEquals("World", a1[1]);
-
-    JSUnit.assertEquals("World", a2[0]);
-    JSUnit.assertEquals("Hello", a2[1]);;
-
-    JSUnit.assertNull(theExcp);
-}
-
-function testMultipleArrayOut() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.multipleArrayOutRemote(function(result, excp) {
-        theResult = result;
-        theExcp = excp;
-        loop.quit();
+    it('can call a remote method with multiple return values', function () {
+        proxy.multipleOutValuesRemote(function(result, excp) {
+            expect(result).toEqual(['Hello', 'World', '!']);
+            expect(excp).toBeNull();
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-
-    let a1 = theResult[0];
-    let a2 = theResult[1];
-
-    JSUnit.assertEquals("Hello", a1[0]);
-    JSUnit.assertEquals("World", a1[1]);
-
-    JSUnit.assertEquals("World", a2[0]);
-    JSUnit.assertEquals("Hello", a2[1]);;
-
-    JSUnit.assertNull(theExcp);
-}
-
-/* COMPAT: This test should test what happens when a TypeError is thrown during
- * argument marshalling, but conversions don't throw TypeErrors anymore, so we
- * can't test that ... until we upgrade to mozjs38 which has Symbols. Converting
- * a Symbol to an int32 or string will throw a TypeError.
- */
-function testArrayOutBadSig() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let theResult, theExcp;
-    proxy.arrayOutBadSigRemote(function(result, excp) {
-        theResult = result;
-        theExcp = excp;
-        loop.quit();
+    it('does not coalesce one array into the array of return values', function () {
+        proxy.oneArrayOutRemote(([result], excp) => {
+            expect(result).toEqual(['Hello', 'World', '!']);
+            expect(excp).toBeNull();
+            loop.quit();
+        });
+        loop.run();
     });
 
-    loop.run();
-    // JSUnit.assertNull(theResult);
-    // JSUnit.assertNotNull(theExcp);
-}
-
-function testAsyncImplementation() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let someString = "Hello world!";
-    let someInt = 42;
-    let theResult, theExcp;
-    proxy.echoRemote(someString, someInt,
-                     function(result, excp) {
-                         theResult = result;
-                         theExcp = excp;
-                         loop.quit();
-                     });
-
-    loop.run();
-    JSUnit.assertNull(theExcp);
-    JSUnit.assertNotNull(theResult);
-    JSUnit.assertEquals(theResult[0], someString);
-    JSUnit.assertEquals(theResult[1], someInt);
-}
+    it('does not coalesce an array of arrays into the array of return values', function () {
+        proxy.arrayOfArrayOutRemote(([[a1, a2]], excp) => {
+            expect(a1).toEqual(['Hello', 'World']);
+            expect(a2).toEqual(['World', 'Hello']);
+            expect(excp).toBeNull();
+            loop.quit();
+        });
+        loop.run();
+    });
 
-function testBytes() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let someBytes = [ 0, 63, 234 ];
-    let theResult, theExcp;
-    for (let i = 0; i < someBytes.length; ++i) {
-        theResult = null;
-        theExcp = null;
-        proxy.byteEchoRemote(someBytes[i], function(result, excp) {
-            [theResult] = result;
-            theExcp = excp;
+    it('can return multiple arrays from a remote method', function () {
+        proxy.multipleArrayOutRemote(([a1, a2], excp) => {
+            expect(a1).toEqual(['Hello', 'World']);
+            expect(a2).toEqual(['World', 'Hello']);
+            expect(excp).toBeNull();
             loop.quit();
         });
+        loop.run();
+    });
 
+    /* COMPAT: This test should test what happens when a TypeError is thrown
+     * during argument marshalling, but conversions don't throw TypeErrors
+     * anymore, so we can't test that ... until we upgrade to mozjs38 which has
+     * Symbols. Converting a Symbol to an int32 or string will throw a TypeError.
+     */
+    xit('handles a bad signature by throwing an exception', function () {
+        proxy.arrayOutBadSigRemote(function(result, excp) {
+            expect(result).toBeNull();
+            expect(excp).not.toBeNull();
+            loop.quit();
+        });
         loop.run();
-        JSUnit.assertNull(theExcp);
-        JSUnit.assertNotNull(theResult);
-        JSUnit.assertEquals(someBytes[i], theResult);
-    }
-}
+    }).pend('currently cannot throw TypeError during conversion');
+
+    it('can call a remote method that is implemented asynchronously', function () {
+        let someString = "Hello world!";
+        let someInt = 42;
+
+        proxy.echoRemote(someString, someInt,
+            function(result, excp) {
+                expect(excp).toBeNull();
+                expect(result).toEqual([someString, someInt]);
+                loop.quit();
+            });
+        loop.run();
+    });
 
-function testStructArray() {
-    let loop = GLib.MainLoop.new(null, false);
+    it('can send and receive bytes from a remote method', function () {
+        let loop = GLib.MainLoop.new(null, false);
 
-    let theResult, theExcp;
-    proxy.structArrayRemote(function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
-    });
-    loop.run();
-    JSUnit.assertNull(theExcp);
-    JSUnit.assertNotNull(theResult);
-    JSUnit.assertEquals(theResult[0][0], 128);
-    JSUnit.assertEquals(theResult[0][1], 123456);
-    JSUnit.assertEquals(theResult[1][0], 42);
-    JSUnit.assertEquals(theResult[1][1], 654321);
-}
+        let someBytes = [ 0, 63, 234 ];
+        someBytes.forEach(b => {
+            proxy.byteEchoRemote(b, ([result], excp) => {
+                expect(excp).toBeNull();
+                expect(result).toEqual(b);
+                loop.quit();
+            });
 
-function testDictSignatures() {
-    let loop = GLib.MainLoop.new(null, false);
-
-    let someDict = {
-        aDouble: new GLib.Variant('d', 10),
-        // should be an integer after round trip
-        anInteger: new GLib.Variant('i', 10.5),
-        // should remain a double
-        aDoubleBeforeAndAfter: new GLib.Variant('d', 10.5),
-    };
-    let theResult, theExcp;
-    proxy.dictEchoRemote(someDict, function(result, excp) {
-        [theResult] = result;
-        theExcp = excp;
-        loop.quit();
+            loop.run();
+        });
     });
 
-    loop.run();
-    JSUnit.assertNull(theExcp);
-    JSUnit.assertNotNull(theResult);
-
-    // verify the fractional part was dropped off int
-    JSUnit.assertEquals(10, theResult['anInteger'].deep_unpack());
+    it('can call a remote method that returns an array of structs', function () {
+        proxy.structArrayRemote(([result], excp) => {
+            expect(excp).toBeNull();
+            expect(result).toEqual([[128, 123456], [42, 654321]]);
+            loop.quit();
+        });
+        loop.run();
+    });
 
-    // and not dropped off a double
-    JSUnit.assertEquals(10.5, theResult['aDoubleBeforeAndAfter'].deep_unpack());
+    it('can send and receive dicts from a remote method', function () {
+        let someDict = {
+            aDouble: new GLib.Variant('d', 10),
+            // should be an integer after round trip
+            anInteger: new GLib.Variant('i', 10.5),
+            // should remain a double
+            aDoubleBeforeAndAfter: new GLib.Variant('d', 10.5),
+        };
 
-    // this JSUnit.assertion is useless, it will work
-    // anyway if the result is really an int,
-    // but it at least checks we didn't lose data
-    JSUnit.assertEquals(10.0, theResult['aDouble'].deep_unpack());
-}
+        proxy.dictEchoRemote(someDict, ([result], excp) => {
+            expect(excp).toBeNull();
+            expect(result).not.toBeNull();
 
-function testFinalize() {
-    // Not really needed, but if we don't cleanup
-    // memory checking will complain
+            // verify the fractional part was dropped off int
+            expect(result['anInteger'].deep_unpack()).toEqual(10);
 
-    Gio.DBus.session.unown_name(own_name_id);
-}
+            // and not dropped off a double
+            expect(result['aDoubleBeforeAndAfter'].deep_unpack()).toEqual(10.5);
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+            // check without type conversion
+            expect(result['aDouble'].deep_unpack()).toBe(10.0);
 
+            loop.quit();
+        });
+        loop.run();
+    });
+});
diff --git a/installed-tests/js/testGIMarshalling.js b/installed-tests/js/testGIMarshalling.js
index b67feb0..99d081b 100644
--- a/installed-tests/js/testGIMarshalling.js
+++ b/installed-tests/js/testGIMarshalling.js
@@ -1,15 +1,4 @@
-if (!('assertEquals' in this)) { /* allow running this test standalone */
-    imports.lang.copyPublicProperties(imports.jsUnit, this);
-    gjstestRun = function() { return imports.jsUnit.gjstestRun(this); };
-}
-
-function assertArrayEquals(expected, got) {
-    assertEquals(expected.length, got.length);
-    for (let i = 0; i < expected.length; i ++) {
-        assertEquals(expected[i], got[i]);
-    }
-}
-
+const ByteArray = imports.byteArray;
 const GIMarshallingTests = imports.gi.GIMarshallingTests;
 
 // We use Gio and GLib to have some objects that we know exist
@@ -18,243 +7,296 @@ const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
 const Lang = imports.lang;
 
-function testCArray() {
-    var array, sum;
-
-    var result = GIMarshallingTests.init_function(null);
-    assertEquals(result.length, 2);
-    var success = result[0];
-    var newArray = result[1];
-    assertEquals(newArray.length, 0);
-
-    array = GIMarshallingTests.array_zero_terminated_return();
-    assertEquals("0", array[0]);
-    assertEquals("1", array[1]);
-    assertEquals("2", array[2]);
-    assertEquals(3, array.length);
-
-    array = GIMarshallingTests.array_zero_terminated_return_struct();
-    assertEquals(3, array.length);
-    assertEquals(42, array[0].long_);
-    assertEquals(43, array[1].long_);
-    assertEquals(44, array[2].long_);
-
-    array = GIMarshallingTests.array_return();
-    assertEquals(4, array.length);
-    assertEquals(-1, array[0]);
-    assertEquals(0, array[1]);
-    assertEquals(1, array[2]);
-    assertEquals(2, array[3]);
-
-    [array, sum] = GIMarshallingTests.array_return_etc(9, 5);
-    assertEquals(14, sum);
-    assertEquals(4, array.length);
-    assertEquals(9, array[0]);
-    assertEquals(0, array[1]);
-    assertEquals(1, array[2]);
-    assertEquals(5, array[3]);
-
-    array = GIMarshallingTests.array_out();
-    assertEquals(4, array.length);
-    assertEquals(-1, array[0]);
-    assertEquals(0, array[1]);
-    assertEquals(1, array[2]);
-    assertEquals(2, array[3]);
-
-    [array, sum] = GIMarshallingTests.array_out_etc(9, 5);
-    assertEquals(14, sum);
-    assertEquals(4, array.length);
-    assertEquals(9, array[0]);
-    assertEquals(0, array[1]);
-    assertEquals(1, array[2]);
-    assertEquals(5, array[3]);
-
-    array = GIMarshallingTests.array_bool_out();
-    assertEquals(4, array.length);
-    assertEquals(true, array[0]);
-    assertEquals(false, array[1]);
-    assertEquals(true, array[2]);
-    assertEquals(true, array[3]);
-
-    assertEquals('const \u2665 utf8', GIMarshallingTests.array_unichar_out());
-    assertEquals('const \u2665 utf8',
-        GIMarshallingTests.array_zero_terminated_return_unichar());
-
-    array = GIMarshallingTests.array_inout([-1, 0, 1, 2]);
-    assertEquals(5, array.length);
-    assertEquals(-2, array[0]);
-    assertEquals(-1, array[1]);
-    assertEquals(0, array[2]);
-    assertEquals(1, array[3]);
-    assertEquals(2, array[4]);
-
-    [array, sum] = GIMarshallingTests.array_inout_etc(9, [-1, 0, 1, 2], 5);
-    assertEquals(14, sum);
-    assertEquals(5, array.length);
-    assertEquals(9, array[0]);
-    assertEquals(-1, array[1]);
-    assertEquals(0, array[2]);
-    assertEquals(1, array[3]);
-    assertEquals(5, array[4]);
-
-    GIMarshallingTests.array_string_in(["foo", "bar"]);
-
-    array = [];
-    for (var i = 0; i < 3; i++) {
-       array[i] = new GIMarshallingTests.BoxedStruct();
-       array[i].long_ = i + 1;
+describe('C array', function () {
+    function createStructArray() {
+        return [1, 2, 3].map(num => {
+            let struct = new GIMarshallingTests.BoxedStruct();
+            struct.long_ = num;
+            return struct;
+        });
     }
 
-    GIMarshallingTests.array_struct_in(array);
+    it('can be passed to a function', function () {
+        expect(() => GIMarshallingTests.array_in([-1, 0, 1, 2])).not.toThrow();
+    });
+
+    it('can be passed to a function with its length parameter before it', function () {
+        expect(() => GIMarshallingTests.array_in_len_before([-1, 0, 1, 2]))
+            .not.toThrow();
+    });
+
+    it('can be passed to a function with zero terminator', function () {
+        expect(() => GIMarshallingTests.array_in_len_zero_terminated([-1, 0, 1, 2]))
+            .not.toThrow();
+    });
+
+    it('can be passed to a function in the style of gtk_init()', function () {
+        let [, newArray] = GIMarshallingTests.init_function(null);
+        expect(newArray).toEqual([]);
+    });
+
+    it('can be returned with zero terminator', function () {
+        expect(GIMarshallingTests.array_zero_terminated_return())
+            .toEqual(['0', '1', '2']);
+    });
+
+    it('can be returned', function () {
+        expect(GIMarshallingTests.array_return()).toEqual([-1, 0, 1, 2]);
+    });
+
+    it('can be returned along with other arguments', function () {
+        let [array, sum] = GIMarshallingTests.array_return_etc(9, 5);
+        expect(sum).toEqual(14);
+        expect(array).toEqual([9, 0, 1, 5]);
+    });
+
+    it('can be an out argument', function () {
+        expect(GIMarshallingTests.array_out()).toEqual([-1, 0, 1, 2]);
+    });
+
+    it('can be an out argument along with other arguments', function () {
+        let [array, sum] = GIMarshallingTests.array_out_etc(9, 5);
+        expect(sum).toEqual(14);
+        expect(array).toEqual([9, 0, 1, 5]);
+    });
+
+    it('can be an in-out argument', function () {
+        expect(GIMarshallingTests.array_inout([-1, 0, 1, 2]))
+            .toEqual([-2, -1, 0, 1, 2]);
+    });
+
+    it('can be an in-out argument along with other arguments', function () {
+        let [array, sum] = GIMarshallingTests.array_inout_etc(9, [-1, 0, 1, 2], 5);
+        expect(sum).toEqual(14);
+        expect(array).toEqual([9, -1, 0, 1, 5]);
+    });
 
     // Run twice to ensure that copies are correctly made for (transfer full)
-    GIMarshallingTests.array_struct_take_in(array);
-    GIMarshallingTests.array_struct_take_in(array);
-
-    GIMarshallingTests.array_uint8_in ("abcd");
-    GIMarshallingTests.array_unichar_in('const \u2665 utf8');
-    GIMarshallingTests.array_unichar_in([0x63, 0x6f, 0x6e, 0x73, 0x74, 0x20,
-        0x2665, 0x20, 0x75, 0x74, 0x66, 0x38]);
-    GIMarshallingTests.array_enum_in([GIMarshallingTests.Enum.VALUE1,
-                                     GIMarshallingTests.Enum.VALUE2,
-                                     GIMarshallingTests.Enum.VALUE3]);
-
-    array = [-1, 0, 1, 2];
-    GIMarshallingTests.array_in(array);
-    GIMarshallingTests.array_in_len_before(array);
-    GIMarshallingTests.array_in_len_zero_terminated(array);
-    GIMarshallingTests.array_in_guint64_len(array);
-    GIMarshallingTests.array_in_guint8_len(array);
-    GIMarshallingTests.array_int64_in(array);
-    GIMarshallingTests.array_uint64_in(array);
-    GIMarshallingTests.array_bool_in(array);
-}
+    it('copies correctly on transfer full', function () {
+        let array = createStructArray();
+        expect(() => {
+            GIMarshallingTests.array_struct_take_in(array);
+            GIMarshallingTests.array_struct_take_in(array);
+        }).not.toThrow();
+    });
 
-function testGArray() {
-    var array;
-    array = GIMarshallingTests.garray_int_none_return();
-    assertEquals(-1, array[0]);
-    assertEquals(0, array[1]);
-    assertEquals(1, array[2]);
-    assertEquals(2, array[3]);
-    array = GIMarshallingTests.garray_utf8_none_return()
-    assertEquals("0", array[0]);
-    assertEquals("1", array[1]);
-    assertEquals("2", array[2]);
-    array = GIMarshallingTests.garray_utf8_container_return()
-    assertEquals("0", array[0]);
-    assertEquals("1", array[1]);
-    assertEquals("2", array[2]);
-    array = GIMarshallingTests.garray_utf8_full_return()
-    assertEquals("0", array[0]);
-    assertEquals("1", array[1]);
-    assertEquals("2", array[2]);
-
-    GIMarshallingTests.garray_int_none_in([-1, 0, 1, 2]);
-    GIMarshallingTests.garray_utf8_none_in(["0", "1", "2"]);
-    GIMarshallingTests.garray_bool_none_in([-1, 0, 1, 2]);
-    GIMarshallingTests.garray_unichar_none_in('const \u2665 utf8');
-    GIMarshallingTests.garray_unichar_none_in([0x63, 0x6f, 0x6e, 0x73, 0x74,
-        0x20, 0x2665, 0x20, 0x75, 0x74, 0x66, 0x38]);
-
-    array = GIMarshallingTests.garray_utf8_none_out();
-    assertEquals("0", array[0]);
-    assertEquals("1", array[1]);
-    assertEquals("2", array[2]);
-    array = GIMarshallingTests.garray_utf8_container_out();
-    assertEquals("0", array[0]);
-    assertEquals("1", array[1]);
-    assertEquals("2", array[2]);
-    array = GIMarshallingTests.garray_utf8_full_out();
-    assertEquals("0", array[0]);
-    assertEquals("1", array[1]);
-    assertEquals("2", array[2]);
-}
+    describe('of structs', function () {
+        it('can be passed to a function', function () {
+            expect(() => GIMarshallingTests.array_struct_in(createStructArray()))
+                .not.toThrow();
+        });
 
-function testByteArray() {
-    var i = 0;
-    var refByteArray = new imports.byteArray.ByteArray();
-    refByteArray[i++] = 0;
-    refByteArray[i++] = 49;
-    refByteArray[i++] = 0xFF;
-    refByteArray[i++] = 51;
-    var byteArray = GIMarshallingTests.bytearray_full_return();
-    assertEquals(refByteArray.length, byteArray.length);
-    for (i = 0; i < refByteArray.length; i++)
-       assertEquals(refByteArray[i], byteArray[i]);
-    GIMarshallingTests.bytearray_none_in(refByteArray);
-
-    // Another test, with a normal array, to test conversion
-    GIMarshallingTests.bytearray_none_in([0, 49, 0xFF, 51]);
-}
+        it('can be returned with zero terminator', function () {
+            let structArray = GIMarshallingTests.array_zero_terminated_return_struct();
+            expect(structArray.map(e => e.long_)).toEqual([42, 43, 44]);
+        });
+    });
 
-function testGBytes() {
-    var i = 0;
-    var refByteArray = new imports.byteArray.ByteArray();
-    refByteArray[i++] = 0;
-    refByteArray[i++] = 49;
-    refByteArray[i++] = 0xFF;
-    refByteArray[i++] = 51;
-    GIMarshallingTests.gbytes_none_in(refByteArray);
-
-    var bytes = GIMarshallingTests.gbytes_full_return();
-    GIMarshallingTests.gbytes_none_in(bytes);
-
-    var array = bytes.toArray();
-    assertEquals(array[0], 0);
-    assertEquals(array[1], 49);
-    assertEquals(array[2], 0xFF);
-    assertEquals(array[3], 51); 
-    
-    bytes = GLib.Bytes.new([0, 49, 0xFF, 51]);
-    GIMarshallingTests.gbytes_none_in(bytes);
-
-    bytes = GLib.Bytes.new("const \u2665 utf8");
-    GIMarshallingTests.utf8_as_uint8array_in(bytes.toArray());
-
-    bytes = GIMarshallingTests.gbytes_full_return();    
-    array = bytes.toArray(); // Array should just be holding a ref, not a copy
-    assertEquals(array[1], 49);
-    array[1] = 42;  // Assignment should force to GByteArray
-    assertEquals(array[1], 42);
-    array[1] = 49;  // Flip the value back
-    GIMarshallingTests.gbytes_none_in(array.toGBytes()); // Now convert back to GBytes
-
-    bytes = GLib.Bytes.new([97, 98, 99, 100]);
-    GIMarshallingTests.array_uint8_in(bytes.toArray());
-    assertRaises(function() {
-       GIMarshallingTests.array_uint8_in(bytes);
+    describe('of booleans', function () {
+        it('is coerced to true/false when passed to a function', function () {
+            expect(() => GIMarshallingTests.array_bool_in([-1, 0, 1, 2]))
+                .not.toThrow();
+        });
+
+        it('can be an out argument', function () {
+            expect(GIMarshallingTests.array_bool_out())
+                .toEqual([true, false, true, true]);
+        });
     });
-}
 
-function testPtrArray() {
-    var array;
+    describe('of unichars', function () {
+        it('can be passed to a function', function () {
+            expect(() => GIMarshallingTests.array_unichar_in('const \u2665 utf8'))
+                .not.toThrow();
+        });
 
-    GIMarshallingTests.gptrarray_utf8_none_in(["0", "1", "2"]);
+        it('can be an out argument', function () {
+            expect(GIMarshallingTests.array_unichar_out())
+                .toEqual('const \u2665 utf8');
+        });
 
-    var refArray = ["0", "1", "2"];
+        it('can be returned with zero terminator', function () {
+            expect(GIMarshallingTests.array_zero_terminated_return_unichar())
+                .toEqual('const \u2665 utf8');
+        });
 
-    assertArrayEquals(refArray, GIMarshallingTests.gptrarray_utf8_none_return());
-    assertArrayEquals(refArray, GIMarshallingTests.gptrarray_utf8_container_return());
-    assertArrayEquals(refArray, GIMarshallingTests.gptrarray_utf8_full_return());
+        it('can be implicitly converted from a number array', function () {
+            expect(() => GIMarshallingTests.array_unichar_in([0x63, 0x6f, 0x6e, 0x73,
+                0x74, 0x20, 0x2665, 0x20, 0x75, 0x74, 0x66, 0x38])).not.toThrow();
+        });
+    });
 
-    assertArrayEquals(refArray, GIMarshallingTests.gptrarray_utf8_none_out());
-    assertArrayEquals(refArray, GIMarshallingTests.gptrarray_utf8_container_out());
-    assertArrayEquals(refArray, GIMarshallingTests.gptrarray_utf8_full_out());
-}
+    describe('of strings', function () {
+        it('can be passed to a function', function () {
+            expect(() => GIMarshallingTests.array_string_in(['foo', 'bar']))
+                .not.toThrow();
+        });
+    });
 
-function testGHashTable() {
-    function dictEquals(dict, ref) {
-        let dict_keys = Object.keys(dict);
-        let ref_keys = Object.keys(ref);
-        assertEquals(ref_keys.length, dict_keys.length);
-        ref_keys.forEach((key, ix) => {
-            assertEquals(key, dict_keys[ix]);
-            assertEquals(ref[key], dict[key]);
+    describe('of enums', function () {
+        it('can be passed to a function', function () {
+            expect(() => GIMarshallingTests.array_enum_in([GIMarshallingTests.Enum.VALUE1,
+                GIMarshallingTests.Enum.VALUE2, GIMarshallingTests.Enum.VALUE3])).not.toThrow();
         });
-    }
+    });
+
+    describe('of bytes', function () {
+        it('can be an in argument with length', function () {
+            expect(() => GIMarshallingTests.array_in_guint8_len([-1, 0, 1, 2]))
+                .not.toThrow();
+        });
+
+        it('can be implicitly converted from a string', function () {
+            expect(() => GIMarshallingTests.array_uint8_in('abcd')).not.toThrow();
+        });
+    });
+
+    describe('of 64-bit ints', function () {
+        it('can be passed to a function', function () {
+            expect(() => GIMarshallingTests.array_int64_in([-1, 0, 1, 2]))
+                .not.toThrow();
+            expect(() => GIMarshallingTests.array_uint64_in([-1, 0, 1, 2]))
+                .not.toThrow();
+        });
+
+        it('can be an in argument with length', function () {
+            expect(() => GIMarshallingTests.array_in_guint64_len([-1, 0, 1, 2]))
+                .not.toThrow();
+        });
+    });
+});
+
+describe('GArray', function () {
+    describe('of integers', function () {
+        it('can be passed in with transfer none', function () {
+            expect(() => GIMarshallingTests.garray_int_none_in([-1, 0, 1, 2]))
+                .not.toThrow();
+        });
+
+        it('can be returned with transfer none', function () {
+            expect(GIMarshallingTests.garray_int_none_return())
+                .toEqual([-1, 0, 1, 2]);
+        });
+    });
+
+    describe('of strings', function () {
+        it('can be passed in with transfer none', function () {
+            expect(() => GIMarshallingTests.garray_utf8_none_in(['0', '1', '2']))
+                .not.toThrow();
+        });
+
+        ['return', 'out'].forEach(method => {
+            ['none', 'container', 'full'].forEach(transfer => {
+                it('can be passed as ' + method + ' with transfer ' + transfer, function () {
+                    expect(GIMarshallingTests['garray_utf8_' + transfer + '_' + method]())
+                        .toEqual(['0', '1', '2']);
+                });
+            });
+        });
+    });
+
+    describe('of booleans', function () {
+        it('can be passed in with transfer none', function () {
+            expect(() => GIMarshallingTests.garray_bool_none_in([-1, 0, 1, 2]))
+                .not.toThrow();
+        });
+    });
+
+    describe('of unichars', function () {
+        it('can be passed in with transfer none', function () {
+            expect(() => GIMarshallingTests.garray_unichar_none_in('const \u2665 utf8'))
+                .not.toThrow();
+        });
+
+        it('can be implicitly converted from a number array', function () {
+            expect(() => GIMarshallingTests.garray_unichar_none_in([0x63, 0x6f, 0x6e,
+                0x73, 0x74, 0x20, 0x2665, 0x20, 0x75, 0x74, 0x66, 0x38])).not.toThrow();
+        });
+    });
+});
+
+describe('GByteArray', function () {
+    const refByteArray = ByteArray.fromArray([0, 49, 0xFF, 51]);
+
+    it('can be passed in with transfer none', function () {
+        expect(() => GIMarshallingTests.bytearray_none_in(refByteArray))
+            .not.toThrow();
+    });
+
+    it('can be returned with transfer full', function () {
+        expect(GIMarshallingTests.bytearray_full_return()).toEqual(refByteArray);
+    });
+
+    it('can be implicitly converted from a normal array', function () {
+        expect(() => GIMarshallingTests.bytearray_none_in([0, 49, 0xFF, 51]))
+            .not.toThrow();
+    });
+});
+
+describe('GBytes', function () {
+    const refByteArray = ByteArray.fromArray([0, 49, 0xFF, 51]);
+
+    it('can be created from an array and passed in', function () {
+        let bytes = GLib.Bytes.new([0, 49, 0xFF, 51]);
+        expect(() => GIMarshallingTests.gbytes_none_in(bytes)).not.toThrow();
+    });
+
+    it('can be created by returning from a function and passed in', function () {
+        var bytes = GIMarshallingTests.gbytes_full_return();
+        expect(() => GIMarshallingTests.gbytes_none_in(bytes)).not.toThrow();
+        expect(bytes.toArray()).toEqual(refByteArray);
+    });
 
-    let INT_DICT = {
+    it('can be implicitly converted from a ByteArray', function () {
+        expect(() => GIMarshallingTests.gbytes_none_in(refByteArray))
+            .not.toThrow();
+    });
+
+    it('can be created from a string and is encoded in UTF-8', function () {
+        let bytes = GLib.Bytes.new("const \u2665 utf8");
+        expect(() => GIMarshallingTests.utf8_as_uint8array_in(bytes.toArray()))
+            .not.toThrow();
+    });
+
+    it('turns into a GByteArray on assignment', function () {
+        let bytes = GIMarshallingTests.gbytes_full_return();
+        let array = bytes.toArray();  // Array should just be holding a ref, not a copy
+        expect(array[1]).toEqual(49);
+        array[1] = 42;  // Assignment should force to GByteArray
+        expect(array[1]).toEqual(42);
+        array[1] = 49;  // Flip the value back
+        // Now convert back to GBytes
+        expect(() => GIMarshallingTests.gbytes_none_in(array.toGBytes()))
+            .not.toThrow();
+    });
+
+    it('cannot be passed to a function expecting a byte array', function () {
+        let bytes = GLib.Bytes.new([97, 98, 99, 100]);
+        expect(() => GIMarshallingTests.array_uint8_in(bytes.toArray())).not.toThrow();
+        expect(() => GIMarshallingTests.array_uint8_in(bytes)).toThrow();
+    });
+});
+
+describe('GPtrArray', function () {
+    const refArray = ['0', '1', '2'];
+
+    it('can be passed to a function with transfer none', function () {
+        expect(() => GIMarshallingTests.gptrarray_utf8_none_in(refArray))
+            .not.toThrow();
+    });
+
+    ['return', 'out'].forEach(method => {
+        ['none', 'container', 'full'].forEach(transfer => {
+            it('can be passed as ' + method + ' with transfer ' + transfer, function () {
+                expect(GIMarshallingTests['gptrarray_utf8_' + transfer + '_' + method]())
+                    .toEqual(refArray);
+            });
+        });
+    });
+});
+
+describe('GHashTable', function () {
+    const INT_DICT = {
         '-1': 1,
         0: 0,
         1: -1,
@@ -278,114 +320,221 @@ function testGHashTable() {
         1: '1',
     };
 
-    dictEquals(GIMarshallingTests.ghashtable_int_none_return(), INT_DICT);
-    dictEquals(GIMarshallingTests.ghashtable_utf8_none_return(), STRING_DICT);
-    dictEquals(GIMarshallingTests.ghashtable_utf8_container_return(), STRING_DICT);
-    dictEquals(GIMarshallingTests.ghashtable_utf8_full_return(), STRING_DICT);
-
-    GIMarshallingTests.ghashtable_int_none_in(INT_DICT);
-    GIMarshallingTests.ghashtable_utf8_none_in(STRING_DICT);
-    GIMarshallingTests.ghashtable_double_in(NUMBER_DICT);
-    GIMarshallingTests.ghashtable_float_in(NUMBER_DICT);
-    GIMarshallingTests.ghashtable_int64_in({
-        '-1': -1,
-        0: 0,
-        1: 1,
-        2: 0x100000000,
+    it('can be passed in with integer value type', function () {
+        expect(() => GIMarshallingTests.ghashtable_int_none_in(INT_DICT))
+            .not.toThrow();
     });
-    GIMarshallingTests.ghashtable_uint64_in({
-        '-1': 0x100000000,
-        0: 0,
-        1: 1,
-        2: 2,
+
+    it('can be passed in with string value type', function () {
+        expect(() => GIMarshallingTests.ghashtable_utf8_none_in(STRING_DICT))
+            .not.toThrow();
     });
 
-    dictEquals(GIMarshallingTests.ghashtable_utf8_none_out(), STRING_DICT);
-    dictEquals(GIMarshallingTests.ghashtable_utf8_container_out(), STRING_DICT);
-    dictEquals(GIMarshallingTests.ghashtable_utf8_full_out(), STRING_DICT);
+    it('can be passed in with float value type', function () {
+        expect(() => GIMarshallingTests.ghashtable_float_in(NUMBER_DICT))
+            .not.toThrow();
+    });
 
-    dictEquals(GIMarshallingTests.ghashtable_utf8_none_inout(STRING_DICT), STRING_DICT_OUT);
-    // FIXME: Container transfer for in parameters not supported
-    //dictEquals(GIMarshallingTests.ghashtable_utf8_container_inout(STRING_DICT), STRING_DICT_OUT);
-    // FIXME: Broken
-    //dictEquals(GIMarshallingTests.ghashtable_utf8_full_inout(STRING_DICT), STRING_DICT_OUT);
-}
+    it('can be passed in with double value type', function () {
+        expect(() => GIMarshallingTests.ghashtable_double_in(NUMBER_DICT))
+            .not.toThrow();
+    });
+
+    it('can be passed in with int64 value type', function () {
+        const int64Dict = {
+            '-1': -1,
+            0: 0,
+            1: 1,
+            2: 0x100000000,
+        };
+        expect(() => GIMarshallingTests.ghashtable_int64_in(int64Dict))
+            .not.toThrow();
+    });
 
-function testGValue() {
-    assertEquals(42, GIMarshallingTests.gvalue_return());
-    assertEquals(42, GIMarshallingTests.gvalue_out());
+    it('can be passed in with uint64 value type', function () {
+        const uint64Dict = {
+            '-1': 0x100000000,
+            0: 0,
+            1: 1,
+            2: 2,
+        };
+        expect(() => GIMarshallingTests.ghashtable_uint64_in(uint64Dict))
+            .not.toThrow();
+    });
 
-    GIMarshallingTests.gvalue_in(42);
-    GIMarshallingTests.gvalue_flat_array([42, "42", true]);
+    it('can be returned with integer value type', function () {
+        expect(GIMarshallingTests.ghashtable_int_none_return()).toEqual(INT_DICT);
+    });
 
-    // gjs doesn't support native enum types
-    // GIMarshallingTests.gvalue_in_enum(GIMarshallingTests.Enum.VALUE_3);
+    ['return', 'out'].forEach(method => {
+        ['none', 'container', 'full'].forEach(transfer => {
+            it('can be passed as ' + method + ' with transfer ' + transfer, function () {
+                expect(GIMarshallingTests['ghashtable_utf8_' + transfer + '_' + method]())
+                    .toEqual(STRING_DICT);
+            });
+        });
+    });
 
-    // Test a flat GValue round-trip return
-    let thing = GIMarshallingTests.return_gvalue_flat_array();
-    assertArrayEquals([42, "42", true], thing);
-}
+    it('can be passed as inout with transfer none', function () {
+        expect(GIMarshallingTests.ghashtable_utf8_none_inout(STRING_DICT))
+            .toEqual(STRING_DICT_OUT);
+    });
 
-function testGType() {
-    assertEquals(GObject.TYPE_NONE, GIMarshallingTests.gtype_return());
-    assertEquals(GObject.TYPE_STRING, GIMarshallingTests.gtype_string_return());
+    xit('can be passed as inout with transfer container', function () {
+        expect(GIMarshallingTests.ghashtable_utf8_container_inout(STRING_DICT))
+            .toEqual(STRING_DICT_OUT);
+    }).pend('Container transfer for in parameters not supported');
 
-    GIMarshallingTests.gtype_in(GObject.TYPE_NONE);
-    GIMarshallingTests.gtype_in(GObject.VoidType);
-    GIMarshallingTests.gtype_string_in(GObject.TYPE_STRING);
-    GIMarshallingTests.gtype_string_in(GObject.String);
-    GIMarshallingTests.gtype_string_in(String);
+    xit('can be passed as inout with transfer full', function () {
+        expect(GIMarshallingTests.ghashtable_utf8_full_inout(STRING_DICT))
+            .toEqual(STRING_DICT_OUT);
+    }).pend('https://bugzilla.gnome.org/show_bug.cgi?id=773763');
+});
 
-    assertEquals(GObject.TYPE_NONE, GIMarshallingTests.gtype_out());
-    assertEquals(GObject.TYPE_STRING, GIMarshallingTests.gtype_string_out());
+describe('GValue', function () {
+    it('can be passed into a function and packed', function () {
+        expect(() => GIMarshallingTests.gvalue_in(42)).not.toThrow();
+    });
 
-    assertEquals(GObject.TYPE_INT, GIMarshallingTests.gtype_inout(GObject.TYPE_NONE));
-}
+    it('array can be passed into a function and packed', function () {
+        expect(() => GIMarshallingTests.gvalue_flat_array([42, '42', true]))
+            .not.toThrow();
+    });
 
-function testGValueGType() {
-    // test that inferring the GType for a primitive value or an object works
-
-    // Primitives (and primitive like)
-    GIMarshallingTests.gvalue_in_with_type(42, GObject.TYPE_INT);
-    GIMarshallingTests.gvalue_in_with_type(42.5, GObject.TYPE_DOUBLE);
-    GIMarshallingTests.gvalue_in_with_type('42', GObject.TYPE_STRING);
-    GIMarshallingTests.gvalue_in_with_type(GObject.TYPE_GTYPE, GObject.TYPE_GTYPE);
-
-    GIMarshallingTests.gvalue_in_with_type(42, GObject.Int);
-    GIMarshallingTests.gvalue_in_with_type(42.5, GObject.Double);
-    GIMarshallingTests.gvalue_in_with_type(42.5, Number);
-
-    // Object and interface
-    GIMarshallingTests.gvalue_in_with_type(new Gio.SimpleAction, Gio.SimpleAction);
-    GIMarshallingTests.gvalue_in_with_type(new Gio.SimpleAction, GObject.Object);
-    GIMarshallingTests.gvalue_in_with_type(new Gio.SimpleAction, GObject.TYPE_OBJECT);
-    GIMarshallingTests.gvalue_in_with_type(new Gio.SimpleAction, Gio.SimpleAction);
-
-    // Boxed and union
-    GIMarshallingTests.gvalue_in_with_type(new GLib.KeyFile, GLib.KeyFile);
-    GIMarshallingTests.gvalue_in_with_type(new GLib.KeyFile, GObject.TYPE_BOXED);
-    GIMarshallingTests.gvalue_in_with_type(GLib.Variant.new('u', 42), GLib.Variant);
-    GIMarshallingTests.gvalue_in_with_type(GLib.Variant.new('u', 42), GObject.TYPE_VARIANT);
-    GIMarshallingTests.gvalue_in_with_type(new GIMarshallingTests.BoxedStruct, 
GIMarshallingTests.BoxedStruct);
-    GIMarshallingTests.gvalue_in_with_type(GIMarshallingTests.union_returnv(), GIMarshallingTests.Union);
-
-    // Other
-    GIMarshallingTests.gvalue_in_with_type(GObject.ParamSpec.string('my-param', '', '', 
GObject.ParamFlags.READABLE, ''),
-                                          GObject.TYPE_PARAM);
-
-    // // Foreign
-    // let Cairo;
-    // try {
-    //     Cairo = imports.cairo;
-    // } catch(e) {
-    //     return;
-    // }
-
-    // let surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 2, 2);
-    // let cr = new Cairo.Context(surface);
-    // GIMarshallingTests.gvalue_in_with_type(cr, Cairo.Context);
-    // GIMarshallingTests.gvalue_in_with_type(surface, Cairo.Surface);
-}
+    xit('enum can be passed into a function and packed', function () {
+        expect(() => GIMarshallingTests.gvalue_in_enum(GIMarshallingTests.Enum.VALUE_3))
+            .not.toThrow();
+    }).pend("GJS doesn't support native enum types");
+
+    it('can be returned and unpacked', function () {
+        expect(GIMarshallingTests.gvalue_return()).toEqual(42);
+    });
+
+    it('can be passed as an out argument and unpacked', function () {
+        expect(GIMarshallingTests.gvalue_out()).toEqual(42);
+    });
+
+    it('array can be passed as an out argument and unpacked', function () {
+        expect(GIMarshallingTests.return_gvalue_flat_array())
+            .toEqual([42, '42', true]);
+    });
+
+    it('can have its type inferred from primitive values', function () {
+        expect(() => GIMarshallingTests.gvalue_in_with_type(42, GObject.TYPE_INT))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(42.5, GObject.TYPE_DOUBLE))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type('42', GObject.TYPE_STRING))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(GObject.TYPE_GTYPE, GObject.TYPE_GTYPE))
+            .not.toThrow();
+    });
+
+    it('type objects can be converted from primitive-like types', function () {
+        expect(() => GIMarshallingTests.gvalue_in_with_type(42, GObject.Int))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(42.5, GObject.Double))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(42.5, Number))
+            .not.toThrow();
+    });
+
+    it('can have its type inferred as a GObject type', function () {
+        expect(() => GIMarshallingTests.gvalue_in_with_type(new Gio.SimpleAction(), Gio.SimpleAction))
+            .not.toThrow();
+    });
+
+    it('can have its type inferred as a superclass', function () {
+        let action = new Gio.SimpleAction();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(action, GObject.Object))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(action, GObject.TYPE_OBJECT))
+            .not.toThrow();
+    });
+
+    it('can have its type inferred as an interface that it implements', function () {
+        expect(() => GIMarshallingTests.gvalue_in_with_type(new Gio.SimpleAction(), Gio.SimpleAction))
+            .not.toThrow();
+    });
+
+    it('can have its type inferred as a boxed type', function () {
+        let keyfile = new GLib.KeyFile();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(keyfile, GLib.KeyFile))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(keyfile, GObject.TYPE_BOXED))
+            .not.toThrow();
+        let struct = new GIMarshallingTests.BoxedStruct();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(struct, GIMarshallingTests.BoxedStruct))
+            .not.toThrow();
+    });
+
+    it('can have its type inferred as GVariant', function () {
+        let variant = GLib.Variant.new('u', 42);
+        expect(() => GIMarshallingTests.gvalue_in_with_type(variant, GLib.Variant))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(variant, GObject.TYPE_VARIANT))
+            .not.toThrow();
+    });
+
+    it('can have its type inferred as a union type', function () {
+        let union = GIMarshallingTests.union_returnv();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(union, GIMarshallingTests.Union))
+            .not.toThrow();
+    });
+
+    it('can have its type inferred as a GParamSpec', function () {
+        let paramSpec = GObject.ParamSpec.string('my-param', '', '',
+            GObject.ParamFlags.READABLE, '');
+        expect(() => GIMarshallingTests.gvalue_in_with_type(paramSpec, GObject.TYPE_PARAM))
+            .not.toThrow();
+    });
+
+    xit('can have its type inferred as a foreign struct', function () {
+        let Cairo;
+        try {
+            Cairo = imports.cairo;
+        } catch(e) {
+            pending('Compiled without Cairo support');
+            return;
+        }
+
+        let surface = new Cairo.ImageSurface(Cairo.Format.ARGB32, 2, 2);
+        let cr = new Cairo.Context(surface);
+        expect(() => GIMarshallingTests.gvalue_in_with_type(cr, Cairo.Context))
+            .not.toThrow();
+        expect(() => GIMarshallingTests.gvalue_in_with_type(surface, Cairo.Surface))
+            .not.toThrow();
+    }).pend('Errors out with "not a subclass of GObject_Boxed"');
+});
+
+describe('GType', function () {
+    it('can be passed into a function', function () {
+        expect(() => GIMarshallingTests.gtype_in(GObject.TYPE_NONE)).not.toThrow();
+        expect(() => GIMarshallingTests.gtype_in(GObject.VoidType)).not.toThrow();
+        expect(() => GIMarshallingTests.gtype_string_in(GObject.TYPE_STRING)).not.toThrow();
+        expect(() => GIMarshallingTests.gtype_string_in(GObject.String)).not.toThrow();
+    });
+
+    it('can be returned', function () {
+        expect(GIMarshallingTests.gtype_return()).toEqual(GObject.TYPE_NONE);
+        expect(GIMarshallingTests.gtype_string_return())
+            .toEqual(GObject.TYPE_STRING);
+    });
+
+    it('can be passed as an out argument', function () {
+        expect(GIMarshallingTests.gtype_out()).toEqual(GObject.TYPE_NONE);
+        expect(GIMarshallingTests.gtype_string_out()).toEqual(GObject.TYPE_STRING);
+    });
+
+    it('can be passed as an inout argument', function () {
+        expect(GIMarshallingTests.gtype_inout(GObject.TYPE_NONE))
+            .toEqual(GObject.TYPE_INT);
+    });
+
+    it('can be implicitly converted from a JS type', function () {
+        expect(() => GIMarshallingTests.gtype_string_in(String)).not.toThrow();
+    });
+});
 
 function callback_return_value_only() {
     return 42;
@@ -407,27 +556,32 @@ function callback_return_value_and_multiple_out_parameters() {
     return [48, 49, 50];
 }
 
-function testCallbacks() {
-    let a, b, c;
-    a = GIMarshallingTests.callback_return_value_only(callback_return_value_only);
-    assertEquals(42, a);
+describe('Callback', function () {
+    it('marshals a return value', function () {
+        expect(GIMarshallingTests.callback_return_value_only(callback_return_value_only))
+            .toEqual(42);
+    });
 
-    a = GIMarshallingTests.callback_one_out_parameter(callback_one_out_parameter);
-    assertEquals(43, a);
+    it('marshals one out parameter', function () {
+        expect(GIMarshallingTests.callback_one_out_parameter(callback_one_out_parameter))
+            .toEqual(43);
+    });
 
-    [a, b] = GIMarshallingTests.callback_multiple_out_parameters(callback_multiple_out_parameters);
-    assertEquals(44, a);
-    assertEquals(45, b);
+    it('marshals multiple out parameters', function () {
+        expect(GIMarshallingTests.callback_multiple_out_parameters(callback_multiple_out_parameters))
+            .toEqual([44, 45]);
+    });
 
-    [a, b] = 
GIMarshallingTests.callback_return_value_and_one_out_parameter(callback_return_value_and_one_out_parameter);
-    assertEquals(46, a);
-    assertEquals(47, b);
+    it('marshals a return value and one out parameter', function () {
+        
expect(GIMarshallingTests.callback_return_value_and_one_out_parameter(callback_return_value_and_one_out_parameter))
+            .toEqual([46, 47]);
+    });
 
-    [a, b, c] = 
GIMarshallingTests.callback_return_value_and_multiple_out_parameters(callback_return_value_and_multiple_out_parameters);
-    assertEquals(48, a);
-    assertEquals(49, b);
-    assertEquals(50, c);
-}
+    it('marshals a return value and multiple out parameters', function () {
+        
expect(GIMarshallingTests.callback_return_value_and_multiple_out_parameters(callback_return_value_and_multiple_out_parameters))
+            .toEqual([48, 49, 50]);
+    });
+});
 
 const VFuncTester = new Lang.Class({
     Name: 'VFuncTester',
@@ -440,34 +594,39 @@ const VFuncTester = new Lang.Class({
     vfunc_vfunc_return_value_and_multiple_out_parameters: callback_return_value_and_multiple_out_parameters
 });
 
-function testVFuncs() {
-    let tester = new VFuncTester();
-    let a, b, c;
-    a = tester.vfunc_return_value_only();
-    assertEquals(42, a);
-
-    a = tester.vfunc_one_out_parameter();
-    assertEquals(43, a);
+describe('Virtual function', function () {
+    let tester;
+    beforeEach(function () {
+        tester = new VFuncTester();
+    });
 
-    [a, b] = tester.vfunc_multiple_out_parameters();
-    assertEquals(44, a);
-    assertEquals(45, b);
+    it('marshals a return value', function () {
+        expect(tester.vfunc_return_value_only()).toEqual(42);
+    });
 
-    [a, b] = tester.vfunc_return_value_and_one_out_parameter();
-    assertEquals(46, a);
-    assertEquals(47, b);
+    it('marshals one out parameter', function () {
+        expect(tester.vfunc_one_out_parameter()).toEqual(43);
+    });
 
-    [a, b, c] = tester.vfunc_return_value_and_multiple_out_parameters();
-    assertEquals(48, a);
-    assertEquals(49, b);
-    assertEquals(50, c);
-}
+    it('marshals multiple out parameters', function () {
+        expect(tester.vfunc_multiple_out_parameters()).toEqual([44, 45]);
+    });
 
-function testInterfaces() {
-    let ifaceImpl = new GIMarshallingTests.InterfaceImpl();
-    let itself = ifaceImpl.get_as_interface();
+    it('marshals a return value and one out parameter', function () {
+        expect(tester.vfunc_return_value_and_one_out_parameter())
+            .toEqual([46, 47]);
+    });
 
-    assertEquals(ifaceImpl, itself);
-}
+    it('marshals a return value and multiple out parameters', function () {
+        expect(tester.vfunc_return_value_and_multiple_out_parameters())
+            .toEqual([48, 49, 50]);
+    });
+});
 
-gjstestRun();
+describe('Interface', function () {
+    it('can be returned', function () {
+        let ifaceImpl = new GIMarshallingTests.InterfaceImpl();
+        let itself = ifaceImpl.get_as_interface();
+        expect(ifaceImpl).toEqual(itself);
+    });
+});
diff --git a/installed-tests/js/testGLib.js b/installed-tests/js/testGLib.js
index d66c14e..4651671 100644
--- a/installed-tests/js/testGLib.js
+++ b/installed-tests/js/testGLib.js
@@ -1,37 +1,43 @@
 const GLib = imports.gi.GLib;
-const JSUnit = imports.jsUnit;
 
-function testVariantConstructor() {
-    let str_variant = new GLib.Variant('s', 'mystring');
-    JSUnit.assertEquals('mystring', str_variant.get_string()[0]);
-    JSUnit.assertEquals('mystring', str_variant.deep_unpack());
+describe('GVariant constructor', function () {
+    it('constructs a string variant', function () {
+        let str_variant = new GLib.Variant('s', 'mystring');
+        expect(str_variant.get_string()[0]).toEqual('mystring');
+        expect(str_variant.deep_unpack()).toEqual('mystring');
+    });
 
-    let str_variant_old = GLib.Variant.new('s', 'mystring');
-    JSUnit.assertTrue(str_variant.equal(str_variant_old));
+    it('constructs a string variant (backwards compatible API)', function () {
+        let str_variant = new GLib.Variant('s', 'mystring');
+        let str_variant_old = GLib.Variant.new('s', 'mystring');
+        expect(str_variant.equal(str_variant_old)).toBeTruthy();
+    });
 
-    let struct_variant = new GLib.Variant('(sogvau)', [
-        'a string',
-        '/a/object/path',
-        'asig', //nature
-        new GLib.Variant('s', 'variant'),
-        [ 7, 3 ]
-    ]);
-    JSUnit.assertEquals(5, struct_variant.n_children());
+    it('constructs a struct variant', function () {
+        let struct_variant = new GLib.Variant('(sogvau)', [
+            'a string',
+            '/a/object/path',
+            'asig', //nature
+            new GLib.Variant('s', 'variant'),
+            [ 7, 3 ]
+        ]);
+        expect(struct_variant.n_children()).toEqual(5);
 
-    let unpacked = struct_variant.deep_unpack();
-    JSUnit.assertEquals('a string', unpacked[0]);
-    JSUnit.assertEquals('/a/object/path', unpacked[1]);
-    JSUnit.assertEquals('asig', unpacked[2]);
-    JSUnit.assertTrue(unpacked[3] instanceof GLib.Variant);
-    JSUnit.assertEquals('variant', unpacked[3].deep_unpack());
-    JSUnit.assertTrue(unpacked[4] instanceof Array);
-    JSUnit.assertEquals(2, unpacked[4].length);
+        let unpacked = struct_variant.deep_unpack();
+        expect(unpacked[0]).toEqual('a string');
+        expect(unpacked[1]).toEqual('/a/object/path');
+        expect(unpacked[2]).toEqual('asig');
+        expect(unpacked[3] instanceof GLib.Variant).toBeTruthy();
+        expect(unpacked[3].deep_unpack()).toEqual('variant');
+        expect(unpacked[4] instanceof Array).toBeTruthy();
+        expect(unpacked[4].length).toEqual(2);
+    });
 
-    let maybe_variant = new GLib.Variant('ms', null);
-    JSUnit.assertEquals(null, maybe_variant.deep_unpack());
+    it('constructs a maybe variant', function () {
+        let maybe_variant = new GLib.Variant('ms', null);
+        expect(maybe_variant.deep_unpack()).toBeNull();
 
-    maybe_variant = new GLib.Variant('ms', 'string');
-    JSUnit.assertEquals('string', maybe_variant.deep_unpack());
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+        maybe_variant = new GLib.Variant('ms', 'string');
+        expect(maybe_variant.deep_unpack()).toEqual('string');
+    });
+});
diff --git a/installed-tests/js/testGObjectClass.js b/installed-tests/js/testGObjectClass.js
index 77d1f0c..c967550 100644
--- a/installed-tests/js/testGObjectClass.js
+++ b/installed-tests/js/testGObjectClass.js
@@ -1,6 +1,5 @@
 // -*- mode: js; indent-tabs-mode: nil -*-
 
-const JSUnit = imports.jsUnit;
 const Lang = imports.lang;
 const GObject = imports.gi.GObject;
 const Gio = imports.gi.Gio;
@@ -144,7 +143,8 @@ const MyInitable = new Lang.Class({
     },
 
     vfunc_init: function(cancellable) { // error?
-        JSUnit.assertTrue(cancellable instanceof Gio.Cancellable);
+        if (!(cancellable instanceof Gio.Cancellable))
+            throw 'Bad argument';
 
         this.inited = true;
     }
@@ -167,8 +167,6 @@ const MyCustomInit = new Lang.Class({
         this.foo = false;
 
         this.parent();
-
-        JSUnit.assert(this.foo);
     },
 
     _instance_init: function() {
@@ -176,192 +174,184 @@ const MyCustomInit = new Lang.Class({
     }
 });
 
-function testGObjectClass() {
-    let myInstance = new MyObject();
-
-    JSUnit.assertEquals('foo', myInstance.readwrite);
-    JSUnit.assertEquals('bar', myInstance.readonly);
-    JSUnit.assertEquals('default', myInstance.construct);
+describe('GObject class', function () {
+    let myInstance;
+    beforeEach(function () {
+        myInstance = new MyObject();
+    });
 
-    let myInstance2 = new MyObject({ readwrite: 'baz', construct: 'asdf' });
+    it('constructs with default values for properties', function () {
+        expect(myInstance.readwrite).toEqual('foo');
+        expect(myInstance.readonly).toEqual('bar');
+        expect(myInstance.construct).toEqual('default');
+    });
 
-    JSUnit.assertEquals('baz', myInstance2.readwrite);
-    JSUnit.assertEquals('bar', myInstance2.readonly);
-    JSUnit.assertEquals('asdf', myInstance2.construct);
+    it('constructs with a hash of property values', function () {
+        let myInstance2 = new MyObject({ readwrite: 'baz', construct: 'asdf' });
+        expect(myInstance2.readwrite).toEqual('baz');
+        expect(myInstance2.readonly).toEqual('bar');
+        expect(myInstance2.construct).toEqual('asdf');
+    });
 
-    let ui = '<interface> \
+    const ui = '<interface> \
                 <object class="Gjs_MyObject" id="MyObject"> \
                   <property name="readwrite">baz</property> \
                   <property name="construct">quz</property> \
                 </object> \
               </interface>';
-    let builder = Gtk.Builder.new_from_string(ui, -1);
-    let myInstance3 = builder.get_object('MyObject');
-    JSUnit.assertEquals('baz', myInstance3.readwrite);
-    JSUnit.assertEquals('bar', myInstance3.readonly);
-    JSUnit.assertEquals('quz', myInstance3.construct);
+
+    it('constructs with property values from Gtk.Builder', function () {
+        let builder = Gtk.Builder.new_from_string(ui, -1);
+        let myInstance3 = builder.get_object('MyObject');
+        expect(myInstance3.readwrite).toEqual('baz');
+        expect(myInstance3.readonly).toEqual('bar');
+        expect(myInstance3.construct).toEqual('quz');
+    });
 
     // the following would (should) cause a CRITICAL:
     // myInstance.readonly = 'val';
     // myInstance.construct = 'val';
-}
-
-function testNotify() {
-    let myInstance = new MyObject();
-    let counter = 0;
 
-    myInstance.connect('notify::readonly', function(obj) {
-        if (obj.readonly == 'changed')
-            counter++;
-    });
+    it('has a notify signal', function () {
+        let notifySpy = jasmine.createSpy('notifySpy');
+        myInstance.connect('notify::readonly', notifySpy);
 
-    myInstance.notify_prop();
-    myInstance.notify_prop();
+        myInstance.notify_prop();
+        myInstance.notify_prop();
 
-    JSUnit.assertEquals(2, counter);
-}
+        expect(notifySpy).toHaveBeenCalledTimes(2);
+    });
 
-function testSignals() {
-    let myInstance = new MyObject();
-    let ok = false;
+    it('can define its own signals', function () {
+        let emptySpy = jasmine.createSpy('emptySpy');
+        myInstance.connect('empty', emptySpy);
+        myInstance.emit_empty();
 
-    myInstance.connect('empty', function() {
-        ok = true;
+        expect(emptySpy).toHaveBeenCalled();
+        expect(myInstance.empty_called).toBeTruthy();
     });
-    myInstance.emit_empty();
-
-    JSUnit.assertEquals(true, ok);
-    JSUnit.assertEquals(true, myInstance.empty_called);
 
-    let args = [ ];
-    myInstance.connect('minimal', function(emitter, one, two) {
-        args.push(one);
-        args.push(two);
+    it('passes emitted arguments to signal handlers', function () {
+        let minimalSpy = jasmine.createSpy('minimalSpy');
+        myInstance.connect('minimal', minimalSpy);
+        myInstance.emit_minimal(7, 5);
 
-        return true;
+        expect(minimalSpy).toHaveBeenCalledWith(myInstance, 7, 5);
     });
-    myInstance.emit_minimal(7, 5);
-
-    JSUnit.assertEquals(7, args[0]);
-    JSUnit.assertEquals(5, args[1]);
 
-    ok = true;
-    myInstance.connect('full', function() {
-        ok = true;
+    it('can return values from signals', function () {
+        let fullSpy = jasmine.createSpy('fullSpy').and.returnValue(42);
+        myInstance.connect('full', fullSpy);
+        let result = myInstance.emit_full();
 
-        return 42;
+        expect(fullSpy).toHaveBeenCalled();
+        expect(result).toEqual(42);
     });
-    myInstance.connect('full', function() {
-        // this should never be called
-        ok = false;
 
-        return -1;
+    it('does not call first-wins signal handlers after one returns a value', function () {
+        let neverCalledSpy = jasmine.createSpy('neverCalledSpy');
+        myInstance.connect('full', () => 42);
+        myInstance.connect('full', neverCalledSpy);
+        myInstance.emit_full();
+
+        expect(neverCalledSpy).not.toHaveBeenCalled();
+        expect(myInstance.full_default_handler_called).toBeFalsy();
     });
-    let result = myInstance.emit_full();
 
-    JSUnit.assertEquals(true, ok);
-    JSUnit.assertUndefined(myInstance.full_default_handler_called);
-    JSUnit.assertEquals(42, result);
+    it('gets the return value of the default handler', function () {
+        let result = myInstance.emit_full();
 
-    let stack = [ ];
-    myInstance.connect('run-last', function() {
-        stack.push(1);
-    });
-    myInstance.emit_run_last(function() {
-        stack.push(2);
+        expect(myInstance.full_default_handler_called).toBeTruthy();
+        expect(result).toEqual(79);
     });
 
-    JSUnit.assertEquals(1, stack[0]);
-    JSUnit.assertEquals(2, stack[1]);
-}
+    it('calls run-last default handler last', function () {
+        let stack = [ ];
+        let runLastSpy = jasmine.createSpy('runLastSpy')
+            .and.callFake(() => { stack.push(1); });
+        myInstance.connect('run-last', runLastSpy);
+        myInstance.emit_run_last(() => { stack.push(2); });
 
-function testSubclass() {
-    // test that we can inherit from something that's not
-    // GObject.Object and still get all the goodies of
-    // GObject.Class
+        expect(stack).toEqual([1, 2]);
+    });
 
-    let instance = new MyApplication({ application_id: 'org.gjs.Application' });
-    let v;
+    it("can inherit from something that's not GObject.Object", function () {
+        // ...and still get all the goodies of GObject.Class
+        let instance = new MyApplication({ application_id: 'org.gjs.Application' });
+        let customSpy = jasmine.createSpy('customSpy');
+        instance.connect('custom', customSpy);
 
-    instance.connect('custom', function(app, num) {
-        v = num;
+        instance.emit_custom(73);
+        expect(customSpy).toHaveBeenCalledWith(instance, 73);
     });
 
-    instance.emit_custom(73);
-    JSUnit.assertEquals(73, v);
-}
+    it('can implement an interface', function () {
+        let instance = new MyInitable();
+        expect(instance.constructor.implements(Gio.Initable)).toBeTruthy();
+    });
 
-function testInterface() {
-    let instance = new MyInitable();
-    JSUnit.assertEquals(false, instance.inited);
+    it('can implement interface vfuncs', function () {
+        let instance = new MyInitable();
+        expect(instance.inited).toBeFalsy();
 
-    instance.init(new Gio.Cancellable);
-    JSUnit.assertEquals(true, instance.inited);
+        instance.init(new Gio.Cancellable());
+        expect(instance.inited).toBeTruthy();
+    });
 
-    JSUnit.assertTrue(instance.constructor.implements(Gio.Initable));
-}
+    it('can be a subclass', function () {
+        let derived = new Derived();
 
-function testDerived() {
-    let derived = new Derived();
+        expect(derived instanceof Derived).toBeTruthy();
+        expect(derived instanceof MyObject).toBeTruthy();
 
-    JSUnit.assertTrue(derived instanceof Derived);
-    JSUnit.assertTrue(derived instanceof MyObject);
+        expect(derived.readwrite).toEqual('yes');
+    });
 
-    JSUnit.assertEquals('yes', derived.readwrite);
-}
+    it('calls its _instance_init() function while chaining up in constructor', function () {
+        let instance = new MyCustomInit();
+        expect(instance.foo).toBeTruthy();
+    });
 
-function testInstanceInit() {
-    new MyCustomInit();
-}
+    it('can have an interface-valued property', function () {
+        const InterfacePropObject = new Lang.Class({
+            Name: 'InterfacePropObject',
+            Extends: GObject.Object,
+            Properties: {
+                'file': GObject.ParamSpec.object('file', 'File', 'File',
+                    GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT_ONLY,
+                    Gio.File.$gtype)
+            },
+        });
+        let file = Gio.File.new_for_path('dummy');
+        expect(() => new InterfacePropObject({ file: file })).not.toThrow();
+    });
 
-function testClassCanHaveInterfaceProperty() {
-    const InterfacePropObject = new Lang.Class({
-        Name: 'InterfacePropObject',
-        Extends: GObject.Object,
-        Properties: {
-            'file': GObject.ParamSpec.object('file', 'File', 'File',
-                GObject.ParamFlags.READABLE | GObject.ParamFlags.WRITABLE | 
GObject.ParamFlags.CONSTRUCT_ONLY,
-                Gio.File.$gtype)
-        }
+    it('can override a property from the parent class', function () {
+        const OverrideObject = new Lang.Class({
+            Name: 'OverrideObject',
+            Extends: MyObject,
+            Properties: {
+                'readwrite': GObject.ParamSpec.override('readwrite', MyObject),
+            },
+            get readwrite() {
+                return this._subclass_readwrite;
+            },
+            set readwrite(val) {
+                this._subclass_readwrite = 'subclass' + val;
+            },
+        });
+        let obj = new OverrideObject();
+        obj.readwrite = 'foo';
+        expect(obj.readwrite).toEqual('subclassfoo');
     });
-    let obj = new InterfacePropObject({ file: Gio.File.new_for_path('dummy') });
-}
-
-function testClassCanOverrideParentClassProperty() {
-    const OverrideObject = new Lang.Class({
-        Name: 'OverrideObject',
-        Extends: MyObject,
-        Properties: {
-            'readwrite': GObject.ParamSpec.override('readwrite', MyObject)
-        },
-        get readwrite() {
-            return this._subclass_readwrite;
-        },
-        set readwrite(val) {
-            this._subclass_readwrite = 'subclass' + val;
-        }
+
+    it('cannot override a non-existent property', function () {
+        expect(() => new Lang.Class({
+            Name: 'BadOverride',
+            Extends: GObject.Object,
+            Properties: {
+                'nonexistent': GObject.ParamSpec.override('nonexistent', GObject.Object),
+            },
+        })).toThrow();
     });
-    let obj = new OverrideObject();
-    obj.readwrite = 'foo';
-    JSUnit.assertEquals(obj.readwrite, 'subclassfoo');
-}
-
-function testClassCannotOverrideNonexistentProperty() {
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'BadOverride',
-        Extends: GObject.Object,
-        Properties: {
-            'nonexistent': GObject.ParamSpec.override('nonexistent', GObject.Object)
-        }
-    }));
-}
-
-function testDefaultHandler() {
-    let myInstance = new MyObject();
-    let result = myInstance.emit_full();
-
-    JSUnit.assertEquals(true, myInstance.full_default_handler_called);
-    JSUnit.assertEquals(79, result);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+});
diff --git a/installed-tests/js/testGObjectInterface.js b/installed-tests/js/testGObjectInterface.js
index 1546198..792b611 100644
--- a/installed-tests/js/testGObjectInterface.js
+++ b/installed-tests/js/testGObjectInterface.js
@@ -3,7 +3,6 @@
 const Gio = imports.gi.Gio;
 const GLib = imports.gi.GLib;
 const GObject = imports.gi.GObject;
-const JSUnit = imports.jsUnit;
 const Lang = imports.lang;
 const Mainloop = imports.mainloop;
 
@@ -116,261 +115,275 @@ const ImplementationOfTwoInterfaces = new Lang.Class({
     }
 });
 
-function testGObjectClassCanImplementInterface() {
-    // Test will fail if the constructor throws an exception
-    let obj = new GObjectImplementingLangInterface();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-}
-
-function testRaisesWhenInterfaceRequiresGObjectInterfaceButNotGObject() {
-    JSUnit.assertRaises(() => new Lang.Interface({
-        Name: 'GObjectInterfaceNotRequiringGObject',
-        GTypeName: 'GTypeNameNotRequiringGObject',
-        Requires: [ Gio.Initable ]
-    }));
-}
-
-function testGObjectCanImplementInterfacesFromJSAndC() {
-    // Test will fail if the constructor throws an exception
-    const ObjectImplementingLangInterfaceAndCInterface = new Lang.Class({
-        Name: 'ObjectImplementingLangInterfaceAndCInterface',
-        Extends: GObject.Object,
-        Implements: [ AnInterface, Gio.Initable ],
-
-        _init: function (props={}) {
-            this.parent(props);
-        }
+describe('GObject interface', function () {
+    it('class can implement a Lang.Interface', function () {
+        let obj;
+        expect(() => { obj = new GObjectImplementingLangInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
     });
-    let obj = new ObjectImplementingLangInterfaceAndCInterface();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-    JSUnit.assertTrue(obj.constructor.implements(Gio.Initable));
-}
-
-function testGObjectInterfaceIsInstanceOfInterfaces() {
-    JSUnit.assertTrue(AGObjectInterface instanceof Lang.Interface);
-    JSUnit.assertTrue(AGObjectInterface instanceof GObject.Interface);
-}
-
-function testGObjectInterfaceCannotBeInstantiated() {
-    JSUnit.assertRaises(() => new AGObjectInterface());
-}
-
-function testGObjectInterfaceTypeName() {
-    JSUnit.assertEquals('ArbitraryGTypeName', AGObjectInterface.$gtype.name);
-}
-
-function testGObjectCanImplementInterface() {
-    // Test will fail if the constructor throws an exception
-    let obj = new GObjectImplementingGObjectInterface();
-    JSUnit.assertTrue(obj.constructor.implements(AGObjectInterface));
-}
-
-function testGObjectImplementingInterfaceHasCorrectClassObject() {
-    JSUnit.assertEquals('[object GObjectClass for GObjectImplementingGObjectInterface]', 
GObjectImplementingGObjectInterface.toString());
-    let obj = new GObjectImplementingGObjectInterface();
-    JSUnit.assertEquals(GObjectImplementingGObjectInterface, obj.constructor);
-    JSUnit.assertEquals('[object GObjectClass for GObjectImplementingGObjectInterface]',
-        obj.constructor.toString());
-}
-
-function testGObjectCanImplementBothGObjectAndNonGObjectInterfaces() {
-    // Test will fail if the constructor throws an exception
-    const GObjectImplementingBothKindsOfInterface = new Lang.Class({
-        Name: 'GObjectImplementingBothKindsOfInterface',
-        Extends: GObject.Object,
-        Implements: [ AnInterface, AGObjectInterface ],
-        Properties: {
-            'interface-prop': GObject.ParamSpec.override('interface-prop',
-                AGObjectInterface)
-        },
-
-        _init: function (props={}) {
-            this.parent(props);
-        },
-        required: function () {},
-        requiredG: function () {}
+
+    it('throws when an interface requires a GObject interface but not GObject.Object', function () {
+        expect(() => new Lang.Interface({
+            Name: 'GObjectInterfaceNotRequiringGObject',
+            GTypeName: 'GTypeNameNotRequiringGObject',
+            Requires: [ Gio.Initable ]
+        })).toThrow();
     });
-    let obj = new GObjectImplementingBothKindsOfInterface();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-    JSUnit.assertTrue(obj.constructor.implements(AGObjectInterface));
-}
-
-function testGObjectCanImplementRequiredFunction() {
-    // Test considered passing if no exception thrown
-    let obj = new GObjectImplementingGObjectInterface();
-    obj.requiredG();
-}
-
-function testGObjectMustImplementRequiredFunction () {
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'BadObject',
-        Extends: GObject.Object,
-        Implements: [ AGObjectInterface ],
-        Properties: {
-            'interface-prop': GObject.ParamSpec.override('interface-prop',
-                AGObjectInterface)
-        }
-    }));
-}
-
-function testGObjectDoesntHaveToImplementOptionalFunction() {
-    // Test will fail if the constructor throws an exception
-    let obj = new MinimalImplementationOfAGObjectInterface();
-    JSUnit.assertTrue(obj.constructor.implements(AGObjectInterface));
-}
-
-function testGObjectCanDeferToInterfaceOptionalFunction() {
-    let obj = new MinimalImplementationOfAGObjectInterface();
-    JSUnit.assertEquals('AGObjectInterface.optionalG()', obj.optionalG());
-}
-
-function testGObjectCanChainUpToInterface() {
-    let obj = new GObjectImplementingGObjectInterface();
-    JSUnit.assertEquals('AGObjectInterface.optionalG()', obj.optionalG());
-}
-
-function testGObjectInterfaceCanRequireOtherInterface() {
-    // Test will fail if the constructor throws an exception
-    let obj = new ImplementationOfTwoInterfaces();
-    JSUnit.assertTrue(obj.constructor.implements(AGObjectInterface));
-    JSUnit.assertTrue(obj.constructor.implements(InterfaceRequiringGObjectInterface));
-}
-
-function testGObjectInterfaceCanChainUpToOtherInterface() {
-    let obj = new ImplementationOfTwoInterfaces();
-    JSUnit.assertEquals('InterfaceRequiringGObjectInterface.optionalG()\nAGObjectInterface.optionalG()',
-        obj.optionalG());
-}
-
-function testGObjectDefersToLastInterfaceOptionalFunction() {
-    const MinimalImplementationOfTwoInterfaces = new Lang.Class({
-        Name: 'MinimalImplementationOfTwoInterfaces',
-        Extends: GObject.Object,
-        Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ],
-        Properties: {
-            'interface-prop': GObject.ParamSpec.override('interface-prop',
-                AGObjectInterface)
-        },
-
-        _init: function (props={}) {
-            this.parent(props);
-        },
-        requiredG: function () {}
+
+    it('can be implemented by a GObject class along with a JS interface', function () {
+        const ObjectImplementingLangInterfaceAndCInterface = new Lang.Class({
+            Name: 'ObjectImplementingLangInterfaceAndCInterface',
+            Extends: GObject.Object,
+            Implements: [ AnInterface, Gio.Initable ],
+
+            _init: function (props={}) {
+                this.parent(props);
+            }
+        });
+        let obj;
+        expect(() => { obj = new ObjectImplementingLangInterfaceAndCInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+        expect(obj.constructor.implements(Gio.Initable)).toBeTruthy();
     });
-    let obj = new MinimalImplementationOfTwoInterfaces();
-    JSUnit.assertEquals('InterfaceRequiringGObjectInterface.optionalG()\nAGObjectInterface.optionalG()',
-        obj.optionalG());
-}
-
-function testGObjectClassMustImplementAllRequiredInterfaces() {
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'BadObject',
-        Implements: [ InterfaceRequiringGObjectInterface ],
-        required: function () {}
-    }));
-}
-
-function testGObjectClassMustImplementRequiredInterfacesInCorrectOrder() {
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'BadObject',
-        Implements: [ InterfaceRequiringGObjectInterface, AGObjectInterface ],
-        required: function () {}
-    }));
-}
-
-function testGObjectInterfaceCanRequireInterfaceFromC() {
-    const InitableInterface = new Lang.Interface({
-        Name: 'InitableInterface',
-        Requires: [ GObject.Object, Gio.Initable ]
+
+    it('is an instance of the interface classes', function () {
+        expect(AGObjectInterface instanceof Lang.Interface).toBeTruthy();
+        expect(AGObjectInterface instanceof GObject.Interface).toBeTruthy();
     });
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'BadObject',
-        Implements: [ InitableInterface ]
-    }));
-}
-
-function testGObjectHasInterfaceSignalsAndClassSignals() {
-    let obj = new GObjectImplementingGObjectInterface();
-    let interface_signal_emitted = false, class_signal_emitted = false;
-    obj.connect('interface-signal', () => {
-        interface_signal_emitted = true;
-        Mainloop.quit('signal');
+
+    it('cannot be instantiated', function () {
+        expect(() => new AGObjectInterface()).toThrow();
     });
-    obj.connect('class-signal', () => {
-        class_signal_emitted = true;
-        Mainloop.quit('signal');
+
+    it('reports its type name', function () {
+        expect(AGObjectInterface.$gtype.name).toEqual('ArbitraryGTypeName');
     });
-    GLib.idle_add(GLib.PRIORITY_DEFAULT, () => obj.emit('interface-signal'));
-    Mainloop.run('signal');
-    GLib.idle_add(GLib.PRIORITY_DEFAULT, () => obj.emit('class-signal'));
-    Mainloop.run('signal');
-    JSUnit.assertTrue(interface_signal_emitted);
-    JSUnit.assertTrue(class_signal_emitted);
-}
-
-function testGObjectHasInterfacePropertiesAndClassProperties() {
-    let obj = new GObjectImplementingGObjectInterface();
-    JSUnit.assertEquals('foobar', obj.interface_prop);
-    JSUnit.assertEquals('meh', obj.class_prop);
-}
-
-// Failing to override an interface property doesn't raise an error but instead
-// logs a critical warning.
-function testGObjectMustOverrideInterfaceProperties() {
-    GLib.test_expect_message('GLib-GObject', GLib.LogLevelFlags.LEVEL_CRITICAL,
-        "Object class * doesn't implement property 'interface-prop' from " +
-        "interface 'ArbitraryGTypeName'");
-    new Lang.Class({
-        Name: 'MyNaughtyObject',
-        Extends: GObject.Object,
-        Implements: [ AGObjectInterface ],
-        _init: function (props={}) {
-            this.parent(props);
-        },
-        requiredG: function () {}
+
+    it('can be implemented by a GObject class', function () {
+        let obj;
+        expect(() => { obj = new GObjectImplementingGObjectInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy();
     });
-    // g_test_assert_expected_messages() is a macro, not introspectable
-    GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectInterface.js',
-        416, 'testGObjectMustOverrideInterfaceProperties');
-}
-
-// This makes sure that we catch the case where the metaclass (e.g.
-// GtkWidgetClass) doesn't specify a meta-interface. In that case we get the
-// meta-interface from the metaclass's parent.
-function testInterfaceIsOfCorrectTypeForMetaclass() {
-    const MyMeta = new Lang.Class({
-        Name: 'MyMeta',
-        Extends: GObject.Class
+
+    it('is implemented by a GObject class with the correct class object', function () {
+        expect(GObjectImplementingGObjectInterface.toString())
+            .toEqual('[object GObjectClass for GObjectImplementingGObjectInterface]');
+        let obj = new GObjectImplementingGObjectInterface();
+        expect(obj.constructor).toEqual(GObjectImplementingGObjectInterface);
+        expect(obj.constructor.toString())
+            .toEqual('[object GObjectClass for GObjectImplementingGObjectInterface]');
     });
-    const MyMetaObject = new MyMeta({
-        Name: 'MyMetaObject'
+
+    it('can be implemented by a class also implementing a Lang.Interface', function () {
+        const GObjectImplementingBothKindsOfInterface = new Lang.Class({
+            Name: 'GObjectImplementingBothKindsOfInterface',
+            Extends: GObject.Object,
+            Implements: [ AnInterface, AGObjectInterface ],
+            Properties: {
+                'interface-prop': GObject.ParamSpec.override('interface-prop',
+                    AGObjectInterface)
+            },
+
+            _init: function (props={}) {
+                this.parent(props);
+            },
+            required: function () {},
+            requiredG: function () {}
+        });
+        let obj;
+        expect(() => { obj = new GObjectImplementingBothKindsOfInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+        expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy();
     });
-    const MyMetaInterface = new Lang.Interface({
-        Name: 'MyMetaInterface',
-        Requires: [ MyMetaObject ]
+
+    it('can have its required function implemented', function () {
+        expect(() => {
+            let obj = new GObjectImplementingGObjectInterface();
+            obj.requiredG();
+        }).not.toThrow();
     });
-    JSUnit.assertTrue(MyMetaInterface instanceof GObject.Interface);
-}
 
-function testSubclassImplementsTheSameInterfaceAsItsParent() {
-    const SubObject = new Lang.Class({
-        Name: 'SubObject',
-        Extends: GObjectImplementingGObjectInterface
+    it('must have its required function implemented', function () {
+        expect(() => new Lang.Class({
+            Name: 'BadObject',
+            Extends: GObject.Object,
+            Implements: [ AGObjectInterface ],
+            Properties: {
+                'interface-prop': GObject.ParamSpec.override('interface-prop',
+                    AGObjectInterface)
+            }
+        })).toThrow();
     });
-    let obj = new SubObject();
-    JSUnit.assertTrue(obj.constructor.implements(AGObjectInterface));
-    JSUnit.assertEquals('foobar', obj.interface_prop);  // override not needed
-}
-
-function testSubclassCanReimplementTheSameInterfaceAsItsParent() {
-    const SubImplementer = new Lang.Class({
-        Name: 'SubImplementer',
-        Extends: GObjectImplementingGObjectInterface,
-        Implements: [ AGObjectInterface ]
+
+    it("doesn't have to have its optional function implemented", function () {
+        let obj;
+        expect(() => { obj = new MinimalImplementationOfAGObjectInterface(); })
+            .not.toThrow();
+        expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy();
+    });
+
+    it('can have its optional function deferred to by the implementation', function () {
+        let obj = new MinimalImplementationOfAGObjectInterface();
+        expect(obj.optionalG()).toEqual('AGObjectInterface.optionalG()');
+    });
+
+    it('can have its function chained up to', function () {
+        let obj = new GObjectImplementingGObjectInterface();
+        expect(obj.optionalG()).toEqual('AGObjectInterface.optionalG()');
     });
-    let obj = new SubImplementer();
-    JSUnit.assertTrue(obj.constructor.implements(AGObjectInterface));
-    JSUnit.assertEquals('foobar', obj.interface_prop);  // override not needed
-}
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('can require another interface', function () {
+        let obj;
+        expect(() => { obj = new ImplementationOfTwoInterfaces(); }).not.toThrow();
+        expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy();
+        expect(obj.constructor.implements(InterfaceRequiringGObjectInterface))
+            .toBeTruthy();
+    });
+
+    it('can chain up to another interface', function () {
+        let obj = new ImplementationOfTwoInterfaces();
+        expect(obj.optionalG())
+            .toEqual('InterfaceRequiringGObjectInterface.optionalG()\nAGObjectInterface.optionalG()');
+    });
+
+    it("defers to the last interface's optional function", function () {
+        const MinimalImplementationOfTwoInterfaces = new Lang.Class({
+            Name: 'MinimalImplementationOfTwoInterfaces',
+            Extends: GObject.Object,
+            Implements: [ AGObjectInterface, InterfaceRequiringGObjectInterface ],
+            Properties: {
+                'interface-prop': GObject.ParamSpec.override('interface-prop',
+                    AGObjectInterface)
+            },
+
+            _init: function (props={}) {
+                this.parent(props);
+            },
+            requiredG: function () {}
+        });
+        let obj = new MinimalImplementationOfTwoInterfaces();
+        expect(obj.optionalG())
+            .toEqual('InterfaceRequiringGObjectInterface.optionalG()\nAGObjectInterface.optionalG()');
+    });
+
+    it('must be implemented by a class that implements all required interfaces', function () {
+        expect(() => new Lang.Class({
+            Name: 'BadObject',
+            Implements: [ InterfaceRequiringGObjectInterface ],
+            required: function () {}
+        })).toThrow();
+    });
+
+    it('must be implemented by a class that implements required interfaces in correct order', function () {
+        expect(() => new Lang.Class({
+            Name: 'BadObject',
+            Implements: [ InterfaceRequiringGObjectInterface, AGObjectInterface ],
+            required: function () {}
+        })).toThrow();
+    });
+
+    it('can require an interface from C', function () {
+        const InitableInterface = new Lang.Interface({
+            Name: 'InitableInterface',
+            Requires: [ GObject.Object, Gio.Initable ]
+        });
+        expect(() => new Lang.Class({
+            Name: 'BadObject',
+            Implements: [ InitableInterface ]
+        })).toThrow();
+    });
+
+    it('can define signals on the implementing class', function () {
+        function quitLoop() {
+            Mainloop.quit('signal');
+        }
+        let obj = new GObjectImplementingGObjectInterface();
+        let interfaceSignalSpy = jasmine.createSpy('interfaceSignalSpy')
+            .and.callFake(quitLoop);
+        let classSignalSpy = jasmine.createSpy('classSignalSpy')
+            .and.callFake(quitLoop);
+        obj.connect('interface-signal', interfaceSignalSpy);
+        obj.connect('class-signal', classSignalSpy);
+        GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+            obj.emit('interface-signal');
+            return GLib.SOURCE_REMOVE;
+        });
+        Mainloop.run('signal');
+        GLib.idle_add(GLib.PRIORITY_DEFAULT, () => {
+            obj.emit('class-signal');
+            return GLib.SOURCE_REMOVE;
+        });
+        Mainloop.run('signal');
+        expect(interfaceSignalSpy).toHaveBeenCalled();
+        expect(classSignalSpy).toHaveBeenCalled();
+    });
+
+    it('can define properties on the implementing class', function () {
+        let obj = new GObjectImplementingGObjectInterface();
+        expect(obj.interface_prop).toEqual('foobar');
+        expect(obj.class_prop).toEqual('meh');
+    });
+
+    it('must have its properties overridden', function () {
+        // Failing to override an interface property doesn't raise an error but
+        // instead logs a critical warning.
+        GLib.test_expect_message('GLib-GObject', GLib.LogLevelFlags.LEVEL_CRITICAL,
+            "Object class * doesn't implement property 'interface-prop' from " +
+            "interface 'ArbitraryGTypeName'");
+        new Lang.Class({
+            Name: 'MyNaughtyObject',
+            Extends: GObject.Object,
+            Implements: [ AGObjectInterface ],
+            _init: function (props={}) {
+                this.parent(props);
+            },
+            requiredG: function () {}
+        });
+        // g_test_assert_expected_messages() is a macro, not introspectable
+        GLib.test_assert_expected_messages_internal('Gjs', 'testGObjectInterface.js',
+            416, 'testGObjectMustOverrideInterfaceProperties');
+    });
+
+    // This makes sure that we catch the case where the metaclass (e.g.
+    // GtkWidgetClass) doesn't specify a meta-interface. In that case we get the
+    // meta-interface from the metaclass's parent.
+    it('gets the correct type for its metaclass', function () {
+        const MyMeta = new Lang.Class({
+            Name: 'MyMeta',
+            Extends: GObject.Class
+        });
+        const MyMetaObject = new MyMeta({
+            Name: 'MyMetaObject'
+        });
+        const MyMetaInterface = new Lang.Interface({
+            Name: 'MyMetaInterface',
+            Requires: [ MyMetaObject ]
+        });
+        expect(MyMetaInterface instanceof GObject.Interface).toBeTruthy();
+    });
+
+    it('can be implemented by a class as well as its parent class', function () {
+        const SubObject = new Lang.Class({
+            Name: 'SubObject',
+            Extends: GObjectImplementingGObjectInterface
+        });
+        let obj = new SubObject();
+        expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy();
+        expect(obj.interface_prop).toEqual('foobar');  // override not needed
+    });
+
+    it('can be reimplemented by a subclass of a class that already implements it', function () {
+        const SubImplementer = new Lang.Class({
+            Name: 'SubImplementer',
+            Extends: GObjectImplementingGObjectInterface,
+            Implements: [ AGObjectInterface ]
+        });
+        let obj = new SubImplementer();
+        expect(obj.constructor.implements(AGObjectInterface)).toBeTruthy();
+        expect(obj.interface_prop).toEqual('foobar');  // override not needed
+    });
+});
diff --git a/installed-tests/js/testGTypeClass.js b/installed-tests/js/testGTypeClass.js
index 4375966..5bcf748 100644
--- a/installed-tests/js/testGTypeClass.js
+++ b/installed-tests/js/testGTypeClass.js
@@ -1,55 +1,64 @@
-// application/javascript;version=1.8
-
-const JSUnit = imports.jsUnit;
-const Everything = imports.gi.Regress;
-const WarnLib = imports.gi.WarnLib;
-
 // We use Gio to have some objects that we know exist
-const GLib = imports.gi.GLib;
 const Gio = imports.gi.Gio;
 const GObject = imports.gi.GObject;
 
-function testGObjectClass() {
-    let find_property = GObject.Object.find_property;
-
-    let p1 = find_property.call(Gio.ThemedIcon, 'name');
+describe('Looking up param specs', function () {
+    let p1, p2;
+    beforeEach(function () {
+        let find_property = GObject.Object.find_property;
+        p1 = find_property.call(Gio.ThemedIcon, 'name');
+        p2 = find_property.call(Gio.SimpleAction, 'enabled');
+    });
 
-    JSUnit.assert(p1 instanceof GObject.ParamSpec);
-    JSUnit.assertEquals('name', p1.name);
+    it('works', function () {
+        expect(p1 instanceof GObject.ParamSpec).toBeTruthy();
+        expect(p2 instanceof GObject.ParamSpec).toBeTruthy();
+    });
 
-    let p2 = find_property.call(Gio.SimpleAction, 'enabled');
+    it('gives the correct name', function () {
+        expect(p1.name).toEqual('name');
+        expect(p2.name).toEqual('enabled');
+    });
 
-    JSUnit.assert(p2 instanceof GObject.ParamSpec);
-    JSUnit.assertEquals('enabled', p2.name);
-    JSUnit.assertEquals(true, p2.default_value);
-}
+    it('gives the default value if present', function () {
+        expect(p2.default_value).toBeTruthy();
+    });
+});
 
-function testGType() {
-    JSUnit.assertEquals("void", GObject.TYPE_NONE.name);
-    JSUnit.assertEquals("gchararray", GObject.TYPE_STRING.name);
+describe('GType object', function () {
+    it('has a name', function () {
+        expect(GObject.TYPE_NONE.name).toEqual('void');
+        expect(GObject.TYPE_STRING.name).toEqual('gchararray');
+    });
 
-    // Make sure "name" is readonly
-    try {
-        GObject.TYPE_STRING.name = "foo";
-    } catch(e) {
-    }
-    JSUnit.assertEquals("gchararray", GObject.TYPE_STRING.name);
+    it('has a read-only name', function () {
+        try {
+            GObject.TYPE_STRING.name = 'foo';
+        } catch (e) {
+        }
+        expect(GObject.TYPE_STRING.name).toEqual('gchararray');
+    });
 
-    // Make sure "name" is permanent
-    try {
-        delete GObject.TYPE_STRING.name;
-    } catch(e) {
-    }
-    JSUnit.assertEquals("gchararray", GObject.TYPE_STRING.name);
+    it('has an undeletable name', function () {
+        try {
+            delete GObject.TYPE_STRING.name;
+        } catch (e) {
+        }
+        expect(GObject.TYPE_STRING.name).toEqual('gchararray');
+    });
 
-    // Make sure "toString" works
-    JSUnit.assertEquals("[object GType for 'void']", GObject.TYPE_NONE.toString());
-    JSUnit.assertEquals("[object GType for 'gchararray']", GObject.TYPE_STRING.toString());
-}
+    it('has a string representation', function () {
+        expect(GObject.TYPE_NONE.toString()).toEqual("[object GType for 'void']");
+        expect(GObject.TYPE_STRING.toString()).toEqual("[object GType for 'gchararray']");
+    });
+});
 
-function testGTypePrototype() {
-    JSUnit.assertNull(GIRepositoryGType.name);
-    JSUnit.assertEquals("[object GType prototype]", GIRepositoryGType.toString());
-}
+describe('GType prototype object', function () {
+    it('has no name', function () {
+        expect(GIRepositoryGType.name).toBeNull();
+    });
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('has a string representation', function () {
+        expect(GIRepositoryGType.toString()).toEqual('[object GType prototype]');
+    });
+});
diff --git a/installed-tests/js/testGettext.js b/installed-tests/js/testGettext.js
index 31f077e..2496292 100644
--- a/installed-tests/js/testGettext.js
+++ b/installed-tests/js/testGettext.js
@@ -1,15 +1,14 @@
 // -*- mode: js; indent-tabs-mode: nil -*-
 
-// Tests for the Gettext module.
 const Gettext = imports.gettext;
-const JSUnit = imports.jsUnit;
 
-function testSetlocale() {
+describe('Gettext module', function () {
     // We don't actually want to mess with the locale, so just use setlocale's
     // query mode. We also don't want to make this test locale-dependent, so
     // just assert that it returns a string with at least length 1 (the shortest
     // locale is "C".)
-    JSUnit.assert(Gettext.setlocale(Gettext.LocaleCategory.ALL, null).length >= 1);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('setlocale returns a locale', function () {
+        let locale = Gettext.setlocale(Gettext.LocaleCategory.ALL, null);
+        expect(locale.length).not.toBeLessThan(1);
+    });
+});
diff --git a/installed-tests/js/testGtk.js b/installed-tests/js/testGtk.js
index e8c42fa..6848548 100755
--- a/installed-tests/js/testGtk.js
+++ b/installed-tests/js/testGtk.js
@@ -1,13 +1,6 @@
-#!/usr/bin/env gjs
-
 const ByteArray = imports.byteArray;
-const Gdk = imports.gi.Gdk;
-const Gio = imports.gi.Gio;
 const Gtk = imports.gi.Gtk;
 const Lang = imports.lang;
-const System = imports.system;
-
-const JSUnit = imports.jsUnit;
 
 // This is ugly here, but usually it would be in a resource
 const template = ' \
@@ -47,17 +40,16 @@ const MyComplexGtkSubclass = new Lang.Class({
     InternalChildren: ['internal-label-child'],
     CssName: 'complex-subclass',
 
-    _init: function(params) {
-        this.parent(params);
+    // _init: function(params) {
+    //     this.parent(params);
+    // },
 
+    testChildrenExist: function () {
         this._internalLabel = this.get_template_child(MyComplexGtkSubclass, 'label-child');
-        JSUnit.assertNotEquals(this._internalLabel, null);
+        expect(this._internalLabel).not.toBeNull();
 
-        JSUnit.assertNotEquals(this.label_child2, null);
-        JSUnit.assertNotEquals(this._internal_label_child, null);
-
-        JSUnit.assertEquals(Gtk.Widget.get_css_name.call(MyComplexGtkSubclass),
-                            'complex-subclass');
+        expect(this.label_child2).not.toBeNull();
+        expect(this._internal_label_child).not.toBeNull();
     }
 });
 
@@ -69,32 +61,55 @@ const MyComplexGtkSubclassFromResource = new Lang.Class({
     Children: ['label-child', 'label-child2'],
     InternalChildren: ['internal-label-child'],
 
-    _init: function(params) {
-        this.parent(params);
+    // _init: function(params) {
+    //     this.parent(params);
+    // },
 
-        this._internalLabel = this.label_child;
-        JSUnit.assertNotEquals(this.label_child, null);
-        JSUnit.assertNotEquals(this.label_child2, null);
-        JSUnit.assertNotEquals(this._internal_label_child, null);
+    testChildrenExist: function () {
+        expect(this.label_child).not.toBeNull();
+        expect(this.label_child2).not.toBeNull();
+        expect(this._internal_label_child).not.toBeNull();
     }
 });
 
-function validateTemplate(content) {
-    let win = new Gtk.Window({ type: Gtk.WindowType.TOPLEVEL });
-    win.add(content);
-
-    JSUnit.assertEquals("label is set to 'Complex!'", 'Complex!', content._internalLabel.get_label());
-    JSUnit.assertEquals("label is set to 'Complex as well!'", 'Complex as well!', 
content.label_child2.get_label());
-    JSUnit.assertEquals("label is set to 'Complex and internal!'", 'Complex and internal!', 
content._internal_label_child.get_label());
-
-    win.destroy();
+function validateTemplate(description, ClassName) {
+    describe(description, function () {
+        let win, content;
+        beforeEach(function () {
+            win = new Gtk.Window({ type: Gtk.WindowType.TOPLEVEL });
+            content = new ClassName();
+            win.add(content);
+        });
+
+        it('sets up internal and public template children', function () {
+            content.testChildrenExist();
+        });
+
+        it('sets up public template children with the correct widgets', function () {
+            expect(content.label_child.get_label()).toEqual('Complex!');
+            expect(content.label_child2.get_label()).toEqual('Complex as well!');
+        });
+
+        it('sets up internal template children with the correct widgets', function () {
+            expect(content._internal_label_child.get_label())
+                .toEqual('Complex and internal!');
+        });
+
+        afterEach(function () {
+            win.destroy();
+        });
+    });
 }
 
-function testGtk() {
-    Gtk.init(null);
+describe('Gtk overrides', function () {
+    beforeAll(function () {
+        Gtk.init(null);
+    });
 
-    validateTemplate(new MyComplexGtkSubclass());
-    validateTemplate(new MyComplexGtkSubclassFromResource());
-}
+    validateTemplate('UI template', MyComplexGtkSubclass);
+    validateTemplate('UI template from resource', MyComplexGtkSubclassFromResource);
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('sets CSS names on classes', function () {
+        expect(Gtk.Widget.get_css_name.call(MyComplexGtkSubclass)).toEqual('complex-subclass');
+    });
+});
diff --git a/installed-tests/js/testImporter.js b/installed-tests/js/testImporter.js
index 8e3f9f0..f2c649a 100644
--- a/installed-tests/js/testImporter.js
+++ b/installed-tests/js/testImporter.js
@@ -1,120 +1,117 @@
-const JSUnit = imports.jsUnit;
-
-function testImporterGI() {
-    var GLib = imports.gi.GLib;
-    JSUnit.assertEquals(GLib.MAJOR_VERSION, 2);
-}
-
-function testImporter() {
-    JSUnit.assertNotUndefined(imports);
-
-    JSUnit.assertRaises(() => imports.nonexistentModuleName);
-    JSUnit.assertRaises(() => imports.alwaysThrows);
-
-    // Try again to make sure that we properly discarded the module object
-    JSUnit.assertRaises(() => imports.alwaysThrows);
-
-    // Import a non-broken module
-    const foobar = imports.foobar;
-    JSUnit.assertNotUndefined(foobar);
-    JSUnit.assertNotUndefined(foobar.foo);
-    JSUnit.assertNotUndefined(foobar.bar);
-    JSUnit.assertEquals(foobar.foo, "This is foo");
-    JSUnit.assertEquals(foobar.bar, "This is bar");
-
-    // Check that deleting the import is a no-op (imported properties are
-    // permanent)
-    JSUnit.assertFalse(delete imports.foobar);
-    JSUnit.assert(imports.foobar == foobar);
-
-    // check that importing a second time gets the same object
-    foobar.somethingElse = "Should remain";
-    const foobar2 = imports.foobar;
-    JSUnit.assertNotUndefined(foobar2.somethingElse);
-    JSUnit.assertEquals(foobar2.somethingElse, "Should remain");
-
-    // Try a sub-module
-    const subB = imports.subA.subB;
-    JSUnit.assertNotUndefined(subB);
-    const subFoobar = subB.foobar;
-    JSUnit.assertNotUndefined(subFoobar);
-    JSUnit.assertNotUndefined(subFoobar.foo);
-    JSUnit.assertNotUndefined(subFoobar.bar);
-    JSUnit.assertEquals(subFoobar.foo, "This is foo");
-    JSUnit.assertEquals(subFoobar.bar, "This is bar");
-    // subFoobar should not be the same as foobar, even though
-    // they have the same basename.
-    JSUnit.assertUndefined(subFoobar.somethingElse);
-    // importing subFoobar a second time should get the same module
-    subFoobar.someProp = "Should be here";
-    const subFoobar2 = imports.subA.subB.foobar;
-    JSUnit.assertNotUndefined(subFoobar2.someProp);
-    JSUnit.assertEquals(subFoobar2.someProp, "Should be here");
-}
-
-function testImporterMetaProps() {
-    const subA = imports.subA;
-    const subB = imports.subA.subB;
-
-    JSUnit.assertNull('imports module name', imports.__moduleName__);
-    JSUnit.assertNull('imports has no parent', imports.__parentModule__);
-    JSUnit.assertEquals('imports.subA module name', 'subA', subA.__moduleName__);
-    JSUnit.assertEquals('imports.subA parent module', imports, subA.__parentModule__);
-    JSUnit.assertEquals('imports.subA.subB module name', 'subB', subB.__moduleName__);
-    JSUnit.assertEquals('imports.subA.subB parent module', subA, subB.__parentModule__);
-}
-
-function testMutualImport() {
+describe('GI importer', function () {
+    it('can import GI modules', function () {
+        var GLib = imports.gi.GLib;
+        expect(GLib.MAJOR_VERSION).toEqual(2);
+    });
+});
+
+describe('Importer', function () {
+    let oldSearchPath;
+    let foobar, subA, subB, subFoobar;
+
+    beforeAll(function () {
+        oldSearchPath = imports.searchPath.slice();
+        imports.searchPath = ['resource:///org/gjs/jsunit/modules'];
+
+        foobar = imports.foobar;
+        subA = imports.subA;
+        subB = imports.subA.subB;
+        subFoobar = subB.foobar;
+    });
+
+    afterAll(function () {
+        imports.searchPath = oldSearchPath;
+    });
+
+    it('exists', function () {
+        expect(imports).toBeDefined();
+    });
+
+    it('throws an error when trying to import a nonexistent module', function () {
+        expect(() => imports.nonexistentModuleName).toThrow();
+    });
+
+    it('throws an error when evaluating the module file throws an error', function () {
+        expect(() => imports.alwaysThrows).toThrow();
+        // Try again to make sure that we properly discarded the module object
+        expect(() => imports.alwaysThrows).toThrow();
+    });
+
+    it('can import a module', function () {
+        expect(foobar).toBeDefined();
+        expect(foobar.foo).toEqual('This is foo');
+        expect(foobar.bar).toEqual('This is bar');
+    });
+
+    it('makes deleting the import a no-op', function () {
+        expect(delete imports.foobar).toBeFalsy();
+        expect(imports.foobar).toBe(foobar);
+    });
+
+    it('gives the same object when importing a second time', function () {
+        foobar.somethingElse = 'Should remain';
+        const foobar2 = imports.foobar;
+        expect(foobar2.somethingElse).toEqual('Should remain');
+    });
+
+    it('can import a submodule', function () {
+        expect(subB).toBeDefined();
+        expect(subFoobar).toBeDefined();
+        expect(subFoobar.foo).toEqual('This is foo');
+        expect(subFoobar.bar).toEqual('This is bar');
+    });
+
+    it('does not share the same object for a module on a different path', function () {
+        foobar.somethingElse = 'Should remain';
+        expect(subFoobar.somethingElse).not.toBeDefined();
+    });
+
+    it('gives the same object when importing a submodule a second time', function () {
+        subFoobar.someProp = 'Should be here';
+        const subFoobar2 = imports.subA.subB.foobar;
+        expect(subFoobar2.someProp).toEqual('Should be here');
+    });
+
+    it('has no meta properties on the toplevel importer', function () {
+        expect(imports.__moduleName__).toBeNull();
+        expect(imports.__parentModule__).toBeNull();
+    });
+
+    it('sets the names of imported modules', function () {
+        expect(subA.__moduleName__).toEqual('subA');
+        expect(subB.__moduleName__).toEqual('subB');
+    });
+
+    it('gives a module the importer object as parent module', function () {
+        expect(subA.__parentModule__).toBe(imports);
+    });
+
+    it('gives a submodule the module as parent module', function () {
+        expect(subB.__parentModule__).toBe(subA);
+    });
+
     // We want to check that the copy of the 'a' module imported directly
     // is the same as the copy that 'b' imports, and that we don't have two
     // copies because of the A imports B imports A loop.
-
-    let A = imports.mutualImport.a;
-    A.incrementCount();
-    JSUnit.assertEquals(1, A.getCount());
-    JSUnit.assertEquals(1, A.getCountViaB());
-}
-
-function testImporterFunctionFromInitFile() {
-    const subB = imports.subA.subB;
-
-    JSUnit.assertNotUndefined(subB.testImporterFunction);
-
-    let result = subB.testImporterFunction();
-
-    JSUnit.assertEquals(result, "__init__ function tested");
-}
-
-function testImporterClassFromInitFile() {
-    const subB = imports.subA.subB;
-
-    JSUnit.assertNotUndefined(subB.ImporterClass);
-
-    let o = new subB.ImporterClass();
-
-    JSUnit.assertNotNull(o);
-
-    let result = o.testMethod();
-
-    JSUnit.assertEquals(result, "__init__ class tested");
-}
-
-function testImporterUTF8() {
-    const ModUnicode = imports.modunicode;
-    JSUnit.assertEquals(ModUnicode.uval, "const \u2665 utf8");
-}
-
-let oldSearchPath;
-
-function setUp() {
-    JSUnit.setUp();
-    oldSearchPath = imports.searchPath.slice();
-    imports.searchPath = ['resource:///org/gjs/jsunit/modules'];
-}
-
-function tearDown() {
-    JSUnit.tearDown();
-    imports.searchPath = oldSearchPath;
-}
-
-JSUnit.gjstestRun(this, setUp, tearDown);
+    it('does not make a separate copy of a module imported in two places', function () {
+        let A = imports.mutualImport.a;
+        A.incrementCount();
+        expect(A.getCount()).toEqual(1);
+        expect(A.getCountViaB()).toEqual(1);
+    });
+
+    it('evaluates an __init__.js file in an imported directory', function () {
+        expect(subB.testImporterFunction()).toEqual('__init__ function tested');
+    });
+
+    it('accesses a class defined in an __init__.js file', function () {
+        let o = new subB.ImporterClass();
+        expect(o).not.toBeNull();
+        expect(o.testMethod()).toEqual('__init__ class tested');
+    });
+
+    it('can import a file encoded in UTF-8', function () {
+        const ModUnicode = imports.modunicode;
+        expect(ModUnicode.uval).toEqual('const \u2665 utf8');
+    });
+});
diff --git a/installed-tests/js/testInterface.js b/installed-tests/js/testInterface.js
index 2af1171..08f3b94 100644
--- a/installed-tests/js/testInterface.js
+++ b/installed-tests/js/testInterface.js
@@ -1,6 +1,5 @@
 // -*- mode: js; indent-tabs-mode: nil -*-
 
-const JSUnit = imports.jsUnit;
 const Lang = imports.lang;
 
 const AnInterface = new Lang.Interface({
@@ -102,239 +101,237 @@ const ImplementationOfTwoInterfaces = new Lang.Class({
     }
 });
 
-function testInterfaceIsInstanceOfLangInterface() {
-    JSUnit.assertTrue(AnInterface instanceof Lang.Interface);
-    JSUnit.assertTrue(InterfaceRequiringOtherInterface instanceof Lang.Interface);
-}
-
-function testInterfaceCannotBeInstantiated() {
-    JSUnit.assertRaises(() => new AnInterface());
-}
-
-function testObjectCanImplementInterface() {
-    // Test will fail if the constructor throws an exception
-    let obj = new ObjectImplementingAnInterface();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-}
-
-function testSuperclassCanImplementInterface() {
-    const ChildWhoseParentImplementsAnInterface = new Lang.Class({
-        Name: "ChildWhoseParentImplementsAnInterface",
-        Extends: ObjectImplementingAnInterface
+describe('An interface', function () {
+    it('is an instance of Lang.Interface', function () {
+        expect(AnInterface instanceof Lang.Interface).toBeTruthy();
+        expect(InterfaceRequiringOtherInterface instanceof Lang.Interface).toBeTruthy();
     });
-    let obj = new ChildWhoseParentImplementsAnInterface();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-}
-
-function testObjectImplementingInterfaceHasCorrectConstructor() {
-    let obj = new ObjectImplementingAnInterface();
-    JSUnit.assertEquals(ObjectImplementingAnInterface, obj.constructor);
-}
-
-function testObjectCanImplementRequiredFunction() {
-    // Test considered passing if no exception thrown
-    let implementer = new ObjectImplementingAnInterface();
-    implementer.required();
-}
-
-function testInterfaceMustHaveName() {
-    JSUnit.assertRaises(function () {
-        const AnInterfaceWithoutAName = new Lang.Interface({
-            required: Lang.Interface.UNIMPLEMENTED
+
+    it('cannot be instantiated', function () {
+        expect(() => new AnInterface()).toThrow();
+    });
+
+    it('can be implemented by a class', function () {
+        let obj;
+        expect(() => { obj = new ObjectImplementingAnInterface(); }).not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+    });
+
+    it("can be implemented by a class's superclass", function () {
+        const ChildWhoseParentImplementsAnInterface = new Lang.Class({
+            Name: "ChildWhoseParentImplementsAnInterface",
+            Extends: ObjectImplementingAnInterface
+        });
+        let obj = new ChildWhoseParentImplementsAnInterface();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+    });
+
+    it("doesn't disturb a class's constructor", function () {
+        let obj = new ObjectImplementingAnInterface();
+        expect(obj.constructor).toEqual(ObjectImplementingAnInterface);
+    });
+
+    it('can have its required method implemented', function () {
+        let implementer = new ObjectImplementingAnInterface();
+        expect(() => implementer.required()).not.toThrow();
+    });
+
+    it('must have a name', function () {
+        expect(() => new Lang.Interface({
+            required: Lang.Interface.UNIMPLEMENTED,
+        })).toThrow();
+    });
+
+    it('must have its required methods implemented', function () {
+        expect(() => new Lang.Class({
+            Name: 'MyBadObject',
+            Implements: [AnInterface],
+        })).toThrow();
+    });
+
+    it('does not have to have its optional methods implemented', function () {
+        let obj;
+        expect(() => obj = new MinimalImplementationOfAnInterface()).not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+    });
+
+    it('can have its optional method deferred to by the implementation', function () {
+        let obj = new MinimalImplementationOfAnInterface();
+        expect(obj.optional()).toEqual('AnInterface.optional()');
+    });
+
+    it('can be chained up to by a class', function () {
+        let obj = new ObjectImplementingAnInterface();
+        expect(obj.optional()).toEqual('AnInterface.optional()');
+    });
+
+    it('can include arguments when being chained up to by a class', function () {
+        let obj = new ObjectImplementingAnInterface();
+        expect(obj.argumentGeneric('arg'))
+            .toEqual('AnInterface.argumentGeneric(arg (hello from class))');
+    });
+
+    it('can have its property getter deferred to', function () {
+        let obj = new ObjectImplementingAnInterface();
+        expect(obj.some_prop).toEqual('AnInterface.some_prop getter');
+    });
+
+    it('can have its property setter deferred to', function () {
+        let obj = new ObjectImplementingAnInterface();
+        obj.some_prop = 'foobar';
+        expect(obj.some_prop_setter_called).toBeTruthy();
+    });
+
+    it('can have its property getter overridden', function () {
+        const ObjectWithGetter = new Lang.Class({
+            Name: 'ObjectWithGetter',
+            Implements: [ AnInterface ],
+            required: function () {},
+            get some_prop() {
+                return 'ObjectWithGetter.some_prop getter';
+            }
         });
+        let obj = new ObjectWithGetter();
+        expect(obj.some_prop).toEqual('ObjectWithGetter.some_prop getter');
     });
-}
-
-function testClassMustImplementRequiredFunction() {
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'MyBadObject',
-        Implements: [ AnInterface ]
-    }));
-}
-
-function testClassDoesntHaveToImplementOptionalFunction() {
-    // Test will fail if the constructor throws an exception
-    let obj = new MinimalImplementationOfAnInterface();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-}
-
-function testObjectCanDeferToInterfaceOptionalFunction() {
-    let obj = new MinimalImplementationOfAnInterface();
-    JSUnit.assertEquals('AnInterface.optional()', obj.optional());
-}
-
-function testObjectCanChainUpToInterface() {
-    let obj = new ObjectImplementingAnInterface();
-    JSUnit.assertEquals('AnInterface.optional()', obj.optional());
-}
-
-function testObjectCanChainUpToInterfaceWithArguments() {
-    let obj = new ObjectImplementingAnInterface();
-    JSUnit.assertEquals('AnInterface.argumentGeneric(arg (hello from class))',
-        obj.argumentGeneric('arg'));
-}
-
-function testObjectCanDeferToInterfaceGetter() {
-    let obj = new ObjectImplementingAnInterface();
-    JSUnit.assertEquals('AnInterface.some_prop getter', obj.some_prop);
-}
-
-function testObjectCanDeferToInterfaceSetter() {
-    let obj = new ObjectImplementingAnInterface();
-    obj.some_prop = 'foobar';
-    JSUnit.assertTrue(obj.some_prop_setter_called);
-}
-
-function testObjectCanOverrideInterfaceGetter() {
-    const ObjectWithGetter = new Lang.Class({
-        Name: 'ObjectWithGetter',
-        Implements: [ AnInterface ],
-        required: function () {},
-        get some_prop() {
-            return 'ObjectWithGetter.some_prop getter';
-        }
+
+    it('can have its property setter overridden', function () {
+        const ObjectWithSetter = new Lang.Class({
+            Name: 'ObjectWithSetter',
+            Implements: [ AnInterface ],
+            required: function () {},
+            set some_prop(value) {  /* setter without getter */// jshint ignore:line
+                this.overridden_some_prop_setter_called = true;
+            }
+        });
+        let obj = new ObjectWithSetter();
+        obj.some_prop = 'foobar';
+        expect(obj.overridden_some_prop_setter_called).toBeTruthy();
+        expect(obj.some_prop_setter_called).not.toBeDefined();
+    });
+
+    it('can require another interface', function () {
+        let obj;
+        expect(() => { obj = new ImplementationOfTwoInterfaces(); }).not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+        expect(obj.constructor.implements(InterfaceRequiringOtherInterface)).toBeTruthy();
+    });
+
+    it('can have empty requires', function () {
+        expect(() => new Lang.Interface({
+            Name: 'InterfaceWithEmptyRequires',
+            Requires: []
+        })).not.toThrow();
+    });
+
+    it('can chain up to another interface', function () {
+        let obj = new ImplementationOfTwoInterfaces();
+        expect(obj.optional())
+            .toEqual('InterfaceRequiringOtherInterface.optional()\nAnInterface.optional()');
+    });
+
+    it('can be chained up to with a generic', function () {
+        let obj = new ObjectImplementingAnInterface();
+        expect(obj.optionalGeneric()).toEqual('AnInterface.optionalGeneric()');
+    });
+
+    it('can chain up to another interface with a generic', function () {
+        let obj = new ImplementationOfTwoInterfaces();
+        expect(obj.optionalGeneric())
+            .toEqual('InterfaceRequiringOtherInterface.optionalGeneric()\nAnInterface.optionalGeneric()');
+    });
+
+    it('has its optional function defer to that of the last interface', function () {
+        const MinimalImplementationOfTwoInterfaces = new Lang.Class({
+            Name: 'MinimalImplementationOfTwoInterfaces',
+            Implements: [ AnInterface, InterfaceRequiringOtherInterface ],
+
+            required: function () {}
+        });
+        let obj = new MinimalImplementationOfTwoInterfaces();
+        expect(obj.optionalGeneric())
+            .toEqual('InterfaceRequiringOtherInterface.optionalGeneric()\nAnInterface.optionalGeneric()');
     });
-    let obj = new ObjectWithGetter();
-    JSUnit.assertEquals('ObjectWithGetter.some_prop getter', obj.some_prop);
-}
-
-function testObjectCanOverrideInterfaceSetter() {
-    const ObjectWithSetter = new Lang.Class({
-        Name: 'ObjectWithSetter',
-        Implements: [ AnInterface ],
-        required: function () {},
-        set some_prop(value) {  /* setter without getter */// jshint ignore:line
-            this.overridden_some_prop_setter_called = true;
-        }
+
+    it('must have all its required interfaces implemented', function () {
+        expect(() => new Lang.Class({
+            Name: 'ObjectWithNotEnoughInterfaces',
+            Implements: [ InterfaceRequiringOtherInterface ],
+            required: function () {}
+        })).toThrow();
     });
-    let obj = new ObjectWithSetter();
-    obj.some_prop = 'foobar';
-    JSUnit.assertTrue(obj.overridden_some_prop_setter_called);
-    JSUnit.assertUndefined(obj.some_prop_setter_called);
-}
-
-function testInterfaceCanRequireOtherInterface() {
-    // Test will fail if the constructor throws an exception
-    let obj = new ImplementationOfTwoInterfaces();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-    JSUnit.assertTrue(obj.constructor.implements(InterfaceRequiringOtherInterface));
-}
-
-function testRequiresCanBeEmpty() {
-    // Test considered passing if no exception thrown
-    const InterfaceWithEmptyRequires = new Lang.Interface({
-        Name: 'InterfaceWithEmptyRequires',
-        Requires: []
+
+    it('must have all its required interfaces implemented in the correct order', function () {
+        expect(() => new Lang.Class({
+            Name: 'ObjectWithInterfacesInWrongOrder',
+            Implements: [ InterfaceRequiringOtherInterface, AnInterface ],
+            required: function () {}
+        })).toThrow();
     });
-}
-
-function testInterfaceCanChainUpToOtherInterface() {
-    let obj = new ImplementationOfTwoInterfaces();
-    JSUnit.assertEquals('InterfaceRequiringOtherInterface.optional()\nAnInterface.optional()',
-        obj.optional());
-}
-
-function testObjectCanChainUpToInterfaceWithGeneric() {
-    let obj = new ObjectImplementingAnInterface();
-    JSUnit.assertEquals('AnInterface.optionalGeneric()',
-        obj.optionalGeneric());
-}
-
-function testInterfaceCanChainUpToOtherInterfaceWithGeneric() {
-    let obj = new ImplementationOfTwoInterfaces();
-    JSUnit.assertEquals('InterfaceRequiringOtherInterface.optionalGeneric()\nAnInterface.optionalGeneric()',
-        obj.optionalGeneric());
-}
-
-function testObjectDefersToLastInterfaceOptionalFunction() {
-    const MinimalImplementationOfTwoInterfaces = new Lang.Class({
-        Name: 'MinimalImplementationOfTwoInterfaces',
-        Implements: [ AnInterface, InterfaceRequiringOtherInterface ],
-
-        required: function () {}
+
+    it('can have its implementation on a parent class', function () {
+        let obj;
+        expect(() => {
+            const ObjectInheritingFromInterfaceImplementation = new Lang.Class({
+                Name: 'ObjectInheritingFromInterfaceImplementation',
+                Extends: ObjectImplementingAnInterface,
+                Implements: [ InterfaceRequiringOtherInterface ],
+            });
+            obj = new ObjectInheritingFromInterfaceImplementation();
+        }).not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+        expect(obj.constructor.implements(InterfaceRequiringOtherInterface)).toBeTruthy();
     });
-    let obj = new MinimalImplementationOfTwoInterfaces();
-    JSUnit.assertEquals('InterfaceRequiringOtherInterface.optionalGeneric()\nAnInterface.optionalGeneric()',
-        obj.optionalGeneric());
-}
-
-function testClassMustImplementAllRequiredInterfaces() {
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'ObjectWithNotEnoughInterfaces',
-        Implements: [ InterfaceRequiringOtherInterface ],
-        required: function () {}
-    }));
-}
-
-function testClassMustImplementRequiredInterfacesInCorrectOrder() {
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'ObjectWithInterfacesInWrongOrder',
-        Implements: [ InterfaceRequiringOtherInterface, AnInterface ],
-        required: function () {}
-    }));
-}
-
-function testInterfacesCanBeImplementedOnAParentClass() {
-    // Test will fail if the constructor throws an exception
-    const ObjectInheritingFromInterfaceImplementation = new Lang.Class({
-        Name: 'ObjectInheritingFromInterfaceImplementation',
-        Extends: ObjectImplementingAnInterface,
-        Implements: [ InterfaceRequiringOtherInterface ],
+
+    it('can require its implementor to be a subclass of some class', function () {
+        let obj;
+        expect(() => {
+            const ObjectImplementingInterfaceRequiringParentObject = new Lang.Class({
+                Name: 'ObjectImplementingInterfaceRequiringParentObject',
+                Extends: ObjectImplementingAnInterface,
+                Implements: [ InterfaceRequiringOtherInterface, InterfaceRequiringClassAndInterface ]
+            });
+            obj = new ObjectImplementingInterfaceRequiringParentObject();
+        }).not.toThrow();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+        expect(obj.constructor.implements(InterfaceRequiringOtherInterface)).toBeTruthy();
+        expect(obj.constructor.implements(InterfaceRequiringClassAndInterface)).toBeTruthy();
     });
-    let obj = new ObjectInheritingFromInterfaceImplementation();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-    JSUnit.assertTrue(obj.constructor.implements(InterfaceRequiringOtherInterface));
-}
-
-function testInterfacesCanRequireBeingImplementedOnASubclass() {
-    // Test will fail if the constructor throws an exception
-    const ObjectImplementingInterfaceRequiringParentObject = new Lang.Class({
-        Name: 'ObjectImplementingInterfaceRequiringParentObject',
-        Extends: ObjectImplementingAnInterface,
-        Implements: [ InterfaceRequiringOtherInterface, InterfaceRequiringClassAndInterface ]
+
+    it('must be implemented by an object which subclasses the required class', function () {
+        expect(() => new Lang.Class({
+            Name: 'ObjectWithoutRequiredParent',
+            Implements: [ AnInterface, InterfaceRequiringOtherInterface, InterfaceRequiringClassAndInterface 
],
+            required: function () {},
+        })).toThrow();
     });
-    let obj = new ObjectImplementingInterfaceRequiringParentObject();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-    JSUnit.assertTrue(obj.constructor.implements(InterfaceRequiringOtherInterface));
-    JSUnit.assertTrue(obj.constructor.implements(InterfaceRequiringClassAndInterface));
-}
-
-function testObjectsMustSubclassIfRequired() {
-    JSUnit.assertRaises(() => new Lang.Class({
-        Name: 'ObjectWithoutRequiredParent',
-        Implements: [ AnInterface, InterfaceRequiringOtherInterface, InterfaceRequiringClassAndInterface ],
-        required: function () {},
-    }));
-}
-
-function testInterfaceMethodsCanCallOtherInterfaceMethods() {
-    let obj = new ObjectImplementingAnInterface();
-    JSUnit.assertEquals('interface private method', obj.usesThis());
-}
-
-function testSubclassImplementsTheSameInterfaceAsItsParent() {
-    const SubObject = new Lang.Class({
-        Name: 'SubObject',
-        Extends: ObjectImplementingAnInterface
+
+    it('can have methods that call others of its methods', function () {
+        let obj = new ObjectImplementingAnInterface();
+        expect(obj.usesThis()).toEqual('interface private method');
     });
-    let obj = new SubObject();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-}
-
-function testSubclassCanReimplementTheSameInterfaceAsItsParent() {
-    const SubImplementer = new Lang.Class({
-        Name: 'SubImplementer',
-        Extends: ObjectImplementingAnInterface,
-        Implements: [ AnInterface ]
+
+    it('is implemented by a subclass of a class that implements it', function () {
+        const SubObject = new Lang.Class({
+            Name: 'SubObject',
+            Extends: ObjectImplementingAnInterface
+        });
+        let obj = new SubObject();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
     });
-    let obj = new SubImplementer();
-    JSUnit.assertTrue(obj.constructor.implements(AnInterface));
-    obj.required();  // should not throw NotImplemented
-}
 
-function testToString() {
-    JSUnit.assertEquals('[interface Interface for AnInterface]',
-                        AnInterface.toString());
-}
+    it('can be reimplemented by a subclass of a class that implements it', function () {
+        const SubImplementer = new Lang.Class({
+            Name: 'SubImplementer',
+            Extends: ObjectImplementingAnInterface,
+            Implements: [ AnInterface ]
+        });
+        let obj = new SubImplementer();
+        expect(obj.constructor.implements(AnInterface)).toBeTruthy();
+        expect(() => obj.required()).not.toThrow();
+    });
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('tells what it is with toString()', function () {
+        expect(AnInterface.toString()).toEqual('[interface Interface for AnInterface]');
+    });
+});
diff --git a/installed-tests/js/testLang.js b/installed-tests/js/testLang.js
index 342a599..51874bd 100644
--- a/installed-tests/js/testLang.js
+++ b/installed-tests/js/testLang.js
@@ -1,112 +1,99 @@
 // tests for imports.lang module
+// except for Lang.Class and Lang.Interface, which are tested in separate files
 
-const JSUnit = imports.jsUnit;
 const Lang = imports.lang;
 
-function testCountProperties() {
-    var foo = { 'a' : 10, 'b' : 11 };
-    JSUnit.assertEquals("number of props", 2, Lang.countProperties(foo));
-}
-
-function testCopyProperties() {
-    var foo = { 'a' : 10, 'b' : 11 };
-    var bar = {};
-
-    Lang.copyProperties(foo, bar);
-
-    JSUnit.assertTrue("a in bar", ('a' in bar));
-    JSUnit.assertTrue("b in bar", ('b' in bar));
-    JSUnit.assertEquals("a is 10", 10, bar.a);
-    JSUnit.assertEquals("b is 11", 11, bar.b);
-    JSUnit.assertEquals("2 items in bar", 2, Lang.countProperties(bar));
-}
-
-function testCopyPublicProperties() {
-    var foo = { 'a' : 10, 'b' : 11, '_c' : 12 };
-    var bar = {};
-
-    Lang.copyPublicProperties(foo, bar);
-
-    JSUnit.assertTrue("a in bar", ('a' in bar));
-    JSUnit.assertTrue("b in bar", ('b' in bar));
-    JSUnit.assertFalse("_c in bar", ('_c' in bar));
-    JSUnit.assertEquals("a is 10", 10, bar.a);
-    JSUnit.assertEquals("b is 11", 11, bar.b);
-    JSUnit.assertEquals("2 items in bar", 2, Lang.countProperties(bar));
-}
-
-function testCopyGetterSetterProperties() {
-    var foo = {
-        'a' : 10,
-        'b' : 11,
-        get c() {
-            return this.a;
-        },
-        set c(n) {
-            this.a = n;
-        }};
-    var bar = {};
-
-    Lang.copyProperties(foo, bar);
-
-    let getterFunc = bar.__lookupGetter__("c");
-    let setterFunc = bar.__lookupSetter__("c");
-
-    // this should return the value of 'a'
-    let c = bar.c;
-
-    // this should set 'a' value
-    bar.c = 13;
-
-    JSUnit.assertTrue("bar has 'c' getter", (getterFunc != null));
-    JSUnit.assertTrue("bar has 'c' setter", (setterFunc != null));
-    JSUnit.assertTrue("bar 'c' value is 10", (c == 10));
-    JSUnit.assertTrue("bar 'a' new value is 13", (bar.a == 13));
-}
-
-function testBind() {
-
-    function Obj() {
-    }
-
-    Obj.prototype = {
-        callback: function() {
-            this.obj = this;
-            this.args = arguments;
-            return true;
-        }
-    };
-
-    let callback;
-
-    let o = new Obj();
-    callback = Lang.bind(o, o.callback);
-    JSUnit.assertEquals(callback(), true);
-    JSUnit.assertNotEquals("o.obj in callback", undefined, o.obj);
-    JSUnit.assertEquals("o.obj in callback", o, o.obj);
-    JSUnit.assertEquals("o.args in callback", 0, o.args.length);
-    JSUnit.assertRaises(function() { return Lang.bind(o, undefined); });
-    JSUnit.assertRaises(function() { return Lang.bind(undefined, function() {}); });
-
-    let o2 = new Obj();
-    callback = Lang.bind(o2, o2.callback, 42, 1138);
-    JSUnit.assertEquals(callback(), true);
-    JSUnit.assertNotEquals("o2.args in callback", undefined, o2.args);
-    JSUnit.assertEquals("o2.args.length in callback", 2, o2.args.length);
-    JSUnit.assertEquals("o2.args[0] in callback", 42, o2.args[0]);
-    JSUnit.assertEquals("o2.args[1] in callback", 1138, o2.args[1]);
-
-    let o3 = new Obj();
-    callback = Lang.bind(o3, o3.callback, 42, 1138);
-    JSUnit.assertEquals(callback(1, 2, 3), true);
-    JSUnit.assertNotEquals("o3.args in callback", undefined, o3.args);
-    JSUnit.assertEquals("o3.args.length in callback", 5, o3.args.length);
-    JSUnit.assertEquals("o3.args[0] in callback", 1, o3.args[0]);
-    JSUnit.assertEquals("o3.args[1] in callback", 2, o3.args[1]);
-    JSUnit.assertEquals("o3.args[2] in callback", 3, o3.args[2]);
-    JSUnit.assertEquals("o3.args[3] in callback", 42, o3.args[3]);
-    JSUnit.assertEquals("o3.args[4] in callback", 1138, o3.args[4]);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
-
+describe('Lang module', function () {
+    it('counts properties with Lang.countProperties()', function () {
+        var foo = { 'a' : 10, 'b' : 11 };
+        expect(Lang.countProperties(foo)).toEqual(2);
+    });
+
+    it('copies properties from one object to another with Lang.copyProperties()', function () {
+        var foo = { 'a' : 10, 'b' : 11 };
+        var bar = {};
+
+        Lang.copyProperties(foo, bar);
+        expect(bar).toEqual(foo);
+    });
+
+    it('copies properties without an underscore with Lang.copyPublicProperties()', function () {
+        var foo = { 'a' : 10, 'b' : 11, '_c' : 12 };
+        var bar = {};
+
+        Lang.copyPublicProperties(foo, bar);
+        expect(bar).toEqual({ 'a': 10, 'b': 11 });
+    });
+
+    it('copies property getters and setters', function () {
+        var foo = {
+            'a' : 10,
+            'b' : 11,
+            get c() {
+                return this.a;
+            },
+            set c(n) {
+                this.a = n;
+            }
+        };
+        var bar = {};
+
+        Lang.copyProperties(foo, bar);
+
+        expect(bar.__lookupGetter__('c')).not.toBeNull();
+        expect(bar.__lookupSetter__('c')).not.toBeNull();
+
+        // this should return the value of 'a'
+        expect(bar.c).toEqual(10);
+
+        // this should set 'a' value
+        bar.c = 13;
+        expect(bar.a).toEqual(13);
+    });
+
+
+    describe('bind()', function () {
+        const Obj = new Lang.Class({
+            Name: 'Obj',
+            callback: function () {
+                return true;
+            },
+        });
+        let o;
+
+        beforeEach(function () {
+            o = new Obj();
+            spyOn(o, 'callback').and.callThrough();
+        });
+
+        it('calls the bound function with the supplied this-object', function () {
+            let callback = Lang.bind(o, o.callback);
+            callback();
+            expect(o.callback.calls.mostRecent()).toEqual({
+                object: o,
+                args: [],
+                returnValue: true,
+            });
+        });
+
+        it('throws an error when no function supplied', function () {
+            expect(() => Lang.bind(o, undefined)).toThrow();
+        });
+
+        it('throws an error when this-object undefined', function () {
+            expect(() => Lang.bind(undefined, function () {})).toThrow();
+        });
+
+        it('supplies extra arguments to the function', function () {
+            let callback = Lang.bind(o, o.callback, 42, 1138);
+            callback();
+            expect(o.callback).toHaveBeenCalledWith(42, 1138);
+        });
+
+        it('appends the extra arguments to any arguments passed', function () {
+            let callback = Lang.bind(o, o.callback, 42, 1138);
+            callback(1, 2, 3);
+            expect(o.callback).toHaveBeenCalledWith(1, 2, 3, 42, 1138);
+        });
+    });
+});
diff --git a/installed-tests/js/testLocale.js b/installed-tests/js/testLocale.js
index b95c8e7..16b17ac 100644
--- a/installed-tests/js/testLocale.js
+++ b/installed-tests/js/testLocale.js
@@ -1,36 +1,37 @@
-// tests for JS_SetLocaleCallbacks().
-const JSUnit = imports.jsUnit;
-
-function testToLocaleDateString() {
-    let date = new Date('12/15/1981');
+describe('JS_SetLocaleCallbacks', function () {
     // Requesting the weekday name tests locale_to_unicode
-    let datestr = date.toLocaleDateString('pt-BR', { weekday: 'long' });
-    JSUnit.assertEquals('terça-feira', datestr);
-}
+    it('toLocaleDateString() works', function () {
+        let date = new Date('12/15/1981');
+        let datestr = date.toLocaleDateString('pt-BR', { weekday: 'long' });
+        expect(datestr).toEqual('terça-feira');
+    });
 
-function testToLocaleLowerCase() {
-    JSUnit.assertEquals("aaa", "AAA".toLocaleLowerCase());
+    it('toLocaleLowerCase() works', function () {
+        expect('AAA'.toLocaleLowerCase()).toEqual('aaa');
+    });
 
     // String conversion is implemented internally to GLib,
     // and is more-or-less independent of locale. (A few
     // characters are handled specially for a few locales,
     // like i in Turkish. But not A WITH ACUTE)
-    JSUnit.assertEquals("\u00e1", "\u00c1".toLocaleLowerCase());
-}
+    it('toLocaleLowerCase() works for Unicode', function () {
+        expect('\u00c1'.toLocaleLowerCase()).toEqual('\u00e1');
+    });
+
+    it('toLocaleUpperCase() works', function () {
+        expect('aaa'.toLocaleUpperCase()).toEqual('AAA');
+    });
 
-function testToLocaleUpperCase() {
-    JSUnit.assertEquals("AAA", "aaa".toLocaleUpperCase());
-    JSUnit.assertEquals("\u00c1", "\u00e1".toLocaleUpperCase());
-}
+    it('toLocaleUpperCase() works for Unicode', function () {
+        expect('\u00e1'.toLocaleUpperCase()).toEqual('\u00c1');
+    });
 
-function testToLocaleCompare() {
     // GLib calls out to libc for collation, so we can't really
     // assume anything - we could even be running in the
     // C locale. The below is pretty safe.
-    JSUnit.assertTrue("a".localeCompare("b") < 0);
-    JSUnit.assertEquals( 0, "a".localeCompare("a"));
-    JSUnit.assertTrue("b".localeCompare("a") > 0);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
-
+    it('localeCompare() works', function () {
+        expect('a'.localeCompare('b')).toBeLessThan(0);
+        expect('a'.localeCompare('a')).toEqual(0);
+        expect('b'.localeCompare('a')).toBeGreaterThan(0);
+    });
+});
diff --git a/installed-tests/js/testMainloop.js b/installed-tests/js/testMainloop.js
index 4d9579d..0ffcf37 100644
--- a/installed-tests/js/testMainloop.js
+++ b/installed-tests/js/testMainloop.js
@@ -1,106 +1,91 @@
-const JSUnit = imports.jsUnit;
 const Mainloop = imports.mainloop;
 
-function testTimeout() {
-    var trackTimeout = {
-        runTenTimes : 0,
-        runOnlyOnce: 0,
-        neverRun: 0
-    };
-
-    Mainloop.timeout_add(10,
-                         function() {
-                             if (trackTimeout.runTenTimes == 10) {
-                                 Mainloop.quit('testtimeout');
-                                 return false;
-                             }
-
-                             trackTimeout.runTenTimes += 1;
-                             return true;
-                         });
-
-    Mainloop.timeout_add(10,
-                         function () {
-                             trackTimeout.runOnlyOnce += 1;
-                             return false;
-                         });
-
-    Mainloop.timeout_add(15000,
-                       function() {
-                           trackTimeout.neverRun += 1;
-                           return false;
-                       });
-
-    Mainloop.run('testtimeout');
-
-    with (trackTimeout) {
-        JSUnit.assertEquals("run ten times", 10, runTenTimes);
-        JSUnit.assertEquals("run only once", 1, runOnlyOnce);
-        JSUnit.assertEquals("never run", 0, neverRun);
-    }
-}
-
-function testIdle() {
-    var trackIdles = {
-        runTwiceCount : 0,
-        runOnceCount : 0,
-        neverRunsCount : 0,
-        quitAfterManyRunsCount : 0
-    };
-    Mainloop.idle_add(function() {
-                          trackIdles.runTwiceCount += 1;
-                          if (trackIdles.runTwiceCount == 2)
-                              return false;
-                          else
-                              return true;
-                      });
-    Mainloop.idle_add(function() {
-                          trackIdles.runOnceCount += 1;
-                          return false;
-                      });
-    var neverRunsId =
-        Mainloop.idle_add(function() {
-                              trackIdles.neverRunsCount += 1;
-                              return false;
-                          });
-    Mainloop.idle_add(function() {
-                          trackIdles.quitAfterManyRunsCount += 1;
-                          if (trackIdles.quitAfterManyRunsCount > 10) {
-                              Mainloop.quit('foobar');
-                              return false;
-                          } else {
-                              return true;
-                          }
-                      });
-
-    Mainloop.source_remove(neverRunsId);
-
-    Mainloop.run('foobar');
-
-    JSUnit.assertEquals("one-shot ran once", 1, trackIdles.runOnceCount);
-    JSUnit.assertEquals("two-shot ran twice", 2, trackIdles.runTwiceCount);
-    JSUnit.assertEquals("removed never ran", 0, trackIdles.neverRunsCount);
-    JSUnit.assertEquals("quit after many ran 11", 11, trackIdles.quitAfterManyRunsCount);
-
-    // check re-entrancy of removing closures while they
-    // are being invoked
-
-    trackIdles.removeId = Mainloop.idle_add(function() {
-                                                Mainloop.source_remove(trackIdles.removeId);
-                                                Mainloop.quit('foobar');
-                                                return false;
-                                            });
-    Mainloop.run('foobar');
+describe('Mainloop.timeout_add()', function () {
+    let runTenTimes, runOnlyOnce, neverRun;
+    beforeAll(function (done) {
+        let count = 0;
+        runTenTimes = jasmine.createSpy('runTenTimes').and.callFake(() => {
+            if (count === 10) {
+                done();
+                return false;
+            }
+            count += 1;
+            return true;
+        });
+        runOnlyOnce = jasmine.createSpy('runOnlyOnce').and.returnValue(false);
+        neverRun = jasmine.createSpy('neverRun').and.throwError();
+
+        Mainloop.timeout_add(10, runTenTimes);
+        Mainloop.timeout_add(10, runOnlyOnce);
+        Mainloop.timeout_add(15000, neverRun);
+    });
+
+    it('runs a timeout function', function () {
+        expect(runOnlyOnce).toHaveBeenCalledTimes(1);
+    });
+
+    it('runs a timeout function until it returns false', function () {
+        expect(runTenTimes).toHaveBeenCalledTimes(11);
+    });
+
+    it('runs a timeout function after an initial timeout', function () {
+        expect(neverRun).not.toHaveBeenCalled();
+    });
+});
+
+describe('Mainloop.idle_add()', function () {
+    let runOnce, runTwice, neverRuns, quitAfterManyRuns;
+    beforeAll(function (done) {
+        runOnce = jasmine.createSpy('runOnce').and.returnValue(false);
+        runTwice = jasmine.createSpy('runTwice').and.returnValues([true, false]);
+        neverRuns = jasmine.createSpy('neverRuns').and.throwError();
+        let count = 0;
+        quitAfterManyRuns = jasmine.createSpy('quitAfterManyRuns').and.callFake(() => {
+            count += 1;
+            if (count > 10) {
+                done();
+                return false;
+            }
+            return true;
+        });
+
+        Mainloop.idle_add(runOnce);
+        Mainloop.idle_add(runTwice);
+        let neverRunsId = Mainloop.idle_add(neverRuns);
+        Mainloop.idle_add(quitAfterManyRuns);
+
+        Mainloop.source_remove(neverRunsId);
+    });
+
+    it('runs an idle function', function () {
+        expect(runOnce).toHaveBeenCalledTimes(1);
+    });
+
+    it('continues to run idle functions that return true', function () {
+        expect(runTwice).toHaveBeenCalledTimes(2);
+        expect(quitAfterManyRuns).toHaveBeenCalledTimes(11);
+    });
+
+    it('does not run idle functions if removed', function () {
+        expect(neverRuns).not.toHaveBeenCalled();
+    });
+
+    it('can remove idle functions while they are being invoked', function (done) {
+        let removeId = Mainloop.idle_add(() => {
+            Mainloop.source_remove(removeId);
+            done();
+            return false;
+        });
+    });
 
     // Add an idle before exit, then never run main loop again.
     // This is to test that we remove idle callbacks when the associated
-    // JSContext is blown away. The leak check in gjs-unit will
+    // JSContext is blown away. The leak check in minijasmine will
     // fail if the idle function is not garbage collected.
-    Mainloop.idle_add(function() {
-                          fail("This should never have been called");
-                          return true;
-                      });
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
-
+    it('does not leak idle callbacks', function () {
+        Mainloop.idle_add(() => {
+            fail('This should never have been called');
+            return true;
+        });
+    });
+});
diff --git a/installed-tests/js/testMetaClass.js b/installed-tests/js/testMetaClass.js
index e570f35..48c92a7 100644
--- a/installed-tests/js/testMetaClass.js
+++ b/installed-tests/js/testMetaClass.js
@@ -1,20 +1,7 @@
 // -*- mode: js; indent-tabs-mode: nil -*-
-const JSUnit = imports.jsUnit;
-
-if (!('assertEquals' in this)) { /* allow running this test standalone */
-    imports.lang.copyPublicProperties(imports.jsUnit, this);
-    gjstestRun = function() { return imports.jsUnit.gjstestRun(window); };
-}
 
 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 NormalClass = new Lang.Class({
     Name: 'NormalClass',
 
@@ -80,49 +67,51 @@ const CustomMetaSubclass = new Lang.Class({
     }
 });
 
-function testMetaClass() {
-    assertArrayEquals(['CustomMetaOne',
-                       'CustomMetaTwo',
-                       'CustomMetaSubclass'], Subclassed);
-
-    JSUnit.assertTrue(NormalClass instanceof Lang.Class);
-    JSUnit.assertTrue(MetaClass instanceof Lang.Class);
-
-    JSUnit.assertTrue(CustomMetaOne instanceof Lang.Class);
-    JSUnit.assertTrue(CustomMetaOne instanceof MetaClass);
-
-    JSUnit.assertEquals(2, CustomMetaTwo.DYNAMIC_CONSTANT);
-    JSUnit.assertUndefined(CustomMetaOne.DYNAMIC_CONSTANT);
-}
-
-function testMetaInstance() {
-    let instanceOne = new CustomMetaOne();
-
-    JSUnit.assertEquals(1, instanceOne.one);
-    JSUnit.assertEquals(2, instanceOne.two);
-
-    JSUnit.assertRaises(function() {
-        instanceOne.dynamic_method();
+describe('A metaclass', function () {
+    it('has its constructor called each time a class is created with it', function () {
+        expect(Subclassed).toEqual(['CustomMetaOne', 'CustomMetaTwo',
+            'CustomMetaSubclass']);
     });
 
-    let instanceTwo = new CustomMetaTwo();
-    JSUnit.assertEquals(1, instanceTwo.one);
-    JSUnit.assertEquals(2, instanceTwo.two);
-    JSUnit.assertEquals(73, instanceTwo.dynamic_method());
-}
-
-function testMetaSubclass() {
-    JSUnit.assertTrue(CustomMetaSubclass instanceof MetaClass);
+    it('is an instance of Lang.Class', function () {
+        expect(NormalClass instanceof Lang.Class).toBeTruthy();
+        expect(MetaClass instanceof Lang.Class).toBeTruthy();
+    });
 
-    let instance = new CustomMetaSubclass();
+    it('produces instances that are instances of itself and Lang.Class', function () {
+        expect(CustomMetaOne instanceof Lang.Class).toBeTruthy();
+        expect(CustomMetaOne instanceof MetaClass).toBeTruthy();
+    });
 
-    JSUnit.assertEquals(1, instance.one);
-    JSUnit.assertEquals(2, instance.two);
-    JSUnit.assertEquals(3, instance.three);
+    it('can dynamically define properties in its constructor', function () {
+        expect(CustomMetaTwo.DYNAMIC_CONSTANT).toEqual(2);
+        expect(CustomMetaOne.DYNAMIC_CONSTANT).not.toBeDefined();
+    });
 
-    JSUnit.assertEquals(73, instance.dynamic_method());
-    JSUnit.assertEquals(2, CustomMetaSubclass.DYNAMIC_CONSTANT);
-}
+    describe('instance', function () {
+        let instanceOne, instanceTwo;
+        beforeEach(function () {
+            instanceOne = new CustomMetaOne();
+            instanceTwo = new CustomMetaTwo();
+        });
+
+        it('gets all the properties from its class and metaclass', function () {
+            expect(instanceOne).toEqual(jasmine.objectContaining({ one: 1, two: 2 }));
+            expect(instanceTwo).toEqual(jasmine.objectContaining({ one: 1, two: 2 }));
+        });
+
+        it('gets dynamically defined properties from metaclass', function () {
+            expect(() => instanceOne.dynamic_method()).toThrow();
+            expect(instanceTwo.dynamic_method()).toEqual(73);
+        });
+    });
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('can be instantiated with Lang.Class but still get the appropriate metaclass', function () {
+        expect(CustomMetaSubclass instanceof MetaClass).toBeTruthy();
+        expect(CustomMetaSubclass.DYNAMIC_CONSTANT).toEqual(2);
 
+        let instance = new CustomMetaSubclass();
+        expect(instance).toEqual(jasmine.objectContaining({ one: 1, two: 2, three: 3 }));
+        expect(instance.dynamic_method()).toEqual(73);
+    });
+});
diff --git a/installed-tests/js/testNamespace.js b/installed-tests/js/testNamespace.js
index bcca986..49c6426 100644
--- a/installed-tests/js/testNamespace.js
+++ b/installed-tests/js/testNamespace.js
@@ -1,8 +1,7 @@
-const JSUnit = imports.jsUnit;
-const Everything = imports.gi.Regress;
+const Regress = imports.gi.Regress;
 
-function testName() {
-    JSUnit.assertEquals(Everything.__name__, "Regress");
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+describe('GI repository namespace', function () {
+    it('supplies a name', function () {
+        expect(Regress.__name__).toEqual('Regress');
+    });
+});
diff --git a/installed-tests/js/testParamSpec.js b/installed-tests/js/testParamSpec.js
index 1b7013c..0620e0c 100644
--- a/installed-tests/js/testParamSpec.js
+++ b/installed-tests/js/testParamSpec.js
@@ -1,5 +1,3 @@
-const JSUnit = imports.jsUnit;
-
 const Regress = imports.gi.Regress;
 const GObject = imports.gi.GObject;
 
@@ -8,96 +6,37 @@ let nick = 'Foo property';
 let blurb = 'This is the foo property';
 let flags = GObject.ParamFlags.READABLE;
 
-function testStringParamSpec() {
-    let stringSpec = GObject.ParamSpec.string(name, nick, blurb, flags,
-                                              'Default Value');
-
-    JSUnit.assertEquals(name, stringSpec.name);
-    JSUnit.assertEquals(nick, stringSpec._nick);
-    JSUnit.assertEquals(blurb, stringSpec._blurb);
-    JSUnit.assertEquals(flags, stringSpec.flags);
-    JSUnit.assertEquals('Default Value', stringSpec.default_value);
-}
-
-function testIntParamSpec() {
-    let intSpec = GObject.ParamSpec.int(name, nick, blurb, flags,
-                                        -100, 100, -42);
-
-    JSUnit.assertEquals(name, intSpec.name);
-    JSUnit.assertEquals(nick, intSpec._nick);
-    JSUnit.assertEquals(blurb, intSpec._blurb);
-    JSUnit.assertEquals(flags, intSpec.flags);
-    JSUnit.assertEquals(-42, intSpec.default_value);
-}
-
-function testUIntParamSpec() {
-    let uintSpec = GObject.ParamSpec.uint(name, nick, blurb, flags,
-                                          20, 100, 42);
-
-    JSUnit.assertEquals(name, uintSpec.name);
-    JSUnit.assertEquals(nick, uintSpec._nick);
-    JSUnit.assertEquals(blurb, uintSpec._blurb);
-    JSUnit.assertEquals(flags, uintSpec.flags);
-    JSUnit.assertEquals(42, uintSpec.default_value);
-}
-
-function testInt64ParamSpec() {
-    let int64Spec = GObject.ParamSpec.int64(name, nick, blurb, flags,
-                                            0x4000,
-                                            0xffffffff,
-                                            0x2266bbff);
-
-    JSUnit.assertEquals(name, int64Spec.name);
-    JSUnit.assertEquals(nick, int64Spec._nick);
-    JSUnit.assertEquals(blurb, int64Spec._blurb);
-    JSUnit.assertEquals(flags, int64Spec.flags);
-    JSUnit.assertEquals(0x2266bbff, int64Spec.default_value);
-}
-
-function testUInt64ParamSpec() {
-    let uint64Spec = GObject.ParamSpec.uint64(name, nick, blurb, flags,
-                                              0,
-                                              0xffffffff,
-                                              0x2266bbff);
-
-    JSUnit.assertEquals(name, uint64Spec.name);
-    JSUnit.assertEquals(nick, uint64Spec._nick);
-    JSUnit.assertEquals(blurb, uint64Spec._blurb);
-    JSUnit.assertEquals(flags, uint64Spec.flags);
-    JSUnit.assertEquals(0x2266bbff, uint64Spec.default_value);
-}
-
-function testEnumParamSpec() {
-    let enumSpec = GObject.ParamSpec.enum(name, nick, blurb, flags,
-                                          Regress.TestEnum,
-                                          Regress.TestEnum.VALUE2);
-
-    JSUnit.assertEquals(name, enumSpec.name);
-    JSUnit.assertEquals(nick, enumSpec._nick);
-    JSUnit.assertEquals(blurb, enumSpec._blurb);
-    JSUnit.assertEquals(flags, enumSpec.flags);
-    JSUnit.assertEquals(Regress.TestEnum.VALUE2, enumSpec.default_value);
-}
-
-function testFlagsParamSpec() {
-    let flagsSpec = GObject.ParamSpec.flags(name, nick, blurb, flags,
-                                            Regress.TestFlags,
-                                            Regress.TestFlags.FLAG2);
-
-    JSUnit.assertEquals(name, flagsSpec.name);
-    JSUnit.assertEquals(nick, flagsSpec._nick);
-    JSUnit.assertEquals(blurb, flagsSpec._blurb);
-    JSUnit.assertEquals(flags, flagsSpec.flags);
-    JSUnit.assertEquals(Regress.TestFlags.FLAG2, flagsSpec.default_value);
-}
-
-function testParamSpecMethod() {
-    let objectSpec = GObject.ParamSpec.object(name, nick, blurb, flags, GObject.Object);
-
-    JSUnit.assertEquals(name, objectSpec.get_name());
-    JSUnit.assertEquals(nick, objectSpec.get_nick());
-    JSUnit.assertEquals(blurb, objectSpec.get_blurb());
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
-
+function testParamSpec(type, params, defaultValue) {
+    describe('GObject.ParamSpec.' + type, function () {
+        let paramSpec;
+        beforeEach(function () {
+            paramSpec = GObject.ParamSpec[type].apply(GObject.ParamSpec,
+                [name, nick, blurb, flags, ...params]);
+        });
+
+        it('has the correct name strings', function () {
+            expect(paramSpec.name).toEqual(name);
+            expect(paramSpec._nick).toEqual(nick);
+            expect(paramSpec._blurb).toEqual(blurb);
+        });
+
+        it('has the correct flags', function () {
+            expect(paramSpec.flags).toEqual(flags);
+        });
+
+        it('has the correct default value', function () {
+            expect(paramSpec.default_value).toEqual(defaultValue);
+        });
+    });
+}
+
+testParamSpec('string', ['Default Value'], 'Default Value');
+testParamSpec('int', [-100, 100, -42], -42);
+testParamSpec('uint', [20, 100, 42], 42);
+testParamSpec('int64', [0x4000, 0xffffffff, 0x2266bbff], 0x2266bbff);
+testParamSpec('uint64', [0, 0xffffffff, 0x2266bbff], 0x2266bbff);
+testParamSpec('enum', [Regress.TestEnum, Regress.TestEnum.VALUE2],
+    Regress.TestEnum.VALUE2);
+testParamSpec('flags', [Regress.TestFlags, Regress.TestFlags.FLAG2],
+    Regress.TestFlags.FLAG2);
+testParamSpec('object', [GObject.Object], null);
diff --git a/installed-tests/js/testSignals.js b/installed-tests/js/testSignals.js
index e3104d8..1ea4d27 100644
--- a/installed-tests/js/testSignals.js
+++ b/installed-tests/js/testSignals.js
@@ -1,139 +1,109 @@
-const JSUnit = imports.jsUnit;
 const GLib = imports.gi.GLib;
-
+const Lang = imports.lang;
 const Signals = imports.signals;
 
-function Foo() {
-    this._init();
-}
-
-Foo.prototype = {
-    _init : function() {
-    }
-};
-
+const Foo = new Lang.Class({
+    Name: 'Foo',
+    _init: function () {},
+});
 Signals.addSignalMethods(Foo.prototype);
 
-function testSimple() {
-    var foo = new Foo();
-    var id = foo.connect('bar',
-                         function(theFoo, a, b) {
-                             theFoo.a = a;
-                             theFoo.b = b;
-                         });
-    foo.emit('bar', "This is a", "This is b");
-    JSUnit.assertEquals("This is a", foo.a);
-    JSUnit.assertEquals("This is b", foo.b);
-    foo.disconnect(id);
-    // this emission should do nothing
-    foo.emit('bar', "Another a", "Another b");
-    // so these values should be unchanged
-    JSUnit.assertEquals("This is a", foo.a);
-    JSUnit.assertEquals("This is b", foo.b);
-}
-
-function testDisconnectDuringEmit() {
-    var foo = new Foo();
-    var toRemove = [];
-    var firstId = foo.connect('bar',
-                         function(theFoo) {
-                             theFoo.disconnect(toRemove[0]);
-                             theFoo.disconnect(toRemove[1]);
-                         });
-    var id = foo.connect('bar',
-                     function(theFoo) {
-                         throw new Error("This should not have been called 1");
-                     });
-    toRemove.push(id);
-
-    id = foo.connect('bar',
-                     function(theFoo) {
-                         throw new Error("This should not have been called 2");
-                     });
-    toRemove.push(id);
-
-    // emit signal; what should happen is that the second two handlers are
-    // disconnected before they get invoked
-    foo.emit('bar');
-
-    // clean up the last handler
-    foo.disconnect(firstId);
-
-    // poke in private implementation to sanity-check
-    JSUnit.assertEquals('no handlers left', 0, foo._signalConnections.length);
-}
-
-function testMultipleSignals() {
-    var foo = new Foo();
-
-    foo.barHandlersCalled = 0;
-    foo.bonkHandlersCalled = 0;
-    foo.connect('bar',
-                function(theFoo) {
-                    theFoo.barHandlersCalled += 1;
-                });
-    foo.connect('bonk',
-                function(theFoo) {
-                    theFoo.bonkHandlersCalled += 1;
-                });
-    foo.connect('bar',
-                function(theFoo) {
-                    theFoo.barHandlersCalled += 1;
-                });
-    foo.emit('bar');
-
-    JSUnit.assertEquals(2, foo.barHandlersCalled);
-    JSUnit.assertEquals(0, foo.bonkHandlersCalled);
-
-    foo.emit('bonk');
-
-    JSUnit.assertEquals(2, foo.barHandlersCalled);
-    JSUnit.assertEquals(1, foo.bonkHandlersCalled);
-
-    foo.emit('bar');
-
-    JSUnit.assertEquals(4, foo.barHandlersCalled);
-    JSUnit.assertEquals(1, foo.bonkHandlersCalled);
-
-    foo.disconnectAll();
-
-    // these post-disconnect emissions should do nothing
-    foo.emit('bar');
-    foo.emit('bonk');
-
-    JSUnit.assertEquals(4, foo.barHandlersCalled);
-    JSUnit.assertEquals(1, foo.bonkHandlersCalled);
-}
-
-function testExceptionInCallback() {
-    let foo = new Foo();
-
-    foo.bar1Called = 0;
-    foo.bar2Called = 0;
-    foo.connect('bar',
-                function(theFoo) {
-                    theFoo.bar1Called += 1;
-                    throw new Error("Exception we are throwing on purpose");
-                });
-    foo.connect('bar',
-                function(theFoo) {
-                    theFoo.bar2Called += 1;
-                });
-
-    // exception in callback does not effect other callbacks
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-                             'JS ERROR: Exception in callback for signal: *');
-    foo.emit('bar');
-    JSUnit.assertEquals(1, foo.bar1Called);
-    JSUnit.assertEquals(1, foo.bar2Called);
-
-    // exception in callback does not disconnect the callback
-    GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
-                             'JS ERROR: Exception in callback for signal: *');
-    foo.emit('bar');
-    JSUnit.assertEquals(2, foo.bar1Called);
-    JSUnit.assertEquals(2, foo.bar2Called);
-}
-
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
-
+describe('Object with signals', function () {
+    let foo, bar;
+    beforeEach(function () {
+        foo = new Foo();
+        bar = jasmine.createSpy('bar');
+    });
+
+    it('calls a signal handler when a signal is emitted', function () {
+        foo.connect('bar', bar);
+        foo.emit('bar', "This is a", "This is b");
+        expect(bar).toHaveBeenCalledWith(foo, 'This is a', 'This is b');
+    });
+
+    it('does not call a signal handler after the signal is disconnected', function () {
+        let id = foo.connect('bar', bar);
+        foo.emit('bar', "This is a", "This is b");
+        bar.calls.reset();
+        foo.disconnect(id);
+        // this emission should do nothing
+        foo.emit('bar', "Another a", "Another b");
+        expect(bar).not.toHaveBeenCalled();
+    });
+
+    it('can disconnect a signal handler during signal emission', function () {
+        var toRemove = [];
+        let firstId = foo.connect('bar', function (theFoo) {
+            theFoo.disconnect(toRemove[0]);
+            theFoo.disconnect(toRemove[1]);
+        });
+        toRemove.push(foo.connect('bar', bar));
+        toRemove.push(foo.connect('bar', bar));
+
+        // emit signal; what should happen is that the second two handlers are
+        // disconnected before they get invoked
+        foo.emit('bar');
+        expect(bar).not.toHaveBeenCalled();
+
+        // clean up the last handler
+        foo.disconnect(firstId);
+
+        // poke in private implementation to sanity-check no handlers left
+        expect(foo._signalConnections.length).toEqual(0);
+    });
+
+    it('distinguishes multiple signals', function () {
+        let bonk = jasmine.createSpy('bonk');
+        foo.connect('bar', bar);
+        foo.connect('bonk', bonk);
+        foo.connect('bar', bar);
+
+        foo.emit('bar');
+        expect(bar).toHaveBeenCalledTimes(2);
+        expect(bonk).not.toHaveBeenCalled();
+
+        foo.emit('bonk');
+        expect(bar).toHaveBeenCalledTimes(2);
+        expect(bonk).toHaveBeenCalledTimes(1);
+
+        foo.emit('bar');
+        expect(bar).toHaveBeenCalledTimes(4);
+        expect(bonk).toHaveBeenCalledTimes(1);
+
+        foo.disconnectAll();
+        bar.calls.reset();
+        bonk.calls.reset();
+
+        // these post-disconnect emissions should do nothing
+        foo.emit('bar');
+        foo.emit('bonk');
+        expect(bar).not.toHaveBeenCalled();
+        expect(bonk).not.toHaveBeenCalled();
+    });
+
+    describe('with exception in signal handler', function () {
+        let bar2;
+        beforeEach(function () {
+            bar.and.throwError('Exception we are throwing on purpose');
+            bar2 = jasmine.createSpy('bar');
+            foo.connect('bar', bar);
+            foo.connect('bar', bar2);
+            GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+                                     'JS ERROR: Exception in callback for signal: *');
+            foo.emit('bar');
+        });
+
+        it('does not affect other callbacks', function () {
+            expect(bar).toHaveBeenCalledTimes(1);
+            expect(bar2).toHaveBeenCalledTimes(1);
+        });
+
+        it('does not disconnect the callback', function () {
+            GLib.test_expect_message('Gjs', GLib.LogLevelFlags.LEVEL_WARNING,
+                                     'JS ERROR: Exception in callback for signal: *');
+            foo.emit('bar');
+            expect(bar).toHaveBeenCalledTimes(2);
+            expect(bar2).toHaveBeenCalledTimes(2);
+        });
+    });
+});
diff --git a/installed-tests/js/testSystem.js b/installed-tests/js/testSystem.js
index 9940d6c..927c715 100644
--- a/installed-tests/js/testSystem.js
+++ b/installed-tests/js/testSystem.js
@@ -1,18 +1,18 @@
-
-const JSUnit = imports.jsUnit;
 const System = imports.system;
 
-function testAddressOf() {
-    let o1 = new Object();
-    let o2 = new Object();
-
-    JSUnit.assert(System.addressOf(o1) == System.addressOf(o1));
-    JSUnit.assert(System.addressOf(o1) != System.addressOf(o2));
-}
-
-function testVersion() {
-    JSUnit.assert(System.version >= 13600);
-}
+describe('System.addressOf()', function () {
+    it('gives the same result for the same object', function () {
+        let o = {};
+        expect(System.addressOf(o)).toEqual(System.addressOf(o));
+    });
 
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+    it('gives different results for different objects', function () {
+        expect(System.addressOf({})).not.toEqual(System.addressOf({}));
+    });
+});
 
+describe('System.version', function () {
+    it('gives a plausible number', function () {
+        expect(System.version).not.toBeLessThan(13600);
+    });
+});
diff --git a/installed-tests/js/testTweener.js b/installed-tests/js/testTweener.js
index 1e94988..850bab3 100644
--- a/installed-tests/js/testTweener.js
+++ b/installed-tests/js/testTweener.js
@@ -1,10 +1,8 @@
-const JSUnit = imports.jsUnit;
 const Tweener = imports.tweener.tweener;
-const Mainloop = imports.mainloop;
 
 function installFrameTicker() {
-    // Set up Tweener to have a "frame pulse" from
-    // our main rendering loop
+    // Set up Tweener to have a "frame pulse" that the Jasmine clock functions
+    // can influence
     let ticker = {
         FRAME_RATE: 50,
 
@@ -14,19 +12,15 @@ function installFrameTicker() {
         start : function() {
             this._currentTime = 0;
 
-            let me = this;
-            this._timeoutID =
-                Mainloop.timeout_add(Math.floor(1000 / me.FRAME_RATE),
-                                     function() {
-                                         me._currentTime += 1000 / me.FRAME_RATE;
-                                         me.emit('prepare-frame');
-                                         return true;
-                                     });
+            this._timeoutID = setInterval(() => {
+                this._currentTime += 1000 / this.FRAME_RATE;
+                this.emit('prepare-frame');
+            }, Math.floor(1000 / this.FRAME_RATE));
         },
 
         stop : function() {
             if ('_timeoutID' in this) {
-                Mainloop.source_remove(this._timeoutID);
+                clearInterval(this._timeoutID);
                 delete this._timeoutID;
             }
 
@@ -42,448 +36,374 @@ function installFrameTicker() {
     Tweener.setFrameTicker(ticker);
 }
 
-function testSimpleTween() {
-    var objectA = {
-        x: 0,
-        y: 0
-    };
-
-    var objectB = {
-        x: 0,
-        y: 0
-    };
-
-    Tweener.addTween(objectA, { x: 10, y: 10, time: 1, transition: "linear",
-                                onComplete: function() { Mainloop.quit('testTweener');}});
-    Tweener.addTween(objectB, { x: 10, y: 10, time: 1, delay: 0.5, transition: "linear" });
-
-    Mainloop.run('testTweener');
-
-    with (objectA) {
-        JSUnit.assertEquals("A: x coordinate", 10, x);
-        JSUnit.assertEquals("A: y coordinate", 10, y);
-    }
-
-    with (objectB) {
-        JSUnit.assertEquals("B: x coordinate", 5, x);
-        JSUnit.assertEquals("B: y coordinate", 5, y);
-    }
-}
-
-function testOnFunctions() {
-    var object = {
-        start: false,
-        update: false,
-        complete: false
-    };
-
-    Tweener.addTween(object, { time: 0.1,
-                               onStart: function () {
-                                   object.start = true;
-                               },
-                               onUpdate: function () {
-                                   object.update = true;
-                               },
-                               onComplete: function() {
-                                   object.complete = true;
-                                   Mainloop.quit('testOnFunctions');
-                               }});
-
-    Mainloop.run('testOnFunctions');
-
-    with (object) {
-        JSUnit.assertEquals("onStart was run", true, start);
-        JSUnit.assertEquals("onUpdate was run", true, update);
-        JSUnit.assertEquals("onComplete was run", true, complete);
-    }
-}
-
-function testPause() {
-    var objectA = {
-        foo: 0
-    };
-
-    var objectB = {
-        bar: 0
-    };
-
-    var objectC = {
-        baaz: 0
-    };
-
-    Tweener.addTween(objectA, { foo: 100, time: 0.1 });
-    Tweener.addTween(objectC, { baaz: 100, time: 0.1 });
-    Tweener.addTween(objectB, { bar: 100, time: 0.1,
-                               onComplete: function() { Mainloop.quit('testPause');}});
-    Tweener.pauseTweens(objectA);
-    JSUnit.assertEquals(false, Tweener.pauseTweens(objectB, "quux")); // This should do nothing
-
-    /* Pause and resume should be equal to doing nothing */
-    Tweener.pauseTweens(objectC, "baaz");
-    Tweener.resumeTweens(objectC, "baaz");
-
-    Mainloop.run('testPause');
-
-    with (objectA) {
-        JSUnit.assertEquals(0, foo);
-    }
-
-    with (objectB) {
-        JSUnit.assertEquals(100, bar);
-    }
-
-    with (objectC) {
-        JSUnit.assertEquals(100, baaz);
-    }
-}
+describe('Tweener', function () {
+    beforeAll(function () {
+        jasmine.clock().install();
+        installFrameTicker();
+    });
+
+    afterAll(function () {
+        jasmine.clock().uninstall();
+    });
+
+    let start, update, overwrite, complete;
+    beforeEach(function () {
+        start = jasmine.createSpy('start');
+        update = jasmine.createSpy('update');
+        overwrite = jasmine.createSpy('overwrite');
+        complete = jasmine.createSpy('complete');
+    });
+
+    it('runs a simple tween', function () {
+        var objectA = {
+            x: 0,
+            y: 0
+        };
+
+        var objectB = {
+            x: 0,
+            y: 0
+        };
+
+        Tweener.addTween(objectA, { x: 10, y: 10, time: 1, transition: "linear" });
+        Tweener.addTween(objectB, { x: 10, y: 10, time: 1, delay: 0.5, transition: "linear" });
+
+        jasmine.clock().tick(1001);
+
+        expect(objectA.x).toEqual(10);
+        expect(objectA.y).toEqual(10);
+        expect(objectB.x).toEqual(5);
+        expect(objectB.y).toEqual(5);
+    });
+
+    it('calls callbacks during the tween', function () {
+        Tweener.addTween({}, {
+            time: 0.1,
+            onStart: start,
+            onUpdate: update,
+            onComplete: complete,
+        });
+
+        jasmine.clock().tick(101);
+        expect(start).toHaveBeenCalled();
+        expect(update).toHaveBeenCalled();
+        expect(complete).toHaveBeenCalled();
+    });
+
+    it('can pause tweens', function () {
+        var objectA = {
+            foo: 0
+        };
+
+        var objectB = {
+            bar: 0
+        };
+
+        var objectC = {
+            baaz: 0
+        };
+
+        Tweener.addTween(objectA, { foo: 100, time: 0.1 });
+        Tweener.addTween(objectC, { baaz: 100, time: 0.1 });
+        Tweener.addTween(objectB, { bar: 100, time: 0.1 });
+
+        Tweener.pauseTweens(objectA);
+        // This should do nothing
+        expect(Tweener.pauseTweens(objectB, 'quux')).toBeFalsy();
+        /* Pause and resume should be equal to doing nothing */
+        Tweener.pauseTweens(objectC, "baaz");
+        Tweener.resumeTweens(objectC, "baaz");
+
+        jasmine.clock().tick(101);
+
+        expect(objectA.foo).toEqual(0);
+        expect(objectB.bar).toEqual(100);
+        expect(objectC.baaz).toEqual(100);
+    });
+
+    it('can remove tweens', function () {
+        var object = {
+            foo: 0,
+            bar: 0,
+            baaz: 0
+        };
+
+        Tweener.addTween(object, { foo: 50, time: 0.1 });
+        Tweener.addTween(object, { bar: 50, time: 0.1 });
+        Tweener.addTween(object, { baaz: 50, time: 0.1});
+
+        /* The Tween on property foo should still be run after removing the other two */
+        Tweener.removeTweens(object, "bar", "baaz");
+
+        jasmine.clock().tick(101);
+
+        expect(object.foo).toEqual(50);
+        expect(object.bar).toEqual(0);
+        expect(object.baaz).toEqual(0);
+    });
+
+    it('overrides a tween with another one acting on the same object and property at the same time', 
function () {
+        var objectA = {
+            foo: 0
+        };
+
+        Tweener.addTween(objectA, { foo: 100, time: 0.1 });
+        Tweener.addTween(objectA, { foo: 0, time: 0.1 });
+
+        jasmine.clock().tick(101);
+
+        expect(objectA.foo).toEqual(0);
+    });
+
+    it('does not override a tween with another one acting not at the same time', function () {
+        var objectB = {
+            bar: 0
+        };
+
+        /* In this case both tweens should be executed, as they don't
+         * act on the object at the same time (the second one has a
+         * delay equal to the running time of the first one) */
+        Tweener.addTween(objectB, { bar: 100, time: 0.1 });
+        Tweener.addTween(objectB, { bar: 150, time: 0.1, delay: 0.1 });
+
+        jasmine.clock(0).tick(201);
+
+        expect(objectB.bar).toEqual(150);
+    });
+
+    it('can pause and resume all tweens', function () {
+        var objectA = {
+            foo: 0
+        };
+        var objectB = {
+            bar: 0
+        };
+
+        Tweener.addTween(objectA, { foo: 100, time: 0.1 });
+        Tweener.addTween(objectB, { bar: 100, time: 0.1 });
+
+        Tweener.pauseAllTweens();
+
+        jasmine.clock().tick(10);
+
+        Tweener.resumeAllTweens();
+
+        jasmine.clock().tick(101);
 
-function testRemoveTweens() {
-    var object = {
-        foo: 0,
-        bar: 0,
-        baaz: 0
-    };
-
-    Tweener.addTween(object, { foo: 50, time: 0.1,
-                               onComplete: function() { Mainloop.quit('testRemoveTweens');}});
-    Tweener.addTween(object, { bar: 50, time: 0.1 });
-    Tweener.addTween(object, { baaz: 50, time: 0.1});
-
-    /* The Tween on property foo should still be run after removing the other two */
-    Tweener.removeTweens(object, "bar", "baaz");
-    Mainloop.run('testRemoveTweens');
-
-    with (object) {
-        JSUnit.assertEquals(50, foo);
-        JSUnit.assertEquals(0, bar);
-        JSUnit.assertEquals(0, baaz);
-    }
-}
-
-function testConcurrent() {
-    var objectA = {
-        foo: 0
-    };
+        expect(objectA.foo).toEqual(100);
+        expect(objectB.bar).toEqual(100);
+    });
+
+    it('can remove all tweens', function () {
+        var objectA = {
+            foo: 0
+        };
+        var objectB = {
+            bar: 0
+        };
+
+        Tweener.addTween(objectA, { foo: 100, time: 0.1 });
+        Tweener.addTween(objectB, { bar: 100, time: 0.1 });
+
+        Tweener.removeAllTweens();
+
+        jasmine.clock().tick(200);
+
+        expect(objectA.foo).toEqual(0);
+        expect(objectB.bar).toEqual(0);
+    });
+
+    it('runs a tween with a time of 0 immediately', function () {
+        var object = {
+            foo: 100
+        };
+
+        Tweener.addTween(object, { foo: 50, time: 0, delay: 0 });
+        Tweener.addTween(object, { foo: 200, time: 0.1,
+            onStart: () => {
+                /* The immediate tween should set it to 50 before we run */
+                expect(object.foo).toEqual(50);
+            },
+        });
 
-    var objectB = {
-        bar: 0
-    };
-
-    /* The second Tween should override the first one, as
-     * they act on the same object, property, and at the same
-     * time.
-     */
-    Tweener.addTween(objectA, { foo: 100, time: 0.1 });
-    Tweener.addTween(objectA, { foo: 0, time: 0.1 });
-
-    /* In this case both tweens should be executed, as they don't
-     * act on the object at the same time (the second one has a
-     * delay equal to the running time of the first one) */
-    Tweener.addTween(objectB, { bar: 100, time: 0.1 });
-    Tweener.addTween(objectB, { bar: 150, time: 0.1, delay: 0.1,
-                               onComplete: function () {
-                                   Mainloop.quit('testConcurrent');}});
-
-    Mainloop.run('testConcurrent');
-
-    with (objectA) {
-        JSUnit.assertEquals(0, foo);
-    }
-
-    with (objectB) {
-        JSUnit.assertEquals(150, bar);
-    }
-}
-
-function testPauseAllResumeAll() {
-    var objectA = {
-        foo: 0
-    };
-    var objectB = {
-        bar: 0
-    };
+        jasmine.clock().tick(101);
 
-    Tweener.addTween(objectA, { foo: 100, time: 0.1 });
-    Tweener.addTween(objectB, { bar: 100, time: 0.1,
-                                onComplete: function () { Mainloop.quit('testPauseAllResumeAll');}});
+        expect(object.foo).toEqual(200);
+    });
 
-    Tweener.pauseAllTweens();
+    it('can call a callback a certain number of times', function () {
+        var object = {
+            foo: 0
+        };
+
+        Tweener.addCaller(object, {
+            onUpdate: () => { object.foo += 1; },
+            count: 10,
+            time: 0.1,
+        });
 
-    Mainloop.timeout_add(10, function () {
-                             Tweener.resumeAllTweens();
-                             return false;
-                         });
+        jasmine.clock().tick(101);
+
+        expect(object.foo).toEqual(10);
+    });
 
-    Mainloop.run('testPauseAllResumeAll');
+    it('can count the number of tweens on an object', function () {
+        var object = {
+            foo: 0,
+            bar: 0,
+            baaz: 0,
+            quux: 0
+        };
+
+        expect(Tweener.getTweenCount(object)).toEqual(0);
+
+        Tweener.addTween(object, { foo: 100, time: 0.1 });
+        expect(Tweener.getTweenCount(object)).toEqual(1);
+        Tweener.addTween(object, { bar: 100, time: 0.1 });
+        expect(Tweener.getTweenCount(object)).toEqual(2);
+        Tweener.addTween(object, { baaz: 100, time: 0.1 });
+        expect(Tweener.getTweenCount(object)).toEqual(3);
+        Tweener.addTween(object, { quux: 100, time: 0.1 });
+        expect(Tweener.getTweenCount(object)).toEqual(4);
 
-    with (objectA) {
-        JSUnit.assertEquals(100, foo);
-    }
-
-    with (objectB) {
-        JSUnit.assertEquals(100, bar);
-    }
-}
-
-function testRemoveAll() {
-    var objectA = {
-        foo: 0
-    };
-    var objectB = {
-        bar: 0
-    };
-
-    Tweener.addTween(objectA, { foo: 100, time: 0.1 });
-    Tweener.addTween(objectB, { bar: 100, time: 0.1 });
-
-    Tweener.removeAllTweens();
-
-    Mainloop.timeout_add(200,
-                        function () {
-                            JSUnit.assertEquals(0, objectA.foo);
-                            JSUnit.assertEquals(0, objectB.bar);
-                            Mainloop.quit('testRemoveAll');
-                            return false;
-                        });
-
-    Mainloop.run('testRemoveAll');
-}
-
-function testImmediateTween() {
-    var object = {
-        foo: 100
-    };
-
-    Tweener.addTween(object, { foo: 50, time: 0, delay: 0 });
-    Tweener.addTween(object, { foo: 200, time: 0.1,
-                               onStart: function () {
-                                   /* The immediate tween should set it to 50 before we run */
-                                   JSUnit.assertEquals(50, object.foo);
-                               },
-                               onComplete: function () {
-                                   Mainloop.quit('testImmediateTween');
-                               }});
-
-    Mainloop.run('testImmediateTween');
-
-    with (object) {
-        JSUnit.assertEquals(200, foo);
-    }
-}
-
-function testAddCaller() {
-    var object = {
-        foo: 0
-    };
-
-    Tweener.addCaller(object, { onUpdate: function () {
-                                    object.foo += 1;
-                                },
-                                onComplete: function () {
-                                    Mainloop.quit('testAddCaller');
-                                },
-                                count: 10,
-                                time: 0.1 });
-
-    Mainloop.run('testAddCaller');
-
-    with (object) {
-        JSUnit.assertEquals(10, foo);
-    }
-}
-
-function testGetTweenCount() {
-    var object = {
-        foo: 0,
-        bar: 0,
-        baaz: 0,
-        quux: 0
-    };
-
-    JSUnit.assertEquals(0, Tweener.getTweenCount(object));
-
-    Tweener.addTween(object, { foo: 100, time: 0.1 });
-    JSUnit.assertEquals(1, Tweener.getTweenCount(object));
-    Tweener.addTween(object, { bar: 100, time: 0.1 });
-    JSUnit.assertEquals(2, Tweener.getTweenCount(object));
-    Tweener.addTween(object, { baaz: 100, time: 0.1 });
-    JSUnit.assertEquals(3, Tweener.getTweenCount(object));
-    Tweener.addTween(object, { quux: 100, time: 0.1,
-                               onComplete: function () {
-                                   Mainloop.quit('testGetTweenCount');}});
-    JSUnit.assertEquals(4, Tweener.getTweenCount(object));
-
-    Tweener.removeTweens(object, "bar", "baaz");
-
-    JSUnit.assertEquals(2, Tweener.getTweenCount(object));
-
-    Mainloop.run('testGetTweenCount');
-
-    JSUnit.assertEquals(0, Tweener.getTweenCount(object));
-}
-
-Tweener.registerSpecialProperty(
-    'negative_x',
-    function(obj) { return -obj.x; },
-    function(obj, val) { obj.x = -val; }
-);
-
-function testSpecialProperty() {
-    var objectA = {
-        x: 0,
-        y: 0
-    };
-
-    Tweener.addTween(objectA, { negative_x: 10, y: 10, time: 1,
-                                transition: "linear",
-                                onComplete: function() { Mainloop.quit('testSpecialProperty');}});
-
-    Mainloop.run('testSpecialProperty');
-
-    with (objectA) {
-        JSUnit.assertEquals("A: x coordinate", -10, x);
-        JSUnit.assertEquals("A: y coordinate", 10, y);
-    }
-}
-
-Tweener.registerSpecialPropertyModifier('discrete',
-                                        discrete_modifier,
-                                        discrete_get);
-function discrete_modifier(props) {
-    return props.map(function (prop) { return { name: prop, parameters: null }; });
-}
-function discrete_get(begin, end, time, params) {
-    return Math.floor(begin + time * (end - begin));
-}
-
-function testSpecialPropertyModifier() {
-    var objectA = {
-        x: 0,
-        y: 0,
-        xFraction: false,
-        yFraction: false
-    };
-
-    Tweener.addTween(objectA, { x: 10, y: 10, time: 1,
-                                discrete: ["x"],
-                                transition: "linear",
-                                onUpdate: function() {
-                                    if (objectA.x != Math.floor(objectA.x))
-                                        objectA.xFraction = true;
-                                    if (objectA.y != Math.floor(objectA.y))
-                                        objectA.yFraction = true;
-                                },
-                                onComplete: function() { Mainloop.quit('testSpecialPropertyModifier');}});
-
-    Mainloop.run('testSpecialPropertyModifier');
-
-    with (objectA) {
-        JSUnit.assertEquals("A: x coordinate", 10, x);
-        JSUnit.assertEquals("A: y coordinate", 10, y);
-        JSUnit.assertEquals("A: x was fractional", false, xFraction);
-        JSUnit.assertEquals("A: y was fractional", true, yFraction);
-    }
-}
-
-Tweener.registerSpecialPropertySplitter(
-    'xnegy',
-    function(val) { return [ { name: "x", value: val },
-                             { name: "y", value: -val } ]; }
-);
-
-function testSpecialPropertySplitter() {
-    var objectA = {
-        x: 0,
-        y: 0
-    };
-
-    Tweener.addTween(objectA, { xnegy: 10, time: 1,
-                                transition: "linear",
-                                onComplete: function() { Mainloop.quit('testSpecialPropertySplitter');}});
-
-    Mainloop.run('testSpecialPropertySplitter');
-
-    with (objectA) {
-        JSUnit.assertEquals("A: x coordinate", 10, x);
-        JSUnit.assertEquals("A: y coordinate", -10, y);
-    }
-}
-
-function testTweenerOverwriteBeforeStart() {
-    var object = {
-        a: 0,
-        b: 0,
-        c: 0,
-        d: 0
-    };
-
-    var startCount = 0;
-    var overwriteCount = 0;
-    var completeCount = 0;
-
-    var tweenA = { a: 10, b: 10, c: 10, d: 10, time: 0.1,
-                   onStart: function() { startCount += 1; },
-                   onOverwrite: function() { overwriteCount += 1; },
-                   onComplete: function() { completeCount += 1; }
-                 };
-    var tweenB = { a: 20, b: 20, c: 20, d: 20, time: 0.1,
-                   onStart: function() { startCount += 1; },
-                   onOverwrite: function() { overwriteCount += 1; },
-                   onComplete: function() {
-                       completeCount += 1;
-                       Mainloop.quit('testTweenerOverwriteBeforeStart');
-                   }
-                 };
-
-    Tweener.addTween(object, tweenA);
-    Tweener.addTween(object, tweenB);
-
-    Mainloop.run('testTweenerOverwriteBeforeStart');
-
-    JSUnit.assertEquals(1, completeCount);
-    JSUnit.assertEquals(1, startCount);
-    JSUnit.assertEquals(1, overwriteCount);
-}
-
-function testTweenerOverwriteAfterStart() {
-    var object = {
-        a: 0,
-        b: 0,
-        c: 0,
-        d: 0
-    };
-
-    var startCount = 0;
-    var overwriteCount = 0;
-    var completeCount = 0;
-
-    var tweenA = { a: 10, b: 10, c: 10, d: 10, time: 0.1,
-                   onStart: function() {
-                     startCount += 1;
-                     Tweener.addTween(object, tweenB);
-                   },
-                   onOverwrite: function() { overwriteCount += 1; },
-                   onComplete: function() { completeCount += 1; }
-                 };
-    var tweenB = { a: 20, b: 20, c: 20, d: 20, time: 0.1,
-                   onStart: function() { startCount += 1; },
-                   onOverwrite: function() { overwriteCount += 1; },
-                   onComplete: function() {
-                       completeCount += 1;
-                       Mainloop.quit('testTweenerOverwriteAfterStart');
-                   }
-                 };
-
-    Tweener.addTween(object, tweenA);
-
-    Mainloop.run('testTweenerOverwriteAfterStart');
-
-    JSUnit.assertEquals(1, completeCount);
-    JSUnit.assertEquals(2, startCount);
-    JSUnit.assertEquals(1, overwriteCount);
-}
-
-installFrameTicker();
-JSUnit.gjstestRun(this, JSUnit.setUp, JSUnit.tearDown);
+        Tweener.removeTweens(object, 'bar', 'baaz');
+        expect(Tweener.getTweenCount(object)).toEqual(2);
+    });
+
+    it('can register special properties', function () {
+        Tweener.registerSpecialProperty(
+            'negative_x',
+            function(obj) { return -obj.x; },
+            function(obj, val) { obj.x = -val; }
+        );
+
+        var objectA = {
+            x: 0,
+            y: 0
+        };
 
+        Tweener.addTween(objectA, { negative_x: 10, y: 10, time: 1, transition: "linear" });
+
+        jasmine.clock().tick(1001);
+
+        expect(objectA.x).toEqual(-10);
+        expect(objectA.y).toEqual(10);
+    });
+
+    it('can register special modifiers for properties', function () {
+        Tweener.registerSpecialPropertyModifier('discrete',
+                                                discrete_modifier,
+                                                discrete_get);
+        function discrete_modifier(props) {
+            return props.map(function (prop) { return { name: prop, parameters: null }; });
+        }
+        function discrete_get(begin, end, time, params) {
+            return Math.floor(begin + time * (end - begin));
+        }
 
+        var objectA = {
+            x: 0,
+            y: 0,
+            xFraction: false,
+            yFraction: false
+        };
+
+        Tweener.addTween(objectA, { x: 10, y: 10, time: 1,
+            discrete: ["x"],
+            transition: "linear",
+            onUpdate: function() {
+                if (objectA.x != Math.floor(objectA.x))
+                    objectA.xFraction = true;
+                if (objectA.y != Math.floor(objectA.y))
+                    objectA.yFraction = true;
+            },
+        });
+
+        jasmine.clock().tick(1001);
+
+        expect(objectA.x).toEqual(10);
+        expect(objectA.y).toEqual(10);
+        expect(objectA.xFraction).toBeFalsy();
+        expect(objectA.yFraction).toBeTruthy();
+    });
+
+    it('can split properties into more than one special property', function () {
+        Tweener.registerSpecialPropertySplitter(
+            'xnegy',
+            function(val) { return [ { name: "x", value: val },
+                                     { name: "y", value: -val } ]; }
+        );
+
+        var objectA = {
+            x: 0,
+            y: 0
+        };
+
+        Tweener.addTween(objectA, { xnegy: 10, time: 1, transition: "linear" });
+
+        jasmine.clock().tick(1001);
+
+        expect(objectA.x).toEqual(10);
+        expect(objectA.y).toEqual(-10);
+    });
+
+    it('calls an overwrite callback when a tween is replaced', function () {
+        var object = {
+            a: 0,
+            b: 0,
+            c: 0,
+            d: 0
+        };
+
+        var tweenA = { a: 10, b: 10, c: 10, d: 10, time: 0.1,
+            onStart: start,
+            onOverwrite: overwrite,
+            onComplete: complete,
+        };
+        var tweenB = { a: 20, b: 20, c: 20, d: 20, time: 0.1,
+            onStart: start,
+            onOverwrite: overwrite,
+            onComplete: complete,
+        };
+
+        Tweener.addTween(object, tweenA);
+        Tweener.addTween(object, tweenB);
+
+        jasmine.clock().tick(101);
+
+        expect(start).toHaveBeenCalledTimes(1);
+        expect(overwrite).toHaveBeenCalledTimes(1);
+        expect(complete).toHaveBeenCalledTimes(1);
+    });
+
+    it('can still overwrite a tween after it has started', function () {
+        var object = {
+            a: 0,
+            b: 0,
+            c: 0,
+            d: 0
+        };
+
+        var tweenA = { a: 10, b: 10, c: 10, d: 10, time: 0.1,
+            onStart: () => {
+                start();
+                Tweener.addTween(object, tweenB);
+            },
+            onOverwrite: overwrite,
+            onComplete: complete,
+        };
+        var tweenB = { a: 20, b: 20, c: 20, d: 20, time: 0.1,
+            onStart: start,
+            onOverwrite: overwrite,
+            onComplete: complete,
+        };
+
+        Tweener.addTween(object, tweenA);
+
+        jasmine.clock().tick(121);
+
+        expect(start).toHaveBeenCalledTimes(2);
+        expect(overwrite).toHaveBeenCalledTimes(1);
+        expect(complete).toHaveBeenCalledTimes(1);
+    });
+});
diff --git a/installed-tests/js/testself.js b/installed-tests/js/testself.js
index a07de52..5899ab5 100644
--- a/installed-tests/js/testself.js
+++ b/installed-tests/js/testself.js
@@ -1,37 +1,30 @@
-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(() => { 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(() => expect(true).toThrow()).toThrow();
+        expect(() => true).not.toThrow();
+    });
+});
diff --git a/installed-tests/minijasmine.cpp b/installed-tests/minijasmine.cpp
new file mode 100644
index 0000000..5cf3e59
--- /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; see test log for assertions\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;
+}
diff --git a/installed-tests/minijasmine.test.in b/installed-tests/minijasmine.test.in
new file mode 100644
index 0000000..0069da3
--- /dev/null
+++ b/installed-tests/minijasmine.test.in
@@ -0,0 +1,4 @@
+[Test]
+Type=session
+Exec=@pkglibexecdir@/installed-tests/minijasmine @pkglibexecdir@/installed-tests/js/@name@
+Output=TAP



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