diff --git a/.bzrignore b/.bzrignore index ba5258506be..1e0564801ab 100644 --- a/.bzrignore +++ b/.bzrignore @@ -11,4 +11,4 @@ RE:^share/ RE:^man/ RE:^lib/ -RE:^doc/_build/ +RE:^addons/\w+/doc/_build/ diff --git a/addons/web/__openerp__.py b/addons/web/__openerp__.py index 36e7ad2db27..d83636de0f9 100644 --- a/addons/web/__openerp__.py +++ b/addons/web/__openerp__.py @@ -41,6 +41,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", @@ -68,5 +69,21 @@ This module provides the core of the OpenERP Web Client. 'qweb' : [ "static/src/xml/*.xml", ], + 'test': [ + "static/test/testing.js", + "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.js", + "static/test/list-editable.js", + "static/test/mutex.js" + ], 'bootstrap': True, + 'twitter': False, } 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 81d151cabdc..a0474854daf 100644 --- a/addons/web/controllers/main.py +++ b/addons/web/controllers/main.py @@ -258,10 +258,20 @@ def concat_files(file_list, reader=None, intersperse=""): files_concat = intersperse.join(files_content) return files_concat, checksum.hexdigest() +concat_js_cache = {} + def concat_js(file_list): content, checksum = concat_files(file_list, intersperse=';') - content = rjsmin(content) - return content, checksum + if checksum in concat_js_cache: + content = concat_js_cache[checksum] + else: + content = rjsmin(content) + concat_js_cache[checksum] = content + return content, checksum + +def fs2web(path): + """convert FS path into web path""" + return '/'.join(path.split(os.path.sep)) def manifest_glob(req, addons, key): if addons is None: @@ -278,7 +288,7 @@ def manifest_glob(req, addons, key): globlist = manifest.get(key, []) for pattern in globlist: for path in glob.glob(os.path.normpath(os.path.join(addons_path, addon, pattern))): - r.append((path, path[len(addons_path):])) + r.append((path, fs2web(path[len(addons_path):]))) return r def manifest_list(req, mods, extension): @@ -637,8 +647,7 @@ class WebClient(openerpweb.Controller): data = fp.read().decode('utf-8') path = file_map[f] - # convert FS path into web path - web_dir = '/'.join(os.path.dirname(path).split(os.path.sep)) + web_dir = os.path.dirname(path) data = re.sub( rx_import, @@ -781,6 +790,17 @@ class Database(openerpweb.Controller): params['db_original_name'], params['db_name']) + @openerpweb.jsonrequest + def duplicate(self, req, fields): + params = dict(map(operator.itemgetter('name', 'value'), fields)) + duplicate_attrs = ( + params['super_admin_pwd'], + params['db_original_name'], + params['db_name'], + ) + + return req.session.proxy("db").duplicate_database(*duplicate_attrs) + @openerpweb.jsonrequest def drop(self, req, fields): password, db = operator.itemgetter( @@ -887,10 +907,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..3c7c4899b6c --- /dev/null +++ b/addons/web/controllers/testing.py @@ -0,0 +1,162 @@ +# 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""" + + + + + OpenERP Testing + + +
+ + +
+ + +""") +NOTFOUND = Template(u""" +

Unable to find the module [${module}], please check that the module + name is correct and the module is on OpenERP's path.

+<< Back to tests +""") +TESTING = Template(u""" + +<%def name="to_path(module, p)">/${module}/${p} + + + + OpenERP Web Tests + + + + + + + + +
+
+ +% for module, jss, tests, templates in files: + % for js in jss: + + % endfor + % if tests or templates: + + % endif + % if tests: + % for test in tests: + + % endfor + % endif +% endfor + +""") + +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) + ] + + # if all three db_info parameters are present, send them to the page + db_info = dict((k, v) for k, v in kwargs.iteritems() + if k in ['source', 'supadmin', 'password']) + if len(db_info) != 3: + db_info = None + + 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']]), db_info=json.dumps(db_info)) + + 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/async.rst b/addons/web/doc/async.rst index 31e6199a6fa..6782fdac029 100644 --- a/addons/web/doc/async.rst +++ b/addons/web/doc/async.rst @@ -240,6 +240,103 @@ for instance (to take advantage of :js:func:`when` 's special treatment of single-value promises). +jQuery.Deferred API +~~~~~~~~~~~~~~~~~~~ + +.. js:function:: when(deferreds…) + + :param deferreds: deferred objects to multiplex + :returns: a multiplexed deferred + :rtype: :js:class:`Deferred` + +.. js:class:: Deferred + + .. js:function:: Deferred.then(doneCallback[, failCallback]) + + Attaches new callbacks to the resolution or rejection of the + deferred object. Callbacks are executed in the order they are + attached to the deferred. + + To provide only a failure callback, pass ``null`` as the + ``doneCallback``, to provide only a success callback the + second argument can just be ignored (and not passed at all). + + Returns a new deferred which resolves to the result of the + corresponding callback, if a callback returns a deferred + itself that new deferred will be used as the resolution of the + chain. + + :param doneCallback: function called when the deferred is resolved + :type doneCallback: Function + :param failCallback: function called when the deferred is rejected + :type failCallback: Function + :returns: the deferred object on which it was called + :rtype: :js:class:`Deferred` + + .. js:function:: Deferred.done(doneCallback) + + Attaches a new success callback to the deferred, shortcut for + ``deferred.then(doneCallback)``. + + This is a jQuery extension to `CommonJS Promises/A`_ providing + little value over calling :js:func:`~Deferred.then` directly, + it should be avoided. + + :param doneCallback: function called when the deferred is resolved + :type doneCallback: Function + :returns: the deferred object on which it was called + :rtype: :js:class:`Deferred` + + .. js:function:: Deferred.fail(failCallback) + + Attaches a new failure callback to the deferred, shortcut for + ``deferred.then(null, failCallback)``. + + A second jQuery extension to `Promises/A `_. Although it provides more value than + :js:func:`~Deferred.done`, it still is not much and should be + avoided as well. + + :param failCallback: function called when the deferred is rejected + :type failCallback: Function + :returns: the deferred object on which it was called + :rtype: :js:class:`Deferred` + + .. js:function:: Deferred.promise() + + Returns a read-only view of the deferred object, with all + mutators (resolve and reject) methods removed. + + .. js:function:: Deferred.resolve(value…) + + Called to resolve a deferred, any value provided will be + passed onto the success handlers of the deferred object. + + Resolving a deferred which has already been resolved or + rejected has no effect. + + .. js:function:: Deferred.reject(value…) + + Called to reject (fail) a deferred, any value provided will be + passed onto the failure handler of the deferred object. + + Rejecting a deferred which has already been resolved or + rejected has no effect. + +.. [#] or simply calling :js:class:`Deferred` as a function, the + result is the same + +.. [#] or not-promises, the `CommonJS Promises/B`_ role of + :js:func:`when` is to be able to treat values and promises + uniformly: :js:func:`when` will pass promises through directly, + but non-promise values and objects will be transformed into a + resolved promise (resolving themselves with the value itself). + + jQuery's :js:func:`when` keeps this behavior making deferreds + easy to build from "static" values, or allowing defensive code + where expected promises are wrapped in :js:func:`when` just in + case. + .. _promises: http://en.wikipedia.org/wiki/Promise_(programming) .. _jQuery's deferred: http://api.jquery.com/category/deferred-object/ .. _CommonJS Promises/A: http://wiki.commonjs.org/wiki/Promises/A diff --git a/addons/web/doc/conf.py b/addons/web/doc/conf.py index 83fc9693f02..54434190707 100644 --- a/addons/web/doc/conf.py +++ b/addons/web/doc/conf.py @@ -254,4 +254,5 @@ intersphinx_mapping = { 'python': ('http://docs.python.org/', None), 'openerpserver': ('http://doc.openerp.com/trunk/developers/server', None), 'openerpdev': ('http://doc.openerp.com/trunk/developers', None), + 'openerpcommand': ('http://doc.openerp.com/trunk/developers/command', None), } diff --git a/addons/web/doc/images/db-query.png b/addons/web/doc/images/db-query.png new file mode 100644 index 00000000000..e063b724001 Binary files /dev/null and b/addons/web/doc/images/db-query.png differ diff --git a/addons/web/doc/images/runner.png b/addons/web/doc/images/runner.png new file mode 100644 index 00000000000..bd48e9d2922 Binary files /dev/null and b/addons/web/doc/images/runner.png differ diff --git a/addons/web/doc/images/runner2.png b/addons/web/doc/images/runner2.png new file mode 100644 index 00000000000..38ea2949cfc Binary files /dev/null and b/addons/web/doc/images/runner2.png differ diff --git a/addons/web/doc/images/tests.png b/addons/web/doc/images/tests.png new file mode 100644 index 00000000000..84083d9e5ed Binary files /dev/null and b/addons/web/doc/images/tests.png differ diff --git a/addons/web/doc/images/tests2.png b/addons/web/doc/images/tests2.png new file mode 100644 index 00000000000..c8a6f8ae9ee Binary files /dev/null and b/addons/web/doc/images/tests2.png differ diff --git a/addons/web/doc/images/tests3.png b/addons/web/doc/images/tests3.png new file mode 100644 index 00000000000..247f70716af Binary files /dev/null and b/addons/web/doc/images/tests3.png differ diff --git a/addons/web/doc/index.rst b/addons/web/doc/index.rst index 3e436200a8c..06534c50903 100644 --- a/addons/web/doc/index.rst +++ b/addons/web/doc/index.rst @@ -14,6 +14,8 @@ Contents: presentation async + testing + widget qweb rpc @@ -24,7 +26,6 @@ Contents: changelog-7.0 - Indices and tables ================== diff --git a/addons/web/doc/test-report.txt b/addons/web/doc/test-report.txt new file mode 100644 index 00000000000..ce52618fe20 --- /dev/null +++ b/addons/web/doc/test-report.txt @@ -0,0 +1,25 @@ +test_empty_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok +test_ids_shortcut (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok +test_regular_find (openerp.addons.web.tests.test_dataset.TestDataSetController) ... ok +web.testing.stack: direct, value, success ... ok +web.testing.stack: direct, deferred, success ... ok +web.testing.stack: direct, value, error ... ok +web.testing.stack: direct, deferred, failure ... ok +web.testing.stack: successful setup ... ok +web.testing.stack: successful teardown ... ok +web.testing.stack: successful setup and teardown ... ok + +[snip ~150 lines] + +test_convert_complex_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_convert_complex_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_convert_literal_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_convert_literal_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_retrieve_nonliteral_context (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok +test_retrieve_nonliteral_domain (openerp.addons.web.tests.test_view.DomainsAndContextsTest) ... ok + +---------------------------------------------------------------------- +Ran 181 tests in 15.706s + +OK + diff --git a/addons/web/doc/testing.rst b/addons/web/doc/testing.rst new file mode 100644 index 00000000000..600d379aa02 --- /dev/null +++ b/addons/web/doc/testing.rst @@ -0,0 +1,693 @@ +.. 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 `_. 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:`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, a test case gets 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 +scratchpad your code can do whatever it wants:: + + // test/demo.js + test('DOM content', function (instance, $scratchpad) { + $scratchpad.html('
ok
'); + 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 to the :js:func:`test case +function `. + +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 + + + + + +

+
+
+
+ +:: + + // 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 +`, and thus test cases need to be async-aware. + +This is done by returning a :js:class:`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 uses the :js:class:`options parameter ` +to specify the number of assertions the 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. This timeout *only* applies to the test itself, not to + the setup and teardown processes. + +.. 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 + `. + +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 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 ` 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 and 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'}).then(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).then(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). + +.. _testing-rpc-rpc: + +Actual RPC +++++++++++ + +A more realistic (but significantly slower and more expensive) way to +perform RPC calls is to perform actual calls to an actually running +OpenERP server. To do this, set the :js:attr:`rpc option +<~TestOptions.rpc>` to ``rpc``, it will not provide any new parameter +but will enable actual RPC, and the automatic creation and destruction +of databases (from a specified source) around tests. + +First, create a basic model we can test stuff with: + +.. code-block:: javascript + + from openerp.osv import orm, fields + + class TestObject(orm.Model): + _name = 'web_tests_demo.model' + + _columns = { + 'name': fields.char("Name", required=True), + 'thing': fields.char("Thing"), + 'other': fields.char("Other", required=True) + } + _defaults = { + 'other': "bob" + } + +then the actual test:: + + test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) { + var Model = new instance.web.Model('web_tests_demo.model'); + return Model.call('create', [{name: "Bob"}]) + .then(function (id) { + return Model.call('read', [[id]]); + }).then(function (records) { + strictEqual(records.length, 1); + var record = records[0]; + strictEqual(record.name, "Bob"); + strictEqual(record.thing, false); + // default value + strictEqual(record.other, 'bob'); + }); + }); + +This test looks like a "mock" RPC test but for the lack of mock +response (and the different ``rpc`` type), however it has further +ranging consequences in that it will copy an existing database to a +new one, run the test in full on that temporary database and destroy +the database, to simulate an isolated and transactional context and +avoid affecting other tests. One of the consequences is that it takes +a *long* time to run (5~10s, most of that time being spent waiting for +a database duplication). + +Furthermore, as the test needs to clone a database, it also has to ask +which database to clone, the database/super-admin password and the +password of the ``admin`` user (in order to authenticate as said +user). As a result, the first time the test runner encounters an +``rpc: "rpc"`` test configuration it will produce the following +prompt: + +.. image:: ./images/db-query.png + :align: center + +and stop the testing process until the necessary information has been +provided. + +The prompt will only appear once per test run, all tests will use the +same "source" database. + +.. note:: + + The handling of that information is currently rather brittle and + unchecked, incorrect values will likely crash the runner. + +.. note:: + + The runner does not currently store this information (for any + longer than a test run that is), the prompt will have to be filled + every time. + +Testing API +----------- + +.. js:function:: openerp.testing.section(name[, options], body) + + A test section, serves as shared namespace for related tests (for + constants or values to only set up once). The ``body`` function + should contain the tests themselves. + + Note that the order in which tests are run is essentially + undefined, do *not* rely on it. + + :param String name: + :param TestOptions options: + :param body: + :type body: Function<:js:func:`~openerp.testing.case`, void> + +.. js:function:: openerp.testing.case(name[, options], callback) + + Registers a test case callback in the test runner, the callback + will only be run once the runner is started (or maybe not at all, + if the test is filtered out). + + :param String name: + :param TestOptions options: + :param callback: + :type callback: Function> + +.. js:class:: TestOptions + + the various options which can be passed to + :js:func:`~openerp.testing.section` or + :js:func:`~openerp.testing.case`. Except for + :js:attr:`~TestOptions.setup` and + :js:attr:`~TestOptions.teardown`, an option on + :js:func:`~openerp.testing.case` will overwrite the corresponding + option on :js:func:`~openerp.testing.section` so + e.g. :js:attr:`~TestOptions.rpc` can be set for a + :js:func:`~openerp.testing.section` and then differently set for + some :js:func:`~openerp.testing.case` of that + :js:func:`~openerp.testing.section` + + .. 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 + + Test case setup, run right before each test case. A section's + :js:func:`~TestOptions.setup` is run before the case's own, if + both are specified. + + .. js:attribute:: TestOptions.teardown + + Test case teardown, a case's :js:func:`~TestOptions.teardown` + is run before the corresponding section if both are present. + + .. 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. + +The test runner can also use two global configuration values set +directly on the ``window`` object: + +* ``oe_all_dependencies`` is an ``Array`` of all modules with a web + component, ordered by dependency (for a module ``A`` with + dependencies ``A'``, any module of ``A'`` must come before ``A`` in + the array) + +* ``oe_db_info`` is an object with 3 keys ``source``, ``supadmin`` and + ``password``. It is used to pre-configure :ref:`actual RPC + ` tests, to avoid a prompt being displayed + (especially for headless situations). + +Running through Python +---------------------- + +The web client includes the means to run these tests on the +command-line (or in a CI system), but while actually running it is +pretty simple the setup of the pre-requisite parts has some +complexities. + +1. Install unittest2_ and QUnitSuite_ in your Python environment. Both + can trivially be installed via `pip `_ or + `easy_install + `_. + + The former is the unit-testing framework used by OpenERP, the + latter is an adapter module to run qunit_ test suites and convert + their result into something unittest2_ can understand and report. + +2. Install PhantomJS_. It is a headless + browser which allows automating running and testing web + pages. QUnitSuite_ uses it to actually run the qunit_ test suite. + + The PhantomJS_ website provides pre-built binaries for some + platforms, and your OS's package management probably provides it as + well. + + If you're building PhantomJS_ from source, I recommend preparing + for some knitting time as it's not exactly fast (it needs to + compile both `Qt `_ and `Webkit + `_, both being pretty big projects). + + .. note:: + + Because PhantomJS_ is webkit-based, it will not be able to test + if Firefox, Opera or Internet Explorer can correctly run the + test suite (and it is only an approximation for Safari and + Chrome). It is therefore recommended to *also* run the test + suites in actual browsers once in a while. + + .. note:: + + The version of PhantomJS_ this was build through is 1.7, + previous versions *should* work but are not actually supported + (and tend to just segfault when something goes wrong in + PhantomJS_ itself so they're a pain to debug). + +3. Set up :ref:`OpenERP Command `, + which will be used to actually run the tests: running the qunit_ + test suite requires a running server, so at this point OpenERP + Server isn't able to do it on its own during the building/testing + process. + +4. Install a new database with all relevant modules (all modules with + a web component at least), then restart the server + + .. note:: + + For some tests, a source database needs to be duplicated. This + operation requires that there be no connection to the database + being duplicated, but OpenERP doesn't currently break + existing/outstanding connections, so restarting the server is + the simplest way to ensure everything is in the right state. + +5. Launch ``oe run-tests -d $DATABASE -mweb`` with the correct + addons-path specified (and replacing ``$DATABASE`` by the source + database you created above) + + .. note:: + + If you leave out ``-mweb``, the runner will attempt to run all + the tests in all the modules, which may or may not work. + +If everything went correctly, you should now see a list of tests with +(hopefully) ``ok`` next to their names, closing with a report of the +number of tests run and the time it took: + +.. literalinclude:: test-report.txt + :language: text + +Congratulation, you have just performed a successful "offline" run of +the OpenERP Web test suite. + +.. note:: + + Note that this runs all the Python tests for the ``web`` module, + but all the web tests for all of OpenERP. This can be surprising. + +.. _qunit: http://qunitjs.com/ + +.. _qunit assertions: http://api.qunitjs.com/category/assert/ + +.. _unittest2: http://pypi.python.org/pypi/unittest2 + +.. _QUnitSuite: http://pypi.python.org/pypi/QUnitSuite/ + +.. _PhantomJS: http://phantomjs.org/ diff --git a/addons/web/http.py b/addons/web/http.py index d4c044efd72..8d13994e1dc 100644 --- a/addons/web/http.py +++ b/addons/web/http.py @@ -458,6 +458,16 @@ class DisableCacheMiddleware(object): start_response(status, new_headers) return self.app(environ, start_wrapped) +def session_path(): + try: + username = getpass.getuser() + except Exception: + username = "unknown" + path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username) + if not os.path.exists(path): + os.mkdir(path, 0700) + return path + class Root(object): """Root WSGI application for the OpenERP Web Client. """ @@ -468,13 +478,7 @@ class Root(object): self._load_addons() # Setup http sessions - try: - username = getpass.getuser() - except Exception: - username = "unknown" - path = os.path.join(tempfile.gettempdir(), "oe-sessions-" + username) - if not os.path.exists(path): - os.mkdir(path, 0700) + path = session_path() self.session_store = werkzeug.contrib.sessions.FilesystemSessionStore(path) self.session_lock = threading.Lock() _logger.debug('HTTP sessions stored in: %s', path) @@ -563,7 +567,7 @@ class Root(object): :rtype: ``Controller | None`` """ if l: - ps = '/' + '/'.join(l) + ps = '/' + '/'.join(filter(None, l)) method_name = 'index' while ps: c = controllers_path.get(ps) diff --git a/addons/web/static/lib/jquery.tipsy/jquery.tipsy.js b/addons/web/static/lib/jquery.tipsy/jquery.tipsy.js index 5929d591261..e978042f21b 100644 --- a/addons/web/static/lib/jquery.tipsy/jquery.tipsy.js +++ b/addons/web/static/lib/jquery.tipsy/jquery.tipsy.js @@ -27,7 +27,8 @@ var $tip = this.tip(); $tip.find('.tipsy-inner')[this.options.html ? 'html' : 'text'](title); - $tip[0].className = 'tipsy openerp oe_tooltip '; // reset classname in case of dynamic gravity + $tip[0].className = 'tipsy '; // reset classname in case of dynamic gravity + $tip.openerpClass('oe_tooltip'); $tip.remove().css({top: 0, left: 0, visibility: 'hidden', display: 'block'}).prependTo(document.body); var pos = $.extend({}, this.$element.offset(), { diff --git a/addons/web/static/src/css/base.css b/addons/web/static/src/css/base.css index a421336c43f..9effff12226 100644 --- a/addons/web/static/src/css/base.css +++ b/addons/web/static/src/css/base.css @@ -20,20 +20,6 @@ font-style: normal; } -@media print { - .oe_topbar, .oe_leftbar, .oe_loading { - display: none !important; - } -} -.openerp.openerp_webclient_container { - height: 100%; -} - -.text-tag .text-button { - height: auto !important; - min-height: 16px; -} - .openerp { padding: 0; margin: 0; @@ -46,6 +32,9 @@ * http://stackoverflow.com/questions/2855589/replace-input-type-file-by-an-image */ } +.openerp.openerp_webclient_container { + height: 100%; +} .openerp :-moz-placeholder { color: #afafb6 !important; font-style: italic !important; @@ -197,6 +186,10 @@ .openerp .oe_bounce_container { display: inline-block; } +.openerp .text-tag .text-button { + height: auto !important; + min-height: 16px; +} .openerp .ui-tabs { position: static; } @@ -1146,6 +1139,7 @@ height: 40px; width: 157px; margin: 14px 0; + border: 0; } .openerp .oe_footer { position: fixed; @@ -2371,6 +2365,77 @@ .openerp .oe_form .oe_form_field_image:hover .oe_form_field_image_controls { display: block; } +.openerp .oe_fileupload { + display: inline-block; + clear: both; + width: 100%; +} +.openerp .oe_fileupload .oe_add { + float: left; + position: relative; + width: 100%; + left: 2px; + top: 7px; +} +.openerp .oe_fileupload .oe_add button { + display: inline; + height: 24px; + font-size: 12px; + line-height: 12px; + vertical-align: middle; +} +.openerp .oe_fileupload .oe_add button.oe_attach { + width: 24px; + overflow: hidden; + width: 24px; + overflow: hidden; + background: transparent; + color: #7c7bad; + box-shadow: none; + border: none; + text-shadow: none; +} +.openerp .oe_fileupload .oe_add button.oe_attach .oe_e { + position: relative; + top: -1px; + left: -9px; +} +.openerp .oe_fileupload .oe_add input.oe_form_binary_file { + display: inline-block; + margin-left: -5px; + height: 28px; + width: 52px; + margin-top: -26px; +} +.openerp .oe_fileupload .oe_add .oe_attach_label { + color: #7c7bad; + margin-left: -3px; +} +.openerp .oe_fileupload .oe_attachments { + margin-bottom: 4px; + margin-right: 0px; + font-size: 12px; + border-radius: 2px; + border: solid 1px rgba(124, 123, 173, 0.14); +} +.openerp .oe_fileupload .oe_attachments .oe_attachment { + padding: 2px; + padding-left: 4px; + padding-right: 4px; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e { + font-size: 23px; + margin-top: -5px; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e:hover { + text-decoration: none; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(odd) { + background: white; +} +.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(even) { + background: #f4f5fa; +} .openerp .oe_form_field_many2one td:first-child { position: relative; } @@ -2903,78 +2968,6 @@ color: #333333; } -.openerp .oe_fileupload { - display: inline-block; - clear: both; - width: 100%; -} -.openerp .oe_fileupload .oe_add { - float: left; - position: relative; - width: 100%; - left: 2px; - top: 7px; -} -.openerp .oe_fileupload .oe_add button { - display: inline; - height: 24px; - font-size: 12px; - line-height: 12px; - vertical-align: middle; -} -.openerp .oe_fileupload .oe_add button.oe_attach { - width: 24px; - overflow: hidden; - width: 24px; - overflow: hidden; - background: transparent; - color: #7c7bad; - box-shadow: none; - border: none; - text-shadow: none; -} -.openerp .oe_fileupload .oe_add button.oe_attach .oe_e { - position: relative; - top: -1px; - left: -9px; -} -.openerp .oe_fileupload .oe_add input.oe_form_binary_file { - display: inline-block; - margin-left: -5px; - height: 28px; - width: 52px; - margin-top: -26px; -} -.openerp .oe_fileupload .oe_add .oe_attach_label { - color: #7c7bad; - margin-left: -3px; -} -.openerp .oe_fileupload .oe_attachments { - margin-bottom: 4px; - margin-right: 0px; - font-size: 12px; - border-radius: 2px; - border: solid 1px rgba(124, 123, 173, 0.14); -} -.openerp .oe_fileupload .oe_attachments .oe_attachment { - padding: 2px; - padding-left: 4px; - padding-right: 4px; -} -.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e { - font-size: 23px; - margin-top: -5px; -} -.openerp .oe_fileupload .oe_attachments .oe_attachment .oe_e:hover { - text-decoration: none; -} -.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(odd) { - background: white; -} -.openerp .oe_fileupload .oe_attachments .oe_attachment:nth-child(even) { - background: #f4f5fa; -} - .kitten-mode-activated { background-image: url(http://placekitten.com/g/1365/769); background-size: cover; @@ -3033,8 +3026,8 @@ div.ui-widget-overlay { .openerp { text-shadow: none; } - .openerp .oe_header_row, .openerp ul.oe_header, .openerp div.oe_mail_thread_action, .openerp .oe_mail_recthread_actions, .openerp .oe_button_box, .openerp .oe_form button, .openerp button.oe_invite, .openerp .oe_form header, .openerp .openerp .oe_notebook > li.ui-state-default { - display: none; + .openerp .oe_header_row, .openerp ul.oe_header, .openerp div.oe_mail_thread_action, .openerp .oe_mail_recthread_actions, .openerp .oe_button_box, .openerp .oe_form button, .openerp button.oe_invite, .openerp .oe_form header, .openerp .openerp .oe_notebook > li.ui-state-default, .openerp .oe_topbar, .openerp .oe_leftbar, .openerp .oe_loading { + display: none !important; } .openerp .oe_list_content button, .openerp .oe_list_content input[type=checkbox] { visibility: hidden; diff --git a/addons/web/static/src/css/base.sass b/addons/web/static/src/css/base.sass index 596426917b5..9211aec7b0c 100644 --- a/addons/web/static/src/css/base.sass +++ b/addons/web/static/src/css/base.sass @@ -140,18 +140,6 @@ $sheet-padding: 16px // }}} -@media print - .oe_topbar, .oe_leftbar, .oe_loading - display: none !important - -.openerp.openerp_webclient_container - height: 100% - -// jQueryUI css bug fixing -.text-tag .text-button - height: auto !important - min-height: 16px - .openerp // Global style {{{ padding: 0 @@ -161,6 +149,8 @@ $sheet-padding: 16px font-size: 13px background: white text-shadow: 0 1px 1px rgba(255, 255, 255, 0.5) + &.openerp_webclient_container + height: 100% // }}} //Placeholder style{{{ \:-moz-placeholder @@ -254,6 +244,11 @@ $sheet-padding: 16px .oe_bounce_container display: inline-block + // Bug lp:1051746 + .text-tag .text-button + height: auto !important + min-height: 16px + // bug noted in jquery ui CSS doesn't seem to occur in IE9, // so remove position:relative .ui-tabs @@ -937,6 +932,7 @@ $sheet-padding: 16px height: 40px width: 157px margin: 14px 0 + border: 0 .oe_footer position: fixed bottom: 0 @@ -1895,6 +1891,64 @@ $sheet-padding: 16px @include box-sizing(border) &:hover .oe_form_field_image_controls display: block + .oe_fileupload + display: inline-block + clear: both + width: 100% + .oe_add + float: left + position: relative + width: 100% + left: +2px + top: +7px + button + display: inline + height: 24px + font-size: 12px + line-height: 12px + vertical-align: middle + button.oe_attach + width: 24px + overflow: hidden + width: 24px + overflow: hidden + background: transparent + color: #7C7BAD + box-shadow: none + border: none + text-shadow: none + .oe_e + position: relative + top: -1px + left: -9px + input.oe_form_binary_file + display: inline-block + margin-left: -5px + height: 28px + width: 52px + margin-top: -26px + .oe_attach_label + color: #7C7BAD + margin-left: -3px + .oe_attachments + margin-bottom: 4px + margin-right: 0px + font-size: 12px + border-radius: 2px + border: solid 1px rgba(124,123,173,0.14) + .oe_attachment + padding: 2px + padding-left: 4px + padding-right: 4px + .oe_e + font-size: 23px + margin-top: -5px + .oe_e:hover + text-decoration: none + .oe_attachment:nth-child(odd) + background: white + .oe_attachment:nth-child(even) + background: #F4F5FA // }}} // FormView.many2one {{{ .oe_form_field_many2one @@ -2297,67 +2351,6 @@ $sheet-padding: 16px float: right color: #333 // }}} - -.openerp - .oe_fileupload - display: inline-block - clear: both - width: 100% - .oe_add - float: left - position: relative - width: 100% - left: +2px - top: +7px - button - display: inline - height: 24px - font-size: 12px - line-height: 12px - vertical-align: middle - button.oe_attach - width: 24px - overflow: hidden - width: 24px - overflow: hidden - background: transparent - color: #7C7BAD - box-shadow: none - border: none - text-shadow: none - .oe_e - position: relative - top: -1px - left: -9px - input.oe_form_binary_file - display: inline-block - margin-left: -5px - height: 28px - width: 52px - margin-top: -26px - .oe_attach_label - color: #7C7BAD - margin-left: -3px - .oe_attachments - margin-bottom: 4px - margin-right: 0px - font-size: 12px - border-radius: 2px - border: solid 1px rgba(124,123,173,0.14) - .oe_attachment - padding: 2px - padding-left: 4px - padding-right: 4px - .oe_e - font-size: 23px - margin-top: -5px - .oe_e:hover - text-decoration: none - .oe_attachment:nth-child(odd) - background: white - .oe_attachment:nth-child(even) - background: #F4F5FA - // Kitten Mode {{{ .kitten-mode-activated background-image: url(http://placekitten.com/g/1365/769) @@ -2367,11 +2360,13 @@ $sheet-padding: 16px opacity: 0.70 // }}} +// jQueryUI top level {{{ // The jQuery-ui overlay and Autocomplete are outside the .openerp div, please don't add indentation !!! div.ui-widget-overlay background: black @include opacity(0.3) - +// TODO: I think only the overlay is problematic, the other top level widgets should use $.fn.openerpClass() +// eg: $el.autocomplete().openerpClass(); .ui-widget font-family: "Lucida Grande", Helvetica, Verdana, Arial, sans-serif color: #4c4c4c @@ -2398,11 +2393,14 @@ div.ui-widget-overlay .ui-corner-all @include radius(3px) +// }}} +// @media print {{{ @media print .openerp - .oe_header_row, ul.oe_header, div.oe_mail_thread_action, .oe_mail_recthread_actions, .oe_button_box, .oe_form button, button.oe_invite, .oe_form header, .openerp .oe_notebook > li.ui-state-default - display: none + .oe_header_row, ul.oe_header, div.oe_mail_thread_action, .oe_mail_recthread_actions, .oe_button_box, .oe_form button, button.oe_invite, .oe_form header, .openerp .oe_notebook > li.ui-state-default, .oe_topbar, .oe_leftbar, .oe_loading + // We use !important here because jQuery adds @style = display: block on elements when using $.fn.show() + display: none !important .oe_list_content button, input[type=checkbox] visibility: hidden @@ -2428,5 +2426,7 @@ div.ui-widget-overlay background: none .openerp div.oe_mail_wall overflow: hidden !important +// }}} + // au BufWritePost,FileWritePost *.sass :!sass --style expanded --line-numbers > "%:p:r.css" // vim:tabstop=4:shiftwidth=4:softtabstop=4:fdm=marker: diff --git a/addons/web/static/src/js/boot.js b/addons/web/static/src/js/boot.js index 05ce709e80a..631a7f8c6f5 100644 --- a/addons/web/static/src/js/boot.js +++ b/addons/web/static/src/js/boot.js @@ -19,12 +19,14 @@ /** * OpenERP instance constructor * - * @param {Array} modules list of modules to initialize + * @param {Array|String} modules list of modules to initialize */ init: function(modules) { - // By default only web will be loaded, the rest will be by loaded - // by openerp.web.Session on the first session_authenticate - modules = _.union(['web'], modules || []); + if (modules === "fuck your shit, don't load anything you cunt") { + modules = []; + } else { + modules = _.union(['web'], modules || []); + } var new_instance = { // links to the global openerp _openerp: openerp, diff --git a/addons/web/static/src/js/chrome.js b/addons/web/static/src/js/chrome.js index c58451ba812..4197880182d 100644 --- a/addons/web/static/src/js/chrome.js +++ b/addons/web/static/src/js/chrome.js @@ -48,7 +48,7 @@ instance.web.Notification = instance.web.Widget.extend({ */ instance.web.dialog = function(element) { var result = element.dialog.apply(element, _.rest(_.toArray(arguments))); - result.dialog("widget").addClass("openerp"); + result.dialog("widget").openerpClass(); return result; }; @@ -190,7 +190,14 @@ instance.web.Dialog = instance.web.Widget.extend({ }); instance.web.CrashManager = instance.web.Class.extend({ + init: function() { + this.active = true; + }, + rpc_error: function(error) { + if (!this.active) { + return; + } if (error.data.fault_code) { var split = ("" + error.data.fault_code).split('\n')[0].split(' -- '); if (split.length > 1) { @@ -205,6 +212,9 @@ instance.web.CrashManager = instance.web.Class.extend({ } }, show_warning: function(error) { + if (!this.active) { + return; + } instance.web.dialog($('
' + QWeb.render('CrashManager.warning', {error: error}) + '
'), { title: "OpenERP " + _.str.capitalize(error.type), buttons: [ @@ -213,7 +223,9 @@ instance.web.CrashManager = instance.web.Class.extend({ }); }, show_error: function(error) { - var self = this; + if (!this.active) { + return; + } var buttons = {}; buttons[_t("Ok")] = function() { $(this).dialog("close"); @@ -310,7 +322,7 @@ instance.web.DatabaseManager = instance.web.Widget.extend({ self.db_list = null; }); var fetch_langs = this.rpc("/web/session/get_lang_list", {}).done(function(result) { - self.lang_list = result.lang_list; + self.lang_list = result; }); return $.when(fetch_db, fetch_langs).done(self.do_render); }, @@ -643,7 +655,7 @@ instance.web.client_actions.add("login", "instance.web.Login"); instance.web.redirect = function(url, wait) { // Dont display a dialog if some xmlhttprequest are in progress if (instance.client && instance.client.crashmanager) { - instance.client.crashmanager.destroy(); + instance.client.crashmanager.active = false; } var wait_server = function() { @@ -659,7 +671,7 @@ instance.web.redirect = function(url, wait) { } else { window.location = url; } -} +}; /** * Client action to reload the whole interface. @@ -941,7 +953,7 @@ instance.web.UserMenu = instance.web.Widget.extend({ on_menu_settings: function() { var self = this; if (!this.getParent().has_uncommitted_changes()) { - self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }, function(result) { + self.rpc("/web/action/load", { action_id: "base.action_res_users_my" }).done(function(result) { result.res_id = instance.session.uid; self.getParent().action_manager.do_action(result); }); @@ -972,6 +984,7 @@ instance.web.Client = instance.web.Widget.extend({ return instance.session.session_bind(this.origin).then(function() { var $e = $(QWeb.render(self._template, {})); self.replaceElement($e); + $e.openerpClass(); self.bind_events(); return self.show_common(); }); @@ -1148,10 +1161,10 @@ instance.web.WebClient = instance.web.Client.extend({ var self = this; var state = event.getState(true); if (!_.isEqual(this._current_state, state)) { - if(state.action === undefined && state.menu_id) { + if(!state.action && state.menu_id) { self.menu.has_been_loaded.done(function() { self.menu.do_reload().done(function() { - self.menu.menu_click(state.menu_id) + self.menu.menu_click(state.menu_id); }); }); } else { diff --git a/addons/web/static/src/js/corelib.js b/addons/web/static/src/js/corelib.js index 6ea0fd53372..06b3940f1e1 100644 --- a/addons/web/static/src/js/corelib.js +++ b/addons/web/static/src/js/corelib.js @@ -774,10 +774,10 @@ instance.web.Widget = instance.web.Class.extend(instance.web.PropertiesMixin, { } return false; }, - rpc: function(url, data, success, error) { - var def = $.Deferred().done(success).fail(error); + rpc: function(url, data, options) { + var def = $.Deferred(); var self = this; - instance.session.rpc(url, data).done(function() { + instance.session.rpc(url, data, options).done(function() { if (!self.isDestroyed()) def.resolve.apply(def, arguments); }).fail(function() { @@ -1231,12 +1231,14 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { * * @param {String} url RPC endpoint * @param {Object} params call parameters + * @param {Object} options additional options for rpc call * @param {Function} success_callback function to execute on RPC call success * @param {Function} error_callback function to execute on RPC call failure * @returns {jQuery.Deferred} jquery-provided ajax deferred */ - rpc: function(url, params) { + rpc: function(url, params, options) { var self = this; + options = options || {}; // url can be an $.ajax option object if (_.isString(url)) { url = { url: url }; @@ -1251,10 +1253,12 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { id: _.uniqueId('r') }; var deferred = $.Deferred(); - this.trigger('request', url, payload); + if (! options.shadow) + this.trigger('request', url, payload); var request = this.rpc_function(url, payload).done( function (response, textStatus, jqXHR) { - self.trigger('response', response); + if (! options.shadow) + self.trigger('response', response); if (!response.error) { if (url.url === '/web/session/eval_domain_and_context') { self.test_eval(params, response.result); @@ -1268,7 +1272,8 @@ instance.web.JsonRPC = instance.web.Class.extend(instance.web.PropertiesMixin, { } ).fail( function(jqXHR, textStatus, errorThrown) { - self.trigger('response_failed', jqXHR); + if (! options.shadow) + self.trigger('response_failed', jqXHR); var error = { code: -32098, message: "XmlHttpRequestError " + errorThrown, diff --git a/addons/web/static/src/js/coresetup.js b/addons/web/static/src/js/coresetup.js index 65b1874e093..a740827c1b5 100644 --- a/addons/web/static/src/js/coresetup.js +++ b/addons/web/static/src/js/coresetup.js @@ -22,9 +22,9 @@ instance.web.Session = instance.web.JsonRPC.extend( /** @lends instance.web.Sess this.name = instance._session_id; this.qweb_mutex = new $.Mutex(); }, - rpc: function(url, params) { + rpc: function(url, params, options) { params.session_id = this.session_id; - return this._super(url, params); + return this._super(url, params, options); }, /** * Setup a sessionm @@ -466,6 +466,16 @@ $.fn.getAttributes = function() { } return o; } +$.fn.openerpClass = function(additionalClass) { + // This plugin should be applied on top level elements + additionalClass = additionalClass || ''; + if (!!$.browser.msie) { + additionalClass += ' openerp_ie'; + } + return this.each(function() { + $(this).addClass('openerp ' + additionalClass); + }); +}; /** Jquery extentions */ $.Mutex = (function() { diff --git a/addons/web/static/src/js/data.js b/addons/web/static/src/js/data.js index 9162d0066c2..524a88bdc48 100644 --- a/addons/web/static/src/js/data.js +++ b/addons/web/static/src/js/data.js @@ -278,9 +278,10 @@ instance.web.Model = instance.web.Class.extend({ * @param {String} method name of the method to call * @param {Array} [args] positional arguments * @param {Object} [kwargs] keyword arguments + * @param {Object} [options] additional options for the rpc() method * @returns {jQuery.Deferred<>} call result */ - call: function (method, args, kwargs) { + call: function (method, args, kwargs, options) { args = args || []; kwargs = kwargs || {}; if (!_.isArray(args)) { @@ -294,7 +295,7 @@ instance.web.Model = instance.web.Class.extend({ method: method, args: args, kwargs: kwargs - }); + }, options); }, /** * Fetches a Query instance bound to this model, for searching diff --git a/addons/web/static/src/js/testing.js b/addons/web/static/src/js/testing.js new file mode 100644 index 00000000000..bf39f543734 --- /dev/null +++ b/addons/web/static/src/js/testing.js @@ -0,0 +1,393 @@ +// 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)).then(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 StackProto = { + execute: function (fn) { + var args = [].slice.call(arguments, 1); + // Warning: here be dragons + var i = 0, setups = this.setups, teardowns = this.teardowns; + var d = $.Deferred(); + + var succeeded, failed; + var success = function () { + succeeded = _.toArray(arguments); + return teardown(); + }; + var failure = function () { + // save first failure + if (!failed) { + failed = _.toArray(arguments); + } + // chain onto next teardown + return teardown(); + }; + + var setup = function () { + // if setup to execute + if (i < setups.length) { + var f = setups[i] || testing.noop; + $.when(f.apply(null, args)).then(function () { + ++i; + setup(); + }, failure); + } else { + $.when(fn.apply(null, args)).then(success, failure); + } + }; + var teardown = function () { + // if teardown to execute + if (i > 0) { + var f = teardowns[--i] || testing.noop; + $.when(f.apply(null, args)).then(teardown, failure); + } else { + if (failed) { + d.reject.apply(d, failed); + } else if (succeeded) { + d.resolve.apply(d, succeeded); + } else { + throw new Error("Didn't succeed or fail?"); + } + } + }; + setup(); + + return d; + }, + push: function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: this.setups.concat([setup]), + teardowns: this.teardowns.concat([teardown]) + }); + }, + unshift: function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: [setup].concat(this.setups), + teardowns: [teardown].concat(this.teardowns) + }); + } + }; + /** + * + * @param {Function} [setup] + * @param {Function} [teardown] + * @return {*} + */ + testing.Stack = function (setup, teardown) { + return _.extend(Object.create(StackProto), { + setups: setup ? [setup] : [], + teardowns: teardown ? [teardown] : [] + }); + }; + + var db = window['oe_db_info']; + testing.section = function (name, options, body) { + if (_.isFunction(options)) { + body = options; + options = {}; + } + _.defaults(options, { + setup: testing.noop, + teardown: testing.noop + }); + + QUnit.module(testing.current_module + '.' + name, {_oe: options}); + 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); + + // Serialize options for this precise test case + // WARNING: typo is from jquery, do not fix! + var env = QUnit.config.currentModuleTestEnviroment; + // section setup + // case setup + // test + // case teardown + // section teardown + var case_stack = testing.Stack() + .push(env._oe.setup, env._oe.teardown) + .push(options.setup, options.teardown); + var opts = _.defaults({}, options, env._oe); + // FIXME: if this test is ignored, will still query + if (opts.rpc === 'rpc' && !db) { + QUnit.config.autostart = false; + db = { + source: null, + supadmin: null, + password: null + }; + var $msg = $('
') + .append('

A test needs to clone a database

') + .append('

Please provide the source clone information

') + .append(' Source DB: ').append('').append('
') + .append(' DB Password: ').append('').append('
') + .append('Admin Password: ').append('').append('
') + .append('') + .submit(function (e) { + e.preventDefault(); + e.stopPropagation(); + db.source = $msg.find('input[name=source]').val(); + db.supadmin = $msg.find('input[name=supadmin]').val(); + db.password = $msg.find('input[name=password]').val(); + QUnit.start(); + $.unblockUI(); + }); + $.blockUI({ + message: $msg, + css: { + fontFamily: 'monospace', + textAlign: 'left', + whiteSpace: 'pre-wrap', + cursor: 'default' + } + }); + } + + QUnit.test(name, function () { + var instance; + if (!opts.dependencies) { + instance = openerp.init(module_deps); + } else { + // empty-but-specified dependencies actually allow running + // without loading any module into the instance + + // TODO: clean up this mess + var d = opts.dependencies.slice(); + // dependencies list should be in deps order, reverse to make + // loading order from last + d.reverse(); + var di = 0; + while (di < d.length) { + var m = /^web\.(\w+)$/.exec(d[di]); + if (m) { + d[di] = m[1]; + } + d.splice.apply(d, [di+1, 0].concat( + _(dependencies[d[di]]).reverse())); + ++di; + } + + instance = openerp.init("fuck your shit, don't load anything you cunt"); + _(d).chain() + .reverse() + .uniq() + .each(function (module) { + openerp.web[module](instance); + }); + } + if (_.isNumber(opts.asserts)) { + expect(opts.asserts); + } + + if (opts.templates) { + for(var i=0; i' + - '' + - '
    ' + - '
  1. ' + - '' + - '' + - '
  2. ' + - '
' + - '
' + - '' + - '

' + - '
' + - ''); - } - }; - var instance; - - module('Widget.proxy', mod); - test('(String)', function () { +openerp.testing.section('Widget.proxy', { + dependencies: ['web.corelib'] +}, function (test) { + test('(String)', function (instance) { var W = instance.web.Widget.extend({ exec: function () { this.executed = true; @@ -37,7 +12,7 @@ $(document).ready(function () { fn(); ok(w.executed, 'should execute the named method in the right context'); }); - test('(String)(*args)', function () { + test('(String)(*args)', function (instance) { var W = instance.web.Widget.extend({ exec: function (arg) { this.executed = arg; @@ -49,7 +24,7 @@ $(document).ready(function () { ok(w.executed, "should execute the named method in the right context"); equal(w.executed, 42, "should be passed the proxy's arguments"); }); - test('(String), include', function () { + test('(String), include', function (instance) { // the proxy function should handle methods being changed on the class // and should always proxy "by name", to the most recent one var W = instance.web.Widget.extend({ @@ -67,23 +42,43 @@ $(document).ready(function () { equal(w.executed, 2, "should be lazily resolved"); }); - test('(Function)', function () { + test('(Function)', function (instance) { var w = new (instance.web.Widget.extend({ })); var fn = w.proxy(function () { this.executed = true; }); fn(); ok(w.executed, "should set the function's context (like Function#bind)"); }); - test('(Function)(*args)', function () { + test('(Function)(*args)', function (instance) { var w = new (instance.web.Widget.extend({ })); var fn = w.proxy(function (arg) { this.executed = arg; }); fn(42); equal(w.executed, 42, "should be passed the proxy's arguments"); }); - - module('Widget.renderElement', mod); - test('no template, default', function () { +}); +openerp.testing.section('Widget.renderElement', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
    ' + + '
  1. ' + + '' + + '' + + '
  2. ' + + '
' + + '
' + + '' + + '

' + + '
' + + '
'); + } +}, function (test) { + test('no template, default', function (instance) { var w = new (instance.web.Widget.extend({ })); var $original = w.$el; @@ -98,7 +93,7 @@ $(document).ready(function () { equal(w.el.attributes.length, 0, "should not have generated any attribute"); ok(_.isEmpty(w.$el.html(), "should not have generated any content")); }); - test('no template, custom tag', function () { + test('no template, custom tag', function (instance) { var w = new (instance.web.Widget.extend({ tagName: 'ul' })); @@ -106,7 +101,7 @@ $(document).ready(function () { equal(w.el.nodeName, 'UL', "should have generated the custom element tag"); }); - test('no template, @id', function () { + test('no template, @id', function (instance) { var w = new (instance.web.Widget.extend({ id: 'foo' })); @@ -116,7 +111,7 @@ $(document).ready(function () { equal(w.$el.attr('id'), 'foo', "should have generated the id attribute"); equal(w.el.id, 'foo', "should also be available via property"); }); - test('no template, @className', function () { + test('no template, @className', function (instance) { var w = new (instance.web.Widget.extend({ className: 'oe_some_class' })); @@ -125,7 +120,7 @@ $(document).ready(function () { equal(w.el.className, 'oe_some_class', "should have the right property"); equal(w.$el.attr('class'), 'oe_some_class', "should have the right attribute"); }); - test('no template, bunch of attributes', function () { + test('no template, bunch of attributes', function (instance) { var w = new (instance.web.Widget.extend({ attributes: { 'id': 'some_id', @@ -152,7 +147,7 @@ $(document).ready(function () { equal(w.$el.attr('spoiler'), 'snape kills dumbledore'); }); - test('template', function () { + test('template', function (instance) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template' })); @@ -162,9 +157,41 @@ $(document).ready(function () { equal(w.$el.children().length, 5); equal(w.el.textContent, '01234'); }); - - module('Widget.$', mod); - test('basic-alias', function () { + test('repeated', { asserts: 4 }, function (instance, $fix) { + var w = new (instance.web.Widget.extend({ + template: 'test.widget.template-value' + })); + w.value = 42; + return w.appendTo($fix) + .done(function () { + equal($fix.find('p').text(), '42', "DOM fixture should contain initial value"); + equal(w.$el.text(), '42', "should set initial value"); + w.value = 36; + w.renderElement(); + equal($fix.find('p').text(), '36', "DOM fixture should use new value"); + equal(w.$el.text(), '36', "should set new value"); + }); + }); +}); +openerp.testing.section('Widget.$', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
    ' + + '
  1. ' + + '' + + '' + + '
  2. ' + + '
' + + '
' + + '
'); + } +}, function (test) { + test('basic-alias', function (instance) { var w = new (instance.web.Widget.extend({ template: 'test.widget.template' })); @@ -173,9 +200,26 @@ $(document).ready(function () { ok(w.$('li:eq(3)').is(w.$el.find('li:eq(3)')), "should do the same thing as calling find on the widget root"); }); - - module('Widget.events', mod); - test('delegate', function () { +}); +openerp.testing.section('Widget.events', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.qweb = new QWeb2.Engine(); + instance.web.qweb.add_template( + '' + + '' + + '
    ' + + '
  1. ' + + '' + + '' + + '
  2. ' + + '
' + + '
' + + '
'); + } +}, function (test) { + test('delegate', function (instance) { var a = []; var w = new (instance.web.Widget.extend({ template: 'test.widget.template', @@ -199,7 +243,7 @@ $(document).ready(function () { ok(a[i], "should pass test " + i); } }); - test('undelegate', function () { + test('undelegate', function (instance) { var clicked = false, newclicked = false; var w = new (instance.web.Widget.extend({ template: 'test.widget.template', @@ -218,22 +262,4 @@ $(document).ready(function () { ok(!clicked, "undelegate should unbind events delegated"); ok(newclicked, "undelegate should only unbind events it created"); }); - - module('Widget.renderElement', mod); - asyncTest('repeated', 4, function () { - var w = new (instance.web.Widget.extend({ - template: 'test.widget.template-value' - })); - w.value = 42; - w.appendTo($fix) - .always(start) - .done(function () { - equal($fix.find('p').text(), '42', "DOM fixture should contain initial value"); - equal(w.$el.text(), '42', "should set initial value"); - w.value = 36; - w.renderElement(); - equal($fix.find('p').text(), '36', "DOM fixture should use new value"); - equal(w.$el.text(), '36', "should set new value"); - }); - }); }); diff --git a/addons/web/static/test/class.js b/addons/web/static/test/class.js index faad4421e31..bb3fb7e12b9 100644 --- a/addons/web/static/test/class.js +++ b/addons/web/static/test/class.js @@ -1,30 +1,25 @@ -$(document).ready(function () { - var openerp; - module('web-class', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - } - }); - test('Basic class creation', function () { - var C = openerp.web.Class.extend({ +openerp.testing.section('class', { + dependencies: ['web.corelib'] +}, function (test) { + test('Basic class creation', function (instance) { + var C = instance.web.Class.extend({ foo: function () { return this.somevar; } }); - var instance = new C(); - instance.somevar = 3; + var i = new C(); + i.somevar = 3; - ok(instance instanceof C); - strictEqual(instance.foo(), 3); + ok(i instanceof C); + strictEqual(i.foo(), 3); }); - test('Class initialization', function () { - var C1 = openerp.web.Class.extend({ + test('Class initialization', function (instance) { + var C1 = instance.web.Class.extend({ init: function () { this.foo = 3; } }); - var C2 = openerp.web.Class.extend({ + var C2 = instance.web.Class.extend({ init: function (arg) { this.foo = arg; } @@ -36,8 +31,8 @@ $(document).ready(function () { strictEqual(i1.foo, 3); strictEqual(i2.foo, 42); }); - test('Inheritance', function () { - var C0 = openerp.web.Class.extend({ + test('Inheritance', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; } @@ -57,8 +52,8 @@ $(document).ready(function () { strictEqual(new C1().foo(), 2); strictEqual(new C2().foo(), 3); }); - test('In-place extension', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 3; }, @@ -83,8 +78,8 @@ $(document).ready(function () { strictEqual(new C0().foo(), 5); strictEqual(new C0().qux(), 5); }); - test('In-place extension and inheritance', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension and inheritance', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); @@ -101,24 +96,24 @@ $(document).ready(function () { strictEqual(new C1().foo(), 4); strictEqual(new C1().bar(), 2); }); - test('In-place extensions alter existing instances', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extensions alter existing instances', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); - var instance = new C0(); - strictEqual(instance.foo(), 1); - strictEqual(instance.bar(), 1); + var i = new C0(); + strictEqual(i.foo(), 1); + strictEqual(i.bar(), 1); C0.include({ foo: function () { return 2; }, bar: function () { return 2 + this._super(); } }); - strictEqual(instance.foo(), 2); - strictEqual(instance.bar(), 3); + strictEqual(i.foo(), 2); + strictEqual(i.bar(), 3); }); - test('In-place extension of subclassed types', function () { - var C0 = openerp.web.Class.extend({ + test('In-place extension of subclassed types', function (instance) { + var C0 = instance.web.Class.extend({ foo: function () { return 1; }, bar: function () { return 1; } }); @@ -126,13 +121,13 @@ $(document).ready(function () { foo: function () { return 1 + this._super(); }, bar: function () { return 1 + this._super(); } }); - var instance = new C1(); - strictEqual(instance.foo(), 2); + var i = new C1(); + strictEqual(i.foo(), 2); C0.include({ foo: function () { return 2; }, bar: function () { return 2 + this._super(); } }); - strictEqual(instance.foo(), 3); - strictEqual(instance.bar(), 4); + strictEqual(i.foo(), 3); + strictEqual(i.bar(), 4); }); }); diff --git a/addons/web/static/test/evals.js b/addons/web/static/test/evals.js index 757d98c5bcc..6eec615b199 100644 --- a/addons/web/static/test/evals.js +++ b/addons/web/static/test/evals.js @@ -1,18 +1,11 @@ -$(document).ready(function () { - var openerp; - - module("eval.contexts", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - } - }); - test('context_sequences', function () { +openerp.testing.section('eval.contexts', { + dependencies: ['web.coresetup'] +}, function (test) { + test('context_sequences', function (instance) { // Context n should have base evaluation context + all of contexts // 0..n-1 in its own evaluation context var active_id = 4; - var result = openerp.session.test_eval_contexts([ + var result = instance.session.test_eval_contexts([ { "__contexts": [ { @@ -55,8 +48,8 @@ $(document).ready(function () { record_id: active_id }); }); - test('non-literal_eval_contexts', function () { - var result = openerp.session.test_eval_contexts([{ + test('non-literal_eval_contexts', function (instance) { + var result = instance.session.test_eval_contexts([{ "__ref": "compound_context", "__contexts": [ {"__ref": "context", "__debug": "{'type':parent.type}", @@ -133,17 +126,15 @@ $(document).ready(function () { }]); deepEqual(result, {type: 'out_invoice'}); }); - module('eval.domains', { - setup: function () { - openerp = window.openerp.testing.instanceFor('coresetup'); - window.openerp.web.dates(openerp); - } - }); - test('current_date', function () { - var current_date = openerp.web.date_to_str(new Date()); - var result = openerp.session.test_eval_domains( +}); +openerp.testing.section('eval.contexts', { + dependencies: ['web.coresetup', 'web.dates'] +}, function (test) { + test('current_date', function (instance) { + var current_date = instance.web.date_to_str(new Date()); + var result = instance.session.test_eval_domains( [[],{"__ref":"domain","__debug":"[('name','>=',current_date),('name','<=',current_date)]","__id":"5dedcfc96648"}], - openerp.session.test_eval_get_context()); + instance.session.test_eval_get_context()); deepEqual(result, [ ['name', '>=', current_date], ['name', '<=', current_date] diff --git a/addons/web/static/test/form.js b/addons/web/static/test/form.js index aace926a96a..0c8107ce9bc 100644 --- a/addons/web/static/test/form.js +++ b/addons/web/static/test/form.js @@ -1,33 +1,21 @@ -$(document).ready(function () { - var openerp; - module("form.widget", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - window.openerp.web.form(openerp); - } - }); - test("compute_domain", function () { +openerp.testing.section('compute_domain', { + dependencies: ['web.form'] +}, function (test) { + test("basic", function (instance) { var fields = { 'a': {value: 3}, 'group_method': {value: 'line'}, 'select1': {value: 'day'}, 'rrule_type': {value: 'monthly'} }; - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['a', '=', 3]], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['group_method','!=','count']], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( [['select1','=','day'], ['rrule_type','=','monthly']], fields)); }); - test("compute_domain or", function () { + test("or", function (instance) { var web = { 'section_id': {value: null}, 'user_id': {value: null}, @@ -38,22 +26,22 @@ $(document).ready(function () { '|', ['user_id','=',3], ['member_ids', 'in', [3]]]; - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'section_id': {value: 42}}))); - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'user_id': {value: 3}}))); - ok(openerp.web.form.compute_domain(domain, _.extend( + ok(instance.web.form.compute_domain(domain, _.extend( {}, web, {'member_ids': {value: 3}}))); }); - test("compute_domain not", function () { + test("not", function (instance) { var fields = { 'a': {value: 5}, 'group_method': {value: 'line'} }; - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( ['!', ['a', '=', 3]], fields)); - ok(openerp.web.form.compute_domain( + ok(instance.web.form.compute_domain( ['!', ['group_method','=','count']], fields)); }); }); diff --git a/addons/web/static/test/formats.js b/addons/web/static/test/formats.js index 4fe07ff0a9c..4536501d71d 100644 --- a/addons/web/static/test/formats.js +++ b/addons/web/static/test/formats.js @@ -1,16 +1,8 @@ -$(document).ready(function () { - var openerp; - - module('server-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - } - }); - test('Parse server datetime', function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); +openerp.testing.section('server-formats', { + dependencies: ['web.coresetup', 'web.dates'] +}, function (test) { + test('Parse server datetime', function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); deepEqual( [date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), date.getUTCHours(), date.getUTCMinutes(), date.getUTCSeconds()], @@ -20,92 +12,86 @@ $(document).ready(function () { date.getHours(), date.getMinutes(), date.getSeconds()], [2009, 5 - 1, 4, 12 - (date.getTimezoneOffset() / 60), 34, 23]); - var date2 = openerp.web.str_to_datetime('2011-12-10 00:00:00'); + var date2 = instance.web.str_to_datetime('2011-12-10 00:00:00'); deepEqual( [date2.getUTCFullYear(), date2.getUTCMonth(), date2.getUTCDate(), date2.getUTCHours(), date2.getUTCMinutes(), date2.getUTCSeconds()], [2011, 12 - 1, 10, 0, 0, 0]); }); - test('Parse server date', function () { - var date = openerp.web.str_to_date("2009-05-04"); + test('Parse server date', function (instance) { + var date = instance.web.str_to_date("2009-05-04"); deepEqual( [date.getFullYear(), date.getMonth(), date.getDate()], [2009, 5 - 1, 4]); }); - test('Parse server time', function () { - var date = openerp.web.str_to_time("12:34:23"); + test('Parse server time', function (instance) { + var date = instance.web.str_to_time("12:34:23"); deepEqual( [date.getHours(), date.getMinutes(), date.getSeconds()], [12, 34, 23]); }); - - module('web-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - window.openerp.web.formats(openerp); - } - }); - test("format_datetime", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"datetime"}); +}); +openerp.testing.section('web-formats', { + dependencies: ['web.formats'] +}, function (test) { + test("format_datetime", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"datetime"}); equal(str, date.toString("MM/dd/yyyy HH:mm:ss")); }); - test("format_date", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"date"}); + test("format_date", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"date"}); equal(str, date.toString("MM/dd/yyyy")); }); - test("format_time", function () { - var date = openerp.web.str_to_datetime("2009-05-04 12:34:23"); - var str = openerp.web.format_value(date, {type:"time"}); + test("format_time", function (instance) { + var date = instance.web.str_to_datetime("2009-05-04 12:34:23"); + var str = instance.web.format_value(date, {type:"time"}); equal(str, date.toString("HH:mm:ss")); }); - test("format_float_time", function () { + test("format_float_time", function (instance) { strictEqual( - openerp.web.format_value(1.0, {type:'float', widget:'float_time'}), + instance.web.format_value(1.0, {type:'float', widget:'float_time'}), '01:00'); strictEqual( - openerp.web.format_value(0.9853, {type:'float', widget:'float_time'}), + instance.web.format_value(0.9853, {type:'float', widget:'float_time'}), '00:59'); strictEqual( - openerp.web.format_value(0.0085, {type:'float', widget:'float_time'}), + instance.web.format_value(0.0085, {type:'float', widget:'float_time'}), '00:01'); strictEqual( - openerp.web.format_value(-1.0, {type:'float', widget:'float_time'}), + instance.web.format_value(-1.0, {type:'float', widget:'float_time'}), '-01:00'); strictEqual( - openerp.web.format_value(-0.9853, {type:'float', widget:'float_time'}), + instance.web.format_value(-0.9853, {type:'float', widget:'float_time'}), '-00:59'); strictEqual( - openerp.web.format_value(-0.0085, {type:'float', widget:'float_time'}), + instance.web.format_value(-0.0085, {type:'float', widget:'float_time'}), '-00:01'); }); - test("format_float", function () { + test("format_float", function (instance) { var fl = 12.1234; - var str = openerp.web.format_value(fl, {type:"float"}); + var str = instance.web.format_value(fl, {type:"float"}); equal(str, "12.12"); - equal(openerp.web.format_value(12.02, {type: 'float'}), + equal(instance.web.format_value(12.02, {type: 'float'}), '12.02'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 3]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 3]}), '0.000'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 4]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 4]}), '0.0002'); - equal(openerp.web.format_value(0.0002, {type: 'float', digits: [1, 6]}), + equal(instance.web.format_value(0.0002, {type: 'float', digits: [1, 6]}), '0.000200'); - equal(openerp.web.format_value(1, {type: 'float', digits: [1, 6]}), + equal(instance.web.format_value(1, {type: 'float', digits: [1, 6]}), '1.000000'); - equal(openerp.web.format_value(1, {type: 'float'}), + equal(instance.web.format_value(1, {type: 'float'}), '1.00'); - equal(openerp.web.format_value(-11.25, {type: 'float'}), + equal(instance.web.format_value(-11.25, {type: 'float'}), "-11.25"); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(1111111.25, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(1111111.25, {type: 'float'}), "1111,11,1.25"); - openerp.web._t.database.parameters.grouping = [1, 0]; - equal(openerp.web.format_value(-11.25, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 0]; + equal(instance.web.format_value(-11.25, {type: 'float'}), "-1,1.25"); }); // test("parse_datetime", function () { @@ -123,29 +109,29 @@ $(document).ready(function () { // var res = openerp.web.parse_value(val.toString("HH:mm:ss"), {type:"time"}); // equal(val.toString("HH:mm:ss"), res.toString("HH:mm:ss")); // }); - test('parse_integer', function () { - var val = openerp.web.parse_value('123,456', {type: 'integer'}); + test('parse_integer', function (instance) { + var val = instance.web.parse_value('123,456', {type: 'integer'}); equal(val, 123456); - openerp.web._t.database.parameters.thousands_sep = '|'; - var val2 = openerp.web.parse_value('123|456', {type: 'integer'}); + instance.web._t.database.parameters.thousands_sep = '|'; + var val2 = instance.web.parse_value('123|456', {type: 'integer'}); equal(val2, 123456); }); - test("parse_float", function () { + test("parse_float", function (instance) { var str = "134,112.1234"; - var val = openerp.web.parse_value(str, {type:"float"}); + var val = instance.web.parse_value(str, {type:"float"}); equal(val, 134112.1234); var str = "-134,112.1234"; - var val = openerp.web.parse_value(str, {type:"float"}); + var val = instance.web.parse_value(str, {type:"float"}); equal(val, -134112.1234); - _.extend(openerp.web._t.database.parameters, { + _.extend(instance.web._t.database.parameters, { decimal_point: ',', thousands_sep: '.' }); - var val3 = openerp.web.parse_value('123.456,789', {type: 'float'}); + var val3 = instance.web.parse_value('123.456,789', {type: 'float'}); equal(val3, 123456.789); }); - test('intersperse', function () { - var g = openerp.web.intersperse; + test('intersperse', function (instance) { + var g = instance.web.intersperse; equal(g("", []), ""); equal(g("0", []), "0"); equal(g("012", []), "012"); @@ -176,60 +162,61 @@ $(document).ready(function () { equal(g("12345678", [3,3,3,3], '.'), '12.345.678'); equal(g("12345678", [3,0], '.'), '12.345.678'); }); - test('format_integer', function () { - openerp.web._t.database.parameters.grouping = [3, 3, 3, 3]; - equal(openerp.web.format_value(1000000, {type: 'integer'}), + test('format_integer', function (instance) { + instance.web._t.database.parameters.grouping = [3, 3, 3, 3]; + equal(instance.web.format_value(1000000, {type: 'integer'}), '1,000,000'); - openerp.web._t.database.parameters.grouping = [3, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'integer'}), + instance.web._t.database.parameters.grouping = [3, 2, -1]; + equal(instance.web.format_value(106500, {type: 'integer'}), '1,06,500'); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'integer'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(106500, {type: 'integer'}), '106,50,0'); }); - test('format_float', function () { - openerp.web._t.database.parameters.grouping = [3, 3, 3, 3]; - equal(openerp.web.format_value(1000000, {type: 'float'}), + test('format_float', function (instance) { + instance.web._t.database.parameters.grouping = [3, 3, 3, 3]; + equal(instance.web.format_value(1000000, {type: 'float'}), '1,000,000.00'); - openerp.web._t.database.parameters.grouping = [3, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'float'}), + instance.web._t.database.parameters.grouping = [3, 2, -1]; + equal(instance.web.format_value(106500, {type: 'float'}), '1,06,500.00'); - openerp.web._t.database.parameters.grouping = [1, 2, -1]; - equal(openerp.web.format_value(106500, {type: 'float'}), + instance.web._t.database.parameters.grouping = [1, 2, -1]; + equal(instance.web.format_value(106500, {type: 'float'}), '106,50,0.00'); - _.extend(openerp.web._t.database.parameters, { + _.extend(instance.web._t.database.parameters, { grouping: [3, 0], decimal_point: ',', thousands_sep: '.' }); - equal(openerp.web.format_value(6000, {type: 'float'}), + equal(instance.web.format_value(6000, {type: 'float'}), '6.000,00'); }); - module('custom-date-formats', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.dates(openerp); - window.openerp.web.formats(openerp); - } +}); +openerp.testing.section('web-formats', { + dependencies: ['web.formats'] +}, function (test) { + test('format stripper', function (instance) { + strictEqual(instance.web.strip_raw_chars('%a, %Y %b %d'), + '%a, %Y %b %d'); + strictEqual(instance.web.strip_raw_chars('%a, %Y.eko %bren %da'), + '%a, %Y. %b %d'); }); - test('format stripper', function () { - strictEqual(openerp.web.strip_raw_chars('%a, %Y %b %d'), '%a, %Y %b %d'); - strictEqual(openerp.web.strip_raw_chars('%a, %Y.eko %bren %da'), '%a, %Y. %b %d'); + test('ES date format', function (instance) { + instance.web._t.database.parameters.date_format = '%a, %Y %b %d'; + var date = instance.web.str_to_date("2009-05-04"); + strictEqual(instance.web.format_value(date, {type:"date"}), + 'Mon, 2009 May 04'); + strictEqual(instance.web.parse_value('Mon, 2009 May 04', {type: 'date'}), + '2009-05-04'); }); - test('ES date format', function () { - openerp.web._t.database.parameters.date_format = '%a, %Y %b %d'; - var date = openerp.web.str_to_date("2009-05-04"); - strictEqual(openerp.web.format_value(date, {type:"date"}), 'Mon, 2009 May 04'); - strictEqual(openerp.web.parse_value('Mon, 2009 May 04', {type: 'date'}), '2009-05-04'); - }); - test('extended ES date format', function () { - openerp.web._t.database.parameters.date_format = '%a, %Y.eko %bren %da'; - var date = openerp.web.str_to_date("2009-05-04"); - strictEqual(openerp.web.format_value(date, {type:"date"}), 'Mon, 2009. May 04'); - strictEqual(openerp.web.parse_value('Mon, 2009. May 04', {type: 'date'}), '2009-05-04'); + test('extended ES date format', function (instance) { + instance.web._t.database.parameters.date_format = '%a, %Y.eko %bren %da'; + var date = instance.web.str_to_date("2009-05-04"); + strictEqual(instance.web.format_value(date, {type:"date"}), + 'Mon, 2009. May 04'); + strictEqual(instance.web.parse_value('Mon, 2009. May 04', {type: 'date'}), + '2009-05-04'); }); }); diff --git a/addons/web/static/test/list-editable.js b/addons/web/static/test/list-editable.js index 282ef70f97b..b02e8abed96 100644 --- a/addons/web/static/test/list-editable.js +++ b/addons/web/static/test/list-editable.js @@ -1,16 +1,13 @@ -$(document).ready(function () { - var $fix = $('#qunit-fixture'); - - var instance; - var baseSetup = function () { - instance = openerp.testing.instanceFor('list_editable'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - }; - - +openerp.testing.section('editor', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + mock('test.model:create', function () { + return 42; + }); + } +}, function (test) { /** * * @param {String} name @@ -30,7 +27,7 @@ $(document).ready(function () { } /** - * @param {Array} fields + * @param {Array} [fields] * @return {Object} */ function makeFormView(fields) { @@ -67,46 +64,37 @@ $(document).ready(function () { }; } - module('editor', { - setup: baseSetup - }); - asyncTest('base-state', 2, function () { + test('base-state', {asserts: 2}, function (instance, $fix) { var e = new instance.web.list.Editor({ dataset: {ids: []}, edition_view: function () { return makeFormView(); } }); - e.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + return e.appendTo($fix) .done(function () { ok(!e.is_editing(), "should not be editing"); ok(e.form instanceof instance.web.FormView, "should use default form type"); }); }); - asyncTest('toggle-edition-save', 4, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; - instance.session.responses['/web/dataset/call_kw:read'] = function () { - return { result: [{ - id: 42, - a: false, - b: false, - c: false - }]}; - }; + test('toggle-edition-save', { + asserts: 4, + setup: function (instance, $s, mock) { + mock('test.model:read', function () { + return [{id: 42, a: false, b: false, c: false}]; + }); + } + }, function (instance, $fix) { var e = new instance.web.list.Editor({ - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ field('a'), field('b'), field('c') ]); } }); var counter = 0; - e.appendTo($fix) + return e.appendTo($fix) .then(function () { return e.edit({}, function () { ++counter; @@ -117,26 +105,21 @@ $(document).ready(function () { equal(counter, 3, "should have configured all fields"); return e.save(); }) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) .done(function (record) { ok(!e.is_editing(), "should have stopped editing"); equal(record.id, 42, "should have newly created id"); }) }); - asyncTest('toggle-edition-cancel', 2, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; + test('toggle-edition-cancel', { asserts: 2 }, function (instance, $fix) { var e = new instance.web.list.Editor({ - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ field('a'), field('b'), field('c') ]); } }); var counter = 0; - e.appendTo($fix) + return e.appendTo($fix) .then(function () { return e.edit({}, function () { ++counter; @@ -145,22 +128,20 @@ $(document).ready(function () { .then(function (form) { return e.cancel(); }) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) .done(function (record) { ok(!e.is_editing(), "should have stopped editing"); ok(!record.id, "should have no id"); }) }); - asyncTest('toggle-save-required', 2, function () { - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return { result: 42 }; - }; + test('toggle-save-required', { + asserts: 2, + fail_on_rejection: false + }, function (instance, $fix) { var e = new instance.web.list.Editor({ do_warn: function () { warnings++; }, - dataset: new instance.web.DataSetSearch(), + dataset: new instance.web.DataSetSearch(null, 'test.model'), prepends_on_create: function () { return false; }, edition_view: function () { return makeFormView([ @@ -169,7 +150,7 @@ $(document).ready(function () { }); var counter = 0; var warnings = 0; - e.appendTo($fix) + return e.appendTo($fix) .then(function () { return e.edit({}, function () { ++counter; @@ -178,78 +159,73 @@ $(document).ready(function () { .then(function (form) { return e.save(); }) - .always(start) .done(function () { ok(false, "cancel should not succeed"); }) .fail(function () { equal(warnings, 1, "should have been warned"); ok(e.is_editing(), "should have kept editing"); - }) + }); }); - - module('list-edition', { - setup: function () { - baseSetup(); - - var records = {}; - _.extend(instance.session.responses, { - '/web/view/load': function () { - return {result: { - type: 'tree', - fields: { - a: {type: 'char', string: "A"}, - b: {type: 'char', string: "B"}, - c: {type: 'char', string: "C"} - }, - arch: { - tag: 'tree', - attrs: {}, - children: [ - {tag: 'field', attrs: {name: 'a'}}, - {tag: 'field', attrs: {name: 'b'}}, - {tag: 'field', attrs: {name: 'c'}} - ] - } - }}; +}); +openerp.testing.section('list.edition', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + var records = {}; + mock('demo:create', function (args) { + records[42] = _.extend({}, args[0]); + return 42; + }); + mock('demo:read', function (args) { + var id = args[0][0]; + if (id in records) { + return [records[id]]; + } + return []; + }); + mock('/web/view/load', function () { + return { + type: 'tree', + fields: { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} }, - '/web/dataset/call_kw:create': function (params) { - records[42] = _.extend({}, params.params.args[0]); - return {result: 42}; - }, - '/web/dataset/call_kw:read': function (params) { - var id = params.params.args[0][0]; - if (id in records) { - return {result: [records[id]]}; - } - return {result: []}; + arch: { + tag: 'tree', + attrs: {}, + children: [ + {tag: 'field', attrs: {name: 'a'}}, + {tag: 'field', attrs: {name: 'b'}}, + {tag: 'field', attrs: {name: 'c'}} + ] } - }) - } - }); - asyncTest('newrecord', 6, function () { + }; + }); + } +}, function (test) { + test('newrecord', {asserts: 6}, function (instance, $fix, mock) { var got_defaults = false; - instance.session.responses['/web/dataset/call_kw:default_get'] = function (params) { - var fields = params.params.args[0]; + mock('demo:default_get', function (args) { + var fields = args[0]; deepEqual( fields, ['a', 'b', 'c'], "should ask defaults for all fields"); got_defaults = true; - return {result: { - a: "qux", - b: "quux" - }}; - }; + return { a: "qux", b: "quux" }; + }); var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); - l.appendTo($fix) + return l.appendTo($fix) .then(l.proxy('reload_content')) .then(function () { return l.start_edition(); }) - .always(start) .then(function () { ok(got_defaults, "should have fetched default values for form"); + return l.save_edition(); }) .then(function (result) { @@ -260,45 +236,39 @@ $(document).ready(function () { "should have used default values"); ok(!result.record.get('c'), "should have no value if there was no default"); - }) - .fail(function (e) { ok(false, e && e.message || e); }); - }); - - module('list-edition-events', { - setup: function () { - baseSetup(); - _.extend(instance.session.responses, { - '/web/view/load': function () { - return {result: { - type: 'tree', - fields: { - a: {type: 'char', string: "A"}, - b: {type: 'char', string: "B"}, - c: {type: 'char', string: "C"} - }, - arch: { - tag: 'tree', - attrs: {}, - children: [ - {tag: 'field', attrs: {name: 'a'}}, - {tag: 'field', attrs: {name: 'b'}}, - {tag: 'field', attrs: {name: 'c'}} - ] - } - }}; - }, - '/web/dataset/call_kw:read': function (params) { - return {result: [{ - id: 1, - a: 'foo', - b: 'bar', - c: 'baz' - }]}; - } }); - } }); - asyncTest('edition events', 4, function () { +}); +openerp.testing.section('list.edition.events', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + mock('demo:read', function () { + return [{ id: 1, a: 'foo', b: 'bar', c: 'baz' }]; + }); + mock('/web/view/load', function () { + return { + type: 'tree', + fields: { + a: {type: 'char', string: "A"}, + b: {type: 'char', string: "B"}, + c: {type: 'char', string: "C"} + }, + arch: { + tag: 'tree', + attrs: {}, + children: [ + {tag: 'field', attrs: {name: 'a'}}, + {tag: 'field', attrs: {name: 'b'}}, + {tag: 'field', attrs: {name: 'c'}} + ] + } + }; + }); + } +}, function (test) { + test('edition events', {asserts: 4}, function (instance, $fix) { var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var o = { counter: 0, @@ -306,9 +276,8 @@ $(document).ready(function () { }; var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); l.on('edit:before edit:after', o, o.onEvent); - l.appendTo($fix) + return l.appendTo($fix) .then(l.proxy('reload_content')) - .always(start) .then(function () { ok(l.options.editable, "should be editable"); equal(o.counter, 0, "should have seen no event yet"); @@ -317,11 +286,10 @@ $(document).ready(function () { .then(function () { ok(l.editor.is_editing(), "should be editing"); equal(o.counter, 2, "should have seen two edition events"); - }) - .fail(function (e) { ok(false, e && e.message); }); + }); }); - asyncTest('edition events: cancelling', 3, function () { + test('edition events: cancelling', {asserts: 3}, function (instance, $fix) { var edit_after = false; var ds = new instance.web.DataSetStatic(null, 'demo', null, [1]); var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); @@ -331,9 +299,8 @@ $(document).ready(function () { l.on('edit:after', {}, function () { edit_after = true; }); - l.appendTo($fix) + return l.appendTo($fix) .then(l.proxy('reload_content')) - .always(start) .then(function () { ok(l.options.editable, "should be editable"); return l.start_edition(); @@ -343,19 +310,18 @@ $(document).ready(function () { ok(!l.editor.is_editing(), "should not be editing"); ok(!edit_after, "should not have fired the edit:after event"); return $.when(); - }) - .fail(function (e) { ok(false, e && e.message || e); }); + }); }); +}); - module('list-edition-onwrite', { - setup: function () { - baseSetup(); - } - }); - - asyncTest('record-to-read', 4, function () { - instance.session.responses['/web/view/load'] = function () { - return {result: { +openerp.testing.section('list.edition.onwrite', { + dependencies: ['web.list_editable'], + rpc: 'mock', + templates: true, +}, function (test) { + test('record-to-read', {asserts: 4}, function (instance, $fix, mock) { + mock('/web/view/load', function () { + return { type: 'tree', fields: { a: {type: 'char', string: "A"} @@ -367,35 +333,27 @@ $(document).ready(function () { {tag: 'field', attrs: {name: 'a'}} ] } - }}; - }; - instance.session.responses['/web/dataset/call_kw:read'] = function (req) { - if (_.isEmpty(req.params.args[0])) { - return {result: []}; - } else if (_.isEqual(req.params.args[0], [1])) { - return {result: [ + }; + }); + mock('demo:read', function (args, kwargs) { + if (_.isEmpty(args[0])) { + return []; + } else if (_.isEqual(args[0], [1])) { + return [ {id: 1, a: 'some value'} - ]}; - } else if (_.isEqual(req.params.args[0], [42])) { - return {result: [ - {id: 42, a: 'foo'} - ]}; + ]; + } else if (_.isEqual(args[0], [42])) { + return [ {id: 42, a: 'foo'} ]; } - throw new Error(JSON.stringify(req.params)); - }; - instance.session.responses['/web/dataset/call_kw:default_get'] = function () { - return {result: {}}; - }; - instance.session.responses['/web/dataset/call_kw:create'] = function () { - return {result: 1}; - }; - instance.session.responses['/web/dataset/call_kw:on_write'] = function () { - return {result: [42]}; - }; + throw new Error(JSON.stringify(_.toArray(arguments))); + }); + mock('demo:default_get', function () { return {}; }); + mock('demo:create', function () { return 1; }); + mock('demo:on_write', function () { return [42]; }); var ds = new instance.web.DataSetStatic(null, 'demo', null, []); var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); - l.appendTo($fix) + return l.appendTo($fix) .then(l.proxy('reload_content')) .then(function () { return l.start_edition(); @@ -406,7 +364,6 @@ $(document).ready(function () { .then(function () { return l.save_edition(); }) - .always(function () { start(); }) .then(function () { strictEqual(ds.ids.length, 2, 'should have id of created + on_write'); @@ -418,8 +375,6 @@ $(document).ready(function () { strictEqual( $fix.find('tbody tr:eq(2)').css('color'), 'rgb(0, 0, 0)', 'should have default color applied'); - }, function (e) { - ok(false, e && e.message || e); }); }); }); diff --git a/addons/web/static/test/list-utils.js b/addons/web/static/test/list-utils.js index 6200ba788ce..84661138315 100644 --- a/addons/web/static/test/list-utils.js +++ b/addons/web/static/test/list-utils.js @@ -1,45 +1,34 @@ -$(document).ready(function () { - var openerp, - create = function (o) { - if (typeof Object.create === 'function') { - return Object.create(o); - } - function Cls() {} - Cls.prototype = o; - return new Cls; - }; - module('list-events', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); +openerp.testing.section('list.events', { + dependencies: ['web.list'] +}, function (test) { + var create = function (o) { + if (typeof Object.create === 'function') { + return Object.create(o); } - }); - test('Simple event triggering', function () { - var e = create(openerp.web.list.Events), passed = false; + function Cls() {} + Cls.prototype = o; + return new Cls; + }; + test('Simple event triggering', function (instance) { + var e = create(instance.web.list.Events), passed = false; e.bind('foo', function () { passed = true; }); e.trigger('foo'); ok(passed); }); - test('Bind all', function () { - var e = create(openerp.web.list.Events), event = null; + test('Bind all', function (instance) { + var e = create(instance.web.list.Events), event = null; e.bind(null, function (ev) { event = ev; }); e.trigger('foo'); strictEqual(event, 'foo'); }); - test('Propagate trigger params', function () { - var e = create(openerp.web.list.Events), p = false; + test('Propagate trigger params', function (instance) { + var e = create(instance.web.list.Events), p = false; e.bind(null, function (_, param) { p = param }); e.trigger('foo', true); strictEqual(p, true) }); - test('Bind multiple callbacks', function () { - var e = create(openerp.web.list.Events), count; + test('Bind multiple callbacks', function (instance) { + var e = create(instance.web.list.Events), count; e.bind('foo', function () { count++; }) .bind('bar', function () { count++; }) .bind(null, function () { count++; }) @@ -59,20 +48,20 @@ $(document).ready(function () { e.trigger('baz'); strictEqual(count, 3); }); - test('Mixin events', function () { - var cls = openerp.web.Class.extend({ + test('Mixin events', function (instance) { + var cls = instance.web.Class.extend({ method: function () { this.trigger('e'); } }); - cls.include(openerp.web.list.Events); - var instance = new cls, triggered = false; + cls.include(instance.web.list.Events); + var i = new cls, triggered = false; - instance.bind('e', function () { triggered = true; }); - instance.method(); + i.bind('e', function () { triggered = true; }); + i.method(); ok(triggered); }); - test('Unbind all handlers', function () { - var e = create(openerp.web.list.Events), passed = 0; + test('Unbind all handlers', function (instance) { + var e = create(instance.web.list.Events), passed = 0; e.bind('foo', function () { passed++; }); e.trigger('foo'); strictEqual(passed, 1); @@ -80,8 +69,8 @@ $(document).ready(function () { e.trigger('foo'); strictEqual(passed, 1); }); - test('Unbind one handler', function () { - var e = create(openerp.web.list.Events), p1 = 0, p2 = 0, + test('Unbind one handler', function (instance) { + var e = create(instance.web.list.Events), p1 = 0, p2 = 0, h1 = function () { p1++; }, h2 = function () { p2++; }; e.bind('foo', h1); e.bind('foo', h2); @@ -93,29 +82,20 @@ $(document).ready(function () { strictEqual(p1, 1); strictEqual(p2, 2); }); - - module('list-records', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('Basic record initialization', function () { - var r = new openerp.web.list.Record({qux: 3}); +}); +openerp.testing.section('list.records', { + dependencies: ['web.list'] +}, function (test) { + test('Basic record initialization', function (instance) { + var r = new instance.web.list.Record({qux: 3}); r.set('foo', 1); r.set('bar', 2); strictEqual(r.get('foo'), 1); strictEqual(r.get('bar'), 2); strictEqual(r.get('qux'), 3); }); - test('Change all the things', function () { - var r = new openerp.web.list.Record(), changed = false, field; + test('Change all the things', function (instance) { + var r = new instance.web.list.Record(), changed = false, field; r.bind('change', function () { changed = true; }); r.bind(null, function (e) { field = field || e.split(':')[1]}); r.set('foo', 1); @@ -123,8 +103,8 @@ $(document).ready(function () { ok(changed); strictEqual(field, 'foo'); }); - test('Change single field', function () { - var r = new openerp.web.list.Record(), changed = 0; + test('Change single field', function (instance) { + var r = new instance.web.list.Record(), changed = 0; r.bind('change:foo', function () { changed++; }); r.set('foo', 1); r.set('bar', 1); @@ -132,21 +112,12 @@ $(document).ready(function () { strictEqual(r.get('bar'), 1); strictEqual(changed, 1); }); - - module('list-collections', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('degenerate-fetch', function () { - var c = new openerp.web.list.Collection(); +}); +openerp.testing.section('list.collections', { + dependencies: ['web.list'] +}, function (test) { + test('degenerate-fetch', function (instance) { + var c = new instance.web.list.Collection(); strictEqual(c.length, 0); c.add({id: 1, value: 2}); c.add({id: 2, value: 3}); @@ -155,16 +126,16 @@ $(document).ready(function () { strictEqual(c.length, 4); var r = c.at(2), r2 = c.get(1); - ok(r instanceof openerp.web.list.Record); + ok(r instanceof instance.web.list.Record); strictEqual(r.get('id'), 3); strictEqual(r.get('value'), 5); - ok(r2 instanceof openerp.web.list.Record); + ok(r2 instanceof instance.web.list.Record); strictEqual(r2.get('id'), 1); strictEqual(r2.get('value'), 2); }); - test('degenerate-indexed-add', function () { - var c = new openerp.web.list.Collection([ + test('degenerate-indexed-add', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -175,8 +146,8 @@ $(document).ready(function () { strictEqual(c.at(1).get('value'), 55); strictEqual(c.at(3).get('value'), 20); }); - test('degenerate-remove', function () { - var c = new openerp.web.list.Collection([ + test('degenerate-remove', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -188,9 +159,9 @@ $(document).ready(function () { equal(c.get(2), undefined); strictEqual(c.at(1).get('value'), 20); }); - test('degenerate-remove-bound', function () { + test('degenerate-remove-bound', function (instance) { var changed = false, - c = new openerp.web.list.Collection([ {id: 1, value: 5} ]); + c = new instance.web.list.Collection([ {id: 1, value: 5} ]); c.bind('change', function () { changed = true; }); var record = c.get(1); c.remove(record); @@ -198,8 +169,8 @@ $(document).ready(function () { ok(!changed, 'removed records should not trigger events in their ' + 'parent collection'); }); - test('degenerate-reset', function () { - var event, obj, c = new openerp.web.list.Collection([ + test('degenerate-reset', function (instance) { + var event, obj, c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -218,9 +189,9 @@ $(document).ready(function () { strictEqual(c.length, 1); strictEqual(c.get(42).get('value'), 55); }); - test('degenerate-reset-bound', function () { + test('degenerate-reset-bound', function (instance) { var changed = false, - c = new openerp.web.list.Collection([ {id: 1, value: 5} ]); + c = new instance.web.list.Collection([ {id: 1, value: 5} ]); c.bind('change', function () { changed = true; }); var record = c.get(1); c.reset(); @@ -229,9 +200,9 @@ $(document).ready(function () { 'parent collection'); }); - test('degenerate-propagations', function () { + test('degenerate-propagations', function (instance) { var values = []; - var c = new openerp.web.list.Collection([ + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -244,8 +215,8 @@ $(document).ready(function () { c.get(3).set('value', 21); deepEqual(values, [6, 11, 21]); }); - test('BTree', function () { - var root = new openerp.web.list.Collection(), + test('BTree', function (instance) { + var root = new instance.web.list.Collection(), c = root.proxy('admin'), total = 0; c.add({id: 1, name: "Administrator", login: 'admin'}); @@ -260,8 +231,8 @@ $(document).ready(function () { c.at(1).set('wealth', 5); strictEqual(total, 47); }); - test('degenerate-successor', function () { - var root = new openerp.web.list.Collection([ + test('degenerate-successor', function (instance) { + var root = new instance.web.list.Collection([ {id: 1, value: 1}, {id: 2, value: 2}, {id: 3, value: 3}, @@ -282,8 +253,8 @@ $(document).ready(function () { root.at(3).attributes, "wraparound should have no effect if not succ(last_record)"); }); - test('successor', function () { - var root = new openerp.web.list.Collection(); + test('successor', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]); root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]); root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]); @@ -298,8 +269,8 @@ $(document).ready(function () { root.get(3).attributes, "should wraparound within a collection"); }); - test('degenerate-predecessor', function () { - var root = new openerp.web.list.Collection([ + test('degenerate-predecessor', function (instance) { + var root = new instance.web.list.Collection([ {id: 1, value: 1}, {id: 2, value: 2}, {id: 3, value: 3}, @@ -320,8 +291,8 @@ $(document).ready(function () { root.at(0).attributes, "wraparound should have no effect if not pred(first_record)"); }); - test('predecessor', function () { - var root = new openerp.web.list.Collection(); + test('predecessor', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('first').add([{id: 1, value: 1}, {id: 2, value: 2}]); root.proxy('second').add([{id: 3, value: 3}, {id: 4, value: 5}]); root.proxy('third').add([{id: 5, value: 8}, {id: 6, value: 13}]); @@ -336,21 +307,12 @@ $(document).ready(function () { root.get(4).attributes, "should wraparound within a collection"); }); - - module('list-hofs', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('each, degenerate', function () { - var c = new openerp.web.list.Collection([ +}); +openerp.testing.section('list.collections.hom', { + dependencies: ['web.list'] +}, function (test) { + test('each, degenerate', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -362,8 +324,8 @@ $(document).ready(function () { ids, [1, 2, 3], 'degenerate collections should be iterated in record order'); }); - test('each, deep', function () { - var root = new openerp.web.list.Collection(), + test('each, deep', function (instance) { + var root = new instance.web.list.Collection(), ids = []; root.proxy('foo').add([ {id: 1, value: 5}, @@ -382,8 +344,8 @@ $(document).ready(function () { ids, [1, 2, 3, 10, 20, 30], 'tree collections should be deeply iterated'); }); - test('map, degenerate', function () { - var c = new openerp.web.list.Collection([ + test('map, degenerate', function (instance) { + var c = new instance.web.list.Collection([ {id: 1, value: 5}, {id: 2, value: 10}, {id: 3, value: 20} @@ -395,8 +357,8 @@ $(document).ready(function () { ids, [1, 2, 3], 'degenerate collections should be iterated in record order'); }); - test('map, deep', function () { - var root = new openerp.web.list.Collection(); + test('map, deep', function (instance) { + var root = new instance.web.list.Collection(); root.proxy('foo').add([ {id: 1, value: 5}, {id: 2, value: 10}, @@ -414,29 +376,20 @@ $(document).ready(function () { ids, [1, 2, 3, 10, 20, 30], 'tree collections should be deeply iterated'); }); - - module("list-weirds", { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.chrome(openerp); - // views loader stuff - window.openerp.web.data(openerp); - window.openerp.web.views(openerp); - window.openerp.web.list(openerp); - } - }); - test('set-from-noid', function () { - var root = new openerp.web.list.Collection(); +}); +openerp.testing.section('list.collection.weirdoes', { + dependencies: ['web.list'] +}, function (test) { + test('set-from-noid', function (instance) { + var root = new instance.web.list.Collection(); root.add({v: 3}); root.at(0).set('id', 42); var record = root.get(42); equal(root.length, 1); equal(record.get('v'), 3, "should have fetched the original record"); }); - test('set-from-previd', function () { - var root = new openerp.web.list.Collection(); + test('set-from-previd', function (instance) { + var root = new instance.web.list.Collection(); root.add({id: 1, v: 2}); root.get(1).set('id', 42); var record = root.get(42); diff --git a/addons/web/static/test/list.js b/addons/web/static/test/list.js index 63cc16a5022..22c33ef104a 100644 --- a/addons/web/static/test/list.js +++ b/addons/web/static/test/list.js @@ -1,19 +1,11 @@ -$(document).ready(function () { - var instance; - var $fix = $('#qunit-fixture'); - - module('list.buttons', { - setup: function () { - instance = openerp.testing.instanceFor('list'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - } - }); - asyncTest('record-deletion', 2, function () { - instance.session.responses['/web/view/load'] = function () { - return {result: { +openerp.testing.section('list.buttons', { + dependencies: ['web.list', 'web.form'], + rpc: 'mock', + templates: true +}, function (test) { + test('record-deletion', {asserts: 2}, function (instance, $fix, mock) { + mock('/web/view/load', function () { + return { type: 'tree', fields: { a: {type: 'char', string: "A"} @@ -26,26 +18,25 @@ $(document).ready(function () { {tag: 'button', attrs: {type: 'object', name: 'foo'}} ] } - }}; - }; - instance.session.responses['/web/dataset/call_kw:read'] = function (req) { - var args = req.params.args[0]; - if (_.isEqual(args, [1, 2, 3])) { - return {result: [ + }; + }); + mock('demo:read', function (args, kwargs) { + if (_.isEqual(args[0], [1, 2, 3])) { + return [ {id: 1, a: 'foo'}, {id: 2, a: 'bar'}, {id: 3, a: 'baz'} - ]}; - } else if (_.isEqual(args, [2])) { + ]; + } else if (_.isEqual(args[0], [2])) { // button action virtually removed record - return {result: []}; + return []; } - throw new Error(JSON.stringify(req.params)); - }; - instance.session.responses['/web/dataset/call_button'] = function () { - return {result: false}; - }; + throw new Error(JSON.stringify(_.toArray(arguments))); + }); + mock('/web/dataset/call_button', function () { return false; }); var ds = new instance.web.DataSetStatic(null, 'demo', null, [1, 2, 3]); - var l = new instance.web.ListView({}, ds, false, {editable: 'top'}); - l.appendTo($fix) + var l = new instance.web.ListView({ + do_action: openerp.testing.noop + }, ds, false, {editable: 'top'}); + return l.appendTo($fix) .then(l.proxy('reload_content')) .then(function () { var d = $.Deferred(); @@ -55,14 +46,11 @@ $(document).ready(function () { $fix.find('table tbody tr:eq(1) button').click(); return d.promise(); }) - .always(function () { start(); }) .then(function () { strictEqual(l.records.length, 2, "should have 2 records left"); strictEqual($fix.find('table tbody tr[data-id]').length, 2, "should have 2 rows left"); - }, function (e) { - ok(false, e && e.message || e); }); }); }); diff --git a/addons/web/static/test/mutex.js b/addons/web/static/test/mutex.js new file mode 100644 index 00000000000..e88911b5b45 --- /dev/null +++ b/addons/web/static/test/mutex.js @@ -0,0 +1,60 @@ +openerp.testing.section('mutex', { + dependencies: ['web.coresetup'], + setup: function (instance) { + } +}, function (test) { + test('simpleScheduling', function (instance) { + var m = new $.Mutex(); + var def1 = $.Deferred(); + var def2 = $.Deferred(); + var p1 = m.exec(function() { return def1; }); + var p2 = m.exec(function() { return def2; }); + equal(p1.state(), "pending"); + equal(p2.state(), "pending"); + def1.resolve(); + equal(p1.state(), "resolved"); + equal(p2.state(), "pending"); + def2.resolve(); + equal(p1.state(), "resolved"); + equal(p2.state(), "resolved"); + }); + test('simpleScheduling2', function (instance) { + var m = new $.Mutex(); + var def1 = $.Deferred(); + var def2 = $.Deferred(); + var p1 = m.exec(function() { return def1; }); + var p2 = m.exec(function() { return def2; }); + equal(p1.state(), "pending"); + equal(p2.state(), "pending"); + def2.resolve(); + equal(p1.state(), "pending"); + equal(p2.state(), "pending"); + def1.resolve(); + equal(p1.state(), "resolved"); + equal(p2.state(), "resolved"); + }); + test('reject', function (instance) { + var m = new $.Mutex(); + var def1 = $.Deferred(); + var def2 = $.Deferred(); + var def3 = $.Deferred(); + var p1 = m.exec(function() {return def1;}); + var p2 = m.exec(function() {return def2;}); + var p3 = m.exec(function() {return def3;}); + equal(p1.state(), "pending"); + equal(p2.state(), "pending"); + equal(p3.state(), "pending"); + def1.resolve(); + equal(p1.state(), "resolved"); + equal(p2.state(), "pending"); + equal(p3.state(), "pending"); + def2.reject(); + equal(p1.state(), "resolved"); + equal(p2.state(), "rejected"); + equal(p3.state(), "pending"); + def3.resolve(); + equal(p1.state(), "resolved"); + equal(p2.state(), "rejected"); + equal(p3.state(), "resolved"); + }); +}); diff --git a/addons/web/static/test/registry.js b/addons/web/static/test/registry.js index ca689a351e0..2ad69a541bc 100644 --- a/addons/web/static/test/registry.js +++ b/addons/web/static/test/registry.js @@ -1,58 +1,55 @@ -$(document).ready(function () { - var openerp; - module('Registry', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - openerp.web.Foo = {}; - openerp.web.Bar = {}; - openerp.web.Foo2 = {}; - } - }); - test('key set', function () { - var reg = new openerp.web.Registry(); +openerp.testing.section('registry', { + dependencies: ['web.corelib'], + setup: function (instance) { + instance.web.Foo = {}; + instance.web.Bar = {}; + instance.web.Foo2 = {}; + } +}, function (test) { + test('key set', function (instance) { + var reg = new instance.web.Registry(); - reg.add('foo', 'openerp.web.Foo') - .add('bar', 'openerp.web.Bar'); - strictEqual(reg.get_object('bar'), openerp.web.Bar); + reg.add('foo', 'instance.web.Foo') + .add('bar', 'instance.web.Bar'); + strictEqual(reg.get_object('bar'), instance.web.Bar); }); - test('extension', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('extension', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); - var reg2 = reg.extend({ 'foo': 'openerp.web.Foo2' }); - strictEqual(reg.get_object('foo'), openerp.web.Foo); - strictEqual(reg2.get_object('foo'), openerp.web.Foo2); + var reg2 = reg.extend({ 'foo': 'instance.web.Foo2' }); + strictEqual(reg.get_object('foo'), instance.web.Foo); + strictEqual(reg2.get_object('foo'), instance.web.Foo2); }); - test('remain-linked', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('remain-linked', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); var reg2 = reg.extend(); - reg.add('foo2', 'openerp.web.Foo2'); - strictEqual(reg.get_object('foo2'), openerp.web.Foo2); - strictEqual(reg2.get_object('foo2'), openerp.web.Foo2); + reg.add('foo2', 'instance.web.Foo2'); + strictEqual(reg.get_object('foo2'), instance.web.Foo2); + strictEqual(reg2.get_object('foo2'), instance.web.Foo2); }); - test('multiget', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('multiget', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); strictEqual(reg.get_any(['qux', 'grault', 'bar', 'foo']), - openerp.web.Bar); + instance.web.Bar); }); - test('extended-multiget', function () { - var reg = new openerp.web.Registry({ - foo: 'openerp.web.Foo', - bar: 'openerp.web.Bar' + test('extended-multiget', function (instance) { + var reg = new instance.web.Registry({ + foo: 'instance.web.Foo', + bar: 'instance.web.Bar' }); var reg2 = reg.extend(); strictEqual(reg2.get_any(['qux', 'grault', 'bar', 'foo']), - openerp.web.Bar); + instance.web.Bar); }); }); diff --git a/addons/web/static/test/rpc.js b/addons/web/static/test/rpc.js index be7cbce9f41..85f562ef225 100644 --- a/addons/web/static/test/rpc.js +++ b/addons/web/static/test/rpc.js @@ -1,16 +1,8 @@ -$(document).ready(function () { - var openerp; - - module('Misordered resolution management', { - setup: function () { - openerp = window.openerp.init([]); - window.openerp.web.corelib(openerp); - window.openerp.web.coresetup(openerp); - window.openerp.web.data(openerp); - } - }); - test('Resolve all correctly ordered, sync', function () { - var dm = new openerp.web.DropMisordered(), flag = false; +openerp.testing.section('misordered resolution managemeng', { + dependencies: ['web.data'] +}, function (test) { + test('Resolve all correctly ordered, sync', function (instance) { + var dm = new instance.web.DropMisordered(), flag = false; var d1 = $.Deferred(), d2 = $.Deferred(), r1 = dm.add(d1), r2 = dm.add(d2); @@ -23,8 +15,8 @@ $(document).ready(function () { ok(flag); }); - test("Don't resolve mis-ordered, sync", function () { - var dm = new openerp.web.DropMisordered(), + test("Don't resolve mis-ordered, sync", function (instance) { + var dm = new instance.web.DropMisordered(), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -44,8 +36,8 @@ $(document).ready(function () { ok(done2); ok(!fail2); }); - test('Fail mis-ordered flag, sync', function () { - var dm = new openerp.web.DropMisordered(true), + test('Fail mis-ordered flag, sync', function (instance) { + var dm = new instance.web.DropMisordered(true), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -66,8 +58,8 @@ $(document).ready(function () { ok(!fail2); }); - asyncTest('Resolve all correctly ordered, async', 1, function () { - var dm = new openerp.web.DropMisordered(); + test('Resolve all correctly ordered, async', {asserts: 1}, function (instance) { + var dm = new instance.web.DropMisordered(); var d1 = $.Deferred(), d2 = $.Deferred(), r1 = dm.add(d1), r2 = dm.add(d2); @@ -75,13 +67,12 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 100); setTimeout(function () { d2.resolve(); }, 200); - $.when(r1, r2).done(function () { - start(); + return $.when(r1, r2).done(function () { ok(true); }); }); - asyncTest("Don't resolve mis-ordered, async", 4, function () { - var dm = new openerp.web.DropMisordered(), + test("Don't resolve mis-ordered, async", {asserts: 4}, function (instance) { + var dm = new instance.web.DropMisordered(), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -94,18 +85,20 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 200); setTimeout(function () { d2.resolve(); }, 100); + var done = $.Deferred(); setTimeout(function () { - start(); // d1 is in limbo ok(!done1); ok(!fail1); // d2 is resolved ok(done2); ok(!fail2); + done.resolve(); }, 400); + return $.when(d1, d2, done); }); - asyncTest('Fail mis-ordered flag, async', 4, function () { - var dm = new openerp.web.DropMisordered(true), + test('Fail mis-ordered flag, async', {asserts: 4}, function (instance) { + var dm = new instance.web.DropMisordered(true), done1 = false, done2 = false, fail1 = false, fail2 = false; @@ -118,6 +111,7 @@ $(document).ready(function () { setTimeout(function () { d1.resolve(); }, 200); setTimeout(function () { d2.resolve(); }, 100); + var done = $.Deferred(); setTimeout(function () { start(); // d1 is failed @@ -126,6 +120,8 @@ $(document).ready(function () { // d2 is resolved ok(done2); ok(!fail2); + done.resolve(); }, 400); + return $.when(d1, d2, done) }); }); diff --git a/addons/web/static/test/search.js b/addons/web/static/test/search.js index 3ef4d6840e2..20534eff76a 100644 --- a/addons/web/static/test/search.js +++ b/addons/web/static/test/search.js @@ -1,11 +1,7 @@ -$(document).ready(function () { - var instance; - module('query', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - } - }); - test('Adding a facet to the query creates a facet and a value', function () { +openerp.testing.section('query', { + dependencies: ['web.search'] +}, function (test) { + test('Adding a facet to the query creates a facet and a value', function (instance) { var query = new instance.web.search.SearchQuery; var field = {}; query.add({ @@ -19,7 +15,7 @@ $(document).ready(function () { equal(facet.get('field'), field); deepEqual(facet.get('values'), [{label: 'Value', value: 3}]); }); - test('Adding two facets', function () { + test('Adding two facets', function (instance) { var query = new instance.web.search.SearchQuery; query.add([ { category: 'Foo', field: {}, values: [{label: 'Value', value: 3}] }, @@ -30,7 +26,7 @@ $(document).ready(function () { equal(query.at(0).values.length, 1); equal(query.at(1).values.length, 1); }); - test('If a facet already exists, add values to it', function () { + test('If a facet already exists, add values to it', function (instance) { var query = new instance.web.search.SearchQuery; var field = {}; query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -43,7 +39,7 @@ $(document).ready(function () { {label: 'V2', value: 1} ]); }); - test('Facet being implicitly changed should trigger change, not add', function () { + test('Facet being implicitly changed should trigger change, not add', function (instance) { var query = new instance.web.search.SearchQuery; var field = {}, added = false, changed = false; query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -54,7 +50,7 @@ $(document).ready(function () { ok(!added, "query.add adding values to a facet should not trigger an add"); ok(changed, "query.add adding values to a facet should not trigger a change"); }); - test('Toggling a facet, value which does not exist should add it', function () { + test('Toggling a facet, value which does not exist should add it', function (instance) { var query = new instance.web.search.SearchQuery; var field = {}; query.toggle({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -65,7 +61,7 @@ $(document).ready(function () { deepEqual(facet.get('values'), [{label: 'V1', value: 0}], "Facet's value should match input"); }); - test('Toggling a facet which exists with a value which does not should add the value to the facet', function () { + test('Toggling a facet which exists with a value which does not should add the value to the facet', function (instance) { var field = {}; var query = new instance.web.search.SearchQuery; query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -79,7 +75,7 @@ $(document).ready(function () { {label: 'V2', value: 1} ]); }); - test('Toggling a facet which exists with a value which does as well should remove the value from the facet', function () { + test('Toggling a facet which exists with a value which does as well should remove the value from the facet', function (instance) { var field = {}; var query = new instance.web.search.SearchQuery; query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -94,7 +90,7 @@ $(document).ready(function () { {label: 'V1', value: 0} ]); }); - test('Toggling off the last value of a facet should remove the facet', function () { + test('Toggling off the last value of a facet should remove the facet', function (instance) { var field = {}; var query = new instance.web.search.SearchQuery; query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -103,7 +99,7 @@ $(document).ready(function () { equal(query.length, 0, 'Should have removed the facet'); }); - test('Intermediate emptiness should not remove the facet', function () { + test('Intermediate emptiness should not remove the facet', function (instance) { var field = {}; var query = new instance.web.search.SearchQuery; query.add({category: 'A', field: field, values: [{label: 'V1', value: 0}]}); @@ -121,7 +117,7 @@ $(document).ready(function () { ]); }); - test('Reseting with multiple facets should still work to load defaults', function () { + test('Reseting with multiple facets should still work to load defaults', function (instance) { var query = new instance.web.search.SearchQuery; var field = {}; query.reset([ @@ -135,78 +131,75 @@ $(document).ready(function () { {label: 'V2', value: 1} ]) }); +}); - module('defaults', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - } - }); - - /** - * Builds a basic search view with a single "dummy" field. The dummy - * extends `instance.web.search.Field`, it does not add any (class) - * attributes beyond what is provided through ``dummy_widget_attributes``. - * - * The view is returned un-started, it is the caller's role to start it - * (or use DOM-insertion methods to start it indirectly). - * - * @param [dummy_widget_attributes={}] - * @param [defaults={}] - * @return {instance.web.SearchView} - */ - function makeSearchView(dummy_widget_attributes, defaults) { - instance.web.search.fields.add( - 'dummy', 'instance.dummy.DummyWidget'); - instance.dummy = {}; - instance.dummy.DummyWidget = instance.web.search.Field.extend( - dummy_widget_attributes || {}); - if (!('/web/searchview/load' in instance.session.responses)) { - instance.session.responses['/web/searchview/load'] = function () { - return {result: {fields_view: { - type: 'search', - fields: { - dummy: {type: 'char', string: "Dummy"} - }, - arch: { - tag: 'search', - attrs: {}, - children: [{ - tag: 'field', - attrs: { - name: 'dummy', - widget: 'dummy' - }, - children: [] - }] - } - }}}; - }; - } - instance.session.responses['/web/searchview/get_filters'] = function () { - return {result: []}; +/** + * Builds a basic search view with a single "dummy" field. The dummy + * extends `instance.web.search.Field`, it does not add any (class) + * attributes beyond what is provided through ``dummy_widget_attributes``. + * + * The view is returned un-started, it is the caller's role to start it + * (or use DOM-insertion methods to start it indirectly). + * + * @param instance + * @param [dummy_widget_attributes={}] + * @param [defaults={}] + * @return {instance.web.SearchView} + */ +var makeSearchView = function (instance, dummy_widget_attributes, defaults) { + instance.web.search.fields.add( + 'dummy', 'instance.dummy.DummyWidget'); + instance.dummy = {}; + instance.dummy.DummyWidget = instance.web.search.Field.extend( + dummy_widget_attributes || {}); + if (!('/web/searchview/load' in instance.session.responses)) { + instance.session.responses['/web/searchview/load'] = function () { + return {fields_view: { + type: 'search', + fields: { + dummy: {type: 'char', string: "Dummy"} + }, + arch: { + tag: 'search', + attrs: {}, + children: [{ + tag: 'field', + attrs: { + name: 'dummy', + widget: 'dummy' + }, + children: [] + }] + } + }}; }; - instance.session.responses['/web/searchview/fields_get'] = function () { - return {result: {fields: { - dummy: {type: 'char', string: 'Dummy'} - }}}; - }; - - var dataset = {model: 'dummy.model', get_context: function () { return {}; }}; - var view = new instance.web.SearchView(null, dataset, false, defaults); - var self = this; - view.on('invalid_search', self, function () { - ok(false, JSON.stringify([].slice(arguments))); - }); - return view; } - asyncTest('calling', 2, function () { + instance.session.responses['/web/searchview/get_filters'] = function () { + return []; + }; + instance.session.responses['/web/searchview/fields_get'] = function () { + return {fields: { + dummy: {type: 'char', string: 'Dummy'} + }}; + }; + + var dataset = {model: 'dummy.model', get_context: function () { return {}; }}; + var view = new instance.web.SearchView(null, dataset, false, defaults); + var self = this; + view.on('invalid_search', self, function () { + ok(false, JSON.stringify([].slice(arguments))); + }); + return view; +}; +openerp.testing.section('defaults', { + dependencies: ['web.search'], + rpc: 'mock', + templates: true, +}, function (test) { + test('calling', {asserts: 2}, function (instance, $s) { var defaults_called = false; - var view = makeSearchView({ + var view = makeSearchView(instance, { facet_for_defaults: function (defaults) { defaults_called = true; return $.when({ @@ -216,9 +209,7 @@ $(document).ready(function () { }); } }, {dummy: 42}); - view.appendTo($('#qunit-fixture')) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($s) .done(function () { ok(defaults_called, "should have called defaults"); deepEqual( @@ -227,7 +218,7 @@ $(document).ready(function () { "should have generated a facet with the default value"); }); }); - asyncTest('FilterGroup', 3, function () { + test('FilterGroup', {asserts: 3}, function (instance) { var view = {inputs: [], query: {on: function () {}}}; var filter_a = new instance.web.search.Filter( {attrs: {name: 'a'}}, view); @@ -235,9 +226,7 @@ $(document).ready(function () { {attrs: {name: 'b'}}, view); var group = new instance.web.search.FilterGroup( [filter_a, filter_b], view); - group.facet_for_defaults({a: true, b: true}) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + return group.facet_for_defaults({a: true, b: true}) .done(function (facet) { var model = facet; if (!(model instanceof instance.web.search.Facet)) { @@ -249,13 +238,11 @@ $(document).ready(function () { strictEqual(values.at(1).get('value'), filter_b); }); }); - asyncTest('Field', 4, function () { + test('Field', {asserts: 4}, function (instance) { var view = {inputs: []}; var f = new instance.web.search.Field( {attrs: {string: 'Dummy', name: 'dummy'}}, {}, view); - f.facet_for_defaults({dummy: 42}) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + return f.facet_for_defaults({dummy: 42}) .done(function (facet) { var model = facet; if (!(model instanceof instance.web.search.Facet)) { @@ -275,15 +262,13 @@ $(document).ready(function () { "facet value should match provided default"); }); }); - asyncTest('Selection: valid value', 4, function () { + test('Selection: valid value', {asserts: 4}, function (instance) { var view = {inputs: []}; var f = new instance.web.search.SelectionField( {attrs: {name: 'dummy', string: 'Dummy'}}, {selection: [[1, "Foo"], [2, "Bar"], [3, "Baz"], [4, "Qux"]]}, view); - f.facet_for_defaults({dummy: 3}) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + return f.facet_for_defaults({dummy: 3}) .done(function (facet) { var model = facet; if (!(model instanceof instance.web.search.Facet)) { @@ -303,36 +288,28 @@ $(document).ready(function () { "facet value should match provided default's selection"); }); }); - asyncTest('Selection: invalid value', 1, function () { + test('Selection: invalid value', {asserts: 1}, function (instance) { var view = {inputs: []}; var f = new instance.web.search.SelectionField( {attrs: {name: 'dummy', string: 'Dummy'}}, {selection: [[1, "Foo"], [2, "Bar"], [3, "Baz"], [4, "Qux"]]}, view); - f.facet_for_defaults({dummy: 42}) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + return f.facet_for_defaults({dummy: 42}) .done(function (facet) { ok(!facet, "an invalid value should result in a not-facet"); }); }); - asyncTest("M2O default: value", 7, function () { + test("M2O default: value", {asserts: 5}, function (instance, $s, mock) { var view = {inputs: []}, id = 4; var f = new instance.web.search.ManyToOneField( {attrs: {name: 'dummy', string: 'Dummy'}}, {relation: 'dummy.model.name'}, view); - instance.session.responses['/web/dataset/call_kw'] = function (req) { - equal(req.params.method, 'name_get', - "m2o should resolve default id"); - equal(req.params.model, f.attrs.relation, - "query model should match m2o relation"); - equal(req.params.args[0], id); - return {result: [[id, "DumDumDum"]]}; - }; - f.facet_for_defaults({dummy: id}) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + mock('dummy.model.name:name_get', function (args) { + equal(args[0], id); + return [[id, "DumDumDum"]]; + }); + return f.facet_for_defaults({dummy: id}) .done(function (facet) { var model = facet; if (!(model instanceof instance.web.search.Facet)) { @@ -352,34 +329,26 @@ $(document).ready(function () { "facet value should match provided default's selection"); }); }); - asyncTest("M2O default: value", 1, function () { + test("M2O default: value", {asserts: 1}, function (instance, $s, mock) { var view = {inputs: []}, id = 4; var f = new instance.web.search.ManyToOneField( {attrs: {name: 'dummy', string: 'Dummy'}}, {relation: 'dummy.model.name'}, view); - instance.session.responses['/web/dataset/call_kw'] = function (req) { - return {result: []}; - }; - f.facet_for_defaults({dummy: id}) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + mock('dummy.model.name:name_get', function () { return [] }); + return f.facet_for_defaults({dummy: id}) .done(function (facet) { ok(!facet, "an invalid m2o default should yield a non-facet"); }); }); - - module('completions', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - } - }); - asyncTest('calling', 4, function () { - var view = makeSearchView({ +}); +openerp.testing.section('completions', { + dependencies: ['web.search'], + rpc: 'mock', + templates: true +}, function (test) { + test('calling', {asserts: 4}, function (instance, $s) { + var view = makeSearchView(instance, { complete: function () { return $.when({ label: "Dummy", @@ -391,10 +360,11 @@ $(document).ready(function () { }); } }); - view.appendTo($('#qunit-fixture')) - .done(function () { + var done = $.Deferred(); + view.appendTo($s) + .then(function () { view.complete_global_search({term: "dum"}, function (completions) { - start(); + done.resolve(); equal(completions.length, 1, "should have a single completion"); var completion = completions[0]; equal(completion.label, "Dummy", @@ -405,9 +375,10 @@ $(document).ready(function () { [{label: 'dummy', value: 42}], "should have provided values"); }); - }); + }).fail(function () { done.reject.apply(done, arguments); }); + return done; }); - asyncTest('facet selection', 2, function () { + test('facet selection', {asserts: 2}, function (instance, $s) { var completion = { label: "Dummy", facet: { @@ -421,10 +392,8 @@ $(document).ready(function () { } }; - var view = makeSearchView({}); - view.appendTo($('#qunit-fixture')) - .always(start) - .fail(function (error) { ok(false, error.message); }) + var view = makeSearchView(instance); + return view.appendTo($s) .done(function () { view.select_completion( {preventDefault: function () {}}, @@ -436,7 +405,7 @@ $(document).ready(function () { "should have the right facet in the query"); }); }); - asyncTest('facet selection: new value existing facet', 3, function () { + test('facet selection: new value existing facet', {asserts: 3}, function (instance, $s) { var field = { get_domain: openerp.testing.noop, get_context: openerp.testing.noop, @@ -451,10 +420,8 @@ $(document).ready(function () { } }; - var view = makeSearchView({}); - view.appendTo($('#qunit-fixture')) - .always(start) - .fail(function (error) { ok(false, error.message); }) + var view = makeSearchView(instance); + return view.appendTo($s) .done(function () { view.query.add({field: field, category: 'Dummy', values: [{label: 'previous', value: 41}]}); @@ -470,23 +437,19 @@ $(document).ready(function () { "should have added selected value to old one"); }); }); - asyncTest('Field', 1, function () { + test('Field', {asserts: 1}, function (instance) { var view = {inputs: []}; var f = new instance.web.search.Field({attrs: {}}, {}, view); - f.complete('foo') - .always(start) - .fail(function (error) { ok(false, error.message); }) + return f.complete('foo') .done(function (completions) { ok(_(completions).isEmpty(), "field should not provide any completion"); }); }); - asyncTest('CharField', 6, function () { + test('CharField', {asserts: 6}, function (instance) { var view = {inputs: []}; var f = new instance.web.search.CharField( {attrs: {string: "Dummy"}}, {}, view); - f.complete('foo<') - .always(start) - .fail(function (error) { ok(false, error.message); }) + return f.complete('foo<') .done(function (completions) { equal(completions.length, 1, "should provide a single completion"); var c = completions[0]; @@ -503,15 +466,13 @@ $(document).ready(function () { "facet should have single value using completion item"); }); }); - asyncTest('Selection: match found', 14, function () { + test('Selection: match found', {asserts: 14}, function (instance) { var view = {inputs: []}; var f = new instance.web.search.SelectionField( {attrs: {string: "Dummy"}}, {selection: [[1, "Foo"], [2, "Bar"], [3, "Baz"], [4, "Bazador"]]}, view); - f.complete("ba") - .always(start) - .fail(function (error) { ok(false, error.message); }) + return f.complete("ba") .done(function (completions) { equal(completions.length, 4, "should provide two completions and a section title"); @@ -536,20 +497,18 @@ $(document).ready(function () { deepEqual(c3.facet.values, [{label: "Bazador", value: 4}]); }); }); - asyncTest('Selection: no match', 1, function () { + test('Selection: no match', {asserts: 1}, function (instance) { var view = {inputs: []}; var f = new instance.web.search.SelectionField( {attrs: {string: "Dummy"}}, {selection: [[1, "Foo"], [2, "Bar"], [3, "Baz"], [4, "Bazador"]]}, view); - f.complete("qux") - .always(start) - .fail(function (error) { ok(false, error.message); }) + return f.complete("qux") .done(function (completions) { ok(!completions, "if no value matches the needle, no completion shall be provided"); }); }); - asyncTest('Date', 6, function () { + test('Date', {asserts: 6}, function (instance) { instance.web._t.database.parameters = { date_format: '%Y-%m-%d', time_format: '%H:%M:%S' @@ -557,9 +516,7 @@ $(document).ready(function () { var view = {inputs: []}; var f = new instance.web.search.DateField( {attrs: {string: "Dummy"}}, {type: 'datetime'}, view); - f.complete('2012-05-21T21:21:21') - .always(start) - .fail(function (error) { ok(false, error.message); }) + return f.complete('2012-05-21T21:21:21') .done(function (completions) { equal(completions.length, 1, "should provide a single completion"); var c = completions[0]; @@ -573,21 +530,17 @@ $(document).ready(function () { new Date(2012, 4, 21, 21, 21, 21).getTime()); }); }); - asyncTest("M2O", 15, function () { - instance.session.responses['/web/dataset/call_kw'] = function (req) { - equal(req.params.method, "name_search"); - equal(req.params.model, "dummy.model"); - deepEqual(req.params.args, []); - deepEqual(req.params.kwargs.name, 'bob'); - return {result: [[42, "choice 1"], [43, "choice @"]]} - }; + test("M2O", {asserts: 13}, function (instance, $s, mock) { + mock('dummy.model:name_search', function (args, kwargs) { + deepEqual(args, []); + strictEqual(kwargs.name, 'bob'); + return [[42, "choice 1"], [43, "choice @"]]; + }); var view = {inputs: []}; var f = new instance.web.search.ManyToOneField( {attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view); - f.complete("bob") - .always(start) - .fail(function (error) { ok(false, error.message); }) + return f.complete("bob") .done(function (c) { equal(c.length, 3, "should return results + title"); var title = c[0]; @@ -607,38 +560,29 @@ $(document).ready(function () { deepEqual(f2.values.toJSON(), [{label: 'choice @', value: 43}]); }); }); - asyncTest("M2O no match", 5, function () { - instance.session.responses['/web/dataset/call_kw'] = function (req) { - equal(req.params.method, "name_search"); - equal(req.params.model, "dummy.model"); - deepEqual(req.params.args, []); - deepEqual(req.params.kwargs.name, 'bob'); - return {result: []} - }; + test("M2O no match", {asserts: 3}, function (instance, $s, mock) { + mock('dummy.model:name_search', function (args, kwargs) { + deepEqual(args, []); + strictEqual(kwargs.name, 'bob'); + return []; + }); var view = {inputs: []}; var f = new instance.web.search.ManyToOneField( {attrs: {string: 'Dummy'}}, {relation: 'dummy.model'}, view); - f.complete("bob") - .always(start) - .fail(function (error) { ok(false, error.message); }) + return f.complete("bob") .done(function (c) { ok(!c, "no match should yield no completion"); }); }); - - module('search-serialization', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - } - }); - asyncTest('No facet, no call', 6, function () { +}); +openerp.testing.section('search-serialization', { + dependencies: ['web.search'], + rpc: 'mock', + templates: true +}, function (test) { + test('No facet, no call', {asserts: 6}, function (instance, $s) { var got_domain = false, got_context = false, got_groupby = false; - var $fix = $('#qunit-fixture'); - var view = makeSearchView({ + var view = makeSearchView(instance, { get_domain: function () { got_domain = true; return null; @@ -654,11 +598,9 @@ $(document).ready(function () { }); var ds, cs, gs; view.on('search_data', this, function (d, c, g) { - ds = d, cs = c, gs = g; + ds = d; cs = c; gs = g; }); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($s) .done(function () { view.do_search(); ok(!got_domain, "no facet, should not have fetched domain"); @@ -671,10 +613,9 @@ $(document).ready(function () { ok(_(gs).isEmpty(), "groupby list should be empty"); }) }); - asyncTest('London, calling', 8, function () { + test('London, calling', {asserts: 8}, function (instance, $fix) { var got_domain = false, got_context = false, got_groupby = false; - var $fix = $('#qunit-fixture'); - var view = makeSearchView({ + var view = makeSearchView(instance, { get_domain: function (facet) { equal(facet.get('category'), "Dummy"); deepEqual(facet.values.toJSON(), [{label: "42", value: 42}]); @@ -692,11 +633,9 @@ $(document).ready(function () { }, {dummy: 42}); var ds, cs, gs; view.on('search_data', this, function (d, c, g) { - ds = d, cs = c, gs = g; + ds = d; cs = c; gs = g; }); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($fix) .done(function () { view.do_search(); ok(got_domain, "should have fetched domain"); @@ -709,9 +648,8 @@ $(document).ready(function () { ok(_(gs).isEmpty(), "groupby list should be empty"); }) }); - asyncTest('Generate domains', 1, function () { - var $fix = $('#qunit-fixture'); - var view = makeSearchView({ + test('Generate domains', {asserts: 1}, function (instance, $fix) { + var view = makeSearchView(instance, { get_domain: function (facet) { return facet.values.map(function (value) { return ['win', '4', value.get('value')]; @@ -720,9 +658,7 @@ $(document).ready(function () { }, {dummy: 42}); var ds; view.on('search_data', this, function (d) { ds = d; }); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($fix) .done(function () { view.do_search(); deepEqual(ds, [[['win', '4', 42]]], @@ -730,7 +666,9 @@ $(document).ready(function () { }); }); - test('Field single value, default domain & context', function () { + test('Field single value, default domain & context', { + rpc: false + }, function (instance) { var f = new instance.web.search.Field({}, {name: 'foo'}, {inputs: []}); var facet = new instance.web.search.Facet({ field: f, @@ -742,7 +680,9 @@ $(document).ready(function () { equal(f.get_context(facet), null, "default field context is null"); }); - test('Field multiple values, default domain & context', function () { + test('Field multiple values, default domain & context', { + rpc: false + }, function (instance) { var f = new instance.web.search.Field({}, {name: 'foo'}, {inputs: []}); var facet = new instance.web.search.Facet({ field: f, @@ -763,7 +703,9 @@ $(document).ready(function () { equal(f.get_context(facet), null, "default field context is null"); }); - test('Field single value, custom domain & context', function () { + test('Field single value, custom domain & context', { + rpc: false + }, function (instance) { var f = new instance.web.search.Field({attrs:{ context: "{'bob': self}", filter_domain: "[['edmund', 'is', self]]" @@ -793,7 +735,9 @@ $(document).ready(function () { self: "great" }, "evaluation context should hold facet value as self"); }); - test("M2O default", function () { + test("M2O default", { + rpc: false + }, function (instance) { var f = new instance.web.search.ManyToOneField( {}, {name: 'foo'}, {inputs: []}); var facet = new instance.web.search.Facet({ @@ -806,7 +750,9 @@ $(document).ready(function () { deepEqual(f.get_context(facet), {default_foo: 42}, "m2o should use value as context default"); }); - test("M2O default multiple values", function () { + test("M2O default multiple values", { + rpc: false + }, function (instance) { var f = new instance.web.search.ManyToOneField( {}, {name: 'foo'}, {inputs: []}); var facet = new instance.web.search.Facet({ @@ -823,7 +769,9 @@ $(document).ready(function () { equal(f.get_context(facet), null, "m2o should not have default context in case of multiple values"); }); - test("M2O custom operator", function () { + test("M2O custom operator", { + rpc: false + }, function (instance) { var f = new instance.web.search.ManyToOneField( {attrs: {operator: 'boos'}}, {name: 'foo'}, {inputs: []}); var facet = new instance.web.search.Facet({ @@ -836,7 +784,9 @@ $(document).ready(function () { deepEqual(f.get_context(facet), {default_foo: 42}, "m2o should use value as context default"); }); - test("M2O custom domain & context", function () { + test("M2O custom domain & context", { + rpc: false + }, function (instance) { var f = new instance.web.search.ManyToOneField({attrs: { context: "{'whee': self}", filter_domain: "[['filter', 'is', self]]" @@ -862,7 +812,7 @@ $(document).ready(function () { }, "custom context's self should be label"); }); - asyncTest('FilterGroup', 6, function () { + test('FilterGroup', {asserts: 6}, function (instance) { var view = {inputs: [], query: {on: function () {}}}; var filter_a = new instance.web.search.Filter( {attrs: {name: 'a', context: 'c1', domain: 'd1'}}, view); @@ -872,9 +822,7 @@ $(document).ready(function () { {attrs: {name: 'c', context: 'c3', domain: 'd3'}}, view); var group = new instance.web.search.FilterGroup( [filter_a, filter_b, filter_c], view); - group.facet_for_defaults({a: true, c: true}) - .always(start) - .fail(function (error) { ok(false, error && error.message); }) + return group.facet_for_defaults({a: true, c: true}) .done(function (facet) { var model = facet; if (!(model instanceof instance.web.search.Facet)) { @@ -897,19 +845,14 @@ $(document).ready(function () { ok(!context.get_eval_context(), "context should have no evaluation context"); }); }); - - module('removal', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - } - }); - asyncTest('clear button', function () { - var $fix = $('#qunit-fixture'); - var view = makeSearchView({ +}); +openerp.testing.section('removal', { + dependencies: ['web.search'], + rpc: 'mock', + templates: true +}, function (test) { + test('clear button', {asserts: 2}, function (instance, $fix) { + var view = makeSearchView(instance, { facet_for_defaults: function (defaults) { return $.when({ field: this, @@ -918,31 +861,22 @@ $(document).ready(function () { }); } }, {dummy: 42}); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($fix) .done(function () { equal(view.query.length, 1, "view should have default facet"); $fix.find('.oe_searchview_clear').click(); equal(view.query.length, 0, "cleared view should not have any facet"); }); }); - - module('drawer', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - } - }); - asyncTest('is-drawn', 2, function () { - var view = makeSearchView(); - var $fix = $('#qunit-fixture'); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) +}); +openerp.testing.section('drawer', { + dependencies: ['web.search'], + rpc: 'mock', + templates: true +}, function (test) { + test('is-drawn', {asserts: 2}, function (instance, $fix) { + var view = makeSearchView(instance); + return view.appendTo($fix) .done(function () { ok($fix.find('.oe_searchview_filters').length, "filters drawer control has been drawn"); @@ -950,50 +884,44 @@ $(document).ready(function () { "filters advanced search has been drawn"); }); }); - - module('filters', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance, { - '/web/searchview/load': function () { - // view with a single group of filters - return {result: {fields_view: { - type: 'search', - fields: {}, - arch: { - tag: 'search', - attrs: {}, - children: [{ - tag: 'filter', - attrs: { string: "Foo1", domain: [ ['foo', '=', '1'] ] }, - children: [] - }, { - tag: 'filter', - attrs: { - name: 'foo2', - string: "Foo2", - domain: [ ['foo', '=', '2'] ] }, - children: [] - }, { - tag: 'filter', - attrs: { string: "Foo3", domain: [ ['foo', '=', '3'] ] }, - children: [] - }] - } - }}}; +}); +openerp.testing.section('filters', { + dependencies: ['web.search'], + rpc: 'mock', + templates: true, + setup: function (instance, $s, mock) { + mock('/web/searchview/load', function () { + // view with a single group of filters + return {fields_view: { + type: 'search', + fields: {}, + arch: { + tag: 'search', + attrs: {}, + children: [{ + tag: 'filter', + attrs: { string: "Foo1", domain: [ ['foo', '=', '1'] ] }, + children: [] + }, { + tag: 'filter', + attrs: { + name: 'foo2', + string: "Foo2", + domain: [ ['foo', '=', '2'] ] }, + children: [] + }, { + tag: 'filter', + attrs: { string: "Foo3", domain: [ ['foo', '=', '3'] ] }, + children: [] + }] } - }); - } - }); - asyncTest('drawn', 3, function () { - var view = makeSearchView(); - var $fix = $('#qunit-fixture'); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + }}; + }); + } +}, function (test) { + test('drawn', {asserts: 3}, function (instance, $fix) { + var view = makeSearchView(instance); + return view.appendTo($fix) .done(function () { var $fs = $fix.find('.oe_searchview_filters ul'); // 3 filters, 1 filtergroup, 1 custom filters widget, @@ -1006,12 +934,9 @@ $(document).ready(function () { "Text content of first filter option should match filter string"); }); }); - asyncTest('click adding from empty query', 4, function () { - var view = makeSearchView(); - var $fix = $('#qunit-fixture'); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + test('click adding from empty query', {asserts: 4}, function (instance, $fix) { + var view = makeSearchView(instance); + return view.appendTo($fix) .done(function () { var $fs = $fix.find('.oe_searchview_filters ul'); $fs.children(':eq(2)').trigger('click'); @@ -1025,12 +950,9 @@ $(document).ready(function () { "value should be third filter"); }); }); - asyncTest('click adding from existing query', 4, function () { - var view = makeSearchView({}, {foo2: true}); - var $fix = $('#qunit-fixture'); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + test('click adding from existing query', {asserts: 4}, function (instance, $fix) { + var view = makeSearchView(instance, {}, {foo2: true}); + return view.appendTo($fix) .done(function () { var $fs = $fix.find('.oe_searchview_filters ul'); $fs.children(':eq(2)').trigger('click'); @@ -1045,16 +967,13 @@ $(document).ready(function () { "second value should be clicked filter"); }); }); - asyncTest('click removing from query', 4, function () { + test('click removing from query', {asserts: 4}, function (instance, $fix) { var calls = 0; - var view = makeSearchView({}, {foo2: true}); + var view = makeSearchView(instance, {}, {foo2: true}); view.on('search_data', null, function () { ++calls; }); - var $fix = $('#qunit-fixture'); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($fix) .done(function () { var $fs = $fix.find('.oe_searchview_filters ul'); // sanity check @@ -1065,29 +984,19 @@ $(document).ready(function () { strictEqual(calls, 1, "one search should have been triggered"); }); }); +}); +openerp.testing.section('saved_filters', { + dependencies: ['web.search'], + rpc: 'mock', + templates: true +}, function (test) { + test('checkboxing', {asserts: 6}, function (instance, $fix, mock) { + var view = makeSearchView(instance); + mock('/web/searchview/get_filters', function () { + return [{ name: "filter name", user_id: 42 }]; + }); - module('saved_filters', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - } - }); - asyncTest('checkboxing', 6, function () { - var view = makeSearchView(); - instance.session.responses['/web/searchview/get_filters'] = function () { - return {result: [{ - name: "filter name", - user_id: 42 - }]}; - }; - var $fix = $('#qunit-fixture'); - - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($fix) .done(function () { var $row = $fix.find('.oe_searchview_custom li:first').click(); @@ -1104,44 +1013,31 @@ $(document).ready(function () { "should have no value set"); }) }); - asyncTest('removal', 1, function () { - var view = makeSearchView(); - instance.session.responses['/web/searchview/get_filters'] = function () { - return {result: [{ - name: "filter name", - user_id: 42 - }]}; - }; - var $fix = $('#qunit-fixture'); + test('removal', {asserts: 1}, function (instance, $fix, mock) { + var view = makeSearchView(instance); + mock('/web/searchview/get_filters', function () { + return [{ name: "filter name", user_id: 42 }]; + }); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($fix) .done(function () { var $row = $fix.find('.oe_searchview_custom li:first').click(); view.query.remove(view.query.at(0)); ok(!$row.hasClass('oe_selected'), "should not be checked anymore"); - }) + }); }); +}); +openerp.testing.section('advanced', { + dependencies: ['web.search'], + rpc: 'mock', + templates: true +}, function (test) { + test('single-advanced', {asserts: 6}, function (instance, $fix) { + var view = makeSearchView(instance); - module('advanced', { - setup: function () { - instance = openerp.testing.instanceFor('search'); - - openerp.testing.loadTemplate(instance); - - openerp.testing.mockifyRPC(instance); - } - }); - asyncTest('single-advanced', 6, function () { - var view = makeSearchView(); - var $fix = $('#qunit-fixture'); - - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($fix) .done(function () { var $advanced = $fix.find('.oe_searchview_advanced'); // open advanced search (not actually useful) @@ -1174,13 +1070,10 @@ $(document).ready(function () { "advanced search facet should return proposed domain"); }); }); - asyncTest('multiple-advanced', 3, function () { - var view = makeSearchView(); - var $fix = $('#qunit-fixture'); + test('multiple-advanced', {asserts: 3}, function (instance, $fix) { + var view = makeSearchView(instance); - view.appendTo($fix) - .always(start) - .fail(function (error) { ok(false, error.message); }) + return view.appendTo($fix) .done(function () { var $advanced = $fix.find('.oe_searchview_advanced'); // open advanced search (not actually useful) diff --git a/addons/web/static/test/test.html b/addons/web/static/test/test.html deleted file mode 100644 index 275db0f37a7..00000000000 --- a/addons/web/static/test/test.html +++ /dev/null @@ -1,63 +0,0 @@ - - - - - OpenERP Web Test Suite - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
-
- - - - - - - - - - - - - diff --git a/addons/web/static/test/testing.js b/addons/web/static/test/testing.js index 1be8e94e74b..5bd008ad7ec 100644 --- a/addons/web/static/test/testing.js +++ b/addons/web/static/test/testing.js @@ -1,97 +1,244 @@ -// 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'], +openerp.testing.section('testing.stack', function (test) { + // I heard you like tests, so I put tests in your testing infrastructure, + // so you can test what you test + var reject = function () { + // utility function, rejects a success + var args = _.toArray(arguments); + return $.Deferred(function (d) { + d.reject.apply(d, ["unexpected success"].concat(args)); + }); }; + test('direct, value, success', {asserts: 1}, function () { + var s = openerp.testing.Stack(); + return s.execute(function () { + return 42; + }).then(function (val) { + strictEqual(val, 42, "should return the handler value"); + }); + }); + test('direct, deferred, success', {asserts: 1}, function () { + var s = openerp.testing.Stack(); + return s.execute(function () { + return $.when(42); + }).then(function (val) { + strictEqual(val, 42, "should return the handler value") + }); + }); + test('direct, deferred, failure', {asserts: 1}, function () { + var s = openerp.testing.Stack(); + return s.execute(function () { + return $.Deferred(function (d) { + d.reject("failed"); + }); + }).then(reject, function (f) { + strictEqual(f, "failed", "should propagate failure"); + return $.when(); + }); + }); - 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]; + test('successful setup', {asserts: 2}, function () { + var setup_done = false; + var s = openerp.testing.Stack(); + return s.push(function () { + return $.Deferred(function (d) { + setTimeout(function () { + setup_done = true; + d.resolve(2); + }, 50); + }); + }).execute(function () { + return 42; + }).then(function (val) { + ok(setup_done, "should have executed setup"); + strictEqual(val, 42, "should return executed function value (not setup)"); + }); + }); + test('successful teardown', {asserts: 2}, function () { + var teardown = false; + var s = openerp.testing.Stack(); + return s.push(null, function () { + return $.Deferred(function (d) { + setTimeout(function () { + teardown = true; + d.resolve(2); + }, 50); + }); + }).execute(function () { + return 42; + }).then(function (val) { + ok(teardown, "should have executed teardown"); + strictEqual(val, 42, "should return executed function value (not setup)"); + }); + }); + test('successful setup and teardown', {asserts: 3}, function () { + var setup = false, teardown = false; + var s = openerp.testing.Stack(); + return s.push(function () { + return $.Deferred(function (d) { + setTimeout(function () { + setup = true; + d.resolve(2); + }, 50); + }); + }, function () { + return $.Deferred(function (d) { + setTimeout(function () { + teardown = true; + d.resolve(2); + }, 50); + }); + }).execute(function () { + return 42; + }).then(function (val) { + ok(setup, "should have executed setup"); + ok(teardown, "should have executed teardown"); + strictEqual(val, 42, "should return executed function value (not setup)"); + }); + }); - 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 = []; } + test('multiple setups', {asserts: 2}, function () { + var setups = 0; + var s = openerp.testing.Stack(); + return s.push(function () { + setups++; + }).push(function () { + setups++; + }).push(function () { + setups++; + }).push(function () { + setups++; + }).execute(function () { + return 42; + }).then(function (val) { + strictEqual(setups, 4, "should have executed all setups of stack"); + strictEqual(val, 42); + }); + }); + test('multiple teardowns', {asserts: 2}, function () { + var teardowns = 0; + var s = openerp.testing.Stack(); + return s.push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + }).execute(function () { + return 42; + }).then(function (val) { + strictEqual(teardowns, 4, "should have executed all teardowns of stack"); + strictEqual(val, 42); + }); + }); + test('holes in setups', {asserts: 2}, function () { + var setups = []; + var s = openerp.testing.Stack(); + return s.push(function () { + setups.push(0); + }).push().push().push(function () { + setups.push(3); + }).push(function () { + setups.push(4); + }).push().push(function () { + setups.push(6); + }).execute(function () { + return 42; + }).then(function (val) { + deepEqual(setups, [0, 3, 4, 6], + "should have executed setups in correct order"); + strictEqual(val, 42); + }); + }); + test('holes in teardowns', {asserts: 2}, function () { + var teardowns = []; + var s = openerp.testing.Stack(); + return s.push(null, function () { + teardowns.push(0); + }).push().push().push(null, function () { + teardowns.push(3); + }).push(null, function () { + teardowns.push(4); + }).push().push(null, function () { + teardowns.push(6); + }).execute(function () { + return 42; + }).then(function (val) { + deepEqual(teardowns, [6, 4, 3, 0], + "should have executed teardowns in correct order"); + strictEqual(val, 42); + }); - 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); - } - } -})(); + test('failed setup', {asserts: 5}, function () { + var setup, teardown, teardown2, code; + return openerp.testing.Stack().push(function () { + setup = true; + }, function () { + teardown = true; + }).push(function () { + return $.Deferred().reject("Fail!"); + }, function () { + teardown2 = true; + }).execute(function () { + code = true; + return 42; + }).then(reject, function (m) { + ok(setup, "should have executed first setup function"); + ok(teardown, "should have executed first teardown function"); + ok(!teardown2, "should not have executed second teardown function"); + strictEqual(m, "Fail!", "should return setup failure message"); + ok(!code, "should not have executed callback"); + return $.when(); + }); + }); + test('failed teardown', {asserts: 2}, function () { + var teardowns = 0; + return openerp.testing.Stack().push(null, function () { + teardowns++; + return $.Deferred().reject('Fail 1'); + }).push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + return $.Deferred().reject('Fail 3'); + }).execute(function () { + return 42; + }).then(reject, function (m) { + strictEqual(teardowns, 3, + "should have tried executing all teardowns"); + strictEqual(m, "Fail 3", "should return first failure message"); + return $.when(); + }); + }); + test('failed call + teardown', {asserts: 2}, function () { + var teardowns = 0; + return openerp.testing.Stack().push(null, function () { + teardowns++; + }).push(null, function () { + teardowns++; + return $.Deferred().reject('Fail 2'); + }).execute(function () { + return $.Deferred().reject("code"); + }).then(reject, function (m) { + strictEqual(teardowns, 2, + "should have tried executing all teardowns"); + strictEqual(m, "code", "should return first failure message"); + return $.when(); + }); + }); + + test('arguments passing', {asserts: 9}, function () { + var asserter = function (a, b, c) { + strictEqual(a, 1); + strictEqual(b, "foo"); + deepEqual(c, {bar: "baz", qux: 42}); + }; + + return openerp.testing.Stack() + .push(asserter, asserter) + .execute(asserter, 1, "foo", {bar: 'baz', qux: 42}); + }); +}); diff --git a/addons/web/test_support/__init__.py b/addons/web/test_support/__init__.py deleted file mode 100644 index 59f6cc67ca2..00000000000 --- a/addons/web/test_support/__init__.py +++ /dev/null @@ -1,37 +0,0 @@ -# -*- coding: utf-8 -*- -import xmlrpclib -from ..common.openerplib.main import Connector - -execute_map = {} - -class TestConnector(Connector): - def db_list_lang(self): - return [('en_US', u'Test Language')] - - def common_authenticate(self, db, login, password, environment): - return 87539319 - - def common_login(self, db, login, password): - return self.common_authenticate(db, login, password, {}) - - def object_execute_kw(self, db, uid, password, model, method, args, kwargs): - if model in execute_map and hasattr(execute_map[model], method): - return getattr(execute_map[model], method)(*args, **kwargs) - - raise xmlrpclib.Fault({ - 'model': model, - 'method': method, - 'args': args, - 'kwargs': kwargs - }, '') - - def send(self, service_name, method, *args): - method_name = '%s_%s' % (service_name, method) - if hasattr(self, method_name): - return getattr(self, method_name)(*args) - - raise xmlrpclib.Fault({ - 'service': service_name, - 'method': method, - 'args': args - }, '') diff --git a/addons/web/test_support/controllers.py b/addons/web/test_support/controllers.py deleted file mode 100644 index f1013fbaaf8..00000000000 --- a/addons/web/test_support/controllers.py +++ /dev/null @@ -1,42 +0,0 @@ -# -*- coding: utf-8 -*- - -from ..common import http, nonliterals -from ..controllers.main import Session - -UID = 87539319 -DB = 'test_db' -LOGIN = 'test_login' -PASSWORD = 'test_password' -CONTEXT = {'lang': 'en_US', 'tz': 'UTC', 'uid': UID} - -def bind(session): - session.bind(DB, UID, LOGIN, PASSWORD) - session.context = CONTEXT - session.build_connection().set_login_info(DB, LOGIN, PASSWORD, UID) - -class TestController(http.Controller): - _cp_path = '/tests' - - @http.jsonrequest - def add_nonliterals(self, req, domains, contexts): - return { - 'domains': [nonliterals.Domain(req.session, domain) - for domain in domains], - 'contexts': [nonliterals.Context(req.session, context) - for context in contexts] - } - -class TestSession(Session): - _cp_path = '/web/session' - - def session_info(self, req): - if not req.session._uid: - bind(req.session) - - return { - "session_id": req.session_id, - "uid": req.session._uid, - "context": CONTEXT, - "db": req.session._db, - "login": req.session._login, - } diff --git a/addons/web/tests/__init__.py b/addons/web/tests/__init__.py index fb1a593bc78..8cb2118e0b6 100644 --- a/addons/web/tests/__init__.py +++ b/addons/web/tests/__init__.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -from . import test_dataset, test_menu, test_serving_base, test_view +from . import test_dataset, test_menu, test_serving_base, test_view, test_js fast_suite = [] checks = [ diff --git a/addons/web/tests/test_js.py b/addons/web/tests/test_js.py new file mode 100644 index 00000000000..a126ce4b2f3 --- /dev/null +++ b/addons/web/tests/test_js.py @@ -0,0 +1,25 @@ +import urlparse +from openerp import sql_db, tools +from qunitsuite.suite import QUnitSuite + +class WebSuite(QUnitSuite): + def __init__(self): + url = urlparse.urlunsplit([ + 'http', + 'localhost:{port}'.format(port=tools.config['xmlrpc_port']), + '/web/tests', + 'mod=*&source={db}&supadmin={supadmin}&password={password}'.format( + db=tools.config['db_name'], + supadmin=tools.config['db_password'] or 'admin', + password=tools.config['admin_passwd'] or 'admin'), + '' + ]) + super(WebSuite, self).__init__(url, 50000) + def run(self, result): + if sql_db._Pool is not None: + sql_db._Pool.close_all(sql_db.dsn(tools.config['db_name'])) + return super(WebSuite, self).run(result) + +def load_tests(loader, standard_tests, _): + standard_tests.addTest(WebSuite()) + return standard_tests diff --git a/addons/web_graph/static/lib/flotr2/lib/bean.js b/addons/web_graph/static/lib/flotr2/lib/bean.js index 6e6e3ef4eb5..1a854771006 100644 --- a/addons/web_graph/static/lib/flotr2/lib/bean.js +++ b/addons/web_graph/static/lib/flotr2/lib/bean.js @@ -9,9 +9,7 @@ */ /*global module:true, define:true*/ !function (name, context, definition) { - if (typeof module !== 'undefined') module.exports = definition(name, context); - else if (typeof define === 'function' && typeof define.amd === 'object') define(definition); - else context[name] = definition(name, context); + context[name] = definition(name, context); }('bean', this, function (name, context) { var win = window , old = context[name] diff --git a/addons/web_kanban/static/src/css/kanban.css b/addons/web_kanban/static/src/css/kanban.css index 105e074df1f..9b74462ee13 100644 --- a/addons/web_kanban/static/src/css/kanban.css +++ b/addons/web_kanban/static/src/css/kanban.css @@ -277,7 +277,7 @@ width: 100%; } .openerp .oe_kanban_view.oe_kanban_ungrouped .oe_kanban_column .oe_kanban_record { - float: left; + display: inline-block; padding: 2px; box-sizing: border-box; -moz-box-sizing: border-box; diff --git a/addons/web_kanban/static/src/css/kanban.sass b/addons/web_kanban/static/src/css/kanban.sass index fbc9e3d8568..b993b648691 100644 --- a/addons/web_kanban/static/src/css/kanban.sass +++ b/addons/web_kanban/static/src/css/kanban.sass @@ -264,7 +264,7 @@ width: 100% &.oe_kanban_ungrouped .oe_kanban_column .oe_kanban_record - float: left + display: inline-block padding: 2px box-sizing: border-box -moz-box-sizing: border-box diff --git a/addons/web_kanban/static/src/js/kanban.js b/addons/web_kanban/static/src/js/kanban.js index 8526dbdc8de..cd1ea74983f 100644 --- a/addons/web_kanban/static/src/js/kanban.js +++ b/addons/web_kanban/static/src/js/kanban.js @@ -992,7 +992,7 @@ instance.web_kanban.KanbanRecord = instance.web.Widget.extend({ } else if (this.record[field] && ! this.record[field].value) { url = "/web/static/src/img/placeholder.png"; } else { - id = escape(JSON.stringify(id)); + id = JSON.stringify(id); if (options.preview_image) field = options.preview_image; url = this.session.url('/web/binary/image', {model: model, field: field, id: id}); diff --git a/addons/web_tests_demo/__init__.py b/addons/web_tests_demo/__init__.py new file mode 100644 index 00000000000..137d472ec74 --- /dev/null +++ b/addons/web_tests_demo/__init__.py @@ -0,0 +1,14 @@ +from openerp.osv import orm, fields + +class TestObject(orm.Model): + _name = 'web_tests_demo.model' + + _columns = { + 'name': fields.char("Name", required=True), + 'thing': fields.char("Thing"), + 'other': fields.char("Other", required=True) + } + _defaults = { + 'other': "bob" + } + diff --git a/addons/web_tests_demo/__openerp__.py b/addons/web_tests_demo/__openerp__.py new file mode 100644 index 00000000000..7404ef6b4fc --- /dev/null +++ b/addons/web_tests_demo/__openerp__.py @@ -0,0 +1,8 @@ +{ + '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'], +} diff --git a/addons/web_tests_demo/static/src/js/demo.js b/addons/web_tests_demo/static/src/js/demo.js new file mode 100644 index 00000000000..b35b44b80f8 --- /dev/null +++ b/addons/web_tests_demo/static/src/js/demo.js @@ -0,0 +1,11 @@ +// static/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; + } + }) + }; +}; diff --git a/addons/web_tests_demo/static/src/xml/demo.xml b/addons/web_tests_demo/static/src/xml/demo.xml new file mode 100644 index 00000000000..1bd3862d70e --- /dev/null +++ b/addons/web_tests_demo/static/src/xml/demo.xml @@ -0,0 +1,7 @@ + +
+ +

+
+
+
diff --git a/addons/web_tests_demo/static/test/demo.js b/addons/web_tests_demo/static/test/demo.js new file mode 100644 index 00000000000..b2d54c83d68 --- /dev/null +++ b/addons/web_tests_demo/static/test/demo.js @@ -0,0 +1,102 @@ +openerp.testing.section('basic section', function (test) { + test('my first test', function () { + ok(true, "this test has run"); + }); + 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"); + }); + test('DOM content', function (instance, $scratchpad) { + $scratchpad.html('
ok
'); + 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"); + }); + + 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']); + }); + + test('asynchronous', { + asserts: 1 + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.resolve(); + }, 100); + return d; + }); + test('unfail rejection', { + asserts: 1, + fail_on_rejection: false + }, function () { + var d = $.Deferred(); + setTimeout(function () { + ok(true); + d.reject(); + }, 100); + return d; + }); + + test('XML-RPC', {rpc: 'mock', asserts: 3}, function (instance, $s, mock) { + mock('people.famous:name_search', function (args, kwargs) { + strictEqual(kwargs.name, 'bob'); + return [ + [1, "Microsoft Bob"], + [2, "Bob the Builder"], + [3, "Silent Bob"] + ]; + }); + return new instance.web.Model('people.famous') + .call('name_search', {name: 'bob'}).then(function (result) { + strictEqual(result.length, 3, "shoud return 3 people"); + strictEqual(result[0][1], "Microsoft Bob", + "the most famous bob should be Microsoft Bob"); + }); + }); + 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).then(function () { + ok(fetched_dbs, "should have fetched databases"); + ok(fetched_langs, "should have fetched languages"); + deepEqual(dbm.db_list, ['foo', 'bar', 'baz']); + }); + }); + + test('actual RPC', {rpc: 'rpc', asserts: 4}, function (instance) { + var Model = new instance.web.Model('web_tests_demo.model'); + return Model.call('create', [{name: "Bob"}]) + .then(function (id) { + return Model.call('read', [[id]]); + }).then(function (records) { + strictEqual(records.length, 1); + var record = records[0]; + strictEqual(record.name, "Bob"); + strictEqual(record.thing, false); + // default value + strictEqual(record.other, 'bob'); + }); + }); +});