diff options
| author | Xavier Morel <xmo@openerp.com> | 2012-10-25 17:47:45 +0200 |
|---|---|---|
| committer | Xavier Morel <xmo@openerp.com> | 2012-10-25 17:47:45 +0200 |
| commit | 99a2dd3938c7d7d4b7e9c724ec8acbb5eeedf35f (patch) | |
| tree | aca872a490542cb318626f806e8a3d3d2db0c1fd /addons/web | |
| parent | 1cf9b2a60da502de27aa67e8318f41ad6692e5a9 (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__.py | 13 | ||||
| -rw-r--r-- | addons/web/controllers/__init__.py | 1 | ||||
| -rw-r--r-- | addons/web/controllers/main.py | 8 | ||||
| -rw-r--r-- | addons/web/controllers/testing.py | 155 | ||||
| -rw-r--r-- | addons/web/doc/images/runner.png | bin | 0 -> 3963 bytes | |||
| -rw-r--r-- | addons/web/doc/images/runner2.png | bin | 0 -> 6807 bytes | |||
| -rw-r--r-- | addons/web/doc/images/tests.png | bin | 0 -> 65388 bytes | |||
| -rw-r--r-- | addons/web/doc/images/tests2.png | bin | 0 -> 37769 bytes | |||
| -rw-r--r-- | addons/web/doc/images/tests3.png | bin | 0 -> 37827 bytes | |||
| -rw-r--r-- | addons/web/doc/index.rst | 2 | ||||
| -rw-r--r-- | addons/web/doc/testing.rst | 479 | ||||
| -rw-r--r-- | addons/web/http.py | 2 | ||||
| -rw-r--r-- | addons/web/static/src/js/chrome.js | 4 | ||||
| -rw-r--r-- | addons/web/static/src/js/testing.js | 179 | ||||
| -rw-r--r-- | addons/web/static/src/js/views.js | 4 | ||||
| -rw-r--r-- | addons/web/static/test/test.html | 62 | ||||
| -rw-r--r-- | addons/web/static/test/testing.js | 97 |
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"><< 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 Binary files differnew file mode 100644 index 00000000000..bd48e9d2922 --- /dev/null +++ b/addons/web/doc/images/runner.png diff --git a/addons/web/doc/images/runner2.png b/addons/web/doc/images/runner2.png Binary files differnew file mode 100644 index 00000000000..38ea2949cfc --- /dev/null +++ b/addons/web/doc/images/runner2.png diff --git a/addons/web/doc/images/tests.png b/addons/web/doc/images/tests.png Binary files differnew file mode 100644 index 00000000000..84083d9e5ed --- /dev/null +++ b/addons/web/doc/images/tests.png diff --git a/addons/web/doc/images/tests2.png b/addons/web/doc/images/tests2.png Binary files differnew file mode 100644 index 00000000000..59835b257b5 --- /dev/null +++ b/addons/web/doc/images/tests2.png diff --git a/addons/web/doc/images/tests3.png b/addons/web/doc/images/tests3.png Binary files differnew file mode 100644 index 00000000000..10b45eddedc --- /dev/null +++ b/addons/web/doc/images/tests3.png 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); - } - } -})(); |
