[qunit] stop dealing with filesystem paths
authorRémi Cardona <remi.cardona@logilab.fr>, Julien Cristau <julien.cristau@logilab.fr>
Thu, 26 Nov 2015 11:30:54 +0100
changeset 10935 049209b9e9d6
parent 10934 3f18ec9d96dd
child 10936 c3606b52092c
[qunit] stop dealing with filesystem paths qunit tests need a few things served by cubicweb: - qunit itself, which is now handled by CWDevtoolsStaticController (serving files in cubicweb/devtools/data) - standard cubicweb or cubes data files, handled by the DataController - the tests themselves and their dependencies. These can live in <apphome>/data or <apphome>/static and be served by one of the STATIC_CONTROLLERS This avoids having to guess in CWSoftwareRootStaticController where to serve things from (some files may be installed, others are in the source tree), and should hopefully make it possible to have these tests pass when using tox, and to write qunit tests for cubes, outside of cubicweb itself. This requires modifying the tests to only declare URL paths instead of filesystem paths, and moving support files below test/data/static.
devtools/qunit.py
devtools/test/data/js_examples/dep_1.js
devtools/test/data/js_examples/deps_2.js
devtools/test/data/js_examples/test_simple_failure.js
devtools/test/data/js_examples/test_simple_success.js
devtools/test/data/js_examples/test_with_dep.js
devtools/test/data/js_examples/test_with_ordered_deps.js
devtools/test/data/js_examples/utils.js
devtools/test/data/static/js_examples/dep_1.js
devtools/test/data/static/js_examples/deps_2.js
devtools/test/data/static/js_examples/test_simple_failure.js
devtools/test/data/static/js_examples/test_simple_success.js
devtools/test/data/static/js_examples/test_with_dep.js
devtools/test/data/static/js_examples/test_with_ordered_deps.js
devtools/test/data/static/js_examples/utils.js
devtools/test/unittest_qunit.py
web/test/data/static/jstests/ajax_url0.html
web/test/data/static/jstests/ajax_url1.html
web/test/data/static/jstests/ajaxresult.json
web/test/data/static/jstests/test_ajax.html
web/test/data/static/jstests/test_ajax.js
web/test/data/static/jstests/test_htmlhelpers.html
web/test/data/static/jstests/test_htmlhelpers.js
web/test/data/static/jstests/test_utils.html
web/test/data/static/jstests/test_utils.js
web/test/data/static/jstests/utils.js
web/test/jstests/ajax_url0.html
web/test/jstests/ajax_url1.html
web/test/jstests/ajaxresult.json
web/test/jstests/test_ajax.html
web/test/jstests/test_ajax.js
web/test/jstests/test_htmlhelpers.html
web/test/jstests/test_htmlhelpers.js
web/test/jstests/test_utils.html
web/test/jstests/test_utils.js
web/test/jstests/utils.js
web/test/test_jscript.py
--- a/devtools/qunit.py	Thu Nov 26 11:23:52 2015 +0100
+++ b/devtools/qunit.py	Thu Nov 26 11:30:54 2015 +0100
@@ -89,27 +89,20 @@
         self._qunit_controller = MyQUnitResultController
         self.vreg.register(MyQUnitResultController)
         self.vreg.register(QUnitView)
-        self.vreg.register(CWSoftwareRootStaticController)
+        self.vreg.register(CWDevtoolsStaticController)
 
     def tearDown(self):
         super(QUnitTestCase, self).tearDown()
         self.vreg.unregister(self._qunit_controller)
         self.vreg.unregister(QUnitView)
-        self.vreg.unregister(CWSoftwareRootStaticController)
-
-    def abspath(self, path):
-        """use self.__module__ to build absolute path if necessary"""
-        if not osp.isabs(path):
-           dirname = osp.dirname(__import__(self.__module__).__file__)
-           return osp.abspath(osp.join(dirname,path))
-        return path
+        self.vreg.unregister(CWDevtoolsStaticController)
 
     def test_javascripts(self):
         for args in self.all_js_tests:
             self.assertIn(len(args), (1, 2))
-            test_file = self.abspath(args[0])
+            test_file = args[0]
             if len(args) > 1:
-                depends   = [self.abspath(dep) for dep in args[1]]
+                depends = args[1]
             else:
                 depends = ()
             for js_test in self._test_qunit(test_file, depends):
@@ -117,10 +110,6 @@
 
     @with_tempdir
     def _test_qunit(self, test_file, depends=(), timeout=10):
-        assert osp.exists(test_file), test_file
-        for dep in depends:
-            assert osp.exists(dep), dep
-
         QUnitView.test_file = test_file
         QUnitView.depends = depends
 
