1 // Test support structures and methods for OpenERP
7 coresetup: ['corelib'],
8 data: ['corelib', 'coresetup'],
10 formats: ['coresetup', 'dates'],
11 chrome: ['corelib', 'coresetup'],
12 views: ['corelib', 'coresetup', 'data', 'chrome'],
13 search: ['data', 'coresetup', 'formats'],
14 list: ['views', 'data'],
15 form: ['data', 'views', 'list', 'formats'],
16 list_editable: ['list', 'form', 'data'],
19 testing.dependencies = window['oe_all_dependencies'] || [];
20 testing.current_module = null;
21 testing.templates = { };
22 testing.add_template = function (name) {
23 var xhr = QWeb2.Engine.prototype.get_xhr();
24 xhr.open('GET', name, false);
26 (testing.templates[testing.current_module] =
27 testing.templates[testing.current_module] || [])
28 .push(xhr.responseXML);
31 * Function which does not do anything
33 testing.noop = function () { };
35 * Alter provided instance's ``session`` attribute to make response
38 * * The ``responses`` parameter can be used to provide a map of (RPC)
39 * paths (e.g. ``/web/view/load``) to a function returning a response
41 * * ``instance.session`` grows a ``responses`` attribute which is
42 * a map of the same (and is in fact initialized to the ``responses``
43 * parameter if one is provided)
45 * Note that RPC requests to un-mocked URLs will be rejected with an
46 * error message: only explicitly specified urls will get a response.
48 * Mocked sessions will *never* perform an actual RPC connection.
50 * @param instance openerp instance being initialized
51 * @param {Object} [responses]
53 testing.mockifyRPC = function (instance, responses) {
54 var session = instance.session;
55 session.responses = responses || {};
56 session.rpc_function = function (url, payload) {
58 var needle = payload.params.model + ':' + payload.params.method;
59 if (url.url === '/web/dataset/call_kw'
60 && needle in this.responses) {
61 fn = this.responses[needle];
63 payload.params.args || [],
64 payload.params.kwargs || {}
67 fn = this.responses[url.url];
72 return $.Deferred().reject({}, 'failed',
73 _.str.sprintf("Url %s not found in mock responses, with arguments %s",
74 url.url, JSON.stringify(payload.params))
78 return $.when(fn.apply(null, params)).then(function (result) {
79 // Wrap for RPC layer unwrapper thingy
80 return {result: result};
83 // not sure why this looks like that
84 return $.Deferred().reject({}, 'failed', String(e));
90 execute: function (fn) {
91 var args = [].slice.call(arguments, 1);
92 // Warning: here be dragons
93 var i = 0, setups = this.setups, teardowns = this.teardowns;
96 var succeeded, failed;
97 var success = function () {
98 succeeded = _.toArray(arguments);
101 var failure = function () {
102 // save first failure
104 failed = _.toArray(arguments);
106 // chain onto next teardown
110 var setup = function () {
111 // if setup to execute
112 if (i < setups.length) {
113 var f = setups[i] || testing.noop;
114 $.when(f.apply(null, args)).then(function () {
119 $.when(fn.apply(null, args)).then(success, failure);
122 var teardown = function () {
123 // if teardown to execute
125 var f = teardowns[--i] || testing.noop;
126 $.when(f.apply(null, args)).then(teardown, failure);
129 d.reject.apply(d, failed);
130 } else if (succeeded) {
131 d.resolve.apply(d, succeeded);
133 throw new Error("Didn't succeed or fail?");
141 push: function (setup, teardown) {
142 return _.extend(Object.create(StackProto), {
143 setups: this.setups.concat([setup]),
144 teardowns: this.teardowns.concat([teardown])
147 unshift: function (setup, teardown) {
148 return _.extend(Object.create(StackProto), {
149 setups: [setup].concat(this.setups),
150 teardowns: [teardown].concat(this.teardowns)
156 * @param {Function} [setup]
157 * @param {Function} [teardown]
160 testing.Stack = function (setup, teardown) {
161 return _.extend(Object.create(StackProto), {
162 setups: setup ? [setup] : [],
163 teardowns: teardown ? [teardown] : []
167 var db = window['oe_db_info'];
168 testing.section = function (name, options, body) {
169 if (_.isFunction(options)) {
173 _.defaults(options, {
175 teardown: testing.noop
178 QUnit.module(testing.current_module + '.' + name, {_oe: options});
181 testing.case = function (name, options, callback) {
182 if (_.isFunction(options)) {
187 var module = testing.current_module;
188 var module_index = _.indexOf(testing.dependencies, module);
189 var module_deps = testing.dependencies.slice(
190 // If module not in deps (because only tests, no JS) -> indexOf
191 // returns -1 -> index becomes 0 -> replace with ``undefined`` so
192 // Array#slice returns a full copy
193 0, module_index + 1 || undefined);
195 // Serialize options for this precise test case
196 // WARNING: typo is from jquery, do not fix!
197 var env = QUnit.config.currentModuleTestEnviroment;
203 var case_stack = testing.Stack()
204 .push(env._oe.setup, env._oe.teardown)
205 .push(options.setup, options.teardown);
206 var opts = _.defaults({}, options, env._oe);
207 // FIXME: if this test is ignored, will still query
208 if (opts.rpc === 'rpc' && !db) {
209 QUnit.config.autostart = false;
215 var $msg = $('<form style="margin: 0 1em 1em;">')
216 .append('<h3>A test needs to clone a database</h3>')
217 .append('<h4>Please provide the source clone information</h4>')
218 .append(' Source DB: ').append('<input name="source">').append('<br>')
219 .append(' DB Password: ').append('<input name="supadmin">').append('<br>')
220 .append('Admin Password: ').append('<input name="password">').append('<br>')
221 .append('<input type="submit" value="OK"/>')
222 .submit(function (e) {
225 db.source = $msg.find('input[name=source]').val();
226 db.supadmin = $msg.find('input[name=supadmin]').val();
227 db.password = $msg.find('input[name=password]').val();
234 fontFamily: 'monospace',
236 whiteSpace: 'pre-wrap',
242 QUnit.test(name, function () {
244 if (!opts.dependencies) {
245 instance = openerp.init(module_deps);
247 // empty-but-specified dependencies actually allow running
248 // without loading any module into the instance
250 // TODO: clean up this mess
251 var d = opts.dependencies.slice();
252 // dependencies list should be in deps order, reverse to make
253 // loading order from last
256 while (di < d.length) {
257 var m = /^web\.(\w+)$/.exec(d[di]);
261 d.splice.apply(d, [di+1, 0].concat(
262 _(dependencies[d[di]]).reverse()));
266 instance = openerp.init("fuck your shit, don't load anything you cunt");
270 .each(function (module) {
271 openerp.web[module](instance);
274 if (instance.session) {
275 instance.session.uid = 42;
277 if (_.isNumber(opts.asserts)) {
278 expect(opts.asserts);
281 if (opts.templates) {
282 for(var i=0; i<module_deps.length; ++i) {
283 var dep = module_deps[i];
284 var templates = testing.templates[dep];
285 if (_.isEmpty(templates)) { continue; }
287 for (var j=0; j < templates.length; ++j) {
288 instance.web.qweb.add_template(templates[j]);
293 var $fixture = $('#qunit-fixture');
295 var mock, async = false;
299 testing.mockifyRPC(instance);
300 mock = function (spec, handler) {
301 instance.session.responses[spec] = handler;
307 // Bunch of random base36 characters
308 var dbname = 'test_' + Math.random().toString(36).slice(2);
309 // Add db setup/teardown at the start of the stack
310 case_stack = case_stack.unshift(function (instance) {
311 // FIXME hack: don't want the session to go through shitty loading process of everything
312 instance.session.session_init = testing.noop;
313 instance.session.load_modules = testing.noop;
314 instance.session.session_bind();
315 return instance.session.rpc('/web/database/duplicate', {
317 {name: 'super_admin_pwd', value: db.supadmin},
318 {name: 'db_original_name', value: db.source},
319 {name: 'db_name', value: dbname}
321 }).then(function (result) {
323 return $.Deferred().reject(result.error).promise();
325 return instance.session.session_authenticate(
326 dbname, 'admin', db.password, true);
328 }, function (instance) {
329 return instance.session.rpc('/web/database/drop', {
331 {name: 'drop_pwd', value: db.supadmin},
332 {name: 'drop_db', value: dbname}
334 }).then(function (result) {
336 return $.Deferred().reject(result.error).promise();
344 // Always execute tests asynchronously
347 case_stack.execute(function () {
348 var result = callback.apply(null, arguments);
349 if (!(result && _.isFunction(result.then))) {
351 ok(false, "asynchronous test cases must return a promise");
354 if (!_.isNumber(opts.asserts)) {
355 ok(false, "asynchronous test cases must specify the "
356 + "number of assertions they expect");
360 return $.Deferred(function (d) {
361 $.when(result).then(function () {
362 d.resolve.apply(d, arguments)
364 d.reject.apply(d, arguments);
366 if (async || (result && result.then)) {
367 // async test can be either implicit async (rpc) or
369 timeout = setTimeout(function () {
370 QUnit.config.semaphore = 1;
371 d.reject({message: "Test timed out"});
375 }, instance, $fixture, mock).always(function () {
376 if (timeout) { clearTimeout(timeout); }
378 }).fail(function (error) {
379 if (options.fail_on_rejection === false) {
383 if (typeof error !== 'object'
384 || typeof error.message !== 'string') {
385 message = JSON.stringify([].slice.apply(arguments));
387 message = error.message;
388 if (error.data && error.data.debug) {
389 message += '\n\n' + error.data.debug;