diff --git a/src/qweb/inline_expressions.ts b/src/qweb/inline_expressions.ts
index 24a533d8..2474c414 100644
--- a/src/qweb/inline_expressions.ts
+++ b/src/qweb/inline_expressions.ts
@@ -25,10 +25,9 @@
// Misc types, constants and helpers
//------------------------------------------------------------------------------
-const RESERVED_WORDS =
- "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
- ","
- );
+const RESERVED_WORDS = "true,false,NaN,null,undefined,debugger,console,window,in,instanceof,new,function,return,this,eval,void,Math,RegExp,Array,Object,Date".split(
+ ","
+);
const WORD_REPLACEMENT: { [key: string]: string } = Object.assign(Object.create(null), {
and: "&&",
diff --git a/tests/components/__snapshots__/concurrency.test.ts.snap b/tests/components/__snapshots__/concurrency.test.ts.snap
index d99f7854..f2fabfde 100644
--- a/tests/components/__snapshots__/concurrency.test.ts.snap
+++ b/tests/components/__snapshots__/concurrency.test.ts.snap
@@ -14,6 +14,45 @@ exports[`async rendering destroying a widget before start is over 1`] = `
}"
`;
+exports[`calling render in destroy 1`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
+
+ let block1 = createBlock(\`
\`);
+
+ return function template(ctx, node, key = \\"\\") {
+ let d1 = ctx['props'].fromA;
+ return block1([d1]);
+ }
+}"
+`;
+
+exports[`calling render in destroy 2`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
+
+ return function template(ctx, node, key = \\"\\") {
+ return component(\`C\`, {fromA: ctx['props'].fromA}, key + \`__1\`, node, ctx);
+ }
+}"
+`;
+
+exports[`calling render in destroy 3`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
+
+ return function template(ctx, node, key = \\"\\") {
+ return component(\`B\`, {fromA: ctx['state']}, key + \`__1\`, node, ctx);
+ }
+}"
+`;
+
exports[`change state and call manually render: no unnecessary rendering 1`] = `
"function anonymous(bdom, helpers
) {
diff --git a/tests/components/basics.test.ts b/tests/components/basics.test.ts
index cafe0584..c644a4a5 100644
--- a/tests/components/basics.test.ts
+++ b/tests/components/basics.test.ts
@@ -742,3 +742,291 @@ describe("basics", () => {
expect(fixture.innerHTML).toBe("");
});
});
+
+describe.skip("mount targets", () => {
+ test("can attach a component to an existing node (if same tagname)", async () => {
+ // class App extends Component {
+ // static template = xml``;
+ // state = useState({ customClass: "custom" });
+ // }
+ // const div = document.createElement("div");
+ // div.classList.add("arbitrary");
+ // div.innerHTML = `pre-existing
`;
+ // fixture.appendChild(div);
+ // const app = await mount(App, { target: div, position: "self" });
+ // expect(fixture.innerHTML).toBe(
+ // `pre-existing
app
another tag
`
+ // );
+ // expect(div).toBe(app.el);
+ // app.state.customClass = "custom2";
+ // await nextTick();
+ // expect(fixture.innerHTML).toBe(
+ // `pre-existing
app
another tag
`
+ // );
+ // expect(div).toBe(app.el);
+ // app.unmount();
+ // // This assert is a best guess
+ // // The use case it covers was not really thought through
+ // // and may change in the future
+ // expect(fixture.innerHTML).toBe("");
+ });
+
+ test("cannot attach a component to an existing node (if not same tagname)", async () => {
+ // class App extends Component {
+ // static template = xml`app `;
+ // }
+ // const div = document.createElement("div");
+ // fixture.appendChild(div);
+ // let error;
+ // try {
+ // await mount(App, { target: div, position: "self" });
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Cannot attach 'App' to target node (not same tag name)");
+ });
+
+ test("can mount a component (with position='first-child')", async () => {
+ // class App extends Component {
+ // static template = xml`app
`;
+ // }
+ // const span = document.createElement("span");
+ // fixture.appendChild(span);
+ // await mount(App, { target: fixture, position: "first-child" });
+ // expect(fixture.innerHTML).toBe("app
");
+ });
+
+ test("can mount a component (with position='last-child')", async () => {
+ // class App extends Component {
+ // static template = xml`app
`;
+ // }
+ // const span = document.createElement("span");
+ // fixture.appendChild(span);
+ // await mount(App, { target: fixture, position: "last-child" });
+ // expect(fixture.innerHTML).toBe("app
");
+ });
+
+ test("default mount option is 'last-child'", async () => {
+ // class App extends Component {
+ // static template = xml`app
`;
+ // }
+ // const span = document.createElement("span");
+ // fixture.appendChild(span);
+ // await mount(App, { target: fixture });
+ // expect(fixture.innerHTML).toBe("app
");
+ });
+});
+
+describe.skip("mount special cases", () => {
+ test("widget can be mounted on different target", async () => {
+ // class MyWidget extends Component {
+ // static template = xml`Hey
`;
+ // patched() {
+ // throw new Error("patched should not be called");
+ // }
+ // }
+ // const div = document.createElement("div");
+ // const span = document.createElement("span");
+ // fixture.appendChild(div);
+ // fixture.appendChild(span);
+ // const w = new MyWidget();
+ // await w.mount(div);
+
+ // expect(fixture.innerHTML).toBe(" ");
+
+ // await w.mount(span);
+ // expect(fixture.innerHTML).toBe("
Hey
");
+ });
+
+ test("widget can be mounted on different target, another situation", async () => {
+ // const def = makeDeferred();
+ // const steps: string[] = [];
+
+ // class MyWidget extends Component {
+ // static template = xml`Hey
`;
+ // async willStart() {
+ // return def;
+ // }
+ // patched() {
+ // throw new Error("patched should not be called");
+ // }
+ // }
+ // const div = document.createElement("div");
+ // const span = document.createElement("span");
+ // fixture.appendChild(div);
+ // fixture.appendChild(span);
+ // const w = new MyWidget();
+
+ // w.mount(div).catch(() => steps.push("1 catch"));
+
+ // await nextTick();
+ // expect(fixture.innerHTML).toBe("
");
+
+ // w.mount(span).then(() => steps.push("2 resolved"));
+
+ // // we wait two microticks because this is the number of internal promises
+ // // that need to be resolved/rejected, and because we want to prove here
+ // // that the first mount operation is cancelled immediately, and not after
+ // // one full tick.
+ // await nextMicroTick();
+ // await nextMicroTick();
+ // expect(steps).toEqual([]);
+ // await nextTick();
+ // expect(fixture.innerHTML).toBe("
");
+
+ // def.resolve();
+ // await nextTick();
+ // expect(steps).toEqual(["2 resolved"]);
+ // expect(fixture.innerHTML).toBe("
Hey
");
+ });
+
+ test("component can be mounted on same target, another situation", async () => {
+ // const def = makeDeferred();
+ // const steps: string[] = [];
+
+ // class MyWidget extends Component {
+ // static template = xml`Hey
`;
+ // async willStart() {
+ // return def;
+ // }
+ // patched() {
+ // throw new Error("patched should not be called");
+ // }
+ // }
+ // const w = new MyWidget();
+
+ // w.mount(fixture).then(() => steps.push("1 resolved"));
+
+ // await nextTick();
+ // expect(fixture.innerHTML).toBe("");
+
+ // w.mount(fixture).then(() => steps.push("2 resolved"));
+
+ // await nextTick();
+ // expect(steps).toEqual([]);
+ // expect(fixture.innerHTML).toBe("");
+
+ // def.resolve();
+ // await nextTick();
+ // expect(fixture.innerHTML).toBe("Hey
");
+ // expect(steps).toEqual(["1 resolved", "2 resolved"]);
+ });
+
+ test("mounting a destroyed widget", async () => {
+ // class MyWidget extends Component {
+ // static template = xml`Hey
`;
+ // }
+ // const w = new MyWidget();
+ // w.destroy(); // because, why not
+
+ // let error;
+ // try {
+ // await w.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(scheduler.tasks.length).toBe(0);
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Cannot mount a destroyed component");
+ // });
+
+ // test("destroying a sub-component cleans itself from parent's vnode", async () => {
+ // class C1 extends Component {
+ // static template = xml``;
+ // }
+ // class P extends Component {
+ // static components = { C1 };
+ // static template = xml``;
+ // state = {
+ // a: "first",
+ // };
+ // }
+ // const parent = new P();
+ // await parent.mount(fixture);
+ // expect(fixture.textContent).toBe("first");
+ // parent.unmount();
+ // parent.state.a = "";
+ // parent.mount(fixture);
+ // parent.state.a = "fixed";
+ // await parent.render();
+ // expect(fixture.textContent).toBe("fixed");
+ });
+
+ test("destroying a sub-component cleans itself from parent's vnode, part 2", async () => {
+ // class C1 extends Component {
+ // static template = xml``;
+ // }
+ // class P extends Component {
+ // static components = { C1 };
+ // static template = xml``;
+ // state = {
+ // a: "first",
+ // };
+ // }
+ // const parent = new P();
+ // await parent.mount(fixture);
+ // expect(fixture.textContent).toBe("firstsome text");
+ // parent.unmount();
+ // parent.state.a = "";
+ // parent.mount(fixture);
+ // parent.state.a = "fixed";
+ // await parent.render();
+ // expect(fixture.textContent).toBe("fixedsome text");
+ });
+
+ test("destroying a sub-component cleans itself from parent's vnode, part 3", async () => {
+ // class C1 extends Component {
+ // static template = xml``;
+ // }
+
+ // class C2 extends Component {
+ // static template = xml` `;
+ // static components = { C1 };
+ // }
+
+ // class P extends Component {
+ // static components = { C2 };
+ // static template = xml``;
+ // state = {
+ // a: "first",
+ // };
+ // }
+ // const parent = new P();
+ // await parent.mount(fixture);
+ // expect(fixture.textContent).toBe("first");
+ // parent.unmount();
+ // parent.state.a = "";
+ // parent.mount(fixture);
+ // parent.state.a = "fixed";
+ // await parent.render();
+ // expect(fixture.textContent).toBe("fixed");
+ });
+
+ test("destroying a sub-component cleans itself from parent's vnode, part 4", async () => {
+ // class C1 extends Component {
+ // static template = xml``;
+ // }
+
+ // class C2 extends Component {
+ // static template = xml` `;
+ // static components = { C1 };
+ // }
+ // class P extends Component {
+ // static components = { C2 };
+ // static template = xml``;
+ // state = {
+ // a: "first",
+ // };
+ // }
+ // const parent = new P();
+ // await parent.mount(fixture);
+ // expect(fixture.textContent).toBe("firstsome text");
+ // parent.unmount();
+ // parent.state.a = "";
+ // parent.mount(fixture);
+ // parent.state.a = "fixed";
+ // await parent.render();
+ // expect(fixture.textContent).toBe("fixedsome text");
+ });
+})
\ No newline at end of file
diff --git a/tests/components/concurrency.test.ts b/tests/components/concurrency.test.ts
index c3e79ec0..383bb31c 100644
--- a/tests/components/concurrency.test.ts
+++ b/tests/components/concurrency.test.ts
@@ -2102,61 +2102,67 @@ test("concurrent renderings scenario 16", async () => {
// expect(fixture.innerHTML).toBe("4 ");
// });
-// test("calling render in destroy", async () => {
-// let a: any = null;
-// let c: any = null;
+// TODO: unskip when t-key is reimplemented properly
+test.skip("calling render in destroy", async () => {
+ const steps: any[] = [];
-// class C extends Component {
-// static template = xml`
-//
-//
-//
`;
-// }
+ let a: any = null;
+ let c: any = null;
-// let flag = false;
-// class B extends Component {
-// static template = xml` `;
-// static components = { C };
+ class C extends Component {
+ static template = xml`
+
+
+
`;
+ }
-// setup() {
-// c = this;
-// }
+ let flag = false;
+ class B extends Component {
+ static template = xml` `;
+ static components = { C };
-// mounted() {
-// if (flag) {
-// this.render();
-// } else {
-// flag = true;
-// }
-// }
-// willUnmount() {
-// c.render();
-// }
-// }
+ setup() {
+ c = this;
+ onMounted(() => {
+ steps.push("B:mounted");
+ if (flag) {
+ this.render();
+ } else {
+ flag = true;
+ }
+ });
-// class A extends Component {
-// static template = xml` `;
-// static components = { B };
-// state = "a";
-// key = 1;
+ onWillUnmount(() => {
+ steps.push("B:willUnmount");
+ c.render();
+ });
+ }
+ }
-// setup() {
-// a = this;
-// }
-// }
+ class A extends Component {
+ static template = xml` `;
+ static components = { B };
+ state = "a";
+ key = 1;
-// const parent = new A();
-// await parent.mount(fixture);
-// expect(fixture.innerHTML).toBe("a
");
+ setup() {
+ a = this;
+ }
+ }
-// a.state = "A";
-// a.key = 2;
-// await a.render();
-// // this nextTick is critical, otherwise jest may silently swallow errors
-// await nextTick();
+ const app = new App(A);
+ await app.mount(fixture);
+ expect(fixture.innerHTML).toBe("a
");
-// expect(fixture.innerHTML).toBe("A
");
-// });
+ a.state = "A";
+ a.key = 2;
+ await a.render();
+ // this nextTick is critical, otherwise jest may silently swallow errors
+ await nextTick();
+
+ expect(steps).toBe(["B:mounted", "B:willUnmount", "B:mounted"]);
+ expect(fixture.innerHTML).toBe("A
");
+});
test("change state and call manually render: no unnecessary rendering", async () => {
const steps: string[] = [];
diff --git a/tests/components/error_handling.test.ts b/tests/components/error_handling.test.ts
index 327af00a..67ef11ef 100644
--- a/tests/components/error_handling.test.ts
+++ b/tests/components/error_handling.test.ts
@@ -1,7 +1,15 @@
import { Component, mount } from "../../src";
import { status } from "../../src/component/status";
import { xml } from "../../src/tags";
-import { makeTestFixture, snapshotEverything } from "../helpers";
+import { makeTestFixture, nextTick, snapshotEverything } from "../helpers";
+import {
+ onMounted,
+ onPatched,
+ onWillPatch,
+ onWillStart,
+ onWillUnmount,
+ useState,
+} from "../../src/index";
let fixture: HTMLElement;
@@ -35,8 +43,7 @@ describe("basics", () => {
expect(fixture.innerHTML).toBe("");
expect(status(parent)).toBe("destroyed");
expect(error).toBeDefined();
- const regexp =
- /Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/g;
+ const regexp = /Cannot read properties of undefined \(reading 'this'\)|Cannot read property 'this' of undefined/g;
expect(error.message).toMatch(regexp);
});
@@ -60,4 +67,545 @@ describe("basics", () => {
expect(console.error).toBeCalledTimes(0);
console.error = consoleError;
});
+
+ test.skip("simple catchError", async () => {
+ class Boom extends Component {
+ static template = xml`
`;
+ }
+
+ class Parent extends Component {
+ static template = xml`
+
+ Error
+
+
+
+
`;
+ static components = { Boom };
+
+ error = false;
+
+ // catchError(error) {
+ // this.error = error;
+ // this.render();
+ // }
+ }
+
+ await mount(Parent, fixture);
+ expect(fixture.innerHTML).toBe("Error
");
+ });
+});
+
+describe.skip("errors and promises", () => {
+ test("a rendering error will reject the mount promise", async () => {
+ const consoleError = console.error;
+ console.error = jest.fn(() => {});
+ // we do not catch error in willPatch anymore
+ class App extends Component {
+ static template = xml`
`;
+ }
+
+ let error;
+ try {
+ await mount(App, fixture);
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
+ expect(error.message).toMatch(regexp);
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ });
+
+ test("an error in mounted call will reject the mount promise", async () => {
+ const consoleError = console.error;
+ console.error = jest.fn(() => {});
+
+ class App extends Component {
+ static template = xml`abc
`;
+ setup() {
+ onMounted(() => {
+ throw new Error("boom");
+ });
+ }
+ }
+
+ let error;
+ try {
+ await mount(App, fixture);
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ expect(error.message).toBe("boom");
+ expect(fixture.innerHTML).toBe("");
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ });
+
+ test("an error in willPatch call will reject the render promise", async () => {
+ const consoleError = console.error;
+ console.error = jest.fn(() => {});
+
+ class App extends Component {
+ static template = xml`
`;
+ val = 3;
+ setup() {
+ onWillPatch(() => {
+ throw new Error("boom");
+ });
+ }
+ }
+
+ const app = await mount(App, fixture);
+ app.val = 4;
+ let error;
+ try {
+ await app.render();
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ expect(error.message).toBe("boom");
+ expect(fixture.innerHTML).toBe("");
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ });
+
+ test("an error in patched call will reject the render promise", async () => {
+ const consoleError = console.error;
+ console.error = jest.fn(() => {});
+
+ class App extends Component {
+ static template = xml`
`;
+ val = 3;
+ setup() {
+ onPatched(() => {
+ throw new Error("boom");
+ });
+ }
+ }
+
+ const app = await mount(App, fixture);
+ app.val = 4;
+ let error;
+ try {
+ await app.render();
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ expect(error.message).toBe("boom");
+ expect(fixture.innerHTML).toBe("");
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ });
+
+ test("a rendering error in a sub component will reject the mount promise", async () => {
+ const consoleError = console.error;
+ console.error = jest.fn(() => {});
+ // we do not catch error in willPatch anymore
+ class Child extends Component {
+ static template = xml`
`;
+ }
+ class App extends Component {
+ static template = xml`
`;
+ static components = { Child };
+ }
+
+ let error;
+ try {
+ await mount(App, fixture);
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
+ expect(error.message).toMatch(regexp);
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ });
+
+ test("a rendering error will reject the render promise", async () => {
+ const consoleError = console.error;
+ console.error = jest.fn(() => {});
+ // we do not catch error in willPatch anymore
+ class App extends Component {
+ static template = xml`
`;
+ flag = false;
+ }
+
+ const app = await mount(App, fixture);
+ expect(fixture.innerHTML).toBe("
");
+ app.flag = true;
+ let error;
+ try {
+ await app.render();
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
+ expect(error.message).toMatch(regexp);
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ });
+
+ test("a rendering error will reject the render promise (with sub components)", async () => {
+ class Child extends Component {
+ static template = xml` `;
+ }
+ class Parent extends Component {
+ static template = xml`
`;
+ static components = { Child };
+ }
+
+ let error;
+ try {
+ await mount(Parent, fixture);
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ const regexp = /Cannot read properties of undefined \(reading 'y'\)|Cannot read property 'y' of undefined/g;
+ expect(error.message).toMatch(regexp);
+ });
+
+ // LPE: relevant: dead code....?
+ test("errors in mounted and in willUnmount", async () => {
+ expect.assertions(1);
+ class Example extends Component {
+ static template = xml`
`;
+ val: any;
+ setup() {
+ onMounted(() => {
+ throw new Error("Error in mounted");
+ this.val = { foo: "bar" };
+ });
+
+ onWillUnmount(() => {
+ console.log(this.val.foo);
+ });
+ }
+ }
+
+ try {
+ await mount(Example, fixture);
+ } catch (e) {
+ expect(e.message).toBe("Error in mounted");
+ }
+ });
+});
+
+describe.skip("can catch errors", () => {
+ test("can catch an error in a component render function", async () => {
+ const consoleError = console.error;
+ console.error = jest.fn();
+ const handler = jest.fn();
+ //env.qweb.on("error", null, handler);
+ class ErrorComponent extends Component {
+ static template = xml`hey
`;
+ }
+ class ErrorBoundary extends Component {
+ static template = xml`
+
+ Error handled
+
+
`;
+ state = useState({ error: false });
+
+ catchError() {
+ this.state.error = true;
+ }
+ }
+ class App extends Component {
+ static template = xml`
+
+
+
`;
+ state = useState({ flag: false });
+ static components = { ErrorBoundary, ErrorComponent };
+ }
+ const app = await mount(App, fixture);
+ expect(fixture.innerHTML).toBe("");
+ app.state.flag = true;
+ await nextTick();
+ expect(fixture.innerHTML).toBe("");
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ expect(handler).toBeCalledTimes(1);
+ });
+
+ test("can catch an error in the initial call of a component render function (parent mounted)", async () => {
+ const handler = jest.fn();
+ //env.qweb.on("error", null, handler);
+ const consoleError = console.error;
+ console.error = jest.fn();
+ class ErrorComponent extends Component {
+ static template = xml`hey
`;
+ }
+ class ErrorBoundary extends Component {
+ static template = xml`
+
+ Error handled
+
+
`;
+ state = useState({ error: false });
+
+ catchError() {
+ this.state.error = true;
+ }
+ }
+ class App extends Component {
+ static template = xml`
+
+
+
`;
+ static components = { ErrorBoundary, ErrorComponent };
+ }
+ await mount(App, fixture);
+ expect(fixture.innerHTML).toBe("");
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ expect(handler).toBeCalledTimes(1);
+ });
+
+ test("can catch an error in the initial call of a component render function (parent updated)", async () => {
+ const handler = jest.fn();
+ //env.qweb.on("error", null, handler);
+ const consoleError = console.error;
+ console.error = jest.fn();
+ class ErrorComponent extends Component {
+ static template = xml`hey
`;
+ }
+ class ErrorBoundary extends Component {
+ static template = xml`
+
+ Error handled
+
+
`;
+ state = useState({ error: false });
+
+ catchError() {
+ this.state.error = true;
+ }
+ }
+ class App extends Component {
+ static template = xml`
+
+
+
`;
+ state = useState({ flag: false });
+ static components = { ErrorBoundary, ErrorComponent };
+ }
+ const app = await mount(App, fixture);
+ app.state.flag = true;
+ await nextTick();
+ expect(fixture.innerHTML).toBe("");
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ expect(handler).toBeCalledTimes(1);
+ });
+
+ test("can catch an error in the constructor call of a component render function", async () => {
+ const handler = jest.fn();
+ //env.qweb.on("error", null, handler);
+ const consoleError = console.error;
+ console.error = jest.fn();
+
+ class ErrorComponent extends Component {
+ static template = xml`Some text
`;
+ setup() {
+ throw new Error("NOOOOO");
+ }
+ }
+ class ErrorBoundary extends Component {
+ static template = xml`
+ Error handled
+
+
`;
+ state = useState({ error: false });
+
+ catchError() {
+ this.state.error = true;
+ }
+ }
+ class App extends Component {
+ static template = xml`
+
+
`;
+ static components = { ErrorBoundary, ErrorComponent };
+ }
+ await mount(App, fixture);
+ expect(fixture.innerHTML).toBe("");
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ expect(handler).toBeCalledTimes(1);
+ });
+
+ test("can catch an error in the willStart call", async () => {
+ const consoleError = console.error;
+ console.error = jest.fn();
+ class ErrorComponent extends Component {
+ static template = xml`Some text
`;
+ setup() {
+ onWillStart(async () => {
+ // we wait a little bit to be in a different stack frame
+ await nextTick();
+ throw new Error("NOOOOO");
+ });
+ }
+ }
+ class ErrorBoundary extends Component {
+ static template = xml`
+
+ Error handled
+
+
`;
+ state = useState({ error: false });
+
+ catchError() {
+ this.state.error = true;
+ }
+ }
+ class App extends Component {
+ static template = xml`
`;
+ static components = { ErrorBoundary, ErrorComponent };
+ }
+ await mount(App, fixture);
+ expect(fixture.innerHTML).toBe("");
+
+ expect(console.error).toBeCalledTimes(0);
+ console.error = consoleError;
+ });
+
+ test.skip("can catch an error in the mounted call", async () => {
+ // we do not catch error in mounted anymore
+ console.error = jest.fn();
+ // env.qweb.addTemplates(`
+ //
+ //
+ // Error handled
+ //
+ //
+ // Some text
+ //
+ //
+ //
+ // `);
+ class ErrorComponent extends Component {
+ mounted() {
+ throw new Error("NOOOOO");
+ }
+ }
+ class ErrorBoundary extends Component {
+ state = useState({ error: false });
+
+ catchError() {
+ this.state.error = true;
+ }
+ }
+ class App extends Component {
+ static components = { ErrorBoundary, ErrorComponent };
+ }
+ await mount(App, fixture);
+ await nextTick();
+ await nextTick();
+ await nextTick();
+ expect(fixture.innerHTML).toBe("");
+ });
+
+ test.skip("can catch an error in the willPatch call", async () => {
+ // we do not catch error in willPatch anymore
+ const consoleError = console.error;
+ console.error = jest.fn();
+ class ErrorComponent extends Component {
+ static template = xml`
`;
+ setup() {
+ onWillPatch(() => {
+ throw new Error("NOOOOO");
+ });
+ }
+ }
+ class ErrorBoundary extends Component {
+ static template = xml`
+
+ Error handled
+
+
`;
+ state = useState({ error: false });
+
+ catchError() {
+ this.state.error = true;
+ }
+ }
+ class App extends Component {
+ static template = xml`
+
+
+
+
`;
+ state = useState({ message: "abc" });
+ static components = { ErrorBoundary, ErrorComponent };
+ }
+ const app = await mount(App, fixture);
+ expect(fixture.innerHTML).toBe("");
+ app.state.message = "def";
+ await nextTick();
+ await nextTick();
+ await nextTick();
+ expect(fixture.innerHTML).toBe("");
+ expect(console.error).toHaveBeenCalledTimes(1);
+ console.error = consoleError;
+ });
+
+ test("catchError in catchError", async () => {
+ class Boom extends Component {
+ static template = xml`
`;
+ }
+
+ class Child extends Component {
+ static template = xml`
+
+
+
`;
+ static components = { Boom };
+
+ // catchError(error) {
+ // throw error;
+ // }
+ }
+
+ class Parent extends Component {
+ static template = xml`
+
+ Error
+
+
+
+
`;
+ static components = { Child };
+
+ error = false;
+
+ // catchError(error) {
+ // this.error = error;
+ // this.render();
+ // }
+ }
+
+ await mount(Parent, fixture);
+ expect(fixture.innerHTML).toBe("Error
");
+ });
});
diff --git a/tests/components/event_handling.test.ts b/tests/components/event_handling.test.ts
index 49153ece..9b777bdd 100644
--- a/tests/components/event_handling.test.ts
+++ b/tests/components/event_handling.test.ts
@@ -72,4 +72,234 @@ describe("event handling", () => {
await nextTick();
expect(fixture.innerHTML).toBe(`test
`);
});
+
+ test.skip("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.skip("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.skip("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.skip("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]);*/
+ });
+
+ test.skip("t-on with prevent modifier in t-foreach", async () => {
+ /*expect.assertions(5);
+ qweb.addTemplate(
+ "test",
+ ``
+ );
+ const steps: string[] = [];
+ const owner = {
+ projects: [
+ { id: 1, name: "Project 1" },
+ { id: 2, name: "Project 2" },
+ ],
+
+ onEdit(projectId, ev) {
+ expect(ev.defaultPrevented).toBe(true);
+ steps.push(projectId);
+ },
+ };
+
+ const node = renderToDOM(qweb, "test", owner, { handlers: [] });
+ expect(node.outerHTML).toBe(
+ ``
+ );
+
+ const links = node.querySelectorAll("a")!;
+ links[0].click();
+ links[1].click();
+
+ expect(steps).toEqual([1, 2]);*/
+ });
+
+ test.skip("t-on with empty handler (only modifiers)", () => {
+ /*expect.assertions(2);
+ qweb.addTemplate(
+ "test",
+ `
+ Button
+
`
+ );
+ const node = renderToDOM(qweb, "test", {}, { handlers: [] });
+
+ node.addEventListener("click", (e) => {
+ expect(e.defaultPrevented).toBe(true);
+ });
+
+ const button = (node).getElementsByTagName("button")[0];
+ button.click();*/
+ });
+
+ test.skip("t-on combined with t-esc", async () => {
+ /*expect.assertions(3);
+ qweb.addTemplate("test", `
`);
+ const steps: string[] = [];
+ const owner = {
+ text: "Click here",
+ onClick() {
+ steps.push("onClick");
+ },
+ };
+
+ const node = renderToDOM(qweb, "test", owner, { handlers: [] });
+ expect(node.outerHTML).toBe(`Click here
`);
+
+ node.querySelector("button")!.click();
+
+ expect(steps).toEqual(["onClick"]);*/
+ });
+
+ test.skip("t-on combined with t-raw", async () => {
+ /*expect.assertions(3);
+ qweb.addTemplate("test", `
`);
+ const steps: string[] = [];
+ const owner = {
+ html: "Click here ",
+ onClick() {
+ steps.push("onClick");
+ },
+ };
+
+ const node = renderToDOM(qweb, "test", owner, { handlers: [] });
+ expect(node.outerHTML).toBe(`Click here
`);
+
+ node.querySelector("button")!.click();
+
+ expect(steps).toEqual(["onClick"]);*/
+ });
+
+ test.skip("t-on with .capture modifier", () => {
+ /*expect.assertions(2);
+ qweb.addTemplate(
+ "test",
+ `
+ Button
+
`
+ );
+
+ const steps: string[] = [];
+ const owner = {
+ onCapture() {
+ steps.push("captured");
+ },
+ doSomething() {
+ steps.push("normal");
+ },
+ };
+ const node = renderToDOM(qweb, "test", owner, { handlers: [] });
+
+ const button = (node).getElementsByTagName("button")[0];
+ button.click();
+ expect(steps).toEqual(["captured", "normal"]);*/
+ });
});
diff --git a/tests/components/props_validation.test.ts b/tests/components/props_validation.test.ts
new file mode 100644
index 00000000..849b0640
--- /dev/null
+++ b/tests/components/props_validation.test.ts
@@ -0,0 +1,808 @@
+// import { Component, Env } from "../../src/component/component";
+// import { makeTestFixture, makeTestEnv, nextTick } from "../helpers";
+// import { useState } from "../../src/hooks";
+// import { QWeb } from "../../src/qweb";
+// import { xml } from "../../src/tags";
+
+// //------------------------------------------------------------------------------
+// // Setup and helpers
+// //------------------------------------------------------------------------------
+
+// let fixture: HTMLElement;
+// let env: Env;
+// let dev: boolean = false;
+
+// beforeEach(() => {
+// fixture = makeTestFixture();
+// env = makeTestEnv();
+// Component.env = env;
+// dev = QWeb.dev;
+// QWeb.dev = true;
+// });
+
+// afterEach(() => {
+// fixture.remove();
+// QWeb.dev = dev;
+// });
+
+// class Widget extends Component {}
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+describe.skip("props validation", () => {
+ test("validation is only done in dev mode", async () => {
+ // class TestWidget extends Widget {
+ // static props = ["message"];
+ // static template = xml`hey
`;
+ // }
+ // class Parent extends Widget {
+ // static components = { TestWidget };
+ // static template = xml`
`;
+ // }
+ // let error;
+ // QWeb.dev = true;
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
+ // error = undefined;
+ // QWeb.dev = false;
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ });
+
+ test("props: list of strings", async () => {
+ // class TestWidget extends Widget {
+ // static props = ["message"];
+ // static template = xml`hey
`;
+ // }
+ // class Parent extends Widget {
+ // static components = { TestWidget };
+ // static template = xml`
`;
+ // }
+ // let error;
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe(`Missing props 'message' (component 'TestWidget')`);
+ });
+
+ test("validate simple types", async () => {
+ // const Tests = [
+ // { type: Number, ok: 1, ko: "1" },
+ // { type: Boolean, ok: true, ko: "1" },
+ // { type: String, ok: "1", ko: 1 },
+ // { type: Object, ok: {}, ko: "1" },
+ // { type: Date, ok: new Date(), ko: "1" },
+ // { type: Function, ok: () => {}, ko: "1" },
+ // ];
+ // let props;
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // get p() {
+ // return props.p;
+ // }
+ // }
+ // for (let test of Tests) {
+ // let TestWidget = class extends Widget {
+ // static template = xml`hey
`;
+ // static props = { p: test.type };
+ // };
+ // Parent.components = { TestWidget };
+ // let error;
+ // props = {};
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe(`Missing props 'p' (component '_a')`);
+ // error = undefined;
+ // props = { p: test.ok };
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // props = { p: test.ko };
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
+ // }
+ });
+
+ test("validate simple types, alternate form", async () => {
+ // const Tests = [
+ // { type: Number, ok: 1, ko: "1" },
+ // { type: Boolean, ok: true, ko: "1" },
+ // { type: String, ok: "1", ko: 1 },
+ // { type: Object, ok: {}, ko: "1" },
+ // { type: Date, ok: new Date(), ko: "1" },
+ // { type: Function, ok: () => {}, ko: "1" },
+ // ];
+ // let props;
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // get p() {
+ // return props.p;
+ // }
+ // }
+ // for (let test of Tests) {
+ // let TestWidget = class extends Component {
+ // static props = { p: { type: test.type } };
+ // static template = xml`hey
`;
+ // };
+ // Parent.components = { TestWidget };
+ // let error;
+ // props = {};
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe(`Missing props 'p' (component '_a')`);
+ // error = undefined;
+ // props = { p: test.ok };
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // props = { p: test.ko };
+ // try {
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'p' in component '_a'");
+ // }
+ });
+
+ test("can validate a prop with multiple types", async () => {
+ // class TestWidget extends Component {
+ // static template = xml`hey
`;
+ // static props = { p: [String, Boolean] };
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // get p() {
+ // return props.p;
+ // }
+ // }
+ // let error;
+ // let props;
+ // try {
+ // props = { p: "string" };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: true };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: 1 };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
+ });
+
+ test("can validate an optional props", async () => {
+ // class TestWidget extends Component {
+ // static template = xml`hey
`;
+ // static props = { p: { type: String, optional: true } };
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // get p() {
+ // return props.p;
+ // }
+ // }
+ // let error;
+ // let props;
+ // try {
+ // props = { p: "key" };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = {};
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: 1 };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
+ });
+
+ test("can validate an array with given primitive type", async () => {
+ // class TestWidget extends Component {
+ // static template = xml`hey
`;
+ // static props = { p: { type: Array, element: String } };
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // get p() {
+ // return props.p;
+ // }
+ // }
+ // let error;
+ // let props;
+ // try {
+ // props = { p: [] };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: ["string"] };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: [1] };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // error = undefined;
+ // try {
+ // props = { p: ["string", 1] };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ });
+
+ test("can validate an array with multiple sub element types", async () => {
+ // class TestWidget extends Component {
+ // static template = xml`hey
`;
+ // static props = { p: { type: Array, element: [String, Boolean] } };
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // get p() {
+ // return props.p;
+ // }
+ // }
+ // let error;
+ // let props;
+ // try {
+ // props = { p: [] };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: ["string"] };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: [false, true, "string"] };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: [true, 1] };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
+ });
+
+ test("can validate an object with simple shape", async () => {
+ // class TestWidget extends Component {
+ // static template = xml`hey
`;
+ // static props = {
+ // p: { type: Object, shape: { id: Number, url: String } },
+ // };
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // get p() {
+ // return props.p;
+ // }
+ // }
+ // let error;
+ // let props;
+ // try {
+ // props = { p: { id: 1, url: "url" } };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: { id: 1, url: "url", extra: true } };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid prop 'p' in component TestWidget (unknown prop 'extra')");
+ // try {
+ // props = { p: { id: "1", url: "url" } };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
+ // error = undefined;
+ // try {
+ // props = { p: { id: 1 } };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
+ });
+
+ test("can validate recursively complicated prop def", async () => {
+ // class TestWidget extends Component {
+ // static template = xml`hey
`;
+ // static props = {
+ // p: {
+ // type: Object,
+ // shape: {
+ // id: Number,
+ // url: [Boolean, { type: Array, element: Number }],
+ // },
+ // },
+ // };
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // get p() {
+ // return props.p;
+ // }
+ // }
+ // let error;
+ // let props;
+ // try {
+ // props = { p: { id: 1, url: true } };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: { id: 1, url: [12] } };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // props = { p: { id: 1, url: [12, true] } };
+ // const p = new Parent();
+ // await p.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'p' in component 'TestWidget'");
+ });
+
+ test("can validate optional attributes in nested sub props", () => {
+ // class TestComponent extends Component {
+ // static props = {
+ // myprop: {
+ // type: Array,
+ // element: {
+ // type: Object,
+ // shape: {
+ // num: { type: Number, optional: true },
+ // },
+ // },
+ // },
+ // };
+ // }
+ // let error;
+ // try {
+ // QWeb.utils.validateProps(TestComponent, { myprop: [{}] });
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // QWeb.utils.validateProps(TestComponent, { myprop: [{ a: 1 }] });
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe(
+ // "Invalid prop 'myprop' in component TestComponent (unknown prop 'a')"
+ // );
+ });
+
+ test("can validate with a custom validator", () => {
+ // class TestComponent extends Component {
+ // static props = {
+ // size: {
+ // validate: (e) => ["small", "medium", "large"].includes(e),
+ // },
+ // };
+ // }
+ // let error;
+ // try {
+ // QWeb.utils.validateProps(TestComponent, { size: "small" });
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // try {
+ // QWeb.utils.validateProps(TestComponent, { size: "abcdef" });
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'size' in component 'TestComponent'");
+ });
+
+ test("can validate with a custom validator, and a type", () => {
+ // const validator = jest.fn((n) => 0 <= n && n <= 10);
+ // class TestComponent extends Component {
+ // static props = {
+ // n: {
+ // type: Number,
+ // validate: validator,
+ // },
+ // };
+ // }
+ // let error;
+ // try {
+ // QWeb.utils.validateProps(TestComponent, { n: 3 });
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeUndefined();
+ // expect(validator).toBeCalledTimes(1);
+ // try {
+ // QWeb.utils.validateProps(TestComponent, { n: "str" });
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
+ // expect(validator).toBeCalledTimes(1);
+ // error = null;
+ // try {
+ // QWeb.utils.validateProps(TestComponent, { n: 100 });
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Invalid Prop 'n' in component 'TestComponent'");
+ // expect(validator).toBeCalledTimes(2);
+ });
+
+ test("props are validated in dev mode (code snapshot)", async () => {
+ // env.qweb.addTemplates(`
+ //
+ //
+ //
+ //
+ //
+ // `);
+ // class Child extends Widget {
+ // static props = ["message"];
+ // }
+ // class App extends Widget {
+ // static components = { Child };
+ // }
+ // const app = new App();
+ // await app.mount(fixture);
+ // expect(fixture.innerHTML).toBe("");
+ // // need to make sure there are 2 call to update props. one at component
+ // // creation, and one at update time.
+ // expect(env.qweb.templates.App.fn.toString()).toMatchSnapshot();
+ });
+
+ test("props: list of strings with optional props", async () => {
+ // class TestWidget extends Widget {
+ // static props = ["message", "someProp?"];
+ // }
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { someProp: 1 });
+ // }).toThrow();
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { message: 1 });
+ // }).not.toThrow();
+ });
+
+ test("props: can be defined with a boolean", async () => {
+ // class TestWidget extends Widget {
+ // static props = { message: true };
+ // }
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, {});
+ // }).toThrow();
+ });
+
+ test("props with type array, and no element", async () => {
+ // class TestWidget extends Widget {
+ // static props = { myprop: { type: Array } };
+ // }
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { myprop: [1] });
+ // }).not.toThrow();
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { myprop: 1 });
+ // }).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`);
+ });
+
+ test("props with type object, and no shape", async () => {
+ // class TestWidget extends Widget {
+ // static props = { myprop: { type: Object } };
+ // }
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { myprop: { a: 3 } });
+ // }).not.toThrow();
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { myprop: false });
+ // }).toThrow(`Invalid Prop 'myprop' in component 'TestWidget'`);
+ });
+
+ test("props: extra props cause an error", async () => {
+ // class TestWidget extends Widget {
+ // static props = ["message"];
+ // }
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
+ // }).toThrow();
+ });
+
+ test("props: extra props cause an error, part 2", async () => {
+ // class TestWidget extends Widget {
+ // static props = { message: true };
+ // }
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { message: 1, flag: true });
+ // }).toThrow();
+ });
+
+ test("props: optional prop do not cause an error", async () => {
+ // class TestWidget extends Widget {
+ // static props = ["message?"];
+ // }
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { message: 1 });
+ // }).not.toThrow();
+ });
+
+ test("optional prop do not cause an error if value is undefined", async () => {
+ // class TestWidget extends Widget {
+ // static props = { message: { type: String, optional: true } };
+ // }
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { message: undefined });
+ // }).not.toThrow();
+ // expect(() => {
+ // QWeb.utils.validateProps(TestWidget, { message: null });
+ // }).toThrow();
+ });
+
+ test("missing required boolean prop causes an error", async () => {
+ // class TestWidget extends Widget {
+ // static props = ["p"];
+ // static template = xml`hey `;
+ // }
+ // class App extends Widget {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // }
+ // const w = new App(undefined, {});
+ // let error;
+ // try {
+ // await w.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
+ });
+
+ test("props are validated whenever component is updated", async () => {
+ // let error;
+ // class TestWidget extends Component {
+ // static props = { p: { type: Number } };
+ // static template = xml`
`;
+ // async __updateProps() {
+ // try {
+ // await Component.prototype.__updateProps.apply(this, arguments);
+ // } catch (e) {
+ // error = e;
+ // }
+ // }
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // state: any = useState({ p: 1 });
+ // }
+ // const w = new Parent();
+ // await w.mount(fixture);
+ // expect(fixture.innerHTML).toBe("");
+ // w.state.p = undefined;
+ // await nextTick();
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Missing props 'p' (component 'TestWidget')");
+ });
+
+ test("default values are applied before validating props at update", async () => {
+ // class TestWidget extends Component {
+ // static props = { p: { type: Number } };
+ // static template = xml`
`;
+ // static defaultProps = { p: 4 };
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // state: any = useState({ p: 1 });
+ // }
+ // const w = new Parent();
+ // await w.mount(fixture);
+ // expect(fixture.innerHTML).toBe("");
+ // w.state.p = undefined;
+ // await nextTick();
+ // expect(fixture.innerHTML).toBe("");
+ });
+
+ test("mix of optional and mandatory", async () => {
+ // class Child extends Component {
+ // static props = {
+ // optional: { type: String, optional: true },
+ // mandatory: Number,
+ // };
+ // static template = xml`
`;
+ // }
+ // class App extends Component {
+ // static components = { Child };
+ // static template = xml`
`;
+ // }
+ // const w = new App(undefined, {});
+ // let error;
+ // try {
+ // await w.mount(fixture);
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe("Missing props 'mandatory' (component 'Child')");
+ });
+});
+
+describe.skip("default props", () => {
+ test("can set default values", async () => {
+ // class TestWidget extends Component {
+ // static defaultProps = { p: 4 };
+ // static template = xml`
`;
+ // }
+ // class Parent extends Component {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // }
+ // const w = new Parent();
+ // await w.mount(fixture);
+ // expect(fixture.innerHTML).toBe("");
+ });
+
+ test("default values are also set whenever component is updated", async () => {
+ // class TestWidget extends Widget {
+ // static template = xml`
`;
+ // static defaultProps = { p: 4 };
+ // }
+ // class Parent extends Widget {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // state: any = useState({ p: 1 });
+ // }
+ // const w = new Parent();
+ // await w.mount(fixture);
+ // expect(fixture.innerHTML).toBe("");
+ // w.state.p = undefined;
+ // await nextTick();
+ // expect(fixture.innerHTML).toBe("");
+ });
+
+ test("can set default required boolean values", async () => {
+ // class TestWidget extends Widget {
+ // static props = ["p", "q"];
+ // static defaultProps = { p: true, q: false };
+ // static template = xml`hey hey `;
+ // }
+ // class App extends Widget {
+ // static template = xml`
`;
+ // static components = { TestWidget };
+ // }
+ // const w = new App(undefined, {});
+ // await w.mount(fixture);
+ // expect(fixture.innerHTML).toBe("heyhey
");
+ });
+});
diff --git a/tests/components/reactivity.test.ts b/tests/components/reactivity.test.ts
new file mode 100644
index 00000000..8868012d
--- /dev/null
+++ b/tests/components/reactivity.test.ts
@@ -0,0 +1,196 @@
+describe.skip("reactivity in lifecycle", () => {
+ test("state changes in willUnmount do not trigger rerender", async () => {
+ // const steps: string[] = [];
+
+ // class Child extends Component {
+ // static template = xml`
+ //
+ // `;
+ // state = useState({ n: 2 });
+ // __render(f) {
+ // steps.push("render");
+ // return super.__render(f);
+ // }
+ // willPatch() {
+ // steps.push("willPatch");
+ // }
+ // patched() {
+ // steps.push("patched");
+ // }
+
+ // willUnmount() {
+ // steps.push("willUnmount");
+ // this.state.n = 3;
+ // }
+ // }
+ // class Parent extends Component {
+ // static template = xml`
+ //
+ //
+ //
+ // `;
+ // static components = { Child };
+ // state = useState({ val: 1, flag: true });
+ // }
+
+ // const widget = await mount(Parent, { target: fixture });
+ // expect(steps).toEqual(["render"]);
+ // expect(fixture.innerHTML).toBe("12
");
+ // widget.state.flag = false;
+ // await nextTick();
+ // // we make sure here that no call to __render is done
+ // expect(steps).toEqual(["render", "willUnmount"]);
+ });
+
+ test("state changes in willUnmount will be applied on remount", async () => {
+ // class TestWidget extends Component {
+ // static template = xml`
+ //
+ // `;
+ // state = useState({ val: 1 });
+ // willUnmount() {
+ // this.state.val = 3;
+ // }
+ // }
+
+ // const widget = new TestWidget();
+ // await widget.mount(fixture);
+ // expect(fixture.innerHTML).toBe("1
");
+ // widget.unmount();
+ // expect(fixture.innerHTML).toBe("");
+ // await nextTick(); // wait for changes to be detected before remounting
+ // await widget.mount(fixture);
+ // expect(fixture.innerHTML).toBe("3
");
+ // // we want to make sure that there are no remaining tasks left at this point.
+ // expect(Component.scheduler.tasks.length).toBe(0);
+ });
+
+ test("change state just before mounting component", async () => {
+ // const steps: number[] = [];
+ // class TestWidget extends Component {
+ // static template = xml`
+ //
+ // `;
+ // state = useState({ val: 1 });
+ // __render(f) {
+ // steps.push(this.state.val);
+ // return super.__render(f);
+ // }
+ // }
+ // TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
+
+ // const widget = new TestWidget();
+ // widget.state.val = 2;
+ // await widget.mount(fixture);
+ // expect(fixture.innerHTML).toBe("2
");
+ // expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
+
+ // // unmount and re-mount, as in this case, willStart won't be called, so it's
+ // // slightly different
+ // widget.unmount();
+ // widget.state.val = 3;
+ // await widget.mount(fixture);
+ // expect(fixture.innerHTML).toBe("3
");
+ // expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(2);
+ // expect(steps).toEqual([2, 3]);
+ });
+
+ test("change state while mounting component", async () => {
+ // const steps: number[] = [];
+ // class TestWidget extends Component {
+ // static template = xml`
+ //
+ // `;
+ // state = useState({ val: 1 });
+ // __render(f) {
+ // steps.push(this.state.val);
+ // return super.__render(f);
+ // }
+ // }
+ // TestWidget.prototype.__render = jest.fn(TestWidget.prototype.__render);
+ // TestWidget.prototype.__patch = jest.fn(TestWidget.prototype.__patch);
+
+ // const widget = new TestWidget();
+ // let prom = widget.mount(fixture);
+ // widget.state.val = 2;
+ // await prom;
+ // expect(fixture.innerHTML).toBe("2
");
+ // expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(1);
+
+ // // unmount and re-mount, as in this case, willStart won't be called, so it's
+ // // slightly different
+ // widget.unmount();
+ // prom = widget.mount(fixture);
+ // widget.state.val = 3;
+ // await prom;
+ // expect(fixture.innerHTML).toBe("3
");
+ // expect(TestWidget.prototype.__render).toHaveBeenCalledTimes(3);
+ // expect(TestWidget.prototype.__patch).toHaveBeenCalledTimes(2);
+ // expect(steps).toEqual([2, 2, 3]);
+ });
+
+ test("change state and render while mounted in detached dom", async () => {
+ // class App extends Component {
+ // static template = xml`
`;
+ // state = useState({ val: 1 });
+ // }
+
+ // const detachedDiv = document.createElement("div");
+ // const app = await mount(App, { target: detachedDiv });
+
+ // expect(detachedDiv.innerHTML).toBe("1
");
+ // app.state.val = 2;
+ // await nextTick();
+ // expect(detachedDiv.innerHTML).toBe("2
");
+ });
+
+ test("destroy and change state after mounted in detached dom", async () => {
+ // class App extends Component {
+ // static template = xml`
`;
+ // state = useState({ val: 1 });
+ // }
+
+ // const detachedDiv = document.createElement("div");
+ // const app = await mount(App, { target: detachedDiv });
+
+ // expect(detachedDiv.innerHTML).toBe("1
");
+
+ // app.destroy();
+ // app.state.val = 2;
+ // await nextTick();
+ // expect(detachedDiv.innerHTML).toBe("");
+ });
+
+ test("change state while component is unmounted", async () => {
+ // let child;
+ // class Child extends Component {
+ // static template = xml` `;
+ // state = useState({
+ // val: "C1",
+ // });
+ // constructor(parent, props) {
+ // super(parent, props);
+ // child = this;
+ // }
+ // }
+
+ // class Parent extends Component {
+ // static components = { Child };
+ // static template = xml`
`;
+ // state = useState({ val: "P1" });
+ // }
+
+ // const parent = new Parent();
+ // await parent.mount(fixture);
+ // expect(fixture.innerHTML).toBe("P1C1
");
+
+ // parent.unmount();
+ // expect(fixture.innerHTML).toBe("");
+
+ // parent.state.val = "P2";
+ // child.state.val = "C2";
+
+ // await parent.mount(fixture);
+ // expect(fixture.innerHTML).toBe("P2C2
");
+ });
+})
\ No newline at end of file
diff --git a/tests/components/style_class.test.ts b/tests/components/style_class.test.ts
index 2f75798f..e44a1262 100644
--- a/tests/components/style_class.test.ts
+++ b/tests/components/style_class.test.ts
@@ -341,8 +341,7 @@ describe("style and class handling", () => {
error = e;
}
expect(error).toBeDefined();
- const regexp =
- /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
+ const regexp = /Cannot read properties of undefined \(reading 'crash'\)|Cannot read property 'crash' of undefined/g;
expect(error.message).toMatch(regexp);
expect(fixture.innerHTML).toBe("");
});
diff --git a/tests/components/style_tag.test.ts b/tests/components/style_tag.test.ts
new file mode 100644
index 00000000..436e331d
--- /dev/null
+++ b/tests/components/style_tag.test.ts
@@ -0,0 +1,159 @@
+// import { Component, Env } from "../../src/component/component";
+// import { processSheet } from "../../src/component/styles";
+// import { xml, css } from "../../src/tags";
+// import { makeTestFixture, makeTestEnv } from "../helpers";
+
+// //------------------------------------------------------------------------------
+// // Setup and helpers
+// //------------------------------------------------------------------------------
+
+// // We create before each test:
+// // - fixture: a div, appended to the DOM, intended to be the target of dom
+// // manipulations. Note that it is removed after each test.
+// // - env: an Env, necessary to create new components
+
+// let fixture: HTMLElement;
+// let env: Env;
+
+// beforeEach(() => {
+// fixture = makeTestFixture();
+// env = makeTestEnv();
+// Component.env = env;
+// document.head.innerHTML = "";
+// });
+
+// afterEach(() => {
+// fixture.remove();
+// });
+
+//------------------------------------------------------------------------------
+// Tests
+//------------------------------------------------------------------------------
+
+describe.skip("styles and component", () => {
+ test("can define an inline stylesheet", async () => {
+ // class App extends Component {
+ // static template = xml`text
`;
+ // static style = css`
+ // .app {
+ // color: red;
+ // }
+ // `;
+ // }
+ // expect(document.head.innerHTML).toBe("");
+ // const app = new App();
+ // expect(document.head.innerHTML).toBe(``);
+ // await app.mount(fixture);
+ // const style = getComputedStyle(app.el!);
+ // expect(style.color).toBe("red");
+ // expect(fixture.innerHTML).toBe('text
');
+ });
+
+ test("inherited components properly apply css", async () => {
+ // class App extends Component {
+ // static template = xml`text
`;
+ // static style = css`
+ // .app {
+ // color: red;
+ // }
+ // `;
+ // }
+ // class SubApp extends App {
+ // static style = css`
+ // .app {
+ // font-weight: bold;
+ // }
+ // `;
+ // }
+ // expect(document.head.innerHTML).toBe("");
+ // const app = new SubApp();
+ // expect(document.head.innerHTML).toBe(``);
+ // await app.mount(fixture);
+ // const style = getComputedStyle(app.el!);
+ // expect(style.color).toBe("red");
+ // expect(style.fontWeight).toBe("bold");
+ // expect(fixture.innerHTML).toBe('text
');
+ });
+
+ test("get a meaningful error message if css helper is missing", async () => {
+ // class App extends Component {
+ // static template = xml`text
`;
+ // static style = `.app {color: red;}`;
+ // }
+ // let error;
+ // try {
+ // new App();
+ // } catch (e) {
+ // error = e;
+ // }
+ // expect(error).toBeDefined();
+ // expect(error.message).toBe(
+ // "Invalid css stylesheet for component 'App'. Did you forget to use the 'css' tag helper?"
+ // );
+ // });
+ // test("inline stylesheets are processed", async () => {
+ // class App extends Component {
+ // static template = xml`text
`;
+ // static style = css`
+ // .app {
+ // color: red;
+ // .some-class {
+ // font-weight: bold;
+ // width: 40px;
+ // }
+ // display: block;
+ // }
+ // `;
+ // }
+ // new App();
+ // expect(document.head.querySelector("style")!.innerHTML).toBe(`.app {
+ // color: red;
+ // }
+ // .app .some-class {
+ // font-weight: bold;
+ // width: 40px;
+ // }
+ // .app {
+ // display: block;
+ // }`);
+ });
+
+ test("properly handle rules with commas", async () => {
+ // const sheet = processSheet(`.parent-a, .parent-b {
+ // .child-a, .child-b {
+ // color: red;
+ // }
+ // }`);
+ // expect(sheet)
+ // .toBe(`.parent-a .child-a, .parent-a .child-b, .parent-b .child-a, .parent-b .child-b {
+ // color: red;
+ // }`);
+ });
+
+ test("handle & selector", async () => {
+ // let sheet = processSheet(`.btn {
+ // &.danger {
+ // color: red;
+ // }
+ // }`);
+ // expect(sheet).toBe(`.btn.danger {
+ // color: red;
+ // }`);
+ // sheet = processSheet(`.some-class {
+ // &.btn {
+ // .other-class ~ & {
+ // color: red;
+ // }
+ // }
+ // }`);
+ // expect(sheet).toBe(`.other-class ~ .some-class.btn {
+ // color: red;
+ // }`);
+ });
+});
diff --git a/tests/components/t_model.test.ts b/tests/components/t_model.test.ts
new file mode 100644
index 00000000..88d2dbf8
--- /dev/null
+++ b/tests/components/t_model.test.ts
@@ -0,0 +1,365 @@
+import { Component } from "../../src/component/component";
+import { mount, useState, xml } from "../../src/index";
+import { editInput, makeTestFixture, nextTick, snapshotEverything } from "../helpers";
+
+snapshotEverything();
+
+let fixture: HTMLElement;
+
+beforeEach(() => {
+ fixture = makeTestFixture();
+});
+
+describe.skip("t-model directive", () => {
+ test("basic use, on an input", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
`;
+ state = useState({ text: "" });
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ expect(fixture.innerHTML).toBe("
");
+
+ const input = fixture.querySelector("input")!;
+ await editInput(input, "test");
+ expect(comp.state.text).toBe("test");
+ expect(fixture.innerHTML).toBe("test
");
+ });
+
+ test("basic use, on an input with bracket expression", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
`;
+ state = useState({ text: "" });
+ }
+
+ const comp = await mount(SomeComponent, fixture);
+ expect(fixture.innerHTML).toBe("
");
+
+ const input = fixture.querySelector("input")!;
+ await editInput(input, "test");
+ expect(comp.state.text).toBe("test");
+ expect(fixture.innerHTML).toBe("test
");
+ });
+
+ test("throws if invalid expression", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
`;
+ state = useState({ text: "" });
+ }
+ let error;
+ try {
+ await mount(SomeComponent, fixture);
+ } catch (e) {
+ error = e;
+ }
+ expect(error).toBeDefined();
+ expect(error.message).toBe(`Invalid t-model expression: "state" (it should be assignable)`);
+ });
+
+ test("basic use, on another key in component", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
`;
+ some = useState({ text: "" });
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ expect(fixture.innerHTML).toBe("
");
+
+ const input = fixture.querySelector("input")!;
+ await editInput(input, "test");
+ expect(comp.some.text).toBe("test");
+ expect(fixture.innerHTML).toBe("test
");
+ });
+
+ test("on an input, type=checkbox", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+ yes
+ no
+
+
`;
+ state = useState({ flag: false });
+ }
+
+ const comp = await mount(SomeComponent, fixture);
+ expect(fixture.innerHTML).toBe('no
');
+
+ let input = fixture.querySelector("input")!;
+ input.click();
+ await nextTick();
+ expect(fixture.innerHTML).toBe('yes
');
+ expect(comp.state.flag).toBe(true);
+
+ input.click();
+ await nextTick();
+ expect(comp.state.flag).toBe(false);
+ });
+
+ test("on an textarea", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
`;
+ state = useState({ text: "" });
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ expect(fixture.innerHTML).toBe("
");
+
+ const textarea = fixture.querySelector("textarea")!;
+ await editInput(textarea, "test");
+ expect(comp.state.text).toBe("test");
+ expect(fixture.innerHTML).toBe("test
");
+ });
+
+ test("on an input type=radio", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+ Choice:
+
`;
+ state = useState({ choice: "" });
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ expect(fixture.innerHTML).toBe(
+ 'Choice:
'
+ );
+
+ const firstInput = fixture.querySelector("input")!;
+ firstInput.click();
+ await nextTick();
+ expect(comp.state.choice).toBe("One");
+ expect(fixture.innerHTML).toBe(
+ 'Choice: One
'
+ );
+
+ const secondInput = fixture.querySelectorAll("input")[1];
+ secondInput.click();
+ await nextTick();
+ expect(comp.state.choice).toBe("Two");
+ expect(fixture.innerHTML).toBe(
+ 'Choice: Two
'
+ );
+ });
+
+ test("on a select", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+ Please select one
+ Red
+ Blue
+
+ Choice:
+
`;
+ state = useState({ color: "" });
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ expect(fixture.innerHTML).toBe(
+ 'Please select one Red Blue Choice:
'
+ );
+
+ const select = fixture.querySelector("select")!;
+ select.value = "red";
+ select.dispatchEvent(new Event("change"));
+ await nextTick();
+
+ expect(comp.state.color).toBe("red");
+ expect(fixture.innerHTML).toBe(
+ 'Please select one Red Blue Choice: red
'
+ );
+ });
+
+ test("on a select, initial state", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+ Please select one
+ Red
+ Blue
+
+
+ `;
+ state = useState({ color: "red" });
+ }
+ await mount(SomeComponent, fixture);
+ const select = fixture.querySelector("select")!;
+ expect(select.value).toBe("red");
+ });
+
+ test("on a sub state key", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
+ `;
+ state = useState({ something: { text: "" } });
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ expect(fixture.innerHTML).toBe("
");
+
+ const input = fixture.querySelector("input")!;
+ await editInput(input, "test");
+ expect(comp.state.something.text).toBe("test");
+ expect(fixture.innerHTML).toBe("test
");
+ });
+
+ test(".lazy modifier", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
+ `;
+ state = useState({ text: "" });
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ expect(fixture.innerHTML).toBe("
");
+
+ const input = fixture.querySelector("input")!;
+ input.value = "test";
+ input.dispatchEvent(new Event("input"));
+ await nextTick();
+ expect(comp.state.text).toBe("");
+ expect(fixture.innerHTML).toBe("
");
+ input.dispatchEvent(new Event("change"));
+ await nextTick();
+ expect(comp.state.text).toBe("test");
+ expect(fixture.innerHTML).toBe("test
");
+ });
+
+ test(".trim modifier", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
+ `;
+ state = useState({ text: "" });
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ const input = fixture.querySelector("input")!;
+ await editInput(input, " test ");
+ expect(comp.state.text).toBe("test");
+ expect(fixture.innerHTML).toBe("test
");
+ });
+
+ test(".number modifier", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
+ `;
+ state = useState({ number: 0 });
+ }
+ const comp = await mount(SomeComponent, fixture);
+ expect(fixture.innerHTML).toBe("0
");
+
+ const input = fixture.querySelector("input")!;
+ await editInput(input, "13");
+ expect(comp.state.number).toBe(13);
+ expect(fixture.innerHTML).toBe("13
");
+
+ await editInput(input, "invalid");
+ expect(comp.state.number).toBe("invalid");
+ expect(fixture.innerHTML).toBe("invalid
");
+ });
+
+ test("in a t-foreach", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
+
+ `;
+ state = useState([
+ { f: false, id: 1 },
+ { f: false, id: 2 },
+ { f: false, id: 3 },
+ ]);
+ }
+ const comp = await mount(SomeComponent, fixture);
+
+ expect(fixture.innerHTML).toBe(
+ '
'
+ );
+
+ const input = fixture.querySelectorAll("input")[1]!;
+ input.click();
+ expect(comp.state[1].f).toBe(true);
+ expect(comp.state[0].f).toBe(false);
+ expect(comp.state[2].f).toBe(false);
+ });
+
+ test("in a t-foreach, part 2", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
+
+ `;
+ state = useState(["zuko", "iroh"]);
+ }
+ const comp = await mount(SomeComponent, fixture);
+ expect(comp.state).toEqual(["zuko", "iroh"]);
+
+ const input = fixture.querySelectorAll("input")[1]!;
+ input.value = "uncle iroh";
+ input.dispatchEvent(new Event("input"));
+ expect(comp.state).toEqual(["zuko", "uncle iroh"]);
+ });
+
+ test("two inputs in a div with a t-key", async () => {
+ class SomeComponent extends Component {
+ static template = xml`
+
+
+
+
+ `;
+ state = useState({ flag: true });
+ }
+ const comp = await mount(SomeComponent, fixture);
+ expect(fixture.innerHTML).toBe('
');
+ fixture.querySelector("input")!.value = "asdf";
+ expect(fixture.querySelector("input")!.value).toBe("asdf");
+ comp.state.flag = false;
+ await nextTick();
+ expect(fixture.innerHTML).toBe('
');
+ expect(fixture.querySelector("input")!.value).toBe("");
+ });
+});
diff --git a/tests/helpers.ts b/tests/helpers.ts
index d74d4722..2f84d669 100644
--- a/tests/helpers.ts
+++ b/tests/helpers.ts
@@ -211,3 +211,10 @@ export function children(w: Component): Component[] {
export function isDirectChildOf(child: Component, parent: Component): boolean {
return children(parent).includes(child);
}
+
+export async function editInput(input: HTMLInputElement | HTMLTextAreaElement, value: string) {
+ input.value = value;
+ input.dispatchEvent(new Event("input"));
+ input.dispatchEvent(new Event("change"));
+ return nextTick();
+}
diff --git a/tests/qweb/__snapshots__/t_if.test.ts.snap b/tests/qweb/__snapshots__/t_if.test.ts.snap
index 0a6ae288..c56b3ef9 100644
--- a/tests/qweb/__snapshots__/t_if.test.ts.snap
+++ b/tests/qweb/__snapshots__/t_if.test.ts.snap
@@ -126,6 +126,24 @@ exports[`t-if boolean value condition false else 1`] = `
}"
`;
+exports[`t-if boolean value condition missing 1`] = `
+"function anonymous(bdom, helpers
+) {
+ let { text, createBlock, list, multi, html, toggler, component } = bdom;
+ let { withDefault, getTemplate, prepareList, withKey, zero, call, callSlot, capture, shallowEqual } = helpers;
+
+ let block1 = createBlock(\` \`);
+
+ return function template(ctx, node, key = \\"\\") {
+ let b2;
+ if (ctx['condition']) {
+ b2 = text(\`fail\`);
+ }
+ return block1([], [b2]);
+ }
+}"
+`;
+
exports[`t-if can use some boolean operators in expressions 1`] = `
"function anonymous(bdom, helpers
) {
diff --git a/tests/qweb/attributes.test.ts b/tests/qweb/attributes.test.ts
index 6652654e..a10eb543 100644
--- a/tests/qweb/attributes.test.ts
+++ b/tests/qweb/attributes.test.ts
@@ -379,4 +379,47 @@ describe("special cases for some specific html attributes/properties", () => {
const input = fixture.querySelector("input")!;
expect(input.indeterminate).toBe(true);
});
+
+ test.skip("textarea with t-att-value", () => {
+ // render input with initial value
+/* qweb.addTemplate("test", ``);
+ const vnode1 = qweb.render("test", { v: "zucchini" });
+ const vnode2 = patch(document.createElement("textarea"), vnode1);
+ let elm = vnode2.elm as HTMLInputElement;
+ expect(elm.value).toBe("zucchini");
+
+ // change value manually in textarea, to simulate user textarea
+ elm.value = "tomato";
+ expect(elm.value).toBe("tomato");
+
+ // rerender with a different value, and patch actual dom, to check that
+ // textarea value was properly reset by owl
+ const vnode3 = qweb.render("test", { v: "potato" });
+ patch(vnode2, vnode3);
+ expect(elm.value).toBe("potato");*/
+ });
+
+ test.skip("select with t-att-value", () => {
+/* const template = `
+
+ Potato
+ Tomato
+ Onion
+ `;
+ qweb.addTemplate("test", template);
+ const vnode1 = qweb.render("test", { value: "tomato" });
+ const vnode2 = patch(document.createElement("select"), vnode1);
+ let elm = vnode2.elm as HTMLSelectElement;
+ expect(elm.value).toBe("tomato");
+
+ elm.value = "potato";
+ expect(elm.value).toBe("potato");
+
+ // rerender with a different value, and patch actual dom, to check that
+ // select value was properly reset by owl
+ const vnode3 = qweb.render("test", { value: "onion" });
+ patch(vnode2, vnode3);
+ expect(elm.value).toBe("onion");
+ expect(qweb.templates.test.fn.toString()).toMatchSnapshot();*/
+ });
});
diff --git a/tests/qweb/simple_templates.test.ts b/tests/qweb/simple_templates.test.ts
index 04b55618..e0ab3c25 100644
--- a/tests/qweb/simple_templates.test.ts
+++ b/tests/qweb/simple_templates.test.ts
@@ -123,3 +123,33 @@ describe("simple templates, mostly static", () => {
expect(renderToString(template)).toBe(template);
});
});
+
+describe("loading templates", () => {
+ test.skip("can initialize qweb with a string", () => {
+/* const templates = `
+
+ jupiler
+ `;
+ const qweb = new QWeb({ templates });
+ expect(renderToString(qweb, "hey")).toBe("jupiler
");*/
+ });
+
+ test.skip("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.skip("does not crash if string does not have templates", () => {
+ /*const data = "";
+ qweb.addTemplates(data);
+ expect(Object.keys(qweb.templates)).toEqual([]);*/
+ });
+});
\ No newline at end of file
diff --git a/tests/qweb/t_if.test.ts b/tests/qweb/t_if.test.ts
index 2d81a048..02a83130 100644
--- a/tests/qweb/t_if.test.ts
+++ b/tests/qweb/t_if.test.ts
@@ -16,6 +16,11 @@ describe("t-if", () => {
expect(renderToString(template, {})).toBe("
");
});
+ test("boolean value condition missing", () => {
+ const template = `fail `;
+ expect(renderToString(template)).toBe(" ");
+ });
+
test("just a t-if", () => {
const template = `ok `;
expect(renderToString(template, { condition: true })).toBe("ok");