diff --git a/.eslintrc b/.eslintrc
index 50c38d583..c9a2a7ecc 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -1,9 +1,10 @@
 {
   "env": {
-    "node": true
+    "node": true,
+    "es6": true
   },
   "parserOptions": {
-    "ecmaVersion": 5
+    "ecmaVersion": 9
   },
   "globals": {
     "window": false,
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 4dcf1a832..d2fa953a2 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -17,9 +17,9 @@ jobs:
     strategy:
       matrix:
         node:
-          - 16
           - 18
           - 20
+          - 22
     timeout-minutes: 10
     steps:
       - uses: actions/checkout@v2
diff --git a/src/components.ts b/src/components.ts
index 948bd958e..26ea05a08 100644
--- a/src/components.ts
+++ b/src/components.ts
@@ -146,7 +146,7 @@ export abstract class Component<T extends object = object> extends Controller<T>
     });
     return function componentBindWrapper(...args) {
       if (!_fn) return;
-      return _fn.apply(_component, ...args);
+      return _fn.apply(_component, args);
     };
   }
 
diff --git a/test/browser/util.js b/test/browser/util.js
deleted file mode 100644
index 3b57025c8..000000000
--- a/test/browser/util.js
+++ /dev/null
@@ -1,6 +0,0 @@
-var chai = require('chai');
-var DerbyStandalone = require('../../src/DerbyStandalone');
-require('../../src/parsing');
-require('../../test-utils').assertions(window, chai.Assertion);
-
-exports.derby = new DerbyStandalone();
diff --git a/test/dom/ComponentHarness.mocha.js b/test/dom/ComponentHarness.mocha.js
index 3273e7bd6..eef5ff9d0 100644
--- a/test/dom/ComponentHarness.mocha.js
+++ b/test/dom/ComponentHarness.mocha.js
@@ -1,23 +1,23 @@
-var expect = require('chai').expect;
-var Component = require('../../src/components').Component;
-var domTestRunner = require('../../src/test-utils/domTestRunner');
+const expect = require('chai').expect;
+const Component = require('../../src/components').Component;
+const domTestRunner = require('../../src/test-utils/domTestRunner');
 
 describe('ComponentHarness', function() {
-  var runner = domTestRunner.install();
+  const runner = domTestRunner.install();
 
   describe('renderDom', function() {
     it('returns a page object', function() {
       function Box() {}
       Box.view = {is: 'box'};
-      var harness = runner.createHarness('<view is="box" />', Box);
-      var page = harness.renderDom();
+      const harness = runner.createHarness('<view is="box" />', Box);
+      const page = harness.renderDom();
       expect(page).instanceof(harness.app.Page);
     });
 
     it('sets component property on returned object', function() {
       function Box() {}
       Box.view = {is: 'box'};
-      var box = runner.createHarness('<view is="box" />', Box).renderDom().component;
+      const box = runner.createHarness('<view is="box" />', Box).renderDom().component;
       expect(box).instanceof(Box);
     });
 
@@ -27,7 +27,7 @@ describe('ComponentHarness', function() {
         is: 'box',
         source: '<index:><div class="box"></div>'
       };
-      var fragment = runner.createHarness('<view is="box" />', Box).renderDom().fragment;
+      const fragment = runner.createHarness('<view is="box" />', Box).renderDom().fragment;
       expect(fragment).instanceof(runner.window.DocumentFragment);
       expect(fragment).html('<div class="box"></div>');
     });
@@ -49,7 +49,7 @@ describe('ComponentHarness', function() {
           '<index:>' +
             '<div class="clown"></div>'
       };
-      var box = runner.createHarness('<view is="box" />', Box, Clown).renderDom().component;
+      const box = runner.createHarness('<view is="box" />', Box, Clown).renderDom().component;
       expect(box.myClown).instanceof(Clown);
     });
 
@@ -61,9 +61,9 @@ describe('ComponentHarness', function() {
           '<index:>' +
             '<div class="box {{if open}}open{{/if}}"></div>'
       };
-      var page = runner.createHarness('<view is="box" />', Box).renderDom();
-      var fragment = page.fragment;
-      var component = page.component;
+      const page = runner.createHarness('<view is="box" />', Box).renderDom();
+      const fragment = page.fragment;
+      const component = page.component;
       expect(fragment).html('<div class="box "></div>');
       component.model.set('open', true);
       expect(fragment).html('<div class="box open"></div>');
@@ -77,8 +77,8 @@ describe('ComponentHarness', function() {
           '<index:>' +
             '<div as="container" class="box {{if open}}open{{/if}}"></div>'
       };
-      var component = runner.createHarness('<view is="box" />', Box).renderDom().component;
-      var container = component.container;
+      const component = runner.createHarness('<view is="box" />', Box).renderDom().component;
+      const container = component.container;
       expect(container.className).equal('box ');
       component.model.set('open', true);
       expect(container.className).equal('box open');
@@ -96,9 +96,9 @@ describe('ComponentHarness', function() {
               '{{/unless}}' +
             '</div>'
       };
-      var page = runner.createHarness('<view is="box" />', Box)
+      const page = runner.createHarness('<view is="box" />', Box)
         .stubComponent('clown').renderDom();
-      var model = page.component.model;
+      const model = page.component.model;
       expect(page.clown).instanceof(Component);
       model.set('hideClown', true);
       expect(page.clown).equal(undefined);
@@ -121,10 +121,10 @@ describe('ComponentHarness', function() {
               '{{/if}}' +
             '</div>'
       };
-      var page = runner.createHarness('<view is="box" show-happy />', Box)
+      const page = runner.createHarness('<view is="box" show-happy />', Box)
         .stubComponent({is: 'clown', asArray: 'clowns'}).renderDom();
-      var clowns = page.clowns;
-      var model = page.component.model;
+      const clowns = page.clowns;
+      const model = page.component.model;
       expect(clowns.length).equal(1);
       expect(clowns[0].model.get('expression')).equal('happy');
       model.set('showSad', true);
@@ -146,7 +146,7 @@ describe('ComponentHarness', function() {
         is: 'box',
         source: '<index:><div class="box"></div>'
       };
-      var harness = runner.createHarness('<view is="box" />', Box);
+      const harness = runner.createHarness('<view is="box" />', Box);
       expect(harness).to.render();
     });
 
@@ -156,7 +156,7 @@ describe('ComponentHarness', function() {
         is: 'box',
         source: '<index:><p><div></div></p>'
       };
-      var harness = runner.createHarness('<view is="box" />', Box);
+      const harness = runner.createHarness('<view is="box" />', Box);
       expect(harness).not.to.render();
     });
 
@@ -166,7 +166,7 @@ describe('ComponentHarness', function() {
         is: 'box',
         source: '<index:><table><tr><td></td></tr></table>'
       };
-      var harness = runner.createHarness('<view is="box" />', Box);
+      const harness = runner.createHarness('<view is="box" />', Box);
       expect(harness).not.to.render();
     });
 
@@ -176,7 +176,7 @@ describe('ComponentHarness', function() {
         is: 'box',
         source: '<index:><div class="box"></div>'
       };
-      var harness = runner.createHarness('<view is="box" />', Box);
+      const harness = runner.createHarness('<view is="box" />', Box);
       expect(harness).to.render('<div class="box"></div>');
     });
 
@@ -186,14 +186,14 @@ describe('ComponentHarness', function() {
         is: 'box',
         source: '<index:><p><div></div></p>'
       };
-      var harness = runner.createHarness('<view is="box" />', Box);
+      const harness = runner.createHarness('<view is="box" />', Box);
       expect(harness).not.to.render('<p><div></div></p>');
     });
 
     it('passes with blank view', function() {
       function Box() {}
       Box.view = {is: 'box'};
-      var harness = runner.createHarness('<view is="box" />', Box);
+      const harness = runner.createHarness('<view is="box" />', Box);
       expect(harness).to.render('');
     });
 
@@ -206,12 +206,12 @@ describe('ComponentHarness', function() {
       Box.prototype.create = function() {
         this.boxElement.className = 'box-changed-in-create';
       };
-      var harness = runner.createHarness('<view is="box" />', Box);
+      const harness = runner.createHarness('<view is="box" />', Box);
       expect(harness).to.render('<div class="box"></div>');
     });
 
     it('works with HTML entities like &nbsp;', function() {
-      var harness = runner.createHarness('&lt;&nbsp;&quot;&gt;');
+      const harness = runner.createHarness('&lt;&nbsp;&quot;&gt;');
       expect(harness).to.render();
       expect(harness).to.render('&lt;&nbsp;"&gt;');
     });
@@ -223,7 +223,7 @@ describe('ComponentHarness', function() {
         source: '<index:><div class="box">{{greeting}}</div>'
       };
       Box.prototype.init = function() {
-        var initialName = this.model.scope('_page.initialName').get();
+        const initialName = this.model.scope('_page.initialName').get();
         expect(initialName).to.equal('Spot');
         this.model.set('name', initialName);
         this.model.start('greeting', ['name'], function(name) {
@@ -233,7 +233,7 @@ describe('ComponentHarness', function() {
           return 'Hello, ' + name;
         });
       };
-      var harness = runner.createHarness('<view is="box" />', Box);
+      const harness = runner.createHarness('<view is="box" />', Box);
       // Have the test depend on state in `_page` to make sure it's not cleared
       // between rendering passes in `.to.render`.
       harness.model.set('_page.initialName', 'Spot');
@@ -244,15 +244,15 @@ describe('ComponentHarness', function() {
 
   describe('fake app.history implementation', function() {
     it('accepts url option', function() {
-      var renderUrl = '/box?size=123';
-      var expectedQueryParams = {size: '123'};
+      const renderUrl = '/box?size=123';
+      const expectedQueryParams = {size: '123'};
 
-      var harness = runner.createHarness(
+      const harness = runner.createHarness(
         'url: {{$render.url}} | query: {{JSON.stringify($render.query)}}'
       );
-      var expectedHtml = 'url: /box?size=123 | query: {"size":"123"}';
+      const expectedHtml = 'url: /box?size=123 | query: {"size":"123"}';
 
-      var page = harness.renderHtml({url: renderUrl});
+      const page = harness.renderHtml({url: renderUrl});
       expectPageParams(page, renderUrl, expectedQueryParams);
       expect(page.html).to.equal(expectedHtml);
 
@@ -262,14 +262,14 @@ describe('ComponentHarness', function() {
     });
 
     it('supports push(url) and replace(url)', function() {
-      var harness = runner.createHarness(
+      const harness = runner.createHarness(
         'url: {{$render.url}} | query: {{JSON.stringify($render.query)}}'
       );
 
-      var page = harness.renderDom();
+      const page = harness.renderDom();
       expectPageParams(page, '', {});
 
-      var newUrl = '/box?size=123';
+      const newUrl = '/box?size=123';
       harness.app.history.push(newUrl);
       expectPageParams(page, newUrl, {size: '123'});
       expect(page.fragment).html('url: /box?size=123 | query: {"size":"123"}');
diff --git a/test/browser/as.mocha.js b/test/dom/as.mocha.js
similarity index 81%
rename from test/browser/as.mocha.js
rename to test/dom/as.mocha.js
index 0c322601d..9e503be29 100644
--- a/test/browser/as.mocha.js
+++ b/test/dom/as.mocha.js
@@ -1,31 +1,45 @@
-var expect = require('chai').expect;
-var derby = require('./util').derby;
+const expect = require('chai').expect;
+const domTestRunner = require('../../src/test-utils/domTestRunner');
 
 describe('as', function() {
+  const runner = domTestRunner.install({
+    jsdomOptions: {
+      // solution for `SecurityError: localStorage is not available for opaque origins`
+      // Racer interfaces with localStorage and the `as-object` tests use `page.model`
+      // methods causing the SecurityError if `url` is not set. Does not appear to impact
+      // `as-array` tests even though they also use `page.model` methods so 🤷
+      url: 'http://localhost/'
+    }
+  });
+
   it('HTML element `as` property', function() {
-    var app = derby.createApp();
+    const { app } = runner.createHarness();
     app.views.register('Body', '<div as="nested[0]"></div>');
-    var page = app.createPage();
-    var fragment = page.getFragment('Body');
+    const page = app.createPage();
+    const fragment = page.getFragment('Body');
     expect(page.nested[0]).html('<div></div>');
     expect(fragment).html('<div></div>');
   });
 
   it('Component `as` property', function() {
-    var app = derby.createApp();
+    const { app } = runner.createHarness();
     app.views.register('Body', '<view is="item" as="nested[0]"></view>');
     app.views.register('item', '<div></div>')
     function Item() {};
     app.component('item', Item);
-    var page = app.createPage();
-    var fragment = page.getFragment('Body');
+    const page = app.createPage();
+    const fragment = page.getFragment('Body');
     expect(page.nested[0]).instanceof(Item);
     expect(page.nested[0].markerNode.nextSibling).html('<div></div>');
     expect(fragment).html('<div></div>');
   });
 
-  it('HTML element `as-object` property', function() {
-    var app = derby.createApp();
+  async function nextTick() {
+    return new Promise((resolve) => process.nextTick(resolve));
+  }
+
+  it('HTML element `as-object` property', async function() {
+    const { app } = runner.createHarness();
     app.views.register('Body',
       '<ul>' +
         '{{each _page.items}}' +
@@ -33,13 +47,13 @@ describe('as', function() {
         '{{/each}}' +
       '</ul>'
     );
-    var page = app.createPage();
+    const page = app.createPage();
     page.model.set('_page.items', [
       {id: 'a', text: 'A'},
       {id: 'b', text: 'B'},
       {id: 'c', text: 'C'}
     ]);
-    var fragment = page.getFragment('Body');
+    const fragment = page.getFragment('Body');
 
     expect(page.nested.map).all.keys('a', 'b', 'c');
     expect(page.nested.map.a).html('<li>A</li>');
@@ -48,12 +62,15 @@ describe('as', function() {
     expect(fragment).html('<ul><li>A</li><li>B</li><li>C</li></ul>');
 
     page.model.remove('_page.items', 1);
+
+    await nextTick();
     expect(page.nested.map).all.keys('a', 'c');
     expect(page.nested.map.a).html('<li>A</li>');
     expect(page.nested.map.c).html('<li>C</li>');
     expect(fragment).html('<ul><li>A</li><li>C</li></ul>');
 
     page.model.unshift('_page.items', {id: 'd', text: 'D'});
+    await nextTick();
     expect(page.nested.map).all.keys('a', 'c', 'd');
     expect(page.nested.map.a).html('<li>A</li>');
     expect(page.nested.map.c).html('<li>C</li>');
@@ -61,12 +78,13 @@ describe('as', function() {
     expect(fragment).html('<ul><li>D</li><li>A</li><li>C</li></ul>');
 
     page.model.del('_page.items');
+    await nextTick();
     expect(page.nested.map).eql({});
     expect(fragment).html('<ul></ul>');
   });
 
-  it('Component `as-object` property', function() {
-    var app = derby.createApp();
+  it('Component `as-object` property', async function() {
+    const { app } = runner.createHarness();
     app.views.register('Body',
       '<ul>' +
         '{{each _page.items}}' +
@@ -79,13 +97,13 @@ describe('as', function() {
     app.views.register('item', '<li>{{@content}}</li>');
     function Item() {};
     app.component('item', Item);
-    var page = app.createPage();
+    const page = app.createPage();
     page.model.set('_page.items', [
       {id: 'a', text: 'A'},
       {id: 'b', text: 'B'},
       {id: 'c', text: 'C'}
     ]);
-    var fragment = page.getFragment('Body');
+    const fragment = page.getFragment('Body');
 
     expect(page.nested.map).all.keys('a', 'b', 'c');
     expect(page.nested.map.a).instanceof(Item);
@@ -97,6 +115,8 @@ describe('as', function() {
     expect(fragment).html('<ul><li>A</li><li>B</li><li>C</li></ul>');
 
     page.model.remove('_page.items', 1);
+
+    await nextTick();
     expect(page.nested.map).all.keys('a', 'c');
     expect(page.nested.map.a).instanceof(Item);
     expect(page.nested.map.c).instanceof(Item);
@@ -105,6 +125,7 @@ describe('as', function() {
     expect(fragment).html('<ul><li>A</li><li>C</li></ul>');
 
     page.model.unshift('_page.items', {id: 'd', text: 'D'});
+    await nextTick();
     expect(page.nested.map).all.keys('a', 'c', 'd');
     expect(page.nested.map.a).instanceof(Item);
     expect(page.nested.map.c).instanceof(Item);
@@ -115,12 +136,13 @@ describe('as', function() {
     expect(fragment).html('<ul><li>D</li><li>A</li><li>C</li></ul>');
 
     page.model.del('_page.items');
+    await nextTick();
     expect(page.nested.map).eql({});
     expect(fragment).html('<ul></ul>');
   });
 
   it('HTML element `as-array` property', function() {
-    var app = derby.createApp();
+    const { app } = runner.createHarness();
     app.views.register('Body',
       '<ul>' +
         '{{each _page.items}}' +
@@ -128,13 +150,13 @@ describe('as', function() {
         '{{/each}}' +
       '</ul>'
     );
-    var page = app.createPage();
+    const page = app.createPage();
     page.model.set('_page.items', [
       {id: 'a', text: 'A'},
       {id: 'b', text: 'B'},
       {id: 'c', text: 'C'}
     ]);
-    var fragment = page.getFragment('Body');
+    const fragment = page.getFragment('Body');
 
     expect(page.nested.list).an('array');
     expect(page.nested.list).length(3);
@@ -162,7 +184,7 @@ describe('as', function() {
   });
 
   it('Component `as-array` property', function() {
-    var app = derby.createApp();
+    const { app } = runner.createHarness();
     app.views.register('Body',
       '<ul>' +
         '{{each _page.items}}' +
@@ -175,13 +197,13 @@ describe('as', function() {
     app.views.register('item', '<li>{{@content}}</li>');
     function Item() {};
     app.component('item', Item);
-    var page = app.createPage();
+    const page = app.createPage();
     page.model.set('_page.items', [
       {id: 'a', text: 'A'},
       {id: 'b', text: 'B'},
       {id: 'c', text: 'C'}
     ]);
-    var fragment = page.getFragment('Body');
+    const fragment = page.getFragment('Body');
 
     expect(page.nested.list).an('array');
     expect(page.nested.list).length(3);
diff --git a/test/dom/bindings.mocha.js b/test/dom/bindings.mocha.js
index 5f724cbce..87f1b75b0 100644
--- a/test/dom/bindings.mocha.js
+++ b/test/dom/bindings.mocha.js
@@ -1,22 +1,22 @@
-var expect = require('chai').expect;
-var domTestRunner = require('../../src/test-utils/domTestRunner');
+const expect = require('chai').expect;
+const domTestRunner = require('../../src/test-utils/domTestRunner');
 
 describe('bindings', function() {
-  var runner = domTestRunner.install();
+  const runner = domTestRunner.install();
 
   describe('bracket dependencies', function() {
     it('bracket inner dependency change', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body', '{{_page.doc[_page.key]}}');
-      var page = app.createPage();
-      var doc = page.model.at('_page.doc');
-      var key = page.model.at('_page.key');
+      const page = app.createPage();
+      const doc = page.model.at('_page.doc');
+      const key = page.model.at('_page.key');
       doc.set({
         one: 'hi',
         two: 'bye'
       });
       key.set('one');
-      var fragment = page.getFragment('Body');
+      const fragment = page.getFragment('Body');
       expect(fragment).html('hi');
       key.set('two');
       expect(fragment).html('bye');
@@ -27,17 +27,17 @@ describe('bindings', function() {
     });
 
     it('bracket outer dependency change', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body', '{{_page.doc[_page.key]}}');
-      var page = app.createPage();
-      var doc = page.model.at('_page.doc');
-      var key = page.model.at('_page.key');
+      const page = app.createPage();
+      const doc = page.model.at('_page.doc');
+      const key = page.model.at('_page.key');
       doc.set({
         one: 'hi',
         two: 'bye'
       });
       key.set('one');
-      var fragment = page.getFragment('Body');
+      const fragment = page.getFragment('Body');
       expect(fragment).html('hi');
       doc.set('one', 'hello')
       expect(fragment).html('hello');
@@ -50,17 +50,17 @@ describe('bindings', function() {
     });
 
     it('bracket inner then outer dependency change', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body', '{{_page.doc[_page.key]}}');
-      var page = app.createPage();
-      var doc = page.model.at('_page.doc');
-      var key = page.model.at('_page.key');
+      const page = app.createPage();
+      const doc = page.model.at('_page.doc');
+      const key = page.model.at('_page.key');
       doc.set({
         one: 'hi',
         two: 'bye'
       });
       key.set('one');
-      var fragment = page.getFragment('Body');
+      const fragment = page.getFragment('Body');
       expect(fragment).html('hi');
       key.set('two');
       expect(fragment).html('bye');
@@ -78,16 +78,16 @@ describe('bindings', function() {
 
   describe('dynamic view instances', function() {
     it('simple dynamic view', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '<view is="{{_page.view}}" optional></view>'
       );
       app.views.register('one', 'One');
       app.views.register('two', 'Two');
-      var page = app.createPage();
-      var view = page.model.at('_page.view');
+      const page = app.createPage();
+      const view = page.model.at('_page.view');
       view.set('one');
-      var fragment = page.getFragment('Body');
+      const fragment = page.getFragment('Body');
       expect(fragment).html('One');
       view.set('two');
       expect(fragment).html('Two');
@@ -97,18 +97,18 @@ describe('bindings', function() {
       expect(fragment).html('One');
     });
     it('bracketed dynamic view', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '<view is="{{_page.names[_page.index]}}" optional></view>'
       );
       app.views.register('one', 'One');
       app.views.register('two', 'Two');
       app.views.register('three', 'Three');
-      var page = app.createPage();
+      const page = app.createPage();
       page.model.set('_page.names', ['one', 'two']);
-      var index = page.model.at('_page.index');
+      const index = page.model.at('_page.index');
       index.set(0);
-      var fragment = page.getFragment('Body');
+      const fragment = page.getFragment('Body');
       expect(fragment).html('One');
       index.set(1);
       expect(fragment).html('Two');
@@ -122,8 +122,8 @@ describe('bindings', function() {
       expect(fragment).html('Three');
     });
     it('only renders if the expression value changes', function() {
-      var app = runner.createHarness().app;
-      var count = 0;
+      const app = runner.createHarness().app;
+      const count = 0;
       app.proto.count = function() {
         return count++;
       };
@@ -133,10 +133,10 @@ describe('bindings', function() {
       app.views.register('Body', '<view is="{{lower(_page.view)}}"></view>');
       app.views.register('one', 'One {{count()}}');
       app.views.register('two', 'Two {{count()}}');
-      var page = app.createPage();
-      var view = page.model.at('_page.view');
+      const page = app.createPage();
+      const view = page.model.at('_page.view');
       view.set('one');
-      var fragment = page.getFragment('Body');
+      const fragment = page.getFragment('Body');
       expect(fragment).html('One 0');
       view.set('two');
       expect(fragment).html('Two 1');
@@ -151,7 +151,7 @@ describe('bindings', function() {
 
   describe('basic blocks', function() {
     it('if', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '{{if _page.nested.value}}' +
           '{{this}}.' +
@@ -159,10 +159,10 @@ describe('bindings', function() {
           'otherwise' +
         '{{/if}}'
       );
-      var page = app.createPage();
-      var fragment = page.getFragment('Body');
+      const page = app.createPage();
+      const fragment = page.getFragment('Body');
       expect(fragment).html('otherwise');
-      var value = page.model.at('_page.nested.value');
+      const value = page.model.at('_page.nested.value');
       value.set(true);
       expect(fragment).html('true.');
       value.set(false);
@@ -171,7 +171,7 @@ describe('bindings', function() {
       expect(fragment).html('hello.');
     });
     it('unless', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '{{unless _page.nested.value}}' +
           'nada' +
@@ -179,10 +179,10 @@ describe('bindings', function() {
           'otherwise' +
         '{{/unless}}'
       );
-      var page = app.createPage();
-      var fragment = page.getFragment('Body');
+      const page = app.createPage();
+      const fragment = page.getFragment('Body');
       expect(fragment).html('nada');
-      var value = page.model.at('_page.nested.value');
+      const value = page.model.at('_page.nested.value');
       value.set(true);
       expect(fragment).html('otherwise');
       value.set(false);
@@ -191,7 +191,7 @@ describe('bindings', function() {
       expect(fragment).html('otherwise');
     });
     it('each else', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '{{each _page.items}}' +
           '{{this}}.' +
@@ -199,10 +199,10 @@ describe('bindings', function() {
           'otherwise' +
         '{{/each}}'
       );
-      var page = app.createPage();
-      var fragment = page.getFragment('Body');
+      const page = app.createPage();
+      const fragment = page.getFragment('Body');
       expect(fragment).html('otherwise');
-      var items = page.model.at('_page.items');
+      const items = page.model.at('_page.items');
       items.set(['one', 'two', 'three']);
       expect(fragment).html('one.two.three.');
       items.set([]);
@@ -218,7 +218,7 @@ describe('bindings', function() {
 
   describe('nested blocks', function() {
     it('each containing if', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '{{each _page.items as #item}}' +
           '{{if _page.toggle}}' +
@@ -226,10 +226,10 @@ describe('bindings', function() {
           '{{/if}}' +
         '{{/each}}'
       );
-      var page = app.createPage();
-      var items = page.model.at('_page.items');
-      var toggle = page.model.at('_page.toggle');
-      var fragment = page.getFragment('Body');
+      const page = app.createPage();
+      const items = page.model.at('_page.items');
+      const toggle = page.model.at('_page.toggle');
+      const fragment = page.getFragment('Body');
       items.set(['one', 'two', 'three']);
       toggle.set(true);
       items.move(2, 1);
@@ -239,7 +239,7 @@ describe('bindings', function() {
 
   describe('as properties', function() {
     it('conditionally rendered', function(done) {
-      var harness = runner.createHarness(`
+      const harness = runner.createHarness(`
       <view is="box" as="box"/>
     `);
       function Box() {}
@@ -254,12 +254,12 @@ describe('bindings', function() {
           {{/if}}>
         `
       };
-      var app = harness.app;
+      const app = harness.app;
       app.component(Box);
-      var page = harness.renderDom();
-      var value = page.component.model.at('_page.foo');
+      const page = harness.renderDom();
+      const value = page.component.model.at('_page.foo');
       value.set(true);
-      var initialElement = page.box.myDiv;
+      const initialElement = page.box.myDiv;
       expect(page.box.myDiv, 'check pre value change')
         .instanceOf(Object)
         .to.have.property('textContent', 'one');
@@ -275,7 +275,7 @@ describe('bindings', function() {
 
     ['__proto__', 'constructor'].forEach(function(badKey) {
       it(`disallows prototype modification with ${badKey}`, function() {
-        var harness = runner.createHarness(`
+        const harness = runner.createHarness(`
           <view is="box"/>
         `);
         function Box() {}
@@ -286,7 +286,7 @@ describe('bindings', function() {
               <div as="${badKey}">one</div>
           `
         };
-        var app = harness.app;
+        const app = harness.app;
         app.component(Box);
         expect(() => harness.renderDom()).to.throw(`Unsafe key "${badKey}"`);
         // Rendering to HTML string should still work, as that doesn't process `as` attributes
@@ -297,7 +297,7 @@ describe('bindings', function() {
 
   function testArray(itemTemplate, itemData) {
     it('each on path', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '<ul>' +
           '{{each _page.items as #item, #i}}' + itemTemplate + '{{/each}}' +
@@ -306,7 +306,7 @@ describe('bindings', function() {
       testEach(app);
     });
     it('each on alias', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '{{with _page.items as #items}}' +
           '<ul>' +
@@ -317,7 +317,7 @@ describe('bindings', function() {
       testEach(app);
     });
     it('each on relative path', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '{{with _page.items}}' +
           '<ul>' +
@@ -328,7 +328,7 @@ describe('bindings', function() {
       testEach(app);
     });
     it('each on relative subpath', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '{{with _page}}' +
           '<ul>' +
@@ -339,7 +339,7 @@ describe('bindings', function() {
       testEach(app);
     });
     it('each on attribute', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '<view is="list" items="{{_page.items}}"></view>'
       );
@@ -351,7 +351,7 @@ describe('bindings', function() {
       testEach(app);
     });
     it('each containing withs', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '<ul>' +
           '{{each _page.items as #item, #i}}' +
@@ -368,7 +368,7 @@ describe('bindings', function() {
       testEach(app);
     });
     it('each containing view instance', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '<ul>' +
           '{{each _page.items as #item, #i}}' +
@@ -380,7 +380,7 @@ describe('bindings', function() {
       testEach(app);
     });
     it('each containing view instance containing with', function() {
-      var app = runner.createHarness().app;
+      const app = runner.createHarness().app;
       app.views.register('Body',
         '<ul>' +
           '{{each _page.items as #item, #i}}' +
@@ -392,9 +392,9 @@ describe('bindings', function() {
       testEach(app);
     });
     function testEach(app) {
-      var page = app.createPage();
-      var items = page.model.at('_page.items');
-      var fragment = page.getFragment('Body');
+      const page = app.createPage();
+      const items = page.model.at('_page.items');
+      const fragment = page.getFragment('Body');
       expect(fragment).html('<ul></ul>');
       items.insert(0, itemData.slice(0, 2));
       expect(fragment).html(
@@ -448,7 +448,7 @@ describe('bindings', function() {
   });
 
   it('array item binding with view function calls', function() {
-    var app = runner.createHarness().app;
+    const app = runner.createHarness().app;
     app.views.register('Body', '<view is="box" list="{{_page.list}}"/ boxName="My box"/>');
     function Box() {}
     Box.view = {
@@ -471,8 +471,8 @@ describe('bindings', function() {
       }
       return str.length;
     };
-    var page = app.createPage();
-    var $items = page.model.at('_page.list.items');
+    const page = app.createPage();
+    const $items = page.model.at('_page.list.items');
     $items.set(['alpha', 'beta']);
     // if getFragment called before second set() call, bindings are evaluated
     // multiple times, leading to suggested bug below of len() called w undefined
@@ -502,7 +502,7 @@ describe('bindings', function() {
   // This is solved by having Derby register its catch-all listeners using
   // the *Immediate events, which operate outside the mutator event queue.
   it('array chained insertions at index 0', function() {
-    var app = runner.createHarness().app;
+    const app = runner.createHarness().app;
     app.views.register('Body',
       '<ul>' +
         '{{each _data.items as #item}}' +
@@ -511,16 +511,16 @@ describe('bindings', function() {
       '</ul>'
     );
 
-    var page = app.createPage();
+    const page = app.createPage();
     page.model.on('insert', '_data.items', function(index, values) {
       if (values[0] === 'B') {
         page.model.insert('_data.items', 0, 'C');
       }
     });
-    var $items = page.model.at('_data.items');
+    const $items = page.model.at('_data.items');
     $items.set(['A']);
 
-    var fragment = page.getFragment('Body');
+    const fragment = page.getFragment('Body');
     expect(fragment).html('<ul><li>A</li></ul>');
     $items.insert(0, 'B');
     expect(fragment).html('<ul><li>C</li><li>B</li><li>A</li></ul>');
@@ -531,7 +531,7 @@ describe('bindings', function() {
     // which handle binding updates. The event model expects that any numeric
     // path segments it receives have been cast into JS numbers, which the
     // Racer model doesn't necessarily guarantee.
-    var app = runner.createHarness().app;
+    const app = runner.createHarness().app;
     app.views.register('Body',
       '<ul>' +
         '{{each _data.items as #item}}' +
@@ -539,15 +539,15 @@ describe('bindings', function() {
         '{{/each}}' +
       '</ul>'
     );
-    var page = app.createPage();
-    var $items = page.model.at('_data.items');
+    const page = app.createPage();
+    const $items = page.model.at('_data.items');
     $items.set([
       {label: 'Red', hexCode: '#ff0000'},
       {label: 'Green', hexCode: '#00ff00'},
       {label: 'Blue', hexCode: '#0000ff'},
     ]);
 
-    var fragment = page.getFragment('Body');
+    const fragment = page.getFragment('Body');
     expect(fragment).html('<ul><li>Red</li><li>Green</li><li>Blue</li></ul>');
     // Test mutation with a numeric path segment.
     page.model.set('_data.items.1.label', 'Verde');
diff --git a/test/browser/components.js b/test/dom/components.browser.mocha.js
similarity index 73%
rename from test/browser/components.js
rename to test/dom/components.browser.mocha.js
index e361e1764..a998bcb0e 100644
--- a/test/browser/components.js
+++ b/test/dom/components.browser.mocha.js
@@ -1,13 +1,14 @@
-var expect = require('chai').expect;
-var templates = require('../../src/templates').templates;
-var derby = require('./util').derby;
+const expect = require('chai').expect;
+const templates = require('../../src/templates').templates;
+const domTestRunner = require('../../src/test-utils/domTestRunner');
 
 describe('components', function() {
+  const runner = domTestRunner.install();
 
   describe('destroy', function() {
     it('emits a "destroy" event when the component is removed from the DOM', function(done) {
-      var app = derby.createApp();
-      var page = app.createPage();
+      const { app } = runner.createHarness();
+      const page = app.createPage();
       app.views.register('Body',
         '{{unless _page.hide}}' +
           '<view is="box" as="box"></view>' +
@@ -26,8 +27,8 @@ describe('components', function() {
     });
 
     it('emits an event declared in the template with `on-destroy`', function(done) {
-      var app = derby.createApp();
-      var page = app.createPage();
+      const { app } = runner.createHarness();
+      const page = app.createPage();
       app.views.register('Body',
         '{{unless _page.hide}}' +
           '<view is="box" on-destroy="destroyBox()"></view>' +
@@ -44,8 +45,8 @@ describe('components', function() {
     });
 
     it('sets `this.isDestroyed` property to true after a component has been fully destroyed', function() {
-      var app = derby.createApp();
-      var page = app.createPage();
+      const { app } = runner.createHarness();
+      const page = app.createPage();
       app.views.register('Body',
         '{{unless _page.hide}}' +
           '<view is="box" as="box"></view>' +
@@ -55,7 +56,7 @@ describe('components', function() {
       function Box() {}
       app.component('box', Box);
       page.getFragment('Body');
-      var box = page.box;
+      const box = page.box;
       expect(box.isDestroyed).equal(false);
       page.model.set('_page.hide', true);
       expect(box.isDestroyed).equal(true);
@@ -64,11 +65,11 @@ describe('components', function() {
 
   describe('bind', function() {
     it('calls a function with `this` being the component and passed in arguments', function() {
-      var app = derby.createApp();
-      var page = app.createPage();
+      const { app } = runner.createHarness();
+      const page = app.createPage();
       app.views.register('Body', '<view is="box"></view>');
       app.views.register('box', '<div>{{area}}</div>');
-      var getArea = function(scale) {
+      const getArea = function(scale) {
         expect(this).instanceof(Box);
         return this.width * this.height * scale;
       };
@@ -78,12 +79,12 @@ describe('components', function() {
         this.height = 4;
       };
       Box.prototype.create = function() {
-        var bound = this.bind(getArea);
-        var area = bound(10);
+        const bound = this.bind(getArea);
+        const area = bound(10);
         this.model.set('area', area);
       };
       app.component('box', Box);
-      var fragment = page.getFragment('Body');
+      const fragment = page.getFragment('Body');
       expect(fragment).html('<div>120</div>');
     });
   });
@@ -93,12 +94,12 @@ describe('components', function() {
       options = options || {};
 
       it('calls a function once with `this` being the component', function(done) {
-        var app = derby.createApp();
-        var page = app.createPage();
+        const { app } = runner.createHarness();
+        const page = app.createPage();
         app.views.register('Body', '<view is="box" as="box"></view>');
         app.views.register('box', '<div></div>');
-        var called = false;
-        var update = function() {
+        const called = false;
+        const update = function() {
           expect(this).instanceof(Box);
           called = true;
           // Will error if called more than once:
@@ -109,7 +110,8 @@ describe('components', function() {
           this.update = getFn.call(this, update);
         };
         app.component('box', Box);
-        var box = page.box;
+        page.getFragment('Body');
+        const box = page.box;
         box.update();
         box.update();
         box.update();
@@ -117,13 +119,13 @@ describe('components', function() {
       });
 
       it('resets and calls again', function(done) {
-        var app = derby.createApp();
-        var page = app.createPage();
+        const { app } = runner.createHarness();
+        const page = app.createPage();
         app.views.register('Body', '<view is="box" as="box"></view>');
         app.views.register('box', '<div></div>');
-        var called = false;
-        var box;
-        var update = function(cb) {
+        const called = false;
+        let box;
+        const update = function(cb) {
           expect(this).instanceof(Box);
           if (called) {
             done();
@@ -142,6 +144,7 @@ describe('components', function() {
           this.update = getFn.call(this, update);
         };
         app.component('box', Box);
+        page.getFragment('Body');
         box = page.box;
         box.update();
         box.update();
@@ -149,13 +152,13 @@ describe('components', function() {
       });
 
       it('calls with the most recent arguments', function(done) {
-        var app = derby.createApp();
-        var page = app.createPage();
+        const { app } = runner.createHarness();
+        const page = app.createPage();
         app.views.register('Body', '<view is="box" as="box"></view>');
         app.views.register('box', '<div></div>');
-        var called = false;
-        var box;
-        var update = function(letter, number, cb) {
+        const called = false;
+        let box;
+        const update = function(letter, number, cb) {
           expect(this).instanceof(Box);
           if (called) {
             expect(letter).equal('e');
@@ -180,6 +183,7 @@ describe('components', function() {
           this.update = getFn.call(this, update);
         };
         app.component('box', Box);
+        page.getFragment('Body');
         box = page.box;
         box.update('a', 1);
         box.update('b', 2);
@@ -222,12 +226,12 @@ describe('components', function() {
       });
     });
     it('debounceAsync does not apply arguments if callback has only one argument', function(done) {
-      var app = derby.createApp();
-      var page = app.createPage();
+      const { app } = runner.createHarness();
+      const page = app.createPage();
       app.views.register('Body', '<view is="box" as="box"></view>');
       app.views.register('box', '<div></div>');
-      var called = false;
-      var update = function(cb) {
+      const called = false;
+      const update = function(cb) {
         expect(cb).a('function');
         if (called) {
           done();
@@ -242,16 +246,19 @@ describe('components', function() {
         this.update = this.debounceAsync(update);
       };
       app.component('box', Box);
+      page.getFragment('Body');
       page.box.update('a', 1);
     });
+
     it('debounceAsync debounces until the async call completes', function(done) {
-      var app = derby.createApp();
-      app.views.register('Body', '<view is="box"></view>');
+      const { app } = runner.createHarness();
+      const page = app.createPage();
+      app.views.register('Body', '<view is="box" as="box"></view>');
       app.views.register('box', '<div></div>');
-      var calls = 0;
-      var intervalCount = 0;
-      var interval;
-      var update = function(cb) {
+      const calls = 0;
+      const intervalCount = 0;
+      let interval;
+      const update = function(cb) {
         if (calls === 0) {
           expect(intervalCount).equal(1);
         } else if (calls < 5) {
@@ -267,7 +274,7 @@ describe('components', function() {
       };
       function Box() {}
       Box.prototype.create = function() {
-        var debounced = this.debounceAsync(update);
+        const debounced = this.debounceAsync(update);
         interval = setInterval(function() {
           intervalCount++;
           debounced();
@@ -277,34 +284,40 @@ describe('components', function() {
         }, 7);
       };
       app.component('box', Box);
+      page.getFragment('Body');
     });
+
     it('throttle calls no more frequently than delay', function(done) {
-      var app = derby.createApp();
+      const { app } = runner.createHarness();
+      const page = app.createPage();
       app.views.register('Body', '<view is="box"></view>');
       app.views.register('box', '<div></div>');
-      var delay = 10;
-      var calls = 0;
-      var tickCount = 0;
-      var timeout;
-      var previous;
-      var update = function() {
+      const delay = 10;
+      // prevent flaky test -- occasionally called 1ms too fast
+      const minAllowedDelay = delay - 1;
+      const calls = 0;
+      const tickCount = 0;
+      let timeout;
+      let previous;
+      const update = function() {
         calls++;
-        var now = +new Date();
+        const now = +new Date();
         if (calls < 20) {
           if (previous) {
-            expect(now - previous).least(delay);
+            const elasped = now - previous;
+            expect(elasped).greaterThanOrEqual(minAllowedDelay);
           }
         } else {
           expect(tickCount).above(calls);
           clearTimeout(timeout);
-          return done();
+          done();
         }
         previous = now;
       };
       function Box() {}
       Box.prototype.create = function() {
-        var debounced = this.throttle(update, delay);
-        var tick = function() {
+        const debounced = this.throttle(update, delay);
+        const tick = function() {
           timeout = setTimeout(function() {
             tickCount++;
             debounced();
@@ -314,13 +327,14 @@ describe('components', function() {
         tick();
       };
       app.component('box', Box);
+      page.getFragment('Body');
     });
   });
 
   describe('dependencies', function() {
     it('gets dependencies rendered inside of components', function() {
-      var app = derby.createApp();
-      var page = app.createPage();
+      const { app } = runner.createHarness();
+      const page = app.createPage();
       app.views.register('Body',
         '<view is="box" title="{{_page.title}}, friend">' +
           '{{_page.message}}!' +
@@ -337,7 +351,7 @@ describe('components', function() {
       );
       app.component('box', function Box() {});
       app.component('box-title', function BoxTitle() {});
-      var view = app.views.find('Body');
+      const view = app.views.find('Body');
       expect(view.dependencies(page.context)).eql([
         ['_page', 'title'],
         ['_page', 'message']
@@ -345,8 +359,8 @@ describe('components', function() {
     });
 
     it('does not return dependencies for local paths within components', function() {
-      var app = derby.createApp();
-      var page = app.createPage();
+      const { app } = runner.createHarness();
+      const page = app.createPage();
       app.views.register('Body',
         '<view is="box" title="{{_page.title}}"></view>'
       );
@@ -368,7 +382,7 @@ describe('components', function() {
       );
       app.component('box', function Box() {});
       app.component('box-title', function BoxTitle() {});
-      var view = app.views.find('Body');
+      const view = app.views.find('Body');
       expect(view.dependencies(page.context)).eql([
         ['_page', 'title'],
         ['_page', 'disclaimer']
@@ -378,159 +392,154 @@ describe('components', function() {
 
   describe('attribute to model binding', function() {
     it('updates model when path attribute changes', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.page.model.set('_page.color', 'blue');
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      page.model.set('_page.color', 'blue');
+      app.views.register('Body',
         '<view is="swatch" value="{{_page.color}}"></view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '<div style="background-color: {{value}}"></div>'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
       expect(fragment).html('<div style="background-color: blue"></div>');
-      this.page.model.set('_page.color', 'gray');
+      page.model.set('_page.color', 'gray');
       expect(fragment).html('<div style="background-color: gray"></div>');
     });
 
     it('updates model when expression attribute changes', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.page.model.set('_page.color', 'blue');
-      this.app.proto.concat = function() {
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      page.model.set('_page.color', 'blue');
+      app.proto.concat = function() {
         return Array.prototype.join.call(arguments, '');
       };
-      this.app.views.register('Body',
+      app.views.register('Body',
         '<view is="swatch" value="{{concat(\'light\', _page.color)}}"></view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{@value}}<view is="color" value="{{value}}"></view>'
       );
-      this.app.views.register('color',
+      app.views.register('color',
         '<div style="background-color: {{value}}"></div>'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
       expect(fragment).html('lightblue<div style="background-color: lightblue"></div>');
-      this.page.model.set('_page.color', 'gray');
+      page.model.set('_page.color', 'gray');
       expect(fragment).html('lightgray<div style="background-color: lightgray"></div>');
     });
 
     it('updates model when template attribute changes', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.page.model.set('_page.color', 'blue');
-      this.app.proto.concat = function() {
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      page.model.set('_page.color', 'blue');
+      app.proto.concat = function() {
         return Array.prototype.join.call(arguments, '');
       };
-      this.app.views.register('Body',
+      app.views.register('Body',
         '<view is="swatch" value="light{{_page.color}}"></view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '<view is="color"></view>'
       );
-      this.app.views.register('color',
+      app.views.register('color',
         '{{value}}<div style="background-color: {{value}}"></div>'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
       expect(fragment).html('lightblue<div style="background-color: lightblue"></div>');
-      this.page.model.set('_page.color', 'gray');
+      page.model.set('_page.color', 'gray');
       expect(fragment).html('lightgray<div style="background-color: lightgray"></div>');
     });
 
     it('updates view expression', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.page.model.set('_page.color', 'blue');
-      this.page.model.set('_page.view', 'back');
-      this.app.proto.concat = function() {
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      page.model.set('_page.color', 'blue');
+      page.model.set('_page.view', 'back');
+      app.proto.concat = function() {
         return Array.prototype.join.call(arguments, '');
       };
-      this.app.views.register('Body',
+      app.views.register('Body',
         '<view is="swatch" value="{{view _page.view, {value: _page.color}}}"></view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '<div style="{{value}}">{{value}}</div>'
       );
-      this.app.views.register('back',
+      app.views.register('back',
         'background-color: light{{@value}}'
       );
-      this.app.views.register('fore',
+      app.views.register('fore',
         'color: light{{@value}}'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
       expect(fragment).html('<div style="background-color: lightblue">background-color: lightblue</div>');
-      this.page.model.set('_page.color', 'gray');
+      page.model.set('_page.color', 'gray');
       expect(fragment).html('<div style="background-color: lightgray">background-color: lightgray</div>');
-      this.page.model.set('_page.view', 'fore');
+      page.model.set('_page.view', 'fore');
       expect(fragment).html('<div style="color: lightgray">color: lightgray</div>');
     });
 
     it('updates when template attribute is updated to new value inside component model', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.page.model.set('_page.color', 'blue');
-      this.app.proto.concat = function() {
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      page.model.set('_page.color', 'blue');
+      app.proto.concat = function() {
         return Array.prototype.join.call(arguments, '');
       };
-      this.app.views.register('Body',
+      app.views.register('Body',
         '<view is="swatch" value="light{{_page.color}}"></view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '<div style="background-color: {{value}}">{{value}}</div>'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('<div style="background-color: lightblue">lightblue</div>');
-      var previous = swatch.model.set('value', 'gray');
+      const previous = swatch.model.set('value', 'gray');
       expect(fragment).html('<div style="background-color: gray">gray</div>');
-      expect(this.page.model.get('_page.color')).equal('blue');
+      expect(page.model.get('_page.color')).equal('blue');
       swatch.model.set('value', previous);
       expect(fragment).html('<div style="background-color: lightblue">lightblue</div>');
     });
 
     it('renders template attribute passed through component and partial with correct context', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.page.model.set('_page.color', 'blue');
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      page.model.set('_page.color', 'blue');
       // `Body` uses the `picture-exhibit` component, passing in the `swatch` template as a
       // `@content` attribute. `swatch` refers to a top-level model path, `_page.color`.
-      this.app.views.register('Body',
+      app.views.register('Body',
         '<view is="picture-exhibit" label="Blue Swatch"><view is="swatch"></view></view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '<div style="background-color: {{_page.color}}">{{_page.color}}</div>'
       );
       // `picture-exhibit` passes `@content` through as a content attribute to `picture-frame`,
       // a simple partial. `picture-frame` then renders the content attribute that got passed
       // all the way through. The value of `@content` is a `swatch` template, and the rendering
       // should use the top-level context, as the usage of `swatch` didn't use `within`.
-      this.app.views.register('picture-exhibit',
+      app.views.register('picture-exhibit',
         '<view is="picture-frame">{{@content}}</view>' +
         '<label>{{@label}}</label>'
       );
-      this.app.views.register('picture-frame',
+      app.views.register('picture-frame',
         '<div class="picture-frame">{{@content}}</div>'
       );
 
       function PictureExhibit() {}
-      this.app.component('picture-exhibit', PictureExhibit);
+      app.component('picture-exhibit', PictureExhibit);
 
-      var fragment = this.page.getFragment('Body');
+      const fragment = page.getFragment('Body');
       expect(fragment).html(
         '<div class="picture-frame">' +
           '<div style="background-color: blue">blue</div>' +
@@ -540,16 +549,16 @@ describe('components', function() {
     });
 
     it('updates within template content', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.page.model.set('_page.width', 10);
-      this.page.model.set('_page.color', 'blue');
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      page.model.set('_page.width', 10);
+      page.model.set('_page.color', 'blue');
+      app.views.register('Body',
         '<view is="swatch" width="{{_page.width}}" within>' +
           'light{{#color}}' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with #root._page.color as #color}}' +
           '<div style="width: {{width}}px; background-color: {{content}}">' +
             '{{content}}' +
@@ -557,32 +566,30 @@ describe('components', function() {
         '{{/with}}'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
       expect(fragment).html('<div style="width: 10px; background-color: lightblue">lightblue</div>');
-      this.page.model.set('_page.color', 'green');
+      page.model.set('_page.color', 'green');
       expect(fragment).html('<div style="width: 10px; background-color: lightgreen">lightgreen</div>');
     });
 
     it('updates within template attribute', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<attribute is="message" within>{{if #show}}Show me!{{else}}Hide me.{{/if}}</attribute>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with show as #show}}' +
           '<div>{{@message}}</div>' +
         '{{/with}}'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('<div>Hide me.</div>');
       expect(swatch.model.get('message')).instanceof(templates.Template);
       swatch.model.set('show', true);
@@ -591,23 +598,22 @@ describe('components', function() {
     });
 
     it('updates within template attribute in model', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<attribute is="message" within>{{if #show}}Show me!{{else}}Hide me.{{/if}}</attribute>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with show as #show}}' +
           '<div>{{message}}</div>' +
         '{{/with}}'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('<div>Hide me.</div>');
       expect(swatch.model.get('message')).instanceof(templates.Template);
       swatch.model.set('show', true);
@@ -616,23 +622,22 @@ describe('components', function() {
     });
 
     it('updates within expression attribute by making it a template', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<attribute is="message" within>{{#show ? "Show me!" : "Hide me."}}</attribute>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with show as #show}}' +
           '<div>{{message}}</div>' +
         '{{/with}}'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('<div>Hide me.</div>');
       expect(swatch.model.get('message')).instanceof(templates.Template);
       expect(swatch.getAttribute('message')).equal('Hide me.');
@@ -644,19 +649,18 @@ describe('components', function() {
     });
 
     it('updates within attribute bound to component model path', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<attribute is="message" within>{{if show}}Show me!{{else}}Hide me.{{/if}}</attribute>' +
         '</view>'
       );
-      this.app.views.register('swatch', '<div>{{message}}</div>');
+      app.views.register('swatch', '<div>{{message}}</div>');
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('<div>Hide me.</div>');
       expect(swatch.model.get('message')).instanceof(templates.Template);
       expect(swatch.getAttribute('message')).equal('Hide me.');
@@ -666,15 +670,15 @@ describe('components', function() {
     });
 
     it('updates array within template attribute', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<item within>{{if #show}}Show me!{{else}}Hide me.{{/if}}</item>' +
           '<item>{{if #show}}Show me!{{else}}Hide me.{{/if}}</item>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with show as #show}}' +
           '{{each @items as #item}}' +
             '{{#item.content}}' +
@@ -683,10 +687,9 @@ describe('components', function() {
         {arrays: 'item/items'}
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('Hide me.Hide me.');
       expect(swatch.getAttribute('items')).eql([
         {content: 'Hide me.'},
@@ -701,15 +704,15 @@ describe('components', function() {
     });
 
     it('updates array within template attribute with content alias', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<item within>{{if #show}}Show me!{{else}}Hide me.{{/if}}</item>' +
           '<item>{{if #show}}Show me!{{else}}Hide me.{{/if}}</item>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with show as #show}}' +
           '{{each @items as #item}}' +
             '{{with #item.content as #itemContent}}' +
@@ -720,10 +723,9 @@ describe('components', function() {
         {arrays: 'item/items'}
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('Hide me.Hide me.');
       expect(swatch.getAttribute('items')).eql([
         {content: 'Hide me.'},
@@ -738,15 +740,15 @@ describe('components', function() {
     });
 
     it('updates array within template attribute in model', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<item within>{{if #show}}Show me!{{else}}Hide me.{{/if}}</item>' +
           '<item>{{if #show}}Show me!{{else}}Hide me.{{/if}}</item>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with show as #show}}' +
           '{{each items as #item}}' +
             '{{#item.content}}' +
@@ -755,10 +757,9 @@ describe('components', function() {
         {arrays: 'item/items'}
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('Hide me.Hide me.');
       expect(swatch.model.get('items').length).equal(2);
       expect(swatch.model.get('items')[0].content).instanceof(templates.Template);
@@ -768,30 +769,29 @@ describe('components', function() {
     });
 
     it('updates array within template attribute in model from partial', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<item within>{{if #show}}Show me!{{else}}Hide me.{{/if}}</item>' +
           '<item>{{if #show}}Show me!{{else}}Hide me.{{/if}}</item>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with show as #show}}' +
           '<view is="swatch-items"></view>' +
         '{{/with}}',
         {arrays: 'item/items'}
       );
-      this.app.views.register('swatch-items',
+      app.views.register('swatch-items',
         '{{each items as #item}}' +
           '{{#item.content}}' +
         '{{/each}}'
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('Hide me.Hide me.');
       expect(swatch.model.get('items').length).equal(2);
       expect(swatch.model.get('items')[0].content).instanceof(templates.Template);
@@ -801,25 +801,24 @@ describe('components', function() {
     });
 
     it('updates array within attribute bound to component model path', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<item within>{{if show}}Show me!{{else}}Hide me.{{/if}}</item>' +
           '<item>{{if show}}Show me!{{else}}Hide me.{{/if}}</item>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{each @items as #item}}' +
           '{{#item.content}}' +
         '{{/each}}',
         {arrays: 'item/items'}
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('Hide me.Hide me.');
       expect(swatch.getAttribute('items')).eql([
         {content: 'Hide me.'},
@@ -834,15 +833,15 @@ describe('components', function() {
     });
 
     it('updates array within expression attribute by making it a template', function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.app.views.register('Body',
+      const app = runner.createHarness().app;
+      const page = app.createPage();
+      app.views.register('Body',
         '<view is="swatch">' +
           '<item within>{{#show ? "Show me!" : "Hide me."}}</item>' +
           '<item>{{#show ? "Show me!" : "Hide me."}}</item>' +
         '</view>'
       );
-      this.app.views.register('swatch',
+      app.views.register('swatch',
         '{{with show as #show}}' +
           '{{each items as #item}}' +
             '{{#item.content}}' +
@@ -851,10 +850,9 @@ describe('components', function() {
         {arrays: 'item/items'}
       );
       function Swatch() {}
-      this.Swatch = Swatch;
-      this.app.component('swatch', Swatch);
-      var fragment = this.page.getFragment('Body');
-      var swatch = this.page._components._1;
+      app.component('swatch', Swatch);
+      const fragment = page.getFragment('Body');
+      const swatch = page._components._1;
       expect(fragment).html('Hide me.Hide me.');
       expect(swatch.model.get('items').length).equal(2);
       expect(swatch.model.get('items')[0].content).instanceof(templates.Template);
@@ -866,35 +864,38 @@ describe('components', function() {
   });
 
   describe('rendering', function() {
+    let app;
+    let page;
+
     beforeEach(function() {
-      this.app = derby.createApp();
-      this.page = this.app.createPage();
-      this.page.model.set('_page.title', 'Good day');
-      this.app.views.register('Body',
+      app = runner.createHarness().app;
+      page = app.createPage();
+      page.model.set('_page.title', 'Good day');
+      app.views.register('Body',
         '<view is="box" role="container" title="{{_page.title}}">' +
           '<view is="box" role="inner1" title="Greeting">Hello.</view>' +
           '<view is="box" role="inner2"></view>' +
         '</view>'
       );
-      this.app.views.register('box',
+      app.views.register('box',
         '<div class="box">' +
           '<view is="box-title" tip="{{@title}}">{{@title}}</view>' +
           '{{@content}}' +
         '</div>'
       );
-      this.app.views.register('box-title',
+      app.views.register('box-title',
         '<b title="{{@tip}}">{{@content}}</b>'
       );
       function Box() {}
       this.Box = Box;
-      this.app.component('box', this.Box);
+      app.component('box', this.Box);
       function BoxTitle() {}
       this.BoxTitle = BoxTitle;
-      this.app.component('box-title', this.BoxTitle);
+      app.component('box-title', this.BoxTitle);
     });
 
     it('renders a component', function() {
-      var html = this.page.get('Body');
+      const html = page.get('Body');
       expect(html).equal(
         '<div class="box">' +
           '<b title="Good day">Good day</b>' +
@@ -905,7 +906,7 @@ describe('components', function() {
     });
 
     it('sets attributes as values on component model', function() {
-      var tests = {
+      const tests = {
         container: function(box, boxTitle) {
           expect(box.model.get('title')).equal('Good day');
           expect(boxTitle.model.get('tip')).equal('Good day');
@@ -928,7 +929,7 @@ describe('components', function() {
     });
 
     it('Component::getAttribute returns passed in values', function() {
-      var tests = {
+      const tests = {
         container: function(box, boxTitle) {
           expect(box.getAttribute('title')).equal('Good day');
           expect(boxTitle.getAttribute('tip')).equal('Good day');
@@ -952,13 +953,13 @@ describe('components', function() {
 
     function testInit(tests) {
       this.BoxTitle.prototype.init = function() {
-        var box = this.parent;
-        var boxTitle = this;
-        var role = box.model.get('role');
+        const box = this.parent;
+        const boxTitle = this;
+        const role = box.model.get('role');
         tests[role](box, boxTitle);
         delete tests[role];
       }
-      this.page.getFragment('Body');
+      page.getFragment('Body');
       expect(Object.keys(tests).length).equal(0);
     }
   });
diff --git a/test/dom/components.mocha.js b/test/dom/components.mocha.js
index 1d9adccaf..0d674322c 100644
--- a/test/dom/components.mocha.js
+++ b/test/dom/components.mocha.js
@@ -1,16 +1,16 @@
-var expect = require('chai').expect;
-var pathLib = require('node:path');
+const expect = require('chai').expect;
+const pathLib = require('node:path');
 const { Component } = require('../../src/components');
-var domTestRunner = require('../../src/test-utils/domTestRunner');
+const domTestRunner = require('../../src/test-utils/domTestRunner');
 
 describe('components', function() {
-  var runner = domTestRunner.install();
+  const runner = domTestRunner.install();
 
   describe('app.component registration', function() {
     describe('passing just component class', function() {
       describe('with static view prop', function() {
         it('external view file', function() {
-          var harness = runner.createHarness();
+          const harness = runner.createHarness();
 
           function SimpleBox() {}
           SimpleBox.view = {
@@ -25,7 +25,7 @@ describe('components', function() {
         });
 
         it('inlined view.source', function() {
-          var harness = runner.createHarness();
+          const harness = runner.createHarness();
 
           function SimpleBox() {}
           SimpleBox.view = {
@@ -39,7 +39,7 @@ describe('components', function() {
         });
 
         it('inferred view file from view name', function() {
-          var harness = runner.createHarness();
+          const harness = runner.createHarness();
 
           // Pre-load view with same name as the component's static `view.is`
           harness.app.loadViews(pathLib.resolve(__dirname, '../fixtures/simple-box'), 'simple-box');
diff --git a/test/browser/dom-events.mocha.js b/test/dom/dom-events.mocha.js
similarity index 70%
rename from test/browser/dom-events.mocha.js
rename to test/dom/dom-events.mocha.js
index 370751577..72e5b64b9 100644
--- a/test/browser/dom-events.mocha.js
+++ b/test/dom/dom-events.mocha.js
@@ -1,30 +1,32 @@
-var expect = require('chai').expect;
-var derby = require('./util').derby;
+const expect = require('chai').expect;
+const domTestRunner = require('../../src/test-utils/domTestRunner');
 
 describe('DOM events', function() {
+  const runner = domTestRunner.install();
+
   it('HTML element markup custom `create` event', function() {
-    var app = derby.createApp();
+    const { app } = runner.createHarness();
     app.views.register('Body',
       '<div on-create="createDiv($element)">' +
         '<span on-create="createSpan($element)"></span>' +
       '</div>'
     );
-    var page = app.createPage();
-    var div, span;
+    const page = app.createPage();
+    let div, span;
     page.createDiv = function(element) {
       div = element;
     };
     page.createSpan = function(element) {
       span = element;
     };
-    var fragment = page.getFragment('Body');
+    const fragment = page.getFragment('Body');
     expect(fragment).html('<div><span></span></div>');
     expect(div).html('<div><span></span></div>');
     expect(span).html('<span></span>');
   });
 
-  it('HTML element markup custom `destroy` event', function() {
-    var app = derby.createApp();
+  it.skip('HTML element markup custom `destroy` event', function() {
+    const { app } = runner.createHarness();
     app.views.register('Body',
       '<div>' +
         '{{unless _page.hide}}' +
@@ -32,12 +34,12 @@ describe('DOM events', function() {
         '{{/unless}}' +
       '</div>'
     );
-    var page = app.createPage();
-    var span;
+    const page = app.createPage();
+    let span;
     page.destroySpan = function(element) {
       span = element;
     };
-    var fragment = page.getFragment('Body');
+    const fragment = page.getFragment('Body');
     expect(fragment).html('<div><span></span></div>');
     expect(span).equal(undefined);
 
@@ -47,7 +49,7 @@ describe('DOM events', function() {
   });
 
   it('dom.on custom `destroy` event', function() {
-    var app = derby.createApp();
+    const { app } = runner.createHarness();
     app.views.register('Body',
       '<div>' +
         '{{unless _page.hide}}' +
@@ -55,9 +57,9 @@ describe('DOM events', function() {
         '{{/unless}}' +
       '</div>'
     );
-    var page = app.createPage();
-    var fragment = page.getFragment('Body');
-    var destroyed = false;
+    const page = app.createPage();
+    const fragment = page.getFragment('Body');
+    const destroyed = false;
     page.dom.on('destroy', page.span, function() {
       destroyed = true;
     });
diff --git a/test/dom/domTestRunner.mocha.js b/test/dom/domTestRunner.mocha.js
index 22e581a93..f8812d740 100644
--- a/test/dom/domTestRunner.mocha.js
+++ b/test/dom/domTestRunner.mocha.js
@@ -3,6 +3,7 @@ var domTestRunner = require('../../src/test-utils/domTestRunner');
 describe('domTestRunner', function() {
   describe('with JSDOM option pretendToBeVisual', function() {
     domTestRunner.install({jsdomOptions: {pretendToBeVisual: true}});
+
     it('has window.requestAnimationFrame', function(done) {
       window.requestAnimationFrame(function() {
         done();
diff --git a/test/browser/forms.js b/test/dom/forms.browser.mocha.js
similarity index 80%
rename from test/browser/forms.js
rename to test/dom/forms.browser.mocha.js
index 8cbae4689..10f980dea 100644
--- a/test/browser/forms.js
+++ b/test/dom/forms.browser.mocha.js
@@ -1,20 +1,27 @@
 var expect = require('chai').expect;
-var derby = require('./util').derby;
+var domTestRunner = require('../../src/test-utils/domTestRunner');
 
 describe('forms', function() {
+  const runner = domTestRunner.install();
+
+  function createEvent(type) {
+    return new runner.window.Event(type, {bubbles: true});
+  }
 
   describe('textarea', function() {
+    let fixture;
 
     beforeEach(function() {
-      this.fixture = document.createElement('ins');
-      document.body.appendChild(this.fixture);
+      fixture = document.createElement('ins');
+      document.body.appendChild(fixture);
     });
+
     afterEach(function() {
-      document.body.removeChild(this.fixture);
+      document.body.removeChild(fixture);
     });
 
     it('renders text content in textarea', function() {
-      var app = derby.createApp();
+      const { app } = runner.createHarness();
       app.views.register('Body', '<textarea>{{_page.text}}</textarea>');
       var page = app.createPage();
       var text = page.model.at('_page.text');
@@ -28,7 +35,7 @@ describe('forms', function() {
     });
 
     it('updates textarea value on model set', function() {
-      var app = derby.createApp();
+      const { app } = runner.createHarness();
       app.views.register('Body', '<textarea>{{_page.text}}</textarea>');
       var page = app.createPage();
       var text = page.model.at('_page.text');
@@ -44,7 +51,7 @@ describe('forms', function() {
     });
 
     it('updates model after changing text and emitting change', function() {
-      var app = derby.createApp();
+      const { app } = runner.createHarness();
       app.views.register('Body', '<textarea>{{_page.text}}</textarea>');
       var page = app.createPage();
       var text = page.model.at('_page.text');
@@ -53,7 +60,7 @@ describe('forms', function() {
       var textarea = fragment.firstChild;
       var textNode = textarea.firstChild;
       // Insert the fragment in the document so that Derby captures events
-      this.fixture.appendChild(fragment);
+      fixture.appendChild(fragment);
       textNode.data = 'Yo';
       textarea.dispatchEvent(createEvent('change'));
       expect(textarea.value).equal('Yo');
@@ -61,7 +68,7 @@ describe('forms', function() {
     });
 
     it('updates model after changing value and emitting change', function() {
-      var app = derby.createApp();
+      const { app } = runner.createHarness();
       app.views.register('Body', '<textarea>{{_page.text}}</textarea>');
       var page = app.createPage();
       var text = page.model.at('_page.text');
@@ -69,14 +76,14 @@ describe('forms', function() {
       var fragment = page.getFragment('Body');
       var textarea = fragment.firstChild;
       // Insert the fragment in the document so that Derby captures events
-      this.fixture.appendChild(fragment);
+      fixture.appendChild(fragment);
       textarea.value = 'Yo';
       textarea.dispatchEvent(createEvent('change'));
       expect(text.get()).equal('Yo');
     });
 
     it('updates model after changing value and emitting input', function() {
-      var app = derby.createApp();
+      const { app } = runner.createHarness();
       app.views.register('Body', '<textarea>{{_page.text}}</textarea>');
       var page = app.createPage();
       var text = page.model.at('_page.text');
@@ -84,22 +91,10 @@ describe('forms', function() {
       var fragment = page.getFragment('Body');
       var textarea = fragment.firstChild;
       // Insert the fragment in the document so that Derby captures events
-      this.fixture.appendChild(fragment);
+      fixture.appendChild(fragment);
       textarea.value = 'Yo';
       textarea.dispatchEvent(createEvent('input'));
       expect(text.get()).equal('Yo');
     });
-
   });
 });
-
-function createEvent(type) {
-  // Current browsers
-  if (typeof Event === 'function') {
-    return new Event(type, {bubbles: true});
-  }
-  // IE and old browsers
-  var event = document.createEvent('Event');
-  event.initEvent(type, true, false);
-  return event;
-}
diff --git a/test/dom/templates/templates.dom.mocha.js b/test/dom/templates/templates.dom.mocha.js
index c1e467c63..20a7aa95b 100644
--- a/test/dom/templates/templates.dom.mocha.js
+++ b/test/dom/templates/templates.dom.mocha.js
@@ -1,7 +1,7 @@
-var chai = require('chai');
-var expect = chai.expect;
-var saddle = require('../../../src/templates/templates');
-var domTestRunner = require('../../../src/test-utils/domTestRunner');
+const chai = require('chai');
+const expect = chai.expect;
+const saddle = require('../../../src/templates/templates');
+const domTestRunner = require('../../../src/test-utils/domTestRunner');
 
 describe('templates rendering', function() {
   domTestRunner.install({jsdomOptions: {pretendToBeVisual: true}});
@@ -9,17 +9,17 @@ describe('templates rendering', function() {
   describe('Static rendering', function() {  
     describe('HTML', function() {
       testStaticRendering(function test(options) {
-        var context = getContext();
-        var html = options.template.get(context);
+        const context = getContext();
+        const html = options.template.get(context);
         expect(html).equal(options.html);
       });
     });
   
     describe('Fragment', function() {
       testStaticRendering(function test(options) {
-        var context = getContext();
+        const context = getContext();
         // getFragment calls appendTo, so these Fragment tests cover appendTo.
-        var fragment = options.template.getFragment(context);
+        const fragment = options.template.getFragment(context);
         options.fragment(fragment);
       });
     });
@@ -30,20 +30,20 @@ describe('templates rendering', function() {
 
     describe('HTML', function() {
       testDynamicRendering(function test(options) {
-        var context = getContext({
+        const context = getContext({
           show: true
         });
-        var html = options.template.get(context);
+        const html = options.template.get(context);
         expect(html).equal(options.html);
       });
     });
   
     describe('Fragment', function() {
       testDynamicRendering(function test(options) {
-        var context = getContext({
+        const context = getContext({
           show: true
         });
-        var fragment = options.template.getFragment(context);
+        const fragment = options.template.getFragment(context);
         options.fragment(fragment);
       });
     });
@@ -174,10 +174,10 @@ function testStaticRendering(test) {
       html: '<div><div><span></span><span></span></div></div>',
       fragment: function(fragment) {
         expect(fragment.childNodes.length).equal(1);
-        var node = fragment.childNodes[0];
+        let node = fragment.childNodes[0];
         expect(node.tagName.toLowerCase()).equal('div');
         expect(node.childNodes.length).equal(1);
-        var node = node.childNodes[0];
+        node = node.childNodes[0];
         expect(node.tagName.toLowerCase()).equal('div');
         expect(node.childNodes.length).equal(2);
         expect(node.childNodes[0].tagName.toLowerCase()).equal('span');
@@ -209,7 +209,7 @@ function testStaticRendering(test) {
       html: '<div>Hello, world.</div>',
       fragment: function(fragment) {
         expect(fragment.childNodes.length).equal(1);
-        var node = fragment.childNodes[0];
+        const node = fragment.childNodes[0];
         expect(node.tagName.toLowerCase()).equal('div');
         expect(node.childNodes.length).equal(2);
         expect(node.childNodes[0].nodeType).equal(3);
@@ -245,7 +245,7 @@ function testStaticRendering(test) {
         expect(fragment.childNodes.length).equal(2);
         expect(fragment.childNodes[0].nodeType).equal(8);
         expect(fragment.childNodes[0].data).equal('Hi');
-        var node = fragment.childNodes[1];
+        const node = fragment.childNodes[1];
         expect(node.tagName.toLowerCase()).equal('div');
         expect(node.childNodes.length).equal(1);
         expect(node.childNodes[0].nodeType).equal(3);
@@ -260,10 +260,10 @@ function testStaticRendering(test) {
       html: '<div>Hi</div><input>',
       fragment: function(fragment) {
         expect(fragment.childNodes.length).equal(2);
-        var node = fragment.childNodes[0];
+        let node = fragment.childNodes[0];
         expect(node.tagName.toLowerCase()).equal('div');
         expect(node.innerHTML).equal('Hi');
-        var node = fragment.childNodes[1];
+        node = fragment.childNodes[1];
         expect(node.tagName.toLowerCase()).equal('input');
       }
     });
@@ -278,7 +278,7 @@ function testStaticRendering(test) {
       ]),
       html: '<table><tbody><tr><td>Hi</td></tr></tbody></table>',
       fragment: function(fragment) {
-        var node = fragment.firstChild;
+        const node = fragment.firstChild;
         expect(node.tagName.toLowerCase()).equal('table');
         node = node.firstChild;
         expect(node.tagName.toLowerCase()).equal('tbody');
@@ -373,7 +373,7 @@ function testDynamicRendering(test) {
 describe('templates DOM manipulation', function() {
   domTestRunner.install({jsdomOptions: {pretendToBeVisual: true}});
 
-  var fixture;
+  let fixture;
   beforeEach(function() {
     fixture = document.getElementById('fixture');
     if (!fixture) {
@@ -388,14 +388,14 @@ describe('templates DOM manipulation', function() {
 
   describe('attachTo', function() {
     function renderAndAttach(template) {
-      var context = getContext();
+      const context = getContext();
       removeChildren(fixture);
       fixture.innerHTML = template.get(context);
       template.attachTo(fixture, fixture.firstChild, context);
     }
   
     it('splits static text nodes', function() {
-      var template = new saddle.Template([
+      const template = new saddle.Template([
         new saddle.Text('Hi'),
         new saddle.Text(' there.')
       ]);
@@ -404,7 +404,7 @@ describe('templates DOM manipulation', function() {
     });
   
     it('splits empty static text nodes', function() {
-      var template = new saddle.Template([
+      const template = new saddle.Template([
         new saddle.Text(''),
         new saddle.Text('')
       ]);
@@ -413,7 +413,7 @@ describe('templates DOM manipulation', function() {
     });
   
     it('splits mixed empty static text nodes', function() {
-      var template = new saddle.Template([
+      const template = new saddle.Template([
         new saddle.Text(''),
         new saddle.Text('Hi'),
         new saddle.Text(''),
@@ -426,7 +426,7 @@ describe('templates DOM manipulation', function() {
     });
   
     it('adds empty text nodes around a comment', function() {
-      var template = new saddle.Template([
+      const template = new saddle.Template([
         new saddle.Text('Hi'),
         new saddle.Text(''),
         new saddle.Comment('cool'),
@@ -438,7 +438,7 @@ describe('templates DOM manipulation', function() {
     });
   
     it('attaches to nested elements', function() {
-      var template = new saddle.Template([
+      const template = new saddle.Template([
         new saddle.Element('ul', null, [
           new saddle.Element('li', null, [
             new saddle.Text('One')
@@ -452,7 +452,7 @@ describe('templates DOM manipulation', function() {
     });
   
     it('attaches to element attributes', function() {
-      var template = new saddle.Template([
+      const template = new saddle.Template([
         new saddle.Element('input', {
           type: new saddle.Attribute('text'),
           autofocus: new saddle.Attribute(true),
@@ -463,7 +463,7 @@ describe('templates DOM manipulation', function() {
     });
   
     it('attaches to <tr> from HTML within tbody context', function() {
-      var template = new saddle.Element('table', null, [
+      const template = new saddle.Element('table', null, [
         new saddle.Element('tbody', null, [
           new saddle.Comment('OK'),
           new saddle.Html('<tr><td>Hi</td></tr>'),
@@ -479,7 +479,7 @@ describe('templates DOM manipulation', function() {
   
     it('traverses with comments in a table and select', function() {
       // IE fails to create comments in certain locations when parsing HTML
-      var template = new saddle.Template([
+      const template = new saddle.Template([
         new saddle.Element('table', null, [
           new saddle.Comment('table comment'),
           new saddle.Element('tbody', null, [
@@ -504,7 +504,7 @@ describe('templates DOM manipulation', function() {
     it('throws when fragment does not match HTML', function() {
       // This template is invalid HTML, and when it is parsed it will produce
       // a different tree structure than when the nodes are created one-by-one
-      var template = new saddle.Template([
+      const template = new saddle.Template([
         new saddle.Element('table', null, [
           new saddle.Element('div', null, [
             new saddle.Element('td', null, [
@@ -523,9 +523,9 @@ describe('templates DOM manipulation', function() {
   describe('binding updates', function() {
     describe('getFragment', function() {
       testBindingUpdates(function render(template, data) {
-        var bindings = [];
-        var context = getContext(data, bindings);
-        var fragment = template.getFragment(context);
+        const bindings = [];
+        const context = getContext(data, bindings);
+        const fragment = template.getFragment(context);
         removeChildren(fixture);
         fixture.appendChild(fragment);
         return bindings;
@@ -534,8 +534,8 @@ describe('templates DOM manipulation', function() {
   
     describe('get + attachTo', function() {
       testBindingUpdates(function render(template, data) {
-        var bindings = [];
-        var context = getContext(data, bindings);
+        const bindings = [];
+        const context = getContext(data, bindings);
         removeChildren(fixture);
         fixture.innerHTML = template.get(context);
         template.attachTo(fixture, fixture.firstChild, context);
@@ -545,10 +545,10 @@ describe('templates DOM manipulation', function() {
 
     function testBindingUpdates(render) {
       it('updates a single TextNode', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.DynamicText(new FakeExpression('text'))
         ]);
-        var binding = render(template).pop();
+        const binding = render(template).pop();
         expect(getText(fixture)).equal('');
         binding.context = getContext({text: 'Yo'});
         binding.update();
@@ -556,14 +556,14 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates sibling TextNodes', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.DynamicText(new FakeExpression('first')),
           new saddle.DynamicText(new FakeExpression('second'))
         ]);
-        var bindings = render(template, {second: 2});
+        const bindings = render(template, {second: 2});
         expect(bindings.length).equal(2);
         expect(getText(fixture)).equal('2');
-        var context = getContext({first: 'one', second: 'two'});
+        const context = getContext({first: 'one', second: 'two'});
         bindings[0].context = context;
         bindings[0].update();
         expect(getText(fixture)).equal('one2');
@@ -573,11 +573,11 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates a TextNode that returns text, then a Template', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.DynamicText(new FakeExpression('dynamicTemplate'))
         ]);
-        var data = {dynamicTemplate: 'Hola'};
-        var binding = render(template, data).pop();
+        const data = {dynamicTemplate: 'Hola'};
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('Hola');
         binding.context = getContext({
           dynamicTemplate: new saddle.DynamicText(new FakeExpression('text')),
@@ -588,14 +588,14 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates a TextNode that returns a Template, then text', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.DynamicText(new FakeExpression('dynamicTemplate'))
         ]);
-        var data = {
+        const data = {
           dynamicTemplate: new saddle.DynamicText(new FakeExpression('text')),
           text: 'Yo'
         };
-        var binding = render(template, data).pop();
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('Yo');
         binding.context = getContext({dynamicTemplate: 'Hola'});
         binding.update();
@@ -603,14 +603,14 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates a TextNode that returns a Template, then another Template', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.DynamicText(new FakeExpression('dynamicTemplate'))
         ]);
-        var data = {
+        const data = {
           dynamicTemplate: new saddle.DynamicText(new FakeExpression('text')),
           text: 'Yo'
         };
-        var binding = render(template, data).pop();
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('Yo');
         binding.context = getContext({
           dynamicTemplate: new saddle.Template([
@@ -625,14 +625,14 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates within a template returned by a TextNode', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.DynamicText(new FakeExpression('dynamicTemplate'))
         ]);
-        var data = {
+        const data = {
           dynamicTemplate: new saddle.DynamicText(new FakeExpression('text')),
           text: 'Yo'
         };
-        var textBinding = render(template, data).shift();
+        const textBinding = render(template, data).shift();
         expect(getText(fixture)).equal('Yo');
         data.text = 'Hola';
         textBinding.context = getContext(data);
@@ -641,10 +641,10 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates a CommentNode', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.DynamicComment(new FakeExpression('comment'))
         ]);
-        var binding = render(template, {comment: 'Hi'}).pop();
+        const binding = render(template, {comment: 'Hi'}).pop();
         expect(fixture.innerHTML).equal('<!--Hi-->');
         binding.context = getContext({comment: 'Bye'});
         binding.update();
@@ -652,40 +652,41 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates raw HTML', function() {
-        var template = new saddle.Template([
+        let children;
+        const template = new saddle.Template([
           new saddle.DynamicHtml(new FakeExpression('html')),
           new saddle.Element('div')
         ]);
-        var binding = render(template, {html: '<b>Hi</b>'}).pop();
-        var children = getChildren(fixture);
+        const binding = render(template, {html: '<b>Hi</b>'}).pop();
+        children = getChildren(fixture);
         expect(children.length).equal(2);
         expect(children[0].tagName.toLowerCase()).equal('b');
         expect(children[0].innerHTML).equal('Hi');
         expect(children[1].tagName.toLowerCase()).equal('div');
         binding.context = getContext({html: '<i>What?</i>'});
         binding.update();
-        var children = getChildren(fixture);
+        children = getChildren(fixture);
         expect(children.length).equal(2);
         expect(children[0].tagName.toLowerCase()).equal('i');
         expect(children[0].innerHTML).equal('What?');
         expect(children[1].tagName.toLowerCase()).equal('div');
         binding.context = getContext({html: 'Hola'});
         binding.update();
-        var children = getChildren(fixture);
+        children = getChildren(fixture);
         expect(children.length).equal(1);
         expect(getText(fixture)).equal('Hola');
         expect(children[0].tagName.toLowerCase()).equal('div');
       });
 
       it('updates an Element attribute', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.Element('div', {
             'class': new saddle.Attribute('message'),
             'data-greeting': new saddle.DynamicAttribute(new FakeExpression('greeting'))
           })
         ]);
-        var binding = render(template).pop();
-        var node = fixture.firstChild;
+        const binding = render(template).pop();
+        const node = fixture.firstChild;
         expect(node.className).equal('message');
         expect(node.getAttribute('data-greeting')).eql(null);
         // Set initial value
@@ -705,14 +706,14 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates text input "value" property', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.Element('input', {
             'value': new saddle.DynamicAttribute(new FakeExpression('text')),
           })
         ]);
 
-        var binding = render(template).pop();
-        var input = fixture.firstChild;
+        const binding = render(template).pop();
+        const input = fixture.firstChild;
 
         // Set initial value to string.
         binding.context = getContext({text: 'Hi'});
@@ -731,15 +732,15 @@ describe('templates DOM manipulation', function() {
       });
 
       it('does not clobber input type="number" value when typing "1.0"', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.Element('input', {
             'type': new saddle.Attribute('number'),
             'value': new saddle.DynamicAttribute(new FakeExpression('amount')),
           })
         ]);
 
-        var binding = render(template).pop();
-        var input = fixture.firstChild;
+        const binding = render(template).pop();
+        const input = fixture.firstChild;
 
         // Make sure that a user-typed input value of "1.0" does not get clobbered by
         // a context value of `1`.
@@ -750,14 +751,14 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates "title" attribute', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.Element('div', {
             'title': new saddle.DynamicAttribute(new FakeExpression('divTooltip')),
           })
         ]);
 
-        var binding = render(template).pop();
-        var node = fixture.firstChild;
+        const binding = render(template).pop();
+        const node = fixture.firstChild;
 
         // Set initial value to string.
         binding.context = getContext({divTooltip: 'My tooltip'});
@@ -776,7 +777,7 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates a Block', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.Block(new FakeExpression('author'), [
             new saddle.Element('h3', null, [
               new saddle.DynamicText(new FakeExpression('name'))
@@ -784,8 +785,9 @@ describe('templates DOM manipulation', function() {
             new saddle.DynamicText(new FakeExpression('name'))
           ])
         ]);
-        var binding = render(template).pop();
-        var children = getChildren(fixture);
+        let children;
+        const binding = render(template).pop();
+        children = getChildren(fixture);
         expect(children.length).equal(1);
         expect(children[0].tagName.toLowerCase()).equal('h3');
         expect(getText(children[0])).equal('');
@@ -793,7 +795,7 @@ describe('templates DOM manipulation', function() {
         // Update entire block context
         binding.context = getContext({author: {name: 'John'}});
         binding.update();
-        var children = getChildren(fixture);
+        children = getChildren(fixture);
         expect(children.length).equal(1);
         expect(children[0].tagName.toLowerCase()).equal('h3');
         expect(getText(children[0])).equal('John');
@@ -801,7 +803,7 @@ describe('templates DOM manipulation', function() {
         // Reset to no data
         binding.context = getContext();
         binding.update();
-        var children = getChildren(fixture);
+        children = getChildren(fixture);
         expect(children.length).equal(1);
         expect(children[0].tagName.toLowerCase()).equal('h3');
         expect(getText(children[0])).equal('');
@@ -809,14 +811,14 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates a single condition ConditionalBlock', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.ConditionalBlock([
             new FakeExpression('show')
           ], [
             [new saddle.Text('shown')]
           ])
         ]);
-        var binding = render(template).pop();
+        const binding = render(template).pop();
         expect(getText(fixture)).equal('');
         // Update value
         binding.context = getContext({show: true});
@@ -829,7 +831,7 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates a multi-condition ConditionalBlock', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.ConditionalBlock([
             new FakeExpression('primary'),
             new FakeExpression('alternate'),
@@ -840,7 +842,7 @@ describe('templates DOM manipulation', function() {
             [new saddle.Text('else')]
           ])
         ]);
-        var binding = render(template).pop();
+        const binding = render(template).pop();
         expect(getText(fixture)).equal('else');
         // Update value
         binding.context = getContext({primary: 'Heyo'});
@@ -857,12 +859,12 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates an each of text', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression())
           ])
         ]);
-        var binding = render(template).pop();
+        const binding = render(template).pop();
         expect(getText(fixture)).equal('');
         // Update value
         binding.context = getContext({items: ['One', 'Two', 'Three']});
@@ -883,14 +885,14 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates an each with an else', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name'))
           ], [
             new saddle.Text('else')
           ])
         ]);
-        var binding = render(template).pop();
+        const binding = render(template).pop();
         expect(getText(fixture)).equal('else');
         // Update value
         binding.context = getContext({items: [
@@ -915,15 +917,15 @@ describe('templates DOM manipulation', function() {
       });
 
       it('inserts in an each', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name'))
           ])
         ]);
-        var binding = render(template).pop();
+        const binding = render(template).pop();
         expect(getText(fixture)).equal('');
         // Insert from null state
-        var data = {items: []};
+        const data = {items: []};
         binding.context = getContext(data);
         insert(binding, data.items, 0, [{name: 'One'}, {name: 'Two'}, {name: 'Three'}]);
         expect(getText(fixture)).equal('OneTwoThree');
@@ -933,34 +935,34 @@ describe('templates DOM manipulation', function() {
       });
 
       it('inserts into empty each with else', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name'))
           ], [
             new saddle.Text('else')
           ])
         ]);
-        var binding = render(template).pop();
+        const binding = render(template).pop();
         expect(getText(fixture)).equal('else');
         // Insert from null state
-        var data = {items: []};
+        const data = {items: []};
         binding.context = getContext(data);
         insert(binding, data.items, 0, [{name: 'One'}, {name: 'Two'}, {name: 'Three'}]);
         expect(getText(fixture)).equal('OneTwoThree');
       });
 
       it('removes all items in an each with else', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name'))
           ], [
             new saddle.Text('else')
           ])
         ]);
-        var data = {items: [
+        const data = {items: [
           {name: 'One'}, {name: 'Two'}, {name: 'Three'}
         ]};
-        var binding = render(template, data).pop();
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('OneTwoThree');
         binding.context = getContext(data);
         // Remove all items
@@ -969,15 +971,15 @@ describe('templates DOM manipulation', function() {
       });
 
       it('removes in an each', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name'))
           ])
         ]);
-        var data = {items: [
+        const data = {items: [
           {name: 'One'}, {name: 'Two'}, {name: 'Three'}
         ]};
-        var binding = render(template, data).pop();
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('OneTwoThree');
         binding.context = getContext(data);
         // Remove inner item
@@ -989,15 +991,15 @@ describe('templates DOM manipulation', function() {
       });
 
       it('moves in an each', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name'))
           ])
         ]);
-        var data = {items: [
+        const data = {items: [
           {name: 'One'}, {name: 'Two'}, {name: 'Three'}
         ]};
-        var binding = render(template, data).pop();
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('OneTwoThree');
         binding.context = getContext(data);
         // Move one item
@@ -1009,7 +1011,7 @@ describe('templates DOM manipulation', function() {
       });
 
       it('insert, move, and remove with multiple node items', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.Element('h3', null, [
               new saddle.DynamicText(new FakeExpression('title'))
@@ -1017,12 +1019,12 @@ describe('templates DOM manipulation', function() {
             new saddle.DynamicText(new FakeExpression('text'))
           ])
         ]);
-        var data = {items: [
+        const data = {items: [
           {title: '1', text: 'one'},
           {title: '2', text: 'two'},
           {title: '3', text: 'three'}
         ]};
-        var binding = render(template, data).pop();
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('1one2two3three');
         binding.context = getContext(data);
         // Insert an item
@@ -1037,7 +1039,7 @@ describe('templates DOM manipulation', function() {
       });
 
       it('inserts to outer nested each', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name')),
             new saddle.EachBlock(new FakeExpression('subitems'), [
@@ -1045,10 +1047,10 @@ describe('templates DOM manipulation', function() {
             ])
           ])
         ]);
-        var binding = render(template).pop();
+        const binding = render(template).pop();
         expect(getText(fixture)).equal('');
         // Insert from null state
-        var data = {items: []};
+        const data = {items: []};
         binding.context = getContext(data);
         insert(binding, data.items, 0, [
           {name: 'One', subitems: [1, 2, 3]},
@@ -1070,7 +1072,7 @@ describe('templates DOM manipulation', function() {
       });
 
       it('removes from outer nested each', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name')),
             new saddle.EachBlock(new FakeExpression('subitems'), [
@@ -1078,12 +1080,12 @@ describe('templates DOM manipulation', function() {
             ])
           ])
         ]);
-        var data = {items: [
+        const data = {items: [
           {name: 'One', subitems: [1, 2, 3]},
           {name: 'Two', subitems: [2, 4, 6]},
           {name: 'Three', subitems: [3, 6, 9]}
         ]};
-        var binding = render(template, data).pop();
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('One123Two246Three369');
         binding.context = getContext(data);
         // Remove inner item
@@ -1095,7 +1097,7 @@ describe('templates DOM manipulation', function() {
       });
 
       it('moves to outer nested each', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.DynamicText(new FakeExpression('name')),
             new saddle.EachBlock(new FakeExpression('subitems'), [
@@ -1103,12 +1105,12 @@ describe('templates DOM manipulation', function() {
             ])
           ])
         ]);
-        var data = {items: [
+        const data = {items: [
           {name: 'One', subitems: [1, 2, 3]},
           {name: 'Two', subitems: [2, 4, 6]},
           {name: 'Three', subitems: [3, 6, 9]}
         ]};
-        var binding = render(template, data).pop();
+        const binding = render(template, data).pop();
         expect(getText(fixture)).equal('One123Two246Three369');
         binding.context = getContext(data);
         // Move one item
@@ -1120,7 +1122,7 @@ describe('templates DOM manipulation', function() {
       });
 
       it('updates an if inside an each', function() {
-        var template = new saddle.Template([
+        const template = new saddle.Template([
           new saddle.EachBlock(new FakeExpression('items'), [
             new saddle.ConditionalBlock([
               new FakeExpression('flag'),
@@ -1131,13 +1133,13 @@ describe('templates DOM manipulation', function() {
             ])
           ])
         ]);
-        var data = {items: [0, 1], flag: true};
-        var bindings = render(template, data);
+        const data = {items: [0, 1], flag: true};
+        const bindings = render(template, data);
         expect(getText(fixture)).equal('AA');
 
-        var eachBinding = bindings[4];
-        var if1Binding = bindings[2];
-        var if2Binding = bindings[0];
+        const eachBinding = bindings[4];
+        const if1Binding = bindings[2];
+        const if2Binding = bindings[0];
 
         data.flag = false;
         if1Binding.update();
@@ -1152,7 +1154,7 @@ describe('templates DOM manipulation', function() {
 });
 
 function getContext(data, bindings) {
-  var contextMeta = new FakeContextMeta();
+  const contextMeta = new FakeContextMeta();
   contextMeta.addBinding = function(binding) {
     if (bindings) {
       bindings.push(binding);
@@ -1169,10 +1171,10 @@ function removeChildren(node) {
 
 // IE <=8 return comments for Node.children
 function getChildren(node) {
-  var nodeChildren = node.children;
-  var children = [];
-  for (var i = 0, len = nodeChildren.length; i < len; i++) {
-    var child = nodeChildren[i];
+  const nodeChildren = node.children;
+  const children = [];
+  for (const i = 0, len = nodeChildren.length; i < len; i++) {
+    const child = nodeChildren[i];
     if (child.nodeType === 1) children.push(child);
   }
   return children;
@@ -1197,7 +1199,7 @@ function remove(binding, array, index, howMany) {
   binding.remove(index, howMany);
 }
 function move(binding, array, from, to, howMany) {
-  var values = array.splice(from, howMany);
+  const values = array.splice(from, howMany);
   array.splice.apply(array, [to, 0].concat(values));
   binding.move(from, to, howMany);
 }
@@ -1251,11 +1253,11 @@ FakeContext.prototype.removeNode = function(node) {
   this.meta.removeNode(node);
 };
 FakeContext.prototype.child = function(expression) {
-  var data = expression.get(this);
+  const data = expression.get(this);
   return new FakeContext(this.meta, data, this);
 };
 FakeContext.prototype.eachChild = function(expression, index) {
-  var data = expression.get(this)[index];
+  const data = expression.get(this)[index];
   return new FakeContext(this.meta, data, this);
 };
 FakeContext.prototype._get = function(property) {
@@ -1271,11 +1273,11 @@ FakeContext.prototype.unpause = function() {
   this.flush();
 };
 FakeContext.prototype.flush = function() {
-  var pending = this.meta.pending;
-  var len = pending.length;
+  const pending = this.meta.pending;
+  const len = pending.length;
   if (!len) return;
   this.meta.pending = [];
-  for (var i = 0; i < len; i++) {
+  for (const i = 0; i < len; i++) {
     pending[i]();
   }
 };