@@ -219,17 +208,17 @@
         req = self._cw
         data = {
             'jquery': req.data_url('jquery.js'),
-            'web_test': req.build_url('cwsoftwareroot/devtools/data'),
+            'devtools': req.build_url('devtools'),
         }
         w(u'''<!DOCTYPE html>
         <html>
         <head>
         <meta http-equiv="content-type" content="application/html; charset=UTF-8"/>
         <!-- JS lib used as testing framework -->
-        <link rel="stylesheet" type="text/css" media="all" href="%(web_test)s/qunit.css" />
+        <link rel="stylesheet" type="text/css" media="all" href="%(devtools)s/qunit.css" />
         <script src="%(jquery)s" type="text/javascript"></script>
-        <script src="%(web_test)s/cwmock.js" type="text/javascript"></script>
-        <script src="%(web_test)s/qunit.js" type="text/javascript"></script>'''
+        <script src="%(devtools)s/cwmock.js" type="text/javascript"></script>
+        <script src="%(devtools)s/qunit.js" type="text/javascript"></script>'''
         % data)
         w(u'<!-- result report tools -->')
         w(u'<script type="text/javascript">')
@@ -276,14 +265,11 @@
         w(u'</script>')
         w(u'<!-- Test script dependencies (tested code for example) -->')
 
-        prefix = len(cubicweb.CW_SOFTWARE_ROOT) + 1
         for dep in self.depends:
-            dep = req.build_url('cwsoftwareroot/') + dep[prefix:]
-            w(u'    <script src="%s" type="text/javascript"></script>' % dep)
+            w(u'    <script src="%s" type="text/javascript"></script>\n' % dep)
 
         w(u'    <!-- Test script itself -->')
-        test_url = req.build_url('cwsoftwareroot/') + self.test_file[prefix:]
-        w(u'    <script src="%s" type="text/javascript"></script>' % test_url)
+        w(u'    <script src="%s" type="text/javascript"></script>' % self.test_file)
         w(u'''  </head>
         <body>
         <div id="qunit-fixture"></div>
@@ -292,16 +278,16 @@
         </html>''')
 
 
-class CWSoftwareRootStaticController(StaticFileController):
-    __regid__ = 'cwsoftwareroot'
+class CWDevtoolsStaticController(StaticFileController):
+    __regid__ = 'devtools'
 
     def publish(self, rset=None):
-        staticdir = cubicweb.CW_SOFTWARE_ROOT
+        staticdir = osp.join(osp.dirname(__file__), 'data')
         relpath = self.relpath[len(self.__regid__) + 1:]
         return self.static_file(osp.join(staticdir, relpath))
 
 
-STATIC_CONTROLLERS.append(CWSoftwareRootStaticController)
+STATIC_CONTROLLERS.append(CWDevtoolsStaticController)
 
 
 if __name__ == '__main__':
--- a/devtools/test/data/js_examples/dep_1.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-a = 4;
--- a/devtools/test/data/js_examples/deps_2.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-b = a +2;
--- a/devtools/test/data/js_examples/test_simple_failure.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,18 +0,0 @@
-$(document).ready(function() {
-
-  QUnit.module("air");
-
-  QUnit.test("test 1", function (assert) {
-      assert.equal(2, 4);
-  });
-
-  QUnit.test("test 2", function (assert) {
-      assert.equal('', '45');
-      assert.equal('1024', '32');
-  });
-
-  QUnit.module("able");
-  QUnit.test("test 3", function (assert) {
-      assert.deepEqual(1, 1);
-  });
-});
--- a/devtools/test/data/js_examples/test_simple_success.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,17 +0,0 @@
-$(document).ready(function() {
-
-  QUnit.module("air");
-
-  QUnit.test("test 1", function (assert) {
-      assert.equal(2, 2);
-  });
-
-  QUnit.test("test 2", function (assert) {
-      assert.equal('45', '45');
-  });
-
-  QUnit.module("able");
-  QUnit.test("test 3", function (assert) {
-      assert.deepEqual(1, 1);
-  });
-});
--- a/devtools/test/data/js_examples/test_with_dep.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-$(document).ready(function() {
-
-  QUnit.module("air");
-
-  QUnit.test("test 1", function (assert) {
-      assert.equal(a, 4);
-  });
-
-});
--- a/devtools/test/data/js_examples/test_with_ordered_deps.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,9 +0,0 @@
-$(document).ready(function() {
-
-  QUnit.module("air");
-
-  QUnit.test("test 1", function (assert) {
-      assert.equal(b, 6);
-  });
-
-});
--- a/devtools/test/data/js_examples/utils.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-function datetuple(d) {
-    return [d.getFullYear(), d.getMonth()+1, d.getDate(), 
-	    d.getHours(), d.getMinutes()];
-}
-    
-function pprint(obj) {
-    print('{');
-    for(k in obj) {
-	print('  ' + k + ' = ' + obj[k]);
-    }
-    print('}');
-}
-
-function arrayrepr(array) {
-    return '[' + array.join(', ') + ']';
-}
-    
-function assertArrayEquals(array1, array2) {
-    if (array1.length != array2.length) {
-	throw new crosscheck.AssertionFailure(array1.join(', ') + ' != ' + array2.join(', '));
-    }
-    for (var i=0; i<array1.length; i++) {
-	if (array1[i] != array2[i]) {
-	    
-	    throw new crosscheck.AssertionFailure(arrayrepr(array1) + ' and ' + arrayrepr(array2)
-						 + ' differs at index ' + i);
-	}
-    }
-}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/static/js_examples/dep_1.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,1 @@
+a = 4;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/static/js_examples/deps_2.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,1 @@
+b = a +2;
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/static/js_examples/test_simple_failure.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,18 @@
+$(document).ready(function() {
+
+  QUnit.module("air");
+
+  QUnit.test("test 1", function (assert) {
+      assert.equal(2, 4);
+  });
+
+  QUnit.test("test 2", function (assert) {
+      assert.equal('', '45');
+      assert.equal('1024', '32');
+  });
+
+  QUnit.module("able");
+  QUnit.test("test 3", function (assert) {
+      assert.deepEqual(1, 1);
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/static/js_examples/test_simple_success.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,17 @@
+$(document).ready(function() {
+
+  QUnit.module("air");
+
+  QUnit.test("test 1", function (assert) {
+      assert.equal(2, 2);
+  });
+
+  QUnit.test("test 2", function (assert) {
+      assert.equal('45', '45');
+  });
+
+  QUnit.module("able");
+  QUnit.test("test 3", function (assert) {
+      assert.deepEqual(1, 1);
+  });
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/static/js_examples/test_with_dep.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,9 @@
+$(document).ready(function() {
+
+  QUnit.module("air");
+
+  QUnit.test("test 1", function (assert) {
+      assert.equal(a, 4);
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/static/js_examples/test_with_ordered_deps.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,9 @@
+$(document).ready(function() {
+
+  QUnit.module("air");
+
+  QUnit.test("test 1", function (assert) {
+      assert.equal(b, 6);
+  });
+
+});
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/devtools/test/data/static/js_examples/utils.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,29 @@
+function datetuple(d) {
+    return [d.getFullYear(), d.getMonth()+1, d.getDate(), 
+	    d.getHours(), d.getMinutes()];
+}
+    
+function pprint(obj) {
+    print('{');
+    for(k in obj) {
+	print('  ' + k + ' = ' + obj[k]);
+    }
+    print('}');
+}
+
+function arrayrepr(array) {
+    return '[' + array.join(', ') + ']';
+}
+    
+function assertArrayEquals(array1, array2) {
+    if (array1.length != array2.length) {
+	throw new crosscheck.AssertionFailure(array1.join(', ') + ' != ' + array2.join(', '));
+    }
+    for (var i=0; i<array1.length; i++) {
+	if (array1[i] != array2[i]) {
+	    
+	    throw new crosscheck.AssertionFailure(arrayrepr(array1) + ' and ' + arrayrepr(array2)
+						 + ' differs at index ' + i);
+	}
+    }
+}
--- a/devtools/test/unittest_qunit.py	Thu Nov 26 11:23:52 2015 +0100
+++ b/devtools/test/unittest_qunit.py	Thu Nov 26 11:30:54 2015 +0100
@@ -3,11 +3,8 @@
 from cubicweb.devtools import qunit
 
 
-JSTESTDIR = osp.abspath(osp.join(osp.dirname(__file__), 'data', 'js_examples'))
-
-
 def js(name):
-    return osp.join(JSTESTDIR, name)
+    return '/static/js_examples/' + name
 
 class QUnitTestCaseTC(qunit.QUnitTestCase):
 
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/ajax_url0.html	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,3 @@
+<div id="ajaxroot">
+  <h1>Hello</h1>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/ajax_url1.html	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,6 @@
+<div id="ajaxroot">
+  <div class="ajaxHtmlHead">
+    <cubicweb:script src="http://foo.js" type="text/javascript"> </cubicweb:script>
+  </div>
+  <h1>Hello</h1>
+</div>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/ajaxresult.json	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,1 @@
+["foo", "bar"]
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/test_ajax.html	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,26 @@
+<html>
+  <head>
+    <!-- dependencies -->
+    <script type="text/javascript">
+      var JSON_BASE_URL = '';
+    </script>
+    <script type="text/javascript" src="/data/jquery.js"></script>
+    <script src="/data/cubicweb.js" type="text/javascript"></script>
+    <script src="/data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
+    <script src="/data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="/data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="/data/cubicweb.ajax.js" type="text/javascript"></script>
+    <!-- qunit files -->
+    <script type="text/javascript" src="/devtools/qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="/devtools/qunit.css" />
+    <!-- test suite -->
+    <script src="/devtools/cwmock.js" type="text/javascript"></script>
+    <script src="test_ajax.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main"> </div>
+    <h1 id="qunit-header">cubicweb.ajax.js functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/test_ajax.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,274 @@
+$(document).ready(function() {
+
+    QUnit.module("ajax", {
+        setup: function() {
+          this.scriptsLength = $('head script[src]').length-1;
+          this.cssLength = $('head link[rel=stylesheet]').length-1;
+          // re-initialize cw loaded cache so that each tests run in a
+          // clean environment, have a lookt at _loadAjaxHtmlHead implementation
+          // in cubicweb.ajax.js for more information.
+          cw.loaded_scripts = [];
+          cw.loaded_links = [];
+        },
+        teardown: function() {
+          $('head script[src]:lt(' + ($('head script[src]').length - 1 - this.scriptsLength) + ')').remove();
+          $('head link[rel=stylesheet]:gt(' + this.cssLength + ')').remove();
+        }
+      });
+
+    function jsSources() {
+        return $.map($('head script[src]'), function(script) {
+            return script.getAttribute('src');
+        });
+    }
+
+    QUnit.test('test simple h1 inclusion (ajax_url0.html)', function (assert) {
+        assert.expect(3);
+        assert.equal($('#qunit-fixture').children().length, 0);
+        var done = assert.async();
+        $('#qunit-fixture').loadxhtml('static/jstests/ajax_url0.html')
+        .addCallback(function() {
+                try {
+                    assert.equal($('#qunit-fixture').children().length, 1);
+                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
+                } finally {
+                    done();
+                };
+            }
+        );
+    });
+
+    QUnit.test('test simple html head inclusion (ajax_url1.html)', function (assert) {
+        assert.expect(6);
+        var scriptsIncluded = jsSources();
+        assert.equal(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
+        var done = assert.async();
+        $('#qunit-fixture').loadxhtml('static/jstests/ajax_url1.html')
+        .addCallback(function() {
+                try {
+                    var origLength = scriptsIncluded.length;
+                    scriptsIncluded = jsSources();
+                    // check that foo.js has been prepended to <head>
+                    assert.equal(scriptsIncluded.length, origLength + 1);
+                    assert.equal(scriptsIncluded.indexOf('http://foo.js'), 0);
+                    // check that <div class="ajaxHtmlHead"> has been removed
+                    assert.equal($('#qunit-fixture').children().length, 1);
+                    assert.equal($('div.ajaxHtmlHead').length, 0);
+                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
+                } finally {
+                    done();
+                };
+            }
+        );
+    });
+
+    QUnit.test('test addCallback', function (assert) {
+        assert.expect(3);
+        assert.equal($('#qunit-fixture').children().length, 0);
+        var done = assert.async();
+        var d = $('#qunit-fixture').loadxhtml('static/jstests/ajax_url0.html');
+        d.addCallback(function() {
+            try {
+                assert.equal($('#qunit-fixture').children().length, 1);
+                assert.equal($('#qunit-fixture h1').html(), 'Hello');
+            } finally {
+                done();
+            };
+        });
+    });
+
+    QUnit.test('test callback after synchronous request', function (assert) {
+        assert.expect(1);
+        var deferred = new Deferred();
+        var result = jQuery.ajax({
+            url: 'static/jstests/ajax_url0.html',
+            async: false,
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+            success: function(data, status) {
+                deferred.success(data);
+            }
+        });
+        var done = assert.async();
+        deferred.addCallback(function() {
+            try {
+                // add an assertion to ensure the callback is executed
+                assert.ok(true, "callback is executed");
+            } finally {
+                done();
+            };
+        });
+    });
+
+    QUnit.test('test addCallback with parameters', function (assert) {
+        assert.expect(3);
+        assert.equal($('#qunit-fixture').children().length, 0);
+        var done = assert.async();
+        var d = $('#qunit-fixture').loadxhtml('static/jstests/ajax_url0.html');
+        d.addCallback(function(data, req, arg1, arg2) {
+            try {
+                assert.equal(arg1, 'Hello');
+                assert.equal(arg2, 'world');
+            } finally {
+                done();
+            };
+        },
+        'Hello', 'world');
+    });
+
+    QUnit.test('test callback after synchronous request with parameters', function (assert) {
+        assert.expect(3);
+        var deferred = new Deferred();
+        deferred.addCallback(function(data, req, arg1, arg2) {
+            // add an assertion to ensure the callback is executed
+            try {
+                assert.ok(true, "callback is executed");
+                assert.equal(arg1, 'Hello');
+                assert.equal(arg2, 'world');
+            } finally {
+                done();
+            };
+        },
+        'Hello', 'world');
+        deferred.addErrback(function() {
+            // throw an exception to start errback chain
+            try {
+                throw this._error;
+            } finally {
+                done();
+            };
+        });
+        var done = assert.async();
+        var result = jQuery.ajax({
+            url: 'static/jstests/ajax_url0.html',
+            async: false,
+            beforeSend: function(xhr) {
+                deferred._req = xhr;
+            },
+            success: function(data, status) {
+                deferred.success(data);
+            }
+        });
+    });
+
+  QUnit.test('test addErrback', function (assert) {
+        assert.expect(1);
+        var done = assert.async();
+        var d = $('#qunit-fixture').loadxhtml('static/jstests/nonexistent.html');
+        d.addCallback(function() {
+            // should not be executed
+            assert.ok(false, "callback is executed");
+        });
+        d.addErrback(function() {
+            try {
+                assert.ok(true, "errback is executed");
+            } finally {
+                done();
+            };
+        });
+    });
+
+    QUnit.test('test callback execution order', function (assert) {
+        assert.expect(3);
+        var counter = 0;
+        var done = assert.async();
+        var d = $('#qunit-fixture').loadxhtml('static/jstests/ajax_url0.html');
+        d.addCallback(function() {
+            assert.equal(++counter, 1); // should be executed first
+        });
+        d.addCallback(function() {
+            assert.equal(++counter, 2);
+        });
+        d.addCallback(function() {
+            try {
+                assert.equal(++counter, 3);
+            } finally {
+                done();
+            }
+        });
+    });
+
+    QUnit.test('test already included resources are ignored (ajax_url1.html)', function (assert) {
+        assert.expect(10);
+        var scriptsIncluded = jsSources();
+        // NOTE:
+        assert.equal(jQuery.inArray('http://foo.js', scriptsIncluded), -1);
+        assert.equal($('head link').length, 1);
+        /* use endswith because in pytest context we have an absolute path */
+        assert.ok($('head link').attr('href').endswith('/qunit.css'), 'qunit.css is loaded');
+        var done = assert.async();
+        $('#qunit-fixture').loadxhtml('static/jstests/ajax_url1.html')
+        .addCallback(function() {
+                var origLength = scriptsIncluded.length;
+                scriptsIncluded = jsSources();
+                try {
+                    // check that foo.js has been inserted in <head>
+                    assert.equal(scriptsIncluded.length, origLength + 1);
+                    assert.equal(scriptsIncluded.indexOf('http://foo.js'), 0);
+                    // check that <div class="ajaxHtmlHead"> has been removed
+                    assert.equal($('#qunit-fixture').children().length, 1);
+                    assert.equal($('div.ajaxHtmlHead').length, 0);
+                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
+                    // qunit.css is not added twice
+                    assert.equal($('head link').length, 1);
+                    /* use endswith because in pytest context we have an absolute path */
+                    assert.ok($('head link').attr('href').endswith('/qunit.css'), 'qunit.css is loaded');
+                } finally {
+                    done();
+                }
+            }
+        );
+    });
+
+    QUnit.test('test synchronous request loadRemote', function (assert) {
+        var res = loadRemote('static/jstests/ajaxresult.json', {},
+        'GET', true);
+        assert.deepEqual(res, ['foo', 'bar']);
+    });
+
+    QUnit.test('test event on CubicWeb', function (assert) {
+        assert.expect(1);
+        var done = assert.async();
+        var events = null;
+        $(CubicWeb).bind('server-response', function() {
+            // check that server-response event on CubicWeb is triggered
+            events = 'CubicWeb';
+        });
+        $('#qunit-fixture').loadxhtml('static/jstests/ajax_url0.html')
+        .addCallback(function() {
+                try {
+                    assert.equal(events, 'CubicWeb');
+                } finally {
+                    done();
+                };
+            }
+        );
+    });
+
+    QUnit.test('test event on node', function (assert) {
+        assert.expect(3);
+        var done = assert.async();
+        var nodes = [];
+        $('#qunit-fixture').bind('server-response', function() {
+            nodes.push('node');
+        });
+        $(CubicWeb).bind('server-response', function() {
+            nodes.push('CubicWeb');
+        });
+        $('#qunit-fixture').loadxhtml('static/jstests/ajax_url0.html')
+        .addCallback(function() {
+                try {
+                    assert.equal(nodes.length, 2);
+                    // check that server-response event on CubicWeb is triggered
+                    // only once and event server-response on node is triggered
+                    assert.equal(nodes[0], 'CubicWeb');
+                    assert.equal(nodes[1], 'node');
+                } finally {
+                    done();
+                };
+            }
+        );
+    });
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/test_htmlhelpers.html	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,22 @@
+<html>
+  <head>
+    <!-- dependencies -->
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
+    <!-- qunit files -->
+    <script type="text/javascript" src="../../../devtools/data/qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="../../../devtools/data/qunit.css" />
+    <!-- test suite -->
+    <script src="cwmock.js" type="text/javascript"></script>
+    <script src="test_htmlhelpers.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main"> </div>
+    <h1 id="qunit-header">cubicweb.htmlhelpers.js functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/test_htmlhelpers.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,36 @@
+$(document).ready(function() {
+
+    QUnit.module("module2", {
+      setup: function() {
+        $('#qunit-fixture').append('<select id="theselect" multiple="multiple" size="2">' +
+    			'</select>');
+      }
+    });
+
+    QUnit.test("test first selected", function (assert) {
+        $('#theselect').append('<option value="foo">foo</option>' +
+    			     '<option selected="selected" value="bar">bar</option>' +
+    			     '<option value="baz">baz</option>' +
+    			     '<option selected="selecetd"value="spam">spam</option>');
+        var selected = firstSelected(document.getElementById("theselect"));
+        assert.equal(selected.value, 'bar');
+    });
+
+    QUnit.test("test first selected 2", function (assert) {
+        $('#theselect').append('<option value="foo">foo</option>' +
+    			     '<option value="bar">bar</option>' +
+    			     '<option value="baz">baz</option>' +
+    			     '<option value="spam">spam</option>');
+        var selected = firstSelected(document.getElementById("theselect"));
+        assert.equal(selected, null);
+    });
+
+    QUnit.module("visibilty");
+    QUnit.test('toggleVisibility', function (assert) {
+        $('#qunit-fixture').append('<div id="foo"></div>');
+        toggleVisibility('foo');
+        assert.ok($('#foo').hasClass('hidden'), 'check hidden class is set');
+    });
+
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/test_utils.html	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,22 @@
+<html>
+  <head>
+    <!-- dependencies -->
+    <script type="text/javascript" src="utils.js"></script>
+    <script type="text/javascript" src="../../data/jquery.js"></script>
+    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.js" type="text/javascript"></script>
+    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
+    <!-- qunit files -->
+    <script type="text/javascript" src="../../../devtools/data/qunit.js"></script>
+    <link rel="stylesheet" type="text/css" media="all" href="../../../devtools/data/qunit.css" />
+    <!-- test suite -->
+    <script src="cwmock.js" type="text/javascript"></script>
+    <script src="test_utils.js" type="text/javascript"></script>
+  </head>
+  <body>
+    <div id="main"> </div>
+    <h1 id="qunit-header">cw.utils functions tests</h1>
+    <h2 id="qunit-banner"></h2>
+    <ol id="qunit-tests">
+  </body>
+</html>
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/test_utils.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,92 @@
+$(document).ready(function() {
+
+  QUnit.module("datetime");
+
+  QUnit.test("test full datetime", function (assert) {
+      assert.equal(cw.utils.toISOTimestamp(new Date(1986, 3, 18, 10, 30, 0, 0)),
+	     '1986-04-18 10:30:00');
+  });
+
+  QUnit.test("test only date", function (assert) {
+      assert.equal(cw.utils.toISOTimestamp(new Date(1986, 3, 18)), '1986-04-18 00:00:00');
+  });
+
+  QUnit.test("test null", function (assert) {
+      assert.equal(cw.utils.toISOTimestamp(null), null);
+  });
+
+  QUnit.module("parsing");
+  QUnit.test("test basic number parsing", function (assert) {
+      var d = strptime('2008/08/08', '%Y/%m/%d');
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
+      d = strptime('2008/8/8', '%Y/%m/%d');
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
+      d = strptime('8/8/8', '%Y/%m/%d');
+      assert.deepEqual(datetuple(d), [8, 8, 8, 0, 0]);
+      d = strptime('0/8/8', '%Y/%m/%d');
+      assert.deepEqual(datetuple(d), [0, 8, 8, 0, 0]);
+      d = strptime('-10/8/8', '%Y/%m/%d');
+      assert.deepEqual(datetuple(d), [-10, 8, 8, 0, 0]);
+      d = strptime('-35000', '%Y');
+      assert.deepEqual(datetuple(d), [-35000, 1, 1, 0, 0]);
+  });
+
+  QUnit.test("test custom format parsing", function (assert) {
+      var d = strptime('2008-08-08', '%Y-%m-%d');
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
+      d = strptime('2008 - !  08: 08', '%Y - !  %m: %d');
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
+      d = strptime('2008-08-08 12:14', '%Y-%m-%d %H:%M');
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 12, 14]);
+      d = strptime('2008-08-08 1:14', '%Y-%m-%d %H:%M');
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 1, 14]);
+      d = strptime('2008-08-08 01:14', '%Y-%m-%d %H:%M');
+      assert.deepEqual(datetuple(d), [2008, 8, 8, 1, 14]);
+  });
+
+  QUnit.module("sliceList");
+  QUnit.test("test slicelist", function (assert) {
+      var list = ['a', 'b', 'c', 'd', 'e', 'f'];
+      assert.deepEqual(cw.utils.sliceList(list, 2),  ['c', 'd', 'e', 'f']);
+      assert.deepEqual(cw.utils.sliceList(list, 2, -2), ['c', 'd']);
+      assert.deepEqual(cw.utils.sliceList(list, -3), ['d', 'e', 'f']);
+      assert.deepEqual(cw.utils.sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
+      assert.deepEqual(cw.utils.sliceList(list),  list);
+  });
+
+  QUnit.module("formContents", {
+    setup: function() {
+      $('#qunit-fixture').append('<form id="test-form"></form>');
+    }
+  });
+  // XXX test fckeditor
+  QUnit.test("test formContents", function (assert) {
+      $('#test-form').append('<input name="input-text" ' +
+			     'type="text" value="toto" />');
+      $('#test-form').append('<textarea rows="10" cols="30" '+
+			     'name="mytextarea">Hello World!</textarea> ');
+      $('#test-form').append('<input name="choice" type="radio" ' +
+			     'value="yes" />');
+      $('#test-form').append('<input name="choice" type="radio" ' +
+			     'value="no" checked="checked"/>');
+      $('#test-form').append('<input name="check" type="checkbox" ' +
+			     'value="yes" />');
+      $('#test-form').append('<input name="check" type="checkbox" ' +
+			     'value="no" checked="checked"/>');
+      $('#test-form').append('<select id="theselect" name="theselect" ' +
+			     'multiple="multiple" size="2"></select>');
+      $('#theselect').append('<option selected="selected" ' +
+			     'value="foo">foo</option>' +
+  			     '<option value="bar">bar</option>');
+      //Append an unchecked radio input : should not be in formContents list
+      $('#test-form').append('<input name="unchecked-choice" type="radio" ' +
+			     'value="one" />');
+      $('#test-form').append('<input name="unchecked-choice" type="radio" ' +
+			     'value="two"/>');
+      assert.deepEqual(cw.utils.formContents($('#test-form')[0]), [
+	['input-text', 'mytextarea', 'choice', 'check', 'theselect'],
+	['toto', 'Hello World!', 'no', 'no', 'foo']
+      ]);
+  });
+});
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/web/test/data/static/jstests/utils.js	Thu Nov 26 11:30:54 2015 +0100
@@ -0,0 +1,29 @@
+function datetuple(d) {
+    return [d.getFullYear(), d.getMonth()+1, d.getDate(),
+	    d.getHours(), d.getMinutes()];
+}
+
+function pprint(obj) {
+    print('{');
+    for(k in obj) {
+	print('  ' + k + ' = ' + obj[k]);
+    }
+    print('}');
+}
+
+function arrayrepr(array) {
+    return '[' + array.join(', ') + ']';
+}
+
+function assertArrayEquals(array1, array2) {
+    if (array1.length != array2.length) {
+	throw new crosscheck.AssertionFailure(array1.join(', ') + ' != ' + array2.join(', '));
+    }
+    for (var i=0; i<array1.length; i++) {
+	if (array1[i] != array2[i]) {
+
+	    throw new crosscheck.AssertionFailure(arrayrepr(array1) + ' and ' + arrayrepr(array2)
+						 + ' differs at index ' + i);
+	}
+    }
+}
--- a/web/test/jstests/ajax_url0.html	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,3 +0,0 @@
-<div id="ajaxroot">
-  <h1>Hello</h1>
-</div>
--- a/web/test/jstests/ajax_url1.html	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,6 +0,0 @@
-<div id="ajaxroot">
-  <div class="ajaxHtmlHead">
-    <cubicweb:script src="http://foo.js" type="text/javascript"> </cubicweb:script>
-  </div>
-  <h1>Hello</h1>
-</div>
--- a/web/test/jstests/ajaxresult.json	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,1 +0,0 @@
-["foo", "bar"]
--- a/web/test/jstests/test_ajax.html	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,26 +0,0 @@
-<html>
-  <head>
-    <!-- dependencies -->
-    <script type="text/javascript">
-      var JSON_BASE_URL = '';
-    </script>
-    <script type="text/javascript" src="../../data/jquery.js"></script>
-    <script src="../../data/cubicweb.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.ajax.js" type="text/javascript"></script>
-    <!-- qunit files -->
-    <script type="text/javascript" src="../../../devtools/data/qunit.js"></script>
-    <link rel="stylesheet" type="text/css" media="all" href="../../../devtools/data/qunit.css" />
-    <!-- test suite -->
-    <script src="cwmock.js" type="text/javascript"></script>
-    <script src="test_ajax.js" type="text/javascript"></script>
-  </head>
-  <body>
-    <div id="main"> </div>
-    <h1 id="qunit-header">cubicweb.ajax.js functions tests</h1>
-    <h2 id="qunit-banner"></h2>
-    <ol id="qunit-tests">
-  </body>
-</html>
--- a/web/test/jstests/test_ajax.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,274 +0,0 @@
-$(document).ready(function() {
-
-    QUnit.module("ajax", {
-        setup: function() {
-          this.scriptsLength = $('head script[src]').length-1;
-          this.cssLength = $('head link[rel=stylesheet]').length-1;
-          // re-initialize cw loaded cache so that each tests run in a
-          // clean environment, have a lookt at _loadAjaxHtmlHead implementation
-          // in cubicweb.ajax.js for more information.
-          cw.loaded_scripts = [];
-          cw.loaded_links = [];
-        },
-        teardown: function() {
-          $('head script[src]:lt(' + ($('head script[src]').length - 1 - this.scriptsLength) + ')').remove();
-          $('head link[rel=stylesheet]:gt(' + this.cssLength + ')').remove();
-        }
-      });
-
-    function jsSources() {
-        return $.map($('head script[src]'), function(script) {
-            return script.getAttribute('src');
-        });
-    }
-
-    QUnit.test('test simple h1 inclusion (ajax_url0.html)', function (assert) {
-        assert.expect(3);
-        assert.equal($('#qunit-fixture').children().length, 0);
-        var done = assert.async();
-        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
-        .addCallback(function() {
-                try {
-                    assert.equal($('#qunit-fixture').children().length, 1);
-                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
-                } finally {
-                    done();
-                };
-            }
-        );
-    });
-
-    QUnit.test('test simple html head inclusion (ajax_url1.html)', function (assert) {
-        assert.expect(6);
-        var scriptsIncluded = jsSources();
-        assert.equal(jQuery.inArray('http://foo.js', scriptsIncluded), - 1);
-        var done = assert.async();
-        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url1.html')
-        .addCallback(function() {
-                try {
-                    var origLength = scriptsIncluded.length;
-                    scriptsIncluded = jsSources();
-                    // check that foo.js has been prepended to <head>
-                    assert.equal(scriptsIncluded.length, origLength + 1);
-                    assert.equal(scriptsIncluded.indexOf('http://foo.js'), 0);
-                    // check that <div class="ajaxHtmlHead"> has been removed
-                    assert.equal($('#qunit-fixture').children().length, 1);
-                    assert.equal($('div.ajaxHtmlHead').length, 0);
-                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
-                } finally {
-                    done();
-                };
-            }
-        );
-    });
-
-    QUnit.test('test addCallback', function (assert) {
-        assert.expect(3);
-        assert.equal($('#qunit-fixture').children().length, 0);
-        var done = assert.async();
-        var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
-        d.addCallback(function() {
-            try {
-                assert.equal($('#qunit-fixture').children().length, 1);
-                assert.equal($('#qunit-fixture h1').html(), 'Hello');
-            } finally {
-                done();
-            };
-        });
-    });
-
-    QUnit.test('test callback after synchronous request', function (assert) {
-        assert.expect(1);
-        var deferred = new Deferred();
-        var result = jQuery.ajax({
-            url: BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html',
-            async: false,
-            beforeSend: function(xhr) {
-                deferred._req = xhr;
-            },
-            success: function(data, status) {
-                deferred.success(data);
-            }
-        });
-        var done = assert.async();
-        deferred.addCallback(function() {
-            try {
-                // add an assertion to ensure the callback is executed
-                assert.ok(true, "callback is executed");
-            } finally {
-                done();
-            };
-        });
-    });
-
-    QUnit.test('test addCallback with parameters', function (assert) {
-        assert.expect(3);
-        assert.equal($('#qunit-fixture').children().length, 0);
-        var done = assert.async();
-        var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
-        d.addCallback(function(data, req, arg1, arg2) {
-            try {
-                assert.equal(arg1, 'Hello');
-                assert.equal(arg2, 'world');
-            } finally {
-                done();
-            };
-        },
-        'Hello', 'world');
-    });
-
-    QUnit.test('test callback after synchronous request with parameters', function (assert) {
-        assert.expect(3);
-        var deferred = new Deferred();
-        deferred.addCallback(function(data, req, arg1, arg2) {
-            // add an assertion to ensure the callback is executed
-            try {
-                assert.ok(true, "callback is executed");
-                assert.equal(arg1, 'Hello');
-                assert.equal(arg2, 'world');
-            } finally {
-                done();
-            };
-        },
-        'Hello', 'world');
-        deferred.addErrback(function() {
-            // throw an exception to start errback chain
-            try {
-                throw this._error;
-            } finally {
-                done();
-            };
-        });
-        var done = assert.async();
-        var result = jQuery.ajax({
-            url: BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html',
-            async: false,
-            beforeSend: function(xhr) {
-                deferred._req = xhr;
-            },
-            success: function(data, status) {
-                deferred.success(data);
-            }
-        });
-    });
-
-  QUnit.test('test addErrback', function (assert) {
-        assert.expect(1);
-        var done = assert.async();
-        var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/nonexistent.html');
-        d.addCallback(function() {
-            // should not be executed
-            assert.ok(false, "callback is executed");
-        });
-        d.addErrback(function() {
-            try {
-                assert.ok(true, "errback is executed");
-            } finally {
-                done();
-            };
-        });
-    });
-
-    QUnit.test('test callback execution order', function (assert) {
-        assert.expect(3);
-        var counter = 0;
-        var done = assert.async();
-        var d = $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html');
-        d.addCallback(function() {
-            assert.equal(++counter, 1); // should be executed first
-        });
-        d.addCallback(function() {
-            assert.equal(++counter, 2);
-        });
-        d.addCallback(function() {
-            try {
-                assert.equal(++counter, 3);
-            } finally {
-                done();
-            }
-        });
-    });
-
-    QUnit.test('test already included resources are ignored (ajax_url1.html)', function (assert) {
-        assert.expect(10);
-        var scriptsIncluded = jsSources();
-        // NOTE:
-        assert.equal(jQuery.inArray('http://foo.js', scriptsIncluded), -1);
-        assert.equal($('head link').length, 1);
-        /* use endswith because in pytest context we have an absolute path */
-        assert.ok($('head link').attr('href').endswith('/qunit.css'), 'qunit.css is loaded');
-        var done = assert.async();
-        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url1.html')
-        .addCallback(function() {
-                var origLength = scriptsIncluded.length;
-                scriptsIncluded = jsSources();
-                try {
-                    // check that foo.js has been inserted in <head>
-                    assert.equal(scriptsIncluded.length, origLength + 1);
-                    assert.equal(scriptsIncluded.indexOf('http://foo.js'), 0);
-                    // check that <div class="ajaxHtmlHead"> has been removed
-                    assert.equal($('#qunit-fixture').children().length, 1);
-                    assert.equal($('div.ajaxHtmlHead').length, 0);
-                    assert.equal($('#qunit-fixture h1').html(), 'Hello');
-                    // qunit.css is not added twice
-                    assert.equal($('head link').length, 1);
-                    /* use endswith because in pytest context we have an absolute path */
-                    assert.ok($('head link').attr('href').endswith('/qunit.css'), 'qunit.css is loaded');
-                } finally {
-                    done();
-                }
-            }
-        );
-    });
-
-    QUnit.test('test synchronous request loadRemote', function (assert) {
-        var res = loadRemote(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajaxresult.json', {},
-        'GET', true);
-        assert.deepEqual(res, ['foo', 'bar']);
-    });
-
-    QUnit.test('test event on CubicWeb', function (assert) {
-        assert.expect(1);
-        var done = assert.async();
-        var events = null;
-        $(CubicWeb).bind('server-response', function() {
-            // check that server-response event on CubicWeb is triggered
-            events = 'CubicWeb';
-        });
-        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
-        .addCallback(function() {
-                try {
-                    assert.equal(events, 'CubicWeb');
-                } finally {
-                    done();
-                };
-            }
-        );
-    });
-
-    QUnit.test('test event on node', function (assert) {
-        assert.expect(3);
-        var done = assert.async();
-        var nodes = [];
-        $('#qunit-fixture').bind('server-response', function() {
-            nodes.push('node');
-        });
-        $(CubicWeb).bind('server-response', function() {
-            nodes.push('CubicWeb');
-        });
-        $('#qunit-fixture').loadxhtml(BASE_URL + 'cwsoftwareroot/web/test/jstests/ajax_url0.html')
-        .addCallback(function() {
-                try {
-                    assert.equal(nodes.length, 2);
-                    // check that server-response event on CubicWeb is triggered
-                    // only once and event server-response on node is triggered
-                    assert.equal(nodes[0], 'CubicWeb');
-                    assert.equal(nodes[1], 'node');
-                } finally {
-                    done();
-                };
-            }
-        );
-    });
-});
-
--- a/web/test/jstests/test_htmlhelpers.html	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<html>
-  <head>
-    <!-- dependencies -->
-    <script type="text/javascript" src="../../data/jquery.js"></script>
-    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.htmlhelpers.js" type="text/javascript"></script>
-    <!-- qunit files -->
-    <script type="text/javascript" src="../../../devtools/data/qunit.js"></script>
-    <link rel="stylesheet" type="text/css" media="all" href="../../../devtools/data/qunit.css" />
-    <!-- test suite -->
-    <script src="cwmock.js" type="text/javascript"></script>
-    <script src="test_htmlhelpers.js" type="text/javascript"></script>
-  </head>
-  <body>
-    <div id="main"> </div>
-    <h1 id="qunit-header">cubicweb.htmlhelpers.js functions tests</h1>
-    <h2 id="qunit-banner"></h2>
-    <ol id="qunit-tests">
-  </body>
-</html>
--- a/web/test/jstests/test_htmlhelpers.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,36 +0,0 @@
-$(document).ready(function() {
-
-    QUnit.module("module2", {
-      setup: function() {
-        $('#qunit-fixture').append('<select id="theselect" multiple="multiple" size="2">' +
-    			'</select>');
-      }
-    });
-
-    QUnit.test("test first selected", function (assert) {
-        $('#theselect').append('<option value="foo">foo</option>' +
-    			     '<option selected="selected" value="bar">bar</option>' +
-    			     '<option value="baz">baz</option>' +
-    			     '<option selected="selecetd"value="spam">spam</option>');
-        var selected = firstSelected(document.getElementById("theselect"));
-        assert.equal(selected.value, 'bar');
-    });
-
-    QUnit.test("test first selected 2", function (assert) {
-        $('#theselect').append('<option value="foo">foo</option>' +
-    			     '<option value="bar">bar</option>' +
-    			     '<option value="baz">baz</option>' +
-    			     '<option value="spam">spam</option>');
-        var selected = firstSelected(document.getElementById("theselect"));
-        assert.equal(selected, null);
-    });
-
-    QUnit.module("visibilty");
-    QUnit.test('toggleVisibility', function (assert) {
-        $('#qunit-fixture').append('<div id="foo"></div>');
-        toggleVisibility('foo');
-        assert.ok($('#foo').hasClass('hidden'), 'check hidden class is set');
-    });
-
-});
-
--- a/web/test/jstests/test_utils.html	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,22 +0,0 @@
-<html>
-  <head>
-    <!-- dependencies -->
-    <script type="text/javascript" src="utils.js"></script>
-    <script type="text/javascript" src="../../data/jquery.js"></script>
-    <script src="../../data/cubicweb.python.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.js" type="text/javascript"></script>
-    <script src="../../data/cubicweb.compat.js" type="text/javascript"></script>
-    <!-- qunit files -->
-    <script type="text/javascript" src="../../../devtools/data/qunit.js"></script>
-    <link rel="stylesheet" type="text/css" media="all" href="../../../devtools/data/qunit.css" />
-    <!-- test suite -->
-    <script src="cwmock.js" type="text/javascript"></script>
-    <script src="test_utils.js" type="text/javascript"></script>
-  </head>
-  <body>
-    <div id="main"> </div>
-    <h1 id="qunit-header">cw.utils functions tests</h1>
-    <h2 id="qunit-banner"></h2>
-    <ol id="qunit-tests">
-  </body>
-</html>
--- a/web/test/jstests/test_utils.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,92 +0,0 @@
-$(document).ready(function() {
-
-  QUnit.module("datetime");
-
-  QUnit.test("test full datetime", function (assert) {
-      assert.equal(cw.utils.toISOTimestamp(new Date(1986, 3, 18, 10, 30, 0, 0)),
-	     '1986-04-18 10:30:00');
-  });
-
-  QUnit.test("test only date", function (assert) {
-      assert.equal(cw.utils.toISOTimestamp(new Date(1986, 3, 18)), '1986-04-18 00:00:00');
-  });
-
-  QUnit.test("test null", function (assert) {
-      assert.equal(cw.utils.toISOTimestamp(null), null);
-  });
-
-  QUnit.module("parsing");
-  QUnit.test("test basic number parsing", function (assert) {
-      var d = strptime('2008/08/08', '%Y/%m/%d');
-      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
-      d = strptime('2008/8/8', '%Y/%m/%d');
-      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
-      d = strptime('8/8/8', '%Y/%m/%d');
-      assert.deepEqual(datetuple(d), [8, 8, 8, 0, 0]);
-      d = strptime('0/8/8', '%Y/%m/%d');
-      assert.deepEqual(datetuple(d), [0, 8, 8, 0, 0]);
-      d = strptime('-10/8/8', '%Y/%m/%d');
-      assert.deepEqual(datetuple(d), [-10, 8, 8, 0, 0]);
-      d = strptime('-35000', '%Y');
-      assert.deepEqual(datetuple(d), [-35000, 1, 1, 0, 0]);
-  });
-
-  QUnit.test("test custom format parsing", function (assert) {
-      var d = strptime('2008-08-08', '%Y-%m-%d');
-      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
-      d = strptime('2008 - !  08: 08', '%Y - !  %m: %d');
-      assert.deepEqual(datetuple(d), [2008, 8, 8, 0, 0]);
-      d = strptime('2008-08-08 12:14', '%Y-%m-%d %H:%M');
-      assert.deepEqual(datetuple(d), [2008, 8, 8, 12, 14]);
-      d = strptime('2008-08-08 1:14', '%Y-%m-%d %H:%M');
-      assert.deepEqual(datetuple(d), [2008, 8, 8, 1, 14]);
-      d = strptime('2008-08-08 01:14', '%Y-%m-%d %H:%M');
-      assert.deepEqual(datetuple(d), [2008, 8, 8, 1, 14]);
-  });
-
-  QUnit.module("sliceList");
-  QUnit.test("test slicelist", function (assert) {
-      var list = ['a', 'b', 'c', 'd', 'e', 'f'];
-      assert.deepEqual(cw.utils.sliceList(list, 2),  ['c', 'd', 'e', 'f']);
-      assert.deepEqual(cw.utils.sliceList(list, 2, -2), ['c', 'd']);
-      assert.deepEqual(cw.utils.sliceList(list, -3), ['d', 'e', 'f']);
-      assert.deepEqual(cw.utils.sliceList(list, 0, -2), ['a', 'b', 'c', 'd']);
-      assert.deepEqual(cw.utils.sliceList(list),  list);
-  });
-
-  QUnit.module("formContents", {
-    setup: function() {
-      $('#qunit-fixture').append('<form id="test-form"></form>');
-    }
-  });
-  // XXX test fckeditor
-  QUnit.test("test formContents", function (assert) {
-      $('#test-form').append('<input name="input-text" ' +
-			     'type="text" value="toto" />');
-      $('#test-form').append('<textarea rows="10" cols="30" '+
-			     'name="mytextarea">Hello World!</textarea> ');
-      $('#test-form').append('<input name="choice" type="radio" ' +
-			     'value="yes" />');
-      $('#test-form').append('<input name="choice" type="radio" ' +
-			     'value="no" checked="checked"/>');
-      $('#test-form').append('<input name="check" type="checkbox" ' +
-			     'value="yes" />');
-      $('#test-form').append('<input name="check" type="checkbox" ' +
-			     'value="no" checked="checked"/>');
-      $('#test-form').append('<select id="theselect" name="theselect" ' +
-			     'multiple="multiple" size="2"></select>');
-      $('#theselect').append('<option selected="selected" ' +
-			     'value="foo">foo</option>' +
-  			     '<option value="bar">bar</option>');
-      //Append an unchecked radio input : should not be in formContents list
-      $('#test-form').append('<input name="unchecked-choice" type="radio" ' +
-			     'value="one" />');
-      $('#test-form').append('<input name="unchecked-choice" type="radio" ' +
-			     'value="two"/>');
-      assert.deepEqual(cw.utils.formContents($('#test-form')[0]), [
-	['input-text', 'mytextarea', 'choice', 'check', 'theselect'],
-	['toto', 'Hello World!', 'no', 'no', 'foo']
-      ]);
-  });
-});
-
--- a/web/test/jstests/utils.js	Thu Nov 26 11:23:52 2015 +0100
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,29 +0,0 @@
-function datetuple(d) {
-    return [d.getFullYear(), d.getMonth()+1, d.getDate(),
-	    d.getHours(), d.getMinutes()];
-}
-
-function pprint(obj) {
-    print('{');
-    for(k in obj) {
-	print('  ' + k + ' = ' + obj[k]);
-    }
-    print('}');
-}
-
-function arrayrepr(array) {
-    return '[' + array.join(', ') + ']';
-}
-
-function assertArrayEquals(array1, array2) {
-    if (array1.length != array2.length) {
-	throw new crosscheck.AssertionFailure(array1.join(', ') + ' != ' + array2.join(', '));
-    }
-    for (var i=0; i<array1.length; i++) {
-	if (array1[i] != array2[i]) {
-
-	    throw new crosscheck.AssertionFailure(arrayrepr(array1) + ' and ' + arrayrepr(array2)
-						 + ' differs at index ' + i);
-	}
-    }
-}
--- a/web/test/test_jscript.py	Thu Nov 26 11:23:52 2015 +0100
+++ b/web/test/test_jscript.py	Thu Nov 26 11:30:54 2015 +0100
@@ -6,28 +6,28 @@
 class JScript(qunit.QUnitTestCase):
 
     all_js_tests = (
-        ("jstests/test_utils.js", (
-            "../../web/data/cubicweb.js",
-            "../../web/data/cubicweb.compat.js",
-            "../../web/data/cubicweb.python.js",
-            "jstests/utils.js",
+        ("/static/jstests/test_utils.js", (
+            "/data/cubicweb.js",
+            "/data/cubicweb.compat.js",
+            "/data/cubicweb.python.js",
+            "/static/jstests/utils.js",
             ),
          ),
 
-        ("jstests/test_htmlhelpers.js", (
-            "../../web/data/cubicweb.js",
-            "../../web/data/cubicweb.compat.js",
-            "../../web/data/cubicweb.python.js",
-            "../../web/data/cubicweb.htmlhelpers.js",
+        ("/static/jstests/test_htmlhelpers.js", (
+            "/data/cubicweb.js",
+            "/data/cubicweb.compat.js",
+            "/data/cubicweb.python.js",
+            "/data/cubicweb.htmlhelpers.js",
             ),
          ),
 
-        ("jstests/test_ajax.js", (
-            "../../web/data/cubicweb.python.js",
-            "../../web/data/cubicweb.js",
-            "../../web/data/cubicweb.compat.js",
-            "../../web/data/cubicweb.htmlhelpers.js",
-            "../../web/data/cubicweb.ajax.js",
+        ("/static/jstests/test_ajax.js", (
+            "/data/cubicweb.python.js",
+            "/data/cubicweb.js",
+            "/data/cubicweb.compat.js",
+            "/data/cubicweb.htmlhelpers.js",
+            "/data/cubicweb.ajax.js",
             ),
          ),
     )