[MERGE] from trunk
[odoo/odoo.git] / addons / web / static / src / js / testing.js
1 // Test support structures and methods for OpenERP
2 openerp.testing = {};
3 (function (testing) {
4     var dependencies = {
5         pyeval: [],
6         corelib: ['pyeval'],
7         coresetup: ['corelib'],
8         data: ['corelib', 'coresetup'],
9         dates: [],
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'],
17     };
18
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);
25         xhr.send(null);
26         (testing.templates[testing.current_module] =
27             testing.templates[testing.current_module] || [])
28                 .push(xhr.responseXML);
29     };
30     /**
31      * Function which does not do anything
32      */
33     testing.noop = function () { };
34     /**
35      * Alter provided instance's ``session`` attribute to make response
36      * mockable:
37      *
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
40      *   to the query.
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)
44      *
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.
47      *
48      * Mocked sessions will *never* perform an actual RPC connection.
49      *
50      * @param instance openerp instance being initialized
51      * @param {Object} [responses]
52      */
53     testing.mockifyRPC = function (instance, responses) {
54         var session = instance.session;
55         session.responses = responses || {};
56         session.rpc_function = function (url, payload) {
57             var fn, params;
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];
62                 params = [
63                     payload.params.args || [],
64                     payload.params.kwargs || {}
65                 ];
66             } else {
67                 fn = this.responses[url.url];
68                 params = [payload];
69             }
70
71             if (!fn) {
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))
75                 ).promise();
76             }
77             try {
78                 return $.when(fn.apply(null, params)).then(function (result) {
79                     // Wrap for RPC layer unwrapper thingy
80                     return {result: result};
81                 });
82             } catch (e) {
83                 // not sure why this looks like that
84                 return $.Deferred().reject({}, 'failed', String(e));
85             }
86         };
87     };
88
89     var StackProto = {
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;
94             var d = $.Deferred();
95
96             var succeeded, failed;
97             var success = function () {
98                 succeeded = _.toArray(arguments);
99                 return teardown();
100             };
101             var failure = function () {
102                 // save first failure
103                 if (!failed) {
104                     failed = _.toArray(arguments);
105                 }
106                 // chain onto next teardown
107                 return teardown();
108             };
109
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 () {
115                         ++i;
116                         setup();
117                     }, failure);
118                 } else {
119                     $.when(fn.apply(null, args)).then(success, failure);
120                 }
121             };
122             var teardown = function () {
123                 // if teardown to execute
124                 if (i > 0) {
125                     var f = teardowns[--i] || testing.noop;
126                     $.when(f.apply(null, args)).then(teardown, failure);
127                 } else {
128                     if (failed) {
129                         d.reject.apply(d, failed);
130                     } else if (succeeded) {
131                         d.resolve.apply(d, succeeded);
132                     } else {
133                         throw new Error("Didn't succeed or fail?");
134                     }
135                 }
136             };
137             setup();
138
139             return d;
140         },
141         push: function (setup, teardown) {
142             return _.extend(Object.create(StackProto), {
143                 setups: this.setups.concat([setup]),
144                 teardowns: this.teardowns.concat([teardown])
145             });
146         },
147         unshift: function (setup, teardown) {
148             return _.extend(Object.create(StackProto), {
149                 setups: [setup].concat(this.setups),
150                 teardowns: [teardown].concat(this.teardowns)
151             });
152         }
153     };
154     /**
155      *
156      * @param {Function} [setup]
157      * @param {Function} [teardown]
158      * @return {*}
159      */
160     testing.Stack = function (setup, teardown) {
161         return _.extend(Object.create(StackProto), {
162             setups: setup ? [setup] : [],
163             teardowns: teardown ? [teardown] : []
164         });
165     };
166
167     var db = window['oe_db_info'];
168     testing.section = function (name, options, body) {
169         if (_.isFunction(options)) {
170             body = options;
171             options = {};
172         }
173         _.defaults(options, {
174             setup: testing.noop,
175             teardown: testing.noop
176         });
177
178         QUnit.module(testing.current_module + '.' + name, {_oe: options});
179         body(testing.case);
180     };
181     testing.case = function (name, options, callback) {
182         if (_.isFunction(options)) {
183             callback = options;
184             options = {};
185         }
186
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);
194
195         // Serialize options for this precise test case
196         // WARNING: typo is from jquery, do not fix!
197         var env = QUnit.config.currentModuleTestEnviroment;
198         // section setup
199         //     case setup
200         //         test
201         //     case teardown
202         // section teardown
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;
210             db = {
211                 source: null,
212                 supadmin: null,
213                 password: null
214             };
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) {
223                     e.preventDefault();
224                     e.stopPropagation();
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();
228                     QUnit.start();
229                     $.unblockUI();
230                 });
231             $.blockUI({
232                 message: $msg,
233                 css: {
234                     fontFamily: 'monospace',
235                     textAlign: 'left',
236                     whiteSpace: 'pre-wrap',
237                     cursor: 'default'
238                 }
239             });
240         }
241
242         QUnit.test(name, function () {
243             var instance;
244             if (!opts.dependencies) {
245                 instance = openerp.init(module_deps);
246             } else {
247                 // empty-but-specified dependencies actually allow running
248                 // without loading any module into the instance
249
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
254                 d.reverse();
255                 var di = 0;
256                 while (di < d.length) {
257                     var m = /^web\.(\w+)$/.exec(d[di]);
258                     if (m) {
259                         d[di] = m[1];
260                     }
261                     d.splice.apply(d, [di+1, 0].concat(
262                         _(dependencies[d[di]]).reverse()));
263                     ++di;
264                 }
265
266                 instance = openerp.init("fuck your shit, don't load anything you cunt");
267                 _(d).chain()
268                     .reverse()
269                     .uniq()
270                     .each(function (module) {
271                         openerp.web[module](instance);
272                     });
273             }
274             if (instance.session) {
275                 instance.session.uid = 42;
276             }
277             if (_.isNumber(opts.asserts)) {
278                 expect(opts.asserts);
279             }
280
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; }
286
287                     for (var j=0; j < templates.length; ++j) {
288                         instance.web.qweb.add_template(templates[j]);
289                     }
290                 }
291             }
292
293             var $fixture = $('#qunit-fixture');
294
295             var mock, async = false;
296             switch (opts.rpc) {
297             case 'mock':
298                 async = true;
299                 testing.mockifyRPC(instance);
300                 mock = function (spec, handler) {
301                     instance.session.responses[spec] = handler;
302                 };
303                 break;
304             case 'rpc':
305                 async = true;
306                 (function () {
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', {
316                         fields: [
317                             {name: 'super_admin_pwd', value: db.supadmin},
318                             {name: 'db_original_name', value: db.source},
319                             {name: 'db_name', value: dbname}
320                         ]
321                     }).then(function (result) {
322                         if (result.error) {
323                             return $.Deferred().reject(result.error).promise();
324                         }
325                         return instance.session.session_authenticate(
326                             dbname, 'admin', db.password, true);
327                     });
328                 }, function (instance) {
329                     return instance.session.rpc('/web/database/drop', {
330                             fields: [
331                                 {name: 'drop_pwd', value: db.supadmin},
332                                 {name: 'drop_db', value: dbname}
333                             ]
334                         }).then(function (result) {
335                         if (result.error) {
336                             return $.Deferred().reject(result.error).promise();
337                         }
338                         return result;
339                     });
340                 });
341                 })();
342             }
343
344             // Always execute tests asynchronously
345             stop();
346             var timeout;
347             case_stack.execute(function () {
348                 var result = callback.apply(null, arguments);
349                 if (!(result && _.isFunction(result.then))) {
350                     if (async) {
351                         ok(false, "asynchronous test cases must return a promise");
352                     }
353                 } else {
354                     if (!_.isNumber(opts.asserts)) {
355                         ok(false, "asynchronous test cases must specify the "
356                                 + "number of assertions they expect");
357                     }
358                 }
359
360                 return $.Deferred(function (d) {
361                     $.when(result).then(function () {
362                         d.resolve.apply(d, arguments)
363                     }, function () {
364                         d.reject.apply(d, arguments);
365                     });
366                     if (async || (result && result.then)) {
367                         // async test can be either implicit async (rpc) or
368                         // promise-returning
369                         timeout = setTimeout(function () {
370                             QUnit.config.semaphore = 1;
371                             d.reject({message: "Test timed out"});
372                         }, 2000);
373                     }
374                 });
375             }, instance, $fixture, mock).always(function () {
376                 if (timeout) { clearTimeout(timeout); }
377                 start();
378             }).fail(function (error) {
379                 if (options.fail_on_rejection === false) {
380                     return;
381                 }
382                 var message;
383                 if (typeof error !== 'object'
384                         || typeof error.message !== 'string') {
385                     message = JSON.stringify([].slice.apply(arguments));
386                 } else {
387                     message = error.message;
388                     if (error.data && error.data.debug) {
389                         message += '\n\n' + error.data.debug;
390                     }
391                 }
392
393                 ok(false, message);
394             });
395         });
396     };
397 })(openerp.testing);