[FIX] re-introduce some missing tests

The point is to have visibility on the development of the owl2 features.
This commit reintroduces some tests keeping them skipped in order to fulfill that purpose.

There still are some missing tests though.
This commit is contained in:
Lucas Perais (lpe)
2021-10-15 10:58:29 +02:00
committed by Aaron Bohy
parent d6668e3439
commit 52fa81c510
16 changed files with 2795 additions and 55 deletions
+3 -4
View File
@@ -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: "&&",
@@ -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(\`<div><block-text-0/></div>\`);
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
) {
+288
View File
@@ -742,3 +742,291 @@ describe("basics", () => {
expect(fixture.innerHTML).toBe("<div><div><span>asdf</span><span>asdf</span></div></div>");
});
});
describe.skip("mount targets", () => {
test("can attach a component to an existing node (if same tagname)", async () => {
// class App extends Component {
// static template = xml`<div t-att-class="state.customClass">app<p>another tag</p></div>`;
// state = useState({ customClass: "custom" });
// }
// const div = document.createElement("div");
// div.classList.add("arbitrary");
// div.innerHTML = `<p>pre-existing</p>`;
// fixture.appendChild(div);
// const app = await mount(App, { target: div, position: "self" });
// expect(fixture.innerHTML).toBe(
// `<div class="arbitrary custom"><p>pre-existing</p>app<p>another tag</p></div>`
// );
// expect(div).toBe(app.el);
// app.state.customClass = "custom2";
// await nextTick();
// expect(fixture.innerHTML).toBe(
// `<div class="arbitrary custom2"><p>pre-existing</p>app<p>another tag</p></div>`
// );
// 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`<span>app</span>`;
// }
// 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`<div>app</div>`;
// }
// const span = document.createElement("span");
// fixture.appendChild(span);
// await mount(App, { target: fixture, position: "first-child" });
// expect(fixture.innerHTML).toBe("<div>app</div><span></span>");
});
test("can mount a component (with position='last-child')", async () => {
// class App extends Component {
// static template = xml`<div>app</div>`;
// }
// const span = document.createElement("span");
// fixture.appendChild(span);
// await mount(App, { target: fixture, position: "last-child" });
// expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
test("default mount option is 'last-child'", async () => {
// class App extends Component {
// static template = xml`<div>app</div>`;
// }
// const span = document.createElement("span");
// fixture.appendChild(span);
// await mount(App, { target: fixture });
// expect(fixture.innerHTML).toBe("<span></span><div>app</div>");
});
});
describe.skip("mount special cases", () => {
test("widget can be mounted on different target", async () => {
// class MyWidget extends Component {
// static template = xml`<div>Hey</div>`;
// 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("<div><div>Hey</div></div><span></span>");
// await w.mount(span);
// expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
});
test("widget can be mounted on different target, another situation", async () => {
// const def = makeDeferred();
// const steps: string[] = [];
// class MyWidget extends Component {
// static template = xml`<div>Hey</div>`;
// 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("<div></div><span></span>");
// 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("<div></div><span></span>");
// def.resolve();
// await nextTick();
// expect(steps).toEqual(["2 resolved"]);
// expect(fixture.innerHTML).toBe("<div></div><span><div>Hey</div></span>");
});
test("component can be mounted on same target, another situation", async () => {
// const def = makeDeferred();
// const steps: string[] = [];
// class MyWidget extends Component {
// static template = xml`<div>Hey</div>`;
// 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("<div>Hey</div>");
// expect(steps).toEqual(["1 resolved", "2 resolved"]);
});
test("mounting a destroyed widget", async () => {
// class MyWidget extends Component {
// static template = xml`<div>Hey</div>`;
// }
// 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`<div><div><t t-esc="props.a"/></div></div>`;
// }
// class P extends Component {
// static components = { C1 };
// static template = xml`<div><div><C1 t-props="state" t-if="state.a"/></div></div>`;
// 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`<div><div><t t-esc="props.a"/></div></div>`;
// }
// class P extends Component {
// static components = { C1 };
// static template = xml`<div><div><C1 t-props="state" t-if="state.a"/>some text</div></div>`;
// 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`<div><div><t t-esc="props.a"/></div></div>`;
// }
// class C2 extends Component {
// static template = xml`<C1 a="props.a"/>`;
// static components = { C1 };
// }
// class P extends Component {
// static components = { C2 };
// static template = xml`<div><div><C2 t-props="state" t-if="state.a"/></div></div>`;
// 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`<div><div><t t-esc="props.a"/></div></div>`;
// }
// class C2 extends Component {
// static template = xml`<C1 a="props.a"/>`;
// static components = { C1 };
// }
// class P extends Component {
// static components = { C2 };
// static template = xml`<div><div><C2 t-props="state" t-if="state.a"/>some text</div></div>`;
// 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");
});
})
+52 -46
View File
@@ -2102,61 +2102,67 @@ test("concurrent renderings scenario 16", async () => {
// expect(fixture.innerHTML).toBe("<span>4</span>");
// });
// 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`
// <div>
// <t t-esc="props.fromA"/>
// </div>`;
// }
let a: any = null;
let c: any = null;
// let flag = false;
// class B extends Component {
// static template = xml`<C fromA="props.fromA"/>`;
// static components = { C };
class C extends Component {
static template = xml`
<div>
<t t-esc="props.fromA"/>
</div>`;
}
// setup() {
// c = this;
// }
let flag = false;
class B extends Component {
static template = xml`<C fromA="props.fromA"/>`;
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`<B t-key="key" fromA="state"/>`;
// static components = { B };
// state = "a";
// key = 1;
onWillUnmount(() => {
steps.push("B:willUnmount");
c.render();
});
}
}
// setup() {
// a = this;
// }
// }
class A extends Component {
static template = xml`<B t-key="key" fromA="state"/>`;
static components = { B };
state = "a";
key = 1;
// const parent = new A();
// await parent.mount(fixture);
// expect(fixture.innerHTML).toBe("<div>a</div>");
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("<div>a</div>");
// expect(fixture.innerHTML).toBe("<div>A</div>");
// });
a.state = "A";
a.key = 2;
await a.render();
// this nextTick is critical, otherwise jest may silently swallow errors
await nextTick();
expect(steps).toBe(["B:mounted", "B:willUnmount", "B:mounted"]);
expect(fixture.innerHTML).toBe("<div>A</div>");
});
test("change state and call manually render: no unnecessary rendering", async () => {
const steps: string[] = [];
+551 -3
View File
@@ -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`<div t-esc="a.b.c"/>`;
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Boom />
</t>
</div>`;
static components = { Boom };
error = false;
// catchError(error) {
// this.error = error;
// this.render();
// }
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>Error</div>");
});
});
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`<div><t t-esc="this.will.crash"/></div>`;
}
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`<div>abc</div>`;
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`<div><t t-esc="val"/></div>`;
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`<div><t t-esc="val"/></div>`;
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`<div><t t-esc="this.will.crash"/></div>`;
}
class App extends Component {
static template = xml`<div><Child/></div>`;
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`<div><t t-if="flag" t-esc="this.will.crash"/></div>`;
flag = false;
}
const app = await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div></div>");
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`<span></span>`;
}
class Parent extends Component {
static template = xml`<div><Child/><t t-esc="x.y"/></div>`;
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`<div/>`;
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`<div>hey<t t-esc="props.flag and state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent flag="state.flag"/></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div><div>heyfalse</div></div></div>");
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent };
}
await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>hey<t t-esc="state.this.will.crash"/></div>`;
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
<div>
<ErrorBoundary t-if="state.flag"><ErrorComponent /></ErrorBoundary>
</div>`;
state = useState({ flag: false });
static components = { ErrorBoundary, ErrorComponent };
}
const app = await mount(App, fixture);
app.state.flag = true;
await nextTick();
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div>Some text</div>`;
setup() {
throw new Error("NOOOOO");
}
}
class ErrorBoundary extends Component {
static template = xml`<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`<div">
<ErrorBoundary><ErrorComponent /></ErrorBoundary>
</div>`;
static components = { ErrorBoundary, ErrorComponent };
}
await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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`<div t-name="ErrorComponent">Some text</div>`;
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`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`<div><ErrorBoundary><ErrorComponent /></ErrorBoundary></div>`;
static components = { ErrorBoundary, ErrorComponent };
}
await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><div>Error handled</div></div>");
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(`
// <templates>
// <div t-name="ErrorBoundary">
// <t t-if="state.error">Error handled</t>
// <t t-else=""><t t-slot="default" /></t>
// </div>
// <div t-name="ErrorComponent">Some text</div>
// <div t-name="App">
// <ErrorBoundary><ErrorComponent /></ErrorBoundary>
// </div>
// </templates>`);
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("<div><div>Error handled</div></div>");
});
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`<div><t t-esc="props.message"/></div>`;
setup() {
onWillPatch(() => {
throw new Error("NOOOOO");
});
}
}
class ErrorBoundary extends Component {
static template = xml`
<div>
<t t-if="state.error">Error handled</t>
<t t-else=""><t t-slot="default" /></t>
</div>`;
state = useState({ error: false });
catchError() {
this.state.error = true;
}
}
class App extends Component {
static template = xml`
<div>
<span><t t-esc="state.message"/></span>
<ErrorBoundary><ErrorComponent message="state.message" /></ErrorBoundary>
</div>`;
state = useState({ message: "abc" });
static components = { ErrorBoundary, ErrorComponent };
}
const app = await mount(App, fixture);
expect(fixture.innerHTML).toBe("<div><span>abc</span><div><div>abc</div></div></div>");
app.state.message = "def";
await nextTick();
await nextTick();
await nextTick();
expect(fixture.innerHTML).toBe("<div><span>def</span><div>Error handled</div></div>");
expect(console.error).toHaveBeenCalledTimes(1);
console.error = consoleError;
});
test("catchError in catchError", async () => {
class Boom extends Component {
static template = xml`<div t-esc="a.b.c"/>`;
}
class Child extends Component {
static template = xml`
<div>
<Boom />
</div>`;
static components = { Boom };
// catchError(error) {
// throw error;
// }
}
class Parent extends Component {
static template = xml`
<div>
<t t-if="error">Error</t>
<t t-else="">
<Child />
</t>
</div>`;
static components = { Child };
error = false;
// catchError(error) {
// this.error = error;
// this.render();
// }
}
await mount(Parent, fixture);
expect(fixture.innerHTML).toBe("<div>Error</div>");
});
});
+230
View File
@@ -72,4 +72,234 @@ describe("event handling", () => {
await nextTick();
expect(fixture.innerHTML).toBe(`<div>test<input type="text"></div>`);
});
test.skip("t-on with prevent and/or stop modifiers", async () => {
/* expect.assertions(7);
qweb.addTemplate(
"test",
`<div>
<button t-on-click.prevent="onClickPrevented">Button 1</button>
<button t-on-click.stop="onClickStopped">Button 2</button>
<button t-on-click.prevent.stop="onClickPreventedAndStopped">Button 3</button>
</div>`
);
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 = (<HTMLElement>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",
`<div>
<button t-on-click="onClick"><span>Button</span></button>
<button t-on-click.self="onClickSelf"><span>Button</span></button>
</div>`
);
let steps: string[] = [];
let owner = {
onClick(e) {
steps.push("onClick");
},
onClickSelf(e) {
steps.push("onClickSelf");
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
const buttons = (<HTMLElement>node).getElementsByTagName("button");
const spans = (<HTMLElement>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",
`<div>
<button t-on-click.self.prevent="onClick"><span>Button</span></button>
</div>`
);
let steps: boolean[] = [];
let owner = {
onClick() {},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
const span = (<HTMLElement>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",
`<div>
<button t-on-click.prevent.self="onClick"><span>Button</span></button>
</div>`
);
let steps: boolean[] = [];
let owner = {
onClick() {},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
(<HTMLElement>node).addEventListener("click", function (e) {
steps.push(e.defaultPrevented);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
const span = (<HTMLElement>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",
`<div>
<t t-foreach="projects" t-as="project">
<a href="#" t-key="project" t-on-click.prevent="onEdit(project.id)">
Edit <t t-esc="project.name"/>
</a>
</t>
</div>`
);
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 = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
expect(node.outerHTML).toBe(
`<div><a href="#"> Edit Project 1</a><a href="#"> Edit Project 2</a></div>`
);
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",
`<div>
<button t-on-click.prevent="">Button</button>
</div>`
);
const node = renderToDOM(qweb, "test", {}, { handlers: [] });
node.addEventListener("click", (e) => {
expect(e.defaultPrevented).toBe(true);
});
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();*/
});
test.skip("t-on combined with t-esc", async () => {
/*expect.assertions(3);
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-esc="text"/></div>`);
const steps: string[] = [];
const owner = {
text: "Click here",
onClick() {
steps.push("onClick");
},
};
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
expect(node.outerHTML).toBe(`<div><button>Click here</button></div>`);
node.querySelector("button")!.click();
expect(steps).toEqual(["onClick"]);*/
});
test.skip("t-on combined with t-raw", async () => {
/*expect.assertions(3);
qweb.addTemplate("test", `<div><button t-on-click="onClick" t-raw="html"/></div>`);
const steps: string[] = [];
const owner = {
html: "Click <b>here</b>",
onClick() {
steps.push("onClick");
},
};
const node = <HTMLElement>renderToDOM(qweb, "test", owner, { handlers: [] });
expect(node.outerHTML).toBe(`<div><button>Click <b>here</b></button></div>`);
node.querySelector("button")!.click();
expect(steps).toEqual(["onClick"]);*/
});
test.skip("t-on with .capture modifier", () => {
/*expect.assertions(2);
qweb.addTemplate(
"test",
`<div t-on-click.capture="onCapture">
<button t-on-click="doSomething">Button</button>
</div>`
);
const steps: string[] = [];
const owner = {
onCapture() {
steps.push("captured");
},
doSomething() {
steps.push("normal");
},
};
const node = renderToDOM(qweb, "test", owner, { handlers: [] });
const button = (<HTMLElement>node).getElementsByTagName("button")[0];
button.click();
expect(steps).toEqual(["captured", "normal"]);*/
});
});
+808
View File
@@ -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`<div>hey</div>`;
// }
// class Parent extends Widget {
// static components = { TestWidget };
// static template = xml`<div><TestWidget /></div>`;
// }
// 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`<div>hey</div>`;
// }
// class Parent extends Widget {
// static components = { TestWidget };
// static template = xml`<div><TestWidget /></div>`;
// }
// 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`<div><TestWidget p="p"/></div>`;
// get p() {
// return props.p;
// }
// }
// for (let test of Tests) {
// let TestWidget = class extends Widget {
// static template = xml`<div>hey</div>`;
// 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`<div><TestWidget p="p"/></div>`;
// get p() {
// return props.p;
// }
// }
// for (let test of Tests) {
// let TestWidget = class extends Component {
// static props = { p: { type: test.type } };
// static template = xml`<div>hey</div>`;
// };
// 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`<div>hey</div>`;
// static props = { p: [String, Boolean] };
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget p="p"/></div>`;
// 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`<div>hey</div>`;
// static props = { p: { type: String, optional: true } };
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget p="p"/></div>`;
// 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`<div>hey</div>`;
// static props = { p: { type: Array, element: String } };
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget p="p"/></div>`;
// 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`<div>hey</div>`;
// static props = { p: { type: Array, element: [String, Boolean] } };
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget p="p"/></div>`;
// 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`<div>hey</div>`;
// static props = {
// p: { type: Object, shape: { id: Number, url: String } },
// };
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget p="p"/></div>`;
// 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`<div>hey</div>`;
// static props = {
// p: {
// type: Object,
// shape: {
// id: Number,
// url: [Boolean, { type: Array, element: Number }],
// },
// },
// };
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget p="p"/></div>`;
// 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(`
// <templates>
// <div t-name="App">
// <Child message="1"/>
// </div>
// <div t-name="Child"><t t-esc="props.message"/></div>
// </templates>`);
// 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("<div><div>1</div></div>");
// // 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`<span><t t-if="props.p">hey</t></span>`;
// }
// class App extends Widget {
// static template = xml`<div><TestWidget/></div>`;
// 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`<div><t t-esc="props.p"/></div>`;
// async __updateProps() {
// try {
// await Component.prototype.__updateProps.apply(this, arguments);
// } catch (e) {
// error = e;
// }
// }
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget p="state.p"/></div>`;
// static components = { TestWidget };
// state: any = useState({ p: 1 });
// }
// const w = new Parent();
// await w.mount(fixture);
// expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// 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`<div><t t-esc="props.p"/></div>`;
// static defaultProps = { p: 4 };
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget p="state.p"/></div>`;
// static components = { TestWidget };
// state: any = useState({ p: 1 });
// }
// const w = new Parent();
// await w.mount(fixture);
// expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// w.state.p = undefined;
// await nextTick();
// expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
test("mix of optional and mandatory", async () => {
// class Child extends Component {
// static props = {
// optional: { type: String, optional: true },
// mandatory: Number,
// };
// static template = xml` <div><t t-esc="props.mandatory"/></div>`;
// }
// class App extends Component {
// static components = { Child };
// static template = xml`<div><Child/></div>`;
// }
// 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`<div><t t-esc="props.p"/></div>`;
// }
// class Parent extends Component {
// static template = xml`<div><TestWidget /></div>`;
// static components = { TestWidget };
// }
// const w = new Parent();
// await w.mount(fixture);
// expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
test("default values are also set whenever component is updated", async () => {
// class TestWidget extends Widget {
// static template = xml`<div><t t-esc="props.p"/></div>`;
// static defaultProps = { p: 4 };
// }
// class Parent extends Widget {
// static template = xml`<div><TestWidget p="state.p"/></div>`;
// static components = { TestWidget };
// state: any = useState({ p: 1 });
// }
// const w = new Parent();
// await w.mount(fixture);
// expect(fixture.innerHTML).toBe("<div><div>1</div></div>");
// w.state.p = undefined;
// await nextTick();
// expect(fixture.innerHTML).toBe("<div><div>4</div></div>");
});
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`<span><t t-if="props.p">hey</t><t t-if="!props.q">hey</t></span>`;
// }
// class App extends Widget {
// static template = xml`<div><TestWidget/></div>`;
// static components = { TestWidget };
// }
// const w = new App(undefined, {});
// await w.mount(fixture);
// expect(fixture.innerHTML).toBe("<div><span>heyhey</span></div>");
});
});
+196
View File
@@ -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`
// <span><t t-esc="props.val"/><t t-esc="state.n"/></span>
// `;
// 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`
// <div>
// <Child t-if="state.flag" val="state.val"/>
// </div>
// `;
// static components = { Child };
// state = useState({ val: 1, flag: true });
// }
// const widget = await mount(Parent, { target: fixture });
// expect(steps).toEqual(["render"]);
// expect(fixture.innerHTML).toBe("<div><span>12</span></div>");
// 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`
// <div><t t-esc="state.val"/></div>
// `;
// state = useState({ val: 1 });
// willUnmount() {
// this.state.val = 3;
// }
// }
// const widget = new TestWidget();
// await widget.mount(fixture);
// expect(fixture.innerHTML).toBe("<div>1</div>");
// widget.unmount();
// expect(fixture.innerHTML).toBe("");
// await nextTick(); // wait for changes to be detected before remounting
// await widget.mount(fixture);
// expect(fixture.innerHTML).toBe("<div>3</div>");
// // 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`
// <div><t t-esc="state.val"/></div>
// `;
// 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("<div>2</div>");
// 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("<div>3</div>");
// 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`
// <div><t t-esc="state.val"/></div>
// `;
// 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("<div>2</div>");
// 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("<div>3</div>");
// 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`<div><t t-esc="state.val"/></div>`;
// state = useState({ val: 1 });
// }
// const detachedDiv = document.createElement("div");
// const app = await mount(App, { target: detachedDiv });
// expect(detachedDiv.innerHTML).toBe("<div>1</div>");
// app.state.val = 2;
// await nextTick();
// expect(detachedDiv.innerHTML).toBe("<div>2</div>");
});
test("destroy and change state after mounted in detached dom", async () => {
// class App extends Component {
// static template = xml`<div><t t-esc="state.val"/></div>`;
// state = useState({ val: 1 });
// }
// const detachedDiv = document.createElement("div");
// const app = await mount(App, { target: detachedDiv });
// expect(detachedDiv.innerHTML).toBe("<div>1</div>");
// 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`<span t-esc="state.val"/>`;
// state = useState({
// val: "C1",
// });
// constructor(parent, props) {
// super(parent, props);
// child = this;
// }
// }
// class Parent extends Component {
// static components = { Child };
// static template = xml`<div><t t-esc="state.val"/><Child/></div>`;
// state = useState({ val: "P1" });
// }
// const parent = new Parent();
// await parent.mount(fixture);
// expect(fixture.innerHTML).toBe("<div>P1<span>C1</span></div>");
// parent.unmount();
// expect(fixture.innerHTML).toBe("");
// parent.state.val = "P2";
// child.state.val = "C2";
// await parent.mount(fixture);
// expect(fixture.innerHTML).toBe("<div>P2<span>C2</span></div>");
});
})
+1 -2
View File
@@ -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("");
});
+159
View File
@@ -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`<div class="app">text</div>`;
// static style = css`
// .app {
// color: red;
// }
// `;
// }
// expect(document.head.innerHTML).toBe("");
// const app = new App();
// expect(document.head.innerHTML).toBe(`<style component=\"App\">.app {
// color: red;
// }</style>`);
// await app.mount(fixture);
// const style = getComputedStyle(app.el!);
// expect(style.color).toBe("red");
// expect(fixture.innerHTML).toBe('<div class="app">text</div>');
});
test("inherited components properly apply css", async () => {
// class App extends Component {
// static template = xml`<div class="app">text</div>`;
// 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(`<style component=\"SubApp\">.app {
// font-weight: bold;
// }</style><style component=\"App\">.app {
// color: red;
// }</style>`);
// await app.mount(fixture);
// const style = getComputedStyle(app.el!);
// expect(style.color).toBe("red");
// expect(style.fontWeight).toBe("bold");
// expect(fixture.innerHTML).toBe('<div class="app">text</div>');
});
test("get a meaningful error message if css helper is missing", async () => {
// class App extends Component {
// static template = xml`<div class="app">text</div>`;
// 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`<div class="app">text</div>`;
// 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;
// }`);
});
});
+365
View File
@@ -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`
<div>
<input t-model="state.text"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test("basic use, on an input with bracket expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state['text']"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test("throws if invalid expression", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model="state"/>
</div>`;
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`<div>
<input t-model="some.text"/>
<span><t t-esc="some.text"/></span>
</div>`;
some = useState({ text: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.some.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test("on an input, type=checkbox", async () => {
class SomeComponent extends Component {
static template = xml`<div>
<input type="checkbox" t-model="state.flag"/>
<span>
<t t-if="state.flag">yes</t>
<t t-else="">no</t>
</span>
</div>`;
state = useState({ flag: false });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe('<div><input type="checkbox"><span>no</span></div>');
let input = fixture.querySelector("input")!;
input.click();
await nextTick();
expect(fixture.innerHTML).toBe('<div><input type="checkbox"><span>yes</span></div>');
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`<div">
<textarea t-model="state.text"/>
<span><t t-esc="state.text"/></span>
</div>`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><textarea></textarea><span></span></div>");
const textarea = fixture.querySelector("textarea")!;
await editInput(textarea, "test");
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><textarea></textarea><span>test</span></div>");
});
test("on an input type=radio", async () => {
class SomeComponent extends Component {
static template = xml`<div>
<input type="radio" id="one" value="One" t-model="state.choice"/>
<input type="radio" id="two" value="Two" t-model="state.choice"/>
<span>Choice: <t t-esc="state.choice"/></span>
</div>`;
state = useState({ choice: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: </span></div>'
);
const firstInput = fixture.querySelector("input")!;
firstInput.click();
await nextTick();
expect(comp.state.choice).toBe("One");
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: One</span></div>'
);
const secondInput = fixture.querySelectorAll("input")[1];
secondInput.click();
await nextTick();
expect(comp.state.choice).toBe("Two");
expect(fixture.innerHTML).toBe(
'<div><input type="radio" id="one" value="One"><input type="radio" id="two" value="Two"><span>Choice: Two</span></div>'
);
});
test("on a select", async () => {
class SomeComponent extends Component {
static template = xml`<div>
<select t-model="state.color">
<option value="">Please select one</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
<span>Choice: <t t-esc="state.color"/></span>
</div>`;
state = useState({ color: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(
'<div><select><option value="">Please select one</option><option value="red">Red</option><option value="blue">Blue</option></select><span>Choice: </span></div>'
);
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(
'<div><select><option value="">Please select one</option><option value="red">Red</option><option value="blue">Blue</option></select><span>Choice: red</span></div>'
);
});
test("on a select, initial state", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<select t-model="state.color">
<option value="">Please select one</option>
<option value="red">Red</option>
<option value="blue">Blue</option>
</select>
</div>
`;
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`
<div>
<input t-model="state.something.text"/>
<span><t t-esc="state.something.text"/></span>
</div>
`;
state = useState({ something: { text: "" } });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "test");
expect(comp.state.something.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test(".lazy modifier", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model.lazy="state.text"/>
<span><t t-esc="state.text"/></span>
</div>
`;
state = useState({ text: "" });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
const input = fixture.querySelector("input")!;
input.value = "test";
input.dispatchEvent(new Event("input"));
await nextTick();
expect(comp.state.text).toBe("");
expect(fixture.innerHTML).toBe("<div><input><span></span></div>");
input.dispatchEvent(new Event("change"));
await nextTick();
expect(comp.state.text).toBe("test");
expect(fixture.innerHTML).toBe("<div><input><span>test</span></div>");
});
test(".trim modifier", async () => {
class SomeComponent extends Component {
static template = xml`
<div t-name="SomeComponent">
<input t-model.trim="state.text"/>
<span><t t-esc="state.text"/></span>
</div>
`;
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("<div><input><span>test</span></div>");
});
test(".number modifier", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<input t-model.number="state.number"/>
<span><t t-esc="state.number"/></span>
</div>
`;
state = useState({ number: 0 });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe("<div><input><span>0</span></div>");
const input = fixture.querySelector("input")!;
await editInput(input, "13");
expect(comp.state.number).toBe(13);
expect(fixture.innerHTML).toBe("<div><input><span>13</span></div>");
await editInput(input, "invalid");
expect(comp.state.number).toBe("invalid");
expect(fixture.innerHTML).toBe("<div><input><span>invalid</span></div>");
});
test("in a t-foreach", async () => {
class SomeComponent extends Component {
static template = xml`
<div>
<t t-foreach="state" t-as="thing" t-key="thing.id">
<input type="checkbox" t-model="thing.f"/>
</t>
</div>
`;
state = useState([
{ f: false, id: 1 },
{ f: false, id: 2 },
{ f: false, id: 3 },
]);
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe(
'<div><input type="checkbox"><input type="checkbox"><input type="checkbox"></div>'
);
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`
<div>
<t t-foreach="state" t-as="thing" t-key="thing_index" >
<input t-model="state[thing_index]"/>
</t>
</div>
`;
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`
<div t-key="'key'">
<input class="a" t-if="state.flag"/>
<input class="b" t-if="!state.flag"/>
</div>
`;
state = useState({ flag: true });
}
const comp = await mount(SomeComponent, fixture);
expect(fixture.innerHTML).toBe('<div><input class="a"></div>');
fixture.querySelector("input")!.value = "asdf";
expect(fixture.querySelector("input")!.value).toBe("asdf");
comp.state.flag = false;
await nextTick();
expect(fixture.innerHTML).toBe('<div><input class="b"></div>');
expect(fixture.querySelector("input")!.value).toBe("");
});
});
+7
View File
@@ -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();
}
@@ -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(\`<span><block-child-0/></span>\`);
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
) {
+43
View File
@@ -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", `<textarea t-att-value="v"/>`);
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 = `
<select t-att-value="value">
<option value="potato">Potato</option>
<option value="tomato">Tomato</option>
<option value="onion">Onion</option>
</select>`;
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();*/
});
});
+30
View File
@@ -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 = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<div t-name="hey">jupiler</div>
</templates>`;
const qweb = new QWeb({ templates });
expect(renderToString(qweb, "hey")).toBe("<div>jupiler</div>");*/
});
test.skip("can load a few templates from a xml string", () => {
/*const data = `<?xml version="1.0" encoding="UTF-8"?>
<templates id="template" xml:space="preserve">
<t t-name="items"><li>ok</li><li>foo</li></t>
<ul t-name="main"><t t-call="items"/></ul>
</templates>`;
qweb.addTemplates(data);
const result = renderToString(qweb, "main");
expect(result).toBe("<ul><li>ok</li><li>foo</li></ul>");*/
});
test.skip("does not crash if string does not have templates", () => {
/*const data = "";
qweb.addTemplates(data);
expect(Object.keys(qweb.templates)).toEqual([]);*/
});
});
+5
View File
@@ -16,6 +16,11 @@ describe("t-if", () => {
expect(renderToString(template, {})).toBe("<div></div>");
});
test("boolean value condition missing", () => {
const template = `<span><t t-if="condition">fail</t></span>`;
expect(renderToString(template)).toBe("<span></span>");
});
test("just a t-if", () => {
const template = `<t t-if="condition">ok</t>`;
expect(renderToString(template, { condition: true })).toBe("ok");