summaryrefslogtreecommitdiff
path: root/addons/web
diff options
context:
space:
mode:
authorXavier Morel <xmo@openerp.com>2012-10-25 17:47:45 +0200
committerXavier Morel <xmo@openerp.com>2012-10-25 17:47:45 +0200
commit99a2dd3938c7d7d4b7e9c724ec8acbb5eeedf35f (patch)
treeaca872a490542cb318626f806e8a3d3d2db0c1fd /addons/web
parent1cf9b2a60da502de27aa67e8318f41ad6692e5a9 (diff)
[ADD] first 90% of non-sucky testing API, last 90% left
also all existing tests shoud now be completely broken and need to be fixed bzr revid: xmo@openerp.com-20121025154745-rw2gktfd6tp68k2m
Diffstat (limited to 'addons/web')
-rw-r--r--addons/web/__openerp__.py13
-rw-r--r--addons/web/controllers/__init__.py1
-rw-r--r--addons/web/controllers/main.py8
-rw-r--r--addons/web/controllers/testing.py155
-rw-r--r--addons/web/doc/images/runner.pngbin0 -> 3963 bytes
-rw-r--r--addons/web/doc/images/runner2.pngbin0 -> 6807 bytes
-rw-r--r--addons/web/doc/images/tests.pngbin0 -> 65388 bytes
-rw-r--r--addons/web/doc/images/tests2.pngbin0 -> 37769 bytes
-rw-r--r--addons/web/doc/images/tests3.pngbin0 -> 37827 bytes
-rw-r--r--addons/web/doc/index.rst2
-rw-r--r--addons/web/doc/testing.rst479
-rw-r--r--addons/web/http.py2
-rw-r--r--addons/web/static/src/js/chrome.js4
-rw-r--r--addons/web/static/src/js/testing.js179
-rw-r--r--addons/web/static/src/js/views.js4
-rw-r--r--addons/web/static/test/test.html62
-rw-r--r--addons/web/static/test/testing.js97
17 files changed, 836 insertions, 170 deletions
diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py
index fcfdca60a2a..7bdfa8b55dd 100644
--- a/addons/web/__openerp__.py
+++ b/addons/web/__openerp__.py
@@ -40,6 +40,7 @@ This module provides the core of the OpenERP Web Client.
"static/lib/cleditor/jquery.cleditor.js",
"static/lib/py.js/lib/py.js",
"static/src/js/boot.js",
+ "static/src/js/testing.js",
"static/src/js/corelib.js",
"static/src/js/coresetup.js",
"static/src/js/dates.js",
@@ -67,4 +68,16 @@ This module provides the core of the OpenERP Web Client.
'qweb' : [
"static/src/xml/*.xml",
],
+ 'test': [
+ "static/test/class.js",
+ "static/test/registry.js",
+ "static/test/form.js",
+ "static/test/list-utils.js",
+ "static/test/formats.js",
+ "static/test/rpc.js",
+ "static/test/evals.js",
+ "static/test/search.js",
+ "static/test/Widget.js",
+ "static/test/list-editable.js"
+ ],
}
diff --git a/addons/web/controllers/__init__.py b/addons/web/controllers/__init__.py
index 12a7e529b67..74c27518ece 100644
--- a/addons/web/controllers/__init__.py
+++ b/addons/web/controllers/__init__.py
@@ -1 +1,2 @@
from . import main
+from . import testing
diff --git a/addons/web/controllers/main.py b/addons/web/controllers/main.py
index 72ffd9567f6..114a896e77a 100644
--- a/addons/web/controllers/main.py
+++ b/addons/web/controllers/main.py
@@ -800,8 +800,7 @@ class Database(openerpweb.Controller):
@openerpweb.jsonrequest
def get_list(self, req):
- dbs = db_list(req)
- return {"db_list": dbs}
+ return db_list(req)
@openerpweb.jsonrequest
def create(self, req, fields):
@@ -922,10 +921,7 @@ class Session(openerpweb.Controller):
@openerpweb.jsonrequest
def get_lang_list(self, req):
try:
- return {
- 'lang_list': (req.session.proxy("db").list_lang() or []),
- 'error': ""
- }
+ return req.session.proxy("db").list_lang() or []
except Exception, e:
return {"error": e, "title": "Languages"}
diff --git a/addons/web/controllers/testing.py b/addons/web/controllers/testing.py
new file mode 100644
index 00000000000..0cfc633a492
--- /dev/null
+++ b/addons/web/controllers/testing.py
@@ -0,0 +1,155 @@
+# coding=utf-8
+# -*- encoding: utf-8 -*-
+
+import glob
+import itertools
+import json
+import operator
+import os
+
+from mako.template import Template
+from openerp.modules import module
+
+from .main import module_topological_sort
+from ..http import Controller, httprequest
+
+NOMODULE_TEMPLATE = Template(u"""<!DOCTYPE html>
+<html>
+ <head>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>OpenERP Testing</title>
+ </head>
+ <body>
+ <form action="/web/tests" method="GET">
+ <button name="mod" value="*">Run all tests</button>
+ <ul>
+ % for name, module in modules:
+ <li>${name} <button name="mod" value="${module}">
+ Run Tests</button></li>
+ % endfor
+ </ul>
+ </form>
+ </body>
+</html>
+""")
+NOTFOUND = Template(u"""
+<p>Unable to find the module [${module}], please check that the module
+ name is correct and the module is on OpenERP's path.</p>
+<a href="/web/tests">&lt;&lt; Back to tests</a>
+""")
+TESTING = Template(u"""<!DOCTYPE html>
+<html style="height: 100%">
+<%def name="to_path(module, p)">/${module}/${p}</%def>
+<head>
+ <meta http-equiv="X-UA-Compatible" content="IE=edge"/>
+ <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+ <title>OpenERP Web Tests</title>
+ <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
+
+ <link rel="stylesheet" href="/web/static/lib/qunit/qunit.css">
+ <script src="/web/static/lib/qunit/qunit.js"></script>
+
+ <script type="text/javascript">
+ // List of modules, each module is preceded by its dependencies
+ var oe_all_dependencies = ${dependencies};
+ QUnit.config.testTimeout = 2000;
+ </script>
+</head>
+<body id="oe" class="openerp">
+ <div id="qunit"></div>
+ <div id="qunit-fixture"></div>
+</body>
+% for module, jss, tests, templates in files:
+ % for js in jss:
+ <script src="${to_path(module, js)}"></script>
+ % endfor
+ % if tests or templates:
+ <script>
+ openerp.testing.current_module = "${module}";
+ % for template in templates:
+ openerp.testing.add_template("${to_path(module, template)}");
+ % endfor
+ </script>
+ % endif
+ % if tests:
+ % for test in tests:
+ <script type="text/javascript" src="${to_path(module, test)}"></script>
+ % endfor
+ % endif
+% endfor
+</html>
+""")
+
+class TestRunnerController(Controller):
+ _cp_path = '/web/tests'
+
+ @httprequest
+ def index(self, req, mod=None, **kwargs):
+ ms = module.get_modules()
+ manifests = dict(
+ (name, desc)
+ for name, desc in zip(ms, map(self.load_manifest, ms))
+ if desc # remove not-actually-openerp-modules
+ )
+
+ if not mod:
+ return NOMODULE_TEMPLATE.render(modules=(
+ (manifest['name'], name)
+ for name, manifest in manifests.iteritems()
+ if any(testfile.endswith('.js')
+ for testfile in manifest['test'])
+ ))
+ sorted_mods = module_topological_sort(dict(
+ (name, manifest.get('depends', []))
+ for name, manifest in manifests.iteritems()
+ ))
+ # to_load and to_test should be zippable lists of the same length.
+ # A falsy value in to_test indicate nothing to test at that index (just
+ # load the corresponding part of to_load)
+ to_test = sorted_mods
+ if mod != '*':
+ if mod not in manifests:
+ return req.not_found(NOTFOUND.render(module=mod))
+ idx = sorted_mods.index(mod)
+ to_test = [None] * len(sorted_mods)
+ to_test[idx] = mod
+
+ tests_candicates = [
+ filter(lambda path: path.endswith('.js'),
+ manifests[mod]['test'] if mod else [])
+ for mod in to_test]
+ # remove trailing test-less modules
+ tests = reversed(list(
+ itertools.dropwhile(
+ operator.not_,
+ reversed(tests_candicates))))
+
+ files = [
+ (mod, manifests[mod]['js'], tests, manifests[mod]['qweb'])
+ for mod, tests in itertools.izip(sorted_mods, tests)
+ ]
+
+ return TESTING.render(files=files, dependencies=json.dumps(
+ [name for name in sorted_mods
+ if module.get_module_resource(name, 'static')
+ if manifests[name]['js']]))
+
+ def load_manifest(self, name):
+ manifest = module.load_information_from_description_file(name)
+ if manifest:
+ path = module.get_module_path(name)
+ manifest['js'] = list(
+ self.expand_patterns(path, manifest.get('js', [])))
+ manifest['test'] = list(
+ self.expand_patterns(path, manifest.get('test', [])))
+ manifest['qweb'] = list(
+ self.expand_patterns(path, manifest.get('qweb', [])))
+ return manifest
+
+ def expand_patterns(self, root, patterns):
+ for pattern in patterns:
+ normalized_pattern = os.path.normpath(os.path.join(root, pattern))
+ for path in glob.glob(normalized_pattern):
+ # replace OS path separators (from join & normpath) by URI ones
+ yield path[len(root):].replace(os.path.sep, '/')
diff --git a/addons/web/doc/images/runner.png b/addons/web/doc/images/runner.png
new file mode 100644
index 00000000000..bd48e9d2922
--- /dev/null
+++ b/addons/web/doc/images/runner.png
Binary files differ
diff --git a/addons/web/doc/images/runner2.png b/addons/web/doc/images/runner2.png
new file mode 100644
index 00000000000..38ea2949cfc
--- /dev/null
+++ b/addons/web/doc/images/runner2.png
Binary files differ
diff --git a/addons/web/doc/images/tests.png b/addons/web/doc/images/tests.png
new file mode 100644
index 00000000000..84083d9e5ed
--- /dev/null
+++ b/addons/web/doc/images/tests.png
Binary files differ
diff --git a/addons/web/doc/images/tests2.png b/addons/web/doc/images/tests2.png
new file mode 100644
index 00000000000..59835b257b5
--- /dev/null
+++ b/addons/web/doc/images/tests2.png
Binary files differ
diff --git a/addons/web/doc/images/tests3.png b/addons/web/doc/images/tests3.png
new file mode 100644
index 00000000000..10b45eddedc
--- /dev/null
+++ b/addons/web/doc/images/tests3.png
Binary files differ
diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst
index 6a0d8dee2d4..a0c66be12fd 100644
--- a/addons/web/doc/index.rst
+++ b/addons/web/doc/index.rst
@@ -24,6 +24,8 @@ Contents:
guides/client-action
+ testing
+
Indices and tables
==================
diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst
new file mode 100644
index 00000000000..34826201dc9
--- /dev/null
+++ b/addons/web/doc/testing.rst
@@ -0,0 +1,479 @@
+.. highlight:: javascript
+
+Testing in OpenERP Web
+======================
+
+Javascript Unit Testing
+-----------------------
+
+OpenERP Web 7.0 includes means to unit-test both the core code of
+OpenERP Web and your own javascript modules. On the javascript side,
+unit-testing is based on QUnit_ with a number of helpers and
+extensions for better integration with OpenERP.
+
+To see what the runner looks like, find (or start) an OpenERP server
+with the web client enabled, and navigate to ``/web/tests`` e.g. `on
+OpenERP's CI <http://trunk.runbot.openerp.com/web/tests>`_. This will
+show the runner selector, which lists all modules with javascript unit
+tests, and allows starting any of them (or all javascript tests in all
+modules at once).
+
+.. image:: ./images/runner.png
+ :align: center
+
+Clicking any runner button will launch the corresponding tests in the
+bundled QUnit_ runner:
+
+.. image:: ./images/tests.png
+ :align: center
+
+Writing a test case
+-------------------
+
+The first step is to list the test file(s). This is done through the
+``test`` key of the openerp manifest, by adding javascript files to it
+(next to the usual YAML files, if any):
+
+.. code-block:: python
+
+ {
+ 'name': "Demonstration of web/javascript tests",
+ 'category': 'Hidden',
+ 'depends': ['web'],
+ 'test': ['static/test/demo.js'],
+ }
+
+and to create the corresponding test file(s)
+
+.. note::
+
+ test files which do not exist will be ignored, if all test files
+ of a module are ignored (can not be found), the test runner will
+ consider that the module has no javascript tests
+
+After that, refreshing the runner selector will display the new module
+and allow running all of its (0 so far) tests:
+
+.. image:: ./images/runner2.png
+ :align: center
+
+The next step is to create a test case::
+
+ openerp.testing.section('basic section', function (test) {
+ test('my first test', function () {
+ ok(false, "this test has run");
+ });
+ });
+
+All testing helpers and structures live in the ``openerp.testing``
+module. OpenERP tests live in a :js:func:`~openerp.testing.section`,
+which is itself part of a module. The first argument to a section is
+the name of the section, the second one is the section body.
+
+:js:func:`~openerp.testing.test`, provided by the
+:js:func:`~openerp.testing.section` to the callback, is used to
+register a given test case which will be run whenever the test runner
+actually does its job. OpenERP Web test case use standard `QUnit
+assertions`_ within them.
+
+Launching the test runner at this point will run the test and display
+the corresponding assertion message, with red colors indicating the
+test failed:
+
+.. image:: ./images/tests2.png
+ :align: center
+
+Fixing the test (by replacing ``false`` to ``true`` in the assertion)
+will make it pass:
+
+.. image:: ./images/tests3.png
+ :align: center
+
+Assertions
+----------
+
+As noted above, OpenERP Web's tests use `qunit assertions`_. They are
+available globally (so they can just be called without references to
+anything). The following list is available:
+
+.. js:function:: ok(state[, message])
+
+ checks that ``state`` is truthy (in the javascript sense)
+
+.. js:function:: strictEqual(actual, expected[, message])
+
+ checks that the actual (produced by a method being tested) and
+ expected values are identical (roughly equivalent to ``ok(actual
+ === expected, message)``)
+
+.. js:function:: notStrictEqual(actual, expected[, message])
+
+ checks that the actual and expected values are *not* identical
+ (roughly equivalent to ``ok(actual !== expected, message)``)
+
+.. js:function:: deepEqual(actual, expected[, message])
+
+ deep comparison between actual and expected: recurse into
+ containers (objects and arrays) to ensure that they have the same
+ keys/number of elements, and the values match.
+
+.. js:function:: notDeepEqual(actual, expected[, message])
+
+ inverse operation to :js:func:`deepEqual`
+
+.. js:function:: throws(block[, expected][, message])
+
+ checks that, when called, the ``block`` throws an
+ error. Optionally validates that error against ``expected``.
+
+ :param Function block:
+ :param expected: if a regexp, checks that the thrown error's
+ message matches the regular expression. If an
+ error type, checks that the thrown error is of
+ that type.
+ :type expected: Error | RegExp
+
+.. js:function:: equal(actual, expected[, message])
+
+ checks that ``actual`` and ``expected`` are loosely equal, using
+ the ``==`` operator and its coercion rules.
+
+.. js:function:: notEqual(actual, expected[, message])
+
+ inverse operation to :js:func:`equal`
+
+Getting an OpenERP instance
+---------------------------
+
+The OpenERP instance is the base through which most OpenERP Web
+modules behaviors (functions, objects, …) are accessed. As a result,
+the test framework automatically builds one, and loads the module
+being tested and all of its dependencies inside it. This new instance
+is provided as the first positional parameter to your test
+cases. Let's observe by adding javascript code (not test code) to the
+test module:
+
+.. code-block:: python
+
+ {
+ 'name': "Demonstration of web/javascript tests",
+ 'category': 'Hidden',
+ 'depends': ['web'],
+ 'js': ['static/src/js/demo.js'],
+ 'test': ['static/test/demo.js'],
+ }
+
+::
+
+ // src/js/demo.js
+ openerp.web_tests_demo = function (instance) {
+ instance.web_tests_demo = {
+ value_true: true,
+ SomeType: instance.web.Class.extend({
+ init: function (value) {
+ this.value = value;
+ }
+ })
+ };
+ };
+
+and then adding a new test case, which simply checks that the
+``instance`` contains all the expected stuff we created in the
+module::
+
+ // test/demo.js
+ test('module content', function (instance) {
+ ok(instance.web_tests_demo.value_true, "should have a true value");
+ var type_instance = new instance.web_tests_demo.SomeType(42);
+ strictEqual(type_instance.value, 42, "should have provided value");
+ });
+
+DOM Scratchpad
+--------------
+
+As in the wider client, arbitrarily accessing document content is
+strongly discouraged during tests. But DOM access is still needed to
+e.g. fully initialize :js:class:`widgets <~openerp.web.Widget>` before
+testing them.
+
+Thus, test cases get a DOM scratchpad as its second positional
+parameter, in a jQuery instance. That scratchpad is fully cleaned up
+before each test, and as long as it doesn't do anything outside the
+scrartchpad your code can do whatever it wants::
+
+ // test/demo.js
+ test('DOM content', function (instance, $scratchpad) {
+ $scratchpad.html('<div><span class="foo bar">ok</span></div>');
+ ok($scratchpad.find('span').hasClass('foo'),
+ "should have provided class");
+ });
+ test('clean scratchpad', function (instance, $scratchpad) {
+ ok(!$scratchpad.children().length, "should have no content");
+ ok(!$scratchpad.text(), "should have no text");
+ });
+
+.. note::
+
+ the top-level element of the scratchpad is not cleaned up, test
+ cases can add text or DOM children but shoud not alter
+ ``$scratchpad`` itself.
+
+Loading templates
+-----------------
+
+To avoid the corresponding processing costs, by default templates are
+not loaded into QWeb. If you need to render e.g. widgets making use of
+QWeb templates, you can request their loading through the
+:js:attr:`~TestOptions.templates` option.
+
+This will automatically load all relevant templates in the instance's
+qweb before running the test case:
+
+.. code-block:: python
+
+ {
+ 'name': "Demonstration of web/javascript tests",
+ 'category': 'Hidden',
+ 'depends': ['web'],
+ 'js': ['static/src/js/demo.js'],
+ 'test': ['static/test/demo.js'],
+ 'qweb': ['static/src/xml/demo.xml'],
+ }
+
+.. code-block:: xml
+
+ <!-- src/xml/demo.xml -->
+ <templates id="template" xml:space="preserve">
+ <t t-name="DemoTemplate">
+ <t t-foreach="5" t-as="value">
+ <p><t t-esc="value"/></p>
+ </t>
+ </t>
+ </templates>
+
+::
+
+ // test/demo.js
+ test('templates', {templates: true}, function (instance) {
+ var s = instance.web.qweb.render('DemoTemplate');
+ var texts = $(s).find('p').map(function () {
+ return $(this).text();
+ }).get();
+
+ deepEqual(texts, ['0', '1', '2', '3', '4']);
+ });
+
+Asynchronous cases
+------------------
+
+The test case examples so far are all synchronous, they execute from
+the first to the last line and once the last line has executed the
+test is done. But the web client is full of :doc:`asynchronous code
+</async>`, and thus test cases need to be async-aware.
+
+This is done by returning a :js:class:`deferred <Deferred>` from the
+case callback::
+
+ // test/demo.js
+ test('asynchronous', {
+ asserts: 1
+ }, function () {
+ var d = $.Deferred();
+ setTimeout(function () {
+ ok(true);
+ d.resolve();
+ }, 100);
+ return d;
+ });
+
+This example also introduces an options object to the test case. In
+this case, it's used to specify the number of assertions the test case
+should expect, if less or more assertions are specified the case will
+count as failed.
+
+Asynchronous test cases *must* specify the number of assertions they
+will run. This allows more easily catching situations where e.g. the
+test architecture was not warned about asynchronous operations.
+
+.. note::
+
+ asynchronous test cases also have a 2 seconds timeout: if the test
+ does not finish within 2 seconds, it will be considered
+ failed. This pretty much always means the test will not resolve.
+
+.. note::
+
+ if the returned deferred is rejected, the test will be failed
+ unless :js:attr:`~TestOptions.fail_on_rejection` is set to
+ ``false``.
+
+RPC
+---
+
+An important subset of asynchronous test cases is test cases which
+need to perform (and chain, to an extent) RPC calls.
+
+.. note::
+
+ because they are a subset of asynchronous cases, RPC cases must
+ also provide a valid :js:attr:`assertions count
+ <TestOptions.asserts>`
+
+By default, test cases will fail when trying to perform an RPC
+call. The ability to perform RPC calls must be explicitly requested by
+a test case (or its containing test suite) through
+:js:attr:`~TestOptions.rpc`, and can be one of two modes: ``mock`` or
+``rpc``.
+
+Mock RPC
+++++++++
+
+The preferred (and most fastest from a setup and execution time point
+of view) way to do RPC during tests is to mock the RPC calls: while
+setting up the test case, provide what the RPC responses "should" be,
+and only test the code between the "user" (the test itself) and the
+RPC call, before the call is effectively done.
+
+To do this, set the :js:attr:`rpc option <~TestOptions.rpc>` to
+``mock``. This will add a third parameter to the test case callback:
+
+.. js:function:: mock(rpc_spec, handler)
+
+ Can be used in two different ways depending on the shape of the
+ first parameter:
+
+ * If it matches the pattern ``model:method`` (if it contains a
+ colon, essentially) the call will set up the mocking of an RPC
+ call straight to the OpenERP server (through XMLRPC) as
+ performed via e.g. :js:func:`openerp.web.Model.call`.
+
+ In that case, ``handler`` should be a function taking two
+ arguments ``args`` and ``kwargs``, matching the corresponding
+ arguments on the server side. Hander should simply return the
+ value as if it were returned by the Python XMLRPC handler::
+
+ test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) {
+ // set up mocking
+ mock('people.famous:name_search', function (args, kwargs) {
+ strictEqual(kwargs.name, 'bob');
+ return [
+ [1, "Microsoft Bob"],
+ [2, "Bob the Builder"],
+ [3, "Silent Bob"]
+ ];
+ });
+
+ // actual test code
+ return new instance.web.Model('people.famous')
+ .call('name_search', {name: 'bob'}).pipe(function (result) {
+ strictEqual(result.length, 3, "shoud return 3 people");
+ strictEqual(result[0][1], "Microsoft Bob",
+ "the most famous bob should be Microsoft Bob");
+ });
+ });
+
+ * Otherwise, if it matches an absolute path (e.g. ``/a/b/c``) it
+ will mock a JSON-RPC call to a web client controller, such as
+ ``/web/webclient/translations``. In that case, the handler takes
+ a single ``params`` argument holding all of the parameters
+ provided over JSON-RPC.
+
+ As previously, the handler should simply return the result value
+ as if returned by the original JSON-RPC handler::
+
+ test('JSON-RPC', {rpc: 'mock', asserts: 3, templates: true}, function (instance, $s, mock) {
+ var fetched_dbs = false, fetched_langs = false;
+ mock('/web/database/get_list', function () {
+ fetched_dbs = true;
+ return ['foo', 'bar', 'baz'];
+ });
+ mock('/web/session/get_lang_list', function () {
+ fetched_langs = true;
+ return [['vo_IS', 'Hopelandic / Vonlenska']];
+ });
+
+ // widget needs that or it blows up
+ instance.webclient = {toggle_bars: openerp.testing.noop};
+ var dbm = new instance.web.DatabaseManager({});
+ return dbm.appendTo($s).pipe(function () {
+ ok(fetched_dbs, "should have fetched databases");
+ ok(fetched_langs, "should have fetched languages");
+ deepEqual(dbm.db_list, ['foo', 'bar', 'baz']);
+ });
+ });
+
+.. note::
+
+ mock handlers can contain assertions, these assertions should be
+ part of the assertions count (and if multiple calls are made to a
+ handler containing assertions, it multiplies the effective number
+ of assertions)
+
+Actual RPC
+++++++++++
+
+.. TODO:: rpc to database (implement & document)
+
+Testing API
+-----------
+
+.. todo:: implement options on sections
+
+.. js:class:: TestOptions
+
+ the various options which can be passed to
+ :js:func:`~openerp.testing.section` or
+ :js:func:`~openerp.testing.case`
+
+ .. js:attribute:: TestOptions.asserts
+
+ An integer, the number of assertions which should run during a
+ normal execution of the test. Mandatory for asynchronous tests.
+
+ .. js:attribute:: TestOptions.setup
+
+ .. todo:: implement & document setup (async?)
+
+ .. js:attribute:: TestOptions.teardown
+
+ .. todo:: implement & document teardown (async?)
+
+ .. js:attribute:: TestOptions.fail_on_rejection
+
+ If the test is asynchronous and its resulting promise is
+ rejected, fail the test. Defaults to ``true``, set to
+ ``false`` to not fail the test in case of rejection::
+
+ // test/demo.js
+ test('unfail rejection', {
+ asserts: 1,
+ fail_on_rejection: false
+ }, function () {
+ var d = $.Deferred();
+ setTimeout(function () {
+ ok(true);
+ d.reject();
+ }, 100);
+ return d;
+ });
+
+ .. js:attribute:: TestOptions.rpc
+
+ RPC method to use during tests, one of ``"mock"`` or
+ ``"rpc"``. Any other value will disable RPC for the test (if
+ they were enabled by the suite for instance).
+
+ .. js:attribute:: TestOptions.templates
+
+ Whether the current module (and its dependencies)'s templates
+ should be loaded into QWeb before starting the test. A
+ boolean, ``false`` by default.
+
+Running through Python
+----------------------
+
+.. todo:: make that work and document it
+
+.. _qunit: http://qunitjs.com/
+
+.. _qunit assertions: http://api.qunitjs.com/category/assert/
diff --git a/addons/web/http.py b/addons/web/http.py
index 65630768737..b1d50597bf1 100644
--- a/addons/web/http.py
+++ b/addons/web/http.py
@@ -537,7 +537,7 @@ class Root(object):
:rtype: ``Controller | None``
"""
if l:
- ps = '/' + '/'.join(l)
+ ps = '/' + '/'.join(filter(None, l))
meth = 'index'
while ps:
c = controllers_path.get(ps)
diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js
index 3cb4437d1c3..80f965a6200 100644
--- a/addons/web/static/src/js/chrome.js
+++ b/addons/web/static/src/js/chrome.js
@@ -296,14 +296,14 @@ instance.web.DatabaseManager = instance.web.Widget.extend({
$('.oe_secondary_menus_container,.oe_user_menu_placeholder').empty();
var fetch_db = this.rpc("/web/database/get_list", {}).pipe(
function(result) {
- self.db_list = result.db_list;
+ self.db_list = result;
},
function (_, ev) {
ev.preventDefault();
self.db_list = null;
});
var fetch_langs = this.rpc("/web/session/get_lang_list", {}).then(function(result) {
- self.lang_list = result.lang_list;
+ self.lang_list = result;
});
return $.when(fetch_db, fetch_langs).then(self.do_render);
},
diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js
new file mode 100644
index 00000000000..4eadc3a5415
--- /dev/null
+++ b/addons/web/static/src/js/testing.js
@@ -0,0 +1,179 @@
+// Test support structures and methods for OpenERP
+openerp.testing = {};
+(function (testing) {
+ var dependencies = {
+ corelib: [],
+ coresetup: ['corelib'],
+ data: ['corelib', 'coresetup'],
+ dates: [],
+ formats: ['coresetup', 'dates'],
+ chrome: ['corelib', 'coresetup'],
+ views: ['corelib', 'coresetup', 'data', 'chrome'],
+ search: ['data', 'coresetup', 'formats'],
+ list: ['views', 'data'],
+ form: ['data', 'views', 'list', 'formats'],
+ list_editable: ['list', 'form', 'data'],
+ };
+
+ testing.dependencies = window['oe_all_dependencies'] || [];
+ testing.current_module = null;
+ testing.templates = { };
+ testing.add_template = function (name) {
+ var xhr = QWeb2.Engine.prototype.get_xhr();
+ xhr.open('GET', name, false);
+ xhr.send(null);
+ (testing.templates[testing.current_module] =
+ testing.templates[testing.current_module] || [])
+ .push(xhr.responseXML);
+ };
+ /**
+ * Function which does not do anything
+ */
+ testing.noop = function () { };
+ /**
+ * Alter provided instance's ``session`` attribute to make response
+ * mockable:
+ *
+ * * The ``responses`` parameter can be used to provide a map of (RPC)
+ * paths (e.g. ``/web/view/load``) to a function returning a response
+ * to the query.
+ * * ``instance.session`` grows a ``responses`` attribute which is
+ * a map of the same (and is in fact initialized to the ``responses``
+ * parameter if one is provided)
+ *
+ * Note that RPC requests to un-mocked URLs will be rejected with an
+ * error message: only explicitly specified urls will get a response.
+ *
+ * Mocked sessions will *never* perform an actual RPC connection.
+ *
+ * @param instance openerp instance being initialized
+ * @param {Object} [responses]
+ */
+ testing.mockifyRPC = function (instance, responses) {
+ var session = instance.session;
+ session.responses = responses || {};
+ session.rpc_function = function (url, payload) {
+ var fn, params;
+ var needle = payload.params.model + ':' + payload.params.method;
+ if (url.url === '/web/dataset/call_kw'
+ && needle in this.responses) {
+ fn = this.responses[needle];
+ params = [
+ payload.params.args || [],
+ payload.params.kwargs || {}
+ ];
+ } else {
+ fn = this.responses[url.url];
+ params = [payload];
+ }
+
+ if (!fn) {
+ return $.Deferred().reject({}, 'failed',
+ _.str.sprintf("Url %s not found in mock responses, with arguments %s",
+ url.url, JSON.stringify(payload.params))
+ ).promise();
+ }
+ try {
+ return $.when(fn.apply(null, params)).pipe(function (result) {
+ // Wrap for RPC layer unwrapper thingy
+ return {result: result};
+ });
+ } catch (e) {
+ // not sure why this looks like that
+ return $.Deferred().reject({}, 'failed', String(e));
+ }
+ };
+ };
+
+ var _load = function (instance, module, loaded) {
+ if (!loaded) { loaded = []; }
+
+ var deps = dependencies[module];
+ if (!deps) { throw new Error("Unknown dependencies for " + module); }
+
+ var to_load = _.difference(deps, loaded);
+ while (!_.isEmpty(to_load)) {
+ _load(instance, to_load[0], loaded);
+ to_load = _.difference(deps, loaded);
+ }
+ openerp.web[module](instance);
+ loaded.push(module);
+ };
+
+ testing.section = function (name, body) {
+ QUnit.module(testing.current_module + '.' + name);
+ body(testing.case);
+ };
+ testing.case = function (name, options, callback) {
+ if (_.isFunction(options)) {
+ callback = options;
+ options = {};
+ }
+
+ var module = testing.current_module;
+ var module_index = _.indexOf(testing.dependencies, module);
+ var module_deps = testing.dependencies.slice(
+ // If module not in deps (because only tests, no JS) -> indexOf
+ // returns -1 -> index becomes 0 -> replace with ``undefined`` so
+ // Array#slice returns a full copy
+ 0, module_index + 1 || undefined);
+ QUnit.test(name, function (env) {
+ var instance = openerp.init(module_deps);
+ if (_.isNumber(options.asserts)) {
+ expect(options.asserts)
+ }
+
+ if (options.templates) {
+ for(var i=0; i<module_deps.length; ++i) {
+ var dep = module_deps[i];
+ var templates = testing.templates[dep];
+ if (_.isEmpty(templates)) { continue; }
+
+ for (var j=0; j < templates.length; ++j) {
+ instance.web.qweb.add_template(templates[j]);
+ }
+ }
+ }
+
+ var mock, async = false;
+ switch (options.rpc) {
+ case 'mock':
+ async = true;
+ testing.mockifyRPC(instance);
+ mock = function (spec, handler) {
+ instance.session.responses[spec] = handler;
+ };
+ break;
+ case 'rpc':
+ async = true;
+ }
+
+ // TODO: explicit dependencies options for web sub-modules (will deprecate _load/instanceFor)
+ var result = callback(instance, $('#qunit-fixture'), mock);
+
+ if (!(result && _.isFunction(result.then))) {
+ if (async) {
+ ok(false, "asynchronous test cases must return a promise");
+ }
+ return;
+ }
+
+ stop();
+ if (!_.isNumber(options.asserts)) {
+ ok(false, "asynchronous test cases must specify the "
+ + "number of assertions they expect");
+ }
+ result.then(function () {
+ start();
+ }, function (error) {
+ start();
+ if (options.fail_on_rejection === false) {
+ return;
+ }
+ ok(false, typeof error === 'object' && error.message
+ ? error.message
+ : JSON.stringify([].slice.call(arguments)));
+ })
+ });
+ };
+})(openerp.testing);
diff --git a/addons/web/static/src/js/views.js b/addons/web/static/src/js/views.js
index d4b676f2c7a..9390f753be7 100644
--- a/addons/web/static/src/js/views.js
+++ b/addons/web/static/src/js/views.js
@@ -776,8 +776,8 @@ instance.web.ViewManagerAction = instance.web.ViewManager.extend({
name: "JS Tests",
target: 'new',
type : 'ir.actions.act_url',
- url: '/web/static/test/test.html'
- })
+ url: '/web/tests?mod=*'
+ });
break;
case 'perm_read':
var ids = current_view.get_selected_ids();
diff --git a/addons/web/static/test/test.html b/addons/web/static/test/test.html
deleted file mode 100644
index 209723af5a0..00000000000
--- a/addons/web/static/test/test.html
+++ /dev/null
@@ -1,62 +0,0 @@
-<!DOCTYPE html>
-<html style="height: 100%">
-<head>
- <meta http-equiv="content-type" content="text/html; charset=utf-8" />
- <title>OpenERP Web Test Suite</title>
- <link rel="shortcut icon" href="/web/static/src/img/favicon.ico" type="image/x-icon"/>
-
- <link rel="stylesheet" href="/web/static/lib/qunit/qunit.css">
- <script src="/web/static/lib/qunit/qunit.js" type="text/javascript"></script>
-
- <script src="/web/static/lib/underscore/underscore.js" type="text/javascript"></script>
- <script src="/web/static/lib/underscore/underscore.string.js" type="text/javascript"></script>
- <script src="/web/static/lib/backbone/backbone.js" type="text/javascript"></script>
-
- <!-- jquery -->
- <script src="/web/static/lib/jquery/jquery-1.7.2.js"></script>
- <script src="/web/static/lib/jquery.ui/js/jquery-ui-1.8.17.custom.min.js"></script>
- <script src="/web/static/lib/jquery.ba-bbq/jquery.ba-bbq.js"></script>
-
- <script src="/web/static/lib/datejs/globalization/en-US.js"></script>
- <script src="/web/static/lib/datejs/core.js"></script>
- <script src="/web/static/lib/datejs/parser.js"></script>
- <script src="/web/static/lib/datejs/sugarpak.js"></script>
- <script src="/web/static/lib/datejs/extras.js"></script>
-
- <script src="/web/static/lib/qweb/qweb2.js"></script>
-
- <script src="/web/static/lib/py.js/lib/py.js"></script>
-
- <script src="/web/static/src/js/boot.js"></script>
- <script src="/web/static/src/js/corelib.js"></script>
- <script src="/web/static/src/js/coresetup.js"></script>
- <script src="/web/static/src/js/dates.js"></script>
- <script src="/web/static/src/js/formats.js"></script>
- <script src="/web/static/src/js/chrome.js"></script>
- <script src="/web/static/src/js/data.js"></script>
- <script src="/web/static/src/js/views.js"></script>
- <script src="/web/static/src/js/search.js"></script>
- <script src="/web/static/src/js/view_form.js"></script>
- <script src="/web/static/src/js/view_list.js"></script>
- <script src="/web/static/src/js/view_list_editable.js"></script>
-
- <script src="/web/static/test/testing.js"></script>
- <script type="text/javascript">
- QUnit.config.testTimeout = 2000;
- </script>
-</head>
- <body id="oe" class="openerp">
- <div id="qunit"></div>
- <div id="qunit-fixture"></div>
- </body>
- <script type="text/javascript" src="/web/static/test/class.js"></script>
- <script type="text/javascript" src="/web/static/test/registry.js"></script>
- <script type="text/javascript" src="/web/static/test/form.js"></script>
- <script type="text/javascript" src="/web/static/test/list-utils.js"></script>
- <script type="text/javascript" src="/web/static/test/formats.js"></script>
- <script type="text/javascript" src="/web/static/test/rpc.js"></script>
- <script type="text/javascript" src="/web/static/test/evals.js"></script>
- <script type="text/javascript" src="/web/static/test/search.js"></script>
- <script type="text/javascript" src="/web/static/test/Widget.js"></script>
- <script type="text/javascript" src="/web/static/test/list-editable.js"></script>
-</html>
diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js
deleted file mode 100644
index 1be8e94e74b..00000000000
--- a/addons/web/static/test/testing.js
+++ /dev/null
@@ -1,97 +0,0 @@
-// Test support structures and methods for OpenERP
-openerp.testing = (function () {
- var xhr = QWeb2.Engine.prototype.get_xhr();
- xhr.open('GET', '/web/static/src/xml/base.xml', false);
- xhr.send(null);
- var doc = xhr.responseXML;
-
- var dependencies = {
- corelib: [],
- coresetup: ['corelib'],
- data: ['corelib', 'coresetup'],
- dates: [],
- formats: ['coresetup', 'dates'],
- chrome: ['corelib', 'coresetup'],
- views: ['corelib', 'coresetup', 'data', 'chrome'],
- search: ['data', 'coresetup', 'formats'],
- list: ['views', 'data'],
- form: ['data', 'views', 'list', 'formats'],
- list_editable: ['list', 'form', 'data'],
- };
-
- return {
- /**
- * Function which does not do anything
- */
- noop: function () { },
- /**
- * Loads 'base.xml' template file into qweb for the provided instance
- *
- * @param instance openerp instance being initialized, to load the template file in
- */
- loadTemplate: function (instance) {
- instance.web.qweb.add_template(doc);
- },
- /**
- * Alter provided instance's ``session`` attribute to make response
- * mockable:
- *
- * * The ``responses`` parameter can be used to provide a map of (RPC)
- * paths (e.g. ``/web/view/load``) to a function returning a response
- * to the query.
- * * ``instance.session`` grows a ``responses`` attribute which is
- * a map of the same (and is in fact initialized to the ``responses``
- * parameter if one is provided)
- *
- * Note that RPC requests to un-mocked URLs will be rejected with an
- * error message: only explicitly specified urls will get a response.
- *
- * Mocked sessions will *never* perform an actual RPC connection.
- *
- * @param instance openerp instance being initialized
- * @param {Object} [responses]
- */
- mockifyRPC: function (instance, responses) {
- var session = instance.session;
- session.responses = responses || {};
- session.rpc_function = function (url, payload) {
- var fn = this.responses[url.url + ':' + payload.params.method]
- || this.responses[url.url];
-
- if (!fn) {
- return $.Deferred().reject({}, 'failed',
- _.str.sprintf("Url %s not found in mock responses, with arguments %s",
- url.url, JSON.stringify(payload.params))
- ).promise();
- }
- return $.when(fn(payload));
- };
- },
- /**
- * Creates an openerp web instance loading the specified module after
- * all of its dependencies.
- *
- * @param {String} module
- * @returns OpenERP Web instance
- */
- instanceFor: function (module) {
- var instance = openerp.init([]);
- this._load(instance, module);
- return instance;
- },
- _load: function (instance, module, loaded) {
- if (!loaded) { loaded = []; }
-
- var deps = dependencies[module];
- if (!deps) { throw new Error("Unknown dependencies for " + module); }
-
- var to_load = _.difference(deps, loaded);
- while (!_.isEmpty(to_load)) {
- this._load(instance, to_load[0], loaded);
- to_load = _.difference(deps, loaded);
- }
- openerp.web[module](instance);
- loaded.push(module);
- }
- }
-})();