import { QWeb } from "../../src/qweb/index"; import { normalize, renderToDOM, renderToString, trim, nextTick } from "../helpers"; //------------------------------------------------------------------------------ // Setup and helpers //------------------------------------------------------------------------------ // We create before each test: // - qweb: a new QWeb instance let qweb: QWeb; beforeEach(() => { qweb = new QWeb(); }); //------------------------------------------------------------------------------ // Tests //------------------------------------------------------------------------------ describe("static templates", () => { test("simple string", () => { qweb.addTemplate("test", "hello vdom"); expect(renderToString(qweb, "test")).toBe("hello vdom"); }); test("simple dynamic value", () => { qweb.addTemplate("test", ''); expect(renderToString(qweb, "test", { text: "hello vdom" })).toBe("hello vdom"); }); test("simple string, with some dynamic value", () => { qweb.addTemplate("test", 'hello '); expect(renderToString(qweb, "test", { text: "vdom" })).toBe("hello vdom"); }); test("empty div", () => { qweb.addTemplate("test", "
"); expect(renderToString(qweb, "test")).toBe("
"); }); test("div with a text node", () => { qweb.addTemplate("test", "
word
"); expect(renderToString(qweb, "test")).toBe("
word
"); }); test("div with a span child node", () => { qweb.addTemplate("test", "
word
"); expect(renderToString(qweb, "test")).toBe("
word
"); }); }); describe("error handling", () => { test("invalid xml", () => { expect(() => 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", "
", true)).not.toThrow("already defined"); expect(() => qweb.addTemplate("test", "
")).toThrow("already defined"); }); test("loadTemplates throw if parser error", () => { expect(() => { qweb.addTemplates(">"); }).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("nice error when t-on is evaluated with a missing event", () => { qweb.addTemplate("templatename", `
`); expect(() => qweb.render("templatename", { someMethod() {} }, { handlers: [] })).toThrow( "Missing event name with t-on directive" ); }); 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( "helloworld" ); }); }); 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("t-set evaluates an expression only once", () => { qweb.addTemplate( "test", `
` ); expect(renderToString(qweb, "test", { value: "stella" })).toBe( "
stella artoisstella artois
" ); }); 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("t-set should reuse variable if possible", () => { qweb.addTemplate( "test", `
v
` ); expect(normalize(renderToString(qweb, "test", { list: ["a", "b"] }))).toBe( "
v1
va
" ); }); 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("
beginokend
"); }); test("boolean value condition false else", () => { qweb.addTemplate( "test", `
beginfail fail-elseend
` ); const result = trim(renderToString(qweb, "test", { condition: false })); expect(result).toBe("
beginfail-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", `Test`); const result = renderToString(qweb, "test"); expect(result).toBe(`Test`); }); 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 expression, other format", () => { 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("class and t-attf-class with ternary operation", () => { qweb.addTemplate("test", `
`); const result = renderToString(qweb, "test", { value: true }); 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 with t-if", () => { qweb.addTemplate("sub", "ok"); qweb.addTemplate("caller", '
'); const expected = "
ok
"; expect(renderToString(qweb, "caller", { flag: true })).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 = `
12
`; expect(result).toBe(expected); }); test("iterate, position", () => { qweb.addTemplate( "test", `
- first last ()
` ); const result = trim(renderToString(qweb, "test")); const expected = `
-first(0)-(1)-(2)-(3)-last(4)
`; 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][1:b2][2:c3]
`; 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"); }); test("warn if no key in some case", () => { const consoleWarn = console.warn; console.warn = jest.fn(); qweb.addTemplate( "test", `
` ); renderToString(qweb, "test"); expect(console.warn).toHaveBeenCalledTimes(1); console.warn = consoleWarn; }); }); 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(`
4 aaa foo default bbb 5 aaa foo default bbb 6 aaa foo default bbb
toto default
`); expect(result).toBe(expected); }); }); describe("t-on", () => { test("can bind event handler", () => { qweb.addTemplate("test", ``); 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", `` ); 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", ``); 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", ``); 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", ``); 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", ``); 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", ``); let owner = { add() { expect(this).toBe(owner); } }; const node = renderToDOM(qweb, "test", owner, { handlers: [] }); (node).click(); }); test("t-on with prevent and/or stop modifiers", async () => { expect.assertions(7); qweb.addTemplate( "test", `
` ); let owner = { onClickPrevented(e) { expect(e.defaultPrevented).toBe(true); expect(e.cancelBubble).toBe(false); }, onClickStopped(e) { expect(e.defaultPrevented).toBe(false); expect(e.cancelBubble).toBe(true); }, onClickPreventedAndStopped(e) { expect(e.defaultPrevented).toBe(true); expect(e.cancelBubble).toBe(true); } }; const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const buttons = (node).getElementsByTagName("button"); buttons[0].click(); buttons[1].click(); buttons[2].click(); }); test("t-on with self modifier", async () => { expect.assertions(2); qweb.addTemplate( "test", `
` ); let steps: string[] = []; let owner = { onClick(e) { steps.push("onClick"); }, onClickSelf(e) { steps.push("onClickSelf"); } }; const node = renderToDOM(qweb, "test", owner, { handlers: [] }); const buttons = (node).getElementsByTagName("button"); const spans = (node).getElementsByTagName("span"); spans[0].click(); spans[1].click(); buttons[0].click(); buttons[1].click(); expect(steps).toEqual(["onClick", "onClick", "onClickSelf"]); }); test("t-on with self and prevent modifiers (order matters)", async () => { expect.assertions(2); qweb.addTemplate( "test", `
` ); let steps: boolean[] = []; let owner = { onClick() {} }; const node = renderToDOM(qweb, "test", owner, { handlers: [] }); (node).addEventListener("click", function(e) { steps.push(e.defaultPrevented); }); const button = (node).getElementsByTagName("button")[0]; const span = (node).getElementsByTagName("span")[0]; span.click(); button.click(); expect(steps).toEqual([false, true]); }); test("t-on with prevent and self modifiers (order matters)", async () => { expect.assertions(2); qweb.addTemplate( "test", `
` ); let steps: boolean[] = []; let owner = { onClick() {} }; const node = renderToDOM(qweb, "test", owner, { handlers: [] }); (node).addEventListener("click", function(e) { steps.push(e.defaultPrevented); }); const button = (node).getElementsByTagName("button")[0]; const span = (node).getElementsByTagName("span")[0]; span.click(); button.click(); expect(steps).toEqual([true, true]); }); }); 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, id: 3 }); expect(refs.myspan3.tagName).toBe("SPAN"); }); test("refs in a loop", () => { qweb.addTemplate( "test", `
` ); let refs: any = {}; renderToDOM(qweb, "test", { refs, items: [1, 2, 3] }); expect(Object.keys(refs)).toEqual(["1", "2", "3"]); }); }); 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.addTemplates(data); const result = renderToString(qweb, "main"); expect(result).toBe("
    • ok
    • foo
    "); }); test("does not crash if string does not have templates", () => { const data = ""; qweb.addTemplates(data); expect(Object.keys(qweb.templates)).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); }); }); describe("t-key", () => { test("can use t-key directive on a node", () => { qweb.addTemplate("test", `
    `); expect(renderToString(qweb, "test", { beer: { id: 12, name: "Chimay Rouge" } })).toBe( "
    Chimay Rouge
    " ); }); test("t-key directive in a list", () => { qweb.addTemplate( "test", `
    ` ); expect( renderToString(qweb, "test", { beers: [{ id: 12, name: "Chimay Rouge" }] }) ).toMatchSnapshot(); }); }); describe("debugging", () => { test("t-debug", () => { const consoleLog = console.log; console.log = jest.fn(); qweb.addTemplate( "test", `
    hey
    ` ); qweb.render("test"); expect(qweb.templates.test.fn.toString()).toMatchSnapshot(); expect(console.log).toHaveBeenCalledTimes(1); console.log = consoleLog; }); test("t-log", () => { const consoleLog = console.log; console.log = jest.fn(); qweb.addTemplate( "test", `
    ` ); qweb.render("test"); expect(qweb.templates.test.fn.toString()).toMatchSnapshot(); expect(console.log).toHaveBeenCalledWith(45); console.log = consoleLog; }); }); describe("update on event bus", () => { test("two consecutive forceUpdates only causes one listener update", async () => { const fn = jest.fn(); qweb.on("update", null, fn); qweb.forceUpdate(); qweb.forceUpdate(); expect(fn).toBeCalledTimes(0); await nextTick(); expect(fn).toBeCalledTimes(1); }); });