Compare commits

..

9 Commits

Author SHA1 Message Date
Géry Debongnie 38380b0b12 wip 2021-10-11 17:48:15 +02:00
Géry Debongnie 8986f06448 wip 2021-10-11 17:48:05 +02:00
Géry Debongnie 1a33d3d8e8 wip 2021-10-11 15:03:56 +02:00
Géry Debongnie 82ab18ad83 wip 2021-10-11 14:00:56 +02:00
Géry Debongnie c86bb6111a wip 2021-10-11 13:26:07 +02:00
Géry Debongnie c0f495661d [REL] v1.4.6
# v1.4.6

- fix: crash in component render (in rare cases)
- fix: build system now target ES2017 instead of ESNext
- fix: remove useless log in prod mode
2021-10-04 15:16:01 +02:00
Géry Debongnie 0f7a8289a6 [FIX] config: do not log anything if in prod mode
It is worse having a dev mode in a production environment than the
opposite. The purpose of the warning was to avoid that situation.

However, in prod mode, it is not really necessary to log the config key.

closes #915
2021-10-04 14:57:30 +02:00
Géry Debongnie 1c3b04f6a8 [FIX] build: fix typescript target to ES2017
This makes sure that we do not leak modern code in odoo.
2021-10-04 14:57:30 +02:00
Géry Debongnie 64db7777dd [FIX] component: async issue
This is a tricky commit. The key point is that the Fiber.complete
method, which commits a rendering to the DOM works like this: it
traverses the component tree, patch the corresponding DOM for each
component, calls the mounted/destroy hooks, and reset the currentfiber
of components to null, all synchronously.

However, this means that it is possible for components to initiate a
rendering (which create a new currentFiber) before the currentFiber is
reset to null, so the internal state of owl is corrupted. This can
occurs in a crash, as in the test that accompanies this commit.

To fix this, we take care of resetting the currentFiber first, while we
walk the component tree. Then, the internal state is always consistent
(i.e. a currentFiber to null means that there is no pending rendering)

