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