From 1e3c76f41d9b74a25f8f2449efcd791e36f5d744 Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 2 Oct 2017 16:09:13 +0200 Subject: [PATCH 1/2] [IMP] web: add web tests to regular JS suite Remove old tests page as it was obviously unmaintained and did not run at all... --- addons/web/static/lib/qweb/qweb-test.js.html | 73 ------------------- addons/web/static/lib/qweb/tests.js | 77 ++++++++++++++++++++ addons/web/views/webclient_templates.xml | 3 + 3 files changed, 80 insertions(+), 73 deletions(-) delete mode 100644 addons/web/static/lib/qweb/qweb-test.js.html create mode 100644 addons/web/static/lib/qweb/tests.js diff --git a/addons/web/static/lib/qweb/qweb-test.js.html b/addons/web/static/lib/qweb/qweb-test.js.html deleted file mode 100644 index 0ee5f27c3bd..00000000000 --- a/addons/web/static/lib/qweb/qweb-test.js.html +++ /dev/null @@ -1,73 +0,0 @@ - - - - - - - - - - - - - -
-
- - diff --git a/addons/web/static/lib/qweb/tests.js b/addons/web/static/lib/qweb/tests.js new file mode 100644 index 00000000000..5b961acd427 --- /dev/null +++ b/addons/web/static/lib/qweb/tests.js @@ -0,0 +1,77 @@ +odoo.define('qweb.tests', function () { +'use strict'; + +function trim(s) { + return s.replace(/(^\s+|\s+$)/g, ''); +} + +/** + * Generates a QUnit.module hook object loading the specified test file + * (from /web/static/lib/qweb) and setting ``this.qweb`` (the qweb + * instance for this module) and ``this.doc`` (the loaded XML file). + * + * Note that test files mix template elements , params elements + * and result elements . A result and an optional params + * object are linked to the corresponding template via the ``id`` + * attribute (result and params have the template name as id). + */ +function hooks(testfile) { + var template = '/web/static/lib/qweb/' + testfile; + return { + before: function () { + var self = this; + this.qweb = new QWeb2.Engine(); + var p = $.Deferred(); + this.qweb.add_template(template, function (_, doc) { + self.doc = doc; + p.resolve(doc); + }); + return p.promise(); + } + } +} +// can't get generative QUnit.test (e.g. QUnit.test in a for(;;) +// or Array#forEach() loop) to work, so each test file has a single test, +// that seems to work +function runtest() { + QUnit.test('', function (assert) { + var templates = this.qweb.templates; + assert.expect(_.reduce(templates, function (acc, _, k) { + return acc + (/^_/.test(k) ? 0 : 1); + }, 0)); + for (var template in templates) { + if (!templates.hasOwnProperty(template)) { continue; } + // ignore templates whose name starts with _, they're + // helpers/internal + if (/^_/.test(template)) { continue; } + + var params = this.doc.querySelector('params#' + template); + var args = params ? JSON.parse(params.textContent) : {}; + + var results = this.doc.querySelector('result#' + template); + assert.equal( + trim(this.qweb.render(template, args)), + trim(results.textContent), + template); + } + }); +} + +var TEMPLATES = [ + ["Output", 'qweb-test-output.xml'], + ["Context-setting", 'qweb-test-set.xml'], + ["Conditionals", 'qweb-test-conditionals.xml'], + ["Attributes manipulation", 'qweb-test-attributes.xml'], + ["Template calling (to the faraway pages)", 'qweb-test-call.xml'], + ["Foreach", 'qweb-test-foreach.xml'], + ["Global", 'qweb-test-global.xml'], + ['Template inheritance', 'qweb-test-extend.xml'] +]; + +QUnit.module('qweb', {}, function () { + TEMPLATES.forEach(function (it) { + QUnit.module(it[0], hooks(it[1]), runtest); + }) +}); + +}); diff --git a/addons/web/views/webclient_templates.xml b/addons/web/views/webclient_templates.xml index 4f69ed286b3..605003443d0 100644 --- a/addons/web/views/webclient_templates.xml +++ b/addons/web/views/webclient_templates.xml @@ -448,6 +448,9 @@ display: inherit; } + + + From 6e646bc8bd551966b0b2512ab9ee8ec07415c6eb Mon Sep 17 00:00:00 2001 From: Xavier Morel Date: Mon, 2 Oct 2017 16:33:39 +0200 Subject: [PATCH 2/2] [FIX] web: make qwebjs tests pass again A bunch of features had been added to the qweb test files and qwebjs did not pass them as features were either implemented differently or not implemented at all: * Fixed wonky and not cross-impl tests - added ``not`` word replacemenet to qwebjs as I wasn't sure how to handle that one cross-impl * fixed t-esc/t-raw to render content *inside* the host node e.g. span@t-esc should render as {contents of esc} * added fallback to body content if t-esc/t-raw yields an empty result * process t-att in order with t-att-* and t-attf-* rather than before them both * suppress attribute generation entirely in case of falsy value --- addons/web/static/lib/qweb/qweb-test-call.xml | 2 +- .../web/static/lib/qweb/qweb-test-global.xml | 4 +- addons/web/static/lib/qweb/qweb2.js | 62 +++++++++++-------- 3 files changed, 40 insertions(+), 28 deletions(-) diff --git a/addons/web/static/lib/qweb/qweb-test-call.xml b/addons/web/static/lib/qweb/qweb-test-call.xml index 45a65eabce9..b0c5deb9fe6 100644 --- a/addons/web/static/lib/qweb/qweb-test-call.xml +++ b/addons/web/static/lib/qweb/qweb-test-call.xml @@ -57,7 +57,7 @@ - + ok diff --git a/addons/web/static/lib/qweb/qweb-test-global.xml b/addons/web/static/lib/qweb/qweb-test-global.xml index 1db3e5920fb..bfcca8c1eba 100644 --- a/addons/web/static/lib/qweb/qweb-test-global.xml +++ b/addons/web/static/lib/qweb/qweb-test-global.xml @@ -18,7 +18,7 @@ - i + i @@ -49,6 +49,6 @@
toto default
-
i
+
i
]]> diff --git a/addons/web/static/lib/qweb/qweb2.js b/addons/web/static/lib/qweb/qweb2.js index c4bb7128e9b..8058cb7745f 100644 --- a/addons/web/static/lib/qweb/qweb2.js +++ b/addons/web/static/lib/qweb/qweb2.js @@ -28,8 +28,10 @@ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. var QWeb2 = { expressions_cache: { }, RESERVED_WORDS: 'true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,typeof,eval,void,Math,RegExp,Array,Object,Date'.split(','), - ACTIONS_PRECEDENCE: 'foreach,if,elif,else,call,set,esc,raw,js,debug,log'.split(','), + ACTIONS_PRECEDENCE: 'foreach,if,elif,else,call,set,js,debug,log'.split(','), WORD_REPLACEMENT: { + // FUCKING CHM FOR FUCK'S SAKE + 'not': '!', 'and': '&&', 'or': '||', 'gt': '>', @@ -76,9 +78,9 @@ var QWeb2 = { return s; }, gen_attribute: function(o) { - if (o !== null && o !== undefined) { + if (o) { if (o.constructor === Array) { - if (o[1] !== null && o[1] !== undefined) { + if (o[1]) { return this.format_attribute(o[0], o[1]); } } else if (typeof o === 'object') { @@ -718,36 +720,52 @@ QWeb2.Element = (function() { }, compile_element : function() { for (var i = 0, ilen = this.engine.actions_precedence.length; i < ilen; i++) { - var a = this.engine.actions_precedence[i]; - if (a in this.actions) { - var value = this.actions[a]; - var key = 'compile_action_' + a; + var compiled_action = this.engine.actions_precedence[i]; + if (compiled_action in this.actions) { + var value = this.actions[compiled_action]; + var key = 'compile_action_' + compiled_action; if (this[key]) { this[key](value); } else if (this.engine[key]) { this.engine[key].call(this, value); } else { - this.engine.tools.exception("No handler method for action '" + a + "'"); + this.engine.tools.exception("No handler method for action '" + compiled_action + "'"); } } } + function print_contents(self) { + var expr, esc, raw; + if (esc = self.actions['esc']) { + expr = "context.engine.tools.html_escape(" + self.format_str(esc) + ");"; + } else if(raw = self.actions['raw']) { + expr = self.format_str(raw); + } + // print non-empty result of t-esc or t-raw, otherwise + // print/process body + if (expr) { + self.top('var expr_result = ' + expr + ';'); + self.top('if (expr_result) {'); + self.top(' r.push(expr_result);'); + self.top('} else {'); + self.bottom('}'); + } + } if (this.tag.toLowerCase() !== this.engine.prefix) { var tag = "<" + this.tag; - for (var a in this.attributes) { - tag += this.engine.tools.gen_attribute([a, this.attributes[a]]); + for (var att in this.attributes) { + tag += this.engine.tools.gen_attribute([att, this.attributes[att]]); } this.top_string(tag); if (this.actions.att) { - this.top("r.push(context.engine.tools.gen_attribute(" + (this.format_expression(this.actions.att)) + "));"); + } for (var a in this.actions) { - var v = this.actions[a]; - var m = a.match(/att-(.+)/); - if (m) { + var m, v = this.actions[a]; + if (a === 'att') { + this.top("r.push(context.engine.tools.gen_attribute(" + (this.format_expression(v)) + "));"); + } else if (m = a.match(/att-(.+)/)) { this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.format_expression(v)) + ")]));"); - } - var m = a.match(/attf-(.+)/); - if (m) { + } else if (m = a.match(/attf-(.+)/)) { this.top("r.push(context.engine.tools.gen_attribute(['" + m[1] + "', (" + (this.string_interpolation(v)) + ")]));"); } } @@ -755,11 +773,13 @@ QWeb2.Element = (function() { // We do not enforce empty content on void elements // because QWeb rendering is not necessarily html. this.top_string("/>"); + return; // ensure we skip processing content } else { this.top_string(">"); this.bottom_string(""); } } + print_contents(this); }, compile_action_if : function(value) { this.top("if (" + (this.format_expression(value)) + ") {"); @@ -817,14 +837,6 @@ QWeb2.Element = (function() { } } }, - compile_action_esc : function(value) { - this.top("r.push(context.engine.tools.html_escape(" - + this.format_expression(value) - + "));"); - }, - compile_action_raw : function(value) { - this.top("r.push(" + (this.format_str(value)) + ");"); - }, compile_action_js : function(value) { this.top("(function(" + value + ") {"); this.bottom("})(dict);");