diff --git a/core/modules/views/tests/js/test/base.js b/core/modules/views/tests/js/test/base.js
new file mode 100644
index 0000000..33f6ec9
--- /dev/null
+++ b/core/modules/views/tests/js/test/base.js
@@ -0,0 +1,27 @@
+var chai = require('chai');
+var assert = chai.assert;
+var sinon = require('sinon');
+
+$ = sinon.stub();
+$.withArgs(sinon.match.any).returns(sinon.stub({on: function() {} }));
+
+var Drupal = {};
+Drupal.behaviors = {};
+document = null;
+drupalSettings = {};
+
+var execfile = require("execfile.js");
+var context = execfile("../../js/base.js", { jQuery: $, Drupal: Drupal, document: document, drupalSettings: drupalSettings});
+
+describe('base', function() {
+  describe('parseQueryString', function() {
+    it('parse empty string', function() {
+      var result = context.Drupal.Views.parseQueryString('');
+      assert.deepEqual({}, result);
+    });
+    it('parse simple query string', function() {
+      var result = context.Drupal.Views.parseQueryString('foo=bar');
+      assert.deepEqual({foo: 'bar'}, result);
+    });
+  });
+});
diff --git a/core/scripts/run-js-tests.sh b/core/scripts/run-js-tests.sh
new file mode 100755
index 0000000..916bffc
--- /dev/null
+++ b/core/scripts/run-js-tests.sh
@@ -0,0 +1,27 @@
+#!/usr/bin/env bash
+
+function HELP {
+  echo -e "Help for run-js-tests.sh"\\n
+  echo -e "Run core tests: core/scripts/run-js-tests.sh core/tests/js/test"
+  echo -e "Run specific modules: core/scripts/run-js-tests.sh core/modules/views/tests/js/test"
+  exit 1
+}
+
+NUMARGS=$#
+if [ $NUMARGS -eq 0 ]; then
+  HELP
+fi
+
+DRUPAL_ROOT=$(pwd)
+export NODE_PATH=${NODE_PATH}:${DRUPAL_ROOT}/core/tests/js/node_modules:${DRUPAL_ROOT}/core/tests/js
+
+PARAMS=("$@")
+PARAMS[0]=${DRUPAL_ROOT}/${PARAMS[0]}
+cd ${PARAMS[0]}/../
+
+if [ "$1" == "--help" ]; then
+  PARAMS=(--help)
+fi
+
+# devtool --break ${DRUPAL_ROOT}/core/tests/js/node_modules/mocha/bin/mocha -qc -- ${PARAMS[@]}
+${DRUPAL_ROOT}/core/tests/js/node_modules/mocha/bin/mocha ${PARAMS[@]}
diff --git a/core/tests/Drupal/Tests/MochaJsTestDiscovery.php b/core/tests/Drupal/Tests/MochaJsTestDiscovery.php
new file mode 100644
index 0000000..4d6cac1
--- /dev/null
+++ b/core/tests/Drupal/Tests/MochaJsTestDiscovery.php
@@ -0,0 +1,36 @@
+<?php
+
+namespace Drupal\Tests;
+
+use Drupal\Core\Extension\Extension;
+use Drupal\Core\Extension\ExtensionDiscovery;
+
+class MochaJsTestDiscovery {
+
+  protected $root;
+
+  protected $sitePath;
+
+  public function __construct($root, $site_path) {
+    $this->root = $root;
+    $this->sitePath = $site_path;
+  }
+
+  public function getTestClasses() {
+    $extension_discovery = new ExtensionDiscovery($this->root, TRUE, [], $this->sitePath);
+
+    $extensions = ['core' => new Extension($this->root, 'core', 'core/core.info.yml')];
+    $extensions += $extension_discovery->scan('module', TRUE);
+    $extensions += $extension_discovery->scan('theme', TRUE);
+    $extensions += $extension_discovery->scan('profile', TRUE);
+  
+    $js_tests = [];
+    foreach ($extensions as $extension) {
+      if (file_exists($this->root . '/' . $extension->getPath() . '/tests/js')) {
+        $js_tests['js--mocha-' . $extension->getName()] = TRUE;
+      };
+    }
+    return $js_tests;
+  }
+
+}
diff --git a/core/tests/Drupal/Tests/MochaJsTestDiscoveryTest.php b/core/tests/Drupal/Tests/MochaJsTestDiscoveryTest.php
new file mode 100644
index 0000000..7c72cb5
--- /dev/null
+++ b/core/tests/Drupal/Tests/MochaJsTestDiscoveryTest.php
@@ -0,0 +1,69 @@
+<?php
+
+namespace Drupal\Tests;
+
+use Drupal\Component\FileCache\FileCacheFactory;
+use org\bovigo\vfs\vfsStream;
+
+class MochaJsTestDiscoveryTest extends \PHPUnit_Framework_TestCase {
+
+  public function testGetClasses() {
+    FileCacheFactory::setPrefix(__METHOD__);
+
+    vfsStream::setup('drupal');
+    $module_a = <<<EOF
+type: module
+core: 8.x
+name: module a
+EOF;
+    $module_b = <<<EOF
+type: module
+core: 8.x
+name: module b
+EOF;
+    $theme_a = <<<EOF
+type: theme
+core: 8.x
+name: theme a
+EOF;
+
+    vfsStream::create([
+      'core/tests/js' => [
+        'test.js' => '',
+      ],
+      'modules' => [
+        'module_a' => [
+          'module_a.info.yml' => $module_a,
+          'tests' => [
+            'js' => [
+              'test.js' => '',
+            ],
+          ],
+        ],
+        'module_b' => [
+          'module_b.info.yml' => $module_b,
+        ],
+      ],
+      'themes' => [
+        'theme_a' => [
+          'theme_a.info.yml' => $theme_a,
+          'tests' => [
+            'js' => [
+              'test.js' => '',
+            ],
+          ],
+        ],
+      ],
+    ]);
+
+    $js_test_discovery = new MochaJsTestDiscovery('vfs://drupal', 'sites/default');
+    $result = $js_test_discovery->getTestClasses();
+    $expected = [
+      'js--mocha-core' => TRUE,
+      'js--mocha-module_a' => TRUE,
+      'js--mocha-theme_a' => TRUE,
+    ];
+    $this->assertEquals($expected, $result);
+  }
+
+}
diff --git a/core/tests/README.md b/core/tests/README.md
index dfd1154..8da9bd2 100644
--- a/core/tests/README.md
+++ b/core/tests/README.md
@@ -2,6 +2,8 @@
 
 ## Functional tests
 
