A note concerning the attributes which I will probably need to take a look at in the vdom version: the Python version has to process attf in order to stringify individual elements, and separately stringify the attribute value so it gets forcefully escaped even if it's markup-safe (because markup-safety and attributes-safety are different). The first should not need to be performed on the JS side, because Markup can not overload addition, therefore in JS String + Markup is String whereas in Python it's Markup (and the String gets forcefully escaped). In general, js!markup is currently much simpler than py!markup, both by necessity (can't overload operators) and simplicity (we might want to overload some of the operations e.g. String#replace, but that's complicated and it's not been strictly necessary for now). Also wrt Markup / _Markup: `class` ctors can only be invoked with `new` meaning they can't be used as template strings or regular functions. Here `class` is useful to avoid the mess of calling the super's constructor explicitly (which may not even be possible for `String`), however it means we need a facade function to support our use-cases. Also update `utils.sprintf` to be Markup-aware: if the format string is a Markup object, interpolated values get automatically escaped (if necessary) and the result remains a Markup object. If we need to perform explicit instance check we can always set `Markup.prototype = _Markup.prototype` (I think), however in theory that's not necessary: there are protocols in place for the relevant pseudo-escaping operations and they ought suffice. qweb/js divergence from qweb/py =============================== Unlike qweb/py, qweb/js will *not* return a Markup object. That is because in js a primitive `string` and a boxed `String` object don't match when typechecking, and while `markup instanceof String` passes, `typeof markup === 'string'` does not. The overwhelming majority of string typechecks are the latter: there are all of 6 `instanceof String` in the entire codebase, all in dependencies, while there are hundreds of `typeof $X === 'string'`, several of which get fed the output of template rendering e.g. `jQuery.parseXML` or `AbstractView#init` (some widgets will render a template then use it as the `arch` of a subview, so `viewInfo.arch` can be the output of a qweb template rendering). This makes for very annoying and somewhat gnarly debugging. Plus jQuery in particular really doesn't like being fed a boxed String, as it will interpret said boxed string as an array, and assume it's an array of DOM elements to wrap, leading to a rather strange jQuery object as output. Since feeding the result of a template rendering to jQuery is a major use-case in non-vdom widgets... that's a bit of an issue. For the same reason while `_.escape` is `Markup`-aware, unlike `markupsafe-escape` it does not *produce*, though it is `Markup`-transparent.
193 lines
7.0 KiB
JavaScript
193 lines
7.0 KiB
JavaScript
odoo.define('web.qweb_tests', function (require) {
|
|
"use strict";
|
|
|
|
const {Markup} = require('web.utils');
|
|
|
|
var qwebPath = '/web/static/lib/qweb/';
|
|
const {hushConsole} = require('@web/../tests/helpers/utils');
|
|
|
|
function trim(s) {
|
|
return s.replace(/(^\s+|\s+$)/g, '');
|
|
}
|
|
|
|
/**
|
|
* Promise-based wrapper for QWeb2.Engine#add_template
|
|
*
|
|
* The base version is callbacks-based which is a bit shit, and it also has
|
|
* variable asynchronicity: it'll be async if passed a URL but not if passed
|
|
* either a Document or an XML string.
|
|
*
|
|
* Either way this converts the `(error, doc)` callback to a `Promise<Document>`
|
|
* as in it literally returns the parsed DOM Document.
|
|
*
|
|
* @param qweb the qweb instance to load the template into
|
|
* @param {String|Document} template qweb template (Document, template file, or template URL)
|
|
*/
|
|
function add_template(qweb, template) {
|
|
return new Promise(function (resolve, reject) {
|
|
qweb.add_template(template, (error, doc) => error ? reject(error) : resolve(doc));
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Loads the template file, and executes all the test template in a
|
|
* qunit module $title
|
|
*
|
|
* @param assert QUnit assertion module
|
|
* @param {String|Document} templateFile template container to load
|
|
* @param {Object} [context] additional rendering context
|
|
*/
|
|
async function loadTest(assert, templateFile, context) {
|
|
const qweb = new window.QWeb2.Engine();
|
|
const doc = await add_template(qweb, qwebPath + templateFile);
|
|
|
|
assert.expect(doc.querySelectorAll('result').length);
|
|
|
|
const templates = qweb.templates;
|
|
for (const template in templates) {
|
|
if (!templates.hasOwnProperty(template)) {
|
|
continue;
|
|
}
|
|
// ignore templates whose name starts with _, they're
|
|
// helpers/internal
|
|
if (/^_/.test(template)) {
|
|
continue;
|
|
}
|
|
|
|
const results = doc.querySelector(`result#${template}`).textContent.replace(/\r/g, '');
|
|
const params = doc.querySelector(`params#${template}`) || {textContent: 'null'};
|
|
const args = {...JSON.parse(params.textContent), ...context};
|
|
|
|
try {
|
|
assert.equal(trim(qweb.render(template, args)), trim(results), template);
|
|
} catch (error) {
|
|
assert.notOk(error.stack || error, `Rendering error for ${template} (in ${templateFile} with context ${JSON.stringify(args)}).`);
|
|
}
|
|
}
|
|
}
|
|
|
|
const table = [
|
|
{name: 'Output', file: 'qweb-test-output.xml'},
|
|
{name: 'Context-setting', file: 'qweb-test-set.xml'},
|
|
{name: 'Conditionals', file: 'qweb-test-conditionals.xml'},
|
|
{name: 'Attributes manipulation', file: 'qweb-test-attributes.xml'},
|
|
{name: 'Templates calling (to the faraway pages)', file: 'qweb-test-call.xml', context: {True: true}},
|
|
{name: 'Foreach', file: 'qweb-test-foreach.xml'},
|
|
{
|
|
name: 'Global', file: 'qweb-test-global.xml',
|
|
// test uses python syntax
|
|
context: {bool: (v) => !!v ? 'True' : 'False'},
|
|
fixture: {
|
|
before() {
|
|
this.WORD_REPLACEMENT = window.QWeb2.WORD_REPLACEMENT;
|
|
window.QWeb2.WORD_REPLACEMENT = _.extend(
|
|
{not: '!', None: 'undefined'},
|
|
this.WORD_REPLACEMENT
|
|
)
|
|
},
|
|
after() {
|
|
window.QWeb2.WORD_REPLACEMENT = this.WORD_REPLACEMENT;
|
|
}
|
|
}
|
|
},
|
|
{name: 'Template Inheritance', file: 'qweb-test-extend.xml'},
|
|
];
|
|
QUnit.module('QWeb', {
|
|
beforeEach() {
|
|
this.oldConsole = window.console;
|
|
window.console = hushConsole;
|
|
},
|
|
afterEach() {window.console = this.oldConsole;}
|
|
}, () => {
|
|
for(const {name, file, context, fixture} of table) {
|
|
QUnit.test(name, async assert => {
|
|
// fake expect to avoid qunit being a pain in the ass, loadTest will
|
|
// update it
|
|
assert.expect(1);
|
|
if (fixture && 'before' in fixture) { await fixture.before(); }
|
|
try {
|
|
await loadTest(assert, file, context);
|
|
} finally {
|
|
if (fixture && 'after' in fixture) { await fixture.after(); }
|
|
}
|
|
});
|
|
}
|
|
QUnit.test('escape', assert => {
|
|
// not strictly about qweb...
|
|
assert.expect(8);
|
|
assert.equal(_.escape('a'), 'a');
|
|
assert.equal(_.escape('<a>'), '<a>');
|
|
assert.equal(_.escape({[_.escapeMethod]() { return 'a'; }}), 'a');
|
|
assert.equal(_.escape({[_.escapeMethod]() { return '<a>'; }}), '<a>');
|
|
assert.equal(_.escape(Markup('a')), 'a');
|
|
assert.equal(_.escape(Markup('<a>')), '<a>');
|
|
assert.equal(_.escape(Markup`a`), 'a');
|
|
assert.equal(_.escape(Markup`<a>`), '<a>');
|
|
});
|
|
QUnit.module('t-out', {}, () => {
|
|
QUnit.test("basics", async assert => {
|
|
assert.expect(5);
|
|
const qweb = new QWeb2.Engine;
|
|
await add_template(qweb, `<templates>
|
|
<t t-name="t-out"><p><t t-out="value"/></p></t>
|
|
</templates>`);
|
|
|
|
assert.equal(
|
|
qweb.render('t-out', {value: '<i>test</i>'}),
|
|
'<p><i>test</i></p>',
|
|
"regular t-out should just escape the contents"
|
|
);
|
|
assert.equal(
|
|
qweb.render('t-out', {value: Markup('<i>test</i>')}),
|
|
'<p><i>test</i></p>',
|
|
"Markup contents should not be escaped"
|
|
);
|
|
assert.equal(
|
|
qweb.render('t-out', {value: Markup`<i>test ${1}</i>`}),
|
|
'<p><i>test 1</i></p>',
|
|
"markup template string should not be escaped"
|
|
);
|
|
const teststr = '<i>test</i>';
|
|
assert.equal(
|
|
qweb.render('t-out', {value: Markup`<b>${teststr}</b>`}),
|
|
'<p><b><i>test</i></b></p>',
|
|
"the markup template should not be escaped but what it uses should be"
|
|
);
|
|
const testMarkup = Markup(teststr);
|
|
assert.equal(
|
|
qweb.render('t-out', {value: Markup`<b>${testMarkup}</b>`}),
|
|
'<p><b><i>test</i></b></p>',
|
|
"markupception"
|
|
);
|
|
});
|
|
QUnit.test("Set", async assert => {
|
|
assert.expect(4);
|
|
const qweb = new QWeb2.Engine;
|
|
await add_template(qweb, `<templates>
|
|
<t t-name="litval">
|
|
<t t-set="x" t-value="'<a/>'"/>
|
|
<x><t t-out="x"/></x>
|
|
</t>
|
|
<t t-name="body">
|
|
<t t-set="x"><a/></t>
|
|
<x><t t-out="x"/></x>
|
|
</t>
|
|
<t t-name="value">
|
|
<t t-set="x" t-value="val"/>
|
|
<x><t t-out="x"/></x>
|
|
</t>
|
|
<t t-name="bodyout">
|
|
<t t-set="x"><t t-out="val"/></t>
|
|
<x><t t-out="x"/></x>
|
|
</t>
|
|
</templates>`);
|
|
|
|
assert.equal(trim(qweb.render('litval')), '<x><a/></x>');
|
|
assert.equal(trim(qweb.render('body')), '<x><a></a></x>');
|
|
assert.equal(trim(qweb.render('value', {val: '<a/>'})), '<x><a/></x>');
|
|
assert.equal(trim(qweb.render('bodyout', {val: '<a/>'})), '<x><a/></x>');
|
|
});
|
|
})
|
|
});
|
|
});
|