import sdAttributes from "../libs/snabbdom/src/modules/attributes";
import sdListeners from "../libs/snabbdom/src/modules/eventlisteners";
import { init } from "../libs/snabbdom/src/snabbdom";
import { EvalContext, QWeb } from "../src/qweb";
import { normalize } from "./helpers";
//------------------------------------------------------------------------------
// Setup and helpers
//------------------------------------------------------------------------------
// We create before each test:
// - qweb: a new QWeb instance
const patch = init([sdAttributes, sdListeners]);
let qweb: QWeb;
beforeEach(() => {
qweb = new QWeb();
});
function trim(str: string): string {
return str.replace(/\s/g, "");
}
function renderToDOM(
qweb: QWeb,
template: string,
context: EvalContext = {},
extra?: any
): HTMLElement | Text {
const vnode = qweb.render(template, context, extra);
// we snapshot here the compiled code. This is useful to prevent unwanted code
// change.
expect(qweb.templates[template].toString()).toMatchSnapshot();
if (vnode.sel === undefined) {
return document.createTextNode(vnode.text!);
}
const node = document.createElement(vnode.sel!);
const result = patch(node, vnode);
return result.elm as HTMLElement;
}
function renderToString(
qweb: QWeb,
t: string,
context: EvalContext = {}
): string {
const node = renderToDOM(qweb, t, context);
return node instanceof Text ? node.textContent! : node.outerHTML;
}
//------------------------------------------------------------------------------
// Tests
//------------------------------------------------------------------------------
describe("static templates", () => {
test("simple string", () => {
qweb.addTemplate("test", "hello vdom ");
expect(renderToString(qweb, "test")).toBe("hello vdom");
});
test("empty div", () => {
qweb.addTemplate("test", "
")).toThrow(
"Invalid XML in template"
);
});
test("template with text node and tag", () => {
qweb.addTemplate("test", `
textother node `);
expect(() => renderToString(qweb, "test")).toThrow(
"A template should not have more than one root node"
);
});
test("nice warning if no template with given name", () => {
expect(() => qweb.render("invalidname")).toThrow("does not exist");
});
test("cannot add twice the same template", () => {
qweb.addTemplate("test", `
`);
expect(() => qweb.addTemplate("test", "
")).toThrow("already defined");
});
test("loadTemplates throw if parser error", () => {
expect(() => {
qweb.loadTemplates("
> ");
}).toThrow("Invalid XML in template");
});
test("nice error when t-on-directive is evaluated with a missing handler", () => {
qweb.addTemplate("templatename", `
`);
expect(() => qweb.render("templatename", {}, { handlers: [] })).toThrow(
"Missing handler 'somemethod' when evaluating template 'templatename'"
);
});
test("error when compiled code is invalid", () => {
qweb.addTemplate(
"templatename",
`
`
);
expect(() => qweb.render("templatename")).toThrow(
"Invalid generated code while compiling template 'templatename': Unexpected token }"
);
});
test("error when unknown directive", () => {
qweb.addTemplate(
"templatename",
`
test
`
);
expect(() => qweb.render("templatename")).toThrow(
"Unknown QWeb directive: 't-best-beer'"
);
});
});
describe("t-esc", () => {
test("literal", () => {
qweb.addTemplate("test", `
`);
expect(renderToString(qweb, "test")).toBe("
ok ");
});
test("variable", () => {
qweb.addTemplate("test", `
`);
expect(renderToString(qweb, "test", { var: "ok" })).toBe("
ok ");
});
test.skip("escaping", () => {
qweb.addTemplate("test", `
`);
expect(renderToString(qweb, "test", { var: "
" })).toBe(
"<ok> "
);
});
test("escaping on a node", () => {
qweb.addTemplate("test", ` `);
expect(renderToString(qweb, "test")).toBe("ok ");
});
test("escaping on a node with a body", () => {
qweb.addTemplate("test", `nope `);
expect(renderToString(qweb, "test")).toBe("ok ");
});
test("escaping on a node with a body, as a default", () => {
qweb.addTemplate("test", `nope `);
expect(renderToString(qweb, "test")).toBe("nope ");
});
});
describe("t-raw", () => {
test("literal", () => {
qweb.addTemplate("test", ` `);
expect(renderToString(qweb, "test")).toBe("ok ");
});
test("variable", () => {
qweb.addTemplate("test", ` `);
expect(renderToString(qweb, "test", { var: "ok" })).toBe("ok ");
});
test("not escaping", () => {
qweb.addTemplate("test", `
`);
expect(renderToString(qweb, "test", { var: " " })).toBe(
"
"
);
});
test("t-raw and another sibling node", () => {
qweb.addTemplate("test", `hello `);
expect(renderToString(qweb, "test", { var: "world " })).toBe(
"hello world "
);
});
});
describe("t-set", () => {
test("set from attribute literal", () => {
qweb.addTemplate(
"test",
`
`
);
expect(renderToString(qweb, "test")).toBe("ok
");
});
test("t-set and t-if", () => {
qweb.addTemplate(
"test",
`
grimbergen
`
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe(
"grimbergen
"
);
});
test("set from body literal", () => {
qweb.addTemplate(
"test",
`ok `
);
expect(renderToString(qweb, "test")).toBe("ok");
});
test("set from attribute lookup", () => {
qweb.addTemplate(
"test",
`
`
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("ok
");
});
test("set from body lookup", () => {
qweb.addTemplate(
"test",
`
`
);
expect(renderToString(qweb, "test", { value: "ok" })).toBe("ok
");
});
test("set from empty body", () => {
qweb.addTemplate("test", `
`);
expect(renderToString(qweb, "test")).toBe("
");
});
test("value priority", () => {
qweb.addTemplate(
"test",
`2
`
);
expect(renderToString(qweb, "test")).toBe("1
");
});
test("evaluate value expression", () => {
qweb.addTemplate(
"test",
`
`
);
expect(renderToString(qweb, "test")).toBe("3
");
});
test("evaluate value expression, part 2", () => {
qweb.addTemplate(
"test",
`
`
);
expect(renderToString(qweb, "test", { somevariable: 43 })).toBe(
"45
"
);
});
});
describe("t-if", () => {
test("boolean value true condition", () => {
qweb.addTemplate("test", `ok
`);
expect(renderToString(qweb, "test", { condition: true })).toBe(
"ok
"
);
});
test("boolean value false condition", () => {
qweb.addTemplate("test", `ok
`);
expect(renderToString(qweb, "test", { condition: false })).toBe(
"
"
);
});
test("boolean value condition missing", () => {
qweb.addTemplate("test", `fail `);
expect(renderToString(qweb, "test")).toBe(" ");
});
test("boolean value condition elif", () => {
qweb.addTemplate(
"test",
`black pearl
yellow submarine
red is dead
beer
`
);
expect(renderToString(qweb, "test", { color: "red" })).toBe(
"red is dead
"
);
});
test("boolean value condition else", () => {
qweb.addTemplate(
"test",
`
begin
ok
ok-else
end
`
);
const result = trim(renderToString(qweb, "test", { condition: true }));
expect(result).toBe("begin okend
");
});
test("boolean value condition false else", () => {
qweb.addTemplate(
"test",
`begin fail
fail-else end
`
);
const result = trim(renderToString(qweb, "test", { condition: false }));
expect(result).toBe(
"begin fail-elseend
"
);
});
test("can use some boolean operators in expressions", () => {
qweb.addTemplate(
"test",
`
and
nope
or
nope
mgt
ngt
mlt
nlt
`
);
const context = {
cond1: true,
cond2: true,
cond3: false,
cond4: false,
m: 5,
n: 2
};
expect(normalize(renderToString(qweb, "test", context))).toBe(
"andormgtnlt
"
);
});
});
describe("attributes", () => {
test("static attributes", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test");
const expected = `
`;
expect(result).toBe(expected);
});
test("static attributes with dashes", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test");
const expected = `
`;
expect(result).toBe(expected);
});
test("static attributes on void elements", () => {
qweb.addTemplate("test", ` `);
const result = renderToString(qweb, "test");
expect(result).toBe(` `);
});
test("dynamic attributes", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test");
expect(result).toBe(`
`);
});
test("dynamic attribute with a dash", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", { id: 32 });
expect(result).toBe(`
`);
});
test("dynamic formatted attributes with a dash", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", { id: 32 });
expect(result).toBe(`
`);
});
test("fixed variable", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", { value: "ok" });
expect(result).toBe(`
`);
});
test("dynamic attribute falsy variable ", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", { value: false });
expect(result).toBe(`
`);
});
test("tuple literal", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test");
expect(result).toBe(`
`);
});
test("tuple variable", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", { value: ["foo", "bar"] });
expect(result).toBe(`
`);
});
test("object", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", {
value: { a: 1, b: 2, c: 3 }
});
expect(result).toBe(`
`);
});
test("format literal", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test");
expect(result).toBe(`
`);
});
test("t-attf-class should combine with class", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test");
expect(result).toBe(`
`);
});
test("format value", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", { value: "a" });
expect(result).toBe(`
`);
});
test("from variables set previously", () => {
qweb.addTemplate(
"test",
`
`
);
const result = renderToString(qweb, "test");
expect(result).toBe('
');
});
test("from object variables set previously", () => {
// Note: standard qweb does not allow this...
qweb.addTemplate(
"test",
`
`
);
const result = renderToString(qweb, "test");
expect(result).toBe('
');
});
test("format expression", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", { value: 5 });
expect(result).toBe(`
`);
});
test("format multiple", () => {
qweb.addTemplate(
"test",
`
`
);
const result = renderToString(qweb, "test", {
value1: 0,
value2: 1,
value3: 2
});
expect(result).toBe(`
`);
});
test.skip("various escapes", () => {
// not needed??
qweb.addTemplate(
"test",
`
`
);
const result = renderToString(qweb, "test", {
bar: 0,
baz: 1,
qux: { qux: "<>" }
});
const expected = `
`;
expect(result).toBe(expected);
});
test("t-att-class and class should combine together", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test", { value: "world" });
expect(result).toBe(`
`);
});
test("t-att-class with object", () => {
qweb.addTemplate(
"test",
`
`
);
const result = renderToString(qweb, "test", { b: true, d: false, f: true });
expect(result).toBe(`
`);
});
});
describe("t-call (template calling", () => {
test("basic caller", () => {
qweb.addTemplate("_basic-callee", "ok
");
qweb.addTemplate("caller", ' ');
const expected = "ok
";
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("t-call not allowed on a non t node", () => {
qweb.addTemplate("_basic-callee", "ok ");
qweb.addTemplate("caller", '
');
expect(() => renderToString(qweb, "caller")).toThrow("Invalid tag");
});
test("with unused body", () => {
qweb.addTemplate("_basic-callee", "ok
");
qweb.addTemplate("caller", 'WHEEE ');
const expected = "ok
";
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("with unused setbody", () => {
qweb.addTemplate("_basic-callee", "ok
");
qweb.addTemplate(
"caller",
' '
);
const expected = "ok
";
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("with used body", () => {
qweb.addTemplate("_callee-printsbody", ' ');
qweb.addTemplate("caller", 'ok ');
const expected = "ok ";
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("with used set body", () => {
qweb.addTemplate("_callee-uses-foo", ' ');
qweb.addTemplate(
"caller",
`
`
);
const expected = "ok ";
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("inherit context", () => {
qweb.addTemplate("_callee-uses-foo", ' ');
qweb.addTemplate(
"caller",
`
`
);
const expected = "1
";
expect(renderToString(qweb, "caller")).toBe(expected);
});
test("scoped parameters", () => {
qweb.addTemplate("_basic-callee", `ok `);
qweb.addTemplate(
"caller",
`
`
);
const expected = "ok
";
expect(trim(renderToString(qweb, "caller"))).toBe(expected);
});
});
describe("foreach", () => {
test("iterate on items", () => {
qweb.addTemplate(
"test",
`
[ : ]
`
);
const result = trim(renderToString(qweb, "test"));
const expected = `[0:33][1:22][2:11]
`;
expect(result).toBe(expected);
});
test("iterate on items (on a element node)", () => {
qweb.addTemplate(
"test",
`
`
);
const result = trim(renderToString(qweb, "test"));
const expected = `1 2
`;
expect(result).toBe(expected);
});
test("iterate, position", () => {
qweb.addTemplate(
"test",
`
- first last ( )
`
);
const result = trim(renderToString(qweb, "test"));
const expected = `-first(even)-(odd)-(even)-(odd)-last(even)
`;
expect(result).toBe(expected);
});
test("iterate, integer param", () => {
qweb.addTemplate(
"test",
`
[ : ]
`
);
const result = trim(renderToString(qweb, "test"));
const expected = `[0:00][1:11][2:22]
`;
expect(result).toBe(expected);
});
test("iterate, dict param", () => {
qweb.addTemplate(
"test",
`
[ : - ]
`
);
const result = trim(
renderToString(qweb, "test", { value: { a: 1, b: 2, c: 3 } })
);
const expected = `[0:a1-even][1:b2-odd][2:c3-even]
`;
expect(result).toBe(expected);
});
test("does not pollute the rendering context", () => {
qweb.addTemplate(
"test",
`
`
);
const context = {};
renderToString(qweb, "test", context);
expect(Object.keys(context).length).toBe(0);
});
test("throws error if invalid loop expression", () => {
qweb.addTemplate(
"test",
`
`
);
expect(() => qweb.render("test")).toThrow("Invalid loop expression");
});
});
describe("misc", () => {
test("global", () => {
qweb.addTemplate("_callee-asc", ` `);
qweb.addTemplate(
"_callee-uses-foo",
`foo default `
);
qweb.addTemplate(
"_callee-asc-toto",
`toto default
`
);
qweb.addTemplate(
"caller",
`
`
);
const result = trim(renderToString(qweb, "caller"));
const expected = trim(`
`);
expect(result).toBe(expected);
});
});
describe("t-on", () => {
test("can bind event handler", () => {
qweb.addTemplate("test", `Click `);
let a = 1;
const node = renderToDOM(
qweb,
"test",
{
add() {
a = 3;
}
},
{ handlers: [] }
);
(node).click();
expect(a).toBe(3);
});
test("can bind two event handlers", () => {
qweb.addTemplate(
"test",
`Click `
);
let steps: string[] = [];
const node = renderToDOM(
qweb,
"test",
{
handleClick() {
steps.push("click");
},
handleDblClick() {
steps.push("dblclick");
}
},
{ handlers: [] }
);
expect(steps).toEqual([]);
(node).click();
expect(steps).toEqual(["click"]);
(node).dispatchEvent(new Event("dblclick"));
expect(steps).toEqual(["click", "dblclick"]);
});
test("can bind handlers with arguments", () => {
qweb.addTemplate("test", `Click `);
let a = 1;
const node = renderToDOM(
qweb,
"test",
{
add(n) {
a = a + n;
}
},
{ handlers: [] }
);
(node).click();
expect(a).toBe(6);
});
test("can bind handlers with object arguments", () => {
qweb.addTemplate(
"test",
`Click `
);
let a = 1;
const node = renderToDOM(
qweb,
"test",
{
add({ val }) {
a = a + val;
}
},
{ handlers: [] }
);
(node).click();
expect(a).toBe(6);
});
test("can bind handlers with empty object", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`Click `
);
const node = renderToDOM(
qweb,
"test",
{
doSomething(arg) {
expect(arg).toEqual({});
}
},
{ handlers: [] }
);
(node).click();
});
test("can bind handlers with empty object (with non empty inner string", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`Click `
);
const node = renderToDOM(
qweb,
"test",
{
doSomething(arg) {
expect(arg).toEqual({});
}
},
{ handlers: [] }
);
(node).click();
});
test("can bind handlers with loop variable as argument", () => {
expect.assertions(2);
qweb.addTemplate(
"test",
`
`
);
const node = renderToDOM(
qweb,
"test",
{
activate(action) {
expect(action).toBe("someval");
}
},
{ handlers: [] }
);
(node).getElementsByTagName("a")[0].click();
});
test("handler is bound to proper owner", () => {
expect.assertions(2);
qweb.addTemplate("test", `Click `);
let owner = {
add() {
expect(this).toBe(owner);
}
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(node).click();
});
});
describe("t-ref", () => {
test("can get a ref on a node", () => {
qweb.addTemplate("test", `
`);
let refs: any = {};
renderToDOM(qweb, "test", { refs });
expect(refs.myspan.tagName).toBe("SPAN");
});
test("can get a dynamic ref on a node", () => {
qweb.addTemplate("test", `
`);
let refs: any = {};
renderToDOM(qweb, "test", { refs });
expect(refs.myspan3.tagName).toBe("SPAN");
});
});
describe("loading templates", () => {
test("can initialize qweb with a string", () => {
const data = `
jupiler
`;
const qweb = new QWeb(data);
expect(renderToString(qweb, "hey")).toBe("jupiler
");
});
test("can load a few templates from a xml string", () => {
const data = `
ok foo
`;
qweb.loadTemplates(data);
const result = renderToString(qweb, "main");
expect(result).toBe("");
});
test("does not crash if string does not have templates", () => {
const data = "";
qweb.loadTemplates(data);
expect(qweb.processedTemplates).toEqual({});
});
});
describe("special cases for some boolean html attributes/properties", () => {
test("input type= checkbox, with t-att-checked", () => {
qweb.addTemplate("test", ` `);
const result = renderToString(qweb, "test", { flag: true });
expect(result).toBe(` `);
});
test("various boolean html attributes", () => {
// the unique assertion here is the code snapshot automatically done by
// renderToString
expect.assertions(1);
qweb.addTemplate(
"test",
`
`
);
renderToString(qweb, "test", { flag: true });
});
});
describe("whitespace handling", () => {
test("white space only text nodes are condensed into a single space", () => {
qweb.addTemplate("test", `
`);
const result = renderToString(qweb, "test");
expect(result).toBe(`
`);
});
test("consecutives whitespaces are condensed into a single space", () => {
qweb.addTemplate("test", ` abc
`);
const result = renderToString(qweb, "test");
expect(result).toBe(` abc
`);
});
test("whitespace only text nodes with newlines are removed", () => {
qweb.addTemplate(
"test",
`
abc
`
);
const result = renderToString(qweb, "test");
expect(result).toBe(`abc
`);
});
test("nothing is done in pre tags", () => {
qweb.addTemplate("test", ` `);
const result = renderToString(qweb, "test");
expect(result).toBe(` `);
const pretagtext = `
some text
`;
qweb.addTemplate("test2", pretagtext);
const result2 = renderToString(qweb, "test2");
expect(result2).toBe(pretagtext);
const pretagwithonlywhitespace = `
`;
qweb.addTemplate("test3", pretagwithonlywhitespace);
const result3 = renderToString(qweb, "test3");
expect(result3).toBe(pretagwithonlywhitespace);
});
});