+First you need to install phantomjs on your system, see http://phantomjs.org/download.html.
+
 * Start PhantomJS:
   ```
   phantomjs --ssl-protocol=any --ignore-ssl-errors=true ./vendor/jcalderonzumba/gastonjs/src/Client/main.js 8510 1024 768 2>&1 >> /dev/null &
@@ -13,3 +15,24 @@
   ./vendor/bin/phpunit -c core --testsuite functional
   ./vendor/bin/phpunit -c core --testsuite functional-javascript
   ```
+
+## Unit testing
+
+### PHP
+
+./vendor/bin/phpunit -c core --testsuite unit
+
+### Javascript
+
+First you need to install the dependencies using npm, see (Installing NPM)$https://docs.npmjs.com/getting-started/installing-node].
+
+```
+cd core/tests/js
+npm install
+```
+
+Once you have installed it, you can run the tests using
+
+```
+node_modules/mocha/bin/mocha
+```
diff --git a/core/tests/js/execfile.js b/core/tests/js/execfile.js
new file mode 100644
index 0000000..7af1b8b
--- /dev/null
+++ b/core/tests/js/execfile.js
@@ -0,0 +1,8 @@
+var vm = require("vm");
+var fs = require("fs");
+module.exports = function(path, context) {
+  context = context || {};
+  var data = fs.readFileSync(path);
+  vm.runInNewContext(data, context, path);
+  return context;
+}
diff --git a/core/tests/js/npm-shrinkwrap.json b/core/tests/js/npm-shrinkwrap.json
new file mode 100644
index 0000000..e611279
--- /dev/null
+++ b/core/tests/js/npm-shrinkwrap.json
@@ -0,0 +1,185 @@
+{
+  "name": "js",
+  "version": "1.0.0",
+  "dependencies": {
+    "assertion-error": {
+      "version": "1.0.1",
+      "from": "assertion-error@>=1.0.1 <2.0.0",
+      "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-1.0.1.tgz"
+    },
+    "chai": {
+      "version": "3.5.0",
+      "from": "chai@>=3.5.0 <4.0.0",
+      "resolved": "https://registry.npmjs.org/chai/-/chai-3.5.0.tgz"
+    },
+    "charenc": {
+      "version": "0.0.1",
+      "from": "charenc@>=0.0.1 <0.1.0",
+      "resolved": "https://registry.npmjs.org/charenc/-/charenc-0.0.1.tgz"
+    },
+    "commander": {
+      "version": "2.3.0",
+      "from": "commander@2.3.0",
+      "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz"
+    },
+    "crypt": {
+      "version": "0.0.1",
+      "from": "crypt@>=0.0.1 <0.1.0",
+      "resolved": "https://registry.npmjs.org/crypt/-/crypt-0.0.1.tgz"
+    },
+    "debug": {
+      "version": "2.2.0",
+      "from": "debug@2.2.0",
+      "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz"
+    },
+    "deep-eql": {
+      "version": "0.1.3",
+      "from": "deep-eql@>=0.1.3 <0.2.0",
+      "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-0.1.3.tgz",
+      "dependencies": {
+        "type-detect": {
+          "version": "0.1.1",
+          "from": "type-detect@0.1.1",
+          "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-0.1.1.tgz"
+        }
+      }
+    },
+    "diff": {
+      "version": "1.4.0",
+      "from": "diff@1.4.0",
+      "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz"
+    },
+    "escape-string-regexp": {
+      "version": "1.0.2",
+      "from": "escape-string-regexp@1.0.2",
+      "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz"
+    },
+    "formatio": {
+      "version": "1.1.1",
+      "from": "formatio@1.1.1",
+      "resolved": "https://registry.npmjs.org/formatio/-/formatio-1.1.1.tgz"
+    },
+    "glob": {
+      "version": "3.2.3",
+      "from": "glob@3.2.3",
+      "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.3.tgz"
+    },
+    "graceful-fs": {
+      "version": "2.0.3",
+      "from": "graceful-fs@>=2.0.0 <2.1.0",
+      "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-2.0.3.tgz"
+    },
+    "growl": {
+      "version": "1.8.1",
+      "from": "growl@1.8.1",
+      "resolved": "https://registry.npmjs.org/growl/-/growl-1.8.1.tgz"
+    },
+    "inherits": {
+      "version": "2.0.1",
+      "from": "inherits@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz"
+    },
+    "is-buffer": {
+      "version": "1.1.3",
+      "from": "is-buffer@>=1.1.1 <1.2.0",
+      "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.3.tgz"
+    },
+    "jade": {
+      "version": "0.26.3",
+      "from": "jade@0.26.3",
+      "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz",
+      "dependencies": {
+        "commander": {
+          "version": "0.6.1",
+          "from": "commander@0.6.1",
+          "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz"
+        },
+        "mkdirp": {
+          "version": "0.3.0",
+          "from": "mkdirp@0.3.0",
+          "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz"
+        }
+      }
+    },
+    "lolex": {
+      "version": "1.3.2",
+      "from": "lolex@1.3.2",
+      "resolved": "https://registry.npmjs.org/lolex/-/lolex-1.3.2.tgz"
+    },
+    "lru-cache": {
+      "version": "2.7.3",
+      "from": "lru-cache@>=2.0.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz"
+    },
+    "md5": {
+      "version": "2.1.0",
+      "from": "md5@>=2.1.0 <3.0.0",
+      "resolved": "https://registry.npmjs.org/md5/-/md5-2.1.0.tgz"
+    },
+    "minimatch": {
+      "version": "0.2.14",
+      "from": "minimatch@>=0.2.11 <0.3.0",
+      "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.2.14.tgz"
+    },
+    "minimist": {
+      "version": "0.0.8",
+      "from": "minimist@0.0.8",
+      "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz"
+    },
+    "mkdirp": {
+      "version": "0.5.1",
+      "from": "mkdirp@0.5.1",
+      "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz"
+    },
+    "mocha": {
+      "version": "2.4.5",
+      "from": "mocha@>=2.4.5 <3.0.0",
+      "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.4.5.tgz"
+    },
+    "mocha-junit-reporter": {
+      "version": "1.11.1",
+      "from": "mocha-junit-reporter@latest",
+      "resolved": "https://registry.npmjs.org/mocha-junit-reporter/-/mocha-junit-reporter-1.11.1.tgz"
+    },
+    "ms": {
+      "version": "0.7.1",
+      "from": "ms@0.7.1",
+      "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz"
+    },
+    "samsam": {
+      "version": "1.1.2",
+      "from": "samsam@1.1.2",
+      "resolved": "https://registry.npmjs.org/samsam/-/samsam-1.1.2.tgz"
+    },
+    "sigmund": {
+      "version": "1.0.1",
+      "from": "sigmund@>=1.0.0 <1.1.0",
+      "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz"
+    },
+    "sinon": {
+      "version": "1.17.3",
+      "from": "sinon@>=1.17.3 <2.0.0",
+      "resolved": "https://registry.npmjs.org/sinon/-/sinon-1.17.3.tgz"
+    },
+    "supports-color": {
+      "version": "1.2.0",
+      "from": "supports-color@1.2.0",
+      "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz"
+    },
+    "type-detect": {
+      "version": "1.0.0",
+      "from": "type-detect@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-1.0.0.tgz"
+    },
+    "util": {
+      "version": "0.10.3",
+      "from": "util@>=0.10.3 <1.0.0",
+      "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz"
+    },
+    "xml": {
+      "version": "1.0.1",
+      "from": "xml@>=1.0.0 <2.0.0",
+      "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz"
+    }
+  }
+}
diff --git a/core/tests/js/package.json b/core/tests/js/package.json
new file mode 100644
index 0000000..26e6fa3
--- /dev/null
+++ b/core/tests/js/package.json
@@ -0,0 +1,17 @@
+{
+  "name": "js",
+  "version": "1.0.0",
+  "description": "",
+  "main": "index.js",
+  "scripts": {
+    "test": "echo \"Error: no test specified\" && exit 1"
+  },
+  "author": "",
+  "license": "ISC",
+  "dependencies": {
+    "chai": "^3.5.0",
+    "mocha": "^2.4.5",
+    "mocha-junit-reporter": "^1.11.1",
+    "sinon": "^1.17.3"
+  }
+}
diff --git a/core/tests/js/test/states.js b/core/tests/js/test/states.js
new file mode 100644
index 0000000..0a6618d
--- /dev/null
+++ b/core/tests/js/test/states.js
@@ -0,0 +1,25 @@
+var execfile = require("execfile");
+var sinon = require('sinon');
+var chai = require('chai');
+var assert = chai.assert;
+
+var Drupal = {};
+Drupal.behaviors = {};
+document = null;
+
+$ = sinon.stub();
+$.withArgs(sinon.match.any).returns(sinon.stub({on: function() {} }));
+
+var context = execfile("../../misc/states.js", { jQuery: $, Drupal: Drupal, document: document});
+
+describe('states', function() {
+  describe('states.Dependent.comparisons.Function', function () {
+    it('it compares', function() {
+      var states = context.Drupal.states;
+      assert.isOk(states.Dependent.comparisons.Function(function(a) {
+        return a == 'hello';
+      }, 'hello'));
+    });
+  });
+});
+
diff --git a/example.gitignore b/example.gitignore
index d1e35ac..00683c7 100644
--- a/example.gitignore
+++ b/example.gitignore
@@ -14,6 +14,9 @@
 # Core's dependencies are managed with Composer.
 vendor
 
+# Ignore javascript test dependencies.
+node_modules
+
 # Ignore configuration files that may contain sensitive information.
 sites/*/settings*.php
 sites/*/services*.yml