closes #904
2021-10-04 14:57:30 +02:00
16 changed files with 1016 additions and 632 deletions
+1 -1
View File
@@ -124,7 +124,7 @@ npm install @odoo/owl
If you want to use a simple `<script>` tag, the last release can be downloaded here:
- [owl-1.4.5](https://github.com/odoo/owl/releases/tag/v1.4.5)
- [owl-1.4.6](https://github.com/odoo/owl/releases/tag/v1.4.6)
## License
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@odoo/owl",
"version": "1.4.5",
"version": "1.4.6",
"description": "Odoo Web Library (OWL)",
"main": "dist/owl.cjs.js",
"browser": "dist/owl.iife.js",
+1 -1
View File
@@ -1,6 +1,6 @@
# 🦉 OWL Roadmap 🦉
- Current version: 1.4.5
- Current version: 1.4.6
- Status: stable
This roadmap is only an attempt at predicting Owl's future. Everything may
+1 -1
View File
@@ -400,6 +400,7 @@ export class Component<Props extends {} = any, T extends Env = Env> {
if (currentFiber && !currentFiber.isRendered && !currentFiber.isCompleted) {
return scheduler.addFiber(currentFiber.root);
}
// if we aren't mounted at this point, it implies that there is a
// currentFiber that is already rendered (isRendered is true), so we are
// about to be mounted
@@ -505,7 +506,6 @@ export class Component<Props extends {} = any, T extends Env = Env> {
const __owl__ = this.__owl__;
__owl__.status = STATUS.MOUNTED;
__owl__.currentFiber = null;
this.mounted();
if (__owl__.mountedCB) {
__owl__.mountedCB();
+1 -4
View File
@@ -198,6 +198,7 @@ export class Fiber {
// build patchQueue
const patchQueue: Fiber[] = [];
const doWork: (Fiber) => Fiber | null = function (f) {
f.component.__owl__.currentFiber = null;
patchQueue.push(f);
return f.child;
};
@@ -255,10 +256,6 @@ export class Fiber {
component.__owl__.pvnode!.elm = component.__owl__.vnode!.elm;
}
}
const compOwl = component.__owl__;
if (fiber === compOwl.currentFiber) {
compOwl.currentFiber = null;
}
}
// insert into the DOM (mount case)
-2
View File
@@ -28,8 +28,6 @@ Object.defineProperty(config, "mode", {
This is not suitable for production use.
See https://github.com/odoo/owl/blob/master/doc/reference/config.md#mode for more information.`);
} else {
console.log(`Owl is now running in 'prod' mode.`);
}
},
});
+85 -37
View File
@@ -44,50 +44,88 @@ function compileValueNode(value: any, node: Element, qweb: QWeb, ctx: Compilatio
} else {
exprID = `scope.${value.id}`;
}
ctx.addIf(`${exprID} != null`);
if (ctx.escaping) {
let protectID;
if (value.hasBody) {
ctx.rootContext.shouldDefineUtils = true;
protectID = ctx.startProtectScope();
ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
);
}
if (ctx.parentTextNode) {
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
} else if (ctx.parentNode) {
ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
} else {
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`);
}
}
if (value.hasBody) {
ctx.stopProtectScope(protectID);
}
} else {
let protectID;
if (value.hasBody) {
ctx.rootContext.shouldDefineUtils = true;
if (value.hasBody) {
ctx.addLine(
`const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
);
ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
} else {
ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
}
protectID = ctx.startProtectScope();
ctx.addLine(
`${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
);
}
if (node.childNodes.length) {
ctx.addElse();
// ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
ctx.addIf(`!${exprID}`);
// if (node.childNodes.length) {
// ctx.addElse();
qweb._compileChildren(node, ctx);
// }
ctx.closeIf();
}
if (ctx.parentTextNode) {
ctx.addIf(`${exprID} != null`);
ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
ctx.closeIf();
} else if (ctx.parentNode) {
ctx.rootContext.shouldDefineUtils = true;
ctx.addLine(`insertValue(c${ctx.parentNode}, ${exprID})`);
} else {
ctx.addIf(`${exprID} != null`);
let nodeID = ctx.generateID();
ctx.rootContext.rootNode = nodeID;
ctx.rootContext.parentTextNode = nodeID;
ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
if (ctx.rootContext.shouldDefineResult) {
ctx.addLine(`result = vn${nodeID}`);
}
ctx.closeIf();
}
if (value.hasBody) {
ctx.stopProtectScope(protectID);
}
ctx.closeIf();
// ctx.addIf(`${exprID} != null`);
// if (ctx.escaping) {
// let protectID;
// if (value.hasBody) {
// ctx.rootContext.shouldDefineUtils = true;
// protectID = ctx.startProtectScope();
// ctx.addLine(
// `${exprID} = ${exprID} instanceof utils.VDomArray ? utils.vDomToString(${exprID}) : ${exprID};`
// );
// }
// if (ctx.parentTextNode) {
// ctx.addLine(`vn${ctx.parentTextNode}.text += ${exprID};`);
// } else if (ctx.parentNode) {
// ctx.addLine(`c${ctx.parentNode}.push({text: ${exprID}});`);
// } else {
// let nodeID = ctx.generateID();
// ctx.rootContext.rootNode = nodeID;
// ctx.rootContext.parentTextNode = nodeID;
// ctx.addLine(`let vn${nodeID} = {text: ${exprID}};`);
// if (ctx.rootContext.shouldDefineResult) {
// ctx.addLine(`result = vn${nodeID}`);
// }
// }
// if (value.hasBody) {
// ctx.stopProtectScope(protectID);
// }
// } else {
// ctx.rootContext.shouldDefineUtils = true;
// if (value.hasBody) {
// ctx.addLine(
// `const vnodeArray = ${exprID} instanceof utils.VDomArray ? ${exprID} : utils.htmlToVDOM(${exprID});`
// );
// ctx.addLine(`c${ctx.parentNode}.push(...vnodeArray);`);
// } else {
// ctx.addLine(`c${ctx.parentNode}.push(...utils.htmlToVDOM(${exprID}));`);
// }
// }
// ctx.closeIf();
}
QWeb.addDirective({
@@ -100,6 +138,16 @@ QWeb.addDirective({
},
});
QWeb.addDirective({
name: "out",
priority: 70,
atNodeEncounter({ node, qweb, ctx }): boolean {
let value = ctx.getValue(node.getAttribute("t-out")!);
compileValueNode(value, node, qweb, ctx.subContext("escaping", true));
return true;
},
});
QWeb.addDirective({
name: "raw",
priority: 80,
+1
View File
@@ -84,6 +84,7 @@ export class CompilationContext {
this.code.unshift(" let QWeb = this.constructor;");
}
if (this.shouldDefineUtils) {
this.code.unshift(" let insertValue = utils.insertValue;");
this.code.unshift(" let utils = this.constructor.utils;");
}
return this.code;
+17 -2
View File
@@ -1,8 +1,9 @@
import { EventBus } from "../core/event_bus";
import { h, patch, VNode } from "../vdom/index";
import { CompilationContext } from "./compilation_context";
import { shallowEqual, escape } from "../utils";
import { shallowEqual, escape, _Markup } from "../utils";
import { addNS } from "../vdom/vdom";
import { htmlToVDOM } from "../vdom/html_to_vdom";
/**
* Owl QWeb Engine
@@ -89,6 +90,16 @@ function isComponent(obj): boolean {
return obj && obj.hasOwnProperty("__owl__");
}
function insertValue(children: any[], value: any) {
if (value != null) {
if (value instanceof _Markup) {
children.push(...htmlToVDOM(value as any));
} else {
children.push({ text: value });
}
}
}
class VDomArray extends Array {
toString() {
return vDomToString(this);
@@ -111,6 +122,7 @@ function vDomToString(vdom: VNode[]): string {
const UTILS: Utils = {
zero: Symbol("zero"),
insertValue,
toClassObj(expr) {
const result = {};
if (typeof expr === "string") {
@@ -599,7 +611,10 @@ export class QWeb extends EventBus {
if (!(dName in QWeb.DIRECTIVE_NAMES)) {
throw new Error(`Unknown QWeb directive: '${attrName}'`);
}
if (node.tagName !== "t" && (attrName === "t-esc" || attrName === "t-raw")) {
if (
node.tagName !== "t" &&
(attrName === "t-esc" || attrName === "t-out" || attrName === "t-raw")
) {
const tNode = document.implementation.createDocument(
"http://www.w3.org/1999/xhtml",
"t",
+62 -1
View File
@@ -15,7 +15,7 @@ import { browser } from "./browser";
export function whenReady(fn?: any) {
return new Promise(function (resolve) {
if (document.readyState !== "loading") {
resolve();
(resolve as any)();
} else {
document.addEventListener("DOMContentLoaded", resolve, false);
}
@@ -101,3 +101,64 @@ export function shallowEqual(p1, p2): boolean {
}
return true;
}
// notable issues:
// * objects can't be negative in JS, so !!"" -> false but
// !!(new String) -> true, likewise markup
// TODO (?)
// * Markup.join / Markup#join => escapes items and returns a Markup
// * Markup#replace => automatically escapes the replacements (difficult impl)
export class _Markup extends String {}
/**
* Returns a markup object, which acts like a String but is considered safe by
* `_.escape`, and will therefore be injected as-is (without additional
* escaping) in templates. Can be used to inject dynamic HTML in templates
* (where the template itself can't), see first example.
*
* Can also be used as a *template tag*, in which case the literal content
* won't be escaped but the substitutions which are not already markup objects
* will be.
*
* ## WARNINGS:
* * A markup object is a `String` (boxed) but not a `string` (primitive), they
* typecheck differently which can be relevant.
* * To strip out the "markupness", just call `String(markup)`.
* * Most string operations (e.g. concatenation, `String#replace`, ...) will
* also strip out markupness
* * If the input is empty, returns a regular string (that way boolean tests
* work as expected).
*
* @returns a markup object
*
* @example regular function
* let h;
* if (someTest) {
* h = Markup(_t("This is a <strong>success</strong>"));
* } else {
* h = Markup(_t("Things did <strong>not</strong> work out"));
* }
* qweb.render("some_template", { message: h });
*
* @example template tag
* const escaped = "<some> text";
* const asis = Markup`some <b>text</b>`;
* const h = Markup`Regular strings get ${escaped} but markup is injected ${asis}`;
*/
export function Markup(v, ...exprs) {
if (!(v instanceof Array)) {
return v ? new _Markup(v) : "";
}
const elements = [];
let i = 0;
for (; i < exprs.length; ++i) {
elements.push(v[i], escape(exprs[i]));
}
elements.push(v[i]);
const s = elements.join("");
if (!s) {
return "";
}
return new _Markup(s);
}
+56
View File
@@ -1388,6 +1388,62 @@ describe("async rendering", () => {
expect(fixture.innerHTML).toBe("<span>4</span>");
});
test("calling render in destroy", async () => {
let a: any = null;
let c: any = null;
class C extends Component {
static template = xml`
<div>
<t t-esc="props.fromA"/>
</div>`;
}
let flag = false;
class B extends Component {
static template = xml`<C fromA="props.fromA"/>`;
static components = { C };
setup() {
c = this;
}
mounted() {
if (flag) {
this.render();
} else {
flag = true;
}
}
willUnmount() {
c.render();
}
}
class A extends Component {
static template = xml`<B t-key="key" fromA="state"/>`;
static components = { B };
state = "a";
key = 1;
setup() {
a = this;
}
}
const parent = new A();
await parent.mount(fixture);
expect(fixture.innerHTML).toBe("<div>a</div>");
a.state = "A";
a.key = 2;
await a.render();
// this nextTick is critical, otherwise jest may silently swallow errors
await nextTick();
expect(fixture.innerHTML).toBe("<div>A</div>");
});
test("change state and call manually render: no unnecessary rendering", async () => {
class Widget extends Component {
static template = xml`<div><t t-esc="state.val"/></div>`;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,122 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`old t-esc directive escaping 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1);
let _2 = scope['var'];
if (_2 != null) {
c1.push({text: _2});
}
return vn1;
}"
`;
exports[`old t-esc directive simple dynamic value 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let scope = Object.create(context);
let result;
let h = this.h;
let _1 = scope['text'];
if (_1 != null) {
let vn2 = {text: _1};
result = vn2
}
return result;
}"
`;
exports[`old t-raw directive literal 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1);
let _2 = 'ok';
if (_2 != null) {
c1.push(...utils.htmlToVDOM(_2));
}
return vn1;
}"
`;
exports[`old t-raw directive not escaping 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('div', p1, c1);
let _2 = scope['var'];
if (_2 != null) {
c1.push(...utils.htmlToVDOM(_2));
}
return vn1;
}"
`;
exports[`old t-raw directive t-raw and another sibling node 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1);
let c2 = [], p2 = {key:2};
let vn2 = h('span', p2, c2);
c1.push(vn2);
c2.push({text: \`hello\`});
let _3 = scope['var'];
if (_3 != null) {
c1.push(...utils.htmlToVDOM(_3));
}
return vn1;
}"
`;
exports[`old t-raw directive t-raw with comment 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1);
let _2 = scope['var'];
if (_2 != null) {
c1.push(...utils.htmlToVDOM(_2));
}
return vn1;
}"
`;
exports[`old t-raw directive variable 1`] = `
"function anonymous(context, extra
) {
// Template name: \\"test\\"
let utils = this.constructor.utils;
let scope = Object.create(context);
let h = this.h;
let c1 = [], p1 = {key:1};
let vn1 = h('span', p1, c1);
let _2 = scope['var'];
if (_2 != null) {
c1.push(...utils.htmlToVDOM(_2));
}
return vn1;
}"
`;
+105 -99
View File
@@ -2,6 +2,7 @@ import { QWeb } from "../../src/qweb/index";
import { config } from "../../src/index";
import { nextTick, normalize, renderToDOM, renderToString, trim } from "../helpers";
import { patch } from "../../src/vdom";
import { Markup } from "../../src/utils";
//------------------------------------------------------------------------------
// Setup and helpers
@@ -29,27 +30,27 @@ describe("static templates", () => {
});
test("simple dynamic value", () => {
qweb.addTemplate("test", '<t><t t-esc="text"/></t>');
qweb.addTemplate("test", '<t><t t-out="text"/></t>');
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
});
test("inline template string in t-esc", () => {
qweb.addTemplate("test", '<t><t t-esc="`text`"/></t>');
test("inline template string in t-out", () => {
qweb.addTemplate("test", '<t><t t-out="`text`"/></t>');
expect(renderToString(qweb, "test")).toBe("text");
});
test("inline template string with content in t-esc", () => {
qweb.addTemplate("test", '<t><t t-set="v" t-value="1"/><t t-esc="`text${v}`"/></t>');
test("inline template string with content in t-out", () => {
qweb.addTemplate("test", '<t><t t-set="v" t-value="1"/><t t-out="`text${v}`"/></t>');
expect(renderToString(qweb, "test")).toBe("text1");
});
test("inline template string with variable in context", () => {
qweb.addTemplate("test", '<t><t t-esc="`text ${v}`"/></t>');
qweb.addTemplate("test", '<t><t t-out="`text ${v}`"/></t>');
expect(renderToString(qweb, "test", { v: "from context" })).toBe("text from context");
});
test("simple string, with some dynamic value", () => {
qweb.addTemplate("test", '<t>hello <t t-esc="text"/></t>');
qweb.addTemplate("test", '<t>hello <t t-out="text"/></t>');
expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom");
});
@@ -149,47 +150,47 @@ describe("error handling", () => {
});
});
describe("t-esc", () => {
describe("t-out", () => {
test("literal", () => {
qweb.addTemplate("test", `<span><t t-esc="'ok'"/></span>`);
qweb.addTemplate("test", `<span><t t-out="'ok'"/></span>`);
expect(renderToString(qweb, "test")).toBe("<span>ok</span>");
});
test("variable", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
qweb.addTemplate("test", `<span><t t-out="var"/></span>`);
expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>");
});
test("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
qweb.addTemplate("test", `<span><t t-out="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>abc</ok>" })).toBe(
"<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>"
);
});
test("escaping on a node", () => {
qweb.addTemplate("test", `<span t-esc="'ok'"/>`);
qweb.addTemplate("test", `<span t-out="'ok'"/>`);
expect(renderToString(qweb, "test")).toBe("<span>ok</span>");
});
test("escaping on a node with a body", () => {
qweb.addTemplate("test", `<span t-esc="'ok'">nope</span>`);
qweb.addTemplate("test", `<span t-out="'ok'">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>ok</span>");
});
test("escaping on a node with a body, as a default", () => {
qweb.addTemplate("test", `<span t-esc="var">nope</span>`);
qweb.addTemplate("test", `<span t-out="var">nope</span>`);
expect(renderToString(qweb, "test")).toBe("<span>nope</span>");
});
test("t-esc is escaped", () => {
qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-esc="var"/></div>`);
test("t-out is escaped", () => {
qweb.addTemplate("test", `<div><t t-set="var"><p>escaped</p></t><t t-out="var"/></div>`);
const domRendered = renderToDOM(qweb, "test");
expect(domRendered.textContent).toBe("<p>escaped</p>");
});
test("t-esc=0 is escaped", () => {
qweb.addTemplate("test", `<span><t t-esc="0"/></span>`);
test("t-out=0 is escaped", () => {
qweb.addTemplate("test", `<span><t t-out="0"/></span>`);
qweb.addTemplate("testCaller", `<div><t t-call="test"><p>escaped</p></t></div>`);
const domRendered = renderToDOM(qweb, "testCaller") as HTMLElement;
expect(domRendered.querySelector("span")!.textContent).toBe("<p>escaped</p>");
@@ -200,11 +201,11 @@ describe("t-esc", () => {
"test",
`
<div>
<p t-esc="v1"/>
<p t-esc="v2"/>
<p t-esc="v3"/>
<p t-esc="v4"/>
<p t-esc="v5"/>
<p t-out="v1"/>
<p t-out="v2"/>
<p t-out="v3"/>
<p t-out="v4"/>
<p t-out="v5"/>
</div>`
);
const vals = {
@@ -219,46 +220,51 @@ describe("t-esc", () => {
);
});
test("t-esc work with spread operator", () => {
qweb.addTemplate("test", `<span><t t-esc="[...state.list]"/></span>`);
test("t-out work with spread operator", () => {
qweb.addTemplate("test", `<span><t t-out="[...state.list]"/></span>`);
const result = renderToString(qweb, "test", { state: { list: [1, 2] } });
expect(result).toBe("<span>1,2</span>");
});
test("t-esc inside t-call, with t-set outside", () => {
test("t-out inside t-call, with t-set outside", () => {
qweb.addTemplate("main", `<div><t t-set="v">Hi</t><t t-call="sub"/></div>`);
qweb.addTemplate("sub", `<span t-esc="v"/>`);
qweb.addTemplate("sub", `<span t-out="v"/>`);
const result = renderToString(qweb, "main");
expect(result).toBe("<div><span>Hi</span></div>");
});
});
describe("t-raw", () => {
describe("t-out (formerly t-raw tests)", () => {
test("literal", () => {
qweb.addTemplate("test", `<span><t t-raw="'ok'"/></span>`);
qweb.addTemplate("test", `<span><t t-out="'ok'"/></span>`);
expect(renderToString(qweb, "test")).toBe("<span>ok</span>");
});
test("variable", () => {
qweb.addTemplate("test", `<span><t t-raw="var"/></span>`);
qweb.addTemplate("test", `<span><t t-out="var"/></span>`);
expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>");
});
test("not escaping", () => {
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
test("not escaping (Markup as template function)", () => {
qweb.addTemplate("test", `<div><t t-out="var"/></div>`);
expect(renderToString(qweb, "test", { var: Markup`<ok></ok>` })).toBe("<div><ok></ok></div>");
});
test("not escaping (Markup as function)", () => {
qweb.addTemplate("test", `<div><t t-out="var"/></div>`);
expect(renderToString(qweb, "test", { var: Markup(`<ok></ok>`) })).toBe("<div><ok></ok></div>");
});
test("t-raw and another sibling node", () => {
qweb.addTemplate("test", `<span><span>hello</span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>world</ok>" })).toBe(
expect(renderToString(qweb, "test", { var: Markup`<ok>world</ok>` })).toBe(
"<span><span>hello</span><ok>world</ok></span>"
);
});
test("t-raw with comment", () => {
qweb.addTemplate("test", `<span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<p>text<!-- top secret --></p>" })).toBe(
expect(renderToString(qweb, "test", { var: Markup`<p>text<!-- top secret --></p>` })).toBe(
"<span><p>text<!-- top secret --></p></span>"
);
});
@@ -266,7 +272,7 @@ describe("t-raw", () => {
describe("t-set", () => {
test("set from attribute literal", () => {
qweb.addTemplate("test", `<div><t t-set="value" t-value="'ok'"/><t t-esc="value"/></div>`);
qweb.addTemplate("test", `<div><t t-set="value" t-value="'ok'"/><t t-out="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>ok</div>");
});
@@ -282,12 +288,12 @@ describe("t-set", () => {
});
test("set from body literal", () => {
qweb.addTemplate("test", `<t><t t-set="value">ok</t><t t-esc="value"/></t>`);
qweb.addTemplate("test", `<t><t t-set="value">ok</t><t t-out="value"/></t>`);
expect(renderToString(qweb, "test")).toBe("ok");
});
test("set from attribute lookup", () => {
qweb.addTemplate("test", `<div><t t-set="stuff" t-value="value"/><t t-esc="stuff"/></div>`);
qweb.addTemplate("test", `<div><t t-set="stuff" t-value="value"/><t t-out="stuff"/></div>`);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
});
@@ -296,8 +302,8 @@ describe("t-set", () => {
"test",
`<div >
<t t-set="v" t-value="value + ' artois'"/>
<t t-esc="v"/>
<t t-esc="v"/>
<t t-out="v"/>
<t t-out="v"/>
</div>`
);
expect(renderToString(qweb, "test", { value: "stella" })).toBe(
@@ -308,23 +314,23 @@ describe("t-set", () => {
test("set from body lookup", () => {
qweb.addTemplate(
"test",
`<div><t t-set="stuff"><t t-esc="value"/></t><t t-esc="stuff"/></div>`
`<div><t t-set="stuff"><t t-out="value"/></t><t t-out="stuff"/></div>`
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("<div>ok</div>");
});
test("set from empty body", () => {
qweb.addTemplate("test", `<div><t t-set="stuff"/><t t-esc="stuff"/></div>`);
qweb.addTemplate("test", `<div><t t-set="stuff"/><t t-out="stuff"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div></div>");
});
test("value priority", () => {
qweb.addTemplate("test", `<div><t t-set="value" t-value="1">2</t><t t-esc="value"/></div>`);
qweb.addTemplate("test", `<div><t t-set="value" t-value="1">2</t><t t-out="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>1</div>");
});
test("evaluate value expression", () => {
qweb.addTemplate("test", `<div><t t-set="value" t-value="1 + 2"/><t t-esc="value"/></div>`);
qweb.addTemplate("test", `<div><t t-set="value" t-value="1 + 2"/><t t-out="value"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>3</div>");
});
@@ -334,7 +340,7 @@ describe("t-set", () => {
`<div>
<t t-set="v" t-value="1"/>
<div t-foreach="list" t-as="elem" t-key="elem_index">
<span>v<t t-esc="v"/></span>
<span>v<t t-out="v"/></span>
<t t-set="v" t-value="elem"/>
</div>
</div>`
@@ -344,12 +350,12 @@ describe("t-set", () => {
);
});
test("t-set with content and sub t-esc", () => {
test("t-set with content and sub t-out", () => {
qweb.addTemplate(
"test",
`<div>
<t t-set="setvar"><t t-esc="beep"/> boop</t>
<t t-esc="setvar"/>
<t t-set="setvar"><t t-out="beep"/> boop</t>
<t t-out="setvar"/>
</div>`
);
@@ -359,7 +365,7 @@ describe("t-set", () => {
test("evaluate value expression, part 2", () => {
qweb.addTemplate(
"test",
`<div><t t-set="value" t-value="somevariable + 2"/><t t-esc="value"/></div>`
`<div><t t-set="value" t-value="somevariable + 2"/><t t-out="value"/></div>`
);
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe("<div>45</div>");
});
@@ -370,7 +376,7 @@ describe("t-set", () => {
`<div>
<t t-if="flag" t-set="ourvar">1</t>
<t t-else="" t-set="ourvar" t-value="0"></t>
<t t-esc="ourvar"/>
<t t-out="ourvar"/>
</div>`
);
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
@@ -383,7 +389,7 @@ describe("t-set", () => {
`<div>
<t t-if="flag" t-set="ourvar" t-value="1"></t>
<t t-else="" t-set="ourvar">0</t>
<t t-esc="ourvar"/>
<t t-out="ourvar"/>
</div>`
);
expect(renderToString(qweb, "test", { flag: true })).toBe("<div>1</div>");
@@ -396,10 +402,10 @@ describe("t-set", () => {
`<div>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2">
<span><t t-esc="v1"/></span>
<span><t t-out="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-raw="v2"/>
<t t-out="v2"/>
</div>`
);
@@ -413,7 +419,7 @@ describe("t-set", () => {
<t t-set="v3" t-value="false"/>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2" t-value="v3">
<span><t t-esc="v1"/></span>
<span><t t-out="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="true"/>
@@ -431,7 +437,7 @@ describe("t-set", () => {
<t t-set="v3" t-value="'Truthy'"/>
<t t-set="v1" t-value="'before'"/>
<t t-set="v2" t-value="v3">
<span><t t-esc="v1"/></span>
<span><t t-out="v1"/></span>
</t>
<t t-set="v1" t-value="'after'"/>
<t t-set="v3" t-value="false"/>
@@ -522,13 +528,13 @@ describe("t-if", () => {
expect(normalize(renderToString(qweb, "test", context))).toBe("<div>andormgtnlt</div>");
});
test("t-esc with t-if", () => {
qweb.addTemplate("test", `<div><t t-if="true" t-esc="'x'"/></div>`);
test("t-out with t-if", () => {
qweb.addTemplate("test", `<div><t t-if="true" t-out="'x'"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>x</div>");
});
test("t-esc with t-elif", () => {
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="" t-esc="'x'"/></div>`);
test("t-out with t-elif", () => {
qweb.addTemplate("test", `<div><t t-if="false">abc</t><t t-else="" t-out="'x'"/></div>`);
expect(renderToString(qweb, "test")).toBe("<div>x</div>");
});
@@ -538,7 +544,7 @@ describe("t-if", () => {
`
<div>
<t t-set="title" t-value="'test'"/>
<t t-if="title"><t t-esc="title"/></t>
<t t-if="title"><t t-out="title"/></t>
</div>`
);
const result = renderToString(qweb, "test");
@@ -812,14 +818,14 @@ describe("t-call (template calling", () => {
});
test("with used body", () => {
qweb.addTemplate("_callee-printsbody", '<h1><t t-esc="0"/></h1>');
qweb.addTemplate("_callee-printsbody", '<h1><t t-out="0"/></h1>');
qweb.addTemplate("caller", '<t t-call="_callee-printsbody">ok</t>');
const expected = "<h1>ok</h1>";
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("with used set body", () => {
qweb.addTemplate("_callee-uses-foo", '<t t-esc="foo"/>');
qweb.addTemplate("_callee-uses-foo", '<t t-out="foo"/>');
qweb.addTemplate(
"caller",
`
@@ -830,7 +836,7 @@ describe("t-call (template calling", () => {
});
test("inherit context", () => {
qweb.addTemplate("_callee-uses-foo", '<t t-esc="foo"/>');
qweb.addTemplate("_callee-uses-foo", '<t t-out="foo"/>');
qweb.addTemplate(
"caller",
`
@@ -849,7 +855,7 @@ describe("t-call (template calling", () => {
<t t-call="_basic-callee">
<t t-set="foo" t-value="42"/>
</t>
<t t-esc="foo"/>
<t t-out="foo"/>
</div>
`
);
@@ -937,7 +943,7 @@ describe("t-call (template calling", () => {
</t>
</div>
<div t-name="nodeTemplate">
<p><t t-esc="node.val"/></p>
<p><t t-out="node.val"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
@@ -966,7 +972,7 @@ describe("t-call (template calling", () => {
</t>
</div>
<div t-name="nodeTemplate">
<p><t t-esc="node.val"/></p>
<p><t t-out="node.val"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
@@ -996,7 +1002,7 @@ describe("t-call (template calling", () => {
</div>
<div t-name="nodeTemplate">
<t t-set="recursive_idx" t-value="recursive_idx + 1"/>
<p><t t-esc="node.val"/> <t t-esc="recursive_idx"/></p>
<p><t t-out="node.val"/> <t t-out="recursive_idx"/></p>
<t t-foreach="node.children or []" t-as="subtree">
<t t-call="nodeTemplate">
<t t-set="node" t-value="subtree"/>
@@ -1027,7 +1033,7 @@ describe("t-call (template calling", () => {
test("t-call, conditional and t-set in t-call body", () => {
QWeb.registerTemplate("callee1", "<div>callee1</div>");
QWeb.registerTemplate("callee2", '<div>callee2 <t t-esc="v"/></div>');
QWeb.registerTemplate("callee2", '<div>callee2 <t t-out="v"/></div>');
QWeb.registerTemplate(
"caller",
`<div>
@@ -1054,7 +1060,7 @@ describe("t-call (template calling", () => {
</t>
</div>
<t t-name="sub">
<span t-esc="val3"/>
<span t-out="val3"/>
</t>
</templates>
`);
@@ -1075,8 +1081,8 @@ describe("t-call (template calling", () => {
</t>
</div>
<t t-name="sub">
<span t-esc="val3"/>
<t t-esc="w"/>
<span t-out="val3"/>
<t t-out="w"/>
</t>
<p t-name="wrapper"><t t-set="w" t-value="'fromwrapper'"/><t t-call="main"/></p>
</templates>
@@ -1099,7 +1105,7 @@ describe("t-call (template calling", () => {
});
test("t-call with t-set inside and body text content", () => {
qweb.addTemplate("sub", `<p><t t-esc="val"/></p>`);
qweb.addTemplate("sub", `<p><t t-out="val"/></p>`);
qweb.addTemplate(
"main",
`
@@ -1121,8 +1127,8 @@ describe("t-call (template calling", () => {
});
test("dynamic t-call", () => {
qweb.addTemplate("foo", `<foo><t t-esc="val"/></foo>`);
qweb.addTemplate("bar", `<bar><t t-esc="val"/></bar>`);
qweb.addTemplate("foo", `<foo><t t-out="val"/></foo>`);
qweb.addTemplate("bar", `<bar><t t-out="val"/></bar>`);
qweb.addTemplate("main", `<div><t t-call="{{template}}"/></div>`);
const expected = "<div><foo>foo</foo></div>";
expect(renderToString(qweb, "main", { template: "foo", val: "foo" })).toBe(expected);
@@ -1141,7 +1147,7 @@ describe("foreach", () => {
`
<div>
<t t-foreach="[3, 2, 1]" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
[<t t-out="item_index"/>: <t t-out="item"/> <t t-out="item_value"/>]
</t>
</div>`
);
@@ -1155,7 +1161,7 @@ describe("foreach", () => {
"test",
`
<div>
<span t-foreach="[1, 2]" t-as="item" t-key="item"><t t-esc="item"/></span>
<span t-foreach="[1, 2]" t-as="item" t-key="item"><t t-out="item"/></span>
</div>`
);
const result = trim(renderToString(qweb, "test"));
@@ -1169,7 +1175,7 @@ describe("foreach", () => {
`
<div>
<t t-foreach="Array(5)" t-as="elem">
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-esc="elem_index"/>)
-<t t-if="elem_first"> first</t><t t-if="elem_last"> last</t> (<t t-out="elem_index"/>)
</t>
</div>`
);
@@ -1184,7 +1190,7 @@ describe("foreach", () => {
`
<div>
<t t-foreach="value" t-as="item">
[<t t-esc="item_index"/>: <t t-esc="item"/> <t t-esc="item_value"/>]
[<t t-out="item_index"/>: <t t-out="item"/> <t t-out="item_value"/>]
</t>
</div>`
);
@@ -1197,7 +1203,7 @@ describe("foreach", () => {
qweb.addTemplate(
"test",
`<div>
<t t-foreach="[1]" t-as="item"><t t-esc="item"/></t>
<t t-foreach="[1]" t-as="item"><t t-out="item"/></t>
</div>`
);
const context = { __owl__: {} };
@@ -1211,7 +1217,7 @@ describe("foreach", () => {
`<div>
<t t-foreach="numbers" t-as="number">
<t t-foreach="letters" t-as="letter">
[<t t-esc="number"/><t t-esc="letter"/>]
[<t t-out="number"/><t t-out="letter"/>]
</t>
</t>
</div>`
@@ -1227,9 +1233,9 @@ describe("foreach", () => {
"test_called",
`<t>
<t t-set="c" t-value="'x' + '_' + a + '_'+ b" />
[<t t-esc="a" />]
[<t t-esc="b" />]
[<t t-esc="c" />]
[<t t-out="a" />]
[<t t-out="b" />]
[<t t-out="c" />]
</t>`
);
qweb.addTemplate(
@@ -1239,9 +1245,9 @@ describe("foreach", () => {
<t t-foreach="letters" t-as="b">
<t t-call="test_called" />
</t>
<span t-esc="c"/>
<span t-out="c"/>
</t>
<span>[<t t-esc="a" />][<t t-esc="b" />][<t t-esc="c" />]</span>
<span>[<t t-out="a" />][<t t-out="b" />][<t t-out="c" />]</span>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
@@ -1254,9 +1260,9 @@ describe("foreach", () => {
qweb.addTemplate(
"test_called",
`<t>
[<t t-esc="a" />]
[<t t-esc="b" />]
[<t t-esc="c" />]
[<t t-out="a" />]
[<t t-out="b" />]
[<t t-out="c" />]
</t>`
);
qweb.addTemplate(
@@ -1268,9 +1274,9 @@ describe("foreach", () => {
<t t-set="c" t-value="'x' + '_' + a + '_'+ b" />
</t>
</t>
<span t-esc="c"/>
<span t-out="c"/>
</t>
<span>[<t t-esc="a" />][<t t-esc="b" />][<t t-esc="c" />]</span>
<span>[<t t-out="a" />][<t t-out="b" />][<t t-out="c" />]</span>
</div>`
);
const context = { numbers: [1, 2, 3], letters: ["a", "b"] };
@@ -1296,7 +1302,7 @@ describe("foreach", () => {
`
<div>
<t t-foreach="[1, 2]" t-as="item">
<span><t t-esc="item"/></span>
<span><t t-out="item"/></span>
</t>
</div>`
);
@@ -1312,14 +1318,14 @@ describe("foreach", () => {
describe("misc", () => {
test("global", () => {
qweb.addTemplate("_callee-asc", `<año t-att-falló="'agüero'" t-raw="0"/>`);
qweb.addTemplate("_callee-uses-foo", `<span t-esc="foo">foo default</span>`);
qweb.addTemplate("_callee-uses-foo", `<span t-out="foo">foo default</span>`);
qweb.addTemplate("_callee-asc-toto", `<div t-raw="toto">toto default</div>`);
qweb.addTemplate(
"caller",
`
<div>
<t t-foreach="[4,5,6]" t-as="value">
<span t-esc="value"/>
<span t-out="value"/>
<t t-call="_callee-asc">
<t t-call="_callee-uses-foo">
<t t-set="foo" t-value="'aaa'"/>
@@ -1719,7 +1725,7 @@ describe("t-on", () => {
`<div>
<t t-foreach="projects" t-as="project">
<a href="#" t-key="project" t-on-click.prevent="onEdit(project.id)">
Edit <t t-esc="project.name"/>
Edit <t t-out="project.name"/>
</a>
</t>
</div>`
@@ -1767,9 +1773,9 @@ describe("t-on", () => {
button.click();
});
test("t-on combined with t-esc", async () => {
test("t-on combined with t-out", async () => {
expect.assertions(3);
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-esc="text"/></div>`);
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-out="text"/></div>`);
const steps: string[] = [];
const owner = {
text: "Click here",
@@ -1852,7 +1858,7 @@ describe("t-ref", () => {
`
<div>
<t t-foreach="items" t-as="item">
<div t-ref="{{item}}" t-key="item"><t t-esc="item"/></div>
<div t-ref="{{item}}" t-key="item"><t t-out="item"/></div>
</t>
</div>`
);
@@ -2038,7 +2044,7 @@ describe("whitespace handling", () => {
describe("t-key", () => {
test("can use t-key directive on a node", () => {
qweb.addTemplate("test", `<div t-key="beer.id"><t t-esc="beer.name"/></div>`);
qweb.addTemplate("test", `<div t-key="beer.id"><t t-out="beer.name"/></div>`);
expect(renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })).toBe(
"<div>Chimay Rouge</div>"
);
@@ -2048,7 +2054,7 @@ describe("t-key", () => {
qweb.addTemplate(
"test",
`<ul>
<li t-foreach="beers" t-as="beer" t-key="beer.id"><t t-esc="beer.name"/></li>
<li t-foreach="beers" t-as="beer" t-key="beer.id"><t t-out="beer.name"/></li>
</ul>`
);
expect(
+66
View File
@@ -0,0 +1,66 @@
import { QWeb } from "../../src/qweb/index";
import { renderToString } from "../helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - qweb: a new QWeb instance
let qweb: QWeb;
beforeEach(() => {
QWeb.TEMPLATES = {};
QWeb.nextId = 1;
qweb = new QWeb();
});
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("old t-esc directive", () => {
test("simple dynamic value", () => {
qweb.addTemplate("test", '<t><t t-esc="text"/></t>');
expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom");
});
test("escaping", () => {
qweb.addTemplate("test", `<span><t t-esc="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>abc</ok>" })).toBe(
"<span>&amp;lt;ok&amp;gt;abc&amp;lt;/ok&amp;gt;</span>"
);
});
});
describe("old t-raw directive", () => {
test("literal", () => {
qweb.addTemplate("test", `<span><t t-raw="'ok'"/></span>`);
expect(renderToString(qweb, "test")).toBe("<span>ok</span>");
});
test("variable", () => {
qweb.addTemplate("test", `<span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "ok" })).toBe("<span>ok</span>");
});
test("not escaping", () => {
qweb.addTemplate("test", `<div><t t-raw="var"/></div>`);
expect(renderToString(qweb, "test", { var: "<ok></ok>" })).toBe("<div><ok></ok></div>");
});
test("t-raw and another sibling node", () => {
qweb.addTemplate("test", `<span><span>hello</span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<ok>world</ok>" })).toBe(
"<span><span>hello</span><ok>world</ok></span>"
);
});
test("t-raw with comment", () => {
qweb.addTemplate("test", `<span><t t-raw="var"/></span>`);
expect(renderToString(qweb, "test", { var: "<p>text<!-- top secret --></p>" })).toBe(
"<span><p>text<!-- top secret --></p></span>"
);
});
});
+2 -2
View File
@@ -13,8 +13,8 @@
// "compileOnSave": false,                   // Signals to the IDE to generate all files for a given tsconfig.json upon saving.
"compilerOptions": {
                                                            // Main options
"target": "esnext",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
"module": "esnext",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
"target": "es2017",                                         // Specify ECMAScript target version: 'es3' (default), 'es5', 'es2015', 'es2016', 'es2017','es2018' or 'esnext'.
"module": "es6",                                         // Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'.
// "lib": ["esnext", "dom"],                 // Specify library files to be included in the compilation.
// "allowJs": false,                 // Allow javascript files to be compiled.
// "checkJs": false,                 // Report errors in .js files.