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", "
")).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(
"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("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",
``
);
expect(normalize(renderToString(qweb, "test", { list: ["a", "b"] }))).toBe(
""
);
});
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 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 = `1 2
`;
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(`
`);
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();
});
test("t-on with prevent and/or stop modifiers", async () => {
expect.assertions(7);
qweb.addTemplate(
"test",
`
Button 1
Button 2
Button 3
`
);
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",
`
Button
Button
`
);
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",
`
Button
`
);
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",
`
Button
`
);
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("");
});
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);
});
});