diff --git a/Gruntfile.js b/Gruntfile.js
index 97f0fd99e5444c175c5c9ff3393bda923652b9b8..fa7c0ef09822afb6b1b2ffc44f0d2329fd43c34c 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -88,25 +88,11 @@ module.exports = function (grunt) {
         ],
 
         spec: function (theme) {
-            var tasks = [],
-                themes = require(configDir + '/themes');
+            var runner = require('./dev/tests/js/jasmine/spec_runner');
 
-            function tasksFor(theme) {
-                return [
-                    'connect:' + theme,
-                    'jasmine:' + theme
-                ];
-            }
-
-            if (!theme) {
-                Object.keys(themes).forEach(function (theme) {
-                    tasks = tasks.concat(tasksFor(theme));
-                });
-            } else {
-                tasks = tasksFor(theme);
-            }
+            runner.init(grunt, { theme: theme });
 
-            grunt.task.run(tasks);
+            grunt.task.run(runner.getTasks());
         }
     }, function (task, name) {
         grunt.registerTask(name, task);
diff --git a/dev/tests/js/jasmine/assets/apply/index.js b/dev/tests/js/jasmine/assets/apply/index.js
index 31268a6aa773230559203076bff7998a1d91c1ea..7fd9b379e1f24ef6329d9ed5c21ef02c3654c7f5 100644
--- a/dev/tests/js/jasmine/assets/apply/index.js
+++ b/dev/tests/js/jasmine/assets/apply/index.js
@@ -3,7 +3,7 @@
  * See COPYING.txt for license details.
  */
 define([
-    'tests/tools',
+    'tests/assets/tools',
     'tests/assets/apply/components/fn',
     'text!./config.json',
     'text!./templates/node.html'
diff --git a/dev/tests/js/jasmine/assets/script/index.js b/dev/tests/js/jasmine/assets/script/index.js
index 031241f4c2d3e8beb24b9d7ee2373daa058a03f7..e9519e7ebe1e6b4e398ef06b68e59facb73850f5 100644
--- a/dev/tests/js/jasmine/assets/script/index.js
+++ b/dev/tests/js/jasmine/assets/script/index.js
@@ -3,7 +3,7 @@
  * See COPYING.txt for license details.
  */
 define([
-    'tests/tools',
+    'tests/assets/tools',
     'text!./config.json',
     'text!./templates/selector.html',
     'text!./templates/virtual.html'
diff --git a/dev/tests/js/jasmine/tools.js b/dev/tests/js/jasmine/assets/tools.js
similarity index 100%
rename from dev/tests/js/jasmine/tools.js
rename to dev/tests/js/jasmine/assets/tools.js
diff --git a/dev/tests/js/jasmine/require.conf.js b/dev/tests/js/jasmine/require.conf.js
index 2a96ba34260978da9aeb25a98debf835cc1b173a..b0158df19c6c4aabea47d7f2ed7e661e5adb6d05 100644
--- a/dev/tests/js/jasmine/require.conf.js
+++ b/dev/tests/js/jasmine/require.conf.js
@@ -3,6 +3,8 @@
  * See COPYING.txt for license details.
  */
 
+'use strict';
+
 require.config({
     baseUrl: './',
     bundles: {
diff --git a/dev/tests/js/jasmine/spec_runner/index.js b/dev/tests/js/jasmine/spec_runner/index.js
new file mode 100644
index 0000000000000000000000000000000000000000..d63429aae1270559b0ef2af345d06d8dde170b8c
--- /dev/null
+++ b/dev/tests/js/jasmine/spec_runner/index.js
@@ -0,0 +1,62 @@
+/**
+ * Copyright © 2015 Magento. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+'use strict';
+
+var tasks = [],
+    _ = require('underscore');
+
+function init(grunt, options) {
+    var _                   = require('underscore'),
+        stripJsonComments   = require('strip-json-comments'),
+        path                = require('path'),
+        config,
+        themes;
+        
+    config = grunt.file.read(__dirname + '/settings.json');
+    config = stripJsonComments(config);
+    config = JSON.parse(config);
+
+    themes = require(path.resolve(process.cwd(), config.themes));
+
+    if (options.theme) {
+        themes = _.pick(themes, options.theme);
+    }
+
+    tasks = Object.keys(themes);
+
+    config.themes = themes;
+
+    enableTasks(grunt, config);
+}
+
+function enableTasks(grunt, config) {
+    var jasmine = require('./tasks/jasmine'),
+        connect = require('./tasks/connect');
+
+    jasmine.init(config);
+    connect.init(config);
+
+    grunt.initConfig({
+        jasmine: jasmine.getTasks(),
+        connect: connect.getTasks()
+    });
+}
+
+function getTasks() {
+    tasks = tasks.map(function (theme) {
+        return [
+            'connect:' + theme,
+            'jasmine:' + theme
+        ]
+    });
+
+    return _.flatten(tasks);
+}
+
+module.exports = {
+    init: init,
+    getTasks: getTasks
+};
\ No newline at end of file
diff --git a/dev/tests/js/jasmine/spec_runner/settings.json b/dev/tests/js/jasmine/spec_runner/settings.json
new file mode 100644
index 0000000000000000000000000000000000000000..df88ea43125ccdd2ef79d3a6cd1dde4ee5b5a87a
--- /dev/null
+++ b/dev/tests/js/jasmine/spec_runner/settings.json
@@ -0,0 +1,72 @@
+{
+    "host": "http://localhost:<%= port %>",
+    "port": 8000,
+    "root": "dev/tests/js/jasmine",
+
+    /**
+     * Path to themes configuration module. Relative to Magento root.
+     * This node is replaced by formatted theme configuration by 'dev/tests/jasmine/spec_runner' module
+     */
+    "themes": "dev/tools/grunt/configs/themes",
+    
+    "files": {
+        /**
+         * Path to RequireJS library. Relative to "server.base" config.
+         */
+        "requireJs": "requirejs/require.js",
+
+        /**
+         * Overriden "grunt-contrib-jasmine" SpecRunner template.
+         */
+        "template": "<%= root %>/spec_runner/template.html",
+
+        /**
+         * These files are included to the page in <head> right after "require.js" in declared sequence.
+         */
+        "requirejsConfigs": [
+            "pub/static/_requirejs/<%= area %>/<%= name %>/<%= locale %>/requirejs-config.js",
+            "<%= root %>/require.conf.js",
+            "<%= root %>/tests/lib/**/*.conf.js",
+            "<%= root %>/tests/app/code/**/base/**/*.conf.js",
+            "<%= root %>/tests/app/code/**/<%= area %>/**/*.conf.js",
+            "<%= root %>/tests/app/design/<%= area %>/<%= name %>/**/*.conf.js"
+        ],
+
+        /**
+         * Files that contain tests. These are loaded to the page via RequireJS after all RequireJS configuration files have been loaded to the page.
+         * The sequence is ignored.
+         */
+        "specs": [
+            "<%= root %>/tests/lib/**/*.test.js",
+            "<%= root %>/tests/app/code/**/base/**/*.test.js",
+            "<%= root %>/tests/app/code/**/<%= area %>/**/*.test.js",
+            "<%= root %>/tests/app/design/<%= area %>/<%= name %>/**/*.test.js"
+        ]
+    },
+    "server": {
+        /**
+         * Directory to serve files from
+         */
+        "base": "pub/static/<%= area %>/<%= name %>/<%= locale %>",
+
+        /**
+         * Strings, mentioned here are interpreted as regular expressions. Use this option to override server's
+         *     default behaviour and serve matched urls "as is" from Magento root.
+         */
+        "serveAsIs": [
+            "^\/_SpecRunner.html",
+            "^\/dev\/tests",
+            "^\/.grunt",
+            "^\/pub\/static"
+        ],
+        "options": {
+            /**
+             * All options mentioned here are defaults for "connect" grunt task.
+             * "debug" option enables server logs
+             * "keepalive" makes "connect" task pause with set up spec server, which you can fetch by %host%:%port%/_SpecRunner.html address in browser
+             */
+            "debug": false,
+            "keepalive": false
+        }
+    }
+}
\ No newline at end of file
diff --git a/dev/tests/js/jasmine/spec_runner/tasks/connect.js b/dev/tests/js/jasmine/spec_runner/tasks/connect.js
new file mode 100644
index 0000000000000000000000000000000000000000..d472f210d3d5fe1abd5e0ac3e68336819eb12f42
--- /dev/null
+++ b/dev/tests/js/jasmine/spec_runner/tasks/connect.js
@@ -0,0 +1,64 @@
+/**
+ * Copyright © 2015 Magento. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+'use strict';
+
+var tasks = {};
+
+function init(config) {
+    var serveStatic = require('serve-static'),
+        grunt       = require('grunt'),
+        _           = require('underscore'),
+        path        = require('path'),
+        ignoredPaths, middleware, themes, files, port;
+
+    port         = config.port;
+    files        = config.files;
+    themes       = config.themes;
+    ignoredPaths = config.server.serveAsIs;
+
+    function serveAsIs(path) {
+        return ignoredPaths.some(function (ignoredPath) {
+            return new RegExp(ignoredPath).test(path);
+        });
+    }
+
+    middleware = function (connect, options, middlewares) {
+        var server = serveStatic(process.cwd());
+
+        middlewares.unshift(function (req, res, next) {
+            var url = req.url;
+                
+            if (serveAsIs(url)) {
+                return server.apply(null, arguments);
+            }
+
+            return next();
+        });
+
+        return middlewares;
+    }
+
+    _.each(themes, function (themeData, themeName) {
+        var options = {
+            base: _.template(config.server.base)(themeData),
+            port: port++,
+            middleware: middleware
+        };
+
+        _.defaults(options, config.server.options);
+
+        tasks[themeName] = { options: options };
+    });
+}
+
+function getTasks() {
+    return tasks;
+}
+
+module.exports = {
+    init: init,
+    getTasks: getTasks
+};
\ No newline at end of file
diff --git a/dev/tests/js/jasmine/spec_runner/tasks/jasmine.js b/dev/tests/js/jasmine/spec_runner/tasks/jasmine.js
new file mode 100644
index 0000000000000000000000000000000000000000..9c2eaeb23996feb26f2f8f4819056cacdbc47bb0
--- /dev/null
+++ b/dev/tests/js/jasmine/spec_runner/tasks/jasmine.js
@@ -0,0 +1,65 @@
+/**
+ * Copyright © 2015 Magento. All rights reserved.
+ * See COPYING.txt for license details.
+ */
+
+'use strict';
+
+var tasks = {},
+    _ = require('underscore');
+
+function init(config) {
+    var grunt  = require('grunt'),
+        expand = grunt.file.expand.bind(grunt.file),
+        themes, root, host, port, files;
+
+    root         = config.root;
+    port         = config.port;
+    files        = config.files;
+    host         = _.template(config.host)({ port: port });
+    themes       = config.themes;
+
+    _.each(themes, function (themeData, themeName) {
+        var specs,
+            configs,
+            render;
+
+        _.extend(themeData, { root: root });
+
+        render  = renderTemplate.bind(null, themeData);
+        specs   = files.specs.map(render);
+        specs   = expand(specs).map(cutJsExtension);
+        configs = files.requirejsConfigs.map(render);
+
+        tasks[themeName] = {
+            src: configs,
+            options: {
+                host: host,
+                template: render(files.template),
+                vendor: files.requireJs,
+
+                /**
+                 * @todo rename "helpers" to "specs" (implies overriding grunt-contrib-jasmine code)
+                 */
+                helpers: specs
+            }
+        }
+    });
+}
+
+function renderTemplate(data, template) {
+    return _.template(template)(data);
+}
+
+function cutJsExtension(path) {
+    return path.replace(/\.js$/, '');
+}
+
+function getTasks() {
+    return tasks;
+}
+
+module.exports = {
+    init: init,
+    getTasks: getTasks
+};
\ No newline at end of file
diff --git a/dev/tests/js/jasmine/spec_runner.html b/dev/tests/js/jasmine/spec_runner/template.html
similarity index 100%
rename from dev/tests/js/jasmine/spec_runner.html
rename to dev/tests/js/jasmine/spec_runner/template.html
diff --git a/dev/tests/js/jasmine/tests/lib/mage/apply.test.js b/dev/tests/js/jasmine/tests/lib/mage/apply.test.js
index 5e3965b5903b76d1089e57cf8d9e783065bfa342..a9d22ac7c67d2f59379da2e8aa67fdc503b8d2ae 100644
--- a/dev/tests/js/jasmine/tests/lib/mage/apply.test.js
+++ b/dev/tests/js/jasmine/tests/lib/mage/apply.test.js
@@ -4,7 +4,7 @@
  */
 define([
     'underscore',
-    'tests/tools',
+    'tests/assets/tools',
     'tests/assets/apply/index',
     'mage/apply/main'
 ], function (_, tools, config, mage) {
diff --git a/dev/tests/js/jasmine/tests/lib/mage/scripts.test.js b/dev/tests/js/jasmine/tests/lib/mage/scripts.test.js
index 05c4c13388919dd9a0ffd482663cf9bb99eda4bc..3bfbd4b637ea97801570ff2a571e8e21c3009d9c 100644
--- a/dev/tests/js/jasmine/tests/lib/mage/scripts.test.js
+++ b/dev/tests/js/jasmine/tests/lib/mage/scripts.test.js
@@ -3,7 +3,7 @@
  * See COPYING.txt for license details.
  */
 define([
-    'tests/tools',
+    'tests/assets/tools',
     'tests/assets/script/index',
     'mage/apply/scripts'
 ], function (tools, config, processScripts) {
diff --git a/dev/tools/grunt/configs/connect.js b/dev/tools/grunt/configs/connect.js
deleted file mode 100644
index 62007a5bec993dff41550d21d017af62916a8ed2..0000000000000000000000000000000000000000
--- a/dev/tools/grunt/configs/connect.js
+++ /dev/null
@@ -1,61 +0,0 @@
-/**
- * Copyright © 2015 Magento. All rights reserved.
- * See COPYING.txt for license details.
- */
-
-'use strict';
-
-var serveStatic = require('serve-static'),
-    _           = require('underscore'),
-    ignoredPaths,
-    middleware,
-    themes,
-    tasks,
-    port = 8000;
-
-ignoredPaths = [
-    /^\/_SpecRunner.html/,
-    /^\/dev\/tests/,
-    /^\/.grunt/,
-    /^\/pub\/static/
-];
-
-function serveAsIs(path) {
-    return ignoredPaths.some(function (ignoredPath) {
-        return ignoredPath.test(path);
-    });
-}
-
-middleware = function (connect, options, middlewares) {
-    middlewares.unshift(function (req, res, next) {
-        var url = req.url,
-            server = serveStatic(process.cwd());
-            
-        if (serveAsIs(url)) {
-            return server.apply(null, arguments);
-        }
-
-        return next();
-    });
-
-    return middlewares;
-}
-
-themes = require('./themes');
-
-tasks = {};
-
-_.each(themes, function (config, theme) {
-    tasks[theme] = {
-        options: {
-            /**
-             * To debug in browser, add "keepalive" option and launch "http://localhost:%PORT%/_SpecRunner.html"
-             */
-            base: 'pub/static/' + config.area + '/' + config.name + '/' + config.locale,
-            port: port++,
-            middleware: middleware
-        }
-    }
-});
-
-module.exports = tasks;
\ No newline at end of file
diff --git a/dev/tools/grunt/configs/jasmine.js b/dev/tools/grunt/configs/jasmine.js
deleted file mode 100644
index b4d6e6f6b5ccf3e96ff09f1890ef78a95afacba6..0000000000000000000000000000000000000000
--- a/dev/tools/grunt/configs/jasmine.js
+++ /dev/null
@@ -1,62 +0,0 @@
-/**
- * Copyright © 2015 Magento. All rights reserved.
- * See COPYING.txt for license details.
- */
-
-'use strict';
-
-var grunt = require('grunt'),
-    _     = require('underscore'),
-    expand = grunt.file.expand.bind(grunt.file),
-    themes,
-    tasks,
-    root = 'dev/tests/js/jasmine',
-    host = 'http://localhost',
-    port = 8000;
-
-function cutExtension(name) {
-    return name.replace(/\.js$/, '');
-}
-
-themes = require('./themes');
-
-tasks = {};
-
-_.each(themes, function (config, theme) {
-    var requireJsConfigs,
-        specs,
-        area = config.area,
-        vendorThemePath = config.name;
-
-    requireJsConfigs = [
-        'pub/static/_requirejs/' + area + '/' + vendorThemePath + '/' + config.locale + '/requirejs-config.js',
-        root + '/require.conf.js',
-        root + '/tests/lib/**/*.conf.js',
-        root + '/tests/app/code/**/base/**/*.conf.js',
-        root + '/tests/app/code/**/' + area + '/**/*.conf.js',
-        root + '/tests/app/design/' + area + '/' + vendorThemePath + '/**/*.conf.js'
-    ];
-
-    specs = [
-        root + '/tests/lib/**/*.test.js',
-        root + '/tests/app/code/**/base/**/*.test.js',
-        root + '/tests/app/code/**/' + area + '/**/*.test.js',
-        root + '/tests/app/design/' + area + '/' + vendorThemePath + '/' + theme + '/**/*.test.js'
-    ];
-
-    tasks[theme] = {
-        src: requireJsConfigs,
-        options: {
-            host: host + ':' + port++,
-            template: root + '/spec_runner.html',
-            vendor: 'requirejs/require.js',
-
-            /**
-             * @todo rename "helpers" to "specs" (implies overriding grunt-contrib-jasmine code)
-             */
-            helpers: expand(specs).map(cutExtension)
-        }
-    }
-});
-
-module.exports = tasks;
\ No newline at end of file
diff --git a/package.json b/package.json
index 48692f0022f092b48d842ac3b9104eaa2299ed26..eaee4e18e2484b09731ec9a8e2a165eef83ae29c 100644
--- a/package.json
+++ b/package.json
@@ -28,6 +28,7 @@
     "morgan": "^1.5.0",
     "node-minify": "^1.0.1",
     "serve-static": "^1.7.1",
+    "strip-json-comments": "^1.0.2",
     "time-grunt": "^1.0.0",
     "underscore": "^1.7.0"
   },